This commit is contained in:
problematicconsumer
2023-07-06 17:18:41 +03:30
commit b617c95f62
352 changed files with 21017 additions and 0 deletions

1
lib/core/app/app.dart Normal file
View File

@@ -0,0 +1 @@
export 'app_view.dart';

View File

@@ -0,0 +1,31 @@
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:hiddify/core/locale/locale.dart';
import 'package:hiddify/core/router/router.dart';
import 'package:hiddify/core/theme/theme.dart';
import 'package:hiddify/utils/utils.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class AppView extends HookConsumerWidget with PresLogger {
const AppView({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final router = ref.watch(routerProvider);
final locale = ref.watch(localeControllerProvider).locale;
final theme = ref.watch(themeControllerProvider);
return MaterialApp.router(
routerConfig: router,
locale: locale,
supportedLocales: LocalePref.locales,
localizationsDelegates: GlobalMaterialLocalizations.delegates,
debugShowCheckedModeBanner: false,
themeMode: theme.themeMode,
theme: theme.light,
darkTheme: theme.dark,
title: 'Hiddify',
).animate().fadeIn();
}
}

View File

@@ -0,0 +1,8 @@
import 'package:hiddify/core/locale/locale.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'core_providers.g.dart';
@Riverpod(keepAlive: true)
TranslationsEn translations(TranslationsRef ref) =>
ref.watch(localeControllerProvider).translations();

View File

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

View File

@@ -0,0 +1,24 @@
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

@@ -0,0 +1,32 @@
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;
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

@@ -0,0 +1,2 @@
export 'prefs_controller.dart';
export 'prefs_state.dart';

View File

@@ -0,0 +1,58 @@
import 'dart:convert';
import 'package:hiddify/core/prefs/prefs_state.dart';
import 'package:hiddify/data/data_providers.dart';
import 'package:hiddify/domain/clash/clash.dart';
import 'package:hiddify/domain/connectivity/connectivity.dart';
import 'package:hiddify/utils/utils.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';
part 'prefs_controller.g.dart';
@Riverpod(keepAlive: true)
class PrefsController extends _$PrefsController with AppLogger {
@override
PrefsState build() {
return PrefsState(
clash: _getClashPrefs(),
network: _getNetworkPrefs(),
);
}
SharedPreferences get _prefs => ref.read(sharedPreferencesProvider);
static const _overridesKey = "clash_overrides";
static const _networkKey = "clash_overrides";
ClashConfig _getClashPrefs() {
final persisted = _prefs.getString(_overridesKey);
if (persisted == null) return ClashConfig.initial;
return ClashConfig.fromJson(jsonDecode(persisted) as Map<String, dynamic>);
}
NetworkPrefs _getNetworkPrefs() {
final persisted = _prefs.getString(_networkKey);
if (persisted == null) return const NetworkPrefs();
return NetworkPrefs.fromJson(jsonDecode(persisted) as Map<String, dynamic>);
}
Future<void> patchClashOverrides(ClashConfigPatch overrides) async {
final newPrefs = state.clash.patch(overrides);
await _prefs.setString(_overridesKey, jsonEncode(newPrefs.toJson()));
state = state.copyWith(clash: newPrefs);
}
Future<void> patchNetworkPrefs({
bool? systemProxy,
bool? bypassPrivateNetworks,
}) async {
final newPrefs = state.network.copyWith(
systemProxy: systemProxy ?? state.network.systemProxy,
bypassPrivateNetworks:
bypassPrivateNetworks ?? state.network.bypassPrivateNetworks,
);
await _prefs.setString(_networkKey, jsonEncode(newPrefs.toJson()));
state = state.copyWith(network: newPrefs);
}
}

View File

@@ -0,0 +1,15 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hiddify/domain/clash/clash.dart';
import 'package:hiddify/domain/connectivity/connectivity.dart';
part 'prefs_state.freezed.dart';
@freezed
class PrefsState with _$PrefsState {
const PrefsState._();
const factory PrefsState({
@Default(ClashConfig()) ClashConfig clash,
@Default(NetworkPrefs()) NetworkPrefs network,
}) = _PrefsState;
}

View File

