8 | {{ title }} 9 |
10 |11 | {{ value }} 12 |
13 |Game | 8 |Key | 9 |
---|---|
{{ name }} | 12 |{{ key }} | 13 |
8 | {{ value }} 9 |
10 |{{ selectedBotsText }}
15 |{{ selectedPropertiesText }}
18 |{{ prettyConfig }}
19 |
20 | string
je bez osnovne vrednosti. Ova vrednost je potrebna i određuje ime bota - koristi se za identifikaciju unutar ASF-a. Mora biti posebna za svakog bota.",
64 | "next": "Sljedeće",
65 | "none": "Ništa",
66 | "other": "Drugo",
67 | "password": "Lozinka",
68 | "password-invalid": "Neispravna lozinka!",
69 | "performance": "Performanse",
70 | "plugins": "Dodaci",
71 | "released-ago-conjunction": " i ",
72 | "releases": "Izdanje",
73 | "releases-install": "Instaliraj ovu verziju",
74 | "releases-not-found": "Nijedno izdanje nije pronađeno!",
75 | "remote-access": "Daljinski pristup",
76 | "restart": "Ponovno pokretanje",
77 | "restart-initiated": "Ponovno pokretanje...",
78 | "save": "Spremi",
79 | "security": "Sigurnost",
80 | "settings-saved": "Podešavanja su sačuvana!",
81 | "setup": "Podešavanje",
82 | "shutdown": "Isključi",
83 | "shutdown-message": "Isključivanje, doviđenja!",
84 | "sidebar-dark-mode": "Tamna tema",
85 | "sidebar-theme": "Tema",
86 | "statistics": "Statistike",
87 | "statistics-memory-usage": "Korištenje memorije",
88 | "statistics-uptime": "Vrijeme rada",
89 | "success": "Uspješno"
90 | }
91 |
--------------------------------------------------------------------------------
/src/i18n/locale/ca-ES.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/src/i18n/locale/hr-HR.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/src/i18n/locale/no-NO.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import './public-path';
2 | import './app';
3 |
4 | // The __ASF_UI_LOADED__ property is used to determine whether the script loaded properly
5 | window.__ASF_UI_LOADED__ = true;
6 |
--------------------------------------------------------------------------------
/src/plugins/i18n.js:
--------------------------------------------------------------------------------
1 | import * as storage from '../utils/storage';
2 | import i18n from '../i18n/lib';
3 | import isAprilFoolsDay from '../utils/isAprilFoolsDay';
4 |
5 | // https://webpack.js.org/guides/dependency-management/#require-context
6 | const requireLocale = require.context('../i18n/locale', false, /.json$/, 'lazy');
7 |
8 | const availableLocales = requireLocale.keys().map(fileName => {
9 | if (fileName === './default.json') return { name: 'en-US', fileName };
10 | return { name: fileName.replace('./', '').replace('.json', ''), fileName };
11 | }).filter(Boolean);
12 |
13 | function getUserLocale(availableLocales, fallbackLocale) {
14 | const year = new Date().getFullYear();
15 | const fooled = storage.get(`fooled-${year}`, false);
16 | if (isAprilFoolsDay() && !fooled && availableLocales.includes('lol-US')) return 'lol-US';
17 |
18 | const selectedLocale = storage.get('locale');
19 | if (selectedLocale && availableLocales.includes(selectedLocale)) return selectedLocale;
20 |
21 | let locale = navigator.language;
22 | if (!locale) return fallbackLocale;
23 |
24 | if (availableLocales.includes(locale)) return locale;
25 |
26 | // Remove regional code, if present
27 | if (locale.includes('-')) {
28 | // eslint-disable-next-line prefer-destructuring
29 | locale = locale.split('-')[0];
30 | if (availableLocales.includes(locale)) return locale;
31 | }
32 |
33 | // Try default regional code
34 | if (availableLocales.includes(`${locale}-${locale.toUpperCase()}`)) return `${locale}-${locale.toUpperCase()}`;
35 |
36 | // Find locale with any regional code
37 | const localeRegex = new RegExp(`${locale}-\\S\\S`);
38 | const matchedLocale = availableLocales.find(locale => localeRegex.test(locale));
39 | if (matchedLocale) return matchedLocale;
40 |
41 | return fallbackLocale;
42 | }
43 |
44 | export default {
45 | install(Vue, store) {
46 | Vue.use(i18n, store, {
47 | locale: getUserLocale(availableLocales.map(locale => locale.name), 'en-US'),
48 | fallbackLocale: 'en-US',
49 | availableLocales,
50 | requireLocale,
51 | });
52 | },
53 | };
54 |
--------------------------------------------------------------------------------
/src/plugins/icons.js:
--------------------------------------------------------------------------------
1 | import { library } from '@fortawesome/fontawesome-svg-core';
2 | import { faGithub } from '@fortawesome/free-brands-svg-icons';
3 |
4 | import {
5 | faWrench, faBars, faLaptop, faUsers, faFileAlt, faTachometerAlt, faPowerOff, faPause, faCogs,
6 | faClock, faTimesCircle, faCheckCircle, faEdit, faTimes, faSquare, faMoon, faPalette, faPlay,
7 | faQuestion, faPlus, faSpinner, faKey, faTrash, faCloudDownloadAlt, faSignOutAlt, faAngleDown,
8 | faLanguage, faGamepad, faClone, faLock, faBookOpen, faCodeBranch, faHourglassEnd, faPaste,
9 | faHourglassHalf, faHourglassStart, faRedoAlt, faClipboard, faPuzzlePiece, faUndoAlt, faEye,
10 | faEyeSlash, faChevronLeft, faChevronRight, faExclamation, faComments, faBan,
11 | } from '@fortawesome/free-solid-svg-icons';
12 |
13 | import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome';
14 |
15 | library.add(
16 | faWrench,
17 | faBars,
18 | faLaptop,
19 | faUsers,
20 | faFileAlt,
21 | faTachometerAlt,
22 | faPowerOff,
23 | faPause,
24 | faCogs,
25 | faClock,
26 | faTimesCircle,
27 | faCheckCircle,
28 | faEdit,
29 | faTimes,
30 | faSquare,
31 | faMoon,
32 | faPalette,
33 | faPlay,
34 | faQuestion,
35 | faPlus,
36 | faSpinner,
37 | faKey,
38 | faTrash,
39 | faCloudDownloadAlt,
40 | faSignOutAlt,
41 | faAngleDown,
42 | faLanguage,
43 | faGamepad,
44 | faClone,
45 | faLock,
46 | faGithub,
47 | faBookOpen,
48 | faPaste,
49 | faExclamation,
50 | faCodeBranch,
51 | faHourglassEnd,
52 | faHourglassHalf,
53 | faHourglassStart,
54 | faRedoAlt,
55 | faClipboard,
56 | faPuzzlePiece,
57 | faUndoAlt,
58 | faEye,
59 | faEyeSlash,
60 | faChevronLeft,
61 | faChevronRight,
62 | faComments,
63 | faBan,
64 | );
65 |
66 | export default {
67 | install(Vue) {
68 | Vue.component('FontAwesomeIcon', FontAwesomeIcon);
69 | Vue.component('FontAwesomeLayers', FontAwesomeLayers);
70 | },
71 | };
72 |
--------------------------------------------------------------------------------
/src/plugins/notifications.js:
--------------------------------------------------------------------------------
1 | import Snotify from 'vue-snotify';
2 | import { get } from '../utils/storage';
3 |
4 | export default {
5 | install(Vue) {
6 | if (this.installed) return;
7 | this.installed = true;
8 |
9 | Vue.use(Snotify, {
10 | toast: {
11 | timeout: 3500,
12 | position: get('settings:notification-position', 'rightBottom'),
13 | pauseOnHover: true,
14 | },
15 | });
16 |
17 | Vue.prototype.$error = function notifyError(message) {
18 | Vue.prototype.$snotify.error(message, this.$t('error'));
19 | };
20 |
21 | Vue.prototype.$success = function notifySuccess(message) {
22 | Vue.prototype.$snotify.success(message, this.$t('success'));
23 | };
24 |
25 | Vue.prototype.$info = function notifyInfo(message) {
26 | Vue.prototype.$snotify.info(message, this.$t('info'));
27 | };
28 | },
29 | };
30 |
--------------------------------------------------------------------------------
/src/plugins/tooltips.js:
--------------------------------------------------------------------------------
1 | import VTooltip from 'v-tooltip';
2 | import { get } from '../utils/storage';
3 |
4 | export default {
5 | install(Vue) {
6 | if (this.installed) return;
7 | this.installed = true;
8 |
9 | Vue.use(VTooltip, {
10 | defaultDelay: {
11 | show: get('settings:tooltip-delay', 0),
12 | },
13 | });
14 | },
15 | };
16 |
--------------------------------------------------------------------------------
/src/public-path.js:
--------------------------------------------------------------------------------
1 | if (window.__BASE_PATH__) {
2 | // eslint-disable-next-line camelcase, no-undef
3 | __webpack_public_path__ = window.__BASE_PATH__;
4 | }
5 |
--------------------------------------------------------------------------------
/src/router/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import VueRouter from 'vue-router';
3 | import VueMeta from 'vue-meta';
4 | import store from '../store';
5 | import * as storage from '../utils/storage';
6 | import routes from './routes';
7 |
8 | Vue.use(VueRouter);
9 | Vue.use(VueMeta);
10 |
11 | const router = new VueRouter({
12 | routes,
13 | base: (window.__BASE_PATH__) ? window.__BASE_PATH__ : '/',
14 | mode: 'history',
15 | });
16 |
17 | router.beforeEach(async (routeTo, routeFrom, next) => {
18 | const noPasswordRequired = routeTo.matched.every(route => route.meta.noPasswordRequired);
19 | if (storage.get('first-time', true) && routeTo.name !== 'welcome') next({ name: 'welcome' });
20 | else if (noPasswordRequired || await store.dispatch('auth/validate')) next();
21 | else next({ name: 'setup' });
22 | });
23 |
24 | router.afterEach(to => {
25 | if (to.name === 'setup') return;
26 | storage.set('last-visited-page', to.name);
27 | });
28 |
29 | router.onError(err => {
30 | if (err.type === 'missing') window.location.reload();
31 | else throw err;
32 | });
33 |
34 | export default router;
35 |
--------------------------------------------------------------------------------
/src/static/images/defaultAvatar.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustArchiNET/ASF-ui/c0e3f384bbfe3c5c904ef13869b977756721b24b/src/static/images/defaultAvatar.jpg
--------------------------------------------------------------------------------
/src/static/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustArchiNET/ASF-ui/c0e3f384bbfe3c5c904ef13869b977756721b24b/src/static/images/logo.png
--------------------------------------------------------------------------------
/src/static/images/lol-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustArchiNET/ASF-ui/c0e3f384bbfe3c5c904ef13869b977756721b24b/src/static/images/lol-US.png
--------------------------------------------------------------------------------
/src/static/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ASF-ui",
3 | "short_name": "ASF-ui",
4 | "description": "The official web interface for ASF",
5 | "icons": [
6 | {
7 | "src": "images/logo.png",
8 | "sizes": "128x128",
9 | "type": "image/png"
10 | },
11 | {
12 | "src": "images/logo.png",
13 | "sizes": "192x192",
14 | "type": "image/png"
15 | },
16 | {
17 | "src": "images/logo.png",
18 | "sizes": "256x256",
19 | "type": "image/png"
20 | },
21 | {
22 | "src": "images/logo.png",
23 | "sizes": "512x512",
24 | "type": "image/png"
25 | }
26 | ],
27 | "display": "standalone",
28 | "background_color": "#222d32"
29 | }
30 |
--------------------------------------------------------------------------------
/src/static/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow: /
3 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import Vuex from 'vuex';
3 |
4 | import modules from './modules';
5 |
6 | Vue.use(Vuex);
7 |
8 | const store = new Vuex.Store({
9 | modules,
10 | strict: process.env.NODE_ENV !== 'production',
11 | });
12 |
13 | // Automatically run the `init` action for every module,
14 | // if one exists.
15 | Object.keys(modules).forEach(moduleName => {
16 | if (modules[moduleName].actions && modules[moduleName].actions.init) {
17 | store.dispatch(`${moduleName}/init`);
18 | }
19 | });
20 |
21 | store.watch((state, getters) => getters['auth/authenticated'], authenticated => {
22 | if (authenticated) {
23 | Object.keys(modules).forEach(moduleName => {
24 | if (modules[moduleName].actions && modules[moduleName].actions.onAuth) {
25 | store.dispatch(`${moduleName}/onAuth`);
26 | }
27 | });
28 | }
29 | });
30 |
31 | export default store;
32 |
--------------------------------------------------------------------------------
/src/store/modules/auth.js:
--------------------------------------------------------------------------------
1 | import { authenticate } from '../../plugins/http';
2 | import * as storage from '../../utils/storage';
3 | import { STATUS, getStatus } from '../../utils/getStatus';
4 |
5 | function createDefer() {
6 | const defer = {};
7 |
8 | defer.promise = new Promise((resolve, reject) => {
9 | defer.resolve = resolve;
10 | defer.reject = reject;
11 | });
12 |
13 | return defer;
14 | }
15 |
16 | const initializer = createDefer();
17 |
18 | export const state = {
19 | password: null,
20 | status: STATUS.NOT_CONNECTED,
21 | initialized: initializer.promise,
22 | };
23 |
24 | export const mutations = {
25 | setPassword: (state, password) => {
26 | state.password = password;
27 | authenticate(password);
28 | if (password) storage.set('ipc-password', password);
29 | else storage.remove('ipc-password');
30 | },
31 | setStatus: (state, status) => (state.status = status),
32 | };
33 |
34 | export const actions = {
35 | async init({ commit, dispatch }) {
36 | const password = storage.get('ipc-password');
37 | if (password) commit('setPassword', password);
38 | await dispatch('updateStatus');
39 | initializer.resolve();
40 | },
41 | async setPassword({ commit, dispatch }, password) {
42 | commit('setPassword', password);
43 | await dispatch('updateStatus');
44 | },
45 | async validate({ state, getters }) {
46 | await state.initialized;
47 | return getters.status === STATUS.AUTHENTICATED;
48 | },
49 | async updateStatus({ commit }) {
50 | const status = await getStatus();
51 | commit('setStatus', status);
52 | },
53 | };
54 |
55 | export const getters = {
56 | password: state => state.password,
57 | authenticated: state => state.status === STATUS.AUTHENTICATED,
58 | status: state => state.status,
59 | };
60 |
--------------------------------------------------------------------------------
/src/store/modules/bots.js:
--------------------------------------------------------------------------------
1 | import * as http from '../../plugins/http';
2 | import { Bot } from '../../models/Bot';
3 |
4 | export const state = {
5 | bots: {},
6 | };
7 |
8 | export const mutations = {
9 | setBots: (state, bots) => (state.bots = bots),
10 | setBot: (state, bot) => (state.bots[bot.name] = bot),
11 | updateBot: (state, { name, ...changes }) => {
12 | if (!state.bots[name]) return;
13 | Object.keys(changes).forEach(key => {
14 | state.bots[name][key] = changes[key];
15 | });
16 | },
17 | };
18 |
19 | export const actions = {
20 | init: async ({ dispatch }) => {
21 | setInterval(() => dispatch('updateBots'), 2500);
22 | },
23 | onAuth: async ({ dispatch }) => {
24 | dispatch('updateBots');
25 | },
26 | updateBots: async ({ dispatch, commit, rootGetters }) => {
27 | if (!rootGetters['auth/authenticated']) return;
28 |
29 | try {
30 | const response = await http.get('bot/asf');
31 | // eslint-disable-next-line no-sequences
32 | commit('setBots', Object.values(response).map(data => new Bot(data)).reduce((bots, bot) => ((bots[bot.name] = bot), bots), {}));
33 | } catch (err) {
34 | dispatch('auth/updateStatus', '', { root: true });
35 | }
36 | },
37 | async updateBot({ commit }, bot) {
38 | commit('updateBot', bot);
39 |
40 | try {
41 | const [response] = await http.get(`bot/${bot.name}`);
42 | commit('setBot', new Bot(response[bot.name]));
43 | } catch (err) {
44 | console.warn(err.message);
45 | }
46 | },
47 | async detectBots({ dispatch, getters }) {
48 | await dispatch('updateBots');
49 | return getters.bots.length !== 0;
50 | },
51 | };
52 |
53 | export const getters = {
54 | bots: state => Object.values(state.bots).sort((a, b) => (a.name > b.name ? 1 : -1)),
55 | bot: state => name => state.bots[name],
56 | status: (state, getters) => status => getters.bots.filter(bot => bot.status === status),
57 | count: (state, getters) => status => getters.status(status).length,
58 | gamesRemaining: (state, getters) => getters.bots.reduce((gamesRemaining, bot) => gamesRemaining + bot.gamesToFarm.length, 0),
59 | timeRemaining: (state, getters) => Math.max(...getters.bots.map(bot => bot.timeRemainingSeconds)),
60 | cardsRemaining: (state, getters) => getters.bots.reduce((cardsRemaining, bot) => cardsRemaining + bot.cardsRemaining, 0),
61 | botsFarmingCount: (state, getters) => getters.bots.filter(bot => bot.status === 'farming').length,
62 | };
63 |
--------------------------------------------------------------------------------
/src/store/modules/index.js:
--------------------------------------------------------------------------------
1 | // Register each file as a corresponding Vuex module. Module nesting
2 | // will mirror [sub-]directory hierarchy and modules are namespaced
3 | // as the camelCase equivalent of their file name.
4 |
5 | import { camelCase } from 'lodash-es';
6 |
7 | // https://webpack.js.org/guides/dependency-management/#require-context
8 | const requireModule = require.context(
9 | // Search for files in the current directory
10 | '.',
11 | // Search for files in subdirectories
12 | true,
13 | // Include any .js files that are not unit tests
14 | /^((?!\.unit\.).)*\.js$/,
15 | );
16 | const root = { modules: {} };
17 |
18 | requireModule.keys().forEach(fileName => {
19 | // Skip this file, as it's not a module
20 | if (fileName === './index.js') return;
21 |
22 | // Get the module path as an array
23 | const modulePath = fileName
24 | // Remove the "./" from the beginning
25 | .replace(/^\.\//, '')
26 | // Remove the file extension from the end
27 | .replace(/\.\w+$/, '')
28 | // Split nested modules into an array path
29 | .split(/\//)
30 | // camelCase all module namespaces and names
31 | .map(camelCase);
32 |
33 | // Get the modules object for the current path
34 | const { modules } = getNamespace(root, modulePath);
35 |
36 | // Add the module to our modules object
37 | modules[modulePath.pop()] = {
38 | // Modules are namespaced by default
39 | namespaced: true,
40 | ...requireModule(fileName),
41 | };
42 |
43 | // Recursively get the namespace of the module, even if nested
44 | function getNamespace(subtree, path) {
45 | if (path.length === 1) return subtree;
46 |
47 | const namespace = path.shift();
48 | subtree.modules[namespace] = { modules: {}, ...subtree.modules[namespace] };
49 | return getNamespace(subtree.modules[namespace], path);
50 | }
51 | });
52 |
53 | export default root.modules;
54 |
--------------------------------------------------------------------------------
/src/store/modules/layout.js:
--------------------------------------------------------------------------------
1 | import * as storage from '../../utils/storage';
2 |
3 | export const state = {
4 | smallNavigation: false,
5 | sideMenu: false,
6 | languageMenu: false,
7 | availableThemes: ['blue', 'red', 'teal', 'purple', 'green', 'orange'],
8 | boxed: false,
9 | };
10 |
11 | export const mutations = {
12 | setSmallNavigation: (state, value) => (state.smallNavigation = value),
13 | toggleNavigation: state => (state.smallNavigation = !state.smallNavigation),
14 | toggleSideMenu: state => (state.sideMenu = !state.sideMenu),
15 | toggleLanguageMenu: state => (state.languageMenu = !state.languageMenu),
16 | toggleBoxed: state => (state.boxed = !state.boxed),
17 | setBoxed: (state, value) => (state.boxed = value),
18 | setSideMenu: (state, value) => (state.sideMenu = value),
19 | setLanguageMenu: (state, value) => (state.languageMenu = value),
20 | };
21 |
22 | export const actions = {
23 | init: ({ commit }) => {
24 | const smallNavigation = storage.get('layout:small-navigation');
25 | if (typeof smallNavigation === 'boolean') commit('setSmallNavigation', smallNavigation);
26 | else if (window.innerWidth < 700) commit('setSmallNavigation', true);
27 |
28 | const boxed = storage.get('layout:boxed-layout');
29 | if (typeof boxed === 'boolean') commit('setBoxed', boxed);
30 | },
31 | toggleNavigation: ({ commit, getters }) => {
32 | commit('toggleNavigation');
33 | storage.set('layout:small-navigation', getters.smallNavigation);
34 | },
35 | toggleSideMenu: ({ commit, getters }) => {
36 | if (getters.languageMenu) commit('setLanguageMenu', false);
37 | commit('toggleSideMenu');
38 | },
39 | toggleLanguageMenu: ({ commit, getters }) => {
40 | if (getters.sideMenu) commit('setSideMenu', false);
41 | commit('toggleLanguageMenu');
42 | },
43 | toggleBoxed: ({ commit, getters }) => {
44 | commit('toggleBoxed');
45 | storage.set('layout:boxed-layout', getters.boxed);
46 | },
47 | setSideMenu: ({ commit, value }) => {
48 | commit('setSideMenu', value);
49 | },
50 | };
51 |
52 | export const getters = {
53 | smallNavigation: state => state.smallNavigation,
54 | sideMenu: state => state.sideMenu,
55 | languageMenu: state => state.languageMenu,
56 | availableThemes: state => state.availableThemes,
57 | boxed: state => state.boxed,
58 | };
59 |
--------------------------------------------------------------------------------
/src/store/modules/storage.js:
--------------------------------------------------------------------------------
1 | import * as http from '../../plugins/http';
2 | import * as storage from '../../utils/storage';
3 |
4 | export const state = {
5 | theme: 'blue',
6 | darkMode: false,
7 | };
8 |
9 | export const mutations = {
10 | changeTheme: (state, theme) => (state.theme = theme),
11 | setDarkMode: (state, value) => (state.darkMode = value),
12 | toggleDarkMode: state => (state.darkMode = !state.darkMode),
13 | };
14 |
15 | export const actions = {
16 | init: async ({ commit }) => {
17 | try {
18 | // get and set config from local storage
19 | const localTheme = storage.get('layout:theme');
20 | const localDarkMode = storage.get('layout:dark-mode');
21 | if (localTheme) commit('changeTheme', localTheme);
22 | if (typeof darkMode === 'boolean') commit('setDarkMode', localDarkMode);
23 | else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) commit('setDarkMode', true);
24 |
25 | // get and set config from ASF
26 | // local config will be overwritten if ASF config is available
27 | const response = await http.get('/storage/asfui-settings');
28 | const { theme: asfTheme, darkMode: asfDarkmode } = response;
29 | if (asfTheme) commit('changeTheme', asfTheme);
30 | if (asfDarkmode) commit('setDarkMode', asfDarkmode);
31 | } catch (err) {
32 | console.warn(err.message);
33 | }
34 | },
35 | changeTheme: ({ commit, state }, theme) => {
36 | commit('changeTheme', theme);
37 | storage.set('layout:theme', theme);
38 | http.post('/storage/asfui-settings', state);
39 | },
40 | toggleDarkMode: ({ commit, getters }) => {
41 | commit('toggleDarkMode');
42 | storage.set('layout:dark-mode', getters.darkMode);
43 | http.post('/storage/asfui-settings', state);
44 | },
45 | };
46 |
47 | export const getters = {
48 | theme: state => state.theme,
49 | darkMode: state => state.darkMode,
50 | };
51 |
--------------------------------------------------------------------------------
/src/style/_container.scss:
--------------------------------------------------------------------------------
1 | .main-container {
2 | height: 100%;
3 | overflow: auto;
4 | padding: 1em;
5 | }
6 |
7 | .main-container--bot-profile {
8 | width: 400px;
9 |
10 | @media screen and (max-width: 530px) {
11 | width: auto;
12 | }
13 | }
14 |
15 | .main-container--center {
16 | align-items: center;
17 | display: flex;
18 | justify-content: center;
19 | }
20 |
21 | .container {
22 | background: var(--color-background-light);
23 | border-radius: 3px;
24 | border-top: 3px solid var(--color-theme);
25 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
26 | margin-bottom: 1em;
27 | max-width: 100%;
28 | padding: 1em;
29 |
30 | &:last-child {
31 | margin-bottom: 0;
32 | }
33 | }
34 |
35 | .container--small {
36 | width: 600px;
37 | }
38 |
39 | .container--fullheight {
40 | box-sizing: border-box;
41 | height: 100%;
42 | }
43 |
--------------------------------------------------------------------------------
/src/style/_notification.scss:
--------------------------------------------------------------------------------
1 | @import 'vue-snotify/styles/_shared/snotify';
2 | @import 'vue-snotify/styles/_shared/animations';
3 | @import 'vue-snotify/styles/_shared/icons';
4 | @import 'vue-snotify/styles/dark/icon';
5 |
6 | $snotify-toast-bg: #fff !default;
7 | $snotify-toast-color: #000 !default;
8 | $snotify-toast-progressBar: #c7c7c7 !default;
9 | $snotify-toast-progressBarPercentage: #4c4c4c !default;
10 |
11 | $snotify-border-width: 4px !default;
12 | $snotify-simple-border-color: #000 !default;
13 | $snotify-success-border-color: #4caf50 !default;
14 | $snotify-info-border-color: #1e88e5 !default;
15 | $snotify-warning-border-color: #ff9800 !default;
16 | $snotify-error-border-color: #f44336 !default;
17 | $snotify-async-border-color: $snotify-info-border-color !default;
18 | $snotify-confirm-border-color: #009688 !default;
19 | $snotify-prompt-border-color: $snotify-confirm-border-color !default;
20 |
21 | .snotify {
22 | @media screen and (max-width: 600px) {
23 | left: 10px;
24 | right: 10px;
25 | width: auto;
26 | }
27 | }
28 |
29 | .snotifyToast {
30 | background-color: var(--color-navigation);
31 | cursor: pointer;
32 | display: block;
33 | height: 100%;
34 | margin: 5px;
35 | max-height: 300px;
36 | opacity: 0;
37 | overflow: hidden;
38 | pointer-events: auto;
39 |
40 | &--in {
41 | animation-name: appear;
42 | }
43 |
44 | &--out {
45 | animation-name: disappear;
46 | }
47 |
48 | &__inner {
49 | align-items: flex-start;
50 | color: var(--color-text);
51 | display: flex;
52 | flex-flow: column nowrap;
53 | font-size: 16px;
54 | justify-content: center;
55 | min-height: 78px;
56 | padding: 5px 65px 5px 15px;
57 | position: relative;
58 | }
59 |
60 | &__noIcon {
61 | padding: 5px 15px 5px 15px;
62 | }
63 |
64 | &__progressBar {
65 | background-color: var(--color-text-disabled);
66 | height: 5px;
67 | position: relative;
68 | width: 100%;
69 |
70 | &__percentage {
71 | background-color: var(--color-text-secondary);
72 | height: 5px;
73 | left: 0;
74 | max-width: 100%;
75 | position: absolute;
76 | top: 0;
77 | }
78 | }
79 |
80 | &__title {
81 | color: var(--color-text);
82 | font-size: 1.8em;
83 | line-height: 1.2em;
84 | margin-bottom: 5px;
85 | }
86 |
87 | &__body {
88 | color: var(--color-text);
89 | font-size: 1em;
90 | }
91 | }
92 |
93 | .snotifyToast-show {
94 | opacity: 1;
95 | transform: translate(0, 0);
96 | }
97 |
98 | .snotifyToast-remove {
99 | max-height: 0;
100 | opacity: 0;
101 | overflow: hidden;
102 | transform: translate(0, 50%);
103 | }
104 |
105 | /***************
106 | ** Modifiers **
107 | **************/
108 |
109 | .snotify-simple {
110 | border-left: $snotify-border-width solid $snotify-simple-border-color;
111 | }
112 |
113 | .snotify-success {
114 | border-left: $snotify-border-width solid $snotify-success-border-color;
115 | }
116 |
117 | .snotify-info {
118 | border-left: $snotify-border-width solid $snotify-info-border-color;
119 | }
120 |
121 | .snotify-warning {
122 | border-left: $snotify-border-width solid $snotify-warning-border-color;
123 | }
124 |
125 | .snotify-error {
126 | border-left: $snotify-border-width solid $snotify-error-border-color;
127 | }
128 |
129 | .snotify-async {
130 | border-left: $snotify-border-width solid $snotify-async-border-color;
131 | }
132 |
133 | .snotify-confirm {
134 | border-left: $snotify-border-width solid $snotify-confirm-border-color;
135 | }
136 |
137 | .snotify-prompt {
138 | border-left: $snotify-border-width solid $snotify-prompt-border-color;
139 | }
140 |
141 | .snotify-confirm,
142 | .snotify-prompt {
143 | .snotifyToast__inner {
144 | padding: 10px 15px;
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/src/style/_status.scss:
--------------------------------------------------------------------------------
1 | .status--disabled {
2 | --color-status: #{$color-status-disabled};
3 |
4 | .app--dark-mode & {
5 | --color-status: #{darken($color-status-disabled, 20)};
6 | }
7 | }
8 |
9 | .status--offline {
10 | --color-status: #{$color-status-offline};
11 |
12 | .app--dark-mode & {
13 | --color-status: #{darken($color-status-offline, 20)};
14 | }
15 | }
16 |
17 | .status--online {
18 | --color-status: #{$color-status-online};
19 |
20 | .app--dark-mode & {
21 | --color-status: #{darken($color-status-online, 20)};
22 | }
23 | }
24 |
25 | .status--farming {
26 | --color-status: #{$color-status-farming};
27 |
28 | .app--dark-mode & {
29 | --color-status: #{darken($color-status-farming, 20)};
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/style/_terminal.scss:
--------------------------------------------------------------------------------
1 | @import './settings';
2 |
3 | .terminal {
4 | background: black;
5 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
6 | box-sizing: border-box;
7 | color: var(--color-text);
8 | cursor: text;
9 | font-family: monospace, monospace;
10 | height: 100%;
11 | line-height: 1;
12 | overflow-y: auto;
13 | padding: 0.5em 1em;
14 | width: 100%;
15 |
16 | @media screen and (max-width: 600px) {
17 | font-size: 10px;
18 | word-break: break-word;
19 | }
20 | }
21 |
22 | .terminal-message {
23 | display: grid;
24 | grid-template-columns: auto auto 1fr;
25 | line-height: 1.1;
26 | margin: 0 0 0.1em;
27 | width: 100%;
28 |
29 | &.terminal-message--truncated {
30 | &:not(:hover) {
31 | .terminal-message__content {
32 | overflow: hidden;
33 | text-overflow: ellipsis;
34 | white-space: nowrap;
35 | }
36 | }
37 | }
38 | }
39 |
40 | .terminal-message__sign {
41 | color: var(--color-text-disabled);
42 | margin-right: 0.5em;
43 | }
44 |
45 | .terminal-message__sign--in {
46 | color: var(--color-theme);
47 | }
48 |
49 | .terminal-message__content {
50 | white-space: pre-wrap;
51 | }
52 |
53 | .terminal-message__time,
54 | .terminal-message__process {
55 | color: var(--color-text-disabled);
56 | }
57 |
58 | .terminal-message__level {
59 | &.terminal-message__level--info {
60 | color: $color-level-info;
61 | }
62 |
63 | &.terminal-message__level--debug {
64 | color: $color-level-debug;
65 | }
66 |
67 | &.terminal-message__level--error {
68 | color: $color-level-error;
69 | }
70 |
71 | &.terminal-message__level--warn {
72 | color: $color-level-warning;
73 | }
74 | }
75 |
76 | .terminal__input-wrapper {
77 | display: grid;
78 | grid-template-areas: 'sign text';
79 | grid-template-columns: auto 1fr;
80 | position: relative;
81 | width: 100%;
82 |
83 | .terminal-message__sign {
84 | color: var(--color-text);
85 | line-height: 1.3em;
86 | }
87 | }
88 |
89 | .terminal__input {
90 | background: transparent;
91 | border: none;
92 | box-sizing: border-box;
93 | color: var(--color-text);
94 | font-family: inherit;
95 | font-size: 100%;
96 | line-height: 1.3em;
97 | margin: 0;
98 | padding: 0;
99 | width: 100%;
100 |
101 | &:focus {
102 | outline: none;
103 | }
104 | }
105 |
106 | .terminal__input--autocomplete {
107 | color: var(--color-text-disabled);
108 | line-height: 1.3em;
109 | padding-left: 1.2em;
110 | pointer-events: none;
111 | position: absolute;
112 | }
113 |
--------------------------------------------------------------------------------
/src/style/_tooltip.scss:
--------------------------------------------------------------------------------
1 | .tooltip {
2 | display: block !important;
3 | max-width: 100%;
4 | font-size: 0.8em;
5 | z-index: 10000 !important;
6 |
7 | .tooltip-inner {
8 | background: var(--color-navigation);
9 | color: var(--color-text);
10 | border-radius: 4px;
11 | padding: 0.25em 0.75em;
12 | }
13 |
14 | .tooltip-arrow {
15 | width: 0;
16 | height: 0;
17 | border-style: solid;
18 | position: absolute;
19 | margin: 5px;
20 | border-color: var(--color-navigation);
21 | z-index: 1;
22 | }
23 |
24 | &[x-placement^="top"] {
25 | margin-bottom: 5px;
26 |
27 | .tooltip-arrow {
28 | border-width: 5px 5px 0 5px;
29 | border-left-color: transparent !important;
30 | border-right-color: transparent !important;
31 | border-bottom-color: transparent !important;
32 | bottom: -5px;
33 | left: calc(50% - 5px);
34 | margin-top: 0;
35 | margin-bottom: 0;
36 | }
37 | }
38 |
39 | &[x-placement^="bottom"] {
40 | margin-top: 5px;
41 |
42 | .tooltip-arrow {
43 | border-width: 0 5px 5px 5px;
44 | border-left-color: transparent !important;
45 | border-right-color: transparent !important;
46 | border-top-color: transparent !important;
47 | top: -5px;
48 | left: calc(50% - 5px);
49 | margin-top: 0;
50 | margin-bottom: 0;
51 | }
52 | }
53 |
54 | &[x-placement^="right"] {
55 | margin-left: 5px;
56 |
57 | .tooltip-arrow {
58 | border-width: 5px 5px 5px 0;
59 | border-left-color: transparent !important;
60 | border-top-color: transparent !important;
61 | border-bottom-color: transparent !important;
62 | left: -5px;
63 | top: calc(50% - 5px);
64 | margin-left: 0;
65 | margin-right: 0;
66 | }
67 | }
68 |
69 | &[x-placement^="left"] {
70 | margin-right: 5px;
71 |
72 | .tooltip-arrow {
73 | border-width: 5px 0 5px 5px;
74 | border-top-color: transparent !important;
75 | border-right-color: transparent !important;
76 | border-bottom-color: transparent !important;
77 | right: -5px;
78 | top: calc(50% - 5px);
79 | margin-left: 0;
80 | margin-right: 0;
81 | }
82 | }
83 |
84 | &[aria-hidden='true'] {
85 | visibility: hidden;
86 | opacity: 0;
87 | transition: opacity .15s, visibility .15s;
88 | }
89 |
90 | &[aria-hidden='false'] {
91 | visibility: visible;
92 | opacity: 1;
93 | transition: opacity .15s;
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/style/_typhography.scss:
--------------------------------------------------------------------------------
1 | .title {
2 | position: relative;
3 | text-align: center;
4 |
5 | &:after {
6 | border-bottom: 2px solid var(--color-theme);
7 | bottom: -0.25em;
8 | content: '';
9 | left: 50%;
10 | margin-left: -20px;
11 | position: absolute;
12 | width: 40px;
13 | }
14 | }
15 |
16 | .subtitle {
17 | text-align: center;
18 | }
19 |
20 | .info {
21 | text-align: center;
22 | margin: 1em;
23 | position: relative;
24 | }
25 |
26 | code {
27 | padding: .05em .3em;
28 | font-size: 85%;
29 | background-color: var(--color-releases-code);
30 | border-radius: 3px;
31 | }
32 |
33 | pre {
34 | white-space: pre-wrap;
35 | padding: 0.5em 1em;
36 | font-size: 90%;
37 | background-color: #2d333b;
38 | border-radius: 3px;
39 | color: var(--color-releases-pre);
40 |
41 | > code {
42 | background-color: inherit;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/style/_utility.scss:
--------------------------------------------------------------------------------
1 | .pull-right {
2 | margin-left: auto !important;
3 | }
4 |
5 | .hidden {
6 | display: none;
7 | }
8 |
--------------------------------------------------------------------------------
/src/style/components.scss:
--------------------------------------------------------------------------------
1 | @import 'settings';
2 | @import 'container';
3 | @import 'typhography';
4 | @import 'form';
5 | @import 'terminal';
6 | @import 'status';
7 | @import 'notification';
8 | @import 'utility';
9 | @import 'tooltip';
10 |
--------------------------------------------------------------------------------
/src/style/partials/_multiselect.scss:
--------------------------------------------------------------------------------
1 | @import '~vue-multiselect/dist/vue-multiselect.min.css';
2 |
3 | .multiselect {
4 | border: 1px solid rgba(var(--color-text-dark), 0.1);
5 | border-radius: 0.1875em;
6 |
7 | @media screen and (max-height: 835px), screen and (max-width: 1366px) {
8 | min-height: 20px;
9 | }
10 |
11 | @media screen and (max-height: 720px), screen and (max-width: 1000px) {
12 | min-height: 20px;
13 | }
14 | }
15 |
16 | .multiselect__select {
17 | @media screen and (max-height: 835px), screen and (max-width: 1366px) {
18 | height: 33px;
19 | }
20 |
21 | @media screen and (max-height: 720px), screen and (max-width: 1000px) {
22 | height: 28px;
23 | }
24 | }
25 |
26 | .multiselect,
27 | .multiselect__input,
28 | .multiselect__single {
29 | font-size: inherit;
30 | background: var(--color-background);
31 | }
32 |
33 | .multiselect__single {
34 | top: 2px;
35 | font-size: 14px;
36 |
37 | @media screen and (max-height: 835px), screen and (max-width: 1366px) {
38 | vertical-align: sub;
39 | font-size: 0.9375em;
40 | }
41 |
42 | @media screen and (max-height: 720px), screen and (max-width: 1000px) {
43 | vertical-align: top;
44 | }
45 | }
46 |
47 | .multiselect,
48 | .multiselect__input::placeholder,
49 | .multiselect__placeholder,
50 | .multiselect__option--selected.multiselect__option--highlight {
51 | color: var(--color-text-dark);
52 | }
53 |
54 | .multiselect__tags,
55 | .multiselect__spinner {
56 | background: var(--color-background);
57 | border-color: var(--color-border);
58 | }
59 |
60 | .multiselect__content-wrapper {
61 | top: 35px;
62 | background: var(--color-background);
63 | border-color: var(--color-border);
64 | color: var(--color-text-dark);
65 | }
66 |
67 | .multiselect__tags {
68 | border: none;
69 |
70 | @media screen and (max-height: 720px), screen and (max-width: 1000px) {
71 | min-height: 20px;
72 | height: 33px;
73 | padding: 5px 40px 0 8px;
74 | }
75 |
76 | @media screen and (max-height: 835px), screen and (max-width: 1366px) {
77 | min-height: 20px;
78 | height: 30px;
79 | padding: 5px 40px 0 8px;
80 | }
81 |
82 | > input {
83 | color: var(--color-text-dark);
84 | margin-left: -5px;
85 | margin-top: 0.7px;
86 | }
87 | }
88 |
89 | .multiselect__tag,
90 | .multiselect__tag-icon:focus,
91 | .multiselect__tag-icon:hover {
92 | background: var(--color-theme);
93 | }
94 |
95 | .multiselect__tag-icon:after {
96 | color: var(--color-button-cancel);
97 | font-size: 22px;
98 | }
99 |
100 | .multiselect__spinner:after,
101 | .multiselect__spinner:before {
102 | border-top-color: var(--color-theme);
103 | }
104 |
105 | .multiselect__option--highlight:after {
106 | color: var(--color-theme);
107 | background: var(--color-background-light);
108 | }
109 |
110 | .multiselect__option--highlight {
111 | background: var(--color-background-light);
112 | color: var(--color-theme);
113 | }
114 |
115 | .multiselect__option--selected {
116 | color: var(--color-theme);
117 | background: var(--color-background);
118 | }
119 |
120 | .multiselect__option--selected.multiselect__option--highlight:hover {
121 | color: var(--color-button-cancel);;
122 | font-weight: 700;
123 | }
124 |
125 | .multiselect__option--selected.multiselect__option--highlight,
126 | .multiselect__option--selected.multiselect__option--highlight:after {
127 | color: var(--color-theme);
128 | background: var(--color-background-light);
129 | font-weight: normal;
130 | }
131 |
132 | .multiselect__option--selected.multiselect__option--highlight:after {
133 | color: #f44336;
134 | }
135 |
--------------------------------------------------------------------------------
/src/style/settings.scss:
--------------------------------------------------------------------------------
1 | $color-theme-blue: #367fa9;
2 | $color-theme-red: #a92616;
3 | $color-theme-teal: #359392;
4 | $color-theme-purple: #504d8e;
5 | $color-theme-green: #00a65a;
6 | $color-theme-orange: #D58D18;
7 |
8 | $color-text: #fff;
9 | $color-text-dark: #111113;
10 | $color-text-info: #ff7300;
11 |
12 | $color-status-disabled: #bfc3cb;
13 | $color-status-offline: #898989;
14 | $color-status-online: #57cbde;
15 | $color-status-farming: #90ba3c;
16 |
17 | $size-navigation: 15rem;
18 | $size-navigation-small: 3rem;
19 |
20 | $color-level-debug: $color-status-offline;
21 | $color-level-info: $color-theme-teal;
22 | $color-level-error: $color-theme-red;
23 | $color-level-warning: $color-theme-orange;
24 |
--------------------------------------------------------------------------------
/src/utils/botExists.js:
--------------------------------------------------------------------------------
1 | export default function botExists(bots, name) {
2 | const targetBot = bots.filter(bot => bot.name === name);
3 | if (targetBot.length > 0) return true;
4 | return false;
5 | }
6 |
--------------------------------------------------------------------------------
/src/utils/compareVersion.js:
--------------------------------------------------------------------------------
1 | export default function compareVersion(a, b) {
2 | const aValues = a.split('.').map(v => parseInt(v, 10));
3 | const bValues = b.split('.').map(v => parseInt(v, 10));
4 |
5 | const versionLength = Math.max(aValues.length, bValues.length);
6 |
7 | for (let i = 0; i < versionLength; ++i) {
8 | const aValue = aValues[i] || 0;
9 | const bValue = bValues[i] || 0;
10 |
11 | if (aValue === bValue) continue;
12 | return (aValue > bValue) ? 1 : -1;
13 | }
14 |
15 | return 0;
16 | }
17 |
--------------------------------------------------------------------------------
/src/utils/configCategories.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 |
3 | const asfCategories = [
4 | { name: Vue.i18n.translate('basic'), fields: ['SteamOwnerID'] },
5 | { name: Vue.i18n.translate('trade'), fields: ['MaxTradeHoldDuration', 'FilterBadBots', 'LicenseID'] },
6 | { name: Vue.i18n.translate('customization'), fields: ['AutoRestart', 'Blacklist', 'CommandPrefix', 'CurrentCulture', 'SteamMessagePrefix'] },
7 | { name: Vue.i18n.translate('remote-access'), fields: ['Headless', 'IPC', 'IPCPassword', 'IPCPasswordFormat'] },
8 | { name: Vue.i18n.translate('connection'), fields: ['ConnectionTimeout', 'SteamProtocols', 'WebProxy', 'WebProxyPassword', 'WebProxyUsername'] },
9 | { name: Vue.i18n.translate('farming'), fields: ['FarmingDelay', 'IdleFarmingPeriod', 'MaxFarmingTime', 'MinFarmingDelayAfterBlock', 'ShutdownIfPossible'] },
10 | { name: Vue.i18n.translate('performance'), fields: ['OptimizationMode', 'ConfirmationsLimiterDelay', 'GiftsLimiterDelay', 'InventoryLimiterDelay', 'LoginLimiterDelay', 'WebLimiterDelay'] },
11 | { name: Vue.i18n.translate('updates'), fields: ['UpdateChannel', 'UpdatePeriod'] },
12 | { name: Vue.i18n.translate('plugins'), fields: ['PluginsUpdateMode', 'PluginsUpdateList'] },
13 | { name: Vue.i18n.translate('advanced'), fields: ['Debug', 'DefaultBot'] },
14 | ];
15 |
16 | const botCategories = [
17 | { name: Vue.i18n.translate('basic'), fields: ['Name', 'SteamLogin', 'SteamPassword', 'Enabled', 'OnlineStatus', 'BotBehaviour'] },
18 | { name: Vue.i18n.translate('security'), fields: ['PasswordFormat', 'UseLoginKeys'] },
19 | { name: Vue.i18n.translate('access'), fields: ['SteamUserPermissions', 'SteamParentalCode'] },
20 | { name: Vue.i18n.translate('trade'), fields: ['SteamTradeToken', 'AcceptGifts', 'TradeCheckPeriod', 'SendTradePeriod', 'CompleteTypesToSend', 'TradingPreferences', 'LootableTypes', 'TransferableTypes', 'MatchableTypes'] },
21 | { name: Vue.i18n.translate('farming'), fields: ['FarmingPreferences', 'FarmingOrders'] },
22 | { name: Vue.i18n.translate('customization'), fields: ['RemoteCommunication', 'SteamMasterClanID', 'UserInterfaceMode', 'OnlinePreferences', 'OnlineFlags', 'RedeemingPreferences', 'GamesPlayedWhileIdle', 'CustomGamePlayedWhileFarming', 'CustomGamePlayedWhileIdle'] },
23 | { name: Vue.i18n.translate('performance'), fields: ['HoursUntilCardDrops'] },
24 | ];
25 |
26 | const newBotCategories = [
27 | { name: Vue.i18n.translate('basic'), fields: ['Name', 'SteamLogin', 'SteamPassword'] },
28 | ];
29 |
30 | const uiCategories = [
31 | { name: Vue.i18n.translate('general'), fields: [Vue.i18n.translate('default-page'), Vue.i18n.translate('notification-position'), Vue.i18n.translate('notify-release'), Vue.i18n.translate('display-categories'), Vue.i18n.translate('tooltip-delay')] },
32 | { name: Vue.i18n.translate('bots'), fields: [Vue.i18n.translate('bot-nicknames'), Vue.i18n.translate('bot-game-name'), Vue.i18n.translate('bot-order-numeric'), Vue.i18n.translate('bot-order-disabled'), Vue.i18n.translate('bot-fav-buttons')] },
33 | { name: Vue.i18n.translate('commands'), fields: [Vue.i18n.translate('timestamps')] },
34 | { name: Vue.i18n.translate('log'), fields: [Vue.i18n.translate('log-previous-amount'), Vue.i18n.translate('log-information'), Vue.i18n.translate('log-timestamp')] },
35 | ];
36 |
37 | export {
38 | asfCategories,
39 | botCategories,
40 | newBotCategories,
41 | uiCategories,
42 | };
43 |
--------------------------------------------------------------------------------
/src/utils/createVirtualDOM.js:
--------------------------------------------------------------------------------
1 | export default function createVirtualDOM(html) {
2 | const virtualDocument = document.implementation.createHTMLDocument();
3 | const virtualDocumentHMLT = virtualDocument.createElement('html');
4 | virtualDocumentHMLT.innerHTML = html;
5 | return virtualDocumentHMLT;
6 | }
7 |
--------------------------------------------------------------------------------
/src/utils/delay.js:
--------------------------------------------------------------------------------
1 | export default function delay(ms = 1000) {
2 | return new Promise(resolve => setTimeout(resolve, ms));
3 | }
4 |
--------------------------------------------------------------------------------
/src/utils/download.js:
--------------------------------------------------------------------------------
1 | const sPropertyRegex = /"s_(\w+)":\s*"(\d+)"/g;
2 |
3 | function prepareModelForDownload(model) {
4 | return JSON.stringify(model, null, 2).replace(sPropertyRegex, '"$1":$2');
5 | }
6 |
7 | function handleDownload(data, prepareModel, name, extenstion) {
8 | const element = document.createElement('a');
9 | const config = (prepareModel) ? prepareModelForDownload(data) : data.join('\n');
10 | element.setAttribute('href', `data:text/plain;charset=utf-8,${encodeURIComponent(config)}`);
11 | element.setAttribute('download', `${name}.${extenstion}`);
12 | element.style.display = 'none';
13 | document.body.appendChild(element);
14 | element.click();
15 | document.body.removeChild(element);
16 | }
17 |
18 | export function downloadConfig(model, name) {
19 | handleDownload(model, true, name, 'json');
20 | }
21 |
22 | export function downloadLog(log) {
23 | handleDownload(log, false, 'log', 'txt');
24 | }
25 |
--------------------------------------------------------------------------------
/src/utils/fetchWiki.js:
--------------------------------------------------------------------------------
1 | import * as http from '../plugins/http';
2 | import getLocaleForWiki from './getLocaleForWiki';
3 |
4 | async function getEndpoint(page, version, locale) {
5 | const wikiLocale = getLocaleForWiki(locale);
6 | const defaultEndpoint = `www/github/wiki/page/${page}${wikiLocale}`;
7 |
8 | if (!version) return defaultEndpoint;
9 |
10 | const currentRelease = await http.get('www/github/release');
11 | if (version >= currentRelease.Version) return defaultEndpoint;
12 |
13 | const oldRelease = await http.get(`www/github/release/${version}`);
14 | const nextReleaseTime = new Date(oldRelease.ReleasedAt);
15 | const wikiHistory = await http.get(`www/github/wiki/history/${page}${wikiLocale}`);
16 |
17 | const wikiRevisions = Object.entries(wikiHistory).map(revision => ({
18 | id: revision[0],
19 | releaseTime: new Date(revision[1]),
20 | }));
21 |
22 | wikiRevisions.sort((a, b) => new Date(b.releaseTime) - new Date(a.releaseTime));
23 |
24 | const latestWikiRevision = wikiRevisions.find(({ releaseTime }) => releaseTime < nextReleaseTime);
25 | return (latestWikiRevision) ? `${defaultEndpoint}?revision=${latestWikiRevision.id}` : defaultEndpoint;
26 | }
27 |
28 | export default async function fetchWiki(page, version, locale) {
29 | const endpoint = await getEndpoint(page, version, locale);
30 | return http.get(endpoint);
31 | }
32 |
--------------------------------------------------------------------------------
/src/utils/getLocaleForHD.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import humanizeDuration from 'humanize-duration';
3 |
4 | export default function getLocaleForHD() {
5 | const supportedLanguages = humanizeDuration.getSupportedLanguages();
6 | const { locale, noRegionalLocale } = Vue.i18n;
7 |
8 | switch (locale) {
9 | case 'zh-CN':
10 | return 'zh_CN';
11 | case 'zh-TW':
12 | case 'zh-HK':
13 | return 'zh_TW';
14 | default:
15 | if (supportedLanguages.includes(noRegionalLocale)) return noRegionalLocale;
16 | return 'en';
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/utils/getLocaleForWiki.js:
--------------------------------------------------------------------------------
1 | export default function getLocaleForWiki(locale) {
2 | return (locale !== 'en-US') ? `-${locale}` : '';
3 | }
4 |
--------------------------------------------------------------------------------
/src/utils/getSelectedText.js:
--------------------------------------------------------------------------------
1 | export default function getSelectedText() {
2 | if ('getSelection' in window) return window.getSelection().toString();
3 | if ('selection' in document && document.selection.type === 'Text') return document.selection.createRange().text;
4 | return null;
5 | }
6 |
--------------------------------------------------------------------------------
/src/utils/getStatus.js:
--------------------------------------------------------------------------------
1 | import * as http from '../plugins/http';
2 | import * as storage from './storage';
3 |
4 | export const STATUS = {
5 | NOT_CONNECTED: 'NOT_CONNECTED',
6 | UNAUTHORIZED: 'UNAUTHORIZED',
7 | AUTHENTICATED: 'AUTHENTICATED',
8 | RATE_LIMITED: 'RATE_LIMITED',
9 | GATEWAY_TIMEOUT: 'GATEWAY_TIMEOUT',
10 | NETWORK_ERROR: 'NETWORK_ERROR',
11 | NO_IPC_PASSWORD: 'NO_IPC_PASSWORD',
12 | };
13 |
14 | export async function getStatus() {
15 | const authenticationRequired = storage.get('cache:authentication-required');
16 | if (authenticationRequired) {
17 | storage.remove('ipc-password');
18 | return STATUS.UNAUTHORIZED;
19 | }
20 |
21 | return http.get('asf')
22 | .then(response => {
23 | storage.remove('cache:authentication-required');
24 | return STATUS.AUTHENTICATED;
25 | })
26 | .catch(err => {
27 | if (err.message === 'HTTP Error 401') {
28 | storage.set('cache:authentication-required', true);
29 | return STATUS.UNAUTHORIZED;
30 | }
31 |
32 | if (err.message === 'HTTP Error 403') {
33 | const result = err.result.Result;
34 | if (result && result.Permanent) {
35 | // assume lack of IPCPassword since Result.Permanent is true
36 | return STATUS.NO_IPC_PASSWORD;
37 | }
38 |
39 | return STATUS.RATE_LIMITED;
40 | }
41 |
42 | if (err.message === 'HTTP Error 504') return STATUS.GATEWAY_TIMEOUT;
43 | if (err.message === 'Network Error') return STATUS.NETWORK_ERROR;
44 | return STATUS.NOT_CONNECTED;
45 | });
46 | }
47 |
--------------------------------------------------------------------------------
/src/utils/getUserInputType.js:
--------------------------------------------------------------------------------
1 | // Todo: Read EUserInputType from api
2 |
3 | const types = {
4 | None: 0,
5 | Login: 1,
6 | Password: 2,
7 | SteamGuard: 3,
8 | SteamParentalCode: 4,
9 | TwoFactorAuthentication: 5,
10 | };
11 |
12 | export default function getUserInputType(id) {
13 | return Object.keys(types).find(value => types[value] === id);
14 | }
15 |
--------------------------------------------------------------------------------
/src/utils/isAprilFoolsDay.js:
--------------------------------------------------------------------------------
1 | export default function isAprilFoolsDay() {
2 | const now = new Date();
3 | return (now.getMonth() === 3 && now.getDate() === 1);
4 | }
5 |
--------------------------------------------------------------------------------
/src/utils/isSameConfig.js:
--------------------------------------------------------------------------------
1 | import { isEqual } from 'lodash-es';
2 |
3 | export default function isSameConfig(newConfig, oldConfig) {
4 | // eslint-disable-next-line no-restricted-syntax
5 | for (const [property] of Object.entries(newConfig)) {
6 | let foundDifference = false;
7 |
8 | if (typeof oldConfig[property] === 'object') {
9 | // we want to use lodash for object comparison since JS sucks
10 | foundDifference = !isEqual(oldConfig[property], newConfig[property]);
11 | } else if (property.startsWith('s_')) {
12 | foundDifference = oldConfig[property] !== newConfig[property].toString();
13 | } else {
14 | foundDifference = oldConfig[property] !== newConfig[property];
15 | }
16 |
17 | if (foundDifference) return false;
18 | }
19 |
20 | return true;
21 | }
22 |
--------------------------------------------------------------------------------
/src/utils/loadParameterDescriptions.js:
--------------------------------------------------------------------------------
1 | import fetchWiki from './fetchWiki';
2 | import getLocaleForWiki from './getLocaleForWiki';
3 | import * as storage from './storage';
4 | import createVirtualDOM from './createVirtualDOM';
5 |
6 | export default async function loadParameterDescriptions(version, locale) {
7 | const descriptionsCache = storage.get(`cache:parameter-descriptions:${locale}`);
8 | if (descriptionsCache) {
9 | const { timestamp, descriptions } = descriptionsCache;
10 | if (timestamp > Date.now() - 6 * 60 * 60 * 1000) return descriptions;
11 | }
12 |
13 | const descriptions = {};
14 | const configWiki = await fetchWiki('Configuration', version, locale);
15 | const virtualDOM = createVirtualDOM(configWiki);
16 | const parametersHTML = Array.from(virtualDOM.querySelectorAll('h3 > code'));
17 |
18 | parametersHTML.forEach(parameterHTML => {
19 | const parameterName = parameterHTML.innerText;
20 | const parameterDescription = [];
21 | let description = parameterHTML.parentElement.parentElement.nextElementSibling;
22 |
23 | while (description && description.tagName.toLowerCase() !== 'hr') {
24 | const wikiLinks = description.querySelectorAll('a[href^="#"]');
25 | const wikiLocale = getLocaleForWiki(locale);
26 | fixWikiLinks(wikiLinks, 'Configuration', wikiLocale);
27 | parameterDescription.push(description.outerHTML);
28 | description = description.nextElementSibling;
29 | }
30 |
31 | descriptions[parameterName] = parameterDescription.join(' ');
32 | });
33 |
34 | storage.set(`cache:parameter-descriptions:${locale}`, { timestamp: Date.now(), descriptions });
35 |
36 | return descriptions;
37 | }
38 |
39 | export function fixWikiLinks(links, page, locale) {
40 | links.forEach(link => {
41 | if (!link) return;
42 |
43 | link.setAttribute('href', `https://github.com/JustArchiNET/ArchiSteamFarm/wiki/${page}${locale}${link.hash}`);
44 | link.setAttribute('target', '_blank');
45 | });
46 | }
47 |
--------------------------------------------------------------------------------
/src/utils/storage.js:
--------------------------------------------------------------------------------
1 | function generateKey(key) {
2 | return `asf-ui:${key}`;
3 | }
4 |
5 | export function set(key, value) {
6 | return localStorage.setItem(generateKey(key), JSON.stringify(value));
7 | }
8 |
9 | export function get(key, defaultValue) {
10 | const storedValue = localStorage.getItem(generateKey(key));
11 | if (!storedValue) return defaultValue;
12 | try { return JSON.parse(storedValue); } catch (err) { return storedValue; }
13 | }
14 |
15 | export function remove(key) {
16 | return localStorage.removeItem(generateKey(key));
17 | }
18 |
--------------------------------------------------------------------------------
/src/utils/swagger/dereference.js:
--------------------------------------------------------------------------------
1 | import { cloneDeep, get, isObject } from 'lodash-es';
2 |
3 | function isRef(node) {
4 | return node?.$ref;
5 | }
6 |
7 | function resolveRef(path, schema) {
8 | const lodashPath = path.substr(2).replace(/\//g, '.');
9 | return get(schema, lodashPath);
10 | }
11 |
12 | function resolve(tree, schema, resolved = new WeakSet()) {
13 | if (resolved.has(tree)) return; // Prevent infinite loop
14 | resolved.add(tree);
15 |
16 | for (const key of Object.keys(tree)) {
17 | if (isRef(tree[key])) tree[key] = resolveRef(tree[key].$ref, schema);
18 | if (isObject(tree[key])) resolve(tree[key], schema, resolved);
19 | }
20 | }
21 |
22 | export function dereference(schema) {
23 | const localSchema = cloneDeep(schema);
24 | resolve(localSchema, localSchema);
25 | return localSchema;
26 | }
27 |
--------------------------------------------------------------------------------
/src/utils/swagger/parse.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { dereference } from './dereference';
3 |
4 | const endpoint = `${window.__BASE_PATH__ ?? '/'}swagger/ASF/swagger.json`;
5 | let schema;
6 |
7 | async function getSchema() {
8 | if (schema) return schema;
9 |
10 | // We save the PROMISE, not the VALUE, in a variable.
11 | // This is very important, as ALL future calls will retrieve this promise (including those made while promise is still pending).
12 | // Such approach, ensures no duplicated HTTP calls are made.
13 | schema = axios.get(endpoint)
14 | .then(response => response.data)
15 | .then(schema => dereference(schema));
16 |
17 | return schema;
18 | }
19 |
20 | export async function getType(name) {
21 | const schema = await getSchema();
22 | const { [name]: type } = schema.components.schemas;
23 | return type.properties;
24 | }
25 |
26 | export async function getDefinitions(name) {
27 | const schema = await getSchema();
28 | return schema.components.schemas[name]['x-definition'];
29 | }
30 |
--------------------------------------------------------------------------------
/src/utils/ui.js:
--------------------------------------------------------------------------------
1 | import * as http from '../plugins/http';
2 | import { state as asf, UPDATECHANNEL } from '../store/modules/asf';
3 | import { set, get } from './storage';
4 |
5 | // eslint-disable-next-line no-undef
6 | export const ui = { gitCommitHash: APP_HASH };
7 |
8 | export async function isReleaseAvailable() {
9 | const lastChecked = get('last-checked-for-update');
10 |
11 | if (lastChecked && (lastChecked > (Date.now() - 60 * 60 * 1000))) {
12 | const latestCachedVersion = get('latest-release');
13 | return (latestCachedVersion > asf.version);
14 | }
15 |
16 | const endpoint = (asf.updateChannel === UPDATECHANNEL.PRERELEASE) ? 'www/github/release' : 'www/github/release/latest';
17 | const release = await http.get(endpoint);
18 |
19 | set('latest-release', release.Version);
20 | set('last-checked-for-update', Date.now());
21 |
22 | return (release.Version > asf.version);
23 | }
24 |
--------------------------------------------------------------------------------
/src/utils/validator.js:
--------------------------------------------------------------------------------
1 | const steamidRegex = /^[1-9][0-9]{16,17}$/;
2 |
3 | function isNumber(value) {
4 | return (`${value}`).split('').every(n => !Number.isNaN(n));
5 | }
6 |
7 | export function steamid() {
8 | return function validate(value) {
9 | const errors = [];
10 |
11 | if (!isNumber(value)) errors.push('not a number');
12 | if (!steamidRegex.test(`${value}`) && `${value}` !== '0') errors.push('not valid steamid');
13 |
14 | return errors;
15 | };
16 | }
17 |
18 | function limitedNumber(min = 0, max) {
19 | return function validate(value) {
20 | const errors = [];
21 |
22 | if (!isNumber(value)) errors.push('not a number');
23 | if (value < min) errors.push(`lesser than allowed (${min})`);
24 | if (value > max) errors.push(`greater than allowed (${max})`);
25 |
26 | return errors;
27 | };
28 | }
29 |
30 | export default {
31 | byte: limitedNumber(0, 255),
32 | uint16: limitedNumber(0, 65535),
33 | uint32: limitedNumber(0, 4294967295),
34 | uint64: steamid(),
35 | };
36 |
--------------------------------------------------------------------------------
/src/utils/waitForRestart.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import delay from './delay';
3 | import store from '../store';
4 |
5 | export default async function waitForRestart(timeout = 120000) {
6 | const timeStarted = Date.now();
7 |
8 | while (timeStarted > Date.now() - timeout) {
9 | await store.dispatch('asf/update');
10 | if (Date.now() - store.getters['asf/startTime'].getTime() < 10000) return;
11 | await delay(1000);
12 | }
13 |
14 | throw new Error(Vue.i18n.translate('restart-failure'));
15 | }
16 |
--------------------------------------------------------------------------------
/src/views/Bots.vue:
--------------------------------------------------------------------------------
1 |
2 | {{ $t('welcome-message') }}
6 | 7 |{{ $t('welcome-message-todo') }}
9 |