├── .editorconfig ├── .eslintrc ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── Bug-report.yml │ ├── Enhancement-idea.yml │ ├── Question.yml │ ├── Wiki-suggestion.yml │ └── config.yml ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── SECURITY.md ├── crowdin.yml ├── previews │ ├── README.md │ ├── bots.png │ └── commands.png ├── renovate.json5 └── workflows │ ├── ci.yml │ ├── crowdin-ci.yml │ ├── lock-threads.yml │ └── translations.yml ├── .gitignore ├── LICENSE ├── generated └── .gitignore ├── package-lock.json ├── package.json ├── scripts ├── generateFlags.js ├── getCommitHash.js └── reverse-proxy │ ├── index.js │ ├── package-lock.json │ └── package.json ├── src ├── App.vue ├── app.js ├── components │ ├── ASF │ │ ├── Bots.vue │ │ ├── BotsFilter.vue │ │ ├── FarmingInfo.vue │ │ ├── FarmingInfoCard.vue │ │ ├── Log.vue │ │ ├── Plugins.vue │ │ └── Releases.vue │ ├── App │ │ ├── Footer.vue │ │ ├── Header.vue │ │ ├── Modal.vue │ │ ├── Navigation.vue │ │ ├── SideMenu.vue │ │ └── partials │ │ │ ├── FooterLink.vue │ │ │ └── SideMenuSwitch.vue │ ├── BGR │ │ ├── Check.vue │ │ ├── Input.vue │ │ ├── Keys.vue │ │ ├── Reset.vue │ │ ├── Status.vue │ │ └── Summary.vue │ ├── Bot │ │ ├── Action.vue │ │ ├── Card.vue │ │ ├── FarmingInfo.vue │ │ ├── Games.vue │ │ └── Link.vue │ ├── Config │ │ ├── Category.vue │ │ ├── Editor.vue │ │ └── Fields │ │ │ ├── Input.vue │ │ │ ├── InputBoolean.vue │ │ │ ├── InputDescription.vue │ │ │ ├── InputDictionary.vue │ │ │ ├── InputEnum.vue │ │ │ ├── InputFlag.vue │ │ │ ├── InputLabel.vue │ │ │ ├── InputList.vue │ │ │ ├── InputNumber.vue │ │ │ ├── InputSelect.vue │ │ │ ├── InputSet.vue │ │ │ ├── InputString.vue │ │ │ ├── InputTag.vue │ │ │ └── InputUnknown.vue │ ├── MassEditor │ │ ├── Bots.vue │ │ ├── Check.vue │ │ ├── Select.vue │ │ ├── Steps.vue │ │ ├── Value.vue │ │ └── partials │ │ │ └── BotsView.vue │ ├── Navigation │ │ ├── Bots.vue │ │ ├── Brand.vue │ │ ├── CategoryTitle.vue │ │ ├── LanguageSwitch.vue │ │ ├── Link.vue │ │ ├── Statistic.vue │ │ └── Statistics.vue │ └── utils │ │ ├── FitText.vue │ │ └── Flag.vue ├── i18n │ ├── lib │ │ ├── README.md │ │ ├── formatter.js │ │ ├── index.js │ │ └── store.js │ └── locale │ │ ├── README.md │ │ ├── be-BY.json │ │ ├── bg-BG.json │ │ ├── bs-BA.json │ │ ├── ca-ES.json │ │ ├── cs-CZ.json │ │ ├── da-DK.json │ │ ├── de-DE.json │ │ ├── default.json │ │ ├── el-GR.json │ │ ├── es-ES.json │ │ ├── fa-IR.json │ │ ├── fi-FI.json │ │ ├── fr-FR.json │ │ ├── he-IL.json │ │ ├── hr-HR.json │ │ ├── hu-HU.json │ │ ├── id-ID.json │ │ ├── it-IT.json │ │ ├── ja-JP.json │ │ ├── ka-GE.json │ │ ├── ko-KR.json │ │ ├── lol-US.json │ │ ├── lt-LT.json │ │ ├── lv-LV.json │ │ ├── nl-NL.json │ │ ├── no-NO.json │ │ ├── pl-PL.json │ │ ├── pt-BR.json │ │ ├── pt-PT.json │ │ ├── ro-RO.json │ │ ├── ru-RU.json │ │ ├── sk-SK.json │ │ ├── sr-CS.json │ │ ├── sv-SE.json │ │ ├── th-TH.json │ │ ├── tr-TR.json │ │ ├── uk-UA.json │ │ ├── vi-VN.json │ │ ├── zh-CN.json │ │ ├── zh-HK.json │ │ └── zh-TW.json ├── index.html ├── index.js ├── models │ └── Bot.js ├── plugins │ ├── http.js │ ├── i18n.js │ ├── icons.js │ ├── notifications.js │ └── tooltips.js ├── public-path.js ├── router │ ├── index.js │ └── routes.js ├── static │ ├── images │ │ ├── defaultAvatar.jpg │ │ ├── logo.png │ │ └── lol-US.png │ ├── manifest.json │ └── robots.txt ├── store │ ├── index.js │ └── modules │ │ ├── asf.js │ │ ├── auth.js │ │ ├── bots.js │ │ ├── index.js │ │ ├── layout.js │ │ ├── settings.js │ │ └── storage.js ├── style │ ├── _container.scss │ ├── _form.scss │ ├── _notification.scss │ ├── _status.scss │ ├── _terminal.scss │ ├── _tooltip.scss │ ├── _typhography.scss │ ├── _utility.scss │ ├── components.scss │ ├── partials │ │ └── _multiselect.scss │ └── settings.scss ├── utils │ ├── botExists.js │ ├── compareVersion.js │ ├── configCategories.js │ ├── createVirtualDOM.js │ ├── delay.js │ ├── download.js │ ├── fetchConfigSchema.js │ ├── fetchWiki.js │ ├── getLocaleForHD.js │ ├── getLocaleForWiki.js │ ├── getSelectedText.js │ ├── getStatus.js │ ├── getUserInputType.js │ ├── isAprilFoolsDay.js │ ├── isSameConfig.js │ ├── loadParameterDescriptions.js │ ├── storage.js │ ├── swagger │ │ ├── dereference.js │ │ └── parse.js │ ├── ui.js │ ├── validator.js │ └── waitForRestart.js └── views │ ├── ASFBans.vue │ ├── ASFConfig.vue │ ├── Bots.vue │ ├── Commands.vue │ ├── Log.vue │ ├── MassEditor.vue │ ├── Plugins.vue │ ├── Releases.vue │ ├── Setup.vue │ ├── UIConfig.vue │ ├── Welcome.vue │ └── modals │ ├── Bot.vue │ ├── Bot2FA.vue │ ├── Bot2FADelete.vue │ ├── BotBGR.vue │ ├── BotConfig.vue │ ├── BotCopy.vue │ ├── BotCreate.vue │ ├── BotDelete.vue │ ├── BotInput.vue │ ├── PasswordEncrypt.vue │ └── PasswordHash.vue └── webpack ├── config.analyze.js ├── config.ci.js ├── config.deploy.js ├── config.js └── config.prod.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:vue/recommended", 9 | "airbnb-base" 10 | ], 11 | "globals": { 12 | "Atomics": "readonly", 13 | "SharedArrayBuffer": "readonly" 14 | }, 15 | "parser": "vue-eslint-parser", 16 | "parserOptions": { 17 | "parser": "@babel/eslint-parser", 18 | "requireConfigFile": false, 19 | "sourceType": "module" 20 | }, 21 | "plugins": [ 22 | "vue" 23 | ], 24 | "rules": { 25 | "indent": ["error", 2, { 26 | "SwitchCase": 1 27 | }], 28 | "space-before-function-paren": ["error", { 29 | "anonymous": "never", 30 | "named": "never", 31 | "asyncArrow": "always" 32 | }], 33 | "prefer-template": "warn", 34 | "arrow-parens": ["error", "as-needed"], 35 | "max-len": "off", 36 | "consistent-return": "off", 37 | "no-param-reassign": ["error", { 38 | "props": false 39 | }], 40 | "no-return-assign": ["error", "except-parens"], 41 | "linebreak-style": "off", 42 | "quote-props": ["warn", "consistent-as-needed"], 43 | "no-tabs": ["error", { 44 | "allowIndentationTabs": true 45 | }], 46 | "no-use-before-define": ["error", { 47 | "functions": false 48 | }], 49 | "no-unused-vars": ["warn", { 50 | "vars": "local", 51 | "args": "none" 52 | }], 53 | "no-await-in-loop": "off", 54 | "no-shadow": "off", 55 | "no-plusplus": "off", 56 | "no-continue": "off", 57 | "no-console": "off", 58 | "no-alert": "off", 59 | "no-underscore-dangle": "off", 60 | "import/no-extraneous-dependencies": ["error", { 61 | "devDependencies": true 62 | }], 63 | "no-bitwise": ["error", { 64 | "allow": ["~", "&=", "|=", "<<"] 65 | }], 66 | "default-case": "off", 67 | "vue/script-indent": ["error", 2, { 68 | "baseIndent": 1, 69 | "switchCase": 1 70 | }], 71 | "vue/html-indent": ["error", 2], 72 | "vue/max-attributes-per-line": "off", 73 | "vue/html-self-closing": ["error", { 74 | "html": { 75 | "void": "never", 76 | "normal": "never", 77 | "component": "never" 78 | } 79 | }], 80 | "vue/singleline-html-element-content-newline": "off", 81 | "vue/comma-dangle": "error", 82 | "vue/no-v-html": "off", 83 | "vue/require-default-prop": "off", 84 | "vue/require-prop-types": "off", 85 | "vue/return-in-computed-property": "off", 86 | "vue/no-use-v-if-with-v-for": "off", 87 | "vue/attribute-hyphenation": ["error", "never"] 88 | }, 89 | "overrides": [{ 90 | "files": ["*.vue"], 91 | "rules": { 92 | "indent": "off" 93 | } 94 | }] 95 | } 96 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Enhancement-idea.yml: -------------------------------------------------------------------------------- 1 | name: ✨ Enhancement idea 2 | description: Suggest an idea to improve ASF-ui 3 | labels: ["✨ Enhancement", "🧐 Evaluation"] 4 | body: 5 | - type: checkboxes 6 | id: checklist 7 | attributes: 8 | label: Checklist 9 | description: Ensure that our enhancement idea form is appropriate for you. 10 | options: 11 | - label: I read and understood ASF-ui's **[Contributing Guidelines](https://github.com/JustArchiNET/ASF-ui/blob/main/.github/CONTRIBUTING.md)** 12 | required: true 13 | - label: I also read **[FAQ](https://github.com/JustArchiNET/ASF-ui/wiki/FAQ)** 14 | required: true 15 | - label: My idea doesn't duplicate existing ASF-ui functionality described on the **[wiki](https://github.com/JustArchiNET/ASF-ui/wiki)** 16 | required: true 17 | - label: I believe that my idea falls into ASF-ui's scope and should be offered as part of ASF-ui built-in functionality 18 | required: true 19 | - label: This is not **[ASF suggestion](https://github.com/JustArchiNET/ArchiSteamFarm/issues/new/choose)** 20 | required: true 21 | - type: textarea 22 | id: enhancement-purpose 23 | attributes: 24 | label: Enhancement purpose 25 | description: Purpose of the enhancement - if it solves some problem, precise in particular which. If it benefits ASF-ui in some other way, precise in particular why. 26 | placeholder: Present the underlying reason why this enhancement makes sense, and what is the context of it. 27 | validations: 28 | required: true 29 | - type: textarea 30 | id: solution 31 | attributes: 32 | label: Solution 33 | description: What would you like to see as a solution to the purpose specified by you above? 34 | placeholder: What would work for you? 35 | validations: 36 | required: true 37 | - type: textarea 38 | id: why-existing-not-sufficient 39 | attributes: 40 | label: Why currently available solutions are not sufficient? 41 | description: Evaluate the existing solutions in regards to your requirements. 42 | placeholder: | 43 | If something you're suggesting is already possible, then explain to us why the currently available solutions are not sufficient. 44 | 45 | If it's not possible yet, then explain to us why it should be. 46 | validations: 47 | required: true 48 | - type: dropdown 49 | id: help 50 | attributes: 51 | label: Can you help us with this enhancement idea? 52 | description: ASF-ui is offered for free and our resources are limited. Helping us increases the chance of making it happen. 53 | options: 54 | - Yes, I can code the solution myself and send a pull request 55 | - Somehow, I can test and offer feedback, but can't code 56 | - No, I don't have time, skills or willings for any of that 57 | validations: 58 | required: true 59 | - type: textarea 60 | id: additional-info 61 | attributes: 62 | label: Additional info 63 | description: Everything else you consider worthy that we didn't ask for. 64 | - type: markdown 65 | attributes: 66 | value: | 67 | --- 68 | #### Thank you for taking the time to fill out this enhancement idea. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Question.yml: -------------------------------------------------------------------------------- 1 | name: ❓ Question 2 | description: Feel free to ask a question related to ASF-ui 3 | labels: ["❓ Question"] 4 | body: 5 | - type: checkboxes 6 | id: checklist 7 | attributes: 8 | label: Checklist 9 | description: Ensure that our wiki suggestion form is appropriate for you. 10 | options: 11 | - label: I read and understood ASF-ui's **[Contributing Guidelines](https://github.com/JustArchiNET/ASF-ui/blob/main/.github/CONTRIBUTING.md)** 12 | required: true 13 | - label: I also read **[FAQ](https://github.com/JustArchiNET/ASF-ui/wiki/FAQ)** 14 | required: true 15 | - label: This is not **[ASF question](https://github.com/JustArchiNET/ArchiSteamFarm/discussions/new?category=Support)** 16 | required: true 17 | - type: textarea 18 | id: question 19 | attributes: 20 | label: The question 21 | description: Please specify your question in regards to ASF-ui. 22 | placeholder: | 23 | How do I change language for ASF-ui? 24 | validations: 25 | required: true 26 | - type: textarea 27 | id: additional-info 28 | attributes: 29 | label: Additional info 30 | description: Everything else you consider worthy that we didn't ask for. 31 | - type: markdown 32 | attributes: 33 | value: | 34 | --- 35 | #### Thank you for taking the time to fill out this question form. 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Wiki-suggestion.yml: -------------------------------------------------------------------------------- 1 | name: 📕 Wiki suggestion 2 | description: All issues related to our wiki documentation, mainly corrections and ideas 3 | labels: ["📕 Wiki", "🧐 Evaluation"] 4 | body: 5 | - type: checkboxes 6 | id: checklist 7 | attributes: 8 | label: Checklist 9 | description: Ensure that our wiki suggestion form is appropriate for you. 10 | options: 11 | - label: I read and understood ASF-ui's **[Contributing Guidelines](https://github.com/JustArchiNET/ASF-ui/blob/main/.github/CONTRIBUTING.md)** 12 | required: true 13 | - label: I also read **[FAQ](https://github.com/JustArchiNET/ASF-ui/wiki/FAQ)** 14 | required: true 15 | - label: This is not a **[translation issue](https://github.com/JustArchiNET/ASF-ui/wiki/Localization)** 16 | required: true 17 | - label: This is not **[ASF wiki suggestion](https://github.com/JustArchiNET/ArchiSteamFarm/issues/new/choose)** 18 | required: true 19 | - type: input 20 | id: wiki-page 21 | attributes: 22 | label: Wiki page 23 | description: If this is a suggestion regarding an existing wiki page, please link it for reference. If the wiki page doesn't exist, suggest its title. 24 | placeholder: https://github.com/JustArchiNET/ASF-ui/wiki/??? 25 | validations: 26 | required: true 27 | - type: textarea 28 | id: issue 29 | attributes: 30 | label: The issue 31 | description: Please specify your issue in regards to our wiki documentation. 32 | placeholder: | 33 | If you're reporting a mistake/correction, state what is wrong. 34 | 35 | If you're suggesting an idea, explain the details. 36 | validations: 37 | required: true 38 | - type: textarea 39 | id: wrong-text 40 | attributes: 41 | label: Wrong text 42 | description: The existing text on the wiki which you classify as wrong. 43 | placeholder: | 44 | If you're suggesting a new page, paragraph or other addition to the wiki, then this section is not mandatory. 45 | render: markdown 46 | - type: textarea 47 | id: suggested-improvement 48 | attributes: 49 | label: Suggested improvement 50 | description: The new or corrected text that would satisfy your issue stated above. You may use **[markdown](https://guides.github.com/features/mastering-markdown)** for formatting. 51 | placeholder: | 52 | # Never Gonna Give You Up by Rick Astley 53 | 54 | ## Verse 1 55 | We're no strangers to love 56 | You know the rules and so do I 57 | A full commitment's what I'm thinking of 58 | You wouldn't get this from any other guy 59 | 60 | ## Pre-Chorus 61 | I just wanna tell you how I'm feeling 62 | Gotta make you understand 63 | 64 | ## Chorus 65 | Never gonna give you up 66 | Never gonna let you down 67 | Never gonna run around and desert you 68 | Never gonna make you cry 69 | Never gonna say goodbye 70 | Never gonna tell a lie and hurt you 71 | 72 | ## More 73 | See **[full version](https://www.youtube.com/watch?v=dQw4w9WgXcQ)**. 74 | render: markdown 75 | validations: 76 | required: true 77 | - type: textarea 78 | id: additional-info 79 | attributes: 80 | label: Additional info 81 | description: Everything else you consider worthy that we didn't ask for. 82 | - type: markdown 83 | attributes: 84 | value: | 85 | --- 86 | #### Thank you for taking the time to fill out this wiki suggestion. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 💻 ArchiSteamFarm issue, unrelated to ASF-ui 4 | url: https://github.com/JustArchiNET/ArchiSteamFarm/issues/new/choose 5 | about: Please open an issue in ArchiSteamFarm repo 6 | - name: 🌐 Localization improvement 7 | url: https://github.com/JustArchiNET/ArchiSteamFarm/wiki/Localization 8 | about: Please use our crowdin platform 9 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | 4 | 5 | ## Screenshots 6 | 7 | 8 | ## Additional information 9 | 10 | 11 | ## Checklist 12 | 13 | - [ ] My pull request is not a duplicate 14 | - [ ] I added a descriptive title to this pull request 15 | - [ ] I added a concise description or a self-explanatory screenshot to this pull request 16 | - [ ] My code follows the code style of this project 17 | - [ ] I have performed a self-review of my own code 18 | -------------------------------------------------------------------------------- /.github/README.md: -------------------------------------------------------------------------------- 1 | # ASF-ui 2 | 3 | The official web interface for [ASF](https://github.com/JustArchiNET/ArchiSteamFarm) 4 | 5 | [![Build status (GitHub)](https://img.shields.io/github/actions/workflow/status/JustArchiNET/ASF-ui/ci.yml?label=GitHub&cacheSeconds=600&logo=github&branch=main)](https://github.com/JustArchiNET/ASF-ui/actions?query=branch%3Amain) 6 | [![Github last commit date](https://img.shields.io/github/last-commit/JustArchiNET/ASF-ui?label=Updated&cacheSeconds=600&logo=github)](https://github.com/JustArchiNET/ASF-ui/commits) 7 | [![License](https://img.shields.io/github/license/JustArchiNET/ASF-ui?label=License&cacheSeconds=2592000&logo=apache)](https://github.com/JustArchiNET/ASF-ui/blob/main/LICENSE) 8 | 9 | 10 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security policy 2 | 3 | We're doing our best to protect our community from all harm, therefore we take security vulnerabilities very seriously. 4 | 5 | If you believe that you've found one, we'd appreciate if you let us know about it. You can do so by contacting us privately at [ASF@JustArchi.net](mailto:ASF@JustArchi.net), where we'll do our best to evaluate your issue ASAP and keep you updated with the development status. If your vulnerability isn't crucial and doesn't result in a direct escalation, therefore can be known publicly while the appropriate fix is being implemented, you can also open a standard **[issue](https://github.com/JustArchiNET/ASF-ui/issues/new/choose)** instead. 6 | 7 | Depending on the severity of the issue, we might take further actions in order to limit potential damage, for example by speeding up the release of the next stable ASF version. This is evaluated on a case-by-case basis. 8 | -------------------------------------------------------------------------------- /.github/crowdin.yml: -------------------------------------------------------------------------------- 1 | "base_path": ".." 2 | "preserve_hierarchy": true 3 | "files": [ 4 | { 5 | "source": "/src/i18n/locale/default.json", 6 | "dest": "/ASF-ui/src/i18n/locale/default.json", 7 | "translation": "/src/i18n/locale/%locale%.json" 8 | } 9 | ] 10 | -------------------------------------------------------------------------------- /.github/previews/README.md: -------------------------------------------------------------------------------- 1 | This directory contains ASF-ui preview images, mainly (but not only) used by ASF's wiki. 2 | 3 | Preview images will automatically be updated if necessary, for example when something in the layout changes. 4 | -------------------------------------------------------------------------------- /.github/previews/bots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustArchiNET/ASF-ui/c0e3f384bbfe3c5c904ef13869b977756721b24b/.github/previews/bots.png -------------------------------------------------------------------------------- /.github/previews/commands.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustArchiNET/ASF-ui/c0e3f384bbfe3c5c904ef13869b977756721b24b/.github/previews/commands.png -------------------------------------------------------------------------------- /.github/renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base", 5 | ":assignee(MrBurrBurr)", 6 | ":automergeBranch", 7 | ":automergeDigest", 8 | ":automergeMinor", 9 | ":disableDependencyDashboard", 10 | ":disableRateLimiting", 11 | ":label(🤖 Automatic)" 12 | ], 13 | "packageRules": [ 14 | { 15 | "assignees": [ "JustArchi" ], 16 | "matchManagers": [ "github-actions" ] 17 | }, 18 | { 19 | "matchPackageNames": ["@fortawesome/vue-fontawesome"], 20 | "allowedVersions": "<3.0.0" 21 | }, 22 | { 23 | "matchPackageNames": ["vue"], 24 | "allowedVersions": "<3.0.0" 25 | }, 26 | { 27 | "matchPackageNames": ["vue-eslint-parser"], 28 | "allowedVersions": "<9.0.0" 29 | }, 30 | { 31 | "matchPackageNames": ["vuex"], 32 | "allowedVersions": "<4.0.0" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ASF-ui-ci 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | NODE_JS_VERSION: 'lts/*' 7 | 8 | permissions: {} 9 | 10 | jobs: 11 | build: 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | os: [macos-latest, ubuntu-latest, windows-latest] 16 | 17 | runs-on: ${{ matrix.os }} 18 | 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@v4.2.2 22 | with: 23 | show-progress: false 24 | 25 | - name: Setup Node.js with npm 26 | uses: actions/setup-node@v4.4.0 27 | with: 28 | check-latest: true 29 | node-version: ${{ env.NODE_JS_VERSION }} 30 | 31 | - name: Verify Node.js 32 | run: node -v 33 | 34 | - name: Verify npm 35 | run: npm -v 36 | 37 | - name: Install npm modules for ASF-ui 38 | run: npm ci --no-progress 39 | 40 | - name: Build ASF-ui 41 | run: npm run-script deploy --no-progress 42 | 43 | - name: Test ASF-ui dev environment 44 | run: npm run-script serve:ci --no-progress 45 | 46 | - name: Upload ASF-ui 47 | uses: actions/upload-artifact@v4.6.2 48 | with: 49 | if-no-files-found: error 50 | name: ${{ matrix.os }}_ASF-ui 51 | path: dist 52 | -------------------------------------------------------------------------------- /.github/workflows/crowdin-ci.yml: -------------------------------------------------------------------------------- 1 | name: ASF-ui-crowdin-ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | permissions: {} 9 | 10 | jobs: 11 | upload: 12 | environment: dev-crowdin 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v4.2.2 18 | with: 19 | show-progress: false 20 | 21 | - name: Upload latest strings for translation on Crowdin 22 | uses: crowdin/github-action@v2.7.0 23 | with: 24 | crowdin_branch_name: main 25 | config: '.github/crowdin.yml' 26 | project_id: ${{ secrets.ASF_CROWDIN_PROJECT_ID }} 27 | token: ${{ secrets.ASF_CROWDIN_API_TOKEN }} 28 | -------------------------------------------------------------------------------- /.github/workflows/lock-threads.yml: -------------------------------------------------------------------------------- 1 | name: ASF-ui-lock-threads 2 | 3 | on: 4 | schedule: 5 | - cron: '0 1 * * *' 6 | workflow_dispatch: 7 | 8 | permissions: 9 | discussions: write 10 | issues: write 11 | pull-requests: write 12 | 13 | concurrency: 14 | group: lock-threads 15 | 16 | jobs: 17 | lock: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Lock inactive threads 21 | uses: dessant/lock-threads@v5.0.1 22 | with: 23 | discussion-inactive-days: 90 24 | issue-inactive-days: 30 25 | pr-inactive-days: 30 26 | -------------------------------------------------------------------------------- /.github/workflows/translations.yml: -------------------------------------------------------------------------------- 1 | name: ASF-ui-translations 2 | 3 | on: 4 | schedule: 5 | - cron: '0 2 * * *' 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | update: 13 | environment: dev-crowdin 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v4.2.2 19 | with: 20 | show-progress: false 21 | token: ${{ secrets.ARCHIBOT_ASF_UI_GITHUB_TOKEN }} 22 | 23 | - name: Download latest translations from Crowdin 24 | uses: crowdin/github-action@v2.7.0 25 | with: 26 | upload_sources: false 27 | download_translations: true 28 | skip_untranslated_strings: true 29 | push_translations: false 30 | crowdin_branch_name: main 31 | config: '.github/crowdin.yml' 32 | project_id: ${{ secrets.ASF_CROWDIN_PROJECT_ID }} 33 | token: ${{ secrets.ASF_CROWDIN_API_TOKEN }} 34 | 35 | - name: Import GPG key 36 | uses: crazy-max/ghaction-import-gpg@v6.3.0 37 | with: 38 | gpg_private_key: ${{ secrets.ARCHIBOT_ASF_UI_GPG_PRIVATE_KEY }} 39 | git_user_signingkey: true 40 | git_commit_gpgsign: true 41 | 42 | - name: Commit and push the changes 43 | shell: sh 44 | run: | 45 | set -eu 46 | 47 | git add -A "src/i18n/locale" 48 | 49 | if ! git diff --cached --quiet; then 50 | git commit -m "Automatic translations update" 51 | 52 | git push 53 | fi 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | dist 4 | dist.zip 5 | out 6 | .DS_Store 7 | test.js 8 | notes.txt 9 | -------------------------------------------------------------------------------- /generated/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "asf-ui", 3 | "version": "0.0.0", 4 | "description": "The official web interface for ASF", 5 | "scripts": { 6 | "serve": "webpack serve --config webpack/config.js", 7 | "serve:ci": "webpack serve --config webpack/config.ci.js", 8 | "start": "webpack serve --config webpack/config.js", 9 | "lint": "eslint src scripts webpack --fix", 10 | "build": "webpack --config webpack/config.prod.js", 11 | "build:analyze": "webpack --config webpack/config.analyze.js", 12 | "deploy": "webpack --config webpack/config.deploy.js" 13 | }, 14 | "private": true, 15 | "devDependencies": { 16 | "@babel/core": "7.27.4", 17 | "@babel/eslint-parser": "7.27.5", 18 | "@babel/plugin-syntax-dynamic-import": "7.8.3", 19 | "@babel/preset-env": "7.27.2", 20 | "babel-loader": "10.0.0", 21 | "before-build-webpack": "0.2.15", 22 | "clean-webpack-plugin": "4.0.0", 23 | "copy-webpack-plugin": "13.0.0", 24 | "css-loader": "7.1.2", 25 | "eslint": "8.57.1", 26 | "eslint-config-airbnb-base": "15.0.0", 27 | "eslint-plugin-import": "2.31.0", 28 | "eslint-plugin-vue": "9.33.0", 29 | "file-loader": "6.2.0", 30 | "sass": "1.89.1", 31 | "sass-loader": "14.2.1", 32 | "url-loader": "4.1.1", 33 | "vue-eslint-parser": "8.3.0", 34 | "vue-loader": "15.11.1", 35 | "vue-template-compiler": "2.7.16", 36 | "webpack": "5.99.9", 37 | "webpack-bundle-analyzer": "4.10.2", 38 | "webpack-cli": "6.0.1", 39 | "webpack-dev-server": "5.2.2", 40 | "webpack-subresource-integrity": "5.1.0" 41 | }, 42 | "dependencies": { 43 | "@fortawesome/fontawesome-svg-core": "6.7.2", 44 | "@fortawesome/free-brands-svg-icons": "6.7.2", 45 | "@fortawesome/free-solid-svg-icons": "6.7.2", 46 | "@fortawesome/vue-fontawesome": "2.0.10", 47 | "axios": "1.9.0", 48 | "copy-to-clipboard": "3.3.3", 49 | "flat": "6.0.1", 50 | "html-webpack-plugin": "5.6.3", 51 | "humanize-duration": "3.32.2", 52 | "linkify-html": "4.3.1", 53 | "linkifyjs": "4.3.1", 54 | "lodash-es": "4.17.21", 55 | "plurals-cldr": "2.0.1", 56 | "svg-country-flags": "1.2.10", 57 | "v-tooltip": "2.1.3", 58 | "vue": "2.7.16", 59 | "vue-meta": "2.4.0", 60 | "vue-multiselect": "2.1.9", 61 | "vue-router": "3.6.5", 62 | "vue-snotify": "3.2.1", 63 | "vuex": "3.6.2" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /scripts/generateFlags.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | module.exports = function generateFlags() { 4 | const files = fs.readdirSync('./src/i18n/locale'); 5 | 6 | const countries = files 7 | .filter(fileName => fileName.endsWith('.json')) 8 | .filter(fileName => fileName !== 'lol-US.json') // Ignore LOLCAT file since 'svg-country-flags' does not support it 9 | .filter(fileName => fileName !== 'ca-ES.json') // Ignore Catalan file since 'svg-country-flags' does not support it 10 | .map(fileName => ((fileName === 'default.json') ? 'en-US' : fileName.replace('.json', ''))) 11 | .map(locale => ((locale === 'sr-CS') ? 'rs' : locale.split('-')[1].toLowerCase())); 12 | 13 | const fileContent = countries 14 | .map(country => `export { default as ${country} } from 'svg-country-flags/png100px/${country}.png';`) 15 | .join('\n'); 16 | 17 | fs.writeFileSync('./generated/flags.js', fileContent); 18 | }; 19 | -------------------------------------------------------------------------------- /scripts/getCommitHash.js: -------------------------------------------------------------------------------- 1 | const { execSync } = require('child_process'); 2 | 3 | module.exports = function getCommitHash() { 4 | try { 5 | return execSync('git rev-parse --short=7 HEAD').toString(); 6 | } catch (err) { 7 | return '0000000'; 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /scripts/reverse-proxy/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const proxy = require('redbird')({ port: 5000 }); 3 | proxy.register('127.0.0.1/ASF', 'http://127.0.0.1:1242/ASF'); 4 | -------------------------------------------------------------------------------- /scripts/reverse-proxy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reverse-proxy", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "dependencies": { 10 | "redbird": "^1.0.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | import App from './App.vue'; 4 | import router from './router'; 5 | import store from './store'; 6 | 7 | import i18n from './plugins/i18n'; 8 | import Notifications from './plugins/notifications'; 9 | import Icons from './plugins/icons'; 10 | import http, { NotificationError } from './plugins/http'; 11 | import Tooltips from './plugins/tooltips'; 12 | 13 | Vue.use(Notifications); 14 | Vue.use(Tooltips); 15 | Vue.use(Icons); 16 | Vue.use(i18n, store); 17 | Vue.use(http); 18 | 19 | const app = new Vue({ 20 | el: '#app', 21 | render: h => h(App), 22 | router, 23 | store, 24 | }); 25 | 26 | window.addEventListener('unhandledrejection', err => { 27 | if (err.reason instanceof NotificationError) { 28 | app.$error(err.reason.message); 29 | err.preventDefault(); 30 | } 31 | }); 32 | -------------------------------------------------------------------------------- /src/components/ASF/Bots.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 69 | 70 | 118 | -------------------------------------------------------------------------------- /src/components/ASF/BotsFilter.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 35 | 36 | 47 | -------------------------------------------------------------------------------- /src/components/ASF/FarmingInfo.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 36 | 37 | 48 | -------------------------------------------------------------------------------- /src/components/ASF/FarmingInfoCard.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 27 | 28 | 72 | -------------------------------------------------------------------------------- /src/components/ASF/Plugins.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 69 | 70 | 127 | -------------------------------------------------------------------------------- /src/components/App/Footer.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 74 | 75 | 110 | -------------------------------------------------------------------------------- /src/components/App/Header.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 65 | 66 | 102 | -------------------------------------------------------------------------------- /src/components/App/Navigation.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 72 | 73 | 103 | -------------------------------------------------------------------------------- /src/components/App/SideMenu.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 43 | 44 | 116 | -------------------------------------------------------------------------------- /src/components/App/partials/FooterLink.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 33 | 34 | 77 | -------------------------------------------------------------------------------- /src/components/App/partials/SideMenuSwitch.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 32 | 33 | 52 | -------------------------------------------------------------------------------- /src/components/BGR/Check.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 44 | 45 | 50 | -------------------------------------------------------------------------------- /src/components/BGR/Input.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 93 | -------------------------------------------------------------------------------- /src/components/BGR/Keys.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 29 | 30 | 47 | -------------------------------------------------------------------------------- /src/components/BGR/Reset.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 31 | -------------------------------------------------------------------------------- /src/components/BGR/Status.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 34 | 35 | 41 | -------------------------------------------------------------------------------- /src/components/BGR/Summary.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 55 | 56 | 61 | -------------------------------------------------------------------------------- /src/components/Bot/Action.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 32 | 33 | 60 | -------------------------------------------------------------------------------- /src/components/Bot/FarmingInfo.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 23 | 24 | 50 | -------------------------------------------------------------------------------- /src/components/Bot/Games.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 48 | 49 | 124 | -------------------------------------------------------------------------------- /src/components/Bot/Link.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 27 | 28 | 55 | -------------------------------------------------------------------------------- /src/components/Config/Category.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 23 | -------------------------------------------------------------------------------- /src/components/Config/Fields/Input.vue: -------------------------------------------------------------------------------- 1 | 75 | -------------------------------------------------------------------------------- /src/components/Config/Fields/InputBoolean.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 28 | -------------------------------------------------------------------------------- /src/components/Config/Fields/InputDescription.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 42 | 43 | 67 | -------------------------------------------------------------------------------- /src/components/Config/Fields/InputDictionary.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 89 | -------------------------------------------------------------------------------- /src/components/Config/Fields/InputEnum.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 35 | -------------------------------------------------------------------------------- /src/components/Config/Fields/InputFlag.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 67 | -------------------------------------------------------------------------------- /src/components/Config/Fields/InputLabel.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 30 | -------------------------------------------------------------------------------- /src/components/Config/Fields/InputList.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 71 | -------------------------------------------------------------------------------- /src/components/Config/Fields/InputNumber.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 27 | -------------------------------------------------------------------------------- /src/components/Config/Fields/InputSet.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 77 | -------------------------------------------------------------------------------- /src/components/Config/Fields/InputString.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 34 | -------------------------------------------------------------------------------- /src/components/Config/Fields/InputTag.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 99 | -------------------------------------------------------------------------------- /src/components/Config/Fields/InputUnknown.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 21 | 22 | 28 | -------------------------------------------------------------------------------- /src/components/MassEditor/Bots.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 68 | 69 | 78 | -------------------------------------------------------------------------------- /src/components/MassEditor/Check.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 114 | 115 | 120 | -------------------------------------------------------------------------------- /src/components/MassEditor/Select.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 95 | 96 | 99 | -------------------------------------------------------------------------------- /src/components/MassEditor/Steps.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 35 | 36 | 117 | -------------------------------------------------------------------------------- /src/components/MassEditor/Value.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 52 | -------------------------------------------------------------------------------- /src/components/MassEditor/partials/BotsView.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 33 | 34 | 103 | -------------------------------------------------------------------------------- /src/components/Navigation/Bots.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 60 | 61 | 108 | -------------------------------------------------------------------------------- /src/components/Navigation/CategoryTitle.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | 19 | 35 | -------------------------------------------------------------------------------- /src/components/Navigation/Link.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 50 | 51 | 128 | -------------------------------------------------------------------------------- /src/components/Navigation/Statistic.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 30 | 31 | 99 | -------------------------------------------------------------------------------- /src/components/Navigation/Statistics.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 24 | 25 | 31 | -------------------------------------------------------------------------------- /src/components/utils/FitText.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 73 | -------------------------------------------------------------------------------- /src/components/utils/Flag.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 26 | 27 | 33 | -------------------------------------------------------------------------------- /src/i18n/lib/formatter.js: -------------------------------------------------------------------------------- 1 | import { indexOf as pluralIndexOf } from 'plurals-cldr'; 2 | 3 | const RE_TOKEN_LIST_VALUE = /^(\d)+/; 4 | const RE_TOKEN_PLURAL_VALUE = /^PLURAL:/; 5 | 6 | function getTokenType(value) { 7 | if (RE_TOKEN_LIST_VALUE.test(value)) return 'list'; 8 | if (RE_TOKEN_PLURAL_VALUE.test(value)) return 'plural'; 9 | return 'named'; 10 | } 11 | 12 | export default class Formatter { 13 | constructor() { 14 | this._caches = new Map(); 15 | } 16 | 17 | static parse(message) { 18 | const tokens = []; 19 | let position = 0; 20 | 21 | let text = ''; 22 | while (position < message.length) { 23 | let char = message[position++]; 24 | 25 | if (char === '{') { 26 | if (text) tokens.push({ type: 'text', value: text }); 27 | text = ''; 28 | 29 | let value = ''; 30 | char = message[position++]; 31 | 32 | let intend = 1; 33 | while (intend !== 0) { 34 | if (char === '{') intend++; 35 | if (char === '}') intend--; 36 | 37 | if (intend === 0) break; 38 | 39 | value += char; 40 | char = message[position++]; 41 | } 42 | 43 | const type = getTokenType(value); 44 | 45 | tokens.push({ type, value }); 46 | } else { 47 | text += char; 48 | } 49 | } 50 | 51 | if (text) tokens.push({ type: 'text', value: text }); 52 | 53 | return tokens; 54 | } 55 | 56 | compile(tokens, values, locale) { 57 | return tokens.map(token => this.compileToken(token, values, locale)).join(''); 58 | } 59 | 60 | compileToken(token, values, locale) { 61 | switch (token.type) { 62 | case 'text': 63 | return token.value; 64 | case 'list': 65 | return values[parseInt(token.value.trim(), 10)]; 66 | case 'plural': 67 | return this.compilePluralToken(token, values, locale); 68 | case 'named': 69 | return values[token.value.trim()]; 70 | default: 71 | return ''; 72 | } 73 | } 74 | 75 | compilePluralToken(token, values, locale) { 76 | // token = { type: 'plural', value: 'PLURAL:|||' } 77 | 78 | const [plural, ...forms] = token.value.split('|'); 79 | // eslint-disable-next-line no-unused-vars 80 | const [_, value] = plural.split(':'); 81 | 82 | // CLDR table does not recognize lol-US as a valid locale 83 | // eslint-disable-next-line no-param-reassign 84 | if (locale === 'lol-US') locale = 'en-US'; 85 | 86 | const pluralizationValue = this.compileToken({ type: getTokenType(value), value }, values, locale); 87 | const pluralizationIndex = pluralIndexOf(locale, parseInt(pluralizationValue, 10)); 88 | 89 | const formIndex = Math.min(pluralizationIndex, forms.length - 1); 90 | return this.interpolate(forms[formIndex], values, locale); 91 | } 92 | 93 | interpolate(message, values, locale = 'en-US') { 94 | const tokens = (this._caches.has(message)) ? this._caches.get(message) : Formatter.parse(message); 95 | if (!this._caches.has(message)) this._caches.set(message, tokens); 96 | return this.compile(tokens, values, locale); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/i18n/lib/index.js: -------------------------------------------------------------------------------- 1 | import storeModule from './store'; 2 | import Formatter from './formatter'; 3 | 4 | export default { 5 | install(Vue, store, config = {}) { 6 | if (this.installed) return; 7 | this.installed = true; 8 | 9 | store.registerModule('i18n', storeModule); 10 | 11 | const formatter = new Formatter(); 12 | 13 | const i18n = { 14 | _availableLocales: [], 15 | _requireLocale() {}, 16 | get locales() { return store.getters['i18n/locales']; }, 17 | get locale() { return store.getters['i18n/locale']; }, 18 | get fallbackLocale() { return store.getters['i18n/fallbackLocale']; }, 19 | get availableLocales() { return this._availableLocales.map(locale => locale.name); }, 20 | get translationPercent() { return store.getters['i18n/translationPercent']; }, 21 | get noRegionalLocale() { return store.getters['i18n/noRegionalLocale']; }, 22 | setAvailableLocales(availableLocales, requireLocale) { 23 | this._availableLocales = availableLocales; 24 | this._requireLocale = requireLocale; 25 | }, 26 | has(name) { 27 | return store.getters['i18n/locales'].includes(name); 28 | }, 29 | async set(name) { 30 | const oldLocale = this.locale; 31 | store.dispatch('i18n/setLocale', { locale: name }); 32 | 33 | this.load(name).catch(err => { 34 | console.warn(err.message); 35 | store.dispatch('i18n/setLocale', { locale: oldLocale }); 36 | }); 37 | }, 38 | async load(name) { 39 | if (this.has(name)) return; 40 | 41 | const locale = this._availableLocales.find(locale => locale.name === name); 42 | if (!locale) throw new Error(`[i18n] Locale "${name}" not available!`); 43 | 44 | store.dispatch('i18n/addLocale', { locale: name, translation: await this._requireLocale(locale.fileName) }); 45 | }, 46 | translate(key, fallbackString, values) { 47 | // eslint-disable-next-line no-param-reassign 48 | if (typeof fallbackString !== 'string') values = fallbackString; 49 | 50 | const translationLocale = store.getters['i18n/translationLocale'](key); 51 | const translationString = (translationLocale) ? store.getters['i18n/translation'](translationLocale, key) : (fallbackString || key); 52 | return formatter.interpolate(translationString, values, translationLocale); 53 | }, 54 | }; 55 | 56 | const finalConfig = { 57 | locale: 'en-US', 58 | translations: {}, 59 | availableLocales: [], 60 | requireLocale() { }, 61 | ...config, 62 | }; 63 | 64 | i18n._availableLocales = finalConfig.availableLocales; 65 | i18n._requireLocale = finalConfig.requireLocale; 66 | 67 | Object.keys(finalConfig.translations).forEach(locale => store.dispatch('i18n/addLocale', { locale, translation: finalConfig.translations[locale] })); 68 | 69 | i18n.set(finalConfig.locale); 70 | 71 | if (finalConfig.fallbackLocale) i18n.load(finalConfig.fallbackLocale); 72 | store.dispatch('i18n/setFallbackLocale', { locale: finalConfig.fallbackLocale || finalConfig.locale }); 73 | 74 | Vue.prototype.$i18n = i18n; 75 | Vue.prototype.$t = i18n.translate; 76 | Vue.i18n = i18n; 77 | }, 78 | }; 79 | -------------------------------------------------------------------------------- /src/i18n/lib/store.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import { flatten } from 'flat'; 3 | 4 | export default { 5 | namespaced: true, 6 | state: { 7 | locale: null, 8 | fallbackLocale: null, 9 | translations: {}, 10 | }, 11 | mutations: { 12 | setLocale(state, { locale }) { 13 | state.locale = locale; 14 | }, 15 | setFallbackLocale(state, { locale }) { 16 | state.fallbackLocale = locale; 17 | }, 18 | addLocale(state, { locale, translation }) { 19 | Vue.set(state.translations, locale, flatten(translation)); 20 | }, 21 | removeLocale(state, { locale }) { 22 | Vue.delete(state.translations, locale); 23 | }, 24 | }, 25 | actions: { 26 | setLocale({ commit }, payload) { 27 | commit('setLocale', payload); 28 | }, 29 | setFallbackLocale({ commit }, payload) { 30 | commit('setFallbackLocale', payload); 31 | }, 32 | addLocale({ commit }, payload) { 33 | commit('addLocale', payload); 34 | }, 35 | removeLocale({ commit }, payload) { 36 | commit('removeLocale', payload); 37 | }, 38 | }, 39 | getters: { 40 | locale: state => state.locale, 41 | fallbackLocale: state => state.fallbackLocale, 42 | locales: state => Object.keys(state.translations), 43 | translation: state => (locale, key) => state.translations[locale][key], 44 | hasTranslation: state => (locale, key) => !!locale && !!state.translations[locale] && !!state.translations[locale][key], 45 | noRegionalLocale: state => ((state.locale) ? state.locale.split('-')[0] : state.locale), 46 | translationLocale: (state, getters) => key => { 47 | if (state.locale && getters.hasTranslation(state.locale, key)) return state.locale; 48 | if (getters.noRegionalLocale && getters.hasTranslation(getters.noRegionalLocale, key)) return getters.noRegionalLocale; 49 | if (state.fallbackLocale && getters.hasTranslation(state.fallbackLocale, key)) return state.fallbackLocale; 50 | }, 51 | translationPercent: (state, getters) => { 52 | if (!state.locale || !state.fallbackLocale) return 0; 53 | if (!state.translations[state.locale] || !state.translations[state.fallbackLocale]) return 0; 54 | const availableStrings = Object.keys(state.translations[state.fallbackLocale]); 55 | return (availableStrings.filter(key => getters.hasTranslation(state.locale, key)).length / availableStrings.length) * 100; 56 | }, 57 | }, 58 | }; 59 | -------------------------------------------------------------------------------- /src/i18n/locale/README.md: -------------------------------------------------------------------------------- 1 | This directory contains ASF-ui strings for display and localization purposes. 2 | 3 | All strings used by ASF-ui can be found in main `default.json` file, and that's also the only `json` file that should be modified - all other `json` files are managed automatically and should not be touched. Please visit **[localization](https://github.com/JustArchiNET/ArchiSteamFarm/wiki/Localization)** section on the wiki if you want to improve translation of other files. 4 | -------------------------------------------------------------------------------- /src/i18n/locale/be-BY.json: -------------------------------------------------------------------------------- 1 | { 2 | "restart-initiated": "Перазапуск...", 3 | "success": "Паспяхова завершана" 4 | } 5 | -------------------------------------------------------------------------------- /src/i18n/locale/bs-BA.json: -------------------------------------------------------------------------------- 1 | { 2 | "2fa-token-copied": "Token kopiran u međuspremnik!", 3 | "2fa-token-copy": "Kopiraj token u međuspremnik", 4 | "2fa-token-refresh": "Osvježi token", 5 | "access": "Pristup", 6 | "add": "Dodaj", 7 | "advanced": "Napredno", 8 | "all": "Sve", 9 | "asf-config": "ASF Konfiguracija", 10 | "back": "Nazad", 11 | "basic": "Osnovno", 12 | "bgr-keys-insert": "Unesite vaše ključeve ovdje", 13 | "bot-copy": "Kopiraj bota", 14 | "bot-fav-buttons-2fa": "2FA", 15 | "bot-fav-buttons-config": "Konfiguracija", 16 | "bot-fav-buttons-pause": "Pauziraj/Nastavi", 17 | "bot-game-name": "Samo imena igrica", 18 | "bot-new": "Napravi novog bota", 19 | "bot-new-copy": "Konfiguracija je bazirana na botu: {name}", 20 | "bot-nicknames": "Nadimci", 21 | "bot-status-disabled": "Onemogućeno", 22 | "bot-status-farming": "Farmovanje", 23 | "bot-status-input": "Potreban unos", 24 | "bot-status-offline": "Izvan mreze", 25 | "bot-status-online": "Na mreži", 26 | "bot-title-delete": "Izbriši {bot}", 27 | "bot-title-pause": "Pauziraj {bot}", 28 | "bot-title-resume": "Nastavi {bot}", 29 | "bot-title-start": "Pokreni {bot}", 30 | "bot-title-stop": "Zaustavi {bot}", 31 | "bots": "Botovi", 32 | "cancel": "Odustani", 33 | "changelog-full": "Puni spisak izmena", 34 | "check": "Provjeri", 35 | "commands": "Komande", 36 | "commands-send": "Kliknite ovdje da pošaljete vašu komandu", 37 | "config": "Konfiguracija", 38 | "config-not-saved": "Konfiguracija nije spremljena.", 39 | "confirm": "Potvrdi", 40 | "confirmation": "Molimo ukucajte {name} da potvrdite.", 41 | "confirmation-title": "Da li apsolutno sigurni?", 42 | "connection": "Veza", 43 | "continue": "Nastavi", 44 | "control": "Kontrola", 45 | "create": "Kreiraj", 46 | "default-page": "Zadana stranica", 47 | "delete": "Izbriši", 48 | "error": "Grеška", 49 | "farming": "Farmovanje", 50 | "farming-info-time": "preostalo vrijeme", 51 | "hash": "Hash", 52 | "hash-success": "Hash uspješno generisan, zapamtite da sačuvate svoju konfiguraciju.", 53 | "info": "Informacije", 54 | "input-label-password": "Steam lozinka", 55 | "input-label-steamguard": "Steam Gurad kod", 56 | "input-label-twofactorauthentication": "2FA kod", 57 | "log-information-time": "Vrijeme", 58 | "log-timestamp-time-only-eu": "HH:MM:SS", 59 | "log-timestamp-time-only-us": "HH:MM:SS PM/AM", 60 | "logout-title": "Odjavite se sa ASF-ui", 61 | "mass-editor-search": "Unesite za pretraživanje...", 62 | "multiple-games": "Više igrica", 63 | "name-description": "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 | 8 | 9 | 24 | 25 | 33 | -------------------------------------------------------------------------------- /src/views/Log.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 22 | 23 | 33 | -------------------------------------------------------------------------------- /src/views/Plugins.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 22 | -------------------------------------------------------------------------------- /src/views/Releases.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 22 | -------------------------------------------------------------------------------- /src/views/Welcome.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 46 | 47 | 54 | -------------------------------------------------------------------------------- /src/views/modals/Bot2FADelete.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 73 | 74 | 87 | -------------------------------------------------------------------------------- /src/views/modals/BotDelete.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 65 | 66 | 79 | -------------------------------------------------------------------------------- /src/views/modals/BotInput.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 104 | 105 | 114 | -------------------------------------------------------------------------------- /webpack/config.analyze.js: -------------------------------------------------------------------------------- 1 | const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); 2 | const config = require('./config.prod'); 3 | 4 | config.plugins.push(new BundleAnalyzerPlugin()); 5 | 6 | module.exports = config; 7 | -------------------------------------------------------------------------------- /webpack/config.ci.js: -------------------------------------------------------------------------------- 1 | const config = require('./config'); 2 | 3 | config.devServer.onListening = function stopDevServer(devServer) { 4 | if (!devServer) { 5 | throw new Error('webpack-dev-server is not defined'); 6 | } 7 | 8 | devServer.stopCallback(() => { 9 | console.log('Stopped dev server'); 10 | }); 11 | }; 12 | 13 | module.exports = config; 14 | -------------------------------------------------------------------------------- /webpack/config.deploy.js: -------------------------------------------------------------------------------- 1 | const config = require('./config.prod'); 2 | 3 | config.devtool = false; 4 | config.stats = 'minimal'; 5 | 6 | module.exports = config; 7 | -------------------------------------------------------------------------------- /webpack/config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const { DefinePlugin } = require('webpack'); 4 | const { VueLoaderPlugin } = require('vue-loader'); 5 | const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 6 | const WebpackBeforeBuildPlugin = require('before-build-webpack'); 7 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 8 | 9 | const generateFlags = require('../scripts/generateFlags'); 10 | const getCommitHash = require('../scripts/getCommitHash'); 11 | 12 | module.exports = { 13 | mode: 'development', 14 | devServer: { 15 | historyApiFallback: true, 16 | open: true, 17 | proxy: [ 18 | { 19 | context: ['/api', '/swagger'], 20 | target: 'http://localhost:1242', 21 | ws: true, 22 | }, 23 | ], 24 | static: { 25 | directory: './src/static', 26 | }, 27 | }, 28 | devtool: 'inline-source-map', 29 | entry: { 30 | main: './src/index.js', 31 | }, 32 | output: { 33 | filename: 'scripts/[name].[contenthash:7].bundle.js', 34 | chunkFilename: 'scripts/[id].[contenthash:7].chunk.js', 35 | path: path.resolve(__dirname, '../dist'), 36 | publicPath: '/', 37 | crossOriginLoading: 'anonymous', 38 | }, 39 | module: { 40 | rules: [ 41 | { 42 | test: /\.vue$/, 43 | loader: 'vue-loader', 44 | }, 45 | { 46 | test: /\.jsx?$/, 47 | exclude: /node_modules/, 48 | use: { 49 | loader: 'babel-loader', 50 | options: { 51 | presets: [ 52 | ['@babel/preset-env', { 53 | targets: { browsers: ['> 1%', 'not ie <= 11'] }, 54 | modules: false, 55 | }], 56 | ], 57 | plugins: ['@babel/plugin-syntax-dynamic-import'], 58 | }, 59 | }, 60 | }, 61 | { 62 | test: /\.scss$/, 63 | use: [ 64 | 'vue-style-loader', 65 | { 66 | loader: 'css-loader', 67 | options: { 68 | esModule: false, 69 | }, 70 | }, 71 | 'sass-loader', 72 | ], 73 | }, 74 | { 75 | test: /\.css$/, 76 | use: [ 77 | 'vue-style-loader', 78 | { 79 | loader: 'css-loader', 80 | options: { 81 | esModule: false, 82 | }, 83 | }, 84 | ], 85 | }, 86 | { 87 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac|woff2?|eot|ttf|otf|png|jpe?g|gif|svg)(\?.*)?$/, 88 | use: { 89 | loader: 'url-loader', 90 | options: { 91 | limit: 8192, 92 | name: 'media/[name].[contenthash:7][ext]', 93 | }, 94 | }, 95 | }, 96 | ], 97 | }, 98 | plugins: [ 99 | new WebpackBeforeBuildPlugin((stats, callback) => { 100 | generateFlags(); 101 | callback(); 102 | }), 103 | new CleanWebpackPlugin(), 104 | new VueLoaderPlugin(), 105 | new DefinePlugin(({ 106 | APP_HASH: JSON.stringify(getCommitHash()), 107 | })), 108 | new HtmlWebpackPlugin({ 109 | template: './src/index.html', 110 | }), 111 | ], 112 | watchOptions: { 113 | ignored: /generated/, 114 | }, 115 | }; 116 | -------------------------------------------------------------------------------- /webpack/config.prod.js: -------------------------------------------------------------------------------- 1 | const { SubresourceIntegrityPlugin } = require('webpack-subresource-integrity'); 2 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 3 | const config = require('./config'); 4 | 5 | delete config.devServer; 6 | 7 | config.mode = 'production'; 8 | config.devtool = 'source-map'; 9 | 10 | config.plugins.push(new CopyWebpackPlugin({ 11 | patterns: [{ 12 | from: './src/static', 13 | to: './', 14 | }], 15 | })); 16 | 17 | config.plugins.push(new SubresourceIntegrityPlugin({ 18 | hashFuncNames: ['sha256', 'sha384'], 19 | })); 20 | 21 | config.performance = { 22 | maxEntrypointSize: 500000, 23 | maxAssetSize: 500000, 24 | }; 25 | 26 | module.exports = config; 27 | --------------------------------------------------------------------------------