From 830a5cd75fef2b8a6ecfb63d9be046506700ae00 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Sun, 10 Sep 2023 14:16:44 +0330 Subject: [PATCH 01/23] Change mark new profile active --- lib/core/prefs/general_prefs.dart | 17 +++++++++++++++++ lib/features/common/profile_tile.dart | 4 ++++ .../profiles/notifier/profiles_notifier.dart | 9 ++++++--- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/lib/core/prefs/general_prefs.dart b/lib/core/prefs/general_prefs.dart index aec3bf2a..47b54783 100644 --- a/lib/core/prefs/general_prefs.dart +++ b/lib/core/prefs/general_prefs.dart @@ -31,3 +31,20 @@ class DebugModeNotifier extends _$DebugModeNotifier { return _pref.update(value); } } + +@riverpod +class MarkNewProfileActive extends _$MarkNewProfileActive { + late final _pref = Pref( + ref.watch(sharedPreferencesProvider), + "mark_new_profile_active", + true, + ); + + @override + bool build() => _pref.getValue(); + + Future update(bool value) { + state = value; + return _pref.update(value); + } +} diff --git a/lib/features/common/profile_tile.dart b/lib/features/common/profile_tile.dart index 8f1edef5..18621e0a 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: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/router/routes/routes.dart'; @@ -32,6 +33,9 @@ class ProfileTile extends HookConsumerWidget { initialOnFailure: (err) { CustomToast.error(t.printError(err)).show(context); }, + initialOnSuccess: () { + if (context.mounted) context.pop(); + }, ); final subInfo = profile.subInfo; diff --git a/lib/features/profiles/notifier/profiles_notifier.dart b/lib/features/profiles/notifier/profiles_notifier.dart index 4db60c13..faa61089 100644 --- a/lib/features/profiles/notifier/profiles_notifier.dart +++ b/lib/features/profiles/notifier/profiles_notifier.dart @@ -1,6 +1,7 @@ import 'dart:async'; 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'; @@ -40,9 +41,9 @@ class ProfilesNotifier extends _$ProfilesNotifier with AppLogger { ProfilesRepository get _profilesRepo => ref.read(profilesRepositoryProvider); - Future selectActiveProfile(String id) async { + Future selectActiveProfile(String id) async { loggy.debug('changing active profile to: [$id]'); - await _profilesRepo.setAsActive(id).mapLeft((f) { + return _profilesRepo.setAsActive(id).getOrElse((f) { loggy.warning('failed to set [$id] as active profile, $f'); throw f; }).run(); @@ -50,10 +51,12 @@ class ProfilesNotifier extends _$ProfilesNotifier with AppLogger { Future addProfile(String url) async { final activeProfile = await ref.read(activeProfileProvider.future); + final markAsActive = + activeProfile == null || ref.read(markNewProfileActiveProvider); loggy.debug("adding profile, url: [$url]"); return ref .read(profilesRepositoryProvider) - .addByUrl(url, markAsActive: activeProfile == null) + .addByUrl(url, markAsActive: markAsActive) .getOrElse((l) { loggy.warning("failed to add profile: $l"); throw l; From 778db2dd83f2d2f3dfaa52f18368abfe586a8adc Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Sun, 10 Sep 2023 20:19:36 +0330 Subject: [PATCH 02/23] Add android service restart --- .../com/hiddify/hiddify/EventHandler.kt | 2 +- .../com/hiddify/hiddify/MethodHandler.kt | 39 +++++++++++++++---- 2 files changed, 33 insertions(+), 8 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 52564687..26f6cacc 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/EventHandler.kt +++ b/android/app/src/main/kotlin/com/hiddify/hiddify/EventHandler.kt @@ -50,7 +50,7 @@ class EventHandler : FlutterPlugin { Log.d(TAG, "new alert: $it") val map = listOf( Pair("status", it.status.name), - Pair("failure", it.alert?.name), + Pair("alert", it.alert?.name), Pair("message", it.message) ) .mapNotNull { p -> p.second?.let { Pair(p.first, p.second) } } 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 33793aaf..1bb5e8fe 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/MethodHandler.kt +++ b/android/app/src/main/kotlin/com/hiddify/hiddify/MethodHandler.kt @@ -2,12 +2,14 @@ package com.hiddify.hiddify import androidx.annotation.NonNull import com.hiddify.hiddify.bg.BoxService +import com.hiddify.hiddify.constant.Status import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.StandardMethodCodec import io.nekohasekai.libbox.Libbox import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay import kotlinx.coroutines.launch class MethodHandler : FlutterPlugin, MethodChannel.MethodCallHandler { @@ -18,10 +20,10 @@ class MethodHandler : FlutterPlugin, MethodChannel.MethodCallHandler { enum class Trigger(val method: String) { ParseConfig("parse_config"), - SetActiveConfigPath("set_active_config_path"), ChangeConfigOptions("change_config_options"), Start("start"), Stop("stop"), + Restart("restart"), SelectOutbound("select_outbound"), UrlTest("url_test"), } @@ -55,12 +57,6 @@ class MethodHandler : FlutterPlugin, MethodChannel.MethodCallHandler { } } - Trigger.SetActiveConfigPath.method -> { - val args = call.arguments as Map<*, *> - Settings.activeConfigPath = args["path"] as String? ?: "" - result.success(true) - } - Trigger.ChangeConfigOptions.method -> { result.runCatching { val args = call.arguments as String @@ -70,6 +66,8 @@ class MethodHandler : FlutterPlugin, MethodChannel.MethodCallHandler { } Trigger.Start.method -> { + val args = call.arguments as Map<*, *> + Settings.activeConfigPath = args["path"] as String? ?: "" MainActivity.instance.startService() result.success(true) } @@ -79,6 +77,33 @@ class MethodHandler : FlutterPlugin, MethodChannel.MethodCallHandler { result.success(true) } + Trigger.Restart.method -> { + GlobalScope.launch { + result.runCatching { + val args = call.arguments as Map<*, *> + Settings.activeConfigPath = args["path"] as String? ?: "" + val mainActivity = MainActivity.instance + val started = mainActivity.serviceStatus.value == Status.Started + if (!started) return@launch success(true) + val restart = Settings.rebuildServiceMode() + if (restart) { + mainActivity.reconnect() + BoxService.stop() + delay(200) + mainActivity.startService() + success(true) + return@launch + } + runCatching { + Libbox.newStandaloneCommandClient().serviceReload() + success(true) + }.onFailure { + error(it) + } + } + } + } + Trigger.SelectOutbound.method -> { GlobalScope.launch { result.runCatching { From 62281b31ce23edcad3831c6614f716069ab9acff Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Sun, 10 Sep 2023 20:19:54 +0330 Subject: [PATCH 03/23] Change android notification permission --- .../com/hiddify/hiddify/MainActivity.kt | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/MainActivity.kt b/android/app/src/main/kotlin/com/hiddify/hiddify/MainActivity.kt index b2971311..a6d526c8 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/MainActivity.kt +++ b/android/app/src/main/kotlin/com/hiddify/hiddify/MainActivity.kt @@ -1,8 +1,14 @@ package com.hiddify.hiddify +import android.annotation.SuppressLint import android.content.Intent +import android.Manifest +import android.content.pm.PackageManager import android.net.VpnService +import android.os.Build import android.util.Log +import androidx.core.app.ActivityCompat +import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat import androidx.lifecycle.MutableLiveData import androidx.lifecycle.lifecycleScope @@ -18,6 +24,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.util.LinkedList + class MainActivity : FlutterFragmentActivity(), ServiceConnection.Callback { companion object { private const val TAG = "ANDROID/MyActivity" @@ -52,7 +59,8 @@ class MainActivity : FlutterFragmentActivity(), ServiceConnection.Callback { fun startService() { if (!ServiceNotification.checkPermission()) { - Log.d(TAG, "missing notification permission") +// Log.d(TAG, "missing notification permission") + grantNotificationPermission() return } lifecycleScope.launch(Dispatchers.IO) { @@ -123,6 +131,29 @@ class MainActivity : FlutterFragmentActivity(), ServiceConnection.Callback { super.onDestroy() } + @SuppressLint("NewApi") + private fun grantNotificationPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + ActivityCompat.requestPermissions( + this, + arrayOf(Manifest.permission.POST_NOTIFICATIONS), + NOTIFICATION_PERMISSION_REQUEST_CODE + ) + } + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + if (requestCode == NOTIFICATION_PERMISSION_REQUEST_CODE) { + if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + startService() + } else onServiceAlert(Alert.RequestNotificationPermission, null) + } + } + private suspend fun prepare() = withContext(Dispatchers.Main) { try { val intent = VpnService.prepare(this@MainActivity) From f303cf7e2446367ebc6372917a61eeea27667c1e Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Sun, 10 Sep 2023 20:20:46 +0330 Subject: [PATCH 04/23] Remove notification service --- lib/services/notification/constants.dart | 9 -- .../local_notification_service.dart | 107 ------------------ lib/services/notification/notification.dart | 1 - .../notification/notification_service.dart | 33 ------ .../stub_notification_service.dart | 31 ----- 5 files changed, 181 deletions(-) delete mode 100644 lib/services/notification/constants.dart delete mode 100644 lib/services/notification/local_notification_service.dart delete mode 100644 lib/services/notification/notification.dart delete mode 100644 lib/services/notification/notification_service.dart delete mode 100644 lib/services/notification/stub_notification_service.dart diff --git a/lib/services/notification/constants.dart b/lib/services/notification/constants.dart deleted file mode 100644 index 5d1b2357..00000000 --- a/lib/services/notification/constants.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:flutter_local_notifications/flutter_local_notifications.dart'; - -const mainChannel = AndroidNotificationChannel( - "com.hiddify.hiddify", - "Hiddify Next", - importance: Importance.high, - enableVibration: false, - playSound: false, -); diff --git a/lib/services/notification/local_notification_service.dart b/lib/services/notification/local_notification_service.dart deleted file mode 100644 index 9fa473b3..00000000 --- a/lib/services/notification/local_notification_service.dart +++ /dev/null @@ -1,107 +0,0 @@ -import 'dart:io'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter_local_notifications/flutter_local_notifications.dart'; -import 'package:hiddify/services/notification/constants.dart'; -import 'package:hiddify/services/notification/notification_service.dart'; -import 'package:hiddify/utils/utils.dart'; - -// TODO: rewrite - -@pragma('vm:entry-point') -void notificationTapBackground(NotificationResponse notificationResponse) { - // TODO: handle action -} - -// ignore: unreachable_from_main -class LocalNotificationService with InfraLogger implements NotificationService { - LocalNotificationService(this.flutterLocalNotificationsPlugin); - - final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin; - String? payload; - - @override - Future init() async { - loggy.debug('initializing'); - const initializationSettings = InitializationSettings( - android: AndroidInitializationSettings('mipmap/ic_launcher'), - linux: LinuxInitializationSettings(defaultActionName: "open"), - macOS: DarwinInitializationSettings(), - ); - - await _initDetails(); - await _initChannels(); - - await flutterLocalNotificationsPlugin.initialize( - initializationSettings, - onDidReceiveNotificationResponse: onDidReceiveNotificationResponse, - onDidReceiveBackgroundNotificationResponse: notificationTapBackground, - ); - } - - Future _initDetails() async { - if (kIsWeb || Platform.isLinux) return; - final initialDetails = - await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails(); - - if (initialDetails?.didNotificationLaunchApp ?? false) { - payload = initialDetails!.notificationResponse?.payload; - loggy.debug('app launched from notification, payload: $payload'); - // TODO: use payload - } - } - - Future _initChannels() async { - await flutterLocalNotificationsPlugin - .resolvePlatformSpecificImplementation< - AndroidFlutterLocalNotificationsPlugin>() - ?.createNotificationChannel(mainChannel); - } - - @override - void onDidReceiveNotificationResponse( - NotificationResponse notificationResponse, - ) { - // TODO: complete - loggy.debug('received notification response, $notificationResponse'); - } - - @override - Future showNotification({ - required int id, - required String title, - String? body, - NotificationDetails? details, - String? payload, - }) async { - loggy.debug('showing notification'); - await flutterLocalNotificationsPlugin.show( - id, - title, - body, - details ?? - NotificationDetails( - android: AndroidNotificationDetails( - mainChannel.id, - mainChannel.name, - ), - ), - payload: payload, - ); - } - - @override - Future removeNotification(int id) async { - loggy.debug('removing notification'); - await flutterLocalNotificationsPlugin.cancel(id); - } - - @override - Future grantPermission() async { - final result = await flutterLocalNotificationsPlugin - .resolvePlatformSpecificImplementation< - AndroidFlutterLocalNotificationsPlugin>() - ?.requestPermission(); - return result ?? false; - } -} diff --git a/lib/services/notification/notification.dart b/lib/services/notification/notification.dart deleted file mode 100644 index 7dcf27d0..00000000 --- a/lib/services/notification/notification.dart +++ /dev/null @@ -1 +0,0 @@ -export 'notification_service.dart'; diff --git a/lib/services/notification/notification_service.dart b/lib/services/notification/notification_service.dart deleted file mode 100644 index 7bda4d63..00000000 --- a/lib/services/notification/notification_service.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'dart:io'; - -import 'package:flutter_local_notifications/flutter_local_notifications.dart'; -import 'package:hiddify/services/notification/local_notification_service.dart'; -import 'package:hiddify/services/notification/stub_notification_service.dart'; - -abstract class NotificationService { - factory NotificationService() { - // HACK temporarily return stub for linux and mac as well - if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) { - return StubNotificationService(); - } - return LocalNotificationService(FlutterLocalNotificationsPlugin()); - } - - Future init(); - - void onDidReceiveNotificationResponse( - NotificationResponse notificationResponse, - ); - - Future grantPermission(); - - Future showNotification({ - required int id, - required String title, - String? body, - NotificationDetails? details, - String? payload, - }); - - Future removeNotification(int id); -} diff --git a/lib/services/notification/stub_notification_service.dart b/lib/services/notification/stub_notification_service.dart deleted file mode 100644 index ab445c29..00000000 --- a/lib/services/notification/stub_notification_service.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:flutter_local_notifications/flutter_local_notifications.dart'; -import 'package:hiddify/services/notification/notification_service.dart'; - -class StubNotificationService implements NotificationService { - @override - Future init() async { - return; - } - - @override - void onDidReceiveNotificationResponse( - NotificationResponse notificationResponse, - ) {} - - @override - Future removeNotification(int id) async {} - - @override - Future showNotification({ - required int id, - required String title, - String? body, - NotificationDetails? details, - String? payload, - }) async {} - - @override - Future grantPermission() async { - return true; - } -} From dedccdd7726776b09b337b8c2d0d6348a94d7283 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Sun, 10 Sep 2023 20:25:04 +0330 Subject: [PATCH 05/23] Add service restart --- lib/bootstrap.dart | 3 +- lib/data/data_providers.dart | 1 - lib/data/repository/core_facade_impl.dart | 51 ++++------ .../connectivity/connection_facade.dart | 11 --- lib/domain/core_facade.dart | 4 +- lib/domain/singbox/singbox_facade.dart | 9 +- .../connectivity/connectivity_controller.dart | 29 +++--- lib/gen/singbox_generated_bindings.dart | 38 +++++--- lib/services/connectivity/connectivity.dart | 3 - .../connectivity/connectivity_service.dart | 26 ----- .../desktop_connectivity_service.dart | 53 ----------- .../mobile_connectivity_service.dart | 94 ------------------- lib/services/service_providers.dart | 13 --- lib/services/singbox/ffi_singbox_service.dart | 75 ++++++++++----- .../singbox/mobile_singbox_service.dart | 71 +++++++++----- lib/services/singbox/shared.dart | 46 +++++++++ lib/services/singbox/singbox_service.dart | 17 +++- libcore | 2 +- 18 files changed, 221 insertions(+), 325 deletions(-) delete mode 100644 lib/domain/connectivity/connection_facade.dart delete mode 100644 lib/services/connectivity/connectivity.dart delete mode 100644 lib/services/connectivity/connectivity_service.dart delete mode 100644 lib/services/connectivity/desktop_connectivity_service.dart delete mode 100644 lib/services/connectivity/mobile_connectivity_service.dart create mode 100644 lib/services/singbox/shared.dart diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index 42343e08..5d73e6b8 100644 --- a/lib/bootstrap.dart +++ b/lib/bootstrap.dart @@ -90,8 +90,7 @@ Future initAppServices( _loggy.debug("initializing app services"); await Future.wait( [ - read(connectivityServiceProvider).init(), - read(notificationServiceProvider).init(), + read(singboxServiceProvider).init(), ], ); _loggy.debug('initialized app services'); diff --git a/lib/data/data_providers.dart b/lib/data/data_providers.dart index 46e27adc..d62220d1 100644 --- a/lib/data/data_providers.dart +++ b/lib/data/data_providers.dart @@ -58,6 +58,5 @@ CoreFacade coreFacade(CoreFacadeRef ref) => CoreFacadeImpl( ref.watch(singboxServiceProvider), ref.watch(filesEditorServiceProvider), ref.watch(clashApiProvider), - ref.watch(connectivityServiceProvider), () => ref.read(configOptionsProvider), ); diff --git a/lib/data/repository/core_facade_impl.dart b/lib/data/repository/core_facade_impl.dart index 19a15032..238d0a6d 100644 --- a/lib/data/repository/core_facade_impl.dart +++ b/lib/data/repository/core_facade_impl.dart @@ -9,7 +9,6 @@ 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/services/connectivity/connectivity.dart'; import 'package:hiddify/services/files_editor_service.dart'; import 'package:hiddify/services/singbox/singbox_service.dart'; import 'package:hiddify/utils/utils.dart'; @@ -19,14 +18,12 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { this.singbox, this.filesEditor, this.clash, - this.connectivity, this.configOptions, ); final SingboxService singbox; final FilesEditorService filesEditor; final ClashApi clash; - final ConnectivityService connectivity; final ConfigOptions Function() configOptions; bool _initialized = false; @@ -89,16 +86,14 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { } @override - TaskEither changeConfig(String fileName) { + TaskEither start(String fileName) { return exceptionHandler( () { final configPath = filesEditor.configPath(fileName); - loggy.debug("changing config to: $configPath"); return setup() .andThen(() => changeConfigOptions(configOptions())) .andThen( - () => - singbox.create(configPath).mapLeft(CoreServiceFailure.create), + () => singbox.start(configPath).mapLeft(CoreServiceFailure.start), ) .run(); }, @@ -107,17 +102,25 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { } @override - TaskEither start() { + TaskEither stop() { return exceptionHandler( - () => singbox.start().mapLeft(CoreServiceFailure.start).run(), + () => singbox.stop().mapLeft(CoreServiceFailure.other).run(), CoreServiceFailure.unexpected, ); } @override - TaskEither stop() { + TaskEither restart(String fileName) { return exceptionHandler( - () => singbox.stop().mapLeft(CoreServiceFailure.other).run(), + () { + final configPath = filesEditor.configPath(fileName); + return changeConfigOptions(configOptions()) + .andThen( + () => + singbox.restart(configPath).mapLeft(CoreServiceFailure.start), + ) + .run(); + }, CoreServiceFailure.unexpected, ); } @@ -160,7 +163,7 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { @override Stream> watchCoreStatus() { - return singbox.watchStatus().map((event) { + return singbox.watchStats().map((event) { final json = jsonDecode(event); return CoreStatus.fromJson(json as Map); }).handleExceptions( @@ -239,29 +242,7 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { ); } - @override - TaskEither connect() { - return exceptionHandler( - () async { - await connectivity.connect(); - return right(unit); - }, - CoreServiceFailure.unexpected, - ); - } - - @override - TaskEither disconnect() { - return exceptionHandler( - () async { - await connectivity.disconnect(); - return right(unit); - }, - CoreServiceFailure.unexpected, - ); - } - @override Stream watchConnectionStatus() => - connectivity.watchConnectionStatus(); + singbox.watchConnectionStatus(); } diff --git a/lib/domain/connectivity/connection_facade.dart b/lib/domain/connectivity/connection_facade.dart deleted file mode 100644 index 87b327d5..00000000 --- a/lib/domain/connectivity/connection_facade.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:fpdart/fpdart.dart'; -import 'package:hiddify/domain/connectivity/connection_status.dart'; -import 'package:hiddify/domain/core_service_failure.dart'; - -abstract interface class ConnectionFacade { - TaskEither connect(); - - TaskEither disconnect(); - - Stream watchConnectionStatus(); -} diff --git a/lib/domain/core_facade.dart b/lib/domain/core_facade.dart index 2679e5b1..a3ada4aa 100644 --- a/lib/domain/core_facade.dart +++ b/lib/domain/core_facade.dart @@ -1,6 +1,4 @@ import 'package:hiddify/domain/clash/clash.dart'; -import 'package:hiddify/domain/connectivity/connectivity.dart'; import 'package:hiddify/domain/singbox/singbox.dart'; -abstract interface class CoreFacade - implements SingboxFacade, ClashFacade, ConnectionFacade {} +abstract interface class CoreFacade implements SingboxFacade, ClashFacade {} diff --git a/lib/domain/singbox/singbox_facade.dart b/lib/domain/singbox/singbox_facade.dart index d8714e2e..496da38d 100644 --- a/lib/domain/singbox/singbox_facade.dart +++ b/lib/domain/singbox/singbox_facade.dart @@ -1,4 +1,5 @@ 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'; @@ -13,12 +14,12 @@ abstract interface class SingboxFacade { ConfigOptions options, ); - TaskEither changeConfig(String fileName); - - TaskEither start(); + TaskEither start(String fileName); TaskEither stop(); + TaskEither restart(String fileName); + Stream>> watchOutbounds(); TaskEither selectOutbound( @@ -28,6 +29,8 @@ abstract interface class SingboxFacade { TaskEither urlTest(String groupTag); + Stream watchConnectionStatus(); + Stream> watchCoreStatus(); Stream> watchLogs(); diff --git a/lib/features/common/connectivity/connectivity_controller.dart b/lib/features/common/connectivity/connectivity_controller.dart index 3e7a4ae1..23df1282 100644 --- a/lib/features/common/connectivity/connectivity_controller.dart +++ b/lib/features/common/connectivity/connectivity_controller.dart @@ -17,15 +17,14 @@ class ConnectivityController extends _$ConnectivityController with AppLogger { if (previous == null) return; final shouldReconnect = previous != next; if (shouldReconnect) { - loggy.debug("active profile modified, reconnect"); - await reconnect(); + await reconnect(next?.id); } }, ); - return _connectivity.watchConnectionStatus(); + return _core.watchConnectionStatus(); } - CoreFacade get _connectivity => ref.watch(coreFacadeProvider); + CoreFacade get _core => ref.watch(coreFacadeProvider); Future toggleConnection() async { if (state case AsyncError()) { @@ -42,13 +41,16 @@ class ConnectivityController extends _$ConnectivityController with AppLogger { } } - Future reconnect() async { - if (state case AsyncData(:final value)) { - if (value case Connected()) { - loggy.debug("reconnecting"); - await _disconnect(); - await _connect(); + Future reconnect(String? profileId) async { + if (state case AsyncData(:final value) when value == const Connected()) { + if (profileId == null) { + return _disconnect(); } + loggy.debug("reconnecting, profile: [$profileId]"); + await _core.restart(profileId).mapLeft((l) { + loggy.warning("error reconnecting: $l"); + state = AsyncError(l, StackTrace.current); + }).run(); } } @@ -65,17 +67,14 @@ class ConnectivityController extends _$ConnectivityController with AppLogger { Future _connect() async { final activeProfile = await ref.read(activeProfileProvider.future); - await _connectivity - .changeConfig(activeProfile!.id) - .andThen(_connectivity.connect) - .mapLeft((l) { + await _core.start(activeProfile!.id).mapLeft((l) { loggy.warning("error connecting: $l"); state = AsyncError(l, StackTrace.current); }).run(); } Future _disconnect() async { - await _connectivity.disconnect().mapLeft((l) { + await _core.stop().mapLeft((l) { loggy.warning("error disconnecting: $l"); state = AsyncError(l, StackTrace.current); }).run(); diff --git a/lib/gen/singbox_generated_bindings.dart b/lib/gen/singbox_generated_bindings.dart index b83ee1f7..a0f8be3f 100644 --- a/lib/gen/singbox_generated_bindings.dart +++ b/lib/gen/singbox_generated_bindings.dart @@ -875,21 +875,23 @@ class SingboxNativeLibrary { ffi.Pointer baseDir, ffi.Pointer workingDir, ffi.Pointer tempDir, + int statusPort, ) { return _setup( baseDir, workingDir, tempDir, + statusPort, ); } late final _setupPtr = _lookup< ffi.NativeFunction< ffi.Void Function(ffi.Pointer, ffi.Pointer, - ffi.Pointer)>>('setup'); + ffi.Pointer, ffi.LongLong)>>('setup'); late final _setup = _setupPtr.asFunction< void Function(ffi.Pointer, ffi.Pointer, - ffi.Pointer)>(); + ffi.Pointer, int)>(); ffi.Pointer parse( ffi.Pointer path, @@ -920,28 +922,20 @@ class SingboxNativeLibrary { late final _changeConfigOptions = _changeConfigOptionsPtr .asFunction Function(ffi.Pointer)>(); - ffi.Pointer create( + ffi.Pointer start( ffi.Pointer configPath, ) { - return _create( + return _start( configPath, ); } - late final _createPtr = _lookup< + late final _startPtr = _lookup< ffi.NativeFunction< - ffi.Pointer Function(ffi.Pointer)>>('create'); - late final _create = _createPtr + ffi.Pointer Function(ffi.Pointer)>>('start'); + late final _start = _startPtr .asFunction Function(ffi.Pointer)>(); - ffi.Pointer start() { - return _start(); - } - - late final _startPtr = - _lookup Function()>>('start'); - late final _start = _startPtr.asFunction Function()>(); - ffi.Pointer stop() { return _stop(); } @@ -950,6 +944,20 @@ class SingboxNativeLibrary { _lookup Function()>>('stop'); late final _stop = _stopPtr.asFunction Function()>(); + ffi.Pointer restart( + ffi.Pointer configPath, + ) { + return _restart( + configPath, + ); + } + + late final _restartPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function(ffi.Pointer)>>('restart'); + late final _restart = _restartPtr + .asFunction Function(ffi.Pointer)>(); + ffi.Pointer startCommandClient( int command, int port, diff --git a/lib/services/connectivity/connectivity.dart b/lib/services/connectivity/connectivity.dart deleted file mode 100644 index 9b6161d6..00000000 --- a/lib/services/connectivity/connectivity.dart +++ /dev/null @@ -1,3 +0,0 @@ -export 'connectivity_service.dart'; -export 'desktop_connectivity_service.dart'; -export 'mobile_connectivity_service.dart'; diff --git a/lib/services/connectivity/connectivity_service.dart b/lib/services/connectivity/connectivity_service.dart deleted file mode 100644 index 2eb86893..00000000 --- a/lib/services/connectivity/connectivity_service.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:hiddify/domain/connectivity/connectivity.dart'; -import 'package:hiddify/services/connectivity/desktop_connectivity_service.dart'; -import 'package:hiddify/services/connectivity/mobile_connectivity_service.dart'; -import 'package:hiddify/services/notification/notification.dart'; -import 'package:hiddify/services/singbox/singbox_service.dart'; -import 'package:hiddify/utils/utils.dart'; - -abstract class ConnectivityService { - factory ConnectivityService( - SingboxService singboxService, - NotificationService notificationService, - ) { - if (PlatformUtils.isDesktop) { - return DesktopConnectivityService(singboxService); - } - return MobileConnectivityService(singboxService, notificationService); - } - - Future init(); - - Stream watchConnectionStatus(); - - Future connect(); - - Future disconnect(); -} diff --git a/lib/services/connectivity/desktop_connectivity_service.dart b/lib/services/connectivity/desktop_connectivity_service.dart deleted file mode 100644 index 1e0d6885..00000000 --- a/lib/services/connectivity/desktop_connectivity_service.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:hiddify/domain/connectivity/connectivity.dart'; -import 'package:hiddify/domain/core_service_failure.dart'; -import 'package:hiddify/services/connectivity/connectivity_service.dart'; -import 'package:hiddify/services/singbox/singbox_service.dart'; -import 'package:hiddify/utils/utils.dart'; -import 'package:rxdart/rxdart.dart'; - -class DesktopConnectivityService - with InfraLogger - implements ConnectivityService { - DesktopConnectivityService(this._singboxService); - - final SingboxService _singboxService; - - late final BehaviorSubject _connectionStatus; - - @override - Future init() async { - loggy.debug("initializing"); - _connectionStatus = - BehaviorSubject.seeded(const ConnectionStatus.disconnected()); - } - - @override - Stream watchConnectionStatus() => _connectionStatus; - - @override - Future connect() async { - loggy.debug('connecting'); - _connectionStatus.value = const ConnectionStatus.connecting(); - await _singboxService.start().match( - (err) { - _connectionStatus.value = ConnectionStatus.disconnected( - CoreConnectionFailure( - CoreServiceStartFailure(err), - ), - ); - }, - (_) => _connectionStatus.value = const ConnectionStatus.connected(), - ).run(); - } - - @override - Future disconnect() async { - loggy.debug("disconnecting"); - _connectionStatus.value = const ConnectionStatus.disconnecting(); - await _singboxService.stop().getOrElse((l) { - _connectionStatus.value = const ConnectionStatus.connected(); - throw l; - }).run(); - _connectionStatus.value = const ConnectionStatus.disconnected(); - } -} diff --git a/lib/services/connectivity/mobile_connectivity_service.dart b/lib/services/connectivity/mobile_connectivity_service.dart deleted file mode 100644 index a923216d..00000000 --- a/lib/services/connectivity/mobile_connectivity_service.dart +++ /dev/null @@ -1,94 +0,0 @@ -import 'package:flutter/services.dart'; -import 'package:hiddify/domain/connectivity/connectivity.dart'; -import 'package:hiddify/domain/core_service_failure.dart'; -import 'package:hiddify/services/connectivity/connectivity_service.dart'; -import 'package:hiddify/services/notification/notification.dart'; -import 'package:hiddify/services/singbox/singbox_service.dart'; -import 'package:hiddify/utils/utils.dart'; -import 'package:rxdart/rxdart.dart'; - -// TODO: rewrite -class MobileConnectivityService - with InfraLogger - implements ConnectivityService { - MobileConnectivityService(this.singbox, this.notifications); - - final SingboxService singbox; - final NotificationService notifications; - - late final EventChannel _statusChannel; - late final EventChannel _alertsChannel; - late final ValueStream _connectionStatus; - - static CoreServiceFailure fromServiceAlert(String key, String? message) { - return switch (key) { - "EmptyConfiguration" => InvalidConfig(message), - "StartCommandServer" || - "CreateService" => - CoreServiceCreateFailure(message), - "StartService" => CoreServiceStartFailure(message), - _ => const CoreServiceOtherFailure(), - }; - } - - static ConnectionStatus fromServiceEvent(dynamic event) { - final status = event['status'] as String; - late ConnectionStatus connectionStatus; - switch (status) { - case "Stopped": - final failure = event["failure"] 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; - } - - @override - Future init() async { - loggy.debug("initializing"); - _statusChannel = const EventChannel("com.hiddify.app/service.status"); - _alertsChannel = const EventChannel("com.hiddify.app/service.alerts"); - final status = - _statusChannel.receiveBroadcastStream().map(fromServiceEvent); - final alerts = - _alertsChannel.receiveBroadcastStream().map(fromServiceEvent); - _connectionStatus = - ValueConnectableStream(Rx.merge([status, alerts])).autoConnect(); - await _connectionStatus.first; - } - - @override - Stream watchConnectionStatus() => _connectionStatus; - - @override - Future connect() async { - loggy.debug("connecting"); - await notifications.grantPermission(); - await singbox.start().getOrElse((l) => throw l).run(); - } - - @override - Future disconnect() async { - loggy.debug("disconnecting"); - await singbox.stop().getOrElse((l) => throw l).run(); - } -} diff --git a/lib/services/service_providers.dart b/lib/services/service_providers.dart index 4c6222aa..87df56f8 100644 --- a/lib/services/service_providers.dart +++ b/lib/services/service_providers.dart @@ -1,6 +1,4 @@ -import 'package:hiddify/services/connectivity/connectivity.dart'; import 'package:hiddify/services/files_editor_service.dart'; -import 'package:hiddify/services/notification/notification.dart'; import 'package:hiddify/services/platform_settings.dart'; import 'package:hiddify/services/runtime_details_service.dart'; import 'package:hiddify/services/singbox/singbox_service.dart'; @@ -8,10 +6,6 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'service_providers.g.dart'; -@Riverpod(keepAlive: true) -NotificationService notificationService(NotificationServiceRef ref) => - NotificationService(); - @Riverpod(keepAlive: true) FilesEditorService filesEditorService(FilesEditorServiceRef ref) => FilesEditorService(); @@ -23,13 +17,6 @@ RuntimeDetailsService runtimeDetailsService(RuntimeDetailsServiceRef ref) => @Riverpod(keepAlive: true) SingboxService singboxService(SingboxServiceRef ref) => SingboxService(); -@Riverpod(keepAlive: true) -ConnectivityService connectivityService(ConnectivityServiceRef ref) => - ConnectivityService( - ref.watch(singboxServiceProvider), - ref.watch(notificationServiceProvider), - ); - @riverpod PlatformSettings platformSettings(PlatformSettingsRef ref) => PlatformSettings(); diff --git a/lib/services/singbox/ffi_singbox_service.dart b/lib/services/singbox/ffi_singbox_service.dart index 1f56e1fe..69582c6b 100644 --- a/lib/services/singbox/ffi_singbox_service.dart +++ b/lib/services/singbox/ffi_singbox_service.dart @@ -7,18 +7,25 @@ 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/config_options.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/utils/utils.dart'; import 'package:loggy/loggy.dart'; import 'package:path/path.dart' as p; +import 'package:rxdart/rxdart.dart'; final _logger = Loggy('FFISingboxService'); -class FFISingboxService with InfraLogger implements SingboxService { +class FFISingboxService + with ServiceStatus, InfraLogger + implements SingboxService { static final SingboxNativeLibrary _box = _gen(); + late final ValueStream _connectionStatus; + late final ReceivePort _connectionStatusReceiver; Stream? _statusStream; Stream? _groupsStream; @@ -39,21 +46,37 @@ class FFISingboxService with InfraLogger implements SingboxService { return SingboxNativeLibrary(lib); } + @override + Future init() async { + loggy.debug("initializing"); + _connectionStatusReceiver = ReceivePort('service status receiver'); + final source = _connectionStatusReceiver + .asBroadcastStream() + .map((event) => jsonDecode(event as String) as Map) + .map(mapEventToStatus); + _connectionStatus = ValueConnectableStream.seeded( + source, + const ConnectionStatus.disconnected(), + ).autoConnect(); + } + @override TaskEither setup( String baseDir, String workingDir, String tempDir, ) { + final port = _connectionStatusReceiver.sendPort.nativePort; return TaskEither( () => CombineWorker().execute( () { + _box.setupOnce(NativeApi.initializeApiDLData); _box.setup( baseDir.toNativeUtf8().cast(), workingDir.toNativeUtf8().cast(), tempDir.toNativeUtf8().cast(), + port, ); - _box.setupOnce(NativeApi.initializeApiDLData); return right(unit); }, ), @@ -98,29 +121,14 @@ class FFISingboxService with InfraLogger implements SingboxService { } @override - TaskEither create(String configPath) { - return TaskEither( - () => CombineWorker().execute( - () async { - final err = _box - .create(configPath.toNativeUtf8().cast()) - .cast() - .toDartString(); - if (err.isNotEmpty) { - return left(err); - } - return right(unit); - }, - ), - ); - } - - @override - TaskEither start() { + TaskEither start(String configPath) { return TaskEither( () => CombineWorker().execute( () { - final err = _box.start().cast().toDartString(); + final err = _box + .start(configPath.toNativeUtf8().cast()) + .cast() + .toDartString(); if (err.isNotEmpty) { return left(err); } @@ -146,7 +154,28 @@ class FFISingboxService with InfraLogger implements SingboxService { } @override - Stream watchStatus() { + TaskEither restart(String configPath) { + return TaskEither( + () => CombineWorker().execute( + () { + final err = _box + .restart(configPath.toNativeUtf8().cast()) + .cast() + .toDartString(); + if (err.isNotEmpty) { + return left(err); + } + return right(unit); + }, + ), + ); + } + + @override + Stream watchConnectionStatus() => _connectionStatus; + + @override + Stream watchStats() { if (_statusStream != null) return _statusStream!; final receiver = ReceivePort('status receiver'); final statusStream = receiver.asBroadcastStream( diff --git a/lib/services/singbox/mobile_singbox_service.dart b/lib/services/singbox/mobile_singbox_service.dart index 4d9b4f22..23708a8d 100644 --- a/lib/services/singbox/mobile_singbox_service.dart +++ b/lib/services/singbox/mobile_singbox_service.dart @@ -2,15 +2,36 @@ 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:rxdart/rxdart.dart'; -class MobileSingboxService with InfraLogger implements SingboxService { - late final MethodChannel _methodChannel = - const MethodChannel("com.hiddify.app/method"); - late final EventChannel _logsChannel = - const EventChannel("com.hiddify.app/service.logs"); +class MobileSingboxService + with ServiceStatus, InfraLogger + implements SingboxService { + late final _methodChannel = const MethodChannel("com.hiddify.app/method"); + 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; + + @override + Future init() async { + loggy.debug("initializing"); + final status = + _statusChannel.receiveBroadcastStream().map(mapEventToStatus); + final alerts = + _alertsChannel.receiveBroadcastStream().map(mapEventToStatus); + _connectionStatus = + ValueConnectableStream(Rx.merge([status, alerts])).autoConnect(); + await _connectionStatus.first; + } @override TaskEither setup( @@ -48,25 +69,14 @@ class MobileSingboxService with InfraLogger implements SingboxService { } @override - TaskEither create(String configPath) { - return TaskEither( - () async { - loggy.debug("creating service for: $configPath"); - await _methodChannel.invokeMethod( - "set_active_config_path", - {"path": configPath}, - ); - return right(unit); - }, - ); - } - - @override - TaskEither start() { + TaskEither start(String configPath) { return TaskEither( () async { loggy.debug("starting"); - await _methodChannel.invokeMethod("start"); + await _methodChannel.invokeMethod( + "start", + {"path": configPath}, + ); return right(unit); }, ); @@ -83,6 +93,20 @@ class MobileSingboxService with InfraLogger implements SingboxService { ); } + @override + TaskEither restart(String configPath) { + return TaskEither( + () async { + loggy.debug("restarting"); + await _methodChannel.invokeMethod( + "restart", + {"path": configPath}, + ); + return right(unit); + }, + ); + } + @override Stream watchOutbounds() { const channel = EventChannel("com.hiddify.app/groups"); @@ -99,7 +123,10 @@ class MobileSingboxService with InfraLogger implements SingboxService { } @override - Stream watchStatus() { + Stream watchConnectionStatus() => _connectionStatus; + + @override + Stream watchStats() { // TODO: implement watchStatus return const Stream.empty(); } diff --git a/lib/services/singbox/shared.dart b/lib/services/singbox/shared.dart new file mode 100644 index 00000000..6c34b189 --- /dev/null +++ b/lib/services/singbox/shared.dart @@ -0,0 +1,46 @@ +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 index 3ea8bdd5..6b8451fb 100644 --- a/lib/services/singbox/singbox_service.dart +++ b/lib/services/singbox/singbox_service.dart @@ -1,6 +1,7 @@ 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'; @@ -9,10 +10,14 @@ abstract interface class SingboxService { factory SingboxService() { if (Platform.isAndroid) { return MobileSingboxService(); + } else if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) { + return FFISingboxService(); } - return FFISingboxService(); + throw Exception("unsupported platform"); } + Future init(); + TaskEither setup( String baseDir, String workingDir, @@ -23,19 +28,21 @@ abstract interface class SingboxService { TaskEither changeConfigOptions(ConfigOptions options); - TaskEither create(String configPath); - - TaskEither start(); + TaskEither start(String configPath); TaskEither stop(); + TaskEither restart(String configPath); + Stream watchOutbounds(); TaskEither selectOutbound(String groupTag, String outboundTag); TaskEither urlTest(String groupTag); - Stream watchStatus(); + Stream watchConnectionStatus(); + + Stream watchStats(); Stream watchLogs(String path); } diff --git a/libcore b/libcore index 30cb888f..c8878156 160000 --- a/libcore +++ b/libcore @@ -1 +1 @@ -Subproject commit 30cb888fa7216581472a706cf5ef852b1086974a +Subproject commit c8878156767c0795324b1a7dc737e026b33fa10f From cd37f5124a3567eef6595e05653ae09a17098804 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Sun, 10 Sep 2023 20:27:07 +0330 Subject: [PATCH 06/23] Change proxies lifecycle --- assets/translations/strings.i18n.json | 8 +++---- assets/translations/strings_fa.i18n.json | 6 ++--- lib/domain/profiles/profile.dart | 2 +- lib/features/common/stats/stats_overview.dart | 12 +++++----- .../proxies/notifier/proxies_notifier.dart | 2 ++ lib/utils/riverpod_utils.dart | 23 +++++++++++++++++++ 6 files changed, 37 insertions(+), 16 deletions(-) create mode 100644 lib/utils/riverpod_utils.dart diff --git a/assets/translations/strings.i18n.json b/assets/translations/strings.i18n.json index 0aacd383..c9ac05bf 100644 --- a/assets/translations/strings.i18n.json +++ b/assets/translations/strings.i18n.json @@ -21,9 +21,7 @@ }, "stats": { "traffic": "Live Traffic", - "trafficTotal": "Total Traffic", - "uplink": "↑", - "downlink": "↓" + "trafficTotal": "Total Traffic" } }, "profile": { @@ -32,7 +30,7 @@ "subscription": { "traffic": "Traffic", "updatedTimeAgo": "Updated ${timeago}", - "remainingDuration": "📅 ${duration} Days Remaining", + "remainingDuration": "${duration} Days Remaining", "expired": "Expired", "noTraffic": "No more traffic" }, @@ -109,7 +107,7 @@ "silentStart": "Silent Start", "openWorkingDir": "Open Working Directory", "ignoreBatteryOptimizations": "Ignore Battery Optimization", - "ignoreBatteryOptimizationsMsg": "Remove restrictions for optimal VPN performance" + "ignoreBatteryOptimizationsMsg": "Remove restrictions for optimal VPN performance" }, "advanced": { "sectionTitle": "Advanced", diff --git a/assets/translations/strings_fa.i18n.json b/assets/translations/strings_fa.i18n.json index 5ca75ce1..74c8b6a9 100644 --- a/assets/translations/strings_fa.i18n.json +++ b/assets/translations/strings_fa.i18n.json @@ -21,9 +21,7 @@ }, "stats": { "traffic": "مصرف لحظه‌ای", - "trafficTotal": "مصرف کل", - "uplink": "↑", - "downlink": "↓" + "trafficTotal": "مصرف کل" } }, "profile": { @@ -32,7 +30,7 @@ "subscription": { "traffic": "ترافیک", "updatedTimeAgo": "بروزرسانی شده در ${timeago}", - "remainingDuration": "📅 ${duration} روز باقی مانده", + "remainingDuration": "${duration} روز باقی مانده", "expired": "منقضی شده", "noTraffic": "پایان ترافیک" }, diff --git a/lib/domain/profiles/profile.dart b/lib/domain/profiles/profile.dart index 62768888..6976151d 100644 --- a/lib/domain/profiles/profile.dart +++ b/lib/domain/profiles/profile.dart @@ -45,7 +45,7 @@ class Profile with _$Profile { if (title.isEmpty) { final contentDisposition = headers['content-disposition']?.single; if (contentDisposition != null) { - final RegExp regExp = RegExp(r'filename="([^"]*)"'); + final RegExp regExp = RegExp('filename="([^"]*)"'); final match = regExp.firstMatch(contentDisposition); if (match != null && match.groupCount >= 1) { title = match.group(1) ?? ''; diff --git a/lib/features/common/stats/stats_overview.dart b/lib/features/common/stats/stats_overview.dart index 3e83ea16..92599321 100644 --- a/lib/features/common/stats/stats_overview.dart +++ b/lib/features/common/stats/stats_overview.dart @@ -24,11 +24,11 @@ class StatsOverview extends HookConsumerWidget { _StatCard( title: t.home.stats.traffic, firstStat: ( - label: t.home.stats.uplink, + label: "↑", data: stats.uplink.speed(), ), secondStat: ( - label: t.home.stats.downlink, + label: "↓", data: stats.downlink.speed(), ), ), @@ -36,11 +36,11 @@ class StatsOverview extends HookConsumerWidget { _StatCard( title: t.home.stats.trafficTotal, firstStat: ( - label: t.home.stats.uplink, + label: "↑", data: stats.uplinkTotal.size(), ), secondStat: ( - label: t.home.stats.downlink, + label: "↓", data: stats.downlinkTotal.size(), ), ), @@ -80,7 +80,7 @@ class _StatCard extends HookConsumerWidget { children: [ Text( firstStat.label, - style: TextStyle(color: Colors.green), + style: const TextStyle(color: Colors.green), ), Text( firstStat.data, @@ -93,7 +93,7 @@ class _StatCard extends HookConsumerWidget { children: [ Text( secondStat.label, - style: TextStyle(color: Colors.red), + style: TextStyle(color: theme.colorScheme.error), ), Text( secondStat.data, diff --git a/lib/features/proxies/notifier/proxies_notifier.dart b/lib/features/proxies/notifier/proxies_notifier.dart index 5613c8e6..f7a49c66 100644 --- a/lib/features/proxies/notifier/proxies_notifier.dart +++ b/lib/features/proxies/notifier/proxies_notifier.dart @@ -8,6 +8,7 @@ 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/utils/pref_notifier.dart'; +import 'package:hiddify/utils/riverpod_utils.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -36,6 +37,7 @@ final proxiesSortProvider = AlwaysAlivePrefNotifier.provider( class ProxiesNotifier extends _$ProxiesNotifier with AppLogger { @override Stream> build() async* { + ref.disposeDelay(const Duration(seconds: 15)); final serviceRunning = await ref.watch(serviceRunningProvider.future); if (!serviceRunning) { throw const CoreServiceNotRunning(); diff --git a/lib/utils/riverpod_utils.dart b/lib/utils/riverpod_utils.dart new file mode 100644 index 00000000..2c7db43d --- /dev/null +++ b/lib/utils/riverpod_utils.dart @@ -0,0 +1,23 @@ +import 'dart:async'; + +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +extension RefLifeCycle on AutoDisposeRef { + void disposeDelay(Duration duration) { + final link = keepAlive(); + Timer? timer; + + onCancel(() { + timer?.cancel(); + timer = Timer(duration, link.close); + }); + + onDispose(() { + timer?.cancel(); + }); + + onResume(() { + timer?.cancel(); + }); + } +} From 93375728f3a3669463d4dbd8228320ce85cc6eb1 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Sun, 10 Sep 2023 20:34:46 +0330 Subject: [PATCH 07/23] Fix barrel file --- lib/domain/connectivity/connectivity.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/domain/connectivity/connectivity.dart b/lib/domain/connectivity/connectivity.dart index c161cf81..8f2bb7cc 100644 --- a/lib/domain/connectivity/connectivity.dart +++ b/lib/domain/connectivity/connectivity.dart @@ -1,3 +1,2 @@ -export 'connection_facade.dart'; export 'connection_failure.dart'; export 'connection_status.dart'; From 73a28576bf18b354d6399db9a0b64b4d4b06fd03 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Mon, 11 Sep 2023 19:40:37 +0330 Subject: [PATCH 08/23] Add core version --- Makefile | 33 +++++++++++++++++---------------- dependencies.properties | 1 + libcore | 2 +- 3 files changed, 19 insertions(+), 17 deletions(-) create mode 100644 dependencies.properties diff --git a/Makefile b/Makefile index 0fb381c9..5cf3f074 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,13 @@ +include dependencies.properties + BINDIR=./libcore/bin ANDROID_OUT=./android/app/libs DESKTOP_OUT=./libcore/bin GEO_ASSETS_DIR=./assets/core -LIBS_DOWNLOAD_URL=https://github.com/hiddify/hiddify-next-core/releases/download/draft + +CORE_NAME=hiddify-libcore +CORE_VERSION=v$(core.version) +LIBS_DOWNLOAD_URL=https://github.com/hiddify/hiddify-next-core/releases/download/$(CORE_VERSION) get: flutter pub get @@ -28,52 +33,48 @@ linux-release: macos-release: flutter_distributor package --platform macos --targets dmg --skip-clean - # flutter build macos --release &&\ - # tree ./build/macos/Build &&\ - # create-dmg --app-drop-link 600 185 "hiddify-macos-universal.dmg" ./build/macos/Build/Products/Release/hiddify.app ios-release: #not tested flutter_distributor package --platform ios --targets ipa --build-export-options-plist ios/exportOptions.plist android-libs: mkdir -p $(ANDROID_OUT) - curl -L $(LIBS_DOWNLOAD_URL)/hiddify-libcore-android.aar.gz | gunzip > $(ANDROID_OUT)/libcore.aar + curl -L $(LIBS_DOWNLOAD_URL)/$(CORE_NAME)-android-$(CORE_VERSION).aar.gz | gunzip > $(ANDROID_OUT)/libcore.aar windows-libs: mkdir -p $(DESKTOP_OUT) - curl -L $(LIBS_DOWNLOAD_URL)/hiddify-libcore-windows-amd64.dll.gz | gunzip > $(DESKTOP_OUT)/libcore.dll + curl -L $(LIBS_DOWNLOAD_URL)/$(CORE_NAME)-windows-amd64-$(CORE_VERSION).dll.gz | gunzip > $(DESKTOP_OUT)/libcore.dll linux-libs: mkdir -p $(DESKTOP_OUT) - curl -L $(LIBS_DOWNLOAD_URL)/hiddify-libcore-linux-amd64.so.gz | gunzip > $(DESKTOP_OUT)/libcore.so + curl -L $(LIBS_DOWNLOAD_URL)/$(CORE_NAME)-linux-amd64-$(CORE_VERSION).so.gz | gunzip > $(DESKTOP_OUT)/libcore.so macos-libs: mkdir -p $(DESKTOP_OUT)/ &&\ - curl -L $(LIBS_DOWNLOAD_URL)/hiddify-libcore-macos-universal.dylib.gz | gunzip > $(DESKTOP_OUT)/libcore.dylib + curl -L $(LIBS_DOWNLOAD_URL)/$(CORE_NAME)-macos-universal-$(CORE_VERSION).dylib.gz | gunzip > $(DESKTOP_OUT)/libcore.dylib ios-libs: #not tested mkdir -p $(DESKTOP_OUT)/ &&\ - curl -L $(LIBS_DOWNLOAD_URL)/hiddify-libcore-ios-universal.xcframework.gz | gunzip > $(DESKTOP_OUT)/libcore.xcframework + curl -L $(LIBS_DOWNLOAD_URL)/$(CORE_NAME)-ios-universal-$(CORE_VERSION).xcframework.gz | gunzip > $(DESKTOP_OUT)/libcore.xcframework get-geo-assets: curl -L https://github.com/SagerNet/sing-geoip/releases/latest/download/geoip.db -o $(GEO_ASSETS_DIR)/geoip.db curl -L https://github.com/SagerNet/sing-geosite/releases/latest/download/geosite.db -o $(GEO_ASSETS_DIR)/geosite.db build-headers: - make -C libcore -f Makefile headers && mv $(BINDIR)/hiddify-libcore-headers.h $(BINDIR)/libcore.h + make -C libcore -f Makefile headers && mv $(BINDIR)/$(CORE_NAME)-headers-*.h $(BINDIR)/libcore.h build-android-libs: - make -C libcore -f Makefile android && mv $(BINDIR)/hiddify-libcore-android.aar $(ANDROID_OUT)/libcore.aar + make -C libcore -f Makefile android && mv $(BINDIR)/$(CORE_NAME)-android-*.aar $(ANDROID_OUT)/libcore.aar build-windows-libs: - make -C libcore -f Makefile windows-amd64 && mv $(BINDIR)/hiddify-libcore-windows-amd64.dll $(DESKTOP_OUT)/libcore.dll + 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)/hiddify-libcore-linux-amd64.dll $(DESKTOP_OUT)/libcore.so + make -C libcore -f Makefile linux-amd64 && mv $(BINDIR)/$(CORE_NAME)-linux-amd64-*.dll $(DESKTOP_OUT)/libcore.so build-macos-libs: - make -C libcore -f Makefile macos-universal && mv $(BINDIR)/hiddify-libcore-macos-universal.dylib $(DESKTOP_OUT)/libcore.dylib - + make -C libcore -f Makefile macos-universal && mv $(BINDIR)/$(CORE_NAME)-macos-universal-*.dylib $(DESKTOP_OUT)/libcore.dylib build-ios-libs: #not tested - make -C libcore -f Makefile ios && mv $(BINDIR)/hiddify-libcore-ios.xcframework $(DESKTOP_OUT)/libcore.xcframework \ No newline at end of file + make -C libcore -f Makefile ios && mv $(BINDIR)/$(CORE_NAME)-ios-*.xcframework $(DESKTOP_OUT)/libcore.xcframework \ No newline at end of file diff --git a/dependencies.properties b/dependencies.properties new file mode 100644 index 00000000..5bef9ce7 --- /dev/null +++ b/dependencies.properties @@ -0,0 +1 @@ +core.version=0.1.0 \ No newline at end of file diff --git a/libcore b/libcore index c8878156..dd711cba 160000 --- a/libcore +++ b/libcore @@ -1 +1 @@ -Subproject commit c8878156767c0795324b1a7dc737e026b33fa10f +Subproject commit dd711cba20e5cfbc404b4660dd0e74d82f07bd1b From 2d6b879cb874df09ad884256c5f33eee31e344d7 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Mon, 11 Sep 2023 20:24:55 +0330 Subject: [PATCH 09/23] Update ci --- .github/workflows/ci.yml | 159 +++++++++++++++++++++++---------------- .prettierrc | 10 +++ Makefile | 15 ++-- 3 files changed, 115 insertions(+), 69 deletions(-) create mode 100644 .prettierrc diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 744a38c1..d529aaf5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,16 +1,24 @@ name: Build - on: push: - branches: [ "main","temp" ] + branches: + - main + tags: + - 'v*' + paths-ignore: + - '**.md' + - 'docs/**' + - '.github/**' + - '!.github/workflows/ci.yml' pull_request: - branches: [ "main" ] -permissions: write-all + branches: + - main concurrency: group: ${{ github.ref }}-${{ github.workflow }} cancel-in-progress: true jobs: build: + permissions: write-all strategy: fail-fast: false matrix: @@ -28,18 +36,10 @@ jobs: aarch: amd64 targets: exe - # Flutter does not support x86 - # - platform: windows - # os: windows-latest - # aarch: 386 - # targets: exe - # target: windows-x86 - - # Flutter does not support x86 - # - platform: linux - # os: ubuntu-latest - # aarch: 386 - # targets: AppImage + - platform: linux + os: ubuntu-latest + aarch: amd64 + targets: AppImage - platform: macos os: macos-11 @@ -51,12 +51,6 @@ jobs: # aarch: universal # targets: ipa - - platform: linux - os: ubuntu-latest - aarch: amd64 - targets: AppImage - - runs-on: ${{ matrix.os }} steps: - name: checkout @@ -79,22 +73,22 @@ jobs: - name: Setup Flutter uses: subosito/flutter-action@v2 with: - # flutter-version: '3.13.x' + flutter-version: '3.13.x' channel: 'stable' - # cache: true - - - name: Build macos dmg - if: matrix.platform == 'macos' || matrix.platform == 'ios' - run: | - brew install create-dmg tree node - npm install -g appdmg + 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 Flutter Distributor + if: ${{ !startsWith(matrix.platform,'android') }} + run: | + dart pub global activate flutter_distributor + - name: Setup Linux dependencies if: matrix.platform == 'linux' run: | @@ -104,11 +98,11 @@ jobs: chmod +x appimagetool mv appimagetool /usr/local/bin/ - - name: Setup Flutter Distributor - if: ${{ !startsWith(matrix.platform,'android') }} -# shell: powershell + - name: Build macos dmg + if: matrix.platform == 'macos' || matrix.platform == 'ios' run: | - dart pub global activate flutter_distributor + brew install create-dmg tree node + npm install -g appdmg - name: Get Geo Assets run: | @@ -127,7 +121,7 @@ jobs: run: | make ${{ matrix.platform }}-libs - - name: Signing properties + - name: Setup Signing Properties env: ANDROID_SIGNING_KEY_ALIAS: ${{ secrets.ANDROID_SIGNING_KEY_ALIAS }} ANDROID_SIGNING_KEY_PASSWORD: ${{ secrets.ANDROID_SIGNING_KEY_PASSWORD }} @@ -145,11 +139,12 @@ jobs: cp release.* app/ cat release.properties cd .. + - name: Release ${{ matrix.platform }} - run: | + run: | make ${{ matrix.platform }}-release - - - name: Copy to out WINDOWS + + - name: Copy to out Windows if: matrix.platform == 'windows' run: | New-Item -ItemType Directory -Force -Name "out" @@ -160,10 +155,10 @@ jobs: move out\*setup.$EXT out\hiddify-${{ matrix.platform }}-x64-setup.$EXT Get-ChildItem -Recurse -File -Filter "*.$EXT" } - mkdir HiddifyNext-portable xcopy /s /v D:\a\hiddify-next\hiddify-next\build\windows\runner\Release\ .\HiddifyNext-portable\ Compress-Archive HiddifyNext-portable out\hiddify-${{ matrix.platform }}-x64-portable.zip + - name: Copy to out Android if: matrix.platform == 'android' run: | @@ -174,10 +169,6 @@ jobs: mv ./build/app/outputs/flutter-apk/*x86_64*.apk out/hiddify-android-x86_64.apk || echo "no x64 apk" mv ./build/app/outputs/flutter-apk/app-release.apk out/hiddify-android-universal.apk || echo "no universal apk" cp ./build/app/outputs/bundle/release/app-release.aab out/bundle.aab - - - - - name: Copy to out unix if: matrix.platform == 'linux' || matrix.platform == 'macos' || matrix.platform == 'ios' @@ -190,33 +181,17 @@ jobs: # Find all files with this extension in SRC_DIR, and copy them to DST_DIR find "." -type f -name "*.$EXT" -exec cp {} "out" \; done - # mv out/*arm64-v8a*.apk out/hiddify-android-arm64.apk || echo "no arm64 apk" - # mv out/*armeabi-v7a*.apk out/hiddify-android-arm7.apk || echo "no arm7 apk" - # mv out/*x86_64*.apk out/hiddify-android-x86_64.apk || echo "no x64 apk" - # mv out/*universal*.apk out/hiddify-android-universal.apk || echo "no universal apk" - #rm out/app-release.apk ||echo "no app-release.apk" - #mv out/*.aab out/hiddify-android-playstore.aab || echo "no aab" mv out/*.AppImage out/hiddify-linux-x64.AppImage || echo "no app image" (cd out&& chmod +x hiddify-linux-x64.AppImage && 7z a hiddify-linux-x64.AppImage.zip hiddify-linux-x64.AppImage)||echo "no app image" mv out/*.dmg out/hiddify-macos-universal.dmg || echo "no macos dmg" - - uses: actions/upload-artifact@v3 + - name: Upload Artifact + uses: actions/upload-artifact@v3 with: name: ${{ matrix.platform }} path: ./out retention-days: 2 - - name: Create or Update Draft Release - uses: softprops/action-gh-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - files: ./out/* - name: "draft" - tag_name: "draft" - prerelease: true - overwrite: true - # - name: Create service_account.json # if: matrix.platform == 'android' # run: echo '${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}' > service_account.json @@ -229,4 +204,62 @@ jobs: # packageName: app.hiddify.com # releaseName: ${{ github.ref }} # releaseFiles: ./build/app/outputs/bundle/release/app-release.aab - # track: internal + # track: internal + + upload-prerelease: + permissions: write-all + if: ${{ github.ref_type=='branch' }} + needs: [build] + runs-on: ubuntu-latest + steps: + - name: Download Artifact + uses: actions/download-artifact@v3 + with: + name: artifact + path: ./out/ + + - name: Display Files Structure + run: ls -R + working-directory: ./out + + - name: Delete Current Release Assets + uses: 8Mi-Tech/delete-release-assets-action@main + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + tag: 'draft' + deleteOnlyFromDrafts: false + + - name: Create or Update Draft Release + uses: softprops/action-gh-release@v1 + if: ${{ success() }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + files: ./out/* + name: 'draft' + tag_name: 'draft' + prerelease: true + + upload-release: + permissions: write-all + if: ${{ github.ref_type=='tag' }} + needs: [build] + runs-on: ubuntu-latest + steps: + - uses: actions/download-artifact@v3 + with: + name: artifact + path: ./out/ + + - name: Display Files Structure + run: ls -R + working-directory: ./out + + - name: Upload Release + uses: softprops/action-gh-release@v1 + if: ${{ success() }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref_name }} + files: ./out/* diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..7f51a41a --- /dev/null +++ b/.prettierrc @@ -0,0 +1,10 @@ +{ + "overrides": [ + { + "files": ".github/**", + "options": { + "singleQuote": true + } + } + ] +} \ No newline at end of file diff --git a/Makefile b/Makefile index 5cf3f074..2aeb50ab 100644 --- a/Makefile +++ b/Makefile @@ -5,9 +5,12 @@ ANDROID_OUT=./android/app/libs DESKTOP_OUT=./libcore/bin GEO_ASSETS_DIR=./assets/core +BRANCH=$(shell git branch --show-current) +VERSION=$(shell git describe --tags --abbrev=0 || echo "unknown version") + CORE_NAME=hiddify-libcore CORE_VERSION=v$(core.version) -LIBS_DOWNLOAD_URL=https://github.com/hiddify/hiddify-next-core/releases/download/$(CORE_VERSION) +CORE_URL=https://github.com/hiddify/hiddify-next-core/releases/download/$(CORE_VERSION) get: flutter pub get @@ -39,23 +42,23 @@ ios-release: #not tested android-libs: mkdir -p $(ANDROID_OUT) - curl -L $(LIBS_DOWNLOAD_URL)/$(CORE_NAME)-android-$(CORE_VERSION).aar.gz | gunzip > $(ANDROID_OUT)/libcore.aar + curl -L $(CORE_URL)/$(CORE_NAME)-android-$(CORE_VERSION).aar.gz | gunzip > $(ANDROID_OUT)/libcore.aar windows-libs: mkdir -p $(DESKTOP_OUT) - curl -L $(LIBS_DOWNLOAD_URL)/$(CORE_NAME)-windows-amd64-$(CORE_VERSION).dll.gz | gunzip > $(DESKTOP_OUT)/libcore.dll + curl -L $(CORE_URL)/$(CORE_NAME)-windows-amd64-$(CORE_VERSION).dll.gz | gunzip > $(DESKTOP_OUT)/libcore.dll linux-libs: mkdir -p $(DESKTOP_OUT) - curl -L $(LIBS_DOWNLOAD_URL)/$(CORE_NAME)-linux-amd64-$(CORE_VERSION).so.gz | gunzip > $(DESKTOP_OUT)/libcore.so + curl -L $(CORE_URL)/$(CORE_NAME)-linux-amd64-$(CORE_VERSION).so.gz | gunzip > $(DESKTOP_OUT)/libcore.so macos-libs: mkdir -p $(DESKTOP_OUT)/ &&\ - curl -L $(LIBS_DOWNLOAD_URL)/$(CORE_NAME)-macos-universal-$(CORE_VERSION).dylib.gz | gunzip > $(DESKTOP_OUT)/libcore.dylib + curl -L $(CORE_URL)/$(CORE_NAME)-macos-universal-$(CORE_VERSION).dylib.gz | gunzip > $(DESKTOP_OUT)/libcore.dylib ios-libs: #not tested mkdir -p $(DESKTOP_OUT)/ &&\ - curl -L $(LIBS_DOWNLOAD_URL)/$(CORE_NAME)-ios-universal-$(CORE_VERSION).xcframework.gz | gunzip > $(DESKTOP_OUT)/libcore.xcframework + curl -L $(CORE_URL)/$(CORE_NAME)-ios-universal-$(CORE_VERSION).xcframework.gz | gunzip > $(DESKTOP_OUT)/libcore.xcframework get-geo-assets: curl -L https://github.com/SagerNet/sing-geoip/releases/latest/download/geoip.db -o $(GEO_ASSETS_DIR)/geoip.db From 94b8c8ddb4e438046f692579418d4984d7235e58 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Mon, 11 Sep 2023 20:54:52 +0330 Subject: [PATCH 10/23] Fix ci build --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d529aaf5..add7e5e3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -188,7 +188,7 @@ jobs: - name: Upload Artifact uses: actions/upload-artifact@v3 with: - name: ${{ matrix.platform }} + name: artifact path: ./out retention-days: 2 From d54917868bbb2151bc2ff1f8fe9ad1f5d323624b Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Mon, 11 Sep 2023 21:19:55 +0330 Subject: [PATCH 11/23] Release v0.1.0 --- Makefile | 27 +++++++++++++-------------- dependencies.properties | 2 +- libcore | 2 +- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/Makefile b/Makefile index 2aeb50ab..3d72ff45 100644 --- a/Makefile +++ b/Makefile @@ -9,8 +9,7 @@ BRANCH=$(shell git branch --show-current) VERSION=$(shell git describe --tags --abbrev=0 || echo "unknown version") CORE_NAME=hiddify-libcore -CORE_VERSION=v$(core.version) -CORE_URL=https://github.com/hiddify/hiddify-next-core/releases/download/$(CORE_VERSION) +CORE_URL=https://github.com/hiddify/hiddify-next-core/releases/download/v$(core.version) get: flutter pub get @@ -40,44 +39,44 @@ macos-release: ios-release: #not tested flutter_distributor package --platform ios --targets ipa --build-export-options-plist ios/exportOptions.plist -android-libs: +android-libs: mkdir -p $(ANDROID_OUT) - curl -L $(CORE_URL)/$(CORE_NAME)-android-$(CORE_VERSION).aar.gz | gunzip > $(ANDROID_OUT)/libcore.aar + curl -L $(CORE_URL)/$(CORE_NAME)-android.aar.gz | gunzip > $(ANDROID_OUT)/libcore.aar windows-libs: mkdir -p $(DESKTOP_OUT) - curl -L $(CORE_URL)/$(CORE_NAME)-windows-amd64-$(CORE_VERSION).dll.gz | gunzip > $(DESKTOP_OUT)/libcore.dll + curl -L $(CORE_URL)/$(CORE_NAME)-windows-amd64.dll.gz | gunzip > $(DESKTOP_OUT)/libcore.dll linux-libs: mkdir -p $(DESKTOP_OUT) - curl -L $(CORE_URL)/$(CORE_NAME)-linux-amd64-$(CORE_VERSION).so.gz | gunzip > $(DESKTOP_OUT)/libcore.so + curl -L $(CORE_URL)/$(CORE_NAME)-linux-amd64.so.gz | gunzip > $(DESKTOP_OUT)/libcore.so macos-libs: mkdir -p $(DESKTOP_OUT)/ &&\ - curl -L $(CORE_URL)/$(CORE_NAME)-macos-universal-$(CORE_VERSION).dylib.gz | gunzip > $(DESKTOP_OUT)/libcore.dylib + curl -L $(CORE_URL)/$(CORE_NAME)-macos-universal.dylib.gz | gunzip > $(DESKTOP_OUT)/libcore.dylib ios-libs: #not tested mkdir -p $(DESKTOP_OUT)/ &&\ - curl -L $(CORE_URL)/$(CORE_NAME)-ios-universal-$(CORE_VERSION).xcframework.gz | gunzip > $(DESKTOP_OUT)/libcore.xcframework + curl -L $(CORE_URL)/$(CORE_NAME)-ios-universal.xcframework.gz | gunzip > $(DESKTOP_OUT)/libcore.xcframework get-geo-assets: curl -L https://github.com/SagerNet/sing-geoip/releases/latest/download/geoip.db -o $(GEO_ASSETS_DIR)/geoip.db curl -L https://github.com/SagerNet/sing-geosite/releases/latest/download/geosite.db -o $(GEO_ASSETS_DIR)/geosite.db build-headers: - make -C libcore -f Makefile headers && mv $(BINDIR)/$(CORE_NAME)-headers-*.h $(BINDIR)/libcore.h + make -C libcore -f Makefile headers && mv $(BINDIR)/$(CORE_NAME)-headers.h $(BINDIR)/libcore.h build-android-libs: - make -C libcore -f Makefile android && mv $(BINDIR)/$(CORE_NAME)-android-*.aar $(ANDROID_OUT)/libcore.aar + make -C libcore -f Makefile android && mv $(BINDIR)/$(CORE_NAME)-android.aar $(ANDROID_OUT)/libcore.aar build-windows-libs: - make -C libcore -f Makefile windows-amd64 && mv $(BINDIR)/$(CORE_NAME)-windows-amd64-*.dll $(DESKTOP_OUT)/libcore.dll + 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.dll $(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 + make -C libcore -f Makefile macos-universal && mv $(BINDIR)/$(CORE_NAME)-macos-universal.dylib $(DESKTOP_OUT)/libcore.dylib build-ios-libs: #not tested - make -C libcore -f Makefile ios && mv $(BINDIR)/$(CORE_NAME)-ios-*.xcframework $(DESKTOP_OUT)/libcore.xcframework \ No newline at end of file + make -C libcore -f Makefile ios && mv $(BINDIR)/$(CORE_NAME)-ios.xcframework $(DESKTOP_OUT)/libcore.xcframework \ No newline at end of file diff --git a/dependencies.properties b/dependencies.properties index 5bef9ce7..f1589913 100644 --- a/dependencies.properties +++ b/dependencies.properties @@ -1 +1 @@ -core.version=0.1.0 \ No newline at end of file +core.version=0.1.1 \ No newline at end of file diff --git a/libcore b/libcore index dd711cba..1b31bd7a 160000 --- a/libcore +++ b/libcore @@ -1 +1 @@ -Subproject commit dd711cba20e5cfbc404b4660dd0e74d82f07bd1b +Subproject commit 1b31bd7a4bf71f6d2373fb2dfd0e0395492c4c7b From 18bffd8646c1c10d2b3cb3ca6e17ad6c875ea7e6 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Tue, 12 Sep 2023 00:05:44 +0330 Subject: [PATCH 12/23] Add accessability semantics --- assets/translations/strings.i18n.json | 7 +- assets/translations/strings_fa.i18n.json | 7 +- lib/core/app/app_view.dart | 7 + lib/features/common/profile_tile.dart | 146 +++++++++--------- lib/features/common/stats/stats_overview.dart | 10 +- lib/features/home/view/home_page.dart | 31 ++-- .../profiles/view/profiles_modal.dart | 5 +- .../widgets/theme_mode_switch_button.dart | 25 ++- pubspec.lock | 8 + pubspec.yaml | 15 +- 10 files changed, 156 insertions(+), 105 deletions(-) diff --git a/assets/translations/strings.i18n.json b/assets/translations/strings.i18n.json index c9ac05bf..e479fbff 100644 --- a/assets/translations/strings.i18n.json +++ b/assets/translations/strings.i18n.json @@ -21,7 +21,9 @@ }, "stats": { "traffic": "Live Traffic", - "trafficTotal": "Total Traffic" + "trafficTotal": "Total Traffic", + "uplink": "Uplink", + "downlink": "Downlink" } }, "profile": { @@ -54,7 +56,8 @@ "successMsg": "Profile updated successfully" }, "edit": { - "buttonTxt": "Edit" + "buttonTxt": "Edit", + "selectActiveTxt": "Select active profile" }, "delete": { "buttonTxt": "Delete", diff --git a/assets/translations/strings_fa.i18n.json b/assets/translations/strings_fa.i18n.json index 74c8b6a9..f47cce18 100644 --- a/assets/translations/strings_fa.i18n.json +++ b/assets/translations/strings_fa.i18n.json @@ -21,7 +21,9 @@ }, "stats": { "traffic": "مصرف لحظه‌ای", - "trafficTotal": "مصرف کل" + "trafficTotal": "مصرف کل", + "uplink": "ارسال", + "downlink": "دریافت" } }, "profile": { @@ -54,7 +56,8 @@ "successMsg": "پروفایل با موفقیت بروزرسانی شد" }, "edit": { - "buttonTxt": "ویرایش" + "buttonTxt": "ویرایش", + "selectActiveTxt": "انتخاب پروفایل فعال" }, "delete": { "buttonTxt": "حذف", diff --git a/lib/core/app/app_view.dart b/lib/core/app/app_view.dart index 5fe37a83..f3f6dd21 100644 --- a/lib/core/app/app_view.dart +++ b/lib/core/app/app_view.dart @@ -1,3 +1,4 @@ +import 'package:accessibility_tools/accessibility_tools.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:hiddify/core/core_providers.dart'; @@ -19,6 +20,12 @@ class AppView extends HookConsumerWidget with PresLogger { ref.watch(commonControllersProvider); return MaterialApp.router( + builder: (context, child) { + return AccessibilityTools( + checkFontOverflows: true, + child: child, + ); + }, routerConfig: router, locale: locale, supportedLocales: AppLocaleUtils.supportedLocales, diff --git a/lib/features/common/profile_tile.dart b/lib/features/common/profile_tile.dart index 18621e0a..5a02154c 100644 --- a/lib/features/common/profile_tile.dart +++ b/lib/features/common/profile_tile.dart @@ -48,6 +48,7 @@ class ProfileTile extends HookConsumerWidget { profile.active ? theme.colorScheme.outlineVariant : Colors.transparent; return Card( + semanticContainer: false, margin: effectiveMargin, elevation: effectiveElevation, shape: RoundedRectangleBorder( @@ -55,86 +56,87 @@ class ProfileTile extends HookConsumerWidget { borderRadius: BorderRadius.circular(16), ), shadowColor: Colors.transparent, - child: InkWell( - onTap: isMain - ? null - : () { - if (selectActiveMutation.state.isInProgress) return; - if (profile.active) return; - selectActiveMutation.setFuture( - ref - .read(profilesNotifierProvider.notifier) - .selectActiveProfile(profile.id), - ); - }, - child: IntrinsicHeight( - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - SizedBox( - width: 48, - child: ProfileActionButton(profile, !isMain), - ), - VerticalDivider( - width: 1, - color: effectiveOutlineColor, - ), - Flexible( - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 4, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (isMain) - Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Material( - borderRadius: BorderRadius.circular(8), - color: Colors.transparent, - clipBehavior: Clip.antiAlias, - child: Semantics( - button: true, - label: t.profile.overviewPageTitle, - child: InkWell( - onTap: () => const ProfilesRoute().go(context), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Text( - profile.name, - style: theme.textTheme.titleMedium, - ), + child: IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SizedBox( + width: 48, + child: ProfileActionButton(profile, !isMain), + ), + VerticalDivider( + width: 1, + color: effectiveOutlineColor, + ), + Flexible( + child: Semantics( + button: true, + label: isMain + ? t.profile.overviewPageTitle + : t.profile.edit.selectActiveTxt, + child: InkWell( + onTap: () { + if (isMain) { + const ProfilesRoute().go(context); + } else { + if (selectActiveMutation.state.isInProgress) return; + if (profile.active) return; + selectActiveMutation.setFuture( + ref + .read(profilesNotifierProvider.notifier) + .selectActiveProfile(profile.id), + ); + } + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 4, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (isMain) + Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Material( + borderRadius: BorderRadius.circular(8), + color: Colors.transparent, + clipBehavior: Clip.antiAlias, + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text( + profile.name, + style: theme.textTheme.titleMedium, ), - const Icon(Icons.arrow_drop_down), - ], - ), + ), + const Icon(Icons.arrow_drop_down), + ], ), ), + ) + else + Text( + profile.name, + style: theme.textTheme.titleMedium, ), - ) - else - Text( - profile.name, - style: theme.textTheme.titleMedium, - ), - if (subInfo != null) ...[ - const Gap(4), - RemainingTrafficIndicator(subInfo.ratio), - const Gap(4), - ProfileSubscriptionInfo(subInfo), - const Gap(4), + if (subInfo != null) ...[ + const Gap(4), + RemainingTrafficIndicator(subInfo.ratio), + const Gap(4), + ProfileSubscriptionInfo(subInfo), + const Gap(4), + ], ], - ], + ), ), ), ), - ], - ), + ), + ], ), ), ); diff --git a/lib/features/common/stats/stats_overview.dart b/lib/features/common/stats/stats_overview.dart index 92599321..e54ae216 100644 --- a/lib/features/common/stats/stats_overview.dart +++ b/lib/features/common/stats/stats_overview.dart @@ -26,10 +26,12 @@ class StatsOverview extends HookConsumerWidget { firstStat: ( label: "↑", data: stats.uplink.speed(), + semanticLabel: t.home.stats.uplink, ), secondStat: ( label: "↓", data: stats.downlink.speed(), + semanticLabel: t.home.stats.downlink, ), ), const Gap(8), @@ -38,10 +40,12 @@ class StatsOverview extends HookConsumerWidget { firstStat: ( label: "↑", data: stats.uplinkTotal.size(), + semanticLabel: t.home.stats.uplink, ), secondStat: ( label: "↓", data: stats.downlinkTotal.size(), + semanticLabel: t.home.stats.downlink, ), ), ], @@ -58,8 +62,8 @@ class _StatCard extends HookConsumerWidget { }); final String title; - final ({String label, String data}) firstStat; - final ({String label, String data}) secondStat; + final ({String label, String data, String semanticLabel}) firstStat; + final ({String label, String data, String semanticLabel}) secondStat; @override Widget build(BuildContext context, WidgetRef ref) { @@ -80,6 +84,7 @@ class _StatCard extends HookConsumerWidget { children: [ Text( firstStat.label, + semanticsLabel: firstStat.semanticLabel, style: const TextStyle(color: Colors.green), ), Text( @@ -93,6 +98,7 @@ class _StatCard extends HookConsumerWidget { children: [ Text( secondStat.label, + semanticsLabel: secondStat.semanticLabel, style: TextStyle(color: theme.colorScheme.error), ), Text( diff --git a/lib/features/home/view/home_page.dart b/lib/features/home/view/home_page.dart index 2f7516c6..e6a14383 100644 --- a/lib/features/home/view/home_page.dart +++ b/lib/features/home/view/home_page.dart @@ -83,6 +83,7 @@ class AppVersionLabel extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); final theme = Theme.of(context); final version = ref.watch( @@ -96,19 +97,23 @@ class AppVersionLabel extends HookConsumerWidget { if (version.isEmpty) return const SizedBox(); - return Container( - decoration: BoxDecoration( - color: theme.colorScheme.secondaryContainer, - borderRadius: BorderRadius.circular(4), - ), - padding: const EdgeInsets.symmetric( - horizontal: 4, - vertical: 1, - ), - child: Text( - version, - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onSecondaryContainer, + return Semantics( + label: t.about.version, + button: false, + child: Container( + decoration: BoxDecoration( + color: theme.colorScheme.secondaryContainer, + borderRadius: BorderRadius.circular(4), + ), + padding: const EdgeInsets.symmetric( + horizontal: 4, + vertical: 1, + ), + child: Text( + version, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSecondaryContainer, + ), ), ), ); diff --git a/lib/features/profiles/view/profiles_modal.dart b/lib/features/profiles/view/profiles_modal.dart index 6c8d7cac..081f68ff 100644 --- a/lib/features/profiles/view/profiles_modal.dart +++ b/lib/features/profiles/view/profiles_modal.dart @@ -125,7 +125,10 @@ class ProfilesSortModal extends HookConsumerWidget { icon: AnimatedRotation( turns: arrowTurn, duration: const Duration(milliseconds: 100), - child: const Icon(Icons.arrow_upward), + child: Icon( + Icons.arrow_upward, + semanticLabel: sort.mode.name, + ), ), ) : null, diff --git a/lib/features/settings/widgets/theme_mode_switch_button.dart b/lib/features/settings/widgets/theme_mode_switch_button.dart index 164d85da..580a633b 100644 --- a/lib/features/settings/widgets/theme_mode_switch_button.dart +++ b/lib/features/settings/widgets/theme_mode_switch_button.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:hiddify/core/core_providers.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; -class ThemeModeSwitch extends StatelessWidget { +class ThemeModeSwitch extends HookConsumerWidget { const ThemeModeSwitch({ super.key, required this.themeMode, @@ -10,7 +12,9 @@ class ThemeModeSwitch extends StatelessWidget { final ValueChanged onChanged; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + final List isSelected = [ themeMode == ThemeMode.light, themeMode == ThemeMode.system, @@ -28,10 +32,19 @@ class ThemeModeSwitch extends StatelessWidget { onChanged(ThemeMode.dark); } }, - children: const [ - Icon(Icons.wb_sunny), - Icon(Icons.phone_iphone), - Icon(Icons.bedtime), + children: [ + Icon( + Icons.wb_sunny, + semanticLabel: t.settings.general.themeModes.light, + ), + Icon( + Icons.phone_iphone, + semanticLabel: t.settings.general.themeModes.system, + ), + Icon( + Icons.bedtime, + semanticLabel: t.settings.general.themeModes.dark, + ), ], ); } diff --git a/pubspec.lock b/pubspec.lock index a2ee14c0..b3239d3b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -9,6 +9,14 @@ packages: url: "https://pub.dev" source: hosted version: "60.0.0" + accessibility_tools: + dependency: "direct main" + description: + name: accessibility_tools + sha256: "0a16adc8dfa3a7ebd38775135d86443011a65d4ecbb438913e4992b5d29135fe" + url: "https://pub.dev" + source: hosted + version: "1.0.0" analyzer: dependency: "direct overridden" description: diff --git a/pubspec.yaml b/pubspec.yaml index 5429d5f9..1f944049 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,10 +1,10 @@ name: hiddify description: A Proxy Frontend. -publish_to: 'none' +publish_to: "none" version: 0.1.0 environment: - sdk: '>=3.0.5 <4.0.0' + sdk: ">=3.0.5 <4.0.0" dependencies: flutter: @@ -65,6 +65,7 @@ dependencies: dartx: ^1.2.0 uuid: ^3.0.7 tint: ^2.0.1 + accessibility_tools: ^1.0.0 # widgets go_router: ^10.1.2 @@ -96,7 +97,7 @@ dev_dependencies: icons_launcher: ^2.1.3 dependency_overrides: - analyzer: '5.12.0' + analyzer: "5.12.0" flutter: uses-material-design: true @@ -152,9 +153,9 @@ flutter_native_splash: image: assets/images/source/ic_launcher_foreground.png ffigen: - name: 'SingboxNativeLibrary' - description: 'Bindings to Singbox' - output: 'lib/gen/singbox_generated_bindings.dart' + name: "SingboxNativeLibrary" + description: "Bindings to Singbox" + output: "lib/gen/singbox_generated_bindings.dart" headers: entry-points: - - 'libcore/bin/libcore.h' \ No newline at end of file + - "libcore/bin/libcore.h" From 8b77ce866c4da55c9c8cfbf356a1ac3078d6f443 Mon Sep 17 00:00:00 2001 From: Hiddify <114227601+hiddify-com@users.noreply.github.com> Date: Tue, 12 Sep 2023 00:49:14 +0200 Subject: [PATCH 13/23] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5e823b6b..95e1a841 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ ## Support -- V2Ray Subscription link +- V2Ray Subscription link (e.g., vless:// vmess:// ss:// trojan:// tuic://) - Clash / Clash Meta link - Singbox Config From 4f13978f693a7de3dc4be803cfc42122995a8ed7 Mon Sep 17 00:00:00 2001 From: Hiddify <114227601+hiddify-com@users.noreply.github.com> Date: Tue, 12 Sep 2023 00:52:35 +0200 Subject: [PATCH 14/23] Update ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index add7e5e3..695ad54f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: Build +name: CI & Build on: push: branches: From 8ed6b35752cc38bb7c3d93d952536d513b643cf6 Mon Sep 17 00:00:00 2001 From: Hiddify <114227601+hiddify-com@users.noreply.github.com> Date: Tue, 12 Sep 2023 10:02:08 +0200 Subject: [PATCH 15/23] Update ci.yml --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 695ad54f..5d43b877 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -118,6 +118,8 @@ jobs: make gen - name: Get Libs ${{ matrix.platform }} + env: + BRANCH: ${{ if github.ref_type == 'tag' }}RELEASE${{ else }}DEVELOP${{ endif }} run: | make ${{ matrix.platform }}-libs From 2a5ce545e7a782c69fedf0266b6f5a2883ad4034 Mon Sep 17 00:00:00 2001 From: Hiddify <114227601+hiddify-com@users.noreply.github.com> Date: Tue, 12 Sep 2023 10:04:07 +0200 Subject: [PATCH 16/23] Update Makefile --- Makefile | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 3d72ff45..c441b821 100644 --- a/Makefile +++ b/Makefile @@ -9,8 +9,11 @@ BRANCH=$(shell git branch --show-current) VERSION=$(shell git describe --tags --abbrev=0 || echo "unknown version") CORE_NAME=hiddify-libcore +ifeq ($(BRANCH),DEVELOP) +CORE_URL=https://github.com/hiddify/hiddify-next-core/releases/download/draft +else CORE_URL=https://github.com/hiddify/hiddify-next-core/releases/download/v$(core.version) - +endif get: flutter pub get @@ -79,4 +82,4 @@ build-macos-libs: make -C libcore -f Makefile macos-universal && mv $(BINDIR)/$(CORE_NAME)-macos-universal.dylib $(DESKTOP_OUT)/libcore.dylib build-ios-libs: #not tested - make -C libcore -f Makefile ios && mv $(BINDIR)/$(CORE_NAME)-ios.xcframework $(DESKTOP_OUT)/libcore.xcframework \ No newline at end of file + make -C libcore -f Makefile ios && mv $(BINDIR)/$(CORE_NAME)-ios.xcframework $(DESKTOP_OUT)/libcore.xcframework From 78b0d4d02f69ee454e042a809924b96481249ae3 Mon Sep 17 00:00:00 2001 From: Hiddify <114227601+hiddify-com@users.noreply.github.com> Date: Tue, 12 Sep 2023 10:07:19 +0200 Subject: [PATCH 17/23] Update ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5d43b877..05ffe935 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -119,7 +119,7 @@ jobs: - name: Get Libs ${{ matrix.platform }} env: - BRANCH: ${{ if github.ref_type == 'tag' }}RELEASE${{ else }}DEVELOP${{ endif }} + BRANCH: ${{ github.ref_type == 'tag' && "RELEASE" || "DEVELOP" }} run: | make ${{ matrix.platform }}-libs From 794b90f149922e6d04192f9ceb817cdd7c7a871d Mon Sep 17 00:00:00 2001 From: Hiddify <114227601+hiddify-com@users.noreply.github.com> Date: Tue, 12 Sep 2023 10:09:22 +0200 Subject: [PATCH 18/23] Update ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 05ffe935..acb9f4d5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -119,7 +119,7 @@ jobs: - name: Get Libs ${{ matrix.platform }} env: - BRANCH: ${{ github.ref_type == 'tag' && "RELEASE" || "DEVELOP" }} + BRANCH: ${{ github.ref_type == 'tag' && 'RELEASE' || 'DEVELOP' }} run: | make ${{ matrix.platform }}-libs From ea81be376320b8f8dc087cb9bdf08197bf68c292 Mon Sep 17 00:00:00 2001 From: Hiddify <114227601+hiddify-com@users.noreply.github.com> Date: Tue, 12 Sep 2023 10:10:14 +0200 Subject: [PATCH 19/23] Update Makefile --- Makefile | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index c441b821..7b66a2ac 100644 --- a/Makefile +++ b/Makefile @@ -9,11 +9,12 @@ BRANCH=$(shell git branch --show-current) VERSION=$(shell git describe --tags --abbrev=0 || echo "unknown version") CORE_NAME=hiddify-libcore -ifeq ($(BRANCH),DEVELOP) -CORE_URL=https://github.com/hiddify/hiddify-next-core/releases/download/draft -else +ifeq ($(BRANCH),RELEASE) CORE_URL=https://github.com/hiddify/hiddify-next-core/releases/download/v$(core.version) +else +CORE_URL=https://github.com/hiddify/hiddify-next-core/releases/download/draft endif + get: flutter pub get From f1b0f8ee4bb4904329aa6c00bc42432e9275d2be Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Tue, 12 Sep 2023 15:22:58 +0330 Subject: [PATCH 20/23] Add basic flavors --- .vscode/launch.json | 33 ++++ Makefile | 20 +- lib/bootstrap.dart | 20 +- lib/core/core_providers.dart | 9 + lib/core/prefs/general_prefs.dart | 9 +- lib/data/data_providers.dart | 9 +- ...ory_impl.dart => app_repository_impl.dart} | 42 ++--- lib/domain/app/app.dart | 6 +- .../{update_failure.dart => app_failure.dart} | 8 +- .../app/{version_info.dart => app_info.dart} | 23 ++- lib/domain/app/app_repository.dart | 9 + lib/domain/app/update_repository.dart | 11 -- lib/domain/environment.dart | 4 + lib/features/about/view/about_page.dart | 178 ++++++++---------- lib/features/common/app_update_notifier.dart | 33 ++++ lib/features/common/common.dart | 2 +- lib/features/common/new_version_dialog.dart | 4 +- lib/features/common/runtime_details.dart | 79 -------- lib/features/home/view/home_page.dart | 19 +- lib/{main.dart => main_dev.dart} | 3 +- lib/main_prod.dart | 8 + lib/services/auto_start_service.dart | 6 +- lib/services/runtime_details_service.dart | 24 --- lib/services/service_providers.dart | 5 - 24 files changed, 271 insertions(+), 293 deletions(-) create mode 100644 .vscode/launch.json rename lib/data/repository/{update_repository_impl.dart => app_repository_impl.dart} (55%) rename lib/domain/app/{update_failure.dart => app_failure.dart} (72%) rename lib/domain/app/{version_info.dart => app_info.dart} (73%) create mode 100644 lib/domain/app/app_repository.dart delete mode 100644 lib/domain/app/update_repository.dart create mode 100644 lib/domain/environment.dart create mode 100644 lib/features/common/app_update_notifier.dart delete mode 100644 lib/features/common/runtime_details.dart rename lib/{main.dart => main_dev.dart} (61%) create mode 100644 lib/main_prod.dart delete mode 100644 lib/services/runtime_details_service.dart diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..2a660029 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,33 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Hiddify Dev", + "request": "launch", + "type": "dart", + "flutterMode": "debug", + "program": "lib/main_dev.dart", + }, + { + "name": "Hiddify Dev Release", + "request": "launch", + "type": "dart", + "flutterMode": "release", + "program": "lib/main_dev.dart", + }, + { + "name": "Hiddify Dev Profile", + "request": "launch", + "type": "dart", + "flutterMode": "profile", + "program": "lib/main_dev.dart", + }, + { + "name": "Hiddify Prod", + "request": "launch", + "type": "dart", + "flutterMode": "release", + "program": "lib/main_prod.dart", + } + ] +} \ No newline at end of file diff --git a/Makefile b/Makefile index 7b66a2ac..56e2c0df 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,13 @@ else CORE_URL=https://github.com/hiddify/hiddify-next-core/releases/download/draft endif +ifeq ($(BRANCH),RELEASE) +FLAVOR=prod +else +FLAVOR=dev +endif +TARGET=lib/main_$(FLAVOR).dart + get: flutter pub get @@ -25,23 +32,24 @@ translate: dart run slang android-release: android-aab-release android-apk-release + android-apk-release: - flutter build apk --target-platform android-arm,android-arm64,android-x64 --split-per-abi + flutter build apk --target-platform android-arm,android-arm64,android-x64 --split-per-abi --target $(TARGET) android-aab-release: - flutter build appbundle + flutter build appbundle --target $(TARGET) windows-release: - flutter_distributor package --platform windows --targets exe --skip-clean + flutter_distributor package --platform windows --targets exe --skip-clean --build-target $(TARGET) linux-release: - flutter_distributor package --platform linux --targets appimage --skip-clean + flutter_distributor package --platform linux --targets appimage --skip-clean --build-target $(TARGET) macos-release: - flutter_distributor package --platform macos --targets dmg --skip-clean + flutter_distributor package --platform macos --targets dmg --skip-clean --build-target $(TARGET) ios-release: #not tested - flutter_distributor package --platform ios --targets ipa --build-export-options-plist ios/exportOptions.plist + flutter_distributor package --platform ios --targets ipa --build-export-options-plist ios/exportOptions.plist --build-target $(TARGET) android-libs: mkdir -p $(ANDROID_OUT) diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index 5d73e6b8..bd4659b6 100644 --- a/lib/bootstrap.dart +++ b/lib/bootstrap.dart @@ -5,10 +5,12 @@ 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.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/features/common/active_profile/active_profile_notifier.dart'; -import 'package:hiddify/features/common/common.dart'; import 'package:hiddify/features/common/window/window_controller.dart'; import 'package:hiddify/features/system_tray/system_tray.dart'; import 'package:hiddify/services/auto_start_service.dart'; @@ -23,15 +25,22 @@ import 'package:window_manager/window_manager.dart'; final _loggy = Loggy('bootstrap'); final _stopWatch = Stopwatch(); -Future lazyBootstrap(WidgetsBinding widgetsBinding) async { +Future lazyBootstrap( + WidgetsBinding widgetsBinding, + Environment env, +) async { _stopWatch.start(); FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); if (PlatformUtils.isDesktop) await windowManager.ensureInitialized(); + final appInfo = await AppRepositoryImpl.getAppInfo(env); final sharedPreferences = await SharedPreferences.getInstance(); final container = ProviderContainer( - overrides: [sharedPreferencesProvider.overrideWithValue(sharedPreferences)], + overrides: [ + appInfoProvider.overrideWithValue(appInfo), + sharedPreferencesProvider.overrideWithValue(sharedPreferences), + ], ); final debug = container.read(debugModeNotifierProvider) || kDebugMode; @@ -40,7 +49,9 @@ Future lazyBootstrap(WidgetsBinding widgetsBinding) async { await filesEditor.init(); initLoggers(container.read, debug); - await container.read(runtimeDetailsServiceProvider).init(); + _loggy.info( + "os: [${Platform.operatingSystem}](${Platform.operatingSystemVersion}), processor count [${Platform.numberOfProcessors}]", + ); _loggy.info("basic setup took [${_stopWatch.elapsedMilliseconds}]ms"); final silentStart = container.read(silentStartNotifierProvider); @@ -104,7 +115,6 @@ Future initControllers( [ read(activeProfileProvider.future), read(deepLinkServiceProvider.future), - read(runtimeDetailsNotifierProvider.future), if (PlatformUtils.isDesktop) read(systemTrayControllerProvider.future), ], ); diff --git a/lib/core/core_providers.dart b/lib/core/core_providers.dart index 3d943dbb..9cd598ff 100644 --- a/lib/core/core_providers.dart +++ b/lib/core/core_providers.dart @@ -1,8 +1,17 @@ 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(); diff --git a/lib/core/prefs/general_prefs.dart b/lib/core/prefs/general_prefs.dart index 47b54783..32f93b5c 100644 --- a/lib/core/prefs/general_prefs.dart +++ b/lib/core/prefs/general_prefs.dart @@ -1,4 +1,6 @@ +import 'package:hiddify/core/core_providers.dart'; import 'package:hiddify/data/data_providers.dart'; +import 'package:hiddify/domain/environment.dart'; import 'package:hiddify/utils/pref_notifier.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -20,8 +22,11 @@ class SilentStartNotifier extends _$SilentStartNotifier { @Riverpod(keepAlive: true) class DebugModeNotifier extends _$DebugModeNotifier { - late final _pref = - Pref(ref.watch(sharedPreferencesProvider), "debug_mode", false); + late final _pref = Pref( + ref.watch(sharedPreferencesProvider), + "debug_mode", + ref.read(envProvider) == Environment.dev, + ); @override bool build() => _pref.getValue(); diff --git a/lib/data/data_providers.dart b/lib/data/data_providers.dart index d62220d1..4e669dba 100644 --- a/lib/data/data_providers.dart +++ b/lib/data/data_providers.dart @@ -1,10 +1,11 @@ import 'package:dio/dio.dart'; +import 'package:hiddify/core/core_providers.dart'; import 'package:hiddify/data/api/clash_api.dart'; 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/repository.dart'; -import 'package:hiddify/data/repository/update_repository_impl.dart'; import 'package:hiddify/domain/app/app.dart'; import 'package:hiddify/domain/constants.dart'; import 'package:hiddify/domain/core_facade.dart'; @@ -27,7 +28,7 @@ SharedPreferences sharedPreferences(SharedPreferencesRef ref) => Dio dio(DioRef ref) => Dio( BaseOptions( headers: { - "User-Agent": ref.watch(runtimeDetailsServiceProvider).userAgent, + "User-Agent": ref.watch(appInfoProvider).userAgent, }, ), ); @@ -47,8 +48,8 @@ ProfilesRepository profilesRepository(ProfilesRepositoryRef ref) => ); @Riverpod(keepAlive: true) -UpdateRepository updateRepository(UpdateRepositoryRef ref) => - UpdateRepositoryImpl(ref.watch(dioProvider)); +AppRepository appRepository(AppRepositoryRef ref) => + AppRepositoryImpl(ref.watch(dioProvider)); @Riverpod(keepAlive: true) ClashApi clashApi(ClashApiRef ref) => ClashApi(Defaults.clashApiPort); diff --git a/lib/data/repository/update_repository_impl.dart b/lib/data/repository/app_repository_impl.dart similarity index 55% rename from lib/data/repository/update_repository_impl.dart rename to lib/data/repository/app_repository_impl.dart index 490c9d7c..ad937964 100644 --- a/lib/data/repository/update_repository_impl.dart +++ b/lib/data/repository/app_repository_impl.dart @@ -1,41 +1,35 @@ +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 UpdateRepositoryImpl +class AppRepositoryImpl with ExceptionHandler, InfraLogger - implements UpdateRepository { - UpdateRepositoryImpl(this.dio); + implements AppRepository { + AppRepositoryImpl(this.dio); final Dio dio; - @override - TaskEither getCurrentVersion() { - return exceptionHandler( - () async { - loggy.debug("getting current app version"); - final packageInfo = await PackageInfo.fromPlatform(); - return right( - InstalledVersionInfo( - version: packageInfo.version, - buildNumber: packageInfo.buildNumber, - installerMedia: packageInfo.installerStore, - ), - ); - }, - (error, stackTrace) { - loggy.warning("error getting current app version", error, stackTrace); - return UpdateFailure.unexpected(error, stackTrace); - }, + static Future getAppInfo(Environment environment) async { + final packageInfo = await PackageInfo.fromPlatform(); + return AppInfo( + name: packageInfo.appName, + version: packageInfo.version, + buildNumber: packageInfo.buildNumber, + installerMedia: packageInfo.installerStore, + operatingSystem: Platform.operatingSystem, + environment: environment, ); } @override - TaskEither getLatestVersion({ + TaskEither getLatestVersion({ bool includePreReleases = false, }) { return exceptionHandler( @@ -44,7 +38,7 @@ class UpdateRepositoryImpl if (response.statusCode != 200 || response.data == null) { loggy.warning("failed to fetch latest version info"); - return left(const UpdateFailure.unexpected()); + return left(const AppFailure.unexpected()); } final releases = response.data! @@ -57,7 +51,7 @@ class UpdateRepositoryImpl } return right(latest); }, - UpdateFailure.unexpected, + AppFailure.unexpected, ); } } diff --git a/lib/domain/app/app.dart b/lib/domain/app/app.dart index 32895acb..1853b271 100644 --- a/lib/domain/app/app.dart +++ b/lib/domain/app/app.dart @@ -1,3 +1,3 @@ -export 'update_failure.dart'; -export 'update_repository.dart'; -export 'version_info.dart'; +export 'app_failure.dart'; +export 'app_info.dart'; +export 'app_repository.dart'; diff --git a/lib/domain/app/update_failure.dart b/lib/domain/app/app_failure.dart similarity index 72% rename from lib/domain/app/update_failure.dart rename to lib/domain/app/app_failure.dart index 97212b36..4f37126e 100644 --- a/lib/domain/app/update_failure.dart +++ b/lib/domain/app/app_failure.dart @@ -2,13 +2,13 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hiddify/core/prefs/prefs.dart'; import 'package:hiddify/domain/failures.dart'; -part 'update_failure.freezed.dart'; +part 'app_failure.freezed.dart'; @freezed -sealed class UpdateFailure with _$UpdateFailure, Failure { - const UpdateFailure._(); +sealed class AppFailure with _$AppFailure, Failure { + const AppFailure._(); - const factory UpdateFailure.unexpected([ + const factory AppFailure.unexpected([ Object? error, StackTrace? stackTrace, ]) = UpdateUnexpectedFailure; diff --git a/lib/domain/app/version_info.dart b/lib/domain/app/app_info.dart similarity index 73% rename from lib/domain/app/version_info.dart rename to lib/domain/app/app_info.dart index 94edce67..db7e3233 100644 --- a/lib/domain/app/version_info.dart +++ b/lib/domain/app/app_info.dart @@ -1,24 +1,27 @@ import 'package:dartx/dartx.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hiddify/domain/environment.dart'; -part 'version_info.freezed.dart'; -part 'version_info.g.dart'; +part 'app_info.freezed.dart'; +part 'app_info.g.dart'; @freezed -class InstalledVersionInfo with _$InstalledVersionInfo { - const InstalledVersionInfo._(); +class AppInfo with _$AppInfo { + const AppInfo._(); - const factory InstalledVersionInfo({ + const factory AppInfo({ + required String name, required String version, required String buildNumber, String? installerMedia, - }) = _InstalledVersionInfo; + required String operatingSystem, + required Environment environment, + }) = _AppInfo; - String get fullVersion => - buildNumber.isBlank ? version : "$version+$buildNumber"; + String get userAgent => "HiddifyNext/$version ($operatingSystem)"; - factory InstalledVersionInfo.fromJson(Map json) => - _$InstalledVersionInfoFromJson(json); + factory AppInfo.fromJson(Map json) => + _$AppInfoFromJson(json); } // TODO ignore drafts diff --git a/lib/domain/app/app_repository.dart b/lib/domain/app/app_repository.dart new file mode 100644 index 00000000..e51bcb35 --- /dev/null +++ b/lib/domain/app/app_repository.dart @@ -0,0 +1,9 @@ +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/app/update_repository.dart b/lib/domain/app/update_repository.dart deleted file mode 100644 index 66a76fc7..00000000 --- a/lib/domain/app/update_repository.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:fpdart/fpdart.dart'; -import 'package:hiddify/domain/app/update_failure.dart'; -import 'package:hiddify/domain/app/version_info.dart'; - -abstract interface class UpdateRepository { - TaskEither getCurrentVersion(); - - TaskEither getLatestVersion({ - bool includePreReleases = false, - }); -} diff --git a/lib/domain/environment.dart b/lib/domain/environment.dart new file mode 100644 index 00000000..c223ad53 --- /dev/null +++ b/lib/domain/environment.dart @@ -0,0 +1,4 @@ +enum Environment { + prod, + dev; +} diff --git a/lib/features/about/view/about_page.dart b/lib/features/about/view/about_page.dart index a9467d20..0c327088 100644 --- a/lib/features/about/view/about_page.dart +++ b/lib/features/about/view/about_page.dart @@ -3,8 +3,8 @@ 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/common.dart'; import 'package:hiddify/features/common/new_version_dialog.dart'; -import 'package:hiddify/features/common/runtime_details.dart'; import 'package:hiddify/gen/assets.gen.dart'; import 'package:hiddify/services/service_providers.dart'; import 'package:hiddify/utils/utils.dart'; @@ -16,33 +16,22 @@ class AboutPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final t = ref.watch(translationsProvider); - final appVersion = ref.watch(appVersionProvider); - - final isCheckingForUpdate = ref.watch( - runtimeDetailsNotifierProvider.select( - (value) => value.maybeWhen( - data: (data) => data.latestVersion.isLoading, - orElse: () => false, - ), - ), - ); + final appInfo = ref.watch(appInfoProvider); + final appUpdate = ref.watch(appUpdateNotifierProvider); ref.listen( - runtimeDetailsNotifierProvider, + appUpdateNotifierProvider, (_, next) async { - if (next case AsyncData(:final value)) { - switch (value.latestVersion) { - case AsyncError(:final error): - CustomToast.error(t.printError(error)).show(context); - default: - if (value.newVersionAvailable) { - await NewVersionDialog( - value.appVersion, - value.latestVersion.value!, - canIgnore: false, - ).show(context); - } - } + switch (next) { + case AsyncData(value: final remoteVersion?): + await NewVersionDialog( + appInfo.version, + remoteVersion, + canIgnore: false, + ).show(context); + case AsyncError(:final error): + if (!context.mounted) return; + CustomToast.error(t.printError(error)).show(context); } }, ); @@ -53,86 +42,77 @@ class AboutPage extends HookConsumerWidget { SliverAppBar( title: Text(t.about.pageTitle), ), - ...switch (appVersion) { - AsyncData(:final value) => [ - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.all(16), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Assets.images.logo.svg(width: 64, height: 64), - const Gap(16), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - t.general.appTitle, - style: Theme.of(context).textTheme.titleLarge, - ), - const Gap(4), - Text( - "${t.about.version} ${value.version} ${value.buildNumber}", - ), - ], - ), - ], - ), - ), - ), - SliverList( - delegate: SliverChildListDelegate( - [ - ListTile( - title: Text(t.about.sourceCode), - trailing: const Icon(Icons.open_in_new), - onTap: () async { - await UriUtils.tryLaunch( - Uri.parse(Constants.githubUrl), - ); - }, + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Assets.images.logo.svg(width: 64, height: 64), + const Gap(16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + t.general.appTitle, + style: Theme.of(context).textTheme.titleLarge, ), - ListTile( - title: Text(t.about.telegramChannel), - trailing: const Icon(Icons.open_in_new), - onTap: () async { - await UriUtils.tryLaunch( - Uri.parse(Constants.telegramChannelUrl), - ); - }, - ), - ListTile( - title: Text(t.about.checkForUpdate), - trailing: isCheckingForUpdate - ? const SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator(), - ) - : const Icon(Icons.update), - onTap: () async { - await ref - .read(runtimeDetailsNotifierProvider.notifier) - .checkForUpdates(); - }, - ), - ListTile( - title: Text(t.settings.general.openWorkingDir), - trailing: const Icon(Icons.arrow_outward_outlined), - onTap: () async { - final path = ref - .read(filesEditorServiceProvider) - .workingDir - .uri; - await UriUtils.tryLaunch(path); - }, + const Gap(4), + Text( + "${t.about.version} ${appInfo.version}", ), ], ), + ], + ), + ), + ), + SliverList( + delegate: SliverChildListDelegate( + [ + ListTile( + title: Text(t.about.sourceCode), + trailing: const Icon(Icons.open_in_new), + onTap: () async { + await UriUtils.tryLaunch( + Uri.parse(Constants.githubUrl), + ); + }, + ), + ListTile( + title: Text(t.about.telegramChannel), + trailing: const Icon(Icons.open_in_new), + onTap: () async { + await UriUtils.tryLaunch( + Uri.parse(Constants.telegramChannelUrl), + ); + }, + ), + ListTile( + title: Text(t.about.checkForUpdate), + trailing: appUpdate.isLoading + ? const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator(), + ) + : const Icon(Icons.update), + onTap: () { + ref.invalidate(appUpdateNotifierProvider); + }, + ), + ListTile( + title: Text(t.settings.general.openWorkingDir), + trailing: const Icon(Icons.arrow_outward_outlined), + onTap: () async { + final path = + ref.read(filesEditorServiceProvider).workingDir.uri; + await UriUtils.tryLaunch(path); + }, ), ], - _ => [], - }, + ), + ), ], ), ); diff --git a/lib/features/common/app_update_notifier.dart b/lib/features/common/app_update_notifier.dart new file mode 100644 index 00000000..4fa2cd46 --- /dev/null +++ b/lib/features/common/app_update_notifier.dart @@ -0,0 +1,33 @@ +import 'package:hiddify/core/core_providers.dart'; +import 'package:hiddify/data/data_providers.dart'; +import 'package:hiddify/domain/app/app.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'app_update_notifier.g.dart'; + +@Riverpod(keepAlive: true) +class AppUpdateNotifier extends _$AppUpdateNotifier with AppLogger { + @override + Future build() async { + loggy.debug("checking for update"); + final currentVersion = ref.watch(appInfoProvider).version; + return ref + .watch(appRepositoryProvider) + .getLatestVersion(includePreReleases: true) + .match( + (l) { + loggy.warning("failed to get latest version, $l"); + throw l; + }, + (remote) { + if (remote.version.compareTo(currentVersion) > 0) { + loggy.info("new version available: $remote"); + return remote; + } + loggy.info("already using latest version[$currentVersion], remote: $remote"); + return null; + }, + ).run(); + } +} diff --git a/lib/features/common/common.dart b/lib/features/common/common.dart index 4ac4edcf..b4eaf530 100644 --- a/lib/features/common/common.dart +++ b/lib/features/common/common.dart @@ -1,5 +1,5 @@ +export 'app_update_notifier.dart'; export 'confirmation_dialogs.dart'; export 'custom_app_bar.dart'; export 'profile_tile.dart'; export 'qr_code_scanner_screen.dart'; -export 'runtime_details.dart'; diff --git a/lib/features/common/new_version_dialog.dart b/lib/features/common/new_version_dialog.dart index f18133a2..6e04a066 100644 --- a/lib/features/common/new_version_dialog.dart +++ b/lib/features/common/new_version_dialog.dart @@ -16,7 +16,7 @@ class NewVersionDialog extends HookConsumerWidget { this.canIgnore = true, }); - final InstalledVersionInfo currentVersion; + final String currentVersion; final RemoteVersionInfo newVersion; final bool canIgnore; @@ -48,7 +48,7 @@ class NewVersionDialog extends HookConsumerWidget { style: theme.textTheme.bodySmall, ), TextSpan( - text: currentVersion.fullVersion, + text: currentVersion, style: theme.textTheme.labelMedium, ), ], diff --git a/lib/features/common/runtime_details.dart b/lib/features/common/runtime_details.dart deleted file mode 100644 index 84a9e642..00000000 --- a/lib/features/common/runtime_details.dart +++ /dev/null @@ -1,79 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:hiddify/data/data_providers.dart'; -import 'package:hiddify/domain/app/app.dart'; -import 'package:hiddify/utils/utils.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'runtime_details.freezed.dart'; -part 'runtime_details.g.dart'; - -// TODO add clash version -@Riverpod(keepAlive: true) -class RuntimeDetailsNotifier extends _$RuntimeDetailsNotifier with AppLogger { - @override - Future build() async { - loggy.debug("initializing"); - final appVersion = await ref - .watch(updateRepositoryProvider) - .getCurrentVersion() - .getOrElse((l) => throw l) - .run(); - return RuntimeDetails(appVersion: appVersion); - } - - Future checkForUpdates() async { - if (state case AsyncData(:final value)) { - switch (value.latestVersion) { - case AsyncLoading(): - return; - default: - loggy.debug("checking for updates"); - state = - AsyncData(value.copyWith(latestVersion: const AsyncLoading())); - // TODO use prefs - const includePreReleases = true; - await ref - .read(updateRepositoryProvider) - .getLatestVersion(includePreReleases: includePreReleases) - .match( - (l) { - loggy.warning("failed to get latest version, $l"); - state = AsyncData( - value.copyWith( - latestVersion: AsyncError(l, StackTrace.current), - ), - ); - }, - (r) { - state = AsyncData( - value.copyWith(latestVersion: AsyncData(r)), - ); - }, - ).run(); - } - } - } -} - -@Riverpod(keepAlive: true) -AsyncValue appVersion(AppVersionRef ref) => ref.watch( - runtimeDetailsNotifierProvider - .select((value) => value.whenData((value) => value.appVersion)), - ); - -@freezed -class RuntimeDetails with _$RuntimeDetails { - const RuntimeDetails._(); - - const factory RuntimeDetails({ - required InstalledVersionInfo appVersion, - @Default(AsyncData(null)) AsyncValue latestVersion, - }) = _RuntimeDetails; - - bool get newVersionAvailable => latestVersion.maybeWhen( - data: (data) => - data != null && - data.fullVersion.compareTo(this.appVersion.fullVersion) > 0, - orElse: () => false, - ); -} diff --git a/lib/features/home/view/home_page.dart b/lib/features/home/view/home_page.dart index e6a14383..771abd23 100644 --- a/lib/features/home/view/home_page.dart +++ b/lib/features/home/view/home_page.dart @@ -1,7 +1,9 @@ +import 'package:dartx/dartx.dart'; 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/environment.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'; @@ -86,16 +88,12 @@ class AppVersionLabel extends HookConsumerWidget { final t = ref.watch(translationsProvider); final theme = Theme.of(context); - final version = ref.watch( - appVersionProvider.select( - (value) => switch (value) { - AsyncData(:final value) => value.version, - _ => "", - }, - ), - ); - - if (version.isEmpty) return const SizedBox(); + final appInfo = ref.watch(appInfoProvider); + final version = appInfo.version + + (appInfo.environment == Environment.prod + ? "" + : " ${appInfo.environment.name}"); + if (version.isBlank) return const SizedBox(); return Semantics( label: t.about.version, @@ -111,6 +109,7 @@ class AppVersionLabel extends HookConsumerWidget { ), child: Text( version, + textDirection: TextDirection.ltr, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSecondaryContainer, ), diff --git a/lib/main.dart b/lib/main_dev.dart similarity index 61% rename from lib/main.dart rename to lib/main_dev.dart index 52630a4d..8dcdbd01 100644 --- a/lib/main.dart +++ b/lib/main_dev.dart @@ -1,7 +1,8 @@ import 'package:flutter/widgets.dart'; import 'package:hiddify/bootstrap.dart'; +import 'package:hiddify/domain/environment.dart'; void main() async { final widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); - return lazyBootstrap(widgetsBinding); + return lazyBootstrap(widgetsBinding, Environment.dev); } diff --git a/lib/main_prod.dart b/lib/main_prod.dart new file mode 100644 index 00000000..81b713ca --- /dev/null +++ b/lib/main_prod.dart @@ -0,0 +1,8 @@ +import 'package:flutter/widgets.dart'; +import 'package:hiddify/bootstrap.dart'; +import 'package:hiddify/domain/environment.dart'; + +void main() async { + final widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); + return lazyBootstrap(widgetsBinding, Environment.prod); +} diff --git a/lib/services/auto_start_service.dart b/lib/services/auto_start_service.dart index a03aeb2c..bb0a31bf 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/services/service_providers.dart'; +import 'package:hiddify/core/core_providers.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:launch_at_startup/launch_at_startup.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -13,9 +13,9 @@ class AutoStartService extends _$AutoStartService with InfraLogger { Future build() async { loggy.debug("initializing"); if (!PlatformUtils.isDesktop) return false; - final packageInfo = ref.watch(runtimeDetailsServiceProvider).packageInfo; + final appInfo = ref.watch(appInfoProvider); launchAtStartup.setup( - appName: packageInfo.appName, + appName: appInfo.name, appPath: Platform.resolvedExecutable, ); final isEnabled = await launchAtStartup.isEnabled(); diff --git a/lib/services/runtime_details_service.dart b/lib/services/runtime_details_service.dart deleted file mode 100644 index ff51bd08..00000000 --- a/lib/services/runtime_details_service.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'dart:io'; - -import 'package:hiddify/utils/utils.dart'; -import 'package:package_info_plus/package_info_plus.dart'; - -class RuntimeDetailsService with InfraLogger { - late final PackageInfo packageInfo; - - String get appVersion => packageInfo.version; - String get buildNumber => packageInfo.buildNumber; - - late final String operatingSystem = Platform.operatingSystem; - late final String userAgent; - - Future init() async { - loggy.debug("initializing"); - packageInfo = await PackageInfo.fromPlatform(); - userAgent = "HiddifyNext/$appVersion ($operatingSystem)"; - - loggy.info( - "os: [$operatingSystem](${Platform.operatingSystemVersion}), processor count [${Platform.numberOfProcessors}]", - ); - } -} diff --git a/lib/services/service_providers.dart b/lib/services/service_providers.dart index 87df56f8..187e04bb 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_settings.dart'; -import 'package:hiddify/services/runtime_details_service.dart'; import 'package:hiddify/services/singbox/singbox_service.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -10,10 +9,6 @@ part 'service_providers.g.dart'; FilesEditorService filesEditorService(FilesEditorServiceRef ref) => FilesEditorService(); -@Riverpod(keepAlive: true) -RuntimeDetailsService runtimeDetailsService(RuntimeDetailsServiceRef ref) => - RuntimeDetailsService(); - @Riverpod(keepAlive: true) SingboxService singboxService(SingboxServiceRef ref) => SingboxService(); From ea6f8b5fad0127625a3fe9e99d9f37ebe0502409 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Wed, 13 Sep 2023 23:19:16 +0330 Subject: [PATCH 21/23] Add android per-app proxy --- .../hiddify/PlatformSettingsHandler.kt | 87 +++++++ .../kotlin/com/hiddify/hiddify/Settings.kt | 55 +++-- .../hiddify/hiddify/bg/AppChangeReceiver.kt | 32 +-- .../com/hiddify/hiddify/bg/VPNService.kt | 3 +- .../hiddify/constant/PerAppProxyMode.kt | 7 + .../hiddify/hiddify/constant/SettingsKey.kt | 7 +- assets/translations/strings.i18n.json | 14 ++ assets/translations/strings_fa.i18n.json | 14 ++ lib/core/prefs/general_prefs.dart | 49 ++++ lib/core/router/routes/mobile_routes.dart | 16 ++ lib/domain/singbox/rules.dart | 24 ++ lib/domain/singbox/singbox.dart | 1 + .../settings/view/per_app_proxy_page.dart | 226 ++++++++++++++++++ lib/features/settings/view/view.dart | 1 + .../widgets/advanced_setting_tiles.dart | 31 +++ lib/services/platform_settings.dart | 57 +++++ 16 files changed, 587 insertions(+), 37 deletions(-) create mode 100644 android/app/src/main/kotlin/com/hiddify/hiddify/constant/PerAppProxyMode.kt create mode 100644 lib/domain/singbox/rules.dart create mode 100644 lib/features/settings/view/per_app_proxy_page.dart diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/PlatformSettingsHandler.kt b/android/app/src/main/kotlin/com/hiddify/hiddify/PlatformSettingsHandler.kt index 0f2a5706..e3e2f7ff 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/PlatformSettingsHandler.kt +++ b/android/app/src/main/kotlin/com/hiddify/hiddify/PlatformSettingsHandler.kt @@ -1,10 +1,21 @@ package com.hiddify.hiddify +import android.Manifest import android.app.Activity import android.content.Intent +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.drawable.VectorDrawable import android.net.Uri import android.os.Build +import android.util.Base64 import androidx.annotation.NonNull +import androidx.core.graphics.drawable.toBitmap +import com.google.gson.Gson +import com.google.gson.annotations.SerializedName +import com.hiddify.hiddify.Application.Companion.packageManager import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding @@ -12,6 +23,10 @@ import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.PluginRegistry import io.flutter.plugin.common.StandardMethodCodec +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import java.io.ByteArrayOutputStream + class PlatformSettingsHandler : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware, PluginRegistry.ActivityResultListener { @@ -23,10 +38,13 @@ class PlatformSettingsHandler : FlutterPlugin, MethodChannel.MethodCallHandler, const val channelName = "com.hiddify.app/platform.settings" const val REQUEST_IGNORE_BATTERY_OPTIMIZATIONS = 44 + val gson = Gson() enum class Trigger(val method: String) { IsIgnoringBatteryOptimizations("is_ignoring_battery_optimizations"), RequestIgnoreBatteryOptimizations("request_ignore_battery_optimizations"), + GetInstalledPackages("get_installed_packages"), + GetPackagesIcon("get_package_icon"), } } @@ -71,6 +89,12 @@ class PlatformSettingsHandler : FlutterPlugin, MethodChannel.MethodCallHandler, return false } + data class AppItem( + @SerializedName("package-name") val packageName: String, + @SerializedName("name") val name: String, + @SerializedName("is-system-app") val isSystemApp: Boolean + ) + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { Trigger.IsIgnoringBatteryOptimizations.method -> { @@ -97,6 +121,69 @@ class PlatformSettingsHandler : FlutterPlugin, MethodChannel.MethodCallHandler, activity?.startActivityForResult(intent, REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) } + Trigger.GetInstalledPackages.method -> { + GlobalScope.launch { + result.runCatching { + val flag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + PackageManager.GET_PERMISSIONS or PackageManager.MATCH_UNINSTALLED_PACKAGES + } else { + @Suppress("DEPRECATION") + PackageManager.GET_PERMISSIONS or PackageManager.GET_UNINSTALLED_PACKAGES + } + val installedPackages = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + packageManager.getInstalledPackages( + PackageManager.PackageInfoFlags.of( + flag.toLong() + ) + ) + } else { + @Suppress("DEPRECATION") + packageManager.getInstalledPackages(flag) + } + val list = mutableListOf() + installedPackages.forEach { + if (it.packageName != Application.application.packageName && + (it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true + || it.packageName == "android") + ) { + list.add( + AppItem( + it.packageName, + it.applicationInfo.loadLabel(packageManager).toString(), + it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM == 1 + ) + ) + } + } + list.sortBy { it.name } + success(gson.toJson(list)) + } + } + } + + Trigger.GetPackagesIcon.method -> { + result.runCatching { + val args = call.arguments as Map<*, *> + val packageName = + args["packageName"] as String? ?: return error("provide packageName") + val drawable = packageManager.getApplicationIcon(packageName) + val bitmap = Bitmap.createBitmap( + drawable.intrinsicWidth, + drawable.intrinsicHeight, + Bitmap.Config.ARGB_8888 + ) + val canvas = Canvas(bitmap) + drawable.setBounds(0, 0, canvas.width, canvas.height) + drawable.draw(canvas) + val byteArrayOutputStream = ByteArrayOutputStream() + bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream) + val base64: String = + Base64.encodeToString(byteArrayOutputStream.toByteArray(), Base64.NO_WRAP) + success(base64) + } + } + else -> result.notImplemented() } } diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/Settings.kt b/android/app/src/main/kotlin/com/hiddify/hiddify/Settings.kt index 9b01b4a5..e801a26a 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/Settings.kt +++ b/android/app/src/main/kotlin/com/hiddify/hiddify/Settings.kt @@ -1,41 +1,62 @@ package com.hiddify.hiddify import android.content.Context +import android.util.Base64 import com.hiddify.hiddify.bg.ProxyService import com.hiddify.hiddify.bg.VPNService +import com.hiddify.hiddify.constant.PerAppProxyMode import com.hiddify.hiddify.constant.ServiceMode import com.hiddify.hiddify.constant.SettingsKey import org.json.JSONObject +import java.io.ByteArrayInputStream import java.io.File +import java.io.ObjectInputStream + object Settings { - const val PER_APP_PROXY_DISABLED = 0 - const val PER_APP_PROXY_EXCLUDE = 1 - const val PER_APP_PROXY_INCLUDE = 2 - private val preferences by lazy { val context = Application.application.applicationContext context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE) } - var perAppProxyEnabled = preferences.getBoolean(SettingsKey.PER_APP_PROXY_ENABLED, false) - var perAppProxyMode = preferences.getInt(SettingsKey.PER_APP_PROXY_MODE, PER_APP_PROXY_EXCLUDE) - var perAppProxyList = preferences.getStringSet(SettingsKey.PER_APP_PROXY_LIST, emptySet())!! - var perAppProxyUpdateOnChange = - preferences.getInt(SettingsKey.PER_APP_PROXY_UPDATE_ON_CHANGE, PER_APP_PROXY_DISABLED) + private const val LIST_IDENTIFIER = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIGxpc3Qu" + + var perAppProxyMode: String + get() = preferences.getString(SettingsKey.PER_APP_PROXY_MODE, PerAppProxyMode.OFF)!! + set(value) = preferences.edit().putString(SettingsKey.PER_APP_PROXY_MODE, value).apply() + + val perAppProxyEnabled: Boolean + get() = perAppProxyMode != PerAppProxyMode.OFF + + val perAppProxyList: List + get() { + val stringValue = if (perAppProxyMode == PerAppProxyMode.INCLUDE) { + preferences.getString(SettingsKey.PER_APP_PROXY_INCLUDE_LIST, "")!!; + } else { + preferences.getString(SettingsKey.PER_APP_PROXY_EXCLUDE_LIST, "")!!; + } + if (!stringValue.startsWith(LIST_IDENTIFIER)) { + return emptyList() + } + return decodeListString(stringValue.substring(LIST_IDENTIFIER.length)) + } + + private fun decodeListString(listString: String): List { + val stream = ObjectInputStream(ByteArrayInputStream(Base64.decode(listString, 0))) + return stream.readObject() as List + } var activeConfigPath: String - get() = preferences.getString(SettingsKey.ACTIVE_CONFIG_PATH, "") ?: "" + get() = preferences.getString(SettingsKey.ACTIVE_CONFIG_PATH, "")!! set(value) = preferences.edit().putString(SettingsKey.ACTIVE_CONFIG_PATH, value).apply() var serviceMode: String - get() = preferences.getString(SettingsKey.SERVICE_MODE, ServiceMode.NORMAL) - ?: ServiceMode.NORMAL + get() = preferences.getString(SettingsKey.SERVICE_MODE, ServiceMode.NORMAL)!! set(value) = preferences.edit().putString(SettingsKey.SERVICE_MODE, value).apply() var configOptions: String - get() = preferences.getString(SettingsKey.CONFIG_OPTIONS, "") ?: "" + get() = preferences.getString(SettingsKey.CONFIG_OPTIONS, "")!! set(value) = preferences.edit().putString(SettingsKey.CONFIG_OPTIONS, value).apply() var debugMode: Boolean @@ -47,11 +68,13 @@ object Settings { var disableMemoryLimit: Boolean get() = preferences.getBoolean(SettingsKey.DISABLE_MEMORY_LIMIT, false) - set(value) = preferences.edit().putBoolean(SettingsKey.DISABLE_MEMORY_LIMIT, value).apply() + set(value) = + preferences.edit().putBoolean(SettingsKey.DISABLE_MEMORY_LIMIT, value).apply() var systemProxyEnabled: Boolean get() = preferences.getBoolean(SettingsKey.SYSTEM_PROXY_ENABLED, true) - set(value) = preferences.edit().putBoolean(SettingsKey.SYSTEM_PROXY_ENABLED, value).apply() + set(value) = + preferences.edit().putBoolean(SettingsKey.SYSTEM_PROXY_ENABLED, value).apply() var startedByUser: Boolean get() = preferences.getBoolean(SettingsKey.STARTED_BY_USER, false) @@ -80,7 +103,7 @@ object Settings { } private suspend fun needVPNService(): Boolean { - if(enableTun) return true + if (enableTun) return true val filePath = activeConfigPath if (filePath.isBlank()) return false val content = JSONObject(File(filePath).readText()) diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/AppChangeReceiver.kt b/android/app/src/main/kotlin/com/hiddify/hiddify/bg/AppChangeReceiver.kt index 0a232e1b..fd028f6c 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/AppChangeReceiver.kt +++ b/android/app/src/main/kotlin/com/hiddify/hiddify/bg/AppChangeReceiver.kt @@ -16,22 +16,22 @@ class AppChangeReceiver : BroadcastReceiver() { } private fun checkUpdate(context: Context, intent: Intent) { - if (!Settings.perAppProxyEnabled) { - return - } - val perAppProxyUpdateOnChange = Settings.perAppProxyUpdateOnChange - if (perAppProxyUpdateOnChange == Settings.PER_APP_PROXY_DISABLED) { - return - } - val packageName = intent.dataString?.substringAfter("package:") - if (packageName.isNullOrBlank()) { - return - } - if ((perAppProxyUpdateOnChange == Settings.PER_APP_PROXY_INCLUDE)) { - Settings.perAppProxyList = Settings.perAppProxyList + packageName - } else { - Settings.perAppProxyList = Settings.perAppProxyList - packageName - } +// if (!Settings.perAppProxyEnabled) { +// return +// } +// val perAppProxyUpdateOnChange = Settings.perAppProxyUpdateOnChange +// if (perAppProxyUpdateOnChange == Settings.PER_APP_PROXY_DISABLED) { +// return +// } +// val packageName = intent.dataString?.substringAfter("package:") +// if (packageName.isNullOrBlank()) { +// return +// } +// if ((perAppProxyUpdateOnChange == Settings.PER_APP_PROXY_INCLUDE)) { +// Settings.perAppProxyList = Settings.perAppProxyList + packageName +// } else { +// Settings.perAppProxyList = Settings.perAppProxyList - packageName +// } } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/VPNService.kt b/android/app/src/main/kotlin/com/hiddify/hiddify/bg/VPNService.kt index ca03dbc7..c0cfba41 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/VPNService.kt +++ b/android/app/src/main/kotlin/com/hiddify/hiddify/bg/VPNService.kt @@ -6,6 +6,7 @@ import android.content.pm.PackageManager.NameNotFoundException import android.net.ProxyInfo import android.net.VpnService import android.os.Build +import com.hiddify.hiddify.constant.PerAppProxyMode import io.nekohasekai.libbox.TunOptions class VPNService : VpnService(), PlatformInterfaceWrapper { @@ -87,7 +88,7 @@ class VPNService : VpnService(), PlatformInterfaceWrapper { if (Settings.perAppProxyEnabled) { val appList = Settings.perAppProxyList - if (Settings.perAppProxyMode == Settings.PER_APP_PROXY_INCLUDE) { + if (Settings.perAppProxyMode == PerAppProxyMode.INCLUDE) { appList.forEach { try { builder.addAllowedApplication(it) diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/constant/PerAppProxyMode.kt b/android/app/src/main/kotlin/com/hiddify/hiddify/constant/PerAppProxyMode.kt new file mode 100644 index 00000000..aafe920a --- /dev/null +++ b/android/app/src/main/kotlin/com/hiddify/hiddify/constant/PerAppProxyMode.kt @@ -0,0 +1,7 @@ +package com.hiddify.hiddify.constant + +object PerAppProxyMode { + const val OFF = "off" + const val INCLUDE = "include" + const val EXCLUDE = "exclude" +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/constant/SettingsKey.kt b/android/app/src/main/kotlin/com/hiddify/hiddify/constant/SettingsKey.kt index 1b6477c8..21e7dd0f 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/constant/SettingsKey.kt +++ b/android/app/src/main/kotlin/com/hiddify/hiddify/constant/SettingsKey.kt @@ -8,10 +8,9 @@ object SettingsKey { const val CONFIG_OPTIONS = "config_options_json" - const val PER_APP_PROXY_ENABLED = "per_app_proxy_enabled" - const val PER_APP_PROXY_MODE = "per_app_proxy_mode" - const val PER_APP_PROXY_LIST = "per_app_proxy_list" - const val PER_APP_PROXY_UPDATE_ON_CHANGE = "per_app_proxy_update_on_change" + const val PER_APP_PROXY_MODE = "${KEY_PREFIX}per_app_proxy_mode" + const val PER_APP_PROXY_INCLUDE_LIST = "${KEY_PREFIX}per_app_proxy_include_list" + const val PER_APP_PROXY_EXCLUDE_LIST = "${KEY_PREFIX}per_app_proxy_exclude_list" const val DEBUG_MODE = "${KEY_PREFIX}debug_mode" const val ENABLE_TUN = "${KEY_PREFIX}enable-tun" diff --git a/assets/translations/strings.i18n.json b/assets/translations/strings.i18n.json index e479fbff..45bb39df 100644 --- a/assets/translations/strings.i18n.json +++ b/assets/translations/strings.i18n.json @@ -117,6 +117,20 @@ "debugMode": "Debug Mode", "debugModeMsg": "Restart the app for applying this change" }, + "network": { + "perAppProxyPageTitle": "Per-app Proxy", + "perAppProxyModes": { + "off": "All", + "offMsg": "Proxy all apps", + "include": "Proxy", + "includeMsg": "Proxy only selected apps", + "exclude": "Bypass", + "excludeMsg": "Do not proxy selected apps" + }, + "showSystemApps": "Show system apps", + "hideSystemApps": "Hide system apps", + "clearSelection": "Clear selection" + }, "config": { "section": { "route": "Route Options", diff --git a/assets/translations/strings_fa.i18n.json b/assets/translations/strings_fa.i18n.json index f47cce18..55f652c1 100644 --- a/assets/translations/strings_fa.i18n.json +++ b/assets/translations/strings_fa.i18n.json @@ -117,6 +117,20 @@ "debugMode": "دیباگ مود", "debugModeMsg": "برای اعمال این تغییر اپ را ری‌استارت کنید" }, + "network": { + "perAppProxyPageTitle": "پراکسی برنامه‌ها", + "perAppProxyModes": { + "off": "همه", + "offMsg": "همه برنامه‌ها پراکسی میشوند", + "include": "پراکسی", + "includeMsg": "تنها برنامه‌های انتخاب شده پراکسی میشوند", + "exclude": "بایپس", + "excludeMsg": "همه بجز برنامه‌های انتخاب شده پراکسی میشوند" + }, + "showSystemApps": "نمایش برنامه‌های سیستمی", + "hideSystemApps": "مخفی کردن برنامه‌های سیستمی", + "clearSelection": "حذف انتخاب‌ها" + }, "config": { "section": { "route": "تنظیمات مسیریاب", diff --git a/lib/core/prefs/general_prefs.dart b/lib/core/prefs/general_prefs.dart index 32f93b5c..6af8727b 100644 --- a/lib/core/prefs/general_prefs.dart +++ b/lib/core/prefs/general_prefs.dart @@ -1,6 +1,7 @@ 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/utils/pref_notifier.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -37,6 +38,54 @@ class DebugModeNotifier extends _$DebugModeNotifier { } } +@Riverpod(keepAlive: true) +class PerAppProxyModeNotifier extends _$PerAppProxyModeNotifier { + late final _pref = Pref( + ref.watch(sharedPreferencesProvider), + "per_app_proxy_mode", + PerAppProxyMode.off, + mapFrom: PerAppProxyMode.values.byName, + mapTo: (value) => value.name, + ); + + @override + PerAppProxyMode build() => _pref.getValue(); + + Future update(PerAppProxyMode value) { + state = value; + return _pref.update(value); + } +} + +@Riverpod(keepAlive: true) +class PerAppProxyList extends _$PerAppProxyList { + late final _include = Pref( + ref.watch(sharedPreferencesProvider), + "per_app_proxy_include_list", + [], + ); + + late final _exclude = Pref( + ref.watch(sharedPreferencesProvider), + "per_app_proxy_exclude_list", + [], + ); + + @override + List build() => + ref.watch(perAppProxyModeNotifierProvider) == PerAppProxyMode.include + ? _include.getValue() + : _exclude.getValue(); + + Future update(List value) { + state = value; + if (ref.read(perAppProxyModeNotifierProvider) == PerAppProxyMode.include) { + return _include.update(value); + } + return _exclude.update(value); + } +} + @riverpod class MarkNewProfileActive extends _$MarkNewProfileActive { late final _pref = Pref( diff --git a/lib/core/router/routes/mobile_routes.dart b/lib/core/router/routes/mobile_routes.dart index c51c506f..98004ed0 100644 --- a/lib/core/router/routes/mobile_routes.dart +++ b/lib/core/router/routes/mobile_routes.dart @@ -22,6 +22,7 @@ part 'mobile_routes.g.dart'; path: SettingsRoute.path, routes: [ TypedGoRoute(path: ConfigOptionsRoute.path), + TypedGoRoute(path: PerAppProxyRoute.path), ], ), TypedGoRoute(path: AboutRoute.path), @@ -84,6 +85,21 @@ class ConfigOptionsRoute extends GoRouteData { } } +class PerAppProxyRoute extends GoRouteData { + const PerAppProxyRoute(); + static const path = 'per-app-proxy'; + + static final GlobalKey $parentNavigatorKey = rootNavigatorKey; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + return const MaterialPage( + fullscreenDialog: true, + child: PerAppProxyPage(), + ); + } +} + class AboutRoute extends GoRouteData { const AboutRoute(); static const path = 'about'; diff --git a/lib/domain/singbox/rules.dart b/lib/domain/singbox/rules.dart new file mode 100644 index 00000000..29cede22 --- /dev/null +++ b/lib/domain/singbox/rules.dart @@ -0,0 +1,24 @@ +import 'package:hiddify/core/prefs/locale_prefs.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/domain/singbox/singbox.dart b/lib/domain/singbox/singbox.dart index 1d47ba21..39bb5774 100644 --- a/lib/domain/singbox/singbox.dart +++ b/lib/domain/singbox/singbox.dart @@ -2,4 +2,5 @@ export 'config_options.dart'; export 'core_status.dart'; export 'outbounds.dart'; export 'proxy_type.dart'; +export 'rules.dart'; export 'singbox_facade.dart'; diff --git a/lib/features/settings/view/per_app_proxy_page.dart b/lib/features/settings/view/per_app_proxy_page.dart new file mode 100644 index 00000000..814dbf07 --- /dev/null +++ b/lib/features/settings/view/per_app_proxy_page.dart @@ -0,0 +1,226 @@ +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_settings.dart'; +import 'package:hiddify/services/service_providers.dart'; +import 'package:hiddify/utils/riverpod_utils.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:loggy/loggy.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:sliver_tools/sliver_tools.dart'; + +part 'per_app_proxy_page.g.dart'; + +final _logger = Loggy("PerAppProxySettings"); + +@riverpod +Future> installedPackagesInfo( + InstalledPackagesInfoRef ref, +) async { + return ref + .watch(platformSettingsProvider) + .getInstalledPackages() + .getOrElse((l) { + _logger.warning("error getting installed packages: $l"); + throw l; + }).run(); +} + +@riverpod +Future packageIcon( + PackageIconRef ref, + String packageName, +) async { + ref.disposeDelay(const Duration(seconds: 10)); + final bytes = await ref + .watch(platformSettingsProvider) + .getPackageIcon(packageName) + .getOrElse((l) { + _logger.warning("error getting package icon: $l"); + throw l; + }).run(); + return MemoryImage(bytes); +} + +class PerAppProxyPage extends HookConsumerWidget with PresLogger { + const PerAppProxyPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + final localizations = MaterialLocalizations.of(context); + + final asyncPackages = ref.watch(installedPackagesInfoProvider); + final perAppProxyMode = ref.watch(perAppProxyModeNotifierProvider); + final perAppProxyList = ref.watch(perAppProxyListProvider); + + final showSystemApps = useState(true); + final isSearching = useState(false); + final searchQuery = useState(""); + + final filteredPackages = useMemoized( + () { + if (showSystemApps.value && searchQuery.value.isBlank) { + return asyncPackages; + } + return asyncPackages.whenData( + (value) { + Iterable result = value; + if (!showSystemApps.value) { + result = result.filter((e) => !e.isSystemApp); + } + if (!searchQuery.value.isBlank) { + result = result.filter( + (e) => e.name + .toLowerCase() + .contains(searchQuery.value.toLowerCase()), + ); + } + return result.toList(); + }, + ); + }, + [asyncPackages, showSystemApps.value, searchQuery.value], + ); + + return Scaffold( + appBar: isSearching.value + ? AppBar( + title: TextFormField( + onChanged: (value) => searchQuery.value = value, + autofocus: true, + decoration: InputDecoration.collapsed( + hintText: "${localizations.searchFieldLabel}...", + ), + ), + leading: IconButton( + onPressed: () { + searchQuery.value = ""; + isSearching.value = false; + }, + icon: const Icon(Icons.close), + tooltip: localizations.cancelButtonLabel, + ), + ) + : AppBar( + title: Text(t.settings.network.perAppProxyPageTitle), + actions: [ + IconButton( + icon: const Icon(Icons.search), + onPressed: () => isSearching.value = true, + tooltip: localizations.searchFieldLabel, + ), + PopupMenuButton( + itemBuilder: (context) { + return [ + PopupMenuItem( + child: Text( + showSystemApps.value + ? t.settings.network.hideSystemApps + : t.settings.network.showSystemApps, + ), + onTap: () => + showSystemApps.value = !showSystemApps.value, + ), + PopupMenuItem( + child: Text(t.settings.network.clearSelection), + onTap: () => ref + .read(perAppProxyListProvider.notifier) + .update([]), + ), + ]; + }, + ), + ], + ), + body: CustomScrollView( + slivers: [ + SliverPinnedHeader( + child: DecoratedBox( + decoration: BoxDecoration( + color: Theme.of(context).scaffoldBackgroundColor, + ), + child: Column( + children: [ + ...PerAppProxyMode.values.map( + (e) => RadioListTile( + title: Text(e.present(t).message), + dense: true, + value: e, + groupValue: perAppProxyMode, + onChanged: (value) async { + await ref + .read(perAppProxyModeNotifierProvider.notifier) + .update(e); + if (e == PerAppProxyMode.off && context.mounted) { + context.pop(); + } + }, + ), + ), + const Divider(height: 1), + ], + ), + ), + ), + switch (filteredPackages) { + AsyncData(value: final packages) => SliverList.builder( + itemBuilder: (context, index) { + final package = packages[index]; + final selected = + perAppProxyList.contains(package.packageName); + return CheckboxListTile( + title: Text( + package.name, + overflow: TextOverflow.ellipsis, + ), + subtitle: Text(package.packageName), + value: selected, + onChanged: (value) async { + final List newSelection; + if (selected) { + newSelection = perAppProxyList + .exceptElement(package.packageName) + .toList(); + } else { + newSelection = [ + ...perAppProxyList, + package.packageName, + ]; + } + await ref + .read(perAppProxyListProvider.notifier) + .update(newSelection); + }, + secondary: SizedBox( + width: 48, + height: 48, + child: ref + .watch(packageIconProvider(package.packageName)) + .when( + data: (data) => Image(image: data), + error: (error, _) => const Icon(Icons.error), + loading: () => const Center( + child: CircularProgressIndicator(), + ), + ), + ), + ); + }, + itemCount: packages.length, + ), + AsyncLoading() => const SliverLoadingBodyPlaceholder(), + AsyncError(:final error) => + SliverErrorBodyPlaceholder(error.toString()), + _ => const SliverToBoxAdapter(), + }, + ], + ), + ); + } +} diff --git a/lib/features/settings/view/view.dart b/lib/features/settings/view/view.dart index 458e1d4d..c94412af 100644 --- a/lib/features/settings/view/view.dart +++ b/lib/features/settings/view/view.dart @@ -1,2 +1,3 @@ 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 a4960be0..82b94bdb 100644 --- a/lib/features/settings/widgets/advanced_setting_tiles.dart +++ b/lib/features/settings/widgets/advanced_setting_tiles.dart @@ -1,8 +1,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/router/routes/routes.dart'; +import 'package:hiddify/domain/singbox/singbox.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; class AdvancedSettingTiles extends HookConsumerWidget { @@ -13,6 +16,7 @@ class AdvancedSettingTiles extends HookConsumerWidget { final t = ref.watch(translationsProvider); final debug = ref.watch(debugModeNotifierProvider); + final perAppProxy = ref.watch(perAppProxyModeNotifierProvider).enabled; return Column( children: [ @@ -23,6 +27,33 @@ class AdvancedSettingTiles extends HookConsumerWidget { await const ConfigOptionsRoute().push(context); }, ), + if (Platform.isAndroid) ...[ + ListTile( + title: Text(t.settings.network.perAppProxyPageTitle), + leading: const Icon(Icons.apps), + trailing: Switch( + value: perAppProxy, + onChanged: (value) async { + final newMode = + perAppProxy ? PerAppProxyMode.off : PerAppProxyMode.exclude; + await ref + .read(perAppProxyModeNotifierProvider.notifier) + .update(newMode); + if (!perAppProxy && context.mounted) { + await const PerAppProxyRoute().push(context); + } + }, + ), + onTap: () async { + if (!perAppProxy) { + await ref + .read(perAppProxyModeNotifierProvider.notifier) + .update(PerAppProxyMode.exclude); + } + if (context.mounted) await const PerAppProxyRoute().push(context); + }, + ), + ], SwitchListTile( title: Text(t.settings.advanced.debugMode), value: debug, diff --git a/lib/services/platform_settings.dart b/lib/services/platform_settings.dart index 17267ea2..37ee6b3f 100644 --- a/lib/services/platform_settings.dart +++ b/lib/services/platform_settings.dart @@ -1,7 +1,13 @@ +import 'dart:convert'; + import 'package:flutter/services.dart'; import 'package:fpdart/fpdart.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hiddify/utils/utils.dart'; +part 'platform_settings.freezed.dart'; +part 'platform_settings.g.dart'; + class PlatformSettings with InfraLogger { late final MethodChannel _methodChannel = const MethodChannel("com.hiddify.app/platform.settings"); @@ -29,4 +35,55 @@ class PlatformSettings with InfraLogger { }, ); } + + 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); } From 90fa8455acc398cbc4f6b7462c2aed7e3381f6d6 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Thu, 14 Sep 2023 12:56:22 +0330 Subject: [PATCH 22/23] Fix per-app proxy selection --- .../settings/view/per_app_proxy_page.dart | 15 +++++++++++++-- lib/utils/pref_notifier.dart | 6 ++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/lib/features/settings/view/per_app_proxy_page.dart b/lib/features/settings/view/per_app_proxy_page.dart index 814dbf07..c1027c80 100644 --- a/lib/features/settings/view/per_app_proxy_page.dart +++ b/lib/features/settings/view/per_app_proxy_page.dart @@ -94,8 +94,16 @@ class PerAppProxyPage extends HookConsumerWidget with PresLogger { title: TextFormField( onChanged: (value) => searchQuery.value = value, autofocus: true, - decoration: InputDecoration.collapsed( + decoration: InputDecoration( hintText: "${localizations.searchFieldLabel}...", + isDense: true, + filled: false, + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + disabledBorder: InputBorder.none, ), ), leading: IconButton( @@ -179,7 +187,10 @@ class PerAppProxyPage extends HookConsumerWidget with PresLogger { package.name, overflow: TextOverflow.ellipsis, ), - subtitle: Text(package.packageName), + subtitle: Text( + package.packageName, + style: Theme.of(context).textTheme.bodySmall, + ), value: selected, onChanged: (value) async { final List newSelection; diff --git a/lib/utils/pref_notifier.dart b/lib/utils/pref_notifier.dart index 6daccc7a..d3cc6be4 100644 --- a/lib/utils/pref_notifier.dart +++ b/lib/utils/pref_notifier.dart @@ -20,7 +20,7 @@ class Pref with InfraLogger { /// Updates the value asynchronously. Future update(T value) async { - loggy.debug("updating preference [$key] to [$value]"); + loggy.debug("updating preference [$key]($T) to [$value]"); try { if (mapTo != null && mapFrom != null) { await prefs.setString(key, mapTo!(value)); @@ -45,10 +45,12 @@ class Pref with InfraLogger { T getValue() { try { - loggy.debug("getting persisted preference [$key]"); + loggy.debug("getting persisted preference [$key]($T)"); if (mapTo != null && mapFrom != null) { final persisted = prefs.getString(key); return persisted != null ? mapFrom!(persisted) : defaultValue; + } else if (T == List) { + return prefs.getStringList(key) as T ?? defaultValue; } return prefs.get(key) as T? ?? defaultValue; } catch (e) { From 6dff3a1841f513f814e21e7aa5561c0dacea6fcf Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Thu, 14 Sep 2023 15:20:48 +0330 Subject: [PATCH 23/23] Improve accessability --- assets/translations/strings.i18n.json | 5 ++++ assets/translations/strings_fa.i18n.json | 5 ++++ lib/core/app/app_view.dart | 16 +++++----- lib/domain/constants.dart | 1 + lib/features/common/profile_tile.dart | 29 ++++++++++++++++--- .../controller/system_tray_controller.dart | 2 ++ lib/utils/number_formatters.dart | 2 ++ 7 files changed, 49 insertions(+), 11 deletions(-) diff --git a/assets/translations/strings.i18n.json b/assets/translations/strings.i18n.json index 45bb39df..725984ab 100644 --- a/assets/translations/strings.i18n.json +++ b/assets/translations/strings.i18n.json @@ -29,10 +29,15 @@ "profile": { "overviewPageTitle": "Profiles", "detailsPageTitle": "Profile", + "activeProfileNameSemanticLabel": "Active profile name: ${name}", + "nonActiveProfileNameSemanticLabel": "Profile name: ${name}", + "activeProfileBtnSemanticLabel": "View all profiles", + "nonActiveProfileBtnSemanticLabel": "Select ${name} as active", "subscription": { "traffic": "Traffic", "updatedTimeAgo": "Updated ${timeago}", "remainingDuration": "${duration} Days Remaining", + "remainingTrafficSemanticLabel": "${consumed} of ${total} traffic consumed", "expired": "Expired", "noTraffic": "No more traffic" }, diff --git a/assets/translations/strings_fa.i18n.json b/assets/translations/strings_fa.i18n.json index 55f652c1..9a39a9fb 100644 --- a/assets/translations/strings_fa.i18n.json +++ b/assets/translations/strings_fa.i18n.json @@ -29,10 +29,15 @@ "profile": { "overviewPageTitle": "پروفایل‌ها", "detailsPageTitle": "پروفایل", + "activeProfileNameSemanticLabel": "نام پروفایل فعال: ${name}", + "nonActiveProfileNameSemanticLabel": "نام پروفایل: ${name}", + "activeProfileBtnSemanticLabel": "همه‌ی پروفایل‌ها", + "nonActiveProfileBtnSemanticLabel": "انتخاب ${name} به عنوان پروفایل فعال", "subscription": { "traffic": "ترافیک", "updatedTimeAgo": "بروزرسانی شده در ${timeago}", "remainingDuration": "${duration} روز باقی مانده", + "remainingTrafficSemanticLabel": "${consumed} از ${total} ترافیک مصرف شده", "expired": "منقضی شده", "noTraffic": "پایان ترافیک" }, diff --git a/lib/core/app/app_view.dart b/lib/core/app/app_view.dart index f3f6dd21..a65b6223 100644 --- a/lib/core/app/app_view.dart +++ b/lib/core/app/app_view.dart @@ -4,6 +4,7 @@ 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/router/router.dart'; +import 'package:hiddify/domain/constants.dart'; import 'package:hiddify/features/common/common_controllers.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -20,12 +21,13 @@ class AppView extends HookConsumerWidget with PresLogger { ref.watch(commonControllersProvider); return MaterialApp.router( - builder: (context, child) { - return AccessibilityTools( - checkFontOverflows: true, - child: child, - ); - }, + // builder: (context, child) { + // return AccessibilityTools( + // checkFontOverflows: true, + // child: child, + // ); + // }, + // showSemanticsDebugger: true, routerConfig: router, locale: locale, supportedLocales: AppLocaleUtils.supportedLocales, @@ -34,7 +36,7 @@ class AppView extends HookConsumerWidget with PresLogger { themeMode: theme.mode, theme: theme.light(), darkTheme: theme.dark(), - title: 'Hiddify Next', + title: Constants.appName, ); } } diff --git a/lib/domain/constants.dart b/lib/domain/constants.dart index c84ec8c5..8151c264 100644 --- a/lib/domain/constants.dart +++ b/lib/domain/constants.dart @@ -1,4 +1,5 @@ abstract class Constants { + static const appName = "Hiddify Next"; static const geoipFileName = "geoip.db"; static const geositeFileName = "geosite.db"; static const configsFolderName = "configs"; diff --git a/lib/features/common/profile_tile.dart b/lib/features/common/profile_tile.dart index 5a02154c..a42ac8c0 100644 --- a/lib/features/common/profile_tile.dart +++ b/lib/features/common/profile_tile.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; import 'package:hiddify/core/core_providers.dart'; @@ -48,7 +49,6 @@ class ProfileTile extends HookConsumerWidget { profile.active ? theme.colorScheme.outlineVariant : Colors.transparent; return Card( - semanticContainer: false, margin: effectiveMargin, elevation: effectiveElevation, shape: RoundedRectangleBorder( @@ -62,7 +62,10 @@ class ProfileTile extends HookConsumerWidget { children: [ SizedBox( width: 48, - child: ProfileActionButton(profile, !isMain), + child: Semantics( + sortKey: const OrdinalSortKey(1), + child: ProfileActionButton(profile, !isMain), + ), ), VerticalDivider( width: 1, @@ -71,9 +74,14 @@ class ProfileTile extends HookConsumerWidget { Flexible( child: Semantics( button: true, + sortKey: isMain ? const OrdinalSortKey(0) : null, + focused: isMain, + liveRegion: isMain, + namesRoute: isMain, label: isMain - ? t.profile.overviewPageTitle - : t.profile.edit.selectActiveTxt, + ? t.profile.activeProfileBtnSemanticLabel + : t.profile + .nonActiveProfileBtnSemanticLabel(name: profile.name), child: InkWell( onTap: () { if (isMain) { @@ -110,6 +118,10 @@ class ProfileTile extends HookConsumerWidget { Flexible( child: Text( profile.name, + semanticsLabel: t.profile + .activeProfileNameSemanticLabel( + name: profile.name, + ), style: theme.textTheme.titleMedium, ), ), @@ -121,6 +133,10 @@ class ProfileTile extends HookConsumerWidget { else Text( profile.name, + semanticsLabel: + t.profile.nonActiveProfileNameSemanticLabel( + name: profile.name, + ), style: theme.textTheme.titleMedium, ), if (subInfo != null) ...[ @@ -320,6 +336,11 @@ class ProfileSubscriptionInfo extends HookConsumerWidget { subInfo.total > 10 * 1099511627776 //10TB ? "∞ GiB" : subInfo.consumption.sizeOf(subInfo.total), + semanticsLabel: + t.profile.subscription.remainingTrafficSemanticLabel( + consumed: subInfo.consumption.sizeGB(), + total: subInfo.total.sizeGB(), + ), style: theme.textTheme.bodySmall, ), ), diff --git a/lib/features/system_tray/controller/system_tray_controller.dart b/lib/features/system_tray/controller/system_tray_controller.dart index da194952..2b3eb10e 100644 --- a/lib/features/system_tray/controller/system_tray_controller.dart +++ b/lib/features/system_tray/controller/system_tray_controller.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:hiddify/core/core_providers.dart'; import 'package:hiddify/domain/connectivity/connectivity.dart'; +import 'package:hiddify/domain/constants.dart'; import 'package:hiddify/features/common/connectivity/connectivity_controller.dart'; import 'package:hiddify/features/common/window/window_controller.dart'; import 'package:hiddify/gen/assets.gen.dart'; @@ -22,6 +23,7 @@ class SystemTrayController extends _$SystemTrayController _trayIconPath, isTemplate: Platform.isMacOS, ); + if (!Platform.isLinux) await trayManager.setToolTip(Constants.appName); trayManager.addListener(this); _initialized = true; } diff --git a/lib/utils/number_formatters.dart b/lib/utils/number_formatters.dart index df1688dd..4ddc25e4 100644 --- a/lib/utils/number_formatters.dart +++ b/lib/utils/number_formatters.dart @@ -6,6 +6,8 @@ extension ByteFormatter on int { static final _sizeOfFormat = InformationSizeFormat(permissibleValueUnits: {InformationUnit.gibibyte}); + String sizeGB() => _sizeOfFormat.format(bytes()); + String sizeOf(int total) => "${_sizeOfFormat.format(bytes())} / ${_sizeOfFormat.format(total.bytes())}";