chuijs 4.0.0 → 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 +117 -0
- package/README.md +62 -27
- package/framework/appLayout/default/chui_app_layout.js +72 -59
- package/framework/appLayout/default/global_style.css +35 -34
- package/framework/components/chui_data_grid/data_grid.js +7 -3
- package/framework/components/chui_notification/notification_update.js +3 -0
- package/framework/components/chui_notification/status_plate.css +7 -0
- package/framework/components/chui_spinner/spinner.js +38 -21
- package/framework/components/chui_spinner/styles.css +34 -28
- package/framework/components/chui_sticky_bar/sticky_bar.js +230 -0
- package/framework/components/chui_sticky_bar/styles.css +37 -0
- package/framework/modules/chui_bridge/preload.js +35 -2
- package/framework/modules/chui_functions.js +22 -6
- package/framework/modules/chui_page/styles.css +3 -26
- package/framework/modules/chui_services/chui_channels.js +84 -2
- package/framework/modules/chui_services/desktop.js +164 -1
- package/index.js +123 -62
- package/package.json +24 -6
|
@@ -40,9 +40,55 @@ const SERVICES_CHANNELS = {
|
|
|
40
40
|
/** Событие main → рендерер: язык приложения изменился */
|
|
41
41
|
LOCALE_EVENT: "chui_services_locale_event",
|
|
42
42
|
/** Событие main → рендерер: сработала глобальная клавиша */
|
|
43
|
-
SHORTCUT_EVENT: "chui_shortcut"
|
|
43
|
+
SHORTCUT_EVENT: "chui_shortcut",
|
|
44
|
+
/** Путь приложения по имени из `APP_PATH_NAMES` */
|
|
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",
|
|
54
|
+
/** Сведения о приложении: имя, версия, язык, сборка */
|
|
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",
|
|
72
|
+
/** Системные диалоги: окно берётся у отправителя */
|
|
73
|
+
DIALOG_OPEN: "chui_services_dialog_open",
|
|
74
|
+
DIALOG_SAVE: "chui_services_dialog_save",
|
|
75
|
+
DIALOG_MESSAGE: "chui_services_dialog_message",
|
|
76
|
+
/** Сессия окна-отправителя: кеш и хранилище */
|
|
77
|
+
SESSION_CACHE_SIZE: "chui_services_session_cache_size",
|
|
78
|
+
SESSION_CLEAR_CACHE: "chui_services_session_clear_cache",
|
|
79
|
+
SESSION_CLEAR_STORAGE: "chui_services_session_clear_storage"
|
|
44
80
|
};
|
|
45
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Имена путей `app.getPath()`, которые может запросить страница. Это белый список: всё, что
|
|
84
|
+
* вне него, main отклоняет — странице не должен быть доступен произвольный каталог системы.
|
|
85
|
+
* Имена совпадают с именами Electron, кроме `logs` (в Electron это `logs`).
|
|
86
|
+
*/
|
|
87
|
+
const APP_PATH_NAMES = [
|
|
88
|
+
"home", "appData", "userData", "sessionData", "temp", "exe", "module",
|
|
89
|
+
"desktop", "documents", "downloads", "music", "pictures", "videos", "logs", "recent", "crashDumps"
|
|
90
|
+
];
|
|
91
|
+
|
|
46
92
|
/**
|
|
47
93
|
* Метод моста preload'а → канал. `Desktop` вызывает метод по имени: в безопасном режиме
|
|
48
94
|
* его берут из `window.chui.services`, в обычном — сами зовут `ipcRenderer.invoke(канал)`.
|
|
@@ -74,7 +120,41 @@ const SERVICES_CALLS = {
|
|
|
74
120
|
themeSet: SERVICES_CHANNELS.THEME_SET,
|
|
75
121
|
themeToggle: SERVICES_CHANNELS.THEME_TOGGLE,
|
|
76
122
|
localeGet: SERVICES_CHANNELS.LOCALE_GET,
|
|
77
|
-
localeSet: SERVICES_CHANNELS.LOCALE_SET
|
|
123
|
+
localeSet: SERVICES_CHANNELS.LOCALE_SET,
|
|
124
|
+
appPath: SERVICES_CHANNELS.APP_PATH,
|
|
125
|
+
appInfo: SERVICES_CHANNELS.APP_INFO,
|
|
126
|
+
appQuit: SERVICES_CHANNELS.APP_QUIT,
|
|
127
|
+
appRestart: SERVICES_CHANNELS.APP_RESTART,
|
|
128
|
+
dialogOpen: SERVICES_CHANNELS.DIALOG_OPEN,
|
|
129
|
+
dialogSave: SERVICES_CHANNELS.DIALOG_SAVE,
|
|
130
|
+
dialogMessage: SERVICES_CHANNELS.DIALOG_MESSAGE,
|
|
131
|
+
sessionCacheSize: SERVICES_CHANNELS.SESSION_CACHE_SIZE,
|
|
132
|
+
sessionClearCache: SERVICES_CHANNELS.SESSION_CLEAR_CACHE,
|
|
133
|
+
sessionClearStorage: SERVICES_CHANNELS.SESSION_CLEAR_STORAGE
|
|
134
|
+
};
|
|
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
|
|
78
158
|
};
|
|
79
159
|
|
|
80
160
|
/** Каналы моста: окно, тема и прикладные сообщения безопасного окна */
|
|
@@ -98,5 +178,7 @@ const BRIDGE_ARGS = {
|
|
|
98
178
|
|
|
99
179
|
exports.SERVICES_CHANNELS = SERVICES_CHANNELS
|
|
100
180
|
exports.SERVICES_CALLS = SERVICES_CALLS
|
|
181
|
+
exports.SERVICES_SYNC_CALLS = SERVICES_SYNC_CALLS
|
|
101
182
|
exports.BRIDGE_CHANNELS = BRIDGE_CHANNELS
|
|
183
|
+
exports.APP_PATH_NAMES = APP_PATH_NAMES
|
|
102
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
|
* Десктоп-сервисы со стороны страницы: буфер обмена, последние документы, прогресс на иконке
|
|
@@ -125,6 +125,107 @@ class Desktop {
|
|
|
125
125
|
onChange: (listener = () => {}) => Desktop.#subscribeLocale(listener)
|
|
126
126
|
};
|
|
127
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Приложение: пути, сведения и сырые модули Electron.
|
|
130
|
+
*
|
|
131
|
+
* Странице модули main не отдают — это и есть рекомендация Electron вместо `remote`,
|
|
132
|
+
* поэтому пути и сведения идут по IPC. В main-процессе IPC нет, и те же методы отвечают
|
|
133
|
+
* значением: `path()` берёт путь у Electron, а `get()`/`getSession()`/`getWebContents()`
|
|
134
|
+
* отдают сами объекты.
|
|
135
|
+
*
|
|
136
|
+
* ```javascript
|
|
137
|
+
* const dir = Desktop.app.path('userData'); // путь приходит сразу, без промиса
|
|
138
|
+
* const info = await Desktop.app.info(); // {name, version, locale, isPackaged}
|
|
139
|
+
* Desktop.app.quit(); // закрыть приложение — тоже без промиса
|
|
140
|
+
* ```
|
|
141
|
+
*/
|
|
142
|
+
static app = {
|
|
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)),
|
|
159
|
+
/** @returns {Promise<{name: string, version: string, locale: string, systemLocale: string, isPackaged: boolean}>} */
|
|
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")
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Системные диалоги — в том окне, которое их вызвало: родитель и модальность main
|
|
202
|
+
* берёт у отправителя. Результат открытия — `{canceled, filePaths}`, как у `dialog.*`.
|
|
203
|
+
*
|
|
204
|
+
* ```javascript
|
|
205
|
+
* const {canceled, filePaths} = await Desktop.dialog.open({properties: ["openFile"]});
|
|
206
|
+
* ```
|
|
207
|
+
*/
|
|
208
|
+
static dialog = {
|
|
209
|
+
open: (options = Object()) => Desktop.#call("dialogOpen", options),
|
|
210
|
+
save: (options = Object()) => Desktop.#call("dialogSave", options),
|
|
211
|
+
message: (options = Object()) => Desktop.#call("dialogMessage", options)
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Сессия окна-отправителя: кеш и хранилище. Именно своя сессия, а не чужого окна.
|
|
216
|
+
*
|
|
217
|
+
* ```javascript
|
|
218
|
+
* await Desktop.session.clearCache();
|
|
219
|
+
* await Desktop.session.clearStorageData({storages: ["cookies"]});
|
|
220
|
+
* ```
|
|
221
|
+
*/
|
|
222
|
+
static session = {
|
|
223
|
+
/** @returns {Promise<number>} размер кеша в байтах */
|
|
224
|
+
getCacheSize: () => Desktop.#call("sessionCacheSize"),
|
|
225
|
+
clearCache: () => Desktop.#call("sessionClearCache"),
|
|
226
|
+
clearStorageData: (options = Object()) => Desktop.#call("sessionClearStorage", options)
|
|
227
|
+
};
|
|
228
|
+
|
|
128
229
|
// accelerator → набор обработчиков этого окна
|
|
129
230
|
static #shortcut_handlers = new Map();
|
|
130
231
|
static #shortcut_event_bound = false;
|
|
@@ -138,6 +239,9 @@ class Desktop {
|
|
|
138
239
|
static #history_handlers = new Set();
|
|
139
240
|
static #history_event_bound = false;
|
|
140
241
|
static #history_bridge_unsubscribe = undefined;
|
|
242
|
+
// Пути приложения, уже полученные значением: `sendSync` останавливает страницу на время
|
|
243
|
+
// ответа, поэтому за одним и тем же именем в main ходят один раз
|
|
244
|
+
static #app_paths = new Map();
|
|
141
245
|
|
|
142
246
|
static #bridge() {
|
|
143
247
|
if (typeof window === "undefined" || window.chui === undefined) return undefined;
|
|
@@ -152,6 +256,65 @@ class Desktop {
|
|
|
152
256
|
return ipcRenderer.invoke(SERVICES_CALLS[method], ...args);
|
|
153
257
|
}
|
|
154
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
|
+
|
|
155
318
|
static #subscribe(accelerator = String(), handler = () => {}) {
|
|
156
319
|
const handlers = Desktop.#shortcut_handlers.get(accelerator) ?? new Set();
|
|
157
320
|
const first = handlers.size === 0;
|
package/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// === ИНСТРУМЕНТЫ ===
|
|
2
|
-
const {app, BrowserWindow, Menu, Tray, ipcMain, ipcRenderer, shell, nativeTheme, session, webContents, systemPreferences} = require('electron');
|
|
3
|
-
const { dialog } = require('@electron/remote/main');
|
|
2
|
+
const {app, BrowserWindow, Menu, Tray, ipcMain, ipcRenderer, shell, nativeTheme, session, webContents, systemPreferences, dialog} = require('electron');
|
|
4
3
|
// Одно хранилище настроек на модуль: его получает приложение через `require('chuijs').store`,
|
|
5
4
|
// из него читают тему, язык и история буфера, а `Settings` берёт его для своих значений по умолчанию
|
|
6
5
|
const {store} = require("./framework/modules/chui_store/chui_store");
|
|
@@ -50,6 +49,7 @@ const {Descriptions} = require('./framework/components/chui_descriptions/descrip
|
|
|
50
49
|
const {Statistic} = require('./framework/components/chui_statistic/statistic');
|
|
51
50
|
const {Backtop} = require('./framework/components/chui_backtop/backtop');
|
|
52
51
|
const {Affix} = require('./framework/components/chui_affix/affix');
|
|
52
|
+
const {StickyBar} = require('./framework/components/chui_sticky_bar/sticky_bar');
|
|
53
53
|
const {Watermark} = require('./framework/components/chui_watermark/watermark');
|
|
54
54
|
const {ShortcutHelp} = require('./framework/components/chui_shortcut_help/shortcut_help');
|
|
55
55
|
const {Tooltip} = require('./framework/components/chui_tooltip/tooltip');
|
|
@@ -78,7 +78,7 @@ const {CodeBlock} = require('./framework/components/chui_code/code')
|
|
|
78
78
|
const {HtmlBlock} = require('./framework/components/chui_html_block/html_block')
|
|
79
79
|
const {sleep, render, getDefaultIcon, formatBytes} = require('./framework/modules/chui_functions');
|
|
80
80
|
const {Shortcuts, Clipboard, RecentDocuments, TaskbarProgress} = require('./framework/modules/chui_services/chui_services');
|
|
81
|
-
const {SERVICES_CHANNELS, BRIDGE_CHANNELS, BRIDGE_ARGS} = require('./framework/modules/chui_services/chui_channels');
|
|
81
|
+
const {SERVICES_CHANNELS, BRIDGE_CHANNELS, BRIDGE_ARGS, APP_PATH_NAMES} = require('./framework/modules/chui_services/chui_channels');
|
|
82
82
|
const {Desktop} = require('./framework/modules/chui_services/desktop');
|
|
83
83
|
const {I18n} = require('./framework/modules/chui_i18n/chui_i18n');
|
|
84
84
|
const {Shell, Theme, Locale, ClipboardHistory: ClipboardHistoryService, Print} = require('./framework/modules/chui_services/chui_services');
|
|
@@ -150,6 +150,23 @@ const {QRCode} = require("./framework/components/chui_qrcode/qrcode");
|
|
|
150
150
|
const {Log} = require("./framework/modules/chui_logger/chui_logger");
|
|
151
151
|
const url = require("node:url");
|
|
152
152
|
|
|
153
|
+
/** Код исполняется на странице (renderer), а не в main: от этого зависит доступ к модулям Electron */
|
|
154
|
+
function inRenderer() {
|
|
155
|
+
return process !== undefined && process.type === "renderer";
|
|
156
|
+
}
|
|
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
|
+
|
|
153
170
|
//VARS
|
|
154
171
|
let isQuiting = false;
|
|
155
172
|
|
|
@@ -228,6 +245,9 @@ class Main {
|
|
|
228
245
|
// Десктоп-сервисы (буфер, недавние, прогресс, глобальные клавиши) и их подписки по окнам
|
|
229
246
|
#services_enabled = false;
|
|
230
247
|
#service_handlers = [];
|
|
248
|
+
// Синхронные каналы снимаются не `removeHandler`, а `removeListener`: они зарегистрированы
|
|
249
|
+
// через `ipcMain.on` — см. `#handleSync`
|
|
250
|
+
#service_sync_handlers = [];
|
|
231
251
|
#shortcut_subscribers = new Map();
|
|
232
252
|
#shortcut_watchers = new Map();
|
|
233
253
|
#progress_windows = new Map();
|
|
@@ -236,6 +256,7 @@ class Main {
|
|
|
236
256
|
#security = {contextIsolation: false, csp: false, channels: []};
|
|
237
257
|
#secure_contents = new Set();
|
|
238
258
|
#bridge_enabled = false;
|
|
259
|
+
#window_actions_bound = false;
|
|
239
260
|
#theme_bound = false;
|
|
240
261
|
#page_message_handlers = new Map();
|
|
241
262
|
#onUncaughtException = (error) => {
|
|
@@ -431,7 +452,6 @@ class Main {
|
|
|
431
452
|
plugins: false,
|
|
432
453
|
nodeIntegration: false,
|
|
433
454
|
contextIsolation: true,
|
|
434
|
-
enableRemoteModule: false,
|
|
435
455
|
sandbox: false,
|
|
436
456
|
preload: Main.SECURE_PRELOAD,
|
|
437
457
|
additionalArguments: Main.#bridgeArguments(options.render, security),
|
|
@@ -448,7 +468,6 @@ class Main {
|
|
|
448
468
|
preload: options.render === undefined ? this.#renderer : options.render,
|
|
449
469
|
spellcheck: false,
|
|
450
470
|
webviewTag: true,
|
|
451
|
-
enableRemoteModule: true,
|
|
452
471
|
webSecurity: this.#webSecurity,
|
|
453
472
|
nodeIntegrationInSubFrames: true,
|
|
454
473
|
},
|
|
@@ -627,6 +646,9 @@ class Main {
|
|
|
627
646
|
enableServices() {
|
|
628
647
|
if (this.#services_enabled) return this;
|
|
629
648
|
this.#services_enabled = true;
|
|
649
|
+
// Кнопки окна: каналы нужны и обычным окнам — в них `AppLayout` зовёт их по IPC,
|
|
650
|
+
// потому что `@electron/remote` в рендерере больше нет
|
|
651
|
+
this.#enableWindowActions();
|
|
630
652
|
this.services = {
|
|
631
653
|
shortcuts: new Shortcuts(),
|
|
632
654
|
shell: new Shell(),
|
|
@@ -686,6 +708,51 @@ class Main {
|
|
|
686
708
|
this.services.clipboard.clear();
|
|
687
709
|
return true;
|
|
688
710
|
});
|
|
711
|
+
// Приложение и системные диалоги для страницы: Electron советует IPC вместо `remote`.
|
|
712
|
+
// Путь — только из белого списка, диалог и сессия — у окна-отправителя.
|
|
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));
|
|
718
|
+
this.#handle(SERVICES_CHANNELS.APP_INFO, () => ({
|
|
719
|
+
name: app.getName(),
|
|
720
|
+
version: app.getVersion(),
|
|
721
|
+
locale: app.getLocale(),
|
|
722
|
+
systemLocale: app.getSystemLocale(),
|
|
723
|
+
isPackaged: app.isPackaged
|
|
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
|
+
});
|
|
750
|
+
this.#handle(SERVICES_CHANNELS.DIALOG_OPEN, (event, options) => Main.#showDialog(event, "showOpenDialog", options));
|
|
751
|
+
this.#handle(SERVICES_CHANNELS.DIALOG_SAVE, (event, options) => Main.#showDialog(event, "showSaveDialog", options));
|
|
752
|
+
this.#handle(SERVICES_CHANNELS.DIALOG_MESSAGE, (event, options) => Main.#showDialog(event, "showMessageBox", options));
|
|
753
|
+
this.#handle(SERVICES_CHANNELS.SESSION_CACHE_SIZE, (event) => event.sender.session.getCacheSize());
|
|
754
|
+
this.#handle(SERVICES_CHANNELS.SESSION_CLEAR_CACHE, (event) => event.sender.session.clearCache());
|
|
755
|
+
this.#handle(SERVICES_CHANNELS.SESSION_CLEAR_STORAGE, (event, options) => event.sender.session.clearStorageData(options));
|
|
689
756
|
this.#handle(SERVICES_CHANNELS.RECENT_ADD, (event, path, name) => {
|
|
690
757
|
this.services.recent.add(path, name);
|
|
691
758
|
return true;
|
|
@@ -741,13 +808,42 @@ class Main {
|
|
|
741
808
|
return this;
|
|
742
809
|
}
|
|
743
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
|
+
|
|
744
830
|
/**
|
|
745
831
|
* Включает мост безопасных окон (`contextBridge`): управление окном, тема и прикладные
|
|
746
832
|
* сообщения по белому списку каналов. Вызывается сам при создании безопасного окна.
|
|
747
833
|
*/
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
834
|
+
/** Диалог в окне отправителя: без окна (уже закрыто) Electron зовётся без родителя */
|
|
835
|
+
static #showDialog(event, method = String(), options = Object()) {
|
|
836
|
+
const window = BrowserWindow.fromWebContents(event.sender);
|
|
837
|
+
return window === null ? dialog[method](options) : dialog[method](window, options);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Каналы кнопок окна. Ими пользуются оба режима: безопасное окно — через мост
|
|
842
|
+
* `window.chui.window`, обычное — прямым `ipcRenderer.invoke`. Регистрируются один раз на приложение.
|
|
843
|
+
*/
|
|
844
|
+
#enableWindowActions() {
|
|
845
|
+
if (this.#window_actions_bound) return this;
|
|
846
|
+
this.#window_actions_bound = true;
|
|
751
847
|
this.#handle(BRIDGE_CHANNELS.WINDOW_ACTION, (event, action) => Main.#applyWindowAction(event.sender, action));
|
|
752
848
|
this.#handle(BRIDGE_CHANNELS.WINDOW_STATE, (event) => {
|
|
753
849
|
const window = BrowserWindow.fromWebContents(event.sender);
|
|
@@ -759,6 +855,13 @@ class Main {
|
|
|
759
855
|
isFocused: window.isFocused()
|
|
760
856
|
};
|
|
761
857
|
});
|
|
858
|
+
return this;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
enableBridge() {
|
|
862
|
+
if (this.#bridge_enabled) return this;
|
|
863
|
+
this.#bridge_enabled = true;
|
|
864
|
+
this.#enableWindowActions();
|
|
762
865
|
this.#handle(BRIDGE_CHANNELS.THEME_GET, () => (nativeTheme.shouldUseDarkColors ? "dark" : "light"));
|
|
763
866
|
this.#handle(BRIDGE_CHANNELS.LOCALE_GET, () => this.services.locale.get());
|
|
764
867
|
this.#handle(BRIDGE_CHANNELS.MESSAGE, (event, channel, ...args) => {
|
|
@@ -790,7 +893,7 @@ class Main {
|
|
|
790
893
|
return this.#send(BRIDGE_CHANNELS.MESSAGE, [channel, payload], windowName);
|
|
791
894
|
}
|
|
792
895
|
|
|
793
|
-
/** Действия кнопок
|
|
896
|
+
/** Действия кнопок окна: и обычные окна, и безопасные зовут их по IPC */
|
|
794
897
|
static #applyWindowAction(sender, action = String()) {
|
|
795
898
|
const window = BrowserWindow.fromWebContents(sender);
|
|
796
899
|
if (window === null || window.isDestroyed()) return false;
|
|
@@ -1008,6 +1111,8 @@ class Main {
|
|
|
1008
1111
|
// Десктоп-сервисы: снимаем обработчики IPC, подписки клавиш и сами клавиши
|
|
1009
1112
|
for (const [channel] of this.#service_handlers) ipcMain.removeHandler(channel);
|
|
1010
1113
|
this.#service_handlers = [];
|
|
1114
|
+
for (const [channel, listener] of this.#service_sync_handlers) ipcMain.removeListener(channel, listener);
|
|
1115
|
+
this.#service_sync_handlers = [];
|
|
1011
1116
|
this.#shortcut_subscribers.clear();
|
|
1012
1117
|
this.#shortcut_watchers.clear();
|
|
1013
1118
|
this.#progress_windows.clear();
|
|
@@ -1021,6 +1126,7 @@ class Main {
|
|
|
1021
1126
|
this.#on_theme_updated = undefined;
|
|
1022
1127
|
this.#page_message_handlers.clear();
|
|
1023
1128
|
this.#bridge_enabled = false;
|
|
1129
|
+
this.#window_actions_bound = false;
|
|
1024
1130
|
if (this.#updaterListening) {
|
|
1025
1131
|
this.#updaterListening = false;
|
|
1026
1132
|
require("electron-updater").autoUpdater.removeListener('update-downloaded', this.#onUpdateDownloaded);
|
|
@@ -1074,9 +1180,7 @@ class Main {
|
|
|
1074
1180
|
Menu.setApplicationMenu(null)
|
|
1075
1181
|
}
|
|
1076
1182
|
this.enableServices();
|
|
1077
|
-
|
|
1078
|
-
require("@electron/remote/main").initialize();
|
|
1079
|
-
require("@electron/remote/main").enable(window.webContents);
|
|
1183
|
+
this.#buildWindow({name: Main.MAIN_WINDOW, hideOnClose: options.hideOnClose});
|
|
1080
1184
|
})
|
|
1081
1185
|
}
|
|
1082
1186
|
|
|
@@ -1168,53 +1272,6 @@ class Styles {
|
|
|
1168
1272
|
};
|
|
1169
1273
|
}
|
|
1170
1274
|
|
|
1171
|
-
class Application {
|
|
1172
|
-
getApp() {
|
|
1173
|
-
if (process && process.type === 'renderer') {
|
|
1174
|
-
return require("@electron/remote").app;
|
|
1175
|
-
} else {
|
|
1176
|
-
return app;
|
|
1177
|
-
}
|
|
1178
|
-
}
|
|
1179
|
-
getSession() {
|
|
1180
|
-
if (process && process.type === 'renderer') {
|
|
1181
|
-
return require("@electron/remote").session;
|
|
1182
|
-
} else {
|
|
1183
|
-
return session;
|
|
1184
|
-
}
|
|
1185
|
-
}
|
|
1186
|
-
getWebContents() {
|
|
1187
|
-
if (process && process.type === 'renderer') {
|
|
1188
|
-
return require("@electron/remote").webContents;
|
|
1189
|
-
} else {
|
|
1190
|
-
return webContents;
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
class App {
|
|
1196
|
-
static get() { return new Application().getApp() }
|
|
1197
|
-
static getSession() { return new Application().getSession() }
|
|
1198
|
-
static getWebContents() { return new Application().getWebContents() }
|
|
1199
|
-
// ПУТИ
|
|
1200
|
-
static homePath() { return new Application().getApp().getPath("home") }
|
|
1201
|
-
static appDataPath() { return new Application().getApp().getPath("appData") }
|
|
1202
|
-
static userDataPath() { return new Application().getApp().getPath("userData") }
|
|
1203
|
-
static sessionDataPath() { return new Application().getApp().getPath("sessionData") }
|
|
1204
|
-
static logsPath() { return new Application().getApp().getPath("logs") }
|
|
1205
|
-
static tempPath() { return new Application().getApp().getPath("temp") }
|
|
1206
|
-
static exePath() { return new Application().getApp().getPath("exe") }
|
|
1207
|
-
static modulePath() { return new Application().getApp().getPath("module") }
|
|
1208
|
-
static desktopPath() { return new Application().getApp().getPath("desktop") }
|
|
1209
|
-
static documentsPath() { return new Application().getApp().getPath("documents") }
|
|
1210
|
-
static downloadsPath() { return new Application().getApp().getPath("downloads") }
|
|
1211
|
-
static musicPath() { return new Application().getApp().getPath("music") }
|
|
1212
|
-
static picturesPath() { return new Application().getApp().getPath("pictures") }
|
|
1213
|
-
static videosPath() { return new Application().getApp().getPath("videos") }
|
|
1214
|
-
static recentPath() { return new Application().getApp().getPath("recent") }
|
|
1215
|
-
static crashDumpsPath() { return new Application().getApp().getPath("crashDumps") }
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
1275
|
module.exports = {
|
|
1219
1276
|
Main: Main,
|
|
1220
1277
|
sleep: sleep,
|
|
@@ -1246,6 +1303,7 @@ module.exports = {
|
|
|
1246
1303
|
Statistic: Statistic,
|
|
1247
1304
|
Backtop: Backtop,
|
|
1248
1305
|
Affix: Affix,
|
|
1306
|
+
StickyBar: StickyBar,
|
|
1249
1307
|
Watermark: Watermark,
|
|
1250
1308
|
ShortcutHelp: ShortcutHelp,
|
|
1251
1309
|
Alert: Alert,
|
|
@@ -1371,8 +1429,6 @@ module.exports = {
|
|
|
1371
1429
|
transliterate: transliterate,
|
|
1372
1430
|
store: store,
|
|
1373
1431
|
//
|
|
1374
|
-
App: App,
|
|
1375
|
-
//
|
|
1376
1432
|
// Десктоп-сервисы: классы main-процесса и доступ из страницы
|
|
1377
1433
|
Shortcuts: Shortcuts,
|
|
1378
1434
|
Shell: Shell,
|
|
@@ -1388,7 +1444,12 @@ module.exports = {
|
|
|
1388
1444
|
//
|
|
1389
1445
|
downloadSession: downloadSession,
|
|
1390
1446
|
Log: Log,
|
|
1391
|
-
chooseFile:
|
|
1447
|
+
chooseFile: {
|
|
1448
|
+
// Диалоги: странице — по IPC (окно берётся у отправителя), в main — обычный dialog Electron
|
|
1449
|
+
showOpenDialog: (options = {}) => (inRenderer() ? Desktop.dialog.open(options) : dialog.showOpenDialog(options)),
|
|
1450
|
+
showSaveDialog: (options = {}) => (inRenderer() ? Desktop.dialog.save(options) : dialog.showSaveDialog(options)),
|
|
1451
|
+
showMessageBox: (options = {}) => (inRenderer() ? Desktop.dialog.message(options) : dialog.showMessageBox(options))
|
|
1452
|
+
},
|
|
1392
1453
|
//
|
|
1393
1454
|
systemPreferences: systemPreferences
|
|
1394
1455
|
}
|