fix image urls, fix context menu
This commit is contained in:
parent
b48ef720b8
commit
d8d12054b7
9 changed files with 157 additions and 39 deletions
|
|
@ -36,6 +36,9 @@ const vaultRoutes = require("./routes/vault");
|
||||||
app.use("/api/fs", fsRoutes);
|
app.use("/api/fs", fsRoutes);
|
||||||
app.use("/api/vault", vaultRoutes);
|
app.use("/api/vault", vaultRoutes);
|
||||||
|
|
||||||
|
// Serve vault files for resource URLs (images, attachments, etc.)
|
||||||
|
app.use("/vault-files", express.static(config.vaultPath));
|
||||||
|
|
||||||
// --- Static serving ---
|
// --- Static serving ---
|
||||||
// Serve the built shim-loader.js
|
// Serve the built shim-loader.js
|
||||||
app.use(
|
app.use(
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,13 @@ export const electronShim = {
|
||||||
webFrame,
|
webFrame,
|
||||||
remote: remoteShim,
|
remote: remoteShim,
|
||||||
|
|
||||||
|
// electron.webUtils - used for drag/drop file path extraction (desktop only)
|
||||||
|
webUtils: {
|
||||||
|
getPathForFile(file) {
|
||||||
|
return "";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
// electron.deprecate - used by Obsidian to mark deprecated APIs
|
// electron.deprecate - used by Obsidian to mark deprecated APIs
|
||||||
deprecate: {
|
deprecate: {
|
||||||
function(fn, name) {
|
function(fn, name) {
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ const syncHandlers = {
|
||||||
vault: () => window.__vaultConfig || { id: "default-vault", path: "/" },
|
vault: () => window.__vaultConfig || { id: "default-vault", path: "/" },
|
||||||
version: () => "1.8.9",
|
version: () => "1.8.9",
|
||||||
"is-dev": () => false,
|
"is-dev": () => false,
|
||||||
"file-url": () => "",
|
"file-url": () => "/vault-files/",
|
||||||
"disable-update": () => true,
|
"disable-update": () => true,
|
||||||
update: () => "",
|
update: () => "",
|
||||||
"disable-gpu": () => false,
|
"disable-gpu": () => false,
|
||||||
|
|
@ -49,7 +49,20 @@ const syncHandlers = {
|
||||||
export const ipcRenderer = {
|
export const ipcRenderer = {
|
||||||
send(channel, ...args) {
|
send(channel, ...args) {
|
||||||
console.log("[shim:ipcRenderer] send:", channel, args);
|
console.log("[shim:ipcRenderer] send:", channel, args);
|
||||||
// TODO: route to server via chosen sync mechanism if needed
|
|
||||||
|
// context-menu: Obsidian sends this and waits (up to 1s) for a response.
|
||||||
|
// In Electron, the main process returns spell-check info + edit flags.
|
||||||
|
// We reply immediately with a response object so Obsidian proceeds to
|
||||||
|
// build and show its HTML context menu without delay.
|
||||||
|
if (channel === "context-menu") {
|
||||||
|
queueMicrotask(() =>
|
||||||
|
ipcRenderer._emit("context-menu", {
|
||||||
|
webContentsId: 1,
|
||||||
|
editFlags: { canCut: true, canCopy: true, canPaste: true },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
sendSync(channel, ...args) {
|
sendSync(channel, ...args) {
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
// @electron/remote shim
|
// @electron/remote shim
|
||||||
// Returned when Obsidian calls: window.require('@electron/remote')
|
// Returned when Obsidian calls: window.require('@electron/remote')
|
||||||
|
|
||||||
import { clipboardShim } from './clipboard.js';
|
import { clipboardShim } from "./clipboard.js";
|
||||||
import { shellShim } from './shell.js';
|
import { shellShim } from "./shell.js";
|
||||||
import { dialogShim } from './dialog.js';
|
import { dialogShim } from "./dialog.js";
|
||||||
import { menuShim, menuItemShim } from './menu.js';
|
import { menuShim, menuItemShim } from "./menu.js";
|
||||||
import { appShim } from './app.js';
|
import { appShim } from "./app.js";
|
||||||
import { windowShim, webContentsShim } from './window.js';
|
import { windowShim, webContentsShim } from "./window.js";
|
||||||
import { themeShim } from './theme.js';
|
import { themeShim } from "./theme.js";
|
||||||
import { sessionShim } from './session.js';
|
import { sessionShim } from "./session.js";
|
||||||
import { systemPreferencesShim } from './system-preferences.js';
|
import { systemPreferencesShim } from "./system-preferences.js";
|
||||||
import { screenShim } from './screen.js';
|
import { screenShim } from "./screen.js";
|
||||||
import { nativeImageShim } from './native-image.js';
|
import { nativeImageShim } from "./native-image.js";
|
||||||
import { notificationShim } from './notification.js';
|
import { notificationShim } from "./notification.js";
|
||||||
|
|
||||||
export const remoteShim = {
|
export const remoteShim = {
|
||||||
clipboard: clipboardShim,
|
clipboard: clipboardShim,
|
||||||
|
|
@ -33,6 +33,8 @@ export const remoteShim = {
|
||||||
return windowShim._current();
|
return windowShim._current();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
webContents: webContentsShim,
|
||||||
|
|
||||||
getCurrentWebContents() {
|
getCurrentWebContents() {
|
||||||
return webContentsShim._current();
|
return webContentsShim._current();
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,7 @@ const currentWindow = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentWebContents = {
|
const currentWebContents = {
|
||||||
|
id: 1,
|
||||||
_zoomLevel: 0,
|
_zoomLevel: 0,
|
||||||
|
|
||||||
get zoomLevel() {
|
get zoomLevel() {
|
||||||
|
|
@ -182,7 +183,25 @@ const currentWebContents = {
|
||||||
|
|
||||||
undo() {},
|
undo() {},
|
||||||
redo() {},
|
redo() {},
|
||||||
pasteAndMatchStyle() {},
|
cut() {
|
||||||
|
document.execCommand("cut");
|
||||||
|
},
|
||||||
|
copy() {
|
||||||
|
document.execCommand("copy");
|
||||||
|
},
|
||||||
|
paste() {
|
||||||
|
document.execCommand("paste");
|
||||||
|
},
|
||||||
|
pasteAndMatchStyle() {
|
||||||
|
document.execCommand("paste");
|
||||||
|
},
|
||||||
|
replaceMisspelling(word) {},
|
||||||
|
|
||||||
|
session: {
|
||||||
|
availableSpellCheckerLanguages: [],
|
||||||
|
setSpellCheckerLanguages(langs) {},
|
||||||
|
addWordToSpellCheckerDictionary(word) {},
|
||||||
|
},
|
||||||
|
|
||||||
setSpellCheckerLanguages(langs) {},
|
setSpellCheckerLanguages(langs) {},
|
||||||
|
|
||||||
|
|
@ -212,4 +231,7 @@ export const windowShim = {
|
||||||
|
|
||||||
export const webContentsShim = {
|
export const webContentsShim = {
|
||||||
_current: () => currentWebContents,
|
_current: () => currentWebContents,
|
||||||
|
fromId(id) {
|
||||||
|
return id === currentWebContents.id ? currentWebContents : null;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -24,20 +24,39 @@ export function createFsPromises(metadataCache, contentCache, transport) {
|
||||||
if (meta && meta.type === "file") {
|
if (meta && meta.type === "file") {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
// If path not in cache at all (and not root), it doesn't exist
|
||||||
|
if (!meta && path && path !== "/" && path !== ".") {
|
||||||
|
const e = new Error(
|
||||||
|
`ENOENT: no such file or directory, scandir '${path}'`,
|
||||||
|
);
|
||||||
|
e.code = "ENOENT";
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
// Serve from metadata cache
|
// Serve from metadata cache
|
||||||
const entries = metadataCache.readdir(path);
|
const entries = metadataCache.readdir(path);
|
||||||
if (entries.length > 0) {
|
|
||||||
return entries.map((e) => e.name);
|
return entries.map((e) => e.name);
|
||||||
}
|
|
||||||
// Fallback to server
|
|
||||||
const serverEntries = await transport.readdir(path);
|
|
||||||
return serverEntries.map((e) => e.name);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async readFile(path, encoding) {
|
async readFile(path, encoding) {
|
||||||
if (typeof encoding === "object") encoding = encoding?.encoding;
|
if (typeof encoding === "object") encoding = encoding?.encoding;
|
||||||
const wantText = encoding === "utf8" || encoding === "utf-8";
|
const wantText = encoding === "utf8" || encoding === "utf-8";
|
||||||
|
|
||||||
|
// Short-circuit: reading a directory is an error
|
||||||
|
const meta = metadataCache.get(path);
|
||||||
|
if (meta && meta.type === "directory") {
|
||||||
|
const e = new Error("EISDIR: illegal operation on a directory, read");
|
||||||
|
e.code = "EISDIR";
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
// Short-circuit: file not in metadata cache → doesn't exist
|
||||||
|
if (!meta && path) {
|
||||||
|
const e = new Error(
|
||||||
|
`ENOENT: no such file or directory, open '${path}'`,
|
||||||
|
);
|
||||||
|
e.code = "ENOENT";
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
// Check content cache
|
// Check content cache
|
||||||
const cached = contentCache.get(path);
|
const cached = contentCache.get(path);
|
||||||
if (cached !== null) {
|
if (cached !== null) {
|
||||||
|
|
@ -142,7 +161,11 @@ export function createFsPromises(metadataCache, contentCache, transport) {
|
||||||
|
|
||||||
async access(path) {
|
async access(path) {
|
||||||
if (metadataCache.has(path)) return;
|
if (metadataCache.has(path)) return;
|
||||||
await transport.access(path);
|
const e = new Error(
|
||||||
|
`ENOENT: no such file or directory, access '${path}'`,
|
||||||
|
);
|
||||||
|
e.code = "ENOENT";
|
||||||
|
throw e;
|
||||||
},
|
},
|
||||||
|
|
||||||
async realpath(path) {
|
async realpath(path) {
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,10 @@ export function createFsSync(metadataCache, contentCache, transport) {
|
||||||
statSync(path) {
|
statSync(path) {
|
||||||
const stat = metadataCache.toStat(path);
|
const stat = metadataCache.toStat(path);
|
||||||
if (!stat) {
|
if (!stat) {
|
||||||
const err = new Error(`ENOENT: no such file or directory, stat '${path}'`);
|
const err = new Error(
|
||||||
err.code = 'ENOENT';
|
`ENOENT: no such file or directory, stat '${path}'`,
|
||||||
|
);
|
||||||
|
err.code = "ENOENT";
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
return stat;
|
return stat;
|
||||||
|
|
@ -19,39 +21,52 @@ export function createFsSync(metadataCache, contentCache, transport) {
|
||||||
|
|
||||||
accessSync(path, mode) {
|
accessSync(path, mode) {
|
||||||
if (!metadataCache.has(path)) {
|
if (!metadataCache.has(path)) {
|
||||||
const err = new Error(`ENOENT: no such file or directory, access '${path}'`);
|
const err = new Error(
|
||||||
err.code = 'ENOENT';
|
`ENOENT: no such file or directory, access '${path}'`,
|
||||||
|
);
|
||||||
|
err.code = "ENOENT";
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
readFileSync(path, encoding) {
|
readFileSync(path, encoding) {
|
||||||
if (typeof encoding === 'object') encoding = encoding?.encoding;
|
if (typeof encoding === "object") encoding = encoding?.encoding;
|
||||||
|
|
||||||
|
// Short-circuit: reading a directory is an error
|
||||||
|
const meta = metadataCache.get(path);
|
||||||
|
if (meta && meta.type === "directory") {
|
||||||
|
const e = new Error("EISDIR: illegal operation on a directory, read");
|
||||||
|
e.code = "EISDIR";
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
// Try content cache first
|
// Try content cache first
|
||||||
const cached = contentCache.get(path);
|
const cached = contentCache.get(path);
|
||||||
if (cached !== null) {
|
if (cached !== null) {
|
||||||
if (encoding === 'utf8' || encoding === 'utf-8') {
|
if (encoding === "utf8" || encoding === "utf-8") {
|
||||||
return typeof cached === 'string' ? cached : new TextDecoder().decode(cached);
|
return typeof cached === "string"
|
||||||
|
? cached
|
||||||
|
: new TextDecoder().decode(cached);
|
||||||
}
|
}
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: synchronous XHR
|
// Fallback: synchronous XHR
|
||||||
console.warn('[shim:fs] readFileSync cache miss, using sync XHR:', path);
|
console.warn("[shim:fs] readFileSync cache miss, using sync XHR:", path);
|
||||||
const data = transport.readFileSync(path, encoding);
|
const data = transport.readFileSync(path, encoding);
|
||||||
contentCache.set(path, data);
|
contentCache.set(path, data);
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|
||||||
writeFileSync(path, data, encoding) {
|
writeFileSync(path, data, encoding) {
|
||||||
if (typeof encoding === 'object') encoding = encoding?.encoding;
|
if (typeof encoding === "object") encoding = encoding?.encoding;
|
||||||
|
|
||||||
// Write to cache immediately (sync return)
|
// Write to cache immediately (sync return)
|
||||||
contentCache.set(path, data);
|
contentCache.set(path, data);
|
||||||
const size = typeof data === 'string' ? data.length : (data.byteLength || 0);
|
const size =
|
||||||
|
typeof data === "string" ? data.length : data.byteLength || 0;
|
||||||
metadataCache.set(path, {
|
metadataCache.set(path, {
|
||||||
type: 'file',
|
type: "file",
|
||||||
size,
|
size,
|
||||||
mtime: Date.now(),
|
mtime: Date.now(),
|
||||||
ctime: metadataCache.get(path)?.ctime || Date.now(),
|
ctime: metadataCache.get(path)?.ctime || Date.now(),
|
||||||
|
|
@ -59,7 +74,11 @@ export function createFsSync(metadataCache, contentCache, transport) {
|
||||||
|
|
||||||
// Fire-and-forget async send to server
|
// Fire-and-forget async send to server
|
||||||
transport.writeFile(path, data, encoding).catch((e) => {
|
transport.writeFile(path, data, encoding).catch((e) => {
|
||||||
console.error('[shim:fs] writeFileSync background save failed:', path, e);
|
console.error(
|
||||||
|
"[shim:fs] writeFileSync background save failed:",
|
||||||
|
path,
|
||||||
|
e,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -67,15 +86,21 @@ export function createFsSync(metadataCache, contentCache, transport) {
|
||||||
contentCache.delete(path);
|
contentCache.delete(path);
|
||||||
metadataCache.delete(path);
|
metadataCache.delete(path);
|
||||||
|
|
||||||
// Fire-and-forget
|
// Fire-and-forget - suppress ENOENT (file already gone, e.g. .OBSIDIANTEST race)
|
||||||
transport.unlink(path).catch((e) => {
|
transport.unlink(path).catch((e) => {
|
||||||
console.error('[shim:fs] unlinkSync background delete failed:', path, e);
|
if (e.code !== "ENOENT") {
|
||||||
|
console.error(
|
||||||
|
"[shim:fs] unlinkSync background delete failed:",
|
||||||
|
path,
|
||||||
|
e,
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
readdirSync(path) {
|
readdirSync(path) {
|
||||||
const entries = metadataCache.readdir(path);
|
const entries = metadataCache.readdir(path);
|
||||||
return entries.map(e => e.name);
|
return entries.map((e) => e.name);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,16 @@ function normPath(p) {
|
||||||
return (p || "").replace(/^\/+/, "");
|
return (p || "").replace(/^\/+/, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert a Uint8Array to base64 without blowing the stack
|
||||||
|
function uint8ToBase64(bytes) {
|
||||||
|
let binary = "";
|
||||||
|
const chunk = 8192;
|
||||||
|
for (let i = 0; i < bytes.length; i += chunk) {
|
||||||
|
binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk));
|
||||||
|
}
|
||||||
|
return btoa(binary);
|
||||||
|
}
|
||||||
|
|
||||||
async function request(method, endpoint, params = {}) {
|
async function request(method, endpoint, params = {}) {
|
||||||
const url = new URL(API_BASE + endpoint, window.location.origin);
|
const url = new URL(API_BASE + endpoint, window.location.origin);
|
||||||
|
|
||||||
|
|
@ -109,7 +119,7 @@ export const transport = {
|
||||||
const isText = typeof content === "string";
|
const isText = typeof content === "string";
|
||||||
return requestJson("POST", "/writeFile", {
|
return requestJson("POST", "/writeFile", {
|
||||||
path: normPath(path),
|
path: normPath(path),
|
||||||
content: isText ? content : btoa(String.fromCharCode(...content)),
|
content: isText ? content : uint8ToBase64(content),
|
||||||
encoding: encoding || (isText ? "utf-8" : "binary"),
|
encoding: encoding || (isText ? "utf-8" : "binary"),
|
||||||
base64: !isText,
|
base64: !isText,
|
||||||
});
|
});
|
||||||
|
|
@ -197,7 +207,7 @@ export const transport = {
|
||||||
const isText = typeof content === "string";
|
const isText = typeof content === "string";
|
||||||
requestSync("POST", "/writeFile", {
|
requestSync("POST", "/writeFile", {
|
||||||
path: normPath(path),
|
path: normPath(path),
|
||||||
content: isText ? content : btoa(String.fromCharCode(...content)),
|
content: isText ? content : uint8ToBase64(content),
|
||||||
encoding: encoding || (isText ? "utf-8" : "binary"),
|
encoding: encoding || (isText ? "utf-8" : "binary"),
|
||||||
base64: !isText,
|
base64: !isText,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,19 @@ window.close = function () {
|
||||||
console.log("[obsidian-bridge] window.close() blocked");
|
console.log("[obsidian-bridge] window.close() blocked");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Suppress the browser's native context menu without breaking Obsidian's.
|
||||||
|
// Problem: preventDefault() blocks the browser menu but also sets
|
||||||
|
// event.defaultPrevented=true, which Obsidian checks to bail out.
|
||||||
|
// Solution: call preventDefault() then shadow defaultPrevented to return false.
|
||||||
|
window.addEventListener(
|
||||||
|
"contextmenu",
|
||||||
|
(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
Object.defineProperty(e, "defaultPrevented", { get: () => false });
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
// Pre-populate fs metadata cache synchronously before app.js runs.
|
// Pre-populate fs metadata cache synchronously before app.js runs.
|
||||||
// This ensures existsSync() works for the vault path during startup.
|
// This ensures existsSync() works for the vault path during startup.
|
||||||
(function initMetadataCache() {
|
(function initMetadataCache() {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue