├── icons ├── pc.png ├── banner.png ├── default.png ├── running.ico ├── stopped.ico └── tracking.ico ├── ui ├── resources │ ├── images │ │ ├── 404.png │ │ ├── pc.png │ │ ├── hold.png │ │ ├── default.png │ │ ├── dropped.png │ │ ├── favicon.ico │ │ ├── finished.png │ │ ├── forever.png │ │ ├── playing.png │ │ └── 404-tutorial.gif │ ├── css │ │ ├── common.css │ │ ├── GamingTime-calendar.css │ │ ├── calendar-controls.css │ │ ├── theme-light.css │ │ ├── theme-dark.css │ │ ├── GamesPerPlatform.css │ │ ├── PCvsEmulation.css │ │ ├── GamingTime.css │ │ ├── IdleTime.css │ │ ├── MostPlayed.css │ │ ├── SessionHistory │ │ │ ├── SessionHistory-calendar.css │ │ │ ├── SessionHistory-cards.css │ │ │ ├── SessionHistory-shared.css │ │ │ └── SessionHistory-games.css │ │ ├── Summary.css │ │ └── AllGames.css │ └── js │ │ ├── PCvsEmulation.js │ │ ├── GamesPerPlatform.js │ │ ├── Manual.js │ │ ├── GameVsTime.js │ │ ├── SessionHistory │ │ ├── SessionHistory-cards.js │ │ ├── SessionHistory-calendar.js │ │ └── SessionHistory-shared.js │ │ ├── calendar-controls.js │ │ └── common.js ├── templates │ ├── Manual.html.template │ ├── PCvsEmulation.html.template │ ├── GamesPerPlatform.html.template │ ├── IdleTime.html.template │ ├── MostPlayed.html.template │ ├── GamingTime.html.template │ ├── Summary.html.template │ ├── AllGames.html.template │ └── SessionHistory.html.template └── 404.html ├── readme-files ├── GamingGaidenBanner.png └── SocialMediaPreview.png ├── modules ├── PSSQLite │ └── 1.1.0 │ │ ├── PSGetModuleInfo.xml │ │ ├── x64 │ │ ├── SQLite.Interop.dll │ │ └── System.Data.SQLite.dll │ │ ├── x86 │ │ ├── SQLite.Interop.dll │ │ └── System.Data.SQLite.dll │ │ ├── core │ │ ├── win-x64 │ │ │ ├── SQLite.Interop.dll │ │ │ └── System.Data.SQLite.dll │ │ └── win-x86 │ │ │ ├── SQLite.Interop.dll │ │ │ └── System.Data.SQLite.dll │ │ ├── Update-Sqlite.ps1 │ │ ├── PSSQLite.psm1 │ │ ├── PSSQLite.psd1 │ │ ├── New-SqliteConnection.ps1 │ │ └── Out-DataTable.ps1 ├── ThreadJob │ └── 2.0.3 │ │ ├── PSGetModuleInfo.xml │ │ └── Microsoft.PowerShell.ThreadJob.dll ├── UserInput.psm1 ├── HelperFunctions.psm1 ├── SetupDatabase.psm1 ├── QueryFunctions.psm1 └── ProcessFunctions.psm1 ├── .gitignore ├── .vscode └── tasks.json ├── Uninstall.bat ├── Build.ps1 ├── Install.bat ├── Manual.md └── Readme.md /icons/pc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kulvind3r/GamingGaiden/HEAD/icons/pc.png -------------------------------------------------------------------------------- /icons/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kulvind3r/GamingGaiden/HEAD/icons/banner.png -------------------------------------------------------------------------------- /icons/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kulvind3r/GamingGaiden/HEAD/icons/default.png -------------------------------------------------------------------------------- /icons/running.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kulvind3r/GamingGaiden/HEAD/icons/running.ico -------------------------------------------------------------------------------- /icons/stopped.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kulvind3r/GamingGaiden/HEAD/icons/stopped.ico -------------------------------------------------------------------------------- /icons/tracking.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kulvind3r/GamingGaiden/HEAD/icons/tracking.ico -------------------------------------------------------------------------------- /ui/resources/images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kulvind3r/GamingGaiden/HEAD/ui/resources/images/404.png -------------------------------------------------------------------------------- /ui/resources/images/pc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kulvind3r/GamingGaiden/HEAD/ui/resources/images/pc.png -------------------------------------------------------------------------------- /ui/resources/images/hold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kulvind3r/GamingGaiden/HEAD/ui/resources/images/hold.png -------------------------------------------------------------------------------- /ui/resources/images/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kulvind3r/GamingGaiden/HEAD/ui/resources/images/default.png -------------------------------------------------------------------------------- /ui/resources/images/dropped.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kulvind3r/GamingGaiden/HEAD/ui/resources/images/dropped.png -------------------------------------------------------------------------------- /ui/resources/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kulvind3r/GamingGaiden/HEAD/ui/resources/images/favicon.ico -------------------------------------------------------------------------------- /ui/resources/images/finished.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kulvind3r/GamingGaiden/HEAD/ui/resources/images/finished.png -------------------------------------------------------------------------------- /ui/resources/images/forever.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kulvind3r/GamingGaiden/HEAD/ui/resources/images/forever.png -------------------------------------------------------------------------------- /ui/resources/images/playing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kulvind3r/GamingGaiden/HEAD/ui/resources/images/playing.png -------------------------------------------------------------------------------- /readme-files/GamingGaidenBanner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kulvind3r/GamingGaiden/HEAD/readme-files/GamingGaidenBanner.png -------------------------------------------------------------------------------- /readme-files/SocialMediaPreview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kulvind3r/GamingGaiden/HEAD/readme-files/SocialMediaPreview.png -------------------------------------------------------------------------------- /ui/resources/images/404-tutorial.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kulvind3r/GamingGaiden/HEAD/ui/resources/images/404-tutorial.gif -------------------------------------------------------------------------------- /modules/PSSQLite/1.1.0/PSGetModuleInfo.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kulvind3r/GamingGaiden/HEAD/modules/PSSQLite/1.1.0/PSGetModuleInfo.xml -------------------------------------------------------------------------------- /modules/PSSQLite/1.1.0/x64/SQLite.Interop.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kulvind3r/GamingGaiden/HEAD/modules/PSSQLite/1.1.0/x64/SQLite.Interop.dll -------------------------------------------------------------------------------- /modules/PSSQLite/1.1.0/x86/SQLite.Interop.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kulvind3r/GamingGaiden/HEAD/modules/PSSQLite/1.1.0/x86/SQLite.Interop.dll -------------------------------------------------------------------------------- /modules/ThreadJob/2.0.3/PSGetModuleInfo.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kulvind3r/GamingGaiden/HEAD/modules/ThreadJob/2.0.3/PSGetModuleInfo.xml -------------------------------------------------------------------------------- /modules/PSSQLite/1.1.0/x64/System.Data.SQLite.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kulvind3r/GamingGaiden/HEAD/modules/PSSQLite/1.1.0/x64/System.Data.SQLite.dll -------------------------------------------------------------------------------- /modules/PSSQLite/1.1.0/x86/System.Data.SQLite.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kulvind3r/GamingGaiden/HEAD/modules/PSSQLite/1.1.0/x86/System.Data.SQLite.dll -------------------------------------------------------------------------------- /modules/PSSQLite/1.1.0/core/win-x64/SQLite.Interop.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kulvind3r/GamingGaiden/HEAD/modules/PSSQLite/1.1.0/core/win-x64/SQLite.Interop.dll -------------------------------------------------------------------------------- /modules/PSSQLite/1.1.0/core/win-x86/SQLite.Interop.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kulvind3r/GamingGaiden/HEAD/modules/PSSQLite/1.1.0/core/win-x86/SQLite.Interop.dll -------------------------------------------------------------------------------- /modules/PSSQLite/1.1.0/core/win-x64/System.Data.SQLite.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kulvind3r/GamingGaiden/HEAD/modules/PSSQLite/1.1.0/core/win-x64/System.Data.SQLite.dll -------------------------------------------------------------------------------- /modules/PSSQLite/1.1.0/core/win-x86/System.Data.SQLite.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kulvind3r/GamingGaiden/HEAD/modules/PSSQLite/1.1.0/core/win-x86/System.Data.SQLite.dll -------------------------------------------------------------------------------- /modules/ThreadJob/2.0.3/Microsoft.PowerShell.ThreadJob.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kulvind3r/GamingGaiden/HEAD/modules/ThreadJob/2.0.3/Microsoft.PowerShell.ThreadJob.dll -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.code-workspace 3 | *.db 4 | *.mp4 5 | *.sql 6 | *.bak 7 | .claude/* 8 | ui/resources/images/cache/* 9 | ui/*.html 10 | !ui/404.html 11 | backups/* 12 | build/* 13 | ignored_* 14 | CLAUDE.md 15 | coderules.md 16 | .serena/ 17 | theme.css 18 | settings.ini -------------------------------------------------------------------------------- /ui/resources/css/common.css: -------------------------------------------------------------------------------- 1 | /* Common Navigation Bar Styles */ 2 | #navigation-bar { 3 | display: flex; 4 | justify-content: space-between; 5 | width: 100%; 6 | max-width: 1840px; 7 | margin: 0 auto; 8 | flex-shrink: 0; 9 | gap: 10px; 10 | } 11 | 12 | #navigation-bar .custom-button { 13 | flex: 1; 14 | min-width: 0; 15 | margin: 0 5px; 16 | } 17 | 18 | .help-button { 19 | flex: 0 0 50px !important; 20 | background-color: var(--accent-blue) !important; 21 | color: var(--text-inverse) !important; 22 | font-weight: bold; 23 | font-size: 20px !important; 24 | } 25 | -------------------------------------------------------------------------------- /ui/resources/css/GamingTime-calendar.css: -------------------------------------------------------------------------------- 1 | /* ===== GAMINGTIME-SPECIFIC CALENDAR STYLES ===== */ 2 | 3 | #calendar-column { 4 | flex: 0 0 380px; 5 | display: flex; 6 | flex-direction: column; 7 | background: var(--bg-tertiary); 8 | border-radius: 8px; 9 | padding: 20px; 10 | box-sizing: border-box; 11 | } 12 | 13 | /* Year Display - GamingTime-specific interactive styles */ 14 | #year-display { 15 | cursor: pointer; 16 | user-select: none; 17 | transition: all 0.2s; 18 | padding: 5px 10px; 19 | border-radius: 4px; 20 | } 21 | 22 | #year-display:hover { 23 | background-color: var(--accent-blue-light); 24 | transform: scale(1.05); 25 | } 26 | 27 | #year-display.yearly-mode { 28 | background-color: var(--accent-blue); 29 | color: var(--text-inverse); 30 | } 31 | 32 | /* ===== CHART COLUMN ===== */ 33 | 34 | #chart-column { 35 | flex: 1; 36 | display: flex; 37 | flex-direction: column; 38 | min-width: 0; 39 | } 40 | -------------------------------------------------------------------------------- /modules/PSSQLite/1.1.0/Update-Sqlite.ps1: -------------------------------------------------------------------------------- 1 | function Update-Sqlite { 2 | [CmdletBinding()] 3 | 4 | param( 5 | [Parameter()] 6 | [string] 7 | $version = '1.0.112', 8 | 9 | [Parameter()] 10 | [ValidateSet('linux-x64','osx-x64','win-x64','win-x86')] 11 | [string] 12 | $OS 13 | ) 14 | 15 | Process { 16 | write-verbose "Creating build directory" 17 | New-Item -ItemType directory build 18 | Set-Location build 19 | 20 | $file = "system.data.sqlite.core.$version" 21 | 22 | write-verbose "downloading files from nuget" 23 | $dl = @{ 24 | uri = "https://www.nuget.org/api/v2/package/System.Data.SQLite.Core/$version" 25 | outfile = "$file.nupkg" 26 | } 27 | Invoke-WebRequest @dl 28 | 29 | write-verbose "unpacking and copying files to module directory" 30 | Expand-Archive $dl.outfile 31 | 32 | $InstallPath = (get-module PSSQlite).path.TrimEnd('PSSQLite.psm1') 33 | copy-item $file/lib/netstandard2.0/System.Data.SQLite.dll $InstallPath/core/$os/ 34 | copy-item $file/runtimes/$os/native/netstandard2.0/SQLite.Interop.dll $InstallPath/core/$os/ 35 | 36 | write-verbose "removing build folder" 37 | Set-location .. 38 | remove-item ./build -recurse 39 | write-verbose "complete" 40 | 41 | Write-Warning "Please reimport the module to use the latest files" 42 | } 43 | } 44 | 45 | -------------------------------------------------------------------------------- /modules/UserInput.psm1: -------------------------------------------------------------------------------- 1 | Add-Type @' 2 | using System; 3 | using System.Diagnostics; 4 | using System.Runtime.InteropServices; 5 | 6 | namespace PInvoke.Win32 { 7 | 8 | public static class UserInput { 9 | 10 | [DllImport("user32.dll", SetLastError=false)] 11 | private static extern bool GetLastInputInfo(ref LASTINPUTINFO plii); 12 | 13 | [StructLayout(LayoutKind.Sequential)] 14 | private struct LASTINPUTINFO { 15 | public uint cbSize; 16 | public int dwTime; 17 | } 18 | 19 | public static DateTime LastInput { 20 | get { 21 | DateTime bootTime = DateTime.UtcNow.AddMilliseconds(-Environment.TickCount); 22 | DateTime lastInput = bootTime.AddMilliseconds(LastInputTicks); 23 | return lastInput; 24 | } 25 | } 26 | 27 | public static TimeSpan IdleTime { 28 | get { 29 | return DateTime.UtcNow.Subtract(LastInput); 30 | } 31 | } 32 | 33 | public static int LastInputTicks { 34 | get { 35 | LASTINPUTINFO lii = new LASTINPUTINFO(); 36 | lii.cbSize = (uint)Marshal.SizeOf(typeof(LASTINPUTINFO)); 37 | GetLastInputInfo(ref lii); 38 | return lii.dwTime; 39 | } 40 | } 41 | } 42 | } 43 | '@ -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "Build Gaming Gaiden", 6 | "type": "shell", 7 | "command": "powershell", 8 | "args": [ 9 | "-NoLogo", 10 | "-ExecutionPolicy", 11 | "bypass", 12 | "-File", 13 | "./Build.ps1" 14 | ], 15 | "group": { 16 | "kind": "build", 17 | "isDefault": true 18 | }, 19 | "presentation": { 20 | "reveal": "silent", 21 | "revealProblems": "onProblem", 22 | "close": true, 23 | "echo": true 24 | } 25 | }, 26 | { 27 | "label": "Run Gaming Gaiden", 28 | "type": "shell", 29 | "command": "powershell", 30 | "args": [ 31 | "-NoLogo", 32 | "-ExecutionPolicy", 33 | "bypass", 34 | "-File", 35 | "./GamingGaiden.ps1" 36 | ], 37 | "options": { 38 | "env": { 39 | "GAIDEN_DEV_MODE": "true" 40 | } 41 | }, 42 | "group": { 43 | "kind": "test", 44 | "isDefault": true 45 | }, 46 | "presentation": { 47 | "reveal": "silent", 48 | "revealProblems": "onProblem", 49 | "close": true, 50 | "echo": true 51 | } 52 | } 53 | ] 54 | } -------------------------------------------------------------------------------- /ui/resources/js/PCvsEmulation.js: -------------------------------------------------------------------------------- 1 | /*global ChartDataLabels, Chart, chartTooltipConfig, chartLegendConfig, chartDataLabelFontConfig, buildGamingData, getChartTextColor, getChartBackgroundColor*/ 2 | /*from chart.js, common.js*/ 3 | 4 | let gamingData = []; 5 | 6 | $("table")[0].setAttribute("id", "data-table"); 7 | 8 | function updateChart() { 9 | const ctx = document.getElementById("pc-vs-emulation-chart").getContext("2d"); 10 | 11 | new Chart(ctx, { 12 | type: "pie", 13 | plugins: [ChartDataLabels], 14 | data: { 15 | labels: gamingData.map((row) => row.platform), 16 | datasets: [ 17 | { 18 | data: gamingData.map((row) => (row.play_time / 60).toFixed(1)), 19 | borderWidth: 2, 20 | borderColor: getChartBackgroundColor(), 21 | backgroundColor: [ 22 | "#1ea1e6", 23 | "#ff6481", 24 | "#3dbebe", 25 | "#ff9d4c", 26 | "#9669f8", 27 | "#ffca63", 28 | "#c7c9cd" 29 | ], 30 | }, 31 | ], 32 | }, 33 | options: { 34 | responsive: true, 35 | maintainAspectRatio: false, 36 | plugins: { 37 | tooltip: chartTooltipConfig, 38 | legend: chartLegendConfig, 39 | datalabels: { 40 | formatter: function (value) { 41 | return value + " Hrs"; 42 | }, 43 | color: getChartTextColor(), 44 | font: chartDataLabelFontConfig, 45 | }, 46 | }, 47 | }, 48 | }); 49 | } 50 | 51 | function loadDataFromTable() { 52 | gamingData = buildGamingData("platform", "play_time"); 53 | updateChart(); 54 | } 55 | 56 | loadDataFromTable(); 57 | -------------------------------------------------------------------------------- /ui/resources/js/GamesPerPlatform.js: -------------------------------------------------------------------------------- 1 | /*global ChartDataLabels, Chart, chartTooltipConfig, chartLegendConfig, chartDataLabelFontConfig, buildGamingData, getChartTextColor, getChartBackgroundColor*/ 2 | /*from chart.js, common.js*/ 3 | let gamingData = []; 4 | 5 | $("table")[0].setAttribute("id", "data-table"); 6 | 7 | function updateChart() { 8 | const ctx = document 9 | .getElementById("games-per-platform-chart") 10 | .getContext("2d"); 11 | 12 | new Chart(ctx, { 13 | type: "doughnut", 14 | plugins: [ChartDataLabels], 15 | data: { 16 | labels: gamingData.map((row) => row.name), 17 | datasets: [ 18 | { 19 | data: gamingData.map((row) => row.count), 20 | borderWidth: 2, 21 | borderColor: getChartBackgroundColor(), 22 | backgroundColor: [ 23 | "#ff6384", 24 | "#36a2eb", 25 | "#ff9f40", 26 | "#4e79a7", 27 | "#e91e63", 28 | "#26a69a", 29 | "#7e57c2", 30 | "#4caf50", 31 | "#ff7043", 32 | "#5c6bc0", 33 | "#8e24aa", 34 | "#d84315", 35 | "#0288d1" 36 | ], 37 | }, 38 | ], 39 | }, 40 | options: { 41 | responsive: true, 42 | maintainAspectRatio: false, 43 | plugins: { 44 | tooltip: chartTooltipConfig, 45 | legend: chartLegendConfig, 46 | datalabels: { 47 | color: getChartTextColor(), 48 | font: chartDataLabelFontConfig, 49 | }, 50 | }, 51 | }, 52 | }); 53 | } 54 | 55 | function loadDataFromTable() { 56 | gamingData = buildGamingData("name", "count"); 57 | updateChart(); 58 | } 59 | 60 | loadDataFromTable(); 61 | -------------------------------------------------------------------------------- /Uninstall.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal enabledelayedexpansion 3 | 4 | set "InstallDirectory=%ALLUSERSPROFILE%\GamingGaiden" 5 | set "DesktopPath=%USERPROFILE%\Desktop" 6 | set "StartupPath=%APPDATA%\Microsoft\Windows\Start Menu\Programs\StartUp" 7 | set "StartMenuPath=%APPDATA%\Microsoft\Windows\Start Menu\Programs\Gaming Gaiden" 8 | 9 | echo Gaming Gaiden Uninstaller 10 | echo. 11 | echo This will remove Gaming Gaiden but preserve your data. 12 | echo Your database and backups will remain at %InstallDirectory% 13 | echo. 14 | set /p UninstallChoice="Do you want to uninstall Gaming Gaiden? Yes/No: " 15 | 16 | if /i not "%UninstallChoice%"=="Yes" if /i not "%UninstallChoice%"=="Y" ( 17 | echo Uninstall cancelled by user. 18 | echo. 19 | pause 20 | exit /b 0 21 | ) 22 | 23 | echo. 24 | echo Closing Gaming Gaiden if running 25 | taskkill /f /im GamingGaiden.exe 2>nul 26 | 27 | echo Removing shortcuts 28 | del "%DesktopPath%\Gaming Gaiden.lnk" 2>nul 29 | del "%StartupPath%\Gaming Gaiden.lnk" 2>nul 30 | rd /s /q "%StartMenuPath%" 2>nul 31 | 32 | echo Removing application files 33 | powershell.exe -NoProfile -Command "$items = Get-ChildItem '%InstallDirectory%' -Exclude backups,GamingGaiden.db,Uninstall.bat -ErrorAction SilentlyContinue; $hasExpected = $items | Where-Object { $_.Name -match '^(modules|icons|ui|GamingGaiden\.exe)$' }; if ($items.Count -gt 0 -and -not $hasExpected) { Write-Host 'ERROR: Install directory does not look like Gaming Gaiden. Aborting.'; exit 1 }; $items | Remove-Item -Recurse -Force" 34 | if errorlevel 1 ( 35 | echo. 36 | echo Uninstall aborted for safety. Directory does not appear to be Gaming Gaiden. 37 | pause 38 | exit /b 1 39 | ) 40 | 41 | echo. 42 | echo Uninstall complete. 43 | echo. 44 | echo Your data has been preserved at %InstallDirectory% 45 | echo - GamingGaiden.db (your game tracking database) 46 | echo - backups\ (your backup files) 47 | echo. 48 | echo To completely remove all data, manually delete %InstallDirectory% 49 | echo. 50 | pause -------------------------------------------------------------------------------- /ui/resources/css/calendar-controls.css: -------------------------------------------------------------------------------- 1 | /* ===== SHARED CALENDAR CONTROLS ===== */ 2 | /* Used by both GamingTime and SessionHistory views */ 3 | 4 | /* Calendar Container */ 5 | #calendar-container { 6 | display: flex; 7 | flex-direction: column; 8 | gap: 20px; 9 | height: 100%; 10 | justify-content: center; 11 | } 12 | 13 | /* ===== YEAR NAVIGATION ===== */ 14 | 15 | #year-navigation { 16 | display: flex; 17 | justify-content: space-around; 18 | align-items: center; 19 | padding: 10px 0; 20 | border-bottom: 2px solid var(--border-light); 21 | } 22 | 23 | #year-display { 24 | font-family: monospace; 25 | font-size: 18px; 26 | font-weight: bold; 27 | text-align: center; 28 | flex: 1; 29 | max-width: 150px; 30 | color: var(--text-primary); 31 | } 32 | 33 | .calendar-nav-btn { 34 | height: 32px; 35 | padding-left: 12px; 36 | padding-right: 12px; 37 | font-size: 16px; 38 | } 39 | 40 | /* ===== MONTH GRID ===== */ 41 | 42 | #month-grid { 43 | display: grid; 44 | grid-template-columns: repeat(3, 1fr); 45 | gap: 8px; 46 | padding: 10px 0; 47 | } 48 | 49 | .month-btn { 50 | padding: 12px; 51 | font-size: 13px; 52 | font-family: monospace; 53 | background-color: var(--bg-primary); 54 | color: var(--text-secondary); 55 | border: 1px solid var(--border-light); 56 | border-radius: 4px; 57 | cursor: pointer; 58 | transition: all 0.2s; 59 | } 60 | 61 | .month-btn:hover { 62 | background-color: var(--bg-button-hover); 63 | transform: translateY(-1px); 64 | } 65 | 66 | .month-btn.selected { 67 | background-color: var(--accent-blue); 68 | color: var(--text-inverse); 69 | font-weight: bold; 70 | border-color: var(--accent-blue-dark); 71 | } 72 | 73 | .month-btn.has-data { 74 | background-color: var(--accent-blue-light); 75 | border-color: var(--accent-blue-light); 76 | } 77 | 78 | .month-btn.has-data.selected { 79 | background-color: var(--accent-blue); 80 | border-color: var(--accent-blue-dark); 81 | } 82 | 83 | .month-btn:disabled { 84 | opacity: 0.3; 85 | cursor: not-allowed; 86 | } 87 | -------------------------------------------------------------------------------- /ui/resources/js/Manual.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', function() { 2 | // Wrap content in main-panel 3 | const contentWrapper = document.getElementById('content-wrapper'); 4 | const mainPanel = document.createElement('div'); 5 | mainPanel.id = 'main-panel'; 6 | mainPanel.innerHTML = contentWrapper.innerHTML; 7 | contentWrapper.innerHTML = ''; 8 | contentWrapper.appendChild(mainPanel); 9 | 10 | const modal = document.getElementById('modal-overlay'); 11 | const modalBody = document.getElementById('modal-body'); 12 | const closeBtn = document.querySelector('.modal-close'); 13 | 14 | // Prevent default details behavior and show modal instead 15 | document.querySelectorAll('details').forEach(details => { 16 | details.addEventListener('toggle', function(e) { 17 | if (this.open) { 18 | e.preventDefault(); 19 | 20 | // Get the content (everything except summary) 21 | const content = Array.from(this.children) 22 | .filter(child => child.tagName !== 'SUMMARY') 23 | .map(child => child.cloneNode(true)); 24 | 25 | // Get summary text for modal title 26 | const summaryText = this.querySelector('summary').textContent; 27 | 28 | // Clear and populate modal 29 | modalBody.innerHTML = ''; 30 | content.forEach(node => modalBody.appendChild(node)); 31 | 32 | // Show modal 33 | modal.classList.add('active'); 34 | 35 | // Close the details element 36 | this.open = false; 37 | } 38 | }); 39 | }); 40 | 41 | // Close modal on X click 42 | closeBtn.addEventListener('click', function() { 43 | modal.classList.remove('active'); 44 | }); 45 | 46 | // Close modal on overlay click 47 | modal.addEventListener('click', function(e) { 48 | if (e.target === modal) { 49 | modal.classList.remove('active'); 50 | } 51 | }); 52 | 53 | // Close modal on ESC key 54 | document.addEventListener('keydown', function(e) { 55 | if (e.key === 'Escape' && modal.classList.contains('active')) { 56 | modal.classList.remove('active'); 57 | } 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /modules/PSSQLite/1.1.0/PSSQLite.psm1: -------------------------------------------------------------------------------- 1 | #handle PS2 2 | if(-not $PSScriptRoot) 3 | { 4 | $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent 5 | } 6 | 7 | #Pick and import assemblies: 8 | if($PSEdition -eq 'core') 9 | { 10 | if ($isWindows) { 11 | if([IntPtr]::size -eq 8) { #64 12 | write-verbose "loading win-x64 core" 13 | $SQLiteAssembly = Join-path $PSScriptRoot "core\win-x64\System.Data.SQLite.dll" 14 | } 15 | elseif([IntPtr]::size -eq 4) { #32 16 | write-verbose "loading win-x32 core" 17 | $SQLiteAssembly = Join-path $PSScriptRoot "core\win-x86\System.Data.SQLite.dll" 18 | } 19 | } 20 | write-verbose -message "is PS Core, loading dotnet core dll" 21 | } 22 | elseif([IntPtr]::size -eq 8) #64 23 | { 24 | write-verbose -message "is x64, loading..." 25 | $SQLiteAssembly = Join-path $PSScriptRoot "x64\System.Data.SQLite.dll" 26 | } 27 | elseif([IntPtr]::size -eq 4) #32 28 | { 29 | $SQLiteAssembly = Join-path $PSScriptRoot "x86\System.Data.SQLite.dll" 30 | } 31 | else 32 | { 33 | Throw "Something is odd with bitness..." 34 | } 35 | 36 | if( -not ($Library = Add-Type -path $SQLiteAssembly -PassThru -ErrorAction stop) ) 37 | { 38 | Throw "This module requires the ADO.NET driver for SQLite:`n`thttp://system.data.sqlite.org/index.html/doc/trunk/www/downloads.wiki" 39 | } 40 | 41 | #Get public and private function definition files. 42 | $Public = Get-ChildItem $PSScriptRoot\*.ps1 -ErrorAction SilentlyContinue 43 | #$Private = Get-ChildItem $PSScriptRoot\Private\*.ps1 -ErrorAction SilentlyContinue 44 | 45 | #Dot source the files 46 | Foreach($import in @($Public)) 47 | { 48 | Try 49 | { 50 | #PS2 compatibility 51 | if($import.fullname) 52 | { 53 | . $import.fullname 54 | } 55 | } 56 | Catch 57 | { 58 | Write-Error "Failed to import function $($import.fullname): $_" 59 | } 60 | } 61 | 62 | #Create some aliases, export public functions 63 | Export-ModuleMember -Function $($Public | Select -ExpandProperty BaseName) 64 | -------------------------------------------------------------------------------- /Build.ps1: -------------------------------------------------------------------------------- 1 | [System.Reflection.Assembly]::LoadWithPartialName('System.Web') | out-null 2 | 3 | #------------------------------------------ 4 | # Pre Build Cleanup 5 | Remove-Item -Recurse .\build\GamingGaiden 6 | Remove-Item -Recurse .\build\GamingGaiden.zip 7 | mkdir -f .\build\GamingGaiden 8 | 9 | Get-ChildItem -File .\ui\*.html -Exclude 404.html | Remove-Item 10 | Remove-Item -Recurse .\ui\resources\images\cache -ErrorAction SilentlyContinue 11 | 12 | #------------------------------------------ 13 | # Build 14 | 15 | # Generate Manual 16 | pandoc.exe --ascii .\Manual.md -o .\ui\Manual.html 17 | $ManualHTML = Get-Content .\ui\Manual.html -Raw 18 | 19 | # Wrap each h3 and its following content until next h3 20 | $ManualHTML = $ManualHTML -replace ']*>([^<]+)((?:(?!$1$2' 21 | 22 | # Wrap all details in a container for column layout 23 | $ManualHTML = $ManualHTML -replace '(
[\s\S]*
)', '
$1
' 24 | 25 | $ManualTemplate = Get-Content .\ui\templates\Manual.html.template 26 | $FinalHTML = $ManualTemplate -replace "_MARKDOWN_HTML_", $ManualHTML 27 | [System.Web.HttpUtility]::HtmlDecode($FinalHTML) | Out-File -encoding UTF8 .\ui\Manual.html 28 | 29 | # Copy source files 30 | $SourceFiles = ".\Install.bat", ".\Uninstall.bat", ".\modules", ".\icons", ".\ui" 31 | Copy-Item -Recurse -Path $SourceFiles -Destination .\build\GamingGaiden\ -Force 32 | 33 | # Add 404 pages 34 | $templateFiles = Get-ChildItem .\ui\templates\*.template -File 35 | foreach ($template in $templateFiles) { 36 | $htmlFileName = $template.Name -replace '\.template$', '' 37 | if ($htmlFileName -ne "Manual.html") { 38 | Copy-Item -Path .\ui\404.html -Destination .\build\GamingGaiden\ui\$htmlFileName -Force 39 | } 40 | } 41 | 42 | # Generate exe 43 | ps12exe -inputFile ".\GamingGaiden.ps1" -outputFile ".\build\GamingGaiden\GamingGaiden.exe" 44 | 45 | # Package 46 | Compress-Archive -Force -Path .\build\GamingGaiden -DestinationPath .\build\GamingGaiden.zip 47 | 48 | #------------------------------------------ 49 | # Post Build Cleanup 50 | Remove-Item -Recurse .\build\GamingGaiden -------------------------------------------------------------------------------- /ui/resources/css/theme-light.css: -------------------------------------------------------------------------------- 1 | /* Gaming Gaiden - Light Theme */ 2 | 3 | :root { 4 | /* Background Colors */ 5 | --bg-primary: #f5f5f5; /* Main body background */ 6 | --bg-panel: white; /* Panels, cards, secondary surfaces */ 7 | --bg-tertiary: #fafafa; /* Calendar columns, game items, tertiary areas */ 8 | --bg-button: #fcfcfd; /* Button default background */ 9 | --bg-button-hover: #e8e8e8; /* Button hover state */ 10 | --bg-table-stripe: #cddbe7; /* Table row striping */ 11 | 12 | /* Text Colors */ 13 | --text-primary: #333; /* Primary text (headings, body, buttons) */ 14 | --text-secondary: #666; /* Secondary text (game details, labels, stats) */ 15 | --text-inverse: #fff; /* Text on dark backgrounds */ 16 | 17 | /* Border Colors */ 18 | --border-primary: #333; /* Table headers, strong dividers */ 19 | --border-light: #ddd; /* All other borders, dividers, inputs */ 20 | 21 | /* Shadow Colors */ 22 | --shadow-primary: rgba(45, 35, 66, 0.4); /* Outer shadow (short) */ 23 | --shadow-secondary: rgba(45, 35, 66, 0.3); /* Outer shadow (diffused) */ 24 | --shadow-inset: #d6d6e7; /* Button inset (purple-tinged) */ 25 | --shadow-image: gray; /* Image drop shadows */ 26 | 27 | /* Accent Colors - Blue */ 28 | --accent-blue: #2196F3; 29 | --accent-blue-dark: #1976D2; 30 | --accent-blue-light: #e3f2fd; 31 | --accent-link-hover: #06e; 32 | 33 | /* Status Colors */ 34 | --status-finished: #059b27; /* Green - Finished games */ 35 | --status-finished-alt: #4CAF50; 36 | --status-hold: #d78f34; /* Orange - On hold */ 37 | --status-forever: #94979c; /* Gray - Forever games */ 38 | --status-dropped: #662f13; /* Brown - Dropped games */ 39 | --status-error: #d32f2f; /* Red - Error states */ 40 | --status-error-dark: #b71c1c; 41 | --status-error-light: #ef9a9a; 42 | --status-error-pale: #ffebee; 43 | 44 | /* Chart Colors */ 45 | --chart-blue: rgba(55, 162, 235, 0.5); 46 | --chart-grid: rgba(0, 0, 0, 0.1); /* Chart grid lines */ 47 | 48 | /* Table Specific */ 49 | --table-header-bg: #333; 50 | 51 | /* Scrollbar Colors */ 52 | --scrollbar-track: #f1f1f1; 53 | --scrollbar-thumb: #888; 54 | --scrollbar-thumb-hover: #555; 55 | } 56 | -------------------------------------------------------------------------------- /ui/resources/css/theme-dark.css: -------------------------------------------------------------------------------- 1 | /* Gaming Gaiden - Dark Theme */ 2 | 3 | :root { 4 | /* Background Colors */ 5 | --bg-primary: #1a1a1a; /* Main body background */ 6 | --bg-panel: #2a2a2a; /* Panels, cards, secondary surfaces */ 7 | --bg-tertiary: #222222; /* Calendar columns, game items, tertiary areas */ 8 | --bg-button: #3a3a3a; /* Button default background */ 9 | --bg-button-hover: #4a4a4a; /* Button hover state */ 10 | --bg-table-stripe: #2d3a47; /* Table row striping */ 11 | 12 | /* Text Colors */ 13 | --text-primary: #e0e0e0; /* Primary text (headings, body, buttons) */ 14 | --text-secondary: #b0b0b0; /* Secondary text (game details, labels, stats) */ 15 | --text-inverse: #1a1a1a; /* Text on light backgrounds */ 16 | 17 | /* Border Colors */ 18 | --border-primary: #555; /* Table headers, strong dividers */ 19 | --border-light: #4a4a4a; /* All other borders, dividers, inputs */ 20 | 21 | /* Shadow Colors */ 22 | --shadow-primary: rgba(0, 0, 0, 0.6); /* Outer shadow (short) */ 23 | --shadow-secondary: rgba(0, 0, 0, 0.4); /* Outer shadow (diffused) */ 24 | --shadow-inset: #1a1a2a; /* Button inset */ 25 | --shadow-image: rgba(0, 0, 0, 0.8); /* Image drop shadows */ 26 | 27 | /* Accent Colors - Blue (adjusted for dark mode visibility) */ 28 | --accent-blue: #42a5f5; 29 | --accent-blue-dark: #1e88e5; 30 | --accent-blue-light: #1e3a5f; 31 | --accent-link-hover: #64b5f6; 32 | 33 | /* Status Colors (adjusted for dark mode visibility) */ 34 | --status-finished: #06c930; /* Brighter green for dark mode */ 35 | --status-finished-alt: #66BB6A; 36 | --status-hold: #ffa726; /* Brighter orange */ 37 | --status-forever: #b0b3b8; /* Lighter gray */ 38 | --status-dropped: #a1694f; /* Lighter brown */ 39 | --status-error: #ef5350; /* Brighter red */ 40 | --status-error-dark: #c62828; 41 | --status-error-light: #e57373; 42 | --status-error-pale: #4a2424; 43 | 44 | /* Chart Colors */ 45 | --chart-blue: rgba(66, 165, 245, 0.5); 46 | --chart-grid: rgba(255, 255, 255, 0.1); /* Chart grid lines */ 47 | 48 | /* Table Specific */ 49 | --table-header-bg: #1a1a1a; 50 | 51 | /* Scrollbar Colors */ 52 | --scrollbar-track: #2a2a2a; 53 | --scrollbar-thumb: #555; 54 | --scrollbar-thumb-hover: #777; 55 | } 56 | -------------------------------------------------------------------------------- /ui/templates/Manual.html.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Gaming Gaiden 5 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |

