This commit is contained in:
2026-03-19 05:35:55 +10:00
parent 8f21756df6
commit 659363c474
19 changed files with 1145 additions and 254 deletions
+92 -53
View File
@@ -1,98 +1,137 @@
<p align="center"> <p align="center">
<img src="data/Vual.png" alt="Vual" width="96"> <img src="data/Vual.png" alt="Vual" width="128">
</p> </p>
<h1 align="center">Vual</h1> <h1 align="center">Vual</h1>
<p align="center"> <p align="center">
GTK4-приложение для запуска Cheat Engine в Steam-играх через Proton Запуск Cheat Engine для Steam-игр через Proton
</p> </p>
<p align="center"> <p align="center">
<a href="LICENSE"><img src="https://img.shields.io/badge/license-GPL--3.0-green?style=flat-square" alt="GPL-3.0"></a> <img src="https://img.shields.io/badge/GTK-4-4a86cf?style=flat-square" alt="GTK 4">
<img src="https://img.shields.io/badge/python-3.11+-blue?style=flat-square" alt="Python 3.11+"> <img src="https://img.shields.io/badge/Libadwaita-1-3584e4?style=flat-square" alt="Libadwaita">
<img src="https://img.shields.io/badge/gtk-4-4a86cf?style=flat-square" alt="GTK 4"> <img src="https://img.shields.io/badge/Python-3.11+-3776ab?style=flat-square" alt="Python 3.11+">
<img src="https://img.shields.io/badge/libadwaita-1-3584e4?style=flat-square" alt="Libadwaita 1"> <a href="LICENSE"><img src="https://img.shields.io/badge/License-GPL--3.0-green?style=flat-square" alt="GPL-3.0"></a>
</p> </p>
## Что это
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, CE подключается к нужному Proton-префиксу
| 🎮 **Библиотека** | Сетка игр с обложками из Steam CDN, поиск, сортировка по имени и статусу | - **Таблицы CE** — привязка .CT-файла к игре, автоматическое открытие вместе с CE
| ▶️ **Запуск** | Запуск игры через Steam URI, автоматическое подключение CE к запущенной Proton-игре | - **protonhax** — автоматическая установка, переключатель launch options для каждой игры, массовое вкл/выкл
| 🔧 **protonhax** | Переключатель launch options для каждой игры и массовое включение/отключение | - **Установка CE** — скачивание с cheatengine.org, распаковка через Wine из Proton
| 📦 **Cheat Engine** | Скачивание с cheatengine.org, распаковка через Wine из Proton, тестовый запуск | - **Тема Wine** — тёмная или светлая тема для всех Proton-префиксов разом
| 🎨 **Тема Wine** | Тёмная и светлая тема для всех Proton-префиксов разом | - **Локализация** — интерфейс EN/RU, язык CE, установка русской локализации из офиц. репозитория CE
| 🌐 **Локализация** | Язык приложения (EN/RU), язык CE, установка русской локализации CE | - **Прозрачность окна** — полупрозрачный фон в стиле frosted glass
| ⚙️ **Настройки** | Путь к Steam, шаблон LaunchOptions, regex-исключения, размер плитки | - **Гайд** — встроенная страница быстрого старта при первом запуске и из меню
## Установка ## Установка
### 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 git clone https://github.com/Cheviiot/vual.git
cd vual cd vual
./vual ./vual
``` ```
### [Stapler](https://stplr.dev/)
```bash
stplr repo add lumenara https://github.com/Cheviiot/Lumenara
stplr refresh && stplr install vual
```
<details> <details>
<summary><strong>Зависимости</strong></summary> <summary><strong>Зависимости</strong></summary>
| Дистрибутив | Команда | | Дистрибутив | Команда |
|---|---| |---|---|
| ALT Linux | `apt-get install python3-module-pygobject3 libgtk4 libadwaita` | | ALT Linux | `apt-get install python3-module-pygobject3 libgtk4 libadwaita python3-module-requests` |
| Fedora | `dnf install python3-gobject gtk4 libadwaita` | | Fedora | `dnf install python3-gobject gtk4 libadwaita python3-requests` |
| Arch | `pacman -S python-gobject gtk4 libadwaita` | | Arch | `pacman -S python-gobject gtk4 libadwaita python-requests` |
| Debian / Ubuntu | `apt install python3-gi gir1.2-gtk-4.0 gir1.2-adw-1` | | 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.
</details> </details>
## Быстрый старт ## Использование
1. Запустите `./vual`
2. Откройте настройки (☰ → Настройки)
3. На вкладке **Cheat Engine** скачайте и распакуйте CE
4. Установите **protonhax** если он ещё не установлен
5. Включите переключатель у нужной игры — Vual пропишет launch options
6. Нажмите ▶ у игры — после запуска CE откроется автоматически
## Структура данных
``` ```
~/.config/vual/ ./vual
└── config.json # Настройки приложения
~/.local/share/vual/
├── cheatengine/ # Установленный Cheat Engine
├── bin/protonhax # Управляемый скрипт protonhax
└── wine_prefix/ # Префикс Wine для тестового запуска CE
~/.cache/vual/
└── covers/ # Кэш обложек Steam
``` ```
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 приветствуются:
исправление ошибок, новые функции, переводы.
## Лицензия ## Лицензия
+76 -39
View File
@@ -1,36 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application"> <component type="desktop-application">
<id>io.github.vual</id> <id>io.github.vual</id>
<name>Vual</name> <name>Vual</name>
<summary>Launch Cheat Engine for Steam games via Proton</summary> <summary>Cheat Engine launcher for Steam + Proton</summary>
<summary xml:lang="ru">Запуск Cheat Engine для Steam-игр через Proton</summary> <summary xml:lang="ru">Запуск Cheat Engine для Steam-игр через Proton</summary>
<metadata_license>CC0-1.0</metadata_license> <metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0-or-later</project_license> <project_license>GPL-3.0-or-later</project_license>
<developer id="io.github.cheviiot">
<name>Cheviiot</name>
</developer>
<description> <description>
<p> <p>
Vual is a GTK4/libadwaita application that lets you browse your Steam Vual lets you run Cheat Engine inside any Steam game that uses Proton.
library, toggle protonhax launch options per game, and run Cheat Engine It reads your Steam library, manages protonhax launch options, downloads
inside the Proton context of a running game — all from a single window. and extracts Cheat Engine automatically, and launches CE in the correct
Wine prefix — all from one compact window.
</p> </p>
<p xml:lang="ru"> <p xml:lang="ru">
Vual — приложение на GTK4/libadwaita, которое показывает библиотеку Steam, Vual позволяет запускать Cheat Engine в любой Steam-игре, работающей
позволяет включать protonhax для каждой игры и запускать Cheat Engine через Proton. Приложение читает библиотеку Steam, управляет launch
внутри Proton-контекста запущенной игры — из одного окна. options для protonhax, автоматически скачивает и распаковывает Cheat
Engine и запускает CE в нужном Wine-префиксе — всё из одного окна.
</p> </p>
<p>Key features:</p>
<p xml:lang="ru">Основные возможности:</p>
<ul> <ul>
<li>Game grid with covers from Steam CDN, search, and sorting</li> <li>Game grid with Steam CDN covers, real-time search, and sorting</li>
<li>Per-game and batch protonhax launch option management</li> <li>One-click launch — game starts via Steam, CE attaches to its Proton prefix</li>
<li>One-click game launch with automatic Cheat Engine attach</li> <li>Bind .CT cheat tables to games so they load together with CE</li>
<li>Download and extract Cheat Engine via Proton Wine</li> <li>Per-game and bulk protonhax toggle with automatic launch option editing</li>
<li>Dark and light Wine theme for all Proton prefixes</li> <li>Download CE from cheatengine.org and extract it through Proton Wine</li>
<li>Application and Cheat Engine language switching (EN/RU)</li> <li>Apply dark or light Wine theme to all Proton prefixes at once</li>
<li xml:lang="ru">Сетка игр с обложками из Steam CDN, поиск и сортировка</li> <li>Install the official Russian CE translation from the CE repository</li>
<li xml:lang="ru">Управление launch options protonhax по играм и массово</li> <li>Built-in quick-start guide on first launch</li>
<li xml:lang="ru">Запуск игры с автоматическим подключением Cheat Engine</li> <li xml:lang="ru">Сетка игр с обложками из Steam CDN, поиск в реальном времени, сортировка</li>
<li xml:lang="ru">Скачивание и распаковка Cheat Engine через Wine из Proton</li> <li xml:lang="ru">Запуск одним кликом — игра стартует через Steam, CE подключается к её Proton-префиксу</li>
<li xml:lang="ru">Тёмная и светлая тема Wine для всех Proton-префиксов</li> <li xml:lang="ru">Привязка .CT-таблиц к играм — открываются вместе с CE</li>
<li xml:lang="ru">Переключение языка приложения и Cheat Engine (EN/RU)</li> <li xml:lang="ru">Переключатель protonhax для каждой игры и массово, автоматическая правка launch options</li>
<li xml:lang="ru">Скачивание CE с cheatengine.org и распаковка через Wine из Proton</li>
<li xml:lang="ru">Тёмная или светлая тема Wine для всех Proton-префиксов разом</li>
<li xml:lang="ru">Установка официальной русской локализации CE из репозитория CE</li>
<li xml:lang="ru">Встроенный гайд при первом запуске</li>
</ul> </ul>
</description> </description>
@@ -40,9 +55,18 @@
<url type="bugtracker">https://github.com/Cheviiot/vual/issues</url> <url type="bugtracker">https://github.com/Cheviiot/vual/issues</url>
<url type="vcs-browser">https://github.com/Cheviiot/vual</url> <url type="vcs-browser">https://github.com/Cheviiot/vual</url>
<provides> <branding>
<binary>vual</binary> <color type="primary" scheme_preference="light">#62a0ea</color>
</provides> <color type="primary" scheme_preference="dark">#1a5fb4</color>
</branding>
<screenshots>
<screenshot type="default">
<caption>Steam game library with covers</caption>
<caption xml:lang="ru">Библиотека Steam с обложками</caption>
<image>https://raw.githubusercontent.com/Cheviiot/vual/main/data/screenshots/main.png</image>
</screenshot>
</screenshots>
<requires> <requires>
<display_length compare="ge">360</display_length> <display_length compare="ge">360</display_length>
@@ -53,31 +77,44 @@
<control>keyboard</control> <control>keyboard</control>
</recommends> </recommends>
<branding> <provides>
<color type="primary" scheme_preference="light">#62a0ea</color> <binary>vual</binary>
<color type="primary" scheme_preference="dark">#1a5fb4</color> </provides>
</branding>
<screenshots>
<screenshot type="default">
<caption>Steam game library</caption>
<caption xml:lang="ru">Библиотека игр Steam</caption>
<image>https://raw.githubusercontent.com/Cheviiot/vual/main/data/screenshots/main.png</image>
</screenshot>
</screenshots>
<releases> <releases>
<release version="0.2.0" date="2026-03-19">
<description>
<p>CE table binding, Wine theme manager, built-in guide, window transparency, Adwyra-inspired theme, Russian CE locale installer.</p>
<p xml:lang="ru">Привязка CE-таблиц, менеджер тем Wine, встроенный гайд, прозрачность окна, тема Adwyra, установка русской локализации CE.</p>
</description>
</release>
<release version="0.1.0" date="2026-03-17"> <release version="0.1.0" date="2026-03-17">
<description> <description>
<p>First release.</p> <p>First release — game grid, protonhax management, CE download and launch.</p>
<p xml:lang="ru">Первый выпуск.</p> <p xml:lang="ru">Первый выпуск — сетка игр, управление protonhax, скачивание и запуск CE.</p>
</description> </description>
</release> </release>
</releases> </releases>
<content_rating type="oars-1.1" /> <content_rating type="oars-1.1" />
<developer id="io.github.cheviiot"> <keywords>
<name>Cheviiot</name> <keyword>cheatengine</keyword>
</developer> <keyword>cheat engine</keyword>
<keyword>steam</keyword>
<keyword>proton</keyword>
<keyword>wine</keyword>
<keyword>games</keyword>
<keyword xml:lang="ru">чит</keyword>
<keyword xml:lang="ru">игры</keyword>
</keywords>
<categories>
<category>Game</category>
<category>Utility</category>
</categories>
<custom>
<value key="GnomeSoftware::key-colors">[(98, 160, 234)]</value>
</custom>
</component> </component>
+123 -1
View File
@@ -4,7 +4,7 @@
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: vual 0.1.0\n" "Project-Id-Version: vual 0.2.0\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-19 00:00+0000\n" "POT-Creation-Date: 2026-03-19 00:00+0000\n"
"PO-Revision-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" msgid "Error applying to %d prefixes"
msgstr "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 # Steam page
msgid "Steam" msgid "Steam"
msgstr "Steam" msgstr "Steam"
@@ -226,6 +251,18 @@ msgstr "Launch game"
msgid "Launch Cheat Engine" msgid "Launch Cheat Engine"
msgstr "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..." msgid "Launching %s..."
msgstr "Launching %s..." msgstr "Launching %s..."
@@ -299,6 +336,91 @@ msgstr "Settings"
msgid "About" msgid "About"
msgstr "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 # CE page additional
msgid "All files" msgid "All files"
msgstr "All files" msgstr "All files"
+129 -1
View File
@@ -4,7 +4,7 @@
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: vual 0.1.0\n" "Project-Id-Version: vual 0.2.0\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-19 00:00+0000\n" "POT-Creation-Date: 2026-03-19 00:00+0000\n"
"PO-Revision-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" msgid "Restart app to apply language"
msgstr "Перезапустите приложение для смены языка" msgstr "Перезапустите приложение для смены языка"
msgid "Transparent window"
msgstr "Прозрачность окна"
msgid "Semi-transparent background"
msgstr "Полупрозрачный фон"
msgid "Grid" msgid "Grid"
msgstr "Сетка" msgstr "Сетка"
@@ -106,6 +112,31 @@ msgstr "Тема применена к %d префиксам"
msgid "Error applying to %d prefixes" msgid "Error applying to %d prefixes"
msgstr "Ошибка применения к %d префиксам" 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 # Steam page
msgid "Steam" msgid "Steam"
msgstr "Steam" msgstr "Steam"
@@ -226,6 +257,18 @@ msgstr "Запустить игру"
msgid "Launch Cheat Engine" msgid "Launch Cheat Engine"
msgstr "Запустить 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..." msgid "Launching %s..."
msgstr "Запуск %s…" msgstr "Запуск %s…"
@@ -299,6 +342,91 @@ msgstr "Настройки"
msgid "About" msgid "About"
msgstr "О программе" 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 # CE page additional
msgid "Actions" msgid "Actions"
msgstr "Действия" msgstr "Действия"
+38 -1
View File
@@ -6,7 +6,7 @@
#, fuzzy #, fuzzy
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: vual 0.1.0\n" "Project-Id-Version: vual 0.2.0\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-19 00:00+0000\n" "POT-Creation-Date: 2026-03-19 00:00+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
@@ -101,6 +101,31 @@ msgstr ""
msgid "Error applying to %d prefixes" msgid "Error applying to %d prefixes"
msgstr "" 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 # Steam page
msgid "Steam" msgid "Steam"
msgstr "" msgstr ""
@@ -224,6 +249,18 @@ msgstr ""
msgid "Launch Cheat Engine" msgid "Launch Cheat Engine"
msgstr "" msgstr ""
msgid "Assign table…"
msgstr ""
msgid "Remove table"
msgstr ""
msgid "Select Cheat Engine table"
msgstr ""
msgid "CE tables (*.CT)"
msgstr ""
msgid "Launching %s..." msgid "Launching %s..."
msgstr "" msgstr ""
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "vual" name = "vual"
version = "0.1.0" version = "0.2.0"
description = "Launch Cheat Engine for Steam games via Proton" description = "Launch Cheat Engine for Steam games via Proton"
readme = "README.md" readme = "README.md"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
+1 -1
View File
@@ -2,7 +2,7 @@
from pathlib import Path from pathlib import Path
__version__ = "0.1.0" __version__ = "0.2.0"
APP_ID = "io.github.vual" APP_ID = "io.github.vual"
APP_NAME = "Vual" APP_NAME = "Vual"
+1 -1
View File
@@ -7,7 +7,7 @@ import sys
def _ensure_gi_available(): def _ensure_gi_available():
"""Add system site-packages to path if gi is not importable (e.g., in venv).""" """Add system site-packages to path if gi is not importable (e.g., in venv)."""
try: try:
import gi # noqa: F401 __import__("gi")
return return
except ImportError: except ImportError:
pass pass
+14
View File
@@ -39,6 +39,10 @@ class VualApp(Adw.Application):
win.set_default_size(self.config.window_width, self.config.window_height) win.set_default_size(self.config.window_width, self.config.window_height)
win.present() win.present()
# Show guide on first run
if not self.config.guide_shown:
win.show_guide()
# ── Startup: register actions ──────────────────────────────── # ── Startup: register actions ────────────────────────────────
def do_startup(self) -> None: def do_startup(self) -> None:
@@ -67,6 +71,10 @@ class VualApp(Adw.Application):
prefs_action.connect("activate", self._on_preferences) prefs_action.connect("activate", self._on_preferences)
self.add_action(prefs_action) 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 = Gio.SimpleAction.new("quit", None)
quit_action.connect("activate", lambda *_: self.quit()) quit_action.connect("activate", lambda *_: self.quit())
self.add_action(quit_action) 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 = PreferencesWindow(config=self.config, transient_for=self.props.active_window)
win.present() win.present()
# ── Guide ────────────────────────────────────────────────────
def _on_guide(self, _action: Gio.SimpleAction, _param: None) -> None:
if self.props.active_window:
self.props.active_window.show_guide()
+8
View File
@@ -37,6 +37,8 @@ class Config:
ce_language: CE language ("system", "ru_RU"). ce_language: CE language ("system", "ru_RU").
wine_theme: Wine color theme ("system", "dark", "light"). wine_theme: Wine color theme ("system", "dark", "light").
app_language: Application UI language ("system", "en", "ru"). 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" ce_executable: str = "~/.local/share/vual/cheatengine/cheatengine-x86_64.exe"
@@ -52,6 +54,8 @@ class Config:
ce_language: str = "system" ce_language: str = "system"
wine_theme: str = "system" wine_theme: str = "system"
app_language: str = "system" app_language: str = "system"
transparent_window: bool = False
guide_shown: bool = False
# ════════════════════════════════════════════════════════════════ # ════════════════════════════════════════════════════════════════
# Load / Save # Load / Save
@@ -81,6 +85,8 @@ class Config:
ce_language=data.get("ce_language", cls.ce_language), ce_language=data.get("ce_language", cls.ce_language),
wine_theme=data.get("wine_theme", cls.wine_theme), wine_theme=data.get("wine_theme", cls.wine_theme),
app_language=data.get("app_language", cls.app_language), 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): except (json.JSONDecodeError, OSError):
pass pass
@@ -107,6 +113,8 @@ class Config:
"ce_language": self.ce_language, "ce_language": self.ce_language,
"wine_theme": self.wine_theme, "wine_theme": self.wine_theme,
"app_language": self.app_language, "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") CONFIG_PATH.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
return CONFIG_PATH return CONFIG_PATH
+260 -70
View File
@@ -1,52 +1,69 @@
/* ═══════════════════════════════════════════════════════════════════════════ /* ═══════════════════════════════════════════════════════════════════════════
Vual — Unified Size System Vual — Adwyra-inspired theme
Minimalist, clean design with soft shadows and accent highlights
Base unit: 4px
All sizes are multiples of 4px for consistency
═══════════════════════════════════════════════════════════════════════════ */ ═══════════════════════════════════════════════════════════════════════════ */
/* ── Transparent Window ────────────────────────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════ window.transparent-window {
HEADER BAR background-color: alpha(@window_bg_color, 0.82);
═══════════════════════════════════════════════════════════════════════════ */ }
/* ── Header Bar ───────────────────────────────────────────────────────────── */
/* Header buttons: 32x32 (8 units) */
.header-btn { .header-btn {
min-width: 32px; min-width: 32px;
min-height: 32px; min-height: 32px;
padding: 0; padding: 0;
margin: 0; margin: 0;
border-radius: 6px; border-radius: 10px;
transition: background-color 150ms ease-out, box-shadow 150ms ease-out;
} }
.header-btn:hover { .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 { .header-btn:active {
background: alpha(@accent_bg_color, 0.2); background-color: alpha(@accent_color, 0.16);
box-shadow: none;
} }
/* Games counter */
.games-counter { .games-counter {
font-size: 0.85em; font-size: 0.84em;
font-weight: 500; font-weight: 450;
opacity: 0.6; opacity: 0.65;
margin-left: 8px; margin-left: 8px;
} }
/* Search entry */ /* ── Search Entry ─────────────────────────────────────────────────────────── */
searchentry { searchentry {
min-width: 220px; 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;
} }
/* ═══════════════════════════════════════════════════════════════════════════ searchentry:focus {
GRID LAYOUT background-color: @view_bg_color;
═══════════════════════════════════════════════════════════════════════════ */ border-color: alpha(@accent_color, 0.35);
box-shadow: 0 0 0 3px alpha(@accent_color, 0.15);
.vual-grid {
/* Padding handled in Python via set_margin_* */
} }
searchentry image {
opacity: 0.5;
}
searchentry:focus image {
opacity: 0.75;
}
/* ── Grid Layout ──────────────────────────────────────────────────────────── */
flowboxchild { flowboxchild {
padding: 0; padding: 0;
margin: 0; margin: 0;
@@ -64,49 +81,47 @@ flowboxchild:selected {
box-shadow: none; box-shadow: none;
} }
/* ═══════════════════════════════════════════════════════════════════════════ /* ── Tile ─────────────────────────────────────────────────────────────────── */
TILE
═══════════════════════════════════════════════════════════════════════════ */
/* Tile container: 12px radius */
.vual-tile { .vual-tile {
border-radius: 12px; border-radius: 14px;
background: @card_bg_color; 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 { .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 ────────────────────────────────────────────────────── */
TILE INFO OVERLAY (bottom)
═══════════════════════════════════════════════════════════════════════════ */
/* Info panel: gradient background */
.vual-tile-info { .vual-tile-info {
padding: 24px 8px 8px 8px; padding: 20px 8px 8px 8px;
background: linear-gradient(to bottom, background: linear-gradient(to bottom,
transparent 0%, transparent 0%,
alpha(black, 0.6) 50%, alpha(black, 0.55) 50%,
alpha(black, 0.8) 100%); alpha(black, 0.75) 100%);
border-radius: 0 0 12px 12px; border-radius: 0 0 14px 14px;
} }
.vual-tile-info label { .vual-tile-info label {
color: white; color: white;
transition: opacity 150ms ease-out;
} }
.vual-tile-info .heading { .vual-tile-info .heading {
font-weight: 600; font-weight: 500;
font-size: 0.85em; font-size: 0.84em;
opacity: 0.9;
} }
/* ═══════════════════════════════════════════════════════════════════════════ .vual-tile:hover .vual-tile-info .heading {
TILE BUTTONS (play, CE) opacity: 1;
═══════════════════════════════════════════════════════════════════════════ */ }
/* ── Tile Buttons ─────────────────────────────────────────────────────────── */
/* Tile buttons: 28x28 (7 units) */
.tile-btn, .tile-btn,
.vual-tile-info button { .vual-tile-info button {
min-width: 28px; min-width: 28px;
@@ -114,28 +129,47 @@ flowboxchild:selected {
padding: 0; padding: 0;
margin: 0; margin: 0;
color: white; color: white;
background: alpha(white, 0.12); background: alpha(white, 0.08);
border-radius: 6px; border-radius: 8px;
border: none; border: none;
transition: background-color 150ms ease-out, box-shadow 150ms ease-out;
} }
.tile-btn:hover, .tile-btn:hover,
.vual-tile-info button: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, .tile-btn:active,
.vual-tile-info button:active { .vual-tile-info button:active {
background: alpha(white, 0.32); background: alpha(white, 0.26);
box-shadow: none;
} }
/* ═══════════════════════════════════════════════════════════════════════════ /* Popover menus — compact */
TILE SWITCH 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 { .vual-tile-info switch {
background: alpha(white, 0.15); background: alpha(white, 0.12);
border: none; border: none;
transition: background-color 150ms ease-out, box-shadow 150ms ease-out;
} }
.vual-tile-info switch:checked { .vual-tile-info switch:checked {
@@ -146,50 +180,206 @@ flowboxchild:selected {
background: white; background: white;
min-width: 16px; min-width: 16px;
min-height: 16px; min-height: 16px;
border-radius: 8px; border-radius: 50%;
box-shadow: 0 1px 3px alpha(black, 0.18);
} }
/* ═══════════════════════════════════════════════════════════════════════════ /* ── Reload Button ────────────────────────────────────────────────────────── */
RELOAD BUTTON (top-left corner)
═══════════════════════════════════════════════════════════════════════════ */
/* Reload: 24x24 (6 units) */
.vual-reload-btn { .vual-reload-btn {
min-width: 24px; min-width: 24px;
min-height: 24px; min-height: 24px;
padding: 0; padding: 0;
margin: 0; margin: 0;
border-radius: 6px; border-radius: 50%;
background: alpha(black, 0.5); background: alpha(black, 0.35);
color: white; color: white;
opacity: 0; opacity: 0;
border: none; border: none;
transition: opacity 200ms ease-out, background-color 200ms ease-out;
} }
.vual-tile:hover .vual-reload-btn { .vual-tile:hover .vual-reload-btn {
opacity: 1; opacity: 0.7;
} }
.vual-reload-btn:hover { .vual-reload-btn:hover {
background: @accent_bg_color; opacity: 1;
background: alpha(@accent_color, 0.75);
} }
/* ═══════════════════════════════════════════════════════════════════════════ /* ── Running Badge ────────────────────────────────────────────────────────── */
RUNNING BADGE (top-right corner)
═══════════════════════════════════════════════════════════════════════════ */
/* Badge: 20px total (14px icon + 3px padding each side) */
.vual-badge { .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%; 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 ─────────────────────────────────────────────────────────────── */
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-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;
} }
+1 -19
View File
@@ -23,7 +23,6 @@ _LOCALE_DIRS = [
# Global translator # Global translator
_translator: gettext.GNUTranslations | gettext.NullTranslations | None = None _translator: gettext.GNUTranslations | gettext.NullTranslations | None = None
_current_lang: str = "system"
def _find_locale_dir() -> Path | None: def _find_locale_dir() -> Path | None:
@@ -47,8 +46,7 @@ def init(lang: str = "system") -> None:
lang: Language code ("system", "en", "ru"). lang: Language code ("system", "en", "ru").
"system" uses system locale. "system" uses system locale.
""" """
global _translator, _current_lang global _translator
_current_lang = lang
# Get system locale # Get system locale
try: try:
@@ -78,23 +76,8 @@ def init(lang: str = "system") -> None:
_translator = gettext.NullTranslations() _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: def _(message: str) -> str:
"""Translate a message string.""" """Translate a message string."""
global _translator
if _translator is None: if _translator is None:
init() init()
return _translator.gettext(message) if _translator else message 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: def ngettext(singular: str, plural: str, n: int) -> str:
"""Translate a plural message.""" """Translate a plural message."""
global _translator
if _translator is None: if _translator is None:
init() init()
return _translator.ngettext(singular, plural, n) if _translator else (singular if n == 1 else plural) return _translator.ngettext(singular, plural, n) if _translator else (singular if n == 1 else plural)
Binary file not shown.
Binary file not shown.
+76
View File
@@ -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)
+102 -25
View File
@@ -19,11 +19,11 @@ gi.require_version("Gdk", "4.0")
gi.require_version("GdkPixbuf", "2.0") gi.require_version("GdkPixbuf", "2.0")
gi.require_version("Pango", "1.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 import requests
from vual import protonhax, steam from vual import protonhax, steam, tables
from vual.config import TILE_SIZES, Config from vual.config import TILE_SIZES, Config
from vual.i18n import _ from vual.i18n import _
@@ -217,14 +217,39 @@ class GameTile(Gtk.Overlay):
btn_play.connect("clicked", self._on_play_clicked) btn_play.connect("clicked", self._on_play_clicked)
row.append(btn_play) row.append(btn_play)
# CE button # CE button with table menu
btn_ce = Gtk.Button() self._btn_ce = Gtk.MenuButton()
btn_ce.set_icon_name("utilities-terminal-symbolic") self._btn_ce.set_icon_name("open-menu-symbolic")
btn_ce.add_css_class("flat") self._btn_ce.add_css_class("flat")
btn_ce.add_css_class("tile-btn") self._btn_ce.add_css_class("tile-btn")
btn_ce.set_tooltip_text(_("Launch Cheat Engine")) self._update_ce_tooltip()
btn_ce.connect("clicked", self._on_ce_clicked)
row.append(btn_ce) 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
spacer = Gtk.Box() spacer = Gtk.Box()
@@ -257,8 +282,7 @@ class GameTile(Gtk.Overlay):
def _build_running_badge(self) -> None: def _build_running_badge(self) -> None:
"""Create running badge in top-right corner.""" """Create running badge in top-right corner."""
self._badge = Gtk.Image.new_from_icon_name("media-playback-start-symbolic") self._badge = Gtk.Box()
self._badge.set_pixel_size(14)
self._badge.add_css_class("vual-badge") self._badge.add_css_class("vual-badge")
self._badge.set_valign(Gtk.Align.START) self._badge.set_valign(Gtk.Align.START)
self._badge.set_halign(Gtk.Align.END) self._badge.set_halign(Gtk.Align.END)
@@ -322,12 +346,35 @@ class GameTile(Gtk.Overlay):
if hasattr(self, "_on_launch"): if hasattr(self, "_on_launch"):
self._on_launch(self._app_id, self._name) 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.""" """Emit CE signal."""
if hasattr(self, "_on_launch_ce"): if hasattr(self, "_on_launch_ce"):
self._on_launch_ce(self._app_id, self._name) 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.""" """Handle activation toggle."""
self._is_active = state self._is_active = state
if hasattr(self, "_on_toggle"): if hasattr(self, "_on_toggle"):
@@ -353,6 +400,9 @@ class GameTile(Gtk.Overlay):
def connect_reload(self, callback) -> None: def connect_reload(self, callback) -> None:
self._on_reload = callback self._on_reload = callback
def connect_pick_table(self, callback) -> None:
self._on_pick_table = callback
# ═══════════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════════
# MainPage — the main games grid # MainPage — the main games grid
@@ -473,11 +523,6 @@ class MainPage(Adw.Bin):
for app_id, tile in self._tiles.items(): for app_id, tile in self._tiles.items():
tile.set_running(app_id in running_ids) tile.set_running(app_id in running_ids)
@property
def games_count(self) -> int:
"""Number of loaded games."""
return len(self._tiles)
# ─── Filter & Sort ──────────────────────────────────────────────────────── # ─── Filter & Sort ────────────────────────────────────────────────────────
def _filter_func(self, child: Gtk.FlowBoxChild) -> bool: 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_launch_ce(self._on_launch_ce)
tile.connect_toggle(self._on_toggle) tile.connect_toggle(self._on_toggle)
tile.connect_reload(self._on_reload_cover) tile.connect_reload(self._on_reload_cover)
tile.connect_pick_table(self._on_pick_table)
self._tiles[app_id] = tile self._tiles[app_id] = tile
self._grid.append(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) self._win.show_toast(_("CE not found: %s") % self._cfg.ce_executable)
return return
args = None args: list[str] = []
if self._cfg.ce_language != "system": 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: if proc:
self._win.show_toast(_("CE launched for %s") % name) self._win.show_toast(_("CE launched for %s") % name)
else: else:
self._win.show_toast(_("Failed to launch CE for %s") % name) 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: def _on_toggle(self, app_id: str, active: bool) -> None:
"""Handle activation toggle.""" """Handle activation toggle."""
tile = self._tiles.get(app_id) tile = self._tiles.get(app_id)
@@ -811,6 +891,3 @@ class MainPage(Adw.Bin):
if restart: if restart:
steam.start_steam() steam.start_steam()
def schedule_auto_launch(self, app_id: str) -> None:
"""Schedule auto CE launch for a game."""
self._auto_ce = app_id
+16
View File
@@ -29,6 +29,7 @@ class PreferencesWindow(Adw.PreferencesWindow):
**kwargs, **kwargs,
) )
self._config = config self._config = config
self._parent = kwargs.get("transient_for")
self._exclusion_rows: list[Adw.EntryRow] = [] self._exclusion_rows: list[Adw.EntryRow] = []
self._release_info: dict | None = None self._release_info: dict | None = None
self._building = True # Prevent saves during initial build 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) self._lang_app_row.connect("notify::selected", self._on_app_lang_changed)
theme_group.add(self._lang_app_row) 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
grid_group = Adw.PreferencesGroup( grid_group = Adw.PreferencesGroup(
title=_("Grid"), title=_("Grid"),
@@ -179,6 +189,12 @@ class PreferencesWindow(Adw.PreferencesWindow):
# Show restart hint # Show restart hint
self._show_toast(_("Restart app to apply language")) 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: def _apply_theme(self) -> None:
style = Adw.StyleManager.get_default() style = Adw.StyleManager.get_default()
schemes = { schemes = {
+207 -5
View File
@@ -31,12 +31,23 @@ class VualWindow(Adw.ApplicationWindow):
self.toast_overlay = Adw.ToastOverlay() self.toast_overlay = Adw.ToastOverlay()
self.set_content(self.toast_overlay) self.set_content(self.toast_overlay)
toolbar_view = Adw.ToolbarView() # Navigation view for guide/main switching
self.toast_overlay.set_child(toolbar_view) 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 from vual.ui.main_page import MainPage
toolbar_view = Adw.ToolbarView()
self.main_page = MainPage(config=self.config, window=self) self.main_page = MainPage(config=self.config, window=self)
# Header bar # Header bar
@@ -102,7 +113,9 @@ class VualWindow(Adw.ApplicationWindow):
toolbar_view.set_content(self.main_page) 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() sort_by = param.get_string()
self.config.sort_by = sort_by self.config.sort_by = sort_by
self.main_page._update_sort() self.main_page._update_sort()
@@ -117,7 +130,7 @@ class VualWindow(Adw.ApplicationWindow):
menu = Gio.Menu() menu = Gio.Menu()
menu.append(_("Preferences"), "app.preferences") menu.append(_("Preferences"), "app.preferences")
menu.append(_("About"), "app.about") menu.append(_("About"), "app.about")
menu.append(_("Quit"), "app.quit") menu.append(_("Guide"), "app.guide")
return menu return menu
# ── Toast helper ───────────────────────────────────────────── # ── Toast helper ─────────────────────────────────────────────
@@ -126,3 +139,192 @@ class VualWindow(Adw.ApplicationWindow):
toast = Adw.Toast(title=message, timeout=timeout) toast = Adw.Toast(title=message, timeout=timeout)
self.toast_overlay.add_toast(toast) self.toast_overlay.add_toast(toast)
return False 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)
-37
View File
@@ -367,40 +367,3 @@ def apply_theme_to_all(steamapps: Path, theme: str) -> tuple[int, int]:
return (success, failed) 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