v1.9.5: S3 — new folder, folder delete with confirmation, folder drag-and-drop

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
chrome-storm-c442
2026-03-03 08:07:40 -05:00
parent 61461767fd
commit f233d5cf70
5 changed files with 130 additions and 17 deletions

View File

@@ -116,6 +116,12 @@ class S3Tab(ctk.CTkFrame):
)
self._refresh_btn.pack(side="left", padx=(0, 5))
self._mkdir_btn = make_icon_button(
btn_frame, "add", t("s3_new_folder"), width=120,
command=self._create_folder,
)
self._mkdir_btn.pack(side="left", padx=(0, 5))
self._upload_btn = make_icon_button(
btn_frame, "upload", t("s3_upload"), width=100,
command=self._upload,
@@ -222,6 +228,11 @@ class S3Tab(ctk.CTkFrame):
command=self._download,
)
self._ctx_menu.add_separator()
self._ctx_menu.add_command(
label=icon_text("add", t("s3_new_folder")),
command=self._create_folder,
)
self._ctx_menu.add_separator()
self._ctx_menu.add_command(
label=icon_text("delete", t("s3_delete")),
command=self._delete,
@@ -516,10 +527,12 @@ class S3Tab(ctk.CTkFrame):
is_folder = name.endswith("/")
# Enable/disable menu items based on type
state = "normal" if not is_folder else "disabled"
self._ctx_menu.entryconfigure(0, state=state) # Link 48h
self._ctx_menu.entryconfigure(1, state=state) # Link permanent
self._ctx_menu.entryconfigure(3, state=state) # Download
self._ctx_menu.entryconfigure(5, state=state) # Delete
file_state = "normal" if not is_folder else "disabled"
self._ctx_menu.entryconfigure(0, state=file_state) # Link 48h
self._ctx_menu.entryconfigure(1, state=file_state) # Link permanent
self._ctx_menu.entryconfigure(3, state=file_state) # Download
# 5 = New Folder — always enabled
# 7 = Delete — enabled for both files and folders
self._ctx_menu.tk_popup(event.x_root, event.y_root)
def _get_selected_key(self) -> str | None:
@@ -570,6 +583,31 @@ class S3Tab(ctk.CTkFrame):
else:
self._status_label.configure(text=t("s3_link_failed"))
def _create_folder(self):
"""Prompt for folder name and create it as an empty prefix in S3."""
if not self._client or not self._current_bucket:
return
dialog = ctk.CTkInputDialog(
text=t("s3_folder_name_prompt"),
title=t("s3_new_folder"),
)
name = dialog.get_input()
if not name or not name.strip():
return
name = name.strip().strip("/")
key = self._current_prefix + name + "/"
self._status_label.configure(text=t("s3_creating_folder"))
def _do():
ok = self._client.create_folder(self._current_bucket, key)
if ok:
self.after(0, self._refresh)
else:
self.after(0, lambda: self._status_label.configure(
text=t("s3_folder_failed")))
threading.Thread(target=_do, daemon=True).start()
def _go_back(self):
if self._nav_stack:
self._current_prefix = self._nav_stack.pop()
@@ -630,24 +668,44 @@ class S3Tab(ctk.CTkFrame):
def _delete(self):
sel = self._tree.selection()
if not sel:
if not sel or not self._client or not self._current_bucket:
return
item = self._tree.item(sel[0])
name = item["values"][0] if item["values"] else ""
if not isinstance(name, str):
name = str(name)
if name.endswith("/"):
return # Don't delete prefixes
# Folder — ask confirmation
clean = name.replace("\U0001f4c1 ", "").strip()
prefix = self._current_prefix + clean
from tkinter import messagebox
ok = messagebox.askyesno(
t("s3_delete"),
t("s3_delete_folder_confirm").format(folder=clean),
)
if not ok:
return
self._status_label.configure(text=t("s3_deleting"))
key = self._current_prefix + name
def _do():
ok = self._client.delete_object(self._current_bucket, key)
if ok:
self.after(0, self._refresh)
else:
def _do_folder():
count = self._client.delete_prefix(self._current_bucket, prefix)
self.after(0, lambda: self._status_label.configure(
text=t("s3_delete_failed")))
text=t("s3_deleted_n").format(count=count)))
self.after(0, self._refresh)
self._status_label.configure(text=t("s3_deleting"))
threading.Thread(target=_do, daemon=True).start()
threading.Thread(target=_do_folder, daemon=True).start()
else:
# Single file
key = self._current_prefix + name
def _do():
ok = self._client.delete_object(self._current_bucket, key)
if ok:
self.after(0, self._refresh)
else:
self.after(0, lambda: self._status_label.configure(
text=t("s3_delete_failed")))
self._status_label.configure(text=t("s3_deleting"))
threading.Thread(target=_do, daemon=True).start()