Help / FAQ

17 |
_MARKDOWN_HTML_
18 | 24 | 25 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ui/resources/css/GamesPerPlatform.css: -------------------------------------------------------------------------------- 1 | body { 2 | display: flex; 3 | flex-direction: column; 4 | font-family: monospace; 5 | background-color: var(--bg-primary); 6 | margin: 0; 7 | padding: 20px; 8 | height: 100vh; 9 | overflow: hidden; 10 | box-sizing: border-box; 11 | } 12 | 13 | h1 { 14 | text-align: center; 15 | margin-bottom: 20px; 16 | color: var(--text-primary); 17 | flex-shrink: 0; 18 | } 19 | 20 | #main-panel { 21 | width: 100%; 22 | background: var(--bg-panel); 23 | border-radius: 8px; 24 | box-shadow: var(--shadow-primary) 0 2px 4px, 25 | var(--shadow-secondary) 0 7px 20px -3px; 26 | padding: 30px; 27 | margin: 0 auto 30px auto; 28 | max-width: 1840px; 29 | display: flex; 30 | flex-direction: column; 31 | align-items: center; 32 | flex: 1; 33 | min-height: 0; 34 | box-sizing: border-box; 35 | } 36 | 37 | #chart-container { 38 | width: 100%; 39 | align-self: stretch; 40 | flex: 1; 41 | min-height: 0; 42 | position: relative; 43 | display: flex; 44 | flex-direction: column; 45 | } 46 | 47 | #chart-container canvas { 48 | flex: 1; 49 | min-height: 0; 50 | } 51 | 52 | .custom-button { 53 | align-items: center; 54 | appearance: none; 55 | background-color: var(--bg-button); 56 | border-radius: 4px; 57 | border-width: 0; 58 | box-shadow: var(--shadow-primary) 0 2px 4px, 59 | var(--shadow-secondary) 0 7px 13px -3px, var(--shadow-inset) 0 -3px 0 inset; 60 | box-sizing: border-box; 61 | color: var(--text-primary); 62 | cursor: pointer; 63 | display: inline-flex; 64 | font-family: monospace; 65 | height: 38px; 66 | justify-content: center; 67 | line-height: 1; 68 | list-style: none; 69 | overflow: hidden; 70 | padding-left: 16px; 71 | padding-right: 16px; 72 | position: relative; 73 | text-align: left; 74 | text-decoration: none; 75 | transition: box-shadow 0.15s, transform 0.15s; 76 | user-select: none; 77 | -webkit-user-select: none; 78 | touch-action: manipulation; 79 | white-space: nowrap; 80 | will-change: box-shadow, transform; 81 | font-size: 18px; 82 | } 83 | 84 | .custom-button:focus { 85 | box-shadow: var(--shadow-inset) 0 0 0 1.5px inset, var(--shadow-primary) 0 2px 4px, 86 | var(--shadow-secondary) 0 7px 13px -3px, var(--shadow-inset) 0 -3px 0 inset; 87 | } 88 | 89 | .custom-button:hover { 90 | box-shadow: var(--shadow-primary) 0 4px 8px, 91 | var(--shadow-secondary) 0 7px 13px -3px, var(--shadow-inset) 0 -3px 0 inset; 92 | transform: translateY(-2px); 93 | } 94 | 95 | .custom-button:active { 96 | box-shadow: var(--shadow-inset) 0 3px 7px inset; 97 | transform: translateY(2px); 98 | } 99 | -------------------------------------------------------------------------------- /ui/resources/css/PCvsEmulation.css: -------------------------------------------------------------------------------- 1 | body { 2 | display: flex; 3 | flex-direction: column; 4 | font-family: monospace; 5 | background-color: var(--bg-primary); 6 | margin: 0; 7 | padding: 20px; 8 | height: 100vh; 9 | overflow: hidden; 10 | box-sizing: border-box; 11 | } 12 | 13 | h1 { 14 | text-align: center; 15 | margin-bottom: 20px; 16 | color: var(--text-primary); 17 | flex-shrink: 0; 18 | } 19 | 20 | #main-panel { 21 | width: 100%; 22 | background: var(--bg-panel); 23 | border-radius: 8px; 24 | box-shadow: var(--shadow-primary) 0 2px 4px, 25 | var(--shadow-secondary) 0 7px 20px -3px; 26 | padding: 30px; 27 | margin: 0 auto 30px auto; 28 | max-width: 1840px; 29 | display: flex; 30 | flex-direction: column; 31 | align-items: center; 32 | flex: 1; 33 | min-height: 0; 34 | box-sizing: border-box; 35 | } 36 | 37 | #chart-container { 38 | width: 100%; 39 | align-self: stretch; 40 | flex: 1; 41 | min-height: 0; 42 | position: relative; 43 | display: flex; 44 | flex-direction: column; 45 | } 46 | 47 | #chart-container canvas { 48 | flex: 1; 49 | min-height: 0; 50 | } 51 | 52 | .custom-button { 53 | align-items: center; 54 | appearance: none; 55 | background-color: var(--bg-button); 56 | border-radius: 4px; 57 | border-width: 0; 58 | box-shadow: var(--shadow-primary) 0 2px 4px, 59 | var(--shadow-secondary) 0 7px 13px -3px, var(--shadow-inset) 0 -3px 0 inset; 60 | box-sizing: border-box; 61 | color: var(--text-primary); 62 | cursor: pointer; 63 | display: inline-flex; 64 | font-family: monospace; 65 | height: 38px; 66 | justify-content: center; 67 | line-height: 1; 68 | list-style: none; 69 | overflow: hidden; 70 | padding-left: 16px; 71 | padding-right: 16px; 72 | position: relative; 73 | text-align: left; 74 | text-decoration: none; 75 | transition: box-shadow 0.15s, transform 0.15s; 76 | user-select: none; 77 | -webkit-user-select: none; 78 | touch-action: manipulation; 79 | white-space: nowrap; 80 | will-change: box-shadow, transform; 81 | font-size: 18px; 82 | } 83 | 84 | .custom-button:focus { 85 | box-shadow: var(--shadow-inset) 0 0 0 1.5px inset, var(--shadow-primary) 0 2px 4px, 86 | var(--shadow-secondary) 0 7px 13px -3px, var(--shadow-inset) 0 -3px 0 inset; 87 | } 88 | 89 | .custom-button:hover { 90 | box-shadow: var(--shadow-primary) 0 4px 8px, 91 | var(--shadow-secondary) 0 7px 13px -3px, var(--shadow-inset) 0 -3px 0 inset; 92 | transform: translateY(-2px); 93 | } 94 | 95 | .custom-button:active { 96 | box-shadow: var(--shadow-inset) 0 3px 7px inset; 97 | transform: translateY(2px); 98 | } 99 | -------------------------------------------------------------------------------- /ui/resources/css/GamingTime.css: -------------------------------------------------------------------------------- 1 | body { 2 | display: flex; 3 | flex-direction: column; 4 | font-family: monospace; 5 | background-color: var(--bg-primary); 6 | margin: 0; 7 | padding: 20px; 8 | height: 100vh; 9 | overflow: hidden; 10 | box-sizing: border-box; 11 | } 12 | 13 | h1 { 14 | text-align: center; 15 | margin-bottom: 20px; 16 | color: var(--text-primary); 17 | flex-shrink: 0; 18 | } 19 | 20 | #main-panel { 21 | width: 100%; 22 | background: var(--bg-panel); 23 | border-radius: 8px; 24 | box-shadow: var(--shadow-primary) 0 2px 4px, 25 | var(--shadow-secondary) 0 7px 20px -3px; 26 | padding: 30px; 27 | margin: 0 auto 30px auto; 28 | max-width: 1840px; 29 | display: flex; 30 | flex-direction: row; 31 | gap: 20px; 32 | align-items: stretch; 33 | flex: 1; 34 | min-height: 0; 35 | box-sizing: border-box; 36 | } 37 | 38 | #chart-container { 39 | width: 100%; 40 | align-self: stretch; 41 | flex: 1; 42 | min-height: 0; 43 | position: relative; 44 | display: flex; 45 | flex-direction: column; 46 | } 47 | 48 | #chart-container canvas { 49 | flex: 1; 50 | min-height: 0; 51 | } 52 | 53 | #total-hours-display { 54 | color: var(--accent-blue); 55 | font-family: monospace; 56 | font-size: 18px; 57 | height: 22px; 58 | flex-shrink: 0; 59 | } 60 | 61 | .custom-button { 62 | align-items: center; 63 | appearance: none; 64 | background-color: var(--bg-button); 65 | border-radius: 4px; 66 | border-width: 0; 67 | box-shadow: var(--shadow-primary) 0 2px 4px, 68 | var(--shadow-secondary) 0 7px 13px -3px, var(--shadow-inset) 0 -3px 0 inset; 69 | box-sizing: border-box; 70 | color: var(--text-primary); 71 | cursor: pointer; 72 | display: inline-flex; 73 | font-family: monospace; 74 | height: 38px; 75 | justify-content: center; 76 | line-height: 1; 77 | list-style: none; 78 | overflow: hidden; 79 | padding-left: 16px; 80 | padding-right: 16px; 81 | position: relative; 82 | text-align: left; 83 | text-decoration: none; 84 | transition: box-shadow 0.15s, transform 0.15s; 85 | user-select: none; 86 | -webkit-user-select: none; 87 | touch-action: manipulation; 88 | white-space: nowrap; 89 | will-change: box-shadow, transform; 90 | font-size: 18px; 91 | } 92 | 93 | .custom-button:focus { 94 | box-shadow: var(--shadow-inset) 0 0 0 1.5px inset, var(--shadow-primary) 0 2px 4px, 95 | var(--shadow-secondary) 0 7px 13px -3px, var(--shadow-inset) 0 -3px 0 inset; 96 | } 97 | 98 | .custom-button:hover { 99 | box-shadow: var(--shadow-primary) 0 4px 8px, 100 | var(--shadow-secondary) 0 7px 13px -3px, var(--shadow-inset) 0 -3px 0 inset; 101 | transform: translateY(-2px); 102 | } 103 | 104 | .custom-button:active { 105 | box-shadow: var(--shadow-inset) 0 3px 7px inset; 106 | transform: translateY(2px); 107 | } -------------------------------------------------------------------------------- /ui/resources/css/IdleTime.css: -------------------------------------------------------------------------------- 1 | body { 2 | display: flex; 3 | flex-direction: column; 4 | font-family: monospace; 5 | background-color: var(--bg-primary); 6 | margin: 0; 7 | padding: 20px; 8 | height: 100vh; 9 | overflow: hidden; 10 | box-sizing: border-box; 11 | } 12 | 13 | h1 { 14 | text-align: center; 15 | margin-bottom: 20px; 16 | color: var(--text-primary); 17 | flex-shrink: 0; 18 | } 19 | 20 | #main-panel { 21 | width: 100%; 22 | background: var(--bg-panel); 23 | border-radius: 8px; 24 | box-shadow: var(--shadow-primary) 0 2px 4px, 25 | var(--shadow-secondary) 0 7px 20px -3px; 26 | padding: 30px; 27 | margin: 0 auto 30px auto; 28 | max-width: 1840px; 29 | display: flex; 30 | flex-direction: column; 31 | align-items: center; 32 | flex: 1; 33 | min-height: 0; 34 | box-sizing: border-box; 35 | } 36 | 37 | #chart-container { 38 | width: 100%; 39 | align-self: stretch; 40 | flex: 1; 41 | min-height: 0; 42 | position: relative; 43 | display: flex; 44 | flex-direction: column; 45 | } 46 | 47 | #chart-container canvas { 48 | flex: 1; 49 | min-height: 0; 50 | } 51 | 52 | #total-label { 53 | display: flex; 54 | margin: 15px auto 10px auto; 55 | justify-content: space-around; 56 | font-family: monospace; 57 | font-size: 18px; 58 | flex-shrink: 0; 59 | color: var(--text-primary); 60 | } 61 | 62 | .custom-button { 63 | align-items: center; 64 | appearance: none; 65 | background-color: var(--bg-button); 66 | border-radius: 4px; 67 | border-width: 0; 68 | box-shadow: var(--shadow-primary) 0 2px 4px, 69 | var(--shadow-secondary) 0 7px 13px -3px, var(--shadow-inset) 0 -3px 0 inset; 70 | box-sizing: border-box; 71 | color: var(--text-primary); 72 | cursor: pointer; 73 | display: inline-flex; 74 | font-family: monospace; 75 | height: 38px; 76 | justify-content: center; 77 | line-height: 1; 78 | list-style: none; 79 | overflow: hidden; 80 | padding-left: 16px; 81 | padding-right: 16px; 82 | position: relative; 83 | text-align: left; 84 | text-decoration: none; 85 | transition: box-shadow 0.15s, transform 0.15s; 86 | user-select: none; 87 | -webkit-user-select: none; 88 | touch-action: manipulation; 89 | white-space: nowrap; 90 | will-change: box-shadow, transform; 91 | font-size: 18px; 92 | } 93 | 94 | .custom-button:focus { 95 | box-shadow: var(--shadow-inset) 0 0 0 1.5px inset, var(--shadow-primary) 0 2px 4px, 96 | var(--shadow-secondary) 0 7px 13px -3px, var(--shadow-inset) 0 -3px 0 inset; 97 | } 98 | 99 | .custom-button:hover { 100 | box-shadow: var(--shadow-primary) 0 4px 8px, 101 | var(--shadow-secondary) 0 7px 13px -3px, var(--shadow-inset) 0 -3px 0 inset; 102 | transform: translateY(-2px); 103 | } 104 | 105 | .custom-button:active { 106 | box-shadow: var(--shadow-inset) 0 3px 7px inset; 107 | transform: translateY(2px); 108 | } 109 | -------------------------------------------------------------------------------- /Install.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal enabledelayedexpansion 3 | 4 | md "%ALLUSERSPROFILE%\GamingGaiden" 5 | 6 | set "InstallDirectory=%ALLUSERSPROFILE%\GamingGaiden" 7 | set "DesktopPath=%USERPROFILE%\Desktop" 8 | set "StartupPath=%APPDATA%\Microsoft\Windows\Start Menu\Programs\StartUp" 9 | set "StartMenuPath=%APPDATA%\Microsoft\Windows\Start Menu\Programs\Gaming Gaiden" 10 | set "IconPath=%InstallDirectory%\icons\running.ico" 11 | 12 | REM Quit GamingGaiden if Already running 13 | echo Closing Gaming Gaiden before installation 14 | taskkill /f /im GamingGaiden.exe 15 | 16 | REM Cleanup Install directory before installation 17 | echo Cleaning install directory 18 | powershell.exe -NoProfile -Command "Get-ChildItem '%InstallDirectory%' -Exclude backups,GamingGaiden.db | Remove-Item -recurse -force" 19 | 20 | REM Install to C:\ProgramData\GamingGaiden 21 | echo Copying Files 22 | xcopy /s/e/q/y "%CD%" "%InstallDirectory%" 23 | del "%InstallDirectory%\Install.bat" 24 | 25 | REM Create shortcuts using powershell and copy to desktop and start menu 26 | echo. 27 | echo Creating Shortcuts 28 | 29 | md "%StartMenuPath%" 30 | 31 | powershell.exe -NoProfile -Command "$WshShell = New-Object -ComObject WScript.Shell; $Shortcut = $WshShell.CreateShortcut('%InstallDirectory%\Gaming Gaiden.lnk'); $Shortcut.TargetPath = '%InstallDirectory%\GamingGaiden.exe'; $Shortcut.WorkingDirectory = '%InstallDirectory%'; $Shortcut.WindowStyle = 7; $Shortcut.Save()" 32 | copy "%InstallDirectory%\Gaming Gaiden.lnk" "%DesktopPath%" 33 | copy "%InstallDirectory%\Gaming Gaiden.lnk" "%StartMenuPath%" 34 | 35 | powershell.exe -NoProfile -Command "$WshShell = New-Object -ComObject WScript.Shell; $Shortcut = $WshShell.CreateShortcut('%InstallDirectory%\Uninstall Gaming Gaiden.lnk'); $Shortcut.TargetPath = '%InstallDirectory%\Uninstall.bat'; $Shortcut.WorkingDirectory = '%InstallDirectory%'; $Shortcut.Save()" 36 | copy "%InstallDirectory%\Uninstall Gaming Gaiden.lnk" "%StartMenuPath%" 37 | del "%InstallDirectory%\Uninstall Gaming Gaiden.lnk" 38 | 39 | REM Unblock all gaming gaiden files as they are downloaded from internet and blocked by default 40 | echo. 41 | echo Unblocking all Gaming Gaiden files 42 | powershell.exe -NoProfile -Command "Get-ChildItem '%InstallDirectory%' -Recurse | Unblock-File" 43 | 44 | REM Copy shortcut to startup directory if user chooses to 45 | echo. 46 | set /p AutoStartChoice="Would you like Gaming Gaiden to auto start at boot? Yes/No: " 47 | if /i "%AutoStartChoice%"=="Yes" ( 48 | copy "%InstallDirectory%\Gaming Gaiden.lnk" "%startupPath%" 49 | echo Auto start successfully setup. 50 | ) else if /i "%AutoStartChoice%"=="Y" ( 51 | copy "%InstallDirectory%\Gaming Gaiden.lnk" "%startupPath%" 52 | echo Auto start successfully setup. 53 | ) else ( 54 | echo Auto start setup cancelled by user. 55 | ) 56 | 57 | echo. 58 | echo Installation successful at %InstallDirectory%. 59 | echo. 60 | echo Run / Remove application using shortcuts on desktop / start menu. 61 | echo. 62 | echo You can now delete the downloaded files if you wish. 63 | echo. 64 | pause -------------------------------------------------------------------------------- /ui/resources/css/MostPlayed.css: -------------------------------------------------------------------------------- 1 | body { 2 | display: flex; 3 | flex-direction: column; 4 | font-family: monospace; 5 | background-color: var(--bg-primary); 6 | margin: 0; 7 | padding: 20px; 8 | height: 100vh; 9 | overflow: hidden; 10 | box-sizing: border-box; 11 | } 12 | 13 | h1 { 14 | text-align: center; 15 | margin-bottom: 20px; 16 | color: var(--text-primary); 17 | flex-shrink: 0; 18 | } 19 | 20 | #main-panel { 21 | width: 100%; 22 | background: var(--bg-panel); 23 | border-radius: 8px; 24 | box-shadow: var(--shadow-primary) 0 2px 4px, 25 | var(--shadow-secondary) 0 7px 20px -3px; 26 | padding: 30px; 27 | margin: 0 auto 30px auto; 28 | max-width: 1840px; 29 | display: flex; 30 | flex-direction: column; 31 | align-items: center; 32 | flex: 1; 33 | min-height: 0; 34 | box-sizing: border-box; 35 | } 36 | 37 | #chart-container { 38 | width: 100%; 39 | align-self: stretch; 40 | flex: 1; 41 | min-height: 0; 42 | position: relative; 43 | display: flex; 44 | flex-direction: column; 45 | } 46 | 47 | #chart-container canvas { 48 | flex: 1; 49 | min-height: 0; 50 | } 51 | 52 | #chart-navigation-bar { 53 | display: flex; 54 | margin: 15px auto 10px auto; 55 | width: 200px; 56 | justify-content: space-around; 57 | font-family: monospace; 58 | font-size: 18px; 59 | flex-shrink: 0; 60 | color: var(--text-primary); 61 | } 62 | 63 | .custom-button { 64 | align-items: center; 65 | appearance: none; 66 | background-color: var(--bg-button); 67 | border-radius: 4px; 68 | border-width: 0; 69 | box-shadow: var(--shadow-primary) 0 2px 4px, 70 | var(--shadow-secondary) 0 7px 13px -3px, var(--shadow-inset) 0 -3px 0 inset; 71 | box-sizing: border-box; 72 | color: var(--text-primary); 73 | cursor: pointer; 74 | display: inline-flex; 75 | font-family: monospace; 76 | height: 38px; 77 | justify-content: center; 78 | line-height: 1; 79 | list-style: none; 80 | overflow: hidden; 81 | padding-left: 16px; 82 | padding-right: 16px; 83 | position: relative; 84 | text-align: left; 85 | text-decoration: none; 86 | transition: box-shadow 0.15s, transform 0.15s; 87 | user-select: none; 88 | -webkit-user-select: none; 89 | touch-action: manipulation; 90 | white-space: nowrap; 91 | will-change: box-shadow, transform; 92 | font-size: 18px; 93 | } 94 | 95 | .custom-button:focus { 96 | box-shadow: var(--shadow-inset) 0 0 0 1.5px inset, var(--shadow-primary) 0 2px 4px, 97 | var(--shadow-secondary) 0 7px 13px -3px, var(--shadow-inset) 0 -3px 0 inset; 98 | } 99 | 100 | .custom-button:hover { 101 | box-shadow: var(--shadow-primary) 0 4px 8px, 102 | var(--shadow-secondary) 0 7px 13px -3px, var(--shadow-inset) 0 -3px 0 inset; 103 | transform: translateY(-2px); 104 | } 105 | 106 | .custom-button:active { 107 | box-shadow: var(--shadow-inset) 0 3px 7px inset; 108 | transform: translateY(2px); 109 | } 110 | -------------------------------------------------------------------------------- /ui/resources/css/SessionHistory/SessionHistory-calendar.css: -------------------------------------------------------------------------------- 1 | /* ===== SESSION HISTORY - CALENDAR VIEW SPECIFIC STYLES ===== */ 2 | 3 | /* Calendar View Content */ 4 | #calendar-view-content { 5 | /* background already inherited from .left-content */ 6 | } 7 | 8 | /* Override calendar container padding for SessionHistory layout */ 9 | #calendar-container { 10 | padding: 15px; 11 | } 12 | 13 | /* ===== CALENDAR VIEW - DAY GRID ===== */ 14 | 15 | /* Day Grid Container */ 16 | #day-grid-container { 17 | padding-top: 10px; 18 | border-top: 2px solid var(--border-light); 19 | } 20 | 21 | #selected-month-display { 22 | font-family: monospace; 23 | font-size: 16px; 24 | font-weight: bold; 25 | text-align: center; 26 | margin-bottom: 10px; 27 | color: var(--text-primary); 28 | } 29 | 30 | #day-headers { 31 | display: grid; 32 | grid-template-columns: repeat(7, 1fr); 33 | gap: 5px; 34 | margin-bottom: 5px; 35 | padding-left: 5px; 36 | } 37 | 38 | .day-header { 39 | font-family: monospace; 40 | font-size: 11px; 41 | font-weight: bold; 42 | text-align: center; 43 | padding: 10px; 44 | color: var(--text-primary); 45 | border: 1px solid transparent; 46 | box-sizing: border-box; 47 | } 48 | 49 | .day-header.weekend { 50 | color: var(--status-error); 51 | } 52 | 53 | #day-grid { 54 | display: grid; 55 | grid-template-columns: repeat(7, 1fr); 56 | gap: 5px; 57 | } 58 | 59 | .day-btn { 60 | padding: 10px; 61 | font-size: 12px; 62 | font-family: monospace; 63 | background-color: var(--bg-primary); 64 | color: var(--text-secondary); 65 | border: 1px solid var(--border-light); 66 | border-radius: 4px; 67 | cursor: pointer; 68 | transition: all 0.2s; 69 | aspect-ratio: 1; 70 | display: flex; 71 | align-items: center; 72 | justify-content: center; 73 | } 74 | 75 | .day-btn:hover { 76 | background-color: var(--bg-button-hover); 77 | transform: translateY(-1px); 78 | } 79 | 80 | .day-btn.selected { 81 | background-color: var(--accent-blue); 82 | color: var(--text-inverse); 83 | font-weight: bold; 84 | border-color: var(--accent-blue-dark); 85 | } 86 | 87 | .day-btn.has-data { 88 | background-color: var(--accent-blue-light); 89 | border-color: var(--accent-blue-light); 90 | } 91 | 92 | .day-btn.has-data.selected { 93 | background-color: var(--accent-blue); 94 | border-color: var(--accent-blue-dark); 95 | } 96 | 97 | .day-btn.weekend.has-data { 98 | background-color: var(--status-error-pale); 99 | border-color: var(--status-error-light); 100 | } 101 | 102 | .day-btn.weekend.selected { 103 | background-color: var(--status-error); 104 | border-color: var(--status-error-dark); 105 | } 106 | 107 | .day-btn.weekend.has-data.selected { 108 | background-color: var(--status-error); 109 | border-color: var(--status-error-dark); 110 | } 111 | 112 | .day-btn.empty { 113 | opacity: 0.2; 114 | cursor: not-allowed; 115 | background-color: var(--bg-tertiary); 116 | } 117 | 118 | .day-btn:disabled { 119 | opacity: 0.3; 120 | cursor: not-allowed; 121 | } 122 | -------------------------------------------------------------------------------- /ui/resources/js/GameVsTime.js: -------------------------------------------------------------------------------- 1 | /*global ChartDataLabels, Chart, chartTitleConfig, gamingData, Log2Axis, getChartTextColor, getChartGridColor, getChartBackgroundColor*/ 2 | /*from chart.js, common.js and html templates*/ 3 | 4 | let chart; 5 | 6 | Log2Axis.id = "log2"; 7 | Log2Axis.defaults = {}; 8 | 9 | Chart.register(Log2Axis); 10 | 11 | function updateChart(gameCount, labelText, stepSize = 1) { 12 | let labels = []; 13 | let data = []; 14 | 15 | if (gameCount > gamingData.length) { 16 | gameCount = gamingData.length; 17 | } 18 | 19 | let i = 0; 20 | for (const game of gamingData) { 21 | if (i == gameCount) break; 22 | labels.push(game.name); 23 | data.push({ game: game.name, time: (game.time / 60).toFixed(1) }); 24 | i++; 25 | } 26 | 27 | if (chart) { 28 | chart.destroy(); 29 | } 30 | 31 | const ctx = document.getElementById("game-vs-time-chart").getContext("2d"); 32 | 33 | chart = new Chart(ctx, { 34 | type: "bar", 35 | plugins: [ChartDataLabels], 36 | data: { 37 | labels: labels, 38 | datasets: [ 39 | { 40 | label: labelText, 41 | data: data.map((row) => row.time), 42 | borderWidth: 2, 43 | }, 44 | ], 45 | }, 46 | options: { 47 | responsive: true, 48 | maintainAspectRatio: false, 49 | indexAxis: "y", 50 | scales: { 51 | y: { 52 | ticks: { 53 | autoSkip: false, 54 | color: getChartTextColor() 55 | }, 56 | grid: { 57 | color: getChartGridColor() 58 | } 59 | }, 60 | // Alignment Hack: Add an identical y scale on right side, to center the graph on page. 61 | // Then hide the right side scale by setting label color identical to background. 62 | yRight: { 63 | position: "right", 64 | grid: { 65 | display: false, 66 | }, 67 | ticks: { 68 | color: getChartBackgroundColor(), 69 | }, 70 | }, 71 | x: { 72 | type: "log2", 73 | ticks: { 74 | stepSize: stepSize, 75 | color: getChartTextColor() 76 | }, 77 | title: chartTitleConfig(labelText, 15), 78 | grid: { 79 | color: getChartGridColor() 80 | } 81 | }, 82 | }, 83 | elements: { 84 | bar: { 85 | borderWidth: 1, 86 | }, 87 | }, 88 | plugins: { 89 | tooltip: { 90 | enabled: false, 91 | }, 92 | legend: { 93 | display: false, 94 | }, 95 | datalabels: { 96 | anchor: "end", 97 | align: "right", 98 | color: getChartTextColor(), 99 | font: { 100 | family: "monospace", 101 | }, 102 | }, 103 | } 104 | }, 105 | }); 106 | } 107 | 108 | // Dummy usage of variables to suppress not used false positive in codacy 109 | // without ignoring the entire file. 110 | updateChart; 111 | -------------------------------------------------------------------------------- /ui/resources/css/SessionHistory/SessionHistory-cards.css: -------------------------------------------------------------------------------- 1 | /* ===== BY DAY/MONTH VIEWS - HEADER & STATS ===== */ 2 | 3 | #cards-header { 4 | margin-bottom: 20px; 5 | border-bottom: 2px solid var(--border-light); 6 | padding-bottom: 15px; 7 | } 8 | 9 | #selected-date-title { 10 | margin: 0 0 10px 0; 11 | color: var(--text-primary); 12 | font-size: 20px; 13 | } 14 | 15 | #date-stats { 16 | display: flex; 17 | gap: 20px; 18 | font-size: 14px; 19 | color: var(--text-secondary); 20 | } 21 | 22 | #date-stats span { 23 | font-family: monospace; 24 | } 25 | 26 | /* ===== BY DAY/MONTH VIEWS - GAME CARDS GRID ===== */ 27 | 28 | #game-cards-grid { 29 | display: grid; 30 | grid-template-columns: repeat(auto-fit, 230px); 31 | grid-auto-rows: 190px; 32 | gap: 20px; 33 | padding: 10px; 34 | overflow-y: auto; 35 | flex: 1; 36 | align-content: start; 37 | justify-content: center; 38 | } 39 | 40 | #game-cards-grid.centered { 41 | align-content: center !important; 42 | } 43 | 44 | .game-card { 45 | border: 1px solid var(--border-light); 46 | border-radius: 8px; 47 | padding: 15px; 48 | background: var(--bg-panel); 49 | box-shadow: var(--shadow-secondary) 0 2px 4px; 50 | transition: transform 0.2s, box-shadow 0.2s; 51 | cursor: pointer; 52 | display: flex; 53 | flex-direction: column; 54 | align-items: center; 55 | gap: 10px; 56 | overflow: visible; 57 | box-sizing: border-box; 58 | position: relative; 59 | } 60 | 61 | .game-card:hover { 62 | transform: translateY(-2px); 63 | box-shadow: var(--shadow-secondary) 0 4px 8px; 64 | border-color: var(--accent-blue); 65 | } 66 | 67 | .game-card-icon { 68 | height: 64px; 69 | width: auto; 70 | max-width: 100%; 71 | filter: drop-shadow(4px 4px 2px var(--shadow-image)); 72 | flex-shrink: 0; 73 | transition: transform 0.2s ease; 74 | cursor: pointer; 75 | position: relative; 76 | } 77 | 78 | .game-card-icon:hover { 79 | transform: scale(2.19); 80 | z-index: 1000; 81 | filter: drop-shadow(8px 8px 4px var(--shadow-image)); 82 | outline: 1px solid white; 83 | } 84 | 85 | .game-card-name { 86 | font-weight: bold; 87 | font-size: 14px; 88 | color: var(--text-primary); 89 | text-align: center; 90 | word-wrap: break-word; 91 | overflow: hidden; 92 | text-overflow: ellipsis; 93 | display: -webkit-box; 94 | -webkit-line-clamp: 2; 95 | -webkit-box-orient: vertical; 96 | max-width: 100%; 97 | line-height: 1.3; 98 | } 99 | 100 | .game-card-platform { 101 | font-size: 12px; 102 | color: var(--text-secondary); 103 | font-style: italic; 104 | white-space: nowrap; 105 | overflow: hidden; 106 | text-overflow: ellipsis; 107 | max-width: 100%; 108 | } 109 | 110 | .game-card-stats { 111 | font-size: 12px; 112 | color: var(--text-secondary); 113 | text-align: center; 114 | white-space: nowrap; 115 | overflow: hidden; 116 | text-overflow: ellipsis; 117 | max-width: 100%; 118 | } 119 | 120 | /* ===== NO GAMES MESSAGE ===== */ 121 | 122 | #no-games-message { 123 | display: flex; 124 | align-items: center; 125 | justify-content: center; 126 | height: 100%; 127 | color: var(--text-secondary); 128 | font-size: 16px; 129 | } 130 | -------------------------------------------------------------------------------- /ui/templates/PCvsEmulation.html.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Gaming Gaiden 5 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 28 | 29 | 30 | 31 |

