Window comes back at its last size, position and maximized state

Saved on resize, move, maximize and close; on start (and hot restart)
the normal size and maximized state come back everywhere, the position
on Windows and X11 when it still lands on a display (Wayland places
windows itself).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
foxyzandClaude Opus 5.5 committed 2026-10-11 15:29:23 +03:00
1 parent 771b961b32
commit 52f1c481a6
5 files changed
+128 -10

No files matched your search

+14 -8
View File
@@ -10,6 +10,7 @@ import 'package:window_manager/window_manager.dart';
import 'app.dart'; import 'app.dart';
import 'src/playback/yt_dlp_app.dart'; import 'src/playback/yt_dlp_app.dart';
import 'src/platform/desktop_entry.dart'; import 'src/platform/desktop_entry.dart';
import 'src/platform/window_memory.dart';
import 'src/screens/home/ui.dart'; import 'src/screens/home/ui.dart';
import 'src/state/session_state.dart'; import 'src/state/session_state.dart';
import 'src/state/settings.dart'; import 'src/state/settings.dart';
@@ -40,23 +41,25 @@ Future<void> main(List<String> args) async {
), ),
); );
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await Hive.initFlutter();
await AppSettings.instance.ensureLoaded();
await windowManager.ensureInitialized(); await windowManager.ensureInitialized();
await windowManager.setPreventClose(true); await windowManager.setPreventClose(true);
windowManager.addListener(_CloseGuard()); final memory = WindowMemory(Hive.isBoxOpen(settingsBoxName) ? Hive.box(settingsBoxName) : null);
const windowOptions = WindowOptions( windowManager.addListener(_CloseGuard(memory));
size: Size(1280, 720), final windowOptions = WindowOptions(
minimumSize: Size(160, 90), size: memory.size,
center: true, minimumSize: const Size(160, 90),
backgroundColor: Color(0xFF000000), center: !memory.hasPosition,
backgroundColor: const Color(0xFF000000),
titleBarStyle: TitleBarStyle.hidden, titleBarStyle: TitleBarStyle.hidden,
title: appName, title: appName,
); );
windowManager.waitUntilReadyToShow(windowOptions, () async { windowManager.waitUntilReadyToShow(windowOptions, () async {
await memory.restore();
await windowManager.show(); await windowManager.show();
await windowManager.focus(); await windowManager.focus();
}); });
await Hive.initFlutter();
await AppSettings.instance.ensureLoaded();
try { try {
await SessionState.instance.loadForm(); await SessionState.instance.loadForm();
} catch (e, st) { } catch (e, st) {
@@ -74,12 +77,15 @@ Future<void> main(List<String> args) async {
/// hosted server shuts down), bounded so a hung socket can't keep the /// hosted server shuts down), bounded so a hung socket can't keep the
/// window open. /// window open.
class _CloseGuard with WindowListener { class _CloseGuard with WindowListener {
final WindowMemory memory;
bool _closing = false; bool _closing = false;
_CloseGuard(this.memory);
@override @override
Future<void> onWindowClose() async { Future<void> onWindowClose() async {
if (_closing) return; if (_closing) return;
_closing = true; _closing = true;
await memory.save();
try { try {
await SessionState.instance.leave().timeout(const Duration(seconds: 2)); await SessionState.instance.leave().timeout(const Duration(seconds: 2));
} catch (e) { } catch (e) {
@@ -0,0 +1,106 @@
import 'dart:async';
import 'dart:io';
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:hive_ce_flutter/hive_flutter.dart';
import 'package:screen_retriever/screen_retriever.dart';
import 'package:window_manager/window_manager.dart';
/// The window's size, position and maximized state across restarts (and
/// hot restarts), in the settings box. The size and position kept are
/// the normal (unmaximized) ones; fullscreen is never saved or restored.
/// Wayland lets no app place its own window, so there only the size and
/// maximized state come back (the compositor decides the position).
class WindowMemory with WindowListener {
static const _key = 'windowBounds';
static const defaultSize = Size(1280, 720);
// Null when the settings box failed to open: defaults, nothing saved.
final Box? _box;
Timer? _save;
WindowMemory(this._box);
static bool get _canPlace => !(Platform.isLinux && Platform.environment['WAYLAND_DISPLAY'] != null);
Map? get _saved => _box?.get(_key) is Map ? _box!.get(_key) as Map : null;
/// The size to open at: the last normal size, else [defaultSize].
Size get size {
final s = _saved;
final w = s?['w'], h = s?['h'];
return w is num && h is num && w >= 160 && h >= 90 ? Size(w.toDouble(), h.toDouble()) : defaultSize;
}
/// Whether a saved position exists to place the window at (else center).
bool get hasPosition => _canPlace && _saved?['x'] is num && _saved?['y'] is num;
/// After the window shows: back to the saved position when it still
/// lands on a connected display, maximized when it was; then listens.
Future<void> restore() async {
final s = _saved;
try {
if (s != null && hasPosition) {
final rect = Offset((s['x'] as num).toDouble(), (s['y'] as num).toDouble()) & size;
if (await _onSomeDisplay(rect)) {
await windowManager.setPosition(rect.topLeft);
} else {
await windowManager.center();
}
}
if (s?['max'] == true) await windowManager.maximize();
} catch (e) {
debugPrint('window restore failed: $e');
}
windowManager.addListener(this);
}
// At least a 100×50 corner of the title area on a display's visible area.
static Future<bool> _onSomeDisplay(Rect r) async {
final grip = Rect.fromLTWH(r.left, r.top, r.width.clamp(0, 100).toDouble(), 50);
for (final d in await screenRetriever.getAllDisplays()) {
final area = (d.visiblePosition ?? Offset.zero) & (d.visibleSize ?? d.size);
if (area.overlaps(grip)) return true;
}
return false;
}
void _later() {
_save?.cancel();
_save = Timer(const Duration(milliseconds: 400), () => unawaited(save()));
}
/// Writes the current state now (also on close).
Future<void> save() async {
_save?.cancel();
final box = _box;
if (box == null) return;
try {
if (await windowManager.isFullScreen() || await windowManager.isMinimized()) return;
final max = await windowManager.isMaximized();
final s = Map<String, Object?>.of((_saved ?? const {}).cast<String, Object?>());
s['max'] = max;
if (!max) {
final b = await windowManager.getBounds();
s['w'] = b.width;
s['h'] = b.height;
if (_canPlace) {
s['x'] = b.left;
s['y'] = b.top;
}
}
await box.put(_key, s);
} catch (e) {
debugPrint('window save failed: $e');
}
}
@override
void onWindowResized() => _later();
@override
void onWindowMoved() => _later();
@override
void onWindowMaximize() => _later();
@override
void onWindowUnmaximize() => _later();
}
+1 -1
View File
@@ -670,7 +670,7 @@ packages:
source: hosted source: hosted
version: "2.0.6" version: "2.0.6"
screen_retriever: screen_retriever:
dependency: transitive dependency: "direct main"
description: description:
name: screen_retriever name: screen_retriever
sha256: ace919117a7520c13a50a6259e60c4a0d4cbe98809468792a91b5c5adada2aa6 sha256: ace919117a7520c13a50a6259e60c4a0d4cbe98809468792a91b5c5adada2aa6
+1
View File
@@ -23,6 +23,7 @@ dependencies:
hive_ce_flutter: ^2.3.4 hive_ce_flutter: ^2.3.4
image: ^4.9.2 image: ^4.9.2
window_manager: ^0.5.2 window_manager: ^0.5.2
screen_retriever: ^0.2.2
crypto: ^3.0.6 crypto: ^3.0.6
ffi: ^2.1.0 ffi: ^2.1.0
# Drag & drop and clipboard files/images (vendored desktop_drop + pasteboard). # Drag & drop and clipboard files/images (vendored desktop_drop + pasteboard).
+6 -1
View File
@@ -923,7 +923,12 @@ every `state` and feed), the pure rules in `queue_logic.dart`, the UI in
## Desktop UI ## Desktop UI
One window, `AppShell` (`lib/src/shell/`), whatever the connection state One window, `AppShell` (`lib/src/shell/`), whatever the connection state
(online = `SessionState.route` is `session`). Motion is shared (online = `SessionState.route` is `session`). The window comes back as it was
(`platform/window_memory.dart`, settings key `windowBounds`, saved on
resize/move/maximize and close): its normal size and maximized state
everywhere, its position where an app may place itself (Windows, X11;
not Wayland, where the compositor decides) and only if it still lands
on a connected display, else centred. Fullscreen is not restored. Motion is shared
(`motion.dart`): one curve (`fastEaseInToSlowEaseOut`, 320 ms), a fade (`motion.dart`): one curve (`fastEaseInToSlowEaseOut`, 320 ms), a fade
through for views that replace each other (the old one gone after 35%, the through for views that replace each other (the old one gone after 35%, the
new one fading and scaling in from 0.985), `showPopover` (cards that fade new one fading and scaling in from 0.985), `showPopover` (cards that fade