diff --git a/analysis_options.yaml b/analysis_options.yaml index ee67a18c..95dc1774 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -2,6 +2,7 @@ analyzer: errors: avoid_catches_without_on_clauses: ignore avoid_print: ignore + document_ignores: ignore lines_longer_than_80_chars: ignore use_if_null_to_convert_nulls_to_bools: ignore include: package:very_good_analysis/analysis_options.7.0.0.yaml diff --git a/firebase.json b/firebase.json deleted file mode 100644 index c662beac..00000000 --- a/firebase.json +++ /dev/null @@ -1 +0,0 @@ -{"flutter":{"platforms":{"android":{"default":{"projectId":"headlines-toolkit","appId":"1:420644261437:android:3fa3052f883bf86958a454","fileOutput":"android/app/google-services.json"}},"dart":{"lib/firebase_options.dart":{"projectId":"headlines-toolkit","configurations":{"android":"1:420644261437:android:3fa3052f883bf86958a454","web":"1:420644261437:web:c20af2372a50f7c958a454"}}}}}} \ No newline at end of file diff --git a/lib/account/bloc/account_bloc.dart b/lib/account/bloc/account_bloc.dart index 5cee4629..e123e064 100644 --- a/lib/account/bloc/account_bloc.dart +++ b/lib/account/bloc/account_bloc.dart @@ -1,6 +1,11 @@ +import 'dart:async'; + import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; -import 'package:ht_authentication_repository/ht_authentication_repository.dart'; +import 'package:ht_auth_repository/ht_auth_repository.dart'; +import 'package:ht_data_repository/ht_data_repository.dart'; +import 'package:ht_shared/ht_shared.dart' + show HtHttpException, User, UserContentPreferences; part 'account_event.dart'; part 'account_state.dart'; @@ -10,41 +15,77 @@ part 'account_state.dart'; /// {@endtemplate} class AccountBloc extends Bloc { /// {@macro account_bloc} - AccountBloc({required HtAuthenticationRepository authenticationRepository}) - : _authenticationRepository = authenticationRepository, - super(const AccountState()) { - on(_onLogoutRequested); + AccountBloc({ + required HtAuthRepository authenticationRepository, + required HtDataRepository + userContentPreferencesRepository, + }) : _authenticationRepository = authenticationRepository, + _userContentPreferencesRepository = userContentPreferencesRepository, + super(const AccountState()) { + // Listen to authentication state changes from the repository + _authenticationRepository.authStateChanges.listen( + (user) => add(_AccountUserChanged(user: user)), + ); + + on<_AccountUserChanged>(_onAccountUserChanged); + on( + _onAccountLoadContentPreferencesRequested, + ); // Handlers for AccountSettingsNavigationRequested and // AccountBackupNavigationRequested are typically handled in the UI layer // (e.g., BlocListener navigating) or could emit specific states if needed. - // For now, we only need the logout logic here. } - final HtAuthenticationRepository _authenticationRepository; + final HtAuthRepository _authenticationRepository; + final HtDataRepository + _userContentPreferencesRepository; + + /// Handles [_AccountUserChanged] events. + /// + /// Updates the state with the current user and triggers loading + /// of user preferences if the user is authenticated. + Future _onAccountUserChanged( + _AccountUserChanged event, + Emitter emit, + ) async { + emit(state.copyWith(user: event.user)); + if (event.user != null) { + // User is authenticated, load preferences + add(AccountLoadContentPreferencesRequested(userId: event.user!.id)); + } else { + // User is unauthenticated, clear preferences + emit(state.copyWith()); + } + } - /// Handles the [AccountLogoutRequested] event. + /// Handles [AccountLoadContentPreferencesRequested] events. /// - /// Attempts to sign out the user using the [HtAuthenticationRepository]. - /// Emits [AccountStatus.loading] before the operation and updates to - /// [AccountStatus.success] or [AccountStatus.failure] based on the outcome. - Future _onLogoutRequested( - AccountLogoutRequested event, + /// Attempts to load the user's content preferences. + Future _onAccountLoadContentPreferencesRequested( + AccountLoadContentPreferencesRequested event, Emitter emit, ) async { emit(state.copyWith(status: AccountStatus.loading)); try { - await _authenticationRepository.signOut(); - // No need to emit success here. The AppBloc listening to the - // repository's user stream will handle the global state change - // and trigger the necessary UI updates/redirects. - // We can emit an initial state again if needed for this BLoC's - // local state. - emit(state.copyWith(status: AccountStatus.initial)); + final preferences = await _userContentPreferencesRepository.read( + id: event.userId, + userId: event.userId, // Preferences are user-scoped + ); + emit( + state.copyWith(status: AccountStatus.success, preferences: preferences), + ); + } on HtHttpException catch (e) { + emit( + state.copyWith( + status: AccountStatus.failure, + errorMessage: 'Failed to load preferences: ${e.message}', + ), + ); } catch (e) { emit( state.copyWith( status: AccountStatus.failure, - errorMessage: 'Logout failed: $e', + errorMessage: 'An unexpected error occurred: $e', ), ); } diff --git a/lib/account/bloc/account_event.dart b/lib/account/bloc/account_event.dart index 194ddeb3..16132911 100644 --- a/lib/account/bloc/account_event.dart +++ b/lib/account/bloc/account_event.dart @@ -1,24 +1,40 @@ part of 'account_bloc.dart'; -/// Base class for all events related to the Account feature. -abstract class AccountEvent extends Equatable { +/// {@template account_event} +/// Base class for Account events. +/// {@endtemplate} +sealed class AccountEvent extends Equatable { + /// {@macro account_event} const AccountEvent(); @override - List get props => []; + List get props => []; } -/// Event triggered when the user requests to navigate to the settings page. -class AccountSettingsNavigationRequested extends AccountEvent { - const AccountSettingsNavigationRequested(); -} +/// {@template _account_user_changed} +/// Internal event triggered when the authenticated user changes. +/// {@endtemplate} +final class _AccountUserChanged extends AccountEvent { + /// {@macro _account_user_changed} + const _AccountUserChanged({required this.user}); + + /// The current authenticated user, or null if unauthenticated. + final User? user; -/// Event triggered when the user requests to log out. -class AccountLogoutRequested extends AccountEvent { - const AccountLogoutRequested(); + @override + List get props => [user]; } -/// Event triggered when the user (anonymous) requests to backup/link account. -class AccountBackupNavigationRequested extends AccountEvent { - const AccountBackupNavigationRequested(); +/// {@template account_load_content_preferences_requested} +/// Event triggered when the user's content preferences need to be loaded. +/// {@endtemplate} +final class AccountLoadContentPreferencesRequested extends AccountEvent { + /// {@macro account_load_content_preferences_requested} + const AccountLoadContentPreferencesRequested({required this.userId}); + + /// The ID of the user whose content preferences should be loaded. + final String userId; + + @override + List get props => [userId]; } diff --git a/lib/account/bloc/account_state.dart b/lib/account/bloc/account_state.dart index 7029ee73..bd7d0e03 100644 --- a/lib/account/bloc/account_state.dart +++ b/lib/account/bloc/account_state.dart @@ -1,26 +1,59 @@ part of 'account_bloc.dart'; -/// Enum representing the status of the Account feature. -enum AccountStatus { initial, loading, success, failure } +/// Defines the status of the account state. +enum AccountStatus { + /// The initial state. + initial, -/// Represents the state of the Account feature. -class AccountState extends Equatable { - const AccountState({this.status = AccountStatus.initial, this.errorMessage}); + /// An operation is in progress. + loading, - /// The current status of the account feature operations. + /// An operation was successful. + success, + + /// An operation failed. + failure, +} + +/// {@template account_state} +/// State for the Account feature. +/// {@endtemplate} +final class AccountState extends Equatable { + /// {@macro account_state} + const AccountState({ + this.status = AccountStatus.initial, + this.user, + this.preferences, + this.errorMessage, + }); + + /// The current status of the account state. final AccountStatus status; - /// An optional error message if an operation failed. + /// The currently authenticated user. + final User? user; + + /// The user's content preferences. + final UserContentPreferences? preferences; + + /// An error message if an operation failed. final String? errorMessage; - /// Creates a copy of the current state with updated values. - AccountState copyWith({AccountStatus? status, String? errorMessage}) { + /// Creates a copy of this [AccountState] with the given fields replaced. + AccountState copyWith({ + AccountStatus? status, + User? user, + UserContentPreferences? preferences, + String? errorMessage, + }) { return AccountState( status: status ?? this.status, + user: user ?? this.user, + preferences: preferences ?? this.preferences, errorMessage: errorMessage ?? this.errorMessage, ); } @override - List get props => [status, errorMessage]; + List get props => [status, user, preferences, errorMessage]; } diff --git a/lib/account/view/account_page.dart b/lib/account/view/account_page.dart index 9bec1868..24c46179 100644 --- a/lib/account/view/account_page.dart +++ b/lib/account/view/account_page.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; -import 'package:ht_authentication_client/ht_authentication_client.dart'; -import 'package:ht_main/account/bloc/account_bloc.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/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 /// {@template account_view} /// Displays the user's account information and actions. @@ -20,11 +20,13 @@ class AccountPage extends StatelessWidget { Widget build(BuildContext context) { final l10n = context.l10n; // Watch AppBloc for user details and authentication status - final user = context.watch().state.user; - final status = user.authenticationStatus; + final appState = context.watch().state; + final user = appState.user; + final status = appState.status; // Use AppStatus from AppBloc state // Determine if the user is anonymous - final isAnonymous = status == AuthenticationStatus.anonymous; + final isAnonymous = + status == AppStatus.anonymous; // Use AppStatus.anonymous return Scaffold( appBar: AppBar(title: Text(l10n.accountPageTitle)), @@ -36,7 +38,27 @@ class AccountPage extends StatelessWidget { _buildUserHeader(context, user, isAnonymous), const SizedBox(height: AppSpacing.xl), // Use AppSpacing // --- Action Tiles --- - // Settings Tile is now the first actionable item below the header + // Content Preferences Tile + ListTile( + leading: const Icon(Icons.tune_outlined), + title: Text(l10n.accountContentPreferencesTile), + trailing: const Icon(Icons.chevron_right), + onTap: () { + context.goNamed(Routes.accountContentPreferencesName); + }, + ), + const Divider(), // Divider after Content Preferences + // Saved Headlines Tile + ListTile( + leading: const Icon(Icons.bookmark_outline), + title: Text(l10n.accountSavedHeadlinesTile), + trailing: const Icon(Icons.chevron_right), + onTap: () { + context.goNamed(Routes.accountSavedHeadlinesName); + }, + ), + const Divider(), // Divider after Saved Headlines + // Settings Tile _buildSettingsTile(context), const Divider(), // Divider after settings ], @@ -45,88 +67,83 @@ class AccountPage extends StatelessWidget { } /// Builds the header section displaying user avatar, name, and status. - Widget _buildUserHeader(BuildContext context, User user, bool isAnonymous) { + Widget _buildUserHeader(BuildContext context, User? user, bool isAnonymous) { final l10n = context.l10n; final theme = Theme.of(context); final textTheme = theme.textTheme; - // Use placeholder if photoUrl is null or empty - final photoUrl = user.photoUrl; - final hasPhoto = photoUrl != null && photoUrl.isNotEmpty; + // Use a generic icon for the avatar + const avatarIcon = Icon(Icons.person, size: 40); + + // Determine display name and status text + final String displayName; + final Widget statusWidget; + + if (isAnonymous) { + displayName = l10n.accountAnonymousUser; + statusWidget = Padding( + padding: const EdgeInsets.only(top: AppSpacing.sm), + child: TextButton( + onPressed: () { + // Navigate to the authentication page in linking mode + context.goNamed( + Routes.authenticationName, + queryParameters: {'context': 'linking'}, + ); + }, + child: Text(l10n.accountSignInPromptButton), + ), + ); + } else { + // For authenticated users, display email and role + displayName = user?.email ?? l10n.accountNoNameUser; + statusWidget = Column( + children: [ + const SizedBox(height: AppSpacing.sm), + Text( + l10n.accountRoleLabel(user?.role.name ?? 'unknown'), // Display role + style: textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: AppSpacing.sm), + OutlinedButton( + style: OutlinedButton.styleFrom( + foregroundColor: theme.colorScheme.error, + side: BorderSide(color: theme.colorScheme.error), + ), + onPressed: () { + // Dispatch AuthenticationSignOutRequested from Auth Bloc + context.read().add( + const AuthenticationSignOutRequested(), + ); + // Global redirect will be handled by AppBloc/GoRouter + }, + child: Text(l10n.accountSignOutTile), + ), + ], + ); + } return Column( children: [ CircleAvatar( radius: 40, - backgroundImage: hasPhoto ? NetworkImage(photoUrl) : null, backgroundColor: theme.colorScheme.primaryContainer, - // Show initials or icon if no photo - child: - !hasPhoto - ? Icon( - Icons.person, - size: 40, - color: theme.colorScheme.onPrimaryContainer, - ) - : null, + child: avatarIcon, ), const SizedBox(height: AppSpacing.lg), // Use AppSpacing Text( - isAnonymous - ? l10n.accountAnonymousUser - : user.displayName ?? l10n.accountNoNameUser, + displayName, style: textTheme.titleLarge, textAlign: TextAlign.center, ), - // Conditionally display Sign In or Logout button - if (isAnonymous) - _buildSignInButton(context) - else - _buildLogoutButton(context), + statusWidget, // Display sign-in button or role/logout button ], ); } - /// Builds the sign-in button for anonymous users. - Widget _buildSignInButton(BuildContext context) { - final l10n = context.l10n; - return Padding( - padding: const EdgeInsets.only(top: AppSpacing.sm), - child: TextButton( - onPressed: () { - // Navigate to the authentication page in linking mode - context.goNamed( - Routes.authenticationName, - queryParameters: {'context': 'linking'}, - ); - }, - child: Text(l10n.accountSignInPromptButton), - ), - ); - } - - /// Builds the logout button for authenticated users. - Widget _buildLogoutButton(BuildContext context) { - final l10n = context.l10n; - final theme = Theme.of(context); - return Padding( - padding: const EdgeInsets.only(top: AppSpacing.sm), - child: OutlinedButton( - style: OutlinedButton.styleFrom( - foregroundColor: theme.colorScheme.error, - side: BorderSide(color: theme.colorScheme.error), - ), - onPressed: () { - context.read().add(const AccountLogoutRequested()); - // Global redirect will be handled by AppBloc/GoRouter - }, - // Assuming l10n.accountSignOutButton exists or will be added - // Reusing existing tile text for now as button text might differ - child: Text(l10n.accountSignOutTile), - ), - ); - } - /// Builds the ListTile for navigating to Settings. Widget _buildSettingsTile(BuildContext context) { final l10n = context.l10n; diff --git a/lib/account/view/content_preferences_page.dart b/lib/account/view/content_preferences_page.dart new file mode 100644 index 00000000..30cceb6e --- /dev/null +++ b/lib/account/view/content_preferences_page.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:ht_main/l10n/l10n.dart'; + +/// {@template content_preferences_page} +/// A placeholder page for managing user content preferences. +/// {@endtemplate} +class ContentPreferencesPage extends StatelessWidget { + /// {@macro content_preferences_page} + const ContentPreferencesPage({super.key}); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return Scaffold( + appBar: AppBar(title: Text(l10n.accountContentPreferencesTile)), + body: const Center(child: Text('CONTENT PREFERENCES PAGE (Placeholder)')), + ); + } +} diff --git a/lib/account/view/saved_headlines_page.dart b/lib/account/view/saved_headlines_page.dart new file mode 100644 index 00000000..8e50e49f --- /dev/null +++ b/lib/account/view/saved_headlines_page.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:ht_main/l10n/l10n.dart'; + +/// {@template saved_headlines_page} +/// A placeholder page for displaying user's saved headlines. +/// {@endtemplate} +class SavedHeadlinesPage extends StatelessWidget { + /// {@macro saved_headlines_page} + const SavedHeadlinesPage({super.key}); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return Scaffold( + appBar: AppBar(title: Text(l10n.accountSavedHeadlinesTile)), + body: const Center(child: Text('SAVED HEADLINES PAGE (Placeholder)')), + ); + } +} diff --git a/lib/app/bloc/app_bloc.dart b/lib/app/bloc/app_bloc.dart index 5785770a..3ba90e87 100644 --- a/lib/app/bloc/app_bloc.dart +++ b/lib/app/bloc/app_bloc.dart @@ -2,69 +2,72 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; -import 'package:flex_color_scheme/flex_color_scheme.dart'; // Added +import 'package:flex_color_scheme/flex_color_scheme.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; -import 'package:ht_authentication_client/ht_authentication_client.dart'; -import 'package:ht_authentication_repository/ht_authentication_repository.dart'; -// Ensure full import for FontSize enum access -import 'package:ht_preferences_client/ht_preferences_client.dart'; -import 'package:ht_preferences_repository/ht_preferences_repository.dart'; +import 'package:ht_auth_repository/ht_auth_repository.dart'; +import 'package:ht_data_repository/ht_data_repository.dart'; +import 'package:ht_shared/ht_shared.dart'; // Import shared models and exceptions part 'app_event.dart'; part 'app_state.dart'; class AppBloc extends Bloc { AppBloc({ - required HtAuthenticationRepository authenticationRepository, - required HtPreferencesRepository preferencesRepository, // Added + required HtAuthRepository authenticationRepository, + required HtDataRepository userAppSettingsRepository, }) : _authenticationRepository = authenticationRepository, - _preferencesRepository = preferencesRepository, // Added + _userAppSettingsRepository = userAppSettingsRepository, // Initialize with default state, load settings after user is known - super(AppState()) { + // Provide a default UserAppSettings instance + super( + const AppState( + settings: UserAppSettings(id: 'default'), + selectedBottomNavigationIndex: 0, + ), + ) { on(_onAppUserChanged); - // Add handler for explicitly refreshing settings if needed later on(_onAppSettingsRefreshed); + on(_onLogoutRequested); + on(_onThemeModeChanged); + on(_onFlexSchemeChanged); + on(_onFontFamilyChanged); + on(_onAppTextScaleFactorChanged); - // Listen directly to the user stream - _userSubscription = _authenticationRepository.user.listen( - (User user) => add(AppUserChanged(user)), // Explicitly type user + // Listen directly to the auth state changes stream + _userSubscription = _authenticationRepository.authStateChanges.listen( + (User? user) => add(AppUserChanged(user)), // Handle nullable user ); } - final HtAuthenticationRepository _authenticationRepository; - final HtPreferencesRepository _preferencesRepository; // Added - late final StreamSubscription _userSubscription; - - // Removed _onAppThemeChanged + final HtAuthRepository _authenticationRepository; + final HtDataRepository _userAppSettingsRepository; + late final StreamSubscription _userSubscription; /// Handles user changes and loads initial settings once user is available. Future _onAppUserChanged( AppUserChanged event, Emitter emit, ) async { - // Determine the AppStatus based on the user's AuthenticationStatus + // Determine the AppStatus based on the user object and its role final AppStatus status; - switch (event.user.authenticationStatus) { - case AuthenticationStatus.unauthenticated: + switch (event.user?.role) { + case null: // User is null (unauthenticated) status = AppStatus.unauthenticated; - // Emit status change immediately for unauthenticated users - emit(state.copyWith(status: status, user: event.user)); - return; // Don't load settings for unauthenticated users - case AuthenticationStatus.anonymous: - status = AppStatus.anonymous; - // Continue to load settings for anonymous - case AuthenticationStatus.authenticated: + case UserRole.standardUser: status = AppStatus.authenticated; - // Continue to load settings for authenticated + // ignore: no_default_cases + default: + status = AppStatus.anonymous; } // Emit user and status update first emit(state.copyWith(status: status, user: event.user)); // Load settings now that we have a user (anonymous or authenticated) - // Use a separate event to avoid complexity within this handler - add(const AppSettingsRefreshed()); + if (event.user != null) { + add(const AppSettingsRefreshed()); + } } /// Handles refreshing/loading app settings (theme, font). @@ -73,100 +76,200 @@ class AppBloc extends Bloc { Emitter emit, ) async { // Avoid loading if user is unauthenticated (shouldn't happen if logic is correct) - if (state.status == AppStatus.unauthenticated) return; + if (state.status == AppStatus.unauthenticated || state.user == null) { + return; + } try { - // Fetch relevant settings - final themeSettings = await _tryFetch( - _preferencesRepository.getThemeSettings, - ); - final appSettings = await _tryFetch( - _preferencesRepository.getAppSettings, + // Fetch relevant settings using the new generic repository + // Use the current user's ID to fetch user-specific settings + final userAppSettings = await _userAppSettingsRepository.read( + id: state.user!.id, + userId: state.user!.id, // Scope to the current user ); - // Map settings to AppState properties - final newThemeMode = _mapAppThemeMode( - themeSettings?.themeMode ?? AppThemeMode.system, // Default + // Map settings from UserAppSettings to AppState properties + final newThemeMode = _mapAppBaseTheme( + userAppSettings.displaySettings.baseTheme, + ); + final newFlexScheme = _mapAppAccentTheme( + userAppSettings.displaySettings.accentTheme, + ); + final newFontFamily = _mapFontFamily( + userAppSettings.displaySettings.fontFamily, ); - final newFlexScheme = _mapAppThemeName( - themeSettings?.themeName ?? AppThemeName.grey, // Default + final newAppTextScaleFactor = _mapTextScaleFactor( + userAppSettings.displaySettings.textScaleFactor, ); - final newFontFamily = _mapAppFontType(appSettings?.appFontType); - // Extract App Font Size - final newAppFontSize = - appSettings?.appFontSize ?? FontSize.medium; // Default emit( state.copyWith( themeMode: newThemeMode, flexScheme: newFlexScheme, - appFontSize: newAppFontSize, // Pass font size + appTextScaleFactor: newAppTextScaleFactor, fontFamily: newFontFamily, - // Use clearFontFamily flag if appSettings was null and we want to reset - clearFontFamily: appSettings == null, + settings: userAppSettings, // Store the fetched settings + ), + ); + } on NotFoundException { + // User settings not found (e.g., first time user), use defaults + print('User app settings not found, using defaults.'); + // Emit state with default settings + emit( + state.copyWith( + themeMode: ThemeMode.system, + flexScheme: FlexScheme.material, + appTextScaleFactor: AppTextScaleFactor.medium, // Default enum value + settings: UserAppSettings( + id: state.user!.id, + ), // Provide default settings ), ); } catch (e) { - // Handle potential errors during settings fetch + // Handle other potential errors during settings fetch // Optionally emit a failure state or log the error - print('Error loading app settings in AppBloc: $e'); - // Keep the existing theme/font state on error + print('Error loading user app settings in AppBloc: $e'); + // Keep the existing theme/font state on error, but ensure settings is not null + emit( + state.copyWith(settings: state.settings), + ); // Ensure settings is present } } - /// Helper to fetch a setting and handle PreferenceNotFoundException gracefully. - Future _tryFetch(Future Function() fetcher) async { - try { - return await fetcher(); - } on PreferenceNotFoundException { - return null; // Setting not found, return null to use default - } catch (e) { - // Rethrow other errors to be caught by the caller - rethrow; - } + // Add handlers for settings changes (dispatching events from UI) + void _onLogoutRequested(AppLogoutRequested event, Emitter emit) { + unawaited(_authenticationRepository.signOut()); + } + + void _onThemeModeChanged(AppThemeModeChanged event, Emitter emit) { + // Update settings and emit new state + final updatedSettings = state.settings.copyWith( + displaySettings: state.settings.displaySettings.copyWith( + baseTheme: + event.themeMode == ThemeMode.light + ? AppBaseTheme.light + : (event.themeMode == ThemeMode.dark + ? AppBaseTheme.dark + : AppBaseTheme.system), + ), + ); + emit(state.copyWith(settings: updatedSettings, themeMode: event.themeMode)); + // Optionally save settings to repository here + // unawaited(_userAppSettingsRepository.update(id: updatedSettings.id, item: updatedSettings)); + } + + void _onFlexSchemeChanged( + AppFlexSchemeChanged event, + Emitter emit, + ) { + // Update settings and emit new state + final updatedSettings = state.settings.copyWith( + displaySettings: state.settings.displaySettings.copyWith( + accentTheme: + event.flexScheme == FlexScheme.blue + ? AppAccentTheme.defaultBlue + : (event.flexScheme == FlexScheme.red + ? AppAccentTheme.newsRed + : AppAccentTheme + .graphiteGray), // Mapping material to graphiteGray + ), + ); + emit( + state.copyWith(settings: updatedSettings, flexScheme: event.flexScheme), + ); + // Optionally save settings to repository here + // unawaited(_userAppSettingsRepository.update(id: updatedSettings.id, item: updatedSettings)); + } + + void _onFontFamilyChanged( + AppFontFamilyChanged event, + Emitter emit, + ) { + // Update settings and emit new state + final updatedSettings = state.settings.copyWith( + displaySettings: state.settings.displaySettings.copyWith( + fontFamily: + event.fontFamily ?? 'SystemDefault', // Map null to 'SystemDefault' + ), + ); + emit( + state.copyWith(settings: updatedSettings, fontFamily: event.fontFamily), + ); + // Optionally save settings to repository here + // unawaited(_userAppSettingsRepository.update(id: updatedSettings.id, item: updatedSettings)); + } + + void _onAppTextScaleFactorChanged( + AppTextScaleFactorChanged event, + Emitter emit, + ) { + // Update settings and emit new state + final updatedSettings = state.settings.copyWith( + displaySettings: state.settings.displaySettings.copyWith( + textScaleFactor: event.appTextScaleFactor, + ), + ); + emit( + state.copyWith( + settings: updatedSettings, + appTextScaleFactor: event.appTextScaleFactor, + ), + ); + // Optionally save settings to repository here + // unawaited(_userAppSettingsRepository.update(id: updatedSettings.id, item: updatedSettings)); } // --- Settings Mapping Helpers --- - ThemeMode _mapAppThemeMode(AppThemeMode mode) { + ThemeMode _mapAppBaseTheme(AppBaseTheme mode) { switch (mode) { - case AppThemeMode.light: + case AppBaseTheme.light: return ThemeMode.light; - case AppThemeMode.dark: + case AppBaseTheme.dark: return ThemeMode.dark; - case AppThemeMode.system: + case AppBaseTheme.system: return ThemeMode.system; } } - FlexScheme _mapAppThemeName(AppThemeName name) { + FlexScheme _mapAppAccentTheme(AppAccentTheme name) { switch (name) { - case AppThemeName.red: - return FlexScheme.red; - case AppThemeName.blue: + case AppAccentTheme.defaultBlue: return FlexScheme.blue; - case AppThemeName.grey: - return FlexScheme.material; // Default grey maps to material + case AppAccentTheme.newsRed: + return FlexScheme.red; + case AppAccentTheme.graphiteGray: + return FlexScheme.material; // Mapping graphiteGray to material for now } } - String? _mapAppFontType(AppFontType? type) { - if (type == null) return null; // Use theme default if null + String? _mapFontFamily(String fontFamily) { + // Assuming 'SystemDefault' means use the theme's default font + if (fontFamily == 'SystemDefault') return null; - switch (type) { - case AppFontType.roboto: + // Map specific font family names to GoogleFonts + switch (fontFamily) { + case 'Roboto': return GoogleFonts.roboto().fontFamily; - case AppFontType.openSans: + case 'OpenSans': return GoogleFonts.openSans().fontFamily; - case AppFontType.lato: + case 'Lato': return GoogleFonts.lato().fontFamily; - case AppFontType.montserrat: + case 'Montserrat': return GoogleFonts.montserrat().fontFamily; - case AppFontType.merriweather: + case 'Merriweather': return GoogleFonts.merriweather().fontFamily; + default: + // If an unknown font family is specified, fall back to theme default + return null; } } + // Map AppTextScaleFactor to AppTextScaleFactor (no change needed) + AppTextScaleFactor _mapTextScaleFactor(AppTextScaleFactor factor) { + return factor; + } + @override Future close() { _userSubscription.cancel(); diff --git a/lib/app/bloc/app_event.dart b/lib/app/bloc/app_event.dart index b71a2197..68aaf5a0 100644 --- a/lib/app/bloc/app_event.dart +++ b/lib/app/bloc/app_event.dart @@ -4,7 +4,7 @@ abstract class AppEvent extends Equatable { const AppEvent(); @override - List get props => []; + List get props => []; // Allow nullable objects in props } @Deprecated('Use SettingsBloc events instead') @@ -17,10 +17,10 @@ class AppThemeChanged extends AppEvent { class AppUserChanged extends AppEvent { const AppUserChanged(this.user); - final User user; + final User? user; // Make user nullable @override - List get props => [user]; + List get props => [user]; // Update props to handle nullable } /// {@template app_settings_refreshed} @@ -31,3 +31,63 @@ class AppSettingsRefreshed extends AppEvent { /// {@macro app_settings_refreshed} const AppSettingsRefreshed(); } + +/// {@template app_logout_requested} +/// Event to request user logout. +/// {@endtemplate} +class AppLogoutRequested extends AppEvent { + /// {@macro app_logout_requested} + const AppLogoutRequested(); +} + +/// {@template app_theme_mode_changed} +/// Event to change the application's theme mode. +/// {@endtemplate} +class AppThemeModeChanged extends AppEvent { + /// {@macro app_theme_mode_changed} + const AppThemeModeChanged(this.themeMode); + + final ThemeMode themeMode; + + @override + List get props => [themeMode]; +} + +/// {@template app_flex_scheme_changed} +/// Event to change the application's FlexColorScheme. +/// {@endtemplate} +class AppFlexSchemeChanged extends AppEvent { + /// {@macro app_flex_scheme_changed} + const AppFlexSchemeChanged(this.flexScheme); + + final FlexScheme flexScheme; + + @override + List get props => [flexScheme]; +} + +/// {@template app_font_family_changed} +/// Event to change the application's font family. +/// {@endtemplate} +class AppFontFamilyChanged extends AppEvent { + /// {@macro app_font_family_changed} + const AppFontFamilyChanged(this.fontFamily); + + final String? fontFamily; + + @override + List get props => [fontFamily]; +} + +/// {@template app_text_scale_factor_changed} +/// Event to change the application's text scale factor. +/// {@endtemplate} +class AppTextScaleFactorChanged extends AppEvent { + /// {@macro app_text_scale_factor_changed} + const AppTextScaleFactorChanged(this.appTextScaleFactor); + + final AppTextScaleFactor appTextScaleFactor; + + @override + List get props => [appTextScaleFactor]; +} diff --git a/lib/app/bloc/app_state.dart b/lib/app/bloc/app_state.dart index df532567..3ef5a2d8 100644 --- a/lib/app/bloc/app_state.dart +++ b/lib/app/bloc/app_state.dart @@ -8,7 +8,7 @@ enum AppStatus { /// The user is authenticated. authenticated, - /// The user is not authenticated. + /// The user is unauthenticated. unauthenticated, /// The user is anonymous (signed in using an anonymous provider). @@ -17,15 +17,17 @@ enum AppStatus { class AppState extends Equatable { /// {@macro app_state} - AppState({ - this.selectedBottomNavigationIndex = 0, + const AppState({ + required this.settings, // Add settings property + required this.selectedBottomNavigationIndex, this.themeMode = ThemeMode.system, - this.appFontSize = FontSize.medium, // Default font size + this.appTextScaleFactor = + AppTextScaleFactor.medium, // Default text scale factor (enum) this.flexScheme = FlexScheme.material, this.fontFamily, this.status = AppStatus.initial, - User? user, - }) : user = user ?? User(); + this.user, // User is now nullable and defaults to null + }); /// The index of the currently selected item in the bottom navigation bar. final int selectedBottomNavigationIndex; @@ -33,8 +35,8 @@ class AppState extends Equatable { /// The overall theme mode (light, dark, system). final ThemeMode themeMode; - /// The font size for the app's UI. - final FontSize appFontSize; + /// The text scale factor for the app's UI. + final AppTextScaleFactor appTextScaleFactor; // Change type to enum /// The active color scheme defined by FlexColorScheme. final FlexScheme flexScheme; @@ -46,8 +48,11 @@ class AppState extends Equatable { /// The current authentication status of the application. final AppStatus status; - /// The current user details. Defaults to an empty user. - final User user; + /// The current user details. Null if unauthenticated. + final User? user; + + /// User-specific application settings. + final UserAppSettings settings; // Add settings property /// Creates a copy of the current state with updated values. AppState copyWith({ @@ -55,9 +60,10 @@ class AppState extends Equatable { ThemeMode? themeMode, FlexScheme? flexScheme, String? fontFamily, - FontSize? appFontSize, // Added + AppTextScaleFactor? appTextScaleFactor, // Change type to enum AppStatus? status, User? user, + UserAppSettings? settings, // Add settings to copyWith bool clearFontFamily = false, }) { return AppState( @@ -66,9 +72,10 @@ class AppState extends Equatable { themeMode: themeMode ?? this.themeMode, flexScheme: flexScheme ?? this.flexScheme, fontFamily: clearFontFamily ? null : fontFamily ?? this.fontFamily, - appFontSize: appFontSize ?? this.appFontSize, // Added + appTextScaleFactor: appTextScaleFactor ?? this.appTextScaleFactor, status: status ?? this.status, user: user ?? this.user, + settings: settings ?? this.settings, // Copy settings ); } @@ -78,8 +85,9 @@ class AppState extends Equatable { themeMode, flexScheme, fontFamily, - appFontSize, // Added + appTextScaleFactor, status, user, + settings, // Include settings in props ]; } diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index acfe8508..9b0335ce 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -1,33 +1,30 @@ // // ignore_for_file: deprecated_member_use -import 'dart:async'; - -import 'package:firebase_dynamic_links/firebase_dynamic_links.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; -import 'package:ht_authentication_repository/ht_authentication_repository.dart'; -import 'package:ht_categories_repository/ht_categories_repository.dart'; -import 'package:ht_countries_repository/ht_countries_repository.dart'; -import 'package:ht_headlines_repository/ht_headlines_repository.dart'; -import 'package:ht_kv_storage_service/ht_kv_storage_service.dart'; +import 'package:ht_auth_repository/ht_auth_repository.dart'; // Auth Repository +import 'package:ht_data_repository/ht_data_repository.dart'; // Generic Data Repository +import 'package:ht_kv_storage_service/ht_kv_storage_service.dart'; // KV Storage Interface import 'package:ht_main/app/bloc/app_bloc.dart'; import 'package:ht_main/authentication/bloc/authentication_bloc.dart'; import 'package:ht_main/l10n/l10n.dart'; import 'package:ht_main/router/router.dart'; import 'package:ht_main/shared/theme/app_theme.dart'; -import 'package:ht_preferences_repository/ht_preferences_repository.dart'; // Added -import 'package:ht_sources_repository/ht_sources_repository.dart'; +import 'package:ht_shared/ht_shared.dart'; // Shared models, FromJson, ToJson, etc. class App extends StatelessWidget { const App({ - required HtAuthenticationRepository htAuthenticationRepository, - required HtHeadlinesRepository htHeadlinesRepository, - required HtCategoriesRepository htCategoriesRepository, - required HtCountriesRepository htCountriesRepository, - required HtSourcesRepository htSourcesRepository, - required HtPreferencesRepository htPreferencesRepository, // Added + required HtAuthRepository htAuthenticationRepository, + required HtDataRepository htHeadlinesRepository, + required HtDataRepository htCategoriesRepository, + required HtDataRepository htCountriesRepository, + required HtDataRepository htSourcesRepository, + required HtDataRepository htUserAppSettingsRepository, + required HtDataRepository + htUserContentPreferencesRepository, + required HtDataRepository htAppConfigRepository, required HtKVStorageService kvStorageService, super.key, }) : _htAuthenticationRepository = htAuthenticationRepository, @@ -35,15 +32,20 @@ class App extends StatelessWidget { _htCategoriesRepository = htCategoriesRepository, _htCountriesRepository = htCountriesRepository, _htSourcesRepository = htSourcesRepository, - _htPreferencesRepository = htPreferencesRepository, // Added + _htUserAppSettingsRepository = htUserAppSettingsRepository, + _htUserContentPreferencesRepository = htUserContentPreferencesRepository, + _htAppConfigRepository = htAppConfigRepository, _kvStorageService = kvStorageService; - final HtAuthenticationRepository _htAuthenticationRepository; - final HtHeadlinesRepository _htHeadlinesRepository; - final HtCategoriesRepository _htCategoriesRepository; - final HtCountriesRepository _htCountriesRepository; - final HtSourcesRepository _htSourcesRepository; - final HtPreferencesRepository _htPreferencesRepository; // Added + final HtAuthRepository _htAuthenticationRepository; + final HtDataRepository _htHeadlinesRepository; + final HtDataRepository _htCategoriesRepository; + final HtDataRepository _htCountriesRepository; + final HtDataRepository _htSourcesRepository; + final HtDataRepository _htUserAppSettingsRepository; + final HtDataRepository + _htUserContentPreferencesRepository; + final HtDataRepository _htAppConfigRepository; final HtKVStorageService _kvStorageService; @override @@ -55,26 +57,28 @@ class App extends StatelessWidget { RepositoryProvider.value(value: _htCategoriesRepository), RepositoryProvider.value(value: _htCountriesRepository), RepositoryProvider.value(value: _htSourcesRepository), - RepositoryProvider.value(value: _htPreferencesRepository), // Added + RepositoryProvider.value(value: _htUserAppSettingsRepository), + RepositoryProvider.value(value: _htUserContentPreferencesRepository), + RepositoryProvider.value(value: _htAppConfigRepository), RepositoryProvider.value(value: _kvStorageService), ], // Use MultiBlocProvider to provide global BLoCs child: MultiBlocProvider( providers: [ BlocProvider( + // AppBloc constructor needs refactoring in Step 4 create: (context) => AppBloc( - authenticationRepository: - context.read(), - preferencesRepository: - context.read(), // Added + authenticationRepository: context.read(), + // Pass generic data repositories for preferences + userAppSettingsRepository: + context.read>(), ), ), BlocProvider( create: (context) => AuthenticationBloc( - authenticationRepository: - context.read(), + authenticationRepository: context.read(), ), ), ], @@ -84,7 +88,10 @@ class App extends StatelessWidget { htCategoriesRepository: _htCategoriesRepository, htCountriesRepository: _htCountriesRepository, htSourcesRepository: _htSourcesRepository, - htPreferencesRepository: _htPreferencesRepository, // Pass down + htUserAppSettingsRepository: _htUserAppSettingsRepository, + htUserContentPreferencesRepository: + _htUserContentPreferencesRepository, + htAppConfigRepository: _htAppConfigRepository, ), ), ); @@ -98,15 +105,20 @@ class _AppView extends StatefulWidget { required this.htCategoriesRepository, required this.htCountriesRepository, required this.htSourcesRepository, - required this.htPreferencesRepository, // Added + required this.htUserAppSettingsRepository, + required this.htUserContentPreferencesRepository, + required this.htAppConfigRepository, }); - final HtAuthenticationRepository htAuthenticationRepository; - final HtHeadlinesRepository htHeadlinesRepository; - final HtCategoriesRepository htCategoriesRepository; - final HtCountriesRepository htCountriesRepository; - final HtSourcesRepository htSourcesRepository; - final HtPreferencesRepository htPreferencesRepository; // Added + final HtAuthRepository htAuthenticationRepository; + final HtDataRepository htHeadlinesRepository; + final HtDataRepository htCategoriesRepository; + final HtDataRepository htCountriesRepository; + final HtDataRepository htSourcesRepository; + final HtDataRepository htUserAppSettingsRepository; + final HtDataRepository + htUserContentPreferencesRepository; + final HtDataRepository htAppConfigRepository; @override State<_AppView> createState() => _AppViewState(); @@ -116,7 +128,7 @@ class _AppViewState extends State<_AppView> { late final GoRouter _router; // Standard notifier that GoRouter listens to. late final ValueNotifier _statusNotifier; - StreamSubscription? _linkSubscription; + // Removed Dynamic Links subscription @override void initState() { @@ -131,89 +143,23 @@ class _AppViewState extends State<_AppView> { htCategoriesRepository: widget.htCategoriesRepository, htCountriesRepository: widget.htCountriesRepository, htSourcesRepository: widget.htSourcesRepository, - htPreferencesRepository: widget.htPreferencesRepository, // Pass to router + htUserAppSettingsRepository: widget.htUserAppSettingsRepository, + htUserContentPreferencesRepository: + widget.htUserContentPreferencesRepository, + htAppConfigRepository: widget.htAppConfigRepository, ); - // --- Initialize Deep Link Handling --- - _initDynamicLinks(); - // ------------------------------------- + // Removed Dynamic Link Initialization } @override void dispose() { - _linkSubscription?.cancel(); _statusNotifier.dispose(); // Dispose the correct notifier + // Removed Dynamic Links subscription cancellation super.dispose(); } - /// Initializes Firebase Dynamic Links listeners. - Future _initDynamicLinks() async { - // Handle links received while the app is running - _linkSubscription = FirebaseDynamicLinks.instance.onLink.listen( - (pendingDynamicLinkData) { - _handleDynamicLink(pendingDynamicLinkData.link); - }, - onError: (Object error) { - debugPrint('Dynamic Link Listener Error: $error'); - }, - ); - - // Handle initial link that opened the app - try { - final initialLink = await FirebaseDynamicLinks.instance.getInitialLink(); - if (initialLink != null) { - await _handleDynamicLink(initialLink.link); - } - } catch (e) { - debugPrint('Error getting initial dynamic link: $e'); - } - } - - /// Processes a received dynamic link URI. - Future _handleDynamicLink(Uri deepLink) async { - // Read BLoC and Repo before the first await. - // The mounted check should ideally happen *before* accessing context. - if (!mounted) return; - final authRepo = context.read(); - // Store the BLoC instance in a local variable BEFORE the await - final authBloc = context.read(); - final linkString = deepLink.toString(); - - debugPrint('Handling dynamic link: $linkString'); - - try { - // The async gap happens here - final isSignInLink = await authRepo.isSignInWithEmailLink( - emailLink: linkString, - ); - debugPrint('Is sign-in link: $isSignInLink'); - - // Check mounted again *after* the await before using BLoC/state - if (!mounted) return; - - if (isSignInLink) { - // Use the local variable 'authBloc' instead of context.read again - authBloc.add( - AuthenticationSignInWithLinkAttempted(emailLink: linkString), - ); - debugPrint('Dispatched AuthenticationSignInWithLinkAttempted'); - } else { - // Handle other types of deep links if necessary - debugPrint( - 'Received deep link is not an email sign-in link: $linkString', - ); - } - } catch (e, st) { - // Catch potential errors during validation/dispatch - debugPrint('Error handling dynamic link: $e\n$st'); - // Optionally show a generic error message to the user via - // a SnackBar or Dialog: - // - // ScaffoldMessenger.of(context).showSnackBar( - // SnackBar(content: Text('Error processing link: $e')), - // ); - } - } + // Removed _initDynamicLinks and _handleDynamicLink methods @override Widget build(BuildContext context) { @@ -229,13 +175,14 @@ class _AppViewState extends State<_AppView> { _statusNotifier.value = state.status; }, child: BlocBuilder( - // Build when theme-related properties change (including font size) + // Build when theme-related properties change (including text scale factor) buildWhen: (previous, current) => previous.themeMode != current.themeMode || previous.flexScheme != current.flexScheme || previous.fontFamily != current.fontFamily || - previous.appFontSize != current.appFontSize, // Added condition + previous.appTextScaleFactor != + current.appTextScaleFactor, // Use text scale factor builder: (context, state) { return MaterialApp.router( debugShowCheckedModeBanner: false, @@ -243,13 +190,15 @@ class _AppViewState extends State<_AppView> { // Pass scheme and font family from state to theme functions theme: lightTheme( scheme: state.flexScheme, - fontFamily: state.fontFamily, - appFontSize: state.appFontSize, // Pass font size + appTextScaleFactor: + state.settings.displaySettings.textScaleFactor, + fontFamily: state.settings.displaySettings.fontFamily, ), darkTheme: darkTheme( scheme: state.flexScheme, - fontFamily: state.fontFamily, - appFontSize: state.appFontSize, // Pass font size + appTextScaleFactor: + state.settings.displaySettings.textScaleFactor, + fontFamily: state.settings.displaySettings.fontFamily, ), routerConfig: _router, localizationsDelegates: AppLocalizations.localizationsDelegates, diff --git a/lib/authentication/bloc/authentication_bloc.dart b/lib/authentication/bloc/authentication_bloc.dart index 53ec79f5..f0f01dea 100644 --- a/lib/authentication/bloc/authentication_bloc.dart +++ b/lib/authentication/bloc/authentication_bloc.dart @@ -1,12 +1,17 @@ -// -// ignore_for_file: lines_longer_than_80_chars - import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; -import 'package:ht_authentication_client/ht_authentication_client.dart'; -import 'package:ht_authentication_repository/ht_authentication_repository.dart'; +import 'package:ht_auth_repository/ht_auth_repository.dart'; +import 'package:ht_shared/ht_shared.dart' + show + AuthenticationException, + HtHttpException, + InvalidInputException, + NetworkException, + OperationFailedException, + ServerException, + User; part 'authentication_event.dart'; part 'authentication_state.dart'; @@ -17,33 +22,42 @@ part 'authentication_state.dart'; class AuthenticationBloc extends Bloc { /// {@macro authentication_bloc} - AuthenticationBloc({ - required HtAuthenticationRepository authenticationRepository, - }) : _authenticationRepository = authenticationRepository, - super(AuthenticationInitial()) { - on( - _onAuthenticationSendSignInLinkRequested, - ); - on( - _onAuthenticationSignInWithLinkAttempted, + AuthenticationBloc({required HtAuthRepository authenticationRepository}) + : _authenticationRepository = authenticationRepository, + super(AuthenticationInitial()) { + // Listen to authentication state changes from the repository + _authenticationRepository.authStateChanges.listen( + (user) => add(_AuthenticationUserChanged(user: user)), ); - on( - _onAuthenticationGoogleSignInRequested, + + on<_AuthenticationUserChanged>(_onAuthenticationUserChanged); + on( + _onAuthenticationRequestSignInCodeRequested, ); + on(_onAuthenticationVerifyCodeRequested); on( _onAuthenticationAnonymousSignInRequested, ); on(_onAuthenticationSignOutRequested); - on( - _onAuthenticationDeleteAccountRequested, - ); } - final HtAuthenticationRepository _authenticationRepository; + final HtAuthRepository _authenticationRepository; - /// Handles [AuthenticationSendSignInLinkRequested] events. - Future _onAuthenticationSendSignInLinkRequested( - AuthenticationSendSignInLinkRequested event, + /// Handles [_AuthenticationUserChanged] events. + Future _onAuthenticationUserChanged( + _AuthenticationUserChanged event, + Emitter emit, + ) async { + if (event.user != null) { + emit(AuthenticationAuthenticated(user: event.user!)); + } else { + emit(AuthenticationUnauthenticated()); + } + } + + /// Handles [AuthenticationRequestSignInCodeRequested] events. + Future _onAuthenticationRequestSignInCodeRequested( + AuthenticationRequestSignInCodeRequested event, Emitter emit, ) async { // Validate email format (basic check) @@ -51,14 +65,25 @@ class AuthenticationBloc emit(const AuthenticationFailure('Please enter a valid email address.')); return; } - emit(AuthenticationLinkSending()); // Indicate link sending + emit( + AuthenticationRequestCodeLoading(), + ); // Indicate code request is sending try { - // Simply call the repository method, email temprary storage storage - // is handled internally - await _authenticationRepository.sendSignInLinkToEmail(email: event.email); - emit(AuthenticationLinkSentSuccess()); // Confirm link sent - } on SendSignInLinkException catch (e) { - emit(AuthenticationFailure('Failed to send link: ${e.error}')); + await _authenticationRepository.requestSignInCode(event.email); + emit( + AuthenticationCodeSentSuccess(email: event.email), + ); // Confirm code requested and include email + } on InvalidInputException catch (e) { + emit(AuthenticationFailure('Invalid input: ${e.message}')); + } on NetworkException catch (_) { + emit(const AuthenticationFailure('Network error occurred.')); + } on ServerException catch (e) { + emit(AuthenticationFailure('Server error: ${e.message}')); + } on OperationFailedException catch (e) { + emit(AuthenticationFailure('Operation failed: ${e.message}')); + } on HtHttpException catch (e) { + // Catch any other HtHttpException subtypes + emit(AuthenticationFailure('HTTP error: ${e.message}')); } catch (e) { // Catch any other unexpected errors emit(AuthenticationFailure('An unexpected error occurred: $e')); @@ -66,67 +91,57 @@ class AuthenticationBloc } } - /// Handles [AuthenticationSignInWithLinkAttempted] events. - /// This assumes the event is dispatched after the app receives the deep link. - Future _onAuthenticationSignInWithLinkAttempted( - AuthenticationSignInWithLinkAttempted event, + /// Handles [AuthenticationVerifyCodeRequested] events. + Future _onAuthenticationVerifyCodeRequested( + AuthenticationVerifyCodeRequested event, Emitter emit, ) async { - emit(AuthenticationLoading()); // General loading for sign-in attempt + emit(AuthenticationLoading()); // Indicate code verification is loading try { - // Call the updated repository method (no email needed here) - await _authenticationRepository.signInWithEmailLink( - emailLink: event.emailLink, - ); - // On success, AppBloc should react to the user stream change from the repo. - // Resetting to Initial state here. - emit(AuthenticationInitial()); - } on InvalidSignInLinkException catch (e) { - emit( - AuthenticationFailure( - 'Sign in failed: Invalid or expired link. ${e.error}', - ), - ); + await _authenticationRepository.verifySignInCode(event.email, event.code); + // On success, the _AuthenticationUserChanged listener will handle + // emitting AuthenticationAuthenticated. + } on InvalidInputException catch (e) { + emit(AuthenticationFailure('Invalid input: ${e.message}')); + } on AuthenticationException catch (e) { + emit(AuthenticationFailure('Authentication failed: ${e.message}')); + } on NetworkException catch (_) { + emit(const AuthenticationFailure('Network error occurred.')); + } on ServerException catch (e) { + emit(AuthenticationFailure('Server error: ${e.message}')); + } on OperationFailedException catch (e) { + emit(AuthenticationFailure('Operation failed: ${e.message}')); + } on HtHttpException catch (e) { + // Catch any other HtHttpException subtypes + emit(AuthenticationFailure('HTTP error: ${e.message}')); } catch (e) { // Catch any other unexpected errors - emit( - AuthenticationFailure( - 'An unexpected error occurred during sign in: $e', - ), - ); + emit(AuthenticationFailure('An unexpected error occurred: $e')); // Optionally log the stackTrace here } } - /// Handles [AuthenticationGoogleSignInRequested] events. - Future _onAuthenticationGoogleSignInRequested( - AuthenticationGoogleSignInRequested event, - Emitter emit, - ) async { - emit(AuthenticationLoading()); - try { - await _authenticationRepository.signInWithGoogle(); - emit(AuthenticationInitial()); - } on GoogleSignInException catch (e) { - emit(AuthenticationFailure(e.toString())); - } catch (e) { - emit(AuthenticationFailure(e.toString())); - } - } - /// Handles [AuthenticationAnonymousSignInRequested] events. Future _onAuthenticationAnonymousSignInRequested( AuthenticationAnonymousSignInRequested event, Emitter emit, ) async { - emit(AuthenticationLoading()); + emit(AuthenticationLoading()); // Indicate anonymous sign-in is loading try { await _authenticationRepository.signInAnonymously(); - emit(AuthenticationInitial()); - } on AnonymousLoginException catch (e) { - emit(AuthenticationFailure(e.toString())); + // On success, the _AuthenticationUserChanged listener will handle + // emitting AuthenticationAuthenticated. + } on NetworkException catch (_) { + emit(const AuthenticationFailure('Network error occurred.')); + } on ServerException catch (e) { + emit(AuthenticationFailure('Server error: ${e.message}')); + } on OperationFailedException catch (e) { + emit(AuthenticationFailure('Operation failed: ${e.message}')); + } on HtHttpException catch (e) { + // Catch any other HtHttpException subtypes + emit(AuthenticationFailure('HTTP error: ${e.message}')); } catch (e) { - emit(AuthenticationFailure(e.toString())); + emit(AuthenticationFailure('An unexpected error occurred: $e')); } } @@ -135,29 +150,22 @@ class AuthenticationBloc AuthenticationSignOutRequested event, Emitter emit, ) async { - emit(AuthenticationLoading()); + emit(AuthenticationLoading()); // Indicate sign-out is loading try { await _authenticationRepository.signOut(); - emit(AuthenticationInitial()); - } on LogoutException catch (e) { - emit(AuthenticationFailure(e.toString())); - } catch (e) { - emit(AuthenticationFailure(e.toString())); - } - } - - Future _onAuthenticationDeleteAccountRequested( - AuthenticationDeleteAccountRequested event, - Emitter emit, - ) async { - emit(AuthenticationLoading()); - try { - await _authenticationRepository.deleteAccount(); - emit(AuthenticationInitial()); - } on DeleteAccountException catch (e) { - emit(AuthenticationFailure(e.toString())); + // On success, the _AuthenticationUserChanged listener will handle + // emitting AuthenticationUnauthenticated. + } on NetworkException catch (_) { + emit(const AuthenticationFailure('Network error occurred.')); + } on ServerException catch (e) { + emit(AuthenticationFailure('Server error: ${e.message}')); + } on OperationFailedException catch (e) { + emit(AuthenticationFailure('Operation failed: ${e.message}')); + } on HtHttpException catch (e) { + // Catch any other HtHttpException subtypes + emit(AuthenticationFailure('HTTP error: ${e.message}')); } catch (e) { - emit(AuthenticationFailure(e.toString())); + emit(AuthenticationFailure('An unexpected error occurred: $e')); } } } diff --git a/lib/authentication/bloc/authentication_event.dart b/lib/authentication/bloc/authentication_event.dart index 9241ac71..ac8e7608 100644 --- a/lib/authentication/bloc/authentication_event.dart +++ b/lib/authentication/bloc/authentication_event.dart @@ -8,16 +8,17 @@ sealed class AuthenticationEvent extends Equatable { const AuthenticationEvent(); @override - List get props => []; + List get props => []; } -/// {@template authentication_send_sign_in_link_requested} -/// Event triggered when the user requests a sign-in link to be sent +/// {@template authentication_request_sign_in_code_requested} +/// Event triggered when the user requests a sign-in code to be sent /// to their email. /// {@endtemplate} -final class AuthenticationSendSignInLinkRequested extends AuthenticationEvent { - /// {@macro authentication_send_sign_in_link_requested} - const AuthenticationSendSignInLinkRequested({required this.email}); +final class AuthenticationRequestSignInCodeRequested + extends AuthenticationEvent { + /// {@macro authentication_request_sign_in_code_requested} + const AuthenticationRequestSignInCodeRequested({required this.email}); /// The user's email address. final String email; @@ -26,27 +27,24 @@ final class AuthenticationSendSignInLinkRequested extends AuthenticationEvent { List get props => [email]; } -/// {@template authentication_sign_in_with_link_attempted} -/// Event triggered when the app attempts to sign in using an email link. -/// This is typically triggered by a deep link handler. +/// {@template authentication_verify_code_requested} +/// Event triggered when the user attempts to sign in using an email and code. /// {@endtemplate} -final class AuthenticationSignInWithLinkAttempted extends AuthenticationEvent { - /// {@macro authentication_sign_in_with_link_attempted} - const AuthenticationSignInWithLinkAttempted({required this.emailLink}); +final class AuthenticationVerifyCodeRequested extends AuthenticationEvent { + /// {@macro authentication_verify_code_requested} + const AuthenticationVerifyCodeRequested({ + required this.email, + required this.code, + }); - /// The sign-in link received by the app. - final String emailLink; + /// The user's email address. + final String email; - @override - List get props => [emailLink]; -} + /// The verification code received by the user. + final String code; -/// {@template authentication_google_sign_in_requested} -/// Event triggered when the user requests to sign in with Google. -/// {@endtemplate} -final class AuthenticationGoogleSignInRequested extends AuthenticationEvent { - /// {@macro authentication_google_sign_in_requested} - const AuthenticationGoogleSignInRequested(); + @override + List get props => [email, code]; } /// {@template authentication_anonymous_sign_in_requested} @@ -65,10 +63,16 @@ final class AuthenticationSignOutRequested extends AuthenticationEvent { const AuthenticationSignOutRequested(); } -/// {@template authentication_delete_account_requested} -/// Event triggered when the user requests to delete their account. +/// {@template _authentication_user_changed} +/// Internal event triggered when the authentication state changes. /// {@endtemplate} -final class AuthenticationDeleteAccountRequested extends AuthenticationEvent { - /// {@macro authentication_delete_account_requested} - const AuthenticationDeleteAccountRequested(); +final class _AuthenticationUserChanged extends AuthenticationEvent { + /// {@macro _authentication_user_changed} + const _AuthenticationUserChanged({required this.user}); + + /// The current authenticated user, or null if unauthenticated. + final User? user; + + @override + List get props => [user]; } diff --git a/lib/authentication/bloc/authentication_state.dart b/lib/authentication/bloc/authentication_state.dart index e2e5ac5c..67c16504 100644 --- a/lib/authentication/bloc/authentication_state.dart +++ b/lib/authentication/bloc/authentication_state.dart @@ -26,7 +26,7 @@ final class AuthenticationLoading extends AuthenticationState {} /// {@endtemplate} final class AuthenticationAuthenticated extends AuthenticationState { /// {@macro authentication_authenticated} - const AuthenticationAuthenticated(this.user); + const AuthenticationAuthenticated({required this.user}); /// The authenticated [User] object. final User user; @@ -40,15 +40,24 @@ final class AuthenticationAuthenticated extends AuthenticationState { /// {@endtemplate} final class AuthenticationUnauthenticated extends AuthenticationState {} -/// {@template authentication_link_sending} -/// State indicating that the sign-in link is being sent. +/// {@template authentication_request_code_loading} +/// State indicating that the sign-in code is being requested. /// {@endtemplate} -final class AuthenticationLinkSending extends AuthenticationState {} +final class AuthenticationRequestCodeLoading extends AuthenticationState {} -/// {@template authentication_link_sent_success} -/// State indicating that the sign-in link was sent successfully. +/// {@template authentication_code_sent_success} +/// State indicating that the sign-in code was sent successfully. /// {@endtemplate} -final class AuthenticationLinkSentSuccess extends AuthenticationState {} +final class AuthenticationCodeSentSuccess extends AuthenticationState { + /// {@macro authentication_code_sent_success} + const AuthenticationCodeSentSuccess({required this.email}); + + /// The email address the code was sent to. + final String email; + + @override + List get props => [email]; +} /// {@template authentication_failure} /// Represents an authentication failure. diff --git a/lib/authentication/view/authentication_page.dart b/lib/authentication/view/authentication_page.dart index 9fb876e0..878d7718 100644 --- a/lib/authentication/view/authentication_page.dart +++ b/lib/authentication/view/authentication_page.dart @@ -127,21 +127,6 @@ class AuthenticationPage extends StatelessWidget { textAlign: TextAlign.center, ), const SizedBox(height: AppSpacing.xxl), - // --- Google Sign-In Button --- - ElevatedButton.icon( - icon: const Icon( - Icons.g_mobiledata, - ), // Placeholder icon - label: Text(l10n.authenticationGoogleSignInButton), - onPressed: - isLoading - ? null - : () => context.read().add( - const AuthenticationGoogleSignInRequested(), - ), - // Style adjustments can be made via ElevatedButtonThemeData - ), - const SizedBox(height: AppSpacing.lg), // --- Email Sign-In Button --- ElevatedButton( // Consider an email icon @@ -153,7 +138,7 @@ class AuthenticationPage extends StatelessWidget { // Navigate to the dedicated email sign-in page, // passing the linking context via 'extra'. context.goNamed( - Routes.emailSignInName, + Routes.requestCodeName, extra: isLinkingContext, ); }, @@ -178,7 +163,8 @@ class AuthenticationPage extends StatelessWidget { // --- Loading Indicator (Optional, for general loading state) --- // If needed, show a general loading indicator when state is AuthenticationLoading - if (isLoading && state is! AuthenticationLinkSending) ...[ + if (isLoading && + state is! AuthenticationRequestCodeLoading) ...[ const SizedBox(height: AppSpacing.xl), const Center(child: CircularProgressIndicator()), ], diff --git a/lib/authentication/view/email_code_verification_page.dart b/lib/authentication/view/email_code_verification_page.dart new file mode 100644 index 00000000..03e1560c --- /dev/null +++ b/lib/authentication/view/email_code_verification_page.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart'; +import 'package:ht_main/l10n/l10n.dart'; +import 'package:ht_main/shared/constants/app_spacing.dart'; + +/// {@template email_code_verification_page} +/// Page where the user enters the 6-digit code sent to their email +/// to complete the sign-in or account linking process. +/// {@endtemplate} +class EmailCodeVerificationPage extends StatelessWidget { + /// {@macro email_code_verification_page} + const EmailCodeVerificationPage({required this.email, super.key}); + + /// The email address the sign-in code was sent to. + final String email; + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final textTheme = Theme.of(context).textTheme; + + return Scaffold( + appBar: AppBar( + title: Text(l10n.emailCodeSentPageTitle), // Updated l10n key + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(AppSpacing.paddingLarge), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.mark_email_read_outlined, // Suggestive icon + size: 80, + // Consider using theme color + // color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(height: AppSpacing.xl), + Text( + l10n.emailCodeSentConfirmation(email), // Pass email to l10n + style: textTheme.titleLarge, // Prominent text style + textAlign: TextAlign.center, + ), + const SizedBox(height: AppSpacing.xxl), + Text( + l10n.emailCodeSentInstructions, // New l10n key for instructions + style: textTheme.bodyMedium, + textAlign: TextAlign.center, + ), + const SizedBox(height: AppSpacing.lg), + // Input field for the 6-digit code + Padding( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.md, + ), + child: TextField( + // TODO(cline): Add controller and validation + keyboardType: TextInputType.number, + maxLength: 6, + textAlign: TextAlign.center, + decoration: InputDecoration( + hintText: l10n.emailCodeVerificationHint, // Add l10n key + border: const OutlineInputBorder(), + counterText: '', // Hide the counter + ), + ), + ), + const SizedBox(height: AppSpacing.xl), + // Verify button + ElevatedButton( + // TODO(cline): Add onPressed logic to dispatch event + onPressed: () { + // Dispatch event to AuthenticationBloc + // context.read().add( + // AuthenticationEmailCodeVerificationRequested( + // email: email, + // code: 'entered_code', // Get code from TextField + // ), + // ); + }, + child: Text( + l10n.emailCodeVerificationButtonLabel, + ), // Add l10n key + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/authentication/view/email_link_sent_page.dart b/lib/authentication/view/email_link_sent_page.dart deleted file mode 100644 index 9611eba0..00000000 --- a/lib/authentication/view/email_link_sent_page.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:ht_main/l10n/l10n.dart'; -import 'package:ht_main/shared/constants/app_spacing.dart'; - -/// {@template email_link_sent_page} -/// Confirmation page shown after a sign-in link has been sent to -/// the user's email. Instructs the user to check their inbox. -/// {@endtemplate} -class EmailLinkSentPage extends StatelessWidget { - /// {@macro email_link_sent_page} - const EmailLinkSentPage({super.key}); - - @override - Widget build(BuildContext context) { - final l10n = context.l10n; - final textTheme = Theme.of(context).textTheme; - - return Scaffold( - appBar: AppBar( - title: Text(l10n.emailLinkSentPageTitle), // New l10n key needed - ), - body: SafeArea( - child: Padding( - padding: const EdgeInsets.all(AppSpacing.paddingLarge), - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon( - Icons.mark_email_read_outlined, // Suggestive icon - size: 80, - // Consider using theme color - // color: Theme.of(context).colorScheme.primary, - ), - const SizedBox(height: AppSpacing.xl), - Text( - l10n.emailLinkSentConfirmation, // New l10n key needed - style: textTheme.titleLarge, // Prominent text style - textAlign: TextAlign.center, - ), - // Optional: Add a button to go back if needed, - // but AppBar back button might suffice. - // const SizedBox(height: AppSpacing.xxl), - // OutlinedButton( - // onPressed: () => context.pop(), // Or navigate elsewhere - // child: Text(l10n.backButtonLabel), // New l10n key - // ), - ], - ), - ), - ), - ), - ); - } -} diff --git a/lib/authentication/view/email_sign_in_page.dart b/lib/authentication/view/request_code_page.dart similarity index 93% rename from lib/authentication/view/email_sign_in_page.dart rename to lib/authentication/view/request_code_page.dart index aa66683f..191ddc47 100644 --- a/lib/authentication/view/email_sign_in_page.dart +++ b/lib/authentication/view/request_code_page.dart @@ -80,20 +80,23 @@ class _EmailSignInView extends StatelessWidget { backgroundColor: colorScheme.error, ), ); - } else if (state is AuthenticationLinkSentSuccess) { - // Navigate to the confirmation page on success - context.goNamed(Routes.emailLinkSentName); + } else if (state is AuthenticationCodeSentSuccess) { + // Navigate to the code verification page on success, passing the email + context.goNamed( + Routes.verifyCodeName, + pathParameters: {'email': state.email}, + ); } }, // BuildWhen prevents unnecessary rebuilds if only listening buildWhen: (previous, current) => current is AuthenticationInitial || - current is AuthenticationLinkSending || + current is AuthenticationRequestCodeLoading || current is AuthenticationFailure, // Rebuild on failure to re-enable form builder: (context, state) { - final isLoading = state is AuthenticationLinkSending; + final isLoading = state is AuthenticationRequestCodeLoading; return Padding( padding: const EdgeInsets.all(AppSpacing.paddingLarge), @@ -164,7 +167,7 @@ class _EmailLinkFormState extends State<_EmailLinkForm> { void _submitForm() { if (_formKey.currentState!.validate()) { context.read().add( - AuthenticationSendSignInLinkRequested( + AuthenticationRequestSignInCodeRequested( email: _emailController.text.trim(), ), ); diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart deleted file mode 100644 index bf7399bf..00000000 --- a/lib/firebase_options.dart +++ /dev/null @@ -1,69 +0,0 @@ -// File generated by FlutterFire CLI. -// ignore_for_file: type=lint -import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; -import 'package:flutter/foundation.dart' - show defaultTargetPlatform, kIsWeb, TargetPlatform; - -/// Default [FirebaseOptions] for use with your Firebase apps. -/// -/// Example: -/// ```dart -/// import 'firebase_options.dart'; -/// // ... -/// await Firebase.initializeApp( -/// options: DefaultFirebaseOptions.currentPlatform, -/// ); -/// ``` -class DefaultFirebaseOptions { - static FirebaseOptions get currentPlatform { - if (kIsWeb) { - return web; - } - switch (defaultTargetPlatform) { - case TargetPlatform.android: - return android; - case TargetPlatform.iOS: - throw UnsupportedError( - 'DefaultFirebaseOptions have not been configured for ios - ' - 'you can reconfigure this by running the FlutterFire CLI again.', - ); - case TargetPlatform.macOS: - throw UnsupportedError( - 'DefaultFirebaseOptions have not been configured for macos - ' - 'you can reconfigure this by running the FlutterFire CLI again.', - ); - case TargetPlatform.windows: - throw UnsupportedError( - 'DefaultFirebaseOptions have not been configured for windows - ' - 'you can reconfigure this by running the FlutterFire CLI again.', - ); - case TargetPlatform.linux: - throw UnsupportedError( - 'DefaultFirebaseOptions have not been configured for linux - ' - 'you can reconfigure this by running the FlutterFire CLI again.', - ); - default: - throw UnsupportedError( - 'DefaultFirebaseOptions are not supported for this platform.', - ); - } - } - - static const FirebaseOptions web = FirebaseOptions( - apiKey: 'AIzaSyCNas7jJch6P5f33O2Ag2GD6FbRHVaYjZQ', - appId: '1:420644261437:web:c20af2372a50f7c958a454', - messagingSenderId: '420644261437', - projectId: 'headlines-toolkit', - authDomain: 'headlines-toolkit.firebaseapp.com', - storageBucket: 'headlines-toolkit.firebasestorage.app', - measurementId: 'G-WXK52X9VZL', - ); - - static const FirebaseOptions android = FirebaseOptions( - apiKey: 'AIzaSyD1LsqoCRmsIbr9zgFyYXg7PGE7qhn3Uvs', - appId: '1:420644261437:android:3fa3052f883bf86958a454', - messagingSenderId: '420644261437', - projectId: 'headlines-toolkit', - storageBucket: 'headlines-toolkit.firebasestorage.app', - ); -} diff --git a/lib/headline-details/bloc/headline_details_bloc.dart b/lib/headline-details/bloc/headline_details_bloc.dart index 450f81ae..b242375c 100644 --- a/lib/headline-details/bloc/headline_details_bloc.dart +++ b/lib/headline-details/bloc/headline_details_bloc.dart @@ -1,21 +1,25 @@ import 'dart:async'; // Ensure async is imported import 'package:bloc/bloc.dart'; -import 'package:ht_headlines_client/ht_headlines_client.dart'; // Import for Headline and Exceptions -import 'package:ht_headlines_repository/ht_headlines_repository.dart'; +import 'package:ht_data_repository/ht_data_repository.dart'; // Generic Data Repository +import 'package:ht_shared/ht_shared.dart' + show + Headline, + HtHttpException, + NotFoundException; // Shared models and standardized exceptions part 'headline_details_event.dart'; part 'headline_details_state.dart'; class HeadlineDetailsBloc extends Bloc { - HeadlineDetailsBloc({required HtHeadlinesRepository headlinesRepository}) + HeadlineDetailsBloc({required HtDataRepository headlinesRepository}) : _headlinesRepository = headlinesRepository, super(HeadlineDetailsInitial()) { on(_onHeadlineDetailsRequested); } - final HtHeadlinesRepository _headlinesRepository; + final HtDataRepository _headlinesRepository; Future _onHeadlineDetailsRequested( HeadlineDetailsRequested event, @@ -23,13 +27,11 @@ class HeadlineDetailsBloc ) async { emit(HeadlineDetailsLoading()); try { - final headline = await _headlinesRepository.getHeadline( - id: event.headlineId, - ); - emit(HeadlineDetailsLoaded(headline: headline!)); - } on HeadlineNotFoundException catch (e) { + final headline = await _headlinesRepository.read(id: event.headlineId); + emit(HeadlineDetailsLoaded(headline: headline)); + } on NotFoundException catch (e) { emit(HeadlineDetailsFailure(message: e.message)); - } on HeadlinesFetchException catch (e) { + } on HtHttpException catch (e) { emit(HeadlineDetailsFailure(message: e.message)); } catch (e) { emit(HeadlineDetailsFailure(message: 'An unexpected error occurred: $e')); diff --git a/lib/headline-details/view/headline_details_page.dart b/lib/headline-details/view/headline_details_page.dart index 3d471f64..4b1e024e 100644 --- a/lib/headline-details/view/headline_details_page.dart +++ b/lib/headline-details/view/headline_details_page.dart @@ -3,10 +3,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:ht_headlines_client/ht_headlines_client.dart'; import 'package:ht_main/headline-details/bloc/headline_details_bloc.dart'; import 'package:ht_main/l10n/l10n.dart'; import 'package:ht_main/shared/shared.dart'; +import 'package:ht_shared/ht_shared.dart' + show Headline; // Import Headline model import 'package:intl/intl.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -265,22 +266,20 @@ class HeadlineDetailsPage extends StatelessWidget { ); } - // Country Chip - if (headline.eventCountry != null) { - // Use country.flagUrl for the avatar + // Country Chip (from Source Headquarters) + if (headline.source?.headquarters != null) { + final country = headline.source!.headquarters!; chips.add( Chip( avatar: CircleAvatar( - // Use CircleAvatar for better image display - radius: chipAvatarSize / 2, // Adjust radius as needed - backgroundColor: Colors.transparent, // Avoid background color clash - backgroundImage: NetworkImage(headline.eventCountry!.flagUrl), + radius: chipAvatarSize / 2, + backgroundColor: Colors.transparent, + backgroundImage: NetworkImage(country.flagUrl), onBackgroundImageError: (exception, stackTrace) { // Optional: Handle image loading errors, e.g., show placeholder }, ), - // Use eventCountry.name - label: Text(headline.eventCountry!.name), + label: Text(country.name), labelStyle: chipLabelStyle, backgroundColor: chipBackgroundColor, padding: chipPadding, diff --git a/lib/headlines-feed/bloc/categories_filter_bloc.dart b/lib/headlines-feed/bloc/categories_filter_bloc.dart index 513d5a53..cfaaf53c 100644 --- a/lib/headlines-feed/bloc/categories_filter_bloc.dart +++ b/lib/headlines-feed/bloc/categories_filter_bloc.dart @@ -3,9 +3,11 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:bloc_concurrency/bloc_concurrency.dart'; import 'package:equatable/equatable.dart'; -import 'package:ht_categories_client/ht_categories_client.dart'; -import 'package:ht_categories_repository/ht_categories_repository.dart'; -// For PaginatedResponse +import 'package:ht_data_repository/ht_data_repository.dart'; // Generic Data Repository +import 'package:ht_shared/ht_shared.dart' + show + Category, + HtHttpException; // Shared models, including Category and standardized exceptions part 'categories_filter_event.dart'; part 'categories_filter_state.dart'; @@ -14,16 +16,17 @@ part 'categories_filter_state.dart'; /// Manages the state for fetching and displaying categories for filtering. /// /// Handles initial fetching and pagination of categories using the -/// provided [HtCategoriesRepository]. +/// provided [HtDataRepository]. /// {@endtemplate} class CategoriesFilterBloc extends Bloc { /// {@macro categories_filter_bloc} /// - /// Requires a [HtCategoriesRepository] to interact with the data layer. - CategoriesFilterBloc({required HtCategoriesRepository categoriesRepository}) - : _categoriesRepository = categoriesRepository, - super(const CategoriesFilterState()) { + /// Requires a [HtDataRepository] to interact with the data layer. + CategoriesFilterBloc({ + required HtDataRepository categoriesRepository, + }) : _categoriesRepository = categoriesRepository, + super(const CategoriesFilterState()) { on( _onCategoriesFilterRequested, transformer: restartable(), // Only process the latest request @@ -34,7 +37,7 @@ class CategoriesFilterBloc ); } - final HtCategoriesRepository _categoriesRepository; + final HtDataRepository _categoriesRepository; /// Number of categories to fetch per page. static const _categoriesLimit = 20; @@ -54,7 +57,7 @@ class CategoriesFilterBloc emit(state.copyWith(status: CategoriesFilterStatus.loading)); try { - final response = await _categoriesRepository.getCategories( + final response = await _categoriesRepository.readAll( limit: _categoriesLimit, ); emit( @@ -66,7 +69,7 @@ class CategoriesFilterBloc clearError: true, // Clear any previous error ), ); - } on GetCategoriesFailure catch (e) { + } on HtHttpException catch (e) { emit(state.copyWith(status: CategoriesFilterStatus.failure, error: e)); } catch (e) { // Catch unexpected errors @@ -87,7 +90,7 @@ class CategoriesFilterBloc emit(state.copyWith(status: CategoriesFilterStatus.loadingMore)); try { - final response = await _categoriesRepository.getCategories( + final response = await _categoriesRepository.readAll( limit: _categoriesLimit, startAfterId: state.cursor, // Use the cursor from the current state ); @@ -100,7 +103,7 @@ class CategoriesFilterBloc cursor: response.cursor, ), ); - } on GetCategoriesFailure catch (e) { + } on HtHttpException catch (e) { // Keep existing data but indicate failure emit( state.copyWith( diff --git a/lib/headlines-feed/bloc/countries_filter_bloc.dart b/lib/headlines-feed/bloc/countries_filter_bloc.dart index 23499dff..dc65bd4f 100644 --- a/lib/headlines-feed/bloc/countries_filter_bloc.dart +++ b/lib/headlines-feed/bloc/countries_filter_bloc.dart @@ -3,9 +3,11 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:bloc_concurrency/bloc_concurrency.dart'; // For transformers import 'package:equatable/equatable.dart'; -import 'package:ht_countries_client/ht_countries_client.dart'; // For Country model and exceptions -import 'package:ht_countries_repository/ht_countries_repository.dart'; -// For PaginatedResponse +import 'package:ht_data_repository/ht_data_repository.dart'; // Generic Data Repository +import 'package:ht_shared/ht_shared.dart' + show + Country, + HtHttpException; // Shared models, including Country and standardized exceptions part 'countries_filter_event.dart'; part 'countries_filter_state.dart'; @@ -14,14 +16,14 @@ part 'countries_filter_state.dart'; /// Manages the state for fetching and displaying countries for filtering. /// /// Handles initial fetching and pagination of countries using the -/// provided [HtCountriesRepository]. +/// provided [HtDataRepository]. /// {@endtemplate} class CountriesFilterBloc extends Bloc { /// {@macro countries_filter_bloc} /// - /// Requires a [HtCountriesRepository] to interact with the data layer. - CountriesFilterBloc({required HtCountriesRepository countriesRepository}) + /// Requires a [HtDataRepository] to interact with the data layer. + CountriesFilterBloc({required HtDataRepository countriesRepository}) : _countriesRepository = countriesRepository, super(const CountriesFilterState()) { on( @@ -34,7 +36,7 @@ class CountriesFilterBloc ); } - final HtCountriesRepository _countriesRepository; + final HtDataRepository _countriesRepository; /// Number of countries to fetch per page. static const _countriesLimit = 20; @@ -53,8 +55,7 @@ class CountriesFilterBloc emit(state.copyWith(status: CountriesFilterStatus.loading)); try { - // Note: Repository uses 'cursor' parameter name here - final response = await _countriesRepository.fetchCountries( + final response = await _countriesRepository.readAll( limit: _countriesLimit, ); emit( @@ -66,7 +67,7 @@ class CountriesFilterBloc clearError: true, // Clear any previous error ), ); - } on CountryFetchFailure catch (e) { + } on HtHttpException catch (e) { emit(state.copyWith(status: CountriesFilterStatus.failure, error: e)); } catch (e) { // Catch unexpected errors @@ -87,10 +88,9 @@ class CountriesFilterBloc emit(state.copyWith(status: CountriesFilterStatus.loadingMore)); try { - // Note: Repository uses 'cursor' parameter name here - final response = await _countriesRepository.fetchCountries( + final response = await _countriesRepository.readAll( limit: _countriesLimit, - cursor: state.cursor, // Use the cursor from the current state + startAfterId: state.cursor, // Use the cursor from the current state ); emit( state.copyWith( @@ -101,7 +101,7 @@ class CountriesFilterBloc cursor: response.cursor, ), ); - } on CountryFetchFailure catch (e) { + } on HtHttpException catch (e) { // Keep existing data but indicate failure emit(state.copyWith(status: CountriesFilterStatus.failure, error: e)); } catch (e) { diff --git a/lib/headlines-feed/bloc/headlines_feed_bloc.dart b/lib/headlines-feed/bloc/headlines_feed_bloc.dart index 9a5f87c6..978a8a31 100644 --- a/lib/headlines-feed/bloc/headlines_feed_bloc.dart +++ b/lib/headlines-feed/bloc/headlines_feed_bloc.dart @@ -3,9 +3,15 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:bloc_concurrency/bloc_concurrency.dart'; import 'package:equatable/equatable.dart'; -import 'package:ht_headlines_client/ht_headlines_client.dart'; // Import for Headline and Exceptions -import 'package:ht_headlines_repository/ht_headlines_repository.dart'; +import 'package:ht_data_repository/ht_data_repository.dart'; // Generic Data Repository import 'package:ht_main/headlines-feed/models/headline_filter.dart'; +import 'package:ht_shared/ht_shared.dart' + show + Category, + Country, + Headline, + HtHttpException, + Source; // Shared models and standardized exceptions part 'headlines_feed_event.dart'; part 'headlines_feed_state.dart'; @@ -14,13 +20,13 @@ part 'headlines_feed_state.dart'; /// Manages the state for the headlines feed feature. /// /// Handles fetching headlines, applying filters, pagination, and refreshing -/// the feed using the provided [HtHeadlinesRepository]. +/// the feed using the provided [HtDataRepository]. /// {@endtemplate} class HeadlinesFeedBloc extends Bloc { /// {@macro headlines_feed_bloc} /// - /// Requires a [HtHeadlinesRepository] to interact with the data layer. - HeadlinesFeedBloc({required HtHeadlinesRepository headlinesRepository}) + /// Requires a [HtDataRepository] to interact with the data layer. + HeadlinesFeedBloc({required HtDataRepository headlinesRepository}) : _headlinesRepository = headlinesRepository, super(HeadlinesFeedLoading()) { on( @@ -37,7 +43,7 @@ class HeadlinesFeedBloc extends Bloc { on(_onHeadlinesFeedFiltersCleared); } - final HtHeadlinesRepository _headlinesRepository; + final HtDataRepository _headlinesRepository; /// The number of headlines to fetch per page during pagination or initial load. static const _headlinesFetchLimit = 10; @@ -54,12 +60,26 @@ class HeadlinesFeedBloc extends Bloc { ) async { emit(HeadlinesFeedLoading()); // Show loading for filter application try { - final response = await _headlinesRepository.getHeadlines( - limit: _headlinesFetchLimit, - categories: event.filter.categories, - sources: event.filter.sources, - eventCountries: event.filter.eventCountries, - ); + final response = await _headlinesRepository.readAllByQuery({ + if (event.filter.categories?.isNotEmpty ?? false) + 'categories': + event.filter.categories! + .whereType() + .map((c) => c.id) + .toList(), + if (event.filter.sources?.isNotEmpty ?? false) + 'sources': + event.filter.sources! + .whereType() + .map((s) => s.id) + .toList(), + if (event.filter.eventCountries?.isNotEmpty ?? false) + 'eventCountries': + event.filter.eventCountries! + .whereType() + .map((c) => c.isoCode) + .toList(), + }, limit: _headlinesFetchLimit,); emit( HeadlinesFeedLoaded( headlines: response.items, @@ -68,7 +88,7 @@ class HeadlinesFeedBloc extends Bloc { filter: event.filter, // Store the applied filter ), ); - } on HeadlinesFetchException catch (e) { + } on HtHttpException catch (e) { emit(HeadlinesFeedError(message: e.message)); } catch (e, st) { // Log the error and stack trace for unexpected errors @@ -88,7 +108,7 @@ class HeadlinesFeedBloc extends Bloc { emit(HeadlinesFeedLoading()); // Show loading indicator try { // Fetch the first page with no filters - final response = await _headlinesRepository.getHeadlines( + final response = await _headlinesRepository.readAll( limit: _headlinesFetchLimit, ); emit( @@ -98,7 +118,7 @@ class HeadlinesFeedBloc extends Bloc { cursor: response.cursor, ), ); - } on HeadlinesFetchException catch (e) { + } on HtHttpException catch (e) { emit(HeadlinesFeedError(message: e.message)); } catch (e, st) { // Log the error and stack trace for unexpected errors @@ -155,12 +175,29 @@ class HeadlinesFeedBloc extends Bloc { } try { - final response = await _headlinesRepository.getHeadlines( + final response = await _headlinesRepository.readAllByQuery( + { + if (currentFilter.categories?.isNotEmpty ?? false) + 'categories': + currentFilter.categories! + .whereType() + .map((c) => c.id) + .toList(), + if (currentFilter.sources?.isNotEmpty ?? false) + 'sources': + currentFilter.sources! + .whereType() + .map((s) => s.id) + .toList(), + if (currentFilter.eventCountries?.isNotEmpty ?? false) + 'eventCountries': + currentFilter.eventCountries! + .whereType() + .map((c) => c.isoCode) + .toList(), + }, limit: _headlinesFetchLimit, startAfterId: currentCursor, // Use determined cursor - categories: currentFilter.categories, - sources: currentFilter.sources, - eventCountries: currentFilter.eventCountries, ); emit( HeadlinesFeedLoaded( @@ -172,7 +209,7 @@ class HeadlinesFeedBloc extends Bloc { filter: currentFilter, // Preserve the filter ), ); - } on HeadlinesFetchException catch (e) { + } on HtHttpException catch (e) { emit(HeadlinesFeedError(message: e.message)); } catch (e, st) { print('Unexpected error in _onHeadlinesFeedFetchRequested: $e\n$st'); @@ -198,12 +235,26 @@ class HeadlinesFeedBloc extends Bloc { try { // Fetch the first page using the current filter - final response = await _headlinesRepository.getHeadlines( - limit: _headlinesFetchLimit, - categories: currentFilter.categories, - sources: currentFilter.sources, - eventCountries: currentFilter.eventCountries, - ); + final response = await _headlinesRepository.readAllByQuery({ + if (currentFilter.categories?.isNotEmpty ?? false) + 'categories': + currentFilter.categories! + .whereType() + .map((c) => c.id) + .toList(), + if (currentFilter.sources?.isNotEmpty ?? false) + 'sources': + currentFilter.sources! + .whereType() + .map((s) => s.id) + .toList(), + if (currentFilter.eventCountries?.isNotEmpty ?? false) + 'eventCountries': + currentFilter.eventCountries! + .whereType() + .map((c) => c.isoCode) + .toList(), + }, limit: _headlinesFetchLimit,); emit( HeadlinesFeedLoaded( headlines: response.items, // Replace headlines on refresh @@ -212,7 +263,7 @@ class HeadlinesFeedBloc extends Bloc { filter: currentFilter, // Preserve the filter ), ); - } on HeadlinesFetchException catch (e) { + } on HtHttpException catch (e) { emit(HeadlinesFeedError(message: e.message)); } catch (e, st) { print('Unexpected error in _onHeadlinesFeedRefreshRequested: $e\n$st'); diff --git a/lib/headlines-feed/bloc/sources_filter_bloc.dart b/lib/headlines-feed/bloc/sources_filter_bloc.dart index f0ec1361..b3207b12 100644 --- a/lib/headlines-feed/bloc/sources_filter_bloc.dart +++ b/lib/headlines-feed/bloc/sources_filter_bloc.dart @@ -3,9 +3,11 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:bloc_concurrency/bloc_concurrency.dart'; // For transformers import 'package:equatable/equatable.dart'; -// For PaginatedResponse -import 'package:ht_sources_client/ht_sources_client.dart'; // Keep existing, also for Source model -import 'package:ht_sources_repository/ht_sources_repository.dart'; +import 'package:ht_data_repository/ht_data_repository.dart'; // Generic Data Repository +import 'package:ht_shared/ht_shared.dart' + show + HtHttpException, + Source; // Shared models, including Source and standardized exceptions part 'sources_filter_event.dart'; part 'sources_filter_state.dart'; @@ -14,13 +16,13 @@ part 'sources_filter_state.dart'; /// Manages the state for fetching and displaying sources for filtering. /// /// Handles initial fetching and pagination of sources using the -/// provided [HtSourcesRepository]. +/// provided [HtDataRepository]. /// {@endtemplate} class SourcesFilterBloc extends Bloc { /// {@macro sources_filter_bloc} /// - /// Requires a [HtSourcesRepository] to interact with the data layer. - SourcesFilterBloc({required HtSourcesRepository sourcesRepository}) + /// Requires a [HtDataRepository] to interact with the data layer. + SourcesFilterBloc({required HtDataRepository sourcesRepository}) : _sourcesRepository = sourcesRepository, super(const SourcesFilterState()) { on( @@ -33,7 +35,7 @@ class SourcesFilterBloc extends Bloc { ); } - final HtSourcesRepository _sourcesRepository; + final HtDataRepository _sourcesRepository; /// Number of sources to fetch per page. static const _sourcesLimit = 20; @@ -52,9 +54,7 @@ class SourcesFilterBloc extends Bloc { emit(state.copyWith(status: SourcesFilterStatus.loading)); try { - final response = await _sourcesRepository.getSources( - limit: _sourcesLimit, - ); + final response = await _sourcesRepository.readAll(limit: _sourcesLimit); emit( state.copyWith( status: SourcesFilterStatus.success, @@ -64,7 +64,7 @@ class SourcesFilterBloc extends Bloc { clearError: true, // Clear any previous error ), ); - } on SourceFetchFailure catch (e) { + } on HtHttpException catch (e) { emit(state.copyWith(status: SourcesFilterStatus.failure, error: e)); } catch (e) { // Catch unexpected errors @@ -85,7 +85,7 @@ class SourcesFilterBloc extends Bloc { emit(state.copyWith(status: SourcesFilterStatus.loadingMore)); try { - final response = await _sourcesRepository.getSources( + final response = await _sourcesRepository.readAll( limit: _sourcesLimit, startAfterId: state.cursor, // Use the cursor from the current state ); @@ -98,7 +98,7 @@ class SourcesFilterBloc extends Bloc { cursor: response.cursor, ), ); - } on SourceFetchFailure catch (e) { + } on HtHttpException catch (e) { // Keep existing data but indicate failure emit(state.copyWith(status: SourcesFilterStatus.failure, error: e)); } catch (e) { diff --git a/lib/headlines-feed/models/headline_filter.dart b/lib/headlines-feed/models/headline_filter.dart index ad26bd3a..00fa8b74 100644 --- a/lib/headlines-feed/models/headline_filter.dart +++ b/lib/headlines-feed/models/headline_filter.dart @@ -1,7 +1,5 @@ import 'package:equatable/equatable.dart'; -import 'package:ht_categories_client/ht_categories_client.dart'; -import 'package:ht_countries_client/ht_countries_client.dart'; -import 'package:ht_sources_client/ht_sources_client.dart'; +import 'package:ht_shared/ht_shared.dart'; /// {@template headline_filter} /// A model representing the filter parameters for headlines. diff --git a/lib/headlines-feed/view/category_filter_page.dart b/lib/headlines-feed/view/category_filter_page.dart index 5893e067..9a1e807b 100644 --- a/lib/headlines-feed/view/category_filter_page.dart +++ b/lib/headlines-feed/view/category_filter_page.dart @@ -4,12 +4,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; -import 'package:ht_categories_client/ht_categories_client.dart'; -// Removed repository import: import 'package:ht_categories_repository/ht_categories_repository.dart'; import 'package:ht_main/headlines-feed/bloc/categories_filter_bloc.dart'; // Import the BLoC import 'package:ht_main/l10n/l10n.dart'; import 'package:ht_main/shared/constants/constants.dart'; import 'package:ht_main/shared/widgets/widgets.dart'; // For loading/error widgets +import 'package:ht_shared/ht_shared.dart' + show Category; // Import Category model /// {@template category_filter_page} /// A page dedicated to selecting news categories for filtering headlines. diff --git a/lib/headlines-feed/view/country_filter_page.dart b/lib/headlines-feed/view/country_filter_page.dart index 212cb240..de40c847 100644 --- a/lib/headlines-feed/view/country_filter_page.dart +++ b/lib/headlines-feed/view/country_filter_page.dart @@ -4,12 +4,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; -import 'package:ht_countries_client/ht_countries_client.dart'; -// Removed repository import: import 'package:ht_countries_repository/ht_countries_repository.dart'; import 'package:ht_main/headlines-feed/bloc/countries_filter_bloc.dart'; // Import the BLoC import 'package:ht_main/l10n/l10n.dart'; import 'package:ht_main/shared/constants/constants.dart'; import 'package:ht_main/shared/widgets/widgets.dart'; // For loading/error widgets +import 'package:ht_shared/ht_shared.dart' show Country; // Import Country model /// {@template country_filter_page} /// A page dedicated to selecting event countries for filtering headlines. diff --git a/lib/headlines-feed/view/headlines_filter_page.dart b/lib/headlines-feed/view/headlines_filter_page.dart index 3f78f030..c4ac1ca0 100644 --- a/lib/headlines-feed/view/headlines_filter_page.dart +++ b/lib/headlines-feed/view/headlines_filter_page.dart @@ -4,14 +4,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; -import 'package:ht_categories_client/ht_categories_client.dart'; -import 'package:ht_countries_client/ht_countries_client.dart'; import 'package:ht_main/headlines-feed/bloc/headlines_feed_bloc.dart'; import 'package:ht_main/headlines-feed/models/headline_filter.dart'; import 'package:ht_main/l10n/l10n.dart'; import 'package:ht_main/router/routes.dart'; import 'package:ht_main/shared/constants/constants.dart'; -import 'package:ht_sources_client/ht_sources_client.dart'; +import 'package:ht_shared/ht_shared.dart' + show Category, Country, Source; // Import models from ht_shared /// {@template headlines_filter_page} /// A full-screen dialog page for selecting headline filters. diff --git a/lib/headlines-feed/view/source_filter_page.dart b/lib/headlines-feed/view/source_filter_page.dart index c351a837..df149e46 100644 --- a/lib/headlines-feed/view/source_filter_page.dart +++ b/lib/headlines-feed/view/source_filter_page.dart @@ -8,8 +8,7 @@ import 'package:ht_main/headlines-feed/bloc/sources_filter_bloc.dart'; // Import import 'package:ht_main/l10n/l10n.dart'; import 'package:ht_main/shared/constants/constants.dart'; import 'package:ht_main/shared/widgets/widgets.dart'; // For loading/error widgets -import 'package:ht_sources_client/ht_sources_client.dart'; -// Removed repository import: import 'package:ht_sources_repository/ht_sources_repository.dart'; +import 'package:ht_shared/ht_shared.dart' show Source; // Import Source model /// {@template source_filter_page} /// A page dedicated to selecting news sources for filtering headlines. diff --git a/lib/headlines-feed/widgets/headline_item_widget.dart b/lib/headlines-feed/widgets/headline_item_widget.dart index f5515dc2..cb27b0cb 100644 --- a/lib/headlines-feed/widgets/headline_item_widget.dart +++ b/lib/headlines-feed/widgets/headline_item_widget.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:ht_headlines_client/ht_headlines_client.dart' show Headline; import 'package:ht_main/router/routes.dart'; import 'package:ht_main/shared/constants/constants.dart'; // Import AppSpacing +import 'package:ht_shared/ht_shared.dart'; // Import models from ht_shared import 'package:intl/intl.dart'; // For date formatting /// A widget that displays a single headline with enhanced styling. @@ -134,10 +134,19 @@ class HeadlineItemWidget extends StatelessWidget { icon: Icons.category_outlined, text: headline.category!.name, ), - if (headline.eventCountry != null) + if (headline.source?.headquarters != + null) // Use source?.headquarters _CountryMetadataItem( - flagUrl: headline.eventCountry!.flagUrl, - countryName: headline.eventCountry!.name, + flagUrl: + headline + .source! + .headquarters! + .flagUrl, // Access flagUrl from headquarters + countryName: + headline + .source! + .headquarters! + .name, // Access name from headquarters ), ], ), diff --git a/lib/headlines-search/bloc/headlines_search_bloc.dart b/lib/headlines-search/bloc/headlines_search_bloc.dart index 2ce12d6f..c3efae8f 100644 --- a/lib/headlines-search/bloc/headlines_search_bloc.dart +++ b/lib/headlines-search/bloc/headlines_search_bloc.dart @@ -1,20 +1,20 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; -import 'package:ht_headlines_client/ht_headlines_client.dart'; -import 'package:ht_headlines_repository/ht_headlines_repository.dart'; +import 'package:ht_data_repository/ht_data_repository.dart'; // Generic Data Repository +import 'package:ht_shared/ht_shared.dart'; // Shared models, including Headline part 'headlines_search_event.dart'; part 'headlines_search_state.dart'; class HeadlinesSearchBloc extends Bloc { - HeadlinesSearchBloc({required HtHeadlinesRepository headlinesRepository}) + HeadlinesSearchBloc({required HtDataRepository headlinesRepository}) : _headlinesRepository = headlinesRepository, super(HeadlinesSearchLoading()) { on(_onSearchFetchRequested); } - final HtHeadlinesRepository _headlinesRepository; + final HtDataRepository _headlinesRepository; static const _limit = 10; Future _onSearchFetchRequested( @@ -38,8 +38,8 @@ class HeadlinesSearchBloc if (!currentState.hasMore) return; try { - final response = await _headlinesRepository.searchHeadlines( - query: event.searchTerm, + final response = await _headlinesRepository.readAllByQuery( + {'query': event.searchTerm}, // Use query map limit: _limit, startAfterId: currentState.cursor, ); @@ -58,8 +58,8 @@ class HeadlinesSearchBloc } } else { try { - final response = await _headlinesRepository.searchHeadlines( - query: event.searchTerm, + final response = await _headlinesRepository.readAllByQuery( + {'query': event.searchTerm}, // Use query map limit: _limit, ); emit( diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index 5c9e9349..b2c86ab5 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -80,6 +80,24 @@ "@accountBackupTile": { "description": "Title for the tile prompting anonymous users to create an account" }, + "accountContentPreferencesTile": "تفضيلات المحتوى", + "@accountContentPreferencesTile": { + "description": "Title for the content preferences navigation tile in the account page" + }, + "accountSavedHeadlinesTile": "العناوين المحفوظة", + "@accountSavedHeadlinesTile": { + "description": "Title for the saved headlines navigation tile in the account page" + }, + "accountRoleLabel": "الدور: {role}", + "@accountRoleLabel": { + "description": "Label displaying the user's role in the account header", + "placeholders": { + "role": { + "type": "String", + "example": "admin" + } + } + }, "authenticationEmailSentSuccess": "تحقق من بريدك الإلكتروني للحصول على رابط تسجيل الدخول.", "@authenticationEmailSentSuccess": { "description": "Success message shown after sending an email sign-in link on the authentication page" @@ -359,66 +377,129 @@ "description": "Button text shown in the user header for anonymous users to initiate sign-in/sign-up" }, "settingsTitle": "الإعدادات", - "@settingsTitle": { "description": "Title for the main settings page" }, + "@settingsTitle": { + "description": "Title for the main settings page" + }, "settingsLoadingHeadline": "جارٍ تحميل الإعدادات...", - "@settingsLoadingHeadline": { "description": "Headline shown while settings are loading" }, + "@settingsLoadingHeadline": { + "description": "Headline shown while settings are loading" + }, "settingsLoadingSubheadline": "يرجى الانتظار بينما نقوم بجلب تفضيلاتك.", - "@settingsLoadingSubheadline": { "description": "Subheadline shown while settings are loading" }, + "@settingsLoadingSubheadline": { + "description": "Subheadline shown while settings are loading" + }, "settingsErrorDefault": "تعذر تحميل الإعدادات.", - "@settingsErrorDefault": { "description": "Default error message when settings fail to load" }, + "@settingsErrorDefault": { + "description": "Default error message when settings fail to load" + }, "settingsAppearanceTitle": "المظهر", - "@settingsAppearanceTitle": { "description": "Title for the appearance settings section/page" }, + "@settingsAppearanceTitle": { + "description": "Title for the appearance settings section/page" + }, "settingsFeedDisplayTitle": "عرض الموجز", - "@settingsFeedDisplayTitle": { "description": "Title for the feed display settings section/page" }, + "@settingsFeedDisplayTitle": { + "description": "Title for the feed display settings section/page" + }, "settingsArticleDisplayTitle": "عرض المقال", - "@settingsArticleDisplayTitle": { "description": "Title for the article display settings section/page" }, + "@settingsArticleDisplayTitle": { + "description": "Title for the article display settings section/page" + }, "settingsNotificationsTitle": "الإشعارات", - "@settingsNotificationsTitle": { "description": "Title for the notification settings section/page" }, + "@settingsNotificationsTitle": { + "description": "Title for the notification settings section/page" + }, "settingsAppearanceThemeModeLight": "فاتح", - "@settingsAppearanceThemeModeLight": { "description": "Label for the light theme mode option" }, + "@settingsAppearanceThemeModeLight": { + "description": "Label for the light theme mode option" + }, "settingsAppearanceThemeModeDark": "داكن", - "@settingsAppearanceThemeModeDark": { "description": "Label for the dark theme mode option" }, + "@settingsAppearanceThemeModeDark": { + "description": "Label for the dark theme mode option" + }, "settingsAppearanceThemeModeSystem": "النظام", - "@settingsAppearanceThemeModeSystem": { "description": "Label for the system theme mode option" }, + "@settingsAppearanceThemeModeSystem": { + "description": "Label for the system theme mode option" + }, "settingsAppearanceThemeNameRed": "أحمر", - "@settingsAppearanceThemeNameRed": { "description": "Label for the red color scheme option" }, + "@settingsAppearanceThemeNameRed": { + "description": "Label for the red color scheme option" + }, "settingsAppearanceThemeNameBlue": "أزرق", - "@settingsAppearanceThemeNameBlue": { "description": "Label for the blue color scheme option" }, + "@settingsAppearanceThemeNameBlue": { + "description": "Label for the blue color scheme option" + }, "settingsAppearanceThemeNameGrey": "رمادي", - "@settingsAppearanceThemeNameGrey": { "description": "Label for the grey color scheme option" }, + "@settingsAppearanceThemeNameGrey": { + "description": "Label for the grey color scheme option" + }, "settingsAppearanceFontSizeSmall": "صغير", - "@settingsAppearanceFontSizeSmall": { "description": "Label for the small font size option" }, + "@settingsAppearanceFontSizeSmall": { + "description": "Label for the small font size option" + }, "settingsAppearanceFontSizeLarge": "كبير", - "@settingsAppearanceFontSizeLarge": { "description": "Label for the large font size option" }, + "@settingsAppearanceFontSizeLarge": { + "description": "Label for the large font size option" + }, "settingsAppearanceFontSizeMedium": "متوسط", - "@settingsAppearanceFontSizeMedium": { "description": "Label for the medium font size option" }, + "@settingsAppearanceFontSizeMedium": { + "description": "Label for the medium font size option" + }, "settingsAppearanceThemeModeLabel": "وضع المظهر", - "@settingsAppearanceThemeModeLabel": { "description": "Label for the theme mode selection dropdown" }, + "@settingsAppearanceThemeModeLabel": { + "description": "Label for the theme mode selection dropdown" + }, "settingsAppearanceThemeNameLabel": "نظام الألوان", - "@settingsAppearanceThemeNameLabel": { "description": "Label for the color scheme selection dropdown" }, + "@settingsAppearanceThemeNameLabel": { + "description": "Label for the color scheme selection dropdown" + }, "settingsAppearanceAppFontSizeLabel": "حجم خط التطبيق", - "@settingsAppearanceAppFontSizeLabel": { "description": "Label for the app font size selection dropdown" }, + "@settingsAppearanceAppFontSizeLabel": { + "description": "Label for the app font size selection dropdown" + }, "settingsAppearanceAppFontTypeLabel": "خط التطبيق", - "@settingsAppearanceAppFontTypeLabel": { "description": "Label for the app font selection dropdown" }, + "@settingsAppearanceAppFontTypeLabel": { + "description": "Label for the app font selection dropdown" + }, + "settingsAppearanceFontWeightLabel": "وزن الخط", + "@settingsAppearanceFontWeightLabel": { + "description": "Label for the font weight setting." + }, "settingsFeedTileTypeImageTop": "صورة في الأعلى", - "@settingsFeedTileTypeImageTop": { "description": "Label for the feed tile type with image on top" }, + "@settingsFeedTileTypeImageTop": { + "description": "Label for the feed tile type with image on top" + }, "settingsFeedTileTypeImageStart": "صورة في البداية", - "@settingsFeedTileTypeImageStart": { "description": "Label for the feed tile type with image at the start" }, + "@settingsFeedTileTypeImageStart": { + "description": "Label for the feed tile type with image at the start" + }, "settingsFeedTileTypeTextOnly": "نص فقط", - "@settingsFeedTileTypeTextOnly": { "description": "Label for the feed tile type with text only" }, + "@settingsFeedTileTypeTextOnly": { + "description": "Label for the feed tile type with text only" + }, "settingsFeedTileTypeLabel": "تخطيط عنصر الموجز", - "@settingsFeedTileTypeLabel": { "description": "Label for the feed tile layout selection dropdown" }, + "@settingsFeedTileTypeLabel": { + "description": "Label for the feed tile layout selection dropdown" + }, "settingsArticleFontSizeLabel": "حجم خط المقال", - "@settingsArticleFontSizeLabel": { "description": "Label for the article font size selection dropdown" }, + "@settingsArticleFontSizeLabel": { + "description": "Label for the article font size selection dropdown" + }, "settingsNotificationsEnableLabel": "تفعيل الإشعارات", - "@settingsNotificationsEnableLabel": { "description": "Label for the switch to enable/disable notifications" }, + "@settingsNotificationsEnableLabel": { + "description": "Label for the switch to enable/disable notifications" + }, "settingsNotificationsCategoriesLabel": "الفئات المتابعة", - "@settingsNotificationsCategoriesLabel": { "description": "Label for the section to select notification categories" }, + "@settingsNotificationsCategoriesLabel": { + "description": "Label for the section to select notification categories" + }, "settingsNotificationsSourcesLabel": "المصادر المتابعة", - "@settingsNotificationsSourcesLabel": { "description": "Label for the section to select notification sources" }, + "@settingsNotificationsSourcesLabel": { + "description": "Label for the section to select notification sources" + }, "settingsNotificationsCountriesLabel": "البلدان المتابعة", - "@settingsNotificationsCountriesLabel": { "description": "Label for the section to select notification countries" }, - + "@settingsNotificationsCountriesLabel": { + "description": "Label for the section to select notification countries" + }, "unknownError": "حدث خطأ غير معروف.", "@unknownError": { "description": "Generic error message shown when an operation fails unexpectedly" @@ -426,5 +507,39 @@ "loadMoreError": "فشل تحميل المزيد من العناصر.", "@loadMoreError": { "description": "Error message shown when pagination fails to load the next set of items" + }, + "settingsAppearanceFontSizeExtraLarge": "كبير جداً", + "@settingsAppearanceFontSizeExtraLarge": { + "description": "Label for the extra large font size option" + }, + "settingsAppearanceFontFamilySystemDefault": "افتراضي النظام", + "@settingsAppearanceFontFamilySystemDefault": { + "description": "Label for the system default font family option" + }, + "emailCodeSentPageTitle": "أدخل الرمز", + "@emailCodeSentPageTitle": { + "description": "AppBar title for the email code verification page" + }, + "emailCodeSentConfirmation": "تم إرسال رمز التحقق إلى {email}. يرجى إدخاله أدناه.", + "@emailCodeSentConfirmation": { + "description": "Confirmation message shown after the email code has been sent", + "placeholders": { + "email": { + "type": "String", + "example": "user@example.com" + } + } + }, + "emailCodeSentInstructions": "أدخل الرمز المكون من 6 أرقام الذي تلقيته.", + "@emailCodeSentInstructions": { + "description": "Instructions for the user to enter the verification code" + }, + "emailCodeVerificationHint": "رمز مكون من 6 أرقام", + "@emailCodeVerificationHint": { + "description": "Hint text for the email code input field" + }, + "emailCodeVerificationButtonLabel": "تحقق من الرمز", + "@emailCodeVerificationButtonLabel": { + "description": "Label for the button to verify the email code" } } diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 177c3073..a6c63b3d 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -80,6 +80,24 @@ "@accountBackupTile": { "description": "Title for the tile prompting anonymous users to create an account" }, + "accountContentPreferencesTile": "Content Preferences", + "@accountContentPreferencesTile": { + "description": "Title for the content preferences navigation tile in the account page" + }, + "accountSavedHeadlinesTile": "Saved Headlines", + "@accountSavedHeadlinesTile": { + "description": "Title for the saved headlines navigation tile in the account page" + }, + "accountRoleLabel": "Role: {role}", + "@accountRoleLabel": { + "description": "Label displaying the user's role in the account header", + "placeholders": { + "role": { + "type": "String", + "example": "admin" + } + } + }, "authenticationEmailSentSuccess": "Check your email for the sign-in link.", "@authenticationEmailSentSuccess": { "description": "Success message shown after sending an email sign-in link on the authentication page" @@ -359,66 +377,129 @@ "description": "Subheadline for empty state on source filter page" }, "settingsTitle": "Settings", - "@settingsTitle": { "description": "Title for the main settings page" }, + "@settingsTitle": { + "description": "Title for the main settings page" + }, "settingsLoadingHeadline": "Loading Settings...", - "@settingsLoadingHeadline": { "description": "Headline shown while settings are loading" }, + "@settingsLoadingHeadline": { + "description": "Headline shown while settings are loading" + }, "settingsLoadingSubheadline": "Please wait while we fetch your preferences.", - "@settingsLoadingSubheadline": { "description": "Subheadline shown while settings are loading" }, + "@settingsLoadingSubheadline": { + "description": "Subheadline shown while settings are loading" + }, "settingsErrorDefault": "Could not load settings.", - "@settingsErrorDefault": { "description": "Default error message when settings fail to load" }, + "@settingsErrorDefault": { + "description": "Default error message when settings fail to load" + }, "settingsAppearanceTitle": "Appearance", - "@settingsAppearanceTitle": { "description": "Title for the appearance settings section/page" }, + "@settingsAppearanceTitle": { + "description": "Title for the appearance settings section/page" + }, "settingsFeedDisplayTitle": "Feed Display", - "@settingsFeedDisplayTitle": { "description": "Title for the feed display settings section/page" }, + "@settingsFeedDisplayTitle": { + "description": "Title for the feed display settings section/page" + }, "settingsArticleDisplayTitle": "Article Display", - "@settingsArticleDisplayTitle": { "description": "Title for the article display settings section/page" }, + "@settingsArticleDisplayTitle": { + "description": "Title for the article display settings section/page" + }, "settingsNotificationsTitle": "Notifications", - "@settingsNotificationsTitle": { "description": "Title for the notification settings section/page" }, + "@settingsNotificationsTitle": { + "description": "Title for the notification settings section/page" + }, "settingsAppearanceThemeModeLight": "Light", - "@settingsAppearanceThemeModeLight": { "description": "Label for the light theme mode option" }, + "@settingsAppearanceThemeModeLight": { + "description": "Label for the light theme mode option" + }, "settingsAppearanceThemeModeDark": "Dark", - "@settingsAppearanceThemeModeDark": { "description": "Label for the dark theme mode option" }, + "@settingsAppearanceThemeModeDark": { + "description": "Label for the dark theme mode option" + }, "settingsAppearanceThemeModeSystem": "System", - "@settingsAppearanceThemeModeSystem": { "description": "Label for the system theme mode option" }, + "@settingsAppearanceThemeModeSystem": { + "description": "Label for the system theme mode option" + }, "settingsAppearanceThemeNameRed": "Red", - "@settingsAppearanceThemeNameRed": { "description": "Label for the red color scheme option" }, + "@settingsAppearanceThemeNameRed": { + "description": "Label for the red color scheme option" + }, "settingsAppearanceThemeNameBlue": "Blue", - "@settingsAppearanceThemeNameBlue": { "description": "Label for the blue color scheme option" }, + "@settingsAppearanceThemeNameBlue": { + "description": "Label for the blue color scheme option" + }, "settingsAppearanceThemeNameGrey": "Grey", - "@settingsAppearanceThemeNameGrey": { "description": "Label for the grey color scheme option" }, + "@settingsAppearanceThemeNameGrey": { + "description": "Label for the grey color scheme option" + }, "settingsAppearanceFontSizeSmall": "Small", - "@settingsAppearanceFontSizeSmall": { "description": "Label for the small font size option" }, + "@settingsAppearanceFontSizeSmall": { + "description": "Label for the small font size option" + }, "settingsAppearanceFontSizeLarge": "Large", - "@settingsAppearanceFontSizeLarge": { "description": "Label for the large font size option" }, + "@settingsAppearanceFontSizeLarge": { + "description": "Label for the large font size option" + }, "settingsAppearanceFontSizeMedium": "Medium", - "@settingsAppearanceFontSizeMedium": { "description": "Label for the medium font size option" }, + "@settingsAppearanceFontSizeMedium": { + "description": "Label for the medium font size option" + }, "settingsAppearanceThemeModeLabel": "Theme Mode", - "@settingsAppearanceThemeModeLabel": { "description": "Label for the theme mode selection dropdown" }, + "@settingsAppearanceThemeModeLabel": { + "description": "Label for the theme mode selection dropdown" + }, "settingsAppearanceThemeNameLabel": "Color Scheme", - "@settingsAppearanceThemeNameLabel": { "description": "Label for the color scheme selection dropdown" }, + "@settingsAppearanceThemeNameLabel": { + "description": "Label for the color scheme selection dropdown" + }, "settingsAppearanceAppFontSizeLabel": "App Font Size", - "@settingsAppearanceAppFontSizeLabel": { "description": "Label for the app font size selection dropdown" }, + "@settingsAppearanceAppFontSizeLabel": { + "description": "Label for the app font size selection dropdown" + }, "settingsAppearanceAppFontTypeLabel": "App Font", - "@settingsAppearanceAppFontTypeLabel": { "description": "Label for the app font selection dropdown" }, + "@settingsAppearanceAppFontTypeLabel": { + "description": "Label for the app font selection dropdown" + }, + "settingsAppearanceFontWeightLabel": "Font Weight", + "@settingsAppearanceFontWeightLabel": { + "description": "Label for the font weight setting." + }, "settingsFeedTileTypeImageTop": "Image Top", - "@settingsFeedTileTypeImageTop": { "description": "Label for the feed tile type with image on top" }, + "@settingsFeedTileTypeImageTop": { + "description": "Label for the feed tile type with image on top" + }, "settingsFeedTileTypeImageStart": "Image Start", - "@settingsFeedTileTypeImageStart": { "description": "Label for the feed tile type with image at the start" }, + "@settingsFeedTileTypeImageStart": { + "description": "Label for the feed tile type with image at the start" + }, "settingsFeedTileTypeTextOnly": "Text Only", - "@settingsFeedTileTypeTextOnly": { "description": "Label for the feed tile type with text only" }, + "@settingsFeedTileTypeTextOnly": { + "description": "Label for the feed tile type with text only" + }, "settingsFeedTileTypeLabel": "Feed Tile Layout", - "@settingsFeedTileTypeLabel": { "description": "Label for the feed tile layout selection dropdown" }, + "@settingsFeedTileTypeLabel": { + "description": "Label for the feed tile layout selection dropdown" + }, "settingsArticleFontSizeLabel": "Article Font Size", - "@settingsArticleFontSizeLabel": { "description": "Label for the article font size selection dropdown" }, + "@settingsArticleFontSizeLabel": { + "description": "Label for the article font size selection dropdown" + }, "settingsNotificationsEnableLabel": "Enable Notifications", - "@settingsNotificationsEnableLabel": { "description": "Label for the switch to enable/disable notifications" }, + "@settingsNotificationsEnableLabel": { + "description": "Label for the switch to enable/disable notifications" + }, "settingsNotificationsCategoriesLabel": "Followed Categories", - "@settingsNotificationsCategoriesLabel": { "description": "Label for the section to select notification categories" }, + "@settingsNotificationsCategoriesLabel": { + "description": "Label for the section to select notification categories" + }, "settingsNotificationsSourcesLabel": "Followed Sources", - "@settingsNotificationsSourcesLabel": { "description": "Label for the section to select notification sources" }, + "@settingsNotificationsSourcesLabel": { + "description": "Label for the section to select notification sources" + }, "settingsNotificationsCountriesLabel": "Followed Countries", - "@settingsNotificationsCountriesLabel": { "description": "Label for the section to select notification countries" }, - + "@settingsNotificationsCountriesLabel": { + "description": "Label for the section to select notification countries" + }, "unknownError": "An unknown error occurred.", "@unknownError": { "description": "Generic error message shown when an operation fails unexpectedly" @@ -426,5 +507,39 @@ "loadMoreError": "Failed to load more items.", "@loadMoreError": { "description": "Error message shown when pagination fails to load the next set of items" + }, + "settingsAppearanceFontSizeExtraLarge": "Extra Large", + "@settingsAppearanceFontSizeExtraLarge": { + "description": "Label for the extra large font size option" + }, + "settingsAppearanceFontFamilySystemDefault": "System Default", + "@settingsAppearanceFontFamilySystemDefault": { + "description": "Label for the system default font family option" + }, + "emailCodeSentPageTitle": "Enter Code", + "@emailCodeSentPageTitle": { + "description": "AppBar title for the email code verification page" + }, + "emailCodeSentConfirmation": "A verification code has been sent to {email}. Please enter it below.", + "@emailCodeSentConfirmation": { + "description": "Confirmation message shown after the email code has been sent", + "placeholders": { + "email": { + "type": "String", + "example": "user@example.com" + } + } + }, + "emailCodeSentInstructions": "Enter the 6-digit code you received.", + "@emailCodeSentInstructions": { + "description": "Instructions for the user to enter the verification code" + }, + "emailCodeVerificationHint": "6-digit code", + "@emailCodeVerificationHint": { + "description": "Hint text for the email code verification input field" + }, + "emailCodeVerificationButtonLabel": "Verify Code", + "@emailCodeVerificationButtonLabel": { + "description": "Button text for verifying the email code" } } diff --git a/lib/main.dart b/lib/main.dart index 9416ebc4..6535bcf3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,99 +1,136 @@ -import 'package:cloud_firestore/cloud_firestore.dart'; -import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:ht_authentication_firebase/ht_authentication_firebase.dart'; -import 'package:ht_authentication_repository/ht_authentication_repository.dart'; -import 'package:ht_categories_firestore/ht_categories_firestore.dart'; -import 'package:ht_categories_repository/ht_categories_repository.dart'; -import 'package:ht_countries_firestore/ht_countries_firestore.dart'; -import 'package:ht_countries_repository/ht_countries_repository.dart'; -import 'package:ht_headlines_firestore/ht_headlines_firestore.dart'; -import 'package:ht_headlines_repository/ht_headlines_repository.dart'; -import 'package:ht_kv_storage_shared_preferences/ht_kv_storage_shared_preferences.dart'; -import 'package:ht_main/app/app.dart'; -import 'package:ht_main/bloc_observer.dart'; -import 'package:ht_main/firebase_options.dart'; -import 'package:ht_preferences_firestore/ht_preferences_firestore.dart'; // Added -import 'package:ht_preferences_repository/ht_preferences_repository.dart'; // Added -import 'package:ht_sources_firestore/ht_sources_firestore.dart'; -import 'package:ht_sources_repository/ht_sources_repository.dart'; +import 'package:ht_auth_api/ht_auth_api.dart'; // Concrete Auth Client Impl +import 'package:ht_auth_repository/ht_auth_repository.dart'; // Auth Repository +import 'package:ht_data_api/ht_data_api.dart'; // Concrete Data Client Impl +import 'package:ht_data_repository/ht_data_repository.dart'; // Data Repository +import 'package:ht_http_client/ht_http_client.dart'; // HTTP Client +import 'package:ht_kv_storage_shared_preferences/ht_kv_storage_shared_preferences.dart'; // KV Storage Impl +import 'package:ht_main/app/app.dart'; // The App widget +import 'package:ht_main/bloc_observer.dart'; // App Bloc Observer +import 'package:ht_shared/ht_shared.dart'; // Shared models, FromJson, ToJson, etc. void main() async { WidgetsFlutterBinding.ensureInitialized(); - await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); Bloc.observer = const AppBlocObserver(); + // 1. Instantiate KV Storage Service final kvStorage = await HtKvStorageSharedPreferences.getInstance(); - // --- Instantiate Repositories --- - // 1. Authentication Repository - // Define ActionCodeSettings for email link sign-in - final actionCodeSettings = ActionCodeSettings( - url: 'https://htmain.page.link/finishLogin', - handleCodeInApp: true, - iOSBundleId: 'com.example.htMain', - androidPackageName: 'com.example.ht_main', - androidInstallApp: true, - androidMinimumVersion: '12', // Optional: Specify minimum Android version + // 2. Declare Auth Repository (will be initialized after TokenProvider) + // This is necessary because the TokenProvider needs a reference to the + // authenticationRepository instance before it's fully initialized. + late final HtAuthRepository authenticationRepository; + + // 3. Define Token Provider + // TODO(refactor): This is a temporary workaround. The HtAuthRepository + // should be refactored to provide a public method/getter to retrieve + // the current authentication token string. This function should then + // call that method. + Future tokenProvider() async { + // For now, return null as we don't have a way to get the token + // from the current HtAuthRepository implementation. + // The HtHttpClient will make unauthenticated requests by default. + // The authentication flow will handle obtaining and storing the token + // via the HtAuthRepository's signIn/verify methods. + // A future refactor is needed to make the token available here. + return null; + } + + // 4. Instantiate HTTP Client + final httpClient = HtHttpClient( + baseUrl: 'http://localhost:8080', // Provided base URL for Dart Frog backend + tokenProvider: tokenProvider, + ); + + // 5. Instantiate Auth Client and Repository + // Concrete client implementation is HtAuthApi from ht_auth_api + final authClient = HtAuthApi(httpClient: httpClient); + // Initialize the authenticationRepository instance + authenticationRepository = HtAuthRepository( + authClient: authClient, + // storageService is not a parameter based on errors. + // Token persistence must be handled within HtAuthRepository using HtKVStorageService internally. + ); + + // 6. Instantiate Data Clients and Repositories for each model type + // Concrete client implementation is HtDataApi from ht_data_api + // Each client needs the httpClient, a modelName string, and fromJson/toJson functions. + + final headlinesClient = HtDataApi( + httpClient: httpClient, + modelName: 'headline', // Assuming 'headline' is the model name for the API + fromJson: Headline.fromJson, + toJson: (headline) => headline.toJson(), + ); + final headlinesRepository = HtDataRepository( + dataClient: headlinesClient, ); - final authenticationClient = HtAuthenticationFirebase( - actionCodeSettings: actionCodeSettings, + final categoriesClient = HtDataApi( + httpClient: httpClient, + modelName: 'category', // Assuming 'category' is the model name for the API + fromJson: Category.fromJson, + toJson: (category) => category.toJson(), ); - final authenticationRepository = HtAuthenticationRepository( - authenticationClient: authenticationClient, - storageService: kvStorage, + final categoriesRepository = HtDataRepository( + dataClient: categoriesClient, ); - // 2. Headlines Repository - final firestore = FirebaseFirestore.instance; - final headlinesClient = HtHeadlinesFirestore(firestore: firestore); - final headlinesRepository = HtHeadlinesRepository(client: headlinesClient); + final countriesClient = HtDataApi( + httpClient: httpClient, + modelName: 'country', // Assuming 'country' is the model name for the API + fromJson: Country.fromJson, + toJson: (country) => country.toJson(), + ); + final countriesRepository = HtDataRepository( + dataClient: countriesClient, + ); - // 3. Categories Repository - final categoriesClient = HtCategoriesFirestore(firestore: firestore); - final categoriesRepository = HtCategoriesRepository( - categoriesClient: categoriesClient, + final sourcesClient = HtDataApi( + httpClient: httpClient, + modelName: 'source', // Assuming 'source' is the model name for the API + fromJson: Source.fromJson, + toJson: (source) => source.toJson(), ); + final sourcesRepository = HtDataRepository(dataClient: sourcesClient); - // 4. Countries Repository - final countriesClient = HtCountriesFirestore(firestore: firestore); - final countriesRepository = HtCountriesRepository( - countriesClient: countriesClient, + final userContentPreferencesClient = HtDataApi( + httpClient: httpClient, + modelName: 'user_content_preferences', // Assuming model name + fromJson: UserContentPreferences.fromJson, + toJson: (prefs) => prefs.toJson(), ); + final userContentPreferencesRepository = + HtDataRepository( + dataClient: userContentPreferencesClient, + ); - // 5. Sources Repository - final sourcesClient = HtSourcesFirestore(firestore: firestore); - final sourcesRepository = HtSourcesRepository(sourcesClient: sourcesClient); + final userAppSettingsClient = HtDataApi( + httpClient: httpClient, + modelName: 'user_app_settings', // Assuming model name + fromJson: UserAppSettings.fromJson, + toJson: (settings) => settings.toJson(), + ); + final userAppSettingsRepository = HtDataRepository( + dataClient: userAppSettingsClient, + ); - // 6. Preferences Repository (Added) - // IMPORTANT: This assumes currentUser is immediately available after auth repo init. - // If not, initialization might need to be deferred or handled differently. - final currentUserId = - authenticationRepository.currentUser.uid; // Assuming 'uid' property - // Firestore typically requires non-empty document IDs. - // Handle cases where the user might be anonymous or ID is empty. - if (currentUserId.isEmpty) { - // Option 1: Throw an error if user ID is required but missing - // throw StateError('User ID is empty, cannot initialize preferences.'); - // Option 2: Use a default ID for anonymous/uninitialized users (use cautiously) - // currentUserId = 'anonymous_user_settings'; - // Option 3: Let the client handle it (assuming it can) - print( - 'Warning: Initializing HtPreferencesFirestore with an empty user ID.', - ); - } - final preferencesClient = HtPreferencesFirestore( - firestore: firestore, - userId: currentUserId, // Pass userId directly + // Assuming AppConfig model exists in ht_shared and has fromJson/toJson + final appConfigClient = HtDataApi( + httpClient: httpClient, + modelName: 'app_config', // Assuming model name + fromJson: AppConfig.fromJson, + toJson: (config) => config.toJson(), ); - final preferencesRepository = HtPreferencesRepository( - preferencesClient: preferencesClient, - // maxHistorySize: 25, // Default is 25 in the provided repo code + final appConfigRepository = HtDataRepository( + dataClient: appConfigClient, ); - // --- End Instantiation --- + // 7. Run the App, injecting repositories + // NOTE: The App widget constructor currently expects specific repository types. + // This will cause type errors that will be fixed in the next step (Step 3) + // when we refactor the App widget and router. runApp( App( htAuthenticationRepository: authenticationRepository, @@ -101,7 +138,9 @@ void main() async { htCategoriesRepository: categoriesRepository, htCountriesRepository: countriesRepository, htSourcesRepository: sourcesRepository, - htPreferencesRepository: preferencesRepository, // Added + htUserAppSettingsRepository: userAppSettingsRepository, + htUserContentPreferencesRepository: userContentPreferencesRepository, + htAppConfigRepository: appConfigRepository, kvStorageService: kvStorage, ), ); diff --git a/lib/router/router.dart b/lib/router/router.dart index 34fd8dcf..3ee518f7 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -1,18 +1,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; -import 'package:ht_authentication_repository/ht_authentication_repository.dart'; -import 'package:ht_categories_repository/ht_categories_repository.dart'; -import 'package:ht_countries_repository/ht_countries_repository.dart'; -import 'package:ht_headlines_repository/ht_headlines_repository.dart'; +import 'package:ht_auth_repository/ht_auth_repository.dart'; // Auth Repository +import 'package:ht_data_repository/ht_data_repository.dart'; // Generic Data Repository import 'package:ht_main/account/bloc/account_bloc.dart'; import 'package:ht_main/account/view/account_page.dart'; import 'package:ht_main/app/bloc/app_bloc.dart'; import 'package:ht_main/app/view/app_shell.dart'; import 'package:ht_main/authentication/bloc/authentication_bloc.dart'; import 'package:ht_main/authentication/view/authentication_page.dart'; -import 'package:ht_main/authentication/view/email_link_sent_page.dart'; -import 'package:ht_main/authentication/view/email_sign_in_page.dart'; +import 'package:ht_main/authentication/view/email_code_verification_page.dart'; +import 'package:ht_main/authentication/view/request_code_page.dart'; // Will be renamed to request_code_page.dart later import 'package:ht_main/headline-details/bloc/headline_details_bloc.dart'; import 'package:ht_main/headline-details/view/headline_details_page.dart'; import 'package:ht_main/headlines-feed/bloc/categories_filter_bloc.dart'; // Import new BLoC @@ -34,8 +32,7 @@ import 'package:ht_main/settings/view/article_settings_page.dart'; // Added import 'package:ht_main/settings/view/feed_settings_page.dart'; // Added import 'package:ht_main/settings/view/notification_settings_page.dart'; // Added import 'package:ht_main/settings/view/settings_page.dart'; // Added -import 'package:ht_preferences_repository/ht_preferences_repository.dart'; // Added -import 'package:ht_sources_repository/ht_sources_repository.dart'; +import 'package:ht_shared/ht_shared.dart'; // Shared models, FromJson, ToJson, etc. /// Creates and configures the GoRouter instance for the application. /// @@ -43,12 +40,15 @@ import 'package:ht_sources_repository/ht_sources_repository.dart'; /// authentication state changes. GoRouter createRouter({ required ValueNotifier authStatusNotifier, - required HtAuthenticationRepository htAuthenticationRepository, - required HtHeadlinesRepository htHeadlinesRepository, - required HtCategoriesRepository htCategoriesRepository, - required HtCountriesRepository htCountriesRepository, - required HtSourcesRepository htSourcesRepository, - required HtPreferencesRepository htPreferencesRepository, // Added + required HtAuthRepository htAuthenticationRepository, + required HtDataRepository htHeadlinesRepository, + required HtDataRepository htCategoriesRepository, + required HtDataRepository htCountriesRepository, + required HtDataRepository htSourcesRepository, + required HtDataRepository htUserAppSettingsRepository, + required HtDataRepository + htUserContentPreferencesRepository, + required HtDataRepository htAppConfigRepository, }) { return GoRouter( refreshListenable: authStatusNotifier, @@ -78,11 +78,11 @@ GoRouter createRouter({ // Base paths for major sections. const authenticationPath = Routes.authentication; // '/authentication' const feedPath = Routes.feed; // Updated path constant - // Specific authentication sub-routes crucial for the email linking flow. - const emailSignInPath = - '$authenticationPath/${Routes.emailSignIn}'; // '/authentication/email-sign-in' - const emailLinkSentPath = - '$authenticationPath/${Routes.emailLinkSent}'; // '/authentication/email-link-sent' + // Specific authentication sub-routes crucial for the email code verification flow. + const requestCodePath = + '$authenticationPath/${Routes.requestCode}'; // '/authentication/request-code' + const verifyCodePath = + '$authenticationPath/${Routes.verifyCode}'; // '/authentication/verify-code' // --- Helper Booleans --- // Check if the navigation target is within the authentication section. @@ -155,13 +155,14 @@ GoRouter createRouter({ return feedPath; // Redirect to feed } } - // **Sub-Case 2.2: Navigating to Specific Email Linking Sub-Routes** - // Explicitly allow access to the necessary pages for the email linking process, + // **Sub-Case 2.2: Navigating to Specific Email Code Verification Sub-Routes** + // Explicitly allow access to the necessary pages for the email code verification process, // even if the 'context=linking' parameter is lost during navigation between these pages. - else if (currentLocation == emailSignInPath || - currentLocation == emailLinkSentPath) { + else if (currentLocation == requestCodePath || + currentLocation.startsWith(verifyCodePath)) { + // Use startsWith for parameterized path print( - ' Action: Allowing navigation to email linking sub-route ($currentLocation).', + ' Action: Allowing navigation to email code verification sub-route ($currentLocation).', ); return null; // Allow access } @@ -217,9 +218,8 @@ GoRouter createRouter({ // print(' Redirect Decision: No specific redirect condition met. Allowing navigation.'); // return null; // Allow access (already covered by the final return null below) }, - // --- Routes --- + // --- Authentication Routes --- routes: [ - // --- Authentication Routes --- GoRoute( path: Routes.authentication, name: Routes.authenticationName, @@ -247,7 +247,7 @@ GoRouter createRouter({ return BlocProvider( create: (context) => AuthenticationBloc( - authenticationRepository: htAuthenticationRepository, + authenticationRepository: context.read(), ), child: AuthenticationPage( headline: headline, @@ -259,18 +259,26 @@ GoRouter createRouter({ }, routes: [ GoRoute( - path: Routes.emailSignIn, - name: Routes.emailSignInName, + path: Routes.requestCode, // Use new path + name: Routes.requestCodeName, // Use new name builder: (context, state) { // Extract the linking context flag from 'extra', default to false. final isLinking = (state.extra as bool?) ?? false; - return EmailSignInPage(isLinkingContext: isLinking); + return EmailSignInPage( + isLinkingContext: isLinking, + ); // Page will be renamed later }, ), GoRoute( - path: Routes.emailLinkSent, - name: Routes.emailLinkSentName, - builder: (context, state) => const EmailLinkSentPage(), + path: + '${Routes.verifyCode}/:email', // Use new path with email parameter + name: Routes.verifyCodeName, // Use new name + builder: (context, state) { + final email = state.pathParameters['email']!; // Extract email + return EmailCodeVerificationPage( + email: email, + ); // Use renamed page + }, ), ], ), @@ -283,19 +291,25 @@ GoRouter createRouter({ BlocProvider( create: (context) => HeadlinesFeedBloc( - headlinesRepository: htHeadlinesRepository, + headlinesRepository: + context.read>(), )..add(const HeadlinesFeedFetchRequested()), ), BlocProvider( create: (context) => HeadlinesSearchBloc( - headlinesRepository: htHeadlinesRepository, + headlinesRepository: + context.read>(), ), ), BlocProvider( create: (context) => AccountBloc( - authenticationRepository: htAuthenticationRepository, + authenticationRepository: + context.read(), + userContentPreferencesRepository: + context + .read>(), ), ), ], @@ -320,7 +334,8 @@ GoRouter createRouter({ return BlocProvider( create: (context) => HeadlineDetailsBloc( - headlinesRepository: htHeadlinesRepository, + headlinesRepository: + context.read>(), )..add(HeadlineDetailsRequested(headlineId: id)), child: HeadlineDetailsPage(headlineId: id), ); @@ -364,7 +379,8 @@ GoRouter createRouter({ create: (context) => CategoriesFilterBloc( categoriesRepository: - context.read(), + context + .read>(), ), child: const CategoryFilterPage(), ), @@ -381,7 +397,8 @@ GoRouter createRouter({ create: (context) => SourcesFilterBloc( sourcesRepository: - context.read(), + context + .read>(), ), child: const SourceFilterPage(), ), @@ -398,7 +415,8 @@ GoRouter createRouter({ create: (context) => CountriesFilterBloc( countriesRepository: - context.read(), + context + .read>(), ), child: const CountryFilterPage(), ), @@ -436,8 +454,11 @@ GoRouter createRouter({ return BlocProvider( create: (context) => SettingsBloc( - preferencesRepository: - context.read(), + userAppSettingsRepository: + context + .read< + HtDataRepository + >(), )..add( const SettingsLoadRequested(), ), // Load on entry @@ -473,6 +494,31 @@ GoRouter createRouter({ ), ], ), + // New routes for Account sub-pages + GoRoute( + path: + Routes + .accountContentPreferences, // Relative path 'content-preferences' + name: Routes.accountContentPreferencesName, + builder: (context, state) { + // TODO(fulleni): Replace with actual ContentPreferencesPage + return const Placeholder( + child: Center(child: Text('CONTENT PREFERENCES PAGE')), + ); + }, + ), + GoRoute( + path: + Routes + .accountSavedHeadlines, // Relative path 'saved-headlines' + name: Routes.accountSavedHeadlinesName, + builder: (context, state) { + // TODO(fulleni): Replace with actual SavedHeadlinesPage + return const Placeholder( + child: Center(child: Text('SAVED HEADLINES PAGE')), + ); + }, + ), ], ), ], diff --git a/lib/router/routes.dart b/lib/router/routes.dart index 2d56dc9f..f9dd885d 100644 --- a/lib/router/routes.dart +++ b/lib/router/routes.dart @@ -48,11 +48,11 @@ abstract final class Routes { static const accountLinking = 'linking'; // Query param context, not a path static const accountLinkingName = 'accountLinking'; // Name for context - // routes for email sign-in flow - static const emailSignIn = 'email-sign-in'; - static const emailSignInName = 'emailSignIn'; - static const emailLinkSent = 'email-link-sent'; - static const emailLinkSentName = 'emailLinkSent'; + // routes for email code verification flow + static const requestCode = 'request-code'; + static const requestCodeName = 'requestCode'; + static const verifyCode = 'verify-code'; + static const verifyCodeName = 'verifyCode'; // --- Settings Sub-Routes (relative to /account/settings) --- static const settingsAppearance = 'appearance'; @@ -66,4 +66,10 @@ abstract final class Routes { // Add names for notification sub-selection routes if needed later // static const settingsNotificationCategories = 'categories'; // static const settingsNotificationCategoriesName = 'settingsNotificationCategories'; + + // --- Account Sub-Routes (relative to /account) --- + static const accountContentPreferences = 'content-preferences'; + static const accountContentPreferencesName = 'accountContentPreferences'; + static const accountSavedHeadlines = 'saved-headlines'; + static const accountSavedHeadlinesName = 'accountSavedHeadlines'; } diff --git a/lib/settings/bloc/settings_bloc.dart b/lib/settings/bloc/settings_bloc.dart index 1399f13a..3f9377b4 100644 --- a/lib/settings/bloc/settings_bloc.dart +++ b/lib/settings/bloc/settings_bloc.dart @@ -3,8 +3,8 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:bloc_concurrency/bloc_concurrency.dart'; import 'package:equatable/equatable.dart'; -import 'package:ht_preferences_client/ht_preferences_client.dart'; // Import models and exceptions -import 'package:ht_preferences_repository/ht_preferences_repository.dart'; +import 'package:ht_data_repository/ht_data_repository.dart'; // Generic Data Repository +import 'package:ht_shared/ht_shared.dart'; // Shared models, including UserAppSettings and UserContentPreferences part 'settings_event.dart'; // Contains event definitions part 'settings_state.dart'; @@ -12,14 +12,15 @@ part 'settings_state.dart'; /// {@template settings_bloc} /// Manages the state for the application settings feature. /// -/// Handles loading settings from [HtPreferencesRepository] and processing +/// Handles loading settings from [HtDataRepository] and processing /// user actions to update settings. /// {@endtemplate} class SettingsBloc extends Bloc { /// {@macro settings_bloc} - SettingsBloc({required HtPreferencesRepository preferencesRepository}) - : _preferencesRepository = preferencesRepository, - super(const SettingsState()) { + SettingsBloc({ + required HtDataRepository userAppSettingsRepository, + }) : _userAppSettingsRepository = userAppSettingsRepository, + super(const SettingsState()) { // Register event handlers on(_onLoadRequested); on( @@ -42,17 +43,13 @@ class SettingsBloc extends Bloc { _onFeedTileTypeChanged, transformer: sequential(), ); - on( - _onArticleFontSizeChanged, // Corrected handler name if it was misspelled - transformer: sequential(), - ); - on( - _onNotificationsEnabledChanged, // Corrected handler name if it was misspelled - transformer: sequential(), - ); + // on( + // _onNotificationsEnabledChanged, // Corrected handler name if it was misspelled + // transformer: sequential(), + // ); } - final HtPreferencesRepository _preferencesRepository; + final HtDataRepository _userAppSettingsRepository; /// Handles the initial loading of all settings. Future _onLoadRequested( @@ -62,47 +59,27 @@ class SettingsBloc extends Bloc { emit(state.copyWith(status: SettingsStatus.loading)); try { // Fetch all settings concurrently - final results = await Future.wait([ - _tryFetch(_preferencesRepository.getAppSettings), - _tryFetch(_preferencesRepository.getArticleSettings), - _tryFetch(_preferencesRepository.getThemeSettings), - _tryFetch(_preferencesRepository.getFeedSettings), - _tryFetch(_preferencesRepository.getNotificationSettings), - ]); + // Note: UserAppSettings and UserContentPreferences are fetched as single objects + // from the new generic repositories. + // TODO(cline): Get actual user ID + final appSettings = await _userAppSettingsRepository.read( + id: 'user_id', + ); // Assuming a fixed ID for user settings - // Process results, using defaults from initial state if fetch returned null + // Process results emit( state.copyWith( status: SettingsStatus.success, - appSettings: results[0] as AppSettings? ?? state.appSettings, - articleSettings: - results[1] as ArticleSettings? ?? state.articleSettings, - themeSettings: results[2] as ThemeSettings? ?? state.themeSettings, - feedSettings: results[3] as FeedSettings? ?? state.feedSettings, - notificationSettings: - results[4] as NotificationSettings? ?? state.notificationSettings, + userAppSettings: appSettings, // Update state with new model clearError: true, ), ); - } catch (e) { - // If any fetch failed beyond PreferenceNotFoundException + } on HtHttpException catch (e) { + // Catch standardized HTTP exceptions emit(state.copyWith(status: SettingsStatus.failure, error: e)); - } - } - - /// Helper to fetch a setting and handle PreferenceNotFoundException gracefully. - Future _tryFetch(Future Function() fetcher) async { - try { - return await fetcher(); - } on PreferenceNotFoundException { - // Setting not found, return null to use default from state - return null; - } on PreferenceUpdateException { - // Rethrow other update/fetch exceptions to be caught by the caller - rethrow; } catch (e) { - // Rethrow unexpected errors - rethrow; + // Catch any other unexpected errors + emit(state.copyWith(status: SettingsStatus.failure, error: e)); } } @@ -111,31 +88,26 @@ class SettingsBloc extends Bloc { SettingsAppThemeModeChanged event, Emitter emit, ) async { - // Manually create new instance as copyWith is missing - final newThemeSettings = ThemeSettings( - themeMode: event.themeMode, - themeName: state.themeSettings.themeName, // Keep existing value - ); - // Optimistically update UI - emit( - state.copyWith( - status: SettingsStatus.success, // Keep success state - themeSettings: newThemeSettings, - ), - ); // Removed trailing comma - + // Read current settings, update, and save try { - await _preferencesRepository.setThemeSettings(newThemeSettings); - // No need to emit again on success, UI already updated - } catch (e) { - // Revert optimistic update on failure and show error - emit( - state.copyWith( - status: SettingsStatus.failure, - themeSettings: state.themeSettings, // Revert to previous - error: e, + // TODO(cline): Get actual user ID + final currentSettings = await _userAppSettingsRepository.read( + id: 'user_id', + ); + final updatedSettings = currentSettings.copyWith( + displaySettings: currentSettings.displaySettings.copyWith( + baseTheme: event.themeMode, ), - ); // Removed trailing comma + ); + await _userAppSettingsRepository.update( + id: 'user_id', + item: updatedSettings, + ); + emit(state.copyWith(userAppSettings: updatedSettings, clearError: true)); + } on HtHttpException catch (e) { + emit(state.copyWith(status: SettingsStatus.failure, error: e)); + } catch (e) { + emit(state.copyWith(status: SettingsStatus.failure, error: e)); } } @@ -144,22 +116,26 @@ class SettingsBloc extends Bloc { SettingsAppThemeNameChanged event, Emitter emit, ) async { - // Manually create new instance - final newThemeSettings = ThemeSettings( - themeMode: state.themeSettings.themeMode, // Keep existing value - themeName: event.themeName, - ); - emit(state.copyWith(themeSettings: newThemeSettings)); + // Read current settings, update, and save try { - await _preferencesRepository.setThemeSettings(newThemeSettings); - } catch (e) { - emit( - state.copyWith( - status: SettingsStatus.failure, - themeSettings: state.themeSettings, - error: e, + // TODO(cline): Get actual user ID + final currentSettings = await _userAppSettingsRepository.read( + id: 'user_id', + ); + final updatedSettings = currentSettings.copyWith( + displaySettings: currentSettings.displaySettings.copyWith( + accentTheme: event.themeName, ), - ); // Removed trailing comma + ); + await _userAppSettingsRepository.update( + id: 'user_id', + item: updatedSettings, + ); + emit(state.copyWith(userAppSettings: updatedSettings, clearError: true)); + } on HtHttpException catch (e) { + emit(state.copyWith(status: SettingsStatus.failure, error: e)); + } catch (e) { + emit(state.copyWith(status: SettingsStatus.failure, error: e)); } } @@ -168,22 +144,26 @@ class SettingsBloc extends Bloc { SettingsAppFontSizeChanged event, Emitter emit, ) async { - // Manually create new instance - final newAppSettings = AppSettings( - appFontSize: event.fontSize, - appFontType: state.appSettings.appFontType, // Keep existing value - ); - emit(state.copyWith(appSettings: newAppSettings)); + // Read current settings, update, and save try { - await _preferencesRepository.setAppSettings(newAppSettings); - } catch (e) { - emit( - state.copyWith( - status: SettingsStatus.failure, - appSettings: state.appSettings, - error: e, + // TODO(cline): Get actual user ID + final currentSettings = await _userAppSettingsRepository.read( + id: 'user_id', + ); + final updatedSettings = currentSettings.copyWith( + displaySettings: currentSettings.displaySettings.copyWith( + textScaleFactor: event.fontSize, ), - ); // Removed trailing comma + ); + await _userAppSettingsRepository.update( + id: 'user_id', + item: updatedSettings, + ); + emit(state.copyWith(userAppSettings: updatedSettings, clearError: true)); + } on HtHttpException catch (e) { + emit(state.copyWith(status: SettingsStatus.failure, error: e)); + } catch (e) { + emit(state.copyWith(status: SettingsStatus.failure, error: e)); } } @@ -192,22 +172,26 @@ class SettingsBloc extends Bloc { SettingsAppFontTypeChanged event, Emitter emit, ) async { - // Manually create new instance - final newAppSettings = AppSettings( - appFontSize: state.appSettings.appFontSize, // Keep existing value - appFontType: event.fontType, - ); - emit(state.copyWith(appSettings: newAppSettings)); + // Read current settings, update, and save try { - await _preferencesRepository.setAppSettings(newAppSettings); - } catch (e) { - emit( - state.copyWith( - status: SettingsStatus.failure, - appSettings: state.appSettings, - error: e, + // TODO(cline): Get actual user ID + final currentSettings = await _userAppSettingsRepository.read( + id: 'user_id', + ); + final updatedSettings = currentSettings.copyWith( + displaySettings: currentSettings.displaySettings.copyWith( + fontFamily: event.fontType, ), - ); // Removed trailing comma + ); + await _userAppSettingsRepository.update( + id: 'user_id', + item: updatedSettings, + ); + emit(state.copyWith(userAppSettings: updatedSettings, clearError: true)); + } on HtHttpException catch (e) { + emit(state.copyWith(status: SettingsStatus.failure, error: e)); + } catch (e) { + emit(state.copyWith(status: SettingsStatus.failure, error: e)); } } @@ -216,71 +200,66 @@ class SettingsBloc extends Bloc { SettingsFeedTileTypeChanged event, Emitter emit, ) async { - // Manually create new instance - final newFeedSettings = FeedSettings(feedListTileType: event.tileType); - emit(state.copyWith(feedSettings: newFeedSettings)); + // Read current settings, update, and save try { - await _preferencesRepository.setFeedSettings(newFeedSettings); - } catch (e) { - emit( - state.copyWith( - status: SettingsStatus.failure, - feedSettings: state.feedSettings, - error: e, + // TODO(cline): Get actual user ID + final currentSettings = await _userAppSettingsRepository.read( + id: 'user_id', + ); + // Note: This event currently only handles HeadlineImageStyle. + // A separate event/logic might be needed for HeadlineDensity. + final updatedSettings = currentSettings.copyWith( + feedPreferences: currentSettings.feedPreferences.copyWith( + headlineImageStyle: event.tileType, ), - ); // Removed trailing comma - } - } - - /// Handles changes to the Article Font Size setting. - Future _onArticleFontSizeChanged( - SettingsArticleFontSizeChanged event, - Emitter emit, - ) async { - // Manually create new instance - final newArticleSettings = ArticleSettings(articleFontSize: event.fontSize); - emit(state.copyWith(articleSettings: newArticleSettings)); - try { - await _preferencesRepository.setArticleSettings(newArticleSettings); + ); + await _userAppSettingsRepository.update( + id: 'user_id', + item: updatedSettings, + ); + emit(state.copyWith(userAppSettings: updatedSettings, clearError: true)); + } on HtHttpException catch (e) { + emit(state.copyWith(status: SettingsStatus.failure, error: e)); } catch (e) { - emit( - state.copyWith( - status: SettingsStatus.failure, - articleSettings: state.articleSettings, - error: e, - ), - ); // Removed trailing comma + emit(state.copyWith(status: SettingsStatus.failure, error: e)); } } /// Handles changes to the Notifications Enabled setting. - Future _onNotificationsEnabledChanged( - SettingsNotificationsEnabledChanged event, - Emitter emit, - ) async { - // Manually create new instance - // Note: This only updates the 'enabled' flag. Updating followed items - // would require copying the lists as well. - final newNotificationSettings = NotificationSettings( - enabled: event.enabled, - categoryNotifications: state.notificationSettings.categoryNotifications, - sourceNotifications: state.notificationSettings.sourceNotifications, - followedEventCountryIds: - state.notificationSettings.followedEventCountryIds, - ); - emit(state.copyWith(notificationSettings: newNotificationSettings)); - try { - await _preferencesRepository.setNotificationSettings( - newNotificationSettings, - ); - } catch (e) { - emit( - state.copyWith( - status: SettingsStatus.failure, - notificationSettings: state.notificationSettings, - error: e, - ), - ); // Removed trailing comma - } - } -} // Added closing brace + // Future _onNotificationsEnabledChanged( + // SettingsNotificationsEnabledChanged event, + // Emitter emit, + // ) async { + // // Read current preferences, update, and save + // try { + // // TODO(cline): Get actual user ID + // final currentPreferences = await _userContentPreferencesRepository.read(id: 'user_id'); + // // Note: This only updates the 'enabled' flag. Updating followed items + // // would require copying the lists as well. + // // The NotificationSettings model from the old preferences client doesn't directly map + // // to UserContentPreferences. Assuming notification enabled state is part of UserAppSettings. + // // Re-evaluating based on UserAppSettings model... UserAppSettings has engagementShownCounts + // // and engagementLastShownTimestamps, but no general notification enabled flag. + // // This suggests the notification enabled setting might need to be added to UserAppSettings + // // or handled differently. For now, I will add a TODO and emit a failure state. + // // TODO(cline): Determine where notification enabled setting is stored in new models. + // emit(state.copyWith(status: SettingsStatus.failure, error: Exception('Notification enabled setting location in new models is TBD.'))); + + // // If it were in UserAppSettings: + // /* + // final currentSettings = await _userAppSettingsRepository.read(id: 'user_id'); + // final updatedSettings = currentSettings.copyWith( + // // Assuming a field like 'notificationsEnabled' exists in UserAppSettings + // notificationsEnabled: event.enabled, + // ); + // await _userAppSettingsRepository.update(id: 'user_id', item: updatedSettings); + // emit(state.copyWith(userAppSettings: updatedSettings, clearError: true)); + // */ + + // } on HtHttpException catch (e) { + // emit(state.copyWith(status: SettingsStatus.failure, error: e)); + // } catch (e) { + // emit(state.copyWith(status: SettingsStatus.failure, error: e)); + // } + // } +} diff --git a/lib/settings/bloc/settings_event.dart b/lib/settings/bloc/settings_event.dart index 3aadfad3..865ffaa7 100644 --- a/lib/settings/bloc/settings_event.dart +++ b/lib/settings/bloc/settings_event.dart @@ -1,7 +1,9 @@ +part of 'settings_bloc.dart'; + // // ignore_for_file: avoid_positional_boolean_parameters -part of 'settings_bloc.dart'; +// Import models and enums from ht_shared /// {@template settings_event} /// Base class for all events related to the settings feature. @@ -32,7 +34,7 @@ class SettingsAppThemeModeChanged extends SettingsEvent { const SettingsAppThemeModeChanged(this.themeMode); /// The newly selected theme mode. - final AppThemeMode themeMode; + final AppBaseTheme themeMode; // Use AppBaseTheme from ht_shared @override List get props => [themeMode]; @@ -46,7 +48,7 @@ class SettingsAppThemeNameChanged extends SettingsEvent { const SettingsAppThemeNameChanged(this.themeName); /// The newly selected theme name. - final AppThemeName themeName; + final AppAccentTheme themeName; // Use AppAccentTheme from ht_shared @override List get props => [themeName]; @@ -60,7 +62,7 @@ class SettingsAppFontSizeChanged extends SettingsEvent { const SettingsAppFontSizeChanged(this.fontSize); /// The newly selected font size. - final FontSize fontSize; + final AppTextScaleFactor fontSize; // Use AppTextScaleFactor from ht_shared @override List get props => [fontSize]; @@ -74,12 +76,26 @@ class SettingsAppFontTypeChanged extends SettingsEvent { const SettingsAppFontTypeChanged(this.fontType); /// The newly selected font type. - final AppFontType fontType; + final String fontType; // Use String for fontFamily @override List get props => [fontType]; } +/// {@template settings_app_font_weight_changed} +/// Event added when the user changes the global app font weight. +/// {@endtemplate} +class SettingsAppFontWeightChanged extends SettingsEvent { + /// {@macro settings_app_font_weight_changed} + const SettingsAppFontWeightChanged(this.fontWeight); + + /// The newly selected font weight. + final AppFontWeight fontWeight; // Use AppFontWeight from ht_shared + + @override + List get props => [fontWeight]; +} + // --- Feed Settings Events --- /// {@template settings_feed_tile_type_changed} @@ -90,28 +106,13 @@ class SettingsFeedTileTypeChanged extends SettingsEvent { const SettingsFeedTileTypeChanged(this.tileType); /// The newly selected feed list tile type. - final FeedListTileType tileType; + // Note: This event might need to be split into density and image style changes. + final HeadlineImageStyle tileType; // Use HeadlineImageStyle from ht_shared @override List get props => [tileType]; } -// --- Article Settings Events --- - -/// {@template settings_article_font_size_changed} -/// Event added when the user changes the article content font size. -/// {@endtemplate} -class SettingsArticleFontSizeChanged extends SettingsEvent { - /// {@macro settings_article_font_size_changed} - const SettingsArticleFontSizeChanged(this.fontSize); - - /// The newly selected font size for articles. - final FontSize fontSize; - - @override - List get props => [fontSize]; -} - // --- Notification Settings Events --- /// {@template settings_notifications_enabled_changed} diff --git a/lib/settings/bloc/settings_state.dart b/lib/settings/bloc/settings_state.dart index 91dc852b..7a12321f 100644 --- a/lib/settings/bloc/settings_state.dart +++ b/lib/settings/bloc/settings_state.dart @@ -23,47 +23,24 @@ class SettingsState extends Equatable { /// {@macro settings_state} const SettingsState({ this.status = SettingsStatus.initial, - this.appSettings = const AppSettings( - // Default value - appFontSize: FontSize.medium, - appFontType: AppFontType.roboto, - ), - this.articleSettings = const ArticleSettings( - // Default value - articleFontSize: FontSize.medium, - ), - this.themeSettings = const ThemeSettings( - // Default value - themeMode: AppThemeMode.system, - themeName: AppThemeName.grey, - ), - this.feedSettings = const FeedSettings( - // Default value - feedListTileType: FeedListTileType.imageStart, - ), - this.notificationSettings = const NotificationSettings( - enabled: false, - ), // Default + // Use new models from ht_shared + this.userAppSettings = const UserAppSettings( + id: '', + ), // Provide a default empty instance + this.userContentPreferences = const UserContentPreferences( + id: '', + ), // Provide a default empty instance this.error, }); /// The current status of loading/updating settings. final SettingsStatus status; - /// Current application-wide settings (font size, font type). - final AppSettings appSettings; + /// Current user application settings. + final UserAppSettings userAppSettings; - /// Current settings specific to article display (font size). - final ArticleSettings articleSettings; - - /// Current theme settings (mode, name/color scheme). - final ThemeSettings themeSettings; - - /// Current settings for the news feed display (tile type). - final FeedSettings feedSettings; - - /// Current notification settings (enabled, followed items). - final NotificationSettings notificationSettings; + /// Current user content preferences. + final UserContentPreferences userContentPreferences; /// An optional error object if the status is [SettingsStatus.failure]. final Object? error; @@ -71,21 +48,16 @@ class SettingsState extends Equatable { /// Creates a copy of the current state with updated values. SettingsState copyWith({ SettingsStatus? status, - AppSettings? appSettings, - ArticleSettings? articleSettings, - ThemeSettings? themeSettings, - FeedSettings? feedSettings, - NotificationSettings? notificationSettings, + UserAppSettings? userAppSettings, // Update parameter type + UserContentPreferences? userContentPreferences, // Update parameter type Object? error, bool clearError = false, // Flag to explicitly clear error }) { return SettingsState( status: status ?? this.status, - appSettings: appSettings ?? this.appSettings, - articleSettings: articleSettings ?? this.articleSettings, - themeSettings: themeSettings ?? this.themeSettings, - feedSettings: feedSettings ?? this.feedSettings, - notificationSettings: notificationSettings ?? this.notificationSettings, + userAppSettings: userAppSettings ?? this.userAppSettings, // Update field + userContentPreferences: + userContentPreferences ?? this.userContentPreferences, // Update field error: clearError ? null : error ?? this.error, ); } @@ -93,11 +65,8 @@ class SettingsState extends Equatable { @override List get props => [ status, - appSettings, - articleSettings, - themeSettings, - feedSettings, - notificationSettings, + userAppSettings, // Update field + userContentPreferences, // Update field error, ]; } diff --git a/lib/settings/view/appearance_settings_page.dart b/lib/settings/view/appearance_settings_page.dart index 9cb9eb2d..b5cd18d7 100644 --- a/lib/settings/view/appearance_settings_page.dart +++ b/lib/settings/view/appearance_settings_page.dart @@ -3,7 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:ht_main/l10n/l10n.dart'; import 'package:ht_main/settings/bloc/settings_bloc.dart'; import 'package:ht_main/shared/constants/constants.dart'; -import 'package:ht_preferences_client/ht_preferences_client.dart'; +import 'package:ht_shared/ht_shared.dart'; // Use types from ht_shared /// {@template appearance_settings_page} /// A page for configuring appearance-related settings like theme and fonts. @@ -12,47 +12,69 @@ class AppearanceSettingsPage extends StatelessWidget { /// {@macro appearance_settings_page} const AppearanceSettingsPage({super.key}); - // Helper to map AppThemeMode enum to user-friendly strings - String _themeModeToString(AppThemeMode mode, AppLocalizations l10n) { + // Helper to map AppBaseTheme enum to user-friendly strings + String _baseThemeToString(AppBaseTheme mode, AppLocalizations l10n) { switch (mode) { - case AppThemeMode.light: - return l10n.settingsAppearanceThemeModeLight; // Add l10n key - case AppThemeMode.dark: - return l10n.settingsAppearanceThemeModeDark; // Add l10n key - case AppThemeMode.system: - return l10n.settingsAppearanceThemeModeSystem; // Add l10n key + case AppBaseTheme.light: + return l10n.settingsAppearanceThemeModeLight; + case AppBaseTheme.dark: + return l10n.settingsAppearanceThemeModeDark; + case AppBaseTheme.system: + return l10n.settingsAppearanceThemeModeSystem; } } - // Helper to map AppThemeName enum to user-friendly strings - String _themeNameToString(AppThemeName name, AppLocalizations l10n) { + // Helper to map AppAccentTheme enum to user-friendly strings + String _accentThemeToString(AppAccentTheme name, AppLocalizations l10n) { switch (name) { - case AppThemeName.red: - return l10n.settingsAppearanceThemeNameRed; // Add l10n key - case AppThemeName.blue: - return l10n.settingsAppearanceThemeNameBlue; // Add l10n key - case AppThemeName.grey: - return l10n.settingsAppearanceThemeNameGrey; // Add l10n key + case AppAccentTheme.newsRed: + return l10n.settingsAppearanceThemeNameRed; + case AppAccentTheme.defaultBlue: + return l10n.settingsAppearanceThemeNameBlue; + case AppAccentTheme.graphiteGray: + return l10n.settingsAppearanceThemeNameGrey; } } - // Helper to map FontSize enum to user-friendly strings - String _fontSizeToString(FontSize size, AppLocalizations l10n) { + // Helper to map AppTextScaleFactor enum to user-friendly strings + String _textScaleFactorToString( + AppTextScaleFactor size, + AppLocalizations l10n, + ) { switch (size) { - case FontSize.small: - return l10n.settingsAppearanceFontSizeSmall; // Add l10n key - case FontSize.large: - return l10n.settingsAppearanceFontSizeLarge; // Add l10n key - case FontSize.medium: - return l10n.settingsAppearanceFontSizeMedium; // Add l10n key + case AppTextScaleFactor.small: + return l10n.settingsAppearanceFontSizeSmall; + case AppTextScaleFactor.large: + return l10n.settingsAppearanceFontSizeLarge; + case AppTextScaleFactor.medium: + return l10n.settingsAppearanceFontSizeMedium; + case AppTextScaleFactor.extraLarge: + return l10n.settingsAppearanceFontSizeExtraLarge; // Add l10n key } } - // Helper to map AppFontType enum to user-friendly strings - // (Using the enum name directly might be sufficient if they are clear) - String _fontTypeToString(AppFontType type, AppLocalizations l10n) { + // Helper to map font family string to user-friendly strings + String _fontFamilyToString(String fontFamily, AppLocalizations l10n) { + // This mapping might need to be more sophisticated if supporting multiple + // specific fonts. For now, just return the string or a placeholder. // Consider adding specific l10n keys if needed, e.g., l10n.fontRoboto - return type.name; // Example: 'roboto', 'openSans' + return fontFamily == 'SystemDefault' + ? l10n + .settingsAppearanceFontFamilySystemDefault // Add l10n key + : fontFamily; + } + + // TODO(cline): Replace with localized strings once localization issue is resolved. + // Helper to map AppFontWeight enum to user-friendly strings (currently uses enum name) + String _fontWeightToString(AppFontWeight weight, AppLocalizations l10n) { + switch (weight) { + case AppFontWeight.light: + return 'Light'; // Temporary: Use enum name or placeholder + case AppFontWeight.regular: + return 'Regular'; // Temporary: Use enum name or placeholder + case AppFontWeight.bold: + return 'Bold'; // Temporary: Use enum name or placeholder + } } @override @@ -73,19 +95,17 @@ class AppearanceSettingsPage extends StatelessWidget { } return Scaffold( - appBar: AppBar( - title: Text(l10n.settingsAppearanceTitle), // Reuse title key - ), + appBar: AppBar(title: Text(l10n.settingsAppearanceTitle)), body: ListView( padding: const EdgeInsets.all(AppSpacing.lg), children: [ - // --- Theme Mode --- - _buildDropdownSetting( + // --- Base Theme --- + _buildDropdownSetting( context: context, - title: l10n.settingsAppearanceThemeModeLabel, // Add l10n key - currentValue: state.themeSettings.themeMode, - items: AppThemeMode.values, - itemToString: (mode) => _themeModeToString(mode, l10n), + title: l10n.settingsAppearanceThemeModeLabel, + currentValue: state.userAppSettings.displaySettings.baseTheme, + items: AppBaseTheme.values, + itemToString: (mode) => _baseThemeToString(mode, l10n), onChanged: (value) { if (value != null) { settingsBloc.add(SettingsAppThemeModeChanged(value)); @@ -94,46 +114,73 @@ class AppearanceSettingsPage extends StatelessWidget { ), const SizedBox(height: AppSpacing.lg), - // --- Theme Name --- - _buildDropdownSetting( + // --- Accent Theme --- + _buildDropdownSetting( context: context, - title: l10n.settingsAppearanceThemeNameLabel, // Add l10n key - currentValue: state.themeSettings.themeName, - items: AppThemeName.values, - itemToString: (name) => _themeNameToString(name, l10n), + title: l10n.settingsAppearanceThemeNameLabel, + currentValue: state.userAppSettings.displaySettings.accentTheme, + items: AppAccentTheme.values, + itemToString: (name) => _accentThemeToString(name, l10n), onChanged: (value) { if (value != null) { - settingsBloc.add(SettingsAppThemeNameChanged(value)); + context.read().add( + SettingsAppThemeNameChanged(value), + ); } }, ), const SizedBox(height: AppSpacing.lg), - // --- App Font Size --- - _buildDropdownSetting( + // --- Text Scale Factor --- + _buildDropdownSetting( context: context, - title: l10n.settingsAppearanceAppFontSizeLabel, // Add l10n key - currentValue: state.appSettings.appFontSize, - items: FontSize.values, - itemToString: (size) => _fontSizeToString(size, l10n), + title: + l10n.settingsAppearanceAppFontSizeLabel, // Reusing key for text size + currentValue: state.userAppSettings.displaySettings.textScaleFactor, + items: AppTextScaleFactor.values, + itemToString: (size) => _textScaleFactorToString(size, l10n), onChanged: (value) { if (value != null) { - settingsBloc.add(SettingsAppFontSizeChanged(value)); + context.read().add( + SettingsAppFontSizeChanged(value), + ); + } + }, + ), + const SizedBox(height: AppSpacing.lg), + + // --- Font Family --- + _buildDropdownSetting( + // Font family is a String + context: context, + title: + l10n.settingsAppearanceAppFontTypeLabel, // Reusing key for font family + currentValue: state.userAppSettings.displaySettings.fontFamily, + items: const [ + 'SystemDefault', + ], // Only SystemDefault supported for now + itemToString: (fontFamily) => _fontFamilyToString(fontFamily, l10n), + onChanged: (value) { + if (value != null) { + context.read().add( + SettingsAppFontTypeChanged(value), + ); } }, ), const SizedBox(height: AppSpacing.lg), - // --- App Font Type --- - _buildDropdownSetting( + // --- Font Weight --- + _buildDropdownSetting( context: context, - title: l10n.settingsAppearanceAppFontTypeLabel, // Add l10n key - currentValue: state.appSettings.appFontType, - items: AppFontType.values, - itemToString: (type) => _fontTypeToString(type, l10n), + title: l10n.settingsAppearanceFontWeightLabel, // Add l10n key + currentValue: state.userAppSettings.displaySettings.fontWeight, + items: AppFontWeight.values, + itemToString: + (weight) => _fontWeightToString(weight, l10n), // Use helper onChanged: (value) { if (value != null) { - settingsBloc.add(SettingsAppFontTypeChanged(value)); + settingsBloc.add(SettingsAppFontWeightChanged(value)); } }, ), diff --git a/lib/settings/view/article_settings_page.dart b/lib/settings/view/article_settings_page.dart index 7b3b10e7..634ed646 100644 --- a/lib/settings/view/article_settings_page.dart +++ b/lib/settings/view/article_settings_page.dart @@ -3,7 +3,8 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:ht_main/l10n/l10n.dart'; import 'package:ht_main/settings/bloc/settings_bloc.dart'; import 'package:ht_main/shared/constants/constants.dart'; -import 'package:ht_preferences_client/ht_preferences_client.dart'; +import 'package:ht_shared/ht_shared.dart' + show AppTextScaleFactor; // Import new enum /// {@template article_settings_page} /// A page for configuring article display settings. @@ -12,15 +13,21 @@ class ArticleSettingsPage extends StatelessWidget { /// {@macro article_settings_page} const ArticleSettingsPage({super.key}); - // Helper to map FontSize enum to user-friendly strings - String _fontSizeToString(FontSize size, AppLocalizations l10n) { - switch (size) { - case FontSize.small: + // Helper to map AppTextScaleFactor enum to user-friendly strings + String _textScaleFactorToString( + AppTextScaleFactor factor, + AppLocalizations l10n, + ) { + switch (factor) { + case AppTextScaleFactor.small: return l10n.settingsAppearanceFontSizeSmall; // Reuse key - case FontSize.large: - return l10n.settingsAppearanceFontSizeLarge; // Reuse key - case FontSize.medium: + case AppTextScaleFactor.medium: return l10n.settingsAppearanceFontSizeMedium; // Reuse key + case AppTextScaleFactor.large: + return l10n.settingsAppearanceFontSizeLarge; // Reuse key + case AppTextScaleFactor.extraLarge: + return l10n + .settingsAppearanceFontSizeExtraLarge; // Add l10n key if needed } } @@ -48,15 +55,21 @@ class ArticleSettingsPage extends StatelessWidget { padding: const EdgeInsets.all(AppSpacing.lg), children: [ // --- Article Font Size --- - _buildDropdownSetting( + _buildDropdownSetting( context: context, title: l10n.settingsArticleFontSizeLabel, // Add l10n key - currentValue: state.articleSettings.articleFontSize, - items: FontSize.values, - itemToString: (size) => _fontSizeToString(size, l10n), + currentValue: + state + .userAppSettings + .displaySettings + .textScaleFactor, // Use new model field + items: AppTextScaleFactor.values, + itemToString: (factor) => _textScaleFactorToString(factor, l10n), onChanged: (value) { if (value != null) { - settingsBloc.add(SettingsArticleFontSizeChanged(value)); + settingsBloc.add( + SettingsAppFontSizeChanged(value), + ); // Use new event } }, ), diff --git a/lib/settings/view/feed_settings_page.dart b/lib/settings/view/feed_settings_page.dart index 17c8b1c9..9d304ec9 100644 --- a/lib/settings/view/feed_settings_page.dart +++ b/lib/settings/view/feed_settings_page.dart @@ -3,7 +3,8 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:ht_main/l10n/l10n.dart'; import 'package:ht_main/settings/bloc/settings_bloc.dart'; import 'package:ht_main/shared/constants/constants.dart'; -import 'package:ht_preferences_client/ht_preferences_client.dart'; +import 'package:ht_shared/ht_shared.dart' + show HeadlineImageStyle; // Import new enum /// {@template feed_settings_page} /// A page for configuring feed display settings. @@ -12,15 +13,15 @@ class FeedSettingsPage extends StatelessWidget { /// {@macro feed_settings_page} const FeedSettingsPage({super.key}); - // Helper to map FeedListTileType enum to user-friendly strings - String _tileTypeToString(FeedListTileType type, AppLocalizations l10n) { - switch (type) { - case FeedListTileType.imageTop: - return l10n.settingsFeedTileTypeImageTop; // Add l10n key - case FeedListTileType.imageStart: - return l10n.settingsFeedTileTypeImageStart; // Add l10n key - case FeedListTileType.textOnly: - return l10n.settingsFeedTileTypeTextOnly; // Add l10n key + // Helper to map HeadlineImageStyle enum to user-friendly strings + String _imageStyleToString(HeadlineImageStyle style, AppLocalizations l10n) { + switch (style) { + case HeadlineImageStyle.hidden: + return l10n.settingsFeedTileTypeTextOnly; // Closest match + case HeadlineImageStyle.smallThumbnail: + return l10n.settingsFeedTileTypeImageStart; // Closest match + case HeadlineImageStyle.largeThumbnail: + return l10n.settingsFeedTileTypeImageTop; // Closest match } } @@ -48,15 +49,21 @@ class FeedSettingsPage extends StatelessWidget { padding: const EdgeInsets.all(AppSpacing.lg), children: [ // --- Feed Tile Type --- - _buildDropdownSetting( + _buildDropdownSetting( context: context, title: l10n.settingsFeedTileTypeLabel, // Add l10n key - currentValue: state.feedSettings.feedListTileType, - items: FeedListTileType.values, - itemToString: (type) => _tileTypeToString(type, l10n), + currentValue: + state + .userAppSettings + .feedPreferences + .headlineImageStyle, // Use new model field + items: HeadlineImageStyle.values, + itemToString: (style) => _imageStyleToString(style, l10n), onChanged: (value) { if (value != null) { - settingsBloc.add(SettingsFeedTileTypeChanged(value)); + settingsBloc.add( + SettingsFeedTileTypeChanged(value), + ); // Use new event } }, ), diff --git a/lib/settings/view/notification_settings_page.dart b/lib/settings/view/notification_settings_page.dart index 1a6eebe4..d4fea364 100644 --- a/lib/settings/view/notification_settings_page.dart +++ b/lib/settings/view/notification_settings_page.dart @@ -27,7 +27,11 @@ class NotificationSettingsPage extends StatelessWidget { ); } - final notificationsEnabled = state.notificationSettings.enabled; + // TODO(cline): Full implementation of Notification Settings UI and BLoC logic + // is pending backend and shared model development (specifically, adding + // a 'notificationsEnabled' field to UserAppSettings or a similar model). + // This UI is temporarily disabled. + const notificationsEnabled = false; // Placeholder value return Scaffold( appBar: AppBar( @@ -40,15 +44,15 @@ class NotificationSettingsPage extends StatelessWidget { SwitchListTile( title: Text(l10n.settingsNotificationsEnableLabel), // Add l10n key value: notificationsEnabled, - onChanged: (bool value) { - settingsBloc.add(SettingsNotificationsEnabledChanged(value)); - }, + onChanged: null, // Disable the switch secondary: const Icon(Icons.notifications_active_outlined), ), const Divider(), // --- Detailed Notification Settings (Conditional) --- - // Only show these if notifications are enabled + // Only show these if notifications are enabled (currently disabled) + // The following section is commented out as it depends on notificationsEnabled + /* if (notificationsEnabled) ...[ ListTile( leading: const Icon(Icons.category_outlined), @@ -93,6 +97,7 @@ class NotificationSettingsPage extends StatelessWidget { }, ), ], + */ ], ), ); diff --git a/lib/shared/theme/app_theme.dart b/lib/shared/theme/app_theme.dart index 6d946eca..71819995 100644 --- a/lib/shared/theme/app_theme.dart +++ b/lib/shared/theme/app_theme.dart @@ -4,7 +4,7 @@ import 'package:flex_color_scheme/flex_color_scheme.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; -import 'package:ht_preferences_client/ht_preferences_client.dart'; // Added for FontSize +import 'package:ht_shared/ht_shared.dart'; // --- Common Sub-theme Settings --- // Defines customizations for various components, shared between light/dark themes. @@ -33,17 +33,19 @@ const FlexSubThemesData _commonSubThemesData = FlexSubThemesData( // Helper function to apply common text theme customizations TextTheme _customizeTextTheme( TextTheme baseTextTheme, { - required FontSize appFontSize, // Added parameter + required AppTextScaleFactor appTextScaleFactor, // Added parameter }) { // Define font size factors double factor; - switch (appFontSize) { - case FontSize.small: + switch (appTextScaleFactor) { + case AppTextScaleFactor.small: factor = 0.85; - case FontSize.large: + case AppTextScaleFactor.large: factor = 1.15; - case FontSize.medium: + case AppTextScaleFactor.medium: factor = 1.0; + case AppTextScaleFactor.extraLarge: + factor = 1.3; // Define factor for extraLarge } // Helper to apply factor safely @@ -118,10 +120,10 @@ TextTheme Function([TextTheme?]) _getGoogleFontTextTheme(String? fontFamily) { /// Defines the application's light theme using FlexColorScheme. /// -/// Takes the active [scheme], [appFontSize], and optional [fontFamily]. +/// Takes the active [scheme], [appTextScaleFactor], and optional [fontFamily]. ThemeData lightTheme({ required FlexScheme scheme, - required FontSize appFontSize, // Added parameter + required AppTextScaleFactor appTextScaleFactor, // Added parameter String? fontFamily, }) { final textThemeGetter = _getGoogleFontTextTheme(fontFamily); @@ -130,18 +132,21 @@ ThemeData lightTheme({ return FlexThemeData.light( scheme: scheme, fontFamily: fontFamily, - // Pass appFontSize to customizeTextTheme - textTheme: _customizeTextTheme(baseTextTheme, appFontSize: appFontSize), + // Pass appTextScaleFactor to customizeTextTheme + textTheme: _customizeTextTheme( + baseTextTheme, + appTextScaleFactor: appTextScaleFactor, + ), subThemesData: _commonSubThemesData, ); } /// Defines the application's dark theme using FlexColorScheme. /// -/// Takes the active [scheme], [appFontSize], and optional [fontFamily]. +/// Takes the active [scheme], [appTextScaleFactor], and optional [fontFamily]. ThemeData darkTheme({ required FlexScheme scheme, - required FontSize appFontSize, // Added parameter + required AppTextScaleFactor appTextScaleFactor, // Added parameter String? fontFamily, }) { final textThemeGetter = _getGoogleFontTextTheme(fontFamily); @@ -152,8 +157,11 @@ ThemeData darkTheme({ return FlexThemeData.dark( scheme: scheme, fontFamily: fontFamily, - // Pass appFontSize to customizeTextTheme - textTheme: _customizeTextTheme(baseTextTheme, appFontSize: appFontSize), + // Pass appTextScaleFactor to customizeTextTheme + textTheme: _customizeTextTheme( + baseTextTheme, + appTextScaleFactor: appTextScaleFactor, + ), subThemesData: _commonSubThemesData, ); } diff --git a/pubspec.lock b/pubspec.lock index 71545e40..d4ed7141 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,26 +5,18 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: dc27559385e905ad30838356c5f5d574014ba39872d732111cd07ac0beff4c57 + sha256: e55636ed79578b9abca5fecf9437947798f5ef7456308b5cb85720b793eac92f url: "https://pub.dev" source: hosted - version: "80.0.0" - _flutterfire_internals: - dependency: transitive - description: - name: _flutterfire_internals - sha256: de9ecbb3ddafd446095f7e833c853aff2fa1682b017921fe63a833f9d6f0e422 - url: "https://pub.dev" - source: hosted - version: "1.3.54" + version: "82.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "192d1c5b944e7e53b24b5586db760db934b177d4147c42fbca8c8c5f1eb8d11e" + sha256: "904ae5bb474d32c38fb9482e2d925d5454cda04ddd0e55d2e6826bc72f6ba8c0" url: "https://pub.dev" source: hosted - version: "7.3.0" + version: "7.4.5" ansicolor: dependency: transitive description: @@ -37,10 +29,10 @@ packages: dependency: transitive description: name: archive - sha256: "7dcbd0f87fe5f61cb28da39a1a8b70dbc106e2fe0516f7836eb7bb2948481a12" + sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" url: "https://pub.dev" source: hosted - version: "4.0.5" + version: "4.0.7" args: dependency: transitive description: @@ -113,30 +105,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" - cloud_firestore: - dependency: "direct main" - description: - name: cloud_firestore - sha256: "89a5e32716794b6a8d0ec1b5dfda988194e92daedaa3f3bed66fa0d0a595252e" - url: "https://pub.dev" - source: hosted - version: "5.6.6" - cloud_firestore_platform_interface: - dependency: transitive - description: - name: cloud_firestore_platform_interface - sha256: "9f012844eb59be6827ed97415875c5a29ccacd28bc79bf85b4680738251a33df" - url: "https://pub.dev" - source: hosted - version: "6.6.6" - cloud_firestore_web: - dependency: transitive - description: - name: cloud_firestore_web - sha256: b8b754269be0e907acd9ff63ad60f66b84c78d330ca1d7e474f86c9527ddc803 - url: "https://pub.dev" - source: hosted - version: "4.4.6" collection: dependency: transitive description: @@ -157,10 +125,10 @@ packages: dependency: transitive description: name: coverage - sha256: "9086475ef2da7102a0c0a4e37e1e30707e7fb7b6d28c209f559a9c5f8ce42016" + sha256: "802bd084fb82e55df091ec8ad1553a7331b61c08251eef19a508b6f3f3a9858d" url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.13.1" crypto: dependency: transitive description: @@ -233,70 +201,6 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" - firebase_auth: - dependency: transitive - description: - name: firebase_auth - sha256: "54c62b2d187709114dd09ce658a8803ee91f9119b0e0d3fc2245130ad9bff9ad" - url: "https://pub.dev" - source: hosted - version: "5.5.2" - firebase_auth_platform_interface: - dependency: transitive - description: - name: firebase_auth_platform_interface - sha256: "5402d13f4bb7f29f2fb819f3b6b5a5a56c9f714aef2276546d397e25ac1b6b8e" - url: "https://pub.dev" - source: hosted - version: "7.6.2" - firebase_auth_web: - dependency: transitive - description: - name: firebase_auth_web - sha256: "2be496911f0807895d5fe8067b70b7d758142dd7fb26485cbe23e525e2547764" - url: "https://pub.dev" - source: hosted - version: "5.14.2" - firebase_core: - dependency: "direct main" - description: - name: firebase_core - sha256: "017d17d9915670e6117497e640b2859e0b868026ea36bf3a57feb28c3b97debe" - url: "https://pub.dev" - source: hosted - version: "3.13.0" - firebase_core_platform_interface: - dependency: transitive - description: - name: firebase_core_platform_interface - sha256: d7253d255ff10f85cfd2adaba9ac17bae878fa3ba577462451163bd9f1d1f0bf - url: "https://pub.dev" - source: hosted - version: "5.4.0" - firebase_core_web: - dependency: transitive - description: - name: firebase_core_web - sha256: "129a34d1e0fb62e2b488d988a1fc26cc15636357e50944ffee2862efe8929b23" - url: "https://pub.dev" - source: hosted - version: "2.22.0" - firebase_dynamic_links: - dependency: "direct main" - description: - name: firebase_dynamic_links - sha256: ae8844d78a14a335e1d69d9a198dd5bcc4571ba4b028e45c0972e093b48530f8 - url: "https://pub.dev" - source: hosted - version: "6.1.5" - firebase_dynamic_links_platform_interface: - dependency: transitive - description: - name: firebase_dynamic_links_platform_interface - sha256: "7cb3b86956268a18c49badd66eb9a9279b71bf7188a7a2a48204f41db2642e78" - url: "https://pub.dev" - source: hosted - version: "0.2.7+5" fixnum: dependency: transitive description: @@ -330,18 +234,18 @@ packages: dependency: "direct main" description: name: flutter_adaptive_scaffold - sha256: "7279d74da2f2531a16d21c2ec327308778c3aedd672dfe4eaf3bf416463501f8" + sha256: "5eb1d1d174304a4e67c4bb402ed38cb4a5ebdac95ce54099e91460accb33d295" url: "https://pub.dev" source: hosted - version: "0.3.2" + version: "0.3.3+1" flutter_bloc: dependency: "direct main" description: name: flutter_bloc - sha256: "1046d719fbdf230330d3443187cc33cc11963d15c9089f6cc56faa42a4c5f0cc" + sha256: cf51747952201a455a1c840f8171d273be009b932c75093020f9af64f2123e38 url: "https://pub.dev" source: hosted - version: "9.1.0" + version: "9.1.1" flutter_localizations: dependency: "direct main" description: flutter @@ -385,10 +289,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: "4cdfcc6a178632d1dbb7a728f8e84a1466211354704b9cdc03eee661d3277732" + sha256: "0b1e06223bee260dee31a171fb1153e306907563a0b0225e8c1733211911429a" url: "https://pub.dev" source: hosted - version: "15.0.0" + version: "15.1.2" google_fonts: dependency: "direct main" description: @@ -397,97 +301,31 @@ packages: url: "https://pub.dev" source: hosted version: "6.2.1" - google_identity_services_web: - dependency: transitive - description: - name: google_identity_services_web - sha256: "55580f436822d64c8ff9a77e37d61f5fb1e6c7ec9d632a43ee324e2a05c3c6c9" - url: "https://pub.dev" - source: hosted - version: "0.3.3" - google_sign_in: - dependency: transitive - description: - name: google_sign_in - sha256: d0a2c3bcb06e607bb11e4daca48bd4b6120f0bbc4015ccebbe757d24ea60ed2a - url: "https://pub.dev" - source: hosted - version: "6.3.0" - google_sign_in_android: - dependency: transitive - description: - name: google_sign_in_android - sha256: "4e52c64366bdb3fe758f683b088ee514cc7a95e69c52b5ee9fc5919e1683d21b" - url: "https://pub.dev" - source: hosted - version: "6.2.0" - google_sign_in_ios: - dependency: transitive - description: - name: google_sign_in_ios - sha256: "29cd125f58f50ceb40e8253d3c0209e321eee3e5df16cd6d262495f7cad6a2bd" - url: "https://pub.dev" - source: hosted - version: "5.8.1" - google_sign_in_platform_interface: - dependency: transitive - description: - name: google_sign_in_platform_interface - sha256: "5f6f79cf139c197261adb6ac024577518ae48fdff8e53205c5373b5f6430a8aa" - url: "https://pub.dev" - source: hosted - version: "2.5.0" - google_sign_in_web: - dependency: transitive - description: - name: google_sign_in_web - sha256: "460547beb4962b7623ac0fb8122d6b8268c951cf0b646dd150d60498430e4ded" - url: "https://pub.dev" - source: hosted - version: "0.12.4+4" - ht_authentication_client: + ht_auth_api: dependency: "direct main" description: path: "." ref: HEAD - resolved-ref: "2b9533fa0cbb92faa4ccd50aead9a90f93225b8d" - url: "https://github.com/headlines-toolkit/ht-authentication-client.git" + resolved-ref: c0a8f8783ec27a6494c99d7bd47cb8600b0a5bec + url: "https://github.com/headlines-toolkit/ht-auth-api.git" source: git version: "0.0.0" - ht_authentication_firebase: + ht_auth_client: dependency: "direct main" description: path: "." ref: HEAD - resolved-ref: "0c32955f69b70ce859f97ce0fd23400732ab108b" - url: "https://github.com/headlines-toolkit/ht-authentication-firebase.git" + resolved-ref: "1c95c775085ed723923f9d9a53fe1c3becdeced4" + url: "https://github.com/headlines-toolkit/ht-auth-client.git" source: git version: "0.0.0" - ht_authentication_repository: + ht_auth_repository: dependency: "direct main" description: path: "." ref: HEAD - resolved-ref: fc6eefd31d1ab5f166b64e295c41bed815a0484f - url: "https://github.com/headlines-toolkit/ht-authentication-repository.git" - source: git - version: "0.0.0" - ht_categories_client: - dependency: transitive - description: - path: "." - ref: HEAD - resolved-ref: abbc8a2797a5ca4a399f9324c1d39b3f5f1323a3 - url: "https://github.com/headlines-toolkit/ht-categories-client.git" - source: git - version: "0.0.0" - ht_countries_client: - dependency: transitive - description: - path: "." - ref: HEAD - resolved-ref: f55b6c8bbcdc192a65f529c509d86b9f15d41072 - url: "https://github.com/headlines-toolkit/ht-countries-client.git" + resolved-ref: "6cc5c3ccab072ad9f666a5a8d081216155423d32" + url: "https://github.com/headlines-toolkit/ht-auth-repository.git" source: git version: "0.0.0" ht_data_api: @@ -495,7 +333,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: "6ff93db0f093ce50571d7697fa739153785b7fda" + resolved-ref: "37d27c0d2e2dfbc2d892aa92abf866b5c0cc1956" url: "https://github.com/headlines-toolkit/ht-data-api.git" source: git version: "0.0.0" @@ -504,7 +342,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: "798ac3a8087a405994f5dbb0b302ded30cc9d6ee" + resolved-ref: "30d92f5211c33ee8a16ae7e10d4b0d04e8e2782d" url: "https://github.com/headlines-toolkit/ht-data-client.git" source: git version: "0.0.0" @@ -513,25 +351,16 @@ packages: description: path: "." ref: HEAD - resolved-ref: "0ad1e36a5c4db338b090c445c0ebc4a562bbe44f" + resolved-ref: "4bcc4c4671ac6b51c211d2a5cbb3ee5a129fd5c7" url: "https://github.com/headlines-toolkit/ht-data-repository.git" source: git version: "0.0.0" - ht_headlines_client: - dependency: transitive - description: - path: "." - ref: HEAD - resolved-ref: a361496439de69bda88c823d253d9be9f27742a3 - url: "https://github.com/headlines-toolkit/ht-headlines-client.git" - source: git - version: "0.0.0" ht_http_client: dependency: "direct main" description: path: "." ref: HEAD - resolved-ref: "89ed2da945e6a37aaeed511f5e225e9690488ff7" + resolved-ref: "6e9f6301bc643798b9a1023f64bd262aa2e069be" url: "https://github.com/headlines-toolkit/ht-http-client.git" source: git version: "0.0.0" @@ -553,67 +382,31 @@ packages: url: "https://github.com/headlines-toolkit/ht-kv-storage-shared-preferences.git" source: git version: "0.0.0" - ht_preferences_client: - dependency: "direct main" - description: - path: "." - ref: HEAD - resolved-ref: "579159723184212352f084cc242b04f0160cf230" - url: "https://github.com/headlines-toolkit/ht-preferences-client.git" - source: git - version: "0.0.0" - ht_preferences_firestore: - dependency: "direct main" - description: - path: "." - ref: HEAD - resolved-ref: "21afa57e5023ee249560bd7c0c62272968af740f" - url: "https://github.com/headlines-toolkit/ht-preferences-firestore.git" - source: git - version: "0.0.0" - ht_preferences_repository: - dependency: "direct main" - description: - path: "." - ref: HEAD - resolved-ref: f485f4ea9bf92da437c74539b89d9b7cd93a3197 - url: "https://github.com/headlines-toolkit/ht-preferences-repository.git" - source: git - version: "0.0.0" ht_shared: dependency: "direct main" description: path: "." ref: HEAD - resolved-ref: fd1d1c173cf4376f0db080909441e53ff0884870 + resolved-ref: "47094efe56f4359473f37c19a6bbf809cf927dc2" url: "https://github.com/headlines-toolkit/ht-shared.git" source: git version: "0.0.0" - ht_sources_client: - dependency: transitive - description: - path: "." - ref: HEAD - resolved-ref: "9eaad425bcba5035b99e26d65a5a0368a3aa8184" - url: "https://github.com/headlines-toolkit/ht-sources-client.git" - source: git - version: "0.0.0" html: dependency: transitive description: name: html - sha256: "9475be233c437f0e3637af55e7702cbbe5c23a68bd56e8a5fa2d426297b7c6c8" + sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" url: "https://pub.dev" source: hosted - version: "0.15.5+1" + version: "0.15.6" http: dependency: transitive description: name: http - sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f + sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" http_multi_server: dependency: transitive description: @@ -786,10 +579,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "0ca7359dad67fd7063cb2892ab0c0737b2daafd807cf1acecd62374c8fae6c12" + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 url: "https://pub.dev" source: hosted - version: "2.2.16" + version: "2.2.17" path_provider_foundation: dependency: transitive description: @@ -858,18 +651,18 @@ packages: dependency: transitive description: name: posix - sha256: a0117dc2167805aa9125b82eee515cc891819bac2f538c83646d355b16f58b9a + sha256: f0d7856b6ca1887cfa6d1d394056a296ae33489db914e365e2044fdada449e62 url: "https://pub.dev" source: hosted - version: "6.0.1" + version: "6.0.2" provider: dependency: transitive description: name: provider - sha256: "489024f942069c2920c844ee18bb3d467c69e48955a4f32d1677f71be103e310" + sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "6.1.5" pub_semver: dependency: transitive description: @@ -878,14 +671,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" - rxdart: - dependency: transitive - description: - name: rxdart - sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" - url: "https://pub.dev" - source: hosted - version: "0.28.0" shared_preferences: dependency: transitive description: @@ -898,10 +683,10 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: c2c8c46297b5d6a80bed7741ec1f2759742c77d272f1a1698176ae828f8e1a18 + sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" url: "https://pub.dev" source: hosted - version: "2.4.9" + version: "2.4.10" shared_preferences_foundation: dependency: transitive description: @@ -1103,10 +888,10 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "1d0eae19bd7606ef60fe69ef3b312a437a16549476c42321d5dc1506c9ca3bf4" + sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79" url: "https://pub.dev" source: hosted - version: "6.3.15" + version: "6.3.16" url_launcher_ios: dependency: transitive description: @@ -1143,10 +928,10 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9" + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" url_launcher_windows: dependency: transitive description: @@ -1207,18 +992,18 @@ packages: dependency: transitive description: name: web_socket - sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" url: "https://pub.dev" source: hosted - version: "0.1.6" + version: "1.0.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.3" webkit_inspection_protocol: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 4d6bbe58..dd34fc69 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,10 +9,7 @@ environment: dependencies: bloc: ^9.0.0 bloc_concurrency: ^0.3.0 - cloud_firestore: ^5.6.5 equatable: ^2.0.7 - firebase_core: ^3.12.1 - firebase_dynamic_links: ^6.1.4 flex_color_scheme: ^8.1.1 flutter: sdk: flutter @@ -23,15 +20,15 @@ dependencies: flutter_native_splash: ^2.4.5 go_router: ^15.0.0 google_fonts: ^6.2.1 - ht_authentication_client: + ht_auth_api: git: - url: https://github.com/headlines-toolkit/ht-authentication-client.git - ht_authentication_firebase: + url: https://github.com/headlines-toolkit/ht-auth-api.git + ht_auth_client: git: - url: https://github.com/headlines-toolkit/ht-authentication-firebase.git - ht_authentication_repository: + url: https://github.com/headlines-toolkit/ht-auth-client.git + ht_auth_repository: git: - url: https://github.com/headlines-toolkit/ht-authentication-repository.git + url: https://github.com/headlines-toolkit/ht-auth-repository.git ht_data_api: git: url: https://github.com/headlines-toolkit/ht-data-api.git @@ -50,15 +47,6 @@ dependencies: ht_kv_storage_shared_preferences: git: url: https://github.com/headlines-toolkit/ht-kv-storage-shared-preferences.git - ht_preferences_client: - git: - url: https://github.com/headlines-toolkit/ht-preferences-client.git - ht_preferences_firestore: - git: - url: https://github.com/headlines-toolkit/ht-preferences-firestore.git - ht_preferences_repository: - git: - url: https://github.com/headlines-toolkit/ht-preferences-repository.git ht_shared: git: url: https://github.com/headlines-toolkit/ht-shared.git