- Settings 'Size' section: 8 col x 4 row grid, 31 buttons labeled a:b (a splits H, b splits W); top-left cell empty placeholder (no 1:1 — title bar double click already maximizes) - Click resizes the window to work.width/b x work.height/a of the display's work area (screen_retriever visibleSize, taskbar excluded); position unchanged, no selection state tracking - minimumSize 400x300 -> 160x100 (4:8 cell needs it) - Settings file-properties rows left-aligned (were centered under path)
167 lines
5.2 KiB
Dart
167 lines
5.2 KiB
Dart
import 'dart:convert';
|
||
import 'dart:io';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:screen_retriever/screen_retriever.dart';
|
||
|
||
/// Persists and restores window position and size.
|
||
class WindowPersistence {
|
||
static File get _configFile {
|
||
String dir;
|
||
if (Platform.isWindows) {
|
||
dir = '${Platform.environment['APPDATA'] ?? Platform.environment['USERPROFILE'] ?? '.'}/imajviewer';
|
||
} else {
|
||
dir = '${Platform.environment['HOME'] ?? '/tmp'}/.config/imajviewer';
|
||
}
|
||
return File('$dir/window.json');
|
||
}
|
||
|
||
/// Görüntü ekran modu: true = pencereyi kapla (cover), false = sığdır (contain).
|
||
/// Anahtar yoksa/bozuksa true (eski davranış).
|
||
static bool get filled {
|
||
final v = _read()?['filled'];
|
||
return v is bool ? v : true;
|
||
}
|
||
|
||
/// Sadece modu günceller; x/y/w/h korunur.
|
||
static void saveFilled(bool value) => _write({'filled': value});
|
||
|
||
/// Save window rect synchronously (safe to call before destroy).
|
||
static void saveSync({
|
||
required Offset position,
|
||
required Size size,
|
||
}) {
|
||
_write({
|
||
'x': position.dx.toInt(),
|
||
'y': position.dy.toInt(),
|
||
'width': size.width.toInt(),
|
||
'height': size.height.toInt(),
|
||
});
|
||
}
|
||
|
||
/// Dosyayı oku (yok/bozuksa null).
|
||
static Map<String, dynamic>? _read() {
|
||
final file = _configFile;
|
||
if (!file.existsSync()) return null;
|
||
try {
|
||
return jsonDecode(file.readAsStringSync()) as Map<String, dynamic>;
|
||
} catch (_) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// Verilen anahtarları yaz; dosyadaki diğer anahtarlar korunur.
|
||
static void _write(Map<String, dynamic> values) {
|
||
try {
|
||
final file = _configFile;
|
||
file.parent.createSync(recursive: true);
|
||
final data = _read() ?? <String, dynamic>{};
|
||
data.addAll(values);
|
||
file.writeAsStringSync(jsonEncode(data));
|
||
} catch (_) {
|
||
// Disk hatası uygulamayı durdurmasın.
|
||
}
|
||
}
|
||
|
||
/// Restore saved window rect. Returns null if no saved config.
|
||
static ({Offset position, Size size})? restore() {
|
||
final data = _read();
|
||
if (data == null) return null;
|
||
|
||
try {
|
||
final x = (data['x'] as num?)?.toDouble() ?? 0;
|
||
final y = (data['y'] as num?)?.toDouble() ?? 0;
|
||
final w = (data['width'] as num?)?.toDouble() ?? 1280;
|
||
final h = (data['height'] as num?)?.toDouble() ?? 800;
|
||
|
||
if (w < 200 || h < 200 || x < -1000 || y < -1000) return null;
|
||
|
||
return (
|
||
position: Offset(x, y),
|
||
size: Size(w, h),
|
||
);
|
||
} catch (_) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// Work area (taskbar excluded) of the display containing the window,
|
||
/// in logical px. Returns null if screen info is unavailable.
|
||
static Future<Size?> workAreaSize({
|
||
required Offset windowPosition,
|
||
}) async {
|
||
try {
|
||
final displays = await screenRetriever.getAllDisplays();
|
||
|
||
// Pencerenin bulunduğu monitörü bul (çoklu monitör desteği)
|
||
Display display = await screenRetriever.getPrimaryDisplay();
|
||
for (final d in displays) {
|
||
final pos = d.visiblePosition;
|
||
final size = d.visibleSize ?? d.size;
|
||
if (pos != null &&
|
||
windowPosition.dx >= pos.dx &&
|
||
windowPosition.dx < pos.dx + size.width &&
|
||
windowPosition.dy >= pos.dy &&
|
||
windowPosition.dy < pos.dy + size.height) {
|
||
display = d;
|
||
break;
|
||
}
|
||
}
|
||
|
||
return display.visibleSize ?? display.size;
|
||
} catch (_) {
|
||
// Fallback: screen info unavailable → caller silently no-ops.
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// Cascade: place new window to the right of the previous one.
|
||
/// Falls to the next row (row start) when it would overflow the screen,
|
||
/// and wraps to the top-left if the next row also overflows.
|
||
static Future<Offset> cascadePosition({
|
||
required Offset previousPosition,
|
||
required Size previousSize,
|
||
}) async {
|
||
try {
|
||
final displays = await screenRetriever.getAllDisplays();
|
||
|
||
// Önceki pencerenin bulunduğu monitörü bul (çoklu monitör desteği)
|
||
Display display = await screenRetriever.getPrimaryDisplay();
|
||
for (final d in displays) {
|
||
final pos = d.visiblePosition;
|
||
final size = d.visibleSize ?? d.size;
|
||
if (pos != null &&
|
||
previousPosition.dx >= pos.dx &&
|
||
previousPosition.dx < pos.dx + size.width &&
|
||
previousPosition.dy >= pos.dy &&
|
||
previousPosition.dy < pos.dy + size.height) {
|
||
display = d;
|
||
break;
|
||
}
|
||
}
|
||
|
||
final workLeft = display.visiblePosition?.dx ?? 0;
|
||
final workTop = display.visiblePosition?.dy ?? 0;
|
||
final workWidth = display.visibleSize?.width ?? display.size.width;
|
||
final workHeight = display.visibleSize?.height ?? display.size.height;
|
||
|
||
var x = previousPosition.dx + previousSize.width;
|
||
var y = previousPosition.dy;
|
||
|
||
if (x + previousSize.width > workLeft + workWidth) {
|
||
x = workLeft;
|
||
y = previousPosition.dy + previousSize.height;
|
||
}
|
||
|
||
if (y + previousSize.height > workTop + workHeight) {
|
||
x = workLeft;
|
||
y = workTop;
|
||
}
|
||
|
||
return Offset(x, y);
|
||
} catch (_) {
|
||
// Fallback: keep previous position if screen info is unavailable.
|
||
return previousPosition;
|
||
}
|
||
}
|
||
}
|