feat: move native communication to FFI (#1457)

This commit is contained in:
Levi authored and GitHub committed 2026-09-05 20:18:42 +08:00
1 parent 177c225f16
commit 39cd9034cd
86 files changed
+4558 -886

No files matched your search

@@ -12,7 +12,6 @@ import '../../api/model/switch_extension.dart';
import '../../api/model/task.dart';
import '../../api/model/update_check_extension_resp.dart';
import '../../api/model/update_extension_settings.dart';
import '../../database/database.dart';
import '../window/app_window_appearance.dart';
import 'capability_rpc.dart';
import 'gopeed_capability.dart';
@@ -147,19 +146,33 @@ void _bindGopeed(CapabilityRegistry registry) {
void _bindStorage(CapabilityRegistry registry) {
registry
..bind(StorageMethods.getCreateHistory, (_) async => Database.instance.getCreateHistory() ?? const <String>[])
..bind(StorageMethods.getCreateHistory, (_) async {
final config = await api.getConfig();
return config.extra.createHistory;
})
..bind(StorageMethods.saveCreateHistory, (urls) async {
final config = await api.getConfig();
final history = List<String>.of(config.extra.createHistory);
for (final url in urls) {
Database.instance.saveCreateHistory(url);
history.remove(url);
history.insert(0, url);
if (history.length > 64) history.removeLast();
}
config.extra.createHistory = history;
await api.putConfig(config);
return const RpcUnit();
})
..bind(StorageMethods.removeCreateHistory, (url) async {
Database.instance.removeCreateHistory(url);
final config = await api.getConfig();
final history = List<String>.of(config.extra.createHistory)..remove(url);
config.extra.createHistory = history;
await api.putConfig(config);
return const RpcUnit();
})
..bind(StorageMethods.clearCreateHistory, (_) async {
Database.instance.clearCreateHistory();
final config = await api.getConfig();
config.extra.createHistory = const [];
await api.putConfig(config);
return const RpcUnit();
});
}
@@ -0,0 +1,64 @@
class ApiServerState {
const ApiServerState({
required this.enabled,
required this.running,
required this.network,
required this.address,
required this.runningPort,
required this.pendingApply,
required this.lastError,
});
final bool enabled;
final bool running;
final String network;
final String address;
final int runningPort;
final bool pendingApply;
final String lastError;
factory ApiServerState.fromJson(Map<String, dynamic> json) {
return ApiServerState(
enabled: json['enabled'] as bool? ?? false,
running: json['running'] as bool? ?? false,
network: json['network'] as String? ?? '',
address: json['address'] as String? ?? '',
runningPort: json['runningPort'] as int? ?? 0,
pendingApply: json['pendingApply'] as bool? ?? false,
lastError: json['lastError'] as String? ?? '',
);
}
String runningAddress() {
if (!running) return '';
if (network == 'unix') return address;
final separator = address.lastIndexOf(':');
final host = separator < 0 ? address : address.substring(0, separator);
return '$host:$runningPort';
}
}
class ApiServerOperationResult {
const ApiServerOperationResult({required this.state, required this.error});
final ApiServerState state;
final String error;
factory ApiServerOperationResult.fromJson(Map<String, dynamic> json) {
final rawState = json['state'] as Map<String, dynamic>?;
return ApiServerOperationResult(
state: rawState == null
? const ApiServerState(
enabled: false,
running: false,
network: '',
address: '',
runningPort: 0,
pendingApply: true,
lastError: '',
)
: ApiServerState.fromJson(rawState),
error: json['error'] as String? ?? '',
);
}
}
@@ -1,12 +1,24 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/services.dart';
import 'api_server_state.dart';
import 'libgopeed_interface.dart';
import 'start_config.dart';
import 'task_event.dart';
class LibgopeedChannel implements LibgopeedInterface {
static const _channel = MethodChannel('gopeed.com/libgopeed');
final _taskEvents = StreamController<TaskEvent>.broadcast();
LibgopeedChannel() {
_channel.setMethodCallHandler((call) async {
if (call.method != 'taskEvent') return;
final payload = jsonDecode(call.arguments as String) as Map<String, dynamic>;
_taskEvents.add(TaskEvent.fromJson(payload));
});
}
@override
Future<int> start(StartConfig cfg) async {
@@ -18,4 +30,40 @@ class LibgopeedChannel implements LibgopeedInterface {
Future<void> stop() async {
return await _channel.invokeMethod('stop');
}
Future<ApiServerOperationResult> _apiServerOperation(String method) async {
final payload = await _channel.invokeMethod<String>(method) ?? '';
return ApiServerOperationResult.fromJson(jsonDecode(payload) as Map<String, dynamic>);
}
@override
Future<ApiServerOperationResult> getApiServerState() => _apiServerOperation('getApiServerState');
@override
Future<ApiServerOperationResult> startApiServer() => _apiServerOperation('startApiServer');
@override
Future<ApiServerOperationResult> stopApiServer() => _apiServerOperation('stopApiServer');
@override
Future<ApiServerOperationResult> restartApiServer() => _apiServerOperation('restartApiServer');
@override
Future<String> invoke(String method, String path, {String query = '', String body = ''}) async {
final result = await _channel.invokeMethod<String>('invoke', {
'method': method,
'path': path,
'query': query,
'body': body,
});
return result ?? '';
}
@override
Stream<TaskEvent> get taskEvents => _taskEvents.stream;
@override
Future<void> subscribeTaskEvents(Set<TaskEventType> events) {
return _channel.invokeMethod<void>('subscribeTaskEvents', {'mask': events.mask});
}
}
+77 -5
View File
@@ -5,24 +5,47 @@ import 'dart:ffi';
import 'package:ffi/ffi.dart';
import '../ffi/libgopeed_bind.dart';
import '../ffi/libgopeed_worker.dart';
import 'api_server_state.dart';
import 'libgopeed_interface.dart';
import 'start_config.dart';
import 'task_event.dart';
class LibgopeedFFi implements LibgopeedInterface {
late LibgopeedBind _libgopeed;
final _taskEvents = StreamController<TaskEvent>.broadcast();
final _worker = LibgopeedWorker();
late final NativeCallable<Void Function(Pointer<Char>)> _taskEventCallback;
LibgopeedFFi(LibgopeedBind libgopeed) {
_libgopeed = libgopeed;
_taskEventCallback = NativeCallable<Void Function(Pointer<Char>)>.listener(_onTaskEvent);
}
void _onTaskEvent(Pointer<Char> payload) {
try {
final decoded = jsonDecode(payload.cast<Utf8>().toDartString()) as Map<String, dynamic>;
_taskEvents.add(TaskEvent.fromJson(decoded));
} finally {
_libgopeed.FreeCString(payload);
}
}
@override
Future<int> start(StartConfig cfg) {
var completer = Completer<int>();
var result = _libgopeed.Start(jsonEncode(cfg).toNativeUtf8().cast());
if (result.r1 != nullptr) {
completer.completeError(Exception(result.r1.cast<Utf8>().toDartString()));
} else {
completer.complete(result.r0);
final cfgPtr = jsonEncode(cfg).toNativeUtf8();
try {
final result = _libgopeed.Start(cfgPtr.cast());
if (result.r1 != nullptr) {
final message = result.r1.cast<Utf8>().toDartString();
_libgopeed.FreeCString(result.r1);
completer.completeError(Exception(message));
} else {
completer.complete(result.r0);
}
} finally {
malloc.free(cfgPtr);
}
return completer.future;
}
@@ -34,4 +57,53 @@ class LibgopeedFFi implements LibgopeedInterface {
completer.complete();
return completer.future;
}
Future<ApiServerOperationResult> _apiServerOperation(String operation) async {
final payload = await _worker.apiServer(operation);
return ApiServerOperationResult.fromJson(jsonDecode(payload) as Map<String, dynamic>);
}
@override
Future<ApiServerOperationResult> getApiServerState() => _apiServerOperation('get');
@override
Future<ApiServerOperationResult> startApiServer() => _apiServerOperation('start');
@override
Future<ApiServerOperationResult> stopApiServer() => _apiServerOperation('stop');
@override
Future<ApiServerOperationResult> restartApiServer() => _apiServerOperation('restart');
@override
Future<String> invoke(String method, String path, {String query = '', String body = ''}) async {
final methodPtr = method.toNativeUtf8();
final pathPtr = path.toNativeUtf8();
final queryPtr = query.toNativeUtf8();
final bodyPtr = body.toNativeUtf8();
Pointer<Char>? resultPtr;
try {
resultPtr = _libgopeed.Invoke(methodPtr.cast(), pathPtr.cast(), queryPtr.cast(), bodyPtr.cast());
if (resultPtr == nullptr) {
throw StateError('Gopeed Invoke returned a null response');
}
return resultPtr.cast<Utf8>().toDartString();
} finally {
malloc.free(methodPtr);
malloc.free(pathPtr);
malloc.free(queryPtr);
malloc.free(bodyPtr);
if (resultPtr != null && resultPtr != nullptr) {
_libgopeed.FreeCString(resultPtr);
}
}
}
@override
Stream<TaskEvent> get taskEvents => _taskEvents.stream;
@override
Future<void> subscribeTaskEvents(Set<TaskEventType> events) async {
_libgopeed.SubscribeTaskEvents(events.mask, events.isEmpty ? 0 : _taskEventCallback.nativeFunction.address);
}
}
@@ -1,7 +1,23 @@
import 'api_server_state.dart';
import 'start_config.dart';
import 'task_event.dart';
abstract class LibgopeedInterface {
Future<int> start(StartConfig cfg);
Future<void> stop();
Future<ApiServerOperationResult> getApiServerState();
Future<ApiServerOperationResult> startApiServer();
Future<ApiServerOperationResult> stopApiServer();
Future<ApiServerOperationResult> restartApiServer();
Future<String> invoke(String method, String path, {String query = '', String body = ''});
Stream<TaskEvent> get taskEvents;
Future<void> subscribeTaskEvents(Set<TaskEventType> events);
}
@@ -19,6 +19,7 @@ class WebViewRpcConfig {
class StartConfig {
late String network;
late String address;
bool apiEnable = false;
late String storage;
late String storageDir;
late int refreshInterval;
@@ -22,6 +22,7 @@ Map<String, dynamic> _$WebViewRpcConfigToJson(WebViewRpcConfig instance) =>
StartConfig _$StartConfigFromJson(Map<String, dynamic> json) => StartConfig()
..network = json['network'] as String
..address = json['address'] as String
..apiEnable = json['apiEnable'] as bool
..storage = json['storage'] as String
..storageDir = json['storageDir'] as String
..refreshInterval = (json['refreshInterval'] as num).toInt()
@@ -36,6 +37,7 @@ Map<String, dynamic> _$StartConfigToJson(StartConfig instance) =>
<String, dynamic>{
'network': instance.network,
'address': instance.address,
'apiEnable': instance.apiEnable,
'storage': instance.storage,
'storageDir': instance.storageDir,
'refreshInterval': instance.refreshInterval,
@@ -0,0 +1,38 @@
enum TaskEventType {
done(1, 'task.done'),
error(2, 'task.error');
const TaskEventType(this.mask, this.wireName);
final int mask;
final String wireName;
static TaskEventType fromWireName(String value) {
return values.firstWhere(
(type) => type.wireName == value,
orElse: () => throw FormatException('Unknown task event type: $value'),
);
}
}
extension TaskEventTypeSet on Set<TaskEventType> {
int get mask => fold(0, (value, type) => value | type.mask);
}
class TaskEvent {
const TaskEvent({required this.type, required this.taskId, required this.name, this.error});
final TaskEventType type;
final String taskId;
final String name;
final String? error;
factory TaskEvent.fromJson(Map<String, dynamic> json) {
return TaskEvent(
type: TaskEventType.fromWireName(json['type'] as String),
taskId: json['taskId'] as String,
name: json['name'] as String? ?? '',
error: json['error'] as String?,
);
}
}
@@ -1,20 +1,9 @@
import '../../database/database.dart';
import '../../util/util.dart';
class AppInitializer {
static Future<void>? _storageInitialization;
static Future<void>? _databaseInitialization;
static Future<void> ensureStorageInitialized() {
return _storageInitialization ??= Util.initStorageDir();
}
static Future<void> ensureDatabaseInitialized() {
return _databaseInitialization ??= _initializeDatabase();
}
static Future<void> _initializeDatabase() async {
await ensureStorageInitialized();
await Database.instance.init();
}
}
@@ -1,6 +1,8 @@
import 'dart:async';
import '../common/api_server_state.dart';
import '../common/start_config.dart';
import '../common/task_event.dart';
import '../libgopeed_boot.dart';
@@ -13,4 +15,40 @@ class LibgopeedBootBrowser implements LibgopeedBoot {
@override
Future<void> stop() async {}
@override
Future<ApiServerOperationResult> getApiServerState() async => _unsupportedApiServerResult;
@override
Future<ApiServerOperationResult> startApiServer() async => _unsupportedApiServerResult;
@override
Future<ApiServerOperationResult> stopApiServer() async => _unsupportedApiServerResult;
@override
Future<ApiServerOperationResult> restartApiServer() async => _unsupportedApiServerResult;
@override
Future<String> invoke(String method, String path, {String query = '', String body = ''}) {
throw UnsupportedError('Native Gopeed invoke is unavailable on web');
}
@override
Stream<TaskEvent> get taskEvents => const Stream.empty();
@override
Future<void> subscribeTaskEvents(Set<TaskEventType> events) async {}
}
const _unsupportedApiServerResult = ApiServerOperationResult(
state: ApiServerState(
enabled: false,
running: false,
network: '',
address: '',
runningPort: 0,
pendingApply: false,
lastError: '',
),
error: '',
);
@@ -6,7 +6,9 @@ import '../../util/util.dart';
import '../common/libgopeed_channel.dart';
import '../common/libgopeed_ffi.dart';
import '../common/libgopeed_interface.dart';
import '../common/api_server_state.dart';
import '../common/start_config.dart';
import '../common/task_event.dart';
import '../ffi/libgopeed_bind.dart';
import '../libgopeed_boot.dart';
@@ -43,6 +45,32 @@ class LibgopeedBootNative implements LibgopeedBoot {
@override
Future<void> stop() async {
await _libgopeed.subscribeTaskEvents({});
await _libgopeed.stop();
}
@override
Future<ApiServerOperationResult> getApiServerState() => _libgopeed.getApiServerState();
@override
Future<ApiServerOperationResult> startApiServer() => _libgopeed.startApiServer();
@override
Future<ApiServerOperationResult> stopApiServer() => _libgopeed.stopApiServer();
@override
Future<ApiServerOperationResult> restartApiServer() => _libgopeed.restartApiServer();
@override
Future<String> invoke(String method, String path, {String query = '', String body = ''}) {
return _libgopeed.invoke(method, path, query: query, body: body);
}
@override
Stream<TaskEvent> get taskEvents => _libgopeed.taskEvents;
@override
Future<void> subscribeTaskEvents(Set<TaskEventType> events) {
return _libgopeed.subscribeTaskEvents(events);
}
}
@@ -29,6 +29,72 @@ class LibgopeedBind {
late final _StopPtr = _lookup<ffi.NativeFunction<ffi.Void Function()>>('Stop');
late final _Stop = _StopPtr.asFunction<void Function()>();
ffi.Pointer<ffi.Char> GetAPIServerState() => _GetAPIServerState();
late final _GetAPIServerStatePtr = _lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>('GetAPIServerState');
late final _GetAPIServerState = _GetAPIServerStatePtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
ffi.Pointer<ffi.Char> StartAPIServer() => _StartAPIServer();
late final _StartAPIServerPtr = _lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>('StartAPIServer');
late final _StartAPIServer = _StartAPIServerPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
ffi.Pointer<ffi.Char> StopAPIServer() => _StopAPIServer();
late final _StopAPIServerPtr = _lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>('StopAPIServer');
late final _StopAPIServer = _StopAPIServerPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
ffi.Pointer<ffi.Char> RestartAPIServer() => _RestartAPIServer();
late final _RestartAPIServerPtr = _lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>('RestartAPIServer');
late final _RestartAPIServer = _RestartAPIServerPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
ffi.Pointer<ffi.Char> Invoke(
ffi.Pointer<ffi.Char> method,
ffi.Pointer<ffi.Char> path,
ffi.Pointer<ffi.Char> query,
ffi.Pointer<ffi.Char> body,
) {
return _Invoke(method, path, query, body);
}
late final _InvokePtr =
_lookup<
ffi.NativeFunction<
ffi.Pointer<ffi.Char> Function(
ffi.Pointer<ffi.Char>,
ffi.Pointer<ffi.Char>,
ffi.Pointer<ffi.Char>,
ffi.Pointer<ffi.Char>,
)
>
>('Invoke');
late final _Invoke =
_InvokePtr.asFunction<
ffi.Pointer<ffi.Char> Function(
ffi.Pointer<ffi.Char>,
ffi.Pointer<ffi.Char>,
ffi.Pointer<ffi.Char>,
ffi.Pointer<ffi.Char>,
)
>();
void SubscribeTaskEvents(int mask, int callback) {
return _SubscribeTaskEvents(mask, callback);
}
late final _SubscribeTaskEventsPtr = _lookup<ffi.NativeFunction<ffi.Void Function(ffi.Uint64, ffi.UintPtr)>>(
'SubscribeTaskEvents',
);
late final _SubscribeTaskEvents = _SubscribeTaskEventsPtr.asFunction<void Function(int, int)>();
void FreeCString(ffi.Pointer<ffi.Char> value) {
return _FreeCString(value);
}
late final _FreeCStringPtr = _lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>('FreeCString');
late final _FreeCString = _FreeCStringPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
}
final class Start_return extends ffi.Struct {
@@ -0,0 +1,144 @@
import 'dart:async';
import 'dart:ffi';
import 'dart:io';
import 'dart:isolate';
import 'package:ffi/ffi.dart';
import 'libgopeed_bind.dart';
class LibgopeedWorker {
LibgopeedWorker() {
_receivePort.listen(_handleWorkerMessage);
_errorPort.listen(_handleWorkerError);
_isolateReady = Isolate.spawn(_workerMain, _receivePort.sendPort, onError: _errorPort.sendPort);
}
final ReceivePort _receivePort = ReceivePort();
final ReceivePort _errorPort = ReceivePort();
final Map<int, Completer<String>> _pending = {};
late final Future<Isolate> _isolateReady;
final Completer<SendPort> _sendPort = Completer<SendPort>();
int _requestID = 0;
Future<String> invoke(String method, String path, String query, String body) async {
await _isolateReady;
final sendPort = await _sendPort.future;
final requestID = _requestID++;
final completer = Completer<String>();
_pending[requestID] = completer;
sendPort.send(['invoke', requestID, method, path, query, body]);
return completer.future;
}
Future<String> apiServer(String operation) async {
await _isolateReady;
final sendPort = await _sendPort.future;
final requestID = _requestID++;
final completer = Completer<String>();
_pending[requestID] = completer;
sendPort.send(['apiServer', requestID, operation]);
return completer.future;
}
void _handleWorkerMessage(dynamic message) {
if (message is SendPort) {
if (!_sendPort.isCompleted) _sendPort.complete(message);
return;
}
final response = message as List<dynamic>;
final requestID = response[0] as int;
final completer = _pending.remove(requestID);
if (completer == null) return;
if (response[1] as bool) {
completer.complete(response[2] as String);
} else {
completer.completeError(StateError(response[2] as String));
}
}
void _handleWorkerError(dynamic message) {
final parts = message is List<dynamic> ? message : <dynamic>[message];
final error = StateError(parts.map((part) => part.toString()).join('\n'));
if (!_sendPort.isCompleted) _sendPort.completeError(error);
for (final completer in _pending.values) {
completer.completeError(error);
}
_pending.clear();
}
static void _workerMain(SendPort mainSendPort) {
final requests = ReceivePort();
final bindings = LibgopeedBind(DynamicLibrary.open(_libraryName()));
mainSendPort.send(requests.sendPort);
requests.listen((dynamic message) {
final request = message as List<dynamic>;
final type = request[0] as String;
final requestID = request[1] as int;
try {
final response = switch (type) {
'invoke' => _invoke(
bindings,
request[2] as String,
request[3] as String,
request[4] as String,
request[5] as String,
),
'apiServer' => _apiServer(bindings, request[2] as String),
_ => throw StateError('Unknown libgopeed worker request: $type'),
};
mainSendPort.send([requestID, true, response]);
} catch (error) {
mainSendPort.send([requestID, false, error.toString()]);
}
});
}
static String _apiServer(LibgopeedBind bindings, String operation) {
final resultPtr = switch (operation) {
'get' => bindings.GetAPIServerState(),
'start' => bindings.StartAPIServer(),
'stop' => bindings.StopAPIServer(),
'restart' => bindings.RestartAPIServer(),
_ => throw StateError('Unknown API server operation: $operation'),
};
if (resultPtr == nullptr) {
throw StateError('Gopeed API server operation returned a null response');
}
try {
return resultPtr.cast<Utf8>().toDartString();
} finally {
bindings.FreeCString(resultPtr);
}
}
static String _invoke(LibgopeedBind bindings, String method, String path, String query, String body) {
final methodPtr = method.toNativeUtf8();
final pathPtr = path.toNativeUtf8();
final queryPtr = query.toNativeUtf8();
final bodyPtr = body.toNativeUtf8();
Pointer<Char>? resultPtr;
try {
resultPtr = bindings.Invoke(methodPtr.cast(), pathPtr.cast(), queryPtr.cast(), bodyPtr.cast());
if (resultPtr == nullptr) {
throw StateError('Gopeed Invoke returned a null response');
}
return resultPtr.cast<Utf8>().toDartString();
} finally {
malloc.free(methodPtr);
malloc.free(pathPtr);
malloc.free(queryPtr);
malloc.free(bodyPtr);
if (resultPtr != null && resultPtr != nullptr) {
bindings.FreeCString(resultPtr);
}
}
}
static String _libraryName() {
if (Platform.isWindows) return 'libgopeed.dll';
if (Platform.isMacOS) return 'libgopeed.dylib';
if (Platform.isLinux) return 'libgopeed.so';
throw UnsupportedError('Desktop FFI is unavailable on ${Platform.operatingSystem}');
}
}
+16
View File
@@ -1,4 +1,6 @@
import 'common/api_server_state.dart';
import 'common/start_config.dart';
import 'common/task_event.dart';
import "libgopeed_boot_stub.dart"
if (dart.library.html) 'entry/libgopeed_boot_browser.dart'
if (dart.library.io) 'entry/libgopeed_boot_native.dart';
@@ -16,4 +18,18 @@ abstract class LibgopeedBoot {
Future<int> start(StartConfig cfg);
Future<void> stop();
Future<ApiServerOperationResult> getApiServerState();
Future<ApiServerOperationResult> startApiServer();
Future<ApiServerOperationResult> stopApiServer();
Future<ApiServerOperationResult> restartApiServer();
Future<String> invoke(String method, String path, {String query = '', String body = ''});
Stream<TaskEvent> get taskEvents;
Future<void> subscribeTaskEvents(Set<TaskEventType> events);
}
@@ -0,0 +1,93 @@
import 'dart:convert';
import 'package:dio/dio.dart';
import '../../../libgopeed_boot.dart';
import '../../../ffi/libgopeed_worker.dart';
import '../../../../util/util.dart';
import '../gopeed_transport.dart';
GopeedTransport createTransport(GopeedTransportConfig config) => NativeGopeedTransport();
class NativeGopeedTransport implements GopeedTransport {
NativeGopeedTransport()
: _externalDio = Dio(
BaseOptions(
contentType: Headers.jsonContentType,
sendTimeout: const Duration(seconds: 5),
connectTimeout: const Duration(seconds: 5),
receiveTimeout: const Duration(seconds: 120),
),
);
final Dio _externalDio;
final LibgopeedWorker? _ffiWorker = Util.isDesktop() ? LibgopeedWorker() : null;
@override
Future<dynamic> request(
String path, {
String method = 'GET',
dynamic data,
Map<String, dynamic>? queryParameters,
}) async {
final request = _RequestParts.from(path, queryParameters);
final requestBody = data == null ? '' : jsonEncode(data);
final payload = _ffiWorker != null
? await _ffiWorker.invoke(method.toUpperCase(), request.path, request.query, requestBody)
: await LibgopeedBoot.instance.invoke(
method.toUpperCase(),
request.path,
query: request.query,
body: requestBody,
);
return jsonDecode(payload);
}
@override
Future<Response<String>> proxyRequest(String uri, {dynamic data, Options? options}) {
return _externalDio.request<String>(
uri,
data: data,
options: (options ?? Options()).copyWith(responseType: ResponseType.plain),
);
}
@override
String join(String path) => path;
}
class _RequestParts {
const _RequestParts(this.path, this.query);
final String path;
final String query;
factory _RequestParts.from(String rawPath, Map<String, dynamic>? queryParameters) {
var path = rawPath;
var query = '';
final queryIndex = rawPath.indexOf('?');
if (queryIndex >= 0) {
path = rawPath.substring(0, queryIndex);
query = rawPath.substring(queryIndex + 1);
}
final extraQuery = _encodeQueryParameters(queryParameters);
if (extraQuery.isNotEmpty) {
query = query.isEmpty ? extraQuery : '$query&$extraQuery';
}
return _RequestParts(path, query);
}
static String _encodeQueryParameters(Map<String, dynamic>? queryParameters) {
if (queryParameters == null || queryParameters.isEmpty) return '';
final segments = <String>[];
queryParameters.forEach((key, value) {
if (value == null) return;
final values = value is Iterable ? value : [value];
for (final item in values) {
if (item == null) continue;
segments.add('${Uri.encodeQueryComponent(key)}=${Uri.encodeQueryComponent(item.toString())}');
}
});
return segments.join('&');
}
}
@@ -0,0 +1,5 @@
import '../gopeed_transport.dart';
GopeedTransport createTransport(GopeedTransportConfig config) {
throw UnsupportedError('Gopeed transport is unavailable on this platform');
}
@@ -0,0 +1,73 @@
import 'package:dio/dio.dart';
import '../gopeed_transport.dart';
GopeedTransport createTransport(GopeedTransportConfig config) => RestGopeedTransport(config);
class RestGopeedTransport implements GopeedTransport {
RestGopeedTransport(GopeedTransportConfig config)
: _dio = Dio(
BaseOptions(
baseUrl: '${Uri.base.origin}/',
contentType: Headers.jsonContentType,
sendTimeout: const Duration(seconds: 5),
connectTimeout: const Duration(seconds: 5),
receiveTimeout: const Duration(seconds: 120),
),
) {
_dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
if (config.apiToken.isNotEmpty) {
options.headers['X-Api-Token'] = config.apiToken;
}
handler.next(options);
},
onError: (error, handler) {
if (error.response?.statusCode == 401) {
config.onUnauthorized?.call();
}
handler.next(error);
},
),
);
}
final Dio _dio;
@override
Future<dynamic> request(
String path, {
String method = 'GET',
dynamic data,
Map<String, dynamic>? queryParameters,
}) async {
final response = await _dio.request<dynamic>(
path,
data: data,
queryParameters: queryParameters,
options: Options(method: method),
);
return response.data;
}
@override
Future<Response<String>> proxyRequest(String uri, {dynamic data, Options? options}) {
options ??= Options();
options.headers ??= {};
options.headers!['X-Target-Uri'] = uri;
return _dio.request<String>(
'/api/web/proxy?t=${DateTime.now().millisecondsSinceEpoch}',
data: data,
options: options.copyWith(responseType: ResponseType.plain),
);
}
@override
String join(String path) {
final baseUrl = _dio.options.baseUrl;
final cleanBaseUrl = baseUrl.endsWith('/') ? baseUrl.substring(0, baseUrl.length - 1) : baseUrl;
final cleanPath = path.startsWith('/') ? path.substring(1) : path;
return '$cleanBaseUrl/$cleanPath';
}
}
@@ -0,0 +1,29 @@
import 'package:dio/dio.dart';
import 'entry/gopeed_transport_stub.dart'
if (dart.library.html) 'entry/gopeed_transport_web.dart'
if (dart.library.io) 'entry/gopeed_transport_native.dart';
class GopeedTransportConfig {
const GopeedTransportConfig({
required this.network,
required this.address,
required this.apiToken,
this.onUnauthorized,
});
final String network;
final String address;
final String apiToken;
final void Function()? onUnauthorized;
}
abstract interface class GopeedTransport {
Future<dynamic> request(String path, {String method = 'GET', dynamic data, Map<String, dynamic>? queryParameters});
Future<Response<String>> proxyRequest(String uri, {dynamic data, Options? options});
String join(String path);
}
GopeedTransport createGopeedTransport(GopeedTransportConfig config) => createTransport(config);
@@ -3,7 +3,6 @@ import 'package:flutter/material.dart' show Colors;
import 'package:flutter/widgets.dart';
import 'package:window_manager/window_manager.dart';
import '../../database/database.dart';
import '../../l10n/l10n.dart';
import 'app_window_chrome.dart';
import 'app_window_payload.dart';
@@ -16,9 +15,8 @@ class AppWindowBootstrap {
await windowManager.ensureInitialized();
final windowState = Database.instance.getWindowState();
final windowOptions = WindowOptions(
size: Size(windowState?.width ?? 1024, windowState?.height ?? 768),
size: const Size(1024, 768),
center: true,
skipTaskbar: false,
backgroundColor: Colors.transparent,