mirror of
https://github.com/c8vzn4ym5g-ai/jdb-command.git
synced 2026-07-22 08:37:24 +00:00
Fix mobile plugin startup
This commit is contained in:
parent
98dbbaac04
commit
33580faebf
6 changed files with 126 additions and 38 deletions
10
README.md
10
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**.
|
||||
|
|
|
|||
89
main.js
89
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
58
verify-mobile-load.cjs
Normal 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;
|
||||
}
|
||||
|
|
@ -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)'))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
{
|
||||
"1.3.0": "1.5.0"
|
||||
"1.3.0": "1.5.0",
|
||||
"1.3.1": "1.5.0"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue