diff --git a/ui/flutter/lib/app/application/app_platform_controller.dart b/ui/flutter/lib/app/application/app_platform_controller.dart index 53e542d1..1d94d134 100644 --- a/ui/flutter/lib/app/application/app_platform_controller.dart +++ b/ui/flutter/lib/app/application/app_platform_controller.dart @@ -54,6 +54,9 @@ class AppPlatformState { enum AppUpdateStatus { idle, checking, upToDate, available, failed } class AppPlatformController extends AsyncNotifier with WindowListener, TrayListener { + @protected + bool get isDesktop => Util.isDesktop(); + bool _started = false; String? _trayLocaleConfig; bool _updateInitialized = false; @@ -74,30 +77,54 @@ class AppPlatformController extends AsyncNotifier with WindowL @override Future build() async { - final runtime = ref.watch(appRuntimeControllerProvider).value; - if (runtime == null || kIsWeb) { - return _snapshot(); - } - if (!Util.isDesktop()) { - _scheduleInitialUpdateCheck(); - return _snapshot(); - } - if (_started) { - final localeConfig = runtime.downloaderConfig.extra.locale; - if (_trayLocaleConfig != localeConfig) await _initTray(); - return _snapshot(); - } - await _start(); - _scheduleInitialUpdateCheck(); + ref.listen(appRuntimeControllerProvider, (_, next) { + unawaited( + _handleRuntimeChange(next.value).catchError((Object error, StackTrace stackTrace) { + if (ref.mounted) state = AsyncValue.error(error, stackTrace); + }), + ); + }); ref.onDispose(() { + if (!_started) return; + _started = false; windowManager.removeListener(this); trayManager.removeListener(this); unawaited(HostRpcService.instance.stop()); unawaited(AppWindowCapabilityHost.instance.stop()); }); + final runtime = ref.read(appRuntimeControllerProvider).value; + if (runtime == null || kIsWeb) { + return _snapshot(); + } + if (!isDesktop) { + _scheduleInitialUpdateCheck(); + return _snapshot(); + } + if (_started) { + return _snapshot(); + } + await _start(); + _scheduleInitialUpdateCheck(); return _snapshot(); } + Future _handleRuntimeChange(AppRuntimeState? runtime) async { + if (runtime == null || kIsWeb) return; + if (!isDesktop) { + _scheduleInitialUpdateCheck(); + return; + } + if (!_started) { + await _start(); + _scheduleInitialUpdateCheck(); + _emit(); + return; + } + if (_trayLocaleConfig != runtime.downloaderConfig.extra.locale) { + await _initTray(); + } + } + void _scheduleInitialUpdateCheck() { if (_updateInitialized) return; _updateInitialized = true; diff --git a/ui/flutter/test/app/application/app_platform_controller_test.dart b/ui/flutter/test/app/application/app_platform_controller_test.dart new file mode 100644 index 00000000..a5795ebb --- /dev/null +++ b/ui/flutter/test/app/application/app_platform_controller_test.dart @@ -0,0 +1,88 @@ +import 'dart:async'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:gopeed/api/model/downloader_config.dart'; +import 'package:gopeed/app/application/app_platform_controller.dart'; +import 'package:gopeed/app/application/app_runtime_controller.dart'; +import 'package:gopeed/core/common/api_server_state.dart'; +import 'package:gopeed/core/common/start_config.dart'; +import 'package:gopeed/util/updater.dart'; + +void main() { + test('runtime updates do not rebuild the platform controller', () async { + final container = ProviderContainer( + overrides: [ + appRuntimeControllerProvider.overrideWith(_TestRuntimeController.new), + appPlatformControllerProvider.overrideWith(_TestPlatformController.new), + ], + ); + addTearDown(container.dispose); + final subscription = container.listen(appPlatformControllerProvider, (_, _) {}, fireImmediately: true); + addTearDown(subscription.close); + + await container.read(appPlatformControllerProvider.future); + final platformController = container.read(appPlatformControllerProvider.notifier) as _TestPlatformController; + final runtimeController = container.read(appRuntimeControllerProvider.notifier) as _TestRuntimeController; + + expect(platformController.buildCount, 1); + + runtimeController.completeRuntime(); + await container.read(appRuntimeControllerProvider.future); + await Future.delayed(Duration.zero); + expect(platformController.updateCheckCount, 1); + + final config = DownloaderConfig.fromJson(runtimeController.state.requireValue.downloaderConfig.toJson()); + config.extra.locale = 'zh'; + runtimeController.replaceDownloaderConfig(config); + await container.pump(); + + expect(platformController.buildCount, 1); + }); +} + +class _TestPlatformController extends AppPlatformController { + int buildCount = 0; + int updateCheckCount = 0; + + @override + bool get isDesktop => false; + + @override + Future build() { + buildCount++; + return super.build(); + } + + @override + Future checkForUpdate() async { + updateCheckCount++; + return null; + } +} + +class _TestRuntimeController extends AppRuntimeController { + final _pendingRuntime = Completer(); + + @override + Future build() => _pendingRuntime.future; + + void completeRuntime() { + _pendingRuntime.complete( + AppRuntimeState( + startConfig: StartConfig(), + apiServerState: const ApiServerState( + enabled: false, + mcpEnabled: false, + running: false, + network: '', + address: '', + runningPort: 0, + pendingApply: false, + lastError: '', + ), + downloaderConfig: DownloaderConfig(), + ), + ); + } +}