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 && (
-
- )}
-
- {generating && }
-
- {hasCache && (
- <>
-
- Audio gespeichert: {(text.data.audio.totalSize / 1024 / 1024).toFixed(2)} MB
-
-
-
- );
-};
-```
-
-## 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.
\ No newline at end of file
diff --git a/apps-archived/reader/.gitignore b/apps/reader/.gitignore
similarity index 100%
rename from apps-archived/reader/.gitignore
rename to apps/reader/.gitignore
diff --git a/apps-archived/reader/CLAUDE.md b/apps/reader/CLAUDE.md
similarity index 100%
rename from apps-archived/reader/CLAUDE.md
rename to apps/reader/CLAUDE.md
diff --git a/apps-archived/reader/apps/mobile/CONTEXT_MENU_SOLUTION.md b/apps/reader/apps/mobile/CONTEXT_MENU_SOLUTION.md
similarity index 100%
rename from apps-archived/reader/apps/mobile/CONTEXT_MENU_SOLUTION.md
rename to apps/reader/apps/mobile/CONTEXT_MENU_SOLUTION.md
diff --git a/apps-archived/reader/apps/mobile/ReadMe/AudioPlayerImprovements.md b/apps/reader/apps/mobile/ReadMe/AudioPlayerImprovements.md
similarity index 100%
rename from apps-archived/reader/apps/mobile/ReadMe/AudioPlayerImprovements.md
rename to apps/reader/apps/mobile/ReadMe/AudioPlayerImprovements.md
diff --git a/apps-archived/reader/apps/mobile/ReadMe/ExpoUI.md b/apps/reader/apps/mobile/ReadMe/ExpoUI.md
similarity index 100%
rename from apps-archived/reader/apps/mobile/ReadMe/ExpoUI.md
rename to apps/reader/apps/mobile/ReadMe/ExpoUI.md
diff --git a/apps/reader/apps/mobile/ReadMe/MinimalDatabase.md b/apps/reader/apps/mobile/ReadMe/MinimalDatabase.md
new file mode 100644
index 000000000..f2f351a85
--- /dev/null
+++ b/apps/reader/apps/mobile/ReadMe/MinimalDatabase.md
@@ -0,0 +1,562 @@
+# 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 && (
+
+ )}
+
+ {generating && }
+
+ {hasCache && (
+ <>
+ Audio gespeichert: {(text.data.audio.totalSize / 1024 / 1024).toFixed(2)} MB
+
+
+ );
+};
+```
+
+## 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.
diff --git a/apps-archived/reader/apps/mobile/ReadMe/ProjectOverview.md b/apps/reader/apps/mobile/ReadMe/ProjectOverview.md
similarity index 100%
rename from apps-archived/reader/apps/mobile/ReadMe/ProjectOverview.md
rename to apps/reader/apps/mobile/ReadMe/ProjectOverview.md
diff --git a/apps-archived/reader/apps/mobile/app-env.d.ts b/apps/reader/apps/mobile/app-env.d.ts
similarity index 100%
rename from apps-archived/reader/apps/mobile/app-env.d.ts
rename to apps/reader/apps/mobile/app-env.d.ts
diff --git a/apps-archived/reader/apps/mobile/app.json b/apps/reader/apps/mobile/app.json
similarity index 100%
rename from apps-archived/reader/apps/mobile/app.json
rename to apps/reader/apps/mobile/app.json
diff --git a/apps-archived/reader/apps/mobile/app/(auth)/_layout.tsx b/apps/reader/apps/mobile/app/(auth)/_layout.tsx
similarity index 100%
rename from apps-archived/reader/apps/mobile/app/(auth)/_layout.tsx
rename to apps/reader/apps/mobile/app/(auth)/_layout.tsx
diff --git a/apps-archived/reader/apps/mobile/app/(auth)/forgot-password.tsx b/apps/reader/apps/mobile/app/(auth)/forgot-password.tsx
similarity index 100%
rename from apps-archived/reader/apps/mobile/app/(auth)/forgot-password.tsx
rename to apps/reader/apps/mobile/app/(auth)/forgot-password.tsx
diff --git a/apps-archived/reader/apps/mobile/app/(auth)/login.tsx b/apps/reader/apps/mobile/app/(auth)/login.tsx
similarity index 100%
rename from apps-archived/reader/apps/mobile/app/(auth)/login.tsx
rename to apps/reader/apps/mobile/app/(auth)/login.tsx
diff --git a/apps-archived/reader/apps/mobile/app/(auth)/register.tsx b/apps/reader/apps/mobile/app/(auth)/register.tsx
similarity index 100%
rename from apps-archived/reader/apps/mobile/app/(auth)/register.tsx
rename to apps/reader/apps/mobile/app/(auth)/register.tsx
diff --git a/apps-archived/reader/apps/mobile/app/(tabs)/_layout.tsx b/apps/reader/apps/mobile/app/(tabs)/_layout.tsx
similarity index 100%
rename from apps-archived/reader/apps/mobile/app/(tabs)/_layout.tsx
rename to apps/reader/apps/mobile/app/(tabs)/_layout.tsx
diff --git a/apps-archived/reader/apps/mobile/app/(tabs)/index.tsx b/apps/reader/apps/mobile/app/(tabs)/index.tsx
similarity index 100%
rename from apps-archived/reader/apps/mobile/app/(tabs)/index.tsx
rename to apps/reader/apps/mobile/app/(tabs)/index.tsx
diff --git a/apps-archived/reader/apps/mobile/app/(tabs)/two.tsx b/apps/reader/apps/mobile/app/(tabs)/two.tsx
similarity index 100%
rename from apps-archived/reader/apps/mobile/app/(tabs)/two.tsx
rename to apps/reader/apps/mobile/app/(tabs)/two.tsx
diff --git a/apps-archived/reader/apps/mobile/app/+html.tsx b/apps/reader/apps/mobile/app/+html.tsx
similarity index 100%
rename from apps-archived/reader/apps/mobile/app/+html.tsx
rename to apps/reader/apps/mobile/app/+html.tsx
diff --git a/apps-archived/reader/apps/mobile/app/+not-found.tsx b/apps/reader/apps/mobile/app/+not-found.tsx
similarity index 100%
rename from apps-archived/reader/apps/mobile/app/+not-found.tsx
rename to apps/reader/apps/mobile/app/+not-found.tsx
diff --git a/apps-archived/reader/apps/mobile/app/_layout.tsx b/apps/reader/apps/mobile/app/_layout.tsx
similarity index 100%
rename from apps-archived/reader/apps/mobile/app/_layout.tsx
rename to apps/reader/apps/mobile/app/_layout.tsx
diff --git a/apps-archived/reader/apps/mobile/app/add-text.tsx b/apps/reader/apps/mobile/app/add-text.tsx
similarity index 100%
rename from apps-archived/reader/apps/mobile/app/add-text.tsx
rename to apps/reader/apps/mobile/app/add-text.tsx
diff --git a/apps-archived/reader/apps/mobile/app/text/[id].tsx b/apps/reader/apps/mobile/app/text/[id].tsx
similarity index 100%
rename from apps-archived/reader/apps/mobile/app/text/[id].tsx
rename to apps/reader/apps/mobile/app/text/[id].tsx
diff --git a/apps-archived/reader/apps/mobile/assets/adaptive-icon.png b/apps/reader/apps/mobile/assets/adaptive-icon.png
similarity index 100%
rename from apps-archived/reader/apps/mobile/assets/adaptive-icon.png
rename to apps/reader/apps/mobile/assets/adaptive-icon.png
diff --git a/apps-archived/reader/apps/mobile/assets/favicon.png b/apps/reader/apps/mobile/assets/favicon.png
similarity index 100%
rename from apps-archived/reader/apps/mobile/assets/favicon.png
rename to apps/reader/apps/mobile/assets/favicon.png
diff --git a/apps-archived/reader/apps/mobile/assets/icon.png b/apps/reader/apps/mobile/assets/icon.png
similarity index 100%
rename from apps-archived/reader/apps/mobile/assets/icon.png
rename to apps/reader/apps/mobile/assets/icon.png
diff --git a/apps-archived/reader/apps/mobile/assets/splash.png b/apps/reader/apps/mobile/assets/splash.png
similarity index 100%
rename from apps-archived/reader/apps/mobile/assets/splash.png
rename to apps/reader/apps/mobile/assets/splash.png
diff --git a/apps-archived/reader/apps/mobile/babel.config.js b/apps/reader/apps/mobile/babel.config.js
similarity index 100%
rename from apps-archived/reader/apps/mobile/babel.config.js
rename to apps/reader/apps/mobile/babel.config.js
diff --git a/apps-archived/reader/apps/mobile/cesconfig.jsonc b/apps/reader/apps/mobile/cesconfig.jsonc
similarity index 100%
rename from apps-archived/reader/apps/mobile/cesconfig.jsonc
rename to apps/reader/apps/mobile/cesconfig.jsonc
diff --git a/apps-archived/reader/apps/mobile/components/ActionMenu.tsx b/apps/reader/apps/mobile/components/ActionMenu.tsx
similarity index 100%
rename from apps-archived/reader/apps/mobile/components/ActionMenu.tsx
rename to apps/reader/apps/mobile/components/ActionMenu.tsx
diff --git a/apps-archived/reader/apps/mobile/components/AudioPlayer.tsx b/apps/reader/apps/mobile/components/AudioPlayer.tsx
similarity index 100%
rename from apps-archived/reader/apps/mobile/components/AudioPlayer.tsx
rename to apps/reader/apps/mobile/components/AudioPlayer.tsx
diff --git a/apps-archived/reader/apps/mobile/components/Button.tsx b/apps/reader/apps/mobile/components/Button.tsx
similarity index 100%
rename from apps-archived/reader/apps/mobile/components/Button.tsx
rename to apps/reader/apps/mobile/components/Button.tsx
diff --git a/apps-archived/reader/apps/mobile/components/ContextMenu.tsx b/apps/reader/apps/mobile/components/ContextMenu.tsx
similarity index 100%
rename from apps-archived/reader/apps/mobile/components/ContextMenu.tsx
rename to apps/reader/apps/mobile/components/ContextMenu.tsx
diff --git a/apps-archived/reader/apps/mobile/components/EditScreenInfo.tsx b/apps/reader/apps/mobile/components/EditScreenInfo.tsx
similarity index 100%
rename from apps-archived/reader/apps/mobile/components/EditScreenInfo.tsx
rename to apps/reader/apps/mobile/components/EditScreenInfo.tsx
diff --git a/apps-archived/reader/apps/mobile/components/FloatingActionButton.tsx b/apps/reader/apps/mobile/components/FloatingActionButton.tsx
similarity index 100%
rename from apps-archived/reader/apps/mobile/components/FloatingActionButton.tsx
rename to apps/reader/apps/mobile/components/FloatingActionButton.tsx
diff --git a/apps-archived/reader/apps/mobile/components/Header.tsx b/apps/reader/apps/mobile/components/Header.tsx
similarity index 100%
rename from apps-archived/reader/apps/mobile/components/Header.tsx
rename to apps/reader/apps/mobile/components/Header.tsx
diff --git a/apps-archived/reader/apps/mobile/components/Icon.tsx b/apps/reader/apps/mobile/components/Icon.tsx
similarity index 100%
rename from apps-archived/reader/apps/mobile/components/Icon.tsx
rename to apps/reader/apps/mobile/components/Icon.tsx
diff --git a/apps-archived/reader/apps/mobile/components/MinimalAudioPlayer.tsx b/apps/reader/apps/mobile/components/MinimalAudioPlayer.tsx
similarity index 100%
rename from apps-archived/reader/apps/mobile/components/MinimalAudioPlayer.tsx
rename to apps/reader/apps/mobile/components/MinimalAudioPlayer.tsx
diff --git a/apps-archived/reader/apps/mobile/components/ScreenContent.tsx b/apps/reader/apps/mobile/components/ScreenContent.tsx
similarity index 100%
rename from apps-archived/reader/apps/mobile/components/ScreenContent.tsx
rename to apps/reader/apps/mobile/components/ScreenContent.tsx
diff --git a/apps-archived/reader/apps/mobile/components/TabBarIcon.tsx b/apps/reader/apps/mobile/components/TabBarIcon.tsx
similarity index 100%
rename from apps-archived/reader/apps/mobile/components/TabBarIcon.tsx
rename to apps/reader/apps/mobile/components/TabBarIcon.tsx
diff --git a/apps-archived/reader/apps/mobile/components/TagFilter.tsx b/apps/reader/apps/mobile/components/TagFilter.tsx
similarity index 100%
rename from apps-archived/reader/apps/mobile/components/TagFilter.tsx
rename to apps/reader/apps/mobile/components/TagFilter.tsx
diff --git a/apps-archived/reader/apps/mobile/components/Text.tsx b/apps/reader/apps/mobile/components/Text.tsx
similarity index 100%
rename from apps-archived/reader/apps/mobile/components/Text.tsx
rename to apps/reader/apps/mobile/components/Text.tsx
diff --git a/apps-archived/reader/apps/mobile/components/TextListItem.tsx b/apps/reader/apps/mobile/components/TextListItem.tsx
similarity index 100%
rename from apps-archived/reader/apps/mobile/components/TextListItem.tsx
rename to apps/reader/apps/mobile/components/TextListItem.tsx
diff --git a/apps-archived/reader/apps/mobile/components/dropdown.tsx b/apps/reader/apps/mobile/components/dropdown.tsx
similarity index 100%
rename from apps-archived/reader/apps/mobile/components/dropdown.tsx
rename to apps/reader/apps/mobile/components/dropdown.tsx
diff --git a/apps-archived/reader/apps/mobile/constants/voices.ts b/apps/reader/apps/mobile/constants/voices.ts
similarity index 100%
rename from apps-archived/reader/apps/mobile/constants/voices.ts
rename to apps/reader/apps/mobile/constants/voices.ts
diff --git a/apps-archived/reader/apps/mobile/docs/browser-extension-concept.md b/apps/reader/apps/mobile/docs/browser-extension-concept.md
similarity index 100%
rename from apps-archived/reader/apps/mobile/docs/browser-extension-concept.md
rename to apps/reader/apps/mobile/docs/browser-extension-concept.md
diff --git a/apps-archived/reader/apps/mobile/docs/deployment-guide.md b/apps/reader/apps/mobile/docs/deployment-guide.md
similarity index 100%
rename from apps-archived/reader/apps/mobile/docs/deployment-guide.md
rename to apps/reader/apps/mobile/docs/deployment-guide.md
diff --git a/apps-archived/reader/apps/mobile/docs/google-cloud-setup.md b/apps/reader/apps/mobile/docs/google-cloud-setup.md
similarity index 100%
rename from apps-archived/reader/apps/mobile/docs/google-cloud-setup.md
rename to apps/reader/apps/mobile/docs/google-cloud-setup.md
diff --git a/apps-archived/reader/apps/mobile/docs/url-extraction-options.md b/apps/reader/apps/mobile/docs/url-extraction-options.md
similarity index 100%
rename from apps-archived/reader/apps/mobile/docs/url-extraction-options.md
rename to apps/reader/apps/mobile/docs/url-extraction-options.md
diff --git a/apps-archived/reader/apps/mobile/eslint.config.js b/apps/reader/apps/mobile/eslint.config.js
similarity index 100%
rename from apps-archived/reader/apps/mobile/eslint.config.js
rename to apps/reader/apps/mobile/eslint.config.js
diff --git a/apps-archived/reader/apps/mobile/global.css b/apps/reader/apps/mobile/global.css
similarity index 100%
rename from apps-archived/reader/apps/mobile/global.css
rename to apps/reader/apps/mobile/global.css
diff --git a/apps-archived/reader/apps/mobile/hooks/useAudio.ts b/apps/reader/apps/mobile/hooks/useAudio.ts
similarity index 100%
rename from apps-archived/reader/apps/mobile/hooks/useAudio.ts
rename to apps/reader/apps/mobile/hooks/useAudio.ts
diff --git a/apps-archived/reader/apps/mobile/hooks/useAuth.ts b/apps/reader/apps/mobile/hooks/useAuth.ts
similarity index 100%
rename from apps-archived/reader/apps/mobile/hooks/useAuth.ts
rename to apps/reader/apps/mobile/hooks/useAuth.ts
diff --git a/apps-archived/reader/apps/mobile/hooks/useTexts.ts b/apps/reader/apps/mobile/hooks/useTexts.ts
similarity index 100%
rename from apps-archived/reader/apps/mobile/hooks/useTexts.ts
rename to apps/reader/apps/mobile/hooks/useTexts.ts
diff --git a/apps-archived/reader/apps/mobile/hooks/useTheme.ts b/apps/reader/apps/mobile/hooks/useTheme.ts
similarity index 100%
rename from apps-archived/reader/apps/mobile/hooks/useTheme.ts
rename to apps/reader/apps/mobile/hooks/useTheme.ts
diff --git a/apps-archived/reader/apps/mobile/metro.config.js b/apps/reader/apps/mobile/metro.config.js
similarity index 100%
rename from apps-archived/reader/apps/mobile/metro.config.js
rename to apps/reader/apps/mobile/metro.config.js
diff --git a/apps-archived/reader/apps/mobile/nativewind-env.d.ts b/apps/reader/apps/mobile/nativewind-env.d.ts
similarity index 100%
rename from apps-archived/reader/apps/mobile/nativewind-env.d.ts
rename to apps/reader/apps/mobile/nativewind-env.d.ts
diff --git a/apps-archived/reader/apps/mobile/package.json b/apps/reader/apps/mobile/package.json
similarity index 100%
rename from apps-archived/reader/apps/mobile/package.json
rename to apps/reader/apps/mobile/package.json
diff --git a/apps-archived/reader/apps/mobile/prettier.config.js b/apps/reader/apps/mobile/prettier.config.js
similarity index 100%
rename from apps-archived/reader/apps/mobile/prettier.config.js
rename to apps/reader/apps/mobile/prettier.config.js
diff --git a/apps-archived/reader/apps/mobile/services/audioService.ts b/apps/reader/apps/mobile/services/audioService.ts
similarity index 100%
rename from apps-archived/reader/apps/mobile/services/audioService.ts
rename to apps/reader/apps/mobile/services/audioService.ts
diff --git a/apps-archived/reader/apps/mobile/services/urlExtractorService.ts b/apps/reader/apps/mobile/services/urlExtractorService.ts
similarity index 100%
rename from apps-archived/reader/apps/mobile/services/urlExtractorService.ts
rename to apps/reader/apps/mobile/services/urlExtractorService.ts
diff --git a/apps-archived/reader/apps/mobile/store/store.ts b/apps/reader/apps/mobile/store/store.ts
similarity index 100%
rename from apps-archived/reader/apps/mobile/store/store.ts
rename to apps/reader/apps/mobile/store/store.ts
diff --git a/apps-archived/reader/apps/mobile/supabase/.temp/cli-latest b/apps/reader/apps/mobile/supabase/.temp/cli-latest
similarity index 100%
rename from apps-archived/reader/apps/mobile/supabase/.temp/cli-latest
rename to apps/reader/apps/mobile/supabase/.temp/cli-latest
diff --git a/apps-archived/reader/apps/mobile/supabase/.temp/gotrue-version b/apps/reader/apps/mobile/supabase/.temp/gotrue-version
similarity index 100%
rename from apps-archived/reader/apps/mobile/supabase/.temp/gotrue-version
rename to apps/reader/apps/mobile/supabase/.temp/gotrue-version
diff --git a/apps-archived/reader/apps/mobile/supabase/.temp/pooler-url b/apps/reader/apps/mobile/supabase/.temp/pooler-url
similarity index 100%
rename from apps-archived/reader/apps/mobile/supabase/.temp/pooler-url
rename to apps/reader/apps/mobile/supabase/.temp/pooler-url
diff --git a/apps-archived/reader/apps/mobile/supabase/.temp/postgres-version b/apps/reader/apps/mobile/supabase/.temp/postgres-version
similarity index 100%
rename from apps-archived/reader/apps/mobile/supabase/.temp/postgres-version
rename to apps/reader/apps/mobile/supabase/.temp/postgres-version
diff --git a/apps-archived/reader/apps/mobile/supabase/.temp/project-ref b/apps/reader/apps/mobile/supabase/.temp/project-ref
similarity index 100%
rename from apps-archived/reader/apps/mobile/supabase/.temp/project-ref
rename to apps/reader/apps/mobile/supabase/.temp/project-ref
diff --git a/apps-archived/reader/apps/mobile/supabase/.temp/rest-version b/apps/reader/apps/mobile/supabase/.temp/rest-version
similarity index 100%
rename from apps-archived/reader/apps/mobile/supabase/.temp/rest-version
rename to apps/reader/apps/mobile/supabase/.temp/rest-version
diff --git a/apps-archived/reader/apps/mobile/supabase/.temp/storage-version b/apps/reader/apps/mobile/supabase/.temp/storage-version
similarity index 100%
rename from apps-archived/reader/apps/mobile/supabase/.temp/storage-version
rename to apps/reader/apps/mobile/supabase/.temp/storage-version
diff --git a/apps-archived/reader/apps/mobile/supabase/functions/extract-url-scrapingbee/index.ts b/apps/reader/apps/mobile/supabase/functions/extract-url-scrapingbee/index.ts
similarity index 100%
rename from apps-archived/reader/apps/mobile/supabase/functions/extract-url-scrapingbee/index.ts
rename to apps/reader/apps/mobile/supabase/functions/extract-url-scrapingbee/index.ts
diff --git a/apps-archived/reader/apps/mobile/supabase/functions/extract-url/index.ts b/apps/reader/apps/mobile/supabase/functions/extract-url/index.ts
similarity index 100%
rename from apps-archived/reader/apps/mobile/supabase/functions/extract-url/index.ts
rename to apps/reader/apps/mobile/supabase/functions/extract-url/index.ts
diff --git a/apps-archived/reader/apps/mobile/supabase/functions/generate-audio/index.ts b/apps/reader/apps/mobile/supabase/functions/generate-audio/index.ts
similarity index 100%
rename from apps-archived/reader/apps/mobile/supabase/functions/generate-audio/index.ts
rename to apps/reader/apps/mobile/supabase/functions/generate-audio/index.ts
diff --git a/apps-archived/reader/apps/mobile/supabase/functions/get-audio-url/index.ts b/apps/reader/apps/mobile/supabase/functions/get-audio-url/index.ts
similarity index 100%
rename from apps-archived/reader/apps/mobile/supabase/functions/get-audio-url/index.ts
rename to apps/reader/apps/mobile/supabase/functions/get-audio-url/index.ts
diff --git a/apps-archived/reader/apps/mobile/supabase/migrations/20240116_create_texts_table.sql b/apps/reader/apps/mobile/supabase/migrations/20240116_create_texts_table.sql
similarity index 100%
rename from apps-archived/reader/apps/mobile/supabase/migrations/20240116_create_texts_table.sql
rename to apps/reader/apps/mobile/supabase/migrations/20240116_create_texts_table.sql
diff --git a/apps-archived/reader/apps/mobile/supabase/migrations/20240117_create_audio_storage.sql b/apps/reader/apps/mobile/supabase/migrations/20240117_create_audio_storage.sql
similarity index 100%
rename from apps-archived/reader/apps/mobile/supabase/migrations/20240117_create_audio_storage.sql
rename to apps/reader/apps/mobile/supabase/migrations/20240117_create_audio_storage.sql
diff --git a/apps-archived/reader/apps/mobile/tailwind.config.js b/apps/reader/apps/mobile/tailwind.config.js
similarity index 100%
rename from apps-archived/reader/apps/mobile/tailwind.config.js
rename to apps/reader/apps/mobile/tailwind.config.js
diff --git a/apps-archived/reader/apps/mobile/tsconfig.json b/apps/reader/apps/mobile/tsconfig.json
similarity index 100%
rename from apps-archived/reader/apps/mobile/tsconfig.json
rename to apps/reader/apps/mobile/tsconfig.json
diff --git a/apps-archived/reader/apps/mobile/types/database.ts b/apps/reader/apps/mobile/types/database.ts
similarity index 100%
rename from apps-archived/reader/apps/mobile/types/database.ts
rename to apps/reader/apps/mobile/types/database.ts
diff --git a/apps-archived/reader/apps/mobile/utils/audioMigration.ts b/apps/reader/apps/mobile/utils/audioMigration.ts
similarity index 100%
rename from apps-archived/reader/apps/mobile/utils/audioMigration.ts
rename to apps/reader/apps/mobile/utils/audioMigration.ts
diff --git a/apps-archived/reader/apps/mobile/utils/storage.ts b/apps/reader/apps/mobile/utils/storage.ts
similarity index 100%
rename from apps-archived/reader/apps/mobile/utils/storage.ts
rename to apps/reader/apps/mobile/utils/storage.ts
diff --git a/apps-archived/reader/apps/mobile/utils/supabase.ts b/apps/reader/apps/mobile/utils/supabase.ts
similarity index 100%
rename from apps-archived/reader/apps/mobile/utils/supabase.ts
rename to apps/reader/apps/mobile/utils/supabase.ts
diff --git a/apps/reader/package.json b/apps/reader/package.json
new file mode 100644
index 000000000..96f5be1c2
--- /dev/null
+++ b/apps/reader/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "@manacore/reader",
+ "version": "0.0.1",
+ "private": true,
+ "scripts": {
+ "dev": "turbo run dev"
+ }
+}
diff --git a/packages/shared-branding/src/app-icons.ts b/packages/shared-branding/src/app-icons.ts
index eb1786ed0..3d649282b 100644
--- a/packages/shared-branding/src/app-icons.ts
+++ b/packages/shared-branding/src/app-icons.ts
@@ -117,6 +117,9 @@ export const APP_ICONS = {
uload: svgToDataUrl(
``
),
+ reader: svgToDataUrl(
+ ``
+ ),
news: svgToDataUrl(
``
),
diff --git a/packages/shared-branding/src/mana-apps.ts b/packages/shared-branding/src/mana-apps.ts
index 9d8977849..fd6cebc5f 100644
--- a/packages/shared-branding/src/mana-apps.ts
+++ b/packages/shared-branding/src/mana-apps.ts
@@ -388,6 +388,22 @@ export const MANA_APPS: ManaApp[] = [
comingSoon: false,
status: 'development',
},
+ {
+ id: 'reader',
+ name: 'Reader',
+ description: {
+ de: 'Text-to-Speech mit Offline-Audio',
+ en: 'Text-to-Speech with Offline Audio',
+ },
+ longDescription: {
+ de: 'Texte in hochwertige Sprache umwandeln und offline anhören.',
+ en: 'Convert text to high-quality speech and listen offline.',
+ },
+ icon: APP_ICONS.reader,
+ color: '#f97316',
+ comingSoon: false,
+ status: 'development',
+ },
{
id: 'news',
name: 'News Hub',
@@ -513,6 +529,7 @@ export const APP_URLS: Record = {
citycorners: { dev: 'http://localhost:5196', prod: 'https://citycorners.mana.how' },
taktik: { dev: 'http://localhost:5197', prod: 'https://taktik.mana.how' },
uload: { dev: 'http://localhost:5173', prod: 'https://ulo.ad' },
+ reader: { dev: 'exp://localhost:8081', prod: 'https://reader.mana.how' },
news: { dev: 'http://localhost:5174', prod: 'https://news.mana.how' },
calc: { dev: 'http://localhost:5198', prod: 'https://calc.mana.how' },
};