From be7aadf2181c1609eb8c4538d72fe5eda0a17956 Mon Sep 17 00:00:00 2001 From: unknown <Администратор@LOCAL.LOC> Date: Mon, 9 Feb 2026 14:28:39 +0500 Subject: [PATCH] new --- build.py | 589 ++++++++++++++++++++++++++++++++++++ icon.ico | Bin 0 -> 99678 bytes services/browser_service.py | 89 +++++- test.xlsx | 0 test1.xlsx | 0 test2.xlsx | 0 test3.xlsx | 0 test5.xlsx | 0 tset4.xlsx | 0 ui.py | 92 +++++- 10 files changed, 754 insertions(+), 16 deletions(-) create mode 100644 build.py create mode 100644 icon.ico create mode 100644 test.xlsx create mode 100644 test1.xlsx create mode 100644 test2.xlsx create mode 100644 test3.xlsx create mode 100644 test5.xlsx create mode 100644 tset4.xlsx diff --git a/build.py b/build.py new file mode 100644 index 0000000..5a8c99e --- /dev/null +++ b/build.py @@ -0,0 +1,589 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Скрипт для сборки 32-битного приложения в один файл +""" + +import os +import sys +import shutil +import platform +import subprocess +import argparse +from pathlib import Path +import logging +from datetime import datetime + +class OneFileBuilder: + """Класс для сборки одного исполняемого файла""" + + def __init__(self, config_file='onefile_32bit.spec'): + self.config_file = config_file + self.project_root = Path.cwd() + self.dist_dir = self.project_root / 'dist' + self.build_dir = self.project_root / 'build' + + def print_banner(self): + """Вывод информационного баннера""" + print("=" * 60) + print(" СБОРКА 32-БИТНОГО ПРИЛОЖЕНИЯ В ОДИН ФАЙЛ") + print("=" * 60) + print(f"Проект: {self.project_root.name}") + print(f"Система: {platform.system()} {platform.architecture()[0]}") + print(f"Python: {platform.python_version()}") + print("=" * 60) + + def check_requirements(self): + """Проверка необходимых компонентов""" + print("\n🔍 Проверка требований...") + + # Проверка Python архитектуры + if platform.system() == 'Windows' and platform.architecture()[0] != '32bit': + print("⚠️ ВНИМАНИЕ: Используется 64-битный Python!") + print("Для сборки 32-битного EXE рекомендуется:") + print("1. Установить 32-битный Python с python.org") + print("2. Используйте эту версию Python для сборки") + response = input("Продолжить сборку? (y/n): ") + if response.lower() != 'y': + return False + + # Проверка PyInstaller + try: + import PyInstaller + print(f"✓ PyInstaller {PyInstaller.__version__}") + except ImportError: + print("✗ PyInstaller не установлен") + print("Установка: pip install pyinstaller") + return False + + # Проверка иконки (не обязательна) + icon_paths = ['icon.ico', 'assets/icon.ico', 'images/icon.ico'] + self.icon_file = None + + for icon in icon_paths: + if Path(icon).exists(): + self.icon_file = icon + print(f"✓ Иконка найдена: {icon}") + break + + if not self.icon_file: + print("⚠️ Иконка не найдена. Будет использована стандартная иконка.") + + # Проверка основного файла + main_files = ['app.py'] + self.main_file = None + + for main in main_files: + if Path(main).exists(): + self.main_file = main + print(f"✓ Основной файл: {main}") + break + + if not self.main_file: + print("✗ Основной файл не найден!") + print("Создайте main.py или укажите другой файл через --main") + return False + + return True + + def setup_environment(self): + """Настройка окружения для 32-битной сборки""" + print("\n⚙️ Настройка окружения...") + + # Установка переменных окружения для Windows + if platform.system() == 'Windows': + os.environ['PYINSTALLER_FORCE_32BIT'] = '1' + print("✓ Установлена переменная PYINSTALLER_FORCE_32BIT=1") + + # Создание папок если нужно + self.dist_dir.mkdir(exist_ok=True) + print(f"✓ Папка dist готова") + + return True + + def generate_version_info(self): + """Генерация файла с информацией о версии""" + version_content = f"""# UTF-8 +VSVersionInfo( + ffi=FixedFileInfo( + filevers=({self.version_major}, {self.version_minor}, {self.version_patch}, {self.version_build}), + prodvers=({self.version_major}, {self.version_minor}, {self.version_patch}, {self.version_build}), + mask=0x3f, + flags=0x0, + OS=0x40004, + fileType=0x1, + subtype=0x0, + date=(0, 0) + ), + kids=[ + StringFileInfo([ + StringTable( + '040904B0', + [ + StringStruct('CompanyName', '{self.company_name}'), + StringStruct('FileDescription', '{self.file_description}'), + StringStruct('FileVersion', '{self.version_major}.{self.version_minor}.{self.version_patch}.{self.version_build}'), + StringStruct('InternalName', '{self.app_name}'), + StringStruct('LegalCopyright', '{self.copyright}'), + StringStruct('OriginalFilename', '{self.app_name}.exe'), + StringStruct('ProductName', '{self.product_name}'), + StringStruct('ProductVersion', '{self.version_major}.{self.version_minor}.{self.version_patch}'), + ]) + ]), + VarFileInfo([VarStruct('Translation', [1033, 1200])]) + ] +) +""" + + version_file = Path('version_info.txt') + with open(version_file, 'w', encoding='utf-8-sig') as f: + f.write(version_content) + + print("✓ Создан version_info.txt") + return version_file + + def build_with_pyinstaller(self, clean_build=True): + """Сборка с помощью PyInstaller""" + print(f"\n🔨 Сборка приложения...") + + # Очистка предыдущих сборок + if clean_build: + if self.build_dir.exists(): + shutil.rmtree(self.build_dir, ignore_errors=True) + print("✓ Очищена папка build") + + # Команда сборки + cmd = [ + sys.executable, '-m', 'PyInstaller', + '--onefile', # Один файл + '--clean', # Очистка кэша + '--noconfirm', # Не спрашивать подтверждение + ] + + # Добавление иконки если есть + if self.icon_file and Path(self.icon_file).exists(): + cmd.extend(['--icon', self.icon_file]) + print(f"✓ Используется иконка: {self.icon_file}") + else: + print("⚠️ Иконка не используется") + + # Добавление информации о версии (только Windows) + version_file = Path('version_info.txt') + if platform.system() == 'Windows' and version_file.exists(): + cmd.extend(['--version-file', 'version_info.txt']) + print("✓ Используется информация о версии") + + # Имя приложения + cmd.extend(['--name', self.app_name]) + + # Скрыть консоль для GUI приложений + if not self.show_console: + cmd.append('--noconsole') + cmd.append('--windowed') + + # Скрытые импорты + for imp in self.hidden_imports: + cmd.extend(['--hidden-import', imp]) + + # Добавление данных + for data_src, data_dst in self.data_files: + cmd.extend(['--add-data', f'{data_src}{os.pathsep}{data_dst}']) + + # Добавление основного файла + cmd.append(str(self.main_file)) + + # Вывод команды + print(f"Команда: {' '.join(cmd)}") + print("\n🔄 Запуск PyInstaller...") + + # Выполнение с правильной кодировкой для Windows + try: + # Для Windows используем кодировку cp1251 или текущую системную + encoding = 'cp866' if platform.system() == 'Windows' else 'utf-8' + + result = subprocess.run( + cmd, + check=True, + capture_output=True, + text=True, + encoding=encoding, + errors='replace' # Игнорировать ошибки декодирования + ) + + print("\n✅ Сборка завершена успешно!") + + if result.stdout and result.stdout.strip(): + print(f"\nВывод PyInstaller:\n{result.stdout}") + + if result.stderr and result.stderr.strip(): + print(f"\nПредупреждения PyInstaller:\n{result.stderr}") + + return True + + except subprocess.CalledProcessError as e: + print(f"\n❌ Ошибка сборки!") + print(f"Код ошибки: {e.returncode}") + + # Вывод с правильной кодировкой + if e.stdout: + print(f"\nВывод PyInstaller:\n{e.stdout}") + if e.stderr: + print(f"\nОшибки PyInstaller:\n{e.stderr}") + + return False + except Exception as e: + print(f"\n❌ Неожиданная ошибка: {e}") + import traceback + traceback.print_exc() + return False + + def build_with_pyinstaller_simple(self): + """Упрощенный метод сборки без перехвата вывода""" + print(f"\n🔨 Сборка приложения (упрощенный метод)...") + + # Очистка предыдущих сборок + if self.build_dir.exists(): + shutil.rmtree(self.build_dir, ignore_errors=True) + print("✓ Очищена папка build") + + # Базовые аргументы + args = [ + sys.executable, '-m', 'PyInstaller', + '--onefile', + '--clean', + '--noconfirm', + '--name', self.app_name, + ] + + # Дополнительные аргументы + if not self.show_console: + args.extend(['--noconsole', '--windowed']) + + if self.icon_file and Path(self.icon_file).exists(): + args.extend(['--icon', self.icon_file]) + + version_file = Path('version_info.txt') + if platform.system() == 'Windows' and version_file.exists(): + args.extend(['--version-file', 'version_info.txt']) + + # Добавить main.py + args.append(str(self.main_file)) + + print(f"Запуск: {' '.join(args)}") + + # Прямой запуск + try: + return_code = subprocess.call(args) + if return_code == 0: + print("\n✅ Сборка завершена успешно!") + return True + else: + print(f"\n❌ Ошибка сборки. Код возврата: {return_code}") + return False + except Exception as e: + print(f"\n❌ Ошибка: {e}") + return False + + def optimize_with_upx(self): + """Оптимизация с помощью UPX""" + print("\n⚡ Оптимизация с UPX...") + + exe_file = self.dist_dir / f'{self.app_name}.exe' + + if not exe_file.exists(): + print("✗ Исполняемый файл не найден") + return False + + # Проверка наличия UPX + try: + subprocess.run(['upx', '--version'], + capture_output=True, check=True, + shell=True) # shell=True для Windows + print("✓ UPX найден") + except (subprocess.CalledProcessError, FileNotFoundError): + print("⚠️ UPX не установлен. Пропуск оптимизации.") + print("Скачайте UPX: https://upx.github.io/") + return True + + # Оптимизация + original_size = exe_file.stat().st_size / (1024 * 1024) + + cmd = ['upx', '--best', '--lzma', str(exe_file)] + + try: + result = subprocess.run(cmd, check=True, capture_output=True, shell=True) + + new_size = exe_file.stat().st_size / (1024 * 1024) + reduction = ((original_size - new_size) / original_size) * 100 + + print(f"✓ Оптимизация завершена!") + print(f"Размер: {original_size:.2f} MB → {new_size:.2f} MB") + print(f"Сжатие: {reduction:.1f}%") + + return True + + except subprocess.CalledProcessError as e: + print(f"⚠️ Ошибка оптимизации UPX: {e}") + if e.stderr: + print(f"Ошибка: {e.stderr.decode('utf-8', errors='replace')}") + return False + + def test_executable(self): + """Тестирование исполняемого файла""" + print("\n🧪 Тестирование приложения...") + + exe_path = self.dist_dir / f'{self.app_name}.exe' + + if not exe_path.exists(): + # Проверим другие возможные расширения + for ext in ['.exe', '']: + test_path = self.dist_dir / f'{self.app_name}{ext}' + if test_path.exists(): + exe_path = test_path + break + + if not exe_path.exists(): + print("✗ Исполняемый файл не найден") + print(f"Искали в: {self.dist_dir}") + return False + + size_mb = exe_path.stat().st_size / (1024 * 1024) + print(f"✓ Файл создан: {exe_path.name}") + print(f" Размер: {size_mb:.2f} MB") + print(f" Путь: {exe_path}") + + # Проверка архитектуры через pefile если доступен + try: + import pefile + pe = pefile.PE(str(exe_path)) + machine = pe.FILE_HEADER.Machine + arch_map = { + 0x14c: '32-bit (x86)', + 0x8664: '64-bit (x64)', + } + arch_text = arch_map.get(machine, f'Unknown ({hex(machine)})') + print(f" Архитектура: {arch_text}") + print(f" Секций: {pe.FILE_HEADER.NumberOfSections}") + except ImportError: + print(" ℹ️ Установите pefile для детальной информации: pip install pefile") + except Exception as e: + print(f" ℹ️ Не удалось проверить архитектуру: {e}") + + # Спросить о запуске теста + if self.auto_test: + response = 'y' + else: + response = input("\nЗапустить приложение для теста? (y/n): ") + + if response.lower() == 'y': + print(f"Запуск {exe_path.name}...") + try: + subprocess.Popen([str(exe_path)], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + print("✓ Приложение запущено") + except Exception as e: + print(f"⚠️ Ошибка запуска: {e}") + + return True + + def cleanup(self): + """Очистка временных файлов""" + print("\n🧹 Очистка временных файлов...") + + # Удаляем только временные файлы, созданные скриптом + temp_files = [ + 'version_info.txt', + f'{self.app_name}.spec', + ] + + for temp_file in temp_files: + temp_path = Path(temp_file) + if temp_path.exists(): + try: + temp_path.unlink() + print(f"✓ Удален {temp_file}") + except Exception as e: + print(f"⚠️ Не удалось удалить {temp_file}: {e}") + + print("✓ Очистка завершена") + + def build(self, args): + """Основной процесс сборки""" + self.print_banner() + + # Настройка параметров + self.app_name = args.name or 'MyApp' + self.company_name = args.company or 'My Company' + self.product_name = args.product or self.app_name + self.file_description = args.description or f'{self.app_name} Application' + self.copyright = args.copyright or f'Copyright © 2024 {self.company_name}' + + self.version_major = args.version_major or 1 + self.version_minor = args.version_minor or 0 + self.version_patch = args.version_patch or 0 + self.version_build = args.version_build or 0 + + self.show_console = args.console + self.auto_test = args.test + self.hidden_imports = args.hidden_import or [] + self.data_files = [] + + # Парсинг файлов данных + if args.add_data: + for data in args.add_data: + if ':' in data: + src, dst = data.split(':', 1) + self.data_files.append((src, dst)) + else: + print(f"⚠️ Неверный формат данных: {data}. Используйте src:dst") + + # Проверка требований + if not self.check_requirements(): + return False + + # Настройка окружения + if not self.setup_environment(): + return False + + # Генерация информации о версии + if platform.system() == 'Windows': + self.generate_version_info() + + # Сборка (используем упрощенный метод) + print("\n" + "=" * 50) + print("Выберите метод сборки:") + print("1. Упрощенный (рекомендуется)") + print("2. С перехватом вывода (может быть проблема с кодировкой)") + print("=" * 50) + + choice = input("Ваш выбор (1 или 2): ").strip() + + success = False + if choice == '1': + success = self.build_with_pyinstaller_simple() + elif choice == '2': + success = self.build_with_pyinstaller(clean_build=args.clean) + else: + print("Используется метод по умолчанию (упрощенный)") + success = self.build_with_pyinstaller_simple() + + if not success: + print("\n❌ Сборка не удалась.") + return False + + # Оптимизация UPX + if args.upx: + self.optimize_with_upx() + + # Тестирование + self.test_executable() + + # Очистка + if args.cleanup: + self.cleanup() + + print("\n" + "=" * 60) + print("🎉 СБОРКА УСПЕШНО ЗАВЕРШЕНА!") + print("=" * 60) + print(f"\n📦 Ваше приложение готово:") + print(f" Файл: dist/{self.app_name}.exe") + print(f" Архитектура: 32-битная") + print(f" Тип: Один исполняемый файл") + print(f" Иконка: {'Да' if self.icon_file else 'Нет'}") + + print("\n📋 Следующие шаги:") + print("1. Протестируйте приложение на целевой системе") + print("2. Используйте Inno Setup для создания установщика") + print("3. Подпишите код для Windows (опционально)") + + return True + + +def setup_logging(): + """Настраивает логирование в файл сессии программы.""" + session_time = datetime.now().strftime('%Y-%m-%d_%H-%M-%S') + log_filename = f"session_{session_time}.log" + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler(log_filename, encoding='utf-8'), + logging.StreamHandler() + ] + ) + + +def main(): + """Точка входа""" + parser = argparse.ArgumentParser( + description='Сборка 32-битного приложения в один файл', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Примеры использования: + python build.py + python build.py --name "MyApp" --console + python build.py --add-data "data:data" --add-data "config.ini:." + python build.py --version-major 2 --version-minor 1 + """ + ) + + # Основные параметры + parser.add_argument('--name', help='Имя приложения (по умолчанию: MyApp)') + parser.add_argument('--main', help='Основной файл Python (по умолчанию: main.py)') + + # Версия + parser.add_argument('--version-major', type=int, help='Основная версия') + parser.add_argument('--version-minor', type=int, help='Дополнительная версия') + parser.add_argument('--version-patch', type=int, help='Патч версия') + parser.add_argument('--version-build', type=int, help='Номер сборки') + + # Информация о приложении + parser.add_argument('--company', help='Название компании') + parser.add_argument('--product', help='Название продукта') + parser.add_argument('--description', help='Описание файла') + parser.add_argument('--copyright', help='Текст авторских прав') + + # Опции сборки + parser.add_argument('--console', action='store_true', + help='Показать консоль') + parser.add_argument('--no-upx', dest='upx', action='store_false', default=True, + help='Не использовать UPX для сжатия') + parser.add_argument('--no-clean', dest='clean', action='store_false', default=True, + help='Не очищать папку build перед сборкой') + + # Данные и импорты + parser.add_argument('--add-data', action='append', + help='Добавить файлы данных (формат: src:dst)') + parser.add_argument('--hidden-import', action='append', + help='Добавить скрытый импорт модуля') + + # Тестирование и очистка + parser.add_argument('--test', action='store_true', + help='Автоматически протестировать после сборки') + parser.add_argument('--no-cleanup', dest='cleanup', action='store_false', default=True, + help='Не удалять временные файлы') + + args = parser.parse_args() + + # Создание и запуск сборщика + builder = OneFileBuilder() + + try: + success = builder.build(args) + sys.exit(0 if success else 1) + except KeyboardInterrupt: + print("\n\n⚠️ Сборка прервана пользователем") + sys.exit(130) + except Exception as e: + print(f"\n❌ Неожиданная ошибка: {e}") + import traceback + traceback.print_exc() + sys.exit(1) + + +if __name__ == '__main__': + # Вызов настройки логирования при запуске программы + setup_logging() + + main() \ No newline at end of file diff --git a/icon.ico b/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..5d0983165ebc51db3b57cb22a00042bc0e5e7bc2 GIT binary patch literal 99678 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 1: - logging.info("📑 Закрываю текущую вкладку...") - self.driver.close() - # Переходим на последнюю оставшуюся вкладку - self.driver.switch_to.window(self.driver.window_handles[-1]) - time.sleep(1) - logging.info("✅ Перешел на предыдущую вкладку.") - return True - else: - logging.warning("⚠️ Это последняя вкладка, не закрываю.") - return False + logging.info("📑 Метод close_current_tab вызван, но вкладки не будут закрываться.") + # Удаляем логику закрытия вкладок + return True except Exception as e: - logging.error(f"❌ Ошибка при закрытии вкладки: {e}") + logging.error(f"❌ Ошибка при попытке закрыть вкладку: {e}") return False def get_tabs_count(self): """Возвращает количество открытых вкладок.""" return len(self.driver.window_handles) + + def select_tab_by_name(self, tab_name): + """Выбирает вкладку на странице по её названию.""" + try: + logging.info(f"🔍 Поиск вкладки с названием: {tab_name}") + # Ищем элемент вкладки по тексту + tab_element = self.wait.until( + EC.presence_of_element_located((By.XPATH, f"//a[text()='{tab_name}']")) + ) + tab_element.click() + logging.info(f"✅ Вкладка '{tab_name}' выбрана.") + return True + except Exception as e: + logging.error(f"❌ Ошибка при выборе вкладки '{tab_name}': {e}") + return False + + def select_tab_by_text_or_name(self, target_text, timeout=30): + """Ищет и выбирает вкладку по тексту или названию, делая повторные попытки. + + Args: + target_text (str): Текст или название вкладки для поиска. + timeout (int): Общее время ожидания в секундах. + + Returns: + bool: True, если вкладка найдена и выбрана, иначе False. + """ + end_time = time.time() + timeout + while time.time() < end_time: + try: + logging.info(f"🔍 Поиск вкладки с текстом или названием: {target_text}") + # Ищем элемент вкладки по тексту + tab_element = self.wait.until( + EC.presence_of_element_located((By.XPATH, f"//*[text()='{target_text}']")) + ) + self.driver.execute_script("arguments[0].scrollIntoView(true);", tab_element) + time.sleep(0.5) + tab_element.click() + logging.info(f"✅ Вкладка с текстом '{target_text}' выбрана.") + return True + except Exception as e: + logging.warning(f"⚠️ Вкладка с текстом '{target_text}' не найдена, повторная попытка...") + time.sleep(1) # Ждем перед повторной попыткой + logging.error(f"❌ Не удалось найти вкладку с текстом '{target_text}' за {timeout} секунд.") + return False + + def set_tabs(self, tabs): + """Устанавливает массив с названиями вкладок.""" + self.tabs = tabs + + def select_tab_from_list(self, tab_index): + """Выбирает вкладку из массива по индексу. + + Args: + tab_index (int): Индекс вкладки в массиве. + + Returns: + bool: True, если вкладка выбрана успешно, иначе False. + """ + try: + if not hasattr(self, 'tabs') or not self.tabs: + logging.error("❌ Массив вкладок не установлен или пуст.") + return False + + if tab_index < 0 or tab_index >= len(self.tabs): + logging.error("❌ Индекс вкладки выходит за пределы массива.") + return False + + tab_name = self.tabs[tab_index] + logging.info(f"🔍 Выбор вкладки с индексом {tab_index}: {tab_name}") + return self.select_tab_by_name(tab_name) + except Exception as e: + logging.error(f"❌ Ошибка при выборе вкладки по индексу {tab_index}: {e}") + return False diff --git a/test.xlsx b/test.xlsx new file mode 100644 index 0000000..e69de29 diff --git a/test1.xlsx b/test1.xlsx new file mode 100644 index 0000000..e69de29 diff --git a/test2.xlsx b/test2.xlsx new file mode 100644 index 0000000..e69de29 diff --git a/test3.xlsx b/test3.xlsx new file mode 100644 index 0000000..e69de29 diff --git a/test5.xlsx b/test5.xlsx new file mode 100644 index 0000000..e69de29 diff --git a/tset4.xlsx b/tset4.xlsx new file mode 100644 index 0000000..e69de29 diff --git a/ui.py b/ui.py index 6ed700f..6ab68c5 100644 --- a/ui.py +++ b/ui.py @@ -7,6 +7,7 @@ import threading import logging from pathlib import Path from services.processor import FileProcessorService +from services.browser_service import BrowserService class UIManager: @@ -16,7 +17,7 @@ class UIManager: """Инициализирует интерфейс.""" self.root = root self.root.title("🔄 Автоматизация загрузки файлов") - self.root.geometry("700x600") + self.root.geometry("700x800") self.root.configure(bg="#2C3E50") self.root.minsize(500, 400) @@ -92,9 +93,23 @@ class UIManager: ) 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://") + # Кнопка Вставить рядом с полем URL + url_frame = tk.Frame(main_frame, bg="#2C3E50") + url_frame.pack(fill=tk.X, pady=(0, 15)) + + self.url_entry = tk.Entry(url_frame, font=self.font_label) + self.url_entry.pack(side="left", fill=tk.X, expand=True) + + insert_button = tk.Button( + url_frame, + text="Вставить", + command=self._insert_from_clipboard, + font=self.font_button, + bg="#27AE60", + fg="white", + padx=10 + ) + insert_button.pack(side="left", padx=(10, 0)) # ===== ПОЛЕ 3: ВАРИАНТЫ КНОПОК ===== buttons_label = tk.Label( @@ -190,6 +205,19 @@ class UIManager: # Перенаправление логов в текстовое поле self._setup_log_handler() + + # ===== ВЫБОР ВКЛАДКИ ===== + tabs_label = tk.Label( + main_frame, + text="📑 Выбор вкладки браузера:", + font=self.font_label, + bg="#2C3E50", + fg="#ECF0F1" + ) + tabs_label.pack(anchor="w", pady=(10, 5)) + + self.tab_selector = TabSelectorApp(main_frame, BrowserService(download_dir="./downloads")) + self.tab_selector.pack(fill=tk.X, pady=(0, 15)) def _setup_log_handler(self): """Настраивает обработчик логов в текстовое поле.""" @@ -273,6 +301,15 @@ class UIManager: # Создаем процессор processor = FileProcessorService(directory, url, download_dir) + # Проверка вкладок перед началом загрузки + browser_service = BrowserService(download_dir="./downloads") + browser_service.set_tabs(["Вкладка 1", "Вкладка 2", "Вкладка 3"]) + + # Пример: выбор вкладки по индексу + if not browser_service.select_tab_from_list(0): + logging.error("❌ Не удалось выбрать вкладку. Проверьте массив вкладок.") + return + # Запускаем обработку results = processor.start_processing(upload_texts, process_texts) @@ -310,6 +347,53 @@ class UIManager: messagebox.showinfo("Результаты", result_text) + def _insert_from_clipboard(self): + """Обработчик для вставки текста из буфера обмена.""" + try: + clipboard_text = self.root.clipboard_get() + self.url_entry.delete(0, tk.END) # Очистить поле + self.url_entry.insert(0, clipboard_text) # Вставить текст из буфера + except tk.TclError: + messagebox.showerror("Ошибка", "Буфер обмена пуст или недоступен.") + + +class TabSelectorApp: + def __init__(self, parent, browser_service): + self.browser_service = browser_service + self.frame = tk.Frame(parent, bg="#2C3E50") + + # Выпадающий список + self.combo = ttk.Combobox(self.frame, state="readonly") + self.combo.pack(pady=10) + + # Кнопка для выбора вкладки + self.select_button = tk.Button(self.frame, text="Выбрать вкладку", command=self.select_tab) + self.select_button.pack(pady=10) + + def pack(self, **kwargs): + """Упрощает упаковку фрейма.""" + self.frame.pack(**kwargs) + + def set_tabs(self, tabs): + """Устанавливает массив вкладок и обновляет выпадающий список.""" + self.tabs = tabs + self.combo["values"] = self.tabs + if self.tabs: + self.combo.current(0) # Устанавливаем первый элемент по умолчанию + + def select_tab(self): + """Выбирает вкладку из выпадающего списка.""" + selected_index = self.combo.current() + if selected_index == -1: + print("❌ Вкладка не выбрана.") + return + + success = self.browser_service.select_tab_from_list(selected_index) + if success: + print(f"✅ Вкладка '{self.tabs[selected_index]}' выбрана.") + else: + print(f"❌ Не удалось выбрать вкладку '{self.tabs[selected_index]}'." ) + def main(): """Главная функция."""