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

View 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,
),
);
}
}

View File

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

View 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);
},
),
],
);
}
}

View 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),
),
),
];
},
);
}
}

View 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),
),
],
);
}
}

View 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()),
),
],
);
}
}

View 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),
],
);
}
}

View File

@@ -0,0 +1,3 @@
export 'appearance_setting_tiles.dart';
export 'clash_setting_tiles.dart';
export 'network_setting_tiles.dart';