From 3590641fadea617063bee7cf725d372cb77359e4 Mon Sep 17 00:00:00 2001 From: Till JS Date: Mon, 30 Mar 2026 00:56:57 +0200 Subject: [PATCH] feat(reader): restore from archive, register in monorepo - Move from apps-archived/ to apps/ - Add root package.json - Register in shared-branding (app icon, mana-apps, URL map) - Add to root CLAUDE.md project table - Expo/React Native TTS app preserved as-is (no rewrite needed) Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 1 + .../apps/mobile/ReadMe/MinimalDatabase.md | 570 ------------------ {apps-archived => apps}/reader/.gitignore | 0 {apps-archived => apps}/reader/CLAUDE.md | 0 .../apps/mobile/CONTEXT_MENU_SOLUTION.md | 0 .../mobile/ReadMe/AudioPlayerImprovements.md | 0 .../reader/apps/mobile/ReadMe/ExpoUI.md | 0 .../apps/mobile/ReadMe/MinimalDatabase.md | 562 +++++++++++++++++ .../apps/mobile/ReadMe/ProjectOverview.md | 0 .../reader/apps/mobile/app-env.d.ts | 0 .../reader/apps/mobile/app.json | 0 .../reader/apps/mobile/app/(auth)/_layout.tsx | 0 .../mobile/app/(auth)/forgot-password.tsx | 0 .../reader/apps/mobile/app/(auth)/login.tsx | 0 .../apps/mobile/app/(auth)/register.tsx | 0 .../reader/apps/mobile/app/(tabs)/_layout.tsx | 0 .../reader/apps/mobile/app/(tabs)/index.tsx | 0 .../reader/apps/mobile/app/(tabs)/two.tsx | 0 .../reader/apps/mobile/app/+html.tsx | 0 .../reader/apps/mobile/app/+not-found.tsx | 0 .../reader/apps/mobile/app/_layout.tsx | 0 .../reader/apps/mobile/app/add-text.tsx | 0 .../reader/apps/mobile/app/text/[id].tsx | 0 .../apps/mobile/assets/adaptive-icon.png | Bin .../reader/apps/mobile/assets/favicon.png | Bin .../reader/apps/mobile/assets/icon.png | Bin .../reader/apps/mobile/assets/splash.png | Bin .../reader/apps/mobile/babel.config.js | 0 .../reader/apps/mobile/cesconfig.jsonc | 0 .../apps/mobile/components/ActionMenu.tsx | 0 .../apps/mobile/components/AudioPlayer.tsx | 0 .../reader/apps/mobile/components/Button.tsx | 0 .../apps/mobile/components/ContextMenu.tsx | 0 .../apps/mobile/components/EditScreenInfo.tsx | 0 .../components/FloatingActionButton.tsx | 0 .../reader/apps/mobile/components/Header.tsx | 0 .../reader/apps/mobile/components/Icon.tsx | 0 .../mobile/components/MinimalAudioPlayer.tsx | 0 .../apps/mobile/components/ScreenContent.tsx | 0 .../apps/mobile/components/TabBarIcon.tsx | 0 .../apps/mobile/components/TagFilter.tsx | 0 .../reader/apps/mobile/components/Text.tsx | 0 .../apps/mobile/components/TextListItem.tsx | 0 .../apps/mobile/components/dropdown.tsx | 0 .../reader/apps/mobile/constants/voices.ts | 0 .../mobile/docs/browser-extension-concept.md | 0 .../apps/mobile/docs/deployment-guide.md | 0 .../apps/mobile/docs/google-cloud-setup.md | 0 .../mobile/docs/url-extraction-options.md | 0 .../reader/apps/mobile/eslint.config.js | 0 .../reader/apps/mobile/global.css | 0 .../reader/apps/mobile/hooks/useAudio.ts | 0 .../reader/apps/mobile/hooks/useAuth.ts | 0 .../reader/apps/mobile/hooks/useTexts.ts | 0 .../reader/apps/mobile/hooks/useTheme.ts | 0 .../reader/apps/mobile/metro.config.js | 0 .../reader/apps/mobile/nativewind-env.d.ts | 0 .../reader/apps/mobile/package.json | 0 .../reader/apps/mobile/prettier.config.js | 0 .../apps/mobile/services/audioService.ts | 0 .../mobile/services/urlExtractorService.ts | 0 .../reader/apps/mobile/store/store.ts | 0 .../apps/mobile/supabase/.temp/cli-latest | 0 .../apps/mobile/supabase/.temp/gotrue-version | 0 .../apps/mobile/supabase/.temp/pooler-url | 0 .../mobile/supabase/.temp/postgres-version | 0 .../apps/mobile/supabase/.temp/project-ref | 0 .../apps/mobile/supabase/.temp/rest-version | 0 .../mobile/supabase/.temp/storage-version | 0 .../extract-url-scrapingbee/index.ts | 0 .../supabase/functions/extract-url/index.ts | 0 .../functions/generate-audio/index.ts | 0 .../supabase/functions/get-audio-url/index.ts | 0 .../20240116_create_texts_table.sql | 0 .../20240117_create_audio_storage.sql | 0 .../reader/apps/mobile/tailwind.config.js | 0 .../reader/apps/mobile/tsconfig.json | 0 .../reader/apps/mobile/types/database.ts | 0 .../apps/mobile/utils/audioMigration.ts | 0 .../reader/apps/mobile/utils/storage.ts | 0 .../reader/apps/mobile/utils/supabase.ts | 0 apps/reader/package.json | 8 + packages/shared-branding/src/app-icons.ts | 3 + packages/shared-branding/src/mana-apps.ts | 17 + 84 files changed, 591 insertions(+), 570 deletions(-) delete mode 100644 apps-archived/reader/apps/mobile/ReadMe/MinimalDatabase.md rename {apps-archived => apps}/reader/.gitignore (100%) rename {apps-archived => apps}/reader/CLAUDE.md (100%) rename {apps-archived => apps}/reader/apps/mobile/CONTEXT_MENU_SOLUTION.md (100%) rename {apps-archived => apps}/reader/apps/mobile/ReadMe/AudioPlayerImprovements.md (100%) rename {apps-archived => apps}/reader/apps/mobile/ReadMe/ExpoUI.md (100%) create mode 100644 apps/reader/apps/mobile/ReadMe/MinimalDatabase.md rename {apps-archived => apps}/reader/apps/mobile/ReadMe/ProjectOverview.md (100%) rename {apps-archived => apps}/reader/apps/mobile/app-env.d.ts (100%) rename {apps-archived => apps}/reader/apps/mobile/app.json (100%) rename {apps-archived => apps}/reader/apps/mobile/app/(auth)/_layout.tsx (100%) rename {apps-archived => apps}/reader/apps/mobile/app/(auth)/forgot-password.tsx (100%) rename {apps-archived => apps}/reader/apps/mobile/app/(auth)/login.tsx (100%) rename {apps-archived => apps}/reader/apps/mobile/app/(auth)/register.tsx (100%) rename {apps-archived => apps}/reader/apps/mobile/app/(tabs)/_layout.tsx (100%) rename {apps-archived => apps}/reader/apps/mobile/app/(tabs)/index.tsx (100%) rename {apps-archived => apps}/reader/apps/mobile/app/(tabs)/two.tsx (100%) rename {apps-archived => apps}/reader/apps/mobile/app/+html.tsx (100%) rename {apps-archived => apps}/reader/apps/mobile/app/+not-found.tsx (100%) rename {apps-archived => apps}/reader/apps/mobile/app/_layout.tsx (100%) rename {apps-archived => apps}/reader/apps/mobile/app/add-text.tsx (100%) rename {apps-archived => apps}/reader/apps/mobile/app/text/[id].tsx (100%) rename {apps-archived => apps}/reader/apps/mobile/assets/adaptive-icon.png (100%) rename {apps-archived => apps}/reader/apps/mobile/assets/favicon.png (100%) rename {apps-archived => apps}/reader/apps/mobile/assets/icon.png (100%) rename {apps-archived => apps}/reader/apps/mobile/assets/splash.png (100%) rename {apps-archived => apps}/reader/apps/mobile/babel.config.js (100%) rename {apps-archived => apps}/reader/apps/mobile/cesconfig.jsonc (100%) rename {apps-archived => apps}/reader/apps/mobile/components/ActionMenu.tsx (100%) rename {apps-archived => apps}/reader/apps/mobile/components/AudioPlayer.tsx (100%) rename {apps-archived => apps}/reader/apps/mobile/components/Button.tsx (100%) rename {apps-archived => apps}/reader/apps/mobile/components/ContextMenu.tsx (100%) rename {apps-archived => apps}/reader/apps/mobile/components/EditScreenInfo.tsx (100%) rename {apps-archived => apps}/reader/apps/mobile/components/FloatingActionButton.tsx (100%) rename {apps-archived => apps}/reader/apps/mobile/components/Header.tsx (100%) rename {apps-archived => apps}/reader/apps/mobile/components/Icon.tsx (100%) rename {apps-archived => apps}/reader/apps/mobile/components/MinimalAudioPlayer.tsx (100%) rename {apps-archived => apps}/reader/apps/mobile/components/ScreenContent.tsx (100%) rename {apps-archived => apps}/reader/apps/mobile/components/TabBarIcon.tsx (100%) rename {apps-archived => apps}/reader/apps/mobile/components/TagFilter.tsx (100%) rename {apps-archived => apps}/reader/apps/mobile/components/Text.tsx (100%) rename {apps-archived => apps}/reader/apps/mobile/components/TextListItem.tsx (100%) rename {apps-archived => apps}/reader/apps/mobile/components/dropdown.tsx (100%) rename {apps-archived => apps}/reader/apps/mobile/constants/voices.ts (100%) rename {apps-archived => apps}/reader/apps/mobile/docs/browser-extension-concept.md (100%) rename {apps-archived => apps}/reader/apps/mobile/docs/deployment-guide.md (100%) rename {apps-archived => apps}/reader/apps/mobile/docs/google-cloud-setup.md (100%) rename {apps-archived => apps}/reader/apps/mobile/docs/url-extraction-options.md (100%) rename {apps-archived => apps}/reader/apps/mobile/eslint.config.js (100%) rename {apps-archived => apps}/reader/apps/mobile/global.css (100%) rename {apps-archived => apps}/reader/apps/mobile/hooks/useAudio.ts (100%) rename {apps-archived => apps}/reader/apps/mobile/hooks/useAuth.ts (100%) rename {apps-archived => apps}/reader/apps/mobile/hooks/useTexts.ts (100%) rename {apps-archived => apps}/reader/apps/mobile/hooks/useTheme.ts (100%) rename {apps-archived => apps}/reader/apps/mobile/metro.config.js (100%) rename {apps-archived => apps}/reader/apps/mobile/nativewind-env.d.ts (100%) rename {apps-archived => apps}/reader/apps/mobile/package.json (100%) rename {apps-archived => apps}/reader/apps/mobile/prettier.config.js (100%) rename {apps-archived => apps}/reader/apps/mobile/services/audioService.ts (100%) rename {apps-archived => apps}/reader/apps/mobile/services/urlExtractorService.ts (100%) rename {apps-archived => apps}/reader/apps/mobile/store/store.ts (100%) rename {apps-archived => apps}/reader/apps/mobile/supabase/.temp/cli-latest (100%) rename {apps-archived => apps}/reader/apps/mobile/supabase/.temp/gotrue-version (100%) rename {apps-archived => apps}/reader/apps/mobile/supabase/.temp/pooler-url (100%) rename {apps-archived => apps}/reader/apps/mobile/supabase/.temp/postgres-version (100%) rename {apps-archived => apps}/reader/apps/mobile/supabase/.temp/project-ref (100%) rename {apps-archived => apps}/reader/apps/mobile/supabase/.temp/rest-version (100%) rename {apps-archived => apps}/reader/apps/mobile/supabase/.temp/storage-version (100%) rename {apps-archived => apps}/reader/apps/mobile/supabase/functions/extract-url-scrapingbee/index.ts (100%) rename {apps-archived => apps}/reader/apps/mobile/supabase/functions/extract-url/index.ts (100%) rename {apps-archived => apps}/reader/apps/mobile/supabase/functions/generate-audio/index.ts (100%) rename {apps-archived => apps}/reader/apps/mobile/supabase/functions/get-audio-url/index.ts (100%) rename {apps-archived => apps}/reader/apps/mobile/supabase/migrations/20240116_create_texts_table.sql (100%) rename {apps-archived => apps}/reader/apps/mobile/supabase/migrations/20240117_create_audio_storage.sql (100%) rename {apps-archived => apps}/reader/apps/mobile/tailwind.config.js (100%) rename {apps-archived => apps}/reader/apps/mobile/tsconfig.json (100%) rename {apps-archived => apps}/reader/apps/mobile/types/database.ts (100%) rename {apps-archived => apps}/reader/apps/mobile/utils/audioMigration.ts (100%) rename {apps-archived => apps}/reader/apps/mobile/utils/storage.ts (100%) rename {apps-archived => apps}/reader/apps/mobile/utils/supabase.ts (100%) create mode 100644 apps/reader/package.json diff --git a/CLAUDE.md b/CLAUDE.md index d0111e4eb..dbdaf663c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -58,6 +58,7 @@ For comprehensive guidelines on code patterns and conventions, see the `.claude/ | **uload** | URL shortener & link management | Server, Web, Landing | | **news** | AI news reader & personal library | Server, Web, Landing | | **wisekeep** | AI transcription & wisdom library | Server, Web, Landing | +| **reader** | Text-to-Speech with offline audio | Mobile | | **calc** | Calculator & converter | Web | | **playground** | LLM playground | Web | diff --git a/apps-archived/reader/apps/mobile/ReadMe/MinimalDatabase.md b/apps-archived/reader/apps/mobile/ReadMe/MinimalDatabase.md deleted file mode 100644 index a566b8c70..000000000 --- a/apps-archived/reader/apps/mobile/ReadMe/MinimalDatabase.md +++ /dev/null @@ -1,570 +0,0 @@ -# Absolut Minimalste Text-to-Speech Datenbank - -## Philosophie -Eine einzige Tabelle für alles. JSONB macht's möglich. Keine Joins, keine Komplexität, nur pure Funktionalität. - -## Die Eine Tabelle - -```sql --- Die einzige Tabelle die du brauchst -CREATE TABLE texts ( - id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, - user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE, - - -- Der eigentliche Content - title TEXT NOT NULL, - content TEXT NOT NULL, - - -- ALLES andere in einem JSONB Feld - data JSONB DEFAULT '{}' NOT NULL, - - -- Nur die absolut nötigen Timestamps - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- Ein Index für Performance -CREATE INDEX idx_texts_user ON texts(user_id); -CREATE INDEX idx_texts_data ON texts USING GIN (data); - --- RLS aktivieren -ALTER TABLE texts ENABLE ROW LEVEL SECURITY; - --- Jeder sieht nur seine eigenen Texte -CREATE POLICY "Own texts only" ON texts - FOR ALL USING (auth.uid() = user_id); - --- Update Timestamp Trigger -CREATE OR REPLACE FUNCTION update_updated_at() -RETURNS TRIGGER AS $$ -BEGIN - NEW.updated_at = NOW(); - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - -CREATE TRIGGER update_texts_updated_at - BEFORE UPDATE ON texts - FOR EACH ROW - EXECUTE FUNCTION update_updated_at(); -``` - -## Was kommt ins `data` JSONB Feld? - -```javascript -// Beispiel eines vollständigen Text-Objekts -{ - id: "uuid-hier", - user_id: "user-uuid", - title: "Mein Buch", - content: "Der eigentliche Text...", - data: { - // Vorlese-Einstellungen - tts: { - speed: 1.0, - voice: "de-DE", - lastPosition: 1234, // Zeichen-Position - lastPlayed: "2024-01-15T10:30:00Z" - }, - - // Audio-Cache (NEU!) - audio: { - hasLocalCache: false, - chunks: [ - { - id: "chunk-1", - start: 0, - end: 1000, // Zeichen-Position - filename: "text-uuid-chunk-1.mp3", - size: 245760, // Bytes - duration: 120, // Sekunden - createdAt: "2024-01-15T10:00:00Z" - } - ], - totalSize: 2457600, // Total in Bytes - lastGenerated: "2024-01-15T10:00:00Z", - settings: { // Settings bei Generierung - voice: "de-DE", - speed: 1.0 - } - }, - - // Organisation (optional) - tags: ["roman", "favorit"], - color: "#FF5733", - - // Statistiken (optional) - stats: { - playCount: 5, - totalTime: 3600, // Sekunden - completed: false - }, - - // Was auch immer du später brauchst - notes: "Für die Zugfahrt", - source: "kindle-import" - }, - created_at: "2024-01-01T10:00:00Z", - updated_at: "2024-01-15T10:30:00Z" -} -``` - -## Basis-Operationen - -### Text erstellen -```javascript -const { data, error } = await supabase - .from('texts') - .insert({ - title: 'Mein Text', - content: 'Inhalt hier...', - data: { - tts: { speed: 1.0, voice: 'de-DE' }, - tags: ['neu'] - } - }); -``` - -### Alle Texte holen -```javascript -const { data: texts } = await supabase - .from('texts') - .select('*') - .order('updated_at', { ascending: false }); -``` - -### Nach Tags filtern -```javascript -const { data: filtered } = await supabase - .from('texts') - .select('*') - .contains('data', { tags: ['favorit'] }); -``` - -### Leseposition updaten -```javascript -const { error } = await supabase - .from('texts') - .update({ - data: { - ...currentData, - tts: { - ...currentData.tts, - lastPosition: 5678, - lastPlayed: new Date().toISOString() - } - } - }) - .eq('id', textId); -``` - -### Statistiken hochzählen -```sql --- Als Postgres Funktion für atomare Updates -CREATE OR REPLACE FUNCTION increment_play_count(text_id UUID) -RETURNS void AS $$ -BEGIN - UPDATE texts - SET data = jsonb_set( - jsonb_set( - data, - '{stats,playCount}', - to_jsonb(COALESCE((data->'stats'->>'playCount')::int, 0) + 1) - ), - '{tts,lastPlayed}', - to_jsonb(NOW()) - ) - WHERE id = text_id; -END; -$$ LANGUAGE plpgsql; - --- Aufruf -SELECT increment_play_count('text-uuid-hier'); -``` - -## Supabase Quickstart - -```bash -# 1. Supabase CLI installieren -npm install -g supabase - -# 2. Projekt initialisieren -supabase init - -# 3. Migration erstellen -supabase migration new create_texts_table - -# 4. SQL von oben in die Migration kopieren - -# 5. Migration ausführen -supabase db push -``` - -## React Native Integration - -```javascript -// hooks/useTexts.js -import { useState, useEffect } from 'react'; -import { supabase } from '../lib/supabase'; - -export const useTexts = () => { - const [texts, setTexts] = useState([]); - const [loading, setLoading] = useState(true); - - useEffect(() => { - fetchTexts(); - }, []); - - const fetchTexts = async () => { - const { data } = await supabase - .from('texts') - .select('*') - .order('updated_at', { ascending: false }); - - setTexts(data || []); - setLoading(false); - }; - - const createText = async (title, content) => { - const { data, error } = await supabase - .from('texts') - .insert({ - title, - content, - data: { tts: { speed: 1.0, voice: 'de-DE' } } - }) - .select() - .single(); - - if (data) { - setTexts([data, ...texts]); - } - return { data, error }; - }; - - const updatePosition = async (textId, position) => { - const text = texts.find(t => t.id === textId); - if (!text) return; - - await supabase - .from('texts') - .update({ - data: { - ...text.data, - tts: { - ...text.data.tts, - lastPosition: position, - lastPlayed: new Date().toISOString() - } - } - }) - .eq('id', textId); - }; - - return { texts, loading, createText, updatePosition, refetch: fetchTexts }; -}; -``` - -## Audio-Cache Management - -```javascript -// hooks/useAudioCache.js -import * as FileSystem from 'expo-file-system'; -import * as Speech from 'expo-speech'; -import { Audio } from 'expo-av'; -import { supabase } from '../lib/supabase'; - -const AUDIO_DIR = `${FileSystem.documentDirectory}audio/`; - -export const useAudioCache = () => { - // Verzeichnis erstellen beim Start - useEffect(() => { - FileSystem.makeDirectoryAsync(AUDIO_DIR, { intermediates: true }) - .catch(() => {}); // Ignorieren wenn bereits existiert - }, []); - - // Text in Chunks aufteilen (z.B. alle 1000 Zeichen) - const chunkText = (text, chunkSize = 1000) => { - const chunks = []; - for (let i = 0; i < text.length; i += chunkSize) { - chunks.push({ - id: `chunk-${chunks.length}`, - start: i, - end: Math.min(i + chunkSize, text.length), - content: text.slice(i, i + chunkSize) - }); - } - return chunks; - }; - - // Audio für einen Chunk generieren und speichern - const generateAudioChunk = async (textId, chunk, settings) => { - const filename = `${textId}-${chunk.id}.mp3`; - const filePath = `${AUDIO_DIR}${filename}`; - - // Option 1: Mit einer TTS API (z.B. Google Cloud TTS) - // const audioData = await callTTSAPI(chunk.content, settings); - // await FileSystem.writeAsStringAsync(filePath, audioData, { - // encoding: FileSystem.EncodingType.Base64 - // }); - - // Option 2: Workaround mit expo-speech (keine direkte MP3 Generierung) - // Hinweis: expo-speech kann nicht direkt als Datei speichern - // Alternative: Web-API oder Cloud-Service nutzen - - const fileInfo = await FileSystem.getInfoAsync(filePath); - - return { - id: chunk.id, - start: chunk.start, - end: chunk.end, - filename, - size: fileInfo.size || 0, - duration: Math.ceil(chunk.content.length / 150) * 60, // Geschätzt - createdAt: new Date().toISOString() - }; - }; - - // Alle Chunks für einen Text generieren - const generateAudioForText = async (textId, content, settings = {}) => { - const chunks = chunkText(content); - const audioChunks = []; - - for (const chunk of chunks) { - const audioChunk = await generateAudioChunk(textId, chunk, settings); - audioChunks.push(audioChunk); - } - - // Metadaten in Supabase updaten - await updateAudioMetadata(textId, audioChunks, settings); - - return audioChunks; - }; - - // Audio-Metadaten in Supabase speichern - const updateAudioMetadata = async (textId, chunks, settings) => { - const totalSize = chunks.reduce((sum, chunk) => sum + chunk.size, 0); - - const { data: currentText } = await supabase - .from('texts') - .select('data') - .eq('id', textId) - .single(); - - await supabase - .from('texts') - .update({ - data: { - ...currentText.data, - audio: { - hasLocalCache: true, - chunks, - totalSize, - lastGenerated: new Date().toISOString(), - settings - } - } - }) - .eq('id', textId); - }; - - // Audio abspielen - const playAudioFromCache = async (textId, startPosition = 0) => { - const { data: text } = await supabase - .from('texts') - .select('data') - .eq('id', textId) - .single(); - - if (!text?.data?.audio?.hasLocalCache) { - throw new Error('Kein Audio-Cache vorhanden'); - } - - // Richtigen Chunk finden - const chunk = text.data.audio.chunks.find( - c => startPosition >= c.start && startPosition < c.end - ); - - if (!chunk) return; - - const filePath = `${AUDIO_DIR}${chunk.filename}`; - const { sound } = await Audio.Sound.createAsync({ uri: filePath }); - - // Position innerhalb des Chunks berechnen - const chunkPosition = startPosition - chunk.start; - const positionMillis = (chunkPosition / chunk.end) * chunk.duration * 1000; - - await sound.setPositionAsync(positionMillis); - await sound.playAsync(); - - return sound; - }; - - // Cache löschen - const clearAudioCache = async (textId) => { - const { data: text } = await supabase - .from('texts') - .select('data') - .eq('id', textId) - .single(); - - if (text?.data?.audio?.chunks) { - for (const chunk of text.data.audio.chunks) { - try { - await FileSystem.deleteAsync(`${AUDIO_DIR}${chunk.filename}`); - } catch (e) { - console.log('Fehler beim Löschen:', e); - } - } - } - - // Metadaten updaten - await supabase - .from('texts') - .update({ - data: { - ...text.data, - audio: { - hasLocalCache: false, - chunks: [], - totalSize: 0 - } - } - }) - .eq('id', textId); - }; - - // Cache-Größe berechnen - const getCacheSize = async () => { - const files = await FileSystem.readDirectoryAsync(AUDIO_DIR); - let totalSize = 0; - - for (const file of files) { - const info = await FileSystem.getInfoAsync(`${AUDIO_DIR}${file}`); - totalSize += info.size || 0; - } - - return totalSize; - }; - - return { - generateAudioForText, - playAudioFromCache, - clearAudioCache, - getCacheSize - }; -}; -``` - -## Beispiel-Screen für Audio-Management - -```javascript -// screens/TextDetailScreen.js -import React, { useState } from 'react'; -import { View, Text, Button, ActivityIndicator } from 'react-native'; -import { useAudioCache } from '../hooks/useAudioCache'; - -export const TextDetailScreen = ({ route }) => { - const { text } = route.params; - const { generateAudioForText, playAudioFromCache, clearAudioCache } = useAudioCache(); - const [generating, setGenerating] = useState(false); - - const hasCache = text.data?.audio?.hasLocalCache; - - const handleGenerateAudio = async () => { - setGenerating(true); - try { - await generateAudioForText(text.id, text.content, { - voice: text.data?.tts?.voice || 'de-DE', - speed: text.data?.tts?.speed || 1.0 - }); - // Text-Objekt neu laden - } catch (error) { - console.error('Fehler beim Generieren:', error); - } finally { - setGenerating(false); - } - }; - - const handlePlay = async () => { - try { - const position = text.data?.tts?.lastPosition || 0; - await playAudioFromCache(text.id, position); - } catch (error) { - // Fallback zu expo-speech - Speech.speak(text.content.slice(position), { - language: text.data?.tts?.voice || 'de-DE', - rate: text.data?.tts?.speed || 1.0 - }); - } - }; - - return ( - - {text.title} - - {!hasCache && ( -