diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 4c9735a..8f862f5 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4,6 +4,7 @@ import { useAuthStore } from './store/authStore' import { usePlayerStore } from './store/playerStore' import Layout from './components/common/Layout' import AudioPlayer from './components/player/AudioPlayer' +import AudioEngine from './components/player/AudioEngine' import Login from './pages/Login' import Setup from './pages/Setup' import Library from './pages/Library' @@ -21,6 +22,7 @@ function AppRoutes() { const { user, loadAuth } = useAuthStore() const { libraries } = useAuthStore() const { expanded, setExpanded } = usePlayerStore() + const item = usePlayerStore((s) => s.item) const [setupNeeded, setSetupNeeded] = useState(null) useEffect(() => { @@ -64,6 +66,8 @@ function AppRoutes() { /> + {item && } + {expanded && (
diff --git a/frontend/src/components/player/AudioEngine.tsx b/frontend/src/components/player/AudioEngine.tsx new file mode 100644 index 0000000..b857864 --- /dev/null +++ b/frontend/src/components/player/AudioEngine.tsx @@ -0,0 +1,229 @@ +import { useEffect, useRef } from 'react' +import { usePlayerStore } from '../../store/playerStore' + +interface Track { + index: number + startOffset: number + duration: number + contentUrl: string + mimeType: string +} + +function findTrackForTime(tracks: Track[], time: number): number { + for (let i = tracks.length - 1; i >= 0; i--) { + if (time >= (tracks[i].startOffset || 0)) return i + } + return 0 +} + +/** + * Globale Audio-Engine. Wird immer gemountet, sobald ein item geladen ist. + * Hält das einzige
@@ -402,7 +235,7 @@ export default function AudioPlayer() { )} diff --git a/frontend/src/components/player/MiniPlayer.tsx b/frontend/src/components/player/MiniPlayer.tsx index 830af60..1948df2 100644 --- a/frontend/src/components/player/MiniPlayer.tsx +++ b/frontend/src/components/player/MiniPlayer.tsx @@ -5,7 +5,7 @@ import CoverImage from '../common/CoverImage' import { coverUrl } from '../../api/items' export default function MiniPlayer() { - const { item, currentTime, duration, isPlaying, setPlaying, stop, setExpanded } = usePlayerStore() + const { item, currentTime, duration, isPlaying, setPlaying, stop, setExpanded, playerError } = usePlayerStore() if (!item) return null const meta = item.media?.metadata || {} @@ -15,11 +15,11 @@ export default function MiniPlayer() { return (
- {/* Progress track */} + {/* Progress track (oder Fehler-Strich rot) */}
diff --git a/frontend/src/store/playerStore.ts b/frontend/src/store/playerStore.ts index 68b6979..a0c1a1d 100644 --- a/frontend/src/store/playerStore.ts +++ b/frontend/src/store/playerStore.ts @@ -12,10 +12,12 @@ interface PlayerState { isPlaying: boolean playbackRate: number volume: number - sleepTimer: number | null // Sekunden verbleibend + sleepTimer: number | null sleepTimerActive: boolean chapters: Chapter[] expanded: boolean + seekRequest: { time: number; counter: number } | null + playerError: string | null play: (item: any) => Promise stop: () => Promise @@ -28,6 +30,7 @@ interface PlayerState { setSleepTimer: (seconds: number) => void cancelSleepTimer: () => void setExpanded: (v: boolean) => void + setPlayerError: (msg: string | null) => void syncProgress: () => Promise } @@ -46,6 +49,8 @@ export const usePlayerStore = create((set, get) => ({ sleepTimerActive: false, chapters: [], expanded: false, + seekRequest: null, + playerError: null, play: async (item: any) => { const { session: oldSession, stop } = get() @@ -60,9 +65,10 @@ export const usePlayerStore = create((set, get) => ({ chapters: session.chapters || [], isPlaying: true, expanded: false, + seekRequest: null, + playerError: null, }) - // Sync alle 15 Sekunden if (syncInterval) clearInterval(syncInterval) syncInterval = setInterval(() => get().syncProgress(), 15000) }, @@ -74,10 +80,13 @@ export const usePlayerStore = create((set, get) => ({ if (session) { try { await closeSession(session.id) } catch { } } - set({ item: null, session: null, isPlaying: false, currentTime: 0, sleepTimerActive: false }) + set({ item: null, session: null, isPlaying: false, currentTime: 0, sleepTimerActive: false, seekRequest: null, playerError: null }) }, - seek: (time) => set({ currentTime: time }), + seek: (time) => set((s) => ({ + currentTime: time, + seekRequest: { time, counter: (s.seekRequest?.counter || 0) + 1 }, + })), setPlaying: (v) => set({ isPlaying: v }), @@ -110,6 +119,8 @@ export const usePlayerStore = create((set, get) => ({ setExpanded: (v) => set({ expanded: v }), + setPlayerError: (msg) => set({ playerError: msg }), + syncProgress: async () => { const { session, currentTime, duration, item } = get() if (!session || !item) return