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
@@ -715,6 +716,21 @@
+
+
+ + + + + + + + + + +
#RunnerPlaytime%
+
+
@@ -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
@@ -680,6 +681,21 @@
+
+
+ + + + + + + + + + +
#RunnerPlaytime%
+
+
@@ -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
@@ -720,6 +721,21 @@
+
+
+ + + + + + + + + + +
#RunnerPlaytime%
+
+
@@ -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
@@ -614,6 +615,21 @@
+
+
+ + + + + + + + + + +
#RunnerPlaytime%
+
+
@@ -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);