Skip to content

Commit 82c7009

Browse files
authored
Merge pull request #177 from flutter-news-app-full-source-code/refactor/search-overhaul
Refactor/search overhaul
2 parents e22610c + f673e33 commit 82c7009

35 files changed

+1035
-1365
lines changed

CHANGELOG.md

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,10 @@
22

33
## 1.4.0 - Upcoming Release
44

5-
- **fix(demo)**: correct data migration logic for anonymous to authenticated user transitions
6-
- **refactor**: improved filter page reset button to clear local selections and disable when not applicable
7-
- **feat**: auto-scroll to active filter chip in SavedFiltersBar
8-
- **fix**: resolve bug where applying a modified filter incorrectly selects the original saved filter
9-
- **refactor**: relocated saved filters management to the account page and introduced reordering capability.
10-
- **feat**: created a reusable, searchable multi-select page for filtering
11-
- **feat**: add 'Followed' filter to quickly view content from followed items
12-
- **feat(demo)**: pre-populate saved filters and settings for new anonymous users
13-
- **fix**: ensure saved filters are immediately visible on app start
14-
- **fix**: corrected a bug where selecting a source type would check all sources
15-
- **feat**: implement saved feed filters with create, rename, and delete
16-
- **feat**: add horizontal filter bar to the headlines feed for quick selection
17-
- **refactor**: replace monolithic "apply followed items" with granular controls
18-
- **refactor**: simplified filter logic by removing the ambiguous 'isUsingFollowedItems' flag
19-
- **refactor**: improved the source filter UI by replacing horizontal scrolling with a more scalable and user-friendly vertical layout and navigation
20-
- **refactor**: Updated the Headline Details page with a fading scroll effect for metadata chips and a new style for the 'Continue Reading' button. The main feed's filter icon was removed from the AppBar in favor of the new filter bar.
5+
- **feat**: overhauled search and account features with a new sliver-based feed UI, integrated search bar, and modal account sheet.
6+
- **feat**: implemented an advanced feed filtering system with a quick-access filter bar, support for custom saved filters (create, rename, delete, reorder), and a dedicated "Followed" content view.
7+
- **refactor**: enhanced the filter creation UI with a reusable multi-select component and a more scalable vertical layout for source selection.
8+
- **fix(demo)**: corrected data migration for anonymous users and now pre-populate saved filters for a richer initial experience.
219

2210
## 1.3.0 - 2025-10-10
2311

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ Click on any category to explore.
3737
- **Quick-Access Filter Bar:** A persistent, horizontal filter bar on the main feed gives users one-tap access to their favorite content views. It includes built-in filters for "All" and "Followed" items, alongside any custom filters the user has saved.
3838
- **Advanced Filter Creation:** A dedicated, full-screen interface allows users to build complex filters by combining multiple `Topics`, `Sources`, and `Countries`.
3939
- **Saved Filters:** Users can name and save their custom filter combinations. These filters appear in the quick-access bar and can be reordered for a fully customized experience.
40-
- **Unified Search:** A dedicated search page helps users find specific content quickly, with the ability to search across headlines, topics, sources, and countries.
40+
- **Integrated Headline Search:** A sleek search bar in the main feed's scrolling app bar provides a focused, full-screen search experience for headlines. A user avatar within the search bar offers instant modal access to account settings.
4141
> **🎯 Your Advantage:** Give your users powerful content discovery tools that keep them engaged and coming back for more.
4242
4343
</details>

lib/account/view/account_page.dart

Lines changed: 158 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ import 'package:flutter_news_app_mobile_client_full_source_code/router/routes.da
88
import 'package:go_router/go_router.dart';
99
import 'package:ui_kit/ui_kit.dart';
1010

