chuijs 4.0.1 → 4.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/README.md +40 -28
- package/framework/appLayout/default/chui_app_layout.js +6 -13
- package/framework/appLayout/default/global_style.css +8 -5
- package/framework/modules/chui_bridge/preload.js +22 -3
- package/framework/modules/chui_page/styles.css +3 -26
- package/framework/modules/chui_services/chui_channels.js +51 -0
- package/framework/modules/chui_services/desktop.js +126 -8
- package/index.js +66 -44
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,58 @@
|
|
|
3
3
|
Формат — [Keep a Changelog](https://keepachangelog.com/ru/1.1.0/),
|
|
4
4
|
версии — [Semantic Versioning](https://semver.org/lang/ru/).
|
|
5
5
|
|
|
6
|
+
## [4.0.2] - 2026-09-29
|
|
7
|
+
|
|
8
|
+
### Исправлено
|
|
9
|
+
|
|
10
|
+
- **Заголовок страницы вернулся в шапку.** `Route#go()` больше не подмешивает название в
|
|
11
|
+
содержимое: узел `page_title` (появился в 4.0.1) и его стили удалены. Он дублировал заголовок,
|
|
12
|
+
который страница рисует сама, а `Page#render()` отдаёт один и тот же узел — повторный переход
|
|
13
|
+
на ту же страницу (кнопка «Назад» из `Page#setBackButton()`) добавлял второй заголовок.
|
|
14
|
+
Название показывает узел `page_name` в шапке, у страницы с меню-баром он по-прежнему скрыт —
|
|
15
|
+
место занимает полоса меню.
|
|
16
|
+
|
|
17
|
+
### Изменено
|
|
18
|
+
|
|
19
|
+
- **`Desktop.app.path()` отвечает значением, а не промисом.** Из путей страница собирает свои
|
|
20
|
+
(файлы, логи, кеш) на старте, где `await` пришлось бы протаскивать через весь код инициализации.
|
|
21
|
+
Путь приходит по синхронному каналу (`SERVICES_SYNC_CALLS`: `ipcRenderer.sendSync` в странице,
|
|
22
|
+
`ipcMain.on` + `event.returnValue` в main) и кешируется — за одним именем в main ходят один раз.
|
|
23
|
+
Ответ идёт конвертом `{value}`/`{error}`: у `sendSync` нет канала для исключения, поэтому отказ
|
|
24
|
+
(имя вне белого списка) приходит странице обычной ошибкой с текстом main. Прежний
|
|
25
|
+
`await Desktop.app.path(...)` продолжает работать — `await` на строке возвращает ту же строку.
|
|
26
|
+
Асинхронный канал `APP_PATH` оставлен: его ключ есть в карте `window.chui.services`.
|
|
27
|
+
|
|
28
|
+
### Добавлено
|
|
29
|
+
|
|
30
|
+
- **`Desktop.app.quit()` и `Desktop.app.restart()`** — закрытие и перезапуск приложения со страницы,
|
|
31
|
+
в том числе из безопасного окна (`window.chui.desktop.app.quit()`). Идут через `Main.stop()`/
|
|
32
|
+
`Main.restart()`, то есть теми же шагами, что пункт меню «Выход»: снимаются обработчики сервисов,
|
|
33
|
+
глобальные клавиши и подписки, а не просто закрывается окно. Отвечают значением, а не промисом
|
|
34
|
+
(синхронные каналы `appQuitSync`/`appRestartSync`), поэтому `await` не обязателен; выход
|
|
35
|
+
откладывается `setImmediate`, чтобы ответ успел уйти странице. Асинхронные каналы
|
|
36
|
+
`APP_QUIT`/`APP_RESTART` оставлены: их ключи есть в карте `window.chui.services`.
|
|
37
|
+
- Тесты `test/chui_services_sync.test.js` и `test/chui_app_lifecycle.test.js`: сверяют синхронные
|
|
38
|
+
каналы (main ↔ мост preload ↔ страница) и то, что закрытие с перезапуском идут именно через
|
|
39
|
+
`Main.stop()`/`Main.restart()`.
|
|
40
|
+
|
|
41
|
+
### Удалено
|
|
42
|
+
|
|
43
|
+
- **Класс `App` и вспомогательный `Application` удалены.** `App.get()`, `App.getSession()`,
|
|
44
|
+
`App.getWebContents()` и все `App.*Path()` больше не экспортируются. Пути теперь отдаёт
|
|
45
|
+
`Desktop.app.path(name)` — и на странице, и в main-процессе: там канала нет, поэтому путь
|
|
46
|
+
берётся у Electron напрямую с той же проверкой белого списка `APP_PATH_NAMES`. Сырые объекты
|
|
47
|
+
Electron в main отдают `Desktop.app.get()`, `Desktop.app.getSession()` и
|
|
48
|
+
`Desktop.app.getWebContents()`; со страницы они бросают ошибку с подсказкой (`Desktop.session.*`,
|
|
49
|
+
`Desktop.dialog` или свой канал из `security.channels`).
|
|
50
|
+
**Ломающее:** `const { App } = require('chuijs')` перестаёт работать.
|
|
51
|
+
- Тест `test/chui_app_paths.test.js` переписан: сверяет белый список с ожидаемым набором имён
|
|
52
|
+
путей и проверяет оба валидатора — `appPathByName` (`index.js`) и `#appPathInMain` (`desktop.js`).
|
|
53
|
+
|
|
54
|
+
### Пакет
|
|
55
|
+
|
|
56
|
+
- Версия 4.0.2.
|
|
57
|
+
|
|
6
58
|
## [4.0.1] - 2026-09-28
|
|
7
59
|
|
|
8
60
|
### Добавлено
|
package/README.md
CHANGED
|
@@ -60,7 +60,7 @@ render(() => new App()).catch(err => Log.error(err))
|
|
|
60
60
|
#### exampleApp / main.js
|
|
61
61
|
```javascript
|
|
62
62
|
/** main.js */
|
|
63
|
-
const { Main, MenuItem, path,
|
|
63
|
+
const { Main, MenuItem, path, Desktop } = require('chuijs');
|
|
64
64
|
const main = new Main({
|
|
65
65
|
name: "exampleApp",
|
|
66
66
|
// размеры окна задаются вложенным объектом sizes
|
|
@@ -73,9 +73,9 @@ const main = new Main({
|
|
|
73
73
|
render: `${__dirname}/app/app.js`,
|
|
74
74
|
devTools: false,
|
|
75
75
|
resizable: true,
|
|
76
|
-
// папка
|
|
76
|
+
// папка загрузок: если не задана, путь выбирает Electron
|
|
77
77
|
paths: {
|
|
78
|
-
downloadPath: path.join(
|
|
78
|
+
downloadPath: path.join(Desktop.app.path("userData"), "downloads")
|
|
79
79
|
}
|
|
80
80
|
/** icon: `${__dirname}/resources/icons/app/icon.png` */
|
|
81
81
|
});
|
|
@@ -95,8 +95,9 @@ main.start({
|
|
|
95
95
|
> поэтому окно доступно через `main.getWindow()` только после старта.
|
|
96
96
|
#### Download Session в main.js
|
|
97
97
|
```javascript
|
|
98
|
-
const {
|
|
99
|
-
|
|
98
|
+
const { Desktop } = require('chuijs');
|
|
99
|
+
const app = Desktop.app.get(); // сырой `app` — только в main-процессе
|
|
100
|
+
app.on('session-created', (session) => {
|
|
100
101
|
session.on('will-download', (e, item, contents) => {
|
|
101
102
|
// ...
|
|
102
103
|
});
|
|
@@ -124,14 +125,14 @@ async function run() {
|
|
|
124
125
|
Создаёт окно, трей и управляет жизненным циклом приложения.
|
|
125
126
|
|
|
126
127
|
```javascript
|
|
127
|
-
const { Main, MenuItem,
|
|
128
|
+
const { Main, MenuItem, Desktop } = require('chuijs');
|
|
128
129
|
const main = new Main({
|
|
129
130
|
name: 'exampleApp',
|
|
130
131
|
sizes: { width: 1366, height: 768, minWidth: 960, minHeight: 540 },
|
|
131
132
|
render: `${__dirname}/app/app.js`,
|
|
132
133
|
devTools: false,
|
|
133
134
|
resizable: true,
|
|
134
|
-
paths: { downloadPath: path.join(
|
|
135
|
+
paths: { downloadPath: path.join(Desktop.app.path('userData'), 'downloads') }
|
|
135
136
|
});
|
|
136
137
|
main.start({ hideOnClose: false, tray: [new MenuItem().quit('Выход')] });
|
|
137
138
|
```
|
|
@@ -178,32 +179,37 @@ main.getWindows(); // [{name, window}, …]
|
|
|
178
179
|
`stop()` и `restart()` снимают свои обработчики: IPC (`show_system_notification`, `SEND_LOG_TEXT`,
|
|
179
180
|
`updateInstallConfirm`), обработчики десктоп-сервисов вместе с глобальными клавишами,
|
|
180
181
|
подписку апдейтера и глобальные обработчики ошибок процессов —
|
|
181
|
-
повторный `start()` подпишет их заново по одному разу.
|
|
182
|
+
повторный `start()` подпишет их заново по одному разу. Со страницы то же делают
|
|
183
|
+
`Desktop.app.quit()` и `Desktop.app.restart()`: страница — код самого приложения, поэтому своей
|
|
184
|
+
кнопке «Выход» не нужен отдельный канал в каждом проекте. Оба отвечают значением, а не промисом
|
|
185
|
+
(синхронный канал, как у `Desktop.app.path`), поэтому `await` не обязателен.
|
|
182
186
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
(Electron не рекомендует `remote`), и её `App.get*` бросает ошибку с подсказкой. Со страницы
|
|
186
|
-
берут `Desktop.app`, `Desktop.session`, `Desktop.dialog` или свой канал из `security.channels`.
|
|
187
|
+
Пути приложения отдаёт `Desktop.app.path(name)` — и в main-процессе, и на странице, значением
|
|
188
|
+
без промиса (на странице — синхронным каналом, в main — прямо у Electron):
|
|
187
189
|
|
|
188
190
|
```javascript
|
|
189
|
-
const {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
191
|
+
const { Desktop } = require('chuijs');
|
|
192
|
+
Desktop.app.path('userData'); // .../AppData/<name>
|
|
193
|
+
Desktop.app.path('downloads');
|
|
194
|
+
Desktop.app.path('logs');
|
|
193
195
|
```
|
|
194
196
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
`
|
|
197
|
+
Имена берутся из белого списка: `home`, `appData`, `userData`, `sessionData`, `temp`, `exe`,
|
|
198
|
+
`module`, `desktop`, `documents`, `downloads`, `music`, `pictures`, `videos`, `logs`, `recent`,
|
|
199
|
+
`crashDumps`; всё, что вне списка, отклоняется. Со страницы то же значение доступно как
|
|
200
|
+
`window.chui.desktop.app.path('userData')`.
|
|
201
|
+
|
|
202
|
+
Сырые объекты Electron (`app`, `session`, `webContents`) странице не отдают — Electron не
|
|
203
|
+
рекомендует `remote`. В main-процессе их отдают `Desktop.app.get()`, `Desktop.app.getSession()`
|
|
204
|
+
и `Desktop.app.getWebContents()`:
|
|
198
205
|
|
|
199
206
|
```javascript
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
App.downloadsPath();
|
|
203
|
-
App.logsPath();
|
|
207
|
+
const app = Desktop.app.get(); // main.js
|
|
208
|
+
app.on('session-created', (session) => { /* … */ });
|
|
204
209
|
```
|
|
205
210
|
|
|
206
|
-
|
|
211
|
+
Со страницы эти методы бросают ошибку с подсказкой: вместо них берут `Desktop.session.*`,
|
|
212
|
+
`Desktop.dialog` или свой канал из `security.channels`.
|
|
207
213
|
|
|
208
214
|
#### `Log`
|
|
209
215
|
Пишет в `userData/logs/app_<date>.log`.
|
|
@@ -280,7 +286,8 @@ await bot.sendMessage({ chat_id: '123', text: 'привет' });
|
|
|
280
286
|
доступны только там), а страница работает с ними через `Desktop`: он сам выбирает транспорт —
|
|
281
287
|
канал `ipcRenderer.invoke` в обычном режиме или методы `window.chui.services` (preload +
|
|
282
288
|
`contextBridge`) в безопасном. Имена каналов лежат в одном модуле `chui_services/chui_channels`,
|
|
283
|
-
поэтому main и рендерер не расходятся.
|
|
289
|
+
поэтому main и рендерер не расходятся. Пути приложения — единственное исключение: они идут
|
|
290
|
+
синхронным каналом (`Desktop.app.path` отвечает значением, а не промисом, см. ниже).
|
|
284
291
|
|
|
285
292
|
```javascript
|
|
286
293
|
const { Desktop } = require('chuijs');
|
|
@@ -302,8 +309,11 @@ offTheme();
|
|
|
302
309
|
await Desktop.print.page(); // системный диалог печати текущего окна
|
|
303
310
|
await Desktop.print.html('<h1>Отчёт</h1>'); // печать HTML из скрытого окна
|
|
304
311
|
await Desktop.print.pdf(html, {path: '/tmp/отчёт.pdf'}); // PDF без диалога (или с ним, без path)
|
|
305
|
-
|
|
312
|
+
const userData = Desktop.app.path('userData'); // каталог настроек приложения, сразу
|
|
306
313
|
await Desktop.app.info(); // {name, version, locale, isPackaged}
|
|
314
|
+
const app = Desktop.app.get(); // сырой `app` — только в main-процессе
|
|
315
|
+
Desktop.app.quit(); // выход: тот же, что у пункта меню «Выход»
|
|
316
|
+
Desktop.app.restart(); // перезапуск процесса (состояние не переносится)
|
|
307
317
|
const {canceled, filePaths} = await Desktop.dialog.open({properties: ['openFile']});
|
|
308
318
|
await Desktop.dialog.save({defaultPath: '/tmp/отчёт.pdf'});
|
|
309
319
|
await Desktop.dialog.message({type: 'question', message: 'Продолжить?'});
|
|
@@ -419,8 +429,10 @@ window.chui.desktop.clipboard.readText(); // то же, что Desktop из re
|
|
|
419
429
|
window.chui.desktop.recent.list();
|
|
420
430
|
window.chui.desktop.progress.set(0.4);
|
|
421
431
|
const off = window.chui.desktop.shortcuts.on('Mod+Shift+P', () => {});
|
|
422
|
-
|
|
432
|
+
window.chui.desktop.app.path('userData'); // путь приложения — сразу, без промиса
|
|
423
433
|
await window.chui.desktop.app.info(); // {name, version, locale, isPackaged}
|
|
434
|
+
window.chui.desktop.app.quit(); // закрыть всё приложение (окно — window.chui.window)
|
|
435
|
+
window.chui.desktop.app.restart(); // перезапустить приложение (тоже без промиса)
|
|
424
436
|
const res = await window.chui.desktop.dialog.open({properties: ['openFile']});
|
|
425
437
|
await window.chui.desktop.dialog.message({type: 'question', message: 'Продолжить?'});
|
|
426
438
|
await window.chui.desktop.session.clearCache();
|
|
@@ -644,7 +656,7 @@ DOM-элемент, поэтому внутри `Page.add(...)`, `Card.add(...)`
|
|
|
644
656
|
[Константы и статические фабрики](docs/topics/constants.html), [Сценарии целиком](docs/topics/scenarios.html),
|
|
645
657
|
[Витрина компонентов](docs/topics/showcase.html).
|
|
646
658
|
|
|
647
|
-
Модули и десктоп-сервисы (`Main`, `
|
|
659
|
+
Модули и десктоп-сервисы (`Main`, `I18n`, `Theme`, `Desktop`, `Log`) описаны выше, в разделе «Модули».
|
|
648
660
|
|
|
649
661
|
### В разработке
|
|
650
662
|
|
|
@@ -89,19 +89,12 @@ class Route extends Events {
|
|
|
89
89
|
//
|
|
90
90
|
setPageName(page.getTitle());
|
|
91
91
|
center.innerHTML = '';
|
|
92
|
-
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const block = document.createElement("page_title");
|
|
99
|
-
const icon = page.getIcon();
|
|
100
|
-
if (icon !== undefined && icon !== null) block.innerHTML = String(icon);
|
|
101
|
-
block.appendChild(document.createTextNode(String(title)));
|
|
102
|
-
page_node.prepend(block);
|
|
103
|
-
}
|
|
104
|
-
center.appendChild(page_node);
|
|
92
|
+
// Содержимое собирает сама страница (`Page#render`), оболочка в него не подмешивает
|
|
93
|
+
// ничего: узел `page_title` (был здесь) дублировал заголовок, который страница
|
|
94
|
+
// рисует сама, а `Page#render` отдаёт один и тот же узел — повторный переход на тот
|
|
95
|
+
// же экземпляр страницы (кнопка «Назад» из `Page#setBackButton`) добавлял второй
|
|
96
|
+
// заголовок. Место названия — шапка (`setPageName`).
|
|
97
|
+
center.appendChild(page.render());
|
|
105
98
|
const _page = center.querySelector('page');
|
|
106
99
|
new Animation(_page).fadeIn();
|
|
107
100
|
// Стирания `center.removeAttribute("style")` здесь больше нет: инлайновых
|
|
@@ -884,11 +884,14 @@ app_menu_empty {
|
|
|
884
884
|
ниже содержимого, и длинное название выдавило бы кнопки за край окна. Полный текст
|
|
885
885
|
лежит в подсказке (её ставит setPageName в chui_app_layout.js). */
|
|
886
886
|
page_name {
|
|
887
|
-
/* Название
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
887
|
+
/* Название показывает шапка, а не содержимое: оболочка в страницу его не подмешивает
|
|
888
|
+
(`Route#go`), поэтому приложение, рисующее свой заголовок, ничего не дублирует.
|
|
889
|
+
Узел — якорь полосы меню (`applyPageMenuBar` ставит её следом), и он же держит текст
|
|
890
|
+
для меню маршрутов и подсказки (`setPageName`).
|
|
891
|
+
У страницы с меню-баром название скрыто — место занимает полоса (правило
|
|
892
|
+
`.header_with_menu_bar page_name` ниже) */
|
|
893
|
+
display: flex;
|
|
894
|
+
align-items: center;
|
|
892
895
|
outline: none;
|
|
893
896
|
box-sizing: border-box;
|
|
894
897
|
min-width: 0;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const {contextBridge, ipcRenderer} = require("electron");
|
|
2
|
-
const {SERVICES_CHANNELS, SERVICES_CALLS, BRIDGE_CHANNELS, BRIDGE_ARGS} = require("../chui_services/chui_channels");
|
|
2
|
+
const {SERVICES_CHANNELS, SERVICES_CALLS, SERVICES_SYNC_CALLS, BRIDGE_CHANNELS, BRIDGE_ARGS} = require("../chui_services/chui_channels");
|
|
3
3
|
// Мост переиспользует ту же обёртку сервисов, что и страница: разница только в транспорте
|
|
4
4
|
// (`Desktop` в preload'е сам вызовет ipcRenderer, потому что `window.chui` ему не виден)
|
|
5
5
|
const {Desktop} = require("../chui_services/desktop");
|
|
@@ -16,8 +16,10 @@ const {Desktop} = require("../chui_services/desktop");
|
|
|
16
16
|
* файл при `DOMContentLoaded`.
|
|
17
17
|
*
|
|
18
18
|
* Формы API:
|
|
19
|
-
* - `window.chui.desktop.{clipboard, recent, progress, shortcuts}` — то же, что `Desktop` из
|
|
19
|
+
* - `window.chui.desktop.{app, clipboard, recent, progress, shortcuts}` — то же, что `Desktop` из
|
|
20
20
|
* `require('chuijs')`, поэтому код страницы переносится между режимами почти без правок;
|
|
21
|
+
* `desktop.app.quit()`/`restart()` закрывают и перезапускают всё приложение, а не окно; ответ
|
|
22
|
+
* у них значением, без промиса (синхронный канал);
|
|
21
23
|
* - `window.chui.services` — плоская карта «метод → канал» (транспорт, который ищет `Desktop`);
|
|
22
24
|
* - `window.chui.window` — минимизировать, развернуть, закрыть, состояние окна;
|
|
23
25
|
* - `window.chui.theme` — текущая тема и подписка на её смену;
|
|
@@ -74,6 +76,12 @@ const services = {};
|
|
|
74
76
|
for (const method of Object.keys(SERVICES_CALLS)) {
|
|
75
77
|
services[method] = (...args) => call(method, ...args);
|
|
76
78
|
}
|
|
79
|
+
// Синхронные вызовы — отдельным циклом и через `sendSync`: на синхронный канал `invoke`
|
|
80
|
+
// не отвечает, промис никогда бы не завершился. Конверт `{value}`/`{error}` разбирает
|
|
81
|
+
// `Desktop.#callSync`, поэтому мост отдаёт ответ как есть — как и обычный канал
|
|
82
|
+
for (const [method, channel] of Object.entries(SERVICES_SYNC_CALLS)) {
|
|
83
|
+
services[method] = (...args) => ipcRenderer.sendSync(channel, ...args);
|
|
84
|
+
}
|
|
77
85
|
// Событие глобальной клавиши: подписка возвращает функцию отписки
|
|
78
86
|
services.onShortcut = (handler = () => {}) => {
|
|
79
87
|
const listener = (_event, accelerator) => handler(accelerator);
|
|
@@ -128,8 +136,19 @@ contextBridge.exposeInMainWorld("chui", {
|
|
|
128
136
|
openExternal: (url = String()) => Desktop.shell.openExternal(url)
|
|
129
137
|
},
|
|
130
138
|
app: {
|
|
139
|
+
// Путь приходит значением, без промиса (синхронный канал), а `Desktop` в preload'е
|
|
140
|
+
// идёт в него сам: `window.chui` своей же странице здесь ещё не виден
|
|
131
141
|
path: (name = String()) => Desktop.app.path(name),
|
|
132
|
-
info: () => Desktop.app.info()
|
|
142
|
+
info: () => Desktop.app.info(),
|
|
143
|
+
// Сырые модули Electron странице не отдают. Методы есть для единообразия
|
|
144
|
+
// с `Desktop`, но со страницы бросают ошибку с подсказкой, что взять взамен
|
|
145
|
+
get: () => Desktop.app.get(),
|
|
146
|
+
getSession: () => Desktop.app.getSession(),
|
|
147
|
+
getWebContents: () => Desktop.app.getWebContents(),
|
|
148
|
+
// Закрытие и перезапуск — как у `Desktop` из пакета: те же шаги, что у пункта меню.
|
|
149
|
+
// Отвечают значением, а не промисом (синхронный канал), поэтому `await` не нужен
|
|
150
|
+
quit: () => Desktop.app.quit(),
|
|
151
|
+
restart: () => Desktop.app.restart()
|
|
133
152
|
},
|
|
134
153
|
dialog: {
|
|
135
154
|
open: (options = {}) => Desktop.dialog.open(options),
|
|
@@ -9,29 +9,6 @@ page {
|
|
|
9
9
|
align-items: baseline;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
/*
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
page_title {
|
|
16
|
-
display: flex;
|
|
17
|
-
align-items: center;
|
|
18
|
-
gap: 10px;
|
|
19
|
-
/* Без собственных отступов: text заголовка стоит по тому же вертикали, что и блоки
|
|
20
|
-
ниже, — иначе он выезжает на 2px вправо и левый край страницы ломается */
|
|
21
|
-
margin: 0 0 calc(10px * var(--density)) 0;
|
|
22
|
-
padding: 0;
|
|
23
|
-
font-size: var(--font_lead_size);
|
|
24
|
-
font-weight: 600;
|
|
25
|
-
color: var(--text_color);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
page_title chui_icon {
|
|
29
|
-
color: var(--link_color);
|
|
30
|
-
/* Кегль значка — как у подписи: при 20px глиф перетягивал строку */
|
|
31
|
-
font-size: var(--font_lead_size);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/* Страница без заголовка не держит пустую строку */
|
|
35
|
-
page_title:empty {
|
|
36
|
-
display: none;
|
|
37
|
-
}
|
|
12
|
+
/* Вид заголовка страницы держит шапка (`page_name` в global_style.css): оболочка
|
|
13
|
+
в содержимое ничего не подмешивает (`Route#go`), поэтому страница начинается
|
|
14
|
+
со своих блоков, а видимый заголовок рисует приложение само */
|
|
@@ -43,8 +43,32 @@ const SERVICES_CHANNELS = {
|
|
|
43
43
|
SHORTCUT_EVENT: "chui_shortcut",
|
|
44
44
|
/** Путь приложения по имени из `APP_PATH_NAMES` */
|
|
45
45
|
APP_PATH: "chui_services_app_path",
|
|
46
|
+
/**
|
|
47
|
+
* Тот же путь, но ответ приходит значением, а не промисом: канал синхронный
|
|
48
|
+
* (`ipcRenderer.sendSync`). `Desktop.app.path` ходит сюда — путь берут на старте, чтобы
|
|
49
|
+
* собрать свои пути к файлам, и ждать промис там негде. Асинхронный `APP_PATH` оставлен:
|
|
50
|
+
* карта `window.chui.services` отдаётся странице целиком, её ключи — часть API безопасного
|
|
51
|
+
* режима, и убрать один из них значило бы сломать чужой вызов.
|
|
52
|
+
*/
|
|
53
|
+
APP_PATH_SYNC: "chui_services_app_path_sync",
|
|
46
54
|
/** Сведения о приложении: имя, версия, язык, сборка */
|
|
47
55
|
APP_INFO: "chui_services_app_info",
|
|
56
|
+
/**
|
|
57
|
+
* Закрыть и перезапустить приложение со страницы. Идут через `Main.stop()`/`restart()`:
|
|
58
|
+
* это те же шаги, что у пункта меню «Выход», — снимаются обработчики сервисов, глобальные
|
|
59
|
+
* клавиши и подписки, а не просто закрывается окно.
|
|
60
|
+
*/
|
|
61
|
+
APP_QUIT: "chui_services_app_quit",
|
|
62
|
+
APP_RESTART: "chui_services_app_restart",
|
|
63
|
+
/**
|
|
64
|
+
* Те же закрытие и перезапуск, но ответ приходит значением, а не промисом: канал
|
|
65
|
+
* синхронный (`ipcRenderer.sendSync`). `Desktop.app.quit()`/`restart()` ходят сюда — выход
|
|
66
|
+
* начинается тем же `Main.stop()`/`restart()`, а страница получает ответ сразу, без `await`.
|
|
67
|
+
* Асинхронные `APP_QUIT`/`APP_RESTART` оставлены: их ключи есть в карте `window.chui.services`,
|
|
68
|
+
* а это часть API безопасного режима.
|
|
69
|
+
*/
|
|
70
|
+
APP_QUIT_SYNC: "chui_services_app_quit_sync",
|
|
71
|
+
APP_RESTART_SYNC: "chui_services_app_restart_sync",
|
|
48
72
|
/** Системные диалоги: окно берётся у отправителя */
|
|
49
73
|
DIALOG_OPEN: "chui_services_dialog_open",
|
|
50
74
|
DIALOG_SAVE: "chui_services_dialog_save",
|
|
@@ -99,6 +123,8 @@ const SERVICES_CALLS = {
|
|
|
99
123
|
localeSet: SERVICES_CHANNELS.LOCALE_SET,
|
|
100
124
|
appPath: SERVICES_CHANNELS.APP_PATH,
|
|
101
125
|
appInfo: SERVICES_CHANNELS.APP_INFO,
|
|
126
|
+
appQuit: SERVICES_CHANNELS.APP_QUIT,
|
|
127
|
+
appRestart: SERVICES_CHANNELS.APP_RESTART,
|
|
102
128
|
dialogOpen: SERVICES_CHANNELS.DIALOG_OPEN,
|
|
103
129
|
dialogSave: SERVICES_CHANNELS.DIALOG_SAVE,
|
|
104
130
|
dialogMessage: SERVICES_CHANNELS.DIALOG_MESSAGE,
|
|
@@ -107,6 +133,30 @@ const SERVICES_CALLS = {
|
|
|
107
133
|
sessionClearStorage: SERVICES_CHANNELS.SESSION_CLEAR_STORAGE
|
|
108
134
|
};
|
|
109
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Синхронные вызовы (`ipcRenderer.sendSync`) — те, где ответ нужен значением, а не промисом.
|
|
138
|
+
*
|
|
139
|
+
* Отдельный map, а не строка в `SERVICES_CALLS`: тот мост превращает в методы страницы через
|
|
140
|
+
* `ipcRenderer.invoke`, а на синхронный канал invoke не отвечает — промис не завершился бы
|
|
141
|
+
* никогда, и страница молча ждала бы ответа.
|
|
142
|
+
*
|
|
143
|
+
* Ответ синхронного канала — конверт `{value}` или `{error}`: у `sendSync` нет канала для
|
|
144
|
+
* исключения, и без конверта причина отказа (имя пути вне белого списка) потерялась бы,
|
|
145
|
+
* а страница получила бы `undefined`. Разбирает конверт `Desktop.#callSync` — одинаково
|
|
146
|
+
* в обычном режиме и в безопасном.
|
|
147
|
+
*
|
|
148
|
+
* `sendSync` останавливает страницу на время ответа (main при этом свободен), поэтому
|
|
149
|
+
* синхронных вызовов должно быть мало и отвечать они обязаны мгновенно: `app.getPath` —
|
|
150
|
+
* чтение кеша Electron, без обращения к диску.
|
|
151
|
+
*/
|
|
152
|
+
const SERVICES_SYNC_CALLS = {
|
|
153
|
+
appPathSync: SERVICES_CHANNELS.APP_PATH_SYNC,
|
|
154
|
+
// Закрытие и перезапуск отвечают мгновенно (`setImmediate` в main откладывает выход),
|
|
155
|
+
// поэтому синхронный канал здесь так же уместен, как у пути: странице не нужен `await`
|
|
156
|
+
appQuitSync: SERVICES_CHANNELS.APP_QUIT_SYNC,
|
|
157
|
+
appRestartSync: SERVICES_CHANNELS.APP_RESTART_SYNC
|
|
158
|
+
};
|
|
159
|
+
|
|
110
160
|
/** Каналы моста: окно, тема и прикладные сообщения безопасного окна */
|
|
111
161
|
const BRIDGE_CHANNELS = {
|
|
112
162
|
WINDOW_ACTION: "chui_bridge_window_action",
|
|
@@ -128,6 +178,7 @@ const BRIDGE_ARGS = {
|
|
|
128
178
|
|
|
129
179
|
exports.SERVICES_CHANNELS = SERVICES_CHANNELS
|
|
130
180
|
exports.SERVICES_CALLS = SERVICES_CALLS
|
|
181
|
+
exports.SERVICES_SYNC_CALLS = SERVICES_SYNC_CALLS
|
|
131
182
|
exports.BRIDGE_CHANNELS = BRIDGE_CHANNELS
|
|
132
183
|
exports.APP_PATH_NAMES = APP_PATH_NAMES
|
|
133
184
|
exports.BRIDGE_ARGS = BRIDGE_ARGS
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const {SERVICES_CHANNELS, SERVICES_CALLS} = require("./chui_channels");
|
|
1
|
+
const {SERVICES_CHANNELS, SERVICES_CALLS, SERVICES_SYNC_CALLS, APP_PATH_NAMES} = require("./chui_channels");
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Десктоп-сервисы со стороны страницы: буфер обмена, последние документы, прогресс на иконке
|
|
@@ -126,19 +126,75 @@ class Desktop {
|
|
|
126
126
|
};
|
|
127
127
|
|
|
128
128
|
/**
|
|
129
|
-
*
|
|
130
|
-
*
|
|
129
|
+
* Приложение: пути, сведения и сырые модули Electron.
|
|
130
|
+
*
|
|
131
|
+
* Странице модули main не отдают — это и есть рекомендация Electron вместо `remote`,
|
|
132
|
+
* поэтому пути и сведения идут по IPC. В main-процессе IPC нет, и те же методы отвечают
|
|
133
|
+
* значением: `path()` берёт путь у Electron, а `get()`/`getSession()`/`getWebContents()`
|
|
134
|
+
* отдают сами объекты.
|
|
131
135
|
*
|
|
132
136
|
* ```javascript
|
|
133
|
-
*
|
|
134
|
-
* await Desktop.app.info();
|
|
137
|
+
* const dir = Desktop.app.path('userData'); // путь приходит сразу, без промиса
|
|
138
|
+
* const info = await Desktop.app.info(); // {name, version, locale, isPackaged}
|
|
139
|
+
* Desktop.app.quit(); // закрыть приложение — тоже без промиса
|
|
135
140
|
* ```
|
|
136
141
|
*/
|
|
137
142
|
static app = {
|
|
138
|
-
/**
|
|
139
|
-
|
|
143
|
+
/**
|
|
144
|
+
* Путь по имени из белого списка (`"userData"`, `"downloads"`, …).
|
|
145
|
+
*
|
|
146
|
+
* Отвечает значением, а не промисом: из этих путей страница собирает свои (файлы,
|
|
147
|
+
* логи, кеш) на старте, и `await` пришлось бы протаскивать через весь код инициализации.
|
|
148
|
+
* Идёт по синхронному каналу (`SERVICES_SYNC_CALLS`), а потому значение берётся один раз
|
|
149
|
+
* на имя и кешируется — `sendSync` останавливает страницу на время ответа, а путь за
|
|
150
|
+
* жизнь приложения не меняется (`app.setPath` в main — редкий случай, и тогда кеш
|
|
151
|
+
* страницы устареет так же, как устарел бы вызов, сделанный раньше).
|
|
152
|
+
* Прежний `await Desktop.app.path(...)` продолжает работать: `await` на строке
|
|
153
|
+
* возвращает ту же строку.
|
|
154
|
+
*
|
|
155
|
+
* Работает и в main-процессе: там канала нет, и путь берётся у Electron напрямую
|
|
156
|
+
* (с тем же белым списком), поэтому `main.js` обходится без отдельного класса путей.
|
|
157
|
+
*/
|
|
158
|
+
path: (name = String()) => Desktop.#appPath(String(name)),
|
|
140
159
|
/** @returns {Promise<{name: string, version: string, locale: string, systemLocale: string, isPackaged: boolean}>} */
|
|
141
|
-
info: () => Desktop.#call("appInfo")
|
|
160
|
+
info: () => Desktop.#call("appInfo"),
|
|
161
|
+
/**
|
|
162
|
+
* Сырой объект `app` Electron. **Только для main-процесса:** странице модули main
|
|
163
|
+
* не отдают, и здесь она получит ошибку с подсказкой — вместо `app` со страницы берут
|
|
164
|
+
* `Desktop.app.info()`, `Desktop.session.*`, `Desktop.dialog` или свой канал.
|
|
165
|
+
*
|
|
166
|
+
* ```javascript
|
|
167
|
+
* const app = Desktop.app.get(); // main.js
|
|
168
|
+
* app.on('session-created', (session) => {});
|
|
169
|
+
* ```
|
|
170
|
+
*/
|
|
171
|
+
get: () => Desktop.#electronObject("app"),
|
|
172
|
+
/** Сырой `session` Electron (только main-процесс; странице — `Desktop.session.*`) */
|
|
173
|
+
getSession: () => Desktop.#electronObject("session"),
|
|
174
|
+
/** Сырой `webContents` Electron (только main-процесс) */
|
|
175
|
+
getWebContents: () => Desktop.#electronObject("webContents"),
|
|
176
|
+
/**
|
|
177
|
+
* Закрыть приложение: тот же выход, что у пункта меню «Выход» — `Main.stop()` снимает
|
|
178
|
+
* обработчики сервисов, глобальные клавиши и подписки, а затем приложение выходит.
|
|
179
|
+
*
|
|
180
|
+
* Отвечает значением, а не промисом (синхронный канал): ждать результат не нужно и
|
|
181
|
+
* `await` ничего не меняет — `await true` даёт то же `true`. Ответ приходит до выхода,
|
|
182
|
+
* потому что main откладывает `stop()` через `setImmediate`.
|
|
183
|
+
*
|
|
184
|
+
* ```javascript
|
|
185
|
+
* new Button({title: 'Выход', clickEvent: () => Desktop.app.quit()});
|
|
186
|
+
* ```
|
|
187
|
+
* @returns {boolean} `true` — выход начат
|
|
188
|
+
*/
|
|
189
|
+
quit: () => Desktop.#callSync("appQuitSync"),
|
|
190
|
+
/**
|
|
191
|
+
* Перезапустить приложение: процесс поднимается заново (`Main.restart()` →
|
|
192
|
+
* `app.relaunch()`), поэтому состояние из памяти не переносится — сохраните его сами.
|
|
193
|
+
*
|
|
194
|
+
* Отвечает значением, а не промисом (синхронный канал), как и `quit()`.
|
|
195
|
+
* @returns {boolean} `true` — перезапуск начат
|
|
196
|
+
*/
|
|
197
|
+
restart: () => Desktop.#callSync("appRestartSync")
|
|
142
198
|
};
|
|
143
199
|
|
|
144
200
|
/**
|
|
@@ -183,6 +239,9 @@ class Desktop {
|
|
|
183
239
|
static #history_handlers = new Set();
|
|
184
240
|
static #history_event_bound = false;
|
|
185
241
|
static #history_bridge_unsubscribe = undefined;
|
|
242
|
+
// Пути приложения, уже полученные значением: `sendSync` останавливает страницу на время
|
|
243
|
+
// ответа, поэтому за одним и тем же именем в main ходят один раз
|
|
244
|
+
static #app_paths = new Map();
|
|
186
245
|
|
|
187
246
|
static #bridge() {
|
|
188
247
|
if (typeof window === "undefined" || window.chui === undefined) return undefined;
|
|
@@ -197,6 +256,65 @@ class Desktop {
|
|
|
197
256
|
return ipcRenderer.invoke(SERVICES_CALLS[method], ...args);
|
|
198
257
|
}
|
|
199
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Синхронный вызов сервиса: в безопасном режиме — метод моста, иначе — канал `sendSync`.
|
|
261
|
+
* Канал отвечает конвертом `{value}`/`{error}` (см. `SERVICES_SYNC_CALLS` в chui_channels.js),
|
|
262
|
+
* поэтому исключение из main превращается здесь в обычную ошибку с его текстом.
|
|
263
|
+
*/
|
|
264
|
+
static #callSync(method = String(), ...args) {
|
|
265
|
+
const services = Desktop.#bridge();
|
|
266
|
+
const answer = services !== undefined && typeof services[method] === "function"
|
|
267
|
+
? services[method](...args)
|
|
268
|
+
: require("electron").ipcRenderer.sendSync(SERVICES_SYNC_CALLS[method], ...args);
|
|
269
|
+
if (answer === null || typeof answer !== "object") return answer;
|
|
270
|
+
if (answer.error !== undefined) throw new Error(answer.error);
|
|
271
|
+
return answer.value;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Путь приложения значением (см. `Desktop.app.path`).
|
|
276
|
+
*
|
|
277
|
+
* В main-процессе IPC недоступен (`ipcRenderer` там нет, а отправителя-страницы не
|
|
278
|
+
* существует), поэтому путь берётся у Electron напрямую — с той же проверкой белого
|
|
279
|
+
* списка, что и в канале. Это позволяет звать `Desktop.app.path()` из `main.js` и со
|
|
280
|
+
* страницы одним и тем же вызовом.
|
|
281
|
+
*/
|
|
282
|
+
static #appPath(name = String()) {
|
|
283
|
+
if (Desktop.#app_paths.has(name)) return Desktop.#app_paths.get(name);
|
|
284
|
+
const value = Desktop.#inMainProcess()
|
|
285
|
+
? Desktop.#appPathInMain(name)
|
|
286
|
+
: Desktop.#callSync("appPathSync", name);
|
|
287
|
+
Desktop.#app_paths.set(name, value);
|
|
288
|
+
return value;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Различает процессы: у main нет `window`, у страницы и preload'а — есть */
|
|
292
|
+
static #inMainProcess() {
|
|
293
|
+
return typeof window === "undefined";
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** main-процесс: путь у Electron напрямую, но только из белого списка */
|
|
297
|
+
static #appPathInMain(name = String()) {
|
|
298
|
+
if (APP_PATH_NAMES.includes(name) === false) {
|
|
299
|
+
throw new Error(`Desktop.app.path: имя «${name}» вне белого списка`);
|
|
300
|
+
}
|
|
301
|
+
return require("electron").app.getPath(name);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Сырой объект Electron для main-процесса.
|
|
306
|
+
*
|
|
307
|
+
* Со страницы `app`/`session`/`webContents` недоступны: Electron не рекомендует `remote`,
|
|
308
|
+
* а мост preload'а отдаёт только узкий `window.chui`. Поэтому вместо тихого `undefined`
|
|
309
|
+
* здесь ошибка с подсказкой, что взять взамен.
|
|
310
|
+
*/
|
|
311
|
+
static #electronObject(name = String()) {
|
|
312
|
+
if (Desktop.#inMainProcess() === false) {
|
|
313
|
+
throw new Error(`Desktop.app: сырой «${name}» доступен только в main-процессе — со страницы возьмите Desktop.app.info(), Desktop.session.* или свой канал из security.channels`);
|
|
314
|
+
}
|
|
315
|
+
return require("electron")[name];
|
|
316
|
+
}
|
|
317
|
+
|
|
200
318
|
static #subscribe(accelerator = String(), handler = () => {}) {
|
|
201
319
|
const handlers = Desktop.#shortcut_handlers.get(accelerator) ?? new Set();
|
|
202
320
|
const first = handlers.size === 0;
|
package/index.js
CHANGED
|
@@ -155,6 +155,18 @@ function inRenderer() {
|
|
|
155
155
|
return process !== undefined && process.type === "renderer";
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Путь приложения по имени из белого списка.
|
|
160
|
+
*
|
|
161
|
+
* Одно правило на два канала (`APP_PATH` и `APP_PATH_SYNC`): странице не должен быть доступен
|
|
162
|
+
* произвольный каталог системы, поэтому всё, что вне `APP_PATH_NAMES`, main отклоняет. Сам
|
|
163
|
+
* `app.getPath` синхронный и без диска — именно поэтому путь можно отдавать значением.
|
|
164
|
+
*/
|
|
165
|
+
function appPathByName(name = String()) {
|
|
166
|
+
if (APP_PATH_NAMES.includes(name) === false) throw new Error(`Desktop.app.path: имя «${name}» вне белого списка`);
|
|
167
|
+
return app.getPath(name);
|
|
168
|
+
}
|
|
169
|
+
|
|
158
170
|
//VARS
|
|
159
171
|
let isQuiting = false;
|
|
160
172
|
|
|
@@ -233,6 +245,9 @@ class Main {
|
|
|
233
245
|
// Десктоп-сервисы (буфер, недавние, прогресс, глобальные клавиши) и их подписки по окнам
|
|
234
246
|
#services_enabled = false;
|
|
235
247
|
#service_handlers = [];
|
|
248
|
+
// Синхронные каналы снимаются не `removeHandler`, а `removeListener`: они зарегистрированы
|
|
249
|
+
// через `ipcMain.on` — см. `#handleSync`
|
|
250
|
+
#service_sync_handlers = [];
|
|
236
251
|
#shortcut_subscribers = new Map();
|
|
237
252
|
#shortcut_watchers = new Map();
|
|
238
253
|
#progress_windows = new Map();
|
|
@@ -695,10 +710,11 @@ class Main {
|
|
|
695
710
|
});
|
|
696
711
|
// Приложение и системные диалоги для страницы: Electron советует IPC вместо `remote`.
|
|
697
712
|
// Путь — только из белого списка, диалог и сессия — у окна-отправителя.
|
|
698
|
-
this.#handle(SERVICES_CHANNELS.APP_PATH, (event, name) =>
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
713
|
+
this.#handle(SERVICES_CHANNELS.APP_PATH, (event, name) => appPathByName(name));
|
|
714
|
+
// `Desktop.app.path` отвечает значением, а не промисом, поэтому у пути есть второй,
|
|
715
|
+
// синхронный канал: `sendSync` ждёт `event.returnValue`, а не ответ `handle`.
|
|
716
|
+
// Асинхронный канал выше оставлен — его ключ есть в карте `window.chui.services`
|
|
717
|
+
this.#handleSync(SERVICES_CHANNELS.APP_PATH_SYNC, (event, name) => appPathByName(name));
|
|
702
718
|
this.#handle(SERVICES_CHANNELS.APP_INFO, () => ({
|
|
703
719
|
name: app.getName(),
|
|
704
720
|
version: app.getVersion(),
|
|
@@ -706,6 +722,31 @@ class Main {
|
|
|
706
722
|
systemLocale: app.getSystemLocale(),
|
|
707
723
|
isPackaged: app.isPackaged
|
|
708
724
|
}));
|
|
725
|
+
// Закрытие и перезапуск со страницы: страница — код самого приложения, поэтому кнопке
|
|
726
|
+
// «Выход» не нужен свой канал в каждом проекте. Идут через `Main.stop()`/`restart()` —
|
|
727
|
+
// те же шаги, что у пункта меню: снимаются обработчики сервисов, глобальные клавиши и
|
|
728
|
+
// подписки, а не просто закрывается окно. Ответ уходит странице до выхода
|
|
729
|
+
// (`setImmediate`): иначе промис отваливался бы с «Render frame was disposed», и
|
|
730
|
+
// `await Desktop.app.quit()` со страницы выглядел бы ошибкой
|
|
731
|
+
this.#handle(SERVICES_CHANNELS.APP_QUIT, () => {
|
|
732
|
+
setImmediate(() => this.stop());
|
|
733
|
+
return true;
|
|
734
|
+
});
|
|
735
|
+
this.#handle(SERVICES_CHANNELS.APP_RESTART, () => {
|
|
736
|
+
setImmediate(() => this.restart());
|
|
737
|
+
return true;
|
|
738
|
+
});
|
|
739
|
+
// У закрытия и перезапуска есть и синхронные каналы: `Desktop.app.quit()`/`restart()`
|
|
740
|
+
// отвечают значением, а не промисом — как `Desktop.app.path`. Выход по-прежнему
|
|
741
|
+
// откладывается `setImmediate`, чтобы `event.returnValue` успел уйти странице.
|
|
742
|
+
this.#handleSync(SERVICES_CHANNELS.APP_QUIT_SYNC, () => {
|
|
743
|
+
setImmediate(() => this.stop());
|
|
744
|
+
return true;
|
|
745
|
+
});
|
|
746
|
+
this.#handleSync(SERVICES_CHANNELS.APP_RESTART_SYNC, () => {
|
|
747
|
+
setImmediate(() => this.restart());
|
|
748
|
+
return true;
|
|
749
|
+
});
|
|
709
750
|
this.#handle(SERVICES_CHANNELS.DIALOG_OPEN, (event, options) => Main.#showDialog(event, "showOpenDialog", options));
|
|
710
751
|
this.#handle(SERVICES_CHANNELS.DIALOG_SAVE, (event, options) => Main.#showDialog(event, "showSaveDialog", options));
|
|
711
752
|
this.#handle(SERVICES_CHANNELS.DIALOG_MESSAGE, (event, options) => Main.#showDialog(event, "showMessageBox", options));
|
|
@@ -767,6 +808,25 @@ class Main {
|
|
|
767
808
|
return this;
|
|
768
809
|
}
|
|
769
810
|
|
|
811
|
+
/**
|
|
812
|
+
* Синхронный канал: `ipcRenderer.sendSync` в странице ждёт `event.returnValue`, а не ответ
|
|
813
|
+
* `handle`, поэтому здесь `ipcMain.on`. Ответ — конверт `{value}`/`{error}`: у синхронного
|
|
814
|
+
* канала нет способа передать исключение, и без конверта страница получила бы `undefined`
|
|
815
|
+
* вместо причины отказа (см. `SERVICES_SYNC_CALLS` в chui_channels.js).
|
|
816
|
+
*/
|
|
817
|
+
#handleSync(channel = String(), handler = () => {}) {
|
|
818
|
+
const listener = (event, ...args) => {
|
|
819
|
+
try {
|
|
820
|
+
event.returnValue = {value: handler(event, ...args)};
|
|
821
|
+
} catch (error) {
|
|
822
|
+
event.returnValue = {error: error instanceof Error ? error.message : String(error)};
|
|
823
|
+
}
|
|
824
|
+
};
|
|
825
|
+
ipcMain.on(channel, listener);
|
|
826
|
+
this.#service_sync_handlers.push([channel, listener]);
|
|
827
|
+
return this;
|
|
828
|
+
}
|
|
829
|
+
|
|
770
830
|
/**
|
|
771
831
|
* Включает мост безопасных окон (`contextBridge`): управление окном, тема и прикладные
|
|
772
832
|
* сообщения по белому списку каналов. Вызывается сам при создании безопасного окна.
|
|
@@ -1051,6 +1111,8 @@ class Main {
|
|
|
1051
1111
|
// Десктоп-сервисы: снимаем обработчики IPC, подписки клавиш и сами клавиши
|
|
1052
1112
|
for (const [channel] of this.#service_handlers) ipcMain.removeHandler(channel);
|
|
1053
1113
|
this.#service_handlers = [];
|
|
1114
|
+
for (const [channel, listener] of this.#service_sync_handlers) ipcMain.removeListener(channel, listener);
|
|
1115
|
+
this.#service_sync_handlers = [];
|
|
1054
1116
|
this.#shortcut_subscribers.clear();
|
|
1055
1117
|
this.#shortcut_watchers.clear();
|
|
1056
1118
|
this.#progress_windows.clear();
|
|
@@ -1210,44 +1272,6 @@ class Styles {
|
|
|
1210
1272
|
};
|
|
1211
1273
|
}
|
|
1212
1274
|
|
|
1213
|
-
class Application {
|
|
1214
|
-
getApp() {
|
|
1215
|
-
if (inRenderer()) throw new Error("App.get(): странице не отдают модули main — возьмите Desktop.app.path()/info() или свой канал из security.channels");
|
|
1216
|
-
return app;
|
|
1217
|
-
}
|
|
1218
|
-
getSession() {
|
|
1219
|
-
if (inRenderer()) throw new Error("App.getSession(): странице не отдают сессию main — возьмите Desktop.session.*");
|
|
1220
|
-
return session;
|
|
1221
|
-
}
|
|
1222
|
-
getWebContents() {
|
|
1223
|
-
if (inRenderer()) throw new Error("App.getWebContents(): странице не отдают webContents main — действия идут через Desktop или свой канал из security.channels");
|
|
1224
|
-
return webContents;
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
|
|
1228
|
-
class App {
|
|
1229
|
-
static get() { return new Application().getApp() }
|
|
1230
|
-
static getSession() { return new Application().getSession() }
|
|
1231
|
-
static getWebContents() { return new Application().getWebContents() }
|
|
1232
|
-
// ПУТИ
|
|
1233
|
-
static homePath() { return new Application().getApp().getPath("home") }
|
|
1234
|
-
static appDataPath() { return new Application().getApp().getPath("appData") }
|
|
1235
|
-
static userDataPath() { return new Application().getApp().getPath("userData") }
|
|
1236
|
-
static sessionDataPath() { return new Application().getApp().getPath("sessionData") }
|
|
1237
|
-
static logsPath() { return new Application().getApp().getPath("logs") }
|
|
1238
|
-
static tempPath() { return new Application().getApp().getPath("temp") }
|
|
1239
|
-
static exePath() { return new Application().getApp().getPath("exe") }
|
|
1240
|
-
static modulePath() { return new Application().getApp().getPath("module") }
|
|
1241
|
-
static desktopPath() { return new Application().getApp().getPath("desktop") }
|
|
1242
|
-
static documentsPath() { return new Application().getApp().getPath("documents") }
|
|
1243
|
-
static downloadsPath() { return new Application().getApp().getPath("downloads") }
|
|
1244
|
-
static musicPath() { return new Application().getApp().getPath("music") }
|
|
1245
|
-
static picturesPath() { return new Application().getApp().getPath("pictures") }
|
|
1246
|
-
static videosPath() { return new Application().getApp().getPath("videos") }
|
|
1247
|
-
static recentPath() { return new Application().getApp().getPath("recent") }
|
|
1248
|
-
static crashDumpsPath() { return new Application().getApp().getPath("crashDumps") }
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
1275
|
module.exports = {
|
|
1252
1276
|
Main: Main,
|
|
1253
1277
|
sleep: sleep,
|
|
@@ -1405,8 +1429,6 @@ module.exports = {
|
|
|
1405
1429
|
transliterate: transliterate,
|
|
1406
1430
|
store: store,
|
|
1407
1431
|
//
|
|
1408
|
-
App: App,
|
|
1409
|
-
//
|
|
1410
1432
|
// Десктоп-сервисы: классы main-процесса и доступ из страницы
|
|
1411
1433
|
Shortcuts: Shortcuts,
|
|
1412
1434
|
Shell: Shell,
|