Сохранять выбор стратегии и автозапуска между запусками
Настройки лежат в settings.json (userData) и восстанавливаются до открытия окна. Списки сайтов уже хранятся в userData/zapret-lists и отдельного сохранения не требуют.
This commit is contained in:
+22
-51
@@ -1,23 +1,18 @@
|
|||||||
import { randomUUID } from 'node:crypto'
|
import { randomUUID } from 'node:crypto'
|
||||||
import { readFile, rename, unlink, writeFile } from 'node:fs/promises'
|
import { readFile, rename, writeFile } from 'node:fs/promises'
|
||||||
import { join } from 'node:path'
|
import { join } from 'node:path'
|
||||||
import { app } from 'electron'
|
import { app } from 'electron'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Выбор пользователя, который должен пережить перезапуск приложения. Лежит в userData
|
* Выбор пользователя между запусками: `settings.json` в userData (на Windows —
|
||||||
* (`%AppData%\RKNboost\settings.json` на Windows — каталог по `productName` из
|
* `%AppData%\RKNboost`), поэтому переживает обновление и переустановку.
|
||||||
* electron-builder.yml) — то есть переживает и обновление, и переустановку приложения.
|
|
||||||
*
|
*
|
||||||
* Списки сайтов сюда намеренно не входят: они и так хранятся рядом, в
|
* Списки сайтов сюда не входят: они лежат рядом, в `userData/zapret-lists/*.txt`, и
|
||||||
* `userData/zapret-lists/*.txt` (см. `zapret/lists.ts`), каждый в своём файле — их читают
|
* читаются прямо оттуда самими tpws и winws (см. `zapret/lists.ts`).
|
||||||
* прямо оттуда сами tpws и winws, и дублировать те же домены ещё и здесь означало бы
|
|
||||||
* держать два источника правды.
|
|
||||||
*
|
*
|
||||||
* `null` в любом поле — «пользователь ничего не выбирал»: тогда решает движок (стратегия
|
* `null` — «пользователь не выбирал», решает движок.
|
||||||
* по умолчанию для платформы, автозапуск — как решил `Win32Engine`).
|
|
||||||
*/
|
*/
|
||||||
export interface AppSettings {
|
export interface AppSettings {
|
||||||
/** Последняя выбранная стратегия обхода. */
|
|
||||||
strategyId: string | null
|
strategyId: string | null
|
||||||
/** Windows: запускать службу вместе с системой. На macOS не используется. */
|
/** Windows: запускать службу вместе с системой. На macOS не используется. */
|
||||||
autoStart: boolean | null
|
autoStart: boolean | null
|
||||||
@@ -25,35 +20,21 @@ export interface AppSettings {
|
|||||||
|
|
||||||
const DEFAULTS: AppSettings = { strategyId: null, autoStart: null }
|
const DEFAULTS: AppSettings = { strategyId: null, autoStart: null }
|
||||||
|
|
||||||
/**
|
|
||||||
* Кэш в памяти нужен не для скорости, а чтобы писать только при реальном изменении:
|
|
||||||
* `rememberPreferences` дёргается из `ZapretEngine.patch()`, то есть на каждое изменение
|
|
||||||
* статуса, включая промежуточные `starting`/`stopping`.
|
|
||||||
*/
|
|
||||||
let cache: AppSettings = { ...DEFAULTS }
|
let cache: AppSettings = { ...DEFAULTS }
|
||||||
|
|
||||||
/**
|
/** До первой загрузки не пишем: иначе движок затёр бы файл умолчаниями. */
|
||||||
* До первой загрузки не пишем ничего: иначе движок, успевший обновить статус раньше
|
|
||||||
* `loadSettings()`, затёр бы файл значениями по умолчанию.
|
|
||||||
*/
|
|
||||||
let loaded = false
|
let loaded = false
|
||||||
|
|
||||||
/** Записи выстраиваем в очередь — параллельные rename в один файл друг друга обгоняют. */
|
/** Очередь, чтобы два быстрых переключения не переставили rename местами. */
|
||||||
let writeQueue: Promise<void> = Promise.resolve()
|
let writeQueue: Promise<void> = Promise.resolve()
|
||||||
|
|
||||||
function settingsPath(): string {
|
function settingsPath(): string {
|
||||||
return join(app.getPath('userData'), 'settings.json')
|
return join(app.getPath('userData'), 'settings.json')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Файл могли испортить или он остался от версии с другими полями — непонятное отбрасываем. */
|
||||||
* Файл мог быть испорчен (аварийное завершение на записи) или остаться от версии с другим
|
|
||||||
* набором полей — читаем его как чужие данные, а не как свои: всё непонятное отбрасываем
|
|
||||||
* и продолжаем с умолчаниями, а не падаем на старте.
|
|
||||||
*/
|
|
||||||
function sanitize(raw: unknown): AppSettings {
|
function sanitize(raw: unknown): AppSettings {
|
||||||
if (typeof raw !== 'object' || raw === null) return { ...DEFAULTS }
|
const value = (typeof raw === 'object' && raw !== null ? raw : {}) as Record<string, unknown>
|
||||||
const value = raw as Record<string, unknown>
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
strategyId: typeof value.strategyId === 'string' && value.strategyId ? value.strategyId : null,
|
strategyId: typeof value.strategyId === 'string' && value.strategyId ? value.strategyId : null,
|
||||||
autoStart: typeof value.autoStart === 'boolean' ? value.autoStart : null
|
autoStart: typeof value.autoStart === 'boolean' ? value.autoStart : null
|
||||||
@@ -65,8 +46,7 @@ export async function loadSettings(): Promise<AppSettings> {
|
|||||||
try {
|
try {
|
||||||
cache = sanitize(JSON.parse(await readFile(settingsPath(), 'utf8')))
|
cache = sanitize(JSON.parse(await readFile(settingsPath(), 'utf8')))
|
||||||
} catch {
|
} catch {
|
||||||
// Файла ещё нет (первый запуск) или он нечитаем — начинаем с умолчаний.
|
cache = { ...DEFAULTS } // первый запуск или нечитаемый файл
|
||||||
cache = { ...DEFAULTS }
|
|
||||||
}
|
}
|
||||||
loaded = true
|
loaded = true
|
||||||
return { ...cache }
|
return { ...cache }
|
||||||
@@ -76,30 +56,21 @@ export async function loadSettings(): Promise<AppSettings> {
|
|||||||
async function writeSettings(settings: AppSettings): Promise<void> {
|
async function writeSettings(settings: AppSettings): Promise<void> {
|
||||||
const path = settingsPath()
|
const path = settingsPath()
|
||||||
const tmp = `${path}.${randomUUID()}.tmp`
|
const tmp = `${path}.${randomUUID()}.tmp`
|
||||||
try {
|
await writeFile(tmp, `${JSON.stringify(settings, null, 2)}\n`, 'utf8')
|
||||||
await writeFile(tmp, `${JSON.stringify(settings, null, 2)}\n`, 'utf8')
|
await rename(tmp, path)
|
||||||
await rename(tmp, path)
|
|
||||||
} catch (error) {
|
|
||||||
// Не смогли сохранить выбор — это неприятно, но не повод ломать работающий обход.
|
|
||||||
console.warn('[settings] не удалось сохранить настройки:', error)
|
|
||||||
await unlink(tmp).catch(() => {})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Запоминает выбор пользователя. Вызывается из `ZapretEngine.patch()` на каждое изменение
|
* Вызывается из `ZapretEngine.patch()`, то есть на каждое изменение статуса — отсюда
|
||||||
* статуса, поэтому пишет на диск только когда что-то из сохраняемого действительно
|
* сравнение с прошлым значением. Записи не ждём: интерфейс не должен зависеть от диска,
|
||||||
* поменялось, и не ждёт записи: статус в интерфейсе не должен зависеть от файловой системы.
|
* а не сохранившийся выбор не повод ломать работающий обход.
|
||||||
*/
|
*/
|
||||||
export function rememberPreferences(preferences: Partial<AppSettings>): void {
|
export function rememberPreferences(settings: AppSettings): void {
|
||||||
if (!loaded) return
|
if (!loaded) return
|
||||||
|
if (settings.strategyId === cache.strategyId && settings.autoStart === cache.autoStart) return
|
||||||
|
|
||||||
const next: AppSettings = {
|
cache = settings
|
||||||
strategyId: preferences.strategyId ?? cache.strategyId,
|
writeQueue = writeQueue
|
||||||
autoStart: preferences.autoStart ?? cache.autoStart
|
.then(() => writeSettings(settings))
|
||||||
}
|
.catch((error: unknown) => console.warn('[settings] не удалось сохранить:', error))
|
||||||
if (next.strategyId === cache.strategyId && next.autoStart === cache.autoStart) return
|
|
||||||
|
|
||||||
cache = next
|
|
||||||
writeQueue = writeQueue.then(() => writeSettings(next))
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user