cbb0d81 antonApr 8, 2026 api

create version-view.ts via macOS

A /sublimated/undefined +174 -0
1import { ItemView, Notice, WorkspaceLeaf } from "obsidian";
2import type SublimatedPlugin from "./main";
3import { SublimatedApi, 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 changes
30 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 if (!this.plugin.settings.apiToken) {
52 contentEl.createEl("p", {
53 text: "API token required for version history.",
54 cls: "sbl-version-empty",
55 });
56 contentEl.createEl("p", {
57 text: "Configure in Settings → sublimated",
58 cls: "sbl-version-hint",
59 });
60 return;
61 }
62
63 // read xattrs to get space + item ID
64 const meta = this.plugin.lastMetadata;
65 if (!meta?.space || !meta?.id) {
66 contentEl.createEl("p", {
67 text: "Not a sublimated file.",
68 cls: "sbl-version-empty",
69 });
70 return;
71 }
72
73 contentEl.createEl("h4", {
74 text: activeFile.name,
75 cls: "sbl-version-filename",
76 });
77
78 const loading = contentEl.createEl("p", {
79 text: "Loading versions...",
80 cls: "sbl-version-loading",
81 });
82
83 try {
84 const api = new SublimatedApi(this.plugin.settings.apiToken);
85 const versions = await api.getVersions(meta.space, meta.id);
86 loading.remove();
87
88 if (versions.length === 0) {
89 contentEl.createEl("p", {
90 text: "No versions found.",
91 cls: "sbl-version-empty",
92 });
93 return;
94 }
95
96 const list = contentEl.createEl("div", {
97 cls: "sbl-version-list",
98 });
99 for (const v of versions) {
100 this.renderVersion(list, v, meta.url);
101 }
102 } catch (e) {
103 loading.remove();
104 contentEl.createEl("p", {
105 text: `Error: ${e instanceof Error ? e.message : "Unknown"}`,
106 cls: "sbl-version-error",
107 });
108 }
109 }
110
111 private renderVersion(
112 container: HTMLElement,
113 v: Version,
114 fileUrl: string | null
115 ): void {
116 const item = container.createDiv({ cls: "sbl-version-item" });
117
118 const header = item.createDiv({ cls: "sbl-version-header" });
119 header.createEl("span", {
120 text: `v${v.number}`,
121 cls: "sbl-version-number",
122 });
123 header.createEl("span", {
124 text: v.authorName,
125 cls: "sbl-version-author",
126 });
127
128 const time = item.createDiv({ cls: "sbl-version-time" });
129 time.createEl("span", {
130 text: formatRelativeTime(v.createdAt),
131 attr: { title: new Date(v.createdAt).toLocaleString() },
132 });
133
134 if (v.message) {
135 item.createDiv({
136 text: v.message,
137 cls: "sbl-version-message",
138 });
139 }
140
141 if (fileUrl) {
142 const webUrl = `https://${fileUrl}?v=${v.number}`;
143 const link = item.createEl("a", {
144 text: "view on web",
145 cls: "sbl-version-web-link",
146 href: webUrl,
147 });
148 link.addEventListener("click", (e) => {
149 e.preventDefault();
150 window.open(webUrl);
151 });
152 }
153 }
154
155 async onClose(): Promise<void> {
156 // cleanup handled by obsidian
157 }
158}
159
160function formatRelativeTime(iso: string): string {
161 const now = Date.now();
162 const then = new Date(iso).getTime();
163 const diff = now - then;
164 const seconds = Math.floor(diff / 1000);
165 const minutes = Math.floor(seconds / 60);
166 const hours = Math.floor(minutes / 60);
167 const days = Math.floor(hours / 24);
168
169 if (days > 30) return new Date(iso).toLocaleDateString();
170 if (days > 0) return `${days}d ago`;
171 if (hours > 0) return `${hours}h ago`;
172 if (minutes > 0) return `${minutes}m ago`;
173 return "just now";
174}