initial
This commit is contained in:
28
lib/services/clash/async_ffi.dart
Normal file
28
lib/services/clash/async_ffi.dart
Normal file
@@ -0,0 +1,28 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:ffi';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:hiddify/services/clash/async_ffi_response.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
|
||||
// TODO: add timeout
|
||||
// TODO: test and improve
|
||||
mixin AsyncFFI implements LoggerMixin {
|
||||
Future<AsyncFfiResponse> runAsync(void Function(int port) run) async {
|
||||
final receivePort = ReceivePort();
|
||||
final responseFuture = receivePort.map(
|
||||
(event) {
|
||||
if (event is String) {
|
||||
receivePort.close();
|
||||
return AsyncFfiResponse.fromJson(
|
||||
jsonDecode(event) as Map<String, dynamic>,
|
||||
);
|
||||
}
|
||||
receivePort.close();
|
||||
throw Exception("unexpected data type[${event.runtimeType}]");
|
||||
},
|
||||
).first;
|
||||
run(receivePort.sendPort.nativePort);
|
||||
return responseFuture;
|
||||
}
|
||||
}
|
||||
18
lib/services/clash/async_ffi_response.dart
Normal file
18
lib/services/clash/async_ffi_response.dart
Normal file
@@ -0,0 +1,18 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'async_ffi_response.freezed.dart';
|
||||
part 'async_ffi_response.g.dart';
|
||||
|
||||
@freezed
|
||||
class AsyncFfiResponse with _$AsyncFfiResponse {
|
||||
const AsyncFfiResponse._();
|
||||
|
||||
const factory AsyncFfiResponse({
|
||||
@JsonKey(name: 'success') required bool success,
|
||||
@JsonKey(name: 'message') String? message,
|
||||
@JsonKey(name: 'data') String? data,
|
||||
}) = _AsyncFfiResponse;
|
||||
|
||||
factory AsyncFfiResponse.fromJson(Map<String, dynamic> json) =>
|
||||
_$AsyncFfiResponseFromJson(json);
|
||||
}
|
||||
2
lib/services/clash/clash.dart
Normal file
2
lib/services/clash/clash.dart
Normal file
@@ -0,0 +1,2 @@
|
||||
export 'clash_service.dart';
|
||||
export 'clash_service_impl.dart';
|
||||
35
lib/services/clash/clash_service.dart
Normal file
35
lib/services/clash/clash_service.dart
Normal file
@@ -0,0 +1,35 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import 'package:hiddify/domain/clash/clash.dart';
|
||||
|
||||
abstract class ClashService {
|
||||
Future<void> init();
|
||||
|
||||
Future<void> start({String configFileName = "config"});
|
||||
|
||||
TaskEither<String, bool> validateConfig(String configPath);
|
||||
|
||||
TaskEither<String, List<ClashProxy>> getProxies();
|
||||
|
||||
TaskEither<String, Unit> changeProxy(
|
||||
String selectorName,
|
||||
String proxyName,
|
||||
);
|
||||
|
||||
TaskEither<String, int> getProxyDelay(
|
||||
String name,
|
||||
String url, {
|
||||
Duration timeout = const Duration(seconds: 10),
|
||||
});
|
||||
|
||||
TaskEither<String, ClashConfig> getConfigs();
|
||||
|
||||
TaskEither<String, Unit> updateConfigs(String path);
|
||||
|
||||
TaskEither<String, Unit> patchConfigs(ClashConfig config);
|
||||
|
||||
Stream<ClashLog> watchLogs(LogLevel level);
|
||||
|
||||
TaskEither<String, ClashTraffic> getTraffic();
|
||||
}
|
||||
235
lib/services/clash/clash_service_impl.dart
Normal file
235
lib/services/clash/clash_service_impl.dart
Normal file
@@ -0,0 +1,235 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:combine/combine.dart';
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import 'package:hiddify/domain/clash/clash.dart';
|
||||
import 'package:hiddify/gen/clash_generated_bindings.dart';
|
||||
import 'package:hiddify/services/clash/async_ffi.dart';
|
||||
import 'package:hiddify/services/clash/clash_service.dart';
|
||||
import 'package:hiddify/services/files_editor_service.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
|
||||
// TODO: logging has potential memory leak
|
||||
class ClashServiceImpl with AsyncFFI, InfraLogger implements ClashService {
|
||||
ClashServiceImpl({required this.filesEditor});
|
||||
|
||||
final FilesEditorService filesEditor;
|
||||
|
||||
late final ClashNativeLibrary _clash;
|
||||
|
||||
@override
|
||||
Future<void> init() async {
|
||||
loggy.debug('initializing');
|
||||
_initClashLib();
|
||||
_clash.initNativeDartBridge(NativeApi.initializeApiDLData);
|
||||
}
|
||||
|
||||
void _initClashLib() {
|
||||
String fullPath = "";
|
||||
if (Platform.environment.containsKey('FLUTTER_TEST')) {
|
||||
fullPath = "core";
|
||||
}
|
||||
if (Platform.isWindows) {
|
||||
fullPath = p.join(fullPath, "libclash.dll");
|
||||
} else if (Platform.isMacOS) {
|
||||
fullPath = p.join(fullPath, "libclash.dylib");
|
||||
} else {
|
||||
fullPath = p.join(fullPath, "libclash.so");
|
||||
}
|
||||
loggy.debug('clash native libs path: "$fullPath"');
|
||||
final lib = DynamicLibrary.open(fullPath);
|
||||
_clash = ClashNativeLibrary(lib);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> start({String configFileName = "config"}) async {
|
||||
loggy.debug('starting clash with config: [$configFileName]');
|
||||
final stopWatch = Stopwatch()..start();
|
||||
final configPath = filesEditor.configPath(configFileName);
|
||||
final response = await runAsync(
|
||||
(port) => _clash.setOptions(
|
||||
port,
|
||||
filesEditor.clashDirPath.toNativeUtf8().cast(),
|
||||
configPath.toNativeUtf8().cast(),
|
||||
),
|
||||
);
|
||||
if (!response.success) throw ClashFailure.core(response.message);
|
||||
stopWatch.stop();
|
||||
loggy.info(
|
||||
"started clash service [${stopWatch.elapsedMilliseconds}ms]",
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
TaskEither<String, bool> validateConfig(String configPath) {
|
||||
return TaskEither(
|
||||
() async {
|
||||
final response = await runAsync(
|
||||
(port) =>
|
||||
_clash.validateConfig(port, configPath.toNativeUtf8().cast()),
|
||||
);
|
||||
if (!response.success) return left(response.message ?? '');
|
||||
return right(response.data! == "true");
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
TaskEither<String, Unit> updateConfigs(String path) {
|
||||
return TaskEither(() async {
|
||||
final stopWatch = Stopwatch()..start();
|
||||
final response = await runAsync(
|
||||
(port) => _clash.updateConfigs(port, path.toNativeUtf8().cast(), 0),
|
||||
);
|
||||
stopWatch.stop();
|
||||
if (response.success) {
|
||||
loggy.info("changed config in [${stopWatch.elapsedMilliseconds}ms]");
|
||||
return right(unit);
|
||||
}
|
||||
return left(response.message ?? '');
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
TaskEither<String, List<ClashProxy>> getProxies() {
|
||||
return TaskEither(
|
||||
() async {
|
||||
final response = await runAsync((port) => _clash.getProxies(port));
|
||||
if (!response.success) return left(response.message ?? "");
|
||||
final proxies = await CombineWorker().executeWithArg(
|
||||
(data) {
|
||||
if (data == null) return <ClashProxy>[];
|
||||
final json = jsonDecode(data)['proxies'] as Map<String, dynamic>;
|
||||
final parsed = json.entries.map(
|
||||
(e) {
|
||||
final proxyMap = (e.value as Map<String, dynamic>)
|
||||
..putIfAbsent('name', () => e.key);
|
||||
return ClashProxy.fromJson(proxyMap);
|
||||
},
|
||||
).toList();
|
||||
return parsed;
|
||||
},
|
||||
response.data,
|
||||
);
|
||||
return right(proxies);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
TaskEither<String, Unit> patchConfigs(ClashConfig config) {
|
||||
return TaskEither(
|
||||
() async {
|
||||
final response = await runAsync(
|
||||
(port) => _clash.patchConfigs(
|
||||
port,
|
||||
jsonEncode(config.toJson()).toNativeUtf8().cast(),
|
||||
),
|
||||
);
|
||||
if (!response.success) return left(response.message ?? "");
|
||||
return right(unit);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
TaskEither<String, ClashConfig> getConfigs() {
|
||||
return TaskEither(
|
||||
() async {
|
||||
final response = await runAsync(
|
||||
(port) => _clash.getConfigs(port),
|
||||
);
|
||||
if (!response.success) return left(response.message ?? "");
|
||||
return right(
|
||||
ClashConfig.fromJson(
|
||||
jsonDecode(response.data!) as Map<String, dynamic>,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
TaskEither<String, Unit> changeProxy(
|
||||
String selectorName,
|
||||
String proxyName,
|
||||
) {
|
||||
return TaskEither(
|
||||
() async {
|
||||
final response = await runAsync(
|
||||
(port) => _clash.updateProxy(
|
||||
port,
|
||||
selectorName.toNativeUtf8().cast(),
|
||||
proxyName.toNativeUtf8().cast(),
|
||||
),
|
||||
);
|
||||
if (!response.success) return left(response.message ?? "");
|
||||
return right(unit);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
TaskEither<String, int> getProxyDelay(
|
||||
String name,
|
||||
String url, {
|
||||
Duration timeout = const Duration(seconds: 10),
|
||||
}) {
|
||||
return TaskEither(
|
||||
() async {
|
||||
final response = await runAsync(
|
||||
(port) => _clash.getProxyDelay(
|
||||
port,
|
||||
name.toNativeUtf8().cast(),
|
||||
url.toNativeUtf8().cast(),
|
||||
timeout.inMilliseconds,
|
||||
),
|
||||
);
|
||||
if (!response.success) return left(response.message ?? "");
|
||||
return right(
|
||||
(jsonDecode(response.data!) as Map<String, dynamic>)["delay"] as int,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<ClashLog> watchLogs(LogLevel level) {
|
||||
final logsPort = ReceivePort();
|
||||
final logsStream = logsPort.map(
|
||||
(event) {
|
||||
final json = jsonDecode(event as String) as Map<String, dynamic>;
|
||||
return ClashLog.fromJson(json);
|
||||
},
|
||||
);
|
||||
_clash.startLog(
|
||||
logsPort.sendPort.nativePort,
|
||||
level.name.toNativeUtf8().cast(),
|
||||
);
|
||||
return logsStream.doOnCancel(() => _clash.stopLog());
|
||||
}
|
||||
|
||||
@override
|
||||
TaskEither<String, ClashTraffic> getTraffic() {
|
||||
return TaskEither(
|
||||
() async {
|
||||
final response = await runAsync(
|
||||
(port) => _clash.getTraffic(port),
|
||||
);
|
||||
if (!response.success) return left(response.message ?? "");
|
||||
return right(
|
||||
ClashTraffic.fromJson(
|
||||
jsonDecode(response.data!) as Map<String, dynamic>,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
3
lib/services/connectivity/connectivity.dart
Normal file
3
lib/services/connectivity/connectivity.dart
Normal file
@@ -0,0 +1,3 @@
|
||||
export 'connectivity_service.dart';
|
||||
export 'desktop_connectivity_service.dart';
|
||||
export 'mobile_connectivity_service.dart';
|
||||
27
lib/services/connectivity/connectivity_service.dart
Normal file
27
lib/services/connectivity/connectivity_service.dart
Normal file
@@ -0,0 +1,27 @@
|
||||
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/utils/utils.dart';
|
||||
|
||||
abstract class ConnectivityService {
|
||||
factory ConnectivityService(NotificationService notification) {
|
||||
if (PlatformUtils.isDesktop) return DesktopConnectivityService();
|
||||
return MobileConnectivityService(notification);
|
||||
}
|
||||
|
||||
Future<void> init();
|
||||
|
||||
// TODO: use declarative states
|
||||
Stream<bool> watchConnectionStatus();
|
||||
|
||||
// TODO: remove
|
||||
Future<bool> grantVpnPermission();
|
||||
|
||||
Future<void> connect({
|
||||
required int httpPort,
|
||||
required int socksPort,
|
||||
bool systemProxy = true,
|
||||
});
|
||||
|
||||
Future<void> disconnect();
|
||||
}
|
||||
64
lib/services/connectivity/desktop_connectivity_service.dart
Normal file
64
lib/services/connectivity/desktop_connectivity_service.dart
Normal file
@@ -0,0 +1,64 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:hiddify/domain/constants.dart';
|
||||
import 'package:hiddify/services/connectivity/connectivity_service.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
import 'package:proxy_manager/proxy_manager.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
|
||||
// TODO: rewrite
|
||||
class DesktopConnectivityService
|
||||
with InfraLogger
|
||||
implements ConnectivityService {
|
||||
// TODO: possibly replace
|
||||
final _proxyManager = ProxyManager();
|
||||
|
||||
final _connectionStatus = BehaviorSubject.seeded(false);
|
||||
|
||||
@override
|
||||
Future<void> init() async {}
|
||||
|
||||
@override
|
||||
Stream<bool> watchConnectionStatus() {
|
||||
return _connectionStatus;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> grantVpnPermission() async => true;
|
||||
|
||||
@override
|
||||
Future<void> connect({
|
||||
required int httpPort,
|
||||
required int socksPort,
|
||||
bool systemProxy = true,
|
||||
}) async {
|
||||
loggy.debug('connecting');
|
||||
await Future.wait([
|
||||
_proxyManager.setAsSystemProxy(
|
||||
ProxyTypes.http,
|
||||
Constants.localHost,
|
||||
httpPort,
|
||||
),
|
||||
_proxyManager.setAsSystemProxy(
|
||||
ProxyTypes.https,
|
||||
Constants.localHost,
|
||||
httpPort,
|
||||
)
|
||||
]);
|
||||
if (!Platform.isWindows) {
|
||||
await _proxyManager.setAsSystemProxy(
|
||||
ProxyTypes.socks,
|
||||
Constants.localHost,
|
||||
socksPort,
|
||||
);
|
||||
}
|
||||
_connectionStatus.value = true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> disconnect() async {
|
||||
loggy.debug("disconnecting");
|
||||
await _proxyManager.cleanSystemProxy();
|
||||
_connectionStatus.value = false;
|
||||
}
|
||||
}
|
||||
86
lib/services/connectivity/mobile_connectivity_service.dart
Normal file
86
lib/services/connectivity/mobile_connectivity_service.dart
Normal file
@@ -0,0 +1,86 @@
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hiddify/domain/connectivity/connectivity_failure.dart';
|
||||
import 'package:hiddify/services/connectivity/connectivity_service.dart';
|
||||
import 'package:hiddify/services/notification/notification.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
|
||||
// TODO: rewrite
|
||||
class MobileConnectivityService
|
||||
with InfraLogger
|
||||
implements ConnectivityService {
|
||||
MobileConnectivityService(this._notificationService);
|
||||
|
||||
final NotificationService _notificationService;
|
||||
|
||||
static const _methodChannel = MethodChannel("Hiddify/VpnService");
|
||||
static const _eventChannel = EventChannel("Hiddify/VpnServiceEvents");
|
||||
|
||||
final _connectionStatus = ValueConnectableStream(
|
||||
_eventChannel.receiveBroadcastStream().map((event) => event as bool),
|
||||
).autoConnect();
|
||||
|
||||
@override
|
||||
Future<void> init() async {
|
||||
loggy.debug("initializing");
|
||||
final initialStatus = _connectionStatus.first;
|
||||
await _methodChannel.invokeMethod("refresh_status");
|
||||
await initialStatus;
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<bool> watchConnectionStatus() {
|
||||
return _connectionStatus;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> grantVpnPermission() async {
|
||||
loggy.debug('requesting vpn permission');
|
||||
final result = await _methodChannel.invokeMethod<bool>("grant_permission");
|
||||
if (!(result ?? false)) {
|
||||
loggy.info("vpn permission denied");
|
||||
}
|
||||
return result ?? false;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> connect({
|
||||
required int httpPort,
|
||||
required int socksPort,
|
||||
bool systemProxy = true,
|
||||
}) async {
|
||||
loggy.debug("connecting");
|
||||
await setPrefs(httpPort, socksPort, systemProxy);
|
||||
final hasNotificationPermission =
|
||||
await _notificationService.grantPermission();
|
||||
if (!hasNotificationPermission) {
|
||||
loggy.warning("notification permission denied");
|
||||
throw const ConnectivityFailure.unexpected();
|
||||
}
|
||||
await _methodChannel.invokeMethod<bool>("start");
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> disconnect() async {
|
||||
loggy.debug("disconnecting");
|
||||
await _methodChannel.invokeMethod<bool>("stop");
|
||||
}
|
||||
|
||||
Future<void> setPrefs(int port, int socksPort, bool systemProxy) async {
|
||||
loggy.debug(
|
||||
'setting connection prefs: httpPort: $port, socksPort: $socksPort, systemProxy: $systemProxy',
|
||||
);
|
||||
final result = await _methodChannel.invokeMethod<bool>(
|
||||
"set_prefs",
|
||||
{
|
||||
"port": port,
|
||||
"socks-port": socksPort,
|
||||
"system-proxy": systemProxy,
|
||||
},
|
||||
);
|
||||
if (!(result ?? false)) {
|
||||
loggy.error("failed to set connection prefs");
|
||||
// TODO: throw
|
||||
}
|
||||
}
|
||||
}
|
||||
44
lib/services/deep_link_service.dart
Normal file
44
lib/services/deep_link_service.dart
Normal file
@@ -0,0 +1,44 @@
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
import 'package:protocol_handler/protocol_handler.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'deep_link_service.g.dart';
|
||||
|
||||
typedef NewProfileLink = ({String? url, String? name});
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class DeepLinkService extends _$DeepLinkService
|
||||
with ProtocolListener, InfraLogger {
|
||||
@override
|
||||
Future<NewProfileLink?> build() async {
|
||||
for (final protocol in _protocols) {
|
||||
await protocolHandler.register(protocol);
|
||||
}
|
||||
protocolHandler.addListener(this);
|
||||
ref.onDispose(() {
|
||||
protocolHandler.removeListener(this);
|
||||
});
|
||||
|
||||
final initialPayload = await protocolHandler.getInitialUrl();
|
||||
if (initialPayload != null) {
|
||||
loggy.debug('initial payload: [$initialPayload]');
|
||||
final link = LinkParser.deep(initialPayload);
|
||||
return link;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static const _protocols = ['clash', 'clashmeta'];
|
||||
|
||||
@override
|
||||
void onProtocolUrlReceived(String url) {
|
||||
super.onProtocolUrlReceived(url);
|
||||
loggy.debug("url received: [$url]");
|
||||
final link = LinkParser.deep(url);
|
||||
if (link == null) {
|
||||
loggy.debug("link was not valid");
|
||||
return;
|
||||
}
|
||||
update((_) => link);
|
||||
}
|
||||
}
|
||||
67
lib/services/files_editor_service.dart
Normal file
67
lib/services/files_editor_service.dart
Normal file
@@ -0,0 +1,67 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hiddify/domain/constants.dart';
|
||||
import 'package:hiddify/gen/assets.gen.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
class FilesEditorService with InfraLogger {
|
||||
late final Directory _supportDir;
|
||||
late final Directory _clashDirectory;
|
||||
late final Directory _logsDirectory;
|
||||
|
||||
Future<void> init() async {
|
||||
loggy.debug('initializing');
|
||||
_supportDir = await getApplicationSupportDirectory();
|
||||
_clashDirectory =
|
||||
Directory(p.join(_supportDir.path, Constants.clashFolderName));
|
||||
loggy.debug('clash directory: $_clashDirectory');
|
||||
if (!await _clashDirectory.exists()) {
|
||||
await _clashDirectory.create(recursive: true);
|
||||
}
|
||||
if (!await File(countryMMDBPath).exists()) {
|
||||
await _populateDefaultCountryMMDB();
|
||||
}
|
||||
if (!await File(defaultConfigPath).exists()) await _populateDefaultConfig();
|
||||
}
|
||||
|
||||
String get clashDirPath => _clashDirectory.path;
|
||||
|
||||
late final logsPath = p.join(
|
||||
_logsDirectory.path,
|
||||
"${DateTime.now().toUtc().toIso8601String().split('T').first}.txt",
|
||||
);
|
||||
|
||||
String get defaultConfigPath => configPath("config");
|
||||
|
||||
String configPath(String fileName) {
|
||||
return p.join(_clashDirectory.path, "$fileName.yaml");
|
||||
}
|
||||
|
||||
Future<void> deleteConfig(String fileName) {
|
||||
return File(configPath(fileName)).delete();
|
||||
}
|
||||
|
||||
String get countryMMDBPath {
|
||||
return p.join(
|
||||
_clashDirectory.path,
|
||||
"${Constants.countryMMDBFileName}.mmdb",
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _populateDefaultConfig() async {
|
||||
loggy.debug('populating default config file');
|
||||
final defaultConfig = await rootBundle.load(Assets.core.clash.config);
|
||||
await File(defaultConfigPath)
|
||||
.writeAsBytes(defaultConfig.buffer.asInt8List());
|
||||
}
|
||||
|
||||
Future<void> _populateDefaultCountryMMDB() async {
|
||||
loggy.debug('populating default country mmdb file');
|
||||
final defaultCountryMMDB = await rootBundle.load(Assets.core.clash.country);
|
||||
await File(countryMMDBPath)
|
||||
.writeAsBytes(defaultCountryMMDB.buffer.asInt8List());
|
||||
}
|
||||
}
|
||||
9
lib/services/notification/constants.dart
Normal file
9
lib/services/notification/constants.dart
Normal file
@@ -0,0 +1,9 @@
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
|
||||
const mainChannel = AndroidNotificationChannel(
|
||||
"com.hiddify.hiddify",
|
||||
"Hiddify",
|
||||
importance: Importance.high,
|
||||
enableVibration: false,
|
||||
playSound: false,
|
||||
);
|
||||
105
lib/services/notification/local_notification_service.dart
Normal file
105
lib/services/notification/local_notification_service.dart
Normal file
@@ -0,0 +1,105 @@
|
||||
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<void> init() async {
|
||||
loggy.debug('initializing');
|
||||
const initializationSettings = InitializationSettings(
|
||||
android: AndroidInitializationSettings('mipmap/ic_launcher'),
|
||||
);
|
||||
|
||||
await _initDetails();
|
||||
await _initChannels();
|
||||
|
||||
await flutterLocalNotificationsPlugin.initialize(
|
||||
initializationSettings,
|
||||
onDidReceiveNotificationResponse: onDidReceiveNotificationResponse,
|
||||
onDidReceiveBackgroundNotificationResponse: notificationTapBackground,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _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<void> _initChannels() async {
|
||||
await flutterLocalNotificationsPlugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin>()
|
||||
?.createNotificationChannel(mainChannel);
|
||||
}
|
||||
|
||||
@override
|
||||
void onDidReceiveNotificationResponse(
|
||||
NotificationResponse notificationResponse,
|
||||
) {
|
||||
// TODO: complete
|
||||
loggy.debug('received notification response, $notificationResponse');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> 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<void> removeNotification(int id) async {
|
||||
loggy.debug('removing notification');
|
||||
await flutterLocalNotificationsPlugin.cancel(id);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> grantPermission() async {
|
||||
final result = await flutterLocalNotificationsPlugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin>()
|
||||
?.requestPermission();
|
||||
return result ?? false;
|
||||
}
|
||||
}
|
||||
1
lib/services/notification/notification.dart
Normal file
1
lib/services/notification/notification.dart
Normal file
@@ -0,0 +1 @@
|
||||
export 'notification_service.dart';
|
||||
30
lib/services/notification/notification_service.dart
Normal file
30
lib/services/notification/notification_service.dart
Normal file
@@ -0,0 +1,30 @@
|
||||
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() {
|
||||
if (Platform.isWindows) return StubNotificationService();
|
||||
return LocalNotificationService(FlutterLocalNotificationsPlugin());
|
||||
}
|
||||
|
||||
Future<void> init();
|
||||
|
||||
void onDidReceiveNotificationResponse(
|
||||
NotificationResponse notificationResponse,
|
||||
);
|
||||
|
||||
Future<bool> grantPermission();
|
||||
|
||||
Future<void> showNotification({
|
||||
required int id,
|
||||
required String title,
|
||||
String? body,
|
||||
NotificationDetails? details,
|
||||
String? payload,
|
||||
});
|
||||
|
||||
Future<void> removeNotification(int id);
|
||||
}
|
||||
31
lib/services/notification/stub_notification_service.dart
Normal file
31
lib/services/notification/stub_notification_service.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:hiddify/services/notification/notification_service.dart';
|
||||
|
||||
class StubNotificationService implements NotificationService {
|
||||
@override
|
||||
Future<void> init() async {
|
||||
return;
|
||||
}
|
||||
|
||||
@override
|
||||
void onDidReceiveNotificationResponse(
|
||||
NotificationResponse notificationResponse,
|
||||
) {}
|
||||
|
||||
@override
|
||||
Future<void> removeNotification(int id) async {}
|
||||
|
||||
@override
|
||||
Future<void> showNotification({
|
||||
required int id,
|
||||
required String title,
|
||||
String? body,
|
||||
NotificationDetails? details,
|
||||
String? payload,
|
||||
}) async {}
|
||||
|
||||
@override
|
||||
Future<bool> grantPermission() async {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
31
lib/services/service_providers.dart
Normal file
31
lib/services/service_providers.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
import 'package:hiddify/services/clash/clash.dart';
|
||||
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/window_manager_service.dart';
|
||||
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();
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
WindowManagerService windowManagerService(WindowManagerServiceRef ref) =>
|
||||
WindowManagerService();
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
ConnectivityService connectivityService(ConnectivityServiceRef ref) =>
|
||||
ConnectivityService(
|
||||
ref.watch(notificationServiceProvider),
|
||||
);
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
ClashService clashService(ClashServiceRef ref) => ClashServiceImpl(
|
||||
filesEditor: ref.read(filesEditorServiceProvider),
|
||||
);
|
||||
20
lib/services/window_manager_service.dart
Normal file
20
lib/services/window_manager_service.dart
Normal file
@@ -0,0 +1,20 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
// TODO: rewrite
|
||||
class WindowManagerService with WindowListener {
|
||||
Future<void> init() async {
|
||||
await windowManager.ensureInitialized();
|
||||
const windowOptions = WindowOptions(
|
||||
size: Size(868, 768),
|
||||
minimumSize: Size(868, 648),
|
||||
center: true,
|
||||
);
|
||||
await windowManager.waitUntilReadyToShow(windowOptions);
|
||||
windowManager.addListener(this);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
windowManager.removeListener(this);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user