dsh-plugin-notify-sounds 1.8.4 → 1.8.6
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/README.md +17 -12
- package/README.zh.md +17 -12
- package/lib/client.js +210 -196
- package/lib/index.js +1080 -1552
- package/package.json +2 -2
package/lib/client.js
CHANGED
|
@@ -185,6 +185,7 @@ window.__ModuleLoader__.load({
|
|
|
185
185
|
"custom.remove": "移除",
|
|
186
186
|
"custom.tooLarge": "文件超过 512KB,请压缩后重试",
|
|
187
187
|
"custom.undecodable": "无法解码该音频:文件可能损坏,或当前环境不支持解码",
|
|
188
|
+
"custom.unreadable": "无法读取该文件:它可能已被移动,或读取权限被撤销,请重新选择",
|
|
188
189
|
"systemNotify.title": "系统通知",
|
|
189
190
|
"systemNotify.description": "页面处于后台时弹出系统通知,点击可回到窗口",
|
|
190
191
|
"systemNotify.granted": "已授权系统通知",
|
|
@@ -201,7 +202,7 @@ window.__ModuleLoader__.load({
|
|
|
201
202
|
"renotify.interval": "重复间隔",
|
|
202
203
|
"renotify.minutes": "{n} 分钟",
|
|
203
204
|
"quiet.title": "静音时段",
|
|
204
|
-
"quiet.description": "每天此时间段内不发出任何提醒(可跨午夜,如 22:00 至次日 08:00
|
|
205
|
+
"quiet.description": "每天此时间段内不发出任何提醒(可跨午夜,如 22:00 至次日 08:00;起止时间相同视为未设置静音)",
|
|
205
206
|
"quiet.start": "开始时间",
|
|
206
207
|
"quiet.end": "结束时间",
|
|
207
208
|
"preview": "试听",
|
|
@@ -222,32 +223,23 @@ window.__ModuleLoader__.load({
|
|
|
222
223
|
"notify.completion.title": "任务完成",
|
|
223
224
|
"notify.completion.body": "智能体已完成当前任务",
|
|
224
225
|
"update.title": "更新",
|
|
225
|
-
"update.description": "从 npm
|
|
226
|
+
"update.description": "从 npm 检查本插件的新版本;安装请用插件管理器(本插件不再自己安装更新)",
|
|
226
227
|
"update.auto": "自动检查更新",
|
|
227
228
|
"update.auto.description": "每天最多静默检查一次,发现新版只在下方标记,不弹通知",
|
|
228
229
|
"update.check": "检查更新",
|
|
229
230
|
"update.current": "当前版本",
|
|
231
|
+
"update.currentUnknown": "无法读取当前版本",
|
|
230
232
|
"update.idle": "尚未检查",
|
|
231
233
|
"update.checking": "检查中…",
|
|
232
234
|
"update.uptodate": "已是最新",
|
|
233
235
|
"update.available": "有新版",
|
|
234
|
-
"update.failed": "
|
|
235
|
-
"update.
|
|
236
|
-
"update.desktopGuidance": "
|
|
236
|
+
"update.failed": "检查失败",
|
|
237
|
+
"update.failedDetail": "两个源都没有给出可用的答复:可能是网络、代理或镜像源临时不可用,稍后重试即可",
|
|
238
|
+
"update.desktopGuidance": "升级走包管理器:左侧栏「插件」→ 在「已安装插件」里卸载本插件 → 点「添加插件」→ 粘贴 dsh-plugin-notify-sounds@{version} → 安装 → 重启 DSH",
|
|
239
|
+
"update.versionPinned": "务必带版本号:发布不满 24 小时的版本会被 pnpm 的 minimumReleaseAge 策略挡掉,只写包名会装回上一个版本",
|
|
237
240
|
"update.webGuidance": "在终端执行 dsh plugin --profile {profile} add dsh-plugin-notify-sounds@{version}",
|
|
238
|
-
"update.
|
|
239
|
-
"update.
|
|
240
|
-
"update.doneDetail": "更新完成:{from} → {to},重启 DSH 后生效",
|
|
241
|
-
"update.applyFailed": "更新失败({stage}),已恢复原状",
|
|
242
|
-
"update.stage.check": "检查",
|
|
243
|
-
"update.stage.download": "下载",
|
|
244
|
-
"update.stage.verify": "校验",
|
|
245
|
-
"update.stage.unpack": "解包",
|
|
246
|
-
"update.stage.manifest": "核对包信息",
|
|
247
|
-
"update.stage.replace": "替换文件",
|
|
248
|
-
"update.stage.unknown": "未知阶段",
|
|
249
|
-
"update.retry": "通常是网络或代理的临时问题,稍后重试即可;若反复失败,可到 {url} 手动安装",
|
|
250
|
-
"update.reinstall": "可在 DSH 的 设置 → 插件 里先卸载本插件,再添加 dsh-plugin-notify-sounds 重新安装;仓库:{url}",
|
|
241
|
+
"update.webGuidancePlain": "在终端执行 dsh plugin add dsh-plugin-notify-sounds@{version}",
|
|
242
|
+
"update.unregistered": "这个安装没有被包管理器登记(记录仍为 {recorded}),下次安装其他插件时可能被退回。请按下面的步骤重新安装一次。",
|
|
251
243
|
};
|
|
252
244
|
|
|
253
245
|
/** English dictionary, checked complete against the zh key set. */
|
|
@@ -275,6 +267,7 @@ window.__ModuleLoader__.load({
|
|
|
275
267
|
"custom.remove": "Remove",
|
|
276
268
|
"custom.tooLarge": "File exceeds 512KB; please compress it and retry",
|
|
277
269
|
"custom.undecodable": "Could not decode this audio: the file may be damaged, or this environment cannot decode audio",
|
|
270
|
+
"custom.unreadable": "Could not read this file: it may have been moved, or read permission was revoked — choose it again",
|
|
278
271
|
"systemNotify.title": "System notifications",
|
|
279
272
|
"systemNotify.description": "Show an OS notification while the page is hidden; click it to focus the window",
|
|
280
273
|
"systemNotify.granted": "Notification permission granted",
|
|
@@ -293,7 +286,7 @@ window.__ModuleLoader__.load({
|
|
|
293
286
|
"quiet.title": "Quiet hours",
|
|
294
287
|
"goalFinalOnly.title": "Only alert on the final goal round",
|
|
295
288
|
"goalFinalOnly.description": "A goal runs several automatic rounds; with this on, intermediate rounds stay silent and one chime marks the end of the goal (or a pause or block). Ordinary sessions are unaffected",
|
|
296
|
-
"quiet.description": "No alerts during this daily time range (may wrap past midnight, e.g. 22:00 to 08:00)",
|
|
289
|
+
"quiet.description": "No alerts during this daily time range (may wrap past midnight, e.g. 22:00 to 08:00; identical start and end means no quiet range)",
|
|
297
290
|
"quiet.start": "Start time",
|
|
298
291
|
"quiet.end": "End time",
|
|
299
292
|
"preview": "Preview",
|
|
@@ -314,32 +307,23 @@ window.__ModuleLoader__.load({
|
|
|
314
307
|
"notify.completion.title": "Task finished",
|
|
315
308
|
"notify.completion.body": "The agent has finished the current task",
|
|
316
309
|
"update.title": "Updates",
|
|
317
|
-
"update.description": "Check npm for a newer version
|
|
310
|
+
"update.description": "Check npm for a newer version; install it through the plugin manager (this plugin no longer installs updates itself)",
|
|
318
311
|
"update.auto": "Check for updates automatically",
|
|
319
312
|
"update.auto.description": "At most one quiet check a day; a newer version is only marked below, never announced",
|
|
320
313
|
"update.check": "Check now",
|
|
321
314
|
"update.current": "This version",
|
|
315
|
+
"update.currentUnknown": "This version could not be read",
|
|
322
316
|
"update.idle": "Not checked yet",
|
|
323
317
|
"update.checking": "Checking…",
|
|
324
318
|
"update.uptodate": "Up to date",
|
|
325
319
|
"update.available": "New version available",
|
|
326
|
-
"update.failed": "Check failed
|
|
327
|
-
"update.
|
|
328
|
-
"update.desktopGuidance": "Update through the package manager:
|
|
320
|
+
"update.failed": "Check failed",
|
|
321
|
+
"update.failedDetail": "Neither source returned a usable answer — the network, a proxy or a mirror may be temporarily unavailable. Try again later.",
|
|
322
|
+
"update.desktopGuidance": "Update through the package manager: sidebar Plugins → uninstall this plugin under Installed plugins → Add plugin → paste dsh-plugin-notify-sounds@{version} → Install → restart DSH",
|
|
323
|
+
"update.versionPinned": "Keep the version: pnpm's minimumReleaseAge policy blocks a release for its first 24 hours, so the bare name resolves to the previous version",
|
|
329
324
|
"update.webGuidance": "Run dsh plugin --profile {profile} add dsh-plugin-notify-sounds@{version} in a terminal",
|
|
330
|
-
"update.
|
|
331
|
-
"update.
|
|
332
|
-
"update.doneDetail": "Updated: {from} → {to} — restart DSH to load it",
|
|
333
|
-
"update.applyFailed": "Update failed ({stage}) — the previous version is intact",
|
|
334
|
-
"update.stage.check": "check",
|
|
335
|
-
"update.stage.download": "download",
|
|
336
|
-
"update.stage.verify": "checksum",
|
|
337
|
-
"update.stage.unpack": "unpack",
|
|
338
|
-
"update.stage.manifest": "package check",
|
|
339
|
-
"update.stage.replace": "file replacement",
|
|
340
|
-
"update.stage.unknown": "unknown step",
|
|
341
|
-
"update.retry": "This is usually a temporary network or proxy problem — try again later. If it keeps failing, install manually from {url}",
|
|
342
|
-
"update.reinstall": "Remove this plugin under Settings → Plugins, then add dsh-plugin-notify-sounds again. Repository: {url}",
|
|
325
|
+
"update.webGuidancePlain": "Run dsh plugin add dsh-plugin-notify-sounds@{version} in a terminal",
|
|
326
|
+
"update.unregistered": "This install is not registered with the package manager (it still records {recorded}), so a later plugin install may roll it back. Reinstall it once with the steps below.",
|
|
343
327
|
};
|
|
344
328
|
//#endregion
|
|
345
329
|
|
|
@@ -801,15 +785,23 @@ window.__ModuleLoader__.load({
|
|
|
801
785
|
/** Active title observer, while a title alert is wanted. */
|
|
802
786
|
let titleObserver;
|
|
803
787
|
|
|
804
|
-
/**
|
|
788
|
+
/**
|
|
789
|
+
* Parse "HH:MM" into minutes past midnight; `NaN` when malformed.
|
|
790
|
+
*
|
|
791
|
+
* Deliberately as strict as the Host's `TIME_OF_DAY` pattern: two-digit
|
|
792
|
+
* hours, two-digit minutes. A hand-edited `9:05` is refused by the
|
|
793
|
+
* Config schema, so accepting it here would mean the row claims a value
|
|
794
|
+
* is fine while the plugin itself declines to load — one field, two
|
|
795
|
+
* verdicts.
|
|
796
|
+
*
|
|
797
|
+
* @param text - the stored value.
|
|
798
|
+
* @returns minutes past midnight, or NaN.
|
|
799
|
+
*/
|
|
805
800
|
function timeOfDayToMinutes(text) {
|
|
806
801
|
if (typeof text !== "string") return NaN;
|
|
807
|
-
const match = /^(\d
|
|
802
|
+
const match = /^([01]\d|2[0-3]):([0-5]\d)$/.exec(text);
|
|
808
803
|
if (match === null) return NaN;
|
|
809
|
-
|
|
810
|
-
const minutes = Number(match[2]);
|
|
811
|
-
if (hours > 23 || minutes > 59) return NaN;
|
|
812
|
-
return hours * 60 + minutes;
|
|
804
|
+
return Number(match[1]) * 60 + Number(match[2]);
|
|
813
805
|
}
|
|
814
806
|
|
|
815
807
|
/**
|
|
@@ -874,64 +866,47 @@ window.__ModuleLoader__.load({
|
|
|
874
866
|
/** Route the Host half answers the update check on; must match the Host half's UPDATE_CHECK_PATH. */
|
|
875
867
|
const UPDATE_CHECK_PATH = "/notify-sounds/update/check";
|
|
876
868
|
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
* Where a user goes when the one-click update cannot finish.
|
|
880
|
-
*
|
|
881
|
-
* Shown as plain selectable text rather than a link: the Desktop shell
|
|
882
|
-
* is a `dsh-app://` page and the Settings primitives expose no external
|
|
883
|
-
* link component, so a URL the user can copy is the honest form — a
|
|
884
|
-
* button that silently did nothing here would be worse than no button.
|
|
885
|
-
*/
|
|
886
|
-
const REPOSITORY_URL = "https://github.com/BillCx330/dsh-plugin-notify-sounds";
|
|
887
|
-
|
|
888
|
-
/**
|
|
889
|
-
* Every stage the Host can report, and whether a fresh install is the remedy.
|
|
890
|
-
*
|
|
891
|
-
* Keyed off the stage rather than off the message: telling someone whose
|
|
892
|
-
* download timed out to uninstall the plugin would be actively bad
|
|
893
|
-
* advice, and telling someone whose archive was rejected to "try again
|
|
894
|
-
* later" leaves them with no way forward at all. `true` means the
|
|
895
|
-
* payload itself could not be applied, which retrying will not change.
|
|
896
|
-
*/
|
|
897
|
-
const UPDATE_STAGES = { check: false, download: false, verify: false, unpack: true, manifest: true, replace: true };
|
|
898
|
-
|
|
899
|
-
/** The localized word for a Host-side update stage. */
|
|
900
|
-
const stageWord = (t, stage) => t(`update.stage.${typeof stage === "string" && Object.hasOwn(UPDATE_STAGES, stage) ? stage : "unknown"}`);
|
|
869
|
+
/** Route the Host half answers a session's goal phase on; must match the Host half's GOAL_PHASE_PATH. */
|
|
870
|
+
const GOAL_PHASE_PATH = "/notify-sounds/goal";
|
|
901
871
|
|
|
902
872
|
/** `v1.2.3` for display, `?` when the Host reported no version. */
|
|
903
873
|
const formatVersion = (text) => (typeof text === "string" && text !== "" ? `v${text}` : "?");
|
|
904
874
|
|
|
905
875
|
/**
|
|
906
|
-
* The
|
|
876
|
+
* The version as a package specifier segment: `1.2.3`, never `v1.2.3`.
|
|
907
877
|
*
|
|
908
|
-
*
|
|
909
|
-
*
|
|
910
|
-
*
|
|
911
|
-
*
|
|
912
|
-
*
|
|
878
|
+
* The display form and the installable form are deliberately different.
|
|
879
|
+
* Every install instruction this package prints is pasted into a package
|
|
880
|
+
* manager, and `pkg@v1.2.3` only works because semver tolerates the
|
|
881
|
+
* prefix — not because anything promises it. A command that fails to
|
|
882
|
+
* parse is worse than a slightly plainer one.
|
|
913
883
|
*/
|
|
914
|
-
const
|
|
884
|
+
const packageVersion = (text) => (typeof text === "string" && text !== "" ? text : "");
|
|
915
885
|
|
|
916
886
|
/**
|
|
917
|
-
* The
|
|
887
|
+
* The version an install instruction should name, or undefined when
|
|
888
|
+
* there is nothing to install.
|
|
889
|
+
*
|
|
890
|
+
* Only these two states need an instruction, and both have to name a
|
|
891
|
+
* version or the command runs into `minimumReleaseAge` and quietly
|
|
892
|
+
* installs the previous release:
|
|
918
893
|
*
|
|
919
|
-
*
|
|
920
|
-
*
|
|
921
|
-
*
|
|
922
|
-
*
|
|
894
|
+
* - a newer version exists → install that one;
|
|
895
|
+
* - the install is not recorded by the profile's lockfile → install
|
|
896
|
+
* the version already on disk, so the record catches up.
|
|
897
|
+
*
|
|
898
|
+
* When both hold, the newer version is the target: reinstalling it
|
|
899
|
+
* fixes the record at the same time. A plain "up to date" install
|
|
900
|
+
* yields undefined, so the row stays quiet.
|
|
901
|
+
*
|
|
902
|
+
* @param payload - the Host's answer, or undefined before one arrives.
|
|
903
|
+
* @param currentKnown - whether `payload.current` is a usable version.
|
|
904
|
+
* @returns the version string, as a package specifier segment.
|
|
923
905
|
*/
|
|
924
|
-
const
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
const named = typeof failed?.to === "string" ? `${formatVersion(current)} → ${formatVersion(failed.to)} · ` : "";
|
|
929
|
-
return {
|
|
930
|
-
detail: `${named}${t("update.applyFailed", { stage: stageWord(t, failed?.stage) })}`,
|
|
931
|
-
advice: UPDATE_STAGES[failed?.stage] === true
|
|
932
|
-
? t("update.reinstall", { url: REPOSITORY_URL })
|
|
933
|
-
: t("update.retry", { url: REPOSITORY_URL }),
|
|
934
|
-
};
|
|
906
|
+
const installTargetVersion = (payload, currentKnown) => {
|
|
907
|
+
if (payload?.updateAvailable === true && typeof payload.latest === "string" && payload.latest !== "") return payload.latest;
|
|
908
|
+
if (payload?.unregistered === true && currentKnown && typeof payload.current === "string" && payload.current !== "") return payload.current;
|
|
909
|
+
return undefined;
|
|
935
910
|
};
|
|
936
911
|
|
|
937
912
|
/** Channel name and claim key shared by every page of this origin. */
|
|
@@ -1073,7 +1048,9 @@ window.__ModuleLoader__.load({
|
|
|
1073
1048
|
// to collapse repeats means the platform closes the toast that
|
|
1074
1049
|
// is being superseded — and a click that arrives for that older
|
|
1075
1050
|
// toast can be dropped with it, which is one whole class of
|
|
1076
|
-
// "clicked the notification and nothing happened".
|
|
1051
|
+
// "clicked the notification and nothing happened". The tag is
|
|
1052
|
+
// only our own key for closing the previous generation, which
|
|
1053
|
+
// is why it is not passed to the constructor below either.
|
|
1077
1054
|
const previous = liveNotifications.get(tag);
|
|
1078
1055
|
if (previous !== undefined) {
|
|
1079
1056
|
liveNotifications.delete(tag);
|
|
@@ -1081,13 +1058,12 @@ window.__ModuleLoader__.load({
|
|
|
1081
1058
|
previous.close();
|
|
1082
1059
|
} catch {}
|
|
1083
1060
|
}
|
|
1084
|
-
// `tag` is deliberately NOT passed to the platform. Collapsing
|
|
1085
|
-
// repeats by tag means the platform supersedes the previous
|
|
1086
|
-
// toast — and a click aimed at the superseded toast can be
|
|
1087
|
-
// dropped with it, which is the whole "clicked the
|
|
1088
|
-
// notification and nothing happened" class of failure. The tag
|
|
1089
|
-
// is only our own key for closing the previous generation.
|
|
1090
1061
|
const notification = new Notification(title, { body, silent: true });
|
|
1062
|
+
// The constructor worked, so this environment can build a toast
|
|
1063
|
+
// after all. Clearing the flag matters: it is what disables the
|
|
1064
|
+
// row (see SystemNotifyRow), and one transient refusal must not
|
|
1065
|
+
// leave the switch dead for the life of the page.
|
|
1066
|
+
notificationConstructionFailed = false;
|
|
1091
1067
|
liveNotifications.set(tag, notification);
|
|
1092
1068
|
// Held here — and deliberately NOT released on click or close.
|
|
1093
1069
|
// Releasing it was what left a replaced (or twice-clicked) toast
|
|
@@ -1175,50 +1151,13 @@ window.__ModuleLoader__.load({
|
|
|
1175
1151
|
|
|
1176
1152
|
//#region settings rows
|
|
1177
1153
|
/**
|
|
1178
|
-
* The
|
|
1179
|
-
*
|
|
1154
|
+
* The Settings section: the row list, the write helper every row shares,
|
|
1155
|
+
* and the "this field was refused" marker they render.
|
|
1180
1156
|
*
|
|
1181
1157
|
* @param props - the form snapshot hook, the field writer, the preview
|
|
1182
1158
|
* player, and the locale seat.
|
|
1183
1159
|
* @returns the Settings section element tree.
|
|
1184
1160
|
*/
|
|
1185
|
-
/**
|
|
1186
|
-
* The one-line notice directly under the section intro: what the last
|
|
1187
|
-
* update did, shown where the page lands.
|
|
1188
|
-
*
|
|
1189
|
-
* An update swaps the plugin's own client file, so DSH reloads this
|
|
1190
|
-
* half and the section is rebuilt from the top — anything anchored at
|
|
1191
|
-
* the bottom of the row list is somewhere the user then has to scroll
|
|
1192
|
-
* to find, and the scroll itself makes the page look like it jumped.
|
|
1193
|
-
* The outcome therefore lives on the Host (it survives the reload) and
|
|
1194
|
-
* this notice repeats it at the top, without moving anything, until
|
|
1195
|
-
* DSH restarts — which is exactly as long as "restart to load it" is
|
|
1196
|
-
* true. Nothing is shown otherwise, so the section is unchanged for
|
|
1197
|
-
* everyone who did not just update.
|
|
1198
|
-
*
|
|
1199
|
-
* @param props - copy.
|
|
1200
|
-
* @returns the notice element, or null when there is nothing to say.
|
|
1201
|
-
*/
|
|
1202
|
-
function UpdateNotice({ t, autoCheck }) {
|
|
1203
|
-
const [applied, setApplied] = useState(undefined);
|
|
1204
|
-
// Explicit `=== true`, not `!== false`: during the first frame
|
|
1205
|
-
// `useConfig` may still be loading, and `undefined` must NOT be
|
|
1206
|
-
// treated as "allowed" — that is the one case where a request
|
|
1207
|
-
// could slip past a user who has the switch off. The dep array is
|
|
1208
|
-
// `[autoCheck]` rather than `[]` so the effect picks up the real
|
|
1209
|
-
// value when it arrives, instead of only ever reading the first
|
|
1210
|
-
// render's prop.
|
|
1211
|
-
useEffect(() => {
|
|
1212
|
-
if (autoCheck !== true) return;
|
|
1213
|
-
fetch(UPDATE_CHECK_PATH, { method: "GET" })
|
|
1214
|
-
.then((response) => response.json())
|
|
1215
|
-
.then((payload) => setApplied(payload?.applied))
|
|
1216
|
-
.catch(() => {});
|
|
1217
|
-
}, [autoCheck]);
|
|
1218
|
-
if (applied === undefined || typeof applied.to !== "string") return null;
|
|
1219
|
-
return h("div", { className: css.hint, key: "text" }, updateAppliedText(t, applied));
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
1161
|
function NotifySoundsSection({ useConfig, setField, preview, t }) {
|
|
1223
1162
|
const state = useConfig((snapshot) => snapshot);
|
|
1224
1163
|
const value = state.status === "ready" ? state.value : undefined;
|
|
@@ -1252,22 +1191,16 @@ window.__ModuleLoader__.load({
|
|
|
1252
1191
|
const pickerIds = hasCustom ? SOUND_IDS : SOUND_IDS.filter((id) => id !== "custom");
|
|
1253
1192
|
/** Accept a stored preset the menu actually offers, else fall back. */
|
|
1254
1193
|
const soundValue = (raw, fallback) => (typeof raw === "string" && pickerIds.includes(raw) ? raw : fallback);
|
|
1255
|
-
//
|
|
1256
|
-
//
|
|
1257
|
-
//
|
|
1258
|
-
const renotifyMinutes =
|
|
1259
|
-
? value.renotifyMinutes
|
|
1260
|
-
: RENOTIFY_INTERVALS.reduce((best, option) => (
|
|
1261
|
-
Math.abs(option - (Number.isFinite(value?.renotifyMinutes) ? value.renotifyMinutes : 5))
|
|
1262
|
-
< Math.abs(best - (Number.isFinite(value?.renotifyMinutes) ? value.renotifyMinutes : 5)) ? option : best
|
|
1263
|
-
), RENOTIFY_INTERVALS[0]);
|
|
1194
|
+
// The interval in force, snapped to a value the control offers — the
|
|
1195
|
+
// same call the reminder timer makes, so the row cannot promise a
|
|
1196
|
+
// cadence the timer does not keep.
|
|
1197
|
+
const renotifyMinutes = renotifyInterval(value?.renotifyMinutes);
|
|
1264
1198
|
const quietStart = Number.isNaN(timeOfDayToMinutes(value?.quietStart)) ? "22:00" : value.quietStart;
|
|
1265
1199
|
const quietEnd = Number.isNaN(timeOfDayToMinutes(value?.quietEnd)) ? "08:00" : value.quietEnd;
|
|
1266
1200
|
|
|
1267
1201
|
return h("div", { className: css.section }, [
|
|
1268
1202
|
h("h2", { className: css.title, key: "title" }, t("title")),
|
|
1269
1203
|
h("p", { className: css.intro, key: "intro" }, t("intro")),
|
|
1270
|
-
h(UpdateNotice, { key: "update-notice", t, autoCheck: value === undefined ? undefined : value.updateAutoCheck !== false }),
|
|
1271
1204
|
h("div", { className: css.rows, key: "rows" }, [
|
|
1272
1205
|
h(SwitchRow, {
|
|
1273
1206
|
key: "enabled",
|
|
@@ -1698,18 +1631,29 @@ window.__ModuleLoader__.load({
|
|
|
1698
1631
|
setError(null);
|
|
1699
1632
|
setBusy(true);
|
|
1700
1633
|
try {
|
|
1701
|
-
|
|
1634
|
+
// The read and the decode are separate failures with separate
|
|
1635
|
+
// causes, so they get separate sentences: a file that was
|
|
1636
|
+
// moved or whose permission was revoked never reached the
|
|
1637
|
+
// decoder at all, and telling the user it "could not be
|
|
1638
|
+
// decoded" sends them to compress a file that is not there.
|
|
1639
|
+
let bytes;
|
|
1640
|
+
try {
|
|
1641
|
+
bytes = await file.arrayBuffer();
|
|
1642
|
+
} catch {
|
|
1643
|
+
setError("custom.unreadable");
|
|
1644
|
+
return;
|
|
1645
|
+
}
|
|
1702
1646
|
const decoded = await validateCustomSoundBytes(bytes);
|
|
1703
1647
|
if (decoded === undefined) {
|
|
1704
1648
|
setError("custom.undecodable");
|
|
1705
1649
|
return;
|
|
1706
1650
|
}
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
//
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1651
|
+
// A refused write is reported by the row's own `failed`
|
|
1652
|
+
// marker; swallowing a rejection here only keeps it from
|
|
1653
|
+
// surfacing as an unhandled promise.
|
|
1654
|
+
try {
|
|
1655
|
+
await onUpload(bytesToBase64(bytes), file.name);
|
|
1656
|
+
} catch {}
|
|
1713
1657
|
} finally {
|
|
1714
1658
|
setBusy(false);
|
|
1715
1659
|
}
|
|
@@ -1825,6 +1769,27 @@ window.__ModuleLoader__.load({
|
|
|
1825
1769
|
/** Re-ping interval choices, in minutes. */
|
|
1826
1770
|
const RENOTIFY_INTERVALS = [1, 5, 15, 30];
|
|
1827
1771
|
|
|
1772
|
+
/**
|
|
1773
|
+
* The interval actually in force, snapped to a value the control offers.
|
|
1774
|
+
*
|
|
1775
|
+
* The schema accepts any whole minute from 1 to 60, while the control
|
|
1776
|
+
* offers four; a hand-edited `7` therefore has to be reconciled
|
|
1777
|
+
* somewhere. It is reconciled HERE, once, and both the row and the
|
|
1778
|
+
* reminder use this function — snapping only for display made the
|
|
1779
|
+
* control say "5 min" while reminders fired every seven, which is the
|
|
1780
|
+
* one outcome a settings row must never produce.
|
|
1781
|
+
*
|
|
1782
|
+
* @param value - the stored preference, possibly absent or out of range.
|
|
1783
|
+
* @returns one of `RENOTIFY_INTERVALS`.
|
|
1784
|
+
*/
|
|
1785
|
+
const renotifyInterval = (value) => {
|
|
1786
|
+
const raw = Number.isFinite(value) ? value : 5;
|
|
1787
|
+
if (RENOTIFY_INTERVALS.includes(raw)) return raw;
|
|
1788
|
+
return RENOTIFY_INTERVALS.reduce((best, option) => (
|
|
1789
|
+
Math.abs(option - raw) < Math.abs(best - raw) ? option : best
|
|
1790
|
+
), RENOTIFY_INTERVALS[0]);
|
|
1791
|
+
};
|
|
1792
|
+
|
|
1828
1793
|
/**
|
|
1829
1794
|
* The repeat-reminder row: its toggle and, beside it, the interval
|
|
1830
1795
|
* SegmentedControl (disabled until the toggle is on).
|
|
@@ -1929,15 +1894,14 @@ window.__ModuleLoader__.load({
|
|
|
1929
1894
|
}
|
|
1930
1895
|
|
|
1931
1896
|
/**
|
|
1932
|
-
* The update row: what this installation runs, what npm has, and
|
|
1933
|
-
*
|
|
1897
|
+
* The update row: what this installation runs, what npm has, and one
|
|
1898
|
+
* button to ask.
|
|
1934
1899
|
*
|
|
1935
|
-
* The check is a read-only question
|
|
1936
|
-
*
|
|
1937
|
-
*
|
|
1938
|
-
*
|
|
1939
|
-
*
|
|
1940
|
-
* opening the section costs no network at all.
|
|
1900
|
+
* The check is a read-only question to this plugin's own Host route.
|
|
1901
|
+
* Installing is the package manager's job (since 1.8.0), so the row
|
|
1902
|
+
* reports and points rather than acting; the route takes no input from
|
|
1903
|
+
* here, and the Host answers from a cache, so opening the section
|
|
1904
|
+
* costs no network at all.
|
|
1941
1905
|
*
|
|
1942
1906
|
* @param props - copy.
|
|
1943
1907
|
* @returns the row element tree.
|
|
@@ -1955,37 +1919,80 @@ window.__ModuleLoader__.load({
|
|
|
1955
1919
|
}
|
|
1956
1920
|
};
|
|
1957
1921
|
// A cached answer makes this free; only a cold Host reaches out.
|
|
1958
|
-
//
|
|
1959
|
-
//
|
|
1960
|
-
// permission to reach out.
|
|
1922
|
+
// `=== true` (not `!== false`), because during the loading frame
|
|
1923
|
+
// `undefined` must not be treated as permission to reach out.
|
|
1961
1924
|
useEffect(() => {
|
|
1962
1925
|
if (autoCheck !== true) return;
|
|
1963
1926
|
check(false)();
|
|
1964
1927
|
}, [autoCheck]);
|
|
1965
1928
|
const payload = state.payload;
|
|
1966
1929
|
const latest = payload?.latest === undefined ? undefined : formatVersion(payload.latest);
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1930
|
+
// A payload without a readable current version is NOT "up to date":
|
|
1931
|
+
// that would turn "I could not read it" into a claim, and hide the
|
|
1932
|
+
// one state where reinstalling is the actual fix.
|
|
1933
|
+
const currentKnown = payload !== undefined && typeof payload.current === "string" && payload.current !== "";
|
|
1934
|
+
// Only an answer that actually came back can report a failure. A
|
|
1935
|
+
// forced re-check must not keep showing the previous failure while
|
|
1936
|
+
// the new request is in the air.
|
|
1937
|
+
const failed = state.phase === "failed" || (state.phase === "done" && payload?.ok === false);
|
|
1938
|
+
let status;
|
|
1972
1939
|
if (state.phase === "checking") status = t("update.checking");
|
|
1973
|
-
if (
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1940
|
+
else if (failed) status = t("update.failed");
|
|
1941
|
+
else if (payload === undefined) status = t("update.idle");
|
|
1942
|
+
else if (!currentKnown) status = t("update.currentUnknown");
|
|
1943
|
+
else if (payload.updateAvailable === true) status = `${t("update.available")} ${latest}`;
|
|
1944
|
+
else status = t("update.uptodate");
|
|
1945
|
+
const detail = currentKnown && !failed
|
|
1946
|
+
? `${t("update.current")} ${formatVersion(payload.current)} · ${status}`
|
|
1947
|
+
: status;
|
|
1948
|
+
// The install belongs to the package manager now: each line below
|
|
1949
|
+
// spells out the one route that applies here, and every one of them
|
|
1950
|
+
// pins the version. A release younger than pnpm's
|
|
1951
|
+
// `minimumReleaseAge` window is otherwise resolved back to the
|
|
1952
|
+
// previous one — which is exactly what "I upgraded but the version
|
|
1953
|
+
// did not move" looks like from the outside.
|
|
1954
|
+
// Two different reasons to send someone to the package manager, and
|
|
1955
|
+
// both need the steps spelled out:
|
|
1956
|
+
//
|
|
1957
|
+
// - a newer version exists (`updateAvailable`);
|
|
1958
|
+
// - the install is not registered (`unregistered`) — the state a
|
|
1959
|
+
// self-updated tree is in: the files say 1.8.6, the lockfile
|
|
1960
|
+
// still records 1.7.0, and the next plugin operation would roll
|
|
1961
|
+
// it back. This is the one prompt that makes an in-app update
|
|
1962
|
+
// permanent, so it must not point at steps that are not shown.
|
|
1963
|
+
const installTarget = installTargetVersion(payload, currentKnown);
|
|
1979
1964
|
const lines = [];
|
|
1965
|
+
if (failed) {
|
|
1966
|
+
// The Host's own `error` is one fixed English sentence; this
|
|
1967
|
+
// line says what it means, without claiming a cause it cannot
|
|
1968
|
+
// know (an offline machine and a mirror returning HTML look
|
|
1969
|
+
// identical from here).
|
|
1970
|
+
lines.push(h("div", { className: css.hint, key: "why" }, t("update.failedDetail")));
|
|
1971
|
+
}
|
|
1980
1972
|
if (payload?.unregistered === true) {
|
|
1981
1973
|
lines.push(h("div", { className: css.hint, key: "unregistered" }, t("update.unregistered", {
|
|
1982
1974
|
recorded: formatVersion(payload.recorded),
|
|
1983
1975
|
})));
|
|
1984
1976
|
}
|
|
1985
|
-
if (
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1977
|
+
if (installTarget !== undefined && installTarget !== "") {
|
|
1978
|
+
const version = packageVersion(installTarget);
|
|
1979
|
+
let guidance;
|
|
1980
|
+
if (isDesktopShell()) {
|
|
1981
|
+
guidance = t("update.desktopGuidance", { version });
|
|
1982
|
+
} else if (typeof payload.profile === "string" && payload.profile !== "") {
|
|
1983
|
+
guidance = t("update.webGuidance", { profile: payload.profile, version });
|
|
1984
|
+
} else {
|
|
1985
|
+
// No profile name is known, so the command must not invent
|
|
1986
|
+
// one: `--profile web` pointed at a profile that does not
|
|
1987
|
+
// exist and failed for a reason that had nothing to do with
|
|
1988
|
+
// the install.
|
|
1989
|
+
guidance = t("update.webGuidancePlain", { version });
|
|
1990
|
+
}
|
|
1991
|
+
lines.push(h("div", { className: css.hint, key: "howto" }, guidance));
|
|
1992
|
+
// Its own line: the steps above are what to do, this is why the
|
|
1993
|
+
// version cannot be dropped — and a parcel of text this long
|
|
1994
|
+
// reads as one dense block when it is glued onto the steps.
|
|
1995
|
+
lines.push(h("div", { className: css.hint, key: "pinned" }, t("update.versionPinned")));
|
|
1989
1996
|
}
|
|
1990
1997
|
return h("div", { className: css.row }, [
|
|
1991
1998
|
h("div", { key: "text" }, [
|
|
@@ -2415,23 +2422,27 @@ window.__ModuleLoader__.load({
|
|
|
2415
2422
|
}
|
|
2416
2423
|
return false;
|
|
2417
2424
|
};
|
|
2418
|
-
/** Route the Host answers a session's goal phase on. */
|
|
2419
|
-
const GOAL_PHASE_PATH = "/notify-sounds/goal";
|
|
2420
2425
|
/** How often an active goal's phase is re-read while waiting for its end. */
|
|
2421
2426
|
const GOAL_POLL_MS = 2000;
|
|
2422
2427
|
/** Longest a goal may stay `active` before its watcher gives up. */
|
|
2423
2428
|
const GOAL_SETTLE_TIMEOUT_MS = 30 * 60 * 1000;
|
|
2429
|
+
/** A single phase read gives up rather than parking the watcher for good. */
|
|
2430
|
+
const GOAL_FETCH_TIMEOUT_MS = 5000;
|
|
2424
2431
|
/**
|
|
2425
2432
|
* One goal phase from the Host, or undefined when it cannot be read.
|
|
2426
2433
|
*
|
|
2427
2434
|
* The Host owns the durable projection; this page's own store only
|
|
2428
2435
|
* carries sessions it has opened, and a completion edge can belong to a
|
|
2429
2436
|
* session this page never opened. Undefined keeps the old behaviour
|
|
2430
|
-
* (alert) for any runtime that does not answer the route
|
|
2437
|
+
* (alert) for any runtime that does not answer the route — and for a
|
|
2438
|
+
* read that timed out, which is the same "cannot know, so speak" case.
|
|
2431
2439
|
*/
|
|
2432
2440
|
const readGoalPhase = async (sessionId) => {
|
|
2433
2441
|
try {
|
|
2434
|
-
const
|
|
2442
|
+
const signal = typeof AbortSignal !== "undefined" && typeof AbortSignal.timeout === "function"
|
|
2443
|
+
? AbortSignal.timeout(GOAL_FETCH_TIMEOUT_MS)
|
|
2444
|
+
: undefined;
|
|
2445
|
+
const response = await fetch(GOAL_PHASE_PATH + "?session=" + encodeURIComponent(sessionId), { method: "GET", signal });
|
|
2435
2446
|
if (!response.ok) return undefined;
|
|
2436
2447
|
const payload = await response.json();
|
|
2437
2448
|
return typeof payload?.phase === "string" ? payload.phase : undefined;
|
|
@@ -2471,7 +2482,13 @@ window.__ModuleLoader__.load({
|
|
|
2471
2482
|
goalWatchers.delete(sessionId);
|
|
2472
2483
|
clearTimeout(pollTimer);
|
|
2473
2484
|
clearTimeout(deadline);
|
|
2474
|
-
|
|
2485
|
+
// The decision to wait was taken at the edge, but the wait
|
|
2486
|
+
// can last half an hour — long enough for the user to turn
|
|
2487
|
+
// the switch OFF precisely because they did not want this
|
|
2488
|
+
// chime. Re-read it here: a preference that was honoured
|
|
2489
|
+
// when it arrived must not be ignored when it leaves.
|
|
2490
|
+
const stillWanted = form.getSnapshot().value?.goalFinalOnly === true;
|
|
2491
|
+
if (chime && stillWanted) alertCompletion(sessionId);
|
|
2475
2492
|
};
|
|
2476
2493
|
goalWatchers.set(sessionId, () => finish(false));
|
|
2477
2494
|
const poll = async () => {
|
|
@@ -2622,7 +2639,7 @@ window.__ModuleLoader__.load({
|
|
|
2622
2639
|
refreshFlash();
|
|
2623
2640
|
const value = form.getSnapshot().value;
|
|
2624
2641
|
if (value?.renotify !== true || pending.size === 0) return;
|
|
2625
|
-
const minutes =
|
|
2642
|
+
const minutes = renotifyInterval(value?.renotifyMinutes);
|
|
2626
2643
|
const intervalMs = minutes * 60000;
|
|
2627
2644
|
const now = Date.now();
|
|
2628
2645
|
const watching = pageIsWatched();
|
|
@@ -2753,21 +2770,12 @@ window.__ModuleLoader__.load({
|
|
|
2753
2770
|
exports.inject = inject;
|
|
2754
2771
|
// Shared contracts with the Host half, exported so the tests can
|
|
2755
2772
|
// cross-check the two spellings against each other (test/host.mjs):
|
|
2756
|
-
// the preset id list behind the Config union, the
|
|
2757
|
-
//
|
|
2758
|
-
// value must survive.
|
|
2773
|
+
// the preset id list behind the Config union, the three routes this
|
|
2774
|
+
// half calls, and the time parser every quiet-hours value must survive.
|
|
2759
2775
|
exports.SOUND_IDS = SOUND_IDS;
|
|
2760
2776
|
exports.FOCUS_PATH = FOCUS_PATH;
|
|
2761
2777
|
exports.UPDATE_CHECK_PATH = UPDATE_CHECK_PATH;
|
|
2762
|
-
|
|
2763
|
-
// the Host names a stage in every failure it reports, so this map is the
|
|
2764
|
-
// one place the advice comes from, and the tests cross-check it against
|
|
2765
|
-
// the stage words the dictionaries carry.
|
|
2766
|
-
exports.UPDATE_STAGES = UPDATE_STAGES;
|
|
2767
|
-
// The two composed messages, so the tests can assert the sentences a
|
|
2768
|
-
// user actually reads rather than only the dictionary keys behind them.
|
|
2769
|
-
exports.updateAppliedText = updateAppliedText;
|
|
2770
|
-
exports.updateFailureText = updateFailureText;
|
|
2778
|
+
exports.GOAL_PHASE_PATH = GOAL_PHASE_PATH;
|
|
2771
2779
|
// The alert election, so the tests can drive two pages against a stubbed
|
|
2772
2780
|
// storage and channel instead of hoping two real tabs cooperate.
|
|
2773
2781
|
exports.createAlertLeader = createAlertLeader;
|
|
@@ -2775,6 +2783,12 @@ window.__ModuleLoader__.load({
|
|
|
2775
2783
|
// scan apart from a strided one.
|
|
2776
2784
|
exports.normalizedGain = normalizedGain;
|
|
2777
2785
|
exports.timeOfDayToMinutes = timeOfDayToMinutes;
|
|
2786
|
+
// The interval the row shows and the timer keeps, so a test can prove
|
|
2787
|
+
// the two agree instead of reading the same number twice.
|
|
2788
|
+
exports.renotifyInterval = renotifyInterval;
|
|
2789
|
+
// Which version an install instruction names, so the "make this update
|
|
2790
|
+
// permanent" case cannot silently become a dead end again.
|
|
2791
|
+
exports.installTargetVersion = installTargetVersion;
|
|
2778
2792
|
return module.exports;
|
|
2779
2793
|
},
|
|
2780
2794
|
});
|