launcher project v 2.0
108
README.md
Normal file
@ -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` для получения дополнительной информации.
|
||||
4
assets/images/arrow-down.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2 4L6 8L10 4" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 253 B |
BIN
assets/images/background.jpg
Normal file
|
After Width: | Height: | Size: 354 KiB |
5
assets/images/news-icon.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 5V19H5V5H19ZM21 3H3V21H21V3ZM17 17H7V16H17V17ZM17 15H7V14H17V15ZM17 12H7V7H17V12Z"
|
||||
fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 268 B |
29
assets/images/news/README.md
Normal file
@ -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
|
||||
BIN
assets/images/news/main_news.jpg
Normal file
|
After Width: | Height: | Size: 169 KiB |
BIN
assets/images/news/update_news.jpg
Normal file
|
After Width: | Height: | Size: 187 KiB |
7
assets/images/ranking-icon.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 15.4L8.24 17.67L9.24 13.39L5.92 10.51L10.3 10.13L12 6.1L13.71 10.14L18.09 10.52L14.77 13.4L15.77 17.68L12 15.4Z"
|
||||
fill="white"/>
|
||||
<path d="M12 2L9.19 8.63L2 9.24L7.46 13.97L5.82 21L12 17.27L18.18 21L16.54 13.97L22 9.24L14.81 8.63L12 2ZM12 15.4L8.24 17.67L9.24 13.39L5.92 10.51L10.3 10.13L12 6.1L13.71 10.14L18.09 10.52L14.77 13.4L15.77 17.68L12 15.4Z"
|
||||
fill="white" fill-opacity="0.4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 569 B |
5
assets/images/server-status.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="8" fill="#2ecc71"/>
|
||||
<circle cx="12" cy="12" r="4" fill="#27ae60"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 244 B |
7
assets/images/settings.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9C10.3431 9 9 10.3431 9 12C9 13.6569 10.3431 15 12 15Z"
|
||||
stroke="#ffffff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M19.4 15C19.2669 15.3016 19.2272 15.6362 19.286 15.9606C19.3448 16.285 19.4995 16.5843 19.73 16.82L19.79 16.88C19.976 17.0657 20.1235 17.2863 20.2241 17.5291C20.3248 17.7719 20.3766 18.0322 20.3766 18.295C20.3766 18.5578 20.3248 18.8181 20.2241 19.0609C20.1235 19.3037 19.976 19.5243 19.79 19.71C19.6043 19.896 19.3837 20.0435 19.1409 20.1441C18.8981 20.2448 18.6378 20.2966 18.375 20.2966C18.1122 20.2966 17.8519 20.2448 17.6091 20.1441C17.3663 20.0435 17.1457 19.896 16.96 19.71L16.9 19.65C16.6643 19.4195 16.365 19.2648 16.0406 19.206C15.7162 19.1472 15.3816 19.1869 15.08 19.32C14.7842 19.4468 14.532 19.6572 14.3543 19.9255C14.1766 20.1938 14.0813 20.5082 14.08 20.83V21C14.08 21.5304 13.8693 22.0391 13.4942 22.4142C13.1191 22.7893 12.6104 23 12.08 23C11.5496 23 11.0409 22.7893 10.6658 22.4142C10.2907 22.0391 10.08 21.5304 10.08 21V20.91C10.0723 20.579 9.96512 20.258 9.77251 19.9887C9.5799 19.7194 9.31074 19.5143 9 19.4C8.69838 19.2669 8.36381 19.2272 8.03941 19.286C7.71502 19.3448 7.41568 19.4995 7.18 19.73L7.12 19.79C6.93425 19.976 6.71368 20.1235 6.47088 20.2241C6.22808 20.3248 5.96783 20.3766 5.705 20.3766C5.44217 20.3766 5.18192 20.3248 4.93912 20.2241C4.69632 20.1235 4.47575 19.976 4.29 19.79C4.10405 19.6043 3.95653 19.3837 3.85588 19.1409C3.75523 18.8981 3.70343 18.6378 3.70343 18.375C3.70343 18.1122 3.75523 17.8519 3.85588 17.6091C3.95653 17.3663 4.10405 17.1457 4.29 16.96L4.35 16.9C4.58054 16.6643 4.73519 16.365 4.794 16.0406C4.85282 15.7162 4.81312 15.3816 4.68 15.08C4.55324 14.7842 4.34276 14.532 4.07447 14.3543C3.80618 14.1766 3.49179 14.0813 3.17 14.08H3C2.46957 14.08 1.96086 13.8693 1.58579 13.4942C1.21071 13.1191 1 12.6104 1 12.08C1 11.5496 1.21071 11.0409 1.58579 10.6658C1.96086 10.2907 2.46957 10.08 3 10.08H3.09C3.42099 10.0723 3.742 9.96512 4.0113 9.77251C4.28059 9.5799 4.48572 9.31074 4.6 9C4.73312 8.69838 4.77282 8.36381 4.714 8.03941C4.65519 7.71502 4.50054 7.41568 4.27 7.18L4.21 7.12C4.02405 6.93425 3.87653 6.71368 3.77588 6.47088C3.67523 6.22808 3.62343 5.96783 3.62343 5.705C3.62343 5.44217 3.67523 5.18192 3.77588 4.93912C3.87653 4.69632 4.02405 4.47575 4.21 4.29C4.39575 4.10405 4.61632 3.95653 4.85912 3.85588C5.10192 3.75523 5.36217 3.70343 5.625 3.70343C5.88783 3.70343 6.14808 3.75523 6.39088 3.85588C6.63368 3.95653 6.85425 4.10405 7.04 4.29L7.1 4.35C7.33568 4.58054 7.63502 4.73519 7.95941 4.794C8.28381 4.85282 8.61838 4.81312 8.92 4.68H9C9.29577 4.55324 9.54802 4.34276 9.72569 4.07447C9.90337 3.80618 9.99872 3.49179 10 3.17V3C10 2.46957 10.2107 1.96086 10.5858 1.58579C10.9609 1.21071 11.4696 1 12 1C12.5304 1 13.0391 1.21071 13.4142 1.58579C13.7893 1.96086 14 2.46957 14 3V3.09C14.0013 3.41179 14.0966 3.72618 14.2743 3.99447C14.452 4.26276 14.7042 4.47324 15 4.6C15.3016 4.73312 15.6362 4.77282 15.9606 4.714C16.285 4.65519 16.5843 4.50054 16.82 4.27L16.88 4.21C17.0657 4.02405 17.2863 3.87653 17.5291 3.77588C17.7719 3.67523 18.0322 3.62343 18.295 3.62343C18.5578 3.62343 18.8181 3.67523 19.0609 3.77588C19.3037 3.87653 19.5243 4.02405 19.71 4.21C19.896 4.39575 20.0435 4.61632 20.1441 4.85912C20.2448 5.10192 20.2966 5.36217 20.2966 5.625C20.2966 5.88783 20.2448 6.14808 20.1441 6.39088C20.0435 6.63368 19.896 6.85425 19.71 7.04L19.65 7.1C19.4195 7.33568 19.2648 7.63502 19.206 7.95941C19.1472 8.28381 19.1869 8.61838 19.32 8.92V9C19.4468 9.29577 19.6572 9.54802 19.9255 9.72569C20.1938 9.90337 20.5082 9.99872 20.83 10H21C21.5304 10 22.0391 10.2107 22.4142 10.5858C22.7893 10.9609 23 11.4696 23 12C23 12.5304 22.7893 13.0391 22.4142 13.4142C22.0391 13.7893 21.5304 14 21 14H20.91C20.5882 14.0013 20.2738 14.0966 20.0055 14.2743C19.7372 14.452 19.5268 14.7042 19.4 15Z"
|
||||
stroke="#ffffff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
5
assets/images/shop-icon.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 6H17C17 3.24 14.76 1 12 1C9.24 1 7 3.24 7 6H5C3.9 6 3 6.9 3 8V20C3 21.1 3.9 22 5 22H19C20.1 22 21 21.1 21 20V8C21 6.9 20.1 6 19 6ZM12 3C13.66 3 15 4.34 15 6H9C9 4.34 10.34 3 12 3ZM19 20H5V8H19V20ZM12 12C10.34 12 9 10.66 9 9H7C7 11.76 9.24 14 12 14C14.76 14 17 11.76 17 9H15C15 10.66 13.66 12 12 12Z"
|
||||
fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 485 B |
BIN
assets/images/wow-logo.png
Normal file
|
After Width: | Height: | Size: 214 KiB |
89
assets/images/wow-logo.svg
Normal file
@ -0,0 +1,89 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="400" height="150" viewBox="0 0 400 150" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<!-- Металлический градиент для основного текста -->
|
||||
<linearGradient id="metallic" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#FFD700"/>
|
||||
<stop offset="50%" style="stop-color:#FFA500"/>
|
||||
<stop offset="100%" style="stop-color:#CD7F32"/>
|
||||
</linearGradient>
|
||||
|
||||
<!-- Градиент для рун -->
|
||||
<linearGradient id="runeGlow" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#00FFFF"/>
|
||||
<stop offset="100%" style="stop-color:#0066FF"/>
|
||||
</linearGradient>
|
||||
|
||||
<!-- Свечение -->
|
||||
<filter id="glow">
|
||||
<feGaussianBlur in="SourceGraphic" stdDeviation="2" result="blur"/>
|
||||
<feComposite in="blur" in2="SourceGraphic" operator="over"/>
|
||||
</filter>
|
||||
|
||||
<!-- Внутреннее свечение для рун -->
|
||||
<filter id="innerGlow">
|
||||
<feGaussianBlur in="SourceAlpha" stdDeviation="2" result="blur"/>
|
||||
<feComposite in="blur" in2="SourceGraphic" operator="in"/>
|
||||
<feMerge>
|
||||
<feMergeNode/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- Фоновая руна (стилизованная под щит) -->
|
||||
<path d="M200,10 L300,50 L300,100 L200,140 L100,100 L100,50 Z"
|
||||
fill="none"
|
||||
stroke="url(#runeGlow)"
|
||||
stroke-width="2"
|
||||
opacity="0.3"
|
||||
filter="url(#glow)"/>
|
||||
|
||||
<!-- Декоративные линии -->
|
||||
<path d="M50,75 L150,75" stroke="url(#metallic)" stroke-width="2" opacity="0.5"/>
|
||||
<path d="M250,75 L350,75" stroke="url(#metallic)" stroke-width="2" opacity="0.5"/>
|
||||
|
||||
<!-- Основной текст -->
|
||||
<g filter="url(#glow)">
|
||||
<!-- Стилизованная буква W -->
|
||||
<path d="M150,40 L170,90 L200,40 L230,90 L250,40"
|
||||
fill="none"
|
||||
stroke="url(#metallic)"
|
||||
stroke-width="8"
|
||||
stroke-linecap="round"/>
|
||||
|
||||
<!-- Текст WoW -->
|
||||
<text x="200" y="100"
|
||||
font-family="Trajan Pro, serif"
|
||||
font-size="72"
|
||||
font-weight="bold"
|
||||
text-anchor="middle"
|
||||
fill="url(#metallic)"
|
||||
stroke="#8B4513"
|
||||
stroke-width="1">
|
||||
WoW
|
||||
</text>
|
||||
</g>
|
||||
|
||||
<!-- Руны по бокам -->
|
||||
<g filter="url(#innerGlow)">
|
||||
<path d="M80,60 L100,40 L120,60 L100,80 Z"
|
||||
fill="none"
|
||||
stroke="url(#runeGlow)"
|
||||
stroke-width="2"/>
|
||||
<path d="M280,60 L300,40 L320,60 L300,80 Z"
|
||||
fill="none"
|
||||
stroke="url(#runeGlow)"
|
||||
stroke-width="2"/>
|
||||
</g>
|
||||
|
||||
<!-- Версия -->
|
||||
<text x="200" y="130"
|
||||
font-family="Trajan Pro, serif"
|
||||
font-size="24"
|
||||
text-anchor="middle"
|
||||
fill="#CD7F32"
|
||||
filter="url(#glow)">
|
||||
WRATH 3.3.5
|
||||
</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
523
assets/styles/main.qss
Normal file
@ -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;
|
||||
}
|
||||
17
config/settings.json
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
63
docs/development_stages.txt
Normal file
@ -0,0 +1,63 @@
|
||||
Этапы разработки WoW Launcher
|
||||
|
||||
1. Базовая структура [Выполнено]
|
||||
- Создание структуры проекта
|
||||
- Настройка основного окна
|
||||
- Добавление фонового изображения
|
||||
|
||||
2. Интерфейс [Выполнено]
|
||||
- Создание шапки с логотипом и навигацией
|
||||
- Добавление карточек статуса
|
||||
- Разработка блока новостей
|
||||
- Создание нижней панели с кнопкой запуска
|
||||
|
||||
3. Стилизация [Выполнено]
|
||||
- Разработка дизайна карточек
|
||||
- Создание и добавление иконок
|
||||
- Настройка градиентов и эффектов
|
||||
- Добавление анимаций
|
||||
|
||||
4. Настройки [Выполнено]
|
||||
- Создание окна настроек
|
||||
- Реализация сохранения настроек в JSON
|
||||
- Добавление вкладок (Игра, Графика, Аддоны)
|
||||
- Реализация выбора пути к игре
|
||||
|
||||
5. Функционал запуска [В разработке]
|
||||
- Проверка наличия игры
|
||||
- Валидация пути к игре
|
||||
- Запуск игры с параметрами
|
||||
- Обработка ошибок запуска
|
||||
|
||||
6. Сетевое взаимодействие [Планируется]
|
||||
- Подключение к API сервера
|
||||
- Получение статуса сервера
|
||||
- Загрузка списка игроков онлайн
|
||||
- Получение новостей с сервера
|
||||
|
||||
7. Система обновлений [Планируется]
|
||||
- Проверка версии клиента
|
||||
- Загрузка обновлений
|
||||
- Отображение прогресса
|
||||
- Установка патчей
|
||||
|
||||
8. Аддоны [Планируется]
|
||||
- Разработка менеджера аддонов
|
||||
- Загрузка списка доступных аддонов
|
||||
- Установка и обновление аддонов
|
||||
- Управление конфигурацией аддонов
|
||||
|
||||
9. Оптимизация [Планируется]
|
||||
- Оптимизация производительности
|
||||
- Улучшение обработки ошибок
|
||||
- Добавление логирования
|
||||
- Оптимизация использования памяти
|
||||
|
||||
10. Тестирование и отладка [Планируется]
|
||||
- Модульное тестирование
|
||||
- Интеграционное тестирование
|
||||
- Тестирование пользовательского интерфейса
|
||||
- Исправление обнаруженных ошибок
|
||||
|
||||
Текущий статус: Разработка функционала запуска игры
|
||||
Следующий этап: Реализация сетевого взаимодействия
|
||||
BIN
images/arrow.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
images/discord.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
images/download.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
images/folder.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
images/forum.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
images/play.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
images/screenshot1.jpg
Normal file
|
After Width: | Height: | Size: 202 KiB |
BIN
images/screenshot2.jpg
Normal file
|
After Width: | Height: | Size: 111 KiB |
BIN
images/screenshot3.jpg
Normal file
|
After Width: | Height: | Size: 111 KiB |
BIN
images/settings.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
images/stop.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
images/support.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
images/wow-logo.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
25
project_structure
Normal file
@ -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
|
||||
4
requirements.txt
Normal file
@ -0,0 +1,4 @@
|
||||
PySide6>=6.5.0
|
||||
aiohttp>=3.8.0
|
||||
aiomysql>=0.2.0
|
||||
cryptography>=41.0.0
|
||||
BIN
src/api/__pycache__/auth_api.cpython-312.pyc
Normal file
BIN
src/api/__pycache__/server_api.cpython-312.pyc
Normal file
102
src/api/auth_api.py
Normal file
@ -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="Ошибка при авторизации"
|
||||
)
|
||||
118
src/api/server_api.py
Normal file
@ -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
|
||||
)
|
||||
30
src/main.py
Normal file
@ -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()
|
||||
BIN
src/ui/__pycache__/login_dialog.cpython-312.pyc
Normal file
BIN
src/ui/__pycache__/main_window.cpython-312.pyc
Normal file
123
src/ui/login_dialog.py
Normal file
@ -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
|
||||
)
|
||||
797
src/ui/main_window.py
Normal file
@ -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)
|
||||