diff --git a/src/app/page.tsx b/src/app/page.tsx index 0c532ab..7e17c07 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -208,16 +208,26 @@ export default function Home() { onToggleFavorite={() => handleToggleFavorite(selectedArticle.id, !selectedArticle.isFavorite) } + onToggleArchive={() => { + handleToggleArchive(selectedArticle.id, !selectedArticle.isArchived); + }} + onDelete={() => { + handleDelete(selectedArticle.id); + }} onOpenSettings={() => setIsSettingsOpen(true)} > void; onToggleFavorite: () => void; + onToggleArchive: () => void; + onDelete: () => void; onOpenSettings: () => void; children?: React.ReactNode; // TTS controls slot } @@ -17,6 +19,8 @@ export function Reader({ settings, onBack, onToggleFavorite, + onToggleArchive, + onDelete, onOpenSettings, children, }: ReaderProps) { @@ -39,8 +43,9 @@ export function Reader({ Back -
+
{children} +
+ + + + + +
+ ); + } + + // Floating player bar when playing return ( -
- {isLoading ? ( - - ) : isPlaying && !isPaused ? ( - <> - - - - ) : isPaused ? ( - <> - - - - ) : ( - - )} -
+ <> + {/* Header button to show it's active */} + + + {/* Floating player bar */} +
+
+ {/* Progress bar */} +
+ + {formatTime(currentTime)} + +
+
+ onSeek(parseFloat(e.target.value))} + className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" + disabled={!duration} + /> +
+ + {formatTime(duration)} + +
+ + {/* Controls */} +
+ + + {isLoading ? ( + + ) : isPaused ? ( + + ) : ( + + )} + + + + +
+
+
+ ); } diff --git a/src/hooks/useTTS.ts b/src/hooks/useTTS.ts index 38b6f74..0fa6e22 100644 --- a/src/hooks/useTTS.ts +++ b/src/hooks/useTTS.ts @@ -13,10 +13,14 @@ interface UseTTSReturn { isPaused: boolean; isLoading: boolean; currentPosition: number; + currentTime: number; + duration: number; + progress: number; // 0-100 play: () => void; pause: () => void; resume: () => void; stop: () => void; + seek: (time: number) => void; voices: SpeechSynthesisVoice[]; } @@ -25,6 +29,8 @@ export function useTTS({ settings, text }: UseTTSOptions): UseTTSReturn { const [isPaused, setIsPaused] = useState(false); const [isLoading, setIsLoading] = useState(false); const [currentPosition, setCurrentPosition] = useState(0); + const [currentTime, setCurrentTime] = useState(0); + const [duration, setDuration] = useState(0); const [voices, setVoices] = useState([]); const utteranceRef = useRef(null); @@ -140,6 +146,14 @@ export function useTTS({ settings, text }: UseTTSOptions): UseTTSReturn { const audio = new Audio(url); audio.playbackRate = 1; // Speed is already applied by Kokoro + audio.onloadedmetadata = () => { + setDuration(audio.duration); + }; + + audio.ontimeupdate = () => { + setCurrentTime(audio.currentTime); + }; + audio.onplay = () => { setIsPlaying(true); setIsPaused(false); @@ -150,6 +164,7 @@ export function useTTS({ settings, text }: UseTTSOptions): UseTTSReturn { setIsPlaying(false); setIsPaused(false); setCurrentPosition(0); + setCurrentTime(0); URL.revokeObjectURL(url); }; @@ -208,17 +223,32 @@ export function useTTS({ settings, text }: UseTTSOptions): UseTTSReturn { setIsPlaying(false); setIsPaused(false); setCurrentPosition(0); + setCurrentTime(0); + setDuration(0); }, [settings.engine]); + const seek = useCallback((time: number) => { + if (audioRef.current) { + audioRef.current.currentTime = time; + setCurrentTime(time); + } + }, []); + + const progress = duration > 0 ? (currentTime / duration) * 100 : 0; + return { isPlaying, isPaused, isLoading, currentPosition, + currentTime, + duration, + progress, play, pause, resume, stop, + seek, voices, }; } diff --git a/src/lib/db/index.ts b/src/lib/db/index.ts index 9e60b82..8a7df3a 100644 --- a/src/lib/db/index.ts +++ b/src/lib/db/index.ts @@ -7,6 +7,7 @@ const dbPath = process.env.DATABASE_PATH || path.join(process.cwd(), "data", "re const sqlite = new Database(dbPath); sqlite.pragma("journal_mode = WAL"); +sqlite.pragma("busy_timeout = 5000"); export const db = drizzle(sqlite, { schema }); export { schema }; diff --git a/src/lib/db/migrate.ts b/src/lib/db/migrate.ts index c492d3d..7c39647 100644 --- a/src/lib/db/migrate.ts +++ b/src/lib/db/migrate.ts @@ -13,10 +13,14 @@ if (!fs.existsSync(dataDir)) { } const sqlite = new Database(dbPath); +sqlite.pragma("journal_mode = WAL"); +sqlite.pragma("busy_timeout = 5000"); const db = drizzle(sqlite); console.log("Running migrations..."); migrate(db, { migrationsFolder: "./drizzle" }); console.log("Migrations complete!"); +// Checkpoint WAL to ensure clean state for build +sqlite.pragma("wal_checkpoint(TRUNCATE)"); sqlite.close();