mirror of
https://github.com/Cheviiot/Vual.git
synced 2026-08-04 00:07:25 +00:00
v0.2.0
This commit is contained in:
@@ -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