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.
Files changed (258) hide show
  1. package/CHANGELOG.md +536 -0
  2. package/framework/appLayout/default/chui_app_layout.js +361 -303
  3. package/framework/appLayout/default/global_style.css +243 -89
  4. package/framework/appLayout/default/main_theme_style.css +188 -101
  5. package/framework/components/chui_accordion/accordion.js +17 -17
  6. package/framework/components/chui_accordion/styles.css +7 -7
  7. package/framework/components/chui_action_sheet/action_sheet.js +35 -32
  8. package/framework/components/chui_action_sheet/styles.css +10 -4
  9. package/framework/components/chui_actions_bar/actions_bar.js +338 -0
  10. package/framework/components/chui_actions_bar/styles.css +139 -0
  11. package/framework/components/chui_affix/affix.js +4 -4
  12. package/framework/components/chui_affix/styles.css +5 -0
  13. package/framework/components/chui_alert/alert.js +11 -5
  14. package/framework/components/chui_alert/styles.css +7 -0
  15. package/framework/components/chui_avatar/avatar.js +13 -13
  16. package/framework/components/chui_avatar/styles.css +1 -1
  17. package/framework/components/chui_avatar_group/avatar_group.js +4 -4
  18. package/framework/components/chui_avatar_group/styles.css +1 -1
  19. package/framework/components/chui_backtop/backtop.js +25 -2
  20. package/framework/components/chui_backtop/styles.css +21 -0
  21. package/framework/components/chui_badge/badge.js +15 -15
  22. package/framework/components/chui_badge/styles.css +6 -6
  23. package/framework/components/chui_banner/banner.js +222 -0
  24. package/framework/components/chui_banner/styles.css +170 -0
  25. package/framework/components/chui_breadcrumb/styles.css +1 -1
  26. package/framework/components/chui_button/button.js +24 -28
  27. package/framework/components/chui_button/styles.css +5 -5
  28. package/framework/components/chui_calendar/calendar.js +6 -6
  29. package/framework/components/chui_card/card.js +14 -14
  30. package/framework/components/chui_card_scroll/card_scroll.js +169 -0
  31. package/framework/components/chui_card_scroll/styles.css +86 -0
  32. package/framework/components/chui_chat/chat.js +73 -51
  33. package/framework/components/chui_chat/styles.css +18 -7
  34. package/framework/components/chui_clipboard_history/clipboard_history.js +25 -20
  35. package/framework/components/chui_clipboard_history/styles.css +10 -8
  36. package/framework/components/chui_code/code.js +8 -8
  37. package/framework/components/chui_code/styles.css +2 -2
  38. package/framework/components/chui_code_editor/code_editor.js +26 -26
  39. package/framework/components/chui_code_editor/styles.css +3 -3
  40. package/framework/components/chui_command_palette/command_palette.js +29 -29
  41. package/framework/components/chui_command_palette/styles.css +5 -5
  42. package/framework/components/chui_console/console.js +9 -9
  43. package/framework/components/chui_console/styles.css +5 -2
  44. package/framework/components/chui_content_block/content_block.js +2 -2
  45. package/framework/components/chui_context_menu/context_menu.js +162 -54
  46. package/framework/components/chui_context_menu/styles.css +82 -8
  47. package/framework/components/chui_custom_element/custom_element.js +11 -11
  48. package/framework/components/chui_data_grid/data_grid.js +268 -268
  49. package/framework/components/chui_data_grid/styles.css +14 -14
  50. package/framework/components/chui_descriptions/styles.css +1 -1
  51. package/framework/components/chui_details/details.js +12 -12
  52. package/framework/components/chui_details/styles.css +7 -7
  53. package/framework/components/chui_diff_viewer/diff_viewer.js +88 -88
  54. package/framework/components/chui_diff_viewer/styles.css +9 -9
  55. package/framework/components/chui_divider/divider.js +6 -6
  56. package/framework/components/chui_divider/styles.css +2 -2
  57. package/framework/components/chui_drawer/drawer.js +37 -14
  58. package/framework/components/chui_drawer/styles.css +18 -4
  59. package/framework/components/chui_drop_zone/drop_zone.js +6 -6
  60. package/framework/components/chui_drop_zone/styles.css +10 -6
  61. package/framework/components/chui_fieldset/fieldset.js +6 -6
  62. package/framework/components/chui_fieldset/styles.css +5 -5
  63. package/framework/components/chui_file_explorer/file_explorer.js +115 -115
  64. package/framework/components/chui_file_explorer/styles.css +6 -6
  65. package/framework/components/chui_form/form.js +11 -11
  66. package/framework/components/chui_gantt/gantt.js +41 -41
  67. package/framework/components/chui_gantt/styles.css +5 -5
  68. package/framework/components/chui_graphs/graphs.js +82 -46
  69. package/framework/components/chui_graphs/styles.css +26 -5
  70. package/framework/components/chui_hero/hero.js +63 -20
  71. package/framework/components/chui_hero/styles.css +176 -7
  72. package/framework/components/chui_html_block/html_block.js +5 -5
  73. package/framework/components/chui_html_block/styles.css +1 -1
  74. package/framework/components/chui_hx/styles.css +2 -2
  75. package/framework/components/chui_icons/icons.js +7 -7
  76. package/framework/components/chui_icons/styles.css +2 -1
  77. package/framework/components/chui_inputs/chui_check_box/styles.css +12 -3
  78. package/framework/components/chui_inputs/chui_chips/chips.js +92 -31
  79. package/framework/components/chui_inputs/chui_chips/styles.css +28 -5
  80. package/framework/components/chui_inputs/chui_color_picker/color_picker.js +20 -15
  81. package/framework/components/chui_inputs/chui_color_picker/styles.css +7 -7
  82. package/framework/components/chui_inputs/chui_combo_box/combo_box.js +58 -58
  83. package/framework/components/chui_inputs/chui_combo_box/styles.css +9 -9
  84. package/framework/components/chui_inputs/chui_date/date.js +93 -95
  85. package/framework/components/chui_inputs/chui_date/styles.css +8 -8
  86. package/framework/components/chui_inputs/chui_date_range/date_range.js +22 -22
  87. package/framework/components/chui_inputs/chui_date_range/styles.css +3 -3
  88. package/framework/components/chui_inputs/chui_datetime/datetime.js +12 -12
  89. package/framework/components/chui_inputs/chui_datetime/styles.css +3 -3
  90. package/framework/components/chui_inputs/chui_email/email.js +10 -10
  91. package/framework/components/chui_inputs/chui_email/styles.css +6 -6
  92. package/framework/components/chui_inputs/chui_file/file.js +8 -8
  93. package/framework/components/chui_inputs/chui_file/styles.css +12 -9
  94. package/framework/components/chui_inputs/chui_hotkey/hotkey.js +39 -39
  95. package/framework/components/chui_inputs/chui_hotkey/styles.css +6 -6
  96. package/framework/components/chui_inputs/chui_masked/masked.js +44 -44
  97. package/framework/components/chui_inputs/chui_masked/styles.css +5 -5
  98. package/framework/components/chui_inputs/chui_mention/mention.js +13 -13
  99. package/framework/components/chui_inputs/chui_mention/styles.css +14 -8
  100. package/framework/components/chui_inputs/chui_multi_combo_box/multi_combo_box.js +147 -137
  101. package/framework/components/chui_inputs/chui_multi_combo_box/styles.css +14 -8
  102. package/framework/components/chui_inputs/chui_number/number.js +42 -45
  103. package/framework/components/chui_inputs/chui_number/styles.css +6 -6
  104. package/framework/components/chui_inputs/chui_otp/otp.js +32 -32
  105. package/framework/components/chui_inputs/chui_otp/styles.css +5 -5
  106. package/framework/components/chui_inputs/chui_password/password.js +30 -36
  107. package/framework/components/chui_inputs/chui_password/styles.css +7 -7
  108. package/framework/components/chui_inputs/chui_radio_button/styles.css +2 -2
  109. package/framework/components/chui_inputs/chui_rating/rating.js +28 -28
  110. package/framework/components/chui_inputs/chui_rating/styles.css +3 -3
  111. package/framework/components/chui_inputs/chui_select_box/select_box.js +54 -54
  112. package/framework/components/chui_inputs/chui_select_box/styles.css +10 -10
  113. package/framework/components/chui_inputs/chui_slider/slider.js +14 -14
  114. package/framework/components/chui_inputs/chui_slider/styles.css +5 -4
  115. package/framework/components/chui_inputs/chui_text/styles.css +7 -7
  116. package/framework/components/chui_inputs/chui_text/text.js +13 -17
  117. package/framework/components/chui_inputs/chui_text_area/styles.css +8 -8
  118. package/framework/components/chui_inputs/chui_text_area/text_area.js +23 -23
  119. package/framework/components/chui_inputs/chui_time/styles.css +13 -17
  120. package/framework/components/chui_inputs/chui_time/time.js +35 -24
  121. package/framework/components/chui_inputs/chui_toggle/styles.css +10 -3
  122. package/framework/components/chui_inputs/chui_toggle/toggle.js +16 -16
  123. package/framework/components/chui_json_editor/json_editor.js +87 -87
  124. package/framework/components/chui_json_editor/styles.css +5 -5
  125. package/framework/components/chui_json_viewer/json_viewer.js +80 -80
  126. package/framework/components/chui_json_viewer/styles.css +6 -6
  127. package/framework/components/chui_kanban/kanban.js +27 -27
  128. package/framework/components/chui_kanban/styles.css +8 -4
  129. package/framework/components/chui_kbd/styles.css +1 -1
  130. package/framework/components/chui_label/label.js +17 -17
  131. package/framework/components/chui_label/styles.css +1 -1
  132. package/framework/components/chui_link/link.js +9 -9
  133. package/framework/components/chui_link/styles.css +11 -4
  134. package/framework/components/chui_list/list.js +11 -11
  135. package/framework/components/chui_list/styles.css +2 -2
  136. package/framework/components/chui_markdown_editor/markdown_editor.js +66 -66
  137. package/framework/components/chui_markdown_editor/styles.css +5 -5
  138. package/framework/components/chui_media/audio.js +240 -246
  139. package/framework/components/chui_media/audio_styles.css +41 -23
  140. package/framework/components/chui_media/image.js +49 -46
  141. package/framework/components/chui_media/image_compare.js +35 -35
  142. package/framework/components/chui_media/image_compare_styles.css +5 -5
  143. package/framework/components/chui_media/image_styles.css +1 -1
  144. package/framework/components/chui_media/video.js +148 -148
  145. package/framework/components/chui_media/video_styles.css +27 -11
  146. package/framework/components/chui_menu_bar/menu_bar.js +54 -54
  147. package/framework/components/chui_menu_bar/styles.css +10 -10
  148. package/framework/components/chui_message_bar/message_bar.js +551 -0
  149. package/framework/components/chui_message_bar/styles.css +248 -0
  150. package/framework/components/chui_modal/modal.js +37 -13
  151. package/framework/components/chui_notification/notification.js +76 -54
  152. package/framework/components/chui_notification/notification_download.js +26 -81
  153. package/framework/components/chui_notification/notification_download_card.js +115 -0
  154. package/framework/components/chui_notification/notification_download_progress.css +2 -2
  155. package/framework/components/chui_notification/notification_download_progress.js +70 -108
  156. package/framework/components/chui_notification/notification_update.js +31 -31
  157. package/framework/components/chui_notification/status_plate.css +3 -3
  158. package/framework/components/chui_notification/styles.css +6 -1
  159. package/framework/components/chui_notification/styles_download.css +1 -1
  160. package/framework/components/chui_notification/styles_update.css +2 -2
  161. package/framework/components/chui_org_chart/org_chart.js +29 -29
  162. package/framework/components/chui_org_chart/styles.css +2 -2
  163. package/framework/components/chui_pagination/pagination.js +3 -3
  164. package/framework/components/chui_pagination/styles.css +1 -1
  165. package/framework/components/chui_paragraph/styles.css +2 -2
  166. package/framework/components/chui_popover/popover.js +29 -23
  167. package/framework/components/chui_popover/styles.css +1 -1
  168. package/framework/components/chui_popups/popups.js +124 -124
  169. package/framework/components/chui_popups/styles.css +5 -5
  170. package/framework/components/chui_print_button/print_button.js +15 -10
  171. package/framework/components/chui_progress_bar/progress_bar.js +34 -12
  172. package/framework/components/chui_progress_bar/styles.css +4 -4
  173. package/framework/components/chui_progress_ring/progress_ring.js +15 -15
  174. package/framework/components/chui_progress_ring/styles.css +1 -1
  175. package/framework/components/chui_qrcode/qrcode.js +12 -12
  176. package/framework/components/chui_query_builder/query_builder.js +46 -46
  177. package/framework/components/chui_query_builder/styles.css +4 -4
  178. package/framework/components/chui_radio_group/styles.css +1 -1
  179. package/framework/components/chui_result/result.js +15 -15
  180. package/framework/components/chui_result/styles.css +3 -3
  181. package/framework/components/chui_scheduler/scheduler.js +74 -74
  182. package/framework/components/chui_scheduler/styles.css +5 -5
  183. package/framework/components/chui_screen_spinner/screen_spinner.js +167 -0
  184. package/framework/components/chui_screen_spinner/styles.css +49 -0
  185. package/framework/components/chui_segmented_control/segmented_control.js +63 -6
  186. package/framework/components/chui_segmented_control/styles.css +32 -6
  187. package/framework/components/chui_settings/settings.js +45 -46
  188. package/framework/components/chui_settings/styles.css +4 -4
  189. package/framework/components/chui_shortcut_help/shortcut_help.js +32 -34
  190. package/framework/components/chui_sidebar_nav/sidebar_nav.js +98 -29
  191. package/framework/components/chui_sidebar_nav/styles.css +9 -3
  192. package/framework/components/chui_slideshow/slideshow.js +85 -85
  193. package/framework/components/chui_slideshow/styles.css +6 -6
  194. package/framework/components/chui_spinner/spinner.js +10 -10
  195. package/framework/components/chui_split_button/split_button.js +30 -12
  196. package/framework/components/chui_split_button/styles.css +21 -5
  197. package/framework/components/chui_splitter/splitter.js +11 -11
  198. package/framework/components/chui_splitter/styles.css +2 -2
  199. package/framework/components/chui_statistic/statistic.js +47 -47
  200. package/framework/components/chui_status_bar/styles.css +2 -2
  201. package/framework/components/chui_stepper/stepper.js +202 -31
  202. package/framework/components/chui_stepper/styles.css +79 -14
  203. package/framework/components/chui_sticky_bar/sticky_bar.js +44 -13
  204. package/framework/components/chui_sticky_bar/styles.css +10 -8
  205. package/framework/components/chui_tabs/chui_tabs.js +74 -26
  206. package/framework/components/chui_tabs/styles.css +40 -12
  207. package/framework/components/chui_text_editor/styles.css +7 -7
  208. package/framework/components/chui_text_editor/text_editor.js +56 -56
  209. package/framework/components/chui_text_editor/text_editor_panel.js +173 -174
  210. package/framework/components/chui_theme_toggle/theme_toggle.js +21 -5
  211. package/framework/components/chui_timeline/styles.css +3 -1
  212. package/framework/components/chui_toast_queue/styles.css +11 -11
  213. package/framework/components/chui_toast_queue/toast_queue.js +30 -27
  214. package/framework/components/chui_toolbar/styles.css +2 -2
  215. package/framework/components/chui_tooltip/styles.css +2 -2
  216. package/framework/components/chui_tooltip/tooltip.js +3 -3
  217. package/framework/components/chui_tour/styles.css +8 -8
  218. package/framework/components/chui_tour/tour.js +72 -72
  219. package/framework/components/chui_transfer/styles.css +6 -6
  220. package/framework/components/chui_transfer/transfer.js +93 -106
  221. package/framework/components/chui_tree_table/styles.css +4 -4
  222. package/framework/components/chui_tree_table/tree_table.js +45 -45
  223. package/framework/components/chui_tree_view/styles.css +10 -8
  224. package/framework/components/chui_tree_view/tree_view.js +185 -183
  225. package/framework/components/chui_user_bar/styles.css +145 -0
  226. package/framework/components/chui_user_bar/user_bar.js +402 -0
  227. package/framework/components/chui_user_list_bar/styles.css +173 -0
  228. package/framework/components/chui_user_list_bar/user_list_bar.js +485 -0
  229. package/framework/components/chui_watermark/watermark.js +17 -17
  230. package/framework/components/chui_webview/webview.js +9 -9
  231. package/framework/components/chui_write_bar/styles.css +172 -0
  232. package/framework/components/chui_write_bar/write_bar.js +320 -0
  233. package/framework/main_window.js +1234 -0
  234. package/framework/modules/chui_a11y/chui_a11y.js +78 -79
  235. package/framework/modules/chui_animations/animations.js +77 -15
  236. package/framework/modules/chui_animations/motion.js +120 -0
  237. package/framework/modules/chui_animations/styles.css +36 -3
  238. package/framework/modules/chui_functions.js +31 -27
  239. package/framework/modules/chui_highlight/styles.css +13 -10
  240. package/framework/modules/chui_i18n/chui_i18n.js +3 -3
  241. package/framework/modules/chui_i18n/locales/feedback.js +62 -0
  242. package/framework/modules/chui_i18n/locales/navigation.js +4 -0
  243. package/framework/modules/chui_json/chui_json.js +2 -2
  244. package/framework/modules/chui_logger/chui_logger.js +23 -8
  245. package/framework/modules/chui_page/page.js +10 -10
  246. package/framework/modules/chui_palette/palette.js +61 -0
  247. package/framework/modules/chui_qr/chui_qr.js +23 -23
  248. package/framework/modules/chui_scrollbar/chui_scrollbar.js +45 -45
  249. package/framework/modules/chui_scrollbar/styles.css +3 -3
  250. package/framework/modules/chui_services/chui_services.js +8 -8
  251. package/framework/modules/chui_services/desktop.js +77 -71
  252. package/framework/modules/chui_sortable/chui_sortable.js +17 -17
  253. package/framework/modules/chui_validate/chui_validate.js +3 -3
  254. package/framework/tray_bar/chui_menu_items.js +2 -2
  255. package/index.d.ts +266 -34
  256. package/index.js +20 -1168
  257. package/package.json +6 -4
  258. 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
- let downloadSession = undefined;
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, getDefaultIcon, formatBytes } = require("./framework/modules/chui_functions");
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 url = require("node:url");
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,