diff --git a/src/main/input/gamepadReader.ts b/src/main/input/gamepadReader.ts index 1a19658..713f6fb 100644 --- a/src/main/input/gamepadReader.ts +++ b/src/main/input/gamepadReader.ts @@ -123,6 +123,12 @@ class JoystickReader { } else if (axis === 1) { if (value < -STICK_THRESHOLD) key = 'ArrowUp' else if (value > STICK_THRESHOLD) key = 'ArrowDown' + } + // Trigger (Axes 2/5): -32767 = nicht gedrückt, +32767 = voll gedrückt + else if (axis === 2) { + if (value > 0) key = 'l2' + } else if (axis === 5) { + if (value > 0) key = 'r2' } else { return } diff --git a/src/main/ipc/handlers.ts b/src/main/ipc/handlers.ts index f1c2106..350a062 100644 --- a/src/main/ipc/handlers.ts +++ b/src/main/ipc/handlers.ts @@ -23,6 +23,7 @@ export const IPC = { playbackStartLive: 'playback:start-live', playbackStartVod: 'playback:start-vod', playbackStop: 'playback:stop', + playbackSeek: 'playback:seek', /** main → renderer */ playbackEvent: 'playback:event' } as const @@ -74,6 +75,7 @@ export function registerIpcHandlers( playback.startVod(vodId, channelLogin, title, durationSeconds) ) ipcMain.handle(IPC.playbackStop, () => playback.stop()) + ipcMain.handle(IPC.playbackSeek, (_e, seconds: number) => playback.seek(seconds)) playback.on('playback-event', (event: PlaybackEvent) => { for (const win of BrowserWindow.getAllWindows()) { diff --git a/src/main/playback/mpvController.ts b/src/main/playback/mpvController.ts index 0b359de..84af438 100644 --- a/src/main/playback/mpvController.ts +++ b/src/main/playback/mpvController.ts @@ -74,6 +74,12 @@ export class MpvController { }) } + seek(seconds: number): void { + if (this.socket?.writable) { + this._send({ command: ['seek', seconds, 'relative'] }) + } + } + quit(): void { if (this.socket?.writable) { try { diff --git a/src/main/playback/playbackService.ts b/src/main/playback/playbackService.ts index b83a9d3..1a217b5 100644 --- a/src/main/playback/playbackService.ts +++ b/src/main/playback/playbackService.ts @@ -141,6 +141,10 @@ export class PlaybackService extends EventEmitter { }) } + seek(seconds: number): void { + this.current?.mpv.seek(seconds) + } + async stop(): Promise { if (!this.current) return const { process: proc, mpv } = this.current diff --git a/src/preload/index.ts b/src/preload/index.ts index c51fea5..43542b8 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -17,6 +17,7 @@ const IPC = { playbackStartLive: 'playback:start-live', playbackStartVod: 'playback:start-vod', playbackStop: 'playback:stop', + playbackSeek: 'playback:seek', playbackEvent: 'playback:event', gamepadInput: 'gamepad-input' @@ -114,6 +115,7 @@ const api = { startVod: (vodId: string, channelLogin: string, title: string, durationSeconds: number): Promise => ipcRenderer.invoke(IPC.playbackStartVod, vodId, channelLogin, title, durationSeconds), stop: (): Promise => ipcRenderer.invoke(IPC.playbackStop), + seek: (seconds: number): Promise => ipcRenderer.invoke(IPC.playbackSeek, seconds), onEvent: (cb: (event: PlaybackEvent) => void): (() => void) => { const listener = (_e: IpcRendererEvent, event: PlaybackEvent): void => cb(event) ipcRenderer.on(IPC.playbackEvent, listener) diff --git a/src/renderer/src/input/gamepad.ts b/src/renderer/src/input/gamepad.ts index 76f1eb5..0aebd12 100644 --- a/src/renderer/src/input/gamepad.ts +++ b/src/renderer/src/input/gamepad.ts @@ -23,6 +23,8 @@ const BUTTON_MAP: Record = { 1: 'Escape', // B 2: 'x', // X 3: 'y', // Y + 6: 'l2', // LT / L2 + 7: 'r2', // RT / R2 12: 'ArrowUp', 13: 'ArrowDown', 14: 'ArrowLeft', diff --git a/src/renderer/src/screens/BrowseScreen.tsx b/src/renderer/src/screens/BrowseScreen.tsx index e934e6f..b848b65 100644 --- a/src/renderer/src/screens/BrowseScreen.tsx +++ b/src/renderer/src/screens/BrowseScreen.tsx @@ -107,6 +107,15 @@ export default function BrowseScreen({ if (!hasFocus) return const onKey = (e: KeyboardEvent): void => { + // Während der Wiedergabe: nur Stop erlauben, Navigation blockieren + if (isPlaying) { + e.preventDefault() + if (e.key === 'Escape' || e.key === 'Enter' || e.key === ' ') { + void window.t4sd.playback.stop() + } + return + } + if (e.key === 'y' || e.key === 'Y') { void load() return @@ -145,12 +154,10 @@ export default function BrowseScreen({ if (ch) onSelectChannel(ch) break } - case 'Escape': { + case 'Escape': e.preventDefault() - if (isPlaying) void window.t4sd.playback.stop() - else onRequestSidebar() + onRequestSidebar() break - } } } else { // grid region @@ -184,12 +191,10 @@ export default function BrowseScreen({ if (game) onSelectCategory(game) break } - case 'Escape': { + case 'Escape': e.preventDefault() - if (isPlaying) void window.t4sd.playback.stop() - else onRequestSidebar() + onRequestSidebar() break - } } } } diff --git a/src/renderer/src/screens/CategoryScreen.tsx b/src/renderer/src/screens/CategoryScreen.tsx index 30572af..4da988a 100644 --- a/src/renderer/src/screens/CategoryScreen.tsx +++ b/src/renderer/src/screens/CategoryScreen.tsx @@ -14,6 +14,7 @@ export default function CategoryScreen({ game, onSelectChannel, onBack }: Props) const [streams, setStreams] = useState([]) const [loadState, setLoadState] = useState('loading') const [focusedIndex, setFocusedIndex] = useState(0) + const [isPlaying, setIsPlaying] = useState(false) const gridRef = useRef(null) const load = useCallback(async () => { @@ -33,6 +34,13 @@ export default function CategoryScreen({ game, onSelectChannel, onBack }: Props) void load() }, [load]) + useEffect(() => { + return window.t4sd.playback.onEvent((ev) => { + if (ev.kind === 'started') setIsPlaying(true) + else if (ev.kind === 'stopped' || ev.kind === 'error') setIsPlaying(false) + }) + }, []) + const getColumns = useCallback((): number => { const grid = gridRef.current if (!grid) return 4 @@ -42,6 +50,15 @@ export default function CategoryScreen({ game, onSelectChannel, onBack }: Props) useEffect(() => { const onKey = (e: KeyboardEvent): void => { + // Während der Wiedergabe: nur Stop erlauben, Navigation blockieren + if (isPlaying) { + e.preventDefault() + if (e.key === 'Escape' || e.key === 'Enter' || e.key === ' ') { + void window.t4sd.playback.stop() + } + return + } + if (e.key === 'y' || e.key === 'Y') { void load() return @@ -94,7 +111,7 @@ export default function CategoryScreen({ game, onSelectChannel, onBack }: Props) window.addEventListener('keydown', onKey) return () => window.removeEventListener('keydown', onKey) - }, [loadState, streams, focusedIndex, getColumns, onBack, onSelectChannel, load]) + }, [loadState, streams, focusedIndex, isPlaying, getColumns, onBack, onSelectChannel, load]) return (
diff --git a/src/renderer/src/screens/ChannelScreen.tsx b/src/renderer/src/screens/ChannelScreen.tsx index e13fcbe..c8d0718 100644 --- a/src/renderer/src/screens/ChannelScreen.tsx +++ b/src/renderer/src/screens/ChannelScreen.tsx @@ -73,6 +73,39 @@ export default function ChannelScreen({ channel, onBack }: Props): JSX.Element { useEffect(() => { const onKey = (e: KeyboardEvent): void => { + const isPlaying = playState === 'playing' || playState === 'starting' + + // Während der Wiedergabe: nur Playback-Steuerung erlauben, keine Navigation + if (isPlaying) { + switch (e.key) { + case 'l2': + e.preventDefault() + void window.t4sd.playback.seek(-300) + break + case 'r2': + e.preventDefault() + void window.t4sd.playback.seek(300) + break + case 'ArrowLeft': + e.preventDefault() + void window.t4sd.playback.seek(-30) + break + case 'ArrowRight': + e.preventDefault() + void window.t4sd.playback.seek(30) + break + case 'Enter': + case ' ': + case 'Escape': + e.preventDefault() + void window.t4sd.playback.stop() + break + default: + e.preventDefault() // alle anderen Tasten blockieren + } + return + } + if (focusRegion === 'hero') { if (e.key === 'ArrowDown' && vods.length > 0) { e.preventDefault() @@ -80,18 +113,12 @@ export default function ChannelScreen({ channel, onBack }: Props): JSX.Element { setShelfIndex(0) } else if (e.key === 'Enter' || e.key === ' ') { e.preventDefault() - if (playState === 'playing' || playState === 'starting') { - void window.t4sd.playback.stop() - } else if (channel.isLive) { + if (channel.isLive) { void handleWatch() } } else if (e.key === 'Escape') { e.preventDefault() - if (playState === 'playing' || playState === 'starting') { - void window.t4sd.playback.stop() - } else { - onBack() - } + onBack() } } else { // shelf region @@ -111,11 +138,7 @@ export default function ChannelScreen({ channel, onBack }: Props): JSX.Element { if (vod) void handleWatchVod(vod) } else if (e.key === 'Escape') { e.preventDefault() - if (playState === 'playing' || playState === 'starting') { - void window.t4sd.playback.stop() - } else { - onBack() - } + onBack() } } } @@ -252,7 +275,7 @@ export default function ChannelScreen({ channel, onBack }: Props): JSX.Element { {playState === 'playing' && ( <> - ● Wiedergabe läuft in mpv + ● Wiedergabe läuft in mpv · ← −30s · → +30s · LT −5min · RT +5min diff --git a/src/renderer/src/types/t4sd.d.ts b/src/renderer/src/types/t4sd.d.ts index e98414c..73ba94c 100644 --- a/src/renderer/src/types/t4sd.d.ts +++ b/src/renderer/src/types/t4sd.d.ts @@ -81,6 +81,7 @@ export interface T4sdApi { startLive: (channelLogin: string, quality?: string) => Promise startVod: (vodId: string, channelLogin: string, title: string, durationSeconds: number) => Promise stop: () => Promise + seek: (seconds: number) => Promise onEvent: (cb: (event: PlaybackEvent) => void) => () => void } gamepad: {