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:
Jamie Pine
2026-01-28 19:10:58 -08:00
parent 9d5d6cb56a
commit 3204e193fa
22 changed files with 2396 additions and 29 deletions
+164
View File
@@ -0,0 +1,164 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { apiClient } from '@/lib/api/client';
import type { StoryCreate, StoryItemCreate, StoryItemBatchUpdate, StoryItemReorder } from '@/lib/api/types';
import { isTauri } from '@/lib/tauri';
export function useStories() {
return useQuery({
queryKey: ['stories'],
queryFn: () => apiClient.listStories(),
});
}
export function useStory(storyId: string | null) {
return useQuery({
queryKey: ['stories', storyId],
queryFn: () => apiClient.getStory(storyId!),
enabled: !!storyId,
});
}
export function useCreateStory() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: StoryCreate) => apiClient.createStory(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['stories'] });
},
});
}
export function useUpdateStory() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ storyId, data }: { storyId: string; data: StoryCreate }) =>
apiClient.updateStory(storyId, data),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: ['stories'] });
queryClient.invalidateQueries({ queryKey: ['stories', variables.storyId] });
},
});
}
export function useDeleteStory() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (storyId: string) => apiClient.deleteStory(storyId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['stories'] });
},
});
}
export function useAddStoryItem() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ storyId, data }: { storyId: string; data: StoryItemCreate }) =>
apiClient.addStoryItem(storyId, data),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: ['stories'] });
queryClient.invalidateQueries({ queryKey: ['stories', variables.storyId] });
},
});
}
export function useRemoveStoryItem() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ storyId, generationId }: { storyId: string; generationId: string }) =>
apiClient.removeStoryItem(storyId, generationId),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: ['stories'] });
queryClient.invalidateQueries({ queryKey: ['stories', variables.storyId] });
},
});
}
export function useUpdateStoryItemTimes() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ storyId, data }: { storyId: string; data: StoryItemBatchUpdate }) =>
apiClient.updateStoryItemTimes(storyId, data),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: ['stories'] });
queryClient.invalidateQueries({ queryKey: ['stories', variables.storyId] });
},
});
}
export function useReorderStoryItems() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ storyId, data }: { storyId: string; data: StoryItemReorder }) =>
apiClient.reorderStoryItems(storyId, data),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: ['stories'] });
queryClient.invalidateQueries({ queryKey: ['stories', variables.storyId] });
},
});
}
export function useExportStoryAudio() {
return useMutation({
mutationFn: async ({ storyId, storyName }: { storyId: string; storyName: string }) => {
const blob = await apiClient.exportStoryAudio(storyId);
// Create safe filename
const safeName = storyName.substring(0, 50).replace(/[^a-z0-9]/gi, '-').toLowerCase();
const filename = `${safeName || 'story'}.wav`;
if (isTauri()) {
// Use Tauri's native save dialog
try {
const { save } = await import('@tauri-apps/plugin-dialog');
const filePath = await save({
defaultPath: filename,
filters: [
{
name: 'Audio File',
extensions: ['wav'],
},
],
});
if (filePath) {
// Write file using Tauri's filesystem API
const { writeBinaryFile } = await import('@tauri-apps/plugin-fs');
const arrayBuffer = await blob.arrayBuffer();
await writeBinaryFile(filePath, new Uint8Array(arrayBuffer));
}
} catch (error) {
console.error('Failed to use Tauri dialog, falling back to browser download:', error);
// Fall back to browser download if Tauri dialog fails
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
}
} else {
// Browser: trigger download
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
}
return blob;
},
});
}