Files
Lutris-Playtime-Report-Gene…/templates/platinum.html
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

1030 lines
38 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lutris Playtime Report</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
@font-face {
font-family: 'Charcoal';
src: url('__FONT_CHARCOAL__') format('truetype');
}
@font-face {
font-family: 'Monaco';
src: url('__FONT_MONACO__') format('truetype');
}
:root {
--mac-bg: #DDDDDD;
--mac-window-bg: #DDDDDD;
--mac-border-dark: #888888;
--mac-border-light: #FFFFFF;
--mac-text: #000000;
--mac-text-muted: #555555;
--mac-selection: #316AC5;
--mac-selection-text: #FFFFFF;
}
* {
box-sizing: border-box;
}
body {
font-family: 'Charcoal', 'Chicago', Geneva, sans-serif;
font-size: 12px;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background-color: var(--mac-bg);
background-image: url('__BACKGROUND_IMAGE__');
background-repeat: repeat;
min-height: 100vh;
color: var(--mac-text);
}
h1 {
font-family: 'Charcoal', 'Chicago', Geneva, sans-serif;
text-align: center;
font-size: 14px;
font-weight: bold;
margin: 0;
color: var(--mac-text);
}
h2 {
font-family: 'Charcoal', 'Chicago', Geneva, sans-serif;
font-size: 12px;
font-weight: bold;
margin: 0 0 10px 0;
color: var(--mac-text);
}
/* Mac OS 9 Window Style */
.window {
background: var(--mac-window-bg);
border-style: solid;
border-color: #000000;
border-width: 1px 2px 2px 1px;
box-shadow:
inset -1px -1px 0 var(--mac-border-dark),
inset 1px 1px 0 var(--mac-border-light);
margin-bottom: 20px;
}
.window-titlebar {
background: #BBBBBB;
background-image: url('__TITLEBAR_BG__');
background-repeat: repeat-x;
height: 22px;
display: flex;
align-items: center;
padding: 0 5px;
border-bottom: 1px solid var(--mac-border-dark);
position: relative;
}
.window-titlebar::before {
content: '';
position: absolute;
left: 25px;
right: 25px;
top: 4px;
bottom: 4px;
background-image: url('__TITLE_STRIPES__');
background-repeat: repeat-x;
background-position: center;
}
.window-close {
width: 13px;
height: 13px;
background-image: url('__CLOSE_BTN__');
background-size: contain;
background-repeat: no-repeat;
flex-shrink: 0;
position: relative;
z-index: 1;
}
.window-hide {
width: 13px;
height: 13px;
background-image: url('__HIDE_BTN__');
background-size: contain;
background-repeat: no-repeat;
flex-shrink: 0;
position: relative;
z-index: 1;
}
.window-shade {
width: 13px;
height: 13px;
background-image: url('__SHADE_BTN__');
background-size: contain;
background-repeat: no-repeat;
flex-shrink: 0;
position: relative;
z-index: 1;
margin-left: 4px;
}
.window-title {
flex: 1;
text-align: center;
font-family: 'Charcoal', 'Chicago', Geneva, sans-serif;
font-size: 12px;
font-weight: bold;
color: var(--mac-text);
position: relative;
z-index: 1;
background: #BBBBBB;
padding: 0 8px;
margin: 0 auto;
max-width: fit-content;
}
.window-content {
padding: 15px;
}
/* Filters styled as Mac checkboxes */
.filters {
display: flex;
justify-content: center;
gap: 15px;
flex-wrap: wrap;
}
.filter-label {
display: flex;
align-items: center;
gap: 4px;
cursor: pointer;
user-select: none;
font-family: 'Charcoal', 'Chicago', Geneva, sans-serif;
font-size: 12px;
}
.filter-label input[type="checkbox"] {
appearance: none;
-webkit-appearance: none;
width: 12px;
height: 12px;
background-image: url('__CHECK_OFF__');
background-size: contain;
background-repeat: no-repeat;
cursor: pointer;
margin: 0;
}
.filter-label input[type="checkbox"]:checked {
background-image: url('__CHECK_ON__');
}
.filter-label .service-name {
text-transform: capitalize;
}
.filter-label .service-count {
color: var(--mac-text-muted);
}
/* Stats in embossed boxes */
.stats {
display: flex;
justify-content: center;
gap: 30px;
flex-wrap: wrap;
}
.stat {
text-align: center;
padding: 10px 20px;
background: var(--mac-window-bg);
border: 1px solid;
border-color: var(--mac-border-dark) var(--mac-border-light) var(--mac-border-light) var(--mac-border-dark);
box-shadow: inset 1px 1px 0 var(--mac-border-dark), inset -1px -1px 0 var(--mac-border-light);
}
.stat-value {
font-family: 'Monaco', 'Courier New', monospace;
font-size: 18px;
font-weight: bold;
color: var(--mac-text);
}
.stat-label {
font-family: 'Charcoal', 'Chicago', Geneva, sans-serif;
font-size: 10px;
color: var(--mac-text-muted);
margin-top: 4px;
}
/* Chart containers */
.charts-wrapper {
display: flex;
gap: 20px;
justify-content: center;
flex-wrap: wrap;
}
.chart-container {
position: relative;
flex: 1;
min-width: 280px;
max-width: 450px;
padding: 10px;
background: #FFFFFF;
border: 1px solid;
border-color: var(--mac-border-dark) var(--mac-border-light) var(--mac-border-light) var(--mac-border-dark);
box-shadow: inset 1px 1px 0 var(--mac-border-dark);
}
.chart-title {
font-family: 'Charcoal', 'Chicago', Geneva, sans-serif;
font-size: 11px;
font-weight: bold;
text-align: center;
margin-bottom: 8px;
color: var(--mac-text);
}
@media (max-width: 700px) {
.charts-wrapper {
flex-direction: column;
align-items: center;
}
.chart-container {
max-width: 100%;
}
}
/* Tables styled like Mac OS 9 lists */
table {
width: 100%;
border-collapse: collapse;
font-family: 'Charcoal', 'Chicago', Geneva, sans-serif;
font-size: 11px;
background: #FFFFFF;
border: 1px solid var(--mac-border-dark);
}
th {
background: linear-gradient(to bottom, #EEEEEE 0%, #CCCCCC 100%);
padding: 4px 8px;
text-align: left;
border-bottom: 1px solid var(--mac-border-dark);
border-right: 1px solid var(--mac-border-light);
font-weight: bold;
color: var(--mac-text);
}
th:last-child {
border-right: none;
}
td {
padding: 3px 8px;
border-bottom: 1px solid #CCCCCC;
color: var(--mac-text);
}
tr:hover td {
background: var(--mac-selection);
color: var(--mac-selection-text);
}
tr:hover .service-badge {
background: rgba(255,255,255,0.3);
color: var(--mac-selection-text);
}
.time {
font-family: 'Monaco', 'Courier New', monospace;
font-size: 10px;
}
.percent {
font-family: 'Monaco', 'Courier New', monospace;
font-size: 10px;
text-align: right;
}
.color-box {
display: inline-block;
width: 10px;
height: 10px;
margin-right: 6px;
vertical-align: middle;
border: 1px solid var(--mac-border-dark);
}
.service-badge {
display: inline-block;
font-size: 9px;
padding: 1px 4px;
background: #EEEEEE;
border: 1px solid #CCCCCC;
color: var(--mac-text-muted);
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;
color: var(--mac-text-muted);
font-style: italic;
}
/* Others row expansion */
.others-row {
cursor: pointer;
}
.others-row td:first-child::before {
content: '';
display: inline-block;
width: 0;
height: 0;
border-left: 5px solid var(--mac-text);
border-top: 3px solid transparent;
border-bottom: 3px solid transparent;
margin-right: 5px;
transition: transform 0.15s;
}
.others-row:hover td:first-child::before {
border-left-color: var(--mac-selection-text);
}
.others-row.expanded td:first-child::before {
transform: rotate(90deg);
}
.others-detail {
display: none;
background: #F5F5F5;
}
.others-detail.visible {
display: table-row;
}
.others-detail td {
padding-left: 25px;
}
/* Mac OS 9 Tabs */
.tabs {
display: flex;
gap: 0;
margin-bottom: 0;
position: relative;
z-index: 1;
padding-left: 10px;
}
.tab {
font-family: 'Charcoal', 'Chicago', Geneva, sans-serif;
font-size: 11px;
padding: 3px 15px 2px 15px;
cursor: pointer;
color: var(--mac-text);
position: relative;
border: none;
background: transparent;
margin-right: -1px;
border-style: solid;
border-width: 10px 11px 2px 11px;
border-image: url('__TAB_INACTIVE__') 10 11 2 11 fill stretch;
}
.tab:hover {
opacity: 0.9;
}
.tab.active {
border-image: url('__TAB_ACTIVE__') 10 11 2 11 fill stretch;
font-weight: bold;
z-index: 2;
margin-bottom: -1px;
padding-bottom: 3px;
}
.tab-content {
background: #f3f3f3;
border: 1px solid #000000;
border-top: 1px solid #888888;
padding: 10px;
box-shadow: inset 1px 1px 0 #FFFFFF, inset -1px -1px 0 #888888;
}
.tab-panel {
display: none;
}
.tab-panel.active {
display: block;
}
.table-wrapper {
overflow-y: visible;
}
/* Mac OS 9 Scrollbars */
::-webkit-scrollbar {
width: 16px;
height: 16px;
}
::-webkit-scrollbar-track {
background-image: url('__SCROLLBAR_TROUGH_V__');
background-repeat: repeat-y;
background-color: #CCCCCC;
border-left: 1px solid #888888;
}
::-webkit-scrollbar-thumb {
background-image: url('__SCROLLBAR_THUMB_V__');
background-repeat: repeat-y;
background-size: contain;
border: 1px solid;
border-color: #FFFFFF #888888 #888888 #FFFFFF;
background-color: #DDDDDD;
min-height: 20px;
}
::-webkit-scrollbar-thumb:hover {
background-color: #EEEEEE;
}
::-webkit-scrollbar-thumb:active {
background-color: #CCCCCC;
}
::-webkit-scrollbar-button:vertical:start:decrement {
background-image: url('__SCROLLBAR_UP__');
background-size: contain;
background-repeat: no-repeat;
background-position: center;
background-color: #DDDDDD;
border: 1px solid;
border-color: #FFFFFF #888888 #888888 #FFFFFF;
height: 16px;
}
::-webkit-scrollbar-button:vertical:end:increment {
background-image: url('__SCROLLBAR_DOWN__');
background-size: contain;
background-repeat: no-repeat;
background-position: center;
background-color: #DDDDDD;
border: 1px solid;
border-color: #FFFFFF #888888 #888888 #FFFFFF;
height: 16px;
}
::-webkit-scrollbar-button:vertical:start:decrement:hover,
::-webkit-scrollbar-button:vertical:end:increment:hover {
background-color: #EEEEEE;
}
::-webkit-scrollbar-button:vertical:start:decrement:active,
::-webkit-scrollbar-button:vertical:end:increment:active {
background-color: #BBBBBB;
border-color: #888888 #FFFFFF #FFFFFF #888888;
}
::-webkit-scrollbar-corner {
background-color: #DDDDDD;
}
/* Firefox scrollbar */
* {
scrollbar-width: auto;
scrollbar-color: #DDDDDD #CCCCCC;
}
/* Scroll to Top Button */
.scroll-top {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 1000;
width: 32px;
height: 32px;
background: var(--mac-window-bg);
border: 1px solid;
border-color: var(--mac-border-light) var(--mac-border-dark) var(--mac-border-dark) var(--mac-border-light);
box-shadow: inset -1px -1px 0 var(--mac-border-dark), inset 1px 1px 0 var(--mac-border-light);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s ease;
}
.scroll-top.visible {
opacity: 1;
visibility: visible;
}
.scroll-top:hover {
background: #EEEEEE;
}
.scroll-top:active {
border-color: var(--mac-border-dark) var(--mac-border-light) var(--mac-border-light) var(--mac-border-dark);
box-shadow: inset 1px 1px 0 var(--mac-border-dark), inset -1px -1px 0 var(--mac-border-light);
}
.scroll-top-arrow {
width: 0;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-bottom: 8px solid var(--mac-text);
}
</style>
</head>
<body>
<!-- Scroll to Top Button -->
<button class="scroll-top" id="scroll-top" title="Scroll to top">
<div class="scroll-top-arrow"></div>
</button>
<div class="window">
<div class="window-titlebar">
<div class="window-close"></div>
<div class="window-title">Lutris Playtime Report</div>
<div class="window-hide"></div>
<div class="window-shade"></div>
</div>
<div class="window-content">
<div class="filters" id="filters"></div>
</div>
</div>
<div class="window">
<div class="window-titlebar">
<div class="window-close"></div>
<div class="window-title">Statistics</div>
<div class="window-hide"></div>
<div class="window-shade"></div>
</div>
<div class="window-content">
<div class="stats">
<div class="stat">
<div class="stat-value" id="total-library">__TOTAL_LIBRARY__</div>
<div class="stat-label">Games in Library</div>
</div>
<div class="stat">
<div class="stat-value" id="total-games">0</div>
<div class="stat-label">Games Played</div>
</div>
<div class="stat">
<div class="stat-value" id="total-time">0h</div>
<div class="stat-label">Total Playtime</div>
</div>
</div>
</div>
</div>
<div class="window">
<div class="window-titlebar">
<div class="window-close"></div>
<div class="window-title">Playtime Distribution</div>
<div class="window-hide"></div>
<div class="window-shade"></div>
</div>
<div class="window-content">
<div class="charts-wrapper">
<div class="chart-container">
<div class="chart-title">Top Games</div>
<canvas id="playtime-chart"></canvas>
</div>
<div class="chart-container">
<div class="chart-title">By Category</div>
<canvas id="categories-chart"></canvas>
</div>
</div>
</div>
</div>
<div class="window">
<div class="window-titlebar">
<div class="window-close"></div>
<div class="window-title">Summaries</div>
<div class="window-hide"></div>
<div class="window-shade"></div>
</div>
<div class="window-content">
<div class="tabs">
<div class="tab active" data-tab="games">Top Games</div>
<div class="tab" data-tab="categories">By Category</div>
</div>
<div class="tab-content">
<div class="tab-panel active" id="tab-games">
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>#</th>
<th>Game</th>
<th>Playtime</th>
<th style="text-align: right">%</th>
</tr>
</thead>
<tbody id="games-table"></tbody>
</table>
</div>
</div>
<div class="tab-panel" id="tab-categories">
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>#</th>
<th>Category</th>
<th>Playtime</th>
<th style="text-align: right">%</th>
</tr>
</thead>
<tbody id="categories-table"></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<script>
const allGames = __ALL_GAMES__;
const topN = __TOP_N__;
// Mac OS 9 inspired colors
const colors = [
'#336699', '#993366', '#669933', '#CC6633', '#663399',
'#339966', '#996633', '#336666', '#993333', '#666699',
'#888888'
];
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 = `
<input type="checkbox" value="${service.name}" checked>
<span class="service-name">${service.name}</span>
<span class="service-count">(${service.count})</span>
`;
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');
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' || cat === 'Horny') 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);
return { chartData: topGames, othersGames, categoriesData, totalPlaytime, totalGames };
}
function updateDisplay() {
const selectedServices = getSelectedServices();
const { chartData, othersGames, categoriesData, 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 =
'<tr><td colspan="4" class="no-data">No games match the selected filters</td></tr>';
document.getElementById('categories-table').innerHTML =
'<tr><td colspan="4" class="no-data">No categories found</td></tr>';
return;
}
chart = new Chart(ctx, {
type: 'doughnut',
data: {
labels: chartData.map(g => g.name),
datasets: [{
data: chartData.map(g => g.playtime),
backgroundColor: colors.slice(0, chartData.length),
borderColor: '#FFFFFF',
borderWidth: 1
}]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'bottom',
labels: {
color: '#000000',
font: {
family: "'Charcoal', 'Chicago', Geneva, sans-serif",
size: 10
},
padding: 10
}
},
tooltip: {
titleFont: {
weight: 'bold'
},
bodyFont: {
weight: 'normal'
},
callbacks: {
title: function(context) {
return context[0].label;
},
beforeBody: function(context) {
const index = context[0].dataIndex;
const service = chartData[index].service;
if (service && service !== 'others') {
return service.charAt(0).toUpperCase() + service.slice(1);
}
return '';
},
label: function(context) {
const value = context.raw;
const percent = ((value / totalPlaytime) * 100).toFixed(1);
return formatTime(value) + ' (' + percent + '%)';
}
}
}
}
}
});
// Categories chart data
const topCategoriesChart = categoriesData.slice(0, topN);
const otherCategoriesChart = categoriesData.slice(topN);
const categoriesChartData = topCategoriesChart.map(c => ({
name: c.name,
playtime: c.playtime
}));
if (otherCategoriesChart.length > 0) {
const othersPlaytime = otherCategoriesChart.reduce((sum, c) => sum + c.playtime, 0);
categoriesChartData.push({
name: `Others (${otherCategoriesChart.length} categories)`,
playtime: othersPlaytime
});
}
if (categoriesChartData.length > 0) {
categoriesChart = new Chart(ctxCategories, {
type: 'doughnut',
data: {
labels: categoriesChartData.map(c => c.name),
datasets: [{
data: categoriesChartData.map(c => c.playtime),
backgroundColor: colors.slice(0, categoriesChartData.length),
borderColor: '#FFFFFF',
borderWidth: 1
}]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'bottom',
labels: {
color: '#000000',
font: {
family: "'Charcoal', 'Chicago', Geneva, sans-serif",
size: 10
},
padding: 10
}
},
tooltip: {
callbacks: {
label: function(context) {
const value = context.raw;
const percent = ((value / totalPlaytime) * 100).toFixed(1);
return ' ' + formatTime(value) + ' (' + percent + '%)';
}
}
}
}
}
});
}
const tbody = document.getElementById('games-table');
tbody.innerHTML = '';
chartData.forEach((game, index) => {
const percent = ((game.playtime / totalPlaytime) * 100).toFixed(1);
const isOthers = game.service === 'others';
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';
}
row.innerHTML = `
<td>${index + 1}</td>
<td>
<span class="color-box" style="background: ${colors[index]}"></span>
${game.name}${serviceBadge}${categoriesBadges}
</td>
<td class="time">${formatTime(game.playtime)}</td>
<td class="percent">${percent}%</td>
`;
tbody.appendChild(row);
if (isOthers && othersGames.length > 0) {
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>${otherCategoriesBadges}
</td>
<td class="time">${formatTime(otherGame.playtime)}</td>
<td class="percent">${otherPercent}%</td>
`;
tbody.appendChild(detailRow);
detailRows.push(detailRow);
});
row.addEventListener('click', () => {
row.classList.toggle('expanded');
detailRows.forEach(dr => dr.classList.toggle('visible'));
});
}
});
const catTbody = document.getElementById('categories-table');
catTbody.innerHTML = '';
if (categoriesData.length === 0) {
catTbody.innerHTML = '<tr><td colspan="4" class="no-data">No categories found</td></tr>';
} else {
const topCategories = categoriesData.slice(0, topN);
const otherCategories = categoriesData.slice(topN);
topCategories.forEach((cat, index) => {
const percent = ((cat.playtime / totalPlaytime) * 100).toFixed(1);
const row = document.createElement('tr');
row.innerHTML = `
<td>${index + 1}</td>
<td>
<span class="color-box" style="background: ${colors[index]}"></span>
${cat.name} <span class="service-badge">${cat.gameCount} games</span>
</td>
<td class="time">${formatTime(cat.playtime)}</td>
<td class="percent">${percent}%</td>
`;
catTbody.appendChild(row);
});
if (otherCategories.length > 0) {
const othersPlaytime = otherCategories.reduce((sum, c) => sum + c.playtime, 0);
const othersPercent = ((othersPlaytime / totalPlaytime) * 100).toFixed(1);
const othersIndex = topCategories.length;
const othersRow = document.createElement('tr');
othersRow.className = 'others-row';
othersRow.innerHTML = `
<td>${othersIndex + 1}</td>
<td>
<span class="color-box" style="background: ${colors[othersIndex]}"></span>
Others (${otherCategories.length} categories)
</td>
<td class="time">${formatTime(othersPlaytime)}</td>
<td class="percent">${othersPercent}%</td>
`;
catTbody.appendChild(othersRow);
const detailRows = [];
otherCategories.forEach((otherCat, otherIndex) => {
const otherPercent = ((otherCat.playtime / totalPlaytime) * 100).toFixed(1);
const detailRow = document.createElement('tr');
detailRow.className = 'others-detail';
detailRow.innerHTML = `
<td>${othersIndex + 1}.${otherIndex + 1}</td>
<td>
${otherCat.name} <span class="service-badge">${otherCat.gameCount} games</span>
</td>
<td class="time">${formatTime(otherCat.playtime)}</td>
<td class="percent">${otherPercent}%</td>
`;
catTbody.appendChild(detailRow);
detailRows.push(detailRow);
});
othersRow.addEventListener('click', () => {
othersRow.classList.toggle('expanded');
detailRows.forEach(dr => dr.classList.toggle('visible'));
});
}
}
}
filtersDiv.addEventListener('change', updateDisplay);
updateDisplay();
// Tab switching
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
const tabId = tab.dataset.tab;
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
document.getElementById('tab-' + tabId).classList.add('active');
});
});
// Scroll to top button
const scrollTopBtn = document.getElementById('scroll-top');
function updateScrollTopVisibility() {
if (window.scrollY > 100) {
scrollTopBtn.classList.add('visible');
} else {
scrollTopBtn.classList.remove('visible');
}
}
window.addEventListener('scroll', updateScrollTopVisibility);
updateScrollTopVisibility();
scrollTopBtn.addEventListener('click', () => {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
});
</script>
</body>
</html>