feat: introduced app_controller, rehooked dependencies from app_settings_cubit, added language picker to settings_page

This commit is contained in:
Aliaksei Tratseuski 2024-05-15 19:36:00 +04:00
parent 0ad15061a3
commit ea2cc28ac9
19 changed files with 840 additions and 426 deletions

View file

@ -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<ApiConfigModel>();
bool _loaded = false;
bool get loaded => _loaded;
// localization
late Locale _locale;
Locale get locale => _locale;
late List<Locale> _supportedLocales;
List<Locale> 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<void> 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(<Future>[
// 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<void> 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<void> 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<void> 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<void> 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<void> 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);
}
}

View file

@ -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<AppController> {
const _AppControllerInjector({
required super.child,
required super.notifier,
});
}
class InheritedAppController extends StatefulWidget {
const InheritedAppController({
required this.child,
super.key,
});
final Widget child;
@override
State<InheritedAppController> createState() => _InheritedAppControllerState();
static AppController of(final BuildContext context) => context
.dependOnInheritedWidgetOfExactType<_AppControllerInjector>()!
.notifier!;
}
class _InheritedAppControllerState extends State<InheritedAppController> {
// 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<void> initialize() async {
late final ThemeData lightThemeData;
late final ThemeData darkThemeData;
late final color_utils.CorePalette colorPalette;
await Future.wait(
<Future<void>>[
() 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,
);
}

View file

@ -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,19 +55,8 @@ class BlocAndProviderConfigState extends State<BlocAndProviderConfig> {
}
@override
Widget build(final BuildContext context) {
const isDark = false;
const isAutoDark = true;
return MultiProvider(
Widget build(final BuildContext context) => MultiProvider(
providers: [
BlocProvider(
create: (final _) => AppSettingsCubit(
isDarkModeOn: isDark,
isAutoDarkModeOn: isAutoDark,
isOnboardingShowing: true,
)..load(),
),
BlocProvider(
create: (final _) => supportSystemCubit,
),
@ -109,5 +97,4 @@ class BlocAndProviderConfigState extends State<BlocAndProviderConfig> {
],
child: widget.child,
);
}
}

View file

@ -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';

View file

@ -0,0 +1,36 @@
/// abstraction for manipulation of stored app preferences
abstract class PreferencesDataSource {
/// should onboarding be shown
Future<bool> getOnboardingFlag();
/// should onboarding be shown
Future<void> 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<bool?> getSystemThemeModeFlag();
/// should system theme mode be enabled
Future<void> setSystemThemeModeFlag(final bool newValue);
/// should dark theme be enabled
Future<bool?> getDarkThemeModeFlag();
/// should dark theme be enabled
Future<void> setDarkThemeModeFlag(final bool newValue);
/// locale, as set by user
///
///
/// when null, app takes system locale
Future<String?> getLocale();
/// locale, as set by user
///
///
/// when null, app takes system locale
Future<void> setLocale(final String newLocale);
}

View file

@ -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<bool> getOnboardingFlag() async =>
_appSettingsBox.get(BNames.shouldShowOnboarding, defaultValue: true);
@override
Future<void> setOnboardingFlag(final bool newValue) async =>
_appSettingsBox.put(BNames.shouldShowOnboarding, newValue);
@override
Future<bool?> getSystemThemeModeFlag() async =>
_appSettingsBox.get(BNames.systemThemeModeOn);
@override
Future<void> setSystemThemeModeFlag(final bool newValue) async =>
_appSettingsBox.put(BNames.systemThemeModeOn, newValue);
@override
Future<bool?> getDarkThemeModeFlag() async =>
_appSettingsBox.get(BNames.darkThemeModeOn);
@override
Future<void> setDarkThemeModeFlag(final bool newValue) async =>
_appSettingsBox.put(BNames.darkThemeModeOn, newValue);
@override
Future<String?> getLocale() async => _appSettingsBox.get(BNames.appLocale);
@override
Future<void> setLocale(final String newLocale) async =>
_appSettingsBox.put(BNames.appLocale, newLocale);
}

View file

@ -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<InheritedPreferencesRepository> createState() =>
_InheritedPreferencesRepositoryState();
static PreferencesRepository? of(final BuildContext context) => context
.dependOnInheritedWidgetOfExactType<_PreferencesRepositoryInjector>()
?.settingsRepository;
}
class _InheritedPreferencesRepositoryState
extends State<InheritedPreferencesRepository> {
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,
);
}

