koishi-plugin-chat-patch 3.0.2 → 5.0.0

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 (194) hide show
  1. package/client/vue/index.vue +60 -634
  2. package/client/web/env.d.ts +56 -0
  3. package/client/web/index.html +21 -0
  4. package/client/web/package-lock.json +3419 -0
  5. package/client/web/package.json +62 -0
  6. package/client/web/public/bcui/css/color-dark.css +35 -0
  7. package/client/web/public/bcui/css/color-light.css +35 -0
  8. package/client/web/public/bcui/css/style.css +468 -0
  9. package/client/web/public/bcui/js/main.js +193 -0
  10. package/client/web/public/css/append-dark.css +13 -0
  11. package/client/web/public/css/append-light.css +13 -0
  12. package/client/web/public/css/append-napcat-dark.css +14 -0
  13. package/client/web/public/css/append-napcat-light.css +14 -0
  14. package/client/web/public/favicon.ico +0 -0
  15. package/client/web/public/img/icons/icon-for-fuck-apple.png +0 -0
  16. package/client/web/public/img/icons/icon-maskable.png +0 -0
  17. package/client/web/public/img/icons/icon.png +0 -0
  18. package/client/web/public/img/icons/icon.svg +10 -0
  19. package/client/web/public/robots.txt +2 -0
  20. package/client/web/public/sw.js +58 -0
  21. package/client/web/scripts/copy-bcui.mjs +17 -0
  22. package/client/web/src/App.vue +1127 -0
  23. package/client/web/src/assets/css/append/append_darwin.css +68 -0
  24. package/client/web/src/assets/css/append/append_full_vibrancy.css +290 -0
  25. package/client/web/src/assets/css/append/append_linux.css +125 -0
  26. package/client/web/src/assets/css/append/append_linux_vibrancy.css +22 -0
  27. package/client/web/src/assets/css/append/append_napcat.css +35 -0
  28. package/client/web/src/assets/css/append/append_new.css +843 -0
  29. package/client/web/src/assets/css/append/append_vibrancy.css +113 -0
  30. package/client/web/src/assets/css/append/append_win32.css +109 -0
  31. package/client/web/src/assets/css/append/mobile/append_mobile_horizontal.css +349 -0
  32. package/client/web/src/assets/css/append/mobile/append_mobile_vertical.css +96 -0
  33. package/client/web/src/assets/css/chat.css +2104 -0
  34. package/client/web/src/assets/css/msg.css +941 -0
  35. package/client/web/src/assets/css/options.css +811 -0
  36. package/client/web/src/assets/css/sys_notice.css +62 -0
  37. package/client/web/src/assets/css/view.css +2105 -0
  38. package/client/web/src/assets/font//346/226/271/346/255/243/345/226/265/345/221/234/344/275/223.ttf +0 -0
  39. package/client/web/src/assets/img/Vue.png +0 -0
  40. package/client/web/src/assets/img/cakes.svg +189 -0
  41. package/client/web/src/assets/img/icons/AppIcon.png +0 -0
  42. package/client/web/src/assets/img/icons/AppIconDark.png +0 -0
  43. package/client/web/src/assets/img/icons/CapacitorAppIcon.png +0 -0
  44. package/client/web/src/assets/img/icons/CapacitorAppIconDark.png +0 -0
  45. package/client/web/src/assets/img/icons/NapcatAppIcon.png +0 -0
  46. package/client/web/src/assets/img/icons/NapcatAppIconDark.png +0 -0
  47. package/client/web/src/assets/img/icons/exit.png +0 -0
  48. package/client/web/src/assets/img/icons/pen.png +0 -0
  49. package/client/web/src/assets/img/stars.svg +89 -0
  50. package/client/web/src/assets/img//346/234/237/345/276/205.png +0 -0
  51. package/client/web/src/assets/l10n/_About I10n.md +35 -0
  52. package/client/web/src/assets/l10n/_l10nconfig.json +7 -0
  53. package/client/web/src/assets/l10n/zh-CN.po +1941 -0
  54. package/client/web/src/assets/logo.png +0 -0
  55. package/client/web/src/assets/pathMap/LLOneBot.yaml +2 -0
  56. package/client/web/src/assets/pathMap/Lagrange.OneBot.yaml +180 -0
  57. package/client/web/src/assets/pathMap/NapCat.Onebot.yaml +224 -0
  58. package/client/web/src/assets/pathMap/SnowLuma.yaml +80 -0
  59. package/client/web/src/assets/qed.txt +1 -0
  60. package/client/web/src/components/AboutPan.vue +197 -0
  61. package/client/web/src/components/BulletinBody.vue +129 -0
  62. package/client/web/src/components/DepPan.vue +310 -0
  63. package/client/web/src/components/EmojiFace.vue +32 -0
  64. package/client/web/src/components/FacePan.vue +461 -0
  65. package/client/web/src/components/FileBody.vue +129 -0
  66. package/client/web/src/components/FileManager.vue +532 -0
  67. package/client/web/src/components/FriendBody.vue +54 -0
  68. package/client/web/src/components/GlobalSessionSearchBar.vue +301 -0
  69. package/client/web/src/components/History.vue +49 -0
  70. package/client/web/src/components/LazyLottie.vue +82 -0
  71. package/client/web/src/components/MergePan.vue +127 -0
  72. package/client/web/src/components/MsgBody.vue +1478 -0
  73. package/client/web/src/components/MusicPlayer.vue +549 -0
  74. package/client/web/src/components/NoticeBody.vue +168 -0
  75. package/client/web/src/components/QzonePermissionPan.vue +150 -0
  76. package/client/web/src/components/RawMsgRenderPreviewPan.vue +190 -0
  77. package/client/web/src/components/ThemeColorPickerPan.vue +420 -0
  78. package/client/web/src/components/TinySessionBody.vue +51 -0
  79. package/client/web/src/components/UmamiInfoPan.vue +1549 -0
  80. package/client/web/src/components/UpdatePan.vue +263 -0
  81. package/client/web/src/components/ViewerCom.vue +1360 -0
  82. package/client/web/src/components/VoiceMsg.vue +626 -0
  83. package/client/web/src/components/WelPan.vue +894 -0
  84. package/client/web/src/components/msg-body/.gitkeep +0 -0
  85. package/client/web/src/components/msg-component/JsonSegComp.vue +138 -0
  86. package/client/web/src/components/msg-component/XmlSegComp.vue +132 -0
  87. package/client/web/src/components/msg-component/jsonComp/AutoReply.vue +97 -0
  88. package/client/web/src/components/msg-component/jsonComp/Contact.lua.vue +129 -0
  89. package/client/web/src/components/msg-component/jsonComp/Feed.lua.vue +94 -0
  90. package/client/web/src/components/msg-component/jsonComp/Forum.vue +84 -0
  91. package/client/web/src/components/msg-component/jsonComp/Mannounce.vue +111 -0
  92. package/client/web/src/components/msg-component/jsonComp/Map.vue +52 -0
  93. package/client/web/src/components/msg-component/jsonComp/Miniapp.lua.vue +53 -0
  94. package/client/web/src/components/msg-component/jsonComp/Miniapp.vue +69 -0
  95. package/client/web/src/components/msg-component/jsonComp/Music.lua.vue +135 -0
  96. package/client/web/src/components/msg-component/jsonComp/Tuwen.lua.vue +56 -0
  97. package/client/web/src/components/notice-component/.gitkeep +0 -0
  98. package/client/web/src/components/notice-component/MealHungryPan.vue +119 -0
  99. package/client/web/src/components/tooltip/CustomFaceTooltip.vue +22 -0
  100. package/client/web/src/components/tooltip/Tooltip.vue +50 -0
  101. package/client/web/src/components/tooltip/Tooltips.vue +14 -0
  102. package/client/web/src/components/tooltip/UserInfoTooltip.vue +69 -0
  103. package/client/web/src/function/base.ts +238 -0
  104. package/client/web/src/function/connect.ts +2042 -0
  105. package/client/web/src/function/debug.ts +10 -0
  106. package/client/web/src/function/elements/information.ts +141 -0
  107. package/client/web/src/function/elements/system.ts +101 -0
  108. package/client/web/src/function/elements/vueComp.ts +62 -0
  109. package/client/web/src/function/favicon.ts +76 -0
  110. package/client/web/src/function/input.ts +69 -0
  111. package/client/web/src/function/model/emoji.ts +127 -0
  112. package/client/web/src/function/model/img.ts +150 -0
  113. package/client/web/src/function/model/msg-body.ts +51 -0
  114. package/client/web/src/function/msg.ts +2674 -0
  115. package/client/web/src/function/notify.ts +225 -0
  116. package/client/web/src/function/option.ts +749 -0
  117. package/client/web/src/function/satori-model.ts +818 -0
  118. package/client/web/src/function/satori.ts +325 -0
  119. package/client/web/src/function/sender.ts +178 -0
  120. package/client/web/src/function/tooltip.ts +49 -0
  121. package/client/web/src/function/utils/appUtil.ts +2288 -0
  122. package/client/web/src/function/utils/avatarUtil.ts +31 -0
  123. package/client/web/src/function/utils/backgroundUtil.ts +110 -0
  124. package/client/web/src/function/utils/linkViewUtil.ts +101 -0
  125. package/client/web/src/function/utils/localHistoryUtil.ts +517 -0
  126. package/client/web/src/function/utils/msgUtil.ts +1242 -0
  127. package/client/web/src/function/utils/pinyin.ts +141 -0
  128. package/client/web/src/function/utils/recordUtil.ts +187 -0
  129. package/client/web/src/function/utils/sessionUtil.ts +114 -0
  130. package/client/web/src/function/utils/systemUtil.ts +664 -0
  131. package/client/web/src/main.ts +91 -0
  132. package/client/web/src/pages/Chat.vue +3453 -0
  133. package/client/web/src/pages/Friends.vue +636 -0
  134. package/client/web/src/pages/Info.vue +717 -0
  135. package/client/web/src/pages/Messages.vue +875 -0
  136. package/client/web/src/pages/Options.vue +82 -0
  137. package/client/web/src/pages/Qzone.vue +1178 -0
  138. package/client/web/src/pages/chat-view/ChatDanmaku.vue +759 -0
  139. package/client/web/src/pages/chat-view/ChatGlagame.vue +642 -0
  140. package/client/web/src/pages/chat-view/ChatTerminal.vue +1044 -0
  141. package/client/web/src/pages/chat-view/SystemNotice.vue +171 -0
  142. package/client/web/src/pages/options/OptAccount.vue +107 -0
  143. package/client/web/src/pages/options/OptAddon.vue +185 -0
  144. package/client/web/src/pages/options/OptDev.vue +704 -0
  145. package/client/web/src/pages/options/OptFunction.vue +403 -0
  146. package/client/web/src/pages/options/OptInfo.vue +165 -0
  147. package/client/web/src/pages/options/OptView.vue +846 -0
  148. package/client/web/src/runtime/backend.ts +310 -0
  149. package/client/web/src/state/auth.ts +14 -0
  150. package/client/web/src/state/chat.ts +36 -0
  151. package/client/web/src/state/connection.ts +20 -0
  152. package/client/web/src/state/contact.ts +37 -0
  153. package/client/web/src/state/qzone.ts +18 -0
  154. package/client/web/src/state/sessionHistory.ts +25 -0
  155. package/client/web/src/state/settings.ts +18 -0
  156. package/client/web/src/state/sticker.ts +10 -0
  157. package/client/web/src/state/ui.ts +56 -0
  158. package/client/web/tsconfig.json +27 -0
  159. package/client/web/tsconfig.tsbuildinfo +1 -0
  160. package/client/web/vite.config.ts +33 -0
  161. package/dist/index.js +1 -12
  162. package/dist/style.css +1 -1
  163. package/lib/index.js +2070 -2077
  164. package/package.json +15 -2
  165. package/readme.md +0 -22
  166. package/src/bootstrap.ts +71 -0
  167. package/src/cache.ts +319 -0
  168. package/src/config.ts +23 -35
  169. package/src/database.ts +650 -0
  170. package/src/index.ts +27 -59
  171. package/src/media.ts +302 -0
  172. package/src/recorder.ts +175 -0
  173. package/src/self-message.ts +456 -0
  174. package/src/server.d.ts +26 -0
  175. package/src/types.ts +108 -48
  176. package/src/web.ts +712 -0
  177. package/client/vue/chat-logic.ts +0 -683
  178. package/client/vue/composables/useChatActions.ts +0 -61
  179. package/client/vue/composables/useChatData.ts +0 -185
  180. package/client/vue/composables/useImageCache.ts +0 -281
  181. package/client/vue/composables/useVideoCache.ts +0 -125
  182. package/client/vue/types.ts +0 -72
  183. package/lib/api-handlers.d.ts +0 -25
  184. package/lib/config.d.ts +0 -14
  185. package/lib/file-manager.d.ts +0 -124
  186. package/lib/index.d.ts +0 -17
  187. package/lib/logger.d.ts +0 -9
  188. package/lib/message-handler.d.ts +0 -35
  189. package/lib/types.d.ts +0 -57
  190. package/lib/utils.d.ts +0 -18
  191. package/src/api-handlers.ts +0 -646
  192. package/src/file-manager.ts +0 -1436
  193. package/src/message-handler.ts +0 -507
  194. package/src/utils.ts +0 -189
