From 20e623dc3743f91805f726b84e2d2d14165b37d7 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Tue, 2 Jun 2026 13:50:35 +0200 Subject: [PATCH] feat(app): Versions-Historie pro Datei im App-Datei-Manager MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step 3 vom File-Versioning-Feature (Step 1+2 lief schon in Diagnostic). App kann jetzt: - pro Datei via 🕒-Button die Versions-Liste anzeigen - alte Versionen als '@.' nach Downloads schreiben - per ⟲ eine alte Version als neue aktive setzen (non-destructive, macht im Backend einen 'restore:'-Commit, die bisherige Version bleibt in der Historie) Drei neue RVS-Message-Type-Paare: file_version_list_request / _response file_version_download_request / _response (base64) file_version_restore_request / _response rvs/server.js: alle sechs Typen in die ALLOWED_TYPES-Whitelist. bridge/aria_bridge.py: handler proxen die Anfragen an diagnostic (http://localhost:3001/api/files-versions / -version-content / -restore). Diagnostic ist eh schon der Owner der git-Repository-Logik. Bridge wrappt die Binary-Antwort als base64 fuer den RVS-Transport. android/src/screens/SettingsScreen.tsx: - State versionsOpen/versionsList/-Loading/-Error - Drei rvs.onMessage-Branches fuer die neuen *_response Types - 🕒-Button in jeder Datei-Zeile (zwischen Auswahl-Checkbox und Mülltonne) - Neues Modal mit Versions-Liste (AKTIV-Badge, short-hash, subject, formatiertes Datum, ⬇ Download + ⟲ Restore-Button pro Eintrag) - Restore-Button hat Confirm-Alert - Bei file_version_restore_response: list refresh + file-list refresh --- android/src/screens/SettingsScreen.tsx | 194 +++++++++++++++++++++++++ bridge/aria_bridge.py | 123 ++++++++++++++++ rvs/server.js | 5 + 3 files changed, 322 insertions(+) diff --git a/android/src/screens/SettingsScreen.tsx b/android/src/screens/SettingsScreen.tsx index 4e1774e..7fdf66e 100644 --- a/android/src/screens/SettingsScreen.tsx +++ b/android/src/screens/SettingsScreen.tsx @@ -180,6 +180,14 @@ const SettingsScreen: React.FC = () => { const [fileManagerSelected, setFileManagerSelected] = useState>(new Set()); const fileZipPending = useRef(null); // requestId fuer ZIP-Antwort const [fileZipBusy, setFileZipBusy] = useState(false); + // Versions-Modal — pro Datei eine kleine Historie aus dem auto-commit-git + // im diagnostic-Container. Browser-Variante davon laeuft schon, hier App- + // Side via RVS-Messages (file_version_list_request/...). + const [versionsOpen, setVersionsOpen] = useState<{name: string; path: string} | null>(null); + const [versionsList, setVersionsList] = useState>([]); + const [versionsLoading, setVersionsLoading] = useState(false); + const [versionsError, setVersionsError] = useState(''); + const versionDlPending = useRef(null); // requestId beim Versions-Download const [voiceCloneVisible, setVoiceCloneVisible] = useState(false); const [tempPath, setTempPath] = useState(''); // Sub-Screen Navigation: null = Hauptmenue, sonst eine der Section-IDs. @@ -540,6 +548,74 @@ const SettingsScreen: React.FC = () => { })(); } + // Datei-Manager: Versions-Liste einer Datei + if (message.type === ('file_version_list_response' as any)) { + const p: any = message.payload || {}; + setVersionsLoading(false); + if (!p.ok) { + setVersionsError(p.error || 'Unbekannter Fehler'); + setVersionsList([]); + } else { + setVersionsError(''); + setVersionsList(p.versions || []); + } + } + + // Datei-Manager: Versions-Inhalt (Download einer alten Version) + if (message.type === ('file_version_download_response' as any)) { + const p: any = message.payload || {}; + if (p.requestId && p.requestId !== versionDlPending.current) return; + versionDlPending.current = null; + if (!p.ok) { + ToastAndroid.show('Download fehlgeschlagen: ' + (p.error || 'unbekannt'), ToastAndroid.LONG); + return; + } + // base64 → Downloads-Ordner. Hash als Suffix damit Original nicht + // ueberschrieben wird wenn beide Versionen nebeneinander vorliegen + // sollen. + (async () => { + try { + const baseName = (p.name as string) || 'aria-version'; + const shortHash = (p.hash as string || '').slice(0, 7); + const dot = baseName.lastIndexOf('.'); + const stem = dot > 0 ? baseName.slice(0, dot) : baseName; + const ext = dot > 0 ? baseName.slice(dot) : ''; + const dir = RNFS.DownloadDirectoryPath; + let target = `${dir}/${stem}@${shortHash}${ext}`; + let i = 1; + while (await RNFS.exists(target)) { + target = `${dir}/${stem}@${shortHash}_${i}${ext}`; + i++; + } + await RNFS.writeFile(target, p.base64, 'base64'); + const sizeKb = Math.round(((p.base64.length * 0.75)) / 1024); + ToastAndroid.show(`Gespeichert: ${target.split('/').pop()} (${sizeKb} KB)`, ToastAndroid.LONG); + } catch (e: any) { + ToastAndroid.show('Speichern fehlgeschlagen: ' + e.message, ToastAndroid.LONG); + } + })(); + } + + // Datei-Manager: Restore-Bestaetigung + if (message.type === ('file_version_restore_response' as any)) { + const p: any = message.payload || {}; + if (!p.ok) { + ToastAndroid.show('Restore fehlgeschlagen: ' + (p.error || 'unbekannt'), ToastAndroid.LONG); + return; + } + ToastAndroid.show(`Version ${(p.hash || '').slice(0,7)} ist jetzt aktiv`, ToastAndroid.SHORT); + // Versions-Liste neu laden damit der neue restore-Commit auftaucht + if (versionsOpen) { + setVersionsLoading(true); + rvs.send('file_version_list_request' as any, { path: versionsOpen.path }); + } + // File-Liste auch refreshen (mtime hat sich geaendert) + if (fileManagerOpen) { + setFileManagerLoading(true); + rvs.send('file_list_request' as any, {}); + } + } + // Voice wurde gespeichert → Liste neu laden + ggf. auswaehlen if (message.type === ('xtts_voice_saved' as any)) { const name = (message.payload as any).name as string; @@ -964,6 +1040,20 @@ const SettingsScreen: React.FC = () => { {fmtSize(f.size)} · {new Date(f.mtime).toLocaleString('de-DE')} + { + // path-relativ-zu-uploads = nur der Dateiname, + // weil der File-Manager-Bereich flach ist + setVersionsOpen({name: f.name, path: f.name}); + setVersionsList([]); + setVersionsError(''); + setVersionsLoading(true); + rvs.send('file_version_list_request' as any, { path: f.name }); + }} + style={{padding:8}} + > + 🕒 + { Alert.alert( @@ -991,6 +1081,110 @@ const SettingsScreen: React.FC = () => { })()} + + {/* Versions-Modal — Historie pro Datei (auto-commit-git im diagnostic) */} + setVersionsOpen(null)} + > + setVersionsOpen(null)} + > + {}} + style={{backgroundColor:'#0D0D1A', borderWidth:1, borderColor:'#1E1E2E', borderRadius:8, width:'90%', maxHeight:'80%'}} + > + + + Versionen — {versionsOpen?.name || ''} + + setVersionsOpen(null)} style={{padding:6}}> + ✕ + + + + {versionsLoading && ( + Lade... + )} + {!!versionsError && ( + {versionsError} + )} + {!versionsLoading && !versionsError && versionsList.length === 0 && ( + + Noch keine Versions-Historie (Datei kommt erst nach dem nächsten Auto-Commit in den Index). + + )} + {versionsList.map(v => ( + + + + {v.isCurrent && ( + + AKTIV + + )} + + {v.hash.slice(0,7)} + + + {v.subject || ''} + + + + {new Date(v.ts).toLocaleString('de-DE')} + + + { + if (!versionsOpen) return; + const reqId = 'verdl_' + Date.now() + '_' + Math.floor(Math.random()*100000); + versionDlPending.current = reqId; + rvs.send('file_version_download_request' as any, { + path: versionsOpen.path, + hash: v.hash, + requestId: reqId, + }); + ToastAndroid.show('Download läuft…', ToastAndroid.SHORT); + }} + style={{paddingVertical:4, paddingHorizontal:10, borderRadius:6, backgroundColor:'#0096FF22'}} + > + ⬇ + + {!v.isCurrent && ( + { + if (!versionsOpen) return; + Alert.alert( + 'Version aktiv setzen?', + `Hash ${v.hash.slice(0,7)} wird als neue aktive Version gespeichert.\n\nDie aktuelle Version bleibt in der Historie und kann später ebenfalls wiederhergestellt werden.`, + [ + { text: 'Abbrechen', style: 'cancel' }, + { text: 'Restore', onPress: () => { + rvs.send('file_version_restore_request' as any, { + path: versionsOpen.path, + hash: v.hash, + }); + ToastAndroid.show('Restore läuft…', ToastAndroid.SHORT); + }}, + ], + ); + }} + style={{paddingVertical:4, paddingHorizontal:10, borderRadius:6, backgroundColor:'#0096FF'}} + > + ⟲ + + )} + + ))} + + + +