diff --git a/src/bot/instance.ts b/src/bot/instance.ts index 728ded7..3f6bf50 100755 --- a/src/bot/instance.ts +++ b/src/bot/instance.ts @@ -498,13 +498,17 @@ export class BotInstance extends EventEmitter { } private async cmdPrev(): Promise { - const prev = this.queue.prev(); - if (prev) { + // Retry-skip up to 4 attempts: history can include failed songs + // that playNext's auto-advance retry-skipped past, so a single + // prev would otherwise land on an unplayable song and leave the + // queue's currentIndex stuck mid-failure. + for (let i = 0; i < 4; i++) { + const prev = this.queue.prev(); + if (!prev) return "No previous song"; const ok = await this.resolveAndPlay(prev); - if (!ok) return "Cannot play previous song"; - return `Now playing: ${prev.name} - ${prev.artist}`; + if (ok) return `Now playing: ${prev.name} - ${prev.artist}`; } - return "No previous song"; + return "Cannot play any previous songs (all failed to resolve)"; } private cmdVol(cmd: ParsedCommand): string { diff --git a/web/src/components/SongCard.vue b/web/src/components/SongCard.vue index 8dd2b1a..c2cded1 100644 --- a/web/src/components/SongCard.vue +++ b/web/src/components/SongCard.vue @@ -156,6 +156,15 @@ function formatDuration(seconds: number): string { transition: opacity var(--transition-fast); } +// Touch devices have no :hover, so the parent-hover-reveals-actions +// pattern leaves all action buttons invisible. Always show on coarse- +// pointer (touch) inputs — this is also where bigger tap targets matter. +@media (pointer: coarse) { + .song-actions { + opacity: 1; + } +} + .action-btn { font-size: 18px; padding: 4px;