Merge branch 'main' into filewatcher
This commit is contained in:
commit
77ec29e250
29 changed files with 1468 additions and 283 deletions
55
CHANGELOG.md
55
CHANGELOG.md
|
|
@ -2,7 +2,60 @@
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
## [0.4.0] - Basil (2026-03-18)
|
## [0.6.1] - Slifer (2026-03-24)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `fetch()` shim that proxies cross-origin requests through `/api/proxy` to bypass CORS restrictions
|
||||||
|
- Automatic `Origin: app://obsidian.md` header injection for cross-origin requests to match Obsidian desktop app
|
||||||
|
- User-Agent forwarding from browser to proxy for cross-origin requests
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Obsidian Sync API authentication now works in browser (was blocked by CORS)
|
||||||
|
- Proxy response headers cleaned to exclude hop-by-hop headers (`content-encoding`, `transfer-encoding`, `content-length`, `connection`)
|
||||||
|
|
||||||
|
## [0.6.0] - Slifer (2026-03-23)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `zlib` shim using `pako` library for compression/decompression operations (deflate, inflate, gzip, gunzip, etc.)
|
||||||
|
- File descriptor operations: `fs.open()`, `fs.read()`, `fs.close()`, `fs.fstat()` and sync variants
|
||||||
|
- `fs.promises.open()` returning FileHandle objects with `stat()`, `read()`, `close()` methods
|
||||||
|
- `showOpenDialog` electron dialog shim with browser file picker and vault upload
|
||||||
|
- `showOpenDialogSync` hacky workaround using file staging registry and two-step upload flow
|
||||||
|
- Enhanced `Buffer` shim with `alloc()`, `allocUnsafe()`, `byteLength()`, and `isEncoding()` methods
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- `MessageDialog` modal dismiss error when confirm button clicked
|
||||||
|
- Dialog shim modal event ordering to prevent null reference errors
|
||||||
|
|
||||||
|
## [0.5.0] - Scatha (2026-03-22)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Compression middleware (gzip/brotli) for API responses to reduce bandwidth
|
||||||
|
- Plugin installation prompt system with per-vault trust flags
|
||||||
|
- Versioning system with cache-busting query parameters on script URLs
|
||||||
|
- Option to install ignis-bridge plugin to vaults imported at runtime
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Auto-creation of default vault now requires `AUTO_CREATE_DEFAULT=true` environment variable
|
||||||
|
- Script URLs (`ignis-ui.js`, `shim-loader.js`) now include version query params for automatic cache invalidation
|
||||||
|
- Cache headers: versioned assets cached for 1 year, non-versioned for 5 minutes
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Vault manager not displaying when no vaults exist
|
||||||
|
- `window.close()` now shows vault manager when no vault is configured
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- Unused `VAULT_PATH` environment variable fallback logic
|
||||||
|
|
||||||
|
## [0.4.0] - Gostir (2026-03-18)
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
|
|
||||||
60
package-lock.json
generated
60
package-lock.json
generated
|
|
@ -1,16 +1,18 @@
|
||||||
{
|
{
|
||||||
"name": "ignis",
|
"name": "ignis",
|
||||||
"version": "0.1.0",
|
"version": "0.5.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ignis",
|
"name": "ignis",
|
||||||
"version": "0.1.0",
|
"version": "0.5.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chokidar": "^3.6.0",
|
"chokidar": "^3.6.0",
|
||||||
|
"compression": "^1.7.4",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^4.21.0",
|
"express": "^4.21.0",
|
||||||
|
"pako": "^2.1.0",
|
||||||
"ws": "^8.16.0"
|
"ws": "^8.16.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
@ -661,6 +663,45 @@
|
||||||
"periscopic": "^3.1.0"
|
"periscopic": "^3.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/compressible": {
|
||||||
|
"version": "2.0.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
|
||||||
|
"integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": ">= 1.43.0 < 2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/compression": {
|
||||||
|
"version": "1.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
|
||||||
|
"integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bytes": "3.1.2",
|
||||||
|
"compressible": "~2.0.18",
|
||||||
|
"debug": "2.6.9",
|
||||||
|
"negotiator": "~0.6.4",
|
||||||
|
"on-headers": "~1.1.0",
|
||||||
|
"safe-buffer": "5.2.1",
|
||||||
|
"vary": "~1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/compression/node_modules/negotiator": {
|
||||||
|
"version": "0.6.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
|
||||||
|
"integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/content-disposition": {
|
"node_modules/content-disposition": {
|
||||||
"version": "0.5.4",
|
"version": "0.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||||
|
|
@ -1357,6 +1398,21 @@
|
||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/on-headers": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pako": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
|
||||||
|
"license": "(MIT AND Zlib)"
|
||||||
|
},
|
||||||
"node_modules/parseurl": {
|
"node_modules/parseurl": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "ignis",
|
"name": "ignis",
|
||||||
"version": "0.4.0",
|
"version": "0.6.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "An Electron shim and server bridge for running Obsidian in a browser.",
|
"description": "An Electron shim and server bridge for running Obsidian in a browser.",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
@ -10,8 +10,10 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chokidar": "^3.6.0",
|
"chokidar": "^3.6.0",
|
||||||
|
"compression": "^1.7.4",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^4.21.0",
|
"express": "^4.21.0",
|
||||||
|
"pako": "^2.1.0",
|
||||||
"ws": "^8.16.0"
|
"ws": "^8.16.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
const { getVersion } = require("../server/version");
|
||||||
|
|
||||||
const asarDir = process.argv[2];
|
const asarDir = process.argv[2];
|
||||||
if (!asarDir) {
|
if (!asarDir) {
|
||||||
|
|
@ -13,7 +14,7 @@ if (!asarDir) {
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function patchHtml(filePath) {
|
function patchHtml(filePath, version) {
|
||||||
const backupPath = filePath + ".orig";
|
const backupPath = filePath + ".orig";
|
||||||
|
|
||||||
if (!fs.existsSync(filePath) && !fs.existsSync(backupPath)) {
|
if (!fs.existsSync(filePath) && !fs.existsSync(backupPath)) {
|
||||||
|
|
@ -46,8 +47,8 @@ function patchHtml(filePath) {
|
||||||
// Inject ignis scripts before the first <script> tag
|
// Inject ignis scripts before the first <script> tag
|
||||||
html = html.replace(
|
html = html.replace(
|
||||||
'<script type="text/javascript"',
|
'<script type="text/javascript"',
|
||||||
'<script type="text/javascript" src="ignis-ui.js"></script>\n' +
|
`<script type="text/javascript" src="ignis-ui.js?v=${version}"></script>\n` +
|
||||||
'<script type="text/javascript" src="shim-loader.js"></script>\n' +
|
`<script type="text/javascript" src="shim-loader.js?v=${version}"></script>\n` +
|
||||||
'<script type="text/javascript"',
|
'<script type="text/javascript"',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -55,4 +56,6 @@ function patchHtml(filePath) {
|
||||||
console.log(`[patch] Patched ${filePath}`);
|
console.log(`[patch] Patched ${filePath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
patchHtml(path.join(asarDir, "index.html"));
|
const version = getVersion();
|
||||||
|
patchHtml(path.join(asarDir, "index.html"), version);
|
||||||
|
console.log(`[patch] Injected version: ${version}`);
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,8 @@ const fs = require("fs");
|
||||||
|
|
||||||
// VAULT_ROOT: a directory that contains vault folders.
|
// VAULT_ROOT: a directory that contains vault folders.
|
||||||
// Each subdirectory is a vault. New vaults are created as new subdirs.
|
// Each subdirectory is a vault. New vaults are created as new subdirs.
|
||||||
// Falls back to parent of VAULT_PATH (single-vault compatibility) or ./vaults.
|
|
||||||
const vaultRoot =
|
const vaultRoot =
|
||||||
process.env.VAULT_ROOT ||
|
process.env.VAULT_ROOT || path.join(__dirname, "..", "vaults");
|
||||||
(process.env.VAULT_PATH
|
|
||||||
? path.dirname(process.env.VAULT_PATH)
|
|
||||||
: path.join(__dirname, "..", "vaults"));
|
|
||||||
|
|
||||||
// Ensure vault root exists
|
// Ensure vault root exists
|
||||||
try {
|
try {
|
||||||
|
|
@ -32,8 +28,11 @@ function discoverVaults() {
|
||||||
console.error("[config] Failed to read VAULT_ROOT:", vaultRoot, e.message);
|
console.error("[config] Failed to read VAULT_ROOT:", vaultRoot, e.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a default vault if none exist
|
// Optionally create a default vault if none exist
|
||||||
if (Object.keys(vaults).length === 0) {
|
if (
|
||||||
|
Object.keys(vaults).length === 0 &&
|
||||||
|
process.env.AUTO_CREATE_DEFAULT === "true"
|
||||||
|
) {
|
||||||
const defaultPath = path.join(vaultRoot, "My Vault");
|
const defaultPath = path.join(vaultRoot, "My Vault");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
const express = require("express");
|
const express = require("express");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
const compression = require("compression");
|
||||||
const config = require("./config");
|
const config = require("./config");
|
||||||
const { setupWebSocket } = require("./ws");
|
const { setupWebSocket } = require("./ws");
|
||||||
const { installPluginInAllVaults } = require("./install-plugin");
|
const { installPluginInAllVaults } = require("./install-plugin");
|
||||||
|
|
@ -12,6 +13,7 @@ const ANSI_RESET = "\x1b[0m";
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
app.use(express.json({ limit: "50mb" }));
|
app.use(express.json({ limit: "50mb" }));
|
||||||
|
app.use(compression());
|
||||||
|
|
||||||
// logger middleware
|
// logger middleware
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
|
|
@ -43,10 +45,12 @@ app.use((req, res, next) => {
|
||||||
const fsRoutes = require("./routes/fs");
|
const fsRoutes = require("./routes/fs");
|
||||||
const vaultRoutes = require("./routes/vault");
|
const vaultRoutes = require("./routes/vault");
|
||||||
const proxyRoutes = require("./routes/proxy");
|
const proxyRoutes = require("./routes/proxy");
|
||||||
|
const versionRoutes = require("./routes/version");
|
||||||
|
|
||||||
app.use("/api/fs", fsRoutes);
|
app.use("/api/fs", fsRoutes);
|
||||||
app.use("/api/vault", vaultRoutes);
|
app.use("/api/vault", vaultRoutes);
|
||||||
app.use("/api/proxy", proxyRoutes);
|
app.use("/api/proxy", proxyRoutes);
|
||||||
|
app.use("/api/version", versionRoutes);
|
||||||
|
|
||||||
// Serve vault files for resource URLs (images, attachments, etc.)
|
// Serve vault files for resource URLs (images, attachments, etc.)
|
||||||
// Vault ID is the first path segment: /vault-files/<vault-id>/path/to/file
|
// Vault ID is the first path segment: /vault-files/<vault-id>/path/to/file
|
||||||
|
|
@ -70,6 +74,20 @@ app.use("/vault-files", (req, res, next) => {
|
||||||
express.static(vaultPath)(req, res, next);
|
express.static(vaultPath)(req, res, next);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Serve dist files with cache headers based on version param
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
if (req.path.match(/\/(ignis-ui|shim-loader)\.js$/)) {
|
||||||
|
if (req.query.v) {
|
||||||
|
// Versioned assets - cache for 1 year
|
||||||
|
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
|
||||||
|
} else {
|
||||||
|
// No version param - short cache for dev/fallback
|
||||||
|
res.setHeader("Cache-Control", "public, max-age=300");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
app.use(express.static(path.join(__dirname, "..", "dist")));
|
app.use(express.static(path.join(__dirname, "..", "dist")));
|
||||||
|
|
||||||
app.use(express.static(config.obsidianAssetsPath));
|
app.use(express.static(config.obsidianAssetsPath));
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,46 @@
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
|
// .ignis metadata helpers
|
||||||
|
async function getIgnisMeta(vaultPath) {
|
||||||
|
const ignisDir = path.join(vaultPath, ".ignis");
|
||||||
|
const metaFile = path.join(ignisDir, "meta.json");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const content = await fs.promises.readFile(metaFile, "utf-8");
|
||||||
|
return JSON.parse(content);
|
||||||
|
} catch (e) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setIgnisMeta(vaultPath, data) {
|
||||||
|
const ignisDir = path.join(vaultPath, ".ignis");
|
||||||
|
const metaFile = path.join(ignisDir, "meta.json");
|
||||||
|
|
||||||
|
await fs.promises.mkdir(ignisDir, { recursive: true });
|
||||||
|
await fs.promises.writeFile(metaFile, JSON.stringify(data, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkPluginInstalled(vaultPath) {
|
||||||
|
const pluginDir = path.join(
|
||||||
|
vaultPath,
|
||||||
|
".obsidian",
|
||||||
|
"plugins",
|
||||||
|
"ignis-bridge",
|
||||||
|
);
|
||||||
|
const manifestPath = path.join(pluginDir, "manifest.json");
|
||||||
|
const mainPath = path.join(pluginDir, "main.js");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.promises.access(manifestPath);
|
||||||
|
await fs.promises.access(mainPath);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function installPluginInVault(vaultPath) {
|
async function installPluginInVault(vaultPath) {
|
||||||
const obsidianDir = path.join(vaultPath, ".obsidian");
|
const obsidianDir = path.join(vaultPath, ".obsidian");
|
||||||
const pluginDir = path.join(obsidianDir, "plugins", "ignis-bridge");
|
const pluginDir = path.join(obsidianDir, "plugins", "ignis-bridge");
|
||||||
|
|
@ -62,4 +102,10 @@ async function installPluginInAllVaults(vaultRoot) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { installPluginInVault, installPluginInAllVaults };
|
module.exports = {
|
||||||
|
installPluginInVault,
|
||||||
|
installPluginInAllVaults,
|
||||||
|
getIgnisMeta,
|
||||||
|
setIgnisMeta,
|
||||||
|
checkPluginInstalled,
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -28,10 +28,19 @@ router.post("/", async (req, res) => {
|
||||||
const upstream = await fetch(url, fetchOpts);
|
const upstream = await fetch(url, fetchOpts);
|
||||||
const respBody = Buffer.from(await upstream.arrayBuffer());
|
const respBody = Buffer.from(await upstream.arrayBuffer());
|
||||||
|
|
||||||
// Forward response headers
|
// Forward response headers, stripping hop-by-hop / encoding headers
|
||||||
|
// since the body is already decompressed by Node's fetch
|
||||||
|
const skipHeaders = new Set([
|
||||||
|
"content-encoding",
|
||||||
|
"transfer-encoding",
|
||||||
|
"content-length",
|
||||||
|
"connection",
|
||||||
|
]);
|
||||||
const respHeaders = {};
|
const respHeaders = {};
|
||||||
upstream.headers.forEach((val, key) => {
|
upstream.headers.forEach((val, key) => {
|
||||||
respHeaders[key] = val;
|
if (!skipHeaders.has(key)) {
|
||||||
|
respHeaders[key] = val;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,12 @@ const express = require("express");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const config = require("../config");
|
const config = require("../config");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
const {
|
||||||
|
checkPluginInstalled,
|
||||||
|
getIgnisMeta,
|
||||||
|
setIgnisMeta,
|
||||||
|
installPluginInVault,
|
||||||
|
} = require("../install-plugin");
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
|
@ -19,7 +25,7 @@ router.get("/list", (req, res) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /api/vault/info?vault=<id> - returns info for a specific vault
|
// GET /api/vault/info?vault=<id> - returns info for a specific vault
|
||||||
router.get("/info", (req, res) => {
|
router.get("/info", async (req, res) => {
|
||||||
const vaultId = req.query.vault || config.defaultVaultId;
|
const vaultId = req.query.vault || config.defaultVaultId;
|
||||||
const vaultPath = config.getVaultPath(vaultId);
|
const vaultPath = config.getVaultPath(vaultId);
|
||||||
|
|
||||||
|
|
@ -27,12 +33,19 @@ router.get("/info", (req, res) => {
|
||||||
return res.status(404).json({ error: "Vault not found", id: vaultId });
|
return res.status(404).json({ error: "Vault not found", id: vaultId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pluginInstalled = await checkPluginInstalled(vaultPath);
|
||||||
|
const ignisMeta = await getIgnisMeta(vaultPath);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
id: vaultId,
|
id: vaultId,
|
||||||
name: vaultId,
|
name: vaultId,
|
||||||
path: vaultPath,
|
path: vaultPath,
|
||||||
platform: process.platform,
|
platform: process.platform,
|
||||||
version: config.obsidianVersion,
|
version: config.obsidianVersion,
|
||||||
|
ignisPlugin: {
|
||||||
|
installed: pluginInstalled,
|
||||||
|
prompted: ignisMeta.pluginPrompted || false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -143,4 +156,42 @@ router.delete("/remove", async (req, res) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// POST /api/vault/install-plugin { vault, dismiss } - install plugin or mark as prompted
|
||||||
|
router.post("/install-plugin", async (req, res) => {
|
||||||
|
const vaultId = req.body?.vault;
|
||||||
|
const dismiss = req.body?.dismiss || false;
|
||||||
|
|
||||||
|
if (!vaultId) {
|
||||||
|
return res.status(400).json({ error: "Missing vault ID" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const vaultPath = config.getVaultPath(vaultId);
|
||||||
|
|
||||||
|
if (!vaultPath) {
|
||||||
|
return res.status(404).json({ error: "Vault not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const meta = await getIgnisMeta(vaultPath);
|
||||||
|
|
||||||
|
if (dismiss) {
|
||||||
|
// User clicked "Don't Ask Again" or "Not Now"
|
||||||
|
meta.pluginPrompted = true;
|
||||||
|
await setIgnisMeta(vaultPath, meta);
|
||||||
|
|
||||||
|
return res.json({ ok: true, prompted: true });
|
||||||
|
} else {
|
||||||
|
// User wants to install the plugin
|
||||||
|
const installed = await installPluginInVault(vaultPath);
|
||||||
|
|
||||||
|
meta.pluginPrompted = true;
|
||||||
|
await setIgnisMeta(vaultPath, meta);
|
||||||
|
|
||||||
|
return res.json({ ok: true, installed, prompted: true });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).json({ error: e.message, code: e.code });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
|
||||||
17
server/routes/version.js
Normal file
17
server/routes/version.js
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
const express = require("express");
|
||||||
|
const { getVersion } = require("../version");
|
||||||
|
const config = require("../config");
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get("/", (req, res) => {
|
||||||
|
const pkg = require("../../package.json");
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
version: getVersion(),
|
||||||
|
semver: pkg.version,
|
||||||
|
obsidianVersion: config.obsidianVersion,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
23
server/version.js
Normal file
23
server/version.js
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const { execSync } = require("child_process");
|
||||||
|
|
||||||
|
function getVersion() {
|
||||||
|
const pkg = JSON.parse(
|
||||||
|
fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf-8"),
|
||||||
|
);
|
||||||
|
const semver = pkg.version;
|
||||||
|
|
||||||
|
let hash;
|
||||||
|
try {
|
||||||
|
hash = execSync("git rev-parse --short=7 HEAD", {
|
||||||
|
encoding: "utf-8",
|
||||||
|
}).trim();
|
||||||
|
} catch (e) {
|
||||||
|
hash = Date.now().toString(36).slice(-7);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${semver}-${hash}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { getVersion };
|
||||||
|
|
@ -48,6 +48,8 @@ export const vaultService = {
|
||||||
body: JSON.stringify({ name }),
|
body: JSON.stringify({ name }),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this._setVaultTrust(name);
|
||||||
|
|
||||||
return this.listVaults();
|
return this.listVaults();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -121,6 +123,10 @@ export const vaultService = {
|
||||||
target.location.href = "/?vault=" + encodeURIComponent(id);
|
target.location.href = "/?vault=" + encodeURIComponent(id);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_setVaultTrust(vaultId, trusted = true) {
|
||||||
|
localStorage.setItem("enable-plugin-" + vaultId, String(trusted));
|
||||||
|
},
|
||||||
|
|
||||||
_migrateLocalStorage(oldId, newId) {
|
_migrateLocalStorage(oldId, newId) {
|
||||||
const pluginKey = "enable-plugin-";
|
const pluginKey = "enable-plugin-";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
import { randomBytes } from "./random-bytes.js";
|
import { randomBytes } from "./random-bytes.js";
|
||||||
import { createHash } from "./create-hash.js";
|
import { createHash } from "./create-hash.js";
|
||||||
import { scrypt } from "./scrypt.js";
|
import { scrypt } from "./scrypt.js";
|
||||||
|
import { randomUUID } from "./random-uuid.js";
|
||||||
|
|
||||||
export const cryptoShim = {
|
export const cryptoShim = {
|
||||||
randomBytes,
|
randomBytes,
|
||||||
createHash,
|
createHash,
|
||||||
scrypt,
|
scrypt,
|
||||||
|
randomUUID,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
3
src/shims/crypto/random-uuid.js
Normal file
3
src/shims/crypto/random-uuid.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export function randomUUID() {
|
||||||
|
return crypto.randomUUID();
|
||||||
|
}
|
||||||
47
src/shims/debug.js
Normal file
47
src/shims/debug.js
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
const DEBUG = true;
|
||||||
|
const _accessLog = new Map(); // "module.property" -> count
|
||||||
|
|
||||||
|
export function wrapWithProxy(obj, name) {
|
||||||
|
if (!DEBUG || !obj || typeof obj !== "object") {
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Proxy(obj, {
|
||||||
|
get(target, prop) {
|
||||||
|
if (
|
||||||
|
typeof prop === "string" &&
|
||||||
|
prop !== "then" &&
|
||||||
|
prop !== "toJSON" &&
|
||||||
|
!prop.startsWith("_")
|
||||||
|
) {
|
||||||
|
const key = `${name}.${prop}`;
|
||||||
|
_accessLog.set(key, (_accessLog.get(key) || 0) + 1);
|
||||||
|
|
||||||
|
if (!(prop in target)) {
|
||||||
|
console.warn(`[shim:MISS] ${key} - property not found on shim`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return target[prop];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function installDebugHelpers(rawRegistry) {
|
||||||
|
window.__shimLog = function () {
|
||||||
|
const sorted = [..._accessLog.entries()].sort((a, b) => b[1] - a[1]);
|
||||||
|
console.table(sorted.map(([k, v]) => ({ api: k, calls: v })));
|
||||||
|
};
|
||||||
|
|
||||||
|
window.__shimMisses = function () {
|
||||||
|
const sorted = [..._accessLog.entries()]
|
||||||
|
.filter(([k]) => {
|
||||||
|
const [mod, prop] = k.split(".");
|
||||||
|
const shim = rawRegistry[mod];
|
||||||
|
return shim && !(prop in shim);
|
||||||
|
})
|
||||||
|
.sort((a, b) => b[1] - a[1]);
|
||||||
|
|
||||||
|
console.table(sorted.map(([k, v]) => ({ api: k, calls: v })));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -3,12 +3,158 @@ import {
|
||||||
showConfirmDialog,
|
showConfirmDialog,
|
||||||
showPromptDialog,
|
showPromptDialog,
|
||||||
} from "../../../ui/bootstrap.js";
|
} from "../../../ui/bootstrap.js";
|
||||||
|
import { transport } from "../../fs/transport.js";
|
||||||
|
|
||||||
|
const IMPORTS_DIR = ".obsidian/imports";
|
||||||
|
|
||||||
|
let stagedFiles = [];
|
||||||
|
|
||||||
|
function buildAcceptString(filters) {
|
||||||
|
if (!filters || filters.length === 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const extensions = filters.flatMap((f) => f.extensions || []);
|
||||||
|
|
||||||
|
if (extensions.includes("*")) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return extensions.map((ext) => "." + ext).join(",");
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickFiles(accept, multiple) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const input = document.createElement("input");
|
||||||
|
input.type = "file";
|
||||||
|
input.multiple = multiple;
|
||||||
|
input.style.display = "none";
|
||||||
|
|
||||||
|
if (accept) {
|
||||||
|
input.accept = accept;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.addEventListener("change", () => {
|
||||||
|
const files = Array.from(input.files || []);
|
||||||
|
input.remove();
|
||||||
|
resolve(files);
|
||||||
|
});
|
||||||
|
|
||||||
|
// User closed the picker without selecting
|
||||||
|
input.addEventListener("cancel", () => {
|
||||||
|
input.remove();
|
||||||
|
resolve([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.appendChild(input);
|
||||||
|
input.click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadToImports(file) {
|
||||||
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
|
const bytes = new Uint8Array(arrayBuffer);
|
||||||
|
const targetPath = IMPORTS_DIR + "/" + file.name;
|
||||||
|
|
||||||
|
await transport.writeFile(targetPath, bytes);
|
||||||
|
|
||||||
|
return "/" + targetPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startWorkaroundFlow(options) {
|
||||||
|
const properties = options?.properties || [];
|
||||||
|
const multiple = properties.includes("multiSelections");
|
||||||
|
const accept = buildAcceptString(options?.filters);
|
||||||
|
|
||||||
|
const files = await pickFiles(accept, multiple);
|
||||||
|
|
||||||
|
if (files.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const paths = [];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const vaultPath = await uploadToImports(file);
|
||||||
|
paths.push(vaultPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
stagedFiles = paths;
|
||||||
|
|
||||||
|
const names = paths.map((p) => p.split("/").pop()).join(", ");
|
||||||
|
|
||||||
|
console.log("[shim:dialog] Files staged for next sync call:", paths);
|
||||||
|
|
||||||
|
await showMessageDialog(
|
||||||
|
"Files Ready",
|
||||||
|
`Uploaded: ${names}\n\nPlease retry the action that brought you here. ` +
|
||||||
|
"The files will be provided automatically.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export const dialogShim = {
|
export const dialogShim = {
|
||||||
async showOpenDialog(browserWindow, options) {
|
async showOpenDialog(browserWindow, options) {
|
||||||
// TODO: implement custom modal with server-side file listing
|
if (typeof browserWindow === "object" && !options) {
|
||||||
console.log("[shim:dialog] showOpenDialog (stub):", options);
|
options = browserWindow;
|
||||||
return { canceled: true, filePaths: [] };
|
}
|
||||||
|
|
||||||
|
const properties = options?.properties || [];
|
||||||
|
const multiple = properties.includes("multiSelections");
|
||||||
|
const accept = buildAcceptString(options?.filters);
|
||||||
|
|
||||||
|
console.log("[shim:dialog] showOpenDialog - opening browser file picker");
|
||||||
|
|
||||||
|
const files = await pickFiles(accept, multiple);
|
||||||
|
|
||||||
|
if (files.length === 0) {
|
||||||
|
return { canceled: true, filePaths: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePaths = [];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const vaultPath = await uploadToImports(file);
|
||||||
|
filePaths.push(vaultPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[shim:dialog] showOpenDialog - uploaded:", filePaths);
|
||||||
|
return { canceled: false, filePaths };
|
||||||
|
},
|
||||||
|
|
||||||
|
showOpenDialogSync(browserWindow, options) {
|
||||||
|
if (typeof browserWindow === "object" && !options) {
|
||||||
|
options = browserWindow;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If files were staged from a previous workaround, return them immediately
|
||||||
|
if (stagedFiles.length > 0) {
|
||||||
|
const paths = stagedFiles;
|
||||||
|
stagedFiles = [];
|
||||||
|
console.log(
|
||||||
|
"[shim:dialog] showOpenDialogSync - returning staged files:",
|
||||||
|
paths,
|
||||||
|
);
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn(
|
||||||
|
"[shim:dialog] showOpenDialogSync requires workaround in browser context",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fire-and-forget: show warning, then optionally start workaround flow
|
||||||
|
showConfirmDialog(
|
||||||
|
"Feature Not Available",
|
||||||
|
"This action requires a native file picker which is not available in the browser.",
|
||||||
|
"A workaround is available: upload your file first, then retry the action. " +
|
||||||
|
"Would you like to proceed?",
|
||||||
|
"Upload File",
|
||||||
|
).then((confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
startWorkaroundFlow(options);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return undefined;
|
||||||
},
|
},
|
||||||
|
|
||||||
async showSaveDialog(browserWindow, options) {
|
async showSaveDialog(browserWindow, options) {
|
||||||
|
|
|
||||||
153
src/shims/fs/fd.js
Normal file
153
src/shims/fs/fd.js
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
// File descriptor shim - maps fake integer fds to in-memory file buffers.
|
||||||
|
// Enables libraries like yauzl that use fs.open/fs.read/fs.close to seek
|
||||||
|
// around files without loading them via readFileSync upfront.
|
||||||
|
|
||||||
|
let nextFd = 100;
|
||||||
|
const openFiles = new Map();
|
||||||
|
|
||||||
|
export function createFdOps(metadataCache, contentCache, transport) {
|
||||||
|
function ensureData(path) {
|
||||||
|
const cached = contentCache.get(path);
|
||||||
|
|
||||||
|
if (cached !== null) {
|
||||||
|
if (typeof cached === "string") {
|
||||||
|
return new TextEncoder().encode(cached);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Synchronous fetch fallback
|
||||||
|
console.warn("[shim:fs] fd open cache miss, using sync XHR:", path);
|
||||||
|
const data = transport.readFileSync(path);
|
||||||
|
contentCache.set(path, data);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEntry(fd) {
|
||||||
|
const entry = openFiles.get(fd);
|
||||||
|
|
||||||
|
if (!entry) {
|
||||||
|
const err = new Error(`EBADF: bad file descriptor, fd ${fd}`);
|
||||||
|
err.code = "EBADF";
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Sync ---
|
||||||
|
|
||||||
|
function openSync(path, flags, mode) {
|
||||||
|
if (!metadataCache.has(path)) {
|
||||||
|
const err = new Error(
|
||||||
|
`ENOENT: no such file or directory, open '${path}'`,
|
||||||
|
);
|
||||||
|
err.code = "ENOENT";
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = ensureData(path);
|
||||||
|
const fd = nextFd++;
|
||||||
|
openFiles.set(fd, { path, data });
|
||||||
|
|
||||||
|
return fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readSync(fd, buffer, offset, length, position) {
|
||||||
|
const entry = getEntry(fd);
|
||||||
|
const available = Math.min(length, entry.data.length - position);
|
||||||
|
|
||||||
|
if (available <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const slice = entry.data.subarray(position, position + available);
|
||||||
|
buffer.set(slice, offset);
|
||||||
|
|
||||||
|
return available;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSync(fd) {
|
||||||
|
openFiles.delete(fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fstatSync(fd) {
|
||||||
|
const entry = getEntry(fd);
|
||||||
|
const stat = metadataCache.toStat(entry.path);
|
||||||
|
|
||||||
|
if (stat) {
|
||||||
|
return stat;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: construct minimal stat from the buffer
|
||||||
|
return {
|
||||||
|
size: entry.data.length,
|
||||||
|
isFile: () => true,
|
||||||
|
isDirectory: () => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Async (callback style) ---
|
||||||
|
|
||||||
|
function open(path, flags, modeOrCb, cb) {
|
||||||
|
if (typeof modeOrCb === "function") {
|
||||||
|
cb = modeOrCb;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fd = openSync(path, flags);
|
||||||
|
queueMicrotask(() => cb(null, fd));
|
||||||
|
} catch (e) {
|
||||||
|
queueMicrotask(() => cb(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function read(fd, buffer, offset, length, position, cb) {
|
||||||
|
try {
|
||||||
|
const bytesRead = readSync(fd, buffer, offset, length, position);
|
||||||
|
queueMicrotask(() => cb(null, bytesRead, buffer));
|
||||||
|
} catch (e) {
|
||||||
|
queueMicrotask(() => cb(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function close(fd, cb) {
|
||||||
|
try {
|
||||||
|
closeSync(fd);
|
||||||
|
|
||||||
|
if (cb) {
|
||||||
|
queueMicrotask(() => cb(null));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (cb) {
|
||||||
|
queueMicrotask(() => cb(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fstat(fd, optionsOrCb, cb) {
|
||||||
|
if (typeof optionsOrCb === "function") {
|
||||||
|
cb = optionsOrCb;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stat = fstatSync(fd);
|
||||||
|
queueMicrotask(() => cb(null, stat));
|
||||||
|
} catch (e) {
|
||||||
|
queueMicrotask(() => cb(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
openSync,
|
||||||
|
readSync,
|
||||||
|
closeSync,
|
||||||
|
fstatSync,
|
||||||
|
open,
|
||||||
|
read,
|
||||||
|
close,
|
||||||
|
fstat,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ import { createFsPromises } from "./promises.js";
|
||||||
import { createFsSync } from "./sync.js";
|
import { createFsSync } from "./sync.js";
|
||||||
import { createFsWatch } from "./watch.js";
|
import { createFsWatch } from "./watch.js";
|
||||||
import { createWatcherClient } from "./watcher-client.js";
|
import { createWatcherClient } from "./watcher-client.js";
|
||||||
|
import { createFdOps } from "./fd.js";
|
||||||
import { constants } from "./constants.js";
|
import { constants } from "./constants.js";
|
||||||
|
|
||||||
const metadataCache = new MetadataCache();
|
const metadataCache = new MetadataCache();
|
||||||
|
|
@ -14,6 +15,7 @@ const fsPromises = createFsPromises(metadataCache, contentCache, transport);
|
||||||
const fsSync = createFsSync(metadataCache, contentCache, transport);
|
const fsSync = createFsSync(metadataCache, contentCache, transport);
|
||||||
const fsWatch = createFsWatch(transport);
|
const fsWatch = createFsWatch(transport);
|
||||||
const watcherClient = createWatcherClient(metadataCache, contentCache, fsWatch);
|
const watcherClient = createWatcherClient(metadataCache, contentCache, fsWatch);
|
||||||
|
const fdOps = createFdOps(metadataCache, contentCache, transport);
|
||||||
|
|
||||||
export const fsShim = {
|
export const fsShim = {
|
||||||
promises: fsPromises,
|
promises: fsPromises,
|
||||||
|
|
@ -26,6 +28,15 @@ export const fsShim = {
|
||||||
statSync: fsSync.statSync,
|
statSync: fsSync.statSync,
|
||||||
readdirSync: fsSync.readdirSync,
|
readdirSync: fsSync.readdirSync,
|
||||||
|
|
||||||
|
open: fdOps.open,
|
||||||
|
openSync: fdOps.openSync,
|
||||||
|
read: fdOps.read,
|
||||||
|
readSync: fdOps.readSync,
|
||||||
|
close: fdOps.close,
|
||||||
|
closeSync: fdOps.closeSync,
|
||||||
|
fstat: fdOps.fstat,
|
||||||
|
fstatSync: fdOps.fstatSync,
|
||||||
|
|
||||||
watch: fsWatch.watch,
|
watch: fsWatch.watch,
|
||||||
constants,
|
constants,
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -208,5 +208,48 @@ export function createFsPromises(metadataCache, contentCache, transport) {
|
||||||
metadataCache.set(path, meta);
|
metadataCache.set(path, meta);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async open(path, flags) {
|
||||||
|
if (!metadataCache.has(path)) {
|
||||||
|
const err = new Error(
|
||||||
|
`ENOENT: no such file or directory, open '${path}'`,
|
||||||
|
);
|
||||||
|
err.code = "ENOENT";
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await this.readFile(path);
|
||||||
|
const fileData =
|
||||||
|
typeof data === "string" ? new TextEncoder().encode(data) : data;
|
||||||
|
|
||||||
|
const fileStat = metadataCache.toStat(path) || {
|
||||||
|
size: fileData.length,
|
||||||
|
isFile: () => true,
|
||||||
|
isDirectory: () => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
async stat() {
|
||||||
|
return fileStat;
|
||||||
|
},
|
||||||
|
|
||||||
|
async read(buffer, offset, length, position) {
|
||||||
|
const available = Math.min(length, fileData.length - position);
|
||||||
|
|
||||||
|
if (available <= 0) {
|
||||||
|
return { bytesRead: 0, buffer };
|
||||||
|
}
|
||||||
|
|
||||||
|
const slice = fileData.subarray(position, position + available);
|
||||||
|
buffer.set(slice, offset);
|
||||||
|
|
||||||
|
return { bytesRead: available, buffer };
|
||||||
|
},
|
||||||
|
|
||||||
|
async close() {
|
||||||
|
// Nothing to clean up - data is in memory
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
275
src/shims/globals.js
Normal file
275
src/shims/globals.js
Normal file
|
|
@ -0,0 +1,275 @@
|
||||||
|
import { processShim } from "./process.js";
|
||||||
|
import {
|
||||||
|
registerPopupWindow,
|
||||||
|
unregisterPopupWindow,
|
||||||
|
} from "./electron/remote/window.js";
|
||||||
|
import { showVaultManager } from "../ui/bootstrap.js";
|
||||||
|
|
||||||
|
function installProcess() {
|
||||||
|
window.process = processShim;
|
||||||
|
}
|
||||||
|
|
||||||
|
function installBuffer() {
|
||||||
|
if (typeof window.Buffer !== "undefined") return;
|
||||||
|
|
||||||
|
window.Buffer = {
|
||||||
|
from: function (data, encoding) {
|
||||||
|
if (typeof data === "string") {
|
||||||
|
return new TextEncoder().encode(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data instanceof ArrayBuffer) {
|
||||||
|
return new Uint8Array(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Uint8Array(data);
|
||||||
|
},
|
||||||
|
alloc: function (size, fill, encoding) {
|
||||||
|
const buf = new Uint8Array(size);
|
||||||
|
|
||||||
|
if (fill !== undefined) {
|
||||||
|
buf.fill(typeof fill === "string" ? fill.charCodeAt(0) : fill);
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf;
|
||||||
|
},
|
||||||
|
allocUnsafe: function (size) {
|
||||||
|
return new Uint8Array(size);
|
||||||
|
},
|
||||||
|
concat: function (arrays) {
|
||||||
|
const total = arrays.reduce((sum, a) => sum + a.length, 0);
|
||||||
|
const result = new Uint8Array(total);
|
||||||
|
let offset = 0;
|
||||||
|
|
||||||
|
for (const arr of arrays) {
|
||||||
|
result.set(arr, offset);
|
||||||
|
offset += arr.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
isBuffer: function (obj) {
|
||||||
|
return obj instanceof Uint8Array;
|
||||||
|
},
|
||||||
|
byteLength: function (str, encoding) {
|
||||||
|
return new TextEncoder().encode(str).length;
|
||||||
|
},
|
||||||
|
isEncoding: function (encoding) {
|
||||||
|
return [
|
||||||
|
"utf8",
|
||||||
|
"utf-8",
|
||||||
|
"ascii",
|
||||||
|
"binary",
|
||||||
|
"base64",
|
||||||
|
"hex",
|
||||||
|
"latin1",
|
||||||
|
].includes((encoding || "").toLowerCase());
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function installWindowClose() {
|
||||||
|
window.close = function () {
|
||||||
|
console.log("[ignis] window.close() blocked");
|
||||||
|
if (!window.__vaultConfig) {
|
||||||
|
showVaultManager();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function installWindowOpen() {
|
||||||
|
window.__popupIframe = null;
|
||||||
|
const _originalOpen = window.open;
|
||||||
|
|
||||||
|
window.open = function (url, target, features) {
|
||||||
|
if (url === "about:blank" || (features && features.includes("popup"))) {
|
||||||
|
console.log("[ignis] intercepted popup:", url, features);
|
||||||
|
|
||||||
|
registerPopupWindow();
|
||||||
|
|
||||||
|
const iframe = document.createElement("iframe");
|
||||||
|
iframe.style.cssText =
|
||||||
|
"position:fixed;left:-9999px;width:0;height:0;border:none;";
|
||||||
|
|
||||||
|
document.body.appendChild(iframe);
|
||||||
|
window.__popupIframe = iframe;
|
||||||
|
|
||||||
|
const iframeWin = iframe.contentWindow;
|
||||||
|
|
||||||
|
iframeWin.require = window.require;
|
||||||
|
iframeWin.module = window.module;
|
||||||
|
iframeWin.Buffer = window.Buffer;
|
||||||
|
iframeWin.process = window.process;
|
||||||
|
iframeWin.global = iframeWin;
|
||||||
|
iframeWin.globalEnhance = window.globalEnhance;
|
||||||
|
|
||||||
|
iframeWin.close = function () {
|
||||||
|
unregisterPopupWindow();
|
||||||
|
iframe.remove();
|
||||||
|
window.__popupIframe = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return iframeWin;
|
||||||
|
}
|
||||||
|
return _originalOpen.call(window, url, target, features);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSameOrigin(url) {
|
||||||
|
if (
|
||||||
|
!url ||
|
||||||
|
url.startsWith("/") ||
|
||||||
|
url.startsWith("./") ||
|
||||||
|
url.startsWith("../")
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.startsWith("data:") || url.startsWith("blob:")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = new URL(url, window.location.origin);
|
||||||
|
return parsed.origin === window.location.origin;
|
||||||
|
} catch {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function installFetchShim() {
|
||||||
|
const originalFetch = window.fetch.bind(window);
|
||||||
|
window.__originalFetch = originalFetch;
|
||||||
|
|
||||||
|
window.fetch = async function (input, init) {
|
||||||
|
let url;
|
||||||
|
|
||||||
|
if (typeof input === "string") {
|
||||||
|
url = input;
|
||||||
|
} else if (input instanceof URL) {
|
||||||
|
url = input.href;
|
||||||
|
} else if (input instanceof Request) {
|
||||||
|
url = input.url;
|
||||||
|
} else {
|
||||||
|
url = String(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSameOrigin(url)) {
|
||||||
|
return originalFetch(input, init);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cross-origin - route through server proxy
|
||||||
|
const method = (
|
||||||
|
init?.method || (input instanceof Request ? input.method : "GET")
|
||||||
|
).toUpperCase();
|
||||||
|
const headers = {};
|
||||||
|
|
||||||
|
if (init?.headers) {
|
||||||
|
const h =
|
||||||
|
init.headers instanceof Headers
|
||||||
|
? init.headers
|
||||||
|
: new Headers(init.headers);
|
||||||
|
h.forEach((val, key) => {
|
||||||
|
headers[key] = val;
|
||||||
|
});
|
||||||
|
} else if (input instanceof Request) {
|
||||||
|
input.headers.forEach((val, key) => {
|
||||||
|
headers[key] = val;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mimic the real Obsidian desktop app headers for cross-origin requests
|
||||||
|
if (!headers["user-agent"] && !headers["User-Agent"]) {
|
||||||
|
headers["user-agent"] = navigator.userAgent;
|
||||||
|
}
|
||||||
|
if (!headers["origin"] && !headers["Origin"]) {
|
||||||
|
headers["origin"] = "app://obsidian.md";
|
||||||
|
}
|
||||||
|
|
||||||
|
let body = null;
|
||||||
|
let binary = false;
|
||||||
|
|
||||||
|
if (init?.body && method !== "GET" && method !== "HEAD") {
|
||||||
|
if (typeof init.body === "string") {
|
||||||
|
body = init.body;
|
||||||
|
} else if (init.body instanceof ArrayBuffer) {
|
||||||
|
body = arrayBufferToBase64(init.body);
|
||||||
|
binary = true;
|
||||||
|
} else if (init.body instanceof Uint8Array) {
|
||||||
|
body = arrayBufferToBase64(init.body.buffer);
|
||||||
|
binary = true;
|
||||||
|
} else if (typeof init.body === "object") {
|
||||||
|
body = JSON.stringify(init.body);
|
||||||
|
} else {
|
||||||
|
body = String(init.body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[shim:fetch] Proxying cross-origin:", method, url);
|
||||||
|
|
||||||
|
const proxyRes = await originalFetch("/api/proxy", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ url, method, headers, body, binary }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!proxyRes.ok) {
|
||||||
|
const err = await proxyRes
|
||||||
|
.json()
|
||||||
|
.catch(() => ({ error: "Proxy request failed" }));
|
||||||
|
throw new TypeError(err.error || "Failed to fetch");
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await proxyRes.json();
|
||||||
|
const respBody = base64ToArrayBuffer(result.body);
|
||||||
|
|
||||||
|
return new Response(respBody, {
|
||||||
|
status: result.status,
|
||||||
|
headers: result.headers,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function installContextMenuFix() {
|
||||||
|
// hacky fix to prevent browser from showing context menu while allowing obsidian context menu
|
||||||
|
window.addEventListener(
|
||||||
|
"contextmenu",
|
||||||
|
(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
Object.defineProperty(e, "defaultPrevented", { get: () => false });
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function installGlobals() {
|
||||||
|
installProcess();
|
||||||
|
installBuffer();
|
||||||
|
installFetchShim();
|
||||||
|
installWindowClose();
|
||||||
|
installWindowOpen();
|
||||||
|
installContextMenuFix();
|
||||||
|
}
|
||||||
117
src/shims/init.js
Normal file
117
src/shims/init.js
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
import { fsShim } from "./fs/index.js";
|
||||||
|
import { installRequestUrlShim } from "./request-url.js";
|
||||||
|
import { vaultService } from "../services/vault-service.js";
|
||||||
|
import { showPluginInstallDialog } from "../ui/bootstrap.js";
|
||||||
|
|
||||||
|
function resolveVaultId() {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
window.__currentVaultId =
|
||||||
|
urlParams.get("vault") || localStorage.getItem("last-vault") || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function initVaultConfig() {
|
||||||
|
try {
|
||||||
|
const vaultParam = window.__currentVaultId
|
||||||
|
? "?vault=" + encodeURIComponent(window.__currentVaultId)
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
|
||||||
|
xhr.open("GET", "/api/vault/info" + vaultParam, false);
|
||||||
|
xhr.send();
|
||||||
|
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
const info = JSON.parse(xhr.responseText);
|
||||||
|
|
||||||
|
window.__currentVaultId = info.id;
|
||||||
|
localStorage.setItem("last-vault", info.id);
|
||||||
|
window.__obsidianVersion = info.version || "0.0.0";
|
||||||
|
|
||||||
|
window.__vaultConfig = {
|
||||||
|
id: info.id,
|
||||||
|
path: "/",
|
||||||
|
};
|
||||||
|
|
||||||
|
window.__ignisPlugin = info.ignisPlugin || null;
|
||||||
|
|
||||||
|
console.log("[ignis] Vault:", window.__vaultConfig);
|
||||||
|
console.log("[ignis] Obsidian version:", window.__obsidianVersion);
|
||||||
|
} else {
|
||||||
|
console.warn("[ignis] No vault found, will show manager");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[ignis] Failed to fetch vault config:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initVaultList() {
|
||||||
|
try {
|
||||||
|
vaultService.listVaultsSync();
|
||||||
|
} catch (e) {
|
||||||
|
window.__vaultList = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initMetadataCache() {
|
||||||
|
try {
|
||||||
|
const vaultParam = window.__currentVaultId
|
||||||
|
? "?vault=" + encodeURIComponent(window.__currentVaultId)
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
|
||||||
|
xhr.open("GET", "/api/fs/tree" + vaultParam, false);
|
||||||
|
xhr.send();
|
||||||
|
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
const tree = JSON.parse(xhr.responseText);
|
||||||
|
|
||||||
|
fsShim._metadataCache.populate(tree);
|
||||||
|
fsShim._metadataCache.set("", { type: "directory" });
|
||||||
|
fsShim._metadataCache.set("/", { type: "directory" });
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"[ignis] Metadata cache populated:",
|
||||||
|
fsShim._metadataCache.size,
|
||||||
|
"entries",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.error("[ignis] Failed to fetch metadata tree:", xhr.status);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[ignis] Failed to init metadata cache:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initPluginPrompt() {
|
||||||
|
if (
|
||||||
|
!window.__ignisPlugin ||
|
||||||
|
window.__ignisPlugin.installed ||
|
||||||
|
window.__ignisPlugin.prompted
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const vaultId = window.__currentVaultId;
|
||||||
|
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
if (document.querySelector(".workspace")) {
|
||||||
|
observer.disconnect();
|
||||||
|
showPluginInstallDialog(vaultId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(document.documentElement, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initialize() {
|
||||||
|
resolveVaultId();
|
||||||
|
initVaultConfig();
|
||||||
|
initVaultList();
|
||||||
|
initMetadataCache();
|
||||||
|
installRequestUrlShim();
|
||||||
|
initPluginPrompt();
|
||||||
|
}
|
||||||
|
|
@ -1,263 +1,10 @@
|
||||||
import { electronShim } from "./electron/index.js";
|
import { installRequire } from "./require.js";
|
||||||
import { remoteShim } from "./electron/remote/index.js";
|
import { installGlobals } from "./globals.js";
|
||||||
import { fsShim } from "./fs/index.js";
|
import { initialize } from "./init.js";
|
||||||
import { pathShim } from "./path.js";
|
|
||||||
import { urlShim } from "./url.js";
|
|
||||||
import { cryptoShim } from "./crypto/index.js";
|
|
||||||
import { processShim } from "./process.js";
|
|
||||||
import { installRequestUrlShim } from "./request-url.js";
|
|
||||||
import {
|
|
||||||
registerPopupWindow,
|
|
||||||
unregisterPopupWindow,
|
|
||||||
} from "./electron/remote/window.js";
|
|
||||||
import * as childProcessShim from "./node/child_process.js";
|
|
||||||
import * as eventsShim from "./node/events.js";
|
|
||||||
import * as osShim from "./node/os.js";
|
|
||||||
import * as netShim from "./node/net.js";
|
|
||||||
import * as httpShim from "./node/http.js";
|
|
||||||
import { vaultService } from "../services/vault-service.js";
|
|
||||||
|
|
||||||
const DEBUG = true;
|
installGlobals(); // process, Buffer, window overrides (before require so Buffer is available)
|
||||||
const _accessLog = new Map(); // "module.property" -> count
|
installRequire(); // shim registry, window.require
|
||||||
|
initialize(); // vault config, metadata cache, plugin prompt
|
||||||
function wrapWithProxy(obj, name) {
|
|
||||||
if (!DEBUG || !obj || typeof obj !== "object") {
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Proxy(obj, {
|
|
||||||
get(target, prop) {
|
|
||||||
if (
|
|
||||||
typeof prop === "string" &&
|
|
||||||
prop !== "then" &&
|
|
||||||
prop !== "toJSON" &&
|
|
||||||
!prop.startsWith("_")
|
|
||||||
) {
|
|
||||||
const key = `${name}.${prop}`;
|
|
||||||
_accessLog.set(key, (_accessLog.get(key) || 0) + 1);
|
|
||||||
|
|
||||||
if (!(prop in target)) {
|
|
||||||
console.warn(`[shim:MISS] ${key} - property not found on shim`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return target[prop];
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
window.__shimLog = function () {
|
|
||||||
const sorted = [..._accessLog.entries()].sort((a, b) => b[1] - a[1]);
|
|
||||||
console.table(sorted.map(([k, v]) => ({ api: k, calls: v })));
|
|
||||||
};
|
|
||||||
|
|
||||||
window.__shimMisses = function () {
|
|
||||||
const sorted = [..._accessLog.entries()]
|
|
||||||
.filter(([k]) => {
|
|
||||||
const [mod, prop] = k.split(".");
|
|
||||||
const shim = rawRegistry[mod];
|
|
||||||
return shim && !(prop in shim);
|
|
||||||
})
|
|
||||||
.sort((a, b) => b[1] - a[1]);
|
|
||||||
|
|
||||||
console.table(sorted.map(([k, v]) => ({ api: k, calls: v })));
|
|
||||||
};
|
|
||||||
|
|
||||||
const rawRegistry = {
|
|
||||||
electron: electronShim,
|
|
||||||
"@electron/remote": remoteShim,
|
|
||||||
"original-fs": fsShim,
|
|
||||||
fs: fsShim,
|
|
||||||
path: pathShim,
|
|
||||||
url: urlShim,
|
|
||||||
crypto: cryptoShim,
|
|
||||||
child_process: childProcessShim,
|
|
||||||
events: eventsShim,
|
|
||||||
os: osShim,
|
|
||||||
net: netShim,
|
|
||||||
http: httpShim,
|
|
||||||
https: httpShim,
|
|
||||||
};
|
|
||||||
|
|
||||||
const shimRegistry = {};
|
|
||||||
for (const [name, shim] of Object.entries(rawRegistry)) {
|
|
||||||
shimRegistry[name] = wrapWithProxy(shim, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
const throwOnRequire = new Set(["btime", "get-fonts", "vibrancy-win"]);
|
|
||||||
|
|
||||||
window.require = function (moduleName) {
|
|
||||||
if (throwOnRequire.has(moduleName)) {
|
|
||||||
throw new Error(`Cannot find module '${moduleName}'`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shimRegistry[moduleName]) {
|
|
||||||
return shimRegistry[moduleName];
|
|
||||||
}
|
|
||||||
|
|
||||||
console.warn("[ignis] Unshimmed require:", moduleName);
|
|
||||||
return wrapWithProxy({}, `UNKNOWN(${moduleName})`);
|
|
||||||
};
|
|
||||||
|
|
||||||
window.process = processShim;
|
|
||||||
|
|
||||||
if (typeof window.Buffer === "undefined") {
|
|
||||||
window.Buffer = {
|
|
||||||
from: function (data, encoding) {
|
|
||||||
if (typeof data === "string") {
|
|
||||||
return new TextEncoder().encode(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data instanceof ArrayBuffer) {
|
|
||||||
return new Uint8Array(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Uint8Array(data);
|
|
||||||
},
|
|
||||||
concat: function (arrays) {
|
|
||||||
const total = arrays.reduce((sum, a) => sum + a.length, 0);
|
|
||||||
const result = new Uint8Array(total);
|
|
||||||
let offset = 0;
|
|
||||||
|
|
||||||
for (const arr of arrays) {
|
|
||||||
result.set(arr, offset);
|
|
||||||
offset += arr.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
isBuffer: function (obj) {
|
|
||||||
return obj instanceof Uint8Array;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
window.close = function () {
|
|
||||||
console.log("[ignis] window.close() blocked");
|
|
||||||
};
|
|
||||||
|
|
||||||
window.__popupIframe = null;
|
|
||||||
const _originalOpen = window.open;
|
|
||||||
window.open = function (url, target, features) {
|
|
||||||
if (url === "about:blank" || (features && features.includes("popup"))) {
|
|
||||||
console.log("[ignis] intercepted popup:", url, features);
|
|
||||||
|
|
||||||
registerPopupWindow();
|
|
||||||
|
|
||||||
const iframe = document.createElement("iframe");
|
|
||||||
iframe.style.cssText =
|
|
||||||
"position:fixed;left:-9999px;width:0;height:0;border:none;";
|
|
||||||
|
|
||||||
document.body.appendChild(iframe);
|
|
||||||
window.__popupIframe = iframe;
|
|
||||||
|
|
||||||
const iframeWin = iframe.contentWindow;
|
|
||||||
|
|
||||||
iframeWin.require = window.require;
|
|
||||||
iframeWin.module = window.module;
|
|
||||||
iframeWin.Buffer = window.Buffer;
|
|
||||||
iframeWin.process = window.process;
|
|
||||||
iframeWin.global = iframeWin;
|
|
||||||
iframeWin.globalEnhance = window.globalEnhance;
|
|
||||||
|
|
||||||
iframeWin.close = function () {
|
|
||||||
unregisterPopupWindow();
|
|
||||||
iframe.remove();
|
|
||||||
window.__popupIframe = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
return iframeWin;
|
|
||||||
}
|
|
||||||
return _originalOpen.call(window, url, target, features);
|
|
||||||
};
|
|
||||||
|
|
||||||
// hacky fix to prevent browser from showing context menu while allowing obsidian context menu
|
|
||||||
window.addEventListener(
|
|
||||||
"contextmenu",
|
|
||||||
(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
Object.defineProperty(e, "defaultPrevented", { get: () => false });
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
const _urlParams = new URLSearchParams(window.location.search);
|
|
||||||
window.__currentVaultId =
|
|
||||||
_urlParams.get("vault") || localStorage.getItem("last-vault") || "";
|
|
||||||
|
|
||||||
(function initVaultConfig() {
|
|
||||||
try {
|
|
||||||
const vaultParam = window.__currentVaultId
|
|
||||||
? "?vault=" + encodeURIComponent(window.__currentVaultId)
|
|
||||||
: "";
|
|
||||||
|
|
||||||
const xhr = new XMLHttpRequest();
|
|
||||||
|
|
||||||
xhr.open("GET", "/api/vault/info" + vaultParam, false);
|
|
||||||
xhr.send();
|
|
||||||
|
|
||||||
if (xhr.status === 200) {
|
|
||||||
const info = JSON.parse(xhr.responseText);
|
|
||||||
|
|
||||||
window.__currentVaultId = info.id;
|
|
||||||
localStorage.setItem("last-vault", info.id);
|
|
||||||
window.__obsidianVersion = info.version || "0.0.0";
|
|
||||||
|
|
||||||
window.__vaultConfig = {
|
|
||||||
id: info.id,
|
|
||||||
path: "/",
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log("[ignis] Vault:", window.__vaultConfig);
|
|
||||||
console.log("[ignis] Obsidian version:", window.__obsidianVersion);
|
|
||||||
} else {
|
|
||||||
console.warn("[ignis] No vault found, will show manager");
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("[ignis] Failed to fetch vault config:", e);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
(function initVaultList() {
|
|
||||||
try {
|
|
||||||
vaultService.listVaultsSync();
|
|
||||||
} catch (e) {
|
|
||||||
window.__vaultList = [];
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
(function initMetadataCache() {
|
|
||||||
try {
|
|
||||||
const vaultParam = window.__currentVaultId
|
|
||||||
? "?vault=" + encodeURIComponent(window.__currentVaultId)
|
|
||||||
: "";
|
|
||||||
|
|
||||||
const xhr = new XMLHttpRequest();
|
|
||||||
|
|
||||||
xhr.open("GET", "/api/fs/tree" + vaultParam, false);
|
|
||||||
xhr.send();
|
|
||||||
|
|
||||||
if (xhr.status === 200) {
|
|
||||||
const tree = JSON.parse(xhr.responseText);
|
|
||||||
|
|
||||||
fsShim._metadataCache.populate(tree);
|
|
||||||
fsShim._metadataCache.set("", { type: "directory" });
|
|
||||||
fsShim._metadataCache.set("/", { type: "directory" });
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
"[ignis] Metadata cache populated:",
|
|
||||||
fsShim._metadataCache.size,
|
|
||||||
"entries",
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.error("[ignis] Failed to fetch metadata tree:", xhr.status);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("[ignis] Failed to init metadata cache:", e);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
installRequestUrlShim();
|
|
||||||
|
|
||||||
// Connect file watcher WebSocket after everything is initialized
|
// Connect file watcher WebSocket after everything is initialized
|
||||||
if (window.__currentVaultId) {
|
if (window.__currentVaultId) {
|
||||||
|
|
|
||||||
138
src/shims/node/zlib.js
Normal file
138
src/shims/node/zlib.js
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
// Zlib shim using pako for browser-side deflate/inflate/gzip/gunzip.
|
||||||
|
// Implements Node's zlib convenience functions (async callback + sync variants).
|
||||||
|
// Streaming classes (createDeflate, createGzip, etc.) are NOT implemented yet.
|
||||||
|
|
||||||
|
import pako from "pako";
|
||||||
|
|
||||||
|
// --- Constants ---
|
||||||
|
|
||||||
|
export const constants = {
|
||||||
|
Z_NO_FLUSH: 0,
|
||||||
|
Z_PARTIAL_FLUSH: 1,
|
||||||
|
Z_SYNC_FLUSH: 2,
|
||||||
|
Z_FULL_FLUSH: 3,
|
||||||
|
Z_FINISH: 4,
|
||||||
|
Z_BLOCK: 5,
|
||||||
|
Z_TREES: 6,
|
||||||
|
Z_OK: 0,
|
||||||
|
Z_STREAM_END: 1,
|
||||||
|
Z_NEED_DICT: 2,
|
||||||
|
Z_ERRNO: -1,
|
||||||
|
Z_STREAM_ERROR: -2,
|
||||||
|
Z_DATA_ERROR: -3,
|
||||||
|
Z_MEM_ERROR: -4,
|
||||||
|
Z_BUF_ERROR: -5,
|
||||||
|
Z_VERSION_ERROR: -6,
|
||||||
|
Z_NO_COMPRESSION: 0,
|
||||||
|
Z_BEST_SPEED: 1,
|
||||||
|
Z_BEST_COMPRESSION: 9,
|
||||||
|
Z_DEFAULT_COMPRESSION: -1,
|
||||||
|
Z_FILTERED: 1,
|
||||||
|
Z_HUFFMAN_ONLY: 2,
|
||||||
|
Z_RLE: 3,
|
||||||
|
Z_FIXED: 4,
|
||||||
|
Z_DEFAULT_STRATEGY: 0,
|
||||||
|
Z_DEFAULT_WINDOWBITS: 15,
|
||||||
|
Z_DEFAULT_MEMLEVEL: 8,
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Helpers ---
|
||||||
|
|
||||||
|
function toUint8Array(buf) {
|
||||||
|
if (buf instanceof Uint8Array) {
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof buf === "string") {
|
||||||
|
return new TextEncoder().encode(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buf instanceof ArrayBuffer) {
|
||||||
|
return new Uint8Array(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ArrayBuffer.isView(buf)) {
|
||||||
|
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Uint8Array(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
function wrapAsync(syncFn) {
|
||||||
|
return function (buf, optionsOrCb, cb) {
|
||||||
|
if (typeof optionsOrCb === "function") {
|
||||||
|
cb = optionsOrCb;
|
||||||
|
optionsOrCb = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = syncFn(buf, optionsOrCb || {});
|
||||||
|
|
||||||
|
if (cb) {
|
||||||
|
queueMicrotask(() => cb(null, result));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (cb) {
|
||||||
|
queueMicrotask(() => cb(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Sync functions ---
|
||||||
|
|
||||||
|
export function deflateSync(buf, options) {
|
||||||
|
return pako.deflate(toUint8Array(buf), options);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function inflateSync(buf, options) {
|
||||||
|
return pako.inflate(toUint8Array(buf), options);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deflateRawSync(buf, options) {
|
||||||
|
return pako.deflateRaw(toUint8Array(buf), options);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function inflateRawSync(buf, options) {
|
||||||
|
return pako.inflateRaw(toUint8Array(buf), options);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function gzipSync(buf, options) {
|
||||||
|
return pako.gzip(toUint8Array(buf), options);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function gunzipSync(buf, options) {
|
||||||
|
return pako.ungzip(toUint8Array(buf), options);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unzipSync(buf, options) {
|
||||||
|
return pako.ungzip(toUint8Array(buf), options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Async functions (callback style) ---
|
||||||
|
|
||||||
|
export const deflate = wrapAsync(deflateSync);
|
||||||
|
export const inflate = wrapAsync(inflateSync);
|
||||||
|
export const deflateRaw = wrapAsync(deflateRawSync);
|
||||||
|
export const inflateRaw = wrapAsync(inflateRawSync);
|
||||||
|
export const gzip = wrapAsync(gzipSync);
|
||||||
|
export const gunzip = wrapAsync(gunzipSync);
|
||||||
|
export const unzip = wrapAsync(unzipSync);
|
||||||
|
|
||||||
|
// --- Streaming stubs (not yet implemented) ---
|
||||||
|
|
||||||
|
function notImplemented(name) {
|
||||||
|
return function () {
|
||||||
|
throw new Error(
|
||||||
|
`zlib.${name}() streaming is not yet implemented. Use the sync/callback variants instead.`,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createDeflate = notImplemented("createDeflate");
|
||||||
|
export const createInflate = notImplemented("createInflate");
|
||||||
|
export const createDeflateRaw = notImplemented("createDeflateRaw");
|
||||||
|
export const createInflateRaw = notImplemented("createInflateRaw");
|
||||||
|
export const createGzip = notImplemented("createGzip");
|
||||||
|
export const createGunzip = notImplemented("createGunzip");
|
||||||
|
export const createUnzip = notImplemented("createUnzip");
|
||||||
67
src/shims/require.js
Normal file
67
src/shims/require.js
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
import { electronShim } from "./electron/index.js";
|
||||||
|
import { remoteShim } from "./electron/remote/index.js";
|
||||||
|
import { fsShim } from "./fs/index.js";
|
||||||
|
import { pathShim } from "./path.js";
|
||||||
|
import { urlShim } from "./url.js";
|
||||||
|
import { cryptoShim } from "./crypto/index.js";
|
||||||
|
import * as childProcessShim from "./node/child_process.js";
|
||||||
|
import * as eventsShim from "./node/events.js";
|
||||||
|
import * as osShim from "./node/os.js";
|
||||||
|
import * as netShim from "./node/net.js";
|
||||||
|
import * as httpShim from "./node/http.js";
|
||||||
|
import * as zlibShim from "./node/zlib.js";
|
||||||
|
import { wrapWithProxy, installDebugHelpers } from "./debug.js";
|
||||||
|
|
||||||
|
const rawRegistry = {
|
||||||
|
electron: electronShim,
|
||||||
|
"@electron/remote": remoteShim,
|
||||||
|
"original-fs": fsShim,
|
||||||
|
fs: fsShim,
|
||||||
|
path: pathShim,
|
||||||
|
url: urlShim,
|
||||||
|
crypto: cryptoShim,
|
||||||
|
child_process: childProcessShim,
|
||||||
|
events: eventsShim,
|
||||||
|
os: osShim,
|
||||||
|
net: netShim,
|
||||||
|
http: httpShim,
|
||||||
|
https: httpShim,
|
||||||
|
zlib: zlibShim,
|
||||||
|
};
|
||||||
|
|
||||||
|
const shimRegistry = {};
|
||||||
|
const throwOnRequire = new Set(["btime", "get-fonts", "vibrancy-win"]);
|
||||||
|
|
||||||
|
export function installRequire() {
|
||||||
|
for (const [name, shim] of Object.entries(rawRegistry)) {
|
||||||
|
shimRegistry[name] = wrapWithProxy(shim, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add buffer shim (protobufjs inquire() checks for this)
|
||||||
|
if (typeof window.Buffer !== "undefined") {
|
||||||
|
shimRegistry.buffer = window.Buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add empty long shim (optional protobufjs dependency, gracefully handled)
|
||||||
|
shimRegistry.long = undefined;
|
||||||
|
|
||||||
|
window.require = function (moduleName) {
|
||||||
|
// Strip node: prefix if present
|
||||||
|
const normalizedName = moduleName.startsWith("node:")
|
||||||
|
? moduleName.slice(5)
|
||||||
|
: moduleName;
|
||||||
|
|
||||||
|
if (throwOnRequire.has(normalizedName)) {
|
||||||
|
throw new Error(`Cannot find module '${moduleName}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shimRegistry[normalizedName]) {
|
||||||
|
return shimRegistry[normalizedName];
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn("[ignis] Unshimmed require:", moduleName);
|
||||||
|
return wrapWithProxy({}, `UNKNOWN(${moduleName})`);
|
||||||
|
};
|
||||||
|
|
||||||
|
installDebugHelpers(rawRegistry);
|
||||||
|
}
|
||||||
37
src/ui/bootstrap.js
vendored
37
src/ui/bootstrap.js
vendored
|
|
@ -1,7 +1,6 @@
|
||||||
import { vaultService } from "../services/vault-service.js";
|
import { vaultService } from "../services/vault-service.js";
|
||||||
|
|
||||||
export function showVaultManager() {
|
export function showVaultManager() {
|
||||||
if (!document.querySelector(".workspace")) return;
|
|
||||||
if (document.querySelector(".vault-manager-overlay")) return;
|
if (document.querySelector(".vault-manager-overlay")) return;
|
||||||
|
|
||||||
new window.IgnisUI.VaultManager({
|
new window.IgnisUI.VaultManager({
|
||||||
|
|
@ -48,6 +47,42 @@ export function showConfirmDialog(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function showPluginInstallDialog(vaultId) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const dialog = new window.IgnisUI.PluginInstallDialog({
|
||||||
|
target: document.body,
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog.$on("install", async () => {
|
||||||
|
try {
|
||||||
|
await fetch("/api/vault/install-plugin", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ vault: vaultId }),
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[ignis] Failed to install plugin:", e);
|
||||||
|
}
|
||||||
|
dialog.$destroy();
|
||||||
|
resolve("install");
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog.$on("dismiss", async () => {
|
||||||
|
try {
|
||||||
|
await fetch("/api/vault/install-plugin", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ vault: vaultId, dismiss: true }),
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[ignis] Failed to dismiss plugin prompt:", e);
|
||||||
|
}
|
||||||
|
dialog.$destroy();
|
||||||
|
resolve("dismiss");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function showPromptDialog(
|
export function showPromptDialog(
|
||||||
title,
|
title,
|
||||||
label,
|
label,
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@
|
||||||
let modalRef;
|
let modalRef;
|
||||||
|
|
||||||
function onConfirm() {
|
function onConfirm() {
|
||||||
dispatch("confirm");
|
|
||||||
modalRef.dismiss();
|
modalRef.dismiss();
|
||||||
|
dispatch("confirm");
|
||||||
}
|
}
|
||||||
|
|
||||||
function onEscape() {
|
function onEscape() {
|
||||||
|
|
|
||||||
89
src/ui/components/layout/PluginInstallDialog.svelte
Normal file
89
src/ui/components/layout/PluginInstallDialog.svelte
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
<script>
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
import Modal from "./Modal.svelte";
|
||||||
|
import Button from "../input/Button.svelte";
|
||||||
|
import { Puzzle, Download, X } from "lucide-svelte";
|
||||||
|
|
||||||
|
export let width = "500px";
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
let modalRef;
|
||||||
|
let installing = false;
|
||||||
|
|
||||||
|
function onInstall() {
|
||||||
|
installing = true;
|
||||||
|
dispatch("install");
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDismiss() {
|
||||||
|
modalRef.dismiss();
|
||||||
|
dispatch("dismiss");
|
||||||
|
}
|
||||||
|
|
||||||
|
function onEscape() {
|
||||||
|
onDismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dismiss() {
|
||||||
|
modalRef.dismiss();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal title="Ignis Bridge Plugin" {width} bind:this={modalRef} on:escape={onEscape} closeOnOverlayClick={false}>
|
||||||
|
<svelte:fragment slot="icon">
|
||||||
|
<Puzzle size="1.25rem" />
|
||||||
|
</svelte:fragment>
|
||||||
|
|
||||||
|
<div class="dialog-body">
|
||||||
|
<p class="dialog-message">This vault doesn't have the Ignis Bridge plugin installed.</p>
|
||||||
|
<p class="dialog-description">
|
||||||
|
The plugin adds additional functionality such as file uploads.
|
||||||
|
Obsidian will work without it, but some features will be unavailable.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<svelte:fragment slot="footer">
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<Button variant="secondary" on:click={onDismiss}>
|
||||||
|
<svelte:fragment slot="icon">
|
||||||
|
<X size="0.875rem" />
|
||||||
|
</svelte:fragment>
|
||||||
|
Not Now
|
||||||
|
</Button>
|
||||||
|
<Button variant="primary" on:click={onInstall} disabled={installing}>
|
||||||
|
<svelte:fragment slot="icon">
|
||||||
|
<Download size="0.875rem" />
|
||||||
|
</svelte:fragment>
|
||||||
|
{installing ? "Installing..." : "Install Plugin"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</svelte:fragment>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.dialog-body {
|
||||||
|
padding: 1.25rem 1.5rem;
|
||||||
|
border-bottom: 1px solid var(--background-modifier-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-message {
|
||||||
|
margin: 0 0 0.5rem;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-description {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -2,3 +2,4 @@ export { default as VaultManager } from "./views/VaultManager.svelte";
|
||||||
export { default as MessageDialog } from "./components/layout/MessageDialog.svelte";
|
export { default as MessageDialog } from "./components/layout/MessageDialog.svelte";
|
||||||
export { default as ConfirmDialog } from "./components/layout/ConfirmDialog.svelte";
|
export { default as ConfirmDialog } from "./components/layout/ConfirmDialog.svelte";
|
||||||
export { default as PromptDialog } from "./components/layout/PromptDialog.svelte";
|
export { default as PromptDialog } from "./components/layout/PromptDialog.svelte";
|
||||||
|
export { default as PluginInstallDialog } from "./components/layout/PluginInstallDialog.svelte";
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@
|
||||||
let dialogValue = "";
|
let dialogValue = "";
|
||||||
let errorMessage = "";
|
let errorMessage = "";
|
||||||
let pendingReload = false;
|
let pendingReload = false;
|
||||||
|
let version = "";
|
||||||
|
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{ id: "rename", label: "Rename" },
|
{ id: "rename", label: "Rename" },
|
||||||
|
|
@ -47,6 +48,16 @@
|
||||||
)
|
)
|
||||||
: vaults;
|
: vaults;
|
||||||
|
|
||||||
|
async function fetchVersion() {
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/version");
|
||||||
|
const data = await res.json();
|
||||||
|
version = data.version;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("[VaultManager] Failed to fetch version:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function refreshVaults() {
|
async function refreshVaults() {
|
||||||
try {
|
try {
|
||||||
vaults = await vaultService.listVaults();
|
vaults = await vaultService.listVaults();
|
||||||
|
|
@ -177,6 +188,7 @@
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
refreshVaults();
|
refreshVaults();
|
||||||
|
fetchVersion();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -246,6 +258,11 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<svelte:fragment slot="footer">
|
<svelte:fragment slot="footer">
|
||||||
|
<div class="footer-left">
|
||||||
|
{#if version}
|
||||||
|
<span class="version-info">Ignis v{version}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
<div class="footer-right">
|
<div class="footer-right">
|
||||||
<Button variant="ghost" on:click={showCreateDialog}>
|
<Button variant="ghost" on:click={showCreateDialog}>
|
||||||
<svelte:fragment slot="icon">
|
<svelte:fragment slot="icon">
|
||||||
|
|
@ -400,8 +417,19 @@
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.footer-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.footer-right {
|
.footer-right {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.version-info {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue