Skip to content

Commit 4c20f19

Browse files
committed
feat(account): improve category follow page
- Improved loading/error states - Added empty state handling - Enhanced UI with theme and icons - Added loading more indicator
1 parent 8e9abac commit 4c20f19

File tree

1 file changed

+104
-54
lines changed

1 file changed

+104
-54
lines changed

lib/account/view/manage_followed_items/categories/add_category_to_follow_page.dart

Lines changed: 104 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,32 @@ class AddCategoryToFollowPage extends StatelessWidget {
1818
@override
1919
Widget build(BuildContext context) {
2020
final l10n = context.l10n;
21+
final theme = Theme.of(context); // Get theme
22+
final textTheme = theme.textTheme; // Get textTheme
23+
2124
return BlocProvider(
22-
create:
23-
(context) => CategoriesFilterBloc(
24-
categoriesRepository: context.read<HtDataRepository<Category>>(),
25-
)..add(CategoriesFilterRequested()),
25+
create: (context) => CategoriesFilterBloc(
26+
categoriesRepository: context.read<HtDataRepository<Category>>(),
27+
)..add(CategoriesFilterRequested()),
2628
child: Scaffold(
27-
appBar: AppBar(title: Text(l10n.addCategoriesPageTitle)),
29+
appBar: AppBar(
30+
title: Text(
31+
l10n.addCategoriesPageTitle,
32+
style: textTheme.titleLarge, // Consistent AppBar title
33+
),
34+
),
2835
body: BlocBuilder<CategoriesFilterBloc, CategoriesFilterState>(
2936
builder: (context, categoriesState) {
30-
if (categoriesState.status == CategoriesFilterStatus.loading) {
31-
return const Center(child: CircularProgressIndicator());
37+
if (categoriesState.status == CategoriesFilterStatus.loading &&
38+
categoriesState.categories.isEmpty) { // Show full loading only if list is empty
39+
return LoadingStateWidget(
40+
icon: Icons.category_outlined,
41+
headline: l10n.categoryFilterLoadingHeadline,
42+
subheadline: l10n.categoryFilterLoadingSubheadline,
43+
);
3244
}
33-
if (categoriesState.status == CategoriesFilterStatus.failure) {
45+
if (categoriesState.status == CategoriesFilterStatus.failure &&
46+
categoriesState.categories.isEmpty) { // Show full error only if list is empty
3447
var errorMessage = l10n.categoryFilterError;
3548
if (categoriesState.error is HtHttpException) {
3649
errorMessage =
@@ -40,78 +53,115 @@ class AddCategoryToFollowPage extends StatelessWidget {
4053
}
4154
return FailureStateWidget(
4255
message: errorMessage,
43-
onRetry:
44-
() => context.read<CategoriesFilterBloc>().add(
45-
CategoriesFilterRequested(),
46-
),
56+
onRetry: () => context
57+
.read<CategoriesFilterBloc>()
58+
.add(CategoriesFilterRequested()),
4759
);
4860
}
49-
if (categoriesState.categories.isEmpty) {
50-
return FailureStateWidget(
51-
message: l10n.categoryFilterEmptyHeadline,
61+
if (categoriesState.categories.isEmpty &&
62+
categoriesState.status == CategoriesFilterStatus.success) { // Show empty only on success
63+
return InitialStateWidget( // Use InitialStateWidget for empty
64+
icon: Icons.search_off_outlined,
65+
headline: l10n.categoryFilterEmptyHeadline,
66+
subheadline: l10n.categoryFilterEmptySubheadline,
5267
);
5368
}
5469

70+
// Handle loading more at the bottom or list display
71+
final categories = categoriesState.categories;
72+
final isLoadingMore = categoriesState.status == CategoriesFilterStatus.loadingMore;
73+
5574
return BlocBuilder<AccountBloc, AccountState>(
56-
buildWhen:
57-
(previous, current) =>
58-
previous.preferences?.followedCategories !=
59-
current.preferences?.followedCategories ||
60-
previous.status != current.status,
75+
buildWhen: (previous, current) =>
76+
previous.preferences?.followedCategories !=
77+
current.preferences?.followedCategories ||
78+
previous.status != current.status,
6179
builder: (context, accountState) {
6280
final followedCategories =
6381
accountState.preferences?.followedCategories ?? [];
6482

6583
return ListView.builder(
66-
padding: const EdgeInsets.all(AppSpacing.md),
67-
itemCount: categoriesState.categories.length,
84+
padding: const EdgeInsets.symmetric( // Consistent padding
85+
horizontal: AppSpacing.paddingMedium,
86+
vertical: AppSpacing.paddingSmall,
87+
).copyWith(bottom: AppSpacing.xxl), // Ensure bottom space for loader
88+
itemCount: categories.length + (isLoadingMore ? 1 : 0),
6889
itemBuilder: (context, index) {
69-
final category = categoriesState.categories[index];
70-
final isFollowed = followedCategories.any(
71-
(fc) => fc.id == category.id,
72-
);
90+
if (index == categories.length && isLoadingMore) {
91+
return const Padding(
92+
padding: EdgeInsets.symmetric(vertical: AppSpacing.lg),
93+
child: Center(child: CircularProgressIndicator()),
94+
);
95+
}
96+
if (index >= categories.length) return const SizedBox.shrink();
97+
98+
final category = categories[index];
99+
final isFollowed =
100+
followedCategories.any((fc) => fc.id == category.id);
101+
final colorScheme = Theme.of(context).colorScheme;
73102

74103
return Card(
75104
margin: const EdgeInsets.only(bottom: AppSpacing.sm),
105+
elevation: 0.5, // Subtle elevation
106+
shape: RoundedRectangleBorder(
107+
borderRadius: BorderRadius.circular(AppSpacing.sm),
108+
side: BorderSide(color: colorScheme.outlineVariant.withOpacity(0.3)),
109+
),
76110
child: ListTile(
77-
leading:
78-
category.iconUrl != null &&
79-
Uri.tryParse(
80-
category.iconUrl!,
81-
)?.isAbsolute ==
82-
true
83-
? SizedBox(
84-
width: 36,
85-
height: 36,
111+
leading: SizedBox( // Standardized leading icon/image size
112+
width: AppSpacing.xl + AppSpacing.xs, // 36
113+
height: AppSpacing.xl + AppSpacing.xs,
114+
child: category.iconUrl != null &&
115+
Uri.tryParse(category.iconUrl!)?.isAbsolute == true
116+
? ClipRRect(
117+
borderRadius: BorderRadius.circular(AppSpacing.xs),
86118
child: Image.network(
87119
category.iconUrl!,
88120
fit: BoxFit.contain,
89-
errorBuilder:
90-
(context, error, stackTrace) =>
91-
const Icon(Icons.category_outlined),
121+
errorBuilder: (context, error, stackTrace) =>
122+
Icon(
123+
Icons.category_outlined,
124+
color: colorScheme.onSurfaceVariant,
125+
size: AppSpacing.lg,
126+
),
127+
loadingBuilder: (context, child, loadingProgress) {
128+
if (loadingProgress == null) return child;
129+
return Center(
130+
child: CircularProgressIndicator(
131+
strokeWidth: 2,
132+
value: loadingProgress.expectedTotalBytes != null
133+
? loadingProgress.cumulativeBytesLoaded /
134+
loadingProgress.expectedTotalBytes!
135+
: null,
136+
),
137+
);
138+
},
92139
),
93140
)
94-
: const Icon(Icons.category_outlined),
95-
title: Text(category.name),
141+
: Icon(
142+
Icons.category_outlined,
143+
color: colorScheme.onSurfaceVariant,
144+
size: AppSpacing.lg,
145+
),
146+
),
147+
title: Text(category.name, style: textTheme.titleMedium),
96148
trailing: IconButton(
97-
icon:
98-
isFollowed
99-
? Icon(
100-
Icons.check_circle,
101-
color:
102-
Theme.of(context).colorScheme.primary,
103-
)
104-
: const Icon(Icons.add_circle_outline),
105-
tooltip:
106-
isFollowed
107-
? l10n.unfollowCategoryTooltip(category.name)
108-
: l10n.followCategoryTooltip(category.name),
149+
icon: isFollowed
150+
? Icon(Icons.check_circle, color: colorScheme.primary)
151+
: Icon(Icons.add_circle_outline, color: colorScheme.onSurfaceVariant),
152+
tooltip: isFollowed
153+
? l10n.unfollowCategoryTooltip(category.name)
154+
: l10n.followCategoryTooltip(category.name),
109155
onPressed: () {
110156
context.read<AccountBloc>().add(
111-
AccountFollowCategoryToggled(category: category),
112-
);
157+
AccountFollowCategoryToggled(category: category),
158+
);
113159
},
114160
),
161+
contentPadding: const EdgeInsets.symmetric( // Consistent padding
162+
horizontal: AppSpacing.paddingMedium,
163+
vertical: AppSpacing.xs,
164+
),
115165
),
116166
);
117167
},

0 commit comments

Comments
 (0)