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:
1 parent
771b961b32
commit
52f1c481a6
5 files changed
+128
-10
No files matched your search
@@ -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();
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in new issue
Block a user