mirror of
https://github.com/maws7140/vault-hub-plugin.git
synced 2026-07-22 06:49:30 +00:00
- 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
2738 lines
104 KiB
JavaScript
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(``);
|
|
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(``);
|
|
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";
|
|
}
|