powerpointproto slides.waow.tech
slides
at main 283 lines 8.1 kB view raw
1import { currentDid, getRpc, getPdsUrl, publicFetch, resolvePdsUrl } from "./client"; 2import { createSlide, deleteSlide, getPublicSlide, getSlide, updateSlide } from "./slide"; 3import { getBlobUrl } from "./blob"; 4import { DECK, type Deck, type DeckRecord, type Slide, type SlideRef } from "./types"; 5 6const rkeyFromUri = (uri: string): string => uri.split("/").pop()!; 7 8export const createDeck = async (name: string, slides: Slide[], thumbnail?: Deck["thumbnail"]): Promise<string | null> => { 9 if (!currentDid) return null; 10 11 const rpc = getRpc(); 12 const now = new Date().toISOString(); 13 14 // create each slide as a separate record 15 const slideRefs: SlideRef[] = []; 16 for (const slide of slides) { 17 const ref = await createSlide(slide); 18 if (!ref) throw new Error("failed to create slide"); 19 slideRefs.push({ subject: ref }); 20 } 21 22 // create deck with references 23 const record: DeckRecord = { 24 name, 25 slides: slideRefs, 26 createdAt: now, 27 updatedAt: now, 28 ...(thumbnail && { thumbnail }), 29 }; 30 31 const res = await rpc.post("com.atproto.repo.createRecord", { 32 input: { 33 repo: currentDid, 34 collection: DECK, 35 record: { $type: DECK, ...record }, 36 }, 37 }); 38 39 if (!res.ok) throw new Error(res.data.error || "failed to create deck"); 40 return res.data.uri; 41}; 42 43export const updateDeck = async (rkey: string, deck: Partial<Deck>): Promise<void> => { 44 if (!currentDid) throw new Error("not logged in"); 45 46 const rpc = getRpc(); 47 48 // get existing deck record 49 const existing = await rpc.get("com.atproto.repo.getRecord", { 50 params: { repo: currentDid, collection: DECK, rkey }, 51 }); 52 53 if (!existing.ok) throw new Error("deck not found"); 54 55 const existingRecord = existing.data.value as DeckRecord; 56 const existingRefs = existingRecord.slides; 57 58 // if slides changed, update/create/delete slide records 59 let slideRefs = existingRefs; 60 if (deck.slides) { 61 slideRefs = []; 62 63 for (let i = 0; i < deck.slides.length; i++) { 64 const slide = deck.slides[i]; 65 66 if (slide.uri && slide.rkey) { 67 // existing slide - update it 68 const ref = await updateSlide(slide.rkey, slide); 69 slideRefs.push({ subject: ref }); 70 } else { 71 // new slide - create it 72 const ref = await createSlide(slide); 73 if (!ref) throw new Error("failed to create slide"); 74 slideRefs.push({ subject: ref }); 75 } 76 } 77 78 // delete removed slides 79 const newUris = new Set(deck.slides.filter(s => s.uri).map(s => s.uri!)); 80 for (const ref of existingRefs) { 81 if (!newUris.has(ref.subject.uri)) { 82 const slideRkey = rkeyFromUri(ref.subject.uri); 83 await deleteSlide(slideRkey).catch(() => { 84 // ignore deletion errors (slide may already be gone) 85 }); 86 } 87 } 88 } 89 90 const record: DeckRecord = { 91 name: deck.name || existingRecord.name, 92 slides: slideRefs, 93 createdAt: existingRecord.createdAt, 94 updatedAt: new Date().toISOString(), 95 // preserve existing thumbnail or use new one if provided 96 ...(deck.thumbnail !== undefined 97 ? (deck.thumbnail ? { thumbnail: deck.thumbnail } : {}) 98 : (existingRecord.thumbnail ? { thumbnail: existingRecord.thumbnail } : {})), 99 }; 100 101 const res = await rpc.post("com.atproto.repo.putRecord", { 102 input: { 103 repo: currentDid, 104 collection: DECK, 105 rkey, 106 record: { $type: DECK, ...record }, 107 }, 108 }); 109 110 if (!res.ok) throw new Error(res.data.error || "failed to update deck"); 111}; 112 113export const deleteDeck = async (rkey: string): Promise<void> => { 114 if (!currentDid) throw new Error("not logged in"); 115 116 const rpc = getRpc(); 117 118 // get deck to find slide refs 119 const existing = await rpc.get("com.atproto.repo.getRecord", { 120 params: { repo: currentDid, collection: DECK, rkey }, 121 }); 122 123 if (existing.ok) { 124 const record = existing.data.value as DeckRecord; 125 // delete all slides 126 for (const ref of record.slides) { 127 const slideRkey = rkeyFromUri(ref.subject.uri); 128 await deleteSlide(slideRkey).catch(() => { 129 // ignore - slide may already be gone 130 }); 131 } 132 } 133 134 // delete deck 135 const res = await rpc.post("com.atproto.repo.deleteRecord", { 136 input: { repo: currentDid, collection: DECK, rkey }, 137 }); 138 139 if (!res.ok) throw new Error(res.data.error || "failed to delete deck"); 140}; 141 142export const getDeck = async (repo: string, rkey: string): Promise<Deck | null> => { 143 const rpc = getRpc(); 144 const res = await rpc.get("com.atproto.repo.getRecord", { 145 params: { repo, collection: DECK, rkey }, 146 }); 147 148 if (!res.ok) return null; 149 150 const record = res.data.value as DeckRecord; 151 152 // resolve all slide refs 153 const slides: Slide[] = []; 154 for (const ref of record.slides) { 155 const slide = await getSlide(ref.subject.uri); 156 if (slide) slides.push(slide); 157 } 158 159 // resolve thumbnail URL if present 160 const thumbnailUrl = record.thumbnail 161 ? getBlobUrl(repo, record.thumbnail.ref.$link, getPdsUrl() || undefined) 162 : undefined; 163 164 return { 165 uri: res.data.uri, 166 repo, 167 rkey, 168 name: record.name, 169 slides, 170 createdAt: record.createdAt, 171 updatedAt: record.updatedAt, 172 thumbnail: record.thumbnail, 173 thumbnailUrl, 174 }; 175}; 176 177export const listMyDecks = async (): Promise<Deck[]> => { 178 if (!currentDid) return []; 179 180 const rpc = getRpc(); 181 const res = await rpc.get("com.atproto.repo.listRecords", { 182 params: { repo: currentDid, collection: DECK, limit: 100 }, 183 }); 184 185 if (!res.ok) return []; 186 187 const pdsUrl = getPdsUrl() || undefined; 188 189 // eslint-disable-next-line @typescript-eslint/no-explicit-any 190 return res.data.records.map((r: any) => { 191 const val = r.value as DeckRecord; 192 const rkey = r.uri.split("/").pop()!; 193 const thumbnailUrl = val.thumbnail 194 ? getBlobUrl(currentDid!, val.thumbnail.ref.$link, pdsUrl) 195 : undefined; 196 // return shallow deck with slide count (slides not resolved yet) 197 return { 198 uri: r.uri, 199 repo: currentDid!, 200 rkey, 201 name: val.name, 202 slides: [], 203 slideCount: val.slides.length, // for display before full resolution 204 createdAt: val.createdAt, 205 updatedAt: val.updatedAt, 206 thumbnail: val.thumbnail, 207 thumbnailUrl, 208 }; 209 }); 210}; 211 212// list public decks for any user (no auth required) 213export const listPublicDecks = async (did: string): Promise<Deck[]> => { 214 const pdsUrl = await resolvePdsUrl(did); 215 216 const data = await publicFetch(pdsUrl, "com.atproto.repo.listRecords", { 217 repo: did, 218 collection: DECK, 219 limit: "100", 220 }); 221 222 if (!data?.records) return []; 223 224 // eslint-disable-next-line @typescript-eslint/no-explicit-any 225 return data.records.map((r: any) => { 226 const val = r.value as DeckRecord; 227 const rkey = r.uri.split("/").pop()!; 228 const thumbnailUrl = val.thumbnail 229 ? getBlobUrl(did, val.thumbnail.ref.$link, pdsUrl) 230 : undefined; 231 return { 232 uri: r.uri, 233 repo: did, 234 rkey, 235 name: val.name, 236 slides: [], 237 slideCount: val.slides.length, 238 createdAt: val.createdAt, 239 updatedAt: val.updatedAt, 240 thumbnail: val.thumbnail, 241 thumbnailUrl, 242 }; 243 }); 244}; 245 246// public deck fetch (no auth required) 247export const getPublicDeck = async (did: string, rkey: string): Promise<Deck | null> => { 248 const pdsUrl = await resolvePdsUrl(did); 249 250 const data = await publicFetch(pdsUrl, "com.atproto.repo.getRecord", { 251 repo: did, 252 collection: DECK, 253 rkey, 254 }); 255 256 if (!data) return null; 257 258 const record = data.value as DeckRecord; 259 260 // resolve all slide refs 261 const slides: Slide[] = []; 262 for (const ref of record.slides) { 263 const slide = await getPublicSlide(ref.subject.uri, pdsUrl); 264 if (slide) slides.push(slide); 265 } 266 267 // resolve thumbnail URL if present 268 const thumbnailUrl = record.thumbnail 269 ? getBlobUrl(did, record.thumbnail.ref.$link, pdsUrl) 270 : undefined; 271 272 return { 273 uri: data.uri, 274 repo: did, 275 rkey, 276 name: record.name, 277 slides, 278 createdAt: record.createdAt, 279 updatedAt: record.updatedAt, 280 thumbnail: record.thumbnail, 281 thumbnailUrl, 282 }; 283};