├── .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 |
2 |
5 |
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 |
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 |
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 |
2 |
3 | {{ text }}
4 |
5 |
6 |
7 |
8 | {{ action.text }}
9 |
10 |
11 |
12 |
13 |
14 |
15 |
55 |
--------------------------------------------------------------------------------
/src/views/Downloads.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
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 |
2 |
3 |
4 |
5 |
6 |
7 | {{ $attrs.game.metadata.name }}
8 |
9 | {{ $attrs.game.name }}
10 |
11 |
12 |
13 |
14 | Download
15 |
16 |
17 |
18 | More
19 |
20 |
21 |
22 |
23 |
24 |
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 |
2 |
3 |
4 |
5 | Fetching
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | mdi-download
16 |
17 | {{ $attrs.game.progress[0].toFixed(2) }}% - {{ $attrs.game.values[0].toFixed(2) }}MB/s
18 | Fetching...
19 |
20 |
21 | Cancel
22 | Pause
23 |
24 |
25 |
26 |
27 |
47 |
48 |
54 |
--------------------------------------------------------------------------------
/src/views/Settings.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | {{ downloadDir }}
15 |
16 |
17 |
18 |
19 |
20 | {{ dark ? 'Light Mode' : 'Dark Mode' }}
21 |
22 |
23 |
24 |
25 |
71 |
--------------------------------------------------------------------------------
/src/components/AppBar/GameSearch.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | {{ item.name.charAt(0) }}
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | Fix
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
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 |
2 |
3 |
4 |
5 |
6 |
7 | mdi-close
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | Download
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | Age Rating
46 | {{ $attrs.game.metadata.esrb_rating.name }}
47 |
48 |
49 |
50 |
51 |
52 | Released
53 | {{ $attrs.game.metadata.released }}
54 |
55 |
56 |
57 |
58 |
59 |
60 | Publisher
61 |
62 | {{ publisher.name }}
63 |
64 |
65 |
66 |
67 |
68 |
69 | Developers
70 |
71 | {{ developer.name }}
72 |
73 |
74 |
75 |
76 |
77 |
78 | Platforms
79 |
80 | {{ platform.platform.name }}
81 |
82 |
83 |
84 |
85 |
86 |
87 | Genres
88 |
89 | {{ genre.name }}
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
118 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {{ $appName || 'Vapor Store' }}
8 |
9 |
10 |
11 |
12 |
13 |
14 | {{ $appName }}
15 |
16 |
17 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | Join the Discord
35 |
36 |
37 |
38 |
39 |
40 |
41 | {{ item.icon }}
42 |
43 |
44 |
45 | {{ item.title }}
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
145 |
146 |
226 |
--------------------------------------------------------------------------------