Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 47 additions & 14 deletions src/mobile/lib/src/app/dashboard/pages/forecast_overview_page.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:airqo/src/app/dashboard/widgets/air_quality_share_sheet.dart';
import 'package:airqo/src/app/dashboard/bloc/forecast/forecast_bloc.dart';
import 'package:airqo/src/app/dashboard/models/airquality_response.dart';
import 'package:airqo/src/app/dashboard/models/forecast_guidance.dart';
Expand Down Expand Up @@ -86,6 +87,7 @@ class _ForecastOverviewPageState extends State<ForecastOverviewPage> {
ForecastTimeScope _timeScope = ForecastTimeScope.daily;
ScrollController? _scrollController;
final _todayStr = DateFormat('yyyy-MM-dd').format(DateTime.now());
final GlobalKey _shareButtonKey = GlobalKey();

@override
void initState() {
Expand Down Expand Up @@ -150,6 +152,23 @@ class _ForecastOverviewPageState extends State<ForecastOverviewPage> {
}
}

Future<void> _openShareSheet() async {
final measurement = widget.measurement;
if (measurement == null) return;

final renderObject =
_shareButtonKey.currentContext?.findRenderObject() as RenderBox?;
final shareOrigin = renderObject == null
? null
: renderObject.localToGlobal(Offset.zero) & renderObject.size;

await showAirQualityShareSheet(
context,
measurement: measurement,
sharePositionOrigin: shareOrigin,
);
}

@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
Expand Down Expand Up @@ -216,14 +235,32 @@ class _ForecastOverviewPageState extends State<ForecastOverviewPage> {
],
),
),
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: Icon(
Icons.close,
color: AppTextColors.modalCloseIcon(context),
if (widget.measurement != null)
IconButton(
key: _shareButtonKey,
onPressed: _openShareSheet,
tooltip: 'Share',
icon: SvgPicture.asset(
'assets/icons/share-icon.svg',
width: 20,
height: 20,
semanticsLabel: 'Share',
colorFilter: ColorFilter.mode(
AppTextColors.modalCloseIcon(context),
BlendMode.srcIn,
),
),
visualDensity: VisualDensity.compact,
)
Comment thread
Copilot marked this conversation as resolved.
else
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: Icon(
Icons.close,
color: AppTextColors.modalCloseIcon(context),
),
visualDensity: VisualDensity.compact,
),
visualDensity: VisualDensity.compact,
),
],
),
),
Expand Down Expand Up @@ -322,15 +359,13 @@ class _ForecastOverviewPageState extends State<ForecastOverviewPage> {
final idx = _selectedDayIndex.clamp(0, forecasts.length - 1);
final day = forecasts[idx];
final selectedDateLabel = DateFormat('EEEE, MMMM d').format(day.time);
final hourEntries =
hourlyEntriesForDate(state.hourlyResponse, day.time);
final hourEntries = hourlyEntriesForDate(state.hourlyResponse, day.time);
_syncHourIndex(hourEntries, day.time);

final hourIdx = hourEntries.isEmpty
? 0
: _selectedHourIndex.clamp(0, hourEntries.length - 1);
final selectedHour =
hourEntries.isNotEmpty ? hourEntries[hourIdx] : null;
final selectedHour = hourEntries.isNotEmpty ? hourEntries[hourIdx] : null;

return SingleChildScrollView(
controller: scrollController,
Expand Down Expand Up @@ -447,9 +482,7 @@ class _ForecastOverviewPageState extends State<ForecastOverviewPage> {
mainAxisSize: MainAxisSize.min,
children: [
Icon(
isNetwork
? Icons.wifi_off_rounded
: Icons.error_outline_rounded,
isNetwork ? Icons.wifi_off_rounded : Icons.error_outline_rounded,
size: 56,
color: AppTextColors.muted(context),
),
Expand Down
185 changes: 185 additions & 0 deletions src/mobile/lib/src/app/dashboard/widgets/air_quality_share_sheet.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import 'dart:typed_data';
import 'dart:ui' as ui;

import 'package:airqo/src/app/dashboard/models/airquality_response.dart';
import 'package:airqo/src/app/dashboard/widgets/air_quality_share_card.dart';
import 'package:airqo/src/app/shared/services/air_quality_share_service.dart';
import 'package:airqo/src/meta/utils/colors.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

Future<void> showAirQualityShareSheet(
BuildContext context, {
required Measurement measurement,
String? fallbackLocationName,
Rect? sharePositionOrigin,
}) {
return showModalBottomSheet<void>(
context: context,
useRootNavigator: true,
isScrollControlled: true,
showDragHandle: true,
backgroundColor: Colors.transparent,
builder: (_) => AirQualityShareSheet(
measurement: measurement,
fallbackLocationName: fallbackLocationName,
sharePositionOrigin: sharePositionOrigin,
),
);
}

class AirQualityShareSheet extends StatefulWidget {
final Measurement measurement;
final String? fallbackLocationName;
final Rect? sharePositionOrigin;

const AirQualityShareSheet({
super.key,
required this.measurement,
this.fallbackLocationName,
this.sharePositionOrigin,
});

@override
State<AirQualityShareSheet> createState() => _AirQualityShareSheetState();
}

class _AirQualityShareSheetState extends State<AirQualityShareSheet> {
final GlobalKey _cardKey = GlobalKey();
bool _isSharingCard = false;

Future<void> _shareCard() async {
if (_isSharingCard) return;

setState(() {
_isSharingCard = true;
});

try {
final imageBytes = await _captureCard();
if (imageBytes == null) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content:
Text('Could not prepare the share card. Please try again.'),
),
);
return;
}

await AirQualityShareService.shareMeasurementCard(
imageBytes,
widget.measurement,
fallbackLocationName: widget.fallbackLocationName,
sharePositionOrigin: widget.sharePositionOrigin,
);
} catch (_) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Could not share the card. Please try again.'),
),
);
} finally {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if (mounted) {
setState(() {
_isSharingCard = false;
});
}
}
}

Future<Uint8List?> _captureCard() async {
final pixelRatio =
MediaQuery.of(context).devicePixelRatio.clamp(2.0, 3.0).toDouble();

await Future<void>.delayed(const Duration(milliseconds: 16));

final boundary =
_cardKey.currentContext?.findRenderObject() as RenderRepaintBoundary?;
if (boundary == null) return null;

final image = await boundary.toImage(pixelRatio: pixelRatio);
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);

return byteData?.buffer.asUint8List();
}

@override
Widget build(BuildContext context) {
final textColor = Theme.of(context).textTheme.headlineSmall?.color;
final subtitleColor =
Theme.of(context).textTheme.bodyMedium?.color?.withValues(alpha: 0.72);

return SafeArea(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
child: DecoratedBox(
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(28),
),
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Share card',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w700,
color: textColor,
),
),
const SizedBox(height: 6),
Text(
'Preview the card before sending it.',
style: TextStyle(
fontSize: 14,
color: subtitleColor,
),
),
const SizedBox(height: 16),
RepaintBoundary(
key: _cardKey,
child: AirQualityShareCard(
measurement: widget.measurement,
fallbackLocationName: widget.fallbackLocationName,
),
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: _isSharingCard ? null : _shareCard,
icon: _isSharingCard
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.ios_share_outlined),
label: Text(
_isSharingCard ? 'Preparing card...' : 'Share card',
),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryColor,
foregroundColor: Colors.white,
minimumSize: const Size.fromHeight(52),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(18),
),
),
),
),
],
),
),
),
),
);
}
}
Loading
Loading