|
51 | 51 |
|
52 | 52 | type MediaKind = 'image' | 'video' | 'audio' | 'other'; |
53 | 53 |
|
| 54 | + type ImageInfo = { |
| 55 | + title: string; |
| 56 | + message?: string | null; |
| 57 | + mime?: string | null; |
| 58 | + }; |
| 59 | +
|
| 60 | + type IconComponent = typeof Link; |
| 61 | +
|
| 62 | + type ToolbarAction = { |
| 63 | + key: string; |
| 64 | + label: string; |
| 65 | + activeLabel?: string; |
| 66 | + icon: IconComponent; |
| 67 | + activeIcon?: IconComponent; |
| 68 | + onClick: () => void; |
| 69 | + disabled?: boolean; |
| 70 | + active?: boolean; |
| 71 | + isVisible: boolean; |
| 72 | + }; |
| 73 | +
|
54 | 74 | const heicExtensions = ['.heic', '.heif']; |
55 | 75 |
|
56 | 76 | let sniffedKind = $state<MediaKind | null>(null); |
|
112 | 132 | heicConverting = true; |
113 | 133 | heicError = null; |
114 | 134 |
|
115 | | - heicConvertPromise = (async () => { |
116 | | - try { |
117 | | - const result = await heicTo({ blob: sourceBlob, type: 'image/png' }); |
118 | | - const pngBlob = result instanceof Blob ? result : null; |
119 | | - if (!pngBlob || token !== heicConversionToken) return null; |
120 | | - heicConvertedBlob = pngBlob; |
121 | | - heicConvertedUrl = URL.createObjectURL(pngBlob); |
122 | | - return pngBlob; |
123 | | - } catch { |
124 | | - if (token === heicConversionToken) { |
125 | | - heicError = 'Could not convert this HEIC image.'; |
126 | | - } |
127 | | - return null; |
128 | | - } finally { |
129 | | - if (token === heicConversionToken) { |
130 | | - heicConverting = false; |
131 | | - heicConvertPromise = null; |
132 | | - } |
133 | | - } |
134 | | - })(); |
| 135 | + heicConvertPromise = runHeicConversion(token); |
135 | 136 |
|
136 | 137 | return await heicConvertPromise; |
137 | 138 | } |
138 | 139 |
|
| 140 | + async function runHeicConversion(token: number) { |
| 141 | + try { |
| 142 | + const result = await heicTo({ blob: sourceBlob!, type: 'image/png' }); |
| 143 | + const pngBlob = result instanceof Blob ? result : null; |
| 144 | + if (!pngBlob || token !== heicConversionToken) return null; |
| 145 | + heicConvertedBlob = pngBlob; |
| 146 | + heicConvertedUrl = URL.createObjectURL(pngBlob); |
| 147 | + return pngBlob; |
| 148 | + } catch { |
| 149 | + if (token === heicConversionToken) { |
| 150 | + heicError = 'Could not convert this HEIC image.'; |
| 151 | + } |
| 152 | + return null; |
| 153 | + } finally { |
| 154 | + if (token === heicConversionToken) { |
| 155 | + heicConverting = false; |
| 156 | + heicConvertPromise = null; |
| 157 | + } |
| 158 | + } |
| 159 | + } |
| 160 | +
|
139 | 161 | function handleDownloadOriginal() { |
140 | 162 | if (ondownload) { |
141 | 163 | ondownload(); |
|
214 | 236 | const isImageUnsupported = $derived(!isHeic && imageSupport?.status === 'unsupported'); |
215 | 237 | const imageSupportMessage = $derived(imageSupport?.message ?? null); |
216 | 238 |
|
| 239 | + const imageInfo = $derived<ImageInfo | null>( |
| 240 | + isHeic && !heicConverting && !heicConvertedUrl |
| 241 | + ? { |
| 242 | + title: heicError ?? 'Unable to preview this HEIC image.', |
| 243 | + message: imageSupportMessage, |
| 244 | + mime: sniffedMime |
| 245 | + } |
| 246 | + : isImageUnsupported |
| 247 | + ? { |
| 248 | + title: 'This image format is not supported in this browser.', |
| 249 | + message: imageSupportMessage, |
| 250 | + mime: sniffedMime |
| 251 | + } |
| 252 | + : null |
| 253 | + ); |
| 254 | +
|
| 255 | + const toolbarActions = $derived<ToolbarAction[]>([ |
| 256 | + { |
| 257 | + key: 'copy-text', |
| 258 | + isVisible: contentText !== null, |
| 259 | + label: 'Copy Text', |
| 260 | + activeLabel: 'Copied Text', |
| 261 | + icon: Copy, |
| 262 | + activeIcon: Check, |
| 263 | + active: textCopied, |
| 264 | + onClick: handleCopyText |
| 265 | + }, |
| 266 | + { |
| 267 | + key: 'copy-link', |
| 268 | + isVisible: Boolean(oncopylink), |
| 269 | + label: 'Copy Link', |
| 270 | + activeLabel: 'Copied Link', |
| 271 | + icon: Link, |
| 272 | + activeIcon: Check, |
| 273 | + active: copied, |
| 274 | + onClick: handleCopyLink |
| 275 | + }, |
| 276 | + { |
| 277 | + key: 'save-original', |
| 278 | + isVisible: Boolean(ondownload) && isHeic, |
| 279 | + label: 'Save Original', |
| 280 | + icon: Download, |
| 281 | + onClick: handleDownloadOriginal |
| 282 | + }, |
| 283 | + { |
| 284 | + key: 'save-png', |
| 285 | + isVisible: Boolean(ondownload) && isHeic, |
| 286 | + label: heicConverting && !heicConvertedBlob ? 'Converting...' : 'Save PNG', |
| 287 | + icon: Download, |
| 288 | + onClick: handleDownloadPng, |
| 289 | + disabled: heicConverting && !heicConvertedBlob |
| 290 | + }, |
| 291 | + { |
| 292 | + key: 'save', |
| 293 | + isVisible: Boolean(ondownload) && !isHeic, |
| 294 | + label: 'Save', |
| 295 | + icon: Download, |
| 296 | + onClick: () => ondownload?.() |
| 297 | + } |
| 298 | + ]); |
| 299 | +
|
| 300 | + const visibleToolbarActions = $derived(toolbarActions.filter((action) => action.isVisible)); |
| 301 | +
|
217 | 302 | $effect(() => { |
218 | 303 | if (!sourceBlob || !isHeic) return; |
219 | | - void convertHeicToPng(); |
| 304 | + convertHeicToPng(); |
220 | 305 | }); |
221 | 306 |
|
222 | 307 | function handleKeydown(event: KeyboardEvent) { |
|
253 | 338 | <span class="truncate text-sm font-medium text-white">{baseName}</span> |
254 | 339 | </div> |
255 | 340 | <div class="flex items-center gap-1"> |
256 | | - {#if contentText !== null} |
| 341 | + {#each visibleToolbarActions as action (action.key)} |
| 342 | + {@const Icon = action.active ? (action.activeIcon ?? action.icon) : action.icon} |
257 | 343 | <Button |
258 | 344 | variant="ghost" |
259 | 345 | size="sm" |
260 | 346 | class="h-7 gap-1.5 px-2 text-xs text-white/70 hover:bg-white/10 hover:text-white" |
261 | | - onclick={handleCopyText} |
| 347 | + disabled={action.disabled} |
| 348 | + onclick={action.onClick} |
262 | 349 | > |
263 | | - {#if textCopied} |
264 | | - <Check class="h-3.5 w-3.5" /> |
265 | | - Copied Text |
266 | | - {:else} |
267 | | - <Copy class="h-3.5 w-3.5" /> |
268 | | - Copy Text |
269 | | - {/if} |
| 350 | + <Icon class="h-3.5 w-3.5" /> |
| 351 | + {action.active ? (action.activeLabel ?? action.label) : action.label} |
270 | 352 | </Button> |
271 | | - {/if} |
272 | | - {#if oncopylink} |
273 | | - <Button |
274 | | - variant="ghost" |
275 | | - size="sm" |
276 | | - class="h-7 gap-1.5 px-2 text-xs text-white/70 hover:bg-white/10 hover:text-white" |
277 | | - onclick={handleCopyLink} |
278 | | - > |
279 | | - {#if copied} |
280 | | - <Check class="h-3.5 w-3.5" /> |
281 | | - Copied Link |
282 | | - {:else} |
283 | | - <Link class="h-3.5 w-3.5" /> |
284 | | - Copy Link |
285 | | - {/if} |
286 | | - </Button> |
287 | | - {/if} |
288 | | - {#if ondownload} |
289 | | - {#if isHeic} |
290 | | - <Button |
291 | | - variant="ghost" |
292 | | - size="sm" |
293 | | - class="h-7 gap-1.5 px-2 text-xs text-white/70 hover:bg-white/10 hover:text-white" |
294 | | - onclick={handleDownloadOriginal} |
295 | | - > |
296 | | - <Download class="h-3.5 w-3.5" /> |
297 | | - Save Original |
298 | | - </Button> |
299 | | - <Button |
300 | | - variant="ghost" |
301 | | - size="sm" |
302 | | - class="h-7 gap-1.5 px-2 text-xs text-white/70 hover:bg-white/10 hover:text-white" |
303 | | - disabled={heicConverting && !heicConvertedBlob} |
304 | | - onclick={handleDownloadPng} |
305 | | - > |
306 | | - <Download class="h-3.5 w-3.5" /> |
307 | | - {#if heicConverting && !heicConvertedBlob} |
308 | | - Converting... |
309 | | - {:else} |
310 | | - Save PNG |
311 | | - {/if} |
312 | | - </Button> |
313 | | - {:else} |
314 | | - <Button |
315 | | - variant="ghost" |
316 | | - size="sm" |
317 | | - class="h-7 gap-1.5 px-2 text-xs text-white/70 hover:bg-white/10 hover:text-white" |
318 | | - onclick={ondownload} |
319 | | - > |
320 | | - <Download class="h-3.5 w-3.5" /> |
321 | | - Save |
322 | | - </Button> |
323 | | - {/if} |
324 | | - {/if} |
| 353 | + {/each} |
325 | 354 | </div> |
326 | 355 | </div> |
327 | 356 |
|
|
338 | 367 | Detecting file type... |
339 | 368 | </div> |
340 | 369 | {:else if isImage} |
341 | | - {#if isHeic} |
342 | | - {#if heicConverting && !heicConvertedUrl} |
343 | | - <div class="flex h-full items-center justify-center text-xs text-white/60"> |
344 | | - <div class="flex items-center gap-2"> |
345 | | - <Spinner class="size-4" /> |
346 | | - <span>Converting HEIC to PNG...</span> |
347 | | - </div> |
| 370 | + {#if isHeic && heicConverting && !heicConvertedUrl} |
| 371 | + <div class="flex h-full items-center justify-center text-xs text-white/60"> |
| 372 | + <div class="flex items-center gap-2"> |
| 373 | + <Spinner class="size-4" /> |
| 374 | + <span>Converting HEIC to PNG...</span> |
348 | 375 | </div> |
349 | | - {:else if heicConvertedUrl} |
350 | | - <div class="flex h-full items-center justify-center"> |
351 | | - <img |
352 | | - src={heicConvertedUrl} |
353 | | - alt={baseName} |
354 | | - title={baseName} |
355 | | - class="max-h-full max-w-full rounded-lg object-contain shadow-2xl" |
356 | | - /> |
357 | | - </div> |
358 | | - {:else} |
359 | | - <div class="flex h-full items-center justify-center"> |
360 | | - <div |
361 | | - class="max-w-md rounded-lg border border-white/10 bg-black/60 p-6 text-center" |
362 | | - > |
363 | | - <p class="text-sm font-semibold"> |
364 | | - {heicError ?? 'Unable to preview this HEIC image.'} |
365 | | - </p> |
366 | | - {#if imageSupportMessage} |
367 | | - <p class="mt-2 text-xs text-white/60">{imageSupportMessage}</p> |
368 | | - {/if} |
369 | | - {#if sniffedMime} |
370 | | - <p class="mt-2 text-xs text-white/40">Detected: {sniffedMime}</p> |
371 | | - {/if} |
372 | | - </div> |
373 | | - </div> |
374 | | - {/if} |
375 | | - {:else if isImageUnsupported} |
| 376 | + </div> |
| 377 | + {:else if isHeic && heicConvertedUrl} |
| 378 | + <div class="flex h-full items-center justify-center"> |
| 379 | + <img |
| 380 | + src={heicConvertedUrl} |
| 381 | + alt={baseName} |
| 382 | + title={baseName} |
| 383 | + class="max-h-full max-w-full rounded-lg object-contain shadow-2xl" |
| 384 | + /> |
| 385 | + </div> |
| 386 | + {:else if imageInfo} |
376 | 387 | <div class="flex h-full items-center justify-center"> |
377 | 388 | <div class="max-w-md rounded-lg border border-white/10 bg-black/60 p-6 text-center"> |
378 | | - <p class="text-sm font-semibold"> |
379 | | - This image format is not supported in this browser. |
380 | | - </p> |
381 | | - {#if imageSupportMessage} |
382 | | - <p class="mt-2 text-xs text-white/60">{imageSupportMessage}</p> |
| 389 | + <p class="text-sm font-semibold">{imageInfo.title}</p> |
| 390 | + {#if imageInfo.message} |
| 391 | + <p class="mt-2 text-xs text-white/60">{imageInfo.message}</p> |
383 | 392 | {/if} |
384 | | - {#if sniffedMime} |
385 | | - <p class="mt-2 text-xs text-white/40">Detected: {sniffedMime}</p> |
| 393 | + {#if imageInfo.mime} |
| 394 | + <p class="mt-2 text-xs text-white/40">Detected: {imageInfo.mime}</p> |
386 | 395 | {/if} |
387 | 396 | </div> |
388 | 397 | </div> |
|
0 commit comments