feat: TurboSu initial release - racing telemetry dashboard

This commit is contained in:
Yei.J. (AskaEth)
2026-07-25 15:23:40 +08:00
commit d61bb61a83
61 changed files with 5270 additions and 0 deletions
View File
+170
View File
@@ -0,0 +1,170 @@
from __future__ import annotations
import json
import os
import shutil
import uuid
from dataclasses import dataclass, field, asdict
from datetime import datetime
from pathlib import Path
from typing import Any
from utils.logger import get_logger
logger = get_logger(__name__)
DATA_DIR = Path(__file__).resolve().parent.parent / "data"
DASHBOARDS_DIR = DATA_DIR / "dashboards"
BUILTIN_DASHBOARDS_DIR = Path(__file__).resolve().parent.parent / "dashboards"
@dataclass
class DashboardTheme:
id: str = field(default_factory=lambda: str(uuid.uuid4()))
name: str = ""
category: str = "basic"
description: str = ""
author: str = ""
version: str = "1.0.0"
preview: str = ""
config: dict[str, Any] = field(default_factory=dict)
created_at: str = field(default_factory=lambda: datetime.now().isoformat())
updated_at: str = field(default_factory=lambda: datetime.now().isoformat())
is_builtin: bool = False
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@classmethod
def from_dict(cls, d: dict[str, Any]) -> DashboardTheme:
return cls(**{k: v for k, v in d.items() if k in cls.__dataclass_fields__})
class DashboardManager:
def __init__(self):
DASHBOARDS_DIR.mkdir(parents=True, exist_ok=True)
self._cache: dict[str, DashboardTheme] = {}
self._load_builtins()
def _load_builtins(self):
if not BUILTIN_DASHBOARDS_DIR.exists():
return
for item in BUILTIN_DASHBOARDS_DIR.iterdir():
if item.is_dir():
cfg_file = item / "config.json"
if cfg_file.exists():
try:
with open(cfg_file, "r", encoding="utf-8") as f:
data = json.load(f)
theme = DashboardTheme.from_dict(data)
theme.is_builtin = True
self._cache[theme.id] = theme
except Exception as e:
logger.error("Failed to load builtin dashboard %s: %s", item.name, e)
def _load_user_dashboards(self):
for f in DASHBOARDS_DIR.glob("*.json"):
try:
with open(f, "r", encoding="utf-8") as fp:
data = json.load(fp)
theme = DashboardTheme.from_dict(data)
self._cache[theme.id] = theme
except Exception as e:
logger.error("Failed to load dashboard %s: %s", f.name, e)
def list_all(self, category: str | None = None) -> list[dict[str, Any]]:
self._cache.clear()
self._load_builtins()
self._load_user_dashboards()
result = [t.to_dict() for t in self._cache.values()]
if category and category != "all":
result = [r for r in result if r.get("category") == category]
return result
def get(self, theme_id: str) -> dict[str, Any] | None:
self._cache.clear()
self._load_builtins()
self._load_user_dashboards()
theme = self._cache.get(theme_id)
return theme.to_dict() if theme else None
def save(self, theme: DashboardTheme) -> bool:
theme.updated_at = datetime.now().isoformat()
theme.is_builtin = False
filepath = DASHBOARDS_DIR / f"{theme.id}.json"
try:
with open(filepath, "w", encoding="utf-8") as f:
json.dump(theme.to_dict(), f, indent=2, ensure_ascii=False)
logger.info("Dashboard saved: %s", theme.id)
return True
except Exception as e:
logger.error("Failed to save dashboard %s: %s", theme.id, e)
return False
def delete(self, theme_id: str) -> bool:
self._cache.clear()
self._load_builtins()
self._load_user_dashboards()
theme = self._cache.get(theme_id)
if theme and theme.is_builtin:
logger.warning("Cannot delete builtin dashboard: %s", theme_id)
return False
filepath = DASHBOARDS_DIR / f"{theme_id}.json"
if filepath.exists():
filepath.unlink()
logger.info("Dashboard deleted: %s", theme_id)
return True
def export_theme(self, theme_id: str) -> dict[str, Any] | None:
theme = self.get(theme_id)
if not theme:
return None
result = {
"type": "dashboard_theme",
"version": "1.0",
"data": theme,
"html": self._read_template_file(theme["id"]),
}
return result
def import_theme(self, data: dict[str, Any]) -> bool:
if data.get("type") != "dashboard_theme":
return False
theme_data = data.get("data", {})
theme = DashboardTheme.from_dict(theme_data)
html_content = data.get("html", "")
if theme.id in self._cache:
theme.id = str(uuid.uuid4())
success = self.save(theme)
if success and html_content:
self._save_template_file(theme.id, html_content)
return success
def _read_template_file(self, theme_id: str) -> str:
for base in [DASHBOARDS_DIR, BUILTIN_DASHBOARDS_DIR]:
tmpl = base / theme_id / "template.html"
if tmpl.exists():
return tmpl.read_text(encoding="utf-8")
return ""
def _save_template_file(self, theme_id: str, content: str):
theme_dir = DASHBOARDS_DIR / theme_id
theme_dir.mkdir(parents=True, exist_ok=True)
tmpl = theme_dir / "template.html"
tmpl.write_text(content, encoding="utf-8")
def get_template(self, theme_id: str) -> str:
return self._read_template_file(theme_id)
def get_categories(self) -> list[str]:
self._cache.clear()
self._load_builtins()
self._load_user_dashboards()
cats = set()
for t in self._cache.values():
if t.category:
cats.add(t.category)
return sorted(cats)
dashboard_manager = DashboardManager()
+147
View File
@@ -0,0 +1,147 @@
from __future__ import annotations
import json
import uuid
from dataclasses import dataclass, field, asdict
from datetime import datetime
from pathlib import Path
from typing import Any
from utils.logger import get_logger
logger = get_logger(__name__)
DATA_DIR = Path(__file__).resolve().parent.parent / "data"
SCENES_DIR = DATA_DIR / "scenes"
@dataclass
class DashboardPlacement:
dashboard_id: str = ""
x: float = 0.0
y: float = 0.0
width: int = 400
height: int = 300
z_index: int = 0
scale_x: float = 1.0
scale_y: float = 1.0
aspect_ratio: str = "auto"
render_mode: str = "contain"
custom_config: dict[str, Any] = field(default_factory=dict)
@dataclass
class SceneCanvas:
width: int = 1920
height: int = 1080
label: str = "16:9"
placements: list[DashboardPlacement] = field(default_factory=list)
@dataclass
class Scene:
id: str = field(default_factory=lambda: str(uuid.uuid4()))
name: str = "New Scene"
game_id: str = ""
description: str = ""
canvases: list[SceneCanvas] = field(default_factory=list)
created_at: str = field(default_factory=lambda: datetime.now().isoformat())
updated_at: str = field(default_factory=lambda: datetime.now().isoformat())
def to_dict(self) -> dict[str, Any]:
d = asdict(self)
d["canvases"] = [asdict(c) for c in self.canvases]
return d
@classmethod
def from_dict(cls, d: dict[str, Any]) -> Scene:
canvases = []
for cd in d.get("canvases", []):
placements = [DashboardPlacement(**p) for p in cd.get("placements", [])]
canvases.append(SceneCanvas(
width=cd.get("width", 1920),
height=cd.get("height", 1080),
label=cd.get("label", ""),
placements=placements,
))
return cls(
id=d.get("id", str(uuid.uuid4())),
name=d.get("name", "New Scene"),
game_id=d.get("game_id", ""),
description=d.get("description", ""),
canvases=canvases,
created_at=d.get("created_at", datetime.now().isoformat()),
updated_at=d.get("updated_at", datetime.now().isoformat()),
)
class SceneManager:
def __init__(self):
SCENES_DIR.mkdir(parents=True, exist_ok=True)
def list_all(self, game_id: str | None = None) -> list[dict[str, Any]]:
scenes = []
for f in SCENES_DIR.glob("*.json"):
try:
with open(f, "r", encoding="utf-8") as fp:
data = json.load(fp)
scene = Scene.from_dict(data)
if game_id is None or scene.game_id == game_id:
scenes.append(scene.to_dict())
except Exception as e:
logger.error("Failed to load scene %s: %s", f.name, e)
return scenes
def get(self, scene_id: str) -> dict[str, Any] | None:
filepath = SCENES_DIR / f"{scene_id}.json"
if not filepath.exists():
return None
try:
with open(filepath, "r", encoding="utf-8") as f:
data = json.load(f)
return Scene.from_dict(data).to_dict()
except Exception as e:
logger.error("Failed to read scene %s: %s", scene_id, e)
return None
def save(self, scene: Scene) -> bool:
scene.updated_at = datetime.now().isoformat()
filepath = SCENES_DIR / f"{scene.id}.json"
try:
with open(filepath, "w", encoding="utf-8") as f:
json.dump(scene.to_dict(), f, indent=2, ensure_ascii=False)
logger.info("Scene saved: %s", scene.id)
return True
except Exception as e:
logger.error("Failed to save scene %s: %s", scene.id, e)
return False
def delete(self, scene_id: str) -> bool:
filepath = SCENES_DIR / f"{scene_id}.json"
if filepath.exists():
filepath.unlink()
logger.info("Scene deleted: %s", scene_id)
return True
return False
def export_scene(self, scene_id: str) -> dict[str, Any] | None:
scene = self.get(scene_id)
if not scene:
return None
return {
"type": "scene",
"version": "1.0",
"data": scene,
}
def import_scene(self, data: dict[str, Any]) -> bool:
if data.get("type") != "scene":
return False
scene_data = data.get("data", {})
if "id" not in scene_data:
scene_data["id"] = str(uuid.uuid4())
scene = Scene.from_dict(scene_data)
return self.save(scene)
scene_manager = SceneManager()