diff --git a/lib/app/bloc/app_bloc.dart b/lib/app/bloc/app_bloc.dart index 3ba90e87..ef21ff33 100644 --- a/lib/app/bloc/app_bloc.dart +++ b/lib/app/bloc/app_bloc.dart @@ -101,6 +101,12 @@ class AppBloc extends Bloc { final newAppTextScaleFactor = _mapTextScaleFactor( userAppSettings.displaySettings.textScaleFactor, ); + // Map language code to Locale + final newLocale = Locale(userAppSettings.language); + + print('[AppBloc] _onAppSettingsRefreshed: userAppSettings.fontFamily: ${userAppSettings.displaySettings.fontFamily}'); + print('[AppBloc] _onAppSettingsRefreshed: userAppSettings.fontWeight: ${userAppSettings.displaySettings.fontWeight}'); + print('[AppBloc] _onAppSettingsRefreshed: newFontFamily mapped to: $newFontFamily'); emit( state.copyWith( @@ -109,6 +115,7 @@ class AppBloc extends Bloc { appTextScaleFactor: newAppTextScaleFactor, fontFamily: newFontFamily, settings: userAppSettings, // Store the fetched settings + locale: newLocale, // Store the new locale ), ); } on NotFoundException { @@ -120,6 +127,7 @@ class AppBloc extends Bloc { themeMode: ThemeMode.system, flexScheme: FlexScheme.material, appTextScaleFactor: AppTextScaleFactor.medium, // Default enum value + locale: const Locale('en'), // Default to English if settings not found settings: UserAppSettings( id: state.user!.id, ), // Provide default settings @@ -243,26 +251,17 @@ class AppBloc extends Bloc { } } - String? _mapFontFamily(String fontFamily) { - // Assuming 'SystemDefault' means use the theme's default font - if (fontFamily == 'SystemDefault') return null; - - // Map specific font family names to GoogleFonts - switch (fontFamily) { - case 'Roboto': - return GoogleFonts.roboto().fontFamily; - case 'OpenSans': - return GoogleFonts.openSans().fontFamily; - case 'Lato': - return GoogleFonts.lato().fontFamily; - case 'Montserrat': - return GoogleFonts.montserrat().fontFamily; - case 'Merriweather': - return GoogleFonts.merriweather().fontFamily; - default: - // If an unknown font family is specified, fall back to theme default - return null; + String? _mapFontFamily(String fontFamilyString) { + // If the input is 'SystemDefault', return null so FlexColorScheme uses its default. + if (fontFamilyString == 'SystemDefault') { + print('[AppBloc] _mapFontFamily: Input is SystemDefault, returning null.'); + return null; } + // Otherwise, return the font family string directly. + // The GoogleFonts.xyz().fontFamily getters often return strings like "Roboto-Regular", + // but FlexColorScheme's fontFamily parameter or GoogleFonts.xyzTextTheme() expect simple names. + print('[AppBloc] _mapFontFamily: Input is $fontFamilyString, returning as is.'); + return fontFamilyString; } // Map AppTextScaleFactor to AppTextScaleFactor (no change needed) diff --git a/lib/app/bloc/app_state.dart b/lib/app/bloc/app_state.dart index 3ef5a2d8..e784ec83 100644 --- a/lib/app/bloc/app_state.dart +++ b/lib/app/bloc/app_state.dart @@ -27,6 +27,7 @@ class AppState extends Equatable { this.fontFamily, this.status = AppStatus.initial, this.user, // User is now nullable and defaults to null + this.locale, // Added locale }); /// The index of the currently selected item in the bottom navigation bar. @@ -54,6 +55,9 @@ class AppState extends Equatable { /// User-specific application settings. final UserAppSettings settings; // Add settings property + /// The current application locale. + final Locale? locale; // Added locale + /// Creates a copy of the current state with updated values. AppState copyWith({ int? selectedBottomNavigationIndex, @@ -64,7 +68,9 @@ class AppState extends Equatable { AppStatus? status, User? user, UserAppSettings? settings, // Add settings to copyWith + Locale? locale, // Added locale bool clearFontFamily = false, + bool clearLocale = false, // Added to allow clearing locale }) { return AppState( selectedBottomNavigationIndex: @@ -76,6 +82,7 @@ class AppState extends Equatable { status: status ?? this.status, user: user ?? this.user, settings: settings ?? this.settings, // Copy settings + locale: clearLocale ? null : locale ?? this.locale, // Added locale ); } @@ -89,5 +96,6 @@ class AppState extends Equatable { status, user, settings, // Include settings in props + locale, // Added locale to props ]; } diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index 9b0335ce..d65c44f3 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -181,9 +181,13 @@ class _AppViewState extends State<_AppView> { previous.themeMode != current.themeMode || previous.flexScheme != current.flexScheme || previous.fontFamily != current.fontFamily || - previous.appTextScaleFactor != - current.appTextScaleFactor, // Use text scale factor + previous.appTextScaleFactor != current.appTextScaleFactor || + previous.locale != current.locale, // Added locale check builder: (context, state) { + print('[_AppViewState] Building MaterialApp.router'); + print('[_AppViewState] state.fontFamily: ${state.fontFamily}'); + print('[_AppViewState] state.settings.displaySettings.fontFamily: ${state.settings.displaySettings.fontFamily}'); + print('[_AppViewState] state.settings.displaySettings.fontWeight: ${state.settings.displaySettings.fontWeight}'); return MaterialApp.router( debugShowCheckedModeBanner: false, themeMode: state.themeMode, @@ -192,15 +196,18 @@ class _AppViewState extends State<_AppView> { scheme: state.flexScheme, appTextScaleFactor: state.settings.displaySettings.textScaleFactor, + appFontWeight: state.settings.displaySettings.fontWeight, // Added fontFamily: state.settings.displaySettings.fontFamily, ), darkTheme: darkTheme( scheme: state.flexScheme, appTextScaleFactor: state.settings.displaySettings.textScaleFactor, + appFontWeight: state.settings.displaySettings.fontWeight, // Added fontFamily: state.settings.displaySettings.fontFamily, ), routerConfig: _router, + locale: state.locale, // Use locale from AppBloc state localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, ); diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index 8aef218b..bb2ca86a 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -516,6 +516,18 @@ "@settingsAppearanceFontFamilySystemDefault": { "description": "Label for the system default font family option" }, + "settingsAppearanceThemeSubPageTitle": "إعدادات المظهر", + "@settingsAppearanceThemeSubPageTitle": { + "description": "Title for the theme settings sub-page under appearance" + }, + "settingsAppearanceFontSubPageTitle": "إعدادات الخط", + "@settingsAppearanceFontSubPageTitle": { + "description": "Title for the font settings sub-page under appearance" + }, + "settingsLanguageTitle": "اللغة", + "@settingsLanguageTitle": { + "description": "Title for the language settings page/section" + }, "emailCodeSentPageTitle": "أدخل الرمز", "@emailCodeSentPageTitle": { "description": "AppBar title for the email code verification page" diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 4ba481fb..5625a1f9 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -516,6 +516,18 @@ "@settingsAppearanceFontFamilySystemDefault": { "description": "Label for the system default font family option" }, + "settingsAppearanceThemeSubPageTitle": "Theme Settings", + "@settingsAppearanceThemeSubPageTitle": { + "description": "Title for the theme settings sub-page under appearance" + }, + "settingsAppearanceFontSubPageTitle": "Font Settings", + "@settingsAppearanceFontSubPageTitle": { + "description": "Title for the font settings sub-page under appearance" + }, + "settingsLanguageTitle": "Language", + "@settingsLanguageTitle": { + "description": "Title for the language settings page/section" + }, "emailCodeSentPageTitle": "Enter Code", "@emailCodeSentPageTitle": { "description": "AppBar title for the email code verification page" diff --git a/lib/router/router.dart b/lib/router/router.dart index 96300e59..6ed44d49 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -38,8 +38,11 @@ import 'package:ht_main/router/routes.dart'; import 'package:ht_main/settings/bloc/settings_bloc.dart'; // Added import 'package:ht_main/settings/view/appearance_settings_page.dart'; // Added import 'package:ht_main/settings/view/feed_settings_page.dart'; // Added +import 'package:ht_main/settings/view/font_settings_page.dart'; // Added for new page +import 'package:ht_main/settings/view/language_settings_page.dart'; // Added for new page import 'package:ht_main/settings/view/notification_settings_page.dart'; // Added import 'package:ht_main/settings/view/settings_page.dart'; // Added +import 'package:ht_main/settings/view/theme_settings_page.dart'; // Added for new page import 'package:ht_shared/ht_shared.dart'; // Shared models, FromJson, ToJson, etc. /// Creates and configures the GoRouter instance for the application. @@ -526,22 +529,19 @@ GoRouter createRouter({ name: Routes.accountName, builder: (context, state) => const AccountPage(), routes: [ - // Sub-route for settings - GoRoute( - path: Routes.settings, // Relative path 'settings' - name: Routes.settingsName, - builder: (context, state) { - // Provide SettingsBloc here for SettingsPage and its children - // Access AppBloc to get the current user ID + // ShellRoute for settings to provide SettingsBloc to children + ShellRoute( + builder: (BuildContext context, GoRouterState state, Widget child) { + // This builder provides SettingsBloc to all routes within this ShellRoute. + // 'child' will be SettingsPage, AppearanceSettingsPage, etc. final appBloc = context.read(); final userId = appBloc.state.user?.id; - return BlocProvider( + return BlocProvider( create: (context) { final settingsBloc = SettingsBloc( userAppSettingsRepository: - context - .read>(), + context.read>(), ); // Only load settings if a userId is available if (userId != null) { @@ -550,39 +550,55 @@ GoRouter createRouter({ ); } else { // Handle case where user is unexpectedly null. - // This might involve logging or emitting an error state - // directly in SettingsBloc if it's designed to handle it, - // or simply not loading settings. - // For now, we'll assume router redirects prevent this. print( - 'Warning: User ID is null when creating SettingsBloc. Settings will not be loaded.', + 'ShellRoute/SettingsBloc: User ID is null when creating SettingsBloc. Settings will not be loaded.', ); } return settingsBloc; }, - child: const SettingsPage(), // Use the actual page + child: child, // child is the actual page widget (SettingsPage, AppearanceSettingsPage, etc.) ); }, - // --- Settings Sub-Routes --- routes: [ GoRoute( - path: Routes.settingsAppearance, // 'appearance' - name: Routes.settingsAppearanceName, - builder: - (context, state) => const AppearanceSettingsPage(), - // SettingsBloc is inherited from parent route - ), - GoRoute( - path: Routes.settingsFeed, // 'feed' - name: Routes.settingsFeedName, - builder: (context, state) => const FeedSettingsPage(), - ), - GoRoute( - path: Routes.settingsNotifications, // 'notifications' - name: Routes.settingsNotificationsName, - builder: - (context, state) => - const NotificationSettingsPage(), + path: Routes.settings, // Relative path 'settings' from /account + name: Routes.settingsName, + builder: (context, state) => const SettingsPage(), + // --- Settings Sub-Routes --- + routes: [ + GoRoute( + path: Routes.settingsAppearance, // 'appearance' relative to /account/settings + name: Routes.settingsAppearanceName, + builder: (context, state) => const AppearanceSettingsPage(), + routes: [ // Children of AppearanceSettingsPage + GoRoute( + path: Routes.settingsAppearanceTheme, // 'theme' relative to /account/settings/appearance + name: Routes.settingsAppearanceThemeName, + builder: (context, state) => const ThemeSettingsPage(), + ), + GoRoute( + path: Routes.settingsAppearanceFont, // 'font' relative to /account/settings/appearance + name: Routes.settingsAppearanceFontName, + builder: (context, state) => const FontSettingsPage(), + ), + ], + ), + GoRoute( + path: Routes.settingsFeed, // 'feed' relative to /account/settings + name: Routes.settingsFeedName, + builder: (context, state) => const FeedSettingsPage(), + ), + GoRoute( + path: Routes.settingsNotifications, // 'notifications' relative to /account/settings + name: Routes.settingsNotificationsName, + builder: (context, state) => const NotificationSettingsPage(), + ), + GoRoute( + path: Routes.settingsLanguage, // 'language' relative to /account/settings + name: Routes.settingsLanguageName, + builder: (context, state) => const LanguageSettingsPage(), + ), + ], ), ], ), diff --git a/lib/router/routes.dart b/lib/router/routes.dart index 2d1477d0..71fbac84 100644 --- a/lib/router/routes.dart +++ b/lib/router/routes.dart @@ -55,12 +55,24 @@ abstract final class Routes { // --- Settings Sub-Routes (relative to /account/settings) --- static const settingsAppearance = 'appearance'; static const settingsAppearanceName = 'settingsAppearance'; + + // --- Appearance Sub-Routes (relative to /account/settings/appearance) --- + static const settingsAppearanceTheme = 'theme'; // Path: /account/settings/appearance/theme + static const settingsAppearanceThemeName = 'settingsAppearanceTheme'; + static const settingsAppearanceFont = 'font'; // Path: /account/settings/appearance/font + static const settingsAppearanceFontName = 'settingsAppearanceFont'; + static const settingsFeed = 'feed'; static const settingsFeedName = 'settingsFeed'; static const settingsArticle = 'article'; static const settingsArticleName = 'settingsArticle'; static const settingsNotifications = 'notifications'; static const settingsNotificationsName = 'settingsNotifications'; + + // --- Language Settings Sub-Route (relative to /account/settings) --- + static const settingsLanguage = 'language'; // Path: /account/settings/language + static const settingsLanguageName = 'settingsLanguage'; + // Add names for notification sub-selection routes if needed later // static const settingsNotificationCategories = 'categories'; // static const settingsNotificationCategoriesName = 'settingsNotificationCategories'; diff --git a/lib/settings/bloc/settings_bloc.dart b/lib/settings/bloc/settings_bloc.dart index 591ef72a..f2efc53d 100644 --- a/lib/settings/bloc/settings_bloc.dart +++ b/lib/settings/bloc/settings_bloc.dart @@ -48,6 +48,10 @@ class SettingsBloc extends Bloc { _onFeedTileTypeChanged, transformer: sequential(), ); + on( + _onLanguageChanged, + transformer: sequential(), + ); // SettingsNotificationsEnabledChanged event and handler removed. } @@ -156,12 +160,14 @@ class SettingsBloc extends Bloc { Emitter emit, ) async { if (state.userAppSettings == null) return; + print('[SettingsBloc] _onAppFontTypeChanged: Received event.fontType: ${event.fontType}'); final updatedSettings = state.userAppSettings!.copyWith( displaySettings: state.userAppSettings!.displaySettings.copyWith( fontFamily: event.fontType, ), ); + print('[SettingsBloc] _onAppFontTypeChanged: Updated settings.fontFamily: ${updatedSettings.displaySettings.fontFamily}'); emit(state.copyWith(userAppSettings: updatedSettings, clearError: true)); await _persistSettings(updatedSettings, emit); } @@ -171,12 +177,14 @@ class SettingsBloc extends Bloc { Emitter emit, ) async { if (state.userAppSettings == null) return; + print('[SettingsBloc] _onAppFontWeightChanged: Received event.fontWeight: ${event.fontWeight}'); final updatedSettings = state.userAppSettings!.copyWith( displaySettings: state.userAppSettings!.displaySettings.copyWith( fontWeight: event.fontWeight, ), ); + print('[SettingsBloc] _onAppFontWeightChanged: Updated settings.fontWeight: ${updatedSettings.displaySettings.fontWeight}'); emit(state.copyWith(userAppSettings: updatedSettings, clearError: true)); await _persistSettings(updatedSettings, emit); } @@ -195,4 +203,17 @@ class SettingsBloc extends Bloc { emit(state.copyWith(userAppSettings: updatedSettings, clearError: true)); await _persistSettings(updatedSettings, emit); } + + Future _onLanguageChanged( + SettingsLanguageChanged event, + Emitter emit, + ) async { + if (state.userAppSettings == null) return; + + final updatedSettings = state.userAppSettings!.copyWith( + language: event.languageCode, + ); + emit(state.copyWith(userAppSettings: updatedSettings, clearError: true)); + await _persistSettings(updatedSettings, emit); + } } diff --git a/lib/settings/bloc/settings_event.dart b/lib/settings/bloc/settings_event.dart index 782d1af2..b553fc9d 100644 --- a/lib/settings/bloc/settings_event.dart +++ b/lib/settings/bloc/settings_event.dart @@ -119,6 +119,20 @@ class SettingsFeedTileTypeChanged extends SettingsEvent { List get props => [tileType]; } +/// {@template settings_language_changed} +/// Event added when the user changes the application language. +/// {@endtemplate} +class SettingsLanguageChanged extends SettingsEvent { + /// {@macro settings_language_changed} + const SettingsLanguageChanged(this.languageCode); + + /// The newly selected language code (e.g., 'en', 'ar'). + final AppLanguage languageCode; // Use AppLanguage typedef from ht_shared + + @override + List get props => [languageCode]; +} + // --- Notification Settings Events --- // SettingsNotificationsEnabledChanged event removed as UserAppSettings // does not currently support a general notifications enabled flag. diff --git a/lib/settings/view/appearance_settings_page.dart b/lib/settings/view/appearance_settings_page.dart index 048d0776..616ac0ea 100644 --- a/lib/settings/view/appearance_settings_page.dart +++ b/lib/settings/view/appearance_settings_page.dart @@ -1,229 +1,56 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; import 'package:ht_main/l10n/l10n.dart'; +import 'package:ht_main/router/routes.dart'; import 'package:ht_main/settings/bloc/settings_bloc.dart'; -import 'package:ht_main/shared/constants/constants.dart'; -import 'package:ht_shared/ht_shared.dart'; // Use types from ht_shared +import 'package:ht_main/shared/constants/app_spacing.dart'; /// {@template appearance_settings_page} -/// A page for configuring appearance-related settings like theme and fonts. +/// A menu page for navigating to theme and font appearance settings. /// {@endtemplate} class AppearanceSettingsPage extends StatelessWidget { /// {@macro appearance_settings_page} const AppearanceSettingsPage({super.key}); - // Helper to map AppBaseTheme enum to user-friendly strings - String _baseThemeToString(AppBaseTheme mode, AppLocalizations l10n) { - switch (mode) { - case AppBaseTheme.light: - return l10n.settingsAppearanceThemeModeLight; - case AppBaseTheme.dark: - return l10n.settingsAppearanceThemeModeDark; - case AppBaseTheme.system: - return l10n.settingsAppearanceThemeModeSystem; - } - } - - // Helper to map AppAccentTheme enum to user-friendly strings - String _accentThemeToString(AppAccentTheme name, AppLocalizations l10n) { - switch (name) { - case AppAccentTheme.newsRed: - return l10n.settingsAppearanceThemeNameRed; - case AppAccentTheme.defaultBlue: - return l10n.settingsAppearanceThemeNameBlue; - case AppAccentTheme.graphiteGray: - return l10n.settingsAppearanceThemeNameGrey; - } - } - - // Helper to map AppTextScaleFactor enum to user-friendly strings - String _textScaleFactorToString( - AppTextScaleFactor size, - AppLocalizations l10n, - ) { - switch (size) { - 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 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 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 Widget build(BuildContext context) { final l10n = context.l10n; - final settingsBloc = context.watch(); - final state = settingsBloc.state; + // SettingsBloc is watched to ensure settings are loaded, + // though this page itself doesn't dispatch events. + final settingsState = context.watch().state; - // Ensure we have loaded state before building controls - if (state.status != SettingsStatus.success) { - // Can show a minimal loading/error or rely on parent page handling + if (settingsState.status != SettingsStatus.success) { return Scaffold( appBar: AppBar(title: Text(l10n.settingsAppearanceTitle)), - body: const Center( - child: CircularProgressIndicator(), - ), // Simple loading + body: const Center(child: CircularProgressIndicator()), ); } return Scaffold( appBar: AppBar(title: Text(l10n.settingsAppearanceTitle)), body: ListView( - padding: const EdgeInsets.all(AppSpacing.lg), + padding: const EdgeInsets.symmetric(vertical: AppSpacing.md), children: [ - // --- Base Theme --- - _buildDropdownSetting( - context: context, - 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)); - } + ListTile( + leading: const Icon(Icons.color_lens_outlined), + title: Text(l10n.settingsAppearanceThemeSubPageTitle), // Use new l10n key + trailing: const Icon(Icons.chevron_right), + onTap: () { + context.goNamed(Routes.settingsAppearanceThemeName); }, ), - const SizedBox(height: AppSpacing.lg), - - // --- Accent Theme --- - _buildDropdownSetting( - context: context, - title: l10n.settingsAppearanceThemeNameLabel, - currentValue: state.userAppSettings!.displaySettings.accentTheme, - items: AppAccentTheme.values, - itemToString: (name) => _accentThemeToString(name, l10n), - onChanged: (value) { - if (value != null) { - context.read().add( - SettingsAppThemeNameChanged(value), - ); - } - }, - ), - const SizedBox(height: AppSpacing.lg), - - // --- Text Scale Factor --- - _buildDropdownSetting( - context: context, - 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) { - 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), - - // --- Font Weight --- - _buildDropdownSetting( - context: context, - 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(SettingsAppFontWeightChanged(value)); - } + const Divider(indent: AppSpacing.lg, endIndent: AppSpacing.lg), + ListTile( + leading: const Icon(Icons.font_download_outlined), + title: Text(l10n.settingsAppearanceFontSubPageTitle), // Use new l10n key + trailing: const Icon(Icons.chevron_right), + onTap: () { + context.goNamed(Routes.settingsAppearanceFontName); }, ), ], ), ); } - - /// Generic helper to build a setting row with a title and a dropdown. - Widget _buildDropdownSetting({ - required BuildContext context, - required String title, - required T currentValue, - required List items, - required String Function(T) itemToString, - required ValueChanged onChanged, - }) { - final textTheme = Theme.of(context).textTheme; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(title, style: textTheme.titleMedium), - const SizedBox(height: AppSpacing.sm), - DropdownButtonFormField( - value: currentValue, - items: - items.map((T value) { - return DropdownMenuItem( - value: value, - child: Text(itemToString(value)), - ); - }).toList(), - onChanged: onChanged, - decoration: const InputDecoration( - border: OutlineInputBorder(), - contentPadding: EdgeInsets.symmetric( - horizontal: AppSpacing.md, - vertical: AppSpacing.sm, - ), - ), - ), - ], - ); - } } diff --git a/lib/settings/view/font_settings_page.dart b/lib/settings/view/font_settings_page.dart new file mode 100644 index 00000000..8455e67d --- /dev/null +++ b/lib/settings/view/font_settings_page.dart @@ -0,0 +1,170 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:ht_main/app/bloc/app_bloc.dart'; // Import AppBloc and events +import 'package:ht_main/l10n/l10n.dart'; +import 'package:ht_main/settings/bloc/settings_bloc.dart'; +import 'package:ht_main/shared/constants/app_spacing.dart'; +import 'package:ht_shared/ht_shared.dart' + show AppTextScaleFactor, AppFontWeight; + +/// {@template font_settings_page} +/// A page for configuring font-related settings like size, family, and weight. +/// {@endtemplate} +class FontSettingsPage extends StatelessWidget { + /// {@macro font_settings_page} + const FontSettingsPage({super.key}); + + // Helper to map AppTextScaleFactor enum to user-friendly strings + String _textScaleFactorToString( + AppTextScaleFactor size, + AppLocalizations l10n, + ) { + switch (size) { + case AppTextScaleFactor.small: + return l10n.settingsAppearanceFontSizeSmall; + case AppTextScaleFactor.large: + return l10n.settingsAppearanceFontSizeLarge; + case AppTextScaleFactor.medium: + return l10n.settingsAppearanceFontSizeMedium; + case AppTextScaleFactor.extraLarge: + return l10n.settingsAppearanceFontSizeExtraLarge; + } + } + + // Helper to map font family string to user-friendly strings + String _fontFamilyToString(String fontFamily, AppLocalizations l10n) { + return fontFamily == 'SystemDefault' + ? l10n.settingsAppearanceFontFamilySystemDefault + : fontFamily; + } + + // Helper to map AppFontWeight enum to user-friendly strings + String _fontWeightToString(AppFontWeight weight, AppLocalizations l10n) { + // Using direct strings as placeholders until specific l10n keys are confirmed + switch (weight) { + case AppFontWeight.light: + return 'Light'; // Placeholder + case AppFontWeight.regular: + return 'Regular'; // Placeholder + case AppFontWeight.bold: + return 'Bold'; // Placeholder + } + } + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final settingsBloc = context.watch(); + final state = settingsBloc.state; + + if (state.status != SettingsStatus.success || + state.userAppSettings == null) { + return Scaffold( + appBar: AppBar(title: Text(l10n.settingsAppearanceTitle)), // Use existing key + body: const Center(child: CircularProgressIndicator()), + ); + } + + return BlocListener( + listener: (context, settingsState) { // Renamed state to avoid conflict + if (settingsState.status == SettingsStatus.success) { + context.read().add(const AppSettingsRefreshed()); + } + }, + child: Scaffold( + appBar: AppBar(title: Text(l10n.settingsAppearanceTitle)), // Use existing key + body: ListView( + padding: const EdgeInsets.all(AppSpacing.lg), + children: [ + // --- Text Scale Factor --- + _buildDropdownSetting( + context: context, + title: l10n.settingsAppearanceAppFontSizeLabel, + currentValue: + state.userAppSettings!.displaySettings.textScaleFactor, + items: AppTextScaleFactor.values, + itemToString: (size) => _textScaleFactorToString(size, l10n), + onChanged: (value) { + if (value != null) { + settingsBloc.add(SettingsAppFontSizeChanged(value)); + } + }, + ), + const SizedBox(height: AppSpacing.lg), + + // --- Font Family --- + _buildDropdownSetting( + context: context, + title: l10n.settingsAppearanceAppFontTypeLabel, + currentValue: state.userAppSettings!.displaySettings.fontFamily, + items: const [ + 'SystemDefault', + 'Roboto', + 'OpenSans', + 'Lato', + 'Montserrat', + 'Merriweather', + ], // Updated font list + itemToString: (fontFamily) => _fontFamilyToString(fontFamily, l10n), + onChanged: (value) { + if (value != null) { + settingsBloc.add(SettingsAppFontTypeChanged(value)); + } + }, + ), + const SizedBox(height: AppSpacing.lg), + + // --- Font Weight --- + _buildDropdownSetting( + context: context, + title: l10n.settingsAppearanceFontWeightLabel, + currentValue: state.userAppSettings!.displaySettings.fontWeight, + items: AppFontWeight.values, + itemToString: (weight) => _fontWeightToString(weight, l10n), + onChanged: (value) { + if (value != null) { + settingsBloc.add(SettingsAppFontWeightChanged(value)); + } + }, + ), + ], + ), + ), // Correctly close BlocListener's child Scaffold + ); + } + + Widget _buildDropdownSetting({ + required BuildContext context, + required String title, + required T currentValue, + required List items, + required String Function(T) itemToString, + required ValueChanged onChanged, + }) { + final textTheme = Theme.of(context).textTheme; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: textTheme.titleMedium), + const SizedBox(height: AppSpacing.sm), + DropdownButtonFormField( + value: currentValue, + items: items.map((T value) { + return DropdownMenuItem( + value: value, + child: Text(itemToString(value)), + ); + }).toList(), + onChanged: onChanged, + decoration: const InputDecoration( + border: OutlineInputBorder(), + contentPadding: EdgeInsets.symmetric( + horizontal: AppSpacing.md, + vertical: AppSpacing.sm, + ), + ), + ), + ], + ); + } +} diff --git a/lib/settings/view/language_settings_page.dart b/lib/settings/view/language_settings_page.dart new file mode 100644 index 00000000..dcbf3f02 --- /dev/null +++ b/lib/settings/view/language_settings_page.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:ht_main/app/bloc/app_bloc.dart'; +import 'package:ht_main/l10n/l10n.dart'; +import 'package:ht_main/settings/bloc/settings_bloc.dart'; +import 'package:ht_main/shared/constants/app_spacing.dart'; +import 'package:ht_shared/ht_shared.dart' show AppLanguage; + +// Defines the available languages and their display names. +// In a real app, this might come from a configuration or be more dynamic. +const Map _supportedLanguages = { + 'en': 'English', + 'ar': 'العربية (Arabic)', +}; + +/// {@template language_settings_page} +/// A page for selecting the application language. +/// {@endtemplate} +class LanguageSettingsPage extends StatelessWidget { + /// {@macro language_settings_page} + const LanguageSettingsPage({super.key}); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final settingsBloc = context.watch(); + final settingsState = settingsBloc.state; + + if (settingsState.status != SettingsStatus.success || + settingsState.userAppSettings == null) { + return Scaffold( + appBar: AppBar(title: Text(l10n.settingsTitle)), // Placeholder l10n key + body: const Center(child: CircularProgressIndicator()), + ); + } + + final currentLanguage = settingsState.userAppSettings!.language; + + return BlocListener( + listener: (context, state) { + if (state.status == SettingsStatus.success) { + context.read().add(const AppSettingsRefreshed()); + } + }, + child: Scaffold( + appBar: AppBar(title: Text(l10n.settingsTitle)), // Placeholder l10n key + body: ListView.separated( + padding: const EdgeInsets.symmetric(vertical: AppSpacing.md), + itemCount: _supportedLanguages.length, + separatorBuilder: (context, index) => + const Divider(indent: AppSpacing.lg, endIndent: AppSpacing.lg), + itemBuilder: (context, index) { + final languageCode = _supportedLanguages.keys.elementAt(index); + final languageName = _supportedLanguages.values.elementAt(index); + final isSelected = languageCode == currentLanguage; + + return ListTile( + title: Text(languageName), + trailing: isSelected + ? Icon(Icons.check, color: Theme.of(context).primaryColor) + : null, + onTap: () { + if (!isSelected) { + // Dispatch event to SettingsBloc + context + .read() + .add(SettingsLanguageChanged(languageCode)); + } + }, + ); + }, + ), + ), + ); + } +} diff --git a/lib/settings/view/settings_page.dart b/lib/settings/view/settings_page.dart index c4d2d0f7..98b61bcf 100644 --- a/lib/settings/view/settings_page.dart +++ b/lib/settings/view/settings_page.dart @@ -24,7 +24,17 @@ class SettingsPage extends StatelessWidget { return Scaffold( appBar: AppBar( - // Standard back button provided by Scaffold/GoRouter + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + if (context.canPop()) { + context.pop(); + } else { + // Fallback if it can't pop, perhaps go to account page directly + context.goNamed(Routes.accountName); + } + }, + ), title: Text(l10n.settingsTitle), // Add l10n key: settingsTitle ), // Use BlocBuilder to react to loading/error states if needed, @@ -70,6 +80,13 @@ class SettingsPage extends StatelessWidget { return ListView( padding: const EdgeInsets.symmetric(vertical: AppSpacing.md), children: [ + _buildSettingsTile( + context: context, + icon: Icons.language_outlined, + title: l10n.settingsLanguageTitle, // Add l10n key + onTap: () => context.goNamed(Routes.settingsLanguageName), + ), + const Divider(indent: AppSpacing.lg, endIndent: AppSpacing.lg), _buildSettingsTile( context: context, icon: Icons.palette_outlined, diff --git a/lib/settings/view/theme_settings_page.dart b/lib/settings/view/theme_settings_page.dart new file mode 100644 index 00000000..5515a001 --- /dev/null +++ b/lib/settings/view/theme_settings_page.dart @@ -0,0 +1,147 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:ht_main/app/bloc/app_bloc.dart'; // Import AppBloc and events +import 'package:ht_main/l10n/l10n.dart'; +import 'package:ht_main/settings/bloc/settings_bloc.dart'; +import 'package:ht_main/shared/constants/app_spacing.dart'; +import 'package:ht_shared/ht_shared.dart' show AppBaseTheme, AppAccentTheme; + +/// {@template theme_settings_page} +/// A page for configuring theme-related settings like base and accent themes. +/// {@endtemplate} +class ThemeSettingsPage extends StatelessWidget { + /// {@macro theme_settings_page} + const ThemeSettingsPage({super.key}); + + // Helper to map AppBaseTheme enum to user-friendly strings + String _baseThemeToString(AppBaseTheme mode, AppLocalizations l10n) { + switch (mode) { + case AppBaseTheme.light: + return l10n.settingsAppearanceThemeModeLight; + case AppBaseTheme.dark: + return l10n.settingsAppearanceThemeModeDark; + case AppBaseTheme.system: + return l10n.settingsAppearanceThemeModeSystem; + } + } + + // Helper to map AppAccentTheme enum to user-friendly strings + String _accentThemeToString(AppAccentTheme name, AppLocalizations l10n) { + switch (name) { + case AppAccentTheme.newsRed: + return l10n.settingsAppearanceThemeNameRed; + case AppAccentTheme.defaultBlue: + return l10n.settingsAppearanceThemeNameBlue; + case AppAccentTheme.graphiteGray: + return l10n.settingsAppearanceThemeNameGrey; + } + } + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final settingsBloc = context.watch(); + final state = settingsBloc.state; + + // Ensure we have loaded state before building controls + // This page should only be reached if settings are successfully loaded + // by the parent ShellRoute providing SettingsBloc. + if (state.status != SettingsStatus.success || + state.userAppSettings == null) { + return Scaffold( + appBar: AppBar(title: Text(l10n.settingsAppearanceTitle)), + body: const Center(child: CircularProgressIndicator()), + ); + } + + return BlocListener( + listener: (context, settingsState) { // Renamed state to avoid conflict + if (settingsState.status == SettingsStatus.success) { + // Check if it's a successful update, not just initial load + // A more robust check might involve comparing previous and current userAppSettings + // For now, refreshing on any success after an interaction is reasonable. + // Ensure AppBloc is available in context before reading + context.read().add(const AppSettingsRefreshed()); + } + // Optionally, show a SnackBar for errors if not handled globally + // if (settingsState.status == SettingsStatus.failure && settingsState.error != null) { + // ScaffoldMessenger.of(context) + // ..hideCurrentSnackBar() + // ..showSnackBar(SnackBar(content: Text('Error: ${settingsState.error}'))); + // } + }, + child: Scaffold( + appBar: AppBar(title: Text(l10n.settingsAppearanceTitle)), + body: ListView( + padding: const EdgeInsets.all(AppSpacing.lg), + children: [ + // --- Base Theme --- + _buildDropdownSetting( + context: context, + 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)); + } + }, + ), + const SizedBox(height: AppSpacing.lg), + + // --- Accent Theme --- + _buildDropdownSetting( + context: context, + 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)); + } + }, + ), + ], + ), + ), // Correctly close BlocListener's child Scaffold + ); + } + + /// Generic helper to build a setting row with a title and a dropdown. + Widget _buildDropdownSetting({ + required BuildContext context, + required String title, + required T currentValue, + required List items, + required String Function(T) itemToString, + required ValueChanged onChanged, + }) { + final textTheme = Theme.of(context).textTheme; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: textTheme.titleMedium), + const SizedBox(height: AppSpacing.sm), + DropdownButtonFormField( + value: currentValue, + items: items.map((T value) { + return DropdownMenuItem( + value: value, + child: Text(itemToString(value)), + ); + }).toList(), + onChanged: onChanged, + decoration: const InputDecoration( + border: OutlineInputBorder(), + contentPadding: EdgeInsets.symmetric( + horizontal: AppSpacing.md, + vertical: AppSpacing.sm, + ), + ), + ), + ], + ); + } +} diff --git a/lib/shared/theme/app_theme.dart b/lib/shared/theme/app_theme.dart index 71819995..f6ab038e 100644 --- a/lib/shared/theme/app_theme.dart +++ b/lib/shared/theme/app_theme.dart @@ -33,8 +33,10 @@ const FlexSubThemesData _commonSubThemesData = FlexSubThemesData( // Helper function to apply common text theme customizations TextTheme _customizeTextTheme( TextTheme baseTextTheme, { - required AppTextScaleFactor appTextScaleFactor, // Added parameter + required AppTextScaleFactor appTextScaleFactor, + required AppFontWeight appFontWeight, // Added parameter }) { + print('[_customizeTextTheme] Received appFontWeight: $appFontWeight, appTextScaleFactor: $appTextScaleFactor'); // Define font size factors double factor; switch (appTextScaleFactor) { @@ -52,39 +54,61 @@ TextTheme _customizeTextTheme( double? applyFactor(double? baseSize) => baseSize != null ? (baseSize * factor).roundToDouble() : null; + // Map AppFontWeight to FontWeight + FontWeight selectedFontWeight; + switch (appFontWeight) { + case AppFontWeight.light: + selectedFontWeight = FontWeight.w300; + break; + case AppFontWeight.regular: + selectedFontWeight = FontWeight.w400; + break; + case AppFontWeight.bold: + selectedFontWeight = FontWeight.w700; + break; + } + print('[_customizeTextTheme] Mapped to selectedFontWeight: $selectedFontWeight'); + return baseTextTheme.copyWith( // --- Headline/Title Styles --- + // Headlines and titles often have their own explicit weights, + // but we can make them configurable if needed. For now, let's assume + // body text is the primary target for user-defined weight. headlineLarge: baseTextTheme.headlineLarge?.copyWith( - fontSize: applyFactor(28), // Apply factor - fontWeight: FontWeight.bold, + fontSize: applyFactor(28), + fontWeight: FontWeight.bold, // Keeping titles bold by default ), headlineMedium: baseTextTheme.headlineMedium?.copyWith( - fontSize: applyFactor(24), // Apply factor - fontWeight: FontWeight.bold, + fontSize: applyFactor(24), + fontWeight: FontWeight.bold, // Keeping titles bold by default ), titleLarge: baseTextTheme.titleLarge?.copyWith( - fontSize: applyFactor(18), // Apply factor - fontWeight: FontWeight.w600, + fontSize: applyFactor(18), + fontWeight: FontWeight.w600, // Keeping titles semi-bold by default ), titleMedium: baseTextTheme.titleMedium?.copyWith( - fontSize: applyFactor(16), // Apply factor - fontWeight: FontWeight.w600, + fontSize: applyFactor(16), + fontWeight: FontWeight.w600, // Keeping titles semi-bold by default ), // --- Body/Content Styles --- + // Apply user-selected font weight to body text bodyLarge: baseTextTheme.bodyLarge?.copyWith( - fontSize: applyFactor(16), // Apply factor + fontSize: applyFactor(16), height: 1.5, + fontWeight: selectedFontWeight, // Apply selected weight ), bodyMedium: baseTextTheme.bodyMedium?.copyWith( - fontSize: applyFactor(14), // Apply factor + fontSize: applyFactor(14), height: 1.4, + fontWeight: selectedFontWeight, // Apply selected weight ), // --- Metadata/Caption Styles --- + // Captions might also benefit from user-defined weight or stay regular. labelSmall: baseTextTheme.labelSmall?.copyWith( - fontSize: applyFactor(12), // Apply factor - fontWeight: FontWeight.normal, + fontSize: applyFactor(12), + fontWeight: selectedFontWeight, // Apply selected weight ), // --- Button Style (Usually default is fine) --- @@ -96,46 +120,51 @@ TextTheme _customizeTextTheme( // based on the provided font family name. // Corrected return type to match GoogleFonts functions (positional optional) TextTheme Function([TextTheme?]) _getGoogleFontTextTheme(String? fontFamily) { - // Map font family names (as used in AppBloc mapping) to GoogleFonts functions - if (fontFamily == GoogleFonts.roboto().fontFamily) { - return GoogleFonts.robotoTextTheme; - } - if (fontFamily == GoogleFonts.openSans().fontFamily) { - return GoogleFonts.openSansTextTheme; - } - if (fontFamily == GoogleFonts.lato().fontFamily) { - return GoogleFonts.latoTextTheme; + print('[_getGoogleFontTextTheme] Received fontFamily: $fontFamily'); + switch (fontFamily) { + case 'Roboto': + print('[_getGoogleFontTextTheme] Returning GoogleFonts.robotoTextTheme'); + return GoogleFonts.robotoTextTheme; + case 'OpenSans': + print('[_getGoogleFontTextTheme] Returning GoogleFonts.openSansTextTheme'); + return GoogleFonts.openSansTextTheme; + case 'Lato': + print('[_getGoogleFontTextTheme] Returning GoogleFonts.latoTextTheme'); + return GoogleFonts.latoTextTheme; + case 'Montserrat': + print('[_getGoogleFontTextTheme] Returning GoogleFonts.montserratTextTheme'); + return GoogleFonts.montserratTextTheme; + case 'Merriweather': + print('[_getGoogleFontTextTheme] Returning GoogleFonts.merriweatherTextTheme'); + return GoogleFonts.merriweatherTextTheme; + case 'SystemDefault': + case null: + default: + print('[_getGoogleFontTextTheme] Defaulting to GoogleFonts.notoSansTextTheme for input: $fontFamily'); + return GoogleFonts.notoSansTextTheme; } - if (fontFamily == GoogleFonts.montserrat().fontFamily) { - return GoogleFonts.montserratTextTheme; - } - if (fontFamily == GoogleFonts.merriweather().fontFamily) { - return GoogleFonts.merriweatherTextTheme; - } - // Add mappings for other AppFontType values if needed - - // Default fallback if fontFamily is null or not recognized - return GoogleFonts.notoSansTextTheme; } /// Defines the application's light theme using FlexColorScheme. /// -/// Takes the active [scheme], [appTextScaleFactor], and optional [fontFamily]. +/// Takes the active [scheme], [appTextScaleFactor], [appFontWeight], and optional [fontFamily]. ThemeData lightTheme({ required FlexScheme scheme, - required AppTextScaleFactor appTextScaleFactor, // Added parameter + required AppTextScaleFactor appTextScaleFactor, + required AppFontWeight appFontWeight, // Added parameter String? fontFamily, }) { + print('[AppTheme.lightTheme] Received scheme: $scheme, appTextScaleFactor: $appTextScaleFactor, appFontWeight: $appFontWeight, fontFamily: $fontFamily'); final textThemeGetter = _getGoogleFontTextTheme(fontFamily); final baseTextTheme = textThemeGetter(); return FlexThemeData.light( scheme: scheme, fontFamily: fontFamily, - // Pass appTextScaleFactor to customizeTextTheme textTheme: _customizeTextTheme( baseTextTheme, appTextScaleFactor: appTextScaleFactor, + appFontWeight: appFontWeight, // Pass new parameter ), subThemesData: _commonSubThemesData, ); @@ -143,12 +172,14 @@ ThemeData lightTheme({ /// Defines the application's dark theme using FlexColorScheme. /// -/// Takes the active [scheme], [appTextScaleFactor], and optional [fontFamily]. +/// Takes the active [scheme], [appTextScaleFactor], [appFontWeight], and optional [fontFamily]. ThemeData darkTheme({ required FlexScheme scheme, - required AppTextScaleFactor appTextScaleFactor, // Added parameter + required AppTextScaleFactor appTextScaleFactor, + required AppFontWeight appFontWeight, // Added parameter String? fontFamily, }) { + print('[AppTheme.darkTheme] Received scheme: $scheme, appTextScaleFactor: $appTextScaleFactor, appFontWeight: $appFontWeight, fontFamily: $fontFamily'); final textThemeGetter = _getGoogleFontTextTheme(fontFamily); final baseTextTheme = textThemeGetter( ThemeData(brightness: Brightness.dark).textTheme, @@ -157,10 +188,10 @@ ThemeData darkTheme({ return FlexThemeData.dark( scheme: scheme, fontFamily: fontFamily, - // Pass appTextScaleFactor to customizeTextTheme textTheme: _customizeTextTheme( baseTextTheme, appTextScaleFactor: appTextScaleFactor, + appFontWeight: appFontWeight, // Pass new parameter ), subThemesData: _commonSubThemesData, );