@@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hiddify/core/router/routes/routes.dart';
import 'package:hiddify/services/deep_link_service.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'app_router.g.dart';
// TODO: test and improve handling of deep link
@riverpod
GoRouter router(RouterRef ref) {
final deepLink = ref.listen(
deepLinkServiceProvider,
(_, next) async {
if (next case AsyncData(value: final link?)) {
await ref.state.push(
NewProfileRoute(url: link.url, name: link.name).location,
);
}
},
);
final initialLink = deepLink.read();
String initialLocation = HomeRoute.path;
if (initialLink case AsyncData(value: final link?)) {
initialLocation = NewProfileRoute(url: link.url, name: link.name).location;
}
return GoRouter(
navigatorKey: rootNavigatorKey,
initialLocation: initialLocation,
debugLogDiagnostics: true,
routes: $routes,
);
}
int getCurrentIndex(BuildContext context) {
final String location = GoRouterState.of(context).location;
if (location == HomeRoute.path) return 0;
if (location.startsWith(ProxiesRoute.path)) return 1;
if (location.startsWith(LogsRoute.path)) return 2;
if (location.startsWith(SettingsRoute.path)) return 3;
return 0;
}
void switchTab(int index, BuildContext context) {
switch (index) {
case 0:
const HomeRoute().go(context);
case 1:
const ProxiesRoute().go(context);
case 2:
const LogsRoute().go(context);
case 3:
const SettingsRoute().go(context);
}
}

View File

@@ -0,0 +1,2 @@
export 'app_router.dart';
export 'routes/routes.dart';

View File

@@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hiddify/core/router/routes/shared_routes.dart';
import 'package:hiddify/features/logs/view/view.dart';
import 'package:hiddify/features/settings/view/view.dart';
import 'package:hiddify/features/wrapper/wrapper.dart';
part 'desktop_routes.g.dart';
@TypedShellRoute<DesktopWrapperRoute>(
routes: [
TypedGoRoute<HomeRoute>(
path: HomeRoute.path,
routes: [
TypedGoRoute<ProfilesRoute>(path: ProfilesRoute.path),
TypedGoRoute<NewProfileRoute>(path: NewProfileRoute.path),
TypedGoRoute<ProfileDetailsRoute>(path: ProfileDetailsRoute.path),
],
),
TypedGoRoute<ProxiesRoute>(path: ProxiesRoute.path),
TypedGoRoute<LogsRoute>(path: LogsRoute.path),
TypedGoRoute<SettingsRoute>(path: SettingsRoute.path),
],
)
class DesktopWrapperRoute extends ShellRouteData {
const DesktopWrapperRoute();
@override
Widget builder(BuildContext context, GoRouterState state, Widget navigator) {
return DesktopWrapper(navigator);
}
}
class LogsRoute extends GoRouteData {
const LogsRoute();
static const path = '/logs';
@override
Page<void> buildPage(BuildContext context, GoRouterState state) {
return const NoTransitionPage(child: LogsPage());
}
}
class SettingsRoute extends GoRouteData {
const SettingsRoute();
static const path = '/settings';
@override
Page<void> buildPage(BuildContext context, GoRouterState state) {
return const NoTransitionPage(child: SettingsPage());
}
}

View File

@@ -0,0 +1,62 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hiddify/core/router/routes/shared_routes.dart';
import 'package:hiddify/features/logs/view/view.dart';
import 'package:hiddify/features/settings/view/view.dart';
import 'package:hiddify/features/wrapper/wrapper.dart';
part 'mobile_routes.g.dart';
@TypedShellRoute<MobileWrapperRoute>(
routes: [
TypedGoRoute<HomeRoute>(
path: HomeRoute.path,
routes: [
TypedGoRoute<ProfilesRoute>(path: ProfilesRoute.path),
TypedGoRoute<NewProfileRoute>(path: NewProfileRoute.path),
TypedGoRoute<ProfileDetailsRoute>(path: ProfileDetailsRoute.path),
],
),
TypedGoRoute<ProxiesRoute>(path: ProxiesRoute.path),
],
)
class MobileWrapperRoute extends ShellRouteData {
const MobileWrapperRoute();
@override
Widget builder(BuildContext context, GoRouterState state, Widget navigator) {
return MobileWrapper(navigator);
}
}
@TypedGoRoute<LogsRoute>(path: LogsRoute.path)
class LogsRoute extends GoRouteData {
const LogsRoute();
static const path = '/logs';
static final GlobalKey<NavigatorState> $parentNavigatorKey = rootNavigatorKey;
@override
Page<void> buildPage(BuildContext context, GoRouterState state) {
return const MaterialPage(
fullscreenDialog: true,
child: LogsPage(),
);
}
}
@TypedGoRoute<SettingsRoute>(path: SettingsRoute.path)
class SettingsRoute extends GoRouteData {
const SettingsRoute();
static const path = '/settings';
static final GlobalKey<NavigatorState> $parentNavigatorKey = rootNavigatorKey;
@override
Page<void> buildPage(BuildContext context, GoRouterState state) {
return const MaterialPage(
fullscreenDialog: true,
child: SettingsPage(),
);
}
}

View File

