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]
|
||||
|
||||
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
|
||||
WHERE playtime > 0
|
||||
ORDER BY playtime DESC
|
||||
@@ -75,6 +75,7 @@ def get_all_games(db_path: str) -> tuple[list[dict], int]:
|
||||
"name": row[1],
|
||||
"playtime": row[2],
|
||||
"service": row[3],
|
||||
"runner": row[4],
|
||||
"categories": game_categories.get(row[0], [])
|
||||
}
|
||||
for row in games_rows
|
||||
|
||||
@@ -683,6 +683,7 @@
|
||||
<div class="tabs">
|
||||
<div class="tab active" data-tab="games">Top Games</div>
|
||||
<div class="tab" data-tab="categories">By Category</div>
|
||||
<div class="tab" data-tab="runners">By Runner</div>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
<div class="tab-panel active" id="tab-games">
|
||||
@@ -715,6 +716,21 @@
|
||||
</table>
|
||||
</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>
|
||||
@@ -893,7 +909,19 @@
|
||||
const categoriesData = Object.values(categoryMap)
|
||||
.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() {
|
||||
@@ -924,7 +952,7 @@
|
||||
|
||||
function updateDisplay() {
|
||||
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-time').textContent = formatTime(totalPlaytime);
|
||||
@@ -941,6 +969,8 @@
|
||||
'<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>';
|
||||
document.getElementById('runners-table').innerHTML =
|
||||
'<tr><td colspan="4" class="no-data">No runners found</td></tr>';
|
||||
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);
|
||||
|
||||
@@ -648,6 +648,7 @@
|
||||
<div class="tabs">
|
||||
<div class="tab active" data-tab="games">Top Games</div>
|
||||
<div class="tab" data-tab="categories">By Category</div>
|
||||
<div class="tab" data-tab="runners">By Runner</div>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
<div class="tab-panel active" id="tab-games">
|
||||
@@ -680,6 +681,21 @@
|
||||
</table>
|
||||
</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>
|
||||
@@ -857,7 +873,19 @@
|
||||
const categoriesData = Object.values(categoryMap)
|
||||
.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() {
|
||||
@@ -881,7 +909,7 @@
|
||||
|
||||
function updateDisplay() {
|
||||
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-time').textContent = formatTime(totalPlaytime);
|
||||
@@ -898,6 +926,8 @@
|
||||
'<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>';
|
||||
document.getElementById('runners-table').innerHTML =
|
||||
'<tr><td colspan="4" class="no-data">No runners found</td></tr>';
|
||||
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);
|
||||
|
||||
@@ -688,6 +688,7 @@
|
||||
<div class="tabs">
|
||||
<div class="tab active" data-tab="games">Top Games</div>
|
||||
<div class="tab" data-tab="categories">By Category</div>
|
||||
<div class="tab" data-tab="runners">By Runner</div>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
<div class="tab-panel active" id="tab-games">
|
||||
@@ -720,6 +721,21 @@
|
||||
</table>
|
||||
</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>
|
||||
@@ -897,7 +913,19 @@
|
||||
const categoriesData = Object.values(categoryMap)
|
||||
.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() {
|
||||
@@ -921,7 +949,7 @@
|
||||
|
||||
function updateDisplay() {
|
||||
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-time').textContent = formatTime(totalPlaytime);
|
||||
@@ -938,6 +966,8 @@
|
||||
'<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>';
|
||||
document.getElementById('runners-table').innerHTML =
|
||||
'<tr><td colspan="4" class="no-data">No runners found</td></tr>';
|
||||
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);
|
||||
|
||||
@@ -582,6 +582,7 @@
|
||||
<div class="tabs">
|
||||
<div class="tab active" data-tab="games">Top Games</div>
|
||||
<div class="tab" data-tab="categories">By Category</div>
|
||||
<div class="tab" data-tab="runners">By Runner</div>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
<div class="tab-panel active" id="tab-games">
|
||||
@@ -614,6 +615,21 @@
|
||||
</table>
|
||||
</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>
|
||||
@@ -728,12 +744,24 @@
|
||||
const categoriesData = Object.values(categoryMap)
|
||||
.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() {
|
||||
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-time').textContent = formatTime(totalPlaytime);
|
||||
@@ -750,6 +778,8 @@
|
||||
'<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>';
|
||||
document.getElementById('runners-table').innerHTML =
|
||||
'<tr><td colspan="4" class="no-data">No runners found</td></tr>';
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user