Refactor
This commit is contained in:
169
lib/features/settings/about/about_page.dart
Normal file
169
lib/features/settings/about/about_page.dart
Normal file
@@ -0,0 +1,169 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hiddify/core/app_info/app_info_provider.dart';
|
||||
import 'package:hiddify/core/localization/translations.dart';
|
||||
import 'package:hiddify/core/model/constants.dart';
|
||||
import 'package:hiddify/core/model/failures.dart';
|
||||
import 'package:hiddify/features/app_update/notifier/app_update_notifier.dart';
|
||||
import 'package:hiddify/features/app_update/notifier/app_update_state.dart';
|
||||
import 'package:hiddify/features/app_update/widget/new_version_dialog.dart';
|
||||
import 'package:hiddify/features/common/nested_app_bar.dart';
|
||||
import 'package:hiddify/gen/assets.gen.dart';
|
||||
import 'package:hiddify/services/service_providers.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
class AboutPage extends HookConsumerWidget {
|
||||
const AboutPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final t = ref.watch(translationsProvider);
|
||||
final appInfo = ref.watch(appInfoProvider).requireValue;
|
||||
final appUpdate = ref.watch(appUpdateNotifierProvider);
|
||||
|
||||
ref.listen(
|
||||
appUpdateNotifierProvider,
|
||||
(_, next) async {
|
||||
if (!context.mounted) return;
|
||||
switch (next) {
|
||||
case AppUpdateStateAvailable(:final versionInfo) ||
|
||||
AppUpdateStateIgnored(:final versionInfo):
|
||||
return NewVersionDialog(
|
||||
appInfo.presentVersion,
|
||||
versionInfo,
|
||||
canIgnore: false,
|
||||
).show(context);
|
||||
case AppUpdateStateError(:final error):
|
||||
return CustomToast.error(t.presentShortError(error)).show(context);
|
||||
case AppUpdateStateNotAvailable():
|
||||
return CustomToast.success(t.appUpdate.notAvailableMsg)
|
||||
.show(context);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
final conditionalTiles = [
|
||||
if (appInfo.release.allowCustomUpdateChecker)
|
||||
ListTile(
|
||||
title: Text(t.about.checkForUpdate),
|
||||
trailing: switch (appUpdate) {
|
||||
AppUpdateStateChecking() => const SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
_ => const Icon(Icons.update),
|
||||
},
|
||||
onTap: () async {
|
||||
await ref.read(appUpdateNotifierProvider.notifier).check();
|
||||
},
|
||||
),
|
||||
if (PlatformUtils.isDesktop)
|
||||
ListTile(
|
||||
title: Text(t.settings.general.openWorkingDir),
|
||||
trailing: const Icon(Icons.arrow_outward_outlined),
|
||||
onTap: () async {
|
||||
final path = ref.read(filesEditorServiceProvider).workingDir.uri;
|
||||
await UriUtils.tryLaunch(path);
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
return Scaffold(
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
NestedAppBar(
|
||||
title: Text(t.about.pageTitle),
|
||||
actions: [
|
||||
PopupMenuButton(
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
child: Text(t.general.addToClipboard),
|
||||
onTap: () {
|
||||
Clipboard.setData(
|
||||
ClipboardData(text: appInfo.format()),
|
||||
);
|
||||
},
|
||||
),
|
||||
];
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Assets.images.logo.svg(width: 64, height: 64),
|
||||
const Gap(16),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
t.general.appTitle,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const Gap(4),
|
||||
Text(
|
||||
"${t.about.version} ${appInfo.presentVersion}",
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverList(
|
||||
delegate: SliverChildListDelegate(
|
||||
[
|
||||
...conditionalTiles,
|
||||
if (conditionalTiles.isNotEmpty) const Divider(),
|
||||
ListTile(
|
||||
title: Text(t.about.sourceCode),
|
||||
trailing: const Icon(Icons.open_in_new),
|
||||
onTap: () async {
|
||||
await UriUtils.tryLaunch(
|
||||
Uri.parse(Constants.githubUrl),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.about.telegramChannel),
|
||||
trailing: const Icon(Icons.open_in_new),
|
||||
onTap: () async {
|
||||
await UriUtils.tryLaunch(
|
||||
Uri.parse(Constants.telegramChannelUrl),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.about.termsAndConditions),
|
||||
trailing: const Icon(Icons.open_in_new),
|
||||
onTap: () async {
|
||||
await UriUtils.tryLaunch(
|
||||
Uri.parse(Constants.termsAndConditionsUrl),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.about.privacyPolicy),
|
||||
trailing: const Icon(Icons.open_in_new),
|
||||
onTap: () async {
|
||||
await UriUtils.tryLaunch(
|
||||
Uri.parse(Constants.privacyPolicyUrl),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
9
lib/features/settings/data/settings_data_providers.dart
Normal file
9
lib/features/settings/data/settings_data_providers.dart
Normal file
@@ -0,0 +1,9 @@
|
||||
import 'package:hiddify/features/settings/data/settings_repository.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'settings_data_providers.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
SettingsRepository settingsRepository(SettingsRepositoryRef ref) {
|
||||
return SettingsRepositoryImpl();
|
||||
}
|
||||
44
lib/features/settings/data/settings_repository.dart
Normal file
44
lib/features/settings/data/settings_repository.dart
Normal file
@@ -0,0 +1,44 @@
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import 'package:hiddify/core/utils/exception_handler.dart';
|
||||
import 'package:hiddify/features/settings/model/settings_failure.dart';
|
||||
import 'package:hiddify/utils/custom_loggers.dart';
|
||||
|
||||
abstract interface class SettingsRepository {
|
||||
TaskEither<SettingsFailure, bool> isIgnoringBatteryOptimizations();
|
||||
TaskEither<SettingsFailure, bool> requestIgnoreBatteryOptimizations();
|
||||
}
|
||||
|
||||
class SettingsRepositoryImpl
|
||||
with ExceptionHandler, InfraLogger
|
||||
implements SettingsRepository {
|
||||
final _methodChannel = const MethodChannel("app.hiddify.com/platform");
|
||||
|
||||
@override
|
||||
TaskEither<SettingsFailure, bool> isIgnoringBatteryOptimizations() {
|
||||
return exceptionHandler(
|
||||
() async {
|
||||
loggy.debug("checking battery optimization status");
|
||||
final result = await _methodChannel
|
||||
.invokeMethod<bool>("is_ignoring_battery_optimizations");
|
||||
loggy.debug("is ignoring battery optimizations? [$result]");
|
||||
return right(result!);
|
||||
},
|
||||
SettingsUnexpectedFailure.new,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
TaskEither<SettingsFailure, bool> requestIgnoreBatteryOptimizations() {
|
||||
return exceptionHandler(
|
||||
() async {
|
||||
loggy.debug("requesting ignore battery optimization");
|
||||
final result = await _methodChannel
|
||||
.invokeMethod<bool>("request_ignore_battery_optimizations");
|
||||
loggy.debug("ignore battery optimization result: [$result]");
|
||||
return right(result!);
|
||||
},
|
||||
SettingsUnexpectedFailure.new,
|
||||
);
|
||||
}
|
||||
}
|
||||
26
lib/features/settings/model/settings_failure.dart
Normal file
26
lib/features/settings/model/settings_failure.dart
Normal file
@@ -0,0 +1,26 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:hiddify/core/localization/translations.dart';
|
||||
import 'package:hiddify/core/model/failures.dart';
|
||||
|
||||
part 'settings_failure.freezed.dart';
|
||||
|
||||
@freezed
|
||||
sealed class SettingsFailure with _$SettingsFailure, Failure {
|
||||
const SettingsFailure._();
|
||||
|
||||
@With<UnexpectedFailure>()
|
||||
const factory SettingsFailure.unexpected([
|
||||
Object? error,
|
||||
StackTrace? stackTrace,
|
||||
]) = SettingsUnexpectedFailure;
|
||||
|
||||
@override
|
||||
({String type, String? message}) present(TranslationsEn t) {
|
||||
return switch (this) {
|
||||
SettingsUnexpectedFailure() => (
|
||||
type: t.failure.unexpected,
|
||||
message: null,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import 'package:hiddify/features/settings/data/settings_data_providers.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'platform_settings_notifier.g.dart';
|
||||
|
||||
@riverpod
|
||||
class IgnoreBatteryOptimizations extends _$IgnoreBatteryOptimizations {
|
||||
@override
|
||||
Future<bool> build() async {
|
||||
return ref
|
||||
.watch(settingsRepositoryProvider)
|
||||
.isIgnoringBatteryOptimizations()
|
||||
.getOrElse((l) => false)
|
||||
.run();
|
||||
}
|
||||
|
||||
Future<void> request() async {
|
||||
await ref
|
||||
.read(settingsRepositoryProvider)
|
||||
.requestIgnoreBatteryOptimizations()
|
||||
.run();
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hiddify/core/core_providers.dart';
|
||||
import 'package:hiddify/core/localization/translations.dart';
|
||||
import 'package:hiddify/features/common/nested_app_bar.dart';
|
||||
import 'package:hiddify/features/settings/widgets/widgets.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
class SettingsPage extends HookConsumerWidget {
|
||||
const SettingsPage({super.key});
|
||||
class SettingsOverviewPage extends HookConsumerWidget {
|
||||
const SettingsOverviewPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@@ -1,299 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hiddify/core/core_providers.dart';
|
||||
import 'package:hiddify/data/repository/config_options_store.dart';
|
||||
import 'package:hiddify/domain/singbox/singbox.dart';
|
||||
import 'package:hiddify/features/log/model/log_level.dart';
|
||||
import 'package:hiddify/features/settings/widgets/widgets.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:humanizer/humanizer.dart';
|
||||
|
||||
class ConfigOptionsPage extends HookConsumerWidget {
|
||||
const ConfigOptionsPage({super.key});
|
||||
|
||||
static final _default = ConfigOptions.initial;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final t = ref.watch(translationsProvider);
|
||||
|
||||
final options = ref.watch(configPreferencesProvider);
|
||||
final serviceMode = ref.watch(serviceModeStoreProvider);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(t.settings.config.pageTitle),
|
||||
actions: [
|
||||
PopupMenuButton(
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
child: Text(t.general.addToClipboard),
|
||||
onTap: () {
|
||||
Clipboard.setData(
|
||||
ClipboardData(text: options.format()),
|
||||
);
|
||||
},
|
||||
),
|
||||
];
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: ListView(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(t.settings.config.logLevel),
|
||||
subtitle: Text(options.logLevel.name.toUpperCase()),
|
||||
onTap: () async {
|
||||
final logLevel = await SettingsPickerDialog(
|
||||
title: t.settings.config.logLevel,
|
||||
selected: options.logLevel,
|
||||
options: LogLevel.choices,
|
||||
getTitle: (e) => e.name.toUpperCase(),
|
||||
resetValue: _default.logLevel,
|
||||
).show(context);
|
||||
if (logLevel == null) return;
|
||||
await ref.read(logLevelStore.notifier).update(logLevel);
|
||||
},
|
||||
),
|
||||
const SettingsDivider(),
|
||||
SettingsSection(t.settings.config.section.route),
|
||||
// SwitchListTile(
|
||||
// title: Text(t.settings.config.bypassLan),
|
||||
// value: options.bypassLan,
|
||||
// onChanged: ref.read(bypassLanStore.notifier).update,
|
||||
// ),
|
||||
SwitchListTile(
|
||||
title: Text(t.settings.config.resolveDestination),
|
||||
value: options.resolveDestination,
|
||||
onChanged: ref.read(resolveDestinationStore.notifier).update,
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.ipv6Mode),
|
||||
subtitle: Text(options.ipv6Mode.present(t)),
|
||||
onTap: () async {
|
||||
final ipv6Mode = await SettingsPickerDialog(
|
||||
title: t.settings.config.ipv6Mode,
|
||||
selected: options.ipv6Mode,
|
||||
options: IPv6Mode.values,
|
||||
getTitle: (e) => e.present(t),
|
||||
resetValue: _default.ipv6Mode,
|
||||
).show(context);
|
||||
if (ipv6Mode == null) return;
|
||||
await ref.read(ipv6ModeStore.notifier).update(ipv6Mode);
|
||||
},
|
||||
),
|
||||
const SettingsDivider(),
|
||||
SettingsSection(t.settings.config.section.dns),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.remoteDnsAddress),
|
||||
subtitle: Text(options.remoteDnsAddress),
|
||||
onTap: () async {
|
||||
final url = await SettingsInputDialog(
|
||||
title: t.settings.config.remoteDnsAddress,
|
||||
initialValue: options.remoteDnsAddress,
|
||||
resetValue: _default.remoteDnsAddress,
|
||||
).show(context);
|
||||
if (url == null || url.isEmpty) return;
|
||||
await ref.read(remoteDnsAddressStore.notifier).update(url);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.remoteDnsDomainStrategy),
|
||||
subtitle: Text(options.remoteDnsDomainStrategy.displayName),
|
||||
onTap: () async {
|
||||
final domainStrategy = await SettingsPickerDialog(
|
||||
title: t.settings.config.remoteDnsDomainStrategy,
|
||||
selected: options.remoteDnsDomainStrategy,
|
||||
options: DomainStrategy.values,
|
||||
getTitle: (e) => e.displayName,
|
||||
resetValue: _default.remoteDnsDomainStrategy,
|
||||
).show(context);
|
||||
if (domainStrategy == null) return;
|
||||
await ref
|
||||
.read(remoteDnsDomainStrategyStore.notifier)
|
||||
.update(domainStrategy);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.directDnsAddress),
|
||||
subtitle: Text(options.directDnsAddress),
|
||||
onTap: () async {
|
||||
final url = await SettingsInputDialog(
|
||||
title: t.settings.config.directDnsAddress,
|
||||
initialValue: options.directDnsAddress,
|
||||
resetValue: _default.directDnsAddress,
|
||||
).show(context);
|
||||
if (url == null || url.isEmpty) return;
|
||||
await ref.read(directDnsAddressStore.notifier).update(url);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.directDnsDomainStrategy),
|
||||
subtitle: Text(options.directDnsDomainStrategy.displayName),
|
||||
onTap: () async {
|
||||
final domainStrategy = await SettingsPickerDialog(
|
||||
title: t.settings.config.directDnsDomainStrategy,
|
||||
selected: options.directDnsDomainStrategy,
|
||||
options: DomainStrategy.values,
|
||||
getTitle: (e) => e.displayName,
|
||||
resetValue: _default.directDnsDomainStrategy,
|
||||
).show(context);
|
||||
if (domainStrategy == null) return;
|
||||
await ref
|
||||
.read(directDnsDomainStrategyStore.notifier)
|
||||
.update(domainStrategy);
|
||||
},
|
||||
),
|
||||
// SwitchListTile(
|
||||
// title: Text(t.settings.config.enableFakeDns),
|
||||
// value: options.enableFakeDns,
|
||||
// onChanged: ref.read(enableFakeDnsStore.notifier).update,
|
||||
// ),
|
||||
const SettingsDivider(),
|
||||
SettingsSection(t.settings.config.section.inbound),
|
||||
// if (PlatformUtils.isDesktop) ...[
|
||||
// SwitchListTile(
|
||||
// title: Text(t.settings.config.enableTun),
|
||||
// value: options.enableTun,
|
||||
// onChanged: ref.read(enableTunStore.notifier).update,
|
||||
// ),
|
||||
// SwitchListTile(
|
||||
// title: Text(t.settings.config.setSystemProxy),
|
||||
// value: options.setSystemProxy,
|
||||
// onChanged: ref.read(setSystemProxyStore.notifier).update,
|
||||
// ),
|
||||
// ],
|
||||
ListTile(
|
||||
title: Text(t.settings.config.serviceMode),
|
||||
subtitle: Text(serviceMode.present(t)),
|
||||
onTap: () async {
|
||||
final pickedMode = await SettingsPickerDialog(
|
||||
title: t.settings.config.serviceMode,
|
||||
selected: serviceMode,
|
||||
options: ServiceMode.choices,
|
||||
getTitle: (e) => e.present(t),
|
||||
resetValue: ServiceMode.defaultMode,
|
||||
).show(context);
|
||||
if (pickedMode == null) return;
|
||||
await ref
|
||||
.read(serviceModeStoreProvider.notifier)
|
||||
.update(pickedMode);
|
||||
},
|
||||
),
|
||||
SwitchListTile(
|
||||
title: Text(t.settings.config.strictRoute),
|
||||
value: options.strictRoute,
|
||||
onChanged: ref.read(strictRouteStore.notifier).update,
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.tunImplementation),
|
||||
subtitle: Text(options.tunImplementation.name),
|
||||
onTap: () async {
|
||||
final tunImplementation = await SettingsPickerDialog(
|
||||
title: t.settings.config.tunImplementation,
|
||||
selected: options.tunImplementation,
|
||||
options: TunImplementation.values,
|
||||
getTitle: (e) => e.name,
|
||||
resetValue: _default.tunImplementation,
|
||||
).show(context);
|
||||
if (tunImplementation == null) return;
|
||||
await ref
|
||||
.read(tunImplementationStore.notifier)
|
||||
.update(tunImplementation);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.mixedPort),
|
||||
subtitle: Text(options.mixedPort.toString()),
|
||||
onTap: () async {
|
||||
final mixedPort = await SettingsInputDialog(
|
||||
title: t.settings.config.mixedPort,
|
||||
initialValue: options.mixedPort,
|
||||
resetValue: _default.mixedPort,
|
||||
validator: isPort,
|
||||
mapTo: int.tryParse,
|
||||
digitsOnly: true,
|
||||
).show(context);
|
||||
if (mixedPort == null) return;
|
||||
await ref.read(mixedPortStore.notifier).update(mixedPort);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.localDnsPort),
|
||||
subtitle: Text(options.localDnsPort.toString()),
|
||||
onTap: () async {
|
||||
final localDnsPort = await SettingsInputDialog(
|
||||
title: t.settings.config.localDnsPort,
|
||||
initialValue: options.localDnsPort,
|
||||
resetValue: _default.localDnsPort,
|
||||
validator: isPort,
|
||||
mapTo: int.tryParse,
|
||||
digitsOnly: true,
|
||||
).show(context);
|
||||
if (localDnsPort == null) return;
|
||||
await ref.read(localDnsPortStore.notifier).update(localDnsPort);
|
||||
},
|
||||
),
|
||||
const SettingsDivider(),
|
||||
SettingsSection(t.settings.config.section.misc),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.connectionTestUrl),
|
||||
subtitle: Text(options.connectionTestUrl),
|
||||
onTap: () async {
|
||||
final url = await SettingsInputDialog(
|
||||
title: t.settings.config.connectionTestUrl,
|
||||
initialValue: options.connectionTestUrl,
|
||||
resetValue: _default.connectionTestUrl,
|
||||
).show(context);
|
||||
if (url == null || url.isEmpty || !isUrl(url)) return;
|
||||
await ref.read(connectionTestUrlStore.notifier).update(url);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.urlTestInterval),
|
||||
subtitle: Text(
|
||||
options.urlTestInterval.toApproximateTime(isRelativeToNow: false),
|
||||
),
|
||||
onTap: () async {
|
||||
final urlTestInterval = await SettingsSliderDialog(
|
||||
title: t.settings.config.urlTestInterval,
|
||||
initialValue: options.urlTestInterval.inMinutes.toDouble(),
|
||||
resetValue: _default.urlTestInterval.inMinutes.toDouble(),
|
||||
min: 1,
|
||||
max: 60,
|
||||
divisions: 60,
|
||||
labelGen: (value) => Duration(minutes: value.toInt())
|
||||
.toApproximateTime(isRelativeToNow: false),
|
||||
).show(context);
|
||||
if (urlTestInterval == null) return;
|
||||
await ref
|
||||
.read(urlTestIntervalStore.notifier)
|
||||
.update(Duration(minutes: urlTestInterval.toInt()));
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.clashApiPort),
|
||||
subtitle: Text(options.clashApiPort.toString()),
|
||||
onTap: () async {
|
||||
final clashApiPort = await SettingsInputDialog(
|
||||
title: t.settings.config.clashApiPort,
|
||||
initialValue: options.clashApiPort,
|
||||
resetValue: _default.clashApiPort,
|
||||
validator: isPort,
|
||||
mapTo: int.tryParse,
|
||||
digitsOnly: true,
|
||||
).show(context);
|
||||
if (clashApiPort == null) return;
|
||||
await ref.read(clashApiPortStore.notifier).update(clashApiPort);
|
||||
},
|
||||
),
|
||||
const Gap(24),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,237 +0,0 @@
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hiddify/core/core_providers.dart';
|
||||
import 'package:hiddify/core/prefs/general_prefs.dart';
|
||||
import 'package:hiddify/domain/singbox/rules.dart';
|
||||
import 'package:hiddify/services/platform_services.dart';
|
||||
import 'package:hiddify/services/service_providers.dart';
|
||||
import 'package:hiddify/utils/riverpod_utils.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:loggy/loggy.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:sliver_tools/sliver_tools.dart';
|
||||
|
||||
part 'per_app_proxy_page.g.dart';
|
||||
|
||||
final _logger = Loggy<AppLogger>("PerAppProxySettings");
|
||||
|
||||
@riverpod
|
||||
Future<List<InstalledPackageInfo>> installedPackagesInfo(
|
||||
InstalledPackagesInfoRef ref,
|
||||
) async {
|
||||
return ref
|
||||
.watch(platformServicesProvider)
|
||||
.getInstalledPackages()
|
||||
.getOrElse((err) {
|
||||
_logger.error("error getting installed packages", err);
|
||||
throw err;
|
||||
}).run();
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<ImageProvider> packageIcon(
|
||||
PackageIconRef ref,
|
||||
String packageName,
|
||||
) async {
|
||||
ref.disposeDelay(const Duration(seconds: 10));
|
||||
final bytes = await ref
|
||||
.watch(platformServicesProvider)
|
||||
.getPackageIcon(packageName)
|
||||
.getOrElse((err) {
|
||||
_logger.warning("error getting package icon", err);
|
||||
throw err;
|
||||
}).run();
|
||||
return MemoryImage(bytes);
|
||||
}
|
||||
|
||||
class PerAppProxyPage extends HookConsumerWidget with PresLogger {
|
||||
const PerAppProxyPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final t = ref.watch(translationsProvider);
|
||||
final localizations = MaterialLocalizations.of(context);
|
||||
|
||||
final asyncPackages = ref.watch(installedPackagesInfoProvider);
|
||||
final perAppProxyMode = ref.watch(perAppProxyModeNotifierProvider);
|
||||
final perAppProxyList = ref.watch(perAppProxyListProvider);
|
||||
|
||||
final showSystemApps = useState(true);
|
||||
final isSearching = useState(false);
|
||||
final searchQuery = useState("");
|
||||
|
||||
final filteredPackages = useMemoized(
|
||||
() {
|
||||
if (showSystemApps.value && searchQuery.value.isBlank) {
|
||||
return asyncPackages;
|
||||
}
|
||||
return asyncPackages.whenData(
|
||||
(value) {
|
||||
Iterable<InstalledPackageInfo> result = value;
|
||||
if (!showSystemApps.value) {
|
||||
result = result.filter((e) => !e.isSystemApp);
|
||||
}
|
||||
if (!searchQuery.value.isBlank) {
|
||||
result = result.filter(
|
||||
(e) => e.name
|
||||
.toLowerCase()
|
||||
.contains(searchQuery.value.toLowerCase()),
|
||||
);
|
||||
}
|
||||
return result.toList();
|
||||
},
|
||||
);
|
||||
},
|
||||
[asyncPackages, showSystemApps.value, searchQuery.value],
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: isSearching.value
|
||||
? AppBar(
|
||||
title: TextFormField(
|
||||
onChanged: (value) => searchQuery.value = value,
|
||||
autofocus: true,
|
||||
decoration: InputDecoration(
|
||||
hintText: "${localizations.searchFieldLabel}...",
|
||||
isDense: true,
|
||||
filled: false,
|
||||
border: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
errorBorder: InputBorder.none,
|
||||
focusedErrorBorder: InputBorder.none,
|
||||
disabledBorder: InputBorder.none,
|
||||
),
|
||||
),
|
||||
leading: IconButton(
|
||||
onPressed: () {
|
||||
searchQuery.value = "";
|
||||
isSearching.value = false;
|
||||
},
|
||||
icon: const Icon(Icons.close),
|
||||
tooltip: localizations.cancelButtonLabel,
|
||||
),
|
||||
)
|
||||
: AppBar(
|
||||
title: Text(t.settings.network.perAppProxyPageTitle),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.search),
|
||||
onPressed: () => isSearching.value = true,
|
||||
tooltip: localizations.searchFieldLabel,
|
||||
),
|
||||
PopupMenuButton(
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
child: Text(
|
||||
showSystemApps.value
|
||||
? t.settings.network.hideSystemApps
|
||||
: t.settings.network.showSystemApps,
|
||||
),
|
||||
onTap: () =>
|
||||
showSystemApps.value = !showSystemApps.value,
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Text(t.settings.network.clearSelection),
|
||||
onTap: () => ref
|
||||
.read(perAppProxyListProvider.notifier)
|
||||
.update([]),
|
||||
),
|
||||
];
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
SliverPinnedHeader(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
...PerAppProxyMode.values.map(
|
||||
(e) => RadioListTile<PerAppProxyMode>(
|
||||
title: Text(e.present(t).message),
|
||||
dense: true,
|
||||
value: e,
|
||||
groupValue: perAppProxyMode,
|
||||
onChanged: (value) async {
|
||||
await ref
|
||||
.read(perAppProxyModeNotifierProvider.notifier)
|
||||
.update(e);
|
||||
if (e == PerAppProxyMode.off && context.mounted) {
|
||||
context.pop();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
switch (filteredPackages) {
|
||||
AsyncData(value: final packages) => SliverList.builder(
|
||||
itemBuilder: (context, index) {
|
||||
final package = packages[index];
|
||||
final selected =
|
||||
perAppProxyList.contains(package.packageName);
|
||||
return CheckboxListTile(
|
||||
title: Text(
|
||||
package.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: Text(
|
||||
package.packageName,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
value: selected,
|
||||
onChanged: (value) async {
|
||||
final List<String> newSelection;
|
||||
if (selected) {
|
||||
newSelection = perAppProxyList
|
||||
.exceptElement(package.packageName)
|
||||
.toList();
|
||||
} else {
|
||||
newSelection = [
|
||||
...perAppProxyList,
|
||||
package.packageName,
|
||||
];
|
||||
}
|
||||
await ref
|
||||
.read(perAppProxyListProvider.notifier)
|
||||
.update(newSelection);
|
||||
},
|
||||
secondary: SizedBox(
|
||||
width: 48,
|
||||
height: 48,
|
||||
child: ref
|
||||
.watch(packageIconProvider(package.packageName))
|
||||
.when(
|
||||
data: (data) => Image(image: data),
|
||||
error: (error, _) => const Icon(Icons.error),
|
||||
loading: () => const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: packages.length,
|
||||
),
|
||||
AsyncLoading() => const SliverLoadingBodyPlaceholder(),
|
||||
AsyncError(:final error) =>
|
||||
SliverErrorBodyPlaceholder(error.toString()),
|
||||
_ => const SliverToBoxAdapter(),
|
||||
},
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export 'config_options_page.dart';
|
||||
export 'per_app_proxy_page.dart';
|
||||
export 'settings_page.dart';
|
||||
@@ -2,11 +2,11 @@ import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hiddify/core/core_providers.dart';
|
||||
import 'package:hiddify/core/prefs/prefs.dart';
|
||||
import 'package:hiddify/core/localization/translations.dart';
|
||||
import 'package:hiddify/core/preferences/general_preferences.dart';
|
||||
import 'package:hiddify/core/router/router.dart';
|
||||
import 'package:hiddify/domain/singbox/singbox.dart';
|
||||
import 'package:hiddify/features/common/general_pref_tiles.dart';
|
||||
import 'package:hiddify/features/per_app_proxy/model/per_app_proxy_mode.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
class AdvancedSettingTiles extends HookConsumerWidget {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hiddify/core/core_providers.dart';
|
||||
import 'package:hiddify/core/prefs/prefs.dart';
|
||||
import 'package:hiddify/core/localization/translations.dart';
|
||||
import 'package:hiddify/core/preferences/general_preferences.dart';
|
||||
import 'package:hiddify/core/theme/app_theme_mode.dart';
|
||||
import 'package:hiddify/core/theme/theme_preferences.dart';
|
||||
import 'package:hiddify/features/common/general_pref_tiles.dart';
|
||||
import 'package:hiddify/services/auto_start_service.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
@@ -14,7 +16,7 @@ class GeneralSettingTiles extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final t = ref.watch(translationsProvider);
|
||||
|
||||
final theme = ref.watch(themeProvider);
|
||||
final themeMode = ref.watch(themePreferencesProvider);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
@@ -43,7 +45,7 @@ class GeneralSettingTiles extends HookConsumerWidget {
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.settings.general.themeMode),
|
||||
subtitle: Text(theme.mode.present(t)),
|
||||
subtitle: Text(themeMode.present(t)),
|
||||
leading: const Icon(Icons.light_mode),
|
||||
onTap: () async {
|
||||
final selectedThemeMode = await showDialog<AppThemeMode>(
|
||||
@@ -56,7 +58,7 @@ class GeneralSettingTiles extends HookConsumerWidget {
|
||||
(e) => RadioListTile(
|
||||
title: Text(e.present(t)),
|
||||
value: e,
|
||||
groupValue: theme.mode,
|
||||
groupValue: themeMode,
|
||||
onChanged: (e) => context.pop(e),
|
||||
),
|
||||
)
|
||||
@@ -66,8 +68,8 @@ class GeneralSettingTiles extends HookConsumerWidget {
|
||||
);
|
||||
if (selectedThemeMode != null) {
|
||||
await ref
|
||||
.read(themeModeNotifierProvider.notifier)
|
||||
.update(selectedThemeMode);
|
||||
.read(themePreferencesProvider.notifier)
|
||||
.changeThemeMode(selectedThemeMode);
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
@@ -1,22 +1,9 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hiddify/core/core_providers.dart';
|
||||
import 'package:hiddify/services/service_providers.dart';
|
||||
import 'package:hiddify/core/localization/translations.dart';
|
||||
import 'package:hiddify/features/settings/notifier/platform_settings_notifier.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'platform_settings_tiles.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<bool> isIgnoringBatteryOptimizations(
|
||||
IsIgnoringBatteryOptimizationsRef ref,
|
||||
) async =>
|
||||
ref
|
||||
.watch(platformServicesProvider)
|
||||
.isIgnoringBatteryOptimizations()
|
||||
.getOrElse((l) => false)
|
||||
.run();
|
||||
|
||||
class PlatformSettingsTiles extends HookConsumerWidget {
|
||||
const PlatformSettingsTiles({super.key});
|
||||
@@ -26,7 +13,7 @@ class PlatformSettingsTiles extends HookConsumerWidget {
|
||||
final t = ref.watch(translationsProvider);
|
||||
|
||||
final isIgnoringBatteryOptimizations =
|
||||
ref.watch(isIgnoringBatteryOptimizationsProvider);
|
||||
ref.watch(ignoreBatteryOptimizationsProvider);
|
||||
|
||||
ListTile buildIgnoreTile(bool enabled) => ListTile(
|
||||
title: Text(t.settings.general.ignoreBatteryOptimizations),
|
||||
@@ -35,11 +22,8 @@ class PlatformSettingsTiles extends HookConsumerWidget {
|
||||
enabled: enabled,
|
||||
onTap: () async {
|
||||
await ref
|
||||
.read(platformServicesProvider)
|
||||
.requestIgnoreBatteryOptimizations()
|
||||
.run();
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
ref.invalidate(isIgnoringBatteryOptimizationsProvider);
|
||||
.read(ignoreBatteryOptimizationsProvider.notifier)
|
||||
.request();
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hiddify/core/core_providers.dart';
|
||||
import 'package:hiddify/core/localization/translations.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user