consolidate build scripts, reorganize source into src/ directory, fix favicon injection

This commit is contained in:
Nystik
2026-03-20 23:46:17 +01:00
parent 2add5238b8
commit 0747a4540d
56 changed files with 46 additions and 45 deletions

View File

@@ -0,0 +1,47 @@
import { ipcRenderer } from "./ipc-renderer.js";
import { webFrame } from "./web-frame.js";
import { remoteShim } from "./remote/index.js";
export const electronShim = {
ipcRenderer,
webFrame,
remote: remoteShim,
safeStorage: {
isEncryptionAvailable() {
return false;
},
encryptString(plainText) {
return Buffer.from(plainText);
},
decryptString(encrypted) {
return encrypted.toString();
},
},
webUtils: {
getPathForFile(file) {
return "";
},
},
deprecate: {
function(fn, name) {
return fn;
},
event(emitter, name) {},
removeFunction(fn, name) {
return fn;
},
log(message) {
console.log("[electron:deprecate]", message);
},
warn(oldName, newName) {},
promisify(fn) {
return fn;
},
renameFunction(fn, newName) {
return fn;
},
},
};

View File

@@ -0,0 +1,260 @@
import { showVaultManager } from "../../ui/bootstrap.js";
import { vaultService } from "../../services/vault-service.js";
const listeners = new Map();
const syncHandlers = {
vault: () => window.__vaultConfig || { id: "default-vault", path: "/" },
version: () => window.__obsidianVersion || "0.0.0",
"is-dev": () => false,
"file-url": () =>
"/vault-files/" + encodeURIComponent(window.__currentVaultId || "") + "/",
"disable-update": () => true,
update: () => "",
"disable-gpu": () => false,
frame: () => null,
"set-icon": () => null,
"get-icon": () => null,
relaunch: () => {
window.location.reload();
return null;
},
starter: () => {
showVaultManager();
return null;
},
help: () => {
window.open("https://help.obsidian.md/", "_blank");
return null;
},
sandbox: () => null,
"copy-asar": () => false,
"check-update": () => null,
"vault-list": () => {
const result = {};
for (const v of window.__vaultList || []) {
result[v.id] = {
path: "/" + v.id,
ts: Date.now(),
open: v.id === vaultService.getCurrentVaultId(),
};
}
return result;
},
"vault-open": (vaultPath, newWindow) => {
const id = (vaultPath || "").replace(/^\/+/, "");
const vault = (window.__vaultList || []).find((v) => v.id === id);
if (!vault && id) {
if (!vaultService.createVaultSync(id)) {
return "Failed to create vault";
}
}
vaultService.openVault(id);
return true;
},
"vault-remove": (vaultPath) => {
const id = (vaultPath || "").replace(/^\/+/, "");
return vaultService.deleteVaultSync(id);
},
"vault-move": (oldPath, newPath) => {
return "Moving vaults is not supported in the web version";
},
"vault-message": () => null,
"get-default-vault-path": () => "/My Vault",
"get-documents-path": () => "/",
"desktop-dir": () => "/desktop",
"documents-dir": () => "/documents",
resources: () => "",
};
function arrayBufferToBase64(buf) {
const bytes = new Uint8Array(buf);
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);
}
function base64ToArrayBuffer(base64) {
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes.buffer;
}
async function handleRequestUrl(requestId, request) {
try {
let body = request.body;
let binary = false;
if (body instanceof ArrayBuffer) {
body = arrayBufferToBase64(body);
binary = true;
}
const res = await fetch("/api/proxy", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
url: request.url,
method: request.method || "GET",
headers: request.headers || {},
contentType: request.contentType,
body,
binary,
}),
});
const proxyResult = await res.json();
if (!res.ok) {
ipcRenderer._emit(requestId, {
error: proxyResult.error || "Proxy request failed",
});
return;
}
// Electron's e.reply(requestId, data) sends on the requestId channel
ipcRenderer._emit(requestId, {
status: proxyResult.status,
headers: proxyResult.headers,
body: base64ToArrayBuffer(proxyResult.body),
});
} catch (e) {
ipcRenderer._emit(requestId, {
error: e.message,
});
}
}
export const ipcRenderer = {
send(channel, ...args) {
console.log("[shim:ipcRenderer] send:", channel, args);
if (channel === "context-menu") {
queueMicrotask(() =>
ipcRenderer._emit("context-menu", {
webContentsId: 1,
editFlags: { canCut: true, canCopy: true, canPaste: true },
}),
);
return;
}
if (channel === "request-url") {
const [requestId, request] = args;
handleRequestUrl(requestId, request);
return;
}
if (channel === "print-to-pdf") {
const iframe = window.__popupIframe;
if (iframe) {
setTimeout(() => {
iframe.contentWindow.print();
setTimeout(() => {
iframe.contentWindow.close();
ipcRenderer._emit("print-to-pdf", { success: true });
}, 500);
}, 200);
} else {
window.print();
queueMicrotask(() => {
ipcRenderer._emit("print-to-pdf", { success: true });
});
}
return;
}
},
sendSync(channel, ...args) {
console.log("[shim:ipcRenderer] sendSync:", channel, args);
if (syncHandlers[channel]) {
return syncHandlers[channel](...args);
}
console.warn("[shim:ipcRenderer] Unhandled sendSync channel:", channel);
return null;
},
on(channel, listener) {
if (!listeners.has(channel)) {
listeners.set(channel, []);
}
listeners.get(channel).push(listener);
return ipcRenderer;
},
once(channel, listener) {
const wrapped = (...args) => {
ipcRenderer.removeListener(channel, wrapped);
listener(...args);
};
return ipcRenderer.on(channel, wrapped);
},
removeListener(channel, listener) {
const arr = listeners.get(channel);
if (arr) {
const idx = arr.indexOf(listener);
if (idx >= 0) {
arr.splice(idx, 1);
}
}
return ipcRenderer;
},
removeAllListeners(channel) {
if (channel) {
listeners.delete(channel);
} else {
listeners.clear();
}
return ipcRenderer;
},
_emit(channel, ...args) {
const arr = listeners.get(channel);
if (arr) {
for (const fn of arr) {
fn({}, ...args);
}
}
},
};

