diff --git a/lib/config/app_controller/app_controller.dart b/lib/config/app_controller/app_controller.dart new file mode 100644 index 00000000..a14eaa19 --- /dev/null +++ b/lib/config/app_controller/app_controller.dart @@ -0,0 +1,183 @@ +import 'package:flutter/material.dart'; +import 'package:material_color_utilities/material_color_utilities.dart' + as color_utils; +import 'package:selfprivacy/config/get_it_config.dart'; +import 'package:selfprivacy/config/preferences_repository/preferences_repository.dart'; + +/// A class that many Widgets can interact with to read current app +/// configuration, update it, or listen to its changes. +/// +/// AppController uses repo to change persistent data. +class AppController with ChangeNotifier { + AppController(this._repo); + + /// repo encapsulates retrieval and storage of preferences + final PreferencesRepository _repo; + + /// TODO: to be removed or changed + late final ApiConfigModel _apiConfigModel = getIt.get(); + + bool _loaded = false; + bool get loaded => _loaded; + + // localization + late Locale _locale; + Locale get locale => _locale; + late List _supportedLocales; + List get supportedLocales => _supportedLocales; + + // theme + late ThemeData _lightTheme; + ThemeData get lightTheme => _lightTheme; + late ThemeData _darkTheme; + ThemeData get darkTheme => _darkTheme; + late color_utils.CorePalette _corePalette; + color_utils.CorePalette get corePalette => _corePalette; + + late bool _systemThemeModeActive; + bool get systemThemeModeActive => _systemThemeModeActive; + + late bool _darkThemeModeActive; + bool get darkThemeModeActive => _darkThemeModeActive; + + ThemeMode get themeMode => systemThemeModeActive + ? ThemeMode.system + : darkThemeModeActive + ? ThemeMode.dark + : ThemeMode.light; + // // Make ThemeMode a private variable so it is not updated directly without + // // also persisting the changes with the repo.. + // late ThemeMode _themeMode; + // // Allow Widgets to read the user's preferred ThemeMode. + // ThemeMode get themeMode => _themeMode; + + late bool _shouldShowOnboarding; + bool get shouldShowOnboarding => _shouldShowOnboarding; + + /// Load the user's settings from the SettingsService. It may load from a + /// local database or the internet. The controller only knows it can load the + /// settings from the service. + Future init({ + // required final AppPreferencesRepository repo, + required final ThemeData lightThemeData, + required final ThemeData darkThemeData, + required final color_utils.CorePalette colorPalette, + }) async { + // _repo = repo; + + await Future.wait([ + // load locale + () async { + _supportedLocales = await _repo.getSupportedLocales(); + + _locale = await _repo.getActiveLocale(); + // preset value to other state holders + await _apiConfigModel.setLocaleCode(_locale.languageCode); + await _repo.setDelegateLocale(_locale); + }(), + + // load theme mode && initialize theme + () async { + _lightTheme = lightThemeData; + _darkTheme = darkThemeData; + _corePalette = colorPalette; + // _themeMode = await _repo.getThemeMode(); + _darkThemeModeActive = await _repo.getDarkThemeModeFlag(); + _systemThemeModeActive = await _repo.getSystemThemeModeFlag(); + }(), + + // load onboarding flag + () async { + _shouldShowOnboarding = await _repo.getShouldShowOnboarding(); + }(), + ]); + + _loaded = true; + // Important! Inform listeners a change has occurred. + notifyListeners(); + } + + // updateRepoReference + + Future setShouldShowOnboarding(final bool newValue) async { + // Do not perform any work if new and old flag values are identical + if (newValue == shouldShowOnboarding) { + return; + } + + // Store the flag in memory + _shouldShowOnboarding = newValue; + + notifyListeners(); + + // Persist the change + await _repo.setShouldShowOnboarding(newValue); + } + + Future setSystemThemeModeFlag(final bool newValue) async { + // Do not perform any work if new and old ThemeMode are identical + if (systemThemeModeActive == newValue) { + return; + } + + // Store the new ThemeMode in memory + _systemThemeModeActive = newValue; + + // Inform listeners a change has occurred. + notifyListeners(); + + // Persist the change + await _repo.setSystemModeFlag(newValue); + } + + Future setDarkThemeModeFlag(final bool newValue) async { + // Do not perform any work if new and old ThemeMode are identical + if (darkThemeModeActive == newValue) { + return; + } + + // Store the new ThemeMode in memory + _darkThemeModeActive = newValue; + + // Inform listeners a change has occurred. + notifyListeners(); + + // Persist the change + await _repo.setDarkThemeModeFlag(newValue); + } + + // /// Update and persist the ThemeMode based on the user's selection. + // Future setThemeMode(final ThemeMode newThemeMode) async { + // // Do not perform any work if new and old ThemeMode are identical + // if (newThemeMode == themeMode) { + // return; + // } + + // // Store the new ThemeMode in memory + // _themeMode = newThemeMode; + + // // Inform listeners a change has occurred. + // notifyListeners(); + + // // Persist the change + // await _repo.setThemeMode(newThemeMode); + // } + + Future setLocale(final Locale newLocale) async { + // Do not perform any work if new and old Locales are identical + if (newLocale == _locale) { + return; + } + + // Store the new Locale in memory + _locale = newLocale; + + /// update locale delegate, which in turn should update deps + await _repo.setDelegateLocale(newLocale); + + // Persist the change + await _repo.setActiveLocale(newLocale); + // Update other locale holders + await _apiConfigModel.setLocaleCode(newLocale.languageCode); + } +} diff --git a/lib/config/app_controller/inherited_app_controller.dart b/lib/config/app_controller/inherited_app_controller.dart new file mode 100644 index 00000000..e7eeac7b --- /dev/null +++ b/lib/config/app_controller/inherited_app_controller.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; +import 'package:material_color_utilities/material_color_utilities.dart' + as color_utils; +import 'package:selfprivacy/config/app_controller/app_controller.dart'; +import 'package:selfprivacy/config/brand_colors.dart'; +import 'package:selfprivacy/config/preferences_repository/inherited_preferences_repository.dart'; +import 'package:selfprivacy/config/preferences_repository/preferences_repository.dart'; +import 'package:selfprivacy/theming/factory/app_theme_factory.dart'; + +class _AppControllerInjector extends InheritedNotifier { + const _AppControllerInjector({ + required super.child, + required super.notifier, + }); +} + +class InheritedAppController extends StatefulWidget { + const InheritedAppController({ + required this.child, + super.key, + }); + + final Widget child; + + @override + State createState() => _InheritedAppControllerState(); + + static AppController of(final BuildContext context) => context + .dependOnInheritedWidgetOfExactType<_AppControllerInjector>()! + .notifier!; +} + +class _InheritedAppControllerState extends State { + // actual state provider + late AppController controller; + // hold local reference to active repo + late PreferencesRepository _repo; + + bool initTriggerred = false; + + @override + void didChangeDependencies() { + /// update reference on dependency change + _repo = InheritedPreferencesRepository.of(context)!; + + if (!initTriggerred) { + /// hook controller repo to local reference + controller = AppController(_repo); + initialize(); + initTriggerred = true; + } + + super.didChangeDependencies(); + } + + Future initialize() async { + late final ThemeData lightThemeData; + late final ThemeData darkThemeData; + late final color_utils.CorePalette colorPalette; + + await Future.wait( + >[ + () async { + lightThemeData = await AppThemeFactory.create( + isDark: false, + fallbackColor: BrandColors.primary, + ); + }(), + () async { + darkThemeData = await AppThemeFactory.create( + isDark: true, + fallbackColor: BrandColors.primary, + ); + }(), + () async { + colorPalette = (await AppThemeFactory.getCorePalette()) ?? + color_utils.CorePalette.of(BrandColors.primary.value); + }(), + ], + ); + + await controller.init( + colorPalette: colorPalette, + lightThemeData: lightThemeData, + darkThemeData: darkThemeData, + ); + + WidgetsBinding.instance.addPostFrameCallback((final _) { + if (mounted) { + setState(() {}); + } + }); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(final BuildContext context) => _AppControllerInjector( + notifier: controller, + child: widget.child, + ); +} diff --git a/lib/config/bloc_config.dart b/lib/config/bloc_config.dart index 2a42456c..83027d7b 100644 --- a/lib/config/bloc_config.dart +++ b/lib/config/bloc_config.dart @@ -8,7 +8,6 @@ import 'package:selfprivacy/logic/bloc/server_jobs/server_jobs_bloc.dart'; import 'package:selfprivacy/logic/bloc/services/services_bloc.dart'; import 'package:selfprivacy/logic/bloc/users/users_bloc.dart'; import 'package:selfprivacy/logic/bloc/volumes/volumes_bloc.dart'; -import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart'; import 'package:selfprivacy/logic/cubit/client_jobs/client_jobs_cubit.dart'; import 'package:selfprivacy/logic/cubit/dns_records/dns_records_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart'; @@ -56,58 +55,46 @@ class BlocAndProviderConfigState extends State { } @override - Widget build(final BuildContext context) { - const isDark = false; - const isAutoDark = true; - - return MultiProvider( - providers: [ - BlocProvider( - create: (final _) => AppSettingsCubit( - isDarkModeOn: isDark, - isAutoDarkModeOn: isAutoDark, - isOnboardingShowing: true, - )..load(), - ), - BlocProvider( - create: (final _) => supportSystemCubit, - ), - BlocProvider( - create: (final _) => serverInstallationCubit, - lazy: false, - ), - BlocProvider( - create: (final _) => usersBloc, - lazy: false, - ), - BlocProvider( - create: (final _) => servicesBloc, - ), - BlocProvider( - create: (final _) => backupsBloc, - ), - BlocProvider( - create: (final _) => dnsRecordsCubit, - ), - BlocProvider( - create: (final _) => recoveryKeyBloc, - ), - BlocProvider( - create: (final _) => devicesBloc, - ), - BlocProvider( - create: (final _) => serverJobsBloc, - ), - BlocProvider(create: (final _) => connectionStatusBloc), - BlocProvider( - create: (final _) => serverDetailsCubit, - ), - BlocProvider(create: (final _) => volumesBloc), - BlocProvider( - create: (final _) => JobsCubit(), - ), - ], - child: widget.child, - ); - } + Widget build(final BuildContext context) => MultiProvider( + providers: [ + BlocProvider( + create: (final _) => supportSystemCubit, + ), + BlocProvider( + create: (final _) => serverInstallationCubit, + lazy: false, + ), + BlocProvider( + create: (final _) => usersBloc, + lazy: false, + ), + BlocProvider( + create: (final _) => servicesBloc, + ), + BlocProvider( + create: (final _) => backupsBloc, + ), + BlocProvider( + create: (final _) => dnsRecordsCubit, + ), + BlocProvider( + create: (final _) => recoveryKeyBloc, + ), + BlocProvider( + create: (final _) => devicesBloc, + ), + BlocProvider( + create: (final _) => serverJobsBloc, + ), + BlocProvider(create: (final _) => connectionStatusBloc), + BlocProvider( + create: (final _) => serverDetailsCubit, + ), + BlocProvider(create: (final _) => volumesBloc), + BlocProvider( + create: (final _) => JobsCubit(), + ), + ], + child: widget.child, + ); } diff --git a/lib/config/hive_config.dart b/lib/config/hive_config.dart index 55b35e9e..11672859 100644 --- a/lib/config/hive_config.dart +++ b/lib/config/hive_config.dart @@ -60,17 +60,20 @@ class HiveConfig { /// Mappings for the different boxes and their keys class BNames { - /// App settings box. Contains app settings like [isDarkModeOn], [isOnboardingShowing] + /// App settings box. Contains app settings like [darkThemeModeOn], [shouldShowOnboarding] static String appSettingsBox = 'appSettings'; /// A boolean field of [appSettingsBox] box. - static String isDarkModeOn = 'isDarkModeOn'; + static String darkThemeModeOn = 'isDarkModeOn'; /// A boolean field of [appSettingsBox] box. - static String isAutoDarkModeOn = 'isAutoDarkModeOn'; + static String systemThemeModeOn = 'isAutoDarkModeOn'; /// A boolean field of [appSettingsBox] box. - static String isOnboardingShowing = 'isOnboardingShowing'; + static String shouldShowOnboarding = 'isOnboardingShowing'; + + /// A string field + static String appLocale = 'appLocale'; /// Encryption key to decrypt [serverInstallationBox] and [usersBox] box. static String serverInstallationEncryptionKey = 'key'; diff --git a/lib/config/preferences_repository/datasources/preferences_datasource.dart b/lib/config/preferences_repository/datasources/preferences_datasource.dart new file mode 100644 index 00000000..53ef8b09 --- /dev/null +++ b/lib/config/preferences_repository/datasources/preferences_datasource.dart @@ -0,0 +1,36 @@ +/// abstraction for manipulation of stored app preferences +abstract class PreferencesDataSource { + /// should onboarding be shown + Future getOnboardingFlag(); + + /// should onboarding be shown + Future setOnboardingFlag(final bool newValue); + + // TODO: should probably deprecate the following, instead add the + // getThemeMode and setThemeMode methods, which store one value instead of + // flags. + + /// should system theme mode be enabled + Future getSystemThemeModeFlag(); + + /// should system theme mode be enabled + Future setSystemThemeModeFlag(final bool newValue); + + /// should dark theme be enabled + Future getDarkThemeModeFlag(); + + /// should dark theme be enabled + Future setDarkThemeModeFlag(final bool newValue); + + /// locale, as set by user + /// + /// + /// when null, app takes system locale + Future getLocale(); + + /// locale, as set by user + /// + /// + /// when null, app takes system locale + Future setLocale(final String newLocale); +} diff --git a/lib/config/preferences_repository/datasources/preferences_hive_datasource.dart b/lib/config/preferences_repository/datasources/preferences_hive_datasource.dart new file mode 100644 index 00000000..80dd9f11 --- /dev/null +++ b/lib/config/preferences_repository/datasources/preferences_hive_datasource.dart @@ -0,0 +1,39 @@ +import 'package:hive/hive.dart'; +import 'package:selfprivacy/config/hive_config.dart'; +import 'package:selfprivacy/config/preferences_repository/datasources/preferences_datasource.dart'; + +/// app preferences data source hive implementation +class PreferencesHiveDataSource implements PreferencesDataSource { + final Box _appSettingsBox = Hive.box(BNames.appSettingsBox); + + @override + Future getOnboardingFlag() async => + _appSettingsBox.get(BNames.shouldShowOnboarding, defaultValue: true); + + @override + Future setOnboardingFlag(final bool newValue) async => + _appSettingsBox.put(BNames.shouldShowOnboarding, newValue); + + @override + Future getSystemThemeModeFlag() async => + _appSettingsBox.get(BNames.systemThemeModeOn); + + @override + Future setSystemThemeModeFlag(final bool newValue) async => + _appSettingsBox.put(BNames.systemThemeModeOn, newValue); + + @override + Future getDarkThemeModeFlag() async => + _appSettingsBox.get(BNames.darkThemeModeOn); + + @override + Future setDarkThemeModeFlag(final bool newValue) async => + _appSettingsBox.put(BNames.darkThemeModeOn, newValue); + + @override + Future getLocale() async => _appSettingsBox.get(BNames.appLocale); + + @override + Future setLocale(final String newLocale) async => + _appSettingsBox.put(BNames.appLocale, newLocale); +} diff --git a/lib/config/preferences_repository/inherited_preferences_repository.dart b/lib/config/preferences_repository/inherited_preferences_repository.dart new file mode 100644 index 00000000..4a2881b1 --- /dev/null +++ b/lib/config/preferences_repository/inherited_preferences_repository.dart @@ -0,0 +1,63 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:selfprivacy/config/preferences_repository/datasources/preferences_datasource.dart'; +import 'package:selfprivacy/config/preferences_repository/preferences_repository.dart'; + +class _PreferencesRepositoryInjector extends InheritedWidget { + const _PreferencesRepositoryInjector({ + required this.settingsRepository, + required super.child, + }); + + final PreferencesRepository settingsRepository; + + @override + bool updateShouldNotify( + covariant final _PreferencesRepositoryInjector oldWidget, + ) => + oldWidget.settingsRepository != settingsRepository; +} + +/// Creates and injects app preferences repository inside widget tree. +class InheritedPreferencesRepository extends StatefulWidget { + const InheritedPreferencesRepository({ + required this.child, + required this.dataSource, + super.key, + }); + + final PreferencesDataSource dataSource; + final Widget child; + + @override + State createState() => + _InheritedPreferencesRepositoryState(); + + static PreferencesRepository? of(final BuildContext context) => context + .dependOnInheritedWidgetOfExactType<_PreferencesRepositoryInjector>() + ?.settingsRepository; +} + +class _InheritedPreferencesRepositoryState + extends State { + late PreferencesRepository repo; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + + /// recreate repo each time dependencies change + repo = PreferencesRepository( + dataSource: widget.dataSource, + setDelegateLocale: EasyLocalization.of(context)!.setLocale, + getDelegateLocale: () => EasyLocalization.of(context)!.locale, + getSupportedLocales: () => EasyLocalization.of(context)!.supportedLocales, + ); + } + + @override + Widget build(final BuildContext context) => _PreferencesRepositoryInjector( + settingsRepository: repo, + child: widget.child, + ); +} diff --git a/lib/config/preferences_repository/preferences_repository.dart b/lib/config/preferences_repository/preferences_repository.dart new file mode 100644 index 00000000..b649d38a --- /dev/null +++ b/lib/config/preferences_repository/preferences_repository.dart @@ -0,0 +1,73 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:selfprivacy/config/preferences_repository/datasources/preferences_datasource.dart'; + +class PreferencesRepository { + const PreferencesRepository({ + required this.dataSource, + required this.getSupportedLocales, + required this.getDelegateLocale, + required this.setDelegateLocale, + }); + + final PreferencesDataSource dataSource; + + /// easy localizations don't expose type of localization provider, + /// so it needs to be this crutchy (I could've created one more class-wrapper, + /// containing needed functions, but perceive it as boilerplate, because we + /// don't need additional encapsulation level here) + final FutureOr Function(Locale) setDelegateLocale; + final FutureOr> Function() getSupportedLocales; + final FutureOr Function() getDelegateLocale; + + Future getSystemThemeModeFlag() async => + (await dataSource.getSystemThemeModeFlag()) ?? true; + + Future setSystemThemeModeFlag(final bool newValue) async => + dataSource.setSystemThemeModeFlag(newValue); + + Future getDarkThemeModeFlag() async => + (await dataSource.getDarkThemeModeFlag()) ?? false; + + Future setDarkThemeModeFlag(final bool newValue) async => + dataSource.setDarkThemeModeFlag(newValue); + + Future setSystemModeFlag(final bool newValue) async => + dataSource.setSystemThemeModeFlag(newValue); + + // Future getThemeMode() async { + // final themeMode = await dataSource.getThemeMode()?? ThemeMode.system; + // } + // + // Future setThemeMode(final ThemeMode newThemeMode) => + // dataSource.setThemeMode(newThemeMode); + + Future> supportedLocales() async => getSupportedLocales(); + + Future getActiveLocale() async { + Locale? chosenLocale; + + final String? storedLocaleCode = await dataSource.getLocale(); + if (storedLocaleCode != null) { + chosenLocale = Locale(storedLocaleCode); + } + + // when it's null fallback on delegate locale + chosenLocale ??= await getDelegateLocale(); + + return chosenLocale; + } + + Future setActiveLocale(final Locale newLocale) async { + await dataSource.setLocale(newLocale.toString()); + } + + /// true when we need to show onboarding + Future getShouldShowOnboarding() async => + dataSource.getOnboardingFlag(); + + /// true when we need to show onboarding + Future setShouldShowOnboarding(final bool newValue) => + dataSource.setOnboardingFlag(newValue); +} diff --git a/lib/logic/cubit/app_settings/app_settings_cubit.dart b/lib/logic/cubit/app_settings/app_settings_cubit.dart deleted file mode 100644 index 548ed812..00000000 --- a/lib/logic/cubit/app_settings/app_settings_cubit.dart +++ /dev/null @@ -1,66 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:hive/hive.dart'; -import 'package:material_color_utilities/material_color_utilities.dart' - as color_utils; -import 'package:selfprivacy/config/brand_colors.dart'; -import 'package:selfprivacy/config/hive_config.dart'; -import 'package:selfprivacy/theming/factory/app_theme_factory.dart'; - -export 'package:provider/provider.dart'; - -part 'app_settings_state.dart'; - -class AppSettingsCubit extends Cubit { - AppSettingsCubit({ - required final bool isDarkModeOn, - required final bool isAutoDarkModeOn, - required final bool isOnboardingShowing, - }) : super( - AppSettingsState( - isDarkModeOn: isDarkModeOn, - isAutoDarkModeOn: isAutoDarkModeOn, - isOnboardingShowing: isOnboardingShowing, - ), - ); - - Box box = Hive.box(BNames.appSettingsBox); - - void load() async { - final bool? isDarkModeOn = box.get(BNames.isDarkModeOn); - final bool? isAutoDarkModeOn = box.get(BNames.isAutoDarkModeOn); - final bool? isOnboardingShowing = box.get(BNames.isOnboardingShowing); - emit( - state.copyWith( - isDarkModeOn: isDarkModeOn, - isAutoDarkModeOn: isAutoDarkModeOn, - isOnboardingShowing: isOnboardingShowing, - ), - ); - WidgetsFlutterBinding.ensureInitialized(); - final color_utils.CorePalette? colorPalette = - await AppThemeFactory.getCorePalette(); - emit( - state.copyWith( - corePalette: colorPalette, - ), - ); - } - - void updateDarkMode({required final bool isDarkModeOn}) { - box.put(BNames.isDarkModeOn, isDarkModeOn); - emit(state.copyWith(isDarkModeOn: isDarkModeOn)); - } - - void updateAutoDarkMode({required final bool isAutoDarkModeOn}) { - box.put(BNames.isAutoDarkModeOn, isAutoDarkModeOn); - emit(state.copyWith(isAutoDarkModeOn: isAutoDarkModeOn)); - } - - void turnOffOnboarding({final bool isOnboardingShowing = false}) { - box.put(BNames.isOnboardingShowing, isOnboardingShowing); - - emit(state.copyWith(isOnboardingShowing: isOnboardingShowing)); - } -} diff --git a/lib/logic/cubit/app_settings/app_settings_state.dart b/lib/logic/cubit/app_settings/app_settings_state.dart deleted file mode 100644 index ad364d66..00000000 --- a/lib/logic/cubit/app_settings/app_settings_state.dart +++ /dev/null @@ -1,35 +0,0 @@ -part of 'app_settings_cubit.dart'; - -class AppSettingsState extends Equatable { - const AppSettingsState({ - required this.isDarkModeOn, - required this.isAutoDarkModeOn, - required this.isOnboardingShowing, - this.corePalette, - }); - - final bool isDarkModeOn; - final bool isAutoDarkModeOn; - final bool isOnboardingShowing; - final color_utils.CorePalette? corePalette; - - AppSettingsState copyWith({ - final bool? isDarkModeOn, - final bool? isAutoDarkModeOn, - final bool? isOnboardingShowing, - final color_utils.CorePalette? corePalette, - }) => - AppSettingsState( - isDarkModeOn: isDarkModeOn ?? this.isDarkModeOn, - isAutoDarkModeOn: isAutoDarkModeOn ?? this.isAutoDarkModeOn, - isOnboardingShowing: isOnboardingShowing ?? this.isOnboardingShowing, - corePalette: corePalette ?? this.corePalette, - ); - - color_utils.CorePalette get corePaletteOrDefault => - corePalette ?? color_utils.CorePalette.of(BrandColors.primary.value); - - @override - List get props => - [isDarkModeOn, isAutoDarkModeOn, isOnboardingShowing, corePalette]; -} diff --git a/lib/main.dart b/lib/main.dart index 4eb1933c..9acae644 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,21 +1,19 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:selfprivacy/config/app_controller/inherited_app_controller.dart'; import 'package:selfprivacy/config/bloc_config.dart'; import 'package:selfprivacy/config/bloc_observer.dart'; import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/config/hive_config.dart'; import 'package:selfprivacy/config/localization.dart'; -import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart'; -import 'package:selfprivacy/theming/factory/app_theme_factory.dart'; +import 'package:selfprivacy/config/preferences_repository/datasources/preferences_hive_datasource.dart'; +import 'package:selfprivacy/config/preferences_repository/inherited_preferences_repository.dart'; import 'package:selfprivacy/ui/router/router.dart'; -// import 'package:wakelock/wakelock.dart'; import 'package:timezone/data/latest.dart' as tz; void main() async { - WidgetsFlutterBinding.ensureInitialized(); - await HiveConfig.init(); // await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); // try { @@ -26,86 +24,111 @@ void main() async { // print(e); // } + await Future.wait( + >[ + HiveConfig.init(), + EasyLocalization.ensureInitialized(), + ], + ); await getItSetup(); - await EasyLocalization.ensureInitialized(); - tz.initializeTimeZones(); - final ThemeData lightThemeData = await AppThemeFactory.create( - isDark: false, - fallbackColor: BrandColors.primary, - ); - final ThemeData darkThemeData = await AppThemeFactory.create( - isDark: true, - fallbackColor: BrandColors.primary, - ); + tz.initializeTimeZones(); Bloc.observer = SimpleBlocObserver(); runApp( - SelfprivacyApp( - lightThemeData: lightThemeData, - darkThemeData: darkThemeData, + Localization( + child: InheritedPreferencesRepository( + dataSource: PreferencesHiveDataSource(), + child: const InheritedAppController( + child: AppBuilder(), + ), + ), ), ); } -class SelfprivacyApp extends StatelessWidget { - SelfprivacyApp({ - required this.lightThemeData, - required this.darkThemeData, - super.key, - }); - - final ThemeData lightThemeData; - final ThemeData darkThemeData; - - final _appRouter = RootRouter(getIt.get().navigatorKey); +class AppBuilder extends StatelessWidget { + const AppBuilder({super.key}); @override - Widget build(final BuildContext context) => Localization( - child: BlocAndProviderConfig( - child: BlocBuilder( - builder: ( - final BuildContext context, - final AppSettingsState appSettings, - ) { - getIt.get().setLocaleCode( - context.locale.languageCode, - ); - return MaterialApp.router( - routeInformationParser: _appRouter.defaultRouteParser(), - routerDelegate: _appRouter.delegate(), - scaffoldMessengerKey: - getIt.get().scaffoldMessengerKey, - localizationsDelegates: context.localizationDelegates, - supportedLocales: context.supportedLocales, - locale: context.locale, - debugShowCheckedModeBanner: false, - title: 'SelfPrivacy', - theme: lightThemeData, - darkTheme: darkThemeData, - themeMode: appSettings.isAutoDarkModeOn - ? ThemeMode.system - : appSettings.isDarkModeOn - ? ThemeMode.dark - : ThemeMode.light, - scrollBehavior: const MaterialScrollBehavior().copyWith( - scrollbars: false, - ), - builder: (final BuildContext context, final Widget? widget) { - Widget error = - const Center(child: Text('...rendering error...')); - if (widget is Scaffold || widget is Navigator) { - error = Scaffold(body: error); - } - ErrorWidget.builder = - (final FlutterErrorDetails errorDetails) => error; + Widget build(final BuildContext context) { + final appController = InheritedAppController.of(context); - return widget ?? error; - }, - ); - }, + if (appController.loaded) { + return const SelfprivacyApp(); + } + + return const SplashScreen(); + } +} + +/// Widget to be shown +/// until essential app initialization is completed +class SplashScreen extends StatelessWidget { + const SplashScreen({super.key}); + + @override + Widget build(final BuildContext context) => const ColoredBox( + color: Colors.white, + child: Center( + child: CircularProgressIndicator.adaptive( + valueColor: AlwaysStoppedAnimation(BrandColors.primary), ), ), ); } + +class SelfprivacyApp extends StatefulWidget { + const SelfprivacyApp({ + super.key, + }); + + @override + State createState() => _SelfprivacyAppState(); +} + +class _SelfprivacyAppState extends State { + final appKey = UniqueKey(); + final _appRouter = RootRouter(getIt.get().navigatorKey); + + @override + Widget build(final BuildContext context) { + final appController = InheritedAppController.of(context); + + return BlocAndProviderConfig( + child: MaterialApp.router( + key: appKey, + title: 'SelfPrivacy', + // routing + routeInformationParser: _appRouter.defaultRouteParser(), + routerDelegate: _appRouter.delegate(), + scaffoldMessengerKey: + getIt.get().scaffoldMessengerKey, + // localization settings + locale: context.locale, + supportedLocales: context.supportedLocales, + localizationsDelegates: context.localizationDelegates, + // theme settings + themeMode: appController.themeMode, + theme: appController.lightTheme, + darkTheme: appController.darkTheme, + // other preferences + debugShowCheckedModeBanner: false, + scrollBehavior: + const MaterialScrollBehavior().copyWith(scrollbars: false), + builder: _builder, + ), + ); + } + + Widget _builder(final BuildContext context, final Widget? widget) { + Widget error = const Center(child: Text('...rendering error...')); + if (widget is Scaffold || widget is Navigator) { + error = Scaffold(body: error); + } + ErrorWidget.builder = (final FlutterErrorDetails errorDetails) => error; + + return widget ?? error; + } +} diff --git a/lib/ui/pages/more/app_settings/app_settings.dart b/lib/ui/pages/more/app_settings/app_settings.dart index 7cce6715..b96fd749 100644 --- a/lib/ui/pages/more/app_settings/app_settings.dart +++ b/lib/ui/pages/more/app_settings/app_settings.dart @@ -1,10 +1,16 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart'; +import 'package:gap/gap.dart'; +import 'package:selfprivacy/config/app_controller/inherited_app_controller.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/ui/components/buttons/dialog_action_button.dart'; import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart'; +import 'package:selfprivacy/ui/router/router.dart'; + +part 'language_picker.dart'; +part 'reset_app_button.dart'; +part 'theme_picker.dart'; @RoutePage() class AppSettingsPage extends StatefulWidget { @@ -16,82 +22,36 @@ class AppSettingsPage extends StatefulWidget { class _AppSettingsPageState extends State { @override - Widget build(final BuildContext context) { - final bool isDarkModeOn = - context.watch().state.isDarkModeOn; - - final bool isSystemDarkModeOn = - context.watch().state.isAutoDarkModeOn; - - return BrandHeroScreen( - hasBackButton: true, - hasFlashButton: false, - bodyPadding: const EdgeInsets.symmetric(vertical: 16), - heroTitle: 'application_settings.title'.tr(), - children: [ - SwitchListTile.adaptive( - title: Text('application_settings.system_dark_theme_title'.tr()), - subtitle: - Text('application_settings.system_dark_theme_description'.tr()), - value: isSystemDarkModeOn, - onChanged: (final value) => context - .read() - .updateAutoDarkMode(isAutoDarkModeOn: !isSystemDarkModeOn), + Widget build(final BuildContext context) => BrandHeroScreen( + hasBackButton: true, + hasFlashButton: false, + bodyPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 16, ), - SwitchListTile.adaptive( - title: Text('application_settings.dark_theme_title'.tr()), - subtitle: Text('application_settings.dark_theme_description'.tr()), - value: Theme.of(context).brightness == Brightness.dark, - onChanged: isSystemDarkModeOn - ? null - : (final value) => context - .read() - .updateDarkMode(isDarkModeOn: !isDarkModeOn), - ), - const Divider(height: 0), - Padding( - padding: const EdgeInsets.all(16), - child: Text( - 'application_settings.dangerous_settings'.tr(), - style: Theme.of(context).textTheme.labelLarge!.copyWith( - color: Theme.of(context).colorScheme.error, - ), + heroTitle: 'application_settings.title'.tr(), + children: [ + _ThemePicker( + key: ValueKey('theme_picker'.tr()), ), - ), - const _ResetAppTile(), - ], - ); - } -} - -class _ResetAppTile extends StatelessWidget { - const _ResetAppTile(); - - @override - Widget build(final BuildContext context) => ListTile( - title: Text('application_settings.reset_config_title'.tr()), - subtitle: Text('application_settings.reset_config_description'.tr()), - onTap: () { - showDialog( - context: context, - builder: (final _) => AlertDialog( - title: Text('modals.are_you_sure'.tr()), - content: Text('modals.purge_all_keys'.tr()), - actions: [ - DialogActionButton( - text: 'modals.purge_all_keys_confirm'.tr(), - isRed: true, - onPressed: () { - context.read().clearAppConfig(); - Navigator.of(context).pop(); - }, - ), - DialogActionButton( - text: 'basis.cancel'.tr(), - ), - ], + const Divider(height: 5, thickness: 0), + _LanguagePicker( + key: ValueKey('language_picker'.tr()), + ), + const Divider(height: 5, thickness: 0), + const Gap(4), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + 'application_settings.dangerous_settings'.tr(), + style: Theme.of(context).textTheme.titleLarge!.copyWith( + color: Theme.of(context).colorScheme.error, + ), ), - ); - }, + ), + _ResetAppTile( + key: ValueKey('reset_app'.tr()), + ), + ], ); } diff --git a/lib/ui/pages/more/app_settings/developer_settings.dart b/lib/ui/pages/more/app_settings/developer_settings.dart index 751eabb6..51e2a2b3 100644 --- a/lib/ui/pages/more/app_settings/developer_settings.dart +++ b/lib/ui/pages/more/app_settings/developer_settings.dart @@ -1,11 +1,12 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:selfprivacy/config/app_controller/inherited_app_controller.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/api_maps/tls_options.dart'; import 'package:selfprivacy/logic/bloc/services/services_bloc.dart'; import 'package:selfprivacy/logic/bloc/volumes/volumes_bloc.dart'; -import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart'; import 'package:selfprivacy/ui/components/list_tiles/section_title.dart'; import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart'; import 'package:selfprivacy/ui/router/router.dart'; @@ -60,17 +61,14 @@ class _DeveloperSettingsPageState extends State { title: Text('developer_settings.reset_onboarding'.tr()), subtitle: Text('developer_settings.reset_onboarding_description'.tr()), - enabled: - !context.watch().state.isOnboardingShowing, - onTap: () => context - .read() - .turnOffOnboarding(isOnboardingShowing: true), + enabled: !InheritedAppController.of(context).shouldShowOnboarding, + onTap: () => InheritedAppController.of(context) + .setShouldShowOnboarding(true), ), ListTile( title: Text('storage.start_migration_button'.tr()), subtitle: Text('storage.data_migration_notice'.tr()), - enabled: - !context.watch().state.isOnboardingShowing, + enabled: InheritedAppController.of(context).shouldShowOnboarding, onTap: () => context.pushRoute( ServicesMigrationRoute( diskStatus: context.read().state.diskStatus, diff --git a/lib/ui/pages/more/app_settings/language_picker.dart b/lib/ui/pages/more/app_settings/language_picker.dart new file mode 100644 index 00000000..75738ccc --- /dev/null +++ b/lib/ui/pages/more/app_settings/language_picker.dart @@ -0,0 +1,53 @@ +part of 'app_settings.dart'; + +class _LanguagePicker extends StatelessWidget { + const _LanguagePicker({super.key}); + + @override + Widget build(final BuildContext context) => ListTile( + title: Text( + 'application_settings.language'.tr(), + ), + subtitle: Text('application_settings.click_to_change_locale'.tr()), + trailing: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Text( + context.locale.toString(), + style: Theme.of(context).textTheme.headlineSmall, + ), + ), + onTap: () async { + final appController = InheritedAppController.of(context); + final Locale? newLocale = await showDialog( + context: context, + builder: (final context) => const _LanguagePickerDialog(), + routeSettings: _LanguagePickerDialog.routeSettings, + ); + + if (newLocale != null) { + await appController.setLocale(newLocale); + } + }, + ); +} + +class _LanguagePickerDialog extends StatelessWidget { + const _LanguagePickerDialog(); + static const routeSettings = RouteSettings(name: 'LanguagePickerDialog'); + + @override + Widget build(final BuildContext context) => SimpleDialog( + title: Text('application_settings.language'.tr()), + children: [ + for (final locale + in InheritedAppController.of(context).supportedLocales) + ListTile( + // TODO: add locale to language name matcher + title: Text(locale.toString()), + onTap: () { + Navigator.of(context).pop(locale); + }, + ) + ], + ); +} diff --git a/lib/ui/pages/more/app_settings/reset_app_button.dart b/lib/ui/pages/more/app_settings/reset_app_button.dart new file mode 100644 index 00000000..92ec3022 --- /dev/null +++ b/lib/ui/pages/more/app_settings/reset_app_button.dart @@ -0,0 +1,42 @@ +part of 'app_settings.dart'; + +class _ResetAppTile extends StatelessWidget { + const _ResetAppTile({super.key}); + + @override + Widget build(final BuildContext context) => ListTile( + title: Text('application_settings.reset_config_title'.tr()), + subtitle: Text('application_settings.reset_config_description'.tr()), + onTap: () => showDialog( + context: context, + builder: (final context) => const _ResetAppDialog(), + ), + ); +} + +class _ResetAppDialog extends StatelessWidget { + const _ResetAppDialog(); + + @override + Widget build(final BuildContext context) => AlertDialog( + title: Text('modals.are_you_sure'.tr()), + content: Text('modals.purge_all_keys'.tr()), + actions: [ + DialogActionButton( + text: 'modals.purge_all_keys_confirm'.tr(), + isRed: true, + onPressed: () { + context.read().clearAppConfig(); + + context.router.maybePop([ + const RootRoute(), + ]); + context.resetLocale(); + }, + ), + DialogActionButton( + text: 'basis.cancel'.tr(), + ), + ], + ); +} diff --git a/lib/ui/pages/more/app_settings/theme_picker.dart b/lib/ui/pages/more/app_settings/theme_picker.dart new file mode 100644 index 00000000..0e123045 --- /dev/null +++ b/lib/ui/pages/more/app_settings/theme_picker.dart @@ -0,0 +1,44 @@ +part of 'app_settings.dart'; + +class _ThemePicker extends StatelessWidget { + const _ThemePicker({super.key}); + + @override + Widget build(final BuildContext context) { + final appController = InheritedAppController.of(context); + // final themeMode = appController.themeMode; + // final bool isSystemThemeModeEnabled = themeMode == ThemeMode.system; + // final bool isDarkModeOn = themeMode == ThemeMode.dark; + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SwitchListTile.adaptive( + title: Text('application_settings.system_theme_mode_title'.tr()), + subtitle: + Text('application_settings.system_theme_mode_description'.tr()), + value: appController.systemThemeModeActive, + onChanged: appController.setSystemThemeModeFlag, + // onChanged: (final newValue) => appController.setThemeMode( + // newValue + // ? ThemeMode.system + // : (isDarkModeOn ? ThemeMode.dark : ThemeMode.light), + // ), + ), + SwitchListTile.adaptive( + title: Text('application_settings.dark_theme_title'.tr()), + subtitle: Text('application_settings.change_application_theme'.tr()), + value: appController.darkThemeModeActive, + onChanged: appController.systemThemeModeActive + ? null + : appController.setDarkThemeModeFlag, + // onChanged: isSystemThemeModeEnabled + // ? null + // : (final newValue) => appController.setThemeMode( + // newValue ? ThemeMode.dark : ThemeMode.light, + // ), + ), + ], + ); + } +} diff --git a/lib/ui/pages/onboarding/onboarding.dart b/lib/ui/pages/onboarding/onboarding.dart index 141c9463..14d7a976 100644 --- a/lib/ui/pages/onboarding/onboarding.dart +++ b/lib/ui/pages/onboarding/onboarding.dart @@ -1,6 +1,6 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; -import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart'; +import 'package:selfprivacy/config/app_controller/inherited_app_controller.dart'; import 'package:selfprivacy/ui/pages/onboarding/views/views.dart'; import 'package:selfprivacy/ui/router/router.dart'; @@ -37,7 +37,8 @@ class _OnboardingPageState extends State { ), OnboardingSecondView( onProceed: () { - context.read().turnOffOnboarding(); + InheritedAppController.of(context) + .setShouldShowOnboarding(false); context.router.replaceAll([ const RootRoute(), const InitializingRoute(), diff --git a/lib/ui/pages/root_route.dart b/lib/ui/pages/root_route.dart index 6ae7607c..5bf0a45f 100644 --- a/lib/ui/pages/root_route.dart +++ b/lib/ui/pages/root_route.dart @@ -1,7 +1,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart'; +import 'package:selfprivacy/config/app_controller/inherited_app_controller.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/ui/layouts/root_scaffold_with_navigation.dart'; import 'package:selfprivacy/ui/router/root_destinations.dart'; @@ -19,31 +19,33 @@ class RootPage extends StatefulWidget implements AutoRouteWrapper { } class _RootPageState extends State with TickerProviderStateMixin { - bool shouldUseSplitView() => false; + @override + void didChangeDependencies() { + if (InheritedAppController.of(context).shouldShowOnboarding) { + context.router.replace(const OnboardingRoute()); + } - final destinations = rootDestinations; + super.didChangeDependencies(); + } @override Widget build(final BuildContext context) { final bool isReady = context.watch().state is ServerInstallationFinished; - if (context.read().state.isOnboardingShowing) { - context.router.replace(const OnboardingRoute()); - } - return AutoRouter( builder: (final context, final child) { - final currentDestinationIndex = destinations.indexWhere( + final currentDestinationIndex = rootDestinations.indexWhere( (final destination) => context.router.isRouteActive(destination.route.routeName), ); final isOtherRouterActive = context.router.root.current.name != RootRoute.name; + final routeName = getRouteTitle(context.router.current.name).tr(); return RootScaffoldWithNavigation( title: routeName, - destinations: destinations, + destinations: rootDestinations, showBottomBar: !(currentDestinationIndex == -1 && !isOtherRouterActive), showFab: isReady, @@ -53,99 +55,3 @@ class _RootPageState extends State with TickerProviderStateMixin { ); } } - -class MainScreenNavigationRail extends StatelessWidget { - const MainScreenNavigationRail({ - required this.destinations, - super.key, - }); - - final List destinations; - - @override - Widget build(final BuildContext context) { - int? activeIndex = destinations.indexWhere( - (final destination) => - context.router.isRouteActive(destination.route.routeName), - ); - if (activeIndex == -1) { - activeIndex = null; - } - - return Padding( - padding: const EdgeInsets.all(8.0), - child: SizedBox( - height: MediaQuery.of(context).size.height, - width: 72, - child: LayoutBuilder( - builder: (final context, final constraints) => SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints(minHeight: constraints.maxHeight), - child: IntrinsicHeight( - child: NavigationRail( - backgroundColor: Colors.transparent, - labelType: NavigationRailLabelType.all, - destinations: destinations - .map( - (final destination) => NavigationRailDestination( - icon: Icon(destination.icon), - label: Text(destination.label), - ), - ) - .toList(), - selectedIndex: activeIndex, - onDestinationSelected: (final index) { - context.router.replaceAll([destinations[index].route]); - }, - ), - ), - ), - ), - ), - ), - ); - } -} - -class MainScreenNavigationDrawer extends StatelessWidget { - const MainScreenNavigationDrawer({ - required this.destinations, - super.key, - }); - - final List destinations; - - @override - Widget build(final BuildContext context) { - int? activeIndex = destinations.indexWhere( - (final destination) => - context.router.isRouteActive(destination.route.routeName), - ); - if (activeIndex == -1) { - activeIndex = null; - } - - return SizedBox( - height: MediaQuery.of(context).size.height, - width: 296, - child: LayoutBuilder( - builder: (final context, final constraints) => NavigationDrawer( - key: const Key('PrimaryNavigationDrawer'), - selectedIndex: activeIndex, - onDestinationSelected: (final index) { - context.router.replaceAll([destinations[index].route]); - }, - children: [ - const SizedBox(height: 18), - ...destinations.map( - (final destination) => NavigationDrawerDestination( - icon: Icon(destination.icon), - label: Text(destination.label), - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/ui/pages/setup/initializing/server_type_picker.dart b/lib/ui/pages/setup/initializing/server_type_picker.dart index bdcabe92..25f559b2 100644 --- a/lib/ui/pages/setup/initializing/server_type_picker.dart +++ b/lib/ui/pages/setup/initializing/server_type_picker.dart @@ -1,7 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:selfprivacy/config/app_controller/inherited_app_controller.dart'; import 'package:selfprivacy/illustrations/stray_deer.dart'; -import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/models/price.dart'; import 'package:selfprivacy/logic/models/server_provider_location.dart'; @@ -205,10 +205,8 @@ class SelectTypePage extends StatelessWidget { ), painter: StrayDeerPainter( colorScheme: Theme.of(context).colorScheme, - colorPalette: context - .read() - .state - .corePaletteOrDefault, + colorPalette: + InheritedAppController.of(context).corePalette, ), ), ),