feat: setup mode selection, per-workspace settings in kebab menu
Setup screen now offers Local vs WebDAV mode choice with cancel button when workspaces exist. Settings moved from drawer bottom into workspace kebab menu, scoped per-workspace. WebDAV section hidden for local workspaces, theme dropdown replaces dark mode toggle. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a1e97bc0fe
commit
12afc91110
|
|
@ -2,7 +2,10 @@
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { app } from "../stores/app.svelte";
|
import { app } from "../stores/app.svelte";
|
||||||
|
|
||||||
let { onclose }: { onclose?: () => void } = $props();
|
let { onclose, workspaceName }: { onclose?: () => void; workspaceName: string } = $props();
|
||||||
|
|
||||||
|
let ws = $derived(app.config?.workspaces[workspaceName]);
|
||||||
|
let isWebdav = $derived(ws?.mode === "webdav");
|
||||||
|
|
||||||
let webdavUrl = $state("");
|
let webdavUrl = $state("");
|
||||||
let webdavUser = $state("");
|
let webdavUser = $state("");
|
||||||
|
|
@ -10,19 +13,15 @@
|
||||||
let testStatus = $state<"idle" | "testing" | "ok" | "fail">("idle");
|
let testStatus = $state<"idle" | "testing" | "ok" | "fail">("idle");
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const ws = app.config?.current_workspace;
|
if (!ws?.webdav_url) return;
|
||||||
if (!ws) return;
|
webdavUrl = ws.webdav_url;
|
||||||
const cfg = app.config?.workspaces[ws];
|
try {
|
||||||
if (cfg?.webdav_url) {
|
const domain = new URL(ws.webdav_url).hostname;
|
||||||
webdavUrl = cfg.webdav_url;
|
invoke<[string, string]>("load_credentials", { domain }).then(([u, p]) => {
|
||||||
try {
|
webdavUser = u;
|
||||||
const domain = new URL(cfg.webdav_url).hostname;
|
webdavPass = p;
|
||||||
invoke<[string, string]>("load_credentials", { domain }).then(([u, p]) => {
|
}).catch(() => {});
|
||||||
webdavUser = u;
|
} catch {}
|
||||||
webdavPass = p;
|
|
||||||
}).catch(() => {});
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
async function testConnection() {
|
async function testConnection() {
|
||||||
|
|
@ -40,9 +39,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveWebdav() {
|
async function saveWebdav() {
|
||||||
if (!app.config?.current_workspace || !webdavUrl.trim()) return;
|
if (!webdavUrl.trim()) return;
|
||||||
await invoke("set_webdav_config", {
|
await invoke("set_webdav_config", {
|
||||||
workspaceName: app.config.current_workspace,
|
workspaceName,
|
||||||
webdavUrl: webdavUrl.trim(),
|
webdavUrl: webdavUrl.trim(),
|
||||||
});
|
});
|
||||||
if (webdavUser && webdavPass) {
|
if (webdavUser && webdavPass) {
|
||||||
|
|
@ -55,13 +54,12 @@
|
||||||
}
|
}
|
||||||
await app.loadConfig();
|
await app.loadConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header
|
<header
|
||||||
class="flex items-center justify-between border-b border-border-light px-4 py-3 dark:border-border-dark"
|
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">Settings</h1>
|
<h1 class="text-lg font-bold">{workspaceName} Settings</h1>
|
||||||
<button
|
<button
|
||||||
onclick={() => onclose?.()}
|
onclick={() => onclose?.()}
|
||||||
class="rounded-lg p-1.5 hover:bg-black/5 dark:hover:bg-white/10"
|
class="rounded-lg p-1.5 hover:bg-black/5 dark:hover:bg-white/10"
|
||||||
|
|
@ -75,53 +73,53 @@
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="flex-1 overflow-y-auto p-4">
|
<main class="flex-1 overflow-y-auto p-4">
|
||||||
<!-- WebDAV Sync -->
|
<!-- WebDAV Sync (only for webdav workspaces) -->
|
||||||
<section class="mb-6">
|
{#if isWebdav}
|
||||||
<h2 class="mb-3 text-sm font-semibold uppercase tracking-wide opacity-50">
|
<section class="mb-6">
|
||||||
WebDAV Sync
|
<h2 class="mb-3 text-sm font-semibold uppercase tracking-wide opacity-50">
|
||||||
</h2>
|
WebDAV Sync
|
||||||
<div class="rounded-xl border border-border-light p-4 dark:border-border-dark">
|
</h2>
|
||||||
<label class="mb-1 block text-xs font-medium opacity-60">Server URL</label>
|
<div class="rounded-xl border border-border-light p-4 dark:border-border-dark">
|
||||||
<input
|
<label class="mb-1 block text-xs font-medium opacity-60">Server URL</label>
|
||||||
type="url"
|
<input
|
||||||
bind:value={webdavUrl}
|
type="url"
|
||||||
placeholder="https://dav.example.com/tasks/"
|
bind:value={webdavUrl}
|
||||||
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"
|
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>
|
<label class="mb-1 block text-xs font-medium opacity-60">Username</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={webdavUser}
|
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"
|
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>
|
<label class="mb-1 block text-xs font-medium opacity-60">Password</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
bind:value={webdavPass}
|
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"
|
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">
|
<div class="flex gap-2">
|
||||||
<button
|
<button
|
||||||
onclick={testConnection}
|
onclick={testConnection}
|
||||||
disabled={!webdavUrl.trim()}
|
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"
|
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"}
|
{testStatus === "testing" ? "Testing..." : testStatus === "ok" ? "Connected" : testStatus === "fail" ? "Failed -- Retry" : "Test Connection"}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onclick={saveWebdav}
|
onclick={saveWebdav}
|
||||||
disabled={!webdavUrl.trim()}
|
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"
|
class="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white hover:bg-primary-hover disabled:opacity-40"
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if app.config?.current_workspace}
|
|
||||||
<div class="mt-3 flex items-center gap-2">
|
<div class="mt-3 flex items-center gap-2">
|
||||||
<select
|
<select
|
||||||
value={app.syncMode}
|
value={app.syncMode}
|
||||||
|
|
@ -137,11 +135,11 @@
|
||||||
disabled={app.syncing}
|
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"
|
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"}
|
{app.syncing ? "Syncing..." : "Sync Now"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{#if app.config.workspaces[app.config.current_workspace]?.last_sync}
|
{#if ws?.last_sync}
|
||||||
{@const lastSync = new Date(app.config.workspaces[app.config.current_workspace].last_sync!)}
|
{@const lastSync = new Date(ws.last_sync)}
|
||||||
{@const secsAgo = Math.floor((Date.now() - lastSync.getTime()) / 1000)}
|
{@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`}
|
{@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">
|
<p class="mt-1.5 text-xs opacity-40">
|
||||||
|
|
@ -151,27 +149,32 @@
|
||||||
{/if}
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
</section>
|
||||||
</section>
|
{/if}
|
||||||
|
|
||||||
<!-- Theme -->
|
<!-- Theme -->
|
||||||
<section>
|
<section>
|
||||||
<h2 class="mb-3 text-sm font-semibold uppercase tracking-wide opacity-50">
|
<h2 class="mb-3 text-sm font-semibold uppercase tracking-wide opacity-50">
|
||||||
Appearance
|
Appearance
|
||||||
</h2>
|
</h2>
|
||||||
<button
|
<div class="rounded-xl border border-border-light p-4 dark:border-border-dark">
|
||||||
onclick={() => app.toggleDarkMode()}
|
<label class="mb-1 block text-xs font-medium opacity-60">Theme</label>
|
||||||
class="flex w-full items-center justify-between rounded-xl border border-border-light p-4 dark:border-border-dark"
|
<select
|
||||||
>
|
value={ws?.theme ?? ""}
|
||||||
<span class="text-sm font-medium">Dark mode</span>
|
onchange={(e) => {
|
||||||
<div
|
const val = (e.target as HTMLSelectElement).value;
|
||||||
class="h-6 w-11 rounded-full transition-colors {app.darkMode ? 'bg-primary' : 'bg-gray-300 dark:bg-gray-600'}"
|
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"
|
||||||
>
|
>
|
||||||
<div
|
<option value="">System default</option>
|
||||||
class="h-5 w-5 translate-y-0.5 rounded-full bg-white shadow transition-transform {app.darkMode ? 'translate-x-5.5' : 'translate-x-0.5'}"
|
<option value="light">Light</option>
|
||||||
></div>
|
<option value="dark">Dark</option>
|
||||||
</div>
|
<option value="nord">Nord</option>
|
||||||
</button>
|
<option value="dracula">Dracula</option>
|
||||||
|
<option value="solarized">Solarized Dark</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<p class="mt-8 text-center text-xs opacity-30">Tauri v2 + Svelte</p>
|
<p class="mt-8 text-center text-xs opacity-30">Tauri v2 + Svelte</p>
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,25 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { open } from "@tauri-apps/plugin-dialog";
|
import { open } from "@tauri-apps/plugin-dialog";
|
||||||
import { app } from "../stores/app.svelte";
|
import { app } from "../stores/app.svelte";
|
||||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
import { platform } from "@tauri-apps/plugin-os";
|
import { platform } from "@tauri-apps/plugin-os";
|
||||||
|
|
||||||
|
let { cancellable = false }: { cancellable?: boolean } = $props();
|
||||||
|
|
||||||
const appWindow = getCurrentWindow();
|
const appWindow = getCurrentWindow();
|
||||||
const currentPlatform = platform();
|
const currentPlatform = platform();
|
||||||
const isDesktop = currentPlatform === "linux" || currentPlatform === "windows";
|
const isDesktop = currentPlatform === "linux" || currentPlatform === "windows";
|
||||||
const isWindows = currentPlatform === "windows";
|
const isWindows = currentPlatform === "windows";
|
||||||
|
const isMobile = currentPlatform === "android" || currentPlatform === "ios";
|
||||||
|
|
||||||
|
let mode = $state<"local" | "webdav" | null>(isMobile ? "webdav" : null);
|
||||||
let name = $state("");
|
let name = $state("");
|
||||||
let path = $state("");
|
let path = $state("");
|
||||||
|
let webdavUrl = $state("");
|
||||||
|
let webdavUser = $state("");
|
||||||
|
let webdavPass = $state("");
|
||||||
|
let testStatus = $state<"idle" | "testing" | "ok" | "fail">("idle");
|
||||||
|
|
||||||
async function pickFolder() {
|
async function pickFolder() {
|
||||||
const selected = await open({ directory: true, multiple: false });
|
const selected = await open({ directory: true, multiple: false });
|
||||||
|
|
@ -26,23 +35,63 @@
|
||||||
const selected = await open({ directory: true, multiple: false });
|
const selected = await open({ directory: true, multiple: false });
|
||||||
if (!selected) return;
|
if (!selected) return;
|
||||||
const folder = selected as string;
|
const folder = selected as string;
|
||||||
// Derive workspace name from folder name
|
|
||||||
const parts = folder.replace(/\\/g, "/").split("/");
|
const parts = folder.replace(/\\/g, "/").split("/");
|
||||||
const wsName = parts[parts.length - 1] || "workspace";
|
const wsName = parts[parts.length - 1] || "workspace";
|
||||||
await app.addWorkspace(wsName, folder);
|
await app.addWorkspace(wsName, folder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function testConnection() {
|
||||||
|
testStatus = "testing";
|
||||||
|
try {
|
||||||
|
await invoke("test_webdav_connection", {
|
||||||
|
url: webdavUrl,
|
||||||
|
username: webdavUser,
|
||||||
|
password: webdavPass,
|
||||||
|
});
|
||||||
|
testStatus = "ok";
|
||||||
|
} catch {
|
||||||
|
testStatus = "fail";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCreateWebdav() {
|
||||||
|
if (!name.trim() || !webdavUrl.trim()) return;
|
||||||
|
await app.addWebdavWorkspace(name.trim(), webdavUrl.trim(), webdavUser, webdavPass);
|
||||||
|
}
|
||||||
|
|
||||||
function handleDrag(e: MouseEvent) {
|
function handleDrag(e: MouseEvent) {
|
||||||
if (e.button !== 0) return;
|
if (e.button !== 0) return;
|
||||||
if ((e.target as HTMLElement).closest("button, input")) return;
|
if ((e.target as HTMLElement).closest("button, input")) return;
|
||||||
if (isDesktop) appWindow.startDragging();
|
if (isDesktop) appWindow.startDragging();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function goBack() {
|
||||||
|
mode = null;
|
||||||
|
name = "";
|
||||||
|
path = "";
|
||||||
|
webdavUrl = "";
|
||||||
|
webdavUser = "";
|
||||||
|
webdavPass = "";
|
||||||
|
testStatus = "idle";
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
<div class="flex h-full flex-col" onmousedown={handleDrag}>
|
<div class="flex h-full flex-col" onmousedown={handleDrag}>
|
||||||
<!-- Title bar area with window controls -->
|
<!-- Title bar area with window controls -->
|
||||||
<header class="flex h-11 shrink-0 items-center justify-end px-2">
|
<header class="flex h-11 shrink-0 items-center justify-between px-2">
|
||||||
|
<div>
|
||||||
|
{#if cancellable}
|
||||||
|
<button
|
||||||
|
onclick={() => app.setScreen("tasks")}
|
||||||
|
class="rounded-lg p-1.5 opacity-50 hover:bg-black/10 hover:opacity-80 dark:hover:bg-white/10"
|
||||||
|
>
|
||||||
|
<svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M17 10a.75.75 0 01-.75.75H5.612l4.158 3.96a.75.75 0 11-1.04 1.08l-5.5-5.25a.75.75 0 010-1.08l5.5-5.25a.75.75 0 111.04 1.08L5.612 9.25H16.25A.75.75 0 0117 10z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
{#if isDesktop}
|
{#if isDesktop}
|
||||||
<div class="flex items-center gap-0.5">
|
<div class="flex items-center gap-0.5">
|
||||||
{#if isWindows}
|
{#if isWindows}
|
||||||
|
|
@ -72,58 +121,162 @@
|
||||||
class="w-full max-w-sm rounded-2xl bg-card-light p-8 shadow-lg dark:bg-card-dark"
|
class="w-full max-w-sm rounded-2xl bg-card-light p-8 shadow-lg dark:bg-card-dark"
|
||||||
>
|
>
|
||||||
<h1 class="mb-1 text-2xl font-bold">Onyx</h1>
|
<h1 class="mb-1 text-2xl font-bold">Onyx</h1>
|
||||||
<p class="mb-6 text-sm text-text-secondary-light dark:text-text-secondary-dark">
|
|
||||||
Create a new workspace or open an existing one.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<label class="mb-1 block text-sm font-medium">
|
{#if mode === null}
|
||||||
Workspace name
|
<!-- Step 1: Choose mode -->
|
||||||
<input
|
<p class="mb-6 text-sm text-text-secondary-light dark:text-text-secondary-dark">
|
||||||
type="text"
|
How would you like to store your tasks?
|
||||||
bind:value={name}
|
</p>
|
||||||
placeholder="My Tasks"
|
|
||||||
class="mt-1 mb-4 w-full rounded-lg border border-border-light bg-transparent px-3 py-2 text-sm font-normal outline-none focus:border-primary dark:border-border-dark"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<!-- svelte-ignore a11y_label_has_associated_control -->
|
|
||||||
<label class="mb-1 block text-sm font-medium">Folder</label>
|
|
||||||
<div class="mb-6 flex gap-2">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
bind:value={path}
|
|
||||||
readonly
|
|
||||||
placeholder="Select a folder…"
|
|
||||||
class="min-w-0 flex-1 rounded-lg border border-border-light bg-transparent px-3 py-2 text-sm dark:border-border-dark"
|
|
||||||
/>
|
|
||||||
<button
|
<button
|
||||||
onclick={pickFolder}
|
onclick={() => (mode = "local")}
|
||||||
class="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white hover:bg-primary-hover"
|
class="mb-3 w-full rounded-xl border border-border-light p-4 text-left hover:bg-black/5 dark:border-border-dark dark:hover:bg-white/10"
|
||||||
>
|
>
|
||||||
Browse
|
<p class="text-sm font-semibold">Local Folder</p>
|
||||||
|
<p class="mt-0.5 text-xs text-text-secondary-light dark:text-text-secondary-dark">
|
||||||
|
Pick a folder on your computer. Files stay local.
|
||||||
|
</p>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onclick={handleCreate}
|
onclick={() => (mode = "webdav")}
|
||||||
disabled={!name.trim() || !path.trim()}
|
class="w-full rounded-xl border border-border-light p-4 text-left hover:bg-black/5 dark:border-border-dark dark:hover:bg-white/10"
|
||||||
class="w-full rounded-lg bg-primary py-2.5 text-sm font-medium text-white hover:bg-primary-hover disabled:opacity-40"
|
>
|
||||||
>
|
<p class="text-sm font-semibold">WebDAV Server</p>
|
||||||
Create Workspace
|
<p class="mt-0.5 text-xs text-text-secondary-light dark:text-text-secondary-dark">
|
||||||
</button>
|
Connect to a WebDAV server. The app manages local files automatically.
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
|
||||||
<div class="my-4 flex items-center gap-3">
|
{:else if mode === "local"}
|
||||||
<div class="h-px flex-1 bg-border-light dark:bg-border-dark"></div>
|
<!-- Step 2a: Local workspace -->
|
||||||
<span class="text-xs opacity-40">or</span>
|
<p class="mb-6 text-sm text-text-secondary-light dark:text-text-secondary-dark">
|
||||||
<div class="h-px flex-1 bg-border-light dark:bg-border-dark"></div>
|
Create a new workspace or open an existing one.
|
||||||
</div>
|
</p>
|
||||||
|
|
||||||
<button
|
<label class="mb-1 block text-sm font-medium">
|
||||||
onclick={handleOpen}
|
Workspace name
|
||||||
class="w-full rounded-lg border border-border-light py-2.5 text-sm font-medium hover:bg-black/5 dark:border-border-dark dark:hover:bg-white/10"
|
<input
|
||||||
>
|
type="text"
|
||||||
Open Existing Folder
|
bind:value={name}
|
||||||
</button>
|
placeholder="My Tasks"
|
||||||
|
class="mt-1 mb-4 w-full rounded-lg border border-border-light bg-transparent px-3 py-2 text-sm font-normal outline-none focus:border-primary dark:border-border-dark"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y_label_has_associated_control -->
|
||||||
|
<label class="mb-1 block text-sm font-medium">Folder</label>
|
||||||
|
<div class="mb-6 flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={path}
|
||||||
|
readonly
|
||||||
|
placeholder="Select a folder..."
|
||||||
|
class="min-w-0 flex-1 rounded-lg border border-border-light bg-transparent px-3 py-2 text-sm dark:border-border-dark"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onclick={pickFolder}
|
||||||
|
class="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white hover:bg-primary-hover"
|
||||||
|
>
|
||||||
|
Browse
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onclick={handleCreate}
|
||||||
|
disabled={!name.trim() || !path.trim()}
|
||||||
|
class="w-full rounded-lg bg-primary py-2.5 text-sm font-medium text-white hover:bg-primary-hover disabled:opacity-40"
|
||||||
|
>
|
||||||
|
Create Workspace
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="my-4 flex items-center gap-3">
|
||||||
|
<div class="h-px flex-1 bg-border-light dark:bg-border-dark"></div>
|
||||||
|
<span class="text-xs opacity-40">or</span>
|
||||||
|
<div class="h-px flex-1 bg-border-light dark:bg-border-dark"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onclick={handleOpen}
|
||||||
|
class="mb-3 w-full rounded-lg border border-border-light py-2.5 text-sm font-medium hover:bg-black/5 dark:border-border-dark dark:hover:bg-white/10"
|
||||||
|
>
|
||||||
|
Open Existing Folder
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{#if !isMobile}
|
||||||
|
<button
|
||||||
|
onclick={goBack}
|
||||||
|
class="w-full rounded-lg py-2 text-sm opacity-50 hover:opacity-80"
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{:else}
|
||||||
|
<!-- Step 2b: WebDAV workspace -->
|
||||||
|
<p class="mb-6 text-sm text-text-secondary-light dark:text-text-secondary-dark">
|
||||||
|
Connect to a WebDAV server for cloud-synced tasks.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<label class="mb-1 block text-sm font-medium">
|
||||||
|
Workspace name
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={name}
|
||||||
|
placeholder="My Tasks"
|
||||||
|
class="mt-1 mb-4 w-full rounded-lg border border-border-light bg-transparent px-3 py-2 text-sm font-normal outline-none focus:border-primary dark:border-border-dark"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<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="mb-4 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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onclick={handleCreateWebdav}
|
||||||
|
disabled={!name.trim() || !webdavUrl.trim()}
|
||||||
|
class="w-full rounded-lg bg-primary py-2.5 text-sm font-medium text-white hover:bg-primary-hover disabled:opacity-40"
|
||||||
|
>
|
||||||
|
Create Workspace
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{#if !isMobile}
|
||||||
|
<button
|
||||||
|
onclick={goBack}
|
||||||
|
class="mt-3 w-full rounded-lg py-2 text-sm opacity-50 hover:opacity-80"
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@
|
||||||
|
|
||||||
let showDrawer = $state(false);
|
let showDrawer = $state(false);
|
||||||
let showSettings = $state(false);
|
let showSettings = $state(false);
|
||||||
|
let settingsWorkspace = $state<string | null>(null);
|
||||||
let showNewList = $state(false);
|
let showNewList = $state(false);
|
||||||
let showWorkspacePicker = $state(false);
|
let showWorkspacePicker = $state(false);
|
||||||
let workspacePickerEl = $state<HTMLDivElement | null>(null);
|
let workspacePickerEl = $state<HTMLDivElement | null>(null);
|
||||||
|
|
@ -152,7 +153,7 @@
|
||||||
clone.style.position = "absolute";
|
clone.style.position = "absolute";
|
||||||
clone.style.top = "-9999px";
|
clone.style.top = "-9999px";
|
||||||
clone.style.left = "-9999px";
|
clone.style.left = "-9999px";
|
||||||
if (app.darkMode) {
|
if (app.isDark) {
|
||||||
clone.classList.add("dark");
|
clone.classList.add("dark");
|
||||||
clone.style.backgroundColor = "var(--color-surface-dark)";
|
clone.style.backgroundColor = "var(--color-surface-dark)";
|
||||||
clone.style.color = "var(--color-text-dark)";
|
clone.style.color = "var(--color-text-dark)";
|
||||||
|
|
@ -192,12 +193,9 @@
|
||||||
showNewList = false;
|
showNewList = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function openSettings() {
|
|
||||||
showSettings = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeSettings() {
|
function closeSettings() {
|
||||||
showSettings = false;
|
showSettings = false;
|
||||||
|
settingsWorkspace = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleHeaderMouseDown(e: MouseEvent) {
|
function handleHeaderMouseDown(e: MouseEvent) {
|
||||||
|
|
@ -256,7 +254,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
<p class="truncate text-sm">{name}</p>
|
<p class="truncate text-sm">{name}</p>
|
||||||
<p class="truncate text-xs opacity-40">{ws?.path ?? ""}</p>
|
<p class="truncate text-xs opacity-40">{ws?.mode === "webdav" ? ws.webdav_url ?? "WebDAV" : ws?.path ?? ""}</p>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<div class="relative shrink-0" data-ws-menu>
|
<div class="relative shrink-0" data-ws-menu>
|
||||||
|
|
@ -270,6 +268,15 @@
|
||||||
</button>
|
</button>
|
||||||
{#if wsMenuName === name}
|
{#if wsMenuName === name}
|
||||||
<div class="absolute right-0 top-full z-40 mt-1 min-w-[140px] rounded-lg border border-border-light bg-surface-light py-1 shadow-lg dark:border-border-dark dark:bg-surface-dark">
|
<div class="absolute right-0 top-full z-40 mt-1 min-w-[140px] rounded-lg border border-border-light bg-surface-light py-1 shadow-lg dark:border-border-dark dark:bg-surface-dark">
|
||||||
|
<button
|
||||||
|
onclick={() => { wsMenuName = null; settingsWorkspace = name; showSettings = true; showWorkspacePicker = false; }}
|
||||||
|
class="flex w-full items-center gap-2 px-3 py-2 text-left text-sm 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="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
Settings
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onclick={() => { wsMenuName = null; confirmRemoveWorkspace = name; }}
|
onclick={() => { wsMenuName = null; confirmRemoveWorkspace = name; }}
|
||||||
class="flex w-full items-center gap-2 px-3 py-2 text-left text-sm text-danger hover:bg-black/5 dark:hover:bg-white/10"
|
class="flex w-full items-center gap-2 px-3 py-2 text-left text-sm text-danger hover:bg-black/5 dark:hover:bg-white/10"
|
||||||
|
|
@ -347,20 +354,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Settings -->
|
|
||||||
<button
|
|
||||||
onclick={openSettings}
|
|
||||||
class="flex shrink-0 items-center gap-2 border-t border-border-light px-5 py-3 text-sm opacity-50 hover:bg-black/5 hover:opacity-80 dark:border-border-dark dark:hover:bg-white/10"
|
|
||||||
>
|
|
||||||
<svg class="h-4.5 w-4.5" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Settings
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Main content panel -->
|
<!-- Main content panel -->
|
||||||
|
|
@ -632,7 +625,7 @@
|
||||||
class="relative flex h-full w-full flex-col overflow-hidden rounded-2xl bg-surface-light transition-transform duration-200 dark:bg-surface-dark {showSettings ? 'scale-100' : 'scale-95'}"
|
class="relative flex h-full w-full flex-col overflow-hidden rounded-2xl bg-surface-light transition-transform duration-200 dark:bg-surface-dark {showSettings ? 'scale-100' : 'scale-95'}"
|
||||||
style="border: 1px solid rgba(255,255,255,0.1); box-shadow: 0 25px 60px rgba(0,0,0,0.7), 0 10px 20px rgba(0,0,0,0.5)"
|
style="border: 1px solid rgba(255,255,255,0.1); box-shadow: 0 25px 60px rgba(0,0,0,0.7), 0 10px 20px rgba(0,0,0,0.5)"
|
||||||
>
|
>
|
||||||
<SettingsScreen onclose={closeSettings} />
|
<SettingsScreen onclose={closeSettings} workspaceName={settingsWorkspace ?? app.config?.current_workspace ?? ""} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue