1import { ItemView, Notice, WorkspaceLeaf } from "obsidian";2import type SublimatedPlugin from "./main";3import { SublimatedApi, AuthRequiredError, type Version } from "./api";4
5export const VERSION_VIEW_TYPE = "sublimated-versions";6
7export class VersionView extends ItemView {8 plugin: SublimatedPlugin;9
10 constructor(leaf: WorkspaceLeaf, plugin: SublimatedPlugin) {11 super(leaf);12 this.plugin = plugin;13 }14
15 getViewType(): string {16 return VERSION_VIEW_TYPE;17 }18
19 getDisplayText(): string {20 return "sublimated versions";21 }22
23 getIcon(): string {24 return "history";25 }26
27 async onOpen(): Promise<void> {28 await this.refresh();29 // refresh when active file changes30 this.registerEvent(31 this.app.workspace.on("active-leaf-change", () => {32 this.refresh();33 })34 );35 }36
37 async refresh(): Promise<void> {38 const { contentEl } = this;39 contentEl.empty();40 contentEl.addClass("sbl-version-view");41
42 const activeFile = this.app.workspace.getActiveFile();43 if (!activeFile) {44 contentEl.createEl("p", {45 text: "No file open.",46 cls: "sbl-version-empty",47 });48 return;49 }50
51 // read xattrs to get space + item ID52 const meta = this.plugin.lastMetadata;53 if (!meta?.space || !meta?.id) {54 contentEl.createEl("p", {55 text: "Not a sublimated file.",56 cls: "sbl-version-empty",57 });58 return;59 }60
61 contentEl.createEl("h4", {62 text: activeFile.name,63 cls: "sbl-version-filename",64 });65
66 const loading = contentEl.createEl("p", {67 text: "Loading versions...",68 cls: "sbl-version-loading",69 });70
71 try {72 const api = new SublimatedApi(this.plugin.settings.apiToken);73 const versions = await api.getVersions(meta.space, meta.id);74 loading.remove();75
76 if (versions.length === 0) {77 contentEl.createEl("p", {78 text: "No versions found.",79 cls: "sbl-version-empty",80 });81 return;82 }83
84 const list = contentEl.createEl("div", {85 cls: "sbl-version-list",86 });87 const groups = groupByDay(versions);88 for (const group of groups) {89 list.createEl("div", {90 text: group.label,91 cls: "sbl-version-day",92 });93 for (const v of group.versions) {94 this.renderVersion(list, v, meta.url);95 }96 }97 } catch (e) {98 loading.remove();99 contentEl.createEl("p", {100 text: `Error: ${e instanceof Error ? e.message : "Unknown"}`,101 cls: "sbl-version-error",102 });103 }104 }105
106 private renderVersion(107 container: HTMLElement,108 v: Version,109 fileUrl: string | null110 ): void {111 const item = container.createDiv({ cls: "sbl-version-item" });112
113 const header = item.createDiv({ cls: "sbl-version-header" });114 header.createEl("span", {115 text: `v${v.number}`,116 cls: "sbl-version-number",117 });118 header.createEl("span", {119 text: v.authorName,120 cls: "sbl-version-author",121 });122
123 const time = item.createDiv({ cls: "sbl-version-time" });124 time.createEl("span", {125 text: formatRelativeTime(v.createdAt),126 attr: { title: new Date(v.createdAt).toLocaleString() },127 });128
129 if (v.message) {130 item.createDiv({131 text: v.message,132 cls: "sbl-version-message",133 });134 }135
136 if (fileUrl) {137 const webUrl = `https://${fileUrl}/~versions/v${v.number}`;138 const link = item.createEl("a", {139 text: "view on web",140 cls: "sbl-version-web-link",141 href: webUrl,142 });143 link.addEventListener("click", (e) => {144 e.preventDefault();145 window.open(webUrl);146 });147 }148 }149
150 async onClose(): Promise<void> {151 // cleanup handled by obsidian152 }153}154
155interface DayGroup {156 label: string;157 versions: Version[];158}159
160function groupByDay(versions: Version[]): DayGroup[] {161 const groups = new Map<string, Version[]>();162 const order: string[] = [];163
164 for (const v of versions) {165 const label = formatDayLabel(v.createdAt);166 if (!groups.has(label)) {167 groups.set(label, []);168 order.push(label);169 }170 groups.get(label)!.push(v);171 }172
173 return order.map((label) => ({174 label,175 versions: groups.get(label)!,176 }));177}178
179function formatDayLabel(iso: string): string {180 const now = new Date();181 const date = new Date(iso);182
183 const nowDay = new Date(now.getFullYear(), now.getMonth(), now.getDate());184 const thenDay = new Date(185 date.getFullYear(),186 date.getMonth(),187 date.getDate()188 );189 const days = Math.round(190 (nowDay.getTime() - thenDay.getTime()) / (1000 * 60 * 60 * 24)191 );192
193 if (days === 0) return "Today";194 if (days === 1) return "Yesterday";195 if (days < 7) return `${days} days ago`;196 return date.toLocaleDateString("en-US", {197 month: "short",198 day: "numeric",199 });200}201
202function formatRelativeTime(iso: string): string {203 const now = Date.now();204 const then = new Date(iso).getTime();205 const diff = now - then;206 const seconds = Math.floor(diff / 1000);207 const minutes = Math.floor(seconds / 60);208 const hours = Math.floor(minutes / 60);209 const days = Math.floor(hours / 24);210
211 if (days > 30) return new Date(iso).toLocaleDateString();212 if (days > 0) return `${days}d ago`;213 if (hours > 0) return `${hours}h ago`;214 if (minutes > 0) return `${minutes}m ago`;215 return "just now";216} v7 of v8
commit 47f08c1
message edit version-view.ts via macOS
author anton
date Apr 11, 2026
size 4.9 KB