20260619
This commit is contained in:
454
ui/session_dialog.slint
Normal file
454
ui/session_dialog.slint
Normal file
@@ -0,0 +1,454 @@
|
||||
import { Theme } from "theme.slint";
|
||||
import { LabeledInput, PrimaryButton, GhostButton } from "widgets.slint";
|
||||
|
||||
export struct SessionDraft {
|
||||
id: string,
|
||||
name: string,
|
||||
group: string,
|
||||
kind: string, // "ssh" | "serial" | "telnet"
|
||||
host: string,
|
||||
port: int,
|
||||
user: string,
|
||||
auth: string, // "password" | "key"
|
||||
password: string,
|
||||
private-key-path: string,
|
||||
proxy: string, // "" | "socks5://host:port" | "http://host:port"
|
||||
// Serial-only (ignored unless kind == "serial")
|
||||
serial-port: string,
|
||||
baud-rate: int,
|
||||
data-bits: int,
|
||||
stop-bits: int,
|
||||
parity: string, // "none" | "odd" | "even"
|
||||
flow-control: string, // "none" | "hardware" | "software"
|
||||
}
|
||||
|
||||
// A single segment in a segmented selector (auth toggle, baud presets, …).
|
||||
component SegOption inherits Rectangle {
|
||||
in property <string> label;
|
||||
in property <bool> active;
|
||||
callback clicked();
|
||||
height: 30px;
|
||||
horizontal-stretch: 1;
|
||||
border-radius: Theme.radius-sm;
|
||||
border-width: 1px;
|
||||
background: active ? Theme.accent : Theme.bg-root;
|
||||
border-color: active ? Theme.accent : Theme.border-subtle;
|
||||
Text {
|
||||
width: parent.width;
|
||||
height: parent.height;
|
||||
text: root.label;
|
||||
color: active ? white : Theme.text-primary;
|
||||
font-size: Theme.fs-md;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
TouchArea {
|
||||
mouse-cursor: pointer;
|
||||
clicked => { root.clicked(); }
|
||||
}
|
||||
}
|
||||
|
||||
component FolderOption inherits Rectangle {
|
||||
in property <string> label;
|
||||
in property <bool> active;
|
||||
callback clicked();
|
||||
|
||||
height: 30px;
|
||||
border-radius: Theme.radius-sm;
|
||||
border-width: 1px;
|
||||
border-color: active ? Theme.accent : Theme.border-subtle;
|
||||
background: active ? Theme.bg-active : Theme.bg-root;
|
||||
|
||||
HorizontalLayout {
|
||||
height: parent.height;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
spacing: 8px;
|
||||
Text {
|
||||
width: 18px;
|
||||
height: parent.height;
|
||||
text: "\u{E2C7}";
|
||||
font-family: "Material Icons";
|
||||
color: active ? Theme.accent : Theme.text-muted;
|
||||
font-size: Theme.fs-sm;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
Text {
|
||||
height: parent.height;
|
||||
text: root.label;
|
||||
color: Theme.text-primary;
|
||||
font-size: Theme.fs-md;
|
||||
vertical-alignment: center;
|
||||
horizontal-stretch: 1;
|
||||
overflow: elide;
|
||||
}
|
||||
if active : Text {
|
||||
width: 18px;
|
||||
height: parent.height;
|
||||
text: "\u{E5CA}";
|
||||
font-family: "Material Icons";
|
||||
color: Theme.accent;
|
||||
font-size: Theme.fs-sm;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
|
||||
TouchArea {
|
||||
mouse-cursor: pointer;
|
||||
clicked => { root.clicked(); }
|
||||
}
|
||||
}
|
||||
|
||||
// Modal dialog for creating / editing a session (SSH / Serial / Telnet).
|
||||
// The dialog is purely presentational — it stores values in mutable
|
||||
// properties and emits `submit` / `cancel` callbacks up to the main window.
|
||||
export component SessionDialog inherits Rectangle {
|
||||
in-out property <bool> is-open: false;
|
||||
in-out property <bool> is-editing: false;
|
||||
in-out property <string> draft-id;
|
||||
in-out property <string> draft-name;
|
||||
in-out property <string> draft-group: "Default";
|
||||
in-out property <string> draft-kind: "ssh";
|
||||
in-out property <string> draft-host;
|
||||
in-out property <string> draft-port: "22";
|
||||
in-out property <string> draft-user: "root";
|
||||
in-out property <string> draft-auth: "password";
|
||||
in-out property <string> draft-password;
|
||||
in-out property <string> draft-key-path;
|
||||
in-out property <string> draft-proxy;
|
||||
in-out property <string> draft-serial-port;
|
||||
in-out property <string> draft-baud: "115200";
|
||||
in-out property <string> draft-data-bits: "8";
|
||||
in-out property <string> draft-stop-bits: "1";
|
||||
in-out property <string> draft-parity: "none";
|
||||
in-out property <string> draft-flow: "none";
|
||||
in property <[string]> folders;
|
||||
|
||||
callback submit(SessionDraft);
|
||||
callback cancel();
|
||||
callback pick-key-file(); // open a native file picker for the private key
|
||||
|
||||
visible: is-open;
|
||||
background: #000000aa;
|
||||
|
||||
// Swallow clicks on the backdrop
|
||||
TouchArea {
|
||||
width: 100%; height: 100%;
|
||||
clicked => { root.cancel(); }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: min(440px, parent.width - 48px);
|
||||
height: min(620px, parent.height - 48px);
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
background: Theme.bg-panel;
|
||||
border-radius: Theme.radius-lg;
|
||||
border-width: 1px;
|
||||
border-color: Theme.border-strong;
|
||||
drop-shadow-blur: 24px;
|
||||
drop-shadow-color: #00000080;
|
||||
|
||||
// Prevent backdrop click from propagating when user clicks inside the card
|
||||
TouchArea {
|
||||
width: 100%; height: 100%;
|
||||
}
|
||||
|
||||
VerticalLayout {
|
||||
padding: 20px;
|
||||
spacing: 12px;
|
||||
|
||||
Text {
|
||||
text: is-editing ? @tr("Edit session") : @tr("New session");
|
||||
color: Theme.text-primary;
|
||||
font-size: Theme.fs-xl;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
Flickable {
|
||||
vertical-stretch: 1;
|
||||
viewport-width: self.width;
|
||||
viewport-height: max(self.height, form-col.preferred-height);
|
||||
|
||||
form-col := VerticalLayout {
|
||||
width: parent.width;
|
||||
alignment: start;
|
||||
spacing: 12px;
|
||||
|
||||
// Connection type selector (always shown).
|
||||
VerticalLayout {
|
||||
spacing: 4px;
|
||||
Text {
|
||||
text: @tr("Connection type");
|
||||
color: Theme.text-secondary;
|
||||
font-size: Theme.fs-sm;
|
||||
}
|
||||
HorizontalLayout {
|
||||
height: 30px;
|
||||
spacing: 6px;
|
||||
SegOption {
|
||||
label: "SSH";
|
||||
active: root.draft-kind == "ssh";
|
||||
clicked => { root.draft-kind = "ssh"; }
|
||||
}
|
||||
SegOption {
|
||||
label: @tr("Serial");
|
||||
active: root.draft-kind == "serial";
|
||||
clicked => { root.draft-kind = "serial"; }
|
||||
}
|
||||
SegOption {
|
||||
label: "Telnet";
|
||||
active: root.draft-kind == "telnet";
|
||||
clicked => { root.draft-kind = "telnet"; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LabeledInput {
|
||||
label: @tr("Name");
|
||||
placeholder: @tr("e.g. production web-01");
|
||||
value <=> root.draft-name;
|
||||
}
|
||||
|
||||
VerticalLayout {
|
||||
spacing: 4px;
|
||||
Text {
|
||||
text: @tr("Folder");
|
||||
color: Theme.text-secondary;
|
||||
font-size: Theme.fs-sm;
|
||||
}
|
||||
VerticalLayout {
|
||||
spacing: 6px;
|
||||
for folder in root.folders : FolderOption {
|
||||
label: folder;
|
||||
active: root.draft-group == folder;
|
||||
clicked => { root.draft-group = folder; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── SSH / Telnet share host + port ──────────────────────────────
|
||||
if root.draft-kind != "serial" : HorizontalLayout {
|
||||
spacing: 10px;
|
||||
LabeledInput {
|
||||
label: @tr("Host / IP");
|
||||
placeholder: "192.168.1.10";
|
||||
value <=> root.draft-host;
|
||||
horizontal-stretch: 2;
|
||||
}
|
||||
LabeledInput {
|
||||
label: @tr("Port");
|
||||
placeholder: root.draft-kind == "telnet" ? "23" : "22";
|
||||
value <=> root.draft-port;
|
||||
horizontal-stretch: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// ── SSH-only: user + auth ───────────────────────────────────────
|
||||
if root.draft-kind == "ssh" : LabeledInput {
|
||||
label: @tr("Username");
|
||||
placeholder: "root";
|
||||
value <=> root.draft-user;
|
||||
}
|
||||
|
||||
if root.draft-kind == "ssh" : VerticalLayout {
|
||||
spacing: 4px;
|
||||
Text {
|
||||
text: @tr("Authentication");
|
||||
color: Theme.text-secondary;
|
||||
font-size: Theme.fs-sm;
|
||||
}
|
||||
HorizontalLayout {
|
||||
height: 30px;
|
||||
spacing: 6px;
|
||||
SegOption {
|
||||
label: @tr("Password");
|
||||
active: root.draft-auth == "password";
|
||||
clicked => { root.draft-auth = "password"; }
|
||||
}
|
||||
SegOption {
|
||||
label: @tr("Private key");
|
||||
active: root.draft-auth == "key";
|
||||
clicked => { root.draft-auth = "key"; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if root.draft-kind == "ssh" && root.draft-auth == "password" : LabeledInput {
|
||||
label: @tr("Password");
|
||||
placeholder: root.is-editing ? @tr("Leave blank to keep the current password") : "••••••••";
|
||||
password: true;
|
||||
value <=> root.draft-password;
|
||||
}
|
||||
|
||||
if root.draft-kind == "ssh" && root.draft-auth == "key" : VerticalLayout {
|
||||
spacing: 4px;
|
||||
Text {
|
||||
text: @tr("Private key file");
|
||||
color: Theme.text-secondary;
|
||||
font-size: Theme.fs-sm;
|
||||
}
|
||||
HorizontalLayout {
|
||||
height: 32px;
|
||||
spacing: 8px;
|
||||
Rectangle {
|
||||
height: 32px;
|
||||
horizontal-stretch: 1;
|
||||
border-radius: Theme.radius-sm;
|
||||
border-width: 1px;
|
||||
border-color: keyin.has-focus ? Theme.accent : Theme.border-subtle;
|
||||
background: Theme.bg-root;
|
||||
keyin := TextInput {
|
||||
x: 10px; y: 0px;
|
||||
width: parent.width - 20px;
|
||||
height: parent.height;
|
||||
text <=> root.draft-key-path;
|
||||
color: Theme.text-primary;
|
||||
font-size: Theme.fs-md;
|
||||
vertical-alignment: center;
|
||||
single-line: true;
|
||||
}
|
||||
if root.draft-key-path == "" : Text {
|
||||
x: 10px; y: 0px;
|
||||
height: parent.height;
|
||||
text: "/home/you/.ssh/id_ed25519";
|
||||
color: Theme.text-muted;
|
||||
font-size: Theme.fs-md;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
width: 64px; height: 32px;
|
||||
border-radius: Theme.radius-sm;
|
||||
border-width: 1px;
|
||||
border-color: Theme.border-subtle;
|
||||
background: browse-ta.has-hover ? Theme.bg-hover : Theme.bg-elevated;
|
||||
Text {
|
||||
width: parent.width;
|
||||
height: parent.height;
|
||||
text: @tr("Browse…");
|
||||
color: Theme.text-primary;
|
||||
font-size: Theme.fs-sm;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
browse-ta := TouchArea {
|
||||
mouse-cursor: pointer;
|
||||
clicked => { root.pick-key-file(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
Text {
|
||||
text: @tr("Pick the private key itself (not the .pub public key)");
|
||||
color: Theme.text-muted;
|
||||
font-size: Theme.fs-xs;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Serial-only fields ──────────────────────────────────────────
|
||||
if root.draft-kind == "serial" : LabeledInput {
|
||||
label: @tr("Serial port");
|
||||
placeholder: "COM3 / /dev/ttyUSB0";
|
||||
value <=> root.draft-serial-port;
|
||||
}
|
||||
|
||||
if root.draft-kind == "serial" : LabeledInput {
|
||||
label: @tr("Baud rate");
|
||||
placeholder: "115200";
|
||||
value <=> root.draft-baud;
|
||||
}
|
||||
|
||||
if root.draft-kind == "serial" : VerticalLayout {
|
||||
spacing: 4px;
|
||||
Text {
|
||||
text: @tr("Data bits / Stop bits / Parity");
|
||||
color: Theme.text-secondary;
|
||||
font-size: Theme.fs-sm;
|
||||
}
|
||||
HorizontalLayout {
|
||||
height: 30px;
|
||||
spacing: 6px;
|
||||
SegOption { label: "7"; active: root.draft-data-bits == "7"; clicked => { root.draft-data-bits = "7"; } }
|
||||
SegOption { label: "8"; active: root.draft-data-bits == "8"; clicked => { root.draft-data-bits = "8"; } }
|
||||
Rectangle { width: 8px; }
|
||||
SegOption { label: "1"; active: root.draft-stop-bits == "1"; clicked => { root.draft-stop-bits = "1"; } }
|
||||
SegOption { label: "2"; active: root.draft-stop-bits == "2"; clicked => { root.draft-stop-bits = "2"; } }
|
||||
Rectangle { width: 8px; }
|
||||
SegOption { label: "N"; active: root.draft-parity == "none"; clicked => { root.draft-parity = "none"; } }
|
||||
SegOption { label: "O"; active: root.draft-parity == "odd"; clicked => { root.draft-parity = "odd"; } }
|
||||
SegOption { label: "E"; active: root.draft-parity == "even"; clicked => { root.draft-parity = "even"; } }
|
||||
}
|
||||
}
|
||||
|
||||
if root.draft-kind == "serial" : VerticalLayout {
|
||||
spacing: 4px;
|
||||
Text {
|
||||
text: @tr("Flow control");
|
||||
color: Theme.text-secondary;
|
||||
font-size: Theme.fs-sm;
|
||||
}
|
||||
HorizontalLayout {
|
||||
height: 30px;
|
||||
spacing: 6px;
|
||||
SegOption { label: @tr("None"); active: root.draft-flow == "none"; clicked => { root.draft-flow = "none"; } }
|
||||
SegOption { label: @tr("Hardware"); active: root.draft-flow == "hardware"; clicked => { root.draft-flow = "hardware"; } }
|
||||
SegOption { label: @tr("Software"); active: root.draft-flow == "software"; clicked => { root.draft-flow = "software"; } }
|
||||
}
|
||||
}
|
||||
|
||||
// ── Optional outbound proxy (SSH / Telnet only) ─────────────────
|
||||
if root.draft-kind != "serial" : VerticalLayout {
|
||||
spacing: 4px;
|
||||
LabeledInput {
|
||||
label: @tr("Proxy (optional)");
|
||||
placeholder: "socks5://127.0.0.1:1080";
|
||||
value <=> root.draft-proxy;
|
||||
}
|
||||
Text {
|
||||
text: @tr("Leave blank to use $ALL_PROXY or connect directly");
|
||||
color: Theme.text-muted;
|
||||
font-size: Theme.fs-xs;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalLayout {
|
||||
height: 30px;
|
||||
alignment: end;
|
||||
spacing: 8px;
|
||||
GhostButton {
|
||||
text: @tr("Cancel");
|
||||
clicked => { root.cancel(); }
|
||||
}
|
||||
PrimaryButton {
|
||||
text: is-editing ? @tr("Save") : @tr("Create");
|
||||
clicked => {
|
||||
root.submit({
|
||||
id: root.draft-id,
|
||||
name: root.draft-name,
|
||||
group: root.draft-group,
|
||||
kind: root.draft-kind,
|
||||
host: root.draft-host,
|
||||
port: root.draft-port.to-float(),
|
||||
user: root.draft-user,
|
||||
auth: root.draft-auth,
|
||||
password: root.draft-password,
|
||||
private-key-path: root.draft-key-path,
|
||||
proxy: root.draft-proxy,
|
||||
serial-port: root.draft-serial-port,
|
||||
baud-rate: root.draft-baud.to-float(),
|
||||
data-bits: root.draft-data-bits.to-float(),
|
||||
stop-bits: root.draft-stop-bits.to-float(),
|
||||
parity: root.draft-parity,
|
||||
flow-control: root.draft-flow,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user