323 lines
12 KiB
Python
323 lines
12 KiB
Python
"""
|
||
Интерфейс (UI) для приложения автоматизации загрузки файлов
|
||
"""
|
||
import tkinter as tk
|
||
from tkinter import filedialog, messagebox, ttk
|
||
import threading
|
||
import logging
|
||
from pathlib import Path
|
||
from services.processor import FileProcessorService
|
||
|
||
|
||
class UIManager:
|
||
"""Менеджер интерфейса приложения"""
|
||
|
||
def __init__(self, root):
|
||
"""Инициализирует интерфейс."""
|
||
self.root = root
|
||
self.root.title("🔄 Автоматизация загрузки файлов")
|
||
self.root.geometry("700x600")
|
||
self.root.configure(bg="#2C3E50")
|
||
self.root.minsize(500, 400)
|
||
|
||
self.font_title = ("Arial", 14, "bold")
|
||
self.font_label = ("Arial", 11)
|
||
self.font_button = ("Arial", 10, "bold")
|
||
|
||
self.processor = None
|
||
self.processing = False
|
||
|
||
self._setup_logging()
|
||
self._create_ui()
|
||
|
||
def _setup_logging(self):
|
||
"""Настраивает логирование в текстовое поле."""
|
||
# Обычное логирование в консоль
|
||
logging.basicConfig(
|
||
level=logging.INFO,
|
||
format='%(asctime)s - %(levelname)s - %(message)s'
|
||
)
|
||
|
||
def _create_ui(self):
|
||
"""Создает элементы интерфейса."""
|
||
# Главный фрейм
|
||
main_frame = tk.Frame(self.root, bg="#2C3E50")
|
||
main_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=20)
|
||
|
||
# Заголовок
|
||
title_label = tk.Label(
|
||
main_frame,
|
||
text="📁 Автоматизация загрузки файлов XLSX",
|
||
font=self.font_title,
|
||
bg="#2C3E50",
|
||
fg="#ECF0F1"
|
||
)
|
||
title_label.pack(pady=(0, 20))
|
||
|
||
# ===== ПОЛЕ 1: ДИРЕКТОРИЯ С ФАЙЛАМИ =====
|
||
dir_label = tk.Label(
|
||
main_frame,
|
||
text="📂 Директория с файлами .xlsx:",
|
||
font=self.font_label,
|
||
bg="#2C3E50",
|
||
fg="#ECF0F1"
|
||
)
|
||
dir_label.pack(anchor="w", pady=(10, 5))
|
||
|
||
dir_frame = tk.Frame(main_frame, bg="#2C3E50")
|
||
dir_frame.pack(fill=tk.X, pady=(0, 15))
|
||
|
||
self.dir_entry = tk.Entry(dir_frame, font=self.font_label, width=60)
|
||
self.dir_entry.pack(side="left", fill=tk.X, expand=True)
|
||
self.dir_entry.bind("<Button-1>", lambda e: self._select_directory())
|
||
|
||
dir_button = tk.Button(
|
||
dir_frame,
|
||
text="🗂️ Выбрать",
|
||
command=self._select_directory,
|
||
font=self.font_button,
|
||
bg="#3498DB",
|
||
fg="white",
|
||
padx=10
|
||
)
|
||
dir_button.pack(side="left", padx=(10, 0))
|
||
|
||
# ===== ПОЛЕ 2: URL СТРАНИЦЫ =====
|
||
url_label = tk.Label(
|
||
main_frame,
|
||
text="🌐 URL страницы для загрузки:",
|
||
font=self.font_label,
|
||
bg="#2C3E50",
|
||
fg="#ECF0F1"
|
||
)
|
||
url_label.pack(anchor="w", pady=(10, 5))
|
||
|
||
self.url_entry = tk.Entry(main_frame, font=self.font_label)
|
||
self.url_entry.pack(anchor="w", fill=tk.X, pady=(0, 15))
|
||
self.url_entry.insert(0, "https://")
|
||
|
||
# ===== ПОЛЕ 3: ВАРИАНТЫ КНОПОК =====
|
||
buttons_label = tk.Label(
|
||
main_frame,
|
||
text="🔘 Варианты текста кнопок (через запятую):",
|
||
font=self.font_label,
|
||
bg="#2C3E50",
|
||
fg="#ECF0F1"
|
||
)
|
||
buttons_label.pack(anchor="w", pady=(10, 5))
|
||
|
||
self.upload_buttons_entry = tk.Entry(main_frame, font=self.font_label)
|
||
self.upload_buttons_entry.pack(anchor="w", fill=tk.X, pady=(0, 5))
|
||
self.upload_buttons_entry.insert(0, "Загрузить файл, Выбрать файл, Добавить файл")
|
||
|
||
upload_label = tk.Label(
|
||
main_frame,
|
||
text="(Кнопка для загрузки файла)",
|
||
font=("Arial", 9),
|
||
bg="#2C3E50",
|
||
fg="#BDC3C7"
|
||
)
|
||
upload_label.pack(anchor="w", pady=(0, 10))
|
||
|
||
self.process_buttons_entry = tk.Entry(main_frame, font=self.font_label)
|
||
self.process_buttons_entry.pack(anchor="w", fill=tk.X, pady=(0, 5))
|
||
self.process_buttons_entry.insert(0, "Загрузить, Обработать, Преобразовать, Изменить, Конвертировать")
|
||
|
||
process_label = tk.Label(
|
||
main_frame,
|
||
text="(Кнопка для обработки/загрузки файла)",
|
||
font=("Arial", 9),
|
||
bg="#2C3E50",
|
||
fg="#BDC3C7"
|
||
)
|
||
process_label.pack(anchor="w", pady=(0, 15))
|
||
|
||
# ===== ЛОГИРОВАНИЕ =====
|
||
log_label = tk.Label(
|
||
main_frame,
|
||
text="📋 Логирование процесса:",
|
||
font=self.font_label,
|
||
bg="#2C3E50",
|
||
fg="#ECF0F1"
|
||
)
|
||
log_label.pack(anchor="w", pady=(10, 5))
|
||
|
||
# Frame для логов с прокруткой
|
||
log_frame = tk.Frame(main_frame, bg="#34495E")
|
||
log_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 15))
|
||
|
||
scrollbar = tk.Scrollbar(log_frame)
|
||
scrollbar.pack(side="right", fill="y")
|
||
|
||
self.log_text = tk.Text(
|
||
log_frame,
|
||
font=("Courier", 9),
|
||
bg="#1C2833",
|
||
fg="#82E0AA",
|
||
height=10,
|
||
yscrollcommand=scrollbar.set
|
||
)
|
||
self.log_text.pack(fill=tk.BOTH, expand=True)
|
||
scrollbar.config(command=self.log_text.yview)
|
||
|
||
# ===== КНОПКИ =====
|
||
button_frame = tk.Frame(main_frame, bg="#2C3E50")
|
||
button_frame.pack(fill=tk.X)
|
||
|
||
self.start_button = tk.Button(
|
||
button_frame,
|
||
text="🚀 Начать загрузку",
|
||
command=self._start_processing,
|
||
font=self.font_button,
|
||
bg="#27AE60",
|
||
fg="white",
|
||
padx=20,
|
||
pady=12
|
||
)
|
||
self.start_button.pack(side="right", padx=(10, 0))
|
||
|
||
self.clear_log_button = tk.Button(
|
||
button_frame,
|
||
text="🗑️ Очистить логи",
|
||
command=self._clear_logs,
|
||
font=self.font_button,
|
||
bg="#E74C3C",
|
||
fg="white",
|
||
padx=15,
|
||
pady=12
|
||
)
|
||
self.clear_log_button.pack(side="right")
|
||
|
||
# Перенаправление логов в текстовое поле
|
||
self._setup_log_handler()
|
||
|
||
def _setup_log_handler(self):
|
||
"""Настраивает обработчик логов в текстовое поле."""
|
||
# Создаем кастомный обработчик для логирования в UI
|
||
class TextHandler(logging.Handler):
|
||
def __init__(self, text_widget):
|
||
super().__init__()
|
||
self.text_widget = text_widget
|
||
|
||
def emit(self, record):
|
||
msg = self.format(record)
|
||
self.text_widget.insert(tk.END, msg + "\n")
|
||
self.text_widget.see(tk.END)
|
||
self.text_widget.update()
|
||
|
||
handler = TextHandler(self.log_text)
|
||
handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s', datefmt='%H:%M:%S'))
|
||
logging.getLogger().addHandler(handler)
|
||
|
||
def _select_directory(self):
|
||
"""Открывает диалог выбора директории."""
|
||
directory = filedialog.askdirectory(title="Выберите директорию с .xlsx файлами")
|
||
if directory:
|
||
self.dir_entry.delete(0, tk.END)
|
||
self.dir_entry.insert(0, directory)
|
||
logging.info(f"✅ Выбрана директория: {directory}")
|
||
|
||
def _clear_logs(self):
|
||
"""Очищает логи."""
|
||
self.log_text.delete(1.0, tk.END)
|
||
logging.info("🗑️ Логи очищены")
|
||
|
||
def _start_processing(self):
|
||
"""Запускает процесс обработки в отдельном потоке."""
|
||
if self.processing:
|
||
messagebox.showwarning("⚠️ Внимание", "Процесс уже запущен!")
|
||
return
|
||
|
||
# Валидация входных данных
|
||
directory = self.dir_entry.get().strip()
|
||
url = self.url_entry.get().strip()
|
||
|
||
if not directory:
|
||
messagebox.showerror("❌ Ошибка", "Выберите директорию с файлами!")
|
||
return
|
||
|
||
if not Path(directory).exists():
|
||
messagebox.showerror("❌ Ошибка", "Указанная директория не существует!")
|
||
return
|
||
|
||
if not url or url == "https://":
|
||
messagebox.showerror("❌ Ошибка", "Введите URL страницы!")
|
||
return
|
||
|
||
# Парсим варианты кнопок
|
||
upload_texts = [t.strip() for t in self.upload_buttons_entry.get().split(",")]
|
||
process_texts = [t.strip() for t in self.process_buttons_entry.get().split(",")]
|
||
|
||
# Отключаем кнопку и запускаем обработку в потоке
|
||
self.processing = True
|
||
self.start_button.config(state="disabled", bg="#95A5A6")
|
||
|
||
thread = threading.Thread(
|
||
target=self._processing_thread,
|
||
args=(directory, url, upload_texts, process_texts),
|
||
daemon=True
|
||
)
|
||
thread.start()
|
||
|
||
def _processing_thread(self, directory, url, upload_texts, process_texts):
|
||
"""Потоковая функция обработки."""
|
||
try:
|
||
logging.info("\n" + "="*70)
|
||
logging.info("🔄 ЗАПУСК АВТОМАТИЗАЦИИ ЗАГРУЗКИ ФАЙЛОВ")
|
||
logging.info("="*70)
|
||
|
||
# Получаем путь для загрузок (рядом с программой)
|
||
download_dir = str(Path(__file__).parent / "downloads")
|
||
Path(download_dir).mkdir(exist_ok=True)
|
||
|
||
# Создаем процессор
|
||
processor = FileProcessorService(directory, url, download_dir)
|
||
|
||
# Запускаем обработку
|
||
results = processor.start_processing(upload_texts, process_texts)
|
||
|
||
# Показываем результаты
|
||
self._show_results(results)
|
||
|
||
except Exception as e:
|
||
logging.error(f"❌ Критическая ошибка: {e}")
|
||
messagebox.showerror("❌ Ошибка", f"Произошла ошибка:\n{str(e)}")
|
||
finally:
|
||
self.processing = False
|
||
self.start_button.config(state="normal", bg="#27AE60")
|
||
|
||
def _show_results(self, results):
|
||
"""Показывает результаты обработки."""
|
||
result_text = (
|
||
f"✅ ОБРАБОТКА ЗАВЕРШЕНА\n\n"
|
||
f"📊 Результаты:\n"
|
||
f"• Всего файлов найдено: {results['total_files']}\n"
|
||
f"• Обработано успешно: {results['processed_files']}\n"
|
||
f"• Ошибок: {len(results['failed_files'])}\n"
|
||
)
|
||
|
||
if results['failed_files']:
|
||
result_text += f"\n❌ Файлы с ошибками:\n"
|
||
for file in results['failed_files'][:5]:
|
||
result_text += f" • {file}\n"
|
||
if len(results['failed_files']) > 5:
|
||
result_text += f" ... и еще {len(results['failed_files']) - 5}"
|
||
|
||
if results['errors']:
|
||
result_text += f"\n⚠️ Системные ошибки:\n"
|
||
for error in results['errors']:
|
||
result_text += f" • {error}\n"
|
||
|
||
messagebox.showinfo("Результаты", result_text)
|
||
|
||
|
||
def main():
|
||
"""Главная функция."""
|
||
root = tk.Tk()
|
||
app = UIManager(root)
|
||
root.mainloop()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|