View file

@ -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<void> Function(Locale) setDelegateLocale;
final FutureOr<List<Locale>> Function() getSupportedLocales;
final FutureOr<Locale> Function() getDelegateLocale;
Future<bool> getSystemThemeModeFlag() async =>
(await dataSource.getSystemThemeModeFlag()) ?? true;
Future<void> setSystemThemeModeFlag(final bool newValue) async =>
dataSource.setSystemThemeModeFlag(newValue);
Future<bool> getDarkThemeModeFlag() async =>
(await dataSource.getDarkThemeModeFlag()) ?? false;
Future<void> setDarkThemeModeFlag(final bool newValue) async =>
dataSource.setDarkThemeModeFlag(newValue);
Future<void> setSystemModeFlag(final bool newValue) async =>
dataSource.setSystemThemeModeFlag(newValue);
// Future<ThemeMode> getThemeMode() async {
// final themeMode = await dataSource.getThemeMode()?? ThemeMode.system;
// }
//
// Future<void> setThemeMode(final ThemeMode newThemeMode) =>
// dataSource.setThemeMode(newThemeMode);
Future<List<Locale>> supportedLocales() async => getSupportedLocales();
Future<Locale> 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<void> setActiveLocale(final Locale newLocale) async {
await dataSource.setLocale(newLocale.toString());
}
/// true when we need to show onboarding
Future<bool> getShouldShowOnboarding() async =>
dataSource.getOnboardingFlag();
/// true when we need to show onboarding
Future<void> setShouldShowOnboarding(final bool newValue) =>
dataSource.setOnboardingFlag(newValue);
}

View file

@ -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<AppSettingsState> {
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));
}
}

View file

@ -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<dynamic> get props =>
[isDarkModeOn, isAutoDarkModeOn, isOnboardingShowing, corePalette];
}

View file

@ -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(
<Future<void>>[
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,
class AppBuilder extends StatelessWidget {
const AppBuilder({super.key});
@override
Widget build(final BuildContext context) {
final appController = InheritedAppController.of(context);
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,
});
final ThemeData lightThemeData;
final ThemeData darkThemeData;
@override
State<SelfprivacyApp> createState() => _SelfprivacyAppState();
}
class _SelfprivacyAppState extends State<SelfprivacyApp> {
final appKey = UniqueKey();
final _appRouter = RootRouter(getIt.get<NavigationService>().navigatorKey);
@override
Widget build(final BuildContext context) => Localization(
child: BlocAndProviderConfig(
child: BlocBuilder<AppSettingsCubit, AppSettingsState>(
builder: (
final BuildContext context,
final AppSettingsState appSettings,
) {
getIt.get<ApiConfigModel>().setLocaleCode(
context.locale.languageCode,
);
return MaterialApp.router(
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<NavigationService>().scaffoldMessengerKey,
localizationsDelegates: context.localizationDelegates,
supportedLocales: context.supportedLocales,
// 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,
title: 'SelfPrivacy',
theme: lightThemeData,
darkTheme: darkThemeData,
themeMode: appSettings.isAutoDarkModeOn
? ThemeMode.system
: appSettings.isDarkModeOn
? ThemeMode.dark
: ThemeMode.light,
scrollBehavior: const MaterialScrollBehavior().copyWith(
scrollbars: false,
scrollBehavior:
const MaterialScrollBehavior().copyWith(scrollbars: false),
builder: _builder,
),
builder: (final BuildContext context, final Widget? widget) {
Widget error =
const Center(child: Text('...rendering error...'));
);
}
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;
ErrorWidget.builder = (final FlutterErrorDetails errorDetails) => error;
return widget ?? error;
},
);
},
),
),
);
}
}

