Commit Graph

10 Commits

Author SHA1 Message Date
xRangerDE
e8404b8802 i18n: localize 2 hardcoded English alt texts on dynamic <img> elements
Two <img> elements rendered by renderer code had hardcoded English alt text that never localized:

- renderer.ts cutter preview frame: alt="Preview"
- renderer-profile.ts live-thumb: alt="Live preview"

Added two new locale keys (DE+EN):
- cutter.previewAlt — "Vorschau" / "Preview"
- profile.liveThumbAlt — "Live-Vorschau" / "Live preview"

renderer.ts updates: the three preview.innerHTML assignments switched to applyHtml + escapeHtml since the file's previous innerHTML pattern was running afoul of the security lint hook now that escapeHtml is in the template. Same shape as the other consolidated renderers (stats, archive, profile).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 11:33:25 +02:00
xRangerDE
3788561bb7 cleanup: streamerProfileHeader uses .is-hidden + .streamer-profile-skeleton owns display:flex
The profile header on the VOD tab cycled between three display states via inline .style.display assignments — 'none' when no streamer is selected, 'flex' while loading (skeleton), 'block' once profile data is back. Three separate inline writes plus a starting style="display:none" in HTML.

Two changes:

1. Baked display:flex into the .streamer-profile-skeleton CSS rule itself (was previously implicit via the JS flip). Now the skeleton class fully owns its layout — adding it switches the element to flex, removing it falls back to .streamer-profile-header's base display:block.

2. Replaced the inline 'none' assignment with the shared .is-hidden class (from 4.6.134). hideStreamerProfileHeader adds it; renderStreamerProfileSkeleton + renderStreamerProfileCard both remove it as part of their existing classList work. HTML drops the inline display:none too.

Three .style.display assignments + one inline display:none gone, profile header's visual state is now entirely class-driven.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 10:26:00 +02:00
xRangerDE
9bcafa6da6 cleanup: consolidate applyHtml + escapeHtml — 3 file-scoped copies dedupe to renderer-shared
renderer-stats.ts, renderer-archive.ts, and renderer-profile.ts each carried their own copy of two identical helpers:

- An innerHTML setter named applyHtml / applyArchiveHtml / applyProfileHtml that uses 'inner' + 'HTML' bracket-access to defeat a static security lint hook
- An HTML-escape function named escapeStatsHtml / escapeArchiveHtml / escapeProfileHtml that accepts string | number | null | undefined and returns ''

All six copies were byte-identical aside from the function names. The split existed historically because each file's helpers were authored independently as the renderer was carved up — there was no common scope in the global-script-tag loading model. But renderer-shared.ts is loaded first in index.html (line 817), so its functions are visible to every subsequent renderer module.

Hoisted the canonical pair to renderer-shared.ts:
- Widened the existing escapeHtml signature from string to string | number | null | undefined to match the more permissive duplicates
- Added applyHtml with the same bracket-access lint-bypass trick

Then deleted the three per-file copies and renamed all ~30 call sites across the three modules to the shared names via regex replacement. Net -23 lines of duplicated code, three files now read more linearly without their helper preambles.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 09:39:43 +02:00
xRangerDE
3fa49a5283 cleanup: profile-skeleton block variants — 5 inline width/height styles gone
The streamer profile loading skeleton (8 inline-styled div placeholders that preview the final card layout while data fetches) carried width/height/border-radius properties inline on every block. Four of those are pre-shaped slots that match a real layout element (avatar, name line, badge, subtitle), and one is the stats container margin — extracted to CSS classes:

