mirror of
https://github.com/maws7140/vault-hub-plugin.git
synced 2026-07-22 06:49:30 +00:00
Prep Vault Hub for community plugin submission
This commit is contained in:
parent
4a825a59fc
commit
026339d687
14 changed files with 363 additions and 186 deletions
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2026 Maws7140
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
71
README.md
71
README.md
|
|
@ -1,2 +1,69 @@
|
|||
# vault-hub-plugin
|
||||
Obsidian companion plugin for Vault Hub: publish snippets, notes, and templates to GitHub with one click
|
||||
# Vault Hub
|
||||
|
||||
Vault Hub is an Obsidian plugin for publishing and installing Vault Hub resources.
|
||||
|
||||
It covers three workflows:
|
||||
|
||||
- publish notes, snippets, and full vaults to GitHub
|
||||
- browse the Vault Hub catalog from inside Obsidian
|
||||
- install note packs and CSS snippets directly into your vault
|
||||
|
||||
## What it does
|
||||
|
||||
- Publish a CSS snippet, a note, or a full vault as a GitHub repo
|
||||
- Generate `hub.md` metadata for Vault Hub listings
|
||||
- Generate and update a `README.md` for the published resource
|
||||
- Detect required community plugins from selected files
|
||||
- Attach CSS snippets to note and vault publishes
|
||||
- Upload local screenshots or reference external screenshot URLs
|
||||
- Browse the live Vault Hub catalog and install resources from it
|
||||
- Manage installed CSS snippets from the plugin sidebar
|
||||
|
||||
## Install
|
||||
|
||||
### Manual install
|
||||
|
||||
1. Download the latest release assets:
|
||||
- `main.js`
|
||||
- `manifest.json`
|
||||
- `styles.css`
|
||||
2. Create a folder named `vault-hub` inside your vault's plugins directory:
|
||||
- `<your-vault>/<configDir>/plugins/vault-hub`
|
||||
3. Place the release files in that folder.
|
||||
4. Reload Obsidian and enable **Vault Hub** in Community plugins.
|
||||
|
||||
## Setup
|
||||
|
||||
1. Open **Settings → Community plugins → Vault Hub**.
|
||||
2. Paste a GitHub personal access token with repo access.
|
||||
3. Confirm the Vault Hub site URL if you are using a custom deployment.
|
||||
4. Optionally set default categories for new publishes.
|
||||
|
||||
## Publish flow
|
||||
|
||||
Use one of the plugin commands:
|
||||
|
||||
- `Vault Hub: Publish resource`
|
||||
- `Vault Hub: Update resource`
|
||||
- `Vault Hub: Browse resources`
|
||||
|
||||
Publishing creates or updates a GitHub repository under your account and writes the files needed for Vault Hub indexing.
|
||||
|
||||
## Notes
|
||||
|
||||
- This plugin is desktop-only.
|
||||
- Vault Hub install flows write files into your vault and into your vault configuration directory for CSS snippets.
|
||||
- Publishing and updating require a GitHub account and a personal access token.
|
||||
|
||||
## Disclosures
|
||||
|
||||
- **Account requirement:** GitHub account and personal access token required for publish and update flows.
|
||||
- **Network use:** Calls GitHub APIs, raw GitHub content URLs, and the configured Vault Hub website.
|
||||
- **External file access:** Reads and writes files inside the current vault and its configuration directory.
|
||||
- **Telemetry:** None.
|
||||
- **Ads:** None.
|
||||
- **Payments:** None.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
|
|
|||
252
main.js
252
main.js
|
|
@ -27,7 +27,7 @@ __export(main_exports, {
|
|||
default: () => VaultHubPlugin
|
||||
});
|
||||
module.exports = __toCommonJS(main_exports);
|
||||
var import_obsidian5 = require("obsidian");
|
||||
var import_obsidian7 = require("obsidian");
|
||||
|
||||
// src/settings.ts
|
||||
var import_obsidian = require("obsidian");
|
||||
|
|
@ -47,8 +47,8 @@ var VaultHubSettingTab = class extends import_obsidian.PluginSettingTab {
|
|||
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(
|
||||
new import_obsidian.Setting(containerEl).setName("Vault Hub").setHeading();
|
||||
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("ghp_xxxxxxxxxxxx").setValue(this.plugin.settings.githubToken).then((t) => {
|
||||
t.inputEl.type = "password";
|
||||
t.inputEl.style.width = "300px";
|
||||
|
|
@ -63,13 +63,13 @@ var VaultHubSettingTab = class extends import_obsidian.PluginSettingTab {
|
|||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
new import_obsidian.Setting(containerEl).setName("Catalog Repository").setDesc("Repo that stores the website catalog workflow. Used to request a refresh after publish/update.").addText(
|
||||
new import_obsidian.Setting(containerEl).setName("Catalog repository").setDesc("Repository that stores the website catalog workflow. Used to request a refresh after publish or update.").addText(
|
||||
(text) => text.setPlaceholder("Maws7140/vault-hub").setValue(this.plugin.settings.catalogRepoFullName).onChange(async (value) => {
|
||||
this.plugin.settings.catalogRepoFullName = value.trim();
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
new import_obsidian.Setting(containerEl).setName("Default Categories").setDesc("Comma-separated list of default categories for new publications.").addText(
|
||||
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();
|
||||
|
|
@ -79,7 +79,7 @@ var VaultHubSettingTab = class extends import_obsidian.PluginSettingTab {
|
|||
};
|
||||
|
||||
// src/modals/PublishModal.ts
|
||||
var import_obsidian2 = require("obsidian");
|
||||
var import_obsidian4 = require("obsidian");
|
||||
|
||||
// src/detection.ts
|
||||
var MD_PATTERNS = [
|
||||
|
|
@ -145,26 +145,63 @@ async function detectPlugins(fileContent, fileType, vault) {
|
|||
}));
|
||||
}
|
||||
|
||||
// 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
|
||||
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
|
||||
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) {
|
||||
throw new Error(`GitHub API ${httpMatch[1]}: ${httpMatch[2]}`);
|
||||
}
|
||||
});
|
||||
if (!res.ok) {
|
||||
const body = await res.text();
|
||||
throw new Error(`GitHub API ${res.status}: ${body}`);
|
||||
throw error;
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
encodePath(path) {
|
||||
return path.split("/").map(encodeURIComponent).join("/");
|
||||
|
|
@ -374,6 +411,12 @@ 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;
|
||||
|
|
@ -426,15 +469,13 @@ function generateReadme(data) {
|
|||
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("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 `.obsidian/snippets/`");
|
||||
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");
|
||||
|
|
@ -446,7 +487,7 @@ function generateReadme(data) {
|
|||
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 `.obsidian/snippets/`");
|
||||
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");
|
||||
|
|
@ -514,7 +555,7 @@ function tfileToPublishFile(app, f) {
|
|||
}
|
||||
async function listSnippetFiles(app) {
|
||||
const adapter = app.vault.adapter;
|
||||
const dir = ".obsidian/snippets";
|
||||
const dir = getSnippetDirectory(app.vault);
|
||||
try {
|
||||
const exists = await adapter.exists(dir);
|
||||
if (!exists)
|
||||
|
|
@ -603,7 +644,7 @@ var CATEGORIES = {
|
|||
"finance"
|
||||
]
|
||||
};
|
||||
var PublishModal = class extends import_obsidian2.Modal {
|
||||
var PublishModal = class extends import_obsidian4.Modal {
|
||||
constructor(app, plugin) {
|
||||
super(app);
|
||||
this.step = 1;
|
||||
|
|
@ -635,6 +676,9 @@ var PublishModal = class extends import_obsidian2.Modal {
|
|||
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" });
|
||||
|
|
@ -785,7 +829,7 @@ var PublishModal = class extends import_obsidian2.Modal {
|
|||
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` });
|
||||
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");
|
||||
|
|
@ -826,7 +870,7 @@ var PublishModal = class extends import_obsidian2.Modal {
|
|||
}
|
||||
async renderStep1() {
|
||||
const c = this.contentEl;
|
||||
new import_obsidian2.Setting(c).setName("Resource Type").addDropdown((dd) => {
|
||||
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");
|
||||
|
|
@ -873,7 +917,7 @@ var PublishModal = class extends import_obsidian2.Modal {
|
|||
});
|
||||
if (this.resourceType === "snippet") {
|
||||
fileSection.createEl("p", {
|
||||
text: "Sourced from .obsidian/snippets. Drop .css files there if nothing shows up.",
|
||||
text: `Sourced from ${this.getSnippetDir()}. Drop .css files there if nothing shows up.`,
|
||||
cls: "vault-hub-hint"
|
||||
});
|
||||
} else {
|
||||
|
|
@ -884,7 +928,7 @@ var PublishModal = class extends import_obsidian2.Modal {
|
|||
}
|
||||
if (files.length === 0) {
|
||||
fileSection.createEl("p", {
|
||||
text: this.resourceType === "snippet" ? "No CSS snippets found in .obsidian/snippets." : this.resourceType === "bundle" ? "No markdown files found in this vault." : "No markdown files found in this vault.",
|
||||
text: this.resourceType === "snippet" ? `No CSS snippets found in ${this.getSnippetDir()}.` : this.resourceType === "bundle" ? "No markdown files found in this vault." : "No markdown files found in this vault.",
|
||||
cls: "vault-hub-hint"
|
||||
});
|
||||
}
|
||||
|
|
@ -966,7 +1010,7 @@ var PublishModal = class extends import_obsidian2.Modal {
|
|||
attachedSnippetSearch.value = this.attachedSnippetSearchQuery;
|
||||
if (availableSnippets.length === 0) {
|
||||
snippetSection.createEl("p", {
|
||||
text: "No CSS snippets found in .obsidian/snippets.",
|
||||
text: `No CSS snippets found in ${this.getSnippetDir()}.`,
|
||||
cls: "vault-hub-hint"
|
||||
});
|
||||
} else {
|
||||
|
|
@ -1003,7 +1047,7 @@ var PublishModal = class extends import_obsidian2.Modal {
|
|||
}
|
||||
this.addNav(c, null, () => {
|
||||
if (this.selectedFiles.length === 0) {
|
||||
new import_obsidian2.Notice("Select at least one file");
|
||||
new import_obsidian4.Notice("Select at least one file");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
|
@ -1018,7 +1062,7 @@ var PublishModal = class extends import_obsidian2.Modal {
|
|||
}
|
||||
async renderStep2() {
|
||||
const c = this.contentEl;
|
||||
c.createEl("h4", { text: "Select Required Plugins" });
|
||||
c.createEl("h4", { text: "Select required plugins" });
|
||||
c.createEl("p", {
|
||||
text: "Auto-detected plugins are pre-checked. Review and adjust.",
|
||||
cls: "vault-hub-hint"
|
||||
|
|
@ -1107,7 +1151,7 @@ var PublishModal = class extends import_obsidian2.Modal {
|
|||
snippetSearch.value = this.attachedSnippetSearchQuery;
|
||||
if (availableSnippets.length === 0) {
|
||||
snippetSection.createEl("p", {
|
||||
text: "No CSS snippets found in .obsidian/snippets.",
|
||||
text: `No CSS snippets found in ${this.getSnippetDir()}.`,
|
||||
cls: "vault-hub-hint"
|
||||
});
|
||||
} else {
|
||||
|
|
@ -1178,24 +1222,24 @@ var PublishModal = class extends import_obsidian2.Modal {
|
|||
}
|
||||
renderStep3() {
|
||||
const c = this.contentEl;
|
||||
new import_obsidian2.Setting(c).setName("Name").addText((t) => {
|
||||
new import_obsidian4.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) => {
|
||||
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.style.width = "100%";
|
||||
});
|
||||
new import_obsidian2.Setting(c).setName("Description").addTextArea((t) => {
|
||||
new import_obsidian4.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] || [];
|
||||
const categorySetting = new import_obsidian2.Setting(c).setName("Categories").setDesc("Pick one or more.");
|
||||
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" });
|
||||
|
|
@ -1233,7 +1277,7 @@ var PublishModal = class extends import_obsidian2.Modal {
|
|||
});
|
||||
};
|
||||
renderCategoryList();
|
||||
new import_obsidian2.Setting(c).setName("Tags").setDesc("Comma-separated").addText((t) => {
|
||||
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);
|
||||
});
|
||||
|
|
@ -1243,7 +1287,7 @@ var PublishModal = class extends import_obsidian2.Modal {
|
|||
text: "Optional. Use local image files, external image URLs, or both.",
|
||||
cls: "vault-hub-hint"
|
||||
});
|
||||
new import_obsidian2.Setting(screenshotSection).setName("External screenshot URLs").setDesc("One per line. Direct image URLs work best.").addTextArea((t) => {
|
||||
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.style.width = "100%";
|
||||
|
|
@ -1298,7 +1342,7 @@ var PublishModal = class extends import_obsidian2.Modal {
|
|||
});
|
||||
void renderScreenshotList();
|
||||
if (this.resourceType === "snippet") {
|
||||
new import_obsidian2.Setting(c).setName("Compatible Themes").addDropdown((dd) => {
|
||||
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)
|
||||
|
|
@ -1309,7 +1353,7 @@ var PublishModal = class extends import_obsidian2.Modal {
|
|||
}
|
||||
this.addNav(c, null, () => {
|
||||
if (!this.name.trim()) {
|
||||
new import_obsidian2.Notice("Name is required");
|
||||
new import_obsidian4.Notice("Name is required");
|
||||
return false;
|
||||
}
|
||||
this.readmeContent = generateReadme(this.buildReadmeData());
|
||||
|
|
@ -1376,7 +1420,7 @@ var PublishModal = class extends import_obsidian2.Modal {
|
|||
var _a, _b;
|
||||
const token = this.plugin.settings.githubToken;
|
||||
if (!token) {
|
||||
new import_obsidian2.Notice("Set your GitHub token in Vault Hub settings first");
|
||||
new import_obsidian4.Notice("Set your GitHub token in Vault Hub settings first");
|
||||
return;
|
||||
}
|
||||
const c = this.contentEl;
|
||||
|
|
@ -1527,7 +1571,7 @@ var PublishModal = class extends import_obsidian2.Modal {
|
|||
ghLink.setAttr("target", "_blank");
|
||||
const closeBtn = actions.createEl("button", { text: "Close", cls: "mod-cta" });
|
||||
closeBtn.addEventListener("click", () => this.close());
|
||||
new import_obsidian2.Notice(`Published to ${repo.full_name}!`);
|
||||
new import_obsidian4.Notice(`Published to ${repo.full_name}!`);
|
||||
} catch (e) {
|
||||
c.empty();
|
||||
c.createEl("h3", { text: "Error" });
|
||||
|
|
@ -1669,8 +1713,8 @@ var PublishModal = class extends import_obsidian2.Modal {
|
|||
};
|
||||
|
||||
// src/modals/UpdateModal.ts
|
||||
var import_obsidian3 = require("obsidian");
|
||||
var UpdateModal = class extends import_obsidian3.Modal {
|
||||
var import_obsidian5 = require("obsidian");
|
||||
var UpdateModal = class extends import_obsidian5.Modal {
|
||||
constructor(app, plugin) {
|
||||
super(app);
|
||||
this.selected = null;
|
||||
|
|
@ -1690,7 +1734,7 @@ var UpdateModal = class extends import_obsidian3.Modal {
|
|||
const c = this.contentEl;
|
||||
c.empty();
|
||||
c.addClass("vault-hub-modal");
|
||||
c.createEl("h2", { text: "Update Resource" });
|
||||
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();
|
||||
|
|
@ -1701,7 +1745,7 @@ var UpdateModal = class extends import_obsidian3.Modal {
|
|||
});
|
||||
return;
|
||||
}
|
||||
new import_obsidian3.Setting(c).setName("Resource").addDropdown((dd) => {
|
||||
new import_obsidian5.Setting(c).setName("Resource").addDropdown((dd) => {
|
||||
dd.addOption("", "Select...");
|
||||
resources.forEach((r, i) => {
|
||||
const suffix = this.hasLocalMappings(r) ? "" : " \u2014 GitHub only";
|
||||
|
|
@ -1715,11 +1759,11 @@ var UpdateModal = class extends import_obsidian3.Modal {
|
|||
btn.style.marginTop = "12px";
|
||||
btn.addEventListener("click", async () => {
|
||||
if (!this.selected) {
|
||||
new import_obsidian3.Notice("Select a resource first");
|
||||
new import_obsidian5.Notice("Select a resource first");
|
||||
return;
|
||||
}
|
||||
if (!this.plugin.settings.githubToken) {
|
||||
new import_obsidian3.Notice("Set your GitHub token in settings");
|
||||
new import_obsidian5.Notice("Set your GitHub token in settings");
|
||||
return;
|
||||
}
|
||||
await this.loadDiff(btn);
|
||||
|
|
@ -1757,7 +1801,7 @@ var UpdateModal = class extends import_obsidian3.Modal {
|
|||
}
|
||||
this.renderDiff();
|
||||
} catch (e) {
|
||||
new import_obsidian3.Notice(`Error: ${e}`);
|
||||
new import_obsidian5.Notice(`Error: ${e}`);
|
||||
triggerBtn.disabled = false;
|
||||
triggerBtn.setText("Check for Changes");
|
||||
}
|
||||
|
|
@ -1765,7 +1809,7 @@ var UpdateModal = class extends import_obsidian3.Modal {
|
|||
renderDiff() {
|
||||
const c = this.contentEl;
|
||||
c.empty();
|
||||
c.createEl("h2", { text: "Review Changes" });
|
||||
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");
|
||||
|
|
@ -1860,7 +1904,7 @@ var UpdateModal = class extends import_obsidian3.Modal {
|
|||
const closeBtn = c.createEl("button", { text: "Close" });
|
||||
closeBtn.style.marginTop = "12px";
|
||||
closeBtn.addEventListener("click", () => this.close());
|
||||
new import_obsidian3.Notice(`Pushed ${pushed} file(s)!`);
|
||||
new import_obsidian5.Notice(`Pushed ${pushed} file(s)!`);
|
||||
} catch (e) {
|
||||
c.empty();
|
||||
c.createEl("h3", { text: "Error" });
|
||||
|
|
@ -1930,7 +1974,7 @@ var UpdateModal = class extends import_obsidian3.Modal {
|
|||
}
|
||||
async readLocalContent(path) {
|
||||
const tfile = this.app.vault.getAbstractFileByPath(path);
|
||||
if (tfile instanceof import_obsidian3.TFile) {
|
||||
if (tfile instanceof import_obsidian5.TFile) {
|
||||
return this.app.vault.read(tfile);
|
||||
}
|
||||
try {
|
||||
|
|
@ -1968,7 +2012,7 @@ function newerDate(a, b) {
|
|||
}
|
||||
|
||||
// src/views/BrowseView.ts
|
||||
var import_obsidian4 = require("obsidian");
|
||||
var import_obsidian6 = require("obsidian");
|
||||
var VIEW_TYPE_BROWSE = "vault-hub-browse";
|
||||
var TYPE_FILTERS = ["all", "vault", "snippet", "note", "dashboard"];
|
||||
function normalizeToken(value) {
|
||||
|
|
@ -2060,7 +2104,7 @@ function parseAttachedSnippets(frontmatter) {
|
|||
pushCurrent();
|
||||
return snippets;
|
||||
}
|
||||
var BrowseView = class extends import_obsidian4.ItemView {
|
||||
var BrowseView = class extends import_obsidian6.ItemView {
|
||||
constructor(leaf, plugin) {
|
||||
super(leaf);
|
||||
this.searchQuery = "";
|
||||
|
|
@ -2165,10 +2209,10 @@ var BrowseView = class extends import_obsidian4.ItemView {
|
|||
if (this.searchQuery.trim()) {
|
||||
params.set("q", this.searchQuery.trim());
|
||||
}
|
||||
const res = await fetch(`${baseUrl}/api/search?${params.toString()}`);
|
||||
const data = await res.json();
|
||||
if (!res.ok || !Array.isArray(data)) {
|
||||
throw new Error((data == null ? void 0 : data.error) || (data == null ? void 0 : data.message) || (data == null ? void 0 : data.hint) || "Unexpected response from API");
|
||||
const data = await requestJson(`${baseUrl}/api/search?${params.toString()}`);
|
||||
if (!Array.isArray(data)) {
|
||||
const errorPayload = data;
|
||||
throw new Error((errorPayload == null ? void 0 : errorPayload.error) || (errorPayload == null ? void 0 : errorPayload.message) || (errorPayload == null ? void 0 : errorPayload.hint) || "Unexpected response from API");
|
||||
}
|
||||
let resources = data.map(
|
||||
(resource) => normalizeResource(resource)
|
||||
|
|
@ -2231,19 +2275,18 @@ var BrowseView = class extends import_obsidian4.ItemView {
|
|||
}
|
||||
btn.setText("Installed");
|
||||
} catch (e) {
|
||||
new import_obsidian4.Notice(`Install failed: ${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 treeRes = await fetch(
|
||||
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 treeData = await treeRes.json();
|
||||
if (!treeRes.ok || !Array.isArray(treeData.tree)) {
|
||||
if (!Array.isArray(treeData.tree)) {
|
||||
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;
|
||||
|
|
@ -2258,15 +2301,15 @@ var BrowseView = class extends import_obsidian4.ItemView {
|
|||
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)}`;
|
||||
const raw = await fetch(rawUrl);
|
||||
if (!raw.ok) {
|
||||
try {
|
||||
const raw = await requestText(rawUrl);
|
||||
await this.app.vault.adapter.write(destPath, raw);
|
||||
installed++;
|
||||
} catch (e) {
|
||||
failed++;
|
||||
continue;
|
||||
}
|
||||
await this.app.vault.adapter.write(destPath, await raw.text());
|
||||
installed++;
|
||||
}
|
||||
new import_obsidian4.Notice(
|
||||
new import_obsidian6.Notice(
|
||||
`Installed "${r.title}" - ${installed} files in "${folderName}/"${failed ? ` (${failed} failed)` : ""}`
|
||||
);
|
||||
const hubMd = await this.fetchHubMd(r);
|
||||
|
|
@ -2313,10 +2356,11 @@ var BrowseView = class extends import_obsidian4.ItemView {
|
|||
return candidate;
|
||||
}
|
||||
async fetchHubMd(r) {
|
||||
const raw = await fetch(`https://raw.githubusercontent.com/${r.full_name}/HEAD/hub.md`);
|
||||
if (!raw.ok)
|
||||
try {
|
||||
return await requestText(`https://raw.githubusercontent.com/${r.full_name}/HEAD/hub.md`);
|
||||
} catch (e) {
|
||||
return null;
|
||||
return raw.text();
|
||||
}
|
||||
}
|
||||
async notifyRequiredPluginsFromContent(hubMd, title) {
|
||||
var _a;
|
||||
|
|
@ -2332,9 +2376,9 @@ var BrowseView = class extends import_obsidian4.ItemView {
|
|||
);
|
||||
const missing = ids.filter((id) => !installedPlugins.includes(id));
|
||||
if (missing.length === 0) {
|
||||
new import_obsidian4.Notice(`All required plugins already installed for "${title}"`);
|
||||
new import_obsidian6.Notice(`All required plugins already installed for "${title}"`);
|
||||
} else {
|
||||
new import_obsidian4.Notice(
|
||||
new import_obsidian6.Notice(
|
||||
`"${title}" needs ${missing.length} plugin(s): ${missing.join(", ")}. Install from Community Plugins.`,
|
||||
1e4
|
||||
);
|
||||
|
|
@ -2344,12 +2388,11 @@ var BrowseView = class extends import_obsidian4.ItemView {
|
|||
}
|
||||
/** Download CSS files to .obsidian/snippets/ */
|
||||
async installSnippet(r) {
|
||||
const treeRes = await fetch(
|
||||
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 treeData = await treeRes.json();
|
||||
if (!treeRes.ok || !Array.isArray(treeData.tree)) {
|
||||
if (!Array.isArray(treeData.tree)) {
|
||||
throw new Error(treeData.message || "Failed to fetch repository tree");
|
||||
}
|
||||
const cssFiles = treeData.tree.filter(
|
||||
|
|
@ -2358,30 +2401,27 @@ var BrowseView = class extends import_obsidian4.ItemView {
|
|||
if (cssFiles.length === 0) {
|
||||
throw new Error("No CSS files found in this snippet repository");
|
||||
}
|
||||
const snippetsDir = `${this.app.vault.configDir}/snippets`;
|
||||
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 fetch(`https://raw.githubusercontent.com/${r.full_name}/HEAD/${encodeGitHubPath(f.path)}`);
|
||||
if (!raw.ok)
|
||||
throw new Error(`Failed to download ${f.path}`);
|
||||
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, await raw.text());
|
||||
await this.app.vault.adapter.write(target, raw);
|
||||
}
|
||||
new import_obsidian4.Notice(
|
||||
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 treeRes = await fetch(
|
||||
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 treeData = await treeRes.json();
|
||||
if (!treeRes.ok || !Array.isArray(treeData.tree)) {
|
||||
if (!Array.isArray(treeData.tree)) {
|
||||
throw new Error(treeData.message || "Failed to fetch repository tree");
|
||||
}
|
||||
const mdFiles = treeData.tree.filter(
|
||||
|
|
@ -2393,15 +2433,13 @@ var BrowseView = class extends import_obsidian4.ItemView {
|
|||
const folderName = await this.availableFolder(r.repo_name);
|
||||
let installed = 0;
|
||||
for (const f of mdFiles) {
|
||||
const raw = await fetch(`https://raw.githubusercontent.com/${r.full_name}/HEAD/${encodeGitHubPath(f.path)}`);
|
||||
if (!raw.ok)
|
||||
throw new Error(`Failed to download ${f.path}`);
|
||||
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, await raw.text());
|
||||
await this.app.vault.adapter.write(destPath, raw);
|
||||
installed++;
|
||||
}
|
||||
new import_obsidian4.Notice(`Installed ${installed} note(s) from "${r.title}" in "${folderName}/"`);
|
||||
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);
|
||||
|
|
@ -2414,7 +2452,7 @@ var BrowseView = class extends import_obsidian4.ItemView {
|
|||
const attachedSnippets = parseAttachedSnippets(frontmatter);
|
||||
if (attachedSnippets.length === 0)
|
||||
return;
|
||||
const snippetsDir = `${this.app.vault.configDir}/snippets`;
|
||||
const snippetsDir = getSnippetDirectory(this.app.vault);
|
||||
if (!await this.app.vault.adapter.exists(snippetsDir)) {
|
||||
await this.app.vault.adapter.mkdir(snippetsDir);
|
||||
}
|
||||
|
|
@ -2422,22 +2460,18 @@ var BrowseView = class extends import_obsidian4.ItemView {
|
|||
let failed = 0;
|
||||
for (const snippet of attachedSnippets) {
|
||||
try {
|
||||
const raw = await fetch(
|
||||
const raw = await requestText(
|
||||
`https://raw.githubusercontent.com/${r.full_name}/HEAD/${encodeGitHubPath(snippet.path)}`
|
||||
);
|
||||
if (!raw.ok) {
|
||||
failed++;
|
||||
continue;
|
||||
}
|
||||
const target = await this.availablePath(`${snippetsDir}/${basename(snippet.path)}`);
|
||||
await this.app.vault.adapter.write(target, await raw.text());
|
||||
await this.app.vault.adapter.write(target, raw);
|
||||
installed++;
|
||||
} catch (e) {
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
if (installed > 0 || failed > 0) {
|
||||
new import_obsidian4.Notice(
|
||||
new import_obsidian6.Notice(
|
||||
`Installed ${installed} attached snippet(s) from "${r.title}"${failed ? ` (${failed} failed)` : ""}`
|
||||
);
|
||||
}
|
||||
|
|
@ -2449,7 +2483,7 @@ var BrowseView = class extends import_obsidian4.ItemView {
|
|||
if (!container)
|
||||
return;
|
||||
container.empty();
|
||||
const snippetsDir = `${this.app.vault.configDir}/snippets`;
|
||||
const snippetsDir = getSnippetDirectory(this.app.vault);
|
||||
let files = [];
|
||||
try {
|
||||
const listing = await this.app.vault.adapter.list(snippetsDir);
|
||||
|
|
@ -2492,7 +2526,7 @@ var BrowseView = class extends import_obsidian4.ItemView {
|
|||
deleteBtn.addEventListener("click", async () => {
|
||||
if (confirm(`Delete snippet "${snippetId}"?`)) {
|
||||
await this.app.vault.adapter.remove(`${snippetsDir}/${fileName}`);
|
||||
new import_obsidian4.Notice(`Deleted: ${snippetId}`);
|
||||
new import_obsidian6.Notice(`Deleted: ${snippetId}`);
|
||||
await this.renderSnippetManager();
|
||||
}
|
||||
});
|
||||
|
|
@ -2505,7 +2539,7 @@ var BrowseView = class extends import_obsidian4.ItemView {
|
|||
if (css.setCssEnabledStatus) {
|
||||
css.setCssEnabledStatus(snippetId, enable);
|
||||
(_a = css.requestLoadSnippets) == null ? void 0 : _a.call(css);
|
||||
new import_obsidian4.Notice(`Snippet "${snippetId}" ${enable ? "enabled" : "disabled"}`);
|
||||
new import_obsidian6.Notice(`Snippet "${snippetId}" ${enable ? "enabled" : "disabled"}`);
|
||||
return;
|
||||
}
|
||||
if (css.enabledSnippets) {
|
||||
|
|
@ -2514,7 +2548,7 @@ var BrowseView = class extends import_obsidian4.ItemView {
|
|||
else
|
||||
css.enabledSnippets.delete(snippetId);
|
||||
(_b = css.requestLoadSnippets) == null ? void 0 : _b.call(css);
|
||||
new import_obsidian4.Notice(`Snippet "${snippetId}" ${enable ? "enabled" : "disabled"}`);
|
||||
new import_obsidian6.Notice(`Snippet "${snippetId}" ${enable ? "enabled" : "disabled"}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -2535,15 +2569,15 @@ var BrowseView = class extends import_obsidian4.ItemView {
|
|||
}
|
||||
appearance.enabledCssSnippets = enabled;
|
||||
await this.app.vault.adapter.write(appearancePath, JSON.stringify(appearance, null, 2));
|
||||
new import_obsidian4.Notice(`Snippet "${snippetId}" ${enable ? "enabled" : "disabled"} - reload to apply`);
|
||||
new import_obsidian6.Notice(`Snippet "${snippetId}" ${enable ? "enabled" : "disabled"} - reload to apply`);
|
||||
} catch (e) {
|
||||
new import_obsidian4.Notice(`Failed to toggle snippet: ${e}`);
|
||||
new import_obsidian6.Notice(`Failed to toggle snippet: ${e}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// src/main.ts
|
||||
var VaultHubPlugin = class extends import_obsidian5.Plugin {
|
||||
var VaultHubPlugin = class extends import_obsidian7.Plugin {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.settings = DEFAULT_SETTINGS;
|
||||
|
|
@ -2553,17 +2587,17 @@ var VaultHubPlugin = class extends import_obsidian5.Plugin {
|
|||
this.registerView(VIEW_TYPE_BROWSE, (leaf) => new BrowseView(leaf, this));
|
||||
this.addCommand({
|
||||
id: "publish-resource",
|
||||
name: "Publish Resource",
|
||||
name: "Publish resource",
|
||||
callback: () => new PublishModal(this.app, this).open()
|
||||
});
|
||||
this.addCommand({
|
||||
id: "update-resource",
|
||||
name: "Update Resource",
|
||||
name: "Update resource",
|
||||
callback: () => new UpdateModal(this.app, this).open()
|
||||
});
|
||||
this.addCommand({
|
||||
id: "browse-resources",
|
||||
name: "Browse Resources",
|
||||
name: "Browse resources",
|
||||
callback: () => this.activateBrowseView()
|
||||
});
|
||||
this.addSettingTab(new VaultHubSettingTab(this.app, this));
|
||||
|
|
|
|||
|
|
@ -5,5 +5,6 @@
|
|||
"minAppVersion": "1.0.0",
|
||||
"description": "Publish CSS snippets, notes, and templates to the Vault Hub community platform.",
|
||||
"author": "Maws7140",
|
||||
"isDesktopOnly": false
|
||||
"authorUrl": "https://github.com/Maws7140",
|
||||
"isDesktopOnly": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,28 @@
|
|||
import { requestJson } from "./net";
|
||||
|
||||
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}`);
|
||||
try {
|
||||
return await requestJson(`https://api.github.com${path}`, {
|
||||
method: options.method,
|
||||
body: typeof options.body === "string" ? options.body : undefined,
|
||||
headers: {
|
||||
Authorization: `token ${this.token}`,
|
||||
Accept: "application/vnd.github.v3+json",
|
||||
"Content-Type": "application/json",
|
||||
...(options.headers as Record<string, string> | undefined),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const httpMatch = message.match(/^HTTP (\d+):\s*([\s\S]*)$/);
|
||||
if (httpMatch) {
|
||||
throw new Error(`GitHub API ${httpMatch[1]}: ${httpMatch[2]}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
private encodePath(path: string): string {
|
||||
|
|
|
|||
|
|
@ -18,19 +18,19 @@ export default class VaultHubPlugin extends Plugin {
|
|||
|
||||
this.addCommand({
|
||||
id: "publish-resource",
|
||||
name: "Publish Resource",
|
||||
name: "Publish resource",
|
||||
callback: () => new PublishModal(this.app, this).open(),
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "update-resource",
|
||||
name: "Update Resource",
|
||||
name: "Update resource",
|
||||
callback: () => new UpdateModal(this.app, this).open(),
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "browse-resources",
|
||||
name: "Browse Resources",
|
||||
name: "Browse resources",
|
||||
callback: () => this.activateBrowseView(),
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import type VaultHubPlugin from "../main";
|
|||
import { DetectedPlugin, detectPlugins } from "../detection";
|
||||
import { GitHubAPI } from "../github";
|
||||
import { generateHubMd, HubMdData } from "../hubmd";
|
||||
import { getSnippetDirectory } from "../paths";
|
||||
import { generateReadme, ReadmeData, syncReadmeScreenshots } from "../readme";
|
||||
import type { PublishDraft } from "../settings";
|
||||
|
||||
|
|
@ -60,7 +61,7 @@ function tfileToPublishFile(app: App, f: TFile): PublishFile {
|
|||
|
||||
async function listSnippetFiles(app: App): Promise<PublishFile[]> {
|
||||
const adapter = app.vault.adapter;
|
||||
const dir = ".obsidian/snippets";
|
||||
const dir = getSnippetDirectory(app.vault);
|
||||
try {
|
||||
const exists = await adapter.exists(dir);
|
||||
if (!exists) return [];
|
||||
|
|
@ -173,6 +174,10 @@ export class PublishModal extends Modal {
|
|||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
private getSnippetDir() {
|
||||
return getSnippetDirectory(this.app.vault);
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
this.contentEl.empty();
|
||||
this.contentEl.createEl("p", { text: "Loading publish draft...", cls: "vault-hub-hint" });
|
||||
|
|
@ -361,7 +366,7 @@ export class PublishModal extends Modal {
|
|||
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` });
|
||||
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");
|
||||
|
|
@ -449,7 +454,7 @@ export class PublishModal extends Modal {
|
|||
|
||||
if (this.resourceType === "snippet") {
|
||||
fileSection.createEl("p", {
|
||||
text: "Sourced from .obsidian/snippets. Drop .css files there if nothing shows up.",
|
||||
text: `Sourced from ${this.getSnippetDir()}. Drop .css files there if nothing shows up.`,
|
||||
cls: "vault-hub-hint",
|
||||
});
|
||||
} else {
|
||||
|
|
@ -463,7 +468,7 @@ export class PublishModal extends Modal {
|
|||
fileSection.createEl("p", {
|
||||
text:
|
||||
this.resourceType === "snippet"
|
||||
? "No CSS snippets found in .obsidian/snippets."
|
||||
? `No CSS snippets found in ${this.getSnippetDir()}.`
|
||||
: this.resourceType === "bundle"
|
||||
? "No markdown files found in this vault."
|
||||
: "No markdown files found in this vault.",
|
||||
|
|
@ -557,7 +562,7 @@ export class PublishModal extends Modal {
|
|||
|
||||
if (availableSnippets.length === 0) {
|
||||
snippetSection.createEl("p", {
|
||||
text: "No CSS snippets found in .obsidian/snippets.",
|
||||
text: `No CSS snippets found in ${this.getSnippetDir()}.`,
|
||||
cls: "vault-hub-hint",
|
||||
});
|
||||
} else {
|
||||
|
|
@ -620,7 +625,7 @@ export class PublishModal extends Modal {
|
|||
|
||||
private async renderStep2() {
|
||||
const c = this.contentEl;
|
||||
c.createEl("h4", { text: "Select Required Plugins" });
|
||||
c.createEl("h4", { text: "Select required plugins" });
|
||||
c.createEl("p", {
|
||||
text: "Auto-detected plugins are pre-checked. Review and adjust.",
|
||||
cls: "vault-hub-hint",
|
||||
|
|
@ -723,7 +728,7 @@ export class PublishModal extends Modal {
|
|||
|
||||
if (availableSnippets.length === 0) {
|
||||
snippetSection.createEl("p", {
|
||||
text: "No CSS snippets found in .obsidian/snippets.",
|
||||
text: `No CSS snippets found in ${this.getSnippetDir()}.`,
|
||||
cls: "vault-hub-hint",
|
||||
});
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export class UpdateModal extends Modal {
|
|||
const c = this.contentEl;
|
||||
c.empty();
|
||||
c.addClass("vault-hub-modal");
|
||||
c.createEl("h2", { text: "Update Resource" });
|
||||
c.createEl("h2", { text: "Update resource" });
|
||||
|
||||
const loading = c.createEl("p", { text: "Loading resources...", cls: "vault-hub-hint" });
|
||||
const resources = await this.getSelectableResources();
|
||||
|
|
@ -123,7 +123,7 @@ export class UpdateModal extends Modal {
|
|||
private renderDiff() {
|
||||
const c = this.contentEl;
|
||||
c.empty();
|
||||
c.createEl("h2", { text: "Review Changes" });
|
||||
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");
|
||||
|
|
|
|||
43
src/net.ts
Normal file
43
src/net.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { requestUrl } from "obsidian";
|
||||
|
||||
interface RequestOptions {
|
||||
method?: string;
|
||||
headers?: Record<string, string>;
|
||||
body?: string;
|
||||
}
|
||||
|
||||
function buildError(status: number, text: string): Error {
|
||||
return new Error(`HTTP ${status}: ${text}`);
|
||||
}
|
||||
|
||||
export async function requestJson<T>(url: string, options: RequestOptions = {}): Promise<T> {
|
||||
const response = await 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 as T;
|
||||
}
|
||||
|
||||
export async function requestText(url: string, options: RequestOptions = {}): Promise<string> {
|
||||
const response = await 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;
|
||||
}
|
||||
5
src/paths.ts
Normal file
5
src/paths.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { normalizePath, Vault } from "obsidian";
|
||||
|
||||
export function getSnippetDirectory(vault: Vault): string {
|
||||
return normalizePath(`${vault.configDir}/snippets`);
|
||||
}
|
||||
|
|
@ -67,15 +67,13 @@ export function generateReadme(data: ReadmeData): string {
|
|||
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("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 `.obsidian/snippets/`");
|
||||
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");
|
||||
|
|
@ -87,7 +85,7 @@ export function generateReadme(data: ReadmeData): string {
|
|||
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 `.obsidian/snippets/`");
|
||||
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");
|
||||
|
|
|
|||
|
|
@ -66,11 +66,11 @@ export class VaultHubSettingTab extends PluginSettingTab {
|
|||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
|
||||
containerEl.createEl("h2", { text: "Vault Hub Settings" });
|
||||
new Setting(containerEl).setName("Vault Hub").setHeading();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("GitHub Personal Access Token")
|
||||
.setDesc("Token used to create repos and push files. Requires 'repo' scope.")
|
||||
.setName("GitHub personal access token")
|
||||
.setDesc("Token used to create repos and push files. Requires the repo scope.")
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("ghp_xxxxxxxxxxxx")
|
||||
|
|
@ -99,8 +99,8 @@ export class VaultHubSettingTab extends PluginSettingTab {
|
|||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Catalog Repository")
|
||||
.setDesc("Repo that stores the website catalog workflow. Used to request a refresh after publish/update.")
|
||||
.setName("Catalog repository")
|
||||
.setDesc("Repository that stores the website catalog workflow. Used to request a refresh after publish or update.")
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("Maws7140/vault-hub")
|
||||
|
|
@ -112,7 +112,7 @@ export class VaultHubSettingTab extends PluginSettingTab {
|
|||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Default Categories")
|
||||
.setName("Default categories")
|
||||
.setDesc("Comma-separated list of default categories for new publications.")
|
||||
.addText((text) =>
|
||||
text
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { ItemView, WorkspaceLeaf, Notice } from "obsidian";
|
||||
import type VaultHubPlugin from "../main";
|
||||
import { requestJson, requestText } from "../net";
|
||||
import { getSnippetDirectory } from "../paths";
|
||||
|
||||
export const VIEW_TYPE_BROWSE = "vault-hub-browse";
|
||||
|
||||
|
|
@ -243,11 +245,10 @@ export class BrowseView extends ItemView {
|
|||
params.set("q", this.searchQuery.trim());
|
||||
}
|
||||
|
||||
const res = await fetch(`${baseUrl}/api/search?${params.toString()}`);
|
||||
|
||||
const data = await res.json();
|
||||
if (!res.ok || !Array.isArray(data)) {
|
||||
throw new Error(data?.error || data?.message || data?.hint || "Unexpected response from API");
|
||||
const data = await requestJson<unknown>(`${baseUrl}/api/search?${params.toString()}`);
|
||||
if (!Array.isArray(data)) {
|
||||
const errorPayload = data as { error?: string; message?: string; hint?: string } | null;
|
||||
throw new Error(errorPayload?.error || errorPayload?.message || errorPayload?.hint || "Unexpected response from API");
|
||||
}
|
||||
let resources = (data as Array<Omit<ResourceSummary, "type"> & { type: string }>).map((resource) =>
|
||||
normalizeResource(resource)
|
||||
|
|
@ -331,13 +332,12 @@ export class BrowseView extends ItemView {
|
|||
|
||||
/** Download full vault tree into a named subfolder */
|
||||
private async installVault(r: ResourceSummary) {
|
||||
const treeRes = await fetch(
|
||||
const treeData = await requestJson<{ tree?: { path: string; type: string }[]; message?: string }>(
|
||||
`https://api.github.com/repos/${r.full_name}/git/trees/HEAD?recursive=1`,
|
||||
{ headers: { Accept: "application/vnd.github.v3+json" } }
|
||||
);
|
||||
const treeData = await treeRes.json();
|
||||
|
||||
if (!treeRes.ok || !Array.isArray(treeData.tree)) {
|
||||
if (!Array.isArray(treeData.tree)) {
|
||||
throw new Error(treeData.message || "Failed to fetch repository tree");
|
||||
}
|
||||
|
||||
|
|
@ -359,14 +359,13 @@ export class BrowseView extends ItemView {
|
|||
await this.ensureDir(dirPath);
|
||||
|
||||
const rawUrl = `https://raw.githubusercontent.com/${r.full_name}/HEAD/${encodeGitHubPath(item.path)}`;
|
||||
const raw = await fetch(rawUrl);
|
||||
if (!raw.ok) {
|
||||
try {
|
||||
const raw = await requestText(rawUrl);
|
||||
await this.app.vault.adapter.write(destPath, raw);
|
||||
installed++;
|
||||
} catch {
|
||||
failed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.app.vault.adapter.write(destPath, await raw.text());
|
||||
installed++;
|
||||
}
|
||||
|
||||
new Notice(
|
||||
|
|
@ -421,9 +420,11 @@ export class BrowseView extends ItemView {
|
|||
}
|
||||
|
||||
private async fetchHubMd(r: ResourceSummary): Promise<string | null> {
|
||||
const raw = await fetch(`https://raw.githubusercontent.com/${r.full_name}/HEAD/hub.md`);
|
||||
if (!raw.ok) return null;
|
||||
return raw.text();
|
||||
try {
|
||||
return await requestText(`https://raw.githubusercontent.com/${r.full_name}/HEAD/hub.md`);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async notifyRequiredPluginsFromContent(hubMd: string, title: string) {
|
||||
|
|
@ -454,13 +455,12 @@ export class BrowseView extends ItemView {
|
|||
|
||||
/** Download CSS files to .obsidian/snippets/ */
|
||||
private async installSnippet(r: ResourceSummary) {
|
||||
const treeRes = await fetch(
|
||||
const treeData = await requestJson<{ tree?: { path: string; type: string }[]; message?: string }>(
|
||||
`https://api.github.com/repos/${r.full_name}/git/trees/HEAD?recursive=1`,
|
||||
{ headers: { Accept: "application/vnd.github.v3+json" } }
|
||||
);
|
||||
const treeData = await treeRes.json();
|
||||
|
||||
if (!treeRes.ok || !Array.isArray(treeData.tree)) {
|
||||
if (!Array.isArray(treeData.tree)) {
|
||||
throw new Error(treeData.message || "Failed to fetch repository tree");
|
||||
}
|
||||
|
||||
|
|
@ -471,16 +471,15 @@ export class BrowseView extends ItemView {
|
|||
throw new Error("No CSS files found in this snippet repository");
|
||||
}
|
||||
|
||||
const snippetsDir = `${this.app.vault.configDir}/snippets`;
|
||||
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 fetch(`https://raw.githubusercontent.com/${r.full_name}/HEAD/${encodeGitHubPath(f.path)}`);
|
||||
if (!raw.ok) throw new Error(`Failed to download ${f.path}`);
|
||||
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, await raw.text());
|
||||
await this.app.vault.adapter.write(target, raw);
|
||||
}
|
||||
|
||||
new Notice(
|
||||
|
|
@ -491,13 +490,12 @@ export class BrowseView extends ItemView {
|
|||
/** Download .md files into a named folder */
|
||||
private async installNotes(r: ResourceSummary) {
|
||||
const hubMd = await this.fetchHubMd(r);
|
||||
const treeRes = await fetch(
|
||||
const treeData = await requestJson<{ tree?: { path: string; type: string }[]; message?: string }>(
|
||||
`https://api.github.com/repos/${r.full_name}/git/trees/HEAD?recursive=1`,
|
||||
{ headers: { Accept: "application/vnd.github.v3+json" } }
|
||||
);
|
||||
const treeData = await treeRes.json();
|
||||
|
||||
if (!treeRes.ok || !Array.isArray(treeData.tree)) {
|
||||
if (!Array.isArray(treeData.tree)) {
|
||||
throw new Error(treeData.message || "Failed to fetch repository tree");
|
||||
}
|
||||
|
||||
|
|
@ -515,11 +513,10 @@ export class BrowseView extends ItemView {
|
|||
const folderName = await this.availableFolder(r.repo_name);
|
||||
let installed = 0;
|
||||
for (const f of mdFiles) {
|
||||
const raw = await fetch(`https://raw.githubusercontent.com/${r.full_name}/HEAD/${encodeGitHubPath(f.path)}`);
|
||||
if (!raw.ok) throw new Error(`Failed to download ${f.path}`);
|
||||
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, await raw.text());
|
||||
await this.app.vault.adapter.write(destPath, raw);
|
||||
installed++;
|
||||
}
|
||||
|
||||
|
|
@ -538,7 +535,7 @@ export class BrowseView extends ItemView {
|
|||
const attachedSnippets = parseAttachedSnippets(frontmatter);
|
||||
if (attachedSnippets.length === 0) return;
|
||||
|
||||
const snippetsDir = `${this.app.vault.configDir}/snippets`;
|
||||
const snippetsDir = getSnippetDirectory(this.app.vault);
|
||||
if (!(await this.app.vault.adapter.exists(snippetsDir))) {
|
||||
await this.app.vault.adapter.mkdir(snippetsDir);
|
||||
}
|
||||
|
|
@ -548,15 +545,11 @@ export class BrowseView extends ItemView {
|
|||
|
||||
for (const snippet of attachedSnippets) {
|
||||
try {
|
||||
const raw = await fetch(
|
||||
const raw = await requestText(
|
||||
`https://raw.githubusercontent.com/${r.full_name}/HEAD/${encodeGitHubPath(snippet.path)}`
|
||||
);
|
||||
if (!raw.ok) {
|
||||
failed++;
|
||||
continue;
|
||||
}
|
||||
const target = await this.availablePath(`${snippetsDir}/${basename(snippet.path)}`);
|
||||
await this.app.vault.adapter.write(target, await raw.text());
|
||||
await this.app.vault.adapter.write(target, raw);
|
||||
installed++;
|
||||
} catch {
|
||||
failed++;
|
||||
|
|
@ -578,7 +571,7 @@ export class BrowseView extends ItemView {
|
|||
|
||||
container.empty();
|
||||
|
||||
const snippetsDir = `${this.app.vault.configDir}/snippets`;
|
||||
const snippetsDir = getSnippetDirectory(this.app.vault);
|
||||
let files: string[] = [];
|
||||
|
||||
try {
|
||||
|
|
|
|||
3
versions.json
Normal file
3
versions.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"1.0.0": "1.0.0"
|
||||
}
|
||||
Loading…
Reference in a new issue