View File

@@ -0,0 +1,43 @@
export const appShim = {
getPath(name) {
const paths = {
userData: "/.obsidian",
home: "/",
documents: "/documents",
desktop: "/desktop",
temp: "/tmp",
appData: "/.obsidian",
};
return paths[name] || "/";
},
getVersion() {
return window.__obsidianVersion || "0.0.0";
},
getName() {
return "Obsidian";
},
getLocale() {
return navigator.language || "en-US";
},
isPackaged: true,
quit() {
console.log("[shim:app] quit (stub)");
},
relaunch() {
window.location.reload();
},
whenReady() {
return Promise.resolve();
},
on() {},
once() {},
removeListener() {},
};

View File

@@ -0,0 +1,40 @@
// stub
export const clipboardShim = {
readText() {
return "";
},
writeText(text) {
navigator.clipboard.writeText(text).catch((e) => {
console.warn("[shim:clipboard] writeText failed:", e);
});
},
readHTML() {
return "";
},
writeHTML(html) {
console.log("[shim:clipboard] writeHTML (stub)");
},
readImage() {
return { isEmpty: () => true, toPNG: () => new Uint8Array(0) };
},
writeImage(image) {
console.log("[shim:clipboard] writeImage (stub)");
},
has(format) {
return false;
},
read(format) {
return "";
},
clear() {
navigator.clipboard.writeText("").catch(() => {});
},
};

View File

@@ -0,0 +1,70 @@
import {
showMessageDialog,
showConfirmDialog,
showPromptDialog,
} from "../../../ui/bootstrap.js";
export const dialogShim = {
async showOpenDialog(browserWindow, options) {
// TODO: implement custom modal with server-side file listing
console.log("[shim:dialog] showOpenDialog (stub):", options);
return { canceled: true, filePaths: [] };
},
async showSaveDialog(browserWindow, options) {
if (typeof browserWindow === "object" && !options) {
options = browserWindow;
}
const defaultName =
options?.defaultPath?.split(/[\/\\]/).pop() || "download";
const name = await showPromptDialog(
"Save File",
"Save as:",
"filename",
defaultName,
"Save",
);
if (!name) {
return { canceled: true, filePath: undefined };
}
return { canceled: false, filePath: "/downloads/" + name };
},
async showMessageBox(browserWindow, options) {
if (typeof browserWindow === "object" && !options) {
options = browserWindow;
}
console.log("[shim:dialog] showMessageBox:", options);
const message = options.message || "";
const detail = options.detail || "";
const buttons = options.buttons || ["OK"];
const fullMessage = message + (detail ? "\n\n" + detail : "");
if (buttons.length <= 1) {
await showMessageDialog(options.title || "Message", fullMessage);
return { response: 0, checkboxChecked: false };
}
const result = await showConfirmDialog(
options.title || "Confirm",
message,
detail,
buttons[0],
);
return {
response: result ? 0 : 1,
checkboxChecked: false,
};
},
showErrorBox(title, content) {
console.error("[shim:dialog] Error:", title, content);
showMessageDialog(title, content);
},
};

View File

@@ -0,0 +1,50 @@
import { clipboardShim } from "./clipboard.js";
import { shellShim } from "./shell.js";
import { dialogShim } from "./dialog.js";
import { menuShim, menuItemShim } from "./menu.js";
import { appShim } from "./app.js";
import { windowShim, webContentsShim } from "./window.js";
import { themeShim } from "./theme.js";
import { sessionShim } from "./session.js";
import { systemPreferencesShim } from "./system-preferences.js";
import { screenShim } from "./screen.js";
import { nativeImageShim } from "./native-image.js";
import { notificationShim } from "./notification.js";
export const remoteShim = {
clipboard: clipboardShim,
shell: shellShim,
dialog: dialogShim,
Menu: menuShim,
MenuItem: menuItemShim,
app: appShim,
BrowserWindow: windowShim,
nativeTheme: themeShim,
session: sessionShim,
systemPreferences: systemPreferencesShim,
screen: screenShim,
nativeImage: nativeImageShim,
Notification: notificationShim,
safeStorage: {
isEncryptionAvailable() {
return false;
},
encryptString(plainText) {
return Buffer.from(plainText);
},
decryptString(encrypted) {
return encrypted.toString();
},
},
getCurrentWindow() {
return windowShim._current();
},
webContents: webContentsShim,
getCurrentWebContents() {
return webContentsShim._current();
},
};

View File

@@ -0,0 +1,53 @@
export class menuShim {
constructor() {
this.items = [];
}
static buildFromTemplate(template) {
const menu = new menuShim();
menu.items = (template || []).map((item) => new menuItemShim(item));
return menu;
}
static setApplicationMenu(menu) {
console.log("[shim:Menu] setApplicationMenu (stub)");
}
static getApplicationMenu() {
return null;
}
popup(options) {
console.log("[shim:Menu] popup (stub)", options);
}
append(menuItem) {
this.items.push(menuItem);
}
insert(pos, menuItem) {
this.items.splice(pos, 0, menuItem);
}
closePopup() {}
}
export class menuItemShim {
constructor(options = {}) {
this.label = options.label || "";
this.type = options.type || "normal";
this.click = options.click || null;
this.role = options.role || null;
this.accelerator = options.accelerator || "";
this.enabled = options.enabled !== false;
this.visible = options.visible !== false;
this.checked = !!options.checked;
this.submenu = options.submenu
? menuShim.buildFromTemplate(
Array.isArray(options.submenu) ? options.submenu : [],
)
: null;
this.id = options.id || "";
}
}

View File

@@ -0,0 +1,20 @@
export const nativeImageShim = {
createFromBuffer(buffer) {
return {
isEmpty: () => !buffer || buffer.length === 0,
getSize: () => ({ width: 0, height: 0 }),
toPNG: () => buffer || new Uint8Array(0),
toJPEG: (quality) => buffer || new Uint8Array(0),
toDataURL: () => "",
};
},
createFromPath(filePath) {
// TODO: could fetch from server and create image
return nativeImageShim.createFromBuffer(new Uint8Array(0));
},
createEmpty() {
return nativeImageShim.createFromBuffer(new Uint8Array(0));
},
};

View File

@@ -0,0 +1,37 @@
export class notificationShim {
constructor(options = {}) {
this.title = options.title || "";
this.body = options.body || "";
this.silent = options.silent || false;
this._handlers = {};
}
show() {
if ("Notification" in window && Notification.permission === "granted") {
new Notification(this.title, { body: this.body, silent: this.silent });
} else if (
"Notification" in window &&
Notification.permission !== "denied"
) {
Notification.requestPermission().then((perm) => {
if (perm === "granted") {
new Notification(this.title, {
body: this.body,
silent: this.silent,
});
}
});
}
}
close() {}
on(event, handler) {
this._handlers[event] = handler;
return this;
}
static isSupported() {
return "Notification" in window;
}
}

View File

@@ -0,0 +1,40 @@
export const screenShim = {
getPrimaryDisplay() {
return {
workAreaSize: {
width: window.screen.availWidth,
height: window.screen.availHeight,
},
size: { width: window.screen.width, height: window.screen.height },
scaleFactor: window.devicePixelRatio || 1,
bounds: {
x: 0,
y: 0,
width: window.screen.width,
height: window.screen.height,
},
workArea: {
x: 0,
y: 0,
width: window.screen.availWidth,
height: window.screen.availHeight,
},
};
},
getAllDisplays() {
return [screenShim.getPrimaryDisplay()];
},
getDisplayNearestPoint(point) {
return screenShim.getPrimaryDisplay();
},
getCursorScreenPoint() {
return { x: 0, y: 0 };
},
on() {},
once() {},
removeListener() {},
};

