AT protocol bookmarking platforms in obsidian
at client-cache 258 lines 8.1 kB view raw
1import { Modal, Notice } from "obsidian"; 2import type ATmarkPlugin from "../main"; 3import { getCollections, getCollectionLinks, createCollectionLink, getRecord, deleteRecord } from "../lib"; 4import type { Main as Collection } from "../lexicons/types/network/cosmik/collection"; 5import type { Main as CollectionLink } from "../lexicons/types/network/cosmik/collectionLink"; 6 7interface CollectionRecord { 8 uri: string; 9 cid: string; 10 value: Collection; 11} 12 13interface CollectionLinkRecord { 14 uri: string; 15 value: CollectionLink; 16} 17 18interface CollectionState { 19 collection: CollectionRecord; 20 isSelected: boolean; 21 wasSelected: boolean; // Original state to track changes 22 linkUri?: string; // URI of existing link (for deletion) 23} 24 25export class EditCardModal extends Modal { 26 plugin: ATmarkPlugin; 27 cardUri: string; 28 cardCid: string; 29 onSuccess?: () => void; 30 collectionStates: CollectionState[] = []; 31 32 constructor(plugin: ATmarkPlugin, cardUri: string, cardCid: string, onSuccess?: () => void) { 33 super(plugin.app); 34 this.plugin = plugin; 35 this.cardUri = cardUri; 36 this.cardCid = cardCid; 37 this.onSuccess = onSuccess; 38 } 39 40 async onOpen() { 41 const { contentEl } = this; 42 contentEl.empty(); 43 contentEl.addClass("atmark-modal"); 44 45 contentEl.createEl("h2", { text: "Edit collections" }); 46 47 if (!this.plugin.client) { 48 contentEl.createEl("p", { text: "Not connected." }); 49 return; 50 } 51 52 const loading = contentEl.createEl("p", { text: "Loading..." }); 53 54 try { 55 const [collectionsResp, linksResp] = await Promise.all([ 56 getCollections(this.plugin.client, this.plugin.settings.identifier), 57 getCollectionLinks(this.plugin.client, this.plugin.settings.identifier), 58 ]); 59 60 loading.remove(); 61 62 if (!collectionsResp.ok) { 63 contentEl.createEl("p", { text: "Failed to load collections.", cls: "atmark-error" }); 64 return; 65 } 66 67 const collections = collectionsResp.data.records as unknown as CollectionRecord[]; 68 const links = (linksResp.ok ? linksResp.data.records : []) as unknown as CollectionLinkRecord[]; 69 70 if (collections.length === 0) { 71 contentEl.createEl("p", { text: "No collections found. Create a collection first." }); 72 return; 73 } 74 75 const cardLinks = links.filter(link => link.value.card.uri === this.cardUri); 76 const linkedCollectionUris = new Map<string, string>(); 77 for (const link of cardLinks) { 78 linkedCollectionUris.set(link.value.collection.uri, link.uri); 79 } 80 81 this.collectionStates = collections.map(collection => ({ 82 collection, 83 isSelected: linkedCollectionUris.has(collection.uri), 84 wasSelected: linkedCollectionUris.has(collection.uri), 85 linkUri: linkedCollectionUris.get(collection.uri), 86 })); 87 88 this.renderCollectionList(contentEl); 89 } catch (err) { 90 loading.remove(); 91 const message = err instanceof Error ? err.message : String(err); 92 contentEl.createEl("p", { text: `Error: ${message}`, cls: "atmark-error" }); 93 } 94 } 95 96 private renderCollectionList(contentEl: HTMLElement) { 97 const list = contentEl.createEl("div", { cls: "atmark-collection-list" }); 98 99 for (const state of this.collectionStates) { 100 const item = list.createEl("label", { cls: "atmark-collection-item" }); 101 102 const checkbox = item.createEl("input", { type: "checkbox", cls: "atmark-collection-checkbox" }); 103 checkbox.checked = state.isSelected; 104 checkbox.addEventListener("change", () => { 105 state.isSelected = checkbox.checked; 106 this.updateSaveButton(); 107 }); 108 109 const info = item.createEl("div", { cls: "atmark-collection-item-info" }); 110 info.createEl("span", { text: state.collection.value.name, cls: "atmark-collection-item-name" }); 111 if (state.collection.value.description) { 112 info.createEl("span", { text: state.collection.value.description, cls: "atmark-collection-item-desc" }); 113 } 114 } 115 116 const actions = contentEl.createEl("div", { cls: "atmark-modal-actions" }); 117 118 const deleteBtn = actions.createEl("button", { text: "Delete", cls: "atmark-btn atmark-btn-danger" }); 119 deleteBtn.addEventListener("click", () => { this.confirmDelete(contentEl); }); 120 121 actions.createEl("div", { cls: "atmark-spacer" }); 122 123 const cancelBtn = actions.createEl("button", { text: "Cancel", cls: "atmark-btn atmark-btn-secondary" }); 124 cancelBtn.addEventListener("click", () => { this.close(); }); 125 126 const saveBtn = actions.createEl("button", { text: "Save", cls: "atmark-btn atmark-btn-primary" }); 127 saveBtn.id = "atmark-save-btn"; 128 saveBtn.disabled = true; 129 saveBtn.addEventListener("click", () => { void this.saveChanges(); }); 130 } 131 132 private confirmDelete(contentEl: HTMLElement) { 133 contentEl.empty(); 134 contentEl.createEl("h2", { text: "Delete card" }); 135 contentEl.createEl("p", { text: "Delete this card?", cls: "atmark-warning-text" }); 136 137 const actions = contentEl.createEl("div", { cls: "atmark-modal-actions" }); 138 139 const cancelBtn = actions.createEl("button", { text: "Cancel", cls: "atmark-btn atmark-btn-secondary" }); 140 cancelBtn.addEventListener("click", () => { 141 void this.onOpen(); 142 }); 143 144 const confirmBtn = actions.createEl("button", { text: "Delete", cls: "atmark-btn atmark-btn-danger" }); 145 confirmBtn.addEventListener("click", () => { void this.deleteCard(); }); 146 } 147 148 private async deleteCard() { 149 if (!this.plugin.client) return; 150 151 const { contentEl } = this; 152 contentEl.empty(); 153 contentEl.createEl("p", { text: "Deleting card..." }); 154 155 try { 156 const rkey = this.cardUri.split("/").pop(); 157 if (!rkey) { 158 contentEl.empty(); 159 contentEl.createEl("p", { text: "Invalid card uri.", cls: "atmark-error" }); 160 return; 161 } 162 163 await deleteRecord( 164 this.plugin.client, 165 this.plugin.settings.identifier, 166 "network.cosmik.card", 167 rkey 168 ); 169 170 new Notice("Card deleted"); 171 this.close(); 172 this.onSuccess?.(); 173 } catch (err) { 174 contentEl.empty(); 175 const message = err instanceof Error ? err.message : String(err); 176 contentEl.createEl("p", { text: `Failed to delete: ${message}`, cls: "atmark-error" }); 177 } 178 } 179 180 private updateSaveButton() { 181 const saveBtn = document.getElementById("atmark-save-btn") as HTMLButtonElement | null; 182 if (!saveBtn) return; 183 184 const hasChanges = this.collectionStates.some(s => s.isSelected !== s.wasSelected); 185 saveBtn.disabled = !hasChanges; 186 } 187 188 private async saveChanges() { 189 if (!this.plugin.client) return; 190 191 const { contentEl } = this; 192 contentEl.empty(); 193 contentEl.createEl("p", { text: "Saving changes..." }); 194 195 try { 196 const toAdd = this.collectionStates.filter(s => s.isSelected && !s.wasSelected); 197 const toRemove = this.collectionStates.filter(s => !s.isSelected && s.wasSelected); 198 199 for (const state of toRemove) { 200 if (state.linkUri) { 201 const rkey = state.linkUri.split("/").pop(); 202 if (rkey) { 203 await deleteRecord( 204 this.plugin.client, 205 this.plugin.settings.identifier, 206 "network.cosmik.collectionLink", 207 rkey 208 ); 209 } 210 } 211 } 212 213 for (const state of toAdd) { 214 const collectionRkey = state.collection.uri.split("/").pop(); 215 if (!collectionRkey) continue; 216 217 const collectionResp = await getRecord( 218 this.plugin.client, 219 this.plugin.settings.identifier, 220 "network.cosmik.collection", 221 collectionRkey 222 ); 223 224 if (!collectionResp.ok || !collectionResp.data.cid) continue; 225 226 await createCollectionLink( 227 this.plugin.client, 228 this.plugin.settings.identifier, 229 this.cardUri, 230 this.cardCid, 231 state.collection.uri, 232 String(collectionResp.data.cid) 233 ); 234 } 235 236 const addedCount = toAdd.length; 237 const removedCount = toRemove.length; 238 const messages: string[] = []; 239 if (addedCount > 0) messages.push(`Added to ${addedCount} collection${addedCount > 1 ? "s" : ""}`); 240 if (removedCount > 0) messages.push(`Removed from ${removedCount} collection${removedCount > 1 ? "s" : ""}`); 241 242 if (messages.length > 0) { 243 new Notice(messages.join(". ")); 244 } 245 246 this.close(); 247 this.onSuccess?.(); 248 } catch (err) { 249 contentEl.empty(); 250 const message = err instanceof Error ? err.message : String(err); 251 contentEl.createEl("p", { text: `Failed to save: ${message}`, cls: "atmark-error" }); 252 } 253 } 254 255 onClose() { 256 this.contentEl.empty(); 257 } 258}