commit c9c90e8ddcc77709048eec8c2aa9378119985093 Author: farkadi Date: Mon Jan 13 20:51:31 2025 +0700 launcher project v 2.0 diff --git a/README.md b/README.md new file mode 100644 index 0000000..cdca8df --- /dev/null +++ b/README.md @@ -0,0 +1,108 @@ +# WoW 3.3.5 Launcher + +Современный лаунчер для World of Warcraft 3.3.5a с поддержкой AzerothCore. + +## Возможности + +- 🎮 Современный пользовательский интерфейс +- 🔄 Отображение статуса серверов в реальном времени +- 👥 Система авторизации с поддержкой SRP6 +- ⚙️ Гибкие настройки игры +- 📰 Система новостей +- 🚀 Быстрый запуск игры + +## Требования + +- Python 3.8+ +- PySide6 +- MySQL Server (для работы с AzerothCore) + +## Установка + +1. Клонируйте репозиторий: +```bash +git clone https://github.com/yourusername/wow-3.3.5-launcher.git +cd wow-3.3.5-launcher +``` + +2. Установите зависимости: +```bash +pip install -r requirements.txt +``` + +3. Настройте конфигурацию в файле `config.json`. + +4. Запустите лаунчер: +```bash +python main.py +``` + +## Структура проекта + +wow_launcher/ +├── assets/ # Ресурсы приложения +│ ├── images/ # Изображения и иконки +│ └── styles/ # QSS стили +├── config/ # Конфигурационные файлы +├── docs/ # Документация +└── src/ # Исходный код +├── api/ # API для работы с сервером +└── ui/ # Пользовательский интерфейс + +## Разработка + +Проект находится в активной разработке. Текущие этапы: +- ✅ Базовая структура +- ✅ Интерфейс +- ✅ Стилизация +- ✅ Настройки +- 🔄 Функционал запуска +- 📝 Сетевое взаимодействие +- 📝 Система обновлений +- 📝 Менеджер аддонов + +## Зависимости +``` +PySide6>=6.5.0 +aiohttp>=3.8.0 +aiomysql>=0.2.0 +cryptography>=41.0.0 +``` +## Технологии + +- Python 3.8+ +- PySide6 (Qt для современного UI) +- MySQL (интеграция с AzerothCore) +- asyncio (асинхронные операции) +- aiomysql (асинхронная работа с MySQL) + +## Особенности реализации + +- Асинхронная архитектура для плавной работы UI +- Безопасная авторизация через SRP6 +- Кэширование данных для быстрой работы +- Современный дизайн в стиле Battle.net +- Поддержка тем оформления через QSS + +## Лицензия + +MIT License + +## Авторы + +- Разработка: [Ваше имя] +- Дизайн: [Имя дизайнера] + +## Благодарности + +- [AzerothCore](https://www.azerothcore.org/) за отличный эмулятор +- Сообществу WoW за поддержку и тестирование + + +## Автор + +- [Your Name](https://git.totmin.ru/farkadi) + +## Лицензия + +Этот проект лицензирован под MIT. Смотрите файл `LICENSE` для получения дополнительной информации. diff --git a/assets/images/arrow-down.svg b/assets/images/arrow-down.svg new file mode 100644 index 0000000..1bd7948 --- /dev/null +++ b/assets/images/arrow-down.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/assets/images/background.jpg b/assets/images/background.jpg new file mode 100644 index 0000000..b5fcac0 Binary files /dev/null and b/assets/images/background.jpg differ diff --git a/assets/images/news-icon.svg b/assets/images/news-icon.svg new file mode 100644 index 0000000..c50198a --- /dev/null +++ b/assets/images/news-icon.svg @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/assets/images/news/README.md b/assets/images/news/README.md new file mode 100644 index 0000000..51428d0 --- /dev/null +++ b/assets/images/news/README.md @@ -0,0 +1,29 @@ +# Требования к изображениям новостей + +## Форматы и размеры + +### Главная новость +- Размер: 356x200px +- Соотношение сторон: 16:9 +- Формат: JPG/PNG +- Качество JPG: 85-90% +- Макс. размер файла: 200KB + +### Дополнительные новости +- Размер: 178x100px +- Соотношение сторон: 16:9 +- Формат: JPG/PNG +- Качество JPG: 85-90% +- Макс. размер файла: 100KB + +## Рекомендации +- Используйте RGB цветовую схему +- Избегайте текста на изображениях +- Оптимизируйте изображения перед загрузкой +- Используйте контрастные изображения +- Центрируйте главный объект изображения + +## Имена файлов +- Используйте только латинские буквы и цифры +- Используйте нижнее подчеркивание вместо пробелов +- Пример: main_news.jpg, update_news.jpg \ No newline at end of file diff --git a/assets/images/news/main_news.jpg b/assets/images/news/main_news.jpg new file mode 100644 index 0000000..430ccdc Binary files /dev/null and b/assets/images/news/main_news.jpg differ diff --git a/assets/images/news/update_news.jpg b/assets/images/news/update_news.jpg new file mode 100644 index 0000000..c58f139 Binary files /dev/null and b/assets/images/news/update_news.jpg differ diff --git a/assets/images/ranking-icon.svg b/assets/images/ranking-icon.svg new file mode 100644 index 0000000..f646206 --- /dev/null +++ b/assets/images/ranking-icon.svg @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/assets/images/server-status.svg b/assets/images/server-status.svg new file mode 100644 index 0000000..51d7a18 --- /dev/null +++ b/assets/images/server-status.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/assets/images/settings.svg b/assets/images/settings.svg new file mode 100644 index 0000000..84df4d9 --- /dev/null +++ b/assets/images/settings.svg @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/assets/images/shop-icon.svg b/assets/images/shop-icon.svg new file mode 100644 index 0000000..8184968 --- /dev/null +++ b/assets/images/shop-icon.svg @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/assets/images/wow-logo.png b/assets/images/wow-logo.png new file mode 100644 index 0000000..2454362 Binary files /dev/null and b/assets/images/wow-logo.png differ diff --git a/assets/images/wow-logo.svg b/assets/images/wow-logo.svg new file mode 100644 index 0000000..3bba3a6 --- /dev/null +++ b/assets/images/wow-logo.svg @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + WoW + + + + + + + + + + + + WRATH 3.3.5 + + \ No newline at end of file diff --git a/assets/styles/main.qss b/assets/styles/main.qss new file mode 100644 index 0000000..d1f9908 --- /dev/null +++ b/assets/styles/main.qss @@ -0,0 +1,523 @@ +/* === Базовые стили === */ +QDialog { + background-color: #1a1a1a; +} + +QLabel { + color: white; +} + +/* === Компоненты карточек === */ +Card { + background-color: rgba(0, 0, 0, 0.7); + border-radius: 15px; + border: 1px solid rgba(255, 255, 255, 0.1); +} + +Card.base-card { + border-radius: 10px; + min-height: 100px; + max-height: 100px; +} + +/* Заголовки и значения карточек */ +Card QLabel.title { + color: #FFB100; + font-size: 12px; + font-weight: bold; +} + +Card QLabel.value { + color: white; + font-size: 20px; + font-weight: bold; +} + +Card QLabel.subtitle { + color: rgba(255, 255, 255, 0.7); + font-size: 12px; +} + +QLabel.status-value-online { + color: #2ecc71; + font-size: 20px; + font-weight: bold; +} + +QLabel.status-value-offline { + color: #e74c3c; + font-size: 20px; + font-weight: bold; +} + +.trend-up { + color: #2ecc71; + font-size: 12px; +} + +/* === Карточки статуса === */ +/* Зеленая карточка */ +.status-card-green { + background: qlineargradient( + x1: 0, y1: 0, x2: 1, y2: 1, + stop: 0 rgba(46, 204, 113, 0.2), + stop: 1 rgba(0, 0, 0, 0.7) + ); + border: 1px solid rgba(46, 204, 113, 0.3); +} + +.status-card-green:hover { + background: qlineargradient( + x1: 0, y1: 0, x2: 1, y2: 1, + stop: 0 rgba(46, 204, 113, 0.3), + stop: 1 rgba(0, 0, 0, 0.8) + ); + border: 1px solid rgba(46, 204, 113, 0.5); +} + +/* Синяя карточка */ +.status-card-blue { + background: qlineargradient( + x1: 0, y1: 0, x2: 1, y2: 1, + stop: 0 rgba(52, 152, 219, 0.2), + stop: 1 rgba(0, 0, 0, 0.7) + ); + border: 1px solid rgba(52, 152, 219, 0.3); +} + +.status-card-blue:hover { + background: qlineargradient( + x1: 0, y1: 0, x2: 1, y2: 1, + stop: 0 rgba(52, 152, 219, 0.3), + stop: 1 rgba(0, 0, 0, 0.8) + ); + border: 1px solid rgba(52, 152, 219, 0.5); +} + +/* Фиолетовая карточка */ +.status-card-purple { + background: qlineargradient( + x1: 0, y1: 0, x2: 1, y2: 1, + stop: 0 rgba(155, 89, 182, 0.2), + stop: 1 rgba(0, 0, 0, 0.7) + ); + border: 1px solid rgba(155, 89, 182, 0.3); +} + +.status-card-purple:hover { + background: qlineargradient( + x1: 0, y1: 0, x2: 1, y2: 1, + stop: 0 rgba(155, 89, 182, 0.3), + stop: 1 rgba(0, 0, 0, 0.8) + ); + border: 1px solid rgba(155, 89, 182, 0.5); +} + +/* Красная карточка */ +.status-card-red { + background: qlineargradient( + x1: 0, y1: 0, x2: 1, y2: 1, + stop: 0 rgba(231, 76, 60, 0.2), + stop: 1 rgba(0, 0, 0, 0.7) + ); + border: 1px solid rgba(231, 76, 60, 0.3); +} + +.status-card-red:hover { + background: qlineargradient( + x1: 0, y1: 0, x2: 1, y2: 1, + stop: 0 rgba(231, 76, 60, 0.3), + stop: 1 rgba(0, 0, 0, 0.8) + ); + border: 1px solid rgba(231, 76, 60, 0.5); +} + +/* === Кнопки === */ +/* Навигационные кнопки */ +QPushButton.nav-item { + background: transparent; + border: none; + color: white; + font-size: 14px; + padding: 10px; + text-align: left; +} + +QPushButton.nav-item:hover { + color: #FFB100; +} + +QPushButton.nav-item:hover QIcon { + fill: #FFB100; +} + +/* Кнопка настроек */ +QPushButton.icon-button { + background: transparent; + border: none; + padding: 8px; +} + +QPushButton.icon-button:hover { + background: rgba(255, 255, 255, 0.1); + border-radius: 4px; +} + +QPushButton.icon-button:pressed { + background: rgba(255, 255, 255, 0.2); +} + +/* Кнопка играть */ +QPushButton.play-button { + background: qlineargradient( + x1: 0, y1: 0, x2: 1, y2: 0, + stop: 0 rgba(255, 177, 0, 0.8), + stop: 1 rgba(255, 177, 0, 0.6) + ); + border: 1px solid rgba(255, 177, 0, 0.3); + border-radius: 8px; + color: white; + font-size: 16px; + font-weight: bold; + text-transform: uppercase; + letter-spacing: 2px; +} + +QPushButton.play-button:hover { + background: qlineargradient( + x1: 0, y1: 0, x2: 1, y2: 0, + stop: 0 rgba(255, 177, 0, 0.9), + stop: 1 rgba(255, 177, 0, 0.7) + ); + border: 1px solid rgba(255, 177, 0, 0.5); +} + +QPushButton.play-button:pressed { + background: qlineargradient( + x1: 0, y1: 0, x2: 1, y2: 0, + stop: 0 rgba(255, 147, 0, 0.8), + stop: 1 rgba(255, 147, 0, 0.6) + ); + border: 1px solid rgba(255, 147, 0, 0.5); + padding-top: 2px; +} + +/* Кнопка играть (пульсация) */ +QPushButton.play-button-pulse { + background: qlineargradient( + x1: 0, y1: 0, x2: 1, y2: 0, + stop: 0 #27ae60, + stop: 0.5 #2ecc71, + stop: 1 #27ae60 + ); + border: none; + border-radius: 25px; + color: white; + font-size: 18px; + font-weight: bold; + text-transform: uppercase; + letter-spacing: 2px; + padding: 0px 20px; +} + +/* Навигационные кнопки */ +QPushButton.nav-button { + background: transparent; + border: none; + color: white; + padding: 8px 16px; + font-weight: bold; +} + +QPushButton.nav-button:hover { + background: rgba(255, 255, 255, 0.1); + border-radius: 4px; +} + +QPushButton.nav-button:pressed { + background: rgba(255, 255, 255, 0.2); +} + +/* === Элементы форм === */ +/* Поля ввода */ +QLineEdit.settings-input { + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 4px; + color: white; + padding: 8px; +} + +/* Выпадающие списки */ +QComboBox.settings-combobox { + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 4px; + color: white; + padding: 8px; +} + +QComboBox.settings-combobox::drop-down { + border: none; +} + +QComboBox.settings-combobox::down-arrow { + image: url(assets/images/arrow-down.svg); + width: 12px; + height: 12px; +} + +/* Чекбоксы */ +QCheckBox.settings-checkbox { + color: white; +} + +QCheckBox.settings-checkbox::indicator { + width: 18px; + height: 18px; + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 4px; +} + +QCheckBox.settings-checkbox::indicator:checked { + background: #2ecc71; +} + +/* === Прогресс бар === */ +QProgressBar.progress-bar { + background: rgba(255, 255, 255, 0.1); + border: none; + border-radius: 2px; +} + +QProgressBar.progress-bar::chunk { + background: qlineargradient( + x1: 0, y1: 0, x2: 1, y2: 0, + stop: 0 rgba(255, 177, 0, 0.8), + stop: 1 rgba(255, 177, 0, 0.6) + ); + border-radius: 2px; +} + +/* === Новости === */ +QFrame.news-card { + background-color: rgba(0, 0, 0, 0.5); + border-radius: 10px; + border: 1px solid rgba(255, 255, 255, 0.1); +} + +QFrame.news-card:hover { + background-color: rgba(0, 0, 0, 0.7); + border: 1px solid rgba(255, 255, 255, 0.2); +} + +QLabel.news-image { + background-color: rgba(0, 0, 0, 0.3); + border-radius: 8px; +} + +QLabel.news-tag { + background-color: #FFB100; + color: black; + border-radius: 4px; + font-size: 12px; + font-weight: bold; + min-width: 80px; + max-width: 120px; +} + +QLabel.news-title { + color: white; + font-size: 16px; + font-weight: bold; +} + +QLabel.news-title[main="true"] { + font-size: 20px; +} + +QLabel.news-text { + color: rgba(255, 255, 255, 0.7); + font-size: 13px; +} + +/* === Диалоги === */ +QDialog.settings-dialog { + background-color: #1a1a1a; +} + +QLabel.settings-label { + color: white; + font-size: 14px; + margin-top: 10px; +} + +QDialog.settings-dialog QTabWidget::pane { + border: 1px solid rgba(255, 255, 255, 0.1); + background-color: rgba(0, 0, 0, 0.3); +} + +QDialog.settings-dialog QTabBar::tab { + background-color: rgba(0, 0, 0, 0.3); + color: white; + padding: 8px 20px; + border: none; +} + +QDialog.settings-dialog QTabBar::tab:hover { + background-color: rgba(255, 255, 255, 0.1); +} + +QDialog.settings-dialog QTabBar::tab:selected { + background-color: rgba(255, 177, 0, 0.2); + border-bottom: 2px solid #FFB100; +} + +/* Форма авторизации */ +QLineEdit.login-input { + padding: 10px 12px; + min-height: 20px; + min-width: 200px; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 4px; + color: white; + font-size: 14px; +} + +QLineEdit.login-input:focus { + border: 1px solid #FFB100; + background: rgba(255, 177, 0, 0.1); +} + +QPushButton.login-button { + background: #FFB100; + border: none; + border-radius: 4px; + padding: 8px 16px; + color: black; + font-weight: bold; +} + +QPushButton.login-button:hover { + background: #FFC133; +} + +QPushButton.link-button { + background: transparent; + border: none; + color: #FFB100; +} + +QPushButton.link-button:hover { + color: #FFC133; +} + +/* Кнопка аккаунта */ +QPushButton.account-button { + background: rgba(255, 177, 0, 0.2); + border: 1px solid #FFB100; + border-radius: 4px; + padding: 8px 16px; + color: #FFB100; + font-weight: bold; +} + +QPushButton.account-button:hover { + background: rgba(255, 177, 0, 0.3); +} + +/* Меню аккаунта */ +QMenu.account-menu { + background: #1a1a1a; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 4px; + padding: 4px; +} + +QMenu.account-menu::item { + padding: 8px 16px; + color: white; +} + +QMenu.account-menu::item:selected { + background: rgba(255, 177, 0, 0.2); +} + +QMenu.account-menu::separator { + height: 1px; + background: rgba(255, 255, 255, 0.1); + margin: 4px 0; +} + +/* === Окно авторизации === */ +QDialog#login-dialog { + background: #1a1a1a; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; +} + +/* Поля ввода в окне авторизации */ +QLineEdit.login-input { + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 4px; + padding: 10px 12px; + min-height: 20px; + min-width: 200px; + color: white; + font-size: 14px; +} + +QLineEdit.login-input:focus { + border: 1px solid #FFB100; + background: rgba(255, 177, 0, 0.1); +} + +QLineEdit.login-input:hover { + border: 1px solid rgba(255, 177, 0, 0.5); +} + +/* Кнопки в окне авторизации */ +QPushButton.link-button { + background: transparent; + border: none; + color: #FFB100; + font-size: 12px; + text-decoration: underline; +} + +QPushButton.link-button:hover { + color: #FFC133; +} + +/* Заголовок окна авторизации */ +QLabel.login-title { + color: white; + font-size: 18px; + font-weight: bold; +} + +/* Кнопка входа */ +QPushButton.login-button { + background: #FFB100; + border: none; + border-radius: 4px; + color: #1a1a1a; + font-weight: bold; + font-size: 14px; +} + +QPushButton.login-button:hover { + background: #FFC133; +} + +QPushButton.login-button:pressed { + background: #E69D00; +} + +QPushButton.login-button:disabled { + background: #666666; + color: #999999; +} \ No newline at end of file diff --git a/config/settings.json b/config/settings.json new file mode 100644 index 0000000..1cf1477 --- /dev/null +++ b/config/settings.json @@ -0,0 +1,17 @@ +{ + "game": { + "path": "/mnt/Disk_D/wow", + "realmlist": "set realmlist logon.server.com", + "launch_options": "" + }, + "graphics": { + "resolution": "1920x1080", + "quality": "\u041d\u0438\u0437\u043a\u043e\u0435", + "windowed": false + }, + "auth": { + "username": null, + "account_id": null, + "auto_login": false + } +} \ No newline at end of file diff --git a/docs/development_stages.txt b/docs/development_stages.txt new file mode 100644 index 0000000..ce79eb2 --- /dev/null +++ b/docs/development_stages.txt @@ -0,0 +1,63 @@ +Этапы разработки WoW Launcher + +1. Базовая структура [Выполнено] + - Создание структуры проекта + - Настройка основного окна + - Добавление фонового изображения + +2. Интерфейс [Выполнено] + - Создание шапки с логотипом и навигацией + - Добавление карточек статуса + - Разработка блока новостей + - Создание нижней панели с кнопкой запуска + +3. Стилизация [Выполнено] + - Разработка дизайна карточек + - Создание и добавление иконок + - Настройка градиентов и эффектов + - Добавление анимаций + +4. Настройки [Выполнено] + - Создание окна настроек + - Реализация сохранения настроек в JSON + - Добавление вкладок (Игра, Графика, Аддоны) + - Реализация выбора пути к игре + +5. Функционал запуска [В разработке] + - Проверка наличия игры + - Валидация пути к игре + - Запуск игры с параметрами + - Обработка ошибок запуска + +6. Сетевое взаимодействие [Планируется] + - Подключение к API сервера + - Получение статуса сервера + - Загрузка списка игроков онлайн + - Получение новостей с сервера + +7. Система обновлений [Планируется] + - Проверка версии клиента + - Загрузка обновлений + - Отображение прогресса + - Установка патчей + +8. Аддоны [Планируется] + - Разработка менеджера аддонов + - Загрузка списка доступных аддонов + - Установка и обновление аддонов + - Управление конфигурацией аддонов + +9. Оптимизация [Планируется] + - Оптимизация производительности + - Улучшение обработки ошибок + - Добавление логирования + - Оптимизация использования памяти + +10. Тестирование и отладка [Планируется] + - Модульное тестирование + - Интеграционное тестирование + - Тестирование пользовательского интерфейса + - Исправление обнаруженных ошибок + +Текущий статус: Разработка функционала запуска игры +Следующий этап: Реализация сетевого взаимодействия \ No newline at end of file diff --git a/images/arrow.png b/images/arrow.png new file mode 100644 index 0000000..838c151 Binary files /dev/null and b/images/arrow.png differ diff --git a/images/discord.png b/images/discord.png new file mode 100644 index 0000000..fb3a82e Binary files /dev/null and b/images/discord.png differ diff --git a/images/download.png b/images/download.png new file mode 100644 index 0000000..2ccfd2b Binary files /dev/null and b/images/download.png differ diff --git a/images/folder.png b/images/folder.png new file mode 100644 index 0000000..2a68c2f Binary files /dev/null and b/images/folder.png differ diff --git a/images/forum.png b/images/forum.png new file mode 100644 index 0000000..7c54ed6 Binary files /dev/null and b/images/forum.png differ diff --git a/images/play.png b/images/play.png new file mode 100644 index 0000000..58fd653 Binary files /dev/null and b/images/play.png differ diff --git a/images/screenshot1.jpg b/images/screenshot1.jpg new file mode 100644 index 0000000..ba1ee20 Binary files /dev/null and b/images/screenshot1.jpg differ diff --git a/images/screenshot2.jpg b/images/screenshot2.jpg new file mode 100644 index 0000000..3788804 Binary files /dev/null and b/images/screenshot2.jpg differ diff --git a/images/screenshot3.jpg b/images/screenshot3.jpg new file mode 100644 index 0000000..e1733ad Binary files /dev/null and b/images/screenshot3.jpg differ diff --git a/images/settings.png b/images/settings.png new file mode 100644 index 0000000..a89598c Binary files /dev/null and b/images/settings.png differ diff --git a/images/stop.png b/images/stop.png new file mode 100644 index 0000000..c1a0beb Binary files /dev/null and b/images/stop.png differ diff --git a/images/support.png b/images/support.png new file mode 100644 index 0000000..1688124 Binary files /dev/null and b/images/support.png differ diff --git a/images/wow-logo.png b/images/wow-logo.png new file mode 100644 index 0000000..4133fc6 Binary files /dev/null and b/images/wow-logo.png differ diff --git a/project_structure b/project_structure new file mode 100644 index 0000000..cc90569 --- /dev/null +++ b/project_structure @@ -0,0 +1,25 @@ +wow_launcher/ +├── assets/ +│ ├── images/ +│ │ ├── background.jpg +│ │ ├── wow-logo.png +│ │ ├── settings.svg +│ │ ├── news-icon.svg +│ │ ├── ranking-icon.svg +│ │ ├── shop-icon.svg +│ │ ├── arrow-down.svg +│ │ └── news/ +│ │ ├── main_news.jpg +│ │ ├── update_news.jpg +│ │ ├── arena_news.jpg +│ │ └── items_news.jpg +│ └── styles/ +│ └── main.qss +├── config/ +│ └── settings.json +├── docs/ +│ └── development_stages.txt +└── src/ + ├── main.py + └── ui/ + └── main_window.py \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1fdaf9d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +PySide6>=6.5.0 +aiohttp>=3.8.0 +aiomysql>=0.2.0 +cryptography>=41.0.0 \ No newline at end of file diff --git a/src/api/__pycache__/auth_api.cpython-312.pyc b/src/api/__pycache__/auth_api.cpython-312.pyc new file mode 100644 index 0000000..ba08b78 Binary files /dev/null and b/src/api/__pycache__/auth_api.cpython-312.pyc differ diff --git a/src/api/__pycache__/server_api.cpython-312.pyc b/src/api/__pycache__/server_api.cpython-312.pyc new file mode 100644 index 0000000..d87bed1 Binary files /dev/null and b/src/api/__pycache__/server_api.cpython-312.pyc differ diff --git a/src/api/auth_api.py b/src/api/auth_api.py new file mode 100644 index 0000000..82146af --- /dev/null +++ b/src/api/auth_api.py @@ -0,0 +1,102 @@ +import asyncio +import aiomysql +from dataclasses import dataclass +from typing import Optional +import hashlib +import binascii +import time + +@dataclass +class AuthResult: + success: bool + message: str + account_id: Optional[int] = None + username: Optional[str] = None + gmlevel: Optional[int] = 0 + +class AuthAPI: + def __init__(self): + self.db_config = { + 'host': '192.168.1.42', + 'port': 3306, + 'user': 'launcher_ro', + 'password': 'rPIisIhn46', + 'db': 'acore_auth' + } + # Константы для SRP6 + self.N = 0x894B645E89E1535BBDAD5B8B290650530801B18EBFBF5E8FAB3C82872A3E9BB7 + self.g = 7 + + def _calculate_verifier(self, username: str, password: str, salt: bytes) -> bytes: + """ + Вычисляет верификатор для SRP6 + """ + # 1. Вычисляем h1 = SHA1("USERNAME:PASSWORD") + username = username.upper() + password = password.upper() + h1 = hashlib.sha1(f"{username}:{password}".encode()).digest() + + # 2. Вычисляем h2 = SHA1(salt || h1) + h2 = int.from_bytes( + hashlib.sha1(salt + h1).digest(), + byteorder='little' + ) + + # 3. Вычисляем (g^h2) % N используя встроенную функцию pow + verifier = pow(self.g, h2, self.N) + + # 4. Конвертируем в байты в little-endian порядке + return verifier.to_bytes(32, byteorder='little') + + async def login(self, username: str, password: str) -> AuthResult: + try: + async with aiomysql.connect(**self.db_config) as conn: + async with conn.cursor() as cur: + # Получаем данные аккаунта + await cur.execute(""" + SELECT id, username, salt, verifier, locked + FROM account + WHERE username = %s + """, (username.upper(),)) + + result = await cur.fetchone() + + if not result: + return AuthResult( + success=False, + message="Неверное имя пользователя или пароль" + ) + + account_id, db_username, salt, stored_verifier, locked = result + + # Проверяем блокировку + if locked: + return AuthResult( + success=False, + message="Аккаунт заблокирован" + ) + + # Вычисляем верификатор + calculated_verifier = self._calculate_verifier(username, password, salt) + + # Сравниваем верификаторы + if calculated_verifier != stored_verifier: + return AuthResult( + success=False, + message="Неверное имя пользователя или пароль" + ) + + return AuthResult( + success=True, + message="Успешная авторизация", + account_id=account_id, + username=db_username, + gmlevel=0 + ) + + except Exception as e: + print(f"Error during login: {e}") + return AuthResult( + success=False, + message="Ошибка при авторизации" + ) \ No newline at end of file diff --git a/src/api/server_api.py b/src/api/server_api.py new file mode 100644 index 0000000..1d0489d --- /dev/null +++ b/src/api/server_api.py @@ -0,0 +1,118 @@ +import asyncio +import aiomysql +from dataclasses import dataclass +from typing import Optional, Tuple +import time + +@dataclass +class ServerStatus: + auth_online: bool + world_online: bool + players_online: int + max_players: int = 1000 + realm_name: str = "WotLK Server" + uptime: str = "Unknown" + +class ServerAPI: + def __init__(self): + """Инициализация API для проверки статуса серверов""" + # Настройки серверов + self.auth_address = ('178.159.92.167', 3724) + self.world_address = ('178.159.92.167', 8085) + # Настройки БД (только чтение) + self.db_config = { + 'host': '192.168.1.42', + 'port': 3306, + 'user': 'launcher_ro', # Пользователь для чтения + 'password': 'rPIisIhn46', + 'db': 'acore_characters' + } + # Кэш + self._last_check = None + self._cache_timeout = 10 + self._players_cache = None + self._players_cache_time = None + self._players_cache_timeout = 30 + + async def get_players_count(self) -> int: + """Получает количество игроков через БД""" + try: + async with aiomysql.connect(**self.db_config) as conn: + async with conn.cursor() as cur: + # Запрос согласно структуре БД AzerothCore + await cur.execute(""" + SELECT COUNT(*) as count + FROM characters + WHERE online > 0 + """) + result = await cur.fetchone() + count = result[0] if result else 0 + + print(f"Current online players: {count}") # Отладочный вывод + + # Обновляем кэш + self._players_cache = count + self._players_cache_time = time.time() + + return count + except Exception as e: + print(f"Error getting players count: {e}") + return self._players_cache if self._players_cache is not None else 0 + + async def check_server(self, host: str, port: int) -> bool: + """Проверяет доступность сервера""" + try: + # Создаем футуру с таймаутом в 2 секунды + reader, writer = await asyncio.wait_for( + asyncio.open_connection(host, port), + timeout=2.0 + ) + writer.close() + await writer.wait_closed() + print(f"Server {host}:{port} is online") + return True + except (ConnectionRefusedError, asyncio.TimeoutError): + print(f"Server {host}:{port} is offline") + return False + except Exception as e: + print(f"Error checking server {host}:{port}: {e}") + return False + + async def get_server_status(self) -> ServerStatus: + """Получает статус серверов""" + # Проверяем кэш + if self._last_check: + if (asyncio.get_event_loop().time() - self._last_check[0]) < self._cache_timeout: + return self._last_check[1] + + try: + # Проверяем оба сервера параллельно + auth_check, world_check = await asyncio.gather( + self.check_server(self.auth_address[0], self.auth_address[1]), + self.check_server(self.world_address[0], self.world_address[1]) + ) + + # Если world сервер онлайн, получаем количество игроков + players_online = 0 + if world_check: + try: + players_online = await self.get_players_count() + except Exception as e: + print(f"Error getting players count: {e}") + + print(f"Status: auth={auth_check}, world={world_check}, players={players_online}") + status = ServerStatus( + auth_online=auth_check, + world_online=world_check, + players_online=players_online + ) + # Сохраняем результат в кэш + self._last_check = (asyncio.get_event_loop().time(), status) + return status + except Exception as e: + print(f"Error in get_server_status: {e}") + return ServerStatus( + auth_online=False, + world_online=False, + players_online=0 + ) \ No newline at end of file diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..c82622b --- /dev/null +++ b/src/main.py @@ -0,0 +1,30 @@ +import sys +from pathlib import Path +import threading +import asyncio + +# Добавляем корневую директорию проекта в PYTHONPATH +project_root = Path(__file__).parent.parent +sys.path.append(str(project_root)) + +from src.ui.main_window import MainWindow +from PySide6.QtWidgets import QApplication + +def run_async_loop(loop): + asyncio.set_event_loop(loop) + loop.run_forever() + +def main(): + app = QApplication(sys.argv) + app.setStyle('Fusion') # Явно устанавливаем стиль Fusion + window = MainWindow() + + # Запускаем event loop в отдельном потоке + thread = threading.Thread(target=run_async_loop, args=(window.loop,), daemon=True) + thread.start() + + window.show() + sys.exit(app.exec()) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/ui/__pycache__/login_dialog.cpython-312.pyc b/src/ui/__pycache__/login_dialog.cpython-312.pyc new file mode 100644 index 0000000..6e42972 Binary files /dev/null and b/src/ui/__pycache__/login_dialog.cpython-312.pyc differ diff --git a/src/ui/__pycache__/main_window.cpython-312.pyc b/src/ui/__pycache__/main_window.cpython-312.pyc new file mode 100644 index 0000000..1bdc987 Binary files /dev/null and b/src/ui/__pycache__/main_window.cpython-312.pyc differ diff --git a/src/ui/login_dialog.py b/src/ui/login_dialog.py new file mode 100644 index 0000000..8637300 --- /dev/null +++ b/src/ui/login_dialog.py @@ -0,0 +1,123 @@ +from PySide6.QtWidgets import ( + QDialog, QVBoxLayout, QLineEdit, + QPushButton, QLabel, QMessageBox +) +from PySide6.QtCore import Qt, Signal, QObject +from src.api.auth_api import AuthAPI, AuthResult +import asyncio + +class LoginSignals(QObject): + success = Signal(AuthResult) + error = Signal(str) + +class LoginDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.auth_api = AuthAPI() + self.auth_result = None + self.signals = LoginSignals() + self.setObjectName("login-dialog") + self.setup_ui() + + # Подключаем сигналы + self.signals.success.connect(self.on_login_success) + self.signals.error.connect(self.on_login_error) + + def setup_ui(self): + """Настройка интерфейса""" + self.setWindowTitle("Авторизация") + self.setFixedSize(350, 320) + + layout = QVBoxLayout(self) + layout.setSpacing(15) + layout.setContentsMargins(30, 30, 30, 30) + + # Заголовок + title = QLabel("Вход в аккаунт") + title.setAlignment(Qt.AlignCenter) + title.setProperty("class", "login-title") + layout.addSpacing(5) + layout.addWidget(title) + layout.addSpacing(20) + + # Поля ввода + self.username = QLineEdit() + self.username.setPlaceholderText("Имя аккаунта") + self.username.setProperty("class", "login-input") + self.username.setFixedHeight(40) + layout.addWidget(self.username) + layout.addSpacing(15) + + self.password = QLineEdit() + self.password.setPlaceholderText("Пароль") + self.password.setEchoMode(QLineEdit.Password) + self.password.setProperty("class", "login-input") + self.password.setFixedHeight(40) + layout.addWidget(self.password) + layout.addSpacing(20) + + # Кнопка входа + self.login_button = QPushButton("Войти") + self.login_button.setProperty("class", "login-button") + self.login_button.setFixedHeight(40) + self.login_button.clicked.connect(self.handle_login) + layout.addWidget(self.login_button) + layout.addSpacing(15) + + # Дополнительные кнопки + self.register_button = QPushButton("Регистрация") + self.register_button.setProperty("class", "link-button") + layout.addWidget(self.register_button) + + self.forgot_button = QPushButton("Забыли пароль?") + self.forgot_button.setProperty("class", "link-button") + layout.addWidget(self.forgot_button) + + def handle_login(self): + """Обработчик нажатия кнопки входа""" + username = self.username.text().strip() + password = self.password.text().strip() + + if not username or not password: + QMessageBox.warning( + self, + "Ошибка", + "Введите имя аккаунта и пароль" + ) + return + + # Отключаем кнопку на время авторизации + self.login_button.setEnabled(False) + self.login_button.setText("Вход...") + + # Запускаем асинхронную авторизацию + loop = asyncio.get_event_loop() + loop.create_task(self.try_login(username, password)) + + async def try_login(self, username: str, password: str): + """Асинхронная попытка авторизации""" + try: + result = await self.auth_api.login(username, password) + if result.success: + self.signals.success.emit(result) + else: + self.signals.error.emit(result.message) + except Exception as e: + self.signals.error.emit(str(e)) + finally: + # Возвращаем кнопку в исходное состояние + self.login_button.setEnabled(True) + self.login_button.setText("Войти") + + def on_login_success(self, result: AuthResult): + """Обработчик успешной авторизации""" + self.auth_result = result + self.accept() + + def on_login_error(self, message: str): + """Обработчик ошибки авторизации""" + QMessageBox.warning( + self, + "Ошибка авторизации", + message + ) \ No newline at end of file diff --git a/src/ui/main_window.py b/src/ui/main_window.py new file mode 100644 index 0000000..e126f7c --- /dev/null +++ b/src/ui/main_window.py @@ -0,0 +1,797 @@ +import json +from pathlib import Path +from PySide6.QtWidgets import ( + QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, + QPushButton, QLabel, QProgressBar, QFrame, + QGridLayout, QLineEdit, QDialog, QTabWidget, + QCheckBox, QFileDialog, QComboBox, QMenu +) +from PySide6.QtCore import Qt, QSize, QTimer, QPoint +from PySide6.QtGui import ( + QPixmap, QPalette, QBrush, QFont, QIcon, + QPainter, QLinearGradient, QColor, QAction +) +from src.api.server_api import ServerAPI +from src.api.auth_api import AuthResult +import asyncio +import sys +from src.ui.login_dialog import LoginDialog + +# Константы +CARD_SPACING = 15 +CARD_MARGINS = (15, 10, 15, 10) +DEFAULT_ICON_SIZE = QSize(20, 20) +PLAY_BUTTON_SIZE = QSize(200, 50) +SETTINGS_BUTTON_SIZE = QSize(40, 40) +PROGRESS_BAR_HEIGHT = 4 + +# Цвета +COLOR_PRIMARY = "#FFB100" +COLOR_SUCCESS = "#2ecc71" +COLOR_BACKGROUND = "rgba(0, 0, 0, 0.7)" + +# Размеры +MAIN_NEWS_IMAGE_HEIGHT = 200 +SMALL_NEWS_IMAGE_HEIGHT = 100 +SETTINGS_DIALOG_WIDTH = 600 + +class Card(QFrame): + """Базовый класс для карточек""" + def __init__(self, title="", parent=None): + super().__init__(parent) + self.layout = QVBoxLayout(self) + if title: + title_label = QLabel(title) + title_label.setProperty("class", "title") + self.layout.addWidget(title_label) + +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + + # Загружаем стили + with open("assets/styles/main.qss", "r") as f: + self.setStyleSheet(f.read()) + + # Создаем event loop для асинхронных операций + self.loop = asyncio.new_event_loop() + asyncio.set_event_loop(self.loop) + + # Инициализация API клиента + self.server_api = ServerAPI() + + # Таймер для обновления статуса + self.status_timer = QTimer() + self.status_timer.timeout.connect(self.update_server_status) + self.status_timer.start(30000) # Обновляем каждые 30 секунд + + # Настройки + self.settings_file = Path("config/settings.json") + self.default_settings = { + "game": { + "path": "", + "realmlist": "logon.server.com", + "launch_options": "" + }, + "graphics": { + "resolution": "1920x1080", + "quality": "Высокое", + "windowed": False + }, + "auth": { + "username": None, + "account_id": None, + "auto_login": False + } + } + + self.settings = self.load_settings() + + self.setWindowTitle("WoW 3.3.5 Launcher") + self.setMinimumSize(1200, 800) + + # Фоновое изображение + self.background = QPixmap("assets/images/background.jpg") + self.updateBackground() + + # Главный виджет + central_widget = QWidget() + self.setCentralWidget(central_widget) + + # Главный layout + main_layout = QVBoxLayout(central_widget) + main_layout.setSpacing(20) + main_layout.setContentsMargins(40, 30, 40, 30) + + # Компоненты + main_layout.addWidget(self.create_header()) + main_layout.addWidget(self.create_status_cards()) + main_layout.addWidget(self.create_content()) + main_layout.addWidget(self.create_footer()) + + # Первоначальное получение статуса + self.update_server_status() + + self.current_user = None # Текущий пользователь + + # Проверяем сохраненную авторизацию + auth_settings = self.settings.get("auth", {}) + if auth_settings.get("auto_login"): + self.current_user = AuthResult( + success=True, + message="Успешная авторизация", + account_id=auth_settings["account_id"], + username=auth_settings["username"] + ) + self.update_ui_after_login() + + def create_content(self): + content = Card("НОВОСТИ") + content.setMinimumHeight(400) + + # Создаем grid layout для новостей + grid = QGridLayout() + grid.setSpacing(15) + content.layout.addLayout(grid) + + # Главная новость (большая карточка слева) + main_news = self.create_news_card( + "Открытие нового сезона", + "Встречайте новый сезон арены с обновленной системой рейтинга и наградами!", + "main_news.jpg", + is_main=True + ) + grid.addWidget(main_news, 0, 0, 2, 2) # Занимает 2x2 ячейки + + # Дополнительные новости (справа) + news_items = [ + { + "title": "Обновление 3.3.5a", + "text": "Список изменений и улучшений в новой версии...", + "image": "update_news.jpg", + "tag": "ОБНОВЛЕНИЕ" + }, + { + "title": "Турнир на арене", + "text": "Регистрация на турнир начинается через...", + "image": "arena_news.jpg", + "tag": "СОБЫТИЕ" + }, + { + "title": "Новые предметы", + "text": "В магазине появились новые предметы...", + "image": "items_news.jpg", + "tag": "МАГАЗИН" + } + ] + + for i, news in enumerate(news_items): + card = self.create_news_card( + news["title"], + news["text"], + news["image"], + tag=news["tag"] + ) + grid.addWidget(card, i, 2) # Добавляем справа + + return content + + def create_header(self): + """Создает шапку с логотипом и навигацией""" + header = QWidget() + layout = QHBoxLayout(header) + layout.setContentsMargins(0, 0, 0, 0) + + # Логотип + logo = QLabel() + logo_pixmap = QPixmap("assets/images/wow-logo.png") + logo.setPixmap(logo_pixmap.scaled(150, 50, Qt.KeepAspectRatio, Qt.SmoothTransformation)) + layout.addWidget(logo) + + # Навигация + nav = QHBoxLayout() + nav.setSpacing(20) + + # Добавляем растягивающийся элемент слева + nav.addStretch() + + # Кнопки навигации + news_btn = self.create_nav_button("Новости", "assets/images/news-icon.svg") + ranking_btn = self.create_nav_button("Рейтинг", "assets/images/ranking-icon.svg") + shop_btn = self.create_nav_button("Магазин", "assets/images/shop-icon.svg") + + nav.addWidget(news_btn) + nav.addWidget(ranking_btn) + nav.addWidget(shop_btn) + + # Кнопка логина/аккаунта + self.account_btn = QPushButton("Войти") + self.account_btn.setProperty("class", "login-button") + self.account_btn.clicked.connect(self.show_login_dialog) + nav.addWidget(self.account_btn) + + # Кнопка настроек + settings_btn = QPushButton() + settings_btn.setIcon(QIcon("assets/images/settings.svg")) + settings_btn.setIconSize(DEFAULT_ICON_SIZE) + settings_btn.setProperty("class", "icon-button") + settings_btn.clicked.connect(self.show_settings) + nav.addWidget(settings_btn) + + layout.addLayout(nav) + return header + + def create_status_cards(self): + """Создает карточки статуса сервера, онлайна и версии.""" + widget = QWidget() + layout = QHBoxLayout(widget) + layout.setSpacing(CARD_SPACING) + layout.setContentsMargins(0, 0, 0, 0) + + # Статус сервера + status_card = Card() + status_card.setObjectName("status_card") + status_card.setProperty("class", "base-card status-card status-card-green") + + status_layout = QVBoxLayout() + status_layout.setSpacing(5) + status_layout.setContentsMargins(*CARD_MARGINS) + + title = self.create_label("СТАТУС СЕРВЕРА", "title") + self.status_label = self.create_label("Онлайн", "status-value-online") + self.realm_name = self.create_label("REALM NAME", "subtitle") + + status_layout.addWidget(title) + status_layout.addWidget(self.status_label) + status_layout.addWidget(self.realm_name) + status_card.layout.addLayout(status_layout) + + # Онлайн + online_card = Card() + online_card.setProperty("class", "status-card status-card-blue") + + online_layout = QVBoxLayout() + online_layout.setSpacing(5) + online_layout.setContentsMargins(*CARD_MARGINS) + + online_title = self.create_label("ИГРОКОВ ОНЛАЙН", "title") + + self.online_count = self.create_label("1500", "value") + + self.online_trend = self.create_label("↑ +125 за час", "trend-up") + + online_layout.addWidget(online_title) + online_layout.addWidget(self.online_count) + online_layout.addWidget(self.online_trend) + online_card.layout.addLayout(online_layout) + + # Версия + version_card = Card() + version_card.setProperty("class", "status-card status-card-purple") + + version_layout = QVBoxLayout() + version_layout.setSpacing(5) + version_layout.setContentsMargins(*CARD_MARGINS) + + version_title = self.create_label("ВЕРСИЯ", "title") + + version_number = self.create_label("3.3.5a", "value") + + build_number = self.create_label("12340", "subtitle") + + version_layout.addWidget(version_title) + version_layout.addWidget(version_number) + version_layout.addWidget(build_number) + version_card.layout.addLayout(version_layout) + + # Добавляем карточки в layout + layout.addWidget(status_card) + layout.addWidget(online_card) + layout.addWidget(version_card) + layout.addStretch() + + return widget + + def create_news_section(self): + """Создает секцию новостей""" + news_widget = QWidget() + layout = QVBoxLayout(news_widget) + layout.setSpacing(20) + layout.setContentsMargins(0, 0, 0, 0) + + # Заголовок секции + title = QLabel("Новости") + title.setProperty("class", "section-title") + layout.addWidget(title) + + # Сетка новостей + news_grid = QGridLayout() + news_grid.setSpacing(20) + + # Главная новость + main_news = self.create_news_card( + "Обновление 3.3.5a", + "Установлен патч 3.3.5a...", + "assets/images/news/main_news.jpg", + True + ) + news_grid.addWidget(main_news, 0, 0, 1, 2) + + # Дополнительные новости + news1 = self.create_news_card( + "Открытие арены", + "Новый сезон арены...", + "assets/images/news/arena_news.jpg" + ) + news_grid.addWidget(news1, 1, 0) + + news2 = self.create_news_card( + "Новые предметы", + "Добавлены новые предметы...", + "assets/images/news/items_news.jpg" + ) + news_grid.addWidget(news2, 1, 1) + + layout.addLayout(news_grid) + return news_widget + + def create_news_card(self, title, text, image_path, tag=None, is_main=False): + card = QFrame() + card.setProperty("class", "news-card") + + layout = QVBoxLayout(card) + layout.setSpacing(10) + + # Изображение новости + image_label = QLabel() + image_label.setProperty("class", "news-image") + pixmap = QPixmap(f"assets/images/news/{image_path}") + + # Проверяем, загрузилось ли изображение + if pixmap.isNull(): + # Создаем заглушку с градиентом + if is_main: + pixmap = QPixmap(400, 200) + else: + pixmap = QPixmap(200, 100) + pixmap.fill(Qt.transparent) + + # Можно добавить градиент или цвет заглушки + painter = QPainter(pixmap) + gradient = QLinearGradient(0, 0, pixmap.width(), 0) + gradient.setColorAt(0, QColor("#2c3e50")) + gradient.setColorAt(1, QColor("#3498db")) + painter.fillRect(pixmap.rect(), gradient) + painter.end() + + # Устанавливаем размер и масштабируем изображение + if is_main: + image_label.setFixedHeight(200) + else: + image_label.setFixedHeight(100) + + scaled_pixmap = pixmap.scaled( + image_label.width() if image_label.width() > 0 else pixmap.width(), + image_label.height(), + Qt.KeepAspectRatioByExpanding, + Qt.SmoothTransformation + ) + + image_label.setPixmap(scaled_pixmap) + layout.addWidget(image_label) # Добавляем изображение в layout + + # Тег (если есть) + if tag: + tag_label = QLabel(tag) + tag_label.setProperty("class", "news-tag") + # Даем тегу возможность подстроиться под содержимое + tag_label.adjustSize() + # Добавляем небольшой отступ для текста внутри тега + tag_label.setContentsMargins(8, 2, 8, 2) + layout.addWidget(tag_label) + + # Заголовок + title_label = QLabel(title) + title_label.setProperty("class", "news-title") + if is_main: + title_label.setProperty("main", "true") + layout.addWidget(title_label) # Добавляем заголовок в layout + + # Текст + text_label = QLabel(text) + text_label.setWordWrap(True) + text_label.setProperty("class", "news-text") + layout.addWidget(text_label) # Добавляем текст в layout + + # Кнопка "Подробнее" + if is_main: + more_btn = QPushButton("Подробнее →") + more_btn.setProperty("class", "more-button") + layout.addWidget(more_btn) + + layout.addStretch() + return card + + def create_footer(self): + footer = Card() + + layout = QVBoxLayout() + footer.layout.addLayout(layout) + + # Кнопки + buttons = QHBoxLayout() + + self.play_button = QPushButton("ИГРАТЬ") + self.play_button.setFixedSize(200, 50) + self.play_button.setProperty("class", "play-button") + + buttons.addWidget(self.play_button) + buttons.addStretch() + + # Прогресс + self.progress = QProgressBar() + self.progress.setFixedHeight(4) + self.progress.setTextVisible(False) + self.progress.setProperty("class", "progress-bar") + self.progress.setValue(100) + + layout.addLayout(buttons) + layout.addWidget(self.progress) + + return footer + + def resizeEvent(self, event): + super().resizeEvent(event) + self.updateBackground() + + def updateBackground(self): + # Получаем размеры окна + window_size = self.size() + + # Масштабируем изображение, чтобы оно покрывало всё окно + scaled_bg = self.background.scaled( + window_size.width() + 50, # Добавляем небольшой запас по ширине + window_size.height() + 50, # и высоте, чтобы избежать пустых краёв + Qt.AspectRatioMode.KeepAspectRatioByExpanding, + Qt.TransformationMode.SmoothTransformation + ) + + # Если изображение больше окна, обрезаем его по центру + if scaled_bg.width() > window_size.width() or scaled_bg.height() > window_size.height(): + x = (scaled_bg.width() - window_size.width()) // 2 + y = (scaled_bg.height() - window_size.height()) // 2 + scaled_bg = scaled_bg.copy( + x, y, + window_size.width(), + window_size.height() + ) + + # Устанавливаем фон + palette = self.palette() + palette.setBrush(QPalette.Window, QBrush(scaled_bg)) + self.setPalette(palette) + + def pulse_play_button(self): + self.play_button.setProperty("class", "play-button-pulse") + + def load_settings(self): + """Загружает настройки из файла""" + if self.settings_file.exists(): + try: + with open(self.settings_file, "r") as f: + return json.load(f) + except: + return self.default_settings.copy() + return self.default_settings.copy() + + def save_settings(self): + """Сохраняет настройки в файл""" + self.settings_file.parent.mkdir(parents=True, exist_ok=True) + with open(self.settings_file, "w") as f: + json.dump(self.settings, f, indent=4) + + def get_setting(self, category, key): + """Получает значение настройки по категории и ключу""" + return self.settings.get(category, {}).get(key, self.default_settings[category][key]) + + def set_setting(self, category, key, value): + """Устанавливает значение настройки""" + if category not in self.settings: + self.settings[category] = {} + self.settings[category][key] = value + self.save_settings() + + def show_settings(self): + dialog = SettingsDialog(self) # Теперь передаем self вместо settings_manager + if dialog.exec(): # Если нажата кнопка "Сохранить" + # Получаем значения из диалога и сохраняем их + self.set_setting("game", "path", dialog.game_path_edit.text()) + self.set_setting("game", "realmlist", dialog.realm_box.text()) + self.set_setting("game", "launch_options", dialog.launch_options.text()) + self.set_setting("graphics", "resolution", dialog.resolution.currentText()) + self.set_setting("graphics", "quality", dialog.graphics.currentText()) + self.set_setting("graphics", "windowed", dialog.windowed.isChecked()) + + def create_label(self, text, class_name, additional_props=None): + """Создает стилизованный QLabel с заданным классом. + + Args: + text (str): Текст метки + class_name (str): Имя класса для стилей + additional_props (dict, optional): Дополнительные свойства + + Returns: + QLabel: Созданная метка + """ + label = QLabel(text) + label.setProperty("class", class_name) + if additional_props: + for key, value in additional_props.items(): + label.setProperty(key, value) + return label + + def update_server_status(self): + """Обновляет информацию о статусе сервера""" + async def get_status(): + status = await self.server_api.get_server_status() + if status: + # Обновляем UI в главном потоке + online = status.auth_online and status.world_online + + # Определяем текст статуса + if online: + status_text = "Онлайн" + elif not status.auth_online and not status.world_online: + status_text = "Оффлайн" + elif not status.auth_online: + status_text = "Auth Оффлайн" + else: + status_text = "World Оффлайн" + + # Обновляем текст и стиль статуса + self.status_label.setText(status_text) + self.status_label.setProperty( + "class", + "status-value-online" if online else "status-value-offline" + ) + self.status_label.style().unpolish(self.status_label) + self.status_label.style().polish(self.status_label) + + # Обновляем стиль карточки статуса + status_card = self.findChild(Card, "status_card") + if status_card: + new_class = f"base-card status-card {'status-card-green' if online else 'status-card-red'}" + print(f"Setting card class to: {new_class}") # Отладочный вывод + status_card.setProperty("class", new_class) + status_card.style().unpolish(status_card) + status_card.style().polish(status_card) + status_card.update() # Принудительно обновляем виджет + + self.realm_name.setText(status.realm_name) + self.online_count.setText(str(status.players_online)) + + # Обновляем тренд + self.online_trend.setText(f"↑ {status.players_online} из {status.max_players}") + else: + self.status_label.setText("Недоступен") + self.status_label.setProperty("class", "status-value-offline") + self.status_label.style().unpolish(self.status_label) + self.status_label.style().polish(self.status_label) + + # Обновляем стиль карточки на красный + status_card = self.findChild(Card, "status_card") + if status_card: + status_card.setProperty("class", "base-card status-card status-card-red") + status_card.style().unpolish(status_card) + status_card.style().polish(status_card) + + # Запускаем асинхронную задачу в нашем event loop + future = asyncio.run_coroutine_threadsafe(get_status(), self.loop) + future.add_done_callback(lambda f: self.handle_status_update_error(f)) + + def handle_status_update_error(self, future): + """Обрабатывает ошибки при обновлении статуса""" + try: + future.result() + except Exception as e: + print(f"Error updating server status: {e}") + + def show_login(self): + """Показывает диалог авторизации""" + dialog = LoginDialog(self) + if dialog.exec_(): + # Успешная авторизация + self.current_user = dialog.auth_result + self.update_ui_after_login() + + def update_ui_after_login(self): + """Обновляет UI после успешной авторизации""" + if self.current_user: + # Сохраняем данные авторизации + self.settings["auth"] = { + "username": self.current_user.username, + "account_id": self.current_user.account_id, + "auto_login": True + } + self.save_settings() # Теперь этот вызов должен работать + # Обновляем кнопку + self.account_btn.setText(self.current_user.username) + self.account_btn.setProperty("class", "account-button") + self.account_btn.style().unpolish(self.account_btn) + self.account_btn.style().polish(self.account_btn) + + def show_login_dialog(self): + """Показывает диалог авторизации""" + if not self.current_user: # Если пользователь не авторизован + dialog = LoginDialog(self) + if dialog.exec_(): + # Успешная авторизация + self.current_user = dialog.auth_result + self.update_ui_after_login() + else: # Если пользователь уже авторизован + self.show_account_menu() + + def show_account_menu(self): + """Показывает меню аккаунта""" + menu = QMenu(self) + menu.setProperty("class", "account-menu") + + # Добавляем информацию об аккаунте + account_info = QAction(f"Аккаунт: {self.current_user.username}", menu) + account_info.setEnabled(False) + menu.addAction(account_info) + + menu.addSeparator() + + # Добавляем действия + logout = QAction("Выйти", menu) + logout.triggered.connect(self.logout) + menu.addAction(logout) + + # Показываем меню под кнопкой + menu.exec_(self.account_btn.mapToGlobal( + QPoint(0, self.account_btn.height()) + )) + + def logout(self): + """Выход из аккаунта""" + self.current_user = None + # Очищаем данные авторизации + self.settings["auth"] = self.default_settings["auth"] + self.save_settings() + self.account_btn.setText("Войти") + self.account_btn.setProperty("class", "login-button") + self.account_btn.style().unpolish(self.account_btn) + self.account_btn.style().polish(self.account_btn) + + def create_nav_button(self, text: str, icon_path: str) -> QPushButton: + """Создает навигационную кнопку""" + btn = QPushButton(text) + btn.setIcon(QIcon(icon_path)) + btn.setIconSize(DEFAULT_ICON_SIZE) + btn.setProperty("class", "nav-button") + return btn + +class SettingsDialog(QDialog): + def __init__(self, main_window, parent=None): + super().__init__(parent) + self.main_window = main_window + self.setWindowTitle("Настройки") + self.setMinimumWidth(600) + self.setProperty("class", "settings-dialog") + + self.setup_ui() + + def setup_ui(self): + layout = QVBoxLayout(self) + + # Создаем вкладки + tabs = QTabWidget() + tabs.addTab(self.create_game_tab(), "Игра") + tabs.addTab(self.create_graphics_tab(), "Графика") + tabs.addTab(self.create_addons_tab(), "Аддоны") + + layout.addWidget(tabs) + + # Кнопки + buttons = QHBoxLayout() + buttons.addStretch() + + save_btn = QPushButton("Сохранить") + save_btn.setProperty("class", "save-button") + + cancel_btn = QPushButton("Отмена") + cancel_btn.setProperty("class", "cancel-button") + + buttons.addWidget(cancel_btn) + buttons.addWidget(save_btn) + + layout.addLayout(buttons) + + # Подключаем сигналы + save_btn.clicked.connect(self.accept) + cancel_btn.clicked.connect(self.reject) + + def create_game_tab(self): + tab = QWidget() + layout = QVBoxLayout(tab) + + # Путь к игре + self.game_path_edit = QLineEdit() + self.game_path_edit.setText(self.main_window.get_setting("game", "path")) + game_path = self.create_path_selector( + self.main_window.create_label("Путь к игре:", "settings-label"), + "Выбрать папку...", + self.game_path_edit + ) + layout.addLayout(game_path) + + # Реалм-лист + self.realm_box = QLineEdit() + self.realm_box.setText(self.main_window.get_setting("game", "realmlist")) + self.realm_box.setPlaceholderText("set realmlist logon.server.com") + self.realm_box.setProperty("class", "settings-input") + layout.addWidget(self.main_window.create_label("Реалм-лист:", "settings-label")) + layout.addWidget(self.realm_box) + + # Дополнительные параметры запуска + self.launch_options = QLineEdit() + self.launch_options.setText(self.main_window.get_setting("game", "launch_options")) + self.launch_options.setPlaceholderText("-console -nosound") + self.launch_options.setProperty("class", "settings-input") + layout.addWidget(self.main_window.create_label("Параметры запуска:", "settings-label")) + layout.addWidget(self.launch_options) + + layout.addStretch() + return tab + + def create_graphics_tab(self): + tab = QWidget() + layout = QVBoxLayout(tab) + + # Разрешение экрана + self.resolution = QComboBox() + self.resolution.addItems(["1920x1080", "1600x900", "1366x768"]) + self.resolution.setProperty("class", "settings-combobox") + layout.addWidget(QLabel("Разрешение:")) + layout.addWidget(self.resolution) + + # Качество графики + self.graphics = QComboBox() + self.graphics.addItems(["Низкое", "Среднее", "Высокое", "Ультра"]) + self.graphics.setProperty("class", "settings-combobox") + layout.addWidget(QLabel("Качество графики:")) + layout.addWidget(self.graphics) + + # Оконный режим + self.windowed = QCheckBox("Оконный режим") + self.windowed.setProperty("class", "settings-checkbox") + layout.addWidget(self.windowed) + + layout.addStretch() + return tab + + def create_addons_tab(self): + tab = QWidget() + layout = QVBoxLayout(tab) + layout.addWidget(QLabel("Список аддонов появится в следующем обновлении")) + layout.addStretch() + return tab + + def create_path_selector(self, label_text, button_text, line_edit): + layout = QHBoxLayout() + + layout.addWidget(label_text) + layout.addWidget(line_edit) + + browse_btn = QPushButton(button_text) + browse_btn.setProperty("class", "browse-button") + browse_btn.clicked.connect(lambda: self.browse_path(line_edit)) + layout.addWidget(browse_btn) + + return layout + + def browse_path(self, line_edit): + path = QFileDialog.getExistingDirectory(self, "Выберите папку") + if path: + line_edit.setText(path) \ No newline at end of file