maws7140_vault-hub-plugin/main.js
Maws 9870c92706 Release 1.0.4
- Replace free-text path entry with searchable file pickers for notes, bundles, and screenshots
- Fix crash (reading 'path') when publishing from a restored draft; guard empty selections
- Make draft hydration type-aware so snippet/markdown/image selections restore correctly
- Trim settings to just the GitHub token (remove Hub URL, Catalog repo, Default categories)
- Remove dead path-parsing helpers; fix unnecessary type assertion in BrowseView
2026-07-03 15:10:39 -04:00

2738 lines
104 KiB
JavaScript

/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
If you want to view the source, please visit the GitHub repository of this plugin.
*/
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/main.ts
var main_exports = {};
__export(main_exports, {
default: () => VaultHubPlugin
});
module.exports = __toCommonJS(main_exports);
var import_obsidian7 = require("obsidian");
// src/settings.ts
var import_obsidian = require("obsidian");
var DEFAULT_SETTINGS = {
githubToken: "",
defaultCategories: [],
vaultHubUrl: "https://obsidianvaulthub.com",
catalogRepoFullName: "Maws7140/vault-hub",
publishedResources: [],
publishDraft: null
};
var VaultHubSettingTab = class extends import_obsidian.PluginSettingTab {
constructor(app, plugin) {
super(app, plugin);
this.plugin = plugin;
}
display() {
const { containerEl } = this;
containerEl.empty();
new import_obsidian.Setting(containerEl).setName("GitHub personal access token").setDesc("Token used to create repos and push files. Requires the repo scope.").addText((text) => {
text.setPlaceholder("GitHub personal access token").setValue(this.plugin.settings.githubToken);
text.inputEl.type = "password";
text.inputEl.addClass("vault-hub-text-input-wide");
text.onChange((value) => {
this.plugin.settings.githubToken = value;
void this.plugin.saveSettings();
});
});
}
};
// src/modals/PublishModal.ts
var import_obsidian4 = require("obsidian");
// src/detection.ts
function parsePluginManifest(raw) {
const parsed = JSON.parse(raw);
if (typeof parsed !== "object" || parsed === null) return null;
const { id, name, version, author } = parsed;
if (typeof id !== "string" || typeof name !== "string" || typeof version !== "string" || typeof author !== "string") {
return null;
}
return { id, name, version, author };
}
var MD_PATTERNS = [
[/```dataviewjs/m, "dataview"],
[/```dataview/m, "dataview"],
[/```datacore/m, "datacore"],
[/```tasks/m, "obsidian-tasks-plugin"],
[/```kanban/m, "obsidian-kanban"],
[/```excalidraw/m, "obsidian-excalidraw-plugin"],
[/```chart/m, "obsidian-charts"],
[/```cards-timeline/m, "any-block"],
[/```button/m, "buttons"],
[/```meta-bind/m, "obsidian-meta-bind-plugin"],
[/`=\s*this\./m, "dataview"],
[/`=\s*dv\./m, "dataview"],
[/`\$=\s*dv\./m, "dataview"],
[/<%\s*tp\./m, "templater-obsidian"],
[/<%[-_*]?\s/m, "templater-obsidian"],
[/kanban-plugin:/m, "obsidian-kanban"],
[/^banner:/m, "obsidian-banners"]
];
var CSS_PATTERNS = [
[/\.dataview/i, "dataview"],
[/\.kanban/i, "obsidian-kanban"],
[/\.tasks-plugin/i, "obsidian-tasks-plugin"],
[/\.excalidraw/i, "obsidian-excalidraw-plugin"],
[/\.cm-table-widget/i, "table-editor-obsidian"]
];
async function getInstalledPlugins(vault) {
const plugins = [];
const pluginsDir = `${vault.configDir}/plugins`;
const listing = await vault.adapter.list(pluginsDir);
for (const folder of listing.folders) {
const manifestPath = `${folder}/manifest.json`;
try {
const raw = await vault.adapter.read(manifestPath);
const manifest = parsePluginManifest(raw);
if (!manifest) continue;
plugins.push({
id: manifest.id,
name: manifest.name,
version: manifest.version,
author: manifest.author,
autoDetected: false
});
} catch (e) {
}
}
return plugins;
}
async function detectPlugins(fileContent, fileType, vault) {
const installed = await getInstalledPlugins(vault);
const installedIds = new Set(installed.map((p) => p.id));
const detectedIds = /* @__PURE__ */ new Set();
const patterns = fileType === "css" ? CSS_PATTERNS : MD_PATTERNS;
for (const [regex, pluginId] of patterns) {
if (regex.test(fileContent) && installedIds.has(pluginId)) {
detectedIds.add(pluginId);
}
}
return installed.map((p) => ({
...p,
autoDetected: detectedIds.has(p.id)
}));
}
// src/net.ts
var import_obsidian2 = require("obsidian");
function buildError(status, text) {
return new Error(`HTTP ${status}: ${text}`);
}
async function requestJson(url, options = {}) {
const response = await (0, import_obsidian2.requestUrl)({
url,
method: options.method || "GET",
headers: options.headers,
body: options.body,
throw: false
});
if (response.status >= 400) {
throw buildError(response.status, response.text || "Request failed");
}
return response.json;
}
async function requestText(url, options = {}) {
const response = await (0, import_obsidian2.requestUrl)({
url,
method: options.method || "GET",
headers: options.headers,
body: options.body,
throw: false
});
if (response.status >= 400) {
throw buildError(response.status, response.text || "Request failed");
}
return response.text;
}
// src/github.ts
function encodeUtf8ToBase64(input) {
const bytes = new TextEncoder().encode(input);
let binary = "";
for (const b of bytes) binary += String.fromCharCode(b);
return btoa(binary);
}
function decodeBase64ToUtf8(input) {
const binary = atob(input);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
return new TextDecoder().decode(bytes);
}
var GitHubAPI = class {
constructor(token) {
this.token = token;
}
async request(path, options = {}) {
try {
return await requestJson(`https://api.github.com${path}`, {
method: options.method,
body: typeof options.body === "string" ? options.body : void 0,
headers: {
Authorization: `token ${this.token}`,
Accept: "application/vnd.github.v3+json",
"Content-Type": "application/json",
...options.headers
}
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
const httpMatch = message.match(/^HTTP (\d+):\s*([\s\S]*)$/);
if (httpMatch) {
const wrapped = new Error(`GitHub API ${httpMatch[1]}: ${httpMatch[2]}`);
wrapped.cause = error;
throw wrapped;
}
throw error;
}
}
encodePath(path) {
return path.split("/").map(encodeURIComponent).join("/");
}
async getUser() {
return this.request("/user");
}
async listAuthenticatedRepos() {
const repos = [];
for (let page = 1; page <= 5; page++) {
const batch = await this.request(
`/user/repos?per_page=100&page=${page}&affiliation=owner&sort=updated`
);
if (!Array.isArray(batch) || batch.length === 0) break;
repos.push(...batch);
if (batch.length < 100) break;
}
return repos;
}
async createRepo(name, description) {
return this.request("/user/repos", {
method: "POST",
body: JSON.stringify({
name,
description,
auto_init: false,
private: false
})
});
}
async addTopics(owner, repo, topics) {
return this.request(`/repos/${owner}/${repo}/topics`, {
method: "PUT",
body: JSON.stringify({ names: topics })
});
}
async createFile(owner, repo, path, content, message) {
const encoded = encodeUtf8ToBase64(content);
return this.request(`/repos/${owner}/${repo}/contents/${this.encodePath(path)}`, {
method: "PUT",
body: JSON.stringify({ message, content: encoded })
});
}
async createBinaryFile(owner, repo, path, base64Content, message) {
return this.request(`/repos/${owner}/${repo}/contents/${this.encodePath(path)}`, {
method: "PUT",
body: JSON.stringify({ message, content: base64Content, encoding: "base64" })
});
}
async getFileSha(owner, repo, path) {
try {
const data = await this.request(
`/repos/${owner}/${repo}/contents/${this.encodePath(path)}`
);
return typeof (data == null ? void 0 : data.sha) === "string" ? data.sha : null;
} catch (error) {
if (String(error).includes("GitHub API 404")) return null;
throw error;
}
}
async updateFile(owner, repo, path, content, message, sha) {
const encoded = encodeUtf8ToBase64(content);
return this.request(`/repos/${owner}/${repo}/contents/${this.encodePath(path)}`, {
method: "PUT",
body: JSON.stringify({ message, content: encoded, sha })
});
}
async updateBinaryFile(owner, repo, path, base64Content, message, sha) {
return this.request(`/repos/${owner}/${repo}/contents/${this.encodePath(path)}`, {
method: "PUT",
body: JSON.stringify({ message, content: base64Content, encoding: "base64", sha })
});
}
async upsertFile(owner, repo, path, content, message) {
const sha = await this.getFileSha(owner, repo, path);
if (sha) {
return this.updateFile(owner, repo, path, content, message, sha);
}
return this.createFile(owner, repo, path, content, message);
}
async upsertBinaryFile(owner, repo, path, base64Content, message) {
const sha = await this.getFileSha(owner, repo, path);
if (sha) {
return this.updateBinaryFile(owner, repo, path, base64Content, message, sha);
}
return this.createBinaryFile(owner, repo, path, base64Content, message);
}
async getFileContent(owner, repo, path) {
try {
const data = await this.request(
`/repos/${owner}/${repo}/contents/${this.encodePath(path)}`
);
if (typeof data.sha !== "string" || typeof data.content !== "string") {
throw new Error(`Unexpected GitHub contents response for ${owner}/${repo}/${path}`);
}
return { sha: data.sha, content: decodeBase64ToUtf8(data.content.replace(/\s/g, "")) };
} catch (error) {
if (String(error).includes("GitHub API 404")) return null;
throw error;
}
}
async listFilesRecursive(owner, repo) {
const data = await this.request(
`/repos/${owner}/${repo}/git/trees/HEAD?recursive=1`
);
const tree = Array.isArray(data.tree) ? data.tree.filter(isGitHubTreeItem) : [];
if (tree.length === 0) return [];
return tree.filter((item) => item.type === "blob").map((item) => ({
path: item.path,
sha: item.sha,
download_url: `https://raw.githubusercontent.com/${owner}/${repo}/HEAD/${this.encodePath(item.path)}`,
type: "file"
}));
}
async fileExists(owner, repo, path) {
return await this.getFileContent(owner, repo, path) !== null;
}
async getRepo(owner, repo) {
return this.request(`/repos/${owner}/${repo}`);
}
async getAvailableRepoName(owner, baseName) {
let candidate = baseName;
let suffix = 2;
for (let attempt = 0; attempt < 100; attempt++) {
try {
await this.getRepo(owner, candidate);
candidate = `${baseName}-${suffix}`;
suffix++;
} catch (error) {
if (String(error).includes("GitHub API 404")) return candidate;
throw error;
}
}
throw new Error(`Could not find an available repo name based on "${baseName}".`);
}
async dispatchRepositoryEvent(owner, repo, eventType, clientPayload) {
return this.request(`/repos/${owner}/${repo}/dispatches`, {
method: "POST",
body: JSON.stringify({
event_type: eventType,
client_payload: clientPayload || {}
})
});
}
};
function isGitHubTreeItem(value) {
if (typeof value !== "object" || value === null) return false;
const item = value;
return typeof item.path === "string" && typeof item.sha === "string" && typeof item.type === "string";
}
// src/hubmd.ts
function generateHubMd(data) {
const lines = [];
lines.push("---");
lines.push("schema: 1");
lines.push(`type: ${data.type}`);
lines.push(`name: "${esc(data.name)}"`);
lines.push(`tagline: "${esc(data.tagline)}"`);
lines.push("description: |");
data.description.split("\n").forEach((line) => lines.push(` ${line}`));
lines.push(`author: ${data.author}`);
lines.push("categories:");
data.categories.forEach((category) => lines.push(` - ${category}`));
if (data.tags.length > 0) {
lines.push("tags:");
data.tags.forEach((tag) => lines.push(` - ${tag}`));
}
if (data.compatibleThemes.length > 0) {
lines.push("compatible_themes:");
data.compatibleThemes.forEach((theme) => lines.push(` - ${theme}`));
}
if (data.screenshots.length > 0) {
lines.push("screenshots:");
data.screenshots.forEach((screenshot) => lines.push(` - ${screenshot}`));
}
const selected = data.plugins.filter((plugin) => plugin.autoDetected);
if (selected.length > 0) {
lines.push("plugins:");
selected.forEach((plugin) => {
lines.push(` - id: ${plugin.id}`);
lines.push(` name: "${esc(plugin.name)}"`);
lines.push(` version: "${esc(plugin.version)}"`);
});
}
if (data.attachedSnippets.length > 0) {
lines.push("attached_snippets:");
data.attachedSnippets.forEach((snippet) => {
lines.push(` - path: "${esc(snippet.path)}"`);
if (snippet.name) lines.push(` name: "${esc(snippet.name)}"`);
if (snippet.optional) lines.push(" optional: true");
});
}
lines.push("environment:");
lines.push(` obsidian_version: "${esc(data.obsidianVersion)}"`);
lines.push(` theme: "${esc(data.theme)}"`);
lines.push(` os: "${esc(data.os)}"`);
lines.push("files:");
data.files.forEach((file) => {
lines.push(` - path: "${esc(file.path)}"`);
lines.push(` type: ${file.type}`);
lines.push(` size: ${file.size}`);
});
lines.push("---");
lines.push("");
lines.push(data.body.trim());
lines.push("");
return lines.join("\n");
}
function esc(value) {
return String(value || "").replace(/\\/g, "\\\\").replace(/"/g, '\\"');
}
// src/paths.ts
var import_obsidian3 = require("obsidian");
function getSnippetDirectory(vault) {
return (0, import_obsidian3.normalizePath)(`${vault.configDir}/snippets`);
}
// src/readme.ts
function generateReadme(data) {
var _a;
const lines = [];
lines.push(`# ${data.name}`);
lines.push("");
lines.push(data.tagline);
lines.push("");
lines.push("## Description");
lines.push("");
lines.push(data.description);
lines.push("");
const selected = data.plugins.filter((p) => p.autoDetected);
const attachedSnippets = data.attachedSnippets || [];
const screenshots = data.screenshots || [];
if (selected.length > 0) {
lines.push("## Required Plugins");
lines.push("");
lines.push("| Plugin | Version | Link |");
lines.push("|--------|---------|------|");
selected.forEach((p) => {
lines.push(
`| ${p.name} | ${p.version} | [GitHub](https://github.com/search?q=${encodeURIComponent(p.name + " obsidian")}) |`
);
});
lines.push("");
}
if (attachedSnippets.length > 0) {
lines.push("## Attached Snippets");
lines.push("");
attachedSnippets.forEach((snippet) => {
lines.push(
`- \`${snippet.path}\`${snippet.optional ? " (optional)" : ""}${snippet.name ? ` - ${snippet.name}` : ""}`
);
});
lines.push("");
}
if (screenshots.length > 0) {
lines.push("## Screenshots");
lines.push("");
screenshots.forEach((screenshot) => {
const alt = screenshot.alt || screenshot.path.split("/").pop() || "Screenshot";
lines.push(`![${alt}](${screenshot.path})`);
lines.push("");
});
}
lines.push("## Installation");
lines.push("");
if (data.type === "snippet") {
lines.push(
`1. Download \`${((_a = data.files[0]) == null ? void 0 : _a.path) || "snippet.css"}\` from this repo`
);
lines.push("2. Place it in your vault's CSS snippets folder");
lines.push("3. Enable it in Settings > Appearance > CSS Snippets");
} else if (data.type === "vault") {
lines.push("1. Download or clone this repo");
lines.push("2. Open the folder as an Obsidian vault");
if (attachedSnippets.length > 0) {
lines.push("3. Copy the attached CSS snippet files into your vault's CSS snippets folder");
lines.push("4. Enable them in Settings > Appearance > CSS Snippets");
if (selected.length > 0) {
lines.push("5. Install or enable the required plugins listed above");
}
} else if (selected.length > 0) {
lines.push("3. Install or enable the required plugins listed above");
}
} else {
lines.push("1. Download the `.md` file(s) from this repo");
lines.push("2. Place them in your vault");
if (attachedSnippets.length > 0) {
lines.push("3. Copy the attached CSS snippet files into your vault's CSS snippets folder");
lines.push("4. Enable them in Settings > Appearance > CSS Snippets");
if (selected.length > 0) {
lines.push("5. Install the required plugins listed above");
}
} else if (selected.length > 0) {
lines.push("3. Install the required plugins listed above");
}
}
lines.push("");
lines.push("---");
lines.push("*Published via [Vault Hub](https://obsidianvaulthub.com)*");
lines.push("");
return lines.join("\n");
}
function syncReadmeScreenshots(readme, screenshots) {
const normalized = readme.replace(/\r\n/g, "\n");
const section = buildScreenshotSection(screenshots);
const pattern = /## Screenshots\n\n[\s\S]*?(?=\n## |\n---\n|\s*$)/;
if (section) {
if (pattern.test(normalized)) {
return normalized.replace(pattern, section.trimEnd());
}
const installHeading = "\n## Installation";
if (normalized.includes(installHeading)) {
return normalized.replace(installHeading, `
${section}${installHeading}`);
}
return `${normalized.trimEnd()}
${section}`;
}
if (pattern.test(normalized)) {
return `${normalized.replace(pattern, "").replace(/\n{3,}/g, "\n\n").trimEnd()}
`;
}
return normalized;
}
function buildScreenshotSection(screenshots) {
if (screenshots.length === 0) return "";
const lines = ["## Screenshots", ""];
screenshots.forEach((screenshot) => {
const alt = screenshot.alt || screenshot.path.split("/").pop() || "Screenshot";
lines.push(`![${alt}](${screenshot.path})`);
lines.push("");
});
return `${lines.join("\n")}
`;
}
// src/modals/PublishModal.ts
var RESERVED_ROOT_PATHS = /* @__PURE__ */ new Set(["readme.md", "hub.md"]);
function isReservedRootPath(path) {
return RESERVED_ROOT_PATHS.has(path.trim().replace(/\\/g, "/").toLowerCase());
}
async function listSnippetFiles(app) {
const adapter = app.vault.adapter;
const dir = getSnippetDirectory(app.vault);
try {
const exists = await adapter.exists(dir);
if (!exists) return [];
const { files } = await adapter.list(dir);
const cssFiles = files.filter((p) => p.toLowerCase().endsWith(".css"));
return Promise.all(
cssFiles.map(async (path) => {
var _a;
const name = path.split("/").pop() || path;
let size = 0;
try {
const stat = await adapter.stat(path);
size = (_a = stat == null ? void 0 : stat.size) != null ? _a : 0;
} catch (e) {
}
return {
path,
name,
extension: "css",
size,
read: () => adapter.read(path),
readBinary: () => adapter.readBinary(path)
};
})
);
} catch (e) {
return [];
}
}
async function listMarkdownFiles(app) {
const adapter = app.vault.adapter;
return app.vault.getMarkdownFiles().filter((file) => !isReservedRootPath(file.path)).map((file) => {
var _a, _b;
return {
path: file.path,
name: file.name,
extension: "md",
size: (_b = (_a = file.stat) == null ? void 0 : _a.size) != null ? _b : 0,
read: () => adapter.read(file.path),
readBinary: () => adapter.readBinary(file.path)
};
}).sort((a, b) => a.path.localeCompare(b.path));
}
async function listImageFiles(app) {
const adapter = app.vault.adapter;
return app.vault.getFiles().filter((file) => IMAGE_EXTENSIONS.has(file.extension.toLowerCase())).map((file) => {
var _a, _b;
return {
path: file.path,
name: file.name,
extension: file.extension.toLowerCase(),
size: (_b = (_a = file.stat) == null ? void 0 : _a.size) != null ? _b : 0,
read: () => adapter.read(file.path),
readBinary: () => adapter.readBinary(file.path)
};
}).sort((a, b) => a.path.localeCompare(b.path));
}
async function getEnabledSnippetIds(app) {
try {
const raw = await app.vault.adapter.read(`${app.vault.configDir}/appearance.json`);
const parsed = parseAppearanceConfig(raw);
return new Set(parsed.enabledCssSnippets.map((value) => value.toLowerCase()));
} catch (e) {
return /* @__PURE__ */ new Set();
}
}
function snippetIdFromFile(file) {
return file.name.replace(/\.css$/i, "").toLowerCase();
}
function toBase64(data) {
return Buffer.from(data).toString("base64");
}
var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set(["png", "jpg", "jpeg", "gif", "webp", "svg"]);
var CATEGORIES = {
snippet: [
"ui-tweak",
"layout",
"typography",
"colors",
"editor",
"sidebar",
"dashboard",
"starter",
"ai"
],
note: [
"dashboard",
"tracker",
"query",
"daily-note",
"project-template",
"kanban",
"book-notes",
"habit-tracker",
"ai"
],
bundle: [
"starter",
"student",
"developer",
"writer",
"researcher",
"ai",
"pkm",
"project-management",
"worldbuilding",
"journaling",
"dashboard",
"tracker",
"finance"
]
};
var PublishModal = class extends import_obsidian4.Modal {
constructor(app, plugin) {
super(app);
this.step = 1;
// Step 1
this.resourceType = "snippet";
this.selectedFiles = [];
this.selectedAttachedSnippets = [];
this.selectedScreenshots = [];
this.externalScreenshotUrls = "";
this.fileSearchQuery = "";
this.attachedSnippetSearchQuery = "";
this.screenshotSearchQuery = "";
// Step 2
this.allPlugins = [];
this.checkedPlugins = /* @__PURE__ */ new Set();
// Step 3
this.name = "";
this.tagline = "";
this.description = "";
this.categories = [];
this.tags = "";
this.compatibleThemes = [];
// Step 4
this.readmeContent = "";
this.pendingSelectedFilePaths = null;
this.pendingAttachedSnippetPaths = null;
this.pendingScreenshotPaths = null;
this.restoredDraft = false;
this.preserveDraftOnClose = true;
this.plugin = plugin;
}
getSnippetDir() {
return getSnippetDirectory(this.app.vault);
}
onOpen() {
this.contentEl.empty();
this.contentEl.createEl("p", { text: "Loading publish draft...", cls: "vault-hub-hint" });
void this.initialize();
}
onClose() {
if (this.preserveDraftOnClose) {
void this.persistDraftOnClose();
}
this.contentEl.empty();
}
getPublishedType() {
if (this.resourceType === "snippet") return "snippet";
if (this.resourceType === "bundle") return "vault";
return "note";
}
async initialize() {
this.restoreDraft();
await this.hydrateDraftSelections();
this.renderStep();
}
restoreDraft() {
const draft = this.plugin.settings.publishDraft;
if (!draft) return;
this.step = Math.min(Math.max(draft.step || 1, 1), 5);
this.resourceType = draft.resourceType || "snippet";
this.pendingSelectedFilePaths = [...draft.selectedFilePaths || []];
this.pendingAttachedSnippetPaths = [...draft.attachedSnippetPaths || []];
this.pendingScreenshotPaths = [...draft.screenshotPaths || []];
this.externalScreenshotUrls = draft.externalScreenshotUrls || "";
this.fileSearchQuery = draft.fileSearchQuery || "";
this.attachedSnippetSearchQuery = draft.attachedSnippetSearchQuery || "";
this.screenshotSearchQuery = draft.screenshotSearchQuery || "";
this.checkedPlugins = new Set(draft.checkedPluginIds || []);
this.name = draft.name || "";
this.tagline = draft.tagline || "";
this.description = draft.description || "";
this.categories = [...draft.categories || []];
this.tags = draft.tags || "";
this.compatibleThemes = [...draft.compatibleThemes || []];
this.readmeContent = draft.readmeContent || "";
this.restoredDraft = true;
}
async persistDraftOnClose() {
if (!this.hasDraftContent()) {
if (this.plugin.settings.publishDraft) {
this.plugin.settings.publishDraft = null;
await this.plugin.saveSettings();
}
return;
}
await this.saveDraft();
}
async hydrateDraftSelections() {
if (this.pendingSelectedFilePaths) {
const source = this.resourceType === "snippet" ? await listSnippetFiles(this.app) : await listMarkdownFiles(this.app);
this.restoreSelectionFromPaths(source, this.pendingSelectedFilePaths, (restored) => {
this.selectedFiles = restored;
this.pendingSelectedFilePaths = null;
});
}
if ((this.resourceType === "note" || this.resourceType === "bundle") && this.pendingAttachedSnippetPaths) {
const snippetFiles = await listSnippetFiles(this.app);
this.restoreSelectionFromPaths(
snippetFiles,
this.pendingAttachedSnippetPaths,
(restored) => {
this.selectedAttachedSnippets = restored;
this.pendingAttachedSnippetPaths = null;
}
);
}
if (this.pendingScreenshotPaths) {
const imageFiles = await listImageFiles(this.app);
this.restoreSelectionFromPaths(imageFiles, this.pendingScreenshotPaths, (restored) => {
this.selectedScreenshots = restored;
this.pendingScreenshotPaths = null;
});
}
}
hasDraftContent() {
var _a, _b, _c;
return this.step > 1 || this.selectedFiles.length > 0 || this.selectedAttachedSnippets.length > 0 || this.selectedScreenshots.length > 0 || (((_a = this.pendingSelectedFilePaths) == null ? void 0 : _a.length) || 0) > 0 || (((_b = this.pendingAttachedSnippetPaths) == null ? void 0 : _b.length) || 0) > 0 || (((_c = this.pendingScreenshotPaths) == null ? void 0 : _c.length) || 0) > 0 || this.externalScreenshotUrls.trim().length > 0 || this.name.trim().length > 0 || this.tagline.trim().length > 0 || this.description.trim().length > 0 || this.categories.length > 0 || this.tags.trim().length > 0 || this.compatibleThemes.length > 0 || this.readmeContent.trim().length > 0;
}
buildDraft() {
return {
step: this.step,
resourceType: this.resourceType,
selectedFilePaths: this.selectedFiles.length > 0 ? this.selectedFiles.map((file) => file.path) : [...this.pendingSelectedFilePaths || []],
attachedSnippetPaths: this.selectedAttachedSnippets.length > 0 ? this.selectedAttachedSnippets.map((file) => file.path) : [...this.pendingAttachedSnippetPaths || []],
screenshotPaths: this.selectedScreenshots.length > 0 ? this.selectedScreenshots.map((file) => file.path) : [...this.pendingScreenshotPaths || []],
externalScreenshotUrls: this.externalScreenshotUrls,
fileSearchQuery: this.fileSearchQuery,
attachedSnippetSearchQuery: this.attachedSnippetSearchQuery,
screenshotSearchQuery: this.screenshotSearchQuery,
checkedPluginIds: [...this.checkedPlugins],
name: this.name,
tagline: this.tagline,
description: this.description,
categories: [...this.categories],
tags: this.tags,
compatibleThemes: [...this.compatibleThemes],
readmeContent: this.readmeContent
};
}
async saveDraft() {
this.plugin.settings.publishDraft = this.buildDraft();
await this.plugin.saveSettings();
}
async discardDraft() {
this.plugin.settings.publishDraft = null;
await this.plugin.saveSettings();
}
resetState() {
this.step = 1;
this.resourceType = "snippet";
this.selectedFiles = [];
this.selectedAttachedSnippets = [];
this.selectedScreenshots = [];
this.externalScreenshotUrls = "";
this.fileSearchQuery = "";
this.attachedSnippetSearchQuery = "";
this.screenshotSearchQuery = "";
this.allPlugins = [];
this.checkedPlugins = /* @__PURE__ */ new Set();
this.name = "";
this.tagline = "";
this.description = "";
this.categories = [];
this.tags = "";
this.compatibleThemes = [];
this.readmeContent = "";
this.pendingSelectedFilePaths = null;
this.pendingAttachedSnippetPaths = null;
this.pendingScreenshotPaths = null;
this.restoredDraft = false;
}
restoreSelectionFromPaths(files, pendingPaths, assign) {
if (!pendingPaths) return;
const wanted = new Set(pendingPaths);
assign(files.filter((file) => wanted.has(file.path)));
}
renderStep() {
this.contentEl.empty();
this.contentEl.addClass("vault-hub-modal");
const header = this.contentEl.createDiv("vault-hub-header");
header.createEl("h2", { text: `Publish resource - step ${this.step} of 5` });
const progress = header.createDiv("vault-hub-progress");
for (let i = 1; i <= 5; i++) {
const dot = progress.createSpan("vault-hub-dot");
if (i === this.step) dot.addClass("active");
else if (i < this.step) dot.addClass("done");
}
if (this.restoredDraft) {
const draftBar = this.contentEl.createDiv("vault-hub-hint");
draftBar.setText("Restored your saved publish draft.");
const discardBtn = draftBar.createEl("button", { text: "Start over" });
discardBtn.type = "button";
discardBtn.addClass("vault-hub-discard-btn");
discardBtn.addEventListener("click", () => void this.resetDraftAndRender());
}
switch (this.step) {
case 1:
void this.renderStep1();
break;
case 2:
void this.renderStep2();
break;
case 3:
this.renderStep3();
break;
case 4:
this.renderStep4();
break;
case 5:
this.renderStep5();
break;
}
}
async renderStep1() {
const c = this.contentEl;
new import_obsidian4.Setting(c).setName("Resource type").addDropdown((dd) => {
dd.addOption("snippet", "CSS snippet");
dd.addOption("note", "Note / template / dashboard");
dd.addOption("bundle", "Vault / multi-file template");
dd.setValue(this.resourceType);
dd.onChange((v) => {
this.resourceType = v;
this.selectedFiles = [];
this.selectedAttachedSnippets = [];
this.checkedPlugins = /* @__PURE__ */ new Set();
this.allPlugins = [];
this.pendingSelectedFilePaths = null;
this.pendingAttachedSnippetPaths = null;
this.renderStep();
});
});
const isBundle = this.resourceType === "bundle";
const files = this.resourceType === "snippet" ? await this.collectCandidateFiles() : await listMarkdownFiles(this.app);
const availableSnippets = this.resourceType === "note" ? await listSnippetFiles(this.app) : [];
this.restoreSelectionFromPaths(files, this.pendingSelectedFilePaths, (restored) => {
this.selectedFiles = restored;
this.pendingSelectedFilePaths = null;
});
if (this.resourceType === "note") {
this.restoreSelectionFromPaths(
availableSnippets,
this.pendingAttachedSnippetPaths,
(restored) => {
this.selectedAttachedSnippets = restored;
this.pendingAttachedSnippetPaths = null;
}
);
}
const fileSection = c.createDiv();
fileSection.createEl("h4", { text: `Select file${isBundle ? "s" : ""}` });
const fileSearch = fileSection.createEl("input", {
type: "text",
placeholder: "Search files...",
cls: "vault-hub-search-input"
});
fileSearch.value = this.fileSearchQuery;
fileSearch.addEventListener("input", () => {
this.fileSearchQuery = fileSearch.value;
renderFileList();
});
const emptySourceText = this.resourceType === "snippet" ? `No CSS snippets found in ${this.getSnippetDir()}. Drop .css files there if nothing shows up.` : isBundle ? "No markdown notes found in this vault." : "No markdown notes found in this vault. Root README.md and hub.md are managed by Vault Hub and are excluded.";
if (files.length === 0) {
fileSection.createEl("p", { text: emptySourceText, cls: "vault-hub-hint" });
} else if (this.resourceType !== "snippet") {
fileSection.createEl("p", {
text: isBundle ? "Pick the markdown notes to publish. Root README.md and hub.md are managed by Vault Hub and are excluded." : "Pick the markdown note to publish.",
cls: "vault-hub-hint"
});
}
let count = null;
if (isBundle && files.length > 0) {
const bulk = fileSection.createDiv("vault-hub-bulk");
const selectAll = bulk.createEl("button", { text: "Select all" });
selectAll.type = "button";
selectAll.addEventListener("click", () => {
this.selectedFiles = files.slice();
renderFileList();
});
const clearAll = bulk.createEl("button", { text: "Clear" });
clearAll.type = "button";
clearAll.addEventListener("click", () => {
this.selectedFiles = [];
renderFileList();
});
count = bulk.createSpan({ cls: "vault-hub-bulk-count" });
}
const list = fileSection.createDiv("vault-hub-file-list");
const emptyFileSearch = fileSection.createEl("p", {
text: "No files match that search.",
cls: "vault-hub-hint vault-hub-hidden"
});
const renderFileList = () => {
const fileSearchNeedle = this.fileSearchQuery.trim().toLowerCase();
const visibleFiles = fileSearchNeedle ? files.filter((file) => file.path.toLowerCase().includes(fileSearchNeedle)) : files;
const selectedPaths = new Set(this.selectedFiles.map((f) => f.path));
list.empty();
emptyFileSearch.toggleClass("vault-hub-hidden", !(files.length > 0 && visibleFiles.length === 0));
if (count) {
count.setText(`${this.selectedFiles.length} / ${files.length} selected`);
}
visibleFiles.forEach((f) => {
const row = list.createDiv("vault-hub-file-row");
const cb = row.createEl("input", { type: isBundle ? "checkbox" : "radio" });
cb.name = "vault-hub-file";
cb.checked = selectedPaths.has(f.path);
cb.addEventListener("change", () => {
if (isBundle) {
if (cb.checked) this.selectedFiles.push(f);
else this.selectedFiles = this.selectedFiles.filter((x) => x.path !== f.path);
} else {
this.selectedFiles = cb.checked ? [f] : [];
}
if (count) {
count.setText(`${this.selectedFiles.length} / ${files.length} selected`);
}
});
row.createSpan({ text: f.path });
});
};
renderFileList();
if (this.resourceType === "note") {
const snippetSection = c.createDiv();
snippetSection.createEl("h4", { text: "Attach CSS snippets" });
snippetSection.createEl("p", {
text: "Optional. These will be uploaded into the repo and listed in hub.md so install can pull them automatically.",
cls: "vault-hub-hint"
});
const filterSnippets = () => {
const needle = this.attachedSnippetSearchQuery.trim().toLowerCase();
if (!needle) return availableSnippets;
return availableSnippets.filter((file) => {
const haystack = `${file.name} ${file.path}`.toLowerCase();
return haystack.includes(needle);
});
};
const attachedSnippetSearch = snippetSection.createEl("input", {
type: "text",
placeholder: "Search snippets...",
cls: "vault-hub-search-input"
});
attachedSnippetSearch.value = this.attachedSnippetSearchQuery;
if (availableSnippets.length === 0) {
snippetSection.createEl("p", {
text: `No CSS snippets found in ${this.getSnippetDir()}.`,
cls: "vault-hub-hint"
});
} else {
const snippetList = snippetSection.createDiv("vault-hub-file-list");
const emptySnippetSearch = snippetSection.createEl("p", {
text: "No snippets match that search.",
cls: "vault-hub-hint vault-hub-hidden"
});
const renderSnippetList = () => {
const visibleSnippets = filterSnippets();
const selectedSnippetPaths = new Set(this.selectedAttachedSnippets.map((f) => f.path));
snippetList.empty();
emptySnippetSearch.toggleClass("vault-hub-hidden", visibleSnippets.length !== 0);
visibleSnippets.forEach((file) => {
const row = snippetList.createDiv("vault-hub-file-row");
const cb = row.createEl("input", { type: "checkbox" });
cb.checked = selectedSnippetPaths.has(file.path);
cb.addEventListener("change", () => {
if (cb.checked) this.selectedAttachedSnippets.push(file);
else this.selectedAttachedSnippets = this.selectedAttachedSnippets.filter((x) => x.path !== file.path);
});
row.createSpan({ text: file.path });
});
};
attachedSnippetSearch.addEventListener("input", () => {
this.attachedSnippetSearchQuery = attachedSnippetSearch.value;
renderSnippetList();
});
renderSnippetList();
}
}
this.addNav(c, null, () => {
if (this.selectedFiles.length === 0) {
new import_obsidian4.Notice("Select at least one file");
return false;
}
return true;
});
}
async collectCandidateFiles() {
const snippets = await listSnippetFiles(this.app);
return snippets.sort((a, b) => a.path.localeCompare(b.path));
}
async renderStep2() {
const c = this.contentEl;
c.createEl("h4", { text: "Select required plugins" });
c.createEl("p", {
text: "Auto-detected plugins are pre-checked. Review and adjust.",
cls: "vault-hub-hint"
});
const loading = c.createDiv({ text: "Scanning..." });
const fileType = this.resourceType === "snippet" ? "css" : "md";
const detectedById = /* @__PURE__ */ new Map();
for (const file of this.selectedFiles) {
const content = await file.read();
const detected = await detectPlugins(content, fileType, this.app.vault);
detected.forEach((plugin) => detectedById.set(plugin.id, plugin));
}
this.allPlugins = [...detectedById.values()];
loading.remove();
if (this.checkedPlugins.size === 0) {
this.allPlugins.forEach((p) => {
if (p.autoDetected) this.checkedPlugins.add(p.id);
});
}
if (this.allPlugins.length > 0) {
const bulk = c.createDiv("vault-hub-bulk");
const selectAll = bulk.createEl("button", { text: "Select all" });
selectAll.type = "button";
selectAll.addEventListener("click", () => {
this.allPlugins.forEach((plugin) => this.checkedPlugins.add(plugin.id));
renderPluginList();
});
const clearAll = bulk.createEl("button", { text: "Clear" });
clearAll.type = "button";
clearAll.addEventListener("click", () => {
this.checkedPlugins.clear();
renderPluginList();
});
const count = bulk.createSpan({ cls: "vault-hub-bulk-count" });
const updateCount = () => {
count.setText(`${this.checkedPlugins.size} / ${this.allPlugins.length} selected`);
};
const list = c.createDiv("vault-hub-plugin-list");
const renderPluginList = () => {
list.empty();
updateCount();
this.allPlugins.forEach((p) => {
const row = list.createDiv("vault-hub-plugin-row");
const cb = row.createEl("input", { type: "checkbox" });
cb.checked = this.checkedPlugins.has(p.id);
cb.addEventListener("change", () => {
if (cb.checked) this.checkedPlugins.add(p.id);
else this.checkedPlugins.delete(p.id);
updateCount();
});
const info = row.createDiv("vault-hub-plugin-info");
info.createSpan({ text: p.name, cls: "vault-hub-plugin-name" });
info.createSpan({ text: ` v${p.version}`, cls: "vault-hub-plugin-version" });
if (p.autoDetected) {
info.createSpan({ text: " (auto-detected)", cls: "vault-hub-auto-badge" });
}
});
};
renderPluginList();
}
if (this.allPlugins.length === 0) {
c.createEl("p", { text: "No community plugins installed.", cls: "vault-hub-hint" });
}
if (this.resourceType === "bundle") {
const snippetSection = c.createDiv();
snippetSection.createEl("h4", { text: "Attach active CSS snippets" });
snippetSection.createEl("p", {
text: "Enabled snippets are preselected. Review the list and keep only the ones this vault actually needs.",
cls: "vault-hub-hint"
});
const availableSnippets = await listSnippetFiles(this.app);
const enabledSnippetIds = await getEnabledSnippetIds(this.app);
if (!this.pendingAttachedSnippetPaths && this.selectedAttachedSnippets.length === 0 && enabledSnippetIds.size > 0) {
this.selectedAttachedSnippets = availableSnippets.filter(
(file) => enabledSnippetIds.has(snippetIdFromFile(file))
);
}
const snippetSearch = snippetSection.createEl("input", {
type: "text",
placeholder: "Search snippets...",
cls: "vault-hub-search-input"
});
snippetSearch.value = this.attachedSnippetSearchQuery;
if (availableSnippets.length === 0) {
snippetSection.createEl("p", {
text: `No CSS snippets found in ${this.getSnippetDir()}.`,
cls: "vault-hub-hint"
});
} else {
const bulk = snippetSection.createDiv("vault-hub-bulk");
const selectAll = bulk.createEl("button", { text: "Select all" });
selectAll.type = "button";
const selectEnabled = bulk.createEl("button", { text: "Select enabled" });
selectEnabled.type = "button";
const clearAll = bulk.createEl("button", { text: "Clear" });
clearAll.type = "button";
const count = bulk.createSpan({ cls: "vault-hub-bulk-count" });
const snippetList = snippetSection.createDiv("vault-hub-file-list");
const emptySnippetSearch = snippetSection.createEl("p", {
text: "No snippets match that search.",
cls: "vault-hub-hint vault-hub-hidden"
});
const renderBundleSnippetList = () => {
const needle = this.attachedSnippetSearchQuery.trim().toLowerCase();
const visibleSnippets = needle ? availableSnippets.filter((file) => `${file.name} ${file.path}`.toLowerCase().includes(needle)) : availableSnippets;
const selectedSnippetPaths = new Set(this.selectedAttachedSnippets.map((file) => file.path));
snippetList.empty();
emptySnippetSearch.toggleClass("vault-hub-hidden", visibleSnippets.length !== 0);
count.setText(`${this.selectedAttachedSnippets.length} / ${availableSnippets.length} selected`);
visibleSnippets.forEach((file) => {
const row = snippetList.createDiv("vault-hub-file-row");
const cb = row.createEl("input", { type: "checkbox" });
cb.checked = selectedSnippetPaths.has(file.path);
cb.addEventListener("change", () => {
if (cb.checked) {
if (!this.selectedAttachedSnippets.some((value) => value.path === file.path)) {
this.selectedAttachedSnippets.push(file);
}
} else {
this.selectedAttachedSnippets = this.selectedAttachedSnippets.filter((x) => x.path !== file.path);
}
count.setText(`${this.selectedAttachedSnippets.length} / ${availableSnippets.length} selected`);
});
const label = row.createDiv("vault-hub-plugin-info");
label.createSpan({ text: file.path, cls: "vault-hub-plugin-name" });
if (enabledSnippetIds.has(snippetIdFromFile(file))) {
label.createSpan({ text: " (enabled)", cls: "vault-hub-auto-badge" });
}
});
};
selectAll.addEventListener("click", () => {
this.selectedAttachedSnippets = availableSnippets.slice();
renderBundleSnippetList();
});
selectEnabled.addEventListener("click", () => {
this.selectedAttachedSnippets = availableSnippets.filter(
(file) => enabledSnippetIds.has(snippetIdFromFile(file))
);
renderBundleSnippetList();
});
clearAll.addEventListener("click", () => {
this.selectedAttachedSnippets = [];
renderBundleSnippetList();
});
snippetSearch.addEventListener("input", () => {
this.attachedSnippetSearchQuery = snippetSearch.value;
renderBundleSnippetList();
});
renderBundleSnippetList();
}
}
this.addNav(c, null, null);
}
renderStep3() {
const c = this.contentEl;
new import_obsidian4.Setting(c).setName("Name").addText((t) => {
t.setPlaceholder("My resource").setValue(this.name);
t.onChange((v) => this.name = v);
t.inputEl.addClass("vault-hub-input-full");
});
new import_obsidian4.Setting(c).setName("Tagline").setDesc("One-line summary").addText((t) => {
t.setPlaceholder("A brief description").setValue(this.tagline);
t.onChange((v) => this.tagline = v);
t.inputEl.addClass("vault-hub-input-full");
});
new import_obsidian4.Setting(c).setName("Description").addTextArea((t) => {
t.setPlaceholder("Detailed description...").setValue(this.description);
t.onChange((v) => this.description = v);
t.inputEl.addClass("vault-hub-textarea-tall");
});
const cats = CATEGORIES[this.resourceType] || [];
const categorySetting = new import_obsidian4.Setting(c).setName("Categories").setDesc("Pick one or more.");
const categoryControl = categorySetting.controlEl.createDiv("vault-hub-multi-select");
const categoryBulk = categoryControl.createDiv("vault-hub-bulk");
const selectAllCategories = categoryBulk.createEl("button", { text: "Select all" });
selectAllCategories.type = "button";
selectAllCategories.addEventListener("click", () => {
this.categories = [...cats];
renderCategoryList();
});
const clearCategories = categoryBulk.createEl("button", { text: "Clear" });
clearCategories.type = "button";
clearCategories.addEventListener("click", () => {
this.categories = [];
renderCategoryList();
});
const categoryCount = categoryBulk.createSpan({ cls: "vault-hub-bulk-count" });
const categoryList = categoryControl.createDiv("vault-hub-checkbox-grid");
const renderCategoryList = () => {
categoryList.empty();
const selected = new Set(this.categories);
categoryCount.setText(`${selected.size} / ${cats.length} selected`);
cats.forEach((cat) => {
const row = categoryList.createEl("label", { cls: "vault-hub-checkbox-row" });
const cb = row.createEl("input", { type: "checkbox" });
cb.checked = selected.has(cat);
cb.addEventListener("change", () => {
if (cb.checked) {
if (!this.categories.includes(cat)) this.categories.push(cat);
} else {
this.categories = this.categories.filter((value) => value !== cat);
}
categoryCount.setText(`${this.categories.length} / ${cats.length} selected`);
});
row.createSpan({ text: cat });
});
};
renderCategoryList();
new import_obsidian4.Setting(c).setName("Tags").setDesc("Comma-separated").addText((t) => {
t.setPlaceholder("Glass, blur, dark").setValue(this.tags);
t.onChange((v) => this.tags = v);
});
const screenshotSection = c.createDiv();
screenshotSection.createEl("h4", { text: "Screenshots" });
screenshotSection.createEl("p", {
text: "Optional. Use local image files, external image urls, or both.",
cls: "vault-hub-hint"
});
new import_obsidian4.Setting(screenshotSection).setName("External screenshot urls").setDesc("One per line. Direct image urls work best.").addTextArea((t) => {
t.setPlaceholder("https://example.com/screenshot.png").setValue(this.externalScreenshotUrls);
t.onChange((v) => this.externalScreenshotUrls = v);
t.inputEl.addClass("vault-hub-textarea-short");
});
const localScreenshotSection = screenshotSection.createDiv();
localScreenshotSection.createEl("h4", { text: "Local screenshots" });
localScreenshotSection.createEl("p", {
text: "Optional. Pick image files from your vault.",
cls: "vault-hub-hint"
});
localScreenshotSection.createEl("p", { text: "Loading images...", cls: "vault-hub-hint" });
void this.renderScreenshotPicker(localScreenshotSection);
if (this.resourceType === "snippet") {
new import_obsidian4.Setting(c).setName("Compatible themes").addDropdown((dd) => {
dd.addOption("any", "Any theme");
["minimal", "velocity", "obsidian-default", "catppuccin"].forEach((t) => {
dd.addOption(t, t);
});
dd.setValue(this.compatibleThemes[0] || "any");
dd.onChange((v) => this.compatibleThemes = [v]);
});
}
this.addNav(c, null, () => {
if (!this.name.trim()) {
new import_obsidian4.Notice("Name is required");
return false;
}
this.readmeContent = generateReadme(this.buildReadmeData());
return true;
});
}
renderStep4() {
const c = this.contentEl;
c.createEl("h4", { text: "Edit readme" });
c.createEl("p", {
text: "Auto-generated from your details. Edit freely.",
cls: "vault-hub-hint"
});
const textarea = c.createEl("textarea", { cls: "vault-hub-readme-editor" });
textarea.value = this.readmeContent;
textarea.addEventListener("input", () => {
this.readmeContent = textarea.value;
});
this.addNav(c, null, null);
}
renderStep5() {
const c = this.contentEl;
c.createEl("h4", { text: "Review & publish" });
const publishedType = this.getPublishedType();
const summary = c.createDiv("vault-hub-summary");
summary.createEl("p", { text: `Type: ${publishedType}${this.resourceType === "bundle" ? " (multi-file)" : ""}` });
summary.createEl("p", { text: `Name: ${this.name}` });
summary.createEl("p", { text: `Files: ${this.selectedFiles.map((f) => f.path).join(", ")}` });
if (this.selectedAttachedSnippets.length > 0) {
summary.createEl("p", {
text: `Attached snippets: ${this.selectedAttachedSnippets.map((f) => f.name).join(", ")}`
});
}
if (this.selectedScreenshots.length > 0) {
summary.createEl("p", {
text: `Screenshots: ${this.selectedScreenshots.map((f) => f.name).join(", ")}`
});
}
const externalScreenshotUrls = this.getExternalScreenshotUrls();
if (externalScreenshotUrls.length > 0) {
summary.createEl("p", {
text: `External screenshots: ${externalScreenshotUrls.length}`
});
}
const selPlugins = this.allPlugins.filter((p) => this.checkedPlugins.has(p.id));
summary.createEl("p", {
text: `Plugins: ${selPlugins.length > 0 ? selPlugins.map((p) => p.name).join(", ") : "None"}`
});
summary.createEl("p", { text: `Categories: ${this.categories.join(", ") || "None"}` });
const btnContainer = c.createDiv("vault-hub-nav");
const backBtn = btnContainer.createEl("button", { text: "Back" });
backBtn.addEventListener("click", () => void this.navigateStep(-1));
const publishBtn = btnContainer.createEl("button", {
text: "Publish",
cls: "mod-cta"
});
publishBtn.addEventListener("click", () => void this.doPublish());
}
async doPublish() {
var _a, _b;
const token = this.plugin.settings.githubToken;
if (!token) {
new import_obsidian4.Notice("Set your GitHub token in vault hub settings first");
return;
}
const resourceFiles = this.getPublishResourceFiles();
if (resourceFiles.length === 0) {
new import_obsidian4.Notice("Select at least one file to publish before continuing.");
this.step = 1;
this.renderStep();
return;
}
const c = this.contentEl;
c.empty();
c.createEl("h3", { text: "Publishing..." });
const status = c.createEl("p", { text: "Creating repository..." });
try {
await this.saveDraft();
const gh = new GitHubAPI(token);
const user = await gh.getUser();
const publishedType = this.getPublishedType();
const slug = this.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "resource";
const repoName = await gh.getAvailableRepoName(user.login, `obsidian-${publishedType}-${slug}`);
status.setText("Creating repository...");
const repo = await gh.createRepo(repoName, this.tagline || this.name);
const [owner, rName] = repo.full_name.split("/");
const topicMap = {
vault: "obsidian-vault-template",
snippet: "obsidian-css-snippet",
note: "obsidian-note-template"
};
status.setText("Adding topic tag...");
await gh.addTopics(owner, rName, [topicMap[publishedType]]);
for (const file of resourceFiles) {
status.setText(`Uploading ${file.path}...`);
const content = await file.read();
await gh.upsertFile(owner, rName, file.path, content, `Sync ${file.path}`);
}
const attachedSnippetFiles = this.getAttachedSnippetFiles();
for (const file of attachedSnippetFiles) {
status.setText(`Uploading ${file.repoPath}...`);
const content = await file.read();
await gh.upsertFile(owner, rName, file.repoPath, content, `Sync ${file.repoPath}`);
}
const screenshotFiles = this.getScreenshotFiles();
const screenshotUrls = [...this.getExternalScreenshotUrls()];
const readmeScreenshots = this.buildReadmeData().screenshots;
for (const file of screenshotFiles) {
status.setText(`Uploading ${file.repoPath}...`);
const binary = await file.readBinary();
await gh.upsertBinaryFile(owner, rName, file.repoPath, toBase64(binary), `Sync ${file.repoPath}`);
screenshotUrls.push(`https://raw.githubusercontent.com/${owner}/${rName}/HEAD/${file.repoPath}`);
}
const readmeContent = syncReadmeScreenshots(
this.readmeContent || generateReadme(this.buildReadmeData()),
readmeScreenshots
);
this.readmeContent = readmeContent;
const finalScreenshotUrls = this.uniqueStrings([
...screenshotUrls,
...this.extractMarkdownImageUrls(readmeContent).map(
(url) => this.resolvePublishedAssetUrl(owner, rName, url)
)
]);
status.setText("Generating hub.md...");
const selectedPlugins = this.allPlugins.filter((p) => this.checkedPlugins.has(p.id)).map((p) => ({ ...p, autoDetected: true }));
const obsVer = getAppVersion(this.app);
const themeName = getVaultThemeName(this.app.vault);
const hubData = {
type: publishedType,
name: this.name,
tagline: this.tagline,
description: this.description,
author: user.login,
categories: this.categories,
tags: this.tags.split(",").map((t) => t.trim()).filter(Boolean),
compatibleThemes: this.compatibleThemes,
screenshots: finalScreenshotUrls,
plugins: selectedPlugins,
attachedSnippets: attachedSnippetFiles.map((file) => ({
path: file.repoPath,
name: file.name,
optional: file.optional
})),
obsidianVersion: obsVer,
theme: themeName,
os: import_obsidian4.Platform.isMacOS ? "macOS" : import_obsidian4.Platform.isWin ? "Windows" : import_obsidian4.Platform.isLinux ? "Linux" : "unknown",
files: resourceFiles.map((f) => ({
path: f.path,
type: f.extension,
size: f.size
})),
body: readmeContent
};
const hubMd = generateHubMd(hubData);
await gh.upsertFile(owner, rName, "hub.md", hubMd, "Sync hub.md");
status.setText("Uploading readme...");
await gh.upsertFile(owner, rName, "README.md", readmeContent, "Sync README.md");
let refreshRequested = false;
const catalogRepo = this.plugin.settings.catalogRepoFullName.trim();
if (catalogRepo.includes("/")) {
const [catalogOwner, catalogName] = catalogRepo.split("/");
try {
status.setText("Requesting catalog refresh...");
await gh.dispatchRepositoryEvent(catalogOwner, catalogName, "catalog_refresh", {
source_repo: repo.full_name,
resource_type: publishedType
});
refreshRequested = true;
} catch (e) {
refreshRequested = false;
}
}
const publishedResource = {
repoFullName: repo.full_name,
localFilePath: ((_a = resourceFiles[0]) == null ? void 0 : _a.path) || ((_b = this.selectedFiles[0]) == null ? void 0 : _b.path) || "",
localFiles: resourceFiles.map((f) => f.path),
fileMappings: [
...resourceFiles.map((f) => ({ localPath: f.path, repoPath: f.path, kind: "resource" })),
...attachedSnippetFiles.map((f) => ({ localPath: f.localPath, repoPath: f.repoPath, kind: "attached-snippet" })),
...screenshotFiles.map((f) => ({ localPath: f.localPath, repoPath: f.repoPath, kind: "screenshot" }))
],
type: publishedType,
lastPublishedAt: (/* @__PURE__ */ new Date()).toISOString()
};
const existingIndex = this.plugin.settings.publishedResources.findIndex(
(resource) => resource.repoFullName === repo.full_name
);
if (existingIndex >= 0) {
this.plugin.settings.publishedResources[existingIndex] = publishedResource;
} else {
this.plugin.settings.publishedResources.push(publishedResource);
}
this.plugin.settings.publishDraft = null;
this.preserveDraftOnClose = false;
await this.plugin.saveSettings();
c.empty();
c.createEl("h3", { text: "Published!" });
c.createEl("p", { text: `Repository: ${repo.full_name}` });
const vaultHubUrl = `https://obsidianvaulthub.com/r/${owner}/${rName}`;
const actions = c.createDiv("vault-hub-success-actions");
const link = actions.createEl("a", {
text: "Open pending page on vault hub",
href: vaultHubUrl,
cls: "mod-cta vault-hub-success-link"
});
link.setAttr("target", "_blank");
c.createEl("p", {
text: refreshRequested ? "Catalog refresh requested. The listing should move from pending to indexed after the workflow finishes." : "Catalog refresh was not requested automatically. The listing may stay pending until the next scheduled refresh.",
cls: "vault-hub-hint"
});
const ghLink = actions.createEl("a", {
text: "View on GitHub",
href: repo.html_url,
cls: "vault-hub-hint"
});
ghLink.setAttr("target", "_blank");
const closeBtn = actions.createEl("button", { text: "Close", cls: "mod-cta" });
closeBtn.addEventListener("click", () => this.close());
new import_obsidian4.Notice(`Published to ${repo.full_name}!`);
} catch (e) {
c.empty();
c.createEl("h3", { text: "Error" });
c.createEl("p", { text: String(e) });
const retryBtn = c.createEl("button", { text: "Back to review" });
retryBtn.addEventListener("click", () => {
this.step = 5;
this.renderStep();
});
}
}
addNav(container, backCheck, nextCheck) {
const nav = container.createDiv("vault-hub-nav");
if (this.step > 1) {
const back = nav.createEl("button", { text: "Back" });
back.addEventListener("click", () => {
if (backCheck && !backCheck()) return;
void this.navigateStep(-1);
});
}
if (this.step < 5) {
const next = nav.createEl("button", { text: "Next", cls: "mod-cta" });
next.addEventListener("click", () => {
if (nextCheck && !nextCheck()) return;
void this.navigateStep(1);
});
}
}
async resetDraftAndRender() {
this.resetState();
await this.discardDraft();
this.renderStep();
}
async navigateStep(delta) {
this.step += delta;
await this.saveDraft();
this.renderStep();
}
async renderScreenshotPicker(section) {
var _a;
const images = await listImageFiles(this.app);
if (this.pendingScreenshotPaths) {
const wanted = new Set(this.pendingScreenshotPaths);
this.selectedScreenshots = images.filter((file) => wanted.has(file.path));
this.pendingScreenshotPaths = null;
}
while (section.childElementCount > 2) {
(_a = section.lastElementChild) == null ? void 0 : _a.remove();
}
if (images.length === 0) {
section.createEl("p", {
text: "No image files found in this vault.",
cls: "vault-hub-hint"
});
return;
}
const search = section.createEl("input", {
type: "text",
placeholder: "Search images...",
cls: "vault-hub-search-input"
});
search.value = this.screenshotSearchQuery;
const bulk = section.createDiv("vault-hub-bulk");
const clearAll = bulk.createEl("button", { text: "Clear" });
clearAll.type = "button";
const count = bulk.createSpan({ cls: "vault-hub-bulk-count" });
const list = section.createDiv("vault-hub-file-list");
const emptySearch = section.createEl("p", {
text: "No images match that search.",
cls: "vault-hub-hint vault-hub-hidden"
});
const render = () => {
const needle = this.screenshotSearchQuery.trim().toLowerCase();
const visible = needle ? images.filter((file) => file.path.toLowerCase().includes(needle)) : images;
const selectedPaths = new Set(this.selectedScreenshots.map((file) => file.path));
list.empty();
emptySearch.toggleClass("vault-hub-hidden", visible.length !== 0);
count.setText(`${this.selectedScreenshots.length} selected`);
visible.forEach((file) => {
const row = list.createDiv("vault-hub-file-row");
const cb = row.createEl("input", { type: "checkbox" });
cb.checked = selectedPaths.has(file.path);
cb.addEventListener("change", () => {
if (cb.checked) {
if (!this.selectedScreenshots.some((value) => value.path === file.path)) {
this.selectedScreenshots.push(file);
}
} else {
this.selectedScreenshots = this.selectedScreenshots.filter((x) => x.path !== file.path);
}
count.setText(`${this.selectedScreenshots.length} selected`);
});
row.createSpan({ text: file.path });
});
};
clearAll.addEventListener("click", () => {
this.selectedScreenshots = [];
render();
});
search.addEventListener("input", () => {
this.screenshotSearchQuery = search.value;
render();
});
render();
}
buildReadmeData() {
const resourceFiles = this.getPublishResourceFiles();
const selected = this.allPlugins.filter((plugin) => this.checkedPlugins.has(plugin.id)).map((plugin) => ({ ...plugin, autoDetected: true }));
return {
name: this.name,
tagline: this.tagline,
description: this.description,
type: this.getPublishedType(),
plugins: selected,
files: resourceFiles.map((file) => ({ path: file.path })),
attachedSnippets: this.getAttachedSnippetFiles().map((file) => ({
path: file.repoPath,
name: file.name,
optional: file.optional
})),
screenshots: [
...this.getScreenshotFiles().map((file) => ({
path: file.repoPath,
alt: file.name
})),
...this.getExternalScreenshotUrls().map((path, index) => ({
path,
alt: `Screenshot ${index + 1}`
}))
]
};
}
getPublishResourceFiles() {
return this.selectedFiles.filter((file) => !isReservedRootPath(file.path));
}
getAttachedSnippetFiles() {
const used = /* @__PURE__ */ new Set();
return this.selectedAttachedSnippets.map((file) => {
const rawName = file.name.replace(/\.css$/i, "") || "snippet";
let candidate = `${rawName}.css`;
let suffix = 2;
while (used.has(candidate.toLowerCase())) {
candidate = `${rawName}-${suffix}.css`;
suffix++;
}
used.add(candidate.toLowerCase());
return {
localPath: file.path,
repoPath: `snippets/${candidate}`,
name: rawName,
read: file.read
};
});
}
getScreenshotFiles() {
const used = /* @__PURE__ */ new Set();
return this.selectedScreenshots.map((file) => {
const dot = file.name.lastIndexOf(".");
const stem = dot > 0 ? file.name.slice(0, dot) : file.name;
const ext = dot > 0 ? file.name.slice(dot + 1).toLowerCase() : file.extension.toLowerCase();
let candidate = `${stem}.${ext}`;
let suffix = 2;
while (used.has(candidate.toLowerCase())) {
candidate = `${stem}-${suffix}.${ext}`;
suffix++;
}
used.add(candidate.toLowerCase());
return {
localPath: file.path,
repoPath: `screenshots/${candidate}`,
name: stem,
readBinary: file.readBinary || (async () => {
await Promise.resolve();
return new ArrayBuffer(0);
})
};
});
}
getExternalScreenshotUrls() {
return this.externalScreenshotUrls.split(/\r?\n/).map((line) => this.extractMarkdownUrl(line.trim())).filter(Boolean);
}
extractMarkdownUrl(value) {
const imageMatch = value.match(/^!\[[^\]]*\]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)$/);
if (imageMatch) return imageMatch[1];
const linkMatch = value.match(/^\[[^\]]+\]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)$/);
if (linkMatch) return linkMatch[1];
return value.replace(/^["']|["']$/g, "");
}
extractMarkdownImageUrls(value) {
const urls = [];
const imagePattern = /!\[[^\]]*\]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)/g;
const screenshotLinkPattern = /\[([^\]]*screenshot[^\]]*)\]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)/gi;
let match;
while (match = imagePattern.exec(value)) {
urls.push(match[1]);
}
while (match = screenshotLinkPattern.exec(value)) {
urls.push(match[2]);
}
return urls;
}
resolvePublishedAssetUrl(owner, repo, url) {
if (/^(https?:|data:|blob:)/i.test(url)) return url;
const clean = url.replace(/^\.?\//, "").replace(/^blob\/[^/]+\//, "");
return `https://raw.githubusercontent.com/${owner}/${repo}/HEAD/${clean}`;
}
uniqueStrings(values) {
return values.filter((value, index, all) => value && all.indexOf(value) === index);
}
};
function parseAppearanceConfig(raw) {
const parsed = JSON.parse(raw);
if (typeof parsed !== "object" || parsed === null) {
return { enabledCssSnippets: [] };
}
const config = parsed;
const enabledCssSnippets = Array.isArray(config.enabledCssSnippets) ? config.enabledCssSnippets.filter(
(value) => typeof value === "string"
) : [];
return { enabledCssSnippets };
}
function getAppVersion(app) {
const maybeVersion = app.appVersion;
return typeof maybeVersion === "string" ? maybeVersion : "unknown";
}
function getVaultThemeName(vault) {
const maybeConfig = vault.config;
return typeof (maybeConfig == null ? void 0 : maybeConfig.cssTheme) === "string" && maybeConfig.cssTheme.length > 0 ? maybeConfig.cssTheme : "default";
}
// src/modals/UpdateModal.ts
var import_obsidian5 = require("obsidian");
var UpdateModal = class extends import_obsidian5.Modal {
constructor(app, plugin) {
super(app);
this.selected = null;
this.files = [];
this.syncedResources = [];
this.selectedHasLocalMappings = false;
this.plugin = plugin;
}
onOpen() {
void this.renderSelect();
}
onClose() {
this.contentEl.empty();
}
// ─── Step 1: select resource ───────────────────────────────
async renderSelect() {
const c = this.contentEl;
c.empty();
c.addClass("vault-hub-modal");
c.createEl("h2", { text: "Update resource" });
const loading = c.createEl("p", { text: "Loading resources...", cls: "vault-hub-hint" });
const resources = await this.getSelectableResources();
loading.remove();
if (resources.length === 0) {
c.createEl("p", {
text: "No published resources yet - publish one first.",
cls: "vault-hub-hint"
});
return;
}
new import_obsidian5.Setting(c).setName("Resource").addDropdown((dd) => {
dd.addOption("", "Select...");
resources.forEach((r, i) => {
const suffix = this.hasLocalMappings(r) ? "" : " - GitHub only";
dd.addOption(String(i), `${r.repoFullName} (${r.type}) - ${timeAgo(new Date(r.lastPublishedAt))}${suffix}`);
});
dd.onChange((v) => {
this.selected = v ? resources[parseInt(v)] : null;
});
});
const btn = c.createEl("button", { text: "Check for changes", cls: "mod-cta" });
btn.addClass("vault-hub-update-status");
btn.addEventListener("click", () => {
if (!this.selected) {
new import_obsidian5.Notice("Select a resource first");
return;
}
if (!this.plugin.settings.githubToken) {
new import_obsidian5.Notice("Set your GitHub token in settings");
return;
}
void this.loadDiff(btn);
});
}
// ─── Step 2: diff ─────────────────────────────────────────
async loadDiff(triggerBtn) {
triggerBtn.disabled = true;
triggerBtn.setText("Checking...");
const token = this.plugin.settings.githubToken;
const [owner, repo] = this.selected.repoFullName.split("/");
try {
const gh = new GitHubAPI(token);
const fileMappings = this.getFileMappings(this.selected);
this.selectedHasLocalMappings = fileMappings.length > 0;
this.files = [];
for (const mapping of fileMappings) {
if (!mapping.localPath.match(/\.(md|css|yml|yaml|js|json|txt|canvas)$/i)) continue;
const entry = {
name: mapping.repoPath,
localPath: mapping.localPath,
repoPath: mapping.repoPath,
downloadUrl: "",
status: "not-found"
};
const local = await this.readLocalContent(mapping.localPath);
if (local !== null) {
const remote = await gh.getFileContent(owner, repo, mapping.repoPath);
entry.localContent = local;
entry.githubSha = remote == null ? void 0 : remote.sha;
entry.status = !remote || local !== remote.content ? "changed" : "unchanged";
}
this.files.push(entry);
}
this.renderDiff();
} catch (e) {
new import_obsidian5.Notice(`Error: ${e}`);
triggerBtn.disabled = false;
triggerBtn.setText("Check for changes");
}
}
renderDiff() {
const c = this.contentEl;
c.empty();
c.createEl("h2", { text: "Review changes" });
c.createEl("p", { text: this.selected.repoFullName, cls: "vault-hub-hint" });
const changed = this.files.filter((f) => f.status === "changed");
const unchanged = this.files.filter((f) => f.status === "unchanged");
const notFound = this.files.filter((f) => f.status === "not-found");
const list = c.createDiv("vault-hub-update-list");
changed.forEach((f) => {
const row = list.createDiv("vault-hub-update-row");
row.createSpan({ text: "~ ", cls: "vault-hub-status-changed" });
row.createSpan({ text: f.name });
row.createSpan({ text: " modified", cls: "vault-hub-hint" });
});
unchanged.forEach((f) => {
const row = list.createDiv("vault-hub-update-row");
row.createSpan({ text: "= ", cls: "vault-hub-status-unchanged" });
row.createSpan({ text: f.name });
row.createSpan({ text: " up to date", cls: "vault-hub-hint" });
});
notFound.forEach((f) => {
const row = list.createDiv("vault-hub-update-row");
row.createSpan({ text: "? ", cls: "vault-hub-status-missing" });
row.createSpan({ text: f.name });
row.createSpan({ text: " not found locally", cls: "vault-hub-hint" });
});
const nav = c.createDiv("vault-hub-nav");
const backBtn = nav.createEl("button", { text: "Back" });
backBtn.addEventListener("click", () => {
void this.renderSelect();
});
if (!this.selectedHasLocalMappings) {
c.createEl("p", {
text: "This repo was found from your GitHub account, but this vault has no local file mappings for it yet.",
cls: "vault-hub-hint"
});
} else if (changed.length === 0) {
c.createEl("p", { text: "Everything is up to date.", cls: "vault-hub-hint" });
} else {
const pushBtn = nav.createEl("button", {
text: `Push ${changed.length} Change${changed.length !== 1 ? "s" : ""}`,
cls: "mod-cta"
});
pushBtn.addEventListener("click", () => {
void this.doPush(changed);
});
}
}
// ─── Step 3: push ─────────────────────────────────────────
async doPush(toUpdate) {
const c = this.contentEl;
c.empty();
const statusEl = c.createEl("p", { text: "Pushing...", cls: "vault-hub-hint" });
const token = this.plugin.settings.githubToken;
const gh = new GitHubAPI(token);
const [owner, repo] = this.selected.repoFullName.split("/");
try {
let pushed = 0;
for (const f of toUpdate) {
statusEl.setText(`Pushing ${f.name}...`);
const existing = await gh.getFileContent(owner, repo, f.repoPath);
if (existing) {
await gh.updateFile(owner, repo, f.repoPath, f.localContent, `Update ${f.repoPath}`, existing.sha);
} else {
await gh.createFile(owner, repo, f.repoPath, f.localContent, `Add ${f.repoPath}`);
}
pushed++;
}
this.selected.lastPublishedAt = (/* @__PURE__ */ new Date()).toISOString();
await this.plugin.saveSettings();
let refreshRequested = false;
const catalogRepo = this.plugin.settings.catalogRepoFullName.trim();
if (catalogRepo.includes("/")) {
const [catalogOwner, catalogName] = catalogRepo.split("/");
try {
await gh.dispatchRepositoryEvent(catalogOwner, catalogName, "catalog_refresh", {
source_repo: this.selected.repoFullName,
update: true
});
refreshRequested = true;
} catch (e) {
refreshRequested = false;
}
}
c.empty();
c.createEl("h3", { text: "Updated!" });
c.createEl("p", { text: `Pushed ${pushed} file(s) to ${this.selected.repoFullName}` });
c.createEl("p", {
text: refreshRequested ? "Catalog refresh requested." : "Catalog refresh was not requested automatically.",
cls: "vault-hub-hint"
});
const [rOwner, rName] = this.selected.repoFullName.split("/");
const link = c.createEl("a", {
text: "View on vault hub",
href: `https://obsidianvaulthub.com/r/${rOwner}/${rName}`,
cls: "mod-cta vault-hub-success-link"
});
link.setAttr("target", "_blank");
const closeBtn = c.createEl("button", { text: "Close" });
closeBtn.addClass("vault-hub-update-status");
closeBtn.addEventListener("click", () => this.close());
new import_obsidian5.Notice(`Pushed ${pushed} file(s)!`);
} catch (e) {
c.empty();
c.createEl("h3", { text: "Error" });
c.createEl("p", { text: String(e) });
const retryBtn = c.createEl("button", { text: "Back" });
retryBtn.addEventListener("click", () => this.renderDiff());
}
}
getFileMappings(resource) {
var _a, _b;
if ((_a = resource.fileMappings) == null ? void 0 : _a.length) return resource.fileMappings.filter((mapping) => Boolean(mapping.localPath));
const localFiles = ((_b = resource.localFiles) == null ? void 0 : _b.filter(Boolean)) || (resource.localFilePath ? [resource.localFilePath] : []);
return localFiles.map((path) => ({
localPath: path,
repoPath: path,
kind: "resource"
}));
}
hasLocalMappings(resource) {
return this.getFileMappings(resource).length > 0;
}
async getSelectableResources() {
const local = this.plugin.settings.publishedResources || [];
const token = this.plugin.settings.githubToken;
const merged = /* @__PURE__ */ new Map();
for (const resource of local) {
merged.set(resource.repoFullName, resource);
}
if (token) {
try {
const gh = new GitHubAPI(token);
const repos = await gh.listAuthenticatedRepos();
for (const repo of repos) {
const inferredType = inferPublishedType(repo.name);
if (!inferredType) continue;
const existing = merged.get(repo.full_name);
if (existing) {
merged.set(repo.full_name, {
...existing,
lastPublishedAt: newerDate(existing.lastPublishedAt, repo.updated_at)
});
} else {
merged.set(repo.full_name, {
repoFullName: repo.full_name,
localFilePath: "",
localFiles: [],
fileMappings: [],
type: inferredType,
lastPublishedAt: repo.updated_at
});
}
}
} catch (e) {
}
}
const resources = [...merged.values()].sort(
(a, b) => new Date(b.lastPublishedAt).getTime() - new Date(a.lastPublishedAt).getTime()
);
if (JSON.stringify(resources) !== JSON.stringify(local)) {
this.plugin.settings.publishedResources = resources;
await this.plugin.saveSettings();
}
this.syncedResources = resources;
return resources;
}
async readLocalContent(path) {
const tfile = this.app.vault.getAbstractFileByPath(path);
if (tfile instanceof import_obsidian5.TFile) {
return this.app.vault.read(tfile);
}
try {
if (await this.app.vault.adapter.exists(path)) {
return await this.app.vault.adapter.read(path);
}
} catch (e) {
}
return null;
}
};
function timeAgo(date) {
const s = Math.floor((Date.now() - date.getTime()) / 1e3);
if (s < 60) return "just now";
const m = Math.floor(s / 60);
if (m < 60) return `${m}m ago`;
const h = Math.floor(m / 60);
if (h < 24) return `${h}h ago`;
return `${Math.floor(h / 24)}d ago`;
}
function inferPublishedType(repoName) {
if (repoName.startsWith("obsidian-snippet-")) return "snippet";
if (repoName.startsWith("obsidian-note-")) return "note";
if (repoName.startsWith("obsidian-vault-")) return "vault";
return null;
}
function newerDate(a, b) {
return new Date(a).getTime() >= new Date(b).getTime() ? a : b;
}
// src/views/BrowseView.ts
var import_obsidian6 = require("obsidian");
var VIEW_TYPE_BROWSE = "vault-hub-browse";
var TYPE_FILTERS = ["all", "vault", "snippet", "note", "dashboard"];
function normalizeToken(value) {
return value.trim().toLowerCase().replace(/[\s_]+/g, "-");
}
var ConfirmModal = class extends import_obsidian6.Modal {
constructor(app, message, onResolve) {
super(app);
this.message = message;
this.onResolve = onResolve;
this.result = false;
}
onOpen() {
this.contentEl.createEl("p", { text: this.message });
const buttons = this.contentEl.createDiv("vault-hub-nav");
const cancel = buttons.createEl("button", { text: "Cancel" });
cancel.addEventListener("click", () => this.close());
const confirm = buttons.createEl("button", { text: "Confirm", cls: "mod-cta" });
confirm.addEventListener("click", () => {
this.result = true;
this.close();
});
}
onClose() {
this.contentEl.empty();
this.onResolve(this.result);
}
};
function confirmModal(app, message) {
return new Promise((resolve) => {
new ConfirmModal(app, message, resolve).open();
});
}
function hasDashboardMarker(values) {
return (values || []).some((value) => normalizeToken(value) === "dashboard");
}
function isCatalogType(value) {
return value === "vault" || value === "snippet" || value === "note" || value === "dashboard";
}
function isCatalogResourcePayload(value) {
if (typeof value !== "object" || value === null) return false;
const resource = value;
return typeof resource.id === "string" && typeof resource.type === "string" && typeof resource.title === "string" && typeof resource.owner === "string" && typeof resource.repo_name === "string" && typeof resource.full_name === "string" && typeof resource.stars === "number";
}
function normalizeResource(resource) {
const rawType = isCatalogType(resource.rawType) ? resource.rawType : resource.type;
const subtype = rawType === "dashboard" || hasDashboardMarker(resource.categories) || hasDashboardMarker(resource.tags) ? "dashboard" : null;
const type = isCatalogType(rawType) && rawType !== "dashboard" ? rawType : "note";
return {
...resource,
rawType: isCatalogType(rawType) ? rawType : void 0,
type,
subtype
};
}
function getErrorMessage(value) {
if (typeof value !== "object" || value === null) return null;
const payload = value;
for (const key of ["error", "message", "hint"]) {
const message = payload[key];
if (typeof message === "string" && message.length > 0) return message;
}
return null;
}
function isGitHubTreeItem2(value) {
if (typeof value !== "object" || value === null) return false;
const item = value;
return typeof item.path === "string" && typeof item.type === "string";
}
function getDisplayKind(resource) {
return resource.subtype === "dashboard" ? "dashboard" : resource.type;
}
function isDashboardResource(resource) {
return resource.subtype === "dashboard";
}
function encodeGitHubPath(path) {
return path.split("/").map(encodeURIComponent).join("/");
}
function basename(path) {
return path.split("/").pop() || path;
}
function extractFrontmatter(text) {
const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
return match ? match[1] : null;
}
function parseRequiredPluginIds(frontmatter) {
const ids = [];
let inPlugins = false;
for (const line of frontmatter.split(/\r?\n/)) {
if (/^plugins:\s*$/.test(line.trim())) {
inPlugins = true;
continue;
}
if (inPlugins && /^[A-Za-z0-9_-][^:]*:\s*/.test(line)) break;
if (!inPlugins) continue;
const match = line.match(/^\s*-\s*id:\s*(.+)\s*$/);
if (match) ids.push(match[1].trim().replace(/^["']|["']$/g, ""));
}
return ids;
}
function parseAttachedSnippets(frontmatter) {
const snippets = [];
let inAttachedSnippets = false;
let current = null;
const pushCurrent = () => {
if (current == null ? void 0 : current.path) snippets.push(current);
current = null;
};
for (const line of frontmatter.split(/\r?\n/)) {
if (/^attached_snippets:\s*$/.test(line.trim())) {
inAttachedSnippets = true;
continue;
}
if (inAttachedSnippets && /^[A-Za-z0-9_-][^:]*:\s*/.test(line)) {
pushCurrent();
break;
}
if (!inAttachedSnippets) continue;
const pathMatch = line.match(/^\s*-\s*path:\s*(.+)\s*$/);
if (pathMatch) {
pushCurrent();
current = { path: pathMatch[1].trim().replace(/^["']|["']$/g, "") };
continue;
}
const nameMatch = line.match(/^\s+name:\s*(.+)\s*$/);
if (nameMatch && current) {
current.name = nameMatch[1].trim().replace(/^["']|["']$/g, "");
continue;
}
const optionalMatch = line.match(/^\s+optional:\s*(.+)\s*$/);
if (optionalMatch && current) {
current.optional = optionalMatch[1].trim().toLowerCase() === "true";
}
}
pushCurrent();
return snippets;
}
var BrowseView = class extends import_obsidian6.ItemView {
constructor(leaf, plugin) {
super(leaf);
this.searchQuery = "";
this.resources = [];
this.activeTab = "browse";
this.filterType = "all";
this.plugin = plugin;
}
getViewType() {
return VIEW_TYPE_BROWSE;
}
getDisplayText() {
return "Vault hub";
}
getIcon() {
return "globe";
}
async onOpen() {
this.render();
await this.loadActiveTab();
}
async onClose() {
await Promise.resolve();
this.contentEl.empty();
}
async loadActiveTab() {
if (this.activeTab === "browse") {
await this.loadResources();
} else {
await this.renderSnippetManager();
}
}
render() {
const c = this.contentEl;
c.empty();
c.addClass("vault-hub-browse");
const tabBar = c.createDiv("vault-hub-tabs");
["browse", "snippets"].forEach((tab) => {
const btn = tabBar.createEl("button", {
text: tab === "browse" ? "Browse" : "Snippets",
cls: `vault-hub-tab${this.activeTab === tab ? " active" : ""}`
});
btn.addEventListener("click", () => {
if (this.activeTab === tab) return;
this.activeTab = tab;
this.render();
void this.loadActiveTab();
});
});
if (this.activeTab === "browse") {
const header = c.createDiv("vault-hub-browse-header");
const searchRow = header.createDiv("vault-hub-search-row");
const input = searchRow.createEl("input", {
type: "text",
placeholder: "Search resources..."
});
input.value = this.searchQuery;
input.addEventListener("input", () => {
this.searchQuery = input.value;
});
input.addEventListener("keydown", (e) => {
if (e.key === "Enter") void this.loadResources();
});
const searchBtn = searchRow.createEl("button", { text: "Search" });
searchBtn.addEventListener("click", () => void this.loadResources());
const typeRow = c.createDiv("vault-hub-type-filters");
TYPE_FILTERS.forEach((type) => {
const btn = typeRow.createEl("button", {
text: type === "all" ? "All" : type[0].toUpperCase() + type.slice(1),
cls: `vault-hub-type-filter${this.filterType === type ? " active" : ""}`
});
if (type !== "all") {
btn.dataset.type = type;
}
btn.addEventListener("click", () => {
this.filterType = type;
typeRow.querySelectorAll(".vault-hub-type-filter").forEach((b) => b.removeClass("active"));
btn.addClass("active");
void this.loadResources();
});
});
c.createDiv("vault-hub-results");
} else {
c.createDiv("vault-hub-snippets-container");
}
}
// ─── BROWSE TAB ───────────────────────────────────────────────
async loadResources() {
const resultsEl = this.contentEl.querySelector(".vault-hub-results");
if (!resultsEl) return;
resultsEl.empty();
resultsEl.createEl("p", { text: "Loading...", cls: "vault-hub-hint" });
try {
const limit = this.filterType === "dashboard" ? 100 : 30;
const baseUrl = this.plugin.settings.vaultHubUrl.replace(/\/+$/, "");
const params = new URLSearchParams({ limit: String(limit) });
if (this.filterType !== "all") {
params.set("type", this.filterType);
}
if (this.searchQuery.trim()) {
params.set("q", this.searchQuery.trim());
}
const data = await requestJson(`${baseUrl}/api/search?${params.toString()}`);
if (!Array.isArray(data)) {
throw new Error(getErrorMessage(data) || "Unexpected response from API");
}
let resources = data.filter(isCatalogResourcePayload).map((resource) => normalizeResource(resource));
if (this.filterType === "dashboard") {
resources = resources.filter((resource) => isDashboardResource(resource));
}
this.resources = resources;
this.renderResults(resultsEl);
} catch (e) {
resultsEl.empty();
resultsEl.createEl("p", { text: `Error: ${e}` });
}
}
renderResults(container) {
container.empty();
if (this.resources.length === 0) {
container.createEl("p", { text: "No resources found.", cls: "vault-hub-hint" });
return;
}
this.resources.forEach((r) => {
const displayKind = getDisplayKind(r);
const card = container.createDiv("vault-hub-result-card");
const badge = card.createSpan("vault-hub-type-badge");
badge.setText(displayKind === "dashboard" ? "dashboard note" : r.type);
badge.dataset.kind = displayKind;
card.createEl("h4", { text: r.title });
if (r.tagline) {
card.createEl("p", { text: r.tagline, cls: "vault-hub-hint" });
}
const meta = card.createDiv("vault-hub-result-meta");
meta.createSpan({ text: `${r.stars} stars` });
meta.createSpan({ text: r.owner });
const actions = card.createDiv("vault-hub-result-actions");
const installBtn = actions.createEl("button", { text: "Install" });
installBtn.addEventListener("click", () => void this.installResource(r, installBtn));
const ghBtn = actions.createEl("button", { text: "GitHub" });
ghBtn.addEventListener("click", () => {
window.open(`https://github.com/${r.full_name}`, "_blank");
});
});
}
// ─── INSTALL FLOWS ────────────────────────────────────────────
async installResource(r, btn) {
btn.disabled = true;
btn.setText("Installing...");
try {
if (r.type === "vault") {
await this.installVault(r);
} else if (r.type === "snippet") {
await this.installSnippet(r);
} else {
await this.installNotes(r);
}
btn.setText("Installed");
} catch (e) {
new import_obsidian6.Notice(`Install failed: ${e}`);
btn.disabled = false;
btn.setText("Install");
}
}
/** Download full vault tree into a named subfolder */
async installVault(r) {
const treeData = await requestJson(
`https://api.github.com/repos/${r.full_name}/git/trees/HEAD?recursive=1`,
{ headers: { Accept: "application/vnd.github.v3+json" } }
);
const tree = Array.isArray(treeData.tree) ? treeData.tree.filter(isGitHubTreeItem2) : [];
if (tree.length === 0) {
throw new Error(treeData.message || "Failed to fetch repository tree");
}
const textExts = /\.(md|canvas|txt|css|js|ts|json|yaml|yml|html|xml|svg|toml|ini|cfg)$/i;
const blobs = tree.filter(
(item) => item.type === "blob" && !item.path.startsWith(".github/") && textExts.test(item.path)
);
const folderName = await this.availableFolder(r.repo_name);
let installed = 0;
let failed = 0;
for (const item of blobs) {
const destPath = `${folderName}/${item.path}`;
const dirPath = destPath.split("/").slice(0, -1).join("/");
await this.ensureDir(dirPath);
const rawUrl = `https://raw.githubusercontent.com/${r.full_name}/HEAD/${encodeGitHubPath(item.path)}`;
try {
const raw = await requestText(rawUrl);
await this.app.vault.adapter.write(destPath, raw);
installed++;
} catch (e) {
failed++;
}
}
new import_obsidian6.Notice(
`Installed "${r.title}" - ${installed} files in "${folderName}/"${failed ? ` (${failed} failed)` : ""}`
);
const hubMd = await this.fetchHubMd(r);
if (hubMd) {
await this.notifyRequiredPluginsFromContent(hubMd, r.title);
}
}
async ensureDir(dirPath) {
if (!dirPath) return;
const parts = dirPath.split("/");
let current = "";
for (const part of parts) {
current = current ? `${current}/${part}` : part;
if (!await this.app.vault.adapter.exists(current)) {
await this.app.vault.adapter.mkdir(current);
}
}
}
async availablePath(path) {
if (!await this.app.vault.adapter.exists(path)) return path;
const slash = path.lastIndexOf("/");
const dir = slash >= 0 ? path.slice(0, slash + 1) : "";
const file = slash >= 0 ? path.slice(slash + 1) : path;
const dot = file.lastIndexOf(".");
const stem = dot > 0 ? file.slice(0, dot) : file;
const ext = dot > 0 ? file.slice(dot) : "";
let suffix = 2;
let candidate = `${dir}${stem}-${suffix}${ext}`;
while (await this.app.vault.adapter.exists(candidate)) {
suffix++;
candidate = `${dir}${stem}-${suffix}${ext}`;
}
return candidate;
}
async availableFolder(baseName) {
let candidate = baseName;
let suffix = 2;
while (await this.app.vault.adapter.exists(candidate)) {
candidate = `${baseName}-${suffix}`;
suffix++;
}
return candidate;
}
async fetchHubMd(r) {
try {
return await requestText(`https://raw.githubusercontent.com/${r.full_name}/HEAD/hub.md`);
} catch (e) {
return null;
}
}
async notifyRequiredPluginsFromContent(hubMd, title) {
var _a, _b;
await Promise.resolve();
try {
const frontmatter = extractFrontmatter(hubMd);
if (!frontmatter) return;
const ids = parseRequiredPluginIds(frontmatter);
if (ids.length === 0) return;
const installedPlugins = Object.keys((_b = (_a = getAppInternals(this.app).plugins) == null ? void 0 : _a.plugins) != null ? _b : {});
const missing = ids.filter((id) => !installedPlugins.includes(id));
if (missing.length === 0) {
new import_obsidian6.Notice(`All required plugins already installed for "${title}"`);
} else {
new import_obsidian6.Notice(
`"${title}" needs ${missing.length} plugin(s): ${missing.join(", ")}. Install from Community Plugins.`,
1e4
);
}
} catch (e) {
}
}
/** Download CSS files to .obsidian/snippets/ */
async installSnippet(r) {
const treeData = await requestJson(
`https://api.github.com/repos/${r.full_name}/git/trees/HEAD?recursive=1`,
{ headers: { Accept: "application/vnd.github.v3+json" } }
);
const tree = Array.isArray(treeData.tree) ? treeData.tree.filter(isGitHubTreeItem2) : [];
if (tree.length === 0) {
throw new Error(treeData.message || "Failed to fetch repository tree");
}
const cssFiles = tree.filter(
(f) => f.type === "blob" && f.path.endsWith(".css")
);
if (cssFiles.length === 0) {
throw new Error("No CSS files found in this snippet repository");
}
const snippetsDir = getSnippetDirectory(this.app.vault);
if (!await this.app.vault.adapter.exists(snippetsDir)) {
await this.app.vault.adapter.mkdir(snippetsDir);
}
for (const f of cssFiles) {
const raw = await requestText(`https://raw.githubusercontent.com/${r.full_name}/HEAD/${encodeGitHubPath(f.path)}`);
const target = await this.availablePath(`${snippetsDir}/${basename(f.path)}`);
await this.app.vault.adapter.write(target, raw);
}
new import_obsidian6.Notice(
`Installed ${cssFiles.length} snippet(s) from "${r.title}" - enable in Settings \u2192 Appearance \u2192 CSS snippets`
);
}
/** Download .md files into a named folder */
async installNotes(r) {
const hubMd = await this.fetchHubMd(r);
const treeData = await requestJson(
`https://api.github.com/repos/${r.full_name}/git/trees/HEAD?recursive=1`,
{ headers: { Accept: "application/vnd.github.v3+json" } }
);
const tree = Array.isArray(treeData.tree) ? treeData.tree.filter(isGitHubTreeItem2) : [];
if (tree.length === 0) {
throw new Error(treeData.message || "Failed to fetch repository tree");
}
const mdFiles = tree.filter(
(f) => f.type === "blob" && f.path.endsWith(".md") && basename(f.path).toLowerCase() !== "readme.md"
);
if (mdFiles.length === 0) {
throw new Error("No note files found in this repository");
}
const folderName = await this.availableFolder(r.repo_name);
let installed = 0;
for (const f of mdFiles) {
const raw = await requestText(`https://raw.githubusercontent.com/${r.full_name}/HEAD/${encodeGitHubPath(f.path)}`);
const destPath = `${folderName}/${f.path}`;
await this.ensureDir(destPath.split("/").slice(0, -1).join("/"));
await this.app.vault.adapter.write(destPath, raw);
installed++;
}
new import_obsidian6.Notice(`Installed ${installed} note(s) from "${r.title}" in "${folderName}/"`);
if (hubMd) {
await this.installAttachedSnippets(r, hubMd);
await this.notifyRequiredPluginsFromContent(hubMd, r.title);
}
}
async installAttachedSnippets(r, hubMd) {
const frontmatter = extractFrontmatter(hubMd);
if (!frontmatter) return;
const attachedSnippets = parseAttachedSnippets(frontmatter);
if (attachedSnippets.length === 0) return;
const snippetsDir = getSnippetDirectory(this.app.vault);
if (!await this.app.vault.adapter.exists(snippetsDir)) {
await this.app.vault.adapter.mkdir(snippetsDir);
}
let installed = 0;
let failed = 0;
for (const snippet of attachedSnippets) {
try {
const raw = await requestText(
`https://raw.githubusercontent.com/${r.full_name}/HEAD/${encodeGitHubPath(snippet.path)}`
);
const target = await this.availablePath(`${snippetsDir}/${basename(snippet.path)}`);
await this.app.vault.adapter.write(target, raw);
installed++;
} catch (e) {
failed++;
}
}
if (installed > 0 || failed > 0) {
new import_obsidian6.Notice(
`Installed ${installed} attached snippet(s) from "${r.title}"${failed ? ` (${failed} failed)` : ""}`
);
}
}
// ─── SNIPPET MANAGER ─────────────────────────────────────────
async renderSnippetManager() {
var _a;
const container = this.contentEl.querySelector(".vault-hub-snippets-container");
if (!container) return;
container.empty();
const snippetsDir = getSnippetDirectory(this.app.vault);
let files;
try {
const listing = await this.app.vault.adapter.list(snippetsDir);
files = listing.files.filter((f) => f.endsWith(".css")).map((f) => f.split("/").pop());
} catch (e) {
container.createEl("p", { text: "No snippets folder found.", cls: "vault-hub-hint" });
return;
}
if (files.length === 0) {
container.createEl("p", { text: "No snippets installed.", cls: "vault-hub-hint" });
return;
}
let enabledSnippets = [];
try {
const raw = await this.app.vault.adapter.read(
`${this.app.vault.configDir}/appearance.json`
);
enabledSnippets = (_a = parseAppearanceConfig2(raw).enabledCssSnippets) != null ? _a : [];
} catch (e) {
}
const list = container.createDiv("vault-hub-snippet-list");
for (const fileName of files) {
const snippetId = fileName.replace(/\.css$/, "");
const isEnabled = enabledSnippets.includes(snippetId);
const row = list.createDiv("vault-hub-snippet-row");
row.createSpan({ text: snippetId, cls: "vault-hub-snippet-name" });
const actions = row.createDiv("vault-hub-snippet-actions");
const toggleBtn = actions.createEl("button", {
text: isEnabled ? "Enabled" : "Disabled",
cls: `vault-hub-snippet-toggle${isEnabled ? " on" : ""}`
});
toggleBtn.addEventListener("click", () => void this.toggleSnippet(snippetId, !isEnabled));
const deleteBtn = actions.createEl("button", {
text: "Delete",
cls: "vault-hub-snippet-delete"
});
deleteBtn.addEventListener("click", () => void this.deleteSnippet(snippetsDir, fileName, snippetId));
}
}
async setSnippetEnabled(snippetId, enable) {
var _a, _b, _c;
const css = getAppInternals(this.app).customCss;
if (css) {
if (css.setCssEnabledStatus) {
css.setCssEnabledStatus(snippetId, enable);
(_a = css.requestLoadSnippets) == null ? void 0 : _a.call(css);
new import_obsidian6.Notice(`Snippet "${snippetId}" ${enable ? "enabled" : "disabled"}`);
return;
}
if (css.enabledSnippets) {
if (enable) css.enabledSnippets.add(snippetId);
else css.enabledSnippets.delete(snippetId);
(_b = css.requestLoadSnippets) == null ? void 0 : _b.call(css);
new import_obsidian6.Notice(`Snippet "${snippetId}" ${enable ? "enabled" : "disabled"}`);
return;
}
}
try {
const appearancePath = `${this.app.vault.configDir}/appearance.json`;
let appearance = {};
try {
appearance = parseAppearanceConfig2(await this.app.vault.adapter.read(appearancePath));
} catch (e) {
}
const enabled = (_c = appearance.enabledCssSnippets) != null ? _c : [];
if (enable && !enabled.includes(snippetId)) {
enabled.push(snippetId);
} else if (!enable) {
const i = enabled.indexOf(snippetId);
if (i !== -1) enabled.splice(i, 1);
}
appearance.enabledCssSnippets = enabled;
await this.app.vault.adapter.write(appearancePath, JSON.stringify(appearance, null, 2));
new import_obsidian6.Notice(`Snippet "${snippetId}" ${enable ? "enabled" : "disabled"} - reload to apply`);
} catch (e) {
new import_obsidian6.Notice(`Failed to toggle snippet: ${e}`);
}
}
async toggleSnippet(snippetId, enable) {
await this.setSnippetEnabled(snippetId, enable);
await this.renderSnippetManager();
}
async deleteSnippet(snippetsDir, fileName, snippetId) {
const ok = await confirmModal(this.app, `Delete snippet "${snippetId}"?`);
if (!ok) return;
await this.app.vault.adapter.remove(`${snippetsDir}/${fileName}`);
new import_obsidian6.Notice(`Deleted: ${snippetId}`);
await this.renderSnippetManager();
}
};
function getAppInternals(app) {
return app;
}
function parseAppearanceConfig2(raw) {
const parsed = JSON.parse(raw);
if (typeof parsed !== "object" || parsed === null) {
return { enabledCssSnippets: [] };
}
const config = parsed;
return {
...config,
enabledCssSnippets: Array.isArray(config.enabledCssSnippets) ? config.enabledCssSnippets.filter((value) => typeof value === "string") : []
};
}
// src/main.ts
var VaultHubPlugin = class extends import_obsidian7.Plugin {
constructor() {
super(...arguments);
this.settings = DEFAULT_SETTINGS;
}
async onload() {
await this.loadSettings();
this.registerView(VIEW_TYPE_BROWSE, (leaf) => new BrowseView(leaf, this));
this.addCommand({
id: "publish-resource",
name: "Publish resource",
callback: () => new PublishModal(this.app, this).open()
});
this.addCommand({
id: "update-resource",
name: "Update resource",
callback: () => new UpdateModal(this.app, this).open()
});
this.addCommand({
id: "browse-resources",
name: "Browse resources",
callback: () => {
void this.activateBrowseView();
}
});
this.addSettingTab(new VaultHubSettingTab(this.app, this));
this.addRibbonIcon("globe", "Vault hub", () => {
void this.activateBrowseView();
});
}
onunload() {
}
async loadSettings() {
const data = await this.loadData();
this.settings = parseSettings(data);
}
async saveSettings() {
await this.saveData(this.settings);
}
async activateBrowseView() {
const { workspace } = this.app;
let leaf = workspace.getLeavesOfType(VIEW_TYPE_BROWSE)[0];
if (!leaf) {
const rightLeaf = workspace.getRightLeaf(false);
if (rightLeaf) {
leaf = rightLeaf;
await leaf.setViewState({ type: VIEW_TYPE_BROWSE, active: true });
}
}
if (leaf) {
if ((0, import_obsidian7.requireApiVersion)("1.7.2")) {
await workspace.revealLeaf(leaf);
} else {
workspace.setActiveLeaf(leaf, { focus: true });
}
}
}
};
function parseSettings(value) {
if (typeof value !== "object" || value === null) {
return { ...DEFAULT_SETTINGS };
}
const data = value;
return {
githubToken: typeof data.githubToken === "string" ? data.githubToken : DEFAULT_SETTINGS.githubToken,
defaultCategories: Array.isArray(data.defaultCategories) ? data.defaultCategories.filter((item) => typeof item === "string") : [...DEFAULT_SETTINGS.defaultCategories],
vaultHubUrl: typeof data.vaultHubUrl === "string" ? data.vaultHubUrl : DEFAULT_SETTINGS.vaultHubUrl,
catalogRepoFullName: typeof data.catalogRepoFullName === "string" ? data.catalogRepoFullName : DEFAULT_SETTINGS.catalogRepoFullName,
publishedResources: Array.isArray(data.publishedResources) ? data.publishedResources.filter(isPublishedResource) : [...DEFAULT_SETTINGS.publishedResources],
publishDraft: isPublishDraft(data.publishDraft) ? data.publishDraft : DEFAULT_SETTINGS.publishDraft
};
}
function isPublishedResource(value) {
if (typeof value !== "object" || value === null) return false;
const resource = value;
return typeof resource.repoFullName === "string" && typeof resource.localFilePath === "string" && typeof resource.lastPublishedAt === "string" && (resource.type === "snippet" || resource.type === "note" || resource.type === "vault" || resource.type === "bundle");
}
function isPublishDraft(value) {
if (typeof value !== "object" || value === null) return false;
const draft = value;
return typeof draft.step === "number" && (draft.resourceType === "snippet" || draft.resourceType === "note" || draft.resourceType === "bundle") && Array.isArray(draft.selectedFilePaths) && Array.isArray(draft.attachedSnippetPaths) && Array.isArray(draft.screenshotPaths) && typeof draft.externalScreenshotUrls === "string" && typeof draft.fileSearchQuery === "string" && typeof draft.attachedSnippetSearchQuery === "string" && typeof draft.screenshotSearchQuery === "string" && Array.isArray(draft.checkedPluginIds) && typeof draft.name === "string" && typeof draft.tagline === "string" && typeof draft.description === "string" && Array.isArray(draft.categories) && typeof draft.tags === "string" && Array.isArray(draft.compatibleThemes) && typeof draft.readmeContent === "string";
}