AT protocol bookmarking platforms in obsidian
at margin 211 lines 5.7 kB view raw
1import { Modal, Notice } from "obsidian"; 2import type { Record } from "@atcute/atproto/types/repo/listRecords"; 3import type { Main as Bookmark } from "../lexicons/types/community/lexicon/bookmarks/bookmark"; 4import type ATmarkPlugin from "../main"; 5import { putRecord, deleteRecord } from "../lib"; 6 7type BookmarkRecord = Record & { value: Bookmark }; 8 9export class EditBookmarkModal extends Modal { 10 plugin: ATmarkPlugin; 11 record: BookmarkRecord; 12 onSuccess?: () => void; 13 tagInputs: HTMLInputElement[] = []; 14 15 constructor(plugin: ATmarkPlugin, record: BookmarkRecord, onSuccess?: () => void) { 16 super(plugin.app); 17 this.plugin = plugin; 18 this.record = record; 19 this.onSuccess = onSuccess; 20 } 21 22 onOpen() { 23 const { contentEl } = this; 24 contentEl.empty(); 25 contentEl.addClass("atmark-modal"); 26 27 contentEl.createEl("h2", { text: "Edit bookmark tags" }); 28 29 if (!this.plugin.client) { 30 contentEl.createEl("p", { text: "Not connected." }); 31 return; 32 } 33 34 const existingTags = this.record.value.tags || []; 35 36 const form = contentEl.createEl("div", { cls: "atmark-form" }); 37 38 // Tags section 39 const tagsGroup = form.createEl("div", { cls: "atmark-form-group" }); 40 tagsGroup.createEl("label", { text: "Tags" }); 41 42 const tagsContainer = tagsGroup.createEl("div", { cls: "atmark-tags-container" }); 43 44 // Render existing tags 45 for (const tag of existingTags) { 46 this.addTagInput(tagsContainer, tag); 47 } 48 49 // Add empty input for new tag 50 this.addTagInput(tagsContainer, ""); 51 52 // Add tag button 53 const addTagBtn = tagsGroup.createEl("button", { 54 text: "Add tag", 55 cls: "atmark-btn atmark-btn-secondary" 56 }); 57 addTagBtn.addEventListener("click", (e) => { 58 e.preventDefault(); 59 this.addTagInput(tagsContainer, ""); 60 }); 61 62 // Action buttons 63 const actions = contentEl.createEl("div", { cls: "atmark-modal-actions" }); 64 65 const deleteBtn = actions.createEl("button", { 66 text: "Delete", 67 cls: "atmark-btn atmark-btn-danger" 68 }); 69 deleteBtn.addEventListener("click", () => { this.confirmDelete(contentEl); }); 70 71 actions.createEl("div", { cls: "atmark-spacer" }); 72 73 const cancelBtn = actions.createEl("button", { 74 text: "Cancel", 75 cls: "atmark-btn atmark-btn-secondary" 76 }); 77 cancelBtn.addEventListener("click", () => { this.close(); }); 78 79 const saveBtn = actions.createEl("button", { 80 text: "Save", 81 cls: "atmark-btn atmark-btn-primary" 82 }); 83 saveBtn.addEventListener("click", () => { void this.saveChanges(); }); 84 } 85 86 private addTagInput(container: HTMLElement, value: string) { 87 const tagRow = container.createEl("div", { cls: "atmark-tag-row" }); 88 89 const input = tagRow.createEl("input", { 90 type: "text", 91 cls: "atmark-input", 92 value, 93 attr: { placeholder: "Enter tag..." } 94 }); 95 this.tagInputs.push(input); 96 97 const removeBtn = tagRow.createEl("button", { 98 text: "×", 99 cls: "atmark-btn atmark-btn-secondary atmark-tag-remove-btn" 100 }); 101 removeBtn.addEventListener("click", (e) => { 102 e.preventDefault(); 103 tagRow.remove(); 104 this.tagInputs = this.tagInputs.filter(i => i !== input); 105 }); 106 } 107 108 private confirmDelete(contentEl: HTMLElement) { 109 contentEl.empty(); 110 contentEl.createEl("h2", { text: "Delete bookmark" }); 111 contentEl.createEl("p", { text: "Delete this bookmark?", cls: "atmark-warning-text" }); 112 113 const actions = contentEl.createEl("div", { cls: "atmark-modal-actions" }); 114 115 const cancelBtn = actions.createEl("button", { 116 text: "Cancel", 117 cls: "atmark-btn atmark-btn-secondary" 118 }); 119 cancelBtn.addEventListener("click", () => { 120 void this.onOpen(); 121 }); 122 123 const confirmBtn = actions.createEl("button", { 124 text: "Delete", 125 cls: "atmark-btn atmark-btn-danger" 126 }); 127 confirmBtn.addEventListener("click", () => { void this.deleteBookmark(); }); 128 } 129 130 private async deleteBookmark() { 131 if (!this.plugin.client) return; 132 133 const { contentEl } = this; 134 contentEl.empty(); 135 contentEl.createEl("p", { text: "Deleting bookmark..." }); 136 137 try { 138 const rkey = this.record.uri.split("/").pop(); 139 if (!rkey) { 140 contentEl.empty(); 141 contentEl.createEl("p", { text: "Invalid bookmark uri.", cls: "atmark-error" }); 142 return; 143 } 144 145 await deleteRecord( 146 this.plugin.client, 147 this.plugin.settings.identifier, 148 "community.lexicon.bookmarks.bookmark", 149 rkey 150 ); 151 152 new Notice("Bookmark deleted"); 153 this.close(); 154 this.onSuccess?.(); 155 } catch (err) { 156 contentEl.empty(); 157 const message = err instanceof Error ? err.message : String(err); 158 contentEl.createEl("p", { text: `Failed to delete: ${message}`, cls: "atmark-error" }); 159 } 160 } 161 162 private async saveChanges() { 163 if (!this.plugin.client) return; 164 165 const { contentEl } = this; 166 contentEl.empty(); 167 contentEl.createEl("p", { text: "Saving changes..." }); 168 169 try { 170 // Get non-empty unique tags 171 const tags = [...new Set( 172 this.tagInputs 173 .map(input => input.value.trim()) 174 .filter(tag => tag.length > 0) 175 )]; 176 177 const rkey = this.record.uri.split("/").pop(); 178 if (!rkey) { 179 contentEl.empty(); 180 contentEl.createEl("p", { text: "Invalid bookmark uri.", cls: "atmark-error" }); 181 return; 182 } 183 184 // Update the record with new tags 185 const updatedRecord: Bookmark = { 186 ...this.record.value, 187 tags, 188 }; 189 190 await putRecord( 191 this.plugin.client, 192 this.plugin.settings.identifier, 193 "community.lexicon.bookmarks.bookmark", 194 rkey, 195 updatedRecord 196 ); 197 198 new Notice("Tags updated"); 199 this.close(); 200 this.onSuccess?.(); 201 } catch (err) { 202 contentEl.empty(); 203 const message = err instanceof Error ? err.message : String(err); 204 contentEl.createEl("p", { text: `Failed to save: ${message}`, cls: "atmark-error" }); 205 } 206 } 207 208 onClose() { 209 this.contentEl.empty(); 210 } 211}