@@ -0,0 +1,16 @@
import 'package:go_router/go_router.dart';
import 'package:hiddify/core/router/routes/desktop_routes.dart' as desktop;
import 'package:hiddify/core/router/routes/mobile_routes.dart' as mobile;
import 'package:hiddify/core/router/routes/shared_routes.dart' as shared;
import 'package:hiddify/utils/utils.dart';
export 'mobile_routes.dart';
export 'shared_routes.dart' hide $appRoutes;
List<RouteBase> get $routes => [
if (PlatformUtils.isDesktop)
...desktop.$appRoutes
else
...mobile.$appRoutes,
...shared.$appRoutes,
];

View File

@@ -0,0 +1,101 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hiddify/features/common/common.dart';
import 'package:hiddify/features/home/view/view.dart';
import 'package:hiddify/features/profile_detail/view/view.dart';
import 'package:hiddify/features/profiles/view/view.dart';
import 'package:hiddify/features/proxies/view/view.dart';
import 'package:hiddify/utils/utils.dart';
part 'shared_routes.g.dart';
List<RouteBase> get $sharedRoutes => $appRoutes;
final GlobalKey<NavigatorState> rootNavigatorKey = GlobalKey<NavigatorState>();
class HomeRoute extends GoRouteData {
const HomeRoute();
static const path = '/';
@override
Page<void> buildPage(BuildContext context, GoRouterState state) {
return const NoTransitionPage(child: HomePage());
}
}
class ProxiesRoute extends GoRouteData {
const ProxiesRoute();
static const path = '/proxies';
@override
Page<void> buildPage(BuildContext context, GoRouterState state) {
return const NoTransitionPage(child: ProxiesPage());
}
}
@TypedGoRoute<AddProfileRoute>(path: AddProfileRoute.path)
class AddProfileRoute extends GoRouteData {
const AddProfileRoute();
static const path = '/add';
static final GlobalKey<NavigatorState> $parentNavigatorKey = rootNavigatorKey;
@override
Page<void> buildPage(BuildContext context, GoRouterState state) {
return BottomSheetPage(
fixed: true,
builder: (controller) => AddProfileModal(scrollController: controller),
);
}
}
class ProfilesRoute extends GoRouteData {
const ProfilesRoute();
static const path = 'profiles';
static final GlobalKey<NavigatorState> $parentNavigatorKey = rootNavigatorKey;
@override
Page<void> buildPage(BuildContext context, GoRouterState state) {
return BottomSheetPage(
builder: (controller) => ProfilesModal(scrollController: controller),
);
}
}
class NewProfileRoute extends GoRouteData {
const NewProfileRoute({this.url, this.name});
static const path = 'profiles/new';
final String? url;
final String? name;
static final GlobalKey<NavigatorState> $parentNavigatorKey = rootNavigatorKey;
@override
Page<void> buildPage(BuildContext context, GoRouterState state) {
return MaterialPage(
fullscreenDialog: true,
child: ProfileDetailPage(
"new",
url: url,
name: name,
),
);
}
}
class ProfileDetailsRoute extends GoRouteData {
const ProfileDetailsRoute(this.id);
final String id;
static const path = 'profiles/:id';
static final GlobalKey<NavigatorState> $parentNavigatorKey = rootNavigatorKey;
@override
Page<void> buildPage(BuildContext context, GoRouterState state) {
return MaterialPage(
fullscreenDialog: true,
child: ProfileDetailPage(id),
);
}
}

View File

