initial
This commit is contained in:
60
lib/features/settings/view/settings_page.dart
Normal file
60
lib/features/settings/view/settings_page.dart
Normal file
@@ -0,0 +1,60 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hiddify/core/core_providers.dart';
|
||||
import 'package:hiddify/features/settings/widgets/widgets.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:recase/recase.dart';
|
||||
|
||||
class SettingsPage extends HookConsumerWidget {
|
||||
const SettingsPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final t = ref.watch(translationsProvider);
|
||||
|
||||
const divider = Divider(indent: 16, endIndent: 16);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(t.settings.pageTitle.titleCase),
|
||||
),
|
||||
body: ListTileTheme(
|
||||
data: ListTileTheme.of(context).copyWith(
|
||||
contentPadding: const EdgeInsetsDirectional.only(start: 48, end: 16),
|
||||
),
|
||||
child: ListView(
|
||||
children: [
|
||||
_SettingsSectionHeader(
|
||||
t.settings.appearance.sectionTitle.titleCase,
|
||||
),
|
||||
const AppearanceSettingTiles(),
|
||||
divider,
|
||||
_SettingsSectionHeader(t.settings.network.sectionTitle.titleCase),
|
||||
const NetworkSettingTiles(),
|
||||
divider,
|
||||
_SettingsSectionHeader(t.settings.clash.sectionTitle.titleCase),
|
||||
const ClashSettingTiles(),
|
||||
const Gap(16),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SettingsSectionHeader extends StatelessWidget {
|
||||
const _SettingsSectionHeader(this.title);
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
1
lib/features/settings/view/view.dart
Normal file
1
lib/features/settings/view/view.dart
Normal file
@@ -0,0 +1 @@
|
||||
export 'settings_page.dart';
|
||||
54
lib/features/settings/widgets/appearance_setting_tiles.dart
Normal file
54
lib/features/settings/widgets/appearance_setting_tiles.dart
Normal file
@@ -0,0 +1,54 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hiddify/core/core_providers.dart';
|
||||
import 'package:hiddify/core/theme/theme.dart';
|
||||
import 'package:hiddify/features/settings/widgets/theme_mode_switch_button.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:recase/recase.dart';
|
||||
|
||||
class AppearanceSettingTiles extends HookConsumerWidget {
|
||||
const AppearanceSettingTiles({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final t = ref.watch(translationsProvider);
|
||||
|
||||
final theme = ref.watch(themeControllerProvider);
|
||||
final themeController = ref.watch(themeControllerProvider.notifier);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(t.settings.appearance.themeMode.titleCase),
|
||||
subtitle: Text(
|
||||
switch (theme.themeMode) {
|
||||
ThemeMode.system => t.settings.appearance.themeModes.system,
|
||||
ThemeMode.light => t.settings.appearance.themeModes.light,
|
||||
ThemeMode.dark => t.settings.appearance.themeModes.dark,
|
||||
}
|
||||
.sentenceCase,
|
||||
),
|
||||
trailing: ThemeModeSwitch(
|
||||
themeMode: theme.themeMode,
|
||||
onChanged: (value) {
|
||||
themeController.change(themeMode: value);
|
||||
},
|
||||
),
|
||||
onTap: () async {
|
||||
await themeController.change(
|
||||
themeMode: Theme.of(context).brightness == Brightness.light
|
||||
? ThemeMode.dark
|
||||
: ThemeMode.light,
|
||||
);
|
||||
},
|
||||
),
|
||||
SwitchListTile(
|
||||
title: Text(t.settings.appearance.trueBlack.titleCase),
|
||||
value: theme.trueBlack,
|
||||
onChanged: (value) {
|
||||
themeController.change(trueBlack: value);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
241
lib/features/settings/widgets/clash_setting_tiles.dart
Normal file
241
lib/features/settings/widgets/clash_setting_tiles.dart
Normal file
@@ -0,0 +1,241 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import 'package:hiddify/core/core_providers.dart';
|
||||
import 'package:hiddify/core/prefs/prefs.dart';
|
||||
import 'package:hiddify/domain/clash/clash.dart';
|
||||
import 'package:hiddify/features/settings/widgets/settings_input_dialog.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:recase/recase.dart';
|
||||
|
||||
class ClashSettingTiles extends HookConsumerWidget {
|
||||
const ClashSettingTiles({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final t = ref.watch(translationsProvider);
|
||||
|
||||
final overrides =
|
||||
ref.watch(prefsControllerProvider.select((value) => value.clash));
|
||||
final notifier = ref.watch(prefsControllerProvider.notifier);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
InputOverrideTile(
|
||||
title: t.settings.clash.overrides.httpPort,
|
||||
value: overrides.httpPort,
|
||||
resetValue: ClashConfig.initial.httpPort,
|
||||
onChange: (value) => notifier.patchClashOverrides(
|
||||
ClashConfigPatch(httpPort: value),
|
||||
),
|
||||
),
|
||||
InputOverrideTile(
|
||||
title: t.settings.clash.overrides.socksPort,
|
||||
value: overrides.socksPort,
|
||||
resetValue: ClashConfig.initial.socksPort,
|
||||
onChange: (value) => notifier.patchClashOverrides(
|
||||
ClashConfigPatch(socksPort: value),
|
||||
),
|
||||
),
|
||||
InputOverrideTile(
|
||||
title: t.settings.clash.overrides.redirPort,
|
||||
value: overrides.redirPort,
|
||||
onChange: (value) => notifier.patchClashOverrides(
|
||||
ClashConfigPatch(redirPort: value),
|
||||
),
|
||||
),
|
||||
InputOverrideTile(
|
||||
title: t.settings.clash.overrides.tproxyPort,
|
||||
value: overrides.tproxyPort,
|
||||
onChange: (value) => notifier.patchClashOverrides(
|
||||
ClashConfigPatch(tproxyPort: value),
|
||||
),
|
||||
),
|
||||
InputOverrideTile(
|
||||
title: t.settings.clash.overrides.mixedPort,
|
||||
value: overrides.mixedPort,
|
||||
resetValue: ClashConfig.initial.mixedPort,
|
||||
onChange: (value) => notifier.patchClashOverrides(
|
||||
ClashConfigPatch(mixedPort: value),
|
||||
),
|
||||
),
|
||||
ToggleOverrideTile(
|
||||
title: t.settings.clash.overrides.allowLan,
|
||||
value: overrides.allowLan,
|
||||
onChange: (value) => notifier.patchClashOverrides(
|
||||
ClashConfigPatch(allowLan: value),
|
||||
),
|
||||
),
|
||||
ToggleOverrideTile(
|
||||
title: t.settings.clash.overrides.ipv6,
|
||||
value: overrides.ipv6,
|
||||
onChange: (value) => notifier.patchClashOverrides(
|
||||
ClashConfigPatch(ipv6: value),
|
||||
),
|
||||
),
|
||||
ChoiceOverrideTile(
|
||||
title: t.settings.clash.overrides.mode,
|
||||
value: overrides.mode,
|
||||
options: TunnelMode.values,
|
||||
onChange: (value) => notifier.patchClashOverrides(
|
||||
ClashConfigPatch(mode: value),
|
||||
),
|
||||
),
|
||||
ChoiceOverrideTile(
|
||||
title: t.settings.clash.overrides.logLevel,
|
||||
value: overrides.logLevel,
|
||||
options: LogLevel.values,
|
||||
onChange: (value) => notifier.patchClashOverrides(
|
||||
ClashConfigPatch(logLevel: value),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class InputOverrideTile extends HookConsumerWidget {
|
||||
const InputOverrideTile({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.value,
|
||||
this.resetValue,
|
||||
required this.onChange,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final int? value;
|
||||
final int? resetValue;
|
||||
final ValueChanged<Option<int>> onChange;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final t = ref.watch(translationsProvider);
|
||||
|
||||
return ListTile(
|
||||
title: Text(title),
|
||||
leadingAndTrailingTextStyle: Theme.of(context).textTheme.bodyMedium,
|
||||
trailing: Text(
|
||||
value == null
|
||||
? t.settings.clash.doNotModify.sentenceCase
|
||||
: value.toString(),
|
||||
),
|
||||
onTap: () async {
|
||||
final result = await SettingsInputDialog<int>(
|
||||
title: title,
|
||||
initialValue: value,
|
||||
resetValue: optionOf(resetValue),
|
||||
).show(context).then(
|
||||
(value) {
|
||||
return value?.match<Option<int>?>(
|
||||
() => none(),
|
||||
(t) {
|
||||
final i = int.tryParse(t);
|
||||
return i == null ? null : some(i);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
if (result == null) return;
|
||||
onChange(result);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ToggleOverrideTile extends HookConsumerWidget {
|
||||
const ToggleOverrideTile({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.value,
|
||||
required this.onChange,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final bool? value;
|
||||
final ValueChanged<Option<bool>> onChange;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final t = ref.watch(translationsProvider);
|
||||
|
||||
return PopupMenuButton<Option<bool>>(
|
||||
initialValue: optionOf(value),
|
||||
onSelected: onChange,
|
||||
child: ListTile(
|
||||
title: Text(title),
|
||||
leadingAndTrailingTextStyle: Theme.of(context).textTheme.bodyMedium,
|
||||
trailing: Text(
|
||||
(value == null
|
||||
? t.settings.clash.doNotModify
|
||||
: value!
|
||||
? t.general.toggle.enabled
|
||||
: t.general.toggle.disabled)
|
||||
.sentenceCase,
|
||||
),
|
||||
),
|
||||
itemBuilder: (_) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
value: none(),
|
||||
child: Text(t.settings.clash.doNotModify.sentenceCase),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: some(true),
|
||||
child: Text(t.general.toggle.enabled.sentenceCase),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: some(false),
|
||||
child: Text(t.general.toggle.disabled.sentenceCase),
|
||||
),
|
||||
];
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChoiceOverrideTile<T extends Enum> extends HookConsumerWidget {
|
||||
const ChoiceOverrideTile({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.value,
|
||||
required this.options,
|
||||
required this.onChange,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final T? value;
|
||||
final List<T> options;
|
||||
final ValueChanged<Option<T>> onChange;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final t = ref.watch(translationsProvider);
|
||||
|
||||
return PopupMenuButton<Option<T>>(
|
||||
initialValue: optionOf(value),
|
||||
onSelected: onChange,
|
||||
child: ListTile(
|
||||
title: Text(title),
|
||||
leadingAndTrailingTextStyle: Theme.of(context).textTheme.bodyMedium,
|
||||
trailing: Text(
|
||||
(value == null ? t.settings.clash.doNotModify : value!.name)
|
||||
.sentenceCase,
|
||||
),
|
||||
),
|
||||
itemBuilder: (_) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
value: none(),
|
||||
child: Text(t.settings.clash.doNotModify.sentenceCase),
|
||||
),
|
||||
...options.map(
|
||||
(e) => PopupMenuItem(
|
||||
value: some(e),
|
||||
child: Text(e.name.sentenceCase),
|
||||
),
|
||||
),
|
||||
];
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
36
lib/features/settings/widgets/network_setting_tiles.dart
Normal file
36
lib/features/settings/widgets/network_setting_tiles.dart
Normal file
@@ -0,0 +1,36 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hiddify/core/core_providers.dart';
|
||||
import 'package:hiddify/core/prefs/prefs.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:recase/recase.dart';
|
||||
|
||||
class NetworkSettingTiles extends HookConsumerWidget {
|
||||
const NetworkSettingTiles({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final t = ref.watch(translationsProvider);
|
||||
|
||||
final prefs =
|
||||
ref.watch(prefsControllerProvider.select((value) => value.network));
|
||||
final notifier = ref.watch(prefsControllerProvider.notifier);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
SwitchListTile(
|
||||
title: Text(t.settings.network.systemProxy.titleCase),
|
||||
subtitle: Text(t.settings.network.systemProxyMsg),
|
||||
value: prefs.systemProxy,
|
||||
onChanged: (value) => notifier.patchNetworkPrefs(systemProxy: value),
|
||||
),
|
||||
SwitchListTile(
|
||||
title: Text(t.settings.network.bypassPrivateNetworks.titleCase),
|
||||
subtitle: Text(t.settings.network.bypassPrivateNetworksMsg),
|
||||
value: prefs.bypassPrivateNetworks,
|
||||
onChanged: (value) =>
|
||||
notifier.patchNetworkPrefs(bypassPrivateNetworks: value),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
77
lib/features/settings/widgets/settings_input_dialog.dart
Normal file
77
lib/features/settings/widgets/settings_input_dialog.dart
Normal file
@@ -0,0 +1,77 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import 'package:hiddify/core/core_providers.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
class SettingsInputDialog<T> extends HookConsumerWidget with PresLogger {
|
||||
const SettingsInputDialog({
|
||||
super.key,
|
||||
required this.title,
|
||||
this.initialValue,
|
||||
this.resetValue = const None(),
|
||||
this.icon,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final T? initialValue;
|
||||
|
||||
/// default value, useful for mandatory fields
|
||||
final Option<T> resetValue;
|
||||
final IconData? icon;
|
||||
|
||||
Future<Option<String>?> show(BuildContext context) async {
|
||||
return showDialog(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
builder: (context) => this,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final t = ref.watch(translationsProvider);
|
||||
final localizations = MaterialLocalizations.of(context);
|
||||
|
||||
final textController = useTextEditingController(
|
||||
text: initialValue?.toString(),
|
||||
);
|
||||
|
||||
return AlertDialog(
|
||||
title: Text(title),
|
||||
icon: icon != null ? Icon(icon) : null,
|
||||
content: TextFormField(
|
||||
controller: textController,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.singleLineFormatter,
|
||||
],
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await Navigator.of(context)
|
||||
.maybePop(resetValue.map((t) => t.toString()));
|
||||
},
|
||||
child: Text(t.general.reset.toUpperCase()),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await Navigator.of(context).maybePop();
|
||||
},
|
||||
child: Text(localizations.cancelButtonLabel.toUpperCase()),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
// onConfirm(textController.value.text);
|
||||
await Navigator.of(context)
|
||||
.maybePop(some(textController.value.text));
|
||||
},
|
||||
child: Text(localizations.okButtonLabel.toUpperCase()),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
38
lib/features/settings/widgets/theme_mode_switch_button.dart
Normal file
38
lib/features/settings/widgets/theme_mode_switch_button.dart
Normal file
@@ -0,0 +1,38 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ThemeModeSwitch extends StatelessWidget {
|
||||
const ThemeModeSwitch({
|
||||
super.key,
|
||||
required this.themeMode,
|
||||
required this.onChanged,
|
||||
});
|
||||
final ThemeMode themeMode;
|
||||
final ValueChanged<ThemeMode> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final List<bool> isSelected = <bool>[
|
||||
themeMode == ThemeMode.light,
|
||||
themeMode == ThemeMode.system,
|
||||
themeMode == ThemeMode.dark,
|
||||
];
|
||||
|
||||
return ToggleButtons(
|
||||
isSelected: isSelected,
|
||||
onPressed: (int newIndex) {
|
||||
if (newIndex == 0) {
|
||||
onChanged(ThemeMode.light);
|
||||
} else if (newIndex == 1) {
|
||||
onChanged(ThemeMode.system);
|
||||
} else {
|
||||
onChanged(ThemeMode.dark);
|
||||
}
|
||||
},
|
||||
children: const <Widget>[
|
||||
Icon(Icons.wb_sunny),
|
||||
Icon(Icons.phone_iphone),
|
||||
Icon(Icons.bedtime),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
3
lib/features/settings/widgets/widgets.dart
Normal file
3
lib/features/settings/widgets/widgets.dart
Normal file
@@ -0,0 +1,3 @@
|
||||
export 'appearance_setting_tiles.dart';
|
||||
export 'clash_setting_tiles.dart';
|
||||
export 'network_setting_tiles.dart';
|
||||
Reference in New Issue
Block a user