Refactor preferences

This commit is contained in:
problematicconsumer
2023-09-06 12:56:30 +03:30
parent cf1acb0b25
commit ef1846e553
26 changed files with 303 additions and 229 deletions

View File

@@ -1,11 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:hiddify/core/locale/locale.dart'; import 'package:hiddify/core/core_providers.dart';
import 'package:hiddify/core/prefs/prefs.dart';
import 'package:hiddify/core/router/router.dart'; import 'package:hiddify/core/router/router.dart';
import 'package:hiddify/core/theme/theme.dart';
import 'package:hiddify/features/common/common_controllers.dart'; import 'package:hiddify/features/common/common_controllers.dart';
import 'package:hiddify/gen/fonts.gen.dart';
import 'package:hiddify/utils/utils.dart'; import 'package:hiddify/utils/utils.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -15,24 +14,21 @@ class AppView extends HookConsumerWidget with PresLogger {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final router = ref.watch(routerProvider); final router = ref.watch(routerProvider);
final locale = ref.watch(localeControllerProvider).locale; final locale = ref.watch(localeProvider).locale;
final theme = ref.watch(themeControllerProvider); final theme = ref.watch(themeProvider);
ref.watch(commonControllersProvider); ref.watch(commonControllersProvider);
// HACK temporary solution
final fontFamily = locale.languageCode == "fa" ? FontFamily.shabnam : "";
return MaterialApp.router( return MaterialApp.router(
routerConfig: router, routerConfig: router,
locale: locale, locale: locale,
supportedLocales: LocalePref.locales, supportedLocales: AppLocale.locales,
localizationsDelegates: GlobalMaterialLocalizations.delegates, localizationsDelegates: GlobalMaterialLocalizations.delegates,
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
themeMode: theme.themeMode, themeMode: theme.mode,
theme: theme.light(fontFamily: fontFamily), theme: theme.light(),
darkTheme: theme.dark(fontFamily: fontFamily), darkTheme: theme.dark(),
title: 'Hiddify', title: 'Hiddify Next',
).animate().fadeIn(); ).animate().fadeIn();
} }
} }

View File

@@ -1,6 +1,15 @@
import 'package:hiddify/core/locale/locale.dart'; import 'package:hiddify/core/prefs/prefs.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
final translationsProvider = Provider<TranslationsEn>( part 'core_providers.g.dart';
(ref) => ref.watch(localeControllerProvider).translations(),
); @Riverpod(keepAlive: true)
TranslationsEn translations(TranslationsRef ref) =>
ref.watch(localeProvider).translations();
@riverpod
AppTheme theme(ThemeRef ref) => AppTheme(
ref.watch(themeModeProvider),
ref.watch(trueBlackThemeProvider),
ref.watch(localeProvider).preferredFontFamily,
);

View File

@@ -1,2 +0,0 @@
export 'locale_controller.dart';
export 'locale_pref.dart';

View File

@@ -1,24 +0,0 @@
import 'package:hiddify/core/locale/locale_pref.dart';
import 'package:hiddify/data/data_providers.dart';
import 'package:hiddify/utils/utils.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';
part 'locale_controller.g.dart';
@Riverpod(keepAlive: true)
class LocaleController extends _$LocaleController with AppLogger {
@override
LocalePref build() {
return LocalePref.values[_prefs.getInt(_localeKey) ?? 0];
}
static const _localeKey = 'locale';
SharedPreferences get _prefs => ref.read(sharedPreferencesProvider);
Future<void> change(LocalePref locale) async {
loggy.debug('changing locale to [$locale]');
await _prefs.setInt(_localeKey, locale.index);
state = locale;
}
}

View File

@@ -1,33 +0,0 @@
import 'package:dartx/dartx.dart';
import 'package:flutter/widgets.dart';
import 'package:hiddify/gen/translations.g.dart';
export 'package:hiddify/gen/translations.g.dart';
enum LocalePref {
en,
fa;
Locale get locale {
return Locale(name);
}
static List<Locale> get locales =>
LocalePref.values.map((e) => e.locale).toList();
static LocalePref fromString(String e) {
return LocalePref.values.firstOrNullWhere((element) => element.name == e) ??
LocalePref.en;
}
static LocalePref deviceLocale() {
return LocalePref.fromString(
AppLocaleUtils.findDeviceLocale().languageCode,
);
}
TranslationsEn translations() {
final appLocale = AppLocaleUtils.parse(name);
return appLocale.build();
}
}