View File

@@ -0,0 +1,20 @@
export const sessionShim = {
defaultSession: {
clearCache() {
return Promise.resolve();
},
clearStorageData() {
return Promise.resolve();
},
setSpellCheckerLanguages(langs) {},
getSpellCheckerLanguages() {
return [];
},
on() {},
once() {},
removeListener() {},
},
};

View File

@@ -0,0 +1,15 @@
export const shellShim = {
openExternal(url) {
window.open(url, "_blank");
return Promise.resolve();
},
openPath(filePath) {
console.log("[shim:shell] openPath (stub):", filePath);
return Promise.resolve("");
},
showItemInFolder(filePath) {
console.log("[shim:shell] showItemInFolder (stub):", filePath);
},
};

View File

@@ -0,0 +1,21 @@
export const systemPreferencesShim = {
getAccentColor() {
return "0078d4"; // Default Windows accent blue
},
isAeroGlassEnabled() {
return false;
},
getMediaAccessStatus(mediaType) {
return "granted";
},
askForMediaAccess(mediaType) {
return Promise.resolve(true);
},
on() {},
once() {},
removeListener() {},
};

View File

@@ -0,0 +1,64 @@
const listeners = [];
const darkQuery =
typeof window !== "undefined"
? window.matchMedia("(prefers-color-scheme: dark)")
: null;
if (darkQuery?.addEventListener) {
darkQuery.addEventListener("change", () => {
for (const fn of listeners) {
fn();
}
});
}
export const themeShim = {
get shouldUseDarkColors() {
return darkQuery ? darkQuery.matches : true;
},
get themeSource() {
return "system";
},
set themeSource(val) {
// No-op in browser; theme is controlled by OS
},
on(event, callback) {
if (event === "updated") {
listeners.push(callback);
}
return themeShim;
},
once(event, callback) {
if (event === "updated") {
const wrapped = () => {
const idx = listeners.indexOf(wrapped);
if (idx >= 0) {
listeners.splice(idx, 1);
}
callback();
};
listeners.push(wrapped);
}
return themeShim;
},
removeListener(event, callback) {
const idx = listeners.indexOf(callback);
if (idx >= 0) {
listeners.splice(idx, 1);
}
return themeShim;
},
removeAllListeners() {
listeners.length = 0;
return themeShim;
},
};

View File

