aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorzlg <zlg@zlg.space>2025-07-29 19:11:50 -0700
committerzlg <zlg@zlg.space>2025-07-29 20:37:57 -0700
commit9b0bda1a13ebac7866099c325b7ab103520ea40b (patch)
tree14697d3852037052a64b079c4a5331c513195644 /src
parentREADME.md: Better clarify dates, add contact details (diff)
downloadvgstash-9b0bda1a13ebac7866099c325b7ab103520ea40b.tar.gz
vgstash-9b0bda1a13ebac7866099c325b7ab103520ea40b.tar.bz2
vgstash-9b0bda1a13ebac7866099c325b7ab103520ea40b.tar.xz
vgstash-9b0bda1a13ebac7866099c325b7ab103520ea40b.zip
The big VGStash-Web commit!
This commit introduces a templated HTML file to insert your username, and a set of files that, when combined with your exported VGStash in JSON format, can be used to display (and browse) your game collection in your Web browser!
Diffstat (limited to 'src')
-rw-r--r--src/vgstash/web/vgstash-favicon.pngbin0 -> 324 bytes
-rw-r--r--src/vgstash/web/vgstash-web.html46
-rw-r--r--src/vgstash/web/vgstash.css333
-rw-r--r--src/vgstash/web/vgstash.js278
4 files changed, 657 insertions, 0 deletions
diff --git a/src/vgstash/web/vgstash-favicon.png b/src/vgstash/web/vgstash-favicon.png
new file mode 100644
index 0000000..5456f3b
--- /dev/null
+++ b/src/vgstash/web/vgstash-favicon.png
Binary files differ
diff --git a/src/vgstash/web/vgstash-web.html b/src/vgstash/web/vgstash-web.html
new file mode 100644
index 0000000..d5b6bdc
--- /dev/null
+++ b/src/vgstash/web/vgstash-web.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <title>vgstash for {{USERNAME}}</title>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf8">
+ <link rel="stylesheet" href="./vgstash.css" type="text/css">
+ <link rel="icon" href="./vgstash-favicon.png" type="image/png">
+ <script src="./vgstash.js"></script>
+ </head>
+ <body>
+ <header id="top">
+ <h1><em>vgstash</em> for {{USERNAME}}</h1>
+ <p id="help" class="help" tabindex="0" title="Help">?</p>
+ </header>
+ <div class="notice">
+ <p>The <strong>vgstash</strong> webview is currently in development, and is slated for the </code>v0.4</code> release. As such, consider this page a <em>work in progress</em>. Thanks. <a href="#" onclick="show_contact();">Issues?</a>
+ </div>
+ <nav>
+ <ul id="filter_list">
+ <li>All</li>
+ <li title="Games marked New or Playing">Backlog</li>
+ <li title="Track the age of games in your backlog">Backlog Age</li>
+ <li title="Games marked Beaten">Beaten</li>
+ <li title="Games marked Complete">Complete</li>
+ <li title="Games I own digitally">Digital</li>
+ <li title="Games marked Beaten or Complete">Done</li>
+ <li title="Games belonging to a collection">Members</li>
+ <li title="Games with notes">Notes</li>
+ <li title="Games that I own somehow">Owned</li>
+ <li title="Games that I own physically">Physical</li>
+ <li title="Games marked Playing">Playlog</li>
+ </ul>
+ </nav>
+ <div id="vgs_view">
+ <!-- script insertion point -->
+ </div>
+ <!-- modal box -->
+ <div id="modal" class="backdrop">
+ <section id="modal_box" class="modal_box">
+ </section>
+ </div>
+ <footer>
+ <a href="https://pypi.org/project/vgstash">VGStash</a> &copy; 2016-2025 <a href="https://zlg.space/">zlg</a>. Licensed under <a href="https://www.gnu.org/licenses/agpl-3.0.html">AGPL 3.0</a>.
+ </footer>
+ </body>
+</html>
diff --git a/src/vgstash/web/vgstash.css b/src/vgstash/web/vgstash.css
new file mode 100644
index 0000000..8d45958
--- /dev/null
+++ b/src/vgstash/web/vgstash.css
@@ -0,0 +1,333 @@
+:root {
+ --main-bg-color: #152c0e;
+ --main-fg-color: #fff;
+ --header-color: #fff;
+ --header-highlight-color: #fff;
+ --notice-box-bg-color: #eceb13;
+ --notice-box-fg-color: var(--main-bg-color);
+ --button-bg-color: #1a5806;
+ --button-fg-color: #fff;
+ --hover-button-bg-color: #acd;
+ --hover-button-fg-color: #125;
+ --selected-button-bg-color: #000;
+ --selected-button-fg-color: #ffff6c;
+ --modal-bg-color: #dfdfdf;
+ --modal-fg-color: var(--main-bg-color);
+ --table-text-color: var(--main-bg-color);
+ --table-spacing-color: #fff;
+ --table-head-bg-color: #eceb13;
+ --table-head-fg-color: var(--table-text-color);
+ --table-even-bg-color: #dfdfdf;
+ --table-even-fg-color: var(--table-text-color);
+ --table-odd-bg-color: #b4b4b4;
+ --table-odd-fg-color: var(--table-text-color);
+ --note-link-color: var(--table-text-color);
+ --row-hover-bg-color: #f198f0;
+ --row-hover-fg-color: #882250;
+ --row-hover-link-color: var(--row-hover-fg-color);
+ --footer-fg-color: var(--main-fg-color);
+ --footer-link-color: #7cf;
+}
+
+/* MODIFY VARIABLES ABOVE THIS LINE */
+
+html {
+ font-size: 16px;
+ font-family: "Fira Sans", "Liberation Sans", "Arial", sans-serif;
+ margin: 0;
+ padding: 0;
+ background: var(--main-bg-color);
+ color: #fff;
+}
+
+body {
+ font-size: 62.5%;
+ margin: 1.6em auto;
+ padding: 0;
+ width: 90%;
+}
+
+#top {
+ margin-bottom: 1.5em;
+ min-height: 2.8em;
+}
+
+#top h1 {
+ color: var(--header-color);
+ margin: 0;
+ padding: 0;
+ line-height: 1.3;
+ font-size: 2.2em;
+ float: left;
+}
+
+#top h1 em {
+ background: var(--header-highlight-color);
+ color: var(--main-bg-color);
+ padding: .15em .2em;
+ border-radius: 0.3em;
+ font-style: normal;
+}
+
+#top p {
+ float: right;
+}
+
+.help {
+ display: inline-block;
+ border: 1px solid #6ce;
+ color: #fff;
+ cursor: pointer;
+ background: #39B;
+ padding: .1em .2em;
+ margin: 0;
+ border-radius: 1em;
+ width: .9em;
+ height: 1.1em;
+ text-align: center;
+ text-shadow: 1px 1px 1px #000, -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000;
+ text-decoration: none;
+ font-size: 1.8em;
+ font-weight: bold;
+}
+
+.notice {
+ border-radius: .5em;
+ background: var(--notice-box-bg-color);
+ color: var(--notice-box-fg-color);
+ padding: .2em .5em;
+ clear: both;
+}
+
+.notice p {
+ margin: .5em;
+ padding: 0;
+ font-size: 1.5em;
+}
+
+nav {
+ margin: 1em 0;
+ padding: 0;
+ /* background: #000; */
+ clear: both;
+}
+
+nav ul {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ display: block;
+}
+
+nav ul li {
+ display: inline-block;
+ background: var(--button-bg-color);
+ box-shadow: 0 0 .1em rgba(5,0,10,0.7);
+ color: var(--button-fg-color);
+ font-size: 1.8em;
+ font-weight: bold;
+ text-decoration: none;
+ border-radius: .4em;
+ margin: .1em .2em;
+ padding: 0.1em 0.4em;
+}
+
+nav ul li:hover,
+nav ul li.selected:hover {
+ cursor: pointer;
+ background: var(--hover-button-bg-color);
+ color: var(--hover-button-fg-color);
+}
+
+nav ul li.selected {
+ background: var(--selected-button-bg-color);
+ color: var(--selected-button-fg-color);
+}
+
+table {
+ font-size: 1em;
+}
+
+#modal {
+ display: none;
+}
+
+.backdrop {
+ position: fixed;
+ left: 0;
+ right; 0;
+ top: 0;
+ left: 0;
+ background: rgba(30,30,30,0.8);
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 1;
+ overflow: auto;
+}
+
+#modal_box {
+ background: var(--modal-bg-color);
+ color: var(--modal-fg-color);
+ border-radius: 1.3em;
+ width: 85%;
+ margin: 3em auto;
+ padding: 1em 0;
+ font-size: 1.6em;
+}
+
+#modal_box > div {
+ padding: 0 1em;
+ margin: 0;
+ /* overflow: auto; */
+}
+
+#modal_close {
+ background: #d30;
+ border-radius: 0.7em;
+ border: 1px solid #e76;
+ box-shadow: 0 0.1em 0.2em #000;
+ color: #fff;
+ cursor: pointer;
+ display: block;
+ float: right;
+ font-size: 2.4em;
+ line-height: 0.8;
+ margin: 0;
+ padding: 0;
+ position: relative;
+ right: -0.4em;
+ text-align: center;
+ text-decoration: none;
+ text-shadow: 1px 1px 1px #000, -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000;
+ top: -0.7em;
+ width: 0.8em;
+}
+
+
+#modal_box h2 {
+ margin: 0;
+ font-size: 1.7em;
+ font-weight: bold;
+ text-align: center;
+ margin-bottom: 0.3em;
+ padding-bottom: 0.3em;
+}
+
+#modal_box pre.notes {
+ white-space: pre-wrap;
+}
+
+#modal_box table {
+ color: var(--main-bg-color);
+ background: var(--table-spacing-color);
+ box-shadow: 0 0 0.8em rgba(17,14,16,0.7);
+ border-radius: 0.3em;
+ line-height: 1.4;
+}
+
+#modal_box table td {
+ padding: 0.3em;
+}
+
+#modal_box thead tr th {
+ background: var(--table-head-bg-color);
+ color: var(--table-head-fg-color);
+ font-size: 1.2em;
+ border-bottom: 1px solid;
+}
+
+#modal_box tbody tr:nth-of-type(2n+1) {
+ background: var(--table-odd-bg-color);
+}
+
+#modal_box tbody tr:nth-of-type(2n) {
+ background: var(--table-even-bg-color);
+}
+
+#modal_box p:last-child {
+ margin: 1em 0 0 0;
+}
+
+#modal_box span.notes {
+ cursor: inherit;
+}
+
+/* VGS VIEW BELOW */
+
+#vgs_view {
+ clear: both;
+}
+
+#vgs_view p:first-child {
+ font-size: 1.4em;
+ text-align: center;
+}
+
+#vgs_view table {
+ color: var(--table-text-color);
+ background: var(--table-spacing-color);
+ box-shadow: 0 0 0.8em rgba(17,14,16,0.7);
+ border-radius: 0.3em;
+ line-height: 1.4;
+ font-size: 1.5em;
+ width: 100%;
+}
+
+#vgs_view table thead {
+ position: sticky;
+ top: 0;
+}
+
+#vgs_view table td {
+ padding: 0.3em;
+}
+
+#vgs_view thead tr th {
+ background: var(--table-head-bg-color);
+ font-size: 1.2em;
+ border-bottom: 1px solid;
+}
+
+#vgs_view tbody tr:hover td {
+ background: var(--row-hover-bg-color);
+ color: var(--row-hover-fg-color);
+}
+
+#vgs_view tbody tr:hover td span.notes {
+ color: var(--row-hover-link-color);
+}
+
+#vgs_view tr:nth-of-type(2n+1) {
+ background: var(--table-odd-bg-color);
+}
+
+#vgs_view tr:nth-of-type(2n) {
+ background: var(--table-even-bg-color);
+}
+
+tr[id|="n"]:target {
+ /* display: table-row !important; */
+ background: red;
+}
+
+span.notes {
+ text-decoration: underline;
+ cursor: pointer;
+ font-weight: bold;
+ color: var(--note-link-color);
+}
+
+footer {
+ margin: 2em auto;
+ font-size: 1.5em;
+ text-align: center;
+ color: var(--footer-fg-color);
+}
+
+footer a:link,
+footer a:visited {
+ /* TODO: color is arbitrary, revisit */
+ color: var(--footer-link-color);
+}
diff --git a/src/vgstash/web/vgstash.js b/src/vgstash/web/vgstash.js
new file mode 100644
index 0000000..1b4046e
--- /dev/null
+++ b/src/vgstash/web/vgstash.js
@@ -0,0 +1,278 @@
+function loadJSON() {
+ var xobj = new XMLHttpRequest();
+ xobj.overrideMimeType("application/json");
+ xobj.open('GET', './vgstash.json', true);
+ xobj.setRequestHeader("Accept", "application/json");
+ xobj.onreadystatechange = function () {
+ if (xobj.readyState == 4 && xobj.status == "200") {
+ // Required use of an anonymous callback as .open will NOT return a value but simply returns undefined in asynchronous mode
+ data_loaded(xobj);
+ }
+ };
+ xobj.send(null);
+}
+
+/* compare two games based on their purchase date, sort ascending */
+function compare_p_date(a, b) {
+ var result;
+ if (a.p_date > b.p_date) {
+ // 1 = a comes AFTER b
+ result = 1;
+ } else if (a.p_date == b.p_date) {
+ // 0 = a and b are EQUAL
+ result = 0;
+ } else if (a.p_date < b.p_date) {
+ // -1 = b comes AFTER a
+ result = -1;
+ }
+ return result;
+}
+
+var current_filter = "";
+var gamecol;
+var anchor;
+var own_map = {
+ 0: "&ndash;",
+ 1: "Physical",
+ 2: "Digital",
+ 3: "Both",
+ 4: "Member"
+}
+var prog_map = {
+ 0: "&ndash;",
+ 1: "New",
+ 2: "Playing",
+ 3: "Beaten",
+ 4: "Complete"
+}
+
+window.addEventListener("load", loadJSON);
+
+function data_loaded(evt) {
+ console.log("VGStash JSON data loaded!");
+ // set some globals
+ gamecol = JSON.parse(evt.responseText);
+ anchor = document.getElementById("vgs_view");
+ // setup filter button actions
+ flist = document.getElementById("filter_list");
+ // console.log(flist.children);
+ let i = 0;
+ // TODO: why doesn't 'for (f in flist)' work correctly here?
+ for (i = 0; i < flist.childElementCount; i++) {
+ flist.children[i].addEventListener("click", update);
+ }
+ document.getElementById("help").addEventListener('click', show_help);
+ // write to the page
+ render(anchor, filter_vgs(gamecol, "all"));
+}
+
+function filter_vgs(vgscol, filter_name) {
+ var new_col = new Array();
+ for (game in vgscol) {
+ vgscol[game].rowid = game;
+ switch (filter_name) {
+ case 'backlog':
+ if (vgscol[game].progress < 3 && vgscol[game].progress > 0) {
+ new_col.push(vgscol[game]);
+ }
+ break;
+ case 'backlog_age':
+ if (vgscol[game].progress < 3 && vgscol[game].progress > 0
+ && vgscol[game].p_date != null) {
+ tgame = vgscol[game];
+ tgame.years = Math.floor(((Date.now() / 1000) - tgame.p_date) / (60 * 60 * 24 * 365));
+ tgame.days = Math.floor(((Date.now() / 1000) - tgame.p_date) / (60 * 60 * 24) % 365);
+ console.log(tgame);
+ console.log(vgscol[game]);
+ new_col.push(tgame);
+ }
+ break;
+ case 'beaten':
+ if (vgscol[game].progress == 3) {
+ new_col.push(vgscol[game]);
+ }
+ break;
+ case 'complete':
+ if (vgscol[game].progress > 3) {
+ new_col.push(vgscol[game]);
+ }
+ break;
+ case 'digital':
+ if (vgscol[game].ownership > 1 && vgscol[game].ownership < 4) {
+ new_col.push(vgscol[game]);
+ }
+ break;
+ case 'done':
+ if (vgscol[game].progress >= 3) {
+ new_col.push(vgscol[game]);
+ }
+ break;
+ case 'members':
+ if (vgscol[game].ownership == 4) {
+ new_col.push(vgscol[game]);
+ }
+ break;
+ case 'notes':
+ if (vgscol[game].notes.length > 0) {
+ new_col.push(vgscol[game]);
+ }
+ break;
+ case 'owned':
+ if (vgscol[game].ownership > 0 && vgscol[game].ownership < 4) {
+ new_col.push(vgscol[game]);
+ }
+ break;
+ case 'physical':
+ if (vgscol[game].ownership % 2 == 1) {
+ new_col.push(vgscol[game]);
+ }
+ break;
+ case 'playlog':
+ if (vgscol[game].progress == 2) {
+ new_col.push(vgscol[game]);
+ }
+ break;
+ default:
+ /* filter 'all' */
+ new_col.push(vgscol[game]);
+ }
+ }
+ if (filter_name == "backlog_age") {
+ // sort
+ new_col.sort(compare_p_date);
+ }
+ current_filter = filter_name;
+ return new_col;
+}
+
+function clear_button_styles() {
+ flist = document.getElementById("filter_list").children;
+ for (f in flist) {
+ flist[f].className = "";
+ }
+}
+
+function update() {
+ clear_button_styles();
+ this.className = "selected";
+ let fname = this.innerText.toLowerCase().replaceAll(" ", "_");
+ console.log(fname);
+ render(anchor, filter_vgs(gamecol, fname));
+}
+
+function render(target, vgscol) {
+ var output = "";
+ var note_count = 0;
+ var game_count = 0;
+ var system_set = new Array();
+ var note_set = new Array();
+
+ if (current_filter == "backlog_age") {
+ output = "<table>\n<colgroup>\n<col id=\"title_col\"></col>\n<col></col>\n<col></col>\n<col></col>\n<col></col>\n<col></col>\n</colgroup><thead>\n<th>Title</th>\n<th>System</th>\n<th>Ownership</th>\n<th>Progress</th>\n<th>Years</th>\n<th>Days</th>\n</thead>\n<tbody>\n";
+ } else {
+ output = "<table>\n<colgroup>\n<col id=\"title_col\"></col>\n<col></col>\n<col></col>\n<col></col>\n</colgroup><thead>\n<th>Title</th>\n<th>System</th>\n<th>Ownership</th>\n<th>Progress</th>\n</thead>\n<tbody>\n";
+ }
+ for (var game in vgscol) {
+ var has_notes = false;
+ if (vgscol[game].notes.length > 0) {
+ has_notes = true;
+ note_count += 1;
+ note_set.push(vgscol[game].notes);
+ }
+ game_count += 1;
+ if (!system_set.includes(vgscol[game].system)) {
+ system_set.push(vgscol[game].system);
+ }
+ if (has_notes) {
+ var ntitle = '<span class="notes" onclick="show_notes(gamecol[' + vgscol[game].rowid + ']);">' + vgscol[game].title + '</span>';
+ } else {
+ var ntitle = vgscol[game].title
+ }
+ if (current_filter == "backlog_age") {
+ output += "<tr><td>" + ntitle + "</td><td>" + vgscol[game].system + "</td><td>" + own_map[vgscol[game].ownership] + "</td><td>" + prog_map[vgscol[game].progress] + "</td><td>" + vgscol[game].years.toString() + "</td><td>" + vgscol[game].days.toString() + "</td></tr>\n";
+ } else {
+ output += "<tr><td>" + ntitle + "</td><td>" + vgscol[game].system + "</td><td>" + own_map[vgscol[game].ownership] + "</td><td>" + prog_map[vgscol[game].progress] + "</td></tr>\n";
+ }
+ }
+ output += "\n</tbody>\n</table>\n";
+ message = "<p>Showing " + game_count + " games across " + system_set.length + " systems.</p>";
+ target.innerHTML = message + output;
+}
+
+function show_modal(heading, content) {
+ var backdrop = document.getElementById("modal");
+ var modal_box = document.getElementById("modal_box");
+ var text = '';
+ text = "<span id=\"modal_close\">&times;</span>\n" +
+ "<h2>" + heading + "</h2>\n" +
+ "<div>" + content + "</div>";
+ modal_box.innerHTML = text;
+ window.addEventListener('click',
+ function(evt) {
+ if (evt.target.id == 'modal_close' ||
+ evt.target == backdrop) {
+ backdrop.style.display = 'none';
+ }
+ }
+ );
+ backdrop.style.display = 'block';
+}
+
+function show_contact() {
+ let mtext = '<p>If the VGStash webview doesn\'t work for you (i.e. the list of games isn\'t appearing), and you are using a browser that supports <code>XmlHttpRequest</code>, please <a href="mailto:zlg+vgsweb@zlg.space">e-mail me</a> and describe the issue. </p>' +
+ '<p>When reporting an error, <strong>please include your browser name, its version, and the error output from the browser Console.</strong> In Firefox, the Console can be accessed directly with <kbd>Ctrl+Shift+K</kbd>. Chrome users can open the Dev Tools with <kbd>Ctrl+Shift+I</kbd> and click on the "Console" tab. Next, just copy the text you see and send it in your error report.' +
+ '<p>Thanks! --zlg</p>';
+ show_modal("Contacting ZLG with Issues", mtext);
+}
+
+function show_help() {
+ let mtext = '<table>' +
+ '<thead>' +
+ '<tr>' +
+ '<th>Text</th>' +
+ '<th>Meaning</th>' +
+ '</tr>' +
+ '</thead>' +
+ '<tbody>' +
+ '<tr>' +
+ '<td>&ndash; (dash)</td>' +
+ '<td>Unbeatable, or Unowned</td>' +
+ '</tr>' +
+ '<tr>' +
+ '<td>Physical</td>' +
+ '<td>A tangible copy of the game, e.g. cartridge, disc, etc.</td>' +
+ '</tr>' +
+ '<tr>' +
+ '<td>Digital</td>' +
+ "<td>A virtual copy of the game, i.e. an executable file on your device's storage.</td>" +
+ '</tr>' +
+ '<tr>' +
+ '<td>Member</td>' +
+ "<td>A game that you own as part of a collection.</td>" +
+ '</tr>' +
+ '<tr>' +
+ '<td>New</td>' +
+ '<td>The game has not been started yet.</td>' +
+ '</tr>' +
+ '<tr>' +
+ '<td>Playing</td>' +
+ '<td>The game is in the "now playing" list.</td>' +
+ '</tr>' +
+ '<tr>' +
+ '<td>Beaten</td>' +
+ "<td>The game's main story has been completed.</td>" +
+ '</tr>' +
+ '<tr>' +
+ '<td>Complete</td>' +
+ '<td>Every objective has been completed, including any achievements or side missions. Exceptions are made for achievements that are unobtainable due to network dependency.</td>' +
+ '</tr>' +
+ '</tbody>' +
+ '</table>' +
+ '<p><span class="notes">Underlined</span> game titles have notes you can read if you click on them.</p>';
+ show_modal("Legend", mtext);
+}
+
+function show_notes(game) {
+ console.log(game);
+ show_modal(game.title + ' [' + game.system + '] Notes', '<pre class="notes">' + game.notes + '</pre>');
+}