diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 165426c..17ac64f 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -121,3 +121,10 @@ pub async fn config_delete_connection(id: String) -> Result<(), String> { all.retain(|c| c.id != id); save_connections(&all) } + +#[tauri::command] +pub async fn config_reorder_connections(ids: Vec) -> Result<(), String> { + let mut all = load_connections()?; + all.sort_by_key(|c| ids.iter().position(|id| id == &c.id).unwrap_or(usize::MAX)); + save_connections(&all) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index be1edd8..1c8e958 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -49,6 +49,7 @@ pub fn run() { config::config_list_connections, config::config_save_connection, config::config_delete_connection, + config::config_reorder_connections, logger::logger_open_dir, logger::logger_dir_path, logger::logs_list, diff --git a/src/components/ButtonEditor.tsx b/src/components/ButtonEditor.tsx index b2a3e65..247a0c9 100644 --- a/src/components/ButtonEditor.tsx +++ b/src/components/ButtonEditor.tsx @@ -1,8 +1,9 @@ -import { createSignal, For, Show } from "solid-js"; +import { createSignal, For, onCleanup, onMount, Show } from "solid-js"; import { CloseX } from "./CloseX"; import { buttons, loadButtons, + moveButton, newButtonId, removeButton, saveButton, @@ -30,6 +31,22 @@ export function ButtonEditor(props: Props) { loadButtons(); + // ESC closes the dialog (or backs out of the edit form). + onMount(() => { + const onKey = (e: KeyboardEvent) => { + if (e.key !== "Escape") return; + if (editing()) { + e.preventDefault(); + setEditing(null); + } else { + e.preventDefault(); + props.onClose(); + } + }; + document.addEventListener("keydown", onKey); + onCleanup(() => document.removeEventListener("keydown", onKey)); + }); + function startEdit(b: CommandButton) { setEditing({ ...b }); } @@ -57,8 +74,26 @@ export function ButtonEditor(props: Props) { 0} fallback={
No buttons. Click + New.
}> - {(b) => ( + {(b, i) => (
+
+ + +
{b.icon ? `${b.icon} ` : ""} @@ -173,3 +208,21 @@ const btn = { cursor: "pointer", "font-weight": 600, } as const; + +const reorderCol = { + display: "flex", + "flex-direction": "column", + gap: "1px", + "margin-right": "4px", +} as const; + +const arrowBtn = { + background: "transparent", + color: "#cdd6f4", + border: "1px solid #45475a", + "border-radius": "4px", + padding: "1px 6px", + "font-size": "9px", + "line-height": "1", + cursor: "pointer", +} as const; diff --git a/src/components/ConnectionDialog.tsx b/src/components/ConnectionDialog.tsx index 2d9db4b..45f07a1 100644 --- a/src/components/ConnectionDialog.tsx +++ b/src/components/ConnectionDialog.tsx @@ -1,10 +1,11 @@ -import { createSignal, For, Show } from "solid-js"; +import { createSignal, For, onCleanup, onMount, Show } from "solid-js"; import { C, overlayStyle as baseOverlay, dialogStyle as baseDialog, inputStyle, btnPrimary, btnSecondary, btnDanger } from "../theme"; import { CloseX } from "./CloseX"; import { connections, defaultLocalShell, deleteConnection, + moveConnection, newConnectionId, upsertConnection, } from "../stores/connections"; @@ -32,6 +33,26 @@ export function ConnectionDialog(props: Props) { const [editing, setEditing] = createSignal(null); const [pwPrompt, setPwPrompt] = createSignal<{ conn: Connection; pw: string } | null>(null); + // ESC closes the dialog. When a sub-state (edit form / password prompt) + // is open, ESC backs out of that state first instead of closing outright. + onMount(() => { + const onKey = (e: KeyboardEvent) => { + if (e.key !== "Escape") return; + if (editing()) { + e.preventDefault(); + setEditing(null); + } else if (pwPrompt()) { + e.preventDefault(); + setPwPrompt(null); + } else { + e.preventDefault(); + props.onClose(); + } + }; + document.addEventListener("keydown", onKey); + onCleanup(() => document.removeEventListener("keydown", onKey)); + }); + function startEdit(c: Connection) { setEditing({ ...c }); } @@ -75,8 +96,26 @@ export function ConnectionDialog(props: Props) { fallback={
No saved connections. Click + New.
} > - {(c) => ( + {(c, i) => (
+
+ + +
{c.name}
@@ -208,6 +247,25 @@ const rowStyle = { "border-radius": "8px", background: C.bg3, "margin-bottom": "6px", + "user-select": "none", +} as const; + +const reorderColStyle = { + display: "flex", + "flex-direction": "column", + gap: "1px", + "margin-right": "4px", +} as const; + +const arrowBtn = { + background: "transparent", + color: C.text2, + border: `1px solid ${C.border}`, + "border-radius": "4px", + padding: "1px 6px", + "font-size": "9px", + "line-height": "1", + cursor: "pointer", } as const; const input = inputStyle; diff --git a/src/ipc/api.ts b/src/ipc/api.ts index 46f33cb..aa91fda 100644 --- a/src/ipc/api.ts +++ b/src/ipc/api.ts @@ -68,6 +68,9 @@ export const api = { deleteConnection: (id: string) => invoke("config_delete_connection", { id }), + reorderConnections: (ids: string[]) => + invoke("config_reorder_connections", { ids }), + // Logging loggerOpenDir: () => invoke("logger_open_dir"), loggerDirPath: () => invoke("logger_dir_path"), diff --git a/src/stores/buttons.ts b/src/stores/buttons.ts index b102ca5..88dbfb7 100644 --- a/src/stores/buttons.ts +++ b/src/stores/buttons.ts @@ -28,6 +28,21 @@ export async function reorderButtons(ids: string[]) { await loadButtons(); } +export async function moveButton(id: string, delta: -1 | 1) { + const arr = [...buttons()]; + const idx = arr.findIndex((b) => b.id === id); + if (idx < 0) return; + const newIdx = idx + delta; + if (newIdx < 0 || newIdx >= arr.length) return; + const [moved] = arr.splice(idx, 1); + arr.splice(newIdx, 0, moved); + setButtons(arr); + await api.buttonsReorder(arr.map((b) => b.id)).catch((e) => { + console.error("moveButton persist failed", e); + loadButtons(); + }); +} + export function newButtonId(): string { return `btn-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`; } diff --git a/src/stores/connections.ts b/src/stores/connections.ts index 1d2d747..4e4d46b 100644 --- a/src/stores/connections.ts +++ b/src/stores/connections.ts @@ -24,6 +24,36 @@ export async function deleteConnection(id: string) { await loadConnections(); } +export async function reorderConnections(sourceId: string, targetId: string | null) { + if (sourceId === targetId) return; + const arr = [...connections()]; + const fromIdx = arr.findIndex((c) => c.id === sourceId); + if (fromIdx < 0) return; + const [moved] = arr.splice(fromIdx, 1); + if (targetId === null) { + arr.push(moved); + } else { + const toIdx = arr.findIndex((c) => c.id === targetId); + if (toIdx < 0) arr.push(moved); + else arr.splice(toIdx, 0, moved); + } + setConnections(arr); + await api.reorderConnections(arr.map((c) => c.id)).catch((e) => { + console.error("reorderConnections persist failed", e); + loadConnections(); + }); +} + +export async function moveConnection(id: string, delta: -1 | 1) { + const arr = connections(); + const idx = arr.findIndex((c) => c.id === id); + if (idx < 0) return; + const newIdx = idx + delta; + if (newIdx < 0 || newIdx >= arr.length) return; + const targetId = delta < 0 ? arr[newIdx].id : (newIdx + 1 < arr.length ? arr[newIdx + 1].id : null); + await reorderConnections(id, targetId); +} + export function newConnectionId(): string { return `conn-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; }