Skip to content

Commit 1482e19

Browse files
authored
Merge pull request #55 from headlines-toolkit/fix_data_sync_for_demo_mode
Fix data sync for demo mode
2 parents 7fff7dc + 90c018c commit 1482e19

File tree

70 files changed

+1012
-935
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

70 files changed

+1012
-935
lines changed

analysis_options.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ analyzer:
33
avoid_catches_without_on_clauses: ignore
44
avoid_print: ignore
55
avoid_redundant_argument_values: ignore
6+
comment_references: ignore
7+
deprecated_member_use: ignore
68
document_ignores: ignore
79
flutter_style_todos: ignore
810
lines_longer_than_80_chars: ignore
11+
prefer_asserts_with_message: ignore
912
use_if_null_to_convert_nulls_to_bools: ignore
1013
include: package:very_good_analysis/analysis_options.7.0.0.yaml
1114
linter:

lib/account/bloc/account_bloc.dart

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'package:bloc/bloc.dart';
44
import 'package:equatable/equatable.dart';
55
import 'package:ht_auth_repository/ht_auth_repository.dart';
66
import 'package:ht_data_repository/ht_data_repository.dart';
7+
import 'package:ht_main/app/config/config.dart' as local_config;
78
import 'package:ht_shared/ht_shared.dart';
89

