Improve gallery video UX and add upload-to-publish media workflow.

Stage raw files in upload/, publish with make sync-media/publish, and polish the lightbox: autoplay, remembered volume, Escape to close, and image/video icons without poster or caption clutter.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-04 23:55:43 +12:00
parent 6c215d40e6
commit 3f5235daaf
22 changed files with 644 additions and 119 deletions

193
app/scripts/sync-media.sh Executable file
View File

@@ -0,0 +1,193 @@
#!/usr/bin/env bash
# Copy photos and convert videos from upload/ into app/images/ (gallery source).
# Videos are always re-encoded for browser playback (H.264 + AAC, yuv420p, faststart).
#
# Usage (from app/):
# ./scripts/sync-media.sh
# UPLOAD_DIR=/upload IMAGES_DIR=/app/images ./scripts/sync-media.sh
#
# Requires: ffmpeg (for videos)
set -euo pipefail
upload_dir="${UPLOAD_DIR:-../upload}"
images_dir="${IMAGES_DIR:-images}"
dry_run=0
force=0
max_width=1920
crf=23
preset=medium
usage() {
sed -n '2,8p' "$0"
echo ""
echo "Options:"
echo " --upload-dir DIR Raw media source (default: ../upload)"
echo " --images-dir DIR Gallery output (default: images)"
echo " --dry-run Print actions only"
echo " --force Re-copy / re-encode even if output looks up to date"
echo " --max-width N Scale down wide video (default: 1920)"
echo " --crf N H.264 quality (default: 23)"
echo " -h, --help Show this help"
}
while [[ $# -gt 0 ]]; do
case "$1" in
--upload-dir) upload_dir=$2; shift 2 ;;
--images-dir) images_dir=$2; shift 2 ;;
--dry-run) dry_run=1; shift ;;
--force) force=1; shift ;;
--max-width) max_width=$2; shift 2 ;;
--crf) crf=$2; shift 2 ;;
-h | --help) usage; exit 0 ;;
*) echo "Unknown option: $1" >&2; usage; exit 1 ;;
esac
done
if ! command -v ffmpeg >/dev/null 2>&1; then
echo "ffmpeg is required." >&2
exit 1
fi
if [[ ! -d "$upload_dir" ]]; then
echo "Upload directory not found: $upload_dir" >&2
exit 1
fi
mkdir -p "$images_dir"
is_video_ext() {
case "${1,,}" in
.mp4 | .mov | .webm) return 0 ;;
*) return 1 ;;
esac
}
is_jpeg_ext() {
case "${1,,}" in
.jpg | .jpeg) return 0 ;;
*) return 1 ;;
esac
}
skip_name() {
local base=${1##*/}
[[ "$base" == .* ]] && return 0
[[ "$base" == *.web.mp4 ]] && return 0
[[ "$base" == *.converting.mp4 ]] && return 0
[[ "$base" == *.part ]] && return 0
return 1
}
skip_path() {
case "$1" in
*/thumbs/* | */hero/*) return 0 ;;
esac
return 1
}
needs_convert() {
local src=$1 dst=$2
if [[ -f "$dst" ]] && [[ "$force" -eq 0 ]] && [[ "$dst" -nt "$src" ]]; then
return 1
fi
return 0
}
copy_one() {
local src=$1 dst=$2 rel
rel=${src#"$upload_dir"/}
rel=${rel#/}
echo "→ copy $rel"
if [[ "$dry_run" -eq 1 ]]; then
echo " would write: ${dst#"$images_dir"/}"
return 0
fi
mkdir -p "$(dirname "$dst")"
cp -p "$src" "$dst"
echo " wrote: ${dst#"$images_dir"/}"
}
convert_one() {
local src=$1 dst=$2
local dir name tmp rel
dir=$(dirname "$dst")
name=$(basename "$dst" .mp4)
tmp="$dir/.${name}.converting.mp4"
rel=${src#"$upload_dir"/}
rel=${rel#/}
echo "→ convert $rel"
if [[ "$dry_run" -eq 1 ]]; then
echo " would write: ${dst#"$images_dir"/}"
return 0
fi
mkdir -p "$dir"
rm -f "$tmp"
trap 'rm -f "$tmp"' RETURN
ffmpeg -nostdin -hide_banner -loglevel error -y \
-i "$src" \
-vf "scale=min(${max_width}\\,iw):-2" \
-c:v libx264 -profile:v high -level:v 4.0 -pix_fmt yuv420p \
-crf "$crf" -preset "$preset" \
-c:a aac -b:a 128k -ac 2 \
-movflags +faststart \
-f mp4 \
"$tmp"
mv -f "$tmp" "$dst"
trap - RETURN
echo " wrote: ${dst#"$images_dir"/}"
}
copied=0
converted=0
skipped=0
while IFS= read -r -d '' src; do
base=$(basename "$src")
ext=".${base##*.}"
if skip_name "$base" || skip_path "$src"; then
continue
fi
rel=${src#"$upload_dir"/}
rel=${rel#/}
dst="$images_dir/$rel"
if is_jpeg_ext "$ext"; then
if [[ -f "$dst" ]] && [[ "$force" -eq 0 ]] && [[ "$dst" -nt "$src" ]]; then
((skipped++)) || true
continue
fi
copy_one "$src" "$dst"
((copied++)) || true
continue
fi
if ! is_video_ext "$ext"; then
continue
fi
dst="$images_dir/${rel%.*}.mp4"
if ! needs_convert "$src" "$dst"; then
((skipped++)) || true
continue
fi
convert_one "$src" "$dst"
((converted++)) || true
done < <(find "$upload_dir" -type f \( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.mov' -o -iname '*.mp4' -o -iname '*.webm' \) -print0)
echo ""
echo "Done. Copied: $copied, converted: $converted, skipped: $skipped."
if [[ "$dry_run" -eq 1 ]]; then
echo "(dry run — no files changed)"
fi