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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user