You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
153 lines
3.5 KiB
153 lines
3.5 KiB
<script lang="ts"> |
|
interface Props { |
|
url: string; |
|
isOpen: boolean; |
|
onClose: () => void; |
|
} |
|
|
|
let { url, isOpen, onClose }: Props = $props(); |
|
|
|
function getMediaType(url: string): 'image' | 'video' | 'audio' | 'unknown' { |
|
const lower = url.toLowerCase(); |
|
if (/\.(jpg|jpeg|png|gif|webp|svg|bmp)$/i.test(lower)) return 'image'; |
|
if (/\.(mp4|webm|ogg|mov|avi|mkv)$/i.test(lower)) return 'video'; |
|
if (/\.(mp3|wav|ogg|flac|aac|m4a)$/i.test(lower)) return 'audio'; |
|
return 'unknown'; |
|
} |
|
|
|
function handleBackdropClick(e: MouseEvent) { |
|
if (e.target === e.currentTarget) { |
|
onClose(); |
|
} |
|
} |
|
|
|
function handleKeyDown(e: KeyboardEvent) { |
|
if (e.key === 'Escape') { |
|
onClose(); |
|
} |
|
} |
|
|
|
const mediaType = $derived(getMediaType(url)); |
|
</script> |
|
|
|
{#if isOpen} |
|
<div |
|
class="media-viewer-backdrop" |
|
onclick={handleBackdropClick} |
|
onkeydown={handleKeyDown} |
|
role="dialog" |
|
aria-modal="true" |
|
tabindex="-1" |
|
> |
|
<div class="media-viewer-content"> |
|
<button class="media-viewer-close" onclick={onClose} aria-label="Close">×</button> |
|
|
|
{#if mediaType === 'image'} |
|
<img src={url} alt="Media" class="media-viewer-media" /> |
|
{:else if mediaType === 'video'} |
|
<video src={url} controls class="media-viewer-media" autoplay={false}> |
|
<track kind="captions" /> |
|
</video> |
|
{:else if mediaType === 'audio'} |
|
<audio src={url} controls class="media-viewer-audio" autoplay={false}></audio> |
|
{:else} |
|
<div class="media-viewer-unknown"> |
|
<p>Unsupported media type</p> |
|
<a href={url} target="_blank" rel="noopener noreferrer" class="media-viewer-link"> |
|
Open in new tab |
|
</a> |
|
</div> |
|
{/if} |
|
</div> |
|
</div> |
|
{/if} |
|
|
|
<style> |
|
.media-viewer-backdrop { |
|
position: fixed; |
|
top: 0; |
|
left: 0; |
|
right: 0; |
|
bottom: 0; |
|
background: rgba(0, 0, 0, 0.9); |
|
z-index: 10000; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
padding: 2rem; |
|
animation: fadeIn 0.2s ease-out; |
|
} |
|
|
|
@keyframes fadeIn { |
|
from { opacity: 0; } |
|
to { opacity: 1; } |
|
} |
|
|
|
.media-viewer-content { |
|
position: relative; |
|
max-width: 90vw; |
|
max-height: 90vh; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
} |
|
|
|
.media-viewer-close { |
|
position: absolute; |
|
top: -2.5rem; |
|
right: 0; |
|
background: rgba(255, 255, 255, 0.2); |
|
border: none; |
|
color: white; |
|
font-size: 2rem; |
|
width: 2.5rem; |
|
height: 2.5rem; |
|
border-radius: 50%; |
|
cursor: pointer; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
line-height: 1; |
|
transition: background 0.2s; |
|
} |
|
|
|
.media-viewer-close:hover { |
|
background: rgba(255, 255, 255, 0.3); |
|
} |
|
|
|
.media-viewer-media { |
|
max-width: 100%; |
|
max-height: 90vh; |
|
object-fit: contain; |
|
border-radius: 0.5rem; |
|
} |
|
|
|
.media-viewer-audio { |
|
width: 100%; |
|
max-width: 600px; |
|
} |
|
|
|
.media-viewer-unknown { |
|
background: var(--fog-post, #ffffff); |
|
padding: 2rem; |
|
border-radius: 0.5rem; |
|
text-align: center; |
|
color: var(--fog-text, #1f2937); |
|
} |
|
|
|
:global(.dark) .media-viewer-unknown { |
|
background: var(--fog-dark-post, #1f2937); |
|
color: var(--fog-dark-text, #f9fafb); |
|
} |
|
|
|
.media-viewer-link { |
|
display: inline-block; |
|
margin-top: 1rem; |
|
color: var(--fog-accent, #64748b); |
|
text-decoration: underline; |
|
} |
|
|
|
:global(.dark) .media-viewer-link { |
|
color: var(--fog-dark-accent, #94a3b8); |
|
} |
|
</style>
|
|
|