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

<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>