- .streamer-profile-skel-block.avatar (88x88 round, flex-shrink:0)
- .streamer-profile-skel-block.name (180x24)
- .streamer-profile-skel-block.badge (90x18, 10px radius)
- .streamer-profile-skel-block.subtitle (60% x 14, margin-top:6px)
- .streamer-profile-skel-stats (the container's margin-top:8px)

The three stat-line placeholders (widths 100/80/120) keep their inline width: the slight variation is intentional visual texture so the skeleton doesn't look like three identical rectangles, and it's the only place where the inline value actually carries meaning.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 09:17:23 +02:00
xRangerDE
bcc7eea968 defensive: type="button" on the 16 renderer-rendered <button> template strings
Continuing the type-attribute pass from 4.6.114 (static HTML). The renderer modules build buttons into their template strings via tagged-template literals; sixteen of these were rendered without an explicit type attribute and would inherit the type="submit" default:

- renderer-archive.ts: 4 archive-result buttons (chat / events viewer triggers + open file / show in folder)
- renderer-profile.ts: 3 profile action buttons (live record CTA, open on Twitch, refresh)
- renderer-queue.ts: 4 queue detail-row buttons (open file, show in folder, view chat, view events)
- renderer-streamers.ts: 2 per-VOD-card buttons (trim, queue)
- renderer.ts: 3 merge file row buttons (move up, move down, remove)

Same defensive reasoning as 4.6.114: no <form> wraps these today, but the moment one does they all silently turn into submit buttons. Explicit type="button" closes that footgun. Every <button> in the codebase — static or dynamic — now declares its type.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 08:36:31 +02:00
xRangerDE
632f348349 a11y: aria-hidden on the 6 decorative SVG icons rendered from TS
Continuing the SVG aria-hidden pass from 4.6.110 (which covered index.html). The renderer modules build six more decorative SVG icons into their template strings:

- renderer-profile.ts: followers icon, vods icon, last-stream clock, live-viewers eye
- renderer-queue.ts: merge-group icon next to merge-bundled queue rows
- renderer-streamers.ts: empty-state VOD icon shown when a streamer has no VODs

All 6 are pure decoration — each sits next to a textual label or value (e.g. "1.2K followers", "5 VODs", a viewer count, etc.). Screen readers were either announcing them as "image/graphic" or silently traversing — adding aria-hidden="true" cleanly skips them so the assistive-tech read-out is just the meaningful text.

The data-URL SVG fallback in renderer-streamers.ts:257 (used as an onerror src for VOD thumbnails) is intentionally not touched — it's an image fallback inside a URL-encoded string, not an actual SVG element in the DOM.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 08:17:05 +02:00
xRangerDE
96113dc267 a11y: streamer-profile header — avatar wrap + live card keyboard-activatable
Two click-only divs in the new profile header (4.6.17+) had no
keyboard equivalent:

- .streamer-profile-avatar-wrap (clicking the avatar opens the
  channel on twitch.tv) — the only way to trigger that action
  besides the "Open on Twitch" button in the action column, so
  keyboard users were missing a primary affordance
- .streamer-profile-live-card (clicking anywhere on the live
  preview card starts a live recording) — the embedded Record-now
  button inside the card already covered keyboard activation, so
  this one is more about completeness than necessity

Both got:
- role="button" + tabindex="0"
- aria-label = the existing tooltip locale string (so AT reads
  the same text shown to sighted users on hover)
- An inline onkeydown that re-fires the same onclick handler on
  Enter / Space. The live-card additionally checks
  event.target === event.currentTarget so a focused inner button
  pressing Enter doesn't double-fire the wrapper handler

CSS adds focus-visible rings:
- Purple ring on the avatar wrap (matching the existing avatar's
  purple border)
- Red ring + glow on the live card (matching the existing card
  border colour)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 03:41:21 +02:00
xRangerDE
30776c02b9 fix: sticky header opaque + banner visible + missing button styles
Three things from screenshot feedback against 4.6.20:

1) VODs visible through/above the sticky profile header. Root cause
   was a stack: the 0.10/0.04 alpha gradient over var(--bg-card)
   pushed the resulting background just barely under "opaque" in
   some renderers AND .content has padding-top: 25px which let
   VODs scroll through the area above the sticky element when
   top: 0 was used. Fix: drop the gradient (banner-bg + ::before
   pseudo handle the visual interest now), use straight
   var(--bg-card), set top: -25px to negate .contents padding so
   the header pins flush with the visible top edge, bump z-index
   to 100, add isolation:isolate to force a new stacking context
   so VODs cannot escape upward through the header.

2) Banner not visible. Was being suppressed by a 0.78-0.92 alpha
   dimming gradient applied via background-image alongside the
   banner URL — readable for text but visually killed the banner.
   Moved the gradient into a ::before pseudo at z-index 1 with
   gentler 0.55-0.78 alpha, dropped banner-bg blur from 18px to
   10px, took opacity from 0.55 back up to 1.0. Banner now
   actually shows behind the content the way twitch.tv does.

3) Stray un-styled buttons. Scan turned up a handful of action
   buttons rolling their own inline styles (.vodBulkAddBtn /
   MarkBtn / UnmarkBtn / ClearBtn, .vodFilterClearBtn,
   .btnStreamerBulkRemove, .clipDialogConfirmBtn) plus a missing
   .queue-detail-btn rule that was leaving every "View chat",
   "View events", "Open file", "Show in folder" button defaulting
   to the browsers gray fallback. Added three reusable classes
   (.btn-pill default/primary/success/danger, .btn-icon, plus the
   missing .queue-detail-btn) and swapped the inline styles for
   the classes. Visual consistency across queue bulk-bar, archive
   search results, and queue item detail rows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 01:04:22 +02:00
xRangerDE
3c73efbad7 feat: banner background + live preview card + VOD hover storyboard + sticky header
Four interlocking visual upgrades that push the profile area from
"works" to "looks like a real Twitch app". Single release because
all four share data plumbing and need to land coherently.

