mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-07 00:50:27 +00:00
fix(macos): prevent closed task windows from reopening (#1492)
This commit is contained in:
9 files changed
+847
-440
No files matched your search
@@ -1,5 +1,7 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../features/tasks/application/task_list_navigation.dart';
|
||||
|
||||
import '../../features/extensions/presentation/pages/extensions_page.dart';
|
||||
import '../../features/extensions/presentation/pages/extension_details_page.dart';
|
||||
@@ -17,6 +19,13 @@ import 'shells/main_shell.dart';
|
||||
class AppRouter {
|
||||
static final GlobalKey<NavigatorState> rootNavigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
static void showDownloadingTasks() {
|
||||
final context = rootNavigatorKey.currentContext;
|
||||
if (context == null || !context.mounted) return;
|
||||
ProviderScope.containerOf(context, listen: false).read(taskListNavigationProvider.notifier).showDownloading();
|
||||
GoRouter.of(context).go('/');
|
||||
}
|
||||
|
||||
static GoRouter build(WebAuthController webAuthController) {
|
||||
return GoRouter(
|
||||
navigatorKey: rootNavigatorKey,
|
||||
|
||||
@@ -16,12 +16,18 @@ import '../window/app_window_appearance.dart';
|
||||
import 'capability_rpc.dart';
|
||||
import 'gopeed_capability.dart';
|
||||
import 'storage_capability.dart';
|
||||
import 'app_navigation_capability.dart';
|
||||
import '../../app/router/app_router.dart';
|
||||
|
||||
class AppCapabilities {
|
||||
AppCapabilities(CapabilityInvoker invoker) : gopeed = GopeedService(invoker), storage = AppStorageService(invoker);
|
||||
AppCapabilities(CapabilityInvoker invoker)
|
||||
: gopeed = GopeedService(invoker),
|
||||
storage = AppStorageService(invoker),
|
||||
navigation = AppNavigationService(invoker);
|
||||
|
||||
final GopeedService gopeed;
|
||||
final AppStorageService storage;
|
||||
final AppNavigationService navigation;
|
||||
}
|
||||
|
||||
class LocalAppCapabilities {
|
||||
@@ -29,6 +35,10 @@ class LocalAppCapabilities {
|
||||
registry = CapabilityRegistry(codecs);
|
||||
_bindGopeed(registry);
|
||||
_bindStorage(registry);
|
||||
registry.bind(NavigationMethods.showDownloadingTasks, (_) {
|
||||
AppRouter.showDownloadingTasks();
|
||||
return const RpcUnit();
|
||||
});
|
||||
capabilities = AppCapabilities(LocalCapabilityInvoker(registry));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import 'capability_rpc.dart';
|
||||
|
||||
abstract final class NavigationMethods {
|
||||
static const showDownloadingTasks = RpcMethod<RpcUnit, RpcUnit>('navigation.tasks.downloading');
|
||||
}
|
||||
|
||||
class AppNavigationService {
|
||||
const AppNavigationService(this._invoker);
|
||||
|
||||
final CapabilityInvoker _invoker;
|
||||
|
||||
Future<void> showDownloadingTasks() async {
|
||||
await _invoker.invoke(NavigationMethods.showDownloadingTasks, const RpcUnit());
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import '../../../../shared/widgets/responsive_navigation_layout.dart';
|
||||
import '../../../../l10n/l10n.dart';
|
||||
import '../../../../util/util.dart';
|
||||
import '../../../tasks/application/tasks_controller.dart';
|
||||
import '../../../tasks/application/task_list_navigation.dart';
|
||||
import '../../../tasks/application/pending_update_task.dart';
|
||||
import '../../../tasks/application/task_batch_selection_controller.dart';
|
||||
import '../../../tasks/domain/task_record.dart';
|
||||
@@ -86,6 +87,11 @@ class _HomePageState extends ConsumerState<HomePage> {
|
||||
Widget build(BuildContext context) {
|
||||
final palette = AppPalette.of(context);
|
||||
final tasksAsync = ref.watch(tasksControllerProvider);
|
||||
ref.listen(taskListNavigationProvider, (_, _) {
|
||||
_searchController.clear();
|
||||
_setActiveFilter(_TaskFilter.downloading);
|
||||
setState(() => _batchMode = false);
|
||||
});
|
||||
final tasksState = tasksAsync.value;
|
||||
final tasks = tasksState?.tasks ?? const <TaskRecord>[];
|
||||
final filteredTasks = _filteredTasks(tasks);
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
final taskListNavigationProvider = NotifierProvider<TaskListNavigation, int>(TaskListNavigation.new);
|
||||
|
||||
// A request also resets an already-visible list's filter, search and selection.
|
||||
class TaskListNavigation extends Notifier<int> {
|
||||
@override
|
||||
int build() => 0;
|
||||
|
||||
void showDownloading() => state++;
|
||||
}
|
||||
@@ -737,6 +737,19 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
|
||||
await windowManager.close();
|
||||
}
|
||||
|
||||
Future<void> _finishCreation() async {
|
||||
if (widget.windowController != null) {
|
||||
// Navigation is best-effort after a successful submission. A navigation
|
||||
// failure must not leave a completed form available for resubmission.
|
||||
try {
|
||||
await ref.read(appCapabilitiesProvider).navigation.showDownloadingTasks();
|
||||
} catch (error) {
|
||||
debugPrint('Unable to select downloading tasks: $error');
|
||||
}
|
||||
}
|
||||
await _closeWindow();
|
||||
}
|
||||
|
||||
Future<void> _loadDefaults() async {
|
||||
try {
|
||||
final config = await ref.read(gopeedServiceProvider).getConfig();
|
||||
@@ -863,7 +876,7 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
|
||||
);
|
||||
}),
|
||||
);
|
||||
await _closeWindow();
|
||||
await _finishCreation();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -879,7 +892,7 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
|
||||
}
|
||||
return;
|
||||
}
|
||||
await _closeWindow();
|
||||
await _finishCreation();
|
||||
} catch (error) {
|
||||
if (mounted) {
|
||||
_showToast(error.toString());
|
||||
|
||||
@@ -26,16 +26,18 @@ class AppDelegate: FlutterAppDelegate {
|
||||
}
|
||||
|
||||
override func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
|
||||
if !flag {
|
||||
for window in NSApp.windows {
|
||||
if !window.isVisible {
|
||||
window.setIsVisible(true)
|
||||
}
|
||||
window.makeKeyAndOrderFront(self)
|
||||
// Closed child windows can remain in NSApp.windows. Restoring every
|
||||
// window resurrects completed create-task forms with their last frame.
|
||||
// Use the main window even when an unfinished child is still visible.
|
||||
if let window = sender.windows.first(where: { $0 is MainFlutterWindow }) {
|
||||
if window.isMiniaturized {
|
||||
window.deminiaturize(self)
|
||||
}
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
window.makeKeyAndOrderFront(self)
|
||||
sender.activate(ignoringOtherApps: true)
|
||||
}
|
||||
return true
|
||||
// We handled reopening; AppKit must not restore other retained windows.
|
||||
return false
|
||||
}
|
||||
|
||||
override func application(_ sender: NSApplication, openFile filename: String) -> Bool {
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -13,6 +13,9 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart' as shad;
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import 'package:gopeed/app/app.dart';
|
||||
import 'package:gopeed/app/router/app_router.dart';
|
||||
import 'package:gopeed/core/capabilities/app_capabilities.dart';
|
||||
import 'package:gopeed/core/capabilities/app_navigation_capability.dart';
|
||||
import 'package:gopeed/app/application/app_appearance_controller.dart';
|
||||
import 'package:gopeed/app/application/app_deep_link_controller.dart';
|
||||
import 'package:gopeed/app/application/app_notification_controller.dart';
|
||||
@@ -93,6 +96,38 @@ import 'package:gopeed/util/updater.dart';
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized().platformDispatcher.localeTestValue = const Locale('en');
|
||||
|
||||
testWidgets('child success returns to downloading from completed and another page', (tester) async {
|
||||
await _setTestSize(tester, const Size(1024, 768));
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
appRuntimeControllerProvider.overrideWith(FakeRuntimeController.new),
|
||||
tasksControllerProvider.overrideWith(CompletedTasksController.new),
|
||||
settingsControllerProvider.overrideWith(FakeSettingsController.new),
|
||||
],
|
||||
child: const GopeedApp(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Completed'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('done.zip'), findsOneWidget);
|
||||
|
||||
// Exercise the same serialized operation dispatched by a child window.
|
||||
await LocalAppCapabilities.instance.registry.invoke(NavigationMethods.showDownloadingTasks.name, {});
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('done.zip'), findsNothing);
|
||||
expect(find.text('Create Task'), findsOneWidget);
|
||||
|
||||
GoRouter.of(AppRouter.rootNavigatorKey.currentContext!).go('/settings');
|
||||
await tester.pumpAndSettle();
|
||||
await LocalAppCapabilities.instance.capabilities.navigation.showDownloadingTasks();
|
||||
await tester.pumpAndSettle();
|
||||
expect(GoRouter.of(AppRouter.rootNavigatorKey.currentContext!).routeInformationProvider.value.uri.path, '/');
|
||||
expect(find.text('done.zip'), findsNothing);
|
||||
expect(find.text('Create Task'), findsOneWidget);
|
||||
});
|
||||
|
||||
test('Web MCP endpoint excludes page path, query, and hash route', () {
|
||||
expect(
|
||||
webMcpEndpoint(Uri.parse('https://gopeed.example:8443/app?source=desktop#/settings/advanced')),
|
||||
|
||||
Reference in new issue
Block a user