@@ -18,19 +18,32 @@ class AddCategoryToFollowPage extends StatelessWidget {
18
18
@override
19
19
Widget build (BuildContext context) {
20
20
final l10n = context.l10n;
21
+ final theme = Theme .of (context); // Get theme
22
+ final textTheme = theme.textTheme; // Get textTheme
23
+
21
24
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 ()),
26
28
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
+ ),
28
35
body: BlocBuilder <CategoriesFilterBloc , CategoriesFilterState >(
29
36
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
+ );
32
44
}
33
- if (categoriesState.status == CategoriesFilterStatus .failure) {
45
+ if (categoriesState.status == CategoriesFilterStatus .failure &&
46
+ categoriesState.categories.isEmpty) { // Show full error only if list is empty
34
47
var errorMessage = l10n.categoryFilterError;
35
48
if (categoriesState.error is HtHttpException ) {
36
49
errorMessage =
@@ -40,78 +53,115 @@ class AddCategoryToFollowPage extends StatelessWidget {
40
53
}
41
54
return FailureStateWidget (
42
55
message: errorMessage,
43
- onRetry:
44
- () => context.read <CategoriesFilterBloc >().add (
45
- CategoriesFilterRequested (),
46
- ),
56
+ onRetry: () => context
57
+ .read <CategoriesFilterBloc >()
58
+ .add (CategoriesFilterRequested ()),
47
59
);
48
60
}
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,
52
67
);
53
68
}
54
69
70
+ // Handle loading more at the bottom or list display
71
+ final categories = categoriesState.categories;
72
+ final isLoadingMore = categoriesState.status == CategoriesFilterStatus .loadingMore;
73
+
55
74
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,
61
79
builder: (context, accountState) {
62
80
final followedCategories =
63
81
accountState.preferences? .followedCategories ?? [];
64
82
65
83
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 ),
68
89
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;
73
102
74
103
return Card (
75
104
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
+ ),
76
110
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),
86
118
child: Image .network (
87
119
category.iconUrl! ,
88
120
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
+ },
92
139
),
93
140
)
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),
96
148
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),
109
155
onPressed: () {
110
156
context.read <AccountBloc >().add (
111
- AccountFollowCategoryToggled (category: category),
112
- );
157
+ AccountFollowCategoryToggled (category: category),
158
+ );
113
159
},
114
160
),
161
+ contentPadding: const EdgeInsets .symmetric ( // Consistent padding
162
+ horizontal: AppSpacing .paddingMedium,
163
+ vertical: AppSpacing .xs,
164
+ ),
115
165
),
116
166
);
117
167
},
0 commit comments