Various AT Protocol integrations with obsidian

enable bookmark sources in settings (#31)

* set enabled sources via settings

* remove header

* check responses

authored by

treethought and committed by
GitHub
75ca4608 2530df67

+100 -68
+1 -1
manifest.json
··· 1 1 { 2 2 "id": "atmosphere", 3 3 "name": "Atmosphere", 4 - "version": "0.1.17", 4 + "version": "0.1.18", 5 5 "minAppVersion": "0.15.0", 6 6 "description": "Various integrations with AT Protocol.", 7 7 "author": "treethought",
+1 -1
package.json
··· 1 1 { 2 2 "name": "obsidian-atmosphere", 3 - "version": "0.1.17", 3 + "version": "0.1.18", 4 4 "description": "Various integrations with AT Protocol.", 5 5 "main": "main.js", 6 6 "type": "module",
+13 -3
src/lib/atproto.ts
··· 2 2 import type { ActorIdentifier, Nsid } from "@atcute/lexicons"; 3 3 4 4 export async function getRecord(client: Client, repo: string, collection: string, rkey: string) { 5 - return await client.get("com.atproto.repo.getRecord", { 5 + const resp = await client.get("com.atproto.repo.getRecord", { 6 6 params: { 7 7 repo: repo as ActorIdentifier, 8 8 collection: collection as Nsid, 9 9 rkey, 10 10 }, 11 11 }); 12 + if (!resp.ok) { 13 + throw new Error(`Failed to get record: ${resp.status} ${resp.data?.message || ""}`); 14 + } 15 + return resp 12 16 } 13 17 14 18 export async function deleteRecord(client: Client, repo: string, collection: string, rkey: string) { 15 - return await client.post("com.atproto.repo.deleteRecord", { 19 + const resp = await client.post("com.atproto.repo.deleteRecord", { 16 20 input: { 17 21 repo: repo as ActorIdentifier, 18 22 collection: collection as Nsid, 19 23 rkey, 20 24 }, 21 25 }); 26 + if (!resp.ok) { 27 + throw new Error(`Failed to delete record: ${resp.status} ${resp.data?.message || ""}`); 28 + } 22 29 } 23 30 24 31 export async function putRecord<T = unknown>(client: Client, repo: string, collection: string, rkey: string, record: T) { 25 - return await client.post("com.atproto.repo.putRecord", { 32 + const resp = await client.post("com.atproto.repo.putRecord", { 26 33 input: { 27 34 repo: repo as ActorIdentifier, 28 35 collection: collection as Nsid, ··· 30 37 record: record as unknown as { [key: string]: unknown }, 31 38 }, 32 39 }); 40 + if (!resp.ok) { 41 + throw new Error(`Failed to put record: ${resp.status} ${resp.data?.message || ""}`); 42 + } 33 43 } 34 44 35 45 export async function getProfile(client: Client, actor: string) {
+15 -1
src/main.ts
··· 131 131 } 132 132 133 133 async loadSettings() { 134 - this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData() as Partial<AtProtoSettings>); 134 + const saved = await this.loadData() as Partial<AtProtoSettings>; 135 + this.settings = Object.assign({}, DEFAULT_SETTINGS, saved, { 136 + bookmarks: { 137 + ...DEFAULT_SETTINGS.bookmarks, 138 + ...saved?.bookmarks, 139 + enabledSources: { 140 + ...DEFAULT_SETTINGS.bookmarks.enabledSources, 141 + ...saved?.bookmarks?.enabledSources, 142 + }, 143 + }, 144 + publish: { 145 + ...DEFAULT_SETTINGS.publish, 146 + ...saved?.publish, 147 + }, 148 + }); 135 149 } 136 150 137 151 async saveSettings() {
+40 -2
src/settings.ts
··· 1 1 import { App, Notice, PluginSettingTab, Setting } from "obsidian"; 2 2 import type AtmospherePlugin from "./main"; 3 3 import { isActorIdentifier } from "@atcute/lexicons/syntax"; 4 - import { VIEW_TYPE_ATMOSPHERE_BOOKMARKS } from "./views/bookmarks"; 4 + import { BookmarksView, VIEW_TYPE_ATMOSPHERE_BOOKMARKS } from "./views/bookmarks"; 5 5 import { VIEW_ATMOSPHERE_STANDARD_FEED } from "./views/standardfeed"; 6 + 6 7 7 8 export interface AtProtoSettings { 8 9 did?: string; 9 10 clipDir: string; 11 + bookmarks: { 12 + enabledSources: Record<string, boolean>; 13 + } 10 14 publish: { 11 15 useFirstHeaderAsTitle: boolean; 12 16 }; ··· 14 18 15 19 export const DEFAULT_SETTINGS: AtProtoSettings = { 16 20 clipDir: "AtmosphereClips", 21 + bookmarks: { 22 + enabledSources: { "semble": true, "margin": true, "bookmark": true }, 23 + }, 17 24 publish: { 18 25 useFirstHeaderAsTitle: false, 19 - } 26 + }, 20 27 }; 21 28 22 29 export class SettingTab extends PluginSettingTab { ··· 115 122 }) 116 123 ); 117 124 125 + 126 + new Setting(containerEl).setName("Bookmarks").setHeading() 127 + 128 + const bookmarkSources: Array<{ name: string; label: string; desc: string }> = [ 129 + { name: "semble", label: "Semble", desc: "Include Semble bookmarks" }, 130 + { name: "margin", label: "Margin", desc: "Include Margin bookmarks" }, 131 + { name: "bookmark", label: "Community", desc: "Include community lexcion bookmarks" }, 132 + ]; 133 + 134 + for (const source of bookmarkSources) { 135 + new Setting(containerEl) 136 + .setName(source.label) 137 + .setDesc(source.desc) 138 + .addToggle(toggle => 139 + toggle 140 + .setValue(this.plugin.settings.bookmarks?.enabledSources?.[source.name] ?? true) 141 + .onChange(async (value) => { 142 + this.plugin.settings.bookmarks.enabledSources[source.name] = value; 143 + await this.plugin.saveSettings(); 144 + for (const leaf of this.app.workspace.getLeavesOfType(VIEW_TYPE_ATMOSPHERE_BOOKMARKS)) { 145 + let view = leaf.view as BookmarksView; 146 + if (view instanceof BookmarksView) { 147 + await view.render() 148 + } 149 + 150 + } 151 + }) 152 + ); 153 + } 154 + 155 + new Setting(containerEl).setName("Publishing").setHeading() 118 156 new Setting(containerEl) 119 157 .setName("Use first header as publish title") 120 158 .setDesc('Use the first level one header instead of filename when no title property is set')
+30 -60
src/views/bookmarks.ts
··· 12 12 13 13 export const VIEW_TYPE_ATMOSPHERE_BOOKMARKS = "atmosphere-bookmarks"; 14 14 15 - type SourceName = "semble" | "bookmark" | "margin"; 16 - 17 15 export class BookmarksView extends ItemView { 18 16 plugin: AtmospherePlugin; 19 - activeSources: Set<SourceName> = new Set(["semble", "margin", "bookmark"]); 20 17 selectedCollections: Set<string> = new Set(); 21 18 selectedTags: Set<string> = new Set(); 22 - sources: Map<SourceName, DataSource> = new Map(); 19 + sources: Map<string, DataSource> = new Map(); 23 20 searchQuery: string = ""; 24 21 private cachedItems: ATBookmarkItem[] = []; 25 22 ··· 29 26 } 30 27 31 28 initSources() { 29 + this.sources.clear(); 32 30 if (this.plugin.settings.did) { 31 + const { enabledSources } = this.plugin.settings.bookmarks; 33 32 const repo = this.plugin.settings.did; 34 - this.sources.set("semble", new SembleSource(this.plugin.client, repo)); 35 - this.sources.set("bookmark", new BookmarkSource(this.plugin.client, repo)); 36 - this.sources.set("margin", new MarginSource(this.plugin.client, repo)); 33 + if (enabledSources["semble"]) this.sources.set("semble", new SembleSource(this.plugin.client, repo)); 34 + if (enabledSources["bookmark"]) this.sources.set("bookmark", new BookmarkSource(this.plugin.client, repo)); 35 + if (enabledSources["margin"]) this.sources.set("margin", new MarginSource(this.plugin.client, repo)); 37 36 } 38 37 } 39 38 ··· 50 49 } 51 50 52 51 private get activeDatasources(): DataSource[] { 53 - return Array.from(this.activeSources, s => this.sources.get(s)!); 52 + return Array.from(this.sources.values()); 54 53 } 55 54 56 55 async onOpen() { ··· 123 122 } 124 123 125 124 async render() { 125 + this.initSources(); 126 126 const container = this.contentEl; 127 127 container.empty(); 128 128 container.addClass("atmosphere-view"); ··· 184 184 } 185 185 186 186 private renderHeader(container: HTMLElement) { 187 - const header = container.createEl("div", { cls: "atmosphere-header" }); 188 - 189 - const topRow = header.createEl("div", { cls: "atmosphere-header-top-row" }); 190 - 191 - const sourceSelector = topRow.createEl("div", { cls: "atmosphere-source-selector" }); 192 - const sources: SourceName[] = ["semble", "margin", "bookmark"]; 193 - 194 - for (const source of sources) { 195 - const label = sourceSelector.createEl("label", { cls: "atmosphere-source-option" }); 196 - 197 - const checkbox = label.createEl("input", { 198 - type: "checkbox", 199 - cls: "atmosphere-source-toggle", 200 - }); 201 - checkbox.checked = this.activeSources.has(source); 202 - checkbox.addEventListener("change", () => { 203 - if (checkbox.checked) { 204 - this.activeSources.add(source); 205 - } else { 206 - this.activeSources.delete(source); 207 - } 208 - void this.render(); 209 - }); 210 - 211 - label.createEl("span", { 212 - text: source.charAt(0).toUpperCase() + source.slice(1), 213 - cls: "atmosphere-source-text", 214 - }); 215 - } 216 - 217 - const refreshBtn = topRow.createEl("button", { 218 - cls: "atmosphere-refresh-btn", 219 - attr: { "aria-label": "Refresh bookmarks" }, 220 - }); 221 - setIcon(refreshBtn, "refresh-cw"); 222 - refreshBtn.addEventListener("click", () => { 223 - refreshBtn.addClass("atmosphere-refresh-btn-spinning"); 224 - void this.refresh(); 225 - refreshBtn.removeClass("atmosphere-refresh-btn-spinning"); 226 - }); 227 - 228 187 this.renderFilters(container); 229 188 } 230 189 ··· 232 191 const filtersEl = container.createEl("div", { cls: "atmosphere-filters" }); 233 192 const toolbarRow = filtersEl.createEl("div", { cls: "atmosphere-filter-toolbar" }); 234 193 235 - const collectionSources = (["semble", "margin"] as SourceName[]).filter(s => this.activeSources.has(s)); 194 + const collectionSources = (["semble", "margin"]).filter(s => this.sources.has(s)); 236 195 if (collectionSources.length > 0) { 237 196 this.renderCollectionsFilterBtn(toolbarRow, collectionSources); 238 197 } 239 198 240 - const tagSources = (["margin", "bookmark"] as SourceName[]).filter(s => this.activeSources.has(s)); 199 + const tagSources = (["margin", "bookmark"]).filter(s => this.sources.has(s)); 241 200 if (tagSources.length > 0) { 242 201 this.renderTagsFilterBtn(toolbarRow, tagSources); 243 202 } ··· 250 209 this.renderGrid(this.cachedItems); 251 210 }); 252 211 212 + const refreshBtn = toolbarRow.createEl("button", { 213 + cls: "atmosphere-refresh-btn", 214 + attr: { "aria-label": "Refresh bookmarks" }, 215 + }); 216 + setIcon(refreshBtn, "refresh-cw"); 217 + refreshBtn.addEventListener("click", () => { 218 + refreshBtn.addClass("atmosphere-refresh-btn-spinning"); 219 + void this.refresh(); 220 + refreshBtn.removeClass("atmosphere-refresh-btn-spinning"); 221 + }); 222 + 253 223 if (this.selectedCollections.size > 0 || this.selectedTags.size > 0) { 254 224 const chipsRow = filtersEl.createEl("div", { cls: "atmosphere-active-chips-row" }); 255 225 if (this.selectedCollections.size > 0 && collectionSources.length > 0) { ··· 261 231 } 262 232 } 263 233 264 - private renderCollectionsFilterBtn(toolbar: HTMLElement, collectionSources: SourceName[]) { 234 + private renderCollectionsFilterBtn(toolbar: HTMLElement, collectionSources: string[]) { 265 235 const group = toolbar.createEl("div", { cls: "atmosphere-filter-btn-group" }); 266 236 267 237 const pickerBtn = group.createEl("button", { ··· 287 257 }); 288 258 } 289 259 290 - private renderTagsFilterBtn(toolbar: HTMLElement, tagSources: SourceName[]) { 260 + private renderTagsFilterBtn(toolbar: HTMLElement, tagSources: string[]) { 291 261 const group = toolbar.createEl("div", { cls: "atmosphere-filter-btn-group" }); 292 262 293 263 const pickerBtn = group.createEl("button", { ··· 308 278 } 309 279 } 310 280 311 - private async fetchAllCollections(sources: SourceName[]): Promise<(SourceFilter & { source: SourceName })[]> { 281 + private async fetchAllCollections(sources: string[]): Promise<(SourceFilter & { source: string })[]> { 312 282 const results = await Promise.all( 313 283 sources.map(async s => { 314 284 const items = await (this.sources.get(s)?.getAvailableCollections?.() ?? Promise.resolve([])); ··· 319 289 return results.flat().filter(c => !seen.has(c.value) && Boolean(seen.add(c.value))); 320 290 } 321 291 322 - private async fetchAllTags(sources: SourceName[]): Promise<(SourceFilter & { source: SourceName })[]> { 292 + private async fetchAllTags(sources: string[]): Promise<(SourceFilter & { source: string })[]> { 323 293 const results = await Promise.all( 324 294 sources.map(async s => { 325 295 const items = await (this.sources.get(s)?.getAvilableTags?.() ?? Promise.resolve([])); ··· 330 300 return results.flat().filter(t => !seen.has(t.value) && Boolean(seen.add(t.value))); 331 301 } 332 302 333 - private async renderActiveCollectionChips(chipsRow: HTMLElement, collectionSources: SourceName[]) { 303 + private async renderActiveCollectionChips(chipsRow: HTMLElement, collectionSources: string[]) { 334 304 const collections = await this.fetchAllCollections(collectionSources); 335 305 for (const c of collections) { 336 306 if (!this.selectedCollections.has(c.value)) continue; ··· 346 316 } 347 317 } 348 318 349 - private async renderActiveTagChips(chipsRow: HTMLElement, tagSources: SourceName[]) { 319 + private async renderActiveTagChips(chipsRow: HTMLElement, tagSources: string[]) { 350 320 const tags = await this.fetchAllTags(tagSources); 351 321 for (const t of tags) { 352 322 if (!this.selectedTags.has(t.value)) continue; ··· 362 332 } 363 333 } 364 334 365 - private async showCollectionsMenu(e: MouseEvent, sources: SourceName[]) { 335 + private async showCollectionsMenu(e: MouseEvent, sources: string[]) { 366 336 e.stopPropagation(); 367 337 const collections = (await this.fetchAllCollections(sources)) 368 338 .sort((a, b) => (a.label ?? a.value).localeCompare(b.label ?? b.value)); ··· 382 352 menu.showAtMouseEvent(e); 383 353 } 384 354 385 - private async showTagsMenu(e: MouseEvent, sources: SourceName[]) { 355 + private async showTagsMenu(e: MouseEvent, sources: string[]) { 386 356 e.stopPropagation(); 387 357 const tags = (await this.fetchAllTags(sources)) 388 358 .sort((a, b) => (a.label ?? a.value).localeCompare(b.label ?? b.value)); ··· 503 473 async onClose() { } 504 474 } 505 475 506 - function sourceIconId(source: "semble" | "bookmark" | "margin"): string { 476 + function sourceIconId(source: string): string { 507 477 if (source === "semble") return "atmosphere-semble"; 508 478 if (source === "margin") return "atmosphere-margin"; 509 479 return "bookmark";