diff --git a/pkg/emscripten/itch/index.html b/pkg/emscripten/itch/index.html
new file mode 100644
index 0000000000..565eca97b2
--- /dev/null
+++ b/pkg/emscripten/itch/index.html
@@ -0,0 +1,148 @@
+
+
+
+
+ RetroArch Web Player
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+
+
+
+
+
+
For now, we recommend you use Google Chrome for the best possible performance.
+

+
+
+
+
+
+
+
+
+
+
+
diff --git a/pkg/emscripten/itch/itch.css b/pkg/emscripten/itch/itch.css
new file mode 100644
index 0000000000..d5e6b52d89
--- /dev/null
+++ b/pkg/emscripten/itch/itch.css
@@ -0,0 +1,99 @@
+/**
+ * RetroArch Web Player
+ *
+ * This provides the basic styling for the RetroArch web player.
+ */
+
+/**
+ * Make sure the background of the player is black.
+ */
+.webplayer-container {
+ background-color: black;
+}
+
+/**
+ * Webplayer Preview when not loaded.
+ */
+.webplayer-preview {
+ margin: 0 auto;
+ cursor: wait;
+ opacity: 0.2;
+ transition: all 0.8s;
+ -webkit-animation: loading 0.8s ease-in-out infinite alternate;
+ -moz-animation: loading 0.8s ease-in-out infinite alternate;
+ animation: loading 0.8s ease-in-out infinite alternate;
+}
+.webplayer-preview.loaded {
+ cursor: pointer;
+ opacity: 1;
+ -webkit-animation: loaded 0.8s ease-in-out;
+ -moz-animation: loaded 0.8s ease-in-out;
+ animation: loaded 0.8s ease-in-out;
+}
+@keyframes loaded {
+ from {
+ opacity: 0.2;
+ }
+ to {
+ opacity: 1;
+ }
+}
+@-moz-keyframes loaded {
+ from {
+ opacity: 0.2;
+ }
+ to {
+ opacity: 1;
+ }
+}
+@-webkit-keyframes loaded {
+ from {
+ opacity: 0.2;
+ }
+ to {
+ opacity: 1;
+ }
+}
+@keyframes loading{
+ from {
+ opacity: 0.2;
+ }
+ to {
+ opacity: 0.35;
+ }
+}
+@-moz-keyframes loading{
+ from {
+ opacity: 0.2;
+ }
+ to {
+ opacity: 0.35;
+ }
+}
+@-webkit-keyframes loading {
+ from {
+ opacity: 0.2;
+ }
+ to {
+ opacity: 0.35;
+ }
+}
+
+/**
+ * Disable the border around the player.
+ */
+canvas.webplayer {
+ border: none;
+ outline: none;
+}
+
+textarea {
+ font-family: monospace;
+ font-size: 0.7em;
+ height: 95%;
+ width: 95%;
+ border-style: none;
+ border-color: transparent;
+ overflow: auto;
+ resize: none;
+}
diff --git a/pkg/emscripten/itch/itch.js b/pkg/emscripten/itch/itch.js
new file mode 100644
index 0000000000..ba42e4274b
--- /dev/null
+++ b/pkg/emscripten/itch/itch.js
@@ -0,0 +1,401 @@
+/**
+ * RetroArch Web Player
+ *
+ * This provides the basic JavaScript for the RetroArch web player.
+ */
+var dropbox = false;
+var client = new Dropbox.Client({ key: "il6e10mfd7pgf8r" });
+var XFS;
+var BrowserFS = browserfs;
+
+var showError = function(error) {
+ switch (error.status) {
+ case Dropbox.ApiError.INVALID_TOKEN:
+ // If you're using dropbox.js, the only cause behind this error is that
+ // the user token expired.
+ // Get the user through the authentication flow again.
+ break;
+
+ case Dropbox.ApiError.NOT_FOUND:
+ // The file or folder you tried to access is not in the user's Dropbox.
+ // Handling this error is specific to your application.
+ break;
+
+ case Dropbox.ApiError.OVER_QUOTA:
+ // The user is over their Dropbox quota.
+ // Tell them their Dropbox is full. Refreshing the page won't help.
+ break;
+
+ case Dropbox.ApiError.RATE_LIMITED:
+ // Too many API requests. Tell the user to try again later.
+ // Long-term, optimize your code to use fewer API calls.
+ break;
+
+ case Dropbox.ApiError.NETWORK_ERROR:
+ // An error occurred at the XMLHttpRequest layer.
+ // Most likely, the user's network connection is down.
+ // API calls will not succeed until the user gets back online.
+ break;
+
+ case Dropbox.ApiError.INVALID_PARAM:
+ case Dropbox.ApiError.OAUTH_ERROR:
+ case Dropbox.ApiError.INVALID_METHOD:
+ default:
+ // Caused by a bug in dropbox.js, in your application, or in Dropbox.
+ // Tell the user an error occurred, ask them to refresh the page.
+ }
+};
+
+function dropboxInit()
+{
+ document.getElementById('btnRun').disabled = true;
+ document.getElementById('btnDrop').disabled = true;
+ $('#icnDrop').removeClass('fa-dropbox');
+ $('#icnDrop').addClass('fa-spinner fa-spin');
+
+
+ client.authDriver(new Dropbox.AuthDriver.Redirect());
+ client.authenticate({ rememberUser: true }, function(error, client)
+ {
+ if (error)
+ {
+ return showError(error);
+ }
+ dropboxSync(client, dropboxSyncComplete);
+ });
+}
+
+function dropboxSyncComplete()
+{
+ document.getElementById('btnRun').disabled = false;
+ $('#icnDrop').removeClass('fa-spinner').removeClass('fa-spin');
+ $('#icnDrop').addClass('fa-dropbox');
+ console.log("WEBPLAYER: Sync successful");
+
+ setupFileSystem("dropbox");
+ setupFolderStructure();
+ preLoadingComplete();
+}
+
+var afs;
+
+function dropboxSync(dropboxClient, cb)
+{
+ var dbfs = new BrowserFS.FileSystem.Dropbox(dropboxClient);
+ // Wrap in afsFS.
+ afs = new BrowserFS.FileSystem.AsyncMirror(
+ new BrowserFS.FileSystem.InMemory(), dbfs);
+
+ afs.initialize(function(err)
+ {
+ // Initialize it as the root file system.
+ //BrowserFS.initialize(afs);
+ cb();
+ });
+}
+
+function preLoadingComplete()
+{
+ /* Make the Preview image clickable to start RetroArch. */
+ $('.webplayer-preview').addClass('loaded').click(function () {
+ startRetroArch();
+ return false;
+ });
+}
+
+
+function setupFileSystem(backend)
+{
+ /* create a mountable filesystem that will server as a root
+ mountpoint for browserfs */
+ var mfs = new BrowserFS.FileSystem.MountableFileSystem();
+
+ /* create an XmlHttpRequest filesystem for the bundled data */
+ var xfs1 = new BrowserFS.FileSystem.XmlHttpRequest
+ (".index-xhr", "https://bot.libretro.com/assets/frontend/bundle/");
+ /* create an XmlHttpRequest filesystem for core assets */
+ var xfs2 = new BrowserFS.FileSystem.XmlHttpRequest
+ (".index-xhr", "https://bot.libretro.com/assets/cores/");
+
+ console.log("WEBPLAYER: Initializing Filesystem");
+ if(backend == "browser")
+ {
+ console.log("WEBPLAYER: Initializing LocalStorage");
+
+ /* create a local filesystem */
+ var lsfs = new BrowserFS.FileSystem.LocalStorage();
+
+ /* mount the filesystems onto mfs */
+ mfs.mount('/home/web_user/retroarch/userdata', lsfs);
+
+ /* create a memory filesystem for content only
+ var imfs = new BrowserFS.FileSystem.InMemory();*/
+
+ /* mount the filesystems onto mfs
+ mfs.mount('/home/web_user/retroarch/userdata/content/', imfs);*/
+ }
+ else
+ {
+ /* mount the filesystems onto mfs */
+ mfs.mount('/home/web_user/retroarch/userdata', afs);
+ }
+
+ mfs.mount('/home/web_user/retroarch/bundle', xfs1);
+ mfs.mount('/home/web_user/retroarch/userdata/content/downloads', xfs2);
+ BrowserFS.initialize(mfs);
+ var BFS = new BrowserFS.EmscriptenFS();
+ FS.mount(BFS, {root: '/home'}, '/home');
+ console.log("WEBPLAYER: " + backend + " filesystem initialized");
+}
+
+/**
+ * Retrieve the value of the given GET parameter.
+ */
+function getParam(name) {
+ var results = new RegExp('[\?&]' + name + '=([^]*)').exec(window.location.href);
+ if (results) {
+ return results[1] || null;
+ }
+}
+
+function setupFolderStructure()
+{
+ FS.createPath('/', '/home/web_user', true, true);
+}
+
+function stat(path)
+{
+ try{
+ FS.stat(path);
+ }
+ catch(err)
+ {
+ console.log("WEBPLAYER: file " + path + " doesn't exist");
+ return false;
+ }
+ return true;
+}
+
+function startRetroArch()
+{
+ $('.webplayer').show();
+ $('.webplayer-preview').hide();
+ document.getElementById('btnDrop').disabled = true;
+ document.getElementById('btnRun').disabled = true;
+
+ $('#btnFullscreen').removeClass('disabled');
+ $('#btnMenu').removeClass('disabled');
+ $('#btnAdd').removeClass('disabled');
+ $('#btnRom').removeClass('disabled');
+
+ document.getElementById("btnAdd").disabled = false;
+ document.getElementById("btnRom").disabled = false;
+ document.getElementById("btnMenu").disabled = false;
+ document.getElementById("btnFullscreen").disabled = false;
+
+ Module['callMain'](Module['arguments']);
+ document.getElementById('canvas').focus();
+}
+
+function selectFiles(files)
+{
+ $('#btnAdd').addClass('disabled');
+ $('#icnAdd').removeClass('fa-plus');
+ $('#icnAdd').addClass('fa-spinner spinning');
+ var count = files.length;
+
+ for (var i = 0; i < files.length; i++)
+ {
+ filereader = new FileReader();
+ filereader.file_name = files[i].name;
+ filereader.readAsArrayBuffer(files[i]);
+ filereader.onload = function(){uploadData(this.result, this.file_name)};
+ filereader.onloadend = function(evt)
+ {
+ console.log("WEBPLAYER: File: " + this.file_name + " Upload Complete");
+ if (evt.target.readyState == FileReader.DONE)
+ {
+ $('#btnAdd').removeClass('disabled');
+ $('#icnAdd').removeClass('fa-spinner spinning');
+ $('#icnAdd').addClass('fa-plus');
+ }
+ }
+ }
+}
+
+function uploadData(data,name)
+{
+ var dataView = new Uint8Array(data);
+ FS.createDataFile('/', name, dataView, true, false);
+
+ var data = FS.readFile(name,{ encoding: 'binary' });
+ FS.writeFile('/home/web_user/retroarch/userdata/content/' + name, data ,{ encoding: 'binary' });
+ FS.unlink(name);
+}
+
+var Module =
+{
+ noInitialRun: true,
+ arguments: ["-v", "--menu"],
+ preRun: [],
+ postRun: [],
+ print: (function()
+ {
+ var element = document.getElementById('output');
+ element.value = ''; // clear browser cache
+ return function(text)
+ {
+ text = Array.prototype.slice.call(arguments).join(' ');
+ element.value += text + "\n";
+ element.scrollTop = 99999; // focus on bottom
+ };
+ })(),
+
+ printErr: function(text)
+ {
+ var text = Array.prototype.slice.call(arguments).join(' ');
+ var element = document.getElementById('output');
+ element.value += text + "\n";
+ element.scrollTop = 99999; // focus on bottom
+ },
+ canvas: document.getElementById('canvas'),
+ totalDependencies: 0,
+ monitorRunDependencies: function(left)
+ {
+ this.totalDependencies = Math.max(this.totalDependencies, left);
+ }
+};
+
+function switchCore(corename) {
+ localStorage.setItem("core", corename);
+}
+
+function switchStorage() {
+ if (localStorage.getItem("backend") == "dropbox")
+ {
+ localStorage.setItem("backend", "browser");
+ location.reload();
+ }
+ else
+ {
+ localStorage.setItem("backend", "dropbox");
+ location.reload();
+ }
+}
+
+// When the browser has loaded everything.
+$(function() {
+ /**
+ * Attempt to disable some default browser keys.
+ */
+ var keys = {
+ 9: "tab",
+ 13: "enter",
+ 16: "shift",
+ 18: "alt",
+ 27: "esc",
+ 33: "rePag",
+ 34: "avPag",
+ 35: "end",
+ 36: "home",
+ 37: "left",
+ 38: "up",
+ 39: "right",
+ 40: "down",
+ 112: "F1",
+ 113: "F2",
+ 114: "F3",
+ 115: "F4",
+ 116: "F5",
+ 117: "F6",
+ 118: "F7",
+ 119: "F8",
+ 120: "F9",
+ 121: "F10",
+ 122: "F11",
+ 123: "F12"
+ };
+ window.addEventListener('keydown', function (e) {
+ if (keys[e.which]) {
+ e.preventDefault();
+ }
+ });
+
+ // Switch the core when selecting one.
+ $('#core-selector a').click(function () {
+ var coreChoice = $(this).data('core');
+ switchCore(coreChoice);
+ });
+
+ // Find which core to load.
+ var core = localStorage.getItem("core", core);
+ if (!core) {
+ core = 'gambatte';
+ }
+ // Make the core the selected core in the UI.
+ var coreTitle = $('#core-selector a[data-core="' + core + '"]').addClass('active').text();
+ $('#dropdownMenu1').text(coreTitle);
+
+ // Load the Core's related JavaScript.
+ $.getScript(core + '_libretro.js', function ()
+ {
+ // Activate the Start RetroArch button.
+ $('#btnRun').removeClass('disabled');
+ $('#icnRun').removeClass('fa-spinner').removeClass('fa-spin');
+ $('#icnRun').addClass('fa-play');
+
+ document.getElementById("btnRun").disabled = false;
+
+ if (localStorage.getItem("backend") == "dropbox")
+ {
+ $('#icnDrop').removeClass('fa-globe');
+ $('#icnDrop').addClass('fa-dropbox');
+ dropboxInit();
+ }
+ else
+ {
+ $('#icnDrop').addClass('fa-globe');
+ $('#icnDrop').removeClass('fa-dropbox');
+ preLoadingComplete();
+ setupFileSystem("browser");
+ setupFolderStructure();
+ }
+ });
+ });
+
+function keyPress(k)
+{
+ kp(k, "keydown");
+ setInterval(function(){kp(k, "keyup")}, 1000);
+}
+
+kp = function(k, event) {
+ var oEvent = document.createEvent('KeyboardEvent');
+
+ // Chromium Hack
+ Object.defineProperty(oEvent, 'keyCode', {
+ get : function() {
+ return this.keyCodeVal;
+ }
+ });
+ Object.defineProperty(oEvent, 'which', {
+ get : function() {
+ return this.keyCodeVal;
+ }
+ });
+
+ if (oEvent.initKeyboardEvent) {
+ oEvent.initKeyboardEvent(event, true, true, document.defaultView, false, false, false, false, k, k);
+ } else {
+ oEvent.initKeyEvent(event, true, true, document.defaultView, false, false, false, false, k, 0);
+ }
+
+ oEvent.keyCodeVal = k;
+
+ if (oEvent.keyCode !== k) {
+ alert("keyCode mismatch " + oEvent.keyCode + "(" + oEvent.which + ")");
+ }
+
+ document.dispatchEvent(oEvent);
+ document.getElementById('canvas').focus();
+}