feat(filemanager): 👁 Open + ⬇ Download pro Datei in App + Diagnostic
Stefan's UX-Wunsch: Datei direkt oeffnen ohne Umweg ueber Download. Plus: in der App fehlte komplett der Per-Row-Download-Button (nur via Checkbox + Bulk-Download). Beides jetzt gefixt. App (SettingsScreen.tsx): - Neue per-Row-Buttons: 👁 Open + ⬇ Download + 🕒 Versionen + 🗑 Loeschen - Open-Pfad nutzt requestId-Praefix 'open-' im file_response-Handler → Datei wird nach CachesDirectory geschrieben (kein Storage-Bloat) → FileOpener-Native-Module (Intent.ACTION_VIEW mit MIME) oeffnet mit dem System-Picker → User waehlt PDF-Viewer / Galerie / Player - guessMimeFromName-Helper fuer den Intent damit Android die passende App findet - Download-Pfad unveraendert ('single-' Praefix), schreibt nach DownloadDirectory mit Suffix-Inkrement bei Namens-Konflikt Diagnostic (server.js + index.html): - Neue Route /api/files-view (gleicher Code-Pfad wie files-download, aber Content-Disposition:inline + echter MIME-Type statt octet-stream) - Browser zeigt PDF / Bilder / Text im neuen Tab statt forcierten Download - 👁-Button in jeder File-Row neben ⬇/🕒/🗑 - Fallback fuer unbekannte MIMEs: octet-stream → Browser bietet Download Bei beiden Pfaden bleibt der Cache nutzbar: nach App-Open kann der User die Datei im jeweiligen Viewer behalten; im Browser bleibt sie im Tab.
This commit is contained in:
@@ -21,9 +21,37 @@ import {
|
||||
PermissionsAndroid,
|
||||
useWindowDimensions,
|
||||
DeviceEventEmitter,
|
||||
NativeModules,
|
||||
} from 'react-native';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import RNFS from 'react-native-fs';
|
||||
|
||||
const { FileOpener } = NativeModules as {
|
||||
FileOpener?: { open: (filePath: string, mimeType: string) => Promise<boolean> };
|
||||
};
|
||||
|
||||
// MIME-Type aus Dateinamen schaetzen — fuer den FileOpener-Intent. Android
|
||||
// nutzt den MIME-Type um die passende App zu finden. Unknown → octet-stream.
|
||||
function guessMimeFromName(name: string): string {
|
||||
const lower = name.toLowerCase();
|
||||
if (lower.endsWith('.pdf')) return 'application/pdf';
|
||||
if (lower.endsWith('.jpg') || lower.endsWith('.jpeg')) return 'image/jpeg';
|
||||
if (lower.endsWith('.png')) return 'image/png';
|
||||
if (lower.endsWith('.gif')) return 'image/gif';
|
||||
if (lower.endsWith('.webp')) return 'image/webp';
|
||||
if (lower.endsWith('.mp3')) return 'audio/mpeg';
|
||||
if (lower.endsWith('.wav')) return 'audio/wav';
|
||||
if (lower.endsWith('.ogg') || lower.endsWith('.opus')) return 'audio/ogg';
|
||||
if (lower.endsWith('.mp4') || lower.endsWith('.m4a')) return 'audio/mp4';
|
||||
if (lower.endsWith('.webm')) return 'video/webm';
|
||||
if (lower.endsWith('.txt')) return 'text/plain';
|
||||
if (lower.endsWith('.md')) return 'text/markdown';
|
||||
if (lower.endsWith('.json')) return 'application/json';
|
||||
if (lower.endsWith('.csv')) return 'text/csv';
|
||||
if (lower.endsWith('.html') || lower.endsWith('.htm')) return 'text/html';
|
||||
if (lower.endsWith('.zip')) return 'application/zip';
|
||||
return 'application/octet-stream';
|
||||
}
|
||||
import DocumentPicker from 'react-native-document-picker';
|
||||
import rvs, { ConnectionState, RVSMessage, ConnectionConfig, ConnectionLogEntry } from '../services/rvs';
|
||||
import {
|
||||
@@ -514,9 +542,11 @@ const SettingsScreen: React.FC = () => {
|
||||
if (message.type === ('file_response' as any)) {
|
||||
const p: any = message.payload || {};
|
||||
const reqId = (p.requestId as string) || '';
|
||||
if (!reqId.startsWith('single-')) return; // nicht unsere Anfrage
|
||||
const isDownload = reqId.startsWith('single-');
|
||||
const isOpen = reqId.startsWith('open-');
|
||||
if (!isDownload && !isOpen) return; // andere Caller (ChatScreen etc.)
|
||||
if (p.error) {
|
||||
ToastAndroid.show('Download fehlgeschlagen: ' + p.error, ToastAndroid.LONG);
|
||||
ToastAndroid.show((isOpen ? 'Öffnen' : 'Download') + ' fehlgeschlagen: ' + p.error, ToastAndroid.LONG);
|
||||
return;
|
||||
}
|
||||
const b64 = (p.base64 as string) || '';
|
||||
@@ -526,10 +556,28 @@ const SettingsScreen: React.FC = () => {
|
||||
'aria-download';
|
||||
(async () => {
|
||||
try {
|
||||
if (isOpen) {
|
||||
// Open-Pfad: nach Caches schreiben + per FileOpener mit System-
|
||||
// Viewer oeffnen. Caches damit der Speicher kein Dauer-Muell wird.
|
||||
const dir = RNFS.CachesDirectoryPath;
|
||||
const target = `${dir}/${fileName}`;
|
||||
await RNFS.writeFile(target, b64, 'base64');
|
||||
const mime = (p.mimeType as string) || guessMimeFromName(fileName);
|
||||
if (FileOpener?.open) {
|
||||
try {
|
||||
await FileOpener.open(target, mime);
|
||||
} catch (e: any) {
|
||||
ToastAndroid.show('Öffnen fehlgeschlagen: ' + (e?.message || e), ToastAndroid.LONG);
|
||||
}
|
||||
} else {
|
||||
ToastAndroid.show('FileOpener-Modul nicht verfügbar — APK neu bauen', ToastAndroid.LONG);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Download-Pfad: nach Downloads-Ordner schreiben, mit Suffix bei
|
||||
// Namens-Konflikt damit nichts ueberschrieben wird.
|
||||
const dir = RNFS.DownloadDirectoryPath;
|
||||
const filePath = `${dir}/${fileName}`;
|
||||
// Falls Datei schon existiert: Suffix anhaengen damit nichts
|
||||
// ueberschrieben wird.
|
||||
let target = filePath;
|
||||
let i = 1;
|
||||
while (await RNFS.exists(target)) {
|
||||
@@ -1040,6 +1088,30 @@ const SettingsScreen: React.FC = () => {
|
||||
{fmtSize(f.size)} · {new Date(f.mtime).toLocaleString('de-DE')}
|
||||
</Text>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
rvs.send('file_request' as any, {
|
||||
serverPath: f.path,
|
||||
requestId: 'open-' + Date.now(),
|
||||
});
|
||||
ToastAndroid.show('Öffne ' + f.name + '…', ToastAndroid.SHORT);
|
||||
}}
|
||||
style={{padding:8}}
|
||||
>
|
||||
<Text style={{color:'#0096FF', fontSize:18}}>👁</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
rvs.send('file_request' as any, {
|
||||
serverPath: f.path,
|
||||
requestId: 'single-' + Date.now(),
|
||||
});
|
||||
ToastAndroid.show('Download läuft…', ToastAndroid.SHORT);
|
||||
}}
|
||||
style={{padding:8}}
|
||||
>
|
||||
<Text style={{color:'#34C759', fontSize:18}}>⬇</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
// path-relativ-zu-uploads = nur der Dateiname,
|
||||
|
||||
Reference in New Issue
Block a user