Add quick settings

This commit is contained in:
problematicconsumer
2024-03-07 17:04:32 +03:30
parent 596e085e6e
commit 70123d80a8
7 changed files with 435 additions and 236 deletions

View File

@@ -182,7 +182,6 @@ build-linux-libs:
build-macos-libs: build-macos-libs:
make -C libcore -f Makefile macos-universal make -C libcore -f Makefile macos-universal
mv $(BINDIR)/$(SRV_NAME) $(DESKTOP_OUT)/
build-ios-libs: build-ios-libs:
rf -rf $(IOS_OUT)/Libcore.xcframework rf -rf $(IOS_OUT)/Libcore.xcframework

View File

@@ -228,12 +228,21 @@
"reconnectMsg": "Reconnect for changes to take effect", "reconnectMsg": "Reconnect for changes to take effect",
"reconnectBtn": "Reconnect", "reconnectBtn": "Reconnect",
"serviceMode": "Service Mode", "serviceMode": "Service Mode",
"quickSettings": "Quick Settings",
"setupWarp": "Setup WARP",
"allOptions": "All Config Options",
"serviceModes": { "serviceModes": {
"proxy": "Proxy Service Only", "proxy": "Proxy Service Only",
"systemProxy": "Set System Proxy", "systemProxy": "Set System Proxy",
"tun": "VPN", "tun": "VPN",
"tunService": "VPN Service" "tunService": "VPN Service"
}, },
"shortServiceModes": {
"proxy": "Proxy",
"systemProxy": "System Proxy",
"tun": "VPN",
"tunService": "VPN Service"
},
"section": { "section": {
"route": "Route Options", "route": "Route Options",
"dns": "DNS Options", "dns": "DNS Options",

View File

@@ -3,6 +3,7 @@ import 'package:go_router/go_router.dart';
import 'package:hiddify/core/router/app_router.dart'; import 'package:hiddify/core/router/app_router.dart';
import 'package:hiddify/features/common/adaptive_root_scaffold.dart'; import 'package:hiddify/features/common/adaptive_root_scaffold.dart';
import 'package:hiddify/features/config_option/overview/config_options_page.dart'; import 'package:hiddify/features/config_option/overview/config_options_page.dart';
import 'package:hiddify/features/config_option/widget/quick_settings_modal.dart';
import 'package:hiddify/features/geo_asset/overview/geo_assets_overview_page.dart'; import 'package:hiddify/features/geo_asset/overview/geo_assets_overview_page.dart';
import 'package:hiddify/features/home/widget/home_page.dart'; import 'package:hiddify/features/home/widget/home_page.dart';
import 'package:hiddify/features/intro/widget/intro_page.dart'; import 'package:hiddify/features/intro/widget/intro_page.dart';
@@ -47,6 +48,10 @@ GlobalKey<NavigatorState>? _dynamicRootKey =
path: "config-options", path: "config-options",
name: ConfigOptionsRoute.name, name: ConfigOptionsRoute.name,
), ),
TypedGoRoute<QuickSettingsRoute>(
path: "quick-settings",
name: QuickSettingsRoute.name,
),
TypedGoRoute<SettingsRoute>( TypedGoRoute<SettingsRoute>(
path: "settings", path: "settings",
name: SettingsRoute.name, name: SettingsRoute.name,
@@ -108,6 +113,10 @@ class MobileWrapperRoute extends ShellRouteData {
path: "profiles/:id", path: "profiles/:id",
name: ProfileDetailsRoute.name, name: ProfileDetailsRoute.name,
), ),
TypedGoRoute<QuickSettingsRoute>(
path: "quick-settings",
name: QuickSettingsRoute.name,
),
], ],
), ),
TypedGoRoute<ProxiesRoute>( TypedGoRoute<ProxiesRoute>(
@@ -277,6 +286,22 @@ class LogsOverviewRoute extends GoRouteData {
} }
} }
class QuickSettingsRoute extends GoRouteData {
const QuickSettingsRoute();
static const name = "Quick Settings";
static final GlobalKey<NavigatorState> $parentNavigatorKey = rootNavigatorKey;
@override
Page<void> buildPage(BuildContext context, GoRouterState state) {
return BottomSheetPage(
fixed: true,
name: name,
builder: (controller) => const QuickSettingsModal(),
);
}
}
class SettingsRoute extends GoRouteData { class SettingsRoute extends GoRouteData {
const SettingsRoute(); const SettingsRoute();
static const name = "Settings"; static const name = "Settings";
@@ -296,7 +321,8 @@ class SettingsRoute extends GoRouteData {
} }
class ConfigOptionsRoute extends GoRouteData { class ConfigOptionsRoute extends GoRouteData {
const ConfigOptionsRoute(); const ConfigOptionsRoute({this.section});
final String? section;
static const name = "Config Options"; static const name = "Config Options";
static final GlobalKey<NavigatorState>? $parentNavigatorKey = _dynamicRootKey; static final GlobalKey<NavigatorState>? $parentNavigatorKey = _dynamicRootKey;
@@ -304,12 +330,15 @@ class ConfigOptionsRoute extends GoRouteData {
@override @override
Page<void> buildPage(BuildContext context, GoRouterState state) { Page<void> buildPage(BuildContext context, GoRouterState state) {
if (useMobileRouter) { if (useMobileRouter) {
return const MaterialPage( return MaterialPage(
name: name, name: name,
child: ConfigOptionsPage(), child: ConfigOptionsPage(section: section),
); );
} }
return const NoTransitionPage(name: name, child: ConfigOptionsPage()); return NoTransitionPage(
name: name,
child: ConfigOptionsPage(section: section),
);
} }
} }

View File

@@ -1,5 +1,6 @@
import 'package:dartx/dartx.dart'; import 'package:dartx/dartx.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hiddify/core/localization/translations.dart'; import 'package:hiddify/core/localization/translations.dart';
import 'package:hiddify/core/model/optional_range.dart'; import 'package:hiddify/core/model/optional_range.dart';
@@ -21,12 +22,49 @@ import 'package:hiddify/utils/utils.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:humanizer/humanizer.dart'; import 'package:humanizer/humanizer.dart';
enum ConfigOptionSection {
warp;
static final _warpKey = GlobalKey(debugLabel: "warp-section-key");
GlobalKey get key => switch (this) { _ => _warpKey };
}
class ConfigOptionsPage extends HookConsumerWidget { class ConfigOptionsPage extends HookConsumerWidget {
const ConfigOptionsPage({super.key}); ConfigOptionsPage({super.key, String? section})
: section =
section != null ? ConfigOptionSection.values.byName(section) : null;
final ConfigOptionSection? section;
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final t = ref.watch(translationsProvider); final t = ref.watch(translationsProvider);
final scrollController = useScrollController();
useMemoized(
() {
if (section != null) {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
final box =
section!.key.currentContext?.findRenderObject() as RenderBox?;
final offset = box?.localToGlobal(Offset.zero);
if (offset == null) return;
final height = scrollController.offset +
offset.dy -
MediaQueryData.fromView(View.of(context)).padding.top -
kToolbarHeight;
scrollController.animateTo(
height,
duration: const Duration(milliseconds: 500),
curve: Curves.decelerate,
);
},
);
}
},
);
String experimental(String txt) { String experimental(String txt) {
return "$txt (${t.settings.experimental})"; return "$txt (${t.settings.experimental})";
@@ -34,6 +72,8 @@ class ConfigOptionsPage extends HookConsumerWidget {
return Scaffold( return Scaffold(
body: CustomScrollView( body: CustomScrollView(
controller: scrollController,
shrinkWrap: true,
slivers: [ slivers: [
NestedAppBar( NestedAppBar(
title: Text(t.settings.config.pageTitle), title: Text(t.settings.config.pageTitle),
@@ -101,7 +141,9 @@ class ConfigOptionsPage extends HookConsumerWidget {
), ),
], ],
), ),
SliverList.list( SliverToBoxAdapter(
child: SingleChildScrollView(
child: Column(
children: [ children: [
TipCard(message: t.settings.experimentalMsg), TipCard(message: t.settings.experimentalMsg),
ChoicePreferenceWidget( ChoicePreferenceWidget(
@@ -116,13 +158,15 @@ class ConfigOptionsPage extends HookConsumerWidget {
SwitchListTile( SwitchListTile(
title: Text(experimental(t.settings.config.bypassLan)), title: Text(experimental(t.settings.config.bypassLan)),
value: ref.watch(ConfigOptions.bypassLan), value: ref.watch(ConfigOptions.bypassLan),
onChanged: ref.watch(ConfigOptions.bypassLan.notifier).update, onChanged:
ref.watch(ConfigOptions.bypassLan.notifier).update,
), ),
SwitchListTile( SwitchListTile(
title: Text(t.settings.config.resolveDestination), title: Text(t.settings.config.resolveDestination),
value: ref.watch(ConfigOptions.resolveDestination), value: ref.watch(ConfigOptions.resolveDestination),
onChanged: onChanged: ref
ref.watch(ConfigOptions.resolveDestination.notifier).update, .watch(ConfigOptions.resolveDestination.notifier)
.update,
), ),
ChoicePreferenceWidget( ChoicePreferenceWidget(
selected: ref.watch(ConfigOptions.ipv6Mode), selected: ref.watch(ConfigOptions.ipv6Mode),
@@ -135,26 +179,28 @@ class ConfigOptionsPage extends HookConsumerWidget {
SettingsSection(t.settings.config.section.dns), SettingsSection(t.settings.config.section.dns),
ValuePreferenceWidget( ValuePreferenceWidget(
value: ref.watch(ConfigOptions.remoteDnsAddress), value: ref.watch(ConfigOptions.remoteDnsAddress),
preferences: ref.watch(ConfigOptions.remoteDnsAddress.notifier), preferences:
ref.watch(ConfigOptions.remoteDnsAddress.notifier),
title: t.settings.config.remoteDnsAddress, title: t.settings.config.remoteDnsAddress,
), ),
ChoicePreferenceWidget( ChoicePreferenceWidget(
selected: ref.watch(ConfigOptions.remoteDnsDomainStrategy), selected: ref.watch(ConfigOptions.remoteDnsDomainStrategy),
preferences: preferences: ref
ref.watch(ConfigOptions.remoteDnsDomainStrategy.notifier), .watch(ConfigOptions.remoteDnsDomainStrategy.notifier),
choices: DomainStrategy.values, choices: DomainStrategy.values,
title: t.settings.config.remoteDnsDomainStrategy, title: t.settings.config.remoteDnsDomainStrategy,
presentChoice: (value) => value.displayName, presentChoice: (value) => value.displayName,
), ),
ValuePreferenceWidget( ValuePreferenceWidget(
value: ref.watch(ConfigOptions.directDnsAddress), value: ref.watch(ConfigOptions.directDnsAddress),
preferences: ref.watch(ConfigOptions.directDnsAddress.notifier), preferences:
ref.watch(ConfigOptions.directDnsAddress.notifier),
title: t.settings.config.directDnsAddress, title: t.settings.config.directDnsAddress,
), ),
ChoicePreferenceWidget( ChoicePreferenceWidget(
selected: ref.watch(ConfigOptions.directDnsDomainStrategy), selected: ref.watch(ConfigOptions.directDnsDomainStrategy),
preferences: preferences: ref
ref.watch(ConfigOptions.directDnsDomainStrategy.notifier), .watch(ConfigOptions.directDnsDomainStrategy.notifier),
choices: DomainStrategy.values, choices: DomainStrategy.values,
title: t.settings.config.directDnsDomainStrategy, title: t.settings.config.directDnsDomainStrategy,
presentChoice: (value) => value.displayName, presentChoice: (value) => value.displayName,
@@ -162,15 +208,17 @@ class ConfigOptionsPage extends HookConsumerWidget {
SwitchListTile( SwitchListTile(
title: Text(t.settings.config.enableDnsRouting), title: Text(t.settings.config.enableDnsRouting),
value: ref.watch(ConfigOptions.enableDnsRouting), value: ref.watch(ConfigOptions.enableDnsRouting),
onChanged: onChanged: ref
ref.watch(ConfigOptions.enableDnsRouting.notifier).update, .watch(ConfigOptions.enableDnsRouting.notifier)
.update,
), ),
const SettingsDivider(), const SettingsDivider(),
SettingsSection(experimental(t.settings.config.section.mux)), SettingsSection(experimental(t.settings.config.section.mux)),
SwitchListTile( SwitchListTile(
title: Text(t.settings.config.enableMux), title: Text(t.settings.config.enableMux),
value: ref.watch(ConfigOptions.enableMux), value: ref.watch(ConfigOptions.enableMux),
onChanged: ref.watch(ConfigOptions.enableMux.notifier).update, onChanged:
ref.watch(ConfigOptions.enableMux.notifier).update,
), ),
ChoicePreferenceWidget( ChoicePreferenceWidget(
selected: ref.watch(ConfigOptions.muxProtocol), selected: ref.watch(ConfigOptions.muxProtocol),
@@ -181,7 +229,8 @@ class ConfigOptionsPage extends HookConsumerWidget {
), ),
ValuePreferenceWidget( ValuePreferenceWidget(
value: ref.watch(ConfigOptions.muxMaxStreams), value: ref.watch(ConfigOptions.muxMaxStreams),
preferences: ref.watch(ConfigOptions.muxMaxStreams.notifier), preferences:
ref.watch(ConfigOptions.muxMaxStreams.notifier),
title: t.settings.config.muxMaxStreams, title: t.settings.config.muxMaxStreams,
inputToValue: int.tryParse, inputToValue: int.tryParse,
digitsOnly: true, digitsOnly: true,
@@ -198,7 +247,8 @@ class ConfigOptionsPage extends HookConsumerWidget {
SwitchListTile( SwitchListTile(
title: Text(t.settings.config.strictRoute), title: Text(t.settings.config.strictRoute),
value: ref.watch(ConfigOptions.strictRoute), value: ref.watch(ConfigOptions.strictRoute),
onChanged: ref.watch(ConfigOptions.strictRoute.notifier).update, onChanged:
ref.watch(ConfigOptions.strictRoute.notifier).update,
), ),
ChoicePreferenceWidget( ChoicePreferenceWidget(
selected: ref.watch(ConfigOptions.tunImplementation), selected: ref.watch(ConfigOptions.tunImplementation),
@@ -236,14 +286,17 @@ class ConfigOptionsPage extends HookConsumerWidget {
const SettingsDivider(), const SettingsDivider(),
SettingsSection(t.settings.config.section.tlsTricks), SettingsSection(t.settings.config.section.tlsTricks),
SwitchListTile( SwitchListTile(
title: Text(experimental(t.settings.config.enableTlsFragment)), title:
Text(experimental(t.settings.config.enableTlsFragment)),
value: ref.watch(ConfigOptions.enableTlsFragment), value: ref.watch(ConfigOptions.enableTlsFragment),
onChanged: onChanged: ref
ref.watch(ConfigOptions.enableTlsFragment.notifier).update, .watch(ConfigOptions.enableTlsFragment.notifier)
.update,
), ),
ValuePreferenceWidget( ValuePreferenceWidget(
value: ref.watch(ConfigOptions.tlsFragmentSize), value: ref.watch(ConfigOptions.tlsFragmentSize),
preferences: ref.watch(ConfigOptions.tlsFragmentSize.notifier), preferences:
ref.watch(ConfigOptions.tlsFragmentSize.notifier),
title: t.settings.config.tlsFragmentSize, title: t.settings.config.tlsFragmentSize,
inputToValue: OptionalRange.tryParse, inputToValue: OptionalRange.tryParse,
presentValue: (value) => value.present(t), presentValue: (value) => value.present(t),
@@ -251,7 +304,8 @@ class ConfigOptionsPage extends HookConsumerWidget {
), ),
ValuePreferenceWidget( ValuePreferenceWidget(
value: ref.watch(ConfigOptions.tlsFragmentSleep), value: ref.watch(ConfigOptions.tlsFragmentSleep),
preferences: ref.watch(ConfigOptions.tlsFragmentSleep.notifier), preferences:
ref.watch(ConfigOptions.tlsFragmentSleep.notifier),
title: t.settings.config.tlsFragmentSleep, title: t.settings.config.tlsFragmentSleep,
inputToValue: OptionalRange.tryParse, inputToValue: OptionalRange.tryParse,
presentValue: (value) => value.present(t), presentValue: (value) => value.present(t),
@@ -267,14 +321,17 @@ class ConfigOptionsPage extends HookConsumerWidget {
.update, .update,
), ),
SwitchListTile( SwitchListTile(
title: Text(experimental(t.settings.config.enableTlsPadding)), title:
Text(experimental(t.settings.config.enableTlsPadding)),
value: ref.watch(ConfigOptions.enableTlsPadding), value: ref.watch(ConfigOptions.enableTlsPadding),
onChanged: onChanged: ref
ref.watch(ConfigOptions.enableTlsPadding.notifier).update, .watch(ConfigOptions.enableTlsPadding.notifier)
.update,
), ),
ValuePreferenceWidget( ValuePreferenceWidget(
value: ref.watch(ConfigOptions.tlsPaddingSize), value: ref.watch(ConfigOptions.tlsPaddingSize),
preferences: ref.watch(ConfigOptions.tlsPaddingSize.notifier), preferences:
ref.watch(ConfigOptions.tlsPaddingSize.notifier),
title: t.settings.config.tlsPaddingSize, title: t.settings.config.tlsPaddingSize,
inputToValue: OptionalRange.tryParse, inputToValue: OptionalRange.tryParse,
presentValue: (value) => value.format(), presentValue: (value) => value.format(),
@@ -282,7 +339,7 @@ class ConfigOptionsPage extends HookConsumerWidget {
), ),
const SettingsDivider(), const SettingsDivider(),
SettingsSection(experimental(t.settings.config.section.warp)), SettingsSection(experimental(t.settings.config.section.warp)),
const WarpOptionsTiles(), WarpOptionsTiles(key: ConfigOptionSection._warpKey),
const SettingsDivider(), const SettingsDivider(),
SettingsSection(t.settings.config.section.misc), SettingsSection(t.settings.config.section.misc),
ValuePreferenceWidget( ValuePreferenceWidget(
@@ -306,8 +363,9 @@ class ConfigOptionsPage extends HookConsumerWidget {
.inMinutes .inMinutes
.coerceIn(0, 60) .coerceIn(0, 60)
.toDouble(), .toDouble(),
onReset: onReset: ref
ref.read(ConfigOptions.urlTestInterval.notifier).reset, .read(ConfigOptions.urlTestInterval.notifier)
.reset,
min: 1, min: 1,
max: 60, max: 60,
divisions: 60, divisions: 60,
@@ -331,6 +389,8 @@ class ConfigOptionsPage extends HookConsumerWidget {
const Gap(24), const Gap(24),
], ],
), ),
),
),
], ],
), ),
); );

View File

@@ -0,0 +1,84 @@
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:hiddify/core/localization/translations.dart';
import 'package:hiddify/core/router/router.dart';
import 'package:hiddify/features/config_option/data/config_option_repository.dart';
import 'package:hiddify/features/config_option/notifier/warp_option_notifier.dart';
import 'package:hiddify/features/config_option/overview/config_options_page.dart';
import 'package:hiddify/singbox/model/singbox_config_enum.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class QuickSettingsModal extends HookConsumerWidget {
const QuickSettingsModal({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final t = ref.watch(translationsProvider);
final warpPrefaceCompleted =
ref.watch(warpOptionNotifierProvider).consentGiven;
return SingleChildScrollView(
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: SegmentedButton(
segments: ServiceMode.choices
.map(
(e) => ButtonSegment(
value: e,
label: Text(
e.presentShort(t),
overflow: TextOverflow.ellipsis,
),
tooltip:
e.isExperimental ? t.settings.experimental : null,
),
)
.toList(),
selected: {ref.watch(ConfigOptions.serviceMode)},
onSelectionChanged: (newSet) => ref
.read(ConfigOptions.serviceMode.notifier)
.update(newSet.first),
),
),
const Gap(8),
if (warpPrefaceCompleted)
SwitchListTile(
value: ref.watch(ConfigOptions.enableWarp),
onChanged: ref.watch(ConfigOptions.enableWarp.notifier).update,
title: Text(t.settings.config.enableWarp),
)
else
ListTile(
title: Text(t.settings.config.setupWarp),
trailing: const Icon(FluentIcons.chevron_right_24_regular),
onTap: () =>
ConfigOptionsRoute(section: ConfigOptionSection.warp.name)
.go(context),
),
SwitchListTile(
value: ref.watch(ConfigOptions.enableTlsFragment),
onChanged:
ref.watch(ConfigOptions.enableTlsFragment.notifier).update,
title: Text(t.settings.config.enableTlsFragment),
),
SwitchListTile(
value: ref.watch(ConfigOptions.enableMux),
onChanged: ref.watch(ConfigOptions.enableMux.notifier).update,
title: Text(t.settings.config.enableMux),
),
ListTile(
title: Text(t.settings.config.allOptions),
trailing: const Icon(FluentIcons.chevron_right_24_regular),
dense: true,
onTap: () => const ConfigOptionsRoute().go(context),
),
const Gap(16),
],
),
);
}
}

View File

@@ -45,6 +45,11 @@ class HomePage extends HookConsumerWidget {
), ),
), ),
actions: [ actions: [
IconButton(
onPressed: () => const QuickSettingsRoute().push(context),
icon: const Icon(FluentIcons.options_24_filled),
tooltip: t.settings.config.quickSettings,
),
IconButton( IconButton(
onPressed: () => const AddProfileRoute().push(context), onPressed: () => const AddProfileRoute().push(context),
icon: const Icon(FluentIcons.add_circle_24_filled), icon: const Icon(FluentIcons.add_circle_24_filled),

View File

@@ -29,13 +29,26 @@ enum ServiceMode {
return [proxy, tun]; return [proxy, tun];
} }
bool get isExperimental => switch (this) {
tun => PlatformUtils.isDesktop,
tunService => PlatformUtils.isDesktop,
_ => false,
};
String present(TranslationsEn t) => switch (this) { String present(TranslationsEn t) => switch (this) {
proxy => t.settings.config.serviceModes.proxy, proxy => t.settings.config.serviceModes.proxy,
systemProxy => t.settings.config.serviceModes.systemProxy, systemProxy => t.settings.config.serviceModes.systemProxy,
tun => tun =>
"${t.settings.config.serviceModes.tun}${PlatformUtils.isDesktop ? " (${t.settings.experimental})" : ""}", "${t.settings.config.serviceModes.tun}${isExperimental ? " (${t.settings.experimental})" : ""}",
tunService => tunService =>
"${t.settings.config.serviceModes.tunService}${PlatformUtils.isDesktop ? " (${t.settings.experimental})" : ""}", "${t.settings.config.serviceModes.tunService}${isExperimental ? " (${t.settings.experimental})" : ""}",
};
String presentShort(TranslationsEn t) => switch (this) {
proxy => t.settings.config.shortServiceModes.proxy,
systemProxy => t.settings.config.shortServiceModes.systemProxy,
tun => t.settings.config.shortServiceModes.tun,
tunService => t.settings.config.shortServiceModes.tunService,
}; };
} }