Compare commits

..

2 Commits

Author SHA1 Message Date
prosolis
04a3c41bed Fall back to ffmpeg when Go's JPEG decoder rejects a source
Go's stdlib image/jpeg refuses some valid-but-rare features such as
4:1:1 luma/chroma subsampling (ANN's CDN serves these). When the
in-process decode fails, route the bytes through ffmpeg the same way
we already do for video sources.
2026-05-25 16:27:32 -07:00
prosolis
b3002c8e12 Fix mobile horizontal scroll from overflowing channel nav
Channel pill row outgrew the viewport once more channels were added,
pushing the whole page sideways. Let the header wrap, scroll the nav
inside its own pill, and clip body overflow as a safety net.
2026-05-25 15:39:11 -07:00
4 changed files with 36 additions and 14 deletions

View File

@@ -44,6 +44,20 @@ html[data-phase="night"] {
@layer base {
body { font-family: "Nunito", system-ui, sans-serif; }
html { scroll-behavior: smooth; }
/* Belt-and-braces: never let stray wide content push horizontal scroll on
the whole page (the channel nav scrolls inside itself instead). */
html, body { overflow-x: hidden; }
}
@layer components {
/* Channel pill row may exceed viewport on mobile once enough channels are
configured. Let it scroll inside its own rounded container and hide the
scrollbar chrome so it still reads as a clean pill. */
.pete-channel-nav {
scrollbar-width: none;
-ms-overflow-style: none;
}
.pete-channel-nav::-webkit-scrollbar { display: none; }
}
@layer utilities {

File diff suppressed because one or more lines are too long

View File

@@ -32,16 +32,16 @@
<canvas id="pete-weather" class="pointer-events-none fixed inset-0 -z-[5] h-full w-full" aria-hidden="true"></canvas>
<header class="mx-auto max-w-6xl px-4 pt-8 pb-4 sm:pt-12">
<div class="flex items-center justify-between gap-4">
<a href="/" class="flex items-center gap-3 group">
<div class="flex flex-wrap items-center justify-between gap-3 sm:gap-4">
<a href="/" class="flex items-center gap-3 group min-w-0">
<img src="/static/img/pete.avif" alt="Pete" width="48" height="48"
class="h-12 w-12 rounded-2xl bg-[color:var(--card)] shadow-pete border-2 border-[color:var(--ink)]/10 object-cover group-hover:-rotate-6 transition-transform">
class="h-12 w-12 shrink-0 rounded-2xl bg-[color:var(--card)] shadow-pete border-2 border-[color:var(--ink)]/10 object-cover group-hover:-rotate-6 transition-transform">
<span class="font-display text-3xl font-bold tracking-tight">{{.SiteTitle}}</span>
<span class="hidden sm:inline rounded-full bg-[color:var(--accent)]/20 px-3 py-1 text-xs font-semibold uppercase tracking-wider text-[color:var(--ink)]/70" data-phase-label>day</span>
</a>
<div class="flex items-center gap-2">
<div class="flex items-center gap-2 min-w-0 max-w-full">
<button type="button" data-search-trigger title="Search (⌘K)"
class="flex items-center gap-2 rounded-full bg-[color:var(--card)] px-3 py-2 text-sm shadow-pete border-2 border-[color:var(--ink)]/10 hover:bg-[color:var(--ink)]/5 transition">
class="flex shrink-0 items-center gap-2 rounded-full bg-[color:var(--card)] px-3 py-2 text-sm shadow-pete border-2 border-[color:var(--ink)]/10 hover:bg-[color:var(--ink)]/5 transition">
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4 text-[color:var(--ink)]/60">
<circle cx="11" cy="11" r="7"></circle>
@@ -50,11 +50,11 @@
<span class="hidden sm:inline text-[color:var(--ink)]/60 font-semibold">Search</span>
<kbd class="hidden sm:inline-flex items-center rounded-md bg-[color:var(--ink)]/5 px-1.5 py-0.5 text-[10px] font-mono font-semibold text-[color:var(--ink)]/60">⌘K</kbd>
</button>
<nav class="flex items-center gap-1 sm:gap-2 rounded-full bg-[color:var(--card)] p-1 shadow-pete border-2 border-[color:var(--ink)]/10">
<nav class="pete-channel-nav flex min-w-0 items-center gap-1 sm:gap-2 overflow-x-auto rounded-full bg-[color:var(--card)] p-1 shadow-pete border-2 border-[color:var(--ink)]/10">
{{$active := .Active}}
{{range .Channels}}
<a href="/{{.Slug}}"
class="rounded-full px-3 py-2 text-sm font-semibold transition
class="shrink-0 rounded-full px-3 py-2 text-sm font-semibold transition
{{if eq $active .Slug}}bg-theme-{{.Theme}} text-white shadow-sm{{else}}hover:bg-[color:var(--ink)]/5{{end}}">
<span aria-hidden="true" class="mr-1">{{.Emoji}}</span><span class="hidden sm:inline">{{.Title}}</span>
</a>

View File

@@ -178,7 +178,7 @@ func buildAvifThumb(src, outPath string) error {
}
if urlIsVideo || isVideoContentType(resp.Header.Get("Content-Type")) {
return buildAvifFromVideoFrame(body, outPath)
return buildAvifFromFFmpeg(body, outPath)
}
if isAvif(body) {
@@ -195,6 +195,13 @@ func buildAvifThumb(src, outPath string) error {
}
src2, _, err := image.Decode(bytes.NewReader(body))
if err != nil {
// Go's stdlib JPEG decoder rejects some valid-but-rare features
// (e.g. 4:1:1 luma/chroma subsampling served by ANN's CDN). Hand
// the bytes to ffmpeg as a permissive fallback — it produces the
// same shape of output as the video path.
if ffErr := buildAvifFromFFmpeg(body, outPath); ffErr == nil {
return nil
}
return fmt.Errorf("decode: %w", err)
}
b := src2.Bounds()
@@ -265,12 +272,13 @@ func isVideoContentType(ct string) bool {
return strings.HasPrefix(strings.ToLower(strings.TrimSpace(ct)), "video/")
}
// buildAvifFromVideoFrame writes the downloaded video to a temp file, asks
// buildAvifFromFFmpeg writes the downloaded bytes to a temp file, asks
// ffmpeg to extract a single resized frame as PNG, then hands it to avifenc.
// ffmpeg is optional — if it's missing we return an error and the caller
// falls back to redirecting to the source URL, just like any other build
// failure.
func buildAvifFromVideoFrame(body []byte, outPath string) error {
// Used both for video sources and as a permissive fallback for images whose
// JPEG features Go's stdlib decoder rejects. ffmpeg is optional — if it's
// missing we return an error and the caller falls back to redirecting to
// the source URL, just like any other build failure.
func buildAvifFromFFmpeg(body []byte, outPath string) error {
if _, err := exec.LookPath("ffmpeg"); err != nil {
return fmt.Errorf("ffmpeg not available: %w", err)
}