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

+126 -16
View File
@@ -27,6 +27,7 @@ import 'package:gopeed/api/model/resource.dart';
import 'package:gopeed/api/model/store_extension.dart';
import 'package:gopeed/api/model/task.dart' as api_task;
import 'package:gopeed/core/common/start_config.dart';
import 'package:gopeed/core/common/api_server_state.dart';
import 'package:gopeed/core/icons/gopeed_icons.dart';
import 'package:gopeed/core/window/app_window_chrome.dart';
import 'package:gopeed/core/window/app_window_frame.dart';
@@ -1329,16 +1330,14 @@ void main() {
await tester.pumpAndSettle();
await tester.tap(find.text('Advanced'));
await tester.pumpAndSettle();
expect(find.text('Runtime status'), findsOneWidget);
expect(find.text('tcp://192.168.1.20:4321'), findsOneWidget);
expect(find.byKey(const ValueKey('api-server-status-dot')), findsOneWidget);
final host = find.byKey(const ValueKey('api-host-input'));
final port = find.byKey(const ValueKey('api-port-input'));
final saveButton = find.byKey(const ValueKey('save-api-config-button'));
expect(tester.widget<AppLoadingButton>(saveButton).variant, AppLoadingButtonVariant.secondary);
final saveButtonClickable = find.descendant(of: saveButton, matching: find.byType(shad.Clickable));
final disabledDecoration =
tester.widget<shad.Clickable>(saveButtonClickable).decoration!.resolve({WidgetState.disabled}) as BoxDecoration;
expect(disabledDecoration.color, AppPalette.dark.surfaceSoft);
expect(disabledDecoration.color, isNot(AppPalette.dark.brand));
expect(tester.widget<AppLoadingButton>(saveButton).variant, AppLoadingButtonVariant.primary);
expect(tester.widget<shad.TextField>(host).controller!.text, '192.168.1.20');
expect(tester.widget<shad.TextField>(port).controller!.text, '4321');
expect(tester.widget<AppLoadingButton>(saveButton).onPressed, isNull);
@@ -1359,6 +1358,10 @@ void main() {
await tester.enterText(host, '127.0.0.1');
await tester.tap(saveButton);
await tester.pumpAndSettle();
expect(find.byKey(const ValueKey('restart-api-server-dialog')), findsOneWidget);
expect(runtimeController.savedStartConfig, isNull);
await tester.tap(find.byKey(const ValueKey('confirm-restart-api-server-button')));
await tester.pumpAndSettle();
expect(runtimeController.savedStartConfig?.network, 'tcp');
expect(runtimeController.savedStartConfig?.address, '127.0.0.1:0');
expect(tester.widget<AppLoadingButton>(saveButton).onPressed, isNull);
@@ -1378,6 +1381,54 @@ void main() {
expect(tester.takeException(), isNull);
});
testWidgets('disabling the API server requires confirmation and keeps listener fields visible', (
WidgetTester tester,
) async {
await _setTestSize(tester, const Size(1024, 900));
final runtimeController = RecordingStartConfigRuntimeController();
await tester.pumpWidget(
ProviderScope(
overrides: [
appRuntimeControllerProvider.overrideWith(() => runtimeController),
appPlatformControllerProvider.overrideWith(FakePlatformController.new),
settingsControllerProvider.overrideWith(FakeSettingsController.new),
],
child: const _SettingsTestApp(),
),
);
await tester.pumpAndSettle();
await tester.tap(find.text('Advanced'));
await tester.pumpAndSettle();
final hostInput = find.byKey(const ValueKey('api-host-input'));
await tester.enterText(hostInput, 'unsaved.example.com');
final toggleButton = find.byKey(const ValueKey('toggle-api-server-button'));
await tester.ensureVisible(toggleButton);
await tester.pumpAndSettle();
await tester.tap(toggleButton);
await tester.pumpAndSettle();
expect(find.byKey(const ValueKey('disable-api-server-dialog')), findsOneWidget);
expect(find.byKey(const ValueKey('api-host-input')), findsOneWidget);
expect(find.byKey(const ValueKey('api-port-input')), findsOneWidget);
await tester.tap(find.byKey(const ValueKey('cancel-disable-api-server-button')));
await tester.pumpAndSettle();
expect(runtimeController.savedStartConfig, isNull);
await tester.tap(toggleButton);
await tester.pumpAndSettle();
await tester.tap(find.byKey(const ValueKey('confirm-disable-api-server-button')));
await tester.pumpAndSettle();
expect(runtimeController.savedStartConfig?.apiEnable, isFalse);
expect(runtimeController.savedStartConfig?.address, '192.168.1.20:4321');
expect(tester.widget<shad.TextField>(hostInput).controller!.text, 'unsaved.example.com');
final statusDot = tester.widget<AnimatedContainer>(find.byKey(const ValueKey('api-server-status-dot')));
expect((statusDot.decoration! as BoxDecoration).color, AppPalette.light.textMuted.withValues(alpha: 0.72));
expect(find.text('tcp://192.168.1.20:4321'), findsNothing);
await tester.pump(const Duration(seconds: 6));
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
});
testWidgets('shared toast appears at top center with content-driven width', (WidgetTester tester) async {
await _setTestSize(tester, const Size(1024, 700));
await tester.pumpWidget(
@@ -2995,10 +3046,9 @@ void main() {
expect(urlValue.semanticsLabel, task.url);
expect(find.text('Task Details'), findsOneWidget);
expect(find.text('Task Name'), findsNothing);
final detailHeader = tester.widget<DecoratedBox>(
find.descendant(of: find.byKey(const ValueKey('app-detail-drawer-header')), matching: find.byType(DecoratedBox)),
);
expect(((detailHeader.decoration as BoxDecoration).border! as Border).bottom.width, 1);
final detailHeader = tester.widget<SizedBox>(find.byKey(const ValueKey('app-detail-drawer-header')));
final detailHeaderDecoration = (detailHeader.child! as DecoratedBox).decoration as BoxDecoration;
expect((detailHeaderDecoration.border! as Border).bottom.width, 1);
expect(find.text('Details'), findsOneWidget);
expect(find.text('Files'), findsOneWidget);
expect(find.text('Statistics'), findsOneWidget);
@@ -4367,9 +4417,14 @@ class _SettingsTestAppState extends State<_SettingsTestApp> {
return shad.ShadcnApp.router(
theme: widget.dark ? AppTheme.dark() : AppTheme.light(),
materialTheme: widget.dark ? AppTheme.materialDark() : AppTheme.materialLight(),
builder: widget.wrapComponentThemes
? (context, child) => AppComponentThemes(child: child ?? const SizedBox.shrink())
: null,
builder: (context, child) {
final content = widget.wrapComponentThemes
? AppComponentThemes(child: child ?? const SizedBox.shrink())
: child ?? const SizedBox.shrink();
final mediaQuery = MediaQuery.maybeOf(context);
if (mediaQuery == null) return content;
return MediaQuery(data: mediaQuery.copyWith(disableAnimations: true), child: content);
},
routerConfig: _router,
);
}
@@ -4534,11 +4589,16 @@ class FakeRuntimeController extends AppRuntimeController {
final cfg = StartConfig()
..network = 'tcp'
..address = '127.0.0.1:9999'
..apiEnable = true
..apiToken = ''
..storage = 'bolt'
..storageDir = ''
..refreshInterval = 0;
return AppRuntimeState(startConfig: cfg, runningPort: 9999, downloaderConfig: DownloaderConfig());
return AppRuntimeState(
startConfig: cfg,
apiServerState: _testApiServerState(running: true, port: 9999),
downloaderConfig: DownloaderConfig(),
);
}
}
@@ -4566,25 +4626,75 @@ class RecordingStartConfigRuntimeController extends AppRuntimeController {
final config = StartConfig()
..network = 'tcp'
..address = '192.168.1.20:4321'
..apiEnable = true
..apiToken = 'token'
..storage = 'bolt'
..storageDir = ''
..refreshInterval = 0;
return AppRuntimeState(startConfig: config, runningPort: 4321, downloaderConfig: DownloaderConfig());
return AppRuntimeState(
startConfig: config,
apiServerState: _testApiServerState(running: true, port: 4321, address: config.address),
downloaderConfig: DownloaderConfig(),
);
}
@override
Future<void> saveStartConfig(StartConfig config) async {
Future<void> saveApiServerConfig(StartConfig config) async =>
_record(config, running: state.requireValue.apiServerState.running);
@override
Future<void> startApiServer() async => _record(state.requireValue.startConfig..apiEnable = true, running: true);
@override
Future<void> stopApiServer() async => _record(state.requireValue.startConfig..apiEnable = false, running: false);
@override
Future<void> restartApiServer(StartConfig config) async => _record(config..apiEnable = true, running: true);
void _record(StartConfig config, {required bool running}) {
savedStartConfig = StartConfig()
..network = config.network
..address = config.address
..apiEnable = config.apiEnable
..apiToken = config.apiToken
..storage = config.storage
..storageDir = config.storageDir
..refreshInterval = config.refreshInterval;
final current = state.requireValue;
state = AsyncValue.data(
AppRuntimeState(
startConfig: savedStartConfig!,
apiServerState: _testApiServerState(
running: running,
port: running ? 54321 : 0,
address: config.address,
enabled: config.apiEnable,
),
downloaderConfig: current.downloaderConfig,
localBackendStarted: current.localBackendStarted,
startupError: current.startupError,
),
);
}
}
ApiServerState _testApiServerState({
required bool running,
required int port,
String address = '127.0.0.1:9999',
bool? enabled,
}) {
return ApiServerState(
enabled: enabled ?? running,
running: running,
network: running ? 'tcp' : '',
address: running ? address : '',
runningPort: port,
pendingApply: false,
lastError: '',
);
}
class FakeSettingsController extends SettingsController {
@override
Future<SettingsState> build() async => SettingsState(config: DownloaderConfig());