Compare commits
5 Commits
f850e4d69c
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa9719cbfe | ||
|
|
f4e5c33c87 | ||
|
|
db575f2bb7 | ||
|
|
3394d66151 | ||
|
|
3a700c4f48 |
21
CLAUDE.md
21
CLAUDE.md
@@ -15,7 +15,7 @@ python generate_report.py
|
|||||||
|
|
||||||
Generate report with custom options:
|
Generate report with custom options:
|
||||||
```bash
|
```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
|
## Architecture
|
||||||
@@ -23,12 +23,16 @@ python generate_report.py --db pga.db --output report.html --top 10 --background
|
|||||||
**Report generator (`generate_report.py`):**
|
**Report generator (`generate_report.py`):**
|
||||||
- Reads Lutris SQLite database (`pga.db`) containing games, categories, and playtime data
|
- 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
|
- 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`):**
|
**HTML templates (`templates/`):**
|
||||||
- Chart.js doughnut charts and dynamic JavaScript filtering
|
- **platinum.html**: Mac OS 9 Platinum visual style
|
||||||
- Mac OS 9 Platinum visual style with placeholder tokens for assets
|
- **brutalism.html**: Bold industrial brutalist design with hard shadows
|
||||||
- Tokens like `__ALL_GAMES__`, `__BACKGROUND_IMAGE__` are replaced at generation time
|
- **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`):**
|
**Database schema (`schema.py`):**
|
||||||
- Reference file documenting Lutris database structure
|
- 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
|
- Fully static, can be hosted on any web server
|
||||||
- Client-side filtering by service (Steam, GOG, itch.io, local)
|
- Client-side filtering by service (Steam, GOG, itch.io, local)
|
||||||
- Expandable "Others" row in games table
|
- 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
|
## Key Data Relationships
|
||||||
|
|
||||||
- Games have a `service` field (steam, gog, itchio, humblebundle, or NULL for local)
|
- Games have a `service` field (steam, gog, itchio, humblebundle, or NULL for local)
|
||||||
- Games link to categories via `games_categories` join table
|
- 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
|
- `playtime` is cumulative hours (REAL), not per-session data
|
||||||
|
|||||||
@@ -154,17 +154,6 @@
|
|||||||
background: var(--accent-tertiary);
|
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 {
|
.card-title {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
@@ -173,6 +162,16 @@
|
|||||||
color: var(--text-primary);
|
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 {
|
.card-content {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
@@ -448,6 +447,18 @@
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 0.5px;
|
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 {
|
.no-data {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -844,7 +855,8 @@
|
|||||||
const topGames = filtered.slice(0, topN).map(g => ({
|
const topGames = filtered.slice(0, topN).map(g => ({
|
||||||
name: g.name,
|
name: g.name,
|
||||||
playtime: g.playtime,
|
playtime: g.playtime,
|
||||||
service: g.service
|
service: g.service,
|
||||||
|
categories: g.categories || []
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let othersGames = [];
|
let othersGames = [];
|
||||||
@@ -853,7 +865,8 @@
|
|||||||
othersGames = filtered.slice(topN).map(g => ({
|
othersGames = filtered.slice(topN).map(g => ({
|
||||||
name: g.name,
|
name: g.name,
|
||||||
playtime: g.playtime,
|
playtime: g.playtime,
|
||||||
service: g.service
|
service: g.service,
|
||||||
|
categories: g.categories || []
|
||||||
}));
|
}));
|
||||||
const othersPlaytime = othersGames.reduce((sum, g) => sum + g.playtime, 0);
|
const othersPlaytime = othersGames.reduce((sum, g) => sum + g.playtime, 0);
|
||||||
const othersCount = othersGames.length;
|
const othersCount = othersGames.length;
|
||||||
@@ -868,7 +881,7 @@
|
|||||||
filtered.forEach(g => {
|
filtered.forEach(g => {
|
||||||
if (g.categories && g.categories.length > 0) {
|
if (g.categories && g.categories.length > 0) {
|
||||||
g.categories.forEach(cat => {
|
g.categories.forEach(cat => {
|
||||||
if (cat === '.hidden' || cat === 'favorite' || cat === 'Horny') return;
|
if (cat === '.hidden' || cat === 'favorite') return;
|
||||||
if (!categoryMap[cat]) {
|
if (!categoryMap[cat]) {
|
||||||
categoryMap[cat] = { name: cat, playtime: 0, gameCount: 0 };
|
categoryMap[cat] = { name: cat, playtime: 0, gameCount: 0 };
|
||||||
}
|
}
|
||||||
@@ -1069,6 +1082,12 @@
|
|||||||
const serviceBadge = !isOthers
|
const serviceBadge = !isOthers
|
||||||
? `<span class="service-badge">${game.service}</span>`
|
? `<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');
|
const row = document.createElement('tr');
|
||||||
if (isOthers) {
|
if (isOthers) {
|
||||||
row.className = 'others-row';
|
row.className = 'others-row';
|
||||||
@@ -1077,7 +1096,7 @@
|
|||||||
<td>${index + 1}</td>
|
<td>${index + 1}</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="color-box" style="background: ${colors[index]}"></span>
|
<span class="color-box" style="background: ${colors[index]}"></span>
|
||||||
${game.name}${serviceBadge}
|
${game.name}${serviceBadge}${categoriesBadges}
|
||||||
</td>
|
</td>
|
||||||
<td class="time">${formatTime(game.playtime)}</td>
|
<td class="time">${formatTime(game.playtime)}</td>
|
||||||
<td class="percent">${percent}%</td>
|
<td class="percent">${percent}%</td>
|
||||||
@@ -1088,13 +1107,19 @@
|
|||||||
const detailRows = [];
|
const detailRows = [];
|
||||||
othersGames.forEach((otherGame, otherIndex) => {
|
othersGames.forEach((otherGame, otherIndex) => {
|
||||||
const otherPercent = ((otherGame.playtime / totalPlaytime) * 100).toFixed(1);
|
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');
|
const detailRow = document.createElement('tr');
|
||||||
detailRow.className = 'others-detail';
|
detailRow.className = 'others-detail';
|
||||||
detailRow.innerHTML = `
|
detailRow.innerHTML = `
|
||||||
<td>${index + 1}.${otherIndex + 1}</td>
|
<td>${index + 1}.${otherIndex + 1}</td>
|
||||||
<td>
|
<td>
|
||||||
${otherGame.name}
|
${otherGame.name}
|
||||||
<span class="service-badge">${otherGame.service}</span>
|
<span class="service-badge">${otherGame.service}</span>${otherCategoriesBadges}
|
||||||
</td>
|
</td>
|
||||||
<td class="time">${formatTime(otherGame.playtime)}</td>
|
<td class="time">${formatTime(otherGame.playtime)}</td>
|
||||||
<td class="percent">${otherPercent}%</td>
|
<td class="percent">${otherPercent}%</td>
|
||||||
|
|||||||
@@ -413,6 +413,17 @@
|
|||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
font-weight: 500;
|
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 {
|
.no-data {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -808,7 +819,8 @@
|
|||||||
const topGames = filtered.slice(0, topN).map(g => ({
|
const topGames = filtered.slice(0, topN).map(g => ({
|
||||||
name: g.name,
|
name: g.name,
|
||||||
playtime: g.playtime,
|
playtime: g.playtime,
|
||||||
service: g.service
|
service: g.service,
|
||||||
|
categories: g.categories || []
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let othersGames = [];
|
let othersGames = [];
|
||||||
@@ -817,7 +829,8 @@
|
|||||||
othersGames = filtered.slice(topN).map(g => ({
|
othersGames = filtered.slice(topN).map(g => ({
|
||||||
name: g.name,
|
name: g.name,
|
||||||
playtime: g.playtime,
|
playtime: g.playtime,
|
||||||
service: g.service
|
service: g.service,
|
||||||
|
categories: g.categories || []
|
||||||
}));
|
}));
|
||||||
const othersPlaytime = othersGames.reduce((sum, g) => sum + g.playtime, 0);
|
const othersPlaytime = othersGames.reduce((sum, g) => sum + g.playtime, 0);
|
||||||
const othersCount = othersGames.length;
|
const othersCount = othersGames.length;
|
||||||
@@ -832,7 +845,7 @@
|
|||||||
filtered.forEach(g => {
|
filtered.forEach(g => {
|
||||||
if (g.categories && g.categories.length > 0) {
|
if (g.categories && g.categories.length > 0) {
|
||||||
g.categories.forEach(cat => {
|
g.categories.forEach(cat => {
|
||||||
if (cat === '.hidden' || cat === 'favorite' || cat === 'Horny') return;
|
if (cat === '.hidden' || cat === 'favorite') return;
|
||||||
if (!categoryMap[cat]) {
|
if (!categoryMap[cat]) {
|
||||||
categoryMap[cat] = { name: cat, playtime: 0, gameCount: 0 };
|
categoryMap[cat] = { name: cat, playtime: 0, gameCount: 0 };
|
||||||
}
|
}
|
||||||
@@ -1013,6 +1026,12 @@
|
|||||||
const serviceBadge = !isOthers
|
const serviceBadge = !isOthers
|
||||||
? `<span class="service-badge">${game.service}</span>`
|
? `<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');
|
const row = document.createElement('tr');
|
||||||
if (isOthers) {
|
if (isOthers) {
|
||||||
row.className = 'others-row';
|
row.className = 'others-row';
|
||||||
@@ -1021,7 +1040,7 @@
|
|||||||
<td>${index + 1}</td>
|
<td>${index + 1}</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="color-box" style="background: ${colors[index]}"></span>
|
<span class="color-box" style="background: ${colors[index]}"></span>
|
||||||
${game.name}${serviceBadge}
|
${game.name}${serviceBadge}${categoriesBadges}
|
||||||
</td>
|
</td>
|
||||||
<td class="time">${formatTime(game.playtime)}</td>
|
<td class="time">${formatTime(game.playtime)}</td>
|
||||||
<td class="percent">${percent}%</td>
|
<td class="percent">${percent}%</td>
|
||||||
@@ -1032,13 +1051,19 @@
|
|||||||
const detailRows = [];
|
const detailRows = [];
|
||||||
othersGames.forEach((otherGame, otherIndex) => {
|
othersGames.forEach((otherGame, otherIndex) => {
|
||||||
const otherPercent = ((otherGame.playtime / totalPlaytime) * 100).toFixed(1);
|
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');
|
const detailRow = document.createElement('tr');
|
||||||
detailRow.className = 'others-detail';
|
detailRow.className = 'others-detail';
|
||||||
detailRow.innerHTML = `
|
detailRow.innerHTML = `
|
||||||
<td>${index + 1}.${otherIndex + 1}</td>
|
<td>${index + 1}.${otherIndex + 1}</td>
|
||||||
<td>
|
<td>
|
||||||
${otherGame.name}
|
${otherGame.name}
|
||||||
<span class="service-badge">${otherGame.service}</span>
|
<span class="service-badge">${otherGame.service}</span>${otherCategoriesBadges}
|
||||||
</td>
|
</td>
|
||||||
<td class="time">${formatTime(otherGame.playtime)}</td>
|
<td class="time">${formatTime(otherGame.playtime)}</td>
|
||||||
<td class="percent">${otherPercent}%</td>
|
<td class="percent">${otherPercent}%</td>
|
||||||
|
|||||||
@@ -445,6 +445,20 @@
|
|||||||
inset 1px 1px 2px var(--shadow-inset-dark),
|
inset 1px 1px 2px var(--shadow-inset-dark),
|
||||||
inset -1px -1px 2px var(--shadow-inset-light);
|
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 {
|
.no-data {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -845,7 +859,8 @@
|
|||||||
const topGames = filtered.slice(0, topN).map(g => ({
|
const topGames = filtered.slice(0, topN).map(g => ({
|
||||||
name: g.name,
|
name: g.name,
|
||||||
playtime: g.playtime,
|
playtime: g.playtime,
|
||||||
service: g.service
|
service: g.service,
|
||||||
|
categories: g.categories || []
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let othersGames = [];
|
let othersGames = [];
|
||||||
@@ -854,7 +869,8 @@
|
|||||||
othersGames = filtered.slice(topN).map(g => ({
|
othersGames = filtered.slice(topN).map(g => ({
|
||||||
name: g.name,
|
name: g.name,
|
||||||
playtime: g.playtime,
|
playtime: g.playtime,
|
||||||
service: g.service
|
service: g.service,
|
||||||
|
categories: g.categories || []
|
||||||
}));
|
}));
|
||||||
const othersPlaytime = othersGames.reduce((sum, g) => sum + g.playtime, 0);
|
const othersPlaytime = othersGames.reduce((sum, g) => sum + g.playtime, 0);
|
||||||
const othersCount = othersGames.length;
|
const othersCount = othersGames.length;
|
||||||
@@ -869,7 +885,7 @@
|
|||||||
filtered.forEach(g => {
|
filtered.forEach(g => {
|
||||||
if (g.categories && g.categories.length > 0) {
|
if (g.categories && g.categories.length > 0) {
|
||||||
g.categories.forEach(cat => {
|
g.categories.forEach(cat => {
|
||||||
if (cat === '.hidden' || cat === 'favorite' || cat === 'Horny') return;
|
if (cat === '.hidden' || cat === 'favorite') return;
|
||||||
if (!categoryMap[cat]) {
|
if (!categoryMap[cat]) {
|
||||||
categoryMap[cat] = { name: cat, playtime: 0, gameCount: 0 };
|
categoryMap[cat] = { name: cat, playtime: 0, gameCount: 0 };
|
||||||
}
|
}
|
||||||
@@ -1050,6 +1066,12 @@
|
|||||||
const serviceBadge = !isOthers
|
const serviceBadge = !isOthers
|
||||||
? `<span class="service-badge">${game.service}</span>`
|
? `<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');
|
const row = document.createElement('tr');
|
||||||
if (isOthers) {
|
if (isOthers) {
|
||||||
row.className = 'others-row';
|
row.className = 'others-row';
|
||||||
@@ -1058,7 +1080,7 @@
|
|||||||
<td>${index + 1}</td>
|
<td>${index + 1}</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="color-box" style="background: ${colors[index]}"></span>
|
<span class="color-box" style="background: ${colors[index]}"></span>
|
||||||
${game.name}${serviceBadge}
|
${game.name}${serviceBadge}${categoriesBadges}
|
||||||
</td>
|
</td>
|
||||||
<td class="time">${formatTime(game.playtime)}</td>
|
<td class="time">${formatTime(game.playtime)}</td>
|
||||||
<td class="percent">${percent}%</td>
|
<td class="percent">${percent}%</td>
|
||||||
@@ -1069,13 +1091,19 @@
|
|||||||
const detailRows = [];
|
const detailRows = [];
|
||||||
othersGames.forEach((otherGame, otherIndex) => {
|
othersGames.forEach((otherGame, otherIndex) => {
|
||||||
const otherPercent = ((otherGame.playtime / totalPlaytime) * 100).toFixed(1);
|
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');
|
const detailRow = document.createElement('tr');
|
||||||
detailRow.className = 'others-detail';
|
detailRow.className = 'others-detail';
|
||||||
detailRow.innerHTML = `
|
detailRow.innerHTML = `
|
||||||
<td>${index + 1}.${otherIndex + 1}</td>
|
<td>${index + 1}.${otherIndex + 1}</td>
|
||||||
<td>
|
<td>
|
||||||
${otherGame.name}
|
${otherGame.name}
|
||||||
<span class="service-badge">${otherGame.service}</span>
|
<span class="service-badge">${otherGame.service}</span>${otherCategoriesBadges}
|
||||||
</td>
|
</td>
|
||||||
<td class="time">${formatTime(otherGame.playtime)}</td>
|
<td class="time">${formatTime(otherGame.playtime)}</td>
|
||||||
<td class="percent">${otherPercent}%</td>
|
<td class="percent">${otherPercent}%</td>
|
||||||
|
|||||||
@@ -299,6 +299,16 @@
|
|||||||
margin-left: 6px;
|
margin-left: 6px;
|
||||||
text-transform: capitalize;
|
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 {
|
.no-data {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
@@ -680,7 +690,8 @@
|
|||||||
const topGames = filtered.slice(0, topN).map(g => ({
|
const topGames = filtered.slice(0, topN).map(g => ({
|
||||||
name: g.name,
|
name: g.name,
|
||||||
playtime: g.playtime,
|
playtime: g.playtime,
|
||||||
service: g.service
|
service: g.service,
|
||||||
|
categories: g.categories || []
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let othersGames = [];
|
let othersGames = [];
|
||||||
@@ -689,7 +700,8 @@
|
|||||||
othersGames = filtered.slice(topN).map(g => ({
|
othersGames = filtered.slice(topN).map(g => ({
|
||||||
name: g.name,
|
name: g.name,
|
||||||
playtime: g.playtime,
|
playtime: g.playtime,
|
||||||
service: g.service
|
service: g.service,
|
||||||
|
categories: g.categories || []
|
||||||
}));
|
}));
|
||||||
const othersPlaytime = othersGames.reduce((sum, g) => sum + g.playtime, 0);
|
const othersPlaytime = othersGames.reduce((sum, g) => sum + g.playtime, 0);
|
||||||
const othersCount = othersGames.length;
|
const othersCount = othersGames.length;
|
||||||
@@ -704,7 +716,7 @@
|
|||||||
filtered.forEach(g => {
|
filtered.forEach(g => {
|
||||||
if (g.categories && g.categories.length > 0) {
|
if (g.categories && g.categories.length > 0) {
|
||||||
g.categories.forEach(cat => {
|
g.categories.forEach(cat => {
|
||||||
if (cat === '.hidden' || cat === 'favorite' || cat === 'Horny') return;
|
if (cat === '.hidden' || cat === 'favorite') return;
|
||||||
if (!categoryMap[cat]) {
|
if (!categoryMap[cat]) {
|
||||||
categoryMap[cat] = { name: cat, playtime: 0, gameCount: 0 };
|
categoryMap[cat] = { name: cat, playtime: 0, gameCount: 0 };
|
||||||
}
|
}
|
||||||
@@ -859,6 +871,12 @@
|
|||||||
const serviceBadge = !isOthers
|
const serviceBadge = !isOthers
|
||||||
? `<span class="service-badge">${game.service}</span>`
|
? `<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');
|
const row = document.createElement('tr');
|
||||||
if (isOthers) {
|
if (isOthers) {
|
||||||
row.className = 'others-row';
|
row.className = 'others-row';
|
||||||
@@ -867,7 +885,7 @@
|
|||||||
<td>${index + 1}</td>
|
<td>${index + 1}</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="color-box" style="background: ${colors[index]}"></span>
|
<span class="color-box" style="background: ${colors[index]}"></span>
|
||||||
${game.name}${serviceBadge}
|
${game.name}${serviceBadge}${categoriesBadges}
|
||||||
</td>
|
</td>
|
||||||
<td class="time">${formatTime(game.playtime)}</td>
|
<td class="time">${formatTime(game.playtime)}</td>
|
||||||
<td class="percent">${percent}%</td>
|
<td class="percent">${percent}%</td>
|
||||||
@@ -878,13 +896,19 @@
|
|||||||
const detailRows = [];
|
const detailRows = [];
|
||||||
othersGames.forEach((otherGame, otherIndex) => {
|
othersGames.forEach((otherGame, otherIndex) => {
|
||||||
const otherPercent = ((otherGame.playtime / totalPlaytime) * 100).toFixed(1);
|
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');
|
const detailRow = document.createElement('tr');
|
||||||
detailRow.className = 'others-detail';
|
detailRow.className = 'others-detail';
|
||||||
detailRow.innerHTML = `
|
detailRow.innerHTML = `
|
||||||
<td>${index + 1}.${otherIndex + 1}</td>
|
<td>${index + 1}.${otherIndex + 1}</td>
|
||||||
<td>
|
<td>
|
||||||
${otherGame.name}
|
${otherGame.name}
|
||||||
<span class="service-badge">${otherGame.service}</span>
|
<span class="service-badge">${otherGame.service}</span>${otherCategoriesBadges}
|
||||||
</td>
|
</td>
|
||||||
<td class="time">${formatTime(otherGame.playtime)}</td>
|
<td class="time">${formatTime(otherGame.playtime)}</td>
|
||||||
<td class="percent">${otherPercent}%</td>
|
<td class="percent">${otherPercent}%</td>
|
||||||
|
|||||||
Reference in New Issue
Block a user