mirror of
https://github.com/Andre-Bzk/twitch4steamdeck.git
synced 2026-11-15 09:33:09 +01:00
feat: playback overlay with seek bar, auto-hide and double-tap skip (v0.3.4)
New PlaybackOverlay component replaces the old hint bar in ChannelScreen. The overlay sits as a fullscreen layer above the app during playback: - Auto-hides after 5 s while playing; stays visible when paused; reactivated by any gamepad key press or mouse movement - Center play/pause button, channel avatar, title, LIVE badge - VOD: seek bar with pointer-capture scrubbing, current time / total duration - Live: pulsing LIVE indicator instead of seek bar - Double-tap zones left/right (±10 s) with animated skip feedback - Gamepad hints in the top bar - New icons: PlayIcon, PauseIcon, StopIcon in Icons.tsx - New IPC channel `playback:time-update` (~1 Hz) pushes absolute playback position from main process to renderer; adds PlaybackTimeUpdate type and onTimeUpdate bridge in preload + t4sd.d.ts - PlaybackEvent extended with durationSeconds and isLive fields
This commit is contained in:
@@ -2,7 +2,7 @@ import { app, BrowserWindow, ipcMain } from 'electron'
|
||||
import type { AuthEvent, AuthService } from '../auth/authService'
|
||||
import type { HelixClient } from '../twitch/helixClient'
|
||||
import type { PlaybackService } from '../playback/playbackService'
|
||||
import type { PlaybackEvent } from '../playback/types'
|
||||
import type { PlaybackEvent, PlaybackTimeUpdate } from '../playback/types'
|
||||
import { getProgressMap } from '../store/historyRepo'
|
||||
|
||||
export const IPC = {
|
||||
@@ -35,7 +35,9 @@ export const IPC = {
|
||||
playbackSetLoggingEnabled: 'playback:set-logging-enabled',
|
||||
playbackGetLogPath: 'playback:get-log-path',
|
||||
/** main → renderer */
|
||||
playbackEvent: 'playback:event'
|
||||
playbackEvent: 'playback:event',
|
||||
/** main → renderer: ~1 Hz position updates during VOD playback */
|
||||
playbackTimeUpdate: 'playback:time-update'
|
||||
} as const
|
||||
|
||||
export function registerIpcHandlers(
|
||||
@@ -122,4 +124,10 @@ export function registerIpcHandlers(
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
playback.on('playback-time-update', (data: PlaybackTimeUpdate) => {
|
||||
for (const win of BrowserWindow.getAllWindows()) {
|
||||
win.webContents.send(IPC.playbackTimeUpdate, data)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import path from 'node:path'
|
||||
import { spawnStreamlink, getStreamUrl, spawnMpv } from './streamlink'
|
||||
import { getMpvIpcPath, MpvController } from './mpvController'
|
||||
import { createTrimmedPlaylist, cleanupTrimmedPlaylist, type TrimResult } from './hlsTrimmer'
|
||||
import type { PlaybackEvent } from './types'
|
||||
import type { PlaybackEvent, PlaybackTimeUpdate } from './types'
|
||||
import * as history from '../store/historyRepo'
|
||||
|
||||
const POSITION_WRITE_INTERVAL_MS = 5_000
|
||||
@@ -137,7 +137,7 @@ export class PlaybackService extends EventEmitter {
|
||||
loadGeneration: 0,
|
||||
pendingLoadGeneration: null
|
||||
}
|
||||
this.emit('playback-event', { kind: 'started', channelLogin } satisfies PlaybackEvent)
|
||||
this.emit('playback-event', { kind: 'started', channelLogin, isLive: true } satisfies PlaybackEvent)
|
||||
}
|
||||
|
||||
async startVod(
|
||||
@@ -241,7 +241,7 @@ export class PlaybackService extends EventEmitter {
|
||||
loadGeneration: 0,
|
||||
pendingLoadGeneration: null
|
||||
}
|
||||
this.emit('playback-event', { kind: 'started' } satisfies PlaybackEvent)
|
||||
this.emit('playback-event', { kind: 'started', durationSeconds } satisfies PlaybackEvent)
|
||||
|
||||
// IPC verbinden, initialen Resume-Seek erst nach `playback-restart` setzen und Position tracken.
|
||||
mpv.connect().then(() => {
|
||||
@@ -268,15 +268,24 @@ export class PlaybackService extends EventEmitter {
|
||||
|
||||
// Position-Tracking
|
||||
let lastWrite = 0
|
||||
let lastTimeUpdateEmit = 0
|
||||
const writePosition = (seconds: number): void => {
|
||||
const now = Date.now()
|
||||
if (now - lastWrite < POSITION_WRITE_INTERVAL_MS) return
|
||||
if (!this.current || this.current.mpv !== mpv) return
|
||||
if (this.isReloading(this.current)) return
|
||||
|
||||
lastWrite = now
|
||||
const absoluteSeconds = seconds + this.current.playbackOffsetSeconds
|
||||
this.current.lastKnownAbsolutePositionSeconds = absoluteSeconds
|
||||
|
||||
// ~1 Hz push to renderer for the player overlay seek bar
|
||||
if (now - lastTimeUpdateEmit >= 1_000) {
|
||||
lastTimeUpdateEmit = now
|
||||
this.emit('playback-time-update', { positionSeconds: absoluteSeconds } satisfies PlaybackTimeUpdate)
|
||||
}
|
||||
|
||||
// 5s throttle for SQLite history writes
|
||||
if (now - lastWrite < POSITION_WRITE_INTERVAL_MS) return
|
||||
lastWrite = now
|
||||
const pos = Math.floor(absoluteSeconds)
|
||||
history.updatePosition(vodId, pos)
|
||||
if (durationSeconds > 0 && pos / durationSeconds > 0.95) {
|
||||
|
||||
@@ -12,4 +12,10 @@ export interface PlaybackEvent {
|
||||
kind: 'started' | 'stopped' | 'error'
|
||||
channelLogin?: string
|
||||
message?: string
|
||||
durationSeconds?: number
|
||||
isLive?: boolean
|
||||
}
|
||||
|
||||
export interface PlaybackTimeUpdate {
|
||||
positionSeconds: number
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user