1
1
//
2
2
// ignore_for_file: avoid_redundant_argument_values
3
3
4
+ import 'package:flutter/foundation.dart' show kIsWeb; // Import kIsWeb
4
5
import 'package:flutter/material.dart' ;
5
6
import 'package:flutter_bloc/flutter_bloc.dart' ;
6
7
import 'package:ht_main/account/bloc/account_bloc.dart' ; // Import AccountBloc
7
- import 'package:ht_main/headline-details/bloc/headline_details_bloc.dart' ;
8
+ import 'package:ht_main/headline-details/bloc/headline_details_bloc.dart' ; // Import BLoC
8
9
import 'package:ht_main/l10n/l10n.dart' ;
9
10
import 'package:ht_main/shared/shared.dart' ;
10
11
import 'package:ht_shared/ht_shared.dart'
11
12
show Headline; // Import Headline model
12
13
import 'package:intl/intl.dart' ;
14
+ import 'package:share_plus/share_plus.dart' ; // Import share_plus
13
15
import 'package:url_launcher/url_launcher_string.dart' ;
14
16
15
- class HeadlineDetailsPage extends StatelessWidget {
16
- const HeadlineDetailsPage ({required this .headlineId, super .key});
17
+ class HeadlineDetailsPage extends StatefulWidget {
18
+ const HeadlineDetailsPage ({
19
+ super .key,
20
+ this .headlineId,
21
+ this .initialHeadline,
22
+ }) : assert (headlineId != null || initialHeadline != null );
17
23
18
- final String headlineId;
24
+ final String ? headlineId;
25
+ final Headline ? initialHeadline;
26
+
27
+ @override
28
+ State <HeadlineDetailsPage > createState () => _HeadlineDetailsPageState ();
29
+ }
30
+
31
+ class _HeadlineDetailsPageState extends State <HeadlineDetailsPage > {
32
+ @override
33
+ void initState () {
34
+ super .initState ();
35
+ if (widget.initialHeadline != null ) {
36
+ context
37
+ .read <HeadlineDetailsBloc >()
38
+ .add (HeadlineProvided (widget.initialHeadline! ));
39
+ } else if (widget.headlineId != null ) {
40
+ context
41
+ .read <HeadlineDetailsBloc >()
42
+ .add (FetchHeadlineById (widget.headlineId! ));
43
+ }
44
+ }
19
45
20
46
@override
21
47
Widget build (BuildContext context) {
22
48
final l10n = context.l10n;
23
- // Keep a reference to headlineDetailsState to use in BlocListener
24
- final headlineDetailsState = context.watch <HeadlineDetailsBloc >().state;
25
49
26
50
return SafeArea (
27
51
child: Scaffold (
28
- // Body contains the BlocBuilder which returns either state widgets
29
- // or the scroll view
30
52
body: BlocListener <AccountBloc , AccountState >(
31
53
listenWhen: (previous, current) {
32
- // Listen if status is failure or if the saved status of *this* headline changed
33
- if (current.status == AccountStatus .failure &&
34
- previous .status != AccountStatus .failure) {
35
- return true ;
36
- }
37
- if (headlineDetailsState is HeadlineDetailsLoaded ) {
38
- final currentHeadlineId = headlineDetailsState .headline.id;
54
+ final detailsState = context. read < HeadlineDetailsBloc >().state;
55
+ if (detailsState is HeadlineDetailsLoaded ) {
56
+ if (current .status == AccountStatus .failure &&
57
+ previous.status != AccountStatus .failure) {
58
+ return true ;
59
+ }
60
+ final currentHeadlineId = detailsState .headline.id;
39
61
final wasPreviouslySaved =
40
62
previous.preferences? .savedHeadlines.any (
41
63
(h) => h.id == currentHeadlineId,
@@ -46,20 +68,18 @@ class HeadlineDetailsPage extends StatelessWidget {
46
68
(h) => h.id == currentHeadlineId,
47
69
) ??
48
70
false ;
49
- // Listen if the specific headline's saved status changed OR
50
- // if a general success occurred (e.g. after an optimistic update that might not change the list length but confirms persistence)
51
71
return (wasPreviouslySaved != isCurrentlySaved) ||
52
72
(current.status == AccountStatus .success &&
53
73
previous.status != AccountStatus .success);
54
74
}
55
75
return false ;
56
76
},
57
77
listener: (context, accountState) {
58
- if (headlineDetailsState is HeadlineDetailsLoaded ) {
59
- final currentHeadline = headlineDetailsState.headline;
78
+ final detailsState = context. read < HeadlineDetailsBloc >().state;
79
+ if (detailsState is HeadlineDetailsLoaded ) {
60
80
final nowIsSaved =
61
81
accountState.preferences? .savedHeadlines.any (
62
- (h) => h.id == currentHeadline .id,
82
+ (h) => h.id == detailsState.headline .id,
63
83
) ??
64
84
false ;
65
85
@@ -72,12 +92,11 @@ class HeadlineDetailsPage extends StatelessWidget {
72
92
content: Text (
73
93
accountState.errorMessage ??
74
94
l10n.headlineSaveErrorSnackbar,
75
- ), // Use specific or generic error
95
+ ),
76
96
backgroundColor: Theme .of (context).colorScheme.error,
77
97
),
78
98
);
79
99
} else {
80
- // Only show success snackbar if the state isn't failure
81
100
ScaffoldMessenger .of (context)
82
101
..hideCurrentSnackBar ()
83
102
..showSnackBar (
@@ -94,36 +113,32 @@ class HeadlineDetailsPage extends StatelessWidget {
94
113
}
95
114
},
96
115
child: BlocBuilder <HeadlineDetailsBloc , HeadlineDetailsState >(
97
- // No need to re-watch headlineDetailsState here, already have it.
98
- // builder: (context, state) // state here is headlineDetailsState
99
- builder: (context, headlineDetailsBuilderState) {
100
- // Handle Loading/Initial/Failure states outside the scroll view
101
- // for better user experience.
102
- // Use headlineDetailsBuilderState for the switch
103
- return switch (headlineDetailsBuilderState) {
104
- HeadlineDetailsInitial _ => InitialStateWidget (
105
- icon: Icons .article,
106
- headline: l10n.headlineDetailsInitialHeadline,
107
- subheadline: l10n.headlineDetailsInitialSubheadline,
108
- ),
109
- HeadlineDetailsLoading _ => LoadingStateWidget (
110
- icon: Icons .downloading,
111
- headline: l10n.headlineDetailsLoadingHeadline,
112
- subheadline: l10n.headlineDetailsLoadingSubheadline,
113
- ),
114
- final HeadlineDetailsFailure state => FailureStateWidget (
115
- message: state.message,
116
- onRetry: () {
117
- context.read <HeadlineDetailsBloc >().add (
118
- HeadlineDetailsRequested (headlineId: headlineId),
119
- );
120
- },
121
- ),
122
- final HeadlineDetailsLoaded state => _buildLoadedContent (
123
- context,
124
- state.headline,
125
- ),
126
- _ => const SizedBox .shrink (), // Should not happen in practice
116
+ builder: (context, state) {
117
+ return switch (state) {
118
+ HeadlineDetailsInitial () ||
119
+ HeadlineDetailsLoading () =>
120
+ LoadingStateWidget (
121
+ icon: Icons .downloading,
122
+ headline: l10n.headlineDetailsLoadingHeadline,
123
+ subheadline: l10n.headlineDetailsLoadingSubheadline,
124
+ ),
125
+ final HeadlineDetailsFailure failureState => FailureStateWidget (
126
+ message: failureState.message,
127
+ onRetry: () {
128
+ if (widget.headlineId != null ) {
129
+ context
130
+ .read <HeadlineDetailsBloc >()
131
+ .add (FetchHeadlineById (widget.headlineId! ));
132
+ }
133
+ // If only initialHeadline was provided and it failed to load
134
+ // (which shouldn't happen with HeadlineProvided),
135
+ // there's no ID to refetch. Consider a different UI.
136
+ },
137
+ ),
138
+ final HeadlineDetailsLoaded loadedState =>
139
+ _buildLoadedContent (context, loadedState.headline),
140
+ // Add a default case to satisfy exhaustiveness
141
+ _ => const Center (child: Text ('Unknown state' )),
127
142
};
128
143
},
129
144
),
@@ -166,10 +181,69 @@ class HeadlineDetailsPage extends StatelessWidget {
166
181
},
167
182
);
168
183
169
- final shareButton = IconButton (
170
- icon: const Icon (Icons .share),
171
- onPressed: () {
172
- // TODO(fulleni): Implement share functionality
184
+ // Use a Builder to get the correct context for sharePositionOrigin
185
+ final Widget shareButtonWidget = Builder (
186
+ builder: (BuildContext buttonContext) {
187
+ return IconButton (
188
+ icon: const Icon (Icons .share),
189
+ tooltip: l10n.shareActionTooltip,
190
+ onPressed: () async {
191
+ final box = buttonContext.findRenderObject () as RenderBox ? ;
192
+ Rect ? sharePositionOrigin;
193
+ if (box != null ) {
194
+ sharePositionOrigin = box.localToGlobal (Offset .zero) & box.size;
195
+ }
196
+
197
+ String shareText = headline.title;
198
+ if (headline.url != null && headline.url! .isNotEmpty) {
199
+ shareText += '\n\n ${headline .url }' ;
200
+ }
201
+
202
+ ShareParams params;
203
+ if (kIsWeb && headline.url != null && headline.url! .isNotEmpty) {
204
+ // For web, prioritize sharing the URL directly as a URI.
205
+ // The 'title' in ShareParams might be used by some platforms or if
206
+ // the plugin's web handling evolves to use it with navigator.share's title field.
207
+ params = ShareParams (
208
+ uri: Uri .parse (headline.url! ),
209
+ title: headline.title, // Title hint for the shared content
210
+ sharePositionOrigin: sharePositionOrigin,
211
+ );
212
+ } else if (headline.url != null && headline.url! .isNotEmpty) {
213
+ // For native platforms with a URL, combine title and URL in text.
214
+ // Subject can be used by email clients.
215
+ params = ShareParams (
216
+ text: '${headline .title }\n\n ${headline .url !}' ,
217
+ subject: headline.title,
218
+ sharePositionOrigin: sharePositionOrigin,
219
+ );
220
+ } else {
221
+ // No URL, share only the title as text (works for all platforms).
222
+ params = ShareParams (
223
+ text: headline.title,
224
+ subject: headline.title, // Subject for email clients
225
+ sharePositionOrigin: sharePositionOrigin,
226
+ );
227
+ }
228
+
229
+ final shareResult = await SharePlus .instance.share (params);
230
+
231
+ // Optional: Handle ShareResult for user feedback
232
+ if (buttonContext.mounted) { // Check if context is still valid
233
+ if (shareResult.status == ShareResultStatus .unavailable) {
234
+ ScaffoldMessenger .of (buttonContext).showSnackBar (
235
+ SnackBar (
236
+ content: Text (
237
+ l10n.sharingUnavailableSnackbar, // Add this l10n key
238
+ ),
239
+ ),
240
+ );
241
+ }
242
+ // You can add more feedback for success/dismissed if desired
243
+ // e.g., print('Share result: ${shareResult.status}, raw: ${shareResult.raw}');
244
+ }
245
+ },
246
+ );
173
247
},
174
248
);
175
249
@@ -181,7 +255,7 @@ class HeadlineDetailsPage extends StatelessWidget {
181
255
icon: const Icon (Icons .arrow_back),
182
256
onPressed: () => Navigator .of (context).pop (),
183
257
),
184
- actions: [bookmarkButton, shareButton],
258
+ actions: [bookmarkButton, shareButtonWidget], // Use the new widget
185
259
// Pinned=false, floating=true, snap=true is common for news apps
186
260
pinned: false ,
187
261
floating: true , // Trailing comma
0 commit comments