Сохранять выбор стратегии и автозапуска между запусками

Настройки лежат в settings.json (userData) и восстанавливаются до открытия
окна. Списки сайтов уже хранятся в userData/zapret-lists и отдельного
сохранения не требуют.
This commit is contained in:
2026-09-03 07:57:12 +03:00
parent 563d8b7292
commit cb499a09d1
+20 -49
View File
@@ -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))
} }