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
8
+ import 'package:ht_main/headline-details/bloc/headline_details_bloc.dart' ; // Import BLoC
7
9
import 'package:ht_main/l10n/l10n.dart' ;
8
10
import 'package:ht_main/shared/shared.dart' ;
9
11
import 'package:ht_shared/ht_shared.dart'
@@ -12,76 +14,134 @@ import 'package:intl/intl.dart';
12
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 .headline, 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 Headline headline;
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
- // Headline is now a direct member: this.headline
24
- // No longer need to watch HeadlineDetailsBloc or its state here.
25
49
26
50
return SafeArea (
27
51
child: Scaffold (
28
52
body: BlocListener <AccountBloc , AccountState >(
29
53
listenWhen: (previous, current) {
30
- if (current.status == AccountStatus .failure &&
31
- previous.status != AccountStatus .failure) {
32
- return true ;
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;
61
+ final wasPreviouslySaved =
62
+ previous.preferences? .savedHeadlines.any (
63
+ (h) => h.id == currentHeadlineId,
64
+ ) ??
65
+ false ;
66
+ final isCurrentlySaved =
67
+ current.preferences? .savedHeadlines.any (
68
+ (h) => h.id == currentHeadlineId,
69
+ ) ??
70
+ false ;
71
+ return (wasPreviouslySaved != isCurrentlySaved) ||
72
+ (current.status == AccountStatus .success &&
73
+ previous.status != AccountStatus .success);
33
74
}
34
- final currentHeadlineId = headline.id;
35
- final wasPreviouslySaved =
36
- previous.preferences? .savedHeadlines.any (
37
- (h) => h.id == currentHeadlineId,
38
- ) ??
39
- false ;
40
- final isCurrentlySaved =
41
- current.preferences? .savedHeadlines.any (
42
- (h) => h.id == currentHeadlineId,
43
- ) ??
44
- false ;
45
- return (wasPreviouslySaved != isCurrentlySaved) ||
46
- (current.status == AccountStatus .success &&
47
- previous.status != AccountStatus .success);
75
+ return false ;
48
76
},
49
77
listener: (context, accountState) {
50
- final nowIsSaved =
51
- accountState.preferences? .savedHeadlines.any (
52
- (h) => h.id == headline.id,
53
- ) ??
54
- false ;
78
+ final detailsState = context.read <HeadlineDetailsBloc >().state;
79
+ if (detailsState is HeadlineDetailsLoaded ) {
80
+ final nowIsSaved =
81
+ accountState.preferences? .savedHeadlines.any (
82
+ (h) => h.id == detailsState.headline.id,
83
+ ) ??
84
+ false ;
55
85
56
- if (accountState.status == AccountStatus .failure &&
57
- accountState.errorMessage != null ) {
58
- ScaffoldMessenger .of (context)
59
- ..hideCurrentSnackBar ()
60
- ..showSnackBar (
61
- SnackBar (
62
- content: Text (
63
- accountState.errorMessage ??
64
- l10n.headlineSaveErrorSnackbar,
86
+ if (accountState.status == AccountStatus .failure &&
87
+ accountState.errorMessage != null ) {
88
+ ScaffoldMessenger .of (context)
89
+ ..hideCurrentSnackBar ()
90
+ ..showSnackBar (
91
+ SnackBar (
92
+ content: Text (
93
+ accountState.errorMessage ??
94
+ l10n.headlineSaveErrorSnackbar,
95
+ ),
96
+ backgroundColor: Theme .of (context).colorScheme.error,
65
97
),
66
- backgroundColor : Theme . of (context).colorScheme.error,
67
- ),
68
- );
69
- } else {
70
- ScaffoldMessenger . of (context)
71
- .. hideCurrentSnackBar ()
72
- .. showSnackBar (
73
- SnackBar (
74
- content : Text (
75
- nowIsSaved
76
- ? l10n.headlineSavedSuccessSnackbar
77
- : l10n.headlineUnsavedSuccessSnackbar ,
98
+ );
99
+ } else {
100
+ ScaffoldMessenger . of (context)
101
+ .. hideCurrentSnackBar ()
102
+ .. showSnackBar (
103
+ SnackBar (
104
+ content : Text (
105
+ nowIsSaved
106
+ ? l10n.headlineSavedSuccessSnackbar
107
+ : l10n.headlineUnsavedSuccessSnackbar,
108
+ ),
109
+ duration : const Duration (seconds : 2 ) ,
78
110
),
79
- duration: const Duration (seconds: 2 ),
80
- ),
81
- );
111
+ );
112
+ }
82
113
}
83
- }, // Corrected: Removed extra closing brace from here
84
- child: _buildLoadedContent (context, headline),
114
+ },
115
+ child: BlocBuilder <HeadlineDetailsBloc , HeadlineDetailsState >(
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' )),
142
+ };
143
+ },
144
+ ),
85
145
),
86
146
),
87
147
);
@@ -121,16 +181,69 @@ class HeadlineDetailsPage extends StatelessWidget {
121
181
},
122
182
);
123
183
124
- final shareButton = IconButton (
125
- icon: const Icon (Icons .share),
126
- tooltip: l10n.shareActionTooltip, // Added tooltip
127
- onPressed: () {
128
- // Construct the share text
129
- // Use headline.url if available, otherwise just the title
130
- final shareText = headline.url != null
131
- ? '${headline .title }\n\n ${headline .url }'
132
- : headline.title;
133
- Share .share (shareText);
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
+ );
134
247
},
135
248
);
136
249
@@ -142,7 +255,7 @@ class HeadlineDetailsPage extends StatelessWidget {
142
255
icon: const Icon (Icons .arrow_back),
143
256
onPressed: () => Navigator .of (context).pop (),
144
257
),
145
- actions: [bookmarkButton, shareButton],
258
+ actions: [bookmarkButton, shareButtonWidget], // Use the new widget
146
259
// Pinned=false, floating=true, snap=true is common for news apps
147
260
pinned: false ,
148
261
floating: true , // Trailing comma
0 commit comments