diff --git a/README.md b/README.md index cef39c4..62460a0 100644 --- a/README.md +++ b/README.md @@ -1,98 +1,137 @@

- Vual + Vual

Vual

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

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

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

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

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

+ +

Key features:

+

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

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

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

+

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

+
+
-

First release.

-

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

+

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

+

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

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