dsh-tiddlywiki 0.26.0 → 0.26.2
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 +2 -0
- package/lib/client.js +8 -8
- package/lib/index.js +2 -1
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/settings-page.ts +51 -10
- package/src/host/admin.ts +11 -2
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-tiddlywiki",
|
|
3
3
|
"description": "TiddlyWiki 5 as the DSH persistent knowledge base: fifteen tiddlywiki_* agent tools (filtered search/recent/tags/batch/rewrite/trash/lint + git sync & conflict resolve), a full TiddlyWiki editor embedded in the GUI center column (same-origin proxy, works over Tailscale/LAN/domain), a right-side wiki tab, a per-session summary tab, a quick-note card with draft auto-save behind a single knowledge FAB, an optional clip bookmarklet that also saves selected images as attachments, and git-based sync with auto-commit. Mounts via the official dsh plugin system — no DSH source changes.",
|
|
4
|
-
"version": "0.26.
|
|
4
|
+
"version": "0.26.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"dsh",
|
|
@@ -169,8 +169,19 @@ function mountSettingsPage(container: HTMLElement): () => void {
|
|
|
169
169
|
const disposers: Array<() => void> = []
|
|
170
170
|
/** Config-section DOM + the server signature it was built from (see renderMain). */
|
|
171
171
|
const configState: ConfigRenderState = {}
|
|
172
|
-
/**
|
|
173
|
-
|
|
172
|
+
/**
|
|
173
|
+
* 插件/主题/语言的「未应用勾选」(见 CatalogPending)。
|
|
174
|
+
*
|
|
175
|
+
* ⚠️ 必须是**空对象**,绝不能预置空 Set(v0.26.1 修 P0;回归由 v0.25.0 的
|
|
176
|
+
* commit 883196f 引入):契约是 `undefined` = 用户还没碰过、完全跟随服务器,
|
|
177
|
+
* 而 `desiredPlugins()` 等用 `??` 回退 —— **空 Set 不是 undefined**,预置成
|
|
178
|
+
* 空 Set 会让「没碰过」被读成「用户期望集合为空」,后果有两层:
|
|
179
|
+
* ① 设置页所有插件/主题/语言勾选框**全部显示为未勾选**(用户看到「装了却没勾」);
|
|
180
|
+
* ② 更严重——什么都没改就点「应用插件」,会 POST 一个**空数组**,把
|
|
181
|
+
* `tiddlywiki.info` 的插件清单整个清空(主题清单、语言同理)。
|
|
182
|
+
* 守门:`scripts/verify-plugin-runtime.mjs` 的「未碰过 ≠ 空集合」一节。
|
|
183
|
+
*/
|
|
184
|
+
const catalogPending: CatalogPending = {}
|
|
174
185
|
/** 首屏是否已成功渲染过内容:决定占位提示与失败时能不能清 body。 */
|
|
175
186
|
let rendered = false
|
|
176
187
|
|
|
@@ -652,6 +663,13 @@ function renderCatalogSection(
|
|
|
652
663
|
const listWrap = make('div', 'dsh-tw-settings-list')
|
|
653
664
|
const applyPlugins = make('button', 'dsh-tw-settings-btn dsh-tw-settings-primary', '应用插件(重启 TW)')
|
|
654
665
|
applyPlugins.type = 'button'
|
|
666
|
+
/**
|
|
667
|
+
* 没碰过勾选时禁用「应用」(v0.26.1):`pending.plugins === undefined` = 跟随服务器,
|
|
668
|
+
* 提交没有意义;把它当成「空期望集合」误提交就是清空整个启动清单。勾选变化时重算。
|
|
669
|
+
*/
|
|
670
|
+
const syncApplyPlugins = (): void => {
|
|
671
|
+
applyPlugins.disabled = pending.plugins === undefined
|
|
672
|
+
}
|
|
655
673
|
|
|
656
674
|
const renderPluginList = (needle: string): void => {
|
|
657
675
|
listWrap.replaceChildren()
|
|
@@ -666,6 +684,7 @@ function renderCatalogSection(
|
|
|
666
684
|
if (input.checked) next.add(plugin.name)
|
|
667
685
|
else next.delete(plugin.name)
|
|
668
686
|
pending.plugins = next
|
|
687
|
+
syncApplyPlugins()
|
|
669
688
|
})
|
|
670
689
|
const label = make('label', 'dsh-tw-settings-row dsh-tw-settings-plugin')
|
|
671
690
|
const name = make('span', 'dsh-tw-settings-name', plugin.label)
|
|
@@ -675,12 +694,14 @@ function renderCatalogSection(
|
|
|
675
694
|
// 状态徽标(v0.26.0):勾选只反映 tiddlywiki.info,运行时真相反映在这里。
|
|
676
695
|
// ① 已在启动清单(勾选)但被 TW 禁用;② 不在启动清单但 wiki 内有同名 tiddler
|
|
677
696
|
// 版本(TW 原生装的)。两种都常见,且不显示就会让用户误判「没装成功」。
|
|
697
|
+
// 判据取**服务器集合**而不是 desiredPlugins():徽标描述的是当前事实,
|
|
698
|
+
// 不该随「还没应用的勾选」抖动(v0.26.1)。
|
|
678
699
|
if (runtimePlugins != null && disabledTitles.has(plugin.title)) {
|
|
679
700
|
const badge = make('span', 'dsh-tw-settings-chip', 'TW 内已禁用')
|
|
680
701
|
badge.dataset.state = 'disabled'
|
|
681
702
|
badge.title = '该插件在 tiddlywiki.info 里,但已在 TW 控制面板被禁用($:/config/Plugins/Disabled)——去 TW 面板重新启用'
|
|
682
703
|
label.append(badge)
|
|
683
|
-
} else if (runtimePlugins != null && !
|
|
704
|
+
} else if (runtimePlugins != null && !serverPlugins.includes(plugin.name) && wikiTitles.has(plugin.title)) {
|
|
684
705
|
const badge = make('span', 'dsh-tw-settings-chip', 'wiki 内已装')
|
|
685
706
|
badge.dataset.state = 'update'
|
|
686
707
|
badge.title = '不在启动清单,但 wiki 里存在同名插件 tiddler(经 TW 原生安装),TW 运行时已在用'
|
|
@@ -692,6 +713,7 @@ function renderCatalogSection(
|
|
|
692
713
|
}
|
|
693
714
|
search.addEventListener('input', () => renderPluginList(search.value))
|
|
694
715
|
applyPlugins.addEventListener('click', () => {
|
|
716
|
+
if (pending.plugins === undefined) return
|
|
695
717
|
applyPlugins.disabled = true
|
|
696
718
|
void (async () => {
|
|
697
719
|
try {
|
|
@@ -702,11 +724,12 @@ function renderCatalogSection(
|
|
|
702
724
|
void refresh()
|
|
703
725
|
} catch (err) {
|
|
704
726
|
toast(`应用失败:${err instanceof Error ? err.message : String(err)}`)
|
|
705
|
-
|
|
727
|
+
syncApplyPlugins()
|
|
706
728
|
}
|
|
707
729
|
})()
|
|
708
730
|
})
|
|
709
731
|
renderPluginList('')
|
|
732
|
+
syncApplyPlugins()
|
|
710
733
|
pluginSection.append(search, listWrap, applyPlugins)
|
|
711
734
|
body.append(pluginSection)
|
|
712
735
|
|
|
@@ -782,6 +805,15 @@ function renderCatalogSection(
|
|
|
782
805
|
: lastLoadedTheme ?? 'tiddlywiki/vanilla'
|
|
783
806
|
/** 活动主题同样走 pending:单选按钮的未应用选择也要活过 refresh(见 CatalogPending)。 */
|
|
784
807
|
const activeThemeName = (): string => pending.themeActive ?? serverActiveThemeName
|
|
808
|
+
const applyThemes = make('button', 'dsh-tw-settings-btn dsh-tw-settings-primary', '应用主题(重启 TW)')
|
|
809
|
+
applyThemes.type = 'button'
|
|
810
|
+
/**
|
|
811
|
+
* 没碰过主题的任何控件时禁用「应用」(v0.26.1,同插件管理):`pending.* === undefined`
|
|
812
|
+
* 才是「跟随服务器」;未碰过就提交会把「空期望集合」写成 `themes: []`,把主题清空。
|
|
813
|
+
*/
|
|
814
|
+
const syncApplyThemes = (): void => {
|
|
815
|
+
applyThemes.disabled = pending.themes === undefined && pending.themeActive === undefined
|
|
816
|
+
}
|
|
785
817
|
const themeWrap = make('div', 'dsh-tw-settings-list')
|
|
786
818
|
for (const theme of themes) {
|
|
787
819
|
const load = make('input', 'dsh-tw-settings-check')
|
|
@@ -793,6 +825,7 @@ function renderCatalogSection(
|
|
|
793
825
|
if (load.checked) next.add(theme.name)
|
|
794
826
|
else next.delete(theme.name)
|
|
795
827
|
pending.themes = next
|
|
828
|
+
syncApplyThemes()
|
|
796
829
|
})
|
|
797
830
|
const act = make('input', 'dsh-tw-settings-check')
|
|
798
831
|
act.type = 'radio'
|
|
@@ -801,6 +834,7 @@ function renderCatalogSection(
|
|
|
801
834
|
act.title = '设为活动主题'
|
|
802
835
|
act.addEventListener('change', () => {
|
|
803
836
|
if (act.checked) pending.themeActive = theme.name
|
|
837
|
+
syncApplyThemes()
|
|
804
838
|
})
|
|
805
839
|
const name = make('span', 'dsh-tw-settings-name', theme.label)
|
|
806
840
|
name.title = theme.name
|
|
@@ -809,9 +843,8 @@ function renderCatalogSection(
|
|
|
809
843
|
row.append(load, act, name, desc)
|
|
810
844
|
themeWrap.append(row)
|
|
811
845
|
}
|
|
812
|
-
const applyThemes = make('button', 'dsh-tw-settings-btn dsh-tw-settings-primary', '应用主题(重启 TW)')
|
|
813
|
-
applyThemes.type = 'button'
|
|
814
846
|
applyThemes.addEventListener('click', () => {
|
|
847
|
+
if (pending.themes === undefined && pending.themeActive === undefined) return
|
|
815
848
|
applyThemes.disabled = true
|
|
816
849
|
void (async () => {
|
|
817
850
|
try {
|
|
@@ -827,10 +860,11 @@ function renderCatalogSection(
|
|
|
827
860
|
void refresh()
|
|
828
861
|
} catch (err) {
|
|
829
862
|
toast(`应用失败:${err instanceof Error ? err.message : String(err)}`)
|
|
830
|
-
|
|
863
|
+
syncApplyThemes()
|
|
831
864
|
}
|
|
832
865
|
})()
|
|
833
866
|
})
|
|
867
|
+
syncApplyThemes()
|
|
834
868
|
themeSection.append(themeHint, themeHead, themeWrap, applyThemes)
|
|
835
869
|
body.append(themeSection)
|
|
836
870
|
|
|
@@ -839,6 +873,12 @@ function renderCatalogSection(
|
|
|
839
873
|
langSection.append(make('h3', 'dsh-tw-settings-h', '语言管理(自带官方语言包)'))
|
|
840
874
|
const langHint = make('div', 'dsh-tw-settings-muted', '勾选启用语言插件并重启 TW;如中文请选 zh-Hans(简体)或 zh-CN。')
|
|
841
875
|
const langWrap = make('div', 'dsh-tw-settings-list')
|
|
876
|
+
const applyLangs = make('button', 'dsh-tw-settings-btn dsh-tw-settings-primary', '应用语言(重启 TW)')
|
|
877
|
+
applyLangs.type = 'button'
|
|
878
|
+
/** 没碰过语言勾选时禁用「应用」(v0.26.1,同插件管理):未碰过 ≠ 期望集合为空。 */
|
|
879
|
+
const syncApplyLangs = (): void => {
|
|
880
|
+
applyLangs.disabled = pending.languages === undefined
|
|
881
|
+
}
|
|
842
882
|
for (const lang of languages) {
|
|
843
883
|
const input = make('input', 'dsh-tw-settings-check')
|
|
844
884
|
input.type = 'checkbox'
|
|
@@ -848,6 +888,7 @@ function renderCatalogSection(
|
|
|
848
888
|
if (input.checked) next.add(lang.name)
|
|
849
889
|
else next.delete(lang.name)
|
|
850
890
|
pending.languages = next
|
|
891
|
+
syncApplyLangs()
|
|
851
892
|
})
|
|
852
893
|
const label = make('label', 'dsh-tw-settings-row dsh-tw-settings-plugin')
|
|
853
894
|
const name = make('span', 'dsh-tw-settings-name', lang.label)
|
|
@@ -856,9 +897,8 @@ function renderCatalogSection(
|
|
|
856
897
|
label.append(input, name, desc)
|
|
857
898
|
langWrap.append(label)
|
|
858
899
|
}
|
|
859
|
-
const applyLangs = make('button', 'dsh-tw-settings-btn dsh-tw-settings-primary', '应用语言(重启 TW)')
|
|
860
|
-
applyLangs.type = 'button'
|
|
861
900
|
applyLangs.addEventListener('click', () => {
|
|
901
|
+
if (pending.languages === undefined) return
|
|
862
902
|
applyLangs.disabled = true
|
|
863
903
|
void (async () => {
|
|
864
904
|
try {
|
|
@@ -868,10 +908,11 @@ function renderCatalogSection(
|
|
|
868
908
|
void refresh()
|
|
869
909
|
} catch (err) {
|
|
870
910
|
toast(`应用失败:${err instanceof Error ? err.message : String(err)}`)
|
|
871
|
-
|
|
911
|
+
syncApplyLangs()
|
|
872
912
|
}
|
|
873
913
|
})()
|
|
874
914
|
})
|
|
915
|
+
syncApplyLangs()
|
|
875
916
|
langSection.append(langHint, langWrap, applyLangs)
|
|
876
917
|
body.append(langSection)
|
|
877
918
|
}
|
package/src/host/admin.ts
CHANGED
|
@@ -359,16 +359,25 @@ export async function bundledCatalog(twRoot: string): Promise<Catalog> {
|
|
|
359
359
|
}
|
|
360
360
|
const out: CatalogEntry[] = []
|
|
361
361
|
for (const dir of dirs) {
|
|
362
|
-
let info: { name?: string; description?: string; dependents?: string[] } = {}
|
|
362
|
+
let info: { title?: string; name?: string; description?: string; dependents?: string[] } = {}
|
|
363
363
|
try {
|
|
364
364
|
info = JSON.parse(await readFile(join(root, dir, 'plugin.info'), 'utf8')) as typeof info
|
|
365
365
|
} catch {
|
|
366
366
|
info = {}
|
|
367
367
|
}
|
|
368
368
|
if (sub === 'themes' && dir !== 'vanilla' && !(await themeHasCss(dir))) continue
|
|
369
|
+
// The tiddler title comes from plugin.info, NOT from the folder name
|
|
370
|
+
// (v0.26.2): they normally agree, but `plugins/tiddlywiki/codemirror-fullscreen-editing`
|
|
371
|
+
// declares `"title": "$:/plugins/tiddlywiki/codemirror-fullscreen"` — deriving
|
|
372
|
+
// the title from the directory made every title-keyed comparison miss that
|
|
373
|
+
// plugin (the settings page's 「TW 内已禁用」/「wiki 内已装」 badges match on
|
|
374
|
+
// the full tiddler title, so a disabled codemirror-fullscreen showed no badge).
|
|
375
|
+
// plugin.info IS the tiddler TW loads, so it is the source of truth; the
|
|
376
|
+
// folder-derived form stays only as a fallback for a missing/empty title.
|
|
377
|
+
const declaredTitle = typeof info.title === 'string' && info.title.length > 0 ? info.title : undefined
|
|
369
378
|
out.push({
|
|
370
379
|
name: `tiddlywiki/${dir}`,
|
|
371
|
-
title: sub === 'plugins' ? `$:/plugins/tiddlywiki/${dir}` : `$:/themes/tiddlywiki/${dir}
|
|
380
|
+
title: declaredTitle ?? (sub === 'plugins' ? `$:/plugins/tiddlywiki/${dir}` : `$:/themes/tiddlywiki/${dir}`),
|
|
372
381
|
label: info.name ?? dir,
|
|
373
382
|
description: info.description ?? '',
|
|
374
383
|
// plugin.info `dependents` are full plugin titles → convert to names.
|