v1.8.0: file manager improvements

- SFTP cleanup on app close and language switch
- Windows drive selector in local panel
- Browse and Refresh buttons for local panel
- Recursive upload/download/delete of folders
- Drag-and-drop between local and remote panels
- Sudo mode toggle for privileged file operations
- New i18n keys for EN/RU/ZH

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
chrome-storm-c442
2026-02-23 16:30:56 -05:00
parent a77ca6fee7
commit 3e9aeababe
7 changed files with 528 additions and 103 deletions

View File

@@ -49,17 +49,23 @@ def _apply_dark_theme():
class FileListWidget(ctk.CTkFrame):
"""File list with columns, sorting, multi-selection."""
"""File list with columns, sorting, multi-selection, drag-and-drop."""
def __init__(self, master, columns: list[tuple[str, int]],
on_navigate=None, on_select=None):
on_navigate=None, on_select=None, on_drop=None):
super().__init__(master, fg_color="#1e1e1e", corner_radius=6)
self._on_navigate = on_navigate
self._on_select = on_select
self._on_drop = on_drop
self._items: list[dict] = []
self._sort_col = None
self._sort_reverse = False
self._drag_partner: "FileListWidget | None" = None
self._drag_start_x = 0
self._drag_start_y = 0
self._dragging = False
self._orig_fg_color = "#1e1e1e"
_apply_dark_theme()
@@ -91,6 +97,11 @@ class FileListWidget(ctk.CTkFrame):
self._tree.bind("<<TreeviewSelect>>", self._on_tree_select)
self._tree.bind("<Return>", self._on_enter)
# Drag-and-drop bindings
self._tree.bind("<ButtonPress-1>", self._on_drag_start, add="+")
self._tree.bind("<B1-Motion>", self._on_drag_motion)
self._tree.bind("<ButtonRelease-1>", self._on_drag_release, add="+")
def populate(self, items: list[dict]):
"""Fill list with items. Each item: {name, size, date, perm, is_dir}."""
self._items = items
@@ -168,3 +179,46 @@ class FileListWidget(ctk.CTkFrame):
self._items = dirs + files
self.populate(self._items)
# ── Drag-and-drop ──
def set_drag_partner(self, partner: "FileListWidget"):
"""Link two panels for drag-and-drop."""
self._drag_partner = partner
def _on_drag_start(self, event):
self._drag_start_x = event.x_root
self._drag_start_y = event.y_root
self._dragging = False
def _on_drag_motion(self, event):
if not self._drag_partner:
return
dx = abs(event.x_root - self._drag_start_x)
dy = abs(event.y_root - self._drag_start_y)
if not self._dragging and (dx > 10 or dy > 10):
sel = self.get_selected()
if sel and any(s["name"] != ".." for s in sel):
self._dragging = True
self._tree.configure(cursor="hand2")
self._drag_partner.configure(fg_color="#1e3a5f")
def _on_drag_release(self, event):
if not self._dragging or not self._drag_partner:
self._dragging = False
return
self._dragging = False
self._tree.configure(cursor="")
self._drag_partner.configure(fg_color=self._orig_fg_color)
# Check if mouse is over partner widget
px = self._drag_partner.winfo_rootx()
py = self._drag_partner.winfo_rooty()
pw = self._drag_partner.winfo_width()
ph = self._drag_partner.winfo_height()
mx, my = event.x_root, event.y_root
if px <= mx <= px + pw and py <= my <= py + ph:
items = [s for s in self.get_selected() if s["name"] != ".."]
if items and self._drag_partner._on_drop:
self._drag_partner._on_drop(items, self)