Various AT Protocol integrations with obsidian
20
fork

Configure Feed

Select the types of activity you want to include in your feed.

unify edit modal, support adding to other osurce collections

+379 -836
-238
src/components/editBookmarkModal.ts
··· 1 - import { Modal, Notice } from "obsidian"; 2 - import type { Record } from "@atcute/atproto/types/repo/listRecords"; 3 - import type { Main as Bookmark } from "../lexicons/types/community/lexicon/bookmarks/bookmark"; 4 - import type AtmospherePlugin from "../main"; 5 - import { putRecord, deleteRecord, getBookmarks } from "../lib"; 6 - 7 - type BookmarkRecord = Record & { value: Bookmark }; 8 - 9 - interface TagState { 10 - tag: string; 11 - isSelected: boolean; 12 - } 13 - 14 - export class EditBookmarkModal extends Modal { 15 - plugin: AtmospherePlugin; 16 - record: BookmarkRecord; 17 - onSuccess?: () => void; 18 - tagStates: TagState[] = []; 19 - newTagInput: HTMLInputElement | null = null; 20 - 21 - constructor(plugin: AtmospherePlugin, record: BookmarkRecord, onSuccess?: () => void) { 22 - super(plugin.app); 23 - this.plugin = plugin; 24 - this.record = record; 25 - this.onSuccess = onSuccess; 26 - } 27 - 28 - async onOpen() { 29 - const { contentEl } = this; 30 - contentEl.empty(); 31 - contentEl.addClass("atmosphere-modal"); 32 - 33 - contentEl.createEl("h2", { text: "Edit bookmark" }); 34 - 35 - if (!this.plugin.client) { 36 - contentEl.createEl("p", { text: "Not connected." }); 37 - return; 38 - } 39 - 40 - const loading = contentEl.createEl("p", { text: "Loading..." }); 41 - 42 - try { 43 - const bookmarksResp = await getBookmarks(this.plugin.client, this.plugin.settings.did!); 44 - loading.remove(); 45 - 46 - const bookmarks = (bookmarksResp.ok ? bookmarksResp.data.records : []) as unknown as BookmarkRecord[]; 47 - 48 - const allTags = new Set<string>(); 49 - for (const bookmark of bookmarks) { 50 - if (bookmark.value.tags) { 51 - for (const tag of bookmark.value.tags) { 52 - allTags.add(tag); 53 - } 54 - } 55 - } 56 - 57 - const currentTags = new Set(this.record.value.tags || []); 58 - this.tagStates = Array.from(allTags).sort().map(tag => ({ 59 - tag, 60 - isSelected: currentTags.has(tag), 61 - })); 62 - 63 - this.renderForm(contentEl); 64 - } catch (err) { 65 - loading.remove(); 66 - const message = err instanceof Error ? err.message : String(err); 67 - contentEl.createEl("p", { text: `Error: ${message}`, cls: "atmosphere-error" }); 68 - } 69 - } 70 - 71 - private renderForm(contentEl: HTMLElement) { 72 - const form = contentEl.createEl("div", { cls: "atmosphere-form" }); 73 - 74 - const tagsGroup = form.createEl("div", { cls: "atmosphere-form-group" }); 75 - tagsGroup.createEl("label", { text: "Tags" }); 76 - 77 - const tagsList = tagsGroup.createEl("div", { cls: "atmosphere-tag-list" }); 78 - for (const state of this.tagStates) { 79 - this.addTagChip(tagsList, state); 80 - } 81 - 82 - const newTagRow = tagsGroup.createEl("div", { cls: "atmosphere-tag-row" }); 83 - this.newTagInput = newTagRow.createEl("input", { 84 - type: "text", 85 - cls: "atmosphere-input", 86 - attr: { placeholder: "Add new tag..." } 87 - }); 88 - const addBtn = newTagRow.createEl("button", { 89 - text: "Add", 90 - cls: "atmosphere-btn atmosphere-btn-secondary", 91 - attr: { type: "button" } 92 - }); 93 - addBtn.addEventListener("click", () => { 94 - const value = this.newTagInput?.value.trim(); 95 - if (value && !this.tagStates.some(s => s.tag === value)) { 96 - const newState = { tag: value, isSelected: true }; 97 - this.tagStates.push(newState); 98 - this.addTagChip(tagsList, newState); 99 - if (this.newTagInput) this.newTagInput.value = ""; 100 - } 101 - }); 102 - 103 - const actions = contentEl.createEl("div", { cls: "atmosphere-modal-actions" }); 104 - 105 - const deleteBtn = actions.createEl("button", { 106 - text: "Delete", 107 - cls: "atmosphere-btn atmosphere-btn-danger" 108 - }); 109 - deleteBtn.addEventListener("click", () => { this.confirmDelete(contentEl); }); 110 - 111 - actions.createEl("div", { cls: "atmosphere-spacer" }); 112 - 113 - const cancelBtn = actions.createEl("button", { 114 - text: "Cancel", 115 - cls: "atmosphere-btn atmosphere-btn-secondary" 116 - }); 117 - cancelBtn.addEventListener("click", () => { this.close(); }); 118 - 119 - const saveBtn = actions.createEl("button", { 120 - text: "Save", 121 - cls: "atmosphere-btn atmosphere-btn-primary" 122 - }); 123 - saveBtn.addEventListener("click", () => { void this.saveChanges(); }); 124 - } 125 - 126 - private addTagChip(container: HTMLElement, state: TagState) { 127 - const item = container.createEl("label", { cls: "atmosphere-tag-item" }); 128 - const checkbox = item.createEl("input", { type: "checkbox" }); 129 - checkbox.checked = state.isSelected; 130 - checkbox.addEventListener("change", () => { 131 - state.isSelected = checkbox.checked; 132 - }); 133 - item.createEl("span", { text: state.tag }); 134 - } 135 - 136 - private confirmDelete(contentEl: HTMLElement) { 137 - contentEl.empty(); 138 - contentEl.createEl("h2", { text: "Delete bookmark" }); 139 - contentEl.createEl("p", { text: "Delete this bookmark?", cls: "atmosphere-warning-text" }); 140 - 141 - const actions = contentEl.createEl("div", { cls: "atmosphere-modal-actions" }); 142 - 143 - const cancelBtn = actions.createEl("button", { 144 - text: "Cancel", 145 - cls: "atmosphere-btn atmosphere-btn-secondary" 146 - }); 147 - cancelBtn.addEventListener("click", () => { 148 - void this.onOpen(); 149 - }); 150 - 151 - const confirmBtn = actions.createEl("button", { 152 - text: "Delete", 153 - cls: "atmosphere-btn atmosphere-btn-danger" 154 - }); 155 - confirmBtn.addEventListener("click", () => { void this.deleteBookmark(); }); 156 - } 157 - 158 - private async deleteBookmark() { 159 - if (!this.plugin.client) return; 160 - 161 - const { contentEl } = this; 162 - contentEl.empty(); 163 - contentEl.createEl("p", { text: "Deleting bookmark..." }); 164 - 165 - try { 166 - const rkey = this.record.uri.split("/").pop(); 167 - if (!rkey) { 168 - contentEl.empty(); 169 - contentEl.createEl("p", { text: "Invalid bookmark uri.", cls: "atmosphere-error" }); 170 - return; 171 - } 172 - 173 - await deleteRecord( 174 - this.plugin.client, 175 - this.plugin.settings.did!, 176 - "community.lexicon.bookmarks.bookmark", 177 - rkey 178 - ); 179 - 180 - new Notice("Bookmark deleted"); 181 - this.close(); 182 - this.onSuccess?.(); 183 - } catch (err) { 184 - contentEl.empty(); 185 - const message = err instanceof Error ? err.message : String(err); 186 - contentEl.createEl("p", { text: `Failed to delete: ${message}`, cls: "atmosphere-error" }); 187 - } 188 - } 189 - 190 - private async saveChanges() { 191 - if (!this.plugin.client) return; 192 - 193 - const { contentEl } = this; 194 - contentEl.empty(); 195 - contentEl.createEl("p", { text: "Saving changes..." }); 196 - 197 - try { 198 - const selectedTags = this.tagStates.filter(s => s.isSelected).map(s => s.tag); 199 - const newTag = this.newTagInput?.value.trim(); 200 - if (newTag && !selectedTags.includes(newTag)) { 201 - selectedTags.push(newTag); 202 - } 203 - const tags = [...new Set(selectedTags)]; 204 - 205 - const rkey = this.record.uri.split("/").pop(); 206 - if (!rkey) { 207 - contentEl.empty(); 208 - contentEl.createEl("p", { text: "Invalid bookmark uri.", cls: "atmosphere-error" }); 209 - return; 210 - } 211 - 212 - const updatedRecord: Bookmark = { 213 - ...this.record.value, 214 - tags, 215 - }; 216 - 217 - await putRecord( 218 - this.plugin.client, 219 - this.plugin.settings.did!, 220 - "community.lexicon.bookmarks.bookmark", 221 - rkey, 222 - updatedRecord 223 - ); 224 - 225 - new Notice("Tags updated"); 226 - this.close(); 227 - this.onSuccess?.(); 228 - } catch (err) { 229 - contentEl.empty(); 230 - const message = err instanceof Error ? err.message : String(err); 231 - contentEl.createEl("p", { text: `Failed to save: ${message}`, cls: "atmosphere-error" }); 232 - } 233 - } 234 - 235 - onClose() { 236 - this.contentEl.empty(); 237 - } 238 - }
-258
src/components/editCardModal.ts
··· 1 - import { Modal, Notice } from "obsidian"; 2 - import type AtmospherePlugin from "../main"; 3 - import type { Main as Collection } from "../lexicons/types/network/cosmik/collection"; 4 - import type { Main as CollectionLink } from "../lexicons/types/network/cosmik/collectionLink"; 5 - import {getSembleCollections, getSembleCollectionLinks, deleteRecord, getRecord, createSembleCollectionLink} from "../lib"; 6 - 7 - interface CollectionRecord { 8 - uri: string; 9 - cid: string; 10 - value: Collection; 11 - } 12 - 13 - interface CollectionLinkRecord { 14 - uri: string; 15 - value: CollectionLink; 16 - } 17 - 18 - interface 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 - 25 - export class EditCardModal extends Modal { 26 - plugin: AtmospherePlugin; 27 - cardUri: string; 28 - cardCid: string; 29 - onSuccess?: () => void; 30 - collectionStates: CollectionState[] = []; 31 - 32 - constructor(plugin: AtmospherePlugin, 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("atmosphere-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 - getSembleCollections(this.plugin.client, this.plugin.settings.did!), 57 - getSembleCollectionLinks(this.plugin.client, this.plugin.settings.did!), 58 - ]); 59 - 60 - loading.remove(); 61 - 62 - if (!collectionsResp.ok) { 63 - contentEl.createEl("p", { text: "Failed to load collections.", cls: "atmosphere-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: "atmosphere-error" }); 93 - } 94 - } 95 - 96 - private renderCollectionList(contentEl: HTMLElement) { 97 - const list = contentEl.createEl("div", { cls: "atmosphere-collection-list" }); 98 - 99 - for (const state of this.collectionStates) { 100 - const item = list.createEl("label", { cls: "atmosphere-collection-item" }); 101 - 102 - const checkbox = item.createEl("input", { type: "checkbox", cls: "atmosphere-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: "atmosphere-collection-item-info" }); 110 - info.createEl("span", { text: state.collection.value.name, cls: "atmosphere-collection-item-name" }); 111 - if (state.collection.value.description) { 112 - info.createEl("span", { text: state.collection.value.description, cls: "atmosphere-collection-item-desc" }); 113 - } 114 - } 115 - 116 - const actions = contentEl.createEl("div", { cls: "atmosphere-modal-actions" }); 117 - 118 - const deleteBtn = actions.createEl("button", { text: "Delete", cls: "atmosphere-btn atmosphere-btn-danger" }); 119 - deleteBtn.addEventListener("click", () => { this.confirmDelete(contentEl); }); 120 - 121 - actions.createEl("div", { cls: "atmosphere-spacer" }); 122 - 123 - const cancelBtn = actions.createEl("button", { text: "Cancel", cls: "atmosphere-btn atmosphere-btn-secondary" }); 124 - cancelBtn.addEventListener("click", () => { this.close(); }); 125 - 126 - const saveBtn = actions.createEl("button", { text: "Save", cls: "atmosphere-btn atmosphere-btn-primary" }); 127 - saveBtn.id = "atmosphere-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: "atmosphere-warning-text" }); 136 - 137 - const actions = contentEl.createEl("div", { cls: "atmosphere-modal-actions" }); 138 - 139 - const cancelBtn = actions.createEl("button", { text: "Cancel", cls: "atmosphere-btn atmosphere-btn-secondary" }); 140 - cancelBtn.addEventListener("click", () => { 141 - void this.onOpen(); 142 - }); 143 - 144 - const confirmBtn = actions.createEl("button", { text: "Delete", cls: "atmosphere-btn atmosphere-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: "atmosphere-error" }); 160 - return; 161 - } 162 - 163 - await deleteRecord( 164 - this.plugin.client, 165 - this.plugin.settings.did!, 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: "atmosphere-error" }); 177 - } 178 - } 179 - 180 - private updateSaveButton() { 181 - const saveBtn = document.getElementById("atmosphere-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.did!, 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.did!, 220 - "network.cosmik.collection", 221 - collectionRkey 222 - ); 223 - 224 - if (!collectionResp.ok || !collectionResp.data.cid) continue; 225 - 226 - await createSembleCollectionLink( 227 - this.plugin.client, 228 - this.plugin.settings.did!, 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: "atmosphere-error" }); 252 - } 253 - } 254 - 255 - onClose() { 256 - this.contentEl.empty(); 257 - } 258 - }
+286
src/components/editItemModal.ts
··· 1 + import { Modal, Notice, setIcon } from "obsidian"; 2 + import type AtmospherePlugin from "../main"; 3 + import type { ATBookmarkItem, DataSource } from "../sources/types"; 4 + import { SembleSource } from "../sources/semble"; 5 + import { MarginSource } from "../sources/margin"; 6 + import { BookmarkSource } from "../sources/community"; 7 + 8 + interface CollectionState { 9 + uri: string; 10 + name: string; 11 + description?: string; 12 + source: "semble" | "margin"; 13 + isSelected: boolean; 14 + wasSelected: boolean; 15 + linkUri?: string; 16 + } 17 + 18 + interface TagState { 19 + tag: string; 20 + isSelected: boolean; 21 + } 22 + 23 + export class EditItemModal extends Modal { 24 + plugin: AtmospherePlugin; 25 + item: ATBookmarkItem; 26 + onSuccess?: () => void; 27 + collectionStates: CollectionState[] = []; 28 + tagStates: TagState[] = []; 29 + newTagInput: HTMLInputElement | null = null; 30 + private sembleSource!: SembleSource; 31 + private marginSource!: MarginSource; 32 + private itemSource!: DataSource; 33 + 34 + constructor(plugin: AtmospherePlugin, item: ATBookmarkItem, onSuccess?: () => void) { 35 + super(plugin.app); 36 + this.plugin = plugin; 37 + this.item = item; 38 + this.onSuccess = onSuccess; 39 + } 40 + 41 + async onOpen() { 42 + const { contentEl } = this; 43 + contentEl.empty(); 44 + contentEl.addClass("atmosphere-modal"); 45 + contentEl.createEl("h2", { text: "Edit item" }); 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 did = this.plugin.settings.did!; 56 + this.sembleSource = new SembleSource(this.plugin.client, did); 57 + this.marginSource = new MarginSource(this.plugin.client, did); 58 + const itemSourceName = this.item.getSource(); 59 + this.itemSource = itemSourceName === "semble" ? this.sembleSource 60 + : itemSourceName === "margin" ? this.marginSource 61 + : new BookmarkSource(this.plugin.client, did); 62 + 63 + const itemUri = this.item.getUri(); 64 + 65 + const canCollect = this.item.canAddToCollections(); 66 + const [sembleColls, sembleAssocs, marginColls, marginAssocs, availableTags] = await Promise.all([ 67 + canCollect ? this.sembleSource.getAvailableCollections!() : Promise.resolve([]), 68 + canCollect ? this.sembleSource.getCollectionAssociations!() : Promise.resolve([]), 69 + canCollect ? this.marginSource.getAvailableCollections!() : Promise.resolve([]), 70 + canCollect ? this.marginSource.getCollectionAssociations!() : Promise.resolve([]), 71 + this.itemSource.getAvilableTags?.() ?? Promise.resolve(undefined), 72 + ]); 73 + 74 + loading.remove(); 75 + 76 + if (canCollect) { 77 + const sembleLinkedUris = new Map<string, string>(); 78 + for (const assoc of sembleAssocs) { 79 + if (assoc.record === itemUri) sembleLinkedUris.set(assoc.collection, assoc.linkUri); 80 + } 81 + 82 + const marginLinkedUris = new Map<string, string>(); 83 + for (const assoc of marginAssocs) { 84 + if (assoc.record === itemUri) marginLinkedUris.set(assoc.collection, assoc.linkUri); 85 + } 86 + 87 + this.collectionStates = [ 88 + ...sembleColls.map(c => ({ 89 + uri: c.value, 90 + name: c.label ?? c.value, 91 + description: c.description, 92 + source: "semble" as const, 93 + isSelected: sembleLinkedUris.has(c.value), 94 + wasSelected: sembleLinkedUris.has(c.value), 95 + linkUri: sembleLinkedUris.get(c.value), 96 + })), 97 + ...marginColls.map(c => ({ 98 + uri: c.value, 99 + name: c.label ?? c.value, 100 + description: c.description, 101 + source: "margin" as const, 102 + isSelected: marginLinkedUris.has(c.value), 103 + wasSelected: marginLinkedUris.has(c.value), 104 + linkUri: marginLinkedUris.get(c.value), 105 + })), 106 + ]; 107 + } 108 + 109 + if (this.item.canAddTags() && availableTags) { 110 + const currentTags = new Set(this.item.getTags()); 111 + this.tagStates = availableTags.map(f => f.value).sort().map(tag => ({ 112 + tag, 113 + isSelected: currentTags.has(tag), 114 + })); 115 + } 116 + 117 + this.renderForm(contentEl); 118 + } catch (err) { 119 + loading.remove(); 120 + const message = err instanceof Error ? err.message : String(err); 121 + contentEl.createEl("p", { text: `Error: ${message}`, cls: "atmosphere-error" }); 122 + } 123 + } 124 + 125 + private renderForm(contentEl: HTMLElement) { 126 + const form = contentEl.createEl("div", { cls: "atmosphere-form" }); 127 + 128 + if (this.item.canAddTags()) { 129 + const tagsGroup = form.createEl("div", { cls: "atmosphere-form-group" }); 130 + tagsGroup.createEl("label", { text: "Tags" }); 131 + 132 + const tagsList = tagsGroup.createEl("div", { cls: "atmosphere-tag-list" }); 133 + for (const state of this.tagStates) { 134 + this.addTagChip(tagsList, state); 135 + } 136 + 137 + const newTagRow = tagsGroup.createEl("div", { cls: "atmosphere-tag-row" }); 138 + this.newTagInput = newTagRow.createEl("input", { 139 + type: "text", 140 + cls: "atmosphere-input", 141 + attr: { placeholder: "Add new tag..." }, 142 + }); 143 + const addBtn = newTagRow.createEl("button", { 144 + text: "Add", 145 + cls: "atmosphere-btn atmosphere-btn-secondary", 146 + attr: { type: "button" }, 147 + }); 148 + addBtn.addEventListener("click", () => { 149 + const value = this.newTagInput?.value.trim(); 150 + if (value && !this.tagStates.some(s => s.tag === value)) { 151 + const newState = { tag: value, isSelected: true }; 152 + this.tagStates.push(newState); 153 + this.addTagChip(tagsList, newState); 154 + if (this.newTagInput) this.newTagInput.value = ""; 155 + } 156 + }); 157 + } 158 + 159 + if (this.collectionStates.length > 0) { 160 + const collectionsGroup = form.createEl("div", { cls: "atmosphere-form-group" }); 161 + collectionsGroup.createEl("label", { text: "Collections" }); 162 + 163 + const collectionsList = collectionsGroup.createEl("div", { cls: "atmosphere-collection-list" }); 164 + for (const state of this.collectionStates) { 165 + const item = collectionsList.createEl("label", { cls: "atmosphere-collection-item" }); 166 + 167 + const checkbox = item.createEl("input", { type: "checkbox", cls: "atmosphere-collection-checkbox" }); 168 + checkbox.checked = state.isSelected; 169 + checkbox.addEventListener("change", () => { state.isSelected = checkbox.checked; }); 170 + 171 + const info = item.createEl("div", { cls: "atmosphere-collection-item-info" }); 172 + info.createEl("span", { text: state.name, cls: "atmosphere-collection-item-name" }); 173 + if (state.description) { 174 + info.createEl("span", { text: state.description, cls: "atmosphere-collection-item-desc" }); 175 + } 176 + 177 + const sourceIcon = item.createEl("span", { cls: "atmosphere-collection-source-icon" }); 178 + setIcon(sourceIcon, state.source === "semble" ? "atmosphere-semble" : "atmosphere-margin"); 179 + } 180 + } 181 + 182 + const actions = contentEl.createEl("div", { cls: "atmosphere-modal-actions" }); 183 + 184 + actions.createEl("button", { text: "Delete", cls: "atmosphere-btn atmosphere-btn-danger" }) 185 + .addEventListener("click", () => { this.confirmDelete(contentEl); }); 186 + 187 + actions.createEl("div", { cls: "atmosphere-spacer" }); 188 + 189 + actions.createEl("button", { text: "Cancel", cls: "atmosphere-btn atmosphere-btn-secondary" }) 190 + .addEventListener("click", () => { this.close(); }); 191 + 192 + actions.createEl("button", { text: "Save", cls: "atmosphere-btn atmosphere-btn-primary" }) 193 + .addEventListener("click", () => { void this.saveChanges(); }); 194 + } 195 + 196 + private addTagChip(container: HTMLElement, state: TagState) { 197 + const item = container.createEl("label", { cls: "atmosphere-tag-item" }); 198 + const checkbox = item.createEl("input", { type: "checkbox" }); 199 + checkbox.checked = state.isSelected; 200 + checkbox.addEventListener("change", () => { state.isSelected = checkbox.checked; }); 201 + item.createEl("span", { text: state.tag }); 202 + } 203 + 204 + private confirmDelete(contentEl: HTMLElement) { 205 + contentEl.empty(); 206 + contentEl.createEl("h2", { text: "Delete item" }); 207 + contentEl.createEl("p", { text: "Are you sure you want to delete this item?", cls: "atmosphere-warning-text" }); 208 + 209 + const actions = contentEl.createEl("div", { cls: "atmosphere-modal-actions" }); 210 + actions.createEl("button", { text: "Cancel", cls: "atmosphere-btn atmosphere-btn-secondary" }) 211 + .addEventListener("click", () => { void this.onOpen(); }); 212 + actions.createEl("button", { text: "Delete", cls: "atmosphere-btn atmosphere-btn-danger" }) 213 + .addEventListener("click", () => { void this.handleDelete(); }); 214 + } 215 + 216 + private async handleDelete() { 217 + const { contentEl } = this; 218 + contentEl.empty(); 219 + contentEl.createEl("p", { text: "Deleting..." }); 220 + 221 + try { 222 + await this.itemSource.deleteItem!(this.item.getUri()); 223 + new Notice("Deleted"); 224 + this.close(); 225 + this.onSuccess?.(); 226 + } catch (err) { 227 + contentEl.empty(); 228 + const message = err instanceof Error ? err.message : String(err); 229 + contentEl.createEl("p", { text: `Failed to delete: ${message}`, cls: "atmosphere-error" }); 230 + } 231 + } 232 + 233 + private async saveChanges() { 234 + if (!this.plugin.client) return; 235 + 236 + // Read pending tag input before clearing DOM 237 + const pendingNewTag = this.newTagInput?.value.trim(); 238 + 239 + const { contentEl } = this; 240 + contentEl.empty(); 241 + contentEl.createEl("p", { text: "Saving..." }); 242 + 243 + try { 244 + const messages: string[] = []; 245 + 246 + if (this.item.canAddTags() && this.itemSource.updateTags) { 247 + const selectedTags = this.tagStates.filter(s => s.isSelected).map(s => s.tag); 248 + if (pendingNewTag && !selectedTags.includes(pendingNewTag)) { 249 + selectedTags.push(pendingNewTag); 250 + } 251 + await this.itemSource.updateTags(this.item.getUri(), [...new Set(selectedTags)]); 252 + messages.push("Tags updated"); 253 + } 254 + 255 + const toAdd = this.collectionStates.filter(s => s.isSelected && !s.wasSelected); 256 + const toRemove = this.collectionStates.filter(s => !s.isSelected && s.wasSelected); 257 + 258 + for (const state of toRemove) { 259 + if (state.linkUri) { 260 + const source = state.source === "semble" ? this.sembleSource : this.marginSource; 261 + await source.removeFromCollection!(state.linkUri); 262 + } 263 + } 264 + 265 + for (const state of toAdd) { 266 + const source = state.source === "semble" ? this.sembleSource : this.marginSource; 267 + await source.addToCollection!(this.item.getUri(), this.item.getCid(), state.uri); 268 + } 269 + 270 + if (toAdd.length > 0) messages.push(`Added to ${toAdd.length} collection${toAdd.length > 1 ? "s" : ""}`); 271 + if (toRemove.length > 0) messages.push(`Removed from ${toRemove.length} collection${toRemove.length > 1 ? "s" : ""}`); 272 + 273 + new Notice(messages.length > 0 ? messages.join(". ") : "Saved"); 274 + this.close(); 275 + this.onSuccess?.(); 276 + } catch (err) { 277 + contentEl.empty(); 278 + const message = err instanceof Error ? err.message : String(err); 279 + contentEl.createEl("p", { text: `Failed to save: ${message}`, cls: "atmosphere-error" }); 280 + } 281 + } 282 + 283 + onClose() { 284 + this.contentEl.empty(); 285 + } 286 + }
-331
src/components/editMarginBookmarkModal.ts
··· 1 - import { Modal, Notice } from "obsidian"; 2 - import type { Record } from "@atcute/atproto/types/repo/listRecords"; 3 - import type { Main as MarginBookmark } from "../lexicons/types/at/margin/bookmark"; 4 - import type { Main as MarginCollection } from "../lexicons/types/at/margin/collection"; 5 - import type { Main as MarginCollectionItem } from "../lexicons/types/at/margin/collectionItem"; 6 - import type AtmospherePlugin from "../main"; 7 - import { putRecord, deleteRecord, getMarginCollections, getMarginCollectionItems, createMarginCollectionItem, getMarginBookmarks } from "../lib"; 8 - 9 - type MarginBookmarkRecord = Record & { value: MarginBookmark }; 10 - type MarginCollectionRecord = Record & { value: MarginCollection }; 11 - type MarginCollectionItemRecord = Record & { value: MarginCollectionItem }; 12 - 13 - interface CollectionState { 14 - collection: MarginCollectionRecord; 15 - isSelected: boolean; 16 - wasSelected: boolean; 17 - linkUri?: string; 18 - } 19 - 20 - interface TagState { 21 - tag: string; 22 - isSelected: boolean; 23 - } 24 - 25 - export class EditMarginBookmarkModal extends Modal { 26 - plugin: AtmospherePlugin; 27 - record: MarginBookmarkRecord; 28 - onSuccess?: () => void; 29 - tagStates: TagState[] = []; 30 - newTagInput: HTMLInputElement | null = null; 31 - collectionStates: CollectionState[] = []; 32 - 33 - constructor(plugin: AtmospherePlugin, record: MarginBookmarkRecord, onSuccess?: () => void) { 34 - super(plugin.app); 35 - this.plugin = plugin; 36 - this.record = record; 37 - this.onSuccess = onSuccess; 38 - } 39 - 40 - async onOpen() { 41 - const { contentEl } = this; 42 - contentEl.empty(); 43 - contentEl.addClass("atmosphere-modal"); 44 - 45 - contentEl.createEl("h2", { text: "Edit margin bookmark" }); 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, itemsResp, bookmarksResp] = await Promise.all([ 56 - getMarginCollections(this.plugin.client, this.plugin.settings.did!), 57 - getMarginCollectionItems(this.plugin.client, this.plugin.settings.did!), 58 - getMarginBookmarks(this.plugin.client, this.plugin.settings.did!), 59 - ]); 60 - 61 - loading.remove(); 62 - 63 - const collections = (collectionsResp.ok ? collectionsResp.data.records : []) as unknown as MarginCollectionRecord[]; 64 - const items = (itemsResp.ok ? itemsResp.data.records : []) as unknown as MarginCollectionItemRecord[]; 65 - const bookmarks = (bookmarksResp.ok ? bookmarksResp.data.records : []) as unknown as MarginBookmarkRecord[]; 66 - 67 - const bookmarkLinks = items.filter(item => item.value.annotation === this.record.uri); 68 - const linkedCollectionUris = new Map<string, string>(); 69 - for (const link of bookmarkLinks) { 70 - linkedCollectionUris.set(link.value.collection, link.uri); 71 - } 72 - 73 - this.collectionStates = collections.map(collection => ({ 74 - collection, 75 - isSelected: linkedCollectionUris.has(collection.uri), 76 - wasSelected: linkedCollectionUris.has(collection.uri), 77 - linkUri: linkedCollectionUris.get(collection.uri), 78 - })); 79 - 80 - const allTags = new Set<string>(); 81 - for (const bookmark of bookmarks) { 82 - if (bookmark.value.tags) { 83 - for (const tag of bookmark.value.tags) { 84 - allTags.add(tag); 85 - } 86 - } 87 - } 88 - 89 - const currentTags = new Set(this.record.value.tags || []); 90 - this.tagStates = Array.from(allTags).sort().map(tag => ({ 91 - tag, 92 - isSelected: currentTags.has(tag), 93 - })); 94 - 95 - this.renderForm(contentEl); 96 - } catch (err) { 97 - loading.remove(); 98 - const message = err instanceof Error ? err.message : String(err); 99 - contentEl.createEl("p", { text: `Error: ${message}`, cls: "atmosphere-error" }); 100 - } 101 - } 102 - 103 - private renderForm(contentEl: HTMLElement) { 104 - const form = contentEl.createEl("div", { cls: "atmosphere-form" }); 105 - 106 - const tagsGroup = form.createEl("div", { cls: "atmosphere-form-group" }); 107 - tagsGroup.createEl("label", { text: "Tags" }); 108 - 109 - const tagsList = tagsGroup.createEl("div", { cls: "atmosphere-tag-list" }); 110 - for (const state of this.tagStates) { 111 - this.addTagChip(tagsList, state); 112 - } 113 - 114 - const newTagRow = tagsGroup.createEl("div", { cls: "atmosphere-tag-row" }); 115 - this.newTagInput = newTagRow.createEl("input", { 116 - type: "text", 117 - cls: "atmosphere-input", 118 - attr: { placeholder: "Add new tag..." } 119 - }); 120 - const addBtn = newTagRow.createEl("button", { 121 - text: "Add", 122 - cls: "atmosphere-btn atmosphere-btn-secondary", 123 - attr: { type: "button" } 124 - }); 125 - addBtn.addEventListener("click", () => { 126 - const value = this.newTagInput?.value.trim(); 127 - if (value && !this.tagStates.some(s => s.tag === value)) { 128 - const newState = { tag: value, isSelected: true }; 129 - this.tagStates.push(newState); 130 - this.addTagChip(tagsList, newState); 131 - if (this.newTagInput) this.newTagInput.value = ""; 132 - } 133 - }); 134 - 135 - if (this.collectionStates.length > 0) { 136 - const collectionsGroup = form.createEl("div", { cls: "atmosphere-form-group" }); 137 - collectionsGroup.createEl("label", { text: "Collections" }); 138 - 139 - const collectionsList = collectionsGroup.createEl("div", { cls: "atmosphere-collection-list" }); 140 - 141 - for (const state of this.collectionStates) { 142 - const item = collectionsList.createEl("label", { cls: "atmosphere-collection-item" }); 143 - 144 - const checkbox = item.createEl("input", { type: "checkbox", cls: "atmosphere-collection-checkbox" }); 145 - checkbox.checked = state.isSelected; 146 - checkbox.addEventListener("change", () => { 147 - state.isSelected = checkbox.checked; 148 - }); 149 - 150 - const info = item.createEl("div", { cls: "atmosphere-collection-item-info" }); 151 - info.createEl("span", { text: state.collection.value.name, cls: "atmosphere-collection-item-name" }); 152 - if (state.collection.value.description) { 153 - info.createEl("span", { text: state.collection.value.description, cls: "atmosphere-collection-item-desc" }); 154 - } 155 - } 156 - } 157 - 158 - const actions = contentEl.createEl("div", { cls: "atmosphere-modal-actions" }); 159 - 160 - const deleteBtn = actions.createEl("button", { 161 - text: "Delete", 162 - cls: "atmosphere-btn atmosphere-btn-danger" 163 - }); 164 - deleteBtn.addEventListener("click", () => { this.confirmDelete(contentEl); }); 165 - 166 - actions.createEl("div", { cls: "atmosphere-spacer" }); 167 - 168 - const cancelBtn = actions.createEl("button", { 169 - text: "Cancel", 170 - cls: "atmosphere-btn atmosphere-btn-secondary" 171 - }); 172 - cancelBtn.addEventListener("click", () => { this.close(); }); 173 - 174 - const saveBtn = actions.createEl("button", { 175 - text: "Save", 176 - cls: "atmosphere-btn atmosphere-btn-primary" 177 - }); 178 - saveBtn.addEventListener("click", () => { void this.saveChanges(); }); 179 - } 180 - 181 - private addTagChip(container: HTMLElement, state: TagState) { 182 - const item = container.createEl("label", { cls: "atmosphere-tag-item" }); 183 - const checkbox = item.createEl("input", { type: "checkbox" }); 184 - checkbox.checked = state.isSelected; 185 - checkbox.addEventListener("change", () => { 186 - state.isSelected = checkbox.checked; 187 - }); 188 - item.createEl("span", { text: state.tag }); 189 - } 190 - 191 - private confirmDelete(contentEl: HTMLElement) { 192 - contentEl.empty(); 193 - contentEl.createEl("h2", { text: "Delete bookmark" }); 194 - contentEl.createEl("p", { text: "Delete this bookmark?", cls: "atmosphere-warning-text" }); 195 - 196 - const actions = contentEl.createEl("div", { cls: "atmosphere-modal-actions" }); 197 - 198 - const cancelBtn = actions.createEl("button", { 199 - text: "Cancel", 200 - cls: "atmosphere-btn atmosphere-btn-secondary" 201 - }); 202 - cancelBtn.addEventListener("click", () => { 203 - void this.onOpen(); 204 - }); 205 - 206 - const confirmBtn = actions.createEl("button", { 207 - text: "Delete", 208 - cls: "atmosphere-btn atmosphere-btn-danger" 209 - }); 210 - confirmBtn.addEventListener("click", () => { void this.deleteBookmark(); }); 211 - } 212 - 213 - private async deleteBookmark() { 214 - if (!this.plugin.client) return; 215 - 216 - const { contentEl } = this; 217 - contentEl.empty(); 218 - contentEl.createEl("p", { text: "Deleting bookmark..." }); 219 - 220 - try { 221 - const rkey = this.record.uri.split("/").pop(); 222 - if (!rkey) { 223 - contentEl.empty(); 224 - contentEl.createEl("p", { text: "Invalid bookmark uri.", cls: "atmosphere-error" }); 225 - return; 226 - } 227 - 228 - await deleteRecord( 229 - this.plugin.client, 230 - this.plugin.settings.did!, 231 - "at.margin.bookmark", 232 - rkey 233 - ); 234 - 235 - new Notice("Bookmark deleted"); 236 - this.close(); 237 - this.onSuccess?.(); 238 - } catch (err) { 239 - contentEl.empty(); 240 - const message = err instanceof Error ? err.message : String(err); 241 - contentEl.createEl("p", { text: `Failed to delete: ${message}`, cls: "atmosphere-error" }); 242 - } 243 - } 244 - 245 - private async saveChanges() { 246 - if (!this.plugin.client) return; 247 - 248 - const { contentEl } = this; 249 - contentEl.empty(); 250 - contentEl.createEl("p", { text: "Saving changes..." }); 251 - 252 - try { 253 - const selectedTags = this.tagStates.filter(s => s.isSelected).map(s => s.tag); 254 - const newTag = this.newTagInput?.value.trim(); 255 - if (newTag && !selectedTags.includes(newTag)) { 256 - selectedTags.push(newTag); 257 - } 258 - const tags = [...new Set(selectedTags)]; 259 - 260 - const rkey = this.record.uri.split("/").pop(); 261 - if (!rkey) { 262 - contentEl.empty(); 263 - contentEl.createEl("p", { text: "Invalid bookmark uri.", cls: "atmosphere-error" }); 264 - return; 265 - } 266 - 267 - const updatedRecord: MarginBookmark = { 268 - ...this.record.value, 269 - tags, 270 - }; 271 - 272 - await putRecord( 273 - this.plugin.client, 274 - this.plugin.settings.did!, 275 - "at.margin.bookmark", 276 - rkey, 277 - updatedRecord 278 - ); 279 - 280 - const collectionsToAdd = this.collectionStates.filter(s => s.isSelected && !s.wasSelected); 281 - const collectionsToRemove = this.collectionStates.filter(s => !s.isSelected && s.wasSelected); 282 - 283 - for (const state of collectionsToRemove) { 284 - if (state.linkUri) { 285 - const linkRkey = state.linkUri.split("/").pop(); 286 - if (linkRkey) { 287 - await deleteRecord( 288 - this.plugin.client, 289 - this.plugin.settings.did!, 290 - "at.margin.collectionItem", 291 - linkRkey 292 - ); 293 - } 294 - } 295 - } 296 - 297 - for (const state of collectionsToAdd) { 298 - await createMarginCollectionItem( 299 - this.plugin.client, 300 - this.plugin.settings.did!, 301 - this.record.uri, 302 - state.collection.uri 303 - ); 304 - } 305 - 306 - const messages: string[] = []; 307 - if (tags.length !== (this.record.value.tags?.length || 0) || 308 - !tags.every(t => this.record.value.tags?.includes(t))) { 309 - messages.push("Tags updated"); 310 - } 311 - if (collectionsToAdd.length > 0) { 312 - messages.push(`Added to ${collectionsToAdd.length} collection${collectionsToAdd.length > 1 ? "s" : ""}`); 313 - } 314 - if (collectionsToRemove.length > 0) { 315 - messages.push(`Removed from ${collectionsToRemove.length} collection${collectionsToRemove.length > 1 ? "s" : ""}`); 316 - } 317 - 318 - new Notice(messages.length > 0 ? messages.join(". ") : "Saved"); 319 - this.close(); 320 - this.onSuccess?.(); 321 - } catch (err) { 322 - contentEl.empty(); 323 - const message = err instanceof Error ? err.message : String(err); 324 - contentEl.createEl("p", { text: `Failed to save: ${message}`, cls: "atmosphere-error" }); 325 - } 326 - } 327 - 328 - onClose() { 329 - this.contentEl.empty(); 330 - } 331 - }
+22 -3
src/sources/community.ts
··· 1 1 import type { Client } from "@atcute/client"; 2 2 import type { Record } from "@atcute/atproto/types/repo/listRecords"; 3 3 import type AtmospherePlugin from "../main"; 4 - import { getBookmarks } from "../lib"; 4 + import { getBookmarks, deleteRecord, getRecord, putRecord } from "../lib"; 5 5 import type { ATBookmarkItem, DataSource, SourceFilter } from "./types"; 6 - import { EditBookmarkModal } from "../components/editBookmarkModal"; 7 6 import type { Main as Bookmark } from "../lexicons/types/community/lexicon/bookmarks/bookmark"; 8 7 import { fetchOgImage } from "../util" 9 8 ··· 55 54 return true; 56 55 } 57 56 57 + canAddToCollections(): boolean { 58 + return false; 59 + } 60 + 58 61 openEditModal(onSuccess?: () => void): void { 59 - new EditBookmarkModal(this.plugin, this.record, onSuccess).open(); 62 + const { EditItemModal } = require("../components/editItemModal"); 63 + new EditItemModal(this.plugin, this, onSuccess).open(); 60 64 } 61 65 62 66 getTitle(): string | undefined { ··· 122 126 } 123 127 124 128 return bookmarks.map((record: BookmarkRecord) => new BookmarkItem(record, plugin)); 129 + } 130 + 131 + async deleteItem(itemUri: string): Promise<void> { 132 + const rkey = itemUri.split("/").pop(); 133 + if (!rkey) throw new Error("Invalid URI"); 134 + await deleteRecord(this.client, this.repo, "community.lexicon.bookmarks.bookmark", rkey); 135 + } 136 + 137 + async updateTags(itemUri: string, tags: string[]): Promise<void> { 138 + const rkey = itemUri.split("/").pop(); 139 + if (!rkey) throw new Error("Invalid URI"); 140 + const resp = await getRecord(this.client, this.repo, "community.lexicon.bookmarks.bookmark", rkey); 141 + if (!resp.ok) throw new Error("Failed to fetch record"); 142 + const existing = resp.data.value as unknown as Bookmark; 143 + await putRecord(this.client, this.repo, "community.lexicon.bookmarks.bookmark", rkey, { ...existing, tags }); 125 144 } 126 145 127 146 async getAvilableTags(): Promise<SourceFilter[]> {
+35 -3
src/sources/margin.ts
··· 1 1 import type { Client } from "@atcute/client"; 2 2 import type { Record } from "@atcute/atproto/types/repo/listRecords"; 3 3 import type AtmospherePlugin from "../main"; 4 - import { getMarginBookmarks, getMarginCollections, getMarginCollectionItems } from "../lib"; 4 + import { getMarginBookmarks, getMarginCollections, getMarginCollectionItems, deleteRecord, createMarginCollectionItem, getRecord, putRecord } from "../lib"; 5 5 import type { ATBookmarkItem, CollectionAssociation, DataSource, SourceFilter } from "./types"; 6 6 import type { Main as MarginBookmark } from "../lexicons/types/at/margin/bookmark"; 7 7 import type { Main as MarginCollection } from "../lexicons/types/at/margin/collection"; 8 8 import type { Main as MarginCollectionItem } from "../lexicons/types/at/margin/collectionItem"; 9 - import { EditMarginBookmarkModal } from "../components/editMarginBookmarkModal"; 10 9 import { fetchOgImage } from "../util" 11 10 12 11 type MarginBookmarkRecord = Record & { value: MarginBookmark }; ··· 48 47 return true; 49 48 } 50 49 50 + canAddToCollections(): boolean { 51 + return true; 52 + } 53 + 51 54 canEdit(): boolean { 52 55 return true; 53 56 } 54 57 55 58 openEditModal(onSuccess?: () => void): void { 56 - new EditMarginBookmarkModal(this.plugin, this.record, onSuccess).open(); 59 + const { EditItemModal } = require("../components/editItemModal"); 60 + new EditItemModal(this.plugin, this, onSuccess).open(); 57 61 } 58 62 59 63 getTitle(): string | undefined { ··· 131 135 return collections.map((c: MarginCollectionRecord) => ({ 132 136 value: c.uri, 133 137 label: c.value.name, 138 + description: c.value.description, 134 139 })); 135 140 } 141 + 136 142 async getCollectionAssociations(): Promise<CollectionAssociation[]> { 137 143 const itemsResp = await getMarginCollectionItems(this.client, this.repo); 138 144 if (!itemsResp.ok) return []; ··· 140 146 return (itemsResp.data.records as MarginCollectionItemRecord[]).map(item => ({ 141 147 record: item.value.annotation, 142 148 collection: item.value.collection, 149 + linkUri: item.uri, 143 150 })); 151 + } 152 + 153 + async deleteItem(itemUri: string): Promise<void> { 154 + const rkey = itemUri.split("/").pop(); 155 + if (!rkey) throw new Error("Invalid URI"); 156 + await deleteRecord(this.client, this.repo, "at.margin.bookmark", rkey); 157 + } 158 + 159 + async addToCollection(itemUri: string, _itemCid: string, collectionUri: string): Promise<void> { 160 + await createMarginCollectionItem(this.client, this.repo, itemUri, collectionUri); 161 + } 162 + 163 + async removeFromCollection(linkUri: string): Promise<void> { 164 + const rkey = linkUri.split("/").pop(); 165 + if (!rkey) throw new Error("Invalid link URI"); 166 + await deleteRecord(this.client, this.repo, "at.margin.collectionItem", rkey); 167 + } 168 + 169 + async updateTags(itemUri: string, tags: string[]): Promise<void> { 170 + const rkey = itemUri.split("/").pop(); 171 + if (!rkey) throw new Error("Invalid URI"); 172 + const resp = await getRecord(this.client, this.repo, "at.margin.bookmark", rkey); 173 + if (!resp.ok) throw new Error("Failed to fetch record"); 174 + const existing = resp.data.value as MarginBookmark; 175 + await putRecord(this.client, this.repo, "at.margin.bookmark", rkey, { ...existing, tags }); 144 176 } 145 177 146 178 async getAvilableTags(): Promise<SourceFilter[]> {
+29 -3
src/sources/semble.ts
··· 1 1 import type { Client } from "@atcute/client"; 2 2 import type { Record } from "@atcute/atproto/types/repo/listRecords"; 3 3 import type AtmospherePlugin from "../main"; 4 - import { getSembleCollections, getSembleCards, getSembleCollectionLinks } from "../lib"; 4 + import { getSembleCollections, getSembleCards, getSembleCollectionLinks, deleteRecord, getRecord, createSembleCollectionLink } from "../lib"; 5 5 import type { Main as Card, NoteContent, UrlContent } from "../lexicons/types/network/cosmik/card"; 6 6 import type { Main as Collection } from "../lexicons/types/network/cosmik/collection"; 7 7 import type { Main as CollectionLink } from "../lexicons/types/network/cosmik/collectionLink"; 8 8 import type { ATBookmarkItem, CollectionAssociation, DataSource, SourceFilter } from "./types"; 9 - import { EditCardModal } from "../components/editCardModal"; 10 9 import { fetchOgImage } from "../util" 11 10 12 11 type CardRecord = Record & { value: Card }; ··· 50 49 return false; 51 50 } 52 51 52 + canAddToCollections(): boolean { 53 + return true; 54 + } 55 + 53 56 canEdit(): boolean { 54 57 return true; 55 58 } 56 59 57 60 openEditModal(onSuccess?: () => void): void { 58 - new EditCardModal(this.plugin, this.record.uri, this.record.cid, onSuccess).open(); 61 + const { EditItemModal } = require("../components/editItemModal"); 62 + new EditItemModal(this.plugin, this, onSuccess).open(); 59 63 } 60 64 61 65 getTitle(): string | undefined { ··· 178 182 return collections.map((c: CollectionRecord) => ({ 179 183 value: c.uri, 180 184 label: c.value.name, 185 + description: c.value.description, 181 186 })); 182 187 } 183 188 ··· 188 193 return (linksResp.data.records as CollectionLinkRecord[]).map(link => ({ 189 194 record: link.value.card.uri, 190 195 collection: link.value.collection.uri, 196 + linkUri: link.uri, 191 197 })); 198 + } 199 + 200 + async deleteItem(itemUri: string): Promise<void> { 201 + const rkey = itemUri.split("/").pop(); 202 + if (!rkey) throw new Error("Invalid URI"); 203 + await deleteRecord(this.client, this.repo, "network.cosmik.card", rkey); 204 + } 205 + 206 + async addToCollection(itemUri: string, itemCid: string, collectionUri: string): Promise<void> { 207 + const collectionRkey = collectionUri.split("/").pop(); 208 + if (!collectionRkey) throw new Error("Invalid collection URI"); 209 + const collectionResp = await getRecord(this.client, this.repo, "network.cosmik.collection", collectionRkey); 210 + if (!collectionResp.ok || !collectionResp.data.cid) throw new Error("Failed to fetch collection"); 211 + await createSembleCollectionLink(this.client, this.repo, itemUri, itemCid, collectionUri, String(collectionResp.data.cid)); 212 + } 213 + 214 + async removeFromCollection(linkUri: string): Promise<void> { 215 + const rkey = linkUri.split("/").pop(); 216 + if (!rkey) throw new Error("Invalid link URI"); 217 + await deleteRecord(this.client, this.repo, "network.cosmik.collectionLink", rkey); 192 218 } 193 219 194 220 }
+7
src/sources/types.ts
··· 3 3 export interface ATBookmarkItem { 4 4 canAddNotes(): boolean; 5 5 canAddTags(): boolean; 6 + canAddToCollections(): boolean; 6 7 canEdit(): boolean; 7 8 openEditModal(onSuccess?: () => void): void; 8 9 getUri(): string; ··· 23 24 export interface SourceFilter { 24 25 value: string; 25 26 label?: string; 27 + description?: string; 26 28 } 27 29 28 30 export interface CollectionAssociation { 29 31 record: string; 30 32 collection: string; 33 + linkUri: string; 31 34 } 32 35 33 36 export interface DataSource { ··· 36 39 getAvailableCollections?(): Promise<SourceFilter[]>; 37 40 getAvilableTags?(): Promise<SourceFilter[]>; 38 41 getCollectionAssociations?(): Promise<CollectionAssociation[]>; 42 + deleteItem?(itemUri: string): Promise<void>; 43 + addToCollection?(itemUri: string, itemCid: string, collectionUri: string): Promise<void>; 44 + removeFromCollection?(linkUri: string): Promise<void>; 45 + updateTags?(itemUri: string, tags: string[]): Promise<void>; 39 46 }