From de346b9fcf2ee630d174a50b026cb20a37b15da7 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sun, 11 May 2025 22:25:19 +0100 Subject: [PATCH] Add sync status inside editor --- .../obsidian-plugin/src/vault-link-plugin.ts | 12 ++++ .../editor-sync-line/editor-sync-line.scss | 43 +++++++++++++++ .../editor-sync-line/editor-sync-line.ts | 55 +++++++++++++++++++ frontend/sync-client/src/sync-client.ts | 2 +- 4 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 frontend/obsidian-plugin/src/views/editor-sync-line/editor-sync-line.scss create mode 100644 frontend/obsidian-plugin/src/views/editor-sync-line/editor-sync-line.ts diff --git a/frontend/obsidian-plugin/src/vault-link-plugin.ts b/frontend/obsidian-plugin/src/vault-link-plugin.ts index 40b9ed5..f675d4d 100644 --- a/frontend/obsidian-plugin/src/vault-link-plugin.ts +++ b/frontend/obsidian-plugin/src/vault-link-plugin.ts @@ -15,8 +15,10 @@ import { SyncClient, rateLimit } from "sync-client"; import { ObsidianFileSystemOperations } from "./obsidian-file-system"; import { SyncSettingsTab } from "./views/settings/settings-tab"; import { registerConsoleForLogging } from "./utils/register-console-for-logging"; +import { updateEditorStatusDisplay } from "./views/editor-sync-line/editor-sync-line"; export default class VaultLinkPlugin extends Plugin { + private readonly disposables: (() => void)[] = []; private settingsTab: SyncSettingsTab | undefined; private client!: SyncClient; private readonly rateLimitedUpdatesPerFile = new Map< @@ -74,11 +76,21 @@ export default class VaultLinkPlugin extends Plugin { this.app.workspace.onLayoutReady(async () => { this.registerEditorEvents(); void this.client.start(); + + const interval = setInterval(() => { + updateEditorStatusDisplay(this.app.workspace, this.client); + }, 200); + this.disposables.push(() => { + clearInterval(interval); + }); }); } public onunload(): void { this.client.stop(); + this.disposables.forEach((disposable) => { + disposable(); + }); } public openSettings(): void { diff --git a/frontend/obsidian-plugin/src/views/editor-sync-line/editor-sync-line.scss b/frontend/obsidian-plugin/src/views/editor-sync-line/editor-sync-line.scss new file mode 100644 index 0000000..a430ac3 --- /dev/null +++ b/frontend/obsidian-plugin/src/views/editor-sync-line/editor-sync-line.scss @@ -0,0 +1,43 @@ +.vault-link-sync-status { + position: absolute; + right: var(--size-4-4); + top: var(--size-4-2); + opacity: 0.7; + cursor: pointer; + + > span { + opacity: 0; + position: absolute; + min-width: 200px; + text-align: right; + padding-right: var(--size-2-2); + + top: 50%; + left: 0; + transform: translateY(-50%) translateX(-100%) translateY(-2px); + transition: opacity 200ms; + } + + &:hover { + > span { + opacity: 1; + } + } + + > .icon { + line-height: 0; + } + + &.loading > .icon { + animation: spin 2s linear infinite; + + @keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } + } + } +} diff --git a/frontend/obsidian-plugin/src/views/editor-sync-line/editor-sync-line.ts b/frontend/obsidian-plugin/src/views/editor-sync-line/editor-sync-line.ts new file mode 100644 index 0000000..6775068 --- /dev/null +++ b/frontend/obsidian-plugin/src/views/editor-sync-line/editor-sync-line.ts @@ -0,0 +1,55 @@ +import type { Workspace } from "obsidian"; +import { FileView, setIcon } from "obsidian"; +import type { SyncClient } from "sync-client"; +import { DocumentUpdateStatus } from "sync-client"; +import "./editor-sync-line.scss"; + +export function updateEditorStatusDisplay( + workspace: Workspace, + client: SyncClient +): void { + workspace.iterateAllLeaves((leaf) => { + if (leaf.view instanceof FileView) { + const filePath = leaf.view.file?.path; + if (filePath == null) { + return; + } + const parent = leaf.view.contentEl.querySelector(".cm-editor"); + if (parent == null) { + return; + } + + const element = + parent.querySelector(".vault-link-sync-status") ?? + parent.createDiv( + { + cls: "vault-link-sync-status" + }, + (el) => { + el.createSpan({ text: "VaultLink sync state" }); + el.createDiv({ + cls: "icon" + }); + } + ); + + const isLoading = + client.getDocumentSyncingStatus(filePath) == + DocumentUpdateStatus.SYNCING; + + if (isLoading) { + element.classList.add("loading"); + } else { + element.classList.remove("loading"); + } + + const iconContainer = element.querySelector(".icon"); + if (iconContainer != null) { + setIcon( + iconContainer as HTMLElement, // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion + isLoading ? "loader" : "circle-check" + ); + } + } + }); +} diff --git a/frontend/sync-client/src/sync-client.ts b/frontend/sync-client/src/sync-client.ts index e64ea6d..94c446e 100644 --- a/frontend/sync-client/src/sync-client.ts +++ b/frontend/sync-client/src/sync-client.ts @@ -16,7 +16,7 @@ import { ConnectionStatus } from "./services/connection-status"; import { UnrestrictedSyncer } from "./sync-operations/unrestricted-syncer"; import { rateLimit } from "./utils/rate-limit"; import { v4 as uuidv4 } from "uuid"; -import { NetworkConnectionStatus } from "./types/network-connection-status"; +import type { NetworkConnectionStatus } from "./types/network-connection-status"; import { DocumentUpdateStatus } from "./types/document-update-status"; export class SyncClient {