mirror of
https://github.com/Cheviiot/Vual.git
synced 2026-08-03 15:57:25 +00:00
v0.2.0
This commit is contained in:
@@ -1,98 +1,137 @@
|
||||
<p align="center">
|
||||
<img src="data/Vual.png" alt="Vual" width="96">
|
||||
<img src="data/Vual.png" alt="Vual" width="128">
|
||||
</p>
|
||||
|
||||
<h1 align="center">Vual</h1>
|
||||
|
||||
<p align="center">
|
||||
GTK4-приложение для запуска Cheat Engine в Steam-играх через Proton
|
||||
Запуск Cheat Engine для Steam-игр через Proton
|
||||
</p>
|
||||
|
||||
<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/python-3.11+-blue?style=flat-square" alt="Python 3.11+">
|
||||
<img src="https://img.shields.io/badge/gtk-4-4a86cf?style=flat-square" alt="GTK 4">
|
||||
<img src="https://img.shields.io/badge/libadwaita-1-3584e4?style=flat-square" alt="Libadwaita 1">
|
||||
<img src="https://img.shields.io/badge/GTK-4-4a86cf?style=flat-square" alt="GTK 4">
|
||||
<img src="https://img.shields.io/badge/Libadwaita-1-3584e4?style=flat-square" alt="Libadwaita">
|
||||
<img src="https://img.shields.io/badge/Python-3.11+-3776ab?style=flat-square" alt="Python 3.11+">
|
||||
<a href="LICENSE"><img src="https://img.shields.io/badge/License-GPL--3.0-green?style=flat-square" alt="GPL-3.0"></a>
|
||||
</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 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
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary><strong>Зависимости</strong></summary>
|
||||
|
||||
| Дистрибутив | Команда |
|
||||
|---|---|
|
||||
| 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.
|
||||
|
||||
</details>
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
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 приветствуются:
|
||||
исправление ошибок, новые функции, переводы.
|
||||
|
||||
## Лицензия
|
||||
|
||||
|
||||
@@ -1,36 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<component type="desktop-application">
|
||||
<id>io.github.vual</id>
|
||||
|
||||
<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>
|
||||
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<project_license>GPL-3.0-or-later</project_license>
|
||||
|
||||
<developer id="io.github.cheviiot">
|
||||
<name>Cheviiot</name>
|
||||
</developer>
|
||||
|
||||
<description>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p xml:lang="ru">
|
||||
Vual — приложение на GTK4/libadwaita, которое показывает библиотеку Steam,
|
||||
позволяет включать protonhax для каждой игры и запускать Cheat Engine
|
||||
внутри Proton-контекста запущенной игры — из одного окна.
|
||||
Vual позволяет запускать Cheat Engine в любой Steam-игре, работающей
|
||||
через Proton. Приложение читает библиотеку Steam, управляет launch
|
||||
options для protonhax, автоматически скачивает и распаковывает Cheat
|
||||
Engine и запускает CE в нужном Wine-префиксе — всё из одного окна.
|
||||
</p>
|
||||
|
||||
<p>Key features:</p>
|
||||
<p xml:lang="ru">Основные возможности:</p>
|
||||
<ul>
|
||||
<li>Game grid with covers from Steam CDN, search, and sorting</li>
|
||||
<li>Per-game and batch protonhax launch option management</li>
|
||||
<li>One-click game launch with automatic Cheat Engine attach</li>
|
||||
<li>Download and extract Cheat Engine via Proton Wine</li>
|
||||
<li>Dark and light Wine theme for all Proton prefixes</li>
|
||||
<li>Application and Cheat Engine language switching (EN/RU)</li>
|
||||
<li xml:lang="ru">Сетка игр с обложками из Steam CDN, поиск и сортировка</li>
|
||||
<li xml:lang="ru">Управление launch options protonhax по играм и массово</li>
|
||||
<li xml:lang="ru">Запуск игры с автоматическим подключением Cheat Engine</li>
|
||||
<li xml:lang="ru">Скачивание и распаковка Cheat Engine через Wine из Proton</li>
|
||||
<li xml:lang="ru">Тёмная и светлая тема Wine для всех Proton-префиксов</li>
|
||||
<li xml:lang="ru">Переключение языка приложения и Cheat Engine (EN/RU)</li>
|
||||
<li>Game grid with Steam CDN covers, real-time search, and sorting</li>
|
||||
<li>One-click launch — game starts via Steam, CE attaches to its Proton prefix</li>
|
||||
<li>Bind .CT cheat tables to games so they load together with CE</li>
|
||||
<li>Per-game and bulk protonhax toggle with automatic launch option editing</li>
|
||||
<li>Download CE from cheatengine.org and extract it through Proton Wine</li>
|
||||
<li>Apply dark or light Wine theme to all Proton prefixes at once</li>
|
||||
<li>Install the official Russian CE translation from the CE repository</li>
|
||||
<li>Built-in quick-start guide on first launch</li>
|
||||
<li xml:lang="ru">Сетка игр с обложками из Steam CDN, поиск в реальном времени, сортировка</li>
|
||||
<li xml:lang="ru">Запуск одним кликом — игра стартует через Steam, CE подключается к её Proton-префиксу</li>
|
||||
<li xml:lang="ru">Привязка .CT-таблиц к играм — открываются вместе с CE</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>
|
||||
</description>
|
||||
|
||||
@@ -40,9 +55,18 @@
|
||||
<url type="bugtracker">https://github.com/Cheviiot/vual/issues</url>
|
||||
<url type="vcs-browser">https://github.com/Cheviiot/vual</url>
|
||||
|
||||
<provides>
|
||||
<binary>vual</binary>
|
||||
</provides>
|
||||
<branding>
|
||||
<color type="primary" scheme_preference="light">#62a0ea</color>
|
||||
<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>
|
||||
<display_length compare="ge">360</display_length>
|
||||
@@ -53,31 +77,44 @@
|
||||
<control>keyboard</control>
|
||||
</recommends>
|
||||
|
||||
<branding>
|
||||
<color type="primary" scheme_preference="light">#62a0ea</color>
|
||||
<color type="primary" scheme_preference="dark">#1a5fb4</color>
|
||||
</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>
|
||||
<provides>
|
||||
<binary>vual</binary>
|
||||
</provides>
|
||||
|
||||
<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">
|
||||
<description>
|
||||
<p>First release.</p>
|
||||
<p xml:lang="ru">Первый выпуск.</p>
|
||||
<p>First release — game grid, protonhax management, CE download and launch.</p>
|
||||
<p xml:lang="ru">Первый выпуск — сетка игр, управление protonhax, скачивание и запуск CE.</p>
|
||||
</description>
|
||||
</release>
|
||||
</releases>
|
||||
|
||||
<content_rating type="oars-1.1" />
|
||||
|
||||
<developer id="io.github.cheviiot">
|
||||
<name>Cheviiot</name>
|
||||
</developer>
|
||||
<keywords>
|
||||
<keyword>cheatengine</keyword>
|
||||
<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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 "Действия"
|
||||
|
||||
+38
-1
@@ -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 ""
|
||||
|
||||
|
||||
+1
-1
@@ -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"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.1.0"
|
||||
__version__ = "0.2.0"
|
||||
APP_ID = "io.github.vual"
|
||||
APP_NAME = "Vual"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
+260
-70
@@ -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;
|
||||
}
|
||||
|
||||
+1
-19
@@ -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)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -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
@@ -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
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
+207
-5
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user