Files
meatshell/ui/app.slint
Hsdi 0f8dfa71a9
All checks were successful
Release / windows-x86_64-gnu (push) Successful in 1h5m41s
自动同步
2026-06-19 11:25:51 +08:00

1758 lines
77 KiB
Plaintext

// Embedded cross-platform fonts (compile-time): terminal monospace + UI icons.
// Embedding means Windows / Linux / macOS all render the same glyphs without
// relying on Consolas / Segoe MDL2 Assets being installed.
//
// Cascadia Mono: full box-drawing + block-elements + braille (needed by btop
// for its sparkline graphs; JetBrains Mono lacks braille → garbled output).
// Material Icons: open-source icon font replacing Windows-only Segoe MDL2 Assets.
import "fonts/CascadiaMono-Regular.ttf";
import "fonts/CascadiaMono-Bold.ttf";
import "fonts/MaterialIcons-Regular.ttf";
import { Theme } from "theme.slint";
import { Sidebar, DiskInfo, ProcessInfo } from "sidebar.slint";
import { TabBar, TabInfo } from "tabs.slint";
import { Welcome, SessionInfo, ConnectionFolderInfo } from "welcome.slint";
import { SessionDialog, SessionDraft } from "session_dialog.slint";
import { ConfirmDialog } from "confirm_dialog.slint";
import { TerminalView, TermSpan, TermMatch, CommandCategory, CommandItem } from "terminal_view.slint";
import { SftpEntry, SftpTreeNode } from "sftp_panel.slint";
export { SessionInfo, ConnectionFolderInfo, SessionDraft, TabInfo, SftpEntry, SftpTreeNode, CommandCategory, CommandItem, ProcessInfo }
// --- TerminalState ---------------------------------------------------------
// Per-terminal view-model exposed to Rust. A TabInfo has the metadata; the
// TerminalState carries the streaming log + status line. Kept as a parallel
// model so opening/closing tabs doesn't churn the log strings.
// One file-transfer record shown in the download manager.
export struct TransferInfo {
id: string,
name: string,
detail: string, // "2.3 MB/5.0 MB" | "已完成" | "失败"
percent: float, // 0.0..1.0
state: int, // 0 active / 1 done / 2 error
is-upload: bool,
}
export struct TerminalState {
id: string,
tab-id: string,
status: string,
spans: [TermSpan], // coloured runs of the visible terminal screen
cursor-row: int, // cursor cell on the grid
cursor-col: int,
rows-used: int, // content rows, for viewport sizing
is-alt-screen: bool,
mouse-tracking: bool,
find-matches: [TermMatch], // search-highlight rectangles
selection: [TermMatch], // drag-selection highlight rectangles
// SFTP panel state
sftp-path: string,
sftp-entries: [SftpEntry],
sftp-status: string,
sftp-loading: bool,
sftp-tree-nodes: [SftpTreeNode],
}
component ConfigButton inherits Rectangle {
in property <string> label;
in property <bool> primary: false;
in property <bool> selected: false;
in property <length> button-width: 84px;
callback clicked();
width: button-width;
height: 30px;
border-radius: Theme.radius-sm;
border-width: primary ? 0px : 1px;
border-color: selected ? Theme.accent : (touch.has-hover ? Theme.border-strong : Theme.border-subtle);
background: primary
? (touch.has-hover ? Theme.accent-hover : Theme.accent)
: selected
? Theme.bg-active
: (touch.has-hover ? Theme.bg-hover : Theme.bg-elevated);
touch := TouchArea {
mouse-cursor: pointer;
clicked => { root.clicked(); }
}
Text {
width: parent.width;
height: parent.height;
text: root.label;
color: root.primary ? #ffffff : Theme.text-primary;
font-size: Theme.fs-sm;
horizontal-alignment: center;
vertical-alignment: center;
overflow: elide;
}
}
component ConfigTextField inherits Rectangle {
in-out property <string> value;
in property <length> field-width: 86px;
width: field-width;
height: 30px;
border-radius: Theme.radius-sm;
border-width: 1px;
border-color: input.has-focus ? Theme.accent : Theme.border-subtle;
background: Theme.bg-root;
input := TextInput {
x: 8px;
width: parent.width - 16px;
height: parent.height;
text <=> root.value;
color: Theme.text-primary;
font-size: Theme.fs-sm;
single-line: true;
vertical-alignment: center;
}
}
component ConfigValueBox inherits Rectangle {
in property <string> value;
in property <string> empty-text;
height: 30px;
border-radius: Theme.radius-sm;
border-width: 1px;
border-color: Theme.border-subtle;
background: Theme.bg-root;
Text {
x: 10px;
width: parent.width - 20px;
height: parent.height;
text: root.value == "" ? root.empty-text : root.value;
color: root.value == "" ? Theme.text-muted : Theme.text-primary;
font-size: Theme.fs-sm;
overflow: elide;
vertical-alignment: center;
}
}
export component AppWindow inherits Window {
title: "meatshell";
icon: @image-url("../assets/icon.png");
default-font-family: Theme.font-ui;
min-width: 960px;
min-height: 600px;
preferred-width: 1200px;
preferred-height: 760px;
background: Theme.bg-root;
// --- Sidebar data ------------------------------------------------------
in property <string> connection-state: @tr("Not connected");
in property <int> conn-state; // 0 gray / 1 green / 2 yellow
in property <string> resource-title: @tr("Local resources"); // @tr("Local resources") | @tr("Server resources")
in property <float> cpu-percent;
in property <float> mem-percent;
in property <float> swap-percent;
in property <string> cpu-detail;
in property <string> mem-detail;
in property <string> swap-detail;
// Network panel follows the active live session tab.
in property <string> net-top-up; // ↑ tx rate text
in property <string> net-top-down; // ↓ rx rate text
in property <[float]> net-top-history;
in property <[string]> net-ifaces; // remote NIC names for the selector
in property <string> net-selected; // currently selected NIC
in property <bool> net-show-selector; // only on a live session tab
in property <[DiskInfo]> disks; // active tab's filesystem usage
in property <[ProcessInfo]> processes; // top resource consumers
callback select-net-iface(string);
// --- Top-right popups --------------------------------------------------
in-out property <bool> download-open: false; // download manager popup
in-out property <bool> settings-open: false; // settings menu popup
in-out property <bool> config-open: false; // app configuration dialog
in-out property <bool> about-open: false; // centered About dialog
in property <string> download-dir; // preset SFTP download folder ("" = ask)
in property <[TransferInfo]> transfers; // download/upload records (newest first)
in property <[CommandCategory]> command-categories;
in property <[CommandItem]> command-items;
in-out property <string> command-selected-category: "default";
in-out property <string> command-selected-id;
in-out property <string> command-category-draft;
in-out property <string> command-edit-id;
in-out property <string> command-edit-name;
in-out property <string> command-edit-command;
in-out property <bool> command-edit-append-cr: true;
in-out property <string> command-param1-name;
in-out property <string> command-param1-value;
in-out property <string> command-param2-name;
in-out property <string> command-param2-value;
in-out property <string> command-param3-name;
in-out property <string> command-param3-value;
in-out property <string> command-param4-name;
in-out property <string> command-param4-value;
in-out property <string> command-param5-name;
in-out property <string> command-param5-value;
in property <[string]> about-libs; // open-source libraries used
in-out property <bool> lang-en: false; // current UI language (false = 中文)
// Two-way binding to Theme.dark — Rust reads/writes this property and
// Slint propagates it to every widget that references Theme colours.
in-out property <bool> dark-mode <=> Theme.dark;
callback toggle-theme(); // flip dark ↔ light
in-out property <length> sftp-panel-height: max(240px, root.height * 0.4); // default: files 2/5, terminal 3/5
in-out property <length> terminal-font-size: 13px;
in-out property <length> sftp-file-font-size: 16px;
in-out property <length> sftp-name-column-width: 180px;
in-out property <bool> sftp-name-width-manual: false;
callback sftp-name-column-width-changed(float);
callback sftp-name-column-auto();
callback pick-download-dir(); // open a folder picker
callback open-download-dir(); // reveal the folder in the OS
callback clear-transfers(); // wipe the transfer history
callback set-language(string); // switch UI language ("zh" / "en")
in-out property <string> max-tabs-text: "10";
in-out property <string> terminal-font-size-text: "13";
in-out property <string> sftp-font-size-text: "16";
in-out property <string> background-path-text;
in-out property <int> background-mode: 1; // 0 none, 1 terminal, 2 fullscreen
in-out property <string> background-blur-text: "3";
in-out property <string> terminal-opacity-text: "88";
in-out property <string> ui-opacity-text: "72";
in-out property <float> terminal-background-opacity: 0.88;
in-out property <float> ui-background-opacity: 0.72;
in-out property <bool> ui-debug-ids: false;
in property <image> background-image;
in-out property <string> config-dir-text;
// WebDAV sync settings
in-out property <bool> webdav-enabled: false;
in-out property <string> webdav-url-text;
in-out property <string> webdav-username-text;
in-out property <string> webdav-password-text;
in-out property <string> webdav-status-text;
in-out property <string> webdav-auto-sync-interval: "0";
callback webdav-test-connection();
callback webdav-push();
callback webdav-pull();
callback webdav-save-settings(bool, string, string, string, string);
callback app-config-save(string, string, string, int, string, string, string, bool, string);
callback app-config-pick-dir();
callback app-background-pick();
callback app-background-clear();
callback debug-click(string, string);
property <bool> fullscreen-background-active: root.background-mode == 2 && root.background-path-text != "";
// --- Session list ------------------------------------------------------
in property <[SessionInfo]> sessions;
in property <[ConnectionFolderInfo]> connection-folders;
in property <[string]> connection-folder-names;
// --- Tabs --------------------------------------------------------------
in property <[TabInfo]> tabs;
in-out property <string> active-tab-id: "welcome";
in-out property <int> tab-visible-start: 0;
in property <[TerminalState]> terminals;
// --- Dialog state ------------------------------------------------------
in-out property <bool> dialog-open: false;
in-out property <bool> dialog-editing: false;
in-out property <string> dialog-id;
in-out property <string> dialog-name;
in-out property <string> dialog-group: "Default";
in-out property <string> dialog-host;
in-out property <string> dialog-port: "22";
in-out property <string> dialog-user: "root";
in-out property <string> dialog-auth: "password";
in-out property <string> dialog-password;
in-out property <string> dialog-key-path;
in-out property <string> dialog-proxy;
in-out property <string> dialog-kind: "ssh";
in-out property <string> dialog-serial-port;
in-out property <string> dialog-baud: "115200";
in-out property <string> dialog-data-bits: "8";
in-out property <string> dialog-stop-bits: "1";
in-out property <string> dialog-parity: "none";
in-out property <string> dialog-flow: "none";
in-out property <bool> folder-dialog-open: false;
in-out property <string> folder-dialog-mode: "create";
in-out property <string> folder-edit-original;
in-out property <string> folder-draft;
in-out property <string> folder-error;
// Delete-confirmation modal state (#28).
in-out property <bool> confirm-delete-open: false;
in-out property <string> confirm-delete-tab;
in-out property <string> confirm-delete-path;
// --- Callbacks up to Rust ---------------------------------------------
callback new-session-clicked();
callback connection-folder-create(string);
callback connection-folder-rename(string, string);
callback connection-folder-delete(string);
callback connection-folder-toggle(string);
callback session-move-folder(string, string);
callback import-ssh-config(); // import hosts from ~/.ssh/config
in-out property <string> ssh-import-hint; // result text shown after import
callback connect-session(string /* session-id */);
callback edit-session(string /* session-id */);
callback remove-session(string /* session-id */);
callback session-dialog-submit(SessionDraft);
callback session-dialog-cancel();
callback session-dialog-pick-key();
callback tab-selected(string);
callback tab-closed(string);
callback new-tab-clicked();
callback previous-tab-clicked();
callback next-tab-clicked();
// Asks Rust to recompute the sidebar (status + resources) for whatever tab
// is now active. Fired on every active-tab change, UI- or code-driven.
callback refresh-sidebar();
changed active-tab-id => { root.refresh-sidebar(); }
// Raw keystroke forwarding: Rust converts key+modifiers to PTY bytes.
callback send-key(string /* tab-id */, string /* key */, bool /* ctrl */, bool /* alt */, bool /* shift */);
// Notifies Rust of a PTY resize (pixel dimensions; Rust converts to cols/rows).
callback terminal-resize(string /* tab-id */, float /* width-px */, float /* height-px */);
// --- SFTP callbacks ---------------------------------------------------
// Navigate to a new remote directory (or ".." to go up).
callback sftp-navigate(string /* tab-id */, string /* path */);
// Trigger a download: Rust opens a save-folder dialog then transfers.
callback sftp-download(string /* tab-id */, string /* remote-path */);
// Trigger an upload: Rust opens a file-picker dialog then transfers.
callback sftp-upload-clicked(string /* tab-id */, string /* remote-dir */);
// Refresh the current directory listing.
callback sftp-refresh(string /* tab-id */, string /* path */);
// Toggle expand/collapse of a tree node (also navigates to that directory).
callback sftp-tree-expand(string /* tab-id */, string /* path */);
callback sftp-delete(string /* tab-id */, string /* path */);
callback sftp-rename(string /* tab-id */, string /* path */, string /* new-name */);
callback sftp-edit(string /* tab-id */, string /* path */);
// --- Clipboard callbacks ----------------------------------------------
// Paste clipboard text into the active terminal.
callback paste-from-clipboard(string /* tab-id */);
// Copy the current terminal screen buffer to the clipboard.
callback copy-terminal-text(string /* tab-id */);
// Clear a session's terminal buffer (its visible screen + scrollback).
callback clear-terminal(string /* tab-id */);
// Recompute search highlights for a session's visible screen.
callback find-query-changed(string /* tab-id */, string /* query */);
// Scroll a session's scrollback history by N lines (+ = up/older).
callback terminal-scroll(string /* tab-id */, int /* delta-lines */);
// Drag-selection lifecycle (grid row/col coordinates).
callback term-select-start(string /* tab-id */, int /* row */, int /* col */);
callback term-select-update(string /* tab-id */, int /* row */, int /* col */);
callback term-select-end(string /* tab-id */);
callback term-select-autoscroll(string /* tab-id */, int /* dir */);
callback command-category-select(string /* category-id */);
callback command-category-add(string /* name */);
callback command-category-rename(string /* category-id */, string /* name */);
callback command-category-delete(string /* category-id */);
callback command-new(string /* category-id */);
callback command-select(string /* command-id */);
callback command-save(string /* command-id */, string /* category-id */, string /* name */, string /* command */, bool /* append-cr */);
callback command-delete(string /* command-id */);
callback command-run(string /* tab-id */, string /* command-id */, string /* p1 */, string /* p2 */, string /* p3 */, string /* p4 */, string /* p5 */);
callback command-move(string /* command-id */, string /* category-id */);
callback terminal-mouse-event(string /* tab-id */, string /* kind */, int /* button */, int /* row */, int /* col */);
if root.fullscreen-background-active : Image {
width: parent.width;
height: parent.height;
source: root.background-image;
image-fit: cover;
}
Rectangle {
width: parent.width;
height: parent.height;
background: root.fullscreen-background-active
? Theme.bg-root.with-alpha(root.ui-background-opacity)
: Theme.bg-root;
}
HorizontalLayout {
spacing: 0;
// Left sidebar
Sidebar {
app-background-active: root.fullscreen-background-active;
ui-opacity: root.ui-background-opacity;
connection-state: root.connection-state;
conn-state: root.conn-state;
resource-title: root.resource-title;
cpu-percent: root.cpu-percent;
mem-percent: root.mem-percent;
swap-percent: root.swap-percent;
cpu-detail: root.cpu-detail;
mem-detail: root.mem-detail;
swap-detail: root.swap-detail;
net-top-up: root.net-top-up;
net-top-down: root.net-top-down;
net-top-history: root.net-top-history;
net-ifaces: root.net-ifaces;
net-selected: root.net-selected;
net-show-selector: root.net-show-selector;
disks: root.disks;
processes: root.processes;
select-net-iface(i) => { root.select-net-iface(i); }
}
// Right side: tabs + content
VerticalLayout {
spacing: 0;
horizontal-stretch: 1;
TabBar {
app-background-active: root.fullscreen-background-active;
ui-opacity: root.ui-background-opacity;
tabs: root.tabs;
active-id: root.active-tab-id;
visible-start <=> root.tab-visible-start;
tab-selected(id) => {
if root.ui-debug-ids { root.debug-click("tabbar", "tab-selected"); }
root.active-tab-id = id;
root.tab-selected(id);
}
tab-closed(id) => {
if root.ui-debug-ids { root.debug-click("tabbar", "tab-closed"); }
root.tab-closed(id);
}
new-tab() => {
if root.ui-debug-ids { root.debug-click("tabbar", "new-tab"); }
root.new-tab-clicked();
}
previous-tab() => {
if root.ui-debug-ids { root.debug-click("tabbar", "previous-tab"); }
root.previous-tab-clicked();
}
next-tab() => {
if root.ui-debug-ids { root.debug-click("tabbar", "next-tab"); }
root.next-tab-clicked();
}
}
// Content area swaps based on active tab kind. Children are
// stacked, not laid out horizontally, so hidden terminal tabs never
// reserve half of the workspace.
Rectangle {
vertical-stretch: 1;
background: root.fullscreen-background-active
? Theme.bg-root.with-alpha(root.ui-background-opacity)
: Theme.bg-root;
clip: true;
// Welcome tab
if root.active-tab-id == "welcome" : Welcome {
width: parent.width;
height: parent.height;
app-background-active: root.fullscreen-background-active;
ui-opacity: root.ui-background-opacity;
sessions: root.sessions;
folders: root.connection-folders;
all-folder-names: root.connection-folder-names;
import-hint: root.ssh-import-hint;
debug-ids: false;
new-session => {
if root.ui-debug-ids { root.debug-click("welcome", "new-session"); }
root.new-session-clicked();
}
new-folder => {
if root.ui-debug-ids { root.debug-click("welcome", "new-folder"); }
root.folder-dialog-mode = "create";
root.folder-edit-original = "";
root.folder-draft = "";
root.folder-error = "";
root.folder-dialog-open = true;
}
rename-folder(name) => {
if root.ui-debug-ids { root.debug-click("welcome", "rename-folder"); }
root.folder-dialog-mode = "rename";
root.folder-edit-original = name;
root.folder-draft = name;
root.folder-error = "";
root.folder-dialog-open = true;
}
delete-folder(name) => {
if root.ui-debug-ids { root.debug-click("welcome", "delete-folder"); }
root.connection-folder-delete(name);
}
toggle-folder(name) => {
if root.ui-debug-ids { root.debug-click("welcome", "toggle-folder"); }
root.connection-folder-toggle(name);
}
move-session-folder(id, folder) => {
if root.ui-debug-ids { root.debug-click("welcome", "move-session-folder"); }
root.session-move-folder(id, folder);
}
import-ssh-config => {
if root.ui-debug-ids { root.debug-click("welcome", "import-ssh-config"); }
root.import-ssh-config();
}
connect-session(id) => {
if root.ui-debug-ids { root.debug-click("welcome", "connect-session"); }
root.connect-session(id);
}
edit-session(id) => {
if root.ui-debug-ids { root.debug-click("welcome", "edit-session"); }
root.edit-session(id);
}
remove-session(id) => {
if root.ui-debug-ids { root.debug-click("welcome", "remove-session"); }
root.remove-session(id);
}
}
// Terminal tabs. Every terminal view fills the same workspace;
// only the active tab is visible and allowed to emit resize
// events from its own timer.
for term[i] in root.terminals : TerminalView {
x: 0px;
y: 0px;
width: parent.width;
height: parent.height;
visible: term.tab-id == root.active-tab-id;
tab-id: term.id;
status-line: term.status;
spans: term.spans;
cursor-row: term.cursor-row;
cursor-col: term.cursor-col;
rows-used: term.rows-used;
is-alt-screen: term.is-alt-screen;
mouse-tracking: term.mouse-tracking;
find-matches: term.find-matches;
selection: term.selection;
sftp-panel-height <=> root.sftp-panel-height;
terminal-font-size: root.terminal-font-size;
click-debug-enabled: root.ui-debug-ids;
background-path: root.background-path-text;
background-mode: root.background-mode;
background-image: root.background-image;
app-background-active: root.fullscreen-background-active;
terminal-bg-opacity: root.terminal-background-opacity;
ui-opacity: root.ui-background-opacity;
sftp-file-font-size: root.sftp-file-font-size;
sftp-name-column-width <=> root.sftp-name-column-width;
sftp-name-width-manual <=> root.sftp-name-width-manual;
sftp-path: term.sftp-path;
sftp-entries: term.sftp-entries;
sftp-status: term.sftp-status;
sftp-loading: term.sftp-loading;
sftp-tree-nodes: term.sftp-tree-nodes;
command-categories: root.command-categories;
command-items: root.command-items;
command-selected-category <=> root.command-selected-category;
command-selected-id <=> root.command-selected-id;
command-category-draft <=> root.command-category-draft;
command-edit-id <=> root.command-edit-id;
command-edit-name <=> root.command-edit-name;
command-edit-command <=> root.command-edit-command;
command-edit-append-cr <=> root.command-edit-append-cr;
command-param1-name <=> root.command-param1-name;
command-param1-value <=> root.command-param1-value;
command-param2-name <=> root.command-param2-name;
command-param2-value <=> root.command-param2-value;
command-param3-name <=> root.command-param3-name;
command-param3-value <=> root.command-param3-value;
command-param4-name <=> root.command-param4-name;
command-param4-value <=> root.command-param4-value;
command-param5-name <=> root.command-param5-name;
command-param5-value <=> root.command-param5-value;
send-key(key, ctrl, alt, shift) => { root.send-key(term.id, key, ctrl, alt, shift) }
terminal-resize(w, h) => { root.terminal-resize(term.id, w, h) }
sftp-navigate(path) => { root.sftp-navigate(term.id, path); }
sftp-download(path) => { root.sftp-download(term.id, path); }
sftp-upload-clicked(dir) => { root.sftp-upload-clicked(term.id, dir); }
sftp-refresh(path) => { root.sftp-refresh(term.id, path); }
sftp-tree-expand(path) => { root.sftp-tree-expand(term.id, path); }
// Gate delete behind an in-app confirmation (#28): stash the
// target and open the modal; the real delete fires on confirm.
sftp-delete(path) => {
root.confirm-delete-tab = term.id;
root.confirm-delete-path = path;
root.confirm-delete-open = true;
}
sftp-rename(path, name) => { root.sftp-rename(term.id, path, name); }
sftp-edit(path) => { root.sftp-edit(term.id, path); }
sftp-name-column-width-changed(px) => { root.sftp-name-column-width-changed(px); }
sftp-name-column-auto() => { root.sftp-name-column-auto(); }
paste-from-clipboard() => { root.paste-from-clipboard(term.id); }
copy-terminal-text() => { root.copy-terminal-text(term.id); }
clear-terminal() => { root.clear-terminal(term.id); }
find-query-changed(q) => { root.find-query-changed(term.id, q); }
terminal-scroll(d) => { root.terminal-scroll(term.id, d); }
select-start(r, c) => { root.term-select-start(term.id, r, c); }
select-update(r, c) => { root.term-select-update(term.id, r, c); }
select-end() => { root.term-select-end(term.id); }
select-autoscroll(dir) => { root.term-select-autoscroll(term.id, dir); }
command-category-select(id) => { root.command-category-select(id); }
command-category-add(name) => { root.command-category-add(name); }
command-category-rename(id, name) => { root.command-category-rename(id, name); }
command-category-delete(id) => { root.command-category-delete(id); }
command-new(category) => { root.command-new(category); }
command-select(id) => { root.command-select(id); }
command-save(id, category, name, command, append_cr) => {
root.command-save(id, category, name, command, append_cr);
}
command-delete(id) => { root.command-delete(id); }
command-run(id, p1, p2, p3, p4, p5) => {
root.command-run(term.id, id, p1, p2, p3, p4, p5);
}
command-move(id, category) => { root.command-move(id, category); }
terminal-mouse-event(kind, button, row, col) => {
root.terminal-mouse-event(term.id, kind, button, row, col);
}
debug-click(parent, button) => { root.debug-click(parent, button); }
}
}
}
}
// --- Top-right buttons: [☀/🌙 theme] [↓ download] [⚙ settings] ----------
// Settings (gear) — far right.
Rectangle {
x: root.width - 34px;
y: 8px;
width: 26px;
height: 26px;
border-radius: Theme.radius-sm;
background: gear-ta.has-hover || root.settings-open ? Theme.bg-hover : transparent;
gear-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
if root.ui-debug-ids { root.debug-click("topbar", "settings"); }
root.settings-open = !root.settings-open;
root.download-open = false;
root.config-open = false;
}
}
Text {
width: parent.width;
height: parent.height;
text: "\u{E8B8}"; // Material Icons settings (gear)
font-family: "Material Icons";
color: Theme.text-secondary;
font-size: Theme.fs-md;
horizontal-alignment: center;
vertical-alignment: center;
}
}
// Download manager — left of settings.
Rectangle {
x: root.width - 64px;
y: 8px;
width: 26px;
height: 26px;
border-radius: Theme.radius-sm;
background: dl-ta.has-hover || root.download-open ? Theme.bg-hover : transparent;
dl-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
if root.ui-debug-ids { root.debug-click("topbar", "downloads"); }
root.download-open = !root.download-open;
root.settings-open = false;
root.config-open = false;
}
}
Text {
width: parent.width;
height: parent.height;
text: "\u{E2C4}"; // Material Icons file_download
font-family: "Material Icons";
color: Theme.text-secondary;
font-size: Theme.fs-md;
horizontal-alignment: center;
vertical-alignment: center;
}
}
// Theme toggle (sun = currently dark → click for light;
// moon = currently light → click for dark) — left of download.
Rectangle {
x: root.width - 94px;
y: 8px;
width: 26px;
height: 26px;
border-radius: Theme.radius-sm;
background: theme-ta.has-hover ? Theme.bg-hover : transparent;
theme-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
if root.ui-debug-ids { root.debug-click("topbar", "toggle-theme"); }
root.toggle-theme();
}
}
Text {
width: parent.width;
height: parent.height;
// light_mode ☀ when dark (click → go light)
// dark_mode 🌙 when light (click → go dark)
text: Theme.dark ? "\u{E518}" : "\u{E51C}";
font-family: "Material Icons";
color: Theme.dark ? #f5c542 : #5856d6; // amber sun / indigo moon
font-size: Theme.fs-md;
horizontal-alignment: center;
vertical-alignment: center;
}
}
// --- Download manager popup -------------------------------------------
// Always present; toggled via `visible` (avoids destroying interactive
// elements mid-event, which crashes when created/removed at the Window root).
if root.download-open : TouchArea {
width: parent.width;
height: parent.height;
clicked => { root.download-open = false; }
}
Rectangle {
x: parent.width - 364px;
y: 40px;
width: 350px;
height: 400px;
visible: root.download-open;
background: Theme.bg-panel;
border-radius: Theme.radius-md;
border-width: 1px;
border-color: Theme.border-strong;
drop-shadow-blur: 16px;
drop-shadow-color: #00000070;
// Swallow pointer events so right-clicks don't fall through to the
// terminal's context menu underneath.
TouchArea {}
VerticalLayout {
padding: 14px;
spacing: 10px;
HorizontalLayout {
Text {
text: @tr("Download settings");
color: Theme.text-primary;
font-size: Theme.fs-md;
font-weight: 600;
horizontal-stretch: 1;
vertical-alignment: center;
}
Rectangle {
width: 20px; height: 20px;
border-radius: Theme.radius-sm;
background: close-ta.has-hover ? Theme.bg-hover : transparent;
close-ta := TouchArea {
mouse-cursor: pointer;
clicked => { root.download-open = false; }
}
Text { text: "\u{E5CD}"; font-family: "Material Icons";
color: Theme.text-secondary; font-size: Theme.fs-sm;
width: parent.width; height: parent.height;
horizontal-alignment: center; vertical-alignment: center; }
}
}
Text {
text: @tr("Save downloads to:");
color: Theme.text-secondary;
font-size: Theme.fs-sm;
}
// Current path display
Rectangle {
height: 26px;
background: Theme.bg-root;
border-radius: Theme.radius-sm;
Text {
x: 8px;
width: parent.width - 16px;
height: parent.height;
text: root.download-dir == "" ? @tr("(ask every time)") : root.download-dir;
color: root.download-dir == "" ? Theme.text-muted : Theme.text-primary;
font-size: Theme.fs-sm;
vertical-alignment: center;
overflow: elide;
}
}
HorizontalLayout {
spacing: 8px;
alignment: end;
Rectangle {
width: 96px; height: 28px;
border-radius: Theme.radius-sm;
background: pick-ta.has-hover ? Theme.accent-hover : Theme.accent;
pick-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
if root.ui-debug-ids { root.debug-click("downloads", "choose-folder"); }
root.pick-download-dir();
}
}
Text { text: @tr("Choose folder"); color: #ffffff; font-size: Theme.fs-sm;
width: parent.width; height: parent.height;
horizontal-alignment: center; vertical-alignment: center; }
}
Rectangle {
width: 76px; height: 28px;
border-radius: Theme.radius-sm;
background: open-ta.has-hover ? Theme.bg-hover : Theme.bg-elevated;
open-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
if root.ui-debug-ids { root.debug-click("downloads", "open-folder"); }
root.open-download-dir();
}
}
Text { text: @tr("Open folder"); color: Theme.text-primary; font-size: Theme.fs-sm;
width: parent.width; height: parent.height;
horizontal-alignment: center; vertical-alignment: center; }
}
}
Rectangle { height: 1px; background: Theme.border-subtle; }
// Transfer records (download/upload progress + history).
HorizontalLayout {
Text {
text: @tr("Transfers");
color: Theme.text-secondary;
font-size: Theme.fs-sm;
horizontal-stretch: 1;
vertical-alignment: center;
}
Rectangle {
width: 44px; height: 20px;
border-radius: Theme.radius-sm;
background: clr-ta.has-hover ? Theme.bg-hover : transparent;
clr-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
if root.ui-debug-ids { root.debug-click("downloads", "clear-transfers"); }
root.clear-transfers();
}
}
Text { text: @tr("Clear"); color: Theme.text-muted; font-size: Theme.fs-xs;
width: parent.width; height: parent.height;
horizontal-alignment: center; vertical-alignment: center; }
}
}
Flickable {
vertical-stretch: 1;
viewport-width: self.width;
viewport-height: max(self.height, xfer-col.preferred-height);
xfer-col := VerticalLayout {
alignment: start;
spacing: 4px;
if root.transfers.length == 0 : Text {
text: @tr("No transfers yet");
color: Theme.text-muted;
font-size: Theme.fs-xs;
}
for t in root.transfers : Rectangle {
height: 36px;
border-radius: Theme.radius-sm;
background: root.fullscreen-background-active ? Theme.bg-root.with-alpha(root.ui-background-opacity) : Theme.bg-root;
VerticalLayout {
padding-left: 8px; padding-right: 8px;
padding-top: 4px; padding-bottom: 4px;
spacing: 3px;
HorizontalLayout {
spacing: 4px;
Text {
text: (t.is-upload ? "↑ " : "↓ ") + t.name;
color: Theme.text-primary;
font-size: Theme.fs-xs;
horizontal-stretch: 1;
overflow: elide;
vertical-alignment: center;
}
Text {
text: t.detail;
color: t.state == 2 ? Theme.danger
: t.state == 1 ? Theme.success
: Theme.text-muted;
font-size: Theme.fs-xs;
vertical-alignment: center;
}
}
// progress bar
Rectangle {
height: 4px;
border-radius: 2px;
background: Theme.bg-elevated;
Rectangle {
x: 0; y: 0; height: parent.height;
width: parent.width * clamp(t.percent, 0.0, 1.0);
border-radius: 2px;
background: t.state == 2 ? Theme.danger
: t.state == 1 ? Theme.success
: Theme.accent;
}
}
}
}
}
}
}
}
// --- Settings menu popup (top-right) ----------------------------------
Rectangle {
x: parent.width - 196px;
y: 40px;
width: 198px;
height: 134px;
visible: root.settings-open;
background: Theme.bg-panel;
border-radius: Theme.radius-md;
border-width: 1px;
border-color: Theme.border-strong;
drop-shadow-blur: 14px;
drop-shadow-color: #00000060;
TouchArea {} // swallow events so they don't reach the terminal
VerticalLayout {
padding: 4px;
spacing: 1px;
// Import hosts from ~/.ssh/config.
Rectangle {
height: 28px;
border-radius: Theme.radius-sm;
background: import-ta.has-hover ? Theme.bg-hover : transparent;
import-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
if root.ui-debug-ids { root.debug-click("settings", "import-ssh-config"); }
root.settings-open = false;
root.import-ssh-config();
}
}
HorizontalLayout {
height: parent.height;
padding-left: 8px; spacing: 8px;
Text { width: 18px; height: parent.height; text: "\u{E890}"; font-family: "Material Icons"; // input/import
color: Theme.text-secondary; font-size: Theme.fs-sm;
horizontal-alignment: center; vertical-alignment: center; }
Text { text: @tr("Import ~/.ssh/config"); color: Theme.text-primary; font-size: Theme.fs-md;
height: parent.height; vertical-alignment: center; horizontal-stretch: 1; }
}
}
// Language toggle: shows the language you'll switch TO.
Rectangle {
height: 28px;
border-radius: Theme.radius-sm;
background: lang-ta.has-hover ? Theme.bg-hover : transparent;
lang-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
if root.ui-debug-ids { root.debug-click("settings", "language"); }
root.settings-open = false;
root.set-language(root.lang-en ? "zh" : "en");
}
}
HorizontalLayout {
height: parent.height;
padding-left: 8px; spacing: 8px;
Text { width: 18px; height: parent.height; text: "\u{E894}"; font-family: "Material Icons"; // language/translate
color: Theme.text-secondary; font-size: Theme.fs-sm;
horizontal-alignment: center; vertical-alignment: center; }
Text { text: root.lang-en ? "切换到中文" : "Switch to English";
color: Theme.text-primary; font-size: Theme.fs-md;
height: parent.height; vertical-alignment: center; horizontal-stretch: 1; }
}
}
Rectangle {
height: 28px;
border-radius: Theme.radius-sm;
background: about-ta.has-hover ? Theme.bg-hover : transparent;
about-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
if root.ui-debug-ids { root.debug-click("settings", "about"); }
root.settings-open = false;
root.about-open = true;
}
}
HorizontalLayout {
height: parent.height;
padding-left: 8px; spacing: 8px;
Text { width: 18px; height: parent.height; text: "\u{E88E}"; font-family: "Material Icons"; // Info
color: Theme.text-secondary; font-size: Theme.fs-sm;
horizontal-alignment: center; vertical-alignment: center; }
Text { text: @tr("About"); color: Theme.text-primary; font-size: Theme.fs-md;
height: parent.height; vertical-alignment: center; horizontal-stretch: 1; }
}
}
Rectangle {
height: 28px;
border-radius: Theme.radius-sm;
background: config-ta.has-hover ? Theme.bg-hover : transparent;
config-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
if root.ui-debug-ids { root.debug-click("settings", "configuration"); }
root.settings-open = false;
root.config-open = true;
}
}
HorizontalLayout {
height: parent.height;
padding-left: 8px; spacing: 8px;
Text { width: 18px; height: parent.height; text: "\u{E8B8}"; font-family: "Material Icons";
color: Theme.text-secondary; font-size: Theme.fs-sm;
horizontal-alignment: center; vertical-alignment: center; }
Text { text: @tr("Configuration"); color: Theme.text-primary; font-size: Theme.fs-md;
height: parent.height; vertical-alignment: center; horizontal-stretch: 1; }
}
}
}
}
// --- Configuration dialog --------------------------------------------
Rectangle {
width: parent.width;
height: parent.height;
visible: root.config-open;
background: #00000070;
TouchArea { clicked => { root.config-open = false; } }
Rectangle {
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
width: min(640px, parent.width - 48px);
height: min(620px, parent.height - 48px);
background: Theme.bg-panel;
border-radius: Theme.radius-md;
border-width: 1px;
border-color: Theme.border-strong;
drop-shadow-blur: 20px;
drop-shadow-color: #00000080;
TouchArea {}
VerticalLayout {
padding: 16px;
spacing: 10px;
HorizontalLayout {
Text {
text: @tr("Configuration");
color: Theme.text-primary;
font-size: Theme.fs-md;
font-weight: 600;
vertical-alignment: center;
horizontal-stretch: 1;
}
Rectangle {
width: 22px; height: 22px;
border-radius: Theme.radius-sm;
background: cfg-close-ta.has-hover ? Theme.bg-hover : transparent;
cfg-close-ta := TouchArea {
mouse-cursor: pointer;
clicked => { root.config-open = false; }
}
Text { text: "\u{E5CD}"; font-family: "Material Icons";
color: Theme.text-secondary; font-size: Theme.fs-sm;
width: parent.width; height: parent.height;
horizontal-alignment: center; vertical-alignment: center; }
}
}
HorizontalLayout {
height: 30px;
spacing: 10px;
Text {
width: 148px;
height: parent.height;
text: @tr("Maximum terminal windows");
color: Theme.text-secondary;
font-size: Theme.fs-sm;
vertical-alignment: center;
overflow: elide;
}
ConfigTextField {
value <=> root.max-tabs-text;
field-width: 86px;
}
Rectangle { horizontal-stretch: 1; }
}
HorizontalLayout {
height: 30px;
spacing: 10px;
Text {
width: 148px;
height: parent.height;
text: @tr("Terminal font size");
color: Theme.text-secondary;
font-size: Theme.fs-sm;
vertical-alignment: center;
overflow: elide;
}
ConfigTextField {
value <=> root.terminal-font-size-text;
field-width: 72px;
}
Text {
width: 26px;
height: parent.height;
text: "px";
color: Theme.text-muted;
font-size: Theme.fs-sm;
vertical-alignment: center;
}
Text {
width: 138px;
height: parent.height;
text: @tr("File manager font size");
color: Theme.text-secondary;
font-size: Theme.fs-sm;
vertical-alignment: center;
overflow: elide;
}
ConfigTextField {
value <=> root.sftp-font-size-text;
field-width: 72px;
}
Text {
width: 26px;
height: parent.height;
text: "px";
color: Theme.text-muted;
font-size: Theme.fs-sm;
vertical-alignment: center;
}
}
HorizontalLayout {
height: 30px;
spacing: 10px;
Text {
width: 148px;
height: parent.height;
text: @tr("Background image");
color: Theme.text-secondary;
font-size: Theme.fs-sm;
vertical-alignment: center;
overflow: elide;
}
ConfigValueBox {
horizontal-stretch: 1;
value: root.background-path-text;
empty-text: @tr("No background image");
}
ConfigButton {
label: @tr("Choose");
button-width: 82px;
clicked => {
if root.ui-debug-ids { root.debug-click("configuration", "choose-background"); }
root.app-background-pick();
}
}
ConfigButton {
label: @tr("Clear");
button-width: 72px;
clicked => {
if root.ui-debug-ids { root.debug-click("configuration", "clear-background"); }
root.app-background-clear();
}
}
}
HorizontalLayout {
height: 30px;
spacing: 10px;
Text {
width: 148px;
height: parent.height;
text: @tr("Background scope");
color: Theme.text-secondary;
font-size: Theme.fs-sm;
vertical-alignment: center;
overflow: elide;
}
ConfigButton {
label: @tr("Terminal");
button-width: 96px;
selected: root.background-mode == 1;
clicked => {
if root.ui-debug-ids { root.debug-click("configuration", "background-terminal"); }
root.background-mode = 1;
}
}
ConfigButton {
label: @tr("Fullscreen");
button-width: 104px;
selected: root.background-mode == 2;
clicked => {
if root.ui-debug-ids { root.debug-click("configuration", "background-fullscreen"); }
root.background-mode = 2;
}
}
Rectangle { horizontal-stretch: 1; }
Text {
height: parent.height;
text: @tr("Blur");
color: Theme.text-secondary;
font-size: Theme.fs-sm;
vertical-alignment: center;
}
ConfigTextField {
value <=> root.background-blur-text;
field-width: 58px;
}
}
HorizontalLayout {
height: 30px;
spacing: 10px;
Text {
width: 148px;
height: parent.height;
text: @tr("Terminal opacity");
color: Theme.text-secondary;
font-size: Theme.fs-sm;
vertical-alignment: center;
overflow: elide;
}
ConfigTextField {
value <=> root.terminal-opacity-text;
field-width: 68px;
}
Text {
width: 24px;
height: parent.height;
text: "%";
color: Theme.text-muted;
font-size: Theme.fs-sm;
vertical-alignment: center;
}
Text {
width: 108px;
height: parent.height;
text: @tr("UI opacity");
color: Theme.text-secondary;
font-size: Theme.fs-sm;
vertical-alignment: center;
overflow: elide;
}
ConfigTextField {
value <=> root.ui-opacity-text;
field-width: 68px;
}
Text {
width: 24px;
height: parent.height;
text: "%";
color: Theme.text-muted;
font-size: Theme.fs-sm;
vertical-alignment: center;
}
Rectangle { horizontal-stretch: 1; }
}
HorizontalLayout {
height: 30px;
spacing: 10px;
Text {
width: 148px;
height: parent.height;
text: @tr("Click log");
color: Theme.text-secondary;
font-size: Theme.fs-sm;
vertical-alignment: center;
overflow: elide;
}
ConfigButton {
label: root.ui-debug-ids ? @tr("On") : @tr("Off");
button-width: 76px;
selected: root.ui-debug-ids;
clicked => {
root.ui-debug-ids = !root.ui-debug-ids;
if root.ui-debug-ids { root.debug-click("configuration", "click-log-on"); }
}
}
Text {
text: @tr("Writes clicked button/parent ids to the debug log");
color: Theme.text-muted;
font-size: Theme.fs-xs;
vertical-alignment: center;
horizontal-stretch: 1;
overflow: elide;
}
}
HorizontalLayout {
height: 30px;
spacing: 10px;
Text {
width: 148px;
height: parent.height;
text: @tr("Configuration directory");
color: Theme.text-secondary;
font-size: Theme.fs-sm;
vertical-alignment: center;
overflow: elide;
}
ConfigValueBox {
horizontal-stretch: 1;
value: root.config-dir-text;
empty-text: "";
}
ConfigButton {
label: @tr("Choose");
button-width: 82px;
clicked => {
if root.ui-debug-ids { root.debug-click("configuration", "choose-config-dir"); }
root.app-config-pick-dir();
}
}
}
// --- WebDAV Sync Settings ---
Rectangle { height: 1px; background: Theme.border-subtle; }
HorizontalLayout {
height: 30px;
spacing: 10px;
Text {
width: 148px;
height: parent.height;
text: @tr("WebDAV sync");
color: Theme.text-secondary;
font-size: Theme.fs-sm;
font-weight: 600;
vertical-alignment: center;
overflow: elide;
}
ConfigButton {
label: root.webdav-enabled ? @tr("On") : @tr("Off");
button-width: 76px;
selected: root.webdav-enabled;
clicked => {
root.webdav-enabled = !root.webdav-enabled;
}
}
Rectangle { horizontal-stretch: 1; }
}
HorizontalLayout {
height: 30px;
spacing: 10px;
Text {
width: 148px;
height: parent.height;
text: @tr("WebDAV URL");
color: Theme.text-secondary;
font-size: Theme.fs-sm;
vertical-alignment: center;
overflow: elide;
}
ConfigTextField {
value <=> root.webdav-url-text;
field-width: 300px;
}
}
HorizontalLayout {
height: 30px;
spacing: 10px;
Text {
width: 148px;
height: parent.height;
text: @tr("WebDAV username");
color: Theme.text-secondary;
font-size: Theme.fs-sm;
vertical-alignment: center;
overflow: elide;
}
ConfigTextField {
value <=> root.webdav-username-text;
field-width: 160px;
}
Text {
width: 80px;
height: parent.height;
text: @tr("Password");
color: Theme.text-secondary;
font-size: Theme.fs-sm;
vertical-alignment: center;
overflow: elide;
}
ConfigTextField {
value <=> root.webdav-password-text;
field-width: 140px;
}
}
HorizontalLayout {
height: 30px;
spacing: 10px;
Text {
width: 148px;
height: parent.height;
text: @tr("Auto-sync interval (min)");
color: Theme.text-secondary;
font-size: Theme.fs-sm;
vertical-alignment: center;
overflow: elide;
}
ConfigTextField {
value <=> root.webdav-auto-sync-interval;
field-width: 80px;
}
Text {
height: parent.height;
text: @tr("0 = off");
color: Theme.text-muted;
font-size: Theme.fs-xs;
vertical-alignment: center;
overflow: elide;
}
}
HorizontalLayout {
height: 30px;
spacing: 10px;
Text {
width: 148px;
height: parent.height;
text: root.webdav-status-text;
color: root.webdav-status-text == @tr("Connection OK") ? Theme.success : Theme.text-muted;
font-size: Theme.fs-xs;
vertical-alignment: center;
overflow: elide;
horizontal-stretch: 1;
}
ConfigButton {
label: @tr("Test");
button-width: 72px;
clicked => { root.webdav-test-connection(); }
}
ConfigButton {
label: @tr("Upload");
button-width: 82px;
clicked => { root.webdav-push(); }
}
ConfigButton {
label: @tr("Download");
button-width: 82px;
clicked => { root.webdav-pull(); }
}
}
HorizontalLayout {
height: 30px;
spacing: 10px;
Rectangle { horizontal-stretch: 1; }
ConfigButton {
label: @tr("Cancel");
button-width: 82px;
clicked => {
if root.ui-debug-ids { root.debug-click("configuration", "cancel"); }
root.config-open = false;
}
}
ConfigButton {
label: @tr("Save");
button-width: 82px;
primary: true;
clicked => {
if root.ui-debug-ids { root.debug-click("configuration", "save"); }
root.webdav-save-settings(
root.webdav-enabled,
root.webdav-url-text,
root.webdav-username-text,
root.webdav-password-text,
root.webdav-auto-sync-interval);
root.app-config-save(
root.max-tabs-text,
root.terminal-font-size-text,
root.sftp-font-size-text,
root.background-mode,
root.background-blur-text,
root.terminal-opacity-text,
root.ui-opacity-text,
root.ui-debug-ids,
root.config-dir-text);
}
}
}
}
}
}
// --- About dialog (centered on the window, with dim backdrop) ---------
Rectangle {
width: parent.width;
height: parent.height;
visible: root.about-open;
background: #00000080; // dim backdrop
TouchArea { clicked => { root.about-open = false; } }
Rectangle {
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
width: 380px;
height: 440px;
background: Theme.bg-panel;
border-radius: Theme.radius-md;
border-width: 1px;
border-color: Theme.border-strong;
drop-shadow-blur: 24px;
drop-shadow-color: #000000a0;
// Swallow clicks so the backdrop doesn't close when clicking inside.
TouchArea {}
VerticalLayout {
padding: 18px;
spacing: 8px;
HorizontalLayout {
Text {
text: @tr("About meatshell");
color: Theme.text-primary;
font-size: Theme.fs-lg;
font-weight: 700;
horizontal-stretch: 1;
vertical-alignment: center;
}
Rectangle {
width: 22px; height: 22px;
border-radius: Theme.radius-sm;
background: ab-close-ta.has-hover ? Theme.bg-hover : transparent;
ab-close-ta := TouchArea {
mouse-cursor: pointer;
clicked => { root.about-open = false; }
}
Text { text: "\u{E5CD}"; font-family: "Material Icons";
color: Theme.text-secondary; font-size: Theme.fs-sm;
width: parent.width; height: parent.height;
horizontal-alignment: center; vertical-alignment: center; }
}
}
Text {
text: @tr("A lightweight Rust + Slint SSH terminal client by 'lil meatball'.");
color: Theme.text-secondary;
font-size: Theme.fs-sm;
wrap: word-wrap;
}
Text {
text: @tr("Open source · MIT / Apache-2.0");
color: Theme.text-muted;
font-size: Theme.fs-xs;
}
Rectangle { height: 1px; background: Theme.border-subtle; }
Text {
text: @tr("Open-source libraries used");
color: Theme.text-secondary;
font-size: Theme.fs-sm;
}
Flickable {
vertical-stretch: 1;
viewport-width: self.width;
viewport-height: max(self.height, libs-col.preferred-height);
libs-col := VerticalLayout {
alignment: start;
spacing: 4px;
for lib in root.about-libs : Text {
text: lib;
color: Theme.text-primary;
font-size: Theme.fs-xs;
wrap: no-wrap;
overflow: elide;
}
}
}
}
}
}
// --- Connection folder dialog ----------------------------------------
Rectangle {
width: parent.width;
height: parent.height;
visible: root.folder-dialog-open;
background: #00000070;
TouchArea { clicked => { root.folder-dialog-open = false; } }
Rectangle {
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
width: min(360px, parent.width - 48px);
height: 194px;
background: Theme.bg-panel;
border-radius: Theme.radius-md;
border-width: 1px;
border-color: Theme.border-strong;
drop-shadow-blur: 20px;
drop-shadow-color: #00000080;
TouchArea {}
VerticalLayout {
padding: 16px;
spacing: 12px;
Text {
text: root.folder-dialog-mode == "rename" ? @tr("Rename category") : @tr("New category");
color: Theme.text-primary;
font-size: Theme.fs-md;
font-weight: 600;
}
Rectangle {
height: 32px;
border-radius: Theme.radius-sm;
border-width: 1px;
border-color: folder-name-input.has-focus ? Theme.accent : Theme.border-subtle;
background: Theme.bg-root;
folder-name-input := TextInput {
x: 10px;
width: parent.width - 20px;
height: parent.height;
text <=> root.folder-draft;
color: Theme.text-primary;
font-size: Theme.fs-md;
single-line: true;
vertical-alignment: center;
edited => { root.folder-error = ""; }
init => { self.focus(); }
key-pressed(e) => {
if (e.text == "\n") {
if (root.folder-dialog-mode == "rename") {
root.connection-folder-rename(root.folder-edit-original, root.folder-draft);
} else {
root.connection-folder-create(root.folder-draft);
}
accept
} else {
reject
}
}
}
if root.folder-draft == "" : Text {
x: 10px;
height: parent.height;
text: @tr("Category name");
color: Theme.text-muted;
font-size: Theme.fs-md;
vertical-alignment: center;
}
}
Text {
height: 14px;
text: root.folder-error;
color: Theme.danger;
font-size: Theme.fs-xs;
visible: root.folder-error != "";
}
HorizontalLayout {
height: 28px;
spacing: 8px;
Rectangle { horizontal-stretch: 1; }
Rectangle {
width: 72px;
height: parent.height;
border-radius: Theme.radius-sm;
background: folder-cancel-ta.has-hover ? Theme.bg-hover : Theme.bg-elevated;
folder-cancel-ta := TouchArea {
mouse-cursor: pointer;
clicked => { root.folder-dialog-open = false; }
}
Text {
width: parent.width;
height: parent.height;
text: @tr("Cancel");
color: Theme.text-primary;
font-size: Theme.fs-sm;
horizontal-alignment: center;
vertical-alignment: center;
}
}
Rectangle {
width: 72px;
height: parent.height;
border-radius: Theme.radius-sm;
background: folder-create-ta.has-hover ? Theme.accent-hover : Theme.accent;
folder-create-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
if (root.folder-dialog-mode == "rename") {
root.connection-folder-rename(root.folder-edit-original, root.folder-draft);
} else {
root.connection-folder-create(root.folder-draft);
}
}
}
Text {
width: parent.width;
height: parent.height;
text: root.folder-dialog-mode == "rename" ? @tr("Rename") : @tr("Create");
color: #ffffff;
font-size: Theme.fs-sm;
horizontal-alignment: center;
vertical-alignment: center;
}
}
}
}
}
}
// --- Modal dialog -----------------------------------------------------
SessionDialog {
width: parent.width;
height: parent.height;
is-open <=> root.dialog-open;
is-editing <=> root.dialog-editing;
draft-id <=> root.dialog-id;
draft-name <=> root.dialog-name;
draft-group <=> root.dialog-group;
draft-host <=> root.dialog-host;
draft-port <=> root.dialog-port;
draft-user <=> root.dialog-user;
draft-auth <=> root.dialog-auth;
draft-password <=> root.dialog-password;
draft-key-path <=> root.dialog-key-path;
draft-proxy <=> root.dialog-proxy;
draft-kind <=> root.dialog-kind;
draft-serial-port <=> root.dialog-serial-port;
draft-baud <=> root.dialog-baud;
draft-data-bits <=> root.dialog-data-bits;
draft-stop-bits <=> root.dialog-stop-bits;
draft-parity <=> root.dialog-parity;
draft-flow <=> root.dialog-flow;
folders: root.connection-folder-names;
submit(draft) => { root.session-dialog-submit(draft); }
cancel => { root.session-dialog-cancel(); }
pick-key-file => { root.session-dialog-pick-key(); }
}
// --- Delete confirmation (#28) ----------------------------------------
ConfirmDialog {
width: parent.width;
height: parent.height;
is-open <=> root.confirm-delete-open;
title: @tr("Confirm delete");
message: @tr("Delete this item? This cannot be undone.");
detail: root.confirm-delete-path;
confirm-label: @tr("Delete");
confirm => {
root.sftp-delete(root.confirm-delete-tab, root.confirm-delete-path);
root.confirm-delete-open = false;
}
cancel => { root.confirm-delete-open = false; }
}
}