diff --git a/src/components/common/CommandPalette.vue b/src/components/common/CommandPalette.vue index 11f051e..25112d6 100644 --- a/src/components/common/CommandPalette.vue +++ b/src/components/common/CommandPalette.vue @@ -116,9 +116,9 @@ const connectionStore = useConnectionStore(); const sessionStore = useSessionStore(); const emit = defineEmits<{ - (e: "open-import"): void; - (e: "open-settings"): void; - (e: "open-new-connection", protocol?: "ssh" | "rdp"): void; + "open-import": []; + "open-settings": []; + "open-new-connection": [protocol?: "ssh" | "rdp"]; }>(); const actions: PaletteAction[] = [ diff --git a/src/components/common/SettingsModal.vue b/src/components/common/SettingsModal.vue index 82ab4bd..1e4f545 100644 --- a/src/components/common/SettingsModal.vue +++ b/src/components/common/SettingsModal.vue @@ -422,9 +422,16 @@ watch( () => settings.value.defaultProtocol, (val) => invoke("set_setting", { key: "default_protocol", value: val }).catch(console.error), ); +let sidebarWidthDebounce: ReturnType; watch( () => settings.value.sidebarWidth, - (val) => invoke("set_setting", { key: "sidebar_width", value: String(val) }).catch(console.error), + (val) => { + clearTimeout(sidebarWidthDebounce); + sidebarWidthDebounce = setTimeout( + () => invoke("set_setting", { key: "sidebar_width", value: String(val) }).catch(console.error), + 300, + ); + }, ); watch( () => settings.value.terminalTheme, diff --git a/src/components/common/StatusBar.vue b/src/components/common/StatusBar.vue index c272b26..2a44ae2 100644 --- a/src/components/common/StatusBar.vue +++ b/src/components/common/StatusBar.vue @@ -47,7 +47,7 @@ const connectionStore = useConnectionStore(); const activeThemeName = ref("Default"); const emit = defineEmits<{ - (e: "open-theme-picker"): void; + "open-theme-picker": []; }>(); const connectionInfo = computed(() => { diff --git a/src/components/sidebar/SidebarToggle.vue b/src/components/sidebar/SidebarToggle.vue index 99635af..d3d5c55 100644 --- a/src/components/sidebar/SidebarToggle.vue +++ b/src/components/sidebar/SidebarToggle.vue @@ -5,11 +5,11 @@ :key="tab.id" class="flex-1 py-2 text-xs font-medium text-center transition-colors cursor-pointer" :class=" - modelValue === tab.id + model === tab.id ? 'text-[var(--wraith-accent-blue)] border-b-2 border-[var(--wraith-accent-blue)]' : 'text-[var(--wraith-text-muted)] hover:text-[var(--wraith-text-secondary)]' " - @click="emit('update:modelValue', tab.id)" + @click="model = tab.id" > {{ tab.label }} @@ -24,11 +24,5 @@ const tabs = [ { id: "sftp" as const, label: "SFTP" }, ]; -defineProps<{ - modelValue: SidebarTab; -}>(); - -const emit = defineEmits<{ - "update:modelValue": [tab: SidebarTab]; -}>(); +const model = defineModel(); diff --git a/src/components/tools/DockerPanel.vue b/src/components/tools/DockerPanel.vue index 6ee57b2..2ccc1a4 100644 --- a/src/components/tools/DockerPanel.vue +++ b/src/components/tools/DockerPanel.vue @@ -81,12 +81,16 @@ import { ref, onMounted } from "vue"; import { invoke } from "@tauri-apps/api/core"; +interface DockerContainer { id: string; name: string; image: string; status: string; ports: string; } +interface DockerImage { repository: string; tag: string; id: string; size: string; } +interface DockerVolume { name: string; driver: string; mountpoint: string; } + const props = defineProps<{ sessionId: string }>(); const tab = ref("containers"); -const containers = ref([]); -const images = ref([]); -const volumes = ref([]); +const containers = ref([]); +const images = ref([]); +const volumes = ref([]); const output = ref(""); async function refresh(): Promise { diff --git a/src/composables/useRdp.ts b/src/composables/useRdp.ts index 8bf1956..9f197e3 100644 --- a/src/composables/useRdp.ts +++ b/src/composables/useRdp.ts @@ -1,4 +1,5 @@ import { ref, onBeforeUnmount } from "vue"; +import type { Ref } from "vue"; import { invoke } from "@tauri-apps/api/core"; /** @@ -152,11 +153,11 @@ export function jsKeyToScancode(code: string): number | null { export interface UseRdpReturn { /** Whether the RDP session is connected (first frame received) */ - connected: ReturnType>; + connected: Ref; /** Whether keyboard capture is enabled */ - keyboardGrabbed: ReturnType>; + keyboardGrabbed: Ref; /** Whether clipboard sync is enabled */ - clipboardSync: ReturnType>; + clipboardSync: Ref; /** Fetch the current frame as RGBA ImageData */ fetchFrame: (sessionId: string, width: number, height: number) => Promise; /** Send a mouse event to the backend */ diff --git a/src/composables/useSftp.ts b/src/composables/useSftp.ts index cf2c2bf..e9600a6 100644 --- a/src/composables/useSftp.ts +++ b/src/composables/useSftp.ts @@ -24,6 +24,11 @@ export interface UseSftpReturn { // Persist the last browsed path per session so switching tabs restores position const sessionPaths: Record = {}; +/** Remove a session's saved path from the module-level cache. Call on session close. */ +export function cleanupSession(sessionId: string): void { + delete sessionPaths[sessionId]; +} + /** * Composable that manages SFTP file browsing state. * Accepts a reactive session ID ref so it reinitializes on tab switch diff --git a/src/stores/connection.store.ts b/src/stores/connection.store.ts index a74b7dc..5c2f50f 100644 --- a/src/stores/connection.store.ts +++ b/src/stores/connection.store.ts @@ -51,22 +51,33 @@ export const useConnectionStore = defineStore("connection", () => { ); }); + /** Memoized map of groupId → filtered connections. Recomputes only when connections or searchQuery change. */ + const connectionsByGroupMap = computed>(() => { + const q = searchQuery.value.toLowerCase().trim(); + const map: Record = {}; + for (const c of connections.value) { + if (c.groupId === null) continue; + if (q) { + const match = + c.name.toLowerCase().includes(q) || + c.hostname.toLowerCase().includes(q) || + c.tags?.some((t) => t.toLowerCase().includes(q)); + if (!match) continue; + } + if (!map[c.groupId]) map[c.groupId] = []; + map[c.groupId].push(c); + } + return map; + }); + /** Get connections belonging to a specific group. */ function connectionsByGroup(groupId: number): Connection[] { - const q = searchQuery.value.toLowerCase().trim(); - const groupConns = connections.value.filter((c) => c.groupId === groupId); - if (!q) return groupConns; - return groupConns.filter( - (c) => - c.name.toLowerCase().includes(q) || - c.hostname.toLowerCase().includes(q) || - c.tags?.some((t) => t.toLowerCase().includes(q)), - ); + return connectionsByGroupMap.value[groupId] ?? []; } /** Check if a group has any matching connections (for search filtering). */ function groupHasResults(groupId: number): boolean { - return connectionsByGroup(groupId).length > 0; + return (connectionsByGroupMap.value[groupId]?.length ?? 0) > 0; } /** Load connections from the Rust backend. */ @@ -101,6 +112,7 @@ export const useConnectionStore = defineStore("connection", () => { groups, searchQuery, filteredConnections, + connectionsByGroupMap, connectionsByGroup, groupHasResults, loadConnections,