mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-10-02 16:45:15 -07:00
Implement story management features and update dependencies
- Introduced story management functionality, including creating, listing, and managing story items. - Added new components for story display and interaction, including StoriesTab, StoryList, and StoryContent. - Integrated drag-and-drop functionality for reordering story items using @dnd-kit. - Updated dependencies for @dnd-kit packages to enhance drag-and-drop capabilities. - Bumped version for @voicebox/app, @voicebox/landing, @voicebox/tauri, and @voicebox/web to 0.1.5. - Enhanced audio playback features to support story mode with auto-play functionality. - Improved error handling and user feedback through toast notifications in story-related actions.
This commit is contained in:
@@ -11,8 +11,11 @@ interface PlayerState {
|
||||
volume: number;
|
||||
isLooping: boolean;
|
||||
shouldRestart: boolean;
|
||||
shouldAutoPlay: boolean;
|
||||
onFinish: (() => void) | null;
|
||||
|
||||
setAudio: (url: string, id: string, profileId: string | null, title?: string) => void;
|
||||
setAudioWithAutoPlay: (url: string, id: string, profileId: string | null, title?: string) => void;
|
||||
setIsPlaying: (playing: boolean) => void;
|
||||
setCurrentTime: (time: number) => void;
|
||||
setDuration: (duration: number) => void;
|
||||
@@ -20,6 +23,8 @@ interface PlayerState {
|
||||
toggleLoop: () => void;
|
||||
restartCurrentAudio: () => void;
|
||||
clearRestartFlag: () => void;
|
||||
clearAutoPlayFlag: () => void;
|
||||
setOnFinish: (callback: (() => void) | null) => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
@@ -34,6 +39,8 @@ export const usePlayerStore = create<PlayerState>((set) => ({
|
||||
volume: 1,
|
||||
isLooping: false,
|
||||
shouldRestart: false,
|
||||
shouldAutoPlay: false,
|
||||
onFinish: null,
|
||||
|
||||
setAudio: (url, id, profileId, title) =>
|
||||
set({
|
||||
@@ -44,6 +51,18 @@ export const usePlayerStore = create<PlayerState>((set) => ({
|
||||
currentTime: 0,
|
||||
isPlaying: false,
|
||||
shouldRestart: false,
|
||||
shouldAutoPlay: false,
|
||||
}),
|
||||
setAudioWithAutoPlay: (url, id, profileId, title) =>
|
||||
set({
|
||||
audioUrl: url,
|
||||
audioId: id,
|
||||
profileId: profileId || null,
|
||||
title: title || null,
|
||||
currentTime: 0,
|
||||
isPlaying: false,
|
||||
shouldRestart: false,
|
||||
shouldAutoPlay: true,
|
||||
}),
|
||||
setIsPlaying: (playing) => set({ isPlaying: playing }),
|
||||
setCurrentTime: (time) => set({ currentTime: time }),
|
||||
@@ -52,6 +71,8 @@ export const usePlayerStore = create<PlayerState>((set) => ({
|
||||
toggleLoop: () => set((state) => ({ isLooping: !state.isLooping })),
|
||||
restartCurrentAudio: () => set({ shouldRestart: true }),
|
||||
clearRestartFlag: () => set({ shouldRestart: false }),
|
||||
clearAutoPlayFlag: () => set({ shouldAutoPlay: false }),
|
||||
setOnFinish: (callback) => set({ onFinish: callback }),
|
||||
reset: () =>
|
||||
set({
|
||||
audioUrl: null,
|
||||
@@ -63,5 +84,7 @@ export const usePlayerStore = create<PlayerState>((set) => ({
|
||||
duration: 0,
|
||||
isLooping: false,
|
||||
shouldRestart: false,
|
||||
shouldAutoPlay: false,
|
||||
onFinish: null,
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import { create } from 'zustand';
|
||||
import type { StoryItemDetail } from '@/lib/api/types';
|
||||
|
||||
interface StoryPlaybackState {
|
||||
// Selection
|
||||
selectedStoryId: string | null;
|
||||
setSelectedStoryId: (id: string | null) => void;
|
||||
|
||||
// Playback state
|
||||
isPlaying: boolean;
|
||||
currentTimeMs: number;
|
||||
totalDurationMs: number;
|
||||
playbackStoryId: string | null;
|
||||
playbackItems: StoryItemDetail[] | null;
|
||||
|
||||
// Actions
|
||||
play: (storyId: string, items: StoryItemDetail[]) => void;
|
||||
pause: () => void;
|
||||
stop: () => void;
|
||||
seek: (timeMs: number) => void;
|
||||
tick: (deltaMs: number) => void; // Called by animation frame
|
||||
}
|
||||
|
||||
export const useStoryStore = create<StoryPlaybackState>((set, get) => ({
|
||||
// Selection
|
||||
selectedStoryId: null,
|
||||
setSelectedStoryId: (id) => set({ selectedStoryId: id }),
|
||||
|
||||
// Playback state
|
||||
isPlaying: false,
|
||||
currentTimeMs: 0,
|
||||
totalDurationMs: 0,
|
||||
playbackStoryId: null,
|
||||
playbackItems: null,
|
||||
|
||||
// Actions
|
||||
play: (storyId, items) => {
|
||||
// Calculate total duration from items
|
||||
const maxEndTimeMs = Math.max(
|
||||
...items.map((item) => item.start_time_ms + item.duration * 1000),
|
||||
0
|
||||
);
|
||||
|
||||
// Find the minimum start time (first item)
|
||||
const minStartTimeMs = Math.min(
|
||||
...items.map((item) => item.start_time_ms),
|
||||
0
|
||||
);
|
||||
|
||||
// If resuming the same story, keep position; otherwise start at first item
|
||||
const currentState = get();
|
||||
const shouldResume = currentState.playbackStoryId === storyId && currentState.currentTimeMs > 0;
|
||||
const startTimeMs = shouldResume ? currentState.currentTimeMs : minStartTimeMs;
|
||||
|
||||
console.log('[StoryStore] Play called:', {
|
||||
storyId,
|
||||
itemCount: items.length,
|
||||
items: items.map(i => ({ id: i.generation_id, start: i.start_time_ms, duration: i.duration })),
|
||||
maxEndTimeMs,
|
||||
minStartTimeMs,
|
||||
startTimeMs,
|
||||
shouldResume,
|
||||
});
|
||||
|
||||
set({
|
||||
isPlaying: true,
|
||||
playbackStoryId: storyId,
|
||||
playbackItems: items,
|
||||
totalDurationMs: maxEndTimeMs,
|
||||
currentTimeMs: startTimeMs,
|
||||
});
|
||||
},
|
||||
|
||||
pause: () => {
|
||||
set({ isPlaying: false });
|
||||
},
|
||||
|
||||
stop: () => {
|
||||
set({
|
||||
isPlaying: false,
|
||||
currentTimeMs: 0,
|
||||
playbackStoryId: null,
|
||||
playbackItems: null,
|
||||
totalDurationMs: 0,
|
||||
});
|
||||
},
|
||||
|
||||
seek: (timeMs) => {
|
||||
const state = get();
|
||||
const clampedTime = Math.max(0, Math.min(timeMs, state.totalDurationMs));
|
||||
set({ currentTimeMs: clampedTime });
|
||||
},
|
||||
|
||||
tick: (deltaMs) => {
|
||||
const state = get();
|
||||
if (!state.isPlaying || !state.playbackItems) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newTime = state.currentTimeMs + deltaMs;
|
||||
const clampedTime = Math.min(newTime, state.totalDurationMs);
|
||||
|
||||
// Auto-stop when reaching the end
|
||||
if (clampedTime >= state.totalDurationMs) {
|
||||
set({
|
||||
currentTimeMs: state.totalDurationMs,
|
||||
isPlaying: false,
|
||||
});
|
||||
} else {
|
||||
set({ currentTimeMs: clampedTime });
|
||||
}
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user