diff --git a/CLAUDE.md b/CLAUDE.md index 405a70f..5aa028a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -46,9 +46,14 @@ python generate_report.py --db pga.db --output report.html --top 10 --background **Style system (`styles.py`):** - CSS definitions for each modern style (brutalism, glassmorphism, neumorphism) -- Theme configurations (colors, fonts, chart options) injected via `__THEME_CSS__` and `__THEME_CONFIG__` +- 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, neumorphism) +- 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) diff --git a/generate_report.py b/generate_report.py index 906d565..8467f0d 100644 --- a/generate_report.py +++ b/generate_report.py @@ -36,6 +36,12 @@ def load_template(template_file: str) -> str: 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: """Load a file and return it as a base64 data URL.""" if path.exists(): @@ -117,10 +123,14 @@ def generate_report(db_path: str, output_path: str, top_n: int, assets_dir: str, 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("__THEME_CONFIG__", theme_config) - html = html.replace("__ALL_GAMES__", json.dumps(all_games)) - html = html.replace("__TOP_N__", str(top_n)) + 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) diff --git a/styles.py b/styles.py index 3002cec..81d8424 100644 --- a/styles.py +++ b/styles.py @@ -1,5 +1,19 @@ """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 diff --git a/templates/brutalism.css b/templates/brutalism.css index 8432b2f..e64c371 100644 --- a/templates/brutalism.css +++ b/templates/brutalism.css @@ -1,3 +1,17 @@ +/*************************************************************************************************** + * 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. * + ****************************************************************************************************/ + :root { --bg-base: #ffffff; --bg-primary: #ffffff; @@ -122,12 +136,12 @@ body { } .theme-toggle .icon-auto::before { - content: '\2600\FE0F'; + content: '☀️'; clip-path: inset(0 50% 0 0); } .theme-toggle .icon-auto::after { - content: '\D83C\DF19'; + content: '🌙'; clip-path: inset(0 0 0 50%); } diff --git a/templates/glassmorphism.css b/templates/glassmorphism.css index 7d9b469..b2494db 100644 --- a/templates/glassmorphism.css +++ b/templates/glassmorphism.css @@ -1,3 +1,17 @@ +/*************************************************************************************************** + * 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. * + ****************************************************************************************************/ + :root { --bg-primary: rgba(255, 255, 255, 0.25); --bg-secondary: rgba(255, 255, 255, 0.15); @@ -114,12 +128,12 @@ body { } .theme-toggle .icon-auto::before { - content: '\2600\FE0F'; + content: '☀️'; clip-path: inset(0 50% 0 0); } .theme-toggle .icon-auto::after { - content: '\D83C\DF19'; + content: '🌙'; clip-path: inset(0 0 0 50%); } diff --git a/templates/modern.html b/templates/modern.html index cdda685..9165db9 100644 --- a/templates/modern.html +++ b/templates/modern.html @@ -1,3 +1,16 @@ +
@@ -131,596 +144,7 @@ diff --git a/templates/neumorphism.css b/templates/neumorphism.css index 51644d3..3fe93e9 100644 --- a/templates/neumorphism.css +++ b/templates/neumorphism.css @@ -1,3 +1,17 @@ +/*************************************************************************************************** + * 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. * + ****************************************************************************************************/ + :root { --bg-base: #e0e5ec; --bg-primary: #e0e5ec; @@ -124,12 +138,12 @@ body { } .theme-toggle .icon-auto::before { - content: '\2600\FE0F'; + content: '☀️'; clip-path: inset(0 50% 0 0); } .theme-toggle .icon-auto::after { - content: '\D83C\DF19'; + content: '🌙'; clip-path: inset(0 0 0 50%); } diff --git a/templates/platinum.html b/templates/platinum.html index 6f2fb9d..d058c99 100644 --- a/templates/platinum.html +++ b/templates/platinum.html @@ -1,3 +1,16 @@ + diff --git a/templates/script.js b/templates/script.js new file mode 100644 index 0000000..3c04cb5 --- /dev/null +++ b/templates/script.js @@ -0,0 +1,604 @@ +/*************************************************************************************************** + * 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. * + ****************************************************************************************************/ + +// Theme management +const themeToggle = document.getElementById('theme-toggle'); +const themeIcon = document.getElementById('theme-icon'); +const themes = ['auto', 'light', 'dark']; +let currentThemeIndex = 0; + +function getSystemTheme() { + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; +} + +function updateThemeIcon() { + const theme = themes[currentThemeIndex]; + if (theme === 'auto') { + themeIcon.textContent = ''; + themeIcon.className = 'icon icon-auto'; + themeToggle.title = 'Theme: Auto (click to change)'; + } else if (theme === 'light') { + themeIcon.textContent = '\u2600\uFE0F'; + themeIcon.className = 'icon'; + themeToggle.title = 'Theme: Light (click to change)'; + } else { + themeIcon.textContent = '\uD83C\uDF19'; + themeIcon.className = 'icon'; + themeToggle.title = 'Theme: Dark (click to change)'; + } +} + +function applyTheme() { + const theme = themes[currentThemeIndex]; + if (theme === 'auto') { + document.documentElement.removeAttribute('data-theme'); + } else { + document.documentElement.setAttribute('data-theme', theme); + } + updateThemeIcon(); + localStorage.setItem('theme', theme); + updateChartColors(); +} + +function loadSavedTheme() { + const saved = localStorage.getItem('theme'); + if (saved) { + currentThemeIndex = themes.indexOf(saved); + if (currentThemeIndex === -1) currentThemeIndex = 0; + } + applyTheme(); +} + +themeToggle.addEventListener('click', () => { + currentThemeIndex = (currentThemeIndex + 1) % themes.length; + applyTheme(); +}); + +window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { + if (themes[currentThemeIndex] === 'auto') { + updateThemeIcon(); + updateChartColors(); + } +}); + +// Data and chart logic +const allGames = __ALL_GAMES__; +const topN = __TOP_N__; + +// Theme-specific configuration +const themeConfig = __THEME_CONFIG__; + +function formatTime(hours) { + const h = Math.floor(hours); + const m = Math.round((hours - h) * 60); + if (h === 0) return m + 'm'; + if (m === 0) return h + 'h'; + return h + 'h ' + m + 'm'; +} + +function getServices() { + const services = {}; + allGames.forEach(g => { + const s = g.service; + if (!services[s]) services[s] = { count: 0, playtime: 0 }; + services[s].count++; + services[s].playtime += g.playtime; + }); + return Object.entries(services) + .sort((a, b) => b[1].playtime - a[1].playtime) + .map(([name, data]) => ({ name, ...data })); +} + +const services = getServices(); +const filtersDiv = document.getElementById('filters'); +services.forEach(service => { + const label = document.createElement('label'); + label.className = 'filter-label'; + label.innerHTML = ` + + ${service.name} + (${service.count}) + `; + filtersDiv.appendChild(label); +}); + +let chart = null; +let categoriesChart = null; +const ctx = document.getElementById('playtime-chart').getContext('2d'); +const ctxCategories = document.getElementById('categories-chart').getContext('2d'); + +// Initialize theme after chart variables are declared +loadSavedTheme(); + +function getSelectedServices() { + const checkboxes = filtersDiv.querySelectorAll('input[type="checkbox"]'); + return Array.from(checkboxes) + .filter(cb => cb.checked) + .map(cb => cb.value); +} + +function getFilteredData(selectedServices) { + const filtered = allGames + .filter(g => selectedServices.includes(g.service)) + .sort((a, b) => b.playtime - a.playtime); + + if (filtered.length === 0) { + return { chartData: [], othersGames: [], categoriesData: [], totalPlaytime: 0, totalGames: 0 }; + } + + const totalPlaytime = filtered.reduce((sum, g) => sum + g.playtime, 0); + const totalGames = filtered.length; + + const topGames = filtered.slice(0, topN).map(g => ({ + name: g.name, + playtime: g.playtime, + service: g.service, + categories: g.categories || [] + })); + + let othersGames = []; + + if (filtered.length > topN) { + othersGames = filtered.slice(topN).map(g => ({ + name: g.name, + playtime: g.playtime, + service: g.service, + categories: g.categories || [] + })); + const othersPlaytime = othersGames.reduce((sum, g) => sum + g.playtime, 0); + const othersCount = othersGames.length; + topGames.push({ + name: `Others (${othersCount} games)`, + playtime: othersPlaytime, + service: 'others' + }); + } + + const categoryMap = {}; + filtered.forEach(g => { + if (g.categories && g.categories.length > 0) { + g.categories.forEach(cat => { + if (cat === '.hidden' || cat === 'favorite') return; + if (!categoryMap[cat]) { + categoryMap[cat] = { name: cat, playtime: 0, gameCount: 0 }; + } + categoryMap[cat].playtime += g.playtime; + categoryMap[cat].gameCount++; + }); + } + }); + const categoriesData = Object.values(categoryMap) + .sort((a, b) => b.playtime - a.playtime); + + const runnerMap = {}; + filtered.forEach(g => { + const runner = g.runner || 'unknown'; + if (!runnerMap[runner]) { + runnerMap[runner] = { name: runner, playtime: 0, gameCount: 0 }; + } + runnerMap[runner].playtime += g.playtime; + runnerMap[runner].gameCount++; + }); + const runnersData = Object.values(runnerMap) + .sort((a, b) => b.playtime - a.playtime); + + return { chartData: topGames, othersGames, categoriesData, runnersData, totalPlaytime, totalGames }; +} + +function getChartTextColor() { + const theme = themes[currentThemeIndex]; + if (theme === 'dark') return themeConfig.textColorDark; + if (theme === 'light') return themeConfig.textColorLight; + return getSystemTheme() === 'dark' ? themeConfig.textColorDark : themeConfig.textColorLight; +} + +function getChartBorderColor() { + const theme = themes[currentThemeIndex]; + if (theme === 'dark') return themeConfig.borderColorDark; + if (theme === 'light') return themeConfig.borderColorLight; + return getSystemTheme() === 'dark' ? themeConfig.borderColorDark : themeConfig.borderColorLight; +} + +function updateChartColors() { + const textColor = getChartTextColor(); + if (typeof chart !== 'undefined' && chart) { + chart.options.plugins.legend.labels.color = textColor; + chart.update(); + } + if (typeof categoriesChart !== 'undefined' && categoriesChart) { + categoriesChart.options.plugins.legend.labels.color = textColor; + categoriesChart.update(); + } +} + +function updateDisplay() { + const selectedServices = getSelectedServices(); + const { chartData, othersGames, categoriesData, runnersData, totalPlaytime, totalGames } = getFilteredData(selectedServices); + + document.getElementById('total-games').textContent = totalGames; + document.getElementById('total-time').textContent = formatTime(totalPlaytime); + + if (chart) { + chart.destroy(); + } + if (categoriesChart) { + categoriesChart.destroy(); + } + + if (chartData.length === 0) { + document.getElementById('games-table').innerHTML = + '