├── 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 = '
' + summaryText + ' ';
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 |
26 | Life Time Summary
27 | Time Spent Gaming
28 | Most Played
29 | All Games
30 | ?
31 | Idle Time
32 | Session History
33 | Games Per Platform
34 | PC vs Emulation
35 |
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 |
37 | _PCVSEMULATIONTABLE_
38 |
39 |
40 |
41 | Life Time Summary
42 | Time Spent Gaming
43 | Most Played
44 | All Games
45 | ?
46 | Idle Time
47 | Session History
48 | Games Per Platform
49 | PC vs Emulation
50 |
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 |
37 | _GAMESPERPLATFORMTABLE_
38 |
39 |
40 |
41 | Life Time Summary
42 | Time Spent Gaming
43 | Most Played
44 | All Games
45 | ?
46 | Idle Time
47 | Session History
48 | Games Per Platform
49 | PC vs Emulation
50 |
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 |
47 | Life Time Summary
48 | Time Spent Gaming
49 | Most Played
50 | All Games
51 | ?
52 | Idle Time
53 | Session History
54 | Games Per Platform
55 | PC vs Emulation
56 |
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 |
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 |
78 | Life Time Summary
79 | Time Spent Gaming
80 | Most Played
81 | All Games
82 | ?
83 | Idle Time
84 | Session History
85 | Games Per Platform
86 | PC vs Emulation
87 |
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 |
42 |
43 | Jan
44 | Feb
45 | Mar
46 | Apr
47 | May
48 | Jun
49 | Jul
50 | Aug
51 | Sep
52 | Oct
53 | Nov
54 | Dec
55 |
56 |
57 |
58 |
64 |
65 | _DAILYPLAYTIMETABLE_
66 |
67 |
68 |
69 |
70 | Life Time Summary
71 | Time Spent Gaming
72 | Most Played
73 | All Games
74 | ?
75 | Idle Time
76 | Session History
77 | Games Per Platform
78 | PC vs Emulation
79 |
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 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
Your Gaming PCs
54 |
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 | _SUMMARYTABLE_
89 | _PCTABLE_
90 | _ANNUALGAMINGHOURSTABLE_
91 |
92 |
93 |
94 | Life Time Summary
95 | Time Spent Gaming
96 | Most Played
97 | All Games
98 | ?
99 | Idle Time
100 | Session History
101 | Games Per Platform
102 | PC vs Emulation
103 |
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 |
122 | Life Time Summary
123 | Time Spent Gaming
124 | Most Played
125 | All Games
126 | Idle Time
127 | Session History
128 | Games Per Platform
129 | PC vs Emulation
130 |
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 | [](https://github.com/kulvind3r/gaminggaiden/stargazers)
4 | [](https://github.com/kulvind3r/GamingGaiden/releases/latest)
5 | 
6 |
7 | [](https://app.codacy.com/gh/kulvind3r/GamingGaiden/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
8 | [](https://github.com/kulvind3r/gaminggaiden/graphs/commit-activity)
9 | [](https://github.com/kulvind3r/gaminggaiden/issues)
10 |
11 | 
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 |
133 |
134 |
_GAMESTABLE_
135 |
136 |
137 |
138 |
139 |
140 | Life Time Summary
141 | Time Spent Gaming
142 | Most Played
143 | All Games
144 | ?
145 | Idle Time
146 | Session History
147 | Games Per Platform
148 | PC vs Emulation
149 |
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 | Games
41 | By Day
42 | By Month
43 |
44 |
45 |
46 |
47 |
48 |
54 |
55 |
56 |
57 |
58 | Name ▼
59 |
60 |
61 | Last Played ▼
62 |
63 |
64 |
65 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
81 |
82 |
83 |
84 | Jan
85 | Feb
86 | Mar
87 | Apr
88 | May
89 | Jun
90 | Jul
91 | Aug
92 | Sep
93 | Oct
94 | Nov
95 | Dec
96 |
97 |
98 |
99 |
100 |
101 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
143 |
144 |
145 |
146 |
Specific Date
147 |
148 |
⮜
149 |
150 |
⮞
151 |
152 |
153 |
154 |
155 |
Select a game from the list to view its session history
156 |
157 |
158 |
159 |
160 |
161 |
168 |
169 |
170 |
171 |
172 |
No games played on this date
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
_SESSIONTABLE_
182 |
_GAMESTABLE_
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 | Life Time Summary
195 | Time Spent Gaming
196 | Most Played
197 | All Games
198 | ?
199 | Idle Time
200 | Session History
201 | Games Per Platform
202 | PC vs Emulation
203 |
204 |
205 |
206 |
--------------------------------------------------------------------------------