From 659363c4749cb46a7d4af1f7b0c0e5b0266245e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80?= <6terio9@vk.com> Date: Thu, 19 Mar 2026 05:35:55 +1000 Subject: [PATCH] v0.2.0 --- README.md | 145 +++++++---- data/io.github.vual.metainfo.xml | 115 ++++++--- po/en.po | 124 +++++++++- po/ru.po | 130 +++++++++- po/vual.pot | 39 ++- pyproject.toml | 2 +- src/vual/__init__.py | 2 +- src/vual/__main__.py | 2 +- src/vual/app.py | 14 ++ src/vual/config.py | 8 + src/vual/data/style.css | 330 +++++++++++++++++++------ src/vual/i18n.py | 20 +- src/vual/locale/en/LC_MESSAGES/vual.mo | Bin 8553 -> 12259 bytes src/vual/locale/ru/LC_MESSAGES/vual.mo | Bin 10917 -> 16089 bytes src/vual/tables.py | 76 ++++++ src/vual/ui/main_page.py | 127 ++++++++-- src/vual/ui/preferences.py | 16 ++ src/vual/window.py | 212 +++++++++++++++- src/vual/wine_theme.py | 37 --- 19 files changed, 1145 insertions(+), 254 deletions(-) create mode 100644 src/vual/tables.py diff --git a/README.md b/README.md index cef39c4..62460a0 100644 --- a/README.md +++ b/README.md @@ -1,98 +1,137 @@

- Vual + Vual

Vual

- GTK4-приложение для запуска Cheat Engine в Steam-играх через Proton + Запуск Cheat Engine для Steam-игр через Proton

- GPL-3.0 - Python 3.11+ - GTK 4 - Libadwaita 1 + GTK 4 + Libadwaita + Python 3.11+ + GPL-3.0

-## Что это -Vual показывает библиотеку установленных Steam-игр в виде сетки с обложками и позволяет в один клик запустить игру, включить для неё [protonhax](https://github.com/jcnils/protonhax) и открыть Cheat Engine прямо внутри Proton-контекста запущенной игры. +## О проекте -Приложение самостоятельно скачивает и распаковывает Cheat Engine, управляет protonhax, настраивает тему Wine для всех Proton-префиксов и поддерживает русский язык — как собственного интерфейса, так и Cheat Engine. +Vual — приложение для запуска Cheat Engine в Steam-играх, работающих через Proton. +Показывает библиотеку Steam, управляет protonhax и launch options, скачивает и +распаковывает CE автоматически, запускает его в нужном Wine-префиксе — всё из одного +окна. ## Возможности -| | | -|---|---| -| 🎮 **Библиотека** | Сетка игр с обложками из Steam CDN, поиск, сортировка по имени и статусу | -| ▶️ **Запуск** | Запуск игры через Steam URI, автоматическое подключение CE к запущенной Proton-игре | -| 🔧 **protonhax** | Переключатель launch options для каждой игры и массовое включение/отключение | -| 📦 **Cheat Engine** | Скачивание с cheatengine.org, распаковка через Wine из Proton, тестовый запуск | -| 🎨 **Тема Wine** | Тёмная и светлая тема для всех Proton-префиксов разом | -| 🌐 **Локализация** | Язык приложения (EN/RU), язык CE, установка русской локализации CE | -| ⚙️ **Настройки** | Путь к Steam, шаблон LaunchOptions, regex-исключения, размер плитки | +- **Сетка игр** — обложки из Steam CDN, поиск в реальном времени, сортировка по имени или статусу +- **Запуск одним кликом** — игра стартует через Steam, CE подключается к нужному Proton-префиксу +- **Таблицы CE** — привязка .CT-файла к игре, автоматическое открытие вместе с CE +- **protonhax** — автоматическая установка, переключатель launch options для каждой игры, массовое вкл/выкл +- **Установка CE** — скачивание с cheatengine.org, распаковка через Wine из Proton +- **Тема Wine** — тёмная или светлая тема для всех Proton-префиксов разом +- **Локализация** — интерфейс EN/RU, язык CE, установка русской локализации из офиц. репозитория CE +- **Прозрачность окна** — полупрозрачный фон в стиле frosted glass +- **Гайд** — встроенная страница быстрого старта при первом запуске и из меню ## Установка +### Stapler + +``` +stplr repo add lumenara https://github.com/Cheviiot/Lumenara.git +stplr refresh && stplr install vual +``` + ### Из исходников -```bash +``` git clone https://github.com/Cheviiot/vual.git cd vual ./vual ``` -### [Stapler](https://stplr.dev/) - -```bash -stplr repo add lumenara https://github.com/Cheviiot/Lumenara -stplr refresh && stplr install vual -``` -
Зависимости | Дистрибутив | Команда | |---|---| -| ALT Linux | `apt-get install python3-module-pygobject3 libgtk4 libadwaita` | -| Fedora | `dnf install python3-gobject gtk4 libadwaita` | -| Arch | `pacman -S python-gobject gtk4 libadwaita` | -| Debian / Ubuntu | `apt install python3-gi gir1.2-gtk-4.0 gir1.2-adw-1` | +| ALT Linux | `apt-get install python3-module-pygobject3 libgtk4 libadwaita python3-module-requests` | +| Fedora | `dnf install python3-gobject gtk4 libadwaita python3-requests` | +| Arch | `pacman -S python-gobject gtk4 libadwaita python-requests` | +| Debian / Ubuntu | `apt install python3-gi gir1.2-gtk-4.0 gir1.2-adw-1 python3-requests` | -Также необходим **Proton** (установленный через Steam) — для распаковки CE и работы protonhax. +Также нужен **Proton** (через Steam) — для распаковки CE и работы protonhax.
-## Быстрый старт - -1. Запустите `./vual` -2. Откройте настройки (☰ → Настройки) -3. На вкладке **Cheat Engine** скачайте и распакуйте CE -4. Установите **protonhax** если он ещё не установлен -5. Включите переключатель у нужной игры — Vual пропишет launch options -6. Нажмите ▶ у игры — после запуска CE откроется автоматически - -## Структура данных +## Использование ``` -~/.config/vual/ -└── config.json # Настройки приложения - -~/.local/share/vual/ -├── cheatengine/ # Установленный Cheat Engine -├── bin/protonhax # Управляемый скрипт protonhax -└── wine_prefix/ # Префикс Wine для тестового запуска CE - -~/.cache/vual/ -└── covers/ # Кэш обложек Steam +./vual ``` +1. При первом запуске откроется встроенный гайд +2. Перейди в **Настройки → Cheat Engine** — скачай и распакуй CE +3. Установи **protonhax** там же +4. Включи переключатель у нужной игры — Vual пропишет launch options +5. Нажми ▶ — игра запустится, CE откроется автоматически + +## Управление + +| | | +|---|---| +| Запуск CE | Клик по кнопке ▶ на плитке игры | +| Поиск | Клик по полю поиска в заголовке | +| Привязать таблицу | Правая кнопка мыши → Assign Table | +| Вкл/выкл protonhax | Переключатель на плитке игры | +| Массовое вкл/выкл | Кнопки Enable All / Disable All | +| Сортировка | Меню сортировки в заголовке | +| Настройки | Главное меню → Preferences | +| Гайд | Главное меню → Guide | + +## Настройки + +| | | По умолчанию | +|---|---|---| +| Тема | Системная / Светлая / Тёмная | Системная | +| Язык | Авто / English / Русский | Авто | +| Размер плиток | Small / Medium / Large | Medium | +| Сортировка | По имени / По статусу | По имени | +| Прозрачность окна | вкл / выкл | выкл | +| Тема Wine | Тёмная / Светлая | — | +| Язык CE | EN / RU | — | + +## Данные + +`~/.config/vual/` + +| | | +|---|---| +| config.json | Настройки | + +`~/.local/share/vual/` + +| | | +|---|---| +| cheatengine/ | Cheat Engine | +| bin/protonhax | protonhax | +| tables/ | .CT-таблицы и привязки | +| wine_prefix/ | Wine-префикс для тестового запуска CE | + +`~/.cache/vual/` + +| | | +|---|---| +| covers/ | Кэш обложек Steam | + ## Участие в разработке -> **Это личный проект.** Vual создаётся одним человеком для собственного использования при помощи ИИ. +> Это личный проект. Vual создаётся одним человеком для собственного использования при помощи ИИ. -Код полностью открыт. Если хотите помочь — Pull Request и Issue приветствуются: исправление ошибок, новые функции, переводы, плагины. +Код полностью открыт. Если хотите помочь — Pull Request и Issue приветствуются: +исправление ошибок, новые функции, переводы. ## Лицензия diff --git a/data/io.github.vual.metainfo.xml b/data/io.github.vual.metainfo.xml index 476ce7c..5f2f758 100644 --- a/data/io.github.vual.metainfo.xml +++ b/data/io.github.vual.metainfo.xml @@ -1,36 +1,51 @@ io.github.vual + Vual - Launch Cheat Engine for Steam games via Proton + Cheat Engine launcher for Steam + Proton Запуск Cheat Engine для Steam-игр через Proton + CC0-1.0 GPL-3.0-or-later + + Cheviiot + +

