Compare commits

..

21 Commits

Author SHA1 Message Date
1233851b87 Fixed missing * in CSS comments. 2026-03-14 03:44:45 -04:00
b6d7060e79 Added descriptive comments to the script file. 2026-03-14 03:44:20 -04:00
90dcc10956 Update tab content corner radius consistency 2026-03-12 12:26:52 -04:00
29f9371c1e Add skeuomorphic modern style support 2026-03-12 12:18:04 -04:00
edc6d84ede Add Synthwave, Vaporwave, Terminal, and High Contrast styles. 2026-03-12 04:16:26 -04:00
fc3e81c2d6 Add Apple-inspired Flat Design style 2026-03-12 03:13:17 -04:00
Miguel Astor
1472c14d44 Add Material Design style 2026-03-08 12:36:14 -04:00
Miguel Astor
c0c25e2719 Add By Runner and By Source charts to playtime reports
- Add runners and sources charts to modern and platinum templates
- Update responsive layout to use grid for multiple charts
- Update .gitignore to exclude pga.db
2026-03-06 06:19:06 -04:00
Miguel Astor
afd11fba3a Separated the modern theme script into it's own file. 2026-03-03 03:56:39 -04:00
Miguel Astor
b56b7176a8 Extract CSS styles to separate files for better maintainability
- Create templates/brutalism.css, glassmorphism.css, neumorphism.css
- Update styles.py to read CSS from files instead of inline strings
- Support --template with .css files (auto-detects modern style)
- CSS files can now be edited with proper syntax highlighting

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-03 03:29:25 -04:00
Miguel Astor
15a8072804 Unify modern templates into single template with style system
- Create templates/modern.html as unified base for brutalism, glassmorphism, neumorphism
- Add styles.py with CSS and chart config for each style
- Add --style argument to generate_report.py (overrides --template)
- Remove individual brutalism.html, glassmorphism.html, neumorphism.html
- Keep platinum.html separate due to unique Mac OS 9 structure

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-03 03:23:43 -04:00
Miguel Astor
31e8d152ae Add By Runner tab to summaries section in all templates
Extract runner field from Lutris database and display playtime
grouped by runner (wine, linux, steam, dosbox, etc.) in a new
third tab alongside Top Games and By Category.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-03 03:09:29 -04:00
Miguel Astor
aa9719cbfe Update CLAUDE.md documentation
- Document all four available templates (platinum, brutalism, glassmorphism, neumorphism)
- Fix template path to use templates/ folder
- Update theme toggle description (auto/light/dark with persistent preference)
- Add responsive design mention
- Fix filtered categories list

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-26 17:29:28 -04:00
Miguel Astor
f4e5c33c87 Improve card header visibility in brutalism template dark mode
- Keep card-header background yellow in both light and dark modes using
  --accent-tertiary variable (#ffff00 light, #ffff33 dark)
- Set card-title text to black in dark mode for optimal contrast against
  yellow background
- Remove dark mode overrides that changed header background to gray

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-26 17:19:52 -04:00
Miguel Astor
db575f2bb7 Include Horny category in category chart and table
Remove Horny from category filter to display it in the categories chart
and By Category table. Now only .hidden and favorite remain filtered.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-26 17:12:52 -04:00
Miguel Astor
3394d66151 Include Horny category in game category badges
Remove Horny from the filter list to display it in category badges.
Only .hidden and favorite categories remain filtered.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-26 17:09:01 -04:00
Miguel Astor
3a700c4f48 Add category badges to game tables in all templates
Display game categories as colored badges after service badges in both
top games and others sections. Categories filtered to exclude .hidden,
favorite, and Horny.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-26 17:07:28 -04:00
Miguel Astor
f850e4d69c Move templates and assets to templates/ folder
- Move all HTML templates to templates/
- Move Platinum assets to templates/Platinum/
- Update default paths in generate_report.py

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-26 03:21:25 -04:00
Miguel Astor
49c7a2bba8 Add brutalism template with bold industrial style
Features high-contrast colors, thick borders, hard shadows,
monospace typography, and no rounded corners. Uses custom
background placeholder that shows theme color when no image
is specified.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-26 03:18:13 -04:00
Miguel Astor
6e7f3e5e16 Add scroll-to-top button to all templates
Floating button in bottom-right corner that appears when scrolling
down and smoothly scrolls to top when clicked. Each template has
matching visual style.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-26 03:09:44 -04:00
Miguel Astor
9e49262118 Add neumorphism template with soft UI style
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-26 03:02:58 -04:00
259 changed files with 7640 additions and 1190 deletions

1
.gitignore vendored
View File

@@ -259,3 +259,4 @@ flycheck_*.el
# Built Visual Studio Code Extensions # Built Visual Studio Code Extensions
*.vsix *.vsix
pga.db

80
AGENTS.md Normal file
View File

@@ -0,0 +1,80 @@
# CLAUDE.md
This file provides guidance to AI coding agents when working with code in this repository.
## Project Overview
Lutris Year in Review is a Python script that generates a static HTML playtime report from a Lutris gaming platform SQLite database. The report displays game playtime statistics with interactive Chart.js visualizations.
## Commands
Generate report with defaults:
```bash
python generate_report.py
```
Generate report with modern style:
```bash
python generate_report.py --style glassmorphism --output report.html
python generate_report.py --style brutalism --output report.html
python generate_report.py --style skeuo --output report.html
python generate_report.py --style neumorphism --output report.html
python generate_report.py --style material --output report.html
python generate_report.py --style flat --output report.html
python generate_report.py --style synthwave --output report.html
python generate_report.py --style vaporwave --output report.html
python generate_report.py --style terminal --output report.html
python generate_report.py --style highcontrast --output report.html
```
Generate report with legacy Platinum template:
```bash
python generate_report.py --template templates/platinum.html --output report.html
```
All options:
```bash
python generate_report.py --db pga.db --output report.html --top 10 --background background.png --style glassmorphism
```
## Architecture
**Report generator (`generate_report.py`):**
- Reads Lutris SQLite database (`pga.db`) containing games, categories, and playtime data
- Embeds all data (games JSON, background image as base64) directly into a self-contained HTML file
- Loads HTML template from `templates/` folder (default: `templates/platinum.html`)
**HTML templates (`templates/`):**
- **modern.html**: Unified template for modern styles (brutalism, glassmorphism, skeuo, neumorphism, material, flat, synthwave, vaporwave, terminal, highcontrast)
- **platinum.html**: Legacy Mac OS 9 Platinum visual style (separate template due to unique structure)
- All templates use Chart.js doughnut charts and dynamic JavaScript filtering
- Placeholder tokens like `__ALL_GAMES__`, `__BACKGROUND_IMAGE__` are replaced at generation time
- Modern templates support light/dark/auto theme toggle button
**Style system (`styles.py`):**
- CSS definitions for each modern style (brutalism, glassmorphism, skeuo, neumorphism, material, flat, synthwave, vaporwave, terminal, highcontrast)
- Theme configurations (colors, fonts, chart options) injected via `__THEME_CSS__`
- Use `--style` argument to select a modern style instead of `--template`
**Javascript (`templates/script.js`):**
- A single common script is used for each modern style (brutalism, glassmorphism, skeuo, neumorphism, material, flat, synthwave, vaporwave, terminal, highcontrast)
- Theme configurations (colors, fonts, chart options) injected via `__THEME_CONFIG__`
- Data inserted via `__ALL_GAMES__` and `__TOP_N__`
**Database schema (`schema.py`):**
- Reference file documenting Lutris database structure
- Key tables: `games` (with `playtime`, `service` fields), `categories`, `games_categories` (many-to-many join)
**Generated output (`report.html`):**
- Fully static, can be hosted on any web server
- Client-side filtering by service (Steam, GOG, itch.io, local)
- Expandable "Others" row in games table
- Light/dark/auto theme toggle button with persistent preference
- Responsive design for mobile and desktop
## Key Data Relationships
- Games have a `service` field (steam, gog, itchio, humblebundle, or NULL for local)
- Games link to categories via `games_categories` join table
- Categories like `.hidden` and `favorite` are filtered out in the report display
- `playtime` is cumulative hours (REAL), not per-session data

View File

@@ -1,48 +1 @@
# CLAUDE.md @AGENTS.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Lutris Year in Review is a Python script that generates a static HTML playtime report from a Lutris gaming platform SQLite database. The report displays game playtime statistics with interactive Chart.js visualizations.
## Commands
Generate report with defaults:
```bash
python generate_report.py
```
Generate report with custom options:
```bash
python generate_report.py --db pga.db --output report.html --top 10 --background background.png --template platinum.html
```
## Architecture
**Report generator (`generate_report.py`):**
- Reads Lutris SQLite database (`pga.db`) containing games, categories, and playtime data
- Embeds all data (games JSON, background image as base64) directly into a self-contained HTML file
- Loads HTML template from external file (default: `platinum.html`)
**HTML template (`platinum.html`):**
- Chart.js doughnut charts and dynamic JavaScript filtering
- Mac OS 9 Platinum visual style with placeholder tokens for assets
- Tokens like `__ALL_GAMES__`, `__BACKGROUND_IMAGE__` are replaced at generation time
**Database schema (`schema.py`):**
- Reference file documenting Lutris database structure
- Key tables: `games` (with `playtime`, `service` fields), `categories`, `games_categories` (many-to-many join)
**Generated output (`report.html`):**
- Fully static, can be hosted on any web server
- Client-side filtering by service (Steam, GOG, itch.io, local)
- Expandable "Others" row in games table
- Light/dark mode support via CSS `prefers-color-scheme`
## Key Data Relationships
- Games have a `service` field (steam, gog, itchio, humblebundle, or NULL for local)
- Games link to categories via `games_categories` join table
- Categories like `.hidden`, `favorite`, `Horny` are filtered out in the report
- `playtime` is cumulative hours (REAL), not per-session data

View File

@@ -21,9 +21,25 @@ import json
import sqlite3 import sqlite3
from pathlib import Path from pathlib import Path
from styles import get_theme_css, get_theme_config
# Directory where this script is located (for finding template.html) # Directory where this script is located (for finding template.html)
SCRIPT_DIR = Path(__file__).parent SCRIPT_DIR = Path(__file__).parent
# Modern styles that use the unified template
MODERN_STYLES = [
"brutalism",
"glassmorphism",
"skeuo",
"neumorphism",
"material",
"flat",
"synthwave",
"vaporwave",
"terminal",
"highcontrast",
]
def load_template(template_file: str) -> str: def load_template(template_file: str) -> str:
"""Load the HTML template from the specified file.""" """Load the HTML template from the specified file."""
@@ -31,6 +47,12 @@ def load_template(template_file: str) -> str:
return template_path.read_text(encoding="utf-8") return template_path.read_text(encoding="utf-8")
def load_script(script_file: str) -> str:
"""Load the JS script from the specified file."""
script_path = SCRIPT_DIR / script_file
return script_path.read_text(encoding="utf-8")
def load_asset_as_base64(path: Path, mime_type: str) -> str: def load_asset_as_base64(path: Path, mime_type: str) -> str:
"""Load a file and return it as a base64 data URL.""" """Load a file and return it as a base64 data URL."""
if path.exists(): if path.exists():
@@ -49,7 +71,7 @@ def get_all_games(db_path: str) -> tuple[list[dict], int]:
total_library = cursor.fetchone()[0] total_library = cursor.fetchone()[0]
cursor.execute(""" cursor.execute("""
SELECT id, name, playtime, COALESCE(service, 'local') as service SELECT id, name, playtime, COALESCE(service, 'local') as service, COALESCE(runner, 'unknown') as runner
FROM games FROM games
WHERE playtime > 0 WHERE playtime > 0
ORDER BY playtime DESC ORDER BY playtime DESC
@@ -75,14 +97,23 @@ def get_all_games(db_path: str) -> tuple[list[dict], int]:
"name": row[1], "name": row[1],
"playtime": row[2], "playtime": row[2],
"service": row[3], "service": row[3],
"categories": game_categories.get(row[0], []) "runner": row[4],
"categories": game_categories.get(row[0], []),
} }
for row in games_rows for row in games_rows
] ]
return games, total_library return games, total_library
def generate_report(db_path: str, output_path: str, top_n: int, assets_dir: str, template_file: str, bg_image_path: str = None) -> None: def generate_report(
db_path: str,
output_path: str,
top_n: int,
assets_dir: str,
template_file: str,
bg_image_path: str | None = None,
style: str | None = None,
) -> None:
"""Generate the HTML report.""" """Generate the HTML report."""
all_games, total_library = get_all_games(db_path) all_games, total_library = get_all_games(db_path)
@@ -98,31 +129,86 @@ def generate_report(db_path: str, output_path: str, top_n: int, assets_dir: str,
# Load background image (custom or default stripes) # Load background image (custom or default stripes)
if bg_image_path and Path(bg_image_path).exists(): if bg_image_path and Path(bg_image_path).exists():
background_image = load_asset_as_base64(Path(bg_image_path), "image/png") background_image = load_asset_as_base64(Path(bg_image_path), "image/png")
background_image_custom = f"url('{background_image}')"
else: else:
background_image = load_asset_as_base64(assets_path / "Others" / "stripes.png", "image/png") background_image = load_asset_as_base64(
assets_path / "Others" / "stripes.png", "image/png"
)
background_image_custom = (
"none" # For templates that prefer no default background
)
# Check if using modern unified template
if style and style in MODERN_STYLES:
html = load_template("templates/modern.html")
# Inject theme CSS and config
theme_css = get_theme_css(style)
theme_config = get_theme_config(style)
# Inject javascript
javascript = load_script("templates/script.js")
javascript = javascript.replace("__ALL_GAMES__", json.dumps(all_games))
javascript = javascript.replace("__TOP_N__", str(top_n))
javascript = javascript.replace("__THEME_CONFIG__", theme_config)
html = html.replace("__THEME_CSS__", theme_css)
html = html.replace("__SCRIPT__", javascript)
html = html.replace("__TOTAL_LIBRARY__", str(total_library))
html = html.replace("__BACKGROUND_IMAGE__", background_image)
html = html.replace("__BACKGROUND_IMAGE_CUSTOM__", background_image_custom)
else:
# Legacy template handling (platinum and others)
# Load fonts # Load fonts
font_charcoal = load_asset_as_base64(assets_path / "Charcoal.ttf", "font/truetype") font_charcoal = load_asset_as_base64(
assets_path / "Charcoal.ttf", "font/truetype"
)
font_monaco = load_asset_as_base64(assets_path / "MONACO.TTF", "font/truetype") font_monaco = load_asset_as_base64(assets_path / "MONACO.TTF", "font/truetype")
# Load images # Load images
titlebar_bg = load_asset_as_base64(assets_path / "Windows" / "title-1-active.png", "image/png") titlebar_bg = load_asset_as_base64(
title_stripes = load_asset_as_base64(assets_path / "Windows" / "title-1-active.png", "image/png") assets_path / "Windows" / "title-1-active.png", "image/png"
close_btn = load_asset_as_base64(assets_path / "Windows" / "close-active.png", "image/png") )
hide_btn = load_asset_as_base64(assets_path / "Windows" / "maximize-active.png", "image/png") title_stripes = load_asset_as_base64(
shade_btn = load_asset_as_base64(assets_path / "Windows" / "shade-active.png", "image/png") assets_path / "Windows" / "title-1-active.png", "image/png"
check_off = load_asset_as_base64(assets_path / "Check-Radio" / "check-normal.png", "image/png") )
check_on = load_asset_as_base64(assets_path / "Check-Radio" / "check-active.png", "image/png") close_btn = load_asset_as_base64(
assets_path / "Windows" / "close-active.png", "image/png"
)
hide_btn = load_asset_as_base64(
assets_path / "Windows" / "maximize-active.png", "image/png"
)
shade_btn = load_asset_as_base64(
assets_path / "Windows" / "shade-active.png", "image/png"
)
check_off = load_asset_as_base64(
assets_path / "Check-Radio" / "check-normal.png", "image/png"
)
check_on = load_asset_as_base64(
assets_path / "Check-Radio" / "check-active.png", "image/png"
)
# Load scrollbar images # Load scrollbar images
scrollbar_trough_v = load_asset_as_base64(assets_path / "Scrollbars" / "trough-scrollbar-vert.png", "image/png") scrollbar_trough_v = load_asset_as_base64(
scrollbar_thumb_v = load_asset_as_base64(assets_path / "Scrollbars" / "slider-vertical.png", "image/png") assets_path / "Scrollbars" / "trough-scrollbar-vert.png", "image/png"
scrollbar_up = load_asset_as_base64(assets_path / "Scrollbars" / "stepper-up.png", "image/png") )
scrollbar_down = load_asset_as_base64(assets_path / "Scrollbars" / "stepper-down.png", "image/png") scrollbar_thumb_v = load_asset_as_base64(
assets_path / "Scrollbars" / "slider-vertical.png", "image/png"
)
scrollbar_up = load_asset_as_base64(
assets_path / "Scrollbars" / "stepper-up.png", "image/png"
)
scrollbar_down = load_asset_as_base64(
assets_path / "Scrollbars" / "stepper-down.png", "image/png"
)
# Load tab images # Load tab images
tab_active = load_asset_as_base64(assets_path / "Tabs" / "tab-top-active.png", "image/png") tab_active = load_asset_as_base64(
tab_inactive = load_asset_as_base64(assets_path / "Tabs" / "tab-top.png", "image/png") assets_path / "Tabs" / "tab-top-active.png", "image/png"
)
tab_inactive = load_asset_as_base64(
assets_path / "Tabs" / "tab-top.png", "image/png"
)
html = load_template(template_file) html = load_template(template_file)
html = html.replace("__ALL_GAMES__", json.dumps(all_games)) html = html.replace("__ALL_GAMES__", json.dumps(all_games))
@@ -131,6 +217,7 @@ def generate_report(db_path: str, output_path: str, top_n: int, assets_dir: str,
html = html.replace("__FONT_CHARCOAL__", font_charcoal) html = html.replace("__FONT_CHARCOAL__", font_charcoal)
html = html.replace("__FONT_MONACO__", font_monaco) html = html.replace("__FONT_MONACO__", font_monaco)
html = html.replace("__BACKGROUND_IMAGE__", background_image) html = html.replace("__BACKGROUND_IMAGE__", background_image)
html = html.replace("__BACKGROUND_IMAGE_CUSTOM__", background_image_custom)
html = html.replace("__TITLEBAR_BG__", titlebar_bg) html = html.replace("__TITLEBAR_BG__", titlebar_bg)
html = html.replace("__TITLE_STRIPES__", title_stripes) html = html.replace("__TITLE_STRIPES__", title_stripes)
html = html.replace("__CLOSE_BTN__", close_btn) html = html.replace("__CLOSE_BTN__", close_btn)
@@ -159,33 +246,50 @@ def main():
parser.add_argument( parser.add_argument(
"--db", "--db",
default="pga.db", default="pga.db",
help="Path to the Lutris SQLite database (default: pga.db)" help="Path to the Lutris SQLite database (default: pga.db)",
) )
parser.add_argument( parser.add_argument(
"--output", "--output",
default="report.html", default="report.html",
help="Output HTML file path (default: report.html)" help="Output HTML file path (default: report.html)",
) )
parser.add_argument( parser.add_argument(
"--top", "--top",
type=int, type=int,
default=10, default=10,
help="Number of top games to show individually (default: 10)" help="Number of top games to show individually (default: 10)",
) )
parser.add_argument( parser.add_argument(
"--assets", "--assets",
default="Platinum", default="templates/Platinum",
help="Path to Platinum assets directory (default: Platinum)" help="Path to Platinum assets directory (default: templates/Platinum)",
) )
parser.add_argument( parser.add_argument(
"--background", "--background",
default=None, default=None,
help="Path to background image for tiling (default: Platinum stripes pattern)" help="Path to background image for tiling (default: Platinum stripes pattern)",
) )
parser.add_argument( parser.add_argument(
"--template", "--template",
default="platinum.html", default="templates/platinum.html",
help="HTML template file to use (default: platinum.html)" help="HTML template file to use (default: templates/platinum.html). Ignored if --style is used.",
)
parser.add_argument(
"--style",
choices=[
"brutalism",
"glassmorphism",
"skeuo",
"neumorphism",
"material",
"flat",
"synthwave",
"vaporwave",
"terminal",
"highcontrast",
],
default=None,
help="Modern style to use (brutalism, glassmorphism, skeuo, neumorphism, material, flat, synthwave, vaporwave, terminal, highcontrast). Overrides --template.",
) )
args = parser.parse_args() args = parser.parse_args()
@@ -198,12 +302,39 @@ def main():
print(f"Error: Assets directory not found: {args.assets}") print(f"Error: Assets directory not found: {args.assets}")
return 1 return 1
# Determine style from --style or --template
style = args.style
# If no --style provided, check if --template points to a modern CSS file
if not style and args.template.endswith(".css"):
template_path = Path(args.template)
style_name = template_path.stem # e.g., "brutalism" from "brutalism.css"
if style_name in MODERN_STYLES:
style = style_name
# Validate template/style
if style and style in MODERN_STYLES:
template_path = SCRIPT_DIR / "templates" / "modern.html"
css_path = SCRIPT_DIR / "templates" / f"{style}.css"
if not css_path.exists():
print(f"Error: CSS file not found: {css_path}")
return 1
else:
template_path = SCRIPT_DIR / args.template template_path = SCRIPT_DIR / args.template
if not template_path.exists(): if not template_path.exists():
print(f"Error: Template file not found: {template_path}") print(f"Error: Template file not found: {template_path}")
return 1 return 1
generate_report(args.db, args.output, args.top, args.assets, args.template, args.background) generate_report(
args.db,
args.output,
args.top,
args.assets,
args.template,
args.background,
style,
)
return 0 return 0

File diff suppressed because it is too large Load Diff

341
styles.py Normal file
View File

@@ -0,0 +1,341 @@
"""CSS styles and theme configurations for modern templates."""
####################################################################################################
# Copyright (C) 2026 by WallyHackenslacker wallyhackenslacker@noreply.git.hackenslacker.space #
# #
# Permission to use, copy, modify, and/or distribute this software for any purpose with or without #
# fee is hereby granted. #
# #
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS #
# SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE #
# AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES #
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, #
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE #
# OF THIS SOFTWARE. #
####################################################################################################
import json
from pathlib import Path
# Directory where templates are located
TEMPLATES_DIR = Path(__file__).parent / "templates"
# Theme configurations for Chart.js
THEME_CONFIGS = {
"brutalism": {
"colors": [
"#ff0000",
"#0000ff",
"#ffff00",
"#00ff00",
"#ff00ff",
"#00ffff",
"#ff8800",
"#8800ff",
"#0088ff",
"#88ff00",
"#888888",
],
"fontFamily": "'Courier New', monospace",
"fontWeight": "bold",
"pointStyle": "rect",
"textColorLight": "#000000",
"textColorDark": "#ffffff",
"borderColorLight": "#000000",
"borderColorDark": "#ffffff",
"borderWidth": 3,
"tooltipBg": "#000000",
"tooltipTitleColor": "#ffffff",
"tooltipBodyColor": "#ffffff",
"tooltipBorderColor": "#ffffff",
"tooltipBorderWidth": 2,
"tooltipCornerRadius": 0,
"uppercaseTooltip": True,
},
"glassmorphism": {
"colors": [
"#6366f1",
"#8b5cf6",
"#ec4899",
"#f43f5e",
"#f97316",
"#eab308",
"#22c55e",
"#14b8a6",
"#06b6d4",
"#3b82f6",
"#64748b",
],
"fontFamily": "'Inter', sans-serif",
"fontWeight": "normal",
"pointStyle": "circle",
"textColorLight": "#1a1a2e",
"textColorDark": "#f0f0f5",
"borderColorLight": "rgba(255, 255, 255, 0.2)",
"borderColorDark": "rgba(255, 255, 255, 0.2)",
"borderWidth": 2,
"tooltipBg": "rgba(0, 0, 0, 0.8)",
"tooltipTitleColor": "#ffffff",
"tooltipBodyColor": "#ffffff",
"tooltipBorderColor": "transparent",
"tooltipBorderWidth": 0,
"tooltipCornerRadius": 8,
"uppercaseTooltip": False,
},
"skeuo": {
"colors": [
"#8b4a2c",
"#a65d37",
"#bf7748",
"#d58a60",
"#e0a97f",
"#6d4f34",
"#857153",
"#9e8362",
"#b58f6c",
"#c9a67f",
"#7f7f7f",
],
"fontFamily": "'Trebuchet MS', 'Lucida Grande', sans-serif",
"fontWeight": "600",
"pointStyle": "rectRounded",
"textColorLight": "#2c2218",
"textColorDark": "#f4e7d5",
"borderColorLight": "rgba(75, 55, 36, 0.35)",
"borderColorDark": "rgba(219, 188, 156, 0.3)",
"borderWidth": 2,
"tooltipBg": "rgba(46, 33, 23, 0.92)",
"tooltipTitleColor": "#f4e7d5",
"tooltipBodyColor": "#f4e7d5",
"tooltipBorderColor": "#d5a67f",
"tooltipBorderWidth": 1,
"tooltipCornerRadius": 10,
"uppercaseTooltip": False,
},
"neumorphism": {
"colors": [
"#6366f1",
"#8b5cf6",
"#ec4899",
"#f43f5e",
"#f97316",
"#eab308",
"#22c55e",
"#14b8a6",
"#06b6d4",
"#3b82f6",
"#64748b",
],
"fontFamily": "'Inter', sans-serif",
"fontWeight": "normal",
"pointStyle": "circle",
"textColorLight": "#2d3436",
"textColorDark": "#f0f0f5",
"borderColorLight": "rgba(255, 255, 255, 0.3)",
"borderColorDark": "rgba(255, 255, 255, 0.3)",
"borderWidth": 3,
"tooltipBg": "rgba(0, 0, 0, 0.8)",
"tooltipTitleColor": "#ffffff",
"tooltipBodyColor": "#ffffff",
"tooltipBorderColor": "transparent",
"tooltipBorderWidth": 0,
"tooltipCornerRadius": 8,
"uppercaseTooltip": False,
},
"material": {
"colors": [
"#6200ee",
"#03dac6",
"#3700b3",
"#018786",
"#b00020",
"#ff0266",
"#aa00ff",
"#0091ea",
"#00c853",
"#ffd600",
"#757575",
],
"fontFamily": "'Roboto', sans-serif",
"fontWeight": "normal",
"pointStyle": "circle",
"textColorLight": "#212121",
"textColorDark": "#ffffff",
"borderColorLight": "#ffffff",
"borderColorDark": "#1e1e1e",
"borderWidth": 2,
"tooltipBg": "rgba(97, 97, 97, 0.9)",
"tooltipTitleColor": "#ffffff",
"tooltipBodyColor": "#ffffff",
"tooltipBorderColor": "transparent",
"tooltipBorderWidth": 0,
"tooltipCornerRadius": 4,
"uppercaseTooltip": False,
},
"flat": {
"colors": [
"#ffadad",
"#ffd6a5",
"#fdffb6",
"#caffbf",
"#9bf6ff",
"#a0c4ff",
"#bdb2ff",
"#ffc6ff",
"#ff9aa2",
"#e2f0cb",
"#b5ead7",
],
"fontFamily": "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
"fontWeight": "500",
"pointStyle": "circle",
"textColorLight": "#1d1d1f",
"textColorDark": "#f5f5f7",
"borderColorLight": "#ffffff",
"borderColorDark": "#1c1c1e",
"borderWidth": 2,
"tooltipBg": "rgba(0, 0, 0, 0.8)",
"tooltipTitleColor": "#ffffff",
"tooltipBodyColor": "#ffffff",
"tooltipBorderColor": "transparent",
"tooltipBorderWidth": 0,
"tooltipCornerRadius": 12,
"uppercaseTooltip": False,
},
"synthwave": {
"colors": [
"#ff00ff",
"#00ffff",
"#ffff00",
"#ff0055",
"#00ff99",
"#7a5af8",
"#ff8800",
"#ff00aa",
"#00ccff",
"#ccff00",
"#999999",
],
"fontFamily": "'Orbitron', 'Segoe UI', sans-serif",
"fontWeight": "bold",
"pointStyle": "triangle",
"textColorLight": "#2d004d",
"textColorDark": "#ff00ff",
"borderColorLight": "rgba(255, 0, 255, 0.2)",
"borderColorDark": "rgba(0, 255, 255, 0.2)",
"borderWidth": 2,
"tooltipBg": "rgba(45, 0, 77, 0.9)",
"tooltipTitleColor": "#00ffff",
"tooltipBodyColor": "#ffffff",
"tooltipBorderColor": "#ff00ff",
"tooltipBorderWidth": 1,
"tooltipCornerRadius": 0,
"uppercaseTooltip": True,
},
"vaporwave": {
"colors": [
"#ff71ce",
"#01cdfe",
"#05ffa1",
"#b967ff",
"#fffb96",
"#ff99cc",
"#99ccff",
"#ccff99",
"#ffcc99",
"#ffffcc",
"#e0e0e0",
],
"fontFamily": "'MS PGothic', 'Palatino Linotype', serif",
"fontWeight": "normal",
"pointStyle": "circle",
"textColorLight": "#ff71ce",
"textColorDark": "#01cdfe",
"borderColorLight": "rgba(255, 255, 255, 0.5)",
"borderColorDark": "rgba(255, 255, 255, 0.2)",
"borderWidth": 1,
"tooltipBg": "rgba(255, 113, 206, 0.8)",
"tooltipTitleColor": "#ffffff",
"tooltipBodyColor": "#ffffff",
"tooltipBorderColor": "#01cdfe",
"tooltipBorderWidth": 2,
"tooltipCornerRadius": 4,
"uppercaseTooltip": False,
},
"terminal": {
"colors": [
"#00ff00",
"#00cc00",
"#009900",
"#006600",
"#003300",
"#33ff33",
"#66ff66",
"#99ff99",
"#ccffcc",
"#ffffff",
"#888888",
],
"fontFamily": "'Courier New', Courier, monospace",
"fontWeight": "bold",
"pointStyle": "rectRot",
"textColorLight": "#006600",
"textColorDark": "#00ff00",
"borderColorLight": "#000000",
"borderColorDark": "#00ff00",
"borderWidth": 1,
"tooltipBg": "#000000",
"tooltipTitleColor": "#00ff00",
"tooltipBodyColor": "#00ff00",
"tooltipBorderColor": "#00ff00",
"tooltipBorderWidth": 1,
"tooltipCornerRadius": 0,
"uppercaseTooltip": True,
},
"highcontrast": {
"colors": [
"#888888",
"#444444",
"#cccccc",
"#666666",
"#aaaaaa",
"#333333",
"#dddddd",
"#555555",
"#bbbbbb",
"#222222",
"#999999",
],
"fontFamily": "Arial, Helvetica, sans-serif",
"fontWeight": "900",
"pointStyle": "rect",
"textColorLight": "#000000",
"textColorDark": "#ffffff",
"borderColorLight": "#000000",
"borderColorDark": "#ffffff",
"borderWidth": 4,
"tooltipBg": "#000000",
"tooltipTitleColor": "#ffffff",
"tooltipBodyColor": "#ffffff",
"tooltipBorderColor": "#ffffff",
"tooltipBorderWidth": 3,
"tooltipCornerRadius": 0,
"uppercaseTooltip": True,
},
}
def get_theme_css(style: str) -> str:
"""Get the CSS for a given style by reading from the corresponding .css file."""
css_file = TEMPLATES_DIR / f"{style}.css"
if css_file.exists():
return css_file.read_text(encoding="utf-8")
# Fallback to glassmorphism if style not found
fallback = TEMPLATES_DIR / "glassmorphism.css"
return fallback.read_text(encoding="utf-8")
def get_theme_config(style: str) -> str:
"""Get the theme config as JSON string for a given style."""
config = THEME_CONFIGS.get(style, THEME_CONFIGS["glassmorphism"])
return json.dumps(config)

View File

Before

Width:  |  Height:  |  Size: 172 B

After

Width:  |  Height:  |  Size: 172 B

View File

Before

Width:  |  Height:  |  Size: 122 B

After

Width:  |  Height:  |  Size: 122 B

View File

Before

Width:  |  Height:  |  Size: 172 B

After

Width:  |  Height:  |  Size: 172 B

View File

Before

Width:  |  Height:  |  Size: 172 B

After

Width:  |  Height:  |  Size: 172 B

View File

Before

Width:  |  Height:  |  Size: 119 B

After

Width:  |  Height:  |  Size: 119 B

View File

Before

Width:  |  Height:  |  Size: 171 B

After

Width:  |  Height:  |  Size: 171 B

View File

Before

Width:  |  Height:  |  Size: 171 B

After

Width:  |  Height:  |  Size: 171 B

View File

Before

Width:  |  Height:  |  Size: 182 B

After

Width:  |  Height:  |  Size: 182 B

View File

Before

Width:  |  Height:  |  Size: 179 B

After

Width:  |  Height:  |  Size: 179 B

View File

Before

Width:  |  Height:  |  Size: 127 B

After

Width:  |  Height:  |  Size: 127 B

View File

Before

Width:  |  Height:  |  Size: 127 B

After

Width:  |  Height:  |  Size: 127 B

View File

Before

Width:  |  Height:  |  Size: 127 B

After

Width:  |  Height:  |  Size: 127 B

View File

Before

Width:  |  Height:  |  Size: 127 B

After

Width:  |  Height:  |  Size: 127 B

View File

Before

Width:  |  Height:  |  Size: 123 B

After

Width:  |  Height:  |  Size: 123 B

View File

Before

Width:  |  Height:  |  Size: 121 B

After

Width:  |  Height:  |  Size: 121 B

View File

Before

Width:  |  Height:  |  Size: 121 B

After

Width:  |  Height:  |  Size: 121 B

View File

Before

Width:  |  Height:  |  Size: 201 B

After

Width:  |  Height:  |  Size: 201 B

View File

Before

Width:  |  Height:  |  Size: 202 B

After

Width:  |  Height:  |  Size: 202 B

View File

Before

Width:  |  Height:  |  Size: 140 B

After

Width:  |  Height:  |  Size: 140 B

View File

Before

Width:  |  Height:  |  Size: 299 B

After

Width:  |  Height:  |  Size: 299 B

View File

Before

Width:  |  Height:  |  Size: 314 B

After

Width:  |  Height:  |  Size: 314 B

View File

Before

Width:  |  Height:  |  Size: 299 B

After

Width:  |  Height:  |  Size: 299 B

View File

Before

Width:  |  Height:  |  Size: 308 B

After

Width:  |  Height:  |  Size: 308 B

View File

Before

Width:  |  Height:  |  Size: 314 B

After

Width:  |  Height:  |  Size: 314 B

View File

Before

Width:  |  Height:  |  Size: 311 B

After

Width:  |  Height:  |  Size: 311 B

View File

Before

Width:  |  Height:  |  Size: 299 B

After

Width:  |  Height:  |  Size: 299 B

View File

Before

Width:  |  Height:  |  Size: 299 B

After

Width:  |  Height:  |  Size: 299 B

View File

Before

Width:  |  Height:  |  Size: 338 B

After

Width:  |  Height:  |  Size: 338 B

View File

Before

Width:  |  Height:  |  Size: 289 B

After

Width:  |  Height:  |  Size: 289 B

View File

Before

Width:  |  Height:  |  Size: 243 B

After

Width:  |  Height:  |  Size: 243 B

View File

Before

Width:  |  Height:  |  Size: 237 B

After

Width:  |  Height:  |  Size: 237 B

View File

Before

Width:  |  Height:  |  Size: 242 B

After

Width:  |  Height:  |  Size: 242 B

View File

Before

Width:  |  Height:  |  Size: 290 B

After

Width:  |  Height:  |  Size: 290 B

View File

Before

Width:  |  Height:  |  Size: 365 B

After

Width:  |  Height:  |  Size: 365 B

View File

Before

Width:  |  Height:  |  Size: 365 B

After

Width:  |  Height:  |  Size: 365 B

View File

Before

Width:  |  Height:  |  Size: 365 B

After

Width:  |  Height:  |  Size: 365 B

View File

Before

Width:  |  Height:  |  Size: 365 B

After

Width:  |  Height:  |  Size: 365 B

View File

Before

Width:  |  Height:  |  Size: 378 B

After

Width:  |  Height:  |  Size: 378 B

View File

Before

Width:  |  Height:  |  Size: 365 B

After

Width:  |  Height:  |  Size: 365 B

View File

Before

Width:  |  Height:  |  Size: 172 B

After

Width:  |  Height:  |  Size: 172 B

View File

Before

Width:  |  Height:  |  Size: 172 B

After

Width:  |  Height:  |  Size: 172 B

View File

Before

Width:  |  Height:  |  Size: 172 B

After

Width:  |  Height:  |  Size: 172 B

View File

Before

Width:  |  Height:  |  Size: 299 B

After

Width:  |  Height:  |  Size: 299 B

View File

Before

Width:  |  Height:  |  Size: 287 B

After

Width:  |  Height:  |  Size: 287 B

View File

Before

Width:  |  Height:  |  Size: 314 B

After

Width:  |  Height:  |  Size: 314 B

View File

Before

Width:  |  Height:  |  Size: 314 B

After

Width:  |  Height:  |  Size: 314 B

View File

Before

Width:  |  Height:  |  Size: 299 B

After

Width:  |  Height:  |  Size: 299 B

View File

Before

Width:  |  Height:  |  Size: 287 B

After

Width:  |  Height:  |  Size: 287 B

View File

Before

Width:  |  Height:  |  Size: 314 B

After

Width:  |  Height:  |  Size: 314 B

View File

Before

Width:  |  Height:  |  Size: 314 B

After

Width:  |  Height:  |  Size: 314 B

View File

Before

Width:  |  Height:  |  Size: 290 B

After

Width:  |  Height:  |  Size: 290 B

View File

Before

Width:  |  Height:  |  Size: 292 B

After

Width:  |  Height:  |  Size: 292 B

View File

Before

Width:  |  Height:  |  Size: 297 B

After

Width:  |  Height:  |  Size: 297 B

View File

Before

Width:  |  Height:  |  Size: 297 B

After

Width:  |  Height:  |  Size: 297 B

View File

Before

Width:  |  Height:  |  Size: 216 B

After

Width:  |  Height:  |  Size: 216 B

View File

Before

Width:  |  Height:  |  Size: 202 B

After

Width:  |  Height:  |  Size: 202 B

View File

Before

Width:  |  Height:  |  Size: 202 B

After

Width:  |  Height:  |  Size: 202 B

View File

Before

Width:  |  Height:  |  Size: 185 B

After

Width:  |  Height:  |  Size: 185 B

View File

Before

Width:  |  Height:  |  Size: 167 B

After

Width:  |  Height:  |  Size: 167 B

View File

Before

Width:  |  Height:  |  Size: 170 B

After

Width:  |  Height:  |  Size: 170 B

View File

Before

Width:  |  Height:  |  Size: 204 B

After

Width:  |  Height:  |  Size: 204 B

View File

Before

Width:  |  Height:  |  Size: 181 B

After

Width:  |  Height:  |  Size: 181 B

View File

Before

Width:  |  Height:  |  Size: 154 B

After

Width:  |  Height:  |  Size: 154 B

View File

Before

Width:  |  Height:  |  Size: 164 B

After

Width:  |  Height:  |  Size: 164 B

View File

Before

Width:  |  Height:  |  Size: 238 B

After

Width:  |  Height:  |  Size: 238 B

View File

Before

Width:  |  Height:  |  Size: 229 B

After

Width:  |  Height:  |  Size: 229 B

View File

Before

Width:  |  Height:  |  Size: 232 B

After

Width:  |  Height:  |  Size: 232 B

View File

Before

Width:  |  Height:  |  Size: 213 B

After

Width:  |  Height:  |  Size: 213 B

View File

Before

Width:  |  Height:  |  Size: 209 B

After

Width:  |  Height:  |  Size: 209 B

View File

Before

Width:  |  Height:  |  Size: 207 B

After

Width:  |  Height:  |  Size: 207 B

View File

Before

Width:  |  Height:  |  Size: 219 B

After

Width:  |  Height:  |  Size: 219 B

View File

Before

Width:  |  Height:  |  Size: 233 B

After

Width:  |  Height:  |  Size: 233 B

View File

Before

Width:  |  Height:  |  Size: 229 B

After

Width:  |  Height:  |  Size: 229 B

View File

Before

Width:  |  Height:  |  Size: 232 B

After

Width:  |  Height:  |  Size: 232 B

View File

Before

Width:  |  Height:  |  Size: 157 B

After

Width:  |  Height:  |  Size: 157 B

View File

Before

Width:  |  Height:  |  Size: 188 B

After

Width:  |  Height:  |  Size: 188 B

View File

Before

Width:  |  Height:  |  Size: 167 B

After

Width:  |  Height:  |  Size: 167 B

View File

Before

Width:  |  Height:  |  Size: 221 B

After

Width:  |  Height:  |  Size: 221 B

View File

Before

Width:  |  Height:  |  Size: 201 B

After

Width:  |  Height:  |  Size: 201 B

View File

Before

Width:  |  Height:  |  Size: 198 B

After

Width:  |  Height:  |  Size: 198 B

View File

Before

Width:  |  Height:  |  Size: 224 B

After

Width:  |  Height:  |  Size: 224 B

View File

Before

Width:  |  Height:  |  Size: 168 B

After

Width:  |  Height:  |  Size: 168 B

View File

Before

Width:  |  Height:  |  Size: 185 B

After

Width:  |  Height:  |  Size: 185 B

View File

Before

Width:  |  Height:  |  Size: 195 B

After

Width:  |  Height:  |  Size: 195 B

View File

Before

Width:  |  Height:  |  Size: 199 B

After

Width:  |  Height:  |  Size: 199 B

View File

Before

Width:  |  Height:  |  Size: 197 B

After

Width:  |  Height:  |  Size: 197 B

View File

Before

Width:  |  Height:  |  Size: 146 B

After

Width:  |  Height:  |  Size: 146 B

View File

Before

Width:  |  Height:  |  Size: 186 B

After

Width:  |  Height:  |  Size: 186 B

View File

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

Before

Width:  |  Height:  |  Size: 181 B

After

Width:  |  Height:  |  Size: 181 B

View File

Before

Width:  |  Height:  |  Size: 184 B

After

Width:  |  Height:  |  Size: 184 B

Some files were not shown because too many files have changed in this diff Show More