fix(gui): wire sync credentials, fix memory leak, race condition, add delete confirmations
- Add load_credentials Tauri command and use it in triggerSync() instead of passing empty username/password strings - Replace raw __TAURI_INTERNALS__.invoke() with proper invoke import in SettingsScreen - Wrap window event listeners in $effect() with cleanup to prevent memory leak on component remount - Return created Task from createTask() and use it directly in NewTaskInput instead of guessing from array index - Add confirm() dialogs before deleting tasks, lists, and workspaces
This commit is contained in:
parent
a54e427cd9
commit
c138a8bcf6
|
|
@ -310,6 +310,11 @@ fn store_credentials(
|
||||||
webdav::store_credentials(&domain, &username, &password).map_err(|e| e.to_string())
|
webdav::store_credentials(&domain, &username, &password).map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn load_credentials(domain: String) -> Result<(String, String), String> {
|
||||||
|
webdav::load_credentials(&domain).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn test_webdav_connection(
|
async fn test_webdav_connection(
|
||||||
url: String,
|
url: String,
|
||||||
|
|
@ -373,6 +378,7 @@ pub fn run() {
|
||||||
reorder_task,
|
reorder_task,
|
||||||
set_webdav_config,
|
set_webdav_config,
|
||||||
store_credentials,
|
store_credentials,
|
||||||
|
load_credentials,
|
||||||
test_webdav_connection,
|
test_webdav_connection,
|
||||||
sync_workspace,
|
sync_workspace,
|
||||||
])
|
])
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,8 @@
|
||||||
|
|
||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
if (!title.trim()) return;
|
if (!title.trim()) return;
|
||||||
await app.createTask(title.trim(), description.trim() || undefined);
|
const created = await app.createTask(title.trim(), description.trim() || undefined);
|
||||||
if (dueDate && app.tasks.length > 0) {
|
if (dueDate && created) {
|
||||||
const created = app.tasks[app.tasks.length - 1];
|
|
||||||
await app.updateTask({ ...created, due_date: dueDate, updated_at: new Date().toISOString() });
|
await app.updateTask({ ...created, due_date: dueDate, updated_at: new Date().toISOString() });
|
||||||
}
|
}
|
||||||
title = "";
|
title = "";
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@
|
||||||
|
|
||||||
async function handleDelete() {
|
async function handleDelete() {
|
||||||
showMenu = false;
|
showMenu = false;
|
||||||
|
if (!confirm(`Delete task "${task.title}"?`)) return;
|
||||||
await app.deleteTask(task.id);
|
await app.deleteTask(task.id);
|
||||||
onback();
|
onback();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
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 }: { onclose?: () => void } = $props();
|
||||||
|
|
@ -11,7 +12,7 @@
|
||||||
async function testConnection() {
|
async function testConnection() {
|
||||||
testStatus = "testing";
|
testStatus = "testing";
|
||||||
try {
|
try {
|
||||||
await (globalThis as any).__TAURI_INTERNALS__.invoke("test_webdav_connection", {
|
await invoke("test_webdav_connection", {
|
||||||
url: webdavUrl,
|
url: webdavUrl,
|
||||||
username: webdavUser,
|
username: webdavUser,
|
||||||
password: webdavPass,
|
password: webdavPass,
|
||||||
|
|
@ -24,18 +25,19 @@
|
||||||
|
|
||||||
async function saveWebdav() {
|
async function saveWebdav() {
|
||||||
if (!app.config?.current_workspace || !webdavUrl.trim()) return;
|
if (!app.config?.current_workspace || !webdavUrl.trim()) return;
|
||||||
await (globalThis as any).__TAURI_INTERNALS__.invoke("set_webdav_config", {
|
await invoke("set_webdav_config", {
|
||||||
workspaceName: app.config.current_workspace,
|
workspaceName: app.config.current_workspace,
|
||||||
webdavUrl: webdavUrl.trim(),
|
webdavUrl: webdavUrl.trim(),
|
||||||
});
|
});
|
||||||
if (webdavUser && webdavPass) {
|
if (webdavUser && webdavPass) {
|
||||||
const domain = new URL(webdavUrl).hostname;
|
const domain = new URL(webdavUrl).hostname;
|
||||||
await (globalThis as any).__TAURI_INTERNALS__.invoke("store_credentials", {
|
await invoke("store_credentials", {
|
||||||
domain,
|
domain,
|
||||||
username: webdavUser,
|
username: webdavUser,
|
||||||
password: webdavPass,
|
password: webdavPass,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
await app.loadConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -39,9 +39,6 @@
|
||||||
if (wsMenuName && !target.closest("[data-ws-menu]")) wsMenuName = null;
|
if (wsMenuName && !target.closest("[data-ws-menu]")) wsMenuName = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
window.addEventListener("mousedown", handleWindowClick);
|
|
||||||
}
|
|
||||||
let newListName = $state("");
|
let newListName = $state("");
|
||||||
let showCompleted = $state(false);
|
let showCompleted = $state(false);
|
||||||
let completedVisible = $state(false);
|
let completedVisible = $state(false);
|
||||||
|
|
@ -52,13 +49,19 @@
|
||||||
let resizing = $state(false);
|
let resizing = $state(false);
|
||||||
let resizeTimer: ReturnType<typeof setTimeout>;
|
let resizeTimer: ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
if (typeof window !== "undefined") {
|
$effect(() => {
|
||||||
window.addEventListener("resize", () => {
|
window.addEventListener("mousedown", handleWindowClick);
|
||||||
|
const handleResize = () => {
|
||||||
resizing = true;
|
resizing = true;
|
||||||
clearTimeout(resizeTimer);
|
clearTimeout(resizeTimer);
|
||||||
resizeTimer = setTimeout(() => (resizing = false), 150);
|
resizeTimer = setTimeout(() => (resizing = false), 150);
|
||||||
|
};
|
||||||
|
window.addEventListener("resize", handleResize);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("mousedown", handleWindowClick);
|
||||||
|
window.removeEventListener("resize", handleResize);
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
async function handleNewList() {
|
async function handleNewList() {
|
||||||
if (!newListName.trim()) return;
|
if (!newListName.trim()) return;
|
||||||
|
|
@ -69,6 +72,8 @@
|
||||||
|
|
||||||
async function handleDeleteList(id: string) {
|
async function handleDeleteList(id: string) {
|
||||||
listMenuId = null;
|
listMenuId = null;
|
||||||
|
const list = app.lists.find(l => l.id === id);
|
||||||
|
if (!confirm(`Delete list "${list?.title ?? id}" and all its tasks?`)) return;
|
||||||
await app.deleteList(id);
|
await app.deleteList(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -270,7 +275,7 @@
|
||||||
{#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
|
<button
|
||||||
onclick={() => { wsMenuName = null; app.removeWorkspace(name); }}
|
onclick={() => { wsMenuName = null; if (confirm(`Remove workspace "${name}"? (Files remain on disk)`)) app.removeWorkspace(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"
|
||||||
>
|
>
|
||||||
<svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
<svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
|
|
||||||
|
|
@ -140,8 +140,8 @@ async function deleteList(id: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createTask(title: string, description?: string) {
|
async function createTask(title: string, description?: string): Promise<Task | null> {
|
||||||
if (!activeListId) return;
|
if (!activeListId) return null;
|
||||||
try {
|
try {
|
||||||
const task = await invoke<Task>("create_task", {
|
const task = await invoke<Task>("create_task", {
|
||||||
listId: activeListId,
|
listId: activeListId,
|
||||||
|
|
@ -150,8 +150,10 @@ async function createTask(title: string, description?: string) {
|
||||||
});
|
});
|
||||||
tasks = [...tasks, task];
|
tasks = [...tasks, task];
|
||||||
error = null;
|
error = null;
|
||||||
|
return task;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = String(e);
|
error = String(e);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -214,11 +216,13 @@ async function triggerSync() {
|
||||||
syncing = true;
|
syncing = true;
|
||||||
error = null;
|
error = null;
|
||||||
try {
|
try {
|
||||||
|
const domain = new URL(ws.webdav_url).hostname;
|
||||||
|
const [username, password] = await invoke<[string, string]>("load_credentials", { domain });
|
||||||
const result = await invoke<SyncResult>("sync_workspace", {
|
const result = await invoke<SyncResult>("sync_workspace", {
|
||||||
workspacePath: ws.path,
|
workspacePath: ws.path,
|
||||||
webdavUrl: ws.webdav_url,
|
webdavUrl: ws.webdav_url,
|
||||||
username: "",
|
username,
|
||||||
password: "",
|
password,
|
||||||
});
|
});
|
||||||
if (result.errors.length > 0) {
|
if (result.errors.length > 0) {
|
||||||
error = result.errors.join("; ");
|
error = result.errors.join("; ");
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue