Skip to content

Commit d9e672b

Browse files
committed
feat(details): Fetch headline by ID
- Fetch headline if ID provided - Load from initialHeadline if available - Handle loading, failure states - Refactor share logic for web/native
1 parent 7c4ecb8 commit d9e672b

File tree

1 file changed

+177
-64
lines changed

1 file changed

+177
-64
lines changed

lib/headline-details/view/headline_details_page.dart

Lines changed: 177 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
//
22
// ignore_for_file: avoid_redundant_argument_values
33

4+
import 'package:flutter/foundation.dart' show kIsWeb; // Import kIsWeb
45
import 'package:flutter/material.dart';
56
import 'package:flutter_bloc/flutter_bloc.dart';
67
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
79
import 'package:ht_main/l10n/l10n.dart';
810
import 'package:ht_main/shared/shared.dart';
911
import 'package:ht_shared/ht_shared.dart'
@@ -12,76 +14,134 @@ import 'package:intl/intl.dart';
1214
import 'package:share_plus/share_plus.dart'; // Import share_plus
1315
import 'package:url_launcher/url_launcher_string.dart';
1416

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);
1723

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+
}
1945

2046
@override
2147
Widget build(BuildContext context) {
2248
final l10n = context.l10n;
23-
// Headline is now a direct member: this.headline
24-
// No longer need to watch HeadlineDetailsBloc or its state here.
2549

2650
return SafeArea(
2751
child: Scaffold(
2852
body: BlocListener<AccountBloc, AccountState>(
2953
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);
3374
}
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;
4876
},
4977
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;
5585

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,
6597
),
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),
78110
),
79-
duration: const Duration(seconds: 2),
80-
),
81-
);
111+
);
112+
}
82113
}
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+
),
85145
),
86146
),
87147
);
@@ -121,16 +181,69 @@ class HeadlineDetailsPage extends StatelessWidget {
121181
},
122182
);
123183

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+
);
134247
},
135248
);
136249

@@ -142,7 +255,7 @@ class HeadlineDetailsPage extends StatelessWidget {
142255
icon: const Icon(Icons.arrow_back),
143256
onPressed: () => Navigator.of(context).pop(),
144257
),
145-
actions: [bookmarkButton, shareButton],
258+
actions: [bookmarkButton, shareButtonWidget], // Use the new widget
146259
// Pinned=false, floating=true, snap=true is common for news apps
147260
pinned: false,
148261
floating: true, // Trailing comma

0 commit comments

Comments
 (0)