Various AT Protocol integrations with obsidian
at main 226 lines 7.6 kB view raw
1import { Modal, Notice, setIcon } from "obsidian"; 2import type AtmospherePlugin from "../main"; 3import { createSembleNote, deleteRecord } from "../lib"; 4import type { ATBookmarkItem } from "../sources/types"; 5import { EditItemModal } from "./editItemModal"; 6 7export class CardDetailModal extends Modal { 8 plugin: AtmospherePlugin; 9 item: ATBookmarkItem; 10 onSuccess?: () => void; 11 noteInput: HTMLTextAreaElement | null = null; 12 13 constructor(plugin: AtmospherePlugin, item: ATBookmarkItem, onSuccess?: () => void) { 14 super(plugin.app); 15 this.plugin = plugin; 16 this.item = item; 17 this.onSuccess = onSuccess; 18 } 19 20 onOpen() { 21 const { contentEl } = this; 22 contentEl.empty(); 23 contentEl.addClass("atmosphere-detail-modal"); 24 25 void this.renderBody(contentEl); 26 27 const collections = this.item.getCollections(); 28 if (collections.length > 0) { 29 this.renderCollectionsSection(contentEl, collections); 30 } 31 32 if (this.item.canAddTags()) { 33 this.renderTagsSection(contentEl); 34 } 35 36 if (this.item.canAddNotes() && this.item.getAttachedNotes) { 37 this.renderNotesSection(contentEl); 38 } 39 40 if (this.item.canAddNotes()) { 41 this.renderAddNoteForm(contentEl); 42 } 43 44 const footer = contentEl.createEl("div", { cls: "atmosphere-detail-footer" }); 45 const footerLeft = footer.createEl("div", { cls: "atmosphere-detail-footer-left" }); 46 const source = this.item.getSource(); 47 const sourceBadge = footerLeft.createEl("span", { cls: `atmosphere-badge atmosphere-badge-${source}` }); 48 setIcon(sourceBadge, sourceIconId(source)); 49 footerLeft.createEl("span", { 50 text: `Created ${new Date(this.item.getCreatedAt()).toLocaleDateString()}`, 51 cls: "atmosphere-detail-date", 52 }); 53 54 if (this.item.canEdit()) { 55 const editBtn = footer.createEl("button", { cls: "atmosphere-detail-edit-btn" }); 56 setIcon(editBtn, "pencil"); 57 editBtn.addEventListener("click", () => { 58 this.close(); 59 new EditItemModal(this.plugin, this.item, this.onSuccess).open(); 60 }); 61 } 62 } 63 64 private async renderBody(contentEl: HTMLElement) { 65 const body = contentEl.createEl("div", { cls: "atmosphere-detail-body" }); 66 67 const title = this.item.getTitle(); 68 if (title) { 69 body.createEl("h2", { text: title, cls: "atmosphere-detail-title" }); 70 } 71 72 const imageUrl = await this.item.getImageUrl(); 73 if (imageUrl) { 74 const img = body.createEl("img", { cls: "atmosphere-detail-image" }); 75 img.src = imageUrl; 76 img.alt = title || "Image"; 77 } 78 79 const description = this.item.getDescription(); 80 if (description) { 81 body.createEl("p", { text: description, cls: "atmosphere-detail-description" }); 82 } 83 84 const siteName = this.item.getSiteName(); 85 if (siteName) { 86 const metaGrid = body.createEl("div", { cls: "atmosphere-detail-meta" }); 87 const metaItem = metaGrid.createEl("div", { cls: "atmosphere-detail-meta-item" }); 88 metaItem.createEl("span", { text: "Site", cls: "atmosphere-detail-meta-label" }); 89 metaItem.createEl("span", { text: siteName, cls: "atmosphere-detail-meta-value" }); 90 } 91 92 const url = this.item.getUrl(); 93 if (url) { 94 const linkWrapper = body.createEl("div", { cls: "atmosphere-detail-link-wrapper" }); 95 const link = linkWrapper.createEl("a", { 96 text: url, 97 href: url, 98 cls: "atmosphere-detail-link", 99 }); 100 link.setAttr("target", "_blank"); 101 } 102 } 103 104 private renderTagsSection(contentEl: HTMLElement) { 105 const tags = this.item.getTags(); 106 if (tags.length === 0) return; 107 const section = contentEl.createEl("div", { cls: "atmosphere-detail-tags" }); 108 section.createEl("h3", { text: "Tags", cls: "atmosphere-detail-section-title" }); 109 const container = section.createEl("div", { cls: "atmosphere-item-tags" }); 110 for (const tag of tags) { 111 container.createEl("span", { text: tag, cls: "atmosphere-tag" }); 112 } 113 } 114 115 private renderCollectionsSection(contentEl: HTMLElement, collections: Array<{ uri: string; name: string; source: string }>) { 116 const section = contentEl.createEl("div", { cls: "atmosphere-detail-collections" }); 117 section.createEl("span", { text: "In collections", cls: "atmosphere-detail-collections-label" }); 118 const badges = section.createEl("div", { cls: "atmosphere-detail-collections-badges" }); 119 for (const collection of collections) { 120 const badge = badges.createEl("span", { cls: "atmosphere-collection" }); 121 const iconEl = badge.createEl("span", { cls: "atmosphere-collection-source-icon" }); 122 setIcon(iconEl, sourceIconId(collection.source as "semble" | "bookmark" | "margin")); 123 badge.createEl("span", { text: collection.name }); 124 } 125 } 126 127 private renderNotesSection(contentEl: HTMLElement) { 128 const notes = this.item.getAttachedNotes?.(); 129 if (!notes || notes.length === 0) return; 130 131 const notesSection = contentEl.createEl("div", { cls: "atmosphere-semble-detail-notes-section" }); 132 notesSection.createEl("h3", { text: "Notes", cls: "atmosphere-detail-section-title" }); 133 134 for (const note of notes) { 135 const noteEl = notesSection.createEl("div", { cls: "atmosphere-semble-detail-note" }); 136 137 const noteContent = noteEl.createEl("div", { cls: "atmosphere-semble-detail-note-content" }); 138 const noteIcon = noteContent.createEl("span", { cls: "atmosphere-semble-detail-note-icon" }); 139 setIcon(noteIcon, "message-square"); 140 noteContent.createEl("p", { text: note.text, cls: "atmosphere-semble-detail-note-text" }); 141 142 const deleteBtn = noteEl.createEl("button", { cls: "atmosphere-semble-note-delete-btn" }); 143 setIcon(deleteBtn, "trash-2"); 144 deleteBtn.addEventListener("click", () => { 145 void this.handleDeleteNote(note.uri); 146 }); 147 } 148 } 149 150 private renderAddNoteForm(contentEl: HTMLElement) { 151 const formSection = contentEl.createEl("div", { cls: "atmosphere-semble-detail-add-note" }); 152 formSection.createEl("h3", { text: "Add a note", cls: "atmosphere-detail-section-title" }); 153 154 const form = formSection.createEl("div", { cls: "atmosphere-semble-add-note-form" }); 155 156 this.noteInput = form.createEl("textarea", { 157 cls: "atmosphere-textarea atmosphere-semble-note-input", 158 attr: { placeholder: "Write a note about this item..." }, 159 }); 160 161 const addBtn = form.createEl("button", { text: "Add note", cls: "atmosphere-btn atmosphere-btn-primary" }); 162 addBtn.addEventListener("click", () => { void this.handleAddNote(); }); 163 } 164 165 private async handleAddNote() { 166 if (!this.plugin.client.loggedIn || !this.noteInput) return; 167 168 const text = this.noteInput.value.trim(); 169 if (!text) { 170 new Notice("Please enter a note"); 171 return; 172 } 173 174 try { 175 await createSembleNote( 176 this.plugin.client, 177 this.plugin.settings.did!, 178 text, 179 { uri: this.item.getUri(), cid: this.item.getCid() } 180 ); 181 182 new Notice("Note added"); 183 this.close(); 184 this.onSuccess?.(); 185 } catch (err) { 186 const message = err instanceof Error ? err.message : String(err); 187 new Notice(`Failed to add note: ${message}`); 188 } 189 } 190 191 private async handleDeleteNote(noteUri: string) { 192 if (!this.plugin.client.loggedIn) return; 193 194 const rkey = noteUri.split("/").pop(); 195 if (!rkey) { 196 new Notice("Invalid note uri"); 197 return; 198 } 199 200 try { 201 await deleteRecord( 202 this.plugin.client, 203 this.plugin.settings.did!, 204 "network.cosmik.card", 205 rkey 206 ); 207 208 new Notice("Note deleted"); 209 this.close(); 210 this.onSuccess?.(); 211 } catch (err) { 212 const message = err instanceof Error ? err.message : String(err); 213 new Notice(`Failed to delete note: ${message}`); 214 } 215 } 216 217 onClose() { 218 this.contentEl.empty(); 219 } 220} 221 222function sourceIconId(source: "semble" | "bookmark" | "margin"): string { 223 if (source === "semble") return "atmosphere-semble"; 224 if (source === "margin") return "atmosphere-margin"; 225 return "bookmark"; 226}