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>
This commit is contained in:
@@ -49,7 +49,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,6 +75,7 @@ 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],
|
||||||
|
"runner": row[4],
|
||||||
"categories": game_categories.get(row[0], [])
|
"categories": game_categories.get(row[0], [])
|
||||||
}
|
}
|
||||||
for row in games_rows
|
for row in games_rows
|
||||||
|
|||||||
@@ -683,6 +683,7 @@
|
|||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<div class="tab active" data-tab="games">Top Games</div>
|
<div class="tab active" data-tab="games">Top Games</div>
|
||||||
<div class="tab" data-tab="categories">By Category</div>
|
<div class="tab" data-tab="categories">By Category</div>
|
||||||
|
<div class="tab" data-tab="runners">By Runner</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
<div class="tab-panel active" id="tab-games">
|
<div class="tab-panel active" id="tab-games">
|
||||||
@@ -715,6 +716,21 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="tab-panel" id="tab-runners">
|
||||||
|
<div class="table-wrapper">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>Runner</th>
|
||||||
|
<th>Playtime</th>
|
||||||
|
<th style="text-align: right">%</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="runners-table"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -893,7 +909,19 @@
|
|||||||
const categoriesData = Object.values(categoryMap)
|
const categoriesData = Object.values(categoryMap)
|
||||||
.sort((a, b) => b.playtime - a.playtime);
|
.sort((a, b) => b.playtime - a.playtime);
|
||||||
|
|
||||||
return { chartData: topGames, othersGames, categoriesData, totalPlaytime, totalGames };
|
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() {
|
function getChartTextColor() {
|
||||||
@@ -924,7 +952,7 @@
|
|||||||
|
|
||||||
function updateDisplay() {
|
function updateDisplay() {
|
||||||
const selectedServices = getSelectedServices();
|
const selectedServices = getSelectedServices();
|
||||||
const { chartData, othersGames, categoriesData, totalPlaytime, totalGames } = getFilteredData(selectedServices);
|
const { chartData, othersGames, categoriesData, runnersData, totalPlaytime, totalGames } = getFilteredData(selectedServices);
|
||||||
|
|
||||||
document.getElementById('total-games').textContent = totalGames;
|
document.getElementById('total-games').textContent = totalGames;
|
||||||
document.getElementById('total-time').textContent = formatTime(totalPlaytime);
|
document.getElementById('total-time').textContent = formatTime(totalPlaytime);
|
||||||
@@ -941,6 +969,8 @@
|
|||||||
'<tr><td colspan="4" class="no-data">No games match the selected filters</td></tr>';
|
'<tr><td colspan="4" class="no-data">No games match the selected filters</td></tr>';
|
||||||
document.getElementById('categories-table').innerHTML =
|
document.getElementById('categories-table').innerHTML =
|
||||||
'<tr><td colspan="4" class="no-data">No categories found</td></tr>';
|
'<tr><td colspan="4" class="no-data">No categories found</td></tr>';
|
||||||
|
document.getElementById('runners-table').innerHTML =
|
||||||
|
'<tr><td colspan="4" class="no-data">No runners found</td></tr>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1199,6 +1229,71 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const runnersTbody = document.getElementById('runners-table');
|
||||||
|
runnersTbody.innerHTML = '';
|
||||||
|
if (runnersData.length === 0) {
|
||||||
|
runnersTbody.innerHTML = '<tr><td colspan="4" class="no-data">No runners found</td></tr>';
|
||||||
|
} else {
|
||||||
|
const topRunners = runnersData.slice(0, topN);
|
||||||
|
const otherRunners = runnersData.slice(topN);
|
||||||
|
|
||||||
|
topRunners.forEach((runner, index) => {
|
||||||
|
const percent = ((runner.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>
|
||||||
|
${runner.name} <span class="service-badge">${runner.gameCount} games</span>
|
||||||
|
</td>
|
||||||
|
<td class="time">${formatTime(runner.playtime)}</td>
|
||||||
|
<td class="percent">${percent}%</td>
|
||||||
|
`;
|
||||||
|
runnersTbody.appendChild(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (otherRunners.length > 0) {
|
||||||
|
const othersPlaytime = otherRunners.reduce((sum, r) => sum + r.playtime, 0);
|
||||||
|
const othersPercent = ((othersPlaytime / totalPlaytime) * 100).toFixed(1);
|
||||||
|
const othersIndex = topRunners.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 (${otherRunners.length} runners)
|
||||||
|
</td>
|
||||||
|
<td class="time">${formatTime(othersPlaytime)}</td>
|
||||||
|
<td class="percent">${othersPercent}%</td>
|
||||||
|
`;
|
||||||
|
runnersTbody.appendChild(othersRow);
|
||||||
|
|
||||||
|
const detailRows = [];
|
||||||
|
otherRunners.forEach((otherRunner, otherIndex) => {
|
||||||
|
const otherPercent = ((otherRunner.playtime / totalPlaytime) * 100).toFixed(1);
|
||||||
|
const detailRow = document.createElement('tr');
|
||||||
|
detailRow.className = 'others-detail';
|
||||||
|
detailRow.innerHTML = `
|
||||||
|
<td>${othersIndex + 1}.${otherIndex + 1}</td>
|
||||||
|
<td>
|
||||||
|
${otherRunner.name} <span class="service-badge">${otherRunner.gameCount} games</span>
|
||||||
|
</td>
|
||||||
|
<td class="time">${formatTime(otherRunner.playtime)}</td>
|
||||||
|
<td class="percent">${otherPercent}%</td>
|
||||||
|
`;
|
||||||
|
runnersTbody.appendChild(detailRow);
|
||||||
|
detailRows.push(detailRow);
|
||||||
|
});
|
||||||
|
|
||||||
|
othersRow.addEventListener('click', () => {
|
||||||
|
othersRow.classList.toggle('expanded');
|
||||||
|
detailRows.forEach(dr => dr.classList.toggle('visible'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
filtersDiv.addEventListener('change', updateDisplay);
|
filtersDiv.addEventListener('change', updateDisplay);
|
||||||
|
|||||||
@@ -648,6 +648,7 @@
|
|||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<div class="tab active" data-tab="games">Top Games</div>
|
<div class="tab active" data-tab="games">Top Games</div>
|
||||||
<div class="tab" data-tab="categories">By Category</div>
|
<div class="tab" data-tab="categories">By Category</div>
|
||||||
|
<div class="tab" data-tab="runners">By Runner</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
<div class="tab-panel active" id="tab-games">
|
<div class="tab-panel active" id="tab-games">
|
||||||
@@ -680,6 +681,21 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="tab-panel" id="tab-runners">
|
||||||
|
<div class="table-wrapper">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>Runner</th>
|
||||||
|
<th>Playtime</th>
|
||||||
|
<th style="text-align: right">%</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="runners-table"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -857,7 +873,19 @@
|
|||||||
const categoriesData = Object.values(categoryMap)
|
const categoriesData = Object.values(categoryMap)
|
||||||
.sort((a, b) => b.playtime - a.playtime);
|
.sort((a, b) => b.playtime - a.playtime);
|
||||||
|
|
||||||
return { chartData: topGames, othersGames, categoriesData, totalPlaytime, totalGames };
|
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() {
|
function getChartTextColor() {
|
||||||
@@ -881,7 +909,7 @@
|
|||||||
|
|
||||||
function updateDisplay() {
|
function updateDisplay() {
|
||||||
const selectedServices = getSelectedServices();
|
const selectedServices = getSelectedServices();
|
||||||
const { chartData, othersGames, categoriesData, totalPlaytime, totalGames } = getFilteredData(selectedServices);
|
const { chartData, othersGames, categoriesData, runnersData, totalPlaytime, totalGames } = getFilteredData(selectedServices);
|
||||||
|
|
||||||
document.getElementById('total-games').textContent = totalGames;
|
document.getElementById('total-games').textContent = totalGames;
|
||||||
document.getElementById('total-time').textContent = formatTime(totalPlaytime);
|
document.getElementById('total-time').textContent = formatTime(totalPlaytime);
|
||||||
@@ -898,6 +926,8 @@
|
|||||||
'<tr><td colspan="4" class="no-data">No games match the selected filters</td></tr>';
|
'<tr><td colspan="4" class="no-data">No games match the selected filters</td></tr>';
|
||||||
document.getElementById('categories-table').innerHTML =
|
document.getElementById('categories-table').innerHTML =
|
||||||
'<tr><td colspan="4" class="no-data">No categories found</td></tr>';
|
'<tr><td colspan="4" class="no-data">No categories found</td></tr>';
|
||||||
|
document.getElementById('runners-table').innerHTML =
|
||||||
|
'<tr><td colspan="4" class="no-data">No runners found</td></tr>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1143,6 +1173,71 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const runnersTbody = document.getElementById('runners-table');
|
||||||
|
runnersTbody.innerHTML = '';
|
||||||
|
if (runnersData.length === 0) {
|
||||||
|
runnersTbody.innerHTML = '<tr><td colspan="4" class="no-data">No runners found</td></tr>';
|
||||||
|
} else {
|
||||||
|
const topRunners = runnersData.slice(0, topN);
|
||||||
|
const otherRunners = runnersData.slice(topN);
|
||||||
|
|
||||||
|
topRunners.forEach((runner, index) => {
|
||||||
|
const percent = ((runner.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>
|
||||||
|
${runner.name} <span class="service-badge">${runner.gameCount} games</span>
|
||||||
|
</td>
|
||||||
|
<td class="time">${formatTime(runner.playtime)}</td>
|
||||||
|
<td class="percent">${percent}%</td>
|
||||||
|
`;
|
||||||
|
runnersTbody.appendChild(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (otherRunners.length > 0) {
|
||||||
|
const othersPlaytime = otherRunners.reduce((sum, r) => sum + r.playtime, 0);
|
||||||
|
const othersPercent = ((othersPlaytime / totalPlaytime) * 100).toFixed(1);
|
||||||
|
const othersIndex = topRunners.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 (${otherRunners.length} runners)
|
||||||
|
</td>
|
||||||
|
<td class="time">${formatTime(othersPlaytime)}</td>
|
||||||
|
<td class="percent">${othersPercent}%</td>
|
||||||
|
`;
|
||||||
|
runnersTbody.appendChild(othersRow);
|
||||||
|
|
||||||
|
const detailRows = [];
|
||||||
|
otherRunners.forEach((otherRunner, otherIndex) => {
|
||||||
|
const otherPercent = ((otherRunner.playtime / totalPlaytime) * 100).toFixed(1);
|
||||||
|
const detailRow = document.createElement('tr');
|
||||||
|
detailRow.className = 'others-detail';
|
||||||
|
detailRow.innerHTML = `
|
||||||
|
<td>${othersIndex + 1}.${otherIndex + 1}</td>
|
||||||
|
<td>
|
||||||
|
${otherRunner.name} <span class="service-badge">${otherRunner.gameCount} games</span>
|
||||||
|
</td>
|
||||||
|
<td class="time">${formatTime(otherRunner.playtime)}</td>
|
||||||
|
<td class="percent">${otherPercent}%</td>
|
||||||
|
`;
|
||||||
|
runnersTbody.appendChild(detailRow);
|
||||||
|
detailRows.push(detailRow);
|
||||||
|
});
|
||||||
|
|
||||||
|
othersRow.addEventListener('click', () => {
|
||||||
|
othersRow.classList.toggle('expanded');
|
||||||
|
detailRows.forEach(dr => dr.classList.toggle('visible'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
filtersDiv.addEventListener('change', updateDisplay);
|
filtersDiv.addEventListener('change', updateDisplay);
|
||||||
|
|||||||
@@ -688,6 +688,7 @@
|
|||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<div class="tab active" data-tab="games">Top Games</div>
|
<div class="tab active" data-tab="games">Top Games</div>
|
||||||
<div class="tab" data-tab="categories">By Category</div>
|
<div class="tab" data-tab="categories">By Category</div>
|
||||||
|
<div class="tab" data-tab="runners">By Runner</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
<div class="tab-panel active" id="tab-games">
|
<div class="tab-panel active" id="tab-games">
|
||||||
@@ -720,6 +721,21 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="tab-panel" id="tab-runners">
|
||||||
|
<div class="table-wrapper">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>Runner</th>
|
||||||
|
<th>Playtime</th>
|
||||||
|
<th style="text-align: right">%</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="runners-table"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -897,7 +913,19 @@
|
|||||||
const categoriesData = Object.values(categoryMap)
|
const categoriesData = Object.values(categoryMap)
|
||||||
.sort((a, b) => b.playtime - a.playtime);
|
.sort((a, b) => b.playtime - a.playtime);
|
||||||
|
|
||||||
return { chartData: topGames, othersGames, categoriesData, totalPlaytime, totalGames };
|
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() {
|
function getChartTextColor() {
|
||||||
@@ -921,7 +949,7 @@
|
|||||||
|
|
||||||
function updateDisplay() {
|
function updateDisplay() {
|
||||||
const selectedServices = getSelectedServices();
|
const selectedServices = getSelectedServices();
|
||||||
const { chartData, othersGames, categoriesData, totalPlaytime, totalGames } = getFilteredData(selectedServices);
|
const { chartData, othersGames, categoriesData, runnersData, totalPlaytime, totalGames } = getFilteredData(selectedServices);
|
||||||
|
|
||||||
document.getElementById('total-games').textContent = totalGames;
|
document.getElementById('total-games').textContent = totalGames;
|
||||||
document.getElementById('total-time').textContent = formatTime(totalPlaytime);
|
document.getElementById('total-time').textContent = formatTime(totalPlaytime);
|
||||||
@@ -938,6 +966,8 @@
|
|||||||
'<tr><td colspan="4" class="no-data">No games match the selected filters</td></tr>';
|
'<tr><td colspan="4" class="no-data">No games match the selected filters</td></tr>';
|
||||||
document.getElementById('categories-table').innerHTML =
|
document.getElementById('categories-table').innerHTML =
|
||||||
'<tr><td colspan="4" class="no-data">No categories found</td></tr>';
|
'<tr><td colspan="4" class="no-data">No categories found</td></tr>';
|
||||||
|
document.getElementById('runners-table').innerHTML =
|
||||||
|
'<tr><td colspan="4" class="no-data">No runners found</td></tr>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1183,6 +1213,71 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const runnersTbody = document.getElementById('runners-table');
|
||||||
|
runnersTbody.innerHTML = '';
|
||||||
|
if (runnersData.length === 0) {
|
||||||
|
runnersTbody.innerHTML = '<tr><td colspan="4" class="no-data">No runners found</td></tr>';
|
||||||
|
} else {
|
||||||
|
const topRunners = runnersData.slice(0, topN);
|
||||||
|
const otherRunners = runnersData.slice(topN);
|
||||||
|
|
||||||
|
topRunners.forEach((runner, index) => {
|
||||||
|
const percent = ((runner.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>
|
||||||
|
${runner.name} <span class="service-badge">${runner.gameCount} games</span>
|
||||||
|
</td>
|
||||||
|
<td class="time">${formatTime(runner.playtime)}</td>
|
||||||
|
<td class="percent">${percent}%</td>
|
||||||
|
`;
|
||||||
|
runnersTbody.appendChild(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (otherRunners.length > 0) {
|
||||||
|
const othersPlaytime = otherRunners.reduce((sum, r) => sum + r.playtime, 0);
|
||||||
|
const othersPercent = ((othersPlaytime / totalPlaytime) * 100).toFixed(1);
|
||||||
|
const othersIndex = topRunners.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 (${otherRunners.length} runners)
|
||||||
|
</td>
|
||||||
|
<td class="time">${formatTime(othersPlaytime)}</td>
|
||||||
|
<td class="percent">${othersPercent}%</td>
|
||||||
|
`;
|
||||||
|
runnersTbody.appendChild(othersRow);
|
||||||
|
|
||||||
|
const detailRows = [];
|
||||||
|
otherRunners.forEach((otherRunner, otherIndex) => {
|
||||||
|
const otherPercent = ((otherRunner.playtime / totalPlaytime) * 100).toFixed(1);
|
||||||
|
const detailRow = document.createElement('tr');
|
||||||
|
detailRow.className = 'others-detail';
|
||||||
|
detailRow.innerHTML = `
|
||||||
|
<td>${othersIndex + 1}.${otherIndex + 1}</td>
|
||||||
|
<td>
|
||||||
|
${otherRunner.name} <span class="service-badge">${otherRunner.gameCount} games</span>
|
||||||
|
</td>
|
||||||
|
<td class="time">${formatTime(otherRunner.playtime)}</td>
|
||||||
|
<td class="percent">${otherPercent}%</td>
|
||||||
|
`;
|
||||||
|
runnersTbody.appendChild(detailRow);
|
||||||
|
detailRows.push(detailRow);
|
||||||
|
});
|
||||||
|
|
||||||
|
othersRow.addEventListener('click', () => {
|
||||||
|
othersRow.classList.toggle('expanded');
|
||||||
|
detailRows.forEach(dr => dr.classList.toggle('visible'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
filtersDiv.addEventListener('change', updateDisplay);
|
filtersDiv.addEventListener('change', updateDisplay);
|
||||||
|
|||||||
@@ -582,6 +582,7 @@
|
|||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<div class="tab active" data-tab="games">Top Games</div>
|
<div class="tab active" data-tab="games">Top Games</div>
|
||||||
<div class="tab" data-tab="categories">By Category</div>
|
<div class="tab" data-tab="categories">By Category</div>
|
||||||
|
<div class="tab" data-tab="runners">By Runner</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
<div class="tab-panel active" id="tab-games">
|
<div class="tab-panel active" id="tab-games">
|
||||||
@@ -614,6 +615,21 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="tab-panel" id="tab-runners">
|
||||||
|
<div class="table-wrapper">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>Runner</th>
|
||||||
|
<th>Playtime</th>
|
||||||
|
<th style="text-align: right">%</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="runners-table"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -728,12 +744,24 @@
|
|||||||
const categoriesData = Object.values(categoryMap)
|
const categoriesData = Object.values(categoryMap)
|
||||||
.sort((a, b) => b.playtime - a.playtime);
|
.sort((a, b) => b.playtime - a.playtime);
|
||||||
|
|
||||||
return { chartData: topGames, othersGames, categoriesData, totalPlaytime, totalGames };
|
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 updateDisplay() {
|
function updateDisplay() {
|
||||||
const selectedServices = getSelectedServices();
|
const selectedServices = getSelectedServices();
|
||||||
const { chartData, othersGames, categoriesData, totalPlaytime, totalGames } = getFilteredData(selectedServices);
|
const { chartData, othersGames, categoriesData, runnersData, totalPlaytime, totalGames } = getFilteredData(selectedServices);
|
||||||
|
|
||||||
document.getElementById('total-games').textContent = totalGames;
|
document.getElementById('total-games').textContent = totalGames;
|
||||||
document.getElementById('total-time').textContent = formatTime(totalPlaytime);
|
document.getElementById('total-time').textContent = formatTime(totalPlaytime);
|
||||||
@@ -750,6 +778,8 @@
|
|||||||
'<tr><td colspan="4" class="no-data">No games match the selected filters</td></tr>';
|
'<tr><td colspan="4" class="no-data">No games match the selected filters</td></tr>';
|
||||||
document.getElementById('categories-table').innerHTML =
|
document.getElementById('categories-table').innerHTML =
|
||||||
'<tr><td colspan="4" class="no-data">No categories found</td></tr>';
|
'<tr><td colspan="4" class="no-data">No categories found</td></tr>';
|
||||||
|
document.getElementById('runners-table').innerHTML =
|
||||||
|
'<tr><td colspan="4" class="no-data">No runners found</td></tr>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -988,6 +1018,71 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const runnersTbody = document.getElementById('runners-table');
|
||||||
|
runnersTbody.innerHTML = '';
|
||||||
|
if (runnersData.length === 0) {
|
||||||
|
runnersTbody.innerHTML = '<tr><td colspan="4" class="no-data">No runners found</td></tr>';
|
||||||
|
} else {
|
||||||
|
const topRunners = runnersData.slice(0, topN);
|
||||||
|
const otherRunners = runnersData.slice(topN);
|
||||||
|
|
||||||
|
topRunners.forEach((runner, index) => {
|
||||||
|
const percent = ((runner.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>
|
||||||
|
${runner.name} <span class="service-badge">${runner.gameCount} games</span>
|
||||||
|
</td>
|
||||||
|
<td class="time">${formatTime(runner.playtime)}</td>
|
||||||
|
<td class="percent">${percent}%</td>
|
||||||
|
`;
|
||||||
|
runnersTbody.appendChild(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (otherRunners.length > 0) {
|
||||||
|
const othersPlaytime = otherRunners.reduce((sum, r) => sum + r.playtime, 0);
|
||||||
|
const othersPercent = ((othersPlaytime / totalPlaytime) * 100).toFixed(1);
|
||||||
|
const othersIndex = topRunners.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 (${otherRunners.length} runners)
|
||||||
|
</td>
|
||||||
|
<td class="time">${formatTime(othersPlaytime)}</td>
|
||||||
|
<td class="percent">${othersPercent}%</td>
|
||||||
|
`;
|
||||||
|
runnersTbody.appendChild(othersRow);
|
||||||
|
|
||||||
|
const detailRows = [];
|
||||||
|
otherRunners.forEach((otherRunner, otherIndex) => {
|
||||||
|
const otherPercent = ((otherRunner.playtime / totalPlaytime) * 100).toFixed(1);
|
||||||
|
const detailRow = document.createElement('tr');
|
||||||
|
detailRow.className = 'others-detail';
|
||||||
|
detailRow.innerHTML = `
|
||||||
|
<td>${othersIndex + 1}.${otherIndex + 1}</td>
|
||||||
|
<td>
|
||||||
|
${otherRunner.name} <span class="service-badge">${otherRunner.gameCount} games</span>
|
||||||
|
</td>
|
||||||
|
<td class="time">${formatTime(otherRunner.playtime)}</td>
|
||||||
|
<td class="percent">${otherPercent}%</td>
|
||||||
|
`;
|
||||||
|
runnersTbody.appendChild(detailRow);
|
||||||
|
detailRows.push(detailRow);
|
||||||
|
});
|
||||||
|
|
||||||
|
othersRow.addEventListener('click', () => {
|
||||||
|
othersRow.classList.toggle('expanded');
|
||||||
|
detailRows.forEach(dr => dr.classList.toggle('visible'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
filtersDiv.addEventListener('change', updateDisplay);
|
filtersDiv.addEventListener('change', updateDisplay);
|
||||||
|
|||||||
Reference in New Issue
Block a user