feat: Lock-Badge + smartes Kontextmenue in lokaler Client-Ansicht

Die lokale Dateiliste im Client zeigt jetzt pro Datei ein 🔒-Badge
mit Nutzername wenn ausgecheckt (wie Server-Ansicht + Web-GUI).
browse_sync_folder zieht den Server-Tree bei jedem Aufruf und
korreliert via Journal-Lookup (oder .cloud-Metadaten) die lokale
Datei mit dem File-Lock-Status.

Rechtsklick-Menue reagiert jetzt auf den Lock-Status:
- Frei              -> "Auschecken (sperren)"
- Eigener/fremder   -> "Entsperren (einchecken)"
Neuer Tauri-Command lock_file_cmd fuer reines Sperren ohne Oeffnen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Hacker
2026-04-12 11:32:01 +02:00
parent 5afb87c9cd
commit 23563622f8
2 changed files with 105 additions and 13 deletions
+26 -7
View File
@@ -86,20 +86,34 @@ async function doMarkOffline(file) {
async function doUnlockFile(file) {
hideContextMenu();
// Find file ID from server tree
const serverFile = findFileInTree(fileTree.value, file.name);
if (!serverFile) {
const fileId = file.file_id ?? findFileInTree(fileTree.value, file.name)?.id;
if (!fileId) {
syncLog.value = [`[${ts()}] Fehler: Datei nicht auf Server gefunden`, ...syncLog.value];
return;
}
try {
await invoke("unlock_file_cmd", { fileId: serverFile.id });
await invoke("unlock_file_cmd", { fileId });
syncLog.value = [`[${ts()}] Entsperrt: ${file.name}`, ...syncLog.value].slice(0, 200);
} catch (err) {
syncLog.value = [`[${ts()}] Fehler: ${err}`, ...syncLog.value].slice(0, 200);
}
}
async function doLockOnly(file) {
hideContextMenu();
const fileId = file.file_id ?? findFileInTree(fileTree.value, file.name)?.id;
if (!fileId) {
syncLog.value = [`[${ts()}] Fehler: Datei nicht auf Server gefunden`, ...syncLog.value];
return;
}
try {
await invoke("lock_file_cmd", { fileId });
syncLog.value = [`[${ts()}] Ausgecheckt: ${file.name}`, ...syncLog.value].slice(0, 200);
} catch (err) {
syncLog.value = [`[${ts()}] Fehler: ${err}`, ...syncLog.value].slice(0, 200);
}
}
function findFileInTree(entries, name) {
for (const e of entries) {
if (e.name === name) return e;
@@ -463,6 +477,7 @@ onUnmounted(() => { unlistenStatus?.(); unlistenLog?.(); unlistenError?.(); unli
<span class="lf-name">{{ f.name }}</span>
<span v-if="f.is_cloud" class="lf-badge cloud">Cloud</span>
<span v-else-if="f.is_offline" class="lf-badge offline">Offline</span>
<span v-if="f.locked" class="lf-badge locked" :title="'Ausgecheckt von ' + f.locked_by">🔒 {{ f.locked_by }}</span>
<span class="lf-size">{{ formatSize(f.cloud_size || f.size) }}</span>
</div>
<div v-if="!localFiles.length" class="empty">Ordner ist leer</div>
@@ -481,12 +496,15 @@ onUnmounted(() => { unlistenStatus?.(); unlistenLog?.(); unlistenError?.(); unli
<div v-if="contextMenu.file?.is_offline" class="cm-item" @click="doOpenOfflineFile(contextMenu.file)">
📂 Oeffnen (auschecken)
</div>
<div v-if="contextMenu.file?.is_offline && !contextMenu.file?.locked" class="cm-item" @click="doLockOnly(contextMenu.file)">
🔒 Auschecken (sperren)
</div>
<div v-if="contextMenu.file?.is_offline && contextMenu.file?.locked" class="cm-item" @click="doUnlockFile(contextMenu.file)">
🔓 Entsperren (einchecken)
</div>
<div v-if="contextMenu.file?.is_offline" class="cm-item" @click="doUnmarkOffline(contextMenu.file)">
☁ Nicht mehr offline (Platzhalter)
</div>
<div v-if="contextMenu.file?.is_offline" class="cm-item" @click="doUnlockFile(contextMenu.file)">
🔓 Entsperren (Freigeben fuer andere)
</div>
<div class="cm-item" @click="hideContextMenu">Abbrechen</div>
</div>
@@ -604,6 +622,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;f
.lf-badge{font-size:.65rem;padding:.1rem .3rem;border-radius:3px;flex-shrink:0}
.lf-badge.cloud{background:#e3f2fd;color:#1565c0}
.lf-badge.offline{background:#e8f5e9;color:#2e7d32}
.lf-badge.locked{background:#fff3e0;color:#e65100}
.lf-size{font-size:.75rem;color:#999;flex-shrink:0}
.checkbox-row{display:flex;align-items:center;gap:.5rem;font-size:.85rem;cursor:pointer}
.context-menu{position:fixed;background:#fff;border:1px solid #ddd;border-radius:6px;box-shadow:0 4px 12px rgba(0,0,0,.15);z-index:9999;min-width:200px;padding:.25rem 0}