fix: beautiful media lightbox, filebrowser noauth, deploy script

MediaLightbox: full glassmorphic redesign with dark backdrop, smooth
transitions, proper video/audio/image support. FileBrowser: noauth
config on persistent volume. Deploy script: fixed sed quoting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-04-11 22:49:01 -04:00
parent 2c98bdd19d
commit 8d82666c82
2 changed files with 274 additions and 122 deletions

View File

@@ -1,105 +1,107 @@
<template>
<Teleport to="body">
<div
v-if="show"
class="lightbox-backdrop"
@click.self="close"
@keydown="onKeydown"
tabindex="0"
ref="backdropEl"
>
<!-- Close button -->
<button class="lightbox-close" @click="close">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<!-- Counter -->
<div v-if="mediaItems.length > 1" class="lightbox-counter">
{{ currentIndex + 1 }} / {{ mediaItems.length }}
</div>
<!-- Previous button -->
<button
v-if="mediaItems.length > 1"
class="lightbox-nav lightbox-nav-prev"
@click.stop="prev"
<Transition name="lightbox-fade">
<div
v-if="show"
class="lightbox-backdrop"
@click.self="close"
@keydown="onKeydown"
tabindex="0"
ref="backdropEl"
>
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
</button>
<!-- Next button -->
<button
v-if="mediaItems.length > 1"
class="lightbox-nav lightbox-nav-next"
@click.stop="next"
>
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</button>
<!-- Media content -->
<div class="lightbox-content" @click.stop>
<!-- Loading -->
<div v-if="loading" class="lightbox-loading">
<div class="w-10 h-10 border-3 border-white/20 border-t-white/80 rounded-full animate-spin"></div>
<!-- Top bar -->
<div class="lightbox-topbar">
<div class="flex items-center gap-3 min-w-0">
<span v-if="mediaItems.length > 1" class="text-sm text-white/50">
{{ currentIndex + 1 }} / {{ mediaItems.length }}
</span>
<p class="text-sm text-white/80 truncate">{{ currentItem?.name }}</p>
</div>
<button class="lightbox-btn" @click="close">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<!-- Image -->
<img
v-else-if="currentItem && currentUrl && isImageFile(currentItem)"
:src="currentUrl"
:alt="currentItem.name"
class="lightbox-media"
@error="onMediaError"
/>
<!-- Video -->
<video
v-else-if="currentItem && currentUrl && isVideoFile(currentItem)"
:src="currentUrl"
:key="currentUrl"
class="lightbox-media"
controls
autoplay
@error="onMediaError"
/>
<!-- Audio -->
<div
v-else-if="currentItem && currentUrl && isAudioFile(currentItem)"
class="flex flex-col items-center justify-center gap-6 p-8"
<!-- Navigation arrows -->
<button
v-if="mediaItems.length > 1"
class="lightbox-nav lightbox-nav-prev"
@click.stop="prev"
>
<div class="w-32 h-32 rounded-2xl bg-gradient-to-br from-orange-500/20 to-orange-600/10 flex items-center justify-center">
<svg class="w-16 h-16 text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3" />
</svg>
<svg class="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
</button>
<button
v-if="mediaItems.length > 1"
class="lightbox-nav lightbox-nav-next"
@click.stop="next"
>
<svg class="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</button>
<!-- Media content -->
<div class="lightbox-content" @click.stop>
<!-- Loading -->
<div v-if="loading" class="flex items-center justify-center h-full">
<div class="w-12 h-12 border-3 border-white/10 border-t-white/70 rounded-full animate-spin"></div>
</div>
<audio
<!-- Image -->
<img
v-else-if="currentItem && currentUrl && isImageFile(currentItem)"
:src="currentUrl"
:key="currentUrl"
controls
autoplay
class="w-full max-w-md"
:alt="currentItem.name"
class="lightbox-media-img"
@error="onMediaError"
/>
</div>
<!-- Error -->
<div v-else-if="mediaError" class="lightbox-error">
<p class="text-white/60">Failed to load media</p>
<!-- Video -->
<video
v-else-if="currentItem && currentUrl && isVideoFile(currentItem)"
:src="currentUrl"
:key="currentUrl"
class="lightbox-media-video"
controls
autoplay
@error="onMediaError"
/>
<!-- Audio -->
<div
v-else-if="currentItem && currentUrl && isAudioFile(currentItem)"
class="lightbox-audio-container"
>
<div class="lightbox-audio-artwork">
<svg class="w-20 h-20 text-orange-400/80" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3" />
</svg>
</div>
<p class="text-white/70 text-sm mt-4 truncate max-w-xs">{{ currentItem.name }}</p>
<audio
:src="currentUrl"
:key="currentUrl"
controls
autoplay
class="lightbox-audio-player"
@error="onMediaError"
/>
</div>
<!-- Error -->
<div v-else-if="mediaError" class="flex flex-col items-center justify-center gap-3">
<svg class="w-12 h-12 text-white/20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4.5c-.77-.833-2.694-.833-3.464 0L3.34 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
<p class="text-white/40 text-sm">Failed to load media</p>
</div>
</div>
</div>
<!-- Filename -->
<div class="lightbox-filename">
<p class="text-sm text-white/70 truncate max-w-md">{{ currentItem?.name }}</p>
</div>
</div>
</Transition>
</Teleport>
</template>
@@ -125,7 +127,6 @@ const mediaError = ref(false)
const currentUrl = ref<string | null>(null)
const backdropEl = ref<HTMLElement | null>(null)
// Cache blob URLs to avoid re-fetching
const urlCache = new Map<string, string>()
const mediaItems = computed(() =>
@@ -212,19 +213,11 @@ function onMediaError() {
}
function onKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') {
e.preventDefault()
close()
} else if (e.key === 'ArrowLeft') {
e.preventDefault()
prev()
} else if (e.key === 'ArrowRight') {
e.preventDefault()
next()
}
if (e.key === 'Escape') { e.preventDefault(); close() }
else if (e.key === 'ArrowLeft') { e.preventDefault(); prev() }
else if (e.key === 'ArrowRight') { e.preventDefault(); next() }
}
// Load media when current item changes
watch(currentItem, (item) => {
if (item) {
loadMedia(item)
@@ -232,7 +225,6 @@ watch(currentItem, (item) => {
}
})
// Initialize when shown
watch(() => props.show, async (visible) => {
if (visible) {
currentIndex.value = props.startIndex
@@ -246,7 +238,6 @@ watch(() => props.show, async (visible) => {
}
}, { immediate: true })
// Clean up blob URLs on unmount
onUnmounted(() => {
for (const url of urlCache.values()) {
URL.revokeObjectURL(url)
@@ -254,3 +245,151 @@ onUnmounted(() => {
urlCache.clear()
})
</script>
<style scoped>
.lightbox-backdrop {
position: fixed;
inset: 0;
z-index: 60;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.92);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
outline: none;
}
.lightbox-topbar {
position: absolute;
top: 0;
left: 0;
right: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.5rem;
background: linear-gradient(to bottom, rgba(0,0,0,0.6) 0%, transparent 100%);
z-index: 10;
}
.lightbox-btn {
width: 2.5rem;
height: 2.5rem;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: rgba(255, 255, 255, 0.7);
background: rgba(255, 255, 255, 0.08);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.1);
transition: all 0.2s;
cursor: pointer;
}
.lightbox-btn:hover {
background: rgba(255, 255, 255, 0.15);
color: white;
}
.lightbox-nav {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 3rem;
height: 3rem;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: rgba(255, 255, 255, 0.5);
background: rgba(255, 255, 255, 0.06);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.08);
transition: all 0.2s;
cursor: pointer;
z-index: 10;
}
.lightbox-nav:hover {
background: rgba(255, 255, 255, 0.12);
color: white;
border-color: rgba(255, 255, 255, 0.15);
}
.lightbox-nav-prev { left: 1rem; }
.lightbox-nav-next { right: 1rem; }
.lightbox-content {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
padding: 4rem 5rem;
}
.lightbox-media-img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
border-radius: 0.5rem;
box-shadow: 0 25px 60px rgba(0, 0, 0, 0.5);
}
.lightbox-media-video {
max-width: 100%;
max-height: 100%;
border-radius: 0.75rem;
box-shadow: 0 25px 60px rgba(0, 0, 0, 0.5);
background: black;
}
.lightbox-audio-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1rem;
padding: 2rem;
}
.lightbox-audio-artwork {
width: 12rem;
height: 12rem;
border-radius: 1.5rem;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, rgba(251, 146, 60, 0.12) 0%, rgba(251, 146, 60, 0.04) 100%);
border: 1px solid rgba(251, 146, 60, 0.15);
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.3);
}
.lightbox-audio-player {
width: 100%;
max-width: 24rem;
margin-top: 1rem;
border-radius: 2rem;
filter: invert(1) hue-rotate(180deg) brightness(0.85) contrast(0.9);
}
/* Transitions */
.lightbox-fade-enter-active,
.lightbox-fade-leave-active {
transition: opacity 0.25s ease;
}
.lightbox-fade-enter-from,
.lightbox-fade-leave-to {
opacity: 0;
}
/* Mobile */
@media (max-width: 768px) {
.lightbox-content { padding: 3.5rem 1rem; }
.lightbox-nav { width: 2.5rem; height: 2.5rem; }
.lightbox-nav-prev { left: 0.5rem; }
.lightbox-nav-next { right: 0.5rem; }
.lightbox-audio-artwork { width: 8rem; height: 8rem; }
}
</style>

