mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-09 10:00:27 +00:00
feat(ui): unify child surfaces and show web task creation in a modal (#1633)
This commit is contained in:
30 files changed
+618
-85
No files matched your search
@@ -13,9 +13,7 @@ import '../../api/model/install_extension.dart';
|
||||
import '../../api/model/request.dart';
|
||||
import '../../core/entry/app_startup_options.dart';
|
||||
import '../../core/utils/content_uri_resolver.dart';
|
||||
import '../../core/window/app_window_launcher.dart';
|
||||
import '../../features/extensions/application/pending_extension_install.dart';
|
||||
import '../../features/tasks/application/pending_create_task.dart';
|
||||
import '../../util/util.dart';
|
||||
import '../router/app_router.dart';
|
||||
import 'app_runtime_controller.dart';
|
||||
@@ -118,18 +116,7 @@ class AppDeepLinkController extends AsyncNotifier<AppDeepLinkState> {
|
||||
await _go('/');
|
||||
}
|
||||
|
||||
Future<void> _openCreate(CreateTask? createTask) async {
|
||||
if (Util.isDesktop()) {
|
||||
final opened = await AppWindowLauncher.openCreateTaskWindow(createTask: createTask);
|
||||
if (!opened) {
|
||||
ref.read(pendingCreateTaskProvider.notifier).set(createTask);
|
||||
await _go('/create');
|
||||
}
|
||||
return;
|
||||
}
|
||||
ref.read(pendingCreateTaskProvider.notifier).set(createTask);
|
||||
await _go('/create');
|
||||
}
|
||||
Future<void> _openCreate(CreateTask? createTask) => AppRouter.openCreateTask(createTask: createTask);
|
||||
|
||||
Future<void> _go(String route) async {
|
||||
if (Util.isDesktop()) {
|
||||
|
||||
@@ -14,9 +14,7 @@ import '../../api/api.dart' as api;
|
||||
import '../../api/model/downloader_config.dart';
|
||||
import '../../core/libgopeed_boot.dart';
|
||||
import '../../core/capabilities/app_capabilities.dart';
|
||||
import '../../core/window/app_window_launcher.dart';
|
||||
import '../../core/window/window_capability_transport.dart';
|
||||
import '../../features/tasks/application/pending_create_task.dart';
|
||||
import '../../features/tasks/application/pending_update_task.dart';
|
||||
import '../../l10n/l10n.dart';
|
||||
import '../../util/package_info.dart';
|
||||
@@ -211,11 +209,7 @@ class AppPlatformController extends AsyncNotifier<AppPlatformState> with WindowL
|
||||
await ref.read(gopeedServiceProvider).createTask(createTask);
|
||||
return;
|
||||
}
|
||||
final opened = await AppWindowLauncher.openCreateTaskWindow(createTask: createTask);
|
||||
if (!opened) {
|
||||
ref.read(pendingCreateTaskProvider.notifier).set(createTask);
|
||||
await _go('/create');
|
||||
}
|
||||
await AppRouter.openCreateTask(createTask: createTask);
|
||||
},
|
||||
onForward: ({required path, required method, data, query}) {
|
||||
return api.forward(path, method: method, data: data, queryParameters: query);
|
||||
@@ -348,10 +342,7 @@ class AppPlatformController extends AsyncNotifier<AppPlatformState> with WindowL
|
||||
|
||||
Future<void> _openCreateTask() async {
|
||||
await _showMainWindow();
|
||||
final opened = await AppWindowLauncher.openCreateTaskWindow();
|
||||
if (!opened) {
|
||||
_go('/create');
|
||||
}
|
||||
await AppRouter.openCreateTask();
|
||||
}
|
||||
|
||||
Future<void> _go(String route) async {
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart' as shad;
|
||||
|
||||
import '../api/model/create_task.dart';
|
||||
import '../app/application/app_appearance_controller.dart';
|
||||
import '../core/window/app_window_bootstrap.dart';
|
||||
import '../core/window/app_window_frame.dart';
|
||||
import '../core/window/app_window_payload.dart';
|
||||
import '../core/window/window_capability_transport.dart';
|
||||
import '../features/tasks/presentation/pages/create_task_window_page.dart';
|
||||
import 'router/app_window_content.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../shared/theme/app_component_themes.dart';
|
||||
import '../shared/theme/app_theme.dart';
|
||||
@@ -82,10 +81,7 @@ class _ChildWindowAppState extends State<ChildWindowApp> with WidgetsBindingObse
|
||||
|
||||
Widget _buildPage(BuildContext context) {
|
||||
return switch (widget.payload.type) {
|
||||
AppWindowType.createTask => CreateTaskWindowPage(
|
||||
windowController: widget.session.controller,
|
||||
initialTask: widget.payload.createTask == null ? null : CreateTask.fromJson(widget.payload.createTask!),
|
||||
),
|
||||
AppWindowType.createTask => AppWindowContent.build(widget.payload, windowController: widget.session.controller),
|
||||
_ => shad.Scaffold(child: Center(child: Text(context.l10n.unsupportedWindowPayload))),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@@ -11,7 +13,13 @@ import '../../features/auth/application/web_auth_controller.dart';
|
||||
import '../../features/auth/presentation/pages/login_page.dart';
|
||||
import '../../features/home/presentation/pages/home_page.dart';
|
||||
import '../../features/settings/presentation/pages/settings_page.dart';
|
||||
import '../../features/tasks/presentation/pages/create_task_window_page.dart';
|
||||
import '../../api/model/create_task.dart';
|
||||
import '../../core/window/app_window_chrome.dart';
|
||||
import '../../core/window/app_window_launcher.dart';
|
||||
import '../../core/window/app_window_payload.dart';
|
||||
import '../../features/tasks/application/pending_create_task.dart';
|
||||
import 'app_window_content.dart';
|
||||
import 'app_window_page.dart';
|
||||
import '../../features/tasks/domain/task_record.dart';
|
||||
import '../../features/tasks/presentation/pages/task_details_page.dart';
|
||||
import '../../features/tasks/presentation/pages/task_files_page.dart';
|
||||
@@ -22,6 +30,37 @@ import 'runtime_page.dart';
|
||||
class AppRouter {
|
||||
static final GlobalKey<NavigatorState> rootNavigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
static bool get _isMobile =>
|
||||
!kIsWeb && (defaultTargetPlatform == TargetPlatform.android || defaultTargetPlatform == TargetPlatform.iOS);
|
||||
|
||||
static Future<void> openCreateTask({CreateTask? createTask}) =>
|
||||
openWindow(AppWindowPayload.createTask(createTask: createTask?.toJson()));
|
||||
|
||||
/// Opens a desktop child window, a Web modal, or a native mobile page.
|
||||
/// Callers never need to choose a presentation or implement a fallback.
|
||||
static Future<void> openWindow(AppWindowPayload payload) async {
|
||||
final location = AppWindowContent.location(payload.type);
|
||||
if (AppWindowChrome.isDesktopWindow && await AppWindowLauncher.open(payload)) return;
|
||||
final context = rootNavigatorKey.currentContext;
|
||||
if (context == null || !context.mounted) return;
|
||||
final router = GoRouter.of(context);
|
||||
if (router.state.uri.path == location) {
|
||||
// Native share intents may arrive while the creation page is already open.
|
||||
if (payload.type == AppWindowType.createTask && payload.createTask != null) {
|
||||
ProviderScope.containerOf(
|
||||
context,
|
||||
listen: false,
|
||||
).read(pendingCreateTaskProvider.notifier).set(CreateTask.fromJson(payload.createTask!));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (_isMobile) {
|
||||
router.go(location, extra: payload.toRaw());
|
||||
} else {
|
||||
unawaited(router.push<void>(location, extra: payload.toRaw()));
|
||||
}
|
||||
}
|
||||
|
||||
static void showDownloadingTasks() {
|
||||
final context = rootNavigatorKey.currentContext;
|
||||
if (context == null || !context.mounted) return;
|
||||
@@ -31,8 +70,7 @@ class AppRouter {
|
||||
|
||||
static GoRouter build(WebAuthController webAuthController) {
|
||||
// Keep desktop and web sections as peers; native mobile retains home below them.
|
||||
final isMobile =
|
||||
!kIsWeb && (defaultTargetPlatform == TargetPlatform.android || defaultTargetPlatform == TargetPlatform.iOS);
|
||||
final isMobile = _isMobile;
|
||||
final sectionRoutes = [
|
||||
GoRoute(
|
||||
path: isMobile ? 'extensions' : '/extensions',
|
||||
@@ -83,7 +121,33 @@ class AppRouter {
|
||||
path: '/',
|
||||
builder: _runtimePage((context, state) => const HomePage()),
|
||||
routes: [
|
||||
GoRoute(path: 'create', builder: _runtimePage((context, state) => const CreateTaskWindowPage())),
|
||||
GoRoute(
|
||||
path: 'create',
|
||||
parentNavigatorKey: rootNavigatorKey,
|
||||
pageBuilder: (context, state) {
|
||||
final payload = state.extra is String
|
||||
? AppWindowPayload.fromRaw(state.extra! as String)
|
||||
: AppWindowPayload.createTask();
|
||||
final child = RuntimePage(
|
||||
builder: (context) => AppWindowContent.build(
|
||||
payload,
|
||||
modal: !isMobile,
|
||||
onClose: (created) {
|
||||
if (created) {
|
||||
showDownloadingTasks();
|
||||
} else if (context.canPop()) {
|
||||
context.pop();
|
||||
} else {
|
||||
context.go('/');
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
return isMobile
|
||||
? NoTransitionPage<void>(key: state.pageKey, child: child)
|
||||
: AppWindowPage<void>(key: state.pageKey, child: child);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: 'tasks/:id',
|
||||
builder: _runtimePage(
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import '../../api/model/create_task.dart';
|
||||
import '../../core/window/app_window_payload.dart';
|
||||
import '../../features/tasks/presentation/pages/create_task_window_page.dart';
|
||||
|
||||
/// Shared destination registry for desktop child engines and in-app windows.
|
||||
abstract final class AppWindowContent {
|
||||
static String location(AppWindowType type) => switch (type) {
|
||||
AppWindowType.createTask => '/create',
|
||||
AppWindowType.main => throw ArgumentError.value(type, 'type', 'Expected a child window'),
|
||||
};
|
||||
|
||||
static Widget build(
|
||||
AppWindowPayload payload, {
|
||||
WindowController? windowController,
|
||||
ValueChanged<bool>? onClose,
|
||||
bool modal = false,
|
||||
}) => switch (payload.type) {
|
||||
AppWindowType.createTask => CreateTaskWindowPage(
|
||||
windowController: windowController,
|
||||
initialTask: payload.createTask == null ? null : CreateTask.fromJson(payload.createTask!),
|
||||
onClose: onClose,
|
||||
modal: modal,
|
||||
),
|
||||
AppWindowType.main => throw ArgumentError.value(payload.type, 'payload.type', 'Expected a child window'),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
|
||||
import '../../shared/theme/app_design_tokens.dart';
|
||||
import '../../shared/theme/app_palette.dart';
|
||||
import '../../core/utils/breakpoints.dart';
|
||||
|
||||
/// A routed modal keeps its underlying page mounted and participates in Back.
|
||||
class AppWindowPage<T> extends Page<T> {
|
||||
const AppWindowPage({super.key, required this.child, this.preferredSize = AppDesignTokens.childWindowSize});
|
||||
|
||||
final Widget child;
|
||||
final Size preferredSize;
|
||||
|
||||
@override
|
||||
Route<T> createRoute(BuildContext context) => DialogRoute<T>(
|
||||
context: context,
|
||||
settings: this,
|
||||
barrierDismissible: false,
|
||||
barrierColor: Theme.of(context).colorScheme.background.withValues(alpha: 0.6),
|
||||
traversalEdgeBehavior: TraversalEdgeBehavior.closedLoop,
|
||||
alignment: Alignment.center,
|
||||
fullScreen: true,
|
||||
transitionBuilder: (context, animation, secondaryAnimation, child) =>
|
||||
FadeTransition(opacity: animation, child: child),
|
||||
builder: (context) => Padding(
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.viewInsetsOf(context).bottom),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final page = ModalRoute.settingsOf(context)! as AppWindowPage<T>;
|
||||
final palette = AppPalette.of(context);
|
||||
return Padding(
|
||||
padding: EdgeInsets.all(
|
||||
constraints.maxWidth < Breakpoints.mobile ? AppDesignTokens.space12 : AppDesignTokens.space32,
|
||||
),
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: page.preferredSize.width, maxHeight: page.preferredSize.height),
|
||||
child: ModalContainer(
|
||||
key: const ValueKey('app-window-modal'),
|
||||
padding: EdgeInsets.zero,
|
||||
filled: true,
|
||||
fillColor: palette.sideBg,
|
||||
borderColor: palette.border,
|
||||
borderRadius: BorderRadius.circular(AppDesignTokens.windowRadius),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: MediaQuery.removeViewInsets(
|
||||
context: context,
|
||||
removeBottom: true,
|
||||
child: MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
removeBottom: true,
|
||||
child: page.child,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import 'dart:async';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../../api/model/create_task.dart';
|
||||
import '../../application/app_deep_link_controller.dart';
|
||||
@@ -11,11 +10,10 @@ import '../../application/app_notification_controller.dart';
|
||||
import '../../application/app_runtime_controller.dart';
|
||||
import '../../application/app_platform_controller.dart';
|
||||
import '../../../features/settings/presentation/widgets/app_update_dialog.dart';
|
||||
import '../../../features/tasks/application/pending_create_task.dart';
|
||||
import '../../../features/tasks/application/pending_update_task.dart';
|
||||
import '../../../features/tasks/application/tasks_controller.dart';
|
||||
import '../../../features/tasks/presentation/widgets/pending_update_dialog.dart';
|
||||
import '../../../core/window/app_window_launcher.dart';
|
||||
import '../app_router.dart';
|
||||
import '../../../shared/widgets/app_toast.dart';
|
||||
|
||||
class MainShell extends ConsumerStatefulWidget {
|
||||
@@ -124,11 +122,5 @@ class _MainShellState extends ConsumerState<MainShell> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _openCapturedCreateTask(CreateTask createTask) async {
|
||||
final opened = await AppWindowLauncher.openCreateTaskWindow(createTask: createTask);
|
||||
if (!opened && mounted) {
|
||||
ref.read(pendingCreateTaskProvider.notifier).set(createTask);
|
||||
context.go('/create');
|
||||
}
|
||||
}
|
||||
Future<void> _openCapturedCreateTask(CreateTask createTask) => AppRouter.openCreateTask(createTask: createTask);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
import '../../l10n/l10n.dart';
|
||||
import '../../shared/theme/app_design_tokens.dart';
|
||||
import 'app_window_chrome.dart';
|
||||
import 'app_window_payload.dart';
|
||||
|
||||
@@ -46,7 +47,7 @@ class AppWindowBootstrap {
|
||||
|
||||
final options = switch (type) {
|
||||
AppWindowType.createTask => WindowOptions(
|
||||
size: Size(820, 620),
|
||||
size: AppDesignTokens.childWindowSize,
|
||||
center: true,
|
||||
skipTaskbar: false,
|
||||
backgroundColor: Colors.transparent,
|
||||
|
||||
@@ -2,22 +2,24 @@ import 'dart:io';
|
||||
|
||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
import '../../api/model/create_task.dart';
|
||||
import '../../util/log_util.dart';
|
||||
import '../capabilities/app_capabilities.dart';
|
||||
import 'app_window_payload.dart';
|
||||
import 'window_capability_transport.dart';
|
||||
|
||||
class AppWindowLauncher {
|
||||
static Future<bool> openCreateTaskWindow({CreateTask? createTask}) async {
|
||||
static Future<bool> open(AppWindowPayload payload) async {
|
||||
if (payload.type == AppWindowType.main) {
|
||||
throw ArgumentError.value(payload.type, 'payload.type', 'Expected a child window');
|
||||
}
|
||||
if (kIsWeb || !(Platform.isWindows || Platform.isMacOS || Platform.isLinux)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await AppWindowCapabilityHost.instance.start(LocalAppCapabilities.instance.registry);
|
||||
final payload = AppWindowPayload.createTask(createTask: createTask?.toJson());
|
||||
// Every request deliberately creates a new child window. Create-task
|
||||
// windows are independent and are never reused.
|
||||
await WindowController.create(WindowConfiguration(arguments: payload.toRaw(), hiddenAtLaunch: true));
|
||||
@@ -27,10 +29,16 @@ class AppWindowLauncher {
|
||||
return true;
|
||||
} catch (error, stackTrace) {
|
||||
try {
|
||||
logger.w('create task window failed', error, stackTrace);
|
||||
logger.w('child window failed', error, stackTrace);
|
||||
} catch (_) {
|
||||
debugPrint('create task window failed: $error');
|
||||
debugPrint('child window failed: $error');
|
||||
}
|
||||
// A tray/deep-link request can arrive while the main window is hidden.
|
||||
// Bring the in-app fallback into view if the child engine cannot start.
|
||||
try {
|
||||
await windowManager.show();
|
||||
await windowManager.focus();
|
||||
} catch (_) {}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import '../../../../app/application/app_runtime_controller.dart';
|
||||
import '../../../../core/capabilities/app_capabilities.dart';
|
||||
import '../../../../core/utils/breakpoints.dart';
|
||||
import '../../../../core/utils/file_explorer.dart';
|
||||
import '../../../../core/window/app_window_launcher.dart';
|
||||
import '../../../../app/router/app_router.dart';
|
||||
import '../../../../core/window/app_window_chrome.dart';
|
||||
import '../../../../shared/theme/app_design_tokens.dart';
|
||||
import '../../../../shared/theme/app_palette.dart';
|
||||
@@ -580,19 +580,7 @@ class _HomePageState extends ConsumerState<HomePage> {
|
||||
}
|
||||
}
|
||||
|
||||
void _handleAddTask() {
|
||||
if (!Util.isDesktop()) {
|
||||
context.go('/create');
|
||||
return;
|
||||
}
|
||||
unawaited(
|
||||
AppWindowLauncher.openCreateTaskWindow().then((opened) {
|
||||
if (!opened && mounted) {
|
||||
context.go('/create');
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
void _handleAddTask() => unawaited(AppRouter.openCreateTask());
|
||||
|
||||
Future<void> _runTaskAction(Future<void> Function() action) async {
|
||||
try {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart' show Icons;
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../../../core/window/app_window_chrome.dart';
|
||||
@@ -28,7 +27,21 @@ class PrimaryRail extends StatelessWidget {
|
||||
children: [
|
||||
SizedBox(
|
||||
height: AppDesignTokens.contentHeaderHeight,
|
||||
child: const Center(child: GopeedAppMark(key: ValueKey('primary-rail-app-mark'))),
|
||||
child: Center(
|
||||
child: AppTooltip(
|
||||
message: context.l10n.task,
|
||||
child: Semantics(
|
||||
label: context.l10n.task,
|
||||
button: true,
|
||||
child: GhostButton(
|
||||
key: const ValueKey('primary-rail-home-button'),
|
||||
density: ButtonDensity.icon,
|
||||
onPressed: () => context.go('/'),
|
||||
child: const ExcludeSemantics(child: GopeedAppMark(key: ValueKey('primary-rail-app-mark'))),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppDesignTokens.space8),
|
||||
_RailItem(
|
||||
|
||||
@@ -44,10 +44,12 @@ import '../widgets/create_task_action_buttons.dart';
|
||||
import '../widgets/resolve_download_dialog.dart';
|
||||
|
||||
class CreateTaskWindowPage extends ConsumerStatefulWidget {
|
||||
const CreateTaskWindowPage({super.key, this.windowController, this.initialTask});
|
||||
const CreateTaskWindowPage({super.key, this.windowController, this.initialTask, this.onClose, this.modal = false});
|
||||
|
||||
final multi_window.WindowController? windowController;
|
||||
final CreateTask? initialTask;
|
||||
final ValueChanged<bool>? onClose;
|
||||
final bool modal;
|
||||
|
||||
@override
|
||||
ConsumerState<CreateTaskWindowPage> createState() => _CreateTaskWindowPageState();
|
||||
@@ -133,7 +135,7 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
|
||||
if (widget.initialTask == null) {
|
||||
ref.read(pendingCreateTaskProvider.notifier).clear();
|
||||
}
|
||||
if (widget.windowController == null && widget.initialTask == null) {
|
||||
if (widget.windowController == null) {
|
||||
// Navigating to /create again reuses this page, so consume subsequent
|
||||
// browser/share requests as well as the initial pending task.
|
||||
ref.listenManual(pendingCreateTaskProvider, (_, next) {
|
||||
@@ -371,7 +373,9 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
|
||||
child: Padding(
|
||||
key: const ValueKey('create-task-safe-content'),
|
||||
padding: EdgeInsets.only(
|
||||
top: AppWindowChrome.reservesHeaderInset
|
||||
top: widget.modal
|
||||
? 0
|
||||
: AppWindowChrome.reservesHeaderInset
|
||||
? AppDesignTokens.windowHeaderHeight
|
||||
: MediaQuery.paddingOf(context).top,
|
||||
),
|
||||
@@ -867,6 +871,10 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
|
||||
}
|
||||
|
||||
Future<void> _closeWindow() async {
|
||||
if (widget.onClose != null) {
|
||||
widget.onClose!(false);
|
||||
return;
|
||||
}
|
||||
if (widget.windowController == null && mounted) {
|
||||
context.go('/');
|
||||
return;
|
||||
@@ -888,6 +896,10 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
|
||||
}
|
||||
}
|
||||
if (!mounted || session != _createSession) return;
|
||||
if (widget.onClose != null) {
|
||||
widget.onClose!(true);
|
||||
return;
|
||||
}
|
||||
if (widget.windowController != null) {
|
||||
// Navigation is best-effort after a successful submission. A navigation
|
||||
// failure must not leave a completed form available for resubmission.
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter/widgets.dart';
|
||||
class AppDesignTokens {
|
||||
const AppDesignTokens._();
|
||||
|
||||
static const Size childWindowSize = Size(820, 620);
|
||||
static const double windowHeaderHeight = 30;
|
||||
static const double windowRadius = 12;
|
||||
// Leave room for native macOS traffic lights while keeping rail widths consistent across platforms.
|
||||
|
||||
Reference in new issue
Block a user