mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-02 04:12:47 +02:00
feat: enhance Obsidian plugin with folder management features, including inclusion/exclusion settings and a status modal for real-time updates
This commit is contained in:
parent
2251e464c7
commit
28d3c628f1
11 changed files with 267 additions and 154 deletions
|
|
@ -1,8 +1,8 @@
|
|||
"""Obsidian plugin ingestion routes (``/api/v1/obsidian/*``).
|
||||
|
||||
Wire surface for the ``surfsense_obsidian/`` plugin. API stability is the
|
||||
``/api/v1/`` prefix plus the additive ``capabilities`` array on /health;
|
||||
no plugin-version gate.
|
||||
Wire surface for the ``surfsense_obsidian/`` plugin. Versioning anchor is
|
||||
the ``/api/v1/`` URL prefix; additive feature detection rides the
|
||||
``capabilities`` array on /health and /connect.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -46,9 +46,6 @@ logger = logging.getLogger(__name__)
|
|||
router = APIRouter(prefix="/obsidian", tags=["obsidian-plugin"])
|
||||
|
||||
|
||||
# Bumped only on non-additive wire changes; additive ones ride extra='ignore'.
|
||||
OBSIDIAN_API_VERSION = "1"
|
||||
|
||||
# Plugins feature-gate on these. Add entries, never rename or remove.
|
||||
OBSIDIAN_CAPABILITIES: list[str] = ["sync", "rename", "delete", "manifest", "stats"]
|
||||
|
||||
|
|
@ -59,10 +56,7 @@ OBSIDIAN_CAPABILITIES: list[str] = ["sync", "rename", "delete", "manifest", "sta
|
|||
|
||||
|
||||
def _build_handshake() -> dict[str, object]:
|
||||
return {
|
||||
"api_version": OBSIDIAN_API_VERSION,
|
||||
"capabilities": list(OBSIDIAN_CAPABILITIES),
|
||||
}
|
||||
return {"capabilities": list(OBSIDIAN_CAPABILITIES)}
|
||||
|
||||
|
||||
async def _resolve_vault_connector(
|
||||
|
|
|
|||
|
|
@ -92,13 +92,11 @@ class ConnectResponse(_PluginBase):
|
|||
connector_id: int
|
||||
vault_id: str
|
||||
search_space_id: int
|
||||
api_version: str
|
||||
capabilities: list[str]
|
||||
|
||||
|
||||
class HealthResponse(_PluginBase):
|
||||
"""API contract handshake. ``capabilities`` is additive-only string list."""
|
||||
|
||||
api_version: str
|
||||
capabilities: list[str]
|
||||
server_time_utc: datetime
|
||||
|
|
|
|||
|
|
@ -64,3 +64,31 @@ export function parseExcludePatterns(raw: string): string[] {
|
|||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0 && !line.startsWith("#"));
|
||||
}
|
||||
|
||||
/** Normalize a folder path: strip leading/trailing slashes; "" or "/" means vault root. */
|
||||
export function normalizeFolder(folder: string): string {
|
||||
return folder.replace(/^\/+|\/+$/g, "");
|
||||
}
|
||||
|
||||
/** True if `path` lives inside `folder` (or `folder` is the vault root). */
|
||||
export function isInFolder(path: string, folder: string): boolean {
|
||||
const f = normalizeFolder(folder);
|
||||
if (f === "") return true;
|
||||
return path === f || path.startsWith(`${f}/`);
|
||||
}
|
||||
|
||||
/** Exclude wins over include. Empty includeFolders means "include everything". */
|
||||
export function isFolderFiltered(
|
||||
path: string,
|
||||
includeFolders: string[],
|
||||
excludeFolders: string[],
|
||||
): boolean {
|
||||
for (const f of excludeFolders) {
|
||||
if (isInFolder(path, f)) return true;
|
||||
}
|
||||
if (includeFolders.length === 0) return false;
|
||||
for (const f of includeFolders) {
|
||||
if (isInFolder(path, f)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
32
surfsense_obsidian/src/folder-suggest-modal.ts
Normal file
32
surfsense_obsidian/src/folder-suggest-modal.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { type App, FuzzySuggestModal, type TFolder } from "obsidian";
|
||||
|
||||
/** Folder picker built on Obsidian's stock {@link FuzzySuggestModal}. */
|
||||
export class FolderSuggestModal extends FuzzySuggestModal<TFolder> {
|
||||
private readonly onPick: (path: string) => void;
|
||||
private readonly excluded: Set<string>;
|
||||
|
||||
constructor(app: App, onPick: (path: string) => void, excluded: string[] = []) {
|
||||
super(app);
|
||||
this.onPick = onPick;
|
||||
this.excluded = new Set(excluded.map((p) => p.replace(/^\/+|\/+$/g, "")));
|
||||
this.setPlaceholder("Type to filter folders…");
|
||||
}
|
||||
|
||||
getItems(): TFolder[] {
|
||||
return this.app.vault
|
||||
.getAllFolders(true)
|
||||
.filter((f) => !this.excluded.has(this.toPath(f)));
|
||||
}
|
||||
|
||||
getItemText(folder: TFolder): string {
|
||||
return this.toPath(folder) || "/";
|
||||
}
|
||||
|
||||
onChooseItem(folder: TFolder): void {
|
||||
this.onPick(this.toPath(folder));
|
||||
}
|
||||
|
||||
private toPath(folder: TFolder): string {
|
||||
return folder.isRoot() ? "" : folder.path;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ import { SurfSenseApiClient } from "./api-client";
|
|||
import { PersistentQueue } from "./queue";
|
||||
import { SurfSenseSettingTab } from "./settings";
|
||||
import { StatusBar } from "./status-bar";
|
||||
import { StatusModal } from "./status-modal";
|
||||
import { SyncEngine } from "./sync-engine";
|
||||
import {
|
||||
DEFAULT_SETTINGS,
|
||||
|
|
@ -20,8 +21,8 @@ export default class SurfSensePlugin extends Plugin {
|
|||
private statusBar: StatusBar | null = null;
|
||||
lastStatus: StatusState = { kind: "idle", queueDepth: 0 };
|
||||
serverCapabilities: string[] = [];
|
||||
serverApiVersion: string | null = null;
|
||||
private settingTab: SurfSenseSettingTab | null = null;
|
||||
private statusListeners = new Set<() => void>();
|
||||
|
||||
async onload() {
|
||||
await this.loadSettings();
|
||||
|
|
@ -48,17 +49,16 @@ export default class SurfSensePlugin extends Plugin {
|
|||
saveSettings: async (mut) => {
|
||||
mut(this.settings);
|
||||
await this.saveSettings();
|
||||
this.settingTab?.renderStatus();
|
||||
this.notifyStatusChange();
|
||||
},
|
||||
setStatus: (s) => {
|
||||
this.lastStatus = s;
|
||||
this.statusBar?.update(s);
|
||||
this.settingTab?.renderStatus();
|
||||
this.notifyStatusChange();
|
||||
},
|
||||
onCapabilities: (caps, apiVersion) => {
|
||||
onCapabilities: (caps) => {
|
||||
this.serverCapabilities = [...caps];
|
||||
this.serverApiVersion = apiVersion;
|
||||
this.settingTab?.renderStatus();
|
||||
this.notifyStatusChange();
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -71,7 +71,7 @@ export default class SurfSensePlugin extends Plugin {
|
|||
this.addSettingTab(this.settingTab);
|
||||
|
||||
const statusHost = this.addStatusBarItem();
|
||||
this.statusBar = new StatusBar(statusHost);
|
||||
this.statusBar = new StatusBar(statusHost, () => this.openStatusModal());
|
||||
this.statusBar.update(this.lastStatus);
|
||||
|
||||
this.registerEvent(
|
||||
|
|
@ -120,6 +120,12 @@ export default class SurfSensePlugin extends Plugin {
|
|||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "open-status",
|
||||
name: "Open sync status",
|
||||
callback: () => this.openStatusModal(),
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "open-settings",
|
||||
name: "Open settings",
|
||||
|
|
@ -150,6 +156,22 @@ export default class SurfSensePlugin extends Plugin {
|
|||
return this.queue?.size ?? 0;
|
||||
}
|
||||
|
||||
openStatusModal(): void {
|
||||
new StatusModal(this.app, this).open();
|
||||
}
|
||||
|
||||
onStatusChange(listener: () => void): void {
|
||||
this.statusListeners.add(listener);
|
||||
}
|
||||
|
||||
offStatusChange(listener: () => void): void {
|
||||
this.statusListeners.delete(listener);
|
||||
}
|
||||
|
||||
private notifyStatusChange(): void {
|
||||
for (const fn of this.statusListeners) fn();
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
const data = (await this.loadData()) as Partial<SurfsensePluginSettings> | null;
|
||||
this.settings = {
|
||||
|
|
@ -157,6 +179,8 @@ export default class SurfSensePlugin extends Plugin {
|
|||
...(data ?? {}),
|
||||
queue: (data?.queue ?? []).map((i: QueueItem) => ({ ...i })),
|
||||
tombstones: { ...(data?.tombstones ?? {}) },
|
||||
includeFolders: [...(data?.includeFolders ?? [])],
|
||||
excludeFolders: [...(data?.excludeFolders ?? [])],
|
||||
excludePatterns: data?.excludePatterns?.length
|
||||
? [...data.excludePatterns]
|
||||
: [...DEFAULT_SETTINGS.excludePatterns],
|
||||
|
|
@ -172,9 +196,6 @@ export default class SurfSensePlugin extends Plugin {
|
|||
if (!this.settings.vaultId) {
|
||||
this.settings.vaultId = generateUuid();
|
||||
}
|
||||
if (!this.settings.vaultName) {
|
||||
this.settings.vaultName = this.app.vault.getName();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ import {
|
|||
Setting,
|
||||
} from "obsidian";
|
||||
import { AuthError } from "./api-client";
|
||||
import { parseExcludePatterns } from "./excludes";
|
||||
import { normalizeFolder, parseExcludePatterns } from "./excludes";
|
||||
import { FolderSuggestModal } from "./folder-suggest-modal";
|
||||
import type SurfSensePlugin from "./main";
|
||||
import type { SearchSpace } from "./types";
|
||||
|
||||
|
|
@ -15,7 +16,6 @@ export class SurfSenseSettingTab extends PluginSettingTab {
|
|||
private readonly plugin: SurfSensePlugin;
|
||||
private searchSpaces: SearchSpace[] = [];
|
||||
private loadingSpaces = false;
|
||||
private statusEl: HTMLElement | null = null;
|
||||
|
||||
constructor(app: App, plugin: SurfSensePlugin) {
|
||||
super(app, plugin);
|
||||
|
|
@ -25,7 +25,6 @@ export class SurfSenseSettingTab extends PluginSettingTab {
|
|||
display(): void {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
containerEl.addClass("surfsense-settings");
|
||||
|
||||
const settings = this.plugin.settings;
|
||||
|
||||
|
|
@ -107,7 +106,6 @@ export class SurfSenseSettingTab extends PluginSettingTab {
|
|||
this.handleApiError(err);
|
||||
}
|
||||
}
|
||||
this.renderStatus();
|
||||
});
|
||||
})
|
||||
.addExtraButton((btn) =>
|
||||
|
|
@ -122,20 +120,6 @@ export class SurfSenseSettingTab extends PluginSettingTab {
|
|||
|
||||
new Setting(containerEl).setName("Vault").setHeading();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Vault name")
|
||||
.setDesc(
|
||||
"Friendly name for this vault. Defaults to your Obsidian vault folder name.",
|
||||
)
|
||||
.addText((text) =>
|
||||
text
|
||||
.setValue(settings.vaultName)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.vaultName = value.trim() || this.app.vault.getName();
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Sync mode")
|
||||
.setDesc("Auto syncs on every edit. Manual only syncs when you trigger it via the command palette.")
|
||||
|
|
@ -150,10 +134,30 @@ export class SurfSenseSettingTab extends PluginSettingTab {
|
|||
}),
|
||||
);
|
||||
|
||||
this.renderFolderList(
|
||||
containerEl,
|
||||
"Include folders",
|
||||
"Folders to sync (leave empty to sync entire vault).",
|
||||
settings.includeFolders,
|
||||
(next) => {
|
||||
this.plugin.settings.includeFolders = next;
|
||||
},
|
||||
);
|
||||
|
||||
this.renderFolderList(
|
||||
containerEl,
|
||||
"Exclude folders",
|
||||
"Folders to exclude from sync (takes precedence over includes).",
|
||||
settings.excludeFolders,
|
||||
(next) => {
|
||||
this.plugin.settings.excludeFolders = next;
|
||||
},
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Exclude patterns")
|
||||
.setName("Advanced exclude patterns")
|
||||
.setDesc(
|
||||
"One pattern per line. Supports * and **. Lines starting with # are comments. Files matching any pattern are skipped.",
|
||||
"Glob fallback for power users. One pattern per line, supports * and **. Lines starting with # are comments. Applied on top of the folder lists above.",
|
||||
)
|
||||
.addTextArea((area) => {
|
||||
area.inputEl.rows = 4;
|
||||
|
|
@ -180,41 +184,12 @@ export class SurfSenseSettingTab extends PluginSettingTab {
|
|||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl).setName("Identity").setHeading();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Vault ID")
|
||||
.setDesc(
|
||||
"Stable identifier for this vault. Used by the backend to keep separate vaults distinct even if their folder names change.",
|
||||
)
|
||||
.addText((text) => {
|
||||
text.inputEl.disabled = true;
|
||||
text.setValue(settings.vaultId);
|
||||
});
|
||||
|
||||
// Device ID is deliberately not exposed: it's an opaque per-install UUID
|
||||
// (see seedIdentity in main.ts) and the web UI only shows a device count.
|
||||
|
||||
new Setting(containerEl).setName("Status").setHeading();
|
||||
this.statusEl = containerEl.createDiv({ cls: "surfsense-settings__status" });
|
||||
this.renderStatus();
|
||||
|
||||
new Setting(containerEl)
|
||||
.addButton((btn) =>
|
||||
btn
|
||||
.setButtonText("Re-sync entire vault")
|
||||
.onClick(async () => {
|
||||
btn.setDisabled(true);
|
||||
try {
|
||||
await this.plugin.engine.maybeReconcile(true);
|
||||
new Notice("Surfsense: re-sync requested.");
|
||||
} catch (err) {
|
||||
this.handleApiError(err);
|
||||
} finally {
|
||||
btn.setDisabled(false);
|
||||
this.renderStatus();
|
||||
}
|
||||
}),
|
||||
.setButtonText("View sync status")
|
||||
.setCta()
|
||||
.onClick(() => this.plugin.openStatusModal()),
|
||||
)
|
||||
.addButton((btn) =>
|
||||
btn.setButtonText("Open releases").onClick(() => {
|
||||
|
|
@ -226,10 +201,6 @@ export class SurfSenseSettingTab extends PluginSettingTab {
|
|||
);
|
||||
}
|
||||
|
||||
hide(): void {
|
||||
this.statusEl = null;
|
||||
}
|
||||
|
||||
private async refreshSearchSpaces(): Promise<void> {
|
||||
this.loadingSpaces = true;
|
||||
try {
|
||||
|
|
@ -242,38 +213,46 @@ export class SurfSenseSettingTab extends PluginSettingTab {
|
|||
}
|
||||
}
|
||||
|
||||
renderStatus(): void {
|
||||
if (!this.statusEl) return;
|
||||
const s = this.plugin.settings;
|
||||
this.statusEl.empty();
|
||||
private renderFolderList(
|
||||
containerEl: HTMLElement,
|
||||
title: string,
|
||||
desc: string,
|
||||
current: string[],
|
||||
write: (next: string[]) => void,
|
||||
): void {
|
||||
const setting = new Setting(containerEl).setName(title).setDesc(desc);
|
||||
|
||||
const rows: { label: string; value: string }[] = [
|
||||
{ label: "Status", value: this.plugin.lastStatus.kind },
|
||||
{
|
||||
label: "Last sync",
|
||||
value: s.lastSyncAt ? new Date(s.lastSyncAt).toLocaleString() : "—",
|
||||
},
|
||||
{
|
||||
label: "Last reconcile",
|
||||
value: s.lastReconcileAt ? new Date(s.lastReconcileAt).toLocaleString() : "—",
|
||||
},
|
||||
{ label: "Files synced", value: String(s.filesSynced ?? 0) },
|
||||
{ label: "Queue depth", value: String(this.plugin.queueDepth) },
|
||||
{
|
||||
label: "API version",
|
||||
value: this.plugin.serverApiVersion ?? "(not yet handshaken)",
|
||||
},
|
||||
{
|
||||
label: "Capabilities",
|
||||
value: this.plugin.serverCapabilities.length
|
||||
? this.plugin.serverCapabilities.join(", ")
|
||||
: "(not yet handshaken)",
|
||||
},
|
||||
];
|
||||
for (const row of rows) {
|
||||
const wrap = this.statusEl.createDiv({ cls: "surfsense-settings__status-row" });
|
||||
wrap.createSpan({ cls: "surfsense-settings__status-label", text: row.label });
|
||||
wrap.createSpan({ cls: "surfsense-settings__status-value", text: row.value });
|
||||
const persist = async (next: string[]): Promise<void> => {
|
||||
const dedup = Array.from(new Set(next.map(normalizeFolder)));
|
||||
write(dedup);
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
};
|
||||
|
||||
setting.addButton((btn) =>
|
||||
btn
|
||||
.setButtonText("Add Folder")
|
||||
.setCta()
|
||||
.onClick(() => {
|
||||
new FolderSuggestModal(
|
||||
this.app,
|
||||
(picked) => {
|
||||
void persist([...current, picked]);
|
||||
},
|
||||
current,
|
||||
).open();
|
||||
}),
|
||||
);
|
||||
|
||||
for (const folder of current) {
|
||||
new Setting(containerEl).setName(folder || "/").addExtraButton((btn) =>
|
||||
btn
|
||||
.setIcon("cross")
|
||||
.setTooltip("Remove")
|
||||
.onClick(() => {
|
||||
void persist(current.filter((f) => f !== folder));
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,11 +28,15 @@ export class StatusBar {
|
|||
private readonly icon: HTMLElement;
|
||||
private readonly text: HTMLElement;
|
||||
|
||||
constructor(host: HTMLElement) {
|
||||
constructor(host: HTMLElement, onClick?: () => void) {
|
||||
this.el = host;
|
||||
this.el.addClass("surfsense-status");
|
||||
this.icon = this.el.createSpan({ cls: "surfsense-status__icon" });
|
||||
this.text = this.el.createSpan({ cls: "surfsense-status__text" });
|
||||
if (onClick) {
|
||||
this.el.addClass("surfsense-status--clickable");
|
||||
this.el.addEventListener("click", onClick);
|
||||
}
|
||||
this.update({ kind: "idle", queueDepth: 0 });
|
||||
}
|
||||
|
||||
|
|
|
|||
82
surfsense_obsidian/src/status-modal.ts
Normal file
82
surfsense_obsidian/src/status-modal.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import { type App, Modal, Notice, Setting } from "obsidian";
|
||||
import type SurfSensePlugin from "./main";
|
||||
|
||||
/**
|
||||
* Read-only status panel. Mirrors what the settings tab used to embed inline,
|
||||
* but as a modal so it's reachable from the status bar / command palette.
|
||||
*
|
||||
* Subscribes to plugin status changes while open so the numbers stay live;
|
||||
* unsubscribes on close.
|
||||
*/
|
||||
export class StatusModal extends Modal {
|
||||
private readonly plugin: SurfSensePlugin;
|
||||
private readonly onChange = (): void => this.render();
|
||||
|
||||
constructor(app: App, plugin: SurfSensePlugin) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
this.titleEl.setText("SurfSense status");
|
||||
this.plugin.onStatusChange(this.onChange);
|
||||
this.render();
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
this.plugin.offStatusChange(this.onChange);
|
||||
this.contentEl.empty();
|
||||
}
|
||||
|
||||
private render(): void {
|
||||
const { contentEl, plugin } = this;
|
||||
contentEl.empty();
|
||||
const s = plugin.settings;
|
||||
|
||||
const rows: Array<[string, string]> = [
|
||||
["Status", plugin.lastStatus.kind],
|
||||
[
|
||||
"Last sync",
|
||||
s.lastSyncAt ? new Date(s.lastSyncAt).toLocaleString() : "—",
|
||||
],
|
||||
[
|
||||
"Last reconcile",
|
||||
s.lastReconcileAt
|
||||
? new Date(s.lastReconcileAt).toLocaleString()
|
||||
: "—",
|
||||
],
|
||||
["Files synced", String(s.filesSynced ?? 0)],
|
||||
["Queue depth", String(plugin.queueDepth)],
|
||||
[
|
||||
"Capabilities",
|
||||
plugin.serverCapabilities.length
|
||||
? plugin.serverCapabilities.join(", ")
|
||||
: "(not yet handshaken)",
|
||||
],
|
||||
];
|
||||
for (const [label, value] of rows) {
|
||||
new Setting(contentEl).setName(label).setDesc(value);
|
||||
}
|
||||
|
||||
new Setting(contentEl)
|
||||
.addButton((btn) =>
|
||||
btn
|
||||
.setButtonText("Re-sync entire vault")
|
||||
.setCta()
|
||||
.onClick(async () => {
|
||||
btn.setDisabled(true);
|
||||
try {
|
||||
await plugin.engine.maybeReconcile(true);
|
||||
new Notice("SurfSense: re-sync requested.");
|
||||
} catch (err) {
|
||||
new Notice(
|
||||
`SurfSense: re-sync failed — ${(err as Error).message}`,
|
||||
);
|
||||
} finally {
|
||||
btn.setDisabled(false);
|
||||
}
|
||||
}),
|
||||
)
|
||||
.addButton((btn) => btn.setButtonText("Close").onClick(() => this.close()));
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ import {
|
|||
TransientError,
|
||||
VaultNotRegisteredError,
|
||||
} from "./api-client";
|
||||
import { isExcluded } from "./excludes";
|
||||
import { isExcluded, isFolderFiltered } from "./excludes";
|
||||
import { buildNotePayload, computeContentHash } from "./payload";
|
||||
import { type BatchResult, PersistentQueue } from "./queue";
|
||||
import type {
|
||||
|
|
@ -31,14 +31,15 @@ export interface SyncEngineDeps {
|
|||
getSettings: () => SyncEngineSettings;
|
||||
saveSettings: (mut: (s: SyncEngineSettings) => void) => Promise<void>;
|
||||
setStatus: (s: StatusState) => void;
|
||||
onCapabilities: (caps: string[], apiVersion: string) => void;
|
||||
onCapabilities: (caps: string[]) => void;
|
||||
}
|
||||
|
||||
export interface SyncEngineSettings {
|
||||
vaultId: string;
|
||||
vaultName: string;
|
||||
connectorId: number | null;
|
||||
searchSpaceId: number | null;
|
||||
includeFolders: string[];
|
||||
excludeFolders: string[];
|
||||
excludePatterns: string[];
|
||||
includeAttachments: boolean;
|
||||
syncMode: "auto" | "manual";
|
||||
|
|
@ -55,7 +56,6 @@ const PENDING_DEBOUNCE_MS = 1500;
|
|||
export class SyncEngine {
|
||||
private readonly deps: SyncEngineDeps;
|
||||
private capabilities: string[] = [];
|
||||
private apiVersion: string | null = null;
|
||||
private pendingMdEdits = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
|
||||
constructor(deps: SyncEngineDeps) {
|
||||
|
|
@ -109,7 +109,7 @@ export class SyncEngine {
|
|||
const resp = await this.deps.apiClient.connect({
|
||||
searchSpaceId: settings.searchSpaceId,
|
||||
vaultId: settings.vaultId,
|
||||
vaultName: settings.vaultName,
|
||||
vaultName: this.deps.app.vault.getName(),
|
||||
});
|
||||
this.applyHealth(resp);
|
||||
await this.deps.saveSettings((s) => {
|
||||
|
|
@ -122,8 +122,7 @@ export class SyncEngine {
|
|||
|
||||
applyHealth(h: HealthResponse): void {
|
||||
this.capabilities = Array.isArray(h.capabilities) ? [...h.capabilities] : [];
|
||||
this.apiVersion = h.api_version ?? null;
|
||||
this.deps.onCapabilities(this.capabilities, this.apiVersion ?? "?");
|
||||
this.deps.onCapabilities(this.capabilities);
|
||||
}
|
||||
|
||||
// ---- vault event handlers --------------------------------------------
|
||||
|
|
@ -485,6 +484,9 @@ export class SyncEngine {
|
|||
}
|
||||
|
||||
private isExcluded(path: string, settings: SyncEngineSettings): boolean {
|
||||
if (isFolderFiltered(path, settings.includeFolders, settings.excludeFolders)) {
|
||||
return true;
|
||||
}
|
||||
return isExcluded(path, settings.excludePatterns);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@ export interface SurfsensePluginSettings {
|
|||
connectorId: number | null;
|
||||
/** UUID for the vault — lives here so Obsidian Sync replicates it across devices. */
|
||||
vaultId: string;
|
||||
vaultName: string;
|
||||
syncMode: "auto" | "manual";
|
||||
includeFolders: string[];
|
||||
excludeFolders: string[];
|
||||
excludePatterns: string[];
|
||||
includeAttachments: boolean;
|
||||
lastSyncAt: number | null;
|
||||
|
|
@ -24,8 +25,9 @@ export const DEFAULT_SETTINGS: SurfsensePluginSettings = {
|
|||
searchSpaceId: null,
|
||||
connectorId: null,
|
||||
vaultId: "",
|
||||
vaultName: "",
|
||||
syncMode: "auto",
|
||||
includeFolders: [],
|
||||
excludeFolders: [],
|
||||
excludePatterns: [".trash", "_attachments", "templates"],
|
||||
includeAttachments: false,
|
||||
lastSyncAt: null,
|
||||
|
|
@ -96,14 +98,12 @@ export interface ConnectResponse {
|
|||
connector_id: number;
|
||||
vault_id: string;
|
||||
search_space_id: number;
|
||||
api_version: string;
|
||||
capabilities: string[];
|
||||
server_time_utc: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface HealthResponse {
|
||||
api_version: string;
|
||||
capabilities: string[];
|
||||
server_time_utc: string;
|
||||
[key: string]: unknown;
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
/*
|
||||
* SurfSense Obsidian plugin styles. Kept tiny on purpose — Obsidian
|
||||
* theming should drive most of the look; we only add the bits we
|
||||
* cannot express via the standard PluginSettingTab/Setting components.
|
||||
* SurfSense Obsidian plugin styles. Status-bar widget only — the settings
|
||||
* tab uses Obsidian's stock Setting rows, no custom CSS needed.
|
||||
*/
|
||||
|
||||
.surfsense-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 0 6px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.surfsense-status--clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.surfsense-status__icon {
|
||||
|
|
@ -23,10 +22,6 @@
|
|||
height: 14px;
|
||||
}
|
||||
|
||||
.surfsense-status__text {
|
||||
font-size: var(--font-ui-smaller);
|
||||
}
|
||||
|
||||
.surfsense-status--ok .surfsense-status__icon {
|
||||
color: var(--color-green);
|
||||
}
|
||||
|
|
@ -42,25 +37,3 @@
|
|||
.surfsense-status--err .surfsense-status__icon {
|
||||
color: var(--color-red);
|
||||
}
|
||||
|
||||
.surfsense-settings__status {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(120px, max-content) 1fr;
|
||||
row-gap: 4px;
|
||||
column-gap: 12px;
|
||||
margin: 8px 0 16px;
|
||||
}
|
||||
|
||||
.surfsense-settings__status-row {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.surfsense-settings__status-label {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-ui-smaller);
|
||||
}
|
||||
|
||||
.surfsense-settings__status-value {
|
||||
font-size: var(--font-ui-smaller);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue