diff --git a/App/css/style.css b/App/css/style.css
new file mode 100644
index 0000000..c92da86
--- /dev/null
+++ b/App/css/style.css
@@ -0,0 +1,188 @@
+/*
+ Styles.css
+*/
+html, body {
+ color: #fff;
+ overflow: hidden;
+ user-select: none;
+ font-family: monospace;
+ background-color: #000;
+ text-shadow: 2px 2px 4px #000;
+}
+input[type='button'], input[type='checkbox'] {
+ outline: none;
+ cursor: pointer;
+}
+input[disabled="disabled"], input[disabled] {
+ cursor: no-drop;
+}
+::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+ border-radius: 4px;
+}
+::-webkit-scrollbar-track {
+ border-radius: 4px;
+ margin: 6px 0px 6px 0px;
+}
+::-webkit-scrollbar-thumb {
+ border-radius: 4px;
+ background: #e7e7e7;
+}
+::-webkit-scrollbar-thumb:hover {
+ border-radius: 4px;
+ background: #f0f0f0;
+}
+::-webkit-scrollbar-thumb:active {
+ background: #fff;
+ border-radius: 4px;
+}
+
+/*
+ Divs
+*/
+.DIV_LIST {
+ top: 38px;
+ left: 0px;
+ position: absolute;
+ width: calc(100% - 280px);
+ height: calc(100% - 286px);
+ background-image: linear-gradient(146deg, #090f1b, #162a50);
+}
+.DIV_LOG {
+ left: 0px;
+ bottom: 0px;
+ width: 100%;
+ height: 248px;
+ position: absolute;
+ background-size: auto 50%;
+ background-position: center;
+ background-repeat: no-repeat;
+ background-image: url('../img/logo.png');
+}
+.DIV_ACTIONS {
+ top: 0px;
+ left: 0px;
+ width: 100%;
+ height: 22px;
+ padding: 8px;
+ position: absolute;
+ background-image: linear-gradient(45deg, #28467f, #1c325f);
+}
+.DIV_OPTIONS {
+ top: 38px;
+ right: 0px;
+ padding: 6px;
+ width: 268px;
+ position: absolute;
+ height: calc(100% - 298px);
+ background-image: linear-gradient(0deg, #162a50, #2a4a86);
+}
+.DIV_TITLE {
+ font-size: 28px;
+ margin-top: 10px;
+ font-weight: bold;
+ text-align: center;
+ margin-bottom: 10px;
+}
+.DIV_LIST_INTERNAL {
+ top: 0px;
+ left: 0px;
+ width: 100%;
+ height: 100%;
+ overflow: auto;
+ transition: 0.4s;
+ position: absolute;
+ background-size: cover;
+ background-color: #0000;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+.GAME_ENTRY {
+ margin: 6px;
+ display: flex;
+ cursor: pointer;
+ overflow: hidden;
+ max-height: 92px;
+ flex-wrap: nowrap;
+ border-radius: 6px;
+ align-items: center;
+ align-content: center;
+ width: calc(100% - 12px);
+ backdrop-filter: blur(4px) invert(0.1);
+ background-image: linear-gradient(90deg, #3a4b6b82, #0000);
+}
+.GAME_ENTRY:hover {
+ box-shadow: 0px 0px 10px #0006;
+ background-image: linear-gradient(90deg, #3a4b6b82, #3a4b6b82);
+}
+.GAME_ENTRY:active {
+ box-shadow: 0px 0px 10px #0006 inset;
+ backdrop-filter: invert(0.68) blur(6px);
+}
+.GAME_DETAILS {
+ cursor: pointer;
+ width: calc(100% - 88px);
+}
+.DIV_RUN_BTN {
+ bottom: 4px;
+ position: absolute;
+ width: calc(100% - 12px);
+}
+
+/*
+ Images
+*/
+.GAME_ICON {
+ width: 66px;
+ margin: 6px;
+ cursor: pointer;
+ border-radius: 6px;
+}
+
+/*
+ Input
+*/
+.APP_LOG {
+ color: #0f0;
+ height: 100%;
+ resize: none;
+ border: none;
+ cursor: text;
+ outline: none;
+ width: calc(100% - 4px);
+ background-repeat: no-repeat;
+ text-shadow: 2px 2px 2px #000;
+ background-image: linear-gradient(180deg, #000000db, #090f1b);
+}
+.BTN_RUN {
+ left: 8px;
+ width: 100%;
+ bottom: 18px;
+ height: 50px;
+ font-size: 18px;
+ margin-top: 2px;
+}
+
+/*
+ Labels
+*/
+.LABEL_checkbox {
+ cursor: pointer;
+ font-size: 16px;
+ font-style: italic;
+}
+.LABEL_emuColor {
+ color: #fffb8e;
+}
+.LABEL_gameTtitle {
+ font-size: 20px;
+ cursor: pointer;
+}
+
+/*
+ Misc
+*/
+.none {
+ display: none;
+}
\ No newline at end of file
diff --git a/App/img/404.png b/App/img/404.png
new file mode 100644
index 0000000..ca18e8c
Binary files /dev/null and b/App/img/404.png differ
diff --git a/App/img/404_BG.png b/App/img/404_BG.png
new file mode 100644
index 0000000..e0e8e33
Binary files /dev/null and b/App/img/404_BG.png differ
diff --git a/App/img/logo.png b/App/img/logo.png
new file mode 100644
index 0000000..9cc705d
Binary files /dev/null and b/App/img/logo.png differ
diff --git a/App/index.htm b/App/index.htm
new file mode 100644
index 0000000..78b6096
--- /dev/null
+++ b/App/index.htm
@@ -0,0 +1,70 @@
+
+
+
+
+
' + 'Path: ' + gList[cGame].eboot + '
';
+ });
+
+ // Insert HTML
+ document.getElementById('DIV_LIST_INTERNAL').innerHTML = tempHtml;
+
+ // Clear BG image
+ TMS.css('DIV_LIST_INTERNAL', {'background-image': 'none'});
+
+ },
+
+ // Select game
+ selectGame: function(gameName){
+
+ if (APP.gameList.list[gameName] !== void 0){
+
+ const cGame = APP.gameList.list[gameName];
+ TMS.css('DIV_LIST_INTERNAL', {
+ 'background-image': 'url("' + cGame.bg + '")'
+ });
+
+ APP.gameList.selectedGame = gameName;
+ APP.design.updateRunButtons();
+
+ }
+
+ },
+
+ // Enable or Disable Run / Stop buttons
+ updateRunButtons: function(){
+
+ // Check if emu is present before allowing to run
+ if (APP.fs.existsSync(APP.settings.data.emuPath) === !0){
+
+ var btnRun = '',
+ logHeight = '248px',
+ btnKill = 'disabled',
+ hackDisplay = 'inline',
+ listHeight = 'calc(100% - 286px)',
+ optionsHeight = 'calc(100% - 298px)';
+
+ // If emu is running
+ if (APP.emuManager.emuRunning === !0){
+
+ btnKill = '';
+ btnRun = 'disabled';
+ listHeight = '172px';
+ hackDisplay = 'none';
+ optionsHeight = '160px';
+ logHeight = 'calc(100% - 210px)';
+
+ }
+
+ // Update GUI
+ TMS.css('DIV_LOG', {'height': logHeight});
+ TMS.css('DIV_LIST', {'height': listHeight});
+ TMS.css('DIV_OPTIONS', {'height': optionsHeight});
+ TMS.css('DIV_HACK_LIST', {'display': hackDisplay});
+
+ // Update Buttons
+ document.getElementById('BTN_RUN').disabled = btnRun;
+ document.getElementById('BTN_KILL').disabled = btnKill;
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/App/js/emumanager.js b/App/js/emumanager.js
new file mode 100644
index 0000000..1caa903
--- /dev/null
+++ b/App/js/emumanager.js
@@ -0,0 +1,51 @@
+/*
+ emumanager.js
+*/
+
+temp_EMUMANAGER = {
+
+ // Emulator is running
+ emuRunning: !1,
+
+ // Run emu
+ runGame: function(){
+
+ // If user selected a game
+ if (APP.gameList.list[APP.gameList.selectedGame] !== void 0){
+
+ // Set main variables
+ var ebootPath = APP.gameList.list[APP.gameList.selectedGame].eboot,
+ emuArgs = ['-e', ebootPath],
+ hList = APP.design.hackList;
+
+ // Get enabled hacks
+ hList.forEach(function(hackName){
+ if (document.getElementById('CHECK_' + hackName).checked === !0){
+ emuArgs.push('-h');
+ emuArgs.push(hackName);
+ }
+ });
+
+ // Log emu args
+ APP.log('\nINFO - Running fpPS4 with args: ' + emuArgs.toString().replace(RegExp(',', 'gi'), ' ') + '\n\n');
+
+ // Run fpPS4
+ APP.runExec(APP.settings.data.emuPath, emuArgs);
+ this.emuRunning = !0;
+
+ // Update GUI
+ APP.design.updateRunButtons();
+
+ }
+
+ },
+
+ // Kill emu process
+ killEmu: function(){
+
+ process.kill(APP.execProcess.pid);
+ this.emuRunning = !1;
+
+ }
+
+}
\ No newline at end of file
diff --git a/App/js/filemanager.js b/App/js/filemanager.js
new file mode 100644
index 0000000..a270110
--- /dev/null
+++ b/App/js/filemanager.js
@@ -0,0 +1,30 @@
+/*
+ filemanager.js
+*/
+
+temp_FILEMANAGER = {
+
+ // Select path
+ selectPath: function(postAction){
+
+ if (postAction !== void 0){
+
+ document.getElementById('APP_FOLDER_LOADER').onchange = function(){
+
+ const cFile = document.getElementById('APP_FOLDER_LOADER').files[0];
+
+ if (cFile.path !== null && cFile.path !== void 0 && cFile.path !== ''){
+ postAction(cFile.path.replace(RegExp('\\\\', 'gi'), '/'));
+ document.getElementById('APP_FOLDER_LOADER').value = '';
+ document.getElementById('APP_FOLDER_LOADER').accept = '';
+ }
+
+ }
+
+ document.getElementById('APP_FOLDER_LOADER').click();
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/App/js/gamelist.js b/App/js/gamelist.js
new file mode 100644
index 0000000..18303ca
--- /dev/null
+++ b/App/js/gamelist.js
@@ -0,0 +1,125 @@
+/*
+ gamelist.js
+*/
+
+temp_GAMELIST = {
+
+ // Game List
+ list: {},
+
+ // Selected game
+ selectedGame: '',
+
+ // Select game path
+ selectPath: function(){
+
+ APP.fileManager.selectPath(function(newGamePath){
+ APP.settings.data.gamePath = newGamePath;
+ APP.settings.save();
+ APP.gameList.load();
+ });
+
+ },
+
+ // Load list
+ load: function(){
+
+ // Check if path exists
+ if (APP.fs.existsSync(APP.settings.data.gamePath) === !0){
+
+ // Reset game list
+ APP.gameList.list = {};
+
+ // Get game list
+ const gList = APP.fs.readdirSync(APP.settings.data.gamePath);
+
+ if (gList.length > 0){
+
+ // Process game list
+ gList.forEach(function(gPath){
+
+ var addGame = !0,
+ elfName = '',
+ ebootName = '',
+ pathBase = APP.settings.data.gamePath + '/' + gPath,
+
+ appBg0 = pathBase + '/pic0.png',
+ appBg1 = pathBase + '/pic1.png',
+ appIcon = pathBase + '/icon.png',
+
+ finalBg = appBg0,
+ finalIcon = appIcon,
+
+ ebootFile = pathBase + '/eboot.bin';
+
+ if (APP.fs.existsSync(ebootFile) === !0){
+ ebootName = 'eboot.bin';
+ }
+
+ // If eboot.bin doesn't exists, look for any .elf file
+ if (APP.fs.existsSync(ebootFile) !== !0){
+
+ var fList = APP.fs.readdirSync(pathBase),
+ execName = fList.filter(function(fName){
+ if (fName.toLowerCase().indexOf('.elf') !== -1){
+ return fName;
+ }
+ })[0];
+
+ ebootName = execName;
+
+ // If not found (undefined), skip
+ if (execName === void 0){
+ addGame = !1;
+ }
+
+ }
+
+ // Set icon
+ if (APP.fs.existsSync(appIcon) === !1){
+ finalIcon = APP.settings.data.nwPath + '/app/img/404.png';
+ }
+
+ // Set BG image
+ if (APP.fs.existsSync(appBg0) === !1){
+ finalBg = appBg1;
+ if (APP.fs.existsSync(appBg1) === !1){
+ finalBg = APP.settings.data.nwPath + '/app/img/404_BG.png';
+ }
+ }
+
+ // If executable exists, set data
+ if (addGame === !0){
+ APP.gameList.list[gPath] = {
+ bg: finalBg,
+ name: gPath,
+ icon: finalIcon,
+ eboot: APP.settings.data.gamePath + '/' + gPath + '/' + ebootName
+ }
+ }
+
+ });
+
+ } else {
+
+ // No games / homebrew found
+ APP.log('INFO - No games / homebrew were detected on current path (' + APP.settings.data.gamePath + ')');
+
+ }
+
+ // Render game list
+ APP.design.renderGameList();
+
+ }
+
+ },
+
+ // Open game folder
+ openFolder: function(){
+
+ // Spawn explorer
+ APP.childProcess.exec('start "" "' + APP.settings.data.gamePath + '"');
+
+ }
+
+}
\ No newline at end of file
diff --git a/App/js/main.js b/App/js/main.js
new file mode 100644
index 0000000..3a624cb
--- /dev/null
+++ b/App/js/main.js
@@ -0,0 +1,124 @@
+/*
+ main.js
+*/
+
+var APP = {
+
+ // App version
+ version: '1.0.0',
+
+ // Import nw modules
+ fs: require('fs'),
+ path: require('path'),
+ childProcess: require('child_process'),
+
+ // Import app modules
+ design: temp_DESIGN,
+ gameList: temp_GAMELIST,
+ settings: temp_SETTINGS,
+ emuManager: temp_EMUMANAGER,
+ fileManager: temp_FILEMANAGER,
+
+ // Log function and variables
+ logData: '',
+ log: function(text, skipLog){
+
+ if (text !== '' && text !== void 0){
+
+ var textarea = document.getElementById('APP_LOG'),
+ previousLog = textarea.value,
+ newLog = previousLog + '\n' + text;
+
+ if (previousLog == ''){
+ newLog = text;
+ }
+
+ if (previousLog.slice(previousLog.length - 1, previousLog.length) === '\n'){
+ newLog = previousLog + text;
+ }
+
+ textarea.value = newLog;
+ APP.logData = newLog;
+
+ // If true, skip internal log
+ if (skipLog !== !0){
+ console.log(text);
+ }
+
+ // Scroll log
+ textarea.scrollTop = textarea.scrollHeight;
+
+ }
+
+ },
+
+ // Run external software
+ execProcess: void 0,
+ runExec: function(exe, args){
+
+ if (exe !== void 0 && exe !== ''){
+
+ /*
+ Spawn process
+ It will change running dir to current exe location
+ */
+ process.chdir(APP.path.parse(exe).dir);
+ APP.execProcess = APP.childProcess.spawn(exe, args);
+
+ // Log on stdout and stderr
+ APP.execProcess.stdout.on('data', function(data){
+ APP.log(data.toString(), !0);
+ });
+ APP.execProcess.stderr.on('data', function(data){
+ APP.log(data.toString(), !0);
+ });
+
+ // Log on close
+ APP.execProcess.on('close', function(code){
+ process.chdir(APP.settings.data.nwPath);
+ APP.emuManager.emuRunning = !1;
+ APP.log('INFO - ' + APP.path.parse(exe).base + ' was closed returning code ' + code);
+ APP.design.updateRunButtons();
+ return code;
+ });
+
+ }
+
+ },
+
+ // About screen
+ about: function(){
+ window.alert('fpPS4 Temmie\'s Launcher - Version: ' + this.version + '\nCreated by TemmieHeartz\n(https://twitter.com/themitosan)\n\nfpPS4 main emulator is created by red-prig\n(https://github.com/red-prig/fpPS4)');
+ },
+
+ // Reload app
+ reload: function(){
+ location.reload();
+ }
+
+}
+
+// Delete modules
+delete temp_DESIGN;
+delete temp_SETTINGS;
+delete temp_GAMELIST;
+delete temp_EMUMANAGER;
+delete temp_FILEMANAGER;
+
+// Start
+window.onload = function(){
+
+ // Main log
+ APP.log('fpPS4 Temmie\'s Launcher - Version: ' + APP.version + '\nRunning on nw.js (node-webkit) version ' + process.versions.nw);
+
+ // Load settings
+ APP.settings.load();
+ APP.settings.checkPaths();
+
+ // Load game list
+ APP.gameList.load();
+
+ // Rener hack list
+ APP.design.renderHacklist();
+
+}
\ No newline at end of file
diff --git a/App/js/settings.js b/App/js/settings.js
new file mode 100644
index 0000000..c8a5165
--- /dev/null
+++ b/App/js/settings.js
@@ -0,0 +1,69 @@
+/*
+ settings.js
+*/
+
+temp_SETTINGS = {
+
+ // Settings list
+ data: {
+ nwPath: '',
+ emuPath: '',
+ gamePath: ''
+ },
+
+ // Load settings
+ load: function() {
+
+ // Create save
+ if (localStorage.getItem('settings') === null){
+ APP.settings.save();
+ }
+
+ const settings = localStorage.getItem('settings');
+ this.data = JSON.parse(settings);
+
+ },
+
+ // Save settings
+ save: function() {
+ localStorage.setItem('settings', JSON.stringify(this.data));
+ },
+
+ // Check paths
+ checkPaths: function(){
+
+ // Fix path
+ this.data.nwPath = nw.__dirname.replace(RegExp('\\\\', 'gi'), '/');
+
+ const mainPath = this.data.nwPath,
+ pathList = [
+ '/Emu',
+ '/Games'
+ ];
+
+ // Try create required paths
+ pathList.forEach(function(cPath){
+ if (APP.fs.existsSync(mainPath + cPath) !== !0){
+ try {
+ APP.fs.mkdirSync(mainPath + cPath);
+ } catch (err) {
+ APP.log('Unable to create path!\n(' + mainPath + cPath + ')\n' + err);
+ }
+ }
+ });
+
+ // Set Games / Emu paths and check if fpPS4 exe is present
+ this.data.gamePath = mainPath + '/Games';
+ this.data.emuPath = mainPath + '/Emu/fpPS4.exe';
+
+ if (APP.fs.existsSync(this.data.emuPath) === !0){
+ APP.log('INFO - Main fpPS4 was found!');
+ } else {
+ const errMsg = 'ERROR - Unable to locate main fpPS4 executable!\nMake sure to insert it on \"Emu\" folder and click on ok.';
+ window.alert(errMsg);
+ APP.log(errMsg);
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..c54ea74
--- /dev/null
+++ b/package.json
@@ -0,0 +1,25 @@
+{
+ "version": "1.0.0",
+ "license": "GPL-2",
+ "main": "App/index.htm",
+ "author": "TemmieHeartz",
+ "name": "fpPS4 Temmie's Launcher",
+ "description": "A simple launcher for fpPS4 project",
+ "repository": "https://github.com/themitosan/fpPS4-Temmie-s-Launcher",
+ "window": {
+ "frame": true,
+ "width": 1180,
+ "height": 710,
+ "toolbar": true,
+ "min_width": 1180,
+ "min_height": 710,
+ "fullscreen": false,
+ "position": "center",
+ "theme-color": "#000",
+ "icon": "App/img/logo.png",
+ "title": "fpPS4 Temmie's Launcher"
+ },
+ "webkit": {
+ "plugin": true
+ }
+}
\ No newline at end of file