Add WebDAV MOVE support and update workspace rename flow to handle both local and WebDAV-backed workspaces. The Tauri rename_workspace command is made async and now performs filesystem rename for local workspaces and issues a WebDAV MOVE (via a new WebDavClient::move_resource) for remote workspaces, updating stored paths and credentials accordingly. A confirmation dialog is added to SettingsScreen to prompt users before renaming, and minor UI/default tweaks are included (SetupScreen default name). This ensures renames update both local folders and remote WebDAV folders reliably and with user confirmation.
272 lines
10 KiB
Svelte
272 lines
10 KiB
Svelte
<script lang="ts">
|
|
import { invoke } from "@tauri-apps/api/core";
|
|
import { app } from "../stores/app.svelte";
|
|
import ConfirmDialog from "../components/ConfirmDialog.svelte";
|
|
|
|
let { onclose, workspaceId, ondelete }: { onclose?: () => void; workspaceId: string; ondelete?: (id: string) => void } = $props();
|
|
|
|
let ws = $derived(app.config?.workspaces[workspaceId]);
|
|
let isWebdav = $derived(ws?.mode === "webdav");
|
|
|
|
let webdavUrl = $state("");
|
|
let webdavUser = $state("");
|
|
let webdavPass = $state("");
|
|
let testStatus = $state<"idle" | "testing" | "ok" | "fail">("idle");
|
|
|
|
let renaming = $state(false);
|
|
let renameValue = $state("");
|
|
let showKebab = $state(false);
|
|
let confirmRename = $state(false);
|
|
|
|
$effect(() => {
|
|
if (!ws?.webdav_url) return;
|
|
webdavUrl = ws.webdav_url;
|
|
try {
|
|
const domain = new URL(ws.webdav_url).hostname;
|
|
invoke<[string, string]>("load_credentials", { domain }).then(([u, p]) => {
|
|
webdavUser = u;
|
|
webdavPass = p;
|
|
}).catch(() => {});
|
|
} catch {}
|
|
});
|
|
|
|
async function testConnection() {
|
|
testStatus = "testing";
|
|
try {
|
|
await invoke("test_webdav_connection", {
|
|
url: webdavUrl,
|
|
username: webdavUser,
|
|
password: webdavPass,
|
|
});
|
|
testStatus = "ok";
|
|
} catch {
|
|
testStatus = "fail";
|
|
}
|
|
}
|
|
|
|
async function saveWebdav() {
|
|
if (!webdavUrl.trim()) return;
|
|
await invoke("set_webdav_config", {
|
|
workspaceId,
|
|
webdavUrl: webdavUrl.trim(),
|
|
});
|
|
if (webdavUser && webdavPass) {
|
|
const domain = new URL(webdavUrl).hostname;
|
|
await invoke("store_credentials", {
|
|
domain,
|
|
username: webdavUser,
|
|
password: webdavPass,
|
|
});
|
|
}
|
|
await app.loadConfig();
|
|
}
|
|
|
|
function startRename() {
|
|
showKebab = false;
|
|
renaming = true;
|
|
renameValue = ws?.name ?? "";
|
|
}
|
|
|
|
async function handleRename() {
|
|
if (!renaming) return;
|
|
renaming = false;
|
|
var trimmed = renameValue.trim();
|
|
if (!trimmed || trimmed === ws?.name) return;
|
|
confirmRename = true;
|
|
}
|
|
|
|
async function doRename() {
|
|
confirmRename = false;
|
|
var trimmed = renameValue.trim();
|
|
if (!trimmed) return;
|
|
await app.renameWorkspace(workspaceId, trimmed);
|
|
}
|
|
function handleWindowClick(e: MouseEvent) {
|
|
if (showKebab && !(e.target as HTMLElement).closest("[data-settings-kebab]")) showKebab = false;
|
|
}
|
|
</script>
|
|
|
|
<svelte:window onclick={handleWindowClick} />
|
|
|
|
<header
|
|
class="flex items-center justify-between border-b border-border-light px-4 py-3 dark:border-border-dark"
|
|
>
|
|
<h1 class="text-lg font-bold">Workspace Settings</h1>
|
|
<button
|
|
onclick={() => onclose?.()}
|
|
class="rounded-lg p-1.5 hover:bg-black/5 dark:hover:bg-white/10"
|
|
>
|
|
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
<path
|
|
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
</header>
|
|
|
|
<!-- Workspace name + kebab -->
|
|
<div class="flex items-center gap-2 px-4 py-3">
|
|
<div class="min-w-0 flex-1">
|
|
{#if renaming}
|
|
<input
|
|
type="text"
|
|
bind:value={renameValue}
|
|
class="w-full bg-transparent text-xl font-bold outline-none"
|
|
onkeydown={(e) => { if (e.key === "Enter") handleRename(); if (e.key === "Escape") { renaming = false; } }}
|
|
onblur={handleRename}
|
|
autofocus
|
|
/>
|
|
{:else}
|
|
<p class="text-xl font-bold">{ws?.name}</p>
|
|
{/if}
|
|
</div>
|
|
<div class="relative shrink-0" data-settings-kebab>
|
|
<button
|
|
onclick={() => showKebab = !showKebab}
|
|
class="rounded-lg p-1.5 hover:bg-black/5 dark:hover:bg-white/10"
|
|
>
|
|
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
<path d="M10 6a1.5 1.5 0 110-3 1.5 1.5 0 010 3zm0 5.5a1.5 1.5 0 110-3 1.5 1.5 0 010 3zm0 5.5a1.5 1.5 0 110-3 1.5 1.5 0 010 3z" />
|
|
</svg>
|
|
</button>
|
|
{#if showKebab}
|
|
<div class="absolute right-0 top-full z-10 mt-1 w-40 rounded-xl border border-border-light bg-surface-light py-1 shadow-lg dark:border-border-dark dark:bg-surface-dark">
|
|
<button
|
|
onclick={startRename}
|
|
class="flex w-full items-center gap-2 px-4 py-2 text-sm hover:bg-black/5 dark:hover:bg-white/10"
|
|
>
|
|
<svg class="h-4 w-4 opacity-60" viewBox="0 0 20 20" fill="currentColor">
|
|
<path d="M2.695 14.763l-1.262 3.154a.5.5 0 00.65.65l3.155-1.262a4 4 0 001.343-.885L17.5 5.5a2.121 2.121 0 00-3-3L3.58 13.42a4 4 0 00-.885 1.343z" />
|
|
</svg>
|
|
Rename
|
|
</button>
|
|
<button
|
|
onclick={() => { showKebab = false; ondelete?.(workspaceId); }}
|
|
class="flex w-full items-center gap-2 px-4 py-2 text-sm text-danger hover:bg-black/5 dark:hover:bg-white/10"
|
|
>
|
|
<svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
|
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
|
|
</svg>
|
|
Delete
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<main class="flex-1 overflow-y-auto p-4">
|
|
<!-- WebDAV Sync (only for webdav workspaces) -->
|
|
{#if isWebdav}
|
|
<section class="mb-6">
|
|
<h2 class="mb-3 text-sm font-semibold uppercase tracking-wide opacity-50">
|
|
WebDAV Sync
|
|
</h2>
|
|
<div class="rounded-xl border border-border-light p-4 dark:border-border-dark">
|
|
<label class="mb-1 block text-xs font-medium opacity-60">Server URL</label>
|
|
<input
|
|
type="url"
|
|
bind:value={webdavUrl}
|
|
placeholder="https://dav.example.com/tasks/"
|
|
class="mb-3 w-full rounded-lg border border-border-light bg-transparent px-3 py-2 text-sm outline-none focus:border-primary dark:border-border-dark"
|
|
/>
|
|
|
|
<label class="mb-1 block text-xs font-medium opacity-60">Username</label>
|
|
<input
|
|
type="text"
|
|
bind:value={webdavUser}
|
|
class="mb-3 w-full rounded-lg border border-border-light bg-transparent px-3 py-2 text-sm outline-none focus:border-primary dark:border-border-dark"
|
|
/>
|
|
|
|
<label class="mb-1 block text-xs font-medium opacity-60">Password</label>
|
|
<input
|
|
type="password"
|
|
bind:value={webdavPass}
|
|
class="mb-4 w-full rounded-lg border border-border-light bg-transparent px-3 py-2 text-sm outline-none focus:border-primary dark:border-border-dark"
|
|
/>
|
|
|
|
<div class="flex gap-2">
|
|
<button
|
|
onclick={testConnection}
|
|
disabled={!webdavUrl.trim()}
|
|
class="rounded-lg border border-border-light px-4 py-2 text-sm font-medium hover:bg-black/5 disabled:opacity-40 dark:border-border-dark dark:hover:bg-white/10"
|
|
>
|
|
{testStatus === "testing" ? "Testing..." : testStatus === "ok" ? "Connected" : testStatus === "fail" ? "Failed -- Retry" : "Test Connection"}
|
|
</button>
|
|
<button
|
|
onclick={saveWebdav}
|
|
disabled={!webdavUrl.trim()}
|
|
class="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white hover:bg-primary-hover disabled:opacity-40"
|
|
>
|
|
Save
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-3 flex items-center gap-2">
|
|
<select
|
|
value={app.syncMode}
|
|
onchange={(e) => app.setSyncMode((e.target as HTMLSelectElement).value as "full" | "push" | "pull")}
|
|
class="appearance-none rounded-lg border border-border-light bg-surface-light px-3 py-2 text-sm text-text-light outline-none focus:border-primary dark:border-border-dark dark:bg-surface-dark dark:text-text-dark"
|
|
>
|
|
<option value="full">Sync both ways</option>
|
|
<option value="push">Push only</option>
|
|
<option value="pull">Pull only</option>
|
|
</select>
|
|
<button
|
|
onclick={() => app.triggerSync()}
|
|
disabled={app.syncing}
|
|
class="flex-1 rounded-lg bg-primary py-2 text-sm font-medium text-white hover:bg-primary-hover disabled:opacity-40"
|
|
>
|
|
{app.syncing ? "Syncing..." : "Sync Now"}
|
|
</button>
|
|
</div>
|
|
{#if app.error}
|
|
<p class="mt-1.5 text-xs text-danger">{app.error}</p>
|
|
{/if}
|
|
{#if ws?.last_sync}
|
|
{@const lastSync = new Date(ws.last_sync)}
|
|
{@const secsAgo = Math.floor((Date.now() - lastSync.getTime()) / 1000)}
|
|
{@const relTime = secsAgo < 60 ? "just now" : secsAgo < 3600 ? `${Math.floor(secsAgo / 60)}m ago` : `${Math.floor(secsAgo / 3600)}h ago`}
|
|
<p class="mt-1.5 text-xs opacity-40">
|
|
Last sync: {relTime}
|
|
{#if app.lastSyncResult}
|
|
· ↑{app.lastSyncResult.uploaded} ↓{app.lastSyncResult.downloaded}
|
|
{/if}
|
|
</p>
|
|
{/if}
|
|
</section>
|
|
{/if}
|
|
|
|
<!-- Theme -->
|
|
<section>
|
|
<label class="mb-1 block text-xs font-medium opacity-60">Theme</label>
|
|
<select
|
|
value={ws?.theme ?? ""}
|
|
onchange={(e) => {
|
|
const val = (e.target as HTMLSelectElement).value;
|
|
app.setTheme(val || null);
|
|
}}
|
|
class="w-full appearance-none rounded-lg border border-border-light bg-surface-light px-3 py-2 text-sm text-text-light outline-none focus:border-primary dark:border-border-dark dark:bg-surface-dark dark:text-text-dark"
|
|
>
|
|
<option value="">System default</option>
|
|
<option value="light">Light</option>
|
|
<option value="dark">Dark</option>
|
|
<option value="nord">Nord</option>
|
|
<option value="dracula">Dracula</option>
|
|
<option value="solarized">Solarized Dark</option>
|
|
</select>
|
|
</section>
|
|
|
|
<p class="mt-8 text-center text-xs opacity-30">Tauri v2 + Svelte</p>
|
|
</main>
|
|
|
|
{#if confirmRename}
|
|
<ConfirmDialog
|
|
message="Rename workspace to '{renameValue.trim()}'?"
|
|
detail={isWebdav ? "This will rename the folder on the WebDAV server." : "This will rename the folder on disk."}
|
|
confirmText="Rename"
|
|
onconfirm={doRename}
|
|
oncancel={() => confirmRename = false}
|
|
/>
|
|
{/if}
|