- Vual is a GTK4/libadwaita application that lets you browse your Steam - library, toggle protonhax launch options per game, and run Cheat Engine - inside the Proton context of a running game — all from a single window. + Vual lets you run Cheat Engine inside any Steam game that uses Proton. + It reads your Steam library, manages protonhax launch options, downloads + and extracts Cheat Engine automatically, and launches CE in the correct + Wine prefix — all from one compact window.

- Vual — приложение на GTK4/libadwaita, которое показывает библиотеку Steam, - позволяет включать protonhax для каждой игры и запускать Cheat Engine - внутри Proton-контекста запущенной игры — из одного окна. + Vual позволяет запускать Cheat Engine в любой Steam-игре, работающей + через Proton. Приложение читает библиотеку Steam, управляет launch + options для protonhax, автоматически скачивает и распаковывает Cheat + Engine и запускает CE в нужном Wine-префиксе — всё из одного окна.

+ +

Key features:

+

Основные возможности:

@@ -40,9 +55,18 @@ https://github.com/Cheviiot/vual/issues https://github.com/Cheviiot/vual - - vual - + + #62a0ea + #1a5fb4 + + + + + Steam game library with covers + Библиотека Steam с обложками + https://raw.githubusercontent.com/Cheviiot/vual/main/data/screenshots/main.png + + 360 @@ -53,31 +77,44 @@ keyboard - - #62a0ea - #1a5fb4 - - - - - Steam game library - Библиотека игр Steam - https://raw.githubusercontent.com/Cheviiot/vual/main/data/screenshots/main.png - - + + vual + + + +

CE table binding, Wine theme manager, built-in guide, window transparency, Adwyra-inspired theme, Russian CE locale installer.

+

Привязка CE-таблиц, менеджер тем Wine, встроенный гайд, прозрачность окна, тема Adwyra, установка русской локализации CE.

+
+
-

First release.

-

Первый выпуск.

+

First release — game grid, protonhax management, CE download and launch.

+

Первый выпуск — сетка игр, управление protonhax, скачивание и запуск CE.