@@ -0,0 +1,749 @@
1
+ /*
2
+ * @FileDescription: 设置功能模块
3
+ * @Author: Stapxs
4
+ * @Date:
5
+ * 2022/09/27
6
+ * 2022/12/07
7
+ * @Version:
8
+ * 1.0 - 初始版本
9
+ * 1.5 - 重构为 ts 版本,代码格式优化
10
+ * @Description: 设置功能相关模块
11
+ */
12
+
13
+ import app from '../main'
14
+
15
+ import { reactive } from 'vue'
16
+ import { Logger, LogType, PopInfo, PopType } from './base'
17
+ import { useSettingsStore } from '../state/settings'
18
+ import { useUIStore } from '../state/ui'
19
+ import { useChatStore } from '../state/chat'
20
+ import {
21
+ loadWinColor,
22
+ sendStatEvent,
23
+ updateWinColor,
24
+ } from '../function/utils/appUtil'
25
+ import { updateBaseOnMsgList } from './utils/msgUtil'
26
+ import { backend } from '../runtime/backend'
27
+ import { refreshFavicon } from './favicon'
28
+
29
+ let cacheConfigs: { [key: string]: any }
30
+
31
+ // =============== 附加设置结构 ===============
32
+
33
+ export type ExtraOptionItemType = 'switch' | 'select' | 'input'| 'password' | 'button'
34
+
35
+ export interface ExtraOptionItem {
36
+ id: string
37
+ label: string
38
+ description?: string
39
+ type: ExtraOptionItemType
40
+ icon?: string | [string, string]
41
+ /**
42
+ * 绑定到配置中的键名;如需持久化并参与 Option.save / load,请提供。
43
+ */
44
+ optionKey?: string
45
+ defaultValue?: any
46
+ /**
47
+ * 选项列表(仅 select 使用)
48
+ */
49
+ options?: { value: string | number | boolean; label: string }[]
50
+ callback?: (value: any) => void
51
+ }
52
+
53
+ export interface ExtraOptionCard {
54
+ id: string
55
+ title: string
56
+ description?: string
57
+ items: ExtraOptionItem[]
58
+ }
59
+
60
+ /**
61
+ * 附加设置卡片列表:供外部模块动态注册并在“附加”标签页中展示。
62
+ */
63
+ export const extraOptionCards = reactive<ExtraOptionCard[]>([])
64
+
65
+ // 设置项的初始值,防止下拉菜单选项为空或者首次使用初始错误
66
+ export const optDefault: { [key: string]: any } = {
67
+ // System
68
+ address: '',
69
+ top_info: {},
70
+ save_password: '',
71
+ notice_group: {},
72
+ auto_connect: true,
73
+ local_emoji_folder: null,
74
+ connection_history: [],
75
+ // View
76
+ language: 'zh-CN',
77
+ opt_dark: false,
78
+ opt_auto_dark: false,
79
+ theme_color: 0,
80
+ opt_auto_win_color: false,
81
+ chat_background: '',
82
+ chat_background_blur: 0,
83
+ chat_background_align: 'center',
84
+ chat_background_fit: 'cover',
85
+ opt_fast_animation: true,
86
+ chat_more_blur: false,
87
+ glass_effect: false,
88
+ initial_scale: 0.85,
89
+ fs_adaptation: 0,
90
+ opt_always_top: false,
91
+ opt_revolve: false,
92
+ use_favicon_notice: true,
93
+ use_super_face: true,
94
+ opt_ind_message: 'right',
95
+ opt_no_auto_load_image: false,
96
+ // Function
97
+ bubble_sort_user: true,
98
+ session_display_mode: 'recent' as 'recent' | 'all',
99
+ close_respond: false,
100
+ msg_taill: '',
101
+ quick_send: 'default',
102
+ group_notice_type: 'none',
103
+ send_face: false,
104
+ send_key: 'none',
105
+ close_ga: false,
106
+ open_ga_bot: true,
107
+ record_recent_emoji: '100times' as 'none' | 'order' | '100times' | '500times',
108
+ enable_local_history: false,
109
+ mixed_load_messages: false,
110
+ disable_local_history_image_cache: false,
111
+ chatview_name: '',
112
+ keepMessagesOnClear: 50,
113
+ // Dev
114
+ msg_type: 2,
115
+ log_level: 'err',
116
+ debug_msg: false,
117
+ custom_css: '',
118
+ // Glagame
119
+ openai_api: '',
120
+ openai_token: '',
121
+ openai_model: '',
122
+ glagame_max_tokens: 1000000,
123
+ glagame_favorability: false,
124
+ glagame_prompt: `你是一个对话辅助助手,你将根据历史的对话内容生成可供玩家可以选择用来直接回复的内容。
125
+
126
+ - 玩家默认是温和、体贴、日常向、有点小俏皮。
127
+ - **不要**为玩家本人的消息生成回复,玩家本人的消息仅供上下文参考。
128
+ - 若对话之间时间相隔较久,可自然应对(如"刚看到消息")。
129
+ - 避免引入与对话无关的新背景。
130
+
131
+ 对话记录中含有玩家本人的消息,请根据提供的当前账号信息自行区分,可以适当模仿玩家的语言风格。`,
132
+ }
133
+
134
+ // =============== 设置项事件 ===============
135
+
136
+ const configFunction: { [key: string]: (value: any) => void } = {
137
+ language: setLanguage,
138
+ opt_dark: setDarkMode,
139
+ theme_color: changeTheme,
140
+ initial_scale: changeInitialScale,
141
+ msg_type: setMsgType,
142
+ opt_auto_win_color: updateWinColorOpt,
143
+ opt_revolve: viewRevolve,
144
+ opt_always_top: viewAlwaysTop,
145
+ opt_fast_animation: updateFarstAnimation,
146
+ bubble_sort_user: clearGroupAssist,
147
+ session_display_mode: clearGroupAssist,
148
+ use_favicon_notice: setFaviconNotice,
149
+ custom_css: injectCustomCss,
150
+ opt_ind_message: updateChatPan
151
+ }
152
+
153
+ // =============== 附加设置注册接口 ===============
154
+
155
+ /**
156
+ * 注册一个新的附加设置卡片。
157
+ * 如果 id 已存在,则仅更新标题/描述并返回原有卡片。
158
+ */
159
+ export function registerExtraOptionCard(card: {
160
+ id: string
161
+ title: string
162
+ description?: string
163
+ }): ExtraOptionCard {
164
+ const exist = extraOptionCards.find((c) => c.id === card.id)
165
+ if (exist) {
166
+ exist.title = card.title
167
+ exist.description = card.description
168
+ return exist
169
+ }
170
+ const created: ExtraOptionCard = {
171
+ id: card.id,
172
+ title: card.title,
173
+ description: card.description,
174
+ items: [],
175
+ }
176
+ extraOptionCards.push(created)
177
+ return created
178
+ }
179
+
180
+ /**
181
+ * 向指定附加设置卡片中注册一项设置。
182
+ * 如果目标卡片不存在,将以 id 作为标题自动创建。
183
+ *
184
+ * - 提供 optionKey + defaultValue 时,会自动写入 optDefault,
185
+ * 并在当前配置中缺失时填充与保存,确保后续加载不会被清理。
186
+ */
187
+ export function registerExtraOptionItem(cardId: string, item: ExtraOptionItem) {
188
+ if (!cardId || !item || !item.id) return
189
+
190
+ let card = extraOptionCards.find((c) => c.id === cardId)
191
+ if (!card) {
192
+ card = {
193
+ id: cardId,
194
+ title: cardId,
195
+ items: [],
196
+ }
197
+ extraOptionCards.push(card)
198
+ }
199
+
200
+ // 去重:相同 id 直接替换
201
+ const existIndex = card.items.findIndex((i) => i.id === item.id)
202
+ if (existIndex >= 0) {
203
+ card.items.splice(existIndex, 1, item)
204
+ } else {
205
+ card.items.push(item)
206
+ }
207
+
208
+ // 如需持久化,补充默认值并保存
209
+ if (item.optionKey) {
210
+ const key = item.optionKey
211
+ if (Object.prototype.hasOwnProperty.call(item, 'defaultValue')) {
212
+ if (optDefault[key] === undefined) {
213
+ optDefault[key] = item.defaultValue
214
+ }
215
+ if (cacheConfigs && cacheConfigs[key] === undefined) {
216
+ cacheConfigs[key] = item.defaultValue
217
+ saveAll()
218
+ }
219
+ }
220
+ }
221
+ }
222
+
223
+ function updateChatPan() {
224
+ const uiStore = useUIStore()
225
+ const chatStore = useChatStore()
226
+ chatStore.chatInfo.show.id = 0
227
+ uiStore.openSideBar = true
228
+ }
229
+
230
+
231
+ function setFaviconNotice(_: boolean) {
232
+ refreshFavicon()
233
+ }
234
+
235
+ function injectCustomCss(value: string) {
236
+ // 移除旧的自定义 CSS
237
+ const oldStyle = document.getElementById('custom-css-inject')
238
+ if (oldStyle) {
239
+ document.head.removeChild(oldStyle)
240
+ }
241
+
242
+ // 如果有新的 CSS 内容,注入它
243
+ if (value && value.trim() !== '') {
244
+ const style = document.createElement('style')
245
+ style.id = 'custom-css-inject'
246
+ style.textContent = value
247
+ document.head.appendChild(style)
248
+ new Logger().add(LogType.UI, '已注入自定义 CSS')
249
+ }
250
+ }
251
+
252
+ function clearGroupAssist() {
253
+ updateBaseOnMsgList()
254
+ }
255
+
256
+ function updateFarstAnimation(_value: boolean) {
257
+ // 快速动画始终启用,不再允许关闭
258
+ if (document.getElementById('disable-transitions')) return
259
+ const style = document.createElement('style')
260
+ style.textContent = `* {
261
+ transition: .1s !important;
262
+ }`
263
+ style.id = 'disable-transitions'
264
+ document.head.appendChild(style)
265
+ }
266
+
267
+ function viewAlwaysTop(value: boolean) {
268
+ backend.call(undefined, 'win:alwaysTop', false, value)
269
+ }
270
+
271
+ function viewRevolve(value: boolean) {
272
+ const baseApp = document.getElementById('base-app')
273
+ if (!baseApp) return
274
+ if (value) {
275
+ baseApp.classList.add('no-touch')
276
+ } else {
277
+ baseApp.classList.remove('no-touch')
278
+ }
279
+ }
280
+
281
+ function updateWinColorOpt(value: boolean) {
282
+ if (value == true) {
283
+ backend.addListener(undefined, 'sys:WinColorChanged', (_, params) => {
284
+ updateWinColor(params, backend.platform == 'win32' ? 'windows' : 'macos')
285
+ })
286
+ loadWinColor()
287
+ }
288
+ }
289
+
290
+ function setMsgType(value: any) {
291
+ if (value) {
292
+ const uiStore = useUIStore()
293
+ uiStore.msgType = Number(value)
294
+ }
295
+ }
296
+
297
+ /**
298
+ * 修改移动端缩放比例
299
+ * @param value 数值(0.5 - 1.5)
300
+ */
301
+ function changeInitialScale(value: number) {
302
+ const viewport = document.getElementById('viewport')
303
+ if (viewport && value && value >= 0.5 && value <= 1.5) {
304
+ (viewport as any).content =
305
+ `width=device-width, initial-scale=${value}, maximum-scale=5, user-scalable=0`
306
+ } else {
307
+ (viewport as any).content =
308
+ 'width=device-width, initial-scale=0.85, maximum-scale=5, user-scalable=0'
309
+ }
310
+ }
311
+
312
+ /**
313
+ * 本项目固定使用简体中文
314
+ */
315
+ function setLanguage(_name: string) {
316
+ app.config.globalProperties.$i18n.locale = 'zh-CN'
317
+ app.config.globalProperties.$i18n.fallbackLocale = 'zh-CN'
318
+ document.querySelector('html')?.setAttribute('lang', 'zh-CN')
319
+ }
320
+
321
+ /**
322
+ * 设置暗黑模式
323
+ * @param value 是否启用暗黑模式
324
+ */
325
+ function setDarkMode(value = true) {
326
+ if (value === true) {
327
+ changeColorMode('dark')
328
+ } else {
329
+ changeColorMode('light')
330
+ }
331
+ }
332
+
333
+ /**
334
+ * 修改颜色模式
335
+ * @param mode 颜色模式
336
+ */
337
+ function changeColorMode(mode: string) {
338
+ const settingsStore = useSettingsStore()
339
+ if (!settingsStore.firstLoad) {
340
+ // 启用颜色渐变动画
341
+ document.body.style.transition =
342
+ 'background, color, background-color .3s'
343
+ } else {
344
+ settingsStore.firstLoad = false
345
+ }
346
+ // 切换颜色
347
+ const match_list = ['color-.*.css', 'prism-.*.css', 'append-.*.css']
348
+ const css_list = document.getElementsByTagName('link')
349
+ for (let i = 0; i < css_list.length; i++) {
350
+ const name = css_list[i].href
351
+ match_list.forEach((value) => {
352
+ if (name.match(value) != null) {
353
+ // 检查切换的文件是否可以被访问到
354
+ if (name != undefined) {
355
+ let newName = name
356
+ if (name.indexOf('dark') > -1) {
357
+ newName = name.replace('dark', 'light')
358
+ } else {
359
+ newName = name.replace('light', 'dark')
360
+ }
361
+ const xhr = new XMLHttpRequest()
362
+ xhr.open('HEAD', newName, false)
363
+ xhr.send()
364
+ if (xhr.status != 200) {
365
+ // 无法访问到对应的颜色模式文件,放弃切换
366
+ new PopInfo().add(
367
+ PopType.ERR,
368
+ '无法切换颜色模式:访问颜色模式文件失败。',
369
+ )
370
+ return
371
+ }
372
+ }
373
+ const newLink = document.createElement('link')
374
+ newLink.setAttribute('rel', 'stylesheet')
375
+ newLink.setAttribute('type', 'text/css')
376
+ if (mode === 'dark') {
377
+ newLink.setAttribute('href', name.replace('light', 'dark'))
378
+ } else {
379
+ newLink.setAttribute('href', name.replace('dark', 'light'))
380
+ }
381
+ const head = document.getElementsByTagName('head').item(0)
382
+ if (head !== null) {
383
+ head.replaceChild(newLink, css_list[i])
384
+ }
385
+ }
386
+ })
387
+ }
388
+ // 如果主题色模式是自定,则刷新系统主题色
389
+ getRaw('theme_color').then((themeColor) => {
390
+ if(themeColor && themeColor > 10) {
391
+ const colorUpdate = ('000000' + Number(themeColor).toString(16)).slice(-6)
392
+ updateWinColor(colorUpdate, 'windows')
393
+ }
394
+ })
395
+ // 刷新页面主题色
396
+ const meta = document.getElementsByName('theme-color')[0]
397
+ if (meta) {
398
+ (meta as HTMLMetaElement).content = getComputedStyle(
399
+ document.documentElement,
400
+ ).getPropertyValue('--color-main')
401
+ }
402
+ // 记录
403
+ settingsStore.darkMode = mode === 'dark'
404
+ // Capacitor: 状态栏颜色(Android)
405
+ if(backend.isMobile()) {
406
+ backend.call('StatusBar', 'setStyle', false, { style: mode.toUpperCase() })
407
+ }
408
+ // Capacitor: VConsole 颜色
409
+ if(backend.function && 'vConsole' in backend.function && backend.function.vConsole) {
410
+ backend.function.vConsole.setOption('theme', mode)
411
+ }
412
+ // 刷新图标
413
+ refreshFavicon()
414
+ }
415
+
416
+ /**
417
+ * 设置主题色
418
+ * @param id 主题色编号
419
+ */
420
+ function changeTheme(id: number) {
421
+ if(id < 10) {
422
+ document.documentElement.style.setProperty(
423
+ '--color-main',
424
+ 'var(--color-main-' + id + ')',
425
+ )
426
+ const meta = document.getElementsByName('theme-color')[0]
427
+ if (meta) {
428
+ (meta as HTMLMetaElement).content = getComputedStyle(
429
+ document.documentElement,
430
+ ).getPropertyValue('--color-main-' + id)
431
+ }
432
+ } else {
433
+ const color = ('000000' + Number(id).toString(16)).slice(-6)
434
+ updateWinColor(color, 'windows')
435
+ }
436
+ // 避免 css 未加载完
437
+ setTimeout(refreshFavicon, 10)
438
+ }
439
+
440
+ // =============== 设置基础功能 ===============
441
+
442
+ /**
443
+ * 读取并序列化 localStorage 中的设置项(electron 读取 electron-store 存储)
444
+ * @returns 设置项集合
445
+ */
446
+ export async function load(): Promise<{ [key: string]: any }> {
447
+ let data = {} as { [key: string]: any }
448
+
449
+ if ('electron' == backend.type) {
450
+ data = backend.callSync('opt:getAll')
451
+ } else if('tauri' == backend.type) {
452
+ data = await backend.call(undefined, 'opt:getAll', true)
453
+ // 处理下 json 字符串
454
+ Object.keys(data).forEach((key) => {
455
+ const value = data[key]
456
+ if (typeof value == 'string') {
457
+ try {
458
+ data[key] = JSON.parse(value)
459
+ } catch (e: unknown) {
460
+ // ignore
461
+ }
462
+ }
463
+ })
464
+ } else {
465
+ data = readWebOptions()
466
+ }
467
+ return loadOptData(data)
468
+ }
469
+
470
+ function readWebOptions(): { [key: string]: any } {
471
+ const str = localStorage.getItem('options')
472
+ if (str == null) return {}
473
+ try {
474
+ const parsed = JSON.parse(str) as unknown
475
+ if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
476
+ return parsed as { [key: string]: any }
477
+ }
478
+ } catch {
479
+ // 兼容旧版 key:value 格式
480
+ }
481
+ const data = {} as { [key: string]: any }
482
+ for (const entry of str.split('&')) {
483
+ if (!entry) continue
484
+ const separator = entry.indexOf(':')
485
+ if (separator <= 0) continue
486
+ data[entry.slice(0, separator)] = entry.slice(separator + 1)
487
+ }
488
+ return data
489
+ }
490
+
491
+ function loadOptData(data: { [key: string]: any }) {
492
+ const options: { [key: string]: any } = {}
493
+ Object.keys(data).forEach((key) => {
494
+ const value = data[key]
495
+ if (value === 'true' || value === 'false') {
496
+ options[key] = value === 'true'
497
+ } else if (value === 'null') {
498
+ options[key] = null
499
+ } else if (typeof value == 'string') {
500
+ try {
501
+ options[key] = decodeURIComponent(value)
502
+ } catch (e: unknown) {
503
+ // 如果 decodeURIComponent 失败(比如 CSS 内容有特殊字符),直接使用原值
504
+ options[key] = value
505
+ }
506
+ try {
507
+ const result = JSON.parse(options[key])
508
+ if (typeof result === 'object' && result !== null) {
509
+ options[key] = result
510
+ }
511
+ } catch (e: unknown) {
512
+ // ignore
513
+ }
514
+ } else {
515
+ options[key] = value
516
+ }
517
+ // 执行设置项操作
518
+ run(key, options[key])
519
+ })
520
+ let optChanged = false
521
+ // 初始化不存在的需要进行初始化的值
522
+ Object.keys(optDefault).forEach((key) => {
523
+ if (options[key] === undefined) {
524
+ optChanged = true
525
+ options[key] = optDefault[key]
526
+ }
527
+ })
528
+ if (options.opt_fast_animation !== true) {
529
+ options.opt_fast_animation = true
530
+ optChanged = true
531
+ run('opt_fast_animation', true)
532
+ }
533
+ // 删除不存在的设置项
534
+ const needless: string[] = []
535
+ for (const key in options) {
536
+ if (optDefault[key] === undefined)
537
+ needless.push(key)
538
+ }
539
+ if (needless.length > 0) {
540
+ for (const key of needless) {
541
+ delete options[key]
542
+ }
543
+ optChanged = true
544
+ }
545
+
546
+ // 保存
547
+ if (optChanged) {
548
+ saveAll(options)
549
+ }
550
+ // 保存返回
551
+ cacheConfigs = options
552
+ return options
553
+ }
554
+
555
+ /**
556
+ * 执行设置项对应的方法
557
+ * @param name 设置项名称
558
+ * @param value 设置项值
559
+ */
560
+ export function run(name: string, value: any) {
561
+ if (typeof configFunction[name] === 'function') configFunction[name](value)
562
+ }
563
+
564
+ /**
565
+ * 获取设置项值
566
+ * @param name 设置项名称
567
+ * @returns 设置项值(如果没有则为 null)
568
+ */
569
+ export function get(name: string): any {
570
+ if (cacheConfigs) {
571
+ const names = Object.keys(cacheConfigs)
572
+ for (let i = 0; i < names.length; i++) {
573
+ if (names[i] === name) {
574
+ const get = cacheConfigs[names[i]]
575
+ try {
576
+ return JSON.parse(get)
577
+ } catch (e: unknown) {
578
+ return get
579
+ }
580
+ }
581
+ }
582
+ }
583
+ return null
584
+ }
585
+
586
+ /**
587
+ * 获取原始设置项值
588
+ * @param name 设置项名称
589
+ * @returns 设置项值(如果没有则为 null)
590
+ * @description <strong>注意:</strong>
591
+ * 此方法获取原始设置项值,不会对值进行 T/F 转换、JSON 解析、URL 解码等操作;
592
+ * 在 Web 端和 Capacitor 端使用时由于存储在 WebStorage 中,需要特别注意预防上述未转换导致的错误。
593
+ */
594
+ export function getRaw(name: string) {
595
+ if ('electron' == backend.type) {
596
+ return backend.call('opt:get', name, true)
597
+ } else if('tauri' == backend.type) {
598
+ return backend.call(undefined, 'opt:get', true, name)
599
+ } else {
600
+ return Promise.resolve(readWebOptions()[name] ?? null)
601
+ }
602
+ }
603
+
604
+ /**
605
+ * 保存设置项
606
+ * @param name 设置项名称
607
+ * @param value 设置项值
608
+ */
609
+ export function save(name: string, value: any) {
610
+ cacheConfigs[name] = value
611
+ saveAll()
612
+ }
613
+ export function saveAll(config = {} as { [key: string]: any }) {
614
+ if (Object.keys(config).length == 0) {
615
+ Object.assign(config, cacheConfigs)
616
+ }
617
+ localStorage.setItem('options', JSON.stringify(config))
618
+
619
+ // electron:将配置保存
620
+ if (backend.isDesktop()) {
621
+ const saveConfig = config
622
+ Object.keys(config).forEach((key) => {
623
+ const isObject = typeof config[key] == 'object'
624
+ saveConfig[key] = isObject ? JSON.stringify(config[key]): config[key]
625
+ })
626
+ backend.call(undefined, 'opt:saveAll', false,
627
+ backend.type == 'tauri' ? { data: saveConfig } : saveConfig)
628
+ }
629
+ }
630
+
631
+ /**
632
+ * 保存并触发设置项操作
633
+ * @param name 设置项名称
634
+ * @param value 设置项值
635
+ */
636
+ export function runAS(name: string, value: any) {
637
+ save(name, value)
638
+ run(name, value)
639
+ }
640
+
641
+ /**
642
+ * 通过 DOM 事件保存并触发设置项操作
643
+ * @param event DOM 事件
644
+ */
645
+ export function runASWEvent(event: Event) {
646
+ const sender = event.target as HTMLElement
647
+ if (sender != null) {
648
+ const type = sender.nodeName
649
+ const name = sender.getAttribute('name')
650
+ let value = null as any
651
+ switch (type) {
652
+ case 'SELECT': {
653
+ value = (sender as HTMLSelectElement).options[
654
+ (sender as HTMLSelectElement).selectedIndex
655
+ ].value
656
+ break
657
+ }
658
+ case 'INPUT': {
659
+ switch ((sender as HTMLInputElement).type) {
660
+ case 'checkbox': {
661
+ value = (sender as HTMLInputElement).checked
662
+ break
663
+ }
664
+ case 'radio': {
665
+ value = sender.dataset.id
666
+ break
667
+ }
668
+ case 'color':
669
+ case 'range':
670
+ case 'number':
671
+ case 'text': {
672
+ value = (sender as HTMLInputElement).value
673
+ break
674
+ }
675
+ }
676
+ break
677
+ }
678
+ }
679
+ if (name !== null) {
680
+ runAS(name, value)
681
+ }
682
+ }
683
+ // 有些设置项需要重启/刷新
684
+ if (sender.dataset.reload == 'true') {
685
+ const { $t } = app.config.globalProperties
686
+ const html =
687
+ '<span>' +
688
+ $t('此操作将在重启应用后生效,现在就要重启吗?') +
689
+ '</span>'
690
+
691
+ const uiStore = useUIStore()
692
+ const popInfo = {
693
+ svg: 'trash-arrow-up',
694
+ html: html,
695
+ title: $t('重启应用'),
696
+ button: [
697
+ {
698
+ text: app.config.globalProperties.$t('确定'),
699
+ fun: () => {
700
+ if (backend.isDesktop()) {
701
+ backend.call(undefined, 'win:relaunch', false)
702
+ } else {
703
+ location.reload()
704
+ }
705
+ },
706
+ },
707
+ {
708
+ text: app.config.globalProperties.$t('取消'),
709
+ master: true,
710
+ fun: () => {
711
+ uiStore.popBoxList.shift()
712
+ },
713
+ },
714
+ ],
715
+ }
716
+ uiStore.popBoxList.push(popInfo)
717
+ }
718
+ }
719
+
720
+ /**
721
+ * 删除设置项
722
+ * @param name 设置项名称
723
+ */
724
+ export function remove(name: string) {
725
+ delete cacheConfigs[name]
726
+ saveAll()
727
+ }
728
+
729
+ // ================ 工具方法 ================
730
+ export function checkDefault(name: string) {
731
+ const settingsStore = useSettingsStore()
732
+ return (settingsStore.sysConfig[name] == undefined ||
733
+ settingsStore.sysConfig[name] == optDefault[name]) ? '' : 'changed'
734
+ }
735
+
736
+ export default {
737
+ get,
738
+ getRaw,
739
+ load,
740
+ save,
741
+ run,
742
+ runAS,
743
+ runASWEvent,
744
+ remove,
745
+ checkDefault,
746
+ extraOptionCards,
747
+ registerExtraOptionCard,
748
+ registerExtraOptionItem,
749
+ }