From 9b0bda1a13ebac7866099c325b7ab103520ea40b Mon Sep 17 00:00:00 2001 From: zlg Date: Tue, 29 Jul 2025 19:11:50 -0700 Subject: 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! --- src/vgstash/web/vgstash-favicon.png | Bin 0 -> 324 bytes src/vgstash/web/vgstash-web.html | 46 +++++ src/vgstash/web/vgstash.css | 333 ++++++++++++++++++++++++++++++++++++ src/vgstash/web/vgstash.js | 278 ++++++++++++++++++++++++++++++ 4 files changed, 657 insertions(+) create mode 100644 src/vgstash/web/vgstash-favicon.png create mode 100644 src/vgstash/web/vgstash-web.html create mode 100644 src/vgstash/web/vgstash.css create mode 100644 src/vgstash/web/vgstash.js (limited to 'src') diff --git a/src/vgstash/web/vgstash-favicon.png b/src/vgstash/web/vgstash-favicon.png new file mode 100644 index 0000000..5456f3b Binary files /dev/null and b/src/vgstash/web/vgstash-favicon.png 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 @@ + + + + vgstash for {{USERNAME}} + + + + + + +
+

vgstash for {{USERNAME}}

+

?

+
+
+

The vgstash webview is currently in development, and is slated for the v0.4 release. As such, consider this page a work in progress. Thanks. Issues? +

+ +
+ +
+ + + + + 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: "–", + 1: "Physical", + 2: "Digital", + 3: "Both", + 4: "Member" +} +var prog_map = { + 0: "–", + 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 = "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"; + } else { + output = "
TitleSystemOwnershipProgressYearsDays
\n\n\n\n\n\n\n\n\n\n\n\n\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 = '' + vgscol[game].title + ''; + } else { + var ntitle = vgscol[game].title + } + if (current_filter == "backlog_age") { + output += "\n"; + } else { + output += "\n"; + } + } + output += "\n\n
TitleSystemOwnershipProgress
" + ntitle + "" + vgscol[game].system + "" + own_map[vgscol[game].ownership] + "" + prog_map[vgscol[game].progress] + "" + vgscol[game].years.toString() + "" + vgscol[game].days.toString() + "
" + ntitle + "" + vgscol[game].system + "" + own_map[vgscol[game].ownership] + "" + prog_map[vgscol[game].progress] + "
\n"; + message = "

Showing " + game_count + " games across " + system_set.length + " systems.

"; + target.innerHTML = message + output; +} + +function show_modal(heading, content) { + var backdrop = document.getElementById("modal"); + var modal_box = document.getElementById("modal_box"); + var text = ''; + text = "×\n" + + "

" + heading + "

\n" + + "
" + content + "
"; + 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 = '

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 XmlHttpRequest, please e-mail me and describe the issue.

' + + '

When reporting an error, please include your browser name, its version, and the error output from the browser Console. In Firefox, the Console can be accessed directly with Ctrl+Shift+K. Chrome users can open the Dev Tools with Ctrl+Shift+I and click on the "Console" tab. Next, just copy the text you see and send it in your error report.' + + '

Thanks! --zlg

'; + show_modal("Contacting ZLG with Issues", mtext); +} + +function show_help() { + let mtext = '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + "" + + '' + + '' + + '' + + "" + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + "" + + '' + + '' + + '' + + '' + + '' + + '' + + '
TextMeaning
– (dash)Unbeatable, or Unowned
PhysicalA tangible copy of the game, e.g. cartridge, disc, etc.
DigitalA virtual copy of the game, i.e. an executable file on your device's storage.
MemberA game that you own as part of a collection.
NewThe game has not been started yet.
PlayingThe game is in the "now playing" list.
BeatenThe game's main story has been completed.
CompleteEvery objective has been completed, including any achievements or side missions. Exceptions are made for achievements that are unobtainable due to network dependency.
' + + '

Underlined game titles have notes you can read if you click on them.

'; + show_modal("Legend", mtext); +} + +function show_notes(game) { + console.log(game); + show_modal(game.title + ' [' + game.system + '] Notes', '
' + game.notes + '
'); +} -- cgit v1.2.3-54-g00ecf