- - Cheviiot - + + cheatengine + cheat engine + steam + proton + wine + games + чит + игры + + + + Game + Utility + + + + [(98, 160, 234)] +
diff --git a/po/en.po b/po/en.po index 954e9be..2100b2a 100644 --- a/po/en.po +++ b/po/en.po @@ -4,7 +4,7 @@ # msgid "" msgstr "" -"Project-Id-Version: vual 0.1.0\n" +"Project-Id-Version: vual 0.2.0\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-03-19 00:00+0000\n" "PO-Revision-Date: 2026-03-19 00:00+0000\n" @@ -106,6 +106,31 @@ msgstr "Theme applied to %d prefixes" msgid "Error applying to %d prefixes" msgstr "Error applying to %d prefixes" +# Visual Effects +msgid "Visual Effects" +msgstr "Visual Effects" + +msgid "Shadows, transparency, and accent colors" +msgstr "Shadows, transparency, and accent colors" + +msgid "Shadows" +msgstr "Shadows" + +msgid "Enhanced tile shadows and hover lift" +msgstr "Enhanced tile shadows and hover lift" + +msgid "Transparency" +msgstr "Transparency" + +msgid "Glassmorphism effect on overlays" +msgstr "Glassmorphism effect on overlays" + +msgid "Accent glow" +msgstr "Accent glow" + +msgid "Glow effect on active elements" +msgstr "Glow effect on active elements" + # Steam page msgid "Steam" msgstr "Steam" @@ -226,6 +251,18 @@ msgstr "Launch game" msgid "Launch Cheat Engine" msgstr "Launch Cheat Engine" +msgid "Assign table…" +msgstr "Assign table…" + +msgid "Remove table" +msgstr "Remove table" + +msgid "Select Cheat Engine table" +msgstr "Select Cheat Engine table" + +msgid "CE tables (*.CT)" +msgstr "CE tables (*.CT)" + msgid "Launching %s..." msgstr "Launching %s..." @@ -299,6 +336,91 @@ msgstr "Settings" msgid "About" msgstr "About" +# Guide +msgid "Guide" +msgstr "Guide" + +msgid "Close guide" +msgstr "Close guide" + +msgid "Welcome to Vual" +msgstr "Welcome to Vual" + +msgid "Getting Started" +msgstr "Getting Started" + +msgid "Set up Vual in a couple of minutes" +msgstr "Set up Vual in a couple of minutes" + +msgid "Vual links Steam, Proton and Cheat Engine so you can launch the game first and open CE in the right prefix without manual setup every time." +msgstr "Vual links Steam, Proton and Cheat Engine so you can launch the game first and open CE in the right prefix without manual setup every time." + +msgid "Before you start" +msgstr "Before you start" + +msgid "Make sure the base tools are ready before enabling games." +msgstr "Make sure the base tools are ready before enabling games." + +msgid "Install or download Cheat Engine in Settings" +msgstr "Install or download Cheat Engine in Settings" + +msgid "Check that the Steam path points to the correct library" +msgstr "Check that the Steam path points to the correct library" + +msgid "Close Steam before changing launch options for many games" +msgstr "Close Steam before changing launch options for many games" + +msgid "How Vual works" +msgstr "How Vual works" + +msgid "The usual workflow is short and always the same." +msgstr "The usual workflow is short and always the same." + +msgid "Pick a game" +msgstr "Pick a game" + +msgid "Find the game in the library and enable the switch on its tile." +msgstr "Find the game in the library and enable the switch on its tile." + +msgid "Start it from Steam" +msgstr "Start it from Steam" + +msgid "Steam launches the game with protonhax and prepares the correct environment." +msgstr "Steam launches the game with protonhax and prepares the correct environment." + +msgid "Open Cheat Engine" +msgstr "Open Cheat Engine" + +msgid "Use the tile menu to launch CE directly for that running game." +msgstr "Use the tile menu to launch CE directly for that running game." + +msgid "Optional: attach a table" +msgstr "Optional: attach a table" + +msgid "Bind a .CT file once and Vual will pass it to Cheat Engine automatically." +msgstr "Bind a .CT file once and Vual will pass it to Cheat Engine automatically." + +msgid "Good to know" +msgstr "Good to know" + +msgid "A few things save time once the basics are configured." +msgstr "A few things save time once the basics are configured." + +msgid "Use search in the header to filter large libraries quickly" +msgstr "Use search in the header to filter large libraries quickly" + +msgid "The menu button on each tile contains CE actions and table binding" +msgstr "The menu button on each tile contains CE actions and table binding" + +msgid "Window transparency can be toggled in Appearance settings" +msgstr "Window transparency can be toggled in Appearance settings" + +msgid "Open Settings" +msgstr "Open Settings" + +msgid "Open Library" +msgstr "Open Library" + # CE page additional msgid "All files" msgstr "All files" diff --git a/po/ru.po b/po/ru.po index 5d4ad10..27ccdc6 100644 --- a/po/ru.po +++ b/po/ru.po @@ -4,7 +4,7 @@ # msgid "" msgstr "" -"Project-Id-Version: vual 0.1.0\n" +"Project-Id-Version: vual 0.2.0\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-03-19 00:00+0000\n" "PO-Revision-Date: 2026-03-19 00:00+0000\n" @@ -54,6 +54,12 @@ msgstr "Язык интерфейса приложения" msgid "Restart app to apply language" msgstr "Перезапустите приложение для смены языка" +msgid "Transparent window" +msgstr "Прозрачность окна" + +msgid "Semi-transparent background" +msgstr "Полупрозрачный фон" + msgid "Grid" msgstr "Сетка" @@ -106,6 +112,31 @@ msgstr "Тема применена к %d префиксам" msgid "Error applying to %d prefixes" msgstr "Ошибка применения к %d префиксам" +# Visual Effects +msgid "Visual Effects" +msgstr "Визуальные эффекты" + +msgid "Shadows, transparency, and accent colors" +msgstr "Тени, прозрачность и акцентные цвета" + +msgid "Shadows" +msgstr "Тени" + +msgid "Enhanced tile shadows and hover lift" +msgstr "Усиленные тени и подъём при наведении" + +msgid "Transparency" +msgstr "Прозрачность" + +msgid "Glassmorphism effect on overlays" +msgstr "Эффект стекла на накладках" + +msgid "Accent glow" +msgstr "Акцентное свечение" + +msgid "Glow effect on active elements" +msgstr "Свечение активных элементов" + # Steam page msgid "Steam" msgstr "Steam" @@ -226,6 +257,18 @@ msgstr "Запустить игру" msgid "Launch Cheat Engine" msgstr "Запустить Cheat Engine" +msgid "Assign table…" +msgstr "Назначить таблицу…" + +msgid "Remove table" +msgstr "Убрать таблицу" + +msgid "Select Cheat Engine table" +msgstr "Выберите таблицу Cheat Engine" + +msgid "CE tables (*.CT)" +msgstr "Таблицы CE (*.CT)" + msgid "Launching %s..." msgstr "Запуск %s…" @@ -299,6 +342,91 @@ msgstr "Настройки" msgid "About" msgstr "О программе" +# Guide +msgid "Guide" +msgstr "Гайд" + +msgid "Close guide" +msgstr "Закрыть гайд" + +msgid "Welcome to Vual" +msgstr "Добро пожаловать в Vual" + +msgid "Getting Started" +msgstr "Быстрый старт" + +msgid "Set up Vual in a couple of minutes" +msgstr "Настройте Vual за пару минут" + +msgid "Vual links Steam, Proton and Cheat Engine so you can launch the game first and open CE in the right prefix without manual setup every time." +msgstr "Vual связывает Steam, Proton и Cheat Engine, чтобы вы могли сначала запускать игру, а затем открывать CE в нужном префиксе без ручной настройки каждый раз." + +msgid "Before you start" +msgstr "Перед началом" + +msgid "Make sure the base tools are ready before enabling games." +msgstr "Убедитесь, что базовые инструменты готовы, прежде чем включать игры." + +msgid "Install or download Cheat Engine in Settings" +msgstr "Установите или скачайте Cheat Engine в настройках" + +msgid "Check that the Steam path points to the correct library" +msgstr "Проверьте, что путь Steam указывает на правильную библиотеку" + +msgid "Close Steam before changing launch options for many games" +msgstr "Закройте Steam перед изменением параметров запуска для множества игр" + +msgid "How Vual works" +msgstr "Как работает Vual" + +msgid "The usual workflow is short and always the same." +msgstr "Обычный сценарий короткий и всегда одинаковый." + +msgid "Pick a game" +msgstr "Выберите игру" + +msgid "Find the game in the library and enable the switch on its tile." +msgstr "Найдите игру в библиотеке и включите переключатель на её плитке." + +msgid "Start it from Steam" +msgstr "Запустите её из Steam" + +msgid "Steam launches the game with protonhax and prepares the correct environment." +msgstr "Steam запустит игру через protonhax и подготовит правильное окружение." + +msgid "Open Cheat Engine" +msgstr "Откройте Cheat Engine" + +msgid "Use the tile menu to launch CE directly for that running game." +msgstr "Используйте меню плитки, чтобы запустить CE прямо для этой запущенной игры." + +msgid "Optional: attach a table" +msgstr "Необязательно: привяжите таблицу" + +msgid "Bind a .CT file once and Vual will pass it to Cheat Engine automatically." +msgstr "Привяжите .CT файл один раз, и Vual будет автоматически передавать его в Cheat Engine." + +msgid "Good to know" +msgstr "Полезно знать" + +msgid "A few things save time once the basics are configured." +msgstr "Несколько вещей заметно экономят время после базовой настройки." + +msgid "Use search in the header to filter large libraries quickly" +msgstr "Используйте поиск в заголовке, чтобы быстро фильтровать большие библиотеки" + +msgid "The menu button on each tile contains CE actions and table binding" +msgstr "Кнопка меню на каждой плитке содержит действия CE и привязку таблиц" + +msgid "Window transparency can be toggled in Appearance settings" +msgstr "Прозрачность окна можно включить или выключить в настройках внешнего вида" + +msgid "Open Settings" +msgstr "Открыть настройки" + +msgid "Open Library" +msgstr "Открыть библиотеку" + # CE page additional msgid "Actions" msgstr "Действия" diff --git a/po/vual.pot b/po/vual.pot index 515bc4d..07e6968 100644 --- a/po/vual.pot +++ b/po/vual.pot @@ -6,7 +6,7 @@ #, fuzzy msgid "" msgstr "" -"Project-Id-Version: vual 0.1.0\n" +"Project-Id-Version: vual 0.2.0\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-03-19 00:00+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" @@ -101,6 +101,31 @@ msgstr "" msgid "Error applying to %d prefixes" msgstr "" +# Visual Effects +msgid "Visual Effects" +msgstr "" + +msgid "Shadows, transparency, and accent colors" +msgstr "" + +msgid "Shadows" +msgstr "" + +msgid "Enhanced tile shadows and hover lift" +msgstr "" + +msgid "Transparency" +msgstr "" + +msgid "Glassmorphism effect on overlays" +msgstr "" + +msgid "Accent glow" +msgstr "" + +msgid "Glow effect on active elements" +msgstr "" + # Steam page msgid "Steam" msgstr "" @@ -224,6 +249,18 @@ msgstr "" msgid "Launch Cheat Engine" msgstr "" +msgid "Assign table…" +msgstr "" + +msgid "Remove table" +msgstr "" + +msgid "Select Cheat Engine table" +msgstr "" + +msgid "CE tables (*.CT)" +msgstr "" + msgid "Launching %s..." msgstr "" diff --git a/pyproject.toml b/pyproject.toml index e0c597b..0d707f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "vual" -version = "0.1.0" +version = "0.2.0" description = "Launch Cheat Engine for Steam games via Proton" readme = "README.md" license = "GPL-3.0-or-later" diff --git a/src/vual/__init__.py b/src/vual/__init__.py index 3d59679..3dcf8d6 100644 --- a/src/vual/__init__.py +++ b/src/vual/__init__.py @@ -2,7 +2,7 @@ from pathlib import Path -__version__ = "0.1.0" +__version__ = "0.2.0" APP_ID = "io.github.vual" APP_NAME = "Vual" diff --git a/src/vual/__main__.py b/src/vual/__main__.py index 93b04bc..d0778b2 100644 --- a/src/vual/__main__.py +++ b/src/vual/__main__.py @@ -7,7 +7,7 @@ import sys def _ensure_gi_available(): """Add system site-packages to path if gi is not importable (e.g., in venv).""" try: - import gi # noqa: F401 + __import__("gi") return except ImportError: pass diff --git a/src/vual/app.py b/src/vual/app.py index 541e6aa..f6225ef 100644 --- a/src/vual/app.py +++ b/src/vual/app.py @@ -39,6 +39,10 @@ class VualApp(Adw.Application): win.set_default_size(self.config.window_width, self.config.window_height) win.present() + # Show guide on first run + if not self.config.guide_shown: + win.show_guide() + # ── Startup: register actions ──────────────────────────────── def do_startup(self) -> None: @@ -67,6 +71,10 @@ class VualApp(Adw.Application): prefs_action.connect("activate", self._on_preferences) self.add_action(prefs_action) + guide_action = Gio.SimpleAction.new("guide", None) + guide_action.connect("activate", self._on_guide) + self.add_action(guide_action) + quit_action = Gio.SimpleAction.new("quit", None) quit_action.connect("activate", lambda *_: self.quit()) self.add_action(quit_action) @@ -97,3 +105,9 @@ class VualApp(Adw.Application): win = PreferencesWindow(config=self.config, transient_for=self.props.active_window) win.present() + + # ── Guide ──────────────────────────────────────────────────── + + def _on_guide(self, _action: Gio.SimpleAction, _param: None) -> None: + if self.props.active_window: + self.props.active_window.show_guide() diff --git a/src/vual/config.py b/src/vual/config.py index f43825b..f30f96a 100644 --- a/src/vual/config.py +++ b/src/vual/config.py @@ -37,6 +37,8 @@ class Config: ce_language: CE language ("system", "ru_RU"). wine_theme: Wine color theme ("system", "dark", "light"). app_language: Application UI language ("system", "en", "ru"). + transparent_window: Enable semi-transparent window background. + guide_shown: Whether the startup guide has been shown. """ ce_executable: str = "~/.local/share/vual/cheatengine/cheatengine-x86_64.exe" @@ -52,6 +54,8 @@ class Config: ce_language: str = "system" wine_theme: str = "system" app_language: str = "system" + transparent_window: bool = False + guide_shown: bool = False # ════════════════════════════════════════════════════════════════ # Load / Save @@ -81,6 +85,8 @@ class Config: ce_language=data.get("ce_language", cls.ce_language), wine_theme=data.get("wine_theme", cls.wine_theme), app_language=data.get("app_language", cls.app_language), + transparent_window=data.get("transparent_window", cls.transparent_window), + guide_shown=data.get("guide_shown", cls.guide_shown), ) except (json.JSONDecodeError, OSError): pass @@ -107,6 +113,8 @@ class Config: "ce_language": self.ce_language, "wine_theme": self.wine_theme, "app_language": self.app_language, + "transparent_window": self.transparent_window, + "guide_shown": self.guide_shown, } CONFIG_PATH.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8") return CONFIG_PATH diff --git a/src/vual/data/style.css b/src/vual/data/style.css index 707d978..d988a93 100644 --- a/src/vual/data/style.css +++ b/src/vual/data/style.css @@ -1,52 +1,69 @@ /* ═══════════════════════════════════════════════════════════════════════════ - Vual — Unified Size System - - Base unit: 4px - All sizes are multiples of 4px for consistency + Vual — Adwyra-inspired theme + Minimalist, clean design with soft shadows and accent highlights ═══════════════════════════════════════════════════════════════════════════ */ +/* ── Transparent Window ────────────────────────────────────────────────── */ -/* ═══════════════════════════════════════════════════════════════════════════ - HEADER BAR - ═══════════════════════════════════════════════════════════════════════════ */ +window.transparent-window { + background-color: alpha(@window_bg_color, 0.82); +} +/* ── Header Bar ───────────────────────────────────────────────────────────── */ -/* Header buttons: 32x32 (8 units) */ .header-btn { min-width: 32px; min-height: 32px; padding: 0; margin: 0; - border-radius: 6px; + border-radius: 10px; + transition: background-color 150ms ease-out, box-shadow 150ms ease-out; } .header-btn:hover { - background: alpha(@accent_bg_color, 0.12); + background-color: alpha(@accent_color, 0.08); + box-shadow: 0 2px 6px alpha(black, 0.06); } .header-btn:active { - background: alpha(@accent_bg_color, 0.2); + background-color: alpha(@accent_color, 0.16); + box-shadow: none; } -/* Games counter */ .games-counter { - font-size: 0.85em; - font-weight: 500; - opacity: 0.6; + font-size: 0.84em; + font-weight: 450; + opacity: 0.65; margin-left: 8px; } -/* Search entry */ +/* ── Search Entry ─────────────────────────────────────────────────────────── */ + searchentry { min-width: 220px; + border-radius: 14px; + padding: 1px 10px; + background-color: alpha(@view_bg_color, 0.3); + border: 1px solid alpha(@borders, 0.1); + box-shadow: 0 1px 3px alpha(black, 0.04); + transition: box-shadow 200ms ease-out, background-color 200ms ease-out, + border-color 200ms ease-out; } -/* ═══════════════════════════════════════════════════════════════════════════ - GRID LAYOUT - ═══════════════════════════════════════════════════════════════════════════ */ - -.vual-grid { - /* Padding handled in Python via set_margin_* */ +searchentry:focus { + background-color: @view_bg_color; + border-color: alpha(@accent_color, 0.35); + box-shadow: 0 0 0 3px alpha(@accent_color, 0.15); } +searchentry image { + opacity: 0.5; +} + +searchentry:focus image { + opacity: 0.75; +} + +/* ── Grid Layout ──────────────────────────────────────────────────────────── */ + flowboxchild { padding: 0; margin: 0; @@ -64,49 +81,47 @@ flowboxchild:selected { box-shadow: none; } -/* ═══════════════════════════════════════════════════════════════════════════ - TILE - ═══════════════════════════════════════════════════════════════════════════ */ +/* ── Tile ─────────────────────────────────────────────────────────────────── */ -/* Tile container: 12px radius */ .vual-tile { - border-radius: 12px; + border-radius: 14px; background: @card_bg_color; - box-shadow: 0 1px 2px alpha(black, 0.08); + box-shadow: 0 1px 3px alpha(black, 0.10), 0 0 0 1px alpha(black, 0.04); + transition: box-shadow 150ms ease-out; } .vual-tile:hover { - box-shadow: 0 2px 8px alpha(black, 0.15); + box-shadow: 0 4px 12px alpha(black, 0.14), 0 0 0 1px alpha(black, 0.05); } -/* ═══════════════════════════════════════════════════════════════════════════ - TILE INFO OVERLAY (bottom) - ═══════════════════════════════════════════════════════════════════════════ */ +/* ── Tile Info Overlay ────────────────────────────────────────────────────── */ -/* Info panel: gradient background */ .vual-tile-info { - padding: 24px 8px 8px 8px; + padding: 20px 8px 8px 8px; background: linear-gradient(to bottom, transparent 0%, - alpha(black, 0.6) 50%, - alpha(black, 0.8) 100%); - border-radius: 0 0 12px 12px; + alpha(black, 0.55) 50%, + alpha(black, 0.75) 100%); + border-radius: 0 0 14px 14px; } .vual-tile-info label { color: white; + transition: opacity 150ms ease-out; } .vual-tile-info .heading { - font-weight: 600; - font-size: 0.85em; + font-weight: 500; + font-size: 0.84em; + opacity: 0.9; } -/* ═══════════════════════════════════════════════════════════════════════════ - TILE BUTTONS (play, CE) - ═══════════════════════════════════════════════════════════════════════════ */ +.vual-tile:hover .vual-tile-info .heading { + opacity: 1; +} + +/* ── Tile Buttons ─────────────────────────────────────────────────────────── */ -/* Tile buttons: 28x28 (7 units) */ .tile-btn, .vual-tile-info button { min-width: 28px; @@ -114,28 +129,47 @@ flowboxchild:selected { padding: 0; margin: 0; color: white; - background: alpha(white, 0.12); - border-radius: 6px; + background: alpha(white, 0.08); + border-radius: 8px; border: none; + transition: background-color 150ms ease-out, box-shadow 150ms ease-out; } .tile-btn:hover, .vual-tile-info button:hover { - background: alpha(white, 0.24); + background: alpha(white, 0.18); + box-shadow: 0 2px 6px alpha(black, 0.12); } .tile-btn:active, .vual-tile-info button:active { - background: alpha(white, 0.32); + background: alpha(white, 0.26); + box-shadow: none; } -/* ═══════════════════════════════════════════════════════════════════════════ - TILE SWITCH - ═══════════════════════════════════════════════════════════════════════════ */ +/* Popover menus — compact */ +popover contents { + padding: 4px; +} + +popover modelbutton { + min-height: 22px; + padding: 2px 8px; + font-size: 0.88em; + border-radius: 6px; + transition: background-color 100ms ease-out; +} + +popover modelbutton:hover { + background-color: alpha(@accent_color, 0.1); +} + +/* ── Tile Switch ──────────────────────────────────────────────────────────── */ .vual-tile-info switch { - background: alpha(white, 0.15); + background: alpha(white, 0.12); border: none; + transition: background-color 150ms ease-out, box-shadow 150ms ease-out; } .vual-tile-info switch:checked { @@ -146,50 +180,206 @@ flowboxchild:selected { background: white; min-width: 16px; min-height: 16px; - border-radius: 8px; + border-radius: 50%; + box-shadow: 0 1px 3px alpha(black, 0.18); } -/* ═══════════════════════════════════════════════════════════════════════════ - RELOAD BUTTON (top-left corner) - ═══════════════════════════════════════════════════════════════════════════ */ +/* ── Reload Button ────────────────────────────────────────────────────────── */ -/* Reload: 24x24 (6 units) */ .vual-reload-btn { min-width: 24px; min-height: 24px; padding: 0; margin: 0; - border-radius: 6px; - background: alpha(black, 0.5); + border-radius: 50%; + background: alpha(black, 0.35); color: white; opacity: 0; border: none; + transition: opacity 200ms ease-out, background-color 200ms ease-out; } .vual-tile:hover .vual-reload-btn { - opacity: 1; + opacity: 0.7; } .vual-reload-btn:hover { - background: @accent_bg_color; + opacity: 1; + background: alpha(@accent_color, 0.75); } -/* ═══════════════════════════════════════════════════════════════════════════ - RUNNING BADGE (top-right corner) - ═══════════════════════════════════════════════════════════════════════════ */ +/* ── Running Badge ────────────────────────────────────────────────────────── */ -/* Badge: 20px total (14px icon + 3px padding each side) */ .vual-badge { - background: @success_bg_color; + min-width: 12px; + min-height: 12px; + background: radial-gradient(circle at 35% 35%, alpha(white, 0.95) 0%, alpha(@accent_color, 0.95) 32%, @accent_color 68%, alpha(@accent_color, 0.88) 100%); border-radius: 50%; - padding: 3px; + border: 1px solid alpha(white, 0.45); + box-shadow: 0 0 0 1px alpha(@accent_color, 0.16), 0 0 8px alpha(@accent_color, 0.55), 0 0 16px alpha(@accent_color, 0.32), 0 2px 6px alpha(black, 0.22); } -/* ═══════════════════════════════════════════════════════════════════════════ - SKELETON (loading placeholder) - ═══════════════════════════════════════════════════════════════════════════ */ +/* ── Skeleton ─────────────────────────────────────────────────────────────── */ .skeleton { - background: alpha(@card_shade_color, 0.2); + background: alpha(@card_shade_color, 0.15); + border-radius: 14px; +} + +/* ═══════════════════════════════════════════════════════════════════════════ + GUIDE PAGE + ═══════════════════════════════════════════════════════════════════════════ */ + +.guide-page { + margin: 0; +} + +.guide-hero { + padding: 24px 24px 28px 24px; + border-radius: 24px; + background: linear-gradient( + 180deg, + alpha(@accent_color, 0.12) 0%, + alpha(@accent_color, 0.04) 100% + ); + border: 1px solid alpha(@accent_color, 0.08); +} + +.guide-eyebrow { + font-size: 0.82em; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + opacity: 0.7; +} + +.guide-title { + font-size: 2.1em; + font-weight: 800; + letter-spacing: -0.03em; +} + +.guide-subtitle { + font-size: 1.02em; + opacity: 0.78; + line-height: 1.35; +} + +.guide-card { + padding: 20px; + border-radius: 20px; + background: alpha(@view_bg_color, 0.55); + border: 1px solid alpha(@borders, 0.12); + box-shadow: 0 1px 3px alpha(black, 0.04); +} + +.guide-card-title { + font-size: 1.08em; + font-weight: 700; +} + +.guide-card-subtitle { + opacity: 0.7; + line-height: 1.35; +} + +.guide-list-row { + margin-top: 2px; +} + +.guide-bullet { + min-width: 12px; + opacity: 0.7; +} + +.guide-list-text { + line-height: 1.35; +} + +.guide-step { + padding: 10px 0; +} + +.guide-step-badge { + border-radius: 999px; + background: alpha(@accent_color, 0.14); + color: @accent_color; + font-weight: 700; +} + +.guide-step-title { + font-weight: 650; +} + +.guide-step-description { + opacity: 0.72; + line-height: 1.35; +} + +/* ═══════════════════════════════════════════════════════════════════════════ + PREFERENCES + ═══════════════════════════════════════════════════════════════════════════ */ + +preferencesgroup > box > .header { + margin-top: 4px; + margin-bottom: 4px; +} + +preferencesgroup > box > box.header > box.title > label.title { + font-weight: 600; + font-size: 0.84em; + letter-spacing: 0.04em; + opacity: 0.55; + text-transform: uppercase; +} + +preferencesgroup > box > .header > label.description { + font-size: 0.8em; +} + +/* Boxed lists */ +.boxed-list { border-radius: 12px; + border: 1px solid alpha(@borders, 0.12); + background-color: alpha(@view_bg_color, 0.45); + box-shadow: 0 1px 3px alpha(black, 0.04); +} + +row { + border-radius: 10px; + margin: 0 4px; + padding: 0; + min-height: 40px; + transition: background-color 120ms ease-out; +} + +row:hover { + background-color: alpha(@accent_color, 0.06); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + UTILITY CLASSES + ═══════════════════════════════════════════════════════════════════════════ */ + +.dimmed { + opacity: 0.45; + transition: opacity 150ms ease-out; +} + +.dimmed:hover { + opacity: 0.9; +} + +.success { color: @success_color; } +.warning { color: @warning_color; } + +separator { + background-color: alpha(@borders, 0.18); + margin: 4px 0; + min-height: 1px; +} + +tooltip { + border-radius: 8px; + padding: 4px 8px; } diff --git a/src/vual/i18n.py b/src/vual/i18n.py index c21a417..73cbfce 100644 --- a/src/vual/i18n.py +++ b/src/vual/i18n.py @@ -23,7 +23,6 @@ _LOCALE_DIRS = [ # Global translator _translator: gettext.GNUTranslations | gettext.NullTranslations | None = None -_current_lang: str = "system" def _find_locale_dir() -> Path | None: @@ -47,8 +46,7 @@ def init(lang: str = "system") -> None: lang: Language code ("system", "en", "ru"). "system" uses system locale. """ - global _translator, _current_lang - _current_lang = lang + global _translator # Get system locale try: @@ -78,23 +76,8 @@ def init(lang: str = "system") -> None: _translator = gettext.NullTranslations() -def set_language(lang: str) -> None: - """Change application language. - - Args: - lang: Language code ("system", "en", "ru"). - """ - init(lang) - - -def get_current_language() -> str: - """Get current language setting.""" - return _current_lang - - def _(message: str) -> str: """Translate a message string.""" - global _translator if _translator is None: init() return _translator.gettext(message) if _translator else message @@ -102,7 +85,6 @@ def _(message: str) -> str: def ngettext(singular: str, plural: str, n: int) -> str: """Translate a plural message.""" - global _translator if _translator is None: init() return _translator.ngettext(singular, plural, n) if _translator else (singular if n == 1 else plural) diff --git a/src/vual/locale/en/LC_MESSAGES/vual.mo b/src/vual/locale/en/LC_MESSAGES/vual.mo index feb9c2a1c15edfafb2964305731eba82d3283fe4..c0b4e2c95cee3c1532139c093f0e31935e3aa008 100644 GIT binary patch literal 12259 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 delta 3438 zcmY+^c~DkW7{~F$rYI-~uP6!#A|Rp&xFDzq=DsVgo8`U{ z=D*)i_7YLVQGa8;#$oNaP^JYN6Nu9>01L1^7Ghgmfk9Y>dd2Phz8zz@-{dl?2GzkujK;gDL<3kZEp0DU zyHv+4?8^NpjK>A2iPocAOR<{@eVD_j85~1(_!Y88<~C};r>J%=Uee5ap=O$lTH18v z&rIQ>6)Qv~RE|1BYfuB%q59j^iS^ga-{pa3=)q(>iQ#w$wS-Sl?fl8E3x;D)OhFAi z1(om$)J%7vR_-0=`8ibLcTrpQCu&QY!&rZ1>S8o?5Qq997kzOKYTyD?=B1d3Ymh$7 zTc{3?p%QIGt;|_e`*Wxjy5ij5!0y~XK&?!m+to7jFjU8p$ZsYWwYM`-Gps~)v>W*| zuX9lcN1gjmP>G*ME%hx_0>7aWYC^5>Q&hqMtiQ%_2T{>XdZ3ml1J!UOs-y9!nax1G zxDb_42`YhlRQsJs-)1lB@V$?5cpUZKPZ){UPy;_kCge7Or0vIpP~?0W7b^1v#{tOp zn>^G2`B>~@%raC0-*F)5%UnUN&;wLIk5T_00yq$QKLOP)6Eko!=IHycbAC95%Iq@g zaNWmP3?^+o?}ysEJme>{0Cjd&J8nkJcn|i$_Ol~7kcsZ1P#I)7#rm0`FU zlkhZZz>j}ps89p1^<59gz{dIze%gXmVK zja1a|7OKNXsKeKRixNpe&3FXPz_~aKkD!+N9xBl$?2Vn6ZZ>A460JeCYea3~In+vA zk750jsWdwclX$9*N>LrvqrQp{F$*u^2n>pCndwAK(U|3u+7AM8+~_orbq@BKLnd&qs1Vv{H+aN2V6pC36V1)Ssdf zzmJ13l>fV_I0jR&1~tLMZYs*?Thta@$HDjjl~6qY#s*?Gs^bdO_k0lb``f5QPGA9E z#{QVbLDA=1i0Z!-wK6ZD2HuL=5_ba?&Gbvm!<(q3j3<31l#8QrD(c1kI0?T%ZAlQz zrFNr{SItb+=er(t7`LHj{23~dd#IKA3)v#KiJ;eD9;9FZ4nZZ9SWM`XoaH=ggR7kS5?oK{Q`QP|M7%}Gd&h4_<&~CarVzDnn~3d1Yne}_oY+NZ zwl5P)2^H@6F)4kdOF`5J#Em8ryHqCL@C`ccVrZnofb zr#=^biI<281n0=q5p9V;ViDm-yg;bvEWAqSY%CyJ%L*!cHGdrj4zn3Wj3+9HAcF1m z?(?(w9HBC%1@D&B&vwkqC=Bp4MW|0pWdt$ATQlYv+(Ybk?nOP}9VDS7c4<-hk}8w8xT2=I z$VGuq1FgS{vaC;~- a&HkQQZm$fkef|Y7|77g| diff --git a/src/vual/tables.py b/src/vual/tables.py new file mode 100644 index 0000000..5675ecd --- /dev/null +++ b/src/vual/tables.py @@ -0,0 +1,76 @@ +"""Cheat Engine table (.CT) storage and per-game binding. + +Tables are stored in ~/.local/share/vual/tables/. +Bindings (app_id → filename) are persisted in tables.json alongside the files. +""" + +from __future__ import annotations + +import json +import shutil +from pathlib import Path + +from gi.repository import GLib + +TABLES_DIR = Path(GLib.get_user_data_dir()) / "vual" / "tables" +_MAP_FILE = TABLES_DIR / "tables.json" + + +# ── Internal helpers ───────────────────────────────────────────── + +def _load_map() -> dict[str, str]: + if _MAP_FILE.exists(): + try: + return json.loads(_MAP_FILE.read_text(encoding="utf-8")) + except (json.JSONDecodeError, OSError): + pass + return {} + + +def _save_map(data: dict[str, str]) -> None: + TABLES_DIR.mkdir(parents=True, exist_ok=True) + _MAP_FILE.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8") + + +# ── Public API ─────────────────────────────────────────────────── + +def bind(app_id: str, src: str | Path) -> Path: + """Copy a .CT file into the tables directory and bind it to *app_id*. + + Returns the destination path inside TABLES_DIR. + """ + src = Path(src) + TABLES_DIR.mkdir(parents=True, exist_ok=True) + + dest = TABLES_DIR / src.name + if dest.resolve() != src.resolve(): + shutil.copy2(src, dest) + + data = _load_map() + data[app_id] = dest.name + _save_map(data) + return dest + + +def unbind(app_id: str) -> None: + """Remove the table binding for *app_id* (file kept on disk).""" + data = _load_map() + if app_id in data: + del data[app_id] + _save_map(data) + + +def get_table(app_id: str) -> Path | None: + """Return the full path to the bound .CT file, or None.""" + data = _load_map() + name = data.get(app_id) + if not name: + return None + path = TABLES_DIR / name + return path if path.is_file() else None + + +def get_table_name(app_id: str) -> str | None: + """Return just the filename of the bound table, or None.""" + data = _load_map() + return data.get(app_id) diff --git a/src/vual/ui/main_page.py b/src/vual/ui/main_page.py index 15eb406..c5a6e28 100644 --- a/src/vual/ui/main_page.py +++ b/src/vual/ui/main_page.py @@ -19,11 +19,11 @@ gi.require_version("Gdk", "4.0") gi.require_version("GdkPixbuf", "2.0") gi.require_version("Pango", "1.0") -from gi.repository import Adw, Gdk, GdkPixbuf, GLib, Gtk, Pango +from gi.repository import Adw, Gdk, GdkPixbuf, Gio, GLib, Gtk, Pango import requests -from vual import protonhax, steam +from vual import protonhax, steam, tables from vual.config import TILE_SIZES, Config from vual.i18n import _ @@ -217,14 +217,39 @@ class GameTile(Gtk.Overlay): btn_play.connect("clicked", self._on_play_clicked) row.append(btn_play) - # CE button - btn_ce = Gtk.Button() - btn_ce.set_icon_name("utilities-terminal-symbolic") - btn_ce.add_css_class("flat") - btn_ce.add_css_class("tile-btn") - btn_ce.set_tooltip_text(_("Launch Cheat Engine")) - btn_ce.connect("clicked", self._on_ce_clicked) - row.append(btn_ce) + # CE button with table menu + self._btn_ce = Gtk.MenuButton() + self._btn_ce.set_icon_name("open-menu-symbolic") + self._btn_ce.add_css_class("flat") + self._btn_ce.add_css_class("tile-btn") + self._update_ce_tooltip() + + ce_menu = Gio.Menu() + ce_menu.append(_("Launch Cheat Engine"), "tile.launch-ce") + ce_menu.append(_("Assign table…"), "tile.assign-table") + ce_menu.append(_("Remove table"), "tile.remove-table") + + pop = Gtk.PopoverMenu.new_from_model(ce_menu) + self._btn_ce.set_popover(pop) + + # Actions + group = Gio.SimpleActionGroup() + + act_launch = Gio.SimpleAction.new("launch-ce", None) + act_launch.connect("activate", lambda *_: self._on_ce_clicked(None)) + group.add_action(act_launch) + + act_assign = Gio.SimpleAction.new("assign-table", None) + act_assign.connect("activate", lambda *_: self._on_assign_table()) + group.add_action(act_assign) + + self._act_remove = Gio.SimpleAction.new("remove-table", None) + self._act_remove.connect("activate", lambda *_: self._on_remove_table()) + self._act_remove.set_enabled(tables.get_table(self._app_id) is not None) + group.add_action(self._act_remove) + + self.insert_action_group("tile", group) + row.append(self._btn_ce) # Spacer spacer = Gtk.Box() @@ -257,8 +282,7 @@ class GameTile(Gtk.Overlay): def _build_running_badge(self) -> None: """Create running badge in top-right corner.""" - self._badge = Gtk.Image.new_from_icon_name("media-playback-start-symbolic") - self._badge.set_pixel_size(14) + self._badge = Gtk.Box() self._badge.add_css_class("vual-badge") self._badge.set_valign(Gtk.Align.START) self._badge.set_halign(Gtk.Align.END) @@ -322,12 +346,35 @@ class GameTile(Gtk.Overlay): if hasattr(self, "_on_launch"): self._on_launch(self._app_id, self._name) - def _on_ce_clicked(self, _btn: Gtk.Button) -> None: + def _on_ce_clicked(self, _btn: Gtk.Button | None) -> None: """Emit CE signal.""" if hasattr(self, "_on_launch_ce"): self._on_launch_ce(self._app_id, self._name) - def _on_switch_toggled(self, switch: Gtk.Switch, state: bool) -> bool: + def _on_assign_table(self) -> None: + """Open file chooser to assign a .CT table.""" + if hasattr(self, "_on_pick_table"): + self._on_pick_table(self._app_id) + + def _on_remove_table(self) -> None: + """Remove the bound table.""" + tables.unbind(self._app_id) + self._act_remove.set_enabled(False) + self._update_ce_tooltip() + + def _update_ce_tooltip(self) -> None: + name = tables.get_table_name(self._app_id) + if name: + self._btn_ce.set_tooltip_text(f"CE: {name}") + else: + self._btn_ce.set_tooltip_text(_("Launch Cheat Engine")) + + def mark_table_bound(self) -> None: + """Refresh table indicator after binding.""" + self._act_remove.set_enabled(tables.get_table(self._app_id) is not None) + self._update_ce_tooltip() + + def _on_switch_toggled(self, _switch: Gtk.Switch, state: bool) -> bool: """Handle activation toggle.""" self._is_active = state if hasattr(self, "_on_toggle"): @@ -353,6 +400,9 @@ class GameTile(Gtk.Overlay): def connect_reload(self, callback) -> None: self._on_reload = callback + def connect_pick_table(self, callback) -> None: + self._on_pick_table = callback + # ═══════════════════════════════════════════════════════════════════════════════ # MainPage — the main games grid @@ -473,11 +523,6 @@ class MainPage(Adw.Bin): for app_id, tile in self._tiles.items(): tile.set_running(app_id in running_ids) - @property - def games_count(self) -> int: - """Number of loaded games.""" - return len(self._tiles) - # ─── Filter & Sort ──────────────────────────────────────────────────────── def _filter_func(self, child: Gtk.FlowBoxChild) -> bool: @@ -567,6 +612,7 @@ class MainPage(Adw.Bin): tile.connect_launch_ce(self._on_launch_ce) tile.connect_toggle(self._on_toggle) tile.connect_reload(self._on_reload_cover) + tile.connect_pick_table(self._on_pick_table) self._tiles[app_id] = tile self._grid.append(tile) @@ -666,16 +712,50 @@ class MainPage(Adw.Bin): self._win.show_toast(_("CE not found: %s") % self._cfg.ce_executable) return - args = None + args: list[str] = [] if self._cfg.ce_language != "system": - args = ["--LANG", self._cfg.ce_language] + args.extend(["--LANG", self._cfg.ce_language]) - proc = protonhax.run_in_proton(app_id, str(self._cfg.ce_executable_path), args) + ct = tables.get_table(app_id) + if ct: + args.append(str(ct)) + + proc = protonhax.run_in_proton(app_id, str(self._cfg.ce_executable_path), args or None) if proc: self._win.show_toast(_("CE launched for %s") % name) else: self._win.show_toast(_("Failed to launch CE for %s") % name) + def _on_pick_table(self, app_id: str) -> None: + """Open file chooser to select a .CT table for *app_id*.""" + dialog = Gtk.FileDialog() + dialog.set_title(_("Select Cheat Engine table")) + + f = Gtk.FileFilter() + f.set_name(_("CE tables (*.CT)")) + f.add_pattern("*.CT") + f.add_pattern("*.ct") + filters = Gio.ListStore.new(Gtk.FileFilter) + filters.append(f) + dialog.set_filters(filters) + + dialog.open(self._win, None, self._on_table_chosen, app_id) + + def _on_table_chosen(self, dialog: Gtk.FileDialog, result: Gio.AsyncResult, app_id: str) -> None: + try: + gfile = dialog.open_finish(result) + except GLib.Error: + return + if not gfile: + return + path = gfile.get_path() + if not path: + return + tables.bind(app_id, path) + tile = self._tiles.get(app_id) + if tile: + tile.mark_table_bound() + def _on_toggle(self, app_id: str, active: bool) -> None: """Handle activation toggle.""" tile = self._tiles.get(app_id) @@ -811,6 +891,3 @@ class MainPage(Adw.Bin): if restart: steam.start_steam() - def schedule_auto_launch(self, app_id: str) -> None: - """Schedule auto CE launch for a game.""" - self._auto_ce = app_id diff --git a/src/vual/ui/preferences.py b/src/vual/ui/preferences.py index 2122854..0e1e3c5 100644 --- a/src/vual/ui/preferences.py +++ b/src/vual/ui/preferences.py @@ -29,6 +29,7 @@ class PreferencesWindow(Adw.PreferencesWindow): **kwargs, ) self._config = config + self._parent = kwargs.get("transient_for") self._exclusion_rows: list[Adw.EntryRow] = [] self._release_info: dict | None = None self._building = True # Prevent saves during initial build @@ -95,6 +96,15 @@ class PreferencesWindow(Adw.PreferencesWindow): self._lang_app_row.connect("notify::selected", self._on_app_lang_changed) theme_group.add(self._lang_app_row) + # Transparent window toggle + self._transparent_row = Adw.SwitchRow( + title=_("Transparent window"), + subtitle=_("Semi-transparent background"), + ) + self._transparent_row.set_active(self._config.transparent_window) + self._transparent_row.connect("notify::active", self._on_transparent_changed) + theme_group.add(self._transparent_row) + # Grid group grid_group = Adw.PreferencesGroup( title=_("Grid"), @@ -179,6 +189,12 @@ class PreferencesWindow(Adw.PreferencesWindow): # Show restart hint self._show_toast(_("Restart app to apply language")) + def _on_transparent_changed(self, row: Adw.SwitchRow, _pspec) -> None: + self._config.transparent_window = row.get_active() + self._save() + if self._parent: + self._parent.apply_transparency() + def _apply_theme(self) -> None: style = Adw.StyleManager.get_default() schemes = { diff --git a/src/vual/window.py b/src/vual/window.py index fe5e0c0..211fcce 100644 --- a/src/vual/window.py +++ b/src/vual/window.py @@ -31,12 +31,23 @@ class VualWindow(Adw.ApplicationWindow): self.toast_overlay = Adw.ToastOverlay() self.set_content(self.toast_overlay) - toolbar_view = Adw.ToolbarView() - self.toast_overlay.set_child(toolbar_view) + # Navigation view for guide/main switching + self._nav_view = Adw.NavigationView() + self.toast_overlay.set_child(self._nav_view) - # Single main page + # Main page + self._main_nav_page = self._build_main_page() + self._nav_view.add(self._main_nav_page) + + # Apply transparency from config + self.apply_transparency() + + def _build_main_page(self) -> Adw.NavigationPage: + """Build the main games grid page.""" from vual.ui.main_page import MainPage + toolbar_view = Adw.ToolbarView() + self.main_page = MainPage(config=self.config, window=self) # Header bar @@ -102,7 +113,9 @@ class VualWindow(Adw.ApplicationWindow): toolbar_view.set_content(self.main_page) - def _on_sort_changed(self, action: Gio.SimpleAction, param: GLib.Variant) -> None: + return Adw.NavigationPage(child=toolbar_view, title=APP_NAME, tag="main") + + def _on_sort_changed(self, _action: Gio.SimpleAction, param: GLib.Variant) -> None: sort_by = param.get_string() self.config.sort_by = sort_by self.main_page._update_sort() @@ -117,7 +130,7 @@ class VualWindow(Adw.ApplicationWindow): menu = Gio.Menu() menu.append(_("Preferences"), "app.preferences") menu.append(_("About"), "app.about") - menu.append(_("Quit"), "app.quit") + menu.append(_("Guide"), "app.guide") return menu # ── Toast helper ───────────────────────────────────────────── @@ -126,3 +139,192 @@ class VualWindow(Adw.ApplicationWindow): toast = Adw.Toast(title=message, timeout=timeout) self.toast_overlay.add_toast(toast) return False + + # ── Transparency ────────────────────────────────────────────── + + def apply_transparency(self) -> None: + """Apply or remove transparent window CSS class.""" + if self.config.transparent_window: + self.add_css_class("transparent-window") + else: + self.remove_css_class("transparent-window") + + # ── Guide page ─────────────────────────────────────────────── + + def show_guide(self) -> None: + """Show fullscreen guide page.""" + guide_page = self._build_guide_page() + self._nav_view.push(guide_page) + + def _build_guide_page(self) -> Adw.NavigationPage: + """Build the guide/onboarding page.""" + toolbar_view = Adw.ToolbarView() + + header = Adw.HeaderBar() + header.add_css_class("flat") + toolbar_view.add_top_bar(header) + + scroll = Gtk.ScrolledWindow(vexpand=True) + toolbar_view.set_content(scroll) + + clamp = Adw.Clamp(maximum_size=760) + scroll.set_child(clamp) + + content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=20) + content.add_css_class("guide-page") + content.set_margin_top(28) + content.set_margin_bottom(40) + content.set_margin_start(24) + content.set_margin_end(24) + clamp.set_child(content) + + hero = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) + hero.add_css_class("guide-hero") + content.append(hero) + + eyebrow = Gtk.Label(label=_("Getting Started"), xalign=0) + eyebrow.add_css_class("guide-eyebrow") + hero.append(eyebrow) + + title = Gtk.Label(label=_("Set up Vual in a couple of minutes"), xalign=0) + title.add_css_class("guide-title") + title.set_wrap(True) + hero.append(title) + + subtitle = Gtk.Label( + label=_("Vual links Steam, Proton and Cheat Engine so you can launch the game first and open CE in the right prefix without manual setup every time."), + xalign=0, + wrap=True, + ) + subtitle.add_css_class("guide-subtitle") + hero.append(subtitle) + + prep_card = self._build_guide_card( + _("Before you start"), + _("Make sure the base tools are ready before enabling games."), + [ + _("Install or download Cheat Engine in Settings"), + _("Check that the Steam path points to the correct library"), + _("Close Steam before changing launch options for many games"), + ], + ) + content.append(prep_card) + + flow_card = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=14) + flow_card.add_css_class("guide-card") + content.append(flow_card) + + flow_title = Gtk.Label(label=_("How Vual works"), xalign=0) + flow_title.add_css_class("guide-card-title") + flow_card.append(flow_title) + + flow_subtitle = Gtk.Label( + label=_("The usual workflow is short and always the same."), + xalign=0, + wrap=True, + ) + flow_subtitle.add_css_class("guide-card-subtitle") + flow_card.append(flow_subtitle) + + steps = [ + (_("Pick a game"), _("Find the game in the library and enable the switch on its tile.")), + (_("Start it from Steam"), _("Steam launches the game with protonhax and prepares the correct environment.")), + (_("Open Cheat Engine"), _("Use the tile menu to launch CE directly for that running game.")), + (_("Optional: attach a table"), _("Bind a .CT file once and Vual will pass it to Cheat Engine automatically.")), + ] + for index, (step_title, step_desc) in enumerate(steps, start=1): + flow_card.append(self._build_guide_step(index, step_title, step_desc)) + + tips_card = self._build_guide_card( + _("Good to know"), + _("A few things save time once the basics are configured."), + [ + _("Use search in the header to filter large libraries quickly"), + _("The menu button on each tile contains CE actions and table binding"), + _("Window transparency can be toggled in Appearance settings"), + ], + ) + content.append(tips_card) + + actions = Gtk.Box(spacing=12) + actions.set_halign(Gtk.Align.CENTER) + actions.set_margin_top(12) + + prefs_btn = Gtk.Button(label=_("Open Settings")) + prefs_btn.add_css_class("pill") + prefs_btn.connect("clicked", self._on_guide_preferences) + actions.append(prefs_btn) + + start_btn = Gtk.Button(label=_("Open Library")) + start_btn.add_css_class("suggested-action") + start_btn.add_css_class("pill") + start_btn.connect("clicked", self._on_guide_close) + actions.append(start_btn) + content.append(actions) + + return Adw.NavigationPage(child=toolbar_view, title=_("Welcome to Vual"), tag="guide") + + def _build_guide_card(self, title: str, subtitle: str, items: list[str]) -> Gtk.Box: + """Build a guide card with bullet rows.""" + card = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) + card.add_css_class("guide-card") + + title_label = Gtk.Label(label=title, xalign=0) + title_label.add_css_class("guide-card-title") + card.append(title_label) + + subtitle_label = Gtk.Label(label=subtitle, xalign=0, wrap=True) + subtitle_label.add_css_class("guide-card-subtitle") + card.append(subtitle_label) + + for item in items: + row = Gtk.Box(spacing=10) + row.add_css_class("guide-list-row") + + bullet = Gtk.Label(label="•") + bullet.add_css_class("guide-bullet") + row.append(bullet) + + item_label = Gtk.Label(label=item, xalign=0, wrap=True) + item_label.add_css_class("guide-list-text") + row.append(item_label) + card.append(row) + + return card + + def _build_guide_step(self, index: int, title: str, description: str) -> Gtk.Box: + """Build a single step row for the guide.""" + row = Gtk.Box(spacing=14) + row.add_css_class("guide-step") + + badge = Gtk.Label(label=str(index)) + badge.add_css_class("guide-step-badge") + badge.set_size_request(34, 34) + badge.set_valign(Gtk.Align.START) + row.append(badge) + + text_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=3) + + title_label = Gtk.Label(label=title, xalign=0) + title_label.add_css_class("guide-step-title") + text_box.append(title_label) + + desc_label = Gtk.Label(label=description, xalign=0, wrap=True) + desc_label.add_css_class("guide-step-description") + text_box.append(desc_label) + + row.append(text_box) + return row + + def _on_guide_close(self, _btn: Gtk.Button) -> None: + """Close guide and mark as shown.""" + self.config.guide_shown = True + self.config.save() + self._nav_view.pop() + + def _on_guide_preferences(self, _btn: Gtk.Button) -> None: + """Open preferences from the guide.""" + self._on_guide_close(_btn) + app = self.get_application() + if app is not None: + app.activate_action("preferences", None) diff --git a/src/vual/wine_theme.py b/src/vual/wine_theme.py index 08f4823..4a148eb 100644 --- a/src/vual/wine_theme.py +++ b/src/vual/wine_theme.py @@ -367,40 +367,3 @@ def apply_theme_to_all(steamapps: Path, theme: str) -> tuple[int, int]: return (success, failed) -def get_current_theme(pfx: Path) -> str | None: - """Detect current theme from prefix colors. - - Returns: - "dark", "light", or None if unknown. - """ - user_reg = pfx / "user.reg" - if not user_reg.is_file(): - return None - - try: - content = user_reg.read_text(encoding="utf-8", errors="replace") - except OSError: - return None - - # Check Window background color - match = re.search(r'"Window"="([^"]+)"', content) - if not match: - return None - - window_color = match.group(1) - - # Dark themes typically have dark Window color - parts = window_color.split() - if len(parts) == 3: - try: - r, g, b = int(parts[0]), int(parts[1]), int(parts[2]) - # If all RGB < 100, it's dark - if r < 100 and g < 100 and b < 100: - return "dark" - # If all RGB > 200, it's light - if r > 200 and g > 200 and b > 200: - return "light" - except ValueError: - pass - - return None