From f0438cae5e3028898e3f294bfe149da53abb1f5b Mon Sep 17 00:00:00 2001 From: Psychotoxical Date: Wed, 8 Apr 2026 12:46:58 +0200 Subject: [PATCH] fix(seekbar): pulsewave clean connection, retrotape rolling wheel at playhead MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pulsewave: draw flat played line only up to where the Gaussian envelope starts (startX), eliminating overlap between the flat line and the wave. Retrotape: replace dual fixed-position reels with a single spinning reel that tracks the playhead position across the full seek bar width. fix(i18n): remove literal 'N' from skip-star descriptions in all languages Replace 'After N skips' with natural phrasing ('after several skips') in EN, FR, NL, NB, ZH. Simplify ratingsSkipStarThresholdLabel to a single word (Skips / Sauts / Overslagen / Hopp / 跳过次数). DE was already fixed. Co-Authored-By: Claude Sonnet 4.6 --- src/components/WaveformSeek.tsx | 132 ++++++++++++++++---------------- src/locales/de.ts | 10 +-- src/locales/en.ts | 4 +- src/locales/fr.ts | 6 +- src/locales/nb.ts | 4 +- src/locales/nl.ts | 4 +- src/locales/zh.ts | 4 +- 7 files changed, 80 insertions(+), 84 deletions(-) diff --git a/src/components/WaveformSeek.tsx b/src/components/WaveformSeek.tsx index 26f69716..cc3c3053 100644 --- a/src/components/WaveformSeek.tsx +++ b/src/components/WaveformSeek.tsx @@ -381,16 +381,6 @@ function drawPulseWave( ctx.fillRect(0, cy - 1, buffered * w, 2); } - // Played flat line - if (progress > 0) { - ctx.globalAlpha = 1; - ctx.fillStyle = played; - ctx.shadowColor = played; - ctx.shadowBlur = 3; - ctx.fillRect(0, cy - 1, px, 2); - ctx.shadowBlur = 0; - } - // Animated pulse centered at playhead const pulseR = Math.min(38, w * 0.13); const amp = Math.min(h * 0.42, 5.5); @@ -398,6 +388,16 @@ function drawPulseWave( const startX = Math.max(0, px - pulseR); const endX = Math.min(w, px + pulseR); + // Flat played line up to where the wave envelope starts + if (progress > 0) { + ctx.globalAlpha = 1; + ctx.fillStyle = played; + ctx.shadowColor = played; + ctx.shadowBlur = 3; + ctx.fillRect(0, cy - 1, startX, 2); + ctx.shadowBlur = 0; + } + ctx.globalAlpha = 1; ctx.strokeStyle = played; ctx.lineWidth = 1.5; @@ -604,80 +604,76 @@ function drawLiquidFill( function drawRetroTape( canvas: HTMLCanvasElement, progress: number, - _buffered: number, + buffered: number, animState: AnimState, ) { const r = setupCanvas(canvas); if (!r) return; const { ctx, w, h } = r; - const { played, unplayed } = getColors(); + const { played, buffered: buffCol, unplayed } = getColors(); const cy = h / 2; - animState.angle += 0.042; + animState.angle += 0.055; - const maxR = Math.floor(h / 2) - 1; - const minR = Math.max(2, maxR * 0.28); - // supply reel (left): shrinks as progress increases - const rL = minR + (maxR - minR) * (1 - progress); - // takeup reel (right): grows as progress increases - const rR = minR + (maxR - minR) * progress; - const lx = maxR + 1; - const rx = w - maxR - 1; + const reelR = Math.min(h / 2 - 0.5, 9); + // Map progress to a center x that keeps the reel fully within the canvas + const px = reelR + (w - 2 * reelR) * progress; - // Tape between reels - const tapeLeft = lx + rL + 0.5; - const tapeRight = rx - rR - 0.5; - if (tapeRight > tapeLeft) { - ctx.globalAlpha = 0.45; - ctx.fillStyle = unplayed; - ctx.fillRect(tapeLeft, cy - 1.5, tapeRight - tapeLeft, 3); + // Background track + ctx.globalAlpha = 0.3; + ctx.fillStyle = unplayed; + ctx.fillRect(0, cy - 1, w, 2); + + if (buffered > 0) { + ctx.globalAlpha = 0.5; + ctx.fillStyle = buffCol; + ctx.fillRect(0, cy - 1, buffered * w, 2); } - const drawReel = ( - cx: number, - radius: number, - angle: number, - isRight: boolean, - ) => { - const color = isRight ? played : unplayed; - const alpha = isRight ? (progress > 0 ? 1 : 0.35) : (progress < 1 ? 0.55 : 0.35); - const glowing = isRight && progress > 0; - - ctx.globalAlpha = alpha; - if (glowing) { ctx.shadowColor = played; ctx.shadowBlur = 6; } - - // Outer ring - ctx.strokeStyle = color; - ctx.lineWidth = 1; - ctx.beginPath(); - ctx.arc(cx, cy, radius, 0, Math.PI * 2); - ctx.stroke(); + // Played portion — up to the left edge of the reel + if (progress > 0) { + ctx.globalAlpha = 1; + ctx.fillStyle = played; + ctx.shadowColor = played; + ctx.shadowBlur = 4; + ctx.fillRect(0, cy - 1, px - reelR, 2); ctx.shadowBlur = 0; + } - // Hub - const hubR = Math.max(1.5, radius * 0.3); - ctx.fillStyle = color; - ctx.beginPath(); - ctx.arc(cx, cy, hubR, 0, Math.PI * 2); - ctx.fill(); + // Spinning reel at playhead + ctx.globalAlpha = 1; + ctx.strokeStyle = played; + ctx.lineWidth = 1; + ctx.shadowColor = played; + ctx.shadowBlur = 7; - // Spokes (only if reel is large enough) - if (radius > hubR + 2.5) { - ctx.lineWidth = 0.9; - ctx.strokeStyle = color; - for (let s = 0; s < 3; s++) { - const a = angle + (s * Math.PI * 2) / 3; - ctx.beginPath(); - ctx.moveTo(cx + Math.cos(a) * (hubR + 0.5), cy + Math.sin(a) * (hubR + 0.5)); - ctx.lineTo(cx + Math.cos(a) * (radius - 0.5), cy + Math.sin(a) * (radius - 0.5)); - ctx.stroke(); - } + // Outer ring + ctx.beginPath(); + ctx.arc(px, cy, reelR, 0, Math.PI * 2); + ctx.stroke(); + ctx.shadowBlur = 0; + + // Hub + const hubR = Math.max(1.5, reelR * 0.28); + ctx.fillStyle = played; + ctx.beginPath(); + ctx.arc(px, cy, hubR, 0, Math.PI * 2); + ctx.fill(); + + // Spokes + if (reelR > hubR + 2) { + ctx.lineWidth = 0.9; + ctx.strokeStyle = played; + for (let s = 0; s < 3; s++) { + const a = animState.angle + (s * Math.PI * 2) / 3; + ctx.beginPath(); + ctx.moveTo(px + Math.cos(a) * (hubR + 0.5), cy + Math.sin(a) * (hubR + 0.5)); + ctx.lineTo(px + Math.cos(a) * (reelR - 0.5), cy + Math.sin(a) * (reelR - 0.5)); + ctx.stroke(); } - }; - - drawReel(lx, rL, -animState.angle, false); - drawReel(rx, rR, animState.angle, true); + } + ctx.shadowBlur = 0; ctx.globalAlpha = 1; } diff --git a/src/locales/de.ts b/src/locales/de.ts index b24fba70..d68c32ba 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -520,13 +520,13 @@ export const deTranslation = { tabSystem: 'System', tabGeneral: 'Allgemein', ratingsSectionTitle: 'Bewertungen', - ratingsSkipStarTitle: 'Überspringen für 1 Stern', + ratingsSkipStarTitle: 'Skip → 1 Stern', ratingsSkipStarDesc: - 'Bei N Überspringen hintereinander: Titel auf 1 Stern setzen. Nur für zuvor unbewertete Titel.', - ratingsSkipStarThresholdLabel: 'Überspringer bis 1★', - ratingsMixFilterTitle: 'Filter nach Bewertung', + 'Wird ein Titel mehrmals hintereinander übersprungen, bekommt er automatisch 1★. Nur für noch nicht bewertete Titel.', + ratingsSkipStarThresholdLabel: 'Skips', + ratingsMixFilterTitle: 'Nach Bewertung filtern', ratingsMixFilterDesc: - 'Inhalte mit niedriger Bewertung in {{mix}} und {{albums}} filtern. Erneut auf den gewählten Stern klicken, um die Schwelle zu deaktivieren.', + 'Gering bewertete Titel in {{mix}} und {{albums}} ausblenden. Nochmals auf den gewählten Stern klicken, um den Filter zu deaktivieren.', ratingsMixMinSong: 'Titel', ratingsMixMinAlbum: 'Alben', ratingsMixMinArtist: 'Interpreten', diff --git a/src/locales/en.ts b/src/locales/en.ts index 654cc814..5f9024b7 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -507,8 +507,8 @@ export const enTranslation = { ratingsSectionTitle: 'Ratings', ratingsSkipStarTitle: 'Skip for 1 star', ratingsSkipStarDesc: - 'After N skips in a row, set the track to 1★. Only for tracks not rated before.', - ratingsSkipStarThresholdLabel: 'Skips before 1★', + 'After several skips in a row, set the track to 1★. Only for tracks not yet rated.', + ratingsSkipStarThresholdLabel: 'Skips', ratingsMixFilterTitle: 'Filter by rating', ratingsMixFilterDesc: 'Filter low-rated items in {{mix}} and {{albums}}. Click the selected star again to turn off the threshold.', diff --git a/src/locales/fr.ts b/src/locales/fr.ts index e8c31c15..39c4066a 100644 --- a/src/locales/fr.ts +++ b/src/locales/fr.ts @@ -520,11 +520,11 @@ export const frTranslation = { ratingsSectionTitle: 'Notes', ratingsSkipStarTitle: 'Passer pour 1 étoile', ratingsSkipStarDesc: - 'Après N sauts d’affilée : mettre le morceau à 1 étoile. Uniquement s’il n’était pas encore noté.', - ratingsSkipStarThresholdLabel: 'Sauts avant 1★', + "Après plusieurs sauts d’affilée, le morceau passe à 1 étoile. Uniquement s’il n’était pas encore noté.", + ratingsSkipStarThresholdLabel: 'Sauts', ratingsMixFilterTitle: 'Filtrage par note', ratingsMixFilterDesc: - 'Filtrer le contenu peu noté dans {{mix}} et {{albums}}. Cliquer de nouveau sur l’étoile choisie désactive le seuil.', + "Filtrer le contenu peu noté dans {{mix}} et {{albums}}. Cliquer de nouveau sur l’étoile choisie désactive le seuil.", ratingsMixMinSong: 'Morceaux', ratingsMixMinAlbum: 'Albums', ratingsMixMinArtist: 'Artistes', diff --git a/src/locales/nb.ts b/src/locales/nb.ts index 2ee8069e..d4bd554b 100644 --- a/src/locales/nb.ts +++ b/src/locales/nb.ts @@ -503,8 +503,8 @@ export const nbTranslation = { ratingsSectionTitle: 'Vurderinger', ratingsSkipStarTitle: 'Hopp for 1 stjerne', ratingsSkipStarDesc: - 'Etter N hopp på rad: sett sporet til 1 stjerne. Bare for spor som ikke var vurdert før.', - ratingsSkipStarThresholdLabel: 'Hopp før 1★', + 'Etter flere hopp på rad: sett sporet til 1 stjerne. Bare for spor som ikke var vurdert før.', + ratingsSkipStarThresholdLabel: 'Hopp', ratingsMixFilterTitle: 'Filtrering etter vurdering', ratingsMixFilterDesc: 'Filtrer innhold med lav vurdering i {{mix}} og {{albums}}. Klikk på den valgte stjerna igjen for å slå av terskelen.', diff --git a/src/locales/nl.ts b/src/locales/nl.ts index c0806b00..8ce7f485 100644 --- a/src/locales/nl.ts +++ b/src/locales/nl.ts @@ -520,8 +520,8 @@ export const nlTranslation = { ratingsSectionTitle: 'Beoordelingen', ratingsSkipStarTitle: 'Overslaan voor 1 ster', ratingsSkipStarDesc: - 'Na N overslagen op rij: nummer op 1 ster zetten. Alleen voor nummers die nog niet beoordeeld waren.', - ratingsSkipStarThresholdLabel: 'Overslagen voor 1★', + 'Na meerdere overslagen op rij: nummer op 1 ster zetten. Alleen voor nummers die nog niet beoordeeld waren.', + ratingsSkipStarThresholdLabel: 'Overslagen', ratingsMixFilterTitle: 'Filter op beoordeling', ratingsMixFilterDesc: 'Content met lage beoordeling filteren in {{mix}} en {{albums}}. Klik opnieuw op de gekozen ster om de drempel uit te zetten.', diff --git a/src/locales/zh.ts b/src/locales/zh.ts index 47aafe45..33da6401 100644 --- a/src/locales/zh.ts +++ b/src/locales/zh.ts @@ -500,8 +500,8 @@ export const zhTranslation = { ratingsSectionTitle: '评分', ratingsSkipStarTitle: '跳过以评 1 星', ratingsSkipStarDesc: - '连续跳过 N 次后将曲目设为 1 星。仅适用于此前未评分的曲目。', - ratingsSkipStarThresholdLabel: '跳过次数(至 1★)', + '连续多次跳过后将曲目设为 1 星。仅适用于此前未评分的曲目。', + ratingsSkipStarThresholdLabel: '跳过次数', ratingsMixFilterTitle: '按评分筛选', ratingsMixFilterDesc: '在{{mix}}与{{albums}}中筛选低评分内容。再次点击所选星标可关闭阈值。',