Phase 6 Fix 4

- Unter "Du folgst" wurde immer der erste Kanal aufgerufen, unabhängig vom ausgewählten Kanal
- Abbrechen der Wiedergabe auch unter "Durchsuchen" und Top-Stream möglich.
This commit is contained in:
Andre
2026-04-11 09:59:31 +02:00
parent f592de7aeb
commit ffad3b045d
10 changed files with 91 additions and 23 deletions
+6
View File
@@ -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
}
+2
View File
@@ -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()) {
+6
View File
@@ -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 {
+4
View File
@@ -141,6 +141,10 @@ export class PlaybackService extends EventEmitter {
})
}
seek(seconds: number): void {
this.current?.mpv.seek(seconds)
}
async stop(): Promise<void> {
if (!this.current) return
const { process: proc, mpv } = this.current
+2
View File
@@ -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<void> =>
ipcRenderer.invoke(IPC.playbackStartVod, vodId, channelLogin, title, durationSeconds),
stop: (): Promise<void> => ipcRenderer.invoke(IPC.playbackStop),
seek: (seconds: number): Promise<void> => 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)
+2
View File
@@ -23,6 +23,8 @@ const BUTTON_MAP: Record<number, string> = {
1: 'Escape', // B
2: 'x', // X
3: 'y', // Y
6: 'l2', // LT / L2
7: 'r2', // RT / R2
12: 'ArrowUp',
13: 'ArrowDown',
14: 'ArrowLeft',
+13 -8
View File
@@ -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,13 +154,11 @@ 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
const cols = getGridColumns()
@@ -184,15 +191,13 @@ 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
}
}
}
}
window.addEventListener('keydown', onKey)
return () => window.removeEventListener('keydown', onKey)
+18 -1
View File
@@ -14,6 +14,7 @@ export default function CategoryScreen({ game, onSelectChannel, onBack }: Props)
const [streams, setStreams] = useState<FollowedChannelInfo[]>([])
const [loadState, setLoadState] = useState<LoadState>('loading')
const [focusedIndex, setFocusedIndex] = useState(0)
const [isPlaying, setIsPlaying] = useState(false)
const gridRef = useRef<HTMLDivElement>(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 (
<div className="category-screen">
+35 -12
View File
@@ -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,19 +113,13 @@ 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()
}
}
} else {
// shelf region
if (e.key === 'ArrowLeft') {
@@ -111,14 +138,10 @@ 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()
}
}
}
}
window.addEventListener('keydown', onKey)
return () => window.removeEventListener('keydown', onKey)
}, [focusRegion, playState, vods, shelfIndex, onBack])
@@ -252,7 +275,7 @@ export default function ChannelScreen({ channel, onBack }: Props): JSX.Element {
{playState === 'playing' && (
<>
<span className="channel-screen__playing-hint">● Wiedergabe läuft in mpv</span>
<span className="channel-screen__playing-hint">● Wiedergabe läuft in mpv · ← −30s · → +30s · LT −5min · RT +5min</span>
<button className="btn" onClick={() => void handleStop()}>
■ Stop (B / Escape)
</button>
+1
View File
@@ -81,6 +81,7 @@ export interface T4sdApi {
startLive: (channelLogin: string, quality?: string) => Promise<void>
startVod: (vodId: string, channelLogin: string, title: string, durationSeconds: number) => Promise<void>
stop: () => Promise<void>
seek: (seconds: number) => Promise<void>
onEvent: (cb: (event: PlaybackEvent) => void) => () => void
}
gamepad: {