View File

@@ -955,7 +955,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q fedimint-gateway; th
if $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q '^lnd$' && [ -f "$LND_CERT" ] && [ -f "$LND_MACAROON" ]; then
log " LND detected — using lnd mode"
$DOCKER run -d --name fedimint-gateway --restart unless-stopped \
--health-cmd="curl -sf http://localhost:8175/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--health-cmd="curl -sf http://localhost:8176/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--memory=$(mem_limit fedimint-gateway) --network archy-net --network-alias fedimint-gateway \
--cap-drop ALL --cap-add CHOWN --cap-add FOWNER --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
--security-opt no-new-privileges:true \
@@ -972,7 +972,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q fedimint-gateway; th
else
log " No LND found — using ldk (built-in Lightning)"
$DOCKER run -d --name fedimint-gateway --restart unless-stopped \
--health-cmd="curl -sf http://localhost:8175/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--health-cmd="curl -sf http://localhost:8176/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--memory=$(mem_limit fedimint-gateway) --network archy-net --network-alias fedimint-gateway \
--cap-drop ALL --cap-add CHOWN --cap-add FOWNER --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
--security-opt no-new-privileges:true \
@@ -1137,20 +1137,28 @@ track_container "searxng"
# OnlyOffice removed — incompatible with rootless Podman (internal postgres/rabbitmq)
# CryptPad is the replacement (single Node.js process, e2e encrypted)
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q filebrowser; then
log "Creating File Browser..."
log "Creating File Browser (noauth — behind Archipelago login)..."
mkdir -p /var/lib/archipelago/filebrowser /var/lib/archipelago/filebrowser-data
# Pre-create default directories so FileBrowser doesn't 404 on first load
mkdir -p /var/lib/archipelago/filebrowser/{Documents,Photos,Music,Downloads,Builds}
# Config with noauth + database on persistent volume (survives container recreation)
cat > /var/lib/archipelago/filebrowser-data/.filebrowser.json << 'FBEOF'
{"port":80,"baseURL":"","address":"0.0.0.0","database":"/data/filebrowser.db","root":"/srv","log":"stdout"}
FBEOF
$DOCKER run -d --name filebrowser --restart unless-stopped \
--health-cmd="curl -sf http://localhost:80/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--health-cmd="wget -q --spider http://localhost:80/health || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--memory=$(mem_limit filebrowser) \
--cap-drop ALL --security-opt no-new-privileges:true \
--read-only --tmpfs=/tmp:rw,noexec,nosuid,size=256m --tmpfs=/run:rw,noexec,nosuid,size=64m \
--tmpfs=/tmp:rw,noexec,nosuid,size=256m --tmpfs=/run:rw,noexec,nosuid,size=64m \
-p 8083:80 \
-v /var/lib/archipelago/filebrowser:/srv \
-v /var/lib/archipelago/filebrowser-data:/data \
"$FILEBROWSER_IMAGE" \
--database=/data/database.db --root=/srv --address=0.0.0.0 --port=80 2>>"$LOG" || true
--config /data/.filebrowser.json 2>>"$LOG" || true
# Set noauth after first start (initializes database on volume)
sleep 3
$DOCKER exec filebrowser /filebrowser config set --auth.method=noauth --database /data/filebrowser.db 2>>"$LOG" || true
$DOCKER exec filebrowser /filebrowser users add admin admin --perm.admin --database /data/filebrowser.db 2>>"$LOG" || true
$DOCKER restart filebrowser 2>>"$LOG" || true
fi
track_container "filebrowser"
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q nginx-proxy-manager; then
@@ -1236,33 +1244,38 @@ fi
# 8b. Indeehub (pull from registry, or use local build)
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q indeedhub; then
INDEEDHUB_IMAGE=""
# Try local image first (pre-built or loaded from ISO)
if $DOCKER images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -q 'localhost/indeedhub'; then
INDEEDHUB_IMAGE="localhost/indeedhub:local"
# Try registry image
elif $DOCKER pull git.tx1138.com/lfg2025/indeedhub:local 2>>"$LOG"; then
INDEEDHUB_IMAGE="git.tx1138.com/lfg2025/indeedhub:local"
# Use image-versions.sh variable if sourced, otherwise detect
if [ -z "${INDEEDHUB_IMAGE:-}" ]; then
INDEEDHUB_IMAGE=""
# Try local image first (pre-built or loaded from ISO)
if $DOCKER images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -q 'localhost/indeedhub'; then
INDEEDHUB_IMAGE="localhost/indeedhub:local"
# Try pinned registry image
elif $DOCKER pull "$ARCHY_REGISTRY/indeedhub:1.0.0" --tls-verify=false 2>>"$LOG"; then
INDEEDHUB_IMAGE="$ARCHY_REGISTRY/indeedhub:1.0.0"
fi
fi
if [ -n "$INDEEDHUB_IMAGE" ]; then
log "Creating Indeehub from $INDEEDHUB_IMAGE..."
$DOCKER run -d --name indeedhub --restart unless-stopped \
--health-cmd="curl -sf http://localhost:80/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--network archy-net --network-alias indeedhub \
--health-cmd="curl -sf http://localhost:7777/health || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--memory=$(mem_limit indeedhub) \
--cap-drop ALL --security-opt no-new-privileges:true \
--read-only --tmpfs /tmp:rw,noexec,nosuid,size=64m --tmpfs /app/.next/cache:rw,noexec,nosuid,size=128m \
-p 8190:3000 \
-e NODE_ENV=production -e NEXT_TELEMETRY_DISABLED=1 \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
-p 7778:7777 \
"$INDEEDHUB_IMAGE" 2>>"$LOG" || true
# Fix IndeedHub for iframe: remove X-Frame-Options so it loads in Archipelago panel
sleep 2
if $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q "^indeedhub$"; then
$DOCKER exec indeedhub sed -i "/X-Frame-Options/d" /etc/nginx/conf.d/default.conf 2>/dev/null || true
# Fix Host header for NIP-98 auth — $host strips port, $http_host preserves it
$DOCKER exec indeedhub sed -i 's|proxy_set_header Host $host;|proxy_set_header Host $http_host;|g' /etc/nginx/conf.d/default.conf 2>/dev/null || true
if [ -f /opt/archipelago/web-ui/nostr-provider.js ]; then
$DOCKER cp /opt/archipelago/web-ui/nostr-provider.js indeedhub:/usr/share/nginx/html/nostr-provider.js 2>/dev/null || true
fi
$DOCKER exec indeedhub nginx -s reload 2>/dev/null || true
log "Applied IndeedHub iframe fix (removed X-Frame-Options)"
log "Applied IndeedHub iframe fix (X-Frame-Options, Host header, nostr-provider)"
fi
fi
fi