Skip to content

Commit b739ef7

Browse files
authored
Merge pull request #507 from OpenPecha/fix-day-task-complete-share-button
feat: implement sharing functionality for plan day images
2 parents 34700f4 + 029f62b commit b739ef7

4 files changed

Lines changed: 483 additions & 103 deletions

File tree

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import 'dart:io';
2+
3+
import 'package:flutter/material.dart';
4+
import 'package:flutter_pecha/core/deep_linking/deep_link_url_builder.dart';
5+
import 'package:flutter_pecha/core/extensions/context_ext.dart';
6+
import 'package:flutter_pecha/shared/utils/helper_functions.dart';
7+
import 'package:http/http.dart' as http;
8+
import 'package:path_provider/path_provider.dart';
9+
import 'package:share_plus/share_plus.dart';
10+
11+
/// Downloads the plan day's shareable image and shares it together with the
12+
/// day completion message and the plan day deep link.
13+
Future<void> sharePlanDayImage({
14+
required BuildContext context,
15+
required String shareableImageUrl,
16+
required int dayNumber,
17+
required String planId,
18+
required String planLanguage,
19+
GlobalKey? shareButtonKey,
20+
}) async {
21+
final url = shareableImageUrl.trim();
22+
if (url.isEmpty) return;
23+
24+
File? tempFile;
25+
try {
26+
final uri = Uri.parse(url);
27+
final response = await http.get(uri).timeout(const Duration(seconds: 15));
28+
if (response.statusCode < 200 || response.statusCode >= 300) {
29+
throw HttpException(
30+
'Failed to download share image (${response.statusCode})',
31+
uri: uri,
32+
);
33+
}
34+
35+
final directory = await getTemporaryDirectory();
36+
final extension = _imageExtensionFromUrl(uri);
37+
tempFile = File(
38+
'${directory.path}/plan_day_${dayNumber}_${DateTime.now().millisecondsSinceEpoch}.$extension',
39+
);
40+
await tempFile.writeAsBytes(response.bodyBytes);
41+
42+
if (!context.mounted) return;
43+
44+
final sharePositionOrigin = getSharePositionOrigin(
45+
context: context,
46+
globalKey: shareButtonKey,
47+
);
48+
49+
final shareMessage = context.l10n.day_completion_share_message;
50+
final planLink =
51+
DeepLinkUrlBuilder.planDayLink(
52+
planId: planId,
53+
dayNumber: dayNumber,
54+
language: planLanguage,
55+
).toString();
56+
57+
await SharePlus.instance.share(
58+
ShareParams(
59+
files: [XFile(tempFile.path)],
60+
text: '$shareMessage\n\n$planLink',
61+
sharePositionOrigin: sharePositionOrigin,
62+
),
63+
);
64+
} catch (_) {
65+
if (context.mounted) {
66+
ScaffoldMessenger.of(context).showSnackBar(
67+
SnackBar(
68+
content: Text(context.l10n.create_image_share_error),
69+
behavior: SnackBarBehavior.floating,
70+
),
71+
);
72+
}
73+
} finally {
74+
if (tempFile != null && await tempFile.exists()) {
75+
try {
76+
await tempFile.delete();
77+
} catch (_) {
78+
// Best-effort temp cleanup.
79+
}
80+
}
81+
}
82+
}
83+
84+
String _imageExtensionFromUrl(Uri uri) {
85+
final lastSegment = uri.pathSegments.isNotEmpty ? uri.pathSegments.last : '';
86+
final extension =
87+
lastSegment.contains('.')
88+
? lastSegment.split('.').last.toLowerCase()
89+
: '';
90+
switch (extension) {
91+
case 'jpg':
92+
case 'jpeg':
93+
case 'png':
94+
case 'webp':
95+
return extension;
96+
default:
97+
return 'png';
98+
}
99+
}

lib/features/plans/presentation/widgets/day_completion_bottom_sheet.dart

Lines changed: 6 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,8 @@
1-
import 'dart:io';
2-
31
import 'package:flutter/material.dart';
42
import 'package:flutter_pecha/core/constants/app_assets.dart';
5-
import 'package:flutter_pecha/core/deep_linking/deep_link_url_builder.dart';
63
import 'package:flutter_pecha/core/extensions/context_ext.dart';
74
import 'package:flutter_pecha/core/widgets/cached_network_image_widget.dart';
8-
import 'package:flutter_pecha/shared/utils/helper_functions.dart';
9-
import 'package:http/http.dart' as http;
10-
import 'package:path_provider/path_provider.dart';
11-
import 'package:share_plus/share_plus.dart';
5+
import 'package:flutter_pecha/features/plans/presentation/utils/plan_day_share.dart';
126

