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.
- package/client/vue/index.vue +60 -634
- package/client/web/env.d.ts +56 -0
- package/client/web/index.html +21 -0
- package/client/web/package-lock.json +3419 -0
- package/client/web/package.json +62 -0
- package/client/web/public/bcui/css/color-dark.css +35 -0
- package/client/web/public/bcui/css/color-light.css +35 -0
- package/client/web/public/bcui/css/style.css +468 -0
- package/client/web/public/bcui/js/main.js +193 -0
- package/client/web/public/css/append-dark.css +13 -0
- package/client/web/public/css/append-light.css +13 -0
- package/client/web/public/css/append-napcat-dark.css +14 -0
- package/client/web/public/css/append-napcat-light.css +14 -0
- package/client/web/public/favicon.ico +0 -0
- package/client/web/public/img/icons/icon-for-fuck-apple.png +0 -0
- package/client/web/public/img/icons/icon-maskable.png +0 -0
- package/client/web/public/img/icons/icon.png +0 -0
- package/client/web/public/img/icons/icon.svg +10 -0
- package/client/web/public/robots.txt +2 -0
- package/client/web/public/sw.js +58 -0
- package/client/web/scripts/copy-bcui.mjs +17 -0
- package/client/web/src/App.vue +1127 -0
- package/client/web/src/assets/css/append/append_darwin.css +68 -0
- package/client/web/src/assets/css/append/append_full_vibrancy.css +290 -0
- package/client/web/src/assets/css/append/append_linux.css +125 -0
- package/client/web/src/assets/css/append/append_linux_vibrancy.css +22 -0
- package/client/web/src/assets/css/append/append_napcat.css +35 -0
- package/client/web/src/assets/css/append/append_new.css +843 -0
- package/client/web/src/assets/css/append/append_vibrancy.css +113 -0
- package/client/web/src/assets/css/append/append_win32.css +109 -0
- package/client/web/src/assets/css/append/mobile/append_mobile_horizontal.css +349 -0
- package/client/web/src/assets/css/append/mobile/append_mobile_vertical.css +96 -0
- package/client/web/src/assets/css/chat.css +2104 -0
- package/client/web/src/assets/css/msg.css +941 -0
- package/client/web/src/assets/css/options.css +811 -0
- package/client/web/src/assets/css/sys_notice.css +62 -0
- package/client/web/src/assets/css/view.css +2105 -0
- package/client/web/src/assets/font//346/226/271/346/255/243/345/226/265/345/221/234/344/275/223.ttf +0 -0
- package/client/web/src/assets/img/Vue.png +0 -0
- package/client/web/src/assets/img/cakes.svg +189 -0
- package/client/web/src/assets/img/icons/AppIcon.png +0 -0
- package/client/web/src/assets/img/icons/AppIconDark.png +0 -0
- package/client/web/src/assets/img/icons/CapacitorAppIcon.png +0 -0
- package/client/web/src/assets/img/icons/CapacitorAppIconDark.png +0 -0
- package/client/web/src/assets/img/icons/NapcatAppIcon.png +0 -0
- package/client/web/src/assets/img/icons/NapcatAppIconDark.png +0 -0
- package/client/web/src/assets/img/icons/exit.png +0 -0
- package/client/web/src/assets/img/icons/pen.png +0 -0
- package/client/web/src/assets/img/stars.svg +89 -0
- package/client/web/src/assets/img//346/234/237/345/276/205.png +0 -0
- package/client/web/src/assets/l10n/_About I10n.md +35 -0
- package/client/web/src/assets/l10n/_l10nconfig.json +7 -0
- package/client/web/src/assets/l10n/zh-CN.po +1941 -0
- package/client/web/src/assets/logo.png +0 -0
- package/client/web/src/assets/pathMap/LLOneBot.yaml +2 -0
- package/client/web/src/assets/pathMap/Lagrange.OneBot.yaml +180 -0
- package/client/web/src/assets/pathMap/NapCat.Onebot.yaml +224 -0
- package/client/web/src/assets/pathMap/SnowLuma.yaml +80 -0
- package/client/web/src/assets/qed.txt +1 -0
- package/client/web/src/components/AboutPan.vue +197 -0
- package/client/web/src/components/BulletinBody.vue +129 -0
- package/client/web/src/components/DepPan.vue +310 -0
- package/client/web/src/components/EmojiFace.vue +32 -0
- package/client/web/src/components/FacePan.vue +461 -0
- package/client/web/src/components/FileBody.vue +129 -0
- package/client/web/src/components/FileManager.vue +532 -0
- package/client/web/src/components/FriendBody.vue +54 -0
- package/client/web/src/components/GlobalSessionSearchBar.vue +301 -0
- package/client/web/src/components/History.vue +49 -0
- package/client/web/src/components/LazyLottie.vue +82 -0
- package/client/web/src/components/MergePan.vue +127 -0
- package/client/web/src/components/MsgBody.vue +1478 -0
- package/client/web/src/components/MusicPlayer.vue +549 -0
- package/client/web/src/components/NoticeBody.vue +168 -0
- package/client/web/src/components/QzonePermissionPan.vue +150 -0
- package/client/web/src/components/RawMsgRenderPreviewPan.vue +190 -0
- package/client/web/src/components/ThemeColorPickerPan.vue +420 -0
- package/client/web/src/components/TinySessionBody.vue +51 -0
- package/client/web/src/components/UmamiInfoPan.vue +1549 -0
- package/client/web/src/components/UpdatePan.vue +263 -0
- package/client/web/src/components/ViewerCom.vue +1360 -0
- package/client/web/src/components/VoiceMsg.vue +626 -0
- package/client/web/src/components/WelPan.vue +894 -0
- package/client/web/src/components/msg-body/.gitkeep +0 -0
- package/client/web/src/components/msg-component/JsonSegComp.vue +138 -0
- package/client/web/src/components/msg-component/XmlSegComp.vue +132 -0
- package/client/web/src/components/msg-component/jsonComp/AutoReply.vue +97 -0
- package/client/web/src/components/msg-component/jsonComp/Contact.lua.vue +129 -0
- package/client/web/src/components/msg-component/jsonComp/Feed.lua.vue +94 -0
- package/client/web/src/components/msg-component/jsonComp/Forum.vue +84 -0
- package/client/web/src/components/msg-component/jsonComp/Mannounce.vue +111 -0
- package/client/web/src/components/msg-component/jsonComp/Map.vue +52 -0
- package/client/web/src/components/msg-component/jsonComp/Miniapp.lua.vue +53 -0
- package/client/web/src/components/msg-component/jsonComp/Miniapp.vue +69 -0
- package/client/web/src/components/msg-component/jsonComp/Music.lua.vue +135 -0
- package/client/web/src/components/msg-component/jsonComp/Tuwen.lua.vue +56 -0
- package/client/web/src/components/notice-component/.gitkeep +0 -0
- package/client/web/src/components/notice-component/MealHungryPan.vue +119 -0
- package/client/web/src/components/tooltip/CustomFaceTooltip.vue +22 -0
- package/client/web/src/components/tooltip/Tooltip.vue +50 -0
- package/client/web/src/components/tooltip/Tooltips.vue +14 -0
- package/client/web/src/components/tooltip/UserInfoTooltip.vue +69 -0
- package/client/web/src/function/base.ts +238 -0
- package/client/web/src/function/connect.ts +2042 -0
- package/client/web/src/function/debug.ts +10 -0
- package/client/web/src/function/elements/information.ts +141 -0
- package/client/web/src/function/elements/system.ts +101 -0
- package/client/web/src/function/elements/vueComp.ts +62 -0
- package/client/web/src/function/favicon.ts +76 -0
- package/client/web/src/function/input.ts +69 -0
- package/client/web/src/function/model/emoji.ts +127 -0
- package/client/web/src/function/model/img.ts +150 -0
- package/client/web/src/function/model/msg-body.ts +51 -0
- package/client/web/src/function/msg.ts +2674 -0
- package/client/web/src/function/notify.ts +225 -0
- package/client/web/src/function/option.ts +749 -0
- package/client/web/src/function/satori-model.ts +818 -0
- package/client/web/src/function/satori.ts +325 -0
- package/client/web/src/function/sender.ts +178 -0
- package/client/web/src/function/tooltip.ts +49 -0
- package/client/web/src/function/utils/appUtil.ts +2288 -0
- package/client/web/src/function/utils/avatarUtil.ts +31 -0
- package/client/web/src/function/utils/backgroundUtil.ts +110 -0
- package/client/web/src/function/utils/linkViewUtil.ts +101 -0
- package/client/web/src/function/utils/localHistoryUtil.ts +517 -0
- package/client/web/src/function/utils/msgUtil.ts +1242 -0
- package/client/web/src/function/utils/pinyin.ts +141 -0
- package/client/web/src/function/utils/recordUtil.ts +187 -0
- package/client/web/src/function/utils/sessionUtil.ts +114 -0
- package/client/web/src/function/utils/systemUtil.ts +664 -0
- package/client/web/src/main.ts +91 -0
- package/client/web/src/pages/Chat.vue +3453 -0
- package/client/web/src/pages/Friends.vue +636 -0
- package/client/web/src/pages/Info.vue +717 -0
- package/client/web/src/pages/Messages.vue +875 -0
- package/client/web/src/pages/Options.vue +82 -0
- package/client/web/src/pages/Qzone.vue +1178 -0
- package/client/web/src/pages/chat-view/ChatDanmaku.vue +759 -0
- package/client/web/src/pages/chat-view/ChatGlagame.vue +642 -0
- package/client/web/src/pages/chat-view/ChatTerminal.vue +1044 -0
- package/client/web/src/pages/chat-view/SystemNotice.vue +171 -0
- package/client/web/src/pages/options/OptAccount.vue +107 -0
- package/client/web/src/pages/options/OptAddon.vue +185 -0
- package/client/web/src/pages/options/OptDev.vue +704 -0
- package/client/web/src/pages/options/OptFunction.vue +403 -0
- package/client/web/src/pages/options/OptInfo.vue +165 -0
- package/client/web/src/pages/options/OptView.vue +846 -0
- package/client/web/src/runtime/backend.ts +310 -0
- package/client/web/src/state/auth.ts +14 -0
- package/client/web/src/state/chat.ts +36 -0
- package/client/web/src/state/connection.ts +20 -0
- package/client/web/src/state/contact.ts +37 -0
- package/client/web/src/state/qzone.ts +18 -0
- package/client/web/src/state/sessionHistory.ts +25 -0
- package/client/web/src/state/settings.ts +18 -0
- package/client/web/src/state/sticker.ts +10 -0
- package/client/web/src/state/ui.ts +56 -0
- package/client/web/tsconfig.json +27 -0
- package/client/web/tsconfig.tsbuildinfo +1 -0
- package/client/web/vite.config.ts +33 -0
- package/dist/index.js +1 -12
- package/dist/style.css +1 -1
- package/lib/index.js +2070 -2077
- package/package.json +15 -2
- package/readme.md +0 -22
- package/src/bootstrap.ts +71 -0
- package/src/cache.ts +319 -0
- package/src/config.ts +23 -35
- package/src/database.ts +650 -0
- package/src/index.ts +27 -59
- package/src/media.ts +302 -0
- package/src/recorder.ts +175 -0
- package/src/self-message.ts +456 -0
- package/src/server.d.ts +26 -0
- package/src/types.ts +108 -48
- package/src/web.ts +712 -0
- package/client/vue/chat-logic.ts +0 -683
- package/client/vue/composables/useChatActions.ts +0 -61
- package/client/vue/composables/useChatData.ts +0 -185
- package/client/vue/composables/useImageCache.ts +0 -281
- package/client/vue/composables/useVideoCache.ts +0 -125
- package/client/vue/types.ts +0 -72
- package/lib/api-handlers.d.ts +0 -25
- package/lib/config.d.ts +0 -14
- package/lib/file-manager.d.ts +0 -124
- package/lib/index.d.ts +0 -17
- package/lib/logger.d.ts +0 -9
- package/lib/message-handler.d.ts +0 -35
- package/lib/types.d.ts +0 -57
- package/lib/utils.d.ts +0 -18
- package/src/api-handlers.ts +0 -646
- package/src/file-manager.ts +0 -1436
- package/src/message-handler.ts +0 -507
- 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
|
+
}
|