diff --git a/generate_report.py b/generate_report.py
index ce6254a..c76b812 100644
--- a/generate_report.py
+++ b/generate_report.py
@@ -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
diff --git a/templates/brutalism.html b/templates/brutalism.html
index 45badff..f01d4c1 100644
--- a/templates/brutalism.html
+++ b/templates/brutalism.html
@@ -683,6 +683,7 @@
Top Games
By Category
+
By Runner
+
+
+
+
+
+ | # |
+ Runner |
+ Playtime |
+ % |
+
+
+
+
+
+
@@ -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 @@
'| No games match the selected filters |
';
document.getElementById('categories-table').innerHTML =
'| No categories found |
';
+ document.getElementById('runners-table').innerHTML =
+ '| No runners found |
';
return;
}
@@ -1199,6 +1229,71 @@
});
}
}
+
+ const runnersTbody = document.getElementById('runners-table');
+ runnersTbody.innerHTML = '';
+ if (runnersData.length === 0) {
+ runnersTbody.innerHTML = '| No runners found |
';
+ } 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 = `
+ ${index + 1} |
+
+
+ ${runner.name} ${runner.gameCount} games
+ |
+ ${formatTime(runner.playtime)} |
+ ${percent}% |
+ `;
+ 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 = `
+ ${othersIndex + 1} |
+
+
+ Others (${otherRunners.length} runners)
+ |
+ ${formatTime(othersPlaytime)} |
+ ${othersPercent}% |
+ `;
+ 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 = `
+ ${othersIndex + 1}.${otherIndex + 1} |
+
+ ${otherRunner.name} ${otherRunner.gameCount} games
+ |
+ ${formatTime(otherRunner.playtime)} |
+ ${otherPercent}% |
+ `;
+ runnersTbody.appendChild(detailRow);
+ detailRows.push(detailRow);
+ });
+
+ othersRow.addEventListener('click', () => {
+ othersRow.classList.toggle('expanded');
+ detailRows.forEach(dr => dr.classList.toggle('visible'));
+ });
+ }
+ }
}
filtersDiv.addEventListener('change', updateDisplay);
diff --git a/templates/glassmorphism.html b/templates/glassmorphism.html
index d7a03f1..49701c0 100644
--- a/templates/glassmorphism.html
+++ b/templates/glassmorphism.html
@@ -648,6 +648,7 @@
Top Games
By Category
+
By Runner
+
+
+
+
+
+ | # |
+ Runner |
+ Playtime |
+ % |
+
+
+
+
+
+
@@ -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 @@
'| No games match the selected filters |
';
document.getElementById('categories-table').innerHTML =
'| No categories found |
';
+ document.getElementById('runners-table').innerHTML =
+ '| No runners found |
';
return;
}
@@ -1143,6 +1173,71 @@
});
}
}
+
+ const runnersTbody = document.getElementById('runners-table');
+ runnersTbody.innerHTML = '';
+ if (runnersData.length === 0) {
+ runnersTbody.innerHTML = '| No runners found |
';
+ } 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 = `
+ ${index + 1} |
+
+
+ ${runner.name} ${runner.gameCount} games
+ |
+ ${formatTime(runner.playtime)} |
+ ${percent}% |
+ `;
+ 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 = `
+ ${othersIndex + 1} |
+
+
+ Others (${otherRunners.length} runners)
+ |
+ ${formatTime(othersPlaytime)} |
+ ${othersPercent}% |
+ `;
+ 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 = `
+ ${othersIndex + 1}.${otherIndex + 1} |
+
+ ${otherRunner.name} ${otherRunner.gameCount} games
+ |
+ ${formatTime(otherRunner.playtime)} |
+ ${otherPercent}% |
+ `;
+ runnersTbody.appendChild(detailRow);
+ detailRows.push(detailRow);
+ });
+
+ othersRow.addEventListener('click', () => {
+ othersRow.classList.toggle('expanded');
+ detailRows.forEach(dr => dr.classList.toggle('visible'));
+ });
+ }
+ }
}
filtersDiv.addEventListener('change', updateDisplay);
diff --git a/templates/neumorphism.html b/templates/neumorphism.html
index 2fca968..6712ba5 100644
--- a/templates/neumorphism.html
+++ b/templates/neumorphism.html
@@ -688,6 +688,7 @@
Top Games
By Category
+
By Runner
+
+
+
+
+
+ | # |
+ Runner |
+ Playtime |
+ % |
+
+
+
+
+
+
@@ -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 @@
'| No games match the selected filters |
';
document.getElementById('categories-table').innerHTML =
'| No categories found |
';
+ document.getElementById('runners-table').innerHTML =
+ '| No runners found |
';
return;
}
@@ -1183,6 +1213,71 @@
});
}
}
+
+ const runnersTbody = document.getElementById('runners-table');
+ runnersTbody.innerHTML = '';
+ if (runnersData.length === 0) {
+ runnersTbody.innerHTML = '| No runners found |
';
+ } 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 = `
+ ${index + 1} |
+
+
+ ${runner.name} ${runner.gameCount} games
+ |
+ ${formatTime(runner.playtime)} |
+ ${percent}% |
+ `;
+ 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 = `
+ ${othersIndex + 1} |
+
+
+ Others (${otherRunners.length} runners)
+ |
+ ${formatTime(othersPlaytime)} |
+ ${othersPercent}% |
+ `;
+ 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 = `
+ ${othersIndex + 1}.${otherIndex + 1} |
+
+ ${otherRunner.name} ${otherRunner.gameCount} games
+ |
+ ${formatTime(otherRunner.playtime)} |
+ ${otherPercent}% |
+ `;
+ runnersTbody.appendChild(detailRow);
+ detailRows.push(detailRow);
+ });
+
+ othersRow.addEventListener('click', () => {
+ othersRow.classList.toggle('expanded');
+ detailRows.forEach(dr => dr.classList.toggle('visible'));
+ });
+ }
+ }
}
filtersDiv.addEventListener('change', updateDisplay);
diff --git a/templates/platinum.html b/templates/platinum.html
index b431fe8..6f2fb9d 100644
--- a/templates/platinum.html
+++ b/templates/platinum.html
@@ -582,6 +582,7 @@
Top Games
By Category
+
By Runner
+
+
+
+
+
+ | # |
+ Runner |
+ Playtime |
+ % |
+
+
+
+
+
+
@@ -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 @@
'| No games match the selected filters |
';
document.getElementById('categories-table').innerHTML =
'| No categories found |
';
+ document.getElementById('runners-table').innerHTML =
+ '| No runners found |
';
return;
}
@@ -988,6 +1018,71 @@
});
}
}
+
+ const runnersTbody = document.getElementById('runners-table');
+ runnersTbody.innerHTML = '';
+ if (runnersData.length === 0) {
+ runnersTbody.innerHTML = '| No runners found |
';
+ } 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 = `
+ ${index + 1} |
+
+
+ ${runner.name} ${runner.gameCount} games
+ |
+ ${formatTime(runner.playtime)} |
+ ${percent}% |
+ `;
+ 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 = `
+ ${othersIndex + 1} |
+
+
+ Others (${otherRunners.length} runners)
+ |
+ ${formatTime(othersPlaytime)} |
+ ${othersPercent}% |
+ `;
+ 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 = `
+ ${othersIndex + 1}.${otherIndex + 1} |
+
+ ${otherRunner.name} ${otherRunner.gameCount} games
+ |
+ ${formatTime(otherRunner.playtime)} |
+ ${otherPercent}% |
+ `;
+ runnersTbody.appendChild(detailRow);
+ detailRows.push(detailRow);
+ });
+
+ othersRow.addEventListener('click', () => {
+ othersRow.classList.toggle('expanded');
+ detailRows.forEach(dr => dr.classList.toggle('visible'));
+ });
+ }
+ }
}
filtersDiv.addEventListener('change', updateDisplay);