137
class DayCompletionBottomSheet extends StatefulWidget {
148
final int dayNumber;
@@ -211,87 +205,22 @@ class _DayCompletionBottomSheetState extends State<DayCompletionBottomSheet> {
211205

212206
setState(() => _isSharing = true);
213207

214-
File? tempFile;
215208
try {
216-
final uri = Uri.parse(url);
217-
final response = await http.get(uri);
218-
if (response.statusCode < 200 || response.statusCode >= 300) {
219-
throw HttpException(
220-
'Failed to download share image (${response.statusCode})',
221-
uri: uri,
222-
);
223-
}
224-
225-
final directory = await getTemporaryDirectory();
226-
final extension = _imageExtensionFromUrl(uri);
227-
tempFile = File(
228-
'${directory.path}/plan_day_${widget.dayNumber}_${DateTime.now().millisecondsSinceEpoch}.$extension',
229-
);
230-
await tempFile.writeAsBytes(response.bodyBytes);
231-
232-
if (!mounted) return;
233-
234-
final sharePositionOrigin = getSharePositionOrigin(
209+
await sharePlanDayImage(
235210
context: context,
236-
globalKey: _shareButtonKey,
237-
);
238-
239-
final shareMessage = context.l10n.day_completion_share_message;
240-
final planLink = DeepLinkUrlBuilder.planDayLink(
241-
planId: widget.planId,
211+
shareableImageUrl: url,
242212
dayNumber: widget.dayNumber,
243-
language: widget.planLanguage,
244-
).toString();
245-
246-
await SharePlus.instance.share(
247-
ShareParams(
248-
files: [XFile(tempFile.path)],
249-
text: '$shareMessage\n\n$planLink',
250-
sharePositionOrigin: sharePositionOrigin,
251-
),
213+
planId: widget.planId,
214+
planLanguage: widget.planLanguage,
215+
shareButtonKey: _shareButtonKey,
252216
);
253-
} catch (_) {
254-
if (mounted) {
255-
ScaffoldMessenger.of(context).showSnackBar(
256-
SnackBar(
257-
content: Text(context.l10n.create_image_share_error),
258-
behavior: SnackBarBehavior.floating,
259-
),
260-
);
261-
}
262217
} finally {
263-
if (tempFile != null && await tempFile.exists()) {
264-
try {
265-
await tempFile.delete();
266-
} catch (_) {
267-
// Best-effort temp cleanup.
268-
}
269-
}
270-
271218
if (mounted) {
272219
setState(() => _isSharing = false);
273220
}
274221
}
275222
}
276223

277-
String _imageExtensionFromUrl(Uri uri) {
278-
final lastSegment =
279-
uri.pathSegments.isNotEmpty ? uri.pathSegments.last : '';
280-
final extension =
281-
lastSegment.contains('.')
282-
? lastSegment.split('.').last.toLowerCase()
283-
: '';
284-
switch (extension) {
285-
case 'jpg':
286-
case 'jpeg':
287-
case 'png':
288-
case 'webp':
289-
return extension;
290-
default:
291-
return 'png';
292-
}
293-
}
294-
295224
Widget _buildProgressBar(BuildContext context, double progress) {
296225
final barWidth = MediaQuery.of(context).size.width - 80;
297226

lib/features/plans/presentation/widgets/plan_track/plan_details.dart

Lines changed: 100 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
66
import 'package:flutter_pecha/core/analytics/analytics_events.dart';
77
import 'package:flutter_pecha/core/analytics/analytics_providers.dart';
88
import 'package:flutter_pecha/core/config/locale/locale_notifier.dart';
9+
import 'package:flutter_pecha/core/config/router/app_routes.dart';
910
import 'package:flutter_pecha/core/error/failures.dart';
1011
import 'package:flutter_pecha/core/constants/app_assets.dart';
1112
import 'package:flutter_pecha/core/l10n/generated/app_localizations.dart';
@@ -23,6 +24,7 @@ import 'package:flutter_pecha/features/plans/data/models/user/user_plans_model.d
2324
import 'package:flutter_pecha/features/plans/data/utils/series_plan_utils.dart';
2425
import 'package:flutter_pecha/features/plans/data/models/user/user_tasks_dto.dart';
2526
import 'package:flutter_pecha/features/plans/domain/subtask_navigation.dart';
27+
import 'package:flutter_pecha/features/plans/presentation/utils/plan_day_share.dart';
2628
import 'package:flutter_pecha/features/plans/presentation/widgets/plan_navigation/plan_navigator.dart';
2729
import 'package:flutter_pecha/core/extensions/context_ext.dart';
2830
import 'package:flutter_pecha/features/plans/data/models/response/user_plan_day_detail_response.dart';
@@ -60,6 +62,8 @@ class _PlanDetailsState extends ConsumerState<PlanDetails> {
6062
final Set<String> _togglingTaskIds = {};
6163
final Map<int, bool> _dayCompletionTracker = {};
6264
final Map<String, bool> _optimisticCompletions = {};
65+
final GlobalKey _shareButtonKey = GlobalKey();
66+
bool _isSharing = false;
6367

6468
@override
6569
void initState() {
@@ -215,7 +219,14 @@ class _PlanDetailsState extends ConsumerState<PlanDetails> {
215219
return AppBar(
216220
leading: IconButton(
217221
icon: const Icon(AppAssets.arrowLeft),
218-
onPressed: () => context.pop(),
222+
onPressed: () {
223+
if (context.canPop()) {
224+
context.pop();
225+
} else {
226+
// Opened via deep link with no route beneath — go home.
227+
context.go(AppRoutes.home);
228+
}
229+
},
219230
),
220231
title: Text(widget.plan.title, style: TextStyle(fontSize: 20)),
221232
elevation: 0,
@@ -711,38 +722,101 @@ class _PlanDetailsState extends ConsumerState<PlanDetails> {
711722
tasks.isNotEmpty &&
712723
tasks.any(PlanSubtaskNavigation.isUserTaskNavigable);
713724

725+
final shareableImageUrl = dayData?.shareableImageUrl?.trim();
726+
final showShareButton =
727+
dayData != null &&
728+
dayData.isCompleted &&
729+
shareableImageUrl != null &&
730+
shareableImageUrl.isNotEmpty;
731+
732+
final buttonStyle = FilledButton.styleFrom(
733+
backgroundColor: Theme.of(context).colorScheme.onSurface,
734+
foregroundColor: Theme.of(context).colorScheme.surface,
735+
disabledBackgroundColor: Theme.of(
736+
context,
737+
).colorScheme.onSurface.withValues(alpha: 0.5),
738+
disabledForegroundColor: Theme.of(
739+
context,
740+
).colorScheme.surface.withValues(alpha: 0.85),
741+
padding: const EdgeInsets.symmetric(vertical: 16),
742+
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
743+
);
744+
714745
return SafeArea(
715746
child: Padding(
716747
padding: const EdgeInsets.all(16.0),
717748
child: SizedBox(
718749
width: double.infinity,
719-
child: FilledButton(
720-
onPressed:
721-
hasReadableContent
722-
? () => _startReading(tasks, audioUrl: audioUrl)
723-
: null,
724-
style: FilledButton.styleFrom(
725-
backgroundColor: Theme.of(context).colorScheme.onSurface,
726-
foregroundColor: Theme.of(context).colorScheme.surface,
727-
disabledBackgroundColor: Theme.of(
728-
context,
729-
).colorScheme.onSurface.withValues(alpha: 0.5),
730-
padding: const EdgeInsets.symmetric(vertical: 16),
731-
shape: RoundedRectangleBorder(
732-
borderRadius: BorderRadius.circular(20),
733-
),
734-
),
735-
child: Text(
736-
localizations.start_reading,
737-
style: const TextStyle(
738-
fontSize: 16,
739-
fontWeight: FontWeight.bold,
740-
letterSpacing: -0.3,
741-
),
742-
),
743-
),
750+
child:
751+
showShareButton
752+
? FilledButton.icon(
753+
key: _shareButtonKey,
754+
onPressed:
755+
_isSharing
756+
? null
757+
: () =>
758+
_shareDay(shareableImageUrl, dayData.dayNumber),
759+
style: buttonStyle,
760+
icon:
761+
_isSharing
762+
? SizedBox(
763+
width: 18,
764+
height: 18,
765+
child: CircularProgressIndicator(
766+
strokeWidth: 2,
767+
color: Theme.of(
768+
context,
769+
).colorScheme.surface.withValues(alpha: 0.85),
770+
),
771+
)
772+
: const Icon(AppAssets.readerShare, size: 22),
773+
label: Text(
774+
localizations.share,
775+
style: const TextStyle(
776+
fontSize: 16,
777+
fontWeight: FontWeight.bold,
778+
letterSpacing: -0.3,
779+
),
780+
),
781+
)
782+
: FilledButton(
783+
onPressed:
784+
hasReadableContent
785+
? () => _startReading(tasks, audioUrl: audioUrl)
786+
: null,
787+
style: buttonStyle,
788+
child: Text(
789+
localizations.start_reading,
790+
style: const TextStyle(
791+
fontSize: 16,
792+
fontWeight: FontWeight.bold,
793+
letterSpacing: -0.3,
794+
),
795+
),
796+
),
744797
),
745798
),
746799
);
747800
}
801+
802+
Future<void> _shareDay(String shareableImageUrl, int dayNumber) async {
803+
if (_isSharing) return;
804+
805+
setState(() => _isSharing = true);
806+
807+
try {
808+
await sharePlanDayImage(
809+
context: context,
810+
shareableImageUrl: shareableImageUrl,
811+
dayNumber: dayNumber,
812+
planId: widget.plan.id,
813+
planLanguage: widget.plan.language,
814+
shareButtonKey: _shareButtonKey,
815+
);
816+
} finally {
817+
if (mounted) {
818+
setState(() => _isSharing = false);
819+
}
820+
}
821+
}
748822
}

0 commit comments

Comments
 (0)