Compare commits

..

5 Commits

Author SHA1 Message Date
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
5 changed files with 146 additions and 39 deletions

View File

@@ -15,7 +15,7 @@ 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
python generate_report.py --db pga.db --output report.html --top 10 --background background.png --template templates/platinum.html
```
## Architecture
@@ -23,12 +23,16 @@ python generate_report.py --db pga.db --output report.html --top 10 --background
**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`)
- Loads HTML template from `templates/` folder (default: `templates/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
**HTML templates (`templates/`):**
- **platinum.html**: Mac OS 9 Platinum visual style
- **brutalism.html**: Bold industrial brutalist design with hard shadows
- **glassmorphism.html**: Modern frosted glass effect
- **neumorphism.html**: Soft 3D neumorphic style
- All templates use Chart.js doughnut charts and dynamic JavaScript filtering
- Placeholder tokens like `__ALL_GAMES__`, `__BACKGROUND_IMAGE__` are replaced at generation time
- Support light/dark mode with theme toggle button
**Database schema (`schema.py`):**
- Reference file documenting Lutris database structure
@@ -38,11 +42,12 @@ python generate_report.py --db pga.db --output report.html --top 10 --background
- 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`
- 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`, `favorite`, `Horny` are filtered out in the report
- Categories like `.hidden` and `favorite` are filtered out in the report display
- `playtime` is cumulative hours (REAL), not per-session data

View File

@@ -154,17 +154,6 @@
background: var(--accent-tertiary);
}
[data-theme="dark"] .card-header,
.dark-theme .card-header {
background: var(--bg-tertiary);
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) .card-header {
background: var(--bg-tertiary);
}
}
.card-title {
font-size: 18px;
font-weight: 900;
@@ -173,6 +162,16 @@
color: var(--text-primary);
}
[data-theme="dark"] .card-title {
color: #000000;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) .card-title {
color: #000000;
}
}
.card-content {
padding: 20px;
}
@@ -448,6 +447,18 @@
font-weight: 700;
letter-spacing: 0.5px;
}
.category-badge {
display: inline-block;
font-size: 10px;
padding: 2px 6px;
background: var(--accent-secondary);
border: 1px solid var(--border-color);
color: var(--bg-primary);
margin-left: 4px;
text-transform: uppercase;
font-weight: 700;
letter-spacing: 0.5px;
}
.no-data {
text-align: center;
@@ -844,7 +855,8 @@
const topGames = filtered.slice(0, topN).map(g => ({
name: g.name,
playtime: g.playtime,
service: g.service
service: g.service,
categories: g.categories || []
}));
let othersGames = [];
@@ -853,7 +865,8 @@
othersGames = filtered.slice(topN).map(g => ({
name: g.name,
playtime: g.playtime,
service: g.service
service: g.service,
categories: g.categories || []
}));
const othersPlaytime = othersGames.reduce((sum, g) => sum + g.playtime, 0);
const othersCount = othersGames.length;
@@ -868,7 +881,7 @@
filtered.forEach(g => {
if (g.categories && g.categories.length > 0) {
g.categories.forEach(cat => {
if (cat === '.hidden' || cat === 'favorite' || cat === 'Horny') return;
if (cat === '.hidden' || cat === 'favorite') return;
if (!categoryMap[cat]) {
categoryMap[cat] = { name: cat, playtime: 0, gameCount: 0 };
}
@@ -1069,6 +1082,12 @@
const serviceBadge = !isOthers
? `<span class="service-badge">${game.service}</span>`
: '';
const categoriesBadges = !isOthers && game.categories && game.categories.length > 0
? game.categories
.filter(cat => cat !== '.hidden' && cat !== 'favorite')
.map(cat => `<span class="category-badge">${cat}</span>`)
.join('')
: '';
const row = document.createElement('tr');
if (isOthers) {
row.className = 'others-row';
@@ -1077,7 +1096,7 @@
<td>${index + 1}</td>
<td>
<span class="color-box" style="background: ${colors[index]}"></span>
${game.name}${serviceBadge}
${game.name}${serviceBadge}${categoriesBadges}
</td>
<td class="time">${formatTime(game.playtime)}</td>
<td class="percent">${percent}%</td>
@@ -1088,13 +1107,19 @@
const detailRows = [];
othersGames.forEach((otherGame, otherIndex) => {
const otherPercent = ((otherGame.playtime / totalPlaytime) * 100).toFixed(1);
const otherCategoriesBadges = otherGame.categories && otherGame.categories.length > 0
? otherGame.categories
.filter(cat => cat !== '.hidden' && cat !== 'favorite')
.map(cat => `<span class="category-badge">${cat}</span>`)
.join('')
: '';
const detailRow = document.createElement('tr');
detailRow.className = 'others-detail';
detailRow.innerHTML = `
<td>${index + 1}.${otherIndex + 1}</td>
<td>
${otherGame.name}
<span class="service-badge">${otherGame.service}</span>
<span class="service-badge">${otherGame.service}</span>${otherCategoriesBadges}
</td>
<td class="time">${formatTime(otherGame.playtime)}</td>
<td class="percent">${otherPercent}%</td>

View File

@@ -413,6 +413,17 @@
text-transform: capitalize;
font-weight: 500;
}
.category-badge {
display: inline-block;
font-size: 10px;
padding: 2px 8px;
background: rgba(99, 102, 241, 0.2);
border-radius: 12px;
color: var(--accent-color);
margin-left: 4px;
text-transform: capitalize;
font-weight: 500;
}
.no-data {
text-align: center;
@@ -808,7 +819,8 @@
const topGames = filtered.slice(0, topN).map(g => ({
name: g.name,
playtime: g.playtime,
service: g.service
service: g.service,
categories: g.categories || []
}));
let othersGames = [];
@@ -817,7 +829,8 @@
othersGames = filtered.slice(topN).map(g => ({
name: g.name,
playtime: g.playtime,
service: g.service
service: g.service,
categories: g.categories || []
}));
const othersPlaytime = othersGames.reduce((sum, g) => sum + g.playtime, 0);
const othersCount = othersGames.length;
@@ -832,7 +845,7 @@
filtered.forEach(g => {
if (g.categories && g.categories.length > 0) {
g.categories.forEach(cat => {
if (cat === '.hidden' || cat === 'favorite' || cat === 'Horny') return;
if (cat === '.hidden' || cat === 'favorite') return;
if (!categoryMap[cat]) {
categoryMap[cat] = { name: cat, playtime: 0, gameCount: 0 };
}
@@ -1013,6 +1026,12 @@
const serviceBadge = !isOthers
? `<span class="service-badge">${game.service}</span>`
: '';
const categoriesBadges = !isOthers && game.categories && game.categories.length > 0
? game.categories
.filter(cat => cat !== '.hidden' && cat !== 'favorite')
.map(cat => `<span class="category-badge">${cat}</span>`)
.join('')
: '';
const row = document.createElement('tr');
if (isOthers) {
row.className = 'others-row';
@@ -1021,7 +1040,7 @@
<td>${index + 1}</td>
<td>
<span class="color-box" style="background: ${colors[index]}"></span>
${game.name}${serviceBadge}
${game.name}${serviceBadge}${categoriesBadges}
</td>
<td class="time">${formatTime(game.playtime)}</td>
<td class="percent">${percent}%</td>
@@ -1032,13 +1051,19 @@
const detailRows = [];
othersGames.forEach((otherGame, otherIndex) => {
const otherPercent = ((otherGame.playtime / totalPlaytime) * 100).toFixed(1);
const otherCategoriesBadges = otherGame.categories && otherGame.categories.length > 0
? otherGame.categories
.filter(cat => cat !== '.hidden' && cat !== 'favorite')
.map(cat => `<span class="category-badge">${cat}</span>`)
.join('')
: '';
const detailRow = document.createElement('tr');
detailRow.className = 'others-detail';
detailRow.innerHTML = `
<td>${index + 1}.${otherIndex + 1}</td>
<td>
${otherGame.name}
<span class="service-badge">${otherGame.service}</span>
<span class="service-badge">${otherGame.service}</span>${otherCategoriesBadges}
</td>
<td class="time">${formatTime(otherGame.playtime)}</td>
<td class="percent">${otherPercent}%</td>

View File

@@ -445,6 +445,20 @@
inset 1px 1px 2px var(--shadow-inset-dark),
inset -1px -1px 2px var(--shadow-inset-light);
}
.category-badge {
display: inline-block;
font-size: 10px;
padding: 3px 10px;
background: var(--bg-tertiary);
border-radius: 12px;
color: var(--accent-color);
margin-left: 4px;
text-transform: capitalize;
font-weight: 500;
box-shadow:
inset 1px 1px 2px var(--shadow-inset-dark),
inset -1px -1px 2px var(--shadow-inset-light);
}
.no-data {
text-align: center;
@@ -845,7 +859,8 @@
const topGames = filtered.slice(0, topN).map(g => ({
name: g.name,
playtime: g.playtime,
service: g.service
service: g.service,
categories: g.categories || []
}));
let othersGames = [];
@@ -854,7 +869,8 @@
othersGames = filtered.slice(topN).map(g => ({
name: g.name,
playtime: g.playtime,
service: g.service
service: g.service,
categories: g.categories || []
}));
const othersPlaytime = othersGames.reduce((sum, g) => sum + g.playtime, 0);
const othersCount = othersGames.length;
@@ -869,7 +885,7 @@
filtered.forEach(g => {
if (g.categories && g.categories.length > 0) {
g.categories.forEach(cat => {
if (cat === '.hidden' || cat === 'favorite' || cat === 'Horny') return;
if (cat === '.hidden' || cat === 'favorite') return;
if (!categoryMap[cat]) {
categoryMap[cat] = { name: cat, playtime: 0, gameCount: 0 };
}
@@ -1050,6 +1066,12 @@
const serviceBadge = !isOthers
? `<span class="service-badge">${game.service}</span>`
: '';
const categoriesBadges = !isOthers && game.categories && game.categories.length > 0
? game.categories
.filter(cat => cat !== '.hidden' && cat !== 'favorite')
.map(cat => `<span class="category-badge">${cat}</span>`)
.join('')
: '';
const row = document.createElement('tr');
if (isOthers) {
row.className = 'others-row';
@@ -1058,7 +1080,7 @@
<td>${index + 1}</td>
<td>
<span class="color-box" style="background: ${colors[index]}"></span>
${game.name}${serviceBadge}
${game.name}${serviceBadge}${categoriesBadges}
</td>
<td class="time">${formatTime(game.playtime)}</td>
<td class="percent">${percent}%</td>
@@ -1069,13 +1091,19 @@
const detailRows = [];
othersGames.forEach((otherGame, otherIndex) => {
const otherPercent = ((otherGame.playtime / totalPlaytime) * 100).toFixed(1);
const otherCategoriesBadges = otherGame.categories && otherGame.categories.length > 0
? otherGame.categories
.filter(cat => cat !== '.hidden' && cat !== 'favorite')
.map(cat => `<span class="category-badge">${cat}</span>`)
.join('')
: '';
const detailRow = document.createElement('tr');
detailRow.className = 'others-detail';
detailRow.innerHTML = `
<td>${index + 1}.${otherIndex + 1}</td>
<td>
${otherGame.name}
<span class="service-badge">${otherGame.service}</span>
<span class="service-badge">${otherGame.service}</span>${otherCategoriesBadges}
</td>
<td class="time">${formatTime(otherGame.playtime)}</td>
<td class="percent">${otherPercent}%</td>

View File

@@ -299,6 +299,16 @@
margin-left: 6px;
text-transform: capitalize;
}
.category-badge {
display: inline-block;
font-size: 9px;
padding: 1px 4px;
background: #D4E4FF;
border: 1px solid #A8C8FF;
color: #2d5a9f;
margin-left: 4px;
text-transform: capitalize;
}
.no-data {
text-align: center;
padding: 20px;
@@ -680,7 +690,8 @@
const topGames = filtered.slice(0, topN).map(g => ({
name: g.name,
playtime: g.playtime,
service: g.service
service: g.service,
categories: g.categories || []
}));
let othersGames = [];
@@ -689,7 +700,8 @@
othersGames = filtered.slice(topN).map(g => ({
name: g.name,
playtime: g.playtime,
service: g.service
service: g.service,
categories: g.categories || []
}));
const othersPlaytime = othersGames.reduce((sum, g) => sum + g.playtime, 0);
const othersCount = othersGames.length;
@@ -704,7 +716,7 @@
filtered.forEach(g => {
if (g.categories && g.categories.length > 0) {
g.categories.forEach(cat => {
if (cat === '.hidden' || cat === 'favorite' || cat === 'Horny') return;
if (cat === '.hidden' || cat === 'favorite') return;
if (!categoryMap[cat]) {
categoryMap[cat] = { name: cat, playtime: 0, gameCount: 0 };
}
@@ -859,6 +871,12 @@
const serviceBadge = !isOthers
? `<span class="service-badge">${game.service}</span>`
: '';
const categoriesBadges = !isOthers && game.categories && game.categories.length > 0
? game.categories
.filter(cat => cat !== '.hidden' && cat !== 'favorite')
.map(cat => `<span class="category-badge">${cat}</span>`)
.join('')
: '';
const row = document.createElement('tr');
if (isOthers) {
row.className = 'others-row';
@@ -867,7 +885,7 @@
<td>${index + 1}</td>
<td>
<span class="color-box" style="background: ${colors[index]}"></span>
${game.name}${serviceBadge}
${game.name}${serviceBadge}${categoriesBadges}
</td>
<td class="time">${formatTime(game.playtime)}</td>
<td class="percent">${percent}%</td>
@@ -878,13 +896,19 @@
const detailRows = [];
othersGames.forEach((otherGame, otherIndex) => {
const otherPercent = ((otherGame.playtime / totalPlaytime) * 100).toFixed(1);
const otherCategoriesBadges = otherGame.categories && otherGame.categories.length > 0
? otherGame.categories
.filter(cat => cat !== '.hidden' && cat !== 'favorite')
.map(cat => `<span class="category-badge">${cat}</span>`)
.join('')
: '';
const detailRow = document.createElement('tr');
detailRow.className = 'others-detail';
detailRow.innerHTML = `
<td>${index + 1}.${otherIndex + 1}</td>
<td>
${otherGame.name}
<span class="service-badge">${otherGame.service}</span>
<span class="service-badge">${otherGame.service}</span>${otherCategoriesBadges}
</td>
<td class="time">${formatTime(otherGame.playtime)}</td>
<td class="percent">${otherPercent}%</td>