dsh-cliproxyapi-pro 1.4.1 → 1.5.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/CHANGELOG.md +17 -0
- package/README.en.md +84 -5
- package/README.md +84 -5
- package/lib/client.js +640 -2
- package/lib/index.js +90 -0
- package/lib/updater.js +376 -0
- package/package.json +3 -2
package/lib/client.js
CHANGED
|
@@ -7,7 +7,7 @@ window.__ModuleLoader__.load({
|
|
|
7
7
|
|
|
8
8
|
const React = require("react");
|
|
9
9
|
const h = React.createElement;
|
|
10
|
-
const { useState, useEffect, useCallback, useMemo } = React;
|
|
10
|
+
const { useState, useEffect, useCallback, useMemo, useRef } = React;
|
|
11
11
|
|
|
12
12
|
const PACKAGE_NAME = "dsh-cliproxyapi-pro";
|
|
13
13
|
const MODULE_ID = "dsh-cliproxyapi-pro";
|
|
@@ -20,6 +20,10 @@ window.__ModuleLoader__.load({
|
|
|
20
20
|
save: "api/dsh-cliproxyapi/save",
|
|
21
21
|
addAccount: "api/dsh-cliproxyapi/accounts/add",
|
|
22
22
|
removeAccount: "api/dsh-cliproxyapi/accounts/remove",
|
|
23
|
+
updateStatus: "api/dsh-cliproxyapi/updates/status",
|
|
24
|
+
updateCheck: "api/dsh-cliproxyapi/updates/check",
|
|
25
|
+
updateSave: "api/dsh-cliproxyapi/updates/save",
|
|
26
|
+
updateDownload: "api/dsh-cliproxyapi/updates/download",
|
|
23
27
|
};
|
|
24
28
|
|
|
25
29
|
const zh = {
|
|
@@ -118,6 +122,79 @@ window.__ModuleLoader__.load({
|
|
|
118
122
|
resetParamsBtn: "恢复自动探测",
|
|
119
123
|
paramsHint: "留空并保存即恢复 CLIProxyAPI 自动探测值。修改后请点击「保存模型勾选」才会写入模型选择器。",
|
|
120
124
|
discoveredValue: "探测值",
|
|
125
|
+
updateTitle: "插件更新",
|
|
126
|
+
updateToggleDetails: "展开或收起插件更新设置",
|
|
127
|
+
updateCompactCurrent: "当前 {version}",
|
|
128
|
+
updateCompactAvailable: "有新版本 {version}",
|
|
129
|
+
updateCompactDownloaded: "{version} 已下载",
|
|
130
|
+
updateCompactError: "更新异常",
|
|
131
|
+
updateCompactStatusUnavailable: "更新服务未就绪",
|
|
132
|
+
updateCompactErrorNetwork: "网络异常",
|
|
133
|
+
updateCompactErrorChannel: "通道未发布",
|
|
134
|
+
updateStatusUnavailableHint: "无法读取更新服务,可能是插件宿主代码尚未重载:重载插件后刷新页面,或检查更新接口是否可用。",
|
|
135
|
+
updateSubtitle: "仅更新 CLIProxyAPI 插件自身,不更新 CPA 服务或 DSH 宿主。设置修改后立即保存。",
|
|
136
|
+
updateNewVersion: "新版本",
|
|
137
|
+
updateVersionSummary: "当前版本 {current} · 最新版本 {latest}",
|
|
138
|
+
updateUnknownVersion: "未知",
|
|
139
|
+
updateNotChecked: "尚未检查",
|
|
140
|
+
updateLoading: "正在读取更新状态…",
|
|
141
|
+
updateUpToDate: "已是所选通道最新版本",
|
|
142
|
+
updateAvailable: "有可用更新",
|
|
143
|
+
updateCheckBtn: "检查更新",
|
|
144
|
+
updateChecking: "正在检查…",
|
|
145
|
+
updateRetryBtn: "重新读取状态",
|
|
146
|
+
updateDownloadBtn: "下载更新",
|
|
147
|
+
updateDownloading: "正在下载并校验…",
|
|
148
|
+
updateDownloadedBtn: "重新下载",
|
|
149
|
+
updateChannelLabel: "更新通道",
|
|
150
|
+
updateChannelHint: "稳定版适合日常使用;Beta 和开发版可能不稳定。切换后请重新检查更新。",
|
|
151
|
+
updateChannelStable: "稳定",
|
|
152
|
+
updateChannelBeta: "Beta",
|
|
153
|
+
updateChannelDev: "开发",
|
|
154
|
+
updateRegionLabel: "下载区域",
|
|
155
|
+
updateRegionHint: "自动选择可用下载区域;也可手动指定全球或中国大陆源。",
|
|
156
|
+
updateRegionAuto: "自动",
|
|
157
|
+
updateRegionGlobal: "全球",
|
|
158
|
+
updateRegionCn: "中国大陆",
|
|
159
|
+
updateEffectiveRegion: "当前使用:{region}",
|
|
160
|
+
updateAutoCheckLabel: "自动检查更新",
|
|
161
|
+
updateAutoCheckHint: "启动时及每 6 小时检查所选通道;关闭后仍可手动检查。",
|
|
162
|
+
updateAutoDownloadLabel: "自动下载更新",
|
|
163
|
+
updateAutoDownloadHint: "自动检查发现新版本后下载并校验离线包;需开启自动检查,不会自动安装或重启 DSH。",
|
|
164
|
+
updateLastChecked: "上次检查:{time}",
|
|
165
|
+
updateSaving: "正在保存更新设置…",
|
|
166
|
+
updateSaved: "更新设置已保存",
|
|
167
|
+
updateLoadFailed: "无法读取插件更新状态,请重试。",
|
|
168
|
+
updateCheckFailed: "检查更新失败,请稍后重试或切换下载区域。",
|
|
169
|
+
updateSaveFailed: "更新设置保存失败,请重试。",
|
|
170
|
+
updateDownloadFailed: "下载更新失败,请稍后重试或切换下载区域。",
|
|
171
|
+
updateNetworkError: "无法连接更新源,请检查网络或切换下载区域后重试。",
|
|
172
|
+
updateMetadataError: "更新源返回了无效的版本信息,请稍后重试。",
|
|
173
|
+
updateChannelUnavailable: "所选通道尚未发布可用版本,请选择其他通道。",
|
|
174
|
+
updateIntegrityError: "下载包完整性校验失败,未应用任何更改。请重新下载。",
|
|
175
|
+
updateInvalidPackage: "下载包不是有效的本插件更新包,未应用任何更改。",
|
|
176
|
+
updateStorageError: "无法保存更新设置或离线包,请检查磁盘空间和目录写入权限。",
|
|
177
|
+
updateBusyError: "已有更新任务正在进行,请等待完成后重试。",
|
|
178
|
+
updateStaleVersion: "可用版本已变化,请重新检查更新后再下载。",
|
|
179
|
+
updateDisposedError: "插件正在重新加载,请稍后重新读取更新状态。",
|
|
180
|
+
updateHttpError: "更新服务请求失败(HTTP {status}),请重试。",
|
|
181
|
+
updateLinkedHint: "当前为本地链接安装,更新不会覆盖源码目录。可检查并下载离线包;若要保留源码开发方式,请自行更新代码。",
|
|
182
|
+
updateUnsupportedHint: "当前 DSH 宿主不支持此更新操作,请升级 DSH 后重试。",
|
|
183
|
+
updateNotInstalledHint: "未找到已安装的插件,无法准备更新。请先通过 DSH 插件管理器安装本插件。",
|
|
184
|
+
updateBlockedHint: "当前安装方式无法准备更新,请使用 DSH 插件管理器检查安装状态。",
|
|
185
|
+
updateConfirmTitle: "确认下载版本 {version}?",
|
|
186
|
+
updateConfirmHint: "只下载并校验离线包,不会自动安装或重启。下载后需关闭 DSH,执行提示的升级命令,再重新启动。",
|
|
187
|
+
updateLinkedConfirmHint: "只下载并校验离线包,不会覆盖源码、安装或重启。若要保留本地链接方式,请手动更新源码后重新启动 DSH;通过离线包重新安装会切换插件安装来源。",
|
|
188
|
+
updateConfirmBtn: "确认下载",
|
|
189
|
+
updateCancelBtn: "取消",
|
|
190
|
+
updateDownloadRequested: "下载任务已提交,完成后将在此显示离线包与升级命令。",
|
|
191
|
+
updateDownloadedHint: "版本 {version} 已下载并校验。请先保存工作并关闭 DSH,再执行下方命令升级,最后重新启动 DSH 生效。",
|
|
192
|
+
updateLinkedDownloadedHint: "版本 {version} 的离线包已下载并校验。当前为本地链接安装;如需保留源码方式,请手动更新源码,之后重新启动 DSH 生效。不要将离线包覆盖到正在使用的源码目录。",
|
|
193
|
+
updateLinkedSourceHint: "未提供自动升级命令以保护本地链接来源。通过离线包重新安装会切换插件安装来源。",
|
|
194
|
+
updateDownloadPathLabel: "已校验的离线包",
|
|
195
|
+
updateUpgradeCommandLabel: "关闭 DSH 后执行升级命令",
|
|
196
|
+
updateCommandUnavailable: "未生成升级命令。请先确认 DSH 支持的离线包升级方式;升级前关闭 DSH,完成后重新启动。",
|
|
197
|
+
updateOfflineHint: "自动下载不等于自动安装。插件不会关闭、重启 DSH,也不会覆盖正在使用的插件文件。",
|
|
121
198
|
};
|
|
122
199
|
|
|
123
200
|
const en = {
|
|
@@ -216,6 +293,79 @@ window.__ModuleLoader__.load({
|
|
|
216
293
|
resetParamsBtn: "Reset to Discovered",
|
|
217
294
|
paramsHint: "Clear a value and save to restore the value discovered from CLIProxyAPI. Click Apply Selected Models to write changes to the picker.",
|
|
218
295
|
discoveredValue: "Discovered",
|
|
296
|
+
updateTitle: "Plugin Updates",
|
|
297
|
+
updateToggleDetails: "Expand or collapse plugin update settings",
|
|
298
|
+
updateCompactCurrent: "Current {version}",
|
|
299
|
+
updateCompactAvailable: "Update {version}",
|
|
300
|
+
updateCompactDownloaded: "{version} downloaded",
|
|
301
|
+
updateCompactError: "Update error",
|
|
302
|
+
updateCompactStatusUnavailable: "Update service unavailable",
|
|
303
|
+
updateCompactErrorNetwork: "Network error",
|
|
304
|
+
updateCompactErrorChannel: "Channel unpublished",
|
|
305
|
+
updateStatusUnavailableHint: "Could not reach the update service; the host plugin code may not be reloaded yet. Reload the plugin and refresh the page, or check that the update endpoint is registered.",
|
|
306
|
+
updateSubtitle: "Updates this CLIProxyAPI plugin only, not the CPA server or DSH. Changes to update settings are saved immediately.",
|
|
307
|
+
updateNewVersion: "New Version",
|
|
308
|
+
updateVersionSummary: "Current {current} · Latest {latest}",
|
|
309
|
+
updateUnknownVersion: "Unknown",
|
|
310
|
+
updateNotChecked: "Not checked yet",
|
|
311
|
+
updateLoading: "Loading update status…",
|
|
312
|
+
updateUpToDate: "Up to date on this channel",
|
|
313
|
+
updateAvailable: "Update available",
|
|
314
|
+
updateCheckBtn: "Check for Updates",
|
|
315
|
+
updateChecking: "Checking…",
|
|
316
|
+
updateRetryBtn: "Reload Status",
|
|
317
|
+
updateDownloadBtn: "Download Update",
|
|
318
|
+
updateDownloading: "Downloading & verifying…",
|
|
319
|
+
updateDownloadedBtn: "Download Again",
|
|
320
|
+
updateChannelLabel: "Update Channel",
|
|
321
|
+
updateChannelHint: "Stable is recommended for daily use. Beta and development builds may be unstable. Check again after switching channels.",
|
|
322
|
+
updateChannelStable: "Stable",
|
|
323
|
+
updateChannelBeta: "Beta",
|
|
324
|
+
updateChannelDev: "Dev",
|
|
325
|
+
updateRegionLabel: "Download Region",
|
|
326
|
+
updateRegionHint: "Automatically select an available region, or choose the global or Mainland China source manually.",
|
|
327
|
+
updateRegionAuto: "Auto",
|
|
328
|
+
updateRegionGlobal: "Global",
|
|
329
|
+
updateRegionCn: "Mainland China",
|
|
330
|
+
updateEffectiveRegion: "Using: {region}",
|
|
331
|
+
updateAutoCheckLabel: "Automatically Check for Updates",
|
|
332
|
+
updateAutoCheckHint: "Check the selected channel at startup and every 6 hours. Manual checks remain available when disabled.",
|
|
333
|
+
updateAutoDownloadLabel: "Automatically Download Updates",
|
|
334
|
+
updateAutoDownloadHint: "Download and verify packages found by automatic checks (requires Auto Check). Never installs or restarts DSH automatically.",
|
|
335
|
+
updateLastChecked: "Last checked: {time}",
|
|
336
|
+
updateSaving: "Saving update settings…",
|
|
337
|
+
updateSaved: "Update settings saved",
|
|
338
|
+
updateLoadFailed: "Could not load plugin update status. Please retry.",
|
|
339
|
+
updateCheckFailed: "Could not check for updates. Retry later or change the download region.",
|
|
340
|
+
updateSaveFailed: "Could not save update settings. Please retry.",
|
|
341
|
+
updateDownloadFailed: "Could not download the update. Retry later or change the download region.",
|
|
342
|
+
updateNetworkError: "Could not reach the update source. Check your network or change the download region and retry.",
|
|
343
|
+
updateMetadataError: "The update source returned invalid version information. Please retry later.",
|
|
344
|
+
updateChannelUnavailable: "No release has been published on this channel. Please choose another channel.",
|
|
345
|
+
updateIntegrityError: "Package integrity verification failed. No changes were applied. Please download again.",
|
|
346
|
+
updateInvalidPackage: "The downloaded file is not a valid update package for this plugin. No changes were applied.",
|
|
347
|
+
updateStorageError: "Could not save update settings or the offline package. Check free disk space and write permissions.",
|
|
348
|
+
updateBusyError: "Another update operation is in progress. Wait for it to finish and retry.",
|
|
349
|
+
updateStaleVersion: "The available version changed. Check for updates again before downloading.",
|
|
350
|
+
updateDisposedError: "The plugin is reloading. Please reload update status shortly.",
|
|
351
|
+
updateHttpError: "The update service request failed (HTTP {status}). Please retry.",
|
|
352
|
+
updateLinkedHint: "This is a linked local installation. Updates never overwrite its source directory. You can check and download an offline package; update the source yourself to keep the development setup.",
|
|
353
|
+
updateUnsupportedHint: "This DSH version does not support this update operation. Upgrade DSH and retry.",
|
|
354
|
+
updateNotInstalledHint: "No installed plugin was found, so an update cannot be prepared. Install this plugin through the DSH plugin manager first.",
|
|
355
|
+
updateBlockedHint: "An update cannot be prepared for this installation. Check it in the DSH plugin manager.",
|
|
356
|
+
updateConfirmTitle: "Download version {version}?",
|
|
357
|
+
updateConfirmHint: "Only downloads and verifies the offline package. Nothing is installed or restarted automatically. Then close DSH, run the provided upgrade command, and start DSH again.",
|
|
358
|
+
updateLinkedConfirmHint: "Only downloads and verifies the offline package. It never overwrites source code, installs, or restarts DSH. To keep the linked setup, update the source manually and restart DSH. Reinstalling from the offline package changes the installation source.",
|
|
359
|
+
updateConfirmBtn: "Confirm Download",
|
|
360
|
+
updateCancelBtn: "Cancel",
|
|
361
|
+
updateDownloadRequested: "Download requested. The verified package and upgrade command will appear here when ready.",
|
|
362
|
+
updateDownloadedHint: "Version {version} has been downloaded and verified. Save your work and close DSH, then run the command below to upgrade. Start DSH again for it to take effect.",
|
|
363
|
+
updateLinkedDownloadedHint: "The offline package for version {version} has been downloaded and verified. This is a linked installation. To keep the source-based setup, update the source manually, then restart DSH. Do not extract the package over source files that are in use.",
|
|
364
|
+
updateLinkedSourceHint: "No upgrade command is provided, to protect the linked installation source. Reinstalling from the offline package changes that source.",
|
|
365
|
+
updateDownloadPathLabel: "Verified Offline Package",
|
|
366
|
+
updateUpgradeCommandLabel: "Upgrade Command — Run After Closing DSH",
|
|
367
|
+
updateCommandUnavailable: "No upgrade command was generated. First verify the offline-package upgrade method supported by your DSH version. Close DSH before upgrading, then start it again.",
|
|
368
|
+
updateOfflineHint: "Automatic downloading is not automatic installation. This plugin never closes or restarts DSH, or overwrites plugin files that are in use.",
|
|
219
369
|
};
|
|
220
370
|
|
|
221
371
|
const CSS = `
|
|
@@ -443,7 +593,7 @@ window.__ModuleLoader__.load({
|
|
|
443
593
|
.dcpa-seg-btn[data-active="true"] {
|
|
444
594
|
background: var(--dsw-alias-bg-base);
|
|
445
595
|
color: var(--dsw-alias-label-primary);
|
|
446
|
-
box-shadow: 0 1px 2px
|
|
596
|
+
box-shadow: 0 1px 2px var(--dsw-alias-border-l1);
|
|
447
597
|
}
|
|
448
598
|
.dcpa-toolbar {
|
|
449
599
|
display: flex;
|
|
@@ -650,6 +800,136 @@ window.__ModuleLoader__.load({
|
|
|
650
800
|
.dcpa-btn-danger:hover:not(:disabled) {
|
|
651
801
|
border-color: var(--dsw-alias-state-error-primary);
|
|
652
802
|
}
|
|
803
|
+
.dcpa-update-row {
|
|
804
|
+
display: flex;
|
|
805
|
+
align-items: center;
|
|
806
|
+
justify-content: space-between;
|
|
807
|
+
gap: 16px;
|
|
808
|
+
padding: 11px 0;
|
|
809
|
+
border-top: 0.5px solid var(--dsw-alias-border-l1);
|
|
810
|
+
}
|
|
811
|
+
.dcpa-update-row .dcpa-field {
|
|
812
|
+
flex: 1;
|
|
813
|
+
min-width: 0;
|
|
814
|
+
}
|
|
815
|
+
.dcpa-update-row .dcpa-seg, .dcpa-update-row .dcpa-actions {
|
|
816
|
+
flex: none;
|
|
817
|
+
}
|
|
818
|
+
.dcpa-update-version {
|
|
819
|
+
font-size: 13px;
|
|
820
|
+
font-weight: 600;
|
|
821
|
+
color: var(--dsw-alias-label-primary);
|
|
822
|
+
overflow-wrap: anywhere;
|
|
823
|
+
}
|
|
824
|
+
.dcpa-update-card {
|
|
825
|
+
display: block;
|
|
826
|
+
padding: 0;
|
|
827
|
+
}
|
|
828
|
+
.dcpa-update-summary {
|
|
829
|
+
display: flex;
|
|
830
|
+
align-items: center;
|
|
831
|
+
gap: 10px;
|
|
832
|
+
min-height: 42px;
|
|
833
|
+
padding: 10px 14px;
|
|
834
|
+
border-radius: 12px;
|
|
835
|
+
cursor: pointer;
|
|
836
|
+
list-style: none;
|
|
837
|
+
}
|
|
838
|
+
.dcpa-update-summary::-webkit-details-marker { display: none; }
|
|
839
|
+
.dcpa-update-summary:hover { background: var(--dsw-alias-bg-layer-2); }
|
|
840
|
+
.dcpa-update-summary:focus-visible {
|
|
841
|
+
outline: 2px solid var(--dsw-alias-brand-primary);
|
|
842
|
+
outline-offset: 2px;
|
|
843
|
+
}
|
|
844
|
+
.dcpa-update-summary-title {
|
|
845
|
+
flex: none;
|
|
846
|
+
font-weight: 600;
|
|
847
|
+
}
|
|
848
|
+
.dcpa-update-summary-current, .dcpa-update-summary-status {
|
|
849
|
+
min-width: 0;
|
|
850
|
+
overflow: hidden;
|
|
851
|
+
text-overflow: ellipsis;
|
|
852
|
+
white-space: nowrap;
|
|
853
|
+
color: var(--dsw-alias-label-secondary);
|
|
854
|
+
font-size: 12px;
|
|
855
|
+
}
|
|
856
|
+
.dcpa-update-summary-current { flex: 0 1 auto; }
|
|
857
|
+
.dcpa-update-summary-status { flex: 1; text-align: right; }
|
|
858
|
+
.dcpa-update-summary-status[data-state="available"],
|
|
859
|
+
.dcpa-update-summary-status[data-state="downloaded"] { color: var(--dsw-alias-brand-primary); }
|
|
860
|
+
.dcpa-update-summary-status[data-state="error"] { color: var(--dsw-alias-state-error-primary); }
|
|
861
|
+
.dcpa-update-summary::after {
|
|
862
|
+
content: "";
|
|
863
|
+
flex: none;
|
|
864
|
+
width: 7px;
|
|
865
|
+
height: 7px;
|
|
866
|
+
margin-right: 3px;
|
|
867
|
+
border-right: 1.5px solid var(--dsw-alias-label-secondary);
|
|
868
|
+
border-bottom: 1.5px solid var(--dsw-alias-label-secondary);
|
|
869
|
+
transform: rotate(-45deg);
|
|
870
|
+
}
|
|
871
|
+
.dcpa-update-card[open] > .dcpa-update-summary::after { transform: rotate(45deg); }
|
|
872
|
+
.dcpa-update-content {
|
|
873
|
+
padding: 0 14px 12px;
|
|
874
|
+
border-top: 0.5px solid var(--dsw-alias-border-l1);
|
|
875
|
+
}
|
|
876
|
+
.dcpa-update-content > .dcpa-subtitle { padding: 10px 0; }
|
|
877
|
+
.dcpa-update-card .dcpa-seg {
|
|
878
|
+
flex-wrap: wrap;
|
|
879
|
+
}
|
|
880
|
+
.dcpa-update-card .dcpa-seg-btn:disabled, .dcpa-update-card .dcpa-switch:disabled {
|
|
881
|
+
opacity: 0.5;
|
|
882
|
+
cursor: default;
|
|
883
|
+
}
|
|
884
|
+
.dcpa-update-card button:focus-visible {
|
|
885
|
+
outline: 2px solid var(--dsw-alias-brand-primary);
|
|
886
|
+
outline-offset: 2px;
|
|
887
|
+
}
|
|
888
|
+
.dcpa-update-notice {
|
|
889
|
+
display: flex;
|
|
890
|
+
flex-direction: column;
|
|
891
|
+
gap: 8px;
|
|
892
|
+
margin: 8px 0;
|
|
893
|
+
padding: 10px 12px;
|
|
894
|
+
border: 0.5px solid var(--dsw-alias-border-l2);
|
|
895
|
+
border-radius: 8px;
|
|
896
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
897
|
+
}
|
|
898
|
+
.dcpa-update-notice p {
|
|
899
|
+
margin: 0;
|
|
900
|
+
}
|
|
901
|
+
.dcpa-update-code {
|
|
902
|
+
display: block;
|
|
903
|
+
margin: 0;
|
|
904
|
+
padding: 8px 10px;
|
|
905
|
+
border: 0.5px solid var(--dsw-alias-border-l1);
|
|
906
|
+
border-radius: 6px;
|
|
907
|
+
background: var(--dsw-alias-bg-base);
|
|
908
|
+
color: var(--dsw-alias-label-primary);
|
|
909
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
910
|
+
font-size: 11px;
|
|
911
|
+
line-height: 17px;
|
|
912
|
+
white-space: pre-wrap;
|
|
913
|
+
overflow-wrap: anywhere;
|
|
914
|
+
user-select: text;
|
|
915
|
+
}
|
|
916
|
+
.dcpa-update-footer {
|
|
917
|
+
display: flex;
|
|
918
|
+
flex-direction: column;
|
|
919
|
+
gap: 8px;
|
|
920
|
+
border-top: 0.5px solid var(--dsw-alias-border-l1);
|
|
921
|
+
padding-top: 10px;
|
|
922
|
+
}
|
|
923
|
+
@media (max-width: 620px) {
|
|
924
|
+
.dcpa-update-row:not(.dcpa-switch-row) {
|
|
925
|
+
align-items: flex-start;
|
|
926
|
+
flex-direction: column;
|
|
927
|
+
gap: 8px;
|
|
928
|
+
}
|
|
929
|
+
.dcpa-update-row .dcpa-actions {
|
|
930
|
+
flex-wrap: wrap;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
653
933
|
`;
|
|
654
934
|
|
|
655
935
|
const REASONING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
@@ -705,6 +985,363 @@ window.__ModuleLoader__.load({
|
|
|
705
985
|
: "dcpa-dot dcpa-dot-idle";
|
|
706
986
|
}
|
|
707
987
|
|
|
988
|
+
const UPDATE_ERROR_KEYS = {
|
|
989
|
+
"network-error": "updateNetworkError",
|
|
990
|
+
"invalid-metadata": "updateMetadataError",
|
|
991
|
+
"invalid-response": "updateMetadataError",
|
|
992
|
+
"channel-unavailable": "updateChannelUnavailable",
|
|
993
|
+
"unavailable-channel": "updateChannelUnavailable",
|
|
994
|
+
"integrity-error": "updateIntegrityError",
|
|
995
|
+
"invalid-package": "updateInvalidPackage",
|
|
996
|
+
"storage-error": "updateStorageError",
|
|
997
|
+
busy: "updateBusyError",
|
|
998
|
+
"stale-version": "updateStaleVersion",
|
|
999
|
+
disposed: "updateDisposedError",
|
|
1000
|
+
"linked-install": "updateLinkedHint",
|
|
1001
|
+
"unsupported-host": "updateUnsupportedHint",
|
|
1002
|
+
"not-installed": "updateNotInstalledHint",
|
|
1003
|
+
};
|
|
1004
|
+
|
|
1005
|
+
function updateErrorText(t, failure, fallback) {
|
|
1006
|
+
const code = failure?.updateCode;
|
|
1007
|
+
if (typeof code === "string" && Object.prototype.hasOwnProperty.call(UPDATE_ERROR_KEYS, code)) {
|
|
1008
|
+
return t(UPDATE_ERROR_KEYS[code]);
|
|
1009
|
+
}
|
|
1010
|
+
if (failure?.httpStatus) {
|
|
1011
|
+
return t("updateHttpError").replace("{status}", String(failure.httpStatus));
|
|
1012
|
+
}
|
|
1013
|
+
// Server messages may be English or contain internal paths. Do not expose them.
|
|
1014
|
+
return t(fallback);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
async function requestPluginUpdate(url, payload, signal) {
|
|
1018
|
+
let res;
|
|
1019
|
+
try {
|
|
1020
|
+
res = await fetch(url, {
|
|
1021
|
+
signal,
|
|
1022
|
+
cache: "no-store",
|
|
1023
|
+
...(payload === undefined
|
|
1024
|
+
? {}
|
|
1025
|
+
: {
|
|
1026
|
+
method: "POST",
|
|
1027
|
+
headers: { "content-type": "application/json" },
|
|
1028
|
+
body: JSON.stringify(payload),
|
|
1029
|
+
}),
|
|
1030
|
+
});
|
|
1031
|
+
} catch (err) {
|
|
1032
|
+
if (err?.name === "AbortError") throw err;
|
|
1033
|
+
const failure = new Error("Update network request failed");
|
|
1034
|
+
failure.updateCode = "network-error";
|
|
1035
|
+
throw failure;
|
|
1036
|
+
}
|
|
1037
|
+
let body;
|
|
1038
|
+
try {
|
|
1039
|
+
body = await res.json();
|
|
1040
|
+
} catch {
|
|
1041
|
+
const failure = new Error("Invalid update response");
|
|
1042
|
+
failure.updateCode = "invalid-response";
|
|
1043
|
+
if (!res.ok) failure.httpStatus = res.status;
|
|
1044
|
+
throw failure;
|
|
1045
|
+
}
|
|
1046
|
+
const validState = body?.update && typeof body.update === "object" && !Array.isArray(body.update);
|
|
1047
|
+
if (!res.ok || body?.ok === false || !validState ||
|
|
1048
|
+
payload !== undefined && (body.update.error || body.update.errorCode)) {
|
|
1049
|
+
const failure = new Error("Update request failed");
|
|
1050
|
+
failure.updateCode = body?.errorCode || body?.update?.errorCode || (!validState ? "invalid-response" : null);
|
|
1051
|
+
failure.httpStatus = !res.ok ? res.status : null;
|
|
1052
|
+
failure.updateState = validState ? body.update : null;
|
|
1053
|
+
throw failure;
|
|
1054
|
+
}
|
|
1055
|
+
return body.update;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
/** Update state is isolated: polling never reloads account data or drafts. */
|
|
1059
|
+
function PluginUpdatePanel({ t }) {
|
|
1060
|
+
const [update, setUpdate] = useState(null);
|
|
1061
|
+
const [loading, setLoading] = useState(true);
|
|
1062
|
+
const [busy, setBusy] = useState(null);
|
|
1063
|
+
const [error, setError] = useState(null);
|
|
1064
|
+
const [notice, setNotice] = useState(null);
|
|
1065
|
+
const [confirmVersion, setConfirmVersion] = useState(null);
|
|
1066
|
+
const lifecycle = useRef(null);
|
|
1067
|
+
|
|
1068
|
+
const loadStatus = useCallback(async () => {
|
|
1069
|
+
const life = lifecycle.current;
|
|
1070
|
+
if (!life?.mounted || life.action || life.polling) return;
|
|
1071
|
+
life.polling = true;
|
|
1072
|
+
const serial = ++life.serial;
|
|
1073
|
+
const controller = new AbortController();
|
|
1074
|
+
life.controllers.add(controller);
|
|
1075
|
+
life.statusController = controller;
|
|
1076
|
+
try {
|
|
1077
|
+
// This GET is read-only. Only an explicit check action probes the registry.
|
|
1078
|
+
const next = await requestPluginUpdate(API.updateStatus, undefined, controller.signal);
|
|
1079
|
+
if (!life.mounted || serial !== life.serial) return;
|
|
1080
|
+
life.snapshot = next;
|
|
1081
|
+
setUpdate(next);
|
|
1082
|
+
setError((previous) => (previous?.source === "status" ? null : previous));
|
|
1083
|
+
} catch (failure) {
|
|
1084
|
+
if (!life.mounted || serial !== life.serial || failure?.name === "AbortError") return;
|
|
1085
|
+
setError((previous) =>
|
|
1086
|
+
previous && previous.source !== "status"
|
|
1087
|
+
? previous
|
|
1088
|
+
: { failure, fallback: "updateLoadFailed", source: "status" }
|
|
1089
|
+
);
|
|
1090
|
+
} finally {
|
|
1091
|
+
life.controllers.delete(controller);
|
|
1092
|
+
if (life.statusController === controller) life.statusController = null;
|
|
1093
|
+
life.polling = false;
|
|
1094
|
+
if (life.mounted) setLoading(false);
|
|
1095
|
+
}
|
|
1096
|
+
}, []);
|
|
1097
|
+
|
|
1098
|
+
useEffect(() => {
|
|
1099
|
+
// A fresh lifetime also prevents old StrictMode/HMR requests from winning.
|
|
1100
|
+
const life = {
|
|
1101
|
+
mounted: true,
|
|
1102
|
+
action: false,
|
|
1103
|
+
polling: false,
|
|
1104
|
+
serial: 0,
|
|
1105
|
+
controllers: new Set(),
|
|
1106
|
+
statusController: null,
|
|
1107
|
+
snapshot: null,
|
|
1108
|
+
};
|
|
1109
|
+
lifecycle.current = life;
|
|
1110
|
+
let timer;
|
|
1111
|
+
const poll = async () => {
|
|
1112
|
+
await loadStatus();
|
|
1113
|
+
if (life.mounted) {
|
|
1114
|
+
timer = setTimeout(poll, life.snapshot?.checking || life.snapshot?.downloading ? 2000 : 15000);
|
|
1115
|
+
}
|
|
1116
|
+
};
|
|
1117
|
+
void poll();
|
|
1118
|
+
return () => {
|
|
1119
|
+
life.mounted = false;
|
|
1120
|
+
life.serial += 1;
|
|
1121
|
+
clearTimeout(timer);
|
|
1122
|
+
for (const controller of life.controllers) controller.abort();
|
|
1123
|
+
life.controllers.clear();
|
|
1124
|
+
};
|
|
1125
|
+
}, [loadStatus]);
|
|
1126
|
+
|
|
1127
|
+
const runAction = useCallback(async (kind, payload = {}) => {
|
|
1128
|
+
const life = lifecycle.current;
|
|
1129
|
+
if (!life?.mounted || life.action || life.snapshot?.checking || life.snapshot?.downloading) return;
|
|
1130
|
+
life.action = true;
|
|
1131
|
+
life.serial += 1;
|
|
1132
|
+
life.statusController?.abort();
|
|
1133
|
+
setBusy(kind);
|
|
1134
|
+
setError(null);
|
|
1135
|
+
setNotice(null);
|
|
1136
|
+
setConfirmVersion(null);
|
|
1137
|
+
const controller = new AbortController();
|
|
1138
|
+
life.controllers.add(controller);
|
|
1139
|
+
const url = kind === "save" ? API.updateSave : kind === "download" ? API.updateDownload : API.updateCheck;
|
|
1140
|
+
try {
|
|
1141
|
+
const next = await requestPluginUpdate(url, payload, controller.signal);
|
|
1142
|
+
if (!life.mounted) return;
|
|
1143
|
+
life.snapshot = next;
|
|
1144
|
+
setUpdate(next);
|
|
1145
|
+
if (kind === "save") setNotice("updateSaved");
|
|
1146
|
+
if (kind === "download" && !next.downloadedVersion) setNotice("updateDownloadRequested");
|
|
1147
|
+
} catch (failure) {
|
|
1148
|
+
if (!life.mounted || failure?.name === "AbortError") return;
|
|
1149
|
+
if (failure?.updateState && typeof failure.updateState === "object") {
|
|
1150
|
+
life.snapshot = failure.updateState;
|
|
1151
|
+
setUpdate(failure.updateState);
|
|
1152
|
+
}
|
|
1153
|
+
setError({
|
|
1154
|
+
failure,
|
|
1155
|
+
fallback: kind === "save" ? "updateSaveFailed" : kind === "download" ? "updateDownloadFailed" : "updateCheckFailed",
|
|
1156
|
+
source: kind,
|
|
1157
|
+
});
|
|
1158
|
+
} finally {
|
|
1159
|
+
life.controllers.delete(controller);
|
|
1160
|
+
life.action = false;
|
|
1161
|
+
if (life.mounted) setBusy(null);
|
|
1162
|
+
}
|
|
1163
|
+
}, []);
|
|
1164
|
+
|
|
1165
|
+
const working = loading || busy !== null || update?.checking === true || update?.downloading === true;
|
|
1166
|
+
const blockedReason = update?.blockedReason;
|
|
1167
|
+
const downloadBlocked = !!blockedReason && blockedReason !== "linked-install";
|
|
1168
|
+
const blockedKey = blockedReason === "linked-install"
|
|
1169
|
+
? "updateLinkedHint"
|
|
1170
|
+
: blockedReason === "unsupported-host"
|
|
1171
|
+
? "updateUnsupportedHint"
|
|
1172
|
+
: blockedReason === "not-installed"
|
|
1173
|
+
? "updateNotInstalledHint"
|
|
1174
|
+
: "updateBlockedHint";
|
|
1175
|
+
const canDownload = !!update?.available && typeof update.latestVersion === "string" && !!update.latestVersion && !downloadBlocked;
|
|
1176
|
+
const downloading = busy === "download" || update?.downloading === true;
|
|
1177
|
+
const checking = busy === "check" || update?.checking === true;
|
|
1178
|
+
const availableVersion = update?.latestVersion || t("updateNotChecked");
|
|
1179
|
+
const versionSummary = t("updateVersionSummary")
|
|
1180
|
+
.replace("{current}", update?.currentVersion || t("updateUnknownVersion"))
|
|
1181
|
+
.replace("{latest}", availableVersion);
|
|
1182
|
+
const statusError = update?.error || update?.errorCode
|
|
1183
|
+
? { updateCode: update.errorCode }
|
|
1184
|
+
: null;
|
|
1185
|
+
const compactState = loading ? "loading" : downloading ? "downloading" : checking ? "checking" :
|
|
1186
|
+
error || statusError ? "error" : update?.downloadedVersion && update.downloadedVersion === update.latestVersion ? "downloaded" :
|
|
1187
|
+
update?.available ? "available" : "idle";
|
|
1188
|
+
// Name the actual failure instead of a generic "update error": the
|
|
1189
|
+
// status endpoint missing (host not reloaded) reads very differently
|
|
1190
|
+
// from a registry that could not be reached.
|
|
1191
|
+
const failureCode = statusError?.updateCode ?? update?.errorCode ?? null;
|
|
1192
|
+
const compactErrorKey = error
|
|
1193
|
+
? error.source === "status" ? "updateCompactStatusUnavailable" : "updateCompactError"
|
|
1194
|
+
: failureCode === "network-error" ? "updateCompactErrorNetwork"
|
|
1195
|
+
: failureCode === "channel-unavailable" ? "updateCompactErrorChannel"
|
|
1196
|
+
: "updateCompactError";
|
|
1197
|
+
const compactText = loading ? t("updateLoading") : downloading ? t("updateDownloading") : checking ? t("updateChecking") :
|
|
1198
|
+
error || statusError ? t(compactErrorKey) : compactState === "downloaded"
|
|
1199
|
+
? t("updateCompactDownloaded").replace("{version}", update.downloadedVersion)
|
|
1200
|
+
: update?.available ? t("updateCompactAvailable").replace("{version}", availableVersion)
|
|
1201
|
+
: update?.latestVersion ? t("updateUpToDate") : t("updateNotChecked");
|
|
1202
|
+
let checkedAt = t("updateNotChecked");
|
|
1203
|
+
if (update?.lastCheckedAt) {
|
|
1204
|
+
const date = new Date(update.lastCheckedAt);
|
|
1205
|
+
if (Number.isFinite(date.getTime())) checkedAt = date.toLocaleString();
|
|
1206
|
+
}
|
|
1207
|
+
const regionName = (region) => t(region === "cn" ? "updateRegionCn" : "updateRegionGlobal");
|
|
1208
|
+
const segmented = (field, label, options, selected) =>
|
|
1209
|
+
h(
|
|
1210
|
+
"div",
|
|
1211
|
+
{ className: "dcpa-seg", role: "group", "aria-label": t(label) },
|
|
1212
|
+
options.map(([value, key]) =>
|
|
1213
|
+
h("button", {
|
|
1214
|
+
key: value,
|
|
1215
|
+
type: "button",
|
|
1216
|
+
className: "dcpa-seg-btn",
|
|
1217
|
+
disabled: working || !update,
|
|
1218
|
+
"aria-pressed": selected === value,
|
|
1219
|
+
"data-active": selected === value ? "true" : "false",
|
|
1220
|
+
onClick: () => { if (selected !== value) void runAction("save", { [field]: value }); },
|
|
1221
|
+
}, t(key))
|
|
1222
|
+
)
|
|
1223
|
+
);
|
|
1224
|
+
const toggle = (field, label, hint, checked) =>
|
|
1225
|
+
h(
|
|
1226
|
+
"div",
|
|
1227
|
+
{ className: "dcpa-update-row dcpa-switch-row" },
|
|
1228
|
+
h("div", { className: "dcpa-field" },
|
|
1229
|
+
h("span", { className: "dcpa-label" }, t(label)),
|
|
1230
|
+
h("p", { className: "dcpa-hint" }, t(hint))
|
|
1231
|
+
),
|
|
1232
|
+
h("button", {
|
|
1233
|
+
type: "button",
|
|
1234
|
+
role: "switch",
|
|
1235
|
+
className: "dcpa-switch",
|
|
1236
|
+
"aria-label": t(label),
|
|
1237
|
+
"aria-checked": checked,
|
|
1238
|
+
disabled: working || !update,
|
|
1239
|
+
onClick: () => { void runAction("save", { [field]: !checked }); },
|
|
1240
|
+
}, h("span", { className: "dcpa-switch-thumb" }))
|
|
1241
|
+
);
|
|
1242
|
+
|
|
1243
|
+
// Native details starts closed. Do not control `open`: polling and saves
|
|
1244
|
+
// must preserve the user's expanded/collapsed choice without remounting.
|
|
1245
|
+
return h(
|
|
1246
|
+
"details",
|
|
1247
|
+
{ className: "dcpa-card dcpa-update-card", "aria-label": t("updateTitle"), "aria-busy": working },
|
|
1248
|
+
h("summary", { className: "dcpa-update-summary", title: t("updateToggleDetails") },
|
|
1249
|
+
h("span", { className: "dcpa-update-summary-title" }, t("updateTitle")),
|
|
1250
|
+
h("span", { className: "dcpa-update-summary-current" },
|
|
1251
|
+
t("updateCompactCurrent").replace("{version}", update?.currentVersion || t("updateUnknownVersion"))),
|
|
1252
|
+
h("span", { className: "dcpa-update-summary-status", "data-state": compactState, role: "status", "aria-live": "polite" }, compactText)
|
|
1253
|
+
),
|
|
1254
|
+
h("div", { className: "dcpa-update-content" },
|
|
1255
|
+
h("p", { className: "dcpa-subtitle" }, t("updateSubtitle")),
|
|
1256
|
+
h("div", { className: "dcpa-update-row" },
|
|
1257
|
+
h("div", { className: "dcpa-field" },
|
|
1258
|
+
h("span", { className: "dcpa-label" }, t("updateNewVersion")),
|
|
1259
|
+
h("span", { className: "dcpa-update-version", role: "status", "aria-live": "polite" },
|
|
1260
|
+
loading ? t("updateLoading") : downloading ? t("updateDownloading") : checking ? t("updateChecking") :
|
|
1261
|
+
update?.available ? availableVersion : update?.latestVersion ? t("updateUpToDate") : t("updateNotChecked")
|
|
1262
|
+
),
|
|
1263
|
+
h("p", { className: "dcpa-hint" }, versionSummary),
|
|
1264
|
+
update?.available ? h("span", { className: "dcpa-hint" }, t("updateAvailable")) : null
|
|
1265
|
+
),
|
|
1266
|
+
h("div", { className: "dcpa-actions" },
|
|
1267
|
+
h("button", {
|
|
1268
|
+
type: "button",
|
|
1269
|
+
className: "dcpa-btn",
|
|
1270
|
+
disabled: working,
|
|
1271
|
+
onClick: () => { if (update) void runAction("check"); else { setLoading(true); void loadStatus(); } },
|
|
1272
|
+
}, checking ? t("updateChecking") : !update && !loading ? t("updateRetryBtn") : t("updateCheckBtn")),
|
|
1273
|
+
h("button", {
|
|
1274
|
+
type: "button",
|
|
1275
|
+
className: "dcpa-btn dcpa-btn-primary",
|
|
1276
|
+
disabled: working || !canDownload,
|
|
1277
|
+
onClick: () => setConfirmVersion(update.latestVersion),
|
|
1278
|
+
}, downloading ? t("updateDownloading") : update?.downloadedVersion === update?.latestVersion && update?.downloadedVersion
|
|
1279
|
+
? t("updateDownloadedBtn") : t("updateDownloadBtn"))
|
|
1280
|
+
)
|
|
1281
|
+
),
|
|
1282
|
+
confirmVersion ? h("div", {
|
|
1283
|
+
className: "dcpa-update-notice",
|
|
1284
|
+
role: "alertdialog",
|
|
1285
|
+
"aria-modal": false,
|
|
1286
|
+
"aria-label": t("updateConfirmTitle").replace("{version}", confirmVersion),
|
|
1287
|
+
},
|
|
1288
|
+
h("p", { className: "dcpa-label" }, t("updateConfirmTitle").replace("{version}", confirmVersion)),
|
|
1289
|
+
h("p", { className: "dcpa-hint" }, t(blockedReason === "linked-install" ? "updateLinkedConfirmHint" : "updateConfirmHint")),
|
|
1290
|
+
h("div", { className: "dcpa-actions" },
|
|
1291
|
+
h("button", {
|
|
1292
|
+
type: "button",
|
|
1293
|
+
className: "dcpa-btn dcpa-btn-primary",
|
|
1294
|
+
disabled: working || !canDownload || confirmVersion !== update?.latestVersion,
|
|
1295
|
+
onClick: () => { void runAction("download", { version: confirmVersion }); },
|
|
1296
|
+
}, t("updateConfirmBtn")),
|
|
1297
|
+
h("button", { type: "button", className: "dcpa-btn", onClick: () => setConfirmVersion(null) }, t("updateCancelBtn"))
|
|
1298
|
+
),
|
|
1299
|
+
confirmVersion !== update?.latestVersion ? h("p", { className: "dcpa-hint" }, t("updateStaleVersion")) : null
|
|
1300
|
+
) : null,
|
|
1301
|
+
h("div", { className: "dcpa-update-row" },
|
|
1302
|
+
h("div", { className: "dcpa-field" },
|
|
1303
|
+
h("span", { className: "dcpa-label" }, t("updateChannelLabel")),
|
|
1304
|
+
h("p", { className: "dcpa-hint" }, t("updateChannelHint"))
|
|
1305
|
+
),
|
|
1306
|
+
segmented("channel", "updateChannelLabel", [
|
|
1307
|
+
["stable", "updateChannelStable"], ["beta", "updateChannelBeta"], ["dev", "updateChannelDev"],
|
|
1308
|
+
], update?.channel || "stable")
|
|
1309
|
+
),
|
|
1310
|
+
h("div", { className: "dcpa-update-row" },
|
|
1311
|
+
h("div", { className: "dcpa-field" },
|
|
1312
|
+
h("span", { className: "dcpa-label" }, t("updateRegionLabel")),
|
|
1313
|
+
h("p", { className: "dcpa-hint" }, t("updateRegionHint")),
|
|
1314
|
+
update?.effectiveRegion ? h("p", { className: "dcpa-hint" }, t("updateEffectiveRegion").replace("{region}", regionName(update.effectiveRegion))) : null
|
|
1315
|
+
),
|
|
1316
|
+
segmented("region", "updateRegionLabel", [
|
|
1317
|
+
["auto", "updateRegionAuto"], ["global", "updateRegionGlobal"], ["cn", "updateRegionCn"],
|
|
1318
|
+
], update?.region || "auto")
|
|
1319
|
+
),
|
|
1320
|
+
toggle("autoCheck", "updateAutoCheckLabel", "updateAutoCheckHint", update?.autoCheck !== false),
|
|
1321
|
+
toggle("autoDownload", "updateAutoDownloadLabel", "updateAutoDownloadHint", update?.autoDownload === true),
|
|
1322
|
+
blockedReason ? h("div", { className: "dcpa-update-notice" }, h("p", { className: "dcpa-hint" }, t(blockedKey))) : null,
|
|
1323
|
+
update?.downloadedVersion && update?.downloadPath ? h("div", { className: "dcpa-update-notice", role: "status" },
|
|
1324
|
+
h("p", { className: "dcpa-label" }, t(blockedReason === "linked-install" ? "updateLinkedDownloadedHint" : "updateDownloadedHint").replace("{version}", update.downloadedVersion)),
|
|
1325
|
+
h("span", { className: "dcpa-label" }, t("updateDownloadPathLabel")),
|
|
1326
|
+
h("code", { className: "dcpa-update-code" }, update.downloadPath),
|
|
1327
|
+
blockedReason !== "linked-install" && update.upgradeCommand ? h(React.Fragment, null,
|
|
1328
|
+
h("span", { className: "dcpa-label" }, t("updateUpgradeCommandLabel")),
|
|
1329
|
+
h("pre", { className: "dcpa-update-code" }, h("code", null, update.upgradeCommand))
|
|
1330
|
+
) : h("p", { className: "dcpa-hint" }, t(blockedReason === "linked-install" ? "updateLinkedSourceHint" : "updateCommandUnavailable"))
|
|
1331
|
+
) : null,
|
|
1332
|
+
h("div", { className: "dcpa-update-footer" },
|
|
1333
|
+
error ? h("p", { className: "dcpa-error", role: "alert" }, updateErrorText(t, error.failure, error.fallback)) :
|
|
1334
|
+
statusError ? h("p", { className: "dcpa-error", role: "alert" }, updateErrorText(t, statusError, "updateCheckFailed")) : null,
|
|
1335
|
+
error?.source === "status" ? h("p", { className: "dcpa-hint", role: "note" }, t("updateStatusUnavailableHint")) : null,
|
|
1336
|
+
busy === "save" ? h("p", { className: "dcpa-hint", role: "status" }, t("updateSaving")) :
|
|
1337
|
+
notice ? h("p", { className: "dcpa-success", role: "status" }, t(notice)) : null,
|
|
1338
|
+
h("p", { className: "dcpa-hint" }, t("updateLastChecked").replace("{time}", checkedAt)),
|
|
1339
|
+
h("p", { className: "dcpa-hint" }, t("updateOfflineHint"))
|
|
1340
|
+
)
|
|
1341
|
+
)
|
|
1342
|
+
);
|
|
1343
|
+
}
|
|
1344
|
+
|
|
708
1345
|
/**
|
|
709
1346
|
* Account list + switcher. Each account is edited in its own
|
|
710
1347
|
* {@link AccountPanel}, remounted when the selection changes.
|
|
@@ -859,6 +1496,7 @@ window.__ModuleLoader__.load({
|
|
|
859
1496
|
error ? h("p", { className: "dcpa-error", role: "alert" }, error) : null,
|
|
860
1497
|
toast ? h("p", { className: "dcpa-success", role: "status" }, toast) : null
|
|
861
1498
|
),
|
|
1499
|
+
h(PluginUpdatePanel, { t }),
|
|
862
1500
|
active
|
|
863
1501
|
? h(AccountPanel, {
|
|
864
1502
|
key: active.id,
|