Compare commits

..

6 Commits

Author SHA1 Message Date
Hördle Bot
be7eda63e2 Bump version to 0.1.6.37 2026-01-24 13:03:38 +01:00
Hördle Bot
2a99f545ef Fix: Zeige Ergebnis statt Solve/Give Up Button bei bereits abgeschlossenen Rätseln
- Verwende gameState.isSolved/isFailed direkt für UI-Logik
- Behebt Problem, dass Solve/Give Up Button bei zurückkehrenden Rätseln angezeigt wurde
- isSolved/isFailed werden jetzt direkt aus gameState gelesen für sofortige Konsistenz
2026-01-24 13:00:51 +01:00
Hördle Bot
6be813fb00 Fix: AudioPlayer startet jetzt korrekt bei startTime + Deployment-Version
- deploy.sh übergibt jetzt explizit APP_VERSION als Build-Argument
- AudioPlayer setzt startTime korrekt beim ersten manuellen Play
- Verbesserte Position-Logik in togglePlay() mit Timeout-Bestätigung
- Behebt Problem, dass Specials beim ersten Segment statt bei startTime starteten
2026-01-24 12:51:50 +01:00
Hördle Bot
71c7f2aab5 Bump version to 0.1.6.36 2026-01-24 12:43:30 +01:00
Hördle Bot
096682929d Fix: Skip-Button startet jetzt beim nächsten Segment + Initialisierung für Specials
- autoPlay verwendet jetzt startPos statt startTime beim Skip
- hasPlayedOnce wird nur bei Song-Wechsel zurückgesetzt, nicht bei mehr Zeit
- processedSrc/processedUnlockedSeconds initial auf null für korrekte Initialisierung
- Sicherstellt, dass Specials weiterhin vom markierten Ausschnitt starten
2026-01-24 12:42:26 +01:00
Hördle Bot
cebdf7a5a2 Fix: Specials-Rätsel spielen jetzt korrekt vom markierten Ausschnitt
- AudioPlayer setzt currentTime jetzt korrekt auf startTime beim Start
- Behebt Bug, bei dem Specials-Rätsel immer vom Anfang des Titels starteten
- Berücksichtigt startTime in togglePlay(), play() und autoPlay
2026-01-24 12:29:03 +01:00
4 changed files with 128 additions and 33 deletions

View File