@@ -0,0 +1,123 @@
import 'package:flex_color_scheme/flex_color_scheme.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
extension AppTheme on ThemePrefs {
ThemeData get light {
return FlexThemeData.light(
scheme: FlexScheme.indigoM3,
surfaceMode: FlexSurfaceMode.highScaffoldLowSurface,
useMaterial3: true,
swapLegacyOnMaterial3: true,
useMaterial3ErrorColors: true,
blendLevel: 1,
subThemesData: const FlexSubThemesData(
useTextTheme: true,
useM2StyleDividerInM3: true,
elevatedButtonSchemeColor: SchemeColor.onPrimaryContainer,
elevatedButtonSecondarySchemeColor: SchemeColor.primaryContainer,
outlinedButtonOutlineSchemeColor: SchemeColor.primary,
toggleButtonsBorderSchemeColor: SchemeColor.primary,
segmentedButtonSchemeColor: SchemeColor.primary,
segmentedButtonBorderSchemeColor: SchemeColor.primary,
unselectedToggleIsColored: true,
sliderValueTinted: true,
inputDecoratorSchemeColor: SchemeColor.primary,
inputDecoratorBackgroundAlpha: 43,
inputDecoratorUnfocusedHasBorder: false,
inputDecoratorFocusedBorderWidth: 1.0,
inputDecoratorPrefixIconSchemeColor: SchemeColor.primary,
popupMenuRadius: 8.0,
popupMenuElevation: 3.0,
drawerIndicatorSchemeColor: SchemeColor.primary,
bottomNavigationBarMutedUnselectedLabel: false,
bottomNavigationBarMutedUnselectedIcon: false,
menuRadius: 8.0,
menuElevation: 3.0,
menuBarRadius: 0.0,
menuBarElevation: 2.0,
menuBarShadowColor: Color(0x00000000),
navigationBarSelectedLabelSchemeColor: SchemeColor.primary,
navigationBarMutedUnselectedLabel: false,
navigationBarSelectedIconSchemeColor: SchemeColor.onPrimary,
navigationBarMutedUnselectedIcon: false,
navigationBarIndicatorSchemeColor: SchemeColor.primary,
navigationBarIndicatorOpacity: 1.00,
navigationRailSelectedLabelSchemeColor: SchemeColor.primary,
navigationRailMutedUnselectedLabel: false,
navigationRailSelectedIconSchemeColor: SchemeColor.onPrimary,
navigationRailMutedUnselectedIcon: false,
navigationRailIndicatorSchemeColor: SchemeColor.primary,
navigationRailIndicatorOpacity: 1.00,
navigationRailBackgroundSchemeColor: SchemeColor.surface,
),
keyColors: const FlexKeyColors(
useSecondary: true,
useTertiary: true,
keepPrimary: true,
),
tones: FlexTones.jolly(Brightness.light),
visualDensity: FlexColorScheme.comfortablePlatformDensity,
);
}
ThemeData get dark {
return FlexThemeData.dark(
scheme: FlexScheme.indigoM3,
useMaterial3: true,
swapLegacyOnMaterial3: true,
useMaterial3ErrorColors: true,
darkIsTrueBlack: trueBlack,
surfaceMode: FlexSurfaceMode.highScaffoldLowSurface,
// blendLevel: 1,
subThemesData: const FlexSubThemesData(
blendTextTheme: true,
useTextTheme: true,
useM2StyleDividerInM3: true,
elevatedButtonSchemeColor: SchemeColor.onPrimaryContainer,
elevatedButtonSecondarySchemeColor: SchemeColor.primaryContainer,
outlinedButtonOutlineSchemeColor: SchemeColor.primary,
toggleButtonsBorderSchemeColor: SchemeColor.primary,
segmentedButtonSchemeColor: SchemeColor.primary,
segmentedButtonBorderSchemeColor: SchemeColor.primary,
unselectedToggleIsColored: true,
sliderValueTinted: true,
inputDecoratorSchemeColor: SchemeColor.primary,
inputDecoratorBackgroundAlpha: 43,
inputDecoratorUnfocusedHasBorder: false,
inputDecoratorFocusedBorderWidth: 1.0,
inputDecoratorPrefixIconSchemeColor: SchemeColor.primary,
popupMenuRadius: 8.0,
popupMenuElevation: 3.0,
drawerIndicatorSchemeColor: SchemeColor.primary,
bottomNavigationBarMutedUnselectedLabel: false,
bottomNavigationBarMutedUnselectedIcon: false,
menuRadius: 8.0,
menuElevation: 3.0,
menuBarRadius: 0.0,
menuBarElevation: 2.0,
menuBarShadowColor: Color(0x00000000),
navigationBarSelectedLabelSchemeColor: SchemeColor.primary,
navigationBarMutedUnselectedLabel: false,
navigationBarSelectedIconSchemeColor: SchemeColor.onPrimary,
navigationBarMutedUnselectedIcon: false,
navigationBarIndicatorSchemeColor: SchemeColor.primary,
navigationBarIndicatorOpacity: 1.00,
navigationRailSelectedLabelSchemeColor: SchemeColor.primary,
navigationRailMutedUnselectedLabel: false,
navigationRailSelectedIconSchemeColor: SchemeColor.onPrimary,
navigationRailMutedUnselectedIcon: false,
navigationRailIndicatorSchemeColor: SchemeColor.primary,
navigationRailIndicatorOpacity: 1.00,
navigationRailBackgroundSchemeColor: SchemeColor.surface,
),
keyColors: const FlexKeyColors(
useSecondary: true,
useTertiary: true,
),
// tones: FlexTones.jolly(Brightness.dark),
visualDensity: FlexColorScheme.comfortablePlatformDensity,
);
}
}

View File

@@ -0,0 +1,6 @@
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

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

View File

@@ -0,0 +1,41 @@
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

@@ -0,0 +1,14 @@
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;
}