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

591
ui/sftp_panel.slint Normal file
View File

@@ -0,0 +1,591 @@
import { Theme } from "theme.slint";
import { ScrollView } from "std-widgets.slint";
// ---------------------------------------------------------------------------
// Data types
// ---------------------------------------------------------------------------
export struct SftpEntry {
name: string,
full-path: string,
is-dir: bool,
size: string,
modified: string,
}
export struct SftpTreeNode {
path: string,
name: string,
depth: int,
expanded: bool,
has-children: bool,
}
// One row of the file-list right-click context menu.
component SftpMenuItem inherits Rectangle {
in property <string> label;
in property <brush> tint: Theme.text-primary;
callback clicked();
height: 26px;
border-radius: Theme.radius-sm;
background: ta.has-hover ? Theme.bg-hover : transparent;
ta := TouchArea {
mouse-cursor: pointer;
clicked => { root.clicked(); }
}
Text {
x: 10px;
text: root.label;
color: root.tint;
font-size: Theme.fs-sm;
vertical-alignment: center;
height: parent.height;
}
}
// ---------------------------------------------------------------------------
// SftpPanel — left directory tree + right file listing
// ---------------------------------------------------------------------------
export component SftpPanel inherits Rectangle {
// --- Properties --------------------------------------------------------
in property <string> current-path: "/";
in property <[SftpEntry]> entries;
in property <string> status: @tr("SFTP not connected");
in property <bool> loading: false;
in property <[SftpTreeNode]> tree-nodes: [];
in property <bool> app-background-active: false;
in property <float> ui-opacity: 0.72;
in-out property <string> path-draft: "/";
in property <length> file-font-size: 16px;
in-out property <length> name-column-width: 180px;
in-out property <bool> name-column-manual: false;
property <bool> rename-open: false;
property <string> rename-path;
property <string> rename-draft;
property <bool> name-column-dragging: false;
property <length> name-column-drag-start-x: 0px;
// --- Callbacks ---------------------------------------------------------
callback navigate(string);
callback download(string);
callback upload-clicked(string);
callback refresh(string);
callback tree-expand(string); // toggle expand/collapse + navigate
callback delete(string); // remove a remote file/dir
callback rename(string, string);
callback edit(string); // download to temp + open + auto-reupload
callback name-column-width-changed(float);
callback name-column-auto();
// -----------------------------------------------------------------------
background: root.app-background-active ? Theme.bg-panel.with-alpha(root.ui-opacity) : Theme.bg-panel;
changed current-path => { root.path-draft = root.current-path; }
VerticalLayout {
spacing: 0;
// --- Toolbar (full width) ------------------------------------------
Rectangle {
height: 30px;
background: root.app-background-active ? Theme.bg-panel-alt.with-alpha(root.ui-opacity) : Theme.bg-panel-alt;
HorizontalLayout {
height: parent.height;
padding-left: 6px;
padding-right: 6px;
spacing: 4px;
// Editable path bar fills the left; action buttons sit on the right.
Rectangle {
y: (parent.height - self.height) / 2;
horizontal-stretch: 1;
height: 22px;
border-radius: Theme.radius-sm;
border-width: 1px;
border-color: path-input.has-focus ? Theme.accent : Theme.border-subtle;
background: root.app-background-active ? Theme.bg-root.with-alpha(root.ui-opacity) : Theme.bg-root;
path-input := TextInput {
x: 8px;
width: parent.width - 16px;
height: parent.height;
text <=> root.path-draft;
color: Theme.text-primary;
font-size: root.file-font-size;
single-line: true;
vertical-alignment: center;
key-pressed(e) => {
if (e.text == "\n") {
root.navigate(self.text);
accept
} else {
reject
}
}
}
}
Rectangle {
y: (parent.height - self.height) / 2;
width: 24px; height: 22px;
border-radius: Theme.radius-sm;
background: go-ta.has-hover ? Theme.bg-hover : transparent;
go-ta := TouchArea {
mouse-cursor: pointer;
clicked => { root.navigate(root.path-draft); }
}
Text { text: "\u{E5C8}"; 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; }
}
up-btn := Rectangle {
y: (parent.height - self.height) / 2;
width: 24px; height: 22px;
border-radius: Theme.radius-sm;
background: up-ta.has-hover ? Theme.bg-hover : transparent;
up-ta := TouchArea {
mouse-cursor: pointer;
clicked => { root.navigate(".."); }
}
Text { text: "↑"; color: Theme.text-secondary; font-size: Theme.fs-md;
width: parent.width; height: parent.height;
horizontal-alignment: center; vertical-alignment: center; }
}
Rectangle {
y: (parent.height - self.height) / 2;
width: 52px; height: 22px;
border-radius: Theme.radius-sm;
background: ul-ta.has-hover ? Theme.accent-hover : Theme.accent;
ul-ta := TouchArea {
mouse-cursor: pointer;
clicked => { root.upload-clicked(root.current-path); }
}
Text { text: @tr("Upload"); color: #ffffff; font-size: Theme.fs-sm;
width: parent.width; height: parent.height;
horizontal-alignment: center; vertical-alignment: center; }
}
Rectangle {
y: (parent.height - self.height) / 2;
width: 24px; height: 22px;
border-radius: Theme.radius-sm;
background: ref-ta.has-hover ? Theme.bg-hover : transparent;
ref-ta := TouchArea {
mouse-cursor: pointer;
clicked => { root.refresh(root.current-path); }
}
Text { text: "\u{E5D5}"; 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; }
}
}
}
// --- Main area: tree (left) + file list (right) --------------------
HorizontalLayout {
vertical-stretch: 1;
spacing: 0;
// ── Left: directory tree ────────────────────────────────────────
VerticalLayout {
width: 160px;
spacing: 0;
// Tree header
Rectangle {
height: 20px;
background: root.app-background-active ? Theme.bg-root.with-alpha(root.ui-opacity) : Theme.bg-root;
Text {
x: 8px;
text: @tr("Directory tree");
color: Theme.text-muted;
font-size: max(10px, root.file-font-size - 2px);
vertical-alignment: center;
height: parent.height;
}
}
Rectangle { height: 1px; background: Theme.border-subtle; }
// Tree list with scrollbar
ScrollView {
vertical-stretch: 1;
tree-list := VerticalLayout {
spacing: 0;
for node[i] in root.tree-nodes : Rectangle {
height: max(24px, root.file-font-size + 10px);
background: node-ta.has-hover
? Theme.bg-hover
: (node.path == root.current-path
? #4a90e230
: (mod(i, 2) == 0 ? transparent : #ffffff08));
node-ta := TouchArea {
mouse-cursor: pointer;
clicked => { root.tree-expand(node.path); }
}
HorizontalLayout {
height: parent.height;
spacing: 0;
// Depth indent spacer
Rectangle { width: node.depth * 12px; }
// Expand/collapse arrow
Text {
width: 14px;
text: node.has-children
? (node.expanded ? "▼" : "▶")
: " ";
color: Theme.text-muted;
font-size: max(9px, root.file-font-size - 3px);
horizontal-alignment: center;
vertical-alignment: center;
}
// Folder name
Text {
text: node.depth == 0 ? "/" : node.name;
color: node.path == root.current-path
? Theme.accent
: Theme.text-primary;
font-size: root.file-font-size;
horizontal-stretch: 1;
overflow: elide;
vertical-alignment: center;
}
}
}
}
}
}
// Vertical separator
Rectangle { width: 1px; background: Theme.border-subtle; }
// ── Right: file listing ─────────────────────────────────────────
right-pane := VerticalLayout {
horizontal-stretch: 1;
spacing: 0;
// Column headers
Rectangle {
height: 20px;
background: root.app-background-active ? Theme.bg-root.with-alpha(root.ui-opacity) : Theme.bg-root;
HorizontalLayout {
height: parent.height;
padding-left: 8px; padding-right: 8px;
Text { text: @tr("Name"); color: Theme.text-muted; font-size: max(10px, root.file-font-size - 2px);
width: clamp(root.name-column-width, 96px, 360px); vertical-alignment: center; }
Rectangle {
width: 6px;
height: parent.height;
background: root.name-column-dragging || name-resize-ta.has-hover ? Theme.accent.with-alpha(0.45) : transparent;
name-resize-ta := TouchArea {
mouse-cursor: col-resize;
pointer-event(ev) => {
if (ev.kind == PointerEventKind.down) {
root.name-column-dragging = true;
root.name-column-drag-start-x = self.mouse-x;
root.name-column-manual = true;
} else if (ev.kind == PointerEventKind.up) {
root.name-column-dragging = false;
root.name-column-width-changed(root.name-column-width / 1px);
}
}
moved => {
if (root.name-column-dragging) {
root.name-column-width = clamp(
root.name-column-width + self.mouse-x - root.name-column-drag-start-x,
96px,
360px
);
root.name-column-drag-start-x = self.mouse-x;
}
}
}
}
if root.name-column-manual : Rectangle {
y: (parent.height - self.height) / 2;
width: 42px;
height: 18px;
border-radius: Theme.radius-sm;
background: name-auto-ta.has-hover ? Theme.bg-hover : transparent;
name-auto-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
root.name-column-manual = false;
root.name-column-auto();
root.refresh(root.current-path);
}
}
Text {
width: parent.width;
height: parent.height;
text: @tr("Auto");
color: Theme.accent;
font-size: max(10px, root.file-font-size - 3px);
horizontal-alignment: center;
vertical-alignment: center;
}
}
Rectangle { horizontal-stretch: 1; }
Text { text: @tr("Size"); color: Theme.text-muted; font-size: max(10px, root.file-font-size - 2px);
width: 72px; horizontal-alignment: right; vertical-alignment: center; }
Text { text: @tr("Modified"); color: Theme.text-muted; font-size: max(10px, root.file-font-size - 2px);
width: 116px; horizontal-alignment: right; vertical-alignment: center; }
}
}
Rectangle { height: 1px; background: Theme.border-subtle; }
// File list with scrollbar
ScrollView {
vertical-stretch: 1;
file-list := VerticalLayout {
spacing: 0;
if root.loading : Rectangle {
height: 40px;
Text { width: parent.width; height: parent.height;
text: @tr("Loading..."); color: Theme.text-muted; font-size: root.file-font-size;
horizontal-alignment: center; vertical-alignment: center; }
}
if !root.loading && root.entries.length == 0 : Rectangle {
height: 40px;
Text { width: parent.width; height: parent.height;
text: @tr("Empty directory"); color: Theme.text-muted; font-size: root.file-font-size;
horizontal-alignment: center; vertical-alignment: center; }
}
for entry[i] in root.entries : row := Rectangle {
height: max(26px, root.file-font-size + 12px);
background: row-ta.has-hover
? #4a90e225
: (mod(i, 2) == 0 ? transparent : #ffffff08);
// Right-click anchor (relative to this row).
property <length> mx;
property <length> my;
row-ta := TouchArea {
mouse-cursor: pointer;
double-clicked => {
if (entry.is-dir) {
root.navigate(entry.full-path);
} else {
root.edit(entry.full-path);
}
}
pointer-event(ev) => {
if (ev.kind == PointerEventKind.down
&& ev.button == PointerEventButton.right) {
row.mx = self.mouse-x;
row.my = self.mouse-y;
row-menu.show();
}
}
}
// Right-click context menu.
row-menu := PopupWindow {
x: row.mx;
y: row.my;
width: 108px;
height: (entry.is-dir ? 2 : 4) * 27px + 8px;
Rectangle {
background: Theme.bg-panel;
border-radius: Theme.radius-sm;
border-width: 1px;
border-color: Theme.border-strong;
drop-shadow-blur: 12px;
drop-shadow-color: #00000060;
VerticalLayout {
padding: 4px;
spacing: 1px;
if !entry.is-dir : SftpMenuItem {
label: @tr("Download");
clicked => { root.download(entry.full-path); }
}
SftpMenuItem {
label: @tr("Rename");
clicked => {
root.rename-path = entry.full-path;
root.rename-draft = entry.name;
root.rename-open = true;
}
}
if !entry.is-dir : SftpMenuItem {
label: @tr("Edit");
clicked => { root.edit(entry.full-path); }
}
SftpMenuItem {
label: @tr("Delete");
tint: #d86c6c;
clicked => { root.delete(entry.full-path); }
}
}
}
}
HorizontalLayout {
height: parent.height;
padding-left: 8px; padding-right: 8px; spacing: 4px;
Text {
text: entry.is-dir ? "📁 " + entry.name : "📄 " + entry.name;
color: entry.is-dir ? Theme.accent : Theme.text-primary;
font-size: root.file-font-size;
width: clamp(root.name-column-width, 96px, 360px);
overflow: elide;
vertical-alignment: center;
}
Rectangle { width: 6px; height: parent.height; }
Rectangle { horizontal-stretch: 1; }
Text {
text: entry.is-dir ? "" : entry.size;
color: Theme.text-secondary; font-size: max(10px, root.file-font-size - 2px);
width: 72px; horizontal-alignment: right; vertical-alignment: center;
}
Text {
text: entry.modified;
color: Theme.text-muted; font-size: max(10px, root.file-font-size - 2px);
width: 116px; horizontal-alignment: right; vertical-alignment: center;
}
}
}
}
}
}
}
if root.rename-open : Rectangle {
width: parent.width;
height: parent.height;
background: #00000040;
TouchArea { clicked => { root.rename-open = false; } }
Rectangle {
x: (parent.width - self.width) / 2;
y: 40px;
width: min(340px, parent.width - 32px);
height: 118px;
border-radius: Theme.radius-md;
border-width: 1px;
border-color: Theme.border-strong;
background: Theme.bg-panel;
drop-shadow-blur: 16px;
drop-shadow-color: #00000080;
TouchArea {}
VerticalLayout {
padding: 12px;
spacing: 8px;
Text {
text: @tr("Rename");
color: Theme.text-primary;
font-size: Theme.fs-sm;
font-weight: 600;
}
Rectangle {
height: 30px;
border-radius: Theme.radius-sm;
border-width: 1px;
border-color: rename-input.has-focus ? Theme.accent : Theme.border-subtle;
background: Theme.bg-root;
rename-input := TextInput {
x: 8px;
width: parent.width - 16px;
height: parent.height;
text <=> root.rename-draft;
color: Theme.text-primary;
font-size: Theme.fs-sm;
single-line: true;
vertical-alignment: center;
key-pressed(e) => {
if (e.text == "\n") {
root.rename(root.rename-path, root.rename-draft);
root.rename-open = false;
accept
} else {
reject
}
}
}
}
HorizontalLayout {
height: 28px;
spacing: 8px;
Rectangle { horizontal-stretch: 1; }
Rectangle {
width: 68px;
height: parent.height;
border-radius: Theme.radius-sm;
background: rn-cancel-ta.has-hover ? Theme.bg-hover : Theme.bg-elevated;
rn-cancel-ta := TouchArea {
mouse-cursor: pointer;
clicked => { root.rename-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: 68px;
height: parent.height;
border-radius: Theme.radius-sm;
background: rn-ok-ta.has-hover ? Theme.accent-hover : Theme.accent;
rn-ok-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
root.rename(root.rename-path, root.rename-draft);
root.rename-open = false;
}
}
Text {
width: parent.width;
height: parent.height;
text: @tr("Save");
color: #ffffff;
font-size: Theme.fs-sm;
horizontal-alignment: center;
vertical-alignment: center;
}
}
}
}
}
}
// --- Status bar (full width) --------------------------------------
Rectangle {
height: 18px;
background: root.app-background-active ? Theme.bg-panel-alt.with-alpha(root.ui-opacity) : Theme.bg-panel-alt;
Text {
x: 8px;
text: root.status;
color: Theme.text-muted;
font-size: Theme.fs-xs;
vertical-alignment: center;
height: parent.height;
overflow: elide;
width: parent.width - 16px;
}
}
}
}