Improved configuration file management with advanced features

- Added send2trash library for safer file deletion
- Implemented folder and file moving functionality in ConfigSelectorWindow
- Created FolderSelectorDialog for moving files to a folder
- Improved file and folder renaming with improved error handling
- Added emoji icons for better visual representation of actions
- Updated requirements.txt to include send2trash library
- Removed drag-and-drop support. Postponed for future
This commit is contained in:
2025-02-19 23:06:15 +03:00
parent ea432d2893
commit 11253286f8
2 changed files with 425 additions and 263 deletions

View File

@@ -38,6 +38,7 @@ from about_window import AboutWindow
from TFTPServer import TFTPServer
import socket
from update_checker import UpdateChecker
from send2trash import send2trash
# Версия программы
VERSION = "1.0.2"
@@ -1248,201 +1249,201 @@ class SerialAppGUI(tk.Tk):
consecutive_errors = 0
MAX_CONSECUTIVE_ERRORS = 3 # Максимальное количество последовательных ошибок
def check_connection():
"""Проверка состояния соединения"""
# Если выполнение остановлено пользователем, просто возвращаем False без сообщений
if self.execution_stop:
return False
if not self.connection or not self.connection.is_open:
# Если остановка произошла во время проверки, не показываем сообщение
if self.execution_stop:
return False
self.append_file_exec_text("[ERROR] Соединение потеряно!\n")
# Автоматически ставим на паузу
self.execution_paused = True
self.after(0, lambda: self.pause_button.config(text="▶ Продолжить"))
# Показываем сообщение только если это не ручная остановка
if not self.execution_stop:
self.after(0, lambda: messagebox.showerror(
"Ошибка соединения",
"Соединение с устройством потеряно!\nВыполнение команд приостановлено.\n\n"
"Пожалуйста:\n"
"1. Проверьте подключение\n"
"2. Нажмите 'Продолжить' после восстановления соединения\n"
" или 'Остановить' для прекращения выполнения"
))
return False
return True
def handle_no_response(cmd_or_block, is_block=False):
"""Обработка отсутствия ответа от устройства"""
nonlocal consecutive_errors
consecutive_errors += 1
if consecutive_errors >= MAX_CONSECUTIVE_ERRORS:
self.append_file_exec_text(
f"[ERROR] Обнаружено {consecutive_errors} последовательных ошибок!\n"
"Возможно, устройство не отвечает или проблемы с соединением.\n"
)
# Автоматически ставим на паузу
self.execution_paused = True
self.after(0, lambda: self.pause_button.config(text="▶ Продолжить"))
# Показываем сообщение пользователю
self.after(0, lambda: messagebox.showerror(
"Устройство не отвечает",
f"Обнаружено {consecutive_errors} последовательных ошибок!\n\n"
"Возможные причины:\n"
"1. Устройство не отвечает на команды\n"
"2. Проблемы с соединением\n"
"3. Неверный формат команд\n\n"
"Выполнение приостановлено.\n"
"Проверьте подключение и состояние устройства,\n"
"затем нажмите 'Продолжить' или 'Остановить'."
))
if not self.execution_stop: # Проверяем, не была ли выполнена остановка
self.append_file_exec_text(
f"[ERROR] Обнаружено {consecutive_errors} последовательных ошибок!\n"
"Возможно, устройство не отвечает или проблемы с соединением.\n"
)
# Автоматически ставим на паузу
self.execution_paused = True
self.after(0, lambda: self.pause_button.config(text="▶ Продолжить"))
# Показываем сообщение пользователю
self.after(0, lambda: messagebox.showerror(
"Устройство не отвечает",
f"Обнаружено {consecutive_errors} последовательных ошибок!\n\n"
"Возможные причины:\n"
"1. Устройство не отвечает на команды\n"
"2. Проблемы с соединением\n"
"3. Неверный формат команд\n\n"
"Выполнение приостановлено.\n"
"Проверьте подключение и состояние устройства,\n"
"затем нажмите 'Продолжить' или 'Остановить'."
))
return False
return True
def wait_before_next_command():
"""Ожидание перед следующей командой с учетом паузы"""
while self.execution_paused and not self.execution_stop:
time.sleep(0.1)
if self.execution_stop:
return False
time.sleep(1) # Базовая задержка между командами
return True
if copy_mode == "line":
# Построчный режим
while self.current_command_index < len(self.commands):
if self.execution_stop:
break
if self.execution_paused:
time.sleep(0.1)
continue
# Проверяем соединение перед каждой командой
if not check_connection():
continue
cmd = self.commands[self.current_command_index]
try:
success, response = send_command_and_process_response(
self.connection,
cmd,
self.settings.get("timeout", 10),
max_attempts=3,
log_callback=self.append_file_exec_text,
login=self.settings.get("login"),
password=self.settings.get("password"),
is_gui=True
)
if not success:
self.append_file_exec_text(f"[ERROR] Не удалось выполнить команду: {cmd}\n")
# Проверяем соединение после неудачной попытки
if not check_connection():
continue
# Обрабатываем отсутствие ответа
if not handle_no_response(cmd):
continue
else:
# Сбрасываем счетчик ошибок при успешном выполнении
consecutive_errors = 0
try:
if copy_mode == "line":
# Построчный режим
while self.current_command_index < len(self.commands):
if self.execution_stop:
break
self.current_command_index += 1
self.after(0, self.update_progress)
time.sleep(1) # Задержка между командами
except Exception as e:
self.append_file_exec_text(f"[ERROR] Ошибка при выполнении команды: {str(e)}\n")
if not check_connection():
if not wait_before_next_command():
break
# Проверяем соединение перед каждой командой
if not self.check_connection():
if self.execution_stop: # Если это остановка, прерываем выполнение
break
continue
break
else:
# Блочный режим
blocks = generate_command_blocks(self.commands, block_size)
total_blocks = len(blocks)
current_block = 0
while current_block < total_blocks:
if self.execution_stop:
break
if self.execution_paused:
time.sleep(0.1)
continue
# Проверяем соединение перед каждым блоком
if not check_connection():
continue
block = blocks[current_block]
try:
# Выводим блок команд без [CMD] префикса
self.append_file_exec_text(f"Выполнение блока команд:\n{block}\n")
success, response = send_command_and_process_response(
self.connection,
block,
self.settings.get("timeout", 10),
max_attempts=3,
log_callback=None, # Отключаем вывод для первой попытки
login=self.settings.get("login"),
password=self.settings.get("password"),
is_gui=True
)
if not success or (response and '^' in response):
self.append_file_exec_text("[WARNING] Ошибка при выполнении блока команд. Отправляю команды по отдельности...\n")
# Проверяем соединение перед отправкой отдельных команд
if not check_connection():
continue
cmd = self.commands[self.current_command_index]
try:
success, response = send_command_and_process_response(
self.connection,
cmd,
self.settings.get("timeout", 10),
max_attempts=3,
log_callback=self.append_file_exec_text if not self.execution_stop else None,
login=self.settings.get("login"),
password=self.settings.get("password"),
is_gui=True
)
if self.execution_stop:
break
# Обрабатываем отсутствие ответа для блока
if not success and not handle_no_response(block, True):
continue
# Отправляем команды блока по отдельности
for cmd in block.splitlines():
if self.execution_stop:
break
if cmd.strip():
if not check_connection():
if not success:
if not self.execution_stop:
self.append_file_exec_text(f"[ERROR] Не удалось выполнить команду: {cmd}\n")
# Проверяем соединение после неудачной попытки
if not self.check_connection():
if self.execution_stop:
break
success, resp = send_command_and_process_response(
self.connection,
cmd,
self.settings.get("timeout", 10),
max_attempts=3,
log_callback=self.append_file_exec_text,
login=self.settings.get("login"),
password=self.settings.get("password"),
is_gui=True
)
if not success:
self.append_file_exec_text(f"[ERROR] Не удалось выполнить команду: {cmd}\n")
if not check_connection():
break
# Обрабатываем отсутствие ответа для отдельной команды
if not handle_no_response(cmd):
break
else:
# Сбрасываем счетчик ошибок при успешном выполнении
consecutive_errors = 0
else:
# Если блок выполнился успешно, выводим ответ и сбрасываем счетчик ошибок
consecutive_errors = 0
if response:
self.append_file_exec_text(f"Ответ устройства:\n{response}\n")
continue
# Обрабатываем отсутствие ответа
if not handle_no_response(cmd):
continue
else:
# Сбрасываем счетчик ошибок при успешном выполнении
consecutive_errors = 0
self.current_command_index += 1
self.after(0, self.update_progress)
except Exception as e:
if not self.execution_stop:
self.append_file_exec_text(f"[ERROR] Ошибка при выполнении команды: {str(e)}\n")
break
else:
# Блочный режим
blocks = generate_command_blocks(self.commands, block_size)
total_blocks = len(blocks)
current_block = 0
while current_block < total_blocks:
if self.execution_stop:
break
if not wait_before_next_command():
break
# Обновляем прогресс на основе количества выполненных блоков
current_block += 1
self.current_command_index = (current_block * 100) // total_blocks
self.after(0, self.update_progress)
time.sleep(1)
except Exception as e:
self.append_file_exec_text(f"[ERROR] Ошибка при выполнении блока команд: {str(e)}\n")
if not check_connection():
# Проверяем соединение перед каждым блоком
if not self.check_connection():
if self.execution_stop:
break
continue
break
# Завершение выполнения
self.after(0, self.execution_completed)
block = blocks[current_block]
try:
if not self.execution_stop:
self.append_file_exec_text(f"Выполнение блока команд:\n{block}\n")
success, response = send_command_and_process_response(
self.connection,
block,
self.settings.get("timeout", 10),
max_attempts=3,
log_callback=None,
login=self.settings.get("login"),
password=self.settings.get("password"),
is_gui=True
)
if self.execution_stop:
break
if not success or (response and '^' in response):
if not self.execution_stop:
self.append_file_exec_text("[WARNING] Ошибка при выполнении блока команд. Отправляю команды по отдельности...\n")
# Проверяем соединение перед отправкой отдельных команд
if not self.check_connection():
if self.execution_stop:
break
continue
# Обрабатываем отсутствие ответа для блока
if not success and not handle_no_response(block, True):
continue
# Отправляем команды блока по отдельности
for cmd in block.splitlines():
if self.execution_stop:
break
if cmd.strip():
if not self.check_connection():
break
success, resp = send_command_and_process_response(
self.connection,
cmd,
self.settings.get("timeout", 10),
max_attempts=3,
log_callback=self.append_file_exec_text if not self.execution_stop else None,
login=self.settings.get("login"),
password=self.settings.get("password"),
is_gui=True
)
if self.execution_stop:
break
if not success:
if not self.execution_stop:
self.append_file_exec_text(f"[ERROR] Не удалось выполнить команду: {cmd}\n")
if not self.check_connection():
break
if not handle_no_response(cmd):
break
else:
consecutive_errors = 0
if not wait_before_next_command():
break
# Если блок выполнился успешно
if success and not (response and '^' in response):
consecutive_errors = 0
if response and not self.execution_stop:
self.append_file_exec_text(f"Ответ устройства:\n{response}\n")
# Обновляем прогресс
current_block += 1
self.current_command_index = (current_block * 100) // total_blocks
self.after(0, self.update_progress)
except Exception as e:
if not self.execution_stop:
self.append_file_exec_text(f"[ERROR] Ошибка при выполнении блока команд: {str(e)}\n")
break
finally:
# Завершение выполнения
self.after(0, self.execution_completed)
def execution_completed(self):
"""Обработка завершения выполнения в главном потоке"""
@@ -1489,27 +1490,38 @@ class SerialAppGUI(tk.Tk):
self.append_file_exec_text("[INFO] Выполнение возобновлено.\n")
def stop_execution(self):
"""Остановка выполнения команд"""
# Устанавливаем флаг остановки
self.execution_stop = True
self.execution_paused = False
self.timer_running = False
# Отключаемся от COM-порта
# Очищаем очередь команд
if hasattr(self, 'commands'):
delattr(self, 'commands')
# Сбрасываем индекс текущей команды и прогресс
self.current_command_index = 0
self.progress_bar['value'] = 0
self.update_progress()
# Отключаемся от COM-порта без вывода сообщений об ошибках
if self.connection:
try:
self.stop_port_monitoring()
self.connection.close()
except:
pass
finally:
self.connection = None
self.append_file_exec_text("[INFO] Соединение закрыто.\n")
except Exception as e:
self.append_file_exec_text(f"[ERROR] Ошибка при закрытии соединения: {str(e)}\n")
self.current_command_index = 0 # Сбрасываем индекс текущей команды
self.progress_bar['value'] = 0 # Сбрасываем прогресс-бар
self.update_progress() # Обновляем отображение прогресса
# Выводим только одно сообщение об остановке
self.append_file_exec_text("[INFO] Выполнение остановлено.\n")
self.reset_execution_buttons() # Сбрасываем состояние кнопок
self.update_status_bar() # Обновляем статус бар
# Сбрасываем состояние кнопок
self.reset_execution_buttons()
self.update_status_bar()
def reset_execution_buttons(self):
self.start_button.config(state="normal")
self.pause_button.config(state="disabled", text="⏸ Пауза")
@@ -1959,36 +1971,31 @@ class SerialAppGUI(tk.Tk):
self.baudrate_label.config(text=f"Скорость: {baudrate}")
self.copy_mode_label.config(text=f"Режим: {copy_mode}")
def on_file_exec_drop(self, event):
"""Обработка drop файла во вкладке выполнения команд"""
try:
files = event.data.split()
if files:
file = files[0] # Берем только первый файл
if file.lower().endswith('.txt'):
filename = os.path.basename(file)
new_path = os.path.join("Configs", filename)
if os.path.exists(new_path):
if not messagebox.askyesno(
"Подтверждение",
f"Файл {filename} уже существует. Перезаписать?"
):
return
# Копируем файл в папку Configs
with open(file, 'r', encoding='utf-8') as source:
content = source.read()
with open(new_path, 'w', encoding='utf-8') as dest:
dest.write(content)
# Устанавливаем путь к файлу
self.file_exec_var.set(new_path)
messagebox.showinfo("Успех", "Файл конфигурации успешно добавлен")
else:
messagebox.showerror("Ошибка", "Поддерживаются только текстовые файлы (.txt)")
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось добавить файл: {str(e)}")
def check_connection(self):
"""Проверка состояния соединения"""
# Если выполнение остановлено пользователем, просто возвращаем False без сообщений
if self.execution_stop:
return False
if not self.connection or not self.connection.is_open:
# Если это не ручная остановка, показываем сообщение
if not self.execution_stop:
self.append_file_exec_text("[ERROR] Соединение потеряно!\n")
# Автоматически ставим на паузу
self.execution_paused = True
self.after(0, lambda: self.pause_button.config(text="▶ Продолжить"))
# Показываем сообщение только если это не ручная остановка
self.after(0, lambda: messagebox.showerror(
"Ошибка соединения",
"Соединение с устройством потеряно!\nВыполнение команд приостановлено.\n\n"
"Пожалуйста:\n"
"1. Проверьте подключение\n"
"2. Нажмите 'Продолжить' после восстановления соединения\n"
" или 'Остановить' для прекращения выполнения"
))
return False
return True
# Класс для окна выбора конфигурации
class ConfigSelectorWindow(tk.Toplevel):
@@ -2041,12 +2048,20 @@ class ConfigSelectorWindow(tk.Toplevel):
self.tree.column("#0", width=400, stretch=True)
self.tree.heading("#0", text="Конфигурация")
# Создаем контекстное меню
self.context_menu = tk.Menu(self, tearoff=0)
self.context_menu.add_command(label="Редактировать", command=self.edit_selected)
self.context_menu.add_command(label="Переименовать", command=self.rename_selected)
self.context_menu.add_separator()
self.context_menu.add_command(label="Удалить", command=self.delete_selected)
# Создаем контекстное меню для файлов
self.file_menu = tk.Menu(self, tearoff=0)
self.file_menu.add_command(label="✍️ Редактировать", command=self.edit_selected)
self.file_menu.add_command(label="📝 Переименовать", command=self.rename_selected)
self.file_menu.add_command(label="📦 Переместить", command=self.move_selected)
self.file_menu.add_separator()
self.file_menu.add_command(label="🗑️ Удалить", command=self.delete_selected)
# Создаем контекстное меню для папок
self.folder_menu = tk.Menu(self, tearoff=0)
self.folder_menu.add_command(label="📝 Переименовать", command=self.rename_selected)
self.folder_menu.add_command(label="📦 Переместить", command=self.move_selected)
self.folder_menu.add_separator()
self.folder_menu.add_command(label="🗑️ Удалить", command=self.delete_selected)
# Привязываем события
self.tree.bind("<Double-1>", self.on_double_click)
@@ -2095,8 +2110,13 @@ class ConfigSelectorWindow(tk.Toplevel):
item = self.tree.identify('item', event.x, event.y)
if item:
self.tree.selection_set(item)
if self.tree.item(item)['text'].startswith("📄"): # Только для файлов
self.context_menu.post(event.x_root, event.y_root)
item_text = self.tree.item(item)['text']
# Определяем, это файл или папка
if item_text.startswith("📁"): # Папка
self.folder_menu.post(event.x_root, event.y_root)
elif item_text.startswith("📄"): # Файл
self.file_menu.post(event.x_root, event.y_root)
def get_selected_item(self):
"""Получение выбранного элемента"""
@@ -2113,18 +2133,95 @@ class ConfigSelectorWindow(tk.Toplevel):
self.edit_config(path)
def rename_selected(self):
"""Переименование выбранного файла"""
"""Переименование выбранного элемента"""
item_id = self.get_selected_item()
if item_id:
path = self.get_full_path(item_id)
self.rename_config(item_id, path)
item_text = self.tree.item(item_id)['text']
# Определяем, это файл или папка
is_folder = item_text.startswith("📁")
current_name = item_text[2:].strip() # Убираем эмодзи
# Запрашиваем новое имя
new_name = simpledialog.askstring(
"Переименование",
"Введите новое имя:" if is_folder else "Введите новое имя файла:",
initialvalue=current_name
)
if new_name:
try:
if not is_folder and not new_name.endswith('.txt'):
new_name += '.txt'
new_path = os.path.join(os.path.dirname(path), new_name)
if os.path.exists(new_path):
if not messagebox.askyesno(
"Подтверждение",
f"{'Папка' if is_folder else 'Файл'} с таким именем уже существует. Перезаписать?"
):
return
os.rename(path, new_path)
self.load_configs() # Перезагружаем список
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось переименовать: {str(e)}")
def move_selected(self):
"""Перемещение выбранного элемента"""
item_id = self.get_selected_item()
if item_id:
source_path = self.get_full_path(item_id)
item_text = self.tree.item(item_id)['text']
is_folder = item_text.startswith("📁")
if os.path.exists(source_path):
# Создаем окно выбора папки
folder_selector = FolderSelectorDialog(self, "Configs")
if folder_selector.result:
target_folder = folder_selector.result
try:
# Получаем имя элемента из исходного пути
name = os.path.basename(source_path)
# Формируем путь назначения
target_path = os.path.join(target_folder, name)
# Проверяем, существует ли элемент в целевой папке
if os.path.exists(target_path):
if not messagebox.askyesno(
"Подтверждение",
f"{'Папка' if is_folder else 'Файл'} {name} уже существует в целевой папке. Перезаписать?"
):
return
# Перемещаем элемент
os.rename(source_path, target_path)
self.load_configs() # Обновляем список
messagebox.showinfo("Успех", f"{'Папка' if is_folder else 'Файл'} успешно перемещен(а)")
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось переместить: {str(e)}")
def delete_selected(self):
"""Удаление выбранного файла"""
"""Удаление выбранного элемента"""
item_id = self.get_selected_item()
if item_id:
path = self.get_full_path(item_id)
self.delete_config(path)
item_text = self.tree.item(item_id)['text']
is_folder = item_text.startswith("📁")
if os.path.exists(path):
# Запрашиваем подтверждение
if messagebox.askyesno(
"Подтверждение",
f"Вы действительно хотите переместить {'папку' if is_folder else 'файл'} {os.path.basename(path)} в корзину?"
):
try:
send2trash(path)
self.load_configs() # Перезагружаем список
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось переместить в корзину: {str(e)}")
def load_configs(self):
"""Загрузка конфигураций из папки Configs"""
@@ -2237,35 +2334,6 @@ class ConfigSelectorWindow(tk.Toplevel):
self.master.load_config_file()
self.destroy()
def rename_config(self, item_id, old_path):
"""Переименование конфигурации"""
if not os.path.isfile(old_path):
return
# Получаем текущее имя файла без эмодзи
current_name = self.tree.item(item_id)['text']
if current_name.startswith("📄 "):
current_name = current_name[2:].strip()
# Запрашиваем новое имя
new_name = simpledialog.askstring(
"Переименование",
"Введите новое имя файла:",
initialvalue=current_name
)
if new_name:
if not new_name.endswith('.txt'):
new_name += '.txt'
new_path = os.path.join(os.path.dirname(old_path), new_name)
try:
os.rename(old_path, new_path)
self.load_configs() # Перезагружаем список
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось переименовать файл: {str(e)}")
def delete_config(self, path):
"""Удаление конфигурации"""
if not os.path.isfile(path):
@@ -2274,13 +2342,13 @@ class ConfigSelectorWindow(tk.Toplevel):
# Запрашиваем подтверждение
if messagebox.askyesno(
"Подтверждение",
f"Вы действительно хотите удалить файл {os.path.basename(path)}?"
f"Вы действительно хотите переместить файл {os.path.basename(path)} в корзину?"
):
try:
os.remove(path)
send2trash(path)
self.load_configs() # Перезагружаем список
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось удалить файл: {str(e)}")
messagebox.showerror("Ошибка", f"Не удалось переместить файл в корзину: {str(e)}")
def on_double_click(self, event):
"""Обработка двойного клика"""
@@ -2308,6 +2376,99 @@ class ConfigSelectorWindow(tk.Toplevel):
y = (self.winfo_screenheight() // 2) - (height // 2)
self.geometry(f"{width}x{height}+{x}+{y}")
class FolderSelectorDialog(tk.Toplevel):
def __init__(self, parent, root_folder):
super().__init__(parent)
self.title("Выберите папку")
self.geometry("500x400") # Увеличиваем начальный размер окна
self.result = None
# Создаем основной фрейм с отступами
main_frame = ttk.Frame(self, padding="10")
main_frame.pack(fill=BOTH, expand=True)
# Добавляем метку с инструкцией
ttk.Label(main_frame, text="Выберите папку назначения:").pack(anchor=W, pady=(0, 5))
# Создаем фрейм для дерева и скроллбара
tree_frame = ttk.Frame(main_frame)
tree_frame.pack(fill=BOTH, expand=True)
# Создаем и настраиваем дерево папок
self.tree = ttk.Treeview(tree_frame, selectmode="browse")
self.tree.pack(side=LEFT, fill=BOTH, expand=True)
# Добавляем скроллбар
scrollbar = ttk.Scrollbar(tree_frame, orient="vertical", command=self.tree.yview)
scrollbar.pack(side=RIGHT, fill=Y)
self.tree.configure(yscrollcommand=scrollbar.set)
# Загружаем структуру папок
self.load_folders(root_folder)
# Кнопки
button_frame = ttk.Frame(main_frame)
button_frame.pack(fill=X, pady=(10, 0))
# Кнопки справа
ttk.Button(button_frame, text="❌ Отмена", command=self.on_cancel).pack(side=RIGHT, padx=(5, 0))
ttk.Button(button_frame, text="✔️ Выбрать папку", command=self.on_select).pack(side=RIGHT)
# Центрируем окно
self.center_window()
# Делаем окно модальным
self.transient(parent)
self.grab_set()
# Устанавливаем минимальный размер окна
self.minsize(400, 300) # Увеличиваем минимальный размер окна
parent.wait_window(self)
def center_window(self):
"""Центрирование окна на экране"""
self.update_idletasks()
width = self.winfo_width()
height = self.winfo_height()
x = (self.winfo_screenwidth() // 2) - (width // 2)
y = (self.winfo_screenheight() // 2) - (height // 2)
self.geometry(f"{width}x{height}+{x}+{y}")
def load_folders(self, root_folder):
"""Загрузка структуры папок"""
def add_folder(path, parent=""):
try:
items = os.listdir(path)
for item in sorted(items):
full_path = os.path.join(path, item)
if os.path.isdir(full_path):
folder_id = self.tree.insert(
parent,
"end",
text="📁 " + item,
values=(full_path,),
open=False
)
add_folder(full_path, folder_id)
except Exception:
pass
# Добавляем корневую папку
root_id = self.tree.insert("", "end", text="📁 Configs", values=(root_folder,), open=True)
add_folder(root_folder, root_id)
def on_select(self):
"""Обработка выбора папки"""
selection = self.tree.selection()
if selection:
self.result = self.tree.item(selection[0])['values'][0]
self.destroy()
def on_cancel(self):
"""Отмена выбора"""
self.destroy()
# ==========================
# Основной запуск приложения
# ==========================

View File

@@ -1,4 +1,5 @@
tftpy>=0.8.0
pyserial>=3.5
requests>=2.31.0
packaging>=23.2
packaging>=23.2
send2trash>=1.8.0