From d3cab28deefef1b10266e4aa348a0a7c0359b18b Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Sun, 12 Nov 2023 12:52:54 +0330 Subject: [PATCH 01/91] Add export config to clipboard --- .../com/hiddify/hiddify/MethodHandler.kt | 17 ++++++++++++ .../com/hiddify/hiddify/bg/BoxService.kt | 4 +++ assets/translations/strings_en.i18n.json | 5 ++++ assets/translations/strings_fa.i18n.json | 5 ++++ assets/translations/strings_ru.i18n.json | 5 ++++ assets/translations/strings_zh.i18n.json | 5 ++++ lib/data/repository/core_facade_impl.dart | 21 +++++++++++++++ lib/domain/singbox/singbox_facade.dart | 4 +++ lib/features/common/profile_tile.dart | 27 +++++++++++++++++++ .../profiles/notifier/profiles_notifier.dart | 13 +++++++++ lib/gen/singbox_generated_bindings.dart | 15 +++++++++++ lib/services/singbox/ffi_singbox_service.dart | 23 ++++++++++++++++ .../singbox/mobile_singbox_service.dart | 18 +++++++++++++ lib/services/singbox/singbox_service.dart | 4 +++ libcore | 2 +- 15 files changed, 167 insertions(+), 1 deletion(-) diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/MethodHandler.kt b/android/app/src/main/kotlin/com/hiddify/hiddify/MethodHandler.kt index d40ae0ba..f2656c25 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/MethodHandler.kt +++ b/android/app/src/main/kotlin/com/hiddify/hiddify/MethodHandler.kt @@ -2,6 +2,7 @@ package com.hiddify.hiddify import android.util.Log import com.hiddify.hiddify.bg.BoxService +import com.hiddify.hiddify.constant.Alert import com.hiddify.hiddify.constant.Status import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.MethodCall @@ -24,6 +25,7 @@ class MethodHandler(private val scope: CoroutineScope) : FlutterPlugin, enum class Trigger(val method: String) { ParseConfig("parse_config"), ChangeConfigOptions("change_config_options"), + GenerateConfig("generate_config"), Start("start"), Stop("stop"), Restart("restart"), @@ -70,6 +72,21 @@ class MethodHandler(private val scope: CoroutineScope) : FlutterPlugin, } } + Trigger.GenerateConfig.method -> { + scope.launch { + result.runCatching { + val args = call.arguments as Map<*, *> + val path = args["path"] as String + val options = Settings.configOptions + if (options.isBlank() || path.isBlank()) { + error("blank properties") + } + val config = BoxService.buildConfig(path, options) + success(config) + } + } + } + Trigger.Start.method -> { scope.launch { result.runCatching { diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/BoxService.kt b/android/app/src/main/kotlin/com/hiddify/hiddify/bg/BoxService.kt index 41fa05e1..6268b739 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/BoxService.kt +++ b/android/app/src/main/kotlin/com/hiddify/hiddify/bg/BoxService.kt @@ -72,6 +72,10 @@ class BoxService( } } + fun buildConfig(path: String, options: String):String { + return Mobile.buildConfig(path, options) + } + fun start() { val intent = runBlocking { withContext(Dispatchers.IO) { diff --git a/assets/translations/strings_en.i18n.json b/assets/translations/strings_en.i18n.json index ad62901c..54bc6b2a 100644 --- a/assets/translations/strings_en.i18n.json +++ b/assets/translations/strings_en.i18n.json @@ -67,6 +67,11 @@ "failureMsg": "Failed to update profile", "successMsg": "Profile updated successfully" }, + "share": { + "buttonText": "Share", + "exportConfigToClipboard": "Export configuration to clipboard", + "exportConfigToClipboardSuccess": "Configuration copied to clipboard" + }, "edit": { "buttonTxt": "Edit", "selectActiveTxt": "Select active profile" diff --git a/assets/translations/strings_fa.i18n.json b/assets/translations/strings_fa.i18n.json index 6e4f06de..7595fbad 100644 --- a/assets/translations/strings_fa.i18n.json +++ b/assets/translations/strings_fa.i18n.json @@ -67,6 +67,11 @@ "failureMsg": "در بروزرسانی پروفایل خطایی رخ داد", "successMsg": "پروفایل با موفقیت بروزرسانی شد" }, + "share": { + "buttonText": "اشتراک گذاری", + "exportConfigToClipboard": "افزودن پیکربندی به کلیپ بورد", + "exportConfigToClipboardSuccess": "پیکربندی در کلیپ بورد کپی شد" + }, "edit": { "buttonTxt": "ویرایش", "selectActiveTxt": "انتخاب پروفایل فعال" diff --git a/assets/translations/strings_ru.i18n.json b/assets/translations/strings_ru.i18n.json index b7d6a63a..21d6b34a 100644 --- a/assets/translations/strings_ru.i18n.json +++ b/assets/translations/strings_ru.i18n.json @@ -67,6 +67,11 @@ "failureMsg": "Ошибка обновления", "successMsg": "Профиль успешно обновлён" }, + "share": { + "buttonText": "Делиться", + "exportConfigToClipboard": "Экспортировать конфигурацию в буфер обмена", + "exportConfigToClipboardSuccess": "Конфигурация скопирована в буфер обмена." + }, "edit": { "buttonTxt": "Изменить", "selectActiveTxt": "Выберите активный профиль" diff --git a/assets/translations/strings_zh.i18n.json b/assets/translations/strings_zh.i18n.json index 54e5030d..6e3ced28 100644 --- a/assets/translations/strings_zh.i18n.json +++ b/assets/translations/strings_zh.i18n.json @@ -67,6 +67,11 @@ "failureMsg": "更新配置文件失败", "successMsg": "配置文件更新成功" }, + "share": { + "buttonText": "分享", + "exportConfigToClipboard": "将配置导出到剪贴板", + "exportConfigToClipboardSuccess": "配置已复制到剪贴板" + }, "edit": { "buttonTxt": "编辑", "selectActiveTxt": "选择活动配置文件" diff --git a/lib/data/repository/core_facade_impl.dart b/lib/data/repository/core_facade_impl.dart index c779e61c..d6d88279 100644 --- a/lib/data/repository/core_facade_impl.dart +++ b/lib/data/repository/core_facade_impl.dart @@ -90,6 +90,27 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { ); } + @override + TaskEither generateConfig( + String fileName, + ) { + return exceptionHandler( + () { + final configPath = filesEditor.configPath(fileName); + final options = configOptions(); + return setup() + .andThen(() => changeConfigOptions(options)) + .andThen( + () => singbox + .generateConfig(configPath) + .mapLeft(CoreServiceFailure.other), + ) + .run(); + }, + CoreServiceFailure.unexpected, + ); + } + @override TaskEither start( String fileName, diff --git a/lib/domain/singbox/singbox_facade.dart b/lib/domain/singbox/singbox_facade.dart index 7b63ee9d..dd7a9544 100644 --- a/lib/domain/singbox/singbox_facade.dart +++ b/lib/domain/singbox/singbox_facade.dart @@ -18,6 +18,10 @@ abstract interface class SingboxFacade { ConfigOptions options, ); + TaskEither generateConfig( + String fileName, + ); + TaskEither start( String fileName, bool disableMemoryLimit, diff --git a/lib/features/common/profile_tile.dart b/lib/features/common/profile_tile.dart index b33f8c2a..8d0ba2ab 100644 --- a/lib/features/common/profile_tile.dart +++ b/lib/features/common/profile_tile.dart @@ -254,6 +254,14 @@ class ProfileActionsMenu extends HookConsumerWidget { initialOnSuccess: () => CustomToast.success(t.profile.update.successMsg).show(context), ); + final exportConfigMutation = useMutation( + initialOnFailure: (err) { + CustomToast.error(t.presentShortError(err)).show(context); + }, + initialOnSuccess: () => + CustomToast.success(t.profile.share.exportConfigToClipboardSuccess) + .show(context), + ); final deleteProfileMutation = useMutation( initialOnFailure: (err) { CustomAlertDialog.fromErr(t.presentError(err)).show(context); @@ -278,6 +286,25 @@ class ProfileActionsMenu extends HookConsumerWidget { ); }, ), + SubmenuButton( + menuChildren: [ + MenuItemButton( + child: Text(t.profile.share.exportConfigToClipboard), + onPressed: () async { + if (exportConfigMutation.state.isInProgress) { + return; + } + exportConfigMutation.setFuture( + ref + .read(profilesNotifierProvider.notifier) + .exportConfigToClipboard(profile), + ); + }, + ), + ], + leadingIcon: const Icon(Icons.share), + child: Text(t.profile.share.buttonText), + ), MenuItemButton( leadingIcon: const Icon(Icons.edit), child: Text(t.profile.edit.buttonTxt), diff --git a/lib/features/profiles/notifier/profiles_notifier.dart b/lib/features/profiles/notifier/profiles_notifier.dart index 87266d2a..6fb165e5 100644 --- a/lib/features/profiles/notifier/profiles_notifier.dart +++ b/lib/features/profiles/notifier/profiles_notifier.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:flutter/services.dart'; import 'package:fpdart/fpdart.dart'; import 'package:hiddify/core/prefs/prefs.dart'; import 'package:hiddify/data/data_providers.dart'; @@ -124,4 +125,16 @@ class ProfilesNotifier extends _$ProfilesNotifier with AppLogger { }, ).run(); } + + Future exportConfigToClipboard(Profile profile) async { + await ref.read(coreFacadeProvider).generateConfig(profile.id).match( + (err) { + loggy.warning('error generating config', err); + throw err; + }, + (configJson) async { + await Clipboard.setData(ClipboardData(text: configJson)); + }, + ).run(); + } } diff --git a/lib/gen/singbox_generated_bindings.dart b/lib/gen/singbox_generated_bindings.dart index 25279a7c..6bbdb039 100644 --- a/lib/gen/singbox_generated_bindings.dart +++ b/lib/gen/singbox_generated_bindings.dart @@ -934,6 +934,21 @@ class SingboxNativeLibrary { late final _changeConfigOptions = _changeConfigOptionsPtr .asFunction Function(ffi.Pointer)>(); + ffi.Pointer generateConfig( + ffi.Pointer path, + ) { + return _generateConfig( + path, + ); + } + + late final _generateConfigPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer)>>('generateConfig'); + late final _generateConfig = _generateConfigPtr + .asFunction Function(ffi.Pointer)>(); + ffi.Pointer start( ffi.Pointer configPath, int disableMemoryLimit, diff --git a/lib/services/singbox/ffi_singbox_service.dart b/lib/services/singbox/ffi_singbox_service.dart index 5d9aa308..87124e55 100644 --- a/lib/services/singbox/ffi_singbox_service.dart +++ b/lib/services/singbox/ffi_singbox_service.dart @@ -5,6 +5,7 @@ import 'dart:io'; import 'dart:isolate'; import 'package:combine/combine.dart'; +import 'package:dartx/dartx.dart'; import 'package:ffi/ffi.dart'; import 'package:fpdart/fpdart.dart'; import 'package:hiddify/domain/connectivity/connectivity.dart'; @@ -137,6 +138,28 @@ class FFISingboxService ); } + @override + TaskEither generateConfig( + String path, + ) { + return TaskEither( + () => CombineWorker().execute( + () { + final response = _box + .generateConfig( + path.toNativeUtf8().cast(), + ) + .cast() + .toDartString(); + if (response.startsWith("error")) { + return left(response.removePrefix("error")); + } + return right(response); + }, + ), + ); + } + @override TaskEither start(String configPath, bool disableMemoryLimit) { loggy.debug("starting, memory limit: [${!disableMemoryLimit}]"); diff --git a/lib/services/singbox/mobile_singbox_service.dart b/lib/services/singbox/mobile_singbox_service.dart index 08887bcf..b27ec2ab 100644 --- a/lib/services/singbox/mobile_singbox_service.dart +++ b/lib/services/singbox/mobile_singbox_service.dart @@ -73,6 +73,24 @@ class MobileSingboxService ); } + @override + TaskEither generateConfig( + String path, + ) { + return TaskEither( + () async { + final configJson = await _methodChannel.invokeMethod( + "generate_config", + {"path": path}, + ); + if (configJson == null || configJson.isEmpty) { + return left("null response"); + } + return right(configJson); + }, + ); + } + @override TaskEither start(String configPath, bool disableMemoryLimit) { return TaskEither( diff --git a/lib/services/singbox/singbox_service.dart b/lib/services/singbox/singbox_service.dart index 0d89ab88..ac1d4d90 100644 --- a/lib/services/singbox/singbox_service.dart +++ b/lib/services/singbox/singbox_service.dart @@ -33,6 +33,10 @@ abstract interface class SingboxService { TaskEither changeConfigOptions(ConfigOptions options); + TaskEither generateConfig( + String path, + ); + TaskEither start(String configPath, bool disableMemoryLimit); TaskEither stop(); diff --git a/libcore b/libcore index 5c8b283d..953e6a02 160000 --- a/libcore +++ b/libcore @@ -1 +1 @@ -Subproject commit 5c8b283d9cd84a4b1412dd65572ae039a822de78 +Subproject commit 953e6a02d73d0a2894071e289ff209b633470e54 From 344f4f706f31987d31df76b0218e78bd687c737e Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Sun, 12 Nov 2023 13:08:48 +0330 Subject: [PATCH 02/91] Add proxy tag dialog --- lib/features/proxies/widgets/proxy_tile.dart | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/features/proxies/widgets/proxy_tile.dart b/lib/features/proxies/widgets/proxy_tile.dart index fe2a2375..180afc42 100644 --- a/lib/features/proxies/widgets/proxy_tile.dart +++ b/lib/features/proxies/widgets/proxy_tile.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; import 'package:hiddify/domain/singbox/singbox.dart'; +import 'package:hiddify/utils/custom_loggers.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -class ProxyTile extends HookConsumerWidget { +class ProxyTile extends HookConsumerWidget with PresLogger { const ProxyTile( this.proxy, { super.key, @@ -56,6 +57,20 @@ class ProxyTile extends HookConsumerWidget { : null, selected: selected, onTap: onSelect, + onLongPress: () async { + showDialog( + context: context, + builder: (context) => AlertDialog( + content: SelectionArea(child: Text(sanitizedTag(proxy.tag))), + actions: [ + TextButton( + onPressed: Navigator.of(context).pop, + child: Text(MaterialLocalizations.of(context).closeButtonLabel), + ), + ], + ), + ); + }, horizontalTitleGap: 4, ); } From 577bb3ded3b709601b4788a15d277be879f493ad Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Sun, 12 Nov 2023 21:55:17 +0330 Subject: [PATCH 03/91] Fix bootstrap bug --- lib/bootstrap.dart | 49 ++++++++----------- .../connectivity/connectivity_controller.dart | 5 +- .../common/window/window_controller.dart | 2 +- .../system_tray/system_tray_controller.dart | 5 +- 4 files changed, 29 insertions(+), 32 deletions(-) diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index 230502ed..4cfa3d18 100644 --- a/lib/bootstrap.dart +++ b/lib/bootstrap.dart @@ -94,13 +94,32 @@ Future _lazyBootstrap( if (silentStart) { FlutterNativeSplash.remove(); } + if (PlatformUtils.isDesktop) { + _logger.debug("initializing [Auto Start Service] and [Window Controller]"); await container.read(autoStartServiceProvider.future); await container.read(windowControllerProvider.future); } - await initAppServices(container.read); - await initControllers(container.read); + await container.read(singboxServiceProvider).init(); + _logger.debug("initialized [Singbox Service]"); + + await container.read(activeProfileProvider.future); + await container.read(deepLinkServiceProvider.future); + if (PlatformUtils.isDesktop) { + try { + await container + .read(systemTrayControllerProvider.future) + .timeout(const Duration(seconds: 1)); + _logger.debug("initialized [System Tray Controller]"); + } catch (error, stackTrace) { + _logger.warning( + "error initializing [System Tray Controller]", + error, + stackTrace, + ); + } + } runApp( ProviderScope( @@ -130,29 +149,3 @@ void initLoggers( logOptions: LogOptions(logLevel), ); } - -Future initAppServices( - Result Function(ProviderListenable) read, -) async { - _logger.debug("initializing app services"); - await Future.wait( - [ - read(singboxServiceProvider).init(), - ], - ); - _logger.debug('initialized app services'); -} - -Future initControllers( - Result Function(ProviderListenable) read, -) async { - _logger.debug("initializing controllers"); - await Future.wait( - [ - read(activeProfileProvider.future), - read(deepLinkServiceProvider.future), - if (PlatformUtils.isDesktop) read(systemTrayControllerProvider.future), - ], - ); - _logger.debug("initialized base controllers"); -} diff --git a/lib/features/common/connectivity/connectivity_controller.dart b/lib/features/common/connectivity/connectivity_controller.dart index a8bf79e3..cea73393 100644 --- a/lib/features/common/connectivity/connectivity_controller.dart +++ b/lib/features/common/connectivity/connectivity_controller.dart @@ -90,8 +90,9 @@ class ConnectivityController extends _$ConnectivityController with AppLogger { final activeProfile = await ref.read(activeProfileProvider.future); await _core .start(activeProfile!.id, ref.read(disableMemoryLimitProvider)) - .mapLeft((err) { - loggy.warning("error connecting $err", err); + .mapLeft((err) async { + loggy.warning("error connecting", err); + await ref.read(startedByUserProvider.notifier).update(false); state = AsyncError(err, StackTrace.current); }).run(); } diff --git a/lib/features/common/window/window_controller.dart b/lib/features/common/window/window_controller.dart index edd375b6..7b4d09de 100644 --- a/lib/features/common/window/window_controller.dart +++ b/lib/features/common/window/window_controller.dart @@ -31,7 +31,7 @@ class WindowController extends _$WindowController await windowManager.hide(); } await Future.delayed( - const Duration(seconds: 1), + const Duration(seconds: 3), () async { if (ref.read(startedByUserProvider)) { loggy.debug("previously started by user, trying to connect"); diff --git a/lib/features/system_tray/system_tray_controller.dart b/lib/features/system_tray/system_tray_controller.dart index 92d179fe..f93c8602 100644 --- a/lib/features/system_tray/system_tray_controller.dart +++ b/lib/features/system_tray/system_tray_controller.dart @@ -30,7 +30,10 @@ class SystemTrayController extends _$SystemTrayController _initialized = true; } - final connection = await ref.watch(connectivityControllerProvider.future); + final connection = switch (ref.watch(connectivityControllerProvider)) { + AsyncData(:final value) => value, + _ => const Disconnected(), + }; final serviceMode = ref.watch(serviceModeStoreProvider); final t = ref.watch(translationsProvider); From c1d3c5445fe419270bc5ae2e5c5a7fce1b679826 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Sun, 12 Nov 2023 22:22:20 +0330 Subject: [PATCH 04/91] Add sub link share --- assets/translations/strings_en.i18n.json | 2 ++ assets/translations/strings_fa.i18n.json | 2 ++ assets/translations/strings_ru.i18n.json | 4 +++- assets/translations/strings_zh.i18n.json | 2 ++ lib/features/common/profile_tile.dart | 15 +++++++++++++++ lib/utils/link_parsers.dart | 13 +++++++++++++ 6 files changed, 37 insertions(+), 1 deletion(-) diff --git a/assets/translations/strings_en.i18n.json b/assets/translations/strings_en.i18n.json index 54bc6b2a..ee72647a 100644 --- a/assets/translations/strings_en.i18n.json +++ b/assets/translations/strings_en.i18n.json @@ -69,6 +69,8 @@ }, "share": { "buttonText": "Share", + "exportToClipboardSuccess": "Exported to clipboard", + "exportSubLinkToClipboard": "Export subscription link to clipboard", "exportConfigToClipboard": "Export configuration to clipboard", "exportConfigToClipboardSuccess": "Configuration copied to clipboard" }, diff --git a/assets/translations/strings_fa.i18n.json b/assets/translations/strings_fa.i18n.json index 7595fbad..65930e3f 100644 --- a/assets/translations/strings_fa.i18n.json +++ b/assets/translations/strings_fa.i18n.json @@ -69,6 +69,8 @@ }, "share": { "buttonText": "اشتراک گذاری", + "exportToClipboardSuccess": "به کلیپ بورد اضافه شد", + "exportSubLinkToClipboard": "افزودن لینک اشتراک به کلیپ بورد", "exportConfigToClipboard": "افزودن پیکربندی به کلیپ بورد", "exportConfigToClipboardSuccess": "پیکربندی در کلیپ بورد کپی شد" }, diff --git a/assets/translations/strings_ru.i18n.json b/assets/translations/strings_ru.i18n.json index 21d6b34a..01f8f7be 100644 --- a/assets/translations/strings_ru.i18n.json +++ b/assets/translations/strings_ru.i18n.json @@ -69,6 +69,8 @@ }, "share": { "buttonText": "Делиться", + "exportToClipboardSuccess": "Экспортировано в буфер обмена", + "exportSubLinkToClipboard": "Экспортировать ссылку на подписку в буфер обмена", "exportConfigToClipboard": "Экспортировать конфигурацию в буфер обмена", "exportConfigToClipboardSuccess": "Конфигурация скопирована в буфер обмена." }, @@ -280,4 +282,4 @@ "short_description": "Автовыбор, SSH, VLESS, Vmess, Trojan, Reality, Sing-Box, Clash, Xray, Shadowsocks", "full_description": "Основная цель HiddifyNext — предоставить безопасный, удобный и эффективный клиент туннелирования. Он позволяет направлять весь трафик или трафик выбранного приложения на выбранный вами удалённый сервер, используя разрешение VPN–сервиса.\n\nПримечание: мы не предоставляем серверы, пользователи должны обеспечивать конфиденциальность своих действий в Интернете, используя собственный сервер или доверенные серверы.\n \nПоддерживаемые серверы:\n— Обычная ссылка на подписку V2ray/Xray\n— Ссылка на подписку Clash\n— Ссылка на подписку на Sing–Box\n\nВ чём уникальные особенности?\n — Удобство\n — Оптимизация и скорость\n — Автоматический выбор минимальной задержки\n — Отображение информации об использовании\n — Простой импорт ссылок одним щелчком мыши\n — Бесплатно и без рекламы\n — Простое переключение ссылок\n — …и много больше\n\nПоддержка:\n• Все протоколы, поддерживаемые Sing-Box\n• VLESS + xtls reality, vision\n• VMESS\n• Trojan\n• ShoadowSocks\n• Reality\n• V2ray\n• Hystria2\n• TUIC\n• SSH\n• ShadowTLS\n\n\nИсходный код доступен по адресу https://github.com/hiddify/Hiddify-Next.\nЯдро приложения основано на открытом исходном коде Sing–Box.\n\nОписание разрешений:\n— СЛУЖБА VPN: поскольку целью данного приложения является предоставление безопасного, удобного и эффективного клиента туннелирования, это разрешение необходимо, чтобы иметь возможность направлять трафик через туннель на удалённый сервер.\n— ЗАПРОС ВСЕХ ПАКЕТОВ: это разрешение позволяет включать или исключать определённые приложения для туннелирования.\n— ИНФОРМИРОВАНИЕ О ЗАВЕРШЕНИИ ЗАГРУЗКИ: это разрешение можно включить или отключить в настройках приложения, чтобы (де)активировать запуск приложения при загрузке устройства.\n— ПОСТОЯННОЕ УВЕДОМЛЕНИЕ: это разрешение необходимо, поскольку используется приоритетная служба для обеспечения непрерывной работы службы VPN.\n— Приложение не содержит рекламы. Сбор аналитики и данных о сбоях происходят только с явного согласия пользователя при первом использовании приложения." } -} +} \ No newline at end of file diff --git a/assets/translations/strings_zh.i18n.json b/assets/translations/strings_zh.i18n.json index 6e3ced28..152dacf2 100644 --- a/assets/translations/strings_zh.i18n.json +++ b/assets/translations/strings_zh.i18n.json @@ -69,6 +69,8 @@ }, "share": { "buttonText": "分享", + "exportToClipboardSuccess": "导出到剪贴板", + "exportSubLinkToClipboard": "将订阅链接导出到剪贴板", "exportConfigToClipboard": "将配置导出到剪贴板", "exportConfigToClipboardSuccess": "配置已复制到剪贴板" }, diff --git a/lib/features/common/profile_tile.dart b/lib/features/common/profile_tile.dart index 8d0ba2ab..a56374ed 100644 --- a/lib/features/common/profile_tile.dart +++ b/lib/features/common/profile_tile.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; import 'package:hiddify/core/core_providers.dart'; @@ -288,6 +289,20 @@ class ProfileActionsMenu extends HookConsumerWidget { ), SubmenuButton( menuChildren: [ + if (profile case RemoteProfile(:final url, :final name)) + MenuItemButton( + child: Text(t.profile.share.exportSubLinkToClipboard), + onPressed: () async { + final link = LinkParser.generateSubShareLink(url, name); + if (link.isNotEmpty) { + await Clipboard.setData(ClipboardData(text: link)); + if (context.mounted) { + CustomToast(t.profile.share.exportToClipboardSuccess) + .show(context); + } + } + }, + ), MenuItemButton( child: Text(t.profile.share.exportConfigToClipboard), onPressed: () async { diff --git a/lib/utils/link_parsers.dart b/lib/utils/link_parsers.dart index 378ea4e3..d82b7aff 100644 --- a/lib/utils/link_parsers.dart +++ b/lib/utils/link_parsers.dart @@ -7,6 +7,19 @@ typedef ProfileLink = ({String url, String name}); // TODO: test and improve abstract class LinkParser { + static String generateSubShareLink(String url, [String? name]) { + final uri = Uri.tryParse(url); + if (uri == null) return ''; + return Uri( + scheme: 'hiddify', + host: 'install-sub', + queryParameters: { + "url": uri.toString(), + if (name != null) "name": name, + }, + ).toString(); + } + // protocols schemas static const protocols = {'clash', 'clashmeta', 'sing-box', 'hiddify'}; static const rawProtocols = { From 21cb2a697d0b5ec578d3baa57c5f6b8505ec9db1 Mon Sep 17 00:00:00 2001 From: Aloxaf Date: Mon, 13 Nov 2023 14:03:36 +0800 Subject: [PATCH 05/91] fix: build-linux-libs --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 9dbd4bbf..05a47450 100644 --- a/Makefile +++ b/Makefile @@ -114,7 +114,7 @@ build-windows-libs: make -C libcore -f Makefile windows-amd64 && mv $(BINDIR)/$(CORE_NAME)-windows-amd64.dll $(DESKTOP_OUT)/libcore.dll build-linux-libs: - make -C libcore -f Makefile linux-amd64 && mv $(BINDIR)/$(CORE_NAME)-linux-amd64.dll $(DESKTOP_OUT)/libcore.so + make -C libcore -f Makefile linux-amd64 && mv $(BINDIR)/$(CORE_NAME)-linux-amd64.so $(DESKTOP_OUT)/libcore.so build-macos-libs: make -C libcore -f Makefile macos-universal && mv $(BINDIR)/$(CORE_NAME)-macos-universal.dylib $(DESKTOP_OUT)/libcore.dylib From ca29a6ad4a292325590d6ec49100cb2d199cae68 Mon Sep 17 00:00:00 2001 From: solokot Date: Mon, 13 Nov 2023 09:20:45 +0300 Subject: [PATCH 06/91] Update strings_ru Fix google translate --- assets/translations/strings_ru.i18n.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/assets/translations/strings_ru.i18n.json b/assets/translations/strings_ru.i18n.json index 01f8f7be..0ef0601c 100644 --- a/assets/translations/strings_ru.i18n.json +++ b/assets/translations/strings_ru.i18n.json @@ -68,11 +68,11 @@ "successMsg": "Профиль успешно обновлён" }, "share": { - "buttonText": "Делиться", - "exportToClipboardSuccess": "Экспортировано в буфер обмена", - "exportSubLinkToClipboard": "Экспортировать ссылку на подписку в буфер обмена", - "exportConfigToClipboard": "Экспортировать конфигурацию в буфер обмена", - "exportConfigToClipboardSuccess": "Конфигурация скопирована в буфер обмена." + "buttonText": "Поделиться", + "exportToClipboardSuccess": "Ссылка скопирована в буфер обмена", + "exportSubLinkToClipboard": "Копировать ссылку на подписку в буфер обмена", + "exportConfigToClipboard": "Копировать конфигурацию в буфер обмена", + "exportConfigToClipboardSuccess": "Конфигурация скопирована в буфер обмена" }, "edit": { "buttonTxt": "Изменить", @@ -282,4 +282,4 @@ "short_description": "Автовыбор, SSH, VLESS, Vmess, Trojan, Reality, Sing-Box, Clash, Xray, Shadowsocks", "full_description": "Основная цель HiddifyNext — предоставить безопасный, удобный и эффективный клиент туннелирования. Он позволяет направлять весь трафик или трафик выбранного приложения на выбранный вами удалённый сервер, используя разрешение VPN–сервиса.\n\nПримечание: мы не предоставляем серверы, пользователи должны обеспечивать конфиденциальность своих действий в Интернете, используя собственный сервер или доверенные серверы.\n \nПоддерживаемые серверы:\n— Обычная ссылка на подписку V2ray/Xray\n— Ссылка на подписку Clash\n— Ссылка на подписку на Sing–Box\n\nВ чём уникальные особенности?\n — Удобство\n — Оптимизация и скорость\n — Автоматический выбор минимальной задержки\n — Отображение информации об использовании\n — Простой импорт ссылок одним щелчком мыши\n — Бесплатно и без рекламы\n — Простое переключение ссылок\n — …и много больше\n\nПоддержка:\n• Все протоколы, поддерживаемые Sing-Box\n• VLESS + xtls reality, vision\n• VMESS\n• Trojan\n• ShoadowSocks\n• Reality\n• V2ray\n• Hystria2\n• TUIC\n• SSH\n• ShadowTLS\n\n\nИсходный код доступен по адресу https://github.com/hiddify/Hiddify-Next.\nЯдро приложения основано на открытом исходном коде Sing–Box.\n\nОписание разрешений:\n— СЛУЖБА VPN: поскольку целью данного приложения является предоставление безопасного, удобного и эффективного клиента туннелирования, это разрешение необходимо, чтобы иметь возможность направлять трафик через туннель на удалённый сервер.\n— ЗАПРОС ВСЕХ ПАКЕТОВ: это разрешение позволяет включать или исключать определённые приложения для туннелирования.\n— ИНФОРМИРОВАНИЕ О ЗАВЕРШЕНИИ ЗАГРУЗКИ: это разрешение можно включить или отключить в настройках приложения, чтобы (де)активировать запуск приложения при загрузке устройства.\n— ПОСТОЯННОЕ УВЕДОМЛЕНИЕ: это разрешение необходимо, поскольку используется приоритетная служба для обеспечения непрерывной работы службы VPN.\n— Приложение не содержит рекламы. Сбор аналитики и данных о сбоях происходят только с явного согласия пользователя при первом использовании приложения." } -} \ No newline at end of file +} From 8320d98dfb6c5dd55e9cd1a3d59eae4dc3c6b600 Mon Sep 17 00:00:00 2001 From: jomertix <150632538+jomertix@users.noreply.github.com> Date: Mon, 13 Nov 2023 14:30:41 +0300 Subject: [PATCH 07/91] Update strings_ru.i18n.json Fix translate --- assets/translations/strings_ru.i18n.json | 88 ++++++++++++------------ 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/assets/translations/strings_ru.i18n.json b/assets/translations/strings_ru.i18n.json index 0ef0601c..0abf425c 100644 --- a/assets/translations/strings_ru.i18n.json +++ b/assets/translations/strings_ru.i18n.json @@ -10,11 +10,11 @@ "disable": "Отключить" }, "sort": "Сортировка", - "sortBy": "Сортировка", + "sortBy": "Сортировать по", "addToClipboard": "Копировать в буфер обмена" }, "intro": { - "termsAndPolicyCaution(rich)": "Продолжая, вы соглашаетесь с ${tap(@:about.termsAndConditions)}", + "termsAndPolicyCaution(rich)": "Продолжая, Вы соглашаетесь с ${tap(@:about.termsAndConditions)}", "start": "Начать" }, "home": { @@ -28,10 +28,10 @@ "connected": "Подключено" }, "stats": { - "traffic": "Скорость", - "trafficTotal": "Трафик", - "uplink": "Исходящий канал", - "downlink": "Входящий канал" + "traffic": "Реальный траффик", + "trafficTotal": "Траффик", + "uplink": "Скорость отправки", + "downlink": "Скорость загрузки" } }, "profile": { @@ -44,34 +44,34 @@ "traffic": "Трафик", "updatedTimeAgo": "Обновлено ${timeago}", "remainingDuration": "Ещё ${duration} дн.", - "remainingTrafficSemanticLabel": "${consumed} из ${total} использованного трафика.", + "remainingTrafficSemanticLabel": "Использовано ${consumed} трафика из ${total}.", "expired": "Истекло", "noTraffic": "Нет доступного трафика" }, "sortBy": { - "lastUpdate": "Последнее обновление", - "name": "По алфавиту" + "lastUpdate": "последнему обновлению", + "name": " алфавиту" }, "add": { "buttonText": "Новый профиль", "shortBtnTxt": "Новый профиль", "fromClipboard": "Добавить из буфера обмена", "scanQr": "Сканировать QR-код", - "manually": "Ручной ввод", + "manually": "Ввести вручную", "addingProfileMsg": "Добавление профиля", - "failureMsg": "Невозможно добавить профиль" + "failureMsg": "Не удалось добавить профиль" }, "update": { "buttonTxt": "Обновить", "tooltip": "Обновить профиль", - "failureMsg": "Ошибка обновления", + "failureMsg": "Не удалось обновить профиль", "successMsg": "Профиль успешно обновлён" }, "share": { "buttonText": "Поделиться", - "exportToClipboardSuccess": "Ссылка скопирована в буфер обмена", - "exportSubLinkToClipboard": "Копировать ссылку на подписку в буфер обмена", - "exportConfigToClipboard": "Копировать конфигурацию в буфер обмена", + "exportToClipboardSuccess": "Скопировано в буфер обмена", + "exportSubLinkToClipboard": "Скопировать ссылку на подписку в буфер обмена", + "exportConfigToClipboard": "Скопировать конфигурацию в буфер обмена", "exportConfigToClipboardSuccess": "Конфигурация скопирована в буфер обмена" }, "edit": { @@ -86,7 +86,7 @@ "save": { "buttonText": "Сохранить", "successMsg": "Профиль успешно сохранён", - "failureMsg": "Невозможно сохранить профиль" + "failureMsg": "Не удалось сохранить профиль" }, "detailsForm": { "nameLabel": "Имя", @@ -123,12 +123,12 @@ }, "settings": { "pageTitle": "Настройки", - "requiresRestartMsg": "Для применения перезапустите приложение.", + "requiresRestartMsg": "Чтобы применить изменения, перезапустите приложение", "general": { "sectionTitle": "Основные", "locale": "Язык", "region": "Регион", - "regionMsg": "Помогает установить параметры по умолчанию для обхода внутренних адресов.", + "regionMsg": "Помогает установить параметры по умолчанию для обхода внутренних адресов", "regions": { "ir": "Иран (ir)", "cn": "Китай (cn)", @@ -143,17 +143,17 @@ "black": "Чёрная тема" }, "enableAnalytics": "Сбор аналитики", - "enableAnalyticsMsg": "Сбор аналитических данных и отправка отчётов о сбоях для улучшения приложения.", + "enableAnalyticsMsg": "Сбор данных аналитики и отправка отчётов о сбоях для улучшения приложения", "autoStart": "Запуск при загрузке", "silentStart": "Тихий запуск", "openWorkingDir": "Открыть рабочую папку", "ignoreBatteryOptimizations": "Отключить оптимизацию батареи", - "ignoreBatteryOptimizationsMsg": "Отключение ограничений для оптимальной производительности VPN." + "ignoreBatteryOptimizationsMsg": "Отключение ограничений для оптимальной производительности VPN" }, "advanced": { "sectionTitle": "Расширенные", "debugMode": "Режим отладки", - "debugModeMsg": "Для применения перезапустите приложение.", + "debugModeMsg": "Чтобы применить изменения, перезапустите приложение", "memoryLimit": "Ограничение памяти" }, "network": { @@ -171,7 +171,7 @@ "clearSelection": "Очистить выбор" }, "config": { - "serviceMode": "Режим сервиса", + "serviceMode": "Режим работы", "serviceModes": { "proxy": "Прокси", "systemProxy": "Системный прокси", @@ -184,19 +184,19 @@ "misc": "Разные параметры" }, "pageTitle": "Параметры конфигурации", - "logLevel": "Подробность журналирования", + "logLevel": "Уровень логирования", "resolveDestination": "Определять назначение", "ipv6Mode": "Маршрутизация IPv6", "ipv6Modes": { "disable": "Отключено", "enable": "Включено", "prefer": "Предпочтительно", - "only": "Эксклюзивно" + "only": "Исключительно" }, - "remoteDnsAddress": "Удалённая DNS", - "remoteDnsDomainStrategy": "Стратегия удалённого домена DNS", - "directDnsAddress": "Прямая DNS", - "directDnsDomainStrategy": "Стратегия прямого домена DNS", + "remoteDnsAddress": "Remote DNS", + "remoteDnsDomainStrategy": "Remote DNS Domain Strategy", + "directDnsAddress": "Direct DNS", + "directDnsDomainStrategy": "Direct DNS Domain Strategy", "mixedPort": "Смешанный порт", "localDnsPort": "Локальный порт DNS", "tunImplementation": "Реализация TUN", @@ -207,7 +207,7 @@ "clashApiPort": "Порт Clash API", "enableTun": "Использовать TUN", "setSystemProxy": "Использовать системный прокси", - "enableFakeDns": "Использовать поддельную DNS", + "enableFakeDns": "Использовать поддельный DNS", "bypassLan": "Обход локальной сети", "strictRoute": "Строгая маршрутизация" } @@ -217,7 +217,7 @@ "version": "Версия", "sourceCode": "Исходный код", "telegramChannel": "Telegram-канал", - "checkForUpdate": "Проверка обновления", + "checkForUpdate": "Проверить обновления", "privacyPolicy": "Политика конфиденциальности", "termsAndConditions": "Условия и положения" }, @@ -242,44 +242,44 @@ } }, "failure": { - "unexpected": "Неожиданная ошибка", + "unexpected": "Непредвиденная ошибка", "clash": { - "unexpected": "Неожиданная ошибка (Clash)", - "core": "Ошибка ${reason}" + "unexpected": "Непредвиденная ошибка (Clash)", + "core": "Ошибка ${reason}" }, "singbox": { - "unexpected": "Неожиданная ошибка (SingBox)", + "unexpected": "Непредвиденная ошибка (SingBox)", "serviceNotRunning": "Сервис не запущен", + "missingPrivilege": "Нет прав", + "missingPrivilegeMsg": "Режим VPN требует прав администратора. Перезапустите приложение от имени администратора или измените режим работы приложения.", "invalidConfigOptions": "Неправильные параметры конфигурации", "invalidConfig": "Неправильная конфигурация", "create": "Ошибка создания сервиса", - "start": "Ошибка запуска сервиса", - "missingPrivilege": "Отсутствующие привилегии", - "missingPrivilegeMsg": "Режим VPN требует прав администратора. Либо перезапустите приложение от имени администратора, либо измените сервисный режим." + "start": "Ошибка запуска сервиса" }, "connectivity": { - "unexpected": "Неожиданная ошибка", + "unexpected": "Непредвиденная ошибка", "missingVpnPermission": "Отсутствует разрешение VPN", - "missingNotificationPermission": "Отсутствует разрешение на уведомление", + "missingNotificationPermission": "Отсутствует разрешение на показ уведомлений", "core": "Ошибка ядра" }, "profiles": { - "unexpected": "Неожиданная ошибка", + "unexpected": "Непредвиденная ошибка", "notFound": "Профиль не найден", "invalidConfig": "Неправильная конфигурация", "invalidUrl": "Неправильный URL" }, "connection": { - "unexpected": "Неожиданная ошибка подключения", - "timeout": "Истекло время подключения", + "unexpected": "Непредвиденная ошибка подключения", + "timeout": "Время подключения истекло", "badResponse": "Неправильный ответ", "connectionError": "Ошибка подключения", "badCertificate": "Неправильный сертификат" } }, "play": { - "title": "Hiddify Next (Preview)", + "title": "Hiddify Next (Бета)", "short_description": "Автовыбор, SSH, VLESS, Vmess, Trojan, Reality, Sing-Box, Clash, Xray, Shadowsocks", - "full_description": "Основная цель HiddifyNext — предоставить безопасный, удобный и эффективный клиент туннелирования. Он позволяет направлять весь трафик или трафик выбранного приложения на выбранный вами удалённый сервер, используя разрешение VPN–сервиса.\n\nПримечание: мы не предоставляем серверы, пользователи должны обеспечивать конфиденциальность своих действий в Интернете, используя собственный сервер или доверенные серверы.\n \nПоддерживаемые серверы:\n— Обычная ссылка на подписку V2ray/Xray\n— Ссылка на подписку Clash\n— Ссылка на подписку на Sing–Box\n\nВ чём уникальные особенности?\n — Удобство\n — Оптимизация и скорость\n — Автоматический выбор минимальной задержки\n — Отображение информации об использовании\n — Простой импорт ссылок одним щелчком мыши\n — Бесплатно и без рекламы\n — Простое переключение ссылок\n — …и много больше\n\nПоддержка:\n• Все протоколы, поддерживаемые Sing-Box\n• VLESS + xtls reality, vision\n• VMESS\n• Trojan\n• ShoadowSocks\n• Reality\n• V2ray\n• Hystria2\n• TUIC\n• SSH\n• ShadowTLS\n\n\nИсходный код доступен по адресу https://github.com/hiddify/Hiddify-Next.\nЯдро приложения основано на открытом исходном коде Sing–Box.\n\nОписание разрешений:\n— СЛУЖБА VPN: поскольку целью данного приложения является предоставление безопасного, удобного и эффективного клиента туннелирования, это разрешение необходимо, чтобы иметь возможность направлять трафик через туннель на удалённый сервер.\n— ЗАПРОС ВСЕХ ПАКЕТОВ: это разрешение позволяет включать или исключать определённые приложения для туннелирования.\n— ИНФОРМИРОВАНИЕ О ЗАВЕРШЕНИИ ЗАГРУЗКИ: это разрешение можно включить или отключить в настройках приложения, чтобы (де)активировать запуск приложения при загрузке устройства.\n— ПОСТОЯННОЕ УВЕДОМЛЕНИЕ: это разрешение необходимо, поскольку используется приоритетная служба для обеспечения непрерывной работы службы VPN.\n— Приложение не содержит рекламы. Сбор аналитики и данных о сбоях происходят только с явного согласия пользователя при первом использовании приложения." + "full_description": "Основная цель HiddifyNext — предоставить безопасный, удобный и эффективный клиент для туннелирования. Приложение позволяет направлять весь трафик или трафик выбранного приложения на указанный Вами удалённый сервер.\n\nПримечание: мы не предоставляем никаких серверов, пользователи сами должны обеспечивать конфиденциальность своих действий в Интернете, используя собственный сервер или доверенные серверы. \n\n Поддерживаются сервера с: \n— Обычной ссылкой на подписку V2ray/Xray \n— Ссылкой на подписку Clash \n— Ссылкой на подписку Sing–Box \n\n В чём наши преимущества? \n— Удобство \n— Оптимизация и скорость \n— Автоматический выбор на основе задержки \n— Отображение информации об использовании \n— Простой импорт ссылок в один клик \n— Бесплатно и без рекламы \n— Простое переключение ссылок \n— …и много больше \n\nПоддерживаются: \n• Все протоколы, поддерживаемые Sing-Box \n• VLESS + xtls reality, vision \n• VMESS \n• Trojan \n• ShoadowSocks \n• Reality \n• V2ray \n• Hystria2 \n• TUIC \n• SSH \n• ShadowTLS \n\nИсходный код доступен по ссылке https://github.com/hiddify/Hiddify-Next\nЯдро приложения основано на открытом исходном коде Sing–Box. \n\n Описание разрешений: \n— СЛУЖБА VPN: поскольку целью данного приложения является предоставление безопасного, удобного и эффективного клиента для туннелирования, это разрешение необходимо, чтобы иметь возможность направлять трафик через туннель на удалённый сервер. \n— ЗАПРОС ВСЕХ ПАКЕТОВ: это разрешение позволяет добавлять или удалять определённые приложения из списка для туннелирования. \n— ИНФОРМИРОВАНИЕ О ЗАВЕРШЕНИИ ЗАГРУЗКИ: это разрешение можно включить или отключить в настройках приложения, чтобы активировать или деактивировать соответственно запуск приложения при загрузке устройства. \n— ПОСТОЯННОЕ УВЕДОМЛЕНИЕ: оно необходимо, так как используется приоритетная служба для обеспечения непрерывной работы VPN подключения. \n— Приложение не содержит рекламы. Сбор аналитики и данных о сбоях происходят только с явного согласия пользователя при первом использовании приложения." } } From c06b3043d9c1e72b57e7ad9511e6ce57b857dfe1 Mon Sep 17 00:00:00 2001 From: jomertix <150632538+jomertix@users.noreply.github.com> Date: Mon, 13 Nov 2023 14:36:05 +0300 Subject: [PATCH 08/91] Update strings_ru.i18n.json Fix translation --- assets/translations/strings_ru.i18n.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/translations/strings_ru.i18n.json b/assets/translations/strings_ru.i18n.json index 0abf425c..05cf4238 100644 --- a/assets/translations/strings_ru.i18n.json +++ b/assets/translations/strings_ru.i18n.json @@ -28,7 +28,7 @@ "connected": "Подключено" }, "stats": { - "traffic": "Реальный траффик", + "traffic": "Текущий траффик", "trafficTotal": "Траффик", "uplink": "Скорость отправки", "downlink": "Скорость загрузки" From 14ff634038f8611be5f6209018e5b4ccd6222c12 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Mon, 13 Nov 2023 14:51:18 +0330 Subject: [PATCH 09/91] ci: ignore appcast --- .github/workflows/build.yml | 50 ++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6c85dd45..f1125903 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,12 +4,13 @@ on: branches: - main tags: - - "v*" + - 'v*' paths-ignore: - - "**.md" - - "docs/**" - - ".github/**" - - "!.github/workflows/build.yml" + - '**.md' + - 'docs/**' + - '.github/**' + - '!.github/workflows/build.yml' + - 'appcast.xml' # pull_request: # branches: # - main @@ -77,15 +78,15 @@ jobs: - name: Setup Flutter uses: subosito/flutter-action@v2 with: - flutter-version: "3.13.x" - channel: "stable" + flutter-version: '3.13.x' + channel: 'stable' cache: true - name: Setup Java if: startsWith(matrix.platform,'android') uses: actions/setup-java@v3 with: - distribution: "zulu" + distribution: 'zulu' java-version: 11 - name: Setup NDK @@ -305,7 +306,7 @@ jobs: uses: 8Mi-Tech/delete-release-assets-action@main with: github_token: ${{ secrets.GITHUB_TOKEN }} - tag: "draft" + tag: 'draft' deleteOnlyFromDrafts: false - name: Create or Update Draft Release @@ -315,8 +316,8 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: files: ./out/* - name: "draft" - tag_name: "draft" + name: 'draft' + tag_name: 'draft' prerelease: true upload-release: @@ -356,7 +357,7 @@ jobs: with: prerelease: ${{ env.CHANNEL == 'dev' }} tag_name: ${{ github.ref_name }} - body_path: "./release.md" + body_path: './release.md' files: ./out/* - name: Create service_account.json @@ -389,28 +390,27 @@ jobs: id: version uses: ashley-taylor/regex-property-action@v1.3 with: - value: "${{ github.ref_name }}" - regex: "^v|.dev$" - flags: "" # Optional, defaults to "g" - replacement: "" + value: '${{ github.ref_name }}' + regex: '^v|.dev$' + flags: '' # Optional, defaults to "g" + replacement: '' - name: Winget Publish if: ${{ env.CHANNEL != 'dev' }} uses: isaacrlevin/winget-publish-action@v.5 with: - publish-type: "Update" - user: "Hiddify" - package: "Next" + publish-type: 'Update' + user: 'Hiddify' + package: 'Next' version: ${{ steps.version.outputs.value }} - url: "https://github.com/hiddify/hiddify-next/releases/download/${{ github.ref_name }}/hiddify-windows-x64-setup.zip" + url: 'https://github.com/hiddify/hiddify-next/releases/download/${{ github.ref_name }}/hiddify-windows-x64-setup.zip' token: ${{ secrets.WINGET_TOKEN }} - - name: Winget Publish Beta uses: isaacrlevin/winget-publish-action@v.5 with: - publish-type: "Update" - user: "Hiddify" - package: "Next" + publish-type: 'Update' + user: 'Hiddify' + package: 'Next' version: ${{ steps.version.outputs.value }} - url: "https://github.com/hiddify/hiddify-next/releases/download/${{ github.ref_name }}/hiddify-windows-x64-setup.zip" + url: 'https://github.com/hiddify/hiddify-next/releases/download/${{ github.ref_name }}/hiddify-windows-x64-setup.zip' token: ${{ secrets.WINGET_TOKEN }} From 0a44e638d77d1bfae8054063dc3100c9bd07308a Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Mon, 13 Nov 2023 15:02:38 +0330 Subject: [PATCH 10/91] Fix code typo --- lib/services/singbox/ffi_singbox_service.dart | 31 +++++++++---------- .../singbox/mobile_singbox_service.dart | 4 +-- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/lib/services/singbox/ffi_singbox_service.dart b/lib/services/singbox/ffi_singbox_service.dart index 87124e55..62bcf6b7 100644 --- a/lib/services/singbox/ffi_singbox_service.dart +++ b/lib/services/singbox/ffi_singbox_service.dart @@ -5,7 +5,6 @@ import 'dart:io'; import 'dart:isolate'; import 'package:combine/combine.dart'; -import 'package:dartx/dartx.dart'; import 'package:ffi/ffi.dart'; import 'package:fpdart/fpdart.dart'; import 'package:hiddify/domain/connectivity/connectivity.dart'; @@ -28,8 +27,8 @@ class FFISingboxService late final ValueStream _connectionStatus; late final ReceivePort _connectionStatusReceiver; - Stream? _statusStream; - Stream? _groupsStream; + Stream? _serviceStatsStream; + Stream? _outboundsStream; static SingboxNativeLibrary _gen() { String fullPath = ""; @@ -152,7 +151,7 @@ class FFISingboxService .cast() .toDartString(); if (response.startsWith("error")) { - return left(response.removePrefix("error")); + return left(response.replaceFirst("error", "")); } return right(response); }, @@ -224,28 +223,28 @@ class FFISingboxService @override Stream watchStats() { - if (_statusStream != null) return _statusStream!; - final receiver = ReceivePort('status receiver'); + if (_serviceStatsStream != null) return _serviceStatsStream!; + final receiver = ReceivePort('service stats receiver'); final statusStream = receiver.asBroadcastStream( onCancel: (_) { - _logger.debug("stopping status command client"); + _logger.debug("stopping stats command client"); final err = _box.stopCommandClient(1).cast().toDartString(); if (err.isNotEmpty) { - _logger.error("error stopping status client"); + _logger.error("error stopping stats client"); } receiver.close(); - _statusStream = null; + _serviceStatsStream = null; }, ).map( (event) { if (event case String _) { if (event.startsWith('error:')) { - loggy.error("[status client] error received: $event"); + loggy.error("[service stats client] error received: $event"); throw event.replaceFirst('error:', ""); } return event; } - loggy.error("[status client] unexpected type, msg: $event"); + loggy.error("[service status client] unexpected type, msg: $event"); throw "invalid type"; }, ); @@ -259,14 +258,14 @@ class FFISingboxService throw err; } - return _statusStream = statusStream; + return _serviceStatsStream = statusStream; } @override Stream watchOutbounds() { - if (_groupsStream != null) return _groupsStream!; + if (_outboundsStream != null) return _outboundsStream!; final receiver = ReceivePort('outbounds receiver'); - final groupsStream = receiver.asBroadcastStream( + final outboundsStream = receiver.asBroadcastStream( onCancel: (_) { _logger.debug("stopping group command client"); final err = _box.stopCommandClient(4).cast().toDartString(); @@ -274,7 +273,7 @@ class FFISingboxService _logger.error("error stopping group client"); } receiver.close(); - _groupsStream = null; + _outboundsStream = null; }, ).map( (event) { @@ -299,7 +298,7 @@ class FFISingboxService throw err; } - return _groupsStream = groupsStream; + return _outboundsStream = outboundsStream; } @override diff --git a/lib/services/singbox/mobile_singbox_service.dart b/lib/services/singbox/mobile_singbox_service.dart index b27ec2ab..f4440772 100644 --- a/lib/services/singbox/mobile_singbox_service.dart +++ b/lib/services/singbox/mobile_singbox_service.dart @@ -13,7 +13,7 @@ class MobileSingboxService with ServiceStatus, InfraLogger implements SingboxService { late final _methodChannel = const MethodChannel("com.hiddify.app/method"); - late final _statusChannel = + late final _connectionStatusChannel = const EventChannel("com.hiddify.app/service.status"); late final _alertsChannel = const EventChannel("com.hiddify.app/service.alerts"); @@ -25,7 +25,7 @@ class MobileSingboxService Future init() async { loggy.debug("initializing"); final status = - _statusChannel.receiveBroadcastStream().map(mapEventToStatus); + _connectionStatusChannel.receiveBroadcastStream().map(mapEventToStatus); final alerts = _alertsChannel.receiveBroadcastStream().map(mapEventToStatus); _connectionStatus = From 25923259238f70c8624fea99f8bf9de79d9cc3a2 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Mon, 13 Nov 2023 17:49:10 +0330 Subject: [PATCH 11/91] Improve qr code scanner ux --- assets/translations/strings_en.i18n.json | 6 + assets/translations/strings_fa.i18n.json | 8 +- assets/translations/strings_ru.i18n.json | 10 +- assets/translations/strings_zh.i18n.json | 8 +- .../common/qr_code_scanner_screen.dart | 122 ++++++++++++++++-- 5 files changed, 136 insertions(+), 18 deletions(-) diff --git a/assets/translations/strings_en.i18n.json b/assets/translations/strings_en.i18n.json index ee72647a..b8f9a14f 100644 --- a/assets/translations/strings_en.i18n.json +++ b/assets/translations/strings_en.i18n.json @@ -57,6 +57,12 @@ "shortBtnTxt": "New Profile", "fromClipboard": "Add From Clipboard", "scanQr": "Scan QR code", + "qrScanner": { + "permissionDeniedError": "Permission denied", + "unexpectedError": "Something went wrong", + "torchSemanticLabel": "Flash light", + "facingSemanticLabel": "Camera facing" + }, "manually": "Manual Entry", "addingProfileMsg": "Adding Profile", "failureMsg": "Failed to add profile" diff --git a/assets/translations/strings_fa.i18n.json b/assets/translations/strings_fa.i18n.json index 65930e3f..1a612e0d 100644 --- a/assets/translations/strings_fa.i18n.json +++ b/assets/translations/strings_fa.i18n.json @@ -59,7 +59,13 @@ "scanQr": "اسکن QR کد", "manually": "افزودن دستی", "addingProfileMsg": "در حال افزودن پروفایل", - "failureMsg": "در افزودن پروفایل خطایی رخ داد" + "failureMsg": "در افزودن پروفایل خطایی رخ داد", + "qrScanner": { + "permissionDeniedError": "اجازه رد شد", + "unexpectedError": "خطایی رخ داده", + "torchSemanticLabel": "چراغ فلاش", + "facingSemanticLabel": "جهت دوربین" + } }, "update": { "buttonTxt": "بروزرسانی", diff --git a/assets/translations/strings_ru.i18n.json b/assets/translations/strings_ru.i18n.json index 05cf4238..b022bd41 100644 --- a/assets/translations/strings_ru.i18n.json +++ b/assets/translations/strings_ru.i18n.json @@ -59,7 +59,13 @@ "scanQr": "Сканировать QR-код", "manually": "Ввести вручную", "addingProfileMsg": "Добавление профиля", - "failureMsg": "Не удалось добавить профиль" + "failureMsg": "Не удалось добавить профиль", + "qrScanner": { + "permissionDeniedError": "Доступ запрещен", + "unexpectedError": "Что-то пошло не так", + "torchSemanticLabel": "Вспышка", + "facingSemanticLabel": "Перед камерой" + } }, "update": { "buttonTxt": "Обновить", @@ -282,4 +288,4 @@ "short_description": "Автовыбор, SSH, VLESS, Vmess, Trojan, Reality, Sing-Box, Clash, Xray, Shadowsocks", "full_description": "Основная цель HiddifyNext — предоставить безопасный, удобный и эффективный клиент для туннелирования. Приложение позволяет направлять весь трафик или трафик выбранного приложения на указанный Вами удалённый сервер.\n\nПримечание: мы не предоставляем никаких серверов, пользователи сами должны обеспечивать конфиденциальность своих действий в Интернете, используя собственный сервер или доверенные серверы. \n\n Поддерживаются сервера с: \n— Обычной ссылкой на подписку V2ray/Xray \n— Ссылкой на подписку Clash \n— Ссылкой на подписку Sing–Box \n\n В чём наши преимущества? \n— Удобство \n— Оптимизация и скорость \n— Автоматический выбор на основе задержки \n— Отображение информации об использовании \n— Простой импорт ссылок в один клик \n— Бесплатно и без рекламы \n— Простое переключение ссылок \n— …и много больше \n\nПоддерживаются: \n• Все протоколы, поддерживаемые Sing-Box \n• VLESS + xtls reality, vision \n• VMESS \n• Trojan \n• ShoadowSocks \n• Reality \n• V2ray \n• Hystria2 \n• TUIC \n• SSH \n• ShadowTLS \n\nИсходный код доступен по ссылке https://github.com/hiddify/Hiddify-Next\nЯдро приложения основано на открытом исходном коде Sing–Box. \n\n Описание разрешений: \n— СЛУЖБА VPN: поскольку целью данного приложения является предоставление безопасного, удобного и эффективного клиента для туннелирования, это разрешение необходимо, чтобы иметь возможность направлять трафик через туннель на удалённый сервер. \n— ЗАПРОС ВСЕХ ПАКЕТОВ: это разрешение позволяет добавлять или удалять определённые приложения из списка для туннелирования. \n— ИНФОРМИРОВАНИЕ О ЗАВЕРШЕНИИ ЗАГРУЗКИ: это разрешение можно включить или отключить в настройках приложения, чтобы активировать или деактивировать соответственно запуск приложения при загрузке устройства. \n— ПОСТОЯННОЕ УВЕДОМЛЕНИЕ: оно необходимо, так как используется приоритетная служба для обеспечения непрерывной работы VPN подключения. \n— Приложение не содержит рекламы. Сбор аналитики и данных о сбоях происходят только с явного согласия пользователя при первом использовании приложения." } -} +} \ No newline at end of file diff --git a/assets/translations/strings_zh.i18n.json b/assets/translations/strings_zh.i18n.json index 152dacf2..a468b31c 100644 --- a/assets/translations/strings_zh.i18n.json +++ b/assets/translations/strings_zh.i18n.json @@ -59,7 +59,13 @@ "scanQr": "扫二维码", "manually": "手动输入", "addingProfileMsg": "添加配置文件", - "failureMsg": "添加配置文件失败" + "failureMsg": "添加配置文件失败", + "qrScanner": { + "permissionDeniedError": "没有权限", + "unexpectedError": "出了些问题", + "torchSemanticLabel": "手电筒", + "facingSemanticLabel": "相机朝向" + } }, "update": { "buttonTxt": "更新", diff --git a/lib/features/common/qr_code_scanner_screen.dart b/lib/features/common/qr_code_scanner_screen.dart index d52175ca..14ef3071 100644 --- a/lib/features/common/qr_code_scanner_screen.dart +++ b/lib/features/common/qr_code_scanner_screen.dart @@ -1,5 +1,7 @@ +import 'package:dartx/dartx.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hiddify/core/core_providers.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; @@ -18,15 +20,17 @@ class QRCodeScannerScreen extends HookConsumerWidget with PresLogger { @override Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + final controller = useMemoized( - () => MobileScannerController( - detectionTimeoutMs: 500, - formats: [BarcodeFormat.qrCode], - ), + () => MobileScannerController(detectionTimeoutMs: 500), ); useEffect(() => controller.dispose, []); + final size = MediaQuery.sizeOf(context); + final overlaySize = (size.shortestSide - 12).coerceAtMost(248); + return Scaffold( extendBodyBehindAppBar: true, appBar: AppBar( @@ -48,6 +52,7 @@ class QRCodeScannerScreen extends HookConsumerWidget with PresLogger { } }, ), + tooltip: t.profile.add.qrScanner.torchSemanticLabel, onPressed: () => controller.toggleTorch(), ), IconButton( @@ -62,21 +67,110 @@ class QRCodeScannerScreen extends HookConsumerWidget with PresLogger { } }, ), + tooltip: t.profile.add.qrScanner.facingSemanticLabel, onPressed: () => controller.switchCamera(), ), ], ), - body: MobileScanner( - controller: controller, - onDetect: (capture) { - final data = capture.barcodes.first; - if (context.mounted && data.type == BarcodeType.url) { - loggy.debug('captured raw: [${data.rawValue}]'); - loggy.debug('captured url: [${data.url?.url}]'); - Navigator.of(context, rootNavigator: true).pop(data.url?.url); - } - }, + body: Stack( + children: [ + MobileScanner( + controller: controller, + onDetect: (capture) { + final data = capture.barcodes.first; + if (context.mounted && data.type == BarcodeType.url) { + loggy.debug('captured raw: [${data.rawValue}]'); + loggy.debug('captured url: [${data.url?.url}]'); + Navigator.of(context, rootNavigator: true).pop(data.url?.url); + } + }, + errorBuilder: (_, error, __) { + final message = switch (error.errorCode) { + MobileScannerErrorCode.permissionDenied => + t.profile.add.qrScanner.permissionDeniedError, + _ => t.profile.add.qrScanner.unexpectedError, + }; + + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Padding( + padding: EdgeInsets.only(bottom: 8), + child: Icon(Icons.error, color: Colors.white), + ), + Text(message), + Text(error.errorDetails?.message ?? ''), + ], + ), + ); + }, + ), + CustomPaint( + painter: ScannerOverlay( + Rect.fromCenter( + center: size.center(Offset.zero), + width: overlaySize, + height: overlaySize, + ), + ), + ), + ], ), ); } } + +class ScannerOverlay extends CustomPainter { + ScannerOverlay(this.scanWindow); + + final Rect scanWindow; + final double borderRadius = 12.0; + + @override + void paint(Canvas canvas, Size size) { + final backgroundPath = Path()..addRect(Rect.largest); + final cutoutPath = Path() + ..addRRect( + RRect.fromRectAndCorners( + scanWindow, + topLeft: Radius.circular(borderRadius), + topRight: Radius.circular(borderRadius), + bottomLeft: Radius.circular(borderRadius), + bottomRight: Radius.circular(borderRadius), + ), + ); + + final backgroundPaint = Paint() + ..color = Colors.black.withOpacity(0.5) + ..style = PaintingStyle.fill + ..blendMode = BlendMode.dstOut; + + final backgroundWithCutout = Path.combine( + PathOperation.difference, + backgroundPath, + cutoutPath, + ); + + final borderPaint = Paint() + ..color = Colors.white + ..style = PaintingStyle.stroke + ..strokeWidth = 3.0; + + final borderRect = RRect.fromRectAndCorners( + scanWindow, + topLeft: Radius.circular(borderRadius), + topRight: Radius.circular(borderRadius), + bottomLeft: Radius.circular(borderRadius), + bottomRight: Radius.circular(borderRadius), + ); + + canvas.drawPath(backgroundWithCutout, backgroundPaint); + canvas.drawRRect(borderRect, borderPaint); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return false; + } +} From beec48c8b41316d0b47458699f973a66e945dc06 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Mon, 13 Nov 2023 17:55:47 +0330 Subject: [PATCH 12/91] Add subscription qr code share --- assets/translations/strings_en.i18n.json | 1 + assets/translations/strings_fa.i18n.json | 3 +- assets/translations/strings_ru.i18n.json | 3 +- assets/translations/strings_zh.i18n.json | 3 +- lib/features/common/profile_tile.dart | 16 ++++++- lib/features/common/qr_code_dialog.dart | 61 ++++++++++++++++++++++++ pubspec.lock | 16 +++++++ pubspec.yaml | 1 + 8 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 lib/features/common/qr_code_dialog.dart diff --git a/assets/translations/strings_en.i18n.json b/assets/translations/strings_en.i18n.json index b8f9a14f..403d6d36 100644 --- a/assets/translations/strings_en.i18n.json +++ b/assets/translations/strings_en.i18n.json @@ -77,6 +77,7 @@ "buttonText": "Share", "exportToClipboardSuccess": "Exported to clipboard", "exportSubLinkToClipboard": "Export subscription link to clipboard", + "subLinkQrCode": "Subscription link QR code", "exportConfigToClipboard": "Export configuration to clipboard", "exportConfigToClipboardSuccess": "Configuration copied to clipboard" }, diff --git a/assets/translations/strings_fa.i18n.json b/assets/translations/strings_fa.i18n.json index 1a612e0d..58236996 100644 --- a/assets/translations/strings_fa.i18n.json +++ b/assets/translations/strings_fa.i18n.json @@ -78,7 +78,8 @@ "exportToClipboardSuccess": "به کلیپ بورد اضافه شد", "exportSubLinkToClipboard": "افزودن لینک اشتراک به کلیپ بورد", "exportConfigToClipboard": "افزودن پیکربندی به کلیپ بورد", - "exportConfigToClipboardSuccess": "پیکربندی در کلیپ بورد کپی شد" + "exportConfigToClipboardSuccess": "پیکربندی در کلیپ بورد کپی شد", + "subLinkQrCode": "کد QR لینک اشتراک" }, "edit": { "buttonTxt": "ویرایش", diff --git a/assets/translations/strings_ru.i18n.json b/assets/translations/strings_ru.i18n.json index b022bd41..069ddabe 100644 --- a/assets/translations/strings_ru.i18n.json +++ b/assets/translations/strings_ru.i18n.json @@ -78,7 +78,8 @@ "exportToClipboardSuccess": "Скопировано в буфер обмена", "exportSubLinkToClipboard": "Скопировать ссылку на подписку в буфер обмена", "exportConfigToClipboard": "Скопировать конфигурацию в буфер обмена", - "exportConfigToClipboardSuccess": "Конфигурация скопирована в буфер обмена" + "exportConfigToClipboardSuccess": "Конфигурация скопирована в буфер обмена", + "subLinkQrCode": "QR-код ссылки на подписку" }, "edit": { "buttonTxt": "Изменить", diff --git a/assets/translations/strings_zh.i18n.json b/assets/translations/strings_zh.i18n.json index a468b31c..590142f6 100644 --- a/assets/translations/strings_zh.i18n.json +++ b/assets/translations/strings_zh.i18n.json @@ -78,7 +78,8 @@ "exportToClipboardSuccess": "导出到剪贴板", "exportSubLinkToClipboard": "将订阅链接导出到剪贴板", "exportConfigToClipboard": "将配置导出到剪贴板", - "exportConfigToClipboardSuccess": "配置已复制到剪贴板" + "exportConfigToClipboardSuccess": "配置已复制到剪贴板", + "subLinkQrCode": "订阅链接二维码" }, "edit": { "buttonTxt": "编辑", diff --git a/lib/features/common/profile_tile.dart b/lib/features/common/profile_tile.dart index a56374ed..c0c7781f 100644 --- a/lib/features/common/profile_tile.dart +++ b/lib/features/common/profile_tile.dart @@ -9,6 +9,7 @@ import 'package:hiddify/core/router/router.dart'; import 'package:hiddify/domain/failures.dart'; import 'package:hiddify/domain/profiles/profiles.dart'; import 'package:hiddify/features/common/confirmation_dialogs.dart'; +import 'package:hiddify/features/common/qr_code_dialog.dart'; import 'package:hiddify/features/profiles/notifier/notifier.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -289,7 +290,7 @@ class ProfileActionsMenu extends HookConsumerWidget { ), SubmenuButton( menuChildren: [ - if (profile case RemoteProfile(:final url, :final name)) + if (profile case RemoteProfile(:final url, :final name)) ...[ MenuItemButton( child: Text(t.profile.share.exportSubLinkToClipboard), onPressed: () async { @@ -303,6 +304,19 @@ class ProfileActionsMenu extends HookConsumerWidget { } }, ), + MenuItemButton( + child: Text(t.profile.share.subLinkQrCode), + onPressed: () async { + final link = LinkParser.generateSubShareLink(url, name); + if (link.isNotEmpty) { + await QrCodeDialog( + link, + message: name, + ).show(context); + } + }, + ), + ], MenuItemButton( child: Text(t.profile.share.exportConfigToClipboard), onPressed: () async { diff --git a/lib/features/common/qr_code_dialog.dart b/lib/features/common/qr_code_dialog.dart new file mode 100644 index 00000000..cc6da442 --- /dev/null +++ b/lib/features/common/qr_code_dialog.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:qr_flutter/qr_flutter.dart'; + +class QrCodeDialog extends StatelessWidget { + const QrCodeDialog( + this.data, { + super.key, + this.message, + this.width = 268, + this.backgroundColor = Colors.white, + }); + + final String data; + final String? message; + final double width; + final Color backgroundColor; + + Future show(BuildContext context) async { + await showDialog(context: context, builder: (context) => this); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: width, + child: QrImageView( + data: data, + backgroundColor: backgroundColor, + ), + ), + if (message != null) + SizedBox( + width: width, + child: Material( + color: theme.colorScheme.background, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + child: Text( + message!, + overflow: TextOverflow.ellipsis, + style: TextStyle(color: theme.colorScheme.onBackground), + ), + ), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 2df07782..c9b8b06a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1013,6 +1013,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.3" + qr: + dependency: transitive + description: + name: qr + sha256: "64957a3930367bf97cc211a5af99551d630f2f4625e38af10edd6b19131b64b3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + qr_flutter: + dependency: "direct main" + description: + name: qr_flutter + sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097" + url: "https://pub.dev" + source: hosted + version: "4.1.0" quiver: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index aa4a95b7..0480cab6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -70,6 +70,7 @@ dependencies: version: ^3.0.2 posix: ^5.0.0 win32: ^5.0.9 + qr_flutter: ^4.1.0 dev_dependencies: flutter_test: From 06699f9b205f616112e807248961475e59189bf4 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Mon, 13 Nov 2023 19:08:26 +0330 Subject: [PATCH 13/91] Fix release command --- Makefile | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 05a47450..333c0284 100644 --- a/Makefile +++ b/Makefile @@ -133,12 +133,10 @@ release: # Create a new tag for release. VERSION_STR="$${VERSION_ARRAY[0]}.$${VERSION_ARRAY[1]}.$${VERSION_ARRAY[2]}" && \ BUILD_NUMBER=$$(( $${VERSION_ARRAY[0]} * 10000 + $${VERSION_ARRAY[1]} * 100 + $${VERSION_ARRAY[2]} )) && \ echo "version: $${VERSION_STR}+$${BUILD_NUMBER}" && \ - sed -i "s/version: .*/version: $${VERSION_STR}\+$${BUILD_NUMBER}/g" pubspec.yaml && \ + sed -i "s/^version: .*/version: $${VERSION_STR}\+$${BUILD_NUMBER}/g" pubspec.yaml && \ git tag $${TAG} > /dev/null && \ git tag -d $${TAG} > /dev/null && \ git add pubspec.yaml CHANGELOG.md && \ - make sync_translate && \ - git add assets/translations/* && \ git commit -m "release: version $${TAG}" && \ echo "creating git tag : v$${TAG}" && \ git tag v$${TAG} && \ From b61768e049bab2592d5368459ab8702681346aab Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Mon, 13 Nov 2023 19:08:40 +0330 Subject: [PATCH 14/91] release: version 0.10.8.dev --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 0480cab6..9a3e918c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: hiddify description: A Proxy Frontend. publish_to: "none" -version: 0.10.7+1007 +version: 0.10.8+1008 environment: sdk: ">=3.1.0 <4.0.0" From 1d359740797e87da03ba979b0a1a3b68eea50b58 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Mon, 13 Nov 2023 19:28:41 +0330 Subject: [PATCH 15/91] Update appcast --- appcast.xml | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/appcast.xml b/appcast.xml index 8ccbcb8d..ad9eee41 100644 --- a/appcast.xml +++ b/appcast.xml @@ -3,31 +3,31 @@ Release - Version 0.10.0 - Sat, 28 Oct 2023 12:00:00 +0000 + Version 0.10.8 + Mon, 13 Nov 2023 19:00:00 +0000 + sparkle:version="0.10.8" sparkle:os="android" /> - Version 0.10.0 - Sat, 28 Oct 2023 12:00:00 +0000 + Version 0.10.8 + Mon, 13 Nov 2023 19:00:00 +0000 + url="https://github.com/hiddify/hiddify-next/releases/download/v0.10.8.dev/hiddify-windows-x64-setup.zip" + sparkle:version="0.10.8" sparkle:os="windows" /> - Version 0.10.0 - Sat, 28 Oct 2023 12:00:00 +0000 + Version 0.10.8 + Mon, 13 Nov 2023 19:00:00 +0000 + url="https://github.com/hiddify/hiddify-next/releases/download/v0.10.8.dev/hiddify-macos-universal.zip" + sparkle:version="0.10.8" sparkle:os="macos" /> - Version 0.10.0 - Sat, 28 Oct 2023 12:00:00 +0000 + Version 0.10.8 + Mon, 13 Nov 2023 19:00:00 +0000 + url="https://github.com/hiddify/hiddify-next/releases/download/v0.10.8.dev/hiddify-linux-x64.zip" + sparkle:version="0.10.8" sparkle:os="linux" /> \ No newline at end of file From bd0e466d0cc4c5ac2a2799bd01818e361f0f42cd Mon Sep 17 00:00:00 2001 From: huajizhige <120686495+huajizhige@users.noreply.github.com> Date: Tue, 14 Nov 2023 07:24:36 +0800 Subject: [PATCH 16/91] inlang: update translations --- assets/translations/strings_fa.i18n.json | 12 ++++++------ assets/translations/strings_ru.i18n.json | 12 ++++++------ assets/translations/strings_zh.i18n.json | 22 +++++++++++----------- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/assets/translations/strings_fa.i18n.json b/assets/translations/strings_fa.i18n.json index 58236996..ffafbc34 100644 --- a/assets/translations/strings_fa.i18n.json +++ b/assets/translations/strings_fa.i18n.json @@ -57,15 +57,15 @@ "shortBtnTxt": "افزودن", "fromClipboard": "افزودن از کلیپ‌بورد", "scanQr": "اسکن QR کد", - "manually": "افزودن دستی", - "addingProfileMsg": "در حال افزودن پروفایل", - "failureMsg": "در افزودن پروفایل خطایی رخ داد", "qrScanner": { "permissionDeniedError": "اجازه رد شد", "unexpectedError": "خطایی رخ داده", "torchSemanticLabel": "چراغ فلاش", "facingSemanticLabel": "جهت دوربین" - } + }, + "manually": "افزودن دستی", + "addingProfileMsg": "در حال افزودن پروفایل", + "failureMsg": "در افزودن پروفایل خطایی رخ داد" }, "update": { "buttonTxt": "بروزرسانی", @@ -77,9 +77,9 @@ "buttonText": "اشتراک گذاری", "exportToClipboardSuccess": "به کلیپ بورد اضافه شد", "exportSubLinkToClipboard": "افزودن لینک اشتراک به کلیپ بورد", + "subLinkQrCode": "کد QR لینک اشتراک", "exportConfigToClipboard": "افزودن پیکربندی به کلیپ بورد", - "exportConfigToClipboardSuccess": "پیکربندی در کلیپ بورد کپی شد", - "subLinkQrCode": "کد QR لینک اشتراک" + "exportConfigToClipboardSuccess": "پیکربندی در کلیپ بورد کپی شد" }, "edit": { "buttonTxt": "ویرایش", diff --git a/assets/translations/strings_ru.i18n.json b/assets/translations/strings_ru.i18n.json index 069ddabe..cca67955 100644 --- a/assets/translations/strings_ru.i18n.json +++ b/assets/translations/strings_ru.i18n.json @@ -57,15 +57,15 @@ "shortBtnTxt": "Новый профиль", "fromClipboard": "Добавить из буфера обмена", "scanQr": "Сканировать QR-код", - "manually": "Ввести вручную", - "addingProfileMsg": "Добавление профиля", - "failureMsg": "Не удалось добавить профиль", "qrScanner": { "permissionDeniedError": "Доступ запрещен", "unexpectedError": "Что-то пошло не так", "torchSemanticLabel": "Вспышка", "facingSemanticLabel": "Перед камерой" - } + }, + "manually": "Ввести вручную", + "addingProfileMsg": "Добавление профиля", + "failureMsg": "Не удалось добавить профиль" }, "update": { "buttonTxt": "Обновить", @@ -77,9 +77,9 @@ "buttonText": "Поделиться", "exportToClipboardSuccess": "Скопировано в буфер обмена", "exportSubLinkToClipboard": "Скопировать ссылку на подписку в буфер обмена", + "subLinkQrCode": "QR-код ссылки на подписку", "exportConfigToClipboard": "Скопировать конфигурацию в буфер обмена", - "exportConfigToClipboardSuccess": "Конфигурация скопирована в буфер обмена", - "subLinkQrCode": "QR-код ссылки на подписку" + "exportConfigToClipboardSuccess": "Конфигурация скопирована в буфер обмена" }, "edit": { "buttonTxt": "Изменить", diff --git a/assets/translations/strings_zh.i18n.json b/assets/translations/strings_zh.i18n.json index 590142f6..d358e9f8 100644 --- a/assets/translations/strings_zh.i18n.json +++ b/assets/translations/strings_zh.i18n.json @@ -57,15 +57,15 @@ "shortBtnTxt": "新的配置文件", "fromClipboard": "从剪贴板添加", "scanQr": "扫二维码", - "manually": "手动输入", - "addingProfileMsg": "添加配置文件", - "failureMsg": "添加配置文件失败", "qrScanner": { - "permissionDeniedError": "没有权限", + "permissionDeniedError": "权限不足", "unexpectedError": "出了些问题", "torchSemanticLabel": "手电筒", "facingSemanticLabel": "相机朝向" - } + }, + "manually": "手动输入", + "addingProfileMsg": "添加配置文件", + "failureMsg": "添加配置文件失败" }, "update": { "buttonTxt": "更新", @@ -77,9 +77,9 @@ "buttonText": "分享", "exportToClipboardSuccess": "导出到剪贴板", "exportSubLinkToClipboard": "将订阅链接导出到剪贴板", + "subLinkQrCode": "订阅链接二维码", "exportConfigToClipboard": "将配置导出到剪贴板", - "exportConfigToClipboardSuccess": "配置已复制到剪贴板", - "subLinkQrCode": "订阅链接二维码" + "exportConfigToClipboardSuccess": "配置已复制到剪贴板" }, "edit": { "buttonTxt": "编辑", @@ -180,7 +180,7 @@ "config": { "serviceMode": "服务方式", "serviceModes": { - "proxy": "代理人", + "proxy": "仅代理", "systemProxy": "系统代理", "tun": "VPN" }, @@ -257,12 +257,12 @@ "singbox": { "unexpected": "意外服务错误", "serviceNotRunning": "服务未运行", + "missingPrivilege": "缺少特权", + "missingPrivilegeMsg": "VPN 模式需要管理员权限。以管理员身份重新启动应用程序或更改服务模式", "invalidConfigOptions": "配置选项无效", "invalidConfig": "无效配置", "create": "服务创建错误", - "start": "服务启动错误", - "missingPrivilege": "缺少特权", - "missingPrivilegeMsg": "VPN 模式需要管理员权限。以管理员身份重新启动应用程序或更改服务模式" + "start": "服务启动错误" }, "connectivity": { "unexpected": "意外失败", From 66cb17872fbcf9ec1c6cf6186187d40a486b4fc7 Mon Sep 17 00:00:00 2001 From: huajizhige <120686495+huajizhige@users.noreply.github.com> Date: Tue, 14 Nov 2023 07:25:49 +0800 Subject: [PATCH 17/91] inlang: update translations --- assets/translations/strings_zh.i18n.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/translations/strings_zh.i18n.json b/assets/translations/strings_zh.i18n.json index d358e9f8..e5d0a1cd 100644 --- a/assets/translations/strings_zh.i18n.json +++ b/assets/translations/strings_zh.i18n.json @@ -216,7 +216,7 @@ "setSystemProxy": "设置系统代理", "enableFakeDns": "Enable Fake DNS", "bypassLan": "Bypass Lan", - "strictRoute": "Strict Route" + "strictRoute": "严格路由" } }, "about": { From d2f8a467d38d5cd93e81074d48694789f14f1f42 Mon Sep 17 00:00:00 2001 From: huajizhige <120686495+huajizhige@users.noreply.github.com> Date: Tue, 14 Nov 2023 07:35:26 +0800 Subject: [PATCH 18/91] inlang: update translations --- assets/translations/strings_zh.i18n.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/translations/strings_zh.i18n.json b/assets/translations/strings_zh.i18n.json index e5d0a1cd..23325063 100644 --- a/assets/translations/strings_zh.i18n.json +++ b/assets/translations/strings_zh.i18n.json @@ -214,7 +214,7 @@ "clashApiPort": "Clash API 端口", "enableTun": "启用 TUN", "setSystemProxy": "设置系统代理", - "enableFakeDns": "Enable Fake DNS", + "enableFakeDns": "启用 Fake DNS", "bypassLan": "Bypass Lan", "strictRoute": "严格路由" } From 9e441c2ee14ad52950d48b3fd4d23fd274616d96 Mon Sep 17 00:00:00 2001 From: huajizhige <120686495+huajizhige@users.noreply.github.com> Date: Tue, 14 Nov 2023 07:51:19 +0800 Subject: [PATCH 19/91] inlang: update translations --- assets/translations/strings_zh.i18n.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/translations/strings_zh.i18n.json b/assets/translations/strings_zh.i18n.json index 23325063..daa6e0d5 100644 --- a/assets/translations/strings_zh.i18n.json +++ b/assets/translations/strings_zh.i18n.json @@ -215,7 +215,7 @@ "enableTun": "启用 TUN", "setSystemProxy": "设置系统代理", "enableFakeDns": "启用 Fake DNS", - "bypassLan": "Bypass Lan", + "bypassLan": "绕过局域网", "strictRoute": "严格路由" } }, From 277c0f1b99583f60bc003dfdbe6a4b5b047df285 Mon Sep 17 00:00:00 2001 From: lymanjre <125398461+lymanjre@users.noreply.github.com> Date: Tue, 14 Nov 2023 10:17:39 +0330 Subject: [PATCH 20/91] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fa5b6f06..fb4883e7 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@
-[![GP-Intalls](https://img.shields.io/endpoint?color=green&logo=google-play&logoColor=green&url=https%3A%2F%2Fplay.cuzi.workers.dev%2Fplay%3Fi%3Dapp.hiddify.com%26l%3DGoogle%2520Play%26m%3D%24shortinstalls&style=flat-square)](https://play.google.com/store/apps/details?id=app.hiddify.com) [![Downloads](https://img.shields.io/github/downloads/hiddify/hiddify-next/total?style=flat-square&logo=github)](https://github.com/hiddify/hiddify-next/releases/)[![Last Version](https://img.shields.io/github/release/hiddify/hiddify-next/all.svg?style=flat-square)](https://github.com/hiddify/hiddify-next/)[![Last Release Date](https://img.shields.io/github/release-date/hiddify/hiddify-next.svg?style=flat-square)](https://github.com/hiddify/hiddify-next/)[![commits](https://img.shields.io/github/commit-activity/m/hiddify/hiddify-next?style=flat-square)](https://github.com/hiddify/hiddify-next/) +[![GP-Intalls](https://img.shields.io/endpoint?color=green&logo=google-play&logoColor=green&url=https%3A%2F%2Fplay.cuzi.workers.dev%2Fplay%3Fi%3Dapp.hiddify.com%26l%3DGoogle%2520Play%26m%3D%24shortinstalls&style=flat-square)](https://play.google.com/store/apps/details?id=app.hiddify.com) [![Downloads](https://img.shields.io/github/downloads/hiddify/hiddify-next/total?style=flat-square&logo=github)](https://github.com/hiddify/hiddify-next/releases/)[![Last Version](https://img.shields.io/github/release/hiddify/hiddify-next/all.svg?style=flat-square)](https://github.com/hiddify/hiddify-next/releases/)[![Last Release Date](https://img.shields.io/github/release-date/hiddify/hiddify-next.svg?style=flat-square)](https://github.com/hiddify/hiddify-next/releases/)[![commits](https://img.shields.io/github/commit-activity/m/hiddify/hiddify-next?style=flat-square)](https://github.com/hiddify/hiddify-next/) [![Youtube](https://img.shields.io/youtube/channel/views/UCxrmeMvVryNfB4XL35lXQNg?label=Youtube&style=flat-square&logo=youtube)](https://www.youtube.com/@hiddify)[![Telegram Channel](https://img.shields.io/endpoint?label=Channel&style=flat-square&url=https%3A%2F%2Ftg.sumanjay.workers.dev%2Fhiddify&color=blue)](https://telegram.dog/hiddify)[![Telegram Group](https://img.shields.io/endpoint?color=neon&label=Support%20Group&style=flat-square&url=https%3A%2F%2Ftg.sumanjay.workers.dev%2Fhiddify_board)](https://telegram.dog/hiddify_board/5)
From 95089b5ea3e7b293684a26a2add5312bc241b6d8 Mon Sep 17 00:00:00 2001 From: lymanjre <125398461+lymanjre@users.noreply.github.com> Date: Tue, 14 Nov 2023 10:18:55 +0330 Subject: [PATCH 21/91] Update README_fa.md --- README_fa.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README_fa.md b/README_fa.md index 060fd675..51309f22 100644 --- a/README_fa.md +++ b/README_fa.md @@ -7,10 +7,9 @@

- - -[![GP-Intalls](https://img.shields.io/endpoint?color=green&logo=google-play&logoColor=green&url=https%3A%2F%2Fplay.cuzi.workers.dev%2Fplay%3Fi%3Dapp.hiddify.com%26l%3DGoogle%2520Play%26m%3D%24shortinstalls&style=flat-square)](https://play.google.com/store/apps/details?id=app.hiddify.com) [![Downloads](https://img.shields.io/github/downloads/hiddify/hiddify-next/total?style=flat-square&logo=github)](https://github.com/hiddify/hiddify-next/releases/)[![Last Version](https://img.shields.io/github/release/hiddify/hiddify-next/all.svg?style=flat-square)](https://github.com/hiddify/hiddify-next/)[![Last Release Date](https://img.shields.io/github/release-date/hiddify/hiddify-next.svg?style=flat-square)](https://github.com/hiddify/hiddify-next/)[![commits](https://img.shields.io/github/commit-activity/m/hiddify/hiddify-next?style=flat-square)](https://github.com/hiddify/hiddify-next/) -[![Youtube](https://img.shields.io/youtube/channel/views/UCxrmeMvVryNfB4XL35lXQNg?label=Youtube&style=flat-square&logo=youtube)](https://www.youtube.com/@hiddify)[![Telegram Channel](https://img.shields.io/endpoint?label=Channel&style=flat-square&url=https%3A%2F%2Ftg.sumanjay.workers.dev%2Fhiddify&color=blue)](https://telegram.dog/hiddify)[![Telegram Group](https://img.shields.io/endpoint?color=neon&label=Support%20Group&style=flat-square&url=https%3A%2F%2Ftg.sumanjay.workers.dev%2Fhiddify_board)](https://telegram.dog/hiddify_board) + +[![GP-Intalls](https://img.shields.io/endpoint?color=green&logo=google-play&logoColor=green&url=https%3A%2F%2Fplay.cuzi.workers.dev%2Fplay%3Fi%3Dapp.hiddify.com%26l%3DGoogle%2520Play%26m%3D%24shortinstalls&style=flat-square)](https://play.google.com/store/apps/details?id=app.hiddify.com) [![Downloads](https://img.shields.io/github/downloads/hiddify/hiddify-next/total?style=flat-square&logo=github)](https://github.com/hiddify/hiddify-next/releases/)[![Last Version](https://img.shields.io/github/release/hiddify/hiddify-next/all.svg?style=flat-square)](https://github.com/hiddify/hiddify-next/releases/)[![Last Release Date](https://img.shields.io/github/release-date/hiddify/hiddify-next.svg?style=flat-square)](https://github.com/hiddify/hiddify-next/releases/)[![commits](https://img.shields.io/github/commit-activity/m/hiddify/hiddify-next?style=flat-square)](https://github.com/hiddify/hiddify-next/) +[![Youtube](https://img.shields.io/youtube/channel/views/UCxrmeMvVryNfB4XL35lXQNg?label=Youtube&style=flat-square&logo=youtube)](https://www.youtube.com/@hiddify)[![Telegram Channel](https://img.shields.io/endpoint?label=Channel&style=flat-square&url=https%3A%2F%2Ftg.sumanjay.workers.dev%2Fhiddify&color=blue)](https://telegram.dog/hiddify)[![Telegram Group](https://img.shields.io/endpoint?color=neon&label=Support%20Group&style=flat-square&url=https%3A%2F%2Ftg.sumanjay.workers.dev%2Fhiddify_board)](https://telegram.dog/hiddify_board/5)
From 3fdea345183065ca0ef6972a29f8a1e37411d01a Mon Sep 17 00:00:00 2001 From: lymanjre <125398461+lymanjre@users.noreply.github.com> Date: Tue, 14 Nov 2023 10:19:07 +0330 Subject: [PATCH 22/91] Update README_cn.md --- README_cn.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_cn.md b/README_cn.md index 9af45c8c..0e8ccfc5 100644 --- a/README_cn.md +++ b/README_cn.md @@ -7,7 +7,7 @@
-[![GP-Intalls](https://img.shields.io/endpoint?color=green&logo=google-play&logoColor=green&url=https%3A%2F%2Fplay.cuzi.workers.dev%2Fplay%3Fi%3Dapp.hiddify.com%26l%3DGoogle%2520Play%26m%3D%24shortinstalls&style=flat-square)](https://play.google.com/store/apps/details?id=app.hiddify.com) [![Downloads](https://img.shields.io/github/downloads/hiddify/hiddify-next/total?style=flat-square&logo=github)](https://github.com/hiddify/hiddify-next/releases/)[![Last Version](https://img.shields.io/github/release/hiddify/hiddify-next/all.svg?style=flat-square)](https://github.com/hiddify/hiddify-next/)[![Last Release Date](https://img.shields.io/github/release-date/hiddify/hiddify-next.svg?style=flat-square)](https://github.com/hiddify/hiddify-next/)[![commits](https://img.shields.io/github/commit-activity/m/hiddify/hiddify-next?style=flat-square)](https://github.com/hiddify/hiddify-next/) +[![GP-Intalls](https://img.shields.io/endpoint?color=green&logo=google-play&logoColor=green&url=https%3A%2F%2Fplay.cuzi.workers.dev%2Fplay%3Fi%3Dapp.hiddify.com%26l%3DGoogle%2520Play%26m%3D%24shortinstalls&style=flat-square)](https://play.google.com/store/apps/details?id=app.hiddify.com) [![Downloads](https://img.shields.io/github/downloads/hiddify/hiddify-next/total?style=flat-square&logo=github)](https://github.com/hiddify/hiddify-next/releases/)[![Last Version](https://img.shields.io/github/release/hiddify/hiddify-next/all.svg?style=flat-square)](https://github.com/hiddify/hiddify-next/releases/)[![Last Release Date](https://img.shields.io/github/release-date/hiddify/hiddify-next.svg?style=flat-square)](https://github.com/hiddify/hiddify-next/releases/)[![commits](https://img.shields.io/github/commit-activity/m/hiddify/hiddify-next?style=flat-square)](https://github.com/hiddify/hiddify-next/) [![Youtube](https://img.shields.io/youtube/channel/views/UCxrmeMvVryNfB4XL35lXQNg?label=Youtube&style=flat-square&logo=youtube)](https://www.youtube.com/@hiddify)[![Telegram Channel](https://img.shields.io/endpoint?label=Channel&style=flat-square&url=https%3A%2F%2Ftg.sumanjay.workers.dev%2Fhiddify&color=blue)](https://telegram.dog/hiddify)[![Telegram Group](https://img.shields.io/endpoint?color=neon&label=Support%20Group&style=flat-square&url=https%3A%2F%2Ftg.sumanjay.workers.dev%2Fhiddify_board)](https://telegram.dog/hiddify_board/5)
From fa84e0808e0f123ea386c032efea13fa51a20b40 Mon Sep 17 00:00:00 2001 From: lymanjre <125398461+lymanjre@users.noreply.github.com> Date: Tue, 14 Nov 2023 10:19:19 +0330 Subject: [PATCH 23/91] Update README_ru.md --- README_ru.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_ru.md b/README_ru.md index c828d642..1003e067 100644 --- a/README_ru.md +++ b/README_ru.md @@ -9,7 +9,7 @@
-[![GP-Intalls](https://img.shields.io/endpoint?color=green&logo=google-play&logoColor=green&url=https%3A%2F%2Fplay.cuzi.workers.dev%2Fplay%3Fi%3Dapp.hiddify.com%26l%3DGoogle%2520Play%26m%3D%24shortinstalls&style=flat-square)](https://play.google.com/store/apps/details?id=app.hiddify.com) [![Downloads](https://img.shields.io/github/downloads/hiddify/hiddify-next/total?style=flat-square&logo=github)](https://github.com/hiddify/hiddify-next/releases/)[![Last Version](https://img.shields.io/github/release/hiddify/hiddify-next/all.svg?style=flat-square)](https://github.com/hiddify/hiddify-next/)[![Last Release Date](https://img.shields.io/github/release-date/hiddify/hiddify-next.svg?style=flat-square)](https://github.com/hiddify/hiddify-next/)[![commits](https://img.shields.io/github/commit-activity/m/hiddify/hiddify-next?style=flat-square)](https://github.com/hiddify/hiddify-next/) +[![GP-Intalls](https://img.shields.io/endpoint?color=green&logo=google-play&logoColor=green&url=https%3A%2F%2Fplay.cuzi.workers.dev%2Fplay%3Fi%3Dapp.hiddify.com%26l%3DGoogle%2520Play%26m%3D%24shortinstalls&style=flat-square)](https://play.google.com/store/apps/details?id=app.hiddify.com) [![Downloads](https://img.shields.io/github/downloads/hiddify/hiddify-next/total?style=flat-square&logo=github)](https://github.com/hiddify/hiddify-next/releases/)[![Last Version](https://img.shields.io/github/release/hiddify/hiddify-next/all.svg?style=flat-square)](https://github.com/hiddify/hiddify-next/releases/)[![Last Release Date](https://img.shields.io/github/release-date/hiddify/hiddify-next.svg?style=flat-square)](https://github.com/hiddify/hiddify-next/releases/)[![commits](https://img.shields.io/github/commit-activity/m/hiddify/hiddify-next?style=flat-square)](https://github.com/hiddify/hiddify-next/) [![Youtube](https://img.shields.io/youtube/channel/views/UCxrmeMvVryNfB4XL35lXQNg?label=Youtube&style=flat-square&logo=youtube)](https://www.youtube.com/@hiddify)[![Telegram Channel](https://img.shields.io/endpoint?label=Channel&style=flat-square&url=https%3A%2F%2Ftg.sumanjay.workers.dev%2Fhiddify&color=blue)](https://telegram.dog/hiddify)[![Telegram Group](https://img.shields.io/endpoint?color=neon&label=Support%20Group&style=flat-square&url=https%3A%2F%2Ftg.sumanjay.workers.dev%2Fhiddify_board)](https://telegram.dog/hiddify_board/5)
From fc1e52d764229c2345ce5c84a7d0c78d4efade03 Mon Sep 17 00:00:00 2001 From: lymanjre <125398461+lymanjre@users.noreply.github.com> Date: Tue, 14 Nov 2023 10:36:36 +0330 Subject: [PATCH 24/91] Update release_message.md --- .github/release_message.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/release_message.md b/.github/release_message.md index 1396d951..7c2fe61c 100644 --- a/.github/release_message.md +++ b/.github/release_message.md @@ -58,7 +58,7 @@ -
+
**List of all changes:** [ChangeLog](https://github.com/hiddify/hiddify-next/blob/main/CHANGELOG.md) From 1afc959191ffbe7b9ddd9ed99a9f655d02a5a41d Mon Sep 17 00:00:00 2001 From: lymanjre <125398461+lymanjre@users.noreply.github.com> Date: Tue, 14 Nov 2023 14:43:03 +0330 Subject: [PATCH 25/91] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fb4883e7..36963f8e 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ ## What is Hiddify-Next? -

A multi-platform client based on Sing-box that serves as a universal proxy tool-chain. This app offers a wide range of capabilities, which are listed below. It also supports a large number of protocols. The app is free to use, ad-free, and open-source. It provides a secure and private tool for getting access to the free internet.

+

A multi-platform auto-client based on Sing-box that serves as a universal proxy tool-chain. This app offers a wide range of capabilities, which are listed below. It also supports a large number of protocols. The app is free to use, ad-free, and open-source. It provides a secure and private tool for getting access to the free internet.

The app is developed using [Flutter](https://flutter.dev) and [Go](https://go.dev). For more information you can read through our [Contribution Guidelines](https://github.com/hiddify/hiddify-next/blob/main/CONTRIBUTING.md) for development. From 7eebd05c4520a358aa1127b1ec984fca4a964c1a Mon Sep 17 00:00:00 2001 From: lymanjre <125398461+lymanjre@users.noreply.github.com> Date: Tue, 14 Nov 2023 14:43:34 +0330 Subject: [PATCH 26/91] Update README_fa.md --- README_fa.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_fa.md b/README_fa.md index 51309f22..a76f6de5 100644 --- a/README_fa.md +++ b/README_fa.md @@ -15,7 +15,7 @@ ## هیدیفای‌نکست چیست؟ -یک کلاینت مالتی‌پلتفرم مبتنی بر [سینگ‌باکس](https://github.com/SagerNet/sing-box) که به عنوان یک ابزار عمومی برای پروکسی عمل می‌کند. این برنامه طیف گسترده‌ای از قابلیت‌ها را ارائه می‌دهد که در زیر لیست شده است. همچنین از تعداد زیادی پروتکل پشتیبانی می‌کند. این برنامه رایگان، بدون آگهی و منبع باز است. این یک ابزار امن و مطمئن برای دسترسی به اینترنت رایگان فراهم می‌کند. +یک کلاینت خودکار مالتی‌پلتفرم مبتنی بر [سینگ‌باکس](https://github.com/SagerNet/sing-box) که به عنوان یک ابزار عمومی برای پروکسی عمل می‌کند. این برنامه طیف گسترده‌ای از قابلیت‌ها را ارائه می‌دهد که در زیر لیست شده است. همچنین از تعداد زیادی پروتکل پشتیبانی می‌کند. این برنامه رایگان، بدون آگهی و منبع باز است. این یک ابزار امن و مطمئن برای دسترسی به اینترنت رایگان فراهم می‌کند. این برنامه با استفاده از [Flutter](https://flutter.dev/) و [Go](https://go.dev/) توسعه یافته است. برای اطلاعات بیشتر در خصوص توسعه می‌توانید [دستورالعمل‌های مشارکت](https://github.com/hiddify/hiddify-next/blob/main/CONTRIBUTING.md) در پروژه ما را مطالعه نمایید. From 084d64c9c9aba4e3700c53a8705170028973ddce Mon Sep 17 00:00:00 2001 From: lymanjre <125398461+lymanjre@users.noreply.github.com> Date: Tue, 14 Nov 2023 14:46:05 +0330 Subject: [PATCH 27/91] Update README_cn.md --- README_cn.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_cn.md b/README_cn.md index 0e8ccfc5..621c0d07 100644 --- a/README_cn.md +++ b/README_cn.md @@ -14,7 +14,7 @@ ## Hiddify-Next 是什么? -基于 [Sing-box](https://github.com/SagerNet/sing-box) 的多平台客户端,用作通用代理工具链。 该应用程序提供了广泛的功能,如下所列。 它还支持大量协议。 该应用程序免费使用、无广告且开源。 它提供了一个安全且私密的工具来访问免费互联网。 +基于 [Sing-box](https://github.com/SagerNet/sing-box) 的多平台客自动客户端,用作通用代理工具链。 该应用程序提供了广泛的功能,如下所列。 它还支持大量协议。 该应用程序免费使用、无广告且开源。 它提供了一个安全且私密的工具来访问免费互联网。 该应用程序是使用 [Flutter](https://flutter.dev/) 和 [Go](https://go.dev/) 开发的。 欲了解更多信息,您可以阅读我们的开发贡献指南。 From cf0a67c921b89b316355f55be69c33dc6c7b6f3b Mon Sep 17 00:00:00 2001 From: lymanjre <125398461+lymanjre@users.noreply.github.com> Date: Tue, 14 Nov 2023 14:47:00 +0330 Subject: [PATCH 28/91] Update README_ru.md --- README_ru.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_ru.md b/README_ru.md index 1003e067..f8c6811a 100644 --- a/README_ru.md +++ b/README_ru.md @@ -15,7 +15,7 @@
## Что такое Hiddify-Next? -Многоплатформенный клиент на основе [Sing-box](https://github.com/SagerNet/sing-box), который служит универсальным набором инструментов прокси. Это приложение предлагает широкий спектр возможностей, которые перечислены ниже. Он также поддерживает большое количество протоколов. Приложение бесплатное, без рекламы и с открытым исходным кодом. Он предоставляет безопасный и конфиденциальный инструмент для получения доступа к бесплатному Интернету. +Многоплатформенный авто-клиент на основе [Sing-box](https://github.com/SagerNet/sing-box), который служит универсальным набором инструментов прокси. Это приложение предлагает широкий спектр возможностей, которые перечислены ниже. Он также поддерживает большое количество протоколов. Приложение бесплатное, без рекламы и с открытым исходным кодом. Он предоставляет безопасный и конфиденциальный инструмент для получения доступа к бесплатному Интернету. Приложение разработано с использованием [Flutter](https://flutter.dev/) и [Go](https://go.dev/). Для получения дополнительной информации вы можете прочитать наши Рекомендации по участию в разработке. From ca65aea69629dd8d46f67d309f266554e687f92c Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Tue, 14 Nov 2023 18:54:25 +0330 Subject: [PATCH 29/91] Fix bottom navigation bar accessibility --- .../common/adaptive_root_scaffold.dart | 26 +++++++------------ 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/lib/features/common/adaptive_root_scaffold.dart b/lib/features/common/adaptive_root_scaffold.dart index 9b3936b5..5dea563b 100644 --- a/lib/features/common/adaptive_root_scaffold.dart +++ b/lib/features/common/adaptive_root_scaffold.dart @@ -148,23 +148,6 @@ class _CustomAdaptiveScaffold extends HookConsumerWidget { ), }, ), - bottomNavigation: useBottomSheet || - Breakpoints.smallMobile.isActive(context) - ? SlotLayout( - config: { - Breakpoints.small: SlotLayout.from( - key: const Key('bottomNavigation'), - builder: (_) => - AdaptiveScaffold.standardBottomNavigationBar( - currentIndex: selectedWithOffset(bottomDestinationRange), - destinations: destinationsSlice(bottomDestinationRange), - onDestinationSelected: (index) => - selectWithOffset(index, bottomDestinationRange), - ), - ), - }, - ) - : null, body: SlotLayout( config: { Breakpoints.standard: SlotLayout.from( @@ -176,6 +159,15 @@ class _CustomAdaptiveScaffold extends HookConsumerWidget { }, ), ), + // AdaptiveLayout bottom sheet has accessibility issues + bottomNavigationBar: useBottomSheet && Breakpoints.small.isActive(context) + ? NavigationBar( + selectedIndex: selectedWithOffset(bottomDestinationRange) ?? 0, + destinations: destinationsSlice(bottomDestinationRange), + onDestinationSelected: (index) => + selectWithOffset(index, bottomDestinationRange), + ) + : null, ); } } From c7446e98bc32c67df3e9535e3a86b75da35a759d Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Tue, 14 Nov 2023 19:16:14 +0330 Subject: [PATCH 30/91] Add independent dns cache option --- dependencies.properties | 2 +- lib/domain/singbox/config_options.dart | 1 + libcore | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/dependencies.properties b/dependencies.properties index 2cb2a52d..d4c735aa 100644 --- a/dependencies.properties +++ b/dependencies.properties @@ -1 +1 @@ -core.version=0.8.0 \ No newline at end of file +core.version=0.8.1 \ No newline at end of file diff --git a/lib/domain/singbox/config_options.dart b/lib/domain/singbox/config_options.dart index c7fcbe1b..6301da98 100644 --- a/lib/domain/singbox/config_options.dart +++ b/lib/domain/singbox/config_options.dart @@ -37,6 +37,7 @@ class ConfigOptions with _$ConfigOptions { @Default(false) bool setSystemProxy, @Default(false) bool bypassLan, @Default(false) bool enableFakeDns, + @Default(true) bool independentDnsCache, List? rules, }) = _ConfigOptions; diff --git a/libcore b/libcore index 953e6a02..89ecc6bf 160000 --- a/libcore +++ b/libcore @@ -1 +1 @@ -Subproject commit 953e6a02d73d0a2894071e289ff209b633470e54 +Subproject commit 89ecc6bf1238e12bbad26182f7a06f9aaf492b9f From 129a522b24786809aa74b6548224e4714f311314 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Tue, 14 Nov 2023 19:48:36 +0330 Subject: [PATCH 31/91] Change http adapter --- ios/Podfile.lock | 6 +++++ lib/data/data_providers.dart | 30 +++++++++++++++-------- linux/flutter/generated_plugins.cmake | 1 + macos/Podfile.lock | 6 +++++ pubspec.lock | 32 +++++++++++++++++++++++++ pubspec.yaml | 1 + windows/flutter/generated_plugins.cmake | 1 + 7 files changed, 67 insertions(+), 10 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 90811d8c..3bd07b0d 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,4 +1,6 @@ PODS: + - cupertino_http (0.0.1): + - Flutter - device_info_plus (0.0.1): - Flutter - Flutter (1.0.0) @@ -99,6 +101,7 @@ PODS: - Flutter DEPENDENCIES: + - cupertino_http (from `.symlinks/plugins/cupertino_http/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - Flutter (from `Flutter`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) @@ -131,6 +134,8 @@ SPEC REPOS: - sqlite3 EXTERNAL SOURCES: + cupertino_http: + :path: ".symlinks/plugins/cupertino_http/ios" device_info_plus: :path: ".symlinks/plugins/device_info_plus/ios" Flutter: @@ -157,6 +162,7 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/url_launcher_ios/ios" SPEC CHECKSUMS: + cupertino_http: 5f8b1161107fe6c8d94a0c618735a033d93fa7db device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef diff --git a/lib/data/data_providers.dart b/lib/data/data_providers.dart index 7a40c5df..e4ac609d 100644 --- a/lib/data/data_providers.dart +++ b/lib/data/data_providers.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:dio/dio.dart'; import 'package:hiddify/core/core_providers.dart'; import 'package:hiddify/core/prefs/general_prefs.dart'; @@ -12,6 +14,7 @@ import 'package:hiddify/domain/constants.dart'; import 'package:hiddify/domain/core_facade.dart'; import 'package:hiddify/domain/profiles/profiles.dart'; import 'package:hiddify/services/service_providers.dart'; +import 'package:native_dio_adapter/native_dio_adapter.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -25,16 +28,23 @@ SharedPreferences sharedPreferences(SharedPreferencesRef ref) => throw UnimplementedError('sharedPreferences must be overridden'); @Riverpod(keepAlive: true) -Dio dio(DioRef ref) => Dio( - BaseOptions( - connectTimeout: const Duration(seconds: 15), - sendTimeout: const Duration(seconds: 15), - receiveTimeout: const Duration(seconds: 15), - headers: { - "User-Agent": ref.watch(appInfoProvider).userAgent, - }, - ), - ); +Dio dio(DioRef ref) { + final dio = Dio( + BaseOptions( + connectTimeout: const Duration(seconds: 15), + sendTimeout: const Duration(seconds: 15), + receiveTimeout: const Duration(seconds: 15), + headers: { + "User-Agent": ref.watch(appInfoProvider).userAgent, + }, + ), + ); + final debug = ref.read(debugModeNotifierProvider); + if (debug && (Platform.isAndroid || Platform.isIOS || Platform.isMacOS)) { + dio.httpClientAdapter = NativeAdapter(); + } + return dio; +} @Riverpod(keepAlive: true) ProfilesDao profilesDao(ProfilesDaoRef ref) => ProfilesDao( diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index e1759bc3..600ec847 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -12,6 +12,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + jni ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 0b92454b..06bee27f 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -1,4 +1,6 @@ PODS: + - cupertino_http (0.0.1): + - FlutterMacOS - device_info_plus (0.0.1): - FlutterMacOS - FlutterMacOS (1.0.0) @@ -48,6 +50,7 @@ PODS: - FlutterMacOS DEPENDENCIES: + - cupertino_http (from `Flutter/ephemeral/.symlinks/plugins/cupertino_http/macos`) - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - mobile_scanner (from `Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos`) @@ -70,6 +73,8 @@ SPEC REPOS: - sqlite3 EXTERNAL SOURCES: + cupertino_http: + :path: Flutter/ephemeral/.symlinks/plugins/cupertino_http/macos device_info_plus: :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos FlutterMacOS: @@ -100,6 +105,7 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: + cupertino_http: afa11b9e2786b62da2671e4ddd32caf792503748 device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 mobile_scanner: 621cf2c34e1c74ae7ce5c6793638ab600723bdea diff --git a/pubspec.lock b/pubspec.lock index c9b8b06a..6030cb3b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -225,6 +225,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + cronet_http: + dependency: transitive + description: + name: cronet_http + sha256: "567170c4bb3d237991b852f446c1b52a3325de8b584a82d195f31dd76310d7d1" + url: "https://pub.dev" + source: hosted + version: "0.4.1" cross_file: dependency: transitive description: @@ -257,6 +265,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.2" + cupertino_http: + dependency: transitive + description: + name: cupertino_http + sha256: "93cdb8a3aa8bc71caf62fdaca66e613871a6aa1388c7cd7f39b6366b5d2f5af6" + url: "https://pub.dev" + source: hosted + version: "1.1.0" cupertino_icons: dependency: "direct main" description: @@ -701,6 +717,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + jni: + dependency: transitive + description: + name: jni + sha256: "0d88790bdf7e298aa65a9094c62b58ea231169a2deb84f23defc7d7955885b43" + url: "https://pub.dev" + source: hosted + version: "0.7.2" js: dependency: transitive description: @@ -813,6 +837,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.5.2" + native_dio_adapter: + dependency: "direct main" + description: + name: native_dio_adapter + sha256: "1967cabe3e9ea68ea5ad6da7a0ed25fa75cf335ec6b92cdf6f32185efa93364b" + url: "https://pub.dev" + source: hosted + version: "1.1.0" neat_periodic_task: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 9a3e918c..508d19a5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -71,6 +71,7 @@ dependencies: posix: ^5.0.0 win32: ^5.0.9 qr_flutter: ^4.1.0 + native_dio_adapter: ^1.1.0 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 0bd3c28a..162a92be 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -15,6 +15,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + jni ) set(PLUGIN_BUNDLED_LIBRARIES) From b993505feeb80f40353ca21133cd029549908f62 Mon Sep 17 00:00:00 2001 From: jomertix <150632538+jomertix@users.noreply.github.com> Date: Wed, 15 Nov 2023 04:19:56 +0300 Subject: [PATCH 32/91] inlang: update translations --- assets/translations/strings_ru.i18n.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/assets/translations/strings_ru.i18n.json b/assets/translations/strings_ru.i18n.json index cca67955..4628bfe2 100644 --- a/assets/translations/strings_ru.i18n.json +++ b/assets/translations/strings_ru.i18n.json @@ -28,8 +28,8 @@ "connected": "Подключено" }, "stats": { - "traffic": "Текущий траффик", - "trafficTotal": "Траффик", + "traffic": "Текущий трафик", + "trafficTotal": "Трафик", "uplink": "Скорость отправки", "downlink": "Скорость загрузки" } @@ -242,9 +242,9 @@ "dashboard": "Панель", "quit": "Выход", "status": { - "connect": "Подключено", + "connect": "Подключиться", "connecting": "Подключение", - "disconnect": "Отключено", + "disconnect": "Отключиться", "disconnecting": "Отключение" } }, From f0d324b71740c8958e9b87f19a997dd6442814f4 Mon Sep 17 00:00:00 2001 From: solokot Date: Wed, 15 Nov 2023 11:58:43 +0300 Subject: [PATCH 33/91] Update Russian translation and partially revert ugly inlang changes --- assets/translations/strings_ru.i18n.json | 80 ++++++++++++------------ 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/assets/translations/strings_ru.i18n.json b/assets/translations/strings_ru.i18n.json index cca67955..e4c50034 100644 --- a/assets/translations/strings_ru.i18n.json +++ b/assets/translations/strings_ru.i18n.json @@ -10,11 +10,11 @@ "disable": "Отключить" }, "sort": "Сортировка", - "sortBy": "Сортировать по", + "sortBy": "Сортировка…", "addToClipboard": "Копировать в буфер обмена" }, "intro": { - "termsAndPolicyCaution(rich)": "Продолжая, Вы соглашаетесь с ${tap(@:about.termsAndConditions)}", + "termsAndPolicyCaution(rich)": "Продолжая, вы соглашаетесь с ${tap(@:about.termsAndConditions)}", "start": "Начать" }, "home": { @@ -28,10 +28,10 @@ "connected": "Подключено" }, "stats": { - "traffic": "Текущий траффик", - "trafficTotal": "Траффик", - "uplink": "Скорость отправки", - "downlink": "Скорость загрузки" + "traffic": "Скорость", + "trafficTotal": "Трафик", + "uplink": "Исходящий канал", + "downlink": "Входящий канал" } }, "profile": { @@ -49,8 +49,8 @@ "noTraffic": "Нет доступного трафика" }, "sortBy": { - "lastUpdate": "последнему обновлению", - "name": " алфавиту" + "lastUpdate": "Последнее обновление", + "name": "Алфавит" }, "add": { "buttonText": "Новый профиль", @@ -58,24 +58,24 @@ "fromClipboard": "Добавить из буфера обмена", "scanQr": "Сканировать QR-код", "qrScanner": { - "permissionDeniedError": "Доступ запрещен", - "unexpectedError": "Что-то пошло не так", + "permissionDeniedError": "Доступ запрещён", + "unexpectedError": "Неизвестная ошибка", "torchSemanticLabel": "Вспышка", - "facingSemanticLabel": "Перед камерой" + "facingSemanticLabel": "Фронтальная камера" }, - "manually": "Ввести вручную", + "manually": "Ручной ввод", "addingProfileMsg": "Добавление профиля", - "failureMsg": "Не удалось добавить профиль" + "failureMsg": "Невозможно добавить профиль" }, "update": { "buttonTxt": "Обновить", "tooltip": "Обновить профиль", - "failureMsg": "Не удалось обновить профиль", + "failureMsg": "Ошибка обновления", "successMsg": "Профиль успешно обновлён" }, "share": { "buttonText": "Поделиться", - "exportToClipboardSuccess": "Скопировано в буфер обмена", + "exportToClipboardSuccess": "Ссылка скопирована в буфер обмена", "exportSubLinkToClipboard": "Скопировать ссылку на подписку в буфер обмена", "subLinkQrCode": "QR-код ссылки на подписку", "exportConfigToClipboard": "Скопировать конфигурацию в буфер обмена", @@ -93,7 +93,7 @@ "save": { "buttonText": "Сохранить", "successMsg": "Профиль успешно сохранён", - "failureMsg": "Не удалось сохранить профиль" + "failureMsg": "Невозможно сохранить профиль" }, "detailsForm": { "nameLabel": "Имя", @@ -130,12 +130,12 @@ }, "settings": { "pageTitle": "Настройки", - "requiresRestartMsg": "Чтобы применить изменения, перезапустите приложение", + "requiresRestartMsg": "Для применения перезапустите приложение.", "general": { "sectionTitle": "Основные", "locale": "Язык", "region": "Регион", - "regionMsg": "Помогает установить параметры по умолчанию для обхода внутренних адресов", + "regionMsg": "Помогает установить параметры по умолчанию для обхода внутренних адресов.", "regions": { "ir": "Иран (ir)", "cn": "Китай (cn)", @@ -150,17 +150,17 @@ "black": "Чёрная тема" }, "enableAnalytics": "Сбор аналитики", - "enableAnalyticsMsg": "Сбор данных аналитики и отправка отчётов о сбоях для улучшения приложения", + "enableAnalyticsMsg": "Сбор аналитических данных и отправка отчётов о сбоях для улучшения приложения.", "autoStart": "Запуск при загрузке", "silentStart": "Тихий запуск", "openWorkingDir": "Открыть рабочую папку", "ignoreBatteryOptimizations": "Отключить оптимизацию батареи", - "ignoreBatteryOptimizationsMsg": "Отключение ограничений для оптимальной производительности VPN" + "ignoreBatteryOptimizationsMsg": "Отключение ограничений для оптимальной производительности VPN." }, "advanced": { "sectionTitle": "Расширенные", "debugMode": "Режим отладки", - "debugModeMsg": "Чтобы применить изменения, перезапустите приложение", + "debugModeMsg": "Для применения перезапустите приложение.", "memoryLimit": "Ограничение памяти" }, "network": { @@ -191,7 +191,7 @@ "misc": "Разные параметры" }, "pageTitle": "Параметры конфигурации", - "logLevel": "Уровень логирования", + "logLevel": "Подробность журналирования", "resolveDestination": "Определять назначение", "ipv6Mode": "Маршрутизация IPv6", "ipv6Modes": { @@ -200,10 +200,10 @@ "prefer": "Предпочтительно", "only": "Исключительно" }, - "remoteDnsAddress": "Remote DNS", - "remoteDnsDomainStrategy": "Remote DNS Domain Strategy", - "directDnsAddress": "Direct DNS", - "directDnsDomainStrategy": "Direct DNS Domain Strategy", + "remoteDnsAddress": "Удалённая DNS", + "remoteDnsDomainStrategy": "Стратегия удалённого домена DNS", + "directDnsAddress": "Прямая DNS", + "directDnsDomainStrategy": "Стратегия прямого домена DNS", "mixedPort": "Смешанный порт", "localDnsPort": "Локальный порт DNS", "tunImplementation": "Реализация TUN", @@ -214,7 +214,7 @@ "clashApiPort": "Порт Clash API", "enableTun": "Использовать TUN", "setSystemProxy": "Использовать системный прокси", - "enableFakeDns": "Использовать поддельный DNS", + "enableFakeDns": "Использовать поддельную DNS", "bypassLan": "Обход локальной сети", "strictRoute": "Строгая маршрутизация" } @@ -224,7 +224,7 @@ "version": "Версия", "sourceCode": "Исходный код", "telegramChannel": "Telegram-канал", - "checkForUpdate": "Проверить обновления", + "checkForUpdate": "Проверка обновления", "privacyPolicy": "Политика конфиденциальности", "termsAndConditions": "Условия и положения" }, @@ -249,15 +249,15 @@ } }, "failure": { - "unexpected": "Непредвиденная ошибка", + "unexpected": "Неожиданная ошибка", "clash": { - "unexpected": "Непредвиденная ошибка (Clash)", + "unexpected": "Неожиданная ошибка (Clash)", "core": "Ошибка ${reason}" }, "singbox": { - "unexpected": "Непредвиденная ошибка (SingBox)", + "unexpected": "Неожиданная ошибка (SingBox)", "serviceNotRunning": "Сервис не запущен", - "missingPrivilege": "Нет прав", + "missingPrivilege": "Отсутствие прав", "missingPrivilegeMsg": "Режим VPN требует прав администратора. Перезапустите приложение от имени администратора или измените режим работы приложения.", "invalidConfigOptions": "Неправильные параметры конфигурации", "invalidConfig": "Неправильная конфигурация", @@ -265,28 +265,28 @@ "start": "Ошибка запуска сервиса" }, "connectivity": { - "unexpected": "Непредвиденная ошибка", + "unexpected": "Неожиданная ошибка", "missingVpnPermission": "Отсутствует разрешение VPN", - "missingNotificationPermission": "Отсутствует разрешение на показ уведомлений", + "missingNotificationPermission": "Отсутствует разрешение на отображение уведомлений", "core": "Ошибка ядра" }, "profiles": { - "unexpected": "Непредвиденная ошибка", + "unexpected": "Неожиданная ошибка", "notFound": "Профиль не найден", "invalidConfig": "Неправильная конфигурация", "invalidUrl": "Неправильный URL" }, "connection": { - "unexpected": "Непредвиденная ошибка подключения", - "timeout": "Время подключения истекло", + "unexpected": "Неожиданная ошибка подключения", + "timeout": "Истекло время подключения", "badResponse": "Неправильный ответ", "connectionError": "Ошибка подключения", "badCertificate": "Неправильный сертификат" } }, "play": { - "title": "Hiddify Next (Бета)", + "title": "Hiddify Next (Preview)", "short_description": "Автовыбор, SSH, VLESS, Vmess, Trojan, Reality, Sing-Box, Clash, Xray, Shadowsocks", - "full_description": "Основная цель HiddifyNext — предоставить безопасный, удобный и эффективный клиент для туннелирования. Приложение позволяет направлять весь трафик или трафик выбранного приложения на указанный Вами удалённый сервер.\n\nПримечание: мы не предоставляем никаких серверов, пользователи сами должны обеспечивать конфиденциальность своих действий в Интернете, используя собственный сервер или доверенные серверы. \n\n Поддерживаются сервера с: \n— Обычной ссылкой на подписку V2ray/Xray \n— Ссылкой на подписку Clash \n— Ссылкой на подписку Sing–Box \n\n В чём наши преимущества? \n— Удобство \n— Оптимизация и скорость \n— Автоматический выбор на основе задержки \n— Отображение информации об использовании \n— Простой импорт ссылок в один клик \n— Бесплатно и без рекламы \n— Простое переключение ссылок \n— …и много больше \n\nПоддерживаются: \n• Все протоколы, поддерживаемые Sing-Box \n• VLESS + xtls reality, vision \n• VMESS \n• Trojan \n• ShoadowSocks \n• Reality \n• V2ray \n• Hystria2 \n• TUIC \n• SSH \n• ShadowTLS \n\nИсходный код доступен по ссылке https://github.com/hiddify/Hiddify-Next\nЯдро приложения основано на открытом исходном коде Sing–Box. \n\n Описание разрешений: \n— СЛУЖБА VPN: поскольку целью данного приложения является предоставление безопасного, удобного и эффективного клиента для туннелирования, это разрешение необходимо, чтобы иметь возможность направлять трафик через туннель на удалённый сервер. \n— ЗАПРОС ВСЕХ ПАКЕТОВ: это разрешение позволяет добавлять или удалять определённые приложения из списка для туннелирования. \n— ИНФОРМИРОВАНИЕ О ЗАВЕРШЕНИИ ЗАГРУЗКИ: это разрешение можно включить или отключить в настройках приложения, чтобы активировать или деактивировать соответственно запуск приложения при загрузке устройства. \n— ПОСТОЯННОЕ УВЕДОМЛЕНИЕ: оно необходимо, так как используется приоритетная служба для обеспечения непрерывной работы VPN подключения. \n— Приложение не содержит рекламы. Сбор аналитики и данных о сбоях происходят только с явного согласия пользователя при первом использовании приложения." + "full_description": "Основная цель HiddifyNext — предоставить безопасный, удобный и эффективный клиент туннелирования. Он позволяет направлять весь трафик или трафик выбранного приложения на выбранный вами удалённый сервер, используя разрешение VPN–сервиса.\n\nПримечание: мы не предоставляем серверы, пользователи должны обеспечивать конфиденциальность своих действий в Интернете, используя собственный сервер или доверенные серверы.\n \nПоддерживаемые серверы:\n— Обычная ссылка на подписку V2ray/Xray\n— Ссылка на подписку Clash\n— Ссылка на подписку на Sing–Box\n\nВ чём уникальные особенности?\n — Удобство\n — Оптимизация и скорость\n — Автоматический выбор минимальной задержки\n — Отображение информации об использовании\n — Простой импорт ссылок одним щелчком мыши\n — Бесплатно и без рекламы\n — Простое переключение ссылок\n — …и много больше\n\nПоддержка:\n• Все протоколы, поддерживаемые Sing-Box\n• VLESS + xtls reality, vision\n• VMESS\n• Trojan\n• ShoadowSocks\n• Reality\n• V2ray\n• Hystria2\n• TUIC\n• SSH\n• ShadowTLS\n\n\nИсходный код доступен по адресу https://github.com/hiddify/Hiddify-Next.\nЯдро приложения основано на открытом исходном коде Sing–Box.\n\nОписание разрешений:\n— СЛУЖБА VPN: поскольку целью данного приложения является предоставление безопасного, удобного и эффективного клиента туннелирования, это разрешение необходимо, чтобы иметь возможность направлять трафик через туннель на удалённый сервер.\n— ЗАПРОС ВСЕХ ПАКЕТОВ: это разрешение позволяет включать или исключать определённые приложения для туннелирования.\n— ИНФОРМИРОВАНИЕ О ЗАВЕРШЕНИИ ЗАГРУЗКИ: это разрешение можно включить или отключить в настройках приложения, чтобы (де)активировать запуск приложения при загрузке устройства.\n— ПОСТОЯННОЕ УВЕДОМЛЕНИЕ: это разрешение необходимо, поскольку используется приоритетная служба для обеспечения непрерывной работы службы VPN.\n— Приложение не содержит рекламы. Сбор аналитики и данных о сбоях происходят только с явного согласия пользователя при первом использовании приложения." } -} \ No newline at end of file +} From 7820486e1a419def11057b6fa9a64a25aa06fe28 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Wed, 15 Nov 2023 18:48:48 +0330 Subject: [PATCH 34/91] Add navigation to system tray --- assets/translations/strings_en.i18n.json | 1 + assets/translations/strings_fa.i18n.json | 3 ++- assets/translations/strings_ru.i18n.json | 3 ++- assets/translations/strings_zh.i18n.json | 3 ++- .../system_tray/system_tray_controller.dart | 24 +++++++++++++++++++ 5 files changed, 31 insertions(+), 3 deletions(-) diff --git a/assets/translations/strings_en.i18n.json b/assets/translations/strings_en.i18n.json index 403d6d36..cae1904b 100644 --- a/assets/translations/strings_en.i18n.json +++ b/assets/translations/strings_en.i18n.json @@ -241,6 +241,7 @@ "tray": { "dashboard": "Dashboard", "quit": "Quit", + "open": "Open", "status": { "connect": "Connect", "connecting": "Connecting", diff --git a/assets/translations/strings_fa.i18n.json b/assets/translations/strings_fa.i18n.json index ffafbc34..c9bc0356 100644 --- a/assets/translations/strings_fa.i18n.json +++ b/assets/translations/strings_fa.i18n.json @@ -246,7 +246,8 @@ "connecting": "در حال اتصال", "disconnect": "قطع اتصال", "disconnecting": "در حال قطع اتصال" - } + }, + "open": "باز کن" }, "failure": { "unexpected": "خطای غیرمنتظره", diff --git a/assets/translations/strings_ru.i18n.json b/assets/translations/strings_ru.i18n.json index cca67955..a41ea710 100644 --- a/assets/translations/strings_ru.i18n.json +++ b/assets/translations/strings_ru.i18n.json @@ -246,7 +246,8 @@ "connecting": "Подключение", "disconnect": "Отключено", "disconnecting": "Отключение" - } + }, + "open": "Открыть" }, "failure": { "unexpected": "Непредвиденная ошибка", diff --git a/assets/translations/strings_zh.i18n.json b/assets/translations/strings_zh.i18n.json index daa6e0d5..f3117624 100644 --- a/assets/translations/strings_zh.i18n.json +++ b/assets/translations/strings_zh.i18n.json @@ -246,7 +246,8 @@ "connecting": "正在连接", "disconnect": "已断开连接", "disconnecting": "正在断开连接" - } + }, + "open": "打开" }, "failure": { "unexpected": "意外错误", diff --git a/lib/features/system_tray/system_tray_controller.dart b/lib/features/system_tray/system_tray_controller.dart index f93c8602..c230fb9f 100644 --- a/lib/features/system_tray/system_tray_controller.dart +++ b/lib/features/system_tray/system_tray_controller.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:hiddify/core/core_providers.dart'; +import 'package:hiddify/core/router/router.dart'; import 'package:hiddify/data/repository/config_options_store.dart'; import 'package:hiddify/domain/connectivity/connectivity.dart'; import 'package:hiddify/domain/constants.dart'; @@ -37,6 +38,13 @@ class SystemTrayController extends _$SystemTrayController final serviceMode = ref.watch(serviceModeStoreProvider); final t = ref.watch(translationsProvider); + final destinations = <(String label, String location)>[ + (t.home.pageTitle, const HomeRoute().location), + (t.proxies.pageTitle, const ProxiesRoute().location), + (t.logs.pageTitle, const LogsRoute().location), + (t.settings.pageTitle, const SettingsRoute().location), + (t.about.pageTitle, const AboutRoute().location), + ]; loggy.debug('updating system tray'); @@ -79,6 +87,22 @@ class SystemTrayController extends _$SystemTrayController ], ), ), + MenuItem.submenu( + label: t.tray.open, + submenu: Menu( + items: [ + ...destinations.map( + (e) => MenuItem( + label: e.$1, + onClick: (_) async { + await ref.read(windowControllerProvider.notifier).show(); + ref.read(routerProvider).go(e.$2); + }, + ), + ), + ], + ), + ), MenuItem.separator(), MenuItem( label: t.tray.quit, From bc1d57f981887bc0f1ad2de52439fd5b80820b36 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Wed, 15 Nov 2023 18:52:28 +0330 Subject: [PATCH 35/91] release: version 0.10.9.dev --- CHANGELOG.md | 14 ++++++++++++-- pubspec.yaml | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cfbff204..63ee613f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,13 @@ - Changed Responsive UI Behavior - Now app is responsive on all platforms with appropriate routing setup. +- Added Simplified Service Modes + - Choose between VPN(Tun), System Proxy and Proxy only modes. (System Proxy available on desktop) +- Added Share Functionality + - Share configuration as json(export to clipboard) or share subscription link as QR code. - Redesigned System Tray on Desktop - - Options have been simplified and a new mode selector is added for easier access to TUN and Proxy modes. + - Options have been simplified and a new mode selector and navigation options are added. +- Added Privilege Checks for VPN(TUN) on Desktop - Added Auto Connect on Start - On desktop, app will try to connect to the last used profile on startup. (if last session was not explicitly disconnected by the user) - Added AppCast Update Checker @@ -16,6 +21,8 @@ - Now you're able to install and update Hiddify Next on Windows using [winget](https://learn.microsoft.com/en-us/windows/package-manager/winget/). - Changed in-app Toasts - Updated Core Sing-box Version to 1.7.0 +- Improved Network Reliability While Adding/Updating Subscriptions +- Improved QR Code Scanner ### Bug Fixes @@ -25,7 +32,10 @@ - Fixed translator script. [PR#108](https://github.com/hiddify/hiddify-next/pull/108) by [Hirad Rasoolinejad](https://github.com/Hiiirad) - Fixed localization mistakes in Chinese. [PR#113](https://github.com/hiddify/hiddify-next/pull/113) and [PR#123](https://github.com/hiddify/hiddify-next/pull/123) by [Nyar233](https://github.com/Nyar233) - Fixed localization mistakes in Chinese Readme. [PR#137](https://github.com/hiddify/hiddify-next/pull/137) by [wldjdjsks](https://github.com/huajizhige) -- Fixed localization mistakes in Chinese. [PR#138](https://github.com/hiddify/hiddify-next/pull/138) by [wldjdjsks](https://github.com/huajizhige) +- Fixed localization mistakes in Chinese. [PR#138](https://github.com/hiddify/hiddify-next/pull/138) and [PR#165](https://github.com/hiddify/hiddify-next/pull/165) by [wldjdjsks](https://github.com/huajizhige) +- Fixed localization mistakes in Russian. [PR#155](https://github.com/hiddify/hiddify-next/pull/155) and [PR#162](https://github.com/hiddify/hiddify-next/pull/162) by [solokot](https://github.com/solokot) +- Fixed linux build libs command. [PR#161](https://github.com/hiddify/hiddify-next/pull/161) by [Aloxaf](https://github.com/Aloxaf) +- Fixed localization mistakes in Russian. [PR#164](https://github.com/hiddify/hiddify-next/pull/164) by [jomertix](https://github.com/jomertix) ## [0.10.0] - 2023-10-27 diff --git a/pubspec.yaml b/pubspec.yaml index 508d19a5..c7642e45 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: hiddify description: A Proxy Frontend. publish_to: "none" -version: 0.10.8+1008 +version: 0.10.9+1009 environment: sdk: ">=3.1.0 <4.0.0" From ad052693f5142b93b6e3ca5702a0db49adbce06c Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Wed, 15 Nov 2023 19:32:47 +0330 Subject: [PATCH 36/91] Update appcast --- appcast.xml | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/appcast.xml b/appcast.xml index ad9eee41..37c7f296 100644 --- a/appcast.xml +++ b/appcast.xml @@ -3,31 +3,31 @@ Release - Version 0.10.8 - Mon, 13 Nov 2023 19:00:00 +0000 + Version 0.10.0 + Sat, 28 Oct 2023 12:00:00 +0000 + sparkle:version="0.10.0" sparkle:os="android" /> - Version 0.10.8 - Mon, 13 Nov 2023 19:00:00 +0000 + Version 0.10.9 + Wed, 15 Nov 2023 19:00:00 +0000 + url="https://github.com/hiddify/hiddify-next/releases/download/v0.10.9.dev/hiddify-windows-x64-setup.zip" + sparkle:version="0.10.9" sparkle:os="windows" /> - Version 0.10.8 - Mon, 13 Nov 2023 19:00:00 +0000 + Version 0.10.9 + Wed, 15 Nov 2023 19:00:00 +0000 + url="https://github.com/hiddify/hiddify-next/releases/download/v0.10.9.dev/hiddify-macos-universal.zip" + sparkle:version="0.10.9" sparkle:os="macos" /> - Version 0.10.8 - Mon, 13 Nov 2023 19:00:00 +0000 + Version 0.10.9 + Wed, 15 Nov 2023 19:00:00 +0000 + url="https://github.com/hiddify/hiddify-next/releases/download/v0.10.9.dev/hiddify-linux-x64.zip" + sparkle:version="0.10.9" sparkle:os="linux" /> \ No newline at end of file From 5cff03e3f5ca6f81f7a549c99df1751d76390520 Mon Sep 17 00:00:00 2001 From: Hiddify <114227601+hiddify-com@users.noreply.github.com> Date: Wed, 15 Nov 2023 18:33:18 +0100 Subject: [PATCH 37/91] Update build.yml --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f1125903..8b60afe9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -392,7 +392,7 @@ jobs: with: value: '${{ github.ref_name }}' regex: '^v|.dev$' - flags: '' # Optional, defaults to "g" + flags: 'gi' # Optional, defaults to "g" replacement: '' - name: Winget Publish if: ${{ env.CHANNEL != 'dev' }} @@ -410,7 +410,7 @@ jobs: with: publish-type: 'Update' user: 'Hiddify' - package: 'Next' + package: 'Next.Beta' version: ${{ steps.version.outputs.value }} url: 'https://github.com/hiddify/hiddify-next/releases/download/${{ github.ref_name }}/hiddify-windows-x64-setup.zip' token: ${{ secrets.WINGET_TOKEN }} From dc624ab145b1a6f01fbcbcf8f7794ef34255ae9f Mon Sep 17 00:00:00 2001 From: Locas Date: Thu, 16 Nov 2023 21:50:06 +0800 Subject: [PATCH 38/91] Fix and improve Chinese README --- README_cn.md | 100 +++++++++++++++++++++++++++------------------------ 1 file changed, 53 insertions(+), 47 deletions(-) diff --git a/README_cn.md b/README_cn.md index 621c0d07..847a9157 100644 --- a/README_cn.md +++ b/README_cn.md @@ -1,55 +1,61 @@ -
- -[**![Lang_farsi](https://user-images.githubusercontent.com/125398461/234186932-52f1fa82-52c6-417f-8b37-08fe9250a55f.png) فارسی**](README_fa.md)          [**Русский 🇷🇺**](README_ru.md)          [**English 🇺🇸**](README.md)           +
+ +[**![Lang_farsi](https://user-images.githubusercontent.com/125398461/234186932-52f1fa82-52c6-417f-8b37-08fe9250a55f.png) فارسی**](README_fa.md)          [**Русский 🇷🇺**](README_ru.md)          [**English 🇺🇸**](README.md) +

- + [![GP-Intalls](https://img.shields.io/endpoint?color=green&logo=google-play&logoColor=green&url=https%3A%2F%2Fplay.cuzi.workers.dev%2Fplay%3Fi%3Dapp.hiddify.com%26l%3DGoogle%2520Play%26m%3D%24shortinstalls&style=flat-square)](https://play.google.com/store/apps/details?id=app.hiddify.com) [![Downloads](https://img.shields.io/github/downloads/hiddify/hiddify-next/total?style=flat-square&logo=github)](https://github.com/hiddify/hiddify-next/releases/)[![Last Version](https://img.shields.io/github/release/hiddify/hiddify-next/all.svg?style=flat-square)](https://github.com/hiddify/hiddify-next/releases/)[![Last Release Date](https://img.shields.io/github/release-date/hiddify/hiddify-next.svg?style=flat-square)](https://github.com/hiddify/hiddify-next/releases/)[![commits](https://img.shields.io/github/commit-activity/m/hiddify/hiddify-next?style=flat-square)](https://github.com/hiddify/hiddify-next/) [![Youtube](https://img.shields.io/youtube/channel/views/UCxrmeMvVryNfB4XL35lXQNg?label=Youtube&style=flat-square&logo=youtube)](https://www.youtube.com/@hiddify)[![Telegram Channel](https://img.shields.io/endpoint?label=Channel&style=flat-square&url=https%3A%2F%2Ftg.sumanjay.workers.dev%2Fhiddify&color=blue)](https://telegram.dog/hiddify)[![Telegram Group](https://img.shields.io/endpoint?color=neon&label=Support%20Group&style=flat-square&url=https%3A%2F%2Ftg.sumanjay.workers.dev%2Fhiddify_board)](https://telegram.dog/hiddify_board/5)
+ + ## Hiddify-Next 是什么? -基于 [Sing-box](https://github.com/SagerNet/sing-box) 的多平台客自动客户端,用作通用代理工具链。 该应用程序提供了广泛的功能,如下所列。 它还支持大量协议。 该应用程序免费使用、无广告且开源。 它提供了一个安全且私密的工具来访问免费互联网。 -该应用程序是使用 [Flutter](https://flutter.dev/) 和 [Go](https://go.dev/) 开发的。 欲了解更多信息,您可以阅读我们的开发贡献指南。 +

一个基于 Sing-box 的跨平台自动客户端,用作通用代理工具链。该应用提供了广泛的功能,如下所列。它还支持大量协议。该应用免费使用、无广告且开源。它为访问自由互联网提供了一个安全且私密的工具。

+该应用是使用 [Flutter](https://flutter.dev/) 和 [Go](https://go.dev/) 开发的。 欲了解更多信息,您可以参阅我们的开发贡献指南。
-Chinese Demo +English Demo +
-## 🚀 主要特点 -⭐ 简单的用户界面易于使用 -✈️ 多平台:Android、Windows、Linux 和 macOS(欢迎 iOS 的 PR) +## 🚀 主要功能 + +⭐ 简单易用的用户界面 + +✈️ 跨平台:Android、Windows、Linux 和 macOS(欢迎 iOS 的 PR) 🔍 基于延迟的自动选择 -🟡 广泛的协议支持:ECH、Sing-box、V2ray、Xray、Vless、Vmess、Trojan、Trojan with websocket、Reality、TUIC、Hysteria、Hysteria2、ShadowTLS、SSH、Clash、Clash meta +🟡 广泛的协议支持:**ECH, Sing-box, V2ray, Xray, Vless, Vmess, Reality, TUIC, Hysteria, ShadowTLS, SSH, Clash, Clash meta** -🟡 支持多种订阅链接导入:Clash、Clash meta、Sing-box 和 Shadowsocks +🟡 支持多种订阅链接导入: **Clash, Clash meta, Sing-box and Shadowsocks** -🔄 自动订阅更新 +🔄 自动更新订阅 -🔎 显示个人资料信息,包括剩余天数和流量使用情况 +🔎 显示包含了剩余天数和流量使用情况的配置文件信息 💻 完全免费,没有任何广告和干扰 -🛡 开源、安全且社区驱动 +🛡 开源、安全且由社区驱动 🌙 深色和浅色模式 -⚙ 与所有代理管理面板的节点兼容 +⚙ 兼容所有的代理管理面板 -⭐ 适用于伊朗、中国、俄罗斯等国家配置 +⭐ 适用于伊朗、中国、俄罗斯或其他国家的配置 -📱 可在 Google Play 上获取 +📱 可在 [Google Play](https://play.google.com/store/apps/details?id=app.hiddify.com) 上获取 ## 下载 @@ -58,7 +64,7 @@ 操作系统 - 下载 + 下载链接 @@ -66,9 +72,9 @@ Android

-
-
- +
+
+ @@ -83,58 +89,58 @@ Linux - + + +
## 安装和教程 -请在 [wiki 页面](https://github.com/hiddify/hiddify-next/wiki) 上查找教程信息。 +请在 [wiki 页面](https://github.com/hiddify/hiddify-next/wiki) 上获取教程信息。 ## 改进翻译 -您可以使用以下链接轻松地为该项目做出贡献以改进翻译: -- [简体中文](https://inlang.com/editor/github.com/hiddify/hiddify-next?lang=en&lang=zh) -- [英语](https://inlang.com/editor/github.com/hiddify/hiddify-next?lang=en) +您可以使用以下链接轻松地为该项目改进翻译以做出贡献: + - [英语](https://inlang.com/editor/github.com/hiddify/hiddify-next?lang=en) - [波斯语](https://inlang.com/editor/github.com/hiddify/hiddify-next?lang=en&lang=fa) - [俄语](https://inlang.com/editor/github.com/hiddify/hiddify-next?lang=en&lang=ru) +- [简体中文](https://inlang.com/editor/github.com/hiddify/hiddify-next?lang=en&lang=zh) ## 致谢 + - [Sing-box](https://github.com/SagerNet/sing-box) - [Sing-box for Android](https://github.com/SagerNet/sing-box-for-android) - [Clash](https://github.com/Dreamacro/clash) - [Clash Meta](https://github.com/MetaCubeX/Clash.Meta) - [FClash](https://github.com/Fclash/Fclash) -- [其他](./pubspec.yaml) -## 捐赠与支持 +- [Others](./pubspec.yaml) -支持我们的最简单方法是单击此页面顶部的 Star (⭐)。 +## 捐赠和支持 + +支持我们的最简单方法是单击此页面顶部的Star (⭐) 。
- + Star History Chart
-我们的服务也需要资金支持。我们所有的活动都是自愿进行的,资金支持将用于项目的开发和维护。您可以在 [此处](https://github.com/hiddify/hiddify-manager/wiki/support) 查看我们的支持地址。 +我们的服务也需要经济支持。我们所有的活动都是志愿性质的,经济支持将被用于项目的发展。您可以在 [这里](https://github.com/hiddify/hiddify-server/wiki/support) 查看我们的支持地址。 +## 合作与联系信息 +我们需要您的合作来推动这个项目的发展。如果您在这些领域是专家,请不要犹豫联系我们并提及您的技能。 + +- Flutter 开发 +- Swift 开发 +- Kotlin 开发 +- Go 开发 +

-## 协作和联系信息 -我们需要您的协作才能继续开发并维护此项目。如果您是这些领域的专家,请随时与我们联系 并提及你的技能。 - -* Flutter 开发 -* Swift 开发 -* Kotlin 开发 -* Go 开发 - - - -
- [![Email](https://img.shields.io/badge/Email-contribute@hiddify.com-005FF9?style=flat-square&logo=mail.ru)](mailto:contribute@hiddify.com) [![Telegram Channel](https://img.shields.io/endpoint?label=Channel&style=flat-square&url=https%3A%2F%2Ftg.sumanjay.workers.dev%2Fhiddify&color=blue)](https://telegram.dog/hiddify) [![Telegram Group](https://img.shields.io/endpoint?color=neon&label=Support%20Group&style=flat-square&url=https%3A%2F%2Ftg.sumanjay.workers.dev%2Fhiddify_board)](https://telegram.dog/hiddify_board) @@ -144,9 +150,10 @@

- 感谢所有参与该项目的人。包括以下列出的人,和更多其他来自 Github 的人。你们对我们的意义非常重大。 ♥

+我们非常感谢所有参与此项目的人,包括在这里的一些人和在Github之外的。这对我们来说意义重大。♥

- + +

@@ -156,4 +163,3 @@ 使用 Contrib.Rocks 制作

- From 63b9e37d1a3b215192f2e4a9b61f42eaad555033 Mon Sep 17 00:00:00 2001 From: hasankarli <42219301+hasankarli@users.noreply.github.com> Date: Thu, 16 Nov 2023 17:28:04 +0300 Subject: [PATCH 39/91] inlang: update translations --- assets/translations/strings_fa.i18n.json | 4 +- assets/translations/strings_ru.i18n.json | 4 +- assets/translations/strings_tr.i18n.json | 293 +++++++++++++++++++++++ assets/translations/strings_zh.i18n.json | 4 +- project.inlang.json | 6 +- 5 files changed, 302 insertions(+), 9 deletions(-) create mode 100644 assets/translations/strings_tr.i18n.json diff --git a/assets/translations/strings_fa.i18n.json b/assets/translations/strings_fa.i18n.json index c9bc0356..48b86072 100644 --- a/assets/translations/strings_fa.i18n.json +++ b/assets/translations/strings_fa.i18n.json @@ -241,13 +241,13 @@ "tray": { "dashboard": "داشبورد", "quit": "خروج", + "open": "باز کن", "status": { "connect": "اتصال", "connecting": "در حال اتصال", "disconnect": "قطع اتصال", "disconnecting": "در حال قطع اتصال" - }, - "open": "باز کن" + } }, "failure": { "unexpected": "خطای غیرمنتظره", diff --git a/assets/translations/strings_ru.i18n.json b/assets/translations/strings_ru.i18n.json index a41ea710..8ef241e2 100644 --- a/assets/translations/strings_ru.i18n.json +++ b/assets/translations/strings_ru.i18n.json @@ -241,13 +241,13 @@ "tray": { "dashboard": "Панель", "quit": "Выход", + "open": "Открыть", "status": { "connect": "Подключено", "connecting": "Подключение", "disconnect": "Отключено", "disconnecting": "Отключение" - }, - "open": "Открыть" + } }, "failure": { "unexpected": "Непредвиденная ошибка", diff --git a/assets/translations/strings_tr.i18n.json b/assets/translations/strings_tr.i18n.json new file mode 100644 index 00000000..3e34d68b --- /dev/null +++ b/assets/translations/strings_tr.i18n.json @@ -0,0 +1,293 @@ +{ + "general": { + "appTitle": "Hiddify Next", + "reset": "Sıfırla", + "toggle": { + "enabled": "Etkin", + "disabled": "Devre dışı" + }, + "state": { + "disable": "Devre dışı bırak" + }, + "sort": "Sırala", + "sortBy": "Sırala", + "addToClipboard": "Panoya ekle" + }, + "intro": { + "termsAndPolicyCaution(rich)": "devam ederek ${tap(@:about.termsAndConditions)} kabul etmiş olursunuz", + "start": "Başla" + }, + "home": { + "pageTitle": "Ana Sayfa", + "emptyProfilesMsg": "Aboneliği profili ekleyerek başlayın", + "noActiveProfileMsg": "Profil seçin", + "connection": { + "tapToConnect": "Bağlanmak için dokunun", + "connecting": "Bağlanıyor", + "disconnecting": "Bağlantı kesiliyor", + "connected": "Bağlandı" + }, + "stats": { + "traffic": "Canlı Trafik", + "trafficTotal": "Toplam Trafik", + "uplink": "Çıkış Yolu", + "downlink": "Giriş Yolu" + } + }, + "profile": { + "overviewPageTitle": "Profiller", + "detailsPageTitle": "Profil", + "activeProfileNameSemanticLabel": "Aktif profil adı: \"${name}\".", + "activeProfileBtnSemanticLabel": "Tüm profilleri görüntüleyin.", + "nonActiveProfileBtnSemanticLabel": "Aktif profil olarak \"${name}\" seçeneğini seçin.", + "subscription": { + "traffic": "Trafik", + "updatedTimeAgo": "${timeago} güncellendi", + "remainingDuration": "${duration} Gün Kaldı", + "remainingTrafficSemanticLabel": "${consumed}/${total} trafik tüketildi.", + "expired": "Süresi Doldu", + "noTraffic": "Kotal Doldu" + }, + "sortBy": { + "lastUpdate": "Yakın zamanda güncellendi", + "name": "Alfabetik" + }, + "add": { + "buttonText": "Yeni profil", + "shortBtnTxt": "Yeni profil", + "fromClipboard": "Panodan Ekle", + "scanQr": "QR kodunu tarayın", + "qrScanner": { + "permissionDeniedError": "İzin reddedildi", + "unexpectedError": "Bir şeyler yanlış gitti", + "torchSemanticLabel": "El feneri", + "facingSemanticLabel": "Kameraya önü" + }, + "manually": "Manuel giriş", + "addingProfileMsg": "Profil Ekleniyor", + "failureMsg": "Profil eklenemedi" + }, + "update": { + "buttonTxt": "Güncelle", + "tooltip": "Profili Güncelle", + "failureMsg": "Profil güncellenemedi", + "successMsg": "Profil başarıyla güncellendi" + }, + "share": { + "buttonText": "Paylaş", + "exportToClipboardSuccess": "Panoya aktarıldı", + "exportSubLinkToClipboard": "Abonelik bağlantısını panoya aktar", + "subLinkQrCode": "QR kodun abonelik bağlantısı ", + "exportConfigToClipboard": "Yapılandırmayı panoya aktar", + "exportConfigToClipboardSuccess": "Yapılandırma panoya kopyalandı" + }, + "edit": { + "buttonTxt": "Düzenle", + "selectActiveTxt": "Etkin profili seçin" + }, + "delete": { + "buttonTxt": "Sil", + "confirmationMsg": "Profil kalıcı olarak silinsin mi?", + "successMsg": "Profil başarıyla silindi" + }, + "save": { + "buttonText": "Kaydet", + "successMsg": "Profil başarıyla kaydedildi", + "failureMsg": "Profil kaydedilemedi" + }, + "detailsForm": { + "nameLabel": "İsim", + "nameHint": "Profil ismi", + "urlLabel": "URL", + "urlHint": "Tam yapılandırma URL'i", + "emptyNameMsg": "İsim gerekli", + "invalidUrlMsg": "Geçersiz URL", + "lastUpdate": "Son Güncelleme", + "updateInterval": "Otomatik güncelleme", + "updateIntervalDialogTitle": "Otomatik Güncelleme Aralığı (saat olarak)" + } + }, + "proxies": { + "pageTitle": "Proxyler", + "emptyProxiesMsg": "Kullanılabilir proxy yok", + "delayTestTooltip": "Test Gecikmesi", + "sortTooltip": "Proxy'leri Sırala", + "sortOptions": { + "unsorted": "Varsayılan", + "name": "Alfabetik olarak", + "delay": "Gecikmeyle" + } + }, + "logs": { + "pageTitle": "Log", + "filterHint": "Filtre", + "allLevelsFilter": "Tüm", + "shareCoreLogs": "Çekirdek Loglarını Paylaş", + "shareAppLogs": "Uygulama Loglarını paylaş", + "pauseTooltip": "Duraklat", + "resumeTooltip": "Devam et", + "clearTooltip": "Temizle" + }, + "settings": { + "pageTitle": "Ayarlar", + "requiresRestartMsg": "Bunun etkili olması için uygulamayı yeniden başlatın", + "general": { + "sectionTitle": "Genel", + "locale": "Dil", + "region": "Bölge", + "regionMsg": "Yerel adresleri atlamak için varsayılan seçeneği seçebilirsin", + "regions": { + "ir": "İran (ir)", + "cn": "Çin (cn)", + "ru": "Rusya (ru)", + "other": "Diğer" + }, + "themeMode": "Tema Modu", + "themeModes": { + "system": "Sistem temasını takip et", + "dark": "Karanlık mod", + "light": "Işık modu", + "black": "Siyah mod" + }, + "enableAnalytics": "Analitikleri Etkinleştir", + "enableAnalyticsMsg": "Uygulamayı iyileştirmek için analiz toplama ve kilitlenme raporları göndermeye izni verin", + "autoStart": "Önyüklemede Başlat", + "silentStart": "Sessiz Başlatma", + "openWorkingDir": "Çalışma Dizinini Aç", + "ignoreBatteryOptimizations": "Pil Optimizasyonunu Devre Dışı Bırak", + "ignoreBatteryOptimizationsMsg": "Optimum VPN performansı için kısıtlamaları kaldırın" + }, + "advanced": { + "sectionTitle": "Gelişmiş", + "debugMode": "Hata ayıklama modu", + "debugModeMsg": "Bu değişikliği uygulamak için uygulamayı yeniden başlatın", + "memoryLimit": "Bellek Sınırı" + }, + "network": { + "perAppProxyPageTitle": "Uygulama başına Proxy", + "perAppProxyModes": { + "off": "Tümü", + "offMsg": "Proxy tüm uygulamalar", + "include": "Proxy", + "includeMsg": "Yalnızca proxy seçilen uygulamalar", + "exclude": "Atlatma", + "excludeMsg": "Seçilen uygulamalara proxy uygulama" + }, + "showSystemApps": "Sistem uygulamalarını göster", + "hideSystemApps": "Sistem uygulamalarını gizle", + "clearSelection": "Seçimi temizle" + }, + "config": { + "serviceMode": "Servis modu", + "serviceModes": { + "proxy": "Proxy", + "systemProxy": "Sistem Proxy", + "tun": "VPN" + }, + "section": { + "route": "Rota Seçenekleri", + "dns": "DNS Seçenekleri", + "inbound": "Gelen Seçenekler", + "misc": "Çeşitli Seçenekler" + }, + "pageTitle": "Yapılandırma Seçenekleri", + "logLevel": "Log Seviyesi", + "resolveDestination": "Hedefi Çöz", + "ipv6Mode": "IPv6 Rotası", + "ipv6Modes": { + "disable": "Devre dışı bırak", + "enable": "Etkinleştir", + "prefer": "Tercih edilen", + "only": "Özel" + }, + "remoteDnsAddress": "Uzak DNS", + "remoteDnsDomainStrategy": "Uzak DNS Domain Stratejisi", + "directDnsAddress": "Doğrudan DNS", + "directDnsDomainStrategy": "Doğrudan DNS Domain Stratejisi", + "mixedPort": "Mixed Port", + "localDnsPort": "Yerel DNS Bağlantı Noktası", + "tunImplementation": "TUN İmplementasyonu", + "mtu": "MTU", + "connectionTestUrl": "Bağlantı Testi URL'i", + "urlTestInterval": "URL Test Aralığı", + "enableClashApi": "Clash API'yi etkinleştir", + "clashApiPort": "Clash API Bağlantı Noktası", + "enableTun": "TUN'u etkinleştir", + "setSystemProxy": "Sistem Proxy'sini Ayarla", + "enableFakeDns": "Sahte DNS'yi Etkinleştir", + "bypassLan": "Lan'ı Atla", + "strictRoute": "Kesin Rota" + } + }, + "about": { + "pageTitle": "Hakkında", + "version": "Sürüm", + "sourceCode": "Kaynak kodu", + "telegramChannel": "Telegram Kanalı", + "checkForUpdate": "Güncellemeleri kontrol et", + "privacyPolicy": "Gizlilik Politikası", + "termsAndConditions": "Şartlar ve koşullar" + }, + "appUpdate": { + "notAvailableMsg": " En son sürümü kullanıyorsunuz", + "dialogTitle": "Yeni Güncell", + "updateMsg": "@:general .appTitle'ın yeni bir sürümü mevcut. Şimdi güncellemek ister misiniz?", + "currentVersionLbl": "Şimdiki versiyon", + "newVersionLbl": "Yeni versiyon", + "updateNowBtnTxt": "Şimdi güncelle", + "laterBtnTxt": "Daha sonra", + "ignoreBtnTxt": "Görmezden gel" + }, + "tray": { + "dashboard": "Gösterge Paneli", + "quit": "Çıkış yap", + "open": "Açık", + "status": { + "connect": "Bağlan", + "connecting": "Bağlanıyor", + "disconnect": "Bağlantıyı kes", + "disconnecting": "Bağlantı kesiliyor" + } + }, + "failure": { + "unexpected": "Beklenmeyen hata", + "clash": { + "unexpected": "Beklenmeyen hata", + "core": "Çakışma Hatası ${reason}" + }, + "singbox": { + "unexpected": "Beklenmedik Hizmet Hatası", + "serviceNotRunning": "Servis çalışmıyor", + "missingPrivilege": "Eksik Ayrıcalık", + "missingPrivilegeMsg": "VPN modu yönetici ayrıcalıkları gerektirir. Uygulamayı yönetici olarak yeniden başlatın veya hizmet modunu değiştirin.", + "invalidConfigOptions": "Geçersiz yapılandırma seçenekleri", + "invalidConfig": "Geçersiz Yapılandırma", + "create": "Servis oluşturma hatası", + "start": "Servis başlatma hatası" + }, + "connectivity": { + "unexpected": "Beklenmedik Hata", + "missingVpnPermission": "Eksik VPN İzni", + "missingNotificationPermission": "Eksik Bildirim İzni", + "core": "Temel Hata" + }, + "profiles": { + "unexpected": "Beklenmedik hata", + "notFound": "Profil bulunamadı", + "invalidConfig": "Geçersiz Yapılandırmalar", + "invalidUrl": "Geçersiz URL" + }, + "connection": { + "unexpected": "Beklenmeyen bağlantı hatası", + "timeout": "Bağlantı zamanaşımına uğradı", + "badResponse": "Kötü yanıt", + "connectionError": "Bağlantı hatası", + "badCertificate": "Kötü sertifika" + } + }, + "play": { + "title": "Hiddify Next (Önizleme)", + "short_description": "Otomatik, SSH, VLESS, Vmess, Trojan, Reality, Sing-Box, Clash, Xray, Shadowsocks", + "full_description": "HiddifyNext'in temel hedefi güvenli, kullanıcı dostu ve verimli bir tünel istemcisi sağlamaktır. VPN Hizmeti iznini kullanarak tüm trafiği veya seçilen uygulama trafiğini seçtiğiniz uzak bir sunucuya yönlendirmenizi sağlar. Not: Herhangi bir sunucu sağlamıyoruz; kullanıcıların kendi barındırılan sunucularını veya güvenilir sunucularını kullanarak çevrimiçi etkinliklerinin gizli kalmasını sağlamaları gerekir. Sunucuları aşağıdakilerle destekliyoruz: - Normal V2ray/Xray Abonelik Bağlantısı - Clash Abonelik Bağlantısı - Sing-Box Abonelik Bağlantısı Benzersiz özelliklerimiz nelerdir? - Kullanıcı Dostu - Optimize Edilmiş ve Hızlı - En Düşük Ping'i otomatik olarak seçin - Kullanıcı kullanım bilgilerini gösterin - Derin bağlantı kullanarak tek tıklamayla alt bağlantıyı kolayca içe aktarın - Ücretsiz ve ADS Yok - Kullanıcı alt bağlantılarını kolayca değiştirin - giderek daha fazla Destek: - Sing-Box tarafından desteklenen tüm Protokoller - VLESS + xtls gerçeklik, vizyon - VMESS - Trojan - ShoadowSocks - Reality - V2ray - Hystria2 - TUIC - SSH - ShadowTLS Kaynak kodu https://github.com/hiddify/Hiddify-Next adresinde mevcuttur. Uygulama çekirdeği açık tabanlıdır. kaynak şarkı kutusu. İzin Açıklaması: - VPN Hizmeti: Bu uygulamanın amacı güvenli, kullanıcı dostu ve verimli bir tünel istemcisi sağlamak olduğundan, trafiği tünel aracılığıyla uzak sunucuya yönlendirebilmek için bu izne ihtiyacımız var. - TÜM PAKETLERİ SORGULAYIN: Bu izin, kullanıcıların tünelleme için belirli uygulamaları dahil etmesine veya hariç tutmasına izin vermek için kullanılır. - ALMA ÖNYÜKLEME TAMAMLANDI: Bu izin, cihaz önyüklemesi sırasında bu uygulamayı etkinleştirmek için uygulama ayarlarından etkinleştirilebilir veya devre dışı bırakılabilir. - BİLDİRİMLER SONRASI: VPN hizmetinin sürekli çalışmasını sağlamak için bir ön plan hizmeti kullandığımız için bu izin önemlidir. - Bu uygulama reklam içermez. Analitik ve kilitlenme verileri yalnızca uygulamanın ilk kullanımında kullanıcının açık rızası ile gerçekleşir." + } +} \ No newline at end of file diff --git a/assets/translations/strings_zh.i18n.json b/assets/translations/strings_zh.i18n.json index f3117624..8a37edce 100644 --- a/assets/translations/strings_zh.i18n.json +++ b/assets/translations/strings_zh.i18n.json @@ -241,13 +241,13 @@ "tray": { "dashboard": "控制面板", "quit": "退出", + "open": "打开", "status": { "connect": "连接", "connecting": "正在连接", "disconnect": "已断开连接", "disconnecting": "正在断开连接" - }, - "open": "打开" + } }, "failure": { "unexpected": "意外错误", diff --git a/project.inlang.json b/project.inlang.json index 8f748ec1..3c284a91 100644 --- a/project.inlang.json +++ b/project.inlang.json @@ -4,7 +4,8 @@ "languageTags": [ "en", "fa", - "ru", + "ru", + "tr", "zh" ], "modules": [ @@ -20,5 +21,4 @@ "@:" ] } -} - +} \ No newline at end of file From c33d555041623c6c541d2f4188002268d7bbff16 Mon Sep 17 00:00:00 2001 From: Hiddify <114227601+hiddify-com@users.noreply.github.com> Date: Thu, 16 Nov 2023 15:57:10 +0100 Subject: [PATCH 40/91] fix bug in strings_tr.i18n.json --- assets/translations/strings_tr.i18n.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/translations/strings_tr.i18n.json b/assets/translations/strings_tr.i18n.json index 3e34d68b..ba5a12df 100644 --- a/assets/translations/strings_tr.i18n.json +++ b/assets/translations/strings_tr.i18n.json @@ -231,7 +231,7 @@ "appUpdate": { "notAvailableMsg": " En son sürümü kullanıyorsunuz", "dialogTitle": "Yeni Güncell", - "updateMsg": "@:general .appTitle'ın yeni bir sürümü mevcut. Şimdi güncellemek ister misiniz?", + "updateMsg": "@:general.appTitle'ın yeni bir sürümü mevcut. Şimdi güncellemek ister misiniz?", "currentVersionLbl": "Şimdiki versiyon", "newVersionLbl": "Yeni versiyon", "updateNowBtnTxt": "Şimdi güncelle", @@ -290,4 +290,4 @@ "short_description": "Otomatik, SSH, VLESS, Vmess, Trojan, Reality, Sing-Box, Clash, Xray, Shadowsocks", "full_description": "HiddifyNext'in temel hedefi güvenli, kullanıcı dostu ve verimli bir tünel istemcisi sağlamaktır. VPN Hizmeti iznini kullanarak tüm trafiği veya seçilen uygulama trafiğini seçtiğiniz uzak bir sunucuya yönlendirmenizi sağlar. Not: Herhangi bir sunucu sağlamıyoruz; kullanıcıların kendi barındırılan sunucularını veya güvenilir sunucularını kullanarak çevrimiçi etkinliklerinin gizli kalmasını sağlamaları gerekir. Sunucuları aşağıdakilerle destekliyoruz: - Normal V2ray/Xray Abonelik Bağlantısı - Clash Abonelik Bağlantısı - Sing-Box Abonelik Bağlantısı Benzersiz özelliklerimiz nelerdir? - Kullanıcı Dostu - Optimize Edilmiş ve Hızlı - En Düşük Ping'i otomatik olarak seçin - Kullanıcı kullanım bilgilerini gösterin - Derin bağlantı kullanarak tek tıklamayla alt bağlantıyı kolayca içe aktarın - Ücretsiz ve ADS Yok - Kullanıcı alt bağlantılarını kolayca değiştirin - giderek daha fazla Destek: - Sing-Box tarafından desteklenen tüm Protokoller - VLESS + xtls gerçeklik, vizyon - VMESS - Trojan - ShoadowSocks - Reality - V2ray - Hystria2 - TUIC - SSH - ShadowTLS Kaynak kodu https://github.com/hiddify/Hiddify-Next adresinde mevcuttur. Uygulama çekirdeği açık tabanlıdır. kaynak şarkı kutusu. İzin Açıklaması: - VPN Hizmeti: Bu uygulamanın amacı güvenli, kullanıcı dostu ve verimli bir tünel istemcisi sağlamak olduğundan, trafiği tünel aracılığıyla uzak sunucuya yönlendirebilmek için bu izne ihtiyacımız var. - TÜM PAKETLERİ SORGULAYIN: Bu izin, kullanıcıların tünelleme için belirli uygulamaları dahil etmesine veya hariç tutmasına izin vermek için kullanılır. - ALMA ÖNYÜKLEME TAMAMLANDI: Bu izin, cihaz önyüklemesi sırasında bu uygulamayı etkinleştirmek için uygulama ayarlarından etkinleştirilebilir veya devre dışı bırakılabilir. - BİLDİRİMLER SONRASI: VPN hizmetinin sürekli çalışmasını sağlamak için bir ön plan hizmeti kullandığımız için bu izin önemlidir. - Bu uygulama reklam içermez. Analitik ve kilitlenme verileri yalnızca uygulamanın ilk kullanımında kullanıcının açık rızası ile gerçekleşir." } -} \ No newline at end of file +} From 82b8e1b6f020f22efe6b1712c634d5d74625f288 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Fri, 17 Nov 2023 21:30:09 +0330 Subject: [PATCH 41/91] Add geo assets settings --- assets/translations/strings_en.i18n.json | 14 ++ assets/translations/strings_fa.i18n.json | 14 ++ assets/translations/strings_ru.i18n.json | 14 ++ assets/translations/strings_zh.i18n.json | 14 ++ lib/core/router/routes/desktop_routes.dart | 20 +++ lib/core/router/routes/mobile_routes.dart | 22 +++ lib/data/data_providers.dart | 43 ++++- lib/data/local/dao/dao.dart | 1 + lib/data/local/dao/geo_assets_dao.dart | 36 ++++ lib/data/local/data_mappers.dart | 27 +++ lib/data/local/database.dart | 23 ++- lib/data/local/schema_versions.dart | 95 ++++++++++ lib/data/local/schemas/drift_schema_v3.json | 1 + lib/data/local/tables.dart | 20 +++ lib/data/repository/config_options_store.dart | 11 -- lib/data/repository/core_facade_impl.dart | 12 +- .../repository/geo_assets_repository.dart | 140 +++++++++++++++ lib/domain/rules/geo_asset.dart | 51 ++++++ lib/domain/rules/geo_asset_failure.dart | 39 ++++ lib/domain/rules/geo_assets_repository.dart | 12 ++ lib/domain/singbox/config_options.dart | 2 + .../settings/geo_assets/geo_asset_tile.dart | 116 ++++++++++++ .../geo_assets/geo_assets_notifier.dart | 28 +++ .../settings/geo_assets/geo_assets_page.dart | 35 ++++ .../widgets/advanced_setting_tiles.dart | 7 + lib/services/files_editor_service.dart | 36 +++- libcore | 2 +- .../local/generated_migrations/schema.dart | 5 +- .../local/generated_migrations/schema_v3.dart | 168 ++++++++++++++++++ test/data/local/migrations_test.dart | 24 ++- 30 files changed, 1003 insertions(+), 29 deletions(-) create mode 100644 lib/data/local/dao/geo_assets_dao.dart create mode 100644 lib/data/local/schemas/drift_schema_v3.json create mode 100644 lib/data/repository/geo_assets_repository.dart create mode 100644 lib/domain/rules/geo_asset.dart create mode 100644 lib/domain/rules/geo_asset_failure.dart create mode 100644 lib/domain/rules/geo_assets_repository.dart create mode 100644 lib/features/settings/geo_assets/geo_asset_tile.dart create mode 100644 lib/features/settings/geo_assets/geo_assets_notifier.dart create mode 100644 lib/features/settings/geo_assets/geo_assets_page.dart create mode 100644 test/data/local/generated_migrations/schema_v3.dart diff --git a/assets/translations/strings_en.i18n.json b/assets/translations/strings_en.i18n.json index cae1904b..98696753 100644 --- a/assets/translations/strings_en.i18n.json +++ b/assets/translations/strings_en.i18n.json @@ -217,6 +217,15 @@ "enableFakeDns": "Enable Fake DNS", "bypassLan": "Bypass Lan", "strictRoute": "Strict Route" + }, + "geoAssets": { + "pageTitle": "Routing Assets", + "version": "Version ${version}", + "fileMissing": "File Missing", + "update": "Update", + "download": "Download", + "failureMsg": "Failed to update asset", + "successMsg": "Successfully updated asset" } }, "about": { @@ -283,6 +292,11 @@ "badResponse": "Bad response", "connectionError": "Connection error", "badCertificate": "Bad certificate" + }, + "geoAssets": { + "unexpected": "Unexpected Error", + "notUpdate": "No Update Available", + "activeNotFound": "Active Geo Asset Not Found" } }, "play": { diff --git a/assets/translations/strings_fa.i18n.json b/assets/translations/strings_fa.i18n.json index 48b86072..426bc200 100644 --- a/assets/translations/strings_fa.i18n.json +++ b/assets/translations/strings_fa.i18n.json @@ -217,6 +217,15 @@ "enableFakeDns": "Enable Fake DNS", "bypassLan": "Bypass Lan", "strictRoute": "Strict Route" + }, + "geoAssets": { + "pageTitle": "فایل‌های مسیریابی", + "version": "نسخه ${version}", + "fileMissing": "فایل موجود نیست", + "update": "به روز رسانی", + "download": "دانلود", + "failureMsg": "دارایی به روز نشد", + "successMsg": "دارایی با موفقیت به روز شد" } }, "about": { @@ -283,6 +292,11 @@ "badResponse": "پاسخ نامعتبر", "connectionError": "خطای اتصال", "badCertificate": "خطای اعتبار سنجی" + }, + "geoAssets": { + "unexpected": "خطای غیرمنتظره", + "notUpdate": "به روز رسانی موجود نیست", + "activeNotFound": "Active Geo Asset یافت نشد" } }, "play": { diff --git a/assets/translations/strings_ru.i18n.json b/assets/translations/strings_ru.i18n.json index 35e085f6..b30959d6 100644 --- a/assets/translations/strings_ru.i18n.json +++ b/assets/translations/strings_ru.i18n.json @@ -217,6 +217,15 @@ "enableFakeDns": "Использовать поддельную DNS", "bypassLan": "Обход локальной сети", "strictRoute": "Строгая маршрутизация" + }, + "geoAssets": { + "pageTitle": "Активы маршрутизации", + "version": "Версия ${version}", + "fileMissing": "Файл отсутствует", + "update": "Обновлять", + "download": "Скачать", + "failureMsg": "Не удалось обновить объект.", + "successMsg": "Объект успешно обновлен." } }, "about": { @@ -283,6 +292,11 @@ "badResponse": "Неправильный ответ", "connectionError": "Ошибка подключения", "badCertificate": "Неправильный сертификат" + }, + "geoAssets": { + "unexpected": "Неожиданная ошибка", + "notUpdate": "Нет доступных обновлений", + "activeNotFound": "Активный географический актив не найден" } }, "play": { diff --git a/assets/translations/strings_zh.i18n.json b/assets/translations/strings_zh.i18n.json index 8a37edce..30ba3d30 100644 --- a/assets/translations/strings_zh.i18n.json +++ b/assets/translations/strings_zh.i18n.json @@ -217,6 +217,15 @@ "enableFakeDns": "启用 Fake DNS", "bypassLan": "绕过局域网", "strictRoute": "严格路由" + }, + "geoAssets": { + "pageTitle": "路由资产", + "version": "版本${version}", + "fileMissing": "文件丢失", + "update": "更新", + "download": "下载", + "failureMsg": "更新资产失败", + "successMsg": "已成功更新资产" } }, "about": { @@ -283,6 +292,11 @@ "badResponse": "错误响应", "connectionError": "连接错误", "badCertificate": "证书无效" + }, + "geoAssets": { + "unexpected": "意外的错误", + "notUpdate": "无可用更新", + "activeNotFound": "未找到活动地理资产" } }, "play": { diff --git a/lib/core/router/routes/desktop_routes.dart b/lib/core/router/routes/desktop_routes.dart index 20c1f87d..186f7ddd 100644 --- a/lib/core/router/routes/desktop_routes.dart +++ b/lib/core/router/routes/desktop_routes.dart @@ -4,6 +4,7 @@ import 'package:hiddify/core/router/routes/shared_routes.dart'; import 'package:hiddify/features/about/view/view.dart'; import 'package:hiddify/features/common/adaptive_root_scaffold.dart'; import 'package:hiddify/features/logs/view/view.dart'; +import 'package:hiddify/features/settings/geo_assets/geo_assets_page.dart'; import 'package:hiddify/features/settings/view/view.dart'; part 'desktop_routes.g.dart'; @@ -48,6 +49,10 @@ part 'desktop_routes.g.dart'; path: ConfigOptionsRoute.path, name: ConfigOptionsRoute.name, ), + TypedGoRoute( + path: GeoAssetsRoute.path, + name: GeoAssetsRoute.name, + ), ], ), TypedGoRoute( @@ -102,6 +107,21 @@ class ConfigOptionsRoute extends GoRouteData { } } +class GeoAssetsRoute extends GoRouteData { + const GeoAssetsRoute(); + static const path = 'routing-assets'; + static const name = 'Routing Assets'; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + return const MaterialPage( + fullscreenDialog: true, + name: name, + child: GeoAssetsPage(), + ); + } +} + class AboutRoute extends GoRouteData { const AboutRoute(); static const path = '/about'; diff --git a/lib/core/router/routes/mobile_routes.dart b/lib/core/router/routes/mobile_routes.dart index 8cfd3ca3..79c28024 100644 --- a/lib/core/router/routes/mobile_routes.dart +++ b/lib/core/router/routes/mobile_routes.dart @@ -5,6 +5,7 @@ import 'package:hiddify/core/router/routes/shared_routes.dart'; import 'package:hiddify/features/about/view/view.dart'; import 'package:hiddify/features/common/adaptive_root_scaffold.dart'; import 'package:hiddify/features/logs/view/view.dart'; +import 'package:hiddify/features/settings/geo_assets/geo_assets_page.dart'; import 'package:hiddify/features/settings/view/view.dart'; part 'mobile_routes.g.dart'; @@ -47,6 +48,10 @@ part 'mobile_routes.g.dart'; path: PerAppProxyRoute.path, name: PerAppProxyRoute.name, ), + TypedGoRoute( + path: GeoAssetsRoute.path, + name: GeoAssetsRoute.name, + ), ], ), TypedGoRoute( @@ -138,6 +143,23 @@ class PerAppProxyRoute extends GoRouteData { } } +class GeoAssetsRoute extends GoRouteData { + const GeoAssetsRoute(); + static const path = 'routing-assets'; + static const name = 'Routing Assets'; + + static final GlobalKey $parentNavigatorKey = rootNavigatorKey; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + return const MaterialPage( + fullscreenDialog: true, + name: name, + child: GeoAssetsPage(), + ); + } +} + class AboutRoute extends GoRouteData { const AboutRoute(); static const path = 'about'; diff --git a/lib/data/data_providers.dart b/lib/data/data_providers.dart index e4ac609d..f1c47452 100644 --- a/lib/data/data_providers.dart +++ b/lib/data/data_providers.dart @@ -8,11 +8,14 @@ import 'package:hiddify/data/local/dao/dao.dart'; import 'package:hiddify/data/local/database.dart'; import 'package:hiddify/data/repository/app_repository_impl.dart'; import 'package:hiddify/data/repository/config_options_store.dart'; +import 'package:hiddify/data/repository/geo_assets_repository.dart'; import 'package:hiddify/data/repository/repository.dart'; import 'package:hiddify/domain/app/app.dart'; import 'package:hiddify/domain/constants.dart'; import 'package:hiddify/domain/core_facade.dart'; import 'package:hiddify/domain/profiles/profiles.dart'; +import 'package:hiddify/domain/rules/geo_assets_repository.dart'; +import 'package:hiddify/domain/singbox/singbox.dart'; import 'package:hiddify/services/service_providers.dart'; import 'package:native_dio_adapter/native_dio_adapter.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -67,6 +70,44 @@ AppRepository appRepository(AppRepositoryRef ref) => @Riverpod(keepAlive: true) ClashApi clashApi(ClashApiRef ref) => ClashApi(Defaults.clashApiPort); +@Riverpod(keepAlive: true) +GeoAssetsDao geoAssetsDao(GeoAssetsDaoRef ref) => GeoAssetsDao( + ref.watch(appDatabaseProvider), + ); + +@Riverpod(keepAlive: true) +GeoAssetsRepository geoAssetsRepository(GeoAssetsRepositoryRef ref) { + return GeoAssetsRepositoryImpl( + geoAssetsDao: ref.watch(geoAssetsDaoProvider), + dio: ref.watch(dioProvider), + filesEditor: ref.watch(filesEditorServiceProvider), + ); +} + +@riverpod +Future configOptions(ConfigOptionsRef ref) async { + final geoAssets = await ref + .watch(geoAssetsRepositoryProvider) + .getActivePair() + .getOrElse((l) => throw l) + .run(); + final filesEditor = ref.watch(filesEditorServiceProvider); + + final serviceMode = ref.watch(serviceModeStoreProvider); + return ref.watch(configPreferencesProvider).copyWith( + enableTun: serviceMode == ServiceMode.tun, + setSystemProxy: serviceMode == ServiceMode.systemProxy, + geoipPath: filesEditor.geoAssetRelativePath( + geoAssets.geoip.providerName, + geoAssets.geoip.fileName, + ), + geositePath: filesEditor.geoAssetRelativePath( + geoAssets.geosite.providerName, + geoAssets.geosite.fileName, + ), + ); +} + @Riverpod(keepAlive: true) CoreFacade coreFacade(CoreFacadeRef ref) => CoreFacadeImpl( ref.watch(singboxServiceProvider), @@ -74,5 +115,5 @@ CoreFacade coreFacade(CoreFacadeRef ref) => CoreFacadeImpl( ref.watch(platformServicesProvider), ref.watch(clashApiProvider), ref.read(debugModeNotifierProvider), - () => ref.read(configOptionsProvider), + () => ref.read(configOptionsProvider.future), ); diff --git a/lib/data/local/dao/dao.dart b/lib/data/local/dao/dao.dart index 1d0c3ad7..e267403f 100644 --- a/lib/data/local/dao/dao.dart +++ b/lib/data/local/dao/dao.dart @@ -1 +1,2 @@ +export 'geo_assets_dao.dart'; export 'profiles_dao.dart'; diff --git a/lib/data/local/dao/geo_assets_dao.dart b/lib/data/local/dao/geo_assets_dao.dart new file mode 100644 index 00000000..3b27a9f6 --- /dev/null +++ b/lib/data/local/dao/geo_assets_dao.dart @@ -0,0 +1,36 @@ +import 'package:drift/drift.dart'; +import 'package:hiddify/data/local/data_mappers.dart'; +import 'package:hiddify/data/local/database.dart'; +import 'package:hiddify/data/local/tables.dart'; +import 'package:hiddify/domain/rules/geo_asset.dart'; +import 'package:hiddify/utils/custom_loggers.dart'; + +part 'geo_assets_dao.g.dart'; + +@DriftAccessor(tables: [GeoAssetEntries]) +class GeoAssetsDao extends DatabaseAccessor + with _$GeoAssetsDaoMixin, InfraLogger { + GeoAssetsDao(super.db); + + Future getActive(GeoAssetType type) async { + return (geoAssetEntries.select() + ..where((tbl) => tbl.active.equals(true)) + ..where((tbl) => tbl.type.equalsValue(type)) + ..limit(1)) + .map(GeoAssetMapper.fromEntry) + .getSingleOrNull(); + } + + Stream> watchAll() { + return geoAssetEntries.select().map(GeoAssetMapper.fromEntry).watch(); + } + + Future edit(GeoAsset patch) async { + await transaction( + () async { + await (update(geoAssetEntries)..where((tbl) => tbl.id.equals(patch.id))) + .write(patch.toCompanion()); + }, + ); + } +} diff --git a/lib/data/local/data_mappers.dart b/lib/data/local/data_mappers.dart index 0646a749..571afd89 100644 --- a/lib/data/local/data_mappers.dart +++ b/lib/data/local/data_mappers.dart @@ -1,6 +1,7 @@ import 'package:drift/drift.dart'; import 'package:hiddify/data/local/database.dart'; import 'package:hiddify/domain/profiles/profiles.dart'; +import 'package:hiddify/domain/rules/geo_asset.dart'; extension ProfileMapper on Profile { ProfileEntriesCompanion toCompanion() { @@ -71,3 +72,29 @@ extension ProfileMapper on Profile { }; } } + +extension GeoAssetMapper on GeoAsset { + GeoAssetEntriesCompanion toCompanion() { + return GeoAssetEntriesCompanion.insert( + id: id, + type: type, + active: active, + name: name, + providerName: providerName, + version: Value(version), + lastCheck: Value(lastCheck), + ); + } + + static GeoAsset fromEntry(GeoAssetEntry e) { + return GeoAsset( + id: e.id, + name: e.name, + type: e.type, + active: e.active, + providerName: e.providerName, + version: e.version, + lastCheck: e.lastCheck, + ); + } +} diff --git a/lib/data/local/database.dart b/lib/data/local/database.dart index a837c1ef..2800f77e 100644 --- a/lib/data/local/database.dart +++ b/lib/data/local/database.dart @@ -3,29 +3,35 @@ import 'dart:io'; import 'package:drift/drift.dart'; import 'package:drift/native.dart'; import 'package:hiddify/data/local/dao/dao.dart'; +import 'package:hiddify/data/local/data_mappers.dart'; import 'package:hiddify/data/local/schema_versions.dart'; import 'package:hiddify/data/local/tables.dart'; import 'package:hiddify/data/local/type_converters.dart'; import 'package:hiddify/domain/profiles/profiles.dart'; +import 'package:hiddify/domain/rules/geo_asset.dart'; import 'package:hiddify/services/files_editor_service.dart'; import 'package:path/path.dart' as p; part 'database.g.dart'; -@DriftDatabase(tables: [ProfileEntries], daos: [ProfilesDao]) +@DriftDatabase( + tables: [ProfileEntries, GeoAssetEntries], + daos: [ProfilesDao, GeoAssetsDao], +) class AppDatabase extends _$AppDatabase { AppDatabase({required QueryExecutor connection}) : super(connection); AppDatabase.connect() : super(_openConnection()); @override - int get schemaVersion => 2; + int get schemaVersion => 3; @override MigrationStrategy get migration { return MigrationStrategy( onCreate: (Migrator m) async { await m.createAll(); + await _prePopulateGeoAssets(); }, onUpgrade: stepByStep( // add type column to profile entries table @@ -41,9 +47,22 @@ class AppDatabase extends _$AppDatabase { ), ); }, + from2To3: (m, schema) async { + await m.createTable(schema.geoAssetEntries); + await _prePopulateGeoAssets(); + }, ), ); } + + Future _prePopulateGeoAssets() async { + await transaction(() async { + final geoAssets = defaultGeoAssets.map((e) => e.toCompanion()); + for (final geoAsset in geoAssets) { + await into(geoAssetEntries).insert(geoAsset); + } + }); + } } LazyDatabase _openConnection() { diff --git a/lib/data/local/schema_versions.dart b/lib/data/local/schema_versions.dart index 7a743f02..da7a4d2b 100644 --- a/lib/data/local/schema_versions.dart +++ b/lib/data/local/schema_versions.dart @@ -111,8 +111,96 @@ i1.GeneratedColumn _column_11(String aliasedName) => i1.GeneratedColumn _column_12(String aliasedName) => i1.GeneratedColumn('support_url', aliasedName, true, type: i1.DriftSqlType.string); + +final class _S3 extends i0.VersionedSchema { + _S3({required super.database}) : super(version: 3); + @override + late final List entities = [ + profileEntries, + geoAssetEntries, + ]; + late final Shape0 profileEntries = Shape0( + source: i0.VersionedTable( + entityName: 'profile_entries', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'PRIMARY KEY(id)', + ], + columns: [ + _column_0, + _column_1, + _column_2, + _column_3, + _column_4, + _column_5, + _column_6, + _column_7, + _column_8, + _column_9, + _column_10, + _column_11, + _column_12, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape1 geoAssetEntries = Shape1( + source: i0.VersionedTable( + entityName: 'geo_asset_entries', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'PRIMARY KEY(id)', + 'UNIQUE(name, provider_name)', + ], + columns: [ + _column_0, + _column_1, + _column_2, + _column_3, + _column_13, + _column_14, + _column_15, + ], + attachedDatabase: database, + ), + alias: null); +} + +class Shape1 extends i0.VersionedTable { + Shape1({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get id => + columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get type => + columnsByName['type']! as i1.GeneratedColumn; + i1.GeneratedColumn get active => + columnsByName['active']! as i1.GeneratedColumn; + i1.GeneratedColumn get name => + columnsByName['name']! as i1.GeneratedColumn; + i1.GeneratedColumn get providerName => + columnsByName['provider_name']! as i1.GeneratedColumn; + i1.GeneratedColumn get version => + columnsByName['version']! as i1.GeneratedColumn; + i1.GeneratedColumn get lastCheck => + columnsByName['last_check']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_13(String aliasedName) => + i1.GeneratedColumn('provider_name', aliasedName, false, + additionalChecks: i1.GeneratedColumn.checkTextLength( + minTextLength: 1, + ), + type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_14(String aliasedName) => + i1.GeneratedColumn('version', aliasedName, true, + type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_15(String aliasedName) => + i1.GeneratedColumn('last_check', aliasedName, true, + type: i1.DriftSqlType.dateTime); i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, _S2 schema) from1To2, + required Future Function(i1.Migrator m, _S3 schema) from2To3, }) { return (currentVersion, database) async { switch (currentVersion) { @@ -121,6 +209,11 @@ i0.MigrationStepWithVersion migrationSteps({ final migrator = i1.Migrator(database, schema); await from1To2(migrator, schema); return 2; + case 2: + final schema = _S3(database: database); + final migrator = i1.Migrator(database, schema); + await from2To3(migrator, schema); + return 3; default: throw ArgumentError.value('Unknown migration from $currentVersion'); } @@ -129,8 +222,10 @@ i0.MigrationStepWithVersion migrationSteps({ i1.OnUpgrade stepByStep({ required Future Function(i1.Migrator m, _S2 schema) from1To2, + required Future Function(i1.Migrator m, _S3 schema) from2To3, }) => i0.VersionedSchema.stepByStepHelper( step: migrationSteps( from1To2: from1To2, + from2To3: from2To3, )); diff --git a/lib/data/local/schemas/drift_schema_v3.json b/lib/data/local/schemas/drift_schema_v3.json new file mode 100644 index 00000000..4e845f77 --- /dev/null +++ b/lib/data/local/schemas/drift_schema_v3.json @@ -0,0 +1 @@ +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.1.0"},"options":{"store_date_time_values_as_text":true},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"profile_entries","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(ProfileType.values)","dart_type_name":"ProfileType"}},{"name":"active","getter_name":"active","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"active\" IN (0, 1))","default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[{"allowed-lengths":{"min":1,"max":null}}]},{"name":"url","getter_name":"url","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"last_update","getter_name":"lastUpdate","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"update_interval","getter_name":"updateInterval","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"DurationTypeConverter()","dart_type_name":"Duration"}},{"name":"upload","getter_name":"upload","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"download","getter_name":"download","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"total","getter_name":"total","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"expire","getter_name":"expire","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"web_page_url","getter_name":"webPageUrl","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"support_url","getter_name":"supportUrl","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":1,"references":[],"type":"table","data":{"name":"geo_asset_entries","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(GeoAssetType.values)","dart_type_name":"GeoAssetType"}},{"name":"active","getter_name":"active","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"active\" IN (0, 1))","default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[{"allowed-lengths":{"min":1,"max":null}}]},{"name":"provider_name","getter_name":"providerName","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[{"allowed-lengths":{"min":1,"max":null}}]},{"name":"version","getter_name":"version","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"last_check","getter_name":"lastCheck","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"],"unique_keys":[["name","provider_name"]]}}]} \ No newline at end of file diff --git a/lib/data/local/tables.dart b/lib/data/local/tables.dart index 17bba834..18fe4cdc 100644 --- a/lib/data/local/tables.dart +++ b/lib/data/local/tables.dart @@ -1,6 +1,7 @@ import 'package:drift/drift.dart'; import 'package:hiddify/data/local/type_converters.dart'; import 'package:hiddify/domain/profiles/profiles.dart'; +import 'package:hiddify/domain/rules/geo_asset.dart'; @DataClassName('ProfileEntry') class ProfileEntries extends Table { @@ -22,3 +23,22 @@ class ProfileEntries extends Table { @override Set get primaryKey => {id}; } + +@DataClassName('GeoAssetEntry') +class GeoAssetEntries extends Table { + TextColumn get id => text()(); + TextColumn get type => textEnum()(); + BoolColumn get active => boolean()(); + TextColumn get name => text().withLength(min: 1)(); + TextColumn get providerName => text().withLength(min: 1)(); + TextColumn get version => text().nullable()(); + DateTimeColumn get lastCheck => dateTime().nullable()(); + + @override + Set get primaryKey => {id}; + + @override + List> get uniqueKeys => [ + {name, providerName}, + ]; +} diff --git a/lib/data/repository/config_options_store.dart b/lib/data/repository/config_options_store.dart index 55c7455c..1f181738 100644 --- a/lib/data/repository/config_options_store.dart +++ b/lib/data/repository/config_options_store.dart @@ -147,19 +147,8 @@ ConfigOptions configPreferences(ConfigPreferencesRef ref) { urlTestInterval: ref.watch(urlTestIntervalStore), enableClashApi: ref.watch(enableClashApiStore), clashApiPort: ref.watch(clashApiPortStore), - // enableTun: ref.watch(enableTunStore), - // setSystemProxy: ref.watch(setSystemProxyStore), bypassLan: ref.watch(bypassLanStore), enableFakeDns: ref.watch(enableFakeDnsStore), rules: ref.watch(rulesProvider), ); } - -@riverpod -ConfigOptions configOptions(ConfigOptionsRef ref) { - final serviceMode = ref.watch(serviceModeStoreProvider); - return ref.watch(configPreferencesProvider).copyWith( - enableTun: serviceMode == ServiceMode.tun, - setSystemProxy: serviceMode == ServiceMode.systemProxy, - ); -} diff --git a/lib/data/repository/core_facade_impl.dart b/lib/data/repository/core_facade_impl.dart index d6d88279..16220f5f 100644 --- a/lib/data/repository/core_facade_impl.dart +++ b/lib/data/repository/core_facade_impl.dart @@ -29,7 +29,7 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { final PlatformServices platformServices; final ClashApi clash; final bool debug; - final ConfigOptions Function() configOptions; + final Future Function() configOptions; bool _initialized = false; @@ -95,9 +95,9 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { String fileName, ) { return exceptionHandler( - () { + () async { final configPath = filesEditor.configPath(fileName); - final options = configOptions(); + final options = await configOptions(); return setup() .andThen(() => changeConfigOptions(options)) .andThen( @@ -119,7 +119,7 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { return exceptionHandler( () async { final configPath = filesEditor.configPath(fileName); - final options = configOptions(); + final options = await configOptions(); loggy.info( "config options: ${options.format()}\nMemory Limit: ${!disableMemoryLimit}", ); @@ -159,9 +159,9 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { bool disableMemoryLimit, ) { return exceptionHandler( - () { + () async { final configPath = filesEditor.configPath(fileName); - return changeConfigOptions(configOptions()) + return changeConfigOptions(await configOptions()) .andThen( () => singbox .restart(configPath, disableMemoryLimit) diff --git a/lib/data/repository/geo_assets_repository.dart b/lib/data/repository/geo_assets_repository.dart new file mode 100644 index 00000000..c0488aa3 --- /dev/null +++ b/lib/data/repository/geo_assets_repository.dart @@ -0,0 +1,140 @@ +import 'dart:io'; + +import 'package:dartx/dartx_io.dart'; +import 'package:dio/dio.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:hiddify/data/local/dao/dao.dart'; +import 'package:hiddify/data/repository/exception_handlers.dart'; +import 'package:hiddify/domain/rules/geo_asset.dart'; +import 'package:hiddify/domain/rules/geo_asset_failure.dart'; +import 'package:hiddify/domain/rules/geo_assets_repository.dart'; +import 'package:hiddify/services/files_editor_service.dart'; +import 'package:hiddify/utils/custom_loggers.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:watcher/watcher.dart'; + +class GeoAssetsRepositoryImpl + with ExceptionHandler, InfraLogger + implements GeoAssetsRepository { + GeoAssetsRepositoryImpl({ + required this.geoAssetsDao, + required this.dio, + required this.filesEditor, + }); + + final GeoAssetsDao geoAssetsDao; + final Dio dio; + final FilesEditorService filesEditor; + + @override + TaskEither + getActivePair() { + return exceptionHandler( + () async { + final geoip = await geoAssetsDao.getActive(GeoAssetType.geoip); + final geosite = await geoAssetsDao.getActive(GeoAssetType.geosite); + if (geoip == null || geosite == null) { + return left(const GeoAssetFailure.activeAssetNotFound()); + } + return right((geoip: geoip, geosite: geosite)); + }, + GeoAssetFailure.unexpected, + ); + } + + @override + Stream>> watchAll() { + final persistedStream = geoAssetsDao.watchAll(); + final filesStream = _watchGeoFiles(); + + return Rx.combineLatest2( + persistedStream, + filesStream, + (assets, files) => assets.map( + (e) { + final path = filesEditor.geoAssetPath(e.providerName, e.fileName); + final file = files.firstOrNullWhere((e) => e.path == path); + final stat = file?.statSync(); + return (e, stat?.size); + }, + ).toList(), + ).handleExceptions(GeoAssetUnexpectedFailure.new); + } + + Iterable _geoFiles = []; + Stream> _watchGeoFiles() async* { + yield await _readGeoFiles(); + yield* Watcher( + filesEditor.geoAssetsDir.path, + pollingDelay: const Duration(seconds: 1), + ).events.asyncMap((event) async { + if (event.type == ChangeType.MODIFY) { + await _readGeoFiles(); + } + return _geoFiles; + }); + } + + Future> _readGeoFiles() async { + return _geoFiles = Directory(filesEditor.geoAssetsDir.path) + .listSync() + .whereType() + .where((e) => e.extension == '.db'); + } + + @override + TaskEither update(GeoAsset geoAsset) { + return exceptionHandler( + () async { + loggy.debug( + "checking latest release of [${geoAsset.name}] on [${geoAsset.repositoryUrl}]", + ); + final response = await dio.get(geoAsset.repositoryUrl); + if (response.statusCode != 200 || response.data == null) { + return left( + GeoAssetFailure.unexpected("invalid response", StackTrace.current), + ); + } + + final path = + filesEditor.geoAssetPath(geoAsset.providerName, geoAsset.name); + final tagName = response.data!['tag_name'] as String; + loggy.debug("latest release of [${geoAsset.name}]: [$tagName]"); + if (tagName == geoAsset.version && await File(path).exists()) { + await geoAssetsDao.edit(geoAsset.copyWith(lastCheck: DateTime.now())); + return left(const GeoAssetFailure.noUpdateAvailable()); + } + + final assets = (response.data!['assets'] as List) + .whereType>(); + final asset = + assets.firstOrNullWhere((e) => e["name"] == geoAsset.name); + if (asset == null) { + return left( + GeoAssetFailure.unexpected( + "couldn't find [${geoAsset.name}] on [${geoAsset.repositoryUrl}]", + StackTrace.current, + ), + ); + } + + final downloadUrl = asset["browser_download_url"] as String; + loggy.debug("[${geoAsset.name}] download url: [$downloadUrl]"); + final tempPath = "$path.tmp"; + await File(path).parent.create(recursive: true); + await dio.download(downloadUrl, tempPath); + await File(tempPath).rename(path); + + await geoAssetsDao.edit( + geoAsset.copyWith( + version: tagName, + lastCheck: DateTime.now(), + ), + ); + + return right(unit); + }, + GeoAssetFailure.unexpected, + ); + } +} diff --git a/lib/domain/rules/geo_asset.dart b/lib/domain/rules/geo_asset.dart new file mode 100644 index 00000000..2420d913 --- /dev/null +++ b/lib/domain/rules/geo_asset.dart @@ -0,0 +1,51 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'geo_asset.freezed.dart'; +part 'geo_asset.g.dart'; + +enum GeoAssetType { geoip, geosite } + +typedef GeoAssetWithFileSize = (GeoAsset geoAsset, int? size); + +@freezed +class GeoAsset with _$GeoAsset { + const GeoAsset._(); + + const factory GeoAsset({ + required String id, + required String name, + required GeoAssetType type, + required bool active, + required String providerName, + String? version, + DateTime? lastCheck, + }) = _GeoAsset; + + factory GeoAsset.fromJson(Map json) => + _$GeoAssetFromJson(json); + + String get fileName => name; + + String get repositoryUrl => + "https://api.github.com/repos/$providerName/releases/latest"; +} + +/// default geoip asset bundled with the app +const defaultGeoip = GeoAsset( + id: "sing-box-geoip", + name: "geoip.db", + type: GeoAssetType.geoip, + active: true, + providerName: "SagerNet/sing-geoip", +); + +/// default geosite asset bundled with the app +const defaultGeosite = GeoAsset( + id: "sing-box-geosite", + name: "geosite.db", + type: GeoAssetType.geosite, + active: true, + providerName: "SagerNet/sing-geosite", +); + +const defaultGeoAssets = [defaultGeoip, defaultGeosite]; diff --git a/lib/domain/rules/geo_asset_failure.dart b/lib/domain/rules/geo_asset_failure.dart new file mode 100644 index 00000000..7beb8ef2 --- /dev/null +++ b/lib/domain/rules/geo_asset_failure.dart @@ -0,0 +1,39 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hiddify/core/prefs/prefs.dart'; +import 'package:hiddify/domain/failures.dart'; + +part 'geo_asset_failure.freezed.dart'; + +@freezed +sealed class GeoAssetFailure with _$GeoAssetFailure, Failure { + const GeoAssetFailure._(); + + const factory GeoAssetFailure.unexpected([ + Object? error, + StackTrace? stackTrace, + ]) = GeoAssetUnexpectedFailure; + + @With() + const factory GeoAssetFailure.noUpdateAvailable() = GeoAssetNoUpdateAvailable; + + const factory GeoAssetFailure.activeAssetNotFound() = + GeoAssetActiveAssetNotFound; + + @override + ({String type, String? message}) present(TranslationsEn t) { + return switch (this) { + GeoAssetUnexpectedFailure() => ( + type: t.failure.geoAssets.unexpected, + message: null, + ), + GeoAssetNoUpdateAvailable() => ( + type: t.failure.geoAssets.notUpdate, + message: null + ), + GeoAssetActiveAssetNotFound() => ( + type: t.failure.geoAssets.activeNotFound, + message: null, + ), + }; + } +} diff --git a/lib/domain/rules/geo_assets_repository.dart b/lib/domain/rules/geo_assets_repository.dart new file mode 100644 index 00000000..1cd6f510 --- /dev/null +++ b/lib/domain/rules/geo_assets_repository.dart @@ -0,0 +1,12 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:hiddify/domain/rules/geo_asset.dart'; +import 'package:hiddify/domain/rules/geo_asset_failure.dart'; + +abstract interface class GeoAssetsRepository { + TaskEither + getActivePair(); + + Stream>> watchAll(); + + TaskEither update(GeoAsset geoAsset); +} diff --git a/lib/domain/singbox/config_options.dart b/lib/domain/singbox/config_options.dart index 6301da98..78b614cc 100644 --- a/lib/domain/singbox/config_options.dart +++ b/lib/domain/singbox/config_options.dart @@ -38,6 +38,8 @@ class ConfigOptions with _$ConfigOptions { @Default(false) bool bypassLan, @Default(false) bool enableFakeDns, @Default(true) bool independentDnsCache, + @Default("geoip.db") String geoipPath, + @Default("geosite.db") String geositePath, List? rules, }) = _ConfigOptions; diff --git a/lib/features/settings/geo_assets/geo_asset_tile.dart b/lib/features/settings/geo_assets/geo_asset_tile.dart new file mode 100644 index 00000000..b05e8dc7 --- /dev/null +++ b/lib/features/settings/geo_assets/geo_asset_tile.dart @@ -0,0 +1,116 @@ +import 'package:dartx/dartx.dart'; +import 'package:flutter/material.dart'; +import 'package:hiddify/core/core_providers.dart'; +import 'package:hiddify/domain/failures.dart'; +import 'package:hiddify/domain/rules/geo_asset.dart'; +import 'package:hiddify/domain/rules/geo_asset_failure.dart'; +import 'package:hiddify/features/settings/geo_assets/geo_assets_notifier.dart'; +import 'package:hiddify/utils/alerts.dart'; +import 'package:hiddify/utils/async_mutation.dart'; +import 'package:hiddify/utils/date_time_formatter.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:humanizer/humanizer.dart'; + +class GeoAssetTile extends HookConsumerWidget { + GeoAssetTile(GeoAssetWithFileSize geoAssetWithFileSize, {super.key}) + : geoAsset = geoAssetWithFileSize.$1, + size = geoAssetWithFileSize.$2; + + final GeoAsset geoAsset; + final int? size; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + final fileMissing = size == null; + + final updateMutation = useMutation( + initialOnFailure: (err) { + if (err case GeoAssetNoUpdateAvailable()) { + CustomToast(t.failure.geoAssets.notUpdate).show(context); + } else { + CustomAlertDialog.fromErr( + t.presentError(err, action: t.settings.geoAssets.failureMsg), + ).show(context); + } + }, + initialOnSuccess: () => + CustomToast.success(t.settings.geoAssets.successMsg).show(context), + ); + + return ListTile( + title: Text.rich( + TextSpan( + children: [ + TextSpan(text: geoAsset.name), + if (geoAsset.providerName.isNotBlank) + TextSpan(text: " (${geoAsset.providerName})"), + ], + ), + ), + isThreeLine: true, + subtitle: updateMutation.state.isInProgress + ? const LinearProgressIndicator() + : Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (geoAsset.version.isNotNullOrBlank) + Padding( + padding: const EdgeInsetsDirectional.only(end: 8), + child: Text( + t.settings.geoAssets.version(version: geoAsset.version!), + overflow: TextOverflow.ellipsis, + ), + ) + else + const SizedBox(), + Flexible( + child: Text.rich( + TextSpan( + children: [ + if (fileMissing) + TextSpan( + text: t.settings.geoAssets.fileMissing, + style: TextStyle( + color: Theme.of(context).colorScheme.error, + ), + ) + else + TextSpan(text: size?.bytes().toString()), + if (geoAsset.lastCheck != null) ...[ + const TextSpan(text: " • "), + TextSpan(text: geoAsset.lastCheck!.format()), + ], + ], + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + selected: geoAsset.active, + trailing: PopupMenuButton( + itemBuilder: (context) { + return [ + PopupMenuItem( + enabled: !updateMutation.state.isInProgress, + onTap: () { + if (updateMutation.state.isInProgress) { + return; + } + updateMutation.setFuture( + ref + .read(geoAssetsNotifierProvider.notifier) + .updateGeoAsset(geoAsset), + ); + }, + child: fileMissing + ? Text(t.settings.geoAssets.download) + : Text(t.settings.geoAssets.update), + ), + ]; + }, + ), + ); + } +} diff --git a/lib/features/settings/geo_assets/geo_assets_notifier.dart b/lib/features/settings/geo_assets/geo_assets_notifier.dart new file mode 100644 index 00000000..d7669962 --- /dev/null +++ b/lib/features/settings/geo_assets/geo_assets_notifier.dart @@ -0,0 +1,28 @@ +import 'package:hiddify/data/data_providers.dart'; +import 'package:hiddify/domain/rules/geo_asset.dart'; +import 'package:hiddify/utils/custom_loggers.dart'; +import 'package:hiddify/utils/riverpod_utils.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'geo_assets_notifier.g.dart'; + +@riverpod +class GeoAssetsNotifier extends _$GeoAssetsNotifier with AppLogger { + @override + Stream> build() { + ref.disposeDelay(const Duration(seconds: 5)); + return ref + .watch(geoAssetsRepositoryProvider) + .watchAll() + .map((event) => event.getOrElse((l) => throw l)); + } + + Future updateGeoAsset(GeoAsset geoAsset) async { + await ref.read(geoAssetsRepositoryProvider).update(geoAsset).getOrElse( + (f) { + loggy.warning("error updating profile", f); + throw f; + }, + ).run(); + } +} diff --git a/lib/features/settings/geo_assets/geo_assets_page.dart b/lib/features/settings/geo_assets/geo_assets_page.dart new file mode 100644 index 00000000..b4ddfdc5 --- /dev/null +++ b/lib/features/settings/geo_assets/geo_assets_page.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:hiddify/core/core_providers.dart'; +import 'package:hiddify/features/settings/geo_assets/geo_asset_tile.dart'; +import 'package:hiddify/features/settings/geo_assets/geo_assets_notifier.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +class GeoAssetsPage extends HookConsumerWidget { + const GeoAssetsPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + final state = ref.watch(geoAssetsNotifierProvider); + + return Scaffold( + body: CustomScrollView( + slivers: [ + SliverAppBar( + title: Text(t.settings.geoAssets.pageTitle), + ), + switch (state) { + AsyncData(value: final geoAssets) => SliverList.builder( + itemBuilder: (context, index) { + final geoAsset = geoAssets[index]; + return GeoAssetTile(geoAsset); + }, + itemCount: geoAssets.length, + ), + _ => const SliverToBoxAdapter(), + }, + ], + ), + ); + } +} diff --git a/lib/features/settings/widgets/advanced_setting_tiles.dart b/lib/features/settings/widgets/advanced_setting_tiles.dart index ef916622..23f7a2d3 100644 --- a/lib/features/settings/widgets/advanced_setting_tiles.dart +++ b/lib/features/settings/widgets/advanced_setting_tiles.dart @@ -30,6 +30,13 @@ class AdvancedSettingTiles extends HookConsumerWidget { await const ConfigOptionsRoute().push(context); }, ), + ListTile( + title: Text(t.settings.geoAssets.pageTitle), + leading: const Icon(Icons.folder), + onTap: () async { + await const GeoAssetsRoute().push(context); + }, + ), if (Platform.isAndroid) ...[ ListTile( title: Text(t.settings.network.perAppProxyPageTitle), diff --git a/lib/services/files_editor_service.dart b/lib/services/files_editor_service.dart index f9a9d298..19a8bb21 100644 --- a/lib/services/files_editor_service.dart +++ b/lib/services/files_editor_service.dart @@ -1,7 +1,9 @@ import 'dart:io'; +import 'package:dartx/dartx.dart'; import 'package:flutter/services.dart'; import 'package:hiddify/domain/constants.dart'; +import 'package:hiddify/domain/rules/geo_asset.dart'; import 'package:hiddify/gen/assets.gen.dart'; import 'package:hiddify/services/platform_services.dart'; import 'package:hiddify/utils/utils.dart'; @@ -26,6 +28,9 @@ class FilesEditorService with InfraLogger { Directory(p.join(workingDir.path, Constants.configsFolderName)); Directory get logsDir => dirs.workingDir; + Directory get geoAssetsDir => + Directory(p.join(workingDir.path, "geo-assets")); + File get appLogsFile => File(p.join(logsDir.path, "app.log")); File get coreLogsFile => File(p.join(logsDir.path, "box.log")); @@ -48,6 +53,9 @@ class FilesEditorService with InfraLogger { if (!await configsDir.exists()) { await configsDir.create(recursive: true); } + if (!await geoAssetsDir.exists()) { + await geoAssetsDir.create(recursive: true); + } if (await appLogsFile.exists()) { await appLogsFile.writeAsString(""); @@ -77,6 +85,20 @@ class FilesEditorService with InfraLogger { return p.join(configsDir.path, "$fileName.json"); } + String geoAssetPath(String providerName, String fileName) { + final prefix = providerName.replaceAll("/", "-").toLowerCase(); + return p.join( + geoAssetsDir.path, + "$prefix${prefix.isBlank ? "" : "-"}$fileName", + ); + } + + /// geoasset's path relative to working directory + String geoAssetRelativePath(String providerName, String fileName) { + final fullPath = geoAssetPath(providerName, fileName); + return p.relative(fullPath, from: workingDir.path); + } + String tempConfigPath(String fileName) => configPath("temp_$fileName"); Future deleteConfig(String fileName) { @@ -85,16 +107,18 @@ class FilesEditorService with InfraLogger { Future _populateGeoAssets() async { loggy.debug('populating geo assets'); - final geoipPath = p.join(workingDir.path, Constants.geoipFileName); + final geoipPath = + geoAssetPath(defaultGeoip.providerName, defaultGeoip.fileName); if (!await File(geoipPath).exists()) { - final defaultGeoip = await rootBundle.load(Assets.core.geoip); - await File(geoipPath).writeAsBytes(defaultGeoip.buffer.asInt8List()); + final bundledGeoip = await rootBundle.load(Assets.core.geoip); + await File(geoipPath).writeAsBytes(bundledGeoip.buffer.asInt8List()); } - final geositePath = p.join(workingDir.path, Constants.geositeFileName); + final geositePath = + geoAssetPath(defaultGeosite.providerName, defaultGeosite.fileName); if (!await File(geositePath).exists()) { - final defaultGeosite = await rootBundle.load(Assets.core.geosite); - await File(geositePath).writeAsBytes(defaultGeosite.buffer.asInt8List()); + final bundledGeosite = await rootBundle.load(Assets.core.geosite); + await File(geositePath).writeAsBytes(bundledGeosite.buffer.asInt8List()); } } } diff --git a/libcore b/libcore index 89ecc6bf..2c2504f9 160000 --- a/libcore +++ b/libcore @@ -1 +1 @@ -Subproject commit 89ecc6bf1238e12bbad26182f7a06f9aaf492b9f +Subproject commit 2c2504f97145453f4fc7866982172e95b533ea73 diff --git a/test/data/local/generated_migrations/schema.dart b/test/data/local/generated_migrations/schema.dart index c8c6ff59..1c9347e9 100644 --- a/test/data/local/generated_migrations/schema.dart +++ b/test/data/local/generated_migrations/schema.dart @@ -5,6 +5,7 @@ import 'package:drift/drift.dart'; import 'package:drift/internal/migrations.dart'; import 'schema_v1.dart' as v1; import 'schema_v2.dart' as v2; +import 'schema_v3.dart' as v3; class GeneratedHelper implements SchemaInstantiationHelper { @override @@ -14,8 +15,10 @@ class GeneratedHelper implements SchemaInstantiationHelper { return v1.DatabaseAtV1(db); case 2: return v2.DatabaseAtV2(db); + case 3: + return v3.DatabaseAtV3(db); default: - throw MissingSchemaException(version, const {1, 2}); + throw MissingSchemaException(version, const {1, 2, 3}); } } } diff --git a/test/data/local/generated_migrations/schema_v3.dart b/test/data/local/generated_migrations/schema_v3.dart new file mode 100644 index 00000000..a07c41dd --- /dev/null +++ b/test/data/local/generated_migrations/schema_v3.dart @@ -0,0 +1,168 @@ +// GENERATED CODE, DO NOT EDIT BY HAND. +// ignore_for_file: type=lint +//@dart=2.12 +import 'package:drift/drift.dart'; + +class ProfileEntries extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + ProfileEntries(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn type = GeneratedColumn( + 'type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn active = GeneratedColumn( + 'active', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("active" IN (0, 1))')); + late final GeneratedColumn name = + GeneratedColumn('name', aliasedName, false, + additionalChecks: GeneratedColumn.checkTextLength( + minTextLength: 1, + ), + type: DriftSqlType.string, + requiredDuringInsert: true); + late final GeneratedColumn url = GeneratedColumn( + 'url', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn lastUpdate = GeneratedColumn( + 'last_update', aliasedName, false, + type: DriftSqlType.dateTime, requiredDuringInsert: true); + late final GeneratedColumn updateInterval = GeneratedColumn( + 'update_interval', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn upload = GeneratedColumn( + 'upload', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn download = GeneratedColumn( + 'download', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn total = GeneratedColumn( + 'total', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn expire = GeneratedColumn( + 'expire', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn webPageUrl = GeneratedColumn( + 'web_page_url', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn supportUrl = GeneratedColumn( + 'support_url', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + @override + List get $columns => [ + id, + type, + active, + name, + url, + lastUpdate, + updateInterval, + upload, + download, + total, + expire, + webPageUrl, + supportUrl + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'profile_entries'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + ProfileEntries createAlias(String alias) { + return ProfileEntries(attachedDatabase, alias); + } +} + +class GeoAssetEntries extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + GeoAssetEntries(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn type = GeneratedColumn( + 'type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn active = GeneratedColumn( + 'active', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("active" IN (0, 1))')); + late final GeneratedColumn name = + GeneratedColumn('name', aliasedName, false, + additionalChecks: GeneratedColumn.checkTextLength( + minTextLength: 1, + ), + type: DriftSqlType.string, + requiredDuringInsert: true); + late final GeneratedColumn providerName = + GeneratedColumn('provider_name', aliasedName, false, + additionalChecks: GeneratedColumn.checkTextLength( + minTextLength: 1, + ), + type: DriftSqlType.string, + requiredDuringInsert: true); + late final GeneratedColumn version = GeneratedColumn( + 'version', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn lastCheck = GeneratedColumn( + 'last_check', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + @override + List get $columns => + [id, type, active, name, providerName, version, lastCheck]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'geo_asset_entries'; + @override + Set get $primaryKey => {id}; + @override + List> get uniqueKeys => [ + {name, providerName}, + ]; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + GeoAssetEntries createAlias(String alias) { + return GeoAssetEntries(attachedDatabase, alias); + } +} + +class DatabaseAtV3 extends GeneratedDatabase { + DatabaseAtV3(QueryExecutor e) : super(e); + late final ProfileEntries profileEntries = ProfileEntries(this); + late final GeoAssetEntries geoAssetEntries = GeoAssetEntries(this); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => + [profileEntries, geoAssetEntries]; + @override + int get schemaVersion => 3; + @override + DriftDatabaseOptions get options => + const DriftDatabaseOptions(storeDateTimeAsText: true); +} diff --git a/test/data/local/migrations_test.dart b/test/data/local/migrations_test.dart index 9a9a15b4..e184117a 100644 --- a/test/data/local/migrations_test.dart +++ b/test/data/local/migrations_test.dart @@ -6,7 +6,6 @@ import 'generated_migrations/schema.dart'; void main() { late SchemaVerifier verifier; - setUpAll(() { verifier = SchemaVerifier(GeneratedHelper()); }); @@ -16,5 +15,28 @@ void main() { final db = AppDatabase(connection: connection); await verifier.migrateAndValidate(db, 2); + await db.close(); + }); + + test('upgrade from v2 to v3', () async { + final connection = await verifier.startAt(2); + final db = AppDatabase(connection: connection); + + await verifier.migrateAndValidate(db, 3); + + final prePopulated = await db.select(db.geoAssetEntries).get(); + await db.close(); + expect(prePopulated.length, equals(2)); + }); + + test('upgrade from v1 to v3 with pre-population', () async { + final connection = await verifier.startAt(1); + final db = AppDatabase(connection: connection); + + await verifier.migrateAndValidate(db, 3); + + final prePopulated = await db.select(db.geoAssetEntries).get(); + await db.close(); + expect(prePopulated.length, equals(2)); }); } From 4a6b112087171c92da99256192516c72c692e695 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Fri, 17 Nov 2023 22:03:51 +0330 Subject: [PATCH 42/91] Fix build --- .github/sync_translate.sh | 1 + assets/translations/strings_ru.i18n.json | 2 +- assets/translations/strings_tr.i18n.json | 16 +++++++++++++++- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/.github/sync_translate.sh b/.github/sync_translate.sh index d67c2b09..d80daa1b 100644 --- a/.github/sync_translate.sh +++ b/.github/sync_translate.sh @@ -14,6 +14,7 @@ python3 auto_translator.py en fa python3 auto_translator.py en zh # python3 auto_translator.py en pt python3 auto_translator.py en ru +python3 auto_translator.py en tr diff --git a/assets/translations/strings_ru.i18n.json b/assets/translations/strings_ru.i18n.json index b30959d6..d278503c 100644 --- a/assets/translations/strings_ru.i18n.json +++ b/assets/translations/strings_ru.i18n.json @@ -304,4 +304,4 @@ "short_description": "Автовыбор, SSH, VLESS, Vmess, Trojan, Reality, Sing-Box, Clash, Xray, Shadowsocks", "full_description": "Основная цель HiddifyNext — предоставить безопасный, удобный и эффективный клиент туннелирования. Он позволяет направлять весь трафик или трафик выбранного приложения на выбранный вами удалённый сервер, используя разрешение VPN–сервиса.\n\nПримечание: мы не предоставляем серверы, пользователи должны обеспечивать конфиденциальность своих действий в Интернете, используя собственный сервер или доверенные серверы.\n \nПоддерживаемые серверы:\n— Обычная ссылка на подписку V2ray/Xray\n— Ссылка на подписку Clash\n— Ссылка на подписку на Sing–Box\n\nВ чём уникальные особенности?\n — Удобство\n — Оптимизация и скорость\n — Автоматический выбор минимальной задержки\n — Отображение информации об использовании\n — Простой импорт ссылок одним щелчком мыши\n — Бесплатно и без рекламы\n — Простое переключение ссылок\n — …и много больше\n\nПоддержка:\n• Все протоколы, поддерживаемые Sing-Box\n• VLESS + xtls reality, vision\n• VMESS\n• Trojan\n• ShoadowSocks\n• Reality\n• V2ray\n• Hystria2\n• TUIC\n• SSH\n• ShadowTLS\n\n\nИсходный код доступен по адресу https://github.com/hiddify/Hiddify-Next.\nЯдро приложения основано на открытом исходном коде Sing–Box.\n\nОписание разрешений:\n— СЛУЖБА VPN: поскольку целью данного приложения является предоставление безопасного, удобного и эффективного клиента туннелирования, это разрешение необходимо, чтобы иметь возможность направлять трафик через туннель на удалённый сервер.\n— ЗАПРОС ВСЕХ ПАКЕТОВ: это разрешение позволяет включать или исключать определённые приложения для туннелирования.\n— ИНФОРМИРОВАНИЕ О ЗАВЕРШЕНИИ ЗАГРУЗКИ: это разрешение можно включить или отключить в настройках приложения, чтобы (де)активировать запуск приложения при загрузке устройства.\n— ПОСТОЯННОЕ УВЕДОМЛЕНИЕ: это разрешение необходимо, поскольку используется приоритетная служба для обеспечения непрерывной работы службы VPN.\n— Приложение не содержит рекламы. Сбор аналитики и данных о сбоях происходят только с явного согласия пользователя при первом использовании приложения." } -} +} \ No newline at end of file diff --git a/assets/translations/strings_tr.i18n.json b/assets/translations/strings_tr.i18n.json index ba5a12df..8b9199d2 100644 --- a/assets/translations/strings_tr.i18n.json +++ b/assets/translations/strings_tr.i18n.json @@ -217,6 +217,15 @@ "enableFakeDns": "Sahte DNS'yi Etkinleştir", "bypassLan": "Lan'ı Atla", "strictRoute": "Kesin Rota" + }, + "geoAssets": { + "pageTitle": "Varlıkları Yönlendirme", + "version": "Sürüm ${version}", + "fileMissing": "Eksik dosya", + "update": "Güncelleme", + "download": "İndirmek", + "failureMsg": "Öğe güncellenemedi", + "successMsg": "Öğe başarıyla güncellendi" } }, "about": { @@ -283,6 +292,11 @@ "badResponse": "Kötü yanıt", "connectionError": "Bağlantı hatası", "badCertificate": "Kötü sertifika" + }, + "geoAssets": { + "unexpected": "Beklenmeyen hata", + "notUpdate": "Güncelleme mevcut değil", + "activeNotFound": "Etkin Coğrafi Varlık Bulunamadı" } }, "play": { @@ -290,4 +304,4 @@ "short_description": "Otomatik, SSH, VLESS, Vmess, Trojan, Reality, Sing-Box, Clash, Xray, Shadowsocks", "full_description": "HiddifyNext'in temel hedefi güvenli, kullanıcı dostu ve verimli bir tünel istemcisi sağlamaktır. VPN Hizmeti iznini kullanarak tüm trafiği veya seçilen uygulama trafiğini seçtiğiniz uzak bir sunucuya yönlendirmenizi sağlar. Not: Herhangi bir sunucu sağlamıyoruz; kullanıcıların kendi barındırılan sunucularını veya güvenilir sunucularını kullanarak çevrimiçi etkinliklerinin gizli kalmasını sağlamaları gerekir. Sunucuları aşağıdakilerle destekliyoruz: - Normal V2ray/Xray Abonelik Bağlantısı - Clash Abonelik Bağlantısı - Sing-Box Abonelik Bağlantısı Benzersiz özelliklerimiz nelerdir? - Kullanıcı Dostu - Optimize Edilmiş ve Hızlı - En Düşük Ping'i otomatik olarak seçin - Kullanıcı kullanım bilgilerini gösterin - Derin bağlantı kullanarak tek tıklamayla alt bağlantıyı kolayca içe aktarın - Ücretsiz ve ADS Yok - Kullanıcı alt bağlantılarını kolayca değiştirin - giderek daha fazla Destek: - Sing-Box tarafından desteklenen tüm Protokoller - VLESS + xtls gerçeklik, vizyon - VMESS - Trojan - ShoadowSocks - Reality - V2ray - Hystria2 - TUIC - SSH - ShadowTLS Kaynak kodu https://github.com/hiddify/Hiddify-Next adresinde mevcuttur. Uygulama çekirdeği açık tabanlıdır. kaynak şarkı kutusu. İzin Açıklaması: - VPN Hizmeti: Bu uygulamanın amacı güvenli, kullanıcı dostu ve verimli bir tünel istemcisi sağlamak olduğundan, trafiği tünel aracılığıyla uzak sunucuya yönlendirebilmek için bu izne ihtiyacımız var. - TÜM PAKETLERİ SORGULAYIN: Bu izin, kullanıcıların tünelleme için belirli uygulamaları dahil etmesine veya hariç tutmasına izin vermek için kullanılır. - ALMA ÖNYÜKLEME TAMAMLANDI: Bu izin, cihaz önyüklemesi sırasında bu uygulamayı etkinleştirmek için uygulama ayarlarından etkinleştirilebilir veya devre dışı bırakılabilir. - BİLDİRİMLER SONRASI: VPN hizmetinin sürekli çalışmasını sağlamak için bir ön plan hizmeti kullandığımız için bu izin önemlidir. - Bu uygulama reklam içermez. Analitik ve kilitlenme verileri yalnızca uygulamanın ilk kullanımında kullanıcının açık rızası ile gerçekleşir." } -} +} \ No newline at end of file From 436fc8133cf12edf35f51ed3c2d313f6a357643e Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Fri, 17 Nov 2023 23:22:12 +0330 Subject: [PATCH 43/91] Add error handling for geo assets --- assets/translations/strings_en.i18n.json | 2 + assets/translations/strings_fa.i18n.json | 4 +- assets/translations/strings_ru.i18n.json | 4 +- assets/translations/strings_tr.i18n.json | 4 +- assets/translations/strings_zh.i18n.json | 4 +- lib/data/local/dao/geo_assets_dao.dart | 6 ++ lib/data/repository/core_facade_impl.dart | 85 +++++++++++-------- lib/data/repository/exception_handlers.dart | 16 ++++ .../repository/geo_assets_repository.dart | 15 +++- lib/domain/core_service_failure.dart | 8 ++ lib/domain/rules/geo_assets_repository.dart | 2 + lib/services/files_editor_service.dart | 4 + libcore | 2 +- 13 files changed, 113 insertions(+), 43 deletions(-) diff --git a/assets/translations/strings_en.i18n.json b/assets/translations/strings_en.i18n.json index 98696753..45b59b37 100644 --- a/assets/translations/strings_en.i18n.json +++ b/assets/translations/strings_en.i18n.json @@ -269,6 +269,8 @@ "serviceNotRunning": "Service is not running", "missingPrivilege": "Missing Privilege", "missingPrivilegeMsg": "VPN mode requires administrator privileges. Either relaunch the app as administrator or change service mode.", + "missingGeoAssets": "Missing Geo Assets", + "missingGeoAssetsMsg": "Geo assets are missing. consider changing active asset or download selected one in the settings.", "invalidConfigOptions": "Invalid configuration options", "invalidConfig": "Invalid Configuration", "create": "Service creation error", diff --git a/assets/translations/strings_fa.i18n.json b/assets/translations/strings_fa.i18n.json index 426bc200..98e59148 100644 --- a/assets/translations/strings_fa.i18n.json +++ b/assets/translations/strings_fa.i18n.json @@ -272,7 +272,9 @@ "invalidConfigOptions": "تنظیمات کانفیگ نامعتبر", "invalidConfig": "کانفیگ غیر معتبر", "create": "در ایجاد سرویس خطایی رخ داده", - "start": "در راه‌اندازی سرویس خطایی رخ داده" + "start": "در راه‌اندازی سرویس خطایی رخ داده", + "missingGeoAssets": "دارایی های جغرافیایی از دست رفته", + "missingGeoAssetsMsg": "دارایی های جغرافیایی گم شده اند. تغییر دارایی فعال را در نظر بگیرید یا یکی را در تنظیمات دانلود کنید." }, "connectivity": { "unexpected": "خطای غیرمنتظره", diff --git a/assets/translations/strings_ru.i18n.json b/assets/translations/strings_ru.i18n.json index d278503c..30f211d3 100644 --- a/assets/translations/strings_ru.i18n.json +++ b/assets/translations/strings_ru.i18n.json @@ -272,7 +272,9 @@ "invalidConfigOptions": "Неправильные параметры конфигурации", "invalidConfig": "Неправильная конфигурация", "create": "Ошибка создания сервиса", - "start": "Ошибка запуска сервиса" + "start": "Ошибка запуска сервиса", + "missingGeoAssets": "Отсутствующие географические ресурсы", + "missingGeoAssetsMsg": "Георесурсы отсутствуют. рассмотрите возможность изменения активного актива или загрузите выбранный в настройках." }, "connectivity": { "unexpected": "Неожиданная ошибка", diff --git a/assets/translations/strings_tr.i18n.json b/assets/translations/strings_tr.i18n.json index 8b9199d2..ea1e802c 100644 --- a/assets/translations/strings_tr.i18n.json +++ b/assets/translations/strings_tr.i18n.json @@ -272,7 +272,9 @@ "invalidConfigOptions": "Geçersiz yapılandırma seçenekleri", "invalidConfig": "Geçersiz Yapılandırma", "create": "Servis oluşturma hatası", - "start": "Servis başlatma hatası" + "start": "Servis başlatma hatası", + "missingGeoAssets": "Eksik Coğrafi Varlıklar", + "missingGeoAssetsMsg": "Coğrafi öğeler eksik. Aktif varlığı değiştirmeyi veya ayarlarda seçileni indirmeyi düşünün." }, "connectivity": { "unexpected": "Beklenmedik Hata", diff --git a/assets/translations/strings_zh.i18n.json b/assets/translations/strings_zh.i18n.json index 30ba3d30..035810f7 100644 --- a/assets/translations/strings_zh.i18n.json +++ b/assets/translations/strings_zh.i18n.json @@ -272,7 +272,9 @@ "invalidConfigOptions": "配置选项无效", "invalidConfig": "无效配置", "create": "服务创建错误", - "start": "服务启动错误" + "start": "服务启动错误", + "missingGeoAssets": "缺少地理资产", + "missingGeoAssetsMsg": "地理资产缺失。考虑更改活动资产或下载设置中选定的资产。" }, "connectivity": { "unexpected": "意外失败", diff --git a/lib/data/local/dao/geo_assets_dao.dart b/lib/data/local/dao/geo_assets_dao.dart index 3b27a9f6..13a04436 100644 --- a/lib/data/local/dao/geo_assets_dao.dart +++ b/lib/data/local/dao/geo_assets_dao.dart @@ -28,6 +28,12 @@ class GeoAssetsDao extends DatabaseAccessor Future edit(GeoAsset patch) async { await transaction( () async { + if (patch.active) { + await (update(geoAssetEntries) + ..where((tbl) => tbl.active.equals(true)) + ..where((tbl) => tbl.type.equalsValue(patch.type))) + .write(const GeoAssetEntriesCompanion(active: Value(false))); + } await (update(geoAssetEntries)..where((tbl) => tbl.id.equals(patch.id))) .write(patch.toCompanion()); }, diff --git a/lib/data/repository/core_facade_impl.dart b/lib/data/repository/core_facade_impl.dart index 16220f5f..d254726c 100644 --- a/lib/data/repository/core_facade_impl.dart +++ b/lib/data/repository/core_facade_impl.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:io'; import 'package:fpdart/fpdart.dart'; import 'package:hiddify/data/api/clash_api.dart'; @@ -33,6 +34,21 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { bool _initialized = false; + TaskEither _getConfigOptions() { + return exceptionHandler( + () async { + final options = await configOptions(); + final geoip = filesEditor.resolveGeoAssetPath(options.geoipPath); + final geosite = filesEditor.resolveGeoAssetPath(options.geositePath); + if (!await File(geoip).exists() || !await File(geosite).exists()) { + return left(const CoreMissingGeoAssets()); + } + return right(options); + }, + CoreServiceFailure.unexpected, + ); + } + @override TaskEither setup() { if (_initialized) return TaskEither.of(unit); @@ -94,21 +110,17 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { TaskEither generateConfig( String fileName, ) { - return exceptionHandler( - () async { + return TaskEither.Do( + ($) async { final configPath = filesEditor.configPath(fileName); - final options = await configOptions(); - return setup() - .andThen(() => changeConfigOptions(options)) - .andThen( - () => singbox - .generateConfig(configPath) - .mapLeft(CoreServiceFailure.other), - ) - .run(); + final options = await $(_getConfigOptions()); + await $(setup()); + await $(changeConfigOptions(options)); + return await $( + singbox.generateConfig(configPath).mapLeft(CoreServiceFailure.other), + ); }, - CoreServiceFailure.unexpected, - ); + ).handleExceptions(CoreServiceFailure.unexpected); } @override @@ -116,33 +128,35 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { String fileName, bool disableMemoryLimit, ) { - return exceptionHandler( - () async { + return TaskEither.Do( + ($) async { final configPath = filesEditor.configPath(fileName); - final options = await configOptions(); + final options = await $(_getConfigOptions()); loggy.info( "config options: ${options.format()}\nMemory Limit: ${!disableMemoryLimit}", ); - if (options.enableTun) { - final hasPrivilege = await platformServices.hasPrivilege(); - if (!hasPrivilege) { - loggy.warning("missing privileges for tun mode"); - return left(const CoreMissingPrivilege()); - } - } - - return setup() - .andThen(() => changeConfigOptions(options)) - .andThen( - () => singbox - .start(configPath, disableMemoryLimit) - .mapLeft(CoreServiceFailure.start), - ) - .run(); + await $( + TaskEither(() async { + if (options.enableTun) { + final hasPrivilege = await platformServices.hasPrivilege(); + if (!hasPrivilege) { + loggy.warning("missing privileges for tun mode"); + return left(const CoreMissingPrivilege()); + } + } + return right(unit); + }), + ); + await $(setup()); + await $(changeConfigOptions(options)); + return await $( + singbox + .start(configPath, disableMemoryLimit) + .mapLeft(CoreServiceFailure.start), + ); }, - CoreServiceFailure.unexpected, - ); + ).handleExceptions(CoreServiceFailure.unexpected); } @override @@ -161,7 +175,8 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { return exceptionHandler( () async { final configPath = filesEditor.configPath(fileName); - return changeConfigOptions(await configOptions()) + return _getConfigOptions() + .flatMap((options) => changeConfigOptions(options)) .andThen( () => singbox .restart(configPath, disableMemoryLimit) diff --git a/lib/data/repository/exception_handlers.dart b/lib/data/repository/exception_handlers.dart index 3805b584..34845072 100644 --- a/lib/data/repository/exception_handlers.dart +++ b/lib/data/repository/exception_handlers.dart @@ -30,3 +30,19 @@ extension StreamExceptionHandler on Stream { ); } } + +extension TaskEitherExceptionHandler on TaskEither { + TaskEither handleExceptions( + F Function(Object error, StackTrace stackTrace) onError, + ) { + return TaskEither( + () async { + try { + return await run(); + } catch (error, stackTrace) { + return Left(onError(error, stackTrace)); + } + }, + ); + } +} diff --git a/lib/data/repository/geo_assets_repository.dart b/lib/data/repository/geo_assets_repository.dart index c0488aa3..104c8677 100644 --- a/lib/data/repository/geo_assets_repository.dart +++ b/lib/data/repository/geo_assets_repository.dart @@ -68,9 +68,7 @@ class GeoAssetsRepositoryImpl filesEditor.geoAssetsDir.path, pollingDelay: const Duration(seconds: 1), ).events.asyncMap((event) async { - if (event.type == ChangeType.MODIFY) { - await _readGeoFiles(); - } + await _readGeoFiles(); return _geoFiles; }); } @@ -137,4 +135,15 @@ class GeoAssetsRepositoryImpl GeoAssetFailure.unexpected, ); } + + @override + TaskEither markAsActive(GeoAsset geoAsset) { + return exceptionHandler( + () async { + await geoAssetsDao.edit(geoAsset.copyWith(active: true)); + return right(unit); + }, + GeoAssetFailure.unexpected, + ); + } } diff --git a/lib/domain/core_service_failure.dart b/lib/domain/core_service_failure.dart index 8e2041dd..87ab9830 100644 --- a/lib/domain/core_service_failure.dart +++ b/lib/domain/core_service_failure.dart @@ -21,6 +21,9 @@ sealed class CoreServiceFailure with _$CoreServiceFailure, Failure { @With() const factory CoreServiceFailure.missingPrivilege() = CoreMissingPrivilege; + @With() + const factory CoreServiceFailure.missingGeoAssets() = CoreMissingGeoAssets; + const factory CoreServiceFailure.invalidConfigOptions([ String? message, ]) = InvalidConfigOptions; @@ -46,6 +49,7 @@ sealed class CoreServiceFailure with _$CoreServiceFailure, Failure { UnexpectedCoreServiceFailure() => null, CoreServiceNotRunning(:final message) => message, CoreMissingPrivilege() => null, + CoreMissingGeoAssets() => null, InvalidConfigOptions(:final message) => message, InvalidConfig(:final message) => message, CoreServiceCreateFailure(:final message) => message, @@ -68,6 +72,10 @@ sealed class CoreServiceFailure with _$CoreServiceFailure, Failure { type: t.failure.singbox.missingPrivilege, message: t.failure.singbox.missingPrivilegeMsg, ), + CoreMissingGeoAssets() => ( + type: t.failure.singbox.missingGeoAssets, + message: t.failure.singbox.missingGeoAssetsMsg, + ), InvalidConfigOptions(:final message) => ( type: t.failure.singbox.invalidConfigOptions, message: message diff --git a/lib/domain/rules/geo_assets_repository.dart b/lib/domain/rules/geo_assets_repository.dart index 1cd6f510..b063129a 100644 --- a/lib/domain/rules/geo_assets_repository.dart +++ b/lib/domain/rules/geo_assets_repository.dart @@ -9,4 +9,6 @@ abstract interface class GeoAssetsRepository { Stream>> watchAll(); TaskEither update(GeoAsset geoAsset); + + TaskEither markAsActive(GeoAsset geoAsset); } diff --git a/lib/services/files_editor_service.dart b/lib/services/files_editor_service.dart index 19a8bb21..db048e81 100644 --- a/lib/services/files_editor_service.dart +++ b/lib/services/files_editor_service.dart @@ -99,6 +99,10 @@ class FilesEditorService with InfraLogger { return p.relative(fullPath, from: workingDir.path); } + String resolveGeoAssetPath(String path) { + return p.absolute(workingDir.path, path); + } + String tempConfigPath(String fileName) => configPath("temp_$fileName"); Future deleteConfig(String fileName) { diff --git a/libcore b/libcore index 2c2504f9..2f865d5c 160000 --- a/libcore +++ b/libcore @@ -1 +1 @@ -Subproject commit 2c2504f97145453f4fc7866982172e95b533ea73 +Subproject commit 2f865d5c8a11966ab11e777bc0b67a999fa07126 From a72828024937c842a1d2e46318c738226dd60a8b Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Fri, 17 Nov 2023 23:44:32 +0330 Subject: [PATCH 44/91] Add recommended geo assets --- assets/translations/strings_en.i18n.json | 3 ++- assets/translations/strings_fa.i18n.json | 3 ++- assets/translations/strings_ru.i18n.json | 3 ++- assets/translations/strings_tr.i18n.json | 3 ++- assets/translations/strings_zh.i18n.json | 3 ++- lib/data/local/dao/geo_assets_dao.dart | 4 ++++ .../repository/geo_assets_repository.dart | 19 +++++++++++++++ lib/domain/rules/geo_asset.dart | 18 +++++++++++++++ lib/domain/rules/geo_assets_repository.dart | 2 ++ .../settings/geo_assets/geo_asset_tile.dart | 5 ++++ .../geo_assets/geo_assets_notifier.dart | 23 ++++++++++++++++++- .../settings/geo_assets/geo_assets_page.dart | 16 +++++++++++++ 12 files changed, 96 insertions(+), 6 deletions(-) diff --git a/assets/translations/strings_en.i18n.json b/assets/translations/strings_en.i18n.json index 45b59b37..6bb29c05 100644 --- a/assets/translations/strings_en.i18n.json +++ b/assets/translations/strings_en.i18n.json @@ -225,7 +225,8 @@ "update": "Update", "download": "Download", "failureMsg": "Failed to update asset", - "successMsg": "Successfully updated asset" + "successMsg": "Successfully updated asset", + "addRecommended": "Add Recommended Assets" } }, "about": { diff --git a/assets/translations/strings_fa.i18n.json b/assets/translations/strings_fa.i18n.json index 98e59148..a774f9a0 100644 --- a/assets/translations/strings_fa.i18n.json +++ b/assets/translations/strings_fa.i18n.json @@ -225,7 +225,8 @@ "update": "به روز رسانی", "download": "دانلود", "failureMsg": "دارایی به روز نشد", - "successMsg": "دارایی با موفقیت به روز شد" + "successMsg": "دارایی با موفقیت به روز شد", + "addRecommended": "اضافه کردن دارایی های توصیه شده" } }, "about": { diff --git a/assets/translations/strings_ru.i18n.json b/assets/translations/strings_ru.i18n.json index 30f211d3..4d75e461 100644 --- a/assets/translations/strings_ru.i18n.json +++ b/assets/translations/strings_ru.i18n.json @@ -225,7 +225,8 @@ "update": "Обновлять", "download": "Скачать", "failureMsg": "Не удалось обновить объект.", - "successMsg": "Объект успешно обновлен." + "successMsg": "Объект успешно обновлен.", + "addRecommended": "Добавить рекомендуемые активы" } }, "about": { diff --git a/assets/translations/strings_tr.i18n.json b/assets/translations/strings_tr.i18n.json index ea1e802c..15309c26 100644 --- a/assets/translations/strings_tr.i18n.json +++ b/assets/translations/strings_tr.i18n.json @@ -225,7 +225,8 @@ "update": "Güncelleme", "download": "İndirmek", "failureMsg": "Öğe güncellenemedi", - "successMsg": "Öğe başarıyla güncellendi" + "successMsg": "Öğe başarıyla güncellendi", + "addRecommended": "Önerilen Varlıkları Ekle" } }, "about": { diff --git a/assets/translations/strings_zh.i18n.json b/assets/translations/strings_zh.i18n.json index 035810f7..649b113b 100644 --- a/assets/translations/strings_zh.i18n.json +++ b/assets/translations/strings_zh.i18n.json @@ -225,7 +225,8 @@ "update": "更新", "download": "下载", "failureMsg": "更新资产失败", - "successMsg": "已成功更新资产" + "successMsg": "已成功更新资产", + "addRecommended": "添加推荐资产" } }, "about": { diff --git a/lib/data/local/dao/geo_assets_dao.dart b/lib/data/local/dao/geo_assets_dao.dart index 13a04436..25ef879a 100644 --- a/lib/data/local/dao/geo_assets_dao.dart +++ b/lib/data/local/dao/geo_assets_dao.dart @@ -12,6 +12,10 @@ class GeoAssetsDao extends DatabaseAccessor with _$GeoAssetsDaoMixin, InfraLogger { GeoAssetsDao(super.db); + Future add(GeoAsset geoAsset) async { + await into(geoAssetEntries).insert(geoAsset.toCompanion()); + } + Future getActive(GeoAssetType type) async { return (geoAssetEntries.select() ..where((tbl) => tbl.active.equals(true)) diff --git a/lib/data/repository/geo_assets_repository.dart b/lib/data/repository/geo_assets_repository.dart index 104c8677..57c7c638 100644 --- a/lib/data/repository/geo_assets_repository.dart +++ b/lib/data/repository/geo_assets_repository.dart @@ -146,4 +146,23 @@ class GeoAssetsRepositoryImpl GeoAssetFailure.unexpected, ); } + + @override + TaskEither addRecommended() { + return exceptionHandler( + () async { + final persistedIds = await geoAssetsDao + .watchAll() + .first + .then((value) => value.map((e) => e.id)); + final missing = + recommendedGeoAssets.where((e) => !persistedIds.contains(e.id)); + for (final geoAsset in missing) { + await geoAssetsDao.add(geoAsset); + } + return right(unit); + }, + GeoAssetFailure.unexpected, + ); + } } diff --git a/lib/domain/rules/geo_asset.dart b/lib/domain/rules/geo_asset.dart index 2420d913..409b7b82 100644 --- a/lib/domain/rules/geo_asset.dart +++ b/lib/domain/rules/geo_asset.dart @@ -49,3 +49,21 @@ const defaultGeosite = GeoAsset( ); const defaultGeoAssets = [defaultGeoip, defaultGeosite]; + +const recommendedGeoAssets = [ + ...defaultGeoAssets, + GeoAsset( + id: "chocolate4U-geoip", + name: "geoip.db", + type: GeoAssetType.geoip, + active: false, + providerName: "Chocolate4U/Iran-sing-box-rules", + ), + GeoAsset( + id: "chocolate4U-geosite", + name: "geosite.db", + type: GeoAssetType.geosite, + active: false, + providerName: "Chocolate4U/Iran-sing-box-rules", + ), +]; diff --git a/lib/domain/rules/geo_assets_repository.dart b/lib/domain/rules/geo_assets_repository.dart index b063129a..2e55632d 100644 --- a/lib/domain/rules/geo_assets_repository.dart +++ b/lib/domain/rules/geo_assets_repository.dart @@ -11,4 +11,6 @@ abstract interface class GeoAssetsRepository { TaskEither update(GeoAsset geoAsset); TaskEither markAsActive(GeoAsset geoAsset); + + TaskEither addRecommended(); } diff --git a/lib/features/settings/geo_assets/geo_asset_tile.dart b/lib/features/settings/geo_assets/geo_asset_tile.dart index b05e8dc7..db1f6de0 100644 --- a/lib/features/settings/geo_assets/geo_asset_tile.dart +++ b/lib/features/settings/geo_assets/geo_asset_tile.dart @@ -89,6 +89,11 @@ class GeoAssetTile extends HookConsumerWidget { ], ), selected: geoAsset.active, + onTap: () async { + await ref + .read(geoAssetsNotifierProvider.notifier) + .markAsActive(geoAsset); + }, trailing: PopupMenuButton( itemBuilder: (context) { return [ diff --git a/lib/features/settings/geo_assets/geo_assets_notifier.dart b/lib/features/settings/geo_assets/geo_assets_notifier.dart index d7669962..0e1cb3db 100644 --- a/lib/features/settings/geo_assets/geo_assets_notifier.dart +++ b/lib/features/settings/geo_assets/geo_assets_notifier.dart @@ -20,7 +20,28 @@ class GeoAssetsNotifier extends _$GeoAssetsNotifier with AppLogger { Future updateGeoAsset(GeoAsset geoAsset) async { await ref.read(geoAssetsRepositoryProvider).update(geoAsset).getOrElse( (f) { - loggy.warning("error updating profile", f); + loggy.warning("error updating geo asset", f); + throw f; + }, + ).run(); + } + + Future markAsActive(GeoAsset geoAsset) async { + await ref + .read(geoAssetsRepositoryProvider) + .markAsActive(geoAsset) + .getOrElse( + (f) { + loggy.warning("error marking geo asset as active", f); + throw f; + }, + ).run(); + } + + Future addRecommended() async { + await ref.read(geoAssetsRepositoryProvider).addRecommended().getOrElse( + (f) { + loggy.warning("error adding recommended geo assets", f); throw f; }, ).run(); diff --git a/lib/features/settings/geo_assets/geo_assets_page.dart b/lib/features/settings/geo_assets/geo_assets_page.dart index b4ddfdc5..b53aedf1 100644 --- a/lib/features/settings/geo_assets/geo_assets_page.dart +++ b/lib/features/settings/geo_assets/geo_assets_page.dart @@ -17,6 +17,22 @@ class GeoAssetsPage extends HookConsumerWidget { slivers: [ SliverAppBar( title: Text(t.settings.geoAssets.pageTitle), + actions: [ + PopupMenuButton( + itemBuilder: (context) { + return [ + PopupMenuItem( + child: Text(t.settings.geoAssets.addRecommended), + onTap: () { + ref + .read(geoAssetsNotifierProvider.notifier) + .addRecommended(); + }, + ), + ]; + }, + ), + ], ), switch (state) { AsyncData(value: final geoAssets) => SliverList.builder( From 38d3a70954983b925e72419d55c06f6e1b46fb30 Mon Sep 17 00:00:00 2001 From: betaxab <30405429+betaxab@users.noreply.github.com> Date: Sat, 18 Nov 2023 16:49:27 +0800 Subject: [PATCH 45/91] inlang: update translations --- assets/translations/strings_zh.i18n.json | 42 ++++++++++++------------ 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/assets/translations/strings_zh.i18n.json b/assets/translations/strings_zh.i18n.json index 649b113b..b4152345 100644 --- a/assets/translations/strings_zh.i18n.json +++ b/assets/translations/strings_zh.i18n.json @@ -45,7 +45,7 @@ "updatedTimeAgo": "更新 ${timeago}", "remainingDuration": "剩余 ${duration} 天", "remainingTrafficSemanticLabel": "已消耗 ${consumed} 流量,共 ${total} 流量。", - "expired": "已到期", + "expired": "已过期", "noTraffic": "超出配额" }, "sortBy": { @@ -56,7 +56,7 @@ "buttonText": "新的配置文件", "shortBtnTxt": "新的配置文件", "fromClipboard": "从剪贴板添加", - "scanQr": "扫二维码", + "scanQr": "扫描二维码", "qrScanner": { "permissionDeniedError": "权限不足", "unexpectedError": "出了些问题", @@ -83,7 +83,7 @@ }, "edit": { "buttonTxt": "编辑", - "selectActiveTxt": "选择活动配置文件" + "selectActiveTxt": "选择并激活配置文件" }, "delete": { "buttonTxt": "删除", @@ -93,7 +93,7 @@ "save": { "buttonText": "保存", "successMsg": "配置文件保存成功", - "failureMsg": "保存配置文件失败" + "failureMsg": "配置文件保存失败" }, "detailsForm": { "nameLabel": "名称", @@ -104,7 +104,7 @@ "invalidUrlMsg": "无效的网址", "lastUpdate": "最后更新", "updateInterval": "自动更新", - "updateIntervalDialogTitle": "自动更新间隔(以小时为单位)" + "updateIntervalDialogTitle": "自动更新间隔(小时)" } }, "proxies": { @@ -140,13 +140,13 @@ "ir": "伊朗 (ir)", "cn": "中国 (cn)", "ru": "俄罗斯 (ru)", - "other": "其他" + "other": "其它" }, "themeMode": "主题模式", "themeModes": { "system": "遵循系统主题", "dark": "黑暗模式", - "light": "明亮模式", + "light": "浅色模式", "black": "深色模式" }, "enableAnalytics": "启用分析", @@ -188,7 +188,7 @@ "route": "路由选项", "dns": "DNS 选项", "inbound": "入站选项", - "misc": "其他选项" + "misc": "其它选项" }, "pageTitle": "配置选项", "logLevel": "日志级别", @@ -219,14 +219,14 @@ "strictRoute": "严格路由" }, "geoAssets": { - "pageTitle": "路由资产", - "version": "版本${version}", + "pageTitle": "路由资源文件", + "version": "版本 ${version}", "fileMissing": "文件丢失", "update": "更新", "download": "下载", - "failureMsg": "更新资产失败", - "successMsg": "已成功更新资产", - "addRecommended": "添加推荐资产" + "failureMsg": "更新资源文件失败", + "successMsg": "已成功更新资源文件", + "addRecommended": "添加建议的资源文件" } }, "about": { @@ -245,7 +245,7 @@ "currentVersionLbl": "当前版本", "newVersionLbl": "新版本", "updateNowBtnTxt": "现在更新", - "laterBtnTxt": "之后", + "laterBtnTxt": "以后再说", "ignoreBtnTxt": "忽略" }, "tray": { @@ -263,19 +263,19 @@ "unexpected": "意外错误", "clash": { "unexpected": "意外错误", - "core": "Clash错误 ${reason}" + "core": "Clash 错误 ${reason}" }, "singbox": { "unexpected": "意外服务错误", "serviceNotRunning": "服务未运行", - "missingPrivilege": "缺少特权", + "missingPrivilege": "缺少权限", "missingPrivilegeMsg": "VPN 模式需要管理员权限。以管理员身份重新启动应用程序或更改服务模式", + "missingGeoAssets": "缺失 GEO 资源文件", + "missingGeoAssetsMsg": "缺失 GEO 资源文件。请考虑更改激活的资源文件或在设置中下载所选资源文件。", "invalidConfigOptions": "配置选项无效", "invalidConfig": "无效配置", "create": "服务创建错误", - "start": "服务启动错误", - "missingGeoAssets": "缺少地理资产", - "missingGeoAssetsMsg": "地理资产缺失。考虑更改活动资产或下载设置中选定的资产。" + "start": "服务启动错误" }, "connectivity": { "unexpected": "意外失败", @@ -299,12 +299,12 @@ "geoAssets": { "unexpected": "意外的错误", "notUpdate": "无可用更新", - "activeNotFound": "未找到活动地理资产" + "activeNotFound": "未找到激活的 GEO 资源文件" } }, "play": { "title": "Hiddify Next(预览)", "short_description": "自动,SSH, VLESS, Vmess, Trojan, Reality, Sing-Box, Clash, Xray, Shadowsocks", - "full_description": "HiddifyNext 的主要目标是提供安全、用户友好且高效的隧道客户端。它使您能够利用 VPN 服务权限将所有流量或选定的应用程序流量路由到您选择的远程服务器。\n\n注:我们不提供任何服务器;用户需要使用自己的自托管服务器或受信任的服务器来确保其在线活动的私密性。\n \n我们支持以下类型的服务器:\n- 普通V2ray/Xray订阅链接\n- Clash订阅链接\n- Sing-Box 订阅链接\n\n我们的独特特点是什么?\n\n-用户友好\n-优化和高速\n-自动选择最低延迟\n-显示用户使用信息\n-通过一键深度链接轻松导入子链接\n-免费且无广告\n-轻松切换用户子链接\n-等等\n\n支持:\n- Sing-Box 支持的所有协议\n- VLESS + xtls 现实、愿景\n- VMESS\n- Trojan\n- ShoadowSocks\n- Reality\n-V2ray\n- Hystria2\n- TUIC\n- SSH\n- ShadowTLS\n\n\n源代码位于https://github.com/hiddify/Hiddify-Next\n应用程序核心基于开源的Sing-Box。\n\n权限说明:\n\nVPN服务:由于此应用程序的目标是提供安全、用户友好和高效的隧道客户端,我们需要此权限以能够通过隧道将流量路由到远程服务器。\n查询所有包:此权限用于允许用户包括或排除特定应用程序以进行隧道传输。\n接收启动完成:此权限可以从应用程序设置中启用或禁用,以在设备启动时激活此应用程序。\n发送通知:此权限是必需的,因为我们使用前台服务来确保VPN服务的持续运行。\n此应用程序没有广告。分析和崩溃数据仅在用户在首次使用应用程序时明确同意的情况下发生。" + "full_description": "HiddifyNext 的主要目标是提供安全、用户友好且高效的隧道客户端。它使您能够利用 VPN 服务权限将所有流量或选定的应用程序流量路由到您选择的远程服务器。\n\n注:我们不提供任何服务器;用户需要使用自己托管的服务器或可信的服务器来确保您在线活动的私密性。\n \n我们支持以下类型的服务器:\n- 普通 V2ray/Xray 订阅链接\n- Clash 订阅链接\n- Sing-Box 订阅链接\n\n我们的特色是什么?\n\n- 用户友好\n- 优化和高速\n- 自动选择最低延迟\n- 显示用户使用信息\n- 通过一键链接轻松导入\n- 免费且无广告\n- 轻松切换线路\n- 等等\n\n支持:\n- Sing-Box 支持的所有协议\n- VLESS + XTLS Reality、Vision 协议\n- VMESS\n- Trojan\n- Shoadowsocks\n- Reality\n- V2ray\n- Hystria2\n- TUIC\n- SSH\n- ShadowTLS\n\n\n源代码位于 https://github.com/hiddify/Hiddify-Next\n应用程序核心基于开源的 Sing-Box。\n\n权限说明:\n\n- VPN 服务:由于此应用程序的目标是提供安全、用户友好和高效的隧道客户端,我们需要此权限以能够通过隧道将流量路由到远程服务器。\n获取应用程序列表:此权限用于允许用户包括或排除特定应用程序以进行隧道传输。\n- 接收开机广播:可以从应用程序设置中启用或禁用此权限,以便在设备启动时激活此应用程序。\n- 发送通知:此权限是必需的,因为我们使用前台服务来确保 VPN 服务的持续运行。\n- 本应用程序没有广告。分析和崩溃数据仅在首次使用应用程序时经用户明确同意的情况下发生。" } } \ No newline at end of file From 00d9ba8d8b550cfdf732593bfc26511c10a75bc0 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Sat, 18 Nov 2023 15:56:27 +0330 Subject: [PATCH 46/91] release: version 0.10.10.dev --- CHANGELOG.md | 9 +++++++-- dependencies.properties | 2 +- pubspec.yaml | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63ee613f..99c9c832 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,8 +17,11 @@ - On desktop, app will try to connect to the last used profile on startup. (if last session was not explicitly disconnected by the user) - Added AppCast Update Checker - Checking for new versions of the app will use a more reliable approach on all platforms. +- Added Geo Asset Settings + - Update geo assets and use recommended providers - Added **winget** Release - Now you're able to install and update Hiddify Next on Windows using [winget](https://learn.microsoft.com/en-us/windows/package-manager/winget/). +- Added Turkish Translations. [PR#173](https://github.com/hiddify/hiddify-next/pull/173) by [Hasan Karlı](https://github.com/hasankarli) - Changed in-app Toasts - Updated Core Sing-box Version to 1.7.0 - Improved Network Reliability While Adding/Updating Subscriptions @@ -33,9 +36,11 @@ - Fixed localization mistakes in Chinese. [PR#113](https://github.com/hiddify/hiddify-next/pull/113) and [PR#123](https://github.com/hiddify/hiddify-next/pull/123) by [Nyar233](https://github.com/Nyar233) - Fixed localization mistakes in Chinese Readme. [PR#137](https://github.com/hiddify/hiddify-next/pull/137) by [wldjdjsks](https://github.com/huajizhige) - Fixed localization mistakes in Chinese. [PR#138](https://github.com/hiddify/hiddify-next/pull/138) and [PR#165](https://github.com/hiddify/hiddify-next/pull/165) by [wldjdjsks](https://github.com/huajizhige) -- Fixed localization mistakes in Russian. [PR#155](https://github.com/hiddify/hiddify-next/pull/155) and [PR#162](https://github.com/hiddify/hiddify-next/pull/162) by [solokot](https://github.com/solokot) +- Fixed localization mistakes in Russian. [PR#155](https://github.com/hiddify/hiddify-next/pull/155), [PR#162](https://github.com/hiddify/hiddify-next/pull/162) and [PR#169](https://github.com/hiddify/hiddify-next/pull/169) by [solokot](https://github.com/solokot) - Fixed linux build libs command. [PR#161](https://github.com/hiddify/hiddify-next/pull/161) by [Aloxaf](https://github.com/Aloxaf) -- Fixed localization mistakes in Russian. [PR#164](https://github.com/hiddify/hiddify-next/pull/164) by [jomertix](https://github.com/jomertix) +- Fixed localization mistakes in Russian. [PR#164](https://github.com/hiddify/hiddify-next/pull/164) and [PR#168](https://github.com/hiddify/hiddify-next/pull/168) by [jomertix](https://github.com/jomertix) +- Fixed localization mistakes in Chinese. [PR#179](https://github.com/hiddify/hiddify-next/pull/179) by [betaxab](https://github.com/betaxab) +- Fixed localization mistakes in Chinese Readme. [PR#172](https://github.com/hiddify/hiddify-next/pull/172) by [Locas](https://github.com/Locas56227) ## [0.10.0] - 2023-10-27 diff --git a/dependencies.properties b/dependencies.properties index d4c735aa..75a0ede1 100644 --- a/dependencies.properties +++ b/dependencies.properties @@ -1 +1 @@ -core.version=0.8.1 \ No newline at end of file +core.version=0.8.2 \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index c7642e45..dac5bd33 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: hiddify description: A Proxy Frontend. publish_to: "none" -version: 0.10.9+1009 +version: 0.10.10+1010 environment: sdk: ">=3.1.0 <4.0.0" From 49bb62c8289d0885a762e01b7f0785690a7d3af7 Mon Sep 17 00:00:00 2001 From: Hiddify Date: Sat, 18 Nov 2023 14:53:27 +0100 Subject: [PATCH 47/91] new: add ic_launcher for android tv --- .../drawable/ic_launcher_banner_forground.xml | 27 ++++++++++++++++++ .../ic_launcher_background.xml | 6 ++++ .../mipmap-anydpi-v26/ic_launcher_banner.xml | 4 +++ .../res/mipmap-mdpi/ic_launcher_banner.png | Bin 0 -> 5869 bytes .../res/mipmap-xxxhdpi/ic_launcher_banner.png | Bin 5172 -> 0 bytes android/app/src/main/res/values/colors.xml | 3 +- 6 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 android/app/src/main/res/drawable/ic_launcher_banner_forground.xml create mode 100644 android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_background.xml create mode 100644 android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_banner.xml create mode 100644 android/app/src/main/res/mipmap-mdpi/ic_launcher_banner.png delete mode 100644 android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_banner.png diff --git a/android/app/src/main/res/drawable/ic_launcher_banner_forground.xml b/android/app/src/main/res/drawable/ic_launcher_banner_forground.xml new file mode 100644 index 00000000..97131576 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_launcher_banner_forground.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_background.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_background.xml new file mode 100644 index 00000000..e7b046de --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_background.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_banner.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_banner.xml new file mode 100644 index 00000000..eef7c65c --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_banner.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_banner.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_banner.png new file mode 100644 index 0000000000000000000000000000000000000000..d9812bd255b03216982020d23c7dfbbeb6768bfd GIT binary patch literal 5869 zcmeHLXH-*5yWRm5ltU8*rHOJ76flDHE{HTKf^-b@MQR3IV(0zxQK zLkP%`E-lie1R->i&_hTdcXR*UweFvLf1kb9UVF_uvu5_1d7o#V_jzt>ZOV64>?i;L zeCB3X?EruS4FHfkJcq$Zn~KE^@N?w8nM()&C|Vx8kUYJ#LNEvku`@LSN_!+0!3LM# z6{{-%P@c@o@;U?nB9F|kUbz_wp-fmN-r7Pot+69l%Kch%ezb@GEz$8z?8d3l1jlFI zCvx9dWLP|p_w27dcI&fI#nsPm%tq0kZ?B?n-W0hZl5~@JNdBqCkJn;F)=tfuKfm(_ z3G!FVCf*#YFz?pYrA@NchP&~N+4FVS4Q9+|2x;v$^&Mhm1J7n_v5663q0#yfK(Mu* z3ow-D5Co)8LU;hdM1Tw6xdw0mTrvOzcqD`dfMy;;0PV>o4H#bfci6uh@_)$?UnGZP zKy0+4RU`Y_dvScm?%`*|Rs`Yo5FgwI=;PfjeQDh*=Q*0+!#Zol>P;>dawck&X$9^l zh}y9!5wgHSIEP@`-R-vm#DB1;Oy7%{0lOQ^HMEjz(m<$=eN)*J-GK-S`E_Rs?L)$T zz1!0>K&Uu`C*h9z1TTQM28@I)t>_$vP+@dgQ*Z$S@-h90;ZEWr4EQ97e&SazioU}o z?e&g&NcsZA%q$-PiN6jUdjWO}qPe{4MFD*|;Jh9Y57^oOcCKLeM=gliVg~gP`o|GN ziyxs-K+y`Y6PuwmI3H=M5eCX?!;V-;C$pX#0M1env-7omoIsk*{{kgNPEOnZh(dew zES=^CdUTk?Ra=h>xxdw^qqjil5M&;so+vPzZAav^C!_b|+ zIWB)$0AjHeDSPhTy8I|EwT)Y9B%_nO)3F=aeOd=`Cq>Z|_HrbbjV!slswSqT?r@}u z%l{m(m{C{GJILJXiCylFt)iuQ~Tpo!Zh)#HvN5 z2)~d9?&4xFlO1%D*;@i~qg4;FVbnuBy{8fwyKqQ|CuMTk0Aq%g%yM2I9()A%pJlf^ zULJVJ}<}1?ol-2aQjaf3F&?c zxw6AW+_QTKjnAf1<0(UD;V3EO-OO;r%Ae-Vo#y7N?vxRYQd+M@8%c&+-GN~3g8h}x z$+8|ipC{L-l~V7b?>r|=s!#{?ObXL5QJVtVCV*Z1k5X!2 zdP7X2LcB|Nwa)JF=0?hJ-!Y}?A-A$wp?_NM&c)4CF|09Vp$CH zJsi~?!!I>*vz|$=Q2Du?6zH~F%xrbNU9!gUwZyGq^kXHLw89V{5ntxYFr1`VlKM`S zjBALQiX`p#-{pAab9sWdOgqU*NIWo~$R%y50yfo66lFL~j-eHchVu~{8(Dkg7FC16 zMqA23Qvb2^FbBGPj%HX3qg4kJh+BSI*KOtCLiODnv$t@8GHr`Udmlp0)#VG9O z0`N^d(i+_`oCU{|2X zY9#L<7MUn-rGNN^(T19BHr6D~OOB^88TVX(xf{vD3fzqnSA87v1G{BVye(UJ0bj@C zXqw}8o}(+3t_g4Zj?^WrMUtC;Dbti9CmGBS2I2MeaML4((wR@WTLVHHb{0EWYOvzO zXfmQJ&EZtdgMcLF_f=be)2cL`u-~cjyla|rsxq+Rl{`p?w1s3qU>LfR}&>km+ng?pxk)fJA##h|6u9y^vpu^lzK8jLOrTe z6;nLaHCF42yFh&3YY;YOg<-eJ+)kwplatz#^>k}L75d1(2e{;I^+)l_`@vkZZ&$m5 zn|fv3lh)E(XsbCtZ#}3wU#5z(ha7!VU?PqMetK=X`&|8^*aaikN@_Wxa*U<6I>$zq z`t?6*icCY~&7}6+z67kLgMf(=C(tR-c!LFH@PDOg*p2#%qOJ5>-EeYLdl#1ZvkwVz2lV|*wKOJfj5GNe$4^VG#emPnuA$O8up7Fe)UW$ zAHKqKr7l^Vvn+<)0Bf*A&B9?lhZn3k%D88z#8LASlUBAoRkG{zd+{nQmPJe8Qv0AaQ3X#;o}p7Jb|49aIZiq z@G+IkQxv2%;M-$hg##1;=2;k!uHmAZcK%36uwY zR$QLEpu7O;UjqR`ppY=EIfg#eC4-zH#GJ9hpyVJagFFAYb3~_q&=Ohs-61nAAfJ`f zyxVqLc;h*BFp%CSC0;`S6^(NlNa|*Wib>pK3F%!L+UmLs_xmmDDO+OnXF1C|;TMOe0yZeR7p4TGbROQC)> zYrhsxiRO+Q#Tr*!8qv`@su8^6`U$)#(Htvve#R3sGjclOrdW2&*%N`i)ZM_9&GwgN z&f}%f!PC>pm<$U_8Ywh@8|e%Gh)uPKjdCAg(N%juTv6V zRL*1DFvS+>X{3~ux`J5m-BcNhOX1lzk_}2ddjWQUFm8c>`bCS;(|g|EW`#0L_h35O zH=YxUHNMMxaP>v5Lch!Kjb)fZ4v47h@k*=u{?ta3-fB~8TEOjhy8ElP6EnI;cRFA9 z|GL1iYEO<`FWgy%9LyW_WM%E~hb8^=+p{#({i^6bCG9tpI(qhcefo?QT5)btRi1t@ zOOO65u<*n>W+Ffw6EjB-9BqBtJ5wIVEu7%5el(-NXJa`P`{|+uC9D%}^b+&MVE7Wj z+6d2#Gy#sLn6_WM_0AHmmMNYtP|uLTq*wWyqPCiJb-!H zxUP<({-Wi7D5a!{2JBDP5Xu%Xb&gTuA-bwChx+^C8{4y#@n}<&TA~wpg%r_2scBVH zE>c@E-m0R-D*36AG%$DCU_-#EnAEa|;4Z>C#g1_o2VPDMqg8SiVqZ^IH80QD7_9I{#xdS+4Lb+Xem6eDsa;usoGdL?rS z%AZxB+CEd?RFJ*5zN0qr19qn z4T!T*B_<}r@pf!|nA~AvmU_*3k)i)Y+OPGNE5C8wLbi*ZCnv`={mfM~fo5i!woq|? zElMr$<+PZHH@&?Zzr#&Y7hJcD5<64iQfvMuJad8FR~}65CxN@Qu$SuqnTRAT_|lVu zswBLl$m%v-rGIm!GyUd~e~MFw&3CpA|M&Wt<_Ds9YmBbMq+f4j`@YBA>s<#zt@U3~ zqaswYc0Kc>KQ2G*Pif=l&kH&2;jx_?ZN#Kju`3-CDkQuax>b&-)A!Ce>qTIXSqz4Z z>+2AdF!bt?P1o40Qc_$ER_s1QqNG8}Lm%#6=-iR}h|L`R4L3p3-gj`b0wdf>txI)? zdB325+l{B!?Cj%tYm}j1vQU|{uNCt)L(sY;UV8Gfa(#7RxAF%<4WpsD0Z!f+e(}m9 z!NXy{`?r62i0M2(p-QkjH?$yD!=6F++->!*%0%}P3=G)68#4%gpRn}JTasDfaukle zeUE*Wu*YD9LCnXKEI1>Of&CsD26~Rq2^s@cZ?XA7uQ2pL50Jp@K(U{Z-_ww#O-W|& z8XPl(7T;RvzoYb;-reO?4A=F~X7>atA&kGT?@-f!jgLIFIF7%$2cTz*Ubb&HEuQ(B zr0n)TOpSSf!|=!~LTW zaeT{1$uS!0JOlqjTNFQO4GxRxAsJ;!JnV`%aSHQ=@FSDgPPd)YyQ^l2p{ z{+x$6DU0IrH5)>I|9m!KJ0Bl6aE}~F_2$|RJZWw05vvU`gf8H;wI_BDp&z{y$Gtj7 zLS6o8lav<@me-}K_vyn$$`!7Rx?Xopm%Td$XR{INVRMvf1G+>hXN1A8{>a1VRKWvF nE`#Inzc$^!hUEX-p=Ar`$f|G0y4|+{z{lLg`f90>=cE4uCWsS7 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_banner.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_banner.png deleted file mode 100644 index f63fa5b9f48e7bb7bdf9cb4a55e8531a7ed5a49b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5172 zcmdT{cU)6hzP|wmP*FfEaZm_B7Xee~CQ_xNQKU-Kgb*MC2_}Jr1SF#fb_78I1#F<` zP-N&WfC?%gY8*sL5D<|XdN1!{`|Qs1?Y_6~uf3noP0l^%`|IVLbJq5N<=WN1t_A>L zt+kbz9RNUb!11`GIQZK;Y3dICtqQbqp##7gxs?|JB;9}lfY>nE;V|PcZaIOf`~?=VR{%OhDao8dy#g*G|`%xD6|$zLmQ!?fz{N-YG7dB9ylnD=H-pGGqd<6 z4E!>H`!X1TSQLuMWFnbbNGi<-rGddMF>P`Akix}eF1tAf8YXxX#QV>dl6A2 ze^LO6!k~j#jUU)RUn+x2_oe<1tbZ*3g8p8Bsw*SMkJXBgK8?TSQCgfrI82>D$RjP_5U6z+wUsFG_{agu$|6iiWikhSN%Hz zk{N+PGJu0o(*om#KN&^Lh+G%~!0*Av|Q|Jr=g-EhCGk}9O zBgtehtR6|1sE77MBXo7WwGo=0+606rQO^sZ;jQbfsfX4hY3X3T)0QmV6Dh>FjyhqD#D(0{M*Q%4EvG=SORe+yA9yPm9Ub$;NPan z|F#FdvSs>`K&JmB++SgIsyBm4ppi^`KyUrabV7m3qgLwhs~xDnn)qey?`rlt9Bc$D z!yhdO{P@wtNEEPh(ZCipRB&Du0M-p!o0&MUZVa_^7;%%ywWA%VEA?L9wB)wzD|P$1 zm&HtqU9^2~uli*bzS^deq;%#?7t+u35XDk$eMZ4y>QHVI;-Nu1e|V}nzjV03O?oKB z@rmm}2rB+md_1yhhwZB0wqN#HZYIeaKMT<0^@I<8_;{X%b>U%~ zw1p+sJGYM6*>J=8H>gXY%lnzxB$umwXLDp5Un3| z+i=9^b{FzH6`y-tjsjP&*U0BmLVQg=>^Yl~(59r{COc*#bCC;y9{+9s!T$8qRZWTA zVRdnD7N)mFh(Scro}tGz1XoLL&-Wf|@SiH$YRKoHOJvZ1^6d<@!p^FaE|qqqWlA3c znk)(BUVec)I~g`GoJ)0{k?Y4%t3F#)0ud3dTQ>6ToF{tKAB;7rXI|Matom$O2}rdr zsBY1t9)X6|Y!+_yVXuKpfbzWZ!Pg%Y^`DpxS?(>hi4Mt}Q0se!Gy7sFN?pzj^>+y=!@b<=i?jzcjDX zxLDUoEo4qGk^~aX5QwOM`p)*`g@%pYM}HqtdFwHgk9T6YpzGWmJz`o|wO6R&eV==K zOK}vjL9YE7E{^4KsK3HxsV2MyO$zT|O*+Q)l@H&Vdn6sfk;hw>iHiE)JZ{mH=|@#R z)?h`$4GR7IvaLbKw`_`Z;bzLBD{qc16qPmYAo7k3AH1*lnb?MmGP>M%TtUC(IoZq% zv=s^#&F!S#Or-_9=`l>XlRj3x(AD1g@9rKc4IzxY zzp-K3;mV$tJtfhJ3*%8Y8zz9}H4xG2yZT1R^0cg(m>0QmrFnMi_?%!x-|%Vwcg4}3 z9u|*2PXN-xRMNx>^=Ws3?L?Um|rEl$D%#QL9yF=k#9bh>dDcdg(jY z>9MHrW2ysreTcw(Q6`(ItyRUxi{3% zxTmo_r{_{^04C#6yldZK{`EO**i_qZDknnk&z=sSpU~mE;f-2yXPAc;VwzeeA`U5C z?tAX*q3=EaG4|sQw%AKX{R#hUW{1kXy=?P!N8R^3Tx#rD)uR!W!R+X~biTs!NcOAE z!b+~pSjpDc()9@U^4n@c*|eb8K7&)Q1+V$7;7edjpl!vZBq7ZGamOCKaLZ#~U4B&5 z{8v*c}2)&BP{{oh%wJL$miJrn+N?2l)G9oknADmAgO|+}15=!t0JXq3;`t z-MT#QG^8Rw@;?F)(YZf9%)Rw<`;@>7?lfl!C1lVaY$!>Wq5dh`+U!zNU){_zKrV}Y zh?ax~o-Nv)rmHJ9?5d3rs;1?EMFt$bz=ed>ml~3)liWlzt9DT{`w-4gcf3GzXk)ue2NwHDY6my5ds-uYm%UAO0t9jHqmTFpD)9BiFKH-z6b*>x<-V;(>{gh<^ z*r=+DG+KO2uPqW@_gJ{_re$i`{HDH91z{AI_y049PvvS`~??}tr2pY~E<+S>BPQLFzE3J!t7hRR|!}wE^z`a4PAo zwNbl%k5STt@l@xquCzEie(m;b)(O&kt;lpp4n#z}RkbmCUu%APS254kany-&j?)rf zkwq(`l6qJjMS&DiyyA_$5t;r)<*Zqc!cW3iHq)Gk(wX@w4heyehed~kQABxwqlC{n z-9HwW8%34YS9msdA_N`1?;zuBv63?P6&#ON`&uTSMpbt_XFj-<{HE!aK+ITNL|nNH zX~`IGtL7v<7%SMhvh6@%(h$?;KXC_A%kq!cGv)L(LIKgr%d+SQ+@;2iyke!I;$Y8~ zMVkRAp94Izs6=;xo8QYnb(VVesnK=W?w9<*`3vjBq?8{Vx0pg8#?MtGt`~0gW3S=9 zKFuYVZ7;e*3>Mfgy`6}?>uBR=s45BjC<)#BVw+qV=%h`VS(DU@=Px3)%Oxs5n^($c z#-~-h!L2#fDE3F09$0Yr31vLAU~FNg?c(Hb4fgwDuO2h>kc91*ghrBv22V!j7j7JO za8*eXh`@4X&{EIyVxM$foU95UEzPjgW-g0!M?m^GUlX! z_d*dt=Fz@s#d|O?Daffn@bop>?5w9F<_g9*rAnV>13O;dvDouO*pL)=LTHsKil)tE z$c>SETn{Dl!pt27bEdT~%i4o;2OPqq?~l~>a@;Sia`iiLMl=GBhZ^J^*!5z>VIxVZ z{~{E)HPa{e|2gwI->JQicj^DA&WRpgD9WTq+x52r3bBXvFhmnVBr~hYCVIjUxMf)h z6Wl7`FPC%`hwWRZFtp&0j-Ic1$ZK`f!z`8zF)z(?3hQEw?B;gId&aw6#$-U${5gX(e*L>!ajj==CT znSTr{C`NKFXtkHy`2D%*oB>cB#Wleiav^3r|Cc=|a$iuQ0(vJ_o72dcJU3f`AWar9 z8O*2;w&hB@Ep}VCo|m<#42)5(Z5-AqC!m9c)NnAk@986V6NK|mGE$CiBMX0hh9GA3 zTg|^WwQ+IUseAfy;_x_6>q}z{jA<9<^O*OCCI-@(Qf95svyLv0Ep{&}oFfjg7x}&M zms3Yl!-lThGd+HzU7|(0AbD{8ddkE3dpyjNC|@0q5|g?%`yxyF>IwG3U}-wjy~^D zg_ItBHq%ze+nxLAqV&N~f41bHxf|^z3?`Mg)Zy1%l6>fro66mWB&S$yllKEP_j-*C zn~ImCs9P7UIhKt%lsjYQY73(oI`8f_Oh0|<6*brV7~H<;UHM1Dr=sWArLLYm&w}y0 zONka!?R=ZqEJquRY1jhcbNlr34AJbtfcfis;hZt$w79M>7Yp$Ibz`sCfBN0)CpX6l ZBg^L*qYqEq83n+{+Wdf7)?WO{{{l;xV(b6_ diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml index 0f386765..90c34714 100644 --- a/android/app/src/main/res/values/colors.xml +++ b/android/app/src/main/res/values/colors.xml @@ -1,4 +1,5 @@ - #FFFFFFFF + #F1F4FB + \ No newline at end of file From ea8f86dff8a62297bbaa7442d1ebfe59e9e9c4e3 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Sun, 19 Nov 2023 12:29:30 +0330 Subject: [PATCH 48/91] Change ir region rules --- lib/data/repository/config_options_store.dart | 2 +- lib/domain/singbox/rules.dart | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/data/repository/config_options_store.dart b/lib/data/repository/config_options_store.dart index 1f181738..0a5169be 100644 --- a/lib/data/repository/config_options_store.dart +++ b/lib/data/repository/config_options_store.dart @@ -99,7 +99,7 @@ List rules(RulesRef ref) => switch (ref.watch(regionNotifierProvider)) { id: "id", name: "name", enabled: true, - domains: "domain:.ir", + domains: "domain:.ir,geosite:ir", ip: "geoip:ir", outbound: RuleOutbound.bypass, ), diff --git a/lib/domain/singbox/rules.dart b/lib/domain/singbox/rules.dart index 96c53c9e..686474d2 100644 --- a/lib/domain/singbox/rules.dart +++ b/lib/domain/singbox/rules.dart @@ -8,9 +8,9 @@ part 'rules.g.dart'; class Rule with _$Rule { @JsonSerializable(fieldRename: FieldRename.kebab) const factory Rule({ - required String id, - required String name, - @Default(false) bool enabled, + @JsonKey(includeToJson: false) required String id, + @JsonKey(includeToJson: false) required String name, + @JsonKey(includeToJson: false) @Default(false) bool enabled, String? domains, String? ip, String? port, From 311542650fd71183afcf8894e0addefbfcb2fdd4 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Sun, 19 Nov 2023 12:36:08 +0330 Subject: [PATCH 49/91] Revert "new: add ic_launcher for android tv" This reverts commit 49bb62c8289d0885a762e01b7f0785690a7d3af7. --- .../drawable/ic_launcher_banner_forground.xml | 27 ------------------ .../ic_launcher_background.xml | 6 ---- .../mipmap-anydpi-v26/ic_launcher_banner.xml | 4 --- .../res/mipmap-mdpi/ic_launcher_banner.png | Bin 5869 -> 0 bytes .../res/mipmap-xxxhdpi/ic_launcher_banner.png | Bin 0 -> 5172 bytes android/app/src/main/res/values/colors.xml | 3 +- 6 files changed, 1 insertion(+), 39 deletions(-) delete mode 100644 android/app/src/main/res/drawable/ic_launcher_banner_forground.xml delete mode 100644 android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_background.xml delete mode 100644 android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_banner.xml delete mode 100644 android/app/src/main/res/mipmap-mdpi/ic_launcher_banner.png create mode 100644 android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_banner.png diff --git a/android/app/src/main/res/drawable/ic_launcher_banner_forground.xml b/android/app/src/main/res/drawable/ic_launcher_banner_forground.xml deleted file mode 100644 index 97131576..00000000 --- a/android/app/src/main/res/drawable/ic_launcher_banner_forground.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_background.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_background.xml deleted file mode 100644 index e7b046de..00000000 --- a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_background.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_banner.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_banner.xml deleted file mode 100644 index eef7c65c..00000000 --- a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_banner.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_banner.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_banner.png deleted file mode 100644 index d9812bd255b03216982020d23c7dfbbeb6768bfd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5869 zcmeHLXH-*5yWRm5ltU8*rHOJ76flDHE{HTKf^-b@MQR3IV(0zxQK zLkP%`E-lie1R->i&_hTdcXR*UweFvLf1kb9UVF_uvu5_1d7o#V_jzt>ZOV64>?i;L zeCB3X?EruS4FHfkJcq$Zn~KE^@N?w8nM()&C|Vx8kUYJ#LNEvku`@LSN_!+0!3LM# z6{{-%P@c@o@;U?nB9F|kUbz_wp-fmN-r7Pot+69l%Kch%ezb@GEz$8z?8d3l1jlFI zCvx9dWLP|p_w27dcI&fI#nsPm%tq0kZ?B?n-W0hZl5~@JNdBqCkJn;F)=tfuKfm(_ z3G!FVCf*#YFz?pYrA@NchP&~N+4FVS4Q9+|2x;v$^&Mhm1J7n_v5663q0#yfK(Mu* z3ow-D5Co)8LU;hdM1Tw6xdw0mTrvOzcqD`dfMy;;0PV>o4H#bfci6uh@_)$?UnGZP zKy0+4RU`Y_dvScm?%`*|Rs`Yo5FgwI=;PfjeQDh*=Q*0+!#Zol>P;>dawck&X$9^l zh}y9!5wgHSIEP@`-R-vm#DB1;Oy7%{0lOQ^HMEjz(m<$=eN)*J-GK-S`E_Rs?L)$T zz1!0>K&Uu`C*h9z1TTQM28@I)t>_$vP+@dgQ*Z$S@-h90;ZEWr4EQ97e&SazioU}o z?e&g&NcsZA%q$-PiN6jUdjWO}qPe{4MFD*|;Jh9Y57^oOcCKLeM=gliVg~gP`o|GN ziyxs-K+y`Y6PuwmI3H=M5eCX?!;V-;C$pX#0M1env-7omoIsk*{{kgNPEOnZh(dew zES=^CdUTk?Ra=h>xxdw^qqjil5M&;so+vPzZAav^C!_b|+ zIWB)$0AjHeDSPhTy8I|EwT)Y9B%_nO)3F=aeOd=`Cq>Z|_HrbbjV!slswSqT?r@}u z%l{m(m{C{GJILJXiCylFt)iuQ~Tpo!Zh)#HvN5 z2)~d9?&4xFlO1%D*;@i~qg4;FVbnuBy{8fwyKqQ|CuMTk0Aq%g%yM2I9()A%pJlf^ zULJVJ}<}1?ol-2aQjaf3F&?c zxw6AW+_QTKjnAf1<0(UD;V3EO-OO;r%Ae-Vo#y7N?vxRYQd+M@8%c&+-GN~3g8h}x z$+8|ipC{L-l~V7b?>r|=s!#{?ObXL5QJVtVCV*Z1k5X!2 zdP7X2LcB|Nwa)JF=0?hJ-!Y}?A-A$wp?_NM&c)4CF|09Vp$CH zJsi~?!!I>*vz|$=Q2Du?6zH~F%xrbNU9!gUwZyGq^kXHLw89V{5ntxYFr1`VlKM`S zjBALQiX`p#-{pAab9sWdOgqU*NIWo~$R%y50yfo66lFL~j-eHchVu~{8(Dkg7FC16 zMqA23Qvb2^FbBGPj%HX3qg4kJh+BSI*KOtCLiODnv$t@8GHr`Udmlp0)#VG9O z0`N^d(i+_`oCU{|2X zY9#L<7MUn-rGNN^(T19BHr6D~OOB^88TVX(xf{vD3fzqnSA87v1G{BVye(UJ0bj@C zXqw}8o}(+3t_g4Zj?^WrMUtC;Dbti9CmGBS2I2MeaML4((wR@WTLVHHb{0EWYOvzO zXfmQJ&EZtdgMcLF_f=be)2cL`u-~cjyla|rsxq+Rl{`p?w1s3qU>LfR}&>km+ng?pxk)fJA##h|6u9y^vpu^lzK8jLOrTe z6;nLaHCF42yFh&3YY;YOg<-eJ+)kwplatz#^>k}L75d1(2e{;I^+)l_`@vkZZ&$m5 zn|fv3lh)E(XsbCtZ#}3wU#5z(ha7!VU?PqMetK=X`&|8^*aaikN@_Wxa*U<6I>$zq z`t?6*icCY~&7}6+z67kLgMf(=C(tR-c!LFH@PDOg*p2#%qOJ5>-EeYLdl#1ZvkwVz2lV|*wKOJfj5GNe$4^VG#emPnuA$O8up7Fe)UW$ zAHKqKr7l^Vvn+<)0Bf*A&B9?lhZn3k%D88z#8LASlUBAoRkG{zd+{nQmPJe8Qv0AaQ3X#;o}p7Jb|49aIZiq z@G+IkQxv2%;M-$hg##1;=2;k!uHmAZcK%36uwY zR$QLEpu7O;UjqR`ppY=EIfg#eC4-zH#GJ9hpyVJagFFAYb3~_q&=Ohs-61nAAfJ`f zyxVqLc;h*BFp%CSC0;`S6^(NlNa|*Wib>pK3F%!L+UmLs_xmmDDO+OnXF1C|;TMOe0yZeR7p4TGbROQC)> zYrhsxiRO+Q#Tr*!8qv`@su8^6`U$)#(Htvve#R3sGjclOrdW2&*%N`i)ZM_9&GwgN z&f}%f!PC>pm<$U_8Ywh@8|e%Gh)uPKjdCAg(N%juTv6V zRL*1DFvS+>X{3~ux`J5m-BcNhOX1lzk_}2ddjWQUFm8c>`bCS;(|g|EW`#0L_h35O zH=YxUHNMMxaP>v5Lch!Kjb)fZ4v47h@k*=u{?ta3-fB~8TEOjhy8ElP6EnI;cRFA9 z|GL1iYEO<`FWgy%9LyW_WM%E~hb8^=+p{#({i^6bCG9tpI(qhcefo?QT5)btRi1t@ zOOO65u<*n>W+Ffw6EjB-9BqBtJ5wIVEu7%5el(-NXJa`P`{|+uC9D%}^b+&MVE7Wj z+6d2#Gy#sLn6_WM_0AHmmMNYtP|uLTq*wWyqPCiJb-!H zxUP<({-Wi7D5a!{2JBDP5Xu%Xb&gTuA-bwChx+^C8{4y#@n}<&TA~wpg%r_2scBVH zE>c@E-m0R-D*36AG%$DCU_-#EnAEa|;4Z>C#g1_o2VPDMqg8SiVqZ^IH80QD7_9I{#xdS+4Lb+Xem6eDsa;usoGdL?rS z%AZxB+CEd?RFJ*5zN0qr19qn z4T!T*B_<}r@pf!|nA~AvmU_*3k)i)Y+OPGNE5C8wLbi*ZCnv`={mfM~fo5i!woq|? zElMr$<+PZHH@&?Zzr#&Y7hJcD5<64iQfvMuJad8FR~}65CxN@Qu$SuqnTRAT_|lVu zswBLl$m%v-rGIm!GyUd~e~MFw&3CpA|M&Wt<_Ds9YmBbMq+f4j`@YBA>s<#zt@U3~ zqaswYc0Kc>KQ2G*Pif=l&kH&2;jx_?ZN#Kju`3-CDkQuax>b&-)A!Ce>qTIXSqz4Z z>+2AdF!bt?P1o40Qc_$ER_s1QqNG8}Lm%#6=-iR}h|L`R4L3p3-gj`b0wdf>txI)? zdB325+l{B!?Cj%tYm}j1vQU|{uNCt)L(sY;UV8Gfa(#7RxAF%<4WpsD0Z!f+e(}m9 z!NXy{`?r62i0M2(p-QkjH?$yD!=6F++->!*%0%}P3=G)68#4%gpRn}JTasDfaukle zeUE*Wu*YD9LCnXKEI1>Of&CsD26~Rq2^s@cZ?XA7uQ2pL50Jp@K(U{Z-_ww#O-W|& z8XPl(7T;RvzoYb;-reO?4A=F~X7>atA&kGT?@-f!jgLIFIF7%$2cTz*Ubb&HEuQ(B zr0n)TOpSSf!|=!~LTW zaeT{1$uS!0JOlqjTNFQO4GxRxAsJ;!JnV`%aSHQ=@FSDgPPd)YyQ^l2p{ z{+x$6DU0IrH5)>I|9m!KJ0Bl6aE}~F_2$|RJZWw05vvU`gf8H;wI_BDp&z{y$Gtj7 zLS6o8lav<@me-}K_vyn$$`!7Rx?Xopm%Td$XR{INVRMvf1G+>hXN1A8{>a1VRKWvF nE`#Inzc$^!hUEX-p=Ar`$f|G0y4|+{z{lLg`f90>=cE4uCWsS7 diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_banner.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_banner.png new file mode 100644 index 0000000000000000000000000000000000000000..f63fa5b9f48e7bb7bdf9cb4a55e8531a7ed5a49b GIT binary patch literal 5172 zcmdT{cU)6hzP|wmP*FfEaZm_B7Xee~CQ_xNQKU-Kgb*MC2_}Jr1SF#fb_78I1#F<` zP-N&WfC?%gY8*sL5D<|XdN1!{`|Qs1?Y_6~uf3noP0l^%`|IVLbJq5N<=WN1t_A>L zt+kbz9RNUb!11`GIQZK;Y3dICtqQbqp##7gxs?|JB;9}lfY>nE;V|PcZaIOf`~?=VR{%OhDao8dy#g*G|`%xD6|$zLmQ!?fz{N-YG7dB9ylnD=H-pGGqd<6 z4E!>H`!X1TSQLuMWFnbbNGi<-rGddMF>P`Akix}eF1tAf8YXxX#QV>dl6A2 ze^LO6!k~j#jUU)RUn+x2_oe<1tbZ*3g8p8Bsw*SMkJXBgK8?TSQCgfrI82>D$RjP_5U6z+wUsFG_{agu$|6iiWikhSN%Hz zk{N+PGJu0o(*om#KN&^Lh+G%~!0*Av|Q|Jr=g-EhCGk}9O zBgtehtR6|1sE77MBXo7WwGo=0+606rQO^sZ;jQbfsfX4hY3X3T)0QmV6Dh>FjyhqD#D(0{M*Q%4EvG=SORe+yA9yPm9Ub$;NPan z|F#FdvSs>`K&JmB++SgIsyBm4ppi^`KyUrabV7m3qgLwhs~xDnn)qey?`rlt9Bc$D z!yhdO{P@wtNEEPh(ZCipRB&Du0M-p!o0&MUZVa_^7;%%ywWA%VEA?L9wB)wzD|P$1 zm&HtqU9^2~uli*bzS^deq;%#?7t+u35XDk$eMZ4y>QHVI;-Nu1e|V}nzjV03O?oKB z@rmm}2rB+md_1yhhwZB0wqN#HZYIeaKMT<0^@I<8_;{X%b>U%~ zw1p+sJGYM6*>J=8H>gXY%lnzxB$umwXLDp5Un3| z+i=9^b{FzH6`y-tjsjP&*U0BmLVQg=>^Yl~(59r{COc*#bCC;y9{+9s!T$8qRZWTA zVRdnD7N)mFh(Scro}tGz1XoLL&-Wf|@SiH$YRKoHOJvZ1^6d<@!p^FaE|qqqWlA3c znk)(BUVec)I~g`GoJ)0{k?Y4%t3F#)0ud3dTQ>6ToF{tKAB;7rXI|Matom$O2}rdr zsBY1t9)X6|Y!+_yVXuKpfbzWZ!Pg%Y^`DpxS?(>hi4Mt}Q0se!Gy7sFN?pzj^>+y=!@b<=i?jzcjDX zxLDUoEo4qGk^~aX5QwOM`p)*`g@%pYM}HqtdFwHgk9T6YpzGWmJz`o|wO6R&eV==K zOK}vjL9YE7E{^4KsK3HxsV2MyO$zT|O*+Q)l@H&Vdn6sfk;hw>iHiE)JZ{mH=|@#R z)?h`$4GR7IvaLbKw`_`Z;bzLBD{qc16qPmYAo7k3AH1*lnb?MmGP>M%TtUC(IoZq% zv=s^#&F!S#Or-_9=`l>XlRj3x(AD1g@9rKc4IzxY zzp-K3;mV$tJtfhJ3*%8Y8zz9}H4xG2yZT1R^0cg(m>0QmrFnMi_?%!x-|%Vwcg4}3 z9u|*2PXN-xRMNx>^=Ws3?L?Um|rEl$D%#QL9yF=k#9bh>dDcdg(jY z>9MHrW2ysreTcw(Q6`(ItyRUxi{3% zxTmo_r{_{^04C#6yldZK{`EO**i_qZDknnk&z=sSpU~mE;f-2yXPAc;VwzeeA`U5C z?tAX*q3=EaG4|sQw%AKX{R#hUW{1kXy=?P!N8R^3Tx#rD)uR!W!R+X~biTs!NcOAE z!b+~pSjpDc()9@U^4n@c*|eb8K7&)Q1+V$7;7edjpl!vZBq7ZGamOCKaLZ#~U4B&5 z{8v*c}2)&BP{{oh%wJL$miJrn+N?2l)G9oknADmAgO|+}15=!t0JXq3;`t z-MT#QG^8Rw@;?F)(YZf9%)Rw<`;@>7?lfl!C1lVaY$!>Wq5dh`+U!zNU){_zKrV}Y zh?ax~o-Nv)rmHJ9?5d3rs;1?EMFt$bz=ed>ml~3)liWlzt9DT{`w-4gcf3GzXk)ue2NwHDY6my5ds-uYm%UAO0t9jHqmTFpD)9BiFKH-z6b*>x<-V;(>{gh<^ z*r=+DG+KO2uPqW@_gJ{_re$i`{HDH91z{AI_y049PvvS`~??}tr2pY~E<+S>BPQLFzE3J!t7hRR|!}wE^z`a4PAo zwNbl%k5STt@l@xquCzEie(m;b)(O&kt;lpp4n#z}RkbmCUu%APS254kany-&j?)rf zkwq(`l6qJjMS&DiyyA_$5t;r)<*Zqc!cW3iHq)Gk(wX@w4heyehed~kQABxwqlC{n z-9HwW8%34YS9msdA_N`1?;zuBv63?P6&#ON`&uTSMpbt_XFj-<{HE!aK+ITNL|nNH zX~`IGtL7v<7%SMhvh6@%(h$?;KXC_A%kq!cGv)L(LIKgr%d+SQ+@;2iyke!I;$Y8~ zMVkRAp94Izs6=;xo8QYnb(VVesnK=W?w9<*`3vjBq?8{Vx0pg8#?MtGt`~0gW3S=9 zKFuYVZ7;e*3>Mfgy`6}?>uBR=s45BjC<)#BVw+qV=%h`VS(DU@=Px3)%Oxs5n^($c z#-~-h!L2#fDE3F09$0Yr31vLAU~FNg?c(Hb4fgwDuO2h>kc91*ghrBv22V!j7j7JO za8*eXh`@4X&{EIyVxM$foU95UEzPjgW-g0!M?m^GUlX! z_d*dt=Fz@s#d|O?Daffn@bop>?5w9F<_g9*rAnV>13O;dvDouO*pL)=LTHsKil)tE z$c>SETn{Dl!pt27bEdT~%i4o;2OPqq?~l~>a@;Sia`iiLMl=GBhZ^J^*!5z>VIxVZ z{~{E)HPa{e|2gwI->JQicj^DA&WRpgD9WTq+x52r3bBXvFhmnVBr~hYCVIjUxMf)h z6Wl7`FPC%`hwWRZFtp&0j-Ic1$ZK`f!z`8zF)z(?3hQEw?B;gId&aw6#$-U${5gX(e*L>!ajj==CT znSTr{C`NKFXtkHy`2D%*oB>cB#Wleiav^3r|Cc=|a$iuQ0(vJ_o72dcJU3f`AWar9 z8O*2;w&hB@Ep}VCo|m<#42)5(Z5-AqC!m9c)NnAk@986V6NK|mGE$CiBMX0hh9GA3 zTg|^WwQ+IUseAfy;_x_6>q}z{jA<9<^O*OCCI-@(Qf95svyLv0Ep{&}oFfjg7x}&M zms3Yl!-lThGd+HzU7|(0AbD{8ddkE3dpyjNC|@0q5|g?%`yxyF>IwG3U}-wjy~^D zg_ItBHq%ze+nxLAqV&N~f41bHxf|^z3?`Mg)Zy1%l6>fro66mWB&S$yllKEP_j-*C zn~ImCs9P7UIhKt%lsjYQY73(oI`8f_Oh0|<6*brV7~H<;UHM1Dr=sWArLLYm&w}y0 zONka!?R=ZqEJquRY1jhcbNlr34AJbtfcfis;hZt$w79M>7Yp$Ibz`sCfBN0)CpX6l ZBg^L*qYqEq83n+{+Wdf7)?WO{{{l;xV(b6_ literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml index 90c34714..0f386765 100644 --- a/android/app/src/main/res/values/colors.xml +++ b/android/app/src/main/res/values/colors.xml @@ -1,5 +1,4 @@ - #F1F4FB - + #FFFFFFFF \ No newline at end of file From b2ebea5b423ae6c24a6df9a3c8e15b64ceaa6663 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Sun, 19 Nov 2023 12:42:33 +0330 Subject: [PATCH 50/91] Change default direct dns --- lib/domain/singbox/config_options.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/domain/singbox/config_options.dart b/lib/domain/singbox/config_options.dart index 78b614cc..6790528a 100644 --- a/lib/domain/singbox/config_options.dart +++ b/lib/domain/singbox/config_options.dart @@ -20,7 +20,7 @@ class ConfigOptions with _$ConfigOptions { @Default(IPv6Mode.disable) IPv6Mode ipv6Mode, @Default("tcp://8.8.8.8") String remoteDnsAddress, @Default(DomainStrategy.auto) DomainStrategy remoteDnsDomainStrategy, - @Default("local") String directDnsAddress, + @Default("8.8.8.8") String directDnsAddress, @Default(DomainStrategy.auto) DomainStrategy directDnsDomainStrategy, @Default(2334) int mixedPort, @Default(6450) int localDnsPort, From eca40c1abb7d9f0ba4f4bb9f0d5d889e34c19c77 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Sun, 19 Nov 2023 12:54:15 +0330 Subject: [PATCH 51/91] release: version 0.11.0 --- CHANGELOG.md | 3 ++- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99c9c832..beaca6b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## [0.11.0] - 2023-11-19 ### New Features and Improvements @@ -67,4 +67,5 @@ - Fixed localization mistakes in Russian. [PR#95](https://github.com/hiddify/hiddify-next/pull/95) by [solokot](https://github.com/solokot) - Fixed localization mistakes in Russian. [PR#74](https://github.com/hiddify/hiddify-next/pull/74) by [Elshad Guseynov](https://github.com/lifeindarkside) +[0.11.0]: https://github.com/hiddify/hiddify-next/releases/tag/0.11.0 [0.10.0]: https://github.com/hiddify/hiddify-next/releases/tag/0.10.0 diff --git a/pubspec.yaml b/pubspec.yaml index dac5bd33..f8cc88f5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: hiddify description: A Proxy Frontend. publish_to: "none" -version: 0.10.10+1010 +version: 0.11.0+1100 environment: sdk: ">=3.1.0 <4.0.0" From 38cffb16207ea2fc1d45af9c832ee89f87a27841 Mon Sep 17 00:00:00 2001 From: lymanjre <125398461+lymanjre@users.noreply.github.com> Date: Sun, 19 Nov 2023 13:26:21 +0330 Subject: [PATCH 52/91] Update release_message.md --- .github/release_message.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/release_message.md b/.github/release_message.md index 7c2fe61c..8b3540fd 100644 --- a/.github/release_message.md +++ b/.github/release_message.md @@ -18,7 +18,10 @@ **Download based on your OS:** +
+ **بر اساس سیستم عامل خود دانلود کنید:** +
From cb551ebeeff28476eeab989d30dc6358f4158859 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Sun, 19 Nov 2023 13:53:33 +0330 Subject: [PATCH 53/91] Fix documentation --- .github/release_message.md | 23 +++++++---------------- CHANGELOG.md | 4 ++-- 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/.github/release_message.md b/.github/release_message.md index 8b3540fd..4a03dfac 100644 --- a/.github/release_message.md +++ b/.github/release_message.md @@ -1,19 +1,17 @@ [![Release Downloads](https://img.shields.io/github/downloads/hiddify/hiddify-next/RELEASE_TAG/total?style=flat-square&logo=github)](https://img.shields.io/github/downloads/hiddify/hiddify-next/RELEASE_TAG/) - - - **Release Highlights:** -- -- +- +-
تغییرات اصلی به فارسی -- -- - +- +- +
**Download based on your OS:** @@ -21,6 +19,7 @@
**بر اساس سیستم عامل خود دانلود کنید:** +
@@ -48,7 +47,7 @@
- + @@ -57,18 +56,10 @@
macOS (v10.14+)
Linux
-
-
**List of all changes:** [ChangeLog](https://github.com/hiddify/hiddify-next/blob/main/CHANGELOG.md) -
- - - - - diff --git a/CHANGELOG.md b/CHANGELOG.md index beaca6b6..0f6ddc53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,5 +67,5 @@ - Fixed localization mistakes in Russian. [PR#95](https://github.com/hiddify/hiddify-next/pull/95) by [solokot](https://github.com/solokot) - Fixed localization mistakes in Russian. [PR#74](https://github.com/hiddify/hiddify-next/pull/74) by [Elshad Guseynov](https://github.com/lifeindarkside) -[0.11.0]: https://github.com/hiddify/hiddify-next/releases/tag/0.11.0 -[0.10.0]: https://github.com/hiddify/hiddify-next/releases/tag/0.10.0 +[0.11.0]: https://github.com/hiddify/hiddify-next/releases/tag/v0.11.0 +[0.10.0]: https://github.com/hiddify/hiddify-next/releases/tag/v0.10.0 From 3d4412a74d6c4e8eb70fce3e37f41e2b915420d7 Mon Sep 17 00:00:00 2001 From: jomertix <150632538+jomertix@users.noreply.github.com> Date: Sun, 19 Nov 2023 18:14:28 +0300 Subject: [PATCH 54/91] inlang: update translations --- assets/translations/strings_fa.i18n.json | 6 +++--- assets/translations/strings_ru.i18n.json | 10 +++++----- assets/translations/strings_tr.i18n.json | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/assets/translations/strings_fa.i18n.json b/assets/translations/strings_fa.i18n.json index a774f9a0..672fce99 100644 --- a/assets/translations/strings_fa.i18n.json +++ b/assets/translations/strings_fa.i18n.json @@ -270,12 +270,12 @@ "serviceNotRunning": "سرویس در حال اجرا نیست", "missingPrivilege": "نیازمند دسترسی", "missingPrivilegeMsg": "حالت VPN به دسترسی administrator نیاز دارد. یا برنامه را به عنوان سرپرست راه اندازی مجدد کنید یا حالت سرویس را تغییر دهید.", + "missingGeoAssets": "دارایی های جغرافیایی از دست رفته", + "missingGeoAssetsMsg": "دارایی های جغرافیایی گم شده اند. تغییر دارایی فعال را در نظر بگیرید یا یکی را در تنظیمات دانلود کنید.", "invalidConfigOptions": "تنظیمات کانفیگ نامعتبر", "invalidConfig": "کانفیگ غیر معتبر", "create": "در ایجاد سرویس خطایی رخ داده", - "start": "در راه‌اندازی سرویس خطایی رخ داده", - "missingGeoAssets": "دارایی های جغرافیایی از دست رفته", - "missingGeoAssetsMsg": "دارایی های جغرافیایی گم شده اند. تغییر دارایی فعال را در نظر بگیرید یا یکی را در تنظیمات دانلود کنید." + "start": "در راه‌اندازی سرویس خطایی رخ داده" }, "connectivity": { "unexpected": "خطای غیرمنتظره", diff --git a/assets/translations/strings_ru.i18n.json b/assets/translations/strings_ru.i18n.json index 4d75e461..4e160aab 100644 --- a/assets/translations/strings_ru.i18n.json +++ b/assets/translations/strings_ru.i18n.json @@ -10,11 +10,11 @@ "disable": "Отключить" }, "sort": "Сортировка", - "sortBy": "Сортировка…", + "sortBy": "Сортировка", "addToClipboard": "Копировать в буфер обмена" }, "intro": { - "termsAndPolicyCaution(rich)": "Продолжая, вы соглашаетесь с ${tap(@:about.termsAndConditions)}", + "termsAndPolicyCaution(rich)": "Продолжая, Вы соглашаетесь с ${tap(@:about.termsAndConditions)}", "start": "Начать" }, "home": { @@ -270,12 +270,12 @@ "serviceNotRunning": "Сервис не запущен", "missingPrivilege": "Отсутствие прав", "missingPrivilegeMsg": "Режим VPN требует прав администратора. Перезапустите приложение от имени администратора или измените режим работы приложения.", + "missingGeoAssets": "Отсутствующие географические ресурсы", + "missingGeoAssetsMsg": "Георесурсы отсутствуют. рассмотрите возможность изменения активного актива или загрузите выбранный в настройках.", "invalidConfigOptions": "Неправильные параметры конфигурации", "invalidConfig": "Неправильная конфигурация", "create": "Ошибка создания сервиса", - "start": "Ошибка запуска сервиса", - "missingGeoAssets": "Отсутствующие географические ресурсы", - "missingGeoAssetsMsg": "Георесурсы отсутствуют. рассмотрите возможность изменения активного актива или загрузите выбранный в настройках." + "start": "Ошибка запуска сервиса" }, "connectivity": { "unexpected": "Неожиданная ошибка", diff --git a/assets/translations/strings_tr.i18n.json b/assets/translations/strings_tr.i18n.json index 15309c26..dab9f2ae 100644 --- a/assets/translations/strings_tr.i18n.json +++ b/assets/translations/strings_tr.i18n.json @@ -270,12 +270,12 @@ "serviceNotRunning": "Servis çalışmıyor", "missingPrivilege": "Eksik Ayrıcalık", "missingPrivilegeMsg": "VPN modu yönetici ayrıcalıkları gerektirir. Uygulamayı yönetici olarak yeniden başlatın veya hizmet modunu değiştirin.", + "missingGeoAssets": "Eksik Coğrafi Varlıklar", + "missingGeoAssetsMsg": "Coğrafi öğeler eksik. Aktif varlığı değiştirmeyi veya ayarlarda seçileni indirmeyi düşünün.", "invalidConfigOptions": "Geçersiz yapılandırma seçenekleri", "invalidConfig": "Geçersiz Yapılandırma", "create": "Servis oluşturma hatası", - "start": "Servis başlatma hatası", - "missingGeoAssets": "Eksik Coğrafi Varlıklar", - "missingGeoAssetsMsg": "Coğrafi öğeler eksik. Aktif varlığı değiştirmeyi veya ayarlarda seçileni indirmeyi düşünün." + "start": "Servis başlatma hatası" }, "connectivity": { "unexpected": "Beklenmedik Hata", From f37fd80ef8d35278fb6580b59e43d85308b31697 Mon Sep 17 00:00:00 2001 From: jomertix <150632538+jomertix@users.noreply.github.com> Date: Sun, 19 Nov 2023 18:32:14 +0300 Subject: [PATCH 55/91] inlang: update translations --- assets/translations/strings_ru.i18n.json | 48 ++++++++++++------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/assets/translations/strings_ru.i18n.json b/assets/translations/strings_ru.i18n.json index 4e160aab..e8223be9 100644 --- a/assets/translations/strings_ru.i18n.json +++ b/assets/translations/strings_ru.i18n.json @@ -49,8 +49,8 @@ "noTraffic": "Нет доступного трафика" }, "sortBy": { - "lastUpdate": "Последнее обновление", - "name": "Алфавит" + "lastUpdate": "по последнему обновлению", + "name": "по названию" }, "add": { "buttonText": "Новый профиль", @@ -58,19 +58,19 @@ "fromClipboard": "Добавить из буфера обмена", "scanQr": "Сканировать QR-код", "qrScanner": { - "permissionDeniedError": "Доступ запрещён", + "permissionDeniedError": "Нет прав", "unexpectedError": "Неизвестная ошибка", "torchSemanticLabel": "Вспышка", "facingSemanticLabel": "Фронтальная камера" }, - "manually": "Ручной ввод", + "manually": "Ввести вручную", "addingProfileMsg": "Добавление профиля", - "failureMsg": "Невозможно добавить профиль" + "failureMsg": "Не удалось добавить профиль" }, "update": { "buttonTxt": "Обновить", "tooltip": "Обновить профиль", - "failureMsg": "Ошибка обновления", + "failureMsg": "Не удалось обновить профиль", "successMsg": "Профиль успешно обновлён" }, "share": { @@ -93,7 +93,7 @@ "save": { "buttonText": "Сохранить", "successMsg": "Профиль успешно сохранён", - "failureMsg": "Невозможно сохранить профиль" + "failureMsg": "Не удалось сохранить профиль" }, "detailsForm": { "nameLabel": "Имя", @@ -130,7 +130,7 @@ }, "settings": { "pageTitle": "Настройки", - "requiresRestartMsg": "Для применения перезапустите приложение.", + "requiresRestartMsg": "Чтобы применить изменения, перезапустите приложение.", "general": { "sectionTitle": "Основные", "locale": "Язык", @@ -150,7 +150,7 @@ "black": "Чёрная тема" }, "enableAnalytics": "Сбор аналитики", - "enableAnalyticsMsg": "Сбор аналитических данных и отправка отчётов о сбоях для улучшения приложения.", + "enableAnalyticsMsg": "Сбор данных аналитики и отправка отчётов о сбоях для улучшения приложения", "autoStart": "Запуск при загрузке", "silentStart": "Тихий запуск", "openWorkingDir": "Открыть рабочую папку", @@ -160,7 +160,7 @@ "advanced": { "sectionTitle": "Расширенные", "debugMode": "Режим отладки", - "debugModeMsg": "Для применения перезапустите приложение.", + "debugModeMsg": "Чтобы применить изменения, перезапустите приложение.", "memoryLimit": "Ограничение памяти" }, "network": { @@ -200,9 +200,9 @@ "prefer": "Предпочтительно", "only": "Исключительно" }, - "remoteDnsAddress": "Удалённая DNS", + "remoteDnsAddress": "Удалённый DNS", "remoteDnsDomainStrategy": "Стратегия удалённого домена DNS", - "directDnsAddress": "Прямая DNS", + "directDnsAddress": "Прямой DNS", "directDnsDomainStrategy": "Стратегия прямого домена DNS", "mixedPort": "Смешанный порт", "localDnsPort": "Локальный порт DNS", @@ -222,10 +222,10 @@ "pageTitle": "Активы маршрутизации", "version": "Версия ${version}", "fileMissing": "Файл отсутствует", - "update": "Обновлять", + "update": "Обновить", "download": "Скачать", - "failureMsg": "Не удалось обновить объект.", - "successMsg": "Объект успешно обновлен.", + "failureMsg": "Не удалось обновить объект", + "successMsg": "Объект успешно обновлен", "addRecommended": "Добавить рекомендуемые активы" } }, @@ -260,31 +260,31 @@ } }, "failure": { - "unexpected": "Неожиданная ошибка", + "unexpected": "Непредвиденная ошибка", "clash": { - "unexpected": "Неожиданная ошибка (Clash)", + "unexpected": "Непредвиденная ошибка (Clash)", "core": "Ошибка ${reason}" }, "singbox": { - "unexpected": "Неожиданная ошибка (SingBox)", + "unexpected": "Непредвиденная ошибка (SingBox)", "serviceNotRunning": "Сервис не запущен", "missingPrivilege": "Отсутствие прав", "missingPrivilegeMsg": "Режим VPN требует прав администратора. Перезапустите приложение от имени администратора или измените режим работы приложения.", - "missingGeoAssets": "Отсутствующие географические ресурсы", - "missingGeoAssetsMsg": "Георесурсы отсутствуют. рассмотрите возможность изменения активного актива или загрузите выбранный в настройках.", + "missingGeoAssets": "Отсутствуют географические ресурсы", + "missingGeoAssetsMsg": "Георесурсы отсутствуют. Изменените выбранный ресурс или загрузите собственный в настройках.", "invalidConfigOptions": "Неправильные параметры конфигурации", "invalidConfig": "Неправильная конфигурация", "create": "Ошибка создания сервиса", "start": "Ошибка запуска сервиса" }, "connectivity": { - "unexpected": "Неожиданная ошибка", + "unexpected": "Непредвиденная ошибка", "missingVpnPermission": "Отсутствует разрешение VPN", - "missingNotificationPermission": "Отсутствует разрешение на отображение уведомлений", + "missingNotificationPermission": "Отсутствует разрешение на показ уведомлений", "core": "Ошибка ядра" }, "profiles": { - "unexpected": "Неожиданная ошибка", + "unexpected": "Непредвиденная ошибка", "notFound": "Профиль не найден", "invalidConfig": "Неправильная конфигурация", "invalidUrl": "Неправильный URL" @@ -305,6 +305,6 @@ "play": { "title": "Hiddify Next (Preview)", "short_description": "Автовыбор, SSH, VLESS, Vmess, Trojan, Reality, Sing-Box, Clash, Xray, Shadowsocks", - "full_description": "Основная цель HiddifyNext — предоставить безопасный, удобный и эффективный клиент туннелирования. Он позволяет направлять весь трафик или трафик выбранного приложения на выбранный вами удалённый сервер, используя разрешение VPN–сервиса.\n\nПримечание: мы не предоставляем серверы, пользователи должны обеспечивать конфиденциальность своих действий в Интернете, используя собственный сервер или доверенные серверы.\n \nПоддерживаемые серверы:\n— Обычная ссылка на подписку V2ray/Xray\n— Ссылка на подписку Clash\n— Ссылка на подписку на Sing–Box\n\nВ чём уникальные особенности?\n — Удобство\n — Оптимизация и скорость\n — Автоматический выбор минимальной задержки\n — Отображение информации об использовании\n — Простой импорт ссылок одним щелчком мыши\n — Бесплатно и без рекламы\n — Простое переключение ссылок\n — …и много больше\n\nПоддержка:\n• Все протоколы, поддерживаемые Sing-Box\n• VLESS + xtls reality, vision\n• VMESS\n• Trojan\n• ShoadowSocks\n• Reality\n• V2ray\n• Hystria2\n• TUIC\n• SSH\n• ShadowTLS\n\n\nИсходный код доступен по адресу https://github.com/hiddify/Hiddify-Next.\nЯдро приложения основано на открытом исходном коде Sing–Box.\n\nОписание разрешений:\n— СЛУЖБА VPN: поскольку целью данного приложения является предоставление безопасного, удобного и эффективного клиента туннелирования, это разрешение необходимо, чтобы иметь возможность направлять трафик через туннель на удалённый сервер.\n— ЗАПРОС ВСЕХ ПАКЕТОВ: это разрешение позволяет включать или исключать определённые приложения для туннелирования.\n— ИНФОРМИРОВАНИЕ О ЗАВЕРШЕНИИ ЗАГРУЗКИ: это разрешение можно включить или отключить в настройках приложения, чтобы (де)активировать запуск приложения при загрузке устройства.\n— ПОСТОЯННОЕ УВЕДОМЛЕНИЕ: это разрешение необходимо, поскольку используется приоритетная служба для обеспечения непрерывной работы службы VPN.\n— Приложение не содержит рекламы. Сбор аналитики и данных о сбоях происходят только с явного согласия пользователя при первом использовании приложения." + "full_description": "Основная цель HiddifyNext — предоставить безопасный, удобный и эффективный клиент туннелирования. Он позволяет направлять весь трафик или трафик выбранного приложения на указанный Вами удалённый сервер.\nПримечание: мы не предоставляем серверы, пользователи должны сами обеспечивать конфиденциальность своих действий в Интернете, используя собственный сервер или доверенные серверы. Поддерживаются сервера с:— Обычной ссылка на подписку V2ray/Xray— Ссылкой на подписку Clash— Ссылко на подписку на Sing–Box\nВ чём уникальные особенности? — Удобство — Оптимизация и скорость — Автоматический выбор минимальной задержки — Отображение информации об использовании — Простой импорт ссылок одним щелчком мыши — Бесплатно и без рекламы — Простое переключение ссылок — …и много больше\nПоддерживаются:• Все протоколы, поддерживаемые Sing-Box• VLESS + xtls reality, vision• VMESS• Trojan• ShoadowSocks• Reality• V2ray• Hystria2• TUIC• SSH• ShadowTLS\nИсходный код доступен по адресу https://github.com/hiddify/Hiddify-Next.Ядро приложения основано на открытом исходном коде Sing–Box.\nОписание разрешений:— СЛУЖБА VPN: поскольку целью данного приложения является предоставление безопасного, удобного и эффективного клиента туннелирования, это разрешение необходимо, чтобы иметь возможность направлять трафик через туннель на удалённый сервер.— ЗАПРОС ВСЕХ ПАКЕТОВ: это разрешение позволяет добавлять или удалять определённые приложения из списка для туннелирования.— ИНФОРМИРОВАНИЕ О ЗАВЕРШЕНИИ ЗАГРУЗКИ: это разрешение можно включить или отключить в настройках приложения, чтобы (де)активировать запуск приложения при загрузке устройства.— ПОСТОЯННОЕ УВЕДОМЛЕНИЕ: это разрешение необходимо, так как используется приоритетная служба для обеспечения непрерывной работы VPN.— Приложение не содержит рекламы. Сбор аналитики и данных о сбоях происходят только с явного согласия пользователя при первом использовании приложения." } } \ No newline at end of file From e50319035fca744c168643ff6b43496eeb93ab95 Mon Sep 17 00:00:00 2001 From: jomertix <150632538+jomertix@users.noreply.github.com> Date: Sun, 19 Nov 2023 18:34:26 +0300 Subject: [PATCH 56/91] Nothing changed strings_fa.i18n.json --- assets/translations/strings_fa.i18n.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/assets/translations/strings_fa.i18n.json b/assets/translations/strings_fa.i18n.json index 672fce99..24a7dfce 100644 --- a/assets/translations/strings_fa.i18n.json +++ b/assets/translations/strings_fa.i18n.json @@ -270,12 +270,12 @@ "serviceNotRunning": "سرویس در حال اجرا نیست", "missingPrivilege": "نیازمند دسترسی", "missingPrivilegeMsg": "حالت VPN به دسترسی administrator نیاز دارد. یا برنامه را به عنوان سرپرست راه اندازی مجدد کنید یا حالت سرویس را تغییر دهید.", - "missingGeoAssets": "دارایی های جغرافیایی از دست رفته", - "missingGeoAssetsMsg": "دارایی های جغرافیایی گم شده اند. تغییر دارایی فعال را در نظر بگیرید یا یکی را در تنظیمات دانلود کنید.", "invalidConfigOptions": "تنظیمات کانفیگ نامعتبر", "invalidConfig": "کانفیگ غیر معتبر", "create": "در ایجاد سرویس خطایی رخ داده", - "start": "در راه‌اندازی سرویس خطایی رخ داده" + "start": "در راه‌اندازی سرویس خطایی رخ داده", + "missingGeoAssets": "دارایی های جغرافیایی از دست رفته", + "missingGeoAssetsMsg": "دارایی های جغرافیایی گم شده اند. تغییر دارایی فعال را در نظر بگیرید یا یکی را در تنظیمات دانلود کنید." }, "connectivity": { "unexpected": "خطای غیرمنتظره", @@ -307,4 +307,4 @@ "short_description": "Auto, SSH, VLESS, Vmess, Trojan, Reality, Sing-Box, Clash, Xray, Shadowsocks", "full_description": "هدف اصلی HiddifyNext ارائه یک کلاینت تونل زنی ایمن، کاربرپسند و کارآمد است. این به شما امکان می دهد تا با استفاده از مجوز VPN-Service، تمام ترافیک یا ترافیک برنامه انتخابی را به یک سرور راه دور مورد نظر خود هدایت کنید.\n\nتوجه: ما هیچ سروری ارائه نمی دهیم. کاربران موظفند با استفاده از سرورهای خود میزبان یا سرورهای مورد اعتماد، فعالیت‌های آنلاین خود را خصوصی نگه دارند.\n \nما از سرورهایی با موارد زیر پشتیبانی می کنیم:\n- لینک اشتراک V2ray/Xray معمولی\n- لینک اشتراک کلش\n- لینک اشتراک Sing-Box\n\nویژگی های منحصر به فرد ما چیست؟\n - کاربر پسند\n - بهینه و سریع\n - به طور خودکار LowestPing را انتخاب کنید\n - نمایش اطلاعات استفاده کاربر\n - به راحتی لینک فرعی را با یک کلیک با استفاده از دیپ لینک وارد کنید\n - رایگان و بدون تبلیغات\n - به راحتی پیوندهای فرعی کاربر را تغییر دهید\n - بیشتر و بیشتر\n\nحمایت کردن:\n- تمام پروتکل های پشتیبانی شده توسط Sing-Box\n- VLESS + xtls \n- VMESS\n- تروجان\n- ShoadowSocks\n- ریالیتی\n- V2ray\n- هیستریا 2\n- TUIC\n- SSH\n- ShadowTLS\n\n\nکد منبع در https://github.com/hiddify/Hiddify-Next وجود دارد\nهسته برنامه بر اساس sing-box منبع باز است.\n\nتوضیحات مجوز:\n- سرویس VPN: از آنجا که هدف این برنامه ارائه یک کلاینت تونل زنی ایمن، کاربر پسند و کارآمد است، ما به این مجوز نیاز داریم تا بتوانیم ترافیک را از طریق تونل به سرور راه دور هدایت کنیم.\n- QUERY ALL PACKAGES: این مجوز برای اجازه دادن به کاربران برای گنجاندن یا حذف برنامه های کاربردی خاص برای تونل زدن استفاده می شود.\n- دریافت بوت تکمیل شد: این مجوز را می توان از تنظیمات برنامه فعال یا غیرفعال کرد تا این برنامه پس از بوت شدن دستگاه فعال شود.\n- اعلان های ارسالی: این مجوز ضروری است زیرا ما از یک سرویس پیش زمینه برای اطمینان از عملکرد مداوم سرویس VPN استفاده می کنیم.\n- این برنامه بدون تبلیغات است. تجزیه و تحلیل و داده های اشکال فقط با رضایت صریح کاربر در اولین استفاده از برنامه اتفاق می افتد." } -} \ No newline at end of file +} From 0b48d0a87c39bd9f1dfc9be71d0580522edd0cee Mon Sep 17 00:00:00 2001 From: jomertix <150632538+jomertix@users.noreply.github.com> Date: Sun, 19 Nov 2023 18:35:31 +0300 Subject: [PATCH 57/91] Nothing changed strings_tr.i18n.json --- assets/translations/strings_tr.i18n.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/assets/translations/strings_tr.i18n.json b/assets/translations/strings_tr.i18n.json index dab9f2ae..f381d98e 100644 --- a/assets/translations/strings_tr.i18n.json +++ b/assets/translations/strings_tr.i18n.json @@ -270,12 +270,12 @@ "serviceNotRunning": "Servis çalışmıyor", "missingPrivilege": "Eksik Ayrıcalık", "missingPrivilegeMsg": "VPN modu yönetici ayrıcalıkları gerektirir. Uygulamayı yönetici olarak yeniden başlatın veya hizmet modunu değiştirin.", - "missingGeoAssets": "Eksik Coğrafi Varlıklar", - "missingGeoAssetsMsg": "Coğrafi öğeler eksik. Aktif varlığı değiştirmeyi veya ayarlarda seçileni indirmeyi düşünün.", "invalidConfigOptions": "Geçersiz yapılandırma seçenekleri", "invalidConfig": "Geçersiz Yapılandırma", "create": "Servis oluşturma hatası", - "start": "Servis başlatma hatası" + "start": "Servis başlatma hatası", + "missingGeoAssets": "Eksik Coğrafi Varlıklar", + "missingGeoAssetsMsg": "Coğrafi öğeler eksik. Aktif varlığı değiştirmeyi veya ayarlarda seçileni indirmeyi düşünün." }, "connectivity": { "unexpected": "Beklenmedik Hata", @@ -307,4 +307,4 @@ "short_description": "Otomatik, SSH, VLESS, Vmess, Trojan, Reality, Sing-Box, Clash, Xray, Shadowsocks", "full_description": "HiddifyNext'in temel hedefi güvenli, kullanıcı dostu ve verimli bir tünel istemcisi sağlamaktır. VPN Hizmeti iznini kullanarak tüm trafiği veya seçilen uygulama trafiğini seçtiğiniz uzak bir sunucuya yönlendirmenizi sağlar. Not: Herhangi bir sunucu sağlamıyoruz; kullanıcıların kendi barındırılan sunucularını veya güvenilir sunucularını kullanarak çevrimiçi etkinliklerinin gizli kalmasını sağlamaları gerekir. Sunucuları aşağıdakilerle destekliyoruz: - Normal V2ray/Xray Abonelik Bağlantısı - Clash Abonelik Bağlantısı - Sing-Box Abonelik Bağlantısı Benzersiz özelliklerimiz nelerdir? - Kullanıcı Dostu - Optimize Edilmiş ve Hızlı - En Düşük Ping'i otomatik olarak seçin - Kullanıcı kullanım bilgilerini gösterin - Derin bağlantı kullanarak tek tıklamayla alt bağlantıyı kolayca içe aktarın - Ücretsiz ve ADS Yok - Kullanıcı alt bağlantılarını kolayca değiştirin - giderek daha fazla Destek: - Sing-Box tarafından desteklenen tüm Protokoller - VLESS + xtls gerçeklik, vizyon - VMESS - Trojan - ShoadowSocks - Reality - V2ray - Hystria2 - TUIC - SSH - ShadowTLS Kaynak kodu https://github.com/hiddify/Hiddify-Next adresinde mevcuttur. Uygulama çekirdeği açık tabanlıdır. kaynak şarkı kutusu. İzin Açıklaması: - VPN Hizmeti: Bu uygulamanın amacı güvenli, kullanıcı dostu ve verimli bir tünel istemcisi sağlamak olduğundan, trafiği tünel aracılığıyla uzak sunucuya yönlendirebilmek için bu izne ihtiyacımız var. - TÜM PAKETLERİ SORGULAYIN: Bu izin, kullanıcıların tünelleme için belirli uygulamaları dahil etmesine veya hariç tutmasına izin vermek için kullanılır. - ALMA ÖNYÜKLEME TAMAMLANDI: Bu izin, cihaz önyüklemesi sırasında bu uygulamayı etkinleştirmek için uygulama ayarlarından etkinleştirilebilir veya devre dışı bırakılabilir. - BİLDİRİMLER SONRASI: VPN hizmetinin sürekli çalışmasını sağlamak için bir ön plan hizmeti kullandığımız için bu izin önemlidir. - Bu uygulama reklam içermez. Analitik ve kilitlenme verileri yalnızca uygulamanın ilk kullanımında kullanıcının açık rızası ile gerçekleşir." } -} \ No newline at end of file +} From e36ee9100fbe84248e1c8b959ec36494729e3c9b Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Sun, 19 Nov 2023 19:32:57 +0330 Subject: [PATCH 58/91] Fix android manifest --- android/app/src/main/AndroidManifest.xml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index ced8b599..18fd9dd8 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,7 @@ - + + @@ -115,4 +116,4 @@ android:name="flutterEmbedding" android:value="2" /> - + \ No newline at end of file From cc59f3ce4886fe6570e4e9c97e772130e83cff86 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Sun, 19 Nov 2023 19:47:13 +0330 Subject: [PATCH 59/91] release: version 0.11.1 --- CHANGELOG.md | 7 +++++++ pubspec.yaml | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f6ddc53..8ecc3bee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [0.11.1] - 2023-11-19 + +### Bug Fixes + +- Fixed Android manifest bug. + ## [0.11.0] - 2023-11-19 ### New Features and Improvements @@ -67,5 +73,6 @@ - Fixed localization mistakes in Russian. [PR#95](https://github.com/hiddify/hiddify-next/pull/95) by [solokot](https://github.com/solokot) - Fixed localization mistakes in Russian. [PR#74](https://github.com/hiddify/hiddify-next/pull/74) by [Elshad Guseynov](https://github.com/lifeindarkside) +[0.11.1]: https://github.com/hiddify/hiddify-next/releases/tag/v0.11.1 [0.11.0]: https://github.com/hiddify/hiddify-next/releases/tag/v0.11.0 [0.10.0]: https://github.com/hiddify/hiddify-next/releases/tag/v0.10.0 diff --git a/pubspec.yaml b/pubspec.yaml index f8cc88f5..fc84b711 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: hiddify description: A Proxy Frontend. publish_to: "none" -version: 0.11.0+1100 +version: 0.11.1+1101 environment: sdk: ">=3.1.0 <4.0.0" From 82bbcf34074584405780f244a83f0541d5cea903 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Mon, 20 Nov 2023 14:05:44 +0330 Subject: [PATCH 60/91] Update appcast --- appcast.xml | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/appcast.xml b/appcast.xml index 37c7f296..ad44f335 100644 --- a/appcast.xml +++ b/appcast.xml @@ -3,31 +3,32 @@ Release - Version 0.10.0 - Sat, 28 Oct 2023 12:00:00 +0000 - + Version 0.11.1 + Sun, 20 Nov 2023 22:00:00 +0000 + - Version 0.10.9 - Wed, 15 Nov 2023 19:00:00 +0000 + Version 0.11.1 + Sun, 20 Nov 2023 22:00:00 +0000 + url="https://github.com/hiddify/hiddify-next/releases/download/v0.11.1/hiddify-windows-x64-setup.zip" + sparkle:version="0.11.1" sparkle:os="windows" /> - Version 0.10.9 - Wed, 15 Nov 2023 19:00:00 +0000 + Version 0.11.1 + Sun, 20 Nov 2023 22:00:00 +0000 + url="https://github.com/hiddify/hiddify-next/releases/download/v0.11.1/hiddify-macos-universal.zip" + sparkle:version="0.11.1" sparkle:os="macos" /> - Version 0.10.9 - Wed, 15 Nov 2023 19:00:00 +0000 + Version 0.11.1 + Sun, 20 Nov 2023 22:00:00 +0000 + url="https://github.com/hiddify/hiddify-next/releases/download/v0.11.1/hiddify-linux-x64.zip" + sparkle:version="0.11.1" sparkle:os="linux" /> \ No newline at end of file From 095921cfa9b7eb954309634152656294edbba7f1 Mon Sep 17 00:00:00 2001 From: lymanjre <125398461+lymanjre@users.noreply.github.com> Date: Mon, 20 Nov 2023 19:03:26 +0330 Subject: [PATCH 61/91] Update release_message.md --- .github/release_message.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/release_message.md b/.github/release_message.md index 4a03dfac..001fed44 100644 --- a/.github/release_message.md +++ b/.github/release_message.md @@ -51,7 +51,7 @@ Linux - + From 5706024921ac9fb04e40c5f45f5180124037b3b5 Mon Sep 17 00:00:00 2001 From: lymanjre <125398461+lymanjre@users.noreply.github.com> Date: Thu, 23 Nov 2023 12:30:23 +0330 Subject: [PATCH 62/91] Update README.md --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 36963f8e..782ae47e 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,12 @@

+
+ + [![GP-Intalls](https://img.shields.io/endpoint?color=green&logo=google-play&logoColor=green&url=https%3A%2F%2Fplay.cuzi.workers.dev%2Fplay%3Fi%3Dapp.hiddify.com%26l%3DGoogle%2520Play%26m%3D%24shortinstalls&style=flat-square)](https://play.google.com/store/apps/details?id=app.hiddify.com) [![Downloads](https://img.shields.io/github/downloads/hiddify/hiddify-next/total?style=flat-square&logo=github)](https://github.com/hiddify/hiddify-next/releases/)[![Last Version](https://img.shields.io/github/release/hiddify/hiddify-next/all.svg?style=flat-square)](https://github.com/hiddify/hiddify-next/releases/)[![Last Release Date](https://img.shields.io/github/release-date/hiddify/hiddify-next.svg?style=flat-square)](https://github.com/hiddify/hiddify-next/releases/)[![commits](https://img.shields.io/github/commit-activity/m/hiddify/hiddify-next?style=flat-square)](https://github.com/hiddify/hiddify-next/) [![Youtube](https://img.shields.io/youtube/channel/views/UCxrmeMvVryNfB4XL35lXQNg?label=Youtube&style=flat-square&logo=youtube)](https://www.youtube.com/@hiddify)[![Telegram Channel](https://img.shields.io/endpoint?label=Channel&style=flat-square&url=https%3A%2F%2Ftg.sumanjay.workers.dev%2Fhiddify&color=blue)](https://telegram.dog/hiddify)[![Telegram Group](https://img.shields.io/endpoint?color=neon&label=Support%20Group&style=flat-square&url=https%3A%2F%2Ftg.sumanjay.workers.dev%2Fhiddify_board)](https://telegram.dog/hiddify_board/5) From 650766831f26ee7370c40f9b152e6041d9bb6db0 Mon Sep 17 00:00:00 2001 From: lymanjre <125398461+lymanjre@users.noreply.github.com> Date: Fri, 24 Nov 2023 23:02:46 +0330 Subject: [PATCH 63/91] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 782ae47e..30cd47d5 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@

A multi-platform auto-client based on Sing-box that serves as a universal proxy tool-chain. This app offers a wide range of capabilities, which are listed below. It also supports a large number of protocols. The app is free to use, ad-free, and open-source. It provides a secure and private tool for getting access to the free internet.

-The app is developed using [Flutter](https://flutter.dev) and [Go](https://go.dev). For more information you can read through our [Contribution Guidelines](https://github.com/hiddify/hiddify-next/blob/main/CONTRIBUTING.md) for development. +The app is developed using [Flutter](https://flutter.dev) and [Go](https://go.dev). For more information about development, you can read through our [Contribution Guidelines](https://github.com/hiddify/hiddify-next/blob/main/CONTRIBUTING.md) .
English Demo From 0a3d9f945ae5ebf30e40d786f713768c9a186059 Mon Sep 17 00:00:00 2001 From: lymanjre <125398461+lymanjre@users.noreply.github.com> Date: Fri, 24 Nov 2023 23:06:36 +0330 Subject: [PATCH 64/91] Update README_ru.md --- README_ru.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_ru.md b/README_ru.md index f8c6811a..d77f319f 100644 --- a/README_ru.md +++ b/README_ru.md @@ -17,7 +17,7 @@ ## Что такое Hiddify-Next? Многоплатформенный авто-клиент на основе [Sing-box](https://github.com/SagerNet/sing-box), который служит универсальным набором инструментов прокси. Это приложение предлагает широкий спектр возможностей, которые перечислены ниже. Он также поддерживает большое количество протоколов. Приложение бесплатное, без рекламы и с открытым исходным кодом. Он предоставляет безопасный и конфиденциальный инструмент для получения доступа к бесплатному Интернету. -Приложение разработано с использованием [Flutter](https://flutter.dev/) и [Go](https://go.dev/). Для получения дополнительной информации вы можете прочитать наши Рекомендации по участию в разработке. +Приложение разработано с использованием [Flutter](https://flutter.dev/) и [Go](https://go.dev/). Для получения дополнительной информации о разработке вы можете прочитать наши [Рекомендации по участию](CONTRIBUTING.md).
English Demo From 5b770ce8f5bd81585de10b2deb07e8e382735433 Mon Sep 17 00:00:00 2001 From: lymanjre <125398461+lymanjre@users.noreply.github.com> Date: Fri, 24 Nov 2023 23:07:01 +0330 Subject: [PATCH 65/91] Update README_ru.md --- README_ru.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_ru.md b/README_ru.md index d77f319f..7a1bf5d3 100644 --- a/README_ru.md +++ b/README_ru.md @@ -17,7 +17,7 @@ ## Что такое Hiddify-Next? Многоплатформенный авто-клиент на основе [Sing-box](https://github.com/SagerNet/sing-box), который служит универсальным набором инструментов прокси. Это приложение предлагает широкий спектр возможностей, которые перечислены ниже. Он также поддерживает большое количество протоколов. Приложение бесплатное, без рекламы и с открытым исходным кодом. Он предоставляет безопасный и конфиденциальный инструмент для получения доступа к бесплатному Интернету. -Приложение разработано с использованием [Flutter](https://flutter.dev/) и [Go](https://go.dev/). Для получения дополнительной информации о разработке вы можете прочитать наши [Рекомендации по участию](CONTRIBUTING.md). +Приложение разработано с использованием [Flutter](https://flutter.dev/) и [Go](https://go.dev/). Для получения дополнительной информации о разработке вы можете прочитать наши [Рекомендации по участию](CONTRIBUTING.md) .
English Demo From f4b22716a9ed3207d0fc7c5cdf3b5b066a87e739 Mon Sep 17 00:00:00 2001 From: lymanjre <125398461+lymanjre@users.noreply.github.com> Date: Fri, 24 Nov 2023 23:10:07 +0330 Subject: [PATCH 66/91] Update README_cn.md --- README_cn.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_cn.md b/README_cn.md index 847a9157..f92eff6c 100644 --- a/README_cn.md +++ b/README_cn.md @@ -20,7 +20,7 @@

一个基于 Sing-box 的跨平台自动客户端,用作通用代理工具链。该应用提供了广泛的功能,如下所列。它还支持大量协议。该应用免费使用、无广告且开源。它为访问自由互联网提供了一个安全且私密的工具。

-该应用是使用 [Flutter](https://flutter.dev/) 和 [Go](https://go.dev/) 开发的。 欲了解更多信息,您可以参阅我们的开发贡献指南。 +该应用是使用 [Flutter](https://flutter.dev/) 和 [Go](https://go.dev/) 开发的。 有关开发的更多信息,您可以阅读我们的[贡献指南](CONTRIBUTING.md)。
English Demo From e493b4c052ad1873d89dd05ffd924e090da3016f Mon Sep 17 00:00:00 2001 From: lymanjre <125398461+lymanjre@users.noreply.github.com> Date: Fri, 24 Nov 2023 23:10:34 +0330 Subject: [PATCH 67/91] Update README_fa.md --- README_fa.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_fa.md b/README_fa.md index a76f6de5..d8d23408 100644 --- a/README_fa.md +++ b/README_fa.md @@ -17,7 +17,7 @@ یک کلاینت خودکار مالتی‌پلتفرم مبتنی بر [سینگ‌باکس](https://github.com/SagerNet/sing-box) که به عنوان یک ابزار عمومی برای پروکسی عمل می‌کند. این برنامه طیف گسترده‌ای از قابلیت‌ها را ارائه می‌دهد که در زیر لیست شده است. همچنین از تعداد زیادی پروتکل پشتیبانی می‌کند. این برنامه رایگان، بدون آگهی و منبع باز است. این یک ابزار امن و مطمئن برای دسترسی به اینترنت رایگان فراهم می‌کند. -این برنامه با استفاده از [Flutter](https://flutter.dev/) و [Go](https://go.dev/) توسعه یافته است. برای اطلاعات بیشتر در خصوص توسعه می‌توانید [دستورالعمل‌های مشارکت](https://github.com/hiddify/hiddify-next/blob/main/CONTRIBUTING.md) در پروژه ما را مطالعه نمایید. +این برنامه با استفاده از [Flutter](https://flutter.dev/) و [Go](https://go.dev/) توسعه یافته است. برای اطلاعات بیشتر در خصوص توسعه می‌توانید [دستورالعمل‌های مشارکت](CONTRIBUTING.md) در پروژه ما را مطالعه نمایید. From 9537703513816de80529f18a3b03eca372dcc3e2 Mon Sep 17 00:00:00 2001 From: lymanjre <125398461+lymanjre@users.noreply.github.com> Date: Fri, 24 Nov 2023 23:11:01 +0330 Subject: [PATCH 68/91] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 30cd47d5..476871d9 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@

A multi-platform auto-client based on Sing-box that serves as a universal proxy tool-chain. This app offers a wide range of capabilities, which are listed below. It also supports a large number of protocols. The app is free to use, ad-free, and open-source. It provides a secure and private tool for getting access to the free internet.

-The app is developed using [Flutter](https://flutter.dev) and [Go](https://go.dev). For more information about development, you can read through our [Contribution Guidelines](https://github.com/hiddify/hiddify-next/blob/main/CONTRIBUTING.md) . +The app is developed using [Flutter](https://flutter.dev) and [Go](https://go.dev). For more information about development, you can read through our [Contribution Guidelines](CONTRIBUTING.md) .
English Demo From 2441d3a5b268b616ba43ab0bd16ad9876ef54fe9 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Sat, 25 Nov 2023 14:32:50 +0330 Subject: [PATCH 69/91] Bump sdk version --- pubspec.lock | 26 +++++++++++++------------- pubspec.yaml | 2 +- windows/flutter/CMakeLists.txt | 7 ++++++- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 6030cb3b..ede38555 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -197,10 +197,10 @@ packages: dependency: transitive description: name: collection - sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.2" + version: "1.18.0" color: dependency: transitive description: @@ -817,10 +817,10 @@ packages: dependency: "direct main" description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" mime: dependency: transitive description: @@ -1374,10 +1374,10 @@ packages: dependency: "direct main" description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" state_notifier: dependency: transitive description: @@ -1390,10 +1390,10 @@ packages: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" stream_transform: dependency: transitive description: @@ -1430,10 +1430,10 @@ packages: dependency: transitive description: name: test_api - sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.6.1" time: dependency: transitive description: @@ -1646,10 +1646,10 @@ packages: dependency: transitive description: name: web - sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 url: "https://pub.dev" source: hosted - version: "0.1.4-beta" + version: "0.3.0" web_socket_channel: dependency: "direct main" description: @@ -1715,5 +1715,5 @@ packages: source: hosted version: "2.1.1" sdks: - dart: ">=3.1.0 <4.0.0" + dart: ">=3.2.0 <4.0.0" flutter: ">=3.13.0" diff --git a/pubspec.yaml b/pubspec.yaml index fc84b711..c731a51b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: "none" version: 0.11.1+1101 environment: - sdk: ">=3.1.0 <4.0.0" + sdk: ">=3.2.0 <4.0.0" dependencies: flutter: diff --git a/windows/flutter/CMakeLists.txt b/windows/flutter/CMakeLists.txt index 930d2071..903f4899 100644 --- a/windows/flutter/CMakeLists.txt +++ b/windows/flutter/CMakeLists.txt @@ -10,6 +10,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake) # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") @@ -92,7 +97,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - windows-x64 $ + ${FLUTTER_TARGET_PLATFORM} $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS From f177de1d9825fcc0bc99365ca280fcf1a7cbc151 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Sat, 25 Nov 2023 14:36:32 +0330 Subject: [PATCH 70/91] Bump ci sdk version --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8b60afe9..d19e9e09 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -78,7 +78,7 @@ jobs: - name: Setup Flutter uses: subosito/flutter-action@v2 with: - flutter-version: '3.13.x' + flutter-version: '3.16.x' channel: 'stable' cache: true From 6040eae6ce63cf28deff455e60525961cd2afe03 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Sat, 25 Nov 2023 15:22:42 +0330 Subject: [PATCH 71/91] Update dependencies --- ios/Podfile.lock | 18 ++++----- macos/Podfile.lock | 18 ++++----- pubspec.lock | 94 +++++++++++++++++++++++----------------------- pubspec.yaml | 35 +++++++++-------- 4 files changed, 82 insertions(+), 83 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 3bd07b0d..deee190a 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -54,7 +54,7 @@ PODS: - GTMSessionFetcher/Core (< 3.0, >= 1.1) - MLImage (= 1.0.0-beta4) - MLKitCommon (~> 9.0) - - mobile_scanner (3.5.2): + - mobile_scanner (3.5.4): - Flutter - GoogleMLKit/BarcodeScanning (~> 4.0.0) - nanopb (2.30909.1): @@ -70,13 +70,13 @@ PODS: - PromisesObjC (2.3.1) - protocol_handler (0.0.1): - Flutter - - Sentry/HybridSDK (8.14.2): - - SentryPrivate (= 8.14.2) + - Sentry/HybridSDK (8.15.2): + - SentryPrivate (= 8.15.2) - sentry_flutter (0.0.1): - Flutter - FlutterMacOS - - Sentry/HybridSDK (= 8.14.2) - - SentryPrivate (8.14.2) + - Sentry/HybridSDK (= 8.15.2) + - SentryPrivate (8.15.2) - share_plus (0.0.1): - Flutter - shared_preferences_foundation (0.0.1): @@ -176,15 +176,15 @@ SPEC CHECKSUMS: MLKitBarcodeScanning: 04e264482c5f3810cb89ebc134ef6b61e67db505 MLKitCommon: c1b791c3e667091918d91bda4bba69a91011e390 MLKitVision: 8baa5f46ee3352614169b85250574fde38c36f49 - mobile_scanner: 5090a13b7a35fc1c25b0d97e18e84f271a6eb605 + mobile_scanner: e866af997a851f0d43e293621443713cb6222fe3 nanopb: d4d75c12cd1316f4a64e3c6963f879ecd4b5e0d5 package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4 protocol_handler: ae9efcf3b307f3fdffcd9d5252775b9f7d9f0d09 - Sentry: e0ea366f95ebb68f26d6030d8c22d6b2e6d23dd0 - sentry_flutter: 9a04c51c373d76ee22167bf1e65bc468c0a91fed - SentryPrivate: 949a21fa59872427edc73b524c3ec8456761d97f + Sentry: 6f5742b4c47c17c9adcf265f6f328cf4a0ed1923 + sentry_flutter: 2c309a1d4b45e59d02cfa15795705687f1e2081b + SentryPrivate: b2f7996f37781080f04a946eb4e377ff63c64195 share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5 shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 sqlite3: 6e2d4a4879854d0ec86b476bf3c3e30870bac273 diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 06bee27f..81f49c08 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -4,7 +4,7 @@ PODS: - device_info_plus (0.0.1): - FlutterMacOS - FlutterMacOS (1.0.0) - - mobile_scanner (3.5.2): + - mobile_scanner (3.5.4): - FlutterMacOS - package_info_plus (0.0.1): - FlutterMacOS @@ -15,13 +15,13 @@ PODS: - FlutterMacOS - screen_retriever (0.0.1): - FlutterMacOS - - Sentry/HybridSDK (8.14.2): - - SentryPrivate (= 8.14.2) + - Sentry/HybridSDK (8.15.2): + - SentryPrivate (= 8.15.2) - sentry_flutter (0.0.1): - Flutter - FlutterMacOS - - Sentry/HybridSDK (= 8.14.2) - - SentryPrivate (8.14.2) + - Sentry/HybridSDK (= 8.15.2) + - SentryPrivate (8.15.2) - share_plus (0.0.1): - FlutterMacOS - shared_preferences_foundation (0.0.1): @@ -108,14 +108,14 @@ SPEC CHECKSUMS: cupertino_http: afa11b9e2786b62da2671e4ddd32caf792503748 device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - mobile_scanner: 621cf2c34e1c74ae7ce5c6793638ab600723bdea + mobile_scanner: a33715761775cdbe498fd9de24d13ef142225962 package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 protocol_handler: 587e1caf6c0b92ce351ab14081968dae49cb8cc6 screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 - Sentry: e0ea366f95ebb68f26d6030d8c22d6b2e6d23dd0 - sentry_flutter: 9a04c51c373d76ee22167bf1e65bc468c0a91fed - SentryPrivate: 949a21fa59872427edc73b524c3ec8456761d97f + Sentry: 6f5742b4c47c17c9adcf265f6f328cf4a0ed1923 + sentry_flutter: 2c309a1d4b45e59d02cfa15795705687f1e2081b + SentryPrivate: b2f7996f37781080f04a946eb4e377ff63c64195 share_plus: 76dd39142738f7a68dd57b05093b5e8193f220f7 shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 sqlite3: 6e2d4a4879854d0ec86b476bf3c3e30870bac273 diff --git a/pubspec.lock b/pubspec.lock index ede38555..7d9c6045 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: transitive description: name: ansicolor - sha256: "607f8fa9786f392043f169898923e6c59b4518242b68b8862eb8a8b7d9c30b4a" + sha256: "8bf17a8ff6ea17499e40a2d2542c2f481cd7615760c6d34065cb22bfd22e6880" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.2" archive: dependency: transitive description: @@ -285,26 +285,26 @@ packages: dependency: "direct dev" description: name: custom_lint - sha256: f9a828b696930cf8307f9a3617b2b65c9b370e484dc845d69100cadb77506778 + sha256: "198ec6b8e084d22f508a76556c9afcfb71706ad3f42b083fe0ee923351a96d90" url: "https://pub.dev" source: hosted - version: "0.5.6" + version: "0.5.7" custom_lint_builder: dependency: transitive description: name: custom_lint_builder - sha256: c6f656a4d83385fc0656ae60410ed06bb382898c45627bfb8bbaa323aea97883 + sha256: dfcfa987d2bd9d0ba751ef4bdef0f6c1aa0062f2a67fe716fd5f3f8b709d6418 url: "https://pub.dev" source: hosted - version: "0.5.6" + version: "0.5.7" custom_lint_core: dependency: transitive description: name: custom_lint_core - sha256: e20a67737adcf0cf2465e734dd624af535add11f9edd1f2d444909b5b0749650 + sha256: f84c3fe2f27ef3b8831953e477e59d4a29c2952623f9eac450d7b40d9cdd94cc url: "https://pub.dev" source: hosted - version: "0.5.6" + version: "0.5.7" dart_style: dependency: transitive description: @@ -349,10 +349,10 @@ packages: dependency: "direct main" description: name: dio - sha256: "417e2a6f9d83ab396ec38ff4ea5da6c254da71e4db765ad737a42af6930140b7" + sha256: "01870acd87986f768e0c09cc4d7a19a59d814af7b34cbeb0b437d2c33bdfea4c" url: "https://pub.dev" source: hosted - version: "5.3.3" + version: "5.3.4" drift: dependency: "direct main" description: @@ -365,10 +365,10 @@ packages: dependency: "direct dev" description: name: drift_dev - sha256: f79281f13411abe4229d6b57956202f047cc49b2c4e0d26ffae7273d6e5e97b1 + sha256: "369d2769d84e0c2d2cb4cd420e4fdb4f975852c83ebb934733c3b382c62961cd" url: "https://pub.dev" source: hosted - version: "2.13.1" + version: "2.13.2" equatable: dependency: transitive description: @@ -450,10 +450,10 @@ packages: dependency: "direct main" description: name: flutter_animate - sha256: "62f346340a96192070e31e3f2a1bd30a28530f1fe8be978821e06cd56b74d6d2" + sha256: "1dbc1aabfb8ec1e9d9feed2b675c21fb6b0a11f99be53ec3bc0f1901af6a8eb7" url: "https://pub.dev" source: hosted - version: "4.2.0+1" + version: "4.3.0" flutter_gen_core: dependency: transitive description: @@ -503,18 +503,18 @@ packages: dependency: "direct main" description: name: flutter_native_splash - sha256: d93394f22f73e810bda59e11ebe83329c5511d6460b6b7509c4e1f3c92d6d625 + sha256: c4d899312b36e7454bedfd0a4740275837b99e532d81c8477579d8183db1de6c url: "https://pub.dev" source: hosted - version: "2.3.5" + version: "2.3.6" flutter_riverpod: dependency: transitive description: name: flutter_riverpod - sha256: "305203d1578f6857675f9730568548b03900ce53afd319f4aa9d2fa943334dbe" + sha256: d261b0f2461e0595b96f92ed807841eb72cea84a6b12b8fd0c76e5ed803e7921 url: "https://pub.dev" source: hosted - version: "2.4.5" + version: "2.4.8" flutter_svg: dependency: "direct main" description: @@ -593,10 +593,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: "5098760d7478aabfe682a462bf121d61bc5dbe5df5aac8dad733564a0aee33bc" + sha256: c247a4f76071c3b97bb5ae8912968870d5565644801c5e09f3bc961b4d874895 url: "https://pub.dev" source: hosted - version: "12.1.0" + version: "12.1.1" go_router_builder: dependency: "direct dev" description: @@ -617,10 +617,10 @@ packages: dependency: "direct main" description: name: hooks_riverpod - sha256: "2827136ecc0c2abffc3a58e575db6d5b84d159977fa1edc223c97bf566aa8c73" + sha256: b271e06606e718cf8185db9a792d1af00e2049e7bf5da09583654e020c00fbaa url: "https://pub.dev" source: hosted - version: "2.4.5" + version: "2.4.8" hotreloader: dependency: transitive description: @@ -833,18 +833,18 @@ packages: dependency: "direct main" description: name: mobile_scanner - sha256: cf978740676ba5b0c17399baf117984b31190bb7a6eaa43e51229ed46abc42ee + sha256: c9ed2bb1bbf4b98394bc4a8477984c8ba2b55f706d634bf27cd9dd1c2e9b3a23 url: "https://pub.dev" source: hosted - version: "3.5.2" + version: "3.5.4" native_dio_adapter: dependency: "direct main" description: name: native_dio_adapter - sha256: "1967cabe3e9ea68ea5ad6da7a0ed25fa75cf335ec6b92cdf6f32185efa93364b" + sha256: a9af4430278dd5a86ded4c5857e58d80e5bc81097e6cc8a176725ac8a0ab39a6 url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" neat_periodic_task: dependency: "direct main" description: @@ -1009,10 +1009,10 @@ packages: dependency: "direct main" description: name: posix - sha256: "3ad26924254fd2354b0e2b95fc8b45ac392ad87434f8e64807b3a1ac077f2256" + sha256: a0117dc2167805aa9125b82eee515cc891819bac2f538c83646d355b16f58b9a url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "6.0.1" process: dependency: transitive description: @@ -1097,42 +1097,42 @@ packages: dependency: transitive description: name: riverpod - sha256: "2e84315036e64c59affaff7443dea51247bc2fe704461a32f26a27986fb63d55" + sha256: "08451ddbaad6eae73e2422d8109775885623340d721c6637b8719c9f4b478848" url: "https://pub.dev" source: hosted - version: "2.4.5" + version: "2.4.8" riverpod_analyzer_utils: dependency: transitive description: name: riverpod_analyzer_utils - sha256: "22a089135785f27e601075023f93c6622c43ef28c3ba1bef303cfbc314028e64" + sha256: d4dabc35358413bf4611fcb6abb46308a67c4ef4cd5e69fd3367b11925c59f57 url: "https://pub.dev" source: hosted - version: "0.4.3" + version: "0.5.0" riverpod_annotation: dependency: "direct main" description: name: riverpod_annotation - sha256: "9330309e4400f40e39a2a1d1c340e775d0fd23451cf2dd2286e03c7896fd2bd5" + sha256: "02c9bced96ed3ed8d9970820d1ce7b16600955bc01aa8b2276f09dd3d9d29ed9" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.2" riverpod_generator: dependency: "direct dev" description: name: riverpod_generator - sha256: "0a1c8eeb3dba2ce704eb1a4c3b8043716d52bedaaaa5b2725e0bde67ca38a46e" + sha256: "94b6c49bba879729611d690d434796e3b4e7c72a27e88b482b92c505e90f90d9" url: "https://pub.dev" source: hosted - version: "2.3.5" + version: "2.3.8" riverpod_lint: dependency: "direct dev" description: name: riverpod_lint - sha256: "97342543496f07c5172e0d1ce98c29499d8245776c94bfc837ceea5525c01ade" + sha256: "6fc64ae102ba39b0889b7aa7f4ef6c5a8f71a2ad215b90c787f319a9407a128b" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.3.6" rxdart: dependency: "direct main" description: @@ -1153,10 +1153,10 @@ packages: dependency: transitive description: name: sentry - sha256: "9cfd325611ab54b57d5e26957466823f05bea9d6cfcc8d48f11817b8bcedf0d1" + sha256: e7ded42974bac5f69e4ca4ddc57d30499dd79381838f24b7e8fd9aa4139e7b79 url: "https://pub.dev" source: hosted - version: "7.12.0" + version: "7.13.2" sentry_dart_plugin: dependency: "direct main" description: @@ -1169,10 +1169,10 @@ packages: dependency: "direct main" description: name: sentry_flutter - sha256: "0cd7d622cb63c94fd1b2f87ab508e158b950bd281e2a80f327ebf73bb217eaf3" + sha256: d6f55ec7a1f681784165021f749007712a72ff57eadf91e963331b6ae326f089 url: "https://pub.dev" source: hosted - version: "7.12.0" + version: "7.13.2" share_plus: dependency: "direct main" description: @@ -1371,7 +1371,7 @@ packages: source: hosted version: "0.32.0" stack_trace: - dependency: "direct main" + dependency: transitive description: name: stack_trace sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" @@ -1502,10 +1502,10 @@ packages: dependency: "direct main" description: name: upgrader - sha256: "889c1ece7af143df32e8ee2126f2ef17b2ab6bb7ed8fc3b1b022d7faa4fdab20" + sha256: "204c5d5d5ac1c09fa956422dee94d7f44f1b612750d29028b7ec7a43b474c135" url: "https://pub.dev" source: hosted - version: "8.2.0" + version: "8.3.0" url_launcher: dependency: "direct main" description: @@ -1662,10 +1662,10 @@ packages: dependency: "direct main" description: name: win32 - sha256: "350a11abd2d1d97e0cc7a28a81b781c08002aa2864d9e3f192ca0ffa18b06ed3" + sha256: "7c99c0e1e2fa190b48d25c81ca5e42036d5cac81430ef249027d97b0935c553f" url: "https://pub.dev" source: hosted - version: "5.0.9" + version: "5.1.0" win32_registry: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c731a51b..3906824d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,20 +20,20 @@ dependencies: fpdart: ^1.1.0 freezed_annotation: ^2.4.1 json_annotation: ^4.8.1 - hooks_riverpod: ^2.4.5 + hooks_riverpod: ^2.4.8 flutter_hooks: ^0.20.3 - riverpod_annotation: ^2.3.0 + riverpod_annotation: ^2.3.2 rxdart: ^0.27.7 drift: ^2.13.1 sqlite3_flutter_libs: ^0.5.18 shared_preferences: ^2.2.2 - dio: ^5.3.3 + dio: ^5.3.4 web_socket_channel: ^2.4.0 ffi: ^2.1.0 path_provider: ^2.1.1 - mobile_scanner: ^3.5.2 + mobile_scanner: ^3.5.4 protocol_handler: ^0.1.5 - flutter_native_splash: ^2.3.5 + flutter_native_splash: ^2.3.6 share_plus: ^7.2.1 window_manager: ^0.3.7 tray_manager: ^0.2.0 @@ -41,14 +41,13 @@ dependencies: url_launcher: ^6.2.1 vclibs: ^0.1.0 launch_at_startup: ^0.2.2 - sentry_flutter: ^7.12.0 + sentry_flutter: ^7.13.2 sentry_dart_plugin: ^1.6.3 combine: ^0.5.6 path: ^1.8.3 loggy: ^2.0.3 flutter_loggy: ^2.0.2 - meta: ^1.9.1 - stack_trace: ^1.11.0 + meta: ^1.10.0 dartx: ^1.2.0 uuid: ^4.2.1 tint: ^2.0.1 @@ -56,22 +55,22 @@ dependencies: neat_periodic_task: ^2.0.1 retry: ^3.1.2 watcher: ^1.1.0 - go_router: ^12.1.0 + go_router: ^12.1.1 flex_color_scheme: ^7.3.1 - flutter_animate: ^4.2.0+1 + flutter_animate: ^4.3.0 flutter_svg: ^2.0.9 gap: ^3.0.1 percent_indicator: ^4.2.3 sliver_tools: ^0.2.12 flutter_adaptive_scaffold: ^0.1.7+1 humanizer: ^2.2.0 - upgrader: ^8.2.0 + upgrader: ^8.3.0 toastification: ^1.1.0 version: ^3.0.2 - posix: ^5.0.0 - win32: ^5.0.9 + posix: ^6.0.1 + win32: ^5.1.0 qr_flutter: ^4.1.0 - native_dio_adapter: ^1.1.0 + native_dio_adapter: ^1.1.1 dev_dependencies: flutter_test: @@ -80,14 +79,14 @@ dev_dependencies: build_runner: ^2.4.6 json_serializable: ^6.7.1 freezed: ^2.4.5 - riverpod_generator: ^2.3.5 - drift_dev: ^2.13.1 + riverpod_generator: ^2.3.8 + drift_dev: ^2.13.2 ffigen: ^8.0.2 slang_build_runner: ^3.25.0 flutter_gen_runner: ^5.3.2 go_router_builder: ^2.3.4 - custom_lint: ^0.5.6 - riverpod_lint: ^2.3.3 + custom_lint: ^0.5.7 + riverpod_lint: ^2.3.6 icons_launcher: ^2.1.5 flutter: From e2d9d5e53ec7b318dc02b1ad3ee6aeefbe6a4b5c Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Sat, 25 Nov 2023 22:00:40 +0330 Subject: [PATCH 72/91] Refactor geo assets --- build.yaml | 2 - lib/bootstrap.dart | 2 + lib/core/router/routes/desktop_routes.dart | 4 +- lib/core/router/routes/mobile_routes.dart | 4 +- lib/data/data_providers.dart | 29 +-- lib/data/local/dao/dao.dart | 2 - lib/data/local/dao/geo_assets_dao.dart | 46 ---- lib/data/local/data_mappers.dart | 27 -- lib/data/local/database.dart | 13 +- lib/data/local/tables.dart | 2 +- lib/data/repository/core_facade_impl.dart | 7 +- .../repository/geo_assets_repository.dart | 168 ------------- .../repository/profiles_repository_impl.dart | 2 +- lib/domain/rules/geo_asset.dart | 69 ------ lib/domain/rules/geo_assets_repository.dart | 16 -- .../geo_asset/data/geo_asset_data_mapper.dart | 31 +++ .../data/geo_asset_data_providers.dart | 31 +++ .../geo_asset/data/geo_asset_data_source.dart | 59 +++++ .../data/geo_asset_path_resolver.dart | 31 +++ .../geo_asset/data/geo_asset_repository.dart | 232 ++++++++++++++++++ .../geo_asset/model/default_geo_assets.dart | 39 +++ .../geo_asset/model/geo_asset_entity.dart | 27 ++ .../geo_asset/model}/geo_asset_failure.dart | 2 +- .../notifier/geo_asset_notifier.dart | 33 +++ .../geo_assets_overview_notifier.dart | 43 ++++ .../overview/geo_assets_overview_page.dart} | 19 +- .../widget}/geo_asset_tile.dart | 65 +++-- .../geo_assets/geo_assets_notifier.dart | 49 ---- lib/services/files_editor_service.dart | 47 ---- 29 files changed, 594 insertions(+), 507 deletions(-) delete mode 100644 lib/data/local/dao/dao.dart delete mode 100644 lib/data/local/dao/geo_assets_dao.dart delete mode 100644 lib/data/repository/geo_assets_repository.dart delete mode 100644 lib/domain/rules/geo_asset.dart delete mode 100644 lib/domain/rules/geo_assets_repository.dart create mode 100644 lib/features/geo_asset/data/geo_asset_data_mapper.dart create mode 100644 lib/features/geo_asset/data/geo_asset_data_providers.dart create mode 100644 lib/features/geo_asset/data/geo_asset_data_source.dart create mode 100644 lib/features/geo_asset/data/geo_asset_path_resolver.dart create mode 100644 lib/features/geo_asset/data/geo_asset_repository.dart create mode 100644 lib/features/geo_asset/model/default_geo_assets.dart create mode 100644 lib/features/geo_asset/model/geo_asset_entity.dart rename lib/{domain/rules => features/geo_asset/model}/geo_asset_failure.dart (97%) create mode 100644 lib/features/geo_asset/notifier/geo_asset_notifier.dart create mode 100644 lib/features/geo_asset/overview/geo_assets_overview_notifier.dart rename lib/features/{settings/geo_assets/geo_assets_page.dart => geo_asset/overview/geo_assets_overview_page.dart} (65%) rename lib/features/{settings/geo_assets => geo_asset/widget}/geo_asset_tile.dart (63%) delete mode 100644 lib/features/settings/geo_assets/geo_assets_notifier.dart diff --git a/build.yaml b/build.yaml index b33aa2b2..c34cb667 100644 --- a/build.yaml +++ b/build.yaml @@ -2,8 +2,6 @@ targets: $default: builders: drift_dev: - generate_for: - - "lib/data/local/**" options: store_date_time_values_as_text: true slang_build_runner: diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index 4cfa3d18..7f4c42fe 100644 --- a/lib/bootstrap.dart +++ b/lib/bootstrap.dart @@ -12,6 +12,7 @@ import 'package:hiddify/data/repository/app_repository_impl.dart'; import 'package:hiddify/domain/environment.dart'; import 'package:hiddify/features/common/active_profile/active_profile_notifier.dart'; import 'package:hiddify/features/common/window/window_controller.dart'; +import 'package:hiddify/features/geo_asset/data/geo_asset_data_providers.dart'; import 'package:hiddify/features/system_tray/system_tray_controller.dart'; import 'package:hiddify/services/auto_start_service.dart'; import 'package:hiddify/services/deep_link_service.dart'; @@ -86,6 +87,7 @@ Future _lazyBootstrap( final filesEditor = container.read(filesEditorServiceProvider); await filesEditor.init(); + await container.read(geoAssetRepositoryProvider.future); initLoggers(container.read, debug); _logger.info(container.read(appInfoProvider).format()); diff --git a/lib/core/router/routes/desktop_routes.dart b/lib/core/router/routes/desktop_routes.dart index 186f7ddd..3830b7c3 100644 --- a/lib/core/router/routes/desktop_routes.dart +++ b/lib/core/router/routes/desktop_routes.dart @@ -3,8 +3,8 @@ import 'package:go_router/go_router.dart'; import 'package:hiddify/core/router/routes/shared_routes.dart'; import 'package:hiddify/features/about/view/view.dart'; import 'package:hiddify/features/common/adaptive_root_scaffold.dart'; +import 'package:hiddify/features/geo_asset/overview/geo_assets_overview_page.dart'; import 'package:hiddify/features/logs/view/view.dart'; -import 'package:hiddify/features/settings/geo_assets/geo_assets_page.dart'; import 'package:hiddify/features/settings/view/view.dart'; part 'desktop_routes.g.dart'; @@ -117,7 +117,7 @@ class GeoAssetsRoute extends GoRouteData { return const MaterialPage( fullscreenDialog: true, name: name, - child: GeoAssetsPage(), + child: GeoAssetsOverviewPage(), ); } } diff --git a/lib/core/router/routes/mobile_routes.dart b/lib/core/router/routes/mobile_routes.dart index 79c28024..5dcf0802 100644 --- a/lib/core/router/routes/mobile_routes.dart +++ b/lib/core/router/routes/mobile_routes.dart @@ -4,8 +4,8 @@ import 'package:hiddify/core/router/app_router.dart'; import 'package:hiddify/core/router/routes/shared_routes.dart'; import 'package:hiddify/features/about/view/view.dart'; import 'package:hiddify/features/common/adaptive_root_scaffold.dart'; +import 'package:hiddify/features/geo_asset/overview/geo_assets_overview_page.dart'; import 'package:hiddify/features/logs/view/view.dart'; -import 'package:hiddify/features/settings/geo_assets/geo_assets_page.dart'; import 'package:hiddify/features/settings/view/view.dart'; part 'mobile_routes.g.dart'; @@ -155,7 +155,7 @@ class GeoAssetsRoute extends GoRouteData { return const MaterialPage( fullscreenDialog: true, name: name, - child: GeoAssetsPage(), + child: GeoAssetsOverviewPage(), ); } } diff --git a/lib/data/data_providers.dart b/lib/data/data_providers.dart index f1c47452..8ffd3327 100644 --- a/lib/data/data_providers.dart +++ b/lib/data/data_providers.dart @@ -4,18 +4,17 @@ import 'package:dio/dio.dart'; import 'package:hiddify/core/core_providers.dart'; import 'package:hiddify/core/prefs/general_prefs.dart'; import 'package:hiddify/data/api/clash_api.dart'; -import 'package:hiddify/data/local/dao/dao.dart'; +import 'package:hiddify/data/local/dao/profiles_dao.dart'; import 'package:hiddify/data/local/database.dart'; import 'package:hiddify/data/repository/app_repository_impl.dart'; import 'package:hiddify/data/repository/config_options_store.dart'; -import 'package:hiddify/data/repository/geo_assets_repository.dart'; import 'package:hiddify/data/repository/repository.dart'; import 'package:hiddify/domain/app/app.dart'; import 'package:hiddify/domain/constants.dart'; import 'package:hiddify/domain/core_facade.dart'; import 'package:hiddify/domain/profiles/profiles.dart'; -import 'package:hiddify/domain/rules/geo_assets_repository.dart'; import 'package:hiddify/domain/singbox/singbox.dart'; +import 'package:hiddify/features/geo_asset/data/geo_asset_data_providers.dart'; import 'package:hiddify/services/service_providers.dart'; import 'package:native_dio_adapter/native_dio_adapter.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -70,38 +69,25 @@ AppRepository appRepository(AppRepositoryRef ref) => @Riverpod(keepAlive: true) ClashApi clashApi(ClashApiRef ref) => ClashApi(Defaults.clashApiPort); -@Riverpod(keepAlive: true) -GeoAssetsDao geoAssetsDao(GeoAssetsDaoRef ref) => GeoAssetsDao( - ref.watch(appDatabaseProvider), - ); - -@Riverpod(keepAlive: true) -GeoAssetsRepository geoAssetsRepository(GeoAssetsRepositoryRef ref) { - return GeoAssetsRepositoryImpl( - geoAssetsDao: ref.watch(geoAssetsDaoProvider), - dio: ref.watch(dioProvider), - filesEditor: ref.watch(filesEditorServiceProvider), - ); -} - @riverpod Future configOptions(ConfigOptionsRef ref) async { final geoAssets = await ref - .watch(geoAssetsRepositoryProvider) + .watch(geoAssetRepositoryProvider) + .requireValue .getActivePair() .getOrElse((l) => throw l) .run(); - final filesEditor = ref.watch(filesEditorServiceProvider); + final geoAssetsPathResolver = ref.watch(geoAssetPathResolverProvider); final serviceMode = ref.watch(serviceModeStoreProvider); return ref.watch(configPreferencesProvider).copyWith( enableTun: serviceMode == ServiceMode.tun, setSystemProxy: serviceMode == ServiceMode.systemProxy, - geoipPath: filesEditor.geoAssetRelativePath( + geoipPath: geoAssetsPathResolver.relativePath( geoAssets.geoip.providerName, geoAssets.geoip.fileName, ), - geositePath: filesEditor.geoAssetRelativePath( + geositePath: geoAssetsPathResolver.relativePath( geoAssets.geosite.providerName, geoAssets.geosite.fileName, ), @@ -112,6 +98,7 @@ Future configOptions(ConfigOptionsRef ref) async { CoreFacade coreFacade(CoreFacadeRef ref) => CoreFacadeImpl( ref.watch(singboxServiceProvider), ref.watch(filesEditorServiceProvider), + ref.watch(geoAssetPathResolverProvider), ref.watch(platformServicesProvider), ref.watch(clashApiProvider), ref.read(debugModeNotifierProvider), diff --git a/lib/data/local/dao/dao.dart b/lib/data/local/dao/dao.dart deleted file mode 100644 index e267403f..00000000 --- a/lib/data/local/dao/dao.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'geo_assets_dao.dart'; -export 'profiles_dao.dart'; diff --git a/lib/data/local/dao/geo_assets_dao.dart b/lib/data/local/dao/geo_assets_dao.dart deleted file mode 100644 index 25ef879a..00000000 --- a/lib/data/local/dao/geo_assets_dao.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:drift/drift.dart'; -import 'package:hiddify/data/local/data_mappers.dart'; -import 'package:hiddify/data/local/database.dart'; -import 'package:hiddify/data/local/tables.dart'; -import 'package:hiddify/domain/rules/geo_asset.dart'; -import 'package:hiddify/utils/custom_loggers.dart'; - -part 'geo_assets_dao.g.dart'; - -@DriftAccessor(tables: [GeoAssetEntries]) -class GeoAssetsDao extends DatabaseAccessor - with _$GeoAssetsDaoMixin, InfraLogger { - GeoAssetsDao(super.db); - - Future add(GeoAsset geoAsset) async { - await into(geoAssetEntries).insert(geoAsset.toCompanion()); - } - - Future getActive(GeoAssetType type) async { - return (geoAssetEntries.select() - ..where((tbl) => tbl.active.equals(true)) - ..where((tbl) => tbl.type.equalsValue(type)) - ..limit(1)) - .map(GeoAssetMapper.fromEntry) - .getSingleOrNull(); - } - - Stream> watchAll() { - return geoAssetEntries.select().map(GeoAssetMapper.fromEntry).watch(); - } - - Future edit(GeoAsset patch) async { - await transaction( - () async { - if (patch.active) { - await (update(geoAssetEntries) - ..where((tbl) => tbl.active.equals(true)) - ..where((tbl) => tbl.type.equalsValue(patch.type))) - .write(const GeoAssetEntriesCompanion(active: Value(false))); - } - await (update(geoAssetEntries)..where((tbl) => tbl.id.equals(patch.id))) - .write(patch.toCompanion()); - }, - ); - } -} diff --git a/lib/data/local/data_mappers.dart b/lib/data/local/data_mappers.dart index 571afd89..0646a749 100644 --- a/lib/data/local/data_mappers.dart +++ b/lib/data/local/data_mappers.dart @@ -1,7 +1,6 @@ import 'package:drift/drift.dart'; import 'package:hiddify/data/local/database.dart'; import 'package:hiddify/domain/profiles/profiles.dart'; -import 'package:hiddify/domain/rules/geo_asset.dart'; extension ProfileMapper on Profile { ProfileEntriesCompanion toCompanion() { @@ -72,29 +71,3 @@ extension ProfileMapper on Profile { }; } } - -extension GeoAssetMapper on GeoAsset { - GeoAssetEntriesCompanion toCompanion() { - return GeoAssetEntriesCompanion.insert( - id: id, - type: type, - active: active, - name: name, - providerName: providerName, - version: Value(version), - lastCheck: Value(lastCheck), - ); - } - - static GeoAsset fromEntry(GeoAssetEntry e) { - return GeoAsset( - id: e.id, - name: e.name, - type: e.type, - active: e.active, - providerName: e.providerName, - version: e.version, - lastCheck: e.lastCheck, - ); - } -} diff --git a/lib/data/local/database.dart b/lib/data/local/database.dart index 2800f77e..f1ecaea9 100644 --- a/lib/data/local/database.dart +++ b/lib/data/local/database.dart @@ -2,22 +2,19 @@ import 'dart:io'; import 'package:drift/drift.dart'; import 'package:drift/native.dart'; -import 'package:hiddify/data/local/dao/dao.dart'; -import 'package:hiddify/data/local/data_mappers.dart'; import 'package:hiddify/data/local/schema_versions.dart'; import 'package:hiddify/data/local/tables.dart'; import 'package:hiddify/data/local/type_converters.dart'; import 'package:hiddify/domain/profiles/profiles.dart'; -import 'package:hiddify/domain/rules/geo_asset.dart'; +import 'package:hiddify/features/geo_asset/data/geo_asset_data_mapper.dart'; +import 'package:hiddify/features/geo_asset/model/default_geo_assets.dart'; +import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart'; import 'package:hiddify/services/files_editor_service.dart'; import 'package:path/path.dart' as p; part 'database.g.dart'; -@DriftDatabase( - tables: [ProfileEntries, GeoAssetEntries], - daos: [ProfilesDao, GeoAssetsDao], -) +@DriftDatabase(tables: [ProfileEntries, GeoAssetEntries]) class AppDatabase extends _$AppDatabase { AppDatabase({required QueryExecutor connection}) : super(connection); @@ -57,7 +54,7 @@ class AppDatabase extends _$AppDatabase { Future _prePopulateGeoAssets() async { await transaction(() async { - final geoAssets = defaultGeoAssets.map((e) => e.toCompanion()); + final geoAssets = defaultGeoAssets.map((e) => e.toEntry()); for (final geoAsset in geoAssets) { await into(geoAssetEntries).insert(geoAsset); } diff --git a/lib/data/local/tables.dart b/lib/data/local/tables.dart index 18fe4cdc..f8a09291 100644 --- a/lib/data/local/tables.dart +++ b/lib/data/local/tables.dart @@ -1,7 +1,7 @@ import 'package:drift/drift.dart'; import 'package:hiddify/data/local/type_converters.dart'; import 'package:hiddify/domain/profiles/profiles.dart'; -import 'package:hiddify/domain/rules/geo_asset.dart'; +import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart'; @DataClassName('ProfileEntry') class ProfileEntries extends Table { diff --git a/lib/data/repository/core_facade_impl.dart b/lib/data/repository/core_facade_impl.dart index d254726c..15bdde09 100644 --- a/lib/data/repository/core_facade_impl.dart +++ b/lib/data/repository/core_facade_impl.dart @@ -10,6 +10,7 @@ import 'package:hiddify/domain/constants.dart'; import 'package:hiddify/domain/core_facade.dart'; import 'package:hiddify/domain/core_service_failure.dart'; import 'package:hiddify/domain/singbox/singbox.dart'; +import 'package:hiddify/features/geo_asset/data/geo_asset_path_resolver.dart'; import 'package:hiddify/services/files_editor_service.dart'; import 'package:hiddify/services/platform_services.dart'; import 'package:hiddify/services/singbox/singbox_service.dart'; @@ -19,6 +20,7 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { CoreFacadeImpl( this.singbox, this.filesEditor, + this.geoAssetPathResolver, this.platformServices, this.clash, this.debug, @@ -27,6 +29,7 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { final SingboxService singbox; final FilesEditorService filesEditor; + final GeoAssetPathResolver geoAssetPathResolver; final PlatformServices platformServices; final ClashApi clash; final bool debug; @@ -38,8 +41,8 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { return exceptionHandler( () async { final options = await configOptions(); - final geoip = filesEditor.resolveGeoAssetPath(options.geoipPath); - final geosite = filesEditor.resolveGeoAssetPath(options.geositePath); + final geoip = geoAssetPathResolver.resolvePath(options.geoipPath); + final geosite = geoAssetPathResolver.resolvePath(options.geositePath); if (!await File(geoip).exists() || !await File(geosite).exists()) { return left(const CoreMissingGeoAssets()); } diff --git a/lib/data/repository/geo_assets_repository.dart b/lib/data/repository/geo_assets_repository.dart deleted file mode 100644 index 57c7c638..00000000 --- a/lib/data/repository/geo_assets_repository.dart +++ /dev/null @@ -1,168 +0,0 @@ -import 'dart:io'; - -import 'package:dartx/dartx_io.dart'; -import 'package:dio/dio.dart'; -import 'package:fpdart/fpdart.dart'; -import 'package:hiddify/data/local/dao/dao.dart'; -import 'package:hiddify/data/repository/exception_handlers.dart'; -import 'package:hiddify/domain/rules/geo_asset.dart'; -import 'package:hiddify/domain/rules/geo_asset_failure.dart'; -import 'package:hiddify/domain/rules/geo_assets_repository.dart'; -import 'package:hiddify/services/files_editor_service.dart'; -import 'package:hiddify/utils/custom_loggers.dart'; -import 'package:rxdart/rxdart.dart'; -import 'package:watcher/watcher.dart'; - -class GeoAssetsRepositoryImpl - with ExceptionHandler, InfraLogger - implements GeoAssetsRepository { - GeoAssetsRepositoryImpl({ - required this.geoAssetsDao, - required this.dio, - required this.filesEditor, - }); - - final GeoAssetsDao geoAssetsDao; - final Dio dio; - final FilesEditorService filesEditor; - - @override - TaskEither - getActivePair() { - return exceptionHandler( - () async { - final geoip = await geoAssetsDao.getActive(GeoAssetType.geoip); - final geosite = await geoAssetsDao.getActive(GeoAssetType.geosite); - if (geoip == null || geosite == null) { - return left(const GeoAssetFailure.activeAssetNotFound()); - } - return right((geoip: geoip, geosite: geosite)); - }, - GeoAssetFailure.unexpected, - ); - } - - @override - Stream>> watchAll() { - final persistedStream = geoAssetsDao.watchAll(); - final filesStream = _watchGeoFiles(); - - return Rx.combineLatest2( - persistedStream, - filesStream, - (assets, files) => assets.map( - (e) { - final path = filesEditor.geoAssetPath(e.providerName, e.fileName); - final file = files.firstOrNullWhere((e) => e.path == path); - final stat = file?.statSync(); - return (e, stat?.size); - }, - ).toList(), - ).handleExceptions(GeoAssetUnexpectedFailure.new); - } - - Iterable _geoFiles = []; - Stream> _watchGeoFiles() async* { - yield await _readGeoFiles(); - yield* Watcher( - filesEditor.geoAssetsDir.path, - pollingDelay: const Duration(seconds: 1), - ).events.asyncMap((event) async { - await _readGeoFiles(); - return _geoFiles; - }); - } - - Future> _readGeoFiles() async { - return _geoFiles = Directory(filesEditor.geoAssetsDir.path) - .listSync() - .whereType() - .where((e) => e.extension == '.db'); - } - - @override - TaskEither update(GeoAsset geoAsset) { - return exceptionHandler( - () async { - loggy.debug( - "checking latest release of [${geoAsset.name}] on [${geoAsset.repositoryUrl}]", - ); - final response = await dio.get(geoAsset.repositoryUrl); - if (response.statusCode != 200 || response.data == null) { - return left( - GeoAssetFailure.unexpected("invalid response", StackTrace.current), - ); - } - - final path = - filesEditor.geoAssetPath(geoAsset.providerName, geoAsset.name); - final tagName = response.data!['tag_name'] as String; - loggy.debug("latest release of [${geoAsset.name}]: [$tagName]"); - if (tagName == geoAsset.version && await File(path).exists()) { - await geoAssetsDao.edit(geoAsset.copyWith(lastCheck: DateTime.now())); - return left(const GeoAssetFailure.noUpdateAvailable()); - } - - final assets = (response.data!['assets'] as List) - .whereType>(); - final asset = - assets.firstOrNullWhere((e) => e["name"] == geoAsset.name); - if (asset == null) { - return left( - GeoAssetFailure.unexpected( - "couldn't find [${geoAsset.name}] on [${geoAsset.repositoryUrl}]", - StackTrace.current, - ), - ); - } - - final downloadUrl = asset["browser_download_url"] as String; - loggy.debug("[${geoAsset.name}] download url: [$downloadUrl]"); - final tempPath = "$path.tmp"; - await File(path).parent.create(recursive: true); - await dio.download(downloadUrl, tempPath); - await File(tempPath).rename(path); - - await geoAssetsDao.edit( - geoAsset.copyWith( - version: tagName, - lastCheck: DateTime.now(), - ), - ); - - return right(unit); - }, - GeoAssetFailure.unexpected, - ); - } - - @override - TaskEither markAsActive(GeoAsset geoAsset) { - return exceptionHandler( - () async { - await geoAssetsDao.edit(geoAsset.copyWith(active: true)); - return right(unit); - }, - GeoAssetFailure.unexpected, - ); - } - - @override - TaskEither addRecommended() { - return exceptionHandler( - () async { - final persistedIds = await geoAssetsDao - .watchAll() - .first - .then((value) => value.map((e) => e.id)); - final missing = - recommendedGeoAssets.where((e) => !persistedIds.contains(e.id)); - for (final geoAsset in missing) { - await geoAssetsDao.add(geoAsset); - } - return right(unit); - }, - GeoAssetFailure.unexpected, - ); - } -} diff --git a/lib/data/repository/profiles_repository_impl.dart b/lib/data/repository/profiles_repository_impl.dart index cc05cd30..d73eb585 100644 --- a/lib/data/repository/profiles_repository_impl.dart +++ b/lib/data/repository/profiles_repository_impl.dart @@ -2,7 +2,7 @@ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:fpdart/fpdart.dart'; -import 'package:hiddify/data/local/dao/dao.dart'; +import 'package:hiddify/data/local/dao/profiles_dao.dart'; import 'package:hiddify/data/repository/exception_handlers.dart'; import 'package:hiddify/domain/enums.dart'; import 'package:hiddify/domain/profiles/profiles.dart'; diff --git a/lib/domain/rules/geo_asset.dart b/lib/domain/rules/geo_asset.dart deleted file mode 100644 index 409b7b82..00000000 --- a/lib/domain/rules/geo_asset.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'geo_asset.freezed.dart'; -part 'geo_asset.g.dart'; - -enum GeoAssetType { geoip, geosite } - -typedef GeoAssetWithFileSize = (GeoAsset geoAsset, int? size); - -@freezed -class GeoAsset with _$GeoAsset { - const GeoAsset._(); - - const factory GeoAsset({ - required String id, - required String name, - required GeoAssetType type, - required bool active, - required String providerName, - String? version, - DateTime? lastCheck, - }) = _GeoAsset; - - factory GeoAsset.fromJson(Map json) => - _$GeoAssetFromJson(json); - - String get fileName => name; - - String get repositoryUrl => - "https://api.github.com/repos/$providerName/releases/latest"; -} - -/// default geoip asset bundled with the app -const defaultGeoip = GeoAsset( - id: "sing-box-geoip", - name: "geoip.db", - type: GeoAssetType.geoip, - active: true, - providerName: "SagerNet/sing-geoip", -); - -/// default geosite asset bundled with the app -const defaultGeosite = GeoAsset( - id: "sing-box-geosite", - name: "geosite.db", - type: GeoAssetType.geosite, - active: true, - providerName: "SagerNet/sing-geosite", -); - -const defaultGeoAssets = [defaultGeoip, defaultGeosite]; - -const recommendedGeoAssets = [ - ...defaultGeoAssets, - GeoAsset( - id: "chocolate4U-geoip", - name: "geoip.db", - type: GeoAssetType.geoip, - active: false, - providerName: "Chocolate4U/Iran-sing-box-rules", - ), - GeoAsset( - id: "chocolate4U-geosite", - name: "geosite.db", - type: GeoAssetType.geosite, - active: false, - providerName: "Chocolate4U/Iran-sing-box-rules", - ), -]; diff --git a/lib/domain/rules/geo_assets_repository.dart b/lib/domain/rules/geo_assets_repository.dart deleted file mode 100644 index 2e55632d..00000000 --- a/lib/domain/rules/geo_assets_repository.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:fpdart/fpdart.dart'; -import 'package:hiddify/domain/rules/geo_asset.dart'; -import 'package:hiddify/domain/rules/geo_asset_failure.dart'; - -abstract interface class GeoAssetsRepository { - TaskEither - getActivePair(); - - Stream>> watchAll(); - - TaskEither update(GeoAsset geoAsset); - - TaskEither markAsActive(GeoAsset geoAsset); - - TaskEither addRecommended(); -} diff --git a/lib/features/geo_asset/data/geo_asset_data_mapper.dart b/lib/features/geo_asset/data/geo_asset_data_mapper.dart new file mode 100644 index 00000000..7906ac0e --- /dev/null +++ b/lib/features/geo_asset/data/geo_asset_data_mapper.dart @@ -0,0 +1,31 @@ +import 'package:drift/drift.dart'; +import 'package:hiddify/data/local/database.dart'; +import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart'; + +extension GeoAssetEntityMapper on GeoAssetEntity { + GeoAssetEntriesCompanion toEntry() { + return GeoAssetEntriesCompanion.insert( + id: id, + type: type, + active: active, + name: name, + providerName: providerName, + version: Value(version), + lastCheck: Value(lastCheck), + ); + } +} + +extension GeoAssetEntryMapper on GeoAssetEntry { + GeoAssetEntity toEntity() { + return GeoAssetEntity( + id: id, + name: name, + type: type, + active: active, + providerName: providerName, + version: version, + lastCheck: lastCheck, + ); + } +} diff --git a/lib/features/geo_asset/data/geo_asset_data_providers.dart b/lib/features/geo_asset/data/geo_asset_data_providers.dart new file mode 100644 index 00000000..1e9be492 --- /dev/null +++ b/lib/features/geo_asset/data/geo_asset_data_providers.dart @@ -0,0 +1,31 @@ +import 'package:hiddify/data/data_providers.dart'; +import 'package:hiddify/features/geo_asset/data/geo_asset_data_source.dart'; +import 'package:hiddify/features/geo_asset/data/geo_asset_path_resolver.dart'; +import 'package:hiddify/features/geo_asset/data/geo_asset_repository.dart'; +import 'package:hiddify/services/service_providers.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'geo_asset_data_providers.g.dart'; + +@Riverpod(keepAlive: true) +Future geoAssetRepository(GeoAssetRepositoryRef ref) async { + final repo = GeoAssetRepositoryImpl( + geoAssetDataSource: ref.watch(geoAssetDataSourceProvider), + geoAssetPathResolver: ref.watch(geoAssetPathResolverProvider), + dio: ref.watch(dioProvider), + ); + await repo.init().getOrElse((l) => throw l).run(); + return repo; +} + +@Riverpod(keepAlive: true) +GeoAssetDataSource geoAssetDataSource(GeoAssetDataSourceRef ref) { + return GeoAssetsDao(ref.watch(appDatabaseProvider)); +} + +@Riverpod(keepAlive: true) +GeoAssetPathResolver geoAssetPathResolver(GeoAssetPathResolverRef ref) { + return GeoAssetPathResolver( + ref.watch(filesEditorServiceProvider).dirs.workingDir, + ); +} diff --git a/lib/features/geo_asset/data/geo_asset_data_source.dart b/lib/features/geo_asset/data/geo_asset_data_source.dart new file mode 100644 index 00000000..b72b751d --- /dev/null +++ b/lib/features/geo_asset/data/geo_asset_data_source.dart @@ -0,0 +1,59 @@ +import 'package:drift/drift.dart'; +import 'package:hiddify/data/local/database.dart'; +import 'package:hiddify/data/local/tables.dart'; +import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart'; +import 'package:hiddify/utils/custom_loggers.dart'; + +part 'geo_asset_data_source.g.dart'; + +abstract interface class GeoAssetDataSource { + Future insert(GeoAssetEntriesCompanion entry); + Future getActiveAssetByType(GeoAssetType type); + Stream> watchAll(); + Future patch(String id, GeoAssetEntriesCompanion entry); +} + +@DriftAccessor(tables: [GeoAssetEntries]) +class GeoAssetsDao extends DatabaseAccessor + with _$GeoAssetsDaoMixin, InfraLogger + implements GeoAssetDataSource { + GeoAssetsDao(super.db); + + @override + Future insert(GeoAssetEntriesCompanion entry) async { + await into(geoAssetEntries).insert(entry); + } + + @override + Future getActiveAssetByType(GeoAssetType type) async { + return (geoAssetEntries.select() + ..where((tbl) => tbl.active.equals(true)) + ..where((tbl) => tbl.type.equalsValue(type)) + ..limit(1)) + .getSingleOrNull(); + } + + @override + Stream> watchAll() { + return geoAssetEntries.select().watch(); + } + + @override + Future patch(String id, GeoAssetEntriesCompanion entry) async { + await transaction( + () async { + if (entry.active.present && entry.active.value) { + final baseEntry = await (select(geoAssetEntries) + ..where((tbl) => tbl.id.equals(id))) + .getSingle(); + await (update(geoAssetEntries) + ..where((tbl) => tbl.active.equals(true)) + ..where((tbl) => tbl.type.equalsValue(baseEntry.type))) + .write(const GeoAssetEntriesCompanion(active: Value(false))); + } + await (update(geoAssetEntries)..where((tbl) => tbl.id.equals(id))) + .write(entry); + }, + ); + } +} diff --git a/lib/features/geo_asset/data/geo_asset_path_resolver.dart b/lib/features/geo_asset/data/geo_asset_path_resolver.dart new file mode 100644 index 00000000..5dc1d117 --- /dev/null +++ b/lib/features/geo_asset/data/geo_asset_path_resolver.dart @@ -0,0 +1,31 @@ +import 'dart:io'; + +import 'package:path/path.dart' as p; + +class GeoAssetPathResolver { + const GeoAssetPathResolver(this._workingDir); + + final Directory _workingDir; + + Directory get directory => Directory(p.join(_workingDir.path, "geo-assets")); + + File file(String providerName, String fileName) { + final prefix = providerName.replaceAll("/", "-").toLowerCase().trim(); + return File( + p.join( + directory.path, + "$prefix${prefix.isEmpty ? "" : "-"}$fileName", + ), + ); + } + + /// geoasset's path relative to working directory + String relativePath(String providerName, String fileName) { + final fullPath = file(providerName, fileName).path; + return p.relative(fullPath, from: _workingDir.path); + } + + String resolvePath(String path) { + return p.absolute(_workingDir.path, path); + } +} diff --git a/lib/features/geo_asset/data/geo_asset_repository.dart b/lib/features/geo_asset/data/geo_asset_repository.dart new file mode 100644 index 00000000..74abb850 --- /dev/null +++ b/lib/features/geo_asset/data/geo_asset_repository.dart @@ -0,0 +1,232 @@ +import 'dart:io'; + +import 'package:dartx/dartx_io.dart'; +import 'package:dio/dio.dart'; +import 'package:drift/drift.dart'; +import 'package:flutter/services.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:hiddify/data/local/database.dart'; +import 'package:hiddify/data/repository/exception_handlers.dart'; +import 'package:hiddify/features/geo_asset/data/geo_asset_data_mapper.dart'; +import 'package:hiddify/features/geo_asset/data/geo_asset_data_source.dart'; +import 'package:hiddify/features/geo_asset/data/geo_asset_path_resolver.dart'; +import 'package:hiddify/features/geo_asset/model/default_geo_assets.dart'; +import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart'; +import 'package:hiddify/features/geo_asset/model/geo_asset_failure.dart'; +import 'package:hiddify/gen/assets.gen.dart'; +import 'package:hiddify/utils/custom_loggers.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:watcher/watcher.dart'; + +abstract interface class GeoAssetRepository { + /// populate bundled geo assets directory with bundled files if needed + TaskEither init(); + TaskEither + getActivePair(); + Stream>> watchAll(); + TaskEither update(GeoAssetEntity geoAsset); + TaskEither markAsActive(GeoAssetEntity geoAsset); + TaskEither addRecommended(); +} + +class GeoAssetRepositoryImpl + with ExceptionHandler, InfraLogger + implements GeoAssetRepository { + GeoAssetRepositoryImpl({ + required this.geoAssetDataSource, + required this.geoAssetPathResolver, + required this.dio, + }); + + final GeoAssetDataSource geoAssetDataSource; + final GeoAssetPathResolver geoAssetPathResolver; + final Dio dio; + + @override + TaskEither init() { + return exceptionHandler( + () async { + loggy.debug("initializing"); + final geoipFile = geoAssetPathResolver.file( + defaultGeoip.providerName, + defaultGeoip.fileName, + ); + final geositeFile = geoAssetPathResolver.file( + defaultGeosite.providerName, + defaultGeosite.fileName, + ); + + final dirExists = await geoAssetPathResolver.directory.exists(); + if (!dirExists) { + await geoAssetPathResolver.directory.create(recursive: true); + } + + if (!dirExists || !await geoipFile.exists()) { + final bundledGeoip = await rootBundle.load(Assets.core.geoip); + await geoipFile.writeAsBytes(bundledGeoip.buffer.asInt8List()); + } + if (!dirExists || !await geositeFile.exists()) { + final bundledGeosite = await rootBundle.load(Assets.core.geosite); + await geositeFile.writeAsBytes(bundledGeosite.buffer.asInt8List()); + } + return right(unit); + }, + GeoAssetUnexpectedFailure.new, + ); + } + + @override + TaskEither + getActivePair() { + return exceptionHandler( + () async { + final geoip = + await geoAssetDataSource.getActiveAssetByType(GeoAssetType.geoip); + final geosite = + await geoAssetDataSource.getActiveAssetByType(GeoAssetType.geosite); + if (geoip == null || geosite == null) { + return left(const GeoAssetFailure.activeAssetNotFound()); + } + return right((geoip: geoip.toEntity(), geosite: geosite.toEntity())); + }, + GeoAssetUnexpectedFailure.new, + ); + } + + @override + Stream>> watchAll() { + final persistedStream = geoAssetDataSource + .watchAll() + .map((event) => event.map((e) => e.toEntity())); + final filesStream = _watchGeoFiles(); + + return Rx.combineLatest2( + persistedStream, + filesStream, + (assets, files) => assets.map( + (e) { + final path = + geoAssetPathResolver.file(e.providerName, e.fileName).path; + final file = files.firstOrNullWhere((e) => e.path == path); + final stat = file?.statSync(); + return (e, stat?.size); + }, + ).toList(), + ).handleExceptions(GeoAssetUnexpectedFailure.new); + } + + Iterable _geoFiles = []; + Stream> _watchGeoFiles() async* { + yield await _readGeoFiles(); + yield* Watcher( + geoAssetPathResolver.directory.path, + pollingDelay: const Duration(seconds: 1), + ).events.asyncMap((event) async { + await _readGeoFiles(); + return _geoFiles; + }); + } + + Future> _readGeoFiles() async { + return _geoFiles = Directory(geoAssetPathResolver.directory.path) + .listSync() + .whereType() + .where((e) => e.extension == '.db'); + } + + @override + TaskEither update(GeoAssetEntity geoAsset) { + return exceptionHandler( + () async { + loggy.debug( + "checking latest release of [${geoAsset.name}] on [${geoAsset.repositoryUrl}]", + ); + final response = await dio.get(geoAsset.repositoryUrl); + if (response.statusCode != 200 || response.data == null) { + return left( + GeoAssetUnexpectedFailure.new( + "invalid response", + StackTrace.current, + ), + ); + } + + final file = + geoAssetPathResolver.file(geoAsset.providerName, geoAsset.name); + final tagName = response.data!['tag_name'] as String; + loggy.debug("latest release of [${geoAsset.name}]: [$tagName]"); + if (tagName == geoAsset.version && await file.exists()) { + await geoAssetDataSource.patch( + geoAsset.id, + GeoAssetEntriesCompanion(lastCheck: Value(DateTime.now())), + ); + return left(const GeoAssetFailure.noUpdateAvailable()); + } + + final assets = (response.data!['assets'] as List) + .whereType>(); + final asset = + assets.firstOrNullWhere((e) => e["name"] == geoAsset.name); + if (asset == null) { + return left( + GeoAssetUnexpectedFailure.new( + "couldn't find [${geoAsset.name}] on [${geoAsset.repositoryUrl}]", + StackTrace.current, + ), + ); + } + + final downloadUrl = asset["browser_download_url"] as String; + loggy.debug("[${geoAsset.name}] download url: [$downloadUrl]"); + final tempPath = "${file.path}.tmp"; + await file.parent.create(recursive: true); + await dio.download(downloadUrl, tempPath); + await File(tempPath).rename(file.path); + + await geoAssetDataSource.patch( + geoAsset.id, + GeoAssetEntriesCompanion( + version: Value(tagName), + lastCheck: Value(DateTime.now()), + ), + ); + + return right(unit); + }, + GeoAssetUnexpectedFailure.new, + ); + } + + @override + TaskEither markAsActive(GeoAssetEntity geoAsset) { + return exceptionHandler( + () async { + await geoAssetDataSource.patch( + geoAsset.id, + const GeoAssetEntriesCompanion(active: Value(true)), + ); + return right(unit); + }, + GeoAssetUnexpectedFailure.new, + ); + } + + @override + TaskEither addRecommended() { + return exceptionHandler( + () async { + final persistedIds = await geoAssetDataSource + .watchAll() + .first + .then((value) => value.map((e) => e.id)); + final missing = + recommendedGeoAssets.where((e) => !persistedIds.contains(e.id)); + for (final geoAsset in missing) { + await geoAssetDataSource.insert(geoAsset.toEntry()); + } + return right(unit); + }, + GeoAssetUnexpectedFailure.new, + ); + } +} diff --git a/lib/features/geo_asset/model/default_geo_assets.dart b/lib/features/geo_asset/model/default_geo_assets.dart new file mode 100644 index 00000000..df00d7a2 --- /dev/null +++ b/lib/features/geo_asset/model/default_geo_assets.dart @@ -0,0 +1,39 @@ +import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart'; + +/// default geoip asset bundled with the app +const defaultGeoip = GeoAssetEntity( + id: "sing-box-geoip", + name: "geoip.db", + type: GeoAssetType.geoip, + active: true, + providerName: "SagerNet/sing-geoip", +); + +/// default geosite asset bundled with the app +const defaultGeosite = GeoAssetEntity( + id: "sing-box-geosite", + name: "geosite.db", + type: GeoAssetType.geosite, + active: true, + providerName: "SagerNet/sing-geosite", +); + +const defaultGeoAssets = [defaultGeoip, defaultGeosite]; + +const recommendedGeoAssets = [ + ...defaultGeoAssets, + GeoAssetEntity( + id: "chocolate4U-geoip", + name: "geoip.db", + type: GeoAssetType.geoip, + active: false, + providerName: "Chocolate4U/Iran-sing-box-rules", + ), + GeoAssetEntity( + id: "chocolate4U-geosite", + name: "geosite.db", + type: GeoAssetType.geosite, + active: false, + providerName: "Chocolate4U/Iran-sing-box-rules", + ), +]; diff --git a/lib/features/geo_asset/model/geo_asset_entity.dart b/lib/features/geo_asset/model/geo_asset_entity.dart new file mode 100644 index 00000000..44a3f9fd --- /dev/null +++ b/lib/features/geo_asset/model/geo_asset_entity.dart @@ -0,0 +1,27 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'geo_asset_entity.freezed.dart'; + +enum GeoAssetType { geoip, geosite } + +typedef GeoAssetWithFileSize = (GeoAssetEntity geoAsset, int? size); + +@freezed +class GeoAssetEntity with _$GeoAssetEntity { + const GeoAssetEntity._(); + + const factory GeoAssetEntity({ + required String id, + required String name, + required GeoAssetType type, + required bool active, + required String providerName, + String? version, + DateTime? lastCheck, + }) = _GeoAssetEntity; + + String get fileName => name; + + String get repositoryUrl => + "https://api.github.com/repos/$providerName/releases/latest"; +} diff --git a/lib/domain/rules/geo_asset_failure.dart b/lib/features/geo_asset/model/geo_asset_failure.dart similarity index 97% rename from lib/domain/rules/geo_asset_failure.dart rename to lib/features/geo_asset/model/geo_asset_failure.dart index 7beb8ef2..161b193e 100644 --- a/lib/domain/rules/geo_asset_failure.dart +++ b/lib/features/geo_asset/model/geo_asset_failure.dart @@ -6,7 +6,7 @@ part 'geo_asset_failure.freezed.dart'; @freezed sealed class GeoAssetFailure with _$GeoAssetFailure, Failure { - const GeoAssetFailure._(); + const GeoAssetFailure._(); const factory GeoAssetFailure.unexpected([ Object? error, diff --git a/lib/features/geo_asset/notifier/geo_asset_notifier.dart b/lib/features/geo_asset/notifier/geo_asset_notifier.dart new file mode 100644 index 00000000..94f1ddac --- /dev/null +++ b/lib/features/geo_asset/notifier/geo_asset_notifier.dart @@ -0,0 +1,33 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:hiddify/features/geo_asset/data/geo_asset_data_providers.dart'; +import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart'; +import 'package:hiddify/utils/custom_loggers.dart'; +import 'package:hiddify/utils/riverpod_utils.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'geo_asset_notifier.g.dart'; + +@riverpod +class FetchGeoAsset extends _$FetchGeoAsset with AppLogger { + @override + Future build(String id) async { + ref.disposeDelay(const Duration(seconds: 10)); + return null; + } + + Future fetch(GeoAssetEntity geoAsset) async { + state = const AsyncLoading(); + state = await AsyncValue.guard( + () => ref + .read(geoAssetRepositoryProvider) + .requireValue + .update(geoAsset) + .getOrElse( + (failure) { + loggy.warning("error updating geo asset $failure", failure); + throw failure; + }, + ).run(), + ); + } +} diff --git a/lib/features/geo_asset/overview/geo_assets_overview_notifier.dart b/lib/features/geo_asset/overview/geo_assets_overview_notifier.dart new file mode 100644 index 00000000..d3ead5b9 --- /dev/null +++ b/lib/features/geo_asset/overview/geo_assets_overview_notifier.dart @@ -0,0 +1,43 @@ +import 'package:hiddify/features/geo_asset/data/geo_asset_data_providers.dart'; +import 'package:hiddify/features/geo_asset/data/geo_asset_repository.dart'; +import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart'; +import 'package:hiddify/utils/custom_loggers.dart'; +import 'package:hiddify/utils/riverpod_utils.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'geo_assets_overview_notifier.g.dart'; + +@riverpod +class GeoAssetsOverviewNotifier extends _$GeoAssetsOverviewNotifier + with AppLogger { + @override + Stream> build() { + ref.disposeDelay(const Duration(seconds: 5)); + return ref + .watch(geoAssetRepositoryProvider) + .requireValue + .watchAll() + .map((event) => event.getOrElse((l) => throw l)); + } + + GeoAssetRepository get _geoAssetRepo => + ref.read(geoAssetRepositoryProvider).requireValue; + + Future markAsActive(GeoAssetEntity geoAsset) async { + await _geoAssetRepo.markAsActive(geoAsset).getOrElse( + (f) { + loggy.warning("error marking geo asset as active", f); + throw f; + }, + ).run(); + } + + Future addRecommended() async { + await _geoAssetRepo.addRecommended().getOrElse( + (f) { + loggy.warning("error adding recommended geo assets", f); + throw f; + }, + ).run(); + } +} diff --git a/lib/features/settings/geo_assets/geo_assets_page.dart b/lib/features/geo_asset/overview/geo_assets_overview_page.dart similarity index 65% rename from lib/features/settings/geo_assets/geo_assets_page.dart rename to lib/features/geo_asset/overview/geo_assets_overview_page.dart index b53aedf1..a611fabd 100644 --- a/lib/features/settings/geo_assets/geo_assets_page.dart +++ b/lib/features/geo_asset/overview/geo_assets_overview_page.dart @@ -1,16 +1,16 @@ import 'package:flutter/material.dart'; import 'package:hiddify/core/core_providers.dart'; -import 'package:hiddify/features/settings/geo_assets/geo_asset_tile.dart'; -import 'package:hiddify/features/settings/geo_assets/geo_assets_notifier.dart'; +import 'package:hiddify/features/geo_asset/overview/geo_assets_overview_notifier.dart'; +import 'package:hiddify/features/geo_asset/widget/geo_asset_tile.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -class GeoAssetsPage extends HookConsumerWidget { - const GeoAssetsPage({super.key}); +class GeoAssetsOverviewPage extends HookConsumerWidget { + const GeoAssetsOverviewPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final t = ref.watch(translationsProvider); - final state = ref.watch(geoAssetsNotifierProvider); + final state = ref.watch(geoAssetsOverviewNotifierProvider); return Scaffold( body: CustomScrollView( @@ -25,7 +25,7 @@ class GeoAssetsPage extends HookConsumerWidget { child: Text(t.settings.geoAssets.addRecommended), onTap: () { ref - .read(geoAssetsNotifierProvider.notifier) + .read(geoAssetsOverviewNotifierProvider.notifier) .addRecommended(); }, ), @@ -38,7 +38,12 @@ class GeoAssetsPage extends HookConsumerWidget { AsyncData(value: final geoAssets) => SliverList.builder( itemBuilder: (context, index) { final geoAsset = geoAssets[index]; - return GeoAssetTile(geoAsset); + return GeoAssetTile( + geoAsset, + onMarkAsActive: () => ref + .read(geoAssetsOverviewNotifierProvider.notifier) + .markAsActive(geoAsset.$1), + ); }, itemCount: geoAssets.length, ), diff --git a/lib/features/settings/geo_assets/geo_asset_tile.dart b/lib/features/geo_asset/widget/geo_asset_tile.dart similarity index 63% rename from lib/features/settings/geo_assets/geo_asset_tile.dart rename to lib/features/geo_asset/widget/geo_asset_tile.dart index db1f6de0..620f1a21 100644 --- a/lib/features/settings/geo_assets/geo_asset_tile.dart +++ b/lib/features/geo_asset/widget/geo_asset_tile.dart @@ -2,40 +2,44 @@ import 'package:dartx/dartx.dart'; import 'package:flutter/material.dart'; import 'package:hiddify/core/core_providers.dart'; import 'package:hiddify/domain/failures.dart'; -import 'package:hiddify/domain/rules/geo_asset.dart'; -import 'package:hiddify/domain/rules/geo_asset_failure.dart'; -import 'package:hiddify/features/settings/geo_assets/geo_assets_notifier.dart'; -import 'package:hiddify/utils/alerts.dart'; -import 'package:hiddify/utils/async_mutation.dart'; -import 'package:hiddify/utils/date_time_formatter.dart'; +import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart'; +import 'package:hiddify/features/geo_asset/model/geo_asset_failure.dart'; +import 'package:hiddify/features/geo_asset/notifier/geo_asset_notifier.dart'; +import 'package:hiddify/utils/utils.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:humanizer/humanizer.dart'; class GeoAssetTile extends HookConsumerWidget { - GeoAssetTile(GeoAssetWithFileSize geoAssetWithFileSize, {super.key}) - : geoAsset = geoAssetWithFileSize.$1, + GeoAssetTile( + GeoAssetWithFileSize geoAssetWithFileSize, { + super.key, + required this.onMarkAsActive, + }) : geoAsset = geoAssetWithFileSize.$1, size = geoAssetWithFileSize.$2; - final GeoAsset geoAsset; + final GeoAssetEntity geoAsset; final int? size; + final VoidCallback onMarkAsActive; @override Widget build(BuildContext context, WidgetRef ref) { final t = ref.watch(translationsProvider); + final fetchState = ref.watch(fetchGeoAssetProvider(geoAsset.id)); final fileMissing = size == null; - final updateMutation = useMutation( - initialOnFailure: (err) { - if (err case GeoAssetNoUpdateAvailable()) { - CustomToast(t.failure.geoAssets.notUpdate).show(context); - } else { - CustomAlertDialog.fromErr( - t.presentError(err, action: t.settings.geoAssets.failureMsg), - ).show(context); + ref.listen( + fetchGeoAssetProvider(geoAsset.id), + (_, next) { + switch (next) { + case AsyncError(:final error): + if (error case GeoAssetNoUpdateAvailable()) { + return CustomToast(t.failure.geoAssets.notUpdate).show(context); + } + CustomAlertDialog.fromErr(t.presentError(error)).show(context); + case AsyncData(value: final _?): + CustomToast.success(t.settings.geoAssets.successMsg).show(context); } }, - initialOnSuccess: () => - CustomToast.success(t.settings.geoAssets.successMsg).show(context), ); return ListTile( @@ -49,7 +53,7 @@ class GeoAssetTile extends HookConsumerWidget { ), ), isThreeLine: true, - subtitle: updateMutation.state.isInProgress + subtitle: fetchState.isLoading ? const LinearProgressIndicator() : Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -89,26 +93,15 @@ class GeoAssetTile extends HookConsumerWidget { ], ), selected: geoAsset.active, - onTap: () async { - await ref - .read(geoAssetsNotifierProvider.notifier) - .markAsActive(geoAsset); - }, + onTap: onMarkAsActive, trailing: PopupMenuButton( itemBuilder: (context) { return [ PopupMenuItem( - enabled: !updateMutation.state.isInProgress, - onTap: () { - if (updateMutation.state.isInProgress) { - return; - } - updateMutation.setFuture( - ref - .read(geoAssetsNotifierProvider.notifier) - .updateGeoAsset(geoAsset), - ); - }, + enabled: !fetchState.isLoading, + onTap: () => ref + .read(FetchGeoAssetProvider(geoAsset.id).notifier) + .fetch(geoAsset), child: fileMissing ? Text(t.settings.geoAssets.download) : Text(t.settings.geoAssets.update), diff --git a/lib/features/settings/geo_assets/geo_assets_notifier.dart b/lib/features/settings/geo_assets/geo_assets_notifier.dart deleted file mode 100644 index 0e1cb3db..00000000 --- a/lib/features/settings/geo_assets/geo_assets_notifier.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:hiddify/data/data_providers.dart'; -import 'package:hiddify/domain/rules/geo_asset.dart'; -import 'package:hiddify/utils/custom_loggers.dart'; -import 'package:hiddify/utils/riverpod_utils.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'geo_assets_notifier.g.dart'; - -@riverpod -class GeoAssetsNotifier extends _$GeoAssetsNotifier with AppLogger { - @override - Stream> build() { - ref.disposeDelay(const Duration(seconds: 5)); - return ref - .watch(geoAssetsRepositoryProvider) - .watchAll() - .map((event) => event.getOrElse((l) => throw l)); - } - - Future updateGeoAsset(GeoAsset geoAsset) async { - await ref.read(geoAssetsRepositoryProvider).update(geoAsset).getOrElse( - (f) { - loggy.warning("error updating geo asset", f); - throw f; - }, - ).run(); - } - - Future markAsActive(GeoAsset geoAsset) async { - await ref - .read(geoAssetsRepositoryProvider) - .markAsActive(geoAsset) - .getOrElse( - (f) { - loggy.warning("error marking geo asset as active", f); - throw f; - }, - ).run(); - } - - Future addRecommended() async { - await ref.read(geoAssetsRepositoryProvider).addRecommended().getOrElse( - (f) { - loggy.warning("error adding recommended geo assets", f); - throw f; - }, - ).run(); - } -} diff --git a/lib/services/files_editor_service.dart b/lib/services/files_editor_service.dart index db048e81..8b6995d3 100644 --- a/lib/services/files_editor_service.dart +++ b/lib/services/files_editor_service.dart @@ -1,10 +1,6 @@ import 'dart:io'; -import 'package:dartx/dartx.dart'; -import 'package:flutter/services.dart'; import 'package:hiddify/domain/constants.dart'; -import 'package:hiddify/domain/rules/geo_asset.dart'; -import 'package:hiddify/gen/assets.gen.dart'; import 'package:hiddify/services/platform_services.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:path/path.dart' as p; @@ -28,9 +24,6 @@ class FilesEditorService with InfraLogger { Directory(p.join(workingDir.path, Constants.configsFolderName)); Directory get logsDir => dirs.workingDir; - Directory get geoAssetsDir => - Directory(p.join(workingDir.path, "geo-assets")); - File get appLogsFile => File(p.join(logsDir.path, "app.log")); File get coreLogsFile => File(p.join(logsDir.path, "box.log")); @@ -53,9 +46,6 @@ class FilesEditorService with InfraLogger { if (!await configsDir.exists()) { await configsDir.create(recursive: true); } - if (!await geoAssetsDir.exists()) { - await geoAssetsDir.create(recursive: true); - } if (await appLogsFile.exists()) { await appLogsFile.writeAsString(""); @@ -68,8 +58,6 @@ class FilesEditorService with InfraLogger { } else { await coreLogsFile.create(recursive: true); } - - await _populateGeoAssets(); } static Future getDatabaseDirectory() async { @@ -85,44 +73,9 @@ class FilesEditorService with InfraLogger { return p.join(configsDir.path, "$fileName.json"); } - String geoAssetPath(String providerName, String fileName) { - final prefix = providerName.replaceAll("/", "-").toLowerCase(); - return p.join( - geoAssetsDir.path, - "$prefix${prefix.isBlank ? "" : "-"}$fileName", - ); - } - - /// geoasset's path relative to working directory - String geoAssetRelativePath(String providerName, String fileName) { - final fullPath = geoAssetPath(providerName, fileName); - return p.relative(fullPath, from: workingDir.path); - } - - String resolveGeoAssetPath(String path) { - return p.absolute(workingDir.path, path); - } - String tempConfigPath(String fileName) => configPath("temp_$fileName"); Future deleteConfig(String fileName) { return File(configPath(fileName)).delete(); } - - Future _populateGeoAssets() async { - loggy.debug('populating geo assets'); - final geoipPath = - geoAssetPath(defaultGeoip.providerName, defaultGeoip.fileName); - if (!await File(geoipPath).exists()) { - final bundledGeoip = await rootBundle.load(Assets.core.geoip); - await File(geoipPath).writeAsBytes(bundledGeoip.buffer.asInt8List()); - } - - final geositePath = - geoAssetPath(defaultGeosite.providerName, defaultGeosite.fileName); - if (!await File(geositePath).exists()) { - final bundledGeosite = await rootBundle.load(Assets.core.geosite); - await File(geositePath).writeAsBytes(bundledGeosite.buffer.asInt8List()); - } - } } From e6c6ec59ad9acaded516d98790359b37080c3a01 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Sat, 25 Nov 2023 22:07:29 +0330 Subject: [PATCH 73/91] Add soffchen geo assets --- .../geo_asset/model/default_geo_assets.dart | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/features/geo_asset/model/default_geo_assets.dart b/lib/features/geo_asset/model/default_geo_assets.dart index df00d7a2..40b3aaa6 100644 --- a/lib/features/geo_asset/model/default_geo_assets.dart +++ b/lib/features/geo_asset/model/default_geo_assets.dart @@ -36,4 +36,18 @@ const recommendedGeoAssets = [ active: false, providerName: "Chocolate4U/Iran-sing-box-rules", ), + GeoAssetEntity( + id: "soffchen-geoip", + name: "geoip.db", + type: GeoAssetType.geoip, + active: false, + providerName: "soffchen/sing-geoip", + ), + GeoAssetEntity( + id: "soffchen-geosite", + name: "geosite.db", + type: GeoAssetType.geosite, + active: false, + providerName: "soffchen/sing-geosite", + ), ]; From 2a977f4142fa30353ead10c0827d01373ca55cec Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Sat, 25 Nov 2023 22:48:22 +0330 Subject: [PATCH 74/91] Fix chinese typography bug --- ...trings_zh.i18n.json => strings_zh-CN.i18n.json} | 0 lib/core/prefs/locale_prefs.dart | 14 +++++++++++++- lib/features/common/general_pref_tiles.dart | 12 ++---------- 3 files changed, 15 insertions(+), 11 deletions(-) rename assets/translations/{strings_zh.i18n.json => strings_zh-CN.i18n.json} (100%) diff --git a/assets/translations/strings_zh.i18n.json b/assets/translations/strings_zh-CN.i18n.json similarity index 100% rename from assets/translations/strings_zh.i18n.json rename to assets/translations/strings_zh-CN.i18n.json diff --git a/lib/core/prefs/locale_prefs.dart b/lib/core/prefs/locale_prefs.dart index 0f294743..95352687 100644 --- a/lib/core/prefs/locale_prefs.dart +++ b/lib/core/prefs/locale_prefs.dart @@ -1,3 +1,4 @@ +import 'package:flutter_localized_locales/flutter_localized_locales.dart'; import 'package:hiddify/data/data_providers.dart'; import 'package:hiddify/gen/fonts.gen.dart'; import 'package:hiddify/gen/translations.g.dart'; @@ -14,7 +15,13 @@ class LocaleNotifier extends _$LocaleNotifier { ref.watch(sharedPreferencesProvider), "locale", AppLocaleUtils.findDeviceLocale(), - mapFrom: AppLocale.values.byName, + mapFrom: (String value) { + // keep backward compatibility with chinese after changing zh to zh_CN + if (value == "zh") { + return AppLocale.zhCn; + } + return AppLocale.values.byName(value); + }, mapTo: (value) => value.name, ); @@ -30,4 +37,9 @@ class LocaleNotifier extends _$LocaleNotifier { extension AppLocaleX on AppLocale { String get preferredFontFamily => this == AppLocale.fa ? FontFamily.shabnam : ""; + + String get localeName => + LocaleNamesLocalizationsDelegate + .nativeLocaleNames[flutterLocale.toString()] ?? + name; } diff --git a/lib/features/common/general_pref_tiles.dart b/lib/features/common/general_pref_tiles.dart index 7114bafd..69a354cb 100644 --- a/lib/features/common/general_pref_tiles.dart +++ b/lib/features/common/general_pref_tiles.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter_localized_locales/flutter_localized_locales.dart'; import 'package:go_router/go_router.dart'; import 'package:hiddify/core/core_providers.dart'; import 'package:hiddify/core/prefs/prefs.dart'; @@ -17,10 +16,7 @@ class LocalePrefTile extends HookConsumerWidget { return ListTile( title: Text(t.settings.general.locale), - subtitle: Text( - LocaleNamesLocalizationsDelegate.nativeLocaleNames[locale.name] ?? - locale.name, - ), + subtitle: Text(locale.localeName), leading: const Icon(Icons.language), onTap: () async { final selectedLocale = await showDialog( @@ -31,11 +27,7 @@ class LocalePrefTile extends HookConsumerWidget { children: AppLocale.values .map( (e) => RadioListTile( - title: Text( - LocaleNamesLocalizationsDelegate - .nativeLocaleNames[e.name] ?? - e.name, - ), + title: Text(e.localeName), value: e, groupValue: locale, onChanged: (e) => context.pop(e), From e2f5f511768aedff97056f0e02f8a35ddee639ac Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Sat, 25 Nov 2023 22:54:25 +0330 Subject: [PATCH 75/91] Update changelog --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ecc3bee..dd1f9043 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## Unreleased + +### New Features and Improvements + +- Added soffchen to recommended geo assets + +### Bug Fixes + +- Fixed geo assets bug where assets were deactivated +- Fixed Chinese typography bug (thanks to [betaxab](https://github.com/betaxab)) +- Fixed localization mistakes in Russian. [PR#189](https://github.com/hiddify/hiddify-next/pull/189) by [jomertix](https://github.com/jomertix) + ## [0.11.1] - 2023-11-19 ### Bug Fixes From 829d58a1a2637a431ec3491bf11d42114e4cd9e6 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Sun, 26 Nov 2023 21:20:58 +0330 Subject: [PATCH 76/91] Refactor profiles --- lib/bootstrap.dart | 4 +- .../in_app_notification_controller.dart | 97 +++++++++ lib/core/router/routes/shared_routes.dart | 12 +- lib/core/widget/custom_alert_dialog.dart | 50 +++++ lib/data/data_providers.dart | 18 +- lib/data/local/data_mappers.dart | 73 ------- lib/data/local/database.dart | 2 +- lib/data/local/tables.dart | 2 +- lib/data/repository/core_facade_impl.dart | 17 +- lib/data/repository/repository.dart | 1 - lib/domain/profiles/profile.dart | 174 --------------- lib/domain/profiles/profiles.dart | 4 - lib/domain/profiles/profiles_repository.dart | 37 ---- .../active_profile_notifier.dart | 18 -- .../has_any_profile_notifier.dart | 14 -- lib/features/common/app_update_notifier.dart | 22 -- lib/features/common/common_controllers.dart | 10 +- .../connectivity/connectivity_controller.dart | 4 +- lib/features/home/view/home_page.dart | 5 +- .../add}/add_profile_modal.dart | 68 ++---- .../profile/data/profile_data_mapper.dart | 85 ++++++++ .../profile/data/profile_data_providers.dart | 32 +++ .../profile/data/profile_data_source.dart} | 89 ++++---- lib/features/profile/data/profile_parser.dart | 105 +++++++++ .../profile/data/profile_path_resolver.dart | 17 ++ .../profile/data/profile_repository.dart} | 203 ++++++++++++------ .../details/profile_details_notifier.dart} | 91 ++++---- .../details/profile_details_page.dart} | 101 +++++---- .../details/profile_details_state.dart | 22 ++ .../profile/model/profile_entity.dart | 57 +++++ .../profile/model/profile_failure.dart} | 2 +- .../profile/model/profile_sort_enum.dart} | 2 + .../notifier/active_profile_notifier.dart | 31 +++ .../profile/notifier/profile_notifier.dart | 140 ++++++++++++ .../notifier/profiles_update_notifier.dart | 92 ++++++++ .../overview/profiles_overview_notifier.dart | 83 +++++++ .../overview/profiles_overview_page.dart} | 31 ++- .../widget}/profile_tile.dart | 68 ++---- .../profile_detail/notifier/notifier.dart | 2 - .../notifier/profile_detail_state.dart | 22 -- lib/features/profile_detail/view/view.dart | 1 - lib/features/profiles/notifier/notifier.dart | 2 - .../profiles/notifier/profiles_notifier.dart | 140 ------------ .../notifier/profiles_update_notifier.dart | 55 ----- lib/features/profiles/view/view.dart | 2 - lib/services/cron_service.dart | 73 ------- lib/services/files_editor_service.dart | 16 -- lib/services/service_providers.dart | 9 - .../profile/data/profile_parser_test.dart} | 25 ++- 49 files changed, 1206 insertions(+), 1024 deletions(-) create mode 100644 lib/core/notification/in_app_notification_controller.dart create mode 100644 lib/core/widget/custom_alert_dialog.dart delete mode 100644 lib/data/local/data_mappers.dart delete mode 100644 lib/domain/profiles/profile.dart delete mode 100644 lib/domain/profiles/profiles.dart delete mode 100644 lib/domain/profiles/profiles_repository.dart delete mode 100644 lib/features/common/active_profile/active_profile_notifier.dart delete mode 100644 lib/features/common/active_profile/has_any_profile_notifier.dart rename lib/features/{profiles/view => profile/add}/add_profile_modal.dart (80%) create mode 100644 lib/features/profile/data/profile_data_mapper.dart create mode 100644 lib/features/profile/data/profile_data_providers.dart rename lib/{data/local/dao/profiles_dao.dart => features/profile/data/profile_data_source.dart} (57%) create mode 100644 lib/features/profile/data/profile_parser.dart create mode 100644 lib/features/profile/data/profile_path_resolver.dart rename lib/{data/repository/profiles_repository_impl.dart => features/profile/data/profile_repository.dart} (53%) rename lib/features/{profile_detail/notifier/profile_detail_notifier.dart => profile/details/profile_details_notifier.dart} (60%) rename lib/features/{profile_detail/view/profile_detail_page.dart => profile/details/profile_details_page.dart} (79%) create mode 100644 lib/features/profile/details/profile_details_state.dart create mode 100644 lib/features/profile/model/profile_entity.dart rename lib/{domain/profiles/profiles_failure.dart => features/profile/model/profile_failure.dart} (97%) rename lib/{domain/profiles/profile_enums.dart => features/profile/model/profile_sort_enum.dart} (91%) create mode 100644 lib/features/profile/notifier/active_profile_notifier.dart create mode 100644 lib/features/profile/notifier/profile_notifier.dart create mode 100644 lib/features/profile/notifier/profiles_update_notifier.dart create mode 100644 lib/features/profile/overview/profiles_overview_notifier.dart rename lib/features/{profiles/view/profiles_modal.dart => profile/overview/profiles_overview_page.dart} (81%) rename lib/features/{common => profile/widget}/profile_tile.dart (87%) delete mode 100644 lib/features/profile_detail/notifier/notifier.dart delete mode 100644 lib/features/profile_detail/notifier/profile_detail_state.dart delete mode 100644 lib/features/profile_detail/view/view.dart delete mode 100644 lib/features/profiles/notifier/notifier.dart delete mode 100644 lib/features/profiles/notifier/profiles_notifier.dart delete mode 100644 lib/features/profiles/notifier/profiles_update_notifier.dart delete mode 100644 lib/features/profiles/view/view.dart delete mode 100644 lib/services/cron_service.dart rename test/{domain/profiles/profile_test.dart => features/profile/data/profile_parser_test.dart} (69%) diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index 7f4c42fe..465d22d6 100644 --- a/lib/bootstrap.dart +++ b/lib/bootstrap.dart @@ -10,9 +10,10 @@ import 'package:hiddify/core/prefs/prefs.dart'; import 'package:hiddify/data/data_providers.dart'; import 'package:hiddify/data/repository/app_repository_impl.dart'; import 'package:hiddify/domain/environment.dart'; -import 'package:hiddify/features/common/active_profile/active_profile_notifier.dart'; import 'package:hiddify/features/common/window/window_controller.dart'; import 'package:hiddify/features/geo_asset/data/geo_asset_data_providers.dart'; +import 'package:hiddify/features/profile/data/profile_data_providers.dart'; +import 'package:hiddify/features/profile/notifier/active_profile_notifier.dart'; import 'package:hiddify/features/system_tray/system_tray_controller.dart'; import 'package:hiddify/services/auto_start_service.dart'; import 'package:hiddify/services/deep_link_service.dart'; @@ -88,6 +89,7 @@ Future _lazyBootstrap( final filesEditor = container.read(filesEditorServiceProvider); await filesEditor.init(); await container.read(geoAssetRepositoryProvider.future); + await container.read(profileRepositoryProvider.future); initLoggers(container.read, debug); _logger.info(container.read(appInfoProvider).format()); diff --git a/lib/core/notification/in_app_notification_controller.dart b/lib/core/notification/in_app_notification_controller.dart new file mode 100644 index 00000000..1894e13e --- /dev/null +++ b/lib/core/notification/in_app_notification_controller.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:hiddify/domain/failures.dart'; +import 'package:hiddify/features/common/adaptive_root_scaffold.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:toastification/toastification.dart'; + +part 'in_app_notification_controller.g.dart'; + +@Riverpod(keepAlive: true) +InAppNotificationController inAppNotificationController( + InAppNotificationControllerRef ref, +) { + return InAppNotificationController(); +} + +enum NotificationType { + info, + error, + success, +} + +class InAppNotificationController with AppLogger { + void showToast( + BuildContext context, + String message, { + NotificationType type = NotificationType.info, + Duration duration = const Duration(seconds: 3), + }) { + toastification.show( + context: context, + title: message, + type: type._toastificationType, + alignment: Alignment.bottomLeft, + autoCloseDuration: duration, + style: ToastificationStyle.fillColored, + pauseOnHover: true, + showProgressBar: false, + dragToClose: true, + closeOnClick: true, + closeButtonShowType: CloseButtonShowType.onHover, + ); + } + + void showErrorToast(String message) { + final context = RootScaffold.stateKey.currentContext; + if (context == null) { + loggy.warning("context is null"); + return; + } + showToast( + context, + message, + type: NotificationType.error, + duration: const Duration(seconds: 5), + ); + } + + void showSuccessToast(String message) { + final context = RootScaffold.stateKey.currentContext; + if (context == null) { + loggy.warning("context is null"); + return; + } + showToast( + context, + message, + type: NotificationType.success, + ); + } + + void showInfoToast(String message) { + final context = RootScaffold.stateKey.currentContext; + if (context == null) { + loggy.warning("context is null"); + return; + } + showToast(context, message); + } + + Future showErrorDialog(PresentableError error) async { + final context = RootScaffold.stateKey.currentContext; + if (context == null) { + loggy.warning("context is null"); + return; + } + CustomAlertDialog.fromErr(error).show(context); + } +} + +extension NotificationTypeX on NotificationType { + ToastificationType get _toastificationType => switch (this) { + NotificationType.success => ToastificationType.success, + NotificationType.error => ToastificationType.error, + NotificationType.info => ToastificationType.info, + }; +} diff --git a/lib/core/router/routes/shared_routes.dart b/lib/core/router/routes/shared_routes.dart index 76410f5f..1960b7cc 100644 --- a/lib/core/router/routes/shared_routes.dart +++ b/lib/core/router/routes/shared_routes.dart @@ -3,8 +3,9 @@ import 'package:go_router/go_router.dart'; import 'package:hiddify/core/router/app_router.dart'; import 'package:hiddify/features/home/view/view.dart'; import 'package:hiddify/features/intro/intro_page.dart'; -import 'package:hiddify/features/profile_detail/view/view.dart'; -import 'package:hiddify/features/profiles/view/view.dart'; +import 'package:hiddify/features/profile/add/add_profile_modal.dart'; +import 'package:hiddify/features/profile/details/profile_details_page.dart'; +import 'package:hiddify/features/profile/overview/profiles_overview_page.dart'; import 'package:hiddify/features/proxies/view/view.dart'; import 'package:hiddify/utils/utils.dart'; @@ -86,7 +87,8 @@ class ProfilesRoute extends GoRouteData { Page buildPage(BuildContext context, GoRouterState state) { return BottomSheetPage( name: name, - builder: (controller) => ProfilesModal(scrollController: controller), + builder: (controller) => + ProfilesOverviewModal(scrollController: controller), ); } } @@ -103,7 +105,7 @@ class NewProfileRoute extends GoRouteData { return const MaterialPage( fullscreenDialog: true, name: name, - child: ProfileDetailPage("new"), + child: ProfileDetailsPage("new"), ); } } @@ -121,7 +123,7 @@ class ProfileDetailsRoute extends GoRouteData { return MaterialPage( fullscreenDialog: true, name: name, - child: ProfileDetailPage(id), + child: ProfileDetailsPage(id), ); } } diff --git a/lib/core/widget/custom_alert_dialog.dart b/lib/core/widget/custom_alert_dialog.dart new file mode 100644 index 00000000..ad96ed1f --- /dev/null +++ b/lib/core/widget/custom_alert_dialog.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:hiddify/domain/failures.dart'; + +class CustomAlertDialog extends StatelessWidget { + const CustomAlertDialog({ + super.key, + this.title, + required this.message, + }); + + final String? title; + final String message; + + factory CustomAlertDialog.fromError(PresentableError error) => + CustomAlertDialog( + title: error.message == null ? null : error.type, + message: error.message ?? error.type, + ); + + Future show(BuildContext context) async { + await showDialog( + context: context, + useRootNavigator: true, + builder: (context) => this, + ); + } + + @override + Widget build(BuildContext context) { + final localizations = MaterialLocalizations.of(context); + + return AlertDialog( + title: title != null ? Text(title!) : null, + content: SingleChildScrollView( + child: SizedBox( + width: 468, + child: Text(message), + ), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text(localizations.okButtonLabel), + ), + ], + ); + } +} diff --git a/lib/data/data_providers.dart b/lib/data/data_providers.dart index 8ffd3327..d70bef5b 100644 --- a/lib/data/data_providers.dart +++ b/lib/data/data_providers.dart @@ -4,7 +4,6 @@ import 'package:dio/dio.dart'; import 'package:hiddify/core/core_providers.dart'; import 'package:hiddify/core/prefs/general_prefs.dart'; import 'package:hiddify/data/api/clash_api.dart'; -import 'package:hiddify/data/local/dao/profiles_dao.dart'; import 'package:hiddify/data/local/database.dart'; import 'package:hiddify/data/repository/app_repository_impl.dart'; import 'package:hiddify/data/repository/config_options_store.dart'; @@ -12,9 +11,9 @@ import 'package:hiddify/data/repository/repository.dart'; import 'package:hiddify/domain/app/app.dart'; import 'package:hiddify/domain/constants.dart'; import 'package:hiddify/domain/core_facade.dart'; -import 'package:hiddify/domain/profiles/profiles.dart'; import 'package:hiddify/domain/singbox/singbox.dart'; import 'package:hiddify/features/geo_asset/data/geo_asset_data_providers.dart'; +import 'package:hiddify/features/profile/data/profile_data_providers.dart'; import 'package:hiddify/services/service_providers.dart'; import 'package:native_dio_adapter/native_dio_adapter.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -48,20 +47,6 @@ Dio dio(DioRef ref) { return dio; } -@Riverpod(keepAlive: true) -ProfilesDao profilesDao(ProfilesDaoRef ref) => ProfilesDao( - ref.watch(appDatabaseProvider), - ); - -@Riverpod(keepAlive: true) -ProfilesRepository profilesRepository(ProfilesRepositoryRef ref) => - ProfilesRepositoryImpl( - profilesDao: ref.watch(profilesDaoProvider), - filesEditor: ref.watch(filesEditorServiceProvider), - singbox: ref.watch(coreFacadeProvider), - dio: ref.watch(dioProvider), - ); - @Riverpod(keepAlive: true) AppRepository appRepository(AppRepositoryRef ref) => AppRepositoryImpl(ref.watch(dioProvider)); @@ -99,6 +84,7 @@ CoreFacade coreFacade(CoreFacadeRef ref) => CoreFacadeImpl( ref.watch(singboxServiceProvider), ref.watch(filesEditorServiceProvider), ref.watch(geoAssetPathResolverProvider), + ref.watch(profilePathResolverProvider), ref.watch(platformServicesProvider), ref.watch(clashApiProvider), ref.read(debugModeNotifierProvider), diff --git a/lib/data/local/data_mappers.dart b/lib/data/local/data_mappers.dart deleted file mode 100644 index 0646a749..00000000 --- a/lib/data/local/data_mappers.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'package:drift/drift.dart'; -import 'package:hiddify/data/local/database.dart'; -import 'package:hiddify/domain/profiles/profiles.dart'; - -extension ProfileMapper on Profile { - ProfileEntriesCompanion toCompanion() { - return switch (this) { - RemoteProfile(:final url, :final options, :final subInfo) => - ProfileEntriesCompanion.insert( - id: id, - type: ProfileType.remote, - active: active, - name: name, - url: Value(url), - lastUpdate: lastUpdate, - updateInterval: Value(options?.updateInterval), - upload: Value(subInfo?.upload), - download: Value(subInfo?.download), - total: Value(subInfo?.total), - expire: Value(subInfo?.expire), - webPageUrl: Value(subInfo?.webPageUrl), - supportUrl: Value(subInfo?.supportUrl), - ), - LocalProfile() => ProfileEntriesCompanion.insert( - id: id, - type: ProfileType.local, - active: active, - name: name, - lastUpdate: lastUpdate, - ), - }; - } - - static Profile fromEntry(ProfileEntry e) { - ProfileOptions? options; - if (e.updateInterval != null) { - options = ProfileOptions(updateInterval: e.updateInterval!); - } - - SubscriptionInfo? subInfo; - if (e.upload != null && - e.download != null && - e.total != null && - e.expire != null) { - subInfo = SubscriptionInfo( - upload: e.upload!, - download: e.download!, - total: e.total!, - expire: e.expire!, - webPageUrl: e.webPageUrl, - supportUrl: e.supportUrl, - ); - } - - return switch (e.type) { - ProfileType.remote => RemoteProfile( - id: e.id, - active: e.active, - name: e.name, - url: e.url!, - lastUpdate: e.lastUpdate, - options: options, - subInfo: subInfo, - ), - ProfileType.local => LocalProfile( - id: e.id, - active: e.active, - name: e.name, - lastUpdate: e.lastUpdate, - ), - }; - } -} diff --git a/lib/data/local/database.dart b/lib/data/local/database.dart index f1ecaea9..e18897d6 100644 --- a/lib/data/local/database.dart +++ b/lib/data/local/database.dart @@ -5,10 +5,10 @@ import 'package:drift/native.dart'; import 'package:hiddify/data/local/schema_versions.dart'; import 'package:hiddify/data/local/tables.dart'; import 'package:hiddify/data/local/type_converters.dart'; -import 'package:hiddify/domain/profiles/profiles.dart'; import 'package:hiddify/features/geo_asset/data/geo_asset_data_mapper.dart'; import 'package:hiddify/features/geo_asset/model/default_geo_assets.dart'; import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart'; +import 'package:hiddify/features/profile/model/profile_entity.dart'; import 'package:hiddify/services/files_editor_service.dart'; import 'package:path/path.dart' as p; diff --git a/lib/data/local/tables.dart b/lib/data/local/tables.dart index f8a09291..ab1d9a59 100644 --- a/lib/data/local/tables.dart +++ b/lib/data/local/tables.dart @@ -1,7 +1,7 @@ import 'package:drift/drift.dart'; import 'package:hiddify/data/local/type_converters.dart'; -import 'package:hiddify/domain/profiles/profiles.dart'; import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart'; +import 'package:hiddify/features/profile/model/profile_entity.dart'; @DataClassName('ProfileEntry') class ProfileEntries extends Table { diff --git a/lib/data/repository/core_facade_impl.dart b/lib/data/repository/core_facade_impl.dart index 15bdde09..be953ec1 100644 --- a/lib/data/repository/core_facade_impl.dart +++ b/lib/data/repository/core_facade_impl.dart @@ -11,6 +11,7 @@ import 'package:hiddify/domain/core_facade.dart'; import 'package:hiddify/domain/core_service_failure.dart'; import 'package:hiddify/domain/singbox/singbox.dart'; import 'package:hiddify/features/geo_asset/data/geo_asset_path_resolver.dart'; +import 'package:hiddify/features/profile/data/profile_path_resolver.dart'; import 'package:hiddify/services/files_editor_service.dart'; import 'package:hiddify/services/platform_services.dart'; import 'package:hiddify/services/singbox/singbox_service.dart'; @@ -21,6 +22,7 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { this.singbox, this.filesEditor, this.geoAssetPathResolver, + this.profilePathResolver, this.platformServices, this.clash, this.debug, @@ -30,6 +32,7 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { final SingboxService singbox; final FilesEditorService filesEditor; final GeoAssetPathResolver geoAssetPathResolver; + final ProfilePathResolver profilePathResolver; final PlatformServices platformServices; final ClashApi clash; final bool debug; @@ -115,12 +118,14 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { ) { return TaskEither.Do( ($) async { - final configPath = filesEditor.configPath(fileName); + final configFile = profilePathResolver.file(fileName); final options = await $(_getConfigOptions()); await $(setup()); await $(changeConfigOptions(options)); return await $( - singbox.generateConfig(configPath).mapLeft(CoreServiceFailure.other), + singbox + .generateConfig(configFile.path) + .mapLeft(CoreServiceFailure.other), ); }, ).handleExceptions(CoreServiceFailure.unexpected); @@ -133,7 +138,7 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { ) { return TaskEither.Do( ($) async { - final configPath = filesEditor.configPath(fileName); + final configFile = profilePathResolver.file(fileName); final options = await $(_getConfigOptions()); loggy.info( "config options: ${options.format()}\nMemory Limit: ${!disableMemoryLimit}", @@ -155,7 +160,7 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { await $(changeConfigOptions(options)); return await $( singbox - .start(configPath, disableMemoryLimit) + .start(configFile.path, disableMemoryLimit) .mapLeft(CoreServiceFailure.start), ); }, @@ -177,12 +182,12 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { ) { return exceptionHandler( () async { - final configPath = filesEditor.configPath(fileName); + final configFile = profilePathResolver.file(fileName); return _getConfigOptions() .flatMap((options) => changeConfigOptions(options)) .andThen( () => singbox - .restart(configPath, disableMemoryLimit) + .restart(configFile.path, disableMemoryLimit) .mapLeft(CoreServiceFailure.start), ) .run(); diff --git a/lib/data/repository/repository.dart b/lib/data/repository/repository.dart index 4f454cc4..a5687644 100644 --- a/lib/data/repository/repository.dart +++ b/lib/data/repository/repository.dart @@ -1,2 +1 @@ export 'core_facade_impl.dart'; -export 'profiles_repository_impl.dart'; diff --git a/lib/domain/profiles/profile.dart b/lib/domain/profiles/profile.dart deleted file mode 100644 index c1f826e3..00000000 --- a/lib/domain/profiles/profile.dart +++ /dev/null @@ -1,174 +0,0 @@ -import 'dart:convert'; - -import 'package:dartx/dartx.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:hiddify/utils/utils.dart'; -import 'package:loggy/loggy.dart'; -import 'package:uuid/uuid.dart'; - -part 'profile.freezed.dart'; -part 'profile.g.dart'; - -final _loggy = Loggy('Profile'); - -enum ProfileType { remote, local } - -@freezed -sealed class Profile with _$Profile { - const Profile._(); - - const factory Profile.remote({ - required String id, - required bool active, - required String name, - required String url, - required DateTime lastUpdate, - ProfileOptions? options, - SubscriptionInfo? subInfo, - }) = RemoteProfile; - - const factory Profile.local({ - required String id, - required bool active, - required String name, - required DateTime lastUpdate, - }) = LocalProfile; - - // ignore: prefer_constructors_over_static_methods - static RemoteProfile fromResponse( - String url, - Map> headers, - ) { - _loggy.debug("Profile Headers: $headers"); - - final titleHeader = headers['profile-title']?.single; - var title = ''; - if (titleHeader != null) { - if (titleHeader.startsWith("base64:")) { - // TODO handle errors - title = - utf8.decode(base64.decode(titleHeader.replaceFirst("base64:", ""))); - } else { - title = titleHeader; - } - } - - if (title.isEmpty) { - final contentDisposition = headers['content-disposition']?.single; - if (contentDisposition != null) { - final RegExp regExp = RegExp('filename="([^"]*)"'); - final match = regExp.firstMatch(contentDisposition); - if (match != null && match.groupCount >= 1) { - title = match.group(1) ?? ''; - } - } - } - if (title.isEmpty) { - final part = url.split("#").lastOrNull; - if (part != null) { - title = part; - } - } - if (title.isEmpty) { - final part = url.split("/").lastOrNull; - if (part != null) { - final pattern = RegExp(r"\.(json|yaml|yml|txt)[\s\S]*"); - title = part.replaceFirst(pattern, ""); - } - } - - final updateIntervalHeader = headers['profile-update-interval']?.single; - ProfileOptions? options; - if (updateIntervalHeader != null) { - final updateInterval = Duration(hours: int.parse(updateIntervalHeader)); - options = ProfileOptions(updateInterval: updateInterval); - } - - final subscriptionInfoHeader = headers['subscription-userinfo']?.single; - SubscriptionInfo? subInfo; - if (subscriptionInfoHeader != null) { - subInfo = SubscriptionInfo.fromResponseHeader(subscriptionInfoHeader); - } - - final webPageUrlHeader = headers['profile-web-page-url']?.single; - final supportUrlHeader = headers['support-url']?.single; - if (subInfo != null) { - subInfo = subInfo.copyWith( - webPageUrl: isUrl(webPageUrlHeader ?? "") ? webPageUrlHeader : null, - supportUrl: isUrl(supportUrlHeader ?? "") ? supportUrlHeader : null, - ); - } - - return RemoteProfile( - id: const Uuid().v4(), - active: false, - name: title.isBlank ? "Remote Profile" : title, - url: url, - lastUpdate: DateTime.now(), - options: options, - subInfo: subInfo, - ); - } - - factory Profile.fromJson(Map json) => - _$ProfileFromJson(json); -} - -@freezed -class ProfileOptions with _$ProfileOptions { - const factory ProfileOptions({ - required Duration updateInterval, - }) = _ProfileOptions; - - factory ProfileOptions.fromJson(Map json) => - _$ProfileOptionsFromJson(json); -} - -@freezed -class SubscriptionInfo with _$SubscriptionInfo { - const SubscriptionInfo._(); - - const factory SubscriptionInfo({ - required int upload, - required int download, - @JsonKey(fromJson: _fromJsonTotal, defaultValue: 9223372036854775807) - required int total, - @JsonKey(fromJson: _dateTimeFromSecondsSinceEpoch) required DateTime expire, - String? webPageUrl, - String? supportUrl, - }) = _SubscriptionInfo; - - bool get isExpired => expire <= DateTime.now(); - - int get consumption => upload + download; - - double get ratio => (consumption / total).clamp(0, 1); - - Duration get remaining => expire.difference(DateTime.now()); - - factory SubscriptionInfo.fromResponseHeader(String header) { - final values = header.split(';'); - final map = { - for (final v in values) - v.split('=').first.trim(): - num.tryParse(v.split('=').second.trim())?.toInt(), - }; - _loggy.debug("Subscription Info: $map"); - return SubscriptionInfo.fromJson(map); - } - - factory SubscriptionInfo.fromJson(Map json) => - _$SubscriptionInfoFromJson(json); -} - -int _fromJsonTotal(dynamic total) { - final totalInt = total as int? ?? -1; - return totalInt > 0 ? totalInt : 9223372036854775807; -} - -DateTime _dateTimeFromSecondsSinceEpoch(dynamic expire) { - final expireInt = expire as int? ?? -1; - return DateTime.fromMillisecondsSinceEpoch( - (expireInt > 0 ? expireInt : 92233720368) * 1000, - ); -} diff --git a/lib/domain/profiles/profiles.dart b/lib/domain/profiles/profiles.dart deleted file mode 100644 index fb63afe8..00000000 --- a/lib/domain/profiles/profiles.dart +++ /dev/null @@ -1,4 +0,0 @@ -export 'profile.dart'; -export 'profile_enums.dart'; -export 'profiles_failure.dart'; -export 'profiles_repository.dart'; diff --git a/lib/domain/profiles/profiles_repository.dart b/lib/domain/profiles/profiles_repository.dart deleted file mode 100644 index 7476d2a2..00000000 --- a/lib/domain/profiles/profiles_repository.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:fpdart/fpdart.dart'; -import 'package:hiddify/domain/enums.dart'; -import 'package:hiddify/domain/profiles/profiles.dart'; - -abstract class ProfilesRepository { - TaskEither get(String id); - - Stream> watchActiveProfile(); - - Stream> watchHasAnyProfile(); - - Stream>> watchAll({ - ProfilesSort sort = ProfilesSort.lastUpdate, - SortMode mode = SortMode.ascending, - }); - - TaskEither addByUrl( - String url, { - bool markAsActive = false, - }); - - TaskEither addByContent( - String content, { - required String name, - bool markAsActive = false, - }); - - TaskEither add(RemoteProfile baseProfile); - - TaskEither update(RemoteProfile baseProfile); - - TaskEither edit(Profile profile); - - TaskEither setAsActive(String id); - - TaskEither delete(String id); -} diff --git a/lib/features/common/active_profile/active_profile_notifier.dart b/lib/features/common/active_profile/active_profile_notifier.dart deleted file mode 100644 index af11965d..00000000 --- a/lib/features/common/active_profile/active_profile_notifier.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:hiddify/data/data_providers.dart'; -import 'package:hiddify/domain/profiles/profiles.dart'; -import 'package:hiddify/utils/utils.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'active_profile_notifier.g.dart'; - -@Riverpod(keepAlive: true) -class ActiveProfile extends _$ActiveProfile with AppLogger { - @override - Stream build() { - loggy.debug("watching active profile"); - return ref - .watch(profilesRepositoryProvider) - .watchActiveProfile() - .map((event) => event.getOrElse((l) => throw l)); - } -} diff --git a/lib/features/common/active_profile/has_any_profile_notifier.dart b/lib/features/common/active_profile/has_any_profile_notifier.dart deleted file mode 100644 index 8ac28b21..00000000 --- a/lib/features/common/active_profile/has_any_profile_notifier.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:hiddify/data/data_providers.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'has_any_profile_notifier.g.dart'; - -@Riverpod(keepAlive: true) -Stream hasAnyProfile( - HasAnyProfileRef ref, -) { - return ref - .watch(profilesRepositoryProvider) - .watchHasAnyProfile() - .map((event) => event.getOrElse((l) => throw l)); -} diff --git a/lib/features/common/app_update_notifier.dart b/lib/features/common/app_update_notifier.dart index 669c718a..a3a39092 100644 --- a/lib/features/common/app_update_notifier.dart +++ b/lib/features/common/app_update_notifier.dart @@ -100,26 +100,4 @@ class AppUpdateNotifier extends _$AppUpdateNotifier with AppLogger { await _ignoreReleasePref.update(versionInfo.version); state = AppUpdateStateIgnored(versionInfo); } - - // Future _schedule() async { - // loggy.debug("scheduling app update checker"); - // return ref.read(cronServiceProvider).schedule( - // key: 'app_update', - // duration: const Duration(hours: 8), - // callback: () async { - // await Future.delayed(const Duration(seconds: 5)); - // final updateState = await check(); - // final context = rootNavigatorKey.currentContext; - // if (context != null && context.mounted) { - // if (updateState - // case AppUpdateStateAvailable(:final versionInfo)) { - // await NewVersionDialog( - // ref.read(appInfoProvider).presentVersion, - // versionInfo, - // ).show(context); - // } - // } - // }, - // ); - // } } diff --git a/lib/features/common/common_controllers.dart b/lib/features/common/common_controllers.dart index 3854b879..a8172a49 100644 --- a/lib/features/common/common_controllers.dart +++ b/lib/features/common/common_controllers.dart @@ -1,9 +1,8 @@ import 'package:hiddify/core/prefs/general_prefs.dart'; import 'package:hiddify/features/common/connectivity/connectivity_controller.dart'; import 'package:hiddify/features/common/window/window_controller.dart'; -import 'package:hiddify/features/profiles/notifier/notifier.dart'; +import 'package:hiddify/features/profile/notifier/profiles_update_notifier.dart'; import 'package:hiddify/features/system_tray/system_tray_controller.dart'; -import 'package:hiddify/services/service_providers.dart'; import 'package:hiddify/utils/platform_utils.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -17,7 +16,7 @@ void commonControllers(CommonControllersRef ref) { introCompletedProvider, (_, completed) async { if (completed) { - await ref.read(cronServiceProvider).startScheduler(); + await ref.read(foregroundProfilesUpdateNotifierProvider.future); } }, fireImmediately: true, @@ -27,11 +26,6 @@ void commonControllers(CommonControllersRef ref) { (previous, next) {}, fireImmediately: true, ); - ref.listen( - profilesUpdateNotifierProvider, - (previous, next) {}, - fireImmediately: true, - ); if (PlatformUtils.isDesktop) { ref.listen( windowControllerProvider, diff --git a/lib/features/common/connectivity/connectivity_controller.dart b/lib/features/common/connectivity/connectivity_controller.dart index cea73393..8f835b6b 100644 --- a/lib/features/common/connectivity/connectivity_controller.dart +++ b/lib/features/common/connectivity/connectivity_controller.dart @@ -3,7 +3,7 @@ import 'package:hiddify/core/prefs/service_prefs.dart'; import 'package:hiddify/data/data_providers.dart'; import 'package:hiddify/domain/connectivity/connectivity.dart'; import 'package:hiddify/domain/core_facade.dart'; -import 'package:hiddify/features/common/active_profile/active_profile_notifier.dart'; +import 'package:hiddify/features/profile/notifier/active_profile_notifier.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:rxdart/rxdart.dart'; @@ -25,7 +25,7 @@ class ConnectivityController extends _$ConnectivityController with AppLogger { }, ); return _core.watchConnectionStatus().doOnData((event) { - if (event case Disconnected(:final connectionFailure?) + if (event case Disconnected(connectionFailure: final _?) when PlatformUtils.isDesktop) { ref.read(startedByUserProvider.notifier).update(false); } diff --git a/lib/features/home/view/home_page.dart b/lib/features/home/view/home_page.dart index e3ed99ba..86512ad8 100644 --- a/lib/features/home/view/home_page.dart +++ b/lib/features/home/view/home_page.dart @@ -3,11 +3,10 @@ import 'package:flutter/material.dart'; import 'package:hiddify/core/core_providers.dart'; import 'package:hiddify/core/router/router.dart'; import 'package:hiddify/domain/failures.dart'; -import 'package:hiddify/features/common/active_profile/active_profile_notifier.dart'; -import 'package:hiddify/features/common/active_profile/has_any_profile_notifier.dart'; import 'package:hiddify/features/common/nested_app_bar.dart'; -import 'package:hiddify/features/common/profile_tile.dart'; import 'package:hiddify/features/home/widgets/widgets.dart'; +import 'package:hiddify/features/profile/notifier/active_profile_notifier.dart'; +import 'package:hiddify/features/profile/widget/profile_tile.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; diff --git a/lib/features/profiles/view/add_profile_modal.dart b/lib/features/profile/add/add_profile_modal.dart similarity index 80% rename from lib/features/profiles/view/add_profile_modal.dart rename to lib/features/profile/add/add_profile_modal.dart index a0ce2ecb..11217720 100644 --- a/lib/features/profiles/view/add_profile_modal.dart +++ b/lib/features/profile/add/add_profile_modal.dart @@ -5,10 +5,8 @@ import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; import 'package:hiddify/core/core_providers.dart'; import 'package:hiddify/core/router/router.dart'; -import 'package:hiddify/domain/failures.dart'; -import 'package:hiddify/domain/profiles/profiles.dart'; import 'package:hiddify/features/common/qr_code_scanner_screen.dart'; -import 'package:hiddify/features/profiles/notifier/notifier.dart'; +import 'package:hiddify/features/profile/notifier/profile_notifier.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -25,40 +23,26 @@ class AddProfileModal extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final t = ref.watch(translationsProvider); + final addProfileState = ref.watch(addProfileProvider); - final mutationTriggered = useState(false); - final addProfileMutation = useMutation( - initialOnFailure: (err) { - mutationTriggered.value = false; - if (err case ProfileInvalidUrlFailure()) { - CustomToast.error( - t.failure.profiles.invalidUrl, - ).show(context); - } else { - CustomAlertDialog.fromErr( - t.presentError(err, action: t.profile.add.failureMsg), - ).show(context); + ref.listen( + addProfileProvider, + (previous, next) { + if (next case AsyncData(value: final _?)) { + WidgetsBinding.instance.addPostFrameCallback( + (_) { + if (context.mounted) context.pop(); + }, + ); } }, - initialOnSuccess: () { - CustomToast.success(t.profile.save.successMsg).show(context); - WidgetsBinding.instance.addPostFrameCallback( - (_) { - if (context.mounted) context.pop(); - }, - ); - }, ); - final showProgressIndicator = - addProfileMutation.state.isInProgress || mutationTriggered.value; - useMemoized(() async { await Future.delayed(const Duration(milliseconds: 200)); if (url != null && context.mounted) { - addProfileMutation.setFuture( - ref.read(profilesNotifierProvider.notifier).addProfile(url!), - ); + if (addProfileState.isLoading) return; + ref.read(addProfileProvider.notifier).add(url!); } }); @@ -112,13 +96,10 @@ class AddProfileModal extends HookConsumerWidget { final captureResult = await Clipboard.getData(Clipboard.kTextPlain) .then((value) => value?.text ?? ''); - if (addProfileMutation.state.isInProgress) return; - mutationTriggered.value = true; - addProfileMutation.setFuture( - ref - .read(profilesNotifierProvider.notifier) - .addProfile(captureResult), - ); + if (addProfileState.isLoading) return; + ref + .read(addProfileProvider.notifier) + .add(captureResult); }, ), const Gap(buttonsGap), @@ -133,15 +114,10 @@ class AddProfileModal extends HookConsumerWidget { await const QRCodeScannerScreen() .open(context); if (captureResult == null) return; - if (addProfileMutation.state.isInProgress) { - return; - } - mutationTriggered.value = true; - addProfileMutation.setFuture( - ref - .read(profilesNotifierProvider.notifier) - .addProfile(captureResult), - ); + if (addProfileState.isLoading) return; + ref + .read(addProfileProvider.notifier) + .add(captureResult); }, ) else @@ -205,7 +181,7 @@ class AddProfileModal extends HookConsumerWidget { const Gap(24), ], ), - crossFadeState: showProgressIndicator + crossFadeState: addProfileState.isLoading ? CrossFadeState.showFirst : CrossFadeState.showSecond, duration: const Duration(milliseconds: 250), diff --git a/lib/features/profile/data/profile_data_mapper.dart b/lib/features/profile/data/profile_data_mapper.dart new file mode 100644 index 00000000..38d15061 --- /dev/null +++ b/lib/features/profile/data/profile_data_mapper.dart @@ -0,0 +1,85 @@ +import 'package:drift/drift.dart'; +import 'package:hiddify/data/local/database.dart'; +import 'package:hiddify/features/profile/model/profile_entity.dart'; + +extension ProfileEntityMapper on ProfileEntity { + ProfileEntriesCompanion toEntry() { + return switch (this) { + RemoteProfileEntity(:final url, :final options, :final subInfo) => + ProfileEntriesCompanion.insert( + id: id, + type: ProfileType.remote, + active: active, + name: name, + url: Value(url), + lastUpdate: lastUpdate, + updateInterval: Value(options?.updateInterval), + upload: Value(subInfo?.upload), + download: Value(subInfo?.download), + total: Value(subInfo?.total), + expire: Value(subInfo?.expire), + webPageUrl: Value(subInfo?.webPageUrl), + supportUrl: Value(subInfo?.supportUrl), + ), + LocalProfileEntity() => ProfileEntriesCompanion.insert( + id: id, + type: ProfileType.local, + active: active, + name: name, + lastUpdate: lastUpdate, + ), + }; + } +} + +extension RemoteProfileEntityMapper on RemoteProfileEntity { + ProfileEntriesCompanion subInfoPatch() { + return ProfileEntriesCompanion( + upload: Value(subInfo?.upload), + download: Value(subInfo?.download), + total: Value(subInfo?.total), + expire: Value(subInfo?.expire), + webPageUrl: Value(subInfo?.webPageUrl), + supportUrl: Value(subInfo?.supportUrl), + ); + } +} + +extension ProfileEntryMapper on ProfileEntry { + ProfileEntity toEntity() { + ProfileOptions? options; + if (updateInterval != null) { + options = ProfileOptions(updateInterval: updateInterval!); + } + + SubscriptionInfo? subInfo; + if (upload != null && download != null && total != null && expire != null) { + subInfo = SubscriptionInfo( + upload: upload!, + download: download!, + total: total!, + expire: expire!, + webPageUrl: webPageUrl, + supportUrl: supportUrl, + ); + } + + return switch (type) { + ProfileType.remote => RemoteProfileEntity( + id: id, + active: active, + name: name, + url: url!, + lastUpdate: lastUpdate, + options: options, + subInfo: subInfo, + ), + ProfileType.local => LocalProfileEntity( + id: id, + active: active, + name: name, + lastUpdate: lastUpdate, + ), + }; + } +} diff --git a/lib/features/profile/data/profile_data_providers.dart b/lib/features/profile/data/profile_data_providers.dart new file mode 100644 index 00000000..5ff0f515 --- /dev/null +++ b/lib/features/profile/data/profile_data_providers.dart @@ -0,0 +1,32 @@ +import 'package:hiddify/data/data_providers.dart'; +import 'package:hiddify/features/profile/data/profile_data_source.dart'; +import 'package:hiddify/features/profile/data/profile_path_resolver.dart'; +import 'package:hiddify/features/profile/data/profile_repository.dart'; +import 'package:hiddify/services/service_providers.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'profile_data_providers.g.dart'; + +@Riverpod(keepAlive: true) +Future profileRepository(ProfileRepositoryRef ref) async { + final repo = ProfileRepositoryImpl( + profileDataSource: ref.watch(profileDataSourceProvider), + profilePathResolver: ref.watch(profilePathResolverProvider), + configValidator: ref.watch(coreFacadeProvider).parseConfig, + dio: ref.watch(dioProvider), + ); + await repo.init().getOrElse((l) => throw l).run(); + return repo; +} + +@Riverpod(keepAlive: true) +ProfileDataSource profileDataSource(ProfileDataSourceRef ref) { + return ProfileDao(ref.watch(appDatabaseProvider)); +} + +@Riverpod(keepAlive: true) +ProfilePathResolver profilePathResolver(ProfilePathResolverRef ref) { + return ProfilePathResolver( + ref.watch(filesEditorServiceProvider).dirs.workingDir, + ); +} diff --git a/lib/data/local/dao/profiles_dao.dart b/lib/features/profile/data/profile_data_source.dart similarity index 57% rename from lib/data/local/dao/profiles_dao.dart rename to lib/features/profile/data/profile_data_source.dart index 3d41c934..a5f6b241 100644 --- a/lib/data/local/dao/profiles_dao.dart +++ b/lib/features/profile/data/profile_data_source.dart @@ -1,12 +1,24 @@ import 'package:drift/drift.dart'; -import 'package:hiddify/data/local/data_mappers.dart'; import 'package:hiddify/data/local/database.dart'; import 'package:hiddify/data/local/tables.dart'; -import 'package:hiddify/domain/enums.dart'; -import 'package:hiddify/domain/profiles/profiles.dart'; +import 'package:hiddify/features/profile/model/profile_sort_enum.dart'; import 'package:hiddify/utils/utils.dart'; -part 'profiles_dao.g.dart'; +part 'profile_data_source.g.dart'; + +abstract interface class ProfileDataSource { + Future getById(String id); + Future getByUrl(String url); + Stream watchActiveProfile(); + Stream watchProfilesCount(); + Stream> watchAll({ + required ProfilesSort sort, + required SortMode sortMode, + }); + Future insert(ProfileEntriesCompanion entry); + Future edit(String id, ProfileEntriesCompanion entry); + Future deleteById(String id); +} Map orderMap = { SortMode.ascending: OrderingMode.asc, @@ -14,41 +26,45 @@ Map orderMap = { }; @DriftAccessor(tables: [ProfileEntries]) -class ProfilesDao extends DatabaseAccessor - with _$ProfilesDaoMixin, InfraLogger { - ProfilesDao(super.db); +class ProfileDao extends DatabaseAccessor + with _$ProfileDaoMixin, InfraLogger + implements ProfileDataSource { + ProfileDao(super.db); - Future getById(String id) async { + @override + Future getById(String id) async { return (profileEntries.select()..where((tbl) => tbl.id.equals(id))) - .map(ProfileMapper.fromEntry) .getSingleOrNull(); } - Future getProfileByUrl(String url) async { - return (select(profileEntries)..where((tbl) => tbl.url.like('%$url%'))) - .map(ProfileMapper.fromEntry) - .get() - .then((value) => value.firstOrNull); + @override + Future getByUrl(String url) async { + return (select(profileEntries) + ..where((tbl) => tbl.url.like('%$url%')) + ..limit(1)) + .getSingleOrNull(); } - Stream watchActiveProfile() { + @override + Stream watchActiveProfile() { return (profileEntries.select() ..where((tbl) => tbl.active.equals(true)) ..limit(1)) - .map(ProfileMapper.fromEntry) .watchSingleOrNull(); } - Stream watchProfileCount() { + @override + Stream watchProfilesCount() { final count = profileEntries.id.count(); return (profileEntries.selectOnly()..addColumns([count])) .map((exp) => exp.read(count)!) .watchSingle(); } - Stream> watchAll({ - ProfilesSort sort = ProfilesSort.lastUpdate, - SortMode mode = SortMode.ascending, + @override + Stream> watchAll({ + required ProfilesSort sort, + required SortMode sortMode, }) { return (profileEntries.select() ..orderBy( @@ -67,56 +83,47 @@ class ProfilesDao extends DatabaseAccessor switch (sort) { ProfilesSort.name => (tbl) => OrderingTerm( expression: tbl.name, - mode: orderMap[mode]!, + mode: orderMap[sortMode]!, ), ProfilesSort.lastUpdate => (tbl) => OrderingTerm( expression: tbl.lastUpdate, - mode: orderMap[mode]!, + mode: orderMap[sortMode]!, ), }, ], )) - .map(ProfileMapper.fromEntry) .watch(); } - Future create(Profile profile) async { + @override + Future insert(ProfileEntriesCompanion entry) async { await transaction( () async { - if (profile.active) { + if (entry.active.present && entry.active.value) { await update(profileEntries) .write(const ProfileEntriesCompanion(active: Value(false))); } - await into(profileEntries).insert(profile.toCompanion()); + await into(profileEntries).insert(entry); }, ); } - Future edit(Profile patch) async { + @override + Future edit(String id, ProfileEntriesCompanion entry) async { await transaction( () async { - if (patch.active) { + if (entry.active.present && entry.active.value) { await update(profileEntries) .write(const ProfileEntriesCompanion(active: Value(false))); } - await (update(profileEntries)..where((tbl) => tbl.id.equals(patch.id))) - .write(patch.toCompanion()); - }, - ); - } - - Future setAsActive(String id) async { - await transaction( - () async { - await update(profileEntries) - .write(const ProfileEntriesCompanion(active: Value(false))); await (update(profileEntries)..where((tbl) => tbl.id.equals(id))) - .write(const ProfileEntriesCompanion(active: Value(true))); + .write(entry); }, ); } - Future removeById(String id) async { + @override + Future deleteById(String id) async { await transaction( () async { await (delete(profileEntries)..where((tbl) => tbl.id.equals(id))).go(); diff --git a/lib/features/profile/data/profile_parser.dart b/lib/features/profile/data/profile_parser.dart new file mode 100644 index 00000000..5496658b --- /dev/null +++ b/lib/features/profile/data/profile_parser.dart @@ -0,0 +1,105 @@ +import 'dart:convert'; + +import 'package:dartx/dartx.dart'; +import 'package:hiddify/features/profile/model/profile_entity.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:uuid/uuid.dart'; + +/// parse profile subscription url and headers for data +/// +/// ***name parser hierarchy:*** +/// - `profile-title` header +/// - `content-disposition` header +/// - url fragment (example: `https://example.com/config#user`) -> name=`user` +/// - url filename extension (example: `https://example.com/config.json`) -> name=`config` +/// - if none of these methods return a non-blank string, fallback to `Remote Profile` +abstract class ProfileParser { + static RemoteProfileEntity parse( + String url, + Map> headers, + ) { + var name = ''; + if (headers['profile-title'] case [final titleHeader]) { + if (titleHeader.startsWith("base64:")) { + name = + utf8.decode(base64.decode(titleHeader.replaceFirst("base64:", ""))); + } else { + name = titleHeader.trim(); + } + } + if (headers['content-disposition'] case [final contentDispositionHeader] + when name.isEmpty) { + final regExp = RegExp('filename="([^"]*)"'); + final match = regExp.firstMatch(contentDispositionHeader); + if (match != null && match.groupCount >= 1) { + name = match.group(1) ?? ''; + } + } + if (Uri.parse(url).fragment case final fragment when name.isEmpty) { + name = fragment; + } + if (url.split("/").lastOrNull case final part? when name.isEmpty) { + final pattern = RegExp(r"\.(json|yaml|yml|txt)[\s\S]*"); + name = part.replaceFirst(pattern, ""); + } + if (name.isBlank) name = "Remote Profile"; + + ProfileOptions? options; + if (headers['profile-update-interval'] case [final updateIntervalStr]) { + final updateInterval = Duration(hours: int.parse(updateIntervalStr)); + options = ProfileOptions(updateInterval: updateInterval); + } + + SubscriptionInfo? subInfo; + if (headers['subscription-userinfo'] case [final subInfoStr]) { + subInfo = parseSubscriptionInfo(subInfoStr); + } + + if (subInfo != null) { + if (headers['profile-web-page-url'] case [final profileWebPageUrl] + when isUrl(profileWebPageUrl)) { + subInfo = subInfo.copyWith(webPageUrl: profileWebPageUrl); + } + if (headers['support-url'] case [final profileSupportUrl] + when isUrl(profileSupportUrl)) { + subInfo = subInfo.copyWith(supportUrl: profileSupportUrl); + } + } + + return RemoteProfileEntity( + id: const Uuid().v4(), + active: false, + name: name, + url: url, + lastUpdate: DateTime.now(), + options: options, + subInfo: subInfo, + ); + } + + static SubscriptionInfo? parseSubscriptionInfo(String subInfoStr) { + final values = subInfoStr.split(';'); + final map = { + for (final v in values) + v.split('=').first.trim(): + num.tryParse(v.split('=').second.trim())?.toInt(), + }; + if (map + case { + "upload": final upload?, + "download": final download?, + "total": final total, + "expire": final expire + }) { + return SubscriptionInfo( + upload: upload, + download: download, + total: total ?? 9223372036854775807, + expire: DateTime.fromMillisecondsSinceEpoch( + (expire ?? 92233720368) * 1000, + ), + ); + } + return null; + } +} diff --git a/lib/features/profile/data/profile_path_resolver.dart b/lib/features/profile/data/profile_path_resolver.dart new file mode 100644 index 00000000..ea340344 --- /dev/null +++ b/lib/features/profile/data/profile_path_resolver.dart @@ -0,0 +1,17 @@ +import 'dart:io'; + +import 'package:path/path.dart' as p; + +class ProfilePathResolver { + const ProfilePathResolver(this._workingDir); + + final Directory _workingDir; + + Directory get directory => Directory(p.join(_workingDir.path, "configs")); + + File file(String fileName) { + return File(p.join(directory.path, "$fileName.json")); + } + + File tempFile(String fileName) => file("$fileName.tmp"); +} diff --git a/lib/data/repository/profiles_repository_impl.dart b/lib/features/profile/data/profile_repository.dart similarity index 53% rename from lib/data/repository/profiles_repository_impl.dart rename to lib/features/profile/data/profile_repository.dart index d73eb585..4a099e1a 100644 --- a/lib/data/repository/profiles_repository_impl.dart +++ b/lib/features/profile/data/profile_repository.dart @@ -1,44 +1,103 @@ import 'dart:io'; import 'package:dio/dio.dart'; +import 'package:drift/drift.dart'; import 'package:fpdart/fpdart.dart'; -import 'package:hiddify/data/local/dao/profiles_dao.dart'; +import 'package:hiddify/data/local/database.dart'; import 'package:hiddify/data/repository/exception_handlers.dart'; -import 'package:hiddify/domain/enums.dart'; -import 'package:hiddify/domain/profiles/profiles.dart'; -import 'package:hiddify/domain/singbox/singbox.dart'; -import 'package:hiddify/services/files_editor_service.dart'; -import 'package:hiddify/utils/utils.dart'; +import 'package:hiddify/domain/core_service_failure.dart'; +import 'package:hiddify/features/profile/data/profile_data_mapper.dart'; +import 'package:hiddify/features/profile/data/profile_data_source.dart'; +import 'package:hiddify/features/profile/data/profile_parser.dart'; +import 'package:hiddify/features/profile/data/profile_path_resolver.dart'; +import 'package:hiddify/features/profile/model/profile_entity.dart'; +import 'package:hiddify/features/profile/model/profile_failure.dart'; +import 'package:hiddify/features/profile/model/profile_sort_enum.dart'; +import 'package:hiddify/utils/custom_loggers.dart'; +import 'package:hiddify/utils/link_parsers.dart'; import 'package:meta/meta.dart'; import 'package:retry/retry.dart'; import 'package:uuid/uuid.dart'; -class ProfilesRepositoryImpl +abstract interface class ProfileRepository { + TaskEither init(); + TaskEither getById(String id); + Stream> watchActiveProfile(); + Stream> watchHasAnyProfile(); + + Stream>> watchAll({ + ProfilesSort sort = ProfilesSort.lastUpdate, + SortMode sortMode = SortMode.ascending, + }); + + TaskEither addByUrl( + String url, { + bool markAsActive = false, + }); + + TaskEither addByContent( + String content, { + required String name, + bool markAsActive = false, + }); + + TaskEither add(RemoteProfileEntity baseProfile); + + TaskEither updateSubscription( + RemoteProfileEntity baseProfile, + ); + + TaskEither patch(ProfileEntity profile); + TaskEither setAsActive(String id); + TaskEither deleteById(String id); +} + +class ProfileRepositoryImpl with ExceptionHandler, InfraLogger - implements ProfilesRepository { - ProfilesRepositoryImpl({ - required this.profilesDao, - required this.filesEditor, - required this.singbox, + implements ProfileRepository { + ProfileRepositoryImpl({ + required this.profileDataSource, + required this.profilePathResolver, + required this.configValidator, required this.dio, }); - final ProfilesDao profilesDao; - final FilesEditorService filesEditor; - final SingboxFacade singbox; + final ProfileDataSource profileDataSource; + final ProfilePathResolver profilePathResolver; + final TaskEither Function( + String path, + String tempPath, + bool debug, + ) configValidator; final Dio dio; @override - TaskEither get(String id) { - return TaskEither.tryCatch( - () => profilesDao.getById(id), + TaskEither init() { + return exceptionHandler( + () async { + if (!await profilePathResolver.directory.exists()) { + await profilePathResolver.directory.create(recursive: true); + } + return right(unit); + }, ProfileUnexpectedFailure.new, ); } @override - Stream> watchActiveProfile() { - return profilesDao.watchActiveProfile().handleExceptions( + TaskEither getById(String id) { + return TaskEither.tryCatch( + () => profileDataSource.getById(id).then((value) => value?.toEntity()), + ProfileUnexpectedFailure.new, + ); + } + + @override + Stream> watchActiveProfile() { + return profileDataSource + .watchActiveProfile() + .map((event) => event?.toEntity()) + .handleExceptions( (error, stackTrace) { loggy.error("error watching active profile", error, stackTrace); return ProfileUnexpectedFailure(error, stackTrace); @@ -48,19 +107,20 @@ class ProfilesRepositoryImpl @override Stream> watchHasAnyProfile() { - return profilesDao - .watchProfileCount() + return profileDataSource + .watchProfilesCount() .map((event) => event != 0) .handleExceptions(ProfileUnexpectedFailure.new); } @override - Stream>> watchAll({ + Stream>> watchAll({ ProfilesSort sort = ProfilesSort.lastUpdate, - SortMode mode = SortMode.ascending, + SortMode sortMode = SortMode.ascending, }) { - return profilesDao - .watchAll(sort: sort, mode: mode) + return profileDataSource + .watchAll(sort: sort, sortMode: sortMode) + .map((event) => event.map((e) => e.toEntity()).toList()) .handleExceptions(ProfileUnexpectedFailure.new); } @@ -71,13 +131,15 @@ class ProfilesRepositoryImpl }) { return exceptionHandler( () async { - final existingProfile = await profilesDao.getProfileByUrl(url); - if (existingProfile case RemoteProfile()) { + final existingProfile = await profileDataSource + .getByUrl(url) + .then((value) => value?.toEntity()); + if (existingProfile case RemoteProfileEntity()) { loggy.info("profile with same url already exists, updating"); final baseProfile = markAsActive ? existingProfile.copyWith(active: true) : existingProfile; - return update(baseProfile).run(); + return updateSubscription(baseProfile).run(); } final profileId = const Uuid().v4(); @@ -85,11 +147,10 @@ class ProfilesRepositoryImpl .flatMap( (profile) => TaskEither( () async { - await profilesDao.create( - profile.copyWith( - id: profileId, - active: markAsActive, - ), + await profileDataSource.insert( + profile + .copyWith(id: profileId, active: markAsActive) + .toEntry(), ); return right(unit); }, @@ -113,30 +174,31 @@ class ProfilesRepositoryImpl return exceptionHandler( () async { final profileId = const Uuid().v4(); - final tempPath = filesEditor.tempConfigPath(profileId); - final path = filesEditor.configPath(profileId); + final file = profilePathResolver.file(profileId); + final tempFile = profilePathResolver.tempFile(profileId); + try { - await File(tempPath).writeAsString(content); + await tempFile.writeAsString(content); final parseResult = - await singbox.parseConfig(path, tempPath, false).run(); + await configValidator(file.path, tempFile.path, false).run(); return parseResult.fold( (err) async { loggy.warning("error parsing config", err); return left(ProfileFailure.invalidConfig(err.msg)); }, (_) async { - final profile = LocalProfile( + final profile = LocalProfileEntity( id: profileId, active: markAsActive, name: name, lastUpdate: DateTime.now(), ); - await profilesDao.create(profile); + await profileDataSource.insert(profile.toEntry()); return right(unit); }, ); } finally { - if (File(tempPath).existsSync()) File(tempPath).deleteSync(); + if (tempFile.existsSync()) tempFile.deleteSync(); } }, (error, stackTrace) { @@ -147,17 +209,19 @@ class ProfilesRepositoryImpl } @override - TaskEither add(RemoteProfile baseProfile) { + TaskEither add(RemoteProfileEntity baseProfile) { return exceptionHandler( () async { return fetch(baseProfile.url, baseProfile.id) .flatMap( (remoteProfile) => TaskEither(() async { - await profilesDao.create( - baseProfile.copyWith( - subInfo: remoteProfile.subInfo, - lastUpdate: DateTime.now(), - ), + await profileDataSource.insert( + baseProfile + .copyWith( + subInfo: remoteProfile.subInfo, + lastUpdate: DateTime.now(), + ) + .toEntry(), ); return right(unit); }), @@ -172,7 +236,9 @@ class ProfilesRepositoryImpl } @override - TaskEither update(RemoteProfile baseProfile) { + TaskEither updateSubscription( + RemoteProfileEntity baseProfile, + ) { return exceptionHandler( () async { loggy.debug( @@ -181,11 +247,11 @@ class ProfilesRepositoryImpl return fetch(baseProfile.url, baseProfile.id) .flatMap( (remoteProfile) => TaskEither(() async { - await profilesDao.edit( - baseProfile.copyWith( - subInfo: remoteProfile.subInfo, - lastUpdate: DateTime.now(), - ), + await profileDataSource.edit( + baseProfile.id, + remoteProfile + .subInfoPatch() + .copyWith(lastUpdate: Value(DateTime.now())), ); return right(unit); }), @@ -200,13 +266,13 @@ class ProfilesRepositoryImpl } @override - TaskEither edit(Profile profile) { + TaskEither patch(ProfileEntity profile) { return exceptionHandler( () async { loggy.debug( "editing profile [${profile.name} (${profile.id})]", ); - await profilesDao.edit(profile); + await profileDataSource.edit(profile.id, profile.toEntry()); return right(unit); }, (error, stackTrace) { @@ -220,7 +286,10 @@ class ProfilesRepositoryImpl TaskEither setAsActive(String id) { return TaskEither.tryCatch( () async { - await profilesDao.setAsActive(id); + await profileDataSource.edit( + id, + const ProfileEntriesCompanion(active: Value(true)), + ); return unit; }, ProfileUnexpectedFailure.new, @@ -228,11 +297,11 @@ class ProfilesRepositoryImpl } @override - TaskEither delete(String id) { + TaskEither deleteById(String id) { return TaskEither.tryCatch( () async { - await profilesDao.removeById(id); - await filesEditor.deleteConfig(id); + await profileDataSource.deleteById(id); + await profilePathResolver.file(id).delete(); return unit; }, ProfileUnexpectedFailure.new, @@ -249,35 +318,35 @@ class ProfilesRepositoryImpl ]; @visibleForTesting - TaskEither fetch( + TaskEither fetch( String url, String fileName, ) { return TaskEither( () async { - final tempPath = filesEditor.tempConfigPath(fileName); - final path = filesEditor.configPath(fileName); + final file = profilePathResolver.file(fileName); + final tempFile = profilePathResolver.tempFile(fileName); try { final response = await retry( - () async => dio.download(url.trim(), tempPath), + () async => dio.download(url.trim(), tempFile.path), maxAttempts: 3, ); final headers = - await _populateHeaders(response.headers.map, tempPath); + await _populateHeaders(response.headers.map, tempFile.path); final parseResult = - await singbox.parseConfig(path, tempPath, false).run(); + await configValidator(file.path, tempFile.path, false).run(); return parseResult.fold( (err) async { loggy.warning("error parsing config", err); return left(ProfileFailure.invalidConfig(err.msg)); }, (_) async { - final profile = Profile.fromResponse(url, headers); + final profile = ProfileParser.parse(url, headers); return right(profile); }, ); } finally { - if (File(tempPath).existsSync()) File(tempPath).deleteSync(); + if (tempFile.existsSync()) tempFile.deleteSync(); } }, ); diff --git a/lib/features/profile_detail/notifier/profile_detail_notifier.dart b/lib/features/profile/details/profile_details_notifier.dart similarity index 60% rename from lib/features/profile_detail/notifier/profile_detail_notifier.dart rename to lib/features/profile/details/profile_details_notifier.dart index 6397164e..6cd5df65 100644 --- a/lib/features/profile_detail/notifier/profile_detail_notifier.dart +++ b/lib/features/profile/details/profile_details_notifier.dart @@ -1,25 +1,27 @@ import 'package:dartx/dartx.dart'; import 'package:fpdart/fpdart.dart'; -import 'package:hiddify/data/data_providers.dart'; -import 'package:hiddify/domain/profiles/profiles.dart'; -import 'package:hiddify/features/profile_detail/notifier/profile_detail_state.dart'; +import 'package:hiddify/features/profile/data/profile_data_providers.dart'; +import 'package:hiddify/features/profile/data/profile_repository.dart'; +import 'package:hiddify/features/profile/details/profile_details_state.dart'; +import 'package:hiddify/features/profile/model/profile_entity.dart'; +import 'package:hiddify/features/profile/model/profile_failure.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:uuid/uuid.dart'; -part 'profile_detail_notifier.g.dart'; +part 'profile_details_notifier.g.dart'; @riverpod -class ProfileDetailNotifier extends _$ProfileDetailNotifier with AppLogger { +class ProfileDetailsNotifier extends _$ProfileDetailsNotifier with AppLogger { @override - Future build( + Future build( String id, { String? url, String? profileName, }) async { if (id == 'new') { - return ProfileDetailState( - profile: RemoteProfile( + return ProfileDetailsState( + profile: RemoteProfileEntity( id: const Uuid().v4(), active: true, name: profileName ?? "", @@ -28,7 +30,7 @@ class ProfileDetailNotifier extends _$ProfileDetailNotifier with AppLogger { ), ); } - final failureOrProfile = await _profilesRepo.get(id).run(); + final failureOrProfile = await _profilesRepo.getById(id).run(); return failureOrProfile.match( (err) { loggy.warning('failed to load profile', err); @@ -40,13 +42,14 @@ class ProfileDetailNotifier extends _$ProfileDetailNotifier with AppLogger { throw const ProfileNotFoundFailure(); } _originalProfile = profile; - return ProfileDetailState(profile: profile, isEditing: true); + return ProfileDetailsState(profile: profile, isEditing: true); }, ); } - ProfilesRepository get _profilesRepo => ref.read(profilesRepositoryProvider); - Profile? _originalProfile; + ProfileRepository get _profilesRepo => + ref.read(profileRepositoryProvider).requireValue; + ProfileEntity? _originalProfile; void setField({String? name, String? url, Option? updateInterval}) { if (state case AsyncData(:final value)) { @@ -74,41 +77,47 @@ class ProfileDetailNotifier extends _$ProfileDetailNotifier with AppLogger { Future save() async { if (state case AsyncData(:final value)) { - if (value.save.isInProgress) return; + if (value.save case AsyncLoading()) return; + final profile = value.profile; Either? failureOrSuccess; - state = AsyncData(value.copyWith(save: const MutationInProgress())); + state = AsyncData(value.copyWith(save: const AsyncLoading())); + switch (profile) { - case RemoteProfile(): + case RemoteProfileEntity(): loggy.debug( 'saving profile, url: [${profile.url}], name: [${profile.name}]', ); if (profile.name.isBlank || profile.url.isBlank) { - loggy.debug('profile save: invalid arguments'); + loggy.debug('save: invalid arguments'); } else if (value.isEditing) { - if (_originalProfile case RemoteProfile(:final url) + if (_originalProfile case RemoteProfileEntity(:final url) when url == profile.url) { loggy.debug('editing profile'); - failureOrSuccess = await _profilesRepo.edit(profile).run(); + failureOrSuccess = await _profilesRepo.patch(profile).run(); } else { loggy.debug('updating profile'); - failureOrSuccess = await _profilesRepo.update(profile).run(); + failureOrSuccess = + await _profilesRepo.updateSubscription(profile).run(); } } else { loggy.debug('adding profile, url: [${profile.url}]'); failureOrSuccess = await _profilesRepo.add(profile).run(); } - case LocalProfile() when value.isEditing: + + case LocalProfileEntity() when value.isEditing: loggy.debug('editing profile'); - failureOrSuccess = await _profilesRepo.edit(profile).run(); + failureOrSuccess = await _profilesRepo.patch(profile).run(); + default: loggy.warning("local profile can't be added manually"); } + state = AsyncData( value.copyWith( save: failureOrSuccess?.fold( - (l) => MutationFailure(l), - (_) => const MutationSuccess(), + (l) => AsyncError(l, StackTrace.current), + (_) => const AsyncData(null), ) ?? value.save, showErrorMessages: true, @@ -119,24 +128,25 @@ class ProfileDetailNotifier extends _$ProfileDetailNotifier with AppLogger { Future updateProfile() async { if (state case AsyncData(:final value)) { - loggy.debug('updating profile'); - if (value.profile case LocalProfile()) { + if (value.update?.isLoading ?? false || !value.isEditing) return; + if (value.profile case LocalProfileEntity()) { loggy.warning("local profile can't be updated"); return; } - if (value.update.isInProgress || !value.isEditing) return; + final profile = value.profile; - loggy.debug('updating profile'); - state = AsyncData(value.copyWith(update: const MutationInProgress())); + state = AsyncData(value.copyWith(update: const AsyncLoading())); + final failureOrUpdatedProfile = await _profilesRepo - .update(profile as RemoteProfile) - .flatMap((_) => _profilesRepo.get(id)) + .updateSubscription(profile as RemoteProfileEntity) + .flatMap((_) => _profilesRepo.getById(id)) .run(); + state = AsyncData( value.copyWith( update: failureOrUpdatedProfile.match( - (l) => MutationFailure(l), - (_) => const MutationSuccess(), + (l) => AsyncError(l, StackTrace.current), + (_) => const AsyncData(null), ), profile: failureOrUpdatedProfile.match( (_) => profile, @@ -149,17 +159,18 @@ class ProfileDetailNotifier extends _$ProfileDetailNotifier with AppLogger { Future delete() async { if (state case AsyncData(:final value)) { - if (value.delete.isInProgress) return; + if (value.delete case AsyncLoading()) return; final profile = value.profile; - loggy.debug('deleting profile'); - state = AsyncData(value.copyWith(delete: const MutationInProgress())); - final result = await _profilesRepo.delete(profile.id).run(); + state = AsyncData(value.copyWith(delete: const AsyncLoading())); + state = AsyncData( value.copyWith( - delete: result.match( - (l) => MutationFailure(l), - (_) => const MutationSuccess(), - ), + delete: await AsyncValue.guard(() async { + await _profilesRepo + .deleteById(profile.id) + .getOrElse((l) => throw l) + .run(); + }), ), ); } diff --git a/lib/features/profile_detail/view/profile_detail_page.dart b/lib/features/profile/details/profile_details_page.dart similarity index 79% rename from lib/features/profile_detail/view/profile_detail_page.dart rename to lib/features/profile/details/profile_details_page.dart index 2c8eb0ff..ce1e5b23 100644 --- a/lib/features/profile_detail/view/profile_detail_page.dart +++ b/lib/features/profile/details/profile_details_page.dart @@ -3,16 +3,16 @@ import 'package:fpdart/fpdart.dart'; import 'package:go_router/go_router.dart'; import 'package:hiddify/core/core_providers.dart'; import 'package:hiddify/domain/failures.dart'; -import 'package:hiddify/domain/profiles/profiles.dart'; import 'package:hiddify/features/common/confirmation_dialogs.dart'; -import 'package:hiddify/features/profile_detail/notifier/notifier.dart'; +import 'package:hiddify/features/profile/details/profile_details_notifier.dart'; +import 'package:hiddify/features/profile/model/profile_entity.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 ProfileDetailPage extends HookConsumerWidget with PresLogger { - const ProfileDetailPage(this.id, {super.key}); +class ProfileDetailsPage extends HookConsumerWidget with PresLogger { + const ProfileDetailsPage(this.id, {super.key}); final String id; @@ -20,65 +20,59 @@ class ProfileDetailPage extends HookConsumerWidget with PresLogger { Widget build(BuildContext context, WidgetRef ref) { final t = ref.watch(translationsProvider); - final provider = profileDetailNotifierProvider(id); + final provider = profileDetailsNotifierProvider(id); final notifier = ref.watch(provider.notifier); ref.listen( - provider.select((data) => data.whenData((value) => value.save)), - (_, asyncSave) { - if (asyncSave case AsyncData(value: final save)) { - switch (save) { - case MutationFailure(:final failure): - final String action; - if (ref.read(provider) case AsyncData(value: final data) - when data.isEditing) { - action = t.profile.save.failureMsg; - } else { - action = t.profile.add.failureMsg; - } - CustomAlertDialog.fromErr(t.presentError(failure, action: action)) - .show(context); - case MutationSuccess(): - CustomToast.success(t.profile.save.successMsg).show(context); - WidgetsBinding.instance.addPostFrameCallback( - (_) { - if (context.mounted) context.pop(); - }, - ); - } + provider.selectAsync((data) => data.save), + (_, next) async { + switch (await next) { + case AsyncData(): + CustomToast.success(t.profile.save.successMsg).show(context); + WidgetsBinding.instance.addPostFrameCallback( + (_) { + if (context.mounted) context.pop(); + }, + ); + case AsyncError(:final error): + final String action; + if (ref.read(provider) case AsyncData(value: final data) + when data.isEditing) { + action = t.profile.save.failureMsg; + } else { + action = t.profile.add.failureMsg; + } + CustomAlertDialog.fromErr(t.presentError(error, action: action)) + .show(context); } }, ); ref.listen( - provider.select((data) => data.whenData((value) => value.update)), - (_, asyncUpdate) { - if (asyncUpdate case AsyncData(value: final update)) { - switch (update) { - case MutationFailure(:final failure): - CustomAlertDialog.fromErr(t.presentError(failure)).show(context); - case MutationSuccess(): - CustomToast.success(t.profile.update.successMsg).show(context); - } + provider.selectAsync((data) => data.update), + (_, next) async { + switch (await next) { + case AsyncData(): + CustomToast.success(t.profile.update.successMsg).show(context); + case AsyncError(:final error): + CustomAlertDialog.fromErr(t.presentError(error)).show(context); } }, ); ref.listen( - provider.select((data) => data.whenData((value) => value.delete)), - (_, asyncDelete) { - if (asyncDelete case AsyncData(value: final delete)) { - switch (delete) { - case MutationFailure(:final failure): - CustomToast.error(t.presentShortError(failure)).show(context); - case MutationSuccess(): - CustomToast.success(t.profile.delete.successMsg).show(context); - WidgetsBinding.instance.addPostFrameCallback( - (_) { - if (context.mounted) context.pop(); - }, - ); - } + provider.selectAsync((data) => data.delete), + (_, next) async { + switch (await next) { + case AsyncData(): + CustomToast.success(t.profile.delete.successMsg).show(context); + WidgetsBinding.instance.addPostFrameCallback( + (_) { + if (context.mounted) context.pop(); + }, + ); + case AsyncError(:final error): + CustomToast.error(t.presentShortError(error)).show(context); } }, ); @@ -102,7 +96,7 @@ class ProfileDetailPage extends HookConsumerWidget with PresLogger { PopupMenuButton( itemBuilder: (context) { return [ - if (state.profile case RemoteProfile()) + if (state.profile case RemoteProfileEntity()) PopupMenuItem( child: Text(t.profile.update.buttonTxt), onTap: () async { @@ -151,7 +145,10 @@ class ProfileDetailPage extends HookConsumerWidget with PresLogger { ), ), if (state.profile - case RemoteProfile(:final url, :final options)) ...[ + case RemoteProfileEntity( + :final url, + :final options + )) ...[ Padding( padding: const EdgeInsets.symmetric( horizontal: 16, diff --git a/lib/features/profile/details/profile_details_state.dart b/lib/features/profile/details/profile_details_state.dart new file mode 100644 index 00000000..894abf24 --- /dev/null +++ b/lib/features/profile/details/profile_details_state.dart @@ -0,0 +1,22 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hiddify/features/profile/model/profile_entity.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +part 'profile_details_state.freezed.dart'; + +@freezed +class ProfileDetailsState with _$ProfileDetailsState { + const ProfileDetailsState._(); + + const factory ProfileDetailsState({ + required ProfileEntity profile, + @Default(false) bool isEditing, + @Default(false) bool showErrorMessages, + AsyncValue? save, + AsyncValue? update, + AsyncValue? delete, + }) = _ProfileDetailsState; + + bool get isBusy => + save is AsyncLoading || delete is AsyncLoading || update is AsyncLoading; +} diff --git a/lib/features/profile/model/profile_entity.dart b/lib/features/profile/model/profile_entity.dart new file mode 100644 index 00000000..144546be --- /dev/null +++ b/lib/features/profile/model/profile_entity.dart @@ -0,0 +1,57 @@ +import 'package:dartx/dartx.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'profile_entity.freezed.dart'; + +enum ProfileType { remote, local } + +@freezed +sealed class ProfileEntity with _$ProfileEntity { + const ProfileEntity._(); + + const factory ProfileEntity.remote({ + required String id, + required bool active, + required String name, + required String url, + required DateTime lastUpdate, + ProfileOptions? options, + SubscriptionInfo? subInfo, + }) = RemoteProfileEntity; + + const factory ProfileEntity.local({ + required String id, + required bool active, + required String name, + required DateTime lastUpdate, + }) = LocalProfileEntity; +} + +@freezed +class ProfileOptions with _$ProfileOptions { + const factory ProfileOptions({ + required Duration updateInterval, + }) = _ProfileOptions; +} + +@freezed +class SubscriptionInfo with _$SubscriptionInfo { + const SubscriptionInfo._(); + + const factory SubscriptionInfo({ + required int upload, + required int download, + required int total, + required DateTime expire, + String? webPageUrl, + String? supportUrl, + }) = _SubscriptionInfo; + + bool get isExpired => expire <= DateTime.now(); + + int get consumption => upload + download; + + double get ratio => (consumption / total).clamp(0, 1); + + Duration get remaining => expire.difference(DateTime.now()); +} diff --git a/lib/domain/profiles/profiles_failure.dart b/lib/features/profile/model/profile_failure.dart similarity index 97% rename from lib/domain/profiles/profiles_failure.dart rename to lib/features/profile/model/profile_failure.dart index 7a9edb8a..529b5269 100644 --- a/lib/domain/profiles/profiles_failure.dart +++ b/lib/features/profile/model/profile_failure.dart @@ -2,7 +2,7 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hiddify/core/prefs/prefs.dart'; import 'package:hiddify/domain/failures.dart'; -part 'profiles_failure.freezed.dart'; +part 'profile_failure.freezed.dart'; @freezed sealed class ProfileFailure with _$ProfileFailure, Failure { diff --git a/lib/domain/profiles/profile_enums.dart b/lib/features/profile/model/profile_sort_enum.dart similarity index 91% rename from lib/domain/profiles/profile_enums.dart rename to lib/features/profile/model/profile_sort_enum.dart index 1ad081bd..04b8f6d4 100644 --- a/lib/domain/profiles/profile_enums.dart +++ b/lib/features/profile/model/profile_sort_enum.dart @@ -17,3 +17,5 @@ enum ProfilesSort { name => Icons.sort_by_alpha, }; } + +enum SortMode { ascending, descending } diff --git a/lib/features/profile/notifier/active_profile_notifier.dart b/lib/features/profile/notifier/active_profile_notifier.dart new file mode 100644 index 00000000..74e4214a --- /dev/null +++ b/lib/features/profile/notifier/active_profile_notifier.dart @@ -0,0 +1,31 @@ +import 'package:hiddify/features/profile/data/profile_data_providers.dart'; +import 'package:hiddify/features/profile/model/profile_entity.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'active_profile_notifier.g.dart'; + +@Riverpod(keepAlive: true) +class ActiveProfile extends _$ActiveProfile with AppLogger { + @override + Stream build() { + loggy.debug("watching active profile"); + return ref + .watch(profileRepositoryProvider) + .requireValue + .watchActiveProfile() + .map((event) => event.getOrElse((l) => throw l)); + } +} + +// TODO: move to specific feature +@Riverpod(keepAlive: true) +Stream hasAnyProfile( + HasAnyProfileRef ref, +) { + return ref + .watch(profileRepositoryProvider) + .requireValue + .watchHasAnyProfile() + .map((event) => event.getOrElse((l) => throw l)); +} diff --git a/lib/features/profile/notifier/profile_notifier.dart b/lib/features/profile/notifier/profile_notifier.dart new file mode 100644 index 00000000..263fdf85 --- /dev/null +++ b/lib/features/profile/notifier/profile_notifier.dart @@ -0,0 +1,140 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:hiddify/core/core_providers.dart'; +import 'package:hiddify/core/notification/in_app_notification_controller.dart'; +import 'package:hiddify/core/prefs/general_prefs.dart'; +import 'package:hiddify/domain/failures.dart'; +import 'package:hiddify/features/common/connectivity/connectivity_controller.dart'; +import 'package:hiddify/features/profile/data/profile_data_providers.dart'; +import 'package:hiddify/features/profile/data/profile_repository.dart'; +import 'package:hiddify/features/profile/model/profile_entity.dart'; +import 'package:hiddify/features/profile/model/profile_failure.dart'; +import 'package:hiddify/features/profile/notifier/active_profile_notifier.dart'; +import 'package:hiddify/utils/riverpod_utils.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'profile_notifier.g.dart'; + +@riverpod +class AddProfile extends _$AddProfile with AppLogger { + @override + AsyncValue build() { + ref.disposeDelay(const Duration(minutes: 1)); + ref.listenSelf( + (previous, next) { + final t = ref.read(translationsProvider); + final notification = ref.read(inAppNotificationControllerProvider); + switch (next) { + case AsyncData(value: final _?): + notification.showSuccessToast(t.profile.save.successMsg); + case AsyncError(:final error): + if (error case ProfileInvalidUrlFailure()) { + notification.showErrorToast(t.failure.profiles.invalidUrl); + } else { + notification.showErrorDialog( + t.presentError(error, action: t.profile.add.failureMsg), + ); + } + } + }, + ); + return const AsyncData(null); + } + + ProfileRepository get _profilesRepo => + ref.read(profileRepositoryProvider).requireValue; + + Future add(String rawInput) async { + if (state.isLoading) return; + state = const AsyncLoading(); + state = await AsyncValue.guard( + () async { + final activeProfile = await ref.read(activeProfileProvider.future); + final markAsActive = + activeProfile == null || ref.read(markNewProfileActiveProvider); + final TaskEither task; + if (LinkParser.parse(rawInput) case (final link)?) { + loggy.debug("adding profile, url: [${link.url}]"); + task = _profilesRepo.addByUrl(link.url, markAsActive: markAsActive); + } else if (LinkParser.protocol(rawInput) case (final parsed)?) { + loggy.debug("adding profile, content"); + task = _profilesRepo.addByContent( + parsed.content, + name: parsed.name, + markAsActive: markAsActive, + ); + } else { + loggy.debug("invalid content"); + throw const ProfileInvalidUrlFailure(); + } + return task.match( + (err) { + loggy.warning("failed to add profile", err); + throw err; + }, + (_) { + loggy.info( + "successfully added profile, mark as active? [$markAsActive]", + ); + return unit; + }, + ).run(); + }, + ); + } +} + +@riverpod +class UpdateProfile extends _$UpdateProfile with AppLogger { + @override + AsyncValue build(String id) { + ref.disposeDelay(const Duration(minutes: 1)); + ref.listenSelf( + (previous, next) { + final t = ref.read(translationsProvider); + final notification = ref.read(inAppNotificationControllerProvider); + switch (next) { + case AsyncData(value: final _?): + notification.showSuccessToast(t.profile.update.successMsg); + case AsyncError(:final error): + notification.showErrorDialog( + t.presentError(error, action: t.profile.update.failureMsg), + ); + } + }, + ); + return const AsyncData(null); + } + + ProfileRepository get _profilesRepo => + ref.read(profileRepositoryProvider).requireValue; + + Future updateProfile(RemoteProfileEntity profile) async { + if (state.isLoading) return; + state = const AsyncLoading(); + state = await AsyncValue.guard( + () async { + return await _profilesRepo.updateSubscription(profile).match( + (err) { + loggy.warning("failed to update profile", err); + throw err; + }, + (_) async { + loggy.info( + 'successfully updated profile, was active? [${profile.active}]', + ); + + await ref.read(activeProfileProvider.future).then((active) async { + if (active != null && active.id == profile.id) { + await ref + .read(connectivityControllerProvider.notifier) + .reconnect(profile.id); + } + }); + return unit; + }, + ).run(); + }, + ); + } +} diff --git a/lib/features/profile/notifier/profiles_update_notifier.dart b/lib/features/profile/notifier/profiles_update_notifier.dart new file mode 100644 index 00000000..5f4325b3 --- /dev/null +++ b/lib/features/profile/notifier/profiles_update_notifier.dart @@ -0,0 +1,92 @@ +import 'package:dartx/dartx.dart'; +import 'package:hiddify/data/data_providers.dart'; +import 'package:hiddify/features/profile/data/profile_data_providers.dart'; +import 'package:hiddify/features/profile/model/profile_entity.dart'; +import 'package:hiddify/utils/custom_loggers.dart'; +import 'package:meta/meta.dart'; +import 'package:neat_periodic_task/neat_periodic_task.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'profiles_update_notifier.g.dart'; + +@Riverpod(keepAlive: true) +class ForegroundProfilesUpdateNotifier + extends _$ForegroundProfilesUpdateNotifier with AppLogger { + static const prefKey = "profiles_update_check"; + static const interval = Duration(minutes: 15); + + @override + Future build() async { + loggy.debug("initializing"); + var cycleCount = 0; + final scheduler = NeatPeriodicTaskScheduler( + name: 'profiles update worker', + interval: interval, + timeout: const Duration(minutes: 5), + task: () async { + loggy.debug("cycle [${cycleCount++}]"); + await updateProfiles(); + }, + ); + + ref.onDispose(() async { + await scheduler.stop(); + }); + + return scheduler.start(); + } + + @visibleForTesting + Future updateProfiles() async { + try { + final previousRun = DateTime.tryParse( + ref.read(sharedPreferencesProvider).getString(prefKey) ?? "", + ); + + if (previousRun != null && previousRun.add(interval) > DateTime.now()) { + loggy.debug("too soon! previous run: [$previousRun]"); + return; + } + loggy.debug("running, previous run: [$previousRun]"); + + final remoteProfiles = await ref + .read(profileRepositoryProvider) + .requireValue + .watchAll() + .map( + (event) => event.getOrElse((f) { + loggy.error("error getting profiles"); + throw f; + }).whereType(), + ) + .first; + + await for (final profile in Stream.fromIterable(remoteProfiles)) { + final updateInterval = profile.options?.updateInterval; + if (updateInterval != null && + updateInterval <= DateTime.now().difference(profile.lastUpdate)) { + await ref + .read(profileRepositoryProvider) + .requireValue + .updateSubscription(profile) + .mapLeft( + (l) => loggy.debug("error updating profile [${profile.id}]", l), + ) + .map( + (_) => + loggy.debug("profile [${profile.id}] updated successfully"), + ) + .run(); + } else { + loggy.debug( + "skipping profile [${profile.id}] update. last successful update: [${profile.lastUpdate}] - interval: [${profile.options?.updateInterval}]", + ); + } + } + } finally { + await ref + .read(sharedPreferencesProvider) + .setString(prefKey, DateTime.now().toIso8601String()); + } + } +} diff --git a/lib/features/profile/overview/profiles_overview_notifier.dart b/lib/features/profile/overview/profiles_overview_notifier.dart new file mode 100644 index 00000000..9434a2d9 --- /dev/null +++ b/lib/features/profile/overview/profiles_overview_notifier.dart @@ -0,0 +1,83 @@ +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:hiddify/data/data_providers.dart'; +import 'package:hiddify/features/profile/data/profile_data_providers.dart'; +import 'package:hiddify/features/profile/data/profile_repository.dart'; +import 'package:hiddify/features/profile/model/profile_entity.dart'; +import 'package:hiddify/features/profile/model/profile_sort_enum.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'profiles_overview_notifier.g.dart'; + +@riverpod +class ProfilesOverviewSortNotifier extends _$ProfilesOverviewSortNotifier + with AppLogger { + @override + ({ProfilesSort by, SortMode mode}) build() { + return (by: ProfilesSort.lastUpdate, mode: SortMode.descending); + } + + void changeSort(ProfilesSort sortBy) => + state = (by: sortBy, mode: state.mode); + + void toggleMode() => state = ( + by: state.by, + mode: state.mode == SortMode.ascending + ? SortMode.descending + : SortMode.ascending + ); +} + +@riverpod +class ProfilesOverviewNotifier extends _$ProfilesOverviewNotifier + with AppLogger { + @override + Stream> build() { + final sort = ref.watch(profilesOverviewSortNotifierProvider); + return _profilesRepo + .watchAll(sort: sort.by, sortMode: sort.mode) + .map((event) => event.getOrElse((l) => throw l)); + } + + ProfileRepository get _profilesRepo => + ref.read(profileRepositoryProvider).requireValue; + + Future selectActiveProfile(String id) async { + loggy.debug('changing active profile to: [$id]'); + return _profilesRepo.setAsActive(id).getOrElse((err) { + loggy.warning('failed to set [$id] as active profile', err); + throw err; + }).run(); + } + + Future deleteProfile(ProfileEntity profile) async { + loggy.debug('deleting profile: ${profile.name}'); + await _profilesRepo.deleteById(profile.id).match( + (err) { + loggy.warning('failed to delete profile', err); + throw err; + }, + (_) { + loggy.info( + 'successfully deleted profile, was active? [${profile.active}]', + ); + return unit; + }, + ).run(); + } + + Future exportConfigToClipboard(ProfileEntity profile) async { + await ref.read(coreFacadeProvider).generateConfig(profile.id).match( + (err) { + loggy.warning('error generating config', err); + throw err; + }, + (configJson) async { + await Clipboard.setData(ClipboardData(text: configJson)); + }, + ).run(); + } +} diff --git a/lib/features/profiles/view/profiles_modal.dart b/lib/features/profile/overview/profiles_overview_page.dart similarity index 81% rename from lib/features/profiles/view/profiles_modal.dart rename to lib/features/profile/overview/profiles_overview_page.dart index f207d9e8..105177d4 100644 --- a/lib/features/profiles/view/profiles_modal.dart +++ b/lib/features/profile/overview/profiles_overview_page.dart @@ -2,16 +2,15 @@ import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:hiddify/core/core_providers.dart'; import 'package:hiddify/core/router/router.dart'; -import 'package:hiddify/domain/enums.dart'; import 'package:hiddify/domain/failures.dart'; -import 'package:hiddify/domain/profiles/profiles.dart'; -import 'package:hiddify/features/common/profile_tile.dart'; -import 'package:hiddify/features/profiles/notifier/notifier.dart'; -import 'package:hiddify/utils/utils.dart'; +import 'package:hiddify/features/profile/model/profile_sort_enum.dart'; +import 'package:hiddify/features/profile/overview/profiles_overview_notifier.dart'; +import 'package:hiddify/features/profile/widget/profile_tile.dart'; +import 'package:hiddify/utils/placeholders.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -class ProfilesModal extends HookConsumerWidget { - const ProfilesModal({ +class ProfilesOverviewModal extends HookConsumerWidget { + const ProfilesOverviewModal({ super.key, this.scrollController, }); @@ -21,7 +20,7 @@ class ProfilesModal extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final t = ref.watch(translationsProvider); - final asyncProfiles = ref.watch(profilesNotifierProvider); + final asyncProfiles = ref.watch(profilesOverviewNotifierProvider); return Stack( children: [ @@ -85,12 +84,14 @@ class ProfilesSortModal extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final t = ref.watch(translationsProvider); + final sortNotifier = + ref.watch(profilesOverviewSortNotifierProvider.notifier); return AlertDialog( title: Text(t.general.sortBy), content: Consumer( builder: (context, ref, child) { - final sort = ref.watch(profilesSortNotifierProvider); + final sort = ref.watch(profilesOverviewSortNotifierProvider); return SingleChildScrollView( child: Column( children: [ @@ -104,13 +105,9 @@ class ProfilesSortModal extends HookConsumerWidget { title: Text(e.present(t)), onTap: () { if (selected) { - ref - .read(profilesSortNotifierProvider.notifier) - .toggleMode(); + sortNotifier.toggleMode(); } else { - ref - .read(profilesSortNotifierProvider.notifier) - .changeSort(e); + sortNotifier.changeSort(e); } }, selected: selected, @@ -118,9 +115,7 @@ class ProfilesSortModal extends HookConsumerWidget { trailing: selected ? IconButton( onPressed: () { - ref - .read(profilesSortNotifierProvider.notifier) - .toggleMode(); + sortNotifier.toggleMode(); }, icon: AnimatedRotation( turns: arrowTurn, diff --git a/lib/features/common/profile_tile.dart b/lib/features/profile/widget/profile_tile.dart similarity index 87% rename from lib/features/common/profile_tile.dart rename to lib/features/profile/widget/profile_tile.dart index c0c7781f..7d526491 100644 --- a/lib/features/common/profile_tile.dart +++ b/lib/features/profile/widget/profile_tile.dart @@ -7,10 +7,11 @@ import 'package:hiddify/core/core_providers.dart'; import 'package:hiddify/core/prefs/prefs.dart'; import 'package:hiddify/core/router/router.dart'; import 'package:hiddify/domain/failures.dart'; -import 'package:hiddify/domain/profiles/profiles.dart'; import 'package:hiddify/features/common/confirmation_dialogs.dart'; import 'package:hiddify/features/common/qr_code_dialog.dart'; -import 'package:hiddify/features/profiles/notifier/notifier.dart'; +import 'package:hiddify/features/profile/model/profile_entity.dart'; +import 'package:hiddify/features/profile/notifier/profile_notifier.dart'; +import 'package:hiddify/features/profile/overview/profiles_overview_notifier.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:percent_indicator/percent_indicator.dart'; @@ -22,7 +23,7 @@ class ProfileTile extends HookConsumerWidget { this.isMain = false, }); - final Profile profile; + final ProfileEntity profile; /// home screen active profile card final bool isMain; @@ -42,7 +43,7 @@ class ProfileTile extends HookConsumerWidget { ); final subInfo = switch (profile) { - RemoteProfile(:final subInfo) => subInfo, + RemoteProfileEntity(:final subInfo) => subInfo, _ => null, }; @@ -65,7 +66,7 @@ class ProfileTile extends HookConsumerWidget { child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (profile is RemoteProfile || !isMain) ...[ + if (profile is RemoteProfileEntity || !isMain) ...[ SizedBox( width: 48, child: Semantics( @@ -95,7 +96,7 @@ class ProfileTile extends HookConsumerWidget { if (profile.active) return; selectActiveMutation.setFuture( ref - .read(profilesNotifierProvider.notifier) + .read(profilesOverviewNotifierProvider.notifier) .selectActiveProfile(profile.id), ); } @@ -173,39 +174,27 @@ class ProfileTile extends HookConsumerWidget { class ProfileActionButton extends HookConsumerWidget { const ProfileActionButton(this.profile, this.showAllActions, {super.key}); - final Profile profile; + final ProfileEntity profile; final bool showAllActions; @override Widget build(BuildContext context, WidgetRef ref) { final t = ref.watch(translationsProvider); - final updateProfileMutation = useMutation( - initialOnFailure: (err) { - CustomAlertDialog.fromErr( - t.presentError(err, action: t.profile.update.failureMsg), - ).show(context); - }, - initialOnSuccess: () => - CustomToast.success(t.profile.update.successMsg).show(context), - ); - - if (profile case RemoteProfile() when !showAllActions) { + if (profile case RemoteProfileEntity() when !showAllActions) { return Semantics( button: true, - enabled: !updateProfileMutation.state.isInProgress, + enabled: !ref.watch(updateProfileProvider(profile.id)).isLoading, child: Tooltip( message: t.profile.update.tooltip, child: InkWell( onTap: () { - if (updateProfileMutation.state.isInProgress) { + if (ref.read(updateProfileProvider(profile.id)).isLoading) { return; } - updateProfileMutation.setFuture( - ref - .read(profilesNotifierProvider.notifier) - .updateProfile(profile as RemoteProfile), - ); + ref + .read(updateProfileProvider(profile.id).notifier) + .updateProfile(profile as RemoteProfileEntity); }, child: const Icon(Icons.update), ), @@ -239,7 +228,7 @@ class ProfileActionButton extends HookConsumerWidget { class ProfileActionsMenu extends HookConsumerWidget { const ProfileActionsMenu(this.profile, this.builder, {super.key, this.child}); - final Profile profile; + final ProfileEntity profile; final MenuAnchorChildBuilder builder; final Widget? child; @@ -247,15 +236,6 @@ class ProfileActionsMenu extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final t = ref.watch(translationsProvider); - final updateProfileMutation = useMutation( - initialOnFailure: (err) { - CustomAlertDialog.fromErr( - t.presentError(err, action: t.profile.update.failureMsg), - ).show(context); - }, - initialOnSuccess: () => - CustomToast.success(t.profile.update.successMsg).show(context), - ); final exportConfigMutation = useMutation( initialOnFailure: (err) { CustomToast.error(t.presentShortError(err)).show(context); @@ -273,24 +253,22 @@ class ProfileActionsMenu extends HookConsumerWidget { return MenuAnchor( builder: builder, menuChildren: [ - if (profile case RemoteProfile()) + if (profile case RemoteProfileEntity()) MenuItemButton( leadingIcon: const Icon(Icons.update), child: Text(t.profile.update.buttonTxt), onPressed: () { - if (updateProfileMutation.state.isInProgress) { + if (ref.read(updateProfileProvider(profile.id)).isLoading) { return; } - updateProfileMutation.setFuture( - ref - .read(profilesNotifierProvider.notifier) - .updateProfile(profile as RemoteProfile), - ); + ref + .read(updateProfileProvider(profile.id).notifier) + .updateProfile(profile as RemoteProfileEntity); }, ), SubmenuButton( menuChildren: [ - if (profile case RemoteProfile(:final url, :final name)) ...[ + if (profile case RemoteProfileEntity(:final url, :final name)) ...[ MenuItemButton( child: Text(t.profile.share.exportSubLinkToClipboard), onPressed: () async { @@ -325,7 +303,7 @@ class ProfileActionsMenu extends HookConsumerWidget { } exportConfigMutation.setFuture( ref - .read(profilesNotifierProvider.notifier) + .read(profilesOverviewNotifierProvider.notifier) .exportConfigToClipboard(profile), ); }, @@ -356,7 +334,7 @@ class ProfileActionsMenu extends HookConsumerWidget { if (deleteConfirmed) { deleteProfileMutation.setFuture( ref - .read(profilesNotifierProvider.notifier) + .read(profilesOverviewNotifierProvider.notifier) .deleteProfile(profile), ); } diff --git a/lib/features/profile_detail/notifier/notifier.dart b/lib/features/profile_detail/notifier/notifier.dart deleted file mode 100644 index a6381143..00000000 --- a/lib/features/profile_detail/notifier/notifier.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'profile_detail_notifier.dart'; -export 'profile_detail_state.dart'; diff --git a/lib/features/profile_detail/notifier/profile_detail_state.dart b/lib/features/profile_detail/notifier/profile_detail_state.dart deleted file mode 100644 index 344cd7c5..00000000 --- a/lib/features/profile_detail/notifier/profile_detail_state.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:hiddify/domain/profiles/profiles.dart'; -import 'package:hiddify/utils/utils.dart'; - -part 'profile_detail_state.freezed.dart'; - -@freezed -class ProfileDetailState with _$ProfileDetailState { - const ProfileDetailState._(); - - const factory ProfileDetailState({ - required Profile profile, - @Default(false) bool isEditing, - @Default(false) bool showErrorMessages, - @Default(MutationState.initial()) MutationState save, - @Default(MutationState.initial()) MutationState update, - @Default(MutationState.initial()) MutationState delete, - }) = _ProfileDetailState; - - bool get isBusy => - save.isInProgress || delete.isInProgress || update.isInProgress; -} diff --git a/lib/features/profile_detail/view/view.dart b/lib/features/profile_detail/view/view.dart deleted file mode 100644 index bcb57dd1..00000000 --- a/lib/features/profile_detail/view/view.dart +++ /dev/null @@ -1 +0,0 @@ -export 'profile_detail_page.dart'; diff --git a/lib/features/profiles/notifier/notifier.dart b/lib/features/profiles/notifier/notifier.dart deleted file mode 100644 index 7fc6e689..00000000 --- a/lib/features/profiles/notifier/notifier.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'profiles_notifier.dart'; -export 'profiles_update_notifier.dart'; diff --git a/lib/features/profiles/notifier/profiles_notifier.dart b/lib/features/profiles/notifier/profiles_notifier.dart deleted file mode 100644 index 6fb165e5..00000000 --- a/lib/features/profiles/notifier/profiles_notifier.dart +++ /dev/null @@ -1,140 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/services.dart'; -import 'package:fpdart/fpdart.dart'; -import 'package:hiddify/core/prefs/prefs.dart'; -import 'package:hiddify/data/data_providers.dart'; -import 'package:hiddify/domain/enums.dart'; -import 'package:hiddify/domain/profiles/profiles.dart'; -import 'package:hiddify/features/common/active_profile/active_profile_notifier.dart'; -import 'package:hiddify/features/common/connectivity/connectivity_controller.dart'; -import 'package:hiddify/utils/utils.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'profiles_notifier.g.dart'; - -@riverpod -class ProfilesSortNotifier extends _$ProfilesSortNotifier with AppLogger { - @override - ({ProfilesSort by, SortMode mode}) build() { - return (by: ProfilesSort.lastUpdate, mode: SortMode.descending); - } - - void changeSort(ProfilesSort sortBy) => - state = (by: sortBy, mode: state.mode); - - void toggleMode() => state = ( - by: state.by, - mode: state.mode == SortMode.ascending - ? SortMode.descending - : SortMode.ascending - ); -} - -@riverpod -class ProfilesNotifier extends _$ProfilesNotifier with AppLogger { - @override - Stream> build() { - final sort = ref.watch(profilesSortNotifierProvider); - return _profilesRepo - .watchAll(sort: sort.by, mode: sort.mode) - .map((event) => event.getOrElse((l) => throw l)); - } - - ProfilesRepository get _profilesRepo => ref.read(profilesRepositoryProvider); - - Future selectActiveProfile(String id) async { - loggy.debug('changing active profile to: [$id]'); - return _profilesRepo.setAsActive(id).getOrElse((err) { - loggy.warning('failed to set [$id] as active profile', err); - throw err; - }).run(); - } - - Future addProfile(String rawInput) async { - final activeProfile = await ref.read(activeProfileProvider.future); - final markAsActive = - activeProfile == null || ref.read(markNewProfileActiveProvider); - final TaskEither task; - if (LinkParser.parse(rawInput) case (final link)?) { - loggy.debug("adding profile, url: [${link.url}]"); - task = ref - .read(profilesRepositoryProvider) - .addByUrl(link.url, markAsActive: markAsActive); - } else if (LinkParser.protocol(rawInput) case (final parsed)?) { - loggy.debug("adding profile, content"); - task = ref.read(profilesRepositoryProvider).addByContent( - parsed.content, - name: parsed.name, - markAsActive: markAsActive, - ); - } else { - loggy.debug("invalid content"); - throw const ProfileInvalidUrlFailure(); - } - return task.match( - (err) { - loggy.warning("failed to add profile", err); - throw err; - }, - (_) { - loggy.info( - "successfully added profile, mark as active? [$markAsActive]", - ); - return unit; - }, - ).run(); - } - - Future updateProfile(RemoteProfile profile) async { - loggy.debug("updating profile"); - return await ref.read(profilesRepositoryProvider).update(profile).match( - (err) { - loggy.warning("failed to update profile", err); - throw err; - }, - (_) async { - loggy.info( - 'successfully updated profile, was active? [${profile.active}]', - ); - - await ref.read(activeProfileProvider.future).then((active) async { - if (active != null && active.id == profile.id) { - await ref - .read(connectivityControllerProvider.notifier) - .reconnect(profile.id); - } - }); - return unit; - }, - ).run(); - } - - Future deleteProfile(Profile profile) async { - loggy.debug('deleting profile: ${profile.name}'); - await _profilesRepo.delete(profile.id).match( - (err) { - loggy.warning('failed to delete profile', err); - throw err; - }, - (_) { - loggy.info( - 'successfully deleted profile, was active? [${profile.active}]', - ); - return unit; - }, - ).run(); - } - - Future exportConfigToClipboard(Profile profile) async { - await ref.read(coreFacadeProvider).generateConfig(profile.id).match( - (err) { - loggy.warning('error generating config', err); - throw err; - }, - (configJson) async { - await Clipboard.setData(ClipboardData(text: configJson)); - }, - ).run(); - } -} diff --git a/lib/features/profiles/notifier/profiles_update_notifier.dart b/lib/features/profiles/notifier/profiles_update_notifier.dart deleted file mode 100644 index cccc9588..00000000 --- a/lib/features/profiles/notifier/profiles_update_notifier.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:fpdart/fpdart.dart'; -import 'package:hiddify/data/data_providers.dart'; -import 'package:hiddify/domain/profiles/profiles.dart'; -import 'package:hiddify/services/service_providers.dart'; -import 'package:hiddify/utils/utils.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'profiles_update_notifier.g.dart'; - -typedef ProfileUpdateResult = ({ - String name, - Either failureOrSuccess -}); - -@Riverpod(keepAlive: true) -class ProfilesUpdateNotifier extends _$ProfilesUpdateNotifier with AppLogger { - @override - Stream build() { - _schedule(); - return const Stream.empty(); - } - - Future _schedule() async { - loggy.debug("scheduling profiles update worker"); - return ref.read(cronServiceProvider).schedule( - key: 'profiles_update', - duration: const Duration(minutes: 10), - callback: () async { - final failureOrProfiles = - await ref.read(profilesRepositoryProvider).watchAll().first; - if (failureOrProfiles case Right(value: final profiles)) { - for (final profile in profiles) { - if (profile case RemoteProfile()) { - loggy.debug("checking profile: [${profile.name}]"); - final updateInterval = profile.options?.updateInterval; - if (updateInterval != null && - updateInterval <= - DateTime.now().difference(profile.lastUpdate)) { - final failureOrSuccess = await ref - .read(profilesRepositoryProvider) - .update(profile) - .run(); - state = AsyncData( - (name: profile.name, failureOrSuccess: failureOrSuccess), - ); - } else { - loggy.debug("skipping profile: [${profile.name}]"); - } - } - } - } - }, - ); - } -} diff --git a/lib/features/profiles/view/view.dart b/lib/features/profiles/view/view.dart deleted file mode 100644 index cb18b1bf..00000000 --- a/lib/features/profiles/view/view.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'add_profile_modal.dart'; -export 'profiles_modal.dart'; diff --git a/lib/services/cron_service.dart b/lib/services/cron_service.dart deleted file mode 100644 index d3fe1cb9..00000000 --- a/lib/services/cron_service.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'dart:async'; - -import 'package:dartx/dartx.dart'; -import 'package:hiddify/utils/custom_loggers.dart'; -import 'package:neat_periodic_task/neat_periodic_task.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -const _cronKeyPrefix = "cron_"; - -typedef Job = ( - String key, - Duration duration, - FutureOr Function() callback, -); - -class CronService with InfraLogger { - CronService(this.prefs); - - final SharedPreferences prefs; - - NeatPeriodicTaskScheduler? _scheduler; - Map jobs = {}; - - void schedule({ - required String key, - required Duration duration, - required FutureOr Function() callback, - }) { - loggy.debug("scheduling [$key]"); - jobs[key] = (key, duration, callback); - } - - Future run(Job job) async { - final key = job.$1; - final prefKey = "$_cronKeyPrefix$key"; - final previousRunTime = DateTime.tryParse(prefs.getString(prefKey) ?? ""); - loggy.debug( - "[$key] > ${previousRunTime == null ? "first run" : "previous run on [$previousRunTime]"}", - ); - - if (previousRunTime != null && - previousRunTime.add(job.$2) > DateTime.now()) { - loggy.debug("[$key] > didn't meet criteria"); - return; - } - - final result = await job.$3(); - await prefs.setString(prefKey, DateTime.now().toIso8601String()); - return result; - } - - Future startScheduler() async { - loggy.debug("starting job scheduler"); - await _scheduler?.stop(); - int runCount = 0; - _scheduler = NeatPeriodicTaskScheduler( - name: "cron job scheduler", - interval: const Duration(minutes: 10), - timeout: const Duration(minutes: 5), - minCycle: const Duration(minutes: 2), - task: () { - loggy.debug("in run ${runCount++}"); - return Future.wait(jobs.values.map(run)); - }, - ); - _scheduler!.start(); - } - - Future stopScheduler() async { - loggy.debug("stopping job scheduler"); - return _scheduler?.stop(); - } -} diff --git a/lib/services/files_editor_service.dart b/lib/services/files_editor_service.dart index 8b6995d3..9de8515e 100644 --- a/lib/services/files_editor_service.dart +++ b/lib/services/files_editor_service.dart @@ -1,6 +1,5 @@ import 'dart:io'; -import 'package:hiddify/domain/constants.dart'; import 'package:hiddify/services/platform_services.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:path/path.dart' as p; @@ -20,8 +19,6 @@ class FilesEditorService with InfraLogger { late final Directories dirs; Directory get workingDir => dirs.workingDir; - Directory get configsDir => - Directory(p.join(workingDir.path, Constants.configsFolderName)); Directory get logsDir => dirs.workingDir; File get appLogsFile => File(p.join(logsDir.path, "app.log")); @@ -43,9 +40,6 @@ class FilesEditorService with InfraLogger { if (!await dirs.workingDir.exists()) { await dirs.workingDir.create(recursive: true); } - if (!await configsDir.exists()) { - await configsDir.create(recursive: true); - } if (await appLogsFile.exists()) { await appLogsFile.writeAsString(""); @@ -68,14 +62,4 @@ class FilesEditorService with InfraLogger { } return getApplicationDocumentsDirectory(); } - - String configPath(String fileName) { - return p.join(configsDir.path, "$fileName.json"); - } - - String tempConfigPath(String fileName) => configPath("temp_$fileName"); - - Future deleteConfig(String fileName) { - return File(configPath(fileName)).delete(); - } } diff --git a/lib/services/service_providers.dart b/lib/services/service_providers.dart index ae7445e0..6b92132a 100644 --- a/lib/services/service_providers.dart +++ b/lib/services/service_providers.dart @@ -1,5 +1,3 @@ -import 'package:hiddify/data/data_providers.dart'; -import 'package:hiddify/services/cron_service.dart'; import 'package:hiddify/services/files_editor_service.dart'; import 'package:hiddify/services/platform_services.dart'; import 'package:hiddify/services/singbox/singbox_service.dart'; @@ -17,10 +15,3 @@ SingboxService singboxService(SingboxServiceRef ref) => SingboxService(); @Riverpod(keepAlive: true) PlatformServices platformServices(PlatformServicesRef ref) => PlatformServices(); - -@Riverpod(keepAlive: true) -CronService cronService(CronServiceRef ref) { - final service = CronService(ref.watch(sharedPreferencesProvider)); - ref.onDispose(() => service.stopScheduler()); - return service; -} diff --git a/test/domain/profiles/profile_test.dart b/test/features/profile/data/profile_parser_test.dart similarity index 69% rename from test/domain/profiles/profile_test.dart rename to test/features/profile/data/profile_parser_test.dart index 15bfa2d0..96737245 100644 --- a/test/domain/profiles/profile_test.dart +++ b/test/features/profile/data/profile_parser_test.dart @@ -1,5 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:hiddify/domain/profiles/profiles.dart'; +import 'package:hiddify/features/profile/data/profile_parser.dart'; +import 'package:hiddify/features/profile/model/profile_entity.dart'; void main() { const validBaseUrl = "https://example.com/configurations/user1/filename.yaml"; @@ -8,14 +9,26 @@ void main() { const validSupportUrl = "https://example.com/support"; group( - "profile fromResponse", + "parse", () { test( - "with no additional metadata", + "url with file extension, no headers", () { - final profile = Profile.fromResponse(validExtendedUrl, {}); + final profile = ProfileParser.parse(validBaseUrl, {}); expect(profile.name, equals("filename")); + expect(profile.url, equals(validBaseUrl)); + expect(profile.options, isNull); + expect(profile.subInfo, isNull); + }, + ); + + test( + "url with url, no headers", + () { + final profile = ProfileParser.parse(validExtendedUrl, {}); + + expect(profile.name, equals("b")); expect(profile.url, equals(validExtendedUrl)); expect(profile.options, isNull); expect(profile.subInfo, isNull); @@ -23,7 +36,7 @@ void main() { ); test( - "with all metadata", + "with base64 profile-title header", () { final headers = >{ "profile-title": ["base64:ZXhhbXBsZVRpdGxl"], @@ -34,7 +47,7 @@ void main() { "profile-web-page-url": [validBaseUrl], "support-url": [validSupportUrl], }; - final profile = Profile.fromResponse(validExtendedUrl, headers); + final profile = ProfileParser.parse(validExtendedUrl, headers); expect(profile.name, equals("exampleTitle")); expect(profile.url, equals(validExtendedUrl)); From 0c1768e05e9b85d444cf63c713ef22b5133b571e Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Sun, 26 Nov 2023 21:59:57 +0330 Subject: [PATCH 77/91] Refactor router --- lib/core/router/app_router.dart | 4 +- lib/core/router/routes.dart | 390 +++++++++++++++++- lib/core/router/routes/desktop_routes.dart | 137 ------ lib/core/router/routes/mobile_routes.dart | 178 -------- lib/core/router/routes/shared_routes.dart | 129 ------ lib/features/common/nested_app_bar.dart | 2 +- .../widgets/empty_profiles_home_body.dart | 2 +- lib/features/profile/widget/profile_tile.dart | 2 +- 8 files changed, 383 insertions(+), 461 deletions(-) delete mode 100644 lib/core/router/routes/desktop_routes.dart delete mode 100644 lib/core/router/routes/mobile_routes.dart delete mode 100644 lib/core/router/routes/shared_routes.dart diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index 5de8f211..74a6587f 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -38,7 +38,9 @@ GoRouter router(RouterRef ref) { navigatorKey: rootNavigatorKey, initialLocation: initialLocation, debugLogDiagnostics: true, - routes: useMobileRouter ? mobileRoutes : desktopRoutes, + routes: [ + if (useMobileRouter) $mobileWrapperRoute else $desktopWrapperRoute, + ], refreshListenable: notifier, redirect: notifier.redirect, observers: [ diff --git a/lib/core/router/routes.dart b/lib/core/router/routes.dart index 5b9acd92..44a2089a 100644 --- a/lib/core/router/routes.dart +++ b/lib/core/router/routes.dart @@ -1,16 +1,380 @@ -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:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hiddify/core/router/app_router.dart'; +import 'package:hiddify/features/about/view/about_page.dart'; +import 'package:hiddify/features/common/adaptive_root_scaffold.dart'; +import 'package:hiddify/features/geo_asset/overview/geo_assets_overview_page.dart'; +import 'package:hiddify/features/home/view/view.dart'; +import 'package:hiddify/features/intro/intro_page.dart'; +import 'package:hiddify/features/logs/view/logs_page.dart'; +import 'package:hiddify/features/profile/add/add_profile_modal.dart'; +import 'package:hiddify/features/profile/details/profile_details_page.dart'; +import 'package:hiddify/features/profile/overview/profiles_overview_page.dart'; +import 'package:hiddify/features/proxies/view/view.dart'; +import 'package:hiddify/features/settings/view/config_options_page.dart'; +import 'package:hiddify/features/settings/view/per_app_proxy_page.dart'; +import 'package:hiddify/features/settings/view/settings_page.dart'; +import 'package:hiddify/utils/utils.dart'; -export 'routes/mobile_routes.dart'; -export 'routes/shared_routes.dart' hide $appRoutes; +part 'routes.g.dart'; -final mobileRoutes = [ - ...shared.$appRoutes, - ...mobile.$appRoutes, -]; +GlobalKey? _dynamicRootKey = + useMobileRouter ? rootNavigatorKey : null; -final desktopRoutes = [ - ...shared.$appRoutes, - ...desktop.$appRoutes, -]; +@TypedShellRoute( + routes: [ + TypedGoRoute(path: "/intro", name: IntroRoute.name), + TypedGoRoute( + path: "/", + name: HomeRoute.name, + routes: [ + TypedGoRoute( + path: "add", + name: AddProfileRoute.name, + ), + TypedGoRoute( + path: "profiles", + name: ProfilesOverviewRoute.name, + ), + TypedGoRoute( + path: "profiles/new", + name: NewProfileRoute.name, + ), + TypedGoRoute( + path: "profiles/:id", + name: ProfileDetailsRoute.name, + ), + TypedGoRoute( + path: "logs", + name: LogsRoute.name, + ), + TypedGoRoute( + path: "settings", + name: SettingsRoute.name, + routes: [ + TypedGoRoute( + path: "config-options", + name: ConfigOptionsRoute.name, + ), + TypedGoRoute( + path: "per-app-proxy", + name: PerAppProxyRoute.name, + ), + TypedGoRoute( + path: "routing-assets", + name: GeoAssetsRoute.name, + ), + ], + ), + TypedGoRoute( + path: "about", + name: AboutRoute.name, + ), + ], + ), + TypedGoRoute( + path: "/proxies", + name: ProxiesRoute.name, + ), + ], +) +class MobileWrapperRoute extends ShellRouteData { + const MobileWrapperRoute(); + + @override + Widget builder(BuildContext context, GoRouterState state, Widget navigator) { + return AdaptiveRootScaffold(navigator); + } +} + +@TypedShellRoute( + routes: [ + TypedGoRoute(path: "/intro", name: IntroRoute.name), + TypedGoRoute( + path: "/", + name: HomeRoute.name, + routes: [ + TypedGoRoute( + path: "add", + name: AddProfileRoute.name, + ), + TypedGoRoute( + path: "profiles", + name: ProfilesOverviewRoute.name, + ), + TypedGoRoute( + path: "profiles/new", + name: NewProfileRoute.name, + ), + TypedGoRoute( + path: "profiles/:id", + name: ProfileDetailsRoute.name, + ), + ], + ), + TypedGoRoute( + path: "/proxies", + name: ProxiesRoute.name, + ), + TypedGoRoute( + path: "/logs", + name: LogsRoute.name, + ), + TypedGoRoute( + path: "/settings", + name: SettingsRoute.name, + routes: [ + TypedGoRoute( + path: "config-options", + name: ConfigOptionsRoute.name, + ), + TypedGoRoute( + path: "routing-assets", + name: GeoAssetsRoute.name, + ), + ], + ), + TypedGoRoute( + path: "/about", + name: AboutRoute.name, + ), + ], +) +class DesktopWrapperRoute extends ShellRouteData { + const DesktopWrapperRoute(); + + @override + Widget builder(BuildContext context, GoRouterState state, Widget navigator) { + return AdaptiveRootScaffold(navigator); + } +} + +class IntroRoute extends GoRouteData { + const IntroRoute(); + static const name = "Intro"; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + return const MaterialPage( + fullscreenDialog: true, + name: name, + child: IntroPage(), + ); + } +} + +class HomeRoute extends GoRouteData { + const HomeRoute(); + static const name = "Home"; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + return const NoTransitionPage( + name: name, + child: HomePage(), + ); + } +} + +class ProxiesRoute extends GoRouteData { + const ProxiesRoute(); + static const name = "Proxies"; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + return const NoTransitionPage( + name: name, + child: ProxiesPage(), + ); + } +} + +class AddProfileRoute extends GoRouteData { + const AddProfileRoute({this.url}); + + final String? url; + + static const name = "Add Profile"; + + static final GlobalKey $parentNavigatorKey = rootNavigatorKey; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + return BottomSheetPage( + fixed: true, + name: name, + builder: (controller) => AddProfileModal( + url: url, + scrollController: controller, + ), + ); + } +} + +class ProfilesOverviewRoute extends GoRouteData { + const ProfilesOverviewRoute(); + static const name = "Profiles"; + + static final GlobalKey $parentNavigatorKey = rootNavigatorKey; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + return BottomSheetPage( + name: name, + builder: (controller) => + ProfilesOverviewModal(scrollController: controller), + ); + } +} + +class NewProfileRoute extends GoRouteData { + const NewProfileRoute(); + static const name = "New Profile"; + + static final GlobalKey $parentNavigatorKey = rootNavigatorKey; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + return const MaterialPage( + fullscreenDialog: true, + name: name, + child: ProfileDetailsPage("new"), + ); + } +} + +class ProfileDetailsRoute extends GoRouteData { + const ProfileDetailsRoute(this.id); + final String id; + static const name = "Profile Details"; + + static final GlobalKey $parentNavigatorKey = rootNavigatorKey; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + return MaterialPage( + fullscreenDialog: true, + name: name, + child: ProfileDetailsPage(id), + ); + } +} + +class LogsRoute extends GoRouteData { + const LogsRoute(); + static const name = "Logs"; + + static final GlobalKey? $parentNavigatorKey = _dynamicRootKey; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + if (useMobileRouter) { + return const MaterialPage( + fullscreenDialog: true, + name: name, + child: LogsPage(), + ); + } + return const NoTransitionPage(name: name, child: LogsPage()); + } +} + +class SettingsRoute extends GoRouteData { + const SettingsRoute(); + static const name = "Settings"; + + static final GlobalKey? $parentNavigatorKey = _dynamicRootKey; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + if (useMobileRouter) { + return const MaterialPage( + fullscreenDialog: true, + name: name, + child: SettingsPage(), + ); + } + return const NoTransitionPage(name: name, child: SettingsPage()); + } +} + +class ConfigOptionsRoute extends GoRouteData { + const ConfigOptionsRoute(); + static const name = "Config Options"; + + static final GlobalKey? $parentNavigatorKey = _dynamicRootKey; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + if (useMobileRouter) { + return const MaterialPage( + fullscreenDialog: true, + name: name, + child: ConfigOptionsPage(), + ); + } + return const MaterialPage( + fullscreenDialog: true, + name: name, + child: ConfigOptionsPage(), + ); + } +} + +class PerAppProxyRoute extends GoRouteData { + const PerAppProxyRoute(); + static const name = "Per-app Proxy"; + + static final GlobalKey $parentNavigatorKey = rootNavigatorKey; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + return const MaterialPage( + fullscreenDialog: true, + name: name, + child: PerAppProxyPage(), + ); + } +} + +class GeoAssetsRoute extends GoRouteData { + const GeoAssetsRoute(); + static const name = "Routing Assets"; + + static final GlobalKey? $parentNavigatorKey = _dynamicRootKey; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + if (useMobileRouter) { + return const MaterialPage( + fullscreenDialog: true, + name: name, + child: GeoAssetsOverviewPage(), + ); + } + return const MaterialPage( + fullscreenDialog: true, + name: name, + child: GeoAssetsOverviewPage(), + ); + } +} + +class AboutRoute extends GoRouteData { + const AboutRoute(); + static const name = "About"; + + static final GlobalKey? $parentNavigatorKey = _dynamicRootKey; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + if (useMobileRouter) { + return const MaterialPage( + fullscreenDialog: true, + name: name, + child: AboutPage(), + ); + } + return const NoTransitionPage(name: name, child: AboutPage()); + } +} diff --git a/lib/core/router/routes/desktop_routes.dart b/lib/core/router/routes/desktop_routes.dart deleted file mode 100644 index 3830b7c3..00000000 --- a/lib/core/router/routes/desktop_routes.dart +++ /dev/null @@ -1,137 +0,0 @@ -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/about/view/view.dart'; -import 'package:hiddify/features/common/adaptive_root_scaffold.dart'; -import 'package:hiddify/features/geo_asset/overview/geo_assets_overview_page.dart'; -import 'package:hiddify/features/logs/view/view.dart'; -import 'package:hiddify/features/settings/view/view.dart'; - -part 'desktop_routes.g.dart'; - -@TypedShellRoute( - routes: [ - TypedGoRoute( - path: HomeRoute.path, - name: HomeRoute.name, - routes: [ - TypedGoRoute( - path: AddProfileRoute.path, - name: AddProfileRoute.name, - ), - TypedGoRoute( - path: ProfilesRoute.path, - name: ProfilesRoute.name, - ), - TypedGoRoute( - path: NewProfileRoute.path, - name: NewProfileRoute.name, - ), - TypedGoRoute( - path: ProfileDetailsRoute.path, - name: ProfileDetailsRoute.name, - ), - ], - ), - TypedGoRoute( - path: ProxiesRoute.path, - name: ProxiesRoute.name, - ), - TypedGoRoute( - path: LogsRoute.path, - name: LogsRoute.name, - ), - TypedGoRoute( - path: SettingsRoute.path, - name: SettingsRoute.name, - routes: [ - TypedGoRoute( - path: ConfigOptionsRoute.path, - name: ConfigOptionsRoute.name, - ), - TypedGoRoute( - path: GeoAssetsRoute.path, - name: GeoAssetsRoute.name, - ), - ], - ), - TypedGoRoute( - path: AboutRoute.path, - name: AboutRoute.name, - ), - ], -) -class DesktopWrapperRoute extends ShellRouteData { - const DesktopWrapperRoute(); - - @override - Widget builder(BuildContext context, GoRouterState state, Widget navigator) { - return AdaptiveRootScaffold(navigator); - } -} - -class LogsRoute extends GoRouteData { - const LogsRoute(); - static const path = '/logs'; - static const name = 'Logs'; - - @override - Page buildPage(BuildContext context, GoRouterState state) { - return const NoTransitionPage(name: name, child: LogsPage()); - } -} - -class SettingsRoute extends GoRouteData { - const SettingsRoute(); - static const path = '/settings'; - static const name = 'Settings'; - - @override - Page buildPage(BuildContext context, GoRouterState state) { - return const NoTransitionPage(name: name, child: SettingsPage()); - } -} - -class ConfigOptionsRoute extends GoRouteData { - const ConfigOptionsRoute(); - static const path = 'config-options'; - static const name = 'Config Options'; - - @override - Page buildPage(BuildContext context, GoRouterState state) { - return const MaterialPage( - fullscreenDialog: true, - name: name, - child: ConfigOptionsPage(), - ); - } -} - -class GeoAssetsRoute extends GoRouteData { - const GeoAssetsRoute(); - static const path = 'routing-assets'; - static const name = 'Routing Assets'; - - @override - Page buildPage(BuildContext context, GoRouterState state) { - return const MaterialPage( - fullscreenDialog: true, - name: name, - child: GeoAssetsOverviewPage(), - ); - } -} - -class AboutRoute extends GoRouteData { - const AboutRoute(); - static const path = '/about'; - static const name = 'About'; - - @override - Page buildPage(BuildContext context, GoRouterState state) { - return const NoTransitionPage( - name: name, - child: AboutPage(), - ); - } -} diff --git a/lib/core/router/routes/mobile_routes.dart b/lib/core/router/routes/mobile_routes.dart deleted file mode 100644 index 5dcf0802..00000000 --- a/lib/core/router/routes/mobile_routes.dart +++ /dev/null @@ -1,178 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:hiddify/core/router/app_router.dart'; -import 'package:hiddify/core/router/routes/shared_routes.dart'; -import 'package:hiddify/features/about/view/view.dart'; -import 'package:hiddify/features/common/adaptive_root_scaffold.dart'; -import 'package:hiddify/features/geo_asset/overview/geo_assets_overview_page.dart'; -import 'package:hiddify/features/logs/view/view.dart'; -import 'package:hiddify/features/settings/view/view.dart'; - -part 'mobile_routes.g.dart'; - -@TypedShellRoute( - routes: [ - TypedGoRoute( - path: HomeRoute.path, - name: HomeRoute.name, - routes: [ - TypedGoRoute( - path: AddProfileRoute.path, - name: AddProfileRoute.name, - ), - TypedGoRoute( - path: ProfilesRoute.path, - name: ProfilesRoute.name, - ), - TypedGoRoute( - path: NewProfileRoute.path, - name: NewProfileRoute.name, - ), - TypedGoRoute( - path: ProfileDetailsRoute.path, - name: ProfileDetailsRoute.name, - ), - TypedGoRoute( - path: LogsRoute.path, - name: LogsRoute.name, - ), - TypedGoRoute( - path: SettingsRoute.path, - name: SettingsRoute.name, - routes: [ - TypedGoRoute( - path: ConfigOptionsRoute.path, - name: ConfigOptionsRoute.name, - ), - TypedGoRoute( - path: PerAppProxyRoute.path, - name: PerAppProxyRoute.name, - ), - TypedGoRoute( - path: GeoAssetsRoute.path, - name: GeoAssetsRoute.name, - ), - ], - ), - TypedGoRoute( - path: AboutRoute.path, - name: AboutRoute.name, - ), - ], - ), - TypedGoRoute( - path: ProxiesRoute.path, - name: ProxiesRoute.name, - ), - ], -) -class MobileWrapperRoute extends ShellRouteData { - const MobileWrapperRoute(); - - @override - Widget builder(BuildContext context, GoRouterState state, Widget navigator) { - return AdaptiveRootScaffold(navigator); - } -} - -class LogsRoute extends GoRouteData { - const LogsRoute(); - static const path = 'logs'; - static const name = 'Logs'; - - static final GlobalKey $parentNavigatorKey = rootNavigatorKey; - - @override - Page buildPage(BuildContext context, GoRouterState state) { - return const MaterialPage( - fullscreenDialog: true, - name: name, - child: LogsPage(), - ); - } -} - -class SettingsRoute extends GoRouteData { - const SettingsRoute(); - static const path = 'settings'; - static const name = 'Settings'; - - static final GlobalKey $parentNavigatorKey = rootNavigatorKey; - - @override - Page buildPage(BuildContext context, GoRouterState state) { - return const MaterialPage( - fullscreenDialog: true, - name: name, - child: SettingsPage(), - ); - } -} - -class ConfigOptionsRoute extends GoRouteData { - const ConfigOptionsRoute(); - static const path = 'config-options'; - static const name = 'Config Options'; - - static final GlobalKey $parentNavigatorKey = rootNavigatorKey; - - @override - Page buildPage(BuildContext context, GoRouterState state) { - return const MaterialPage( - fullscreenDialog: true, - name: name, - child: ConfigOptionsPage(), - ); - } -} - -class PerAppProxyRoute extends GoRouteData { - const PerAppProxyRoute(); - static const path = 'per-app-proxy'; - static const name = 'Per-app Proxy'; - - static final GlobalKey $parentNavigatorKey = rootNavigatorKey; - - @override - Page buildPage(BuildContext context, GoRouterState state) { - return const MaterialPage( - fullscreenDialog: true, - name: name, - child: PerAppProxyPage(), - ); - } -} - -class GeoAssetsRoute extends GoRouteData { - const GeoAssetsRoute(); - static const path = 'routing-assets'; - static const name = 'Routing Assets'; - - static final GlobalKey $parentNavigatorKey = rootNavigatorKey; - - @override - Page buildPage(BuildContext context, GoRouterState state) { - return const MaterialPage( - fullscreenDialog: true, - name: name, - child: GeoAssetsOverviewPage(), - ); - } -} - -class AboutRoute extends GoRouteData { - const AboutRoute(); - static const path = 'about'; - static const name = 'About'; - - static final GlobalKey $parentNavigatorKey = rootNavigatorKey; - - @override - Page buildPage(BuildContext context, GoRouterState state) { - return const MaterialPage( - fullscreenDialog: true, - name: name, - child: AboutPage(), - ); - } -} diff --git a/lib/core/router/routes/shared_routes.dart b/lib/core/router/routes/shared_routes.dart deleted file mode 100644 index 1960b7cc..00000000 --- a/lib/core/router/routes/shared_routes.dart +++ /dev/null @@ -1,129 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:hiddify/core/router/app_router.dart'; -import 'package:hiddify/features/home/view/view.dart'; -import 'package:hiddify/features/intro/intro_page.dart'; -import 'package:hiddify/features/profile/add/add_profile_modal.dart'; -import 'package:hiddify/features/profile/details/profile_details_page.dart'; -import 'package:hiddify/features/profile/overview/profiles_overview_page.dart'; -import 'package:hiddify/features/proxies/view/view.dart'; -import 'package:hiddify/utils/utils.dart'; - -part 'shared_routes.g.dart'; - -class HomeRoute extends GoRouteData { - const HomeRoute(); - static const path = '/'; - static const name = 'Home'; - - @override - Page buildPage(BuildContext context, GoRouterState state) { - return const NoTransitionPage( - name: name, - child: HomePage(), - ); - } -} - -class ProxiesRoute extends GoRouteData { - const ProxiesRoute(); - static const path = '/proxies'; - static const name = 'Proxies'; - - @override - Page buildPage(BuildContext context, GoRouterState state) { - return const NoTransitionPage( - name: name, - child: ProxiesPage(), - ); - } -} - -class AddProfileRoute extends GoRouteData { - const AddProfileRoute({this.url}); - static const path = 'add'; - static const name = 'Add Profile'; - final String? url; - - static final GlobalKey $parentNavigatorKey = rootNavigatorKey; - - @override - Page buildPage(BuildContext context, GoRouterState state) { - return BottomSheetPage( - fixed: true, - name: name, - builder: (controller) => AddProfileModal( - url: url, - scrollController: controller, - ), - ); - } -} - -@TypedGoRoute(path: IntroRoute.path, name: IntroRoute.name) -class IntroRoute extends GoRouteData { - const IntroRoute(); - static const path = '/intro'; - static const name = 'Intro'; - - @override - Page buildPage(BuildContext context, GoRouterState state) { - return const MaterialPage( - fullscreenDialog: true, - name: name, - child: IntroPage(), - ); - } -} - -class ProfilesRoute extends GoRouteData { - const ProfilesRoute(); - static const path = 'profiles'; - static const name = 'Profiles'; - - static final GlobalKey $parentNavigatorKey = rootNavigatorKey; - - @override - Page buildPage(BuildContext context, GoRouterState state) { - return BottomSheetPage( - name: name, - builder: (controller) => - ProfilesOverviewModal(scrollController: controller), - ); - } -} - -class NewProfileRoute extends GoRouteData { - const NewProfileRoute(); - static const path = 'profiles/new'; - static const name = 'New Profile'; - - static final GlobalKey $parentNavigatorKey = rootNavigatorKey; - - @override - Page buildPage(BuildContext context, GoRouterState state) { - return const MaterialPage( - fullscreenDialog: true, - name: name, - child: ProfileDetailsPage("new"), - ); - } -} - -class ProfileDetailsRoute extends GoRouteData { - const ProfileDetailsRoute(this.id); - final String id; - static const path = 'profiles/:id'; - static const name = 'Profile Details'; - - static final GlobalKey $parentNavigatorKey = rootNavigatorKey; - - @override - Page buildPage(BuildContext context, GoRouterState state) { - return MaterialPage( - fullscreenDialog: true, - name: name, - child: ProfileDetailsPage(id), - ); - } -} diff --git a/lib/features/common/nested_app_bar.dart b/lib/features/common/nested_app_bar.dart index 0d872532..e9281f03 100644 --- a/lib/features/common/nested_app_bar.dart +++ b/lib/features/common/nested_app_bar.dart @@ -7,7 +7,7 @@ bool showDrawerButton(BuildContext context) { if (!useMobileRouter) return true; final String location = GoRouterState.of(context).uri.path; if (location == const HomeRoute().location || - location == const ProfilesRoute().location) return true; + location == const ProfilesOverviewRoute().location) return true; if (location.startsWith(const ProxiesRoute().location)) return true; return false; } diff --git a/lib/features/home/widgets/empty_profiles_home_body.dart b/lib/features/home/widgets/empty_profiles_home_body.dart index a633dce9..8937ce18 100644 --- a/lib/features/home/widgets/empty_profiles_home_body.dart +++ b/lib/features/home/widgets/empty_profiles_home_body.dart @@ -44,7 +44,7 @@ class EmptyActiveProfileHomeBody extends HookConsumerWidget { Text(t.home.noActiveProfileMsg), const Gap(16), OutlinedButton( - onPressed: () => const ProfilesRoute().push(context), + onPressed: () => const ProfilesOverviewRoute().push(context), child: Text(t.profile.overviewPageTitle), ), ], diff --git a/lib/features/profile/widget/profile_tile.dart b/lib/features/profile/widget/profile_tile.dart index 7d526491..c0000cb4 100644 --- a/lib/features/profile/widget/profile_tile.dart +++ b/lib/features/profile/widget/profile_tile.dart @@ -90,7 +90,7 @@ class ProfileTile extends HookConsumerWidget { child: InkWell( onTap: () { if (isMain) { - const ProfilesRoute().go(context); + const ProfilesOverviewRoute().go(context); } else { if (selectActiveMutation.state.isInProgress) return; if (profile.active) return; From 9499c292d71e173beb4e2afd9d038927bba3c877 Mon Sep 17 00:00:00 2001 From: Hiddify Date: Mon, 27 Nov 2023 13:23:32 +0100 Subject: [PATCH 78/91] new: send all releases to beta by default --- .github/workflows/build.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8b60afe9..59f82e2b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -370,7 +370,8 @@ jobs: packageName: app.hiddify.com releaseName: ${{ github.ref }} releaseFiles: ./hiddify-android-market.aab - track: ${{ env.CHANNEL == 'dev' && 'beta' || 'internal' }} + # track: ${{ env.CHANNEL == 'dev' && 'beta' || 'internal' }} + track: 'beta' # - name: "Upload app to TestFlight" # uses: apple-actions/upload-testflight-build@v1 From 0961cfeb6803e1d06dd8fcbdb2e4cbe302290adc Mon Sep 17 00:00:00 2001 From: Hiddify Date: Mon, 27 Nov 2023 13:24:02 +0100 Subject: [PATCH 79/91] new: add user-agent like clash sing-box for better compatibility --- lib/domain/app/app_info.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/domain/app/app_info.dart b/lib/domain/app/app_info.dart index 1e02660a..51d09283 100644 --- a/lib/domain/app/app_info.dart +++ b/lib/domain/app/app_info.dart @@ -19,7 +19,7 @@ class AppInfo with _$AppInfo { required Environment environment, }) = _AppInfo; - String get userAgent => "HiddifyNext/$version ($operatingSystem)"; + String get userAgent => "HiddifyNext/$version ($operatingSystem) like ClashMeta v2ray sing-box"; String get presentVersion => environment == Environment.prod ? version From d12d56b612fe902ca4b4c4f8fb29d7637869abe2 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Mon, 27 Nov 2023 15:33:10 +0330 Subject: [PATCH 80/91] Update core (singbox 1.7.0-rc.2) --- .../com/hiddify/hiddify/bg/PlatformInterfaceWrapper.kt | 5 +++++ dependencies.properties | 2 +- libcore | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/PlatformInterfaceWrapper.kt b/android/app/src/main/kotlin/com/hiddify/hiddify/bg/PlatformInterfaceWrapper.kt index 9345bef7..7ce41726 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/PlatformInterfaceWrapper.kt +++ b/android/app/src/main/kotlin/com/hiddify/hiddify/bg/PlatformInterfaceWrapper.kt @@ -10,6 +10,7 @@ import io.nekohasekai.libbox.NetworkInterfaceIterator import io.nekohasekai.libbox.PlatformInterface import io.nekohasekai.libbox.StringIterator import io.nekohasekai.libbox.TunOptions +import io.nekohasekai.libbox.WIFIState import java.net.Inet6Address import java.net.InetSocketAddress import java.net.InterfaceAddress @@ -101,6 +102,10 @@ interface PlatformInterfaceWrapper : PlatformInterface { override fun clearDNSCache() { } + override fun readWIFIState(): WIFIState? { + return null + } + private class InterfaceArray(private val iterator: Enumeration) : NetworkInterfaceIterator { diff --git a/dependencies.properties b/dependencies.properties index 75a0ede1..7615b712 100644 --- a/dependencies.properties +++ b/dependencies.properties @@ -1 +1 @@ -core.version=0.8.2 \ No newline at end of file +core.version=0.8.3 \ No newline at end of file diff --git a/libcore b/libcore index 2f865d5c..2d0f5eee 160000 --- a/libcore +++ b/libcore @@ -1 +1 @@ -Subproject commit 2f865d5c8a11966ab11e777bc0b67a999fa07126 +Subproject commit 2d0f5eee4d8db693d68954e8cca7a80ac749ba86 From bb745c2ec1ae4f76dfa48faf05b89a1db1dbba2e Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Mon, 27 Nov 2023 15:33:25 +0330 Subject: [PATCH 81/91] Remove unused clash api --- lib/data/api/clash_api.dart | 145 ---------------------- lib/data/data_providers.dart | 6 - lib/data/repository/core_facade_impl.dart | 66 ---------- lib/domain/clash/clash.dart | 6 - lib/domain/clash/clash_config.dart | 66 ---------- lib/domain/clash/clash_enums.dart | 62 --------- lib/domain/clash/clash_facade.dart | 24 ---- lib/domain/clash/clash_log.dart | 22 ---- lib/domain/clash/clash_proxy.dart | 61 --------- lib/domain/clash/clash_traffic.dart | 17 --- lib/domain/core_facade.dart | 3 +- 11 files changed, 1 insertion(+), 477 deletions(-) delete mode 100644 lib/data/api/clash_api.dart delete mode 100644 lib/domain/clash/clash.dart delete mode 100644 lib/domain/clash/clash_config.dart delete mode 100644 lib/domain/clash/clash_enums.dart delete mode 100644 lib/domain/clash/clash_facade.dart delete mode 100644 lib/domain/clash/clash_log.dart delete mode 100644 lib/domain/clash/clash_proxy.dart delete mode 100644 lib/domain/clash/clash_traffic.dart diff --git a/lib/data/api/clash_api.dart b/lib/data/api/clash_api.dart deleted file mode 100644 index c73397c9..00000000 --- a/lib/data/api/clash_api.dart +++ /dev/null @@ -1,145 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:dio/dio.dart'; -import 'package:fpdart/fpdart.dart'; -import 'package:hiddify/domain/clash/clash.dart'; -import 'package:hiddify/domain/constants.dart'; -import 'package:hiddify/utils/utils.dart'; -import 'package:web_socket_channel/web_socket_channel.dart'; - -class ClashApi with InfraLogger { - ClashApi(int port) : address = "${Constants.localHost}:$port"; - - final String address; - - late final _clashDio = Dio( - BaseOptions( - baseUrl: "http://$address", - connectTimeout: const Duration(seconds: 3), - receiveTimeout: const Duration(seconds: 10), - sendTimeout: const Duration(seconds: 3), - ), - ); - - TaskEither> getProxies() { - return TaskEither( - () async { - final response = await _clashDio.get("/proxies"); - if (response.statusCode != 200 || response.data == null) { - return left(response.statusMessage ?? ""); - } - final proxies = ((jsonDecode(response.data! as String) - as Map)["proxies"] as Map) - .entries - .map( - (e) { - final proxyMap = (e.value as Map) - ..putIfAbsent('name', () => e.key); - return ClashProxy.fromJson(proxyMap); - }, - ); - return right(proxies.toList()); - }, - ); - } - - TaskEither changeProxy(String selectorName, String proxyName) { - return TaskEither( - () async { - final response = await _clashDio.put( - "/proxies/$selectorName", - data: {"name": proxyName}, - ); - if (response.statusCode != HttpStatus.noContent) { - return left(response.statusMessage ?? ""); - } - return right(unit); - }, - ); - } - - TaskEither getProxyDelay( - String name, - String url, { - Duration timeout = const Duration(seconds: 10), - }) { - return TaskEither( - () async { - final response = await _clashDio.get( - "/proxies/$name/delay", - queryParameters: { - "timeout": timeout.inMilliseconds, - "url": url, - }, - ); - if (response.statusCode != 200 || response.data == null) { - return left(response.statusMessage ?? ""); - } - return right(response.data!["delay"] as int); - }, - ); - } - - TaskEither getConfigs() { - return TaskEither( - () async { - final response = await _clashDio.get("/configs"); - if (response.statusCode != 200 || response.data == null) { - return left(response.statusMessage ?? ""); - } - final config = - ClashConfig.fromJson(response.data as Map); - return right(config); - }, - ); - } - - TaskEither updateConfigs(String path) { - return TaskEither.of(unit); - } - - TaskEither patchConfigs(ClashConfig config) { - return TaskEither( - () async { - final response = await _clashDio.patch( - "/configs", - data: config.toJson(), - ); - if (response.statusCode != HttpStatus.noContent) { - return left(response.statusMessage ?? ""); - } - return right(unit); - }, - ); - } - - Stream watchLogs(LogLevel level) { - return const Stream.empty(); - } - - Stream watchTraffic() { - final channel = WebSocketChannel.connect( - Uri.parse("ws://$address/traffic"), - ); - return channel.stream.map( - (event) { - return ClashTraffic.fromJson( - jsonDecode(event as String) as Map, - ); - }, - ); - } - - TaskEither getTraffic() { - return TaskEither( - () async { - final response = await _clashDio.get>("/traffic"); - if (response.statusCode != 200 || response.data == null) { - return left(response.statusMessage ?? ""); - } - return right(ClashTraffic.fromJson(response.data!)); - }, - ); - } -} diff --git a/lib/data/data_providers.dart b/lib/data/data_providers.dart index d70bef5b..0f1b653c 100644 --- a/lib/data/data_providers.dart +++ b/lib/data/data_providers.dart @@ -3,13 +3,11 @@ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:hiddify/core/core_providers.dart'; import 'package:hiddify/core/prefs/general_prefs.dart'; -import 'package:hiddify/data/api/clash_api.dart'; import 'package:hiddify/data/local/database.dart'; import 'package:hiddify/data/repository/app_repository_impl.dart'; import 'package:hiddify/data/repository/config_options_store.dart'; import 'package:hiddify/data/repository/repository.dart'; import 'package:hiddify/domain/app/app.dart'; -import 'package:hiddify/domain/constants.dart'; import 'package:hiddify/domain/core_facade.dart'; import 'package:hiddify/domain/singbox/singbox.dart'; import 'package:hiddify/features/geo_asset/data/geo_asset_data_providers.dart'; @@ -51,9 +49,6 @@ Dio dio(DioRef ref) { AppRepository appRepository(AppRepositoryRef ref) => AppRepositoryImpl(ref.watch(dioProvider)); -@Riverpod(keepAlive: true) -ClashApi clashApi(ClashApiRef ref) => ClashApi(Defaults.clashApiPort); - @riverpod Future configOptions(ConfigOptionsRef ref) async { final geoAssets = await ref @@ -86,7 +81,6 @@ CoreFacade coreFacade(CoreFacadeRef ref) => CoreFacadeImpl( ref.watch(geoAssetPathResolverProvider), ref.watch(profilePathResolverProvider), ref.watch(platformServicesProvider), - ref.watch(clashApiProvider), ref.read(debugModeNotifierProvider), () => ref.read(configOptionsProvider.future), ); diff --git a/lib/data/repository/core_facade_impl.dart b/lib/data/repository/core_facade_impl.dart index be953ec1..375a1e8b 100644 --- a/lib/data/repository/core_facade_impl.dart +++ b/lib/data/repository/core_facade_impl.dart @@ -2,11 +2,8 @@ import 'dart:convert'; import 'dart:io'; import 'package:fpdart/fpdart.dart'; -import 'package:hiddify/data/api/clash_api.dart'; import 'package:hiddify/data/repository/exception_handlers.dart'; -import 'package:hiddify/domain/clash/clash.dart'; import 'package:hiddify/domain/connectivity/connection_status.dart'; -import 'package:hiddify/domain/constants.dart'; import 'package:hiddify/domain/core_facade.dart'; import 'package:hiddify/domain/core_service_failure.dart'; import 'package:hiddify/domain/singbox/singbox.dart'; @@ -24,7 +21,6 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { this.geoAssetPathResolver, this.profilePathResolver, this.platformServices, - this.clash, this.debug, this.configOptions, ); @@ -34,7 +30,6 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { final GeoAssetPathResolver geoAssetPathResolver; final ProfilePathResolver profilePathResolver; final PlatformServices platformServices; - final ClashApi clash; final bool debug; final Future Function() configOptions; @@ -263,67 +258,6 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { ); } - @override - TaskEither getConfigs() { - return exceptionHandler( - () async => clash.getConfigs().mapLeft(CoreServiceFailure.other).run(), - CoreServiceFailure.unexpected, - ); - } - - @override - TaskEither patchOverrides(ClashConfig overrides) { - return exceptionHandler( - () async => - clash.patchConfigs(overrides).mapLeft(CoreServiceFailure.other).run(), - CoreServiceFailure.unexpected, - ); - } - - @override - TaskEither> getProxies() { - return exceptionHandler( - () async => clash.getProxies().mapLeft(CoreServiceFailure.other).run(), - CoreServiceFailure.unexpected, - ); - } - - @override - TaskEither changeProxy( - String selectorName, - String proxyName, - ) { - return exceptionHandler( - () async => clash - .changeProxy(selectorName, proxyName) - .mapLeft(CoreServiceFailure.other) - .run(), - CoreServiceFailure.unexpected, - ); - } - - @override - Stream> watchTraffic() { - return clash.watchTraffic().handleExceptions(CoreServiceFailure.unexpected); - } - - @override - TaskEither testDelay( - String proxyName, { - String testUrl = Defaults.connectionTestUrl, - }) { - return exceptionHandler( - () async { - final result = clash - .getProxyDelay(proxyName, testUrl) - .mapLeft(CoreServiceFailure.other) - .run(); - return result; - }, - CoreServiceFailure.unexpected, - ); - } - @override Stream watchConnectionStatus() => singbox.watchConnectionStatus(); diff --git a/lib/domain/clash/clash.dart b/lib/domain/clash/clash.dart deleted file mode 100644 index 4db9fff2..00000000 --- a/lib/domain/clash/clash.dart +++ /dev/null @@ -1,6 +0,0 @@ -export 'clash_config.dart'; -export 'clash_enums.dart'; -export 'clash_facade.dart'; -export 'clash_log.dart'; -export 'clash_proxy.dart'; -export 'clash_traffic.dart'; diff --git a/lib/domain/clash/clash_config.dart b/lib/domain/clash/clash_config.dart deleted file mode 100644 index 5e515ea4..00000000 --- a/lib/domain/clash/clash_config.dart +++ /dev/null @@ -1,66 +0,0 @@ -import 'package:fpdart/fpdart.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:hiddify/domain/clash/clash_enums.dart'; - -part 'clash_config.freezed.dart'; -part 'clash_config.g.dart'; - -@freezed -class ClashConfig with _$ClashConfig { - const ClashConfig._(); - - @JsonSerializable(includeIfNull: false, fieldRename: FieldRename.kebab) - const factory ClashConfig({ - @JsonKey(name: 'port') int? httpPort, - int? socksPort, - int? redirPort, - int? tproxyPort, - int? mixedPort, - List? authentication, - bool? allowLan, - String? bindAddress, - TunnelMode? mode, - LogLevel? logLevel, - bool? ipv6, - }) = _ClashConfig; - - ClashConfig patch(ClashConfigPatch patch) { - return copyWith( - httpPort: (patch.httpPort ?? optionOf(httpPort)).toNullable(), - socksPort: (patch.socksPort ?? optionOf(socksPort)).toNullable(), - redirPort: (patch.redirPort ?? optionOf(redirPort)).toNullable(), - tproxyPort: (patch.tproxyPort ?? optionOf(tproxyPort)).toNullable(), - mixedPort: (patch.mixedPort ?? optionOf(mixedPort)).toNullable(), - authentication: - (patch.authentication ?? optionOf(authentication)).toNullable(), - allowLan: (patch.allowLan ?? optionOf(allowLan)).toNullable(), - bindAddress: (patch.bindAddress ?? optionOf(bindAddress)).toNullable(), - mode: (patch.mode ?? optionOf(mode)).toNullable(), - logLevel: (patch.logLevel ?? optionOf(logLevel)).toNullable(), - ipv6: (patch.ipv6 ?? optionOf(ipv6)).toNullable(), - ); - } - - factory ClashConfig.fromJson(Map json) => - _$ClashConfigFromJson(json); -} - -@freezed -class ClashConfigPatch with _$ClashConfigPatch { - const ClashConfigPatch._(); - - @JsonSerializable(includeIfNull: false) - const factory ClashConfigPatch({ - Option? httpPort, - Option? socksPort, - Option? redirPort, - Option? tproxyPort, - Option? mixedPort, - Option>? authentication, - Option? allowLan, - Option? bindAddress, - Option? mode, - Option? logLevel, - Option? ipv6, - }) = _ClashConfigPatch; -} diff --git a/lib/domain/clash/clash_enums.dart b/lib/domain/clash/clash_enums.dart deleted file mode 100644 index f8ad5af3..00000000 --- a/lib/domain/clash/clash_enums.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'package:flutter/material.dart'; - -enum TunnelMode { - rule, - global, - direct; -} - -enum LogLevel { - info, - warning, - error, - debug, - silent; - - Color get color => switch (this) { - info => Colors.lightGreen, - warning => Colors.orangeAccent, - error => Colors.redAccent, - debug => Colors.lightBlue, - _ => Colors.white, - }; -} - -enum ProxyType { - direct("Direct"), - reject("Reject"), - compatible("Compatible"), - pass("Pass"), - shadowSocks("ShadowSocks"), - shadowSocksR("ShadowSocksR"), - snell("Snell"), - socks5("Socks5"), - http("Http"), - vmess("Vmess"), - vless("Vless"), - trojan("Trojan"), - hysteria("Hysteria"), - wireGuard("WireGuard"), - tuic("Tuic"), - ssh("SSH"), - relay("Relay"), - selector("Selector"), - fallback("Fallback"), - urlTest("URLTest", "urltest"), - loadBalance("LoadBalance"), - unknown("Unknown"); - - const ProxyType(this.label, [this._key]); - - final String? _key; - final String label; - - String get key => _key ?? name; - - static List groupValues = [ - selector, - fallback, - urlTest, - loadBalance, - ]; -} diff --git a/lib/domain/clash/clash_facade.dart b/lib/domain/clash/clash_facade.dart deleted file mode 100644 index 2a2443ee..00000000 --- a/lib/domain/clash/clash_facade.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:fpdart/fpdart.dart'; -import 'package:hiddify/domain/clash/clash.dart'; -import 'package:hiddify/domain/constants.dart'; -import 'package:hiddify/domain/core_service_failure.dart'; - -abstract class ClashFacade { - TaskEither getConfigs(); - - TaskEither patchOverrides(ClashConfig overrides); - - TaskEither> getProxies(); - - TaskEither changeProxy( - String selectorName, - String proxyName, - ); - - TaskEither testDelay( - String proxyName, { - String testUrl = Defaults.connectionTestUrl, - }); - - Stream> watchTraffic(); -} diff --git a/lib/domain/clash/clash_log.dart b/lib/domain/clash/clash_log.dart deleted file mode 100644 index c016e6ba..00000000 --- a/lib/domain/clash/clash_log.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:hiddify/domain/clash/clash_enums.dart'; - -part 'clash_log.freezed.dart'; -part 'clash_log.g.dart'; - -@freezed -class ClashLog with _$ClashLog { - const ClashLog._(); - - const factory ClashLog({ - @JsonKey(name: 'type') required LogLevel level, - @JsonKey(name: 'payload') required String message, - @JsonKey(defaultValue: DateTime.now) required DateTime time, - }) = _ClashLog; - - String get timeStamp => - "${time.month}-${time.day} ${time.hour}:${time.minute}:${time.second}"; - - factory ClashLog.fromJson(Map json) => - _$ClashLogFromJson(json); -} diff --git a/lib/domain/clash/clash_proxy.dart b/lib/domain/clash/clash_proxy.dart deleted file mode 100644 index 28e5b9ad..00000000 --- a/lib/domain/clash/clash_proxy.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:dartx/dartx.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:hiddify/domain/clash/clash_enums.dart'; - -part 'clash_proxy.freezed.dart'; -part 'clash_proxy.g.dart'; - -// TODO: test and improve -@Freezed(fromJson: true) -sealed class ClashProxy with _$ClashProxy { - const ClashProxy._(); - - const factory ClashProxy.group({ - required String name, - @JsonKey(fromJson: _typeFromJson) required ProxyType type, - required List all, - required String now, - @Default(false) bool udp, - List? history, - @JsonKey(includeFromJson: false, includeToJson: false) int? delay, - }) = ClashProxyGroup; - - const factory ClashProxy.item({ - required String name, - @JsonKey(fromJson: _typeFromJson) required ProxyType type, - @Default(false) bool udp, - List? history, - @JsonKey(includeFromJson: false, includeToJson: false) int? delay, - }) = ClashProxyItem; - - factory ClashProxy.fromJson(Map json) { - final isGroup = json.containsKey('all') || - json.containsKey('now') || - ProxyType.groupValues.any( - (e) => e.label == json.getOrElse('type', () => null), - ); - if (isGroup) { - return ClashProxyGroup.fromJson(json); - } else { - return ClashProxyItem.fromJson(json); - } - } -} - -ProxyType _typeFromJson(dynamic type) => - ProxyType.values - .firstOrNullWhere((e) => e.key == (type as String?)?.toLowerCase()) ?? - ProxyType.unknown; - -@freezed -class ClashHistory with _$ClashHistory { - const ClashHistory._(); - - const factory ClashHistory({ - required String time, - required int delay, - }) = _ClashHistory; - - factory ClashHistory.fromJson(Map json) => - _$ClashHistoryFromJson(json); -} diff --git a/lib/domain/clash/clash_traffic.dart b/lib/domain/clash/clash_traffic.dart deleted file mode 100644 index 8602bc26..00000000 --- a/lib/domain/clash/clash_traffic.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'clash_traffic.freezed.dart'; -part 'clash_traffic.g.dart'; - -@freezed -class ClashTraffic with _$ClashTraffic { - const ClashTraffic._(); - - const factory ClashTraffic({ - @JsonKey(name: 'up') required int upload, - @JsonKey(name: 'down') required int download, - }) = _ClashTraffic; - - factory ClashTraffic.fromJson(Map json) => - _$ClashTrafficFromJson(json); -} diff --git a/lib/domain/core_facade.dart b/lib/domain/core_facade.dart index a3ada4aa..15d0ba48 100644 --- a/lib/domain/core_facade.dart +++ b/lib/domain/core_facade.dart @@ -1,4 +1,3 @@ -import 'package:hiddify/domain/clash/clash.dart'; import 'package:hiddify/domain/singbox/singbox.dart'; -abstract interface class CoreFacade implements SingboxFacade, ClashFacade {} +abstract interface class CoreFacade implements SingboxFacade {} From 9c165e178b48a3c26eb10ac131c5aec606e33378 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Tue, 28 Nov 2023 18:24:31 +0330 Subject: [PATCH 82/91] Refactor logs --- lib/bootstrap.dart | 4 +- lib/core/router/app_router.dart | 4 +- lib/core/router/routes.dart | 18 ++--- lib/data/repository/config_options_store.dart | 1 + lib/data/repository/core_facade_impl.dart | 18 ----- lib/domain/singbox/box_log.dart | 62 ---------------- lib/domain/singbox/config_options.dart | 2 +- lib/domain/singbox/singbox.dart | 1 - lib/domain/singbox/singbox_facade.dart | 4 -- lib/features/log/data/log_data_providers.dart | 23 ++++++ lib/features/log/data/log_parser.dart | 33 +++++++++ lib/features/log/data/log_path_resolver.dart | 19 +++++ lib/features/log/data/log_repository.dart | 70 +++++++++++++++++++ lib/features/log/model/log_entity.dart | 13 ++++ lib/features/log/model/log_failure.dart | 25 +++++++ lib/features/log/model/log_level.dart | 25 +++++++ .../overview/logs_overview_notifier.dart} | 31 ++++---- .../overview/logs_overview_page.dart} | 24 +++---- .../log/overview/logs_overview_state.dart | 18 +++++ lib/features/logs/notifier/logs_state.dart | 17 ----- lib/features/logs/notifier/notifier.dart | 2 - lib/features/logs/view/view.dart | 1 - .../settings/view/config_options_page.dart | 1 + .../system_tray/system_tray_controller.dart | 2 +- lib/services/files_editor_service.dart | 17 ----- 25 files changed, 272 insertions(+), 163 deletions(-) delete mode 100644 lib/domain/singbox/box_log.dart create mode 100644 lib/features/log/data/log_data_providers.dart create mode 100644 lib/features/log/data/log_parser.dart create mode 100644 lib/features/log/data/log_path_resolver.dart create mode 100644 lib/features/log/data/log_repository.dart create mode 100644 lib/features/log/model/log_entity.dart create mode 100644 lib/features/log/model/log_failure.dart create mode 100644 lib/features/log/model/log_level.dart rename lib/features/{logs/notifier/logs_notifier.dart => log/overview/logs_overview_notifier.dart} (79%) rename lib/features/{logs/view/logs_page.dart => log/overview/logs_overview_page.dart} (92%) create mode 100644 lib/features/log/overview/logs_overview_state.dart delete mode 100644 lib/features/logs/notifier/logs_state.dart delete mode 100644 lib/features/logs/notifier/notifier.dart delete mode 100644 lib/features/logs/view/view.dart diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index 465d22d6..3a610431 100644 --- a/lib/bootstrap.dart +++ b/lib/bootstrap.dart @@ -12,6 +12,7 @@ import 'package:hiddify/data/repository/app_repository_impl.dart'; import 'package:hiddify/domain/environment.dart'; import 'package:hiddify/features/common/window/window_controller.dart'; import 'package:hiddify/features/geo_asset/data/geo_asset_data_providers.dart'; +import 'package:hiddify/features/log/data/log_data_providers.dart'; import 'package:hiddify/features/profile/data/profile_data_providers.dart'; import 'package:hiddify/features/profile/notifier/active_profile_notifier.dart'; import 'package:hiddify/features/system_tray/system_tray_controller.dart'; @@ -88,6 +89,7 @@ Future _lazyBootstrap( final filesEditor = container.read(filesEditorServiceProvider); await filesEditor.init(); + await container.read(logRepositoryProvider.future); await container.read(geoAssetRepositoryProvider.future); await container.read(profileRepositoryProvider.future); @@ -145,7 +147,7 @@ void initLoggers( final logToFile = debug || (!Platform.isAndroid && !Platform.isIOS); if (logToFile) { _loggers.addPrinter( - FileLogPrinter(read(filesEditorServiceProvider).appLogsFile.path), + FileLogPrinter(read(logPathResolverProvider).appFile().path), ); } Loggy.initLoggy( diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index 74a6587f..1339c2d9 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -53,7 +53,7 @@ int getCurrentIndex(BuildContext context) { final String location = GoRouterState.of(context).uri.path; if (location == const HomeRoute().location) return 0; if (location.startsWith(const ProxiesRoute().location)) return 1; - if (location.startsWith(const LogsRoute().location)) return 2; + if (location.startsWith(const LogsOverviewRoute().location)) return 2; if (location.startsWith(const SettingsRoute().location)) return 3; if (location.startsWith(const AboutRoute().location)) return 4; return 0; @@ -66,7 +66,7 @@ void switchTab(int index, BuildContext context) { case 1: const ProxiesRoute().go(context); case 2: - const LogsRoute().go(context); + const LogsOverviewRoute().go(context); case 3: const SettingsRoute().go(context); case 4: diff --git a/lib/core/router/routes.dart b/lib/core/router/routes.dart index 44a2089a..06c50e7f 100644 --- a/lib/core/router/routes.dart +++ b/lib/core/router/routes.dart @@ -6,7 +6,7 @@ import 'package:hiddify/features/common/adaptive_root_scaffold.dart'; import 'package:hiddify/features/geo_asset/overview/geo_assets_overview_page.dart'; import 'package:hiddify/features/home/view/view.dart'; import 'package:hiddify/features/intro/intro_page.dart'; -import 'package:hiddify/features/logs/view/logs_page.dart'; +import 'package:hiddify/features/log/overview/logs_overview_page.dart'; import 'package:hiddify/features/profile/add/add_profile_modal.dart'; import 'package:hiddify/features/profile/details/profile_details_page.dart'; import 'package:hiddify/features/profile/overview/profiles_overview_page.dart'; @@ -44,9 +44,9 @@ GlobalKey? _dynamicRootKey = path: "profiles/:id", name: ProfileDetailsRoute.name, ), - TypedGoRoute( + TypedGoRoute( path: "logs", - name: LogsRoute.name, + name: LogsOverviewRoute.name, ), TypedGoRoute( path: "settings", @@ -116,9 +116,9 @@ class MobileWrapperRoute extends ShellRouteData { path: "/proxies", name: ProxiesRoute.name, ), - TypedGoRoute( + TypedGoRoute( path: "/logs", - name: LogsRoute.name, + name: LogsOverviewRoute.name, ), TypedGoRoute( path: "/settings", @@ -260,8 +260,8 @@ class ProfileDetailsRoute extends GoRouteData { } } -class LogsRoute extends GoRouteData { - const LogsRoute(); +class LogsOverviewRoute extends GoRouteData { + const LogsOverviewRoute(); static const name = "Logs"; static final GlobalKey? $parentNavigatorKey = _dynamicRootKey; @@ -272,10 +272,10 @@ class LogsRoute extends GoRouteData { return const MaterialPage( fullscreenDialog: true, name: name, - child: LogsPage(), + child: LogsOverviewPage(), ); } - return const NoTransitionPage(name: name, child: LogsPage()); + return const NoTransitionPage(name: name, child: LogsOverviewPage()); } } diff --git a/lib/data/repository/config_options_store.dart b/lib/data/repository/config_options_store.dart index 0a5169be..55cb31b4 100644 --- a/lib/data/repository/config_options_store.dart +++ b/lib/data/repository/config_options_store.dart @@ -3,6 +3,7 @@ import 'package:flutter/foundation.dart'; import 'package:hiddify/core/prefs/prefs.dart'; import 'package:hiddify/data/data_providers.dart'; import 'package:hiddify/domain/singbox/singbox.dart'; +import 'package:hiddify/features/log/model/log_level.dart'; import 'package:hiddify/utils/pref_notifier.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; diff --git a/lib/data/repository/core_facade_impl.dart b/lib/data/repository/core_facade_impl.dart index 375a1e8b..eea0b4df 100644 --- a/lib/data/repository/core_facade_impl.dart +++ b/lib/data/repository/core_facade_impl.dart @@ -240,24 +240,6 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { ); } - @override - Stream>> watchLogs() { - return singbox.watchLogs(filesEditor.coreLogsFile.path).handleExceptions( - (error, stackTrace) { - loggy.warning("error watching logs", error, stackTrace); - return CoreServiceFailure.unexpected(error, stackTrace); - }, - ); - } - - @override - TaskEither clearLogs() { - return exceptionHandler( - () => singbox.clearLogs().mapLeft(CoreServiceFailure.other).run(), - CoreServiceFailure.unexpected, - ); - } - @override Stream watchConnectionStatus() => singbox.watchConnectionStatus(); diff --git a/lib/domain/singbox/box_log.dart b/lib/domain/singbox/box_log.dart deleted file mode 100644 index 84ee25c1..00000000 --- a/lib/domain/singbox/box_log.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'package:dartx/dartx.dart'; -import 'package:flutter/material.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:tint/tint.dart'; - -part 'box_log.freezed.dart'; - -enum LogLevel { - trace, - debug, - info, - warn, - error, - fatal, - panic; - - static List get choices => values.takeFirst(4); - - Color? get color => switch (this) { - trace => Colors.lightBlueAccent, - debug => Colors.grey, - info => Colors.lightGreen, - warn => Colors.orange, - error => Colors.redAccent, - fatal => Colors.red, - panic => Colors.red, - }; -} - -@freezed -class BoxLog with _$BoxLog { - const factory BoxLog({ - LogLevel? level, - DateTime? time, - required String message, - }) = _BoxLog; - - factory BoxLog.parse(String log) { - log = log.strip(); - DateTime? time; - if (log.length > 25) { - time = DateTime.tryParse(log.substring(6, 25)); - } - if (time != null) { - log = log.substring(26); - } - final level = LogLevel.values.firstOrNullWhere( - (e) { - if (log.startsWith(e.name.toUpperCase())) { - log = log.removePrefix(e.name.toUpperCase()); - return true; - } - return false; - }, - ); - return BoxLog( - level: level, - time: time, - message: log.trim(), - ); - } -} diff --git a/lib/domain/singbox/config_options.dart b/lib/domain/singbox/config_options.dart index 6790528a..f350e79e 100644 --- a/lib/domain/singbox/config_options.dart +++ b/lib/domain/singbox/config_options.dart @@ -2,8 +2,8 @@ import 'dart:convert'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hiddify/core/prefs/prefs.dart'; -import 'package:hiddify/domain/singbox/box_log.dart'; import 'package:hiddify/domain/singbox/rules.dart'; +import 'package:hiddify/features/log/model/log_level.dart'; part 'config_options.freezed.dart'; part 'config_options.g.dart'; diff --git a/lib/domain/singbox/singbox.dart b/lib/domain/singbox/singbox.dart index 0ca7decc..441637cd 100644 --- a/lib/domain/singbox/singbox.dart +++ b/lib/domain/singbox/singbox.dart @@ -1,4 +1,3 @@ -export 'box_log.dart'; export 'config_options.dart'; export 'core_status.dart'; export 'outbounds.dart'; diff --git a/lib/domain/singbox/singbox_facade.dart b/lib/domain/singbox/singbox_facade.dart index dd7a9544..4231afb0 100644 --- a/lib/domain/singbox/singbox_facade.dart +++ b/lib/domain/singbox/singbox_facade.dart @@ -46,8 +46,4 @@ abstract interface class SingboxFacade { Stream watchConnectionStatus(); Stream> watchCoreStatus(); - - Stream>> watchLogs(); - - TaskEither clearLogs(); } diff --git a/lib/features/log/data/log_data_providers.dart b/lib/features/log/data/log_data_providers.dart new file mode 100644 index 00000000..199b7dbc --- /dev/null +++ b/lib/features/log/data/log_data_providers.dart @@ -0,0 +1,23 @@ +import 'package:hiddify/features/log/data/log_path_resolver.dart'; +import 'package:hiddify/features/log/data/log_repository.dart'; +import 'package:hiddify/services/service_providers.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'log_data_providers.g.dart'; + +@Riverpod(keepAlive: true) +Future logRepository(LogRepositoryRef ref) async { + final repo = LogRepositoryImpl( + singbox: ref.watch(singboxServiceProvider), + logPathResolver: ref.watch(logPathResolverProvider), + ); + await repo.init().getOrElse((l) => throw l).run(); + return repo; +} + +@Riverpod(keepAlive: true) +LogPathResolver logPathResolver(LogPathResolverRef ref) { + return LogPathResolver( + ref.watch(filesEditorServiceProvider).dirs.workingDir, + ); +} diff --git a/lib/features/log/data/log_parser.dart b/lib/features/log/data/log_parser.dart new file mode 100644 index 00000000..84b496d1 --- /dev/null +++ b/lib/features/log/data/log_parser.dart @@ -0,0 +1,33 @@ +// ignore_for_file: parameter_assignments + +import 'package:dartx/dartx.dart'; +import 'package:hiddify/features/log/model/log_entity.dart'; +import 'package:hiddify/features/log/model/log_level.dart'; +import 'package:tint/tint.dart'; + +abstract class LogParser { + static LogEntity parseSingbox(String log) { + log = log.strip(); + DateTime? time; + if (log.length > 25) { + time = DateTime.tryParse(log.substring(6, 25)); + } + if (time != null) { + log = log.substring(26); + } + final level = LogLevel.values.firstOrNullWhere( + (e) { + if (log.startsWith(e.name.toUpperCase())) { + log = log.removePrefix(e.name.toUpperCase()); + return true; + } + return false; + }, + ); + return LogEntity( + level: level, + time: time, + message: log.trim(), + ); + } +} diff --git a/lib/features/log/data/log_path_resolver.dart b/lib/features/log/data/log_path_resolver.dart new file mode 100644 index 00000000..08762f28 --- /dev/null +++ b/lib/features/log/data/log_path_resolver.dart @@ -0,0 +1,19 @@ +import 'dart:io'; + +import 'package:path/path.dart' as p; + +class LogPathResolver { + const LogPathResolver(this._workingDir); + + final Directory _workingDir; + + Directory get directory => _workingDir; + + File coreFile() { + return File(p.join(directory.path, "box.log")); + } + + File appFile() { + return File(p.join(directory.path, "app.log")); + } +} diff --git a/lib/features/log/data/log_repository.dart b/lib/features/log/data/log_repository.dart new file mode 100644 index 00000000..bd699d42 --- /dev/null +++ b/lib/features/log/data/log_repository.dart @@ -0,0 +1,70 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:hiddify/data/repository/exception_handlers.dart'; +import 'package:hiddify/features/log/data/log_parser.dart'; +import 'package:hiddify/features/log/data/log_path_resolver.dart'; +import 'package:hiddify/features/log/model/log_entity.dart'; +import 'package:hiddify/features/log/model/log_failure.dart'; +import 'package:hiddify/services/singbox/singbox_service.dart'; +import 'package:hiddify/utils/custom_loggers.dart'; + +abstract interface class LogRepository { + TaskEither init(); + Stream>> watchLogs(); + TaskEither clearLogs(); +} + +class LogRepositoryImpl + with ExceptionHandler, InfraLogger + implements LogRepository { + LogRepositoryImpl({ + required this.singbox, + required this.logPathResolver, + }); + + final SingboxService singbox; + final LogPathResolver logPathResolver; + + @override + TaskEither init() { + return exceptionHandler( + () async { + if (!await logPathResolver.directory.exists()) { + await logPathResolver.directory.create(recursive: true); + } + if (await logPathResolver.coreFile().exists()) { + await logPathResolver.coreFile().writeAsString(""); + } else { + await logPathResolver.coreFile().create(recursive: true); + } + if (await logPathResolver.appFile().exists()) { + await logPathResolver.appFile().writeAsString(""); + } else { + await logPathResolver.appFile().create(recursive: true); + } + return right(unit); + }, + LogUnexpectedFailure.new, + ); + } + + @override + Stream>> watchLogs() { + return singbox + .watchLogs(logPathResolver.coreFile().path) + .map((event) => event.map(LogParser.parseSingbox).toList()) + .handleExceptions( + (error, stackTrace) { + loggy.warning("error watching logs", error, stackTrace); + return LogFailure.unexpected(error, stackTrace); + }, + ); + } + + @override + TaskEither clearLogs() { + return exceptionHandler( + () => singbox.clearLogs().mapLeft(LogFailure.unexpected).run(), + LogFailure.unexpected, + ); + } +} diff --git a/lib/features/log/model/log_entity.dart b/lib/features/log/model/log_entity.dart new file mode 100644 index 00000000..878927a9 --- /dev/null +++ b/lib/features/log/model/log_entity.dart @@ -0,0 +1,13 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hiddify/features/log/model/log_level.dart'; + +part 'log_entity.freezed.dart'; + +@freezed +class LogEntity with _$LogEntity { + const factory LogEntity({ + LogLevel? level, + DateTime? time, + required String message, + }) = _LogEntity; +} diff --git a/lib/features/log/model/log_failure.dart b/lib/features/log/model/log_failure.dart new file mode 100644 index 00000000..f1a4e8fe --- /dev/null +++ b/lib/features/log/model/log_failure.dart @@ -0,0 +1,25 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hiddify/core/prefs/locale_prefs.dart'; +import 'package:hiddify/domain/failures.dart'; + +part 'log_failure.freezed.dart'; + +@freezed +sealed class LogFailure with _$LogFailure, Failure { + const LogFailure._(); + + const factory LogFailure.unexpected([ + Object? error, + StackTrace? stackTrace, + ]) = LogUnexpectedFailure; + + @override + ({String type, String? message}) present(TranslationsEn t) { + return switch (this) { + LogUnexpectedFailure() => ( + type: "unexpected", + message: null, + ), + }; + } +} diff --git a/lib/features/log/model/log_level.dart b/lib/features/log/model/log_level.dart new file mode 100644 index 00000000..a81cf47c --- /dev/null +++ b/lib/features/log/model/log_level.dart @@ -0,0 +1,25 @@ +import 'package:dartx/dartx.dart'; +import 'package:flutter/material.dart'; + +enum LogLevel { + trace, + debug, + info, + warn, + error, + fatal, + panic; + + /// [LogLevel] selectable by user as preference + static List get choices => values.takeFirst(4); + + Color? get color => switch (this) { + trace => Colors.lightBlueAccent, + debug => Colors.grey, + info => Colors.lightGreen, + warn => Colors.orange, + error => Colors.redAccent, + fatal => Colors.red, + panic => Colors.red, + }; +} diff --git a/lib/features/logs/notifier/logs_notifier.dart b/lib/features/log/overview/logs_overview_notifier.dart similarity index 79% rename from lib/features/logs/notifier/logs_notifier.dart rename to lib/features/log/overview/logs_overview_notifier.dart index b089840d..8b3c4243 100644 --- a/lib/features/logs/notifier/logs_notifier.dart +++ b/lib/features/log/overview/logs_overview_notifier.dart @@ -1,21 +1,22 @@ import 'dart:async'; -import 'package:hiddify/data/data_providers.dart'; -import 'package:hiddify/domain/singbox/singbox.dart'; -import 'package:hiddify/features/logs/notifier/logs_state.dart'; +import 'package:hiddify/features/log/data/log_data_providers.dart'; +import 'package:hiddify/features/log/model/log_entity.dart'; +import 'package:hiddify/features/log/model/log_level.dart'; +import 'package:hiddify/features/log/overview/logs_overview_state.dart'; import 'package:hiddify/utils/riverpod_utils.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:rxdart/rxdart.dart'; -part 'logs_notifier.g.dart'; +part 'logs_overview_notifier.g.dart'; @riverpod -class LogsNotifier extends _$LogsNotifier with AppLogger { +class LogsOverviewNotifier extends _$LogsOverviewNotifier with AppLogger { @override - LogsState build() { + LogsOverviewState build() { ref.disposeDelay(const Duration(seconds: 20)); - state = const LogsState(); + state = const LogsOverviewState(); ref.onDispose( () { loggy.debug("disposing"); @@ -41,7 +42,7 @@ class LogsNotifier extends _$LogsNotifier with AppLogger { ); _addListeners(); - return const LogsState(); + return const LogsOverviewState(); } StreamSubscription? _listener; @@ -50,7 +51,8 @@ class LogsNotifier extends _$LogsNotifier with AppLogger { loggy.debug("adding listeners"); await _listener?.cancel(); _listener = ref - .read(coreFacadeProvider) + .read(logRepositoryProvider) + .requireValue .watchLogs() .throttle( (_) => Stream.value(_listener?.isPaused ?? false), @@ -78,15 +80,14 @@ class LogsNotifier extends _$LogsNotifier with AppLogger { ).listen((event) {}); } - Iterable _logs = []; + Iterable _logs = []; final _debouncer = CallbackDebouncer(const Duration(milliseconds: 200)); LogLevel? _levelFilter; String _filter = ""; - Future> _computeLogs() async { - final logs = _logs.map(BoxLog.parse); - if (_levelFilter == null && _filter.isEmpty) return logs.toList(); - return logs.where((e) { + Future> _computeLogs() async { + if (_levelFilter == null && _filter.isEmpty) return _logs.toList(); + return _logs.where((e) { return (_filter.isEmpty || e.message.contains(_filter)) && (_levelFilter == null || e.level == null || @@ -108,7 +109,7 @@ class LogsNotifier extends _$LogsNotifier with AppLogger { Future clear() async { loggy.debug("clearing"); - await ref.read(coreFacadeProvider).clearLogs().match( + await ref.read(logRepositoryProvider).requireValue.clearLogs().match( (l) { loggy.warning("error clearing logs", l); }, diff --git a/lib/features/logs/view/logs_page.dart b/lib/features/log/overview/logs_overview_page.dart similarity index 92% rename from lib/features/logs/view/logs_page.dart rename to lib/features/log/overview/logs_overview_page.dart index ace41283..3c8e4f75 100644 --- a/lib/features/logs/view/logs_page.dart +++ b/lib/features/log/overview/logs_overview_page.dart @@ -5,25 +5,25 @@ import 'package:gap/gap.dart'; import 'package:hiddify/core/core_providers.dart'; import 'package:hiddify/core/prefs/prefs.dart'; import 'package:hiddify/domain/failures.dart'; -import 'package:hiddify/domain/singbox/singbox.dart'; import 'package:hiddify/features/common/nested_app_bar.dart'; -import 'package:hiddify/features/logs/notifier/notifier.dart'; -import 'package:hiddify/services/service_providers.dart'; +import 'package:hiddify/features/log/data/log_data_providers.dart'; +import 'package:hiddify/features/log/model/log_level.dart'; +import 'package:hiddify/features/log/overview/logs_overview_notifier.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:sliver_tools/sliver_tools.dart'; -class LogsPage extends HookConsumerWidget with PresLogger { - const LogsPage({super.key}); +class LogsOverviewPage extends HookConsumerWidget with PresLogger { + const LogsOverviewPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final t = ref.watch(translationsProvider); - final state = ref.watch(logsNotifierProvider); - final notifier = ref.watch(logsNotifierProvider.notifier); + final state = ref.watch(logsOverviewNotifierProvider); + final notifier = ref.watch(logsOverviewNotifierProvider.notifier); final debug = ref.watch(debugModeNotifierProvider); - final filesEditor = ref.watch(filesEditorServiceProvider); + final pathResolver = ref.watch(logPathResolverProvider); final filterController = useTextEditingController(text: state.filter); @@ -33,8 +33,8 @@ class LogsPage extends HookConsumerWidget with PresLogger { child: Text(t.logs.shareCoreLogs), onTap: () async { await UriUtils.tryShareOrLaunchFile( - Uri.parse(filesEditor.coreLogsFile.path), - fileOrDir: filesEditor.logsDir.uri, + Uri.parse(pathResolver.coreFile().path), + fileOrDir: pathResolver.directory.uri, ); }, ), @@ -42,8 +42,8 @@ class LogsPage extends HookConsumerWidget with PresLogger { child: Text(t.logs.shareAppLogs), onTap: () async { await UriUtils.tryShareOrLaunchFile( - Uri.parse(filesEditor.appLogsFile.path), - fileOrDir: filesEditor.logsDir.uri, + Uri.parse(pathResolver.appFile().path), + fileOrDir: pathResolver.directory.uri, ); }, ), diff --git a/lib/features/log/overview/logs_overview_state.dart b/lib/features/log/overview/logs_overview_state.dart new file mode 100644 index 00000000..abc47ca5 --- /dev/null +++ b/lib/features/log/overview/logs_overview_state.dart @@ -0,0 +1,18 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hiddify/features/log/model/log_entity.dart'; +import 'package:hiddify/features/log/model/log_level.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'logs_overview_state.freezed.dart'; + +@freezed +class LogsOverviewState with _$LogsOverviewState { + const LogsOverviewState._(); + + const factory LogsOverviewState({ + @Default(AsyncLoading()) AsyncValue> logs, + @Default(false) bool paused, + @Default("") String filter, + LogLevel? levelFilter, + }) = _LogsOverviewState; +} diff --git a/lib/features/logs/notifier/logs_state.dart b/lib/features/logs/notifier/logs_state.dart deleted file mode 100644 index 4318870a..00000000 --- a/lib/features/logs/notifier/logs_state.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:hiddify/domain/singbox/singbox.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'logs_state.freezed.dart'; - -@freezed -class LogsState with _$LogsState { - const LogsState._(); - - const factory LogsState({ - @Default(AsyncLoading()) AsyncValue> logs, - @Default(false) bool paused, - @Default("") String filter, - LogLevel? levelFilter, - }) = _LogsState; -} diff --git a/lib/features/logs/notifier/notifier.dart b/lib/features/logs/notifier/notifier.dart deleted file mode 100644 index 69135d86..00000000 --- a/lib/features/logs/notifier/notifier.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'logs_notifier.dart'; -export 'logs_state.dart'; diff --git a/lib/features/logs/view/view.dart b/lib/features/logs/view/view.dart deleted file mode 100644 index 2982c609..00000000 --- a/lib/features/logs/view/view.dart +++ /dev/null @@ -1 +0,0 @@ -export 'logs_page.dart'; diff --git a/lib/features/settings/view/config_options_page.dart b/lib/features/settings/view/config_options_page.dart index a4064fe2..50899b71 100644 --- a/lib/features/settings/view/config_options_page.dart +++ b/lib/features/settings/view/config_options_page.dart @@ -4,6 +4,7 @@ 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'; diff --git a/lib/features/system_tray/system_tray_controller.dart b/lib/features/system_tray/system_tray_controller.dart index c230fb9f..04108033 100644 --- a/lib/features/system_tray/system_tray_controller.dart +++ b/lib/features/system_tray/system_tray_controller.dart @@ -41,7 +41,7 @@ class SystemTrayController extends _$SystemTrayController final destinations = <(String label, String location)>[ (t.home.pageTitle, const HomeRoute().location), (t.proxies.pageTitle, const ProxiesRoute().location), - (t.logs.pageTitle, const LogsRoute().location), + (t.logs.pageTitle, const LogsOverviewRoute().location), (t.settings.pageTitle, const SettingsRoute().location), (t.about.pageTitle, const AboutRoute().location), ]; diff --git a/lib/services/files_editor_service.dart b/lib/services/files_editor_service.dart index 9de8515e..86bf47c3 100644 --- a/lib/services/files_editor_service.dart +++ b/lib/services/files_editor_service.dart @@ -2,7 +2,6 @@ import 'dart:io'; import 'package:hiddify/services/platform_services.dart'; import 'package:hiddify/utils/utils.dart'; -import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; typedef Directories = ({ @@ -19,10 +18,6 @@ class FilesEditorService with InfraLogger { late final Directories dirs; Directory get workingDir => dirs.workingDir; - Directory get logsDir => dirs.workingDir; - - File get appLogsFile => File(p.join(logsDir.path, "app.log")); - File get coreLogsFile => File(p.join(logsDir.path, "box.log")); Future init() async { dirs = await platformServices.getPaths().getOrElse( @@ -40,18 +35,6 @@ class FilesEditorService with InfraLogger { if (!await dirs.workingDir.exists()) { await dirs.workingDir.create(recursive: true); } - - if (await appLogsFile.exists()) { - await appLogsFile.writeAsString(""); - } else { - await appLogsFile.create(recursive: true); - } - - if (await coreLogsFile.exists()) { - await coreLogsFile.writeAsString(""); - } else { - await coreLogsFile.create(recursive: true); - } } static Future getDatabaseDirectory() async { From ed614988a2324f4a2b2fe0cefd7bafcd0334c147 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Fri, 1 Dec 2023 12:56:24 +0330 Subject: [PATCH 83/91] Refactor --- lib/bootstrap.dart | 24 +- lib/core/app_info/app_info_provider.dart | 30 ++ lib/core/core_providers.dart | 23 -- .../database/app_database.dart} | 24 +- .../connection/database_connection.dart | 14 + .../converters/duration_converter.dart} | 0 lib/core/database/database_provider.dart | 7 + .../database}/schema_versions.dart | 0 .../database}/schemas/drift_schema_v1.json | 0 .../database}/schemas/drift_schema_v2.json | 0 .../database/schemas/drift_schema_v3.json | 286 ++++++++++++++++ .../database/tables/database_tables.dart} | 2 +- .../http_client/http_client_provider.dart | 28 ++ lib/core/localization/locale_extensions.dart | 13 + lib/core/localization/locale_preferences.dart | 28 ++ lib/core/localization/translations.dart | 11 + lib/core/model/app_info_entity.dart | 32 ++ lib/{domain => core/model}/constants.dart | 11 - lib/core/model/directories.dart | 7 + lib/{domain => core/model}/environment.dart | 0 lib/{domain => core/model}/failures.dart | 2 +- lib/core/model/region.dart | 15 + .../in_app_notification_controller.dart | 2 +- .../general_preferences.dart} | 40 ++- .../preferences/preferences_provider.dart | 8 + .../service_preferences.dart} | 11 +- lib/core/prefs/locale_prefs.dart | 45 --- lib/core/prefs/prefs.dart | 4 - lib/core/prefs/theme_prefs.dart | 25 -- lib/core/router/app_router.dart | 3 +- lib/core/router/routes.dart | 20 +- lib/core/{prefs => theme}/app_theme.dart | 67 +--- lib/core/theme/app_theme_mode.dart | 25 ++ lib/core/theme/theme_extensions.dart | 40 +++ lib/core/theme/theme_preferences.dart | 26 ++ .../utils/exception_handler.dart} | 0 lib/{ => core}/utils/ffi_utils.dart | 0 lib/core/utils/json_converters.dart | 11 + lib/core/widget/custom_alert_dialog.dart | 2 +- lib/data/data_providers.dart | 86 ----- lib/data/local/schemas/drift_schema_v3.json | 1 - lib/data/repository/app_repository_impl.dart | 62 ---- lib/data/repository/config_options_store.dart | 155 --------- lib/data/repository/core_facade_impl.dart | 246 ------------- lib/data/repository/repository.dart | 1 - lib/domain/app/app.dart | 3 - lib/domain/app/app_failure.dart | 23 -- lib/domain/app/app_info.dart | 87 ----- lib/domain/app/app_repository.dart | 9 - lib/domain/connectivity/connectivity.dart | 2 - lib/domain/core_facade.dart | 3 - lib/domain/core_service_failure.dart | 101 ------ lib/domain/enums.dart | 1 - lib/domain/singbox/config_options.dart | 109 ------ lib/domain/singbox/core_status.dart | 31 -- lib/domain/singbox/outbounds.dart | 44 --- lib/domain/singbox/rules.dart | 73 ---- lib/domain/singbox/service_mode.dart | 24 -- lib/domain/singbox/singbox.dart | 7 - lib/domain/singbox/singbox_facade.dart | 49 --- lib/features/about/view/view.dart | 1 - .../app/widget/app.dart} | 24 +- .../data/app_update_data_providers.dart | 12 + .../data/app_update_repository.dart | 55 +++ .../data/github_release_parser.dart | 36 ++ .../app_update/model/app_update_failure.dart | 26 ++ .../model/remote_version_entity.dart | 22 ++ .../notifier}/app_update_notifier.dart | 52 +-- .../app_update/notifier/app_update_state.dart | 19 ++ .../widget}/new_version_dialog.dart | 9 +- .../common/adaptive_root_scaffold.dart | 4 +- lib/features/common/common_controllers.dart | 6 +- lib/features/common/general_pref_tiles.dart | 14 +- .../common/qr_code_scanner_screen.dart | 2 +- lib/features/common/stats_provider.dart | 23 -- .../common/window/window_controller.dart | 14 +- .../data/config_option_data_providers.dart | 17 + .../data/config_option_repository.dart | 172 ++++++++++ .../model/config_option_entity.dart | 80 +++++ .../model/config_option_failure.dart | 26 ++ .../model/config_option_patch.dart | 39 +++ .../notifier/config_option_notifier.dart | 31 ++ .../overview/config_options_page.dart | 323 ++++++++++++++++++ .../data/connection_data_providers.dart | 24 ++ .../data/connection_platform_source.dart | 67 ++++ .../data/connection_repository.dart | 214 ++++++++++++ .../connection/model}/connection_failure.dart | 39 ++- .../connection/model}/connection_status.dart | 4 +- .../notifier/connection_notifier.dart} | 31 +- .../geo_asset/data/geo_asset_data_mapper.dart | 2 +- .../data/geo_asset_data_providers.dart | 5 +- .../geo_asset/data/geo_asset_data_source.dart | 4 +- .../geo_asset/data/geo_asset_repository.dart | 4 +- .../geo_asset/model/geo_asset_failure.dart | 6 +- .../overview/geo_assets_overview_page.dart | 2 +- .../geo_asset/widget/geo_asset_tile.dart | 4 +- lib/features/home/view/view.dart | 1 - .../connection_button.dart | 24 +- .../empty_profiles_home_body.dart | 2 +- .../home/{view => widget}/home_page.dart | 11 +- lib/features/home/widgets/widgets.dart | 2 - .../intro/{ => widget}/intro_page.dart | 6 +- lib/features/log/data/log_data_providers.dart | 1 + lib/features/log/data/log_repository.dart | 4 +- lib/features/log/model/log_failure.dart | 7 +- .../log/overview/logs_overview_page.dart | 6 +- .../data/per_app_proxy_data_providers.dart | 9 + .../data/per_app_proxy_repository.dart | 55 +++ .../model/installed_package_info.dart | 17 + .../model/per_app_proxy_mode.dart | 24 ++ .../overview/per_app_proxy_notifier.dart | 36 ++ .../overview}/per_app_proxy_page.dart | 46 +-- .../profile/add/add_profile_modal.dart | 2 +- .../profile/data/profile_data_mapper.dart | 2 +- .../profile/data/profile_data_providers.dart | 8 +- .../profile/data/profile_data_source.dart | 4 +- .../profile/data/profile_repository.dart | 100 +++--- .../profile/details/profile_details_page.dart | 4 +- .../profile/model/profile_failure.dart | 4 +- .../profile/model/profile_sort_enum.dart | 2 +- .../profile/notifier/profile_notifier.dart | 10 +- .../notifier/profiles_update_notifier.dart | 6 +- .../overview/profiles_overview_notifier.dart | 3 +- .../overview/profiles_overview_page.dart | 4 +- lib/features/profile/widget/profile_tile.dart | 5 +- lib/features/proxies/notifier/notifier.dart | 1 - lib/features/proxies/view/view.dart | 1 - lib/features/proxies/widgets/widgets.dart | 1 - .../proxy/data/proxy_data_providers.dart | 12 + lib/features/proxy/data/proxy_repository.dart | 78 +++++ lib/features/proxy/model/proxy_entity.dart | 37 ++ lib/features/proxy/model/proxy_failure.dart | 33 ++ .../overview/proxies_overview_notifier.dart} | 50 +-- .../overview/proxies_overview_page.dart} | 16 +- .../widgets => proxy/widget}/proxy_tile.dart | 12 +- .../view => settings/about}/about_page.dart | 14 +- .../data/settings_data_providers.dart | 9 + .../settings/data/settings_repository.dart | 44 +++ .../settings/model/settings_failure.dart | 26 ++ .../notifier/platform_settings_notifier.dart | 25 ++ .../settings_overview_page.dart} | 6 +- .../settings/view/config_options_page.dart | 299 ---------------- lib/features/settings/view/view.dart | 3 - .../widgets/advanced_setting_tiles.dart | 6 +- .../widgets/general_setting_tiles.dart | 16 +- .../widgets/platform_settings_tiles.dart | 26 +- .../widgets/settings_input_dialog.dart | 2 +- .../stats/data/stats_data_providers.dart | 10 + lib/features/stats/data/stats_repository.dart | 33 ++ lib/features/stats/model/stats_entity.dart | 22 ++ lib/features/stats/model/stats_failure.dart | 26 ++ .../stats/notifier/stats_notifier.dart | 23 ++ .../widget}/side_bar_stats_overview.dart | 11 +- .../system_tray/system_tray_controller.dart | 27 +- lib/main_dev.dart | 2 +- lib/main_prod.dart | 2 +- lib/services/auto_start_service.dart | 4 +- lib/services/platform_services.dart | 135 -------- lib/services/service_providers.dart | 4 - lib/services/singbox/shared.dart | 46 --- lib/services/singbox/singbox_service.dart | 59 ---- lib/singbox/model/singbox_config_enum.dart | 68 ++++ lib/singbox/model/singbox_config_option.dart | 62 ++++ lib/singbox/model/singbox_outbound.dart | 40 +++ .../model/singbox_proxy_type.dart} | 0 lib/singbox/model/singbox_rule.dart | 35 ++ lib/singbox/model/singbox_stats.dart | 22 ++ lib/singbox/model/singbox_status.dart | 48 +++ .../service}/ffi_singbox_service.dart | 66 ++-- .../service/platform_singbox_service.dart} | 69 ++-- lib/singbox/service/singbox_service.dart | 60 ++++ .../service/singbox_service_provider.dart | 9 + lib/utils/link_parsers.dart | 2 +- lib/utils/mutation_state.dart | 2 +- lib/utils/pref_notifier.dart | 4 +- lib/utils/sentry_utils.dart | 2 +- .../generated_migrations/schema.dart | 0 .../generated_migrations/schema_v1.dart | 0 .../generated_migrations/schema_v2.dart | 0 .../generated_migrations/schema_v3.dart | 0 .../database}/migrations_test.dart | 2 +- 181 files changed, 3092 insertions(+), 2341 deletions(-) create mode 100644 lib/core/app_info/app_info_provider.dart delete mode 100644 lib/core/core_providers.dart rename lib/{data/local/database.dart => core/database/app_database.dart} (71%) create mode 100644 lib/core/database/connection/database_connection.dart rename lib/{data/local/type_converters.dart => core/database/converters/duration_converter.dart} (100%) create mode 100644 lib/core/database/database_provider.dart rename lib/{data/local => core/database}/schema_versions.dart (100%) rename lib/{data/local => core/database}/schemas/drift_schema_v1.json (100%) rename lib/{data/local => core/database}/schemas/drift_schema_v2.json (100%) create mode 100644 lib/core/database/schemas/drift_schema_v3.json rename lib/{data/local/tables.dart => core/database/tables/database_tables.dart} (95%) create mode 100644 lib/core/http_client/http_client_provider.dart create mode 100644 lib/core/localization/locale_extensions.dart create mode 100644 lib/core/localization/locale_preferences.dart create mode 100644 lib/core/localization/translations.dart create mode 100644 lib/core/model/app_info_entity.dart rename lib/{domain => core/model}/constants.dart (63%) create mode 100644 lib/core/model/directories.dart rename lib/{domain => core/model}/environment.dart (100%) rename lib/{domain => core/model}/failures.dart (97%) create mode 100644 lib/core/model/region.dart rename lib/core/{prefs/general_prefs.dart => preferences/general_preferences.dart} (76%) create mode 100644 lib/core/preferences/preferences_provider.dart rename lib/core/{prefs/service_prefs.dart => preferences/service_preferences.dart} (63%) delete mode 100644 lib/core/prefs/locale_prefs.dart delete mode 100644 lib/core/prefs/prefs.dart delete mode 100644 lib/core/prefs/theme_prefs.dart rename lib/core/{prefs => theme}/app_theme.dart (77%) create mode 100644 lib/core/theme/app_theme_mode.dart create mode 100644 lib/core/theme/theme_extensions.dart create mode 100644 lib/core/theme/theme_preferences.dart rename lib/{data/repository/exception_handlers.dart => core/utils/exception_handler.dart} (100%) rename lib/{ => core}/utils/ffi_utils.dart (100%) create mode 100644 lib/core/utils/json_converters.dart delete mode 100644 lib/data/data_providers.dart delete mode 100644 lib/data/local/schemas/drift_schema_v3.json delete mode 100644 lib/data/repository/app_repository_impl.dart delete mode 100644 lib/data/repository/config_options_store.dart delete mode 100644 lib/data/repository/core_facade_impl.dart delete mode 100644 lib/data/repository/repository.dart delete mode 100644 lib/domain/app/app.dart delete mode 100644 lib/domain/app/app_failure.dart delete mode 100644 lib/domain/app/app_info.dart delete mode 100644 lib/domain/app/app_repository.dart delete mode 100644 lib/domain/connectivity/connectivity.dart delete mode 100644 lib/domain/core_facade.dart delete mode 100644 lib/domain/core_service_failure.dart delete mode 100644 lib/domain/enums.dart delete mode 100644 lib/domain/singbox/config_options.dart delete mode 100644 lib/domain/singbox/core_status.dart delete mode 100644 lib/domain/singbox/outbounds.dart delete mode 100644 lib/domain/singbox/rules.dart delete mode 100644 lib/domain/singbox/service_mode.dart delete mode 100644 lib/domain/singbox/singbox.dart delete mode 100644 lib/domain/singbox/singbox_facade.dart delete mode 100644 lib/features/about/view/view.dart rename lib/{core/app/app_view.dart => features/app/widget/app.dart} (65%) create mode 100644 lib/features/app_update/data/app_update_data_providers.dart create mode 100644 lib/features/app_update/data/app_update_repository.dart create mode 100644 lib/features/app_update/data/github_release_parser.dart create mode 100644 lib/features/app_update/model/app_update_failure.dart create mode 100644 lib/features/app_update/model/remote_version_entity.dart rename lib/features/{common => app_update/notifier}/app_update_notifier.dart (58%) create mode 100644 lib/features/app_update/notifier/app_update_state.dart rename lib/features/{common => app_update/widget}/new_version_dialog.dart (91%) delete mode 100644 lib/features/common/stats_provider.dart create mode 100644 lib/features/config_option/data/config_option_data_providers.dart create mode 100644 lib/features/config_option/data/config_option_repository.dart create mode 100644 lib/features/config_option/model/config_option_entity.dart create mode 100644 lib/features/config_option/model/config_option_failure.dart create mode 100644 lib/features/config_option/model/config_option_patch.dart create mode 100644 lib/features/config_option/notifier/config_option_notifier.dart create mode 100644 lib/features/config_option/overview/config_options_page.dart create mode 100644 lib/features/connection/data/connection_data_providers.dart create mode 100644 lib/features/connection/data/connection_platform_source.dart create mode 100644 lib/features/connection/data/connection_repository.dart rename lib/{domain/connectivity => features/connection/model}/connection_failure.dart (51%) rename lib/{domain/connectivity => features/connection/model}/connection_status.dart (90%) rename lib/features/{common/connectivity/connectivity_controller.dart => connection/notifier/connection_notifier.dart} (75%) delete mode 100644 lib/features/home/view/view.dart rename lib/features/home/{widgets => widget}/connection_button.dart (84%) rename lib/features/home/{widgets => widget}/empty_profiles_home_body.dart (96%) rename lib/features/home/{view => widget}/home_page.dart (90%) delete mode 100644 lib/features/home/widgets/widgets.dart rename lib/features/intro/{ => widget}/intro_page.dart (95%) create mode 100644 lib/features/per_app_proxy/data/per_app_proxy_data_providers.dart create mode 100644 lib/features/per_app_proxy/data/per_app_proxy_repository.dart create mode 100644 lib/features/per_app_proxy/model/installed_package_info.dart create mode 100644 lib/features/per_app_proxy/model/per_app_proxy_mode.dart create mode 100644 lib/features/per_app_proxy/overview/per_app_proxy_notifier.dart rename lib/features/{settings/view => per_app_proxy/overview}/per_app_proxy_page.dart (85%) delete mode 100644 lib/features/proxies/notifier/notifier.dart delete mode 100644 lib/features/proxies/view/view.dart delete mode 100644 lib/features/proxies/widgets/widgets.dart create mode 100644 lib/features/proxy/data/proxy_data_providers.dart create mode 100644 lib/features/proxy/data/proxy_repository.dart create mode 100644 lib/features/proxy/model/proxy_entity.dart create mode 100644 lib/features/proxy/model/proxy_failure.dart rename lib/features/{proxies/notifier/proxies_notifier.dart => proxy/overview/proxies_overview_notifier.dart} (72%) rename lib/features/{proxies/view/proxies_page.dart => proxy/overview/proxies_overview_page.dart} (91%) rename lib/features/{proxies/widgets => proxy/widget}/proxy_tile.dart (88%) rename lib/features/{about/view => settings/about}/about_page.dart (91%) create mode 100644 lib/features/settings/data/settings_data_providers.dart create mode 100644 lib/features/settings/data/settings_repository.dart create mode 100644 lib/features/settings/model/settings_failure.dart create mode 100644 lib/features/settings/notifier/platform_settings_notifier.dart rename lib/features/settings/{view/settings_page.dart => overview/settings_overview_page.dart} (85%) delete mode 100644 lib/features/settings/view/config_options_page.dart delete mode 100644 lib/features/settings/view/view.dart create mode 100644 lib/features/stats/data/stats_data_providers.dart create mode 100644 lib/features/stats/data/stats_repository.dart create mode 100644 lib/features/stats/model/stats_entity.dart create mode 100644 lib/features/stats/model/stats_failure.dart create mode 100644 lib/features/stats/notifier/stats_notifier.dart rename lib/features/{common => stats/widget}/side_bar_stats_overview.dart (89%) delete mode 100644 lib/services/singbox/shared.dart delete mode 100644 lib/services/singbox/singbox_service.dart create mode 100644 lib/singbox/model/singbox_config_enum.dart create mode 100644 lib/singbox/model/singbox_config_option.dart create mode 100644 lib/singbox/model/singbox_outbound.dart rename lib/{domain/singbox/proxy_type.dart => singbox/model/singbox_proxy_type.dart} (100%) create mode 100644 lib/singbox/model/singbox_rule.dart create mode 100644 lib/singbox/model/singbox_stats.dart create mode 100644 lib/singbox/model/singbox_status.dart rename lib/{services/singbox => singbox/service}/ffi_singbox_service.dart (84%) rename lib/{services/singbox/mobile_singbox_service.dart => singbox/service/platform_singbox_service.dart} (68%) create mode 100644 lib/singbox/service/singbox_service.dart create mode 100644 lib/singbox/service/singbox_service_provider.dart rename test/{data/local => core/database}/generated_migrations/schema.dart (100%) rename test/{data/local => core/database}/generated_migrations/schema_v1.dart (100%) rename test/{data/local => core/database}/generated_migrations/schema_v2.dart (100%) rename test/{data/local => core/database}/generated_migrations/schema_v3.dart (100%) rename test/{data/local => core/database}/migrations_test.dart (95%) diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index 3a610431..2458c244 100644 --- a/lib/bootstrap.dart +++ b/lib/bootstrap.dart @@ -4,12 +4,11 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; -import 'package:hiddify/core/app/app_view.dart'; -import 'package:hiddify/core/core_providers.dart'; -import 'package:hiddify/core/prefs/prefs.dart'; -import 'package:hiddify/data/data_providers.dart'; -import 'package:hiddify/data/repository/app_repository_impl.dart'; -import 'package:hiddify/domain/environment.dart'; +import 'package:hiddify/core/app_info/app_info_provider.dart'; +import 'package:hiddify/core/model/environment.dart'; +import 'package:hiddify/core/preferences/general_preferences.dart'; +import 'package:hiddify/core/preferences/preferences_provider.dart'; +import 'package:hiddify/features/app/widget/app.dart'; import 'package:hiddify/features/common/window/window_controller.dart'; import 'package:hiddify/features/geo_asset/data/geo_asset_data_providers.dart'; import 'package:hiddify/features/log/data/log_data_providers.dart'; @@ -19,11 +18,11 @@ import 'package:hiddify/features/system_tray/system_tray_controller.dart'; import 'package:hiddify/services/auto_start_service.dart'; import 'package:hiddify/services/deep_link_service.dart'; import 'package:hiddify/services/service_providers.dart'; +import 'package:hiddify/singbox/service/singbox_service_provider.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:loggy/loggy.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:window_manager/window_manager.dart'; final _logger = Loggy('bootstrap'); @@ -41,15 +40,14 @@ Future lazyBootstrap( _loggers.addPrinter(sentryLogger); Loggy.initLoggy(); - final appInfo = await AppRepositoryImpl.getAppInfo(env); - final sharedPreferences = await SharedPreferences.getInstance(); final container = ProviderContainer( overrides: [ - appInfoProvider.overrideWithValue(appInfo), - sharedPreferencesProvider.overrideWithValue(sharedPreferences), + environmentProvider.overrideWithValue(env), ], ); + final appInfo = await container.read(appInfoProvider.future); + await container.read(sharedPreferencesProvider.future); final enableAnalytics = container.read(enableAnalyticsProvider); await SentryFlutter.init( @@ -94,7 +92,7 @@ Future _lazyBootstrap( await container.read(profileRepositoryProvider.future); initLoggers(container.read, debug); - _logger.info(container.read(appInfoProvider).format()); + _logger.info(container.read(appInfoProvider).requireValue.format()); final silentStart = container.read(silentStartNotifierProvider); if (silentStart) { @@ -131,7 +129,7 @@ Future _lazyBootstrap( ProviderScope( parent: container, child: SentryUserInteractionWidget( - child: const AppView(), + child: const App(), ), ), ); diff --git a/lib/core/app_info/app_info_provider.dart b/lib/core/app_info/app_info_provider.dart new file mode 100644 index 00000000..48042618 --- /dev/null +++ b/lib/core/app_info/app_info_provider.dart @@ -0,0 +1,30 @@ +import 'dart:io'; + +import 'package:hiddify/core/model/app_info_entity.dart'; +import 'package:hiddify/core/model/environment.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'app_info_provider.g.dart'; + +@Riverpod(keepAlive: true) +Environment environment(EnvironmentRef ref) => + throw Exception("override environmentProvider"); + +@Riverpod(keepAlive: true) +class AppInfo extends _$AppInfo { + @override + Future build() async { + final packageInfo = await PackageInfo.fromPlatform(); + final environment = ref.watch(environmentProvider); + return AppInfoEntity( + name: packageInfo.appName, + version: packageInfo.version, + buildNumber: packageInfo.buildNumber, + release: Release.read(), + operatingSystem: Platform.operatingSystem, + operatingSystemVersion: Platform.operatingSystemVersion, + environment: environment, + ); + } +} diff --git a/lib/core/core_providers.dart b/lib/core/core_providers.dart deleted file mode 100644 index 35ce17e4..00000000 --- a/lib/core/core_providers.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:hiddify/core/prefs/prefs.dart'; -import 'package:hiddify/domain/app/app.dart'; -import 'package:hiddify/domain/environment.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'core_providers.g.dart'; - -@Riverpod(keepAlive: true) -AppInfo appInfo(AppInfoRef ref) => - throw UnimplementedError('AppInfo must be overridden'); - -@Riverpod(keepAlive: true) -Environment env(EnvRef ref) => ref.watch(appInfoProvider).environment; - -@Riverpod(keepAlive: true) -TranslationsEn translations(TranslationsRef ref) => - ref.watch(localeNotifierProvider).build(); - -@Riverpod(keepAlive: true) -AppTheme theme(ThemeRef ref) => AppTheme( - ref.watch(themeModeNotifierProvider), - ref.watch(localeNotifierProvider).preferredFontFamily, - ); diff --git a/lib/data/local/database.dart b/lib/core/database/app_database.dart similarity index 71% rename from lib/data/local/database.dart rename to lib/core/database/app_database.dart index e18897d6..5130d37a 100644 --- a/lib/data/local/database.dart +++ b/lib/core/database/app_database.dart @@ -1,24 +1,20 @@ -import 'dart:io'; - import 'package:drift/drift.dart'; -import 'package:drift/native.dart'; -import 'package:hiddify/data/local/schema_versions.dart'; -import 'package:hiddify/data/local/tables.dart'; -import 'package:hiddify/data/local/type_converters.dart'; +import 'package:hiddify/core/database/connection/database_connection.dart'; +import 'package:hiddify/core/database/converters/duration_converter.dart'; +import 'package:hiddify/core/database/schema_versions.dart'; +import 'package:hiddify/core/database/tables/database_tables.dart'; import 'package:hiddify/features/geo_asset/data/geo_asset_data_mapper.dart'; import 'package:hiddify/features/geo_asset/model/default_geo_assets.dart'; import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart'; import 'package:hiddify/features/profile/model/profile_entity.dart'; -import 'package:hiddify/services/files_editor_service.dart'; -import 'package:path/path.dart' as p; -part 'database.g.dart'; +part 'app_database.g.dart'; @DriftDatabase(tables: [ProfileEntries, GeoAssetEntries]) class AppDatabase extends _$AppDatabase { AppDatabase({required QueryExecutor connection}) : super(connection); - AppDatabase.connect() : super(_openConnection()); + AppDatabase.connect() : super(openConnection()); @override int get schemaVersion => 3; @@ -61,11 +57,3 @@ class AppDatabase extends _$AppDatabase { }); } } - -LazyDatabase _openConnection() { - return LazyDatabase(() async { - final dbDir = await FilesEditorService.getDatabaseDirectory(); - final file = File(p.join(dbDir.path, 'db.sqlite')); - return NativeDatabase.createInBackground(file); - }); -} diff --git a/lib/core/database/connection/database_connection.dart b/lib/core/database/connection/database_connection.dart new file mode 100644 index 00000000..f05450e5 --- /dev/null +++ b/lib/core/database/connection/database_connection.dart @@ -0,0 +1,14 @@ +import 'dart:io'; + +import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; +import 'package:hiddify/services/files_editor_service.dart'; +import 'package:path/path.dart' as p; + +LazyDatabase openConnection() { + return LazyDatabase(() async { + final dbDir = await FilesEditorService.getDatabaseDirectory(); + final file = File(p.join(dbDir.path, 'db.sqlite')); + return NativeDatabase.createInBackground(file); + }); +} diff --git a/lib/data/local/type_converters.dart b/lib/core/database/converters/duration_converter.dart similarity index 100% rename from lib/data/local/type_converters.dart rename to lib/core/database/converters/duration_converter.dart diff --git a/lib/core/database/database_provider.dart b/lib/core/database/database_provider.dart new file mode 100644 index 00000000..fb388e20 --- /dev/null +++ b/lib/core/database/database_provider.dart @@ -0,0 +1,7 @@ +import 'package:hiddify/core/database/app_database.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'database_provider.g.dart'; + +@Riverpod(keepAlive: true) +AppDatabase appDatabase(AppDatabaseRef ref) => AppDatabase.connect(); diff --git a/lib/data/local/schema_versions.dart b/lib/core/database/schema_versions.dart similarity index 100% rename from lib/data/local/schema_versions.dart rename to lib/core/database/schema_versions.dart diff --git a/lib/data/local/schemas/drift_schema_v1.json b/lib/core/database/schemas/drift_schema_v1.json similarity index 100% rename from lib/data/local/schemas/drift_schema_v1.json rename to lib/core/database/schemas/drift_schema_v1.json diff --git a/lib/data/local/schemas/drift_schema_v2.json b/lib/core/database/schemas/drift_schema_v2.json similarity index 100% rename from lib/data/local/schemas/drift_schema_v2.json rename to lib/core/database/schemas/drift_schema_v2.json diff --git a/lib/core/database/schemas/drift_schema_v3.json b/lib/core/database/schemas/drift_schema_v3.json new file mode 100644 index 00000000..3cfc5fb9 --- /dev/null +++ b/lib/core/database/schemas/drift_schema_v3.json @@ -0,0 +1,286 @@ +{ + "_meta": { + "description": "This file contains a serialized version of schema entities for drift.", + "version": "1.1.0" + }, + "options": { + "store_date_time_values_as_text": true + }, + "entities": [ + { + "id": 0, + "references": [], + "type": "table", + "data": { + "name": "profile_entries", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "type", + "getter_name": "type", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(ProfileType.values)", + "dart_type_name": "ProfileType" + } + }, + { + "name": "active", + "getter_name": "active", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"active\" IN (0, 1))", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "allowed-lengths": { + "min": 1, + "max": null + } + } + ] + }, + { + "name": "url", + "getter_name": "url", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_update", + "getter_name": "lastUpdate", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "update_interval", + "getter_name": "updateInterval", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "DurationTypeConverter()", + "dart_type_name": "Duration" + } + }, + { + "name": "upload", + "getter_name": "upload", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "download", + "getter_name": "download", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "total", + "getter_name": "total", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "expire", + "getter_name": "expire", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "web_page_url", + "getter_name": "webPageUrl", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "support_url", + "getter_name": "supportUrl", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 1, + "references": [], + "type": "table", + "data": { + "name": "geo_asset_entries", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "type", + "getter_name": "type", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(GeoAssetType.values)", + "dart_type_name": "GeoAssetType" + } + }, + { + "name": "active", + "getter_name": "active", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"active\" IN (0, 1))", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "allowed-lengths": { + "min": 1, + "max": null + } + } + ] + }, + { + "name": "provider_name", + "getter_name": "providerName", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "allowed-lengths": { + "min": 1, + "max": null + } + } + ] + }, + { + "name": "version", + "getter_name": "version", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_check", + "getter_name": "lastCheck", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "id" + ], + "unique_keys": [ + [ + "name", + "provider_name" + ] + ] + } + } + ] +} \ No newline at end of file diff --git a/lib/data/local/tables.dart b/lib/core/database/tables/database_tables.dart similarity index 95% rename from lib/data/local/tables.dart rename to lib/core/database/tables/database_tables.dart index ab1d9a59..469f33de 100644 --- a/lib/data/local/tables.dart +++ b/lib/core/database/tables/database_tables.dart @@ -1,5 +1,5 @@ import 'package:drift/drift.dart'; -import 'package:hiddify/data/local/type_converters.dart'; +import 'package:hiddify/core/database/converters/duration_converter.dart'; import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart'; import 'package:hiddify/features/profile/model/profile_entity.dart'; diff --git a/lib/core/http_client/http_client_provider.dart b/lib/core/http_client/http_client_provider.dart new file mode 100644 index 00000000..0798a9d4 --- /dev/null +++ b/lib/core/http_client/http_client_provider.dart @@ -0,0 +1,28 @@ +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:hiddify/core/app_info/app_info_provider.dart'; +import 'package:hiddify/core/preferences/general_preferences.dart'; +import 'package:native_dio_adapter/native_dio_adapter.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'http_client_provider.g.dart'; + +@Riverpod(keepAlive: true) +Dio httpClient(HttpClientRef ref) { + final dio = Dio( + BaseOptions( + connectTimeout: const Duration(seconds: 15), + sendTimeout: const Duration(seconds: 15), + receiveTimeout: const Duration(seconds: 15), + headers: { + "User-Agent": ref.watch(appInfoProvider).requireValue.userAgent, + }, + ), + ); + final debug = ref.read(debugModeNotifierProvider); + if (debug && (Platform.isAndroid || Platform.isIOS || Platform.isMacOS)) { + dio.httpClientAdapter = NativeAdapter(); + } + return dio; +} diff --git a/lib/core/localization/locale_extensions.dart b/lib/core/localization/locale_extensions.dart new file mode 100644 index 00000000..e42d81e8 --- /dev/null +++ b/lib/core/localization/locale_extensions.dart @@ -0,0 +1,13 @@ +import 'package:flutter_localized_locales/flutter_localized_locales.dart'; +import 'package:hiddify/gen/fonts.gen.dart'; +import 'package:hiddify/gen/translations.g.dart'; + +extension AppLocaleX on AppLocale { + String get preferredFontFamily => + this == AppLocale.fa ? FontFamily.shabnam : ""; + + String get localeName => + LocaleNamesLocalizationsDelegate + .nativeLocaleNames[flutterLocale.toString()] ?? + name; +} diff --git a/lib/core/localization/locale_preferences.dart b/lib/core/localization/locale_preferences.dart new file mode 100644 index 00000000..28da4eff --- /dev/null +++ b/lib/core/localization/locale_preferences.dart @@ -0,0 +1,28 @@ +import 'package:hiddify/core/preferences/preferences_provider.dart'; +import 'package:hiddify/gen/translations.g.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'locale_preferences.g.dart'; + +@Riverpod(keepAlive: true) +class LocalePreferences extends _$LocalePreferences { + @override + AppLocale build() { + final persisted = + ref.watch(sharedPreferencesProvider).requireValue.getString("locale"); + if (persisted == null) return AppLocaleUtils.findDeviceLocale(); + // keep backward compatibility with chinese after changing zh to zh_CN + if (persisted == "zh") { + return AppLocale.zhCn; + } + return AppLocale.values.byName(persisted); + } + + Future changeLocale(AppLocale value) async { + state = value; + await ref + .read(sharedPreferencesProvider) + .requireValue + .setString("locale", value.name); + } +} diff --git a/lib/core/localization/translations.dart b/lib/core/localization/translations.dart new file mode 100644 index 00000000..461ffab2 --- /dev/null +++ b/lib/core/localization/translations.dart @@ -0,0 +1,11 @@ +import 'package:hiddify/core/localization/locale_preferences.dart'; +import 'package:hiddify/gen/translations.g.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +export 'package:hiddify/gen/translations.g.dart'; + +part 'translations.g.dart'; + +@Riverpod(keepAlive: true) +TranslationsEn translations(TranslationsRef ref) => + ref.watch(localePreferencesProvider).build(); diff --git a/lib/core/model/app_info_entity.dart b/lib/core/model/app_info_entity.dart new file mode 100644 index 00000000..c239d7fa --- /dev/null +++ b/lib/core/model/app_info_entity.dart @@ -0,0 +1,32 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hiddify/core/model/environment.dart'; + +part 'app_info_entity.freezed.dart'; + +@freezed +class AppInfoEntity with _$AppInfoEntity { + const AppInfoEntity._(); + + const factory AppInfoEntity({ + required String name, + required String version, + required String buildNumber, + required Release release, + required String operatingSystem, + required String operatingSystemVersion, + required Environment environment, + }) = _AppInfoEntity; + + String get userAgent => + "HiddifyNext/$version ($operatingSystem) like ClashMeta v2ray sing-box"; + + String get presentVersion => environment == Environment.prod + ? version + : "$version ${environment.name}"; + + /// formats app info for sharing + String format() => ''' +$name v$version ($buildNumber) [${environment.name}] +${release.name} release +$operatingSystem [$operatingSystemVersion]'''; +} diff --git a/lib/domain/constants.dart b/lib/core/model/constants.dart similarity index 63% rename from lib/domain/constants.dart rename to lib/core/model/constants.dart index da4e62dd..e2ffb59f 100644 --- a/lib/domain/constants.dart +++ b/lib/core/model/constants.dart @@ -1,9 +1,5 @@ abstract class Constants { static const appName = "Hiddify Next"; - static const geoipFileName = "geoip.db"; - static const geositeFileName = "geosite.db"; - static const configsFolderName = "configs"; - static const localHost = "127.0.0.1"; static const githubUrl = "https://github.com/hiddify/hiddify-next"; static const githubReleasesApiUrl = "https://api.github.com/repos/hiddify/hiddify-next/releases"; @@ -15,10 +11,3 @@ abstract class Constants { static const privacyPolicyUrl = "https://hiddify.com/en/privacy-policy/"; static const termsAndConditionsUrl = "https://hiddify.com/terms/"; } - -abstract class Defaults { - static const clashApiPort = 9090; - static const mixedPort = 2334; - static const connectionTestUrl = "https://www.gstatic.com/generate_204"; - static const concurrentTestCount = 5; -} diff --git a/lib/core/model/directories.dart b/lib/core/model/directories.dart new file mode 100644 index 00000000..b940b75d --- /dev/null +++ b/lib/core/model/directories.dart @@ -0,0 +1,7 @@ +import 'dart:io'; + +typedef Directories = ({ + Directory baseDir, + Directory workingDir, + Directory tempDir +}); diff --git a/lib/domain/environment.dart b/lib/core/model/environment.dart similarity index 100% rename from lib/domain/environment.dart rename to lib/core/model/environment.dart diff --git a/lib/domain/failures.dart b/lib/core/model/failures.dart similarity index 97% rename from lib/domain/failures.dart rename to lib/core/model/failures.dart index 1640ac61..1f28b6d9 100644 --- a/lib/domain/failures.dart +++ b/lib/core/model/failures.dart @@ -1,5 +1,5 @@ import 'package:dio/dio.dart'; -import 'package:hiddify/core/prefs/prefs.dart'; +import 'package:hiddify/core/localization/translations.dart'; typedef PresentableError = ({String type, String? message}); diff --git a/lib/core/model/region.dart b/lib/core/model/region.dart new file mode 100644 index 00000000..4b4a65eb --- /dev/null +++ b/lib/core/model/region.dart @@ -0,0 +1,15 @@ +import 'package:hiddify/core/localization/translations.dart'; + +enum Region { + ir, + cn, + ru, + other; + + String present(TranslationsEn t) => switch (this) { + ir => t.settings.general.regions.ir, + cn => t.settings.general.regions.cn, + ru => t.settings.general.regions.ru, + other => t.settings.general.regions.other, + }; +} diff --git a/lib/core/notification/in_app_notification_controller.dart b/lib/core/notification/in_app_notification_controller.dart index 1894e13e..7ef2113c 100644 --- a/lib/core/notification/in_app_notification_controller.dart +++ b/lib/core/notification/in_app_notification_controller.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:hiddify/domain/failures.dart'; +import 'package:hiddify/core/model/failures.dart'; import 'package:hiddify/features/common/adaptive_root_scaffold.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; diff --git a/lib/core/prefs/general_prefs.dart b/lib/core/preferences/general_preferences.dart similarity index 76% rename from lib/core/prefs/general_prefs.dart rename to lib/core/preferences/general_preferences.dart index 656ffb41..af0a8dbe 100644 --- a/lib/core/prefs/general_prefs.dart +++ b/lib/core/preferences/general_preferences.dart @@ -1,19 +1,22 @@ import 'package:flutter/foundation.dart'; -import 'package:hiddify/core/core_providers.dart'; -import 'package:hiddify/data/data_providers.dart'; -import 'package:hiddify/domain/environment.dart'; -import 'package:hiddify/domain/singbox/singbox.dart'; +import 'package:hiddify/core/app_info/app_info_provider.dart'; +import 'package:hiddify/core/model/environment.dart'; +import 'package:hiddify/core/model/region.dart'; +import 'package:hiddify/core/preferences/preferences_provider.dart'; +import 'package:hiddify/features/per_app_proxy/model/per_app_proxy_mode.dart'; import 'package:hiddify/utils/pref_notifier.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -part 'general_prefs.g.dart'; +part 'general_preferences.g.dart'; + +// TODO refactor bool _debugIntroPage = false; @Riverpod(keepAlive: true) class IntroCompleted extends _$IntroCompleted { late final _pref = Pref( - ref.watch(sharedPreferencesProvider), + ref.watch(sharedPreferencesProvider).requireValue, "intro_completed", false, ); @@ -33,7 +36,7 @@ class IntroCompleted extends _$IntroCompleted { @Riverpod(keepAlive: true) class RegionNotifier extends _$RegionNotifier { late final _pref = Pref( - ref.watch(sharedPreferencesProvider), + ref.watch(sharedPreferencesProvider).requireValue, "region", Region.other, mapFrom: Region.values.byName, @@ -51,8 +54,11 @@ class RegionNotifier extends _$RegionNotifier { @Riverpod(keepAlive: true) class SilentStartNotifier extends _$SilentStartNotifier { - late final _pref = - Pref(ref.watch(sharedPreferencesProvider), "silent_start", false); + late final _pref = Pref( + ref.watch(sharedPreferencesProvider).requireValue, + "silent_start", + false, + ); @override bool build() => _pref.getValue(); @@ -66,7 +72,7 @@ class SilentStartNotifier extends _$SilentStartNotifier { @Riverpod(keepAlive: true) class EnableAnalytics extends _$EnableAnalytics { late final _pref = Pref( - ref.watch(sharedPreferencesProvider), + ref.watch(sharedPreferencesProvider).requireValue, "enable_analytics", true, ); @@ -83,7 +89,7 @@ class EnableAnalytics extends _$EnableAnalytics { @Riverpod(keepAlive: true) class DisableMemoryLimit extends _$DisableMemoryLimit { late final _pref = Pref( - ref.watch(sharedPreferencesProvider), + ref.watch(sharedPreferencesProvider).requireValue, "disable_memory_limit", false, ); @@ -100,9 +106,9 @@ class DisableMemoryLimit extends _$DisableMemoryLimit { @Riverpod(keepAlive: true) class DebugModeNotifier extends _$DebugModeNotifier { late final _pref = Pref( - ref.watch(sharedPreferencesProvider), + ref.watch(sharedPreferencesProvider).requireValue, "debug_mode", - ref.read(envProvider) == Environment.dev, + ref.read(environmentProvider) == Environment.dev, ); @override @@ -117,7 +123,7 @@ class DebugModeNotifier extends _$DebugModeNotifier { @Riverpod(keepAlive: true) class PerAppProxyModeNotifier extends _$PerAppProxyModeNotifier { late final _pref = Pref( - ref.watch(sharedPreferencesProvider), + ref.watch(sharedPreferencesProvider).requireValue, "per_app_proxy_mode", PerAppProxyMode.off, mapFrom: PerAppProxyMode.values.byName, @@ -136,13 +142,13 @@ class PerAppProxyModeNotifier extends _$PerAppProxyModeNotifier { @Riverpod(keepAlive: true) class PerAppProxyList extends _$PerAppProxyList { late final _include = Pref( - ref.watch(sharedPreferencesProvider), + ref.watch(sharedPreferencesProvider).requireValue, "per_app_proxy_include_list", [], ); late final _exclude = Pref( - ref.watch(sharedPreferencesProvider), + ref.watch(sharedPreferencesProvider).requireValue, "per_app_proxy_exclude_list", [], ); @@ -165,7 +171,7 @@ class PerAppProxyList extends _$PerAppProxyList { @riverpod class MarkNewProfileActive extends _$MarkNewProfileActive { late final _pref = Pref( - ref.watch(sharedPreferencesProvider), + ref.watch(sharedPreferencesProvider).requireValue, "mark_new_profile_active", true, ); diff --git a/lib/core/preferences/preferences_provider.dart b/lib/core/preferences/preferences_provider.dart new file mode 100644 index 00000000..533f6bd8 --- /dev/null +++ b/lib/core/preferences/preferences_provider.dart @@ -0,0 +1,8 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +part 'preferences_provider.g.dart'; + +@Riverpod(keepAlive: true) +Future sharedPreferences(SharedPreferencesRef ref) async => + SharedPreferences.getInstance(); diff --git a/lib/core/prefs/service_prefs.dart b/lib/core/preferences/service_preferences.dart similarity index 63% rename from lib/core/prefs/service_prefs.dart rename to lib/core/preferences/service_preferences.dart index d9cc2cbb..847c614f 100644 --- a/lib/core/prefs/service_prefs.dart +++ b/lib/core/preferences/service_preferences.dart @@ -1,14 +1,17 @@ -import 'package:hiddify/data/data_providers.dart'; +import 'package:hiddify/core/preferences/preferences_provider.dart'; import 'package:hiddify/utils/pref_notifier.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -part 'service_prefs.g.dart'; +part 'service_preferences.g.dart'; @Riverpod(keepAlive: true) class StartedByUser extends _$StartedByUser with AppLogger { - late final _pref = - Pref(ref.watch(sharedPreferencesProvider), "started_by_user", false); + late final _pref = Pref( + ref.watch(sharedPreferencesProvider).requireValue, + "started_by_user", + false, + ); @override bool build() => _pref.getValue(); diff --git a/lib/core/prefs/locale_prefs.dart b/lib/core/prefs/locale_prefs.dart deleted file mode 100644 index 95352687..00000000 --- a/lib/core/prefs/locale_prefs.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:flutter_localized_locales/flutter_localized_locales.dart'; -import 'package:hiddify/data/data_providers.dart'; -import 'package:hiddify/gen/fonts.gen.dart'; -import 'package:hiddify/gen/translations.g.dart'; -import 'package:hiddify/utils/pref_notifier.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -export 'package:hiddify/gen/translations.g.dart'; - -part 'locale_prefs.g.dart'; - -@Riverpod(keepAlive: true) -class LocaleNotifier extends _$LocaleNotifier { - late final _pref = Pref( - ref.watch(sharedPreferencesProvider), - "locale", - AppLocaleUtils.findDeviceLocale(), - mapFrom: (String value) { - // keep backward compatibility with chinese after changing zh to zh_CN - if (value == "zh") { - return AppLocale.zhCn; - } - return AppLocale.values.byName(value); - }, - mapTo: (value) => value.name, - ); - - @override - AppLocale build() => _pref.getValue(); - - Future update(AppLocale value) { - state = value; - return _pref.update(value); - } -} - -extension AppLocaleX on AppLocale { - String get preferredFontFamily => - this == AppLocale.fa ? FontFamily.shabnam : ""; - - String get localeName => - LocaleNamesLocalizationsDelegate - .nativeLocaleNames[flutterLocale.toString()] ?? - name; -} diff --git a/lib/core/prefs/prefs.dart b/lib/core/prefs/prefs.dart deleted file mode 100644 index 2d8fa8fc..00000000 --- a/lib/core/prefs/prefs.dart +++ /dev/null @@ -1,4 +0,0 @@ -export 'app_theme.dart'; -export 'general_prefs.dart'; -export 'locale_prefs.dart'; -export 'theme_prefs.dart'; diff --git a/lib/core/prefs/theme_prefs.dart b/lib/core/prefs/theme_prefs.dart deleted file mode 100644 index a9f21b3b..00000000 --- a/lib/core/prefs/theme_prefs.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:hiddify/core/prefs/app_theme.dart'; -import 'package:hiddify/data/data_providers.dart'; -import 'package:hiddify/utils/pref_notifier.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'theme_prefs.g.dart'; - -@Riverpod(keepAlive: true) -class ThemeModeNotifier extends _$ThemeModeNotifier { - late final _pref = Pref( - ref.watch(sharedPreferencesProvider), - "theme_mode", - AppThemeMode.system, - mapFrom: AppThemeMode.values.byName, - mapTo: (value) => value.name, - ); - - @override - AppThemeMode build() => _pref.getValue(); - - Future update(AppThemeMode value) { - state = value; - return _pref.update(value); - } -} diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index 1339c2d9..44e14eac 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -1,7 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:hiddify/core/prefs/prefs.dart'; +import 'package:hiddify/core/preferences/general_preferences.dart'; import 'package:hiddify/core/router/routes.dart'; import 'package:hiddify/services/deep_link_service.dart'; import 'package:hiddify/utils/utils.dart'; @@ -92,6 +92,7 @@ class RouterListenable extends _$RouterListenable }); } +// ignore: avoid_build_context_in_providers String? redirect(BuildContext context, GoRouterState state) { // if (this.state.isLoading || this.state.hasError) return null; diff --git a/lib/core/router/routes.dart b/lib/core/router/routes.dart index 06c50e7f..d5ec6869 100644 --- a/lib/core/router/routes.dart +++ b/lib/core/router/routes.dart @@ -1,19 +1,19 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:hiddify/core/router/app_router.dart'; -import 'package:hiddify/features/about/view/about_page.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/geo_asset/overview/geo_assets_overview_page.dart'; -import 'package:hiddify/features/home/view/view.dart'; -import 'package:hiddify/features/intro/intro_page.dart'; +import 'package:hiddify/features/home/widget/home_page.dart'; +import 'package:hiddify/features/intro/widget/intro_page.dart'; import 'package:hiddify/features/log/overview/logs_overview_page.dart'; +import 'package:hiddify/features/per_app_proxy/overview/per_app_proxy_page.dart'; import 'package:hiddify/features/profile/add/add_profile_modal.dart'; import 'package:hiddify/features/profile/details/profile_details_page.dart'; import 'package:hiddify/features/profile/overview/profiles_overview_page.dart'; -import 'package:hiddify/features/proxies/view/view.dart'; -import 'package:hiddify/features/settings/view/config_options_page.dart'; -import 'package:hiddify/features/settings/view/per_app_proxy_page.dart'; -import 'package:hiddify/features/settings/view/settings_page.dart'; +import 'package:hiddify/features/proxy/overview/proxies_overview_page.dart'; +import 'package:hiddify/features/settings/about/about_page.dart'; +import 'package:hiddify/features/settings/overview/settings_overview_page.dart'; import 'package:hiddify/utils/utils.dart'; part 'routes.g.dart'; @@ -184,7 +184,7 @@ class ProxiesRoute extends GoRouteData { Page buildPage(BuildContext context, GoRouterState state) { return const NoTransitionPage( name: name, - child: ProxiesPage(), + child: ProxiesOverviewPage(), ); } } @@ -291,10 +291,10 @@ class SettingsRoute extends GoRouteData { return const MaterialPage( fullscreenDialog: true, name: name, - child: SettingsPage(), + child: SettingsOverviewPage(), ); } - return const NoTransitionPage(name: name, child: SettingsPage()); + return const NoTransitionPage(name: name, child: SettingsOverviewPage()); } } diff --git a/lib/core/prefs/app_theme.dart b/lib/core/theme/app_theme.dart similarity index 77% rename from lib/core/prefs/app_theme.dart rename to lib/core/theme/app_theme.dart index 786be4f5..60bce95b 100644 --- a/lib/core/prefs/app_theme.dart +++ b/lib/core/theme/app_theme.dart @@ -1,31 +1,9 @@ +// mostly exact copy of flex color scheme 7.1's fabulous 12 theme import 'package:flex_color_scheme/flex_color_scheme.dart'; import 'package:flutter/material.dart'; -import 'package:hiddify/core/prefs/locale_prefs.dart'; +import 'package:hiddify/core/theme/app_theme_mode.dart'; +import 'package:hiddify/core/theme/theme_extensions.dart'; -enum AppThemeMode { - system, - light, - dark, - black; - - String present(TranslationsEn t) => switch (this) { - system => t.settings.general.themeModes.system, - light => t.settings.general.themeModes.light, - dark => t.settings.general.themeModes.dark, - black => t.settings.general.themeModes.black, - }; - - ThemeMode get flutterThemeMode => switch (this) { - system => ThemeMode.system, - light => ThemeMode.light, - dark => ThemeMode.dark, - black => ThemeMode.dark, - }; - - bool get trueBlack => this == black; -} - -// mostly exact copy of flex color scheme 7.1's fabulous 12 theme class AppTheme { AppTheme( this.mode, @@ -160,42 +138,3 @@ class AppTheme { ); } } - -class ConnectionButtonTheme extends ThemeExtension { - const ConnectionButtonTheme({ - this.idleColor, - this.connectedColor, - }); - - final Color? idleColor; - final Color? connectedColor; - - static const ConnectionButtonTheme light = ConnectionButtonTheme( - idleColor: Color(0xFF4a4d8b), - connectedColor: Color(0xFF44a334), - ); - - @override - ThemeExtension copyWith({ - Color? idleColor, - Color? connectedColor, - }) => - ConnectionButtonTheme( - idleColor: idleColor ?? this.idleColor, - connectedColor: connectedColor ?? this.connectedColor, - ); - - @override - ThemeExtension lerp( - covariant ThemeExtension? other, - double t, - ) { - if (other is! ConnectionButtonTheme) { - return this; - } - return ConnectionButtonTheme( - idleColor: Color.lerp(idleColor, other.idleColor, t), - connectedColor: Color.lerp(connectedColor, other.connectedColor, t), - ); - } -} diff --git a/lib/core/theme/app_theme_mode.dart b/lib/core/theme/app_theme_mode.dart new file mode 100644 index 00000000..5696f70c --- /dev/null +++ b/lib/core/theme/app_theme_mode.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:hiddify/core/localization/translations.dart'; + +enum AppThemeMode { + system, + light, + dark, + black; + + String present(TranslationsEn t) => switch (this) { + system => t.settings.general.themeModes.system, + light => t.settings.general.themeModes.light, + dark => t.settings.general.themeModes.dark, + black => t.settings.general.themeModes.black, + }; + + ThemeMode get flutterThemeMode => switch (this) { + system => ThemeMode.system, + light => ThemeMode.light, + dark => ThemeMode.dark, + black => ThemeMode.dark, + }; + + bool get trueBlack => this == black; +} diff --git a/lib/core/theme/theme_extensions.dart b/lib/core/theme/theme_extensions.dart new file mode 100644 index 00000000..7a7895c2 --- /dev/null +++ b/lib/core/theme/theme_extensions.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; + +class ConnectionButtonTheme extends ThemeExtension { + const ConnectionButtonTheme({ + this.idleColor, + this.connectedColor, + }); + + final Color? idleColor; + final Color? connectedColor; + + static const ConnectionButtonTheme light = ConnectionButtonTheme( + idleColor: Color(0xFF4a4d8b), + connectedColor: Color(0xFF44a334), + ); + + @override + ThemeExtension copyWith({ + Color? idleColor, + Color? connectedColor, + }) => + ConnectionButtonTheme( + idleColor: idleColor ?? this.idleColor, + connectedColor: connectedColor ?? this.connectedColor, + ); + + @override + ThemeExtension lerp( + covariant ThemeExtension? other, + double t, + ) { + if (other is! ConnectionButtonTheme) { + return this; + } + return ConnectionButtonTheme( + idleColor: Color.lerp(idleColor, other.idleColor, t), + connectedColor: Color.lerp(connectedColor, other.connectedColor, t), + ); + } +} diff --git a/lib/core/theme/theme_preferences.dart b/lib/core/theme/theme_preferences.dart new file mode 100644 index 00000000..29fd4d61 --- /dev/null +++ b/lib/core/theme/theme_preferences.dart @@ -0,0 +1,26 @@ +import 'package:hiddify/core/preferences/preferences_provider.dart'; +import 'package:hiddify/core/theme/app_theme_mode.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'theme_preferences.g.dart'; + +@Riverpod(keepAlive: true) +class ThemePreferences extends _$ThemePreferences { + @override + AppThemeMode build() { + final persisted = ref + .watch(sharedPreferencesProvider) + .requireValue + .getString("theme_mode"); + if (persisted == null) return AppThemeMode.system; + return AppThemeMode.values.byName(persisted); + } + + Future changeThemeMode(AppThemeMode value) async { + state = value; + await ref + .read(sharedPreferencesProvider) + .requireValue + .setString("theme_mode", value.name); + } +} diff --git a/lib/data/repository/exception_handlers.dart b/lib/core/utils/exception_handler.dart similarity index 100% rename from lib/data/repository/exception_handlers.dart rename to lib/core/utils/exception_handler.dart diff --git a/lib/utils/ffi_utils.dart b/lib/core/utils/ffi_utils.dart similarity index 100% rename from lib/utils/ffi_utils.dart rename to lib/core/utils/ffi_utils.dart diff --git a/lib/core/utils/json_converters.dart b/lib/core/utils/json_converters.dart new file mode 100644 index 00000000..290f6da8 --- /dev/null +++ b/lib/core/utils/json_converters.dart @@ -0,0 +1,11 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +class IntervalInSecondsConverter implements JsonConverter { + const IntervalInSecondsConverter(); + + @override + Duration fromJson(int json) => Duration(seconds: json); + + @override + int toJson(Duration object) => object.inSeconds; +} diff --git a/lib/core/widget/custom_alert_dialog.dart b/lib/core/widget/custom_alert_dialog.dart index ad96ed1f..c3ca3f27 100644 --- a/lib/core/widget/custom_alert_dialog.dart +++ b/lib/core/widget/custom_alert_dialog.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:hiddify/domain/failures.dart'; +import 'package:hiddify/core/model/failures.dart'; class CustomAlertDialog extends StatelessWidget { const CustomAlertDialog({ diff --git a/lib/data/data_providers.dart b/lib/data/data_providers.dart deleted file mode 100644 index 0f1b653c..00000000 --- a/lib/data/data_providers.dart +++ /dev/null @@ -1,86 +0,0 @@ -import 'dart:io'; - -import 'package:dio/dio.dart'; -import 'package:hiddify/core/core_providers.dart'; -import 'package:hiddify/core/prefs/general_prefs.dart'; -import 'package:hiddify/data/local/database.dart'; -import 'package:hiddify/data/repository/app_repository_impl.dart'; -import 'package:hiddify/data/repository/config_options_store.dart'; -import 'package:hiddify/data/repository/repository.dart'; -import 'package:hiddify/domain/app/app.dart'; -import 'package:hiddify/domain/core_facade.dart'; -import 'package:hiddify/domain/singbox/singbox.dart'; -import 'package:hiddify/features/geo_asset/data/geo_asset_data_providers.dart'; -import 'package:hiddify/features/profile/data/profile_data_providers.dart'; -import 'package:hiddify/services/service_providers.dart'; -import 'package:native_dio_adapter/native_dio_adapter.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -part 'data_providers.g.dart'; - -@Riverpod(keepAlive: true) -AppDatabase appDatabase(AppDatabaseRef ref) => AppDatabase.connect(); - -@Riverpod(keepAlive: true) -SharedPreferences sharedPreferences(SharedPreferencesRef ref) => - throw UnimplementedError('sharedPreferences must be overridden'); - -@Riverpod(keepAlive: true) -Dio dio(DioRef ref) { - final dio = Dio( - BaseOptions( - connectTimeout: const Duration(seconds: 15), - sendTimeout: const Duration(seconds: 15), - receiveTimeout: const Duration(seconds: 15), - headers: { - "User-Agent": ref.watch(appInfoProvider).userAgent, - }, - ), - ); - final debug = ref.read(debugModeNotifierProvider); - if (debug && (Platform.isAndroid || Platform.isIOS || Platform.isMacOS)) { - dio.httpClientAdapter = NativeAdapter(); - } - return dio; -} - -@Riverpod(keepAlive: true) -AppRepository appRepository(AppRepositoryRef ref) => - AppRepositoryImpl(ref.watch(dioProvider)); - -@riverpod -Future configOptions(ConfigOptionsRef ref) async { - final geoAssets = await ref - .watch(geoAssetRepositoryProvider) - .requireValue - .getActivePair() - .getOrElse((l) => throw l) - .run(); - final geoAssetsPathResolver = ref.watch(geoAssetPathResolverProvider); - - final serviceMode = ref.watch(serviceModeStoreProvider); - return ref.watch(configPreferencesProvider).copyWith( - enableTun: serviceMode == ServiceMode.tun, - setSystemProxy: serviceMode == ServiceMode.systemProxy, - geoipPath: geoAssetsPathResolver.relativePath( - geoAssets.geoip.providerName, - geoAssets.geoip.fileName, - ), - geositePath: geoAssetsPathResolver.relativePath( - geoAssets.geosite.providerName, - geoAssets.geosite.fileName, - ), - ); -} - -@Riverpod(keepAlive: true) -CoreFacade coreFacade(CoreFacadeRef ref) => CoreFacadeImpl( - ref.watch(singboxServiceProvider), - ref.watch(filesEditorServiceProvider), - ref.watch(geoAssetPathResolverProvider), - ref.watch(profilePathResolverProvider), - ref.watch(platformServicesProvider), - ref.read(debugModeNotifierProvider), - () => ref.read(configOptionsProvider.future), - ); diff --git a/lib/data/local/schemas/drift_schema_v3.json b/lib/data/local/schemas/drift_schema_v3.json deleted file mode 100644 index 4e845f77..00000000 --- a/lib/data/local/schemas/drift_schema_v3.json +++ /dev/null @@ -1 +0,0 @@ -{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.1.0"},"options":{"store_date_time_values_as_text":true},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"profile_entries","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(ProfileType.values)","dart_type_name":"ProfileType"}},{"name":"active","getter_name":"active","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"active\" IN (0, 1))","default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[{"allowed-lengths":{"min":1,"max":null}}]},{"name":"url","getter_name":"url","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"last_update","getter_name":"lastUpdate","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"update_interval","getter_name":"updateInterval","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"DurationTypeConverter()","dart_type_name":"Duration"}},{"name":"upload","getter_name":"upload","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"download","getter_name":"download","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"total","getter_name":"total","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"expire","getter_name":"expire","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"web_page_url","getter_name":"webPageUrl","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"support_url","getter_name":"supportUrl","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":1,"references":[],"type":"table","data":{"name":"geo_asset_entries","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(GeoAssetType.values)","dart_type_name":"GeoAssetType"}},{"name":"active","getter_name":"active","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"active\" IN (0, 1))","default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[{"allowed-lengths":{"min":1,"max":null}}]},{"name":"provider_name","getter_name":"providerName","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[{"allowed-lengths":{"min":1,"max":null}}]},{"name":"version","getter_name":"version","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"last_check","getter_name":"lastCheck","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"],"unique_keys":[["name","provider_name"]]}}]} \ No newline at end of file diff --git a/lib/data/repository/app_repository_impl.dart b/lib/data/repository/app_repository_impl.dart deleted file mode 100644 index f353da0e..00000000 --- a/lib/data/repository/app_repository_impl.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'dart:io'; - -import 'package:dio/dio.dart'; -import 'package:fpdart/fpdart.dart'; -import 'package:hiddify/data/repository/exception_handlers.dart'; -import 'package:hiddify/domain/app/app.dart'; -import 'package:hiddify/domain/constants.dart'; -import 'package:hiddify/domain/environment.dart'; -import 'package:hiddify/utils/custom_loggers.dart'; -import 'package:package_info_plus/package_info_plus.dart'; - -class AppRepositoryImpl - with ExceptionHandler, InfraLogger - implements AppRepository { - AppRepositoryImpl(this.dio); - - final Dio dio; - - static Future getAppInfo(Environment environment) async { - final packageInfo = await PackageInfo.fromPlatform(); - return AppInfo( - name: packageInfo.appName, - version: packageInfo.version, - buildNumber: packageInfo.buildNumber, - release: Release.read(), - operatingSystem: Platform.operatingSystem, - operatingSystemVersion: Platform.operatingSystemVersion, - environment: environment, - ); - } - - // TODO add market-specific update checking - @override - TaskEither getLatestVersion({ - bool includePreReleases = false, - Release release = Release.general, - }) { - return exceptionHandler( - () async { - if (!release.allowCustomUpdateChecker) { - throw Exception("custom update checkers are not supported"); - } - final response = await dio.get(Constants.githubReleasesApiUrl); - if (response.statusCode != 200 || response.data == null) { - loggy.warning("failed to fetch latest version info"); - return left(const AppFailure.unexpected()); - } - - final releases = response.data! - .map((e) => RemoteVersionInfo.fromJson(e as Map)); - late RemoteVersionInfo latest; - if (includePreReleases) { - latest = releases.first; - } else { - latest = releases.firstWhere((e) => e.preRelease == false); - } - return right(latest); - }, - AppFailure.unexpected, - ); - } -} diff --git a/lib/data/repository/config_options_store.dart b/lib/data/repository/config_options_store.dart deleted file mode 100644 index 55cb31b4..00000000 --- a/lib/data/repository/config_options_store.dart +++ /dev/null @@ -1,155 +0,0 @@ -// ignore_for_file: avoid_manual_providers_as_generated_provider_dependency -import 'package:flutter/foundation.dart'; -import 'package:hiddify/core/prefs/prefs.dart'; -import 'package:hiddify/data/data_providers.dart'; -import 'package:hiddify/domain/singbox/singbox.dart'; -import 'package:hiddify/features/log/model/log_level.dart'; -import 'package:hiddify/utils/pref_notifier.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'config_options_store.g.dart'; - -bool _debugConfigBuilder = false; -final _default = ConfigOptions.initial; - -@Riverpod(keepAlive: true) -class ServiceModeStore extends _$ServiceModeStore { - late final _pref = Pref( - ref.watch(sharedPreferencesProvider), - "service-mode", - ServiceMode.defaultMode, - mapFrom: ServiceMode.values.byName, - mapTo: (value) => value.name, - ); - - @override - ServiceMode build() => _pref.getValue(); - - Future update(ServiceMode value) { - state = value; - return _pref.update(value); - } -} - -final logLevelStore = PrefNotifier.provider( - "log-level", - _default.logLevel, - mapFrom: LogLevel.values.byName, - mapTo: (value) => value.name, -); -final resolveDestinationStore = - PrefNotifier.provider("resolve-destination", _default.resolveDestination); -final ipv6ModeStore = PrefNotifier.provider( - "ipv6-mode", - _default.ipv6Mode, - mapFrom: IPv6Mode.values.byName, - mapTo: (value) => value.name, -); -final remoteDnsAddressStore = - PrefNotifier.provider("remote-dns-address", _default.remoteDnsAddress); -final remoteDnsDomainStrategyStore = PrefNotifier.provider( - "remote-domain-dns-strategy", - _default.remoteDnsDomainStrategy, - mapFrom: DomainStrategy.values.byName, - mapTo: (value) => value.name, -); -final directDnsAddressStore = - PrefNotifier.provider("direct-dns-address", _default.directDnsAddress); -final directDnsDomainStrategyStore = PrefNotifier.provider( - "direct-domain-dns-strategy", - _default.directDnsDomainStrategy, - mapFrom: DomainStrategy.values.byName, - mapTo: (value) => value.name, -); -final mixedPortStore = PrefNotifier.provider("mixed-port", _default.mixedPort); -final localDnsPortStore = - PrefNotifier.provider("localDns-port", _default.localDnsPort); -final tunImplementationStore = PrefNotifier.provider( - "tun-implementation", - _default.tunImplementation, - mapFrom: TunImplementation.values.byName, - mapTo: (value) => value.name, -); -final mtuStore = PrefNotifier.provider("mtu", _default.mtu); -final connectionTestUrlStore = - PrefNotifier.provider("connection-test-url", _default.connectionTestUrl); -final urlTestIntervalStore = PrefNotifier.provider( - "url-test-interval", - _default.urlTestInterval, - mapFrom: (value) => Duration(seconds: value), - mapTo: (value) => value.inSeconds, -); -final enableClashApiStore = - PrefNotifier.provider("enable-clash-api", _default.enableClashApi); -final clashApiPortStore = - PrefNotifier.provider("clash-api-port", _default.clashApiPort); -// final enableTunStore = PrefNotifier.provider("enable-tun", _default.enableTun); -// final setSystemProxyStore = -// PrefNotifier.provider("set-system-proxy", _default.setSystemProxy); -final strictRouteStore = - PrefNotifier.provider("strict-route", _default.strictRoute); -final bypassLanStore = PrefNotifier.provider("bypass-lan", _default.bypassLan); -final enableFakeDnsStore = - PrefNotifier.provider("enable-fake-dns", _default.enableFakeDns); - -// HACK temporary -@riverpod -List rules(RulesRef ref) => switch (ref.watch(regionNotifierProvider)) { - Region.ir => [ - const Rule( - id: "id", - name: "name", - enabled: true, - domains: "domain:.ir,geosite:ir", - ip: "geoip:ir", - outbound: RuleOutbound.bypass, - ), - ], - Region.cn => [ - const Rule( - id: "id", - name: "name", - enabled: true, - domains: "domain:.cn,geosite:cn", - ip: "geoip:cn", - outbound: RuleOutbound.bypass, - ), - ], - Region.ru => [ - const Rule( - id: "id", - name: "name", - enabled: true, - domains: "domain:.ru", - ip: "geoip:ru", - outbound: RuleOutbound.bypass, - ), - ], - _ => [], - }; - -@riverpod -ConfigOptions configPreferences(ConfigPreferencesRef ref) { - return ConfigOptions( - executeConfigAsIs: kDebugMode && _debugConfigBuilder, - logLevel: ref.watch(logLevelStore), - resolveDestination: ref.watch(resolveDestinationStore), - ipv6Mode: ref.watch(ipv6ModeStore), - remoteDnsAddress: ref.watch(remoteDnsAddressStore), - remoteDnsDomainStrategy: ref.watch(remoteDnsDomainStrategyStore), - directDnsAddress: ref.watch(directDnsAddressStore), - directDnsDomainStrategy: ref.watch(directDnsDomainStrategyStore), - mixedPort: ref.watch(mixedPortStore), - localDnsPort: ref.watch(localDnsPortStore), - tunImplementation: ref.watch(tunImplementationStore), - mtu: ref.watch(mtuStore), - strictRoute: ref.watch(strictRouteStore), - connectionTestUrl: ref.watch(connectionTestUrlStore), - urlTestInterval: ref.watch(urlTestIntervalStore), - enableClashApi: ref.watch(enableClashApiStore), - clashApiPort: ref.watch(clashApiPortStore), - bypassLan: ref.watch(bypassLanStore), - enableFakeDns: ref.watch(enableFakeDnsStore), - rules: ref.watch(rulesProvider), - ); -} diff --git a/lib/data/repository/core_facade_impl.dart b/lib/data/repository/core_facade_impl.dart deleted file mode 100644 index eea0b4df..00000000 --- a/lib/data/repository/core_facade_impl.dart +++ /dev/null @@ -1,246 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:fpdart/fpdart.dart'; -import 'package:hiddify/data/repository/exception_handlers.dart'; -import 'package:hiddify/domain/connectivity/connection_status.dart'; -import 'package:hiddify/domain/core_facade.dart'; -import 'package:hiddify/domain/core_service_failure.dart'; -import 'package:hiddify/domain/singbox/singbox.dart'; -import 'package:hiddify/features/geo_asset/data/geo_asset_path_resolver.dart'; -import 'package:hiddify/features/profile/data/profile_path_resolver.dart'; -import 'package:hiddify/services/files_editor_service.dart'; -import 'package:hiddify/services/platform_services.dart'; -import 'package:hiddify/services/singbox/singbox_service.dart'; -import 'package:hiddify/utils/utils.dart'; - -class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { - CoreFacadeImpl( - this.singbox, - this.filesEditor, - this.geoAssetPathResolver, - this.profilePathResolver, - this.platformServices, - this.debug, - this.configOptions, - ); - - final SingboxService singbox; - final FilesEditorService filesEditor; - final GeoAssetPathResolver geoAssetPathResolver; - final ProfilePathResolver profilePathResolver; - final PlatformServices platformServices; - final bool debug; - final Future Function() configOptions; - - bool _initialized = false; - - TaskEither _getConfigOptions() { - return exceptionHandler( - () async { - final options = await configOptions(); - final geoip = geoAssetPathResolver.resolvePath(options.geoipPath); - final geosite = geoAssetPathResolver.resolvePath(options.geositePath); - if (!await File(geoip).exists() || !await File(geosite).exists()) { - return left(const CoreMissingGeoAssets()); - } - return right(options); - }, - CoreServiceFailure.unexpected, - ); - } - - @override - TaskEither setup() { - if (_initialized) return TaskEither.of(unit); - return exceptionHandler( - () { - loggy.debug("setting up singbox"); - return singbox - .setup( - filesEditor.dirs.baseDir.path, - filesEditor.dirs.workingDir.path, - filesEditor.dirs.tempDir.path, - debug, - ) - .map((r) { - loggy.debug("setup complete"); - _initialized = true; - return r; - }) - .mapLeft(CoreServiceFailure.other) - .run(); - }, - CoreServiceFailure.unexpected, - ); - } - - @override - TaskEither parseConfig( - String path, - String tempPath, - bool debug, - ) { - return exceptionHandler( - () { - return singbox - .parseConfig(path, tempPath, debug) - .mapLeft(CoreServiceFailure.invalidConfig) - .run(); - }, - CoreServiceFailure.unexpected, - ); - } - - @override - TaskEither changeConfigOptions( - ConfigOptions options, - ) { - return exceptionHandler( - () { - return singbox - .changeConfigOptions(options) - .mapLeft(CoreServiceFailure.invalidConfigOptions) - .run(); - }, - CoreServiceFailure.unexpected, - ); - } - - @override - TaskEither generateConfig( - String fileName, - ) { - return TaskEither.Do( - ($) async { - final configFile = profilePathResolver.file(fileName); - final options = await $(_getConfigOptions()); - await $(setup()); - await $(changeConfigOptions(options)); - return await $( - singbox - .generateConfig(configFile.path) - .mapLeft(CoreServiceFailure.other), - ); - }, - ).handleExceptions(CoreServiceFailure.unexpected); - } - - @override - TaskEither start( - String fileName, - bool disableMemoryLimit, - ) { - return TaskEither.Do( - ($) async { - final configFile = profilePathResolver.file(fileName); - final options = await $(_getConfigOptions()); - loggy.info( - "config options: ${options.format()}\nMemory Limit: ${!disableMemoryLimit}", - ); - - await $( - TaskEither(() async { - if (options.enableTun) { - final hasPrivilege = await platformServices.hasPrivilege(); - if (!hasPrivilege) { - loggy.warning("missing privileges for tun mode"); - return left(const CoreMissingPrivilege()); - } - } - return right(unit); - }), - ); - await $(setup()); - await $(changeConfigOptions(options)); - return await $( - singbox - .start(configFile.path, disableMemoryLimit) - .mapLeft(CoreServiceFailure.start), - ); - }, - ).handleExceptions(CoreServiceFailure.unexpected); - } - - @override - TaskEither stop() { - return exceptionHandler( - () => singbox.stop().mapLeft(CoreServiceFailure.other).run(), - CoreServiceFailure.unexpected, - ); - } - - @override - TaskEither restart( - String fileName, - bool disableMemoryLimit, - ) { - return exceptionHandler( - () async { - final configFile = profilePathResolver.file(fileName); - return _getConfigOptions() - .flatMap((options) => changeConfigOptions(options)) - .andThen( - () => singbox - .restart(configFile.path, disableMemoryLimit) - .mapLeft(CoreServiceFailure.start), - ) - .run(); - }, - CoreServiceFailure.unexpected, - ); - } - - @override - Stream>> watchOutbounds() { - return singbox.watchOutbounds().map((event) { - return (jsonDecode(event) as List).map((e) { - return OutboundGroup.fromJson(e as Map); - }).toList(); - }).handleExceptions( - (error, stackTrace) { - loggy.error("error watching outbounds", error, stackTrace); - return CoreServiceFailure.unexpected(error, stackTrace); - }, - ); - } - - @override - TaskEither selectOutbound( - String groupTag, - String outboundTag, - ) { - return exceptionHandler( - () => singbox - .selectOutbound(groupTag, outboundTag) - .mapLeft(CoreServiceFailure.other) - .run(), - CoreServiceFailure.unexpected, - ); - } - - @override - TaskEither urlTest(String groupTag) { - return exceptionHandler( - () => singbox.urlTest(groupTag).mapLeft(CoreServiceFailure.other).run(), - CoreServiceFailure.unexpected, - ); - } - - @override - Stream> watchCoreStatus() { - return singbox.watchStats().map((event) { - final json = jsonDecode(event); - return CoreStatus.fromJson(json as Map); - }).handleExceptions( - (error, stackTrace) { - loggy.warning("error watching status", error, stackTrace); - return CoreServiceFailure.unexpected(error, stackTrace); - }, - ); - } - - @override - Stream watchConnectionStatus() => - singbox.watchConnectionStatus(); -} diff --git a/lib/data/repository/repository.dart b/lib/data/repository/repository.dart deleted file mode 100644 index a5687644..00000000 --- a/lib/data/repository/repository.dart +++ /dev/null @@ -1 +0,0 @@ -export 'core_facade_impl.dart'; diff --git a/lib/domain/app/app.dart b/lib/domain/app/app.dart deleted file mode 100644 index 1853b271..00000000 --- a/lib/domain/app/app.dart +++ /dev/null @@ -1,3 +0,0 @@ -export 'app_failure.dart'; -export 'app_info.dart'; -export 'app_repository.dart'; diff --git a/lib/domain/app/app_failure.dart b/lib/domain/app/app_failure.dart deleted file mode 100644 index b340cbbb..00000000 --- a/lib/domain/app/app_failure.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:hiddify/core/prefs/prefs.dart'; -import 'package:hiddify/domain/failures.dart'; - -part 'app_failure.freezed.dart'; - -@freezed -sealed class AppFailure with _$AppFailure, Failure { - const AppFailure._(); - - @With() - const factory AppFailure.unexpected([ - Object? error, - StackTrace? stackTrace, - ]) = UpdateUnexpectedFailure; - - @override - ({String type, String? message}) present(TranslationsEn t) { - return switch (this) { - UpdateUnexpectedFailure() => (type: t.failure.unexpected, message: null), - }; - } -} diff --git a/lib/domain/app/app_info.dart b/lib/domain/app/app_info.dart deleted file mode 100644 index 51d09283..00000000 --- a/lib/domain/app/app_info.dart +++ /dev/null @@ -1,87 +0,0 @@ -import 'package:dartx/dartx.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:hiddify/domain/environment.dart'; - -part 'app_info.freezed.dart'; -part 'app_info.g.dart'; - -@freezed -class AppInfo with _$AppInfo { - const AppInfo._(); - - const factory AppInfo({ - required String name, - required String version, - required String buildNumber, - required Release release, - required String operatingSystem, - required String operatingSystemVersion, - required Environment environment, - }) = _AppInfo; - - String get userAgent => "HiddifyNext/$version ($operatingSystem) like ClashMeta v2ray sing-box"; - - String get presentVersion => environment == Environment.prod - ? version - : "$version ${environment.name}"; - - /// formats app info for sharing - String format() => ''' -$name v$version ($buildNumber) [${environment.name}] -${release.name} release -$operatingSystem [$operatingSystemVersion]'''; - - factory AppInfo.fromJson(Map json) => - _$AppInfoFromJson(json); -} - -// TODO ignore drafts -@Freezed() -class RemoteVersionInfo with _$RemoteVersionInfo { - const RemoteVersionInfo._(); - - const factory RemoteVersionInfo({ - required String version, - required String buildNumber, - required String releaseTag, - required bool preRelease, - required String url, - required DateTime publishedAt, - required Environment flavor, - }) = _RemoteVersionInfo; - - String get presentVersion => - flavor == Environment.prod ? version : "$version ${flavor.name}"; - - // ignore: prefer_constructors_over_static_methods - static RemoteVersionInfo fromJson(Map json) { - final fullTag = json['tag_name'] as String; - final fullVersion = fullTag.removePrefix("v").split("-").first.split("+"); - var version = fullVersion.first; - var buildNumber = fullVersion.elementAtOrElse(1, (index) => ""); - var flavor = Environment.prod; - for (final env in Environment.values) { - final suffix = ".${env.name}"; - if (version.endsWith(suffix)) { - version = version.removeSuffix(suffix); - flavor = env; - break; - } else if (buildNumber.endsWith(suffix)) { - buildNumber = buildNumber.removeSuffix(suffix); - flavor = env; - break; - } - } - final preRelease = json["prerelease"] as bool; - final publishedAt = DateTime.parse(json["published_at"] as String); - return RemoteVersionInfo( - version: version, - buildNumber: buildNumber, - releaseTag: fullTag, - preRelease: preRelease, - url: json["html_url"] as String, - publishedAt: publishedAt, - flavor: flavor, - ); - } -} diff --git a/lib/domain/app/app_repository.dart b/lib/domain/app/app_repository.dart deleted file mode 100644 index e51bcb35..00000000 --- a/lib/domain/app/app_repository.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:fpdart/fpdart.dart'; -import 'package:hiddify/domain/app/app_failure.dart'; -import 'package:hiddify/domain/app/app_info.dart'; - -abstract interface class AppRepository { - TaskEither getLatestVersion({ - bool includePreReleases = false, - }); -} diff --git a/lib/domain/connectivity/connectivity.dart b/lib/domain/connectivity/connectivity.dart deleted file mode 100644 index 8f2bb7cc..00000000 --- a/lib/domain/connectivity/connectivity.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'connection_failure.dart'; -export 'connection_status.dart'; diff --git a/lib/domain/core_facade.dart b/lib/domain/core_facade.dart deleted file mode 100644 index 15d0ba48..00000000 --- a/lib/domain/core_facade.dart +++ /dev/null @@ -1,3 +0,0 @@ -import 'package:hiddify/domain/singbox/singbox.dart'; - -abstract interface class CoreFacade implements SingboxFacade {} diff --git a/lib/domain/core_service_failure.dart b/lib/domain/core_service_failure.dart deleted file mode 100644 index 87ab9830..00000000 --- a/lib/domain/core_service_failure.dart +++ /dev/null @@ -1,101 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:hiddify/core/prefs/prefs.dart'; -import 'package:hiddify/domain/failures.dart'; - -part 'core_service_failure.freezed.dart'; - -@freezed -sealed class CoreServiceFailure with _$CoreServiceFailure, Failure { - const CoreServiceFailure._(); - - @With() - const factory CoreServiceFailure.unexpected( - Object? error, - StackTrace? stackTrace, - ) = UnexpectedCoreServiceFailure; - - @With() - const factory CoreServiceFailure.serviceNotRunning([String? message]) = - CoreServiceNotRunning; - - @With() - const factory CoreServiceFailure.missingPrivilege() = CoreMissingPrivilege; - - @With() - const factory CoreServiceFailure.missingGeoAssets() = CoreMissingGeoAssets; - - const factory CoreServiceFailure.invalidConfigOptions([ - String? message, - ]) = InvalidConfigOptions; - - @With() - const factory CoreServiceFailure.invalidConfig([ - String? message, - ]) = InvalidConfig; - - const factory CoreServiceFailure.create([ - String? message, - ]) = CoreServiceCreateFailure; - - const factory CoreServiceFailure.start([ - String? message, - ]) = CoreServiceStartFailure; - - const factory CoreServiceFailure.other([ - String? message, - ]) = CoreServiceOtherFailure; - - String? get msg => switch (this) { - UnexpectedCoreServiceFailure() => null, - CoreServiceNotRunning(:final message) => message, - CoreMissingPrivilege() => null, - CoreMissingGeoAssets() => null, - InvalidConfigOptions(:final message) => message, - InvalidConfig(:final message) => message, - CoreServiceCreateFailure(:final message) => message, - CoreServiceStartFailure(:final message) => message, - CoreServiceOtherFailure(:final message) => message, - }; - - @override - ({String type, String? message}) present(TranslationsEn t) { - return switch (this) { - UnexpectedCoreServiceFailure() => ( - type: t.failure.singbox.unexpected, - message: null, - ), - CoreServiceNotRunning(:final message) => ( - type: t.failure.singbox.serviceNotRunning, - message: message - ), - CoreMissingPrivilege() => ( - type: t.failure.singbox.missingPrivilege, - message: t.failure.singbox.missingPrivilegeMsg, - ), - CoreMissingGeoAssets() => ( - type: t.failure.singbox.missingGeoAssets, - message: t.failure.singbox.missingGeoAssetsMsg, - ), - InvalidConfigOptions(:final message) => ( - type: t.failure.singbox.invalidConfigOptions, - message: message - ), - InvalidConfig(:final message) => ( - type: t.failure.singbox.invalidConfig, - message: message - ), - CoreServiceCreateFailure(:final message) => ( - type: t.failure.singbox.create, - message: message - ), - CoreServiceStartFailure(:final message) => ( - type: t.failure.singbox.start, - message: message - ), - CoreServiceOtherFailure(:final message) => ( - type: t.failure.singbox.unexpected, - message: message - ), - }; - } -} diff --git a/lib/domain/enums.dart b/lib/domain/enums.dart deleted file mode 100644 index e5ce3ee4..00000000 --- a/lib/domain/enums.dart +++ /dev/null @@ -1 +0,0 @@ -enum SortMode { ascending, descending } diff --git a/lib/domain/singbox/config_options.dart b/lib/domain/singbox/config_options.dart deleted file mode 100644 index f350e79e..00000000 --- a/lib/domain/singbox/config_options.dart +++ /dev/null @@ -1,109 +0,0 @@ -import 'dart:convert'; - -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:hiddify/core/prefs/prefs.dart'; -import 'package:hiddify/domain/singbox/rules.dart'; -import 'package:hiddify/features/log/model/log_level.dart'; - -part 'config_options.freezed.dart'; -part 'config_options.g.dart'; - -@freezed -class ConfigOptions with _$ConfigOptions { - const ConfigOptions._(); - - @JsonSerializable(fieldRename: FieldRename.kebab) - const factory ConfigOptions({ - @Default(false) bool executeConfigAsIs, - @Default(LogLevel.warn) LogLevel logLevel, - @Default(false) bool resolveDestination, - @Default(IPv6Mode.disable) IPv6Mode ipv6Mode, - @Default("tcp://8.8.8.8") String remoteDnsAddress, - @Default(DomainStrategy.auto) DomainStrategy remoteDnsDomainStrategy, - @Default("8.8.8.8") String directDnsAddress, - @Default(DomainStrategy.auto) DomainStrategy directDnsDomainStrategy, - @Default(2334) int mixedPort, - @Default(6450) int localDnsPort, - @Default(TunImplementation.mixed) TunImplementation tunImplementation, - @Default(9000) int mtu, - @Default(true) bool strictRoute, - @Default("http://cp.cloudflare.com/") String connectionTestUrl, - @IntervalConverter() - @Default(Duration(minutes: 10)) - Duration urlTestInterval, - @Default(true) bool enableClashApi, - @Default(6756) int clashApiPort, - @Default(false) bool enableTun, - @Default(false) bool setSystemProxy, - @Default(false) bool bypassLan, - @Default(false) bool enableFakeDns, - @Default(true) bool independentDnsCache, - @Default("geoip.db") String geoipPath, - @Default("geosite.db") String geositePath, - List? rules, - }) = _ConfigOptions; - - static ConfigOptions initial = const ConfigOptions(); - - String format() { - const encoder = JsonEncoder.withIndent(' '); - return encoder.convert(toJson()); - } - - factory ConfigOptions.fromJson(Map json) => - _$ConfigOptionsFromJson(json); -} - -@JsonEnum(valueField: 'key') -enum IPv6Mode { - disable("ipv4_only"), - enable("prefer_ipv4"), - prefer("prefer_ipv6"), - only("ipv6_only"); - - const IPv6Mode(this.key); - - final String key; - - String present(TranslationsEn t) => switch (this) { - disable => t.settings.config.ipv6Modes.disable, - enable => t.settings.config.ipv6Modes.enable, - prefer => t.settings.config.ipv6Modes.prefer, - only => t.settings.config.ipv6Modes.only, - }; -} - -@JsonEnum(valueField: 'key') -enum DomainStrategy { - auto(""), - preferIpv6("prefer_ipv6"), - preferIpv4("prefer_ipv4"), - ipv4Only("ipv4_only"), - ipv6Only("ipv6_only"); - - const DomainStrategy(this.key); - - final String key; - - String get displayName => switch (this) { - auto => "auto", - _ => key, - }; -} - -enum TunImplementation { - mixed, - system, - gVisor; -} - -class IntervalConverter implements JsonConverter { - const IntervalConverter(); - - @override - Duration fromJson(String json) => - Duration(minutes: int.parse(json.replaceAll("m", ""))); - - @override - String toJson(Duration object) => "${object.inMinutes}m"; -} diff --git a/lib/domain/singbox/core_status.dart b/lib/domain/singbox/core_status.dart deleted file mode 100644 index 979e57df..00000000 --- a/lib/domain/singbox/core_status.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'core_status.freezed.dart'; -part 'core_status.g.dart'; - -@freezed -class CoreStatus with _$CoreStatus { - const CoreStatus._(); - - @JsonSerializable(fieldRename: FieldRename.kebab) - const factory CoreStatus({ - required int connectionsIn, - required int connectionsOut, - required int uplink, - required int downlink, - required int uplinkTotal, - required int downlinkTotal, - }) = _CoreStatus; - - factory CoreStatus.empty() => const CoreStatus( - connectionsIn: 0, - connectionsOut: 0, - uplink: 0, - downlink: 0, - uplinkTotal: 0, - downlinkTotal: 0, - ); - - factory CoreStatus.fromJson(Map json) => - _$CoreStatusFromJson(json); -} diff --git a/lib/domain/singbox/outbounds.dart b/lib/domain/singbox/outbounds.dart deleted file mode 100644 index e99d0b11..00000000 --- a/lib/domain/singbox/outbounds.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:dartx/dartx.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:hiddify/domain/singbox/proxy_type.dart'; - -part 'outbounds.freezed.dart'; -part 'outbounds.g.dart'; - -@freezed -class OutboundGroup with _$OutboundGroup { - @JsonSerializable(fieldRename: FieldRename.kebab) - const factory OutboundGroup({ - required String tag, - @JsonKey(fromJson: _typeFromJson) required ProxyType type, - required String selected, - @Default([]) List items, - }) = _OutboundGroup; - - factory OutboundGroup.fromJson(Map json) => - _$OutboundGroupFromJson(json); -} - -@freezed -class OutboundGroupItem with _$OutboundGroupItem { - const OutboundGroupItem._(); - - @JsonSerializable(fieldRename: FieldRename.kebab) - const factory OutboundGroupItem({ - required String tag, - @JsonKey(fromJson: _typeFromJson) required ProxyType type, - required int urlTestDelay, - String? selectedTag, - }) = _OutboundGroupItem; - - factory OutboundGroupItem.fromJson(Map json) => - _$OutboundGroupItemFromJson(json); -} - -ProxyType _typeFromJson(dynamic type) => - ProxyType.values - .firstOrNullWhere((e) => e.key == (type as String?)?.toLowerCase()) ?? - ProxyType.unknown; - -String sanitizedTag(String tag) => - tag.replaceFirst(RegExp(r"\§[^]*"), "").trimRight(); diff --git a/lib/domain/singbox/rules.dart b/lib/domain/singbox/rules.dart deleted file mode 100644 index 686474d2..00000000 --- a/lib/domain/singbox/rules.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:hiddify/core/prefs/locale_prefs.dart'; - -part 'rules.freezed.dart'; -part 'rules.g.dart'; - -@freezed -class Rule with _$Rule { - @JsonSerializable(fieldRename: FieldRename.kebab) - const factory Rule({ - @JsonKey(includeToJson: false) required String id, - @JsonKey(includeToJson: false) required String name, - @JsonKey(includeToJson: false) @Default(false) bool enabled, - String? domains, - String? ip, - String? port, - String? protocol, - @Default(RuleNetwork.tcpAndUdp) RuleNetwork network, - @Default(RuleOutbound.proxy) RuleOutbound outbound, - }) = _Rule; - - factory Rule.fromJson(Map json) => _$RuleFromJson(json); -} - -enum RuleOutbound { proxy, bypass, block } - -@JsonEnum(valueField: 'key') -enum RuleNetwork { - tcpAndUdp(""), - tcp("tcp"), - udp("udp"); - - const RuleNetwork(this.key); - - final String? key; -} - -enum PerAppProxyMode { - off, - include, - exclude; - - bool get enabled => this != off; - - ({String title, String message}) present(TranslationsEn t) => switch (this) { - off => ( - title: t.settings.network.perAppProxyModes.off, - message: t.settings.network.perAppProxyModes.offMsg, - ), - include => ( - title: t.settings.network.perAppProxyModes.include, - message: t.settings.network.perAppProxyModes.includeMsg, - ), - exclude => ( - title: t.settings.network.perAppProxyModes.exclude, - message: t.settings.network.perAppProxyModes.excludeMsg, - ), - }; -} - -enum Region { - ir, - cn, - ru, - other; - - String present(TranslationsEn t) => switch (this) { - ir => t.settings.general.regions.ir, - cn => t.settings.general.regions.cn, - ru => t.settings.general.regions.ru, - other => t.settings.general.regions.other, - }; -} diff --git a/lib/domain/singbox/service_mode.dart b/lib/domain/singbox/service_mode.dart deleted file mode 100644 index 34481e2d..00000000 --- a/lib/domain/singbox/service_mode.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:hiddify/core/prefs/locale_prefs.dart'; -import 'package:hiddify/utils/platform_utils.dart'; - -enum ServiceMode { - proxy, - systemProxy, - tun; - - static ServiceMode get defaultMode => - PlatformUtils.isDesktop ? systemProxy : tun; - - static List get choices { - if (PlatformUtils.isDesktop) { - return values; - } - return [proxy, tun]; - } - - String present(TranslationsEn t) => switch (this) { - proxy => t.settings.config.serviceModes.proxy, - systemProxy => t.settings.config.serviceModes.systemProxy, - tun => t.settings.config.serviceModes.tun, - }; -} diff --git a/lib/domain/singbox/singbox.dart b/lib/domain/singbox/singbox.dart deleted file mode 100644 index 441637cd..00000000 --- a/lib/domain/singbox/singbox.dart +++ /dev/null @@ -1,7 +0,0 @@ -export 'config_options.dart'; -export 'core_status.dart'; -export 'outbounds.dart'; -export 'proxy_type.dart'; -export 'rules.dart'; -export 'service_mode.dart'; -export 'singbox_facade.dart'; diff --git a/lib/domain/singbox/singbox_facade.dart b/lib/domain/singbox/singbox_facade.dart deleted file mode 100644 index 4231afb0..00000000 --- a/lib/domain/singbox/singbox_facade.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:fpdart/fpdart.dart'; -import 'package:hiddify/domain/connectivity/connectivity.dart'; -import 'package:hiddify/domain/core_service_failure.dart'; -import 'package:hiddify/domain/singbox/config_options.dart'; -import 'package:hiddify/domain/singbox/core_status.dart'; -import 'package:hiddify/domain/singbox/outbounds.dart'; - -abstract interface class SingboxFacade { - TaskEither setup(); - - TaskEither parseConfig( - String path, - String tempPath, - bool debug, - ); - - TaskEither changeConfigOptions( - ConfigOptions options, - ); - - TaskEither generateConfig( - String fileName, - ); - - TaskEither start( - String fileName, - bool disableMemoryLimit, - ); - - TaskEither stop(); - - TaskEither restart( - String fileName, - bool disableMemoryLimit, - ); - - Stream>> watchOutbounds(); - - TaskEither selectOutbound( - String groupTag, - String outboundTag, - ); - - TaskEither urlTest(String groupTag); - - Stream watchConnectionStatus(); - - Stream> watchCoreStatus(); -} diff --git a/lib/features/about/view/view.dart b/lib/features/about/view/view.dart deleted file mode 100644 index 8f120a21..00000000 --- a/lib/features/about/view/view.dart +++ /dev/null @@ -1 +0,0 @@ -export 'about_page.dart'; diff --git a/lib/core/app/app_view.dart b/lib/features/app/widget/app.dart similarity index 65% rename from lib/core/app/app_view.dart rename to lib/features/app/widget/app.dart index f4e11bd3..0ab58bbe 100644 --- a/lib/core/app/app_view.dart +++ b/lib/features/app/widget/app.dart @@ -2,11 +2,14 @@ import 'package:accessibility_tools/accessibility_tools.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:hiddify/core/core_providers.dart'; -import 'package:hiddify/core/prefs/prefs.dart'; +import 'package:hiddify/core/localization/locale_extensions.dart'; +import 'package:hiddify/core/localization/locale_preferences.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/model/constants.dart'; import 'package:hiddify/core/router/router.dart'; -import 'package:hiddify/domain/constants.dart'; -import 'package:hiddify/features/common/app_update_notifier.dart'; +import 'package:hiddify/core/theme/app_theme.dart'; +import 'package:hiddify/core/theme/theme_preferences.dart'; +import 'package:hiddify/features/app_update/notifier/app_update_notifier.dart'; import 'package:hiddify/features/common/common_controllers.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -14,14 +17,15 @@ import 'package:upgrader/upgrader.dart'; bool _debugAccessibility = false; -class AppView extends HookConsumerWidget with PresLogger { - const AppView({super.key}); +class App extends HookConsumerWidget with PresLogger { + const App({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final router = ref.watch(routerProvider); - final locale = ref.watch(localeNotifierProvider).flutterLocale; - final theme = ref.watch(themeProvider); + final locale = ref.watch(localePreferencesProvider); + final themeMode = ref.watch(themePreferencesProvider); + final theme = AppTheme(themeMode, locale.preferredFontFamily); ref.watch(commonControllersProvider); @@ -29,11 +33,11 @@ class AppView extends HookConsumerWidget with PresLogger { return MaterialApp.router( routerConfig: router, - locale: locale, + locale: locale.flutterLocale, supportedLocales: AppLocaleUtils.supportedLocales, localizationsDelegates: GlobalMaterialLocalizations.delegates, debugShowCheckedModeBanner: false, - themeMode: theme.mode.flutterThemeMode, + themeMode: themeMode.flutterThemeMode, theme: theme.light(), darkTheme: theme.dark(), title: Constants.appName, diff --git a/lib/features/app_update/data/app_update_data_providers.dart b/lib/features/app_update/data/app_update_data_providers.dart new file mode 100644 index 00000000..834c4c10 --- /dev/null +++ b/lib/features/app_update/data/app_update_data_providers.dart @@ -0,0 +1,12 @@ +import 'package:hiddify/core/http_client/http_client_provider.dart'; +import 'package:hiddify/features/app_update/data/app_update_repository.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'app_update_data_providers.g.dart'; + +@Riverpod(keepAlive: true) +AppUpdateRepository appUpdateRepository( + AppUpdateRepositoryRef ref, +) { + return AppUpdateRepositoryImpl(dio: ref.watch(httpClientProvider)); +} diff --git a/lib/features/app_update/data/app_update_repository.dart b/lib/features/app_update/data/app_update_repository.dart new file mode 100644 index 00000000..6242b271 --- /dev/null +++ b/lib/features/app_update/data/app_update_repository.dart @@ -0,0 +1,55 @@ +import 'package:dio/dio.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:hiddify/core/model/constants.dart'; +import 'package:hiddify/core/model/environment.dart'; +import 'package:hiddify/core/utils/exception_handler.dart'; +import 'package:hiddify/features/app_update/data/github_release_parser.dart'; +import 'package:hiddify/features/app_update/model/app_update_failure.dart'; +import 'package:hiddify/features/app_update/model/remote_version_entity.dart'; +import 'package:hiddify/utils/utils.dart'; + +abstract interface class AppUpdateRepository { + TaskEither getLatestVersion({ + bool includePreReleases = false, + Release release = Release.general, + }); +} + +class AppUpdateRepositoryImpl + with ExceptionHandler, InfraLogger + implements AppUpdateRepository { + AppUpdateRepositoryImpl({required this.dio}); + + final Dio dio; + + @override + TaskEither getLatestVersion({ + bool includePreReleases = false, + Release release = Release.general, + }) { + return exceptionHandler( + () async { + if (!release.allowCustomUpdateChecker) { + throw Exception("custom update checkers are not supported"); + } + final response = await dio.get(Constants.githubReleasesApiUrl); + if (response.statusCode != 200 || response.data == null) { + loggy.warning("failed to fetch latest version info"); + return left(const AppUpdateFailure.unexpected()); + } + + final releases = response.data!.map( + (e) => GithubReleaseParser.parse(e as Map), + ); + late RemoteVersionEntity latest; + if (includePreReleases) { + latest = releases.first; + } else { + latest = releases.firstWhere((e) => e.preRelease == false); + } + return right(latest); + }, + AppUpdateFailure.unexpected, + ); + } +} diff --git a/lib/features/app_update/data/github_release_parser.dart b/lib/features/app_update/data/github_release_parser.dart new file mode 100644 index 00000000..2dd07d9d --- /dev/null +++ b/lib/features/app_update/data/github_release_parser.dart @@ -0,0 +1,36 @@ +import 'package:dartx/dartx.dart'; +import 'package:hiddify/core/model/environment.dart'; +import 'package:hiddify/features/app_update/model/remote_version_entity.dart'; + +abstract class GithubReleaseParser { + static RemoteVersionEntity parse(Map json) { + final fullTag = json['tag_name'] as String; + final fullVersion = fullTag.removePrefix("v").split("-").first.split("+"); + var version = fullVersion.first; + var buildNumber = fullVersion.elementAtOrElse(1, (index) => ""); + var flavor = Environment.prod; + for (final env in Environment.values) { + final suffix = ".${env.name}"; + if (version.endsWith(suffix)) { + version = version.removeSuffix(suffix); + flavor = env; + break; + } else if (buildNumber.endsWith(suffix)) { + buildNumber = buildNumber.removeSuffix(suffix); + flavor = env; + break; + } + } + final preRelease = json["prerelease"] as bool; + final publishedAt = DateTime.parse(json["published_at"] as String); + return RemoteVersionEntity( + version: version, + buildNumber: buildNumber, + releaseTag: fullTag, + preRelease: preRelease, + url: json["html_url"] as String, + publishedAt: publishedAt, + flavor: flavor, + ); + } +} diff --git a/lib/features/app_update/model/app_update_failure.dart b/lib/features/app_update/model/app_update_failure.dart new file mode 100644 index 00000000..f83f3382 --- /dev/null +++ b/lib/features/app_update/model/app_update_failure.dart @@ -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 'app_update_failure.freezed.dart'; + +@freezed +sealed class AppUpdateFailure with _$AppUpdateFailure, Failure { + const AppUpdateFailure._(); + + @With() + const factory AppUpdateFailure.unexpected([ + Object? error, + StackTrace? stackTrace, + ]) = AppUpdateUnexpectedFailure; + + @override + ({String type, String? message}) present(TranslationsEn t) { + return switch (this) { + AppUpdateUnexpectedFailure() => ( + type: t.failure.unexpected, + message: null, + ), + }; + } +} diff --git a/lib/features/app_update/model/remote_version_entity.dart b/lib/features/app_update/model/remote_version_entity.dart new file mode 100644 index 00000000..c57434d1 --- /dev/null +++ b/lib/features/app_update/model/remote_version_entity.dart @@ -0,0 +1,22 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hiddify/core/model/environment.dart'; + +part 'remote_version_entity.freezed.dart'; + +@Freezed() +class RemoteVersionEntity with _$RemoteVersionEntity { + const RemoteVersionEntity._(); + + const factory RemoteVersionEntity({ + required String version, + required String buildNumber, + required String releaseTag, + required bool preRelease, + required String url, + required DateTime publishedAt, + required Environment flavor, + }) = _RemoteVersionEntity; + + String get presentVersion => + flavor == Environment.prod ? version : "$version ${flavor.name}"; +} diff --git a/lib/features/common/app_update_notifier.dart b/lib/features/app_update/notifier/app_update_notifier.dart similarity index 58% rename from lib/features/common/app_update_notifier.dart rename to lib/features/app_update/notifier/app_update_notifier.dart index a3a39092..357b95bb 100644 --- a/lib/features/common/app_update_notifier.dart +++ b/lib/features/app_update/notifier/app_update_notifier.dart @@ -1,17 +1,18 @@ import 'package:flutter/foundation.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:hiddify/core/core_providers.dart'; -import 'package:hiddify/core/prefs/prefs.dart'; -import 'package:hiddify/data/data_providers.dart'; -import 'package:hiddify/domain/app/app.dart'; -import 'package:hiddify/domain/constants.dart'; +import 'package:hiddify/core/app_info/app_info_provider.dart'; +import 'package:hiddify/core/localization/locale_preferences.dart'; +import 'package:hiddify/core/model/constants.dart'; +import 'package:hiddify/core/preferences/preferences_provider.dart'; +import 'package:hiddify/features/app_update/data/app_update_data_providers.dart'; +import 'package:hiddify/features/app_update/model/app_update_failure.dart'; +import 'package:hiddify/features/app_update/model/remote_version_entity.dart'; +import 'package:hiddify/features/app_update/notifier/app_update_state.dart'; import 'package:hiddify/utils/pref_notifier.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:upgrader/upgrader.dart'; import 'package:version/version.dart'; -part 'app_update_notifier.freezed.dart'; part 'app_update_notifier.g.dart'; const _debugUpgrader = true; @@ -22,33 +23,17 @@ Upgrader upgrader(UpgraderRef ref) => Upgrader( debugLogging: _debugUpgrader && kDebugMode, durationUntilAlertAgain: const Duration(hours: 12), messages: UpgraderMessages( - code: ref.watch(localeNotifierProvider).languageCode, + code: ref.watch(localePreferencesProvider).languageCode, ), ); -@freezed -class AppUpdateState with _$AppUpdateState { - const factory AppUpdateState.initial() = AppUpdateStateInitial; - const factory AppUpdateState.disabled() = AppUpdateStateDisabled; - const factory AppUpdateState.checking() = AppUpdateStateChecking; - const factory AppUpdateState.error(AppFailure error) = AppUpdateStateError; - const factory AppUpdateState.available(RemoteVersionInfo versionInfo) = - AppUpdateStateAvailable; - const factory AppUpdateState.ignored(RemoteVersionInfo versionInfo) = - AppUpdateStateIgnored; - const factory AppUpdateState.notAvailable() = AppUpdateStateNotAvailable; -} - @Riverpod(keepAlive: true) class AppUpdateNotifier extends _$AppUpdateNotifier with AppLogger { @override - AppUpdateState build() { - // _schedule(); - return const AppUpdateState.initial(); - } + AppUpdateState build() => const AppUpdateState.initial(); Pref get _ignoreReleasePref => Pref( - ref.read(sharedPreferencesProvider), + ref.read(sharedPreferencesProvider).requireValue, 'ignored_release_version', null, ); @@ -56,15 +41,14 @@ class AppUpdateNotifier extends _$AppUpdateNotifier with AppLogger { Future check() async { loggy.debug("checking for update"); state = const AppUpdateState.checking(); - final appInfo = ref.watch(appInfoProvider); - // TODO use market-specific update checkers + final appInfo = ref.watch(appInfoProvider).requireValue; if (!appInfo.release.allowCustomUpdateChecker) { loggy.debug( "custom update checkers are not allowed for [${appInfo.release.name}] release", ); return state = const AppUpdateState.disabled(); } - return ref.watch(appRepositoryProvider).getLatestVersion().match( + return ref.watch(appUpdateRepositoryProvider).getLatestVersion().match( (err) { loggy.warning("failed to get latest version", err); return state = AppUpdateState.error(err); @@ -88,16 +72,16 @@ class AppUpdateNotifier extends _$AppUpdateNotifier with AppLogger { } catch (error, stackTrace) { loggy.warning("error parsing versions", error, stackTrace); return state = AppUpdateState.error( - AppFailure.unexpected(error, stackTrace), + AppUpdateFailure.unexpected(error, stackTrace), ); } }, ).run(); } - Future ignoreRelease(RemoteVersionInfo versionInfo) async { - loggy.debug("ignoring release [${versionInfo.version}]"); - await _ignoreReleasePref.update(versionInfo.version); - state = AppUpdateStateIgnored(versionInfo); + Future ignoreRelease(RemoteVersionEntity version) async { + loggy.debug("ignoring release [${version.version}]"); + await _ignoreReleasePref.update(version.version); + state = AppUpdateStateIgnored(version); } } diff --git a/lib/features/app_update/notifier/app_update_state.dart b/lib/features/app_update/notifier/app_update_state.dart new file mode 100644 index 00000000..fe00d9ec --- /dev/null +++ b/lib/features/app_update/notifier/app_update_state.dart @@ -0,0 +1,19 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hiddify/features/app_update/model/app_update_failure.dart'; +import 'package:hiddify/features/app_update/model/remote_version_entity.dart'; + +part 'app_update_state.freezed.dart'; + +@freezed +class AppUpdateState with _$AppUpdateState { + const factory AppUpdateState.initial() = AppUpdateStateInitial; + const factory AppUpdateState.disabled() = AppUpdateStateDisabled; + const factory AppUpdateState.checking() = AppUpdateStateChecking; + const factory AppUpdateState.error(AppUpdateFailure error) = + AppUpdateStateError; + const factory AppUpdateState.available(RemoteVersionEntity versionInfo) = + AppUpdateStateAvailable; + const factory AppUpdateState.ignored(RemoteVersionEntity versionInfo) = + AppUpdateStateIgnored; + const factory AppUpdateState.notAvailable() = AppUpdateStateNotAvailable; +} diff --git a/lib/features/common/new_version_dialog.dart b/lib/features/app_update/widget/new_version_dialog.dart similarity index 91% rename from lib/features/common/new_version_dialog.dart rename to lib/features/app_update/widget/new_version_dialog.dart index 197b32c4..e636fd8c 100644 --- a/lib/features/common/new_version_dialog.dart +++ b/lib/features/app_update/widget/new_version_dialog.dart @@ -1,13 +1,12 @@ import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; -import 'package:hiddify/core/core_providers.dart'; -import 'package:hiddify/domain/app/app.dart'; -import 'package:hiddify/features/common/app_update_notifier.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/features/app_update/model/remote_version_entity.dart'; +import 'package:hiddify/features/app_update/notifier/app_update_notifier.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -// TODO add release notes class NewVersionDialog extends HookConsumerWidget with PresLogger { NewVersionDialog( this.currentVersion, @@ -16,7 +15,7 @@ class NewVersionDialog extends HookConsumerWidget with PresLogger { }) : super(key: _dialogKey); final String currentVersion; - final RemoteVersionInfo newVersion; + final RemoteVersionEntity newVersion; final bool canIgnore; static final _dialogKey = GlobalKey(debugLabel: 'new version dialog'); diff --git a/lib/features/common/adaptive_root_scaffold.dart b/lib/features/common/adaptive_root_scaffold.dart index 5dea563b..91d15336 100644 --- a/lib/features/common/adaptive_root_scaffold.dart +++ b/lib/features/common/adaptive_root_scaffold.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart'; -import 'package:hiddify/core/core_providers.dart'; +import 'package:hiddify/core/localization/translations.dart'; import 'package:hiddify/core/router/router.dart'; -import 'package:hiddify/features/common/side_bar_stats_overview.dart'; +import 'package:hiddify/features/stats/widget/side_bar_stats_overview.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; abstract interface class RootScaffold { diff --git a/lib/features/common/common_controllers.dart b/lib/features/common/common_controllers.dart index a8172a49..b0e63882 100644 --- a/lib/features/common/common_controllers.dart +++ b/lib/features/common/common_controllers.dart @@ -1,6 +1,6 @@ -import 'package:hiddify/core/prefs/general_prefs.dart'; -import 'package:hiddify/features/common/connectivity/connectivity_controller.dart'; +import 'package:hiddify/core/preferences/general_preferences.dart'; import 'package:hiddify/features/common/window/window_controller.dart'; +import 'package:hiddify/features/connection/notifier/connection_notifier.dart'; import 'package:hiddify/features/profile/notifier/profiles_update_notifier.dart'; import 'package:hiddify/features/system_tray/system_tray_controller.dart'; import 'package:hiddify/utils/platform_utils.dart'; @@ -22,7 +22,7 @@ void commonControllers(CommonControllersRef ref) { fireImmediately: true, ); ref.listen( - connectivityControllerProvider, + connectionNotifierProvider, (previous, next) {}, fireImmediately: true, ); diff --git a/lib/features/common/general_pref_tiles.dart b/lib/features/common/general_pref_tiles.dart index 69a354cb..1ac1c373 100644 --- a/lib/features/common/general_pref_tiles.dart +++ b/lib/features/common/general_pref_tiles.dart @@ -1,8 +1,10 @@ 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/domain/singbox/singbox.dart'; +import 'package:hiddify/core/localization/locale_extensions.dart'; +import 'package:hiddify/core/localization/locale_preferences.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/model/region.dart'; +import 'package:hiddify/core/preferences/general_preferences.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; class LocalePrefTile extends HookConsumerWidget { @@ -12,7 +14,7 @@ class LocalePrefTile extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final t = ref.watch(translationsProvider); - final locale = ref.watch(localeNotifierProvider); + final locale = ref.watch(localePreferencesProvider); return ListTile( title: Text(t.settings.general.locale), @@ -39,8 +41,8 @@ class LocalePrefTile extends HookConsumerWidget { ); if (selectedLocale != null) { await ref - .read(localeNotifierProvider.notifier) - .update(selectedLocale); + .read(localePreferencesProvider.notifier) + .changeLocale(selectedLocale); } }, ); diff --git a/lib/features/common/qr_code_scanner_screen.dart b/lib/features/common/qr_code_scanner_screen.dart index 14ef3071..152df8a0 100644 --- a/lib/features/common/qr_code_scanner_screen.dart +++ b/lib/features/common/qr_code_scanner_screen.dart @@ -1,7 +1,7 @@ import 'package:dartx/dartx.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.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'; import 'package:mobile_scanner/mobile_scanner.dart'; diff --git a/lib/features/common/stats_provider.dart b/lib/features/common/stats_provider.dart deleted file mode 100644 index 5ddbd43b..00000000 --- a/lib/features/common/stats_provider.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:hiddify/data/data_providers.dart'; -import 'package:hiddify/domain/singbox/singbox.dart'; -import 'package:hiddify/features/common/connectivity/connectivity_controller.dart'; -import 'package:hiddify/utils/utils.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'stats_provider.g.dart'; - -@riverpod -class Stats extends _$Stats with AppLogger { - @override - Stream build() async* { - final serviceRunning = await ref.watch(serviceRunningProvider.future); - if (serviceRunning) { - yield* ref - .watch(coreFacadeProvider) - .watchCoreStatus() - .map((event) => event.getOrElse((_) => CoreStatus.empty())); - } else { - yield* Stream.value(CoreStatus.empty()); - } - } -} diff --git a/lib/features/common/window/window_controller.dart b/lib/features/common/window/window_controller.dart index 7b4d09de..bc8884dd 100644 --- a/lib/features/common/window/window_controller.dart +++ b/lib/features/common/window/window_controller.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:hiddify/core/prefs/prefs.dart'; -import 'package:hiddify/core/prefs/service_prefs.dart'; -import 'package:hiddify/features/common/connectivity/connectivity_controller.dart'; +import 'package:hiddify/core/preferences/general_preferences.dart'; +import 'package:hiddify/core/preferences/service_preferences.dart'; +import 'package:hiddify/features/connection/notifier/connection_notifier.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:window_manager/window_manager.dart'; @@ -16,10 +16,10 @@ class WindowController extends _$WindowController Future build() async { await windowManager.ensureInitialized(); const size = Size(868, 668); - const minumumSize = Size(368, 568); + const minimumSize = Size(368, 568); const windowOptions = WindowOptions( size: size, - minimumSize: minumumSize, + minimumSize: minimumSize, center: true, ); await windowManager.setPreventClose(true); @@ -35,9 +35,7 @@ class WindowController extends _$WindowController () async { if (ref.read(startedByUserProvider)) { loggy.debug("previously started by user, trying to connect"); - return ref - .read(connectivityControllerProvider.notifier) - .mayConnect(); + return ref.read(connectionNotifierProvider.notifier).mayConnect(); } }, ); diff --git a/lib/features/config_option/data/config_option_data_providers.dart b/lib/features/config_option/data/config_option_data_providers.dart new file mode 100644 index 00000000..c9b5f9b4 --- /dev/null +++ b/lib/features/config_option/data/config_option_data_providers.dart @@ -0,0 +1,17 @@ +import 'package:hiddify/core/preferences/preferences_provider.dart'; +import 'package:hiddify/features/config_option/data/config_option_repository.dart'; +import 'package:hiddify/features/geo_asset/data/geo_asset_data_providers.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'config_option_data_providers.g.dart'; + +@Riverpod(keepAlive: true) +ConfigOptionRepository configOptionRepository( + ConfigOptionRepositoryRef ref, +) { + return ConfigOptionRepositoryImpl( + preferences: ref.watch(sharedPreferencesProvider).requireValue, + geoAssetRepository: ref.watch(geoAssetRepositoryProvider).requireValue, + geoAssetPathResolver: ref.watch(geoAssetPathResolverProvider), + ); +} diff --git a/lib/features/config_option/data/config_option_repository.dart b/lib/features/config_option/data/config_option_repository.dart new file mode 100644 index 00000000..f302b1ce --- /dev/null +++ b/lib/features/config_option/data/config_option_repository.dart @@ -0,0 +1,172 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:hiddify/core/model/region.dart'; +import 'package:hiddify/core/utils/exception_handler.dart'; +import 'package:hiddify/features/config_option/model/config_option_entity.dart'; +import 'package:hiddify/features/config_option/model/config_option_failure.dart'; +import 'package:hiddify/features/config_option/model/config_option_patch.dart'; +import 'package:hiddify/features/geo_asset/data/geo_asset_path_resolver.dart'; +import 'package:hiddify/features/geo_asset/data/geo_asset_repository.dart'; +import 'package:hiddify/singbox/model/singbox_config_enum.dart'; +import 'package:hiddify/singbox/model/singbox_config_option.dart'; +import 'package:hiddify/singbox/model/singbox_rule.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +abstract interface class ConfigOptionRepository { + TaskEither + getFullSingboxConfigOption(); + TaskEither getConfigOption(); + TaskEither updateConfigOption( + ConfigOptionPatch patch, + ); +} + +class ConfigOptionRepositoryImpl + with ExceptionHandler, InfraLogger + implements ConfigOptionRepository { + ConfigOptionRepositoryImpl({ + required this.preferences, + required this.geoAssetRepository, + required this.geoAssetPathResolver, + }); + + final SharedPreferences preferences; + final GeoAssetRepository geoAssetRepository; + final GeoAssetPathResolver geoAssetPathResolver; + + @override + TaskEither + getFullSingboxConfigOption() { + return exceptionHandler( + () async { + final region = + Region.values.byName(preferences.getString("region") ?? "other"); + final rules = switch (region) { + Region.ir => [ + const SingboxRule( + domains: "domain:.ir,geosite:ir", + ip: "geoip:ir", + outbound: RuleOutbound.bypass, + ), + ], + Region.cn => [ + const SingboxRule( + domains: "domain:.cn,geosite:cn", + ip: "geoip:cn", + outbound: RuleOutbound.bypass, + ), + ], + Region.ru => [ + const SingboxRule( + domains: "domain:.ru", + ip: "geoip:ru", + outbound: RuleOutbound.bypass, + ), + ], + _ => [], + }; + + final geoAssets = await geoAssetRepository + .getActivePair() + .getOrElse((l) => throw l) + .run(); + + final persisted = + await getConfigOption().getOrElse((l) => throw l).run(); + final singboxConfigOption = SingboxConfigOption( + executeConfigAsIs: false, + logLevel: persisted.logLevel, + resolveDestination: persisted.resolveDestination, + ipv6Mode: persisted.ipv6Mode, + remoteDnsAddress: persisted.remoteDnsAddress, + remoteDnsDomainStrategy: persisted.remoteDnsDomainStrategy, + directDnsAddress: persisted.directDnsAddress, + directDnsDomainStrategy: persisted.directDnsDomainStrategy, + mixedPort: persisted.mixedPort, + localDnsPort: persisted.localDnsPort, + tunImplementation: persisted.tunImplementation, + mtu: persisted.mtu, + strictRoute: persisted.strictRoute, + connectionTestUrl: persisted.connectionTestUrl, + urlTestInterval: persisted.urlTestInterval, + enableClashApi: persisted.enableClashApi, + clashApiPort: persisted.clashApiPort, + enableTun: persisted.serviceMode == ServiceMode.tun, + setSystemProxy: persisted.serviceMode == ServiceMode.systemProxy, + bypassLan: persisted.bypassLan, + enableFakeDns: persisted.enableFakeDns, + independentDnsCache: persisted.independentDnsCache, + geoipPath: geoAssetPathResolver.relativePath( + geoAssets.geoip.providerName, + geoAssets.geoip.fileName, + ), + geositePath: geoAssetPathResolver.relativePath( + geoAssets.geosite.providerName, + geoAssets.geosite.fileName, + ), + rules: rules, + ); + return right(singboxConfigOption); + }, + ConfigOptionUnexpectedFailure.new, + ); + } + + @override + TaskEither getConfigOption() { + return exceptionHandler( + () async { + final map = ConfigOptionEntity.initial.toJson(); + for (final key in map.keys) { + final persisted = preferences.get(key); + if (persisted != null) { + final defaultValue = map[key]; + if (defaultValue != null && + persisted.runtimeType != defaultValue.runtimeType) { + loggy.warning( + "error getting preference[$key], expected type: [${defaultValue.runtimeType}] - received value: [$persisted](${persisted.runtimeType})", + ); + continue; + } + map[key] = persisted; + } + } + final options = ConfigOptionEntity.fromJson(map); + return right(options); + }, + ConfigOptionUnexpectedFailure.new, + ); + } + + @override + TaskEither updateConfigOption( + ConfigOptionPatch patch, + ) { + return exceptionHandler( + () async { + final map = patch.toJson(); + for (final key in map.keys) { + final value = map[key]; + if (value != null) { + loggy.debug("updating [$key] to [$value]"); + + switch (value) { + case bool _: + await preferences.setBool(key, value); + case String _: + await preferences.setString(key, value); + case int _: + await preferences.setInt(key, value); + case double _: + await preferences.setDouble(key, value); + default: + loggy.warning("unexpected type"); + } + } + } + return right(unit); + }, + ConfigOptionUnexpectedFailure.new, + ); + } +} diff --git a/lib/features/config_option/model/config_option_entity.dart b/lib/features/config_option/model/config_option_entity.dart new file mode 100644 index 00000000..9e95f62e --- /dev/null +++ b/lib/features/config_option/model/config_option_entity.dart @@ -0,0 +1,80 @@ +import 'dart:convert'; + +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hiddify/core/utils/json_converters.dart'; +import 'package:hiddify/features/config_option/model/config_option_patch.dart'; +import 'package:hiddify/features/log/model/log_level.dart'; +import 'package:hiddify/singbox/model/singbox_config_enum.dart'; + +part 'config_option_entity.freezed.dart'; +part 'config_option_entity.g.dart'; + +@freezed +class ConfigOptionEntity with _$ConfigOptionEntity { + const ConfigOptionEntity._(); + + @JsonSerializable(fieldRename: FieldRename.kebab) + const factory ConfigOptionEntity({ + required ServiceMode serviceMode, + @Default(LogLevel.warn) LogLevel logLevel, + @Default(false) bool resolveDestination, + @Default(IPv6Mode.disable) IPv6Mode ipv6Mode, + @Default("tcp://8.8.8.8") String remoteDnsAddress, + @Default(DomainStrategy.auto) DomainStrategy remoteDnsDomainStrategy, + @Default("8.8.8.8") String directDnsAddress, + @Default(DomainStrategy.auto) DomainStrategy directDnsDomainStrategy, + @Default(2334) int mixedPort, + @Default(6450) int localDnsPort, + @Default(TunImplementation.mixed) TunImplementation tunImplementation, + @Default(9000) int mtu, + @Default(true) bool strictRoute, + @Default("http://cp.cloudflare.com/") String connectionTestUrl, + @IntervalInSecondsConverter() + @Default(Duration(minutes: 10)) + Duration urlTestInterval, + @Default(true) bool enableClashApi, + @Default(6756) int clashApiPort, + @Default(false) bool bypassLan, + @Default(false) bool enableFakeDns, + @Default(true) bool independentDnsCache, + }) = _ConfigOptionEntity; + + static ConfigOptionEntity initial = ConfigOptionEntity( + serviceMode: ServiceMode.defaultMode, + ); + + String format() { + const encoder = JsonEncoder.withIndent(' '); + return encoder.convert(toJson()); + } + + ConfigOptionEntity patch(ConfigOptionPatch patch) { + return copyWith( + serviceMode: patch.serviceMode ?? serviceMode, + logLevel: patch.logLevel ?? logLevel, + resolveDestination: patch.resolveDestination ?? resolveDestination, + ipv6Mode: patch.ipv6Mode ?? ipv6Mode, + remoteDnsAddress: patch.remoteDnsAddress ?? remoteDnsAddress, + remoteDnsDomainStrategy: + patch.remoteDnsDomainStrategy ?? remoteDnsDomainStrategy, + directDnsAddress: patch.directDnsAddress ?? directDnsAddress, + directDnsDomainStrategy: + patch.directDnsDomainStrategy ?? directDnsDomainStrategy, + mixedPort: patch.mixedPort ?? mixedPort, + localDnsPort: patch.localDnsPort ?? localDnsPort, + tunImplementation: patch.tunImplementation ?? tunImplementation, + mtu: patch.mtu ?? mtu, + strictRoute: patch.strictRoute ?? strictRoute, + connectionTestUrl: patch.connectionTestUrl ?? connectionTestUrl, + urlTestInterval: patch.urlTestInterval ?? urlTestInterval, + enableClashApi: patch.enableClashApi ?? enableClashApi, + clashApiPort: patch.clashApiPort ?? clashApiPort, + bypassLan: patch.bypassLan ?? bypassLan, + enableFakeDns: patch.enableFakeDns ?? enableFakeDns, + independentDnsCache: patch.independentDnsCache ?? independentDnsCache, + ); + } + + factory ConfigOptionEntity.fromJson(Map json) => + _$ConfigOptionEntityFromJson(json); +} diff --git a/lib/features/config_option/model/config_option_failure.dart b/lib/features/config_option/model/config_option_failure.dart new file mode 100644 index 00000000..bc5c9ab5 --- /dev/null +++ b/lib/features/config_option/model/config_option_failure.dart @@ -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 'config_option_failure.freezed.dart'; + +@freezed +sealed class ConfigOptionFailure with _$ConfigOptionFailure, Failure { + const ConfigOptionFailure._(); + + @With() + const factory ConfigOptionFailure.unexpected([ + Object? error, + StackTrace? stackTrace, + ]) = ConfigOptionUnexpectedFailure; + + @override + ({String type, String? message}) present(TranslationsEn t) { + return switch (this) { + ConfigOptionUnexpectedFailure() => ( + type: t.failure.unexpected, + message: null, + ), + }; + } +} diff --git a/lib/features/config_option/model/config_option_patch.dart b/lib/features/config_option/model/config_option_patch.dart new file mode 100644 index 00000000..b19c1dc8 --- /dev/null +++ b/lib/features/config_option/model/config_option_patch.dart @@ -0,0 +1,39 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hiddify/core/utils/json_converters.dart'; +import 'package:hiddify/features/log/model/log_level.dart'; +import 'package:hiddify/singbox/model/singbox_config_enum.dart'; + +part 'config_option_patch.freezed.dart'; +part 'config_option_patch.g.dart'; + +@freezed +class ConfigOptionPatch with _$ConfigOptionPatch { + const ConfigOptionPatch._(); + + @JsonSerializable(fieldRename: FieldRename.kebab) + const factory ConfigOptionPatch({ + ServiceMode? serviceMode, + LogLevel? logLevel, + bool? resolveDestination, + IPv6Mode? ipv6Mode, + String? remoteDnsAddress, + DomainStrategy? remoteDnsDomainStrategy, + String? directDnsAddress, + DomainStrategy? directDnsDomainStrategy, + int? mixedPort, + int? localDnsPort, + TunImplementation? tunImplementation, + int? mtu, + bool? strictRoute, + String? connectionTestUrl, + @IntervalInSecondsConverter() Duration? urlTestInterval, + bool? enableClashApi, + int? clashApiPort, + bool? bypassLan, + bool? enableFakeDns, + bool? independentDnsCache, + }) = _ConfigOptionPatch; + + factory ConfigOptionPatch.fromJson(Map json) => + _$ConfigOptionPatchFromJson(json); +} diff --git a/lib/features/config_option/notifier/config_option_notifier.dart b/lib/features/config_option/notifier/config_option_notifier.dart new file mode 100644 index 00000000..fba59b9c --- /dev/null +++ b/lib/features/config_option/notifier/config_option_notifier.dart @@ -0,0 +1,31 @@ +import 'package:hiddify/features/config_option/data/config_option_data_providers.dart'; +import 'package:hiddify/features/config_option/model/config_option_entity.dart'; +import 'package:hiddify/features/config_option/model/config_option_patch.dart'; +import 'package:hiddify/utils/custom_loggers.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'config_option_notifier.g.dart'; + +@Riverpod(keepAlive: true) +class ConfigOptionNotifier extends _$ConfigOptionNotifier with AppLogger { + @override + Future build() { + return ref + .watch(configOptionRepositoryProvider) + .getConfigOption() + .getOrElse((l) { + loggy.error("error getting persisted options $l", l); + throw l; + }).run(); + } + + Future updateOption(ConfigOptionPatch patch) async { + if (state case AsyncData(value: final options)) { + await ref + .read(configOptionRepositoryProvider) + .updateConfigOption(patch) + .map((_) => state = AsyncData(options.patch(patch))) + .run(); + } + } +} diff --git a/lib/features/config_option/overview/config_options_page.dart b/lib/features/config_option/overview/config_options_page.dart new file mode 100644 index 00000000..db639f57 --- /dev/null +++ b/lib/features/config_option/overview/config_options_page.dart @@ -0,0 +1,323 @@ +import 'package:dartx/dartx.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:gap/gap.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/features/config_option/model/config_option_entity.dart'; +import 'package:hiddify/features/config_option/model/config_option_patch.dart'; +import 'package:hiddify/features/config_option/notifier/config_option_notifier.dart'; +import 'package:hiddify/features/log/model/log_level.dart'; +import 'package:hiddify/features/settings/widgets/sections_widgets.dart'; +import 'package:hiddify/features/settings/widgets/settings_input_dialog.dart'; +import 'package:hiddify/singbox/model/singbox_config_enum.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}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + + final defaultOptions = ConfigOptionEntity.initial; + final asyncOptions = ref.watch(configOptionNotifierProvider); + + Future changeOption(ConfigOptionPatch patch) async { + await ref.read(configOptionNotifierProvider.notifier).updateOption(patch); + } + + return Scaffold( + appBar: AppBar( + title: Text(t.settings.config.pageTitle), + actions: [ + if (asyncOptions case AsyncData(value: final options)) + PopupMenuButton( + itemBuilder: (context) { + return [ + PopupMenuItem( + child: Text(t.general.addToClipboard), + onTap: () { + Clipboard.setData( + ClipboardData(text: options.format()), + ); + }, + ), + ]; + }, + ), + ], + ), + body: switch (asyncOptions) { + AsyncData(value: final options) => 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: defaultOptions.logLevel, + ).show(context); + if (logLevel == null) return; + await changeOption(ConfigOptionPatch(logLevel: 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: (value) async => + changeOption(ConfigOptionPatch(resolveDestination: value)), + ), + 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: defaultOptions.ipv6Mode, + ).show(context); + if (ipv6Mode == null) return; + await changeOption(ConfigOptionPatch(ipv6Mode: 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: defaultOptions.remoteDnsAddress, + ).show(context); + if (url == null || url.isEmpty) return; + await changeOption(ConfigOptionPatch(remoteDnsAddress: 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: defaultOptions.remoteDnsDomainStrategy, + ).show(context); + if (domainStrategy == null) return; + await changeOption( + ConfigOptionPatch(remoteDnsDomainStrategy: 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: defaultOptions.directDnsAddress, + ).show(context); + if (url == null || url.isEmpty) return; + await changeOption(ConfigOptionPatch(directDnsAddress: 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: defaultOptions.directDnsDomainStrategy, + ).show(context); + if (domainStrategy == null) return; + await changeOption( + ConfigOptionPatch(directDnsDomainStrategy: 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(options.serviceMode.present(t)), + onTap: () async { + final pickedMode = await SettingsPickerDialog( + title: t.settings.config.serviceMode, + selected: options.serviceMode, + options: ServiceMode.choices, + getTitle: (e) => e.present(t), + resetValue: ServiceMode.defaultMode, + ).show(context); + if (pickedMode == null) return; + await changeOption( + ConfigOptionPatch(serviceMode: pickedMode), + ); + }, + ), + SwitchListTile( + title: Text(t.settings.config.strictRoute), + value: options.strictRoute, + onChanged: (value) async => + changeOption(ConfigOptionPatch(strictRoute: value)), + ), + 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: defaultOptions.tunImplementation, + ).show(context); + if (tunImplementation == null) return; + await changeOption( + ConfigOptionPatch(tunImplementation: 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: defaultOptions.mixedPort, + validator: isPort, + mapTo: int.tryParse, + digitsOnly: true, + ).show(context); + if (mixedPort == null) return; + await changeOption(ConfigOptionPatch(mixedPort: 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: defaultOptions.localDnsPort, + validator: isPort, + mapTo: int.tryParse, + digitsOnly: true, + ).show(context); + if (localDnsPort == null) return; + await changeOption( + ConfigOptionPatch(localDnsPort: 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: defaultOptions.connectionTestUrl, + ).show(context); + if (url == null || url.isEmpty || !isUrl(url)) return; + await changeOption(ConfigOptionPatch(connectionTestUrl: 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 + .coerceIn(0, 60) + .toDouble(), + resetValue: + defaultOptions.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 changeOption( + ConfigOptionPatch( + urlTestInterval: + 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: defaultOptions.clashApiPort, + validator: isPort, + mapTo: int.tryParse, + digitsOnly: true, + ).show(context); + if (clashApiPort == null) return; + await changeOption( + ConfigOptionPatch(clashApiPort: clashApiPort), + ); + }, + ), + const Gap(24), + ], + ), + // TODO show appropriate error/loading widgets + _ => const SizedBox(), + }, + ); + } +} diff --git a/lib/features/connection/data/connection_data_providers.dart b/lib/features/connection/data/connection_data_providers.dart new file mode 100644 index 00000000..a33f150d --- /dev/null +++ b/lib/features/connection/data/connection_data_providers.dart @@ -0,0 +1,24 @@ +import 'package:hiddify/features/config_option/data/config_option_data_providers.dart'; +import 'package:hiddify/features/connection/data/connection_platform_source.dart'; +import 'package:hiddify/features/connection/data/connection_repository.dart'; +import 'package:hiddify/features/geo_asset/data/geo_asset_data_providers.dart'; +import 'package:hiddify/features/profile/data/profile_data_providers.dart'; +import 'package:hiddify/services/service_providers.dart'; +import 'package:hiddify/singbox/service/singbox_service_provider.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'connection_data_providers.g.dart'; + +@Riverpod(keepAlive: true) +ConnectionRepository connectionRepository( + ConnectionRepositoryRef ref, +) { + return ConnectionRepositoryImpl( + directories: ref.watch(filesEditorServiceProvider).dirs, + configOptionRepository: ref.watch(configOptionRepositoryProvider), + singbox: ref.watch(singboxServiceProvider), + platformSource: ConnectionPlatformSourceImpl(), + profilePathResolver: ref.watch(profilePathResolverProvider), + geoAssetPathResolver: ref.watch(geoAssetPathResolverProvider), + ); +} diff --git a/lib/features/connection/data/connection_platform_source.dart b/lib/features/connection/data/connection_platform_source.dart new file mode 100644 index 00000000..7304abb6 --- /dev/null +++ b/lib/features/connection/data/connection_platform_source.dart @@ -0,0 +1,67 @@ +import 'dart:ffi'; +import 'dart:io'; + +import 'package:hiddify/core/utils/ffi_utils.dart'; +import 'package:hiddify/utils/custom_loggers.dart'; +import 'package:posix/posix.dart'; +import 'package:win32/win32.dart'; + +abstract interface class ConnectionPlatformSource { + Future checkPrivilege(); +} + +class ConnectionPlatformSourceImpl + with InfraLogger + implements ConnectionPlatformSource { + @override + Future checkPrivilege() async { + try { + if (Platform.isWindows) { + bool isElevated = false; + withMemory(sizeOf(), (phToken) { + withMemory(sizeOf(), (pReturnedSize) { + withMemory(sizeOf<_TokenElevation>(), + (pElevation) { + if (OpenProcessToken( + GetCurrentProcess(), + TOKEN_QUERY, + phToken.cast(), + ) == + 1) { + if (GetTokenInformation( + phToken.value, + TOKEN_INFORMATION_CLASS.TokenElevation, + pElevation, + sizeOf<_TokenElevation>(), + pReturnedSize, + ) == + 1) { + isElevated = pElevation.ref.tokenIsElevated != 0; + } + } + if (phToken.value != 0) { + CloseHandle(phToken.value); + } + }); + }); + }); + return isElevated; + } else if (Platform.isLinux || Platform.isMacOS) { + final euid = geteuid(); + return euid == 0; + } else { + return true; + } + } catch (e) { + loggy.warning("error checking privilege", e); + return true; // return true so core handles it + } + } +} + +sealed class _TokenElevation extends Struct { + /// A nonzero value if the token has elevated privileges; + /// otherwise, a zero value. + @Int32() + external int tokenIsElevated; +} diff --git a/lib/features/connection/data/connection_repository.dart b/lib/features/connection/data/connection_repository.dart new file mode 100644 index 00000000..8561253a --- /dev/null +++ b/lib/features/connection/data/connection_repository.dart @@ -0,0 +1,214 @@ +import 'dart:io'; + +import 'package:fpdart/fpdart.dart'; +import 'package:hiddify/core/model/directories.dart'; +import 'package:hiddify/core/utils/exception_handler.dart'; +import 'package:hiddify/features/config_option/data/config_option_repository.dart'; +import 'package:hiddify/features/connection/data/connection_platform_source.dart'; +import 'package:hiddify/features/connection/model/connection_failure.dart'; +import 'package:hiddify/features/connection/model/connection_status.dart'; +import 'package:hiddify/features/geo_asset/data/geo_asset_path_resolver.dart'; +import 'package:hiddify/features/profile/data/profile_path_resolver.dart'; +import 'package:hiddify/singbox/model/singbox_config_option.dart'; +import 'package:hiddify/singbox/model/singbox_status.dart'; +import 'package:hiddify/singbox/service/singbox_service.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:meta/meta.dart'; + +abstract interface class ConnectionRepository { + Stream watchConnectionStatus(); + TaskEither connect( + String fileName, + bool disableMemoryLimit, + ); + TaskEither disconnect(); + TaskEither reconnect( + String fileName, + bool disableMemoryLimit, + ); +} + +class ConnectionRepositoryImpl + with ExceptionHandler, InfraLogger + implements ConnectionRepository { + ConnectionRepositoryImpl({ + required this.directories, + required this.singbox, + required this.platformSource, + required this.configOptionRepository, + required this.profilePathResolver, + required this.geoAssetPathResolver, + }); + + final Directories directories; + final SingboxService singbox; + final ConnectionPlatformSource platformSource; + final ConfigOptionRepository configOptionRepository; + final ProfilePathResolver profilePathResolver; + final GeoAssetPathResolver geoAssetPathResolver; + + bool _initialized = false; + + @override + Stream watchConnectionStatus() { + return singbox.watchStatus().map( + (event) => switch (event) { + SingboxStopped(:final alert?, :final message) => Disconnected( + switch (alert) { + SingboxAlert.emptyConfiguration => + ConnectionFailure.invalidConfig(message), + SingboxAlert.requestNotificationPermission => + ConnectionFailure.missingNotificationPermission(message), + SingboxAlert.requestVPNPermission => + ConnectionFailure.missingVpnPermission(message), + SingboxAlert.startCommandServer || + SingboxAlert.createService || + SingboxAlert.startService => + ConnectionFailure.unexpected(message), + }, + ), + SingboxStopped() => const Disconnected(), + SingboxStarting() => const Connecting(), + SingboxStarted() => const Connected(), + SingboxStopping() => const Disconnecting(), + }, + ); + } + + @visibleForTesting + TaskEither getConfigOption() { + return TaskEither.Do( + ($) async { + final options = await $( + configOptionRepository + .getFullSingboxConfigOption() + .mapLeft((l) => const InvalidConfigOption()), + ); + + return $( + TaskEither( + () async { + final geoip = geoAssetPathResolver.resolvePath(options.geoipPath); + final geosite = + geoAssetPathResolver.resolvePath(options.geositePath); + if (!await File(geoip).exists() || + !await File(geosite).exists()) { + return left(const ConnectionFailure.missingGeoAssets()); + } + return right(options); + }, + ), + ); + }, + ).handleExceptions(UnexpectedConnectionFailure.new); + } + + @visibleForTesting + TaskEither applyConfigOption( + SingboxConfigOption options, + ) { + return exceptionHandler( + () { + return singbox + .changeOptions(options) + .mapLeft(InvalidConfigOption.new) + .run(); + }, + UnexpectedConnectionFailure.new, + ); + } + + @visibleForTesting + TaskEither setup() { + if (_initialized) return TaskEither.of(unit); + return exceptionHandler( + () { + loggy.debug("setting up singbox"); + return singbox + .setup( + directories, + false, + ) + .map((r) { + loggy.debug("setup complete"); + _initialized = true; + return r; + }) + .mapLeft(UnexpectedConnectionFailure.new) + .run(); + }, + UnexpectedConnectionFailure.new, + ); + } + + @override + TaskEither connect( + String fileName, + bool disableMemoryLimit, + ) { + return TaskEither.Do( + ($) async { + final options = await $(getConfigOption()); + loggy.info( + "config options: ${options.format()}\nMemory Limit: ${!disableMemoryLimit}", + ); + + await $( + TaskEither(() async { + if (options.enableTun) { + final hasPrivilege = await platformSource.checkPrivilege(); + if (!hasPrivilege) { + loggy.warning("missing privileges for tun mode"); + return left(const MissingPrivilege()); + } + } + return right(unit); + }), + ); + await $(setup()); + loggy.debug("after setup"); + await $(applyConfigOption(options)); + loggy.debug("after apply"); + return await $( + singbox + .start( + profilePathResolver.file(fileName).path, + disableMemoryLimit, + ) + .mapLeft(UnexpectedConnectionFailure.new), + ); + }, + ).handleExceptions(UnexpectedConnectionFailure.new); + } + + @override + TaskEither disconnect() { + return exceptionHandler( + () => singbox.stop().mapLeft(UnexpectedConnectionFailure.new).run(), + UnexpectedConnectionFailure.new, + ); + } + + @override + TaskEither reconnect( + String fileName, + bool disableMemoryLimit, + ) { + return exceptionHandler( + () async { + return getConfigOption() + .flatMap((options) => applyConfigOption(options)) + .andThen( + () => singbox + .restart( + profilePathResolver.file(fileName).path, + disableMemoryLimit, + ) + .mapLeft(UnexpectedConnectionFailure.new), + ) + .run(); + }, + UnexpectedConnectionFailure.new, + ); + } +} diff --git a/lib/domain/connectivity/connection_failure.dart b/lib/features/connection/model/connection_failure.dart similarity index 51% rename from lib/domain/connectivity/connection_failure.dart rename to lib/features/connection/model/connection_failure.dart index b8b39f04..4586f2be 100644 --- a/lib/domain/connectivity/connection_failure.dart +++ b/lib/features/connection/model/connection_failure.dart @@ -1,7 +1,6 @@ import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:hiddify/core/prefs/prefs.dart'; -import 'package:hiddify/domain/core_service_failure.dart'; -import 'package:hiddify/domain/failures.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/model/failures.dart'; part 'connection_failure.freezed.dart'; @@ -24,8 +23,21 @@ sealed class ConnectionFailure with _$ConnectionFailure, Failure { String? message, ]) = MissingNotificationPermission; - const factory ConnectionFailure.core(CoreServiceFailure failure) = - CoreConnectionFailure; + @With() + const factory ConnectionFailure.missingPrivilege() = MissingPrivilege; + + @With() + const factory ConnectionFailure.missingGeoAssets() = MissingGeoAssets; + + @With() + const factory ConnectionFailure.invalidConfigOption([ + String? message, + ]) = InvalidConfigOption; + + @With() + const factory ConnectionFailure.invalidConfig([ + String? message, + ]) = InvalidConfig; @override ({String type, String? message}) present(TranslationsEn t) { @@ -42,7 +54,22 @@ sealed class ConnectionFailure with _$ConnectionFailure, Failure { type: t.failure.connectivity.missingNotificationPermission, message: message ), - CoreConnectionFailure(:final failure) => failure.present(t), + MissingPrivilege() => ( + type: t.failure.singbox.missingPrivilege, + message: t.failure.singbox.missingPrivilegeMsg, + ), + MissingGeoAssets() => ( + type: t.failure.singbox.missingGeoAssets, + message: t.failure.singbox.missingGeoAssetsMsg, + ), + InvalidConfigOption(:final message) => ( + type: t.failure.singbox.invalidConfigOptions, + message: message, + ), + InvalidConfig(:final message) => ( + type: t.failure.singbox.invalidConfig, + message: message, + ), }; } } diff --git a/lib/domain/connectivity/connection_status.dart b/lib/features/connection/model/connection_status.dart similarity index 90% rename from lib/domain/connectivity/connection_status.dart rename to lib/features/connection/model/connection_status.dart index df84b363..c6497db1 100644 --- a/lib/domain/connectivity/connection_status.dart +++ b/lib/features/connection/model/connection_status.dart @@ -1,6 +1,6 @@ import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:hiddify/core/prefs/prefs.dart'; -import 'package:hiddify/domain/connectivity/connection_failure.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/features/connection/model/connection_failure.dart'; part 'connection_status.freezed.dart'; diff --git a/lib/features/common/connectivity/connectivity_controller.dart b/lib/features/connection/notifier/connection_notifier.dart similarity index 75% rename from lib/features/common/connectivity/connectivity_controller.dart rename to lib/features/connection/notifier/connection_notifier.dart index 8f835b6b..21c22339 100644 --- a/lib/features/common/connectivity/connectivity_controller.dart +++ b/lib/features/connection/notifier/connection_notifier.dart @@ -1,17 +1,17 @@ -import 'package:hiddify/core/prefs/prefs.dart'; -import 'package:hiddify/core/prefs/service_prefs.dart'; -import 'package:hiddify/data/data_providers.dart'; -import 'package:hiddify/domain/connectivity/connectivity.dart'; -import 'package:hiddify/domain/core_facade.dart'; +import 'package:hiddify/core/preferences/general_preferences.dart'; +import 'package:hiddify/core/preferences/service_preferences.dart'; +import 'package:hiddify/features/connection/data/connection_data_providers.dart'; +import 'package:hiddify/features/connection/data/connection_repository.dart'; +import 'package:hiddify/features/connection/model/connection_status.dart'; import 'package:hiddify/features/profile/notifier/active_profile_notifier.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:rxdart/rxdart.dart'; -part 'connectivity_controller.g.dart'; +part 'connection_notifier.g.dart'; @Riverpod(keepAlive: true) -class ConnectivityController extends _$ConnectivityController with AppLogger { +class ConnectionNotifier extends _$ConnectionNotifier with AppLogger { @override Stream build() { ref.listen( @@ -24,7 +24,7 @@ class ConnectivityController extends _$ConnectivityController with AppLogger { } }, ); - return _core.watchConnectionStatus().doOnData((event) { + return _connectionRepo.watchConnectionStatus().doOnData((event) { if (event case Disconnected(connectionFailure: final _?) when PlatformUtils.isDesktop) { ref.read(startedByUserProvider.notifier).update(false); @@ -33,7 +33,8 @@ class ConnectivityController extends _$ConnectivityController with AppLogger { }); } - CoreFacade get _core => ref.watch(coreFacadeProvider); + ConnectionRepository get _connectionRepo => + ref.read(connectionRepositoryProvider); Future mayConnect() async { if (state case AsyncData(:final value)) { @@ -66,8 +67,8 @@ class ConnectivityController extends _$ConnectivityController with AppLogger { } loggy.info("active profile changed, reconnecting"); await ref.read(startedByUserProvider.notifier).update(true); - await _core - .restart(profileId, ref.read(disableMemoryLimitProvider)) + await _connectionRepo + .reconnect(profileId, ref.read(disableMemoryLimitProvider)) .mapLeft((err) { loggy.warning("error reconnecting", err); state = AsyncError(err, StackTrace.current); @@ -88,8 +89,8 @@ class ConnectivityController extends _$ConnectivityController with AppLogger { Future _connect() async { final activeProfile = await ref.read(activeProfileProvider.future); - await _core - .start(activeProfile!.id, ref.read(disableMemoryLimitProvider)) + await _connectionRepo + .connect(activeProfile!.id, ref.read(disableMemoryLimitProvider)) .mapLeft((err) async { loggy.warning("error connecting", err); await ref.read(startedByUserProvider.notifier).update(false); @@ -98,7 +99,7 @@ class ConnectivityController extends _$ConnectivityController with AppLogger { } Future _disconnect() async { - await _core.stop().mapLeft((err) { + await _connectionRepo.disconnect().mapLeft((err) { loggy.warning("error disconnecting", err); state = AsyncError(err, StackTrace.current); }).run(); @@ -108,6 +109,6 @@ class ConnectivityController extends _$ConnectivityController with AppLogger { @Riverpod(keepAlive: true) Future serviceRunning(ServiceRunningRef ref) => ref .watch( - connectivityControllerProvider.selectAsync((data) => data.isConnected), + connectionNotifierProvider.selectAsync((data) => data.isConnected), ) .onError((error, stackTrace) => false); diff --git a/lib/features/geo_asset/data/geo_asset_data_mapper.dart b/lib/features/geo_asset/data/geo_asset_data_mapper.dart index 7906ac0e..0f2401e5 100644 --- a/lib/features/geo_asset/data/geo_asset_data_mapper.dart +++ b/lib/features/geo_asset/data/geo_asset_data_mapper.dart @@ -1,5 +1,5 @@ import 'package:drift/drift.dart'; -import 'package:hiddify/data/local/database.dart'; +import 'package:hiddify/core/database/app_database.dart'; import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart'; extension GeoAssetEntityMapper on GeoAssetEntity { diff --git a/lib/features/geo_asset/data/geo_asset_data_providers.dart b/lib/features/geo_asset/data/geo_asset_data_providers.dart index 1e9be492..44a8c51f 100644 --- a/lib/features/geo_asset/data/geo_asset_data_providers.dart +++ b/lib/features/geo_asset/data/geo_asset_data_providers.dart @@ -1,4 +1,5 @@ -import 'package:hiddify/data/data_providers.dart'; +import 'package:hiddify/core/database/database_provider.dart'; +import 'package:hiddify/core/http_client/http_client_provider.dart'; import 'package:hiddify/features/geo_asset/data/geo_asset_data_source.dart'; import 'package:hiddify/features/geo_asset/data/geo_asset_path_resolver.dart'; import 'package:hiddify/features/geo_asset/data/geo_asset_repository.dart'; @@ -12,7 +13,7 @@ Future geoAssetRepository(GeoAssetRepositoryRef ref) async { final repo = GeoAssetRepositoryImpl( geoAssetDataSource: ref.watch(geoAssetDataSourceProvider), geoAssetPathResolver: ref.watch(geoAssetPathResolverProvider), - dio: ref.watch(dioProvider), + dio: ref.watch(httpClientProvider), ); await repo.init().getOrElse((l) => throw l).run(); return repo; diff --git a/lib/features/geo_asset/data/geo_asset_data_source.dart b/lib/features/geo_asset/data/geo_asset_data_source.dart index b72b751d..b3c63aac 100644 --- a/lib/features/geo_asset/data/geo_asset_data_source.dart +++ b/lib/features/geo_asset/data/geo_asset_data_source.dart @@ -1,6 +1,6 @@ import 'package:drift/drift.dart'; -import 'package:hiddify/data/local/database.dart'; -import 'package:hiddify/data/local/tables.dart'; +import 'package:hiddify/core/database/app_database.dart'; +import 'package:hiddify/core/database/tables/database_tables.dart'; import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart'; import 'package:hiddify/utils/custom_loggers.dart'; diff --git a/lib/features/geo_asset/data/geo_asset_repository.dart b/lib/features/geo_asset/data/geo_asset_repository.dart index 74abb850..3d39e13f 100644 --- a/lib/features/geo_asset/data/geo_asset_repository.dart +++ b/lib/features/geo_asset/data/geo_asset_repository.dart @@ -5,8 +5,8 @@ import 'package:dio/dio.dart'; import 'package:drift/drift.dart'; import 'package:flutter/services.dart'; import 'package:fpdart/fpdart.dart'; -import 'package:hiddify/data/local/database.dart'; -import 'package:hiddify/data/repository/exception_handlers.dart'; +import 'package:hiddify/core/database/app_database.dart'; +import 'package:hiddify/core/utils/exception_handler.dart'; import 'package:hiddify/features/geo_asset/data/geo_asset_data_mapper.dart'; import 'package:hiddify/features/geo_asset/data/geo_asset_data_source.dart'; import 'package:hiddify/features/geo_asset/data/geo_asset_path_resolver.dart'; diff --git a/lib/features/geo_asset/model/geo_asset_failure.dart b/lib/features/geo_asset/model/geo_asset_failure.dart index 161b193e..882864da 100644 --- a/lib/features/geo_asset/model/geo_asset_failure.dart +++ b/lib/features/geo_asset/model/geo_asset_failure.dart @@ -1,6 +1,6 @@ import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:hiddify/core/prefs/prefs.dart'; -import 'package:hiddify/domain/failures.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/model/failures.dart'; part 'geo_asset_failure.freezed.dart'; @@ -8,6 +8,7 @@ part 'geo_asset_failure.freezed.dart'; sealed class GeoAssetFailure with _$GeoAssetFailure, Failure { const GeoAssetFailure._(); + @With() const factory GeoAssetFailure.unexpected([ Object? error, StackTrace? stackTrace, @@ -16,6 +17,7 @@ sealed class GeoAssetFailure with _$GeoAssetFailure, Failure { @With() const factory GeoAssetFailure.noUpdateAvailable() = GeoAssetNoUpdateAvailable; + @With() const factory GeoAssetFailure.activeAssetNotFound() = GeoAssetActiveAssetNotFound; diff --git a/lib/features/geo_asset/overview/geo_assets_overview_page.dart b/lib/features/geo_asset/overview/geo_assets_overview_page.dart index a611fabd..6917721c 100644 --- a/lib/features/geo_asset/overview/geo_assets_overview_page.dart +++ b/lib/features/geo_asset/overview/geo_assets_overview_page.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:hiddify/core/core_providers.dart'; +import 'package:hiddify/core/localization/translations.dart'; import 'package:hiddify/features/geo_asset/overview/geo_assets_overview_notifier.dart'; import 'package:hiddify/features/geo_asset/widget/geo_asset_tile.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; diff --git a/lib/features/geo_asset/widget/geo_asset_tile.dart b/lib/features/geo_asset/widget/geo_asset_tile.dart index 620f1a21..61fbbee6 100644 --- a/lib/features/geo_asset/widget/geo_asset_tile.dart +++ b/lib/features/geo_asset/widget/geo_asset_tile.dart @@ -1,7 +1,7 @@ import 'package:dartx/dartx.dart'; import 'package:flutter/material.dart'; -import 'package:hiddify/core/core_providers.dart'; -import 'package:hiddify/domain/failures.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/model/failures.dart'; import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart'; import 'package:hiddify/features/geo_asset/model/geo_asset_failure.dart'; import 'package:hiddify/features/geo_asset/notifier/geo_asset_notifier.dart'; diff --git a/lib/features/home/view/view.dart b/lib/features/home/view/view.dart deleted file mode 100644 index e4ff2696..00000000 --- a/lib/features/home/view/view.dart +++ /dev/null @@ -1 +0,0 @@ -export 'home_page.dart'; diff --git a/lib/features/home/widgets/connection_button.dart b/lib/features/home/widget/connection_button.dart similarity index 84% rename from lib/features/home/widgets/connection_button.dart rename to lib/features/home/widget/connection_button.dart index afef3330..f2ff6489 100644 --- a/lib/features/home/widgets/connection_button.dart +++ b/lib/features/home/widget/connection_button.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:gap/gap.dart'; -import 'package:hiddify/core/core_providers.dart'; -import 'package:hiddify/core/prefs/prefs.dart'; -import 'package:hiddify/domain/connectivity/connectivity.dart'; -import 'package:hiddify/domain/failures.dart'; -import 'package:hiddify/features/common/connectivity/connectivity_controller.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/model/failures.dart'; +import 'package:hiddify/core/theme/theme_extensions.dart'; +import 'package:hiddify/features/connection/model/connection_status.dart'; +import 'package:hiddify/features/connection/notifier/connection_notifier.dart'; import 'package:hiddify/gen/assets.gen.dart'; import 'package:hiddify/utils/alerts.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -17,10 +17,10 @@ class ConnectionButton extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final t = ref.watch(translationsProvider); - final connectionStatus = ref.watch(connectivityControllerProvider); + final connectionStatus = ref.watch(connectionNotifierProvider); ref.listen( - connectivityControllerProvider, + connectionNotifierProvider, (_, next) { if (next case AsyncError(:final error)) { CustomAlertDialog.fromErr(t.presentError(error)).show(context); @@ -42,18 +42,16 @@ class ConnectionButton extends HookConsumerWidget { : buttonTheme.idleColor!; return _ConnectionButton( - onTap: () => ref - .read(connectivityControllerProvider.notifier) - .toggleConnection(), + onTap: () => + ref.read(connectionNotifierProvider.notifier).toggleConnection(), enabled: !status.isSwitching, label: status.present(t), buttonColor: connectionLogoColor, ); case AsyncError(): return _ConnectionButton( - onTap: () => ref - .read(connectivityControllerProvider.notifier) - .toggleConnection(), + onTap: () => + ref.read(connectionNotifierProvider.notifier).toggleConnection(), enabled: true, label: const Disconnected().present(t), buttonColor: buttonTheme.idleColor!, diff --git a/lib/features/home/widgets/empty_profiles_home_body.dart b/lib/features/home/widget/empty_profiles_home_body.dart similarity index 96% rename from lib/features/home/widgets/empty_profiles_home_body.dart rename to lib/features/home/widget/empty_profiles_home_body.dart index 8937ce18..eed2b370 100644 --- a/lib/features/home/widgets/empty_profiles_home_body.dart +++ b/lib/features/home/widget/empty_profiles_home_body.dart @@ -1,6 +1,6 @@ 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/core/router/router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; diff --git a/lib/features/home/view/home_page.dart b/lib/features/home/widget/home_page.dart similarity index 90% rename from lib/features/home/view/home_page.dart rename to lib/features/home/widget/home_page.dart index 86512ad8..785b0e2b 100644 --- a/lib/features/home/view/home_page.dart +++ b/lib/features/home/widget/home_page.dart @@ -1,15 +1,16 @@ import 'package:dartx/dartx.dart'; import 'package:flutter/material.dart'; -import 'package:hiddify/core/core_providers.dart'; +import 'package:hiddify/core/app_info/app_info_provider.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/model/failures.dart'; import 'package:hiddify/core/router/router.dart'; -import 'package:hiddify/domain/failures.dart'; import 'package:hiddify/features/common/nested_app_bar.dart'; -import 'package:hiddify/features/home/widgets/widgets.dart'; +import 'package:hiddify/features/home/widget/connection_button.dart'; +import 'package:hiddify/features/home/widget/empty_profiles_home_body.dart'; import 'package:hiddify/features/profile/notifier/active_profile_notifier.dart'; import 'package:hiddify/features/profile/widget/profile_tile.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; - import 'package:sliver_tools/sliver_tools.dart'; class HomePage extends HookConsumerWidget { @@ -91,7 +92,7 @@ class AppVersionLabel extends HookConsumerWidget { final t = ref.watch(translationsProvider); final theme = Theme.of(context); - final version = ref.watch(appInfoProvider).presentVersion; + final version = ref.watch(appInfoProvider).requireValue.presentVersion; if (version.isBlank) return const SizedBox(); return Semantics( diff --git a/lib/features/home/widgets/widgets.dart b/lib/features/home/widgets/widgets.dart deleted file mode 100644 index b043254e..00000000 --- a/lib/features/home/widgets/widgets.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'connection_button.dart'; -export 'empty_profiles_home_body.dart'; diff --git a/lib/features/intro/intro_page.dart b/lib/features/intro/widget/intro_page.dart similarity index 95% rename from lib/features/intro/intro_page.dart rename to lib/features/intro/widget/intro_page.dart index 53da7370..5732a17d 100644 --- a/lib/features/intro/intro_page.dart +++ b/lib/features/intro/widget/intro_page.dart @@ -2,9 +2,9 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; -import 'package:hiddify/core/core_providers.dart'; -import 'package:hiddify/core/prefs/prefs.dart'; -import 'package:hiddify/domain/constants.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/model/constants.dart'; +import 'package:hiddify/core/preferences/general_preferences.dart'; import 'package:hiddify/features/common/general_pref_tiles.dart'; import 'package:hiddify/gen/assets.gen.dart'; import 'package:hiddify/utils/utils.dart'; diff --git a/lib/features/log/data/log_data_providers.dart b/lib/features/log/data/log_data_providers.dart index 199b7dbc..c84aa7b7 100644 --- a/lib/features/log/data/log_data_providers.dart +++ b/lib/features/log/data/log_data_providers.dart @@ -1,6 +1,7 @@ import 'package:hiddify/features/log/data/log_path_resolver.dart'; import 'package:hiddify/features/log/data/log_repository.dart'; import 'package:hiddify/services/service_providers.dart'; +import 'package:hiddify/singbox/service/singbox_service_provider.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'log_data_providers.g.dart'; diff --git a/lib/features/log/data/log_repository.dart b/lib/features/log/data/log_repository.dart index bd699d42..4f4cb443 100644 --- a/lib/features/log/data/log_repository.dart +++ b/lib/features/log/data/log_repository.dart @@ -1,10 +1,10 @@ import 'package:fpdart/fpdart.dart'; -import 'package:hiddify/data/repository/exception_handlers.dart'; +import 'package:hiddify/core/utils/exception_handler.dart'; import 'package:hiddify/features/log/data/log_parser.dart'; import 'package:hiddify/features/log/data/log_path_resolver.dart'; import 'package:hiddify/features/log/model/log_entity.dart'; import 'package:hiddify/features/log/model/log_failure.dart'; -import 'package:hiddify/services/singbox/singbox_service.dart'; +import 'package:hiddify/singbox/service/singbox_service.dart'; import 'package:hiddify/utils/custom_loggers.dart'; abstract interface class LogRepository { diff --git a/lib/features/log/model/log_failure.dart b/lib/features/log/model/log_failure.dart index f1a4e8fe..5815053e 100644 --- a/lib/features/log/model/log_failure.dart +++ b/lib/features/log/model/log_failure.dart @@ -1,6 +1,6 @@ import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:hiddify/core/prefs/locale_prefs.dart'; -import 'package:hiddify/domain/failures.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/model/failures.dart'; part 'log_failure.freezed.dart'; @@ -8,6 +8,7 @@ part 'log_failure.freezed.dart'; sealed class LogFailure with _$LogFailure, Failure { const LogFailure._(); + @With() const factory LogFailure.unexpected([ Object? error, StackTrace? stackTrace, @@ -17,7 +18,7 @@ sealed class LogFailure with _$LogFailure, Failure { ({String type, String? message}) present(TranslationsEn t) { return switch (this) { LogUnexpectedFailure() => ( - type: "unexpected", + type: t.failure.unexpected, message: null, ), }; diff --git a/lib/features/log/overview/logs_overview_page.dart b/lib/features/log/overview/logs_overview_page.dart index 3c8e4f75..17b1b8c9 100644 --- a/lib/features/log/overview/logs_overview_page.dart +++ b/lib/features/log/overview/logs_overview_page.dart @@ -2,9 +2,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:fpdart/fpdart.dart'; import 'package:gap/gap.dart'; -import 'package:hiddify/core/core_providers.dart'; -import 'package:hiddify/core/prefs/prefs.dart'; -import 'package:hiddify/domain/failures.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/model/failures.dart'; +import 'package:hiddify/core/preferences/general_preferences.dart'; import 'package:hiddify/features/common/nested_app_bar.dart'; import 'package:hiddify/features/log/data/log_data_providers.dart'; import 'package:hiddify/features/log/model/log_level.dart'; diff --git a/lib/features/per_app_proxy/data/per_app_proxy_data_providers.dart b/lib/features/per_app_proxy/data/per_app_proxy_data_providers.dart new file mode 100644 index 00000000..74f01681 --- /dev/null +++ b/lib/features/per_app_proxy/data/per_app_proxy_data_providers.dart @@ -0,0 +1,9 @@ +import 'package:hiddify/features/per_app_proxy/data/per_app_proxy_repository.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'per_app_proxy_data_providers.g.dart'; + +@Riverpod(keepAlive: true) +PerAppProxyRepository perAppProxyRepository(PerAppProxyRepositoryRef ref) { + return PerAppProxyRepositoryImpl(); +} diff --git a/lib/features/per_app_proxy/data/per_app_proxy_repository.dart b/lib/features/per_app_proxy/data/per_app_proxy_repository.dart new file mode 100644 index 00000000..7219727c --- /dev/null +++ b/lib/features/per_app_proxy/data/per_app_proxy_repository.dart @@ -0,0 +1,55 @@ +import 'dart:convert'; + +import 'package:flutter/services.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:hiddify/features/per_app_proxy/model/installed_package_info.dart'; +import 'package:hiddify/utils/utils.dart'; + +abstract interface class PerAppProxyRepository { + TaskEither> getInstalledPackages(); + TaskEither getPackageIcon(String packageName); +} + +class PerAppProxyRepositoryImpl + with InfraLogger + implements PerAppProxyRepository { + final _methodChannel = const MethodChannel("app.hiddify.com/platform"); + + @override + TaskEither> getInstalledPackages() { + return TaskEither( + () async { + loggy.debug("getting installed packages info"); + final result = + await _methodChannel.invokeMethod("get_installed_packages"); + if (result == null) return left("null response"); + return right( + (jsonDecode(result) as List).map((e) { + return InstalledPackageInfo.fromJson(e as Map); + }).toList(), + ); + }, + ); + } + + @override + TaskEither getPackageIcon(String packageName) { + return TaskEither( + () async { + loggy.debug("getting package [$packageName] icon"); + final result = await _methodChannel.invokeMethod( + "get_package_icon", + {"packageName": packageName}, + ); + if (result == null) return left("null response"); + final Uint8List decoded; + try { + decoded = base64.decode(result); + } catch (e) { + return left("error parsing base64 response"); + } + return right(decoded); + }, + ); + } +} diff --git a/lib/features/per_app_proxy/model/installed_package_info.dart b/lib/features/per_app_proxy/model/installed_package_info.dart new file mode 100644 index 00000000..b951c170 --- /dev/null +++ b/lib/features/per_app_proxy/model/installed_package_info.dart @@ -0,0 +1,17 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'installed_package_info.freezed.dart'; +part 'installed_package_info.g.dart'; + +@freezed +class InstalledPackageInfo with _$InstalledPackageInfo { + @JsonSerializable(fieldRename: FieldRename.kebab) + const factory InstalledPackageInfo({ + required String packageName, + required String name, + required bool isSystemApp, + }) = _InstalledPackageInfo; + + factory InstalledPackageInfo.fromJson(Map json) => + _$InstalledPackageInfoFromJson(json); +} diff --git a/lib/features/per_app_proxy/model/per_app_proxy_mode.dart b/lib/features/per_app_proxy/model/per_app_proxy_mode.dart new file mode 100644 index 00000000..6a84bb34 --- /dev/null +++ b/lib/features/per_app_proxy/model/per_app_proxy_mode.dart @@ -0,0 +1,24 @@ +import 'package:hiddify/core/localization/translations.dart'; + +enum PerAppProxyMode { + off, + include, + exclude; + + bool get enabled => this != off; + + ({String title, String message}) present(TranslationsEn t) => switch (this) { + off => ( + title: t.settings.network.perAppProxyModes.off, + message: t.settings.network.perAppProxyModes.offMsg, + ), + include => ( + title: t.settings.network.perAppProxyModes.include, + message: t.settings.network.perAppProxyModes.includeMsg, + ), + exclude => ( + title: t.settings.network.perAppProxyModes.exclude, + message: t.settings.network.perAppProxyModes.excludeMsg, + ), + }; +} diff --git a/lib/features/per_app_proxy/overview/per_app_proxy_notifier.dart b/lib/features/per_app_proxy/overview/per_app_proxy_notifier.dart new file mode 100644 index 00000000..0830151f --- /dev/null +++ b/lib/features/per_app_proxy/overview/per_app_proxy_notifier.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:hiddify/features/per_app_proxy/data/per_app_proxy_data_providers.dart'; +import 'package:hiddify/features/per_app_proxy/model/installed_package_info.dart'; +import 'package:hiddify/utils/riverpod_utils.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'per_app_proxy_notifier.g.dart'; + +@riverpod +Future> installedPackagesInfo( + InstalledPackagesInfoRef ref, +) async { + return ref + .watch(perAppProxyRepositoryProvider) + .getInstalledPackages() + .getOrElse((err) { + // _logger.error("error getting installed packages", err); + throw err; + }).run(); +} + +@riverpod +Future packageIcon( + PackageIconRef ref, + String packageName, +) async { + ref.disposeDelay(const Duration(seconds: 10)); + final bytes = await ref + .watch(perAppProxyRepositoryProvider) + .getPackageIcon(packageName) + .getOrElse((err) { + // _logger.warning("error getting package icon", err); + throw err; + }).run(); + return MemoryImage(bytes); +} diff --git a/lib/features/settings/view/per_app_proxy_page.dart b/lib/features/per_app_proxy/overview/per_app_proxy_page.dart similarity index 85% rename from lib/features/settings/view/per_app_proxy_page.dart rename to lib/features/per_app_proxy/overview/per_app_proxy_page.dart index d885110a..651fa6ff 100644 --- a/lib/features/settings/view/per_app_proxy_page.dart +++ b/lib/features/per_app_proxy/overview/per_app_proxy_page.dart @@ -2,51 +2,15 @@ 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/core/localization/translations.dart'; +import 'package:hiddify/core/preferences/general_preferences.dart'; +import 'package:hiddify/features/per_app_proxy/model/installed_package_info.dart'; +import 'package:hiddify/features/per_app_proxy/model/per_app_proxy_mode.dart'; +import 'package:hiddify/features/per_app_proxy/overview/per_app_proxy_notifier.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("PerAppProxySettings"); - -@riverpod -Future> installedPackagesInfo( - InstalledPackagesInfoRef ref, -) async { - return ref - .watch(platformServicesProvider) - .getInstalledPackages() - .getOrElse((err) { - _logger.error("error getting installed packages", err); - throw err; - }).run(); -} - -@riverpod -Future 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}); diff --git a/lib/features/profile/add/add_profile_modal.dart b/lib/features/profile/add/add_profile_modal.dart index 11217720..1a470601 100644 --- a/lib/features/profile/add/add_profile_modal.dart +++ b/lib/features/profile/add/add_profile_modal.dart @@ -3,7 +3,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.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/core/router/router.dart'; import 'package:hiddify/features/common/qr_code_scanner_screen.dart'; import 'package:hiddify/features/profile/notifier/profile_notifier.dart'; diff --git a/lib/features/profile/data/profile_data_mapper.dart b/lib/features/profile/data/profile_data_mapper.dart index 38d15061..6abe7bc3 100644 --- a/lib/features/profile/data/profile_data_mapper.dart +++ b/lib/features/profile/data/profile_data_mapper.dart @@ -1,5 +1,5 @@ import 'package:drift/drift.dart'; -import 'package:hiddify/data/local/database.dart'; +import 'package:hiddify/core/database/app_database.dart'; import 'package:hiddify/features/profile/model/profile_entity.dart'; extension ProfileEntityMapper on ProfileEntity { diff --git a/lib/features/profile/data/profile_data_providers.dart b/lib/features/profile/data/profile_data_providers.dart index 5ff0f515..3520b6a5 100644 --- a/lib/features/profile/data/profile_data_providers.dart +++ b/lib/features/profile/data/profile_data_providers.dart @@ -1,8 +1,10 @@ -import 'package:hiddify/data/data_providers.dart'; +import 'package:hiddify/core/database/database_provider.dart'; +import 'package:hiddify/core/http_client/http_client_provider.dart'; import 'package:hiddify/features/profile/data/profile_data_source.dart'; import 'package:hiddify/features/profile/data/profile_path_resolver.dart'; import 'package:hiddify/features/profile/data/profile_repository.dart'; import 'package:hiddify/services/service_providers.dart'; +import 'package:hiddify/singbox/service/singbox_service_provider.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'profile_data_providers.g.dart'; @@ -12,8 +14,8 @@ Future profileRepository(ProfileRepositoryRef ref) async { final repo = ProfileRepositoryImpl( profileDataSource: ref.watch(profileDataSourceProvider), profilePathResolver: ref.watch(profilePathResolverProvider), - configValidator: ref.watch(coreFacadeProvider).parseConfig, - dio: ref.watch(dioProvider), + singbox: ref.watch(singboxServiceProvider), + dio: ref.watch(httpClientProvider), ); await repo.init().getOrElse((l) => throw l).run(); return repo; diff --git a/lib/features/profile/data/profile_data_source.dart b/lib/features/profile/data/profile_data_source.dart index a5f6b241..8d8bc6bd 100644 --- a/lib/features/profile/data/profile_data_source.dart +++ b/lib/features/profile/data/profile_data_source.dart @@ -1,6 +1,6 @@ import 'package:drift/drift.dart'; -import 'package:hiddify/data/local/database.dart'; -import 'package:hiddify/data/local/tables.dart'; +import 'package:hiddify/core/database/app_database.dart'; +import 'package:hiddify/core/database/tables/database_tables.dart'; import 'package:hiddify/features/profile/model/profile_sort_enum.dart'; import 'package:hiddify/utils/utils.dart'; diff --git a/lib/features/profile/data/profile_repository.dart b/lib/features/profile/data/profile_repository.dart index 4a099e1a..4f4494c8 100644 --- a/lib/features/profile/data/profile_repository.dart +++ b/lib/features/profile/data/profile_repository.dart @@ -3,9 +3,8 @@ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:drift/drift.dart'; import 'package:fpdart/fpdart.dart'; -import 'package:hiddify/data/local/database.dart'; -import 'package:hiddify/data/repository/exception_handlers.dart'; -import 'package:hiddify/domain/core_service_failure.dart'; +import 'package:hiddify/core/database/app_database.dart'; +import 'package:hiddify/core/utils/exception_handler.dart'; import 'package:hiddify/features/profile/data/profile_data_mapper.dart'; import 'package:hiddify/features/profile/data/profile_data_source.dart'; import 'package:hiddify/features/profile/data/profile_parser.dart'; @@ -13,6 +12,7 @@ import 'package:hiddify/features/profile/data/profile_path_resolver.dart'; import 'package:hiddify/features/profile/model/profile_entity.dart'; import 'package:hiddify/features/profile/model/profile_failure.dart'; import 'package:hiddify/features/profile/model/profile_sort_enum.dart'; +import 'package:hiddify/singbox/service/singbox_service.dart'; import 'package:hiddify/utils/custom_loggers.dart'; import 'package:hiddify/utils/link_parsers.dart'; import 'package:meta/meta.dart'; @@ -43,6 +43,8 @@ abstract interface class ProfileRepository { TaskEither add(RemoteProfileEntity baseProfile); + TaskEither generateConfig(String id); + TaskEither updateSubscription( RemoteProfileEntity baseProfile, ); @@ -58,17 +60,13 @@ class ProfileRepositoryImpl ProfileRepositoryImpl({ required this.profileDataSource, required this.profilePathResolver, - required this.configValidator, + required this.singbox, required this.dio, }); final ProfileDataSource profileDataSource; final ProfilePathResolver profilePathResolver; - final TaskEither Function( - String path, - String tempPath, - bool debug, - ) configValidator; + final SingboxService singbox; final Dio dio; @override @@ -165,6 +163,23 @@ class ProfileRepositoryImpl ); } + @visibleForTesting + TaskEither validateConfig( + String path, + String tempPath, + bool debug, + ) { + return exceptionHandler( + () { + return singbox + .validateConfigByPath(path, tempPath, debug) + .mapLeft(ProfileFailure.invalidConfig) + .run(); + }, + ProfileUnexpectedFailure.new, + ); + } + @override TaskEither addByContent( String content, { @@ -179,24 +194,20 @@ class ProfileRepositoryImpl try { await tempFile.writeAsString(content); - final parseResult = - await configValidator(file.path, tempFile.path, false).run(); - return parseResult.fold( - (err) async { - loggy.warning("error parsing config", err); - return left(ProfileFailure.invalidConfig(err.msg)); - }, - (_) async { - final profile = LocalProfileEntity( - id: profileId, - active: markAsActive, - name: name, - lastUpdate: DateTime.now(), - ); - await profileDataSource.insert(profile.toEntry()); - return right(unit); - }, - ); + return await validateConfig(file.path, tempFile.path, false) + .andThen( + () => TaskEither(() async { + final profile = LocalProfileEntity( + id: profileId, + active: markAsActive, + name: name, + lastUpdate: DateTime.now(), + ); + await profileDataSource.insert(profile.toEntry()); + return right(unit); + }), + ) + .run(); } finally { if (tempFile.existsSync()) tempFile.deleteSync(); } @@ -235,6 +246,21 @@ class ProfileRepositoryImpl ); } + @override + TaskEither generateConfig(String id) { + return TaskEither.Do( + ($) async { + final configFile = profilePathResolver.file(id); + // TODO pass options + return await $( + singbox + .generateFullConfigByPath(configFile.path) + .mapLeft(ProfileFailure.unexpected), + ); + }, + ).handleExceptions(ProfileFailure.unexpected); + } + @override TaskEither updateSubscription( RemoteProfileEntity baseProfile, @@ -333,18 +359,14 @@ class ProfileRepositoryImpl ); final headers = await _populateHeaders(response.headers.map, tempFile.path); - final parseResult = - await configValidator(file.path, tempFile.path, false).run(); - return parseResult.fold( - (err) async { - loggy.warning("error parsing config", err); - return left(ProfileFailure.invalidConfig(err.msg)); - }, - (_) async { - final profile = ProfileParser.parse(url, headers); - return right(profile); - }, - ); + return await validateConfig(file.path, tempFile.path, false) + .andThen( + () => TaskEither(() async { + final profile = ProfileParser.parse(url, headers); + return right(profile); + }), + ) + .run(); } finally { if (tempFile.existsSync()) tempFile.deleteSync(); } diff --git a/lib/features/profile/details/profile_details_page.dart b/lib/features/profile/details/profile_details_page.dart index ce1e5b23..4c55d4ed 100644 --- a/lib/features/profile/details/profile_details_page.dart +++ b/lib/features/profile/details/profile_details_page.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:fpdart/fpdart.dart'; import 'package:go_router/go_router.dart'; -import 'package:hiddify/core/core_providers.dart'; -import 'package:hiddify/domain/failures.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/model/failures.dart'; import 'package:hiddify/features/common/confirmation_dialogs.dart'; import 'package:hiddify/features/profile/details/profile_details_notifier.dart'; import 'package:hiddify/features/profile/model/profile_entity.dart'; diff --git a/lib/features/profile/model/profile_failure.dart b/lib/features/profile/model/profile_failure.dart index 529b5269..440daf38 100644 --- a/lib/features/profile/model/profile_failure.dart +++ b/lib/features/profile/model/profile_failure.dart @@ -1,6 +1,6 @@ import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:hiddify/core/prefs/prefs.dart'; -import 'package:hiddify/domain/failures.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/model/failures.dart'; part 'profile_failure.freezed.dart'; diff --git a/lib/features/profile/model/profile_sort_enum.dart b/lib/features/profile/model/profile_sort_enum.dart index 04b8f6d4..5852a515 100644 --- a/lib/features/profile/model/profile_sort_enum.dart +++ b/lib/features/profile/model/profile_sort_enum.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:hiddify/core/prefs/prefs.dart'; +import 'package:hiddify/core/localization/translations.dart'; enum ProfilesSort { lastUpdate, diff --git a/lib/features/profile/notifier/profile_notifier.dart b/lib/features/profile/notifier/profile_notifier.dart index 263fdf85..7514b24b 100644 --- a/lib/features/profile/notifier/profile_notifier.dart +++ b/lib/features/profile/notifier/profile_notifier.dart @@ -1,9 +1,9 @@ import 'package:fpdart/fpdart.dart'; -import 'package:hiddify/core/core_providers.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/model/failures.dart'; import 'package:hiddify/core/notification/in_app_notification_controller.dart'; -import 'package:hiddify/core/prefs/general_prefs.dart'; -import 'package:hiddify/domain/failures.dart'; -import 'package:hiddify/features/common/connectivity/connectivity_controller.dart'; +import 'package:hiddify/core/preferences/general_preferences.dart'; +import 'package:hiddify/features/connection/notifier/connection_notifier.dart'; import 'package:hiddify/features/profile/data/profile_data_providers.dart'; import 'package:hiddify/features/profile/data/profile_repository.dart'; import 'package:hiddify/features/profile/model/profile_entity.dart'; @@ -127,7 +127,7 @@ class UpdateProfile extends _$UpdateProfile with AppLogger { await ref.read(activeProfileProvider.future).then((active) async { if (active != null && active.id == profile.id) { await ref - .read(connectivityControllerProvider.notifier) + .read(connectionNotifierProvider.notifier) .reconnect(profile.id); } }); diff --git a/lib/features/profile/notifier/profiles_update_notifier.dart b/lib/features/profile/notifier/profiles_update_notifier.dart index 5f4325b3..222f7f8e 100644 --- a/lib/features/profile/notifier/profiles_update_notifier.dart +++ b/lib/features/profile/notifier/profiles_update_notifier.dart @@ -1,5 +1,5 @@ import 'package:dartx/dartx.dart'; -import 'package:hiddify/data/data_providers.dart'; +import 'package:hiddify/core/preferences/preferences_provider.dart'; import 'package:hiddify/features/profile/data/profile_data_providers.dart'; import 'package:hiddify/features/profile/model/profile_entity.dart'; import 'package:hiddify/utils/custom_loggers.dart'; @@ -40,7 +40,8 @@ class ForegroundProfilesUpdateNotifier Future updateProfiles() async { try { final previousRun = DateTime.tryParse( - ref.read(sharedPreferencesProvider).getString(prefKey) ?? "", + ref.read(sharedPreferencesProvider).requireValue.getString(prefKey) ?? + "", ); if (previousRun != null && previousRun.add(interval) > DateTime.now()) { @@ -86,6 +87,7 @@ class ForegroundProfilesUpdateNotifier } finally { await ref .read(sharedPreferencesProvider) + .requireValue .setString(prefKey, DateTime.now().toIso8601String()); } } diff --git a/lib/features/profile/overview/profiles_overview_notifier.dart b/lib/features/profile/overview/profiles_overview_notifier.dart index 9434a2d9..7dcc68d4 100644 --- a/lib/features/profile/overview/profiles_overview_notifier.dart +++ b/lib/features/profile/overview/profiles_overview_notifier.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:flutter/services.dart'; import 'package:fpdart/fpdart.dart'; -import 'package:hiddify/data/data_providers.dart'; import 'package:hiddify/features/profile/data/profile_data_providers.dart'; import 'package:hiddify/features/profile/data/profile_repository.dart'; import 'package:hiddify/features/profile/model/profile_entity.dart'; @@ -70,7 +69,7 @@ class ProfilesOverviewNotifier extends _$ProfilesOverviewNotifier } Future exportConfigToClipboard(ProfileEntity profile) async { - await ref.read(coreFacadeProvider).generateConfig(profile.id).match( + await _profilesRepo.generateConfig(profile.id).match( (err) { loggy.warning('error generating config', err); throw err; diff --git a/lib/features/profile/overview/profiles_overview_page.dart b/lib/features/profile/overview/profiles_overview_page.dart index 105177d4..3e875b13 100644 --- a/lib/features/profile/overview/profiles_overview_page.dart +++ b/lib/features/profile/overview/profiles_overview_page.dart @@ -1,8 +1,8 @@ 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/core/model/failures.dart'; import 'package:hiddify/core/router/router.dart'; -import 'package:hiddify/domain/failures.dart'; import 'package:hiddify/features/profile/model/profile_sort_enum.dart'; import 'package:hiddify/features/profile/overview/profiles_overview_notifier.dart'; import 'package:hiddify/features/profile/widget/profile_tile.dart'; diff --git a/lib/features/profile/widget/profile_tile.dart b/lib/features/profile/widget/profile_tile.dart index c0000cb4..6d7f6c4b 100644 --- a/lib/features/profile/widget/profile_tile.dart +++ b/lib/features/profile/widget/profile_tile.dart @@ -3,10 +3,9 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:gap/gap.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/model/failures.dart'; import 'package:hiddify/core/router/router.dart'; -import 'package:hiddify/domain/failures.dart'; import 'package:hiddify/features/common/confirmation_dialogs.dart'; import 'package:hiddify/features/common/qr_code_dialog.dart'; import 'package:hiddify/features/profile/model/profile_entity.dart'; diff --git a/lib/features/proxies/notifier/notifier.dart b/lib/features/proxies/notifier/notifier.dart deleted file mode 100644 index 60e74984..00000000 --- a/lib/features/proxies/notifier/notifier.dart +++ /dev/null @@ -1 +0,0 @@ -export 'proxies_notifier.dart'; diff --git a/lib/features/proxies/view/view.dart b/lib/features/proxies/view/view.dart deleted file mode 100644 index b35ebe17..00000000 --- a/lib/features/proxies/view/view.dart +++ /dev/null @@ -1 +0,0 @@ -export 'proxies_page.dart'; diff --git a/lib/features/proxies/widgets/widgets.dart b/lib/features/proxies/widgets/widgets.dart deleted file mode 100644 index 6565a8f3..00000000 --- a/lib/features/proxies/widgets/widgets.dart +++ /dev/null @@ -1 +0,0 @@ -export 'proxy_tile.dart'; diff --git a/lib/features/proxy/data/proxy_data_providers.dart b/lib/features/proxy/data/proxy_data_providers.dart new file mode 100644 index 00000000..1c0c0823 --- /dev/null +++ b/lib/features/proxy/data/proxy_data_providers.dart @@ -0,0 +1,12 @@ +import 'package:hiddify/features/proxy/data/proxy_repository.dart'; +import 'package:hiddify/singbox/service/singbox_service_provider.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'proxy_data_providers.g.dart'; + +@Riverpod(keepAlive: true) +ProxyRepository proxyRepository(ProxyRepositoryRef ref) { + return ProxyRepositoryImpl( + singbox: ref.watch(singboxServiceProvider), + ); +} diff --git a/lib/features/proxy/data/proxy_repository.dart b/lib/features/proxy/data/proxy_repository.dart new file mode 100644 index 00000000..f318ecaf --- /dev/null +++ b/lib/features/proxy/data/proxy_repository.dart @@ -0,0 +1,78 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:hiddify/core/utils/exception_handler.dart'; +import 'package:hiddify/features/proxy/model/proxy_entity.dart'; +import 'package:hiddify/features/proxy/model/proxy_failure.dart'; +import 'package:hiddify/singbox/service/singbox_service.dart'; +import 'package:hiddify/utils/custom_loggers.dart'; + +abstract interface class ProxyRepository { + Stream>> watchProxies(); + TaskEither selectProxy( + String groupTag, + String outboundTag, + ); + TaskEither urlTest(String groupTag); +} + +class ProxyRepositoryImpl + with ExceptionHandler, InfraLogger + implements ProxyRepository { + ProxyRepositoryImpl({required this.singbox}); + + final SingboxService singbox; + + @override + Stream>> watchProxies() { + return singbox.watchOutbounds().map((event) { + final groupWithSelected = { + for (final group in event) group.tag: group.selected, + }; + return event + .map( + (e) => ProxyGroupEntity( + tag: e.tag, + type: e.type, + selected: e.selected, + items: e.items + .map( + (e) => ProxyItemEntity( + tag: e.tag, + type: e.type, + urlTestDelay: e.urlTestDelay, + selectedTag: groupWithSelected[e.tag], + ), + ) + .toList(), + ), + ) + .toList(); + }).handleExceptions( + (error, stackTrace) { + loggy.error("error watching proxies", error, stackTrace); + return ProxyUnexpectedFailure(error, stackTrace); + }, + ); + } + + @override + TaskEither selectProxy( + String groupTag, + String outboundTag, + ) { + return exceptionHandler( + () => singbox + .selectOutbound(groupTag, outboundTag) + .mapLeft(ProxyUnexpectedFailure.new) + .run(), + ProxyUnexpectedFailure.new, + ); + } + + @override + TaskEither urlTest(String groupTag) { + return exceptionHandler( + () => singbox.urlTest(groupTag).mapLeft(ProxyUnexpectedFailure.new).run(), + ProxyUnexpectedFailure.new, + ); + } +} diff --git a/lib/features/proxy/model/proxy_entity.dart b/lib/features/proxy/model/proxy_entity.dart new file mode 100644 index 00000000..2dbf96d0 --- /dev/null +++ b/lib/features/proxy/model/proxy_entity.dart @@ -0,0 +1,37 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hiddify/singbox/model/singbox_proxy_type.dart'; + +part 'proxy_entity.freezed.dart'; + +@freezed +class ProxyGroupEntity with _$ProxyGroupEntity { + const ProxyGroupEntity._(); + + const factory ProxyGroupEntity({ + required String tag, + required ProxyType type, + required String selected, + @Default([]) List items, + }) = _ProxyGroupEntity; + + String get name => _sanitizedTag(tag); +} + +@freezed +class ProxyItemEntity with _$ProxyItemEntity { + const ProxyItemEntity._(); + + const factory ProxyItemEntity({ + required String tag, + required ProxyType type, + required int urlTestDelay, + String? selectedTag, + }) = _ProxyItemEntity; + + String get name => _sanitizedTag(tag); + String? get selectedName => + selectedTag == null ? null : _sanitizedTag(selectedTag!); +} + +String _sanitizedTag(String tag) => + tag.replaceFirst(RegExp(r"\§[^]*"), "").trimRight(); diff --git a/lib/features/proxy/model/proxy_failure.dart b/lib/features/proxy/model/proxy_failure.dart new file mode 100644 index 00000000..8fcf1b35 --- /dev/null +++ b/lib/features/proxy/model/proxy_failure.dart @@ -0,0 +1,33 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/model/failures.dart'; + +part 'proxy_failure.freezed.dart'; + +@freezed +sealed class ProxyFailure with _$ProxyFailure, Failure { + const ProxyFailure._(); + + @With() + const factory ProxyFailure.unexpected([ + Object? error, + StackTrace? stackTrace, + ]) = ProxyUnexpectedFailure; + + @With() + const factory ProxyFailure.serviceNotRunning() = ServiceNotRunning; + + @override + ({String type, String? message}) present(TranslationsEn t) { + return switch (this) { + ProxyUnexpectedFailure() => ( + type: t.failure.unexpected, + message: null, + ), + ServiceNotRunning() => ( + type: t.failure.singbox.serviceNotRunning, + message: null, + ), + }; + } +} diff --git a/lib/features/proxies/notifier/proxies_notifier.dart b/lib/features/proxy/overview/proxies_overview_notifier.dart similarity index 72% rename from lib/features/proxies/notifier/proxies_notifier.dart rename to lib/features/proxy/overview/proxies_overview_notifier.dart index 743d3d20..6c3fc4a7 100644 --- a/lib/features/proxies/notifier/proxies_notifier.dart +++ b/lib/features/proxy/overview/proxies_overview_notifier.dart @@ -2,18 +2,19 @@ import 'dart:async'; import 'package:combine/combine.dart'; import 'package:dartx/dartx.dart'; -import 'package:hiddify/core/prefs/prefs.dart'; -import 'package:hiddify/data/data_providers.dart'; -import 'package:hiddify/domain/core_service_failure.dart'; -import 'package:hiddify/domain/singbox/singbox.dart'; -import 'package:hiddify/features/common/connectivity/connectivity_controller.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/preferences/preferences_provider.dart'; +import 'package:hiddify/features/connection/notifier/connection_notifier.dart'; +import 'package:hiddify/features/proxy/data/proxy_data_providers.dart'; +import 'package:hiddify/features/proxy/model/proxy_entity.dart'; +import 'package:hiddify/features/proxy/model/proxy_failure.dart'; import 'package:hiddify/utils/pref_notifier.dart'; import 'package:hiddify/utils/riverpod_utils.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:rxdart/rxdart.dart'; -part 'proxies_notifier.g.dart'; +part 'proxies_overview_notifier.g.dart'; enum ProxiesSort { unsorted, @@ -30,7 +31,7 @@ enum ProxiesSort { @Riverpod(keepAlive: true) class ProxiesSortNotifier extends _$ProxiesSortNotifier { late final _pref = Pref( - ref.watch(sharedPreferencesProvider), + ref.watch(sharedPreferencesProvider).requireValue, "proxies_sort_mode", ProxiesSort.unsorted, mapFrom: ProxiesSort.values.byName, @@ -47,18 +48,18 @@ class ProxiesSortNotifier extends _$ProxiesSortNotifier { } @riverpod -class ProxiesNotifier extends _$ProxiesNotifier with AppLogger { +class ProxiesOverviewNotifier extends _$ProxiesOverviewNotifier with AppLogger { @override - Stream> build() async* { + Stream> build() async* { ref.disposeDelay(const Duration(seconds: 15)); final serviceRunning = await ref.watch(serviceRunningProvider.future); if (!serviceRunning) { - throw const CoreServiceNotRunning(); + throw const ServiceNotRunning(); } final sortBy = ref.watch(proxiesSortNotifierProvider); yield* ref - .watch(coreFacadeProvider) - .watchOutbounds() + .watch(proxyRepositoryProvider) + .watchProxies() .throttleTime( const Duration(milliseconds: 100), leading: false, @@ -75,17 +76,17 @@ class ProxiesNotifier extends _$ProxiesNotifier with AppLogger { .asyncMap((proxies) async => _sortOutbounds(proxies, sortBy)); } - Future> _sortOutbounds( - List outbounds, + Future> _sortOutbounds( + List proxies, ProxiesSort sortBy, ) async { return CombineWorker().execute( () { final groupWithSelected = { - for (final o in outbounds) o.tag: o.selected, + for (final o in proxies) o.tag: o.selected, }; - final sortedOutbounds = []; - for (final group in outbounds) { + final sortedProxies = []; + for (final group in proxies) { final sortedItems = switch (sortBy) { ProxiesSort.name => group.items.sortedBy((e) => e.tag), ProxiesSort.delay => group.items.sortedWith((a, b) { @@ -99,7 +100,7 @@ class ProxiesNotifier extends _$ProxiesNotifier with AppLogger { }), ProxiesSort.unsorted => group.items, }; - final items = []; + final items = []; for (final item in sortedItems) { if (groupWithSelected.keys.contains(item.tag)) { items @@ -108,9 +109,9 @@ class ProxiesNotifier extends _$ProxiesNotifier with AppLogger { items.add(item); } } - sortedOutbounds.add(group.copyWith(items: items)); + sortedProxies.add(group.copyWith(items: items)); } - return sortedOutbounds; + return sortedProxies; }, ); } @@ -121,8 +122,8 @@ class ProxiesNotifier extends _$ProxiesNotifier with AppLogger { ); if (state case AsyncData(value: final outbounds)) { await ref - .read(coreFacadeProvider) - .selectOutbound(groupTag, outboundTag) + .read(proxyRepositoryProvider) + .selectProxy(groupTag, outboundTag) .getOrElse((err) { loggy.warning("error selecting outbound", err); throw err; @@ -140,7 +141,10 @@ class ProxiesNotifier extends _$ProxiesNotifier with AppLogger { Future urlTest(String groupTag) async { loggy.debug("testing group: [$groupTag]"); if (state case AsyncData()) { - await ref.read(coreFacadeProvider).urlTest(groupTag).getOrElse((err) { + await ref + .read(proxyRepositoryProvider) + .urlTest(groupTag) + .getOrElse((err) { loggy.error("error testing group", err); throw err; }).run(); diff --git a/lib/features/proxies/view/proxies_page.dart b/lib/features/proxy/overview/proxies_overview_page.dart similarity index 91% rename from lib/features/proxies/view/proxies_page.dart rename to lib/features/proxy/overview/proxies_overview_page.dart index c5952064..1720473a 100644 --- a/lib/features/proxies/view/proxies_page.dart +++ b/lib/features/proxy/overview/proxies_overview_page.dart @@ -1,21 +1,21 @@ import 'package:flutter/material.dart'; -import 'package:hiddify/core/core_providers.dart'; -import 'package:hiddify/domain/failures.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/model/failures.dart'; import 'package:hiddify/features/common/nested_app_bar.dart'; -import 'package:hiddify/features/proxies/notifier/notifier.dart'; -import 'package:hiddify/features/proxies/widgets/widgets.dart'; +import 'package:hiddify/features/proxy/overview/proxies_overview_notifier.dart'; +import 'package:hiddify/features/proxy/widget/proxy_tile.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -class ProxiesPage extends HookConsumerWidget with PresLogger { - const ProxiesPage({super.key}); +class ProxiesOverviewPage extends HookConsumerWidget with PresLogger { + const ProxiesOverviewPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final t = ref.watch(translationsProvider); - final asyncProxies = ref.watch(proxiesNotifierProvider); - final notifier = ref.watch(proxiesNotifierProvider.notifier); + final asyncProxies = ref.watch(proxiesOverviewNotifierProvider); + final notifier = ref.watch(proxiesOverviewNotifierProvider.notifier); final sortBy = ref.watch(proxiesSortNotifierProvider); final selectActiveProxyMutation = useMutation( diff --git a/lib/features/proxies/widgets/proxy_tile.dart b/lib/features/proxy/widget/proxy_tile.dart similarity index 88% rename from lib/features/proxies/widgets/proxy_tile.dart rename to lib/features/proxy/widget/proxy_tile.dart index 180afc42..e23630e0 100644 --- a/lib/features/proxies/widgets/proxy_tile.dart +++ b/lib/features/proxy/widget/proxy_tile.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:hiddify/domain/singbox/singbox.dart'; +import 'package:hiddify/features/proxy/model/proxy_entity.dart'; import 'package:hiddify/utils/custom_loggers.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -11,7 +11,7 @@ class ProxyTile extends HookConsumerWidget with PresLogger { required this.onSelect, }); - final OutboundGroupItem proxy; + final ProxyItemEntity proxy; final bool selected; final VoidCallback onSelect; @@ -22,7 +22,7 @@ class ProxyTile extends HookConsumerWidget with PresLogger { return ListTile( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), title: Text( - sanitizedTag(proxy.tag), + proxy.name, overflow: TextOverflow.ellipsis, ), leading: Padding( @@ -40,9 +40,9 @@ class ProxyTile extends HookConsumerWidget with PresLogger { TextSpan( text: proxy.type.label, children: [ - if (proxy.selectedTag != null) + if (proxy.selectedName != null) TextSpan( - text: ' (${sanitizedTag(proxy.selectedTag!)})', + text: ' (${proxy.selectedName})', style: Theme.of(context).textTheme.bodySmall, ), ], @@ -61,7 +61,7 @@ class ProxyTile extends HookConsumerWidget with PresLogger { showDialog( context: context, builder: (context) => AlertDialog( - content: SelectionArea(child: Text(sanitizedTag(proxy.tag))), + content: SelectionArea(child: Text(proxy.name)), actions: [ TextButton( onPressed: Navigator.of(context).pop, diff --git a/lib/features/about/view/about_page.dart b/lib/features/settings/about/about_page.dart similarity index 91% rename from lib/features/about/view/about_page.dart rename to lib/features/settings/about/about_page.dart index bbec9e5f..fb7f1b81 100644 --- a/lib/features/about/view/about_page.dart +++ b/lib/features/settings/about/about_page.dart @@ -1,12 +1,14 @@ 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/domain/constants.dart'; -import 'package:hiddify/domain/failures.dart'; -import 'package:hiddify/features/common/app_update_notifier.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/features/common/new_version_dialog.dart'; import 'package:hiddify/gen/assets.gen.dart'; import 'package:hiddify/services/service_providers.dart'; import 'package:hiddify/utils/utils.dart'; @@ -18,7 +20,7 @@ class AboutPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final t = ref.watch(translationsProvider); - final appInfo = ref.watch(appInfoProvider); + final appInfo = ref.watch(appInfoProvider).requireValue; final appUpdate = ref.watch(appUpdateNotifierProvider); ref.listen( diff --git a/lib/features/settings/data/settings_data_providers.dart b/lib/features/settings/data/settings_data_providers.dart new file mode 100644 index 00000000..000f5b73 --- /dev/null +++ b/lib/features/settings/data/settings_data_providers.dart @@ -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(); +} diff --git a/lib/features/settings/data/settings_repository.dart b/lib/features/settings/data/settings_repository.dart new file mode 100644 index 00000000..3aa949ee --- /dev/null +++ b/lib/features/settings/data/settings_repository.dart @@ -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 isIgnoringBatteryOptimizations(); + TaskEither requestIgnoreBatteryOptimizations(); +} + +class SettingsRepositoryImpl + with ExceptionHandler, InfraLogger + implements SettingsRepository { + final _methodChannel = const MethodChannel("app.hiddify.com/platform"); + + @override + TaskEither isIgnoringBatteryOptimizations() { + return exceptionHandler( + () async { + loggy.debug("checking battery optimization status"); + final result = await _methodChannel + .invokeMethod("is_ignoring_battery_optimizations"); + loggy.debug("is ignoring battery optimizations? [$result]"); + return right(result!); + }, + SettingsUnexpectedFailure.new, + ); + } + + @override + TaskEither requestIgnoreBatteryOptimizations() { + return exceptionHandler( + () async { + loggy.debug("requesting ignore battery optimization"); + final result = await _methodChannel + .invokeMethod("request_ignore_battery_optimizations"); + loggy.debug("ignore battery optimization result: [$result]"); + return right(result!); + }, + SettingsUnexpectedFailure.new, + ); + } +} diff --git a/lib/features/settings/model/settings_failure.dart b/lib/features/settings/model/settings_failure.dart new file mode 100644 index 00000000..345c03a4 --- /dev/null +++ b/lib/features/settings/model/settings_failure.dart @@ -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() + 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, + ), + }; + } +} diff --git a/lib/features/settings/notifier/platform_settings_notifier.dart b/lib/features/settings/notifier/platform_settings_notifier.dart new file mode 100644 index 00000000..2afb3af1 --- /dev/null +++ b/lib/features/settings/notifier/platform_settings_notifier.dart @@ -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 build() async { + return ref + .watch(settingsRepositoryProvider) + .isIgnoringBatteryOptimizations() + .getOrElse((l) => false) + .run(); + } + + Future request() async { + await ref + .read(settingsRepositoryProvider) + .requestIgnoreBatteryOptimizations() + .run(); + await Future.delayed(const Duration(seconds: 1)); + ref.invalidateSelf(); + } +} diff --git a/lib/features/settings/view/settings_page.dart b/lib/features/settings/overview/settings_overview_page.dart similarity index 85% rename from lib/features/settings/view/settings_page.dart rename to lib/features/settings/overview/settings_overview_page.dart index 164641fe..3fee58b2 100644 --- a/lib/features/settings/view/settings_page.dart +++ b/lib/features/settings/overview/settings_overview_page.dart @@ -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) { diff --git a/lib/features/settings/view/config_options_page.dart b/lib/features/settings/view/config_options_page.dart deleted file mode 100644 index 50899b71..00000000 --- a/lib/features/settings/view/config_options_page.dart +++ /dev/null @@ -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), - ], - ), - ); - } -} diff --git a/lib/features/settings/view/view.dart b/lib/features/settings/view/view.dart deleted file mode 100644 index c94412af..00000000 --- a/lib/features/settings/view/view.dart +++ /dev/null @@ -1,3 +0,0 @@ -export 'config_options_page.dart'; -export 'per_app_proxy_page.dart'; -export 'settings_page.dart'; diff --git a/lib/features/settings/widgets/advanced_setting_tiles.dart b/lib/features/settings/widgets/advanced_setting_tiles.dart index 23f7a2d3..f82281f9 100644 --- a/lib/features/settings/widgets/advanced_setting_tiles.dart +++ b/lib/features/settings/widgets/advanced_setting_tiles.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 { diff --git a/lib/features/settings/widgets/general_setting_tiles.dart b/lib/features/settings/widgets/general_setting_tiles.dart index d042c1d4..6c246c43 100644 --- a/lib/features/settings/widgets/general_setting_tiles.dart +++ b/lib/features/settings/widgets/general_setting_tiles.dart @@ -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( @@ -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); } }, ), diff --git a/lib/features/settings/widgets/platform_settings_tiles.dart b/lib/features/settings/widgets/platform_settings_tiles.dart index 11f83834..cdeb18e4 100644 --- a/lib/features/settings/widgets/platform_settings_tiles.dart +++ b/lib/features/settings/widgets/platform_settings_tiles.dart @@ -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 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(); }, ); diff --git a/lib/features/settings/widgets/settings_input_dialog.dart b/lib/features/settings/widgets/settings_input_dialog.dart index 394158da..84a552be 100644 --- a/lib/features/settings/widgets/settings_input_dialog.dart +++ b/lib/features/settings/widgets/settings_input_dialog.dart @@ -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'; diff --git a/lib/features/stats/data/stats_data_providers.dart b/lib/features/stats/data/stats_data_providers.dart new file mode 100644 index 00000000..e352369c --- /dev/null +++ b/lib/features/stats/data/stats_data_providers.dart @@ -0,0 +1,10 @@ +import 'package:hiddify/features/stats/data/stats_repository.dart'; +import 'package:hiddify/singbox/service/singbox_service_provider.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'stats_data_providers.g.dart'; + +@Riverpod(keepAlive: true) +StatsRepository statsRepository(StatsRepositoryRef ref) { + return StatsRepositoryImpl(singbox: ref.watch(singboxServiceProvider)); +} diff --git a/lib/features/stats/data/stats_repository.dart b/lib/features/stats/data/stats_repository.dart new file mode 100644 index 00000000..49025344 --- /dev/null +++ b/lib/features/stats/data/stats_repository.dart @@ -0,0 +1,33 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:hiddify/core/utils/exception_handler.dart'; +import 'package:hiddify/features/stats/model/stats_entity.dart'; +import 'package:hiddify/features/stats/model/stats_failure.dart'; +import 'package:hiddify/singbox/service/singbox_service.dart'; +import 'package:hiddify/utils/custom_loggers.dart'; + +abstract interface class StatsRepository { + Stream> watchStats(); +} + +class StatsRepositoryImpl + with ExceptionHandler, InfraLogger + implements StatsRepository { + StatsRepositoryImpl({required this.singbox}); + + final SingboxService singbox; + + @override + Stream> watchStats() { + return singbox + .watchStats() + .map( + (event) => StatsEntity( + uplink: event.uplink, + downlink: event.downlink, + uplinkTotal: event.downlink, + downlinkTotal: event.downlinkTotal, + ), + ) + .handleExceptions(StatsUnexpectedFailure.new); + } +} diff --git a/lib/features/stats/model/stats_entity.dart b/lib/features/stats/model/stats_entity.dart new file mode 100644 index 00000000..13bc86f2 --- /dev/null +++ b/lib/features/stats/model/stats_entity.dart @@ -0,0 +1,22 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'stats_entity.freezed.dart'; + +@freezed +class StatsEntity with _$StatsEntity { + const StatsEntity._(); + + const factory StatsEntity({ + required int uplink, + required int downlink, + required int uplinkTotal, + required int downlinkTotal, + }) = _StatsEntity; + + factory StatsEntity.empty() => const StatsEntity( + uplink: 0, + downlink: 0, + uplinkTotal: 0, + downlinkTotal: 0, + ); +} diff --git a/lib/features/stats/model/stats_failure.dart b/lib/features/stats/model/stats_failure.dart new file mode 100644 index 00000000..bce4ead3 --- /dev/null +++ b/lib/features/stats/model/stats_failure.dart @@ -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 'stats_failure.freezed.dart'; + +@freezed +sealed class StatsFailure with _$StatsFailure, Failure { + const StatsFailure._(); + + @With() + const factory StatsFailure.unexpected([ + Object? error, + StackTrace? stackTrace, + ]) = StatsUnexpectedFailure; + + @override + ({String type, String? message}) present(TranslationsEn t) { + return switch (this) { + StatsUnexpectedFailure() => ( + type: t.failure.unexpected, + message: null, + ), + }; + } +} diff --git a/lib/features/stats/notifier/stats_notifier.dart b/lib/features/stats/notifier/stats_notifier.dart new file mode 100644 index 00000000..d04adbf8 --- /dev/null +++ b/lib/features/stats/notifier/stats_notifier.dart @@ -0,0 +1,23 @@ +import 'package:hiddify/features/connection/notifier/connection_notifier.dart'; +import 'package:hiddify/features/stats/data/stats_data_providers.dart'; +import 'package:hiddify/features/stats/model/stats_entity.dart'; +import 'package:hiddify/utils/custom_loggers.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'stats_notifier.g.dart'; + +@riverpod +class StatsNotifier extends _$StatsNotifier with AppLogger { + @override + Stream build() async* { + final serviceRunning = await ref.watch(serviceRunningProvider.future); + if (serviceRunning) { + yield* ref + .watch(statsRepositoryProvider) + .watchStats() + .map((event) => event.getOrElse((_) => StatsEntity.empty())); + } else { + yield* Stream.value(StatsEntity.empty()); + } + } +} diff --git a/lib/features/common/side_bar_stats_overview.dart b/lib/features/stats/widget/side_bar_stats_overview.dart similarity index 89% rename from lib/features/common/side_bar_stats_overview.dart rename to lib/features/stats/widget/side_bar_stats_overview.dart index b3395cee..a1b6ff6b 100644 --- a/lib/features/common/side_bar_stats_overview.dart +++ b/lib/features/stats/widget/side_bar_stats_overview.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; -import 'package:hiddify/core/core_providers.dart'; -import 'package:hiddify/domain/singbox/singbox.dart'; -import 'package:hiddify/features/common/stats_provider.dart'; -import 'package:hiddify/utils/utils.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/features/stats/model/stats_entity.dart'; +import 'package:hiddify/features/stats/notifier/stats_notifier.dart'; +import 'package:hiddify/utils/number_formatters.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; class SideBarStatsOverview extends HookConsumerWidget { @@ -13,7 +13,8 @@ class SideBarStatsOverview extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final t = ref.watch(translationsProvider); - final stats = ref.watch(statsProvider).asData?.value ?? CoreStatus.empty(); + final stats = + ref.watch(statsNotifierProvider).asData?.value ?? StatsEntity.empty(); return Padding( padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), diff --git a/lib/features/system_tray/system_tray_controller.dart b/lib/features/system_tray/system_tray_controller.dart index 04108033..f4c0906f 100644 --- a/lib/features/system_tray/system_tray_controller.dart +++ b/lib/features/system_tray/system_tray_controller.dart @@ -1,14 +1,15 @@ import 'dart:io'; -import 'package:hiddify/core/core_providers.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/model/constants.dart'; import 'package:hiddify/core/router/router.dart'; -import 'package:hiddify/data/repository/config_options_store.dart'; -import 'package:hiddify/domain/connectivity/connectivity.dart'; -import 'package:hiddify/domain/constants.dart'; -import 'package:hiddify/domain/singbox/singbox.dart'; -import 'package:hiddify/features/common/connectivity/connectivity_controller.dart'; import 'package:hiddify/features/common/window/window_controller.dart'; +import 'package:hiddify/features/config_option/model/config_option_patch.dart'; +import 'package:hiddify/features/config_option/notifier/config_option_notifier.dart'; +import 'package:hiddify/features/connection/model/connection_status.dart'; +import 'package:hiddify/features/connection/notifier/connection_notifier.dart'; import 'package:hiddify/gen/assets.gen.dart'; +import 'package:hiddify/singbox/model/singbox_config_enum.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:tray_manager/tray_manager.dart'; @@ -31,11 +32,13 @@ class SystemTrayController extends _$SystemTrayController _initialized = true; } - final connection = switch (ref.watch(connectivityControllerProvider)) { + final connection = switch (ref.watch(connectionNotifierProvider)) { AsyncData(:final value) => value, _ => const Disconnected(), }; - final serviceMode = ref.watch(serviceModeStoreProvider); + final serviceMode = await ref + .watch(configOptionNotifierProvider.future) + .then((value) => value.serviceMode); final t = ref.watch(translationsProvider); final destinations = <(String label, String location)>[ @@ -79,8 +82,8 @@ class SystemTrayController extends _$SystemTrayController final newMode = ServiceMode.values.byName(menuItem.key!); loggy.debug("switching service mode: [$newMode]"); await ref - .read(serviceModeStoreProvider.notifier) - .update(newMode); + .read(configOptionNotifierProvider.notifier) + .updateOption(ConfigOptionPatch(serviceMode: newMode)); }, ), ), @@ -137,11 +140,11 @@ class SystemTrayController extends _$SystemTrayController } Future handleClickSetAsSystemProxy(MenuItem menuItem) async { - return ref.read(connectivityControllerProvider.notifier).toggleConnection(); + return ref.read(connectionNotifierProvider.notifier).toggleConnection(); } Future handleClickExitApp(MenuItem menuItem) async { - await ref.read(connectivityControllerProvider.notifier).abortConnection(); + await ref.read(connectionNotifierProvider.notifier).abortConnection(); await trayManager.destroy(); return ref.read(windowControllerProvider.notifier).quit(); } diff --git a/lib/main_dev.dart b/lib/main_dev.dart index 8dcdbd01..0e845d6b 100644 --- a/lib/main_dev.dart +++ b/lib/main_dev.dart @@ -1,6 +1,6 @@ import 'package:flutter/widgets.dart'; import 'package:hiddify/bootstrap.dart'; -import 'package:hiddify/domain/environment.dart'; +import 'package:hiddify/core/model/environment.dart'; void main() async { final widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); diff --git a/lib/main_prod.dart b/lib/main_prod.dart index 81b713ca..43110c09 100644 --- a/lib/main_prod.dart +++ b/lib/main_prod.dart @@ -1,6 +1,6 @@ import 'package:flutter/widgets.dart'; import 'package:hiddify/bootstrap.dart'; -import 'package:hiddify/domain/environment.dart'; +import 'package:hiddify/core/model/environment.dart'; void main() async { final widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); diff --git a/lib/services/auto_start_service.dart b/lib/services/auto_start_service.dart index bb0a31bf..7c60739e 100644 --- a/lib/services/auto_start_service.dart +++ b/lib/services/auto_start_service.dart @@ -1,6 +1,6 @@ import 'dart:io'; -import 'package:hiddify/core/core_providers.dart'; +import 'package:hiddify/core/app_info/app_info_provider.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:launch_at_startup/launch_at_startup.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -13,7 +13,7 @@ class AutoStartService extends _$AutoStartService with InfraLogger { Future build() async { loggy.debug("initializing"); if (!PlatformUtils.isDesktop) return false; - final appInfo = ref.watch(appInfoProvider); + final appInfo = ref.watch(appInfoProvider).requireValue; launchAtStartup.setup( appName: appInfo.name, appPath: Platform.resolvedExecutable, diff --git a/lib/services/platform_services.dart b/lib/services/platform_services.dart index 267f467c..71aadcaa 100644 --- a/lib/services/platform_services.dart +++ b/lib/services/platform_services.dart @@ -1,19 +1,10 @@ -import 'dart:convert'; -import 'dart:ffi'; import 'dart:io'; import 'package:flutter/services.dart'; import 'package:fpdart/fpdart.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hiddify/services/files_editor_service.dart'; -import 'package:hiddify/utils/ffi_utils.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:posix/posix.dart'; -import 'package:win32/win32.dart'; - -part 'platform_services.freezed.dart'; -part 'platform_services.g.dart'; class PlatformServices with InfraLogger { final _methodChannel = const MethodChannel("app.hiddify.com/platform"); @@ -47,130 +38,4 @@ class PlatformServices with InfraLogger { }, ); } - - Future hasPrivilege() async { - try { - if (Platform.isWindows) { - bool isElevated = false; - withMemory(sizeOf(), (phToken) { - withMemory(sizeOf(), (pReturnedSize) { - withMemory(sizeOf<_TokenElevation>(), - (pElevation) { - if (OpenProcessToken( - GetCurrentProcess(), - TOKEN_QUERY, - phToken.cast(), - ) == - 1) { - if (GetTokenInformation( - phToken.value, - TOKEN_INFORMATION_CLASS.TokenElevation, - pElevation, - sizeOf<_TokenElevation>(), - pReturnedSize, - ) == - 1) { - isElevated = pElevation.ref.tokenIsElevated != 0; - } - } - if (phToken.value != 0) { - CloseHandle(phToken.value); - } - }); - }); - }); - return isElevated; - } else if (Platform.isLinux || Platform.isMacOS) { - final euid = geteuid(); - return euid == 0; - } else { - return true; - } - } catch (e) { - loggy.warning("error checking privilege", e); - return true; // return true so core handles it - } - } - - TaskEither isIgnoringBatteryOptimizations() { - return TaskEither( - () async { - loggy.debug("checking battery optimization status"); - final result = await _methodChannel - .invokeMethod("is_ignoring_battery_optimizations"); - loggy.debug("is ignoring battery optimizations? [$result]"); - return right(result!); - }, - ); - } - - TaskEither requestIgnoreBatteryOptimizations() { - return TaskEither( - () async { - loggy.debug("requesting ignore battery optimization"); - final result = await _methodChannel - .invokeMethod("request_ignore_battery_optimizations"); - loggy.debug("ignore battery optimization result: [$result]"); - return right(result!); - }, - ); - } - - TaskEither> getInstalledPackages() { - return TaskEither( - () async { - loggy.debug("getting installed packages info"); - final result = - await _methodChannel.invokeMethod("get_installed_packages"); - if (result == null) return left("null response"); - return right( - (jsonDecode(result) as List).map((e) { - return InstalledPackageInfo.fromJson(e as Map); - }).toList(), - ); - }, - ); - } - - TaskEither getPackageIcon( - String packageName, - ) { - return TaskEither( - () async { - loggy.debug("getting package [$packageName] icon"); - final result = await _methodChannel.invokeMethod( - "get_package_icon", - {"packageName": packageName}, - ); - if (result == null) return left("null response"); - final Uint8List decoded; - try { - decoded = base64.decode(result); - } catch (e) { - return left("error parsing base64 response"); - } - return right(decoded); - }, - ); - } -} - -@freezed -class InstalledPackageInfo with _$InstalledPackageInfo { - @JsonSerializable(fieldRename: FieldRename.kebab) - const factory InstalledPackageInfo({ - required String packageName, - required String name, - required bool isSystemApp, - }) = _InstalledPackageInfo; - - factory InstalledPackageInfo.fromJson(Map json) => - _$InstalledPackageInfoFromJson(json); -} - -sealed class _TokenElevation extends Struct { - /// A nonzero value if the token has elevated privileges; - /// otherwise, a zero value. - @Int32() - external int tokenIsElevated; } diff --git a/lib/services/service_providers.dart b/lib/services/service_providers.dart index 6b92132a..c568c9c0 100644 --- a/lib/services/service_providers.dart +++ b/lib/services/service_providers.dart @@ -1,6 +1,5 @@ import 'package:hiddify/services/files_editor_service.dart'; import 'package:hiddify/services/platform_services.dart'; -import 'package:hiddify/services/singbox/singbox_service.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'service_providers.g.dart'; @@ -9,9 +8,6 @@ part 'service_providers.g.dart'; FilesEditorService filesEditorService(FilesEditorServiceRef ref) => FilesEditorService(ref.watch(platformServicesProvider)); -@Riverpod(keepAlive: true) -SingboxService singboxService(SingboxServiceRef ref) => SingboxService(); - @Riverpod(keepAlive: true) PlatformServices platformServices(PlatformServicesRef ref) => PlatformServices(); diff --git a/lib/services/singbox/shared.dart b/lib/services/singbox/shared.dart deleted file mode 100644 index 6c34b189..00000000 --- a/lib/services/singbox/shared.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:hiddify/domain/connectivity/connectivity.dart'; -import 'package:hiddify/domain/core_service_failure.dart'; - -mixin ServiceStatus { - ConnectionStatus mapEventToStatus(dynamic event) { - final status = event['status'] as String; - late ConnectionStatus connectionStatus; - switch (status) { - case "Stopped": - final failure = event["alert"] as String?; - final message = event["message"] as String?; - connectionStatus = ConnectionStatus.disconnected( - switch (failure) { - null => null, - "RequestVPNPermission" => MissingVpnPermission(message), - "RequestNotificationPermission" => - MissingNotificationPermission(message), - "EmptyConfiguration" || - "StartCommandServer" || - "CreateService" || - "StartService" => - CoreConnectionFailure(fromServiceAlert(failure, message)), - _ => const UnexpectedConnectionFailure(), - }, - ); - case "Starting": - connectionStatus = const Connecting(); - case "Started": - connectionStatus = const Connected(); - case "Stopping": - connectionStatus = const Disconnecting(); - } - return connectionStatus; - } - - CoreServiceFailure fromServiceAlert(String key, String? message) { - return switch (key) { - "EmptyConfiguration" => InvalidConfig(message), - "StartCommandServer" || - "CreateService" => - CoreServiceCreateFailure(message), - "StartService" => CoreServiceStartFailure(message), - _ => const CoreServiceOtherFailure(), - }; - } -} diff --git a/lib/services/singbox/singbox_service.dart b/lib/services/singbox/singbox_service.dart deleted file mode 100644 index ac1d4d90..00000000 --- a/lib/services/singbox/singbox_service.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'dart:io'; - -import 'package:fpdart/fpdart.dart'; -import 'package:hiddify/domain/connectivity/connectivity.dart'; -import 'package:hiddify/domain/singbox/singbox.dart'; -import 'package:hiddify/services/singbox/ffi_singbox_service.dart'; -import 'package:hiddify/services/singbox/mobile_singbox_service.dart'; - -abstract interface class SingboxService { - factory SingboxService() { - if (Platform.isAndroid || Platform.isIOS) { - return MobileSingboxService(); - } else if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) { - return FFISingboxService(); - } - throw Exception("unsupported platform"); - } - - Future init(); - - TaskEither setup( - String baseDir, - String workingDir, - String tempDir, - bool debug, - ); - - TaskEither parseConfig( - String path, - String tempPath, - bool debug, - ); - - TaskEither changeConfigOptions(ConfigOptions options); - - TaskEither generateConfig( - String path, - ); - - TaskEither start(String configPath, bool disableMemoryLimit); - - TaskEither stop(); - - TaskEither restart(String configPath, bool disableMemoryLimit); - - Stream watchOutbounds(); - - TaskEither selectOutbound(String groupTag, String outboundTag); - - TaskEither urlTest(String groupTag); - - Stream watchConnectionStatus(); - - Stream watchStats(); - - Stream> watchLogs(String path); - - TaskEither clearLogs(); -} diff --git a/lib/singbox/model/singbox_config_enum.dart b/lib/singbox/model/singbox_config_enum.dart new file mode 100644 index 00000000..a7da0a56 --- /dev/null +++ b/lib/singbox/model/singbox_config_enum.dart @@ -0,0 +1,68 @@ +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/utils/platform_utils.dart'; +import 'package:json_annotation/json_annotation.dart'; + +enum ServiceMode { + proxy, + systemProxy, + tun; + + static ServiceMode get defaultMode => + PlatformUtils.isDesktop ? systemProxy : tun; + + static List get choices { + if (PlatformUtils.isDesktop) { + return values; + } + return [proxy, tun]; + } + + String present(TranslationsEn t) => switch (this) { + proxy => t.settings.config.serviceModes.proxy, + systemProxy => t.settings.config.serviceModes.systemProxy, + tun => t.settings.config.serviceModes.tun, + }; +} + +@JsonEnum(valueField: 'key') +enum IPv6Mode { + disable("ipv4_only"), + enable("prefer_ipv4"), + prefer("prefer_ipv6"), + only("ipv6_only"); + + const IPv6Mode(this.key); + + final String key; + + String present(TranslationsEn t) => switch (this) { + disable => t.settings.config.ipv6Modes.disable, + enable => t.settings.config.ipv6Modes.enable, + prefer => t.settings.config.ipv6Modes.prefer, + only => t.settings.config.ipv6Modes.only, + }; +} + +@JsonEnum(valueField: 'key') +enum DomainStrategy { + auto(""), + preferIpv6("prefer_ipv6"), + preferIpv4("prefer_ipv4"), + ipv4Only("ipv4_only"), + ipv6Only("ipv6_only"); + + const DomainStrategy(this.key); + + final String key; + + String get displayName => switch (this) { + auto => "auto", + _ => key, + }; +} + +enum TunImplementation { + mixed, + system, + gVisor; +} diff --git a/lib/singbox/model/singbox_config_option.dart b/lib/singbox/model/singbox_config_option.dart new file mode 100644 index 00000000..0a11102a --- /dev/null +++ b/lib/singbox/model/singbox_config_option.dart @@ -0,0 +1,62 @@ +import 'dart:convert'; + +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hiddify/features/log/model/log_level.dart'; +import 'package:hiddify/singbox/model/singbox_config_enum.dart'; +import 'package:hiddify/singbox/model/singbox_rule.dart'; + +part 'singbox_config_option.freezed.dart'; +part 'singbox_config_option.g.dart'; + +@freezed +class SingboxConfigOption with _$SingboxConfigOption { + const SingboxConfigOption._(); + + @JsonSerializable(fieldRename: FieldRename.kebab) + const factory SingboxConfigOption({ + required bool executeConfigAsIs, + required LogLevel logLevel, + required bool resolveDestination, + required IPv6Mode ipv6Mode, + required String remoteDnsAddress, + required DomainStrategy remoteDnsDomainStrategy, + required String directDnsAddress, + required DomainStrategy directDnsDomainStrategy, + required int mixedPort, + required int localDnsPort, + required TunImplementation tunImplementation, + required int mtu, + required bool strictRoute, + required String connectionTestUrl, + @IntervalConverter() required Duration urlTestInterval, + required bool enableClashApi, + required int clashApiPort, + required bool enableTun, + required bool setSystemProxy, + required bool bypassLan, + required bool enableFakeDns, + required bool independentDnsCache, + required String geoipPath, + required String geositePath, + required List rules, + }) = _SingboxConfigOption; + + String format() { + const encoder = JsonEncoder.withIndent(' '); + return encoder.convert(toJson()); + } + + factory SingboxConfigOption.fromJson(Map json) => + _$SingboxConfigOptionFromJson(json); +} + +class IntervalConverter implements JsonConverter { + const IntervalConverter(); + + @override + Duration fromJson(String json) => + Duration(minutes: int.parse(json.replaceAll("m", ""))); + + @override + String toJson(Duration object) => "${object.inMinutes}m"; +} diff --git a/lib/singbox/model/singbox_outbound.dart b/lib/singbox/model/singbox_outbound.dart new file mode 100644 index 00000000..556c40d7 --- /dev/null +++ b/lib/singbox/model/singbox_outbound.dart @@ -0,0 +1,40 @@ +import 'package:dartx/dartx.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hiddify/singbox/model/singbox_proxy_type.dart'; + +part 'singbox_outbound.freezed.dart'; +part 'singbox_outbound.g.dart'; + +@freezed +class SingboxOutboundGroup with _$SingboxOutboundGroup { + @JsonSerializable(fieldRename: FieldRename.kebab) + const factory SingboxOutboundGroup({ + required String tag, + @JsonKey(fromJson: _typeFromJson) required ProxyType type, + required String selected, + @Default([]) List items, + }) = _SingboxOutboundGroup; + + factory SingboxOutboundGroup.fromJson(Map json) => + _$SingboxOutboundGroupFromJson(json); +} + +@freezed +class SingboxOutboundGroupItem with _$SingboxOutboundGroupItem { + const SingboxOutboundGroupItem._(); + + @JsonSerializable(fieldRename: FieldRename.kebab) + const factory SingboxOutboundGroupItem({ + required String tag, + @JsonKey(fromJson: _typeFromJson) required ProxyType type, + required int urlTestDelay, + }) = _SingboxOutboundGroupItem; + + factory SingboxOutboundGroupItem.fromJson(Map json) => + _$SingboxOutboundGroupItemFromJson(json); +} + +ProxyType _typeFromJson(dynamic type) => + ProxyType.values + .firstOrNullWhere((e) => e.key == (type as String?)?.toLowerCase()) ?? + ProxyType.unknown; diff --git a/lib/domain/singbox/proxy_type.dart b/lib/singbox/model/singbox_proxy_type.dart similarity index 100% rename from lib/domain/singbox/proxy_type.dart rename to lib/singbox/model/singbox_proxy_type.dart diff --git a/lib/singbox/model/singbox_rule.dart b/lib/singbox/model/singbox_rule.dart new file mode 100644 index 00000000..b927ffee --- /dev/null +++ b/lib/singbox/model/singbox_rule.dart @@ -0,0 +1,35 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'singbox_rule.freezed.dart'; +part 'singbox_rule.g.dart'; + +@freezed +class SingboxRule with _$SingboxRule { + const SingboxRule._(); + + @JsonSerializable(fieldRename: FieldRename.kebab) + const factory SingboxRule({ + String? domains, + String? ip, + String? port, + String? protocol, + @Default(RuleNetwork.tcpAndUdp) RuleNetwork network, + @Default(RuleOutbound.proxy) RuleOutbound outbound, + }) = _SingboxRule; + + factory SingboxRule.fromJson(Map json) => + _$SingboxRuleFromJson(json); +} + +enum RuleOutbound { proxy, bypass, block } + +@JsonEnum(valueField: 'key') +enum RuleNetwork { + tcpAndUdp(""), + tcp("tcp"), + udp("udp"); + + const RuleNetwork(this.key); + + final String? key; +} diff --git a/lib/singbox/model/singbox_stats.dart b/lib/singbox/model/singbox_stats.dart new file mode 100644 index 00000000..b0badbeb --- /dev/null +++ b/lib/singbox/model/singbox_stats.dart @@ -0,0 +1,22 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'singbox_stats.freezed.dart'; +part 'singbox_stats.g.dart'; + +@freezed +class SingboxStats with _$SingboxStats { + const SingboxStats._(); + + @JsonSerializable(fieldRename: FieldRename.kebab) + const factory SingboxStats({ + required int connectionsIn, + required int connectionsOut, + required int uplink, + required int downlink, + required int uplinkTotal, + required int downlinkTotal, + }) = _SingboxStats; + + factory SingboxStats.fromJson(Map json) => + _$SingboxStatsFromJson(json); +} diff --git a/lib/singbox/model/singbox_status.dart b/lib/singbox/model/singbox_status.dart new file mode 100644 index 00000000..88d9d1cc --- /dev/null +++ b/lib/singbox/model/singbox_status.dart @@ -0,0 +1,48 @@ +import 'package:dartx/dartx.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'singbox_status.freezed.dart'; + +@freezed +sealed class SingboxStatus with _$SingboxStatus { + const SingboxStatus._(); + + const factory SingboxStatus.stopped({ + SingboxAlert? alert, + String? message, + }) = SingboxStopped; + const factory SingboxStatus.starting() = SingboxStarting; + const factory SingboxStatus.started() = SingboxStarted; + const factory SingboxStatus.stopping() = SingboxStopping; + + factory SingboxStatus.fromEvent(dynamic event) { + switch (event) { + case { + "status": "Stopped", + "alert": final String? alertStr, + "message": final String? messageStr, + }: + final alert = SingboxAlert.values.firstOrNullWhere( + (e) => alertStr?.toLowerCase() == e.name.toLowerCase(), + ); + return SingboxStatus.stopped(alert: alert, message: messageStr); + case {"status": "Starting"}: + return const SingboxStarting(); + case {"status": "Started"}: + return const SingboxStarted(); + case {"status": "Stopping"}: + return const SingboxStopping(); + default: + throw Exception("unexpected status [$event]"); + } + } +} + +enum SingboxAlert { + requestVPNPermission, + requestNotificationPermission, + emptyConfiguration, + startCommandServer, + createService, + startService; +} diff --git a/lib/services/singbox/ffi_singbox_service.dart b/lib/singbox/service/ffi_singbox_service.dart similarity index 84% rename from lib/services/singbox/ffi_singbox_service.dart rename to lib/singbox/service/ffi_singbox_service.dart index 62bcf6b7..e7bbeede 100644 --- a/lib/services/singbox/ffi_singbox_service.dart +++ b/lib/singbox/service/ffi_singbox_service.dart @@ -7,11 +7,13 @@ import 'dart:isolate'; import 'package:combine/combine.dart'; import 'package:ffi/ffi.dart'; import 'package:fpdart/fpdart.dart'; -import 'package:hiddify/domain/connectivity/connectivity.dart'; -import 'package:hiddify/domain/singbox/singbox.dart'; +import 'package:hiddify/core/model/directories.dart'; import 'package:hiddify/gen/singbox_generated_bindings.dart'; -import 'package:hiddify/services/singbox/shared.dart'; -import 'package:hiddify/services/singbox/singbox_service.dart'; +import 'package:hiddify/singbox/model/singbox_config_option.dart'; +import 'package:hiddify/singbox/model/singbox_outbound.dart'; +import 'package:hiddify/singbox/model/singbox_stats.dart'; +import 'package:hiddify/singbox/model/singbox_status.dart'; +import 'package:hiddify/singbox/service/singbox_service.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:loggy/loggy.dart'; import 'package:path/path.dart' as p; @@ -20,15 +22,13 @@ import 'package:watcher/watcher.dart'; final _logger = Loggy('FFISingboxService'); -class FFISingboxService - with ServiceStatus, InfraLogger - implements SingboxService { +class FFISingboxService with InfraLogger implements SingboxService { static final SingboxNativeLibrary _box = _gen(); - late final ValueStream _connectionStatus; - late final ReceivePort _connectionStatusReceiver; - Stream? _serviceStatsStream; - Stream? _outboundsStream; + late final ValueStream _status; + late final ReceivePort _statusReceiver; + Stream? _serviceStatsStream; + Stream>? _outboundsStream; static SingboxNativeLibrary _gen() { String fullPath = ""; @@ -50,34 +50,32 @@ class FFISingboxService @override Future init() async { loggy.debug("initializing"); - _connectionStatusReceiver = ReceivePort('service status receiver'); - final source = _connectionStatusReceiver + _statusReceiver = ReceivePort('service status receiver'); + final source = _statusReceiver .asBroadcastStream() - .map((event) => jsonDecode(event as String) as Map) - .map(mapEventToStatus); - _connectionStatus = ValueConnectableStream.seeded( + .map((event) => jsonDecode(event as String)) + .map(SingboxStatus.fromEvent); + _status = ValueConnectableStream.seeded( source, - const ConnectionStatus.disconnected(), + const SingboxStopped(), ).autoConnect(); } @override TaskEither setup( - String baseDir, - String workingDir, - String tempDir, + Directories directories, bool debug, ) { - final port = _connectionStatusReceiver.sendPort.nativePort; + final port = _statusReceiver.sendPort.nativePort; return TaskEither( () => CombineWorker().execute( () { _box.setupOnce(NativeApi.initializeApiDLData); final err = _box .setup( - baseDir.toNativeUtf8().cast(), - workingDir.toNativeUtf8().cast(), - tempDir.toNativeUtf8().cast(), + directories.baseDir.path.toNativeUtf8().cast(), + directories.workingDir.path.toNativeUtf8().cast(), + directories.tempDir.path.toNativeUtf8().cast(), port, debug ? 1 : 0, ) @@ -93,7 +91,7 @@ class FFISingboxService } @override - TaskEither parseConfig( + TaskEither validateConfigByPath( String path, String tempPath, bool debug, @@ -119,7 +117,7 @@ class FFISingboxService } @override - TaskEither changeConfigOptions(ConfigOptions options) { + TaskEither changeOptions(SingboxConfigOption options) { return TaskEither( () => CombineWorker().execute( () { @@ -138,7 +136,7 @@ class FFISingboxService } @override - TaskEither generateConfig( + TaskEither generateFullConfigByPath( String path, ) { return TaskEither( @@ -219,10 +217,10 @@ class FFISingboxService } @override - Stream watchConnectionStatus() => _connectionStatus; + Stream watchStatus() => _status; @override - Stream watchStats() { + Stream watchStats() { if (_serviceStatsStream != null) return _serviceStatsStream!; final receiver = ReceivePort('service stats receiver'); final statusStream = receiver.asBroadcastStream( @@ -242,7 +240,9 @@ class FFISingboxService loggy.error("[service stats client] error received: $event"); throw event.replaceFirst('error:', ""); } - return event; + return SingboxStats.fromJson( + jsonDecode(event) as Map, + ); } loggy.error("[service status client] unexpected type, msg: $event"); throw "invalid type"; @@ -262,7 +262,7 @@ class FFISingboxService } @override - Stream watchOutbounds() { + Stream> watchOutbounds() { if (_outboundsStream != null) return _outboundsStream!; final receiver = ReceivePort('outbounds receiver'); final outboundsStream = receiver.asBroadcastStream( @@ -282,7 +282,9 @@ class FFISingboxService loggy.error("[group client] error received: $event"); throw event.replaceFirst('error:', ""); } - return event; + return (jsonDecode(event) as List).map((e) { + return SingboxOutboundGroup.fromJson(e as Map); + }).toList(); } loggy.error("[group client] unexpected type, msg: $event"); throw "invalid type"; diff --git a/lib/services/singbox/mobile_singbox_service.dart b/lib/singbox/service/platform_singbox_service.dart similarity index 68% rename from lib/services/singbox/mobile_singbox_service.dart rename to lib/singbox/service/platform_singbox_service.dart index f4440772..52cb7f66 100644 --- a/lib/services/singbox/mobile_singbox_service.dart +++ b/lib/singbox/service/platform_singbox_service.dart @@ -2,48 +2,51 @@ import 'dart:convert'; import 'package:flutter/services.dart'; import 'package:fpdart/fpdart.dart'; -import 'package:hiddify/domain/connectivity/connection_status.dart'; -import 'package:hiddify/domain/singbox/config_options.dart'; -import 'package:hiddify/services/singbox/shared.dart'; -import 'package:hiddify/services/singbox/singbox_service.dart'; -import 'package:hiddify/utils/utils.dart'; +import 'package:hiddify/core/model/directories.dart'; +import 'package:hiddify/singbox/model/singbox_config_option.dart'; +import 'package:hiddify/singbox/model/singbox_outbound.dart'; +import 'package:hiddify/singbox/model/singbox_stats.dart'; +import 'package:hiddify/singbox/model/singbox_status.dart'; +import 'package:hiddify/singbox/service/singbox_service.dart'; +import 'package:hiddify/utils/custom_loggers.dart'; import 'package:rxdart/rxdart.dart'; -class MobileSingboxService - with ServiceStatus, InfraLogger - implements SingboxService { +class PlatformSingboxService with InfraLogger implements SingboxService { late final _methodChannel = const MethodChannel("com.hiddify.app/method"); - late final _connectionStatusChannel = + late final _statusChannel = const EventChannel("com.hiddify.app/service.status"); late final _alertsChannel = const EventChannel("com.hiddify.app/service.alerts"); late final _logsChannel = const EventChannel("com.hiddify.app/service.logs"); - late final ValueStream _connectionStatus; + late final ValueStream _status; @override Future init() async { loggy.debug("initializing"); - final status = - _connectionStatusChannel.receiveBroadcastStream().map(mapEventToStatus); - final alerts = - _alertsChannel.receiveBroadcastStream().map(mapEventToStatus); - _connectionStatus = - ValueConnectableStream(Rx.merge([status, alerts])).autoConnect(); - await _connectionStatus.first; + final status = _statusChannel.receiveBroadcastStream().map( + (event) { + return SingboxStatus.fromEvent(event); + }, + ); + final alerts = _alertsChannel.receiveBroadcastStream().map( + (event) { + return SingboxStatus.fromEvent(event); + }, + ); + _status = ValueConnectableStream(Rx.merge([status, alerts])).autoConnect(); + await _status.first; } @override TaskEither setup( - String baseDir, - String workingDir, - String tempDir, + Directories directories, bool debug, ) => TaskEither.of(unit); @override - TaskEither parseConfig( + TaskEither validateConfigByPath( String path, String tempPath, bool debug, @@ -61,7 +64,7 @@ class MobileSingboxService } @override - TaskEither changeConfigOptions(ConfigOptions options) { + TaskEither changeOptions(SingboxConfigOption options) { return TaskEither( () async { await _methodChannel.invokeMethod( @@ -74,7 +77,7 @@ class MobileSingboxService } @override - TaskEither generateConfig( + TaskEither generateFullConfigByPath( String path, ) { return TaskEither( @@ -92,13 +95,13 @@ class MobileSingboxService } @override - TaskEither start(String configPath, bool disableMemoryLimit) { + TaskEither start(String path, bool disableMemoryLimit) { return TaskEither( () async { loggy.debug("starting"); await _methodChannel.invokeMethod( "start", - {"path": configPath}, + {"path": path}, ); return right(unit); }, @@ -117,13 +120,13 @@ class MobileSingboxService } @override - TaskEither restart(String configPath, bool disableMemoryLimit) { + TaskEither restart(String path, bool disableMemoryLimit) { return TaskEither( () async { loggy.debug("restarting"); await _methodChannel.invokeMethod( "restart", - {"path": configPath}, + {"path": path}, ); return right(unit); }, @@ -131,13 +134,15 @@ class MobileSingboxService } @override - Stream watchOutbounds() { + Stream> watchOutbounds() { const channel = EventChannel("com.hiddify.app/groups"); loggy.debug("watching outbounds"); return channel.receiveBroadcastStream().map( (event) { if (event case String _) { - return event; + return (jsonDecode(event) as List).map((e) { + return SingboxOutboundGroup.fromJson(e as Map); + }).toList(); } loggy.error("[group client] unexpected type, msg: $event"); throw "invalid type"; @@ -146,11 +151,11 @@ class MobileSingboxService } @override - Stream watchConnectionStatus() => _connectionStatus; + Stream watchStatus() => _status; @override - Stream watchStats() { - // TODO: implement watchStatus + Stream watchStats() { + // TODO: implement watchStats return const Stream.empty(); } diff --git a/lib/singbox/service/singbox_service.dart b/lib/singbox/service/singbox_service.dart new file mode 100644 index 00000000..3ad2d8c5 --- /dev/null +++ b/lib/singbox/service/singbox_service.dart @@ -0,0 +1,60 @@ +import 'dart:io'; + +import 'package:fpdart/fpdart.dart'; +import 'package:hiddify/core/model/directories.dart'; +import 'package:hiddify/singbox/model/singbox_config_option.dart'; +import 'package:hiddify/singbox/model/singbox_outbound.dart'; +import 'package:hiddify/singbox/model/singbox_stats.dart'; +import 'package:hiddify/singbox/model/singbox_status.dart'; +import 'package:hiddify/singbox/service/ffi_singbox_service.dart'; +import 'package:hiddify/singbox/service/platform_singbox_service.dart'; + +abstract interface class SingboxService { + factory SingboxService() { + if (Platform.isAndroid || Platform.isIOS) { + return PlatformSingboxService(); + } else if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) { + return FFISingboxService(); + } + throw Exception("unsupported platform"); + } + + Future init(); + + TaskEither setup( + Directories directories, + bool debug, + ); + + TaskEither validateConfigByPath( + String path, + String tempPath, + bool debug, + ); + + TaskEither changeOptions(SingboxConfigOption options); + + TaskEither generateFullConfigByPath( + String path, + ); + + TaskEither start(String path, bool disableMemoryLimit); + + TaskEither stop(); + + TaskEither restart(String path, bool disableMemoryLimit); + + Stream> watchOutbounds(); + + TaskEither selectOutbound(String groupTag, String outboundTag); + + TaskEither urlTest(String groupTag); + + Stream watchStatus(); + + Stream watchStats(); + + Stream> watchLogs(String path); + + TaskEither clearLogs(); +} diff --git a/lib/singbox/service/singbox_service_provider.dart b/lib/singbox/service/singbox_service_provider.dart new file mode 100644 index 00000000..569d7ff1 --- /dev/null +++ b/lib/singbox/service/singbox_service_provider.dart @@ -0,0 +1,9 @@ +import 'package:hiddify/singbox/service/singbox_service.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'singbox_service_provider.g.dart'; + +@Riverpod(keepAlive: true) +SingboxService singboxService(SingboxServiceRef ref) { + return SingboxService(); +} diff --git a/lib/utils/link_parsers.dart b/lib/utils/link_parsers.dart index d82b7aff..b481564b 100644 --- a/lib/utils/link_parsers.dart +++ b/lib/utils/link_parsers.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import 'package:hiddify/domain/singbox/singbox.dart'; +import 'package:hiddify/singbox/model/singbox_proxy_type.dart'; import 'package:hiddify/utils/validators.dart'; typedef ProfileLink = ({String url, String name}); diff --git a/lib/utils/mutation_state.dart b/lib/utils/mutation_state.dart index b83f9a4c..53aab501 100644 --- a/lib/utils/mutation_state.dart +++ b/lib/utils/mutation_state.dart @@ -1,5 +1,5 @@ import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:hiddify/domain/failures.dart'; +import 'package:hiddify/core/model/failures.dart'; part 'mutation_state.freezed.dart'; diff --git a/lib/utils/pref_notifier.dart b/lib/utils/pref_notifier.dart index 90e6dfd3..6d05cc2d 100644 --- a/lib/utils/pref_notifier.dart +++ b/lib/utils/pref_notifier.dart @@ -1,4 +1,4 @@ -import 'package:hiddify/data/data_providers.dart'; +import 'package:hiddify/core/preferences/preferences_provider.dart'; import 'package:hiddify/utils/custom_loggers.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -75,7 +75,7 @@ class PrefNotifier extends AutoDisposeNotifier with InfraLogger { final P Function(T)? _mapTo; late final Pref _pref = Pref( - ref.watch(sharedPreferencesProvider), + ref.watch(sharedPreferencesProvider).requireValue, _key, _defaultValue, mapFrom: _mapFrom, diff --git a/lib/utils/sentry_utils.dart b/lib/utils/sentry_utils.dart index 44971fd6..a8976a50 100644 --- a/lib/utils/sentry_utils.dart +++ b/lib/utils/sentry_utils.dart @@ -1,7 +1,7 @@ import 'dart:io'; import 'package:dio/dio.dart'; -import 'package:hiddify/domain/failures.dart'; +import 'package:hiddify/core/model/failures.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; diff --git a/test/data/local/generated_migrations/schema.dart b/test/core/database/generated_migrations/schema.dart similarity index 100% rename from test/data/local/generated_migrations/schema.dart rename to test/core/database/generated_migrations/schema.dart diff --git a/test/data/local/generated_migrations/schema_v1.dart b/test/core/database/generated_migrations/schema_v1.dart similarity index 100% rename from test/data/local/generated_migrations/schema_v1.dart rename to test/core/database/generated_migrations/schema_v1.dart diff --git a/test/data/local/generated_migrations/schema_v2.dart b/test/core/database/generated_migrations/schema_v2.dart similarity index 100% rename from test/data/local/generated_migrations/schema_v2.dart rename to test/core/database/generated_migrations/schema_v2.dart diff --git a/test/data/local/generated_migrations/schema_v3.dart b/test/core/database/generated_migrations/schema_v3.dart similarity index 100% rename from test/data/local/generated_migrations/schema_v3.dart rename to test/core/database/generated_migrations/schema_v3.dart diff --git a/test/data/local/migrations_test.dart b/test/core/database/migrations_test.dart similarity index 95% rename from test/data/local/migrations_test.dart rename to test/core/database/migrations_test.dart index e184117a..3739b818 100644 --- a/test/data/local/migrations_test.dart +++ b/test/core/database/migrations_test.dart @@ -1,6 +1,6 @@ import 'package:drift_dev/api/migrations.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:hiddify/data/local/database.dart'; +import 'package:hiddify/core/database/app_database.dart'; import 'generated_migrations/schema.dart'; From d1ec932ffff6db9b4e2165958cc82941c551c4e7 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Fri, 1 Dec 2023 14:22:00 +0330 Subject: [PATCH 84/91] Fix status mapper --- .../kotlin/com/hiddify/hiddify/EventHandler.kt | 5 +++-- lib/singbox/model/singbox_status.dart | 2 ++ .../service/platform_singbox_service.dart | 18 ++++++------------ 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/EventHandler.kt b/android/app/src/main/kotlin/com/hiddify/hiddify/EventHandler.kt index cf5717ad..95a58b42 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/EventHandler.kt +++ b/android/app/src/main/kotlin/com/hiddify/hiddify/EventHandler.kt @@ -6,6 +6,7 @@ import com.hiddify.hiddify.constant.Alert import com.hiddify.hiddify.constant.Status import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.JSONMethodCodec class EventHandler : FlutterPlugin { @@ -22,8 +23,8 @@ class EventHandler : FlutterPlugin { private var alertsObserver: Observer? = null override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { - statusChannel = EventChannel(flutterPluginBinding.binaryMessenger, SERVICE_STATUS) - alertsChannel = EventChannel(flutterPluginBinding.binaryMessenger, SERVICE_ALERTS) + statusChannel = EventChannel(flutterPluginBinding.binaryMessenger, SERVICE_STATUS, JSONMethodCodec.INSTANCE) + alertsChannel = EventChannel(flutterPluginBinding.binaryMessenger, SERVICE_ALERTS, JSONMethodCodec.INSTANCE) statusChannel!!.setStreamHandler(object : EventChannel.StreamHandler { override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { diff --git a/lib/singbox/model/singbox_status.dart b/lib/singbox/model/singbox_status.dart index 88d9d1cc..04751b7a 100644 --- a/lib/singbox/model/singbox_status.dart +++ b/lib/singbox/model/singbox_status.dart @@ -26,6 +26,8 @@ sealed class SingboxStatus with _$SingboxStatus { (e) => alertStr?.toLowerCase() == e.name.toLowerCase(), ); return SingboxStatus.stopped(alert: alert, message: messageStr); + case {"status": "Stopped"}: + return const SingboxStatus.stopped(); case {"status": "Starting"}: return const SingboxStarting(); case {"status": "Started"}: diff --git a/lib/singbox/service/platform_singbox_service.dart b/lib/singbox/service/platform_singbox_service.dart index 52cb7f66..7ed3b131 100644 --- a/lib/singbox/service/platform_singbox_service.dart +++ b/lib/singbox/service/platform_singbox_service.dart @@ -14,9 +14,9 @@ import 'package:rxdart/rxdart.dart'; class PlatformSingboxService with InfraLogger implements SingboxService { late final _methodChannel = const MethodChannel("com.hiddify.app/method"); late final _statusChannel = - const EventChannel("com.hiddify.app/service.status"); + const EventChannel("com.hiddify.app/service.status", JSONMethodCodec()); late final _alertsChannel = - const EventChannel("com.hiddify.app/service.alerts"); + const EventChannel("com.hiddify.app/service.alerts", JSONMethodCodec()); late final _logsChannel = const EventChannel("com.hiddify.app/service.logs"); late final ValueStream _status; @@ -24,16 +24,10 @@ class PlatformSingboxService with InfraLogger implements SingboxService { @override Future init() async { loggy.debug("initializing"); - final status = _statusChannel.receiveBroadcastStream().map( - (event) { - return SingboxStatus.fromEvent(event); - }, - ); - final alerts = _alertsChannel.receiveBroadcastStream().map( - (event) { - return SingboxStatus.fromEvent(event); - }, - ); + final status = + _statusChannel.receiveBroadcastStream().map(SingboxStatus.fromEvent); + final alerts = + _alertsChannel.receiveBroadcastStream().map(SingboxStatus.fromEvent); _status = ValueConnectableStream(Rx.merge([status, alerts])).autoConnect(); await _status.first; } From b2bb18ed8c6f6c20f9eefa71c701263bb3c6563c Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Fri, 1 Dec 2023 14:51:15 +0330 Subject: [PATCH 85/91] Update core (singbox v1.7) --- dependencies.properties | 2 +- libcore | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dependencies.properties b/dependencies.properties index 7615b712..ac07d8b0 100644 --- a/dependencies.properties +++ b/dependencies.properties @@ -1 +1 @@ -core.version=0.8.3 \ No newline at end of file +core.version=0.8.4 \ No newline at end of file diff --git a/libcore b/libcore index 2d0f5eee..24648282 160000 --- a/libcore +++ b/libcore @@ -1 +1 @@ -Subproject commit 2d0f5eee4d8db693d68954e8cca7a80ac749ba86 +Subproject commit 24648282bc0a47cf19ccc3ca40ce32e898b2765f From 29d4d634cd1edb226970fdd32b14af47c7aa8914 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Fri, 1 Dec 2023 17:52:57 +0330 Subject: [PATCH 86/91] Update dependencies --- ios/Podfile.lock | 4 ++-- macos/Podfile.lock | 4 ++-- pubspec.lock | 32 ++++++++++++++++---------------- pubspec.yaml | 14 +++++++------- 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index deee190a..1137722d 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -54,7 +54,7 @@ PODS: - GTMSessionFetcher/Core (< 3.0, >= 1.1) - MLImage (= 1.0.0-beta4) - MLKitCommon (~> 9.0) - - mobile_scanner (3.5.4): + - mobile_scanner (3.5.5): - Flutter - GoogleMLKit/BarcodeScanning (~> 4.0.0) - nanopb (2.30909.1): @@ -176,7 +176,7 @@ SPEC CHECKSUMS: MLKitBarcodeScanning: 04e264482c5f3810cb89ebc134ef6b61e67db505 MLKitCommon: c1b791c3e667091918d91bda4bba69a91011e390 MLKitVision: 8baa5f46ee3352614169b85250574fde38c36f49 - mobile_scanner: e866af997a851f0d43e293621443713cb6222fe3 + mobile_scanner: 202ab6f652e40a9add68b10de4c4fb2a745c4348 nanopb: d4d75c12cd1316f4a64e3c6963f879ecd4b5e0d5 package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 81f49c08..068ef535 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -4,7 +4,7 @@ PODS: - device_info_plus (0.0.1): - FlutterMacOS - FlutterMacOS (1.0.0) - - mobile_scanner (3.5.4): + - mobile_scanner (3.5.5): - FlutterMacOS - package_info_plus (0.0.1): - FlutterMacOS @@ -108,7 +108,7 @@ SPEC CHECKSUMS: cupertino_http: afa11b9e2786b62da2671e4ddd32caf792503748 device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - mobile_scanner: a33715761775cdbe498fd9de24d13ef142225962 + mobile_scanner: d12930b68bf502497f78b8b5182aeccfaa1e04f6 package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 protocol_handler: 587e1caf6c0b92ce351ab14081968dae49cb8cc6 diff --git a/pubspec.lock b/pubspec.lock index 7d9c6045..e4d7966e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -109,10 +109,10 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "10c6bcdbf9d049a0b666702cf1cee4ddfdc38f02a19d35ae392863b47519848b" + sha256: "67d591d602906ef9201caf93452495ad1812bea2074f04e25dbd7c133785821b" url: "https://pub.dev" source: hosted - version: "2.4.6" + version: "2.4.7" build_runner_core: dependency: transitive description: @@ -349,18 +349,18 @@ packages: dependency: "direct main" description: name: dio - sha256: "01870acd87986f768e0c09cc4d7a19a59d814af7b34cbeb0b437d2c33bdfea4c" + sha256: "797e1e341c3dd2f69f2dad42564a6feff3bfb87187d05abb93b9609e6f1645c3" url: "https://pub.dev" source: hosted - version: "5.3.4" + version: "5.4.0" drift: dependency: "direct main" description: name: drift - sha256: ef2ddafe89c1f5f26767e5eada65d739de4e9d2820303f7249f15a005999d5fc + sha256: d542088d353585a252f015b81c1e7603c57c996ba59a80d53a3f4644cc47f543 url: "https://pub.dev" source: hosted - version: "2.13.1" + version: "2.13.2" drift_dev: dependency: "direct dev" description: @@ -833,18 +833,18 @@ packages: dependency: "direct main" description: name: mobile_scanner - sha256: c9ed2bb1bbf4b98394bc4a8477984c8ba2b55f706d634bf27cd9dd1c2e9b3a23 + sha256: c3e5bba1cb626b6ab4fc46610f72a136803f6854267967e19f4a4a6a31ff9b74 url: "https://pub.dev" source: hosted - version: "3.5.4" + version: "3.5.5" native_dio_adapter: dependency: "direct main" description: name: native_dio_adapter - sha256: a9af4430278dd5a86ded4c5857e58d80e5bc81097e6cc8a176725ac8a0ab39a6 + sha256: "2bb78b5e20b012ae6a0b5ea6405a3583f2ea6c85be9f7d909decabcb1992c25d" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.2.0" neat_periodic_task: dependency: "direct main" description: @@ -873,10 +873,10 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: "7e76fad405b3e4016cd39d08f455a4eb5199723cf594cd1b8916d47140d93017" + sha256: "88bc797f44a94814f2213db1c9bd5badebafdfb8290ca9f78d4b9ee2a3db4d79" url: "https://pub.dev" source: hosted - version: "4.2.0" + version: "5.0.1" package_info_plus_platform_interface: dependency: transitive description: @@ -1502,10 +1502,10 @@ packages: dependency: "direct main" description: name: upgrader - sha256: "204c5d5d5ac1c09fa956422dee94d7f44f1b612750d29028b7ec7a43b474c135" + sha256: d63081e43d1daa1d0e4f8177b56311523985ac77c25519d559040e3c14fb947e url: "https://pub.dev" source: hosted - version: "8.3.0" + version: "8.4.0" url_launcher: dependency: "direct main" description: @@ -1662,10 +1662,10 @@ packages: dependency: "direct main" description: name: win32 - sha256: "7c99c0e1e2fa190b48d25c81ca5e42036d5cac81430ef249027d97b0935c553f" + sha256: b0f37db61ba2f2e9b7a78a1caece0052564d1bc70668156cf3a29d676fe4e574 url: "https://pub.dev" source: hosted - version: "5.1.0" + version: "5.1.1" win32_registry: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3906824d..a56a63e0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,20 +24,20 @@ dependencies: flutter_hooks: ^0.20.3 riverpod_annotation: ^2.3.2 rxdart: ^0.27.7 - drift: ^2.13.1 + drift: ^2.13.2 sqlite3_flutter_libs: ^0.5.18 shared_preferences: ^2.2.2 - dio: ^5.3.4 + dio: ^5.4.0 web_socket_channel: ^2.4.0 ffi: ^2.1.0 path_provider: ^2.1.1 - mobile_scanner: ^3.5.4 + mobile_scanner: ^3.5.5 protocol_handler: ^0.1.5 flutter_native_splash: ^2.3.6 share_plus: ^7.2.1 window_manager: ^0.3.7 tray_manager: ^0.2.0 - package_info_plus: ^4.2.0 + package_info_plus: ^5.0.1 url_launcher: ^6.2.1 vclibs: ^0.1.0 launch_at_startup: ^0.2.2 @@ -68,15 +68,15 @@ dependencies: toastification: ^1.1.0 version: ^3.0.2 posix: ^6.0.1 - win32: ^5.1.0 + win32: ^5.1.1 qr_flutter: ^4.1.0 - native_dio_adapter: ^1.1.1 + native_dio_adapter: ^1.2.0 dev_dependencies: flutter_test: sdk: flutter lint: ^2.2.0 - build_runner: ^2.4.6 + build_runner: ^2.4.7 json_serializable: ^6.7.1 freezed: ^2.4.5 riverpod_generator: ^2.3.8 From 85e066a7aa62f49dbb45accdfc635e80d736f1d0 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Fri, 1 Dec 2023 17:53:05 +0330 Subject: [PATCH 87/91] Update changelog --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd1f9043..8592e5e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## [0.12.0.dev] - 2023-12-01 ### New Features and Improvements @@ -8,7 +8,10 @@ ### Bug Fixes +- Refactored significant portions of the app +- Fixed incorrect profile parsing when missing headers - Fixed geo assets bug where assets were deactivated +- Updated sing-box to version 1.7.0 - Fixed Chinese typography bug (thanks to [betaxab](https://github.com/betaxab)) - Fixed localization mistakes in Russian. [PR#189](https://github.com/hiddify/hiddify-next/pull/189) by [jomertix](https://github.com/jomertix) @@ -85,6 +88,7 @@ - Fixed localization mistakes in Russian. [PR#95](https://github.com/hiddify/hiddify-next/pull/95) by [solokot](https://github.com/solokot) - Fixed localization mistakes in Russian. [PR#74](https://github.com/hiddify/hiddify-next/pull/74) by [Elshad Guseynov](https://github.com/lifeindarkside) +[0.12.0.dev]: https://github.com/hiddify/hiddify-next/releases/tag/v0.12.0.dev [0.11.1]: https://github.com/hiddify/hiddify-next/releases/tag/v0.11.1 [0.11.0]: https://github.com/hiddify/hiddify-next/releases/tag/v0.11.0 [0.10.0]: https://github.com/hiddify/hiddify-next/releases/tag/v0.10.0 From b4f5c3ab846cd0c6f4edffa01ee0f7c30501bdf6 Mon Sep 17 00:00:00 2001 From: Hiddify <114227601+hiddify-com@users.noreply.github.com> Date: Fri, 1 Dec 2023 23:14:19 +0100 Subject: [PATCH 88/91] Create dev-i.yml --- .github/workflows/dev-i.yml | 193 ++++++++++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 .github/workflows/dev-i.yml diff --git a/.github/workflows/dev-i.yml b/.github/workflows/dev-i.yml new file mode 100644 index 00000000..4e8cac6d --- /dev/null +++ b/.github/workflows/dev-i.yml @@ -0,0 +1,193 @@ +name: dev i +on: + push: + branches: + - main + tags: + - 'v*' + paths-ignore: + - '**.md' + - 'docs/**' + - '.github/**' + - '!.github/workflows/build.yml' + - 'appcast.xml' + # pull_request: + # branches: + # - main +concurrency: + group: ${{ github.ref }}-${{ github.workflow }} + cancel-in-progress: true + +env: + CHANNEL: ${{ github.ref_type == 'tag' && endsWith(github.ref_name, 'dev') && 'dev' || github.ref_type != 'tag' && 'dev' || 'prod' }} + NDK_VERSION: r26b + +jobs: + build: + permissions: write-all + strategy: + fail-fast: false + matrix: + include: + # - platform: android-apk + # os: ubuntu-latest + # targets: apk + + # - platform: android-aab + # os: ubuntu-latest + # targets: aab + + # - platform: windows + # os: windows-latest + # aarch: amd64 + # targets: exe + # filename: hiddify-windows-x64 + + # - platform: linux + # os: ubuntu-latest + # aarch: amd64 + # targets: AppImage + # filename: hiddify-linux-x64 + + # - platform: macos + # os: macos-13 + # aarch: universal + # targets: dmg + # filename: hiddify-macos-universal + + - platform: ios + os: macos-13 + aarch: universal + filename: hiddify-ios + targets: ipa + + runs-on: ${{ matrix.os }} + steps: + - name: checkout + uses: actions/checkout@v3 + - name: Install macos dmg needed tools + if: matrix.platform == 'macos' || matrix.platform == 'ios' + run: | + # xcode-select --install || softwareupdate --all --install --force + # brew uninstall --force $(brew list | grep python@) && brew cleanup || echo "python not installed" + brew uninstall --ignore-dependencies python@3.12 + brew reinstall python@3.10 + python3 -m pip install --upgrade setuptools pip + brew install create-dmg tree + npm install -g appdmg + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: '3.16.x' + channel: 'stable' + cache: true + + + - name: Setup Flutter Distributor + if: ${{ !startsWith(matrix.platform,'android') }} + run: | + dart pub global activate flutter_distributor + + + - name: Get Geo Assets + run: | + make get-geo-assets + + - name: Get Dependencies + run: | + make get + + - name: Generate + run: | + make translate + make gen + + - name: Get Libs ${{ matrix.platform }} + run: | + make ${{ matrix.platform }}-libs + + + - name: Setup Apple certificate and provisioning profile + if: startsWith(matrix.os,'macos') + env: + BUILD_CERTIFICATE_BASE64: ${{ secrets.APPLE_BUILD_CERTIFICATE_BASE64 }} + P12_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_P12_PASSWORD }} + BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.APPLE_BUILD_PROVISION_PROFILE_BASE64 }} + BUILD_PACKET_TUNNEL_PROVISION_PROFILE_BASE64: ${{ secrets.APPLE_BUILD_PACKET_TUNNEL_PROVISION_PROFILE_BASE64 }} + KEYCHAIN_PASSWORD: ${{ secrets.APPLE_KEYCHAIN_PASSWORD }} + run: | + # create variables + CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12 + PP_PATH=$RUNNER_TEMP/build_pp.mobileprovision + PP_PACKET_TUNNEL_PATH=$RUNNER_TEMP/build_pppt.mobileprovision + KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db + + # import certificate and provisioning profile from secrets + echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH + echo -n "$BUILD_PROVISION_PROFILE_BASE64" | base64 --decode -o $PP_PATH + echo -n "$BUILD_PACKET_TUNNEL_PROVISION_PROFILE_BASE64" | base64 --decode -o $PP_PACKET_TUNNEL_PATH + + # create temporary keychain + security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + security set-keychain-settings -lut 21600 $KEYCHAIN_PATH + security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + + # import certificate to keychain + security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH + security list-keychain -d user -s $KEYCHAIN_PATH + + # apply provisioning profile + mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles + cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles + cp $PP_PACKET_TUNNEL_PATH ~/Library/MobileDevice/Provisioning\ Profiles + + - name: Release ${{ matrix.platform }} + env: + SENTRY_DSN: ${{ secrets.SENTRY_DSN }} + run: | + make ${{ matrix.platform }}-release + + - name: Upload Debug Symbols + if: ${{ github.ref_type == 'tag' }} + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_ORG: ${{ secrets.SENTRY_ORG }} + SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} + SENTRY_DIST: ${{ matrix.platform == 'android-aab' && 'google-play' || 'general' }} + run: | + flutter packages pub run sentry_dart_plugin + + + - name: Copy to out unix + if: matrix.platform == 'linux' || matrix.platform == 'macos' || matrix.platform == 'ios' + run: | + ls -R dist/ + mkdir out + mkdir tmp_out + EXT="${{ matrix.targets }}" + mv dist/*/*.$EXT tmp_out/${{matrix.filename}}.$EXT + chmod +x tmp_out/${{matrix.filename}}.$EXT + if [ "${{matrix.platform}}" == "linux" ];then + cp ./.github/help/linux/* tmp_out/ + else + cp ./.github/help/mac-windows/* tmp_out/ + fi + if [[ "${{matrix.platform}}" == 'ios' ]];then + mv tmp_out/${{matrix.filename}}.ipa bin/${{matrix.filename}}.ipa + else + cd tmp_out + 7z a ${{matrix.filename}}.zip ./ + mv *.zip ../out/ + fi + + - name: Clean up keychain and provisioning profile + if: ${{ always() && startsWith(matrix.os,'macos')}} + run: | + security delete-keychain $RUNNER_TEMP/app-signing.keychain-db + rm ~/Library/MobileDevice/Provisioning\ Profiles/build_pp.mobileprovision + - name: Upload Artifact + uses: actions/upload-artifact@v3 + with: + name: artifact + path: ./out + retention-days: 2 From e182d88a87da5afbb3aca71a37060566c6282b90 Mon Sep 17 00:00:00 2001 From: Hiddify <114227601+hiddify-com@users.noreply.github.com> Date: Fri, 1 Dec 2023 23:15:55 +0100 Subject: [PATCH 89/91] Update pubspec.yaml --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index a56a63e0..7151a08d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: hiddify -description: A Proxy Frontend. +description: Cross Platform Multi Protocol Proxy Frontend. publish_to: "none" version: 0.11.1+1101 From e467784713163a41129eee265210225271cdf10b Mon Sep 17 00:00:00 2001 From: Hiddify <114227601+hiddify-com@users.noreply.github.com> Date: Sun, 3 Dec 2023 08:51:59 +0100 Subject: [PATCH 90/91] Update dev-i.yml --- .github/workflows/dev-i.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/dev-i.yml b/.github/workflows/dev-i.yml index 4e8cac6d..95e99556 100644 --- a/.github/workflows/dev-i.yml +++ b/.github/workflows/dev-i.yml @@ -75,6 +75,9 @@ jobs: python3 -m pip install --upgrade setuptools pip brew install create-dmg tree npm install -g appdmg + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: '15.0.1' - name: Setup Flutter uses: subosito/flutter-action@v2 with: From 79695d14650f09f3062cbba1cd0c250dd09b988f Mon Sep 17 00:00:00 2001 From: Hiddify <114227601+hiddify-com@users.noreply.github.com> Date: Sun, 3 Dec 2023 08:52:39 +0100 Subject: [PATCH 91/91] Update dev-i.yml --- .github/workflows/dev-i.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dev-i.yml b/.github/workflows/dev-i.yml index 95e99556..1fad9d30 100644 --- a/.github/workflows/dev-i.yml +++ b/.github/workflows/dev-i.yml @@ -9,7 +9,7 @@ on: - '**.md' - 'docs/**' - '.github/**' - - '!.github/workflows/build.yml' + - '!.github/workflows/*' - 'appcast.xml' # pull_request: # branches: