mirror of
https://github.com/maws7140/vault-hub-plugin.git
synced 2026-07-22 06:49:30 +00:00
Add Vault Hub Obsidian plugin
5-step publish wizard, plugin dependency detection, browse view with Supabase search, update flow, and GitHub-backed publishing.
This commit is contained in:
parent
cb80d550b3
commit
16c36f2082
17 changed files with 4957 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
node_modules/
|
||||
41
esbuild.config.mjs
Normal file
41
esbuild.config.mjs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import esbuild from "esbuild";
|
||||
import process from "process";
|
||||
import builtins from "builtin-modules";
|
||||
|
||||
const banner = `/*
|
||||
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||
If you want to view the source, please visit the GitHub repository of this plugin.
|
||||
*/
|
||||
`;
|
||||
|
||||
const prod = process.argv[2] === "production";
|
||||
|
||||
esbuild
|
||||
.build({
|
||||
banner: { js: banner },
|
||||
entryPoints: ["src/main.ts"],
|
||||
bundle: true,
|
||||
external: [
|
||||
"obsidian",
|
||||
"electron",
|
||||
"@codemirror/autocomplete",
|
||||
"@codemirror/collab",
|
||||
"@codemirror/commands",
|
||||
"@codemirror/language",
|
||||
"@codemirror/lint",
|
||||
"@codemirror/search",
|
||||
"@codemirror/state",
|
||||
"@codemirror/view",
|
||||
"@lezer/common",
|
||||
"@lezer/highlight",
|
||||
"@lezer/lr",
|
||||
...builtins,
|
||||
],
|
||||
format: "cjs",
|
||||
target: "es2018",
|
||||
logLevel: "info",
|
||||
sourcemap: prod ? false : "inline",
|
||||
treeShaking: true,
|
||||
outfile: "main.js",
|
||||
})
|
||||
.catch(() => process.exit(1));
|
||||
969
main.js
Normal file
969
main.js
Normal file
|
|
@ -0,0 +1,969 @@
|
|||
/*
|
||||
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_obsidian5 = require("obsidian");
|
||||
|
||||
// src/settings.ts
|
||||
var import_obsidian = require("obsidian");
|
||||
var DEFAULT_SETTINGS = {
|
||||
githubToken: "",
|
||||
defaultCategories: [],
|
||||
vaultHubUrl: "http://localhost:3000",
|
||||
publishedResources: []
|
||||
};
|
||||
var VaultHubSettingTab = class extends import_obsidian.PluginSettingTab {
|
||||
constructor(app, plugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
display() {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
containerEl.createEl("h2", { text: "Vault Hub Settings" });
|
||||
new import_obsidian.Setting(containerEl).setName("GitHub Personal Access Token").setDesc("Token used to create repos and push files. Requires 'repo' scope.").addText(
|
||||
(text) => text.setPlaceholder("ghp_xxxxxxxxxxxx").setValue(this.plugin.settings.githubToken).then((t) => {
|
||||
t.inputEl.type = "password";
|
||||
t.inputEl.style.width = "300px";
|
||||
}).onChange(async (value) => {
|
||||
this.plugin.settings.githubToken = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
new import_obsidian.Setting(containerEl).setName("Vault Hub URL").setDesc("URL of the Vault Hub website (default: http://localhost:3000 for development).").addText(
|
||||
(text) => text.setPlaceholder("http://localhost:3000").setValue(this.plugin.settings.vaultHubUrl).onChange(async (value) => {
|
||||
this.plugin.settings.vaultHubUrl = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
new import_obsidian.Setting(containerEl).setName("Default Categories").setDesc("Comma-separated list of default categories for new publications.").addText(
|
||||
(text) => text.setPlaceholder("appearance, workflow").setValue(this.plugin.settings.defaultCategories.join(", ")).onChange(async (value) => {
|
||||
this.plugin.settings.defaultCategories = value.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// src/modals/PublishModal.ts
|
||||
var import_obsidian2 = require("obsidian");
|
||||
|
||||
// src/detection.ts
|
||||
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 = JSON.parse(raw);
|
||||
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/github.ts
|
||||
var GitHubAPI = class {
|
||||
constructor(token) {
|
||||
this.token = token;
|
||||
}
|
||||
async request(path, options = {}) {
|
||||
const res = await fetch(`https://api.github.com${path}`, {
|
||||
...options,
|
||||
headers: {
|
||||
Authorization: `token ${this.token}`,
|
||||
Accept: "application/vnd.github.v3+json",
|
||||
"Content-Type": "application/json",
|
||||
...options.headers
|
||||
}
|
||||
});
|
||||
if (!res.ok) {
|
||||
const body = await res.text();
|
||||
throw new Error(`GitHub API ${res.status}: ${body}`);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
async getUser() {
|
||||
return this.request("/user");
|
||||
}
|
||||
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 = btoa(unescape(encodeURIComponent(content)));
|
||||
return this.request(`/repos/${owner}/${repo}/contents/${path}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ message, content: encoded })
|
||||
});
|
||||
}
|
||||
async updateFile(owner, repo, path, content, message, sha) {
|
||||
const encoded = btoa(unescape(encodeURIComponent(content)));
|
||||
return this.request(`/repos/${owner}/${repo}/contents/${path}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ message, content: encoded, sha })
|
||||
});
|
||||
}
|
||||
async getFileContent(owner, repo, path) {
|
||||
try {
|
||||
const data = await this.request(
|
||||
`/repos/${owner}/${repo}/contents/${path}`
|
||||
);
|
||||
return { sha: data.sha, content: atob(data.content) };
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// src/hubyml.ts
|
||||
function generateHubYml(data) {
|
||||
const lines = [];
|
||||
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((l) => lines.push(` ${l}`));
|
||||
lines.push(`author: ${data.author}`);
|
||||
lines.push("categories:");
|
||||
data.categories.forEach((c) => lines.push(` - ${c}`));
|
||||
if (data.tags.length > 0) {
|
||||
lines.push("tags:");
|
||||
data.tags.forEach((t) => lines.push(` - ${t}`));
|
||||
}
|
||||
if (data.compatibleThemes.length > 0) {
|
||||
lines.push("compatible_themes:");
|
||||
data.compatibleThemes.forEach((t) => lines.push(` - ${t}`));
|
||||
}
|
||||
if (data.screenshots.length > 0) {
|
||||
lines.push("screenshots:");
|
||||
data.screenshots.forEach((s) => lines.push(` - ${s}`));
|
||||
}
|
||||
const selected = data.plugins.filter((p) => p.autoDetected);
|
||||
if (selected.length > 0) {
|
||||
lines.push("plugins:");
|
||||
selected.forEach((p) => {
|
||||
lines.push(` - id: ${p.id}`);
|
||||
lines.push(` name: ${p.name}`);
|
||||
lines.push(` version: ${p.version}`);
|
||||
});
|
||||
}
|
||||
lines.push("environment:");
|
||||
lines.push(` obsidian_version: "${data.obsidianVersion}"`);
|
||||
lines.push(` theme: ${data.theme}`);
|
||||
lines.push(` os: ${data.os}`);
|
||||
lines.push("files:");
|
||||
data.files.forEach((f) => {
|
||||
lines.push(` - path: ${f.path}`);
|
||||
lines.push(` type: ${f.type}`);
|
||||
lines.push(` size: ${f.size}`);
|
||||
});
|
||||
return lines.join("\n") + "\n";
|
||||
}
|
||||
function esc(s) {
|
||||
return s.replace(/"/g, '\\"');
|
||||
}
|
||||
|
||||
// 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);
|
||||
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("");
|
||||
}
|
||||
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 `.obsidian/snippets/` folder"
|
||||
);
|
||||
lines.push("3. Enable it in Settings > Appearance > CSS Snippets");
|
||||
} else {
|
||||
lines.push("1. Download the `.md` file(s) from this repo");
|
||||
lines.push("2. Place them in your vault");
|
||||
if (selected.length > 0) {
|
||||
lines.push("3. Install the required plugins listed above");
|
||||
}
|
||||
}
|
||||
lines.push("");
|
||||
lines.push("---");
|
||||
lines.push("*Published via [Vault Hub](https://vaulthub.dev)*");
|
||||
lines.push("");
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
// src/modals/PublishModal.ts
|
||||
var CATEGORIES = {
|
||||
snippet: [
|
||||
"ui-tweak",
|
||||
"theme-override",
|
||||
"layout",
|
||||
"typography",
|
||||
"color-scheme",
|
||||
"editor",
|
||||
"sidebar",
|
||||
"publishing"
|
||||
],
|
||||
note: [
|
||||
"dashboard",
|
||||
"tracker",
|
||||
"query",
|
||||
"daily-note",
|
||||
"project-template",
|
||||
"kanban",
|
||||
"database",
|
||||
"automation"
|
||||
],
|
||||
bundle: [
|
||||
"dashboard",
|
||||
"tracker",
|
||||
"query",
|
||||
"project-template",
|
||||
"automation"
|
||||
]
|
||||
};
|
||||
var PublishModal = class extends import_obsidian2.Modal {
|
||||
constructor(app, plugin) {
|
||||
super(app);
|
||||
this.step = 1;
|
||||
// Step 1
|
||||
this.resourceType = "snippet";
|
||||
this.selectedFiles = [];
|
||||
// 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.plugin = plugin;
|
||||
}
|
||||
onOpen() {
|
||||
this.renderStep();
|
||||
}
|
||||
onClose() {
|
||||
this.contentEl.empty();
|
||||
}
|
||||
renderStep() {
|
||||
this.contentEl.empty();
|
||||
this.contentEl.addClass("vault-hub-modal");
|
||||
const header = this.contentEl.createDiv("vault-hub-header");
|
||||
header.createEl("h2", { text: `Publish Resource \u2014 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");
|
||||
}
|
||||
switch (this.step) {
|
||||
case 1:
|
||||
this.renderStep1();
|
||||
break;
|
||||
case 2:
|
||||
this.renderStep2();
|
||||
break;
|
||||
case 3:
|
||||
this.renderStep3();
|
||||
break;
|
||||
case 4:
|
||||
this.renderStep4();
|
||||
break;
|
||||
case 5:
|
||||
this.renderStep5();
|
||||
break;
|
||||
}
|
||||
}
|
||||
renderStep1() {
|
||||
const c = this.contentEl;
|
||||
new import_obsidian2.Setting(c).setName("Resource Type").addDropdown((dd) => {
|
||||
dd.addOption("snippet", "CSS Snippet");
|
||||
dd.addOption("note", "Note / Template");
|
||||
dd.addOption("bundle", "Bundle (multiple files)");
|
||||
dd.setValue(this.resourceType);
|
||||
dd.onChange((v) => {
|
||||
this.resourceType = v;
|
||||
this.selectedFiles = [];
|
||||
this.renderStep();
|
||||
});
|
||||
});
|
||||
const ext = this.resourceType === "snippet" ? ".css" : ".md";
|
||||
const files = this.app.vault.getFiles().filter((f) => f.extension === ext.slice(1)).sort((a, b) => a.path.localeCompare(b.path));
|
||||
const fileSection = c.createDiv();
|
||||
fileSection.createEl("h4", { text: `Select file${this.resourceType === "bundle" ? "s" : ""}` });
|
||||
const list = fileSection.createDiv("vault-hub-file-list");
|
||||
files.forEach((f) => {
|
||||
const row = list.createDiv("vault-hub-file-row");
|
||||
const cb = row.createEl("input", { type: this.resourceType === "bundle" ? "checkbox" : "radio" });
|
||||
cb.name = "vault-hub-file";
|
||||
cb.checked = this.selectedFiles.includes(f);
|
||||
cb.addEventListener("change", () => {
|
||||
if (this.resourceType === "bundle") {
|
||||
if (cb.checked)
|
||||
this.selectedFiles.push(f);
|
||||
else
|
||||
this.selectedFiles = this.selectedFiles.filter((x) => x !== f);
|
||||
} else {
|
||||
this.selectedFiles = cb.checked ? [f] : [];
|
||||
}
|
||||
});
|
||||
row.createSpan({ text: f.path });
|
||||
});
|
||||
this.addNav(c, null, () => {
|
||||
if (this.selectedFiles.length === 0) {
|
||||
new import_obsidian2.Notice("Select at least one file");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
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 content = await this.app.vault.read(this.selectedFiles[0]);
|
||||
const fileType = this.resourceType === "snippet" ? "css" : "md";
|
||||
this.allPlugins = await detectPlugins(content, fileType, this.app.vault);
|
||||
loading.remove();
|
||||
this.allPlugins.forEach((p) => {
|
||||
if (p.autoDetected)
|
||||
this.checkedPlugins.add(p.id);
|
||||
});
|
||||
const list = c.createDiv("vault-hub-plugin-list");
|
||||
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);
|
||||
});
|
||||
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" });
|
||||
}
|
||||
});
|
||||
if (this.allPlugins.length === 0) {
|
||||
c.createEl("p", { text: "No community plugins installed.", cls: "vault-hub-hint" });
|
||||
}
|
||||
this.addNav(c, null, null);
|
||||
}
|
||||
renderStep3() {
|
||||
const c = this.contentEl;
|
||||
new import_obsidian2.Setting(c).setName("Name").addText((t) => {
|
||||
t.setPlaceholder("My Resource").setValue(this.name);
|
||||
t.onChange((v) => this.name = v);
|
||||
t.inputEl.style.width = "100%";
|
||||
});
|
||||
new import_obsidian2.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.style.width = "100%";
|
||||
});
|
||||
new import_obsidian2.Setting(c).setName("Description").addTextArea((t) => {
|
||||
t.setPlaceholder("Detailed description...").setValue(this.description);
|
||||
t.onChange((v) => this.description = v);
|
||||
t.inputEl.style.width = "100%";
|
||||
t.inputEl.style.minHeight = "80px";
|
||||
});
|
||||
const cats = CATEGORIES[this.resourceType] || [];
|
||||
new import_obsidian2.Setting(c).setName("Category").addDropdown((dd) => {
|
||||
dd.addOption("", "Select...");
|
||||
cats.forEach((cat) => dd.addOption(cat, cat));
|
||||
dd.setValue(this.categories[0] || "");
|
||||
dd.onChange((v) => this.categories = v ? [v] : []);
|
||||
});
|
||||
new import_obsidian2.Setting(c).setName("Tags").setDesc("Comma-separated").addText((t) => {
|
||||
t.setPlaceholder("glass, blur, dark").setValue(this.tags);
|
||||
t.onChange((v) => this.tags = v);
|
||||
});
|
||||
if (this.resourceType === "snippet") {
|
||||
new import_obsidian2.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_obsidian2.Notice("Name is required");
|
||||
return false;
|
||||
}
|
||||
const selected = this.allPlugins.filter((p) => this.checkedPlugins.has(p.id)).map((p) => ({ ...p, autoDetected: true }));
|
||||
const readmeData = {
|
||||
name: this.name,
|
||||
tagline: this.tagline,
|
||||
description: this.description,
|
||||
type: this.resourceType,
|
||||
plugins: selected,
|
||||
files: this.selectedFiles.map((f) => ({ path: f.name }))
|
||||
};
|
||||
this.readmeContent = generateReadme(readmeData);
|
||||
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 summary = c.createDiv("vault-hub-summary");
|
||||
summary.createEl("p", { text: `Type: ${this.resourceType}` });
|
||||
summary.createEl("p", { text: `Name: ${this.name}` });
|
||||
summary.createEl("p", { text: `Files: ${this.selectedFiles.map((f) => f.name).join(", ")}` });
|
||||
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", () => {
|
||||
this.step--;
|
||||
this.renderStep();
|
||||
});
|
||||
const publishBtn = btnContainer.createEl("button", {
|
||||
text: "Publish",
|
||||
cls: "mod-cta"
|
||||
});
|
||||
publishBtn.addEventListener("click", () => this.doPublish());
|
||||
}
|
||||
async doPublish() {
|
||||
var _a;
|
||||
const token = this.plugin.settings.githubToken;
|
||||
if (!token) {
|
||||
new import_obsidian2.Notice("Set your GitHub token in Vault Hub settings first");
|
||||
return;
|
||||
}
|
||||
const c = this.contentEl;
|
||||
c.empty();
|
||||
c.createEl("h3", { text: "Publishing..." });
|
||||
const status = c.createEl("p", { text: "Creating repository..." });
|
||||
try {
|
||||
const gh = new GitHubAPI(token);
|
||||
const user = await gh.getUser();
|
||||
const slug = this.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
||||
const repoName = `obsidian-${this.resourceType}-${slug}`;
|
||||
status.setText("Creating repository...");
|
||||
const repo = await gh.createRepo(repoName, this.tagline || this.name);
|
||||
const [owner, rName] = repo.full_name.split("/");
|
||||
const topicMap = {
|
||||
snippet: "obsidian-css-snippet",
|
||||
note: "obsidian-note-template",
|
||||
bundle: "obsidian-note-template"
|
||||
};
|
||||
status.setText("Adding topic tag...");
|
||||
await gh.addTopics(owner, rName, [topicMap[this.resourceType]]);
|
||||
for (const file of this.selectedFiles) {
|
||||
status.setText(`Uploading ${file.name}...`);
|
||||
const content = await this.app.vault.read(file);
|
||||
await gh.createFile(owner, rName, file.name, content, `Add ${file.name}`);
|
||||
}
|
||||
status.setText("Generating hub.yml...");
|
||||
const selectedPlugins = this.allPlugins.filter((p) => this.checkedPlugins.has(p.id)).map((p) => ({ ...p, autoDetected: true }));
|
||||
const obsVer = this.app.appVersion || "unknown";
|
||||
const themeName = ((_a = this.app.vault.config) == null ? void 0 : _a.cssTheme) || "default";
|
||||
const hubData = {
|
||||
type: this.resourceType,
|
||||
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: [],
|
||||
plugins: selectedPlugins,
|
||||
obsidianVersion: obsVer,
|
||||
theme: themeName,
|
||||
os: navigator.platform,
|
||||
files: this.selectedFiles.map((f) => ({
|
||||
path: f.name,
|
||||
type: f.extension,
|
||||
size: f.stat.size
|
||||
}))
|
||||
};
|
||||
const hubYml = generateHubYml(hubData);
|
||||
await gh.createFile(owner, rName, "hub.yml", hubYml, "Add hub.yml");
|
||||
status.setText("Uploading README...");
|
||||
await gh.createFile(owner, rName, "README.md", this.readmeContent, "Add README");
|
||||
this.plugin.settings.publishedResources.push({
|
||||
repoFullName: repo.full_name,
|
||||
localFilePath: this.selectedFiles[0].path,
|
||||
type: this.resourceType,
|
||||
lastPublishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
||||
});
|
||||
await this.plugin.saveSettings();
|
||||
c.empty();
|
||||
c.createEl("h3", { text: "Published!" });
|
||||
c.createEl("p", { text: `Repository: ${repo.full_name}` });
|
||||
const link = c.createEl("a", {
|
||||
text: repo.html_url,
|
||||
href: repo.html_url
|
||||
});
|
||||
link.setAttr("target", "_blank");
|
||||
c.createEl("p", {
|
||||
text: "It will appear on Vault Hub within 6 hours.",
|
||||
cls: "vault-hub-hint"
|
||||
});
|
||||
const closeBtn = c.createEl("button", { text: "Close", cls: "mod-cta" });
|
||||
closeBtn.addEventListener("click", () => this.close());
|
||||
new import_obsidian2.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;
|
||||
this.step--;
|
||||
this.renderStep();
|
||||
});
|
||||
}
|
||||
if (this.step < 5) {
|
||||
const next = nav.createEl("button", { text: "Next", cls: "mod-cta" });
|
||||
next.addEventListener("click", () => {
|
||||
if (nextCheck && !nextCheck())
|
||||
return;
|
||||
this.step++;
|
||||
this.renderStep();
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// src/modals/UpdateModal.ts
|
||||
var import_obsidian3 = require("obsidian");
|
||||
var UpdateModal = class extends import_obsidian3.Modal {
|
||||
constructor(app, plugin) {
|
||||
super(app);
|
||||
this.selected = null;
|
||||
this.plugin = plugin;
|
||||
}
|
||||
onOpen() {
|
||||
this.render();
|
||||
}
|
||||
onClose() {
|
||||
this.contentEl.empty();
|
||||
}
|
||||
render() {
|
||||
const c = this.contentEl;
|
||||
c.empty();
|
||||
c.createEl("h2", { text: "Update Published Resource" });
|
||||
const resources = this.plugin.settings.publishedResources;
|
||||
if (resources.length === 0) {
|
||||
c.createEl("p", { text: "No published resources yet. Use Publish first." });
|
||||
return;
|
||||
}
|
||||
new import_obsidian3.Setting(c).setName("Select Resource").addDropdown((dd) => {
|
||||
dd.addOption("", "Choose...");
|
||||
resources.forEach((r, i) => {
|
||||
dd.addOption(String(i), `${r.repoFullName} (${r.type})`);
|
||||
});
|
||||
dd.onChange((v) => {
|
||||
this.selected = v ? resources[parseInt(v)] : null;
|
||||
});
|
||||
});
|
||||
const updateBtn = c.createEl("button", { text: "Push Update", cls: "mod-cta" });
|
||||
updateBtn.addEventListener("click", () => this.doUpdate());
|
||||
}
|
||||
async doUpdate() {
|
||||
if (!this.selected) {
|
||||
new import_obsidian3.Notice("Select a resource first");
|
||||
return;
|
||||
}
|
||||
const token = this.plugin.settings.githubToken;
|
||||
if (!token) {
|
||||
new import_obsidian3.Notice("Set your GitHub token in settings first");
|
||||
return;
|
||||
}
|
||||
const c = this.contentEl;
|
||||
c.empty();
|
||||
c.createEl("h3", { text: "Updating..." });
|
||||
try {
|
||||
const gh = new GitHubAPI(token);
|
||||
const [owner, repo] = this.selected.repoFullName.split("/");
|
||||
const file = this.app.vault.getAbstractFileByPath(this.selected.localFilePath);
|
||||
if (!file) {
|
||||
new import_obsidian3.Notice(`File not found: ${this.selected.localFilePath}`);
|
||||
return;
|
||||
}
|
||||
const content = await this.app.vault.read(file);
|
||||
const existing = await gh.getFileContent(owner, repo, file.name);
|
||||
if (existing) {
|
||||
await gh.updateFile(
|
||||
owner,
|
||||
repo,
|
||||
file.name,
|
||||
content,
|
||||
`Update ${file.name}`,
|
||||
existing.sha
|
||||
);
|
||||
} else {
|
||||
await gh.createFile(owner, repo, file.name, content, `Add ${file.name}`);
|
||||
}
|
||||
this.selected.lastPublishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
||||
await this.plugin.saveSettings();
|
||||
c.empty();
|
||||
c.createEl("h3", { text: "Updated!" });
|
||||
c.createEl("p", { text: `Pushed to ${this.selected.repoFullName}` });
|
||||
const closeBtn = c.createEl("button", { text: "Close", cls: "mod-cta" });
|
||||
closeBtn.addEventListener("click", () => this.close());
|
||||
new import_obsidian3.Notice("Resource updated!");
|
||||
} catch (e) {
|
||||
c.empty();
|
||||
c.createEl("h3", { text: "Error" });
|
||||
c.createEl("p", { text: String(e) });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// src/views/BrowseView.ts
|
||||
var import_obsidian4 = require("obsidian");
|
||||
var VIEW_TYPE_BROWSE = "vault-hub-browse";
|
||||
var SUPABASE_URL = "https://oxvxiqiushhpwzqtpzdg.supabase.co";
|
||||
var SUPABASE_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im94dnhpcWl1c2hocHd6cXRwemRnIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzU0MTI4NjAsImV4cCI6MjA5MDk4ODg2MH0.jOJloJvJmV2Q-xuY1wBbUjg0s7DnRGt6htycj4HBtEA";
|
||||
var BrowseView = class extends import_obsidian4.ItemView {
|
||||
constructor(leaf, plugin) {
|
||||
super(leaf);
|
||||
this.searchQuery = "";
|
||||
this.resources = [];
|
||||
this.plugin = plugin;
|
||||
}
|
||||
getViewType() {
|
||||
return VIEW_TYPE_BROWSE;
|
||||
}
|
||||
getDisplayText() {
|
||||
return "Vault Hub";
|
||||
}
|
||||
getIcon() {
|
||||
return "globe";
|
||||
}
|
||||
async onOpen() {
|
||||
this.render();
|
||||
await this.loadResources();
|
||||
}
|
||||
async onClose() {
|
||||
this.contentEl.empty();
|
||||
}
|
||||
render() {
|
||||
const c = this.contentEl;
|
||||
c.empty();
|
||||
c.addClass("vault-hub-browse");
|
||||
const header = c.createDiv("vault-hub-browse-header");
|
||||
header.createEl("h3", { text: "Vault Hub" });
|
||||
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")
|
||||
this.loadResources();
|
||||
});
|
||||
const searchBtn = searchRow.createEl("button", { text: "Search" });
|
||||
searchBtn.addEventListener("click", () => this.loadResources());
|
||||
c.createDiv("vault-hub-results");
|
||||
}
|
||||
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 {
|
||||
let url = `${SUPABASE_URL}/rest/v1/resources?select=id,type,title,owner,repo_name,full_name,tagline,stars,vote_count&is_active=eq.true&order=trending_score.desc&limit=30`;
|
||||
if (this.searchQuery.trim()) {
|
||||
url += `&or=(title.ilike.*${encodeURIComponent(this.searchQuery)}*,description.ilike.*${encodeURIComponent(this.searchQuery)}*)`;
|
||||
}
|
||||
const res = await fetch(url, {
|
||||
headers: {
|
||||
apikey: SUPABASE_KEY,
|
||||
Authorization: `Bearer ${SUPABASE_KEY}`
|
||||
}
|
||||
});
|
||||
this.resources = await res.json();
|
||||
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 card = container.createDiv("vault-hub-result-card");
|
||||
const typeColors = {
|
||||
vault: "#7f6df2",
|
||||
snippet: "#22d3ee",
|
||||
note: "#fb923c"
|
||||
};
|
||||
const badge = card.createSpan("vault-hub-type-badge");
|
||||
badge.setText(r.type);
|
||||
badge.style.backgroundColor = typeColors[r.type] || "#666";
|
||||
const title = card.createEl("h4");
|
||||
title.setText(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.vote_count} votes` });
|
||||
meta.createSpan({ text: r.owner });
|
||||
const actions = card.createDiv("vault-hub-result-actions");
|
||||
const installBtn = actions.createEl("button", { text: "Install" });
|
||||
installBtn.addEventListener("click", () => this.installResource(r));
|
||||
const ghBtn = actions.createEl("button", { text: "GitHub" });
|
||||
ghBtn.addEventListener("click", () => {
|
||||
window.open(`https://github.com/${r.full_name}`, "_blank");
|
||||
});
|
||||
});
|
||||
}
|
||||
async installResource(r) {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`https://api.github.com/repos/${r.full_name}/contents`,
|
||||
{ headers: { Accept: "application/vnd.github.v3+json" } }
|
||||
);
|
||||
const files = await res.json();
|
||||
if (r.type === "snippet") {
|
||||
const cssFile = files.find(
|
||||
(f) => f.name.endsWith(".css")
|
||||
);
|
||||
if (cssFile) {
|
||||
const raw = await fetch(cssFile.download_url);
|
||||
const content = await raw.text();
|
||||
const snippetPath = `${this.app.vault.configDir}/snippets/${cssFile.name}`;
|
||||
await this.app.vault.adapter.write(snippetPath, content);
|
||||
new import_obsidian4.Notice(`Installed snippet: ${cssFile.name}`);
|
||||
}
|
||||
} else {
|
||||
const mdFiles = files.filter(
|
||||
(f) => f.name.endsWith(".md") && f.name !== "README.md"
|
||||
);
|
||||
for (const f of mdFiles) {
|
||||
const raw = await fetch(f.download_url);
|
||||
const content = await raw.text();
|
||||
await this.app.vault.create(f.name, content);
|
||||
new import_obsidian4.Notice(`Installed: ${f.name}`);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
new import_obsidian4.Notice(`Install failed: ${e}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// src/main.ts
|
||||
var VaultHubPlugin = class extends import_obsidian5.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: () => this.activateBrowseView()
|
||||
});
|
||||
this.addSettingTab(new VaultHubSettingTab(this.app, this));
|
||||
this.addRibbonIcon("globe", "Vault Hub", () => {
|
||||
this.activateBrowseView();
|
||||
});
|
||||
}
|
||||
onunload() {
|
||||
}
|
||||
async loadSettings() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
}
|
||||
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) {
|
||||
workspace.revealLeaf(leaf);
|
||||
}
|
||||
}
|
||||
};
|
||||
9
manifest.json
Normal file
9
manifest.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"id": "vault-hub",
|
||||
"name": "Vault Hub",
|
||||
"version": "1.0.0",
|
||||
"minAppVersion": "1.0.0",
|
||||
"description": "Publish CSS snippets, notes, and templates to the Vault Hub community platform.",
|
||||
"author": "Maws7140",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
2443
package-lock.json
generated
Normal file
2443
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
23
package.json
Normal file
23
package.json
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"name": "vault-hub-plugin",
|
||||
"version": "1.0.0",
|
||||
"description": "Obsidian plugin for Vault Hub community platform",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"build": "node esbuild.config.mjs production"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Maws7140",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.0",
|
||||
"esbuild": "^0.19.11",
|
||||
"obsidian": "latest",
|
||||
"typescript": "^5.3.3",
|
||||
"tslib": "^2.6.2",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"builtin-modules": "^3.3.0"
|
||||
}
|
||||
}
|
||||
94
src/detection.ts
Normal file
94
src/detection.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import { Vault } from "obsidian";
|
||||
|
||||
export interface DetectedPlugin {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
author: string;
|
||||
autoDetected: boolean;
|
||||
}
|
||||
|
||||
interface PluginManifest {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
author: string;
|
||||
}
|
||||
|
||||
const MD_PATTERNS: [RegExp, string][] = [
|
||||
[/```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"],
|
||||
];
|
||||
|
||||
const CSS_PATTERNS: [RegExp, string][] = [
|
||||
[/\.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"],
|
||||
];
|
||||
|
||||
export async function getInstalledPlugins(
|
||||
vault: Vault
|
||||
): Promise<DetectedPlugin[]> {
|
||||
const plugins: DetectedPlugin[] = [];
|
||||
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: PluginManifest = JSON.parse(raw);
|
||||
plugins.push({
|
||||
id: manifest.id,
|
||||
name: manifest.name,
|
||||
version: manifest.version,
|
||||
author: manifest.author,
|
||||
autoDetected: false,
|
||||
});
|
||||
} catch {
|
||||
// skip plugins without valid manifest
|
||||
}
|
||||
}
|
||||
|
||||
return plugins;
|
||||
}
|
||||
|
||||
export async function detectPlugins(
|
||||
fileContent: string,
|
||||
fileType: "css" | "md",
|
||||
vault: Vault
|
||||
): Promise<DetectedPlugin[]> {
|
||||
const installed = await getInstalledPlugins(vault);
|
||||
const installedIds = new Set(installed.map((p) => p.id));
|
||||
const detectedIds = new Set<string>();
|
||||
|
||||
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),
|
||||
}));
|
||||
}
|
||||
90
src/github.ts
Normal file
90
src/github.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
export class GitHubAPI {
|
||||
constructor(private token: string) {}
|
||||
|
||||
private async request(path: string, options: RequestInit = {}) {
|
||||
const res = await fetch(`https://api.github.com${path}`, {
|
||||
...options,
|
||||
headers: {
|
||||
Authorization: `token ${this.token}`,
|
||||
Accept: "application/vnd.github.v3+json",
|
||||
"Content-Type": "application/json",
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
const body = await res.text();
|
||||
throw new Error(`GitHub API ${res.status}: ${body}`);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async getUser(): Promise<{ login: string; avatar_url: string }> {
|
||||
return this.request("/user");
|
||||
}
|
||||
|
||||
async createRepo(
|
||||
name: string,
|
||||
description: string
|
||||
): Promise<{ full_name: string; html_url: string }> {
|
||||
return this.request("/user/repos", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
description,
|
||||
auto_init: false,
|
||||
private: false,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
async addTopics(owner: string, repo: string, topics: string[]) {
|
||||
return this.request(`/repos/${owner}/${repo}/topics`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ names: topics }),
|
||||
});
|
||||
}
|
||||
|
||||
async createFile(
|
||||
owner: string,
|
||||
repo: string,
|
||||
path: string,
|
||||
content: string,
|
||||
message: string
|
||||
) {
|
||||
const encoded = btoa(unescape(encodeURIComponent(content)));
|
||||
return this.request(`/repos/${owner}/${repo}/contents/${path}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ message, content: encoded }),
|
||||
});
|
||||
}
|
||||
|
||||
async updateFile(
|
||||
owner: string,
|
||||
repo: string,
|
||||
path: string,
|
||||
content: string,
|
||||
message: string,
|
||||
sha: string
|
||||
) {
|
||||
const encoded = btoa(unescape(encodeURIComponent(content)));
|
||||
return this.request(`/repos/${owner}/${repo}/contents/${path}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ message, content: encoded, sha }),
|
||||
});
|
||||
}
|
||||
|
||||
async getFileContent(
|
||||
owner: string,
|
||||
repo: string,
|
||||
path: string
|
||||
): Promise<{ sha: string; content: string } | null> {
|
||||
try {
|
||||
const data = await this.request(
|
||||
`/repos/${owner}/${repo}/contents/${path}`
|
||||
);
|
||||
return { sha: data.sha, content: atob(data.content) };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
75
src/hubyml.ts
Normal file
75
src/hubyml.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { DetectedPlugin } from "./detection";
|
||||
|
||||
export interface HubYmlData {
|
||||
type: "snippet" | "note" | "bundle";
|
||||
name: string;
|
||||
tagline: string;
|
||||
description: string;
|
||||
author: string;
|
||||
categories: string[];
|
||||
tags: string[];
|
||||
compatibleThemes: string[];
|
||||
screenshots: string[];
|
||||
plugins: DetectedPlugin[];
|
||||
obsidianVersion: string;
|
||||
theme: string;
|
||||
os: string;
|
||||
files: { path: string; type: string; size: number }[];
|
||||
}
|
||||
|
||||
export function generateHubYml(data: HubYmlData): string {
|
||||
const lines: string[] = [];
|
||||
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((l) => lines.push(` ${l}`));
|
||||
lines.push(`author: ${data.author}`);
|
||||
|
||||
lines.push("categories:");
|
||||
data.categories.forEach((c) => lines.push(` - ${c}`));
|
||||
|
||||
if (data.tags.length > 0) {
|
||||
lines.push("tags:");
|
||||
data.tags.forEach((t) => lines.push(` - ${t}`));
|
||||
}
|
||||
|
||||
if (data.compatibleThemes.length > 0) {
|
||||
lines.push("compatible_themes:");
|
||||
data.compatibleThemes.forEach((t) => lines.push(` - ${t}`));
|
||||
}
|
||||
|
||||
if (data.screenshots.length > 0) {
|
||||
lines.push("screenshots:");
|
||||
data.screenshots.forEach((s) => lines.push(` - ${s}`));
|
||||
}
|
||||
|
||||
const selected = data.plugins.filter((p) => p.autoDetected);
|
||||
if (selected.length > 0) {
|
||||
lines.push("plugins:");
|
||||
selected.forEach((p) => {
|
||||
lines.push(` - id: ${p.id}`);
|
||||
lines.push(` name: ${p.name}`);
|
||||
lines.push(` version: ${p.version}`);
|
||||
});
|
||||
}
|
||||
|
||||
lines.push("environment:");
|
||||
lines.push(` obsidian_version: "${data.obsidianVersion}"`);
|
||||
lines.push(` theme: ${data.theme}`);
|
||||
lines.push(` os: ${data.os}`);
|
||||
|
||||
lines.push("files:");
|
||||
data.files.forEach((f) => {
|
||||
lines.push(` - path: ${f.path}`);
|
||||
lines.push(` type: ${f.type}`);
|
||||
lines.push(` size: ${f.size}`);
|
||||
});
|
||||
|
||||
return lines.join("\n") + "\n";
|
||||
}
|
||||
|
||||
function esc(s: string): string {
|
||||
return s.replace(/"/g, '\\"');
|
||||
}
|
||||
68
src/main.ts
Normal file
68
src/main.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { Plugin } from "obsidian";
|
||||
import {
|
||||
VaultHubSettings,
|
||||
DEFAULT_SETTINGS,
|
||||
VaultHubSettingTab,
|
||||
} from "./settings";
|
||||
import { PublishModal } from "./modals/PublishModal";
|
||||
import { UpdateModal } from "./modals/UpdateModal";
|
||||
import { BrowseView, VIEW_TYPE_BROWSE } from "./views/BrowseView";
|
||||
|
||||
export default class VaultHubPlugin extends Plugin {
|
||||
settings: VaultHubSettings = 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: () => this.activateBrowseView(),
|
||||
});
|
||||
|
||||
this.addSettingTab(new VaultHubSettingTab(this.app, this));
|
||||
|
||||
this.addRibbonIcon("globe", "Vault Hub", () => {
|
||||
this.activateBrowseView();
|
||||
});
|
||||
}
|
||||
|
||||
onunload() {}
|
||||
|
||||
async loadSettings() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
|
||||
private 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) {
|
||||
workspace.revealLeaf(leaf);
|
||||
}
|
||||
}
|
||||
}
|
||||
444
src/modals/PublishModal.ts
Normal file
444
src/modals/PublishModal.ts
Normal file
|
|
@ -0,0 +1,444 @@
|
|||
import {
|
||||
App,
|
||||
Modal,
|
||||
Setting,
|
||||
Notice,
|
||||
TFile,
|
||||
TextAreaComponent,
|
||||
DropdownComponent,
|
||||
} from "obsidian";
|
||||
import type VaultHubPlugin from "../main";
|
||||
import { DetectedPlugin, detectPlugins, getInstalledPlugins } from "../detection";
|
||||
import { GitHubAPI } from "../github";
|
||||
import { generateHubYml, HubYmlData } from "../hubyml";
|
||||
import { generateReadme, ReadmeData } from "../readme";
|
||||
|
||||
type ResourceType = "snippet" | "note" | "bundle";
|
||||
|
||||
const CATEGORIES: Record<string, string[]> = {
|
||||
snippet: [
|
||||
"ui-tweak", "theme-override", "layout", "typography",
|
||||
"color-scheme", "editor", "sidebar", "publishing",
|
||||
],
|
||||
note: [
|
||||
"dashboard", "tracker", "query", "daily-note",
|
||||
"project-template", "kanban", "database", "automation",
|
||||
],
|
||||
bundle: [
|
||||
"dashboard", "tracker", "query", "project-template", "automation",
|
||||
],
|
||||
};
|
||||
|
||||
export class PublishModal extends Modal {
|
||||
plugin: VaultHubPlugin;
|
||||
step = 1;
|
||||
|
||||
// Step 1
|
||||
resourceType: ResourceType = "snippet";
|
||||
selectedFiles: TFile[] = [];
|
||||
|
||||
// Step 2
|
||||
allPlugins: DetectedPlugin[] = [];
|
||||
checkedPlugins: Set<string> = new Set();
|
||||
|
||||
// Step 3
|
||||
name = "";
|
||||
tagline = "";
|
||||
description = "";
|
||||
categories: string[] = [];
|
||||
tags = "";
|
||||
compatibleThemes: string[] = [];
|
||||
|
||||
// Step 4
|
||||
readmeContent = "";
|
||||
|
||||
constructor(app: App, plugin: VaultHubPlugin) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
this.renderStep();
|
||||
}
|
||||
|
||||
onClose() {
|
||||
this.contentEl.empty();
|
||||
}
|
||||
|
||||
private 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");
|
||||
}
|
||||
|
||||
switch (this.step) {
|
||||
case 1: this.renderStep1(); break;
|
||||
case 2: this.renderStep2(); break;
|
||||
case 3: this.renderStep3(); break;
|
||||
case 4: this.renderStep4(); break;
|
||||
case 5: this.renderStep5(); break;
|
||||
}
|
||||
}
|
||||
|
||||
private renderStep1() {
|
||||
const c = this.contentEl;
|
||||
|
||||
new Setting(c)
|
||||
.setName("Resource Type")
|
||||
.addDropdown((dd: DropdownComponent) => {
|
||||
dd.addOption("snippet", "CSS Snippet");
|
||||
dd.addOption("note", "Note / Template");
|
||||
dd.addOption("bundle", "Bundle (multiple files)");
|
||||
dd.setValue(this.resourceType);
|
||||
dd.onChange((v: string) => {
|
||||
this.resourceType = v as ResourceType;
|
||||
this.selectedFiles = [];
|
||||
this.renderStep();
|
||||
});
|
||||
});
|
||||
|
||||
const ext = this.resourceType === "snippet" ? ".css" : ".md";
|
||||
const files = this.app.vault
|
||||
.getFiles()
|
||||
.filter((f: TFile) => f.extension === ext.slice(1))
|
||||
.sort((a: TFile, b: TFile) => a.path.localeCompare(b.path));
|
||||
|
||||
const fileSection = c.createDiv();
|
||||
fileSection.createEl("h4", { text: `Select file${this.resourceType === "bundle" ? "s" : ""}` });
|
||||
|
||||
const list = fileSection.createDiv("vault-hub-file-list");
|
||||
files.forEach((f: TFile) => {
|
||||
const row = list.createDiv("vault-hub-file-row");
|
||||
const cb = row.createEl("input", { type: this.resourceType === "bundle" ? "checkbox" : "radio" });
|
||||
cb.name = "vault-hub-file";
|
||||
cb.checked = this.selectedFiles.includes(f);
|
||||
cb.addEventListener("change", () => {
|
||||
if (this.resourceType === "bundle") {
|
||||
if (cb.checked) this.selectedFiles.push(f);
|
||||
else this.selectedFiles = this.selectedFiles.filter((x) => x !== f);
|
||||
} else {
|
||||
this.selectedFiles = cb.checked ? [f] : [];
|
||||
}
|
||||
});
|
||||
row.createSpan({ text: f.path });
|
||||
});
|
||||
|
||||
this.addNav(c, null, () => {
|
||||
if (this.selectedFiles.length === 0) {
|
||||
new Notice("Select at least one file");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private 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 content = await this.app.vault.read(this.selectedFiles[0]);
|
||||
const fileType = this.resourceType === "snippet" ? "css" : "md";
|
||||
this.allPlugins = await detectPlugins(content, fileType as "css" | "md", this.app.vault);
|
||||
|
||||
loading.remove();
|
||||
|
||||
// Pre-check auto-detected
|
||||
this.allPlugins.forEach((p) => {
|
||||
if (p.autoDetected) this.checkedPlugins.add(p.id);
|
||||
});
|
||||
|
||||
const list = c.createDiv("vault-hub-plugin-list");
|
||||
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);
|
||||
});
|
||||
|
||||
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" });
|
||||
}
|
||||
});
|
||||
|
||||
if (this.allPlugins.length === 0) {
|
||||
c.createEl("p", { text: "No community plugins installed.", cls: "vault-hub-hint" });
|
||||
}
|
||||
|
||||
this.addNav(c, null, null);
|
||||
}
|
||||
|
||||
private renderStep3() {
|
||||
const c = this.contentEl;
|
||||
|
||||
new Setting(c).setName("Name").addText((t) => {
|
||||
t.setPlaceholder("My Resource").setValue(this.name);
|
||||
t.onChange((v: string) => (this.name = v));
|
||||
t.inputEl.style.width = "100%";
|
||||
});
|
||||
|
||||
new Setting(c).setName("Tagline").setDesc("One-line summary").addText((t) => {
|
||||
t.setPlaceholder("A brief description").setValue(this.tagline);
|
||||
t.onChange((v: string) => (this.tagline = v));
|
||||
t.inputEl.style.width = "100%";
|
||||
});
|
||||
|
||||
new Setting(c).setName("Description").addTextArea((t: TextAreaComponent) => {
|
||||
t.setPlaceholder("Detailed description...").setValue(this.description);
|
||||
t.onChange((v: string) => (this.description = v));
|
||||
t.inputEl.style.width = "100%";
|
||||
t.inputEl.style.minHeight = "80px";
|
||||
});
|
||||
|
||||
const cats = CATEGORIES[this.resourceType] || [];
|
||||
new Setting(c).setName("Category").addDropdown((dd: DropdownComponent) => {
|
||||
dd.addOption("", "Select...");
|
||||
cats.forEach((cat) => dd.addOption(cat, cat));
|
||||
dd.setValue(this.categories[0] || "");
|
||||
dd.onChange((v: string) => (this.categories = v ? [v] : []));
|
||||
});
|
||||
|
||||
new Setting(c).setName("Tags").setDesc("Comma-separated").addText((t) => {
|
||||
t.setPlaceholder("glass, blur, dark").setValue(this.tags);
|
||||
t.onChange((v: string) => (this.tags = v));
|
||||
});
|
||||
|
||||
if (this.resourceType === "snippet") {
|
||||
new Setting(c).setName("Compatible Themes").addDropdown((dd: DropdownComponent) => {
|
||||
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: string) => (this.compatibleThemes = [v]));
|
||||
});
|
||||
}
|
||||
|
||||
this.addNav(c, null, () => {
|
||||
if (!this.name.trim()) {
|
||||
new Notice("Name is required");
|
||||
return false;
|
||||
}
|
||||
// Generate README for next step
|
||||
const selected = this.allPlugins
|
||||
.filter((p) => this.checkedPlugins.has(p.id))
|
||||
.map((p) => ({ ...p, autoDetected: true }));
|
||||
|
||||
const readmeData: ReadmeData = {
|
||||
name: this.name,
|
||||
tagline: this.tagline,
|
||||
description: this.description,
|
||||
type: this.resourceType,
|
||||
plugins: selected,
|
||||
files: this.selectedFiles.map((f) => ({ path: f.name })),
|
||||
};
|
||||
this.readmeContent = generateReadme(readmeData);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private 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);
|
||||
}
|
||||
|
||||
private renderStep5() {
|
||||
const c = this.contentEl;
|
||||
c.createEl("h4", { text: "Review & Publish" });
|
||||
|
||||
const summary = c.createDiv("vault-hub-summary");
|
||||
summary.createEl("p", { text: `Type: ${this.resourceType}` });
|
||||
summary.createEl("p", { text: `Name: ${this.name}` });
|
||||
summary.createEl("p", { text: `Files: ${this.selectedFiles.map((f) => f.name).join(", ")}` });
|
||||
|
||||
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", () => { this.step--; this.renderStep(); });
|
||||
|
||||
const publishBtn = btnContainer.createEl("button", {
|
||||
text: "Publish",
|
||||
cls: "mod-cta",
|
||||
});
|
||||
publishBtn.addEventListener("click", () => this.doPublish());
|
||||
}
|
||||
|
||||
private async doPublish() {
|
||||
const token = this.plugin.settings.githubToken;
|
||||
if (!token) {
|
||||
new Notice("Set your GitHub token in Vault Hub settings first");
|
||||
return;
|
||||
}
|
||||
|
||||
const c = this.contentEl;
|
||||
c.empty();
|
||||
c.createEl("h3", { text: "Publishing..." });
|
||||
const status = c.createEl("p", { text: "Creating repository..." });
|
||||
|
||||
try {
|
||||
const gh = new GitHubAPI(token);
|
||||
const user = await gh.getUser();
|
||||
|
||||
const slug = this.name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-|-$/g, "");
|
||||
const repoName = `obsidian-${this.resourceType}-${slug}`;
|
||||
|
||||
status.setText("Creating repository...");
|
||||
const repo = await gh.createRepo(repoName, this.tagline || this.name);
|
||||
const [owner, rName] = repo.full_name.split("/");
|
||||
|
||||
// Topic tag
|
||||
const topicMap: Record<string, string> = {
|
||||
snippet: "obsidian-css-snippet",
|
||||
note: "obsidian-note-template",
|
||||
bundle: "obsidian-note-template",
|
||||
};
|
||||
status.setText("Adding topic tag...");
|
||||
await gh.addTopics(owner, rName, [topicMap[this.resourceType]]);
|
||||
|
||||
// Upload resource files
|
||||
for (const file of this.selectedFiles) {
|
||||
status.setText(`Uploading ${file.name}...`);
|
||||
const content = await this.app.vault.read(file);
|
||||
await gh.createFile(owner, rName, file.name, content, `Add ${file.name}`);
|
||||
}
|
||||
|
||||
// Generate and upload hub.yml
|
||||
status.setText("Generating hub.yml...");
|
||||
const selectedPlugins = this.allPlugins
|
||||
.filter((p) => this.checkedPlugins.has(p.id))
|
||||
.map((p) => ({ ...p, autoDetected: true }));
|
||||
|
||||
const obsVer = (this.app as unknown as { appVersion?: string }).appVersion || "unknown";
|
||||
const themeName = ((this.app.vault as unknown as { config?: { cssTheme?: string } }).config?.cssTheme) || "default";
|
||||
|
||||
const hubData: HubYmlData = {
|
||||
type: this.resourceType,
|
||||
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: [],
|
||||
plugins: selectedPlugins,
|
||||
obsidianVersion: obsVer,
|
||||
theme: themeName,
|
||||
os: navigator.platform,
|
||||
files: this.selectedFiles.map((f) => ({
|
||||
path: f.name,
|
||||
type: f.extension,
|
||||
size: f.stat.size,
|
||||
})),
|
||||
};
|
||||
|
||||
const hubYml = generateHubYml(hubData);
|
||||
await gh.createFile(owner, rName, "hub.yml", hubYml, "Add hub.yml");
|
||||
|
||||
// Upload README
|
||||
status.setText("Uploading README...");
|
||||
await gh.createFile(owner, rName, "README.md", this.readmeContent, "Add README");
|
||||
|
||||
// Save to published resources
|
||||
this.plugin.settings.publishedResources.push({
|
||||
repoFullName: repo.full_name,
|
||||
localFilePath: this.selectedFiles[0].path,
|
||||
type: this.resourceType,
|
||||
lastPublishedAt: new Date().toISOString(),
|
||||
});
|
||||
await this.plugin.saveSettings();
|
||||
|
||||
// Success
|
||||
c.empty();
|
||||
c.createEl("h3", { text: "Published!" });
|
||||
c.createEl("p", { text: `Repository: ${repo.full_name}` });
|
||||
const link = c.createEl("a", {
|
||||
text: repo.html_url,
|
||||
href: repo.html_url,
|
||||
});
|
||||
link.setAttr("target", "_blank");
|
||||
c.createEl("p", {
|
||||
text: "It will appear on Vault Hub within 6 hours.",
|
||||
cls: "vault-hub-hint",
|
||||
});
|
||||
|
||||
const closeBtn = c.createEl("button", { text: "Close", cls: "mod-cta" });
|
||||
closeBtn.addEventListener("click", () => this.close());
|
||||
|
||||
new 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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private addNav(
|
||||
container: HTMLElement,
|
||||
backCheck: (() => boolean) | null,
|
||||
nextCheck: (() => boolean) | null
|
||||
) {
|
||||
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;
|
||||
this.step--;
|
||||
this.renderStep();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.step < 5) {
|
||||
const next = nav.createEl("button", { text: "Next", cls: "mod-cta" });
|
||||
next.addEventListener("click", () => {
|
||||
if (nextCheck && !nextCheck()) return;
|
||||
this.step++;
|
||||
this.renderStep();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
107
src/modals/UpdateModal.ts
Normal file
107
src/modals/UpdateModal.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import { App, Modal, Setting, Notice } from "obsidian";
|
||||
import type VaultHubPlugin from "../main";
|
||||
import { PublishedResource } from "../settings";
|
||||
import { GitHubAPI } from "../github";
|
||||
|
||||
export class UpdateModal extends Modal {
|
||||
plugin: VaultHubPlugin;
|
||||
selected: PublishedResource | null = null;
|
||||
|
||||
constructor(app: App, plugin: VaultHubPlugin) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
this.render();
|
||||
}
|
||||
|
||||
onClose() {
|
||||
this.contentEl.empty();
|
||||
}
|
||||
|
||||
private render() {
|
||||
const c = this.contentEl;
|
||||
c.empty();
|
||||
c.createEl("h2", { text: "Update Published Resource" });
|
||||
|
||||
const resources = this.plugin.settings.publishedResources;
|
||||
if (resources.length === 0) {
|
||||
c.createEl("p", { text: "No published resources yet. Use Publish first." });
|
||||
return;
|
||||
}
|
||||
|
||||
new Setting(c).setName("Select Resource").addDropdown((dd) => {
|
||||
dd.addOption("", "Choose...");
|
||||
resources.forEach((r, i) => {
|
||||
dd.addOption(String(i), `${r.repoFullName} (${r.type})`);
|
||||
});
|
||||
dd.onChange((v) => {
|
||||
this.selected = v ? resources[parseInt(v)] : null;
|
||||
});
|
||||
});
|
||||
|
||||
const updateBtn = c.createEl("button", { text: "Push Update", cls: "mod-cta" });
|
||||
updateBtn.addEventListener("click", () => this.doUpdate());
|
||||
}
|
||||
|
||||
private async doUpdate() {
|
||||
if (!this.selected) {
|
||||
new Notice("Select a resource first");
|
||||
return;
|
||||
}
|
||||
|
||||
const token = this.plugin.settings.githubToken;
|
||||
if (!token) {
|
||||
new Notice("Set your GitHub token in settings first");
|
||||
return;
|
||||
}
|
||||
|
||||
const c = this.contentEl;
|
||||
c.empty();
|
||||
c.createEl("h3", { text: "Updating..." });
|
||||
|
||||
try {
|
||||
const gh = new GitHubAPI(token);
|
||||
const [owner, repo] = this.selected.repoFullName.split("/");
|
||||
|
||||
// Read current local file
|
||||
const file = this.app.vault.getAbstractFileByPath(this.selected.localFilePath);
|
||||
if (!file) {
|
||||
new Notice(`File not found: ${this.selected.localFilePath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const content = await this.app.vault.read(file as import("obsidian").TFile);
|
||||
|
||||
// Get existing file SHA from GitHub
|
||||
const existing = await gh.getFileContent(owner, repo, file.name);
|
||||
|
||||
if (existing) {
|
||||
await gh.updateFile(
|
||||
owner, repo, file.name, content,
|
||||
`Update ${file.name}`, existing.sha
|
||||
);
|
||||
} else {
|
||||
await gh.createFile(owner, repo, file.name, content, `Add ${file.name}`);
|
||||
}
|
||||
|
||||
// Update timestamp
|
||||
this.selected.lastPublishedAt = new Date().toISOString();
|
||||
await this.plugin.saveSettings();
|
||||
|
||||
c.empty();
|
||||
c.createEl("h3", { text: "Updated!" });
|
||||
c.createEl("p", { text: `Pushed to ${this.selected.repoFullName}` });
|
||||
|
||||
const closeBtn = c.createEl("button", { text: "Close", cls: "mod-cta" });
|
||||
closeBtn.addEventListener("click", () => this.close());
|
||||
|
||||
new Notice("Resource updated!");
|
||||
} catch (e) {
|
||||
c.empty();
|
||||
c.createEl("h3", { text: "Error" });
|
||||
c.createEl("p", { text: String(e) });
|
||||
}
|
||||
}
|
||||
}
|
||||
63
src/readme.ts
Normal file
63
src/readme.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import { DetectedPlugin } from "./detection";
|
||||
|
||||
export interface ReadmeData {
|
||||
name: string;
|
||||
tagline: string;
|
||||
description: string;
|
||||
type: "snippet" | "note" | "bundle";
|
||||
plugins: DetectedPlugin[];
|
||||
files: { path: string }[];
|
||||
}
|
||||
|
||||
export function generateReadme(data: ReadmeData): string {
|
||||
const lines: string[] = [];
|
||||
|
||||
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);
|
||||
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("");
|
||||
}
|
||||
|
||||
lines.push("## Installation");
|
||||
lines.push("");
|
||||
|
||||
if (data.type === "snippet") {
|
||||
lines.push(
|
||||
`1. Download \`${data.files[0]?.path || "snippet.css"}\` from this repo`
|
||||
);
|
||||
lines.push(
|
||||
"2. Place it in your vault's `.obsidian/snippets/` folder"
|
||||
);
|
||||
lines.push("3. Enable it in Settings > Appearance > CSS Snippets");
|
||||
} else {
|
||||
lines.push("1. Download the `.md` file(s) from this repo");
|
||||
lines.push("2. Place them in your vault");
|
||||
if (selected.length > 0) {
|
||||
lines.push("3. Install the required plugins listed above");
|
||||
}
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
lines.push("---");
|
||||
lines.push("*Published via [Vault Hub](https://vaulthub.dev)*");
|
||||
lines.push("");
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
85
src/settings.ts
Normal file
85
src/settings.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import { App, PluginSettingTab, Setting } from "obsidian";
|
||||
import type VaultHubPlugin from "./main";
|
||||
|
||||
export interface PublishedResource {
|
||||
repoFullName: string;
|
||||
localFilePath: string;
|
||||
type: "snippet" | "note" | "bundle";
|
||||
lastPublishedAt: string;
|
||||
}
|
||||
|
||||
export interface VaultHubSettings {
|
||||
githubToken: string;
|
||||
defaultCategories: string[];
|
||||
vaultHubUrl: string;
|
||||
publishedResources: PublishedResource[];
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: VaultHubSettings = {
|
||||
githubToken: "",
|
||||
defaultCategories: [],
|
||||
vaultHubUrl: "http://localhost:3000",
|
||||
publishedResources: [],
|
||||
};
|
||||
|
||||
export class VaultHubSettingTab extends PluginSettingTab {
|
||||
plugin: VaultHubPlugin;
|
||||
|
||||
constructor(app: App, plugin: VaultHubPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
|
||||
containerEl.createEl("h2", { text: "Vault Hub Settings" });
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("GitHub Personal Access Token")
|
||||
.setDesc("Token used to create repos and push files. Requires 'repo' scope.")
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("ghp_xxxxxxxxxxxx")
|
||||
.setValue(this.plugin.settings.githubToken)
|
||||
.then((t) => {
|
||||
t.inputEl.type = "password";
|
||||
t.inputEl.style.width = "300px";
|
||||
})
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.githubToken = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Vault Hub URL")
|
||||
.setDesc("URL of the Vault Hub website (default: http://localhost:3000 for development).")
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("http://localhost:3000")
|
||||
.setValue(this.plugin.settings.vaultHubUrl)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.vaultHubUrl = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Default Categories")
|
||||
.setDesc("Comma-separated list of default categories for new publications.")
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("appearance, workflow")
|
||||
.setValue(this.plugin.settings.defaultCategories.join(", "))
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.defaultCategories = value
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s.length > 0);
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
193
src/views/BrowseView.ts
Normal file
193
src/views/BrowseView.ts
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
import { ItemView, WorkspaceLeaf, Notice, TFile } from "obsidian";
|
||||
import type VaultHubPlugin from "../main";
|
||||
|
||||
export const VIEW_TYPE_BROWSE = "vault-hub-browse";
|
||||
|
||||
const SUPABASE_URL = "https://oxvxiqiushhpwzqtpzdg.supabase.co";
|
||||
const SUPABASE_KEY =
|
||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im94dnhpcWl1c2hocHd6cXRwemRnIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzU0MTI4NjAsImV4cCI6MjA5MDk4ODg2MH0.jOJloJvJmV2Q-xuY1wBbUjg0s7DnRGt6htycj4HBtEA";
|
||||
|
||||
interface ResourceSummary {
|
||||
id: string;
|
||||
type: string;
|
||||
title: string;
|
||||
owner: string;
|
||||
repo_name: string;
|
||||
full_name: string;
|
||||
tagline: string | null;
|
||||
stars: number;
|
||||
vote_count: number;
|
||||
}
|
||||
|
||||
export class BrowseView extends ItemView {
|
||||
plugin: VaultHubPlugin;
|
||||
searchQuery = "";
|
||||
resources: ResourceSummary[] = [];
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, plugin: VaultHubPlugin) {
|
||||
super(leaf);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
getViewType(): string {
|
||||
return VIEW_TYPE_BROWSE;
|
||||
}
|
||||
|
||||
getDisplayText(): string {
|
||||
return "Vault Hub";
|
||||
}
|
||||
|
||||
getIcon(): string {
|
||||
return "globe";
|
||||
}
|
||||
|
||||
async onOpen() {
|
||||
this.render();
|
||||
await this.loadResources();
|
||||
}
|
||||
|
||||
async onClose() {
|
||||
this.contentEl.empty();
|
||||
}
|
||||
|
||||
private render() {
|
||||
const c = this.contentEl;
|
||||
c.empty();
|
||||
c.addClass("vault-hub-browse");
|
||||
|
||||
const header = c.createDiv("vault-hub-browse-header");
|
||||
header.createEl("h3", { text: "Vault Hub" });
|
||||
|
||||
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: KeyboardEvent) => {
|
||||
if (e.key === "Enter") this.loadResources();
|
||||
});
|
||||
|
||||
const searchBtn = searchRow.createEl("button", { text: "Search" });
|
||||
searchBtn.addEventListener("click", () => this.loadResources());
|
||||
|
||||
c.createDiv("vault-hub-results");
|
||||
}
|
||||
|
||||
private 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 {
|
||||
let url = `${SUPABASE_URL}/rest/v1/resources?select=id,type,title,owner,repo_name,full_name,tagline,stars,vote_count&is_active=eq.true&order=trending_score.desc&limit=30`;
|
||||
|
||||
if (this.searchQuery.trim()) {
|
||||
url += `&or=(title.ilike.*${encodeURIComponent(this.searchQuery)}*,description.ilike.*${encodeURIComponent(this.searchQuery)}*)`;
|
||||
}
|
||||
|
||||
const res = await fetch(url, {
|
||||
headers: {
|
||||
apikey: SUPABASE_KEY,
|
||||
Authorization: `Bearer ${SUPABASE_KEY}`,
|
||||
},
|
||||
});
|
||||
|
||||
this.resources = await res.json();
|
||||
this.renderResults(resultsEl as HTMLElement);
|
||||
} catch (e) {
|
||||
resultsEl.empty();
|
||||
resultsEl.createEl("p", { text: `Error: ${e}` });
|
||||
}
|
||||
}
|
||||
|
||||
private renderResults(container: HTMLElement) {
|
||||
container.empty();
|
||||
|
||||
if (this.resources.length === 0) {
|
||||
container.createEl("p", {
|
||||
text: "No resources found.",
|
||||
cls: "vault-hub-hint",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.resources.forEach((r) => {
|
||||
const card = container.createDiv("vault-hub-result-card");
|
||||
|
||||
const typeColors: Record<string, string> = {
|
||||
vault: "#7f6df2",
|
||||
snippet: "#22d3ee",
|
||||
note: "#fb923c",
|
||||
};
|
||||
|
||||
const badge = card.createSpan("vault-hub-type-badge");
|
||||
badge.setText(r.type);
|
||||
badge.style.backgroundColor = typeColors[r.type] || "#666";
|
||||
|
||||
const title = card.createEl("h4");
|
||||
title.setText(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.vote_count} votes` });
|
||||
meta.createSpan({ text: r.owner });
|
||||
|
||||
const actions = card.createDiv("vault-hub-result-actions");
|
||||
|
||||
const installBtn = actions.createEl("button", { text: "Install" });
|
||||
installBtn.addEventListener("click", () => this.installResource(r));
|
||||
|
||||
const ghBtn = actions.createEl("button", { text: "GitHub" });
|
||||
ghBtn.addEventListener("click", () => {
|
||||
window.open(`https://github.com/${r.full_name}`, "_blank");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async installResource(r: ResourceSummary) {
|
||||
try {
|
||||
// Fetch file list from GitHub
|
||||
const res = await fetch(
|
||||
`https://api.github.com/repos/${r.full_name}/contents`,
|
||||
{ headers: { Accept: "application/vnd.github.v3+json" } }
|
||||
);
|
||||
const files = await res.json();
|
||||
|
||||
if (r.type === "snippet") {
|
||||
const cssFile = files.find(
|
||||
(f: { name: string }) => f.name.endsWith(".css")
|
||||
);
|
||||
if (cssFile) {
|
||||
const raw = await fetch(cssFile.download_url);
|
||||
const content = await raw.text();
|
||||
const snippetPath = `${this.app.vault.configDir}/snippets/${cssFile.name}`;
|
||||
await this.app.vault.adapter.write(snippetPath, content);
|
||||
new Notice(`Installed snippet: ${cssFile.name}`);
|
||||
}
|
||||
} else {
|
||||
const mdFiles = files.filter(
|
||||
(f: { name: string }) =>
|
||||
f.name.endsWith(".md") && f.name !== "README.md"
|
||||
);
|
||||
for (const f of mdFiles) {
|
||||
const raw = await fetch(f.download_url);
|
||||
const content = await raw.text();
|
||||
await this.app.vault.create(f.name, content);
|
||||
new Notice(`Installed: ${f.name}`);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
new Notice(`Install failed: ${e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
230
styles.css
Normal file
230
styles.css
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
/* Vault Hub Plugin Styles */
|
||||
|
||||
/* Browse View */
|
||||
.vault-hub-browse {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.vault-hub-browse-header h3 {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.vault-hub-search-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.vault-hub-search-row input[type="text"] {
|
||||
flex: 1;
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
background: var(--background-primary);
|
||||
color: var(--text-normal);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.vault-hub-search-row button {
|
||||
padding: 6px 14px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.vault-hub-hint {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
/* Result Cards */
|
||||
.vault-hub-result-card {
|
||||
padding: 12px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 8px;
|
||||
background: var(--background-secondary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.vault-hub-result-card h4 {
|
||||
margin: 6px 0 4px 0;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.vault-hub-type-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75em;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.vault-hub-result-meta {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 6px;
|
||||
font-size: 0.8em;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.vault-hub-result-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.vault-hub-result-actions button {
|
||||
padding: 4px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Modal Styles */
|
||||
.vault-hub-modal {
|
||||
min-width: 480px;
|
||||
}
|
||||
|
||||
.vault-hub-header {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.vault-hub-header h2 {
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
/* Progress Dots */
|
||||
.vault-hub-progress {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.vault-hub-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: var(--background-modifier-border);
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.vault-hub-dot.active {
|
||||
background: var(--interactive-accent);
|
||||
box-shadow: 0 0 6px var(--interactive-accent);
|
||||
}
|
||||
|
||||
.vault-hub-dot.done {
|
||||
background: var(--text-success);
|
||||
}
|
||||
|
||||
/* File List */
|
||||
.vault-hub-file-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 6px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.vault-hub-file-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.vault-hub-file-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.vault-hub-file-row:hover {
|
||||
background: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
/* Plugin List */
|
||||
.vault-hub-plugin-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.vault-hub-plugin-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 10px;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.vault-hub-plugin-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.vault-hub-plugin-info {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.vault-hub-plugin-name {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.vault-hub-plugin-version {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.vault-hub-auto-badge {
|
||||
font-size: 0.75em;
|
||||
color: var(--text-success);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* README Editor */
|
||||
.vault-hub-readme-editor {
|
||||
width: 100%;
|
||||
min-height: 300px;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
background: var(--background-primary);
|
||||
color: var(--text-normal);
|
||||
font-family: var(--font-monospace);
|
||||
font-size: 0.85em;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/* Summary */
|
||||
.vault-hub-summary {
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
background: var(--background-secondary);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.vault-hub-summary p {
|
||||
margin: 4px 0;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
/* Navigation */
|
||||
.vault-hub-nav {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.vault-hub-nav button {
|
||||
padding: 6px 18px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
22
tsconfig.json
Normal file
22
tsconfig.json
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"inlineSourceMap": true,
|
||||
"inlineSources": true,
|
||||
"module": "ESNext",
|
||||
"target": "ES6",
|
||||
"allowJs": true,
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"isolatedModules": true,
|
||||
"strictNullChecks": true,
|
||||
"lib": ["DOM", "ES5", "ES6", "ES7", "ES2015", "ES2020"],
|
||||
"outDir": "./dist",
|
||||
"sourceMap": false,
|
||||
"paths": {
|
||||
"obsidian": ["node_modules/obsidian"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
Loading…
Reference in a new issue