910
part 'account_event.dart';
@@ -14,8 +15,10 @@ class AccountBloc extends Bloc<AccountEvent, AccountState> {
1415
required HtAuthRepository authenticationRepository,
1516
required HtDataRepository<UserContentPreferences>
1617
userContentPreferencesRepository,
18+
required local_config.AppEnvironment environment,
1719
}) : _authenticationRepository = authenticationRepository,
1820
_userContentPreferencesRepository = userContentPreferencesRepository,
21+
_environment = environment,
1922
super(const AccountState()) {
2023
// Listen to user changes from HtAuthRepository
2124
_userSubscription = _authenticationRepository.authStateChanges.listen((
@@ -37,6 +40,7 @@ class AccountBloc extends Bloc<AccountEvent, AccountState> {
3740
final HtAuthRepository _authenticationRepository;
3841
final HtDataRepository<UserContentPreferences>
3942
_userContentPreferencesRepository;
43+
final local_config.AppEnvironment _environment;
4044
late StreamSubscription<User?> _userSubscription;
4145

4246
Future<void> _onAccountUserChanged(
@@ -62,7 +66,7 @@ class AccountBloc extends Bloc<AccountEvent, AccountState> {
6266
try {
6367
final preferences = await _userContentPreferencesRepository.read(
6468
id: event.userId,
65-
userId: event.userId, // Scope to the current user
69+
userId: event.userId,
6670
);
6771
emit(
6872
state.copyWith(
@@ -72,7 +76,39 @@ class AccountBloc extends Bloc<AccountEvent, AccountState> {
7276
),
7377
);
7478
} on NotFoundException {
75-
// If preferences not found, create a default one for the user
79+
// In demo mode, a short delay is introduced here to mitigate a race
80+
// condition during anonymous to authenticated data migration.
81+
// This ensures that the DemoDataMigrationService has a chance to
82+
// complete its migration of UserContentPreferences before AccountBloc
83+
// attempts to create a new default preference for the authenticated user.
84+
// This is a temporary stub for the demo environment only and is not
85+
// needed in production/development where backend handles migration.
86+
if (_environment == local_config.AppEnvironment.demo) {
87+
// ignore: inference_failure_on_instance_creation
88+
await Future.delayed(const Duration(seconds: 1));
89+
// After delay, re-attempt to read the preferences.
90+
// This is crucial because migration might have completed during the delay.
91+
try {
92+
final migratedPreferences = await _userContentPreferencesRepository
93+
.read(id: event.userId, userId: event.userId);
94+
emit(
95+
state.copyWith(
96+
status: AccountStatus.success,
97+
preferences: migratedPreferences,
98+
clearErrorMessage: true,
99+
),
100+
);
101+
return; // Exit if successfully read after migration
102+
} on NotFoundException {
103+
// Still not found after delay, proceed to create default.
104+
print(
105+
'[AccountBloc] UserContentPreferences still not found after '
106+
'migration delay. Creating default preferences.',
107+
);
108+
}
109+
}
110+
// If preferences not found (either initially or after re-attempt),
111+
// create a default one for the user.
76112
final defaultPreferences = UserContentPreferences(id: event.userId);
77113
try {
78114
await _userContentPreferencesRepository.create(
@@ -86,11 +122,29 @@ class AccountBloc extends Bloc<AccountEvent, AccountState> {
86122
clearErrorMessage: true,
87123
),
88124
);
125+
} on ConflictException {
126+
// If a conflict occurs during creation (e.g., another process
127+
// created it concurrently), attempt to read it again to get the
128+
// existing one. This can happen if the migration service
129+
// created it right after the second NotFoundException.
130+
print(
131+
'[AccountBloc] Conflict during creation of UserContentPreferences. '
132+
'Attempting to re-read.',
133+
);
134+
final existingPreferences = await _userContentPreferencesRepository
135+
.read(id: event.userId, userId: event.userId);
136+
emit(
137+
state.copyWith(
138+
status: AccountStatus.success,
139+
preferences: existingPreferences,
140+
clearErrorMessage: true,
141+
),
142+
);
89143
} catch (e) {
90144
emit(
91145
state.copyWith(
92146
status: AccountStatus.failure,
93-
errorMessage: 'Failed to create default preferences.',
147+
errorMessage: 'Failed to create default preferences: $e',
94148
),
95149
);
96150
}

lib/account/bloc/available_sources_bloc.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,14 @@ class AvailableSourcesBloc
3535
// Assuming readAll without parameters fetches all items.
3636
// Add pagination if necessary for very large datasets.
3737
final response = await _sourcesRepository.readAll(
38-
// limit: _sourcesLimit, // Uncomment if pagination is needed
38+
// limit: _sourcesLimit,
3939
);
4040
emit(
4141
state.copyWith(
4242
status: AvailableSourcesStatus.success,
4343
availableSources: response.items,
44-
// hasMore: response.hasMore, // Uncomment if pagination is needed
45-
// cursor: response.cursor, // Uncomment if pagination is needed
44+
// hasMore: response.hasMore,
45+
// cursor: response.cursor,
4646
clearError: true,
4747
),
4848
);

lib/account/bloc/available_sources_state.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ class AvailableSourcesState extends Equatable {
4141
status,
4242
availableSources,
4343
error,
44-
// hasMore, // Add if pagination is implemented
45-
// cursor, // Add if pagination is implemented
44+
// hasMore,
45+
// cursor,
4646
];
4747
}

lib/account/view/account_page.dart

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ import 'package:flutter/material.dart';
22
import 'package:flutter_bloc/flutter_bloc.dart';
33
import 'package:go_router/go_router.dart';
44
import 'package:ht_main/app/bloc/app_bloc.dart';
5-
import 'package:ht_main/authentication/bloc/authentication_bloc.dart'; // Import AuthenticationBloc
5+
import 'package:ht_main/authentication/bloc/authentication_bloc.dart';
66
import 'package:ht_main/l10n/l10n.dart';
77
import 'package:ht_main/router/routes.dart';
88
import 'package:ht_main/shared/constants/app_spacing.dart';
9-
import 'package:ht_shared/ht_shared.dart'; // Import User and AppStatus
9+
import 'package:ht_shared/ht_shared.dart';
1010

1111
/// {@template account_view}
1212
/// Displays the user's account information and actions.
@@ -24,23 +24,18 @@ class AccountPage extends StatelessWidget {
2424
final user = appState.user;
2525
final status = appState.status;
2626
final isAnonymous = status == AppStatus.anonymous;
27-
final theme = Theme.of(context); // Get theme for AppBar
28-
final textTheme = theme.textTheme; // Get textTheme for AppBar
27+
final theme = Theme.of(context);
28+
final textTheme = theme.textTheme;
2929

3030
return Scaffold(
3131
appBar: AppBar(
32-
title: Text(
33-
l10n.accountPageTitle,
34-
style: textTheme.titleLarge, // Consistent AppBar title style
35-
),
32+
title: Text(l10n.accountPageTitle, style: textTheme.titleLarge),
3633
),
3734
body: ListView(
38-
padding: const EdgeInsets.all(
39-
AppSpacing.paddingMedium,
40-
), // Adjusted padding
35+
padding: const EdgeInsets.all(AppSpacing.paddingMedium),
4136
children: [
4237
_buildUserHeader(context, user, isAnonymous),
43-
const SizedBox(height: AppSpacing.lg), // Adjusted spacing
38+
const SizedBox(height: AppSpacing.lg),
4439
ListTile(
4540
leading: Icon(
4641
Icons.tune_outlined,
@@ -91,11 +86,11 @@ class AccountPage extends StatelessWidget {
9186
final l10n = context.l10n;
9287
final theme = Theme.of(context);
9388
final textTheme = theme.textTheme;
94-
final colorScheme = theme.colorScheme; // Get colorScheme
89+
final colorScheme = theme.colorScheme;
9590

9691
final avatarIcon = Icon(
97-
Icons.person_outline, // Use outlined version
98-
size: AppSpacing.xxl, // Standardized size
92+
Icons.person_outline,
93+
size: AppSpacing.xxl,
9994
color: colorScheme.onPrimaryContainer,
10095
);
10196

@@ -105,7 +100,7 @@ class AccountPage extends StatelessWidget {
105100
if (isAnonymous) {
106101
displayName = l10n.accountAnonymousUser;
107102
statusWidget = Padding(
108-
padding: const EdgeInsets.only(top: AppSpacing.md), // Increased padding
103+
padding: const EdgeInsets.only(top: AppSpacing.md),
109104
child: ElevatedButton.icon(
110105
// Changed to ElevatedButton
111106
icon: const Icon(Icons.link_outlined),
@@ -128,7 +123,7 @@ class AccountPage extends StatelessWidget {
128123
} else {
129124
displayName = user?.email ?? l10n.accountNoNameUser;
130125
statusWidget = Column(
131-
mainAxisSize: MainAxisSize.min, // To keep column tight
126+
mainAxisSize: MainAxisSize.min,
132127
children: [
133128
// if (user?.role != null) ...[
134129
// // Show role only if available
@@ -141,7 +136,7 @@ class AccountPage extends StatelessWidget {
141136
// textAlign: TextAlign.center,
142137
// ),
143138
// ],
144-
const SizedBox(height: AppSpacing.md), // Consistent spacing
139+
const SizedBox(height: AppSpacing.md),
145140
OutlinedButton.icon(
146141
// Changed to OutlinedButton.icon
147142
icon: Icon(Icons.logout, color: colorScheme.error),
@@ -168,14 +163,14 @@ class AccountPage extends StatelessWidget {
168163
return Column(
169164
children: [
170165
CircleAvatar(
171-
radius: AppSpacing.xxl - AppSpacing.sm, // Standardized radius (40)
166+
radius: AppSpacing.xxl - AppSpacing.sm,
172167
backgroundColor: colorScheme.primaryContainer,
173168
child: avatarIcon,
174169
),
175-
const SizedBox(height: AppSpacing.md), // Adjusted spacing
170+
const SizedBox(height: AppSpacing.md),
176171
Text(
177172
displayName,
178-
style: textTheme.headlineSmall, // More prominent style
173+
style: textTheme.headlineSmall,
179174
textAlign: TextAlign.center,
180175
),
181176
statusWidget,

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

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,16 @@ 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
21+
final theme = Theme.of(context);
22+
final textTheme = theme.textTheme;
2323

2424
return BlocProvider(
2525
create: (context) => CategoriesFilterBloc(
2626
categoriesRepository: context.read<HtDataRepository<Category>>(),
2727
)..add(CategoriesFilterRequested()),
2828
child: Scaffold(
2929
appBar: AppBar(
30-
title: Text(
31-
l10n.addCategoriesPageTitle,
32-
style: textTheme.titleLarge, // Consistent AppBar title
33-
),
30+
title: Text(l10n.addCategoriesPageTitle, style: textTheme.titleLarge),
3431
),
3532
body: BlocBuilder<CategoriesFilterBloc, CategoriesFilterState>(
3633
builder: (context, categoriesState) {
@@ -86,14 +83,11 @@ class AddCategoryToFollowPage extends StatelessWidget {
8683
accountState.preferences?.followedCategories ?? [];
8784

8885
return ListView.builder(
89-
padding:
90-
const EdgeInsets.symmetric(
91-
// Consistent padding
92-
horizontal: AppSpacing.paddingMedium,
93-
vertical: AppSpacing.paddingSmall,
94-
).copyWith(
95-
bottom: AppSpacing.xxl,
96-
), // Ensure bottom space for loader
86+
padding: const EdgeInsets.symmetric(
87+
// Consistent padding
88+
horizontal: AppSpacing.paddingMedium,
89+
vertical: AppSpacing.paddingSmall,
90+
).copyWith(bottom: AppSpacing.xxl),
9791
itemCount: categories.length + (isLoadingMore ? 1 : 0),
9892
itemBuilder: (context, index) {
9993
if (index == categories.length && isLoadingMore) {
@@ -114,7 +108,7 @@ class AddCategoryToFollowPage extends StatelessWidget {
114108

115109
return Card(
116110
margin: const EdgeInsets.only(bottom: AppSpacing.sm),
117-
elevation: 0.5, // Subtle elevation
111+
elevation: 0.5,
118112
shape: RoundedRectangleBorder(
119113
borderRadius: BorderRadius.circular(AppSpacing.sm),
120114
side: BorderSide(
@@ -124,7 +118,7 @@ class AddCategoryToFollowPage extends StatelessWidget {
124118
child: ListTile(
125119
leading: SizedBox(
126120
// Standardized leading icon/image size
127-
width: AppSpacing.xl + AppSpacing.xs, // 36
121+
width: AppSpacing.xl + AppSpacing.xs,
128122
height: AppSpacing.xl + AppSpacing.xs,
129123
child:
130124
category.iconUrl != null &&

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

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
22
import 'package:flutter_bloc/flutter_bloc.dart';
33
import 'package:go_router/go_router.dart';
44
import 'package:ht_main/account/bloc/account_bloc.dart';
5-
import 'package:ht_main/entity_details/view/entity_details_page.dart'; // Import for Arguments
5+
import 'package:ht_main/entity_details/view/entity_details_page.dart';
66
import 'package:ht_main/l10n/l10n.dart';
77
import 'package:ht_main/router/routes.dart';
88
import 'package:ht_main/shared/widgets/widgets.dart';
@@ -23,11 +23,11 @@ class FollowedCategoriesListPage extends StatelessWidget {
2323

2424
return Scaffold(
2525
appBar: AppBar(
26-
title: const Text('Followed Categories'), // Placeholder
26+
title: const Text('Followed Categories'),
2727
actions: [
2828
IconButton(
2929
icon: const Icon(Icons.add_circle_outline),
30-
tooltip: 'Add Category to Follow', // Placeholder
30+
tooltip: 'Add Category to Follow',
3131
onPressed: () {
3232
context.goNamed(Routes.addCategoryToFollowName);
3333
},
@@ -40,17 +40,16 @@ class FollowedCategoriesListPage extends StatelessWidget {
4040
state.preferences == null) {
4141
return LoadingStateWidget(
4242
icon: Icons.category_outlined,
43-
headline: 'Loading Followed Categories...', // Placeholder
44-
subheadline: l10n.pleaseWait, // Assuming this exists
43+
headline: 'Loading Followed Categories...',
44+
subheadline: l10n.pleaseWait,
4545
);
4646
}
4747

4848
if (state.status == AccountStatus.failure &&
4949
state.preferences == null) {
5050
return FailureStateWidget(
5151
message:
52-
state.errorMessage ??
53-
'Could not load followed categories.', // Placeholder
52+
state.errorMessage ?? 'Could not load followed categories.',
5453
onRetry: () {
5554
if (state.user?.id != null) {
5655
context.read<AccountBloc>().add(
@@ -63,10 +62,9 @@ class FollowedCategoriesListPage extends StatelessWidget {
6362

6463
if (followedCategories.isEmpty) {
6564
return const InitialStateWidget(
66-
icon: Icons.no_sim_outlined, // Placeholder icon
67-
headline: 'No Followed Categories', // Placeholder
68-
subheadline:
69-
'Start following categories to see them here.', // Placeholder
65+
icon: Icons.no_sim_outlined,
66+
headline: 'No Followed Categories',
67+
subheadline: 'Start following categories to see them here.',
7068
);
7169
}
7270

@@ -99,7 +97,7 @@ class FollowedCategoriesListPage extends StatelessWidget {
9997
Icons.remove_circle_outline,
10098
color: Colors.red,
10199
),
102-
tooltip: 'Unfollow Category', // Placeholder
100+
tooltip: 'Unfollow Category',
103101
onPressed: () {
104102
context.read<AccountBloc>().add(
105103
AccountFollowCategoryToggled(category: category),

0 commit comments

Comments
 (0)