Fix mobile plugin startup

This commit is contained in:
Jason Lai 2026-07-22 10:00:21 +08:00
parent 98dbbaac04
commit 33580faebf
6 changed files with 126 additions and 38 deletions

View file

@ -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**.

89
main.js
View file

@ -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: "ProjectJDB 自動判斷" });
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 = "ProjectJDB 自動判斷";
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;
}

View file

@ -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",

58
verify-mobile-load.cjs Normal file
View file

@ -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;
}

View file

@ -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)'))
}

View file

@ -1,3 +1,4 @@
{
"1.3.0": "1.5.0"
"1.3.0": "1.5.0",
"1.3.1": "1.5.0"
}