View file

@ -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<AppSettingsPage> {
@override
Widget build(final BuildContext context) {
final bool isDarkModeOn =
context.watch<AppSettingsCubit>().state.isDarkModeOn;
final bool isSystemDarkModeOn =
context.watch<AppSettingsCubit>().state.isAutoDarkModeOn;
return BrandHeroScreen(
Widget build(final BuildContext context) => BrandHeroScreen(
hasBackButton: true,
hasFlashButton: false,
bodyPadding: const EdgeInsets.symmetric(vertical: 16),
bodyPadding: const EdgeInsets.symmetric(
horizontal: 12,
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<AppSettingsCubit>()
.updateAutoDarkMode(isAutoDarkModeOn: !isSystemDarkModeOn),
_ThemePicker(
key: ValueKey('theme_picker'.tr()),
),
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<AppSettingsCubit>()
.updateDarkMode(isDarkModeOn: !isDarkModeOn),
const Divider(height: 5, thickness: 0),
_LanguagePicker(
key: ValueKey('language_picker'.tr()),
),
const Divider(height: 0),
const Divider(height: 5, thickness: 0),
const Gap(4),
Padding(
padding: const EdgeInsets.all(16),
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
'application_settings.dangerous_settings'.tr(),
style: Theme.of(context).textTheme.labelLarge!.copyWith(
style: Theme.of(context).textTheme.titleLarge!.copyWith(
color: Theme.of(context).colorScheme.error,
),
),
),
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<ServerInstallationCubit>().clearAppConfig();
Navigator.of(context).pop();
},
),
DialogActionButton(
text: 'basis.cancel'.tr(),
_ResetAppTile(
key: ValueKey('reset_app'.tr()),
),
],
),
);
},
);
}

View file

@ -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<DeveloperSettingsPage> {
title: Text('developer_settings.reset_onboarding'.tr()),
subtitle:
Text('developer_settings.reset_onboarding_description'.tr()),
enabled:
!context.watch<AppSettingsCubit>().state.isOnboardingShowing,
onTap: () => context
.read<AppSettingsCubit>()
.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<AppSettingsCubit>().state.isOnboardingShowing,
enabled: InheritedAppController.of(context).shouldShowOnboarding,
onTap: () => context.pushRoute(
ServicesMigrationRoute(
diskStatus: context.read<VolumesBloc>().state.diskStatus,

View file

@ -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<Locale?>(
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);
},
)
],
);
}

View file

@ -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<ServerInstallationCubit>().clearAppConfig();
context.router.maybePop([
const RootRoute(),
]);
context.resetLocale();
},
),
DialogActionButton(
text: 'basis.cancel'.tr(),
),
],
);
}

View file

@ -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,
// ),
),
],
);
}
}

View file

@ -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<OnboardingPage> {
),
OnboardingSecondView(
onProceed: () {
context.read<AppSettingsCubit>().turnOffOnboarding();
InheritedAppController.of(context)
.setShouldShowOnboarding(false);
context.router.replaceAll([
const RootRoute(),
const InitializingRoute(),

View file

@ -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<RootPage> 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<ServerInstallationCubit>().state
is ServerInstallationFinished;
if (context.read<AppSettingsCubit>().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<RootPage> with TickerProviderStateMixin {
);
}
}
class MainScreenNavigationRail extends StatelessWidget {
const MainScreenNavigationRail({
required this.destinations,
super.key,
});
final List<RouteDestination> 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<RouteDestination> 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),
),
),
],
),
),
);
}
}

View file

@ -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<AppSettingsCubit>()
.state
.corePaletteOrDefault,
colorPalette:
InheritedAppController.of(context).corePalette,
),
),
),