feat: Offline-Dateien werden beim erneuten Oeffnen wieder ausgecheckt
Bisher hat der Client nur beim ersten Oeffnen (.cloud-Platzhalter -> Download) gesperrt. Nach dem Einchecken und erneutem Doppelklick blieb die Datei ungesperrt, weil der Open-Pfad fehlte. Neuer Tauri-Command open_offline_file loest die Server-Datei-ID ueber das Sync-Journal auf, sperrt auf dem Server und oeffnet lokal mit der Standard-App. Im lokalen Dateibrowser: - Doppelklick auf eine bereits offline vorhandene Datei checkt sie nun aus und oeffnet sie (vorher: keine Reaktion) - Rechtsklick-Menue hat "Oeffnen (auschecken)" fuer Offline-Dateien Das Lock triggert wie gehabt notify_file_change -> SSE -> Web-UI aktualisiert den Lock-Status sofort. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -319,6 +319,52 @@ async fn open_cloud_file(state: State<'_, AppState>, cloud_path: String) -> Resu
|
|||||||
Ok(real_path.to_string_lossy().to_string())
|
Ok(real_path.to_string_lossy().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Open a real (already-downloaded) file: lock it on the server, then open
|
||||||
|
/// it with the default application. Used for files that are already offline-
|
||||||
|
/// available so they still get checked out.
|
||||||
|
#[tauri::command]
|
||||||
|
async fn open_offline_file(state: State<'_, AppState>, real_path: String) -> Result<String, String> {
|
||||||
|
let path = PathBuf::from(&real_path);
|
||||||
|
if !path.exists() {
|
||||||
|
return Err(format!("Datei nicht gefunden: {}", real_path));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve file_id by matching this path against the configured sync paths
|
||||||
|
// and looking the relative path up in the journal.
|
||||||
|
let (sync_path_id, rel_path) = {
|
||||||
|
let paths = state.sync_paths.lock().unwrap().clone();
|
||||||
|
let mut best: Option<(String, String)> = None;
|
||||||
|
for sp in &paths {
|
||||||
|
let base = PathBuf::from(&sp.local_dir);
|
||||||
|
if let Ok(rel) = path.strip_prefix(&base) {
|
||||||
|
let rel_str = rel.to_string_lossy().replace('\\', "/");
|
||||||
|
best = Some((sp.id.clone(), rel_str));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
best.ok_or("Datei gehoert zu keinem konfigurierten Sync-Pfad")?
|
||||||
|
};
|
||||||
|
|
||||||
|
let journal = state.journal.clone();
|
||||||
|
let entry = journal.get(&sync_path_id, &rel_path)
|
||||||
|
.ok_or("Datei nicht im Sync-Journal - erst einmal synchronisieren")?;
|
||||||
|
let file_id = entry.file_id.ok_or("Keine Server-ID im Journal")?;
|
||||||
|
|
||||||
|
let api = state.api.lock().unwrap().clone().ok_or("Nicht eingeloggt")?;
|
||||||
|
match api.lock_file(file_id, "Desktop Sync Client").await {
|
||||||
|
Ok(_) => {
|
||||||
|
eprintln!("[OpenOffline] Locked {} on server", rel_path);
|
||||||
|
let mut locked = state.locked_files.lock().unwrap();
|
||||||
|
if !locked.contains(&file_id) { locked.push(file_id); }
|
||||||
|
}
|
||||||
|
Err(e) => return Err(format!("Sperre fehlgeschlagen: {}", e)),
|
||||||
|
}
|
||||||
|
|
||||||
|
open::that(&path)
|
||||||
|
.map_err(|e| format!("Oeffnen fehlgeschlagen: {}", e))?;
|
||||||
|
Ok(real_path)
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn unlock_file_cmd(state: State<'_, AppState>, file_id: i64) -> Result<String, String> {
|
async fn unlock_file_cmd(state: State<'_, AppState>, file_id: i64) -> Result<String, String> {
|
||||||
let api = state.api.lock().unwrap().clone().ok_or("Nicht eingeloggt")?;
|
let api = state.api.lock().unwrap().clone().ok_or("Nicht eingeloggt")?;
|
||||||
@@ -890,6 +936,7 @@ pub fn run() {
|
|||||||
start_sync,
|
start_sync,
|
||||||
run_sync_now,
|
run_sync_now,
|
||||||
open_cloud_file,
|
open_cloud_file,
|
||||||
|
open_offline_file,
|
||||||
get_file_tree,
|
get_file_tree,
|
||||||
get_status,
|
get_status,
|
||||||
unlock_file_cmd,
|
unlock_file_cmd,
|
||||||
|
|||||||
@@ -132,6 +132,16 @@ async function doOpenCloudFile(file) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function doOpenOfflineFile(file) {
|
||||||
|
hideContextMenu();
|
||||||
|
try {
|
||||||
|
await invoke("open_offline_file", { realPath: file.path });
|
||||||
|
syncLog.value = [`[${ts()}] Ausgecheckt + geoeffnet: ${file.name}`, ...syncLog.value].slice(0, 200);
|
||||||
|
} catch (err) {
|
||||||
|
syncLog.value = [`[${ts()}] Fehler: ${err}`, ...syncLog.value].slice(0, 200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let unlistenStatus, unlistenLog, unlistenError, unlistenFileChange, unlistenTrigger, unlistenCloudOpen;
|
let unlistenStatus, unlistenLog, unlistenError, unlistenFileChange, unlistenTrigger, unlistenCloudOpen;
|
||||||
|
|
||||||
async function handleLogin() {
|
async function handleLogin() {
|
||||||
@@ -441,7 +451,7 @@ onUnmounted(() => { unlistenStatus?.(); unlistenLog?.(); unlistenError?.(); unli
|
|||||||
<div class="local-file-list">
|
<div class="local-file-list">
|
||||||
<div v-for="f in localFiles" :key="f.path"
|
<div v-for="f in localFiles" :key="f.path"
|
||||||
class="local-file-item"
|
class="local-file-item"
|
||||||
@dblclick="f.is_folder ? openLocalFolder(f) : (f.is_cloud ? doOpenCloudFile(f) : null)"
|
@dblclick="f.is_folder ? openLocalFolder(f) : (f.is_cloud ? doOpenCloudFile(f) : doOpenOfflineFile(f))"
|
||||||
@contextmenu="showContextMenu($event, f)">
|
@contextmenu="showContextMenu($event, f)">
|
||||||
<span class="lf-icon">{{ f.is_folder ? '📁' : (f.is_cloud ? '☁' : '📄') }}</span>
|
<span class="lf-icon">{{ f.is_folder ? '📁' : (f.is_cloud ? '☁' : '📄') }}</span>
|
||||||
<span class="lf-name">{{ f.name }}</span>
|
<span class="lf-name">{{ f.name }}</span>
|
||||||
@@ -462,6 +472,9 @@ onUnmounted(() => { unlistenStatus?.(); unlistenLog?.(); unlistenError?.(); unli
|
|||||||
<div v-if="contextMenu.file?.is_cloud" class="cm-item" @click="doMarkOffline(contextMenu.file)">
|
<div v-if="contextMenu.file?.is_cloud" class="cm-item" @click="doMarkOffline(contextMenu.file)">
|
||||||
💾 Offline verfuegbar machen
|
💾 Offline verfuegbar machen
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="contextMenu.file?.is_offline" class="cm-item" @click="doOpenOfflineFile(contextMenu.file)">
|
||||||
|
📂 Oeffnen (auschecken)
|
||||||
|
</div>
|
||||||
<div v-if="contextMenu.file?.is_offline" class="cm-item" @click="doUnmarkOffline(contextMenu.file)">
|
<div v-if="contextMenu.file?.is_offline" class="cm-item" @click="doUnmarkOffline(contextMenu.file)">
|
||||||
☁ Nicht mehr offline (Platzhalter)
|
☁ Nicht mehr offline (Platzhalter)
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user