View File

@@ -1,12 +1,19 @@
import 'package:flex_color_scheme/flex_color_scheme.dart'; import 'package:flex_color_scheme/flex_color_scheme.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hiddify/core/theme/theme_prefs.dart';
// mostly exact copy of flex color scheme 7.1's fabulous 12 theme // mostly exact copy of flex color scheme 7.1's fabulous 12 theme
extension AppTheme on ThemePrefs { class AppTheme {
ThemeData light({ AppTheme(
String fontFamily = "Shabnam", this.mode,
}) { this.trueBlack,
this.fontFamily,
);
final ThemeMode mode;
final bool trueBlack;
final String fontFamily;
ThemeData light() {
return FlexThemeData.light( return FlexThemeData.light(
scheme: FlexScheme.indigoM3, scheme: FlexScheme.indigoM3,
surfaceMode: FlexSurfaceMode.highScaffoldLowSurface, surfaceMode: FlexSurfaceMode.highScaffoldLowSurface,
@@ -62,12 +69,13 @@ extension AppTheme on ThemePrefs {
tones: FlexTones.jolly(Brightness.light), tones: FlexTones.jolly(Brightness.light),
visualDensity: FlexColorScheme.comfortablePlatformDensity, visualDensity: FlexColorScheme.comfortablePlatformDensity,
fontFamily: fontFamily, fontFamily: fontFamily,
extensions: <ThemeExtension<dynamic>>{
ConnectionButtonTheme.light,
},
); );
} }
ThemeData dark({ ThemeData dark() {
String fontFamily = "Shabnam",
}) {
return FlexThemeData.dark( return FlexThemeData.dark(
scheme: FlexScheme.indigoM3, scheme: FlexScheme.indigoM3,
useMaterial3: true, useMaterial3: true,
@@ -124,6 +132,48 @@ extension AppTheme on ThemePrefs {
// tones: FlexTones.jolly(Brightness.dark), // tones: FlexTones.jolly(Brightness.dark),
visualDensity: FlexColorScheme.comfortablePlatformDensity, visualDensity: FlexColorScheme.comfortablePlatformDensity,
fontFamily: fontFamily, fontFamily: fontFamily,
extensions: <ThemeExtension<dynamic>>{
ConnectionButtonTheme.light,
},
);
}
}
class ConnectionButtonTheme extends ThemeExtension<ConnectionButtonTheme> {
const ConnectionButtonTheme({
this.idleColor,
this.connectedColor,
});
final Color? idleColor;
final Color? connectedColor;
static const ConnectionButtonTheme light = ConnectionButtonTheme(
idleColor: Color(0xFF4a4d8b),
connectedColor: Color(0xFF44a334),
);
@override
ThemeExtension<ConnectionButtonTheme> copyWith({
Color? idleColor,
Color? connectedColor,
}) =>
ConnectionButtonTheme(
idleColor: idleColor ?? this.idleColor,
connectedColor: connectedColor ?? this.connectedColor,
);
@override
ThemeExtension<ConnectionButtonTheme> lerp(
covariant ThemeExtension<ConnectionButtonTheme>? other,
double t,
) {
if (other is! ConnectionButtonTheme) {
return this;
}
return ConnectionButtonTheme(
idleColor: Color.lerp(idleColor, other.idleColor, t),
connectedColor: Color.lerp(connectedColor, other.connectedColor, t),
); );
} }
} }

View File

@@ -0,0 +1,44 @@
import 'package:dartx/dartx.dart';
import 'package:flutter/widgets.dart';
import 'package:hiddify/gen/fonts.gen.dart';
import 'package:hiddify/gen/translations.g.dart';
import 'package:hiddify/utils/pref_notifier.dart';
export 'package:hiddify/gen/translations.g.dart';
final localeProvider = AlwaysAlivePrefNotifier.provider(
"locale",
AppLocale.deviceLocale(),
mapFrom: AppLocale.values.byName,
mapTo: (value) => value.name,
);
enum AppLocale {
en,
fa;
Locale get locale {
return Locale(name);
}
static List<Locale> get locales =>
AppLocale.values.map((e) => e.locale).toList();
static AppLocale fromString(String e) {
return AppLocale.values.firstOrNullWhere((element) => element.name == e) ??
AppLocale.en;
}
static AppLocale deviceLocale() {
return AppLocale.fromString(
AppLocaleUtils.findDeviceLocale().languageCode,
);
}
TranslationsEn translations() {
final appLocale = AppLocaleUtils.parse(name);
return appLocale.build();
}
String get preferredFontFamily => this == fa ? FontFamily.shabnam : "";
}

View File

@@ -1 +1,4 @@
export 'app_theme.dart';
export 'general_prefs.dart'; export 'general_prefs.dart';
export 'locale_prefs.dart';
export 'theme_prefs.dart';

View File

@@ -0,0 +1,14 @@
import 'package:flutter/material.dart';
import 'package:hiddify/utils/pref_notifier.dart';
final themeModeProvider = AlwaysAlivePrefNotifier.provider(
"theme_mode",
ThemeMode.system,
mapFrom: ThemeMode.values.byName,
mapTo: (value) => value.name,
);
final trueBlackThemeProvider = AlwaysAlivePrefNotifier.provider(
"true_black_theme",
false,
);

View File

@@ -1,6 +0,0 @@
import 'package:flutter/widgets.dart';
abstract class ConnectionButtonColor {
static const connected = Color.fromRGBO(89, 140, 82, 1);
static const disconnected = Color.fromRGBO(74, 77, 139, 1);
}

View File

@@ -1,4 +0,0 @@
export 'app_theme.dart';
export 'constants.dart';
export 'theme_controller.dart';
export 'theme_prefs.dart';

View File

@@ -1,41 +0,0 @@
import 'package:flutter/material.dart';
import 'package:hiddify/core/theme/theme_prefs.dart';
import 'package:hiddify/data/data_providers.dart';
import 'package:hiddify/utils/utils.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';
part 'theme_controller.g.dart';
@Riverpod(keepAlive: true)
class ThemeController extends _$ThemeController with AppLogger {
@override
ThemePrefs build() {
return ThemePrefs(
themeMode: ThemeMode.values[_prefs.getInt(_themeModeKey) ?? 0],
trueBlack: _prefs.getBool(_trueBlackKey) ?? false,
);
}
SharedPreferences get _prefs => ref.read(sharedPreferencesProvider);
static const _themeModeKey = "theme_mode";
static const _trueBlackKey = "true_black";
Future<void> change({
ThemeMode? themeMode,
bool? trueBlack,
}) async {
loggy.debug('changing theme, mode=$themeMode, trueBlack=$trueBlack');
if (themeMode != null) {
await _prefs.setInt(_themeModeKey, themeMode.index);
}
if (trueBlack != null) {
await _prefs.setBool(_trueBlackKey, trueBlack);
}
state = state.copyWith(
themeMode: themeMode ?? state.themeMode,
trueBlack: trueBlack ?? state.trueBlack,
);
}
}

View File

@@ -1,14 +0,0 @@
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'theme_prefs.freezed.dart';
@freezed
class ThemePrefs with _$ThemePrefs {
const ThemePrefs._();
const factory ThemePrefs({
@Default(ThemeMode.system) ThemeMode themeMode,
@Default(false) bool trueBlack,
}) = _ThemePrefs;
}

View File

@@ -1,5 +1,5 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hiddify/core/locale/locale.dart'; import 'package:hiddify/core/prefs/prefs.dart';
import 'package:hiddify/domain/failures.dart'; import 'package:hiddify/domain/failures.dart';
part 'update_failure.freezed.dart'; part 'update_failure.freezed.dart';

View File

@@ -1,5 +1,5 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hiddify/core/locale/locale.dart'; import 'package:hiddify/core/prefs/prefs.dart';
import 'package:hiddify/domain/core_service_failure.dart'; import 'package:hiddify/domain/core_service_failure.dart';
import 'package:hiddify/domain/failures.dart'; import 'package:hiddify/domain/failures.dart';
@@ -27,9 +27,9 @@ sealed class ConnectionFailure with _$ConnectionFailure, Failure {
@override @override
({String type, String? message}) present(TranslationsEn t) { ({String type, String? message}) present(TranslationsEn t) {
return switch (this) { return switch (this) {
UnexpectedConnectionFailure() => ( UnexpectedConnectionFailure(:final error) => (
type: t.failure.connectivity.unexpected, type: t.failure.connectivity.unexpected,
message: null message: t.mayPrintError(error),
), ),
MissingVpnPermission(:final message) => ( MissingVpnPermission(:final message) => (
type: t.failure.connectivity.missingVpnPermission, type: t.failure.connectivity.missingVpnPermission,

View File

@@ -1,5 +1,5 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hiddify/core/locale/locale.dart'; import 'package:hiddify/core/prefs/prefs.dart';
import 'package:hiddify/domain/connectivity/connection_failure.dart'; import 'package:hiddify/domain/connectivity/connection_failure.dart';
part 'connection_status.freezed.dart'; part 'connection_status.freezed.dart';

View File

@@ -1,5 +1,5 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hiddify/core/locale/locale.dart'; import 'package:hiddify/core/prefs/prefs.dart';
import 'package:hiddify/domain/failures.dart'; import 'package:hiddify/domain/failures.dart';
part 'core_service_failure.freezed.dart'; part 'core_service_failure.freezed.dart';
@@ -49,9 +49,9 @@ sealed class CoreServiceFailure with _$CoreServiceFailure, Failure {
@override @override
({String type, String? message}) present(TranslationsEn t) { ({String type, String? message}) present(TranslationsEn t) {
return switch (this) { return switch (this) {
UnexpectedCoreServiceFailure() => ( UnexpectedCoreServiceFailure(:final error) => (
type: t.failure.singbox.unexpected, type: t.failure.singbox.unexpected,
message: null message: t.mayPrintError(error),
), ),
CoreServiceNotRunning(:final message) => ( CoreServiceNotRunning(:final message) => (
type: t.failure.singbox.serviceNotRunning, type: t.failure.singbox.serviceNotRunning,

View File

@@ -1,19 +1,29 @@
import 'package:hiddify/core/locale/locale.dart'; import 'package:dio/dio.dart';
import 'package:hiddify/core/prefs/prefs.dart';
// TODO: rewrite
mixin Failure { mixin Failure {
({String type, String? message}) present(TranslationsEn t); ({String type, String? message}) present(TranslationsEn t);
} }
extension ErrorPresenter on TranslationsEn { extension ErrorPresenter on TranslationsEn {
String printError(Object error) { String? _errorToMessage(Object error) {
if (error case Failure()) { switch (error) {
case Failure():
final err = error.present(this); final err = error.present(this);
return err.type + (err.message == null ? "" : ": ${err.message}"); return err.type + (err.message == null ? "" : ": ${err.message}");
case DioException():
return error.toString();
default:
return null;
} }
return failure.unexpected;
} }
String printError(Object error) =>
_errorToMessage(error) ?? failure.unexpected;
String? mayPrintError(Object? error) =>
error != null ? _errorToMessage(error) : null;
({String type, String? message}) presentError(Object error) { ({String type, String? message}) presentError(Object error) {
if (error case Failure()) return error.present(this); if (error case Failure()) return error.present(this);
return (type: failure.unexpected, message: null); return (type: failure.unexpected, message: null);

View File

@@ -1,5 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hiddify/core/locale/locale.dart'; import 'package:hiddify/core/prefs/prefs.dart';
enum ProfilesSort { enum ProfilesSort {
lastUpdate, lastUpdate,

View File

@@ -1,5 +1,5 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hiddify/core/locale/locale.dart'; import 'package:hiddify/core/prefs/prefs.dart';
import 'package:hiddify/domain/failures.dart'; import 'package:hiddify/domain/failures.dart';
part 'profiles_failure.freezed.dart'; part 'profiles_failure.freezed.dart';
@@ -21,9 +21,9 @@ sealed class ProfileFailure with _$ProfileFailure, Failure {
@override @override
({String type, String? message}) present(TranslationsEn t) { ({String type, String? message}) present(TranslationsEn t) {
return switch (this) { return switch (this) {
ProfileUnexpectedFailure() => ( ProfileUnexpectedFailure(:final error) => (
type: t.failure.profiles.unexpected, type: t.failure.profiles.unexpected,
message: null message: t.mayPrintError(error),
), ),
ProfileNotFoundFailure() => ( ProfileNotFoundFailure() => (
type: t.failure.profiles.notFound, type: t.failure.profiles.notFound,

View File

@@ -1,5 +1,5 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hiddify/core/locale/locale.dart'; import 'package:hiddify/core/prefs/prefs.dart';
import 'package:hiddify/utils/platform_utils.dart'; import 'package:hiddify/utils/platform_utils.dart';
part 'config_options.freezed.dart'; part 'config_options.freezed.dart';

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hiddify/core/core_providers.dart'; import 'package:hiddify/core/core_providers.dart';
import 'package:hiddify/core/locale/locale.dart'; import 'package:hiddify/core/prefs/prefs.dart';
import 'package:hiddify/core/router/routes/routes.dart'; import 'package:hiddify/core/router/routes/routes.dart';
import 'package:hiddify/domain/failures.dart'; import 'package:hiddify/domain/failures.dart';
import 'package:hiddify/domain/profiles/profiles.dart'; import 'package:hiddify/domain/profiles/profiles.dart';

View File

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hiddify/core/core_providers.dart'; import 'package:hiddify/core/core_providers.dart';
import 'package:hiddify/core/theme/theme.dart'; import 'package:hiddify/core/prefs/prefs.dart';
import 'package:hiddify/domain/connectivity/connectivity.dart'; import 'package:hiddify/domain/connectivity/connectivity.dart';
import 'package:hiddify/domain/failures.dart'; import 'package:hiddify/domain/failures.dart';
import 'package:hiddify/features/common/connectivity/connectivity_controller.dart'; import 'package:hiddify/features/common/connectivity/connectivity_controller.dart';
@@ -34,11 +34,13 @@ class ConnectionButton extends HookConsumerWidget {
}, },
); );
final buttonTheme = Theme.of(context).extension<ConnectionButtonTheme>()!;
switch (connectionStatus) { switch (connectionStatus) {
case AsyncData(value: final status): case AsyncData(value: final status):
final Color connectionLogoColor = status.isConnected final Color connectionLogoColor = status.isConnected
? ConnectionButtonColor.connected ? buttonTheme.connectedColor!
: ConnectionButtonColor.disconnected; : buttonTheme.idleColor!;
return _ConnectionButton( return _ConnectionButton(
onTap: () => ref onTap: () => ref
@@ -55,7 +57,7 @@ class ConnectionButton extends HookConsumerWidget {
.toggleConnection(), .toggleConnection(),
enabled: true, enabled: true,
label: const Disconnected().present(t), label: const Disconnected().present(t),
buttonColor: ConnectionButtonColor.disconnected, buttonColor: buttonTheme.idleColor!,
); );
default: default:
// HACK // HACK

View File

@@ -2,9 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_localized_locales/flutter_localized_locales.dart'; import 'package:flutter_localized_locales/flutter_localized_locales.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hiddify/core/core_providers.dart'; import 'package:hiddify/core/core_providers.dart';
import 'package:hiddify/core/locale/locale.dart'; import 'package:hiddify/core/prefs/prefs.dart';
import 'package:hiddify/core/prefs/general_prefs.dart';
import 'package:hiddify/core/theme/theme.dart';
import 'package:hiddify/features/settings/widgets/theme_mode_switch_button.dart'; import 'package:hiddify/features/settings/widgets/theme_mode_switch_button.dart';
import 'package:hiddify/utils/utils.dart'; import 'package:hiddify/utils/utils.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -16,10 +14,9 @@ class GeneralSettingTiles extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final t = ref.watch(translationsProvider); final t = ref.watch(translationsProvider);
final locale = ref.watch(localeControllerProvider); final locale = ref.watch(localeProvider);
final theme = ref.watch(themeControllerProvider); final theme = ref.watch(themeProvider);
final themeController = ref.watch(themeControllerProvider.notifier);
return Column( return Column(
children: [ children: [
@@ -31,12 +28,12 @@ class GeneralSettingTiles extends HookConsumerWidget {
), ),
leading: const Icon(Icons.language), leading: const Icon(Icons.language),
onTap: () async { onTap: () async {
final selectedLocale = await showDialog<LocalePref>( final selectedLocale = await showDialog<AppLocale>(
context: context, context: context,
builder: (context) { builder: (context) {
return SimpleDialog( return SimpleDialog(
title: Text(t.settings.general.locale), title: Text(t.settings.general.locale),
children: LocalePref.values children: AppLocale.values
.map( .map(
(e) => RadioListTile( (e) => RadioListTile(
title: Text( title: Text(
@@ -54,31 +51,27 @@ class GeneralSettingTiles extends HookConsumerWidget {
}, },
); );
if (selectedLocale != null) { if (selectedLocale != null) {
await ref await ref.read(localeProvider.notifier).update(selectedLocale);
.read(localeControllerProvider.notifier)
.change(selectedLocale);
} }
}, },
), ),
ListTile( ListTile(
title: Text(t.settings.general.themeMode), title: Text(t.settings.general.themeMode),
subtitle: Text( subtitle: Text(
switch (theme.themeMode) { switch (theme.mode) {
ThemeMode.system => t.settings.general.themeModes.system, ThemeMode.system => t.settings.general.themeModes.system,
ThemeMode.light => t.settings.general.themeModes.light, ThemeMode.light => t.settings.general.themeModes.light,
ThemeMode.dark => t.settings.general.themeModes.dark, ThemeMode.dark => t.settings.general.themeModes.dark,
}, },
), ),
trailing: ThemeModeSwitch( trailing: ThemeModeSwitch(
themeMode: theme.themeMode, themeMode: theme.mode,
onChanged: (value) { onChanged: ref.read(themeModeProvider.notifier).update,
themeController.change(themeMode: value);
},
), ),
leading: const Icon(Icons.light_mode), leading: const Icon(Icons.light_mode),
onTap: () async { onTap: () async {
await themeController.change( await ref.read(themeModeProvider.notifier).update(
themeMode: Theme.of(context).brightness == Brightness.light Theme.of(context).brightness == Brightness.light
? ThemeMode.dark ? ThemeMode.dark
: ThemeMode.light, : ThemeMode.light,
); );
@@ -87,9 +80,7 @@ class GeneralSettingTiles extends HookConsumerWidget {
SwitchListTile( SwitchListTile(
title: Text(t.settings.general.trueBlack), title: Text(t.settings.general.trueBlack),
value: theme.trueBlack, value: theme.trueBlack,
onChanged: (value) { onChanged: ref.read(trueBlackThemeProvider.notifier).update,
themeController.change(trueBlack: value);
},
), ),
if (PlatformUtils.isDesktop) ...[ if (PlatformUtils.isDesktop) ...[
SwitchListTile( SwitchListTile(

View File

@@ -30,7 +30,12 @@ class CustomAlertDialog extends StatelessWidget {
return AlertDialog( return AlertDialog(
title: title != null ? Text(title!) : null, title: title != null ? Text(title!) : null,
content: Text(message), content: SingleChildScrollView(
child: SizedBox(
width: 468,
child: Text(message),
),
),
actions: [ actions: [
TextButton( TextButton(
onPressed: () { onPressed: () {

View File

@@ -1,14 +1,25 @@
import 'package:hiddify/data/data_providers.dart'; import 'package:hiddify/data/data_providers.dart';
import 'package:hiddify/utils/custom_loggers.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
class PrefNotifier<T> extends AutoDisposeNotifier<T> { class PrefNotifier<T> extends AutoDisposeNotifier<T>
PrefNotifier(this._key, this._defaultValue, this._mapFrom, this._mapTo); with _Prefs<T>, InfraLogger {
PrefNotifier(
this.key,
this.defaultValue,
this.mapFrom,
this.mapTo,
);
final String _key; @override
final T _defaultValue; final String key;
final T Function(String)? _mapFrom; @override
final String Function(T)? _mapTo; final T defaultValue;
@override
final T Function(String)? mapFrom;
@override
final String Function(T)? mapTo;
static AutoDisposeNotifierProvider<PrefNotifier<T>, T> provider<T>( static AutoDisposeNotifierProvider<PrefNotifier<T>, T> provider<T>(
String key, String key,
@@ -16,39 +27,102 @@ class PrefNotifier<T> extends AutoDisposeNotifier<T> {
T Function(String value)? mapFrom, T Function(String value)? mapFrom,
String Function(T value)? mapTo, String Function(T value)? mapTo,
}) => }) =>
NotifierProvider.autoDispose( AutoDisposeNotifierProvider(
() => PrefNotifier(key, defaultValue, mapFrom, mapTo), () => PrefNotifier(key, defaultValue, mapFrom, mapTo),
); );
SharedPreferences get _prefs => ref.read(sharedPreferencesProvider); @override
SharedPreferences get prefs => ref.read(sharedPreferencesProvider);
/// Updates the value asynchronously. @override
Future<void> update(T value) async { Future<void> update(T value) async {
if (_mapTo != null && _mapFrom != null) { super.update(value);
await _prefs.setString(_key, _mapTo!(value));
} else {
switch (value) {
case String _:
await _prefs.setString(_key, value);
case bool _:
await _prefs.setBool(_key, value);
case int _:
await _prefs.setInt(_key, value);
case double _:
await _prefs.setDouble(_key, value);
case List<String> _:
await _prefs.setStringList(_key, value);
}
}
super.state = value; super.state = value;
} }
@override @override
T build() { T build() => getValue();
if (_mapTo != null && _mapFrom != null) { }
final persisted = _prefs.getString(_key);
return persisted != null ? _mapFrom!(persisted) : _defaultValue; class AlwaysAlivePrefNotifier<T> extends Notifier<T>
with _Prefs<T>, InfraLogger {
AlwaysAlivePrefNotifier(
this.key,
this.defaultValue,
this.mapFrom,
this.mapTo,
);
@override
final String key;
@override
final T defaultValue;
@override
final T Function(String)? mapFrom;
@override
final String Function(T)? mapTo;
static NotifierProvider<AlwaysAlivePrefNotifier<T>, T> provider<T>(
String key,
T defaultValue, {
T Function(String value)? mapFrom,
String Function(T value)? mapTo,
}) =>
NotifierProvider(
() => AlwaysAlivePrefNotifier(key, defaultValue, mapFrom, mapTo),
);
@override
SharedPreferences get prefs => ref.read(sharedPreferencesProvider);
@override
Future<void> update(T value) async {
super.update(value);
super.state = value;
}
@override
T build() => getValue();
}
mixin _Prefs<T> implements LoggerMixin {
String get key;
T get defaultValue;
T Function(String)? get mapFrom;
String Function(T)? get mapTo;
SharedPreferences get prefs;
/// Updates the value asynchronously.
Future<void> update(T value) async {
if (mapTo != null && mapFrom != null) {
await prefs.setString(key, mapTo!(value));
} else {
switch (value) {
case String _:
await prefs.setString(key, value);
case bool _:
await prefs.setBool(key, value);
case int _:
await prefs.setInt(key, value);
case double _:
await prefs.setDouble(key, value);
case List<String> _:
await prefs.setStringList(key, value);
}
}
}
T getValue() {
try {
if (mapTo != null && mapFrom != null) {
final persisted = prefs.getString(key);
return persisted != null ? mapFrom!(persisted) : defaultValue;
}
return prefs.get(key) as T? ?? defaultValue;
} catch (e) {
loggy.warning("error getting preference[$key]: $e");
return defaultValue;
} }
return _prefs.get(_key) as T? ?? _defaultValue;
} }
} }