From 58a342b4070f41b26a1385c8de5620f80010c45b Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Thu, 27 Nov 2025 15:31:38 +0100 Subject: [PATCH] feat(reader): integrate reader app into monorepo structure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Restructure to apps/reader/apps/mobile/ pattern - Rename package to @reader/mobile - Add reader:dev and dev:reader:mobile scripts - Update CLAUDE.md with monorepo commands - Remove standalone .git repository - Convert from npm to pnpm 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- apps/reader/.gitignore | 40 + apps/reader/CLAUDE.md | 141 +++ apps/reader/apps/mobile/.mcp.json | 16 + .../apps/mobile/CONTEXT_MENU_SOLUTION.md | 58 ++ .../mobile/ReadMe/AudioPlayerImprovements.md | 78 ++ apps/reader/apps/mobile/ReadMe/ExpoUI.md | 226 +++++ .../apps/mobile/ReadMe/MinimalDatabase.md | 570 ++++++++++++ .../apps/mobile/ReadMe/ProjectOverview.md | 217 +++++ apps/reader/apps/mobile/app-env.d.ts | 2 + apps/reader/apps/mobile/app.json | 47 + .../reader/apps/mobile/app/(auth)/_layout.tsx | 15 + .../mobile/app/(auth)/forgot-password.tsx | 120 +++ apps/reader/apps/mobile/app/(auth)/login.tsx | 125 +++ .../apps/mobile/app/(auth)/register.tsx | 144 ++++ .../reader/apps/mobile/app/(tabs)/_layout.tsx | 34 + apps/reader/apps/mobile/app/(tabs)/index.tsx | 338 ++++++++ apps/reader/apps/mobile/app/(tabs)/two.tsx | 211 +++++ apps/reader/apps/mobile/app/+html.tsx | 46 + apps/reader/apps/mobile/app/+not-found.tsx | 24 + apps/reader/apps/mobile/app/_layout.tsx | 35 + apps/reader/apps/mobile/app/add-text.tsx | 269 ++++++ apps/reader/apps/mobile/app/text/[id].tsx | 214 +++++ .../apps/mobile/assets/adaptive-icon.png | Bin 0 -> 17547 bytes apps/reader/apps/mobile/assets/favicon.png | Bin 0 -> 1466 bytes apps/reader/apps/mobile/assets/icon.png | Bin 0 -> 22380 bytes apps/reader/apps/mobile/assets/splash.png | Bin 0 -> 47346 bytes apps/reader/apps/mobile/babel.config.js | 10 + apps/reader/apps/mobile/cesconfig.jsonc | 46 + .../apps/mobile/components/ActionMenu.tsx | 186 ++++ .../apps/mobile/components/AudioPlayer.tsx | 469 ++++++++++ apps/reader/apps/mobile/components/Button.tsx | 207 +++++ .../apps/mobile/components/ContextMenu.tsx | 156 ++++ .../apps/mobile/components/EditScreenInfo.tsx | 29 + .../components/FloatingActionButton.tsx | 42 + apps/reader/apps/mobile/components/Header.tsx | 89 ++ apps/reader/apps/mobile/components/Icon.tsx | 128 +++ .../mobile/components/MinimalAudioPlayer.tsx | 90 ++ .../apps/mobile/components/ScreenContent.tsx | 25 + .../apps/mobile/components/TabBarIcon.tsx | 15 + .../apps/mobile/components/TagFilter.tsx | 53 ++ apps/reader/apps/mobile/components/Text.tsx | 158 ++++ .../apps/mobile/components/TextListItem.tsx | 93 ++ .../apps/mobile/components/dropdown.tsx | 135 +++ apps/reader/apps/mobile/constants/voices.ts | 386 +++++++++ .../mobile/docs/browser-extension-concept.md | 116 +++ .../apps/mobile/docs/deployment-guide.md | 270 ++++++ .../apps/mobile/docs/google-cloud-setup.md | 126 +++ .../mobile/docs/url-extraction-options.md | 133 +++ apps/reader/apps/mobile/eslint.config.js | 15 + apps/reader/apps/mobile/global.css | 3 + apps/reader/apps/mobile/hooks/useAudio.ts | 416 +++++++++ apps/reader/apps/mobile/hooks/useAuth.ts | 113 +++ apps/reader/apps/mobile/hooks/useTexts.ts | 177 ++++ apps/reader/apps/mobile/hooks/useTheme.ts | 179 ++++ apps/reader/apps/mobile/metro.config.js | 10 + apps/reader/apps/mobile/nativewind-env.d.ts | 3 + apps/reader/apps/mobile/package.json | 59 ++ apps/reader/apps/mobile/prettier.config.js | 10 + .../apps/mobile/services/audioService.ts | 339 ++++++++ .../mobile/services/urlExtractorService.ts | 131 +++ apps/reader/apps/mobile/store/store.ts | 84 ++ .../apps/mobile/supabase/.temp/cli-latest | 1 + .../apps/mobile/supabase/.temp/gotrue-version | 1 + .../apps/mobile/supabase/.temp/pooler-url | 1 + .../mobile/supabase/.temp/postgres-version | 1 + .../apps/mobile/supabase/.temp/project-ref | 1 + .../apps/mobile/supabase/.temp/rest-version | 1 + .../mobile/supabase/.temp/storage-version | 1 + .../extract-url-scrapingbee/index.ts | 299 +++++++ .../supabase/functions/extract-url/index.ts | 332 +++++++ .../functions/generate-audio/index.ts | 498 +++++++++++ .../supabase/functions/get-audio-url/index.ts | 110 +++ .../20240116_create_texts_table.sql | 62 ++ .../20240117_create_audio_storage.sql | 31 + apps/reader/apps/mobile/tailwind.config.js | 10 + apps/reader/apps/mobile/tsconfig.json | 12 + apps/reader/apps/mobile/types/database.ts | 79 ++ .../apps/mobile/utils/audioMigration.ts | 60 ++ apps/reader/apps/mobile/utils/storage.ts | 29 + apps/reader/apps/mobile/utils/supabase.ts | 14 + package.json | 14 +- pnpm-lock.yaml | 810 +++++++++++++++++- 82 files changed, 9831 insertions(+), 7 deletions(-) create mode 100644 apps/reader/.gitignore create mode 100644 apps/reader/CLAUDE.md create mode 100644 apps/reader/apps/mobile/.mcp.json create mode 100644 apps/reader/apps/mobile/CONTEXT_MENU_SOLUTION.md create mode 100644 apps/reader/apps/mobile/ReadMe/AudioPlayerImprovements.md create mode 100644 apps/reader/apps/mobile/ReadMe/ExpoUI.md create mode 100644 apps/reader/apps/mobile/ReadMe/MinimalDatabase.md create mode 100644 apps/reader/apps/mobile/ReadMe/ProjectOverview.md create mode 100644 apps/reader/apps/mobile/app-env.d.ts create mode 100644 apps/reader/apps/mobile/app.json create mode 100644 apps/reader/apps/mobile/app/(auth)/_layout.tsx create mode 100644 apps/reader/apps/mobile/app/(auth)/forgot-password.tsx create mode 100644 apps/reader/apps/mobile/app/(auth)/login.tsx create mode 100644 apps/reader/apps/mobile/app/(auth)/register.tsx create mode 100644 apps/reader/apps/mobile/app/(tabs)/_layout.tsx create mode 100644 apps/reader/apps/mobile/app/(tabs)/index.tsx create mode 100644 apps/reader/apps/mobile/app/(tabs)/two.tsx create mode 100644 apps/reader/apps/mobile/app/+html.tsx create mode 100644 apps/reader/apps/mobile/app/+not-found.tsx create mode 100644 apps/reader/apps/mobile/app/_layout.tsx create mode 100644 apps/reader/apps/mobile/app/add-text.tsx create mode 100644 apps/reader/apps/mobile/app/text/[id].tsx create mode 100644 apps/reader/apps/mobile/assets/adaptive-icon.png create mode 100644 apps/reader/apps/mobile/assets/favicon.png create mode 100644 apps/reader/apps/mobile/assets/icon.png create mode 100644 apps/reader/apps/mobile/assets/splash.png create mode 100644 apps/reader/apps/mobile/babel.config.js create mode 100644 apps/reader/apps/mobile/cesconfig.jsonc create mode 100644 apps/reader/apps/mobile/components/ActionMenu.tsx create mode 100644 apps/reader/apps/mobile/components/AudioPlayer.tsx create mode 100644 apps/reader/apps/mobile/components/Button.tsx create mode 100644 apps/reader/apps/mobile/components/ContextMenu.tsx create mode 100644 apps/reader/apps/mobile/components/EditScreenInfo.tsx create mode 100644 apps/reader/apps/mobile/components/FloatingActionButton.tsx create mode 100644 apps/reader/apps/mobile/components/Header.tsx create mode 100644 apps/reader/apps/mobile/components/Icon.tsx create mode 100644 apps/reader/apps/mobile/components/MinimalAudioPlayer.tsx create mode 100644 apps/reader/apps/mobile/components/ScreenContent.tsx create mode 100644 apps/reader/apps/mobile/components/TabBarIcon.tsx create mode 100644 apps/reader/apps/mobile/components/TagFilter.tsx create mode 100644 apps/reader/apps/mobile/components/Text.tsx create mode 100644 apps/reader/apps/mobile/components/TextListItem.tsx create mode 100644 apps/reader/apps/mobile/components/dropdown.tsx create mode 100644 apps/reader/apps/mobile/constants/voices.ts create mode 100644 apps/reader/apps/mobile/docs/browser-extension-concept.md create mode 100644 apps/reader/apps/mobile/docs/deployment-guide.md create mode 100644 apps/reader/apps/mobile/docs/google-cloud-setup.md create mode 100644 apps/reader/apps/mobile/docs/url-extraction-options.md create mode 100644 apps/reader/apps/mobile/eslint.config.js create mode 100644 apps/reader/apps/mobile/global.css create mode 100644 apps/reader/apps/mobile/hooks/useAudio.ts create mode 100644 apps/reader/apps/mobile/hooks/useAuth.ts create mode 100644 apps/reader/apps/mobile/hooks/useTexts.ts create mode 100644 apps/reader/apps/mobile/hooks/useTheme.ts create mode 100644 apps/reader/apps/mobile/metro.config.js create mode 100644 apps/reader/apps/mobile/nativewind-env.d.ts create mode 100644 apps/reader/apps/mobile/package.json create mode 100644 apps/reader/apps/mobile/prettier.config.js create mode 100644 apps/reader/apps/mobile/services/audioService.ts create mode 100644 apps/reader/apps/mobile/services/urlExtractorService.ts create mode 100644 apps/reader/apps/mobile/store/store.ts create mode 100644 apps/reader/apps/mobile/supabase/.temp/cli-latest create mode 100644 apps/reader/apps/mobile/supabase/.temp/gotrue-version create mode 100644 apps/reader/apps/mobile/supabase/.temp/pooler-url create mode 100644 apps/reader/apps/mobile/supabase/.temp/postgres-version create mode 100644 apps/reader/apps/mobile/supabase/.temp/project-ref create mode 100644 apps/reader/apps/mobile/supabase/.temp/rest-version create mode 100644 apps/reader/apps/mobile/supabase/.temp/storage-version create mode 100644 apps/reader/apps/mobile/supabase/functions/extract-url-scrapingbee/index.ts create mode 100644 apps/reader/apps/mobile/supabase/functions/extract-url/index.ts create mode 100644 apps/reader/apps/mobile/supabase/functions/generate-audio/index.ts create mode 100644 apps/reader/apps/mobile/supabase/functions/get-audio-url/index.ts create mode 100644 apps/reader/apps/mobile/supabase/migrations/20240116_create_texts_table.sql create mode 100644 apps/reader/apps/mobile/supabase/migrations/20240117_create_audio_storage.sql create mode 100644 apps/reader/apps/mobile/tailwind.config.js create mode 100644 apps/reader/apps/mobile/tsconfig.json create mode 100644 apps/reader/apps/mobile/types/database.ts create mode 100644 apps/reader/apps/mobile/utils/audioMigration.ts create mode 100644 apps/reader/apps/mobile/utils/storage.ts create mode 100644 apps/reader/apps/mobile/utils/supabase.ts diff --git a/apps/reader/.gitignore b/apps/reader/.gitignore new file mode 100644 index 000000000..f5f1c697b --- /dev/null +++ b/apps/reader/.gitignore @@ -0,0 +1,40 @@ +# Dependencies +node_modules/ + +# Build outputs +dist/ +web-build/ + +# Expo +.expo/ +expo-env.d.ts + +# React Native (generated via expo prebuild) +ios/ +android/ + +# Environment +.env +.env.local +.env.production + +# Debug +npm-debug.* +*.log + +# Certificates & Keys +*.jks +*.p8 +*.p12 +*.key +*.mobileprovision +*.orig.* + +# Metro +.metro-health-check* + +# IDE +.idea/ + +# OS +.DS_Store diff --git a/apps/reader/CLAUDE.md b/apps/reader/CLAUDE.md new file mode 100644 index 000000000..cbe53dc14 --- /dev/null +++ b/apps/reader/CLAUDE.md @@ -0,0 +1,141 @@ +# CLAUDE.md - Reader + +This file provides guidance to Claude Code when working with the Reader project. + +## Project Overview + +Reader is a Text-to-Speech React Native application built with Expo that converts text to high-quality audio using Google Chirp voices. It stores audio locally for offline playback and syncs data across devices via Supabase. + +## Architecture + +``` +apps/reader/ +├── apps/ +│ └── mobile/ # Expo React Native App (@reader/mobile) +│ ├── app/ # Expo Router navigation +│ │ ├── (tabs)/ # Tab navigation screens +│ │ ├── (auth)/ # Auth flow routes +│ │ └── _layout.tsx +│ ├── components/ # Reusable UI components +│ ├── hooks/ # Custom React hooks +│ ├── services/ # Business logic services +│ ├── store/ # Zustand state management +│ ├── types/ # TypeScript types +│ ├── utils/ # Utilities (Supabase client, etc.) +│ ├── assets/ # Images, fonts +│ └── package.json # @reader/mobile +├── packages/ # For future shared code +├── CLAUDE.md # This file +└── .gitignore +``` + +## Development Commands + +```bash +# From monorepo root +pnpm install + +# Start Reader mobile app +pnpm reader:dev +# Or directly +pnpm dev:reader:mobile + +# From apps/reader/apps/mobile/ +pnpm dev # Start Expo dev server +pnpm ios # Run on iOS simulator +pnpm android # Run on Android emulator +pnpm web # Run on web + +# Code quality +pnpm lint # Run ESLint +pnpm format # Format with Prettier + +# Build for production +pnpm build:preview # Preview build +pnpm build:prod # Production build +``` + +## Tech Stack + +| Component | Technology | +|-----------|------------| +| Framework | React Native 0.79.5 + Expo SDK 53 | +| Navigation | Expo Router v5 (file-based) | +| Styling | NativeWind (Tailwind CSS for RN) | +| State | Zustand | +| Backend | Supabase (PostgreSQL + Auth) | +| Language | TypeScript | + +## Database Design + +Single `texts` table with JSONB field for flexibility: +- Stores texts, metadata, tags, and reading progress +- Audio files stored locally, paths tracked in DB +- Designed for future expansion without migrations + +See `apps/mobile/ReadMe/MinimalDatabase.md` for details. + +## Key Implementation Patterns + +### Navigation (Expo Router) +```tsx +// File-based routing in apps/mobile/app/ +// (tabs)/ - Tab navigation screens +// (auth)/ - Auth flow routes +``` + +### Styling (NativeWind) +```tsx + + Hello + +``` + +### State Management (Zustand) +```tsx +import { useStore } from '~/store/store'; +const { state, actions } = useStore(); +``` + +### Supabase Client +```tsx +// Client configured in apps/mobile/utils/supabase.ts +import { supabase } from '~/utils/supabase'; +``` + +### Path Alias +Use `~/*` for absolute imports from mobile root: +```tsx +import { Button } from '~/components/Button'; +``` + +## Environment Variables + +Create `apps/reader/apps/mobile/.env`: +```bash +EXPO_PUBLIC_SUPABASE_URL=your_supabase_url +EXPO_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key +``` + +## Current Implementation Status + +- [x] Expo Router setup with tab navigation +- [x] Supabase integration +- [x] Zustand store (user state, settings, audio player) +- [x] NativeWind styling +- [x] User authentication (Login, Register, Forgot Password) +- [x] Text management UI (List, Add, View, Delete) +- [x] Settings screen +- [x] Text-to-Speech with Google Cloud TTS +- [x] Audio player with progress tracking +- [x] Offline audio storage (Expo FileSystem) +- [x] Tag system with filtering +- [x] Supabase Edge Functions for audio generation +- [x] Audio chunk system for large texts +- [x] Local audio caching + +## Detailed Documentation + +- `apps/mobile/ReadMe/ProjectOverview.md` - Project vision (German) +- `apps/mobile/ReadMe/MinimalDatabase.md` - Database design +- `apps/mobile/docs/` - Additional documentation diff --git a/apps/reader/apps/mobile/.mcp.json b/apps/reader/apps/mobile/.mcp.json new file mode 100644 index 000000000..bb7cd28b7 --- /dev/null +++ b/apps/reader/apps/mobile/.mcp.json @@ -0,0 +1,16 @@ +{ + "mcpServers": { + "supabase": { + "command": "npx", + "args": [ + "-y", + "@supabase/mcp-server-supabase@latest", + "--read-only", + "--project-ref=tiecnhktvovcqsrnunko" + ], + "env": { + "SUPABASE_ACCESS_TOKEN": "sbp_2faafc5ad01cb28bd9e9bc00bad8e9629e839c44" + } + } + } +} diff --git a/apps/reader/apps/mobile/CONTEXT_MENU_SOLUTION.md b/apps/reader/apps/mobile/CONTEXT_MENU_SOLUTION.md new file mode 100644 index 000000000..655eac0f0 --- /dev/null +++ b/apps/reader/apps/mobile/CONTEXT_MENU_SOLUTION.md @@ -0,0 +1,58 @@ +# Context Menu Solution + +I've fixed the "View config not found for component 'ContextMenu'" error by replacing the native `react-native-context-menu-view` with a cross-platform solution that works with Expo Go. + +## What was changed: + +1. **Removed the native dependency**: Uninstalled `react-native-context-menu-view` which requires native code and doesn't work with Expo Go. + +2. **Created two alternative solutions**: + +### Option 1: ActionMenu (Currently Active) +- Located in `/components/ActionMenu.tsx` +- Uses native ActionSheetIOS on iOS for a truly native experience +- Custom modal implementation for Android that slides up from bottom +- Triggered by long press on list items + +### Option 2: ContextMenu (Alternative) +- Located in `/components/ContextMenu.tsx` +- Custom modal-based context menu that appears near the pressed item +- Works on both platforms with consistent behavior +- More traditional context menu appearance + +## How it works: + +The ActionMenu component wraps your list items and provides a long-press gesture handler. When activated: +- On iOS: Shows native ActionSheetIOS +- On Android: Shows a custom bottom sheet modal + +## Usage: + +```tsx + { + // Handle selection + }}> + + +``` + +## Benefits: +- Works with Expo Go (no development build required) +- Native feel on iOS +- Consistent experience across platforms +- No native dependencies + +## If you need a development build: + +If you prefer to use the native context menu and are willing to use a development build: +1. Re-install `react-native-context-menu-view` +2. Run `npx expo prebuild` +3. Run `npx expo run:ios` or `npx expo run:android` + +The current solution allows you to continue using Expo Go for development while providing a good user experience. \ No newline at end of file diff --git a/apps/reader/apps/mobile/ReadMe/AudioPlayerImprovements.md b/apps/reader/apps/mobile/ReadMe/AudioPlayerImprovements.md new file mode 100644 index 000000000..9593d4315 --- /dev/null +++ b/apps/reader/apps/mobile/ReadMe/AudioPlayerImprovements.md @@ -0,0 +1,78 @@ +⏺ AudioPlayer Verbesserungsplan + +1. Geschwindigkeitssteuerung + +- Playback-Speed Kontrolle + (0.5x, 0.75x, 1x, 1.25x, 1.5x, + 2x) +- Geschwindigkeit bleibt für + alle Audios gespeichert +- Kleiner Button neben den + Controls + +2. Visuelle Verbesserungen + +- Waveform-Visualisierung + während der Wiedergabe +- Bessere Progress-Bar mit + Touch-Gesten zum Scrubben +- Pulsierender Play-Button + während des Ladens + +3. Erweiterte Navigation + +- Kapitel-Support (bei + längeren Texten) +- Lesezeichen setzen während + der Wiedergabe +- Zu bestimmten Zeitstempeln + springen + +4. Sleep Timer + +- Timer zum automatischen + Stoppen (15, 30, 45, 60 Min) +- Fade-Out am Ende +- Visual Countdown + +5. Kontinuierliche Wiedergabe + +- Automatisch nächsten Text + abspielen +- Queue-System für mehrere + Texte +- Shuffle-Modus + +6. Mini-Player + +- Kompakter Player am unteren + Bildschirmrand +- Bleibt beim Navigieren + sichtbar +- Swipe-to-dismiss + +7. Offline-Optimierung + +- Download-Button für lokale + Speicherung +- Download-Progress anzeigen +- Cache-Management UI + +8. Statistiken & History + +- Listening History anzeigen +- Fortschritt pro Text tracken +- Gesamte Hörzeit + +9. Accessibility + +- VoiceOver Support verbessern +- Größere Touch-Targets +- Keyboard-Shortcuts (iPad) + +10. Performance + +- Preloading des nächsten + Chunks +- Smooth Chunk-Übergänge +- Background Audio optimieren diff --git a/apps/reader/apps/mobile/ReadMe/ExpoUI.md b/apps/reader/apps/mobile/ReadMe/ExpoUI.md new file mode 100644 index 000000000..8d551bba9 --- /dev/null +++ b/apps/reader/apps/mobile/ReadMe/ExpoUI.md @@ -0,0 +1,226 @@ +Expo UI + +A set of components that allow you to build UIs directly with SwiftUI and Jetpack Compose from React. + +Bundled version: +~0.1.1-alpha.10 +This library is currently in alpha and will frequently experience breaking changes. It is not available in the Expo Go app – use development builds to try it out. +@expo/ui is a set of native input components that allows you to build fully native interfaces with SwiftUI and Jetpack Compose. It aims to provide the commonly used features and components that a typical app will need. + +Installation +Terminal + +Copy + +npx expo install @expo/ui +If you are installing this in an existing React Native app, make sure to install expo in your project. + +Swift UI examples +BottomSheet + +iOS + +Code + +BottomSheet component on iOS. +Button + +iOS + +Code + +Button component on iOS. +CircularProgress + +iOS + +Code + +CircularProgress component on iOS. +ColorPicker + +iOS + +Code + +ColorPicker component on iOS. +ContextMenu +Note: Also known as DropdownMenu. + +iOS + +Code + +ContextMenu component on iOS. +DateTimePicker (date) + +iOS + +Code + +DateTimePicker (date) component on iOS. +DateTimePicker (time) + +iOS + +Code + +DateTimePicker (time) component on iOS. +Gauge + +iOS + +Code + +Gauge component on iOS. +LinearProgress + +iOS + +Code + +LinearProgress component on iOS. +List + +iOS + +Code + +List component on iOS. +Picker (segmented) + +iOS + +Code + +Picker component on iOS. +Picker (wheel) + +iOS + +Code + +Picker component on iOS. +Slider + +iOS + +Code + +Slider component on iOS. +Switch (toggle) +Note: Also known as Toggle. + +iOS + +Code + +Switch component on iOS. +Switch (checkbox) + +iOS + +Code + +Picker component on iOS. +TextInput + +iOS + +Code + +TextInput component on iOS. +Jetpack Compose examples +Button + +Android + +Code + +Button component on Android. +CircularProgress + +Android + +Code + +CircularProgress component on Android. +ContextMenu +Note: Also known as DropdownMenu. + +Android + +Code + +ContextMenu component on Android. +DateTimePicker (date) + +Android + +Code + +DateTimePicker component on Android. +DateTimePicker (time) + +Android + +Code + +DateTimePicker (time) component on Android. +LinearProgress + +Android + +Code + +LinearProgress component on Android. +Picker (radio) + +Android + +Code + +Picker component (radio) on Android. +Picker (segmented) + +Android + +Code + +Picker component on Android. +Slider + +Android + +Code + +Slider component on Android. +Switch (toggle) +Note: Also known as Toggle. + +Android + +Code + +Switch component on Android. +Switch (checkbox) + +Android + +Code + +Switch (checkbox variant) component on Android. +TextInput + +Android + +Code + +TextInput component on Android. +API +Full documentation is not yet available. Use TypeScript types to explore the API. + +// Import from the SwiftUI package +import { BottomSheet } from '@expo/ui/swift-ui'; +// Import from the Jetpack Compose package +import { Button } from '@expo/ui/jetpack-compose'; diff --git a/apps/reader/apps/mobile/ReadMe/MinimalDatabase.md b/apps/reader/apps/mobile/ReadMe/MinimalDatabase.md new file mode 100644 index 000000000..a566b8c70 --- /dev/null +++ b/apps/reader/apps/mobile/ReadMe/MinimalDatabase.md @@ -0,0 +1,570 @@ +# 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 && ( +