@@ -0,0 +1,371 @@
const currentWindowState = {
title: "Obsidian",
isMaximized: false,
isMinimized: false,
isFullScreen: false,
isAlwaysOnTop: false,
bounds: { x: 0, y: 0, width: window.innerWidth, height: window.innerHeight },
focusTime: Date.now(),
};
const currentWindow = {
isMaximized: () => currentWindowState.isMaximized,
isMinimized: () => currentWindowState.isMinimized,
isFullScreen: () => !!document.fullscreenElement,
isAlwaysOnTop: () => currentWindowState.isAlwaysOnTop,
isFocused: () => document.hasFocus(),
isVisible: () => true,
isDestroyed: () => false,
minimize() {
console.log("[shim:window] minimize (stub)");
},
maximize() {
currentWindowState.isMaximized = true;
},
unmaximize() {
currentWindowState.isMaximized = false;
},
restore() {
currentWindowState.isMinimized = false;
},
close() {
console.log("[shim:window] close (stub)");
},
focus() {
window.focus();
},
show() {},
hide() {},
setTitle(title) {
currentWindowState.title = title;
document.title = title;
},
getTitle() {
return currentWindowState.title;
},
setAlwaysOnTop(flag) {
currentWindowState.isAlwaysOnTop = flag;
},
setFullScreen(flag) {
if (flag) {
document.documentElement.requestFullscreen?.();
} else {
document.exitFullscreen?.();
}
},
getBounds() {
return {
x: window.screenX,
y: window.screenY,
width: window.innerWidth,
height: window.innerHeight,
};
},
setBounds(bounds) {
console.log("[shim:window] setBounds (stub):", bounds);
},
setSize(width, height) {},
setPosition(x, y) {},
center() {},
setTrafficLightPosition() {},
setWindowButtonPosition() {},
get webContents() {
return webContentsShim._current();
},
get menuBarVisible() {
return false;
},
set menuBarVisible(v) {},
get loaded() {
return true;
},
set loaded(v) {},
get focusTime() {
return currentWindowState.focusTime;
},
set focusTime(v) {
currentWindowState.focusTime = v;
},
on(event, handler) {
if (event === "focus") {
window.addEventListener("focus", handler);
} else if (event === "blur") {
window.addEventListener("blur", handler);
} else if (event === "resize") {
window.addEventListener("resize", handler);
}
return currentWindow;
},
once(event, handler) {
if (event === "focus") {
window.addEventListener("focus", handler, { once: true });
}
return currentWindow;
},
removeListener() {
return currentWindow;
},
removeAllListeners() {
return currentWindow;
},
};
const currentWebContents = {
id: 1,
_zoomLevel: 0,
get zoomLevel() {
return this._zoomLevel;
},
set zoomLevel(v) {
this._zoomLevel = v;
},
executeJavaScript(code) {
try {
return Promise.resolve(eval(code));
} catch (e) {
return Promise.reject(e);
}
},
getZoomFactor() {
return Math.pow(1.2, this._zoomLevel);
},
getZoomLevel() {
return this._zoomLevel;
},
setZoomLevel(v) {
this._zoomLevel = v;
},
isDevToolsOpened() {
return false;
},
openDevTools() {},
setWindowOpenHandler(handler) {
this._windowOpenHandler = handler;
},
printToPDF(options) {
return new Promise((resolve) => {
window.print();
resolve(Buffer.from([]));
});
},
capturePage(rect) {
// TODO: could use html2canvas
console.log("[shim:webContents] capturePage (stub)");
return Promise.resolve({
toPNG: () => new Uint8Array(0),
toJPEG: () => new Uint8Array(0),
});
},
undo() {},
redo() {},
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) {},
on(event, handler) {
return currentWebContents;
},
once(event, handler) {
return currentWebContents;
},
removeListener() {
return currentWebContents;
},
get isSecured() {
return true;
},
set isSecured(v) {},
};
// Popup tracking for PDF export etc.
let _popupWindow = null;
let _popupWebContents = null;
export function registerPopupWindow() {
_popupWebContents = {
id: 2,
_zoomLevel: 0,
getZoomFactor() {
return 1;
},
getZoomLevel() {
return 0;
},
setZoomLevel() {},
printToPDF(options) {
return Promise.resolve(Buffer.from([]));
},
executeJavaScript(code) {
try {
return Promise.resolve(eval(code));
} catch (e) {
return Promise.reject(e);
}
},
on() {
return _popupWebContents;
},
once() {
return _popupWebContents;
},
removeListener() {
return _popupWebContents;
},
isDestroyed() {
return false;
},
isFocused() {
return false;
},
};
_popupWindow = {
id: 2,
webContents: _popupWebContents,
isDestroyed() {
return false;
},
isFocused() {
return false;
},
isVisible() {
return false;
},
close() {
_popupWindow = null;
_popupWebContents = null;
},
destroy() {
_popupWindow = null;
_popupWebContents = null;
},
on() {
return _popupWindow;
},
once() {
return _popupWindow;
},
removeListener() {
return _popupWindow;
},
};
return _popupWindow;
}
export function unregisterPopupWindow() {
_popupWindow = null;
_popupWebContents = null;
}
export const windowShim = {
_current: () => currentWindow,
getFocusedWindow() {
return currentWindow;
},
getAllWindows() {
const wins = [currentWindow];
if (_popupWindow) {
wins.push(_popupWindow);
}
return wins;
},
fromId(id) {
if (id === currentWindow.id) {
return currentWindow;
}
if (_popupWindow && id === _popupWindow.id) {
return _popupWindow;
}
return null;
},
fromWebContents(wc) {
if (wc === currentWebContents) {
return currentWindow;
}
if (_popupWebContents && wc === _popupWebContents) {
return _popupWindow;
}
return null;
},
};
export const webContentsShim = {
_current: () => currentWebContents,
fromId(id) {
if (id === currentWebContents.id) {
return currentWebContents;
}
if (_popupWebContents && id === _popupWebContents.id) {
return _popupWebContents;
}
return null;
},
getAllWebContents() {
const wcs = [currentWebContents];
if (_popupWebContents) {
wcs.push(_popupWebContents);
}
return wcs;
},
};

View File

@@ -0,0 +1,24 @@
let currentZoom = 0;
export const webFrame = {
getZoomLevel() {
return currentZoom;
},
setZoomLevel(level) {
currentZoom = level;
// Approximate Electron's zoom behavior via CSS zoom
// Electron zoom level 0 = 100%, each step is ~20%
const scale = Math.pow(1.2, level);
document.body.style.zoom = scale;
},
getZoomFactor() {
return Math.pow(1.2, currentZoom);
},
setZoomFactor(factor) {
currentZoom = Math.log(factor) / Math.log(1.2);
document.body.style.zoom = factor;
},
};