chuijs 4.0.3 → 4.0.4
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 +536 -0
- package/framework/appLayout/default/chui_app_layout.js +361 -303
- package/framework/appLayout/default/global_style.css +243 -89
- package/framework/appLayout/default/main_theme_style.css +188 -101
- package/framework/components/chui_accordion/accordion.js +17 -17
- package/framework/components/chui_accordion/styles.css +7 -7
- package/framework/components/chui_action_sheet/action_sheet.js +35 -32
- package/framework/components/chui_action_sheet/styles.css +10 -4
- package/framework/components/chui_actions_bar/actions_bar.js +338 -0
- package/framework/components/chui_actions_bar/styles.css +139 -0
- package/framework/components/chui_affix/affix.js +4 -4
- package/framework/components/chui_affix/styles.css +5 -0
- package/framework/components/chui_alert/alert.js +11 -5
- package/framework/components/chui_alert/styles.css +7 -0
- package/framework/components/chui_avatar/avatar.js +13 -13
- package/framework/components/chui_avatar/styles.css +1 -1
- package/framework/components/chui_avatar_group/avatar_group.js +4 -4
- package/framework/components/chui_avatar_group/styles.css +1 -1
- package/framework/components/chui_backtop/backtop.js +25 -2
- package/framework/components/chui_backtop/styles.css +21 -0
- package/framework/components/chui_badge/badge.js +15 -15
- package/framework/components/chui_badge/styles.css +6 -6
- package/framework/components/chui_banner/banner.js +222 -0
- package/framework/components/chui_banner/styles.css +170 -0
- package/framework/components/chui_breadcrumb/styles.css +1 -1
- package/framework/components/chui_button/button.js +24 -28
- package/framework/components/chui_button/styles.css +5 -5
- package/framework/components/chui_calendar/calendar.js +6 -6
- package/framework/components/chui_card/card.js +14 -14
- package/framework/components/chui_card_scroll/card_scroll.js +169 -0
- package/framework/components/chui_card_scroll/styles.css +86 -0
- package/framework/components/chui_chat/chat.js +73 -51
- package/framework/components/chui_chat/styles.css +18 -7
- package/framework/components/chui_clipboard_history/clipboard_history.js +25 -20
- package/framework/components/chui_clipboard_history/styles.css +10 -8
- package/framework/components/chui_code/code.js +8 -8
- package/framework/components/chui_code/styles.css +2 -2
- package/framework/components/chui_code_editor/code_editor.js +26 -26
- package/framework/components/chui_code_editor/styles.css +3 -3
- package/framework/components/chui_command_palette/command_palette.js +29 -29
- package/framework/components/chui_command_palette/styles.css +5 -5
- package/framework/components/chui_console/console.js +9 -9
- package/framework/components/chui_console/styles.css +5 -2
- package/framework/components/chui_content_block/content_block.js +2 -2
- package/framework/components/chui_context_menu/context_menu.js +162 -54
- package/framework/components/chui_context_menu/styles.css +82 -8
- package/framework/components/chui_custom_element/custom_element.js +11 -11
- package/framework/components/chui_data_grid/data_grid.js +268 -268
- package/framework/components/chui_data_grid/styles.css +14 -14
- package/framework/components/chui_descriptions/styles.css +1 -1
- package/framework/components/chui_details/details.js +12 -12
- package/framework/components/chui_details/styles.css +7 -7
- package/framework/components/chui_diff_viewer/diff_viewer.js +88 -88
- package/framework/components/chui_diff_viewer/styles.css +9 -9
- package/framework/components/chui_divider/divider.js +6 -6
- package/framework/components/chui_divider/styles.css +2 -2
- package/framework/components/chui_drawer/drawer.js +37 -14
- package/framework/components/chui_drawer/styles.css +18 -4
- package/framework/components/chui_drop_zone/drop_zone.js +6 -6
- package/framework/components/chui_drop_zone/styles.css +10 -6
- package/framework/components/chui_fieldset/fieldset.js +6 -6
- package/framework/components/chui_fieldset/styles.css +5 -5
- package/framework/components/chui_file_explorer/file_explorer.js +115 -115
- package/framework/components/chui_file_explorer/styles.css +6 -6
- package/framework/components/chui_form/form.js +11 -11
- package/framework/components/chui_gantt/gantt.js +41 -41
- package/framework/components/chui_gantt/styles.css +5 -5
- package/framework/components/chui_graphs/graphs.js +82 -46
- package/framework/components/chui_graphs/styles.css +26 -5
- package/framework/components/chui_hero/hero.js +63 -20
- package/framework/components/chui_hero/styles.css +176 -7
- package/framework/components/chui_html_block/html_block.js +5 -5
- package/framework/components/chui_html_block/styles.css +1 -1
- package/framework/components/chui_hx/styles.css +2 -2
- package/framework/components/chui_icons/icons.js +7 -7
- package/framework/components/chui_icons/styles.css +2 -1
- package/framework/components/chui_inputs/chui_check_box/styles.css +12 -3
- package/framework/components/chui_inputs/chui_chips/chips.js +92 -31
- package/framework/components/chui_inputs/chui_chips/styles.css +28 -5
- package/framework/components/chui_inputs/chui_color_picker/color_picker.js +20 -15
- package/framework/components/chui_inputs/chui_color_picker/styles.css +7 -7
- package/framework/components/chui_inputs/chui_combo_box/combo_box.js +58 -58
- package/framework/components/chui_inputs/chui_combo_box/styles.css +9 -9
- package/framework/components/chui_inputs/chui_date/date.js +93 -95
- package/framework/components/chui_inputs/chui_date/styles.css +8 -8
- package/framework/components/chui_inputs/chui_date_range/date_range.js +22 -22
- package/framework/components/chui_inputs/chui_date_range/styles.css +3 -3
- package/framework/components/chui_inputs/chui_datetime/datetime.js +12 -12
- package/framework/components/chui_inputs/chui_datetime/styles.css +3 -3
- package/framework/components/chui_inputs/chui_email/email.js +10 -10
- package/framework/components/chui_inputs/chui_email/styles.css +6 -6
- package/framework/components/chui_inputs/chui_file/file.js +8 -8
- package/framework/components/chui_inputs/chui_file/styles.css +12 -9
- package/framework/components/chui_inputs/chui_hotkey/hotkey.js +39 -39
- package/framework/components/chui_inputs/chui_hotkey/styles.css +6 -6
- package/framework/components/chui_inputs/chui_masked/masked.js +44 -44
- package/framework/components/chui_inputs/chui_masked/styles.css +5 -5
- package/framework/components/chui_inputs/chui_mention/mention.js +13 -13
- package/framework/components/chui_inputs/chui_mention/styles.css +14 -8
- package/framework/components/chui_inputs/chui_multi_combo_box/multi_combo_box.js +147 -137
- package/framework/components/chui_inputs/chui_multi_combo_box/styles.css +14 -8
- package/framework/components/chui_inputs/chui_number/number.js +42 -45
- package/framework/components/chui_inputs/chui_number/styles.css +6 -6
- package/framework/components/chui_inputs/chui_otp/otp.js +32 -32
- package/framework/components/chui_inputs/chui_otp/styles.css +5 -5
- package/framework/components/chui_inputs/chui_password/password.js +30 -36
- package/framework/components/chui_inputs/chui_password/styles.css +7 -7
- package/framework/components/chui_inputs/chui_radio_button/styles.css +2 -2
- package/framework/components/chui_inputs/chui_rating/rating.js +28 -28
- package/framework/components/chui_inputs/chui_rating/styles.css +3 -3
- package/framework/components/chui_inputs/chui_select_box/select_box.js +54 -54
- package/framework/components/chui_inputs/chui_select_box/styles.css +10 -10
- package/framework/components/chui_inputs/chui_slider/slider.js +14 -14
- package/framework/components/chui_inputs/chui_slider/styles.css +5 -4
- package/framework/components/chui_inputs/chui_text/styles.css +7 -7
- package/framework/components/chui_inputs/chui_text/text.js +13 -17
- package/framework/components/chui_inputs/chui_text_area/styles.css +8 -8
- package/framework/components/chui_inputs/chui_text_area/text_area.js +23 -23
- package/framework/components/chui_inputs/chui_time/styles.css +13 -17
- package/framework/components/chui_inputs/chui_time/time.js +35 -24
- package/framework/components/chui_inputs/chui_toggle/styles.css +10 -3
- package/framework/components/chui_inputs/chui_toggle/toggle.js +16 -16
- package/framework/components/chui_json_editor/json_editor.js +87 -87
- package/framework/components/chui_json_editor/styles.css +5 -5
- package/framework/components/chui_json_viewer/json_viewer.js +80 -80
- package/framework/components/chui_json_viewer/styles.css +6 -6
- package/framework/components/chui_kanban/kanban.js +27 -27
- package/framework/components/chui_kanban/styles.css +8 -4
- package/framework/components/chui_kbd/styles.css +1 -1
- package/framework/components/chui_label/label.js +17 -17
- package/framework/components/chui_label/styles.css +1 -1
- package/framework/components/chui_link/link.js +9 -9
- package/framework/components/chui_link/styles.css +11 -4
- package/framework/components/chui_list/list.js +11 -11
- package/framework/components/chui_list/styles.css +2 -2
- package/framework/components/chui_markdown_editor/markdown_editor.js +66 -66
- package/framework/components/chui_markdown_editor/styles.css +5 -5
- package/framework/components/chui_media/audio.js +240 -246
- package/framework/components/chui_media/audio_styles.css +41 -23
- package/framework/components/chui_media/image.js +49 -46
- package/framework/components/chui_media/image_compare.js +35 -35
- package/framework/components/chui_media/image_compare_styles.css +5 -5
- package/framework/components/chui_media/image_styles.css +1 -1
- package/framework/components/chui_media/video.js +148 -148
- package/framework/components/chui_media/video_styles.css +27 -11
- package/framework/components/chui_menu_bar/menu_bar.js +54 -54
- package/framework/components/chui_menu_bar/styles.css +10 -10
- package/framework/components/chui_message_bar/message_bar.js +551 -0
- package/framework/components/chui_message_bar/styles.css +248 -0
- package/framework/components/chui_modal/modal.js +37 -13
- package/framework/components/chui_notification/notification.js +76 -54
- package/framework/components/chui_notification/notification_download.js +26 -81
- package/framework/components/chui_notification/notification_download_card.js +115 -0
- package/framework/components/chui_notification/notification_download_progress.css +2 -2
- package/framework/components/chui_notification/notification_download_progress.js +70 -108
- package/framework/components/chui_notification/notification_update.js +31 -31
- package/framework/components/chui_notification/status_plate.css +3 -3
- package/framework/components/chui_notification/styles.css +6 -1
- package/framework/components/chui_notification/styles_download.css +1 -1
- package/framework/components/chui_notification/styles_update.css +2 -2
- package/framework/components/chui_org_chart/org_chart.js +29 -29
- package/framework/components/chui_org_chart/styles.css +2 -2
- package/framework/components/chui_pagination/pagination.js +3 -3
- package/framework/components/chui_pagination/styles.css +1 -1
- package/framework/components/chui_paragraph/styles.css +2 -2
- package/framework/components/chui_popover/popover.js +29 -23
- package/framework/components/chui_popover/styles.css +1 -1
- package/framework/components/chui_popups/popups.js +124 -124
- package/framework/components/chui_popups/styles.css +5 -5
- package/framework/components/chui_print_button/print_button.js +15 -10
- package/framework/components/chui_progress_bar/progress_bar.js +34 -12
- package/framework/components/chui_progress_bar/styles.css +4 -4
- package/framework/components/chui_progress_ring/progress_ring.js +15 -15
- package/framework/components/chui_progress_ring/styles.css +1 -1
- package/framework/components/chui_qrcode/qrcode.js +12 -12
- package/framework/components/chui_query_builder/query_builder.js +46 -46
- package/framework/components/chui_query_builder/styles.css +4 -4
- package/framework/components/chui_radio_group/styles.css +1 -1
- package/framework/components/chui_result/result.js +15 -15
- package/framework/components/chui_result/styles.css +3 -3
- package/framework/components/chui_scheduler/scheduler.js +74 -74
- package/framework/components/chui_scheduler/styles.css +5 -5
- package/framework/components/chui_screen_spinner/screen_spinner.js +167 -0
- package/framework/components/chui_screen_spinner/styles.css +49 -0
- package/framework/components/chui_segmented_control/segmented_control.js +63 -6
- package/framework/components/chui_segmented_control/styles.css +32 -6
- package/framework/components/chui_settings/settings.js +45 -46
- package/framework/components/chui_settings/styles.css +4 -4
- package/framework/components/chui_shortcut_help/shortcut_help.js +32 -34
- package/framework/components/chui_sidebar_nav/sidebar_nav.js +98 -29
- package/framework/components/chui_sidebar_nav/styles.css +9 -3
- package/framework/components/chui_slideshow/slideshow.js +85 -85
- package/framework/components/chui_slideshow/styles.css +6 -6
- package/framework/components/chui_spinner/spinner.js +10 -10
- package/framework/components/chui_split_button/split_button.js +30 -12
- package/framework/components/chui_split_button/styles.css +21 -5
- package/framework/components/chui_splitter/splitter.js +11 -11
- package/framework/components/chui_splitter/styles.css +2 -2
- package/framework/components/chui_statistic/statistic.js +47 -47
- package/framework/components/chui_status_bar/styles.css +2 -2
- package/framework/components/chui_stepper/stepper.js +202 -31
- package/framework/components/chui_stepper/styles.css +79 -14
- package/framework/components/chui_sticky_bar/sticky_bar.js +44 -13
- package/framework/components/chui_sticky_bar/styles.css +10 -8
- package/framework/components/chui_tabs/chui_tabs.js +74 -26
- package/framework/components/chui_tabs/styles.css +40 -12
- package/framework/components/chui_text_editor/styles.css +7 -7
- package/framework/components/chui_text_editor/text_editor.js +56 -56
- package/framework/components/chui_text_editor/text_editor_panel.js +173 -174
- package/framework/components/chui_theme_toggle/theme_toggle.js +21 -5
- package/framework/components/chui_timeline/styles.css +3 -1
- package/framework/components/chui_toast_queue/styles.css +11 -11
- package/framework/components/chui_toast_queue/toast_queue.js +30 -27
- package/framework/components/chui_toolbar/styles.css +2 -2
- package/framework/components/chui_tooltip/styles.css +2 -2
- package/framework/components/chui_tooltip/tooltip.js +3 -3
- package/framework/components/chui_tour/styles.css +8 -8
- package/framework/components/chui_tour/tour.js +72 -72
- package/framework/components/chui_transfer/styles.css +6 -6
- package/framework/components/chui_transfer/transfer.js +93 -106
- package/framework/components/chui_tree_table/styles.css +4 -4
- package/framework/components/chui_tree_table/tree_table.js +45 -45
- package/framework/components/chui_tree_view/styles.css +10 -8
- package/framework/components/chui_tree_view/tree_view.js +185 -183
- package/framework/components/chui_user_bar/styles.css +145 -0
- package/framework/components/chui_user_bar/user_bar.js +402 -0
- package/framework/components/chui_user_list_bar/styles.css +173 -0
- package/framework/components/chui_user_list_bar/user_list_bar.js +485 -0
- package/framework/components/chui_watermark/watermark.js +17 -17
- package/framework/components/chui_webview/webview.js +9 -9
- package/framework/components/chui_write_bar/styles.css +172 -0
- package/framework/components/chui_write_bar/write_bar.js +320 -0
- package/framework/main_window.js +1234 -0
- package/framework/modules/chui_a11y/chui_a11y.js +78 -79
- package/framework/modules/chui_animations/animations.js +77 -15
- package/framework/modules/chui_animations/motion.js +120 -0
- package/framework/modules/chui_animations/styles.css +36 -3
- package/framework/modules/chui_functions.js +31 -27
- package/framework/modules/chui_highlight/styles.css +13 -10
- package/framework/modules/chui_i18n/chui_i18n.js +3 -3
- package/framework/modules/chui_i18n/locales/feedback.js +62 -0
- package/framework/modules/chui_i18n/locales/navigation.js +4 -0
- package/framework/modules/chui_json/chui_json.js +2 -2
- package/framework/modules/chui_logger/chui_logger.js +23 -8
- package/framework/modules/chui_page/page.js +10 -10
- package/framework/modules/chui_palette/palette.js +61 -0
- package/framework/modules/chui_qr/chui_qr.js +23 -23
- package/framework/modules/chui_scrollbar/chui_scrollbar.js +45 -45
- package/framework/modules/chui_scrollbar/styles.css +3 -3
- package/framework/modules/chui_services/chui_services.js +8 -8
- package/framework/modules/chui_services/desktop.js +77 -71
- package/framework/modules/chui_sortable/chui_sortable.js +17 -17
- package/framework/modules/chui_validate/chui_validate.js +3 -3
- package/framework/tray_bar/chui_menu_items.js +2 -2
- package/index.d.ts +266 -34
- package/index.js +20 -1168
- package/package.json +6 -4
- package/framework/components/telegram_bot/chui_telegram_bot.js +0 -191
package/index.js
CHANGED
|
@@ -1,18 +1,5 @@
|
|
|
1
1
|
// === ИНСТРУМЕНТЫ ===
|
|
2
|
-
const {
|
|
3
|
-
app,
|
|
4
|
-
BrowserWindow,
|
|
5
|
-
Menu,
|
|
6
|
-
Tray,
|
|
7
|
-
ipcMain,
|
|
8
|
-
ipcRenderer,
|
|
9
|
-
shell,
|
|
10
|
-
nativeTheme,
|
|
11
|
-
session,
|
|
12
|
-
webContents,
|
|
13
|
-
systemPreferences,
|
|
14
|
-
dialog,
|
|
15
|
-
} = require("electron");
|
|
2
|
+
const { BrowserWindow, ipcMain, ipcRenderer, shell, systemPreferences, dialog } = require("electron");
|
|
16
3
|
// Одно хранилище настроек на модуль: его получает приложение через `require('chuijs').store`,
|
|
17
4
|
// из него читают тему, язык и история буфера, а `Settings` берёт его для своих значений по умолчанию
|
|
18
5
|
const { store } = require("./framework/modules/chui_store/chui_store");
|
|
@@ -22,7 +9,7 @@ const os = require("os");
|
|
|
22
9
|
|
|
23
10
|
const request = require("@cypress/request");
|
|
24
11
|
const { transliterate } = require("transliteration");
|
|
25
|
-
|
|
12
|
+
const downloadSession = undefined;
|
|
26
13
|
// === === ===
|
|
27
14
|
// process.env
|
|
28
15
|
process.env["ELECTRON_DISABLE_SECURITY_WARNINGS"] = "true";
|
|
@@ -99,19 +86,13 @@ const { Details } = require("./framework/components/chui_details/details");
|
|
|
99
86
|
const { Accordion } = require("./framework/components/chui_accordion/accordion");
|
|
100
87
|
const { CodeBlock } = require("./framework/components/chui_code/code");
|
|
101
88
|
const { HtmlBlock } = require("./framework/components/chui_html_block/html_block");
|
|
102
|
-
const { sleep, render,
|
|
89
|
+
const { sleep, render, formatBytes } = require("./framework/modules/chui_functions");
|
|
103
90
|
const {
|
|
104
91
|
Shortcuts,
|
|
105
92
|
Clipboard,
|
|
106
93
|
RecentDocuments,
|
|
107
94
|
TaskbarProgress,
|
|
108
95
|
} = require("./framework/modules/chui_services/chui_services");
|
|
109
|
-
const {
|
|
110
|
-
SERVICES_CHANNELS,
|
|
111
|
-
BRIDGE_CHANNELS,
|
|
112
|
-
BRIDGE_ARGS,
|
|
113
|
-
APP_PATH_NAMES,
|
|
114
|
-
} = require("./framework/modules/chui_services/chui_channels");
|
|
115
96
|
const { Desktop } = require("./framework/modules/chui_services/desktop");
|
|
116
97
|
const { I18n } = require("./framework/modules/chui_i18n/chui_i18n");
|
|
117
98
|
const {
|
|
@@ -169,7 +150,6 @@ const { Form } = require("./framework/components/chui_form/form");
|
|
|
169
150
|
const { SlideShow } = require("./framework/components/chui_slideshow/slideshow");
|
|
170
151
|
const { FieldSet } = require("./framework/components/chui_fieldset/fieldset");
|
|
171
152
|
const { Popup } = require("./framework/components/chui_popups/popups");
|
|
172
|
-
const { TelegramBot } = require("./framework/components/telegram_bot/chui_telegram_bot");
|
|
173
153
|
const { MenuBar } = require("./framework/components/chui_menu_bar/menu_bar");
|
|
174
154
|
const { CommandPalette } = require("./framework/components/chui_command_palette/command_palette");
|
|
175
155
|
const { ContextMenu } = require("./framework/components/chui_context_menu/context_menu");
|
|
@@ -184,1160 +164,26 @@ const {
|
|
|
184
164
|
const { MultiComboBox } = require("./framework/components/chui_inputs/chui_multi_combo_box/multi_combo_box");
|
|
185
165
|
const { Mention } = require("./framework/components/chui_inputs/chui_mention/mention");
|
|
186
166
|
const { Chat } = require("./framework/components/chui_chat/chat");
|
|
167
|
+
const { WriteBar } = require("./framework/components/chui_write_bar/write_bar");
|
|
168
|
+
const { MessageBar } = require("./framework/components/chui_message_bar/message_bar");
|
|
169
|
+
const { UserBar } = require("./framework/components/chui_user_bar/user_bar");
|
|
170
|
+
const { UserListBar } = require("./framework/components/chui_user_list_bar/user_list_bar");
|
|
171
|
+
const { ActionsBar } = require("./framework/components/chui_actions_bar/actions_bar");
|
|
172
|
+
const { Banner } = require("./framework/components/chui_banner/banner");
|
|
173
|
+
const { CardScroll } = require("./framework/components/chui_card_scroll/card_scroll");
|
|
174
|
+
const { ScreenSpinner } = require("./framework/components/chui_screen_spinner/screen_spinner");
|
|
187
175
|
const { ActionSheet } = require("./framework/components/chui_action_sheet/action_sheet");
|
|
188
176
|
const { ImageCompare } = require("./framework/components/chui_media/image_compare");
|
|
189
177
|
const { OrgChart } = require("./framework/components/chui_org_chart/org_chart");
|
|
190
178
|
const { QRCode } = require("./framework/components/chui_qrcode/qrcode");
|
|
191
179
|
const { Log } = require("./framework/modules/chui_logger/chui_logger");
|
|
192
|
-
const
|
|
180
|
+
const { Main } = require("./framework/main_window");
|
|
193
181
|
|
|
194
182
|
/** Код исполняется на странице (renderer), а не в main: от этого зависит доступ к модулям Electron */
|
|
195
183
|
function inRenderer() {
|
|
196
184
|
return process !== undefined && process.type === "renderer";
|
|
197
185
|
}
|
|
198
186
|
|
|
199
|
-
/**
|
|
200
|
-
* Путь приложения по имени из белого списка.
|
|
201
|
-
*
|
|
202
|
-
* Одно правило на два канала (`APP_PATH` и `APP_PATH_SYNC`): странице не должен быть доступен
|
|
203
|
-
* произвольный каталог системы, поэтому всё, что вне `APP_PATH_NAMES`, main отклоняет. Сам
|
|
204
|
-
* `app.getPath` синхронный и без диска — именно поэтому путь можно отдавать значением.
|
|
205
|
-
*/
|
|
206
|
-
function appPathByName(name = String()) {
|
|
207
|
-
if (APP_PATH_NAMES.includes(name) === false) throw new Error(`Desktop.app.path: имя «${name}» вне белого списка`);
|
|
208
|
-
return app.getPath(name);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
//VARS
|
|
212
|
-
let isQuiting = false;
|
|
213
|
-
|
|
214
|
-
class Main {
|
|
215
|
-
/** Имя главного окна в реестре: `main.getWindow()` возвращает именно его */
|
|
216
|
-
static MAIN_WINDOW = "main";
|
|
217
|
-
/** Preload безопасного режима (contextBridge) */
|
|
218
|
-
static SECURE_PRELOAD = path.join(__dirname, "framework/modules/chui_bridge/preload.js");
|
|
219
|
-
/**
|
|
220
|
-
* Строгая политика для безопасных окон: скрипты и стили только свои (и файлы приложения),
|
|
221
|
-
* никаких внешних источников, `connect-src 'none'` — окну не нужна сеть.
|
|
222
|
-
*/
|
|
223
|
-
static SECURE_CSP =
|
|
224
|
-
"default-src 'none'; script-src 'self' file:; style-src 'self' file: 'unsafe-inline'; img-src 'self' file: data: blob:; font-src 'self' file: data:; media-src 'self' file: data: blob:; connect-src 'none'";
|
|
225
|
-
/**
|
|
226
|
-
* Разметка страницы окна: пустая оболочка без собственного кода — всё приходит из
|
|
227
|
-
* preload'а приложения (обычный режим) или из моста (`security: {contextIsolation: true}`,
|
|
228
|
-
* он подключает `render` как `<script>` документа). Файлом в пакете не лежит: страница
|
|
229
|
-
* пишется в userData при первом окне — см. `#windowPage()`. Окно живёт на `file:`
|
|
230
|
-
* документе, потому что только так Chromium разрешает подключать файлы фреймворка.
|
|
231
|
-
*/
|
|
232
|
-
static WINDOW_PAGE =
|
|
233
|
-
'<!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><title>app</title></head><body></body></html>';
|
|
234
|
-
/** Путь к уже записанной странице окна: файл готовится один раз на процесс */
|
|
235
|
-
static #window_page = undefined;
|
|
236
|
-
#tray = undefined;
|
|
237
|
-
//
|
|
238
|
-
#app_icon = undefined;
|
|
239
|
-
#window = undefined;
|
|
240
|
-
// Реестр окон: имя → BrowserWindow (дополнительные окна создаёт createWindow())
|
|
241
|
-
#windows = new Map();
|
|
242
|
-
#before_quit_bound = false;
|
|
243
|
-
#web_request_bound = false;
|
|
244
|
-
//
|
|
245
|
-
#appName = undefined;
|
|
246
|
-
#height = undefined;
|
|
247
|
-
#width = undefined;
|
|
248
|
-
#minHeight = undefined;
|
|
249
|
-
#minWidth = undefined;
|
|
250
|
-
#renderer = undefined;
|
|
251
|
-
#devTools = false;
|
|
252
|
-
#devToolsOpened = false;
|
|
253
|
-
#webSecurity = true;
|
|
254
|
-
#resizable = true;
|
|
255
|
-
// IPC: обработчики регистрируются один раз и снимаются в stop()/restart()
|
|
256
|
-
#ipcRegistered = false;
|
|
257
|
-
#updaterListening = false;
|
|
258
|
-
#lastUpdateInfo = undefined;
|
|
259
|
-
#onShowSystemNotification = (e, title, body) => {
|
|
260
|
-
let { Notification } = require("electron");
|
|
261
|
-
new Notification({ title, body }).show();
|
|
262
|
-
};
|
|
263
|
-
#onSendLogText = (e, text, type) => {
|
|
264
|
-
if (type === "info") {
|
|
265
|
-
console.info(text);
|
|
266
|
-
} else if (type === "error") {
|
|
267
|
-
console.error(text);
|
|
268
|
-
}
|
|
269
|
-
this.#writeLog(text);
|
|
270
|
-
};
|
|
271
|
-
#onUpdateInstallConfirm = (e, check) => {
|
|
272
|
-
if (check) {
|
|
273
|
-
Log.info("Установка обновления...");
|
|
274
|
-
require("electron-updater").autoUpdater.quitAndInstall();
|
|
275
|
-
}
|
|
276
|
-
};
|
|
277
|
-
#onUpdateDownloaded = async () => {
|
|
278
|
-
await this.#sendNotificationUpdateLoadClose();
|
|
279
|
-
await this.#sendNotificationUpdate(this.#appName, I18n.t("appLayout.update.downloaded"));
|
|
280
|
-
setTimeout(async () => await this.#sendNotificationUpdateClose(), 3000);
|
|
281
|
-
Log.info("Обновление скачано!");
|
|
282
|
-
const version =
|
|
283
|
-
this.#lastUpdateInfo === undefined ? app.getVersion() : this.#lastUpdateInfo.versionInfo.version;
|
|
284
|
-
this.#window.webContents.send("checkUpdatesTrue", true, version);
|
|
285
|
-
};
|
|
286
|
-
// Глобальные обработчики ошибок процесса: регистрируются один раз, снимаются в stop()/restart()
|
|
287
|
-
#processHandlersRegistered = false;
|
|
288
|
-
// Десктоп-сервисы (буфер, недавние, прогресс, глобальные клавиши) и их подписки по окнам
|
|
289
|
-
#services_enabled = false;
|
|
290
|
-
#service_handlers = [];
|
|
291
|
-
// Синхронные каналы снимаются не `removeHandler`, а `removeListener`: они зарегистрированы
|
|
292
|
-
// через `ipcMain.on` — см. `#handleSync`
|
|
293
|
-
#service_sync_handlers = [];
|
|
294
|
-
#shortcut_subscribers = new Map();
|
|
295
|
-
#shortcut_watchers = new Map();
|
|
296
|
-
#progress_windows = new Map();
|
|
297
|
-
#on_theme_updated = undefined;
|
|
298
|
-
// Безопасный режим: preload-мост, contextIsolation и CSP вместо Node в рендерере
|
|
299
|
-
#security = { contextIsolation: false, csp: false, channels: [] };
|
|
300
|
-
#secure_contents = new Set();
|
|
301
|
-
#bridge_enabled = false;
|
|
302
|
-
#window_actions_bound = false;
|
|
303
|
-
#theme_bound = false;
|
|
304
|
-
#page_message_handlers = new Map();
|
|
305
|
-
#onUncaughtException = (error) => {
|
|
306
|
-
Main.#logProcessError("Необработанное исключение", error);
|
|
307
|
-
};
|
|
308
|
-
#onUnhandledRejection = (reason) => {
|
|
309
|
-
Main.#logProcessError("Необработанный reject промиса", reason);
|
|
310
|
-
};
|
|
311
|
-
static #logProcessError(prefix, value) {
|
|
312
|
-
const text = `${prefix}: ${Main.#formatError(value)}`;
|
|
313
|
-
try {
|
|
314
|
-
Log.error(text);
|
|
315
|
-
} catch (e) {
|
|
316
|
-
// Сам обработчик ошибок падать не должен (например, недоступен userData для лога)
|
|
317
|
-
console.error(text, e);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
static #formatError(value) {
|
|
321
|
-
if (value === undefined || value === null) return I18n.t("appLayout.error.unknown");
|
|
322
|
-
if (value.stack !== undefined) return value.stack;
|
|
323
|
-
return String(value);
|
|
324
|
-
}
|
|
325
|
-
// ПУТИ: папка загрузок из options.paths — применяется, только если приложение её задало
|
|
326
|
-
#downloadPath = undefined;
|
|
327
|
-
#download_bound = false;
|
|
328
|
-
// Загрузка в заданную папку: имя файла берём у Electron, существующее не перезаписываем
|
|
329
|
-
#onWillDownload = (event, item) => {
|
|
330
|
-
if (this.#downloadPath === undefined) return;
|
|
331
|
-
if (item === undefined || typeof item.setSavePath !== "function") return;
|
|
332
|
-
const target = path.join(this.#downloadPath, item.getFilename());
|
|
333
|
-
if (fs.existsSync(target)) return;
|
|
334
|
-
try {
|
|
335
|
-
if (!fs.existsSync(this.#downloadPath)) fs.mkdirSync(this.#downloadPath, { recursive: true });
|
|
336
|
-
item.setSavePath(target);
|
|
337
|
-
} catch (error) {
|
|
338
|
-
Log.error(`Не удалось сохранить загрузку в ${this.#downloadPath}: ${Main.#formatError(error)}`);
|
|
339
|
-
}
|
|
340
|
-
};
|
|
341
|
-
|
|
342
|
-
constructor(
|
|
343
|
-
options = {
|
|
344
|
-
name: String(),
|
|
345
|
-
sizes: { height: Number(), width: Number(), minHeight: Number(), minWidth: Number() },
|
|
346
|
-
render: String(),
|
|
347
|
-
devTools: Boolean(),
|
|
348
|
-
webSecurity: Boolean(),
|
|
349
|
-
resizable: Boolean(),
|
|
350
|
-
security: { contextIsolation: Boolean(), csp: Boolean(), channels: [] },
|
|
351
|
-
paths: {
|
|
352
|
-
downloadPath: String(),
|
|
353
|
-
},
|
|
354
|
-
}
|
|
355
|
-
) {
|
|
356
|
-
this.#app_icon = options.icon;
|
|
357
|
-
if (this.#app_icon === undefined) {
|
|
358
|
-
if (process.platform === "darwin") {
|
|
359
|
-
this.#app_icon = Main.#resizeIconTray(getDefaultIcon());
|
|
360
|
-
} else {
|
|
361
|
-
this.#app_icon = getDefaultIcon();
|
|
362
|
-
}
|
|
363
|
-
} else {
|
|
364
|
-
if (process.platform === "darwin") {
|
|
365
|
-
this.#app_icon = Main.#resizeIconTray(options.icon);
|
|
366
|
-
} else {
|
|
367
|
-
this.#app_icon = options.icon;
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
// app.commandLine.appendSwitch('--enable-features', 'OverlayScrollbar')
|
|
371
|
-
app.commandLine.appendSwitch("disable-hid-blocklist");
|
|
372
|
-
app.commandLine.appendSwitch("enable-features=OverlayScrollbar");
|
|
373
|
-
// app.commandLine.appendSwitch("disable-http-cache");
|
|
374
|
-
app.commandLine.appendSwitch("enable-transparent-visuals");
|
|
375
|
-
app.commandLine.appendSwitch("enable-gpu-rasterization", "true");
|
|
376
|
-
app.commandLine.appendSwitch("enable-native-gpu-memory-buffers", "true");
|
|
377
|
-
app.commandLine.appendSwitch("high-dpi-support", "true");
|
|
378
|
-
app.commandLine.appendSwitch("device-scale-factor", "true");
|
|
379
|
-
app.commandLine.appendSwitch("disable-touch-adjustment", "true");
|
|
380
|
-
app.commandLine.appendSwitch("main-frame-resizes-are-orientation-changes", "true");
|
|
381
|
-
app.commandLine.appendSwitch("disable-pinch", "true");
|
|
382
|
-
|
|
383
|
-
// Без опции загрузки идут обычным путём Electron: раньше значение вычислялось и не использовалось
|
|
384
|
-
this.#downloadPath = options.paths === undefined ? undefined : options.paths.downloadPath;
|
|
385
|
-
|
|
386
|
-
// app.commandLine.appendSwitch('autoplay-policy', 'no-user-gesture-required');
|
|
387
|
-
|
|
388
|
-
// Options
|
|
389
|
-
this.#appName = options.name;
|
|
390
|
-
this.#height = options.sizes.height;
|
|
391
|
-
this.#width = options.sizes.width;
|
|
392
|
-
this.#minHeight = options.sizes.minHeight;
|
|
393
|
-
this.#minWidth = options.sizes.minWidth;
|
|
394
|
-
this.#renderer = options.render;
|
|
395
|
-
this.#webSecurity = options.webSecurity;
|
|
396
|
-
this.#resizable = options.resizable;
|
|
397
|
-
this.#devTools = options.devTools;
|
|
398
|
-
if (options.security !== undefined) this.#security = Main.#normalizeSecurity(options.security);
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
static #resizeIconTray(image) {
|
|
402
|
-
return require("electron").nativeImage.createFromPath(image).resize({ width: 16, height: 16 });
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
/** Опции безопасности окна: по умолчанию включён привычный режим (Node в рендерере) */
|
|
406
|
-
static #normalizeSecurity(security = Object()) {
|
|
407
|
-
return {
|
|
408
|
-
contextIsolation: security.contextIsolation === true,
|
|
409
|
-
csp: security.csp === undefined ? true : security.csp,
|
|
410
|
-
channels: security.channels === undefined ? [] : security.channels,
|
|
411
|
-
};
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
/** CSP окна: `true` — политика фреймворка, строка — своя, `false` — без политики */
|
|
415
|
-
static #cspFor(security = Object()) {
|
|
416
|
-
if (security.contextIsolation !== true) return undefined;
|
|
417
|
-
if (security.csp === false || security.csp === undefined || security.csp === null) return undefined;
|
|
418
|
-
return security.csp === true ? Main.SECURE_CSP : String(security.csp);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// Аргументы preload'а моста: путь страницы, белый список каналов и CSP
|
|
422
|
-
static #bridgeArguments(render = undefined, security = Object()) {
|
|
423
|
-
const args = [];
|
|
424
|
-
if (render !== undefined) args.push(`${BRIDGE_ARGS.RENDER}${render}`);
|
|
425
|
-
if (security.channels !== undefined && security.channels.length > 0) {
|
|
426
|
-
args.push(`${BRIDGE_ARGS.CHANNELS}${security.channels.join(",")}`);
|
|
427
|
-
}
|
|
428
|
-
const csp = Main.#cspFor(security);
|
|
429
|
-
if (csp !== undefined) args.push(`${BRIDGE_ARGS.CSP}${csp}`);
|
|
430
|
-
return args;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
/**
|
|
434
|
-
* Путь к файлу страницы окна. Разметка держится строкой (`WINDOW_PAGE`), а окно грузится
|
|
435
|
-
* только с `file:` документа (у `data:`-документа Chromium запрещает подресурсы
|
|
436
|
-
* `file:` — «Not allowed to load local resource»), поэтому файл пишется в userData и
|
|
437
|
-
* перезаписывается, если его содержимое отличается от текущей разметки.
|
|
438
|
-
*/
|
|
439
|
-
static #windowPage() {
|
|
440
|
-
if (Main.#window_page !== undefined) return Main.#window_page;
|
|
441
|
-
const page = path.join(app.getPath("userData"), "chui_window.html");
|
|
442
|
-
try {
|
|
443
|
-
if (!fs.existsSync(page) || fs.readFileSync(page, "utf8") !== Main.WINDOW_PAGE) {
|
|
444
|
-
fs.writeFileSync(page, Main.WINDOW_PAGE, "utf8");
|
|
445
|
-
}
|
|
446
|
-
} catch (error) {
|
|
447
|
-
// Без страницы окно не создать: ошибка должна дойти до вызывающего с понятным текстом
|
|
448
|
-
throw new Error(`Не удалось создать страницу окна «${page}»: ${Main.#formatError(error)}`, {
|
|
449
|
-
cause: error,
|
|
450
|
-
});
|
|
451
|
-
}
|
|
452
|
-
Main.#window_page = page;
|
|
453
|
-
return page;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
static #keyToChangeLower(obj, keyToChange, value) {
|
|
457
|
-
let keyToChangeLower = keyToChange.toLowerCase();
|
|
458
|
-
for (const key of Object.keys(obj)) {
|
|
459
|
-
if (key.toLowerCase() === keyToChangeLower) {
|
|
460
|
-
obj[key] = value;
|
|
461
|
-
return;
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
obj[keyToChange] = value;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
/**
|
|
468
|
-
* Создаёт окно и ставит его в реестр. Главное окно (`start()`) и дополнительные
|
|
469
|
-
* (`createWindow()`) собираются одним кодом: у всех окон одинаковые webPreferences,
|
|
470
|
-
* иконка, поведение закрытия и подписка на `chui_resizable_false`.
|
|
471
|
-
*/
|
|
472
|
-
#buildWindow(
|
|
473
|
-
options = {
|
|
474
|
-
name: String(),
|
|
475
|
-
title: String(),
|
|
476
|
-
sizes: Object(),
|
|
477
|
-
render: String(),
|
|
478
|
-
parent: undefined,
|
|
479
|
-
security: Object(),
|
|
480
|
-
hideOnClose: Boolean(),
|
|
481
|
-
resizable: Boolean(),
|
|
482
|
-
devTools: Boolean(),
|
|
483
|
-
}
|
|
484
|
-
) {
|
|
485
|
-
const sizes = options.sizes === undefined ? {} : options.sizes;
|
|
486
|
-
const name = options.name === undefined ? Main.MAIN_WINDOW : options.name;
|
|
487
|
-
const resizable = options.resizable === undefined ? this.#resizable : options.resizable;
|
|
488
|
-
// У окна может быть своя политика: `createWindow({security: {...}})`
|
|
489
|
-
const security = options.security === undefined ? this.#security : Main.#normalizeSecurity(options.security);
|
|
490
|
-
const secure = security.contextIsolation === true;
|
|
491
|
-
this.#window = new BrowserWindow({
|
|
492
|
-
transparent: false,
|
|
493
|
-
width: sizes.width === undefined ? this.#width : sizes.width,
|
|
494
|
-
height: sizes.height === undefined ? this.#height : sizes.height,
|
|
495
|
-
minHeight: sizes.minHeight === undefined ? this.#minHeight : sizes.minHeight,
|
|
496
|
-
minWidth: sizes.minWidth === undefined ? this.#minWidth : sizes.minWidth,
|
|
497
|
-
name: name,
|
|
498
|
-
title: options.title === undefined ? this.#appName : options.title,
|
|
499
|
-
show: false,
|
|
500
|
-
icon: this.#app_icon,
|
|
501
|
-
roundedCorners: true,
|
|
502
|
-
// Безопасный режим: в preload идёт мост фреймворка, а скрипт приложения
|
|
503
|
-
// выполняется как обычный <script> документа (см. chui_bridge/preload.js)
|
|
504
|
-
webPreferences: secure
|
|
505
|
-
? {
|
|
506
|
-
plugins: false,
|
|
507
|
-
nodeIntegration: false,
|
|
508
|
-
contextIsolation: true,
|
|
509
|
-
sandbox: false,
|
|
510
|
-
preload: Main.SECURE_PRELOAD,
|
|
511
|
-
additionalArguments: Main.#bridgeArguments(options.render, security),
|
|
512
|
-
spellcheck: false,
|
|
513
|
-
webviewTag: false,
|
|
514
|
-
webSecurity: this.#webSecurity,
|
|
515
|
-
nodeIntegrationInSubFrames: false,
|
|
516
|
-
}
|
|
517
|
-
: {
|
|
518
|
-
plugins: false,
|
|
519
|
-
nodeIntegration: true,
|
|
520
|
-
contextIsolation: false,
|
|
521
|
-
// Рендерер окна: у дополнительных окон может быть свой скрипт
|
|
522
|
-
preload: options.render === undefined ? this.#renderer : options.render,
|
|
523
|
-
spellcheck: false,
|
|
524
|
-
webviewTag: true,
|
|
525
|
-
webSecurity: this.#webSecurity,
|
|
526
|
-
nodeIntegrationInSubFrames: true,
|
|
527
|
-
},
|
|
528
|
-
frame: false,
|
|
529
|
-
resizable: resizable,
|
|
530
|
-
minimizable: true,
|
|
531
|
-
maximizable: true,
|
|
532
|
-
center: true,
|
|
533
|
-
hasShadow: true,
|
|
534
|
-
parent: options.parent,
|
|
535
|
-
});
|
|
536
|
-
const window = this.#window;
|
|
537
|
-
const webContentsId = window.webContents.id;
|
|
538
|
-
this.#windows.set(name, window);
|
|
539
|
-
if (secure) {
|
|
540
|
-
this.#secure_contents.add(webContentsId);
|
|
541
|
-
this.enableBridge();
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
this.#bindWebRequest(window);
|
|
545
|
-
window
|
|
546
|
-
// Путь из userData может содержать пробелы и кириллицу — приводим его к file: URL
|
|
547
|
-
.loadURL(url.pathToFileURL(Main.#windowPage()).href)
|
|
548
|
-
.then(() => this.#bindBeforeQuit())
|
|
549
|
-
.catch((error) => {
|
|
550
|
-
// Окно закрыли до конца загрузки: навигация прерывается (ERR_FAILED/ERR_ABORTED),
|
|
551
|
-
// это не ошибка загрузки и в лог попадать не должна
|
|
552
|
-
if (window.isDestroyed() || this.#windows.get(name) !== window) return;
|
|
553
|
-
const code = String((error && error.code) || "");
|
|
554
|
-
if (code === "ERR_ABORTED" || code === "ERR_FAILED") return;
|
|
555
|
-
Main.#logProcessError("Не удалось загрузить окно", error);
|
|
556
|
-
});
|
|
557
|
-
|
|
558
|
-
if ((options.devTools === undefined ? this.#devTools : options.devTools) === true) {
|
|
559
|
-
window.webContents.openDevTools();
|
|
560
|
-
if (name === Main.MAIN_WINDOW) this.#devToolsOpened = true;
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
// Окно могли закрыть раньше, чем истекла задержка показа: show() у уничтоженного
|
|
564
|
-
// окна бросает «Object has been destroyed» прямо в таймере и всплывает в main
|
|
565
|
-
window.on("ready-to-show", () =>
|
|
566
|
-
setTimeout(() => {
|
|
567
|
-
if (!window.isDestroyed()) window.show();
|
|
568
|
-
}, 650)
|
|
569
|
-
);
|
|
570
|
-
// Подписки глобальных клавиш привязаны к отправителю: окно закрылось — снимаем их
|
|
571
|
-
window.on("closed", () => {
|
|
572
|
-
if (this.#windows.get(name) === window) this.#windows.delete(name);
|
|
573
|
-
this.#secure_contents.delete(webContentsId);
|
|
574
|
-
this.#dropShortcutSubscriptions(webContentsId);
|
|
575
|
-
this.#progress_windows.delete(webContentsId);
|
|
576
|
-
});
|
|
577
|
-
|
|
578
|
-
if (options.hideOnClose === true) {
|
|
579
|
-
window.on("close", (evt) => {
|
|
580
|
-
if (!isQuiting) evt.preventDefault();
|
|
581
|
-
window.hide();
|
|
582
|
-
});
|
|
583
|
-
}
|
|
584
|
-
if (resizable === false) {
|
|
585
|
-
window.webContents.on("did-finish-load", () => window.webContents.send("chui_resizable_false"));
|
|
586
|
-
}
|
|
587
|
-
return window;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
// `before-quit` нужен один раз на приложение, а не на каждое созданное окно
|
|
591
|
-
#bindBeforeQuit() {
|
|
592
|
-
if (this.#before_quit_bound) return;
|
|
593
|
-
this.#before_quit_bound = true;
|
|
594
|
-
app.on("before-quit", () => {
|
|
595
|
-
isQuiting = true;
|
|
596
|
-
});
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
// Обход CORS при `webSecurity: false`: у сессии один слушатель, а не по одному на окно
|
|
600
|
-
#bindWebRequest(window) {
|
|
601
|
-
if (this.#webSecurity || this.#web_request_bound) return;
|
|
602
|
-
this.#web_request_bound = true;
|
|
603
|
-
window.webContents.session.webRequest.onBeforeSendHeaders((details, callback) => {
|
|
604
|
-
const { requestHeaders } = details;
|
|
605
|
-
Main.#keyToChangeLower(requestHeaders, "Access-Control-Allow-Origin", ["*"]);
|
|
606
|
-
Main.#keyToChangeLower(requestHeaders, "X-Frame-Options", ["*"]);
|
|
607
|
-
callback({ requestHeaders });
|
|
608
|
-
});
|
|
609
|
-
window.webContents.session.webRequest.onHeadersReceived((details, callback) => {
|
|
610
|
-
const { responseHeaders } = details;
|
|
611
|
-
Main.#keyToChangeLower(responseHeaders, "Access-Control-Allow-Origin", ["*"]);
|
|
612
|
-
Main.#keyToChangeLower(responseHeaders, "Access-Control-Allow-Headers", ["*"]);
|
|
613
|
-
Main.#keyToChangeLower(responseHeaders, "X-Frame-Options", ["*"]);
|
|
614
|
-
callback({
|
|
615
|
-
responseHeaders,
|
|
616
|
-
cancel: false,
|
|
617
|
-
});
|
|
618
|
-
});
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
/**
|
|
622
|
-
* Дополнительное окно приложения: свой скрипт рендерера (`render`), свой layout
|
|
623
|
-
* (`AppLayout` регистрирует узлы в своём документе — см. `chui_layout`) и своя запись
|
|
624
|
-
* в реестре окон. Возвращает `BrowserWindow`, поэтому вызывать нужно с `await`:
|
|
625
|
-
* окно создаётся только после `app.whenReady()`.
|
|
626
|
-
*
|
|
627
|
-
* ```javascript
|
|
628
|
-
* const win = await main.createWindow({ name: 'preview', title: 'Просмотр',
|
|
629
|
-
* sizes: {width: 900, height: 600}, render: `${__dirname}/app/preview.js` });
|
|
630
|
-
* main.sendTo('preview', 'preview_update', payload);
|
|
631
|
-
* ```
|
|
632
|
-
*/
|
|
633
|
-
async createWindow(
|
|
634
|
-
options = {
|
|
635
|
-
name: String(),
|
|
636
|
-
title: String(),
|
|
637
|
-
sizes: Object(),
|
|
638
|
-
render: String(),
|
|
639
|
-
parent: undefined,
|
|
640
|
-
security: Object(),
|
|
641
|
-
hideOnClose: Boolean(),
|
|
642
|
-
resizable: Boolean(),
|
|
643
|
-
devTools: Boolean(),
|
|
644
|
-
}
|
|
645
|
-
) {
|
|
646
|
-
await app.whenReady();
|
|
647
|
-
this.enableServices();
|
|
648
|
-
return this.#buildWindow(options);
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
/** Окно по имени реестра; без имени или с `Main.MAIN_WINDOW` — главное окно */
|
|
652
|
-
getWindow(name = Main.MAIN_WINDOW) {
|
|
653
|
-
return this.#windows.get(name);
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
/** Главное окно приложения (создаётся в `start()`) */
|
|
657
|
-
getMainWindow() {
|
|
658
|
-
return this.#windows.get(Main.MAIN_WINDOW);
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
/** Все открытые окна: `[{name, window, secure}]` в порядке создания */
|
|
662
|
-
getWindows() {
|
|
663
|
-
return [...this.#windows].map(([name, window]) => ({
|
|
664
|
-
name: name,
|
|
665
|
-
window: window,
|
|
666
|
-
secure: this.#secure_contents.has(window.webContents.id),
|
|
667
|
-
}));
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
focusWindow(name = Main.MAIN_WINDOW) {
|
|
671
|
-
const window = this.#windows.get(name);
|
|
672
|
-
if (window === undefined) return false;
|
|
673
|
-
if (window.isMinimized()) window.restore();
|
|
674
|
-
window.show();
|
|
675
|
-
window.focus();
|
|
676
|
-
return true;
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
/** Закрывает окно (не приложение) и убирает его из реестра */
|
|
680
|
-
closeWindow(name = Main.MAIN_WINDOW) {
|
|
681
|
-
const window = this.#windows.get(name);
|
|
682
|
-
if (window === undefined) return false;
|
|
683
|
-
window.close();
|
|
684
|
-
return true;
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
/** Сообщение в конкретное окно; без имени — в главное */
|
|
688
|
-
sendTo(name = Main.MAIN_WINDOW, channel = String(), ...args) {
|
|
689
|
-
const window = this.#windows.get(name);
|
|
690
|
-
if (window === undefined || window.isDestroyed()) return false;
|
|
691
|
-
window.webContents.send(channel, ...args);
|
|
692
|
-
return true;
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
/**
|
|
696
|
-
* Папка загрузок из `options.paths.downloadPath`: одна подписка на сессию, снимается
|
|
697
|
-
* в `#removeGlobalHandlers()`. Без опции Electron выбирает путь загрузки сам.
|
|
698
|
-
*/
|
|
699
|
-
#bindDownloadPath() {
|
|
700
|
-
if (this.#downloadPath === undefined || this.#download_bound) return;
|
|
701
|
-
this.#download_bound = true;
|
|
702
|
-
session.defaultSession.on("will-download", this.#onWillDownload);
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
/**
|
|
706
|
-
* Включает десктоп-сервисы: обработчики IPC для `Desktop` (буфер, недавние документы,
|
|
707
|
-
* прогресс на иконке, глобальные клавиши) и сами сервисы main-процесса.
|
|
708
|
-
* Вызывается один раз из `start()` и `createWindow()`.
|
|
709
|
-
*/
|
|
710
|
-
enableServices() {
|
|
711
|
-
if (this.#services_enabled) return this;
|
|
712
|
-
this.#services_enabled = true;
|
|
713
|
-
// Кнопки окна: каналы нужны и обычным окнам — в них `AppLayout` зовёт их по IPC,
|
|
714
|
-
// потому что `@electron/remote` в рендерере больше нет
|
|
715
|
-
this.#enableWindowActions();
|
|
716
|
-
this.services = {
|
|
717
|
-
shortcuts: new Shortcuts(),
|
|
718
|
-
shell: new Shell(),
|
|
719
|
-
clipboard: new Clipboard(),
|
|
720
|
-
recent: new RecentDocuments(),
|
|
721
|
-
progress: new TaskbarProgress(),
|
|
722
|
-
theme: new Theme(store),
|
|
723
|
-
locale: new Locale(store),
|
|
724
|
-
clipboard_history: new ClipboardHistoryService({ store: store }),
|
|
725
|
-
print: new Print(),
|
|
726
|
-
};
|
|
727
|
-
// Сохранённая тема применяется до создания окна: иначе при запуске была бы видна
|
|
728
|
-
// вспышка системной палитры
|
|
729
|
-
this.services.theme.restore();
|
|
730
|
-
// Язык применяется и в main-процессе: его строки (меню, уведомления об обновлении)
|
|
731
|
-
// собираются там же, где живёт сервис локали
|
|
732
|
-
this.services.locale.restore();
|
|
733
|
-
I18n.setLocale(this.services.locale.get(), true);
|
|
734
|
-
this.#bindThemeEvents();
|
|
735
|
-
this.#bindDownloadPath();
|
|
736
|
-
this.#handle(SERVICES_CHANNELS.CLIPBOARD_READ_TEXT, () => this.services.clipboard.readText());
|
|
737
|
-
this.#handle(SERVICES_CHANNELS.CLIPBOARD_WRITE_TEXT, (event, text) => {
|
|
738
|
-
this.services.clipboard.writeText(text);
|
|
739
|
-
// Всё, что приложение кладёт в буфер само, должно попасть в историю
|
|
740
|
-
this.services.clipboard_history.add(text);
|
|
741
|
-
this.#emitClipboardHistory();
|
|
742
|
-
return true;
|
|
743
|
-
});
|
|
744
|
-
this.#handle(SERVICES_CHANNELS.CLIPBOARD_HISTORY_LIST, () => this.services.clipboard_history.list());
|
|
745
|
-
this.#handle(SERVICES_CHANNELS.CLIPBOARD_HISTORY_ADD, (event, text) => {
|
|
746
|
-
this.services.clipboard_history.add(text);
|
|
747
|
-
this.#emitClipboardHistory();
|
|
748
|
-
return this.services.clipboard_history.list();
|
|
749
|
-
});
|
|
750
|
-
this.#handle(SERVICES_CHANNELS.CLIPBOARD_HISTORY_COPY, (event, text) => {
|
|
751
|
-
this.services.clipboard_history.copy(text);
|
|
752
|
-
this.#emitClipboardHistory();
|
|
753
|
-
return this.services.clipboard_history.list();
|
|
754
|
-
});
|
|
755
|
-
this.#handle(SERVICES_CHANNELS.CLIPBOARD_HISTORY_REMOVE, (event, target) => {
|
|
756
|
-
this.services.clipboard_history.remove(target);
|
|
757
|
-
this.#emitClipboardHistory();
|
|
758
|
-
return this.services.clipboard_history.list();
|
|
759
|
-
});
|
|
760
|
-
this.#handle(SERVICES_CHANNELS.CLIPBOARD_HISTORY_CLEAR, () => {
|
|
761
|
-
this.services.clipboard_history.clear();
|
|
762
|
-
this.#emitClipboardHistory();
|
|
763
|
-
return this.services.clipboard_history.list();
|
|
764
|
-
});
|
|
765
|
-
this.#handle(SERVICES_CHANNELS.CLIPBOARD_HAS, (event, format) => this.services.clipboard.has(format));
|
|
766
|
-
this.#handle(SERVICES_CHANNELS.CLIPBOARD_READ_IMAGE, () => this.services.clipboard.readImageDataURL());
|
|
767
|
-
this.#handle(SERVICES_CHANNELS.CLIPBOARD_WRITE_IMAGE, (event, dataURL) => {
|
|
768
|
-
this.services.clipboard.writeImageDataURL(dataURL);
|
|
769
|
-
return true;
|
|
770
|
-
});
|
|
771
|
-
this.#handle(SERVICES_CHANNELS.CLIPBOARD_CLEAR, () => {
|
|
772
|
-
this.services.clipboard.clear();
|
|
773
|
-
return true;
|
|
774
|
-
});
|
|
775
|
-
// Приложение и системные диалоги для страницы: Electron советует IPC вместо `remote`.
|
|
776
|
-
// Путь — только из белого списка, диалог и сессия — у окна-отправителя.
|
|
777
|
-
this.#handle(SERVICES_CHANNELS.APP_PATH, (event, name) => appPathByName(name));
|
|
778
|
-
// `Desktop.app.path` отвечает значением, а не промисом, поэтому у пути есть второй,
|
|
779
|
-
// синхронный канал: `sendSync` ждёт `event.returnValue`, а не ответ `handle`.
|
|
780
|
-
// Асинхронный канал выше оставлен — его ключ есть в карте `window.chui.services`
|
|
781
|
-
this.#handleSync(SERVICES_CHANNELS.APP_PATH_SYNC, (event, name) => appPathByName(name));
|
|
782
|
-
this.#handle(SERVICES_CHANNELS.APP_INFO, () => ({
|
|
783
|
-
name: app.getName(),
|
|
784
|
-
version: app.getVersion(),
|
|
785
|
-
locale: app.getLocale(),
|
|
786
|
-
systemLocale: app.getSystemLocale(),
|
|
787
|
-
isPackaged: app.isPackaged,
|
|
788
|
-
}));
|
|
789
|
-
// Закрытие и перезапуск со страницы: страница — код самого приложения, поэтому кнопке
|
|
790
|
-
// «Выход» не нужен свой канал в каждом проекте. Идут через `Main.stop()`/`restart()` —
|
|
791
|
-
// те же шаги, что у пункта меню: снимаются обработчики сервисов, глобальные клавиши и
|
|
792
|
-
// подписки, а не просто закрывается окно. Ответ уходит странице до выхода
|
|
793
|
-
// (`setImmediate`): иначе промис отваливался бы с «Render frame was disposed», и
|
|
794
|
-
// `await Desktop.app.quit()` со страницы выглядел бы ошибкой
|
|
795
|
-
this.#handle(SERVICES_CHANNELS.APP_QUIT, () => {
|
|
796
|
-
setImmediate(() => this.stop());
|
|
797
|
-
return true;
|
|
798
|
-
});
|
|
799
|
-
this.#handle(SERVICES_CHANNELS.APP_RESTART, () => {
|
|
800
|
-
setImmediate(() => this.restart());
|
|
801
|
-
return true;
|
|
802
|
-
});
|
|
803
|
-
// У закрытия и перезапуска есть и синхронные каналы: `Desktop.app.quit()`/`restart()`
|
|
804
|
-
// отвечают значением, а не промисом — как `Desktop.app.path`. Выход по-прежнему
|
|
805
|
-
// откладывается `setImmediate`, чтобы `event.returnValue` успел уйти странице.
|
|
806
|
-
this.#handleSync(SERVICES_CHANNELS.APP_QUIT_SYNC, () => {
|
|
807
|
-
setImmediate(() => this.stop());
|
|
808
|
-
return true;
|
|
809
|
-
});
|
|
810
|
-
this.#handleSync(SERVICES_CHANNELS.APP_RESTART_SYNC, () => {
|
|
811
|
-
setImmediate(() => this.restart());
|
|
812
|
-
return true;
|
|
813
|
-
});
|
|
814
|
-
this.#handle(SERVICES_CHANNELS.DIALOG_OPEN, (event, options) =>
|
|
815
|
-
Main.#showDialog(event, "showOpenDialog", options)
|
|
816
|
-
);
|
|
817
|
-
this.#handle(SERVICES_CHANNELS.DIALOG_SAVE, (event, options) =>
|
|
818
|
-
Main.#showDialog(event, "showSaveDialog", options)
|
|
819
|
-
);
|
|
820
|
-
this.#handle(SERVICES_CHANNELS.DIALOG_MESSAGE, (event, options) =>
|
|
821
|
-
Main.#showDialog(event, "showMessageBox", options)
|
|
822
|
-
);
|
|
823
|
-
this.#handle(SERVICES_CHANNELS.SESSION_CACHE_SIZE, (event) => event.sender.session.getCacheSize());
|
|
824
|
-
this.#handle(SERVICES_CHANNELS.SESSION_CLEAR_CACHE, (event) => event.sender.session.clearCache());
|
|
825
|
-
this.#handle(SERVICES_CHANNELS.SESSION_CLEAR_STORAGE, (event, options) =>
|
|
826
|
-
event.sender.session.clearStorageData(options)
|
|
827
|
-
);
|
|
828
|
-
this.#handle(SERVICES_CHANNELS.RECENT_ADD, (event, path, name) => {
|
|
829
|
-
this.services.recent.add(path, name);
|
|
830
|
-
return true;
|
|
831
|
-
});
|
|
832
|
-
this.#handle(SERVICES_CHANNELS.RECENT_CLEAR, () => {
|
|
833
|
-
this.services.recent.clear();
|
|
834
|
-
return true;
|
|
835
|
-
});
|
|
836
|
-
this.#handle(SERVICES_CHANNELS.RECENT_LIST, () => this.services.recent.get());
|
|
837
|
-
this.#handle(SERVICES_CHANNELS.PROGRESS_SET, (event, value, mode) => {
|
|
838
|
-
this.#progressFor(event).set(value, mode);
|
|
839
|
-
return true;
|
|
840
|
-
});
|
|
841
|
-
this.#handle(SERVICES_CHANNELS.PROGRESS_CLEAR, (event) => {
|
|
842
|
-
this.#progressFor(event).clear();
|
|
843
|
-
return true;
|
|
844
|
-
});
|
|
845
|
-
this.#handle(SERVICES_CHANNELS.SHELL_OPEN_EXTERNAL, (event, url) => this.services.shell.openExternal(url));
|
|
846
|
-
this.#handle(SERVICES_CHANNELS.PRINT_PAGE, (event, options) =>
|
|
847
|
-
this.services.print.page(BrowserWindow.fromWebContents(event.sender), this.#printOptions(event, options))
|
|
848
|
-
);
|
|
849
|
-
this.#handle(SERVICES_CHANNELS.PRINT_HTML, (event, html, options) =>
|
|
850
|
-
this.services.print.html(html, this.#printOptions(event, options))
|
|
851
|
-
);
|
|
852
|
-
this.#handle(SERVICES_CHANNELS.PRINT_PDF, (event, html, options) =>
|
|
853
|
-
this.services.print.pdf(html, this.#printOptions(event, options))
|
|
854
|
-
);
|
|
855
|
-
this.#handle(SERVICES_CHANNELS.THEME_GET, () => ({
|
|
856
|
-
theme: this.services.theme.get(),
|
|
857
|
-
source: this.services.theme.getSource(),
|
|
858
|
-
}));
|
|
859
|
-
this.#handle(SERVICES_CHANNELS.THEME_SET, (event, source) => {
|
|
860
|
-
this.services.theme.set(source);
|
|
861
|
-
return { theme: this.services.theme.get(), source: this.services.theme.getSource() };
|
|
862
|
-
});
|
|
863
|
-
this.#handle(SERVICES_CHANNELS.THEME_TOGGLE, () => {
|
|
864
|
-
this.services.theme.toggle();
|
|
865
|
-
return { theme: this.services.theme.get(), source: this.services.theme.getSource() };
|
|
866
|
-
});
|
|
867
|
-
this.#handle(SERVICES_CHANNELS.LOCALE_GET, () => this.services.locale.get());
|
|
868
|
-
this.#handle(SERVICES_CHANNELS.LOCALE_SET, (event, locale) => {
|
|
869
|
-
const applied = this.services.locale.set(locale);
|
|
870
|
-
// Язык main-процесса меняется вместе с окнами: иначе меню и уведомления об обновлении
|
|
871
|
-
// остались бы на прежнем языке
|
|
872
|
-
I18n.setLocale(applied, true);
|
|
873
|
-
// Локаль, в отличие от темы, не имеет системного события — окна узнают о смене отсюда
|
|
874
|
-
this.#emitLocale(applied);
|
|
875
|
-
return applied;
|
|
876
|
-
});
|
|
877
|
-
this.#handle(SERVICES_CHANNELS.SHORTCUT_SUBSCRIBE, (event, accelerator) =>
|
|
878
|
-
this.#subscribeShortcut(event, accelerator)
|
|
879
|
-
);
|
|
880
|
-
this.#handle(SERVICES_CHANNELS.SHORTCUT_UNSUBSCRIBE, (event, accelerator) => {
|
|
881
|
-
this.#unsubscribeShortcut(event, accelerator);
|
|
882
|
-
return true;
|
|
883
|
-
});
|
|
884
|
-
return this;
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
// ipcMain.handle + запись для снятия в stop()/restart()
|
|
888
|
-
#handle(channel = String(), handler = () => {}) {
|
|
889
|
-
ipcMain.handle(channel, handler);
|
|
890
|
-
this.#service_handlers.push([channel, handler]);
|
|
891
|
-
return this;
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
/**
|
|
895
|
-
* Синхронный канал: `ipcRenderer.sendSync` в странице ждёт `event.returnValue`, а не ответ
|
|
896
|
-
* `handle`, поэтому здесь `ipcMain.on`. Ответ — конверт `{value}`/`{error}`: у синхронного
|
|
897
|
-
* канала нет способа передать исключение, и без конверта страница получила бы `undefined`
|
|
898
|
-
* вместо причины отказа (см. `SERVICES_SYNC_CALLS` в chui_channels.js).
|
|
899
|
-
*/
|
|
900
|
-
#handleSync(channel = String(), handler = () => {}) {
|
|
901
|
-
const listener = (event, ...args) => {
|
|
902
|
-
try {
|
|
903
|
-
event.returnValue = { value: handler(event, ...args) };
|
|
904
|
-
} catch (error) {
|
|
905
|
-
event.returnValue = { error: error instanceof Error ? error.message : String(error) };
|
|
906
|
-
}
|
|
907
|
-
};
|
|
908
|
-
ipcMain.on(channel, listener);
|
|
909
|
-
this.#service_sync_handlers.push([channel, listener]);
|
|
910
|
-
return this;
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
/**
|
|
914
|
-
* Включает мост безопасных окон (`contextBridge`): управление окном, тема и прикладные
|
|
915
|
-
* сообщения по белому списку каналов. Вызывается сам при создании безопасного окна.
|
|
916
|
-
*/
|
|
917
|
-
/** Диалог в окне отправителя: без окна (уже закрыто) Electron зовётся без родителя */
|
|
918
|
-
static #showDialog(event, method = String(), options = Object()) {
|
|
919
|
-
const window = BrowserWindow.fromWebContents(event.sender);
|
|
920
|
-
return window === null ? dialog[method](options) : dialog[method](window, options);
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
/**
|
|
924
|
-
* Каналы кнопок окна. Ими пользуются оба режима: безопасное окно — через мост
|
|
925
|
-
* `window.chui.window`, обычное — прямым `ipcRenderer.invoke`. Регистрируются один раз на приложение.
|
|
926
|
-
*/
|
|
927
|
-
#enableWindowActions() {
|
|
928
|
-
if (this.#window_actions_bound) return this;
|
|
929
|
-
this.#window_actions_bound = true;
|
|
930
|
-
this.#handle(BRIDGE_CHANNELS.WINDOW_ACTION, (event, action) => Main.#applyWindowAction(event.sender, action));
|
|
931
|
-
this.#handle(BRIDGE_CHANNELS.WINDOW_STATE, (event) => {
|
|
932
|
-
const window = BrowserWindow.fromWebContents(event.sender);
|
|
933
|
-
if (window === null) return {};
|
|
934
|
-
return {
|
|
935
|
-
isMaximized: window.isMaximized(),
|
|
936
|
-
isMinimized: window.isMinimized(),
|
|
937
|
-
isFullScreen: window.isFullScreen(),
|
|
938
|
-
isFocused: window.isFocused(),
|
|
939
|
-
};
|
|
940
|
-
});
|
|
941
|
-
return this;
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
enableBridge() {
|
|
945
|
-
if (this.#bridge_enabled) return this;
|
|
946
|
-
this.#bridge_enabled = true;
|
|
947
|
-
this.#enableWindowActions();
|
|
948
|
-
this.#handle(BRIDGE_CHANNELS.THEME_GET, () => (nativeTheme.shouldUseDarkColors ? "dark" : "light"));
|
|
949
|
-
this.#handle(BRIDGE_CHANNELS.LOCALE_GET, () => this.services.locale.get());
|
|
950
|
-
this.#handle(BRIDGE_CHANNELS.MESSAGE, (event, channel, ...args) => {
|
|
951
|
-
const handler = this.#page_message_handlers.get(channel);
|
|
952
|
-
if (handler === undefined) return { handled: false };
|
|
953
|
-
return handler(...args, event);
|
|
954
|
-
});
|
|
955
|
-
this.#bindThemeEvents();
|
|
956
|
-
return this;
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
/**
|
|
960
|
-
* Обработчик сообщений из безопасного окна. Канал должен быть указан в
|
|
961
|
-
* `security: {channels: [...]}`, иначе мост его не отправит.
|
|
962
|
-
*
|
|
963
|
-
* ```javascript
|
|
964
|
-
* main.onPageMessage('preview:save', (payload) => console.log(payload));
|
|
965
|
-
* main.sendToPage('preview:update', {ready: true}, 'preview');
|
|
966
|
-
* ```
|
|
967
|
-
*/
|
|
968
|
-
onPageMessage(channel = String(), handler = () => {}) {
|
|
969
|
-
this.enableBridge();
|
|
970
|
-
this.#page_message_handlers.set(channel, handler);
|
|
971
|
-
return this;
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
/** Сообщение в страницу безопасного окна: его `messaging.on(channel)` получит полезную нагрузку */
|
|
975
|
-
sendToPage(channel = String(), payload = undefined, windowName = undefined) {
|
|
976
|
-
return this.#send(BRIDGE_CHANNELS.MESSAGE, [channel, payload], windowName);
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
/** Действия кнопок окна: и обычные окна, и безопасные зовут их по IPC */
|
|
980
|
-
static #applyWindowAction(sender, action = String()) {
|
|
981
|
-
const window = BrowserWindow.fromWebContents(sender);
|
|
982
|
-
if (window === null || window.isDestroyed()) return false;
|
|
983
|
-
if (action === "minimize") window.minimize();
|
|
984
|
-
else if (action === "toggle-maximize") {
|
|
985
|
-
if (window.isMaximized()) window.unmaximize();
|
|
986
|
-
else window.maximize();
|
|
987
|
-
} else if (action === "restore") window.restore();
|
|
988
|
-
else if (action === "toggle-fullscreen") window.setFullScreen(!window.isFullScreen());
|
|
989
|
-
else if (action === "close") window.close();
|
|
990
|
-
else return false;
|
|
991
|
-
return true;
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
// Смена темы: системную ловим событием Electron, выбранную пользователем — сами.
|
|
995
|
-
// Слушатель один на приложение, окна получают событие по своим каналам
|
|
996
|
-
#bindThemeEvents() {
|
|
997
|
-
if (this.#theme_bound) return;
|
|
998
|
-
this.#theme_bound = true;
|
|
999
|
-
this.#on_theme_updated = () => this.#emitTheme();
|
|
1000
|
-
nativeTheme.on("updated", this.#on_theme_updated);
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
/**
|
|
1004
|
-
* Сообщить окнам текущую тему. Обычные окна слушают `Desktop.theme.onChange`,
|
|
1005
|
-
* безопасные — мост `window.chui.theme.onChange`.
|
|
1006
|
-
*
|
|
1007
|
-
* Вызывается из события `nativeTheme` — оно приходит и от системы, и после
|
|
1008
|
-
* `theme.set()/toggle()` (Electron сообщает о смене `shouldUseDarkColors`).
|
|
1009
|
-
* Поэтому обработчики IPC ничего дополнительно не рассылают: смену источника
|
|
1010
|
-
* без смены палитры подписчикам темы сообщать не о чем.
|
|
1011
|
-
*/
|
|
1012
|
-
#emitTheme() {
|
|
1013
|
-
const theme = nativeTheme.shouldUseDarkColors ? "dark" : "light";
|
|
1014
|
-
for (const window of this.#windows.values()) {
|
|
1015
|
-
if (window.isDestroyed()) continue;
|
|
1016
|
-
window.webContents.send(SERVICES_CHANNELS.THEME_EVENT, theme);
|
|
1017
|
-
}
|
|
1018
|
-
for (const id of this.#secure_contents) {
|
|
1019
|
-
const contents = webContents.fromId(id);
|
|
1020
|
-
if (contents === undefined || contents.isDestroyed()) continue;
|
|
1021
|
-
contents.send(BRIDGE_CHANNELS.THEME_EVENT, theme);
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
/**
|
|
1026
|
-
* Сообщить окнам текущий язык. Обычные окна слушают `Desktop.locale.onChange`,
|
|
1027
|
-
* безопасные — мост `window.chui.locale.onChange`.
|
|
1028
|
-
*/
|
|
1029
|
-
#emitLocale(locale = String()) {
|
|
1030
|
-
for (const window of this.#windows.values()) {
|
|
1031
|
-
if (window.isDestroyed()) continue;
|
|
1032
|
-
window.webContents.send(SERVICES_CHANNELS.LOCALE_EVENT, locale);
|
|
1033
|
-
}
|
|
1034
|
-
for (const id of this.#secure_contents) {
|
|
1035
|
-
const contents = webContents.fromId(id);
|
|
1036
|
-
if (contents === undefined || contents.isDestroyed()) continue;
|
|
1037
|
-
contents.send(BRIDGE_CHANNELS.LOCALE_EVENT, locale);
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
/** История буфера меняется в main — панели в окнах обновляются по этому событию */
|
|
1042
|
-
#emitClipboardHistory() {
|
|
1043
|
-
const items = this.services.clipboard_history.list();
|
|
1044
|
-
for (const window of this.#windows.values()) {
|
|
1045
|
-
if (window.isDestroyed()) continue;
|
|
1046
|
-
window.webContents.send(SERVICES_CHANNELS.CLIPBOARD_HISTORY_EVENT, items);
|
|
1047
|
-
}
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
// Прогресс считаем по окну-отправителю: у каждого окна своя иконка
|
|
1051
|
-
#progressFor(event) {
|
|
1052
|
-
const id = event.sender.id;
|
|
1053
|
-
let progress = this.#progress_windows.get(id);
|
|
1054
|
-
if (progress === undefined) {
|
|
1055
|
-
progress = new TaskbarProgress(BrowserWindow.fromWebContents(event.sender));
|
|
1056
|
-
this.#progress_windows.set(id, progress);
|
|
1057
|
-
}
|
|
1058
|
-
return progress;
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
/**
|
|
1062
|
-
* Подписка окна на глобальную клавишу. Клавиша регистрируется один раз на все окна;
|
|
1063
|
-
* `false` в ответе означает, что комбинацию занял другой процесс.
|
|
1064
|
-
*/
|
|
1065
|
-
#subscribeShortcut(event, accelerator = String()) {
|
|
1066
|
-
const id = event.sender.id;
|
|
1067
|
-
const subscribers = this.#shortcut_subscribers.get(accelerator) ?? new Set();
|
|
1068
|
-
const first = subscribers.size === 0;
|
|
1069
|
-
if (first && !this.services.shortcuts.register(accelerator, () => this.#emitShortcut(accelerator))) {
|
|
1070
|
-
return false;
|
|
1071
|
-
}
|
|
1072
|
-
subscribers.add(id);
|
|
1073
|
-
this.#shortcut_subscribers.set(accelerator, subscribers);
|
|
1074
|
-
const watched = this.#shortcut_watchers.get(id) ?? new Set();
|
|
1075
|
-
watched.add(accelerator);
|
|
1076
|
-
this.#shortcut_watchers.set(id, watched);
|
|
1077
|
-
return true;
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
#unsubscribeShortcut(event, accelerator = String()) {
|
|
1081
|
-
const id = event.sender.id;
|
|
1082
|
-
const subscribers = this.#shortcut_subscribers.get(accelerator);
|
|
1083
|
-
if (subscribers !== undefined) {
|
|
1084
|
-
subscribers.delete(id);
|
|
1085
|
-
if (subscribers.size === 0) {
|
|
1086
|
-
this.#shortcut_subscribers.delete(accelerator);
|
|
1087
|
-
this.services.shortcuts.unregister(accelerator);
|
|
1088
|
-
}
|
|
1089
|
-
}
|
|
1090
|
-
const watched = this.#shortcut_watchers.get(id);
|
|
1091
|
-
if (watched !== undefined) {
|
|
1092
|
-
watched.delete(accelerator);
|
|
1093
|
-
if (watched.size === 0) this.#shortcut_watchers.delete(id);
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
/**
|
|
1098
|
-
* Опции печати из окна: диалог сохранения PDF должен быть модальным к окну-отправителю,
|
|
1099
|
-
* поэтому `parent` берём из события, а не из запроса страницы.
|
|
1100
|
-
*/
|
|
1101
|
-
#printOptions(event, options = Object()) {
|
|
1102
|
-
return { ...options, parent: BrowserWindow.fromWebContents(event.sender) };
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
#emitShortcut(accelerator = String()) {
|
|
1106
|
-
const subscribers = this.#shortcut_subscribers.get(accelerator);
|
|
1107
|
-
if (subscribers === undefined) return;
|
|
1108
|
-
for (const id of [...subscribers]) {
|
|
1109
|
-
const contents = webContents.fromId(id);
|
|
1110
|
-
if (contents === undefined || contents.isDestroyed()) continue;
|
|
1111
|
-
contents.send(SERVICES_CHANNELS.SHORTCUT_EVENT, accelerator);
|
|
1112
|
-
}
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
// Окно закрылось: его подписки больше не нужны, а осиротевшие клавиши освобождаем
|
|
1116
|
-
#dropShortcutSubscriptions(webContentsId = Number()) {
|
|
1117
|
-
const watched = this.#shortcut_watchers.get(webContentsId);
|
|
1118
|
-
if (watched === undefined) return;
|
|
1119
|
-
this.#shortcut_watchers.delete(webContentsId);
|
|
1120
|
-
for (const accelerator of watched) {
|
|
1121
|
-
const subscribers = this.#shortcut_subscribers.get(accelerator);
|
|
1122
|
-
if (subscribers === undefined) continue;
|
|
1123
|
-
subscribers.delete(webContentsId);
|
|
1124
|
-
if (subscribers.size !== 0) continue;
|
|
1125
|
-
this.#shortcut_subscribers.delete(accelerator);
|
|
1126
|
-
this.services.shortcuts.unregister(accelerator);
|
|
1127
|
-
}
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
toggleDevTools(name = Main.MAIN_WINDOW) {
|
|
1131
|
-
const window = this.#windows.get(name);
|
|
1132
|
-
if (window === undefined) return false;
|
|
1133
|
-
if (this.#devToolsOpened && name === Main.MAIN_WINDOW) {
|
|
1134
|
-
window.webContents.closeDevTools();
|
|
1135
|
-
this.#devToolsOpened = false;
|
|
1136
|
-
return true;
|
|
1137
|
-
}
|
|
1138
|
-
window.webContents.openDevTools();
|
|
1139
|
-
if (name === Main.MAIN_WINDOW) this.#devToolsOpened = true;
|
|
1140
|
-
return true;
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
#hideAndShow() {
|
|
1144
|
-
const window = this.#targetWindow();
|
|
1145
|
-
if (window === undefined) return;
|
|
1146
|
-
let visible = window.isVisible();
|
|
1147
|
-
let focus = window.isFocused();
|
|
1148
|
-
|
|
1149
|
-
if (visible && focus) {
|
|
1150
|
-
window.hide();
|
|
1151
|
-
} else if (!visible && !focus) {
|
|
1152
|
-
window.show();
|
|
1153
|
-
} else {
|
|
1154
|
-
window.focus();
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
// Окно-адресат: по умолчанию главное, но уведомление можно отправить и в своё окно
|
|
1159
|
-
#targetWindow(name = undefined) {
|
|
1160
|
-
return this.#windows.get(name === undefined ? Main.MAIN_WINDOW : name) ?? this.#window;
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
#send(channel = String(), args = [], windowName = undefined) {
|
|
1164
|
-
const window = this.#targetWindow(windowName);
|
|
1165
|
-
if (window === undefined || window.isDestroyed()) return false;
|
|
1166
|
-
window.webContents.send(channel, ...args);
|
|
1167
|
-
return true;
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
stop() {
|
|
1171
|
-
this.#removeGlobalHandlers();
|
|
1172
|
-
if (process.platform !== "darwin") {
|
|
1173
|
-
app.quit();
|
|
1174
|
-
} else {
|
|
1175
|
-
app.exit(0);
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
|
-
restart() {
|
|
1180
|
-
this.#removeGlobalHandlers();
|
|
1181
|
-
app.relaunch();
|
|
1182
|
-
app.exit(0);
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1185
|
-
#removeGlobalHandlers() {
|
|
1186
|
-
ipcMain.removeListener("show_system_notification", this.#onShowSystemNotification);
|
|
1187
|
-
ipcMain.removeListener("SEND_LOG_TEXT", this.#onSendLogText);
|
|
1188
|
-
ipcMain.removeListener("updateInstallConfirm", this.#onUpdateInstallConfirm);
|
|
1189
|
-
// Папка загрузок: подписка на сессию снимается вместе с остальными глобальными обработчиками
|
|
1190
|
-
if (this.#download_bound) {
|
|
1191
|
-
this.#download_bound = false;
|
|
1192
|
-
session.defaultSession.removeListener("will-download", this.#onWillDownload);
|
|
1193
|
-
}
|
|
1194
|
-
// Десктоп-сервисы: снимаем обработчики IPC, подписки клавиш и сами клавиши
|
|
1195
|
-
for (const [channel] of this.#service_handlers) ipcMain.removeHandler(channel);
|
|
1196
|
-
this.#service_handlers = [];
|
|
1197
|
-
for (const [channel, listener] of this.#service_sync_handlers) ipcMain.removeListener(channel, listener);
|
|
1198
|
-
this.#service_sync_handlers = [];
|
|
1199
|
-
this.#shortcut_subscribers.clear();
|
|
1200
|
-
this.#shortcut_watchers.clear();
|
|
1201
|
-
this.#progress_windows.clear();
|
|
1202
|
-
if (this.services !== undefined) this.services.shortcuts.destroy();
|
|
1203
|
-
this.#services_enabled = false;
|
|
1204
|
-
// Мост безопасных окон: обработчики ipcMain уже сняты выше, остались события темы
|
|
1205
|
-
if (this.#theme_bound && this.#on_theme_updated !== undefined) {
|
|
1206
|
-
nativeTheme.removeListener("updated", this.#on_theme_updated);
|
|
1207
|
-
}
|
|
1208
|
-
this.#theme_bound = false;
|
|
1209
|
-
this.#on_theme_updated = undefined;
|
|
1210
|
-
this.#page_message_handlers.clear();
|
|
1211
|
-
this.#bridge_enabled = false;
|
|
1212
|
-
this.#window_actions_bound = false;
|
|
1213
|
-
if (this.#updaterListening) {
|
|
1214
|
-
this.#updaterListening = false;
|
|
1215
|
-
require("electron-updater").autoUpdater.removeListener("update-downloaded", this.#onUpdateDownloaded);
|
|
1216
|
-
}
|
|
1217
|
-
if (this.#processHandlersRegistered) {
|
|
1218
|
-
this.#processHandlersRegistered = false;
|
|
1219
|
-
process.removeListener("uncaughtException", this.#onUncaughtException);
|
|
1220
|
-
process.removeListener("unhandledRejection", this.#onUnhandledRejection);
|
|
1221
|
-
}
|
|
1222
|
-
this.#ipcRegistered = false;
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
|
-
start(options = { hideOnClose: Boolean(), globalMenu: [], tray: [], extensions: [] }) {
|
|
1226
|
-
nativeTheme.themeSource = "system";
|
|
1227
|
-
// Обработчики процесса — один раз на экземпляр Main (иначе копились при повторном start())
|
|
1228
|
-
if (this.#ipcRegistered === false) {
|
|
1229
|
-
this.#ipcRegistered = true;
|
|
1230
|
-
ipcMain.on("show_system_notification", this.#onShowSystemNotification);
|
|
1231
|
-
ipcMain.on("SEND_LOG_TEXT", this.#onSendLogText);
|
|
1232
|
-
}
|
|
1233
|
-
// Необработанные ошибки main-процесса — в тот же лог, что и Log.error (консоль + файл)
|
|
1234
|
-
if (this.#processHandlersRegistered === false) {
|
|
1235
|
-
this.#processHandlersRegistered = true;
|
|
1236
|
-
process.on("uncaughtException", this.#onUncaughtException);
|
|
1237
|
-
process.on("unhandledRejection", this.#onUnhandledRejection);
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
app.whenReady().then(() => {
|
|
1241
|
-
session.defaultSession.setPermissionRequestHandler((webContents, permission, callback) => {
|
|
1242
|
-
if (permission === "media") {
|
|
1243
|
-
callback(true); // Approve microphone/camera access
|
|
1244
|
-
} else {
|
|
1245
|
-
callback(false);
|
|
1246
|
-
}
|
|
1247
|
-
});
|
|
1248
|
-
|
|
1249
|
-
if (options.extensions) {
|
|
1250
|
-
for (let extension of options.extensions)
|
|
1251
|
-
session.defaultSession
|
|
1252
|
-
.loadExtension(extension)
|
|
1253
|
-
.then((r) => Log.info(r))
|
|
1254
|
-
.catch((e) => Log.error(e));
|
|
1255
|
-
}
|
|
1256
|
-
if (options.tray) {
|
|
1257
|
-
this.#tray = new Tray(this.#app_icon);
|
|
1258
|
-
const context = Menu.buildFromTemplate(options.tray);
|
|
1259
|
-
this.#tray.setContextMenu(context);
|
|
1260
|
-
|
|
1261
|
-
this.#tray.on("click", () => this.#hideAndShow());
|
|
1262
|
-
}
|
|
1263
|
-
if (options.globalMenu) {
|
|
1264
|
-
const menu = Menu.buildFromTemplate(options.globalMenu);
|
|
1265
|
-
Menu.setApplicationMenu(menu);
|
|
1266
|
-
} else {
|
|
1267
|
-
Menu.setApplicationMenu(null);
|
|
1268
|
-
}
|
|
1269
|
-
this.enableServices();
|
|
1270
|
-
this.#buildWindow({ name: Main.MAIN_WINDOW, hideOnClose: options.hideOnClose });
|
|
1271
|
-
});
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
#writeLog(text) {
|
|
1275
|
-
let today = new Date().toLocaleDateString().replace("/", ".");
|
|
1276
|
-
let log_dir_path = path.join(app.getPath("userData"), "logs");
|
|
1277
|
-
let log_file_path = path.join(log_dir_path, `app_${today}.log`);
|
|
1278
|
-
|
|
1279
|
-
if (!fs.existsSync(log_dir_path)) {
|
|
1280
|
-
fs.mkdirSync(log_dir_path, { recursive: true });
|
|
1281
|
-
fs.writeFileSync(log_file_path, "", "utf-8");
|
|
1282
|
-
}
|
|
1283
|
-
|
|
1284
|
-
fs.writeFile(log_file_path, `${text}\n`, { flag: "a+" }, (err) => {
|
|
1285
|
-
if (err) console.error(err);
|
|
1286
|
-
});
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
enableAutoUpdateApp(start = Number()) {
|
|
1290
|
-
const { autoUpdater } = require("electron-updater");
|
|
1291
|
-
// Подписки на апдейтер и подтверждение установки — один раз на экземпляр Main:
|
|
1292
|
-
// раньше ipcMain.on("updateInstallConfirm") и autoUpdater.on добавлялись
|
|
1293
|
-
// внутри update-downloaded, то есть на каждое событие.
|
|
1294
|
-
if (this.#updaterListening === false) {
|
|
1295
|
-
this.#updaterListening = true;
|
|
1296
|
-
autoUpdater.on("update-downloaded", this.#onUpdateDownloaded);
|
|
1297
|
-
ipcMain.on("updateInstallConfirm", this.#onUpdateInstallConfirm);
|
|
1298
|
-
}
|
|
1299
|
-
setTimeout(async () => {
|
|
1300
|
-
autoUpdater.autoInstallOnAppQuit = false;
|
|
1301
|
-
let updates = await autoUpdater.checkForUpdates();
|
|
1302
|
-
if (updates !== null) {
|
|
1303
|
-
this.#lastUpdateInfo = updates;
|
|
1304
|
-
if (updates.versionInfo.version > app.getVersion()) {
|
|
1305
|
-
await this.#sendNotificationUpdateLoad(
|
|
1306
|
-
this.#appName,
|
|
1307
|
-
I18n.t("appLayout.update.downloading", { version: updates.versionInfo.version })
|
|
1308
|
-
);
|
|
1309
|
-
}
|
|
1310
|
-
}
|
|
1311
|
-
}, start);
|
|
1312
|
-
}
|
|
1313
|
-
async #sendNotificationUpdateLoad(text, body) {
|
|
1314
|
-
this.#send("sendNotificationUpdateLoad", [text, body]);
|
|
1315
|
-
}
|
|
1316
|
-
async #sendNotificationUpdateLoadClose() {
|
|
1317
|
-
this.#send("sendNotificationUpdateLoadClose");
|
|
1318
|
-
}
|
|
1319
|
-
async #sendNotificationUpdate(text, body) {
|
|
1320
|
-
this.#send("sendNotificationUpdate", [text, body]);
|
|
1321
|
-
}
|
|
1322
|
-
async #sendNotificationUpdateClose() {
|
|
1323
|
-
this.#send("sendNotificationUpdateClose");
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
|
-
/** Уведомление о загрузке; последним аргументом можно указать окно реестра */
|
|
1327
|
-
sendDownload(text, body, windowName = undefined) {
|
|
1328
|
-
setTimeout(() => this.#send("sendNotificationDownload", [text, body], windowName), 1);
|
|
1329
|
-
}
|
|
1330
|
-
sendDownloadUpdate(text, body, windowName = undefined) {
|
|
1331
|
-
setTimeout(() => this.#send("sendNotificationDownloadUpdate", [text, body], windowName), 1);
|
|
1332
|
-
}
|
|
1333
|
-
sendDownloadComplete(text, body, windowName = undefined) {
|
|
1334
|
-
setTimeout(() => this.#send("sendNotificationDownloadComplete", [text, body], windowName), 250);
|
|
1335
|
-
}
|
|
1336
|
-
sendDownloadError(text, body, windowName = undefined) {
|
|
1337
|
-
setTimeout(() => this.#send("sendNotificationDownloadError", [text, body], windowName), 250);
|
|
1338
|
-
}
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
187
|
class Styles {
|
|
1342
188
|
static WORD_BREAK = { NORMAL: "normal", BREAK_ALL: "break-all", KEEP_ALL: "keep-all", BREAK_WORD: "break-word" };
|
|
1343
189
|
static TEXT_ALIGN = { CENTER: "center", END: "end", START: "start" };
|
|
@@ -1455,6 +301,14 @@ module.exports = {
|
|
|
1455
301
|
MultiComboBox: MultiComboBox,
|
|
1456
302
|
Mention: Mention,
|
|
1457
303
|
Chat: Chat,
|
|
304
|
+
WriteBar: WriteBar,
|
|
305
|
+
MessageBar: MessageBar,
|
|
306
|
+
UserBar: UserBar,
|
|
307
|
+
UserListBar: UserListBar,
|
|
308
|
+
ActionsBar: ActionsBar,
|
|
309
|
+
Banner: Banner,
|
|
310
|
+
CardScroll: CardScroll,
|
|
311
|
+
ScreenSpinner: ScreenSpinner,
|
|
1458
312
|
ActionSheet: ActionSheet,
|
|
1459
313
|
ImageCompare: ImageCompare,
|
|
1460
314
|
OrgChart: OrgChart,
|
|
@@ -1506,8 +360,6 @@ module.exports = {
|
|
|
1506
360
|
CustomElement: CustomElement,
|
|
1507
361
|
Console: Console,
|
|
1508
362
|
//
|
|
1509
|
-
TelegramBot: TelegramBot,
|
|
1510
|
-
//
|
|
1511
363
|
BrowserWindow: BrowserWindow,
|
|
1512
364
|
ipcMain: ipcMain,
|
|
1513
365
|
ipcRenderer: ipcRenderer,
|