├── .browserslistrc ├── public ├── favicon.ico ├── favicon.png ├── loading.html ├── index.html └── loading.css ├── src ├── assets │ ├── logo.png │ └── logo.svg ├── shims-vue.d.ts ├── shims-vuetify.d.ts ├── store │ └── index.ts ├── views │ ├── Home.vue │ ├── Downloads.vue │ ├── Settings.vue │ └── Games.vue ├── event-bus │ └── index.ts ├── shims-tsx.d.ts ├── main.ts ├── plugins │ └── vuetify.ts ├── modules │ ├── metadata.ts │ └── config.ts ├── router │ └── index.ts ├── workers │ └── categories.worker.ts ├── components │ ├── Services │ │ └── Snackbar.vue │ ├── Games │ │ ├── GameCard.vue │ │ └── GameOverview.vue │ ├── Downloads │ │ └── DownloadItemCard.vue │ └── AppBar │ │ └── GameSearch.vue ├── downloader │ ├── fetch.ts │ └── index.ts ├── background.ts └── App.vue ├── babel.config.js ├── .gitignore ├── tsconfig.json ├── .github └── workflows │ └── ci.yml ├── vue.config.js ├── README.md └── package.json /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SushyDev/vapor-store/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SushyDev/vapor-store/HEAD/public/favicon.png -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SushyDev/vapor-store/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue' 3 | export default Vue 4 | } 5 | -------------------------------------------------------------------------------- /src/shims-vuetify.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'vuetify/lib/framework' { 2 | import Vuetify from 'vuetify' 3 | export default Vuetify 4 | } 5 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | 4 | Vue.use(Vuex); 5 | 6 | export default new Vuex.Store({ 7 | state: {}, 8 | mutations: {}, 9 | actions: {}, 10 | modules: {}, 11 | }); 12 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 6 | 10 | -------------------------------------------------------------------------------- /src/event-bus/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | export const GameBus = new Vue(); 3 | export const LoadingBus = new Vue(); 4 | export const SnackBus = new Vue(); 5 | 6 | export const setLoading = (type: boolean) => LoadingBus.$emit('loading', type); 7 | -------------------------------------------------------------------------------- /src/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from 'vue' 2 | 3 | declare global { 4 | namespace JSX { 5 | // tslint:disable no-empty-interface 6 | interface Element extends VNode {} 7 | // tslint:disable no-empty-interface 8 | interface ElementClass extends Vue {} 9 | interface IntrinsicElements { 10 | [elem: string]: any 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | 25 | #Electron-builder output 26 | /dist_electron -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import App from './App.vue'; 3 | import router from './router'; 4 | import store from './store'; 5 | import vuetify from './plugins/vuetify'; 6 | 7 | Vue.config.productionTip = false; 8 | 9 | new Vue({ 10 | router, 11 | store, 12 | vuetify, 13 | data: { 14 | // Uh oh - appName is *also* the name of the 15 | // instance property we defined! 16 | appName: 'The name of some other app', 17 | }, 18 | render: (h) => h(App), 19 | }).$mount('#app'); 20 | -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | Artboard 46 2 | -------------------------------------------------------------------------------- /src/plugins/vuetify.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuetify from 'vuetify/lib/framework'; 3 | import colors from 'vuetify/lib/util/colors'; 4 | 5 | Vue.use(Vuetify); 6 | 7 | export default new Vuetify({ 8 | theme: { 9 | dark: true, 10 | themes: { 11 | dark: { 12 | primary: colors.lightBlue.accent2, 13 | secondary: colors.pink.accent1, 14 | }, 15 | light: { 16 | primary: colors.lightBlue.accent2, 17 | secondary: colors.pink.accent1, 18 | }, 19 | }, 20 | }, 21 | }); 22 | -------------------------------------------------------------------------------- /src/modules/metadata.ts: -------------------------------------------------------------------------------- 1 | // !#Gets gameinfo by name 2 | const getGameByName = (gameName: string) => fetch(`https://api.rawg.io/api/games?search=${gameName}`); 3 | // !#Fetches steamgriddb data 4 | // ? First gets game by name 5 | // ? Then use the id fetched from the name to search the cover by id 6 | // ? then return both arrays that are fetched from the steamgriddb 7 | exports.fetch = (name: string) => getGameByName(name).then((nameResult: any) => (nameResult.results[0] ? nameResult.results[0] : {...nameResult.results[0], url: '../img/game-cover.png'})); 8 | 9 | // # Fetch some extra data 10 | exports.fetchExtra = (gameID: string) => fetch(`https://api.rawg.io/api/games/${gameID}`); 11 | -------------------------------------------------------------------------------- /public/loading.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Vapor Store is loading 6 | 7 | 8 | 9 |
10 |
11 |

