Refactor preferences
This commit is contained in:
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
export 'locale_controller.dart';
|
|
||||||
export 'locale_pref.dart';
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
44
lib/core/prefs/locale_prefs.dart
Normal file
44
lib/core/prefs/locale_prefs.dart
Normal 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 : "";
|
||||||
|
}
|
||||||
@@ -1 +1,4 @@
|
|||||||
|
export 'app_theme.dart';
|
||||||
export 'general_prefs.dart';
|
export 'general_prefs.dart';
|
||||||
|
export 'locale_prefs.dart';
|
||||||
|
export 'theme_prefs.dart';
|
||||||
|
|||||||
14
lib/core/prefs/theme_prefs.dart
Normal file
14
lib/core/prefs/theme_prefs.dart
Normal 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,
|
||||||
|
);
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export 'app_theme.dart';
|
|
||||||
export 'constants.dart';
|
|
||||||
export 'theme_controller.dart';
|
|
||||||
export 'theme_prefs.dart';
|
|
||||||
@@ -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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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';
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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: () {
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user