AT protocol bookmarking platforms in obsidian
1import { Notice, Plugin, WorkspaceLeaf } from "obsidian";
2import type { Client } from "@atcute/client";
3import { DEFAULT_SETTINGS, AtProtoSettings, SettingTab } from "./settings";
4import { createAuthenticatedClient, createPublicClient } from "./auth";
5import { getProfile } from "./lib";
6import { ATmarkView, VIEW_TYPE_ATMARK } from "views/atmark";
7import type { ProfileData } from "components/profileIcon";
8
9export default class ATmarkPlugin extends Plugin {
10 settings: AtProtoSettings = DEFAULT_SETTINGS;
11 client: Client | null = null;
12 profile: ProfileData | null = null;
13
14 async onload() {
15 await this.loadSettings();
16 await this.initClient();
17
18 this.registerView(VIEW_TYPE_ATMARK, (leaf) => {
19 return new ATmarkView(leaf, this);
20 });
21
22 // eslint-disable-next-line obsidianmd/ui/sentence-case
23 this.addRibbonIcon("layers", "Open ATmark", () => {
24 void this.activateView(VIEW_TYPE_ATMARK);
25 });
26
27 this.addCommand({
28 id: "view",
29 name: "Open view",
30 callback: () => { void this.activateView(VIEW_TYPE_ATMARK); },
31 });
32
33 this.addSettingTab(new SettingTab(this.app, this));
34 }
35
36
37 private async initClient() {
38 const { identifier, appPassword } = this.settings;
39 if (identifier && appPassword) {
40 try {
41 this.client = await createAuthenticatedClient({ identifier, password: appPassword });
42 await this.fetchProfile();
43 new Notice("Connected");
44 } catch (err) {
45 const message = err instanceof Error ? err.message : String(err);
46 new Notice(`Auth failed: ${message}`);
47 this.client = createPublicClient();
48 this.profile = null;
49 }
50 } else {
51 this.client = createPublicClient();
52 this.profile = null;
53 }
54 }
55
56 private async fetchProfile() {
57 if (!this.client || !this.settings.identifier) {
58 this.profile = null;
59 return;
60 }
61 try {
62 const resp = await getProfile(this.client, this.settings.identifier);
63 if (resp.ok) {
64 this.profile = {
65 did: resp.data.did,
66 handle: resp.data.handle,
67 displayName: resp.data.displayName,
68 avatar: resp.data.avatar,
69 };
70 } else {
71 this.profile = null;
72 }
73 } catch (e) {
74 console.error("Failed to fetch profile:", e);
75 this.profile = null;
76 }
77 }
78
79 async refreshClient() {
80 await this.initClient();
81 }
82
83
84 async activateView(v: string) {
85 const { workspace } = this.app;
86
87 let leaf: WorkspaceLeaf | null = null;
88 const leaves = workspace.getLeavesOfType(v);
89
90 if (leaves.length > 0) {
91 // A leaf with our view already exists, use that
92 leaf = leaves[0] as WorkspaceLeaf;
93 void workspace.revealLeaf(leaf);
94 return;
95 }
96
97 // Our view could not be found in the workspace, create a new leaf
98 leaf = workspace.getMostRecentLeaf()
99 await leaf?.setViewState({ type: v, active: true });
100
101 // "Reveal" the leaf in case it is in a collapsed sidebar
102 if (leaf) {
103 void workspace.revealLeaf(leaf);
104 }
105 }
106
107 async loadSettings() {
108 this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData() as Partial<AtProtoSettings>);
109 }
110
111 async saveSettings() {
112 await this.saveData(this.settings);
113 }
114
115 onunload() { }
116}