This commit is contained in:
2026-06-19 03:09:53 +08:00
Unverified
commit d32882443f
49 changed files with 19399 additions and 0 deletions

454
ui/session_dialog.slint Normal file
View 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,
});
}
}
}
}
}
}