@@ -22,8 +22,8 @@ const AudioPlayer = forwardRef<AudioPlayerRef, AudioPlayerProps>(({ src, unlocke
const [progress, setProgress] = useState(0); const [progress, setProgress] = useState(0);
const [hasPlayedOnce, setHasPlayedOnce] = useState(false); const [hasPlayedOnce, setHasPlayedOnce] = useState(false);
const [processedSrc, setProcessedSrc] = useState(src); const [processedSrc, setProcessedSrc] = useState<string | null>(null);
const [processedUnlockedSeconds, setProcessedUnlockedSeconds] = useState(unlockedSeconds); const [processedUnlockedSeconds, setProcessedUnlockedSeconds] = useState<number | null>(null);
useEffect(() => { useEffect(() => {
console.log('[AudioPlayer] MOUNTED'); console.log('[AudioPlayer] MOUNTED');
@@ -41,7 +41,7 @@ const AudioPlayer = forwardRef<AudioPlayerRef, AudioPlayerProps>(({ src, unlocke
let startPos = startTime; let startPos = startTime;
// If same song but more time unlocked, start from where previous segment ended // If same song but more time unlocked, start from where previous segment ended
if (src === processedSrc && unlockedSeconds > processedUnlockedSeconds) { if (processedSrc !== null && src === processedSrc && processedUnlockedSeconds !== null && unlockedSeconds > processedUnlockedSeconds) {
startPos = startTime + processedUnlockedSeconds; startPos = startTime + processedUnlockedSeconds;
} }
@@ -62,8 +62,11 @@ const AudioPlayer = forwardRef<AudioPlayerRef, AudioPlayerProps>(({ src, unlocke
const initialPercent = unlockedSeconds > 0 ? (initialElapsed / unlockedSeconds) * 100 : 0; const initialPercent = unlockedSeconds > 0 ? (initialElapsed / unlockedSeconds) * 100 : 0;
setProgress(Math.min(initialPercent, 100)); setProgress(Math.min(initialPercent, 100));
setHasPlayedOnce(false); // Reset for new segment // Only reset hasPlayedOnce if the song changed, not if just more time was unlocked
if (processedSrc !== null && src !== processedSrc) {
setHasPlayedOnce(false); // Reset for new song
onHasPlayedChange?.(false); // Notify parent onHasPlayedChange?.(false); // Notify parent
}
// Update processed state // Update processed state
setProcessedSrc(src); setProcessedSrc(src);
@@ -72,7 +75,11 @@ const AudioPlayer = forwardRef<AudioPlayerRef, AudioPlayerProps>(({ src, unlocke
if (autoPlay) { if (autoPlay) {
// Delay play slightly to ensure currentTime sticks // Delay play slightly to ensure currentTime sticks
setTimeout(() => { setTimeout(() => {
const playPromise = audioRef.current?.play(); if (audioRef.current) {
// Use startPos (which may be startTime + processedUnlockedSeconds if more time was unlocked)
// instead of always using startTime
audioRef.current.currentTime = startPos;
const playPromise = audioRef.current.play();
if (playPromise !== undefined) { if (playPromise !== undefined) {
playPromise playPromise
.then(() => { .then(() => {
@@ -86,8 +93,16 @@ const AudioPlayer = forwardRef<AudioPlayerRef, AudioPlayerProps>(({ src, unlocke
setIsPlaying(false); setIsPlaying(false);
}); });
} }
}
}, 150); }, 150);
} }
} else if (startTime !== undefined && startTime > 0) {
// If startTime is set and we haven't processed changes, ensure currentTime is at least at startTime
// This handles the case where the audio element was reset or reloaded, or when manually playing for the first time
const current = audioRef.current.currentTime;
if (current < startTime) {
audioRef.current.currentTime = startTime;
}
} }
} }
}, [src, unlockedSeconds, startTime, autoPlay, processedSrc, processedUnlockedSeconds]); }, [src, unlockedSeconds, startTime, autoPlay, processedSrc, processedUnlockedSeconds]);
@@ -97,6 +112,16 @@ const AudioPlayer = forwardRef<AudioPlayerRef, AudioPlayerProps>(({ src, unlocke
play: () => { play: () => {
if (!audioRef.current) return; if (!audioRef.current) return;
// Check if we need to reset to startTime
const current = audioRef.current.currentTime;
const elapsed = current - startTime;
// Reset if: never played before, current position is before startTime, or we've exceeded the unlocked segment
if (!hasPlayedOnce || current < startTime || elapsed >= unlockedSeconds) {
// Reset to start of segment
audioRef.current.currentTime = startTime;
}
const playPromise = audioRef.current.play(); const playPromise = audioRef.current.play();
if (playPromise !== undefined) { if (playPromise !== undefined) {
playPromise playPromise
@@ -121,8 +146,35 @@ const AudioPlayer = forwardRef<AudioPlayerRef, AudioPlayerProps>(({ src, unlocke
if (isPlaying) { if (isPlaying) {
audioRef.current.pause(); audioRef.current.pause();
setIsPlaying(false);
} else { } else {
// Ensure we're at the correct position before playing
const current = audioRef.current.currentTime;
const elapsed = current - startTime;
// Determine target position
let targetPos = startTime;
// If we've played before and we're within the unlocked segment, continue from current position
if (hasPlayedOnce && current >= startTime && elapsed < unlockedSeconds) {
targetPos = current; // Continue from current position
} else {
// Reset to start of segment if: never played, before startTime, or exceeded unlocked segment
targetPos = startTime;
}
// Set position before playing
audioRef.current.currentTime = targetPos;
// Ensure position sticks (browser might reset it)
setTimeout(() => {
if (audioRef.current && Math.abs(audioRef.current.currentTime - targetPos) > 0.5) {
audioRef.current.currentTime = targetPos;
}
}, 50);
audioRef.current.play(); audioRef.current.play();
setIsPlaying(true);
onPlay?.(); onPlay?.();
if (hasPlayedOnce) { if (hasPlayedOnce) {
@@ -132,7 +184,6 @@ const AudioPlayer = forwardRef<AudioPlayerRef, AudioPlayerProps>(({ src, unlocke
onHasPlayedChange?.(true); // Notify parent onHasPlayedChange?.(true); // Notify parent
} }
} }
setIsPlaying(!isPlaying);
}; };
const handleTimeUpdate = () => { const handleTimeUpdate = () => {

View File

@@ -96,6 +96,10 @@ export default function Game({ dailyPuzzle, genre = null, isSpecial = false, max
if (gameState.isSolved && !gameState.yearGuessed && dailyPuzzle?.releaseYear) { if (gameState.isSolved && !gameState.yearGuessed && dailyPuzzle?.releaseYear) {
setShowYearModal(true); setShowYearModal(true);
} }
} else {
// Reset states when gameState is null (e.g., during loading)
setHasWon(false);
setHasLost(false);
} }
}, [gameState, dailyPuzzle]); }, [gameState, dailyPuzzle]);
@@ -164,6 +168,10 @@ export default function Game({ dailyPuzzle, genre = null, isSpecial = false, max
); );
if (!gameState) return <div>{t('loadingState')}</div>; if (!gameState) return <div>{t('loadingState')}</div>;
// Use gameState directly for isSolved/isFailed to ensure consistency when returning to completed puzzles
const isSolved = gameState?.isSolved ?? hasWon;
const isFailed = gameState?.isFailed ?? hasLost;
const handleGuess = (song: any) => { const handleGuess = (song: any) => {
if (isProcessingGuess) return; if (isProcessingGuess) return;
// Prevent guessing if already solved or failed // Prevent guessing if already solved or failed
@@ -176,6 +184,7 @@ export default function Game({ dailyPuzzle, genre = null, isSpecial = false, max
if (song.id === dailyPuzzle.songId) { if (song.id === dailyPuzzle.songId) {
addGuess(song.title, true); addGuess(song.title, true);
setHasWon(true); setHasWon(true);
// gameState.isSolved will be updated by useGameState
// Track puzzle solved event // Track puzzle solved event
if (typeof window !== 'undefined' && window.plausible) { if (typeof window !== 'undefined' && window.plausible) {
window.plausible('puzzle_solved', { window.plausible('puzzle_solved', {
@@ -196,6 +205,7 @@ export default function Game({ dailyPuzzle, genre = null, isSpecial = false, max
if (gameState.guesses.length + 1 >= maxAttempts) { if (gameState.guesses.length + 1 >= maxAttempts) {
setHasLost(true); setHasLost(true);
setHasWon(false); setHasWon(false);
// gameState.isFailed will be updated by useGameState
// Track puzzle lost event // Track puzzle lost event
if (typeof window !== 'undefined' && window.plausible) { if (typeof window !== 'undefined' && window.plausible) {
window.plausible('puzzle_solved', { window.plausible('puzzle_solved', {
@@ -236,6 +246,7 @@ export default function Game({ dailyPuzzle, genre = null, isSpecial = false, max
if (gameState.guesses.length + 1 >= maxAttempts) { if (gameState.guesses.length + 1 >= maxAttempts) {
setHasLost(true); setHasLost(true);
setHasWon(false); setHasWon(false);
// gameState.isFailed will be updated by useGameState
// Track puzzle lost event // Track puzzle lost event
if (typeof window !== 'undefined' && window.plausible) { if (typeof window !== 'undefined' && window.plausible) {
window.plausible('puzzle_solved', { window.plausible('puzzle_solved', {
@@ -260,6 +271,7 @@ export default function Game({ dailyPuzzle, genre = null, isSpecial = false, max
giveUp(); // Ensure game is marked as failed and score reset to 0 giveUp(); // Ensure game is marked as failed and score reset to 0
setHasLost(true); setHasLost(true);
setHasWon(false); setHasWon(false);
// gameState.isFailed will be updated by useGameState
// Track puzzle lost event // Track puzzle lost event
if (typeof window !== 'undefined' && window.plausible) { if (typeof window !== 'undefined' && window.plausible) {
window.plausible('puzzle_solved', { window.plausible('puzzle_solved', {
@@ -409,19 +421,19 @@ export default function Game({ dailyPuzzle, genre = null, isSpecial = false, max
if (i < gameState.guesses.length) { if (i < gameState.guesses.length) {
if (gameState.guesses[i] === 'SKIPPED') { if (gameState.guesses[i] === 'SKIPPED') {
emojiGrid += '⬛'; emojiGrid += '⬛';
} else if (hasWon && i === gameState.guesses.length - 1) { } else if (isSolved && i === gameState.guesses.length - 1) {
emojiGrid += '🟩'; emojiGrid += '🟩';
} else { } else {
emojiGrid += '🟥'; emojiGrid += '🟥';
} }
} else { } else {
// If game is lost, fill remaining slots with black squares // If game is lost, fill remaining slots with black squares
emojiGrid += hasLost ? '⬛' : '⬜'; emojiGrid += isFailed ? '⬛' : '⬜';
} }
} }
const speaker = hasWon ? '🔉' : '🔇'; const speaker = isSolved ? '🔉' : '🔇';
const bonusStar = (hasWon && gameState.yearGuessed && dailyPuzzle.releaseYear && gameState.scoreBreakdown.some(item => item.reason === 'Bonus: Correct Year')) ? '⭐' : ''; const bonusStar = (isSolved && gameState.yearGuessed && dailyPuzzle.releaseYear && gameState.scoreBreakdown.some(item => item.reason === 'Bonus: Correct Year')) ? '⭐' : '';
const genreText = genre ? `${isSpecial ? t('special') : t('genre')}: ${genre}\n` : ''; const genreText = genre ? `${isSpecial ? t('special') : t('genre')}: ${genre}\n` : '';
// Use current domain from window.location to support both hoerdle.de and hördle.de // Use current domain from window.location to support both hoerdle.de and hördle.de
@@ -534,7 +546,7 @@ export default function Game({ dailyPuzzle, genre = null, isSpecial = false, max
src={dailyPuzzle.audioUrl} src={dailyPuzzle.audioUrl}
unlockedSeconds={unlockedSeconds} unlockedSeconds={unlockedSeconds}
startTime={dailyPuzzle.startTime} startTime={dailyPuzzle.startTime}
autoPlay={lastAction === 'SKIP' || (lastAction === 'GUESS' && !hasWon && !hasLost)} autoPlay={lastAction === 'SKIP' || (lastAction === 'GUESS' && !isSolved && !isFailed)}
onReplay={addReplay} onReplay={addReplay}
onHasPlayedChange={setHasPlayedAudio} onHasPlayedChange={setHasPlayedAudio}
/> />
@@ -543,7 +555,7 @@ export default function Game({ dailyPuzzle, genre = null, isSpecial = false, max
<div className="guess-list"> <div className="guess-list">
{gameState.guesses.map((guess, i) => { {gameState.guesses.map((guess, i) => {
const isCorrect = hasWon && i === gameState.guesses.length - 1; const isCorrect = isSolved && i === gameState.guesses.length - 1;
return ( return (
<div key={i} className="guess-item"> <div key={i} className="guess-item">
<span className="guess-number">#{i + 1}</span> <span className="guess-number">#{i + 1}</span>
@@ -555,7 +567,7 @@ export default function Game({ dailyPuzzle, genre = null, isSpecial = false, max
})} })}
</div> </div>
{!hasWon && !hasLost && ( {!isSolved && !isFailed && (
<> <>
<div id="tour-input"> <div id="tour-input">
<GuessInput onGuess={handleGuess} disabled={isProcessingGuess} /> <GuessInput onGuess={handleGuess} disabled={isProcessingGuess} />
@@ -586,13 +598,13 @@ export default function Game({ dailyPuzzle, genre = null, isSpecial = false, max
</> </>
)} )}
{(hasWon || hasLost) && ( {(isSolved || isFailed) && (
<div className={`message-box ${hasWon ? 'success' : 'failure'}`}> <div className={`message-box ${isSolved ? 'success' : 'failure'}`}>
<h2 style={{ fontSize: '1.5rem', fontWeight: 'bold', marginBottom: '0.5rem' }}> <h2 style={{ fontSize: '1.5rem', fontWeight: 'bold', marginBottom: '0.5rem' }}>
{hasWon ? t('won') : t('lost')} {isSolved ? t('won') : t('lost')}
</h2> </h2>
<div style={{ fontSize: '2rem', fontWeight: 'bold', margin: '1rem 0', color: hasWon ? 'var(--success)' : 'var(--danger)' }}> <div style={{ fontSize: '2rem', fontWeight: 'bold', margin: '1rem 0', color: isSolved ? 'var(--success)' : 'var(--danger)' }}>
{t('score')}: {gameState.score} {t('score')}: {gameState.score}
</div> </div>
@@ -610,7 +622,7 @@ export default function Game({ dailyPuzzle, genre = null, isSpecial = false, max
</ul> </ul>
</details> </details>
<p>{hasWon ? t('comeBackTomorrow') : t('theSongWas')}</p> <p>{isSolved ? t('comeBackTomorrow') : t('theSongWas')}</p>
<div style={{ margin: '1.5rem 0', padding: '1rem', background: 'rgba(255,255,255,0.5)', borderRadius: '0.5rem', display: 'flex', flexDirection: 'column', alignItems: 'center' }}> <div style={{ margin: '1.5rem 0', padding: '1rem', background: 'rgba(255,255,255,0.5)', borderRadius: '0.5rem', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<img <img

View File

@@ -1,6 +1,6 @@
{ {
"name": "hoerdle", "name": "hoerdle",
"version": "0.1.6.35", "version": "0.1.6.37",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",

View File

@@ -69,6 +69,38 @@ git fetch --prune --tags origin master
git fetch --tags origin git fetch --tags origin
git reset --hard origin/master git reset --hard origin/master
# Determine version: try git tag first, then package.json
echo "🏷️ Determining version..."
APP_VERSION=""
# Try to get exact tag if we're on a tagged commit
if git describe --tags --exact-match HEAD 2>/dev/null; then
APP_VERSION=$(git describe --tags --exact-match HEAD 2>/dev/null)
echo " Found exact tag: $APP_VERSION"
else
# Try to get latest tag
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
if [ -n "$LATEST_TAG" ]; then
APP_VERSION="$LATEST_TAG"
echo " Using latest tag: $APP_VERSION"
else
# Fallback to package.json
if [ -f "package.json" ]; then
PACKAGE_VERSION=$(grep -o '"version": "[^"]*"' package.json 2>/dev/null | cut -d'"' -f4)
if [ -n "$PACKAGE_VERSION" ]; then
APP_VERSION="v${PACKAGE_VERSION}"
echo " Using package.json version: $APP_VERSION"
fi
fi
fi
fi
if [ -z "$APP_VERSION" ]; then
echo "⚠️ Could not determine version, using 'dev'"
APP_VERSION="dev"
fi
echo "📦 Building with version: $APP_VERSION"
# Prüfe und erstelle/repariere Netzwerk falls nötig # Prüfe und erstelle/repariere Netzwerk falls nötig
echo "🌐 Prüfe Docker-Netzwerk..." echo "🌐 Prüfe Docker-Netzwerk..."
if ! docker network ls | grep -q "hoerdle_default"; then if ! docker network ls | grep -q "hoerdle_default"; then
@@ -84,7 +116,7 @@ echo ""
# Build new image in background (doesn't stop running container) # Build new image in background (doesn't stop running container)
echo "🔨 Building new Docker image (this runs while app is still online)..." echo "🔨 Building new Docker image (this runs while app is still online)..."
docker compose build docker compose build --build-arg APP_VERSION="$APP_VERSION"
# Quick restart with pre-built image # Quick restart with pre-built image
echo "🔄 Restarting with new image (minimal downtime)..." echo "🔄 Restarting with new image (minimal downtime)..."