From d5a2afdea56a03fa52fe81f0b53f5eb48553a473 Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 28 Sep 2026 16:41:16 +0200 Subject: [PATCH] feat: add locking styles --- .../components/DSP408Panel/DSP408Panel.css | 6 + .../components/DSP408Panel/DSP408Panel.tsx | 50 +++-- .../PasswordDialog/PasswordDialog.css | 204 ++++++++++++++++++ .../PasswordDialog/PasswordDialog.tsx | 116 ++++++++++ src/features/dsp408/dsp408.ts | 67 ++++-- src/types.ts | 2 + 6 files changed, 408 insertions(+), 37 deletions(-) create mode 100644 src/features/dsp408/components/PasswordDialog/PasswordDialog.css create mode 100644 src/features/dsp408/components/PasswordDialog/PasswordDialog.tsx diff --git a/src/features/dsp408/components/DSP408Panel/DSP408Panel.css b/src/features/dsp408/components/DSP408Panel/DSP408Panel.css index cf69751..e76fad7 100644 --- a/src/features/dsp408/components/DSP408Panel/DSP408Panel.css +++ b/src/features/dsp408/components/DSP408Panel/DSP408Panel.css @@ -29,3 +29,9 @@ .app-root:has(.sidebar-rail--expanded) .dsp-loading-overlay { left: var(--sidebar-width-expanded); } + +.app-root { + position: relative; + width: 100%; + height: 100%; +} diff --git a/src/features/dsp408/components/DSP408Panel/DSP408Panel.tsx b/src/features/dsp408/components/DSP408Panel/DSP408Panel.tsx index c2c7885..4ae053d 100644 --- a/src/features/dsp408/components/DSP408Panel/DSP408Panel.tsx +++ b/src/features/dsp408/components/DSP408Panel/DSP408Panel.tsx @@ -28,6 +28,7 @@ import { DSP408 } from '../../../../types'; import { SidebarId } from '../../constants'; import './DSP408Panel.css'; import MiscellaneousPanel from '../MiscellaneousSection/MiscellaneousSection'; +import { PasswordDialog } from '../PasswordDialog/PasswordDialog'; function DSP408Panel({ dsp, api }: { dsp: DSP408; api: DSP408Api }) { const [activeSidebar, setActiveSidebar] = useState('overview'); @@ -187,27 +188,36 @@ function DSP408Panel({ dsp, api }: { dsp: DSP408; api: DSP408Api }) { return (
- setSidebarExpanded((v) => !v)} - /> + setSidebarExpanded((v) => !v)} + /> - -
{renderTab()}
-
+ +
{renderTab()}
+
+ + + {dsp.passwordRequired && ( + api.connectWithPassword(pw)} + onCancel={() => api.cancelPassword()} + /> + )}
); } diff --git a/src/features/dsp408/components/PasswordDialog/PasswordDialog.css b/src/features/dsp408/components/PasswordDialog/PasswordDialog.css new file mode 100644 index 0000000..f273ed1 --- /dev/null +++ b/src/features/dsp408/components/PasswordDialog/PasswordDialog.css @@ -0,0 +1,204 @@ +.password-dialog-overlay { + position: fixed; + inset: 0; + + display: flex; + align-items: center; + justify-content: center; + + padding: var(--space-4); + + background: rgba(0, 0, 0, 0.55); + + z-index: 1000; + + user-select: none; + -webkit-user-select: none; +} + +.password-dialog { + width: 340px; + max-width: 100%; + + display: flex; + flex-direction: column; + + animation: password-dialog-enter 120ms ease-out; +} + +/* Header */ + +.password-dialog__header { + display: flex; + align-items: flex-start; + justify-content: space-between; + + gap: var(--space-3); + + min-width: 0; +} + +.password-dialog__header-text { + flex: 1; + min-width: 0; +} + +.password-dialog__title { + margin: 0; + + font-size: var(--font-lg); + font-weight: var(--font-weight-semibold); + color: var(--text-primary); + + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.password-dialog__type { + margin-top: var(--space-1); + + font-size: var(--font-sm); + color: var(--text-muted); + + font-family: var(--font-mono); + letter-spacing: 0.03em; + + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Content */ + +.password-dialog__content { + margin-top: var(--space-5); +} + +.password-dialog__field { + display: flex; + flex-direction: column; + gap: var(--space-2); +} + +.password-dialog__field label { + font-size: var(--font-sm); + color: var(--text-muted); +} + +/* Password */ + +.password-dialog__password-wrap { + position: relative; + width: 100%; +} + +.password-dialog__password-wrap input { + width: 100%; + height: var(--card-control-height); + + box-sizing: border-box; + + padding: 0 var(--space-3); + + padding-right: calc(var(--space-3) + 28px); + + background: var(--bg-panel); + color: var(--text-primary); + + border: var(--border-width) solid var(--border-hairline); + border-radius: var(--radius-sm); + + font-family: var(--font-mono); + font-size: var(--font-sm); + + outline: none; + + appearance: none; + -webkit-appearance: none; + + transition: + border-color var(--dur-base) var(--ease-standard), + box-shadow var(--dur-base) var(--ease-standard); +} + +.password-dialog__password-wrap input:focus { + border-color: var(--accent-brand); + + box-shadow: + 0 0 0 2px + color-mix(in srgb, var(--accent-brand) 20%, transparent); +} + +.password-dialog__password-wrap input::placeholder { + color: var(--text-muted); + opacity: 0.5; +} + +.password-dialog__password-toggle { + position: absolute; + top: 50%; + right: var(--space-2); + + transform: translateY(-50%); + + display: flex; + align-items: center; + justify-content: center; + + width: 28px; + height: 28px; + + padding: 0; + + border: none; + background: transparent; + + color: var(--accent-brand); + + cursor: pointer; +} + +.password-dialog__password-toggle:hover { + color: var(--text-muted); +} + +.password-dialog__password-toggle svg { + width: auto; + height: 65%; +} + +/* Actions */ + +.password-dialog__actions { + margin-top: var(--space-5); + + display: flex; + justify-content: flex-end; + align-items: center; + + gap: var(--space-3); +} + +/* Animation */ + +@keyframes password-dialog-enter { + from { + opacity: 0; + transform: translateY(4px) scale(0.98); + } + + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +.password-dialog__password-wrap input::-ms-reveal, +.password-dialog__password-wrap input::-ms-clear { + display: none; +} + +.password-dialog__password-wrap input::-webkit-credentials-auto-fill-button { + display: none; +} diff --git a/src/features/dsp408/components/PasswordDialog/PasswordDialog.tsx b/src/features/dsp408/components/PasswordDialog/PasswordDialog.tsx new file mode 100644 index 0000000..a747d6d --- /dev/null +++ b/src/features/dsp408/components/PasswordDialog/PasswordDialog.tsx @@ -0,0 +1,116 @@ +import { useState } from 'react'; + +import Button from '../../../../components/Button/Button'; +import Card from '../../../../components/Card/Card'; +import { EyeIcon, EyeOffIcon } from '../../../../assets/icons/EyesIcons'; + +import './PasswordDialog.css'; + +export function PasswordDialog({ + name, + onSubmit, + onCancel, +}: { + name: string; + onSubmit: (password: string) => void; + onCancel: () => void; +}) { + const [password, setPassword] = useState(''); + const [isPasswordVisible, setIsPasswordVisible] = useState(false); + + const handleSubmit = () => { + if (password.length === 4) { + onSubmit(password); + } + }; + + const handlePasswordChange = (value: string) => { + if (/^[\x20-\x7E]*$/.test(value) && value.length <= 4) { + setPassword(value); + } + }; + + const handleOverlayClick = (e: React.MouseEvent) => { + if (e.target === e.currentTarget) { + onCancel(); + } + }; + + return ( +
+
+ +
+
+

+ Device Locked +

+ +
+ {name} +
+
+
+ +
+
+ + +
+ handlePasswordChange(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter' && password.length === 4) { + handleSubmit(); + } + + if (e.key === 'Escape') { + onCancel(); + } + }} + /> + + +
+
+
+ +
+ + + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/features/dsp408/dsp408.ts b/src/features/dsp408/dsp408.ts index a6aa8a9..a72b8e3 100644 --- a/src/features/dsp408/dsp408.ts +++ b/src/features/dsp408/dsp408.ts @@ -127,16 +127,61 @@ export async function create(params: { } export function forDSP408(dsp: DSP408, setDsps: SetDsps, notify: NotifyFn) { - async function updateDSPState(): Promise { + async function updateDSPState(): Promise { try { const state = await invoke('get_dsp_state', { id: dsp.id }); updateDSP(setDsps, dsp.id, (d) => ({ ...d, state }) as DSP408); + + return state; } catch (err) { notify(`Failed to fetch state: ${String(err)}`, 'error', dsp.name); throw err; } } + const isLocked = (s: DSP408['state']) => s.flags.is_locked; + + async function abortLocked() { + await invoke('disconnect_dsp408', { id: dsp.id }).catch(() => {}); + updateDSP(setDsps, dsp.id, (d) => ({ ...d, status: 'disconnected', passwordRequired: true })); + } + + async function connectInternal(password?: string) { + updateDSP(setDsps, dsp.id, (d) => ({ ...d, status: 'connecting', passwordRequired: false })); + try { + await invoke('connect_dsp408', { id: dsp.id }); + + let state = await updateDSPState(); + + if (isLocked(state)) { + if (password === undefined) { + await abortLocked(); + return; + } + + try { + await invoke('unlock_device', { id: dsp.id, password }); // adjust name/signature + } catch { + // wrong password + } + + state = await updateDSPState(); + if (isLocked(state)) { + await abortLocked(); + notify('Wrong password', 'error', dsp.name); + return; + } + } + + updateDSP(setDsps, dsp.id, (d) => ({ ...d, status: 'connected', pollingPaused: false })); + notify('Connected', 'success', dsp.name); + startPolling(dsp, setDsps); + } catch (err) { + updateDSP(setDsps, dsp.id, (d) => ({ ...d, status: 'disconnected' })); + notify(`Connection failed: ${String(err)}`, 'error', dsp.name); + } + } + return { type: 'DSP408' as const, @@ -155,22 +200,10 @@ export function forDSP408(dsp: DSP408, setDsps: SetDsps, notify: NotifyFn) { updateDSPState, - async connect() { - updateDSP(setDsps, dsp.id, (d) => ({ ...d, status: 'connecting' })); - try { - await invoke('connect_dsp408', { id: dsp.id }); - - await updateDSPState(); - - updateDSP(setDsps, dsp.id, (d) => ({ ...d, status: 'connected', pollingPaused: false })); - - notify('Connected', 'success', dsp.name); - - startPolling(dsp, setDsps); - } catch (err) { - updateDSP(setDsps, dsp.id, (d) => ({ ...d, status: 'disconnected' })); - notify(`Connection failed: ${String(err)}`, 'error', dsp.name); - } + connect: () => connectInternal(), + connectWithPassword: (password: string) => connectInternal(password), + cancelPassword() { + updateDSP(setDsps, dsp.id, (d) => ({ ...d, passwordRequired: false })); }, async disconnect() { diff --git a/src/types.ts b/src/types.ts index 911abc2..8bd41ad 100644 --- a/src/types.ts +++ b/src/types.ts @@ -38,6 +38,8 @@ interface DSPBase { deviceId: number; mode: DSPMode; + passwordRequired?: boolean; + status: DSPStatus; pollingPaused: boolean;