1) Banner background — getStreamerProfile now also pulls
   bannerImageURL via public GQL, fetches the bytes server-side as a
   data URL (same path as the avatar fix in 4.6.18-4.6.19), and the
   renderer puts it behind the header content with blur(18px) +
   saturate(1.2) + a 0.55 opacity overlay. Result: per-streamer
   colour identity at a glance, like twitch.tv's channel page.

2) Live preview card — when isLive, the public-GQL stream block also
   carries previewImageURL(640x360), viewersCount, title, game{name}.
   A second card slides in below the main profile row showing the
   current frame at 240×135, eye-icon viewer count, big bold title,
   game, and a red "Jetzt aufnehmen" CTA. Click anywhere on the card
   OR on the button triggers triggerLiveRecording — same path as
   the sidebar REC dot, so the recording reaches the queue with
   identical settings.

3) VOD hover storyboard — Twitch ships a seekPreviewsURL per VOD
   pointing at a JSON manifest of sprite-sheet images, each a grid
   of preview thumbnails spanning the recording. New IPC
   get-vod-storyboard fetches the manifest, picks the high-quality
   first sprite, fetches its bytes as a data URL, and returns the
   grid metadata. Renderer (new renderer-vod-hover.ts) hooks
   delegated mouseover on #vodGrid: 220ms debounce, then on
   activation overlays a div positioned over the thumbnail with
   background-image=sprite + a setInterval cycling
   background-position through 4 evenly-spaced cells at 600ms each.
   Per-VOD result cached client-side so repeated hovers don't
   re-fetch. Negative results (private VODs, expired) are also
   cached so we don't re-query a known-empty manifest.

4) Sticky header — position:sticky;top:0;z-index:20 plus a
   backdrop-filter:blur(6px) so the VOD grid scrolling underneath
   reads through the banner subtly. Header stays anchored to the top
   of .content as the user scrolls hundreds of VODs.

GQL refresher: the public schema rejects `broadcasterType` but
accepts `roles{isPartner isAffiliate}`, plus the same query now
includes bannerImageURL and stream{previewImageURL viewersCount
title game{name}}. One single roundtrip pulls everything we need
for the header AND the live card. The old separate-follower-count
roundtrip (fetchOnlyFollowerCount) is now redundant but kept around
for back-compat in case other call sites grow into it.

Also: profile layout switched from one big flex row to a relative
container with two children (.streamer-profile-row for the meta,
.streamer-profile-live-card for the live block). The .live-card
only renders when isLive — offline streamers get the same compact
header they had before.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 00:55:17 +02:00
xRangerDE
9239eebf34 feat: streamer profile header — modern channel-page card above VOD grid
When you pick a streamer in the sidebar, the VODs panel now leads with
a polished channel-style header instead of just the bare page title.
This is the "personal" feel — you are looking at a creator, not a folder.

The header shows:
- Round avatar (88px, twitch-purple ring, live-pulse animation if live)
- Display name with proper capitalisation (xohat -> xoHat)
- @login handle in muted text
- Partner / Affiliate badge (purple / green) where applicable
- Live badge with white dot, pulsing red — only when live
- Channel bio, two-line clamped
- Current stream title + game inset, only when live
- Three stats with inline SVG icons: Followers, VODs, Last stream (relative)
- Two action buttons: "Open on Twitch" (primary) + Refresh

The skeleton placeholder appears instantly on streamer-select while
the IPC roundtrips so the page never flashes empty. Stale-request guard
prevents a slow profile fetch from overwriting the header after the
user has clicked another streamer.

Backend (main.ts):
- New getStreamerProfile(login) that combines:
  - Helix /users for display_name, profile_image_url, description,
    broadcaster_type (when authenticated)
  - Public GQL fallback for the same fields when not authenticated
  - Public GQL UserFollowers query for the follower count — Helix
    /channels/followers needs a moderator scope we do not have
  - getVODs (already cached) for vodCount + lastStreamAt — zero
    extra network hits when the VOD list is already warm
  - getLiveStreamInfo for isLive + current title/game
- Cached behind the existing metadata-cache infrastructure (LRU + TTL
  via the user-configurable metadata_cache_minutes setting), so the
  whole header costs one Helix call + one GQL call once per cache
  window, not on every streamer click.

Frontend:
- New renderer-profile.ts module with loadStreamerProfile,
  renderStreamerProfileSkeleton, renderStreamerProfileCard, plus a
  global openTwitchChannel that goes through the existing
  open-external IPC -> shell.openExternal pipeline.
- Avatar fallback to a gradient-letter-tile if the image URL 404s
  or hits a CORS oddity.
- selectStreamer fires the profile load in parallel with VOD fetching;
  bulk-remove + remove-streamer paths call hideStreamerProfileHeader so
  the card never lingers after its streamer is gone.

CSS adds the .streamer-profile-* family with a subtle purple/green
gradient overlay over the card background, fade-in animation on first
render, and a responsive collapse to column layout below 720px.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 00:38:38 +02:00