a242693751
- XTTS v2: Docker setup for Gaming-PC (GPU), bridge via RVS relay - XTTS: Voice cloning UI in Diagnostic (multi-file upload) - XTTS: Engine selectable (Piper local vs XTTS remote) with fallback - Auto-Update: RVS serves APK over WebSocket (no HTTP needed) - Auto-Update: App checks version on start, prompts install - Auto-Update: release.sh copies APK to RVS via scp - Bridge: TTS engine abstraction (piper/xtts), config persistent - Bridge: xtts_response handler, tts_request on-demand - Diagnostic: TTS engine dropdown, XTTS voice panel, voice cloning - App: Play button on ARIA messages, chat search, update service - Wake word: Disabled LiveAudioStream (crash fix), Phase 1 placeholder - Watchdog: Container restart after 8min stuck - Chat backup: on-the-fly to /shared/config/chat_backup.jsonl Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
150 lines
4.8 KiB
TypeScript
150 lines
4.8 KiB
TypeScript
/**
|
|
* Auto-Update Service — prueft und installiert App-Updates via RVS
|
|
*
|
|
* Flow:
|
|
* 1. App sendet "update_check" mit aktueller Version an RVS
|
|
* 2. RVS vergleicht → sendet "update_available" mit Download-URL
|
|
* 3. App zeigt Benachrichtigung → User bestaetigt → Download + Install
|
|
*/
|
|
|
|
import { Alert, Linking, Platform } from 'react-native';
|
|
import RNFS from 'react-native-fs';
|
|
import rvs, { RVSMessage } from './rvs';
|
|
|
|
// Aktuelle App-Version (aus package.json via Build)
|
|
const APP_VERSION = '0.0.2.3'; // TODO: aus nativer Build-Config lesen
|
|
|
|
type UpdateCallback = (info: UpdateInfo) => void;
|
|
|
|
export interface UpdateInfo {
|
|
version: string;
|
|
downloadUrl: string;
|
|
size: number;
|
|
}
|
|
|
|
class UpdateService {
|
|
private listeners: UpdateCallback[] = [];
|
|
private checking = false;
|
|
private downloading = false;
|
|
|
|
constructor() {
|
|
// Auf update_available Nachrichten lauschen
|
|
rvs.onMessage((msg: RVSMessage) => {
|
|
if (msg.type === 'update_available' as any) {
|
|
const info: UpdateInfo = {
|
|
version: (msg.payload.version as string) || '',
|
|
downloadUrl: (msg.payload.downloadUrl as string) || '',
|
|
size: (msg.payload.size as number) || 0,
|
|
};
|
|
if (info.version && this.isNewer(info.version)) {
|
|
console.log(`[Update] Neue Version verfuegbar: ${info.version} (aktuell: ${APP_VERSION})`);
|
|
this.listeners.forEach(cb => cb(info));
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/** Bei App-Start Update pruefen */
|
|
checkForUpdate(): void {
|
|
if (this.checking) return;
|
|
this.checking = true;
|
|
|
|
console.log(`[Update] Pruefe auf Updates (aktuell: ${APP_VERSION})`);
|
|
rvs.send('update_check' as any, { version: APP_VERSION });
|
|
|
|
setTimeout(() => { this.checking = false; }, 10000);
|
|
}
|
|
|
|
/** Callback registrieren */
|
|
onUpdateAvailable(callback: UpdateCallback): () => void {
|
|
this.listeners.push(callback);
|
|
return () => {
|
|
this.listeners = this.listeners.filter(cb => cb !== callback);
|
|
};
|
|
}
|
|
|
|
/** Update-Dialog anzeigen */
|
|
promptUpdate(info: UpdateInfo): void {
|
|
const sizeMB = (info.size / 1024 / 1024).toFixed(1);
|
|
Alert.alert(
|
|
'ARIA Update verfuegbar',
|
|
`Version ${info.version} (${sizeMB} MB)\n\nAktuell: ${APP_VERSION}\n\nJetzt herunterladen und installieren?`,
|
|
[
|
|
{ text: 'Spaeter', style: 'cancel' },
|
|
{
|
|
text: 'Installieren',
|
|
onPress: () => this.downloadAndInstall(info),
|
|
},
|
|
],
|
|
);
|
|
}
|
|
|
|
/** APK ueber WebSocket herunterladen und installieren */
|
|
async downloadAndInstall(info: UpdateInfo): Promise<void> {
|
|
if (this.downloading) return;
|
|
this.downloading = true;
|
|
|
|
try {
|
|
console.log(`[Update] Fordere APK v${info.version} an...`);
|
|
Alert.alert('Download gestartet', `Version ${info.version} wird ueber RVS heruntergeladen...`);
|
|
|
|
// APK ueber WebSocket anfordern
|
|
rvs.send('update_download' as any, {});
|
|
|
|
// Auf update_data warten (einmalig)
|
|
const apkData = await new Promise<{base64: string, fileName: string}>((resolve, reject) => {
|
|
const timeout = setTimeout(() => reject(new Error('Download-Timeout (60s)')), 60000);
|
|
const unsub = rvs.onMessage((msg: RVSMessage) => {
|
|
if ((msg.type as string) === 'update_data') {
|
|
clearTimeout(timeout);
|
|
unsub();
|
|
if (msg.payload.error) {
|
|
reject(new Error(msg.payload.error as string));
|
|
} else {
|
|
resolve({
|
|
base64: msg.payload.base64 as string,
|
|
fileName: msg.payload.fileName as string || `ARIA-${info.version}.apk`,
|
|
});
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// Base64 als APK-Datei speichern
|
|
const destPath = `${RNFS.CachesDirectoryPath}/${apkData.fileName}`;
|
|
await RNFS.writeFile(destPath, apkData.base64, 'base64');
|
|
const fileSize = await RNFS.stat(destPath);
|
|
console.log(`[Update] APK gespeichert: ${destPath} (${(parseInt(fileSize.size) / 1024 / 1024).toFixed(1)}MB)`);
|
|
|
|
// APK installieren (oeffnet Android-Installer)
|
|
if (Platform.OS === 'android') {
|
|
await Linking.openURL(`file://${destPath}`);
|
|
}
|
|
} catch (err: any) {
|
|
console.error(`[Update] Fehler: ${err.message}`);
|
|
Alert.alert('Update fehlgeschlagen', err.message);
|
|
} finally {
|
|
this.downloading = false;
|
|
}
|
|
}
|
|
|
|
/** Versionsvergleich */
|
|
private isNewer(remote: string): boolean {
|
|
const r = remote.split('.').map(Number);
|
|
const l = APP_VERSION.split('.').map(Number);
|
|
for (let i = 0; i < Math.max(r.length, l.length); i++) {
|
|
const diff = (r[i] || 0) - (l[i] || 0);
|
|
if (diff > 0) return true;
|
|
if (diff < 0) return false;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
getCurrentVersion(): string {
|
|
return APP_VERSION;
|
|
}
|
|
}
|
|
|
|
const updateService = new UpdateService();
|
|
export default updateService;
|