mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-22 14:46:41 +02:00
chore: archive inactive projects to apps-archived/
Move inactive projects out of active workspace: - bauntown (community website) - maerchenzauber (AI story generation) - memoro (voice memo app) - news (news aggregation) - nutriphi (nutrition tracking) - reader (reading app) - uload (URL shortener) - wisekeep (AI wisdom extraction) Update CLAUDE.md documentation: - Add presi to active projects - Document archived projects section - Update workspace configuration Archived apps can be re-activated by moving back to apps/ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
b97149ac12
commit
61d181fbc2
3148 changed files with 437 additions and 46640 deletions
|
|
@ -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
|
||||
226
apps-archived/reader/apps/mobile/ReadMe/ExpoUI.md
Normal file
226
apps-archived/reader/apps/mobile/ReadMe/ExpoUI.md
Normal file
|
|
@ -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';
|
||||
570
apps-archived/reader/apps/mobile/ReadMe/MinimalDatabase.md
Normal file
570
apps-archived/reader/apps/mobile/ReadMe/MinimalDatabase.md
Normal file
|
|
@ -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 (
|
||||
<View>
|
||||
<Text>{text.title}</Text>
|
||||
|
||||
{!hasCache && (
|
||||
<Button
|
||||
title="Audio generieren & speichern"
|
||||
onPress={handleGenerateAudio}
|
||||
disabled={generating}
|
||||
/>
|
||||
)}
|
||||
|
||||
{generating && <ActivityIndicator />}
|
||||
|
||||
{hasCache && (
|
||||
<>
|
||||
<Text>
|
||||
Audio gespeichert: {(text.data.audio.totalSize / 1024 / 1024).toFixed(2)} MB
|
||||
</Text>
|
||||
<Button title="Offline abspielen" onPress={handlePlay} />
|
||||
<Button title="Cache löschen" onPress={() => clearAudioCache(text.id)} />
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Vorteile dieser Struktur
|
||||
|
||||
✅ **Eine Tabelle** = Keine Joins, keine Komplexität
|
||||
✅ **JSONB** = Unendlich erweiterbar ohne Migrations
|
||||
✅ **Performance** = PostgreSQL's JSONB ist super schnell
|
||||
✅ **Einfach** = Jeder versteht es sofort
|
||||
✅ **Flexibel** = Neue Features sind nur ein JSON-Feld entfernt
|
||||
|
||||
## Erweiterungsbeispiele
|
||||
|
||||
```javascript
|
||||
// Später: Lesezeichen hinzufügen
|
||||
data.bookmarks = [
|
||||
{ position: 1234, note: "Wichtige Stelle", created: "2024-01-15" }
|
||||
];
|
||||
|
||||
// Später: Sharing hinzufügen
|
||||
data.sharing = {
|
||||
isPublic: false,
|
||||
shareToken: "abc123",
|
||||
sharedWith: ["email@example.com"]
|
||||
};
|
||||
|
||||
// Später: AI-Features
|
||||
data.ai = {
|
||||
summary: "KI-generierte Zusammenfassung",
|
||||
keywords: ["Thema1", "Thema2"],
|
||||
difficulty: "medium"
|
||||
};
|
||||
```
|
||||
|
||||
## Das war's! 🎉
|
||||
|
||||
Mit dieser einen Tabelle kannst du:
|
||||
- Texte speichern ✓
|
||||
- Vorlesen mit gespeicherter Position ✓
|
||||
- Tags/Kategorien verwalten ✓
|
||||
- Statistiken tracken ✓
|
||||
- Beliebig erweitern ✓
|
||||
|
||||
Keine zweite Tabelle nötig. Kein Over-Engineering. Einfach machen.
|
||||
217
apps-archived/reader/apps/mobile/ReadMe/ProjectOverview.md
Normal file
217
apps-archived/reader/apps/mobile/ReadMe/ProjectOverview.md
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
# Reader - Projektübersicht
|
||||
|
||||
## Was ist Reader?
|
||||
|
||||
Reader ist eine moderne Text-to-Speech App, die Texte mit hochqualitativen KI-Stimmen vorliest und für die Offline-Nutzung speichert. Die App kombiniert die neuesten Google Chirp Stimmen mit einer eleganten Benutzeroberfläche und intelligenter Audio-Verwaltung.
|
||||
|
||||
## Kernfunktionen
|
||||
|
||||
### 📚 Text-Management
|
||||
|
||||
- **Import**: Texte manuell eingeben oder aus Dateien importieren
|
||||
- **Organisation**: Einfache Tag-basierte Verwaltung
|
||||
- **Synchronisation**: Automatischer Sync zwischen Geräten via Supabase
|
||||
- **Lesefortschritt**: Merkt sich wo du aufgehört hast
|
||||
|
||||
### 🎧 Premium Audio-Wiedergabe
|
||||
|
||||
- **Google Chirp Stimmen**: Natürlich klingende KI-Stimmen in Studio-Qualität
|
||||
- **Offline-Verfügbarkeit**: Einmal generiert, immer verfügbar
|
||||
- **Anpassbar**: Geschwindigkeit und Tonhöhe individuell einstellbar
|
||||
- **Nahtlose Wiedergabe**: Intelligentes Chunk-System für unterbrechungsfreies Hören
|
||||
|
||||
### 💾 Smart Caching
|
||||
|
||||
- **Automatische Segmentierung**: Lange Texte werden intelligent aufgeteilt
|
||||
- **Progressives Laden**: Chunks werden bei Bedarf geladen
|
||||
- **Speicherverwaltung**: Übersicht über genutzten Speicherplatz
|
||||
- **Selective Sync**: Wähle welche Texte offline verfügbar sein sollen
|
||||
|
||||
### 👤 Benutzerfreundlichkeit
|
||||
|
||||
- **Ein-Klick-Generierung**: Audio für komplette Texte erstellen
|
||||
- **Hintergrund-Wiedergabe**: Weiterhören während andere Apps genutzt werden
|
||||
- **Sleep Timer**: Automatisches Stoppen nach eingestellter Zeit
|
||||
- **Lesezeichen**: Wichtige Stellen markieren
|
||||
|
||||
## Technische Architektur
|
||||
|
||||
### Frontend: React Native mit Expo
|
||||
|
||||
- **Plattformen**: iOS und Android aus einer Codebasis
|
||||
- **UI Framework**: Native Komponenten für beste Performance
|
||||
- **Offline-First**: Funktioniert auch ohne Internetverbindung
|
||||
- **State Management**: React Context für einfache Datenverwaltung
|
||||
|
||||
### Backend: Supabase
|
||||
|
||||
- **Datenbank**: PostgreSQL mit einer minimalistischen Tabelle
|
||||
- **Authentifizierung**: Sichere Benutzerkonten out-of-the-box
|
||||
- **Realtime Sync**: Änderungen werden sofort synchronisiert
|
||||
- **Edge Functions**: Serverless Audio-Generierung
|
||||
|
||||
### Audio-Pipeline: Google Cloud TTS
|
||||
|
||||
- **Chirp Voices**: Neueste Generation von Google's Text-to-Speech
|
||||
- **Studio-Qualität**: Broadcast-taugliche Sprachausgabe
|
||||
- **Mehrsprachig**: Unterstützung für 40+ Sprachen
|
||||
- **Neural Synthesis**: KI-basierte Sprachgenerierung
|
||||
|
||||
## Projektkonzept für Google Chirp Integration
|
||||
|
||||
### Phase 1: Infrastruktur-Setup
|
||||
|
||||
**Ziel**: Grundlegende Verbindungen zwischen allen Systemen herstellen
|
||||
|
||||
**Google Cloud Konfiguration**:
|
||||
|
||||
- Google Cloud Projekt erstellen und Text-to-Speech API aktivieren
|
||||
- Service Account für sichere API-Zugriffe einrichten
|
||||
- Zugriffsschlüssel generieren und sicher speichern
|
||||
- Kostenkontrolle durch Quotas und Budgets einrichten
|
||||
|
||||
**Supabase Edge Functions Setup**:
|
||||
|
||||
- Zwei Hauptfunktionen: Audio-Generierung und Batch-Processing
|
||||
- Sichere Speicherung der Google Cloud Credentials
|
||||
- CORS-Konfiguration für App-Zugriffe
|
||||
- Error Handling und Logging-Strategie
|
||||
|
||||
### Phase 2: Audio-Generierungs-Pipeline
|
||||
|
||||
**Ziel**: Robuste und skalierbare Audio-Erstellung
|
||||
|
||||
**Text-Segmentierung**:
|
||||
|
||||
- Intelligente Aufteilung an Satzgrenzen
|
||||
- Optimale Chunk-Größe für Balance zwischen Qualität und Performance
|
||||
- Metadaten für nahtlose Wiedergabe speichern
|
||||
|
||||
**Batch-Processing**:
|
||||
|
||||
- Parallele Verarbeitung mit Rate Limiting
|
||||
- Fortschrittsanzeige für Benutzer
|
||||
- Fehlerbehandlung für einzelne Chunks
|
||||
- Automatische Wiederholung bei Fehlern
|
||||
|
||||
**Storage-Strategie**:
|
||||
|
||||
- Supabase Storage für zentrale Audio-Dateien
|
||||
- Signierte URLs mit Ablaufzeit
|
||||
- Lokaler Cache auf Geräten
|
||||
- Intelligente Garbage Collection
|
||||
|
||||
### Phase 3: App-Integration
|
||||
|
||||
**Ziel**: Nahtlose Benutzererfahrung
|
||||
|
||||
**Audio-Service Layer**:
|
||||
|
||||
- Abstraktion der Komplexität
|
||||
- Queue-Management für Wiedergabe
|
||||
- Prefetching für unterbrechungsfreies Hören
|
||||
- Fallback-Mechanismen
|
||||
|
||||
**UI/UX Konzepte**:
|
||||
|
||||
- Ein-Tap Audio-Generierung
|
||||
- Visuelles Feedback während Processing
|
||||
- Download-Progress für Offline-Sync
|
||||
- Intuitive Playback-Controls
|
||||
|
||||
### Phase 4: Optimierung & Skalierung
|
||||
|
||||
**Ziel**: Production-ready System
|
||||
|
||||
**Performance**:
|
||||
|
||||
- CDN-Integration für schnelle Downloads
|
||||
- Chunk-Größen-Optimierung
|
||||
- Parallele Downloads
|
||||
- Background Processing
|
||||
|
||||
**Kosten-Optimierung**:
|
||||
|
||||
- Caching bereits generierter Audios
|
||||
- Deduplizierung gleicher Textpassagen
|
||||
- Nutzungsbasierte Limits
|
||||
- Premium-Tier für Heavy Users
|
||||
|
||||
**Monitoring**:
|
||||
|
||||
- Verwendungsstatistiken
|
||||
- Error Tracking
|
||||
- Performance Metriken
|
||||
- Kosten-Überwachung
|
||||
|
||||
## Alleinstellungsmerkmale
|
||||
|
||||
### 🎯 Was Reader besonders macht:
|
||||
|
||||
1. **Höchste Audioqualität**: Google Chirp Stimmen klingen natürlicher als Standard TTS
|
||||
2. **True Offline**: Einmal generiert, für immer verfügbar - kein Streaming nötig
|
||||
3. **Minimalistisches Design**: Fokus auf das Wesentliche ohne überflüssige Features
|
||||
4. **Privacy-First**: Deine Texte bleiben deine Texte
|
||||
5. **Fair Pricing**: Einmalige Generierung statt ständige Streaming-Kosten
|
||||
|
||||
## Monetarisierung
|
||||
|
||||
### Freemium Modell:
|
||||
|
||||
- **Free Tier**: 10.000 Zeichen/Monat
|
||||
- **Pro**: 500.000 Zeichen/Monat + Premium Stimmen
|
||||
- **Team**: Unbegrenzt + Collaboration Features
|
||||
|
||||
### Kostenstruktur:
|
||||
|
||||
- Google TTS: ~$16 per 1 Million Zeichen (Chirp Voices)
|
||||
- Supabase: $25/Monat für Pro Features
|
||||
- Storage: $0.021 per GB/Monat
|
||||
|
||||
## Zeitplan
|
||||
|
||||
**Woche 1-2**: Setup & Basis-Integration
|
||||
|
||||
- Google Cloud und Supabase konfigurieren
|
||||
- Edge Functions entwickeln
|
||||
- Basis-App mit Authentifizierung
|
||||
|
||||
**Woche 3-4**: Audio-Pipeline
|
||||
|
||||
- Chunk-System implementieren
|
||||
- Storage-Integration
|
||||
- Playback-Funktionalität
|
||||
|
||||
**Woche 5-6**: Polish & Launch
|
||||
|
||||
- UI/UX Verfeinerung
|
||||
- Testing & Bugfixing
|
||||
- App Store Vorbereitung
|
||||
|
||||
## Erfolgsmetriken
|
||||
|
||||
- **Nutzer-Aktivierung**: 80% generieren ersten Audio innerhalb 5 Minuten
|
||||
- **Retention**: 40% Daily Active Users
|
||||
- **Audio-Qualität**: <2% Neu-Generierungen wegen Qualität
|
||||
- **Performance**: <3 Sekunden für Start der Wiedergabe
|
||||
- **Conversion**: 5% Free-to-Pro nach 30 Tagen
|
||||
|
||||
## Risiken & Mitigationen
|
||||
|
||||
**API-Kosten**:
|
||||
|
||||
- Monitoring und Alerts
|
||||
- Caching-Strategien
|
||||
- User Limits
|
||||
|
||||
**Technische Komplexität**:
|
||||
|
||||
- Schrittweise Integration
|
||||
- Ausführliches Testing
|
||||
- Fallback-Optionen
|
||||
|
||||
**Skalierung**:
|
||||
|
||||
- Edge Function Limits beachten
|
||||
- CDN frühzeitig einplanen
|
||||
- Horizontale Skalierung vorbereiten
|
||||
Loading…
Add table
Add a link
Reference in a new issue