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 label; in property 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 current-path: "/"; in property <[SftpEntry]> entries; in property status: @tr("SFTP not connected"); in property loading: false; in property <[SftpTreeNode]> tree-nodes: []; in property app-background-active: false; in property ui-opacity: 0.72; in-out property path-draft: "/"; in property file-font-size: 16px; in-out property name-column-width: 180px; in-out property name-column-manual: false; property rename-open: false; property rename-path; property rename-draft; property name-column-dragging: false; property 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 mx; property 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; } } } }