diff --git a/README.md b/README.md index 8773820..1806ba2 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,16 @@ It turns a `jdb-command` code block into one focused product screen: ## Installation +### Beta installation while directory review is pending + +Until JDB Command appears in Obsidian's official Community Plugins search: + +1. Install and enable **BRAT** from **Settings -> Community plugins -> Browse**. +2. Open this link on the device: `obsidian://brat?plugin=c8vzn4ym5g-ai/jdb-command`. +3. Confirm the repository, then enable **JDB Command** under Community plugins. + +The BRAT channel installs the same signed-off GitHub release assets documented below. It is a temporary installation path, not evidence that official directory review has completed. + ### Community Plugins directory After the plugin is approved, open **Settings -> Community plugins -> Browse**, search for **JDB Command**, select **Install**, and then select **Enable**. diff --git a/main.js b/main.js index 9c7a47d..9bcd6e1 100644 --- a/main.js +++ b/main.js @@ -1,18 +1,17 @@ const { Plugin, Notice, normalizePath } = require("obsidian"); const ROUTES = [ - { id: "travel-os", label: "Travel OS", pattern: /(travel|trip|coffee|cafe|caf矇|??|??|?|??)/i }, - { id: "ai-family-book", label: "AI Family Book", pattern: /(ai family book|maya|manuscript|chapter|book|蝡?|??/i }, - { id: "jdb-runtime", label: "JDB Runtime", pattern: /(jdb|runtime|obsidian|digital brain|?訾?憭扯|?嗡辣蝞?/i } + { id: "travel-os", label: "Travel OS", pattern: /(travel|trip|coffee|cafe|café|旅遊|旅行|咖啡|遊記)/i }, + { id: "ai-family-book", label: "AI Family Book", pattern: /(ai family book|maya|manuscript|chapter|book|章節|書)/i }, + { id: "jdb-runtime", label: "JDB Runtime", pattern: /(jdb|runtime|obsidian|digital brain|數位大腦|收件箱)/i } ]; function routeCommand(text) { - return ROUTES.find((route) => route.pattern.test(text)) || { id: "auto", label: "JDB ?芸??斗" }; + return ROUTES.find((route) => route.pattern.test(text)) || { id: "auto", label: "JDB 自動判斷" }; } class JdbCommandPlugin extends Plugin { - async onload() { - this.registerMarkdownCodeBlockProcessor("jdb-command", (_source, element) => this.renderForm(element)); + onload() { this.addCommand({ id: "open-jdb-home", name: "Open JDB command center", @@ -21,31 +20,49 @@ class JdbCommandPlugin extends Plugin { if (file) await this.app.workspace.getLeaf(false).openFile(file); } }); + + const registerCommandBlock = () => { + this.registerMarkdownCodeBlockProcessor("jdb-command", (_source, element) => { + try { + this.renderForm(element); + } catch (error) { + console.error("JDB Command render failed", error); + element.empty(); + element.createDiv({ + cls: "jdb-command-error", + text: "JDB Command could not render this page. Reopen the note or restart Obsidian." + }); + } + }); + }; + + if (this.app.workspace.layoutReady) registerCommandBlock(); + else this.app.workspace.onLayoutReady(registerCommandBlock); } renderForm(element) { const wrapper = element.createDiv({ cls: "jdb-command" }); - wrapper.createEl("h2", { text: "鈭支誨 JDB 撌乩?" }); + wrapper.createEl("h2", { text: "交代 JDB 工作" }); wrapper.createDiv({ cls: "jdb-command-version", text: `JDB Command v${this.manifest.version}` }); - wrapper.createEl("p", { cls: "jdb-command-intro", text: "銝€?亥店鈭支誨撌乩?嚗?閬???抒????單?瑼??DB ????Project?? }); + wrapper.createEl("p", { cls: "jdb-command-intro", text: "一句話交代工作,需要時加入照片、語音或檔案。JDB 會自動判斷 Project。" }); const textarea = wrapper.createEl("textarea", { cls: "jdb-command-text" }); - textarea.placeholder = "靘?嚗???17 撘萇???Travel OS ??Coffee嚗ˊ雿?3 蝭?閮€?; + textarea.placeholder = "例如:把這 17 張照片放入 Travel OS 的 Coffee,製作 3 篇遊記。"; textarea.rows = 5; - textarea.setAttribute("aria-label", "JDB 撌乩??誘"); - const route = wrapper.createDiv({ cls: "jdb-command-route", text: "Project嚗DB ?芸??斗" }); - textarea.addEventListener("input", () => { route.textContent = `Project嚗?{routeCommand(textarea.value).label}`; }); + textarea.setAttribute("aria-label", "JDB 工作指令"); + const route = wrapper.createDiv({ cls: "jdb-command-route", text: "Project:JDB 自動判斷" }); + textarea.addEventListener("input", () => { route.textContent = `Project:${routeCommand(textarea.value).label}`; }); const selectedFiles = []; const fileInput = wrapper.createEl("input", { cls: "jdb-command-files", type: "file" }); fileInput.multiple = true; fileInput.accept = "image/*,audio/*,.pdf,.doc,.docx,.txt,.md"; - fileInput.setAttribute("aria-label", "?豢??抒????單?瑼?"); - const addFiles = wrapper.createEl("button", { cls: "jdb-command-add-files", text: "??抒?嚗?獢? }); + fileInput.setAttribute("aria-label", "選擇照片、語音或檔案"); + const addFiles = wrapper.createEl("button", { cls: "jdb-command-add-files", text: "加入照片/檔案" }); addFiles.type = "button"; - const selection = wrapper.createDiv({ cls: "jdb-command-selection", text: "撠?瑼?" }); + const selection = wrapper.createDiv({ cls: "jdb-command-selection", text: "尚未加入檔案" }); const preview = wrapper.createDiv({ cls: "jdb-command-preview" }); - const clearFiles = wrapper.createEl("button", { cls: "jdb-command-clear-files", text: "皜撌脤瑼?" }); + const clearFiles = wrapper.createEl("button", { cls: "jdb-command-clear-files", text: "清除已選檔案" }); clearFiles.type = "button"; clearFiles.hidden = true; @@ -53,11 +70,11 @@ class JdbCommandPlugin extends Plugin { selection.empty(); preview.empty(); if (!selectedFiles.length) { - selection.setText("撠?瑼?"); + selection.setText("尚未加入檔案"); clearFiles.hidden = true; return; } - selection.createEl("strong", { text: `撌脣???${selectedFiles.length} ??獢 }); + selection.createEl("strong", { text: `已加入 ${selectedFiles.length} 個檔案` }); for (const entry of selectedFiles) { const item = preview.createDiv({ cls: "jdb-command-preview-item" }); if (entry.file.type.startsWith("image/")) { @@ -65,12 +82,12 @@ class JdbCommandPlugin extends Plugin { image.src = URL.createObjectURL(entry.file); image.addEventListener("load", () => URL.revokeObjectURL(image.src), { once: true }); } else { - item.createDiv({ cls: "jdb-command-preview-file", text: "瑼?" }); + item.createDiv({ cls: "jdb-command-preview-file", text: "檔案" }); } item.createEl("span", { text: entry.file.name, attr: { title: entry.file.name } }); - const remove = item.createEl("button", { cls: "jdb-command-remove-file", text: "蝘駁" }); + const remove = item.createEl("button", { cls: "jdb-command-remove-file", text: "移除" }); remove.type = "button"; - remove.setAttribute("aria-label", `蝘駁 ${entry.file.name}`); + remove.setAttribute("aria-label", `移除 ${entry.file.name}`); remove.addEventListener("click", () => { const index = selectedFiles.findIndex((selected) => selected.key === entry.key); if (index >= 0) selectedFiles.splice(index, 1); @@ -94,7 +111,7 @@ class JdbCommandPlugin extends Plugin { renderSelection(); }); - const submit = wrapper.createEl("button", { cls: "mod-cta jdb-command-submit", text: "?漱 JDB" }); + const submit = wrapper.createEl("button", { cls: "mod-cta jdb-command-submit", text: "送交 JDB" }); const status = wrapper.createDiv({ cls: "jdb-command-status", attr: { role: "status", "aria-live": "polite" } }); const receipt = wrapper.createDiv({ cls: "jdb-command-receipt" }); receipt.hidden = true; @@ -102,21 +119,21 @@ class JdbCommandPlugin extends Plugin { const renderReceipt = (result) => { receipt.empty(); receipt.hidden = false; - receipt.createEl("h3", { text: "JDB ?嗡辣?" }); - receipt.createDiv({ cls: "receipt-status", text: "???€??撌脤€脣 JDB ?嗡辣蝞? }); - receipt.createDiv({ cls: "receipt-id", text: `?嗡辣蝺刻?嚗?{result.id}` }); - receipt.createDiv({ cls: "receipt-project", text: `Project嚗?{result.projectLabel}` }); - receipt.createDiv({ cls: "receipt-file-count", text: `撌脫??${result.savedFiles.length} ??獢 }); + receipt.createEl("h3", { text: "JDB 收件回執" }); + receipt.createDiv({ cls: "receipt-status", text: "處理狀態:已進入 JDB 收件箱" }); + receipt.createDiv({ cls: "receipt-id", text: `收件編號:${result.id}` }); + receipt.createDiv({ cls: "receipt-project", text: `Project:${result.projectLabel}` }); + receipt.createDiv({ cls: "receipt-file-count", text: `已收到 ${result.savedFiles.length} 個檔案` }); if (result.savedFiles.length) { const savedPreview = receipt.createDiv({ cls: "jdb-command-preview jdb-command-receipt-preview" }); for (const file of result.savedFiles) { const item = savedPreview.createDiv({ cls: "jdb-command-preview-item" }); if (file.resourceUrl) item.createEl("img", { attr: { src: file.resourceUrl, alt: file.name } }); - else item.createDiv({ cls: "jdb-command-preview-file", text: "瑼?" }); + else item.createDiv({ cls: "jdb-command-preview-file", text: "檔案" }); item.createEl("span", { text: file.name }); } } - const openReceipt = receipt.createEl("button", { text: "???嗡辣?" }); + const openReceipt = receipt.createEl("button", { text: "開啟收件回執" }); openReceipt.type = "button"; openReceipt.addEventListener("click", async () => { const note = this.app.vault.getAbstractFileByPath(result.notePath); @@ -125,22 +142,22 @@ class JdbCommandPlugin extends Plugin { }; submit.addEventListener("click", async () => { const command = textarea.value.trim(); - if (!command) { new Notice("隢?頛詨閬漱隞?JDB ?極雿€?); textarea.focus(); return; } + if (!command) { new Notice("請先輸入要交代 JDB 的工作。"); textarea.focus(); return; } submit.disabled = true; - status.textContent = "甇??漱??; + status.textContent = "正在送交…"; try { const result = await this.submit(command, selectedFiles.map((entry) => entry.file)); textarea.value = ""; selectedFiles.splice(0, selectedFiles.length); renderSelection(); - route.textContent = "Project嚗DB ?芸??斗"; - status.textContent = `?漱??嚗?{result.id}`; + route.textContent = "Project:JDB 自動判斷"; + status.textContent = `送交成功:${result.id}`; renderReceipt(result); - new Notice(`JDB 撌脫?唬遙?? ${result.savedFiles.length} ??獢€); + new Notice(`JDB 已收到任務與 ${result.savedFiles.length} 個檔案。`); } catch (error) { console.error("JDB command submission failed", error); - status.textContent = "?漱憭望?嚗?隞方?瑼?隞???隢炎?亙?甇亙??岫銝€甈~€?; - new Notice("JDB ?漱憭望???); + status.textContent = "送交失敗;指令與檔案仍保留,請檢查同步後再試一次。"; + new Notice("JDB 送交失敗。"); } finally { submit.disabled = false; } diff --git a/manifest.json b/manifest.json index 85e4552..9454815 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "jdb-command", "name": "JDB Command", - "version": "1.3.0", + "version": "1.3.1", "minAppVersion": "1.5.0", "description": "Unified command entry for Jason's Digital Brain.", "author": "Jason Lai", diff --git a/verify-mobile-load.cjs b/verify-mobile-load.cjs new file mode 100644 index 0000000..17a8c9f --- /dev/null +++ b/verify-mobile-load.cjs @@ -0,0 +1,58 @@ +const assert = require("node:assert/strict"); +const Module = require("node:module"); + +let layoutCallback; +let processorRegistrations = 0; +let commandRegistrations = 0; + +class Plugin { + constructor() { + this.app = { + workspace: { + layoutReady: false, + onLayoutReady(callback) { + layoutCallback = callback; + }, + getLeaf() { + return { openFile: async () => {} }; + } + }, + vault: { + getAbstractFileByPath() { + return null; + } + } + }; + this.manifest = { version: "test" }; + } + + addCommand() { + commandRegistrations += 1; + } + + registerMarkdownCodeBlockProcessor() { + processorRegistrations += 1; + } +} + +const originalLoad = Module._load; +Module._load = function load(request, parent, isMain) { + if (request === "obsidian") { + return { Plugin, Notice: class Notice {}, normalizePath: (path) => path }; + } + return originalLoad(request, parent, isMain); +}; + +try { + const JdbCommandPlugin = require("./main.js"); + const plugin = new JdbCommandPlugin(); + assert.doesNotThrow(() => plugin.onload()); + assert.equal(commandRegistrations, 1, "command registration should remain available during startup"); + assert.equal(processorRegistrations, 0, "mobile UI must not initialize before layout readiness"); + assert.equal(typeof layoutCallback, "function", "layout-ready callback must be registered"); + assert.doesNotThrow(() => layoutCallback()); + assert.equal(processorRegistrations, 1, "command block must register after layout readiness"); + console.log("mobile-load: passed"); +} finally { + Module._load = originalLoad; +} diff --git a/verify-release.ps1 b/verify-release.ps1 index 6749860..eeb3dc7 100644 --- a/verify-release.ps1 +++ b/verify-release.ps1 @@ -16,6 +16,8 @@ $checks = [ordered]@{ 'ui.preview-and-remove' = ($main.Contains('URL.createObjectURL') -and $main.Contains('jdb-command-remove-file') -and $main.Contains('aria-live')) 'files.multi-selection-accumulates' = ($main.Contains('fileInput.multiple = true') -and $main.Contains('selectedFiles.push')) 'failure.partial-files-rollback' = ($main.Contains('await this.app.vault.delete(partial, true)')) + 'mobile.defer-until-layout-ready' = ($main.Contains('workspace.onLayoutReady(registerCommandBlock)') -and $main.Contains('workspace.layoutReady')) + 'mobile.render-failure-isolated' = ($main.Contains('JDB Command render failed') -and $main.Contains('jdb-command-error')) 'privacy.no-network-adapter' = (-not ($main -match '\b(fetch|XMLHttpRequest|WebSocket)\b')) 'styles.mobile-layout' = ($styles.Contains('@media (max-width: 480px)')) } diff --git a/versions.json b/versions.json index 31242ce..d562c2a 100644 --- a/versions.json +++ b/versions.json @@ -1,3 +1,4 @@ { - "1.3.0": "1.5.0" + "1.3.0": "1.5.0", + "1.3.1": "1.5.0" }