11-
/// {@template account_view}
12-
/// Displays the user's account information and actions.
13-
/// Adapts UI based on authentication status (authenticated vs. anonymous).
11+
/// {@template account_page}
12+
/// A full-screen modal page that displays user account information and actions.
13+
///
14+
/// This page serves as the main entry point for all account-related
15+
/// sections like settings, saved items, and content preferences.
1416
/// {@endtemplate}
1517
class AccountPage extends StatelessWidget {
16-
/// {@macro account_view}
18+
/// {@macro account_page}
1719
const AccountPage({super.key});
1820

1921
@override
@@ -24,176 +26,185 @@ class AccountPage extends StatelessWidget {
2426
final user = appState.user;
2527
final status = appState.status;
2628
final isAnonymous = status == AppLifeCycleStatus.anonymous;
27-
final theme = Theme.of(context);
28-
final textTheme = theme.textTheme;
2929

3030
return Scaffold(
3131
appBar: AppBar(
32-
title: Text(l10n.accountPageTitle, style: textTheme.titleLarge),
33-
),
34-
body: ListView(
35-
padding: const EdgeInsets.all(AppSpacing.paddingMedium),
36-
children: [
37-
_buildUserHeader(context, user, isAnonymous),
38-
const SizedBox(height: AppSpacing.lg),
39-
ListTile(
40-
leading: Icon(
41-
Icons.tune_outlined,
42-
color: theme.colorScheme.primary,
43-
),
44-
title: Text(
45-
l10n.accountContentPreferencesTile,
46-
style: textTheme.titleMedium,
47-
),
48-
trailing: const Icon(Icons.chevron_right),
49-
onTap: () {
50-
context.goNamed(Routes.manageFollowedItemsName);
51-
},
52-
),
53-
const Divider(
54-
indent: AppSpacing.paddingMedium,
55-
endIndent: AppSpacing.paddingMedium,
56-
),
57-
ListTile(
58-
leading: Icon(
59-
Icons.bookmark_outline,
60-
color: theme.colorScheme.primary,
61-
),
62-
title: Text(
63-
l10n.accountSavedHeadlinesTile,
64-
style: textTheme.titleMedium,
65-
),
66-
trailing: const Icon(Icons.chevron_right),
67-
onTap: () {
68-
context.goNamed(Routes.accountSavedHeadlinesName);
69-
},
70-
),
71-
const Divider(
72-
indent: AppSpacing.paddingMedium,
73-
endIndent: AppSpacing.paddingMedium,
74-
),
75-
ListTile(
76-
leading: Icon(
77-
Icons.filter_alt_outlined,
78-
color: theme.colorScheme.primary,
79-
),
80-
title: Text(
81-
l10n.accountSavedFiltersTile,
82-
style: textTheme.titleMedium,
83-
),
84-
trailing: const Icon(Icons.chevron_right),
85-
onTap: () {
86-
context.goNamed(Routes.accountSavedFiltersName);
87-
},
88-
),
89-
const Divider(
90-
indent: AppSpacing.paddingMedium,
91-
endIndent: AppSpacing.paddingMedium,
92-
),
93-
_buildSettingsTile(context),
94-
const Divider(
95-
indent: AppSpacing.paddingMedium,
96-
endIndent: AppSpacing.paddingMedium,
32+
leading: IconButton(
33+
icon: const Icon(Icons.close),
34+
onPressed: () => context.pop(),
35+
),
36+
title: Text(l10n.bottomNavAccountLabel),
37+
actions: [
38+
IconButton(
39+
icon: const Icon(Icons.settings_outlined),
40+
onPressed: () => context.pushNamed(Routes.settingsName),
9741
),
9842
],
9943
),
44+
body: SingleChildScrollView(
45+
child: Padding(
46+
padding: const EdgeInsets.all(AppSpacing.paddingMedium),
47+
child: Column(
48+
mainAxisSize: MainAxisSize.min,
49+
children: [
50+
_buildUserHeader(context, user, isAnonymous),
51+
const SizedBox(height: AppSpacing.lg),
52+
_buildNavigationList(context),
53+
],
54+
),
55+
),
56+
),
10057
);
10158
}
10259

60+
/// Builds the header section of the sheet, displaying the user's avatar,
61+
/// name, and a primary action button (Sign Out or Link Account).
10362
Widget _buildUserHeader(BuildContext context, User? user, bool isAnonymous) {
10463
final l10n = AppLocalizationsX(context).l10n;
10564
final theme = Theme.of(context);
106-
final textTheme = theme.textTheme;
10765
final colorScheme = theme.colorScheme;
10866

109-
final avatarIcon = Icon(
110-
Icons.person_outline,
111-
size: AppSpacing.xxl,
112-
color: colorScheme.onPrimaryContainer,
113-
);
114-
115-
final String displayName;
116-
final Widget statusWidget;
67+
final String statusText;
68+
final String accountTypeText;
69+
final Widget actionButton;
11770

11871
if (isAnonymous) {
119-
displayName = l10n.accountAnonymousUser;
120-
statusWidget = Padding(
121-
padding: const EdgeInsets.only(top: AppSpacing.md),
122-
child: ElevatedButton.icon(
123-
// Changed to ElevatedButton
124-
icon: const Icon(Icons.link_outlined),
125-
label: Text(l10n.accountSignInPromptButton),
126-
style: ElevatedButton.styleFrom(
127-
padding: const EdgeInsets.symmetric(
128-
horizontal: AppSpacing.lg,
129-
vertical: AppSpacing.sm,
130-
),
131-
textStyle: textTheme.labelLarge,
132-
),
133-
onPressed: () {
134-
context.goNamed(Routes.accountLinkingName);
135-
},
136-
),
137-
);
72+
statusText = l10n.accountAnonymousUser;
73+
accountTypeText = l10n.accountGuestAccount;
74+
actionButton = _buildSignInButton(context);
13875
} else {
139-
displayName = user?.email ?? l10n.accountNoNameUser;
140-
statusWidget = Column(
141-
mainAxisSize: MainAxisSize.min,
142-
children: [
143-
const SizedBox(height: AppSpacing.md),
144-
OutlinedButton.icon(
145-
// Changed to OutlinedButton.icon
146-
icon: Icon(Icons.logout, color: colorScheme.error),
147-
label: Text(l10n.accountSignOutTile),
148-
style: OutlinedButton.styleFrom(
149-
foregroundColor: colorScheme.error,
150-
side: BorderSide(color: colorScheme.error.withOpacity(0.5)),
151-
padding: const EdgeInsets.symmetric(
152-
horizontal: AppSpacing.lg,
153-
vertical: AppSpacing.sm,
154-
),
155-
textStyle: textTheme.labelLarge,
156-
),
157-
onPressed: () {
158-
context.read<AuthenticationBloc>().add(
159-
const AuthenticationSignOutRequested(),
160-
);
161-
},
162-
),
163-
],
164-
);
76+
statusText = user?.email ?? l10n.accountNoNameUser;
77+
78+
final String roleDisplayName;
79+
switch (user?.appRole) {
80+
case AppUserRole.standardUser:
81+
roleDisplayName = l10n.accountRoleStandard;
82+
case AppUserRole.premiumUser:
83+
roleDisplayName = l10n.accountRolePremium;
84+
case AppUserRole.guestUser:
85+
roleDisplayName = l10n.accountGuestAccount;
86+
case null:
87+
roleDisplayName = '';
88+
}
89+
accountTypeText = roleDisplayName;
90+
actionButton = _buildSignOutButton(context);
16591
}
16692

167-
return Column(
168-
children: [
169-
CircleAvatar(
170-
radius: AppSpacing.xxl - AppSpacing.sm,
171-
backgroundColor: colorScheme.primaryContainer,
172-
child: avatarIcon,
173-
),
174-
const SizedBox(height: AppSpacing.md),
175-
Text(
176-
displayName,
177-
style: textTheme.headlineSmall,
178-
textAlign: TextAlign.center,
93+
return Card(
94+
child: Padding(
95+
padding: const EdgeInsets.all(AppSpacing.md),
96+
child: Row(
97+
crossAxisAlignment: CrossAxisAlignment.center,
98+
children: [
99+
ClipRRect(
100+
borderRadius: BorderRadius.circular(AppSpacing.sm),
101+
child: Container(
102+
width: AppSpacing.xxl + AppSpacing.sm,
103+
height: AppSpacing.xxl + AppSpacing.sm,
104+
color: colorScheme.primaryContainer,
105+
child: Icon(
106+
Icons.person_outline,
107+
size: AppSpacing.xl,
108+
color: colorScheme.onPrimaryContainer,
109+
),
110+
),
111+
),
112+
const SizedBox(width: AppSpacing.md),
113+
Expanded(
114+
child: Column(
115+
crossAxisAlignment: CrossAxisAlignment.start,
116+
children: [
117+
Text(
118+
statusText,
119+
style: theme.textTheme.titleMedium?.copyWith(
120+
fontWeight: FontWeight.bold,
121+
),
122+
maxLines: 1,
123+
overflow: TextOverflow.ellipsis,
124+
),
125+
const SizedBox(height: AppSpacing.xs),
126+
if (accountTypeText.isNotEmpty)
127+
Text(
128+
accountTypeText,
129+
style: theme.textTheme.bodySmall?.copyWith(
130+
color: colorScheme.onSurfaceVariant,
131+
),
132+
maxLines: 1,
133+
overflow: TextOverflow.ellipsis,
134+
),
135+
],
136+
),
137+
),
138+
const SizedBox(width: AppSpacing.md),
139+
actionButton,
140+
],
179141
),
180-
statusWidget,
181-
],
142+
),
143+
);
144+
}
145+
146+
/// Builds the sign-in button for anonymous users.
147+
Widget _buildSignInButton(BuildContext context) {
148+
final l10n = AppLocalizationsX(context).l10n;
149+
return ElevatedButton(
150+
onPressed: () => context.goNamed(Routes.accountLinkingName),
151+
child: Text(l10n.accountSignInPromptButton),
182152
);
183153
}
184154

185-
Widget _buildSettingsTile(BuildContext context) {
155+
/// Builds the sign-out button for authenticated users.
156+
Widget _buildSignOutButton(BuildContext context) {
157+
final l10n = AppLocalizationsX(context).l10n;
158+
return OutlinedButton(
159+
onPressed: () => context.read<AuthenticationBloc>().add(
160+
const AuthenticationSignOutRequested(),
161+
),
162+
child: Text(l10n.accountSignOutTile),
163+
);
164+
}
165+
166+
/// Builds the list of navigation tiles for accessing different
167+
/// account-related sections.
168+
Widget _buildNavigationList(BuildContext context) {
186169
final l10n = AppLocalizationsX(context).l10n;
187170
final theme = Theme.of(context);
188171
final textTheme = theme.textTheme;
189172

190-
return ListTile(
191-
leading: Icon(Icons.settings_outlined, color: theme.colorScheme.primary),
192-
title: Text(l10n.accountSettingsTile, style: textTheme.titleMedium),
193-
trailing: const Icon(Icons.chevron_right),
194-
onTap: () {
195-
context.goNamed(Routes.settingsName);
196-
},
173+
// Helper to create a ListTile with consistent styling.
174+
Widget buildTile({
175+
required IconData icon,
176+
required String title,
177+
required VoidCallback onTap,
178+
}) {
179+
return ListTile(
180+
leading: Icon(icon, color: theme.colorScheme.primary),
181+
title: Text(title, style: textTheme.titleMedium),
182+
trailing: const Icon(Icons.chevron_right),
183+
onTap: onTap,
184+
);
185+
}
186+
187+
return Column(
188+
children: [
189+
buildTile(
190+
icon: Icons.tune_outlined,
191+
title: l10n.accountContentPreferencesTile,
192+
onTap: () => context.pushNamed(Routes.manageFollowedItemsName),
193+
),
194+
const Divider(),
195+
buildTile(
196+
icon: Icons.bookmark_outline,
197+
title: l10n.accountSavedHeadlinesTile,
198+
onTap: () => context.pushNamed(Routes.accountSavedHeadlinesName),
199+
),
200+
const Divider(),
201+
buildTile(
202+
icon: Icons.filter_alt_outlined,
203+
title: l10n.accountSavedFiltersTile,
204+
onTap: () => context.pushNamed(Routes.accountSavedFiltersName),
205+
),
206+
const Divider(),
207+
],
197208
);
198209
}
199210
}

lib/account/view/manage_followed_items/countries/followed_countries_list_page.dart renamed to lib/account/view/followed_contents/countries/followed_countries_list_page.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class FollowedCountriesListPage extends StatelessWidget {
2727
icon: const Icon(Icons.add_circle_outline),
2828
tooltip: l10n.addCountriesTooltip,
2929
onPressed: () {
30-
context.goNamed(Routes.addCountryToFollowName);
30+
context.pushNamed(Routes.addCountryToFollowName);
3131
},
3232
),
3333
],

0 commit comments

Comments
 (0)