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 'src/playback/yt_dlp_app.dart';
|
||||
import 'src/platform/desktop_entry.dart';
|
||||
import 'src/platform/window_memory.dart';
|
||||
import 'src/screens/home/ui.dart';
|
||||
import 'src/state/session_state.dart';
|
||||
import 'src/state/settings.dart';
|
||||
@@ -40,23 +41,25 @@ Future<void> main(List<String> args) async {
|
||||
),
|
||||
);
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await Hive.initFlutter();
|
||||
await AppSettings.instance.ensureLoaded();
|
||||
await windowManager.ensureInitialized();
|
||||
await windowManager.setPreventClose(true);
|
||||
windowManager.addListener(_CloseGuard());
|
||||
const windowOptions = WindowOptions(
|
||||
size: Size(1280, 720),
|
||||
minimumSize: Size(160, 90),
|
||||
center: true,
|
||||
backgroundColor: Color(0xFF000000),
|
||||
final memory = WindowMemory(Hive.isBoxOpen(settingsBoxName) ? Hive.box(settingsBoxName) : null);
|
||||
windowManager.addListener(_CloseGuard(memory));
|
||||
final windowOptions = WindowOptions(
|
||||
size: memory.size,
|
||||
minimumSize: const Size(160, 90),
|
||||
center: !memory.hasPosition,
|
||||
backgroundColor: const Color(0xFF000000),
|
||||
titleBarStyle: TitleBarStyle.hidden,
|
||||
title: appName,
|
||||
);
|
||||
windowManager.waitUntilReadyToShow(windowOptions, () async {
|
||||
await memory.restore();
|
||||
await windowManager.show();
|
||||
await windowManager.focus();
|
||||
});
|
||||
await Hive.initFlutter();
|
||||
await AppSettings.instance.ensureLoaded();
|
||||
try {
|
||||
await SessionState.instance.loadForm();
|
||||
} 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
|
||||
/// window open.
|
||||
class _CloseGuard with WindowListener {
|
||||
final WindowMemory memory;
|
||||
bool _closing = false;
|
||||
_CloseGuard(this.memory);
|
||||
|
||||
@override
|
||||
Future<void> onWindowClose() async {
|
||||
if (_closing) return;
|
||||
_closing = true;
|
||||
await memory.save();
|
||||
try {
|
||||
await SessionState.instance.leave().timeout(const Duration(seconds: 2));
|
||||
} 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
|
||||
version: "2.0.6"
|
||||
screen_retriever:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: screen_retriever
|
||||
sha256: ace919117a7520c13a50a6259e60c4a0d4cbe98809468792a91b5c5adada2aa6
|
||||
|
||||
@@ -23,6 +23,7 @@ dependencies:
|
||||
hive_ce_flutter: ^2.3.4
|
||||
image: ^4.9.2
|
||||
window_manager: ^0.5.2
|
||||
screen_retriever: ^0.2.2
|
||||
crypto: ^3.0.6
|
||||
ffi: ^2.1.0
|
||||
# Drag & drop and clipboard files/images (vendored desktop_drop + pasteboard).
|
||||
|
||||
Reference in new issue
Block a user