Vapor Store

12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "sourceMap": true, 14 | "baseUrl": ".", 15 | "types": ["webpack-env"], 16 | "paths": { 17 | "@/*": ["src/*"] 18 | }, 19 | "lib": ["esnext", "dom", "dom.iterable", "scripthost"] 20 | }, 21 | "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "tests/**/*.ts", "tests/**/*.tsx", "src/downloader/helper.js", "src/views/categories.js"], 22 | "exclude": ["node_modules"] 23 | } 24 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 12 | 13 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueRouter, {RouteConfig} from 'vue-router'; 3 | import Home from '../views/Home.vue'; 4 | 5 | Vue.use(VueRouter); 6 | 7 | const routes: Array = [ 8 | { 9 | path: '*', 10 | redirect: '/', 11 | }, 12 | { 13 | path: '/', 14 | name: 'Home', 15 | component: Home, 16 | }, 17 | { 18 | path: '/games', 19 | name: 'Games', 20 | component: () => import('../views/Games.vue'), 21 | }, 22 | { 23 | path: '/downloads', 24 | name: 'Downloads', 25 | component: () => import('../views/Downloads.vue'), 26 | }, 27 | { 28 | path: '/settings', 29 | name: 'Settings', 30 | component: () => import('../views/Settings.vue'), 31 | }, 32 | ]; 33 | 34 | const router = new VueRouter({ 35 | mode: 'history', 36 | base: process.env.BASE_URL, 37 | routes, 38 | }); 39 | 40 | export default router; 41 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build/release 2 | 3 | on: 4 | push: 5 | paths: 6 | - 'package.json' 7 | jobs: 8 | release: 9 | runs-on: ${{ matrix.os }} 10 | 11 | strategy: 12 | matrix: 13 | os: [windows-latest] 14 | 15 | steps: 16 | - name: Check out Git repository 17 | uses: actions/checkout@v1 18 | 19 | - name: Install Node.js, NPM and Yarn 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: 10 23 | 24 | - name: Build/release Electron app 25 | uses: samuelmeuli/action-electron-builder@v1 26 | with: 27 | # GitHub token, automatically provided to the action 28 | # (No need to define this secret in the repo settings) 29 | github_token: ${{ secrets.github_token }} 30 | skip_build: true 31 | use_vue_cli: true 32 | dist: 'dist_electron' 33 | args: "-p always" 34 | # If the commit is tagged with a version (e.g. "v1.0.0"), 35 | # release the app after building 36 | release: $(date +'%Y-%m-%d') -------------------------------------------------------------------------------- /src/workers/categories.worker.ts: -------------------------------------------------------------------------------- 1 | const ctx: Worker = self as any; 2 | ctx.addEventListener('message', (event) => { 3 | const games: object = event.data.games; 4 | const categories: object[] = event.data.categories; 5 | 6 | setInterval(() => ctx.postMessage(categories), 2000); 7 | 8 | setTimeout(() => ctx.postMessage(categories), 1000); 9 | 10 | (async () => { 11 | for (let [i, game] of Object.entries(games)) { 12 | try { 13 | if (!game.name) return; 14 | const request = await fetch(`https://api.rawg.io/api/games?search=${game.name}`); 15 | const returned = await request.json(); 16 | game = {...game, metadata: returned.results[0]}; 17 | const index = categories.findIndex((category: any) => category.name == game.metadata.genres[0].name); 18 | 19 | // @ts-ignore 20 | if (!categories[index].games.find((exist: object) => exist.metadata.id == game.metadata.id)) categories[index].games.push(game); 21 | } catch (err) { 22 | // ! It is normal for some errors to happen here 23 | } 24 | } 25 | ctx.postMessage('done'); 26 | })(); 27 | }); 28 | -------------------------------------------------------------------------------- /src/components/Services/Snackbar.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 55 | -------------------------------------------------------------------------------- /src/views/Downloads.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 36 | 37 | 46 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const WorkerPlugin = require('worker-plugin'); 2 | module.exports = { 3 | transpileDependencies: ['vuetify'], 4 | pluginOptions: { 5 | electronBuilder: { 6 | nodeIntegration: true, 7 | enableRemoteModule: true, 8 | externals: ['puppeteer', 'electron-dl'], 9 | 10 | builderOptions: { 11 | appId: 'vapor.store.vue', 12 | productName: 'Vapor Store Vue', 13 | asar: true, 14 | publish: [ 15 | { 16 | provider: 'github', 17 | owner: 'SushyDev', 18 | repo: 'vapor-store', 19 | releaseType: 'prerelease', 20 | }, 21 | ], 22 | nsis: { 23 | oneClick: false, 24 | perMachine: false, 25 | allowToChangeInstallationDirectory: true, 26 | }, 27 | win: { 28 | target: 'nsis', 29 | icon: 'public/favicon.png', 30 | }, 31 | linux: { 32 | icon: 'public/favicon.png', 33 | }, 34 | }, 35 | }, 36 | }, 37 | devServer: { 38 | disableHostCheck: true, 39 | port: '1234', 40 | }, 41 | configureWebpack: { 42 | plugins: [new WorkerPlugin()], 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vapor Store 2 2 | 3 | ```diff 4 | - WARNING: Vapor Store 2 is made to simplify downloading and installing games in a preinstalled format from the internet via a repository/source 5 | - I'm not responsible for the content of the sources our users may use 6 | - I am also not responsible for any legal troubles or computer issues you may face 7 | ``` 8 | 9 | ## Table of Contents 10 | - [Vapor Store 2](#vapor-store-2) 11 | - [Info](#info) 12 | - [Download](#download) 13 | - [Requirements](#requirements) 14 | - [How to set up](#how-to-set-up) 15 | - [How to build](#how-to-build-it-yourself) 16 | - [Roadmap](#roadmap) 17 | - [Ideas](#ideas) 18 | - [Issues](#issues) 19 | - [Support my work](#support-my-work) 20 | 21 | ## Info 22 | 23 | ### Download 24 | 25 | - [Releases Page](https://github.com/SushyDev/vapor-store/releases) 26 | - [Vapor Store](https://get-vapor.vercel.app/downloads.html) 27 | 28 | ### Requirements 29 | 30 | - [Vapor Store](https://get-vapor.vercel.app/downloads.html) 31 | - [A Repo file](https://discord.gg/ZjDTpmf) 32 | 33 | ### How to set up 34 | 35 | 1. Download & Install Vapor Store 36 | 2. Download a repo 37 | 3. Go to your Vapor Store Settings 38 | 4. Select the repo file 39 | 40 | ### How to build 41 | 42 | Building Vapor Store is very simple because its a Electron app, you simply need to download the code from git and run the build command 43 | 44 | ### Issues 45 | 46 | Please report 47 | 48 | If you're having any other issues / bugs join our [Discord](https://discord.gg/ZjDTpmf) to report a bug and get support or open an issue on this repository. 49 | 50 | ## Support my work 51 | 52 | - [Donate](https://ko-fi.com/sushy) 53 | -------------------------------------------------------------------------------- /src/components/Games/GameCard.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 46 | -------------------------------------------------------------------------------- /src/downloader/fetch.ts: -------------------------------------------------------------------------------- 1 | /* 2 | ! 3 | ! TSC giving errors about HTML Elements in the compiler 4 | ! 5 | */ 6 | 7 | // # Fetch download URL from page with Puppeteer 8 | export const fetchDownload = async (url: string) => { 9 | const puppeteer = require('puppeteer'); 10 | 11 | const getPath = () => (process.platform == 'linux' ? puppeteer.executablePath().replace('/electron/', '/puppeteer/') : puppeteer.executablePath().replace('app.asar', 'app.asar.unpacked')); 12 | 13 | const browser = await puppeteer.launch({executablePath: getPath()}); 14 | 15 | const page = await browser.newPage(); 16 | 17 | await page.goto(url); 18 | 19 | // ? Wait for download button visible 20 | await page.waitForSelector('.btn-download', {visible: true}); 21 | 22 | // ? Change button to redirect, then click 23 | await page.evaluate(() => { 24 | // @ts-ignore 25 | const button: HTMLAnchorElement = document.querySelector('.btn-download')!; 26 | button.target = '_self'; 27 | button.click(); 28 | return; 29 | }); 30 | 31 | // ? Wait until upload heaven loaded 32 | await page.waitForNavigation({waitUntil: 'networkidle0'}); 33 | // ? Wait for 5 second cooldown 34 | await page.waitForSelector('#downloadNowBtn', {visible: true}); 35 | 36 | // ? Click download button 37 | await page.evaluate(() => { 38 | // @ts-ignore 39 | const button: HTMLButtonElement = document.querySelector('#downloadNowBtn')!; 40 | button.click(); 41 | return; 42 | }); 43 | 44 | // ? Wait for redirect 45 | await page.waitForNavigation({waitUntil: 'networkidle0'}); 46 | 47 | // ? Get download URL from 'here' button 48 | const downloadURL: string | null = await page.evaluate(() => { 49 | return document.getElementsByClassName('alert-success')[0].getElementsByTagName('a')[0].href || null; 50 | }); 51 | 52 | await browser.close(); 53 | 54 | // ? Close chromium instance 55 | return downloadURL; 56 | }; 57 | -------------------------------------------------------------------------------- /public/loading.css: -------------------------------------------------------------------------------- 1 | body { 2 | width: 100vw; 3 | height: 100vh; 4 | padding: 0; 5 | margin: 0; 6 | } 7 | body .loading { 8 | width: 100vw; 9 | height: 100vh; 10 | background: #40c4ff; 11 | border-radius: 25px; 12 | z-index: 0; 13 | overflow: hidden; 14 | transform: translateZ(0); 15 | } 16 | body .overlay { 17 | position: absolute; 18 | width: 100vw; 19 | height: 100vh; 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | z-index: 2; 24 | } 25 | body .overlay p { 26 | color: white; 27 | font-size: 8.5vw; 28 | font-family: sans-serif; 29 | font-weight: bold; 30 | } 31 | body .ripple { 32 | z-index: 1; 33 | border: 1px solid transparent; 34 | } 35 | body .ripple .circle:nth-child(5) { 36 | width: 1000px; 37 | height: 1000px; 38 | left: -500px; 39 | bottom: -500px; 40 | opacity: 0.2; 41 | } 42 | body .ripple .circle:nth-child(4) { 43 | width: 800px; 44 | height: 800px; 45 | left: -400px; 46 | bottom: -400px; 47 | opacity: 0.5; 48 | } 49 | body .ripple .circle:nth-child(3) { 50 | width: 600px; 51 | height: 600px; 52 | left: -300px; 53 | bottom: -300px; 54 | opacity: 0.7; 55 | } 56 | body .ripple .circle:nth-child(2) { 57 | width: 400px; 58 | height: 400px; 59 | left: -200px; 60 | bottom: -200px; 61 | opacity: 0.8; 62 | } 63 | body .ripple .circle:nth-child(1) { 64 | width: 200px; 65 | height: 200px; 66 | left: -100px; 67 | bottom: -100px; 68 | opacity: 1; 69 | } 70 | body .ripple .circle { 71 | position: absolute; 72 | border-radius: 50%; 73 | background: #f57ba4; 74 | animation: ripple 3s infinite; 75 | box-shadow: 0px 0px 1px 0px #40c4ff; 76 | } 77 | @keyframes ripple { 78 | 0% { 79 | transform: scale(0.8); 80 | } 81 | 50% { 82 | transform: scale(1.2); 83 | } 84 | 100% { 85 | transform: scale(0.8); 86 | } 87 | } 88 | 89 | * { 90 | user-select: none; 91 | cursor: default; 92 | } 93 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vapor-store-vue", 3 | "version": "2.1.0-alpha-3", 4 | "description": "Vapor Store", 5 | "author": "SushyDev", 6 | "license": "GPL-3.0", 7 | "homepage": "https://github.com/SushyDev/vapor-store", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/SushyDev/vapor-store.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/SushyDev/vapor-store" 14 | }, 15 | "main": "background.js", 16 | "scripts": { 17 | "start": "npm run serve", 18 | "serve": "vue-cli-service electron:serve", 19 | "build": "vue-cli-service electron:build --win nsis", 20 | "getdeps": "npm i", 21 | "web:serve": "vue-cli-service serve", 22 | "web:build": "vue-cli-service build", 23 | "postinstall": "electron-builder install-app-deps", 24 | "postuninstall": "electron-builder install-app-deps" 25 | }, 26 | "dependencies": { 27 | "core-js": "^3.6.5", 28 | "fs": "^0.0.1-security", 29 | "node-downloader-helper": "^1.0.18", 30 | "puppeteer": "^8.0.0", 31 | "puppeteer-core": "^8.0.0", 32 | "puppeteer-in-electron": "^3.0.3", 33 | "vue": "^2.6.11", 34 | "vue-class-component": "^7.2.3", 35 | "vue-property-decorator": "^9.1.2", 36 | "vue-router": "^3.2.0", 37 | "vuetify": "^2.4.0", 38 | "vuex": "^3.4.0", 39 | "ws": "^6.2.1" 40 | }, 41 | "devDependencies": { 42 | "@types/electron-devtools-installer": "^2.2.0", 43 | "@types/puppeteer": "^5.4.3", 44 | "@vue/cli-plugin-babel": "~4.5.0", 45 | "@vue/cli-plugin-router": "~4.5.0", 46 | "@vue/cli-plugin-typescript": "~4.5.0", 47 | "@vue/cli-plugin-vuex": "~4.5.0", 48 | "@vue/cli-service": "~4.5.0", 49 | "electron": "^11.0.0", 50 | "electron-devtools-installer": "^3.1.0", 51 | "sass": "^1.32.0", 52 | "sass-loader": "^10.0.0", 53 | "typescript": "~4.1.5", 54 | "vue-cli-plugin-electron-builder": "~2.0.0-rc.6", 55 | "vue-cli-plugin-vuetify": "~2.3.1", 56 | "vue-template-compiler": "^2.6.11", 57 | "vuetify-loader": "^1.7.0", 58 | "worker-plugin": "^5.0.0" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/components/Downloads/DownloadItemCard.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 47 | 48 | 54 | -------------------------------------------------------------------------------- /src/views/Settings.vue: -------------------------------------------------------------------------------- 1 | 25 | 71 | -------------------------------------------------------------------------------- /src/components/AppBar/GameSearch.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 75 | -------------------------------------------------------------------------------- /src/background.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import {app, protocol, BrowserWindow, ipcMain} from 'electron'; 4 | import {createProtocol} from 'vue-cli-plugin-electron-builder/lib'; 5 | import installExtension, {VUEJS_DEVTOOLS} from 'electron-devtools-installer'; 6 | const isDev = process.env.NODE_ENV !== 'production'; 7 | 8 | // ? Scheme must be registered before the app is ready 9 | protocol.registerSchemesAsPrivileged([{scheme: 'app', privileges: {secure: true, standard: true}}]); 10 | 11 | async function spawnMain() { 12 | const win = new BrowserWindow({ 13 | frame: false, 14 | minWidth: 990, 15 | minHeight: 670, 16 | show: false, 17 | paintWhenInitiallyHidden: true, 18 | icon: 'public/favicon.png', 19 | webPreferences: { 20 | enableRemoteModule: true, 21 | backgroundThrottling: false, 22 | nodeIntegration: (process.env.ELECTRON_NODE_INTEGRATION as unknown) as boolean, 23 | }, 24 | }); 25 | 26 | win.setMenuBarVisibility(false); 27 | 28 | if (process.env.WEBPACK_DEV_SERVER_URL) { 29 | await win.loadURL(process.env.WEBPACK_DEV_SERVER_URL as string); 30 | 31 | // ? if (!process.env.IS_TEST) win.webContents.openDevTools(); 32 | } else { 33 | win.loadURL('app://./index.html'); 34 | } 35 | 36 | return win; 37 | } 38 | 39 | async function spawnLoading() { 40 | const win = new BrowserWindow({ 41 | frame: false, 42 | transparent: true, 43 | width: 500, 44 | height: 500, 45 | icon: 'public/favicon.png', 46 | }); 47 | 48 | win.setIgnoreMouseEvents(true); 49 | win.setMenuBarVisibility(false); 50 | win.setResizable(false); 51 | win.focus(); 52 | 53 | if (process.env.WEBPACK_DEV_SERVER_URL) { 54 | // ? Load the url of the dev server if in development mode 55 | await win.loadURL('http://localhost:1234/loading.html'); 56 | } else { 57 | createProtocol('app'); 58 | win.loadURL('app://./loading.html'); 59 | } 60 | 61 | return win; 62 | } 63 | 64 | app.on('window-all-closed', () => app.quit()); 65 | 66 | const startApp = async () => { 67 | const loading = await spawnLoading(); 68 | const main = await spawnMain(); 69 | 70 | ipcMain.once('loaded', () => { 71 | loading.close(); 72 | main.show(); 73 | }); 74 | 75 | checkForSingleInstance(main); 76 | }; 77 | 78 | app.on('ready', async () => { 79 | if (isDev && !process.env.IS_TEST) { 80 | // ? Install Vue Devtools 81 | try { 82 | await installExtension(VUEJS_DEVTOOLS); 83 | } catch (e) { 84 | console.error('Vue Devtools failed to install:', e.toString()); 85 | } 86 | } 87 | 88 | setTimeout(() => startApp(), process.platform === 'linux' ? 1000 : 0); 89 | }); 90 | 91 | if (isDev) { 92 | process.on('SIGTERM', () => { 93 | app.quit(); 94 | }); 95 | } 96 | 97 | // # Make app single instance 98 | function checkForSingleInstance(window: any) { 99 | const SingleInstance = app.requestSingleInstanceLock(); 100 | if (!SingleInstance) { 101 | app.quit(); 102 | } else { 103 | app.on('second-instance', () => { 104 | console.log('single'); 105 | if (!window) return; 106 | if (window.isMinimized()) window.restore(); 107 | window.focus(); 108 | }); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/modules/config.ts: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const {app} = require('electron').remote; 3 | const {ipcRenderer} = require('electron'); 4 | const path = require('path'); 5 | 6 | // ? Vapor Store files directory's 7 | export const vaporData = (): string => path.resolve(app.getPath('userData')); 8 | export const vaporFiles = (): string => path.join(vaporData(), 'Files'); 9 | export const vaporGames = (): string => path.join(vaporData(), 'Games'); 10 | export const configFolder = (): string => path.join(vaporFiles(), 'json'); 11 | 12 | // ? Files 13 | export const vaporConfig = (): string => path.join(configFolder(), 'config.json'); 14 | 15 | // # Startup checks 16 | export const initialize = function(): void { 17 | const defaults: object = {downloadDir: '' as string, darkMode: true as boolean, optBeta: false as boolean, autoExtract: true as boolean}; 18 | 19 | // # Check if config folder exists 20 | if (!fs.existsSync(configFolder())) { 21 | // ! Create folder (and subfolders if necessary) 22 | console.warn('Config folder does not exist!, creating folder tree...'); 23 | fs.mkdir(configFolder(), {recursive: true as boolean}, (err: Error) => { 24 | if (err) { 25 | console.error('Something went wrong creating folder tree'); 26 | console.error(err); 27 | } else { 28 | checkConfigFile(); 29 | } 30 | }); 31 | } else { 32 | checkConfigFile(); 33 | } 34 | 35 | // # Overwrite config file with defaults 36 | async function writeConfigFile() { 37 | try { 38 | fs.writeFileSync(vaporConfig(), JSON.stringify(defaults)); 39 | } catch (err) { 40 | console.error('Something went wrong creating the config file'); 41 | console.error(err); 42 | return; 43 | } 44 | console.warn('Successfully written config file'); 45 | validateConfigFile(); 46 | } 47 | 48 | // # Check if config file exists 49 | function checkConfigFile() { 50 | if (!fs.existsSync(vaporConfig())) { 51 | console.warn('Config file does not exist!, creating config file...'); 52 | writeConfigFile(); 53 | } else { 54 | validateConfigFile(); 55 | } 56 | } 57 | 58 | // # Check if config file is valid json 59 | async function validateConfigFile() { 60 | const data = await fs.readFileSync(vaporConfig(), 'UTF-8'); 61 | 62 | try { 63 | JSON.parse(data); 64 | } catch (err) { 65 | console.warn('Error reading config file, resetting...'); 66 | writeConfigFile(); 67 | return; 68 | } 69 | 70 | // ? Wait 50ms for IPCMain to start listening 71 | setTimeout(() => ipcRenderer.send('loaded'), 50); 72 | } 73 | }; 74 | 75 | // # Get current config contents 76 | export const get = (): object => JSON.parse(fs.readFileSync(vaporConfig(), 'UTF-8')); 77 | 78 | // # Add item to config 79 | export const setItem = (newItem: object) => { 80 | try { 81 | const data = fs.readFileSync(vaporConfig(), 'UTF-8'); 82 | const config: object = JSON.parse(data); 83 | const newConfig: object = {...config, ...newItem}; 84 | 85 | updateConfig(newConfig); 86 | } catch (err) { 87 | console.error('Something went wrong updating the config'); 88 | console.error(err); 89 | } 90 | }; 91 | 92 | // # Overwrite entire config with content 93 | async function updateConfig(content: object) { 94 | try { 95 | await fs.writeFileSync(vaporConfig(), JSON.stringify(content), 'UTF-8'); 96 | } catch (err) { 97 | console.error('Something went wrong updating the config'); 98 | console.error(err); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/views/Games.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 147 | -------------------------------------------------------------------------------- /src/downloader/index.ts: -------------------------------------------------------------------------------- 1 | const downloads: object[] | any[] = []; 2 | const ids: string[] = []; 3 | 4 | export const getDownloads = (): object[] => downloads; 5 | 6 | // # Gets called on 'Download' click in store 7 | export async function download(game: object | any) { 8 | const {SnackBus} = await import('@/event-bus'); 9 | 10 | SnackBus.$emit('new', { 11 | text: `Download for ${game.name} started`, 12 | duration: 4000, 13 | }); 14 | 15 | // ? If game id in ids's list don't continue 16 | if (ids.includes(game.metadata.id)) { 17 | SnackBus.$emit('new', { 18 | text: `${game.name} already downloading`, 19 | duration: 4000, 20 | }); 21 | return; 22 | } 23 | 24 | // ? Add game to downloads array 25 | addToDownloads(game); 26 | const index: number = await downloads.findIndex((download) => download.metadata.id === game.metadata.id); 27 | 28 | // ? Fetch download url 29 | const {fetchDownload} = await import('./fetch'); 30 | 31 | const downloadURL = await fetchDownload(game.url).catch((err) => { 32 | console.error(`Error fetching ${game.url}`); 33 | console.error(err); 34 | return null; 35 | }); 36 | 37 | // # If no download url 38 | if (!downloadURL) { 39 | removeFromDownloads(index); 40 | 41 | SnackBus.$emit('new', { 42 | text: `Error occured fetching URL for ${game.name}`, 43 | duration: 4000, 44 | actions: [ 45 | { 46 | text: 'Retry', 47 | click: () => require('@/downloader').download(game), 48 | }, 49 | ], 50 | }); 51 | 52 | return; 53 | } 54 | 55 | // ? Start download 56 | downloadProcess(downloadURL, index).catch((err) => { 57 | console.error(`Something went wrong downloading ${game.name}`); 58 | console.error(err); 59 | }); 60 | } 61 | 62 | /* 63 | ! 64 | ! Implement Pause/Cancel feature 65 | ! Later maybe implement resume after restart? 66 | ! 67 | ! See: 68 | ! https://github.com/hgouveia/node-downloader-helper 69 | ! 70 | */ 71 | 72 | // # Downloader process 73 | async function downloadProcess(url: string, index: number) { 74 | const {get} = await import('@/modules/config'); 75 | const {DownloaderHelper} = await import('node-downloader-helper'); 76 | 77 | // @ts-ignore // ! FIX? 78 | const itemDownloadDir = get().downloadDir; 79 | 80 | const downloaderOptions = { 81 | override: true, 82 | progressThrottle: 100, 83 | }; 84 | 85 | // ? Instanciate downloader 86 | const dl = new DownloaderHelper(url, itemDownloadDir, downloaderOptions); 87 | 88 | // ? Append downloader process to the game object 89 | downloads[index] = {...downloads[index], dl}; 90 | 91 | // ? Download events 92 | dl.on('download', (downloadInfo: object | any) => onDownload(downloadInfo)); 93 | dl.on('stateChanged', (state: object | any) => onChange(state)); 94 | dl.on('progress.throttled', (stats: object | any) => onProgress(stats)); 95 | dl.on('end', (downloadInfo: object | any) => onEnd(downloadInfo)); 96 | 97 | function onDownload(downloadInfo: object | any) {} 98 | 99 | function onProgress(stats: object | any) { 100 | try { 101 | const mbs: number = stats.speed / (1024 * 1024); 102 | const progress: number[] = downloads[index].progress; 103 | const values: number[] = downloads[index].values; 104 | 105 | values.push(mbs); 106 | if (values.length >= 51) values.shift(); 107 | 108 | // ! Strange hack to make progress reactive state 109 | progress.shift(); 110 | progress.push(stats.progress); 111 | 112 | // ? Push changes 113 | downloads[index] = {...downloads[index], values, progress}; 114 | } catch (err) { 115 | console.error(`Something went wrong updating download progress for ${downloads[index].name}`); 116 | console.error(err); 117 | } 118 | } 119 | 120 | function onEnd(downloadInfo: object | any) { 121 | console.log('Download Completed', downloadInfo); 122 | } 123 | 124 | function onChange(state: object | any) { 125 | console.warn(downloads[index].name, ' State: ', state); 126 | 127 | switch (state) { 128 | case 'STOPPED': 129 | onFinished(); 130 | } 131 | } 132 | 133 | function onFinished() { 134 | downloads[index] = {...downloads[index], removed: true}; 135 | removeFromDownloads(index); 136 | } 137 | 138 | // ? Start download 139 | dl.start().catch((err: any) => { 140 | console.error(`Something went wrong downloading ${downloads[index].name}`); 141 | console.error(err); 142 | }); 143 | } 144 | 145 | // # Add game to download arrays 146 | function addToDownloads(game: object | any) { 147 | ids.push(game.metadata.id); 148 | 149 | game = {...game, values: [0], progress: [0], removed: [false]}; 150 | 151 | downloads.push(game); 152 | } 153 | 154 | // # Remove game from array's 155 | function removeFromDownloads(index: number) { 156 | downloads.splice(index, 1); 157 | ids.splice(index, 1); 158 | } 159 | 160 | // # Cancel download 161 | export function cancel(game: object | any) { 162 | const index: number = downloads.findIndex((download) => download.metadata.id === game.metadata.id); 163 | 164 | downloads[index].dl.stop(); 165 | 166 | console.log('cancel', game); 167 | } 168 | 169 | // # Pause/Continue download 170 | export function pause(game: object | any) { 171 | const index: number = downloads.findIndex((download) => download.metadata.id === game.metadata.id); 172 | 173 | downloads[index].dl.pause(); 174 | 175 | setTimeout(() => downloads[index].dl.resume(), 1000); 176 | 177 | console.log('pause', game); 178 | } 179 | -------------------------------------------------------------------------------- /src/components/Games/GameOverview.vue: -------------------------------------------------------------------------------- 1 | 99 | 100 | 118 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 62 | 63 | 145 | 146 | 226 | --------------------------------------------------------------------------------