1 Commits
Author SHA1 Message Date
maeneko 2126d2b3ef Windows: одна кнопка автозапуска и версия 0.6.2
CI / Линт и типы (push) Canceled after 0s
CI / Сборка (Windows) (push) Canceled after 0s
CI / Сборка (Linux) (push) Canceled after 0s
CI / Сборка (macOS) (push) Canceled after 0s
На Windows в настройках было две похожие кнопки: автозапуск службы обхода
и автозапуск самого приложения (задача планировщика). Теперь это одна кнопка
«Запускать вместе с Windows»: при загрузке поднимается служба, а SenBoost
открывается свёрнутым в трей, без окна.

Задача планировщика больше не переключается отдельно — followZapretAutoStart
ставит и снимает её вслед за автозапуском службы (без лишнего UAC: приложение
и так работает с правами администратора). На macOS и Linux ничего не меняется.
2026-09-11 02:54:04 +03:00
7 changed files with 90 additions and 28 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "senboost", "name": "senboost",
"version": "0.6.1", "version": "0.6.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "senboost", "name": "senboost",
"version": "0.6.1", "version": "0.6.2",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "senboost", "name": "senboost",
"version": "0.6.1", "version": "0.6.2",
"description": "Кроссплатформенное десктопное приложение на Electron + React + TypeScript", "description": "Кроссплатформенное десктопное приложение на Electron + React + TypeScript",
"main": "./out/main/index.js", "main": "./out/main/index.js",
"desktopName": "senboost.desktop", "desktopName": "senboost.desktop",
+43
View File
@@ -1,6 +1,9 @@
import { app } from 'electron' import { app } from 'electron'
import type { ZapretStatus } from '../shared/ipc-contract'
import * as linux from './app-autostart.linux' import * as linux from './app-autostart.linux'
import * as win32 from './app-autostart.win32' import * as win32 from './app-autostart.win32'
import { isTrayActive } from './tray'
import { onZapretStatusChanged, zapretStatus } from './zapret'
/** /**
* Автозапуск САМОГО приложения вместе с системой — не путать с автозапуском обхода * Автозапуск САМОГО приложения вместе с системой — не путать с автозапуском обхода
@@ -8,6 +11,9 @@ import * as win32 from './app-autostart.win32'
* приложения, а этот открывает приложение свёрнутым в трей, чтобы обход можно было включить * приложения, а этот открывает приложение свёрнутым в трей, чтобы обход можно было включить
* и выключить одним кликом. * и выключить одним кликом.
* *
* На Windows отдельного переключателя у него нет — он следует за автозапуском службы
* (см. `followZapretAutoStart`), в настройках одна кнопка «Запускать вместе с Windows».
*
* Источник истины — сама система (реестр/планировщик, SMAppService, каталог автозапуска), * Источник истины — сама система (реестр/планировщик, SMAppService, каталог автозапуска),
* в settings.json ничего не дублируем: после переустановки системы или приложения * в settings.json ничего не дублируем: после переустановки системы или приложения
* сохранённое значение всё равно разошлось бы с реальностью. * сохранённое значение всё равно разошлось бы с реальностью.
@@ -43,6 +49,43 @@ export async function setAppAutoStart(enabled: boolean): Promise<void> {
} }
} }
/**
* Windows: служба обхода стартует при загрузке — значит, и приложение должно подняться
* при входе, свёрнутым в трей, чтобы обход было чем выключить. Две кнопки для этого путали:
* обе назывались «запускать вместе с Windows», а по отдельности смысла почти не имели.
*
* Задача планировщика существует ровно тогда, когда служба установлена и стоит на `Auto`.
* Приложение и так работает с правами администратора, так что лишнего UAC это не добавляет —
* пользователь видит один запрос, на смену типа запуска службы. Подписка на статус, а не
* вызов из IPC: обход включают и из меню трея, и первое включение ставит службу уже с `Auto`.
*
* Вызывать после `recoverZapret()` — до него статус ещё не знает о службе.
*/
export function followZapretAutoStart(): void {
if (process.platform !== 'win32') return
let applied: boolean | null = null
let queue = Promise.resolve()
const sync = (status: ZapretStatus): void => {
const wanted = status.serviceInstalled && status.autoStart && isTrayActive()
if (wanted === applied) return
applied = wanted
queue = queue
.then(async () => {
if ((await win32.isAppAutoStartEnabled()) !== wanted) await win32.setAppAutoStart(wanted)
})
.catch((error) => {
// Главное — служба, она уже настроена. Без задачи обход всё равно поднимется при
// загрузке, просто без значка в трее; в dev-режиме сюда попадаем всегда (нет прав).
console.warn('[autostart] не удалось синхронизировать задачу планировщика:', error)
})
}
onZapretStatusChanged(sync)
sync(zapretStatus())
}
/** /**
* Запуск пришёл из автозапуска — окно показывать не нужно, приложение садится в трей. * Запуск пришёл из автозапуска — окно показывать не нужно, приложение садится в трей.
* *
+3
View File
@@ -13,6 +13,9 @@ const TASK_NAME = 'SenBoost Autostart'
* (`app.setLoginItemSettings`): exe собран с `requestedExecutionLevel: requireAdministrator` * (`app.setLoginItemSettings`): exe собран с `requestedExecutionLevel: requireAdministrator`
* (см. electron-builder.yml), а UAC при входе в систему не спрашивают — приложение из ключа * (см. electron-builder.yml), а UAC при входе в систему не спрашивают — приложение из ключа
* реестра просто не запустилось бы. Задача с `/RL HIGHEST` поднимает его с правами сразу. * реестра просто не запустилось бы. Задача с `/RL HIGHEST` поднимает его с правами сразу.
*
* Сама по себе из интерфейса не переключается: её ставит и снимает `followZapretAutoStart`
* (`app-autostart.ts`) вслед за автозапуском службы обхода.
*/ */
function taskCommand(): string { function taskCommand(): string {
// В dev первым аргументом electron.exe нужен каталог приложения, в сборке — нет. // В dev первым аргументом electron.exe нужен каталог приложения, в сборке — нет.
+2 -1
View File
@@ -1,6 +1,6 @@
import { app } from 'electron' import { app } from 'electron'
import { electronApp, optimizer } from '@electron-toolkit/utils' import { electronApp, optimizer } from '@electron-toolkit/utils'
import { shouldStartHidden } from './app-autostart' import { followZapretAutoStart, shouldStartHidden } from './app-autostart'
import { beginQuit, createWindow, showMainWindow } from './window' import { beginQuit, createWindow, showMainWindow } from './window'
import { showFirstRunSupportDialog } from './first-run' import { showFirstRunSupportDialog } from './first-run'
import { registerIpcHandlers } from './ipc' import { registerIpcHandlers } from './ipc'
@@ -61,6 +61,7 @@ if (elevatedTask) {
// На macOS чинит прокси, оставшийся от аварийно завершённого прошлого запуска, // На macOS чинит прокси, оставшийся от аварийно завершённого прошлого запуска,
// до того как окно вообще откроется. // до того как окно вообще откроется.
void recoverZapret().then(() => { void recoverZapret().then(() => {
followZapretAutoStart()
const window = createWindow({ show: !startHidden }) const window = createWindow({ show: !startHidden })
// Ждём первого кадра: диалог должен появиться поверх готового окна, а не пустого. // Ждём первого кадра: диалог должен появиться поверх готового окна, а не пустого.
// Свёрнутому в трей приложению показывать модальное окно некуда — пользователь его // Свёрнутому в трей приложению показывать модальное окно некуда — пользователь его
+31 -22
View File
@@ -8,7 +8,8 @@ import { LIST_TITLES } from '../lib/zapretListMeta'
import ListEditor from './ListEditor' import ListEditor from './ListEditor'
/** /**
* Настройки: списки сайтов + автозапуск (macOS/Windows). Выбор стратегии и проверка * Настройки: списки сайтов + автозапуск (macOS/Windows; на Windows одна кнопка на службу
* и приложение в трее). Выбор стратегии и проверка
* соединения переехали на главный экран (ZapretCard.tsx) — рядом с кнопкой * соединения переехали на главный экран (ZapretCard.tsx) — рядом с кнопкой
* включения, где ими и пользуются. Отдельный экран за шестерёнкой в шапке. * включения, где ими и пользуются. Отдельный экран за шестерёнкой в шапке.
*/ */
@@ -72,33 +73,40 @@ export default function SettingsView({
{background.settings.closeToTray ? 'Включено' : 'Выключено'} {background.settings.closeToTray ? 'Включено' : 'Выключено'}
</span> </span>
</button> </button>
<button {/* На Windows автозапуск приложения идёт в комплекте с автозапуском службы —
type="button" одна кнопка ниже (см. followZapretAutoStart в main). */}
className="settings-row" {!isWindows && (
disabled={background.busy} <button
onClick={() => type="button"
void background.save({ launchAtLogin: !background.settings?.launchAtLogin }) className="settings-row"
} disabled={background.busy}
> onClick={() =>
<span>Запускать приложение вместе с системой</span> void background.save({ launchAtLogin: !background.settings?.launchAtLogin })
<span }
className={`chip ${background.settings.launchAtLogin ? 'chip--success' : 'chip--neutral'}`}
> >
{background.settings.launchAtLogin ? 'Включён' : 'Выключен'} <span>Запускать приложение вместе с системой</span>
</span> <span
</button> className={`chip ${background.settings.launchAtLogin ? 'chip--success' : 'chip--neutral'}`}
>
{background.settings.launchAtLogin ? 'Включён' : 'Выключен'}
</span>
</button>
)}
{background.error && <p className="list-editor__error">{background.error}</p>} {background.error && <p className="list-editor__error">{background.error}</p>}
<p className="list-editor__hint"> <p className="list-editor__hint">
Приложение открывается свёрнутым в трей и обход можно включить одним кликом по значку. {isWindows
Это не то же самое, что автозапуск обхода ниже: тот работает вообще без запущенного ? 'Закрытое окно прячется в трей, обход продолжает работать. Включить и ' +
приложения. 'выключить его можно одним кликом по значку.'
: 'Приложение открывается свёрнутым в трей и обход можно включить одним кликом ' +
'по значку. Это не то же самое, что автозапуск обхода ниже: тот работает ' +
'вообще без запущенного приложения.'}
</p> </p>
</div> </div>
)} )}
{supportsAutoStart && ( {supportsAutoStart && (
<div className="settings__group"> <div className="settings__group">
<span className="settings__label">Автозапуск обхода</span> <span className="settings__label">{isWindows ? 'Автозапуск' : 'Автозапуск обхода'}</span>
<button <button
type="button" type="button"
className="settings-row" className="settings-row"
@@ -111,9 +119,10 @@ export default function SettingsView({
</button> </button>
<p className="list-editor__hint"> <p className="list-editor__hint">
{isWindows {isWindows
? 'Меняется через запрос прав администратора. Если выключить обход кнопкой на ' + ? 'Обход включается при загрузке Windows, а SenBoost открывается свёрнутым в ' +
'главном экране, при включённом автозапуске он вернётся не раньше следующей ' + 'трей, без окна. Меняется через запрос прав администратора. Если выключить ' +
'перезагрузки.' 'обход кнопкой на главном экране, при включённом автозапуске он вернётся ' +
'не раньше следующей перезагрузки.'
: 'Меняется через пароль администратора. Действует, пока обход включён: ' + : 'Меняется через пароль администратора. Действует, пока обход включён: ' +
'выключение кнопкой на главном экране полностью снимает установку — ' + 'выключение кнопкой на главном экране полностью снимает установку — ' +
'автозапуск снова понадобится включить вместе с обходом.'} 'автозапуск снова понадобится включить вместе с обходом.'}
+8 -2
View File
@@ -28,7 +28,10 @@ export interface ThemeState {
export interface BackgroundSettings { export interface BackgroundSettings {
/** Закрытие окна прячет его в трей, а не завершает приложение. */ /** Закрытие окна прячет его в трей, а не завершает приложение. */
closeToTray: boolean closeToTray: boolean
/** Приложение запускается вместе с системой, сразу свёрнутым в трей. */ /**
* Приложение запускается вместе с системой, сразу свёрнутым в трей. На Windows только
* для чтения: следует за `ZapretStatus.autoStart` (см. `followZapretAutoStart`).
*/
launchAtLogin: boolean launchAtLogin: boolean
/** /**
* Иконка в трее реально создалась. На Linux без AppIndicator трея нет — тогда прятать * Иконка в трее реально создалась. На Linux без AppIndicator трея нет — тогда прятать
@@ -149,7 +152,10 @@ export interface IpcHandlers {
* nftables (потребуется пароль через pkexec). * nftables (потребуется пароль через pkexec).
*/ */
'zapret:set-strategy': (strategyId: string) => ZapretStatus 'zapret:set-strategy': (strategyId: string) => ZapretStatus
/** macOS/Windows: автозапуск с системой; требует пароль администратора/UAC. */ /**
* macOS/Windows: автозапуск с системой; требует пароль администратора/UAC. На Windows
* вместе со службой при входе поднимается и приложение, свёрнутым в трей.
*/
'zapret:set-autostart': (enabled: boolean) => ZapretStatus 'zapret:set-autostart': (enabled: boolean) => ZapretStatus
'zapret:lists': () => ZapretList[] 'zapret:lists': () => ZapretList[]