101 lines
3.7 KiB
TypeScript
101 lines
3.7 KiB
TypeScript
import { readFile } from 'node:fs/promises'
|
|
import { mkdirSync, renameSync, writeFileSync } from 'node:fs'
|
|
import { dirname } from 'node:path'
|
|
import type { Rectangle } from 'electron'
|
|
|
|
export interface WindowPreferences {
|
|
control: { bounds: Rectangle; maximized: boolean } | null
|
|
display: { id: number; label: string } | null
|
|
}
|
|
const defaults = (): WindowPreferences => ({ control: null, display: null })
|
|
function object(value: unknown): Record<string, unknown> {
|
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
throw new Error('Некорректные настройки окон')
|
|
return value as Record<string, unknown>
|
|
}
|
|
function integer(value: unknown, min: number, max: number): number {
|
|
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < min || value > max)
|
|
throw new Error('Некорректная геометрия окна')
|
|
return value
|
|
}
|
|
export function parseWindowPreferences(value: unknown): WindowPreferences {
|
|
const data = object(value)
|
|
if (data.version !== 1) throw new Error('Неизвестная версия настроек окон')
|
|
const result = defaults()
|
|
if (data.control !== null) {
|
|
const c = object(data.control)
|
|
const b = object(c.bounds)
|
|
if (typeof c.maximized !== 'boolean') throw new Error('Некорректное состояние окна')
|
|
result.control = {
|
|
maximized: c.maximized,
|
|
bounds: {
|
|
x: integer(b.x, -10000000, 10000000),
|
|
y: integer(b.y, -10000000, 10000000),
|
|
width: integer(b.width, 1, 32768),
|
|
height: integer(b.height, 1, 32768),
|
|
},
|
|
}
|
|
}
|
|
if (data.display !== null) {
|
|
const d = object(data.display)
|
|
if (typeof d.label !== 'string' || d.label.length > 1024)
|
|
throw new Error('Некорректный дисплей')
|
|
result.display = {
|
|
id: integer(d.id, Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER),
|
|
label: d.label,
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
export function fitControlBounds(saved: Rectangle | undefined, work: Rectangle): Rectangle {
|
|
const width = Math.min(work.width, Math.max(Math.min(640, work.width), saved?.width ?? 1000))
|
|
const height = Math.min(work.height, Math.max(Math.min(480, work.height), saved?.height ?? 800))
|
|
return {
|
|
x: Math.max(
|
|
work.x,
|
|
Math.min(
|
|
saved?.x ?? work.x + Math.floor((work.width - width) / 2),
|
|
work.x + work.width - width,
|
|
),
|
|
),
|
|
y: Math.max(
|
|
work.y,
|
|
Math.min(
|
|
saved?.y ?? work.y + Math.floor((work.height - height) / 2),
|
|
work.y + work.height - height,
|
|
),
|
|
),
|
|
width,
|
|
height,
|
|
}
|
|
}
|
|
export function preferredDisplayId(
|
|
preferred: WindowPreferences['display'],
|
|
displays: { id: number; label: string }[],
|
|
primary: number,
|
|
): number | null {
|
|
return (
|
|
displays.find((d) => d.id !== primary && d.id === preferred?.id && d.label === preferred.label)
|
|
?.id ?? null
|
|
)
|
|
}
|
|
export class WindowSettings {
|
|
value: WindowPreferences = defaults()
|
|
constructor(private path: string) {}
|
|
async load() {
|
|
try {
|
|
this.value = parseWindowPreferences(JSON.parse(await readFile(this.path, 'utf8')))
|
|
} catch (error) {
|
|
if ((error as NodeJS.ErrnoException).code !== 'ENOENT')
|
|
throw new Error('Настройки окон повреждены; использовано безопасное расположение')
|
|
}
|
|
}
|
|
save() {
|
|
const data = JSON.stringify({ version: 1, ...this.value })
|
|
mkdirSync(dirname(this.path), { recursive: true })
|
|
// Маленький файл пишется синхронно, чтобы последний resize не потерялся при выходе.
|
|
writeFileSync(`${this.path}.tmp`, data, { mode: 0o600 })
|
|
renameSync(`${this.path}.tmp`, this.path)
|
|
}
|
|
}
|