Skip to content

Fix data sync for demo mode #55

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Jun 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ analyzer:
avoid_catches_without_on_clauses: ignore
avoid_print: ignore
avoid_redundant_argument_values: ignore
comment_references: ignore
deprecated_member_use: ignore
document_ignores: ignore
flutter_style_todos: ignore
lines_longer_than_80_chars: ignore
prefer_asserts_with_message: ignore
use_if_null_to_convert_nulls_to_bools: ignore
include: package:very_good_analysis/analysis_options.7.0.0.yaml
linter:
Expand Down
60 changes: 57 additions & 3 deletions lib/account/bloc/account_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:ht_auth_repository/ht_auth_repository.dart';
import 'package:ht_data_repository/ht_data_repository.dart';
import 'package:ht_main/app/config/config.dart' as local_config;
import 'package:ht_shared/ht_shared.dart';

part 'account_event.dart';
Expand All @@ -14,8 +15,10 @@ class AccountBloc extends Bloc<AccountEvent, AccountState> {
required HtAuthRepository authenticationRepository,
required HtDataRepository<UserContentPreferences>
userContentPreferencesRepository,
required local_config.AppEnvironment environment,
}) : _authenticationRepository = authenticationRepository,
_userContentPreferencesRepository = userContentPreferencesRepository,
_environment = environment,
super(const AccountState()) {
// Listen to user changes from HtAuthRepository
_userSubscription = _authenticationRepository.authStateChanges.listen((
Expand All @@ -37,6 +40,7 @@ class AccountBloc extends Bloc<AccountEvent, AccountState> {
final HtAuthRepository _authenticationRepository;
final HtDataRepository<UserContentPreferences>
_userContentPreferencesRepository;
final local_config.AppEnvironment _environment;
late StreamSubscription<User?> _userSubscription;

Future<void> _onAccountUserChanged(
Expand All @@ -62,7 +66,7 @@ class AccountBloc extends Bloc<AccountEvent, AccountState> {
try {
final preferences = await _userContentPreferencesRepository.read(
id: event.userId,
userId: event.userId, // Scope to the current user
userId: event.userId,
);
emit(
state.copyWith(
Expand All @@ -72,7 +76,39 @@ class AccountBloc extends Bloc<AccountEvent, AccountState> {
),
);
} on NotFoundException {
// If preferences not found, create a default one for the user
// In demo mode, a short delay is introduced here to mitigate a race
// condition during anonymous to authenticated data migration.
// This ensures that the DemoDataMigrationService has a chance to
// complete its migration of UserContentPreferences before AccountBloc
// attempts to create a new default preference for the authenticated user.
// This is a temporary stub for the demo environment only and is not
// needed in production/development where backend handles migration.
if (_environment == local_config.AppEnvironment.demo) {
// ignore: inference_failure_on_instance_creation
await Future.delayed(const Duration(seconds: 1));
// After delay, re-attempt to read the preferences.
// This is crucial because migration might have completed during the delay.
try {
final migratedPreferences = await _userContentPreferencesRepository
.read(id: event.userId, userId: event.userId);
emit(
state.copyWith(
status: AccountStatus.success,
preferences: migratedPreferences,
clearErrorMessage: true,
),
);
return; // Exit if successfully read after migration
} on NotFoundException {
// Still not found after delay, proceed to create default.
print(
'[AccountBloc] UserContentPreferences still not found after '
'migration delay. Creating default preferences.',
);
}
}
// If preferences not found (either initially or after re-attempt),
// create a default one for the user.
final defaultPreferences = UserContentPreferences(id: event.userId);
try {
await _userContentPreferencesRepository.create(
Expand All @@ -86,11 +122,29 @@ class AccountBloc extends Bloc<AccountEvent, AccountState> {
clearErrorMessage: true,
),
);
} on ConflictException {
// If a conflict occurs during creation (e.g., another process
// created it concurrently), attempt to read it again to get the
// existing one. This can happen if the migration service
// created it right after the second NotFoundException.
print(
'[AccountBloc] Conflict during creation of UserContentPreferences. '
'Attempting to re-read.',
);
final existingPreferences = await _userContentPreferencesRepository
.read(id: event.userId, userId: event.userId);
emit(
state.copyWith(
status: AccountStatus.success,
preferences: existingPreferences,
clearErrorMessage: true,
),
);
} catch (e) {
emit(
state.copyWith(
status: AccountStatus.failure,
errorMessage: 'Failed to create default preferences.',
errorMessage: 'Failed to create default preferences: $e',
),
);
}
Expand Down
6 changes: 3 additions & 3 deletions lib/account/bloc/available_sources_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,14 @@ class AvailableSourcesBloc
// Assuming readAll without parameters fetches all items.
// Add pagination if necessary for very large datasets.
final response = await _sourcesRepository.readAll(
// limit: _sourcesLimit, // Uncomment if pagination is needed
// limit: _sourcesLimit,
);
emit(
state.copyWith(
status: AvailableSourcesStatus.success,
availableSources: response.items,
// hasMore: response.hasMore, // Uncomment if pagination is needed
// cursor: response.cursor, // Uncomment if pagination is needed
// hasMore: response.hasMore,
// cursor: response.cursor,
clearError: true,
),
);
Expand Down
4 changes: 2 additions & 2 deletions lib/account/bloc/available_sources_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class AvailableSourcesState extends Equatable {
status,
availableSources,
error,
// hasMore, // Add if pagination is implemented
// cursor, // Add if pagination is implemented
// hasMore,
// cursor,
];
}
37 changes: 16 additions & 21 deletions lib/account/view/account_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:ht_main/app/bloc/app_bloc.dart';
import 'package:ht_main/authentication/bloc/authentication_bloc.dart'; // Import AuthenticationBloc
import 'package:ht_main/authentication/bloc/authentication_bloc.dart';
import 'package:ht_main/l10n/l10n.dart';
import 'package:ht_main/router/routes.dart';
import 'package:ht_main/shared/constants/app_spacing.dart';
import 'package:ht_shared/ht_shared.dart'; // Import User and AppStatus
import 'package:ht_shared/ht_shared.dart';

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

return Scaffold(
appBar: AppBar(
title: Text(
l10n.accountPageTitle,
style: textTheme.titleLarge, // Consistent AppBar title style
),
title: Text(l10n.accountPageTitle, style: textTheme.titleLarge),
),
body: ListView(
padding: const EdgeInsets.all(
AppSpacing.paddingMedium,
), // Adjusted padding
padding: const EdgeInsets.all(AppSpacing.paddingMedium),
children: [
_buildUserHeader(context, user, isAnonymous),
const SizedBox(height: AppSpacing.lg), // Adjusted spacing
const SizedBox(height: AppSpacing.lg),
ListTile(
leading: Icon(
Icons.tune_outlined,
Expand Down Expand Up @@ -91,11 +86,11 @@ class AccountPage extends StatelessWidget {
final l10n = context.l10n;
final theme = Theme.of(context);
final textTheme = theme.textTheme;
final colorScheme = theme.colorScheme; // Get colorScheme
final colorScheme = theme.colorScheme;

final avatarIcon = Icon(
Icons.person_outline, // Use outlined version
size: AppSpacing.xxl, // Standardized size
Icons.person_outline,
size: AppSpacing.xxl,
color: colorScheme.onPrimaryContainer,
);

Expand All @@ -105,7 +100,7 @@ class AccountPage extends StatelessWidget {
if (isAnonymous) {
displayName = l10n.accountAnonymousUser;
statusWidget = Padding(
padding: const EdgeInsets.only(top: AppSpacing.md), // Increased padding
padding: const EdgeInsets.only(top: AppSpacing.md),
child: ElevatedButton.icon(
// Changed to ElevatedButton
icon: const Icon(Icons.link_outlined),
Expand All @@ -128,7 +123,7 @@ class AccountPage extends StatelessWidget {
} else {
displayName = user?.email ?? l10n.accountNoNameUser;
statusWidget = Column(
mainAxisSize: MainAxisSize.min, // To keep column tight
mainAxisSize: MainAxisSize.min,
children: [
// if (user?.role != null) ...[
// // Show role only if available
Expand All @@ -141,7 +136,7 @@ class AccountPage extends StatelessWidget {
// textAlign: TextAlign.center,
// ),
// ],
const SizedBox(height: AppSpacing.md), // Consistent spacing
const SizedBox(height: AppSpacing.md),
OutlinedButton.icon(
// Changed to OutlinedButton.icon
icon: Icon(Icons.logout, color: colorScheme.error),
Expand All @@ -168,14 +163,14 @@ class AccountPage extends StatelessWidget {
return Column(
children: [
CircleAvatar(
radius: AppSpacing.xxl - AppSpacing.sm, // Standardized radius (40)
radius: AppSpacing.xxl - AppSpacing.sm,
backgroundColor: colorScheme.primaryContainer,
child: avatarIcon,
),
const SizedBox(height: AppSpacing.md), // Adjusted spacing
const SizedBox(height: AppSpacing.md),
Text(
displayName,
style: textTheme.headlineSmall, // More prominent style
style: textTheme.headlineSmall,
textAlign: TextAlign.center,
),
statusWidget,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,16 @@ class AddCategoryToFollowPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final theme = Theme.of(context); // Get theme
final textTheme = theme.textTheme; // Get textTheme
final theme = Theme.of(context);
final textTheme = theme.textTheme;

return BlocProvider(
create: (context) => CategoriesFilterBloc(
categoriesRepository: context.read<HtDataRepository<Category>>(),
)..add(CategoriesFilterRequested()),
child: Scaffold(
appBar: AppBar(
title: Text(
l10n.addCategoriesPageTitle,
style: textTheme.titleLarge, // Consistent AppBar title
),
title: Text(l10n.addCategoriesPageTitle, style: textTheme.titleLarge),
),
body: BlocBuilder<CategoriesFilterBloc, CategoriesFilterState>(
builder: (context, categoriesState) {
Expand Down Expand Up @@ -86,14 +83,11 @@ class AddCategoryToFollowPage extends StatelessWidget {
accountState.preferences?.followedCategories ?? [];

return ListView.builder(
padding:
const EdgeInsets.symmetric(
// Consistent padding
horizontal: AppSpacing.paddingMedium,
vertical: AppSpacing.paddingSmall,
).copyWith(
bottom: AppSpacing.xxl,
), // Ensure bottom space for loader
padding: const EdgeInsets.symmetric(
// Consistent padding
horizontal: AppSpacing.paddingMedium,
vertical: AppSpacing.paddingSmall,
).copyWith(bottom: AppSpacing.xxl),
itemCount: categories.length + (isLoadingMore ? 1 : 0),
itemBuilder: (context, index) {
if (index == categories.length && isLoadingMore) {
Expand All @@ -114,7 +108,7 @@ class AddCategoryToFollowPage extends StatelessWidget {

return Card(
margin: const EdgeInsets.only(bottom: AppSpacing.sm),
elevation: 0.5, // Subtle elevation
elevation: 0.5,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppSpacing.sm),
side: BorderSide(
Expand All @@ -124,7 +118,7 @@ class AddCategoryToFollowPage extends StatelessWidget {
child: ListTile(
leading: SizedBox(
// Standardized leading icon/image size
width: AppSpacing.xl + AppSpacing.xs, // 36
width: AppSpacing.xl + AppSpacing.xs,
height: AppSpacing.xl + AppSpacing.xs,
child:
category.iconUrl != null &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:ht_main/account/bloc/account_bloc.dart';
import 'package:ht_main/entity_details/view/entity_details_page.dart'; // Import for Arguments
import 'package:ht_main/entity_details/view/entity_details_page.dart';
import 'package:ht_main/l10n/l10n.dart';
import 'package:ht_main/router/routes.dart';
import 'package:ht_main/shared/widgets/widgets.dart';
Expand All @@ -23,11 +23,11 @@ class FollowedCategoriesListPage extends StatelessWidget {

return Scaffold(
appBar: AppBar(
title: const Text('Followed Categories'), // Placeholder
title: const Text('Followed Categories'),
actions: [
IconButton(
icon: const Icon(Icons.add_circle_outline),
tooltip: 'Add Category to Follow', // Placeholder
tooltip: 'Add Category to Follow',
onPressed: () {
context.goNamed(Routes.addCategoryToFollowName);
},
Expand All @@ -40,17 +40,16 @@ class FollowedCategoriesListPage extends StatelessWidget {
state.preferences == null) {
return LoadingStateWidget(
icon: Icons.category_outlined,
headline: 'Loading Followed Categories...', // Placeholder
subheadline: l10n.pleaseWait, // Assuming this exists
headline: 'Loading Followed Categories...',
subheadline: l10n.pleaseWait,
);
}

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

if (followedCategories.isEmpty) {
return const InitialStateWidget(
icon: Icons.no_sim_outlined, // Placeholder icon
headline: 'No Followed Categories', // Placeholder
subheadline:
'Start following categories to see them here.', // Placeholder
icon: Icons.no_sim_outlined,
headline: 'No Followed Categories',
subheadline: 'Start following categories to see them here.',
);
}

Expand Down Expand Up @@ -99,7 +97,7 @@ class FollowedCategoriesListPage extends StatelessWidget {
Icons.remove_circle_outline,
color: Colors.red,
),
tooltip: 'Unfollow Category', // Placeholder
tooltip: 'Unfollow Category',
onPressed: () {
context.read<AccountBloc>().add(
AccountFollowCategoryToggled(category: category),
Expand Down
Loading
Loading