PC Vs Emulation PlayTime

32 |
33 |
34 | 35 |
36 |
37 |
_PCVSEMULATIONTABLE_
38 | 39 | 40 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /ui/templates/GamesPerPlatform.html.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Gaming Gaiden 5 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 28 | 29 | 30 | 31 |

Games You Played On Each Platform

32 |
33 |
34 | 35 |
36 |
37 |
_GAMESPERPLATFORMTABLE_
38 | 39 | 40 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /ui/templates/IdleTime.html.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Gaming Gaiden 5 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 28 | 29 | 30 | 31 |

Time you left games Idle

32 |
33 |
34 | 35 |
36 |
Total Time Wasted: _TOTALIDLETIME_
37 |
38 |
_GAMESIDLETIMETABLE_
39 | 40 | 41 | 46 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /modules/PSSQLite/1.1.0/PSSQLite.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | 3 | # Script module or binary module file associated with this manifest. 4 | ModuleToProcess = 'PSSQLite.psm1' 5 | 6 | # Version number of this module. 7 | ModuleVersion = '1.1.0' 8 | 9 | # ID used to uniquely identify this module 10 | GUID = '381f3394-9b8a-492e-94b4-b3aa9e775761' 11 | 12 | # Author of this module 13 | Author = 'Warren Frame' 14 | 15 | # Company or vendor of this module 16 | CompanyName = '' 17 | 18 | # Copyright statement for this module 19 | # Copyright = '(c) 2014 ramblingcookiemonster. All rights reserved.' 20 | 21 | # Description of the functionality provided by this module 22 | Description = 'Query SQLite databases' 23 | 24 | # Minimum version of the Windows PowerShell engine required by this module 25 | PowerShellVersion = '2.0' 26 | 27 | # Name of the Windows PowerShell host required by this module 28 | # PowerShellHostName = '' 29 | 30 | # Minimum version of the Windows PowerShell host required by this module 31 | # PowerShellHostVersion = '' 32 | 33 | # Minimum version of Microsoft .NET Framework required by this module 34 | # DotNetFrameworkVersion = '' 35 | 36 | # Minimum version of the common language runtime (CLR) required by this module 37 | # CLRVersion = '' 38 | 39 | # Processor architecture (None, X86, Amd64) required by this module 40 | # ProcessorArchitecture = '' 41 | 42 | # Modules that must be imported into the global environment prior to importing this module 43 | # RequiredModules = @() 44 | 45 | # Assemblies that must be loaded prior to importing this module 46 | # RequiredAssemblies = @() 47 | 48 | # Script files (.ps1) that are run in the caller's environment prior to importing this module. 49 | # ScriptsToProcess = @() 50 | 51 | # Type files (.ps1xml) to be loaded when importing this module 52 | # TypesToProcess = @() 53 | 54 | # Format files (.ps1xml) to be loaded when importing this module 55 | # FormatsToProcess = @() 56 | 57 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess 58 | # NestedModules = @() 59 | 60 | # Functions to export from this module 61 | FunctionsToExport = @('Invoke-SqliteBulkCopy', 'Invoke-SqliteQuery', 'New-SqliteConnection', 'Out-DataTable', 'Update-Sqlite') 62 | 63 | # Cmdlets to export from this module 64 | # CmdletsToExport = '*' 65 | 66 | # Variables to export from this module 67 | # VariablesToExport = '*' 68 | 69 | # Aliases to export from this module 70 | # AliasesToExport = '*' 71 | 72 | # List of all modules packaged with this module 73 | # ModuleList = @() 74 | 75 | # List of all files packaged with this module 76 | # FileList = @() 77 | 78 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. 79 | PrivateData = @{ 80 | PSData = @{ 81 | # Tags applied to this module. These help with module discovery in online galleries. 82 | Tags = @('sql', 'sqlite', 'Query', 'Database') 83 | 84 | # A URL to the license for this module. 85 | LicenseUri = 'https://github.com/RamblingCookieMonster/PSSQLite/blob/master/LICENSE' 86 | 87 | # A URL to the main website for this project. 88 | ProjectUri = 'https://github.com/RamblingCookieMonster/PSSQLite' 89 | } # End of PSData hashtable 90 | 91 | } # End of PrivateData hashtable 92 | 93 | # HelpInfo URI of this module 94 | # HelpInfoURI = '' 95 | 96 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 97 | # DefaultCommandPrefix = '' 98 | 99 | } 100 | 101 | -------------------------------------------------------------------------------- /ui/resources/js/SessionHistory/SessionHistory-cards.js: -------------------------------------------------------------------------------- 1 | /* global MONTH_NAMES, filterSessionsByDateStr, aggregateGamesBySessions, updateStatsDisplay, switchMainView, selectGame */ 2 | /* from sessionHistory-shared.js, sessionHistory-cards.js, sessionHistory-games.js */ 3 | 4 | // ===== DATA AGGREGATION FOR BY DAY/MONTH VIEWS ===== 5 | 6 | // Get games played on a specific date 7 | function getGamesForDate(dateStr) { 8 | const sessionsOnDate = filterSessionsByDateStr(dateStr, false); 9 | return aggregateGamesBySessions(sessionsOnDate); 10 | } 11 | 12 | // Get games played in a specific month 13 | function getGamesForMonth(monthStr) { 14 | const sessionsInMonth = filterSessionsByDateStr(monthStr, true); 15 | return aggregateGamesBySessions(sessionsInMonth); 16 | } 17 | 18 | // ===== GAME CARDS LOADING & RENDERING ===== 19 | 20 | // Load and render game cards for a specific date 21 | // eslint-disable-next-line no-unused-vars 22 | function loadGameCardsForDate(dateStr) { 23 | const games = getGamesForDate(dateStr); 24 | 25 | // Update header 26 | const date = new Date(dateStr); 27 | const dateDisplay = date.toLocaleDateString('en-US', { 28 | weekday: 'long', 29 | year: 'numeric', 30 | month: 'long', 31 | day: 'numeric' 32 | }); 33 | 34 | document.getElementById('selected-date-title').textContent = 35 | `Games Played on ${dateDisplay}`; 36 | 37 | // Update stats and render 38 | updateStatsDisplay(games); 39 | renderGameCards(games); 40 | } 41 | 42 | // Load and render game cards for a specific month 43 | // eslint-disable-next-line no-unused-vars 44 | function loadGameCardsForMonth(monthStr) { 45 | const games = getGamesForMonth(monthStr); 46 | 47 | // Update header 48 | const [year, month] = monthStr.split('-'); 49 | const monthDisplay = `${MONTH_NAMES[parseInt(month) - 1]} ${year}`; 50 | 51 | document.getElementById('selected-date-title').textContent = 52 | `Games Played in ${monthDisplay}`; 53 | 54 | // Update stats and render 55 | updateStatsDisplay(games); 56 | renderGameCards(games); 57 | } 58 | 59 | // Update grid alignment based on overflow 60 | function updateGridAlignment() { 61 | const gridElement = document.getElementById('game-cards-grid'); 62 | 63 | // Use setTimeout to ensure DOM has updated and measurements are accurate 64 | setTimeout(() => { 65 | const hasOverflow = gridElement.scrollHeight > gridElement.clientHeight; 66 | 67 | if (hasOverflow) { 68 | gridElement.classList.remove('centered'); 69 | } else { 70 | gridElement.classList.add('centered'); 71 | } 72 | }, 0); 73 | } 74 | 75 | // Render game cards grid 76 | function renderGameCards(games) { 77 | const gridElement = document.getElementById('game-cards-grid'); 78 | const noGamesMsg = document.getElementById('no-games-message'); 79 | 80 | if (games.length === 0) { 81 | gridElement.innerHTML = ''; 82 | noGamesMsg.style.display = 'flex'; 83 | gridElement.classList.remove('centered'); 84 | return; 85 | } 86 | 87 | noGamesMsg.style.display = 'none'; 88 | gridElement.innerHTML = ''; 89 | 90 | games.forEach(game => { 91 | const card = document.createElement('div'); 92 | card.className = 'game-card'; 93 | card.dataset.gameName = game.game_name; 94 | 95 | const hours = (game.totalDuration / 60).toFixed(1); 96 | const sessionsText = game.sessionCount === 1 ? 'session' : 'sessions'; 97 | 98 | card.innerHTML = ` 99 | ${game.game_name} 100 |
${game.game_name}
101 |
${game.platform}
102 |
${game.sessionCount} ${sessionsText} • ${hours}h
103 | `; 104 | 105 | // Click handler: switch to games view and select this game 106 | card.addEventListener('click', () => { 107 | switchMainView('games'); 108 | selectGame(game.game_name); 109 | }); 110 | 111 | gridElement.appendChild(card); 112 | }); 113 | 114 | // Update alignment after cards are rendered 115 | updateGridAlignment(); 116 | } 117 | -------------------------------------------------------------------------------- /Manual.md: -------------------------------------------------------------------------------- 1 | ### Track PC games 2 | 1. Notify icon menu *Settings => Add Game*. 3 | 2. Add the executable of game using *Add Exe*. 4 | 3. Icon should auto update. You can set a new icon by using *Search* (searches google for game icon) and *Update* (browse for image) buttons. 5 | 4. Change the auto populated *Name* to a better one and click *Ok*. 6 | 7 | ### Track emulated games 8 | 9 | **Requires launching games via command line params** like 10 | 11 | - *pcsx2-qtx64-avx2.exe %ROM%* 12 | - *retroarch.exe -L cores\\flycast_libretro.dll %ROM%*. 13 | 14 | Most frontends already use the above way. 15 | 16 | 1. *Settings => Add Emulator* 17 | 2. Enter platform (NES, Genesis, PS2, etc.) 18 | 3. *Add Exe* - emulator executable (can add multiple per platform) 19 | 4. For Retroarch: *Add Core* when prompted 20 | 5. *Rom Extns* - comma-separated list without dots/spaces (zip,chd,rvg) 21 | 22 | **Games auto-register using ROM filename. Renaming ROM creates new entry.** 23 | 24 | ### Update tracked game status, edit play time, change icon etc. 25 | 26 | App menu: *Settings => Edit Game*, select game from list (searchable). 27 | 28 | - Change executable (after reinstall) 29 | - Update icon (*Search* for online, *Update* to browse - png/jpg supported) 30 | - Manually adjust play time 31 | - Change platform 32 | - Mark as finished / other status (checkbox) 33 | 34 | ### Update emulator for platform 35 | 36 | *Settings => Edit Emulator*, select platform from list (searchable). 37 | 38 | - Update executable path or add new exe 39 | - Change Retroarch core 40 | - Modify ROM extensions 41 | 42 | ### Pause/Resume tracking 43 | 44 | App menu: *Stop Tracker* to pause, *Start Tracker* to resume. 45 | 46 | ### Disable/Enable auto start 47 | 48 | **Disable:** Press *Win+R*, enter *shell:startup*, delete *Gaming Gaiden* shortcut. 49 | 50 | **Re-enable:** Run *install.bat* from install directory, choose *yes* for auto start. 51 | 52 | ### Restore data 53 | 54 | 1. App menu: *Settings => Open Install Directory*. Go to *backups* folder. 55 | 2. Exit app. 56 | 3. Copy database file from backup folder zip to install directory. 57 | 4. Restart app. 58 | 59 | ### Games launched from emulator application directly are not tracked 60 | 61 | Games launched from emulator GUI (Retroarch, PCSX2, Dolphin, Duckstation) lack command line parameters in Windows process. Use a frontend (EmulationStation, Launchbox) or desktop shortcuts with command lines. 62 | 63 | ### Track multiple platforms using a single emulator 64 | 65 | 1. Copy emulator exe with platform-specific names. Example: Copy *Dolphin.exe* to *Dolphin-Wii.exe* and *Dolphin-Gamecube.exe*. 66 | 2. Register each platform with its renamed exe. 67 | 3. Update frontend/shortcuts to use new exes. 68 | 69 | Alternative: Name platform *"Gamecube and Wii"* using single exe. 70 | 71 | ### Track multiple platforms using a single Retroarch core 72 | 73 | 1. Copy *retroarch.exe* with platform-specific names (e.g. *Retroarch-Genesis.exe*, *Retroarch-GameGear.exe*). 74 | 2. Register each platform with its renamed exe. 75 | 3. Update frontend/shortcuts. 76 | 77 | Alternative: Name platform *"Genesis & GameGear"* using single core. 78 | 79 | ### Track games on multiple PCs with shared database 80 | 81 | **Requires:** Cloud sync directory (OneDrive, etc.) accessible on all PCs. 82 | 83 | 1. Install app on all PCs. Pick PC with most data as database source. 84 | 2. On source PC: *Settings => Open Install Directory*, copy *backups* and *GamingGaiden.db* to synced folder. 85 | 3. Exit app on all PCs. 86 | 4. Delete *backups* and *GamingGaiden.db* from each PC's install directory. 87 | 5. Use [Link Shell Extension](https://schinagl.priv.at/nt/hardlinkshellext/linkshellextension.html) to create symlinks from install directories to files in synced folder. 88 | 6. Start app on all PCs. 89 | 90 | ### Track gaming pc usage on a shared database 91 | 92 | After setting up database share, on each installation of gaming gaiden set the correct pc as the current pc in *Settings => Gaming PCs* section. 93 | 94 | Games will be correctly tagged to the gaming pc on which they are played and pc usage will be updated using session times. -------------------------------------------------------------------------------- /ui/templates/MostPlayed.html.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Gaming Gaiden 5 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 28 | 29 | 30 | 31 |

Your Most Played Games

32 |
33 |
34 | 35 |
36 |
37 |
Top
38 | 39 |
Games
40 |
41 |
42 |
_GAMESPLAYTIMETABLE_
43 | 44 | 45 | 77 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /ui/resources/js/calendar-controls.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | /** 4 | * Calendar Controls Module 5 | * 6 | * Provides reusable calendar navigation utilities for Gaming Time and Session History pages. 7 | * Handles year display, year navigation, and month grid updates with configurable callbacks. 8 | */ 9 | 10 | /** 11 | * Formats a month string as "YYYY-MM" 12 | * @param {number} year - Full year (e.g., 2025) 13 | * @param {number} month - Month index (0-11) 14 | * @returns {string} Formatted month string 15 | */ 16 | function formatMonthString(year, month) { 17 | return `${year}-${String(month + 1).padStart(2, '0')}`; 18 | } 19 | 20 | /** 21 | * Formats a date string as "YYYY-MM-DD" 22 | * @param {number} year - Full year (e.g., 2025) 23 | * @param {number} month - Month index (0-11) 24 | * @param {number} day - Day of month (1-31) 25 | * @returns {string} Formatted date string 26 | */ 27 | function formatDateString(year, month, day) { 28 | return `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; 29 | } 30 | 31 | /** 32 | * Updates the calendar year display element 33 | * @param {number} calendarYear - The year to display 34 | * @param {Object} [config] - Optional configuration 35 | * @param {Function} [config.yearDisplayCallback] - Callback for custom year display styling 36 | */ 37 | function updateYearDisplay(calendarYear, config = {}) { 38 | const yearDisplayElement = document.getElementById('year-display'); 39 | yearDisplayElement.textContent = calendarYear; 40 | 41 | // Allow custom callback for additional styling (e.g., yearly mode indicator) 42 | if (config.yearDisplayCallback) { 43 | config.yearDisplayCallback(yearDisplayElement); 44 | } 45 | } 46 | 47 | /** 48 | * Sets up year navigation buttons with boundary checking 49 | * @param {Object} config - Configuration object 50 | * @param {number} config.firstYear - Minimum year boundary 51 | * @param {number} config.finalYear - Maximum year boundary 52 | * @param {Function} config.getCalendarYear - Getter for current calendar year 53 | * @param {Function} config.setCalendarYear - Setter for calendar year 54 | * @param {Function} config.onYearChange - Callback when year changes 55 | */ 56 | function setupYearNavigation(config) { 57 | const { 58 | firstYear, 59 | finalYear, 60 | getCalendarYear, 61 | setCalendarYear, 62 | onYearChange 63 | } = config; 64 | 65 | document.getElementById('prev-year-button').addEventListener('click', () => { 66 | const currentYear = getCalendarYear(); 67 | if (currentYear > firstYear) { 68 | setCalendarYear(currentYear - 1); 69 | onYearChange(); 70 | } 71 | }); 72 | 73 | document.getElementById('next-year-button').addEventListener('click', () => { 74 | const currentYear = getCalendarYear(); 75 | if (currentYear < finalYear) { 76 | setCalendarYear(currentYear + 1); 77 | onYearChange(); 78 | } 79 | }); 80 | } 81 | 82 | /** 83 | * Updates the month grid buttons with data availability and selection state 84 | * @param {Object} config - Configuration object 85 | * @param {number} config.calendarYear - Current calendar year 86 | * @param {Set} config.availableMonths - Set of available month strings (YYYY-MM) 87 | * @param {Function} config.isMonthSelected - Function to check if month is selected 88 | * @param {Function} config.onMonthClick - Callback when month is clicked 89 | * @param {boolean} [config.disableInteraction] - Disable month selection (e.g., in yearly view) 90 | */ 91 | function updateMonthGrid(config) { 92 | const { 93 | calendarYear, 94 | availableMonths, 95 | isMonthSelected, 96 | onMonthClick, 97 | disableInteraction = false 98 | } = config; 99 | 100 | const monthButtons = document.querySelectorAll('.month-btn'); 101 | 102 | monthButtons.forEach((btn, index) => { 103 | const monthStr = formatMonthString(calendarYear, index); 104 | const hasData = availableMonths.has(monthStr); 105 | 106 | // Update visual state 107 | btn.classList.toggle('has-data', hasData); 108 | btn.classList.toggle('selected', isMonthSelected(index)); 109 | btn.disabled = !hasData; 110 | 111 | // Apply interaction override if needed 112 | if (disableInteraction) { 113 | btn.style.pointerEvents = 'none'; 114 | btn.style.opacity = '0.5'; 115 | } else { 116 | btn.style.pointerEvents = 'auto'; 117 | btn.style.opacity = '1'; 118 | } 119 | 120 | // Remove existing click handler 121 | btn.onclick = null; 122 | 123 | // Add new click handler if data exists 124 | if (hasData) { 125 | btn.onclick = () => onMonthClick(index); 126 | } 127 | }); 128 | } 129 | -------------------------------------------------------------------------------- /ui/templates/GamingTime.html.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Gaming Gaiden 5 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 30 | 31 | 32 | 33 |

Time You Spent Playing Games

34 |
35 |
36 |
37 |
38 | 39 |
40 | 41 |
42 |
43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |
56 |
57 |
58 |
59 |
60 | 61 |
62 |
63 |
64 |
65 |
_DAILYPLAYTIMETABLE_
66 | 67 | 68 | 69 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /ui/resources/js/common.js: -------------------------------------------------------------------------------- 1 | /*global Chart*/ 2 | /*from chart.js*/ 3 | 4 | var chartTooltipConfig = { 5 | displayColors: false, 6 | yAlign: "top", 7 | caretPadding: 7, 8 | callbacks: { 9 | label: function () { 10 | return ""; 11 | }, 12 | }, 13 | }; 14 | 15 | var chartLegendConfig = { 16 | onClick: null, 17 | position: "bottom", 18 | labels: { 19 | boxWidth: 20, 20 | padding: 40, 21 | color: getChartTextColor(), 22 | font: { 23 | size: 18, 24 | weight: "bold", 25 | family: "monospace", 26 | }, 27 | }, 28 | }; 29 | 30 | var chartDataLabelFontConfig = { 31 | size: 18, 32 | weight: "bold", 33 | family: "monospace", 34 | }; 35 | 36 | function getChartBackgroundColor() { 37 | // Read the --bg-panel CSS variable from the root element 38 | return getComputedStyle(document.documentElement).getPropertyValue('--bg-panel').trim(); 39 | } 40 | 41 | function getChartTextColor() { 42 | // Read the --text-primary CSS variable from the root element 43 | return getComputedStyle(document.documentElement).getPropertyValue('--text-primary').trim(); 44 | } 45 | 46 | function getChartGridColor() { 47 | // Read the --chart-grid CSS variable from the root element 48 | return getComputedStyle(document.documentElement).getPropertyValue('--chart-grid').trim(); 49 | } 50 | 51 | // Chart.js plugin to apply theme-aware background color 52 | const chartBackgroundPlugin = { 53 | id: 'chartBackground', 54 | beforeDraw: (chart) => { 55 | const ctx = chart.canvas.getContext('2d'); 56 | ctx.save(); 57 | ctx.globalCompositeOperation = 'destination-over'; 58 | ctx.fillStyle = getChartBackgroundColor(); 59 | ctx.fillRect(0, 0, chart.width, chart.height); 60 | ctx.restore(); 61 | } 62 | }; 63 | 64 | // Register the plugin globally when Chart is available 65 | if (typeof Chart !== 'undefined') { 66 | Chart.register(chartBackgroundPlugin); 67 | } 68 | 69 | function chartTitleConfig(title, padding = 0, color = null) { 70 | return { 71 | display: true, 72 | padding: padding, 73 | color: color || getChartTextColor(), 74 | text: title, 75 | font: { 76 | size: 18, 77 | family: "monospace", 78 | weight: "normal" 79 | }, 80 | }; 81 | } 82 | 83 | function buildGamingData( 84 | key1, 85 | key2, 86 | tableId = "data-table", 87 | querySelectorTag = null 88 | ) { 89 | let table = document.getElementById(tableId); 90 | if (querySelectorTag != null) { 91 | table = document.getElementById(tableId).querySelector(querySelectorTag); 92 | } 93 | const rows = table.querySelectorAll("tbody tr"); 94 | 95 | let gamingData = Array.from(rows).map((row) => { 96 | const value1 = row.cells[0].textContent; 97 | const value2 = parseFloat(row.cells[1].textContent); 98 | return { [key1]: value1, [key2]: value2 }; 99 | }); 100 | 101 | // Remove header row data 102 | gamingData.shift(); 103 | 104 | return gamingData; 105 | } 106 | 107 | // Create custom log axis in base 2 108 | class Log2Axis extends Chart.Scale { 109 | constructor(cfg) { 110 | super(cfg); 111 | this._startValue = undefined; 112 | this._valueRange = 0; 113 | } 114 | 115 | parse(raw, index) { 116 | const value = Chart.LinearScale.prototype.parse.apply(this, [raw, index]); 117 | return isFinite(value) && value > 0 ? value : null; 118 | } 119 | 120 | determineDataLimits() { 121 | const { min, max } = this.getMinMax(true); 122 | this.min = isFinite(min) ? Math.max(0, min) : null; 123 | this.max = isFinite(max) ? Math.max(0, max) : null; 124 | } 125 | 126 | buildTicks() { 127 | const ticks = []; 128 | 129 | let power = Math.floor(Math.log2(this.min || 1)); 130 | let maxPower = Math.ceil(Math.log2(this.max || 2)); 131 | while (power <= maxPower) { 132 | ticks.push({ 133 | value: Math.pow(2, power), 134 | }); 135 | power += 1; 136 | } 137 | 138 | this.min = ticks[0].value; 139 | this.max = ticks[ticks.length - 1].value; 140 | return ticks; 141 | } 142 | 143 | /** 144 | * @protected 145 | */ 146 | configure() { 147 | const start = this.min; 148 | 149 | super.configure(); 150 | 151 | this._startValue = Math.log2(start); 152 | this._valueRange = Math.log2(this.max) - Math.log2(start); 153 | } 154 | 155 | getPixelForValue(value) { 156 | if (value === undefined || value === 0) { 157 | value = this.min; 158 | } 159 | 160 | return this.getPixelForDecimal( 161 | value === this.min ? 0 : (Math.log2(value) - this._startValue) / this._valueRange 162 | ); 163 | } 164 | 165 | getValueForPixel(pixel) { 166 | const decimal = this.getDecimalForPixel(pixel); 167 | return Math.pow(2, this._startValue + decimal * this._valueRange); 168 | } 169 | } 170 | 171 | // Dummy usage of variables to suppress not used false positive in codacy 172 | // without ignoring the entire file. 173 | chartTooltipConfig; 174 | chartDataLabelFontConfig; 175 | chartLegendConfig; 176 | chartTitleConfig; 177 | buildGamingData; 178 | Log2Axis; 179 | getChartBackgroundColor; 180 | getChartTextColor; 181 | getChartGridColor; 182 | chartBackgroundPlugin; 183 | -------------------------------------------------------------------------------- /modules/PSSQLite/1.1.0/New-SqliteConnection.ps1: -------------------------------------------------------------------------------- 1 | function New-SQLiteConnection 2 | { 3 | <# 4 | .SYNOPSIS 5 | Creates a SQLiteConnection to a SQLite data source 6 | 7 | .DESCRIPTION 8 | Creates a SQLiteConnection to a SQLite data source 9 | 10 | .PARAMETER DataSource 11 | SQLite Data Source to connect to. 12 | 13 | .PARAMETER Password 14 | Specifies A Secure String password to use in the SQLite connection string. 15 | 16 | SECURITY NOTE: If you use the -Debug switch, the connectionstring including plain text password will be sent to the debug stream. 17 | 18 | .PARAMETER ReadOnly 19 | If specified, open SQLite data source as read only 20 | 21 | .PARAMETER Open 22 | We open the connection by default. You can use this parameter to create a connection without opening it. 23 | 24 | .OUTPUTS 25 | System.Data.SQLite.SQLiteConnection 26 | 27 | .EXAMPLE 28 | $Connection = New-SQLiteConnection -DataSource C:\NAMES.SQLite 29 | Invoke-SQLiteQuery -SQLiteConnection $Connection -query $Query 30 | 31 | # Connect to C:\NAMES.SQLite, invoke a query against it 32 | 33 | .EXAMPLE 34 | $Connection = New-SQLiteConnection -DataSource :MEMORY: 35 | Invoke-SqliteQuery -SQLiteConnection $Connection -Query "CREATE TABLE OrdersToNames (OrderID INT PRIMARY KEY, fullname TEXT);" 36 | Invoke-SqliteQuery -SQLiteConnection $Connection -Query "INSERT INTO OrdersToNames (OrderID, fullname) VALUES (1,'Cookie Monster');" 37 | Invoke-SqliteQuery -SQLiteConnection $Connection -Query "PRAGMA STATS" 38 | 39 | # Create a connection to a SQLite data source in memory 40 | # Create a table in the memory based datasource, verify it exists with PRAGMA STATS 41 | 42 | $Connection.Close() 43 | $Connection.Open() 44 | Invoke-SqliteQuery -SQLiteConnection $Connection -Query "PRAGMA STATS" 45 | 46 | #Close the connection, open it back up, verify that the ephemeral data no longer exists 47 | 48 | .LINK 49 | https://github.com/RamblingCookieMonster/Invoke-SQLiteQuery 50 | 51 | .LINK 52 | Invoke-SQLiteQuery 53 | 54 | .FUNCTIONALITY 55 | SQL 56 | 57 | #> 58 | [cmdletbinding()] 59 | [OutputType([System.Data.SQLite.SQLiteConnection])] 60 | param( 61 | [Parameter( Position=0, 62 | Mandatory=$true, 63 | ValueFromPipeline=$true, 64 | ValueFromPipelineByPropertyName=$true, 65 | ValueFromRemainingArguments=$false, 66 | HelpMessage='SQL Server Instance required...' )] 67 | [Alias( 'Instance', 'Instances', 'ServerInstance', 'Server', 'Servers','cn','Path','File','FullName','Database' )] 68 | [ValidateNotNullOrEmpty()] 69 | [string[]] 70 | $DataSource, 71 | 72 | [Parameter( Position=2, 73 | Mandatory=$false, 74 | ValueFromPipelineByPropertyName=$true, 75 | ValueFromRemainingArguments=$false )] 76 | [System.Security.SecureString] 77 | $Password, 78 | 79 | [Parameter( Position=3, 80 | Mandatory=$false, 81 | ValueFromPipelineByPropertyName=$true, 82 | ValueFromRemainingArguments=$false )] 83 | [Switch] 84 | $ReadOnly, 85 | 86 | [Parameter( Position=4, 87 | Mandatory=$false, 88 | ValueFromPipelineByPropertyName=$true, 89 | ValueFromRemainingArguments=$false )] 90 | [bool] 91 | $Open = $True 92 | ) 93 | Process 94 | { 95 | foreach($DataSRC in $DataSource) 96 | { 97 | if ($DataSRC -match ':MEMORY:' ) 98 | { 99 | $Database = $DataSRC 100 | } 101 | else 102 | { 103 | $Database = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($DataSRC) 104 | } 105 | 106 | Write-Verbose "Querying Data Source '$Database'" 107 | [string]$ConnectionString = "Data Source=$Database;" 108 | if ($Password) 109 | { 110 | $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Password) 111 | $PlainPassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) 112 | $ConnectionString += "Password=$PlainPassword;" 113 | } 114 | if($ReadOnly) 115 | { 116 | $ConnectionString += "Read Only=True;" 117 | } 118 | 119 | $conn = New-Object System.Data.SQLite.SQLiteConnection -ArgumentList $ConnectionString 120 | $conn.ParseViaFramework = $true #Allow UNC paths, thanks to Ray Alex! 121 | Write-Debug "ConnectionString $ConnectionString" 122 | 123 | if($Open) 124 | { 125 | Try 126 | { 127 | $conn.Open() 128 | } 129 | Catch 130 | { 131 | Write-Error $_ 132 | continue 133 | } 134 | } 135 | 136 | write-Verbose "Created SQLiteConnection:`n$($Conn | Out-String)" 137 | 138 | $Conn 139 | } 140 | } 141 | } -------------------------------------------------------------------------------- /ui/resources/css/SessionHistory/SessionHistory-shared.css: -------------------------------------------------------------------------------- 1 | /* ===== BASE STYLES ===== */ 2 | 3 | body { 4 | display: flex; 5 | flex-direction: column; 6 | font-family: monospace; 7 | background-color: var(--bg-primary); 8 | margin: 0; 9 | padding: 20px; 10 | height: 100vh; 11 | overflow: hidden; 12 | box-sizing: border-box; 13 | } 14 | 15 | h1 { 16 | text-align: center; 17 | margin-bottom: 20px; 18 | color: var(--text-primary); 19 | flex-shrink: 0; 20 | } 21 | 22 | /* Two-column layout */ 23 | #main-container { 24 | display: flex; 25 | gap: 20px; 26 | width: 100%; 27 | max-width: 1840px; 28 | margin: 0 auto 30px auto; /* Bottom margin for nav bar */ 29 | flex: 1; 30 | min-height: 0; 31 | } 32 | 33 | /* ===== LEFT/RIGHT COLUMN STRUCTURE ===== */ 34 | 35 | /* LEFT COLUMN */ 36 | #left-column { 37 | flex: 0 0 380px; 38 | display: flex; 39 | flex-direction: row; /* Changed to row to accommodate view buttons + content */ 40 | background: var(--bg-panel); 41 | border-radius: 8px; 42 | box-shadow: var(--shadow-primary) 0 2px 4px, 43 | var(--shadow-secondary) 0 7px 20px -3px; 44 | overflow: hidden; 45 | align-self: stretch; 46 | } 47 | 48 | /* RIGHT COLUMN */ 49 | #right-column { 50 | flex: 1; 51 | display: flex; 52 | flex-direction: column; 53 | background: var(--bg-panel); 54 | border-radius: 8px; 55 | box-shadow: var(--shadow-primary) 0 2px 4px, 56 | var(--shadow-secondary) 0 7px 20px -3px; 57 | padding: 20px; 58 | align-self: stretch; 59 | } 60 | 61 | /* ===== VIEW SWITCHER BUTTONS ===== */ 62 | 63 | /* View Buttons (Vertical Tabs) */ 64 | #view-buttons { 65 | display: flex; 66 | flex-direction: column; 67 | width: 60px; 68 | flex-shrink: 0; 69 | gap: 0; 70 | border-right: 1px solid var(--border-light); 71 | background: var(--bg-tertiary); 72 | height: 100%; 73 | } 74 | 75 | .view-btn { 76 | flex: 1; 77 | padding: 15px 8px; 78 | font-size: 16px; 79 | font-family: monospace; 80 | background-color: var(--bg-primary); 81 | color: var(--text-secondary); 82 | border: none; 83 | border-bottom: 1px solid var(--border-light); 84 | cursor: pointer; 85 | transition: background-color 0.2s; 86 | text-align: center; 87 | word-wrap: break-word; 88 | line-height: 1.3; 89 | display: flex; 90 | align-items: center; 91 | justify-content: center; 92 | } 93 | 94 | .view-btn:last-child { 95 | border-bottom: none; 96 | } 97 | 98 | .view-btn:hover { 99 | background-color: var(--bg-button-hover); 100 | } 101 | 102 | .view-btn.active { 103 | background-color: var(--bg-panel); 104 | color: var(--text-primary); 105 | font-weight: bold; 106 | border-left: 3px solid var(--accent-blue); 107 | } 108 | 109 | /* Left content wrapper */ 110 | .left-content { 111 | flex: 1; 112 | display: flex; 113 | flex-direction: column; 114 | overflow: hidden; 115 | background: var(--bg-panel); 116 | } 117 | 118 | /* ===== RIGHT CONTENT WRAPPER ===== */ 119 | 120 | /* Right content wrapper class */ 121 | .right-content { 122 | display: flex; 123 | flex-direction: column; 124 | height: 100%; 125 | flex: 1; 126 | } 127 | 128 | /* Games View (Right Column) - contains chart */ 129 | #games-view-right { 130 | /* Inherits from .right-content */ 131 | } 132 | 133 | /* Game Cards View (Right Column) */ 134 | #cards-view-right { 135 | /* Inherits from .right-content */ 136 | } 137 | 138 | /* ===== BOTTOM NAVIGATION BAR ===== */ 139 | 140 | /* Navigation Bar (consistent with other pages) */ 141 | .custom-button { 142 | align-items: center; 143 | appearance: none; 144 | background-color: var(--bg-button); 145 | border-radius: 4px; 146 | border-width: 0; 147 | box-shadow: var(--shadow-primary) 0 2px 4px, 148 | var(--shadow-secondary) 0 7px 13px -3px, var(--shadow-inset) 0 -3px 0 inset; 149 | box-sizing: border-box; 150 | color: var(--text-primary); 151 | cursor: pointer; 152 | display: inline-flex; 153 | font-family: monospace; 154 | height: 38px; 155 | justify-content: center; 156 | line-height: 1; 157 | list-style: none; 158 | overflow: hidden; 159 | padding-left: 16px; 160 | padding-right: 16px; 161 | position: relative; 162 | text-align: left; 163 | text-decoration: none; 164 | transition: box-shadow 0.15s, transform 0.15s; 165 | user-select: none; 166 | -webkit-user-select: none; 167 | touch-action: manipulation; 168 | white-space: nowrap; 169 | will-change: box-shadow, transform; 170 | font-size: 18px; 171 | } 172 | 173 | .custom-button:focus { 174 | box-shadow: var(--shadow-inset) 0 0 0 1.5px inset, var(--shadow-primary) 0 2px 4px, 175 | var(--shadow-secondary) 0 7px 13px -3px, var(--shadow-inset) 0 -3px 0 inset; 176 | } 177 | 178 | .custom-button:hover { 179 | box-shadow: var(--shadow-primary) 0 4px 8px, 180 | var(--shadow-secondary) 0 7px 13px -3px, var(--shadow-inset) 0 -3px 0 inset; 181 | transform: translateY(-2px); 182 | } 183 | 184 | .custom-button:active { 185 | box-shadow: var(--shadow-inset) 0 3px 7px inset; 186 | transform: translateY(2px); 187 | } 188 | 189 | /* ===== SCROLLBAR STYLING ===== */ 190 | 191 | /* Scrollbar styling for games list */ 192 | #games-list-container::-webkit-scrollbar { 193 | width: 8px; 194 | } 195 | 196 | #games-list-container::-webkit-scrollbar-track { 197 | background: var(--scrollbar-track); 198 | } 199 | 200 | #games-list-container::-webkit-scrollbar-thumb { 201 | background: var(--scrollbar-thumb); 202 | border-radius: 4px; 203 | } 204 | 205 | #games-list-container::-webkit-scrollbar-thumb:hover { 206 | background: var(--scrollbar-thumb-hover); 207 | } 208 | -------------------------------------------------------------------------------- /ui/templates/Summary.html.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Gaming Gaiden 5 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 29 | 30 | 31 | 32 |

Life Time Summary

33 |
34 |
_SUMMARYSTATEMENT_
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
Your Gaming PCs
54 |
55 | 56 |
57 | 58 |
59 |
_PCWARNING_
60 |
61 |
62 |
63 |
64 | 65 |
66 |
67 |
68 |
69 |
70 |

Add Gaming PCs to see more stats.

Settings => Gaming PCs

71 |

72 |

73 |

74 |

75 |

76 |

77 |

78 | ✞ Includes play time of games you may have deleted. Idle time is excluded. 79 |

80 |
81 |
82 |
83 | 84 |
85 |
86 |
87 |
88 | 89 | 90 | 91 | 92 | 93 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /ui/resources/css/SessionHistory/SessionHistory-games.css: -------------------------------------------------------------------------------- 1 | /* ===== GAMES VIEW - SEARCH & SORT ===== */ 2 | 3 | #search-container { 4 | padding: 15px; 5 | border-bottom: 1px solid var(--border-light); 6 | background: var(--bg-tertiary); 7 | } 8 | 9 | #game-search { 10 | width: 100%; 11 | padding: 10px; 12 | font-size: 14px; 13 | font-family: monospace; 14 | border: 1px solid var(--border-light); 15 | border-radius: 4px; 16 | box-sizing: border-box; 17 | } 18 | 19 | #game-search:focus { 20 | outline: none; 21 | border-color: var(--status-finished-alt); 22 | } 23 | 24 | #sort-container { 25 | display: flex; 26 | gap: 0; 27 | border-bottom: 1px solid var(--border-light); 28 | background: var(--bg-tertiary); 29 | } 30 | 31 | .sort-btn { 32 | flex: 1; 33 | padding: 10px; 34 | font-size: 13px; 35 | font-family: monospace; 36 | background-color: var(--bg-primary); 37 | color: var(--text-secondary); 38 | border: none; 39 | border-right: 1px solid var(--border-light); 40 | cursor: pointer; 41 | transition: background-color 0.2s; 42 | } 43 | 44 | .sort-btn:last-child { 45 | border-right: none; 46 | } 47 | 48 | .sort-btn:hover { 49 | background-color: var(--bg-button-hover); 50 | } 51 | 52 | .sort-btn.active { 53 | background-color: var(--bg-panel); 54 | color: var(--text-primary); 55 | font-weight: bold; 56 | } 57 | 58 | .sort-arrow { 59 | font-size: 10px; 60 | margin-left: 4px; 61 | } 62 | 63 | /* ===== GAMES VIEW - GAMES LIST ===== */ 64 | 65 | #games-list-container { 66 | flex: 1; 67 | overflow-y: auto; 68 | padding: 0; 69 | } 70 | 71 | #games-list { 72 | list-style: none; 73 | margin: 0; 74 | padding: 0; 75 | } 76 | 77 | .game-list-item { 78 | padding: 15px; 79 | border-bottom: 1px solid var(--border-light); 80 | cursor: pointer; 81 | transition: background-color 0.2s; 82 | display: flex; 83 | align-items: center; 84 | gap: 12px; 85 | } 86 | 87 | .game-list-item:hover { 88 | background-color: var(--bg-tertiary); 89 | } 90 | 91 | .game-list-item.active { 92 | background-color: var(--accent-blue-light); 93 | border-left: 4px solid var(--accent-blue); 94 | } 95 | 96 | .game-list-item { 97 | padding: 15px; 98 | border-bottom: 1px solid var(--border-light); 99 | cursor: pointer; 100 | transition: background-color 0.2s; 101 | display: flex; 102 | align-items: center; 103 | gap: 12px; 104 | } 105 | 106 | .game-icon { 107 | height: 36px; 108 | width: auto; 109 | filter: drop-shadow(4px 4px 2px var(--shadow-image)); 110 | flex-shrink: 0; 111 | cursor: pointer; 112 | position: relative; 113 | } 114 | 115 | .game-info { 116 | flex: 1; 117 | min-width: 0; 118 | } 119 | 120 | .game-name { 121 | font-weight: bold; 122 | font-size: 14px; 123 | margin-bottom: 5px; 124 | color: var(--text-primary); 125 | } 126 | 127 | .game-meta { 128 | font-size: 12px; 129 | color: var(--text-secondary); 130 | display: flex; 131 | justify-content: space-between; 132 | } 133 | 134 | .platform { 135 | font-style: italic; 136 | } 137 | 138 | .stats { 139 | color: var(--text-secondary); 140 | } 141 | 142 | /* Games View Content - inherits from .left-content */ 143 | #games-view-content { 144 | /* No additional styling needed - inherits from .left-content */ 145 | } 146 | 147 | /* ===== GAMES VIEW - SELECTED GAME HEADER ===== */ 148 | 149 | #selected-game-header { 150 | margin-bottom: 20px; 151 | border-bottom: 2px solid var(--border-light); 152 | padding-bottom: 15px; 153 | } 154 | 155 | #selected-game-name { 156 | margin: 0 0 5px 0; 157 | color: var(--text-primary); 158 | font-size: 20px; 159 | } 160 | 161 | .selected-game-icon { 162 | height: 140px; 163 | width: auto; 164 | filter: drop-shadow(4px 4px 2px var(--shadow-image)); 165 | vertical-align: middle; 166 | margin-left: 12px; 167 | float: right; 168 | } 169 | 170 | #selected-game-stats { 171 | margin: 10px 0 0 0; 172 | display: flex; 173 | flex-direction: column; 174 | gap: 5px; 175 | color: var(--text-secondary); 176 | font-size: 16px; 177 | } 178 | 179 | .stat-item { 180 | display: flex; 181 | gap: 8px; 182 | align-items: baseline; 183 | } 184 | 185 | .stat-label { 186 | font-size: 16px; 187 | color: var(--text-secondary); 188 | } 189 | 190 | .stat-value { 191 | font-size: 16px; 192 | color: var(--text-secondary); 193 | } 194 | 195 | /* ===== GAMES VIEW - CHART CONTROLS ===== */ 196 | 197 | /* Chart Navigation Bar */ 198 | #chart-navigation-bar { 199 | display: flex; 200 | margin-top: 15px; 201 | margin-bottom: 10px; 202 | width: 500px; 203 | margin-left: auto; 204 | margin-right: auto; 205 | justify-content: space-around; 206 | align-items: center; 207 | } 208 | 209 | #date-display { 210 | font-family: monospace; 211 | font-size: 16px; 212 | text-align: center; 213 | flex: 1; 214 | max-width: 300px; 215 | color: var(--text-primary); 216 | } 217 | 218 | #view-toggle-button { 219 | display: block; 220 | margin-left: auto; 221 | margin-right: auto; 222 | margin-top: 10px; 223 | } 224 | 225 | /* Smaller button sizes for navigation and toggle */ 226 | #chart-navigation-bar .custom-button { 227 | height: 32px; 228 | padding-left: 12px; 229 | padding-right: 12px; 230 | font-size: 16px; 231 | } 232 | 233 | #view-toggle-button { 234 | height: 32px; 235 | padding-left: 12px; 236 | padding-right: 12px; 237 | font-size: 16px; 238 | } 239 | 240 | #chart-container { 241 | flex: 1; 242 | position: relative; 243 | min-height: 400px; 244 | display: flex; 245 | flex-direction: column; 246 | } 247 | 248 | #session-chart { 249 | flex: 1; 250 | min-height: 0; 251 | } 252 | 253 | /* ===== GAMES VIEW - NO SELECTION MESSAGE ===== */ 254 | 255 | #no-selection-message { 256 | display: flex; 257 | align-items: center; 258 | justify-content: center; 259 | height: 100%; 260 | color: var(--text-secondary); 261 | font-size: 16px; 262 | } 263 | -------------------------------------------------------------------------------- /ui/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Gaming Gaiden 5 | 10 | 11 | 12 | 115 | 116 |
117 | 118 |

Please render this page atleast once from
app menu before using web ui buttons

119 | 120 |
121 | 131 | 132 | -------------------------------------------------------------------------------- /ui/resources/css/Summary.css: -------------------------------------------------------------------------------- 1 | body { 2 | display: flex; 3 | flex-direction: column; 4 | font-family: monospace; 5 | background-color: var(--bg-primary); 6 | margin: 0; 7 | padding: 20px; 8 | height: 100vh; 9 | overflow: hidden; 10 | box-sizing: border-box; 11 | } 12 | 13 | h1 { 14 | text-align: center; 15 | margin-bottom: 20px; 16 | color: var(--text-primary); 17 | flex-shrink: 0; 18 | } 19 | 20 | #main-panel { 21 | width: 100%; 22 | background: var(--bg-panel); 23 | border-radius: 8px; 24 | box-shadow: var(--shadow-primary) 0 2px 4px, 25 | var(--shadow-secondary) 0 7px 20px -3px; 26 | padding: 30px; 27 | margin: 0 auto 30px auto; 28 | max-width: 1840px; 29 | display: flex; 30 | flex-direction: column; 31 | align-items: center; 32 | flex: 1; 33 | min-height: 0; 34 | box-sizing: border-box; 35 | } 36 | 37 | #summary { 38 | font-size: 18px; 39 | margin: 5px; 40 | flex-shrink: 0; 41 | text-align: center; 42 | color: var(--text-primary); 43 | } 44 | 45 | #top-row { 46 | display: flex; 47 | width: 100%; 48 | margin: 5px; 49 | font-size: 18px; 50 | flex-shrink: 0; 51 | } 52 | 53 | #summary-chart-legend { 54 | flex: 1 1 60%; 55 | display: flex; 56 | flex-direction: column; 57 | align-items: center; 58 | } 59 | 60 | #pc-title-column { 61 | flex: 1 1 40%; 62 | display: flex; 63 | flex-direction: column; 64 | align-items: center; 65 | color: var(--text-primary); 66 | } 67 | 68 | #legend { 69 | display: flex; 70 | margin: 5px; 71 | justify-content: space-around; 72 | align-items: center; 73 | width: 500px; 74 | } 75 | 76 | #sub-legend { 77 | display: flex; 78 | margin: 5px; 79 | justify-content: space-around; 80 | align-items: center; 81 | width: 650px; 82 | } 83 | 84 | .legend-icon { 85 | width: 20px; 86 | height: 20px; 87 | border-radius: 50%; 88 | margin-right: -20px; 89 | } 90 | 91 | #finished-icon { 92 | background: var(--status-finished); 93 | } 94 | 95 | #progress-icon { 96 | background: var(--accent-blue); 97 | } 98 | 99 | #hold-icon { 100 | background: var(--status-hold); 101 | } 102 | 103 | #forever-icon { 104 | background: var(--status-forever); 105 | } 106 | 107 | #dropped-icon { 108 | background: var(--status-dropped); 109 | } 110 | 111 | #pc-section-title { 112 | margin-top: 5px; 113 | font-weight: bold; 114 | } 115 | 116 | #pc-navigation-bar { 117 | display: flex; 118 | width: 75%; 119 | justify-content: space-around; 120 | align-items: center; 121 | } 122 | 123 | .custom-button { 124 | align-items: center; 125 | appearance: none; 126 | background-color: var(--bg-button); 127 | border-radius: 4px; 128 | border-width: 0; 129 | box-shadow: var(--shadow-primary) 0 2px 4px, 130 | var(--shadow-secondary) 0 7px 13px -3px, var(--shadow-inset) 0 -3px 0 inset; 131 | box-sizing: border-box; 132 | color: var(--text-primary); 133 | cursor: pointer; 134 | display: inline-flex; 135 | font-family: monospace; 136 | height: 38px; 137 | justify-content: center; 138 | line-height: 1; 139 | list-style: none; 140 | overflow: hidden; 141 | padding-left: 16px; 142 | padding-right: 16px; 143 | position: relative; 144 | text-align: left; 145 | text-decoration: none; 146 | transition: box-shadow 0.15s, transform 0.15s; 147 | user-select: none; 148 | -webkit-user-select: none; 149 | touch-action: manipulation; 150 | white-space: nowrap; 151 | will-change: box-shadow, transform; 152 | font-size: 18px; 153 | } 154 | 155 | .custom-button:focus { 156 | box-shadow: var(--shadow-inset) 0 0 0 1.5px inset, var(--shadow-primary) 0 2px 4px, 157 | var(--shadow-secondary) 0 7px 13px -3px, var(--shadow-inset) 0 -3px 0 inset; 158 | } 159 | 160 | .custom-button:hover { 161 | box-shadow: var(--shadow-primary) 0 4px 8px, 162 | var(--shadow-secondary) 0 7px 13px -3px, var(--shadow-inset) 0 -3px 0 inset; 163 | transform: translateY(-2px); 164 | } 165 | 166 | .custom-button:active { 167 | box-shadow: var(--shadow-inset) 0 3px 7px inset; 168 | transform: translateY(2px); 169 | } 170 | 171 | #main-row { 172 | display: flex; 173 | flex: 1; 174 | min-height: 0; 175 | justify-content: space-around; 176 | width: 100%; 177 | } 178 | 179 | #summary-chart-container { 180 | flex: 1 1 60%; 181 | padding-left: 1%; 182 | max-width: 100%; 183 | max-height: 100%; 184 | min-height: 0; 185 | display: flex; 186 | flex-direction: column; 187 | } 188 | 189 | #summary-chart-container canvas { 190 | flex: 1; 191 | min-height: 0; 192 | } 193 | 194 | #stats-container { 195 | flex: 1 1 40%; 196 | display: flex; 197 | flex-direction: column; 198 | min-height: 0; 199 | } 200 | 201 | #pc-icon img { 202 | width: auto; 203 | height: auto; 204 | max-height: 90%; 205 | max-width: 90%; 206 | object-fit: contain; 207 | filter: drop-shadow(4px 4px 2px var(--shadow-image)); 208 | transition: transform 0.2s ease; 209 | cursor: pointer; 210 | } 211 | 212 | #pc-icon img:hover { 213 | transform: scale(2); 214 | z-index: 1000; 215 | outline: 1px solid white; 216 | filter: drop-shadow(8px 8px 4px var(--shadow-image)); 217 | } 218 | 219 | #pc-icon { 220 | width: 50%; 221 | aspect-ratio: 1/1; 222 | display: flex; 223 | align-items: center; 224 | justify-content: center; 225 | margin: 10px; 226 | } 227 | 228 | #pc-stats-section { 229 | flex: 1; 230 | display: flex; 231 | min-height: 0; 232 | } 233 | 234 | #pc-stats { 235 | display: flex; 236 | flex-direction: column; 237 | justify-content: space-evenly; 238 | height: 90%; 239 | padding: 10px; 240 | color: var(--text-primary); 241 | } 242 | 243 | #pc-stats p { 244 | font-size: 17px; 245 | margin: 5px 0; 246 | text-align: left; 247 | color: var(--text-primary); 248 | } 249 | 250 | #annual-time-chart-container { 251 | flex: 1; 252 | display: flex; 253 | flex-direction: column; 254 | min-height: 0; 255 | } 256 | 257 | #annual-time-chart-container canvas { 258 | flex: 1; 259 | min-height: 0; 260 | } 261 | 262 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | [![GitHub stars](https://img.shields.io/github/stars/kulvind3r/gaminggaiden)](https://github.com/kulvind3r/gaminggaiden/stargazers) 4 | [![GitHub Downloads (latest)](https://img.shields.io/github/downloads/kulvind3r/gaminggaiden/latest/total?label=Downloads%20-%20Latest&color=%23FFD166)](https://github.com/kulvind3r/GamingGaiden/releases/latest) 5 | ![GitHub Downloads (all)](https://img.shields.io/github/downloads/kulvind3r/gaminggaiden/total?label=Downloads%20-%20Total&color=%23FFD166) 6 | 7 | [![Codacy Quality](https://app.codacy.com/project/badge/Grade/c4a01f22c3864d8c80b8c6891a6feb5f)](https://app.codacy.com/gh/kulvind3r/GamingGaiden/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) 8 | [![GitHub commit activity](https://img.shields.io/github/commit-activity/m/kulvind3r/gaminggaiden?label=Commit%20Activity&color=%23073B4C)](https://github.com/kulvind3r/gaminggaiden/graphs/commit-activity) 9 | [![GitHub issues](https://img.shields.io/github/issues/kulvind3r/gaminggaiden?label=Issues&color=%23118AB2)](https://github.com/kulvind3r/gaminggaiden/issues) 10 | 11 | ![Gaming Gaiden](./readme-files/GamingGaidenBanner.png) 12 | 13 |
14 | 15 | ### 外伝 (Gaiden) 16 | 17 | Japanese 18 | 19 | noun (common) 20 | 21 | A Tale; Side Story; 22 | 23 | A small powershell tray application for windows os to track gaming time. Helps you record your gaming story over the years. 24 | 25 | https://github.com/user-attachments/assets/4837b88c-e403-4069-a3f5-3f0147e9328a 26 | 27 | ## Features 28 | - #### Time Tracking and Emulator Support 29 | - Tracks play time for PC or emulated games. 30 | - Auto tracks new roms after registering any emulator just once. 31 | - Supports Retroarch cores. 32 | - Detects and removes idle time from gaming sessions. 33 | - Out of box HWiNFO64 integration with session time and tracking status metrics. 34 | - #### UI and Statistics 35 | - Fast browser based UI with search and sorting. Quick view popup for recent games. 36 | - Multiple in depth statistics on gaming. Lifetime summary, monthly/yearly time analysis, most played games, games per emulator etc. 37 | - Integrated google image search for game icons / box art. 38 | - Mark games as Finished / Playing to track backlog completion. 39 | - #### Quality of Life Features 40 | - Small size (~7 MB). High performance (Sub 5 sec game detection). Light on cpu & ram. 41 | - Completely offline and portable, all data stored in local database. 42 | - Automated data backup after each gaming session. 43 | 44 | > [!WARNING] 45 | > Gaming Gaiden is only available for download on this Github repo. Any copy available elsewhere could be malicious. 46 | 47 | ## How to install / upgrade / use 48 | 1. Open a Powershell window as admin and run below command to allow powershell modules to load on your system. Choose `Yes` when prompted. 49 | - `Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser` 50 | 51 | 2. Download ***GamingGaiden.zip*** from the [latest release](https://github.com/kulvind3r/GamingGaiden/releases/latest). 52 | 3. Extract ***GamingGaiden*** folder and Run `Install.bat`. Choose Yes/No for autostart at Boot. 53 | 4. Use the shortcut on desktop / start menu for launching the application. 54 | 5. Regularly backup your `GamingGaiden.db` and `backups` folder to avoid data loss. Click ***Settings => Open Install Directory*** option in app menu to find them. 55 | 56 | ## How to uninstall 57 | 1. Save any backups if you want from the `C:\ProgramData\GamingGaiden\backups` directory. 58 | 2. Delete the directory `C:\ProgramData\GamingGaiden` 59 | 3. Delete the shortcuts from Start Menu and Desktop. 60 | 4. Open Run Dialog by pressing `Win + R` , enter `"%APPDATA%\Microsoft\Windows\Start Menu\Programs\StartUp"` in the dialog (including quotes) and press enter. Delete the shortcut from the directory that opens. 61 | 62 | ## Unknown Publisher 63 | Windows SmartScreen may warn that the application is from an ***Unknown Publisher*** because it lacks signature from a public CA. 64 | Signing cost for apps is hundreds of dollars per year. Can't afford them. 65 | 66 | ## Antivirus False Positives 67 | > :hearts: 68 | > Anitvirus false positives are hard to fight. 69 | > If you have found the app useful and safe. Please leave a star on github to increase trust. 70 | 71 | GamingGaiden performs following tasks that are similar to common malware behavior, leading it to be flagged as malware by antivirus software. 72 | 73 | - Scanning running programs to detect and track games. 74 | - Adding registry entries for HWinfo64 integration. 75 | - Periodically sleeping to conserve resources. 76 | - Monitoring user activity to detect idle time. 77 | - Packaged as an executable using ps12exe. 78 | 79 | Its PowerShell-based implementation also raises flags as powershell scripts can be used maliciously and have low trust in tech community. 80 | 81 | Antivirus flag such behavior to keep users safe without doing actual verification of malicious actiity. Fixing false positives requires manually requesting antivirus providers to unflag GamingGaiden or rewriting it in a compiled language like C#. Even then there is no guarantee of a fix due to it's functionality being process scanning. 82 | 83 | Given that I wrote it for personal use, above is not something I can work on atleast for some time. The source code is open and available for anyone to review and ensure nothing wrong is happening. Users are responsible for their own safety and actions when using the program. 84 | 85 | Please remember that open-source software comes without any support or warranties. 86 | 87 | ## Attributions 88 | Made with love using 89 | 90 | - [PSSQLite](https://www.powershellgallery.com/packages/PSSQLite) by [Warren Frame](https://github.com/RamblingCookieMonster) 91 | - [ps12exe](https://github.com/steve02081504/ps12exe) by [Steve Green](https://github.com/steve02081504) 92 | - [DOMPurify](https://github.com/cure53/DOMPurify) by [Cure53](https://github.com/cure53) 93 | - [DataTables](https://datatables.net/) 94 | - [Jquery](https://jquery.com/) 95 | - [ChartJs](https://www.chartjs.org/) 96 | - Various Icons from [Icons8](https://icons8.com) 97 | - Game Cartridge Icon from [FreePik on Flaticon](https://www.flaticon.com/free-icons/game-cartridge) 98 | - Cute [Ninja Vector by Catalyststuff on Freepik](https://www.freepik.com/free-vector/cute-ninja-gaming-cartoon-vector-icon-illustration-people-technology-icon-concept-isolated-flat_42903434.htm) 99 | - [Ninja Garden Font](https://www.fontspace.com/ninja-garden-font-f32923) by [Iconian Fonts](https://www.fontspace.com/iconian-fonts) 100 | -------------------------------------------------------------------------------- /ui/resources/css/AllGames.css: -------------------------------------------------------------------------------- 1 | body { 2 | display: flex; 3 | flex-direction: column; 4 | font-family: monospace; 5 | background-color: var(--bg-primary); 6 | margin: 0; 7 | padding: 20px; 8 | min-height: 100vh; 9 | overflow: auto; 10 | box-sizing: border-box; 11 | } 12 | 13 | #header-container { 14 | display: flex; 15 | justify-content: space-between; 16 | align-items: center; 17 | width: 100%; 18 | max-width: 1840px; 19 | margin: 0 auto 20px auto; 20 | gap: 20px; 21 | flex-shrink: 0; 22 | } 23 | 24 | #header-container h1 { 25 | flex: 1; 26 | text-align: center; 27 | margin: 0; 28 | color: var(--text-primary); 29 | } 30 | 31 | #TotalPlaytime-placeholder { 32 | flex: 0 1 auto; 33 | min-width: 200px; 34 | } 35 | 36 | #filter-placeholder { 37 | flex: 0 1 auto; 38 | min-width: 200px; 39 | display: flex; 40 | justify-content: flex-end; 41 | } 42 | 43 | h1 { 44 | text-align: center; 45 | margin-bottom: 20px; 46 | color: var(--text-primary); 47 | flex-shrink: 0; 48 | } 49 | 50 | #main-panel { 51 | width: 100%; 52 | background: var(--bg-panel); 53 | border-radius: 8px; 54 | box-shadow: var(--shadow-primary) 0 2px 4px, 55 | var(--shadow-secondary) 0 7px 20px -3px; 56 | padding: 30px; 57 | margin: 0 auto 30px auto; 58 | max-width: 1840px; 59 | display: flex; 60 | flex-direction: column; 61 | align-items: center; 62 | box-sizing: border-box; 63 | } 64 | 65 | #games-table-container { 66 | width: 100%; 67 | align-self: stretch; 68 | opacity: 0; 69 | transition: opacity 0.3s ease-in; 70 | color: var(--text-primary) 71 | } 72 | 73 | #games-table-container.ready { 74 | opacity: 1; 75 | } 76 | 77 | table { 78 | width: 100% !important; 79 | border: 1px solid var(--border-primary); 80 | } 81 | 82 | /* Table Header styles */ 83 | th { 84 | background-color: var(--table-header-bg); 85 | color: var(--text-inverse); 86 | font-weight: bold; 87 | text-align: center !important; 88 | } 89 | 90 | th:first-child { 91 | color: var(--table-header-bg); 92 | } 93 | 94 | /* Table Row styles */ 95 | tr:nth-child(even) { 96 | background-color: var(--bg-table-stripe) !important; 97 | } 98 | 99 | /* Table Cell & Column styles */ 100 | td { 101 | border: 1px solid var(--border-primary); 102 | width: auto; 103 | text-align: center; 104 | height: 50px; 105 | padding: 6px 5px !important; 106 | } 107 | 108 | /* Define specific styles for the Icon column */ 109 | td:first-child { 110 | width: 5%; 111 | position: relative; 112 | overflow: visible; 113 | } 114 | 115 | td:first-child img { 116 | height: 70px; 117 | margin-left: auto; 118 | margin-right: auto; 119 | display: block; 120 | filter: drop-shadow(4px 4px 2px var(--shadow-image)); 121 | transition: transform 0.2s ease; 122 | cursor: pointer; 123 | position: relative; 124 | } 125 | 126 | td:first-child img:hover { 127 | transform: scale(2); 128 | z-index: 1000; 129 | filter: drop-shadow(8px 8px 4px var(--shadow-image)); 130 | outline: 1px solid white; 131 | } 132 | 133 | /* Define specific styles for the Name column */ 134 | td:nth-child(2) { 135 | font-weight: bold; 136 | width: 35%; 137 | text-transform: uppercase; 138 | } 139 | 140 | /* Define specific styles for the Sessions column */ 141 | td:nth-child(5) { 142 | width: 5%; 143 | } 144 | 145 | /* Define specific styles for the Status column */ 146 | td:nth-child(6) { 147 | width: 5%; 148 | } 149 | 150 | td:nth-child(6) img { 151 | height: inherit; 152 | margin-left: auto; 153 | margin-right: auto; 154 | display: block; 155 | } 156 | 157 | /* Define specific styles for the Gaming PC column */ 158 | td:nth-child(7) { 159 | width: auto; 160 | line-height: 1.4; 161 | } 162 | 163 | /* Override default data table css styles */ 164 | #DataTables_Table_0_filter { 165 | margin: 0; 166 | text-align: right; 167 | color: var(--text-primary); 168 | } 169 | 170 | #DataTables_Table_0_info { 171 | margin-left: 0; 172 | color: var(--text-primary); 173 | } 174 | 175 | #DataTables_Table_0_paginate .disabled { 176 | color: var(--text-secondary) !important; 177 | opacity: 0.4; 178 | } 179 | 180 | #DataTables_Table_0_paginate { 181 | margin-right: 0; 182 | color: var(--text-primary); 183 | } 184 | 185 | /* Playtime relative bar fill */ 186 | .playtimegradient:not(th) { 187 | background-image: linear-gradient( 188 | var(--chart-blue), 189 | var(--chart-blue) 190 | ); 191 | background-repeat: no-repeat; 192 | background-position-x: 8px; 193 | background-position-y: 6px; 194 | } 195 | 196 | /* Other elements */ 197 | #TotalPlaytime { 198 | white-space: pre; 199 | margin: 0; 200 | text-align: left; 201 | color: var(--text-primary) 202 | } 203 | 204 | #button-container { 205 | display: flex; 206 | justify-content: center; 207 | width: 100%; 208 | margin-top: 10px; 209 | } 210 | 211 | .custom-button { 212 | align-items: center; 213 | appearance: none; 214 | background-color: var(--bg-button); 215 | border-radius: 4px; 216 | border-width: 0; 217 | box-shadow: var(--shadow-primary) 0 2px 4px, 218 | var(--shadow-secondary) 0 7px 13px -3px, var(--shadow-inset) 0 -3px 0 inset; 219 | box-sizing: border-box; 220 | color: var(--text-primary); 221 | cursor: pointer; 222 | display: inline-flex; 223 | font-family: monospace; 224 | height: 38px; 225 | justify-content: center; 226 | line-height: 1; 227 | list-style: none; 228 | overflow: hidden; 229 | padding-left: 16px; 230 | padding-right: 16px; 231 | position: relative; 232 | text-align: left; 233 | text-decoration: none; 234 | transition: box-shadow 0.15s, transform 0.15s; 235 | user-select: none; 236 | -webkit-user-select: none; 237 | touch-action: manipulation; 238 | white-space: nowrap; 239 | will-change: box-shadow, transform; 240 | font-size: 18px; 241 | } 242 | 243 | .custom-button:focus { 244 | box-shadow: var(--shadow-inset) 0 0 0 1.5px inset, var(--shadow-primary) 0 2px 4px, 245 | var(--shadow-secondary) 0 7px 13px -3px, var(--shadow-inset) 0 -3px 0 inset; 246 | } 247 | 248 | .custom-button:hover { 249 | box-shadow: var(--shadow-primary) 0 4px 8px, 250 | var(--shadow-secondary) 0 7px 13px -3px, var(--shadow-inset) 0 -3px 0 inset; 251 | transform: translateY(-2px); 252 | } 253 | 254 | .custom-button:active { 255 | box-shadow: var(--shadow-inset) 0 3px 7px inset; 256 | transform: translateY(2px); 257 | } 258 | 259 | -------------------------------------------------------------------------------- /modules/HelperFunctions.psm1: -------------------------------------------------------------------------------- 1 | function Log($MSG) { 2 | $mutex = New-Object System.Threading.Mutex($false, "LogFileLock") 3 | 4 | if ($mutex.WaitOne(500)) { 5 | Write-Output "$(Get-date -f s) : $MSG" >> ".\GamingGaiden.log" 6 | [void]$mutex.ReleaseMutex() 7 | } 8 | } 9 | 10 | function SQLEscapedMatchPattern($pattern) { 11 | return $pattern -replace "'", "''" 12 | } 13 | 14 | function ToBase64($String) { 15 | return [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($String)) 16 | } 17 | 18 | function PlayTimeMinsToString($PlayTime) { 19 | $minutes = $null; $hours = [math]::divrem($PlayTime, 60, [ref]$minutes); 20 | return ("{0} Hr {1} Min" -f $hours, $minutes) 21 | } 22 | 23 | function ResizeImage() { 24 | 25 | param( 26 | [string]$ImagePath, 27 | [string]$EntityName, 28 | [bool]$HD = $false 29 | ) 30 | 31 | $imageFileName = ToBase64 $EntityName 32 | $WIA = New-Object -com wia.imagefile 33 | $WIA.LoadFile($ImagePath) 34 | $WIP = New-Object -ComObject wia.imageprocess 35 | $scale = $WIP.FilterInfos.Item("Scale").FilterId 36 | $WIP.Filters.Add($scale) 37 | 38 | $WIP.Filters[1].Properties("PreserveAspectRatio") = $true 39 | 40 | if ($HD) { 41 | if ($WIA.Width -gt 720 -or $WIA.Height -gt 720) { 42 | $WIP.Filters[1].Properties("MaximumWidth") = 720 43 | $WIP.Filters[1].Properties("MaximumHeight") = 720 44 | } 45 | else { 46 | $WIP.Filters[1].Properties("MaximumWidth") = $WIA.Width 47 | $WIP.Filters[1].Properties("MaximumHeight") = $WIA.Height 48 | } 49 | } 50 | else { 51 | $WIP.Filters[1].Properties("MaximumWidth") = 140 52 | $WIP.Filters[1].Properties("MaximumHeight") = 140 53 | } 54 | 55 | $scaledImage = $WIP.Apply($WIA) 56 | $scaledImagePath = $null 57 | if ($ImagePath -like '*.png') { 58 | $scaledImagePath = "$env:TEMP\GmGdn-{0}-$imageFileName.png" -f $(Get-Random) 59 | } 60 | else { 61 | $scaledImagePath = "$env:TEMP\GmGdn-{0}-$imageFileName.jpg" -f $(Get-Random) 62 | } 63 | 64 | $scaledImage.SaveFile($scaledImagePath) 65 | return $scaledImagePath 66 | } 67 | 68 | function CreateMenuItem($Text) { 69 | $menuItem = New-Object System.Windows.Forms.ToolStripmenuItem 70 | $menuItem.Text = "$Text" 71 | 72 | return $menuItem 73 | } 74 | 75 | function OpenFileDialog($Title, $Filters, $DirectoryPath = [Environment]::GetFolderPath('Desktop')) { 76 | $fileBrowser = New-Object System.Windows.Forms.OpenFileDialog -Property @{ 77 | InitialDirectory = $DirectoryPath 78 | Filter = $Filters 79 | Title = $Title 80 | } 81 | return $fileBrowser 82 | } 83 | 84 | function ShowMessage($Msg, $Buttons, $Type) { 85 | [System.Windows.Forms.MessageBox]::Show($Msg, 'Gaming Gaiden', $Buttons, $Type) | Out-Null 86 | } 87 | 88 | function CalculateFileHash ($FilePath) { 89 | $fileName = (Get-Item $FilePath).Name 90 | Copy-Item $FilePath "$env:TEMP\$fileName" 91 | 92 | $fileHash = Get-FileHash "$env:TEMP\$fileName" 93 | Remove-Item "$env:TEMP\$fileName" 94 | 95 | return $fileHash.Hash 96 | } 97 | 98 | function BackupDatabase { 99 | Log "Backing up database" 100 | 101 | $workingDirectory = (Get-Location).Path 102 | mkdir -f $workingDirectory\backups | Out-Null 103 | $timestamp = Get-Date -f "dd-MM-yyyy-HH.mm.ss" 104 | 105 | Copy-Item ".\GamingGaiden.db" "$env:TEMP\" 106 | Compress-Archive "$env:TEMP\GamingGaiden.db" ".\backups\GamingGaiden-$timestamp.zip" 107 | Remove-Item "$env:TEMP\GamingGaiden.db" 108 | 109 | Get-ChildItem -Path .\backups -File | Sort-Object -Property CreationTime | Select-Object -SkipLast 5 | Remove-Item 110 | } 111 | 112 | function RunDBQuery ($Query, $SQLParameters = $null) { 113 | if ($null -eq $SQLParameters) { 114 | $result = Invoke-SqliteQuery -Query $Query -DataBase ".\GamingGaiden.db" 115 | } 116 | else { 117 | $result = Invoke-SqliteQuery -Query $Query -DataBase ".\GamingGaiden.db" -SqlParameters $SQLParameters 118 | } 119 | return $result 120 | } 121 | 122 | function CreateForm($Text, $SizeX, $SizeY, $IconPath) { 123 | $form = New-Object System.Windows.Forms.Form 124 | $form.Text = $Text 125 | $form.Size = New-Object Drawing.Size($SizeX, $SizeY) 126 | $form.StartPosition = 'CenterScreen' 127 | $form.FormBorderStyle = 'FixedDialog' 128 | $form.Icon = [System.Drawing.Icon]::new($IconPath) 129 | $form.Topmost = $true 130 | $form.ShowInTaskbar = $false 131 | 132 | return $form 133 | } 134 | 135 | function Createlabel($Text, $DrawX, $DrawY) { 136 | $label = New-Object System.Windows.Forms.Label 137 | $label.AutoSize = $true 138 | $label.Location = New-Object Drawing.Point($DrawX, $DrawY) 139 | $label.Text = $Text 140 | 141 | return $label 142 | } 143 | 144 | function CreateTextBox($Text, $DrawX, $DrawY, $SizeX, $SizeY) { 145 | $textBox = New-Object System.Windows.Forms.TextBox 146 | $textBox.Text = $Text 147 | $textBox.Location = New-Object Drawing.Point($DrawX, $DrawY) 148 | $textBox.Size = New-Object System.Drawing.Size($SizeX, $SizeY) 149 | 150 | return $textBox 151 | } 152 | 153 | function CreateButton($Text, $DrawX, $DrawY) { 154 | $button = New-Object System.Windows.Forms.Button 155 | $button.Location = New-Object Drawing.Point($DrawX, $DrawY) 156 | $button.Text = $Text 157 | 158 | return $button 159 | } 160 | 161 | function CreatePictureBox() { 162 | param( 163 | [string]$ImagePath, 164 | [int]$DrawX, 165 | [int]$DrawY, 166 | [int]$SizeX, 167 | [int]$SizeY, 168 | [string]$SizeMode = "center" 169 | ) 170 | 171 | $pictureBox = New-Object Windows.Forms.PictureBox 172 | $pictureBox.Location = New-Object Drawing.Point($DrawX, $DrawY) 173 | $pictureBox.Size = New-Object Drawing.Size($SizeX, $SizeY) 174 | $pictureBox.SizeMode = [System.Windows.Forms.PictureBoxSizeMode]::CenterImage 175 | if ($SizeMode -eq "zoom") { 176 | $pictureBox.SizeMode = [System.Windows.Forms.PictureBoxSizeMode]::Zoom 177 | } 178 | $pictureBox.Image = [System.Drawing.Image]::FromFile($ImagePath) 179 | 180 | return $pictureBox 181 | } 182 | 183 | function Get-AppVersion { 184 | $exePath = [System.Diagnostics.Process]::GetCurrentProcess().MainModule.FileName 185 | $versionInfo = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($exePath) 186 | $version = "{0}.{1}.{2}" -f $versionInfo.FileMajorPart, $versionInfo.FileMinorPart, $versionInfo.FileBuildPart 187 | return "v" + $version 188 | } -------------------------------------------------------------------------------- /modules/PSSQLite/1.1.0/Out-DataTable.ps1: -------------------------------------------------------------------------------- 1 | function Out-DataTable 2 | { 3 | <# 4 | .SYNOPSIS 5 | Creates a DataTable for an object 6 | 7 | .DESCRIPTION 8 | Creates a DataTable based on an object's properties. 9 | 10 | .PARAMETER InputObject 11 | One or more objects to convert into a DataTable 12 | 13 | .PARAMETER NonNullable 14 | A list of columns to set disable AllowDBNull on 15 | 16 | .INPUTS 17 | Object 18 | Any object can be piped to Out-DataTable 19 | 20 | .OUTPUTS 21 | System.Data.DataTable 22 | 23 | .EXAMPLE 24 | $dt = Get-psdrive | Out-DataTable 25 | 26 | # This example creates a DataTable from the properties of Get-psdrive and assigns output to $dt variable 27 | 28 | .EXAMPLE 29 | Get-Process | Select Name, CPU | Out-DataTable | Invoke-SQLBulkCopy -ServerInstance $SQLInstance -Database $Database -Table $SQLTable -force -verbose 30 | 31 | # Get a list of processes and their CPU, create a datatable, bulk import that data 32 | 33 | .NOTES 34 | Adapted from script by Marc van Orsouw and function from Chad Miller 35 | Version History 36 | v1.0 - Chad Miller - Initial Release 37 | v1.1 - Chad Miller - Fixed Issue with Properties 38 | v1.2 - Chad Miller - Added setting column datatype by property as suggested by emp0 39 | v1.3 - Chad Miller - Corrected issue with setting datatype on empty properties 40 | v1.4 - Chad Miller - Corrected issue with DBNull 41 | v1.5 - Chad Miller - Updated example 42 | v1.6 - Chad Miller - Added column datatype logic with default to string 43 | v1.7 - Chad Miller - Fixed issue with IsArray 44 | v1.8 - ramblingcookiemonster - Removed if($Value) logic. This would not catch empty strings, zero, $false and other non-null items 45 | - Added perhaps pointless error handling 46 | 47 | .LINK 48 | https://github.com/RamblingCookieMonster/PowerShell 49 | 50 | .LINK 51 | Invoke-SQLBulkCopy 52 | 53 | .LINK 54 | Invoke-Sqlcmd2 55 | 56 | .LINK 57 | New-SQLConnection 58 | 59 | .FUNCTIONALITY 60 | SQL 61 | #> 62 | [CmdletBinding()] 63 | [OutputType([System.Data.DataTable])] 64 | param( 65 | [Parameter( Position=0, 66 | Mandatory=$true, 67 | ValueFromPipeline = $true)] 68 | [PSObject[]]$InputObject, 69 | 70 | [string[]]$NonNullable = @() 71 | ) 72 | 73 | Begin 74 | { 75 | $dt = New-Object Data.datatable 76 | $First = $true 77 | 78 | function Get-ODTType 79 | { 80 | param($type) 81 | 82 | $types = @( 83 | 'System.Boolean', 84 | 'System.Byte[]', 85 | 'System.Byte', 86 | 'System.Char', 87 | 'System.Datetime', 88 | 'System.Decimal', 89 | 'System.Double', 90 | 'System.Guid', 91 | 'System.Int16', 92 | 'System.Int32', 93 | 'System.Int64', 94 | 'System.Single', 95 | 'System.UInt16', 96 | 'System.UInt32', 97 | 'System.UInt64') 98 | 99 | if ( $types -contains $type ) { 100 | Write-Output "$type" 101 | } 102 | else { 103 | Write-Output 'System.String' 104 | } 105 | } #Get-Type 106 | } 107 | Process 108 | { 109 | foreach ($Object in $InputObject) 110 | { 111 | $DR = $DT.NewRow() 112 | foreach ($Property in $Object.PsObject.Properties) 113 | { 114 | $Name = $Property.Name 115 | $Value = $Property.Value 116 | 117 | #RCM: what if the first property is not reflective of all the properties? Unlikely, but... 118 | if ($First) 119 | { 120 | $Col = New-Object Data.DataColumn 121 | $Col.ColumnName = $Name 122 | 123 | #If it's not DBNull or Null, get the type 124 | if ($Value -isnot [System.DBNull] -and $Value -ne $null) 125 | { 126 | $Col.DataType = [System.Type]::GetType( $(Get-ODTType $property.TypeNameOfValue) ) 127 | } 128 | 129 | #Set it to nonnullable if specified 130 | if ($NonNullable -contains $Name ) 131 | { 132 | $col.AllowDBNull = $false 133 | } 134 | 135 | try 136 | { 137 | $DT.Columns.Add($Col) 138 | } 139 | catch 140 | { 141 | Write-Error "Could not add column $($Col | Out-String) for property '$Name' with value '$Value' and type '$($Value.GetType().FullName)':`n$_" 142 | } 143 | } 144 | 145 | Try 146 | { 147 | #Handle arrays and nulls 148 | if ($property.GetType().IsArray) 149 | { 150 | $DR.Item($Name) = $Value | ConvertTo-XML -As String -NoTypeInformation -Depth 1 151 | } 152 | elseif($Value -eq $null) 153 | { 154 | $DR.Item($Name) = [DBNull]::Value 155 | } 156 | else 157 | { 158 | $DR.Item($Name) = $Value 159 | } 160 | } 161 | Catch 162 | { 163 | Write-Error "Could not add property '$Name' with value '$Value' and type '$($Value.GetType().FullName)'" 164 | continue 165 | } 166 | 167 | #Did we get a null or dbnull for a non-nullable item? let the user know. 168 | if($NonNullable -contains $Name -and ($Value -is [System.DBNull] -or $Value -eq $null)) 169 | { 170 | write-verbose "NonNullable property '$Name' with null value found: $($object | out-string)" 171 | } 172 | 173 | } 174 | 175 | Try 176 | { 177 | $DT.Rows.Add($DR) 178 | } 179 | Catch 180 | { 181 | Write-Error "Failed to add row '$($DR | Out-String)':`n$_" 182 | } 183 | 184 | $First = $false 185 | } 186 | } 187 | 188 | End 189 | { 190 | Write-Output @(,$dt) 191 | } 192 | 193 | } #Out-DataTable -------------------------------------------------------------------------------- /ui/resources/js/SessionHistory/SessionHistory-calendar.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | /*global formatMonthString, formatDateString, updateYearDisplay, setupYearNavigation, updateMonthGrid, availableDates, availableMonths, maxDate, minDate, MONTH_NAMES, loadGameCardsForDate, loadGameCardsForMonth, mainView:writable, calendarYear:writable, calendarMonth:writable, calendarDay:writable */ 3 | /*from calendar-controls.js, SessionHistory-shared.js, SessionHistory-cards.js */ 4 | 5 | // ===== MAIN VIEW SWITCHING ===== 6 | 7 | // Setup main view buttons 8 | function setupMainViewButtons() { 9 | document.querySelectorAll('.view-btn').forEach(btn => { 10 | btn.addEventListener('click', () => { 11 | const view = btn.dataset.view; 12 | switchMainView(view); 13 | }); 14 | }); 15 | } 16 | 17 | // Switch between main views (games, byday, bymonth) 18 | function switchMainView(view) { 19 | mainView = view; 20 | 21 | // Update button active states 22 | document.querySelectorAll('.view-btn').forEach(btn => { 23 | btn.classList.toggle('active', btn.dataset.view === view); 24 | }); 25 | 26 | // Show/hide left column content 27 | document.getElementById('games-view-content').style.display = 28 | view === 'games' ? 'flex' : 'none'; 29 | document.getElementById('calendar-view-content').style.display = 30 | view !== 'games' ? 'flex' : 'none'; 31 | 32 | // Show/hide right column content 33 | document.getElementById('games-view-right').style.display = 34 | view === 'games' ? 'flex' : 'none'; 35 | document.getElementById('cards-view-right').style.display = 36 | view !== 'games' ? 'flex' : 'none'; 37 | 38 | // Show/hide day grid based on view 39 | document.getElementById('day-grid-container').style.display = 40 | view === 'byday' ? 'block' : 'none'; 41 | 42 | if (view === 'byday') { 43 | // Initialize to most recent date with data 44 | initializeByDayView(); 45 | } else if (view === 'bymonth') { 46 | // Initialize to most recent month with data 47 | initializeByMonthView(); 48 | } 49 | } 50 | 51 | // Initialize By Day view with most recent date 52 | function initializeByDayView() { 53 | if (maxDate) { 54 | calendarYear = maxDate.getFullYear(); 55 | calendarMonth = maxDate.getMonth(); 56 | calendarDay = maxDate.getDate(); 57 | } 58 | 59 | refreshYearDisplay(); 60 | refreshMonthGrid(); 61 | updateDayGrid(); 62 | 63 | // Load game cards for the most recent date 64 | const dateStr = formatDateString(calendarYear, calendarMonth, calendarDay); 65 | loadGameCardsForDate(dateStr); 66 | } 67 | 68 | // Initialize By Month view with most recent month 69 | function initializeByMonthView() { 70 | if (maxDate) { 71 | calendarYear = maxDate.getFullYear(); 72 | calendarMonth = maxDate.getMonth(); 73 | } 74 | 75 | refreshYearDisplay(); 76 | refreshMonthGrid(); 77 | 78 | // Load game cards for the most recent month 79 | const monthStr = formatMonthString(calendarYear, calendarMonth); 80 | loadGameCardsForMonth(monthStr); 81 | } 82 | 83 | // ===== CALENDAR NAVIGATION ===== 84 | 85 | // Setup year navigation 86 | function initYearNavigation() { 87 | setupYearNavigation({ 88 | firstYear: minDate ? minDate.getFullYear() : 0, 89 | finalYear: maxDate ? maxDate.getFullYear() : 9999, 90 | getCalendarYear: () => calendarYear, 91 | setCalendarYear: (year) => { calendarYear = year; }, 92 | onYearChange: () => { 93 | refreshYearDisplay(); 94 | refreshMonthGrid(); 95 | if (mainView === 'byday') { 96 | updateDayGrid(); 97 | } 98 | } 99 | }); 100 | } 101 | 102 | // Update year display (wrapper) 103 | function refreshYearDisplay() { 104 | updateYearDisplay(calendarYear); 105 | } 106 | 107 | // Update month grid (wrapper) 108 | function refreshMonthGrid() { 109 | updateMonthGrid({ 110 | calendarYear: calendarYear, 111 | availableMonths: availableMonths, 112 | isMonthSelected: (monthIndex) => monthIndex === calendarMonth, 113 | onMonthClick: (monthIndex) => { 114 | calendarMonth = monthIndex; 115 | refreshMonthGrid(); 116 | 117 | if (mainView === 'byday') { 118 | // Find first day with data in this month 119 | const daysInMonth = new Date(calendarYear, calendarMonth + 1, 0).getDate(); 120 | for (let day = 1; day <= daysInMonth; day++) { 121 | const dateStr = formatDateString(calendarYear, calendarMonth, day); 122 | if (availableDates.has(dateStr)) { 123 | calendarDay = day; 124 | break; 125 | } 126 | } 127 | updateDayGrid(); 128 | const dateStr = formatDateString(calendarYear, calendarMonth, calendarDay); 129 | loadGameCardsForDate(dateStr); 130 | } else if (mainView === 'bymonth') { 131 | const monthStr = formatMonthString(calendarYear, calendarMonth); 132 | loadGameCardsForMonth(monthStr); 133 | } 134 | } 135 | }); 136 | } 137 | 138 | // Update day grid for selected month 139 | function updateDayGrid() { 140 | const dayGrid = document.getElementById('day-grid'); 141 | dayGrid.innerHTML = ''; 142 | 143 | // Update selected month display 144 | document.getElementById('selected-month-display').textContent = 145 | `${MONTH_NAMES[calendarMonth]} ${calendarYear}`; 146 | 147 | // Get first day of month and number of days 148 | const firstDay = new Date(calendarYear, calendarMonth, 1).getDay(); 149 | const daysInMonth = new Date(calendarYear, calendarMonth + 1, 0).getDate(); 150 | 151 | // Add empty cells for days before month starts 152 | for (let i = 0; i < firstDay; i++) { 153 | const emptyBtn = document.createElement('button'); 154 | emptyBtn.className = 'day-btn empty'; 155 | emptyBtn.disabled = true; 156 | dayGrid.appendChild(emptyBtn); 157 | } 158 | 159 | // Add day buttons 160 | for (let day = 1; day <= daysInMonth; day++) { 161 | const dateStr = formatDateString(calendarYear, calendarMonth, day); 162 | const hasData = availableDates.has(dateStr); 163 | const dayOfWeek = new Date(calendarYear, calendarMonth, day).getDay(); 164 | const isWeekend = dayOfWeek === 0 || dayOfWeek === 6; // Sunday or Saturday 165 | 166 | const dayBtn = document.createElement('button'); 167 | dayBtn.className = 'day-btn'; 168 | dayBtn.textContent = day; 169 | 170 | if (hasData) { 171 | dayBtn.classList.add('has-data'); 172 | } 173 | 174 | if (calendarDay === day) { 175 | dayBtn.classList.add('selected'); 176 | } 177 | 178 | if (isWeekend) { 179 | dayBtn.classList.add('weekend'); 180 | } 181 | 182 | dayBtn.disabled = !hasData; 183 | 184 | if (hasData) { 185 | dayBtn.addEventListener('click', () => { 186 | calendarDay = day; 187 | updateDayGrid(); 188 | loadGameCardsForDate(dateStr); 189 | }); 190 | } 191 | 192 | dayGrid.appendChild(dayBtn); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /modules/SetupDatabase.psm1: -------------------------------------------------------------------------------- 1 | function SetupDatabase() { 2 | try { 3 | $dbConnection = New-SQLiteConnection -DataSource ".\GamingGaiden.db" 4 | 5 | $createGamesTableQuery = "CREATE TABLE IF NOT EXISTS games ( 6 | name TEXT PRIMARY KEY NOT NULL, 7 | exe_name TEXT, 8 | icon BLOB, 9 | play_time INTEGER, 10 | last_play_date INTEGER, 11 | completed TEXT, 12 | platform TEXT)" 13 | 14 | Invoke-SqliteQuery -Query $createGamesTableQuery -SQLiteConnection $dbConnection 15 | 16 | $createPlatformsTableQuery = "CREATE TABLE IF NOT EXISTS emulated_platforms ( 17 | name TEXT PRIMARY KEY NOT NULL, 18 | exe_name TEXT, 19 | core TEXT, 20 | rom_extensions TEXT)" 21 | 22 | Invoke-SqliteQuery -Query $createPlatformsTableQuery -SQLiteConnection $dbConnection 23 | 24 | $createDailyPlaytimeTableQuery = "CREATE TABLE IF NOT EXISTS daily_playtime ( 25 | play_date TEXT PRIMARY KEY NOT NULL, 26 | play_time INTEGER)" 27 | 28 | Invoke-SqliteQuery -Query $createDailyPlaytimeTableQuery -SQLiteConnection $dbConnection 29 | 30 | $createPCTableQuery = "CREATE TABLE IF NOT EXISTS gaming_pcs ( 31 | name TEXT PRIMARY KEY NOT NULL, 32 | icon BLOB, 33 | cost TEXT, 34 | currency TEXT, 35 | start_date INTEGER, 36 | end_date INTEGER, 37 | current TEXT)" 38 | 39 | Invoke-SqliteQuery -Query $createPCTableQuery -SQLiteConnection $dbConnection 40 | 41 | $gamesTableSchema = Invoke-SqliteQuery -query "PRAGMA table_info('games')" -SQLiteConnection $dbConnection 42 | 43 | # Migration 1 44 | if (-Not $gamesTableSchema.name.Contains("idle_time")) { 45 | $addIdleTimeColumnInGamesTableQuery = "ALTER TABLE games ADD COLUMN idle_time INTEGER DEFAULT 0" 46 | Invoke-SqliteQuery -Query $addIdleTimeColumnInGamesTableQuery -SQLiteConnection $dbConnection 47 | } 48 | # End Migration 1 49 | 50 | # Migration 2 51 | if (-Not $gamesTableSchema.name.Contains("session_count")) { 52 | $addSessionCountColumnInGamesTableQuery = "ALTER TABLE games ADD COLUMN session_count INTEGER DEFAULT 0" 53 | Invoke-SqliteQuery -Query $addSessionCountColumnInGamesTableQuery -SQLiteConnection $dbConnection 54 | } 55 | # End Migration 2 56 | 57 | # Migration 3 58 | if (-Not $gamesTableSchema.name.Contains("rom_based_name")) { 59 | $addRomBasedNameColumnInGamesTableQuery = "ALTER TABLE games ADD COLUMN rom_based_name TEXT" 60 | $updateRomBasedNameColumnValues = "UPDATE games SET rom_based_name = name WHERE exe_name IN (SELECT DISTINCT exe_name FROM emulated_platforms)" 61 | 62 | Invoke-SqliteQuery -Query $addRomBasedNameColumnInGamesTableQuery -SQLiteConnection $dbConnection 63 | Invoke-SqliteQuery -Query $updateRomBasedNameColumnValues -SQLiteConnection $dbConnection 64 | } 65 | # End Migration 3 66 | 67 | # Migration 4 68 | if (-Not $gamesTableSchema.name.Contains("status")) { 69 | $addStatusColumnInGamesTableQuery = "ALTER TABLE games ADD COLUMN status TEXT" 70 | 71 | Invoke-SqliteQuery -Query $addStatusColumnInGamesTableQuery -SQLiteConnection $dbConnection 72 | } 73 | # End Migration 4 74 | 75 | # Migration 5 76 | $createSessionHistoryTableQuery = "CREATE TABLE IF NOT EXISTS session_history ( 77 | id INTEGER PRIMARY KEY AUTOINCREMENT, 78 | game_name TEXT NOT NULL, 79 | start_time INTEGER NOT NULL, 80 | duration INTEGER NOT NULL, 81 | FOREIGN KEY (game_name) REFERENCES games(name))" 82 | 83 | Invoke-SqliteQuery -Query $createSessionHistoryTableQuery -SQLiteConnection $dbConnection 84 | # End Migration 5 85 | 86 | # Migration 6 - Multi-PC tracking: Add gaming_pc_name, total_play_time and in_use columns 87 | if (-Not $gamesTableSchema.name.Contains("gaming_pc_name")) { 88 | $addGamingPCNameColumnInGamesTableQuery = "ALTER TABLE games ADD COLUMN gaming_pc_name TEXT" 89 | Invoke-SqliteQuery -Query $addGamingPCNameColumnInGamesTableQuery -SQLiteConnection $dbConnection 90 | } 91 | 92 | $gamingPCsTableSchema = Invoke-SqliteQuery -query "PRAGMA table_info('gaming_pcs')" -SQLiteConnection $dbConnection 93 | 94 | if ($gamingPCsTableSchema.name.Contains("current")) { 95 | # Create new table without current column 96 | $createNewPCTableQuery = "CREATE TABLE gaming_pcs_new ( 97 | name TEXT PRIMARY KEY NOT NULL, 98 | icon BLOB, 99 | cost TEXT, 100 | currency TEXT, 101 | start_date INTEGER, 102 | end_date INTEGER, 103 | in_use TEXT, 104 | total_play_time INTEGER DEFAULT 0)" 105 | 106 | Invoke-SqliteQuery -Query $createNewPCTableQuery -SQLiteConnection $dbConnection 107 | 108 | # Copy all data from old table to new table 109 | $copyDataQuery = "INSERT INTO gaming_pcs_new (name, icon, cost, currency, start_date, end_date, in_use) 110 | SELECT name, icon, cost, currency, start_date, end_date, current 111 | FROM gaming_pcs" 112 | 113 | Invoke-SqliteQuery -Query $copyDataQuery -SQLiteConnection $dbConnection 114 | 115 | # Drop old table 116 | Invoke-SqliteQuery -Query "DROP TABLE gaming_pcs" -SQLiteConnection $dbConnection 117 | 118 | # Rename new table to original name 119 | Invoke-SqliteQuery -Query "ALTER TABLE gaming_pcs_new RENAME TO gaming_pcs" -SQLiteConnection $dbConnection 120 | } 121 | # End Migration 6 122 | 123 | $dbConnection.Close() 124 | $dbConnection.Dispose() 125 | } 126 | catch { 127 | [System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms') | out-null 128 | [System.Windows.Forms.MessageBox]::Show("Exception: $($_.Exception.Message). Check log for details", 'Gaming Gaiden', "OK", "Error") 129 | 130 | $timestamp = Get-date -f s 131 | Write-Output "$timestamp : Error: A user or system error has caused an exception. Database setup could not be finished. Check log for details." >> ".\GamingGaiden.log" 132 | Write-Output "$timestamp : Exception: $($_.Exception.Message)" >> ".\GamingGaiden.log" 133 | exit 1; 134 | } 135 | } -------------------------------------------------------------------------------- /ui/templates/AllGames.html.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Gaming Gaiden 5 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 125 | 126 | 127 | 128 |
129 |
130 |

All Games

131 |
132 |
133 |
134 |
_GAMESTABLE_
135 |
136 | 137 |
138 |
139 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /ui/resources/js/SessionHistory/SessionHistory-shared.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | /*global Chart, Log2Axis, ChartDataLabels, chartTitleConfig, getChartTextColor, getChartGridColor, initYearNavigation, sortGamesList, selectGame, updateGridAlignment, setupSearch, setupSorting, updateSortButtons, applySortAndRender, setupViewToggle, setupDateNavigation, setupMainViewButtons */ 3 | /* from common.js, SessionHistory-games.js, SessionHistory-calendar.js, SessionHistory-cards.js */ 4 | 5 | 6 | // ===== GLOBAL STATE VARIABLES ===== 7 | 8 | // Parse data from hidden tables 9 | let allSessions = []; 10 | let gamesList = []; 11 | let currentChart = null; 12 | let selectedGame = null; 13 | let currentView = 'alltime'; // 'alltime' or 'specificdate' 14 | let selectedDay = null; 15 | let selectedDayIndex = 0; 16 | let sessionsByDay = []; 17 | let currentSortField = 'lastPlayed'; // 'name' or 'lastPlayed' 18 | let currentSortDirection = 'desc'; // 'asc' or 'desc' 19 | 20 | // New state variables for view switching and calendar 21 | let mainView = 'games'; // 'games', 'byday', or 'bymonth' 22 | let calendarYear = new Date().getFullYear(); 23 | let calendarMonth = new Date().getMonth(); 24 | let calendarDay = null; 25 | let availableDates = new Set(); // Set of 'YYYY-MM-DD' strings 26 | let availableMonths = new Set(); // Set of 'YYYY-MM' strings 27 | let minDate = null; 28 | let maxDate = null; 29 | 30 | // Shared constants 31 | const MONTH_NAMES = ['January', 'February', 'March', 'April', 'May', 'June', 32 | 'July', 'August', 'September', 'October', 'November', 'December']; 33 | 34 | // Register custom log2 scale for logarithmic Y-axis 35 | Log2Axis.id = "log2"; 36 | Log2Axis.defaults = {}; 37 | Chart.register(Log2Axis); 38 | 39 | // ===== DATA PARSING FUNCTIONS ===== 40 | 41 | // Parse sessions data 42 | function parseSessionsData() { 43 | const table = document.getElementById("sessions-data").querySelector("table"); 44 | const rows = table.querySelectorAll("tbody tr"); 45 | 46 | allSessions = Array.from(rows).map((row) => { 47 | return { 48 | id: row.cells[0].textContent, 49 | game_name: row.cells[1].textContent, 50 | platform: row.cells[2].textContent, 51 | session_date: row.cells[3].textContent, 52 | start_time: parseInt(row.cells[4].textContent), 53 | duration: parseFloat(row.cells[5].textContent) 54 | }; 55 | }); 56 | 57 | // Remove header row 58 | allSessions.shift(); 59 | } 60 | 61 | // Parse games list data 62 | function parseGamesData() { 63 | const table = document.getElementById("games-data").querySelector("table"); 64 | const rows = table.querySelectorAll("tbody tr"); 65 | 66 | gamesList = Array.from(rows).map((row) => { 67 | return { 68 | game_name: row.cells[0].textContent, 69 | platform: row.cells[1].textContent, 70 | icon: row.cells[2].textContent, 71 | session_count: parseInt(row.cells[3].textContent), 72 | total_duration: parseFloat(row.cells[4].textContent) 73 | }; 74 | }); 75 | 76 | // Remove header row 77 | gamesList.shift(); 78 | } 79 | 80 | // ===== DATE AVAILABILITY FUNCTIONS ===== 81 | 82 | // Build available dates/months set from session data 83 | function buildAvailableDates() { 84 | availableDates.clear(); 85 | availableMonths.clear(); 86 | 87 | let minTimestamp = Infinity; 88 | let maxTimestamp = -Infinity; 89 | 90 | allSessions.forEach((session) => { 91 | const date = new Date(session.start_time * 1000); 92 | const year = date.getFullYear(); 93 | const month = String(date.getMonth() + 1).padStart(2, '0'); 94 | const day = String(date.getDate()).padStart(2, '0'); 95 | const dateStr = `${year}-${month}-${day}`; // YYYY-MM-DD 96 | const monthStr = `${year}-${month}`; // YYYY-MM 97 | 98 | availableDates.add(dateStr); 99 | availableMonths.add(monthStr); 100 | 101 | if (session.start_time < minTimestamp) minTimestamp = session.start_time; 102 | if (session.start_time > maxTimestamp) maxTimestamp = session.start_time; 103 | }); 104 | 105 | if (minTimestamp !== Infinity) { 106 | minDate = new Date(minTimestamp * 1000); 107 | maxDate = new Date(maxTimestamp * 1000); 108 | } 109 | } 110 | 111 | // ===== SHARED HELPER FUNCTIONS ===== 112 | 113 | // Filter sessions by date string (YYYY-MM-DD) or month string (YYYY-MM) 114 | function filterSessionsByDateStr(dateStr, isMonth = false) { 115 | return allSessions.filter(session => { 116 | const sessionDate = new Date(session.start_time * 1000); 117 | const year = sessionDate.getFullYear(); 118 | const month = String(sessionDate.getMonth() + 1).padStart(2, '0'); 119 | const day = String(sessionDate.getDate()).padStart(2, '0'); 120 | const sessionStr = isMonth 121 | ? `${year}-${month}` 122 | : `${year}-${month}-${day}`; 123 | return sessionStr === dateStr; 124 | }); 125 | } 126 | 127 | // Group sessions by game and aggregate stats 128 | function aggregateGamesBySessions(sessions) { 129 | const gameMap = {}; 130 | 131 | sessions.forEach(session => { 132 | if (!gameMap[session.game_name]) { 133 | const gameInfo = gamesList.find(g => g.game_name === session.game_name); 134 | gameMap[session.game_name] = { 135 | game_name: session.game_name, 136 | platform: session.platform, 137 | icon: gameInfo ? gameInfo.icon : '', 138 | sessions: [], 139 | sessionCount: 0, 140 | totalDuration: 0, 141 | lastPlayed: 0 142 | }; 143 | } 144 | 145 | gameMap[session.game_name].sessions.push(session); 146 | gameMap[session.game_name].sessionCount++; 147 | gameMap[session.game_name].totalDuration += session.duration; 148 | gameMap[session.game_name].lastPlayed = Math.max( 149 | gameMap[session.game_name].lastPlayed, 150 | session.start_time 151 | ); 152 | }); 153 | 154 | // Convert to array and sort by last played 155 | return Object.values(gameMap).sort((a, b) => b.lastPlayed - a.lastPlayed); 156 | } 157 | 158 | // Update total games and hours stats in UI 159 | function updateStatsDisplay(games) { 160 | const totalGames = games.length; 161 | const totalMinutes = games.reduce((sum, game) => sum + game.totalDuration, 0); 162 | const totalHours = (totalMinutes / 60).toFixed(1); 163 | 164 | document.getElementById('total-games-count').textContent = 165 | `${totalGames} game${totalGames !== 1 ? 's' : ''}`; 166 | document.getElementById('total-time-played').textContent = 167 | `${totalHours}h total`; 168 | } 169 | 170 | // ===== INITIALIZATION ===== 171 | 172 | // Initialize on page load (after jQuery table processing) 173 | $(document).ready(function() { 174 | parseSessionsData(); 175 | parseGamesData(); 176 | buildAvailableDates(); 177 | setupSearch(); 178 | setupSorting(); 179 | updateSortButtons(); 180 | applySortAndRender(); // Initial render with default sort (last played, desc) 181 | setupViewToggle(); 182 | setupDateNavigation(); 183 | setupMainViewButtons(); 184 | initYearNavigation(); 185 | 186 | // Auto-select first game if available 187 | if (gamesList.length > 0) { 188 | // Select first game from sorted list 189 | const sorted = sortGamesList(gamesList, currentSortField, currentSortDirection); 190 | selectGame(sorted[0].game_name); 191 | } 192 | 193 | // Update grid alignment on window resize 194 | window.addEventListener('resize', updateGridAlignment); 195 | }); 196 | -------------------------------------------------------------------------------- /modules/QueryFunctions.psm1: -------------------------------------------------------------------------------- 1 | function IsExeEmulator($DetectedExe) { 2 | Log "Is $DetectedExe an Emulator?" 3 | 4 | $pattern = SQLEscapedMatchPattern $DetectedExe.Trim() 5 | $findExeQuery = "SELECT COUNT(*) as '' FROM emulated_platforms WHERE exe_name LIKE '%{0}%'" -f $pattern 6 | 7 | $exesFound = (RunDBQuery $findExeQuery).Column1 8 | 9 | Log ("Check result: {0}" -f ($exesFound -gt 0)) 10 | return ($exesFound -gt 0) 11 | } 12 | 13 | function DoesEntityExists($Table, $Column, $EntityName) { 14 | Log "Does $EntityName exists in $Table ?" 15 | 16 | $entityNamePattern = SQLEscapedMatchPattern($EntityName.Trim()) 17 | $validateEntityQuery = "SELECT * FROM {0} WHERE {1} LIKE '{2}'" -f $Table, $Column, $entityNamePattern 18 | 19 | $entityFound = RunDBQuery $validateEntityQuery 20 | 21 | Log "Discovered entity: $entityFound" 22 | return $entityFound 23 | } 24 | 25 | function CheckExeCoreCombo($ExeList, $Core) { 26 | Log "Is $ExeList already registered with $Core?" 27 | 28 | $exeListPattern = SQLEscapedMatchPattern($ExeList.Trim()) 29 | $coreNamePattern = SQLEscapedMatchPattern($Core.Trim()) 30 | $validateEntityQuery = "SELECT * FROM emulated_platforms WHERE exe_name LIKE '%{0}%' AND core LIKE '{1}'" -f $exeListPattern, $coreNamePattern 31 | 32 | $entityFound = RunDBQuery $validateEntityQuery 33 | 34 | Log "Detected exe core Combo: $entityFound" 35 | return $entityFound 36 | } 37 | 38 | function GetPlayTime($GameName) { 39 | Log "Get existing gameplay time for $GameName" 40 | 41 | $gameNamePattern = SQLEscapedMatchPattern($GameName.Trim()) 42 | $getGamePlayTimeQuery = "SELECT play_time FROM games WHERE name LIKE '{0}'" -f $gameNamePattern 43 | 44 | $recordedGamePlayTime = (RunDBQuery $getGamePlayTimeQuery).play_time 45 | 46 | Log "Detected gameplay time: $recordedGamePlayTime min" 47 | return $recordedGamePlayTime 48 | } 49 | 50 | function GetIdleTime($GameName) { 51 | Log "Get existing game idle time for $GameName" 52 | 53 | $gameNamePattern = SQLEscapedMatchPattern($GameName.Trim()) 54 | $getGameIdleTimeQuery = "SELECT idle_time FROM games WHERE name LIKE '{0}'" -f $gameNamePattern 55 | 56 | $recordedGameIdleTime = (RunDBQuery $getGameIdleTimeQuery).idle_time 57 | 58 | Log "Detected game idle time: $recordedGameIdleTime min" 59 | return $recordedGameIdleTime 60 | } 61 | 62 | function FindEmulatedGame($DetectedEmulatorExe, $EmulatorCommandLine) { 63 | Log "Finding emulated game for $DetectedEmulatorExe" 64 | 65 | $pattern = SQLEscapedMatchPattern $DetectedEmulatorExe.Trim() 66 | $getRomExtensionsQuery = "SELECT rom_extensions FROM emulated_platforms WHERE exe_name LIKE '%{0}%'" -f $pattern 67 | $romExtensions = (RunDBQuery $getRomExtensionsQuery).rom_extensions.Split(',') 68 | 69 | $romName = $null 70 | foreach ($romExtension in $romExtensions) { 71 | $romName = [System.Text.RegularExpressions.Regex]::Match($EmulatorCommandLine, "[^`"\\]*\.$romExtension").Value 72 | 73 | if ($romName -ne "") { 74 | $romName = $romName -replace ".$romExtension", "" 75 | break 76 | } 77 | } 78 | 79 | $romBasedGameName = [regex]::Replace($romName, '\([^)]*\)|\[[^\]]*\]', "") 80 | 81 | Log ("Detected game: {0}" -f $romBasedGameName.Trim()) 82 | return $romBasedGameName.Trim() 83 | } 84 | 85 | function FindEmulatedGameCore($DetectedEmulatorExe, $EmulatorCommandLine) { 86 | Log "Finding core in use by $DetectedEmulatorExe" 87 | 88 | $coreName = $null 89 | 90 | $pattern = SQLEscapedMatchPattern $DetectedEmulatorExe.Trim() 91 | $getCoresQuery = "SELECT core FROM emulated_platforms WHERE exe_name LIKE '%{0}%'" -f $pattern 92 | $cores = (RunDBQuery $getCoresQuery).core 93 | if ( $cores.Length -le 1) { 94 | $coreName = $cores[0] 95 | } 96 | else { 97 | foreach ($core in $cores) { 98 | if ($EmulatorCommandLine.Contains($core)) { 99 | $coreName = $core 100 | } 101 | } 102 | } 103 | 104 | Log "Detected core: $coreName" 105 | return $coreName 106 | } 107 | 108 | function FindEmulatedGamePlatform($DetectedEmulatorExe, $Core) { 109 | $getPlatformQuery = $null 110 | 111 | $exePattern = SQLEscapedMatchPattern $DetectedEmulatorExe.Trim() 112 | if ($Core.Length -eq 0 ) { 113 | Log "Finding platform for $DetectedEmulatorExe" 114 | $getPlatformQuery = "SELECT name FROM emulated_platforms WHERE exe_name LIKE '%{0}%' AND core LIKE ''" -f $exePattern 115 | } 116 | else { 117 | Log "Finding platform for $DetectedEmulatorExe and core $Core" 118 | $corePattern = SQLEscapedMatchPattern $Core.Trim() 119 | $getPlatformQuery = "SELECT name FROM emulated_platforms WHERE exe_name LIKE '%{0}%' AND core LIKE '{1}'" -f $exePattern, $corePattern 120 | } 121 | 122 | $emulatedGamePlatform = (RunDBQuery $getPlatformQuery).name 123 | 124 | Log "Detected platform : $emulatedGamePlatform" 125 | return $emulatedGamePlatform 126 | } 127 | 128 | function FindEmulatedGameDetails($DetectedEmulatorExe) { 129 | Log "Finding emulated game details for $DetectedEmulatorExe" 130 | 131 | $emulatorCommandLine = Get-CimInstance -ClassName Win32_Process -Filter "name = '$DetectedEmulatorExe.exe'" | Select-Object -ExpandProperty CommandLine 132 | 133 | $emulatedGameRomBasedName = FindEmulatedGame $DetectedEmulatorExe $emulatorCommandLine 134 | if ($emulatedGameRomBasedName.Length -eq 0) { 135 | Log "Error: Detected emulated game name of 0 char length. Returning" 136 | return $false 137 | } 138 | 139 | $coreName = $null 140 | if ($DetectedEmulatorExe.ToLower() -like "*retroarch*") { 141 | Log "Retroarch detected. Detecting core next" 142 | $coreName = FindEmulatedGameCore $DetectedEmulatorExe $emulatorCommandLine 143 | 144 | if ($null -eq $coreName) { 145 | Log "Error: No core detected. Most likely platform is not registered. Please register platform." 146 | return $false 147 | } 148 | } 149 | 150 | $emulatedGamePlatform = FindEmulatedGamePlatform $DetectedEmulatorExe $coreName 151 | 152 | if ($emulatedGamePlatform -is [system.array]) { 153 | Log "Error: Multiple platforms detected. Returning." 154 | return $false 155 | } 156 | 157 | Log "Found emulated game details. Rom Based Name: $emulatedGameRomBasedName, Exe: $DetectedEmulatorExe, Platform: $emulatedGamePlatform" 158 | return New-Object PSObject -Property @{ RomBasedName = $emulatedGameRomBasedName; Exe = $DetectedEmulatorExe ; Platform = $emulatedGamePlatform } 159 | } 160 | 161 | function GetGameDetails($Game) { 162 | Log "Finding Details of $Game" 163 | 164 | $pattern = SQLEscapedMatchPattern $Game.Trim() 165 | $getGameDetailsQuery = "SELECT * FROM games WHERE name LIKE '{0}'" -f $pattern 166 | 167 | $gameDetails = RunDBQuery $getGameDetailsQuery 168 | 169 | Log ("Found details: name: {0}, exe_name: {1}, platform: {2}, play_time: {3}" -f $gameDetails.name, $gameDetails.exe_name, $gameDetails.platform, $gameDetails.play_time) 170 | return $gameDetails 171 | } 172 | 173 | function GetPCDetails($PC) { 174 | Log "Finding Details of $PC" 175 | 176 | $pattern = SQLEscapedMatchPattern $PC.Trim() 177 | $getPCDetailsQuery = "SELECT * FROM gaming_pcs WHERE name LIKE '{0}'" -f $pattern 178 | 179 | $PCDetails = RunDBQuery $getPCDetailsQuery 180 | 181 | Log ("Found details: name: {0}, cost: {1}, start_date: {2}, end_date: {3}, in_use: {4}" -f $PCDetails.name, $PCDetails.cost, $PCDetails.start_date, $PCDetails.end_date, $PCDetails.in_use) 182 | return $PCDetails 183 | } 184 | 185 | function GetPlatformDetails($Platform) { 186 | Log "Finding Details of $Platform" 187 | 188 | $pattern = SQLEscapedMatchPattern $Platform.Trim() 189 | $getPlatformDetailsQuery = "SELECT * FROM emulated_platforms WHERE name LIKE '{0}'" -f $pattern 190 | 191 | $platformDetails = RunDBQuery $getPlatformDetailsQuery 192 | 193 | Log ("Found details: name: {0}, exe_name: {1}, core: {2}" -f $platformDetails.name, $platformDetails.exe_name, $platformDetails.core) 194 | return $platformDetails 195 | } -------------------------------------------------------------------------------- /modules/ProcessFunctions.psm1: -------------------------------------------------------------------------------- 1 | function DetectGame() { 2 | Log "Starting game detection" 3 | 4 | # Fetch games in order of most recent to least recent 5 | $getGameExesQuery = "SELECT exe_name FROM games ORDER BY last_play_date DESC" 6 | $getEmulatorExesQuery = "SELECT exe_name FROM emulated_platforms" 7 | 8 | $gameExeList = @((RunDBQuery $getGameExesQuery).exe_name) 9 | $rawEmulatorExes = @((RunDBQuery $getEmulatorExesQuery).exe_name) 10 | 11 | # Flatten the returned result rows containing multiple emulator exes into list with one exe per item 12 | $emulatorExeList = ($rawEmulatorExes -join ',') -split ',' 13 | 14 | $exeList = [string[]] (($gameExeList + $emulatorExeList) | Select-Object -Unique) 15 | 16 | # PERFORMANCE OPTIMIZATION: CPU & MEMORY 17 | # Process games in batches of 35 with most recent 10 games processed every batch. 5 sec wait b/w every batch. 18 | # Processes 300 games in 60 sec. Most recent 10 games guaranteed to be detected in 5 sec, accounting for 99% of UX in typical usage. 19 | # Uses ~ 3% cpu in active blips of less than 1s, every 5s. 20 | # Benchmarked on a 2019 Ryzen 3550H in low power mode (1.7 GHz Clk with boost disabled), Windows 10 21H2. 21 | # No new objects are created inside infinite loops to prevent objects explosion, keeps Memory usage ~ 50 MB or less. 22 | if ($exeList.length -le 35) { 23 | # If exeList is of size 35 or less. process whole list in every batch 24 | while ($true) { 25 | foreach ($exe in $exeList) { 26 | if ($null = [System.Diagnostics.Process]::GetProcessesByName($exe)) { 27 | Log "Found $exe running. Exiting detection" 28 | return $exe 29 | } 30 | } 31 | Start-Sleep -s 5 32 | } 33 | } 34 | else { 35 | # If exeList is longer than 35. 36 | $startIndex = 10; $batchSize = 25 37 | while ($true) { 38 | # Process most recent 10 games in every batch. 39 | for ($i = 0; $i -lt 10; $i++) { 40 | if ($null = [System.Diagnostics.Process]::GetProcessesByName($exeList[$i])) { 41 | Log "Found $($exeList[$i]) running. Exiting detection" 42 | return $exeList[$i] 43 | } 44 | } 45 | # Rest of the games in incrementing way. 25 in each batch. 46 | $endIndex = [Math]::Min($startIndex + $batchSize, $exeList.length) 47 | 48 | for ($i = $startIndex; $i -lt $endIndex; $i++) { 49 | if ($null = [System.Diagnostics.Process]::GetProcessesByName($exeList[$i])) { 50 | Log "Found $($exeList[$i]) running. Exiting detection" 51 | return $exeList[$i] 52 | } 53 | } 54 | 55 | if ($startIndex + $batchSize -lt $exeList.length) { 56 | $startIndex = $startIndex + $batchSize 57 | } 58 | else { 59 | $startIndex = 10 60 | } 61 | 62 | Start-Sleep -s 5 63 | } 64 | } 65 | } 66 | 67 | function TimeTrackerLoop($DetectedExe) { 68 | $hwInfoSensorSession = 'HKCU:\SOFTWARE\HWiNFO64\Sensors\Custom\Gaming Gaiden\Other1' 69 | $playTimeForCurrentSession = 0 70 | $idleSessionsCount = 0 71 | $idleSessions = New-Object int[] 100; 72 | $exeStartTime = ($null = [System.Diagnostics.Process]::GetProcessesByName($DetectedExe)).StartTime | Sort-Object | Select-Object -First 1 73 | 74 | while ($null = [System.Diagnostics.Process]::GetProcessesByName($DetectedExe)) { 75 | $playTimeForCurrentSession = [int16] (New-TimeSpan -Start $exeStartTime).TotalMinutes 76 | $idleTime = [int16] ([PInvoke.Win32.UserInput]::IdleTime).Minutes 77 | 78 | if ($idleTime -ge 10) { 79 | # Entered idle Session 80 | while ($idleTime -ge 5) { 81 | # Track idle Time for current Idle Session 82 | $idleSessions[$idleSessionsCount] = $idleTime 83 | $idleTime = [int16] ([PInvoke.Win32.UserInput]::IdleTime).Minutes 84 | 85 | # Keep the hwinfo sensor updated to current play time session length while tracking idle session 86 | $playTimeForCurrentSession = [int16] (New-TimeSpan -Start $exeStartTime).TotalMinutes 87 | Set-Itemproperty -path $hwInfoSensorSession -Name 'Value' -value $playTimeForCurrentSession 88 | 89 | Start-Sleep -s 5 90 | } 91 | # Exited Idle Session, increment idle session counter for storing next idle sessions's length 92 | $idleSessionsCount++ 93 | } 94 | 95 | Set-Itemproperty -path $hwInfoSensorSession -Name 'Value' -value $playTimeForCurrentSession 96 | Start-Sleep -s 5 97 | } 98 | 99 | $idleTimeForCurrentSession = ($idleSessions | Measure-Object -Sum).Sum 100 | Log "Play time for current session: $playTimeForCurrentSession min. Idle time for current session: $idleTimeForCurrentSession min." 101 | 102 | $PlayTimeExcludingIdleTime = $playTimeForCurrentSession - $idleTimeForCurrentSession 103 | Log "Play time for current session excluding Idle time $PlayTimeExcludingIdleTime min" 104 | 105 | return @($PlayTimeExcludingIdleTime, $idleTimeForCurrentSession) 106 | } 107 | 108 | function MonitorGame($DetectedExe) { 109 | Log "Starting monitoring for $DetectedExe" 110 | 111 | $databaseFileHashBefore = CalculateFileHash '.\GamingGaiden.db' 112 | Log "Database hash before: $databaseFileHashBefore" 113 | 114 | $emulatedGameDetails = $null 115 | $gameName = $null 116 | $romBasedName = $null 117 | $entityFound = $null 118 | $updatedPlayTime = 0 119 | $updatedLastPlayDate = (Get-Date ([datetime]::UtcNow) -UFormat %s).Split('.').Get(0) 120 | 121 | # Capture process start time for session history 122 | $processStartTime = ($null = [System.Diagnostics.Process]::GetProcessesByName($DetectedExe)).StartTime | Sort-Object | Select-Object -First 1 123 | $sessionStartTimeUnix = [int](Get-Date ($processStartTime.ToUniversalTime()) -UFormat %s) 124 | 125 | if (IsExeEmulator $DetectedExe) { 126 | $emulatedGameDetails = FindEmulatedGameDetails $DetectedExe 127 | if ($emulatedGameDetails -eq $false) { 128 | Log "Error: Problem in fetching emulated game details. See earlier logs for more info" 129 | Log "Error: Cannot resume detection until $DetectedExe exits. No playtime will be recorded." 130 | 131 | TimeTrackerLoop $DetectedExe 132 | return 133 | } 134 | 135 | $romBasedName = $emulatedGameDetails.RomBasedName 136 | $entityFound = DoesEntityExists "games" "rom_based_name" $romBasedName 137 | } 138 | else { 139 | $entityFound = DoesEntityExists "games" "exe_name" $DetectedExe 140 | } 141 | 142 | if ($null -ne $entityFound) { 143 | $gameName = $entityFound.name 144 | } 145 | else { 146 | $gameName = $romBasedName 147 | } 148 | 149 | # Create Temp file to signal parent process to update notification icon color to show game is running 150 | Write-Output "$gameName" > "$env:TEMP\GmGdn-TrackingGame.txt" 151 | $sessionTimeDetails = TimeTrackerLoop $DetectedExe 152 | $currentPlayTime = $sessionTimeDetails[0] 153 | $currentIdleTime = $sessionTimeDetails[1] 154 | # Remove Temp file to signal parent process to update notification icon color to show game has finished 155 | Remove-Item "$env:TEMP\GmGdn-TrackingGame.txt" 156 | 157 | if ($null -ne $entityFound) { 158 | Log "Game Already Exists. Updating PlayTime and Last Played Date" 159 | $recordedGamePlayTime = GetPlayTime $gameName 160 | $recordedGameIdleTime = GetIdleTime $gameName 161 | $updatedPlayTime = $recordedGamePlayTime + $currentPlayTime 162 | $updatedIdleTime = $recordedGameIdleTime + $currentIdleTime 163 | 164 | # Get current PC and append if needed 165 | $currentPC = Read-Setting "current_pc" 166 | $updatedPCList = "" 167 | if ($null -ne $currentPC) { 168 | $gameNamePattern = SQLEscapedMatchPattern($gameName.Trim()) 169 | $getGamingPCQuery = "SELECT gaming_pc_name FROM games WHERE name LIKE '{0}'" -f $gameNamePattern 170 | $existingPCs = (RunDBQuery $getGamingPCQuery).gaming_pc_name 171 | 172 | if ([string]::IsNullOrEmpty($existingPCs)) { 173 | $updatedPCList = $currentPC 174 | } elseif ($existingPCs -notlike "*$currentPC*") { 175 | $updatedPCList = $existingPCs + "," + $currentPC 176 | } 177 | } 178 | 179 | UpdateGameOnSession -GameName $gameName -GamePlayTime $updatedPlayTime -GameIdleTime $updatedIdleTime -GameLastPlayDate $updatedLastPlayDate -GameGamingPCName $updatedPCList 180 | } 181 | else { 182 | Log "Detected emulated game is new and doesn't exist already. Adding to database." 183 | 184 | $currentPC = Read-Setting "current_pc" 185 | $pcNameForGame = if ($null -ne $currentPC) { $currentPC } else { "" } 186 | 187 | SaveGame -GameName $gameName -GameExeName $DetectedExe -GameIconPath "./icons/default.png" ` 188 | -GamePlayTime $currentPlayTime -GameIdleTime $currentIdleTime -GameLastPlayDate $updatedLastPlayDate -GameCompleteStatus 'FALSE' -GamePlatform $emulatedGameDetails.Platform -GameSessionCount 1 -GameRomBasedName $gameName -GameGamingPCName $pcNameForGame 189 | } 190 | 191 | RecordPlaytimOnDate($currentPlayTime) 192 | 193 | # Record individual session history 194 | RecordSessionHistory -GameName $gameName -StartTime $sessionStartTimeUnix -Duration $currentPlayTime 195 | 196 | # Update current PC playtime 197 | $currentPC = Read-Setting "current_pc" 198 | if ($null -ne $currentPC) { 199 | UpdatePCPlaytime -PCName $currentPC -DurationMinutes $currentPlayTime 200 | } 201 | 202 | $databaseFileHashAfter = CalculateFileHash '.\GamingGaiden.db' 203 | Log "Database hash after: $databaseFileHashAfter" 204 | 205 | if ($databaseFileHashAfter -ne $databaseFileHashBefore) { 206 | BackupDatabase 207 | } 208 | } -------------------------------------------------------------------------------- /ui/templates/SessionHistory.html.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Gaming Gaiden - Session History 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 29 | 30 | 31 |

Session History

32 | 33 | 34 |
35 | 36 | 37 |
38 | 39 |
40 | 41 | 42 | 43 |
44 | 45 | 46 |
47 |
48 | 54 |
55 | 56 |
57 | 60 | 63 |
64 | 65 |
66 |
    67 | 68 |
69 |
70 |
71 | 72 | 73 | 116 |
117 | 118 | 119 |
120 | 121 |
122 |
123 |

Select a game to view sessions

124 |
125 |
126 | Platform: 127 | 128 |
129 |
130 | Sessions: 131 | 132 |
133 |
134 | Total Time: 135 | 136 |
137 |
138 | Last Played: 139 | 140 |
141 |
142 |
143 | 144 |
145 | 146 | 147 | 152 |
153 | 154 |
155 |

Select a game from the list to view its session history

156 |
157 |
158 | 159 | 160 | 175 |
176 | 177 |
178 | 179 | 180 |
181 |
_SESSIONTABLE_
182 |
_GAMESTABLE_
183 |
184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 204 | 205 | 206 | --------------------------------------------------------------------------------