[
  {
    "path": ".github/FUNDING.yml",
    "content": "liberapay: 0neGal\ncustom: [\"github.com/R2Northstar\"]\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a bug report to help us fix problems\ntitle: 'bug: Short description of your bug'\nlabels: bug\nassignees: ''\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Version:**\nIf possible add on `--version` or simply the version of Viper\n\n**Additional Info**\nAny extra info should go here!\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/empty-template.md",
    "content": "---\nname: Empty Template\nabout: An empty issue template with nothing in it.\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea or feature to be added to Viper\ntitle: 'feat: Your Feature'\nlabels: enhancement\nassignees: ''\n\n---\n\n**What feature would you like added?**\nA clear and concise description of the feature\n\n**Why should this feature be added?**\nWhat benefit does this feature offer?\n\n**Additional Info**\nFeel free to put anything here or delete it if not relevant.\n"
  },
  {
    "path": ".github/workflows/dev_builds.yml",
    "content": "name: Development builds CI\non:\n  push:\n  pull_request:\n    types: [opened, reopened]\n\njobs:\n  build-windows:\n    name: \"Create Windows development builds\"\n    runs-on: \"windows-latest\"\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v3\n      - name: Setup Node environment\n        uses: actions/setup-node@v3\n        with:\n          node-version: 20\n      - name: Install dependencies\n        run: npm install\n      - name: Create builds\n        run: npm run build:windows\n      - name: Archive production artifacts\n        uses: actions/upload-artifact@v4\n        with:\n          name: viper-windows-builds\n          path: |\n            dist/*.exe\n  build-linux:\n    name: \"Create Linux development builds\"\n    runs-on: \"ubuntu-latest\"\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v3\n      - name: Setup Node environment\n        uses: actions/setup-node@v3\n        with:\n          node-version: 20\n      - name: Install dependencies\n        run: npm install\n      - name: Create builds\n        run: npm run build:linux\n      - name: Archive production artifacts\n        uses: actions/upload-artifact@v4\n        with:\n          name: viper-linux-builds\n          path: |\n            dist/*.AppImage\n            dist/*.tar.gz\n            dist/*.deb\n            dist/*.rpm\n"
  },
  {
    "path": ".github/workflows/localizations.yml",
    "content": "name: Localizations\non:\n  push:\n  pull_request:\n    types: [opened, reopened]\n\njobs:\n  check-localizations:\n    name: \"Check localizations\"\n    runs-on: \"ubuntu-latest\"\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v3\n      - name: Setup Node environment\n        uses: actions/setup-node@v3\n        with:\n          node-version: 20\n      - name: Install dependencies\n        run: npm install\n      - name: Check localizations\n        run: npm run langs:check\n"
  },
  {
    "path": ".github/workflows/release_builds.yml",
    "content": "name: Release CI\non:\n  release:\n    types: [ prereleased ]\njobs:\n  build-windows:\n    name: \"Create Windows release builds\"\n    runs-on: \"windows-latest\"\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v3\n      - name: Setup Node environment\n        uses: actions/setup-node@v3\n        with:\n          node-version: 20\n      - name: Install dependencies\n        run: npm install\n      - name: Create builds\n        run: npm run build:windows\n      - name: Upload production artifacts to release\n        uses: xresloader/upload-to-github-release@v1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          file: \"dist/*.exe*;dist/latest.yml\"\n          release_id: ${{ github.event.release.id }}\n          draft: false\n          prerelease: true\n  build-linux:\n    name: \"Create Linux release builds\"\n    runs-on: \"ubuntu-latest\"\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v3\n      - name: Setup Node environment\n        uses: actions/setup-node@v3\n        with:\n          node-version: 20\n      - name: Install dependencies\n        run: npm install\n      - name: Create builds\n        run: npm run build:linux\n      - name: Upload production artifacts to release\n        uses: xresloader/upload-to-github-release@v1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          file: \"dist/*.AppImage;dist/*.tar.gz;dist/*.deb;dist/*.rpm;dist/latest-linux.yml\"\n          release_id: ${{ github.event.release.id }}\n          draft: false\n          prerelease: true\n"
  },
  {
    "path": ".gitignore",
    "content": ".vscode/\n\ndist/\nnode_modules/\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "<p align=\"center\">\n\t<img src=\"src/assets/icons/512x512.png\" width=\"200px\"><br>\n\t<a href=\"FAQ.md\">FAQ</a> | \n\t<a href=\"CONTRIBUTING.md\">Contributing</a> | \n\t<a href=\"https://github.com/0neGal/viper/releases\">Releases</a><br>\n</p>\n\n## Contributor Code of Conduct\n\n### Our Pledge\n\nWe as members, contributors, and leaders pledge to make participation in our\ncommunity a harassment-free experience for everyone, regardless of age, body\nsize, visible or invisible disability, ethnicity, sex characteristics, gender\nidentity and expression, level of experience, education, socio-economic status,\nnationality, personal appearance, race, religion, or sexual identity\nand orientation.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming,\ndiverse, inclusive, and healthy community.\n\n### Our Standards\n\nExamples of behavior that contributes to a positive environment for our\ncommunity include:\n\n - Demonstrating empathy and kindness toward other people\n - Being respectful of differing opinions, viewpoints, and experiences\n - Giving and gracefully accepting constructive feedback\n - Accepting responsibility and apologizing to those affected by our\n   mistakes, and learning from the experience\n - Focusing on what is best not just for us as individuals, but for the\n   overall community\n\nExamples of unacceptable behavior include:\n\n - The use of sexualized language or imagery, and sexual attention or\n   advances of any kind\n - Trolling, insulting or derogatory comments, and personal or political\n   attacks Public or private harassment Publishing others' private\n - information, such as a physical or email address, without their\n   explicit permission\n - Other conduct which could reasonably be considered inappropriate in a\n   professional setting\n\n### Enforcement Responsibilities\n\nCommunity leaders are responsible for clarifying and enforcing our standards of\nacceptable behavior and will take appropriate and fair corrective action in\nresponse to any behavior that they deem inappropriate, threatening, offensive,\nor harmful.\n\nCommunity leaders have the right and responsibility to remove, edit, or reject\ncomments, commits, code, wiki edits, issues, and other contributions that are\nnot aligned to this Code of Conduct, and will communicate reasons for moderation\ndecisions when appropriate.\n\n### Scope\n\nThis Code of Conduct applies within all community spaces, and also applies when\nan individual is officially representing the community in public spaces.\nExamples of representing our community include using an official e-mail address,\nposting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event.\n\n### Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may\nbe reported to the community leaders responsible for enforcement at <a\nhref=\"mail:mail@0negal.com\">mail@0negal.com</a> or <a\nhref=\"https://twitter.com/0neGal\">@0neGal</a> on Twitter.\nAll complaints will be reviewed and investigated promptly and fairly.\n\nAll community leaders are obligated to respect the privacy and security of the\nreporter of any incident.\n\n### Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in determining\nthe consequences for any action they deem in violation of this Code of Conduct:\n\n#### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior deemed\nunprofessional or unwelcome in the community.\n\n**Consequence**: A private, written warning from community leaders, providing\nclarity around the nature of the violation and an explanation of why the\nbehavior was inappropriate. A public apology may be requested.\n\n#### 2. Warning\n\n**Community Impact**: A violation through a single incident or series\nof actions.\n\n**Consequence**: A warning with consequences for continued behavior. No\ninteraction with the people involved, including unsolicited interaction with\nthose enforcing the Code of Conduct, for a specified period of time. This\nincludes avoiding interactions in community spaces as well as external channels\nlike social media. Violating these terms may lead to a temporary or\npermanent ban.\n\n#### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards, including\nsustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or public\ncommunication with the community for a specified period of time. No public or\nprivate interaction with the people involved, including unsolicited interaction\nwith those enforcing the Code of Conduct, is allowed during this period.\nViolating these terms may lead to a permanent ban.\n\n#### 4. Permanent Ban\n\n**Community Impact**: Demonstrating a pattern of violation of community\nstandards, including sustained inappropriate behavior,  harassment of an\nindividual, or aggression toward or disparagement of classes of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction within\nthe community.\n\n### Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage],\nversion 2.0, available at\nhttps://www.contributor-covenant.org/version/2/0/code_of_conduct.html.\n\nCommunity Impact Guidelines were inspired by [Mozilla's code of conduct\nenforcement ladder](https://github.com/mozilla/diversity).\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see the FAQ at\nhttps://www.contributor-covenant.org/faq. Translations are available at\nhttps://www.contributor-covenant.org/translations.\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "<p align=\"center\">\n\t<img src=\"src/assets/icons/512x512.png\" width=\"200px\"><br>\n\t<a href=\"FAQ.md\">FAQ</a> | \n\t<a href=\"CONTRIBUTING.md\">Contributing</a> | \n\t<a href=\"https://github.com/0neGal/viper/releases\">Releases</a><br>\n</p>\n\n## Contributing\n\nGenerally speaking, as Viper is a FOSS (GPLv3) project any bug reports, pull requests, or other types of help, forks are appreciated, but preferably unless it's off-topic to the goals of Viper, it would be far more appreciated if you were to make a pull request, or similar, and get it upstream.\n\n**HOWEVER:** Before opening issues and pull requests and similar, please do read our <a href=\"CODE_OF_CONDUCT.md\">code of conduct</a>, as to avoid issues.\n\n### General contributions\n\nIf you're contributing, please follow <a href=\"CODE_OF_CONDUCT.md\">code of conduct</a> as mentioned above, along with that please do not change code style. Viper uses double quotes, 1 tab per indent, and semicolons at the end of function calls.\n\n`//` for 1-2 lines of comments, max of 72 comment text width always, preferably if textwidth outside of comments also exceeds 72 characters if possible find a way to make it fit within 72 characters. Lower case comments are also generally preferred when using `//`\n\n`/* */` is useful in some cases, refer to the code example below.\n\n`() => {}` is also preferred over `function () {}` when possible.\n\nSingle use functions are also generally discouraged, if a function is used once there's no need to make it a function.\n\nAs shown below below:\n\n```js\nfunction foo(bar) {\n\t/*\n\t\tThis is a very long comment, however, it does not exceed 72\n\t\tcharacters, and instead wraps down so it fits nicely on a small\n\t\tscreen without having to mess with soft-wrapping or similar.\n\t*/\n\tconsole.log(bar);\n\t// and this is a comment using \"//\",\n\t// which is two lines long\n}\n\n// examples of () => {}\nsetInterval(() => {\n\tfoo(\"I RUN EVERY 1000MS\");\n}, 1000)\n```\n\nWith the above information you should be able to comfortably make contributions without too much hassle...\n\n### Localizing/Translating\n\nViper has a very simple i18n system, please read below for instructions.\n\n#### Language file\n\nThe language file will be a file inside the `src/lang/` folder, name it according to the [ISO 3166-1 Alpha-2 standard](https://en.m.wikipedia.org/wiki/ISO_3166-1_alpha-2) in lowercase, meaning English = `en.json`, Spanish = `es.json`, French = `fr.json`, and so on.\n\nEverything inside the file is pretty straight forward, the only special one is the `lang.title` string, please set this to `<language in english> - <language in native language>`, meaning for French it's, `French - Français`. This will be shown inside the language selection screen.\n\nIf you don't feel like editing a JSON file and copying keys back and forwards from other complete language files, then you're in luck, because `scripts/langs.js` help with this exact problem. Simply make sure a valid localization file already exists, meaning it could have just `{}` inside it. Then after that you can use the language script to manage it:\n\n```sh\n$ npm install\n$ npm run langs:localize\n```\n\nYou'll be prompted to select a language to edit, then if there are missing keys you'll be prompted for whether you just want to add those keys or if you want to edit all keys. After that you'll get a list of all the keys available, you'll simply select any of them, then you'll be prompted for the value for the key, then hit <kbd>Enter</kbd> when you're done, and you'll be put right back to the list of keys as before. You'll then edit as needed, when you're done select `Save changes`, you're changes will then be written to the localization file and it'll automatically be formatted for you.\n\nTo sum it up:\n\n 1. Make sure a parseable JSON localization file exists in `src/lang/`\n 2. Run `npm run langs:localize` (after `npm install`)\n 3. Select your desired language\n 4. Select the key you want to add/change\n 5. Edit it as you wish\n 6. Save whenever your done\n 7. Commit your changes!\n\nIf you do happen to manually edit your localization files remember to run the following commands before committing and pushing changes:\n\n```sh\n$ npm install\n$ npm langs:check # verifies the files are parseable and not missing keys\n$ npm langs:format # formats and sorts the file for you\n```\n\n#### Maintainers file\n\nIf you're okay with being contacted in the future when new strings have to be localized please put your contact links inside this file, under your language. Preferably put the link to your GitHub profile as that is the easiest contact method for obvious reasons.\n"
  },
  {
    "path": "FAQ.md",
    "content": "<p align=\"center\">\n\t<img src=\"src/assets/icons/512x512.png\" width=\"200px\"><br>\n\t<a href=\"FAQ.md\">FAQ</a> | \n\t<a href=\"CONTRIBUTING.md\">Contributing</a> | \n\t<a href=\"https://github.com/0neGal/viper/releases\">Releases</a><br>\n</p>\n\n## Frequently Asked Questions (FAQ)\n\n### How do I install Viper?\n\nAs briefly covered in the README, you've multiple choices for installing Viper.\n\n#### Windows\n\nThere's the installer, and the portable `.exe`, we recommend using the installer as it can auto update, there's a Download button on the README which downloads the newest version of the installer.\n\n#### Linux\n\nOn the [releases page](https://github.com/0neGal/viper/releases/latest) there are various package distributions we release, feel free to pick any one of them, but as with Windows only one of them supports auto-installation, that one being the AppImage.\n\n#### After Install\n\nAfter you've gotten Viper installed you'll select your game path (this may or may not be done automatically, however if we can't find your game automatically we'll ask you to select it manually), then go onto the Northstar tab, click \"Install\", and now you can launch Northstar and have fun on the frontier.\n\nIf you keep getting errors about your game path being wrong follow the [instructions further below...](#this-folder-is-not-a-valid-game-path)\n\n### How do I install mods?\n\nWe recommend using Thunderstore, which allows you to easily find mods and install them, to do so simply go into the Northstar tab, then go into the Mods section, and there'll be a button aptly named **\"Find Mods\"**, clicking it will open an easy to use UI that'll let you search for mods and install them.\n\n### \"This folder is not a valid game path.\"\n\nWhen selecting a game path make sure it actually is *the game path*. Your game path is where the `Titanfall2.exe` is located, usually inside `C:\\Program Files (x86)\\Origin Games\\Titanfall2`, however if you've installed the game somewhere else or you've installed the game through Steam it may be located somewhere else.\n\nFor Steam users, you can inside Steam right click on the game, click **Properties**, then in the window that opens **Local Files**, then **Browse**, the folder that it opens is the game path.\n\nIdeally Viper should be able to find Titanfall automatically, however given we can't predict every installation of the game we can't always find the game.\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>.\n"
  },
  {
    "path": "PUBLISH.md",
    "content": "# Publishing a new release\n\n 1. Make sure your code works!\n 2. Update `package.json` version\n 3. Make sure `package.json`'s `repository.url` key references correct repository\n 4. Ensure application builds correctly with `npm run build:[windows/linux]`\n 5. Expose `GH_TOKEN` environment var with your Github token (`build/publish.sh` asks for it)\n 6. Build and publish with `npm run publish:[windows/linux]`\n    - Optionally just use `build/publish.sh`, however that only works on Linux/Systems with a `/bin/sh` file, it also checks whether all files have been localized, and that the version numbers have been updated\n 7. Edit the draft release message and publish the new release!\n\n## CI release\n\nIf you don't want to build releases yourself, you can make GitHub build them for you!\n\n 1. Make sure your code works!\n 2. Update `package.json` version\n 3. Make sure `package.json`'s `repository.url` key references correct repository\n 4. Ensure application builds correctly with `npm run build:[windows/linux]`\n 5. Create a prerelease with newest version name\n    - Creating the prerelease will trigger CI, that will build all executables\n    - You can use build time to update release notes :)\n 6. When all binaries have been uploaded to the prerelease, you can publish it!"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n\t<img src=\"src/assets/icons/512x512.png\" width=\"200px\"><br>\n\t<a href=\"https://0negal.github.io/viper/index.html?win-setup\"><img src=\"assets/download.png\" width=\"300px\"></a><br>\n\t<a href=\"FAQ.md\">FAQ</a> | \n\t<a href=\"CONTRIBUTING.md\">Contributing</a> | \n\t<a href=\"https://github.com/0neGal/viper/releases\">Releases</a><br>\n</p>\n\n## What is Viper?\n\nViper is a launcher and updater for [Northstar](https://github.com/R2Northstar/Northstar), and not much more than that.\n\n## Install\n\nDownloads are available on the [releases page](https://github.com/0neGal/viper/releases/latest). \n\nPlease note that some versions will update themselves automatically when a new release is available (just like Origin or Steam) and some will NOT, so choose it accordingly. Only the AppImage, Flatpak and Windows Setup/Installer can auto-update.\n\n**Windows:** [`Viper Setup [x.y.z].exe`](https://0negal.github.io/viper/index.html?win-setup) (auto-updates, and is recommended), [`Viper [x.y.z].exe`](https://0negal.github.io/viper/index.html?win-portable) (single executable, no fuss)\n\n**Linux:** [`.AppImage`](https://0negal.github.io/viper/index.html?appimage) or [Flatpak](https://flathub.org/apps/details/com.github._0negal.Viper) (auto-updates), [AUR (unofficial)](https://aur.archlinux.org/packages/viper-bin), [`.deb`](https://0negal.github.io/viper/index.html?deb), [`.rpm`](https://0negal.github.io/viper/index.html?rpm), [`.tar.gz`](https://0negal.github.io/viper/index.html?linux)\n\n<a href=\"https://github.com/0neGal/viper/releases\"><img src=\"https://img.shields.io/github/v/release/0neGal/viper\" alt=\"GitHub release (latest by date)\"></a>\n<img src=\"https://img.shields.io/github/downloads/0neGal/viper/latest/total\" alt=\"GitHub release downloads (latest by date)\">\n<img src=\"https://img.shields.io/flathub/downloads/com.github._0negal.Viper?label=Flathub%20installs\" alt=\"Flathub installs (Total)\">\n\n## What can it do specifically?\n\n<p>\nCurrently Viper is capable of:\n\n<img src=\"assets/ns-launch.png\" align=\"right\" width=\"50%\">\n\n * Updating/Installing Northstar\n * Launching Vanilla and or Northstar\n * Manage Mods\n * Auto-Update itself \n * Be pretty!\n\nThere are of course many other things that it can do, but summed up very simply, that is what Viper is capable of doing. With every update Viper gets more features and alike, often many optional features are also available in the settings menu.\n\n</p>\n\n## Configuration\n\nAll settings take place in the settings page, found in the top right corner of the app, where you can change all kinds of settings. You can also manually go in and edit your config file (`viper.json`), if you feel so inclined.\n\nYour configuration file will be found in `%APPDATA%\\viper.json` on Windows, and inside either `~/.config` or through your environment variables (`$XDG_CONFIG_HOME`) on Linux, the latter has priority.\n\n## Support\n\nTo get support please [open a GitHub issue](https://github.com/0neGal/viper/issues/new/choose), and clearly describe the problem with as many details as possible.\n\n### Frequently Asked Questions (FAQ)\n\nMany of the questions and problems you may have might be able to be answered by reading the [FAQ page](FAQ.md). So before opening an issue or asking for support please read through it first!\n\n## Sidenote\n\nGiven that we already have so many Northstar updaters and launchers I urge people to instead of creating new launchers unless there's a very specific reason, just make a pull request on one of the existing ones, otherwise we'll continue to have new ones.\n\n<p align=\"center\">\n\tRelevant xkcd:<br>\n\t<img src=\"assets/xkcd.png\">\n</p>\n\nSome of the existing launchers are listed below:\n * Viper - A launcher, updater and mod manager with an easy to use GUI\n * [FlightCore](https://github.com/R2NorthstarTools/FlightCore) - A more minimal GUI manager and updater, with similar features to Viper\n * [ViperSH](https://github.com/0neGal/viper-sh) - A Bourne Shell, CLI only, Northstar updater and mod manager\n * [VTOL](https://github.com/BigSpice/VTOL) - an updater and manager for mods, very feature rich\n * [r2modman](https://github.com/ebkr/r2modmanPlus) - General purpose mod manager, which has support for Northstar\n * [Papa](https://github.com/AnActualEmerald/papa) - a CLI only installer, updater, and mod manager\n * [FIITE](https://github.com/EladNLG/FastestInstallerInTheEast) - a minimalistic CLI installer and updater.\n\n## Development\n\nIf you wanna edit Viper's code, run it, and so on, you can simply do something along the lines of the below:\n\n```sh\n$ git clone https://github.com/0neGal/viper\n\n$ cd viper\n\n$ npm i\n\n$ npm run start\n```\n\nThis'll launch it with the Electron build installed by `npm`.\n\nAdditionally, if you're creating your own fork you easily publish builds and or make builds with either `npm run publish` or `npm run build` respectively, the prior requiring a `$GH_TOKEN` to be set, as it creates the release itself so it needs a token with access to your repo. So you'd do something along the lines of:\n\n```sh\n$ GH_TOKEN=\"<your very long, private and wonderful token>\" npm run publish\n```\n\nKeep in mind building all Linux builds may take a while on some systems, as packaging the `tar.gz` release can take a while on many CPUs, at least from my testing. All other builds should be done quickly. When using the `publish` command it also automatically uploads the needed files to deploy auto-updates, keep in mind you'd need to have the `repository` setting changed to your new fork's location, otherwise it'll fetch from the original.\n\n## Credits\n\n<a href=\"https://github.com/0neGal/viper/graphs/contributors\">\n\t<img src=\"https://contrib.rocks/image?repo=0neGal/viper\" />\n</a>\n\n**Logo:** Imply#9781<br>\n**Viper Background:** [Uber Panzerhund](https://www.reddit.com/r/titanfall/comments/fwuh2x/take_to_the_skies)<br>\n**Titanfall+Northstar Logo:** [Aftonstjarma](https://www.steamgriddb.com/logo/47851)\n"
  },
  {
    "path": "docs/index.html",
    "content": "<!DOCTYPE html>\n<html>\n\t<head>\n\t\t<title>Viper</title>\n\t\t<meta content=\"Viper\" property=\"og:title\">\n\t\t<meta content=\"https://0negal.github.io/viper/images/image.png\" property=\"og:image\">\n\t\t<meta content=\"Launcher+Updater for TF|2 Northstar\" property=\"og:description\">\n\n\t\t<link rel=\"shortcut icon\" href=\"images/viper.png\" type=\"image/png\">\n\t\t<meta content=\"#C7777F\" data-react-helmet=\"true\" name=\"theme-color\">\n\n\t\t<link rel=\"stylesheet\" href=\"main.css\">\n\t\t<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n\t\t<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n\t\t<link href=\"https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap\" rel=\"stylesheet\">\n\n\t\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0,user-scalable=0,viewport-fit=cover\">\n\t</head>\n\t<body>\n\t\t<div class=\"background\">\n\t\t\t<div class=\"images\">\n\t\t\t\t<div class=\"image active-noanim\"></div>\n\t\t\t</div>\n\t\t\t<div class=\"color\"></div>\n\t\t</div>\n\n\t\t<div class=\"main\">\n\t\t\t<div class=\"box\">\n\t\t\t\t<div class=\"info\">\n\t\t\t\t\t<div class=\"image\">\n\t\t\t\t\t\t<img alt=\"Viper's logo\" src=\"images/viper.png\">\n\t\t\t\t\t\t<h1>Viper</h1>\n\t\t\t\t\t</div>\n\t\t\t\t\t<button>\n\t\t\t\t\t\t<img src=\"images/windows.png\">\n\t\t\t\t\t\tDownload!\n\t\t\t\t\t</button>\n\t\t\t\t\t<a href=\"?release-page\">Click for more download options</a>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"box\">\n\t\t\t\t<img title=\"A screenshot of Viper\" alt=\"A screenshot of Viper\" src=\"images/preview.png\" class=\"preview\"></div>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<script src=\"main.js\"></script>\n\t\t<script src=\"redirect.js\"></script>\n\t</body>\n</html>\n"
  },
  {
    "path": "docs/main.css",
    "content": "/* base properties */\nbody, button {\n\tmargin: 0;\n\tcolor: white;\n\tuser-select: none;\n\tbackground: black;\n\tfont-family: \"Roboto\", sans-serif;\n}\n\n/* positioning, sizing and everything for background elements */\n.background, .background .color, .background .image {\n\ttop: 0;\n\tleft: 0;\n\tright: 0;\n\tbottom: 0;\n\tz-index: -1;\n\tposition: fixed;\n\tbackground-size: cover;\n\tbackground-position: center;\n}\n\n/* tinted background color */\n.background .color {\n\tbackground-color: rgb(0, 0, 0, 0.7);\n}\n\n/* background image elements */\n.background .image {\n\topacity: 0.0;\n\ttransform: scale(1.0);\n\tbackground-position-x: 50%;\n\ttransition: 0.8s opacity ease-in-out,\n\t\t5.0s transform ease-out,\n\t\t5.0s background-position-x ease-out;\n\tbackground-image: url(\"images/backgrounds/1.jpg\");\n}\n\n/* appear but dont animate */\n.background .image.active-noanim {\n\topacity: 1.0;\n}\n\n/* show and animate the image */\n.background .image.active {\n\topacity: 1.0;\n\ttransform: scale(1.05);\n\tbackground-position-x: 25%;\n}\n\n/* main container for everything */\n\n.main {\n\twidth: 80vw;\n\theight: 100vh;\n\tdisplay: flex;\n\tmargin: 0 auto;\n\tmax-width: 800px;\n\tposition: relative;\n\talign-items: center;\n}\n\n/* actual containers with content */\n\n.box {\n\twidth: 100%;\n\theight: 80%;\n\tdisplay: flex;\n\tmax-height: 400px;\n\tborder-radius: 15px;\n\talign-items: center;\n\tjustify-content: center;\n}\n\n/* preview image */\n\n.box .preview {\n\twidth: 100%;\n\tcursor: default;\n\tborder-radius: 15px;\n\ttransition: 0.3s ease-in-out;\n\ttransition-property: box-shadow, transform;\n\tbox-shadow: 0px 8px 5px rgba(0, 0, 0, 0.1);\n}\n\n.box .preview:hover {\n\ttransform: scale(1.05);\n\tbox-shadow: 0px 5px 25px rgba(0, 0, 0, 0.4);\n}\n\n/* hide preview image devices with less than 900px in width */\n@media (max-width: 900px) {\n\t.box:nth-child(2) {\n\t\tdisplay: none;\n\t}\n}\n\n/* main div with text and content */\n.info {\n\ttext-align: center;\n}\n\n/* Viper logo+text */\n.info .image {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\tfont-size: min(3vw, 30px);\n}\n\n/* Viper logo sizing */\n.info .image img {\n\twidth: 30%;\n\tmax-width: 400px;\n}\n\n/* more download options link */\n\n.info a {\n\topacity: 0.8;\n\tcolor: #C7777F;\n\ttext-decoration: none;\n\ttransition: filter 0.2s ease-in;\n}\n\n.info a:hover {\n\tfilter: brightness(80%);\n}\n\n/* big download button */\n\nbutton {\n\tborder: none;\n\tcolor: white;\n\tdisplay: flex;\n\tmargin: 15px auto;\n\tfont-size: 24px;\n\tfont-weight: bold;\n\tpadding: 20px 40px;\n\talign-items: center;\n\tborder-radius: 10px;\n\tjustify-content: center;\n\ttransition: 0.2s ease-in-out;\n\tbackground: linear-gradient(45deg, #81A1C1, #7380ED);\n\tfilter: drop-shadow(0px 8px 5px rgba(0, 0, 0, 0.1));\n}\n\nbutton:hover {\n\ttransform: scale(1.05);\n\tfilter: drop-shadow(0px 5px 15px rgba(0, 0, 0, 0.3)) brightness(110%);\n}\n\nbutton:active {\n\topacity: 0.7;transform: scale(0.98);\n\tfilter: drop-shadow(0px 5px 10px rgba(0, 0, 0, 0.4));\n}\n\n/* platform icon in button */\nbutton img {\n\twidth: 28px;\n\tmargin-right: 15px;\n}\n\n"
  },
  {
    "path": "docs/main.js",
    "content": "// change platform icon in \"Download!\" button if on Linux\nif (navigator.userAgent.match(\"Linux\")) {\n\tdocument.querySelector(\"button img\").src = \"images/linux.png\";\n}\n\n// functionality of \"Download!\" button\ndocument.querySelector(\"button\").addEventListener(\"click\", () => {\n\t// if on Linux, navigate to .AppImage file\n\tif (navigator.userAgent.match(\"Linux\")) {\n\t\treturn location.replace(\"?appimage\");\n\t}\n\n\t// if not on Linux, navigate to .exe setup file\n\tlocation.replace(\"?win-setup\");\n})\n\n// how many wallpapers are in images/backgrounds/\nlet backgrounds = 7; \n\n// initializes the elements for the backgrounds\nfunction init_backgrounds() {\n\t// run through `backgrounds`\n\tfor (let i = 2; i < backgrounds + 1; i++) {\n\t\t// create background element\n\t\tlet background = document.createElement(\"div\");\n\n\t\t// add relevant classes\n\t\tbackground.classList.add(\"image\");\n\n\t\t// set `background-image` CSS property\n\t\tbackground.style.backgroundImage =\n\t\t\t`url(\"images/backgrounds/${i}.jpg\")`;\n\n\t\t// add image to DOM\n\t\tdocument.querySelector(\".background .images\").appendChild(\n\t\t\tbackground\n\t\t)\n\t}\n}; init_backgrounds()\n\n// changes the current image to a random image, if the image picked is\n// the same as the one currently being shown, then we re-run this\n// function, aka duplicates do not happen!\nfunction change_background() {\n\t// get the ID for the new image\n\tlet new_image = Math.floor(Math.random() * (backgrounds - 2) + 1);\n\t// get list of image elements\n\tlet images = document.querySelector(\".background .images\").children;\n\n\t// if the new images is the current images, cancel and re-run\n\tif (images[new_image] ==\n\t\tdocument.querySelector(\".background .images .active\")) {\n\n\t\treturn change_background();\n\t}\n\n\t// run through the images\n\tfor (let i = 0; i < images.length; i++) {\n\t\t// if we're at the new active image, make it active\n\t\tif (i == new_image) {\n\t\t\timages[i].classList.add(\"active\");\n\t\t\tcontinue;\n\t\t}\n\n\t\t// remove any possible `.active` class from this image\n\t\timages[i].classList.remove(\"active\");\n\t}\n}\n\n// makes the initial (Viper) background/image animate on page load\ndocument.addEventListener(\"DOMContentLoaded\", () => {\n\tlet image = document.querySelector(\".image.active-noanim\");\n\n\timage.classList.add(\"active\");\n\timage.classList.remove(\"active-noanim\");\n})\n\n// change wallpaper every 5 seconds\nsetInterval(change_background, 5000);\n"
  },
  {
    "path": "docs/redirect.js",
    "content": "let repo = \"viper\";\nlet author = \"0neGal\";\nlet api = \"https://api.github.com/repos\";\n\nasync function init() {\n\tlet release = await (await fetch(`${api}/${author}/${repo}/releases/latest`)).json();\n\tlet assets = release.assets;\n\n\tlet get = (asset) => {\n\t\tfor (let i in assets) {\n\t\t\tif (assets[i].name.match(asset)) {\n\t\t\t\treturn assets[i].browser_download_url;\n\t\t\t}\n\t\t}\n\t}\n\n\tlet url;\n\tlet search = location.search.replace(/^\\?/, \"\");\n\tswitch(search) {\n\t\tcase \"win-setup\":\n\t\t\turl = get(/Viper-Setup-.*\\.exe$/);\n\t\t\tbreak;\n\t\tcase \"win-portable\":\n\t\t\turl = get(/Viper-.*\\.exe$/);\n\t\t\tbreak;\n\t\tcase \"appimage\":\n\t\t\turl = get(/Viper-.*\\.AppImage$/);\n\t\t\tbreak;\n\t\tcase \"linux\":\n\t\t\turl = get(/viper-.*.tar\\.gz$/);\n\t\t\tbreak;\n\t\tcase \"rpm\":\n\t\t\turl = get(/viper-.*\\.x86_64\\.rpm$/);\n\t\t\tbreak;\n\t\tcase \"deb\":\n\t\t\turl = get(/viper_.*_amd64\\.deb$/);\n\t\t\tbreak;\n\t\tcase \"release-page\":\n\t\t\turl = release.html_url;\n\t\t\tbreak;\n\t\tdefault:\n\t\t\treturn;\n\t}\n\n\tlocation.replace(url);\n}; init()\n"
  },
  {
    "path": "package.json",
    "content": "{\n\t\"name\": \"viper\",\n\t\"productName\": \"Viper\",\n\t\"version\": \"1.13.0\",\n\t\"description\": \"Launcher+Updater for TF|2 Northstar\",\n\t\"main\": \"src/index.js\",\n\t\"build\": {\n\t\t\"appId\": \"com.0negal.viper\",\n\t\t\"directories\": {\n\t\t\t\"buildResources\": \"src/assets/icons\"\n\t\t},\n\t\t\"nsis\": {\n\t\t\t\"installerIcon\": \"icon.ico\",\n\t\t\t\"uninstallerIcon\": \"icon.ico\",\n\t\t\t\"installerHeaderIcon\": \"icon.ico\"\n\t\t},\n\t\t\"linux\": {\n\t\t\t\"icon\": \"512x512.png\",\n\t\t\t\"category\": \"Game\",\n\t\t\t\"target\": [\n\t\t\t\t\"AppImage\",\n\t\t\t\t\"tar.gz\",\n\t\t\t\t\"deb\",\n\t\t\t\t\"rpm\"\n\t\t\t]\n\t\t}\n\t},\n\t\"scripts\": {\n\t\t\"langs:check\": \"node scripts/langs.js --check\",\n\t\t\"langs:format\": \"node scripts/langs.js --format\",\n\t\t\"langs:localize\": \"node scripts/langs.js --localize\",\n\t\t\"start\": \"npx electron src/index.js\",\n\t\t\"debug\": \"npm run devtools\",\n\t\t\"devtools\": \"npx electron src/index.js --devtools\",\n\t\t\"build\": \"npx electron-builder --win nsis --win portable --linux --publish never\",\n\t\t\"build:windows\": \"npx electron-builder --win nsis --win portable --publish never\",\n\t\t\"build:linux\": \"npx electron-builder --linux --publish never\",\n\t\t\"publish\": \"npx electron-builder --win nsis --win portable --linux --publish always\",\n\t\t\"publish:windows\": \"npx electron-builder --win nsis --win portable --publish always\",\n\t\t\"publish:linux\": \"npx electron-builder --linux --publish always\"\n\t},\n\t\"repository\": {\n\t\t\"type\": \"git\",\n\t\t\"url\": \"git+https://github.com/0neGal/viper\"\n\t},\n\t\"author\": \"0neGal <mail@0negal.com>\",\n\t\"license\": \"GPL-3.0-or-later\",\n\t\"bugs\": {\n\t\t\"url\": \"https://github.com/0neGal/viper/issues\"\n\t},\n\t\"homepage\": \"https://github.com/0neGal/viper#readme\",\n\t\"dependencies\": {\n\t\t\"electron-updater\": \"^6.3.0\",\n\t\t\"enquirer\": \"^2.4.1\",\n\t\t\"flattenizer\": \"^1.1.3\",\n\t\t\"follow-redirects\": \"^1.15.6\",\n\t\t\"fs-extra\": \"^10.0.0\",\n\t\t\"fuse.js\": \"^6.5.3\",\n\t\t\"jsonrepair\": \"^2.2.1\",\n\t\t\"marked\": \"^4.0.10\",\n\t\t\"minimist\": \"^1.2.8\",\n\t\t\"recursive-copy\": \"^2.0.13\",\n\t\t\"simple-vdf\": \"^1.1.1\",\n\t\t\"unzip-stream\": \"^0.3.2\"\n\t},\n\t\"devDependencies\": {\n\t\t\"electron\": \"^33.2.1\",\n\t\t\"electron-builder\": \"^24.13.3\"\n\t}\n}\n"
  },
  {
    "path": "scripts/downloads.js",
    "content": "const https = require(\"https\");\nconst args = require(\"minimist\")(process.argv.slice(2), {\n\tboolean: [\n\t\t\"help\",\n\t\t\"total\",\n\t\t\"releases\",\n\t\t\"sort-releases\",\n\t\t\"total-platforms\",\n\t\t\"include-prereleases\"\n\t],\n\n\tdefault: {\n\t\t\"help\": false,\n\t\t\"total\": true,\n\t\t\"releases\": true,\n\t\t\"sort-releases\": false,\n\t\t\"total-platforms\": true,\n\t\t\"include-prereleases\": false\n\t}\n})\n\n// help message\nif (args[\"help\"]) {\n\tconsole.log(`options:\n  --help                  shows this help message\n  --no-total              dont show total downloads\n  --no-releases           dont show individual releases\n  --sort-releases         sorts releases by download count\n  --no-total-platforms    hides individual total platform downloads \n  --include-prereleases   includes prereleases when listing out\n                          individual releases\n\t`.trim()) // the trim removes the last blank newline\n\n\tprocess.exit(0);\n}\n\n// when parsing releases, these will be filled\nlet releases = {};\nlet total = {\n\tall: 0,\n\tlinux: 0,\n\twindows: 0\n}\n\n// calculates what percentage `value` is of `total`\nlet percent = (total, value) => {\n\treturn (value / total * 100).toFixed(2) + \"%\";\n}\n\n// parses a release object from the GitHub API, and then it changes\n// `releases` and `total` accordingly\nlet parse_release = (release) => {\n\tif (release.prerelease && ! args[\"include-prereleases\"]) {\n\t\treturn;\n\t}\n\n\tlet name = release.name;\n\tlet assets = release.assets;\n\n\t// run through downloadable files from release\n\tfor (let i = 0; i < assets.length; i++) {\n\t\t// dont count blockmaps\n\t\tif (assets[i].name.match(\"blockmap\")) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tswitch(assets[i].name) {\n\t\t\t// dont count these files\n\t\t\tcase \"latest.yml\":\n\t\t\tcase \"latest-linux.yml\":\n\t\t\t\tcontinue;\n\n\t\t\tdefault:\n\t\t\t\tlet platform;\n\t\t\t\tlet downloads = assets[i].download_count;\n\n\t\t\t\t// assume platform is Windows if filename ends with\n\t\t\t\t// `.exe`, and Linux if not\n\t\t\t\tif (assets[i].name.endsWith(\".exe\")) {\n\t\t\t\t\tplatform = \"windows\";\n\t\t\t\t} else {\n\t\t\t\t\tplatform = \"linux\";\n\t\t\t\t}\n\n\t\t\t\t// create new object for this release in `releases` if\n\t\t\t\t// it doesn't already have one\n\t\t\t\tif (! releases[name]) {\n\t\t\t\t\treleases[name] = {\n\t\t\t\t\t\tall: 0,\n\t\t\t\t\t\tlinux: 0,\n\t\t\t\t\t\twindows: 0\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// add total download counts\n\t\t\t\ttotal.all += downloads;\n\t\t\t\treleases[name].all += downloads;\n\n\t\t\t\t// add platform specific download counts\n\t\t\t\ttotal[platform] += downloads;\n\t\t\t\treleases[name][platform] += downloads;\n\t\t}\n\t}\n}\n\n// pads out `str` until it's `length` long\nlet pad = (str, length) => {\n\tstr = String(str);\n\n\twhile (str.length < length) {\n\t\tstr += \" \";\n\t}\n\n\treturn str;\n}\n\n// takes in `obj` and prints out a table based on it, each key in the\n// objects inside `obj` will be used as a column, and it'll attempt to\n// pad them so it the output looks properly \"columnized\"\nlet print_table = (obj) => {\n\t// keep track of the longest value of a key\n\tlet max_lengths = {};\n\n\t// run through all the keys\n\tfor (let i in obj) {\n\t\tfor (let ii in obj[i]) {\n\t\t\t// get length of current key\n\t\t\tlet length = String(obj[i][ii]).length;\n\n\t\t\t// if the currently longest value for this type of key is\n\t\t\t// smaller than `length`, then we make `length` the newest\n\t\t\t// longest value for this type of key, the same happens if\n\t\t\t// there's not been a registered longest key for this type\n\t\t\t// of key yet\n\t\t\tif (max_lengths[ii] < length || ! max_lengths[ii]) {\n\t\t\t\tmax_lengths[ii] = length;\n\t\t\t}\n\t\t}\n\t}\n\n\t// run through object again\n\tfor (let i in obj) {\n\t\t// we'll add to this, then print it later\n\t\tlet line_str = \"\";\n\n\t\t// run through keys\n\t\tfor (let ii in obj[i]) {\n\t\t\t// add value to `line_str` padding it to the longest found\n\t\t\t// value for this type of key and adding 5 for some margin\n\t\t\tline_str += pad(obj[i][ii], max_lengths[ii] + 5);\n\t\t}\n\n\t\t// print the line of this table out\n\t\tconsole.log(line_str);\n\t}\n}\n\nlet parse_json = (json) => {\n\t// parse `json`\n\tjson = JSON.parse(json);\n\n\t// run through releases and parse them\n\tfor (let i = 0; i < json.length; i++) {\n\t\tparse_release(json[i]);\n\t}\n\n\t// should we should print the individual release stats?\n\tif (args[\"releases\"]) {\n\t\t// should we sort the releases?\n\t\tif (args[\"sort-releases\"]) {\n\t\t\t// the finalized and sorted `releases` will go here\n\t\t\tlet sorted = {};\n\n\t\t\t// sort `releases` using the `.all` property\n\t\t\treleases = Object.keys(releases).sort((a, b) => {\n\t\t\t\treturn releases[b].all - releases[a].all;\n\t\t\t}).forEach((key) => {\n\t\t\t\tsorted[key] = releases[key];\n\t\t\t})\n\n\t\t\t// set `releases` to now be the sorted version\n\t\t\treleases = sorted;\n\t\t}\n\n\t\t// initialize `table` with a header\n\t\tlet table = [{\n\t\t\tversion: \"VERSION\",\n\t\t\ttotal:   \"TOTAL\",\n\t\t\twindows: \"WINDOWS\",\n\t\t\tlinux:   \"LINUX\"\n\t\t}]\n\n\t\t// runs through `releases` adding the various lines of the table\n\t\t// to `table`, essentially converting it to something we can use\n\t\t// `print_table()` on and nothing else\n\t\tfor (let i in releases) {\n\t\t\ttable.push({\n\t\t\t\tversion: i,\n\n\t\t\t\ttotal: releases[i].all,\n\n\t\t\t\twindows: releases[i].windows + \" (\" + percent(\n\t\t\t\t\treleases[i].all, releases[i].windows\n\t\t\t\t) + \")\",\n\n\t\t\t\tlinux: releases[i].linux + \" (\" + percent(\n\t\t\t\t\treleases[i].all, releases[i].linux\n\t\t\t\t) + \")\"\n\t\t\t})\n\t\t}\n\n\t\t// print the finalized table\n\t\tprint_table(table);\n\t}\n\n\t// if we dont want to print the total downloads, then we just exit\n\tif (! args[\"total\"]) {\n\t\tprocess.exit(0);\n\t}\n\n\t// if we've already printed the list of releases, we add an extra\n\t// space between this and the total\n\tif (args[\"releases\"]) {\n\t\tconsole.log();\n\t}\n\n\t// print out the total\n\tconsole.log(\"Total downloads: \" + total.all);\n\n\t// if we shouldn't print the platform distribution, then these empty\n\t// strings will be used instead\n\tlet linux_percent = \"\";\n\tlet windows_percent = \"\";\n\n\t// get the percent of platform distribution\n\tif (args[\"total-platforms\"]) {\n\t\tlet linux_percent =\n\t\t\t\"(\" + percent(total.all, total.linux) + \")\";\n\n\t\tlet windows_percent =\n\t\t\t\"(\" + percent(total.all, total.windows) + \")\";\n\t}\n\n\t// print Windows platform distribution\n\tconsole.log(\n\t\t\"        Windows: \" +\n\t\ttotal.windows, windows_percent\n\t)\n\n\t// print Linux platform distribution\n\tconsole.log(\n\t\t\"          Linux: \" +\n\t\ttotal.linux, linux_percent\n\t)\n}\n\nlet link = \"/repos/0neGal/viper/releases\";\n\n// request info about releases via GitHub's API\nhttps.get({\n\thost: \"api.github.com\",\n\tport: 443,\n\tpath: link,\n\tmethod: \"GET\",\n\theaders: { \"User-Agent\": \"viper\" }\n}, (res) => {\n\tres.setEncoding(\"utf8\");\n\tlet res_data = \"\";\n\n\tres.on(\"data\", data => {\n\t\tres_data += data;\n\t})\n\n\t// we got the data, now lets parse it!\n\tres.on(\"end\", () => {\n\t\tparse_json(res_data);\n\t})\n})\n"
  },
  {
    "path": "scripts/langs.js",
    "content": "const fs = require(\"fs\");\nconst dialog = require(\"enquirer\");\nconst flat = require(\"flattenizer\");\nconst args = require(\"minimist\")(process.argv.slice(2), {\n\tboolean: [\n\t\t\"help\",\n\t\t\"check\",\n\t\t\"format\",\n\t\t\"localize\"\n\t],\n\n\tdefault: {\n\t\t\"help\": false,\n\t\t\"check\": false,\n\t\t\"format\": false,\n\t\t\"localize\": false\n\t}\n})\n\nconsole = require(\"../src/modules/console\");\n\n// help message\nif (args[\"help\"]) {\n\tconsole.log(`options:\n  --help                  shows this help message\n  --check                 checks for incorrectly formatted lang files\n                          and missing localizations\n  --format                formats all lang files correctly if the files\n                          can be read and parsed\n  --localize              allows you add missing incorrectly\n                          localizations, and edit old ones\n\t`.trim()) // the trim removes the last blank newline\n\n\tprocess.exit(0);\n}\n\n// move into `scripts` folder, makes sure all file system requests work\n// identically to `require()`\nprocess.chdir(__dirname);\n\n// get list of files in `src/lang/`, except for `maintainers.json` these\n// should all be language files\nlet langs = fs.readdirSync(\"../src/lang\");\n\n// get the English language file and flatten it\nlet lang = flat.flatten(require(\"../src/lang/en.json\"));\n\n// formats all files automatically, nothing too fancy, it ignores\n// `en.json` however, as its manually edited.\nlet format = (logging = true) => {\n\t// run through langs\n\tlangs.forEach((locale_file) => {\n\t\t// ignore these files\n\t\tif (locale_file == \"en.json\"\n\t\t\t|| locale_file == \"maintainers.json\") {\n\t\t\treturn;\n\t\t}\n\n\t\t// path to lang file\n\t\tlet file_path = \"../src/lang/\" + locale_file;\n\n\t\ttry {\n\t\t\t// attempt read, parse and flatten `file_path`\n\t\t\tlet json = flat.flatten(\n\t\t\t\tJSON.parse(fs.readFileSync(file_path))\n\t\t\t)\n\n\t\t\t// sort `json`\n\t\t\tjson = Object.fromEntries(\n\t\t\t\tObject.entries(json).sort()\n\t\t\t)\n\n\t\t\t// delete keys that are only found in `locale_file` but not\n\t\t\t// in the English localization file, if something doesn't\n\t\t\t// exist in the English localization file, then it shouldn't\n\t\t\t// exist at all!\n\t\t\tfor (let i in json) {\n\t\t\t\tif (! lang[i]) {\n\t\t\t\t\tdelete json[i];\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tjson = flat.unflatten(json);\n\n\t\t\t// attempt to stringify earlier JSON, with default\n\t\t\t// formatting, directly into `file_path`\n\t\t\tfs.writeFileSync(\n\t\t\t\tfile_path, JSON.stringify(json, null, \"\\t\")\n\t\t\t)\n\t\t}catch(err) {\n\t\t\t// something went wrong!\n\t\t\tconsole.error(\"Couldn't format: \" + locale_file);\n\t\t}\n\t})\n\n\tconsole.ok(\"Formatted all localization files.\");\n}\n\n// starts up a prompt interface to edit localization strings, letting\n// you both add missing ones, and change old ones\nlet localize = async () => {\n\t// check if there's any missing keys\n\tlet problems = check(false);\n\n\t// this'll have the `choices` for language picking prompt\n\tlet lang_list = [];\n\n\t// run through langs\n\tlangs.forEach((locale_file) => {\n\t\t// ignore these files\n\t\tif (locale_file == \"en.json\"\n\t\t\t|| locale_file == \"maintainers.json\") {\n\t\t\treturn;\n\t\t}\n\n\t\t// default value\n\t\tlet lang_name = locale_file;\n\n\t\t// are we missing any keys? if so, add a \"(missing)\" label at\n\t\t// the end of the language name\n\t\tlet missing = (problems[lang_name] && problems[lang_name].length);\n\t\tif (missing) {\n\t\t\tlang_name += ` (missing: ${missing})`;\n\t\t}\n\n\t\t// add to list of langs\n\t\tlang_list.push(lang_name);\n\t})\n\n\t// prompt for which lang to edit\n\tlet picked_lang = await new dialog.AutoComplete({\n\t\tchoices: lang_list,\n\t\tmessage: \"Pick a language to edit:\",\n\t}).run()\n\n\t// remove extra labels after the lang file name itself\n\tpicked_lang = picked_lang.replace(/ \\(.*/, \"\");\n\n\t// this'll contain the languages flattened contents\n\tlet lang_keys;\n\n\ttry {\n\t\t// attempt to read, parse and flatten the language file\n\t\tlang_keys = flat.flatten(require(\"../src/lang/\" + picked_lang));\n\t}catch (err) {\n\t\t// something went wrong!\n\t\tconsole.error(\"Couldn't read and parse language file\");\n\t\tprocess.exit(1);\n\t}\n\n\t// should we just show the keys that are missing, or everything?\n\tlet just_missing = false;\n\n\t// get just the flattened keys of the language\n\tlet keys = Object.keys(lang_keys);\n\n\t// are there any missing keys?\n\tif (problems[picked_lang].length) {\n\t\t// prompt for whether we should only show missing keys\n\t\tjust_missing = await new dialog.Confirm({\n\t\t\tmessage: \"Add just missing keys without editing all keys?\",\n\t\t}).run()\n\n\t\t// if we should just show missing keys, remove all other keys,\n\t\t// if we're allowed to show other keys, then we'll at least add\n\t\t// the missing keys\n\t\tif (just_missing) {\n\t\t\tkeys = problems[picked_lang];\n\t\t} else {\n\t\t\tkeys = [\n\t\t\t\t...problems[picked_lang],\n\t\t\t\t...keys,\n\t\t\t]\n\t\t}\n\t}\n\n\t// add \"Save changes\" option\n\tkeys = [\n\t\t\"Save changes\",\n\t\t...keys\n\t]\n\n\t// add \"(missing)\" label to missing keys\n\tfor (let i = 0; i < keys.length; i++) {\n\t\tif (! just_missing && problems[picked_lang].includes(keys[i])) {\n\t\t\tkeys[i] = keys[i] + \" \\x1b[91m(missing)\\x1b[0m\";\n\t\t}\n\t}\n\n\t// this'll hold the flattened edits we make\n\tlet edited_keys = {};\n\n\t// starts the process of editing a key\n\tlet edit_key = async () => {\n\t\t// prompt for which key to edit\n\t\tlet key_to_edit = await new dialog.AutoComplete({\n\t\t\tlimit: 15,\n\t\t\tchoices: [...keys],\n\t\t\tmessage: \"Pick a key to edit:\"\n\t\t}).run()\n\n\t\t// if \"Save changes\" was picked then return all the edits we've\n\t\t// made and stop prompting for new edits\n\t\tif (key_to_edit == \"Save changes\") {\n\t\t\treturn edited_keys;\n\t\t}\n\n\t\t// strip labels from chosen key name\n\t\tkey_to_edit = key_to_edit.split(\" \")[0];\n\n\t\t// prompt for what to set the key to\n\t\tlet edited_key = await new dialog.Input({\n\t\t\ttype: \"input\",\n\t\t\tmessage: `Editing: ${key_to_edit}\\n` +\n\t\t\t\"  Original string: \" + lang[key_to_edit] + \"\\n\"\n\t\t}).run()\n\n\t\t// add the edited key in `edited_keys`\n\t\tedited_keys[key_to_edit] = edited_key;\n\n\t\t// add \"(edited)\" to the label of this key\n\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\tif (keys[i].split(\" \")[0] == key_to_edit) {\n\t\t\t\tkeys[i] = key_to_edit + \" \\x1b[94m(edited)\\x1b[0m\";\n\t\t\t}\n\t\t}\n\n\t\t// clear screen and ask for the next edit to be made\n\t\tconsole.clear();\n\t\treturn edit_key();\n\t}\n\n\t// start the process of key editing, whenever the below function\n\t// returns with the list of edits, it also only first returns when\n\t// all the changes have been made and \"Save changes\" has been\n\t// selected in the menu\n\tlet changes = await edit_key();\n\n\tconsole.clear();\n\n\ttry {\n\t\t// merge edits and original lang file, then unflatten them\n\t\tlet final_json = flat.unflatten({\n\t\t\t...lang_keys,\n\t\t\t...changes\n\t\t})\n\n\t\t// attempt to write `final_json` to the language file\n\t\tfs.writeFileSync(\n\t\t\t\"../src/lang/\" + picked_lang,\n\t\t\tJSON.stringify(final_json, null, \"\\t\")\n\t\t)\n\n\t\tconsole.ok(\"Saved changes: \" + picked_lang);\n\n\t\t// check for changes\n\t\tcheck(true);\n\n\t\t// format everything\n\t\tformat(true);\n\t}catch(err) {\n\t\t// something went wrong!\n\t\tconsole.error(\"Failed to save changes: \" + picked_lang);\n\t}\n}\n\n// checks whether or not language files are missing any keys, and\n// whether they're even parseable.\n//\n// an object will be returned containing information about each file and\n// which, if any, keys are missing from them\nlet check = (logging = true) => {\n\t// this'll contain the missing keys for all the files, if any\n\tlet problems = {};\n\n\t// this'll be changed to `true` if any errors at any point arise\n\tlet has_problems = false;\n\n\t// get list of maintainers for each language\n\tlet maintainers = require(\"../src/lang/maintainers.json\");\n\n\t// run through langs\n\tlangs.forEach((locale_file) => {\n\t\t// ignore this file, it's not a language file\n\t\tif (locale_file == \"maintainers.json\") {return}\n\n\t\t// this'll contain missing keys\n\t\tlet missing = [];\n\n\t\t// this'll contain the flattened language file contents\n\t\tlet locale = false;\n\n\t\t// this is the list of maintainers for this language\n\t\tlet lang_maintainers = maintainers.list[\n\t\t\tlocale_file.replace(/\\..*$/, \"\")\n\t\t]\n\n\t\t// attempt read, parse and flatten language file\n\t\ttry {\n\t\t\tlocale = flat.flatten(require(\"../src/lang/\" + locale_file));\n\t\t}catch(err) {\n\t\t\t// we couldn't parse it!\n\t\t\tif (logging) {\n\t\t\t\thas_problems = true;\n\t\t\t\tconsole.error(`!! ${locale_file} is not formatted right !!`);\n\t\t\t}\n\n\t\t\treturn;\n\t\t}\n\n\t\t// run through keys, and note ones that are missing from\n\t\t// `en.json` in this lang\n\t\tfor (let i in lang) {\n\t\t\tif (! locale[i]) {\n\t\t\t\tmissing.push(i);\n\t\t\t}\n\t\t}\n\n\t\t// add missing keys to `problems`\n\t\tproblems[locale_file] = missing;\n\n\t\t// was there any missing keys?\n\t\tif (missing.length > 0) {\n\t\t\t// this is a problem\n\t\t\thas_problems = true;\n\n\t\t\t// do nothing if we're not supposed to log anything\n\t\t\tif (! logging) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// log language file with missing keys\n\t\t\tconsole.error(`${locale_file} is missing:`);\n\n\t\t\t// log missing keys\n\t\t\tfor (let i in missing) {\n\t\t\t\tconsole.error(`  ${missing[i]}`);\n\t\t\t}\n\n\t\t\t// spacing\n\t\t\tconsole.log();\n\n\t\t\t// log the maintainers for this language\n\t\t\tconsole.log(\"Maintainers: \");\n\t\t\tfor (let i in lang_maintainers) {\n\t\t\t\tconsole.log(`  ${lang_maintainers[i]}`);\n\t\t\t}\n\n\t\t\tconsole.log(\"\\n\");\n\t\t}\n\t})\n\n\t// if no problems occurred, and we can log things, then print that\n\t// everything went just fine!\n\tif (! has_problems && logging) {\n\t\tconsole.ok(\"All localizations are complete and parseable.\");\n\t}\n\n\treturn problems;\n}\n\n// run `check()` if `--check()` is set\nif (args[\"check\"]) {\n\tlet has_problems = false;\n\n\t// check localizations, and set `has_problems` depending on whether\n\t// any localization files have problems\n\tObject.values(check()).forEach((item) => {\n\t\tif (item.length) {\n\t\t\thas_problems = true;\n\t\t}\n\t});\n\n\t// exit with the correct exit code\n\tif (has_problems) {\n\t\tprocess.exit(1);\n\t} else {\n\t\tprocess.exit();\n\t}\n}\n\n// run `format()` if `--format` is set\nif (args[\"format\"]) {\n\tformat();\n}\n\n// run `localize()` if `--localize` is set\nif (args[\"localize\"]) {\n\tlocalize();\n}\n"
  },
  {
    "path": "scripts/publish.sh",
    "content": "#!/bin/sh\n\nVERSION=\"v$(jq '.version' package.json -r)\"\nLOCK1=\"v$(jq '.version' package-lock.json -r)\"\nLOCK2=\"v$(jq '.packages[\"\"].version' package-lock.json -r)\"\n\nREPO=\"$(jq '.repository.url' package.json -r | sed 's/.*.com\\///g')\"\n\nREMOTEVERSION=\"$(curl --silent \"https://api.github.com/repos/0neGal/viper/releases/latest\" | grep '\"tag_name\":' | sed -E 's/.*\"([^\"]+)\".*/\\1/')\"\n\n[ \"$REMOTEVERSION\" = \"$VERSION\" ] && {\n\techo \"A release already exists with the current version number!\"\n\texit 1\n}\n\n[ \"$VERSION\" != \"$LOCK1\" ] && {\n\techo \"Two seperate version numbers in package.json and package-lock.json\"\n\techo \"  $VERSION, $LOCK1\"\n\texit 1\n}\n\n[ \"$LOCK1\" != \"$LOCK2\" ] && {\n\techo \"Version mismatches in package-lock.json\"\n\techo \"  $LOCK1, $LOCK2\"\n\texit 1\n}\n\n\nnode scripts/langs.js --check || {\n\techo \"Please fix localization errors before publishing...\"\n\texit 1\n}\n\nTOKEN=\"$GH_TOKEN\"\n\n[ \"$TOKEN\" = \"\" ] && {\n\techo \"GH_TOKEN is not set, please type it below:\"\n\tread -p \"> \" TOKEN\n}\n\nGH_TOKEN=\"$TOKEN\" npm run publish\n"
  },
  {
    "path": "src/app/css/dragui.css",
    "content": "@import \"theming.css\";\n\n/*\n This stylesheet is meant for the DragUI, i.e the UI that pops up when dragging\n a modfile over the window.\n*/\n\n#dragUI {\n\ttop: 0;\n\tleft: 0;\n\tright: 0;\n\tbottom: 0;\n\tcolor: white;\n\topacity: 0.0;\n\tposition: fixed;\n\tz-index: 1000000;\n\tpointer-events: none;\n\tbackground: var(--bg);\n\tbackdrop-filter: blur(15px);\n\ttransition: 0.1s ease-in-out;\n}\n\n#dragUI.shown {\n\topacity: 1.0;\n\tpointer-events: all;\n}\n\n#dragUI #dragitems {\n\t--size: 25vw;\n\ttop: 50%;\n\tleft: 50%;\n\topacity: 0.6;\n\tposition: absolute;\n\ttext-align: center;\n\twidth: var(--size);\n\theight: var(--size);\n\tmargin-top: calc(var(--size) / 2 * -1);\n\tmargin-left: calc(var(--size) / 2 * -1);\n}\n\n#dragUI #dragitems #icon {\n\twidth: 100%;\n\theight: 100%;\n\tfilter: invert(1);\n\ttransform: scale(0.45);\n\tbackground-size: cover;\n\tbackground-image: url(\"../icons/download.png\");\n\ttransition: 0.1s ease-in-out;\n}\n\n#dragUI.shown #dragitems #icon {\n\ttransform: scale(0.5);\n}\n\n#dragUI #dragitems #text {\n\ttop: -5vw;\n\tposition: relative;\n}\n"
  },
  {
    "path": "src/app/css/grid.css",
    "content": "@import \"theming.css\";\n\n.grid, .grid .el, .popup .misc, .popup .loading {\n\t--spacing: calc(var(--padding) / 2);\n\t--height: calc(var(--padding) * 3.5);\n\t--mischeight: calc(var(--padding) * 1.5);\n\n\tanimation-duration: 0.15s;\n\tanimation-iteration-count: 1;\n\tanimation-name: fadein;\n\tanimation-fill-mode: forwards;\n\tanimation-timing-function: ease-in-out;\n\n\topacity: 0.0;\n\ttransition: 0.15s ease-in-out;\n}\n\n.grid .el.no-animation,\n.popup .misc.no-animation,\n.popup .loading.no-animation {\n\topacity: 1.0;\n\tanimation-name: none;\n}\n\n.grid .el, input.search,\n.popup #close, .popup .misc button,\n.option .actions select, .option .actions input {\n\tcolor: white;\n\tdisplay: flex;\n\talign-items: center;\n\theight: var(--height);\n\tmargin: var(--spacing);\n\tpadding: var(--spacing);\n\tbackground: var(--selbg);\n\tborder-radius: var(--spacing);\n\twidth: calc(50% - var(--spacing) * 4);\n}\n\n.popup .misc, input.search, .option .actions input {\n\t--height: var(--mischeight);\n}\n\n.popup .misc {\n\tdisplay: flex;\n}\n\n.popup .misc.vertical {\n\tdisplay: block;\n}\n\n.popup .misc.fixed {\n\twidth: 100%;\n\tposition: fixed;\n}\n\ninput.search,\n.option .actions input,\n.option .actions select {\n\tborder: none;\n\toutline: none;\n\ttransition: filter 0.15s ease-in-out;\n\twidth: calc(100% - var(--spacing) * 2);\n}\n\ninput.search:focus, \n.option .actions input:focus,\n.option .actions button:active {\n\tfilter: brightness(1.5);\n}\n\n.popup .misc button {\n\t--height: calc(var(--padding) * 1.5);\n\n\tpadding: 0px;\n\tmargin-left: 0px;\n\tpadding: 0px !important;\n\twidth: var(--height) !important;\n}\n\n.popup .misc button.long {\n\twidth: max-content !important;\n\tpadding-right: var(--spacing) !important;\n}\n\n.popup .misc button select {\n\tcolor: white;\n\tborder: none;\n\topacity: 0.6;\n\toutline: none;\n\tbackground: transparent;\n}\n\n.popup .misc button select option {\n\tcolor: white;\n\tbackground: var(--selbg);\n}\n\n.popup .misc button img {\n\tmargin: 0px;\n\topacity: 0.6;\n\twidth: var(--height);\n\ttransform: scale(0.5);\n\theight: var(--height) !important;\n}\n\n.popup .misc button:last-child {\n\tmargin-left: 0px !important;\n}\n\n.popup#preview #close,\n.popup .misc.vertical button {\n\tmargin: var(--spacing) var(--spacing) 0 auto !important;\n}\n\n.popup .loading {\n\twidth: 100%;\n\tcolor: white;\n\tdisplay: flex;\n\tposition: absolute;\n\ttext-align: center;\n\talign-items: center;\n\tjustify-content: center;\n\theight: calc(100% - var(--mischeight) - var(--height));\n}\n\n.popup .message {\n\tcolor: white;\n\ttext-align: center;\n\tmargin: var(--padding);\n\twidth: calc(100% - var(--padding));\n}\n\n.grid .el .image, .grid .el .image img {\n\twidth: var(--height);\n\theight: var(--height);\n\ttransition: opacity 0.2s;\n\tmargin-right: var(--spacing);\n\tborder-radius: var(--spacing);\n}\n\n.grid input {\n\tmargin: 0;\n\twidth: 100%;\n}\n\n#modsdiv .el:has(.switch:not(.on)) .image img {\n\topacity: 0.5;\n}\n\n.grid .el .image img.blur {\n\tz-index: -1;\n\tposition: relative;\n\tfilter: blur(10px);\n\ttop: calc(var(--height) * -1 + 5px);\n}\n\n.grid .el .text {\n\theight: inherit;\n\toverflow: hidden;\n\toverflow-x: scroll;\n\tpadding: var(--padding) 0px;\n}\n\n.grid .el.has-icon .text {\n\twidth: calc(100% - var(--height));\n}\n\n.grid .el .title, .grid .el .description {\n\theight: 1.2em;\n\toverflow: hidden;\n\twhite-space: nowrap;\n\ttext-overflow: ellipsis;\n}\n\n.grid .el .title {\n\tfont-size: 1.2em;\n\tfont-weight: 700;\n}\n\n.popup .message #loadmore {\n\tbackground: rgb(var(--blue2));\n}\n\n.grid .el .description {font-size: 0.8em}\n.grid .el button {\n\tmargin-top: var(--spacing);\n}\n\n.grid .el button.info {\n\tbackground: rgb(var(--blue2));\n}\n\n.grid .el .switch {\n\ttop: 3px;\n\tposition: relative;\n\tbackground: var(--bg);\n}\n\n.grid .el .switch:not(.grid .el .switch.on)::after {\n\tbackground: var(--selbg);\n}\n"
  },
  {
    "path": "src/app/css/launcher.css",
    "content": "@import \"theming.css\";\n\n/*\n This stylesheet is meant for various elements around the launcher,\n notably the navbar, launch buttons, and sidebar.\n*/\n\n.gamesContainer {\n\twidth: 10%;\n\theight: 100%;\n\tmin-width: 95px;\n\tmax-width: 120px;\n\n\tfloat: left;\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-content: center;\n}\n\n.mainContainer {\n\theight: 100%;\n\tflex-grow: 1;\n\tdisplay: flex;\n\tposition: relative;\n}\n\n/* nav bar buttons */\n.gamesContainer button {\n\tbackground-size: 90%;\n\tbackground-position: center;\n\tbackground-repeat: no-repeat;\n\n\tborder: none;\n\ttransition: 0.3s ease-in-out;\n\tbackground-color: transparent;\n\n\tmargin: 20px;\n\tposition: relative;\n\tborder-radius: 0px;\n\tbox-sizing: border-box;\n\tflex-basis: calc(100% - 10px);\n}\n\n.gamesContainer button.inactive {\n\topacity: 0.5;\n\ttransform: scale(0.9);\n}\n\n.gamesContainer button.inactive:hover {\n\ttransform: scale(0.95);\n\tfilter: brightness(120%);\n}\n\n.gamesContainer button::before {\n\tcontent: \"\";\n\tdisplay: block;\n\tpadding-top: 100%;\n}\n\n.gamesContainer button .content {\n\twidth: 100%;\n\theight: 100%;\n\ttop: 0; left: 0;\n\tposition: absolute;\n}\n\n#vpBtn {background-image: url(\"../icons/viper.png\")}\n#nsBtn {background-image: url(\"../icons/northstar.png\")}\n#tfBtn {\n\tbackground-image: url(\"../icons/titanfall2.png\");\n\tbackground-size: 69%; /* nice */\n}\n\n.contentContainer {\n\twidth: 85%;\n\tcolor: white;\n\tflex-grow: 1;\n\topacity: 1.0;\n\tmargin-left: 5%;\n\tposition: absolute;\n\ttransition: 0.15s ease-in-out;\n}\n\n.contentContainer.hidden {\n\topacity: 0.0;\n\tpointer-events: none;\n}\n\n.contentMenu {\n\tpadding: 0;\n\tflex-grow: 1;\n\tdisplay: flex;\n\tfont-size: 20px;\n\tlist-style: none;\n\tmargin-bottom: 0;\n\talign-items: center;\n\tjustify-content: center;\n\tmargin-top: var(--padding);\n}\n\n.contentMenu li {\n\topacity: 0.6;\n\tmargin: 0 26px;\n\tcursor: pointer;\n\tfont-weight: 700;\n\ttext-align: center;\n\theight: fit-content;\n\ttransition: opacity 0.3s ease-in-out;\n}\n\n.contentMenu li:last-child {margin-right: 0px}\n.contentMenu li:first-child {margin-left: 0px}\n\n.contentMenu li:hover,\n.contentMenu li.active-selection {opacity: 0.7}\n\n.contentMenu li[active] {\n\topacity: 1.0;\n\tpointer-events: none;\n}\n\n.contentMenu li::after {\n\ttop: 10px;\n\twidth: 30px;\n\theight: 5px;\n\topacity: 0.0;\n\tcontent: \" \";\n\tdisplay: block;\n\ttext-align: center;\n\tposition: relative;\n\tborder-radius: 50px;\n\tleft: calc(50% - 15px);\n\tbackground: rgb(var(--red));\n\ttransition: 0.2s ease-in-out;\n}\n\n.contentMenu li[active]::after {\n\ttop: 5px;\n\topacity: 1.0;\n}\n\n.section {\n\topacity: 1.0;\n\tposition: fixed;\n\tright: calc(var(--padding) * 2);\n\tleft: calc(100px + var(--padding));\n\ttransition: opacity 0.15s ease-in-out;\n}\n\n.section.hidden {\n\topacity: 0.0;\n\tpointer-events: none;\n}\n\n.section .release-block {\n\tmargin-top: 0px;\n\tbackground: var(--bg);\n\tpadding: var(--padding);\n\tbackdrop-filter: blur(15px);\n\tmargin-bottom: var(--padding);\n\tborder-radius: calc(var(--padding) / 3);\n}\n\n.section .release-block {\n\tbackdrop-filter: none;\n\tmargin: var(--padding);\n\tbackground: var(--selbg);\n}\n\n.section .release-block p:nth-child(1) {\n\topacity: 0.8;\n\tmargin-top: 0px;\n\tmargin-bottom: 0px;\n}\n\n.section .release-block h1:nth-child(2) {\n\tmargin-top: 0px;\n}\n\n.section .release-block :last-child {\n\tmargin-bottom: 0px;\n}\n\n.contentBody img {max-width: 100%}\n.contentBody .img {text-align: center}\n\n.contentBody .section > :first-child:not(.release-block) {\n\tmargin-top: 35px;\n}\n\n.contentContainer .playBtnContainer {\n\ttext-align: center;\n}\n\n.contentContainer .playBtn {\n\twidth: 27%;\n\theight: 11%;\n\tborder: none;\n\tcolor: white;\n\tpadding: 20px;\n\tfont-size: 24px;\n\toverflow: hidden;\n\tfont-weight: bold;\n\tmargin-top: 100px;\n\tmargin-bottom: 10px;\n\tborder-radius: 10px;\n\tbackground: var(--redbg);\n\ttransition: 0.2s ease-in-out;\n\tfilter: drop-shadow(0px 8px 5px rgba(0, 0, 0, 0.1));\n\n\t--progress: 100%;\n}\n\n.contentContainer .playBtn:hover {\n\ttransform: scale(1.05);\n\tfilter: drop-shadow(0px 5px 15px rgba(0, 0, 0, 0.3)) brightness(110%);\n}\n\n.contentContainer .playBtn:active {\n\topacity: 0.7;transform: scale(0.98);\n\tfilter: drop-shadow(0px 5px 10px rgba(0, 0, 0, 0.4));\n}\n\n.contentContainer .playBtn:before {\n\ttop: 0;\n\tleft: 0;\n\tbottom: 0;\n\tcontent: \" \";\n\topacity: 0.5;\n\tposition: absolute;\n\tright: var(--progress);\n\tfilter: brightness(1.5);\n\tbackground: var(--bluebg);\n\ttransition: 0.2s ease-in-out;\n}\n\n.contentContainer #nsMain .playBtn {\n\tbackground: var(--bluebg);\n}\n\n#nsContent .contentMenu {\n\tmargin-bottom: 0;\n}\n\n.contentBody .img img {\n\ttransform: scale(0.85);\n}\n\n.contentBody .img {\n\twidth: 100%;\n\ttext-align: center;\n}\n\n#nsMods {\n\theight: 80vh;\n}\n\n#nsRelease, #vpReleaseNotes {\n\theight: 80vh;\n\tmargin-top: 35px;\n\toverflow-y: scroll;\n\tbackground: var(--bg);\n\tflex-direction: column;\n\tbackdrop-filter: blur(15px);\n\tborder-radius: calc(var(--padding) / 3);\n}\n\n.inline * {\n\tdisplay: inline-block;\n}\n\n#vpMain {\n\tmargin-top: 140px;\n\ttext-align: center;\n}\n\n#vpMain img {\n\twidth: 20%;\n}\n\n#vpVersion {\n\tfont-size: 16px;\n}\n\n.simplebar-scrollbar:before {\n\tbackground: rgb(var(--red)) !important;\n}\n\nbutton:has(img):not(img:only-child) {\n\ttop: 2px;\n\tposition: relative;\n\talign-items: center;\n\tdisplay: inline-flex;\n\tjustify-content: center;\n}\n\nbutton:has(img):not(img:only-child) img {\n\twidth: 1em;\n\theight: 1em;\n\theight: fit-content;\n\tmargin-right: calc(var(--padding) / 3)\n}\n\nbutton:has(img):has(span) img {\n\topacity: 0.0;\n\ttransition: opacity 0.2s ease-in-out;\n}\n\nbutton:has(img):has(span):hover img,\nbutton:has(img):has(span).active-selection img {\n\topacity: 1.0;\n}\n\nbutton:has(img):has(span) span {\n\tright: 0.8em;\n\tposition: relative;\n\ttransition: right 0.2s ease-in-out;\n}\n\nbutton:has(img):has(span):hover span,\nbutton:has(img):has(span).active-selection span {\n\tright: 0.1em;\n}\n\nbutton:disabled {\n\topacity: 0.5;\n\tpointer-events: none;\n}\n\nbutton.visual {\n\topacity: 1.0;\n\tpadding-right: 0px;\n\tpointer-events: none;\n\tbackground: transparent !important;\n}\n\ncode {\n\tfont-size: 16px;\n\tpadding: 2px 5px;\n\tborder-radius: 3px;\n\tbackground-color: #00000070;\n}\n\n#nsMods .line {\n\twidth: 100%;\n\tdisplay: flex;\n\tfont-weight: 700;\n\talign-items: center;\n\tmargin: calc(var(--padding) / 2);\n\tmargin-top: calc(var(--padding) / 2);\n}\n\n#modsdiv {\n\toverflow-y: scroll;\n\tbackground: var(--bg);\n\tbackdrop-filter: blur(15px);\n\tpadding: calc(var(--padding) / 2);\n\theight: calc(80vh - var(--padding));\n\tborder-radius: calc(var(--padding) / 3);\n}\n\n#modsdiv .mod {\n\tdisplay: flex;\n\tborder-radius: 5px;\n\ttransition: 0.1s ease-in-out;\n\tmargin: calc(var(--padding) / 3);\n\tpadding: calc(var(--padding) / 3);\n}\n\n#modsdiv .mod.selected {\n\tbackground: var(--selbg);\n}\n\n#modsdiv .mod .disabled, .modbtns {\n\tmargin-left: auto;\n}\n\n.modbtns button {\n\tmargin-left: var(--spacing);\n\t--spacing: calc(var(--padding) / 3);\n\tmargin-top: calc(var(--spacing) / 2);\n\tmargin-bottom: calc(var(--spacing) / 2);\n}\n\n#serverstatus {\n\t--spacing: calc(var(--padding) / 5);\n\t\n\ttransition-duration: 0.2s;\n\ttransition-timing-function: ease-in-out;\n\ttransition-property: background, opacity;\n\n\topacity: 0.0;\n\tdisplay: block;\n\tmargin: 0 auto;\n\tfont-weight: 700;\n\twidth: fit-content;\n\tcolor: transparent;\n\tborder-radius: 50px;\n\tflex-basis: max-content;\n\tbackground: transparent;\n\tmargin-top: calc(var(--spacing) * 2);\n\tpadding: var(--spacing) calc(var(--spacing) * 3);\n}\n\n#serverstatus.up,\n#serverstatus.down {\n\tcolor: white;\n\topacity: 1.0;\n}\n\n#serverstatus.up {background: rgb(var(--blue));}\n#serverstatus.down {background: rgb(var(--red));}\n"
  },
  {
    "path": "src/app/css/popups.css",
    "content": "@import \"theming.css\";\n\n/*\n This stylesheet is meant for the various popups we use, whether it be the\n previewer, the browser, the settings menu, or anything alike.\n*/\n\n.popup {\n\t--spacing: var(--padding);\n\t--top-spacing: calc(var(--spacing) + calc(var(--spacing) * 1.6));\n\n\tz-index: 2;\n\topacity: 0.0;\n\tposition: fixed;\n\toverflow-y: scroll;\n\tpointer-events: none;\n\tleft: var(--spacing);\n\tright: var(--spacing);\n\tbottom: var(--spacing);\n\ttop: var(--top-spacing);\n\n\tbackground: var(--bg);\n\ttransform: scale(0.98);\n\tborder-radius: calc(var(--padding) / 3);\n\n\ttransition-duration: 0.15s;\n\ttransition-property: opacity, transform;\n\ttransition-timing-function: ease-in-out;\n}\n\n.popup.blur {\n\tbackdrop-filter: blur(15px);\n}\n\n.popup.shown {\n\topacity: 1.0;\n\tpointer-events: all;\n\ttransform: scale(1.0);\n}\n\n.popup.small {\n\tleft: 20vw;\n\tright: 20vw;\n\tbottom: calc(var(--padding) * 2);\n\ttop: calc(var(--padding) * 2 + var(--top-spacing));\n}\n\n#overlay {\n\ttop: 0;\n\tleft: 0;\n\tright: 0;\n\tbottom: 0;\n\tz-index: 1;\n\topacity: 0.0;\n\tposition: fixed;\n\tbackground: var(--bg);\n\tpointer-events: none;\n\ttransition: opacity 0.15s ease-in-out;\n}\n\n#overlay.shown {\n\topacity: 1.0;\n\tpointer-events: all;\n\tbackdrop-filter: blur(10px);\n}\n\n/* browser/preview popup { */\n@keyframes fadein {\n\t0% {opacity: 0.0}\n\t100% {opacity: 1.0}\n}\n\n.popup webview {\n\twidth: 78%;\n\tmargin: 0 auto;\n\tfilter: opacity(1.0);\n\ttransition: filter 0.15s ease-in-out;\n\tmargin-top: calc(var(--spacing) / 2);\n\theight: calc(100% - calc(var(--spacing) / 2));\n}\n\n.popup webview.loading {\n\tfilter: opacity(0.0);\n\tpointer-events: none;\n}\n/* } */\n\n/* settings popup { */\n\n.popup .options details {\n\topacity: 1.0;\n\ttransition: 0.15s opacity ease-in-out;\n}\n\n.popup .options details:not([open]) {\n\topacity: 0.5;\n}\n\n.popup .options details:hover:not([open]) {\n\topacity: 0.8;\n}\n\n.popup .options details summary {\n\tcursor: pointer;\n\tlist-style-type: none;\n}\n\n.popup .options {\n\tcolor: white;\n\tmargin: calc(var(--padding) / 2);\n}\n\n.popup .options .option,\n.popup .options .buttons {\n\twidth: 100%;\n\tdisplay: flex;\n\tmargin-bottom: var(--padding);\n\tjustify-content: space-between;\n}\n\n.popup .overlay {\n\tz-index: 1;\n\tcolor: white;\n\topacity: 0.0;\n\tposition: fixed;\n\tpointer-events: none;\n\ttransform: scale(0.9);\n\tbackground: var(--selbg);\n\tbackdrop-filter: blur(15px);\n\ttransition: 0.15s ease-in-out;\n\tpadding: calc(var(--spacing) / 2);\n\tborder-radius: calc(var(--spacing) / 2);\n}\n\n.popup .overlay.shown {\n\topacity: 1.0;\n\tpointer-events: all;\n\ttransform: scale(1.0);\n}\n\n#options.popup .misc button {\n\tmargin-left: 0px;\n\twidth: auto !important;\n\tpadding-right: calc(var(--padding) / 2) !important;\n}\n\n.check {\n\tdisplay:flex;\n\tcursor: pointer;\n}\n\n.check::before {\n\twidth: 1em;\n\theight: 1em;\n\tcontent: \" \";\n\tbackground-size: 75%;\n\tfilter: brightness(1.3);\n\tbackground-position: center;\n\tbackground-repeat: no-repeat;\n\ttransition: 0.15s ease-in-out;\n\tbackground-color: var(--selbg);\n\tmargin-right: calc(var(--spacing) / 3);\n\tborder-radius: calc(var(--spacing) / 4);\n}\n\n.check.checked::before {\n\tbackground-color: rgb(var(--red));\n\tbackground-image: url(../icons/check.png);\n}\n\n.option .text,\n.buttons .text {\n\tfont-weight: 600;\n}\n\n.option .text .desc,\n.buttons .text .desc {\n\topacity: 0.8;\n\tfont-weight: 500;\n\tfont-size: 0.9em;\n\tmax-width: 400px;\n\tmargin-top: calc(var(--padding) / 3);\n}\n\n.option .actions input,\n.option .actions select {\n\twidth: 100%;\n\tmargin: 0px;\n\t--spacing: calc(var(--padding) / 3);\n}\n\n.option[type=array] .actions input {\n\tword-spacing: 15px;\n\tmargin-right: 15vw;\n}\n\n.buttons .actions {\n\ttext-align: right;\n}\n\n.buttons .actions button {\n\tmargin-bottom: calc(var(--padding) / 5);\n}\n\n.option .actions button,\n.buttons .actions button {\n\tbackground: var(--selbg);\n}\n\n.title {\n\tdisplay: flex;\n\tmargin-top: calc(var(--padding) * 2);\n\tmargin-bottom: calc(var(--padding) / 2);\n}\n\n.title:first-child {\n\tmargin-top: 0px;\n}\n\n.title img {\n\twidth: 30px;\n\theight: 30px;\n\tmargin: auto 0;\n}\n\n.title h2 {\n\tmargin: 0px;\n\tmargin-left: calc(var(--padding) / 3);\n}\n\n/* } */\n"
  },
  {
    "path": "src/app/css/selection.css",
    "content": "#selection {\n\tz-index: 99999999999999;\n\n\ttransition: 0.15s ease-in-out;\n\ttransition-property:\n\t\topacity, border-radius, background,\n\t\ttransform, width, height, top, left;\n\n\tposition: fixed;\n\tpointer-events: none;\n\ttransform: scale(1.0);\n\tbackground: rgba(255, 255, 255, 0.3);\n\tborder-radius: calc(var(--padding) / 2);\n}\n\n:has(.active-selection.scroll-selection) #selection {\n\tbackground: rgba(255, 255, 255, 0.2);\n}\n\n#selection.keyboard-selecting,\n#selection.controller-selecting {\n\ttransform: scale(1.1);\n}\n"
  },
  {
    "path": "src/app/css/theming.css",
    "content": "/*\n   the only reason some of these properties have an !important property\n   is for it to overwrite Thunderstore's CSS in the preview <webview>\n*/\n\n:root {\n\t--red: 199, 119, 127 !important;\n\t--red2: 181 97 105;\n\n\t--blue: 129, 161, 193;\n\t--blue2: 139, 143, 185;\n\n\t--orange: 213, 151, 131;\n\t--orange2: 197 129 107;\n\n\n\t--padding: 25px;\n\n\t--bg: rgba(0, 0, 0, 0.5);\n\t--selbg: rgba(80, 80, 80, 0.5);\n\t--redbg: linear-gradient(45deg, rgb(var(--red)), #FA4343);\n\t--bluebg: linear-gradient(45deg, rgb(var(--blue)), #7380ED);\n}\n\nbody, button, input {\n\tfont-family: \"Roboto\", sans-serif !important;\n}\n\nbody, button, img, a {\n\tuser-select: none;\n\t-webkit-user-drag: none;\n}\n\na {\n\ttext-decoration: none !important;\n\tcolor: rgb(var(--red)) !important;\n\ttransition: filter 0.2s ease-in !important;\n}\n\na.disabled:not([onclick=\"kill('game')\"]) {\n\topacity: 0.5;\n\tpointer-events: none;\n}\n\na:hover, a.active-selection {\n\tfilter: brightness(80%) !important;\n}\n\ninput:disabled {\n\topacity: 0.5;\n}\n\n::-webkit-scrollbar {\n\twidth: 18px !important;\n}\n \n::-webkit-scrollbar-track {\n\tborder-radius: 10px !important;\n\tbackground: transparent !important;\n}\n \n::-webkit-scrollbar-thumb {\n\tborder: 5px solid transparent;\n\tborder-radius: 10px !important;\n\tbox-shadow: rgb(var(--red)) 0px 0px 10px 10px inset;\n}\n\n::selection {\n\tcolor: black !important;\n\tbackground: rgb(var(--red)) !important;\n}\n\n.bg-blue {background: rgb(var(--blue)) !important}\n.bg-blue2 {background: rgb(var(--blue2)) !important}\n\n.bg-orange {background: rgb(var(--orange)) !important}\n.bg-orange2 {background: rgb(var(--orange2)) !important}\n\n.bg-red {background: rgb(var(--red)) !important}\n.bg-red2 {background: rgb(var(--red2)) !important}\n"
  },
  {
    "path": "src/app/css/toasts.css",
    "content": "@import \"theming.css\";\n\n#toasts {\n\tposition: fixed;\n\tz-index: 100000;\n\tright: calc(var(--padding) * 1.5);\n\tbottom: calc(var(--padding) * 1.5);\n}\n\n@keyframes bodyfadeaway {\n\t0% {opacity: 0.0; transform: scale(0.95)}\n\t100% {opacity: 1.0; transform: scale(1.0)}\n}\n\n#toasts .toast {\n\twidth: 300px;\n\topacity: 0.0;\n\tcursor: pointer;\n\toverflow: hidden;\n\tmax-height: 100vh;\n\ttransform: scale(0.95);\n\tbackground: var(--selbg);\n\ttransition: 0.2s ease-in-out;\n\tbackdrop-filter: blur(15px);\n\tpadding: calc(var(--padding) / 2);\n\tmargin-top: calc(var(--padding) / 2);\n\tborder-radius: calc(var(--padding) / 2.5);\n\tbox-shadow: 0px 5px 15px rgba(0, 0, 0, 0.2);\n\n\tanimation-duration: 0.2s;\n\tanimation-iteration-count: 1;\n\tanimation-name: bodyfadeaway;\n\tanimation-fill-mode: forwards;\n\tanimation-timing-function: ease-in-out;\n}\n\n#toasts .toast .title:only-child {\n\tmargin-bottom: 0px;\n}\n\n#toasts .toast.hidden {\n\tmargin-top: 0px;\n\tmax-height: 0px;\n\tpadding-top: 0px;\n\tpadding-bottom: 0px;\n\tfilter: opacity(0.0);\n\ttransform: scale(0.95);\n}\n\n#toasts .toast:not(.hidden):hover {filter: opacity(0.9)}\n#toasts .toast:not(.hidden):active {filter: opacity(0.8)}\n\n.toast .description {\n\topacity: 0.8;\n\tfont-size: 0.8em;\n\tfont-weight: 600;\n\tword-break: break-word;\n}\n"
  },
  {
    "path": "src/app/css/tooltip.css",
    "content": "@import \"theming.css\";\n\n#tooltip {\n\tcolor: white;\n\topacity: 0.0;\n\tposition: fixed;\n\tfont-weight: 600;\n\tfont-size: 0.8em;\n\twidth: max-content;\n\tz-index: 99999999999;\n\tpointer-events: none;\n\tbackground: var(--bg);\n\tbackdrop-filter: blur(15px);\n\ttransition: opacity 0.15s ease-in-out;\n\tborder-radius: calc(var(--padding) / 1);\n\tpadding: calc(var(--padding) / 2.8) calc(var(--padding) / 1.8);\n}\n\n#tooltip.visible {\n\topacity: 1.0;\n}\n"
  },
  {
    "path": "src/app/css/webview.css",
    "content": "body {\n\toverflow-x: hidden;\n\tbackground: transparent !important;\n\tbackground-color: transparent !important;\n\tbackground-image: transparent !important;\n}\n\n.background {\n\tdisplay: none;\n}\n\n.footer,\n#ncmp_tool,\n.bottom-ads,\n.ncmp__banner,\n#get-app-alert,\n.navbar, .bottom-padding,\n.card-header, .breadcrumb,\n.list-group, .mb-4, .my-2, .mt-2,\n#thunderstore-mod-manager-ad-alert {\n\tdisplay: none !important;\n}\n\n.mt-2.mb-2 {display: block !important}\n.card {transform: translateY(-1.0rem)}\n"
  },
  {
    "path": "src/app/fonts/import.css",
    "content": "@font-face {\n\tfont-weight: 100;\n\tfont-style: italic;\n\tfont-family: \"Roboto\";\n\tsrc: url(\"Roboto-ThinItalic.ttf\");\n}\n\n@font-face {\n\tfont-weight: 300;\n\tfont-style: italic;\n\tfont-family: \"Roboto\";\n\tsrc: url(\"Roboto-Italic.ttf\");\n}\n\n@font-face {\n\tfont-weight: 400;\n\tfont-style: italic;\n\tfont-family: \"Roboto\";\n\tsrc: url(\"Roboto-Italic.ttf\");\n}\n\n@font-face {\n\tfont-weight: 500;\n\tfont-style: italic;\n\tfont-family: \"Roboto\";\n\tsrc: url(\"Roboto-MediumItalic.ttf\");\n}\n\n@font-face {\n\tfont-weight: 700;\n\tfont-style: italic;\n\tfont-family: \"Roboto\";\n\tsrc: url(\"Roboto-BoldItalic.ttf\");\n}\n\n@font-face {\n\tfont-weight: 900;\n\tfont-style: italic;\n\tfont-family: \"Roboto\";\n\tsrc: url(\"Roboto-BlackItalic.ttf\");\n}\n\n@font-face {\n\tfont-weight: 100;\n\tfont-style: normal;\n\tfont-family: \"Roboto\";\n\tsrc: url(\"Roboto-Thin.ttf\");\n}\n\n@font-face {\n\tfont-weight: 300;\n\tfont-style: normal;\n\tfont-family: \"Roboto\";\n\tsrc: url(\"Roboto-Light.ttf\");\n}\n\n@font-face {\n\tfont-weight: 400;\n\tfont-style: normal;\n\tfont-family: \"Roboto\";\n\tsrc: url(\"Roboto-Regular.ttf\");\n}\n\n@font-face {\n\tfont-weight: 500;\n\tfont-style: normal;\n\tfont-family: \"Roboto\";\n\tsrc: url(\"Roboto-Medium.ttf\");\n}\n\n@font-face {\n\tfont-weight: 700;\n\tfont-style: normal;\n\tfont-family: \"Roboto\";\n\tsrc: url(\"Roboto-Bold.ttf\");\n}\n\n@font-face {\n\tfont-weight: 900;\n\tfont-style: normal;\n\tfont-family: \"Roboto\";\n\tsrc: url(\"Roboto-Black.ttf\");\n}\n"
  },
  {
    "path": "src/app/index.html",
    "content": "<html>\n\t<head>\n\t\t<meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\">\n\t\t<link rel=\"stylesheet\" href=\"fonts/import.css\">\n\t\t<link rel=\"stylesheet\" href=\"main.css\">\n\t\t<meta charset=\"utf-8\">\n\t</head>\n\t<body>\n\t\t<div id=\"selection\"></div>\n\n\t\t<div id=\"tooltip\">Test</div>\n\n\t\t<div id=\"bgHolder\"></div>\n\t\t<div id=\"toasts\"></div>\n\n\t\t<div id=\"winbtns\">\n\t\t\t<div class=\"hidden\" id=\"offline\" tooltip=\"%%tooltip.offline%%\" tooltip-position=\"horizontal\">\n\t\t\t\t<img src=\"icons/offline.png\">\n\t\t\t</div>\n\t\t\t<div id=\"settings\" tooltip=\"%%tooltip.settings%%\" tooltip-position=\"horizontal\" onclick=\"settings.popup.toggle()\">\n\t\t\t\t<img src=\"icons/settings.png\">\n\t\t\t</div>\n\t\t\t<div id=\"minimize\" tooltip=\"%%tooltip.minimize%%\" tooltip-position=\"horizontal\" onclick=\"ipcRenderer.send('minimize')\">\n\t\t\t\t<img src=\"icons/minimize.png\">\n\t\t\t</div>\n\t\t\t<div id=\"close\" tooltip=\"%%tooltip.close%%\" tooltip-position=\"horizontal\" onclick=\"ipcRenderer.send('exit')\">\n\t\t\t\t<img src=\"icons/close.png\">\n\t\t\t</div>\n\t\t</div>\n\n\t\t<div id=\"dragUI\">\n\t\t\t<div id=\"dragitems\">\n\t\t\t\t<div id=\"icon\"></div>\n\t\t\t\t<div id=\"text\">%%gui.mods.drag_n_drop%%</div>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<div id=\"overlay\" onclick=\"popups.set_all(false)\"></div>\n\t\t<div class=\"popup\" id=\"options\">\n\t\t\t<div class=\"misc\">\n\t\t\t\t<input class=\"search default-selection requires-internet\" placeholder=\"%%gui.search%%\">\n\t\t\t\t<button id=\"apply\" onclick=\"settings.popup.apply();settings.popup.toggle(false)\">\n\t\t\t\t\t<img src=\"icons/apply.png\">\n\t\t\t\t\t%%gui.settings.save%%\n\t\t\t\t</button>\n\t\t\t\t<button id=\"close\" onclick=\"settings.popup.toggle(false);settings.popup.load()\">\n\t\t\t\t\t<img src=\"icons/close.png\">\n\t\t\t\t\t%%gui.settings.discard%%\n\t\t\t\t</button>\n\t\t\t</div>\n\t\t\t<div class=\"options\">\n\t\t\t\t<details open>\n\t\t\t\t\t<summary>\n\t\t\t\t\t\t<div class=\"title\">\n\t\t\t\t\t\t\t<img src=\"icons/game.png\">\n\t\t\t\t\t\t\t<h2>%%gui.settings.title.ns%%</h2>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</summary>\n\t\t\t\t\t<div class=\"option\" name=\"nsargs\">\n\t\t\t\t\t\t<div class=\"text\">\n\t\t\t\t\t\t\t%%gui.settings.nsargs.title%%\n\t\t\t\t\t\t\t<div class=\"desc\">\n\t\t\t\t\t\t\t\t%%gui.settings.nsargs.desc%%\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"actions\">\n\t\t\t\t\t\t\t<input>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</details>\n\t\t\t\t<details open platform=\"linux\">\n\t\t\t\t\t<summary>\n\t\t\t\t\t\t<div class=\"title\">\n\t\t\t\t\t\t\t<img src=\"icons/linux.png\">\n\t\t\t\t\t\t\t<h2>%%gui.settings.title.linux%%</h2>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</summary>\n\t\t\t\t\t<div class=\"option\" name=\"linux_launch_method\">\n\t\t\t\t\t\t<div class=\"text\">\n\t\t\t\t\t\t\t%%gui.settings.linux_launch_method.title%%\n\t\t\t\t\t\t\t<div class=\"desc\">\n\t\t\t\t\t\t\t\t%%gui.settings.linux_launch_method.desc%%\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"actions\">\n\t\t\t\t\t\t\t<select>\n\t\t\t\t\t\t\t\t<option value=\"steam_auto\">%%gui.settings.linux_launch_method.methods.steam_auto%%</option>\n\t\t\t\t\t\t\t\t<option value=\"steam_executable\">%%gui.settings.linux_launch_method.methods.steam_executable%%</option>\n\t\t\t\t\t\t\t\t<option value=\"steam_flatpak\">%%gui.settings.linux_launch_method.methods.steam_flatpak%%</option>\n\t\t\t\t\t\t\t\t<option value=\"steam_protocol\">%%gui.settings.linux_launch_method.methods.steam_protocol%%</option>\n\t\t\t\t\t\t\t\t<option value=\"custom_command\">%%gui.settings.linux_launch_method.methods.command%%</option>\n\t\t\t\t\t\t\t</select>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"option\" name=\"linux_launch_cmd_ns\">\n\t\t\t\t\t\t<div class=\"text\">\n\t\t\t\t\t\t\t%%gui.settings.linux_launch_cmd_ns.title%%\n\t\t\t\t\t\t\t<div class=\"desc\">\n\t\t\t\t\t\t\t\t%%gui.settings.linux_launch_cmd_ns.desc%%\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"actions\">\n\t\t\t\t\t\t\t<input>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"option\" name=\"linux_launch_cmd_vanilla\">\n\t\t\t\t\t\t<div class=\"text\">\n\t\t\t\t\t\t\t%%gui.settings.linux_launch_cmd_vanilla.title%%\n\t\t\t\t\t\t\t<div class=\"desc\">\n\t\t\t\t\t\t\t\t%%gui.settings.linux_launch_cmd_vanilla.desc%%\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"actions\">\n\t\t\t\t\t\t\t<input>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</details>\n\n\t\t\t\t<details open>\n\t\t\t\t\t<summary>\n\t\t\t\t\t\t<div class=\"title\">\n\t\t\t\t\t\t\t<img src=\"icons/language.png\">\n\t\t\t\t\t\t\t<h2>%%gui.settings.title.language%%</h2>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</summary>\n\n\t\t\t\t\t<div class=\"option\" name=\"autolang\">\n\t\t\t\t\t\t<div class=\"text\">\n\t\t\t\t\t\t\t%%gui.settings.autolang.title%%\n\t\t\t\t\t\t\t<div class=\"desc\">\n\t\t\t\t\t\t\t\t%%gui.settings.autolang.desc%%\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"actions\">\n\t\t\t\t\t\t\t<button class=\"switch off\"></button>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"option\" name=\"forcedlang\">\n\t\t\t\t\t\t<div class=\"text\">\n\t\t\t\t\t\t\t%%gui.settings.forcedlang.title%%\n\t\t\t\t\t\t\t<div class=\"desc\">\n\t\t\t\t\t\t\t\t%%gui.settings.forcedlang.desc%%\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"actions\">\n\t\t\t\t\t\t\t<select onchange=\"settings.popup.switch(document.querySelector(`.option[name='autolang'] button`), false)\">\n\t\t\t\t\t\t\t\t<option></option>\n\t\t\t\t\t\t\t</select>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</details>\n\n\t\t\t\t<details open>\n\t\t\t\t\t<summary>\n\t\t\t\t\t\t<div class=\"title\">\n\t\t\t\t\t\t\t<img src=\"icons/updates.png\">\n\t\t\t\t\t\t\t<h2>%%gui.settings.title.updates%%</h2>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</summary>\n\n\t\t\t\t\t<div class=\"option\" name=\"autoupdate\">\n\t\t\t\t\t\t<div class=\"text\">\n\t\t\t\t\t\t\t%%gui.settings.autoupdate.title%%\n\t\t\t\t\t\t\t<div class=\"desc\">\n\t\t\t\t\t\t\t\t%%gui.settings.autoupdate.desc%%\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"actions\">\n\t\t\t\t\t\t\t<button class=\"switch on\"></button>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"option\" name=\"nsupdate\">\n\t\t\t\t\t\t<div class=\"text\">\n\t\t\t\t\t\t\t%%gui.settings.nsupdate.title%%\n\t\t\t\t\t\t\t<div class=\"desc\">\n\t\t\t\t\t\t\t\t%%gui.settings.nsupdate.desc%%\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"actions\">\n\t\t\t\t\t\t\t<button class=\"switch on\"></button>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"option\" name=\"excludes\" type=\"array\">\n\t\t\t\t\t\t<div class=\"text\">\n\t\t\t\t\t\t\t%%gui.settings.excludes.title%%\n\t\t\t\t\t\t\t<div class=\"desc\">\n\t\t\t\t\t\t\t\t%%gui.settings.excludes.desc%%\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"actions\">\n\t\t\t\t\t\t\t<input type=\"text\" class=\"disable-when-installing\">\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"buttons\">\n\t\t\t\t\t\t<div class=\"text\">\n\t\t\t\t\t\t\t%%gui.settings.updatebuttons.title%%\n\t\t\t\t\t\t\t<div class=\"desc\">\n\t\t\t\t\t\t\t\t%%gui.settings.updatebuttons.desc%%\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"actions\">\n\t\t\t\t\t\t\t<button onclick=\"request.delete_cache()\">%%gui.settings.updatebuttons.buttons.reset_cached_api_requests%%</button>\n\t\t\t\t\t\t\t<button onclick=\"update.ns(true)\" class=\"disable-when-installing\">%%gui.settings.updatebuttons.buttons.force_northstar_reinstall%%</button>\n\t\t\t\t\t\t\t<button onclick=\"update.delete_cache()\" class=\"disable-when-installing\">%%gui.settings.updatebuttons.buttons.force_delete_install_cache%%</button>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</details>\n\n\t\t\t\t<details open>\n\t\t\t\t\t<summary>\n\t\t\t\t\t\t<div class=\"title\">\n\t\t\t\t\t\t\t<img src=\"icons/settings.png\">\n\t\t\t\t\t\t\t<h2>%%gui.settings.title.misc%%</h2>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</summary>\n\n\t\t\t\t\t<div class=\"option\" name=\"originkill\">\n\t\t\t\t\t\t<div class=\"text\">\n\t\t\t\t\t\t\t%%gui.settings.originkill.title%%\n\t\t\t\t\t\t\t<div class=\"desc\">\n\t\t\t\t\t\t\t\t%%gui.settings.originkill.desc%%\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"actions\">\n\t\t\t\t\t\t\t<button class=\"switch off\"></button>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"buttons\">\n\t\t\t\t\t\t<div class=\"text\">\n\t\t\t\t\t\t\t%%gui.settings.miscbuttons.title%%\n\t\t\t\t\t\t\t<div class=\"desc\">\n\t\t\t\t\t\t\t\t%%gui.settings.miscbuttons.desc%%\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"actions\">\n\t\t\t\t\t\t\t<button onclick=\"process.relaunch()\">%%gui.settings.miscbuttons.buttons.restart_viper%%</button>\n\t\t\t\t\t\t\t<button onclick=\"settings.reset()\">%%gui.settings.miscbuttons.buttons.reset_config%%</button>\n\t\t\t\t\t\t\t<button onclick=\"gamepath.open()\">%%gui.settings.miscbuttons.buttons.open_gamepath%%</button>\n\t\t\t\t\t\t\t<button onclick=\"kill('game')\">%%gui.settings.miscbuttons.buttons.force_quit_game%%</button>\n\t\t\t\t\t\t\t<button onclick=\"kill('origin')\">%%gui.settings.miscbuttons.buttons.force_quit_origin%%</button>\n\t\t\t\t\t\t\t<button onclick=\"gamepath.set()\" class=\"disable-when-installing\">%%gui.settings.miscbuttons.buttons.change_gamepath%%</button>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</details>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<div class=\"popup\" id=\"browser\">\n\t\t\t<div class=\"overlay\" id=\"filters\">\n\t\t\t\t<div class=\"checks\">\n\t\t\t\t\t<div class=\"check checked\" value=\"Mods\">%%gui.browser.filter.mods%%</div>\n\t\t\t\t\t<div class=\"check checked\" value=\"Skins\">%%gui.browser.filter.skins%%</div>\n\t\t\t\t\t<div class=\"check checked\" value=\"Client-side\">%%gui.browser.filter.client%%</div>\n\t\t\t\t\t<div class=\"check\" value=\"Server-side\">%%gui.browser.filter.server%%</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t<div class=\"misc\">\n\t\t\t\t<input class=\"search default-selection\" placeholder=\"%%gui.search%%\">\n\t\t\t\t<button class=\"long\" id=\"sort\">\n\t\t\t\t\t<img src=\"icons/sort.png\" value=\"unix_created\">\n\t\t\t\t\t<select class=\"no-navigate\">\n\t\t\t\t\t\t<option value=\"unix_created\">%%gui.browser.sort.newest%%</option>\n\t\t\t\t\t\t<option value=\"unix_updated\">%%gui.browser.sort.last_updated%%</option>\n\t\t\t\t\t\t<option value=\"rating_score\">%%gui.browser.sort.highest_rating%%</option>\n\t\t\t\t\t\t<option value=\"downloads\">%%gui.browser.sort.most_downloads%%</option>\n\t\t\t\t\t</select>\n\t\t\t\t</button>\n\t\t\t\t<button id=\"filter\" onclick=\"browser.filters.toggle()\">\n\t\t\t\t\t<img src=\"icons/filter.png\">\n\t\t\t\t</button>\n\t\t\t\t<button id=\"close\" onclick=\"browser.toggle(false)\">\n\t\t\t\t\t<img src=\"icons/close.png\">\n\t\t\t\t</button>\n\t\t\t</div>\n\t\t\t<div id=\"browserEntries\" class=\"grid\">\n\t\t\t\t<div class=\"loading\">%%gui.browser.loading%%</div>\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"popup small blur\" id=\"preview\">\n\t\t\t<div class=\"misc fixed vertical default-selection\">\n\t\t\t\t<button id=\"close\" onclick=\"browser.preview.hide()\">\n\t\t\t\t\t<img src=\"icons/close.png\">\n\t\t\t\t</button>\n\t\t\t\t<button id=\"external\" onclick=\"\">\n\t\t\t\t\t<img src=\"icons/external.png\">\n\t\t\t\t</button>\n\t\t\t</div>\n\n\t\t\t<webview class=\"scroll-selection\"></webview>\n\t\t</div>\n\t\t\n\t\t<nav class=\"gamesContainer\">\n\t\t\t<button id=\"vpBtn\" tooltip=\"%%tooltip.pages.viper%%\" tooltip-position=\"horizontal\" onclick=\"launcher.change_page(0)\"></button>\n\t\t\t<button id=\"nsBtn\" tooltip=\"%%tooltip.pages.northstar%%\" tooltip-position=\"horizontal\" onclick=\"launcher.change_page(1)\"></button>\n\t\t\t<button id=\"tfBtn\" tooltip=\"%%tooltip.pages.titanfall%%\" tooltip-position=\"horizontal\" onclick=\"launcher.change_page(2)\"></button>\n\t\t</nav>\n\n\t\t<div class=\"mainContainer\">\n\t\t\t<div id=\"vpContent\" class=\"contentContainer\">\n\t\t\t\t<ul class=\"contentMenu\">\n\t\t\t\t\t<li id=\"vpMainBtn\" active onclick=\"launcher.show_vp('main')\">%%viper.menu.main%%</li>\n\t\t\t\t\t<li id=\"vpReleaseBtn\" onclick=\"launcher.show_vp('release')\">%%viper.menu.release%%</li>\n\t\t\t\t\t<li id=\"vpInfoBtn\" onclick=\"launcher.show_vp('info')\">%%viper.menu.info%%</li>\n\t\t\t\t</ul>\n\t\t\t\t<div class=\"contentBody\">\n\t\t\t\t\t<div id=\"vpMain\" class=\"section\">\n\t\t\t\t\t\t<img src=\"icons/viper.png\"/>\n\t\t\t\t\t\t<div class=\"inline\" style=\"margin-top: 20px;\">\n\t\t\t\t\t\t\t<div id=\"vpversion\"></div> |\n\t\t\t\t\t\t\t<a id=\"setpath\" href=\"#\" onclick=\"gamepath.set()\" class=\"disable-when-installing\">%%gui.setpath%%</a>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div id=\"vpReleaseNotes\" class=\"scroll-selection hidden section\"></div>\n\t\t\t\t\t<div id=\"vpInfo\" class=\"hidden section\">\n\t\t\t\t\t\t<h2>%%viper.info.links%%</h2>\n\t\t\t\t\t\t<ul>\n\t\t\t\t\t\t\t<li>%%viper.info.discord%% <a href=\"https://northstar.tf/discord\">northstar.tf/discord</a></li>\n\t\t\t\t\t\t\t<li>%%viper.info.issues%% <a href=\"https://github.com/0neGal/viper/issues\">github.com/0neGal/viper/issues</a></li>\n\t\t\t\t\t\t</ul>\n\t\t\t\t\t\t<h2>%%viper.info.credits%%</h2>\n\t\t\t\t\t\t<ul>\n\t\t\t\t\t\t\t<li>Viper Logo: Imply#9781</li>\n\t\t\t\t\t\t\t<li>Viper Background: <a href=\"https://www.reddit.com/r/titanfall/comments/fwuh2x/take_to_the_skies\">Uber Panzerhund</a></li>\n\t\t\t\t\t\t\t<li>Titanfall+Northstar Logo: <a href=\"https://www.steamgriddb.com/logo/47851\">Aftonstjarma</a></li>\n\t\t\t\t\t\t</ul>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t<div id=\"nsContent\" class=\"contentContainer\">\n\t\t\t\t<ul class=\"contentMenu\">\n\t\t\t\t\t<li id=\"nsMainBtn\" active onclick=\"launcher.show_ns('main')\">%%ns.menu.main%%</li>\n\t\t\t\t\t<li id=\"nsModsBtn\" onclick=\"launcher.show_ns('mods')\">%%ns.menu.mods%%</li>\n\t\t\t\t\t<li id=\"nsReleaseBtn\" onclick=\"launcher.show_ns('release')\">%%ns.menu.release%%</li>\n\t\t\t\t</ul>\n\t\t\t\t<div class=\"contentBody\">\n\t\t\t\t\t<div id=\"nsMain\" class=\"section\">\n\t\t\t\t\t\t<div class=\"img\"><img src=\"../assets/ns.png\"></div>\n\t\t\t\t\t\t<div class=\"playBtnContainer\">\n\t\t\t\t\t\t\t<button id=\"playNsBtn\" class=\"playBtn\" onclick=\"launch('northstar')\">%%gui.launch%%</button>\n\t\t\t\t\t\t\t<div class=\"inline\">\n\t\t\t\t\t\t\t\t<div id=\"nsversion\"></div>\n\t\t\t\t\t\t\t\t<a id=\"update\" href=\"#\" onclick=\"update.ns()\" class=\"disable-when-installing requires-internet\">(%%gui.update.check%%)</a>\n\t\t\t\t\t\t\t\t<div id=\"serverstatus\" class=\"checking\"></div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div id=\"nsMods\" class=\"hidden section\">\n\t\t\t\t\t\t<div id=\"modsdiv\" class=\"grid\">\n\t\t\t\t\t\t\t<div class=\"line\">\n\t\t\t\t\t\t\t\t<div class=\"text\" id=\"modcount\">%%gui.mods.title%%</div>\n\t\t\t\t\t\t\t\t<div class=\"buttons modbtns\">\n\t\t\t\t\t\t\t\t\t<button id=\"removeall\" class=\"bg-red2\" onclick=\"mods.remove('allmods')\">\n\t\t\t\t\t\t\t\t\t\t<img src=\"icons/trash.png\">\n\t\t\t\t\t\t\t\t\t\t%%gui.mods.remove_all%%\n\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t\t<button id=\"toggleall\" class=\"bg-orange2\" onclick=\"mods.toggle('allmods')\">\n\t\t\t\t\t\t\t\t\t\t<img src=\"icons/toggles.png\">\n\t\t\t\t\t\t\t\t\t\t%%gui.mods.toggle_all%%\n\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t\t<button id=\"installmod\" class=\"bg-blue\" onclick=\"mods.install_prompt()\">\n\t\t\t\t\t\t\t\t\t\t<img src=\"icons/downloads.png\">\n\t\t\t\t\t\t\t\t\t\t%%gui.mods.install%%\n\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t\t<button id=\"findmod\" class=\"requires-internet bg-blue2\" onclick=\"browser.toggle(true)\">\n\t\t\t\t\t\t\t\t\t\t<img src=\"icons/search.png\">\n\t\t\t\t\t\t\t\t\t\t%%gui.mods.find%%\n\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"line\">\n\t\t\t\t\t\t\t\t<input id=\"mods-search\" class=\"search default-selection\" placeholder=\"%%gui.search%%\">\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div id=\"nsRelease\" class=\"scroll-selection hidden section\"></div>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t<div id=\"tfContent\" class=\"contentContainer\">\n\t\t\t\t<ul class=\"contentMenu\"><li style=\"opacity:0.0\">filler</li></ul>\n\t\t\t\t<div class=\"contentBody\">\n\t\t\t\t\t<div class=\"section\">\n\t\t\t\t\t\t<div class=\"img\"><img src=\"../assets/vanilla.png\"></div>\n\t\t\t\t\t\t<div class=\"playBtnContainer\">\n\t\t\t\t\t\t\t<button class=\"playBtn\" onclick=\"launch('vanilla')\">%%gui.launch%%</button>\n\t\t\t\t\t\t\t<div class=\"inline\">\n\t\t\t\t\t\t\t\t<div id=\"tf2Version\"></div>\n\t\t\t\t\t\t\t\t<a id=\"tfquit\" style=\"display: none\"\n\t\t\t\t\t\t\t\t\thref=\"#\" onclick=\"kill('game')\">(%%ns.menu.force_quit%%)</a>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<script src=\"main.js\"></script>\n\t</body>\n</html>\n"
  },
  {
    "path": "src/app/js/browser.js",
    "content": "const Fuse = require(\"fuse.js\");\nconst { ipcRenderer, shell } = require(\"electron\");\n\nconst lang = require(\"../../lang\");\n\nconst popups = require(\"./popups\");\nconst toasts = require(\"./toasts\");\nconst request = require(\"./request\");\n\nvar browser_fuse;\nvar packages = [];\n\nvar packagecount = 0;\n\nvar browser_el = document.getElementById(\"browser\")\n\nvar browser = {\n\tmaxentries: 50,\n\tmod_versions: {},\n\tfilters: {\n\t\tgetpkgs: () => {\n\t\t\tlet pkgs = [];\n\t\t\tlet other = [];\n\t\t\tfor (let i in packages) {\n\t\t\t\tif (! browser.filters.isfiltered(packages[i].categories)) {\n\t\t\t\t\tpkgs.push(packages[i]);\n\t\t\t\t} else {\n\t\t\t\t\tother.push(packages[i]);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn pkgs;\n\t\t},\n\t\tget: () => {\n\t\t\tlet filtered = [];\n\t\t\tlet unfiltered = [];\n\t\t\tlet checks = browser_el.querySelectorAll(\"#filters .check\");\n\n\t\t\tfor (let i = 0; i < checks.length; i++) {\n\t\t\t\tif (! checks[i].classList.contains(\"checked\")) {\n\t\t\t\t\tfiltered.push(checks[i].getAttribute(\"value\"));\n\t\t\t\t} else {\n\t\t\t\t\tunfiltered.push(checks[i].getAttribute(\"value\"));\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tfiltered,\n\t\t\t\tunfiltered\n\t\t\t};\n\t\t},\n\t\tisfiltered: (categories) => {\n\t\t\tlet filtered = browser.filters.get().filtered;\n\t\t\tlet unfiltered = browser.filters.get().unfiltered;\n\t\t\tlet state = false;\n\n\t\t\tlet filters = [\n\t\t\t\t\"Mods\", \"Skins\",\n\t\t\t\t\"Client-side\", \"Server-side\",\n\t\t\t];\n\n\t\t\tlet newcategories = [];\n\t\t\tfor (let i = 0; i < categories.length; i++) {\n\t\t\t\tif (filters.includes(categories[i])) {\n\t\t\t\t\tnewcategories.push(categories[i]);\n\t\t\t\t}\n\t\t\t}; categories = newcategories;\n\n\t\t\tif (categories.length == 0) {return true}\n\t\t\tfor (let i = 0; i < categories.length; i++) {\n\t\t\t\tif (filtered.includes(categories[i])) {\n\t\t\t\t\tstate = true;\n\t\t\t\t\tcontinue\n\t\t\t\t} else if (unfiltered.includes(categories[i])) {\n\t\t\t\t\tstate = false;\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tstate = true;\n\t\t\t}\n\n\t\t\treturn state;\n\t\t},\n\t\ttoggle: (state) => {\n\t\t\tif (state == false) {\n\t\t\t\tfilters.classList.remove(\"shown\");\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tfilters.classList.toggle(\"shown\");\n\t\t\tlet filterRect = filter.getBoundingClientRect();\n\t\t\tlet spacing = parseInt(getComputedStyle(filters).getPropertyValue(\"--spacing\"));\n\n\t\t\tfilters.style.top = filterRect.bottom - (spacing + (spacing * 1.3));\n\t\t\tfilters.style.right = filterRect.right - filterRect.left + filterRect.width - (spacing / 2);\n\t\t},\n\t},\n\ttoggle: (state) => {\n\t\tbrowser_el.scrollTo(0, 0);\n\t\tpopups.set(\"#browser\", state);\n\n\t\tif (state) {\n\t\t\tif (browserEntries.querySelectorAll(\".el\").length == 0) {\n\t\t\t\tbrowser.loadfront();\n\t\t\t}\n\t\t} else if (state === false) {\n\t\t\tbrowser.filters.toggle(false);\n\t\t}\n\t},\n\tinstall: async (package_obj, clear_queue = false) => {\n\t\tlet can_connect = await request.check_with_toasts(\n\t\t\t\"Thunderstore\", \"https://thunderstore.io\"\n\t\t)\n\n\t\tif (! can_connect) {\n\t\t\treturn;\n\t\t}\n\n\t\treturn mods.install_from_url(\n\t\t\tpackage_obj.download || package_obj.versions[0].download_url,\n\t\t\tpackage_obj.dependencies || package_obj.versions[0].dependencies,\n\t\t\tclear_queue,\n\n\t\t\tpackage_obj.author || package_obj.owner,\n\t\t\tpackage_obj.name || package_obj.pkg.name,\n\t\t\tpackage_obj.version || package_obj.versions[0].version_number\n\t\t)\n\t},\n\tadd_pkg_properties: () => {\n\t\tfor (let i = 0; i < packages.length; i++) {\n\t\t\tlet properties = packages[i];\n\t\t\tlet normalized = mods.normalize(packages[i].name);\n\n\t\t\tlet has_update = false;\n\t\t\tlet local_name = false;\n\t\t\tlet local_version = false;\n\t\t\tlet remote_version = packages[i].versions[0].version_number;\n\t\t\tremote_version = version.format(remote_version);\n\n\t\t\tif (mods.list()) {\n\t\t\t\tfor (let ii = 0; ii < mods.list().all.length; ii++) {\n\t\t\t\t\tlet mod = mods.list().all[ii];\n\n\t\t\t\t\tif (mods.normalize(mod.name) !== normalized && (\n\t\t\t\t\t\t! mod.package ||\n\t\t\t\t\t\tmod.package.author + \"-\" + mod.package.package_name !==\n\t\t\t\t\t\tpackages[i].full_name\n\t\t\t\t\t)) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\tlocal_name = mod.name;\n\t\t\t\t\tlocal_version = version.format(mod.version);\n\t\t\t\t\tif (version.is_newer(remote_version, local_version)) {\n\t\t\t\t\t\thas_update = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tlet install = () => {\n\t\t\t\treturn browser.install({...properties});\n\t\t\t}\n\n\t\t\tpackages[i].unix_created = new Date(packages[i].date_created).getTime();\n\t\t\tpackages[i].unix_updated = new Date(packages[i].date_updated).getTime();\n\n\t\t\tpackages[i].install = install;\n\t\t\tpackages[i].has_update = has_update;\n\t\t\tpackages[i].local_version = local_version;\n\n\t\t\tpackages[i].downloads = 0;\n\n\t\t\tfor (let version of packages[i].versions) {\n\t\t\t\tpackages[i].downloads += version.downloads || 0;\n\t\t\t}\n\n\t\t\tif (local_version) {\n\t\t\t\tbrowser.mod_versions[normalized] = {\n\t\t\t\t\tinstall: install,\n\t\t\t\t\thas_update: has_update,\n\t\t\t\t\tlocal_name: local_name,\n\t\t\t\t\tlocal_version: local_version,\n\n\t\t\t\t\tpackage: packages[i]\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n\t// sorts `pkgs` based on `property` in package object\n\tsort: (pkgs, property) => {\n\t\t// get property from sort selector, if not specified\n\t\tif (! property) {\n\t\t\tproperty = sort.querySelector(\"select\").value;\n\n\t\t\t// if we somehow still don't have a property, just return\n\t\t\tif (! property) {\n\t\t\t\treturn pkgs;\n\t\t\t}\n\t\t}\n\n\t\t// if `property` doesn't even exist, just return\n\t\tif (typeof pkgs[0][property] == \"undefined\") {\n\t\t\treturn pkgs;\n\t\t}\n\n\t\t// sort in descending order\n\t\treturn pkgs.sort((a, b) => {\n\t\t\treturn b[property] - a[property];\n\t\t})\n\t},\n\tloadfront: async () => {\n\t\tbrowser.loading();\n\n\t\tpackagecount = 0;\n\t\t\n\t\tif (packages.length < 1) {\n\t\t\tlet host = \"northstar.thunderstore.io\";\n\t\t\tlet path = \"/api/v1/package/\";\n\n\t\t\tpackages = [];\n\n\t\t\t// attempt to get the list of packages from Thunderstore, if\n\t\t\t// this has been done recently, it'll simply return a cached\n\t\t\t// version of the request\n\t\t\ttry {\n\t\t\t\tpackages = JSON.parse(\n\t\t\t\t\tawait request(host, path, \"thunderstore-packages\")\n\t\t\t\t)\n\t\t\t}catch(err) {\n\t\t\t\tconsole.error(err)\n\t\t\t}\n\n\t\t\tbrowser.add_pkg_properties();\n\n\t\t\tbrowser_fuse = new Fuse(packages, {\n\t\t\t\tkeys: [\"full_name\"]\n\t\t\t})\n\t\t}\n\t\t\n\t\tlet pkgs = browser.sort(browser.filters.getpkgs());\n\n\t\tfor (let i in pkgs) {\n\t\t\tif (packagecount >= browser.maxentries) {\n\t\t\t\tbrowser.endoflist();\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tbrowser.mod_el_from_obj(pkgs[i]);\n\t\t\tpackagecount++;\n\t\t}\n\t},\n\tloading: (string) => {\n\t\tif (browser.filters.get().unfiltered.length == 0) {\n\t\t\tstring = lang(\"gui.browser.no_results\");\n\t\t}\n\n\t\tif (string) {\n\t\t\tbrowserEntries.innerHTML = `<div class=\"loading\">${string}</div>`;\n\t\t}\n\n\t\tif (! browserEntries.querySelector(\".loading\")) {\n\t\t\tbrowserEntries.innerHTML = `<div class=\"loading\">${lang('gui.browser.loading')}</div>`;\n\t\t}\n\t},\n\tendoflist: (is_end) => {\n\t\tlet pkgs = [];\n\t\tlet filtered = browser.filters.getpkgs();\n\t\tfor (let i = 0; i < filtered.length; i++) {\n\t\t\tif (filtered[packagecount + i]) {\n\t\t\t\tpkgs.push(filtered[packagecount + i]);\n\t\t\t} else {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif (browserEntries.querySelector(\".message\")) {\n\t\t\tbrowserEntries.querySelector(\".message\").remove();\n\t\t}\n\n\t\tif (pkgs.length == 0 || is_end) {\n\t\t\tbrowser.msg(`${lang('gui.browser.endoflist')}`);\n\t\t\treturn\n\t\t}\n\n\t\tbrowser.msg(`<button id=\"loadmore\">` +\n\t\t\t\t`<img src=\"icons/down.png\">` +\n\t\t\t\t`<span>${lang(\"gui.browser.load_more\")}</span>` +\n\t\t\t`</button>`);\n\n\t\tloadmore.addEventListener(\"click\", () => {\n\t\t\tbrowser.loadpkgs(pkgs);\n\t\t\tbrowser.endoflist(! pkgs.length);\n\t\t})\n\t},\n\tsearch: (string) => {\n\t\tbrowser.loading();\n\t\tlet res = browser_fuse.search(string);\n\n\t\tif (res.length < 1) {\n\t\t\tbrowser.loading(lang(\"gui.browser.no_results\"));\n\t\t\treturn\n\t\t}\n\n\t\tpackagecount = 0;\n\n\t\tlet count = 0;\n\t\tfor (let i = 0; i < res.length; i++) {\n\t\t\tif (count >= browser.maxentries) {break}\n\t\t\tif (browser.filters.isfiltered(res[i].item.categories)) {continue}\n\t\t\tbrowser.mod_el_from_obj(res[i].item);\n\t\t\tcount++;\n\t\t}\n\n\t\tif (count < 1) {\n\t\t\tbrowser.loading(lang(\"gui.browser.no_results\"));\n\t\t}\n\t},\n\tsetbutton: (mod, string, icon) => {\n\t\tmod = mods.normalize(mod);\n\t\tif (browserEntries.querySelector(`#mod-${mod}`)) {\n\t\t\tlet elems = browserEntries.querySelectorAll(`.el#mod-${mod}`);\n\n\t\t\tfor (let i = 0; i < elems.length; i++) {\n\t\t\t\tif (icon) {\n\t\t\t\t\tstring = `<img src=\"icons/${icon}.png\">` +\n\t\t\t\t\t\t`<span>${string}</span>`;\n\t\t\t\t}\n\n\t\t\t\telems[i].querySelector(\".text button\").innerHTML = string;\n\t\t\t}\n\t\t} else {\n\t\t\tlet make = (str) => {\n\t\t\t\tif (browserEntries.querySelector(`#mod-${str}`)) {\n\t\t\t\t\treturn browser.setbutton(str, string, icon);\n\t\t\t\t} else {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tsetTimeout(() => {\n\t\t\t\tfor (let i = 0; i < mods.list().all.length; i++) {\n\t\t\t\t\tlet modname = mods.normalize(mods.list().all[i].name);\n\t\t\t\t\tlet modfolder = mods.normalize(mods.list().all[i].folder_name);\n\n\t\t\t\t\tif (mod.includes(modname)) {\n\t\t\t\t\t\tif (! make(modname)) {\n\t\t\t\t\t\t\tif (mods.list().all[i].manifest_name) {\n\t\t\t\t\t\t\t\tmake(mods.normalize(mods.list().all[i].manifest_name));\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\telse if (mod.includes(modfolder)) {make(modfolder);break}\n\t\t\t\t}\n\t\t\t}, 1501)\n\t\t}\n\t},\n\tloadpkgs: (pkgs, clear) => {\n\t\tif (clear) {packagecount = 0}\n\n\t\tif (browserEntries.querySelector(\".message\")) {\n\t\t\tbrowserEntries.querySelector(\".message\").remove();\n\t\t}\n\n\t\tlet count = 0;\n\t\tfor (let i in pkgs) {\n\t\t\tif (count >= browser.maxentries) {\n\t\t\t\tif (pkgs[i] === undefined) {\n\t\t\t\t\tbrowser.endoflist(true);\n\t\t\t\t}\n\n\t\t\t\tbrowser.endoflist();\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tbrowser.mod_el_from_obj(pkgs[i]);\n\t\t\t}catch(e) {}\n\n\t\t\tcount++;\n\t\t\tpackagecount++;\n\t\t}\n\t},\n\tmsg: (html) => {\n\t\tlet msg = document.createElement(\"div\");\n\t\tmsg.classList.add(\"message\");\n\t\tmsg.innerHTML = html;\n\t\t\n\t\tbrowserEntries.appendChild(msg);\n\t}\n}\n\nsort.querySelector(\"select\").addEventListener(\"change\", () => {\n\tbrowser.loadfront();\n})\n\nsetInterval(browser.add_pkg_properties, 1500);\n\nif (navigator.onLine) {\n\tbrowser.loadfront();\n}\n\nvar view = document.querySelector(\".popup#preview webview\");\nbrowser.preview = {\n\tshow: () => {\n\t\tpopups.show(preview, false);\n\t},\n\thide: () => {\n\t\tpopups.hide(preview, false);\n\t},\n\tset: (url, autoshow) => {\n\t\tif (autoshow != false) {browser.preview.show()}\n\n\t\tview.src = url;\n\n\t\tdocument.querySelector(\"#preview #external\").setAttribute(\n\t\t\t\"onclick\",\n\t\t\t`require(\"electron\").shell.openExternal(\"${url}\")`\n\t\t)\n\t}\n}\n\nbrowser.mod_el_from_obj = (obj) => {\n\tlet pkg = {...obj, ...obj.versions[0]};\n\n\tbrowser.mod_el({\n\t\tpkg: pkg,\n\t\ttitle: pkg.name,\n\t\timage: pkg.icon,\n\t\tauthor: pkg.owner,\n\t\turl: pkg.package_url,\n\t\tdownload: pkg.download_url,\n\t\tversion: pkg.version_number,\n\t\tcategories: pkg.categories,\n\t\tdescription: pkg.description,\n\t\tdependencies: pkg.dependencies,\n\t})\n}\n\nbrowser.mod_el = (properties) => {\n\tif (browser.filters.isfiltered(properties.categories)) {return}\n\n\tproperties = {\n\t\ttitle: \"No name\",\n\t\tversion: \"1.0.0\",\n\t\timage: \"icons/no-image.png\",\n\t\tauthor: \"Unnamed Pilot\",\n\t\tdescription: \"No description\",\n\t\t...properties\n\t}\n\n\tif (properties.version[0] != \"v\") {\n\t\tproperties.version = \"v\" + properties.version;\n\t}\n\n\tif (browserEntries.querySelector(\".loading\")) {\n\t\tbrowserEntries.innerHTML = \"\";\n\t}\n\n\tlet installicon = \"downloads\";\n\tlet installstr = lang(\"gui.browser.install\");\n\tlet normalized_title = mods.normalize(properties.title)\n\tlet installcallback = () => {\n\t\tbrowser.install(properties);\n\t}\n\n\tlet nondefault_install = {\n\t\t\"vanillaplus\": \"https://github.com/NachosChipeados/NP.VanillaPlus/blob/main/README.md\"\n\t}\n\n\tif (normalized_title in nondefault_install) {\n\t\tinstallicon = \"open\";\n\t\tinstallstr = lang(\"gui.browser.guide\");\n\n\t\tinstallcallback = () => {\n\t\t\tshell.openExternal(nondefault_install[normalized_title])\n\t\t}\n\t}\n\telse if (properties.pkg.local_version) {\n\t\tinstallicon = \"redo\";\n\t\tinstallstr = lang(\"gui.browser.reinstall\");\n\n\t\tif (properties.pkg.has_update) {\n\t\t\tinstallicon = \"downloads\";\n\t\t\tinstallstr = lang(\"gui.browser.update\");\n\t\t}\n\t}\n\n\tlet entry = document.createElement(\"div\");\n\tentry.classList.add(\"el\");\n\tentry.id = `mod-${normalized_title}`;\n\n\tentry.innerHTML = `\n\t\t<div class=\"image\">\n\t\t\t<img src=\"${properties.image}\">\n\t\t\t<img class=\"blur\" src=\"${properties.image}\">\n\t\t</div>\n\t\t<div class=\"text\">\n\t\t\t<div class=\"title\">${properties.title}</div>\n\t\t\t<div class=\"description\">${properties.description}</div>\n\t\t\t<button class=\"install bg-blue requires-internet\" onclick=''>\n\t\t\t\t<img src=\"icons/${installicon}.png\">\n\t\t\t\t<span>${installstr}</span>\n\t\t\t</button>\n\t\t\t<button class=\"info requires-internet\" onclick=\"browser.preview.set('${properties.url}')\">\n\t\t\t\t<img src=\"icons/open.png\">\n\t\t\t\t<span>${lang('gui.browser.view')}</span>\n\t\t\t</button>\n\n\t\t\t<button class=\"visual\">${properties.version}</button>\n\t\t\t<button class=\"visual\">\n\t\t\t\t${lang(\"gui.browser.made_by\")}\n\t\t\t\t${properties.author}\n\t\t\t</button>\n\t\t</div>\n\t`\n\n\tentry.querySelector(\"button.install\").addEventListener(\"click\", installcallback)\n\n\tbrowserEntries.appendChild(entry);\n}\n\nbrowser.packages = async () => {\n\tawait browser.loadfront();\n\treturn packages;\n}\n\nlet recent_toasts = {};\nfunction add_recent_toast(name, timeout = 3000) {\n\tif (recent_toasts[name]) {return}\n\n\trecent_toasts[name] = true;\n\n\tsetTimeout(() => {\n\t\tdelete recent_toasts[name];\n\t}, timeout)\n}\n\nipcRenderer.on(\"removed-mod\", (_, mod) => {\n\tset_buttons(true);\n\tbrowser.setbutton(mod.name, lang(\"gui.browser.install\"), \"downloads\");\n\n\tif (mod.manifest_name) {\n\t\tbrowser.setbutton(mod.manifest_name, lang(\"gui.browser.install\"), \"downloads\");\n\t}\n})\n\nipcRenderer.on(\"failed-mod\", (_, modname) => {\n\tif (recent_toasts[\"failed\" + modname]) {return}\n\tadd_recent_toast(\"failed\" + modname);\n\n\tset_buttons(true);\n\ttoasts.show({\n\t\ttimeout: 10000,\n\t\tscheme: \"error\",\n\t\ttitle: lang(\"gui.toast.title.failed\"),\n\t\tdescription: lang(\"gui.toast.desc.failed\")\n\t})\n})\n\nipcRenderer.on(\"legacy-duped-mod\", (_, modname) => {\n\tif (recent_toasts[\"duped\" + modname]) {return}\n\tadd_recent_toast(\"duped\" + modname);\n\n\tset_buttons(true);\n\ttoasts.show({\n\t\ttimeout: 10000,\n\t\tscheme: \"warning\",\n\t\ttitle: lang(\"gui.toast.title.duped\"),\n\t\tdescription: modname + \" \" + lang(\"gui.toast.desc.duped\")\n\t})\n})\n\nipcRenderer.on(\"no-internet\", () => {\n\ttoasts.show({\n\t\ttimeout: 10000,\n\t\tscheme: \"error\",\n\t\ttitle: lang(\"gui.toast.title.no_internet\"),\n\t\tdescription: lang(\"gui.toast.desc.no_internet\")\n\t})\n})\n\nipcRenderer.on(\"installed-mod\", (_, mod) => {\n\tif (recent_toasts[\"installed\" + mod.name]) {return}\n\tadd_recent_toast(\"installed\" + mod.name);\n\n\tlet name = mod.fancy_name || mod.name;\n\n\tset_buttons(true);\n\tbrowser.setbutton(name, lang(\"gui.browser.reinstall\"), \"redo\");\n\n\tif (mod.malformed) {\n\t\ttoasts.show({\n\t\t\ttimeout: 8000,\n\t\t\tscheme: \"warning\",\n\t\t\ttitle: lang(\"gui.toast.title.malformed\"),\n\t\t\tdescription: name + \" \" + lang(\"gui.toast.desc.malformed\")\n\t\t})\n\t}\n\n\ttoasts.show({\n\t\tscheme: \"success\",\n\t\ttitle: lang(\"gui.toast.title.installed\"),\n\t\tdescription: name + \" \" + lang(\"gui.toast.desc.installed\")\n\t})\n\n\tif (mods.install_queue.length != 0) {\n\t\tmods.install_from_url(\n\t\t\t\"https://thunderstore.io/package/download/\" + mods.install_queue[0].pkg,\n\t\t\tfalse, false, mods.install_queue[0].author, mods.install_queue[0].package_name, mods.install_queue[0].version\n\t\t)\n\n\t\tmods.install_queue.shift();\n\t}\n})\n\nlet searchtimeout;\nlet searchstr = \"\";\nlet search = document.querySelector(\"#browser .search\");\nsearch.addEventListener(\"keyup\", () => {\n\tbrowser.filters.toggle(false);\n\tclearTimeout(searchtimeout);\n\n\tif (searchstr != search.value) {\n\t\tif (search.value.replaceAll(\" \", \"\") == \"\") {\n\t\t\tsearchstr = \"\";\n\t\t\tbrowser.loadfront();\n\t\t\treturn\n\t\t}\n\n\t\tsearchtimeout = setTimeout(() => {\n\t\t\tbrowser.search(search.value);\n\t\t\tsearchstr = search.value;\n\t\t}, 500)\n\t}\n})\n\nlet mouse_events = [\"scroll\", \"mousedown\", \"touchdown\"];\nmouse_events.forEach((event) => {\n\tdocument.body.addEventListener(event, () => {\n\t\tlet mouse_at = document.elementsFromPoint(mouseX, mouseY);\n\n\t\tif (! mouse_at.includes(document.querySelector(\"#preview\"))) {\n\t\t\tbrowser.preview.hide();\n\t\t}\n\n\t\tif (! mouse_at.includes(document.querySelector(\"#filter\"))\n\t\t\t&& ! mouse_at.includes(document.querySelector(\".overlay\"))) {\n\t\t\tbrowser.filters.toggle(false);\n\t\t}\n\t})\n});\n\nview.addEventListener(\"dom-ready\", () => {\n\tlet css = [\n\t\tfs.readFileSync(__dirname + \"/../css/theming.css\", \"utf8\"),\n\t\tfs.readFileSync(__dirname + \"/../css/webview.css\", \"utf8\")\n\t]\n\n\tview.insertCSS(css.join(\" \"));\n})\n\nview.addEventListener(\"did-stop-loading\", () => {\n\tview.style.display = \"flex\";\n\tsetTimeout(() => {\n\t\tview.classList.remove(\"loading\");\n\t}, 200)\n})\n\nview.addEventListener(\"did-start-loading\", () => {\n\tview.style.display = \"none\";\n\tview.classList.add(\"loading\");\n})\n\nlet mouseY = 0;\nlet mouseX = 0;\nbrowser_el.addEventListener(\"mousemove\", (event) => {\n\tmouseY = event.clientY;\n\tmouseX = event.clientX;\n})\n\nlet checks = document.querySelectorAll(\".check\");\nfor (let i = 0; i < checks.length; i++) {\n\tchecks[i].setAttribute(\"onclick\", \n\t\t\"this.classList.toggle('checked');browser.loadfront();search.value = ''\"\n\t)\n}\n\nmodule.exports = browser;\n"
  },
  {
    "path": "src/app/js/dom_events.js",
    "content": "const popups = require(\"./popups\");\nconst settings = require(\"./settings\");\n\nlet drag_timer;\ndocument.addEventListener(\"dragover\", (e) => {\n\te.preventDefault();\n\te.stopPropagation();\n\tdragUI.classList.add(\"shown\");\n\n\tclearTimeout(drag_timer);\n\tdrag_timer = setTimeout(() => {\n\t\tdragUI.classList.remove(\"shown\");\n\t}, 5000)\n})\n\ndocument.addEventListener(\"mouseover\", () => {\n\tclearTimeout(drag_timer);\n\tdragUI.classList.remove(\"shown\");\n})\n\ndocument.addEventListener(\"drop\", (e) => {\n\te.preventDefault();\n\te.stopPropagation();\n\n\tdragUI.classList.remove(\"shown\");\n\tmods.install_from_path(e.dataTransfer.files[0].path);\n})\n\ndocument.body.addEventListener(\"click\", (e) => {\n\tif (e.target.tagName.toLowerCase() === \"a\"\n\t\t&& e.target.protocol != \"file:\") {\n\n\t\te.preventDefault();\n\t\tshell.openExternal(e.target.href);\n\t}\n})\n"
  },
  {
    "path": "src/app/js/events.js",
    "content": "const EventEmitter = require(\"events\");\nclass Emitter extends EventEmitter {};\nconst events = new Emitter();\n\nmodule.exports = events;\n"
  },
  {
    "path": "src/app/js/gamepad.js",
    "content": "const popups = require(\"./popups\");\nconst settings = require(\"./settings\");\nconst launcher = require(\"./launcher\");\nconst navigate = require(\"./navigate\");\n\nwindow.addEventListener(\"gamepadconnected\", (e) => {\n\tconsole.log(\"Gamepad connected:\", e.gamepad.id);\n}, false)\n\nwindow.addEventListener(\"gamepaddisconnected\", (e) => {\n\tconsole.log(\"Gamepad disconnected:\", e.gamepad.id);\n}, false)\n\n// this contains the names/directions of axes and IDs that have\n// previously been pressed, if it is found that these were recently\n// pressed in the next iteration of the `setInterval()` below than the\n// iteration is skipped\n//\n// the value of each item is equivalent to the amount of iterations to\n// wait, so `up: 3` will cause it to wait 3 iterations, before `up` can\n// be pressed again\nlet delay_press = {};\n\nlet held_buttons = {};\n\nsetInterval(() => {\n\tlet gamepads = navigator.getGamepads();\n\n\t// this has a list of all the directions that the `.axes[]` are\n\t// pointing in, letting us navigate in that direction\n\tlet directions = {}\n\n\t// keeps track of which buttons `delay_press` that have already been\n\t// lowered, that way we can lower the ones that haven't been lowered\n\t// through a button press\n\tlet lowered_delay = [];\n\n\t// is the select/accept button being held\n\tlet selecting = false;\n\n\tfor (let i in gamepads) {\n\t\tif (! gamepads[i]) {continue}\n\t\t// every other `.axes[]` element is a different coordinate, each\n\t\t// analog stick has 2 elements in `.axes[]`, the first one is\n\t\t// the x coordinate, second is the y coordinate\n\t\t//\n\t\t// so we use this to keep track of which coordinate we're\n\t\t// currently on, and thereby the direction of the float inside\n\t\t// `.axes[i]`\n\t\tlet coord = \"x\";\n\t\tlet deadzone = 0.5;\n\n\t\tfor (let ii = 0; ii < gamepads[i].axes.length; ii++) {\n\t\t\tlet value = gamepads[i].axes[ii];\n\n\t\t\t// check if we're beyond the deadzone in both the negative\n\t\t\t// and positive direction, and then using `coord` add a\n\t\t\t// direction to `directions`\n\t\t\tif (value < -deadzone) {\n\t\t\t\tif (coord == \"y\") {\n\t\t\t\t\tdirections.up = true;\n\t\t\t\t} else {\n\t\t\t\t\tdirections.left = true;\n\t\t\t\t}\n\t\t\t} else if (value > deadzone) {\n\t\t\t\tif (coord == \"y\") {\n\t\t\t\t\tdirections.down = true;\n\t\t\t\t} else {\n\t\t\t\t\tdirections.right = true;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// flip `coord`\n\t\t\tif (coord == \"x\") {\n\t\t\t\tcoord = \"y\";\n\t\t\t} else {\n\t\t\t\tcoord = \"x\";\n\t\t\t}\n\t\t}\n\n\t\t// only support \"standard\" button layouts/mappings\n\t\t//\n\t\t// TODO: for anybody reading this in the future, the support\n\t\t// for other mappings is something that's on the table,\n\t\t// however, due to not having all the hardware in the world,\n\t\t// this will have to be up to someone else\n\t\tif (gamepads[i].mapping != \"standard\") {\n\t\t\tcontinue;\n\t\t}\n\n\t\tfor (let ii = 0; ii < gamepads[i].buttons.length; ii++) {\n\t\t\tif (! gamepads[i].buttons[ii].pressed) {\n\t\t\t\theld_buttons[ii] = false;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// a list of known combinations of buttons for the most\n\t\t\t// common brands out there, more should possibly be added\n\t\t\tlet brands = {\n\t\t\t\t\"Xbox\": {\n\t\t\t\t\taccept: 0,\n\t\t\t\t\tcancel: 1\n\t\t\t\t},\n\t\t\t\t\"Nintendo\": {\n\t\t\t\t\taccept: 1,\n\t\t\t\t\tcancel: 0\n\t\t\t\t},\n\t\t\t\t\"PlayStation\": {\n\t\t\t\t\taccept: 0,\n\t\t\t\t\tcancel: 1\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// this is the most common setup, to my understanding, with\n\t\t\t// the exception of third party Nintendo controller, may\n\t\t\t// need to be adjusted in the future\n\t\t\tlet buttons = {\n\t\t\t\taccept: 0,\n\t\t\t\tcancel: 1\n\t\t\t}\n\n\t\t\t// set `cancel` and `accept` accordingly to the ID of the\n\t\t\t// gamepad, if its a known brand\n\t\t\tfor (let brand in brands) {\n\t\t\t\t// unknown brand\n\t\t\t\tif (! gamepads[i].id.includes(brand)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// set buttons according to brand\n\t\t\t\tbuttons = brands[brand];\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// if the button that's being pressed is the \"accept\"\n\t\t\t// button, then we set `selecting` to `true`, this is done\n\t\t\t// before we check for the button delay so that holding the\n\t\t\t// button keeps the selection in place, until the button is\n\t\t\t// no longer pressed\n\t\t\tif (ii == buttons.accept) {\n\t\t\t\tselecting = true;\n\t\t\t}\n\n\t\t\t// if this button is still delayed, we lower the delay and\n\t\t\t// then go to the next button\n\t\t\tif (delay_press[ii]) {\n\t\t\t\tdelay_press[ii]--;\n\t\t\t\tlowered_delay.push(ii);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// add delay to this button, so it doesn't get clicked\n\t\t\t// immediately again after this\n\t\t\tdelay_press[ii] = 3;\n\n\t\t\tif (held_buttons[ii]) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\theld_buttons[ii] = true;\n\n\t\t\t// interpret `ii` as a specific button/action, using the\n\t\t\t// standard IDs: https://w3c.github.io/gamepad/#remapping\n\t\t\tswitch(ii) {\n\t\t\t\t// settings popup (center cluster buttons)\n\t\t\t\tcase 8: settings.popup.toggle(); break;\n\t\t\t\tcase 9: settings.popup.toggle(); break;\n\n\t\t\t\t// change active section (top bumpers)\n\t\t\t\tcase 4: launcher.relative_section(\"left\"); break;\n\t\t\t\tcase 5: launcher.relative_section(\"right\"); break;\n\n\t\t\t\t// navigate selection (dpad)\n\t\t\t\tcase 12: navigate.move(\"up\"); break;\n\t\t\t\tcase 13: navigate.move(\"down\"); break;\n\t\t\t\tcase 14: navigate.move(\"left\"); break;\n\t\t\t\tcase 15: navigate.move(\"right\"); break;\n\n\t\t\t\t// click selected element\n\t\t\t\tcase buttons.accept: navigate.select(); break;\n\n\t\t\t\t// close last opened popup\n\t\t\t\tcase buttons.cancel: popups.hide_last(); break;\n\t\t\t}\n\t\t}\n\t}\n\n\tfor (let i in directions) {\n\t\tif (directions[i] === true) {\n\t\t\t// if this direction is still delayed, we lower the delay,\n\t\t\t// and then go to the next direction\n\t\t\tif (delay_press[i]) {\n\t\t\t\tdelay_press[i]--;\n\t\t\t\tlowered_delay.push(i);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// move in the direction\n\t\t\tnavigate.move(i);\n\n\t\t\t// add delay to this direction, to prevent it from being\n\t\t\t// triggered immediately again\n\t\t\tdelay_press[i] = 5;\n\t\t}\n\t}\n\n\t// run through buttons that have or have had a delay\n\tfor (let i in delay_press) {\n\t\t// if a button has a delay, and it hasn't already been lowered,\n\t\t// then we lower it\n\t\tif (delay_press[i] && ! lowered_delay.includes(i)) {\n\t\t\tdelay_press[i]--;\n\t\t}\n\t}\n\n\tlet selection_el = document.getElementById(\"selection\");\n\n\t// add `.selecting` to `#selection` depending on whether\n\t// `selecting`, is set or not\n\tif (selecting) {\n\t\tselection_el.classList.add(\"controller-selecting\");\n\t} else {\n\t\tselection_el.classList.remove(\"controller-selecting\");\n\t}\n}, 50)\n\n\nlet can_keyboard_navigate = (e) => {\n\t// quite empty right now, might add more in the future, these are\n\t// just element selectors where movement with the keyboard is off\n\tlet ignore_on_focus = [\n\t\t\"input\",\n\t\t\"select\"\n\t]\n\n\t// check for whether the active element is one that matches\n\t// something in `ignore_on_focus`\n\tfor (let i = 0; i < ignore_on_focus.length; i++) {\n\t\tif (! document.activeElement.matches(ignore_on_focus)) {\n\t\t\t// active element does not match to `ignore_on_focus[i]`\n\t\t\tcontinue;\n\t\t}\n\n\t\t// if the key that's being pressed is \"Escape\" then we unfocus\n\t\t// to the currently focused active element, this lets you go\n\t\t// into an input, and then exit it as well\n\t\tif (e.key == \"Escape\") {\n\t\t\tdocument.activeElement.blur();\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t// check if there's already an active selection\n\tif (document.querySelector(\".active-selection\")) {\n\t\t// this is a list of keys where this keyboard event will be\n\t\t// cancelled on, this prevents key events from being sent to\n\t\t// element, but still lets you type\n\t\tlet cancel_keys = [\n\t\t\t\"Space\", \"Enter\",\n\t\t\t\"ArrowUp\", \"ArrowDown\",\n\t\t\t\"ArrowLeft\", \"ArrowRight\"\n\t\t]\n\n\t\t// cancel this keyboard event if `e.key` is inside `cancel_keys`\n\t\tif (cancel_keys.includes(e.code)) {\n\t\t\te.preventDefault();\n\t\t}\n\t}\n\n\treturn true;\n}\n\nwindow.addEventListener(\"keydown\", (e) => {\n\t// do nothing if we cant navigate\n\tif (! can_keyboard_navigate(e)) {\n\t\treturn;\n\t}\n\n\tlet select = () => {\n\t\t// do nothing if this is a repeat key press\n\t\tif (e.repeat) {return}\n\n\t\t// select `.active-selection`\n\t\tnavigate.select();\n\n\t\t// add `.keyboard-selecting` to `#selection`\n\t\tdocument.getElementById(\"selection\")\n\t\t\t.classList.add(\"keyboard-selecting\");\n\t}\n\n\t// perform the relevant action for the key that was pressed\n\tswitch(e.code) {\n\t\t// select\n\t\tcase \"Space\": return select();\n\t\tcase \"Enter\": return select();\n\n\t\t// close popup\n\t\tcase \"Escape\": return popups.hide_last();\n\n\t\t// move selection\n\t\tcase \"KeyK\": case \"ArrowUp\": return navigate.move(\"up\")\n\t\tcase \"KeyJ\": case \"ArrowDown\": return navigate.move(\"down\")\n\t\tcase \"KeyH\": case \"ArrowLeft\": return navigate.move(\"left\")\n\t\tcase \"KeyL\": case \"ArrowRight\": return navigate.move(\"right\")\n\t}\n})\n\nwindow.addEventListener(\"keyup\", (e) => {\n\tif (! can_keyboard_navigate(e)) {\n\t\treturn;\n\t}\n\n\tlet selection_el = document.getElementById(\"selection\");\n\n\t// perform the relevant action for the key that was pressed\n\tswitch(e.code) {\n\t\t// the second and third cases here are for SteamDeck bumper\n\t\t// button support whilst inside the desktop layout\n\t\tcase \"KeyQ\":\n\t\tcase \"ControlLeft\": case \"ControlRight\":\n\t\t\tlauncher.relative_section(\"left\"); break;\n\t\tcase \"KeyE\":\n\t\tcase \"AltLeft\": case \"AltRight\":\n\t\t\tlauncher.relative_section(\"right\"); break;\n\n\t\tcase \"Space\": return selection_el\n\t\t\t\t.classList.remove(\"keyboard-selecting\");\n\n\t\tcase \"Enter\": return selection_el\n\t\t\t\t.classList.remove(\"keyboard-selecting\");\n\t}\n})\n"
  },
  {
    "path": "src/app/js/gamepath.js",
    "content": "const ipcRenderer = require(\"electron\").ipcRenderer;\n\nconst lang = require(\"../../lang\");\nconst process = require(\"./process\");\nconst launcher = require(\"./launcher\");\nconst settings = require(\"./settings\");\n\n// frontend part of settings a new game path\nipcRenderer.on(\"newpath\", (_, newpath) => {\n\tset_buttons(true);\n\n\tsettings.set({gamepath: newpath});\n\n\tipcRenderer.send(\"gui-getmods\");\n\tipcRenderer.send(\"save-settings\", settings.data());\n})\n\n// a previously valid gamepath no longer exists, and is therefore lost\nipcRenderer.on(\"gamepath-lost\", () => {\n\tlauncher.change_page(0);\n\tset_buttons(false, true);\n\talert(lang(\"gui.gamepath.lost\"));\n})\n\n// error out when no game path is set\nipcRenderer.on(\"no-path-selected\", () => {\n\talert(lang(\"gui.gamepath.must\"));\n\tprocess.exit();\n})\n\n// error out when game path is wrong\nipcRenderer.on(\"wrong-path\", () => {\n\talert(lang(\"gui.gamepath.wrong\"));\n\tgamepath.set(false);\n})\n\n// reports to the main process about game path status.\nmodule.exports = {\n\topen: () => {\n\t\tlet gamepath = settings.data().gamepath;\n\n\t\tif (gamepath) {\n\t\t\trequire(\"electron\").shell.openPath(gamepath);\n\t\t} else {\n\t\t\talert(lang(\"gui.settings.miscbuttons.open_gamepath_alert\"));\n\t\t}\n\t},\n\n\tset: (value) => {\n\t\tipcRenderer.send(\"setpath\", value);\n\t}\n}\n"
  },
  {
    "path": "src/app/js/is_running.js",
    "content": "const lang = require(\"../../lang\");\n\n// is the game running?\nlet is_running = false;\n\n// updates play buttons depending on whether the game is running\nipcRenderer.on(\"is-running\", (event, running) => {\n\tlet set_playbtns = (text) => {\n\t\tlet playbtns = document.querySelectorAll(\".playBtn\");\n\t\tfor (let i = 0; i < playbtns.length; i++) {\n\t\t\tplaybtns[i].innerHTML = text;\n\t\t}\n\t}\n\n\tif (running && is_running != running) {\n\t\tset_buttons(false);\n\t\tset_playbtns(lang(\"general.running\"));\n\n\t\tis_running = running;\n\n\t\t// show force quit button in Titanfall tab\n\t\ttfquit.style.display = \"inline-block\";\n\n\t\tupdate.setAttribute(\"onclick\", \"kill('game')\");\n\t\tupdate.innerHTML = \"(\" + lang(\"ns.menu.force_quit\") + \")\";\n\t\treturn;\n\t}\n\n\tif (is_running != running) {\n\t\tset_buttons(true);\n\t\tset_playbtns(lang(\"gui.launch\"));\n\n\t\tis_running = running;\n\n\t\t// hide force quit button in Titanfall tab\n\t\ttfquit.style.display = \"none\";\n\n\t\tupdate.setAttribute(\"onclick\", \"update.ns()\");\n\t\tupdate.innerHTML = \"(\" + lang(\"gui.update.check\") + \")\";\n\t}\n})\n\n// return whether the game is running\nmodule.exports = () => {\n\treturn is_running;\n}\n"
  },
  {
    "path": "src/app/js/kill.js",
    "content": "const ipcRenderer = require(\"electron\").ipcRenderer;\n\n// attempts to kill something using the main process' `modules/kill.js`\n// functions, it simply attempts to run `kill[function_name]()`, if it\n// doesn't exist, nothing happens\nmodule.exports = (function_name) => {\n\tipcRenderer.send(\"kill\", function_name);\n}\n"
  },
  {
    "path": "src/app/js/launch.js",
    "content": "const update = require(\"./update\");\n\n// tells the main process to launch `game_version`\nmodule.exports = (game_version) => {\n\tif (game_version == \"vanilla\") {\n\t\tipcRenderer.send(\"launch\", game_version);\n\t\treturn;\n\t}\n\n\tif (update.ns.should_install) {\n\t\tupdate.ns();\n\t} else {\n\t\tipcRenderer.send(\"launch\", game_version);\n\t}\n}\n"
  },
  {
    "path": "src/app/js/launcher.js",
    "content": "const popups = require(\"./popups\");\nconst markdown = require(\"marked\").parse;\n\nlet launcher = {};\n\nvar servercount;\nvar playercount;\nvar masterserver;\n\n// changes the main page, this is the tabs in the sidebar\nlauncher.change_page = (page) => {\n\tlet btns = document.querySelectorAll(\".gamesContainer button\");\n\tlet pages = document.querySelectorAll(\".mainContainer .contentContainer\");\n\n\tfor (let i = 0; i < pages.length; i++) {\n\t\tpages[i].classList.add(\"hidden\");\n\t}\n\n\tfor (let i = 0; i < btns.length; i++) {\n\t\tbtns[i].classList.add(\"inactive\");\n\t}\n\n\tpages[page].classList.remove(\"hidden\");\n\tbtns[page].classList.remove(\"inactive\");\n\tbgHolder.setAttribute(\"bg\", page);\n}; launcher.change_page(1)\n\nlauncher.format_release = (notes) => {\n\tif (! notes) {return \"\"}\n\n\tlet content = \"\";\n\n\tif (notes.length === 1) {\n\t\tcontent = notes[0];\n\t} else {\n\t\tfor (let release of notes) {\n\t\t\tif (release.prerelease) {continue}\n\t\t\tlet new_content = \n\t\t\t\t// release date\n\t\t\t\tnew Date(release.published_at).toLocaleString() +\n\t\t\t\t\"\\n\" +\n\n\t\t\t\t// release name\n\t\t\t\t`# ${release.name}` +\n\t\t\t\t\"\\n\\n\" +\n\n\t\t\t\t// actual release text/body\n\t\t\t\trelease.body +\n\t\t\t\t\"\\n\\n\\n\";\n\n\t\t\tcontent +=\n\t\t\t\t\"<div class='release-block'>\\n\"\n\t\t\t\t\t+ markdown(new_content, {breaks: true}) + \"\\n\" +\n\t\t\t\t\"</div>\";\n\t\t}\n\t\n\t\tcontent = content.replaceAll(/\\@(\\S+)/g, `<a href=\"https://github.com/$1\">@$1</a>`);\n\t}\n\n\treturn markdown(content, {\n\t\tbreaks: true\n\t});\n}\n\n// sets content of `div` to a single release block with centered text\n// inside it, the text being `lang(lang_key)`\nlauncher.error = (div, lang_key) => {\n\tdiv.innerHTML =\n\t\t\"<div class='release-block'>\" +\n\t\t\t\"<p><center>\" +\n\t\t\t\tlang(lang_key) +\n\t\t\t\"</center></p>\" +\n\t\t\"</div>\";\n}\n\n// updates the Viper release notes\nipcRenderer.on(\"vp-notes\", (event, response) => {\n\tif (! response) {\n\t\treturn launcher.error(\n\t\t\tvpReleaseNotes,\n\t\t\t\"request.no_vp_release_notes\"\n\t\t)\n\t}\n\n\tvpReleaseNotes.innerHTML = launcher.format_release(response);\n});\n\n// updates the Northstar release notes\nipcRenderer.on(\"ns-notes\", (event, response) => {\n\tif (! response) {\n\t\treturn launcher.error(\n\t\t\tnsRelease,\n\t\t\t\"request.no_ns_release_notes\"\n\t\t)\n\t}\n\n\tnsRelease.innerHTML = launcher.format_release(response);\n});\n\nlauncher.load_vp_notes = async () => {\n\tipcRenderer.send(\"get-vp-notes\");\t\n}; launcher.load_vp_notes();\n\nlauncher.load_ns_notes = async () => {\n\tipcRenderer.send(\"get-ns-notes\");\n}; launcher.load_ns_notes();\n\n// TODO: We gotta make this more automatic instead of switch statements\n// it's both not pretty, but adding more sections requires way too much\n// effort, compared to how it should be.\nlauncher.show_vp = (section) => {\n\tif (![\"main\", \"release\", \"info\", \"credits\"].includes(section)) throw new Error(\"unknown vp section\");\n\tvpMainBtn.removeAttribute(\"active\");\n\tvpReleaseBtn.removeAttribute(\"active\");\n\tvpInfoBtn.removeAttribute(\"active\");\n\n\tvpMain.classList.add(\"hidden\");\n\tvpReleaseNotes.classList.add(\"hidden\");\n\tvpInfo.classList.add(\"hidden\");\n\n\tswitch(section) {\n\t\tcase \"main\":\n\t\t\tvpMainBtn.setAttribute(\"active\", \"\");\n\t\t\tvpMain.classList.remove(\"hidden\");\n\t\t\tbreak;\n\t\tcase \"release\":\n\t\t\tvpReleaseBtn.setAttribute(\"active\", \"\");\n\t\t\tvpReleaseNotes.classList.remove(\"hidden\");\n\t\t\tbreak;\n\t\tcase \"info\":\n\t\t\tvpInfoBtn.setAttribute(\"active\", \"\");\n\t\t\tvpInfo.classList.remove(\"hidden\");\n\t\t\tbreak;\n\t}\n}\n\nlauncher.show_ns = (section) => {\n\tif (! [\"main\", \"release\", \"mods\"].includes(section)) {\n\t\tthrow new Error(\"unknown ns section\");\n\t}\n\n\tnsMainBtn.removeAttribute(\"active\");\n\tnsModsBtn.removeAttribute(\"active\");\n\tnsReleaseBtn.removeAttribute(\"active\");\n\n\tnsMain.classList.add(\"hidden\");\n\tnsMods.classList.add(\"hidden\");\n\tnsRelease.classList.add(\"hidden\");\n\n\tswitch(section) {\n\t\tcase \"main\":\n\t\t\tnsMainBtn.setAttribute(\"active\", \"\");\n\t\t\tnsMain.classList.remove(\"hidden\");\n\t\t\tbreak;\n\t\tcase \"mods\":\n\t\t\tnsModsBtn.setAttribute(\"active\", \"\");\n\t\t\tnsMods.style.display = \"block\";\n\t\t\tnsMods.classList.remove(\"hidden\");\n\t\t\tbreak;\n\t\tcase \"release\":\n\t\t\tnsReleaseBtn.setAttribute(\"active\", \"\");\n\t\t\tnsRelease.classList.remove(\"hidden\");\n\t\t\tbreak;\n\t}\n}\n\n// changes the active section on the currently active\n// `.contentContainer` in the direction specified\n//\n// `direction` can be: left or right\nlauncher.relative_section = (direction) => {\n\t// prevent switching section if a popup is open\n\tif (popups.open_list().length) {\n\t\treturn;\n\t}\n\n\t// the `.contentMenu` in the currently active tab\n\tlet active_menu = document.querySelector(\n\t\t\".contentContainer:not(.hidden) .contentMenu\"\n\t)\n\n\t// get the currently active section\n\tlet active_section = active_menu.querySelector(\"[active]\");\n\n\t// no need to do anything, if there's somehow no active section\n\tif (! active_section) {return}\n\n\t// these will be filled out\n\tlet prev_section, next_section;\n\n\t// get list of all the sections\n\tlet sections = active_menu.querySelectorAll(\"li\");\n\n\tfor (let i = 0; i < sections.length; i++) {\n\t\tif (sections[i] != active_section) {\n\t\t\tcontinue;\n\t\t}\n\n\t\t// make `next_section` be the next element in `sections`\n\t\tnext_section = sections[i + 1];\n\n\t\t// if we're at the first iteration, use the last element in\n\t\t// `sections` as the previous section, otherwise make it the\n\t\t// element before this iteration\n\t\tif (i == 0) {\n\t\t\tprev_section = sections[sections.length - 1];\n\t\t} else {\n\t\t\tprev_section = sections[i - 1];\n\t\t}\n\t}\n\n\tlet new_section;\n\n\t// if we're going left, and a previous section was found, click it\n\tif (direction == \"left\" && prev_section) {\n\t\tnew_section = prev_section;\n\t} else if (direction == \"right\") {\n\t\t// click the next section, if one was found, otherwise just\n\t\t// assume that the first section is the next section, as the\n\t\t// active section is likely just the last section, so we wrap\n\t\t// around instead\n\t\tif (next_section) {\n\t\t\tnew_section = next_section;\n\t\t} else if (sections[0]) {\n\t\t\tnew_section = sections[0];\n\t\t}\n\t}\n\n\tif (new_section) {\n\t\tnew_section.click();\n\n\t\t// if there's an active selection, we select the new section, as\n\t\t// that selection may be in a section that's now hidden\n\t\tif (document.querySelector(\".active-selection\")) {\n\t\t\tnavigate.selection(new_section);\n\t\t}\n\t}\n}\n\nlauncher.check_servers = async () => {\n\tserverstatus.classList.add(\"checking\");\n\n\ttry {\n\t\tlet host = \"northstar.tf\";\n\t\tlet path = \"/client/servers\";\n\n\t\t// ask the masterserver for the list of servers, if this has\n\t\t// been done recently, it'll simply return the cached version\n\t\tlet servers = JSON.parse(\n\t\t\tawait request(host, path, \"ns-servers\", false)\n\t\t)\n\n\t\tmasterserver = true;\n\n\t\tplayercount = 0;\n\t\tservercount = servers.length;\n\n\t\tfor (let i = 0; i < servers.length; i++) {\n\t\t\tplayercount += servers[i].playerCount\n\t\t}\n\t}catch (err) {\n\t\tplayercount = 0;\n\t\tservercount = 0;\n\t\tmasterserver = false;\n\t}\n\n\tserverstatus.classList.remove(\"checking\");\n\n\tif (servercount == 0 || ! servercount || ! playercount) {masterserver = false}\n\n\tlet playerstr = lang(\"gui.server.players\");\n\tif (playercount == 1) {\n\t\tplayerstr = lang(\"gui.server.player\");\n\t}\n\n\tif (masterserver) {\n\t\tserverstatus.classList.add(\"up\");\n\t\tserverstatus.classList.remove(\"down\");\n\t\tserverstatus.innerHTML = `${servercount} ${lang(\"gui.server.servers\")} - ${playercount} ${playerstr}`;\n\t} else {\n\t\tserverstatus.classList.add(\"down\");\n\t\tserverstatus.classList.remove(\"up\");\n\t\tserverstatus.innerHTML = lang(\"gui.server.offline\");\n\n\t}\n}; launcher.check_servers()\n\n// refreshes every 5 minutes\nsetInterval(() => {\n\tlauncher.check_servers();\n}, 300000)\n\nmodule.exports = launcher;\n"
  },
  {
    "path": "src/app/js/localize.js",
    "content": "// localizes `string`, removing instances of `%%string%%` with\n// `lang(\"string\")` and so forth\nfunction localize_string(string) {\n\tlet parts = string.split(\"%%\");\n\n\t// basic checks to make sure `string` has lang strings\n\tif (parts.length == 0\n\t\t|| string.trim() == \"\" || ! string.match(\"%%\")) {\n\t\treturn string;\n\t}\n\n\tfor (let i = 0; i < parts.length; i++) {\n\t\t// simply checks to make sure it is actually a lang string.\n\t\tif (parts[i][0] != \" \" && \n\t\t\tparts[i][parts[i].length - 1] != \" \") {\n\n\t\t\t// get string\n\t\t\tlet lang_str = lang(parts[i]);\n\n\t\t\t// make sure we got a string back, and if not, do nothing\n\t\t\tif (typeof lang_str !== \"string\") {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// replace this part with the lang string\n\t\t\tparts[i] = lang_str;\n\t\t}\n\t}\n\n\t// return finalized formatted string\n\treturn parts.join(\"\");\n}\n\n// runs `localize_string()` on `el`'s attributes, text nodes and children\nfunction localize_el(el) {\n\t// we don't want to mess with script tags\n\tif (el.tagName == \"SCRIPT\") {return}\n\n\tlet attributes = el.getAttributeNames();\n\n\t// run through child nodes\n\tfor (let i = 0; i < el.childNodes.length; i++) {\n\t\t// if the node isn't a text node, we do nothing\n\t\tif (el.childNodes[i].nodeType != Node.TEXT_NODE) {\n\t\t\tcontinue;\n\t\t}\n\n\t\t// the node is a text node, so we set its `.textContent` by\n\t\t// running `format_string()` on it\n\t\tel.childNodes[i].textContent =\n\t\t\tlocalize_string(el.childNodes[i].textContent)\n\t}\n\n\t// run through attributes and run `format_string()` on their values\n\tfor (let i = 0; i < attributes.length; i++) {\n\t\tlet attr = el.getAttribute(attributes[i]);\n\t\tel.setAttribute(attributes[i], localize_string(attr))\n\t}\n\n\t// run `replace_in_el()` on `el`'s children\n\tfor (let i = 0; i < el.children.length; i++) {\n\t\tlocalize_el(el.children[i]);\n\t}\n}\n\n// localizes lang strings on (almost) all the elements inside `<body>`\nmodule.exports = () => {\n\tlocalize_el(document.body);\n}\n"
  },
  {
    "path": "src/app/js/mods.js",
    "content": "const util = require('util');\nconst ipcRenderer = require(\"electron\").ipcRenderer;\n\nconst lang = require(\"../../lang\");\n\nconst version = require(\"./version\");\nconst toasts = require(\"./toasts\");\nconst set_buttons = require(\"./set_buttons\");\n\nlet mods = {};\n\nlet mods_list = {\n\tall: [],\n\tenabled: [],\n\tdisabled: []\n}\n\n// returns the list of mods\nmods.list = () => {\n\treturn mods_list;\n}\n\nmods.load = (mods_obj) => {\n\tmodcount.innerHTML = `${lang(\"gui.mods.count\")} ${mods_obj.all.length}`;\n\n\tlet normalized_names = [];\n\n\tlet set_mod = (mod) => {\n\t\tlet name = mod.name;\n\t\tif (mod.package) {\n\t\t\tname = mod.package.package_name;\n\t\t}\n\n\t\tlet normalized_name = \"mod-list-\" + mods.normalize(name);\n\n\t\tnormalized_names.push(normalized_name);\n\n\t\tlet el = document.getElementById(normalized_name);\n\t\tif (el) {\n\t\t\tif (mod.disabled) {\n\t\t\t\tel.querySelector(\".switch\").classList.remove(\"on\");\n\t\t\t} else {\n\t\t\t\tel.querySelector(\".switch\").classList.add(\"on\");\n\t\t\t}\n\n\t\t\treturn;\n\t\t}\n\n\t\tlet div = document.createElement(\"div\");\n\t\tdiv.classList.add(\"el\");\n\t\tdiv.id = normalized_name;\n\n\t\tlet mod_details = {\n\t\t\tname: mod.name,\n\t\t\tversion: mod.version,\n\t\t\tdescription: mod.description\n\t\t}\n\n\t\tif (mod.package) {\n\t\t\tmod_details = {\n\t\t\t\timage: mod.package.icon,\n\t\t\t\tname: mod.package.manifest.name,\n\t\t\t\tversion: mod.package.manifest.version_number,\n\t\t\t\tdescription: mod.package.manifest.description\n\t\t\t}\n\t\t}\n\n\t\tdiv.innerHTML += `\n\t\t\t<div class=\"image\">\n\t\t\t\t<img src=\"${mod_details.image || \"\"}\">\n\t\t\t\t<img class=\"blur\" src=\"\">\n\t\t\t</div>\n\t\t\t<div class=\"text\">\n\t\t\t\t<div class=\"title\">${mod_details.name}</div>\n\t\t\t\t<div class=\"description\">${mod_details.description}</div>\n\t\t\t\t<button class=\"switch on orange\"></button>\n\t\t\t\t<button class=\"update bg-blue requires-internet\">\n\t\t\t\t\t<img src=\"icons/downloads.png\">\n\t\t\t\t\t<span>${lang(\"gui.browser.update\")}</span>\n\t\t\t\t</button>\n\t\t\t\t<button class=\"bg-red remove\">\n\t\t\t\t\t<img src=\"icons/trash.png\">\n\t\t\t\t\t<span>${lang(\"gui.mods.remove\")}</span>\n\t\t\t\t</button>\n\n\t\t\t\t<button class=\"visual\">${version.format(mod_details.version)}</button>\n\t\t\t\t<button class=\"visual\">\n\t\t\t\t\t${lang(\"gui.browser.made_by\")}\n\t\t\t\t\t${mod.author || lang(\"gui.mods.unknown_author\")}\n\t\t\t\t</button>\n\t\t\t</div>\n\t\t`;\n\n\t\tif (mod_details.name.match(/^Northstar\\..*/)) {\n\t\t\tdiv.querySelector(\"img\").src = \"icons/northstar.png\"\n\t\t}\n\n\t\tdiv.querySelector(\".remove\").onclick = () => {\n\t\t\tif (! mod.package) {\n\t\t\t\treturn mods.remove(mod.name);\n\t\t\t}\n\n\t\t\tfor (let i = 0; i < mod.packaged_mods.length; i++) {\n\t\t\t\tmods.remove(mod.packaged_mods[i]);\n\t\t\t}\n\t\t}\n\n\t\tif (mod.disabled) {\n\t\t\tdiv.querySelector(\".switch\").classList.remove(\"on\");\n\t\t}\n\n\t\tdiv.querySelector(\".switch\").addEventListener(\"click\", () => {\n\t\t\tif (! mod.package) {\n\t\t\t\treturn mods.toggle(mod.name);\n\t\t\t}\n\n\t\t\tfor (let i = 0; i < mod.packaged_mods.length; i++) {\n\t\t\t\tmods.toggle(mod.packaged_mods[i]);\n\t\t\t}\n\t\t})\n\n\t\tdiv.querySelector(\".image\").style.display = \"none\";\n\n\t\tmodsdiv.append(div);\n\t}\n\n\tfor (let i = 0; i < mods_obj.all.length; i++) {\n\t\tset_mod(mods_obj.all[i]);\n\t}\n\n\tlet mod_els = document.querySelectorAll(\"#modsdiv .el\");\n\tlet mod_update_els = [];\n\n\tfor (let i = 0; i < mod_els.length; i++) {\n\t\tlet update_btn = mod_els[i].querySelector(\".update\");\n\n\t\tif (update_btn && update_btn.style.display != \"none\") {\n\t\t\tmod_update_els.push(mod_els[i].id);\n\t\t} else {\n\t\t\tbreak;\n\t\t}\n\t}\n\n\tfor (let i = 0; i < mod_els.length; i++) {\n\t\tlet mod = mod_els[i].id.replace(/^mod-list-/, \"\");\n\n\t\tif (! normalized_names.includes(mod_els[i].id)) {\n\t\t\tmod_els[i].remove();\n\t\t\treturn;\n\t\t}\n\n\t\tlet image_container = mod_els[i].querySelector(\".image\");\n\t\tlet image_el = image_container.querySelector(\"img\")\n\t\tlet image_blur_el = image_container.querySelector(\"img.blur\")\n\n\t\tif (browser.mod_versions[mod]) {\n\t\t\timage_el.src = browser.mod_versions[mod].package.versions[0].icon;\n\t\t}\n\n\t\tif (image_el.getAttribute(\"src\") &&\n\t\t\t! image_container.parentElement.classList.contains(\"has-icon\")) {\n\n\t\t\tlet image_src = image_el.getAttribute(\"src\");\n\n\t\t\timage_blur_el.src = image_src;\n\t\t\timage_container.style.display = null;\n\n\t\t\timage_container.parentElement.classList.add(\"has-icon\");\n\t\t}\n\n\t\tif (browser.mod_versions[mod]\n\t\t\t&& browser.mod_versions[mod].has_update) {\n\n\t\t\tmod_els[i].querySelector(\".update\").style.display = null;\n\n\t\t\tmod_els[i].querySelector(\".update\").setAttribute(\n\t\t\t\t\"onclick\", `browser.mod_versions[\"${mod}\"].install()`\n\t\t\t)\n\n\t\t\tif (mod_update_els.includes(mod_els[i].id)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tlet mod_el = mod_els[i].cloneNode(true);\n\n\t\t\t// copy click event of the remove button to the new button\n\t\t\tmod_el.querySelector(\".remove\").onclick =\n\t\t\t\tmod_els[i].querySelector(\".remove\").onclick;\n\n\t\t\tmod_el.classList.add(\"no-animation\");\n\n\t\t\tmod_el.querySelector(\".switch\").addEventListener(\"click\", () => {\n\t\t\t\tif (browser.mod_versions[mod].local_name) {\n\t\t\t\t\tmods.toggle(browser.mod_versions[mod].local_name);\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tmod_els[i].remove();\n\t\t\tmodsdiv.querySelector(\".line:has(.search)\").after(mod_el);\n\t\t} else {\n\t\t\tmod_els[i].querySelector(\".update\").style.display = \"none\";\n\t\t}\n\t}\n}\n\n// attempts to filter `#modsdiv` with `query`\nmods.search = (query) => {\n\t// if no `query` is given, use the search input's value\n\tif (! query) {\n\t\tquery = mods.search.el.value;\n\t}\n\n\t// normalizes `string`\n\tlet normalize = (string) => {\n\t\treturn string\n\t\t\t.trim().toLowerCase()\n\t\t\t.replaceAll(\" \", \"\").replaceAll(\".\", \"\");\n\t}\n\n\t// normalize `query`\n\tquery = normalize(query);\n\n\t// get all mod elements\n\tlet mod_els = document.querySelectorAll(\"#modsdiv .el\");\n\n\t// run through all the mod elements\n\tfor (let mod_el of mod_els) {\n\t\t// get the normalized name of the mod\n\t\tlet name = normalize(mod_el.querySelector(\".title\").innerText);\n\n\t\t// if the name has `query` in it, show it\n\t\tif (name.match(query)) {\n\t\t\tmod_el.style.display = null;\n\t\t} else { // hide if it doesn't match\n\t\t\tmod_el.style.display = \"none\";\n\t\t}\n\t}\n}\n\nmods.search.el = document.getElementById(\"mods-search\");\nmods.search.el.addEventListener(\"keyup\", () => {mods.search()})\n\nmods.remove = (mod) => {\n\tif (mod.toLowerCase().match(/^northstar\\./)) {\n\t\tif (! confirm(lang(\"gui.mods.required_confirm\"))) {\n\t\t\treturn;\n\t\t}\n\t} else if (mod == \"allmods\") {\n\t\tif (! confirm(lang(\"gui.mods.remove_all_confirm\"))) {\n\t\t\treturn;\n\t\t}\n\t}\n\n\tipcRenderer.send(\"remove-mod\", mod);\n}\n\nmods.toggle = (mod) => {\n\t// is this a core mod?\n\tif (mod.toLowerCase().match(/^northstar\\./)) {\n\t\t// keep track of whether this mod is disabled\n\t\tlet is_disabled = false;\n\n\t\t// run through disabled mods\n\t\tfor (let mod_obj of mods_list.disabled) {\n\t\t\t// if `mod` is `mod_obj`, update `is_disabled`\n\t\t\tif (mod_obj.name.toLowerCase() == mod.toLowerCase()) {\n\t\t\t\tis_disabled = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\t// show prompt if the mod is enabled\n\t\tif (! is_disabled && ! confirm(lang(\"gui.mods.required_confirm\"))) {\n\t\t\treturn;\n\t\t}\n\t} else if (mod == \"allmods\") {\n\t\tif (! confirm(lang(\"gui.mods.toggle_all_confirm\"))) {\n\t\t\treturn;\n\t\t}\n\t}\n\n\tipcRenderer.send(\"toggle-mod\", mod);\n}\n\nmods.install_queue = [];\n\n// tells the main process to install a mod through the file selector\nmods.install_prompt = () => {\n\tset_buttons(false);\n\tipcRenderer.send(\"install-mod\");\n}\n\n// tells the main process to directly install a mod from this path\nmods.install_from_path = (path) => {\n\tset_buttons(false);\n\tipcRenderer.send(\"install-from-path\", path);\n}\n\n// tells the main process to install a mod from a URL\nmods.install_from_url = (url, dependencies, clearqueue, author, package_name, version) => {\n\tif (clearqueue) {mods.install_queue = []};\n\n\tlet prettydepends = [];\n\n\tif (dependencies) {\n\t\tlet newdepends = [];\n\t\tfor (let i = 0; i < dependencies.length; i++) {\n\t\t\tlet depend = dependencies[i].toLowerCase();\n\t\t\tif (! depend.match(/northstar-northstar-.*/)) {\n\t\t\t\tdepend = dependencies[i].replaceAll(\"-\", \"/\");\n\t\t\t\tlet pkg = depend.split(\"/\");\n\t\t\t\tif (! mods.is_installed(pkg[1])) {\n\t\t\t\t\tnewdepends.push({\n\t\t\t\t\t\tpkg: depend,\n\t\t\t\t\t\tauthor: pkg[0],\n\t\t\t\t\t\tversion: pkg[2],\n\t\t\t\t\t\tpackage_name: pkg[1]\n\t\t\t\t\t});\n\n\t\t\t\t\tprettydepends.push(`${pkg[1]} v${pkg[2]} - ${lang(\"gui.browser.made_by\")} ${pkg[0]}`);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tdependencies = newdepends;\n\t} \n\n\tif (dependencies && dependencies.length != 0) {\n\t\tlet confirminstall = confirm(lang(\"gui.mods.confirm_dependencies\") + prettydepends.join(\"\\n\"));\n\t\tif (! confirminstall) {\n\t\t\treturn;\n\t\t}\n\t}\n\n\tset_buttons(false);\n\tipcRenderer.send(\"install-from-url\", url, author, package_name, version);\n\n\tif (dependencies) {\n\t\tmods.install_queue = dependencies;\n\t}\n}\n\nmods.is_installed = (modname) => {\n\tfor (let i = 0; i < mods.list().all.length; i++) {\n\t\tlet mod = mods.list().all[i];\n\t\tif (mod.manifest_name) {\n\t\t\tif (mod.manifest_name.match(modname)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t} else if (mod.name.match(modname)) {\n\t\t\treturn true;\n\t\t}\n\t}\n\n\treturn false;\n}\n\nmods.normalize = (items) => {\n\tlet main = (string) => {\n\t\treturn string.replaceAll(\" \", \"\")\n\t\t\t.replaceAll(\".\", \"\").replaceAll(\"-\", \"\")\n\t\t\t.replaceAll(\"_\", \"\").toLowerCase();\n\t}\n\tif (typeof items == \"string\") {\n\t\treturn main(items);\n\t} else {\n\t\tlet newArray = [];\n\t\tfor (let i = 0; i < items.length; i++) {\n\t\t\tnewArray.push(main(items[i]));\n\t\t}\n\n\t\treturn newArray;\n\t}\n}\n\n// updates the installed mods\nipcRenderer.on(\"mods\", (event, mods_obj) => {\n\tmods_list = mods_obj;\n\tif (! mods_obj) {return}\n\n\tmods.load(mods_obj);\n})\n\nipcRenderer.on(\"protocol-install-mod\", async (event, data) => {\n\tconst domain = data[0];\n\tconst author = data[1];\n\tconst package_name = data[2];\n\tconst version = data[3];\n\n\tconst packages = await browser.packages();\n\n\tconst package = packages.find((package) => { return package.owner == author && package.name == package_name; })\n\tif (!package) {\n\t\talert(util.format(lang(\"gui.mods.cant_find_specific\"), author, package_name));\n\t\treturn;\n\t}\n\n\tconst package_obj = package.versions.find((package_version) => { return package_version.version_number == version; })\n\tif (!package_obj) {\n\t\talert(util.format(lang(\"gui.mods.cant_find_version\"), version, author, package_name))\n\t\treturn;\n\t}\n\n\ttoasts.show({\n\t\ttimeout: 3000,\n\t\tscheme: \"info\",\n\t\ttitle: lang(\"gui.mods.installing\"),\n\t\tdescription: package_obj.full_name\n\t})\n\n\tmods.install_from_url(\n\t\tpackage_obj.download_url,\n\t\tpackage_obj.dependencies,\n\t\tfalse,\n\n\t\tauthor,\n\t\tpackage_name,\n\t\tversion\n\t);\n})\n\nmodule.exports = mods;\n"
  },
  {
    "path": "src/app/js/navigate.js",
    "content": "const events = require(\"./events\");\nconst popups = require(\"./popups\");\nconst settings = require(\"./settings\");\n\nlet navigate = {\n\tusing: false\n}\n\n// sets `#selection` to the correct position, size and border radius,\n// according to what is currently the `.active-selection`, if none is\n// found, it'll instead be hidden\nnavigate.selection = (new_selection) => {\n\t// if we're not allowed to unselect an element, then make sure that\n\t// element is still unselected, and then clear\n\t// `navigate.dont_unselect`\n\tif (navigate.dont_unselect\n\t\t&& navigate.dont_unselect != new_selection) {\n\n\t\tnavigate.dont_unselect.classList.add(\"active-selection\");\n\t\tnavigate.dont_unselect = false;\n\t\treturn;\n\t}\n\n\tif (new_selection) {\n\t\tlet selected = document.querySelectorAll(\".active-selection\");\n\n\t\t// make sure just `new_selection` has `.active-selection`\n\t\tfor (let i = 0; i < selected.length; i++) {\n\t\t\tif (selected[i] != new_selection) {\n\t\t\t\tselected[i].classList.remove(\"active-selection\");\n\t\t\t}\n\t\t}\n\n\t\tnew_selection.classList.add(\"active-selection\");\n\t}\n\n\t// shorthands\n\tlet selection_el = document.getElementById(\"selection\");\n\tlet active_el = document.querySelector(\".active-selection\");\n\n\t// make sure there's an `active_el`, and hide the `selection_el` if\n\t// that isn't the case\n\tif (! active_el) {\n\t\tselection_el.style.opacity = \"0.0\";\n\t\treturn;\n\t}\n\n\t// this adds space between the `selection_el` and ``\n\tlet padding = 8;\n\n\t// attempt to get the border radius of `active_el`\n\tlet radius = getComputedStyle(active_el).borderRadius;\n\n\t// if there's no radius set, we default to the default of\n\t// `selection_el` through using `null`\n\tif (! radius || radius == \"0px\") {\n\t\tradius = null;\n\t}\n\n\t// set visibility and radius\n\tselection_el.style.opacity = \"1.0\";\n\tselection_el.style.borderRadius = radius;\n\n\t// get bounds for position and size calculations of `selection_el`\n\tlet active_bounds = active_el.getBoundingClientRect();\n\n\t// set top and left side coordinate subtracting the padding\n\tselection_el.style.top = active_bounds.top - padding + \"px\";\n\tselection_el.style.left = active_bounds.left - padding + \"px\";\n\n\t// set width of `selection_el` with the padding\n\tselection_el.style.width =\n\t\tactive_bounds.width + (padding * 2) + \"px\";\n\n\t// set height of `selection_el` with the padding\n\tselection_el.style.height =\n\t\tactive_bounds.height + (padding * 2) + \"px\";\n}\n\n// data from the last iterations of the interval below\nlet last_sel = {\n\tel: false,\n\tbounds: false\n}\n\n// auto update `#selection` if `.active-selection` changes bounds, but\n// not element by itself\nsetInterval(() => {\n\t// get active selection\n\tlet selected = document.querySelector(\".active-selection\");\n\n\t// if there's no active selection, reset `last_sel`\n\tif (! selected) {\n\t\tlast_sel.el = false;\n\t\tlast_sel.bounds = false;\n\n\t\treturn;\n\t}\n\n\t// get stringified bounds\n\tlet bounds = JSON.stringify(selected.getBoundingClientRect());\n\n\t// if `last_sel.el` is not `selected` the selected element was\n\t// changed, so we just set `last_el` and nothing more\n\tif (last_sel.el != selected) {\n\t\tlast_sel.el = selected;\n\t\tlast_sel.bounds = bounds;\n\n\t\treturn;\n\t}\n\n\t// if stringified bounds changed we update `#selection`\n\tif (bounds != last_sel.bounds) {\n\t\tnavigate.selection();\n\t\tlast_sel.el = selected;\n\t\tlast_sel.bounds = bounds;\n\t}\n}, 50)\n\n// these events cause the `#selection` element to reposition itself\nwindow.addEventListener(\"resize\", () => {navigate.selection()}, true);\nwindow.addEventListener(\"scroll\", () => {navigate.selection()}, true);\n\n// listen for click events, and hide the `#selection` element, when\n// emitting a mouse event we will want to hide, as it then isn't needed\nwindow.addEventListener(\"click\", (e) => {\n\t// make sure its a trusted click event, and therefore actually a\n\t// mouse, and not anything else\n\tif (! e.isTrusted) {\n\t\treturn;\n\t}\n\n\t// we're no longer using navigation functions\n\tnavigate.using = false;\n\n\t// get the `.active-selection`\n\tlet active_el = document.querySelector(\".active-selection\");\n\n\t// if there's an `active_el` then we unselect it, and update the\n\t// `#selection` element, hiding it\n\tif (active_el) {\n\t\tactive_el.classList.remove(\"active-selection\");\n\t\tnavigate.selection();\n\t}\n})\n\n// returns a list of valid elements that should be possible to navigate\n// to/select with the `#selection` element\n//\n// setting `div` makes it limit itself to elements inside that, without\n// it, it'll use `document.body` or the active popup, if one is found\nnavigate.get_els = (div) => {\n\tlet els = [];\n\n\t// is `div` not set, and is there a popup shown\n\tif (! div && document.body.querySelector(\".popup.shown\")) {\n\t\t// the spread operator is to convert from a `NodeList` to an\n\t\t// `Array`, and then we need to reverse this to get the ones\n\t\t// that are layered on top first.\n\t\tlet popups_list = [...popups.list()].reverse();\n\n\t\t// run through the list of popups\n\t\tfor (let i = 0; i < popups_list.length; i++) {\n\t\t\t// if this popup is shown, we make it the current `div`\n\t\t\tif (popups_list[i].classList.contains(\"shown\")) {\n\t\t\t\tdiv = popups_list[i];\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\t// get buttons inside `#winbtns`\n\t\tels = [...document.body.querySelectorAll(\"#winbtns [onclick]\")];\n\t} if (! div) { // default\n\t\tdiv = document.body;\n\t}\n\n\t// this gets the list of all the elements we should be able to\n\t// select inside `div`, on top of anything that's already in `els`\n\tels = [...els, ...div.querySelectorAll([\n\t\t\"a\",\n\t\t\"input\",\n\t\t\"button\",\n\t\t\"select\",\n\t\t\"textarea\",\n\t\t\"[onclick]\",\n\t\t\".scroll-selection\"\n\t])]\n\n\t// this'll contain a filtered list of `els`\n\tlet new_els = [];\n\n\t// filter out elements we don't care about\n\tfilter: for (let i = 0; i < els.length; i++) {\n\t\t// elements that match on `els.closest()` with any of these will\n\t\t// be stripped away, as we dont want them\n\t\tlet ignore_closest = [\n\t\t\t\"#overlay\",\n\t\t\t\".no-navigate\",\n\t\t\t\"button.visual\",\n\t\t\t\".scroll-selection\",\n\t\t\t\".popup:not(.shown)\"\n\t\t]\n\n\t\t// ignore, even if `.closest()` matches, if its just matching on\n\t\t// itself instead of a different element\n\t\tlet ignore_closest_self = [\n\t\t\t\".scroll-selection\"\n\t\t]\n\n\t\t// check if `els[i].closest()` matches on any of the elements\n\t\t// inside of `ignore_closest`\n\t\tfor (let ii = 0; ii < ignore_closest.length; ii++) {\n\t\t\tlet closest = els[i].closest(ignore_closest[ii]);\n\n\t\t\t// check if `.closest()` matches, but not on itself\n\t\t\tif (closest) {\n\t\t\t\t// ignore if `closest` is just `els[i]` and the selector\n\t\t\t\t// is inside `ignore_closest_self`\n\t\t\t\tif (closest == els[i] &&\n\t\t\t\t\tignore_closest_self.includes(ignore_closest[ii])) {\n\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// it matches\n\t\t\t\tcontinue filter;\n\t\t\t}\n\t\t}\n\n\t\t// make sure `els[i]` is visible on screen\n\t\tlet visible = els[i].checkVisibility({\n\t\t\tcheckOpacity: true,\n\t\t\tvisibilityProperty: true,\n\t\t\tcheckVisibilityCSS: true,\n\t\t\tcontentVisibilityAuto: true\n\t\t})\n\n\t\t// filter out if not visible\n\t\tif (! visible) {continue}\n\n\t\t// add to filtered list\n\t\tnew_els.push(els[i])\n\t}\n\n\t// return the filtered list of elements\n\treturn new_els;\n}\n\n// attempts to select the currently default selection, if inside a popup\n// we'll look for a `.default-selection`, if it doesn't exist we'll\n// simply use the first selectable element in it\n//\n// if not inside a popup we'll just use the currently selected tab in\n// the `.gamesContainer` sidebar\nnavigate.default_selection = () => {\n\t// if we're not currently using any navigation functions, this\n\t// function shouldn't do anything, as it'll cause a selection to be\n\t// made, when it shouldn't be\n\tif (! navigate.using) {\n\t\treturn;\n\t}\n\n\t// the spread operator is to convert from a `NodeList` to an\n\t// `Array`, and then we need to reverse this to get the ones\n\t// that are layered on top first.\n\tlet popups_list = [...popups.list()].reverse();\n\n\tlet active_popup;\n\n\t// run through the list of popups\n\tfor (let i = 0; i < popups_list.length; i++) {\n\t\t// if this popup is shown, set set `active_popup` to it\n\t\tif (popups_list[i].classList.contains(\"shown\")) {\n\t\t\tactive_popup = popups_list[i];\n\t\t\tbreak;\n\t\t}\n\t}\n\n\t// is there no active popup?\n\tif (! active_popup) {\n\t\t// select the currently selected page in `.gamesContainer`\n\t\tdocument.querySelector(\n\t\t\t\".gamesContainer :not(.inactive)\"\n\t\t).classList.add(\"active-selection\");\n\n\t\t// update the `#selection` element\n\t\tnavigate.selection();\n\n\t\treturn;\n\t}\n\n\t// get the default element inside the active popup\n\tlet popup_default = active_popup.querySelector(\"default-selection\");\n\n\t// did we not find a default selection element?\n\tif (! popup_default) {\n\t\t// select the first selectable element in the popup\n\t\tnavigate.get_els(active_popup)[0].classList.add(\n\t\t\t\"active-selection\"\n\t\t)\n\n\t\t// update the `#selection` element\n\t\tnavigate.selection();\n\n\t\treturn;\n\t}\n\n\t// select the default selection\n\tpopup_default.classList.add(\"active-selection\");\n\n\t// update the `#selection` element\n\tnavigate.selection();\n}\n\n// this navigates `#selection` in the direction of `direction`\n// this can be: up, down, left and right\nnavigate.move = async (direction) => {\n\t// make sure we note down that we're using navigation functions\n\tnavigate.using = true;\n\n\t// get the `.active-selection` if there is one\n\tlet active = document.querySelector(\".active-selection\");\n\n\t// if there is no active selection, then attempt to select the\n\t// default selection\n\tif (! active) {\n\t\tnavigate.default_selection()\n\n\t\tactive = document.querySelector(\".active-selection\");\n\n\t\t// if there is somehow still no active selection we stop here\n\t\tif (! active) {\n\t\t\treturn;\n\t\t}\n\t}\n\n\t// is the active selection one that should be scrollable?\n\tif (active.classList.contains(\"scroll-selection\")) {\n\t\t// scroll the respective `direction` if `active` has any more\n\t\t// scroll left in that direction\n\n\t\t// short hand to easily scroll in `direction` by `amount` with\n\t\t// smooth scrolling enabled\n\t\tlet scroll = (direction, amount) => {\n\t\t\t// update the `#selection` element\n\t\t\tnavigate.selection();\n\n\t\t\t// scroll inside `<webview>` if the active selection is one\n\t\t\tif (active.tagName == \"WEBVIEW\") {\n\t\t\t\tactive.executeJavaScript(`\n\t\t\t\t\tdocument.scrollingElement.scrollBy({\n\t\t\t\t\t\tbehavior: \"smooth\",\n\t\t\t\t\t\t${direction}: ${amount}\n\t\t\t\t\t})\n\t\t\t\t`)\n\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tactive.scrollBy({\n\t\t\t\tbehavior: \"smooth\",\n\t\t\t\t[direction]: amount\n\t\t\t})\n\t\t}\n\n\t\t// get values needed for determining if we should scroll the\n\t\t// active selection, and by how much\n\t\tlet scroll_el = {\n\t\t\ttop: active.scrollTop,\n\t\t\tleft: active.scrollLeft,\n\t\t\twidth: active.scrollWidth,\n\t\t\theight: active.scrollHeight,\n\t\t\tbounds: {\n\t\t\t\twidth: active.clientWidth,\n\t\t\t\theight: active.clientWidth\n\t\t\t}\n\t\t}\n\n\t\t// get `scroll_el` from inside a `<webview>` if the active\n\t\t// selection is one\n\t\tif (active.tagName == \"WEBVIEW\") {\n\t\t\tscroll_el = await active.executeJavaScript(`(() => {\n\t\t\t\treturn {\n\t\t\t\t\ttop: document.scrollingElement.scrollTop,\n\t\t\t\t\tleft: document.scrollingElement.scrollLeft,\n\t\t\t\t\twidth: document.scrollingElement.scrollWidth,\n\t\t\t\t\theight: document.scrollingElement.scrollHeight,\n\t\t\t\t\tbounds: {\n\t\t\t\t\t\twidth: document.scrollingElement.clientWidth,\n\t\t\t\t\t\theight: document.scrollingElement.clientHeight\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t})()`)\n\t\t}\n\n\t\t// decrease to increase scroll length, and in reverse\n\t\tlet scroll_scale = 2;\n\n\t\tif (direction == \"up\" && scroll_el.top > 0) {\n\t\t\treturn scroll(\"top\", -scroll_el.bounds.height / scroll_scale);\n\t\t}\n\n\t\tif (direction == \"down\" &&\n\t\t\tscroll_el.top <= scroll_el.height &&\n\t\t\tscroll_el.height != scroll_el.bounds.height) {\n\n\t\t\treturn scroll(\"top\", scroll_el.bounds.height / scroll_scale);\n\t\t}\n\n\t\tif (direction == \"left\" && scroll_el.left > 0) {\n\t\t\treturn scroll(\"left\", -width / scroll_scale);\n\t\t}\n\n\t\tif (direction == \"right\" &&\n\t\t\tscroll_el.left <= scroll_el.width &&\n\t\t\tscroll_el.width != scroll_el.bounds.width) {\n\n\t\t\treturn scroll(\"left\", scroll_el.bounds.width / scroll_scale);\n\t\t}\n\t}\n\n\t// attempt to get the element in the `direction` requested\n\tlet move_to_el = navigate.get_relative_el(active, direction);\n\n\t// if no element is found, do nothing\n\tif (! move_to_el) {return}\n\n\t// switch `.active-selection` from `active` to `move_to_el`\n\tactive.classList.remove(\"active-selection\");\n\tmove_to_el.classList.add(\"active-selection\");\n\n\t// update the `#selection` element\n\tnavigate.selection();\n\n\t// make sure the selecting classes are removed, and thereby the\n\t// scale/pressed effect with it\n\tdocument.getElementById(\"selection\").classList.remove(\n\t\t\"keyboard-selecting\",\n\t\t\"controller-selecting\"\n\t)\n\n\t// stop here if `move_to_el` is a child to `document.body`\n\tif (move_to_el.parentElement == document.body) {\n\t\treturn;\n\t}\n\n\t// this element will be scrolled in view later\n\tlet scroll_el = move_to_el;\n\n\t// these elements cant be scrolled\n\tlet no_scroll_parents = [\n\t\t\".el .text\",\n\t\t\".gamesContainer\",\n\t]\n\n\t// run through unscrollable parent elements\n\tfor (let i = 0; i < no_scroll_parents.length; i++) {\n\t\t// check if `move_to_el.closest()` matches on anything in\n\t\t// `no_scroll_parents`\n\t\tlet no_scroll_parent = move_to_el.closest(\n\t\t\tno_scroll_parents[i]\n\t\t)\n\n\t\tif (! no_scroll_parent) {\n\t\t\t// it does not match\n\t\t\tcontinue;\n\t\t}\n\n\t\t// it matches, so we make the new `scroll_el` the parent\n\t\tscroll_el = no_scroll_parent;\n\t}\n\n\t// refuse to scroll to begin with, if any of these are a parent\n\tlet ignore_parents = [\n\t\t\".contentMenu\",\n\t\t\".gamesContainer\",\n\t]\n\n\t// check if `ignore_parents` match on `move_to_el`, and if so, stop\n\tfor (let i = 0; i < ignore_parents.length; i++) {\n\t\tif (move_to_el.closest(ignore_parents[i])) {\n\t\t\treturn;\n\t\t}\n\t}\n\n\tif (scroll_el.closest(\".grid .el\")) {\n\t\tscroll_el = scroll_el.closest(\".grid .el\");\n\t}\n\n\t// scroll `scroll_el` smoothly into view, centered\n\tscroll_el.scrollIntoView({\n\t\tblock: \"center\",\n\t\tinline: \"center\",\n\t\tbehavior: \"smooth\",\n\t})\n}\n\n// selects the currently selected element, by clicking or focusing it\nnavigate.select = () => {\n\t// make sure we note down that we're using navigation functions\n\tnavigate.using = true;\n\n\t// get the current selection\n\tlet active = document.querySelector(\".active-selection\");\n\n\t// make sure there is a selection\n\tif (! active) {return}\n\n\t// slight delay to prevent some timing issues\n\tsetTimeout(() => {\n\t\t// if `active` is a switch, use `settings.popup.switch()` on it,\n\t\t// to be able to toggle it\n\t\tif (active.closest(\".switch\")) {\n\t\t\tactive.closest(\".switch\").click();\n\t\t\tsettings.popup.switch(active.closest(\".switch\"));\n\t\t\treturn;\n\t\t}\n\n\t\t// correctly open a `<select>`\n\t\t//\n\t\t// they require special handling, as unless the Enter key is\n\t\t// pressed, then the menu won't open, and instead the element is\n\t\t// focused, which isn't great\n\t\t//\n\t\t// so we make the main process send a fake Enter key press\n\t\tif (active.closest(\"select\") || active.querySelector(\"select\")) {\n\t\t\t// make sure this element doesn't get unselected\n\t\t\tnavigate.dont_unselect = active;\n\n\t\t\tactive = active.closest(\"select\") || active.querySelector(\"select\");\n\n\t\t\t// make sure `<select>` is focused\n\t\t\tactive.focus();\n\t\t\tactive.click();\n\n\t\t\t// send fake Enter key to open selection menu\n\t\t\tipcRenderer.send(\"send-enter-key\");\n\n\t\t\t// make sure element is unselected\n\t\t\tactive.addEventListener(\"change\", () => {\n\t\t\t\tactive.blur();\n\t\t\t}, { once: true })\n\n\t\t\treturn;\n\t\t}\n\n\t\t// click and focus `active`\n\t\tactive.focus();\n\t\tactive.click();\n\t}, 150)\n}\n\n// selects the closest and hopefully most correct element to select next\n// to `relative_el` in the direction of `direction`\n//\n// the direction can be: up, down, left and right\nnavigate.get_relative_el = (relative_el, direction) => {\n\t// get selectable elements\n\tlet els = navigate.get_els();\n\n\t// get bounds of `relative_el`\n\tlet bounds = relative_el.getBoundingClientRect();\n\n\t// get the centered coordinates of `relative_el`\n\tlet relative = {\n\t\tx: bounds.left + (bounds.width / 2),\n\t\ty: bounds.top + (bounds.height / 2)\n\t}\n\n\t// update the coordinates on the element itself\n\trelative_el.coords = relative;\n\n\t// attempt to return the element in the correct direction\n\t// if `x` or `y` is a number that's greater or less than 0 then\n\t// we'll go in the direction of the coordinate that is as such\n\t//\n\t// meaning `get_el(1, 0)` will go to the right\n\tlet get_el = (x = 0, y = 0) => {\n\t\t// `coord` is the coordinate that we're trying to get an element\n\t\t// on, and `rev_coord` is just the opposite coord\n\t\tlet coord, rev_coord;\n\n\t\t// set `coord` and `rev_coord` according to `x` and `y`\n\t\tif (x > 0 || x < 0) {\n\t\t\tcoord = \"x\";\n\t\t\trev_coord = \"y\";\n\t\t} else if (y > 0 || y < 0) {\n\t\t\tcoord = \"y\";\n\t\t\trev_coord = \"x\";\n\t\t} else { // something unexpected was given\n\t\t\treturn false;\n\t\t}\n\n\t\t// this is the distance between each point which we check for\n\t\t// selectable elements, increasing this improves performance,\n\t\t// but lowers accuracy, and likewise in reverse\n\t\tlet jump_distance = 5;\n\n\t\t// this is the coordinates to check in the direct coord check\n\t\tlet check = {\n\t\t\tx: relative.x,\n\t\t\ty: relative.y\n\t\t}\n\n\t\t// this will contain the element that directly next to\n\t\t// `relative_el` from checking every point from `relative_el`\n\t\t// into `direction`\n\t\tlet direct_el;\n\n\t\t// this is the amount of pixels inbetween `relative_el` and\n\t\t// `direct_el`, this means it doesn't have the distance from the\n\t\t// center of `direct_el` or anything included, just the raw\n\t\t// distance between them, this number could vary in accuracy\n\t\t// depending on how big or small `jump_distance` is\n\t\tlet direct_distance = 0;\n\n\t\t// attempt to find an element from a straight line from\n\t\t// `relative_el`, by checking whether there's an element at each\n\t\t// point in the `direction` specified\n\t\twhile (! direct_el) {\n\t\t\t// add `jump_distance` to the coordinates we're checking\n\t\t\tcheck.x += x * jump_distance;\n\t\t\tcheck.y += y * jump_distance;\n\n\t\t\t// get the elements at the coordinates we're checking\n\t\t\tlet els_at = document.elementsFromPoint(check.x, check.y);\n\n\t\t\t// run through all the elements we found\n\t\t\tfor (let i = 0; i < els_at.length; i++) {\n\t\t\t\t// make sure `els_at[i]` isn't `relative_el` and that\n\t\t\t\t// its a selectable element\n\t\t\t\tif (els_at[i] == relative_el\n\t\t\t\t\t|| ! els.includes(els_at[i])) {\n\n\t\t\t\t\t// not selectable or is `relative_el`\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// set `direct_el`\n\t\t\t\tdirect_el = els_at[i];\n\n\t\t\t\t// get the bounds of `direct_el`\n\t\t\t\tlet direct_bounds = direct_el.getBoundingClientRect();\n\n\t\t\t\t// get the centered coordinates for `direct_el`\n\t\t\t\tlet direct_coords = {\n\t\t\t\t\tx: direct_bounds.left + (direct_bounds.width / 2),\n\t\t\t\t\ty: direct_bounds.top + (direct_bounds.height / 2)\n\t\t\t\t}\n\n\t\t\t\t// update the coordinates on the element itself\n\t\t\t\tdirect_el.coords = direct_coords;\n\n\t\t\t\t// get the difference between `relative_el` and\n\t\t\t\t// `direct_el`'s coordinates, effectively their distance\n\t\t\t\tlet diff_x = direct_coords.x - relative.x;\n\t\t\t\tlet diff_y = direct_coords.y - relative.y;\n\n\t\t\t\t// make sure this element is marked as the element that\n\t\t\t\t// was found directly\n\t\t\t\tdirect_el.is_direct_el = true;\n\n\t\t\t\t// update the distance on the element itself\n\t\t\t\tdirect_el.distance = Math.sqrt(\n\t\t\t\t\tdiff_x*diff_x + diff_y*diff_y\n\t\t\t\t)\n\n\t\t\t\t// set the distance on the coord we're checking on the\n\t\t\t\t// element itself\n\t\t\t\tdirect_el.coord_distance = direct_distance;\n\n\t\t\t\tbreak; // we found the `direct_el` we can stop now\n\t\t\t}\n\n\t\t\t// if `els_at` has `relative_el` then we reset\n\t\t\t// `direct_distance`\n\t\t\tif (els_at.includes(relative_el)) {\n\t\t\t\tdirect_distance = 0;\n\t\t\t} else {\n\t\t\t\t// add `jump_distance` to `direct_distance`, because\n\t\t\t\t// we're no longer on the `relative_el` nor the\n\t\t\t\t// `direct_el`\n\t\t\t\tdirect_distance += jump_distance;\n\t\t\t}\n\n\t\t\t// are we beyond the edges of the window\n\t\t\tif (check.x < 0 || check.y < 0 ||\n\t\t\t\tcheck.x > innerWidth || check.y > innerHeight) {\n\n\t\t\t\t// did we find no elements?\n\t\t\t\tif (! els_at.length) {\n\t\t\t\t\tbreak; // stop searching\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// this contains elements in the respective directions\n\t\tlet positions = {\n\t\t\tup: [], down: [],\n\t\t\tleft: [], right: []\n\t\t}\n\n\t\t// gets the nearest elements from the selectable elements\n\t\tfor (let i = 0; i < els.length; i++) {\n\t\t\t// get bounds\n\t\t\tlet el_bounds = els[i].getBoundingClientRect();\n\n\t\t\t// get centered coordinates\n\t\t\tlet el_coords = {\n\t\t\t\tx: el_bounds.left + (el_bounds.width / 2),\n\t\t\t\ty: el_bounds.top + (el_bounds.height / 2)\n\t\t\t}\n\n\t\t\t// get the difference between `el_coords` and `direct_el`'s\n\t\t\t// coordinates, effectively their distance\n\t\t\tlet diff_x = el_coords.x - relative.x;\n\t\t\tlet diff_y = el_coords.y - relative.y;\n\n\t\t\t// is this element not an element that was previously a\n\t\t\t// `direct_el`?\n\t\t\tif (! els[i].is_direct_el) {\n\t\t\t\t// update centerd coordinates on the element itself\n\t\t\t\tels[i].coords = el_coords;\n\n\t\t\t\t// set the distance on the element itself\n\t\t\t\tels[i].distance = Math.sqrt(\n\t\t\t\t\tdiff_x*diff_x + diff_y*diff_y\n\t\t\t\t)\n\n\t\t\t\t// get and set the distance on the coord we're checking\n\t\t\t\t// on the element itself\n\t\t\t\tels[i].coord_distance = Math.abs(\n\t\t\t\t\trelative[coord] - el_coords[coord]\n\t\t\t\t)\n\t\t\t} else {\n\t\t\t\tels[i].is_direct_el = false;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// put `els[i]` in the correct place in `positions`\n\t\t\tif (el_coords.x < relative.x) {\n\t\t\t\tpositions.left.push(els[i]);\n\t\t\t} if (el_coords.x > relative.x) {\n\t\t\t\tpositions.right.push(els[i]);\n\t\t\t} if (el_coords.y < relative.y) {\n\t\t\t\tpositions.up.push(els[i]);\n\t\t\t} if (el_coords.y > relative.y) {\n\t\t\t\tpositions.down.push(els[i]);\n\t\t\t}\n\t\t}\n\n\t\t// this will contain the element closest to `relative_el` in the\n\t\t// correct direction, but not necessarily at the same\n\t\t// coordinates\n\t\tlet closest_el;\n\n\t\t// set `closest_el` to the elements closest element in the\n\t\t// respective position using `direction`\n\t\tfor (let i = 0; i < positions[direction].length; i++) {\n\t\t\t// get the element\n\t\t\tlet el = positions[direction][i];\n\n\t\t\t// is this the first check, or is it closer than the\n\t\t\t// previous `closest_el`, then update `closest_el`\n\t\t\tif (! closest_el || closest_el.distance > el.distance) {\n\t\t\t\tclosest_el = el;\n\t\t\t}\n\t\t}\n\n\t\t// was there found a `closest_el` and `direct_el`\n\t\tif (closest_el && direct_el) {\n\t\t\t// simply return `direct_el` if its the same as `closest_el`\n\t\t\tif (closest_el == direct_el) {\n\t\t\t\treturn direct_el;\n\t\t\t}\n\n\t\t\t// if the parent element of `closest_el` and `direct_el` is\n\t\t\t// the same, then we prefer the `direct_el`\n\t\t\t//\n\t\t\t// unless the parent is `document.body`\n\t\t\tif (closest_el.parentElement == direct_el.parentElement &&\n\t\t\t\tdirect_el.parentElement !== document.body) {\n\t\t\t\treturn direct_el;\n\t\t\t}\n\n\t\t\t// get the difference between `relative_el` and `direct_el`\n\t\t\t// on the coordinate of `direction`\n\t\t\tlet same_coord_diff = Math.abs(\n\t\t\t\tdirect_el.coords[rev_coord] -\n\t\t\t\trelative_el.coords[rev_coord]\n\t\t\t)\n\n\t\t\t// if the difference is less than 3 then we just return the\n\t\t\t// `direct_el` as its only a couple pixels off being on the\n\t\t\t// same coordinate as `relative_el`\n\t\t\tif (same_coord_diff < 3) {\n\t\t\t\treturn direct_el;\n\t\t\t}\n\n\t\t\t// get the difference is distance on `direct_el` and\n\t\t\t// `closest_el`\n\t\t\tlet difference = Math.abs(\n\t\t\t\tdirect_el.distance - closest_el.distance\n\t\t\t)\n\n\t\t\t// is the distance les than 50?\n\t\t\tif (difference < 50) {\n\t\t\t\t// get the difference between `direct_el` and\n\t\t\t\t// `relative_el`\n\t\t\t\tlet direct_diff = Math.abs(\n\t\t\t\t\tdirect_el.coords[rev_coord] -\n\t\t\t\t\trelative_el.coords[rev_coord]\n\t\t\t\t)\n\n\t\t\t\t// get the difference between `closest_el` and\n\t\t\t\t// `relative_el`\n\t\t\t\tlet closest_diff = Math.abs(\n\t\t\t\t\tclosest_el.coords[rev_coord] -\n\t\t\t\t\trelative_el.coords[rev_coord]\n\t\t\t\t)\n\n\t\t\t\t// if the `direct_el` is closer to `relative_el`, return\n\t\t\t\t// that, otherwise return `closet_el`\n\t\t\t\tif (direct_diff < closest_diff) {\n\t\t\t\t\treturn direct_el;\n\t\t\t\t} else if (closest_diff < direct_diff) {\n\t\t\t\t\treturn closest_el;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// is `direct_el` closer than `closest_el` in either\n\t\t\t// `.coord_distance` or `.distance`\n\t\t\t//\n\t\t\t// if not just return `closest_el`\n\t\t\tif (direct_el.coord_distance <= closest_el.coord_distance ||\n\t\t\t\tdirect_el.distance <= closest_el.distance) {\n\n\t\t\t\t// if `direct_el` is closer in `.coord_distance` then\n\t\t\t\t// return `direct_el`\n\t\t\t\tif (direct_el.coord_distance <=\n\t\t\t\t\tclosest_el.coord_distance) {\n\n\t\t\t\t\treturn direct_el;\n\t\t\t\t}\n\n\t\t\t\t// if the difference in `.distance` is less than 50,\n\t\t\t\t// then return `direct_el`\n\t\t\t\tif (difference < 50) {\n\t\t\t\t\treturn direct_el;\n\t\t\t\t}\n\n\t\t\t\t// if the `.distance` is overall closer on `direct_el`\n\t\t\t\t// than `closest_el` then we return `direct_el`,\n\t\t\t\t// otherwise we return `closest_el`\n\t\t\t\tif (direct_el.distance < closest_el.distance) {\n\t\t\t\t\treturn direct_el;\n\t\t\t\t} else {\n\t\t\t\t\treturn closest_el;\n\t\t\t\t}\n\t\t\t} else { // `direct_el` is unarguably too far away\n\t\t\t\treturn closest_el;\n\t\t\t}\n\t\t} else if (! direct_el && ! closest_el) {\n\t\t\t// do nothing if no element at all was found\n\t\t\treturn false;\n\t\t}\n\n\t\t// return whichever element we did find\n\t\treturn direct_el || closest_el;\n\t}\n\n\t// translate `direction` into `get_el()` args\n\tswitch(direction) {\n\t\tcase \"up\": return get_el(0, -1);\n\t\tcase \"down\": return get_el(0, 1);\n\t\tcase \"left\": return get_el(-1, 0);\n\t\tcase \"right\": return get_el(1, 0);\n\t}\n}\n\n// contains a list of the last selections we had before a popup was\n// opened, letting us go back to those selections when they're closed\nlet last_popup_selections = [];\n\n// attempt to reselect the default selection when a popup is either\n// closed or opened\nevents.on(\"popup-changed\", (e) => {\n\t// get the active selection\n\tlet active_el = document.querySelector(\".active-selection\");\n\n\t// make sure there is a selection\n\tif (! active_el) {\n\t\treturn;\n\t}\n\n\t// ignore if `active_el` is a `<select>`\n\tif (active_el.closest(\"select\")) {\n\t\treturn;\n\t}\n\n\t// add `active_el` to `last_popup_selections` if we opened a popup\n\tif (e.new_state) {\n\t\tlast_popup_selections.push({\n\t\t\tel: active_el,\n\t\t\tpopup: e.popup\n\t\t})\n\t} else { // we're closing a popup\n\t\t// this may contain the element we had opened before the popup\n\t\t// we're closing was opened\n\t\tlet last_selection;\n\n\t\t// remove selections that are for this popup\n\t\tlast_popup_selections = last_popup_selections.filter((item) => {\n\t\t\t// is this selection for this popup?\n\t\t\tlet is_popup = item.popup == e.popup;\n\n\t\t\t// set `last_selection` to `.el` if its this popup we're\n\t\t\t// closing, thereby getting the last selection made before\n\t\t\t// we opened this popup\n\t\t\tif (is_popup) {\n\t\t\t\tlast_selection = item.el;\n\t\t\t}\n\n\t\t\treturn ! is_popup;\n\t\t})\n\n\t\t// select `last_selection` if one was found\n\t\tif (last_selection) {\n\t\t\tsetTimeout(() => {\n\t\t\t\tnavigate.selection(last_selection);\n\t\t\t}, 150) // needed due to popup animation\n\n\t\t\treturn;\n\t\t}\n\t}\n\n\t// remove the currently active selection\n\tactive_el.classList.remove(\"active-selection\");\n\n\t// update the `#selection` element\n\tnavigate.selection();\n\n\t// wait a moment to allow the popup to open or close completely\n\tsetTimeout(() => {\n\t\t// select the default selection\n\t\tnavigate.default_selection();\n\t}, 300)\n})\n\n// automatically deselect a selection if its no longer visible\nsetInterval(() => {\n\t// get the active selection\n\tlet active_el = document.querySelector(\".active-selection\");\n\n\tif (! active_el) {return}\n\n\tlet visible = active_el.checkVisibility({\n\t\tcheckOpacity: true,\n\t\tvisibilityProperty: true,\n\t\tcheckVisibilityCSS: true,\n\t\tcontentVisibilityAuto: true\n\t})\n\n\tif (! visible) {\n\t\tnavigate.default_selection();\n\t}\n}, 500)\n\nmodule.exports = navigate;\n"
  },
  {
    "path": "src/app/js/popups.js",
    "content": "let popups = {};\n\npopups.set = (popup, state, auto_close_all = true) => {\n\tlet popup_el = popup;\n\n\tif (typeof popup == \"string\") {\n\t\tpopup_el = document.querySelector(popup);\n\t}\n\n\tif (! popup_el) {return false}\n\n\tif (auto_close_all && overlay.classList.contains(\"shown\")) {\n\t\tpopups.set_all(false, popup_el);\n\t}\n\n\tif (! state && state !== false) {\n\t\tstate = ! open_list.includes(popup_el);\n\t}\n\n\tif (state) {\n\t\tpopups.open_list.add(popup_el);\n\t\toverlay.classList.add(\"shown\");\n\t\tpopup_el.classList.add(\"shown\");\n\t} else if (! state) {\n\t\tpopups.open_list.remove(popup_el);\n\t\tpopup_el.classList.remove(\"shown\");\n\t\tif (! open_list.length) {\n\t\t\toverlay.classList.remove(\"shown\");\n\t\t}\n\t}\n\n\tevents.emit(\"popup-changed\", {\n\t\tpopup: popup_el,\n\t\tnew_state: state\n\t})\n}\n\npopups.show = (popup, auto_close_all = true) => {\n\treturn popups.set(popup, true, auto_close_all);\n}\n\npopups.hide = (popup, auto_close_all = true) => {\n\treturn popups.set(popup, false, auto_close_all);\n}\n\npopups.list = () => {\n\treturn document.querySelectorAll(\".popup\");\n}\n\npopups.set_all = (state = false, exclude_popup) => {\n\tlet popups_list = document.querySelectorAll(\".popup.shown\");\n\n\tfor (let i = 0; i < popups_list.length; i++) {\n\t\tif (popups_list[i] == exclude_popup) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tpopups.set(popups_list[i], state, false);\n\t}\n}\n\n// attempts to hide just the last shown popup\npopups.hide_last = () => {\n\tif (open_list.length) {\n\t\tpopups.hide(open_list[open_list.length - 1], false);\n\t}\n}\n\nlet open_list = [];\npopups.open_list = () => {\n\treturn open_list;\n}\n\npopups.open_list.remove = (el) => {\n\t// no need to do anything if `el` isn't even in `open_list`\n\tif (! open_list.includes(el)) {\n\t\treturn;\n\t}\n\n\t// filtered list\n\tlet list = [];\n\n\t// run through open popups\n\tfor (let i = 0; i < open_list.length; i++) {\n\t\t// add popup to `list` if it isn't `el`\n\t\tif (open_list[i] != el && el.classList.contains(\"shown\")) {\n\t\t\tlist.push(open_list[i]);\n\t\t}\n\t}\n\n\t// set `open_list` to the now filtered `list`\n\topen_list = list;\n}\n\npopups.open_list.add = (el) => {\n\t// make sure the `el` isn't already in the list\n\tpopups.open_list.remove(el);\n\n\t// add `el` to the end of the list\n\topen_list.push(el);\n}\n\nmodule.exports = popups;\n"
  },
  {
    "path": "src/app/js/process.js",
    "content": "const ipcRenderer = require(\"electron\").ipcRenderer;\n\nipcRenderer.on(\"log\", (_, msg) => {\n\tconsole.log(msg)\n})\n\nipcRenderer.on(\"alert\", (_, data) => {\n\talert(data.message);\n\tipcRenderer.send(\"alert-closed-\" + data.id);\n})\n\nipcRenderer.on(\"confirm\", (_, data) => {\n\tlet confirmed = confirm(data.message);\n\tipcRenderer.send(\"confirm-closed-\" + data.id, confirmed);\n})\n\nmodule.exports = {\n\t// attempts to relaunch the process\n\trelaunch: () => {\n\t\tipcRenderer.send(\"relaunch\");\n\t},\n\n\t// attempts to exit the process (closing Viper)\n\texit: () => {\n\t\tipcRenderer.send(\"exit\")\n\t}\n}\n"
  },
  {
    "path": "src/app/js/request.js",
    "content": "const lang = require(\"../../lang\");\nconst toasts = require(\"./toasts\");\nconst launcher = require(\"./launcher\");\nconst set_buttons = require(\"./set_buttons\");\nconst ipcRenderer = require(\"electron\").ipcRenderer;\n\n// invokes `requests.get()` from `src/modules/requests.js` through the\n// main process, and returns the output\nlet request = async (...args) => {\n\treturn await ipcRenderer.invoke(\"request\", ...args);\n}\n\n// invokes `requests.check()` from `src/modules/requests.js` through the\n// main process, and returns the output\nrequest.check = async (...args) => {\n\treturn await ipcRenderer.invoke(\"request-check\", ...args);\n}\n\nrequest.delete_cache = () => {\n\tipcRenderer.send(\"delete-request-cache\");\n}\n\n// does `request.check(...args)` and shows toast if the check failed,\n// using `name` inside the toast message\nrequest.check_with_toasts = async (name, ...args) => {\n\t// perform check\n\tlet can_connect = (\n\t\tawait request.check(...args)\n\t).succeeded.length;\n\n\t// show toast, as the check failed\n\tif (! can_connect) {\n\t\ttoasts.show({\n\t\t\ttimeout: 10000,\n\t\t\tscheme: \"error\",\n\t\t\ttitle: lang(\"gui.toast.title.failed_to_connect\"),\n\t\t\tdescription: lang(\"gui.toast.desc.failed_to_connect\").replaceAll(\"%s\", name)\n\t\t})\n\t}\n\n\treturn can_connect;\n}\n\n// keeps track of whether we've already sent a toast since we last went\n// offline, to prevent multiple toasts\nlet sent_error_toast = false;\n\n// shows or hides offline icon, and shows toast depending on `is_online`\nlet state_action = async (is_online) => {\n\tif (is_online) {\n\t\t// hide offline icon\n\t\tsent_error_toast = false;\n\t\toffline.classList.add(\"hidden\");\n\n\t\t// re-enable buttons that require internet\n\t\tset_buttons(\n\t\t\ttrue, false,\n\t\t\tdocument.querySelectorAll(\".requires-internet\")\n\t\t)\n\n\t\tawait launcher.check_servers();\n\t\tserverstatus.style.opacity = \"1.0\";\n\t} else {\n\t\t// show toast\n\t\tif (! sent_error_toast) {\n\t\t\tsent_error_toast = true;\n\t\t\tipcRenderer.send(\"no-internet\");\n\t\t}\n\n\t\t// show offline icon\n\t\toffline.classList.remove(\"hidden\");\n\n\t\t// disable buttons that require internet\n\t\tset_buttons(\n\t\t\tfalse, false,\n\t\t\tdocument.querySelectorAll(\".requires-internet\")\n\t\t)\n\n\t\tserverstatus.style.opacity = \"0.0\";\n\n\t\t// close mod browser\n\t\ttry {\n\t\t\trequire(\"./browser\").toggle(false);\n\t\t} catch(err) {}\n\t}\n}\n\nsetTimeout(() => state_action(navigator.onLine), 100);\nwindow.addEventListener(\"online\", () => state_action(navigator.onLine));\nwindow.addEventListener(\"offline\", () => state_action(navigator.onLine));\n\n// checks a list of endpoints/domains we need to be functioning for a\n// lot of the WAN functionality\nlet check_endpoints = async () => {\n\t// if we're not online according to the navigator, it's highly\n\t// unlikely the endpoints will succeed\n\tif (! navigator.onLine) {\n\t\treturn state_action(false);\n\t}\n\n\t// check endpoints\n\tlet status = await request.check([\n\t\t\"https://github.com\",\n\t\t\"https://northstar.tf\",\n\t\t\"https://thunderstore.io\"\n\t])\n\n\t// handle result of check\n\tstate_action(!! status.succeeded.length);\n}\n\n// check endpoints on startup\nsetTimeout(check_endpoints, 100);\n\n// check endpoints every 30 seconds\nsetInterval(check_endpoints, 30000);\n\nmodule.exports = request;\n"
  },
  {
    "path": "src/app/js/set_buttons.js",
    "content": "const ipcRenderer = require(\"electron\").ipcRenderer;\n\nipcRenderer.on(\"set-buttons\", (_, state) => {\n\tset_buttons(state);\n})\n\n// disables or enables certain buttons when for example\n// updating/installing Northstar.\nmodule.exports = (state, enable_gamepath_btns, elements) => {\n\tif (! elements) {\n\t\tplayNsBtn.disabled = ! state;\n\t}\n\n\tlet disable_array = (array) => {\n\t\tfor (let i = 0; i < array.length; i++) {\n\t\t\tarray[i].disabled = ! state;\n\n\t\t\tif (state) {\n\t\t\t\tarray[i].classList.remove(\"disabled\");\n\t\t\t} else {\n\t\t\t\tarray[i].classList.add(\"disabled\");\n\t\t\t}\n\t\t}\n\t}\n\n\tdisable_array(elements || document.querySelectorAll([\n\t\t\"#modsdiv .el button\",\n\t\t\".disable-when-installing\",\n\t\t\".playBtnContainer .playBtn\",\n\t\t\"#nsMods .buttons.modbtns button\",\n\t\t\"#browser #browserEntries .text button\",\n\t]))\n\n\tif (enable_gamepath_btns) {\n\t\tlet gamepath_btns = query_all('*[onclick=\"gamepath.set()\"]');\n\n\t\tfor (let i = 0; i < gamepath_btns.length; i++) {\n\t\t\tgamepath_btns[i].disabled = false;\n\t\t\tgamepath_btns[i].classList.remove(\"disabled\");\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/app/js/settings.js",
    "content": "const fs = require(\"fs\");\nconst Fuse = require(\"fuse.js\");\nconst ipcRenderer = require(\"electron\").ipcRenderer;\n\nconst lang = require(\"../../lang\");\n\nconst events = require(\"./events\");\nconst popups = require(\"./popups\");\n\nlet settings_fuse;\n\n// base settings, and future set settings data\nlet settings_data = {\n\tnsargs: \"\",\n\tgamepath: \"\",\n\tnsupdate: true,\n\tautolang: true,\n\tforcedlang: \"en\",\n\tautoupdate: true,\n\toriginkill: false,\n\tzip: \"/northstar.zip\",\n\tlang: navigator.language,\n\texcludes: [\n\t\t\"ns_startup_args.txt\",\n\t\t\"ns_startup_args_dedi.txt\"\n\t]\n}\n\n// loads the settings\nif (fs.existsSync(\"viper.json\")) {\n\tlet iteration = 0;\n\n\t// loads the config file, it's loaded in an interval like this in\n\t// case the config file is currently being written to, if we were to\n\t// read from the file during that, then it'd be empty or similar.\n\t//\n\t// and because of that, we check if the file is empty when loading\n\t// it, if so we wait 100ms, then check again, and we keep doing that\n\t// hoping it'll become normal again. unless we've checked it 50\n\t// times, then we simply give up, and force the user to re-select\n\t// the gamepath, as this'll make the config file readable again.\n\t//\n\t// ideally it'll never have to check those 50 times, it's only in\n\t// case something terrible has gone awry, like if a write got\n\t// interrupted, or a user messed with the file.\n\t//\n\t// were it to truly be broken, then it'd take up to 5 seconds to\n\t// then reset, this is basically nothing, especially considering\n\t// this should only happen in very rare cases... hopefully!\n\tlet config_interval = setInterval(() => {\n\t\tlet gamepath = require(\"./gamepath\");\n\n\t\titeration++;\n\n\t\tconfig = json(\"viper.json\") || {};\n\n\t\t// checks whether `settings_data.gamepath` is set, and if so,\n\t\t// it'll attempt to load ns_startup_args.txt\n\t\tlet parse_settings = () => {\n\t\t\t// if gamepath is not set, attempt to set it\n\t\t\tif (settings_data.gamepath.length === 0) {\n\t\t\t\tgamepath.set(false);\n\t\t\t} else {\n\t\t\t\t// if the gamepath is set, we'll simply tell the main\n\t\t\t\t// process about it, and it'll then show the main\n\t\t\t\t// renderer BrowserWindow\n\t\t\t\tgamepath.set(true);\n\t\t\t}\n\n\t\t\t// filepath to Northstar's startup args file\n\t\t\tlet args = path.join(settings_data.gamepath, \"ns_startup_args.txt\");\n\n\t\t\t// check file exists, and that no `nsargs` setting was set\n\t\t\tif (! settings_data.nsargs && fs.existsSync(args)) {\n\t\t\t\t// load arguments from file into `settings`\n\t\t\t\tsettings_data.nsargs = fs.readFileSync(args, \"utf8\") || \"\";\n\t\t\t}\n\t\t}\n\n\t\t// make sure config isn't empty\n\t\tif (Object.keys(config).length !== 0) {\n\t\t\t// add `config` to `settings_data`\n\t\t\tsettings.set(config);\n\t\t\tparse_settings();\n\n\t\t\tclearInterval(config_interval);\n\t\t\treturn;\n\t\t}\n\n\t\t// we've attempted to load the config 50 times now, give up\n\t\tif (iteration >= 50) {\n\t\t\t// request a new gamepath be set\n\t\t\tgamepath.set(false);\n\t\t\tclearInterval(config_interval);\n\t\t}\n\t}, 100)\n} else {\n\trequire(\"./gamepath\").set();\n}\n\nipcRenderer.on(\"changed-settings\", (e, new_settings) => {\n\t// attempt to set `settings_data` to `new_settings`\n\ttry {\n\t\tsettings.set(new_settings);\n\t}catch(e) {}\n})\n\nlet settings = {\n\tdefault: {...settings_data},\n\n\tdata: () => {return settings_data},\n\n\t// asks the main process to reset the config/settings file\n\treset: () => {\n\t\tipcRenderer.send(\"reset-config\");\n\t},\n\n\t// merges `object` with `settings_data`, unless `no_merge` is set,\n\t// then it replaces it entirely\n\tset: (object, no_merge) => {\n\t\tif (no_merge) {\n\t\t\tsettings_data = object;\n\t\t\treturn;\n\t\t}\n\n\t\tsettings_data = {\n\t\t\t...settings_data,\n\t\t\t...object\n\t\t}\n\t},\n\n\tpopup: {}\n}\n\nsettings.popup.toggle = (state) => {\n\tsettings.popup.load();\n\toptions.scrollTo(0, 0);\n\n\tpopups.set(\"#options\", state);\n}\n\nsettings.popup.apply = () => {\n\tsettings.set(settings.popup.get());\n\tipcRenderer.send(\"save-settings\", settings.popup.get());\n}\n\nsettings.popup.get = () => {\n\tlet opts = {};\n\tlet options = document.querySelectorAll(\".option\");\n\n\tfor (let i = 0; i < options.length; i++) {\n\t\tlet optName = options[i].getAttribute(\"name\");\n\t\tif (options[i].querySelector(\".actions input\")) {\n\t\t\tlet input = options[i].querySelector(\".actions input\").value;\n\t\t\tif (options[i].getAttribute(\"type\")) {\n\t\t\t\topts[optName] = input.split(\" \");\n\t\t\t} else {\n\t\t\t\topts[optName] = input;\n\t\t\t}\n\t\t} else if (options[i].querySelector(\".actions select\")) {\n\t\t\topts[optName] = options[i].querySelector(\".actions select\").value;\n\t\t} else if (options[i].querySelector(\".actions .switch\")) {\n\t\t\tif (options[i].querySelector(\".actions .switch.on\")) {\n\t\t\t\topts[optName] = true;\n\t\t\t} else {\n\t\t\t\topts[optName] = false;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn opts;\n}\n\nsettings.popup.load = () => {\n\t// re-opens any closed categories\n\tlet categories = document.querySelectorAll(\"#options details\");\n\tfor (let i = 0; i < categories.length; i++) {\n\t\tcategories[i].setAttribute(\"open\", true);\n\n\t\t// hide categories that aren't for the current platform\n\t\tlet for_platform = categories[i].getAttribute(\"platform\");\n\t\tif (for_platform && process.platform != for_platform) {\n\t\t\tcategories[i].style.display = \"none\";\n\t\t\tcategories[i].setAttribute(\"perma-hidden\", true);\n\t\t}\n\t}\n\n\tlet options = document.querySelectorAll(\".option\");\n\n\tfor (let i = 0; i < options.length; i++) {\n\t\t// hide options that aren't for the current platform\n\t\tlet for_platform = options[i].getAttribute(\"platform\");\n\t\tif (for_platform && process.platform != for_platform) {\n\t\t\toptions[i].style.display = \"none\";\n\t\t\toptions[i].setAttribute(\"perma-hidden\", true);\n\t\t}\n\n\t\tlet optName = options[i].getAttribute(\"name\");\n\t\tif (optName == \"forcedlang\") {\n\t\t\tlet div = options[i].querySelector(\"select\");\n\n\t\t\tdiv.innerHTML = \"\";\n\t\t\tlet lang_dir = __dirname + \"/../../lang\";\n\t\t\tlet langs = fs.readdirSync(lang_dir);\n\t\t\tfor (let i in langs) {\n\t\t\t\tlet lang_file = require(lang_dir + \"/\" + langs[i]);\n\t\t\t\tlet lang_no_extension = langs[i].replace(/\\..*$/, \"\");\n\n\t\t\t\tif (! lang_file.lang || ! lang_file.lang.title) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tlet title = lang_file.lang.title;\n\n\t\t\t\tif (title) {\n\t\t\t\t\tdiv.innerHTML += `<option value=\"${lang_no_extension}\">${title}</option>`\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\n\t\t\tdiv.value = settings_data.forcedlang;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (settings_data[optName] != undefined) {\n\t\t\t// check if setting has a `<select>`\n\t\t\tlet select_el = options[i].querySelector(\".actions select\");\n\t\t\tif (select_el) {\n\t\t\t\t// get `<option>` for settings value, if it exists\n\t\t\t\tlet option = select_el.querySelector(\n\t\t\t\t\t`option[value=\"${settings_data[optName]}\"]`\n\t\t\t\t)\n\n\t\t\t\t// check if it exists\n\t\t\t\tif (option) {\n\t\t\t\t\t// set the `<select>` to the settings value\n\t\t\t\t\tselect_el.value = settings_data[optName];\n\t\t\t\t} else { // use the default value\n\t\t\t\t\tselect_el.value = settings.default[optName];\n\t\t\t\t}\n\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tswitch(typeof settings_data[optName]) {\n\t\t\t\tcase \"string\":\n\t\t\t\t\toptions[i].querySelector(\".actions input\").value = settings_data[optName];\n\t\t\t\t\tbreak\n\t\t\t\tcase \"object\":\n\t\t\t\t\toptions[i].querySelector(\".actions input\").value = settings_data[optName].join(\" \");\n\t\t\t\t\tbreak\n\t\t\t\tcase \"boolean\":\n\t\t\t\t\tlet switchDiv = options[i].querySelector(\".actions .switch\");\n\t\t\t\t\tif (settings_data[optName]) {\n\t\t\t\t\t\tswitchDiv.classList.add(\"on\");\n\t\t\t\t\t\tswitchDiv.classList.remove(\"off\");\n\t\t\t\t\t} else {\n\t\t\t\t\t\tswitchDiv.classList.add(\"off\");\n\t\t\t\t\t\tswitchDiv.classList.remove(\"on\");\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\n\t\t\t}\n\t\t}\n\t}\n\n\tlet selects = document.querySelectorAll(\"#options select\");\n\tfor (let el of selects) {\n\t\tel.addEventListener(\"change\", () => {\n\t\t\tel.blur();\n\t\t})\n\t}\n\n\t// create Fuse based on options from `get_search_arr()`\n\tsettings_fuse = new Fuse(get_search_arr(), {\n\t\tkeys: [\"text\"],\n\t\tthreshold: 0.4,\n\t\tignoreLocation: true\n\t})\n\n\t// reset search\n\tsettings.popup.search();\n\tsearch_el.value = \"\";\n\n\tipcRenderer.send(\"can-autoupdate\");\n\tipcRenderer.on(\"cant-autoupdate\", () => {\n\t\tdocument.querySelector(\".option[name=autoupdate]\")\n\t\t\t.style.display = \"none\";\n\n\t\tdocument.querySelector(\".option[name=autoupdate]\")\n\t\t\t.setAttribute(\"perma-hidden\", true);\n\t})\n}\n\nsettings.popup.switch = (el, state) => {\n\tif (! el) {return}\n\n\t// prevent switches from being switched when disabled\n\tif (el.getAttribute(\"disabled\") != null\n\t\t|| el.classList.contains(\"disabled\")) {\n\n\t\treturn;\n\t}\n\n\tif (state) {\n\t\treturn el.classList.add(\"on\");\n\t} else if (state === false) {\n\t\treturn el.classList.remove(\"on\");\n\t}\n\n\tif (el.classList.contains(\"switch\") && el.tagName == \"BUTTON\") {\n\t\tel.classList.toggle(\"on\");\n\t}\n}\n\n// searches for `query` in the list of options, hides the options\n// that dont match, and shows the one that do, if `query` is falsy,\n// it'll simply reset everything back to being visible\nsettings.popup.search = (query = \"\") => {\n\t// get list of elements that can be hidden\n\tlet search_els = [\n\t\t...document.querySelectorAll(\".options .title\"),\n\t\t...document.querySelectorAll(\".options .option\"),\n\t\t...document.querySelectorAll(\".options .buttons\"),\n\t\t...document.querySelectorAll(\".options .details\"),\n\t]\n\n\t// this sets the visibility of all elements found in\n\t// `search_els` unless the element has the `perma-hidden`\n\t// attribute set\n\tlet set_all = (state) => {\n\t\t// set `state` to the CSS equivalent\n\t\tif (state) {\n\t\t\tstate = null;\n\t\t} else {\n\t\t\tstate = \"none\";\n\t\t}\n\n\t\t// run through elements\n\t\tfor (let i = 0; i < search_els.length; i++) {\n\t\t\t// check that the element shouldn't be perma hidden, and\n\t\t\t// if so, hide it correctly.\n\t\t\tif (search_els[i].hasAttribute(\"perma-hidden\")) {\n\t\t\t\tsearch_els[i].style.display = \"none\";\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// set the visibility\n\t\t\tsearch_els[i].style.display = state;\n\t\t}\n\t}\n\n\t// check if `query` is empty, and reset search if so\n\tif (! query || ! query.trim()) {\n\t\tset_all(true);\n\t} else {\n\t\t// hide everything\n\t\tset_all(false);\n\t}\n\n\t// unhides `el` and relevant elements related to it\n\tlet unhide = (el) => {\n\t\t// list of elements that could be relevant through `el`'s\n\t\t// `.closest()` function\n\t\tlet closest = [\n\t\t\t\".option\",\n\t\t\t\"details\",\n\t\t\t\".buttons\",\n\t\t]\n\n\t\t// run through `closest`\n\t\tfor (let i = 0; i < closest.length; i++) {\n\t\t\t// shorthand\n\t\t\tlet closest_el = el.closest(closest[i]);\n\n\t\t\t// was a relevant element found?\n\t\t\tif (closest_el) {\n\t\t\t\t// is it supposed to always be hidden? do nothing\n\t\t\t\tif (el.hasAttribute(\"perma-hidden\")\n\t\t\t\t\t|| closest_el.hasAttribute(\"perma-hidden\")) {\n\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\t// reset visibility\n\t\t\t\tclosest_el.style.display = null;\n\n\t\t\t\t// are we at a `<details>`?\n\t\t\t\tif (closest[i] == \"details\") {\n\t\t\t\t\t// attempt to get a `.title` inside that\n\t\t\t\t\t// `<details>` element\n\t\t\t\t\tlet title = closest_el.querySelector(\".title\");\n\n\t\t\t\t\t// did we find a title?\n\t\t\t\t\tif (title) {\n\t\t\t\t\t\t// reset visibility of title and also reset\n\t\t\t\t\t\t// open state of `<details>`\n\t\t\t\t\t\ttitle.style.display = null;\n\t\t\t\t\t\tclosest_el.setAttribute(\"open\", false);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// do a Fuse.js search with `query`\n\tlet res = settings_fuse.search(query);\n\n\t// if nothing was found, reset all\n\tif (res.length == 0) {\n\t\treturn set_all(true);\n\t}\n\n\t// run through results and unhide all of them\n\tfor (let i = 0; i < res.length; i++) {\n\t\tunhide(res[i].item.el);\n\t}\n}\n\n// search on key events in search input\nlet search_el = document.body.querySelector(\"#options .search\");\nsearch_el.addEventListener(\"keyup\", () => {\n\tsettings.popup.search(search_el.value);\n})\n\n// returns a Fuse.js compatible Array for searching through the settings\nfunction get_search_arr() {\n\t// list of elements that should be taken into consideration when\n\t// trying to do a search\n\tlet searchables_queries = [\n\t\t\".option .text\",\n\t\t\".buttons .text\",\n\t\t\".option .actions input\",\n\t\t\".option .actions select\",\n\t\t\".buttons .actions button:not(.switch)\",\n\t]\n\n\tlet searchables_els = [];\n\n\t// run through queries\n\tfor (let i = 0; i < searchables_queries.length; i++) {\n\t\t// attempt to get element(s) with that query\n\t\tlet query = document.querySelectorAll(\n\t\t\t\".options \" + searchables_queries[i]\n\t\t)\n\n\t\t// if something was found add it\n\t\tsearchables_els = [\n\t\t\t...query,\n\t\t\t...searchables_els\n\t\t]\n\t}\n\n\tlet searchables = [];\n\n\t// run through the found elements\n\tfor (let i = 0; i < searchables_els.length; i++) {\n\t\tlet el = searchables_els[i];\n\t\tswitch(el.tagName.toLowerCase()) {\n\t\t\t// is this an `<input>`?\n\t\t\tcase \"input\":\n\t\t\t\t// if no value is in the input, do nothing\n\t\t\t\tif (! el.value) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// add to list of searchable elements, using the value\n\t\t\t\t// of the input as the text\n\t\t\t\tsearchables.push({\n\t\t\t\t\tel: el,\n\t\t\t\t\ttext: el.value\n\t\t\t\t})\n\t\t\t\tbreak;\n\n\t\t\t// is this a `<select>`?\n\t\t\tcase \"select\":\n\t\t\t\t// get options in `<select>`\n\t\t\t\tlet options = el.children;\n\n\t\t\t\t// run through options\n\t\t\t\tfor (let ii = 0; ii < options.length; ii++) {\n\t\t\t\t\t// add to list of searchable elements, using the\n\t\t\t\t\t// insides of the option as the text, and the\n\t\t\t\t\t// element being the `<select>` and not the\n\t\t\t\t\t// `<option>`!\n\t\t\t\t\tsearchables.push({\n\t\t\t\t\t\tel: el,\n\t\t\t\t\t\ttext: options[ii].innerText\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t// add to list of searchable elements using the insides\n\t\t\t\t// of the element as the text\n\t\t\t\tsearchables.push({\n\t\t\t\t\tel: el,\n\t\t\t\t\ttext: el.innerText\n\t\t\t\t})\n\t\t}\n\t}\n\n\t// return the actual searchable elements and text\n\treturn searchables;\n}\n\nevents.on(\"popup-changed\", () => {\n\tlet settings_is_shown =\n\t\tdocument.getElementById(\"options\")\n\t\t.classList.contains(\"shown\");\n\n\tlet settings_btn = document.getElementById(\"settings\");\n\n\tif (settings_is_shown) {\n\t\tsettings_btn.classList.add(\"shown\");\n\t} else {\n\t\tsettings_btn.classList.remove(\"shown\");\n\t}\n})\n\ndocument.body.addEventListener(\"click\", (e) => {\n\tlet el = document.elementFromPoint(e.clientX, e.clientY);\n\tsettings.popup.switch(el);\n})\n\nsettings.popup.load();\n\n// sets the lang to the system default\nipcRenderer.send(\"setlang\", settings_data.lang);\n\nmodule.exports = settings;\n"
  },
  {
    "path": "src/app/js/toasts.js",
    "content": "let toasts = {};\n\ntoasts.show = (properties) => {\n\tlet toast = {\n\t\ttimeout: 3000,\n\t\tfg: \"#FFFFFF\",\n\t\tbg: \"var(--selbg)\",\n\t\tcallback: () => {},\n\t\ttitle: \"Untitled Toast\",\n\t\tdescription: \"No description provided for toast\",\n\t\t...properties\n\t}\n\n\tswitch(toast.scheme) {\n\t\tcase \"error\":\n\t\t\ttoast.fg = \"#FFFFFF\";\n\t\t\ttoast.bg = \"rgb(var(--red))\";\n\t\t\tbreak\n\t\tcase \"success\":\n\t\t\ttoast.fg = \"#FFFFFF\";\n\t\t\ttoast.bg = \"#60D394\";\n\t\t\tbreak\n\t\tcase \"warning\":\n\t\t\ttoast.fg = \"#FFFFFF\";\n\t\t\ttoast.bg = \"#FF9B85\";\n\t\t\tbreak\n\t\tcase \"info\":\n\t\t\ttoast.fg = \"#FFFFFF\";\n\t\t\ttoast.bg = \"rgb(var(--blue))\";\n\t\t\tbreak\n\t}\n\n\n\tlet id = Date.now();\n\tif (document.getElementById(id)) {id = id + 1}\n\tlet el = document.createElement(\"div\");\n\n\tel.classList.add(\"toast\");\n\n\tel.style.color = toast.fg;\n\tel.style.background = toast.bg;\n\n\tel.id = id;\n\tel.addEventListener(\"click\", () => {\n\t\ttoasts.dismiss(id);\n\t\ttoast.callback();\n\t})\n\n\tel.innerHTML = `\n\t\t<div class=\"title\">${toast.title}</div>\n\t\t<div class=\"description\">${toast.description}</div>\n\t`\n\n\tif (! toast.title) {\n\t\tel.querySelector(\".title\").remove();\n\t}\n\n\tif (! toast.description) {\n\t\tel.querySelector(\".description\").remove();\n\t}\n\n\tdocument.getElementById(\"toasts\").appendChild(el);\n\n\tsetTimeout(() => {\n\t\ttoasts.dismiss(id);\n\t}, toast.timeout)\n}\n\n// dismissed/closes toasts with `id` as their ID\ntoasts.dismiss = (id) => {\n\tid = document.getElementById(id);\n\n\tif (id) {\n\t\tid.classList.add(\"hidden\");\n\t\tsetTimeout(() => {\n\t\t\tid.remove();\n\t\t}, 500)\n\t}\n}\n\nipcRenderer.on(\"toast\", (_, properties) => {\n\ttoasts.show(properties);\n})\n\nmodule.exports = toasts;\n"
  },
  {
    "path": "src/app/js/tooltip.js",
    "content": "var tooltip = {\n\ttarget: false,\n\tdiv: document.getElementById(\"tooltip\"),\n}\n\ntooltip.show = (target, text, vertical_positioned) => {\n\tif (target == tooltip.target) {\n\t\treturn;\n\t}\n\n\ttooltip.target = target;\n\n\tlet div = tooltip.div;\n\n\tlet padding = parseFloat(\n\t\tgetComputedStyle(div).getPropertyValue(\"padding\")\n\t);\n\n\tlet tooltip_padding = padding / 1.2;\n\n\tlet get_positions = () => {\n\t\tdiv.innerHTML = text;\n\n\t\tlet div_rect = div.getBoundingClientRect();\n\t\tlet target_rect = target.getBoundingClientRect();\n\n\t\tlet x_pos = 0;\n\t\tlet y_pos = 0;\n\n\t\tif (vertical_positioned) {\n\t\t\tx_pos = target_rect.x + (target_rect.width / 2);\n\t\t\tx_pos = x_pos - (div_rect.width / 2);\n\n\t\t\tif (x_pos < padding) {\n\t\t\t\tx_pos = padding;\n\t\t\t} else if (x_pos + div_rect.width > window.innerWidth - padding) {\n\t\t\t\tx_pos = window.innerWidth - div_rect.width - padding;\n\t\t\t}\n\n\t\t\ty_pos = target_rect.y + target_rect.height + tooltip_padding;\n\n\t\t\tif (y_pos + div_rect.height > window.innerHeight) {\n\t\t\t\ty_pos = target_rect.y - div_rect.height - tooltip_padding;\n\t\t\t}\n\t\t} else {\n\t\t\tx_pos = target_rect.x - div_rect.width - tooltip_padding;\n\n\t\t\tif (x_pos < 0) {\n\t\t\t\tx_pos = target_rect.x + target_rect.width + tooltip_padding;\n\t\t\t}\n\n\t\t\tif (x_pos > window.innerWidth - padding) {\n\t\t\t\tx_pos = window.innerWidth - div_rect.width - padding;\n\t\t\t}\n\n\t\t\ty_pos = target_rect.y + (target_rect.height / 2);\n\t\t\ty_pos = y_pos - (div_rect.height / 2);\n\t\t}\n\n\t\treturn {x: x_pos, y: y_pos}\n\t}\n\n\tlet transition_duration = parseFloat(\n\t\tgetComputedStyle(div).getPropertyValue(\"transition-duration\")\n\t) * 1000;\n\n\tif (div.classList.contains(\"visible\")) {\n\t\tdiv.classList.remove(\"visible\");\n\t}\n\n\treturn new Promise((resolve) => {\n\t\tsetTimeout(() => {\n\t\t\tif (tooltip.target != target) {\n\t\t\t\treturn resolve();\n\t\t\t}\n\n\t\t\tlet pos = get_positions();\n\t\t\tdiv.style.top = pos.y + \"px\";\n\t\t\tdiv.style.left = pos.x + \"px\";\n\t\t}, transition_duration)\n\n\t\tsetTimeout(() => {\n\t\t\tif (tooltip.target != target) {\n\t\t\t\treturn resolve();\n\t\t\t}\n\n\t\t\tdiv.classList.add(\"visible\");\n\n\t\t\tresolve();\n\t\t}, transition_duration * 2)\n\t})\n}\n\nlet mouse_y = 0;\nlet mouse_x = 0;\n\nlet tooltip_event = (event) => {\n\tif (event && event.x && event.y) {\n\t\tmouse_y = event.y;\n\t\tmouse_x = event.x;\n\t} else {\n\t\tevent = {\n\t\t\tx: mouse_x,\n\t\t\ty: mouse_y\n\t\t}\n\t}\n\n\tlet at_mouse = document.elementFromPoint(event.x, event.y);\n\n\tif (! at_mouse) {return}\n\n\tlet tooltip_text = at_mouse.getAttribute(\"tooltip\");\n\tif (! tooltip_text) {\n\t\ttooltip.target = false;\n\t\ttooltip.div.classList.remove(\"visible\");\n\n\t\treturn;\n\t}\n\n\tlet position = at_mouse.getAttribute(\"tooltip-position\") || \"vertical\";\n\ttooltip.show(at_mouse, tooltip_text, (position == \"vertical\"));\n}\n\nsetInterval(tooltip_event, 1000);\ndocument.addEventListener(\"click\", tooltip_event);\ndocument.addEventListener(\"mouseup\", tooltip_event);\ndocument.addEventListener(\"mousedown\", tooltip_event);\ndocument.addEventListener(\"mousemove\", tooltip_event);\ndocument.addEventListener(\"mouseenter\", tooltip_event);\ndocument.addEventListener(\"mouseleave\", tooltip_event);\n"
  },
  {
    "path": "src/app/js/update.js",
    "content": "const ipcRenderer = require(\"electron\").ipcRenderer;\n\nconst lang = require(\"../../lang\");\n\n// updates version numbers\nipcRenderer.on(\"version\", (event, versions) => {\n\tvpversion.innerText = versions.vp;\n\tnsversion.innerText = versions.ns;\n\ttf2Version.innerText = versions.tf2;\n\n\tif (versions.ns == \"unknown\") {\n\t\tlet buttons = document.querySelectorAll(\".modbtns button\");\n\n\t\tfor (let i = 0; i < buttons.length; i++) {\n\t\t\tbuttons[i].disabled = true;\n\t\t}\n\n\t\t// since Northstar is not installed, we cannot launch it\n\t\tupdate.ns.should_install = true;\n\t\tplayNsBtn.innerText = lang(\"gui.install\");\n\t}\n}); ipcRenderer.send(\"get-version\");\n\n// when an update is available it'll ask the user about it\nipcRenderer.on(\"update-available\", () => {\n\tif (confirm(lang(\"gui.update.available\"))) {\n\t\tipcRenderer.send(\"update-now\");\n\t}\n})\n\n// frontend part of updating Northstar\nipcRenderer.on(\"ns-update-event\", (event, options) => {\n\tlet key = options.key;\n\tif (typeof options == \"string\") {\n\t\tkey = options;\n\t}\n\n\t// updates text in update button to `lang(key)`\n\tlet update_btn = () => {\n\t\tdocument.getElementById(\"update\")\n\t\t\t.innerText = `(${lang(key)})`;\n\t}\n\n\tlet delay, now;\n\n\tswitch(key) {\n\t\tcase \"cli.update.uptodate_short\":\n\t\tcase \"cli.update.no_internet\":\n\t\t\t// initial value\n\t\t\tdelay = 0;\n\n\t\t\t// get current time\n\t\t\tnow = new Date().getTime();\n\n\t\t\t// check if `update.ns.last_checked` was less than 500ms\n\t\t\t// since now, this variable is set when `update.ns()` is\n\t\t\t// called\n\t\t\tif (now - update.ns.last_checked < 500) {\n\t\t\t\t// if less than 500ms has passed, set `delay` to the\n\t\t\t\t// amount of milliseconds missing until we've hit that\n\t\t\t\t// 500ms threshold\n\t\t\t\tdelay = 500 - (now - update.ns.last_checked);\n\t\t\t}\n\n\t\t\t// request up-to-date version numbers\n\t\t\tipcRenderer.send(\"get-version\");\n\n\t\t\t// wait `delay`ms\n\t\t\tsetTimeout(() => {\n\t\t\t\t// set buttons accordingly\n\t\t\t\tupdate_btn();\n\t\t\t\tset_buttons(true);\n\t\t\t\tupdate.ns.progress(false);\n\t\t\t\tplayNsBtn.innerText = lang(\"gui.launch\");\n\t\t\t}, delay)\n\n\t\t\tbreak;\n\t\tcase \"cli.update.failed\":\n\t\t\t// initial value\n\t\t\tdelay = 0;\n\n\t\t\t// get current time\n\t\t\tnow = new Date().getTime();\n\n\t\t\t// check if `update.ns.last_checked` was less than 500ms\n\t\t\t// since now, this variable is set when `update.ns()` is\n\t\t\t// called\n\t\t\tif (now - update.ns.last_checked < 500) {\n\t\t\t\t// if less than 500ms has passed, set `delay` to the\n\t\t\t\t// amount of milliseconds missing until we've hit that\n\t\t\t\t// 500ms threshold\n\t\t\t\tdelay = 500 - (now - update.ns.last_checked);\n\t\t\t}\n\n\t\t\t// Request version number\n\t\t\t// this will also handle the play button label for us\n\t\t\tipcRenderer.send(\"get-version\");\n\n\t\t\tsetTimeout(() => {\n\t\t\t\tupdate_btn();\n\t\t\t\tset_buttons(true);\n\t\t\t\tupdate.ns.progress(false);\n\t\t\t}, delay)\n\t\tdefault:\n\t\t\tupdate_btn();\n\n\t\t\tif (options.progress) {\n\t\t\t\tupdate.ns.progress(options.progress);\n\t\t\t}\n\n\t\t\tif (options.btn_text) {\n\t\t\t\tplayNsBtn.innerText = options.btn_text;\n\t\t\t}\n\n\t\t\tset_buttons(false);\n\t\t\tbreak;\n\t}\n})\n\nlet update = {\n\t// deletes install and update cache\n\tdelete_cache: () => {\n\t\tipcRenderer.send(\"delete-install-cache\");\n\t},\n\n\t// updates Northstar, `force_update` forcefully updates Northstar,\n\t// causing it to update, even if its already up-to-date\n\tns: async (force_update) => {\n\t\tlet can_connect = await request.check_with_toasts(\n\t\t\t\"GitHub\", \"https://github.com\"\n\t\t)\n\n\t\tif (! can_connect) {\n\t\t\treturn;\n\t\t}\n\n\t\tupdate.ns.last_checked = new Date().getTime();\n\t\tipcRenderer.send(\"update-northstar\", force_update);\n\t\tupdate.ns.should_install = false;\n\t}\n}\n\n// should Northstar be updated?\nupdate.ns.should_install = false;\n\n// when was the last time we checked for a Northstar update\nupdate.ns.last_checked = new Date().getTime();\n\n// `percent` should be a number between 0 to 100, if it's `false` it'll\n// reset it back to nothing instantly, with no animation\nupdate.ns.progress = (percent) => {\n\t// reset button progress\n\tif (percent === false) {\n\t\tdocument.querySelector(\".contentContainer #nsMain .playBtn\")\n\t\t\t.style.setProperty(\"--progress\", \"unset\");\n\n\t\treturn;\n\t}\n\n\tpercent = parseInt(percent);\n\n\t// make sure we're dealing with a number\n\tif (isNaN(percent) || typeof percent !== \"number\") {\n\t\treturn false;\n\t}\n\n\t// limit percent, while this barely has a difference, if you were to\n\t// set a very high number, the CSS would then use a very high\n\t// number, not great.\n\tif (percent > 100) {\n\t\tpercent = 100;\n\t} else if (percent < 0) {\n\t\tpercent = 0;\n\t}\n\n\t// invert number to it works in the CSS\n\tpercent = 100 - percent;\n\n\t// set the CSS progress variable\n\tdocument.querySelector(\".contentContainer #nsMain .playBtn\")\n\t\t.style.setProperty(\"--progress\", percent + \"%\");\n}\n\nmodule.exports = update;\n"
  },
  {
    "path": "src/app/js/version.js",
    "content": "module.exports = {\n\tis_newer: (version1, version2) => {\n\t\tversion1 = version.format(version1, true).split(\".\");\n\t\tversion2 = version.format(version2, true).split(\".\");\n\n\t\tfor (let i = 0; i < version1.length; i++) {\n\n\n\t\t\tlet nums = [\n\t\t\t\tparseInt(version1[i]) || 0,\n\t\t\t\tparseInt(version2[i]) || 0\n\t\t\t];\n\t\t\tif (nums[0] > nums[1]) {\n\t\t\t\treturn true;\n\t\t\t} else if (nums[0] < nums[1]) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t},\n\tformat: (version_number, no_leading_v) => {\n\t\tversion_number = version_number.trim();\n\n\t\tif (no_leading_v) {\n\t\t\tif (version_number[0] == \"v\") {\n\t\t\t\treturn version_number.slice(1, version_number.length);\n\t\t\t}\n\n\t\t\treturn version_number;\n\t\t\tif (no_leading_v) {\n\t\t\t\treturn version_number\n\t\t\t}\n\n\t\t\treturn \"v\" + version_number;\n\t\t} else {\n\t\t\tif (version_number[0] != \"v\") {\n\t\t\t\treturn \"v\" + version_number;\n\t\t\t}\n\t\t}\n\n\t\treturn version_number;\n\t}\n}\n"
  },
  {
    "path": "src/app/main.css",
    "content": "@import \"css/grid.css\";\n@import \"css/dragui.css\";\n@import \"css/toasts.css\";\n@import \"css/popups.css\";\n@import \"css/tooltip.css\";\n@import \"css/theming.css\";\n@import \"css/launcher.css\";\n@import \"css/selection.css\";\n\nbody {\n\tmargin: 0;\n\toverflow: hidden;\n}\n\nbutton {outline: none}\nb, strong {font-weight: 700}\nbody, input, button {font-weight: 500}\n\nbutton {\n\tborder: none;\n\tcolor: white;\n\toutline: none;\n\tcursor: pointer;\n\tfont-weight: 700;\n\tpadding: 5px 10px;\n\tborder-radius: 5px;\n\ttransition: 0.2s ease-in-out;\n}\n\nbutton:hover {filter: brightness(110%)}\nbutton:active {filter: brightness(90%)}\n\n.popup, #modsdiv, #modsdiv .el,\n.release-block, #nsRelease, #vpReleaseNotes {\n\toutline: 1px solid #444444;\n\tborder: 3px solid var(--bg) inset;\n}\n\n.playBtn, .gamesContainer button, #winbtns div {\n\tcursor: pointer;\n}\n\n/* window buttons { */\n#winbtns {\n\tz-index: 2;\n\tdisplay: flex;\n\tposition: fixed;\n\ttop: var(--padding);\n\tright: calc(var(--padding) / 2);\n}\n\n#winbtns div {\n\twidth: 25px;\n\topacity: 0.6;\n\theight: 25px;\n\tposition: relative;\n\tbackground-size: contain;\n\ttransition: 0.25s ease-in-out;\n\tmargin-right: calc(var(--padding) / 2);\n}\n\n#winbtns div.hidden {\n\twidth: 0px;\n\topacity: 0.0;\n\tmargin-right: 0px;\n\tpointer-events: none;\n}\n\n#winbtns div img {\n\theight: 100%;\n\ttransition: transform 0.25s ease-in-out;\n}\n\n#winbtns #settings.shown img {\n\ttransform: rotate(90deg);\n}\n\n#winbtns div:hover {opacity: 1.0}\n#winbtns div:active {transform: scale(0.95)}\n/* } */\n\nimg {pointer-events: none}\n\n#bgHolder {\n\ttop: -5px;\n\tleft: -5px;\n\tright: -5px;\n\tbottom: -5px;\n\tz-index:  -1;\n\tposition: absolute;\n\tbackground-size: cover;\n\tbackground-position: center;\n\tbackground-repeat: no-repeat;\n\ttransition: background-image 0.1s ease-in-out;\n\tfilter: brightness(0.4) blur(2px) grayscale(0.6);\n}\n\n#bgHolder[bg=\"0\"] {background-image: url(\"../assets/bg/viper.jpg\")}\n#bgHolder[bg=\"1\"] {background-image: url(\"../assets/bg/northstar.jpg\")}\n#bgHolder[bg=\"2\"] {background-image: url(\"../assets/bg/tf2.jpg\")}\n\n/* drag control */\n#bgHolder,\n.contentContainer, \n.gamesContainer {\n\tuser-select: none;\n\t-webkit-app-region: drag;\n}\n\n#overlay.shown ~ #bgHolder,\n#overlay.shown ~ .contentContainer, \n#overlay.shown ~ .gamesContainer {\n\t-webkit-app-region: no-drag;\n}\n\na, button, #close, #nsRelease, #vpReleaseNotes,\n.mod, #overlay, #modsdiv, #winbtns, .contentMenu {\n\t-webkit-app-region: no-drag;\n}\n\n.grid {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-content: start;\n}\n\n.switch {\n\twidth: 50px;\n\theight: 25px;\n\tborder-radius: 50px;\n\tbackground: var(--selbg);\n}\n\n.switch.on {\n\t--on-color: var(--red);\n\tbackground: rgba(var(--on-color), 0.4) !important;\n}\n\n.switch::after {\n\tleft: -5px;\n\twidth: 15px;\n\theight: 15px;\n\tcontent: \" \";\n\tdisplay: block;\n\tbackground: red;\n\tposition: relative;\n\tborder-radius: 50px;\n\tbackground: var(--bg);\n\ttransition: 0.2s ease-in-out;\n}\n\n.switch.on::after {\n\tleft: 15px;\n\twidth: 20px;\n\topacity: 0.8;\n\tbackground: rgb(var(--on-color));\n}\n\n.switch.on.red2 {--on-color: var(--red2)}\n.switch.on.blue {--on-color: var(--blue)}\n.switch.on.blue2 {--on-color: var(--blue2)}\n.switch.on.orange {--on-color: var(--orange)}\n.switch.on.orange2 {--on-color: var(--orange2)}\n\n.switch.on:hover::after {\n\ttransform: scale(1.2);\n}\n\n"
  },
  {
    "path": "src/app/main.js",
    "content": "const fs = require(\"fs\");\nconst path = require(\"path\");\nconst Fuse = require(\"fuse.js\");\nconst { app, ipcRenderer, shell } = require(\"electron\");\n\nconst lang = require(\"../lang\");\n\nipcRenderer.on(\"unknown-error\", (event, err) => {\n\ttoasts.show({\n\t\ttimeout: 10000,\n\t\tscheme: \"error\",\n\t\ttitle: lang(\"gui.toast.title.unknown_error\"),\n\t\tdescription: lang(\"gui.toast.desc.unknown_error\"),\n\t\tcallback: () => {\n\t\t\ttoasts.show({\n\t\t\t\ttimeout: 15000,\n\t\t\t\tscheme: \"error\",\n\t\t\t\ttitle: \"\",\n\t\t\t\tdescription: err.stack.replaceAll(\"\\n\", \"<br>\")\n\t\t\t})\n\t\t}\n\t})\n\n\tconsole.error(err.stack);\n})\n\nconst json = require(\"../modules/json\");\n\nconst kill = require(\"./js/kill\");\nconst mods = require(\"./js/mods\");\nconst toasts = require(\"./js/toasts\");\nconst update = require(\"./js/update\");\nconst events = require(\"./js/events\");\nconst launch = require(\"./js/launch\");\nconst popups = require(\"./js/popups\");\nconst browser = require(\"./js/browser\");\nconst tooltip = require(\"./js/tooltip\");\nconst version = require(\"./js/version\");\nconst request = require(\"./js/request\");\nconst process = require(\"./js/process\");\nconst settings = require(\"./js/settings\");\nconst gamepath = require(\"./js/gamepath\");\nconst launcher = require(\"./js/launcher\");\nconst is_running = require(\"./js/is_running\");\nconst set_buttons = require(\"./js/set_buttons\");\n\nconst navigate = require(\"./js/navigate\");\nconst gamepad = require(\"./js/gamepad\");\n\nrequire(\"./js/dom_events\");\nrequire(\"./js/localize\")();\n"
  },
  {
    "path": "src/cli.js",
    "content": "const fs = require(\"fs\");\nconst { app, ipcMain } = require(\"electron\");\n\nconst Emitter = require(\"events\");\nconst events = new Emitter();\n\nconst cli = app.commandLine;\nconst lang = require(\"./lang\");\nconst json = require(\"./modules/json\");\nconsole = require(\"./modules/console\");\n\nfunction hasArgs() {\n\t// Makes sure the GUI isn't launched.\n\t// TODO: Perhaps we should get a better way of doing this, at the\n\t// very least we should use a switch case here.\n\tif (cli.hasSwitch(\"cli\") ||\n\t\tcli.hasSwitch(\"help\") ||\n\t\tcli.hasSwitch(\"mods\") ||\n\t\tcli.hasSwitch(\"update\") ||\n\t\tcli.hasSwitch(\"launch\") ||\n\t\tcli.hasSwitch(\"setpath\") ||\n\t\tcli.hasSwitch(\"version\") ||\n\t\tcli.hasSwitch(\"gamepath\") ||\n\t\tcli.hasSwitch(\"togglemod\") ||\n\t\tcli.hasSwitch(\"removemod\") ||\n\t\tcli.hasSwitch(\"installmod\") ||\n\t\tcli.hasSwitch(\"update-viper\")) {\n\t\treturn true;\n\t} else {return false}\n}\n\n// Exits the CLI, when run without CLI being on it'll do nothing, this\n// is needed as without even if no code is executed it'll continue to\n// run as Electron is still technically running.\nfunction exit(code) {\n\tif (hasArgs()) {process.exit(code)}\n}\n\n// Ensures the gamepath exists, it's called by options that require the\n// gamepath to be able to work.\nfunction gamepathExists() {\n\tif (fs.existsSync(\"viper.json\")) {\n\t\tgamepath = json(\"viper.json\").gamepath;\n\n\t\tif (! fs.existsSync(gamepath)) {\n\t\t\tconsole.error(lang(\"cli.gamepath.lost\"));\n\t\t\texit(1);\n\t\t} else {\n\t\t\treturn true;\n\t\t}\n\t}\n}\n\n// General CLI initialization\n//\n// A lot of the CLI is handled through events sent back to the main process for\n// it to handle, this is because we re-use these events for the renderer as.\nasync function init() {\n\t// --help menu/argument\n\tif (cli.hasSwitch(\"help\")) {\n\tconsole.log(`options:\n  --help          ${lang(\"cli.help.help\")}\n  --version       ${lang(\"cli.help.version\")}\n  --devtools      ${lang(\"cli.help.devtools\")}\n\n  --cli           ${lang(\"cli.help.cli\")}\n  --update        ${lang(\"cli.help.update\")}\n  --update-viper  ${lang(\"cli.help.update_vp\")}\n  --setpath       ${lang(\"cli.help.setpath\")}\n  --no-vp-updates ${lang(\"cli.help.no_vp_updates\")}\n\n  --installmod    ${lang(\"cli.help.install_mod\")}\n  --removemod     ${lang(\"cli.help.remove_mod\")}\n  --togglemod     ${lang(\"cli.help.toggle_mod\")}`)\n\t\t// In the future --setpath should be able to understand\n\t\t// relative paths, instead of just absolute ones.\n\t\texit();\n\t}\n\n\t// --update\n\tif (cli.hasSwitch(\"update\") && gamepathExists()) {ipcMain.emit(\"update\")}\n\t// --version\n\tif (cli.hasSwitch(\"version\") && gamepathExists()) {\n\t\tlet version = require(\"./modules/version\");\n\n\t\tconsole.log(\"Viper: v\" + require(\"../package.json\").version);\n\t\tconsole.log(\"Titanfall 2: \" + version.titanfall());\n\t\tconsole.log(\"Northstar: \" + version.northstar());\n\t\tconsole.log(\"Node: \" + process.version);\n\t\tconsole.log(\"Electron: v\" + process.versions.electron);\n\t\texit();\n\t}\n\n\t// --setpath\n\tif (cli.hasSwitch(\"setpath\")) {\n\t\t// Checks to verify the path is legitimate\n\t\tif (cli.getSwitchValue(\"setpath\") != \"\") {\n\t\t\tipcMain.emit(\"setpathcli\", cli.getSwitchValue(\"setpath\"));\n\t\t} else {\n\t\t\tconsole.error(lang(\"cli.setpath.no_arg\"));\n\t\t\texit(1);\n\t\t}\n\t}\n\n\t// --launch\n\tif (gamepathExists() && cli.hasSwitch(\"launch\")) {\n\t\tswitch(cli.getSwitchValue(\"launch\")) {\n\t\t\tcase \"vanilla\":\n\t\t\t\tipcMain.emit(\"launchVanilla\");\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tipcMain.emit(\"launch\");\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\t// Mod related args, --installmod, --removemod, --togglemod\n\tif (cli.hasSwitch(\"installmod\") && gamepathExists()) {ipcMain.emit(\"install-mod\")}\n\tif (cli.hasSwitch(\"removemod\") && gamepathExists()) {ipcMain.emit(\"remove-mod\", \"\", cli.getSwitchValue(\"removemod\"))}\n\tif (cli.hasSwitch(\"togglemod\") && gamepathExists()) {ipcMain.emit(\"toggle-mod\", \"\", cli.getSwitchValue(\"togglemod\"))}\n\n\t// Prints out the list of mods\n\tif (cli.hasSwitch(\"mods\") && gamepathExists()) {ipcMain.emit(\"getmods\")}\n}\n\nmodule.exports = {\n\thasArgs,\n\tinit, exit, \n\thasParam: (arg) => {\n\t\treturn cli.hasSwitch(arg);\n\t},\n\tparam: (arg) => {\n\t\treturn cli.getSwitchValue(arg);\n\t}\n}\n"
  },
  {
    "path": "src/index.js",
    "content": "const path = require(\"path\");\nconst { app, BrowserWindow, dialog } = require(\"electron\");\n\n// makes it so Electron cache doesn't get stored in your system's config\n// folder, and instead changing it over to using the system's cache\n// folder instead\napp.setPath(\"userData\", path.join(app.getPath(\"cache\"), app.name));\n\n// ensures PWD/CWD is the config folder where viper.json is located\nprocess.chdir(app.getPath(\"appData\"));\n\nconst cli = require(\"./cli\");\nconst lang = require(\"./lang\");\n\nconst mods = require(\"./modules/mods\");\nconst update = require(\"./modules/update\");\nconst version = require(\"./modules/version\");\nconst settings = require(\"./modules/settings\");\nconst protocol = require(\"./modules/protocol\");\n\n// loads `ipcMain` events that dont fit in any of the modules directly\nrequire(\"./modules/ipc\");\n\n// required to load launch IPC events\nrequire(\"./modules/launch\");\n\nconsole = require(\"./modules/console\");\n\n// Starts the actual BrowserWindow, which is only run when using the\n// GUI, for the CLI this function is never called.\nfunction start() {\n\tlet win = new BrowserWindow({\n\t\twidth: 1000,\n\t\theight: 600,\n\t\ttitle: \"Viper\",\n\n\t\t// Hides the window initially, it'll be shown when the DOM is\n\t\t// loaded, as to not cause visual issues.\n\t\tshow: false,\n\n\t\t// In the future we may want to allow the user to resize the window,\n\t\t// as it's fairly responsive, but for now we won't allow that.\n\t\tresizable: false,\n\n\t\tframe: false,\n\t\ttitleBarStyle: \"hidden\",\n\t\ticon: path.join(__dirname, \"assets/icons/512x512.png\"),\n\n\t\twebPreferences: {\n\t\t\twebviewTag: true,\n\t\t\tnodeIntegration: true,\n\t\t\tcontextIsolation: false\n\t\t}\n\t})\n\n\t// makes sending things to the renderer a little more readable\n\twin.send = (channel, data) => {\n\t\twin.webContents.send(channel, data);\n\t}; send = win.send;\n\n\t// give `./win` the main window, `./win()` will then be equal\n\t// to `win`, but its accessible anywhere\n\trequire(\"./win\").set(win);\n\n\t// when --devtools is added it'll open the dev tools\n\tif (cli.hasParam(\"devtools\")) {\n\t\t// for some unknown, mysterious reason, the devtools just wont\n\t\t// open if you call this immediately, that's how its worked for\n\t\t// a very long time, and suddenly it stopped working, and this\n\t\t// seemingly was the only fix\n\t\tsetTimeout(() => {\n\t\t\twin.openDevTools();\n\t\t}, 1)\n\t}\n\n\t// we dont need this!\n\twin.removeMenu();\n\n\t// load `src/app/index.html` (the app)\n\twin.loadURL(\"file://\" + __dirname + \"/app/index.html\", {\n\t\tuserAgent: \"viper/\" + version.viper(),\n\t})\n\n\t// print exceptions to terminal, and forward the exception to the\n\t// renderer, it'll then show a more user friendly error message\n\tprocess.on(\"uncaughtException\", (err) => {\n\t\tsend(\"unknown-error\", err);\n\t\tconsole.error(err);\n\t})\n\n\t// load list of mods on initial load\n\twin.webContents.on(\"dom-ready\", () => {\n\t\tprotocol();\n\t\tsend(\"mods\", mods.list());\n\t})\n\n\t// start auto-update process\n\tif (settings().autoupdate) {\n\t\tif (cli.hasParam(\"no-vp-updates\")) {\n\t\t\tupdate.northstar_autoupdate();\n\t\t} else {\n\t\t\tupdate.viper(false);\n\t\t}\n\t} else {\n\t\tupdate.northstar_autoupdate();\n\t}\n}\n\n\n// starts the GUI or CLI\nif (cli.hasArgs()) {\n\tif (cli.hasParam(\"update-viper\")) {\n\t\tupdate.viper(true);\n\t} else {\n\t\t// start the CLI\n\t\tcli.init();\n\t}\n} else {\n\tapp.setAsDefaultProtocolClient(\"ror2mm\");\n\n\tconst app_lock = app.requestSingleInstanceLock()\n\n\t// start the window/GUI\n\tapp.on(\"ready\", () => {\n\t\tif (!app_lock) {\n\t\t\t// Viper is already running\n\t\t\tif (process.argv.length <= (app.isPackaged ? 1 : 2))\n\t\t\t{\n\t\t\t\tdialog.showMessageBoxSync({\n\t\t\t\t\ttitle: lang(\"viper.menu.main\"),\n\t\t\t\t\tmessage: lang(\"viper.already_running\")\n\t\t\t\t});\n\t\t\t}\n\t\t\tapp.quit();\n\t\t}\n\n\t\tstart();\n\t})\n\n\tapp.on('second-instance', (event, commandLine, workingDirectory, additionalData) => {\n\t\tprotocol(commandLine);\n\t})\n\n}\n"
  },
  {
    "path": "src/lang/de.json",
    "content": "{\n\t\"cli\": {\n\t\t\"auto_updates\": {\n\t\t\t\"available\": \"Ein Update für Northstar wurde gefunden!\",\n\t\t\t\"checking\": \"Überprüfe Northstar auf Updates...\",\n\t\t\t\"no_update\": \"Kein Update für Northstar vorhanden.\",\n\t\t\t\"updating_ns\": \"Updateprozess wird gestartet...\"\n\t\t},\n\t\t\"gamepath\": {\n\t\t\t\"lost\": \"Installationspfad wurde nicht gefunden, bitte stelle sicher das er gemountet ist!\"\n\t\t},\n\t\t\"help\": {\n\t\t\t\"cli\": \"Zwingt die CLI Einstellung auf \\\"an\\\".\",\n\t\t\t\"devtools\": \"Öffnet Entwickler/Debug Werkzeuge.\",\n\t\t\t\"help\": \"Zeigt die Hilfe Nachricht an.\",\n\t\t\t\"install_mod\": \"Installiert einen Mod, eine ZIP-Datei oder einen Ordner.\",\n\t\t\t\"no_vp_updates\": \"Überschreibt viper.json und deaktiviert das Aktualisieren von Viper.\",\n\t\t\t\"remove_mod\": \"Entfernt einen Mod.\",\n\t\t\t\"setpath\": \"Setzt den Installationspfad von Titanfall 2.\",\n\t\t\t\"toggle_mod\": \"Aktiviert/Deaktiviert einen Mod.\",\n\t\t\t\"update\": \"Aktualisiert den Installationspfad von Northstar, durch den gegeben Installationspfad von Titanfall 2.\",\n\t\t\t\"update_vp\": \"Aktualisiert Viper, falls dies unterstützt wird.\",\n\t\t\t\"version\": \"Gibt die Versions Informationen aus.\"\n\t\t},\n\t\t\"launch\": {\n\t\t\t\"linux_error\": \"Das Spiel starten ist derzeit nicht auf Linux unterstützt\"\n\t\t},\n\t\t\"mods\": {\n\t\t\t\"cant_find\": \"Ein Mod mit diesem Namen konnte nicht gefunden werden!\",\n\t\t\t\"failed\": \"Mod konnte nicht installiert werden!\",\n\t\t\t\"improper_json\": \"Die mod.json vom Mod %s ist fehlerhaft!\",\n\t\t\t\"installed\": \"Mod wurde erfolgreich installiert!\",\n\t\t\t\"not_a_mod\": \"Die angegebene Datei/der angegebene Ordner ist kein Mod!\",\n\t\t\t\"removed\": \"Mod wurde erfolgreich entfernt!\",\n\t\t\t\"toggled\": \"Mod wurde erfolgreich aktiviert/deaktiviert!\",\n\t\t\t\"toggled_all\": \"Alle Mods wurde erfolgreich aktiviert/deaktiviert!\"\n\t\t},\n\t\t\"setpath\": {\n\t\t\t\"no_arg\": \"Keine Argument für --setpath wurden angegeben!\"\n\t\t},\n\t\t\"update\": {\n\t\t\t\"checking\": \"Überprüfe auf Updates...\",\n\t\t\t\"current\": \"Jetzige Version:\",\n\t\t\t\"download_done\": \"Herunterladen abgeschlossen! Extrahiere...\",\n\t\t\t\"downloading\": \"Wird heruntergeladen...\",\n\t\t\t\"failed\": \"Installation/Aktualisierung fehlgeschlagen!\",\n\t\t\t\"finished\": \"Installation/Aktualisierung abgeschlossen!\",\n\t\t\t\"no_internet\": \"Keine Internetverbindung\",\n\t\t\t\"uptodate\": \"Installation ist bereits auf dem neusten Stand (%s), aktualisieren wird übersprungen.\",\n\t\t\t\"uptodate_short\": \"Auf dem neusten stand\"\n\t\t}\n\t},\n\t\"general\": {\n\t\t\"auto_updates\": {\n\t\t\t\"game_running\": \"Spiel wird ausgeführt, Northstar wird nicht aktualisiert.\"\n\t\t},\n\t\t\"launching\": \"Starte!\",\n\t\t\"missing_path\": \"Installationspfad konnte nicht automatisch gefunden werden, bitte setze diesen manuell!\",\n\t\t\"mods\": {\n\t\t\t\"disabled\": \"Deaktivierte Mods:\",\n\t\t\t\"enabled\": \"Aktivierte Mods:\",\n\t\t\t\"installed\": \"Installierte Mods:\"\n\t\t},\n\t\t\"not_installed\": \"Northstar ist nicht installiert!\",\n\t\t\"running\": \"Läuft bereits.\"\n\t},\n\t\"gui\": {\n\t\t\"browser\": {\n\t\t\t\"end_of_list\": \"Alle Packete wurden geladen.\",\n\t\t\t\"filter\": {\n\t\t\t\t\"client\": \"Client-seitig\",\n\t\t\t\t\"mods\": \"Mods\",\n\t\t\t\t\"server\": \"Server-seitig\",\n\t\t\t\t\"skins\": \"Skins\"\n\t\t\t},\n\t\t\t\"guide\": \"Anleitung\",\n\t\t\t\"info\": \"Info\",\n\t\t\t\"install\": \"Installieren\",\n\t\t\t\"load_more\": \"Lade mehr...\",\n\t\t\t\"loading\": \"Lade Mods...\",\n\t\t\t\"made_by\": \"von\",\n\t\t\t\"no_results\": \"Keine Ergebnisse...\",\n\t\t\t\"reinstall\": \"Neuinstallieren\",\n\t\t\t\"sort\": {\n\t\t\t\t\"highest_rating\": \"Meist Bewertet\",\n\t\t\t\t\"last_updated\": \"Zuletzt Aktualisiert\",\n\t\t\t\t\"most_downloads\": \"Meist Heruntergeladen\",\n\t\t\t\t\"newest\": \"Neuste\"\n\t\t\t},\n\t\t\t\"update\": \"Aktualisieren\",\n\t\t\t\"view\": \"Anschauen\"\n\t\t},\n\t\t\"exit\": \"Schließen\",\n\t\t\"gamepath\": {\n\t\t\t\"found_missing_perms\": \"Es wurde ein Spielepfad automatisch gefunden, jedoch fehlen dir die Rechte Datein zulesen oder zuschreiben in diesem Pfad. Bitte wähle einen anderen Pfad aus oder erhalte die benötigten Berechtigungen für den gewählten Pfad:\\n\\n\",\n\t\t\t\"lost\": \"Der angegeben Installationspfad ist nicht mehr gültig!\\n\\nBitte stelle sicher das die Festplatte mounted ist, oder falls sich der Installationspfad geändert hat, das du diesen auch in Viper aktualisierst!\\n\\nViper wird möglicherweiße bis zum nächsten Neustart nicht funktionieren!\",\n\t\t\t\"lost_perms\": \"Wie es aussieht hat der jetzige Nutzer die Rechte Datein zulesen oder zuschreiben im Spielepfad verloren, bitte wähle einen richtigen Pfad aus oder erhalte die benötigten Berechtigungen für den jetzigen:\\n\\n\",\n\t\t\t\"missing_perms\": \"Wie es aussieht hat der jetzige Nutzer die Rechte Datein zulesen oder zuschreiben im Spielepfad nicht, bitte wähle einen richtigen Pfad aus oder erhalte die benötigten Berechtigungen für den gewählten Pfad:\\n\\n\",\n\t\t\t\"must\": \"Der Installationspfad muss gesetzt worden sein um Viper zu nutzen.\",\n\t\t\t\"wrong\": \"Der angegebene Installationspfad ist nicht gültig!\"\n\t\t},\n\t\t\"install\": \"Installieren\",\n\t\t\"launch\": \"Starten\",\n\t\t\"mods\": {\n\t\t\t\"cant_find_specific\": \"Mod %s-%s kann nicht gefunden werden!\",\n\t\t\t\"cant_find_version\": \"Version %s von Mod %s-%s kann nicht gefunden werden!\",\n\t\t\t\"confirm_dependencies\": \"Dieser Mod benötigt weitere Mods, diese werden unter dieser Nachricht angezeigt. Beim drücken auf \\\"Ok\\\" stimmst du zu da diese Installiert werden.\\n\\n\",\n\t\t\t\"confirm_plugins\": {\n\t\t\t\t\"description\": \"Native plugins haben sehr viel mehr Rechte als reguläre Mods, da durch ist das nutzen dieser um einiges unsicherer denn es ist einfacher ihnen zuschaden! Bitte installieren sie nur native plugins von vertrauten Entwicklern oder ähnliches, falls dir bewusst ist was du machst kannst du diese Nachricht ignorieren.\",\n\t\t\t\t\"title\": \"Das folgende Packet hat native plugins::\"\n\t\t\t},\n\t\t\t\"count\": \"Installierte Mods:\",\n\t\t\t\"disabled_tag\": \"Deaktiviert\",\n\t\t\t\"drag_n_drop\": \"Drag and drop den Mod um ihn zu installieren!\",\n\t\t\t\"extracting\": \"Extrahiere den Mod...\",\n\t\t\t\"find\": \"Suche nach Mods\",\n\t\t\t\"install\": \"Installiere den Mod\",\n\t\t\t\"installed_mod\": \"Mod installiert!\",\n\t\t\t\"installing\": \"Installiere den Mod...\",\n\t\t\t\"not_a_mod\": \"Kein Mod!\",\n\t\t\t\"nothing_selected\": \"Es wurde kein Mod ausgewählt.\",\n\t\t\t\"remove\": \"Entfernen\",\n\t\t\t\"remove_all\": \"Entferne alle Mods\",\n\t\t\t\"remove_all_confirm\": \"Das Entfernen aller Mods führt meist dazu das eine Neuinstallation von Northstar nötig ist. Bist du dir sicher das du diese Aktion durchführen willst?\",\n\t\t\t\"required_confirm\": \"Du hast einen von Northstar benötigten Mod ausgewählt, bist du dir sicher das du diese Aktion durchführen willst\",\n\t\t\t\"title\": \"Mods\",\n\t\t\t\"toggle_all\": \"Aktiviere/Deaktiviere alle Mods\",\n\t\t\t\"toggle_all_confirm\": \"Das Deaktivieren aller Mods kann zum Deaktiveren von Mods führen die von Northstar benötigt werden. Bist du dir sicher das du diese Aktion durchführen willst?\",\n\t\t\t\"unknown_author\": \"Unbekannt\"\n\t\t},\n\t\t\"nsupdate\": {\n\t\t\t\"gaming\": {\n\t\t\t\t\"body\": \"Ein Update für Northstar ist verfügbar! Du kannst die Aktualisierung nach dem schließen erzwingen!\",\n\t\t\t\t\"title\": \"Northstar Update verfübar!\"\n\t\t\t}\n\t\t},\n\t\t\"search\": \"Suchen...\",\n\t\t\"server\": {\n\t\t\t\"offline\": \"Masterserver ist offline\",\n\t\t\t\"player\": \"Spieler\",\n\t\t\t\"players\": \"Spieler\",\n\t\t\t\"servers\": \"Server\"\n\t\t},\n\t\t\"setpath\": \"Installationspfad aktualisieren.\",\n\t\t\"settings\": {\n\t\t\t\"autolang\": {\n\t\t\t\t\"desc\": \"Beim Aktiveren versucht Viper die richtige Sprache durch deine Systemsprache zu erkennen, durch das deaktivieren ist die manuelle Auswahl aktiviert.\",\n\t\t\t\t\"title\": \"Automatische Spracherkennung\"\n\t\t\t},\n\t\t\t\"autoupdate\": {\n\t\t\t\t\"desc\": \"Viper wird sich automatisch selbst aktualisieren!\",\n\t\t\t\t\"title\": \"Viper Auto-Updates\"\n\t\t\t},\n\t\t\t\"discard\": \"Verwerfen\",\n\t\t\t\"excludes\": {\n\t\t\t\t\"desc\": \"Beim Aktualisieren von Northstar werden diese Datein nicht überschrieben. Solang du nicht weißt was du verändert solltest du diese Datein auch nicht berabeiten. Dateinamen sollte durch eine Lücke getrennt werden.\",\n\t\t\t\t\"title\": \"Behalte Datein beim aktualisieren.\"\n\t\t\t},\n\t\t\t\"forcedlang\": {\n\t\t\t\t\"desc\": \"Wenn \\\"Automatische Spracherkennung\\\" deaktiviert ist, wird diese Option genutzt um die Sprachen zu ändern. Oft ist ein Neustart nötig!\",\n\t\t\t\t\"title\": \"Sprache\"\n\t\t\t},\n\t\t\t\"linux_launch_cmd_ns\": {\n\t\t\t\t\"desc\": \"Dieser Befehl wird zum starten von Northstar ausgeführt wenn die Startmethode \\\"Eigener Befehl\\\" ausgewählt ist. Startvariabeln werden am ende des Befehl angehangen und als Environmentvariable $TF_ARGS mitgegeben.\",\n\t\t\t\t\"title\": \"Northstar Startbefehl\"\n\t\t\t},\n\t\t\t\"linux_launch_cmd_vanilla\": {\n\t\t\t\t\"desc\": \"Dieser Befehl wird zum starten von Vanilla ausgeführt wenn die Startmethode \\\"Eigener Befehl\\\" ausgewählt ist. Startvariabeln werden am ende des Befehls angehangen und als Environmentvariable $TF_ARGS mitgegeben.\",\n\t\t\t\t\"title\": \"Vanilla Startbefehl\"\n\t\t\t},\n\t\t\t\"linux_launch_method\": {\n\t\t\t\t\"desc\": \"Die Methode welche zum start des Spieles auf Linux genutzt wird.\",\n\t\t\t\t\"methods\": {\n\t\t\t\t\t\"command\": \"Eigener Befehl\",\n\t\t\t\t\t\"steam_auto\": \"Steam (Automatisch)\",\n\t\t\t\t\t\"steam_executable\": \"Steam (Ausführungsdatei)\",\n\t\t\t\t\t\"steam_flatpak\": \"Steam (Flatpak)\",\n\t\t\t\t\t\"steam_protocol\": \"Steam (Protokoll)\"\n\t\t\t\t},\n\t\t\t\t\"title\": \"Linux Startmethode\"\n\t\t\t},\n\t\t\t\"miscbuttons\": {\n\t\t\t\t\"buttons\": {\n\t\t\t\t\t\"change_gamepath\": \"Spielepfad ändern\",\n\t\t\t\t\t\"force_quit_game\": \"Titanfall/Northstar schließung erzwingen\",\n\t\t\t\t\t\"force_quit_origin\": \"Origin/EA Desktop schließung erzwingen\",\n\t\t\t\t\t\"open_gamepath\": \"Spielepfad öffnen\",\n\t\t\t\t\t\"reset_config\": \"Konfiguration zurücksetzung erzwingen\",\n\t\t\t\t\t\"restart_viper\": \"Viper neustarten\"\n\t\t\t\t},\n\t\t\t\t\"desc\": \"Beim weiteren Problemen könnten diesen Aktion helfen.\",\n\t\t\t\t\"open_gamepath_alert\": \"Fehlerhafter Spielepfad, dadurch kann keiner geöffnet werden. Bitte wähle den richtigen Pfad zuerst!\",\n\t\t\t\t\"reset_config_alert\": \"Bestätige die Zurücksetzung der Konfiguration, Viper wird nach dem bestätigen die Konfiguration Datein löschen und neustarten.\",\n\t\t\t\t\"title\": \"Zusatz Aktionen\"\n\t\t\t},\n\t\t\t\"nsargs\": {\n\t\t\t\t\"desc\": \"Hier kannst du Startoptionen für Northstar/Titanfall setzen.\",\n\t\t\t\t\"title\": \"Startoptionen\"\n\t\t\t},\n\t\t\t\"nsupdate\": {\n\t\t\t\t\"desc\": \"Viper wird Northstar automatisch aktualisieren, eine manuelle Aktualisierung ist trotzdem möglich.\",\n\t\t\t\t\"title\": \"Northstar Auto-Updates\"\n\t\t\t},\n\t\t\t\"originkill\": {\n\t\t\t\t\"desc\": \"Wenn Viper sich schließt soll Origin und oder EA Desktop sich auch schließen.\",\n\t\t\t\t\"title\": \"Automatisch Origin und oder EA Desktop schließen\"\n\t\t\t},\n\t\t\t\"save\": \"Speichern\",\n\t\t\t\"title\": {\n\t\t\t\t\"language\": \"Sprache\",\n\t\t\t\t\"linux\": \"Linux\",\n\t\t\t\t\"misc\": \"Sonstiges\",\n\t\t\t\t\"ns\": \"Northstar\",\n\t\t\t\t\"updates\": \"Updates\"\n\t\t\t},\n\t\t\t\"updatebuttons\": {\n\t\t\t\t\"buttons\": {\n\t\t\t\t\t\"force_delete_install_cache\": \"Zwischengespeicherte Installationsdateien löschung erzwingen\",\n\t\t\t\t\t\"force_northstar_reinstall\": \"Northstar neuinstallation erzwingen\",\n\t\t\t\t\t\"reset_cached_api_requests\": \"API zwischenspeicher leerung erzwingen\"\n\t\t\t\t},\n\t\t\t\t\"desc\": \"Bei Problemen mit updates könnte diese Aktionen helfen.\",\n\t\t\t\t\"title\": \"Reparations Aktionen\"\n\t\t\t}\n\t\t},\n\t\t\"toast\": {\n\t\t\t\"desc\": {\n\t\t\t\t\"duped\": \"hat mehrere Ordner in sich mit dem selben Namen, wodurch ein duplizierter Ordner ensteht! Falls du der Entwickler bist solltest du dies beheben!\",\n\t\t\t\t\"failed\": \"Ein unbekannter Fehler ist aufgetaucht beim Installieren, die Schuld kann beim Autor liegen oder bei Viper selbst!\",\n\t\t\t\t\"failed_launch_command\": \"Es gab einen Fehler beim ausführen vom Startbefehl.\",\n\t\t\t\t\"failed_to_connect\": \"Eine Verbindung mit %s konnte nicht hergestellt werden, überprüfe deine Internetverbindung, Firewall oder ob %s nicht erreichbar ist.\",\n\t\t\t\t\"installed\": \"wurde installiert!\",\n\t\t\t\t\"malformed\": \"hat eine fehlerhafte Ordnerstruktur, falls du der Entwickler bist, solltest du dies beheben.\",\n\t\t\t\t\"missing_flatpak\": \"Konnte nicht mit Flatpak starten, da keine Instanz gefunden wurde.\",\n\t\t\t\t\"missing_flatpak_steam\": \"Konnte nicht mit der Flatpak version von Steam starten, da keine Instanz gefunden wurde.\",\n\t\t\t\t\"missing_launch_command\": \"Es wurde kein Startbefehl angegeben, bitte setze einen fest.\",\n\t\t\t\t\"missing_steam\": \"Kann nicht direkt über Steam starten, da keine Instanz gefunden wurde.\",\n\t\t\t\t\"no_internet\": \"Viper funktioniert möglicherweise nicht korrekt\",\n\t\t\t\t\"unknown_error\": \"Ein unbekannter Fehler ist aufgetreten für mehr details drücken! Es wird empfohlen einen Screenshot von der detalierten Fehlernachricht zu machen wenn ein Bug-Report erstellt wird!\"\n\t\t\t},\n\t\t\t\"title\": {\n\t\t\t\t\"duped\": \"Duplizierter Ordner name!\",\n\t\t\t\t\"failed\": \"Fehler beim Installieren des Mods!\",\n\t\t\t\t\"failed_launch_command\": \"Ausführungsfehler vom Startbefehl!\",\n\t\t\t\t\"failed_to_connect\": \"Verbindung fehlgeschlagen\",\n\t\t\t\t\"installed\": \"Mod installiert!\",\n\t\t\t\t\"malformed\": \"Fehlerhafte Ordnerstruktur!\",\n\t\t\t\t\"missing_flatpak\": \"Flatpak fehlt!\",\n\t\t\t\t\"missing_flatpak_steam\": \"Flatpak Steam fehlt!\",\n\t\t\t\t\"missing_launch_command\": \"Fehlender Startbefehl!\",\n\t\t\t\t\"missing_steam\": \"Steam fehlt!\",\n\t\t\t\t\"no_internet\": \"Kein Internet\",\n\t\t\t\t\"unknown_error\": \"Unbekannter Fehler!\"\n\t\t\t}\n\t\t},\n\t\t\"update\": {\n\t\t\t\"available\": \"Ein neues update für Viper ist verfügbar willst du Viper neustarten und diese installieren?\",\n\t\t\t\"button\": \"Aktualisieren\",\n\t\t\t\"check\": \"Auf Update überprüfen\",\n\t\t\t\"downloading\": \"Herunterladen...\",\n\t\t\t\"extracting\": \"Extrahiere update...\",\n\t\t\t\"finished\": \"Fertig! Spielbereit!\",\n\t\t\t\"uptodate\": \"Bereits auf dem neusten Stand!\"\n\t\t},\n\t\t\"versions\": {\n\t\t\t\"northstar\": \"Northstar Version\",\n\t\t\t\"viper\": \"Viper Version\"\n\t\t},\n\t\t\"welcome\": \"Willkommen zu Viper!\"\n\t},\n\t\"lang\": {\n\t\t\"title\": \"German - Deutsch\"\n\t},\n\t\"ns\": {\n\t\t\"menu\": {\n\t\t\t\"force_quit\": \"Schließen erzwingen\",\n\t\t\t\"main\": \"Northstar Launcher\",\n\t\t\t\"mods\": \"Mods\",\n\t\t\t\"release\": \"Release Notes\"\n\t\t}\n\t},\n\t\"request\": {\n\t\t\"no_ns_release_notes\": \"<h3>Northstar Release Notes konnten nicht geladen werden.</h3>Versuche es erneut später!\",\n\t\t\"no_vp_release_notes\": \"<h3>Viper Release Notes konnten nicht geladen werden.</h3>Versuche es erneut später!\"\n\t},\n\t\"tooltip\": {\n\t\t\"close\": \"Schließen\",\n\t\t\"minimize\": \"Minimieren\",\n\t\t\"offline\": \"Internetverbindung nicht Verfügbar\",\n\t\t\"pages\": {\n\t\t\t\"northstar\": \"Northstar\",\n\t\t\t\"titanfall\": \"Titanfall 2\",\n\t\t\t\"viper\": \"Viper\"\n\t\t},\n\t\t\"settings\": \"Einstellungen\"\n\t},\n\t\"viper\": {\n\t\t\"already_running\": \"Viper läuft bereits\",\n\t\t\"info\": {\n\t\t\t\"credits\": \"Credits\",\n\t\t\t\"discord\": \"Tritt dem Discord bei:\",\n\t\t\t\"issues\": \"Report ein Problem mit Viper:\",\n\t\t\t\"links\": \"Links\"\n\t\t},\n\t\t\"menu\": {\n\t\t\t\"info\": \"Extras\",\n\t\t\t\"main\": \"Viper\",\n\t\t\t\"release\": \"Release Notes\"\n\t\t}\n\t}\n}"
  },
  {
    "path": "src/lang/en.json",
    "content": "{\n\t\"lang\": {\n\t\t\"title\": \"English\"\n\t},\n\n\t\"cli\": {\n\t\t\"help\": {\n\t\t\t\"help\": \"shows this help message\",\n\t\t\t\"devtools\": \"opens the dev/debug tools\",\n\t\t\t\"version\": \"outputs version info\",\n\t\t\t\"cli\": \"forces the CLI to enable\",\n\t\t\t\"update\": \"updates Northstar from your set game path\",\n\t\t\t\"setpath\": \"sets your game path\",\n\t\t\t\"update_vp\": \"updates Viper itself, if supported.\",\n\t\t\t\"no_vp_updates\": \"overwrites viper.json and disables Viper updates\",\n\t\t\t\"install_mod\": \"installs a mod, folder or zip\",\n\t\t\t\"remove_mod\": \"removes a mod\",\n\t\t\t\"toggle_mod\": \"toggles a mod\"\n\t\t},\n\n\t\t\"setpath\": {\n\t\t\t\"no_arg\": \"No argument provided for --setpath\"\n\t\t},\n\n\t\t\"update\": {\n\t\t\t\"current\": \"Current version:\",\n\t\t\t\"downloading\": \"Downloading...\",\n\t\t\t\"checking\": \"Checking for updates...\",\n\t\t\t\"download_done\": \"Download done! Extracting...\",\n\t\t\t\"finished\": \"Installation/Update finished!\",\n\t\t\t\"failed\": \"Installation/Update failed!\",\n\t\t\t\"uptodate\": \"Latest version (%s) is already installed, skipping update.\",\n\t\t\t\"uptodate_short\": \"Up-to-date\",\n\t\t\t\"no_internet\": \"No Internet connection\"\n\t\t},\n\n\t\t\"auto_updates\": {\n\t\t\t\"checking\": \"Checking for Northstar updates...\",\n\t\t\t\"available\": \"Northstar update available!\",\n\t\t\t\"updating_ns\": \"Launching update process...\",\n\t\t\t\"no_update\": \"No Northstar update available.\"\n\t\t},\n\n\t\t\"launch\": {\n\t\t\t\"linux_error\": \"Launching the game is not currently supported on Linux\"\n\t\t},\n\n\t\t\"gamepath\": {\n\t\t\t\"lost\": \"Gamepath not found, make sure it's mounted!\"\n\t\t},\n\n\t\t\"mods\": {\n\t\t\t\"failed\": \"Failed to install mod!\",\n\t\t\t\"removed\": \"Successfully removed mod!\",\n\t\t\t\"toggled\": \"Successfully toggled mod\",\n\t\t\t\"installed\": \"Successfully installed mod!\",\n\t\t\t\"cant_find\": \"Can't find a mod with that name!\",\n\t\t\t\"not_a_mod\": \"Selected folder/file is not a mod\",\n\t\t\t\"toggled_all\": \"Successfully toggled all mods\",\n\t\t\t\"improper_json\": \"%s's mod.json has formatting errors\"\n\t\t}\n\t},\n\t\"gui\": {\n\t\t\"exit\": \"Exit\",\n\t\t\"search\": \"Search...\",\n\t\t\"welcome\": \"Welcome to Viper!\",\n\t\t\"setpath\": \"Change Game Path\",\n\n\t\t\"versions\": {\n\t\t\t\"viper\": \"Viper version\",\n\t\t\t\"northstar\": \"Northstar version\"\n\t\t},\n\n\t\t\"update\": {\n\t\t\t\"button\": \"Update\",\n\t\t\t\"check\": \"Check for updates\",\n\t\t\t\"downloading\": \"Downloading...\",\n\t\t\t\"extracting\": \"Extracting update...\",\n\t\t\t\"finished\": \"Done! Ready to play!\",\n\t\t\t\"uptodate\": \"Already up to date!\",\n\t\t\t\"available\": \"A new update for Viper is available, do you want to restart and apply it?\"\n\t\t},\n\n\t\t\"mods\": {\n\t\t\t\"title\": \"Mods\",\n\t\t\t\"count\": \"Mods Installed:\",\n\t\t\t\"disabled_tag\": \"Disabled\",\n\t\t\t\"remove\": \"Remove\",\n\t\t\t\"install\": \"Install Mod\",\n\t\t\t\"find\": \"Find Mods\",\n\t\t\t\"toggle_all\": \"Toggle All\",\n\t\t\t\"toggle_all_confirm\": \"Toggling all mods could disable mods required for Northstar to function. Are you sure?\",\n\t\t\t\"remove_all\": \"Remove All\",\n\t\t\t\"remove_all_confirm\": \"Removing all mods will usually require you to reinstall Northstar. Are you sure?\",\n\t\t\t\"nothing_selected\": \"You've not selected a mod.\",\n\t\t\t\"required_confirm\": \"You've selected a core mod, Northstar may not function without it. Are you sure?\",\n\n\t\t\t\"not_a_mod\": \"Not a mod!\",\n\t\t\t\"unknown_author\": \"Unknown\",\n\t\t\t\"extracting\": \"Extracting mod...\",\n\t\t\t\"installing\": \"Installing mod...\",\n\t\t\t\"installed_mod\": \"Installed mod!\",\n\t\t\t\"drag_n_drop\": \"Drag and drop a mod to install\",\n\t\t\t\"confirm_dependencies\": \"This package has dependencies, shown below, clicking \\\"Ok\\\" will install the package and the dependencies.\\n\\n\",\n\t\t\t\"cant_find_specific\": \"Can't find mod %s-%s!\",\n\t\t\t\"cant_find_version\": \"Can't find version %s of mod %s-%s!\",\n\n\t\t\t\"confirm_plugins\": {\n\t\t\t\t\"title\": \"The following package has native plugins:\",\n\t\t\t\t\"description\": \"Native plugins have far more system access than a regular mod, and because of this they're inherently less secure to have installed, as a malicious plugin could do far more harm this way. If this plugin is one from a trusted developer or similar or you know what you're doing, then you can disregard this message completely.\"\n\t\t\t}\n\t\t},\n\n\t\t\"browser\": {\n\t\t\t\"info\": \"Info\",\n\t\t\t\"view\": \"View\",\n\t\t\t\"made_by\": \"by\",\n\t\t\t\"update\": \"Update\",\n\t\t\t\"install\": \"Install\",\n\t\t\t\"reinstall\": \"Re-Install\",\n\t\t\t\"loading\": \"Loading mods...\",\n\t\t\t\"load_more\": \"Load more...\",\n\t\t\t\"end_of_list\": \"All packages have been loaded.\",\n\t\t\t\"no_results\": \"No results...\",\n\t\t\t\"guide\": \"Guide\",\n\n\t\t\t\"sort\": {\n\t\t\t\t\"newest\": \"Newest\",\n\t\t\t\t\"last_updated\": \"Last Updated\",\n\t\t\t\t\"highest_rating\": \"Highest Rated\",\n\t\t\t\t\"most_downloads\": \"Most Downloaded\"\n\t\t\t},\n\n\t\t\t\"filter\": {\n\t\t\t\t\"mods\": \"Mods\",\n\t\t\t\t\"skins\": \"Skins\",\n\t\t\t\t\"client\": \"Client-side\",\n\t\t\t\t\"server\": \"Server-side\"\n\t\t\t}\n\t\t},\n\n\t\t\"settings\": {\n\t\t\t\"save\": \"Save\",\n\t\t\t\"discard\": \"Discard\",\n\n\t\t\t\"title\": {\n\t\t\t\t\"ns\": \"Northstar\",\n\t\t\t\t\"linux\": \"Linux\",\n\t\t\t\t\"language\": \"Language\",\n\t\t\t\t\"updates\": \"Updates\",\n\t\t\t\t\"misc\": \"Miscellaneous\"\n\t\t\t},\n\n\t\t\t\"nsargs\": {\n\t\t\t\t\"title\": \"Launch options\",\n\t\t\t\t\"desc\": \"Here you can add launch options for Northstar/Titanfall.\"\n\t\t\t},\n\n\t\t\t\"autolang\": {\n\t\t\t\t\"title\": \"Auto-Detect Language\",\n\t\t\t\t\"desc\": \"When enabled, Viper tries to automatically detect your system language, when disabled you can manually change the language below.\"\n\t\t\t},\n\n\t\t\t\"forcedlang\": {\n\t\t\t\t\"title\": \"Language\",\n\t\t\t\t\"desc\": \"When \\\"Auto-Detect Language\\\" is disabled, this will decide the language. Requires a restart to take effect.\"\n\t\t\t},\n\n\t\t\t\"autoupdate\": {\n\t\t\t\t\"title\": \"Viper Auto-Updates\",\n\t\t\t\t\"desc\": \"Viper will automatically keep itself up-to-date.\"\n\t\t\t},\n\n\t\t\t\"nsupdate\": {\n\t\t\t\t\"title\": \"Northstar Auto-Updates\",\n\t\t\t\t\"desc\": \"Viper will automatically keep Northstar up-to-date, however it can still manually be updated through the Northstar page.\"\n\t\t\t},\n\n\t\t\t\"excludes\": {\n\t\t\t\t\"title\": \"Retain files on update\",\n\t\t\t\t\"desc\": \"When Northstar is updated, files specified here will not be overwritten by files from the new Northstar update, unless you know what you're changing, you should probably not change anything here. Each file is separated with a space.\"\n\t\t\t},\n\n\t\t\t\"originkill\": {\n\t\t\t\t\"title\": \"Automatically quit Origin and or EA Desktop\",\n\t\t\t\t\"desc\": \"When Viper exits, automatically quit Origin and or EA Desktop as well.\"\n\t\t\t},\n\n\t\t\t\"updatebuttons\": {\n\t\t\t\t\"title\": \"Repair actions\",\n\t\t\t\t\"desc\": \"If you're having problems with updates, some of these buttons may help in fixing them.\",\n\t\t\t\t\"buttons\": {\n\t\t\t\t\t\"reset_cached_api_requests\": \"Reset cached API requests\",\n\t\t\t\t\t\"force_northstar_reinstall\": \"Force re-install of Northstar\",\n\t\t\t\t\t\"force_delete_install_cache\": \"Force delete cached install files\"\n\t\t\t\t}\n\t\t\t},\n\n\t\t\t\"miscbuttons\": {\n\t\t\t\t\"title\": \"Misc repair actions\",\n\t\t\t\t\"desc\": \"If you're having problems then some of these buttons may help fixing it\",\n\t\t\t\t\"buttons\": {\n\t\t\t\t\t\"open_gamepath\": \"Open gamepath\",\n\t\t\t\t\t\"reset_config\": \"Reset config file\",\n\t\t\t\t\t\"restart_viper\": \"Restart Viper\",\n\t\t\t\t\t\"change_gamepath\": \"Change gamepath\",\n\t\t\t\t\t\"force_quit_game\": \"Force quit Titanfall and Northstar\",\n\t\t\t\t\t\"force_quit_origin\": \"Force quit Origin and or EA Desktop\"\n\t\t\t\t},\n\n\t\t\t\t\"open_gamepath_alert\": \"No valid gamepath is selected, so there's no gamepath to open, please select a valid gamepath first!\",\n\t\t\t\t\"reset_config_alert\": \"Please confirm that you want to reset the config file, after confirming Viper will delete the config file, and then restart.\"\n\t\t\t},\n\n\t\t\t\"linux_launch_method\": {\n\t\t\t\t\"title\": \"Linux launch method\",\n\t\t\t\t\"desc\": \"The method to use when launching the game on Linux\",\n\t\t\t\t\"methods\": {\n\t\t\t\t\t\"steam_auto\": \"Steam (Auto)\",\n\t\t\t\t\t\"steam_executable\": \"Steam (Executable)\",\n\t\t\t\t\t\"steam_flatpak\": \"Steam (Flatpak)\",\n\t\t\t\t\t\"steam_protocol\": \"Steam (Protocol)\",\n\t\t\t\t\t\"command\": \"Custom command\"\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"linux_launch_cmd_ns\": {\n\t\t\t\t\"title\": \"Northstar launch command\",\n\t\t\t\t\"desc\": \"This is the command that will be used when you use \\\"Custom command\\\" as the launch method and you're launching Northstar, launch options are appended at the end of the command and in an environment variable named $TF_ARGS\"\n\t\t\t},\n\t\t\t\"linux_launch_cmd_vanilla\": {\n\t\t\t\t\"title\": \"Vanilla launch command\",\n\t\t\t\t\"desc\": \"This is the command that will be used when you use \\\"Custom command\\\" as the launch method and you're launching the vanilla game, launch options are appended at the end of the command and in an environment variable named $TF_ARGS\"\n\t\t\t}\n\t\t},\n\n\t\t\"nsupdate\": {\n\t\t\t\"gaming\": {\n\t\t\t\t\"title\": \"Northstar update available!\",\n\t\t\t\t\"body\": \"An update for Northstar is available.\\nYou can force its installation after closing the game.\"\n\t\t\t}\n\t\t},\n\n\t\t\"server\": {\n\t\t\t\"player\": \"player\",\n\t\t\t\"players\": \"players\",\n\t\t\t\"servers\": \"servers\",\n\t\t\t\"offline\": \"Masterserver is Offline\"\n\t\t},\n\n\t\t\"launch\": \"Launch\",\n\t\t\"install\": \"Install\",\n\n\t\t\"gamepath\": {\n\t\t\t\"must\": \"The game path must be set to start Viper.\",\n\t\t\t\"wrong\": \"This folder is not a valid game path.\",\n\t\t\t\"lost\": \"Gamepath no longer exists/can't be found!\\n\\nMake sure your drive is mounted properly, or if you moved your game location that you update the game path.\\n\\nViper may not work properly until next restart!\",\n\t\t\t\"lost_perms\": \"Your user seems to have lost permissions to create and or read files in the selected gamepath, please select a gamepath or gain access to read and write to the folder:\\n\\n\",\n\t\t\t\"found_missing_perms\": \"Automatically found a valid gamepath, however your user doesn't have permissions to create and or read files in it, please manually select a different gamepath or gain access to read and write to the folder:\\n\\n\",\n\t\t\t\"missing_perms\": \"Your user doesn't have permissions to create and or read files in the selected gamepath, please select a different gamepath or gain access to read and write to the folder:\\n\\n\"\n\t\t},\n\n\t\t\"toast\": {\n\t\t\t\"title\": {\n\t\t\t\t\"installed\": \"Mod installed!\",\n\t\t\t\t\"duped\": \"Duplicate folder names!\",\n\t\t\t\t\"failed\": \"Failed to install\",\n\t\t\t\t\"malformed\": \"Incorrect folder structure!\",\n\t\t\t\t\"unknown_error\": \"Unknown Error!\",\n\t\t\t\t\"no_internet\": \"No Internet\",\n\t\t\t\t\"failed_to_connect\": \"Failed to connect\",\n\t\t\t\t\"failed_launch_command\": \"Failed running launch command\",\n\t\t\t\t\"missing_launch_command\": \"Missing launch command\",\n\t\t\t\t\"missing_steam\": \"Missing Steam\",\n\t\t\t\t\"missing_flatpak\": \"Missing Flatpak\",\n\t\t\t\t\"missing_flatpak_steam\": \"Missing Flatpak Steam\"\n\t\t\t},\n\n\t\t\t\"desc\": {\n\t\t\t\t\"installed\": \"has been installed successfully!\",\n\t\t\t\t\"malformed\": \"has an incorrect folder structure, if you're the developer, you should fix this.\",\n\t\t\t\t\"failed\": \"An unknown error occurred while trying to install the mod. This may be the author's fault, and it may also be Viper's fault.\",\n\t\t\t\t\"duped\": \"has multiple mod folders in it, with the same name, causing duplicate folders, if you're the developer, you should fix this.\",\n\t\t\t\t\"unknown_error\": \"An unknown error occurred, click for more details. You may want to take a screenshot of the detailed error when filing a bug report.\",\n\t\t\t\t\"no_internet\": \"Viper may not work properly.\",\n\t\t\t\t\"failed_to_connect\": \"A connection could not be established to %s, check your internet, firewall or whether %s is currently down\",\n\t\t\t\t\"failed_launch_command\": \"Something went wrong whilst running the custom launch command\",\n\t\t\t\t\"missing_launch_command\": \"There's currently no custom launch command set, one has to be configured to launch\",\n\t\t\t\t\"missing_steam\": \"Can't launch with Steam directly, as it doesn't seem to be installed\",\n\t\t\t\t\"missing_flatpak\": \"Can't launch with Flatpak, as it doesn't seem to be installed\",\n\t\t\t\t\"missing_flatpak_steam\": \"Can't launch with the Flatpak version of Steam, as it doesn't seem to be installed\"\n\t\t\t}\n\t\t}\n\t},\n\n\t\"viper\": {\n\t\t\"already_running\": \"Viper is already running\",\n\n\t\t\"menu\": {\n\t\t\t\"main\": \"Viper\",\n\t\t\t\"release\": \"Release Notes\",\n\t\t\t\"info\": \"Extras\"\n\t\t},\n\n\t\t\"info\": {\n\t\t\t\"links\": \"Links\",\n\t\t\t\"credits\": \"Credits\",\n\t\t\t\"discord\": \"Join Discord:\",\n\t\t\t\"issues\": \"Report issues with Viper:\"\n\t\t}\n\t},\n\n\t\"ns\": {\n\t\t\"menu\": {\n\t\t\t\"main\": \"Northstar Launcher\",\n\t\t\t\"mods\": \"Mods\",\n\t\t\t\"release\": \"Release Notes\",\n\t\t\t\"force_quit\": \"Force quit game\"\n\t\t}\n\t},\n\n\t\"general\": {\n\t\t\"auto_updates\": {\n\t\t\t\"game_running\": \"Game is running, refusing to update Northstar\"\n\t\t},\n\n\t\t\"mods\": {\n\t\t\t\"enabled\": \"Enabled mods:\",\n\t\t\t\"disabled\": \"Disabled mods:\",\n\t\t\t\"installed\": \"Installed mods:\"\n\t\t},\n\n\t\t\"missing_path\": \"Game location could not be found automatically! Please select it manually!\",\n\t\t\"not_installed\": \"Northstar is not installed!\",\n\t\t\"running\": \"Running\",\n\t\t\"launching\": \"Launching\"\n\t},\n\n\t\"request\": {\n\t\t\"no_vp_release_notes\": \"<h3>Couldn't fetch Viper release notes.</h3>Try again later!\",\n\t\t\"no_ns_release_notes\": \"<h3>Couldn't fetch Northstar release notes.</h3>Try again later!\"\n\t},\n\n\t\"tooltip\": {\n\t\t\"close\": \"Close Viper\",\n\t\t\"minimize\": \"Minimize Viper\",\n\t\t\"settings\": \"Settings\",\n\t\t\"offline\": \"Internet Offline\",\n\t\t\"pages\": {\n\t\t\t\"viper\": \"Viper\",\n\t\t\t\"northstar\": \"Northstar\",\n\t\t\t\"titanfall\": \"Titanfall 2\"\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/lang/es.json",
    "content": "{\n\t\"cli\": {\n\t\t\"auto_updates\": {\n\t\t\t\"available\": \"¡Actualización de Northsar disponible!\",\n\t\t\t\"checking\": \"Buscando actualizaciones de Northstar...\",\n\t\t\t\"no_update\": \"No hay actualizaciones de Northstar disponibles.\",\n\t\t\t\"updating_ns\": \"Lanzando proceso de actualización...\"\n\t\t},\n\t\t\"gamepath\": {\n\t\t\t\"lost\": \"El directorio del juego no fue encontrado, ¡asegúrate de que esté disponible!\"\n\t\t},\n\t\t\"help\": {\n\t\t\t\"cli\": \"obliga la linea de comandos a habilitarse\",\n\t\t\t\"devtools\": \"abre las herramientas de desarrollador/depuración \",\n\t\t\t\"help\": \"muestra este mensaje de ayuda\",\n\t\t\t\"install_mod\": \"instala una modificación, desde una carpeta o zip\",\n\t\t\t\"no_vp_updates\": \"sobrescribe viper.json y deshabilita las actualizaciones de Viper\",\n\t\t\t\"remove_mod\": \"remueve una modificación\",\n\t\t\t\"setpath\": \"establece la ruta del juego\",\n\t\t\t\"toggle_mod\": \"alterna el estado de la modificación\",\n\t\t\t\"update\": \"actualiza Northstar desde la ruta de juego establecida\",\n\t\t\t\"update_vp\": \"actualiza Viper si es soportado\",\n\t\t\t\"version\": \"muestra la información de la versión\"\n\t\t},\n\t\t\"launch\": {\n\t\t\t\"linux_error\": \"La ejecución del juego en Linux aun no está implementada\"\n\t\t},\n\t\t\"mods\": {\n\t\t\t\"cant_find\": \"¡No se encuentra una modificación con ese nombre!\",\n\t\t\t\"failed\": \"¡Fallo al instalar el mod!\",\n\t\t\t\"improper_json\": \"%s's mod.json tiene errores de formato\",\n\t\t\t\"installed\": \"¡Modificiación instalada exitosamente!\",\n\t\t\t\"not_a_mod\": \"La carpeta o el archivo seleccionado no es una modificación\",\n\t\t\t\"removed\": \"¡Modificación removida exitosamente!\",\n\t\t\t\"toggled\": \"¡El estado de la modificación ha cambiado exitosamente!\",\n\t\t\t\"toggled_all\": \"¡El estado de todas las modifiaciones ha sido cambiado exitosamente!\"\n\t\t},\n\t\t\"setpath\": {\n\t\t\t\"no_arg\": \"No se ha proporcionado ningún argumento para --setpath\"\n\t\t},\n\t\t\"update\": {\n\t\t\t\"checking\": \"Buscando actualizaciones...\",\n\t\t\t\"current\": \"Versión actual:\",\n\t\t\t\"download_done\": \"¡Descarga completa! Extrayendo...\",\n\t\t\t\"downloading\": \"Descargando...\",\n\t\t\t\"finished\": \"Instalación/Actualización completada!\",\n\t\t\t\"no_internet\": \"Sin conexión a internet\",\n\t\t\t\"uptodate\": \"La ultima versión (%s) ya está instalada, omitiendo actualización.\",\n\t\t\t\"uptodate_short\": \"Está actualizado\"\n\t\t}\n\t},\n\t\"general\": {\n\t\t\"auto_updates\": {\n\t\t\t\"game_running\": \"El juego está en ejecución, no se puede actualizar Northstar\"\n\t\t},\n\t\t\"launching\": \"Ejecutando\",\n\t\t\"missing_path\": \"¡La ruta del jueno no se ha podido encontrar automaticamente! ¡Por favor, elige la ruta manualmente!\",\n\t\t\"mods\": {\n\t\t\t\"disabled\": \"Modificaciones deshabilitadas:\",\n\t\t\t\"enabled\": \"Modificaciones habilitadas:\",\n\t\t\t\"installed\": \"Modificiaciones instaladas:\"\n\t\t},\n\t\t\"not_installed\": \"¡Northstar no se ha instalado!\",\n\t\t\"running\": \"Ejecutandose\"\n\t},\n\t\"gui\": {\n\t\t\"browser\": {\n\t\t\t\"end_of_list\": \"Todos los paquetes han sido cargados\",\n\t\t\t\"filter\": {\n\t\t\t\t\"client\": \"Del lado del cliente\",\n\t\t\t\t\"mods\": \"Modificaciones\",\n\t\t\t\t\"server\": \"Del lado del servidor\",\n\t\t\t\t\"skins\": \"Skins\"\n\t\t\t},\n\t\t\t\"guide\": \"Guía\",\n\t\t\t\"info\": \"Información\",\n\t\t\t\"install\": \"Instalar\",\n\t\t\t\"load_more\": \"Cargar más...\",\n\t\t\t\"loading\": \"Cargando modificaciones...\",\n\t\t\t\"made_by\": \"hecho por\",\n\t\t\t\"no_results\": \"Sin resultados...\",\n\t\t\t\"reinstall\": \"Re-Instalar\",\n\t\t\t\"update\": \"Actualizar\",\n\t\t\t\"view\": \"Ver\"\n\t\t},\n\t\t\"exit\": \"Salir\",\n\t\t\"gamepath\": {\n\t\t\t\"found_missing_perms\": \"Se encontró automáticamente una ruta de juego válida, pero tu usuario no tiene permisos para crear o leer archivos en ella. Por favor, selecciona manualmente una ruta de juego diferente o obtén acceso para leer y escribir en la carpeta:\\n\\n\",\n\t\t\t\"lost\": \"¡El directorio del juego ya no existe/no se puede encontrar!\\n\\nAsegúrate de que tu unidad esté instalada correctamente o, si cambiaste la ubicación del juego, actualiza la ruta del juego.\\n\\nViper puede no funcionar correctamente hasta el próximo reinicio!\",\n\t\t\t\"lost_perms\": \"Tu usuario parece haber perdido permisos para crear o leer archivos en la ruta de juego seleccionada. Por favor, selecciona una ruta de juego válida o recupera el acceso para leer y escribir en la carpeta:\\n\\n\",\n\t\t\t\"missing_perms\": \"Tu usuario no tiene permisos para crear o leer archivos en la ruta de juego seleccionada. Por favor, selecciona una ruta de juego diferente o obtén acceso para leer y escribir en la carpeta:\\n\\n\",\n\t\t\t\"must\": \"La ruta del juego debe establecerse para ejecutar Viper.\",\n\t\t\t\"wrong\": \"Esta carpeta no es una ruta válida para el juego.\"\n\t\t},\n\t\t\"install\": \"Instalar\",\n\t\t\"launch\": \"Ejecutar\",\n\t\t\"mods\": {\n\t\t\t\"confirm_dependencies\": \"Este paquete tiene dependencias, se muestran abajo. Presionar \\\"Ok\\\" instalará el paquete y las dependencias.\\n\\n\",\n\t\t\t\"confirm_plugins\": {\n\t\t\t\t\"description\": \"Los complementos nativos tienen mucho más acceso al sistema que un mod regular y, por lo tanto, son inherentemente menos seguros de instalar, ya que un complemento malicioso podría causar mucho más daño de esta manera. Si este complemento es de un desarrollador confiable o similar o si sabe lo que está haciendo, entonces puede ignorar completamente este mensaje.\",\n\t\t\t\t\"title\": \"El siguiente paquete tiene complementos nativos:\"\n\t\t\t},\n\t\t\t\"count\": \"Modificaciones instaladas:\",\n\t\t\t\"disabled_tag\": \"Deshabilitado\",\n\t\t\t\"drag_n_drop\": \"Arrastra y suelta una modificación para instalarla\",\n\t\t\t\"extracting\": \"Extrayendo modificación...\",\n\t\t\t\"find\": \"Buscar modificaciones\",\n\t\t\t\"install\": \"Instalar modificación\",\n\t\t\t\"installed_mod\": \"¡Modificación instalada!\",\n\t\t\t\"installing\": \"Instalando modificación...\",\n\t\t\t\"not_a_mod\": \"¡No es una modificación!\",\n\t\t\t\"nothing_selected\": \"No has seleccionado una modificación.\",\n\t\t\t\"remove\": \"Remover\",\n\t\t\t\"remove_all\": \"Remover todo\",\n\t\t\t\"remove_all_confirm\": \"Eliminar todas las modificaciones generalmente requerirá que reinstales Northstar. ¿Está seguro?\",\n\t\t\t\"required_confirm\": \"Ha seleccionado un mod esencial, es posible que Northstar no funcione sin él. ¿Está seguro?\",\n\t\t\t\"title\": \"Modificaciones (Mods)\",\n\t\t\t\"toggle_all\": \"Activar / Desactivar todo\",\n\t\t\t\"toggle_all_confirm\": \"Alternar todo podría deshabilitar las modificaciones requeridas para que Northstar funcione. ¿Está seguro?\",\n\t\t\t\"unknown_author\": \"Desconocido\"\n\t\t},\n\t\t\"nsupdate\": {\n\t\t\t\"gaming\": {\n\t\t\t\t\"body\": \"Una actualización de northstar está disponible.\\nPuedes forzar su instalación despues de cerrar el juego.\",\n\t\t\t\t\"title\": \"¡Actualización de Northstar disponible!\"\n\t\t\t}\n\t\t},\n\t\t\"search\": \"Buscar...\",\n\t\t\"server\": {\n\t\t\t\"offline\": \"El servidor principal está desconectado\",\n\t\t\t\"player\": \"jugador\",\n\t\t\t\"players\": \"jugadores\",\n\t\t\t\"servers\": \"servidores\"\n\t\t},\n\t\t\"setpath\": \"Cambiar la ruta del juego\",\n\t\t\"settings\": {\n\t\t\t\"autolang\": {\n\t\t\t\t\"desc\": \"Cuando está habilitado, Viper intenta detectar automáticamente el idioma de su sistema, cuando está deshabilitado, puede cambiar manualmente el idioma a continuación.\",\n\t\t\t\t\"title\": \"Detectar automáticamente el idioma\"\n\t\t\t},\n\t\t\t\"autoupdate\": {\n\t\t\t\t\"desc\": \"Viper se mantendrá automáticamente actualizado.\",\n\t\t\t\t\"title\": \"Actualizaciones automáticas de Viper\"\n\t\t\t},\n\t\t\t\"discard\": \"Descartar\",\n\t\t\t\"excludes\": {\n\t\t\t\t\"desc\": \"Cuando se actualice Northstar, los archivos especificados aquí no se sobrescribirán con archivos de la nueva actualización de Northstar. A menos que sepa lo que está cambiando, probablemente no debería cambiar nada aquí. Cada archivo se debe separar con un espacio.\",\n\t\t\t\t\"title\": \"Conservar archivos en la actualización\"\n\t\t\t},\n\t\t\t\"forcedlang\": {\n\t\t\t\t\"desc\": \"Cuando \\\"Detectar automáticamente el idioma\\\" está deshabilitado, ésta opción decidirá el lenguaje. Se necesita reiniciar para que surja efecto.\",\n\t\t\t\t\"title\": \"Idioma\"\n\t\t\t},\n\t\t\t\"linux_launch_cmd_ns\": {\n\t\t\t\t\"desc\": \"Este es el comando que será usado cuando seleccione \\\"Comando personalizado\\\" como el método de ejecución al lanzar Northstar. Las opciones de ejecución seran añadidas al final de dicho comando y en una variable de entorno llamada $TF_ARGS\",\n\t\t\t\t\"title\": \"Comando de lanzamiento para Northstar\"\n\t\t\t},\n\t\t\t\"linux_launch_cmd_vanilla\": {\n\t\t\t\t\"desc\": \"Este es el comando que será usado cuando seleccione \\\"Comando personalizado\\\" como el método de ejecución al lanzar la versión \\\"Vanilla\\\" del juego. Las opciones de ejecución seran añadidas al final de dicho comando y en una variable de entorno llamada $TF_ARGS\",\n\t\t\t\t\"title\": \"Comando de ejecución para \\\"Vanilla\\\"\"\n\t\t\t},\n\t\t\t\"linux_launch_method\": {\n\t\t\t\t\"desc\": \"El método a usarse cuando ejecute el juego en Linux\",\n\t\t\t\t\"methods\": {\n\t\t\t\t\t\"command\": \"Comando personalizado\",\n\t\t\t\t\t\"steam_auto\": \"Steam (Automático)\",\n\t\t\t\t\t\"steam_executable\": \"Steam (Ejecutable)\",\n\t\t\t\t\t\"steam_flatpak\": \"Steam (Flatpak)\",\n\t\t\t\t\t\"steam_protocol\": \"Steam (Protocolo)\"\n\t\t\t\t},\n\t\t\t\t\"title\": \"Modo de ejecución en Linux\"\n\t\t\t},\n\t\t\t\"miscbuttons\": {\n\t\t\t\t\"buttons\": {\n\t\t\t\t\t\"change_gamepath\": \"Cambiar ruta del juego\",\n\t\t\t\t\t\"force_quit_game\": \"Cerrar forzosamente Titanfall y Northstar\",\n\t\t\t\t\t\"force_quit_origin\": \"Cerrar forzosamente Origin y/o EA Desktop\",\n\t\t\t\t\t\"open_gamepath\": \"Abrir ruta del juego\",\n\t\t\t\t\t\"reset_config\": \"Restablecer archivo de configuración\",\n\t\t\t\t\t\"restart_viper\": \"Reiniciar Viper\"\n\t\t\t\t},\n\t\t\t\t\"desc\": \"Si estás teniendo problemas, entonces algunos de estos botones pueden ayudar a solucionarlo\",\n\t\t\t\t\"open_gamepath_alert\": \"No se ha seleccionado una ruta de juego válida, por lo que no hay una ruta de juego para abrir. ¡Por favor, selecciona primero una ruta de juego válida!\",\n\t\t\t\t\"reset_config_alert\": \"Por favor, confirma que deseas restablecer el archivo de configuración. Después de confirmar, Viper eliminará el archivo de configuración y luego reiniciará.\",\n\t\t\t\t\"title\": \"Acciones de reparación diversas\"\n\t\t\t},\n\t\t\t\"nsargs\": {\n\t\t\t\t\"desc\": \"Aqui puedes añadir opciones de lanzamiento para Northstar/Titanfall.\",\n\t\t\t\t\"title\": \"Opciones de lanzamiento\"\n\t\t\t},\n\t\t\t\"nsupdate\": {\n\t\t\t\t\"desc\": \"Viper mantendrá Northstar actualizado automáticamente, sin embargo, todavía se puede actualizar manualmente a través de la sección de Northstar.\",\n\t\t\t\t\"title\": \"Actualizaciones automáticas de Northstar\"\n\t\t\t},\n\t\t\t\"originkill\": {\n\t\t\t\t\"desc\": \"Cuando Viper se cierra, cerrar junto a Origin o la aplicación de EA\",\n\t\t\t\t\"title\": \"Cerrar Origin o la aplicación de EA automáticamente\"\n\t\t\t},\n\t\t\t\"save\": \"Guardar\",\n\t\t\t\"title\": {\n\t\t\t\t\"language\": \"Idioma\",\n\t\t\t\t\"linux\": \"Linux\",\n\t\t\t\t\"misc\": \"Misceláneos\",\n\t\t\t\t\"ns\": \"Northstar\",\n\t\t\t\t\"updates\": \"Actualizaciones\"\n\t\t\t},\n\t\t\t\"updatebuttons\": {\n\t\t\t\t\"buttons\": {\n\t\t\t\t\t\"force_delete_install_cache\": \"Forzar la eliminación de archivos de instalación en caché\",\n\t\t\t\t\t\"force_northstar_reinstall\": \"Forzar la reinstalación de Northstar\",\n\t\t\t\t\t\"reset_cached_api_requests\": \"Restablecer las solicitudes de API en caché\"\n\t\t\t\t},\n\t\t\t\t\"desc\": \"Si estás teniendo problemas con las actualizaciones, algunos de estos botones pueden ayudar a solucionarlos.\",\n\t\t\t\t\"title\": \"Opciones de reparación\"\n\t\t\t}\n\t\t},\n\t\t\"toast\": {\n\t\t\t\"desc\": {\n\t\t\t\t\"duped\": \"¡Nombres de las carpetas duplicados!\",\n\t\t\t\t\"failed\": \"Se produjo un error desconocido al intentar instalar la modificación. Esto puede ser culpa del autor de la modificación, y también puede ser culpa de Viper.\",\n\t\t\t\t\"installed\": \"¡Ha sido instalado exitosamente!\",\n\t\t\t\t\"malformed\": \"tiene una estructura de carpetas incorrecta, si usted es el desarrollador, debe corregir esto.\",\n\t\t\t\t\"no_internet\": \"Viper puede funcionar de forma incorrecta.\",\n\t\t\t\t\"unknown_error\": \"Ha ocurrido un error desconocido, presiona para más detalles. Recomendamos que tomes una captura de pantalla del error con sus detalles cuando reportes un error.\"\n\t\t\t},\n\t\t\t\"title\": {\n\t\t\t\t\"duped\": \"tiene varias carpetas de la modificación con el mismo nombre, lo que genera carpetas duplicadas. Si eres el desarrollador, deberías arreglar esto.\",\n\t\t\t\t\"failed\": \"¡Falló al instalar!\",\n\t\t\t\t\"failed_launch_command\": \"Error al ejecutar el comando de lanzamiento\",\n\t\t\t\t\"installed\": \"¡Modificación instalada!\",\n\t\t\t\t\"malformed\": \"¡Estructura de las carpetas incorrecta!\",\n\t\t\t\t\"missing_launch_command\": \"El comando de lanzamiento no fue asignado o no se encuentra\",\n\t\t\t\t\"no_internet\": \"Sin Internet\",\n\t\t\t\t\"unknown_error\": \"¡Error desconocido!\"\n\t\t\t}\n\t\t},\n\t\t\"update\": {\n\t\t\t\"available\": \"Hay una actualización disponible para Viper, ¿desea reiniciar y aplicarla?\",\n\t\t\t\"button\": \"Actualizar\",\n\t\t\t\"check\": \"Buscar actualizaciones\",\n\t\t\t\"downloading\": \"Descargando...\",\n\t\t\t\"extracting\": \"Extrayendo actualización...\",\n\t\t\t\"finished\": \"¡Hecho! ¡Está listo para jugar!\",\n\t\t\t\"uptodate\": \"¡Ya está actualizado!\"\n\t\t},\n\t\t\"versions\": {\n\t\t\t\"northstar\": \"Versión de Northstar\",\n\t\t\t\"viper\": \"Versión de Viper\"\n\t\t},\n\t\t\"welcome\": \"Bienvenido a Viper!\"\n\t},\n\t\"lang\": {\n\t\t\"title\": \"Spanish - Español\"\n\t},\n\t\"ns\": {\n\t\t\"menu\": {\n\t\t\t\"force_quit\": \"Cerrar forzosamente el juego\",\n\t\t\t\"main\": \"Northstar Launcher\",\n\t\t\t\"mods\": \"Modificaciones\",\n\t\t\t\"release\": \"Notas de actualización\"\n\t\t}\n\t},\n\t\"request\": {\n\t\t\"no_ns_release_notes\": \"<h3>No se pudo encontrar las notas de lanzamiento de Northstar.</h3>¡Intenta mas tarde!\",\n\t\t\"no_vp_release_notes\": \"<h3>No se pudo encontrar las notas de lanzamiento de Viper.</h3>¡Intenta mas tarde!\"\n\t},\n\t\"tooltip\": {\n\t\t\"close\": \"Cerrar Viper\",\n\t\t\"minimize\": \"Minimizar Viper\",\n\t\t\"pages\": {\n\t\t\t\"northstar\": \"Northstar\",\n\t\t\t\"titanfall\": \"Titanfall 2\",\n\t\t\t\"viper\": \"Viper\"\n\t\t},\n\t\t\"settings\": \"Configuraciones\"\n\t},\n\t\"viper\": {\n\t\t\"info\": {\n\t\t\t\"credits\": \"Creditos\",\n\t\t\t\"discord\": \"Unete al Discord:\",\n\t\t\t\"issues\": \"Reporta problemas de Viper:\",\n\t\t\t\"links\": \"Links\"\n\t\t},\n\t\t\"menu\": {\n\t\t\t\"info\": \"Extras\",\n\t\t\t\"main\": \"Viper\",\n\t\t\t\"release\": \"Notas de la versión\"\n\t\t}\n\t}\n}"
  },
  {
    "path": "src/lang/fr.json",
    "content": "{\n\t\"cli\": {\n\t\t\"auto_updates\": {\n\t\t\t\"available\": \"Mise à jour Northstar disponible !\",\n\t\t\t\"checking\": \"Vérification des mises à jour pour Northstar...\",\n\t\t\t\"no_update\": \"Pas de mise à jour Northstar disponible.\",\n\t\t\t\"updating_ns\": \"Lancement de la mise à jour...\"\n\t\t},\n\t\t\"gamepath\": {\n\t\t\t\"lost\": \"Chemin du jeu non trouvé, vérifiez qu'il est bien accessible !\"\n\t\t},\n\t\t\"help\": {\n\t\t\t\"cli\": \"Force l'activation de la CLI\",\n\t\t\t\"devtools\": \"Ouvre les outils de développement/débugging\",\n\t\t\t\"help\": \"affiche ce message d'aide\",\n\t\t\t\"install_mod\": \"Installe un mod, dossier ou zip\",\n\t\t\t\"no_vp_updates\": \"écrase viper.json et désactive les mises à jour de Viper\",\n\t\t\t\"remove_mod\": \"Supprime un mod\",\n\t\t\t\"setpath\": \"enregistre le chemin du client de jeu\",\n\t\t\t\"toggle_mod\": \"Active/désactive un mod\",\n\t\t\t\"update\": \"met à jour Northstar sur le chemin du jeu précisé\",\n\t\t\t\"update_vp\": \"met à jour le client Viper, si le format actuel le permet.\",\n\t\t\t\"version\": \"retourne des informations sur la version du logiciel\"\n\t\t},\n\t\t\"launch\": {\n\t\t\t\"linux_error\": \"Le support du jeu sur Linux n'est pas encore implémenté.\"\n\t\t},\n\t\t\"mods\": {\n\t\t\t\"cant_find\": \"Aucun mod avec ce nom n'a pu être trouvé.\",\n\t\t\t\"failed\": \"L'installation du mod a échoué.\",\n\t\t\t\"improper_json\": \"Le mod.json de %s présente des erreurs de formatage.\",\n\t\t\t\"installed\": \"Mod installé !\",\n\t\t\t\"not_a_mod\": \"Le fichier/dossier sélectionné n'est pas un mod.\",\n\t\t\t\"removed\": \"Le mod a bien été supprimé.\",\n\t\t\t\"toggled\": \"Le mod a bien été activé/désactivé.\",\n\t\t\t\"toggled_all\": \"Tous les mods ont bien été activés/désactivés.\"\n\t\t},\n\t\t\"setpath\": {\n\t\t\t\"no_arg\": \"Aucun argument donné à --setpath\"\n\t\t},\n\t\t\"update\": {\n\t\t\t\"checking\": \"Vérification des mises à jour...\",\n\t\t\t\"current\": \"Version actuelle :\",\n\t\t\t\"download_done\": \"Téléchargement terminé ! Extraction des fichiers...\",\n\t\t\t\"downloading\": \"Téléchargement en cours...\",\n\t\t\t\"failed\": \"Échec de la mise à jour !\",\n\t\t\t\"finished\": \"Mise à jour terminée !\",\n\t\t\t\"no_internet\": \"Pas de connexion Internet\",\n\t\t\t\"uptodate\": \"La dernière version (%s) est déjà installée.\",\n\t\t\t\"uptodate_short\": \"Votre client est à jour\"\n\t\t}\n\t},\n\t\"general\": {\n\t\t\"auto_updates\": {\n\t\t\t\"game_running\": \"Le jeu est en cours d'exécution, impossible de lancer la mise à jour.\"\n\t\t},\n\t\t\"launching\": \"Lancement\",\n\t\t\"missing_path\": \"Le chemin du client n'a pu être trouvé automatiquement, merci de le sélectionner manuellement.\",\n\t\t\"mods\": {\n\t\t\t\"disabled\": \"Mods désactivés :\",\n\t\t\t\"enabled\": \"Mods activés :\",\n\t\t\t\"installed\": \"Mods installés :\"\n\t\t},\n\t\t\"not_installed\": \"Northstar n'est pas installé !\",\n\t\t\"running\": \"En cours d'exécution\"\n\t},\n\t\"gui\": {\n\t\t\"browser\": {\n\t\t\t\"end_of_list\": \"Fin de la liste de mods.<br>Utilisez la barre de recherche pour en trouver davantage !\",\n\t\t\t\"filter\": {\n\t\t\t\t\"client\": \"Côté client\",\n\t\t\t\t\"mods\": \"Mods\",\n\t\t\t\t\"server\": \"Côté serveur\",\n\t\t\t\t\"skins\": \"Skins\"\n\t\t\t},\n\t\t\t\"guide\": \"Guide\",\n\t\t\t\"info\": \"Info\",\n\t\t\t\"install\": \"Installer\",\n\t\t\t\"load_more\": \"Charger plus de mods...\",\n\t\t\t\"loading\": \"Chargement des mods...\",\n\t\t\t\"made_by\": \"par\",\n\t\t\t\"no_results\": \"Pas de résultat\",\n\t\t\t\"reinstall\": \"Réinstaller\",\n\t\t\t\"sort\": {\n\t\t\t\t\"highest_rating\": \"Mieux noté\",\n\t\t\t\t\"last_updated\": \"Récemment mis à jour\",\n\t\t\t\t\"most_downloads\": \"Plus téléchargé\",\n\t\t\t\t\"newest\": \"Nouveau\"\n\t\t\t},\n\t\t\t\"update\": \"Mise à jour\",\n\t\t\t\"view\": \"Voir\"\n\t\t},\n\t\t\"exit\": \"Fermer\",\n\t\t\"gamepath\": {\n\t\t\t\"found_missing_perms\": \"Chemin du jeu automatiquement trouvé, cependant l'utilisateur courant n'a pas les permissions de lecture/écriture de fichiers dans le dossier sélectionné, veuillez manuellement sélectionner un autre dossier ou accorder les permissions en lecture/écriture à l'utilisateur :\\n\\n\",\n\t\t\t\"lost\": \"Le chemin du jeu ne peut être trouvé / n'existe plus !\\n\\nVeuillez vérifier que votre disqué est correctement monté, ou, si vous avez déplacé votre jeu, que vous avez mis à jour le chemin du dossier.\\n\\nViper ne fonctionnera pas correctement jusqu'au prochain redémarrage.\",\n\t\t\t\"lost_perms\": \"L'utilisateur semble avoir perdu les permissions de lecture/écriture de fichiers dans le dossier sélectionné, veuillez sélectionner un autre dossier ou accorder les permissions en lecture/écriture à l'utilisateur :\\n\\n\",\n\t\t\t\"missing_perms\": \"L'utilisateur courant n'a pas les permissions de lecture/écriture de fichiers dans le dossier sélectionné, veuillez manuellement sélectionner un autre dossier ou accorder les permissions en lecture/écriture à l'utilisateur :\\n\\n\",\n\t\t\t\"must\": \"Vous devez sélectionner le chemin du dossier du jeu Titanfall 2 pour pouvoir lancer Viper.\",\n\t\t\t\"wrong\": \"Ce dossier ne contient pas le jeu Titanfall 2, et n'est donc pas valide.\"\n\t\t},\n\t\t\"install\": \"Installer\",\n\t\t\"launch\": \"Jouer\",\n\t\t\"mods\": {\n\t\t\t\"cant_find_specific\": \"Incapable de trouver le mod %s-%s!\",\n\t\t\t\"cant_find_version\": \"Incapable de trouver la version %s du mod %s-%s!\",\n\t\t\t\"confirm_dependencies\": \"Ce mod a des dépendances (affichées ci-dessous), cliquer \\\"Ok\\\" les installera en même temps que le mod.\\n\\n\",\n\t\t\t\"confirm_plugins\": {\n\t\t\t\t\"description\": \"Les plugins ont des accès à votre système, comparés aux mods classiques, et sont de fait plus dangereux à l'installation, comme pourrait l'être un plugin contenant un malware. Si ce plugin provient d'un tiers de confiance ou si vous savez ce que vous faites, ne tenez pas compte de ce message.\",\n\t\t\t\t\"title\": \"Ce mod contient des plugins :\"\n\t\t\t},\n\t\t\t\"count\": \"Mods installés :\",\n\t\t\t\"disabled_tag\": \"Désactivé\",\n\t\t\t\"drag_n_drop\": \"Glissez/déposez un mod pour l'installer\",\n\t\t\t\"extracting\": \"Extraction du mod...\",\n\t\t\t\"find\": \"Chercher des mods\",\n\t\t\t\"install\": \"Installer le mod\",\n\t\t\t\"installed_mod\": \"Mod installé !\",\n\t\t\t\"installing\": \"Installation du mod...\",\n\t\t\t\"not_a_mod\": \"Ceci n'est pas un mod !\",\n\t\t\t\"nothing_selected\": \"Aucun mod n'est sélectionné.\",\n\t\t\t\"remove\": \"Supprimer\",\n\t\t\t\"remove_all\": \"Tout supprimer\",\n\t\t\t\"remove_all_confirm\": \"Supprimer tous les mods vous forcera à réinstaller Northstar, souhaitez-vous faire cela ?\",\n\t\t\t\"required_confirm\": \"Vous avez sélectionné un mod de base, Northstar peut ne pas fonctionner sans celui-ci. Souhaitez-vous faire cela ?\",\n\t\t\t\"title\": \"Mods\",\n\t\t\t\"toggle_all\": \"Activer/désactiver tous les mods\",\n\t\t\t\"toggle_all_confirm\": \"Cette action pourrait désactiver des mods nécessaires au bon fonctionnement de Northstar. Souhaitez-vous faire cela ?\",\n\t\t\t\"unknown_author\": \"Inconnu\"\n\t\t},\n\t\t\"nsupdate\": {\n\t\t\t\"gaming\": {\n\t\t\t\t\"body\": \"Une mise à jour pour Northstar est disponible.\\nVous pourrez l'installer après avoir fermé le jeu.\",\n\t\t\t\t\"title\": \"Mise à jour Northstar disponible !\"\n\t\t\t}\n\t\t},\n\t\t\"search\": \"Rechercher\",\n\t\t\"server\": {\n\t\t\t\"offline\": \"Le serveur maître est hors-ligne\",\n\t\t\t\"player\": \"joueur\",\n\t\t\t\"players\": \"joueurs\",\n\t\t\t\"servers\": \"serveurs\"\n\t\t},\n\t\t\"setpath\": \"Mettre à jour le chemin du jeu\",\n\t\t\"settings\": {\n\t\t\t\"autolang\": {\n\t\t\t\t\"desc\": \"Lorsque activée, Viper essaie de déterminer automatiquement la langue de votre système ; désactiver cette option vous permet de sélectionner manuellement la langue utilisée.\",\n\t\t\t\t\"title\": \"Auto-détection de la langue\"\n\t\t\t},\n\t\t\t\"autoupdate\": {\n\t\t\t\t\"desc\": \"Viper se tient automatiquement à jour.\",\n\t\t\t\t\"title\": \"Mises à jour pour Viper\"\n\t\t\t},\n\t\t\t\"discard\": \"Annuler\",\n\t\t\t\"excludes\": {\n\t\t\t\t\"desc\": \"Lorsque Northstar est mis à jour, ces fichiers ne seront pas écrasés par ceux provenant de la mise à jour; les noms de fichiers sont séparés par un espace.\",\n\t\t\t\t\"title\": \"Fichiers à conserver\"\n\t\t\t},\n\t\t\t\"forcedlang\": {\n\t\t\t\t\"desc\": \"Lorsque \\\"Auto-détection de la langue\\\" est désactivée, cette option permet de sélectionner la langue (requiert un redémarrage).\",\n\t\t\t\t\"title\": \"Langue\"\n\t\t\t},\n\t\t\t\"linux_launch_cmd_ns\": {\n\t\t\t\t\"desc\": \"Commande qui sera utilisée lorsque vous sélectionnez \\\"Commande personnalisée\\\" comme méthode de lancement de Northstar ; les arguments de démarrage sont ajoutés à la fin de cette commande et dans une variable d'environnement nommée $TF_ARGS\",\n\t\t\t\t\"title\": \"Commande de lancement de Northstar\"\n\t\t\t},\n\t\t\t\"linux_launch_cmd_vanilla\": {\n\t\t\t\t\"desc\": \"Commande qui sera utilisée lorsque vous sélectionnez \\\"Commande personnalisée\\\" comme méthode de lancement vanilla ; les arguments de démarrage sont ajoutés à la fin de cette commande et dans une variable d'environnement nommée $TF_ARGS\",\n\t\t\t\t\"title\": \"Commande de lancement vanilla\"\n\t\t\t},\n\t\t\t\"linux_launch_method\": {\n\t\t\t\t\"desc\": \"La façon de lancer le jeu sur Linux\",\n\t\t\t\t\"methods\": {\n\t\t\t\t\t\"command\": \"Commande personnalisée\",\n\t\t\t\t\t\"steam_auto\": \"Steam (auto)\",\n\t\t\t\t\t\"steam_executable\": \"Steam (exécutable)\",\n\t\t\t\t\t\"steam_flatpak\": \"Steam (Flatpak)\",\n\t\t\t\t\t\"steam_protocol\": \"Steam (protocole)\"\n\t\t\t\t},\n\t\t\t\t\"title\": \"Méthode de lancement sur Linux\"\n\t\t\t},\n\t\t\t\"miscbuttons\": {\n\t\t\t\t\"buttons\": {\n\t\t\t\t\t\"change_gamepath\": \"Changer le dossier du jeu\",\n\t\t\t\t\t\"force_quit_game\": \"Forcer la fermeture de Titanfall et Northstar\",\n\t\t\t\t\t\"force_quit_origin\": \"Forcer la fermeture d'Origin et/ou EA Desktop\",\n\t\t\t\t\t\"open_gamepath\": \"Ouvrir le dossier du jeu\",\n\t\t\t\t\t\"reset_config\": \"Réinitialiser le fichier de configuration\",\n\t\t\t\t\t\"restart_viper\": \"Redémarrer Viper\"\n\t\t\t\t},\n\t\t\t\t\"desc\": \"Si vous avez des problèmes de mises à jour, certaines de ces options pourraient vous aider.\",\n\t\t\t\t\"open_gamepath_alert\": \"Aucun chemin de jeu n'est sélectionné, et ne peut donc être ouvert ; veuillez en sélectionner un d'abord !\",\n\t\t\t\t\"reset_config_alert\": \"Veuillez confirmer que vous voulez réinitialiser le fichier de configuration ; après confirmation, Viper supprimera ce fichier et redémarrera.\",\n\t\t\t\t\"title\": \"Paramètres de récupération\"\n\t\t\t},\n\t\t\t\"nsargs\": {\n\t\t\t\t\"desc\": \"Vous pouvez ajouter ici des options de démarrage pour Northstar/Titanfall.\",\n\t\t\t\t\"title\": \"Options de lancement\"\n\t\t\t},\n\t\t\t\"nsupdate\": {\n\t\t\t\t\"desc\": \"Viper tient automatiquement Northstar à jour (n'empêche pas de le mettre à jour manuellement via sa page dédiée).\",\n\t\t\t\t\"title\": \"Mises à jour pour Northstar\"\n\t\t\t},\n\t\t\t\"originkill\": {\n\t\t\t\t\"desc\": \"Lorsque Viper est fermé, Origin et/ou EA app sera également automatiquement fermé.\",\n\t\t\t\t\"title\": \"Quitter automatiquement Origin et/ou EA app\"\n\t\t\t},\n\t\t\t\"save\": \"Sauvegarder\",\n\t\t\t\"title\": {\n\t\t\t\t\"language\": \"Langue\",\n\t\t\t\t\"linux\": \"Linux\",\n\t\t\t\t\"misc\": \"Divers\",\n\t\t\t\t\"ns\": \"Northstar\",\n\t\t\t\t\"updates\": \"Mises à jour\"\n\t\t\t},\n\t\t\t\"updatebuttons\": {\n\t\t\t\t\"buttons\": {\n\t\t\t\t\t\"force_delete_install_cache\": \"Supprimer le cache d'installation du jeu\",\n\t\t\t\t\t\"force_northstar_reinstall\": \"Forcer la réinstallation de Northstar\",\n\t\t\t\t\t\"reset_cached_api_requests\": \"Supprimer le cache local d'API\"\n\t\t\t\t},\n\t\t\t\t\"desc\": \"Si vous avez des problèmes de mises à jour, certaines de ces options pourraient vous aider.\",\n\t\t\t\t\"title\": \"Paramètres de récupération\"\n\t\t\t}\n\t\t},\n\t\t\"toast\": {\n\t\t\t\"desc\": {\n\t\t\t\t\"duped\": \"contient plusieurs dossiers ayant le même nom ; si vous êtes le développer, vous devriez réparer ceci.\",\n\t\t\t\t\"failed\": \"Une erreur inconnue est survenue lors de l'installation du mod. Cela peut être du ressort de l'auteur du mod ou de Viper.\",\n\t\t\t\t\"failed_launch_command\": \"Quelque chose s'est mal passé lors de l'exécution de la commande\",\n\t\t\t\t\"failed_to_connect\": \"Connexion à %s impossible, vérifiez votre connexion Internet, votre pare-feu ou vérifiez si %s est actuellement hors-ligne\",\n\t\t\t\t\"installed\": \"a été installé avec succès !\",\n\t\t\t\t\"malformed\": \"a une structure de dossier incorrecte ; si vous êtes son développeur, vous devriez réparer ça.\",\n\t\t\t\t\"missing_flatpak\": \"Lancement via Flatpak impossible (ne semble pas être installé)\",\n\t\t\t\t\"missing_flatpak_steam\": \"Lancement via Flatpak Steam impossible (ne semble pas être installé)\",\n\t\t\t\t\"missing_launch_command\": \"Il n'y a actuellement pas d'argument de lancement configuré, il en faut au moins un pour lancer le jeu\",\n\t\t\t\t\"missing_steam\": \"Lancement via Steam impossible (ne semble pas être installé)\",\n\t\t\t\t\"no_internet\": \"Viper ne fonctionnera pas correctement tant que la connexion n'est pas rétablie.\",\n\t\t\t\t\"unknown_error\": \"Une erreur inconnue est survenue, cliquez pour plus de détails. Vous devriez prendre une capture d'écran de l'erreur si vous comptez créer un ticket.\"\n\t\t\t},\n\t\t\t\"title\": {\n\t\t\t\t\"duped\": \"Nom de dossier dupliqué !\",\n\t\t\t\t\"failed\": \"L'installation a échoué\",\n\t\t\t\t\"failed_launch_command\": \"Echec du lancement de la commande\",\n\t\t\t\t\"failed_to_connect\": \"Echec de la connexion\",\n\t\t\t\t\"installed\": \"Mod installé !\",\n\t\t\t\t\"malformed\": \"La structure du dossier du mod est incorrecte.\",\n\t\t\t\t\"missing_flatpak\": \"Flatpak manquant\",\n\t\t\t\t\"missing_flatpak_steam\": \"Flatpak Steam manquant\",\n\t\t\t\t\"missing_launch_command\": \"Commande de lancement manquante\",\n\t\t\t\t\"missing_steam\": \"Steam manquant\",\n\t\t\t\t\"no_internet\": \"Pas de connexion Internet\",\n\t\t\t\t\"unknown_error\": \"Erreur inconnue\"\n\t\t\t}\n\t\t},\n\t\t\"update\": {\n\t\t\t\"available\": \"Une mise à jour pour Viper est disponible, voulez-vous l'installer maintenant ?\",\n\t\t\t\"button\": \"Mise à jour\",\n\t\t\t\"check\": \"Vérifier les mises à jour\",\n\t\t\t\"downloading\": \"Téléchargement de la mise à jour...\",\n\t\t\t\"extracting\": \"Extraction des fichiers...\",\n\t\t\t\"finished\": \"Terminé, vous pouvez jouer !\",\n\t\t\t\"uptodate\": \"Déjà à jour !\"\n\t\t},\n\t\t\"versions\": {\n\t\t\t\"northstar\": \"Version de Northstar\",\n\t\t\t\"viper\": \"Version de Viper\"\n\t\t},\n\t\t\"welcome\": \"Bienvenue sur Viper !\"\n\t},\n\t\"lang\": {\n\t\t\"title\": \"French - Français\"\n\t},\n\t\"ns\": {\n\t\t\"menu\": {\n\t\t\t\"force_quit\": \"Forcer la fermeture du jeu\",\n\t\t\t\"main\": \"Lanceur Northstar\",\n\t\t\t\"mods\": \"Mods\",\n\t\t\t\"release\": \"Notes de mises à jour\"\n\t\t}\n\t},\n\t\"request\": {\n\t\t\"no_ns_release_notes\": \"<h3>Impossible de récupérer les notes de mises à jour de Northstar.</h3>Veuillez réessayer plus tard.\",\n\t\t\"no_vp_release_notes\": \"<h3>Impossible de récupérer les notes de mises à jour de Viper.</h3>Veuillez réessayer plus tard.\"\n\t},\n\t\"tooltip\": {\n\t\t\"close\": \"Fermer Viper\",\n\t\t\"minimize\": \"Réduire Viper\",\n\t\t\"offline\": \"Internet déconnecté\",\n\t\t\"pages\": {\n\t\t\t\"northstar\": \"Northstar\",\n\t\t\t\"titanfall\": \"Titanfall 2\",\n\t\t\t\"viper\": \"Viper\"\n\t\t},\n\t\t\"settings\": \"Paramètres\"\n\t},\n\t\"viper\": {\n\t\t\"already_running\": \"Viper est déjà en cours d'éxecution\",\n\t\t\"info\": {\n\t\t\t\"credits\": \"Remerciements\",\n\t\t\t\"discord\": \"Rejoingnez le serveur Discord :\",\n\t\t\t\"issues\": \"Un problème avec Viper ? Créez un ticket ici :\",\n\t\t\t\"links\": \"Liens utiles\"\n\t\t},\n\t\t\"menu\": {\n\t\t\t\"info\": \"Informations\",\n\t\t\t\"main\": \"Viper\",\n\t\t\t\"release\": \"Notes de mises à jour\"\n\t\t}\n\t}\n}"
  },
  {
    "path": "src/lang/maintainers.json",
    "content": "{\n\t\"explanation\": \"This file is for storing contact information for the various maintainers of various languages/localizations\",\n\n\t\"list\": {\n\t\t\"es\": [\n\t\t\t\"https://github.com/XNovaDelta\",\n\t\t\t\"https://twitter.com/XNovaDelta\",\n\t\t\t\"https://www.facebook.com/Juanesgtgt2/\"\n\t\t],\n\n\t\t\"fr\": [\n\t\t\t\"https://github.com/Alystrasz\"\n\t\t],\n\n\t\t\"de\": [\n\t\t\t\"https://github.com/DxsSucuk\",\n\t\t\t\"https://twitter.com/memerinoto\"\n\t\t],\n\n\t\t\"zh\": [\n\t\t\t\"https://github.com/KenMizz\"\n\t\t]\n\t}\n}\n"
  },
  {
    "path": "src/lang/zh.json",
    "content": "{\n\t\"cli\": {\n\t\t\"auto_updates\": {\n\t\t\t\"available\": \"Northstar有可用的新更新！\",\n\t\t\t\"checking\": \"正在检查Northstar更新...\",\n\t\t\t\"no_update\": \"无可用的Northstar更新\",\n\t\t\t\"updating_ns\": \"启动更新进程中...\"\n\t\t},\n\t\t\"gamepath\": {\n\t\t\t\"lost\": \"没有找到游戏目录, 请确保它已经挂载!\"\n\t\t},\n\t\t\"help\": {\n\t\t\t\"cli\": \"强制开启CLI\",\n\t\t\t\"devtools\": \"打开开发者调试工具\",\n\t\t\t\"help\": \"显示该帮助信息\",\n\t\t\t\"install_mod\": \"安装一个模组, 文件夹, 或zip压缩包\",\n\t\t\t\"no_vp_updates\": \"覆盖vipers.json并禁止其更新\",\n\t\t\t\"remove_mod\": \"移除该模组\",\n\t\t\t\"setpath\": \"设置您的游戏目录\",\n\t\t\t\"toggle_mod\": \"开关该模组\",\n\t\t\t\"update\": \"从您设置的游戏目录更新Northstar\",\n\t\t\t\"update_vp\": \"如果支持的话, 自更新Viper.\",\n\t\t\t\"version\": \"输出版本信息\"\n\t\t},\n\t\t\"launch\": {\n\t\t\t\"linux_error\": \"当前不支持在Linux平台上启动游戏\"\n\t\t},\n\t\t\"mods\": {\n\t\t\t\"cant_find\": \"无法找到含有该名字的模组!\",\n\t\t\t\"failed\": \"模组安装失败!\",\n\t\t\t\"improper_json\": \"%s的mod.json含有格式错误\",\n\t\t\t\"installed\": \"安装模组成功!\",\n\t\t\t\"not_a_mod\": \"选中的文件夹/文件不是一个模组\",\n\t\t\t\"removed\": \"成功移除该模组!\",\n\t\t\t\"toggled\": \"成功开关该模组!\",\n\t\t\t\"toggled_all\": \"成功开关所有模组\"\n\t\t},\n\t\t\"setpath\": {\n\t\t\t\"no_arg\": \"没有为--setpath 提供argument\"\n\t\t},\n\t\t\"update\": {\n\t\t\t\"checking\": \"检查更新中...\",\n\t\t\t\"current\": \"当前版本:\",\n\t\t\t\"download_done\": \"下载完毕！解压中...\",\n\t\t\t\"downloading\": \"下载中...\",\n\t\t\t\"failed\": \"安装/更新 失败！\",\n\t\t\t\"finished\": \"安装/更新 完成！\",\n\t\t\t\"no_internet\": \"无互联网连接\",\n\t\t\t\"uptodate\": \"最新版本 (%s) 已安装, 跳过更新.\",\n\t\t\t\"uptodate_short\": \"当前是最新版本\"\n\t\t}\n\t},\n\t\"general\": {\n\t\t\"auto_updates\": {\n\t\t\t\"game_running\": \"游戏运行中, 拒绝更新Northstar\"\n\t\t},\n\t\t\"launching\": \"启动中\",\n\t\t\"missing_path\": \"无法找到游戏目录！请手动选择！\",\n\t\t\"mods\": {\n\t\t\t\"disabled\": \"禁用的模组:\",\n\t\t\t\"enabled\": \"启用的模组:\",\n\t\t\t\"installed\": \"已安装的模组:\"\n\t\t},\n\t\t\"not_installed\": \"没有安装Northstar!\",\n\t\t\"running\": \"运行中\"\n\t},\n\t\"gui\": {\n\t\t\"browser\": {\n\t\t\t\"end_of_list\": \"所有包全部加载完毕.\",\n\t\t\t\"filter\": {\n\t\t\t\t\"client\": \"客户端\",\n\t\t\t\t\"mods\": \"模组\",\n\t\t\t\t\"server\": \"服务器端\",\n\t\t\t\t\"skins\": \"皮肤\"\n\t\t\t},\n\t\t\t\"guide\": \"安装指南\",\n\t\t\t\"info\": \"详情\",\n\t\t\t\"install\": \"安装\",\n\t\t\t\"load_more\": \"加载更多...\",\n\t\t\t\"loading\": \"加载模组中...\",\n\t\t\t\"made_by\": \"来自\",\n\t\t\t\"no_results\": \"没有相关结果...\",\n\t\t\t\"reinstall\": \"重新安装\",\n\t\t\t\"sort\": {\n\t\t\t\t\"highest_rating\": \"最高评分\",\n\t\t\t\t\"last_updated\": \"最近更新\",\n\t\t\t\t\"most_downloads\": \"最多下载\",\n\t\t\t\t\"newest\": \"最新\"\n\t\t\t},\n\t\t\t\"update\": \"更新\",\n\t\t\t\"view\": \"查看\"\n\t\t},\n\t\t\"exit\": \"退出\",\n\t\t\"gamepath\": {\n\t\t\t\"found_missing_perms\": \"自动找到了一个有效的游戏目录, 然而您的用户没有对该目录的读写权限. 请手动指定一个不同的游戏目录或获取对该目录的读写权限:\\n\\n\",\n\t\t\t\"lost\": \"无法找到游戏目录!\\n\\n请确认您是否移动了游戏目录或硬盘是否被正确挂载\\n\\nViper在下一次重启前可能无法正常工作!\",\n\t\t\t\"lost_perms\": \"您的用户没有对当前设定的游戏目录创建和读取文件的权限, 请选择一个新的游戏目录或获取对该目录的读写权限:\\n\\n\",\n\t\t\t\"missing_perms\": \"您的用户没有对选中的游戏目录的读写权限, 请手动指定一个不同的游戏目录或获取对该目录的读写权限:\\n\\n\",\n\t\t\t\"must\": \"游戏目录必须被设置才能启动Viper.\",\n\t\t\t\"wrong\": \"该目录不是一个有效的游戏目录\"\n\t\t},\n\t\t\"install\": \"安装\",\n\t\t\"launch\": \"启动\",\n\t\t\"mods\": {\n\t\t\t\"cant_find_specific\": \"无法找到模组%s-%s！\",\n\t\t\t\"cant_find_version\": \"无法找到%s版的模组%s-%s！\",\n\t\t\t\"confirm_dependencies\": \"此包含有依赖项, 以下展示, 点击 \\\"Ok\\\" 将会安装此包和它的依赖项.\\n\\n\",\n\t\t\t\"confirm_plugins\": {\n\t\t\t\t\"description\": \"Native插件比普通的模组拥有更多对系统的访问权限, 如果安装了恶意插件可能会导致您的计算机受到损害. 如果这个插件是来自于一位信任的开发者或您知道自己在干什么, 那么请忽略这条信息.\",\n\t\t\t\t\"title\": \"以下包含有native插件:\"\n\t\t\t},\n\t\t\t\"count\": \"已安装的模组数:\",\n\t\t\t\"disabled_tag\": \"已禁用\",\n\t\t\t\"drag_n_drop\": \"将Mod拖至这里进行安装\",\n\t\t\t\"extracting\": \"解压模组中...\",\n\t\t\t\"find\": \"寻找模组\",\n\t\t\t\"install\": \"安装模组\",\n\t\t\t\"installed_mod\": \"模组安装成功!\",\n\t\t\t\"installing\": \"安装模组中...\",\n\t\t\t\"not_a_mod\": \"不是一个模组!\",\n\t\t\t\"nothing_selected\": \"您还没有选择一个模组.\",\n\t\t\t\"remove\": \"移除\",\n\t\t\t\"remove_all\": \"移除所有\",\n\t\t\t\"remove_all_confirm\": \"移除所有模组通常会需要您重新安装Northstar, 您确定吗?\",\n\t\t\t\"required_confirm\": \"您选择了一个核心模组, Northstar没有它可能无法工作, 您确定吗?\",\n\t\t\t\"title\": \"模组\",\n\t\t\t\"toggle_all\": \"开关所有\",\n\t\t\t\"toggle_all_confirm\": \"开关所有模组可能会导致使Northstar需要的模组被禁用, 您确定吗?\",\n\t\t\t\"unknown_author\": \"未知\"\n\t\t},\n\t\t\"nsupdate\": {\n\t\t\t\"gaming\": {\n\t\t\t\t\"body\": \"Northstar有一个可用的更新\\n您可以在退出游戏后强制安装\",\n\t\t\t\t\"title\": \"有可用的Northstar更新！\"\n\t\t\t}\n\t\t},\n\t\t\"search\": \"搜索...\",\n\t\t\"server\": {\n\t\t\t\"offline\": \"主服务器为离线状态\",\n\t\t\t\"player\": \"玩家\",\n\t\t\t\"players\": \"玩家\",\n\t\t\t\"servers\": \"服务器\"\n\t\t},\n\t\t\"setpath\": \"更换游戏目录\",\n\t\t\"settings\": {\n\t\t\t\"autolang\": {\n\t\t\t\t\"desc\": \"当开启后, Viper会尝试自动检测您的系统语言. 关闭此选项的话, 您可以在以下设置更换语言.\",\n\t\t\t\t\"title\": \"自动检测语言\"\n\t\t\t},\n\t\t\t\"autoupdate\": {\n\t\t\t\t\"desc\": \"Viper将会自动更新至最新版本.\",\n\t\t\t\t\"title\": \"Viper自动更新\"\n\t\t\t},\n\t\t\t\"discard\": \"取消\",\n\t\t\t\"excludes\": {\n\t\t\t\t\"desc\": \"当Northstar更新后, 在此指定的文件将不会被新的Northstar更新覆盖. 除非您知道自己在修改什么, 您可能不会想要改动任何东西. 每一个文件都以空格隔开.\",\n\t\t\t\t\"title\": \"在更新时保留文件\"\n\t\t\t},\n\t\t\t\"forcedlang\": {\n\t\t\t\t\"desc\": \"当 \\\"自动检测语言\\\" 被禁用后, 此设置将会作为Viper的语言. 需要重启Viper来生效.\",\n\t\t\t\t\"title\": \"语言\"\n\t\t\t},\n\t\t\t\"linux_launch_cmd_ns\": {\n\t\t\t\t\"desc\": \"当您以 \\\"自定义命令\\\" 选项启动Northstar客户端时，此项将会以$TF_ARGS的形式包含在启动命令的尾部作为Northstar的启动参数\",\n\t\t\t\t\"title\": \"Northstar启动参数\"\n\t\t\t},\n\t\t\t\"linux_launch_cmd_vanilla\": {\n\t\t\t\t\"desc\": \"当您以 \\\"自定义命令\\\" 选项启动原版客户端时，此项将会以$TF_ARGS的形式包含在启动命令的尾部作为原版的启动参数\",\n\t\t\t\t\"title\": \"原版启动参数\"\n\t\t\t},\n\t\t\t\"linux_launch_method\": {\n\t\t\t\t\"desc\": \"在Linux系统中启动游戏的方式\",\n\t\t\t\t\"methods\": {\n\t\t\t\t\t\"command\": \"自定义命令\",\n\t\t\t\t\t\"steam_auto\": \"Steam（自动）\",\n\t\t\t\t\t\"steam_executable\": \"Steam（程序）\",\n\t\t\t\t\t\"steam_flatpak\": \"Steam (Flatpak)\",\n\t\t\t\t\t\"steam_protocol\": \"Steam (Protocol)\"\n\t\t\t\t},\n\t\t\t\t\"title\": \"Linux启动方式\"\n\t\t\t},\n\t\t\t\"miscbuttons\": {\n\t\t\t\t\"buttons\": {\n\t\t\t\t\t\"change_gamepath\": \"更改游戏目录\",\n\t\t\t\t\t\"force_quit_game\": \"强制退出泰坦陨落2和Northstar\",\n\t\t\t\t\t\"force_quit_origin\": \"强制退出Origin和/或EA Desktop\",\n\t\t\t\t\t\"open_gamepath\": \"打开游戏目录\",\n\t\t\t\t\t\"reset_config\": \"重置配置文件\",\n\t\t\t\t\t\"restart_viper\": \"重启Viper\"\n\t\t\t\t},\n\t\t\t\t\"desc\": \"如果您遇到了问题,那么这些按钮或许会帮助您修复它们.\",\n\t\t\t\t\"open_gamepath_alert\": \"没有一个有效的游戏目录被选择, 所以它不能被打开. 请先选择一个有效的游戏目录!\",\n\t\t\t\t\"reset_config_alert\": \"请确认您想要重置配置文件, 确认后Viper将会删除配置文件并重启.\",\n\t\t\t\t\"title\": \"其他修复操作\"\n\t\t\t},\n\t\t\t\"nsargs\": {\n\t\t\t\t\"desc\": \"在这里您可以为Northstar/泰坦陨落添加启动选项\",\n\t\t\t\t\"title\": \"启动选项\"\n\t\t\t},\n\t\t\t\"nsupdate\": {\n\t\t\t\t\"desc\": \"Viper将会自动更新Northstar至最新版本, 不过您还是可以在Northstar页面下手动更新\",\n\t\t\t\t\"title\": \"Northstar自动更新\"\n\t\t\t},\n\t\t\t\"originkill\": {\n\t\t\t\t\"desc\": \"当Viper退出后, 同时自动退出Origin和/或EA Desktop\",\n\t\t\t\t\"title\": \"自动退出Origin和/或EA Desktop\"\n\t\t\t},\n\t\t\t\"save\": \"保存\",\n\t\t\t\"title\": {\n\t\t\t\t\"language\": \"语言\",\n\t\t\t\t\"linux\": \"Linux\",\n\t\t\t\t\"misc\": \"杂项\",\n\t\t\t\t\"ns\": \"Northstar\",\n\t\t\t\t\"updates\": \"更新\"\n\t\t\t},\n\t\t\t\"updatebuttons\": {\n\t\t\t\t\"buttons\": {\n\t\t\t\t\t\"force_delete_install_cache\": \"强制删除已缓存的安装文件\",\n\t\t\t\t\t\"force_northstar_reinstall\": \"强制重新安装Northstar\",\n\t\t\t\t\t\"reset_cached_api_requests\": \"重置缓存的API请求\"\n\t\t\t\t},\n\t\t\t\t\"desc\": \"如果您更新后遇到了问题, 这些按钮或许会帮助您修复它们.\",\n\t\t\t\t\"title\": \"修复操作\"\n\t\t\t}\n\t\t},\n\t\t\"toast\": {\n\t\t\t\"desc\": {\n\t\t\t\t\"duped\": \"存在多个同名的模组文件夹, 如果你是它的开发者, 你应该修复这个问题.\",\n\t\t\t\t\"failed\": \"在尝试安装该时发生了未知错误, 这可能是作者的问题, 也可能是Viper的问题\",\n\t\t\t\t\"failed_launch_command\": \"在运行自定义启动命令时发生了一些错误\",\n\t\t\t\t\"failed_to_connect\": \"无法连接至%s，请检查您的网络、防火墙或当前%s不可用\",\n\t\t\t\t\"installed\": \"已成功安装!\",\n\t\t\t\t\"malformed\": \"拥有错误的文件夹结构, 如果您是它的开发者, 您应该修复这个问题.\",\n\t\t\t\t\"missing_flatpak\": \"无法以Flatpak的方式启动，看起来它并没有被安装\",\n\t\t\t\t\"missing_flatpak_steam\": \"无法以Steam (Flatpak) 的方式启动，看起来它并没有被安装\",\n\t\t\t\t\"missing_launch_command\": \"当前缺少运行游戏的自定义命令，需指定来启动游戏\",\n\t\t\t\t\"missing_steam\": \"无法以Steam的方式启动，看起来它并没有被安装\",\n\t\t\t\t\"no_internet\": \"Viper可能不会正常工作.\",\n\t\t\t\t\"unknown_error\": \"一个未知错误产生了, 点击来查看更多细节. 您可能会想要对错误信息进行截图用于bug反馈\"\n\t\t\t},\n\t\t\t\"title\": {\n\t\t\t\t\"duped\": \"重复的文件夹名字!\",\n\t\t\t\t\"failed\": \"安装失败\",\n\t\t\t\t\"failed_launch_command\": \"运行启动命令失败\",\n\t\t\t\t\"failed_to_connect\": \"连接失败\",\n\t\t\t\t\"installed\": \"模组安装成功!\",\n\t\t\t\t\"malformed\": \"错误的文件夹结构!\",\n\t\t\t\t\"missing_flatpak\": \"缺少Flatpak\",\n\t\t\t\t\"missing_flatpak_steam\": \"缺少Steam（Flatpak)\",\n\t\t\t\t\"missing_launch_command\": \"缺少启动命令\",\n\t\t\t\t\"missing_steam\": \"缺少Steam\",\n\t\t\t\t\"no_internet\": \"没有互联网\",\n\t\t\t\t\"unknown_error\": \"未知错误!\"\n\t\t\t}\n\t\t},\n\t\t\"update\": {\n\t\t\t\"available\": \"Viper有一个可用的新更新, 您想要重启Viper并应用它吗?\",\n\t\t\t\"button\": \"更新\",\n\t\t\t\"check\": \"检查更新\",\n\t\t\t\"downloading\": \"下载中...\",\n\t\t\t\"extracting\": \"解压更新中...\",\n\t\t\t\"finished\": \"完成!\",\n\t\t\t\"uptodate\": \"已经为最新版本!\"\n\t\t},\n\t\t\"versions\": {\n\t\t\t\"northstar\": \"Northstar版本\",\n\t\t\t\"viper\": \"Viper版本\"\n\t\t},\n\t\t\"welcome\": \"欢迎来到Viper!\"\n\t},\n\t\"lang\": {\n\t\t\"title\": \"Chinese(Simplified) - 简体中文\"\n\t},\n\t\"ns\": {\n\t\t\"menu\": {\n\t\t\t\"force_quit\": \"强制退出游戏\",\n\t\t\t\"main\": \"Northstar启动器\",\n\t\t\t\"mods\": \"模组\",\n\t\t\t\"release\": \"更新日志\"\n\t\t}\n\t},\n\t\"request\": {\n\t\t\"no_ns_release_notes\": \"<h3>无法获取Northstar的更新日志.</h3>请稍后再试!\",\n\t\t\"no_vp_release_notes\": \"<h3>无法获取Viper的更新日志.</h3>请稍后再试!\"\n\t},\n\t\"tooltip\": {\n\t\t\"close\": \"关闭Viper\",\n\t\t\"minimize\": \"最小化Viper\",\n\t\t\"offline\": \"离线\",\n\t\t\"pages\": {\n\t\t\t\"northstar\": \"Northstar\",\n\t\t\t\"titanfall\": \"泰坦陨落2\",\n\t\t\t\"viper\": \"Viper\"\n\t\t},\n\t\t\"settings\": \"设置\"\n\t},\n\t\"viper\": {\n\t\t\"already_running\": \"Viper已在运行中\",\n\t\t\"info\": {\n\t\t\t\"credits\": \"鸣谢\",\n\t\t\t\"discord\": \"加入Discord:\",\n\t\t\t\"issues\": \"向Viper反馈问题:\",\n\t\t\t\"links\": \"链接\"\n\t\t},\n\t\t\"menu\": {\n\t\t\t\"info\": \"额外内容\",\n\t\t\t\"main\": \"Viper\",\n\t\t\t\"release\": \"更新日志\"\n\t\t}\n\t}\n}"
  },
  {
    "path": "src/lang.js",
    "content": "const fs = require(\"fs\");\n\nconst flat = require(\"flattenizer\");\nconst json = require(\"./modules/json\");\n\nconst enLang = flat.flatten(json(__dirname + \"/lang/en.json\"));\nlet lang = \"\";\nvar langObj = {};\n\nfunction _loadTranslation(forcedlang) {\n\tif (fs.existsSync(\"viper.json\")) {\n\t\t// Validate viper.json\n\t\tlet opts = {\n\t\t\tlang: \"en\",\n\t\t\tautolang: true,\n\t\t}\n\n\t\ttry {\n\t\t\topts = json(\"viper.json\");\n\t\t}catch (e) {}\n\n\t\tlang = opts.lang;\n\n\t\tif (! lang) {lang = \"en\"}\n\n\t\tif (forcedlang) {lang = forcedlang}\n\n\t\tif (opts.autolang == false) {\n\t\t\tlang = opts.forcedlang;\n\t\t\tif (! lang) {lang = \"en\"}\n\t\t}\n\n\t\tif (! fs.existsSync(__dirname + `/lang/${lang}.json`)) {\n\t\t\tif (fs.existsSync(__dirname + `/lang/${lang.replace(/-.*$/, \"\")}.json`)) {\n\t\t\t\tlang = lang.replace(/-.*$/, \"\");\n\t\t\t} else {\n\t\t\t\tlang = \"en\";\n\t\t\t}\n\t\t}\n\t} else {\n\t\tlang = \"en\";\n\t}\n\n\tlangObj = flat.flatten(json(__dirname + `/lang/${lang}.json`) || {});\n}\n\n\nmodule.exports = (string, forcedlang) => {\n\tif (lang === \"\") {\n\t\t_loadTranslation();\n\t}\n\t \n\tif (forcedlang) {\n\t\t_loadTranslation(forcedlang);\n\t}\n\n\tif (langObj[string]) {\n\t\treturn langObj[string];\n\t} else {\n\t\tif (enLang[string]) {\n\t\t\treturn enLang[string];\n\t\t} else {\n\t\t\t// If it's not in the default lang either, it returns the\n\t\t\t// string, this is absolute fallback.\n\t\t\treturn string;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/modules/console.js",
    "content": "let new_console = {\n\t...console\n}\n\nlet orig_console = console;\n\nnew_console.ok = (...args) => {\n\torig_console.log('\\x1b[92m%s\\x1b[0m', ...args)\n}\n\nnew_console.misc = (...args) => {\n\torig_console.warn('\\x1b[90m%s\\x1b[0m', ...args)\n}\n\nnew_console.info = (...args) => {\n\torig_console.warn('\\x1b[94m%s\\x1b[0m', ...args)\n}\n\nnew_console.warn = (...args) => {\n\torig_console.warn('\\x1b[93m%s\\x1b[0m', ...args)\n}\n\nnew_console.error = (...args) => {\n\torig_console.error('\\x1b[91m%s\\x1b[0m', ...args)\n}\n\nmodule.exports = new_console;\n"
  },
  {
    "path": "src/modules/findgame.js",
    "content": "const fs = require(\"fs\");\nconst path = require(\"path\");\nconst vdf = require(\"simple-vdf\");\nconst { app } = require(\"electron\");\n\nconst util = require(\"util\");\nconst exec = util.promisify(require(\"child_process\").exec);\n\nconsole = require(\"./console\");\n\nmodule.exports = async () => {\n\tlet gamepath = \"\";\n\t\n\t// autodetect path through Powershell and Windows registry\n\tif (process.platform == \"win32\") {\n\t\ttry {\n\t\t\tconst {stdout} = await exec(\"Get-ItemProperty -Path Registry::HKEY_LOCAL_MACHINE\\\\SOFTWARE\\\\Respawn\\\\Titanfall2\\\\ -Name \\\"Install Dir\\\"\", {\"shell\":\"powershell.exe\"});\n\n\t\t\tgamepath = stdout.split('\\n')\n\t\t\t\t.filter(r => r.indexOf(\"Install Dir\") !== -1)[0]\n\t\t\t\t.replace(/\\s+/g,' ')\n\t\t\t\t.trim()\n\t\t\t\t.replace(\"Install Dir : \",\"\");\n\n\t\t\tif (gamepath) {return gamepath}\n\t\t} catch (err) {}\n\t}\n\n\t// reads, then parses VDF files, to search for Titanfall\n\tfunction readvdf(data) {\n\t\tdata = vdf.parse(data); // parse read_data\n\n\t\t// verify VDF was parsed correctly\n\t\tif (! data || typeof data !== \"object\" || ! data.libraryfolders) {\n\t\t\treturn;\n\t\t}\n\n\t\t// list of folders where the game could possibly be installed at\n\t\tlet values = Object.values(data[\"libraryfolders\"]);\n\n\t\tif (typeof values[values.length - 1] != \"object\") {\n\t\t\tvalues.pop(1);\n\t\t}\n\t\t\n\t\t// `.length - 1` This is because the last value is `contentstatsid`\n\t\tfor (let i = 0; i < values.length; i++) {\n\t\t\tlet data_array = Object.values(values[i]);\n\t\t\t\n\t\t\tif (fs.existsSync(data_array[0] + \"/steamapps/common/Titanfall2/Titanfall2.exe\")) {\n\t\t\t\tconsole.ok(\"Found game in:\", data_array[0]);\n\t\t\t\treturn data_array[0] + \"/steamapps/common/Titanfall2\";\n\t\t\t} else {\n\t\t\t\tconsole.error(\"Game not in:\", data_array[0]);\n\t\t\t}\n\t\t}\n\t}\n\n\tlet vdf_files = [];\n\n\t// set `folders` to paths where the VDF file can be\n\tswitch (process.platform) {\n\t\tcase \"win32\":\n\t\t\tvdf_files = [\"C:\\\\Program Files (x86)\\\\Steam\\\\steamapps\\\\libraryfolders.vdf\"];\n\t\t\tbreak\n\t\tcase \"linux\":\n\t\tcase \"openbsd\":\n\t\tcase \"freebsd\":\n\t\t\tlet home = app.getPath(\"home\");\n\t\t\tvdf_files = [\n\t\t\t\tpath.join(home, \"/.steam/steam/steamapps/libraryfolders.vdf\"),\n\t\t\t\tpath.join(home, \".var/app/com.valvesoftware.Steam/.steam/steam/steamapps/libraryfolders.vdf\"),\n\t\t\t\tpath.join(home, \".var/app/com.valvesoftware.Steam/.local/share/Steam/steamapps/libraryfolders.vdf\")\n\t\t\t]\n\t\t\tbreak\n\t}\n\n\t// searches VDF files\n\tfor (let i = 0; i < vdf_files.length; i++) {\n\t\tif (! fs.existsSync(vdf_files[i])) {continue}\n\t\tconsole.info(\"Searching VDF file at:\", vdf_files[i]);\n\n\t\tlet data = fs.readFileSync(vdf_files[i]);\n\t\tlet read_vdf = readvdf(data.toString());\n\t\tif (read_vdf) {return read_vdf}\n\t}\n\n\treturn gamepath || false;\n}\n"
  },
  {
    "path": "src/modules/gamepath.js",
    "content": "const path = require(\"path\");\nconst fs = require(\"fs-extra\");\n\nconst win = require(\"../win\");\nconst { dialog, ipcMain } = require(\"electron\");\n\nconst cli = require(\"../cli\");\nconst lang = require(\"../lang\");\n\nconst version = require(\"./version\");\nconst settings = require(\"./settings\");\nconst findgame = require(\"./findgame\");\n\nlet gamepath = {};\n\nipcMain.on(\"setpath-cli\", () => {gamepath.set()});\nipcMain.on(\"setpath\", (event, value, force_dialog) => {\n\tif (! value) {\n\t\tif (! win().isVisible()) {\n\t\t\tgamepath.set(win(), force_dialog);\n\t\t} else {\n\t\t\tgamepath.set(win(), force_dialog || true);\n\t\t}\n\t} else if (! win().isVisible()) {\n\t\twin().show();\n\t}\n})\n\n\n// allows renderer to set a new renderer\nipcMain.on(\"newpath\", (event, newpath) => {\n\tif (newpath === false && ! win().isVisible()) {\n\t\twin().send(\"no-path-selected\");\n\t} else {\n\t\tversion.send_info();\n\n\t\tif (! win().isVisible()) {\n\t\t\twin().show();\n\t\t}\n\t}\n})\n\nipcMain.on(\"wrong-path\", () => {\n\twin().send(\"wrong-path\");\n})\n\nipcMain.on(\"found-missing-perms\", async (e, selected_gamepath) => {\n\tgamepath.setting = true;\n\tawait win().alert(lang(\"gui.gamepath.found_missing_perms\") + selected_gamepath);\n\tipcMain.emit(\"setpath\", null, false, true);\n})\n\nipcMain.on(\"missing-perms\", async (e, selected_gamepath) => {\n\tgamepath.setting = true;\n\tawait win().alert(lang(\"gui.gamepath.missing_perms\") + selected_gamepath);\n\tipcMain.emit(\"setpath\");\n})\n\nipcMain.on(\"gamepath-lost-perms\", async (e, selected_gamepath) => {\n\tif (! gamepath.setting && gamepath.lost_perms != selected_gamepath) {\n\t\tgamepath.lost_perms = selected_gamepath;\n\t\tawait win().alert(lang(\"gui.gamepath.lost_perms\") + selected_gamepath);\n\t\tipcMain.emit(\"setpath\");\n\t}\n})\n\n// ensures gamepath still exists and is valid on startup\nlet gamepathlost = false;\nipcMain.on(\"gamepath-lost\", (event, ...args) => {\n\tif (! gamepathlost) {\n\t\tgamepathlost = true;\n\t\twin().send(\"gamepath-lost\");\n\t}\n})\n\n// returns true/false depending on if the gamepath currently exists/is\n// mounted, used to avoid issues...\ngamepath.exists = (folder) => {\n\treturn fs.existsSync(folder || settings().gamepath);\n}\n\n// returns false if the user doesn't have read/write permissions to the\n// selected gamepath, if no gamepath is set, then this will always\n// return `false`, handle that correctly!\ngamepath.has_perms = (folder = settings().gamepath) => {\n\tif (! gamepath.exists(folder)) {\n\t\treturn false;\n\t}\n\n\ttry {\n\t\tfs.accessSync(\n\t\t\tfolder,\n\t\t\tfs.constants.R_OK | fs.constants.W_OK\n\t\t)\n\n\t\tlet test_file_path = path.join(folder, \".viper_test\");\n\t\tfs.writeFileSync(test_file_path, \"\");\n\t\tfs.unlinkSync(test_file_path);\n\n\t\treturn true;\n\t} catch (err) {\n\t\treturn false;\n\t}\n}\n\ngamepath.setting = false;\n\n// requests to set the game path\n//\n// if running with CLI it takes in the --setpath argument otherwise it\n// open the systems file browser for the user to select a path.\ngamepath.set = async (win, force_dialog) => {\n\tgamepath.setting = true;\n\n\t// actually sets and saves the gamepath in the settings\n\tfunction set_gamepath(folder) {\n\t\t// set settings\n\t\tsettings().set(\"gamepath\", folder);\n\t\tsettings().set(\"zip\", path.join(\n\t\t\tsettings().gamepath + \"/northstar.zip\"\n\t\t))\n\n\t\tsettings().save(); // save settings\n\n\t\t// tell the renderer the path has changed\n\t\twin.webContents.send(\"newpath\", settings().gamepath);\n\t\tipcMain.emit(\"newpath\", null, settings().gamepath);\n\t}\n\n\tif (! win) { // CLI\n\t\t// sets the path to the --setpath argument's value\n\t\tset_gamepath(cli.param(\"setpath\"));\n\t} else { // GUI\n\t\t// unless specified, we will first try to automatically find the\n\t\t// gamepath, and then later fallback to the GUI/manual selection\n\t\tif (! force_dialog) {\n\t\t\tfunction set_gamepath(folder, force_dialog) {\n\t\t\t\tsettings().set(\"gamepath\", folder);\n\t\t\t\tsettings().set(\"zip\", path.join(\n\t\t\t\t\tsettings().gamepath + \"/northstar.zip\")\n\t\t\t\t)\n\n\t\t\t\tsettings().save();\n\t\t\t\twin.webContents.send(\"newpath\", settings().gamepath);\n\t\t\t\tipcMain.emit(\"newpath\", null, settings().gamepath);\n\n\t\t\t\tgamepath.setting = false;\n\t\t\t}\n\n\t\t\tlet found_gamepath = await findgame();\n\n\t\t\tif (found_gamepath) {\n\t\t\t\tif (! gamepath.has_perms(found_gamepath)) {\n\t\t\t\t\tipcMain.emit(\"found-missing-perms\", null, found_gamepath);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tset_gamepath(found_gamepath);\n\t\t\t\treturn gamepath.setting = false;\n\t\t\t}\n\n\t\t\tawait win.alert(lang(\"general.missing_path\"));\n\t\t}\n\n\t\t// fallback to GUI/manual selection\n\t\tdialog.showOpenDialog({properties: [\"openDirectory\"]}).then(res => {\n\t\t\tif (res.canceled) {\n\t\t\t\tipcMain.emit(\"newpath\", null, false);\n\t\t\t\treturn gamepath.setting = false;\n\t\t\t}\n\n\t\t\tdelete gamepath.lost_perms;\n\n\t\t\tif (! fs.existsSync(path.join(res.filePaths[0], \"Titanfall2.exe\"))) {\n\t\t\t\tipcMain.emit(\"wrong-path\");\n\t\t\t\treturn gamepath.setting = false;\n\t\t\t}\n\n\t\t\tif (! gamepath.has_perms(res.filePaths[0])) {\n\t\t\t\tipcMain.emit(\"missing-perms\", null, res.filePaths[0]);\n\t\t\t\treturn gamepath.setting = false;\n\t\t\t}\n\n\t\t\tset_gamepath(res.filePaths[0]);\n\n\t\t\tcli.exit();\n\t\t\treturn gamepath.setting = false;\n\t\t}).catch(err => {\n\t\t\tconsole.error(err);\n\t\t\tgamepath.setting = false;\n\t\t})\n\t}\n}\n\n// periodically check for the gamepath still existing, in case the\n// folder is on a disk that gets unmounted, or anything similar, we dont\n// want to assume the gamepath is available forever and ever.\nsetInterval(() => {\n\tif (gamepath.exists()) {\n\t\tif (! gamepath.has_perms()) {\n\t\t\treturn ipcMain.emit(\"gamepath-lost-perms\", null, settings().gamepath);\n\t\t}\n\n\t\tipcMain.emit(\"gui-getmods\");\n\t} else {\n\t\tif (fs.existsSync(\"viper.json\")) {\n\t\t\tif (settings().gamepath != \"\") {\n\t\t\t\tipcMain.emit(\"gamepath-lost\");\n\t\t\t}\n\t\t}\n\t}\n}, 1500)\n\nmodule.exports = gamepath;\n"
  },
  {
    "path": "src/modules/in_path.js",
    "content": "const fs = require(\"fs\");\nconst join = require(\"path\").join;\n\n// checks whether `executable_to_check` is in `$PATH`\nmodule.exports = (executable_to_check) => {\n\t// get folders in `$PATH`\n\tlet path_dirs = process.env[\"PATH\"].split(\":\");\n\n\t// run through folders\n\tfor (let i = 0; i < path_dirs.length; i++) {\n\t\t// path to executable this iteration\n\t\tlet executable = join(path_dirs[i], executable_to_check);\n\n\t\t// if `executable` exists and is a file, then we found it\n\t\tif (fs.existsSync(executable)\n\t\t\t&& fs.statSync(executable).isFile()) {\n\n\t\t\treturn true;\n\t\t}\n\t}\n\n\t// we didn't find `executable_to_check`\n\treturn false;\n}\n"
  },
  {
    "path": "src/modules/ipc.js",
    "content": "const win = require(\"../win\");\nconst { app, ipcMain } = require(\"electron\");\n\nconst kill = require(\"./kill\");\nconst settings = require(\"./settings\");\nconst is_running = require(\"./is_running\");\n\nipcMain.on(\"exit\", () => {\n\tif (settings().originkill) {\n\t\tis_running.origin().then((running) => {\n\t\t\tif (running) {\n\t\t\t\tkill.origin().then(process.exit(0));\n\t\t\t} else {\n\t\t\t\tprocess.exit(0)\t;\n\t\t\t}\n\t\t})\n\t} else {\n\t\tprocess.exit(0);\n\t}\n})\n\nipcMain.on(\"minimize\", () => {\n\twin().minimize();\n})\n\nipcMain.on(\"relaunch\", () => {\n\tapp.relaunch({\n\t\targs: process.argv.slice(1)\n\t})\n\n\tapp.exit(0);\n})\n\nipcMain.on(\"send-enter-key\", (e, coords) => {\n\te.sender.sendInputEvent({\n\t\ttype: \"char\",\n\t\tkeyCode: \"enter\"\n\t})\n})\n"
  },
  {
    "path": "src/modules/is_running.js",
    "content": "const win = require(\"../win\");\nconst exec = require(\"child_process\").exec;\n\nlet is_running = {};\n\nsetInterval(async () => {\n\twin().send(\"is-running\", await is_running.game());\n}, 1000)\n\n// a simple function that checks whether any of a given set of process\n// names are running, you can either input a string or an Array of\n// strings\nasync function check_processes(processes) {\n\tif (typeof processes == \"string\") {\n\t\tprocesses = [processes];\n\t}\n\n\treturn new Promise(resolve => {\n\t\tif (! Array.isArray(processes)) {\n\t\t\treject(false);\n\t\t}\n\n\t\t// while we could use a Node module to do this instead, I\n\t\t// decided not to do so. As this achieves exactly the same\n\t\t// thing. And it's not much more clunky.\n\t\tlet cmd = (() => {\n\t\t\tswitch (process.platform) {\n\t\t\t\tcase \"linux\": return \"ps aux\";\n\t\t\t\tcase \"win32\": return \"tasklist\";\n\t\t\t}\n\t\t})();\n\n\t\texec(cmd, (err, stdout) => {\n\t\t\tfor (let i = 0; i < processes.length; i++) {\n\t\t\t\tif (stdout.includes(processes[i])) {\n\t\t\t\t\tresolve(true);\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tif (i == processes.length - 1) {resolve(false)}\n\t\t\t}\n\t\t});\n\t});\n}\n\nis_running.game = () => {\n\treturn check_processes([\n\t\t\"NorthstarLauncher.exe\",\n\t\t\"Titanfall2.exe\", \"Titanfall2-unpacked.exe\"\n\t])\n}\n\nis_running.origin = () => {\n\treturn check_processes([\n\t\t\"Origin.exe\",\n\t\t\"EADesktop.exe\",\n\t\t\"CrBrowserMain\",\n\t\t\"EABackgroundSer\"\n\t])\n}\n\nis_running.titanfall = () => {\n\treturn check_processes([\n\t\t\"Titanfall2.exe\", \"Titanfall2-unpacked.exe\"\n\t])\n}\n\nis_running.northstar = () => {\n\treturn check_processes([\n\t\t\"NorthstarLauncher.exe\",\n\t])\n}\n\nmodule.exports = is_running;\n"
  },
  {
    "path": "src/modules/json.js",
    "content": "const fs = require(\"fs\");\nconst repair = require(\"jsonrepair\");\n\nfunction read(file) {\n\tlet json = false;\n\n\t// make sure the file actually exists\n\tif (! fs.existsSync(file)) {\n\t\treturn false;\n\t}\n\n\t// make sure we're actually reading a file\n\tif (! fs.statSync(file).isFile()) {\n\t\treturn false;\n\t}\n\n\t// read the file\n\tlet file_content = fs.readFileSync(file, \"utf8\");\n\n\t// attempt to parse it\n\ttry {\n\t\tjson = JSON.parse(file_content);\n\t}catch(err) {\n\t\t// attempt to repair then parse\n\t\ttry {\n\t\t\tjson = JSON.parse(repair(file_content));\n\t\t}catch(repair_err) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\treturn json;\n}\n\nmodule.exports = read;\n"
  },
  {
    "path": "src/modules/kill.js",
    "content": "const exec = require(\"child_process\").exec;\nconst ipcMain = require(\"electron\").ipcMain;\n\nipcMain.on(\"kill\", (function_name) => {\n\tif (typeof kill[function_name] == \"function\") {\n\t\tkill[function_name]();\n\t}\n})\n\n// a simple function to kill processes with a certain name\nasync function kill(process_name) {\n\treturn new Promise(resolve => {\n\t\tlet proc = process_name;\n\t\tlet cmd = (() => {\n\t\t\tswitch (process.platform) {\n\t\t\t\tcase \"linux\": return \"killall -9 \" + proc;\n\t\t\t\tcase \"win32\": return \"taskkill /IM \" + proc + \" /F\";\n\t\t\t}\n\t\t})()\n\n\t\texec(cmd, (err, stdout) => {\n\t\t\t// just try and fail silently if we don't find it w/e\n\t\t\tresolve(true);\n\t\t})\n\t})\n}\n\nkill.process = kill;\n\nkill.origin = async () => {\n\tlet origin = await kill(\"Origin.exe\");\n\tlet eadesktop = await kill(\"EADesktop.exe\");\n\n\t// these should be Linux only, and the above shouldn't succeed if\n\t// these don't succeed, so we shouldn't have to check whether these\n\t// actually succeeded or not\n\tawait kill(\"CrBrowserMain\");\n\tawait kill(\"EABackgroundSer\");\n\n\tif (origin || eadesktop) {\n\t\treturn true;\n\t}\n\n\treturn false;\n}\n\nkill.game = async () => {\n\tlet tf2 = await kill(\"Titanfall2.exe\");\n\tlet northstar = await kill(\"NorthstarLauncher.exe\");\n\tlet tf2_unpacked = await kill(\"Titanfall2-unpacked.exe\");\n\n\tif (tf2 || northstar || tf2_unpacked) {\n\t\treturn true;\n\t}\n\n\treturn false;\n}\n\nmodule.exports = kill;\n"
  },
  {
    "path": "src/modules/launch.js",
    "content": "const { ipcMain, shell } = require(\"electron\");\nconst { exec, execSync } = require(\"child_process\");\n\nconst cli = require(\"../cli\");\nconst win = require(\"../win\");\nconst lang = require(\"../lang\");\n\nconst in_path = require(\"./in_path\");\nconst settings = require(\"./settings\");\n\nconsole = require(\"./console\");\n\nipcMain.on(\"launch\", (_, game_version) => {\n\tlaunch(game_version)\n})\n\n// launches the game\n//\n// either Northstar or Vanilla. Linux support is not currently a thing,\n// however it'll be added at some point.\nfunction launch(game_version, method = settings().linux_launch_method) {\n\tconsole.info(\n\t\tlang(\"general.launching\"),\n\t\tgame_version || \"Northstar\" + \"...\"\n\t)\n\n\t// change current directory to gamepath\n\tprocess.chdir(settings().gamepath);\n\n\tlet launch_args = settings().nsargs || \"\";\n\n\t// add `-vanilla` or `-northstar` depending on `game_version`\n\tif (game_version == \"vanilla\") {\n\t\tlaunch_args += \" -vanilla\"\n\t} else {\n\t\tlaunch_args += \" -northstar\"\n\t}\n\n\n\t// Linux launch support\n\tif (process.platform == \"linux\") {\n\t\tlet flatpak_id = \"com.valvesoftware.Steam\";\n\t\tlet steam_run_str = `steam://run/1237970//${launch_args}/`;\n\t\tconsole.log(steam_run_str)\n\n\t\t// returns whether the Flatpak version of Steam is installed\n\t\tlet flatpak_steam_installed = () => {\n\t\t\t// this will throw an error if the command fails,\n\t\t\t// either because of an error with the command, or\n\t\t\t// because it's not installed, either way,\n\t\t\t// indicating it's not installed\n\t\t\ttry {\n\t\t\t\texecSync(\n\t\t\t\t\t`flatpak info ${flatpak_id}`\n\t\t\t\t)\n\n\t\t\t\treturn true;\n\t\t\t}catch(err) {}\n\n\t\t\treturn false;\n\t\t}\n\n\t\tswitch(method) {\n\t\t\tcase \"steam_auto\":\n\t\t\t\t// if a Steam executable is found, use that\n\t\t\t\tif (in_path(\"steam\")) {\n\t\t\t\t\treturn launch(game_version, \"steam_executable\");\n\n\t\t\t\t// is Flatpak (likely) installed?\n\t\t\t\t} else if (in_path(\"flatpak\")) { \n\n\t\t\t\t\tif (flatpak_steam_installed()) {\n\t\t\t\t\t\treturn launch(game_version, \"steam_flatpak\");\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// fallback to Steam protocol\n\t\t\t\treturn launch(game_version, \"steam_protocol\");\n\n\t\t\tcase \"steam_flatpak\":\n\t\t\t\t// make sure Flatpak is installed, and show an error\n\t\t\t\t// toast if not\n\t\t\t\tif (! in_path(\"flatpak\")) {\n\t\t\t\t\twin().toast({\n\t\t\t\t\t\tscheme: \"error\",\n\t\t\t\t\t\ttitle: lang(\"gui.toast.title.missing_flatpak\"),\n\t\t\t\t\t\tdescription: lang(\n\t\t\t\t\t\t\t\"gui.toast.desc.missing_flatpak\"\n\t\t\t\t\t\t)\n\t\t\t\t\t})\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// make sure the Flatpak version of Steam is installed,\n\t\t\t\t// and show an error toast if not\n\t\t\t\tif (! flatpak_steam_installed()) {\n\t\t\t\t\twin().toast({\n\t\t\t\t\t\tscheme: \"error\",\n\t\t\t\t\t\ttitle: lang(\n\t\t\t\t\t\t\t\"gui.toast.title.missing_flatpak_steam\"\n\t\t\t\t\t\t),\n\t\t\t\t\t\tdescription: lang(\n\t\t\t\t\t\t\t\"gui.toast.desc.missing_flatpak_steam\"\n\t\t\t\t\t\t)\n\t\t\t\t\t})\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// attempt to launch the game with Flatpak Steam\n\t\t\t\texec(`flatpak run ${flatpak_id} \"${steam_run_str}\"`, {\n\n\t\t\t\t\tcwd: settings().gamepath\n\t\t\t\t})\n\n\t\t\t\treturn;\n\n\t\t\tcase \"steam_executable\":\n\t\t\t\t// make sure Steam is installed, and show an error toast\n\t\t\t\t// if not\n\t\t\t\tif (! in_path(\"steam\")) {\n\t\t\t\t\twin().toast({\n\t\t\t\t\t\tscheme: \"error\",\n\t\t\t\t\t\ttitle: lang(\"gui.toast.title.missing_steam\"),\n\t\t\t\t\t\tdescription: lang(\n\t\t\t\t\t\t\t\"gui.toast.desc.missing_steam\"\n\t\t\t\t\t\t)\n\t\t\t\t\t})\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// attempt to launch the game with the Steam executable\n\t\t\t\texec(`steam \"${steam_run_str}\"`, {\n\t\t\t\t\tcwd: settings().gamepath\n\t\t\t\t})\n\n\t\t\t\treturn;\n\n\t\t\tcase \"steam_protocol\":\n\t\t\t\t// attempt to launch the game with the Steam protocol\n\t\t\t\tshell.openExternal(steam_run_str)\n\n\t\t\t\treturn;\n\t\t}\n\n\t\t// launch Vanilla with custom command\n\t\tlet command = settings().linux_launch_cmd_ns;\n\t\tif (game_version == \"vanilla\") {\n\t\t\tcommand = settings().linux_launch_cmd_vanilla;\n\t\t}\n\n\t\t// make sure the custom command is not just whitespace, and show\n\t\t// an error toast if it is\n\t\tif (! command.trim()) {\n\t\t\twin().toast({\n\t\t\t\tscheme: \"error\",\n\t\t\t\ttitle: lang(\"gui.toast.title.missing_launch_command\"),\n\t\t\t\tdescription: lang(\n\t\t\t\t\t\"gui.toast.desc.missing_launch_command\"\n\t\t\t\t)\n\t\t\t})\n\n\t\t\treturn;\n\t\t}\n\n\t\t// launch Northstar with custom command\n\t\ttry {\n\t\t\texec(command, {\n\t\t\t\tcwd: settings().gamepath,\n\t\t\t\tenv: {\n\t\t\t\t\t...process.env,\n\t\t\t\t\tTF_ARGS: launch_args\n\t\t\t\t}\n\t\t\t})\n\t\t}catch(err) { \n\t\t\t// show error if custom commands fails\n\t\t\t// this should basically never trigger, it should only\n\t\t\t// trigger if `command` isn't set to something valid\n\t\t\twin().toast({\n\t\t\t\tscheme: \"error\",\n\t\t\t\ttitle: lang(\"gui.toast.title.failed_launch_command\"),\n\t\t\t\tdescription: lang(\n\t\t\t\t\t\"gui.toast.desc.failed_launch_command\"\n\t\t\t\t)\n\t\t\t})\n\t\t}\n\n\t\treturn;\n\t}\n\n\t// default launches with Northstar\n\tlet executable = \"NorthstarLauncher.exe\"\n\n\t// change over to using vanilla executable\n\tif (game_version == \"vanilla\") {\n\t\texecutable = \"Titanfall2.exe\"\n\t}\n\n\t// launch executable/game\n\texec(executable + \" \" + launch_args, {\n\t\tcwd: settings().gamepath\n\t})\n}\n\nmodule.exports = launch;\n"
  },
  {
    "path": "src/modules/mods.js",
    "content": "const path = require(\"path\");\nconst fs = require(\"fs-extra\");\nconst unzip = require(\"unzip-stream\");\nconst copy = require(\"recursive-copy\");\nconst { https } = require(\"follow-redirects\");\nconst { app, ipcMain, dialog } = require(\"electron\");\n\nconst json = require(\"./json\");\nconst update = require(\"./update\");\nconst version = require(\"./version\");\nconst settings = require(\"./settings\");\n\nconsole = require(\"./console\");\n\nconst win = require(\"../win\");\nconst cli = require(\"../cli\");\nconst lang = require(\"../lang\");\n\nvar mods = {\n\tinstalling: [],\n\tdupe_msg_sent: false,\n}\n\nipcMain.on(\"remove-mod\", (event, mod) => {\n\tmods.remove(mod);\n})\n\nipcMain.on(\"toggle-mod\", (event, mod) => {\n\tmods.toggle(mod);\n})\n\n// lets renderer install mods from a path\nipcMain.on(\"install-from-path\", (event, path) => {\n\tmods.install(path);\n})\n\nipcMain.on(\"no-internet\", () => {\n\twin().send(\"no-internet\");\n})\n\nipcMain.on(\"install-mod\", () => {\n\tif (cli.hasArgs()) {\n\t\tmods.install(cli.param(\"installmod\"));\n\t} else {\n\t\tdialog.showOpenDialog({properties: [\"openFile\"]}).then(res => {\n\t\t\tif (res.filePaths.length != 0) {\n\t\t\t\tmods.install(res.filePaths[0]);\n\t\t\t} else {\n\t\t\t\twin().send(\"set-buttons\", true);\n\t\t\t}\n\t\t}).catch(err => {error(err)});\n\t}\n})\n\n// sends installed mods info to renderer\nipcMain.on(\"gui-getmods\", (event, ...args) => {\n\twin().send(\"mods\", mods.list());\n})\n\nipcMain.on(\"getmods\", () => {\n\tlet mods = mods.list();\n\n\tif (mods.all.length > 0) {\n\t\tlog(`${lang(\"general.mods.installed\")} ${mods.all.length}`);\n\t\tlog(`${lang(\"general.mods.enabled\")} ${mods.enabled.length}`);\n\t\tfor (let i = 0; i < mods.enabled.length; i++) {\n\t\t\tlog(`  ${mods.enabled[i].name} ${mods.enabled[i].version}`);\n\t\t}\n\n\t\tif (mods.disabled.length > 0) {\n\t\t\tlog(`${lang(\"general.mods.disabled\")} ${mods.disabled.length}`);\n\t\t\tfor (let i = 0; i < mods.disabled.length; i++) {\n\t\t\t\tlog(`  ${mods.disabled[i].name} ${mods.disabled[i].version}`);\n\t\t\t}\n\t\t}\n\t\tcli.exit(0);\n\t} else {\n\t\tlog(\"No mods installed\");\n\t\tcli.exit(0);\n\t}\n})\n\nfunction update_path() {\n\tmods.path = path.join(settings().gamepath, \"R2Northstar/mods\");\n}; update_path();\n\n// returns a list of mods\n//\n// it'll return 3 arrays, all, enabled, disabled. all being a\n// combination of the other two, enabled being enabled mods, and you\n// guessed it, disabled being disabled mods.\nmods.list = () => {\n\tupdate_path();\n\n\t// make sure Northstar is actually installed\n\tif (version.northstar() == \"unknown\") {\n\t\t// notify user of missing Northstar, unless its because its\n\t\t// currently being updated\n\t\tif (! update.northstar.updating) {\n\t\t\twin().log(lang(\"general.not_installed\"));\n\t\t\tconsole.error(lang(\"general.not_installed\"));\n\t\t}\n\n\t\tcli.exit(1);\n\t\treturn false;\n\t}\n\n\tlet enabled = [];\n\tlet disabled = [];\n\n\t// return early if the mods folder doesn't even exist\n\tif (! fs.existsSync(mods.path)) {\n\t\t// create the folder for later\n\t\tfs.mkdirSync(path.join(mods.path), {recursive: true});\n\n\t\treturn {\n\t\t\tenabled: [],\n\t\t\tdisabled: [],\n\t\t\tall: []\n\t\t};\n\t}\n\n\tlet get_in_dir = (dir, package_obj) => {\n\t\tlet packaged_mods = [];\n\t\tlet files = fs.readdirSync(dir);\n\n\t\tfiles.forEach((file) => {\n\t\t\t// return early if `file` isn't a folder\n\t\t\tif (! fs.statSync(path.join(dir, file)).isDirectory()) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tlet modjson = path.join(dir, file, \"mod.json\");\n\n\t\t\t// return early if mod.json doesn't exist or isn't a file\n\t\t\tif (! fs.existsSync(modjson) || ! fs.statSync(modjson).isFile()) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tlet mod = json(modjson);\n\t\t\tif (! mod) {return}\n\n\t\t\tlet obj = {\n\t\t\t\tauthor: mod.Author || false,\n\t\t\t\tversion: mod.Version || \"unknown\",\n\t\t\t\tname: mod.Name || \"unknown\",\n\t\t\t\tdescription: mod.Description || \"\",\n\n\t\t\t\tfolder_name: file,\n\t\t\t\tfolder_path: path.join(dir, file),\n\n\t\t\t\tpackage: package_obj || false\n\t\t\t}\n\n\t\t\tif (obj.package) {\n\t\t\t\tpackaged_mods.push(obj.name);\n\t\t\t\tobj.author = obj.package.author;\n\t\t\t\tobj.version = obj.package.version;\n\t\t\t}\n\n\t\t\tobj.disabled = ! mods.modfile.get(obj.name);\n\n\t\t\t// add manifest data from manifest.json, if it exists\n\t\t\tlet manifest_file = path.join(dir, file, \"manifest.json\");\n\t\t\tif (fs.existsSync(manifest_file)) {\n\t\t\t\tlet manifest = json(manifest_file);\n\t\t\t\tif (manifest != false) {\n\t\t\t\t\tobj.manifest_name = manifest.name;\n\t\t\t\t\tif (obj.version == \"unknown\") {\n\t\t\t\t\t\tobj.version = manifest.version_number;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// add author data from author file, if it exists\n\t\t\tlet author_file = path.join(dir, file, \"thunderstore_author.txt\");\n\t\t\tif (fs.existsSync(author_file)) {\n\t\t\t\tobj.author = fs.readFileSync(author_file, \"utf8\");\n\t\t\t}\n\n\t\t\t// add mod to their respective disabled or enabled Array\n\t\t\tif (obj.disabled) {\n\t\t\t\tdisabled.push(obj);\n\t\t\t} else {\n\t\t\t\tenabled.push(obj);\n\t\t\t}\n\t\t})\n\n\t\tif (packaged_mods.length == 0) {\n\t\t\treturn;\n\t\t}\n\n\t\tlet add_packaged_mods = (mods_array) => {\n\t\t\tfor (let i = 0; i < mods_array.length; i++) {\n\t\t\t\tif (mods_array[i].package.package_name !==\n\t\t\t\t\tpackage_obj.package_name) {\n\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tmods_array[i].packaged_mods = packaged_mods;\n\t\t\t}\n\n\t\t\treturn mods_array;\n\t\t}\n\n\t\tenabled = add_packaged_mods(enabled);\n\t\tdisbled = add_packaged_mods(disabled);\n\t}\n\n\t// get mods in `mods` folder\n\tget_in_dir(mods.path);\n\n\t// get mods in `packages` folder\n\tlet packages = require(\"./packages\");\n\tlet package_list = require(\"./packages\").list(packages.path, true);\n\tfor (let i in package_list) {\n\t\t// make sure the package actually has mods\n\t\tif (! package_list[i].has_mods) {\n\t\t\tcontinue;\n\t\t}\n\n\t\t// search the package's `mods` folder\n\t\tget_in_dir(\n\t\t\tpath.join(package_list[i].package_path, \"mods\"),\n\t\t\tpackage_list[i]\n\t\t)\n\t}\n\n\treturn {\n\t\tenabled: enabled,\n\t\tdisabled: disabled,\n\t\tall: [...enabled, ...disabled]\n\t};\n}\n\n// gets information about a mod\n//\n// folder name, version, name and whatever else is in the mod.json, keep\n// in mind if the mod developer didn't format their JSON file the\n// absolute basics will be provided and we can't know the version or\n// similar.\nmods.get = (mod) => {\n\tupdate_path();\n\n\t// make sure Northstar is actually installed\n\tif (version.northstar() == \"unknown\") {\n\t\t// notify user of missing Northstar, unless its because its\n\t\t// currently being updated\n\t\tif (! update.northstar.updating) {\n\t\t\twin().log(lang(\"general.not_installed\"));\n\t\t\tconsole.error(lang(\"general.not_installed\"));\n\t\t}\n\n\t\tcli.exit(1);\n\t\treturn false;\n\t}\n\n\t// retrieve list of mods\n\tlet list = mods.list().all;\n\n\t// search for mod in list\n\tfor (let i = 0; i < list.length; i++) {\n\t\tif (list[i].name == mod) {\n\t\t\t// found mod, return data\n\t\t\treturn list[i];\n\t\t} else {continue}\n\t}\n\n\t// mod wasn't found\n\treturn false;\n}\n\n// makes sure enabledmods.json exists\nfunction modfile_pre() {\n\tmods.modfile.file = path.join(mods.path, \"..\", \"enabledmods.json\");\n\n\t// check that the folder enabledmods.json is in exists, and create\n\t// it if it doesn't exist\n\tif (! fs.existsSync(mods.path)) {\n\t\tfs.mkdirSync(path.join(mods.path), {recursive: true});\n\t}\n\n\t// check that enabledmods.json itself exists, and create it if not\n\tif (! fs.existsSync(mods.modfile.file)) {\n\t\tfs.writeFileSync(mods.modfile.file, \"{}\");\n\t}\n}\n\n// manages the enabledmods.json file\n//\n// it can both return info about the file, but also toggle mods in it,\n// generate the file itself, and so on.\nmods.modfile = {};\n\n// generate the enabledmods.json file\nmods.modfile.gen = () => {\n\tmodfile_pre();\n\n\tlet names = {};\n\tlet list = mods.list().all; // get list of all mods\n\tfor (let i = 0; i < list.length; i++) {\n\t\t// add every mod to the list\n\t\tnames[list[i].name] = true\n\t}\n\n\t// write the actual file\n\tfs.writeFileSync(mods.modfile.file, JSON.stringify(names));\n}\n\n// enable/disable a mod inside enabledmods.json\nmods.modfile.set = (mod, state) => {\n\tmodfile_pre();\n\n\tlet data = json(mods.modfile.file); // get current data\n\tdata[mod] = state; // set mod state\n\n\t// write new data\n\tfs.writeFileSync(mods.modfile.file, JSON.stringify(data));\n}\n\n// disable a mod inside enabledmods.json\nmods.modfile.disable = (mod) => {\n\treturn mods.modfile.set(mod, false);\n}\n\n// enable a mod inside enabledmods.json\nmods.modfile.enable = (mod) => {\n\treturn mods.modfile.set(mod, true);\n}\n\n// toggle a mod inside enabledmods.json\nmods.modfile.toggle = (mod) => {\n\tmodfile_pre();\n\n\tlet data = json(mods.modfile.file);\n\tif (data[mod] != undefined) {\n\t\tdata[mod] = ! data[mod];\n\t} else {\n\t\tdata[mod] = false;\n\t}\n\n\tfs.writeFileSync(mods.modfile.file, JSON.stringify(data));\n}\n\n// return whether a mod is disabled or enabled\nmods.modfile.get = (mod) => {\n\tmodfile_pre();\n\n\t// read enabledmods.json\n\tlet data = json(mods.modfile.file);\n\n\tif (! data || typeof data !== \"object\") {\n\t\treturn true;\n\t}\n\n\tif (data[mod]) { // enabled\n\t\treturn true;\n\t} else if (data[mod] === false) { // disabled\n\t\treturn false;\n\t} else { // fallback to enabled\n\t\treturn true;\n\t}\n}\n\n// installs mods from a file path\n//\n// either a zip or folder is supported, we'll also try to search inside\n// the zip or folder to see if buried in another folder or not, as\n// sometimes that's the case.\nmods.install = (mod, opts) => {\n\tupdate_path();\n\n\tlet modname = mod.replace(/^.*(\\\\|\\/|\\:)/, \"\");\n\n\topts = {\n\t\tforked: false,\n\t\tauthor: false,\n\t\tdestname: false,\n\t\tmalformed: false,\n\t\tmanifest_file: false,\n\t\t...opts\n\t}\n\n\tif (! opts.forked) {\n\t\tmods.installing = [];\n\t\tmods.dupe_msg_sent = false;\n\t}\n\n\tif (version.northstar() == \"unknown\") {\n\t\t// notify user of missing Northstar, unless its because its\n\t\t// currently being updated\n\t\tif (! update.northstar.updating) {\n\t\t\twin().log(lang(\"general.not_installed\"));\n\t\t\tconsole.error(lang(\"general.not_installed\"));\n\t\t}\n\n\t\tcli.exit(1);\n\t\treturn false;\n\t}\n\n\tlet notamod = () => {\n\t\twin().log(lang(\"gui.mods.not_a_mod\"));\n\t\tconsole.error(lang(\"cli.mods.not_a_mod\"));\n\t\tcli.exit(1);\n\t\treturn false;\n\t}\n\n\tlet installed = () => {\n\t\tconsole.ok(lang(\"cli.mods.installed\"));\n\t\tcli.exit();\n\n\t\twin().log(lang(\"gui.mods.installedmod\"));\n\n\t\tif (modname == \"mods\") {\n\t\t\tlet manifest = path.join(app.getPath(\"userData\"), \"Archives/manifest.json\");\n\n\t\t\tif (fs.existsSync(manifest)) {\n\t\t\t\tmodname = require(manifest).name;\n\t\t\t}\n\t\t}\n\n\t\twin().send(\"installed-mod\", {\n\t\t\tname: modname,\n\t\t\tmalformed: opts.malformed,\n\t\t})\n\n\t\twin().send(\"mods\", mods.list());\n\t\treturn true;\n\t}\n\n\tif (! fs.existsSync(mod)) {return notamod()}\n\n\tif (fs.statSync(mod).isDirectory()) {\n\t\twin().log(lang(\"gui.mods.installing\"));\n\t\tfiles = fs.readdirSync(mod);\n\t\tif (fs.existsSync(path.join(mod, \"mod.json\")) &&\n\t\t\tfs.statSync(path.join(mod, \"mod.json\")).isFile()) {\n\n\n\t\t\tif (! json(path.join(mod, \"mod.json\"))) {\n\t\t\t\twin().send(\"failed-mod\");\n\t\t\t\treturn notamod();\n\t\t\t}\n\n\t\t\tif (fs.existsSync(path.join(mods.path, modname))) {\n\t\t\t\tfs.rmSync(path.join(mods.path, modname), {recursive: true});\n\t\t\t}\n\n\t\t\tlet copydest = path.join(mods.path, modname);\n\t\t\tif (typeof opts.destname == \"string\") {\n\t\t\t\tcopydest = path.join(mods.path, opts.destname)\n\t\t\t}\n\n\t\t\tcopy(mod, copydest, (err) => {\n\t\t\t\tif (err) {\n\t\t\t\t\twin().send(\"failed-mod\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tcopy(opts.manifest_file, path.join(copydest, \"manifest.json\"), (err) => {\n\t\t\t\t\tif (err) {\n\t\t\t\t\t\twin().send(\"failed-mod\");\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (opts.author) {\n\t\t\t\t\t\tfs.writeFileSync(\n\t\t\t\t\t\t\tpath.join(copydest, \"thunderstore_author.txt\"),\n\t\t\t\t\t\t\topts.author\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\n\t\t\t\t\treturn installed();\n\t\t\t\t});\n\t\t\t});\n\n\t\t\treturn;\n\t\t} else {\n\t\t\tmod_files = fs.readdirSync(mod);\n\n\t\t\tfor (let i = 0; i < mod_files.length; i++) {\n\t\t\t\tif (fs.statSync(path.join(mod, mod_files[i])).isDirectory()) {\n\t\t\t\t\tif (fs.existsSync(path.join(mod, mod_files[i], \"mod.json\")) &&\n\t\t\t\t\t\tfs.statSync(path.join(mod, mod_files[i], \"mod.json\")).isFile()) {\n\n\t\t\t\t\t\tlet mod_name = mod_files[i];\n\t\t\t\t\t\tlet use_mod_name = false;\n\n\t\t\t\t\t\twhile (mods.installing.includes(mod_name)) {\n\t\t\t\t\t\t\tif (! mods.dupe_msg_sent) {\n\t\t\t\t\t\t\t\tmods.dupe_msg_sent = true;\n\t\t\t\t\t\t\t\twin().send(\"duped-mod\", mod_name);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tuse_mod_name = true;\n\t\t\t\t\t\t\tmod_name = mod_name + \" (dupe)\";\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tmods.installing.push(mod_name);\n\n\t\t\t\t\t\tlet install = false;\n\t\t\t\t\t\tif (use_mod_name) {\n\t\t\t\t\t\t\tinstall = mods.install(path.join(mod, mod_files[i]), {\n\t\t\t\t\t\t\t\t...opts,\n\t\t\t\t\t\t\t\tforked: true,\n\t\t\t\t\t\t\t\tdestname: mod_name,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tinstall = mods.install(path.join(mod, mod_files[i]), {\n\t\t\t\t\t\t\t\t...opts,\n\t\t\t\t\t\t\t\tforked: true\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (install) {return true};\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn notamod();\n\t\t}\n\t} else {\n\t\twin().log(lang(\"gui.mods.extracting\"));\n\t\tlet cache = path.join(app.getPath(\"userData\"), \"Archives\");\n\t\tif (fs.existsSync(cache)) {\n\t\t\tfs.rmSync(cache, {recursive: true});\n\t\t\tfs.mkdirSync(path.join(cache, \"mods\"), {recursive: true});\n\t\t} else {\n\t\t\tfs.mkdirSync(path.join(cache, \"mods\"), {recursive: true});\n\t\t}\n\n\t\ttry {\n\t\t\tif (mod.replace(/.*\\./, \"\").toLowerCase() == \"zip\") {\n\t\t\t\tfs.createReadStream(mod).pipe(unzip.Extract({path: cache}))\n\t\t\t\t.on(\"finish\", () => {\n\t\t\t\t\tsetTimeout(() => {\n\t\t\t\t\t\tlet manifest = path.join(cache, \"manifest.json\");\n\t\t\t\t\t\tif (fs.existsSync(manifest)) {\n\t\t\t\t\t\t\tfiles = fs.readdirSync(path.join(cache, \"mods\"));\n\t\t\t\t\t\t\tif (fs.existsSync(path.join(cache, \"mods/mod.json\"))) {\n\t\t\t\t\t\t\t\tif (mods.install(path.join(cache, \"mods\"), {\n\t\t\t\t\t\t\t\t\t\t...opts,\n\n\t\t\t\t\t\t\t\t\t\tforked: true,\n\t\t\t\t\t\t\t\t\t\tmalformed: true,\n\t\t\t\t\t\t\t\t\t\tmanifest_file: manifest,\n\t\t\t\t\t\t\t\t\t\tdestname: require(manifest).name\n\t\t\t\t\t\t\t\t\t})) {\n\n\t\t\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tfor (let i = 0; i < files.length; i++) {\n\t\t\t\t\t\t\t\t\tlet mod = path.join(cache, \"mods\", files[i]);\n\t\t\t\t\t\t\t\t\tif (fs.statSync(mod).isDirectory()) {\n\t\t\t\t\t\t\t\t\t\tsetTimeout(() => {\n\t\t\t\t\t\t\t\t\t\t\tif (mods.install(mod, {\n\t\t\t\t\t\t\t\t\t\t\t\t\t...opts,\n\t\t\t\t\t\t\t\t\t\t\t\t\tforked: true,\n\t\t\t\t\t\t\t\t\t\t\t\t\tdestname: false,\n\t\t\t\t\t\t\t\t\t\t\t\t\tmanifest_file: manifest\n\t\t\t\t\t\t\t\t\t\t\t\t})) {\n\n\t\t\t\t\t\t\t\t\t\t\t\treturn true\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}, 1000)\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tif (files.length == 0) {\n\t\t\t\t\t\t\t\t\twin().send(\"failed-mod\");\n\t\t\t\t\t\t\t\t\treturn notamod();\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\treturn notamod();\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (mods.install(cache, {\n\t\t\t\t\t\t\t...opts,\n\t\t\t\t\t\t\tforked: true\n\t\t\t\t\t\t})) {\n\t\t\t\t\t\t\tinstalled();\n\t\t\t\t\t\t} else {return notamod()}\n\t\t\t\t\t}, 1000)\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\treturn notamod();\n\t\t\t}\n\t\t}catch(err) {return notamod()}\n\t}\n}\n\n// installs mods from URL's\n//\n// this'll simply download the file that the URL points to and then\n// install it with mods.install()\nmods.installFromURL = (url, author) => {\n\tupdate_path();\n\n\t// download mod to a temporary location\n\thttps.get(url, (res) => {\n\t\tlet tmp = path.join(app.getPath(\"userData\"), \"Temp\");\n\t\tlet modlocation = path.join(tmp, \"/mod.zip\");\n\n\t\t// make sure the temporary folder exists\n\t\tif (fs.existsSync(tmp)) {\n\t\t\tif (! fs.statSync(tmp).isDirectory()) {\n\t\t\t\tfs.rmSync(tmp);\n\t\t\t}\n\t\t} else {\n\t\t\tfs.mkdirSync(tmp);\n\t\t\tif (fs.existsSync(modlocation)) {\n\t\t\t\tfs.rmSync(modlocation);\n\t\t\t}\n\t\t}\n\n\t\t// write out the file to the temporary location\n\t\tlet stream = fs.createWriteStream(modlocation);\n\t\tres.pipe(stream);\n\n\t\tstream.on(\"finish\", () => {\n\t\t\tstream.close();\n\n\t\t\t// attempt to install the downloaded mod\n\t\t\tmods.install(modlocation, {\n\t\t\t\tauthor: author\n\t\t\t})\n\t\t})\n\t})\n}\n\n// removes mods\n//\n// takes in the names of the mod then removes it, no confirmation,\n// that'd be up to the GUI.\nmods.remove = (mod) => {\n\tupdate_path();\n\n\t// make sure Northstar is actually installed\n\tif (version.northstar() == \"unknown\") {\n\t\t// notify user of missing Northstar, unless its because its\n\t\t// currently being updated\n\t\tif (! update.northstar.updating) {\n\t\t\twin().log(lang(\"general.not_installed\"));\n\t\t\tconsole.error(lang(\"general.not_installed\"));\n\t\t}\n\n\t\tcli.exit(1);\n\t\treturn false;\n\t}\n\n\t// removes all mods installed, no exceptions\n\tif (mod == \"allmods\") {\n\t\tlet modlist = mods.list().all;\n\t\tfor (let i = 0; i < modlist.length; i++) {\n\t\t\tmods.remove(modlist[i].name);\n\t\t}\n\t\treturn\n\t}\n\n\tlet mod_data = mods.get(mod);\n\tlet mod_name = mod_data.folder_name;\n\n\tif (! mod_name) {\n\t\tconsole.error(lang(\"cli.mods.cant_find\"));\n\t\tcli.exit(1);\n\t\treturn;\n\t}\n\n\tlet mod_path = mod_data.folder_path;\n\n\t// if the mod comes from a package, we'll want to set `mod_path` to\n\t// the package's folder, that way everything gets removed cleanly\n\tif (mod_data.package) {\n\t\tmod_path = mod_data.package.package_path;\n\t}\n\n\t// return early if `mod_path` isn't a folder\n\tif (! fs.statSync(mod_path).isDirectory()) {\n\t\treturn cli.exit(1);\n\t}\n\n\tlet manifest_name = null;\n\n\t// if the mod has a manifest.json we want to save it now so we can\n\t// send it later when telling the renderer about the deleted mod\n\tif (fs.existsSync(path.join(mod_path, \"manifest.json\"))) {\n\t\tmanifest_name = json(path.join(mod_path, \"manifest.json\")).name;\n\t}\n\n\t// actually remove the mod itself\n\tfs.rmSync(mod_path, {recursive: true});\n\n\tconsole.ok(lang(\"cli.mods.removed\"));\n\tcli.exit();\n\n\twin().send(\"mods\", mods.list()); // send updated list to renderer\n\n\t// tell the renderer that the mod has been removed, along with\n\t// relevant info for it to properly update everything graphically\n\twin().send(\"removed-mod\", {\n\t\tname: mod.replace(/^.*(\\\\|\\/|\\:)/, \"\"),\n\t\tmanifest_name: manifest_name\n\t})\n}\n\n// toggles mods\n//\n// if a mod is enabled it'll disable it, vice versa it'll enable it if\n// it's disabled. You could have a direct .disable() function if you\n// checked for if a mod is already disable and if not run the function.\n// However we currently have no need for that.\nmods.toggle = (mod, fork) => {\n\tupdate_path();\n\n\t// make sure Northstar is actually installed\n\tif (version.northstar() == \"unknown\") {\n\t\t// notify user of missing Northstar, unless its because its\n\t\t// currently being updated\n\t\tif (! update.northstar.updating) {\n\t\t\twin().log(lang(\"general.not_installed\"));\n\t\t\tconsole.error(lang(\"general.not_installed\"));\n\t\t}\n\n\t\tcli.exit(1);\n\t\treturn false;\n\t}\n\n\t// toggles all mods, thereby inverting the current enabled states\n\t//\n\t// this skips core mods, as there's generally little use to have\n\t// this affect them\n\tif (mod == \"allmods\") {\n\t\tlet modlist = mods.list().all; // get list of all mods\n\t\tfor (let i = 0; i < modlist.length; i++) { // run through list\n\t\t\t// skip core mods\n\t\t\tif (modlist[i].name.toLowerCase().match(/^northstar\\./)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tmods.toggle(modlist[i].name, true); // enable mod\n\t\t}\n\n\t\tconsole.ok(lang(\"cli.mods.toggled_all\"));\n\t\tcli.exit(0);\n\t\treturn\n\t}\n\n\t// toggle specific mod\n\tmods.modfile.toggle(mod);\n\n\tif (! fork) {\n\t\tconsole.ok(lang(\"cli.mods.toggled\"));\n\t\tcli.exit();\n\t}\n\n\t// send updated modlist to renderer\n\twin().send(\"mods\", mods.list());\n}\n\nmodule.exports = mods;\n"
  },
  {
    "path": "src/modules/packages.js",
    "content": "const path = require(\"path\");\nconst fs = require(\"fs-extra\");\nconst unzip = require(\"unzip-stream\");\nconst { app, ipcMain } = require(\"electron\");\nconst https = require(\"follow-redirects\").https;\n\nconst win = require(\"../win\");\nconst lang = require(\"../lang\");\n\nconst json = require(\"./json\");\nconst settings = require(\"./settings\");\n\nconsole = require(\"./console\");\n\nvar packages = {};\n\n// lets renderer install packages\nipcMain.on(\"install-from-url\", (event, url, author, package_name, version) => {\n\tpackages.install(url, author, package_name, version);\n})\n\nfunction update_path() {\n\tpackages.path = path.join(settings().gamepath, \"R2Northstar/packages\");\n\t\n\t// make sure the `packages` folder exists\n\tif (fs.existsSync(packages.path)) {\n\t\t// if it does, but it's a file, remove it\n\t\tif (fs.lstatSync(packages.path).isFile()) {\n\t\t\tfs.rmSync(packages.path);\n\t\t} else {return}\n\t} \n\n\t// only create folder if the profile folder exists\n\tif (fs.existsSync(path.dirname(packages.path))) {\n\t\t// create the folder, in case it doesn't already exist\n\t\tfs.mkdirSync(packages.path);\n\t}\n\n}; update_path();\n\npackages.format_name = (author, package_name, version) => {\n\tif (version && version[0] == \"v\") {\n\t\tversion = version.substring(1, version.length);\n\t}\n\n\treturn author + \"-\" + package_name + \"-\" + version;\n}\n\n// splits the package name into it's individual parts\npackages.split_name = (name) => {\n\tlet split = name.split(\"-\");\n\n\t// make sure there are only 3 parts\n\tif (split.length !== 3) {\n\t\treturn false;\n\t}\n\n\t// return parts\n\treturn {\n\t\tauthor: split[0],\n\t\tversion: split[2],\n\t\tpackage_name: split[1]\n\t}\n}\n\npackages.list = (dir = packages.path, no_functions) => {\n\tupdate_path();\n\n\tif (! fs.existsSync(dir)\n\t\t|| dir == \"R2Northstar/packages\") {\n\t\treturn {};\n\t}\n\n\tlet files = fs.readdirSync(dir);\n\tlet package_list = {};\n\n\tfor (let i = 0; i < files.length; i++) {\n\t\tlet package_path = path.join(dir, files[i]);\n\t\tlet verification = packages.verify(package_path);\n\n\t\tlet split_name = packages.split_name(files[i]);\n\n\t\tif (! split_name) {continue}\n\n\t\t// make sure the package is actually package\n\t\tswitch(verification) {\n\t\t\tcase true:\n\t\t\tcase \"has-plugins\":\n\t\t\t\tpackage_list[files[i]] = {\n\t\t\t\t\t// adds `author`, `package_name` and `version`\n\t\t\t\t\t...split_name,\n\n\t\t\t\t\ticon: false, // will be set later\n\t\t\t\t\tpackage_path: package_path, // path to package\n\n\t\t\t\t\t// this is whether or not the package has plugins\n\t\t\t\t\thas_plugins: (verification == \"has-plugins\"),\n\n\t\t\t\t\t// this will be set later on\n\t\t\t\t\thas_mods: false,\n\n\t\t\t\t\t// contents of `manifest.json` or `false` if it can\n\t\t\t\t\t// be parsed correctly\n\t\t\t\t\tmanifest: json(\n\t\t\t\t\t\tpath.join(package_path, \"manifest.json\")\n\t\t\t\t\t),\n\t\t\t\t}\n\n\t\t\t\t// if the package has a `mods` folder, and it's not\n\t\t\t\t// empty, then we can assume that the package does\n\t\t\t\t// indeed have mods\n\t\t\t\tlet mods_dir = path.join(package_path, \"mods\");\n\t\t\t\tif (fs.existsSync(mods_dir) &&\n\t\t\t\t\tfs.lstatSync(mods_dir).isDirectory() &&\n\t\t\t\t\tfs.readdirSync(mods_dir).length >= 1) {\n\n\t\t\t\t\tpackage_list[files[i]].has_mods = true;\n\t\t\t\t}\n\n\t\t\t\t// add `.remove()` function, mostly just a shorthand,\n\t\t\t\t// unless `no_functions` is `true`\n\t\t\t\tif (! no_functions) {\n\t\t\t\t\tpackage_list[files[i]].remove = () => {\n\t\t\t\t\t\treturn packages.remove(\n\t\t\t\t\t\t\tsplit_name.author,\n\t\t\t\t\t\t\tsplit_name.package_name,\n\t\t\t\t\t\t\tsplit_name.version,\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// set the `.icon` property\n\t\t\t\tlet icon_file = path.join(package_path, \"icon.png\");\n\t\t\t\tif (fs.existsSync(icon_file) &&\n\t\t\t\t\tfs.lstatSync(icon_file).isFile()) {\n\n\t\t\t\t\tpackage_list[files[i]].icon = icon_file;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\treturn package_list;\n}\n\npackages.remove = (author, package_name, version) => {\n\t// if `version` is not set, we'll search for a package with the same\n\t// `author` and `package_name` and use the version from that,\n\t// this'll be useful when updating, of course this assumes that\n\t// nobody has two versions of the same package installed\n\t//\n\t// TODO: perhaps we should remove duplicate packages?\n\tif (! version) {\n\t\t// get list of packages\n\t\tlet list = packages.list();\n\n\t\t// iterate through them\n\t\tfor (let i in list) {\n\t\t\t// check for `author` and `package_name` being the same\n\t\t\tif (list[i].author == author &&\n\t\t\t\tlist[i].package_name == package_name) {\n\n\t\t\t\t// set `version` to the found package\n\t\t\t\tversion = list[i].version;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\tlet name = packages.format_name(author, package_name, version);\n\tlet package_path = path.join(packages.path, name);\n\t\n\t// make sure the package even exists to begin with\n\tif (! fs.existsSync(package_path)) {\n\t\treturn false;\n\t}\n\n\tfs.rmSync(package_path, {recursive: true});\n\n\t// return the inverse of whether the package still exists, this'll\n\t// be equivalent to whether or not the removal was successful\n\treturn !! fs.existsSync(package_path);\n}\n\npackages.install = async (url, author, package_name, version) => {\n\tupdate_path();\n\n\tif (! fs.existsSync(packages.path)) {\n\t\tconsole.error(\n\t\t\t\"Can't install package, packages folder doesn't exist\",\n\t\t\t\"and couldn't be created\"\n\t\t)\n\n\t\treturn false;\n\t}\n\n\n\tlet name = packages.format_name(author, package_name, version);\n\n\t// removes zip's and folders\n\tlet cleanup = () => {\n\t\tconsole.info(\"Cleaning up cache folder of mod:\", name);\n\t\tif (zip_path && fs.existsSync(zip_path)) {\n\t\t\tfs.rm(zip_path, {recursive: true});\n\t\t\tconsole.ok(\"Cleaned archive of mod:\", name);\n\t\t}\n\n\t\tif (package_path && fs.existsSync(package_path)) {\n\t\t\tfs.rm(zip_path, {recursive: true});\n\t\t\tconsole.ok(\"Cleaned mod folder:\", name);\n\t\t}\n\n\t\tconsole.ok(\"Cleaned up cache folder of mod:\", name);\n\t}\n\n\tconsole.info(\"Downloading package:\", name);\n\t// download `url` to a temporary dir, and return the path to it\n\tlet zip_path = await packages.download(url, name);\n\n\tconsole.info(\"Extracting package:\", name);\n\t// extract the zip file we downloaded before, and return the path of\n\t// the folder that we extracted it to\n\tlet package_path = await packages.extract(zip_path, name);\n\n\n\tconsole.info(\"Verifying package:\", name);\n\tlet verification = packages.verify(package_path);\n\n\tswitch(verification) {\n\t\tcase true: break;\n\t\tcase \"has-plugins\":\n\t\t\t// if the package has plugins, then we want to prompt the\n\t\t\t// user, and make absolutely certain that they do want to\n\t\t\t// install this package, as plugins have security concerns\n\t\t\tlet confirmation = await win().confirm(\n\t\t\t\t`${lang(\"gui.mods.confirm_plugins_title\")} ${name} \\n\\n` +\n\t\t\t\tlang(\"gui.mods.confirm_plugins_description\")\n\t\t\t)\n\n\t\t\t// check whether the user cancelled or confirmed the\n\t\t\t// installation, and act accordingly\n\t\t\tif (! confirmation) {\n\t\t\t\treturn console.ok(\"Cancelled package installation:\", name);\n\t\t\t}\n\t\t\tbreak;\n\t\tdefault:\n\t\t\twin().send(\"failed-mod\", name);\n\n\t\t\t// other unhandled error\n\t\t\tconsole.error(\n\t\t\t\t\"Verification of package failed:\", name,\n\t\t\t\t\", reason:\", verification\n\t\t\t);\n\n\t\t\treturn cleanup();\n\t}\n\n\tconsole.ok(\"Verified package:\", name);\n\n\tconsole.info(\"Deleting older version(s), if it exists:\", name);\n\t// check and delete any mod with the name package details in the old\n\t// `mods` folder, if there are any at all\n\tlet mods = require(\"./mods\");\n\tlet mods_list = mods.list().all;\n\tfor (let i = 0; i < mods_list.length; i++) {\n\t\tlet mod = mods_list[i];\n\n\t\tif (mod.manifest_name == package_name) {\n\t\t\tmods.remove(mod.name);\n\t\t\tcontinue;\n\t\t}\n\n\t\t// normalizes a string, i.e attempt to make two strings\n\t\t// identical, that simply have slightly different formatting, as\n\t\t// an example, these strings:\n\t\t//\n\t\t//   \"Mod_Name\" and \"Mod name\"\n\t\t//\n\t\t// will just become:\n\t\t//\n\t\t//   \"modname\"\n\t\tlet normalize = (string) => {\n\t\t\treturn string.toLowerCase()\n\t\t\t\t.replaceAll(\"_\", \"\")\n\t\t\t\t.replaceAll(\".\", \"\")\n\t\t\t\t.replaceAll(\" \", \"\");\n\t\t}\n\n\t\t// check if the mod's name from it's `mod.json` file when\n\t\t// normalized, is the same as the normalized name of the package\n\t\tif (normalize(mod.name) == normalize(package_name)) {\n\t\t\tmods.remove(mod.name);\n\t\t\tcontinue;\n\t\t}\n\n\t\t// check if the name of the mod's folder when normalized, is the\n\t\t// same as the normalized name of the package\n\t\tif (normalize(mod.folder_name) == normalize(package_name)) {\n\t\t\tmods.remove(mod.name);\n\t\t\tcontinue;\n\t\t}\n\t}\n\n\t// removes older version of package inside the `packages` folder\n\tpackages.remove(author, package_name);\n\tpackages.remove(author, package_name, version);\n\n\tconsole.info(\"Moving package:\", name);\n\tlet moved = packages.move(package_path);\n\n\tif (! moved) {\n\t\twin().send(\"failed-mod\", name);\n\t\tconsole.error(\"Moving package failed:\", name);\n\n\t\tcleanup();\n\n\t\treturn false;\n\t}\n\n\twin().send(\"installed-mod\", {\n\t\tname: name,\n\t\tfancy_name: package_name\n\t})\n\n\tconsole.ok(\"Installed package:\", name);\n\tcleanup();\n\n\treturn true;\n}\n\npackages.download = async (url, name) => {\n\tupdate_path();\n\n\treturn new Promise((resolve) => {\n\t\t// download mod to a temporary location\n\t\thttps.get(url, (res) => {\n\t\t\tlet tmp = path.join(app.getPath(\"userData\"), \"Temp\");\n\n\t\t\tlet zip_name = name || \"package\";\n\t\t\tlet zip_path = path.join(tmp, `${zip_name}.zip`);\n\n\t\t\t// make sure the temporary folder exists\n\t\t\tif (fs.existsSync(tmp)) {\n\t\t\t\t// if it's not a folder, then delete it\n\t\t\t\tif (! fs.statSync(tmp).isDirectory()) {\n\t\t\t\t\tfs.rmSync(tmp);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// create the folder\n\t\t\t\tfs.mkdirSync(tmp);\n\n\t\t\t\t// if there's already a zip file at `zip_path`, then we\n\t\t\t\t// simple remove it, otherwise problems will occur\n\t\t\t\tif (fs.existsSync(zip_path)) {\n\t\t\t\t\tfs.rmSync(zip_path);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// write out the file to the temporary location\n\t\t\tlet stream = fs.createWriteStream(zip_path);\n\t\t\tres.pipe(stream);\n\n\t\t\tstream.on(\"finish\", () => {\n\t\t\t\tstream.close();\n\n\t\t\t\t// return the path of the downloaded zip file\n\t\t\t\tresolve(zip_path);\n\t\t\t})\n\t\t})\n\t})\n}\n\npackages.extract = async (zip_path, name) => {\n\t// this is where everything from `zip_path` will be extracted\n\tlet extract_dir = path.join(path.dirname(zip_path), name);\n\n\t// delete `extract_dir` if it does exist\n\tif (fs.existsSync(extract_dir)) {\n\t\tfs.rmSync(extract_dir, {recursive: true});\n\t}\n\n\t// make an empty folder at `extract_dir`\n\tfs.mkdirSync(extract_dir);\n\n\treturn new Promise((resolve) => {\n\t\tfs.createReadStream(zip_path).pipe(\n\t\t\tunzip.Extract({\n\t\t\t\tpath: extract_dir\n\t\t\t}\n\t\t)).on(\"finish\", () => {\n\t\t\tsetInterval(() => {\n\t\t\t\tresolve(extract_dir);\n\t\t\t}, 1000)\n\t\t});\n\t})\n}\n\npackages.verify = (package_path) => {\n\t// make sure `package_path` is even exists\n\tif (! fs.existsSync(package_path)) {\n\t\treturn \"does-not-exist\";\n\t}\n\n\t// make sure `package_path` is not a folder\n\tif (fs.lstatSync(package_path).isFile()) {\n\t\treturn \"is-file\";\n\t}\n\n\t// make sure a manifest file exists, this is required for\n\t// Thunderstore packages, and is therefore also assumed to be\n\t// required here\n\tlet manifest = path.join(package_path, \"manifest.json\");\n\tif (! fs.existsSync(manifest) ||\n\t\tfs.lstatSync(manifest).isDirectory()) {\n\n\t\treturn \"missing-manifest\";\n\t}\n\n\t// check if there are any plugins in the package\n\tlet mods_path = path.join(package_path, \"mods\");\n\tlet plugins = path.join(package_path, \"plugins\");\n\tif (fs.existsSync(plugins) && fs.lstatSync(plugins).isDirectory()) {\n\t\t// package has plugins, the function calling `packages.verify()`\n\t\t// will have to handle this at their own discretion\n\t\treturn \"has-plugins\";\n\t} else if (! fs.existsSync(mods_path) || fs.lstatSync(mods_path).isFile()) {\n\t\t// if there are no plugins, then we check if there are any mods,\n\t\t// if not, then it means there are both no plugins and mods, so\n\t\t// we signal that back\n\t\treturn \"no-mods\";\n\t}\n\n\t// make sure files in the `mods` folder actually are mods, and if\n\t// none of them are, then we make sure to return back that are no\n\t// mods installed\n\tlet found_mod = false;\n\tlet mods = fs.readdirSync(mods_path);\n\tfor (let i = 0; i < mods.length; i++) {\n\t\tlet mod_file = path.join(mods_path, mods[i], \"mod.json\");\n\n\t\t// make sure mod.json exists, and is a file, otherwise, this\n\t\t// is unlikely to be a mod folder\n\t\tif (! fs.existsSync(mod_file)\n\t\t\t|| ! fs.statSync(mod_file).isFile()) {\n\t\t\tcontinue;\n\t\t}\n\n\t\t// attempt to read the mod.json file, and if it succeeds, then\n\t\t// this is likely to be a mod\n\t\tlet json_data = json(mod_file);\n\t\tif (json_data) {\n\t\t\tfound_mod = true;\n\t\t}\n\t}\n\n\tif (! found_mod) {return \"no-mods\"}\n\n\t// all files exist, and everything is just fine\n\treturn true;\n}\n\n// moves `package_path` to the packages folder\npackages.move = (package_path) => {\n\tupdate_path();\n\n\t// make sure we're actually dealing with a real folder\n\tif (! fs.existsSync(package_path) ||\n\t\t! fs.lstatSync(package_path).isDirectory()) {\n\n\t\treturn false;\n\t}\n\n\t// get path to the package's destination\n\tlet new_path = path.join(\n\t\tpackages.path, path.basename(package_path)\n\t)\n\n\t// attempt to move `package_path` to the packages folder\n\ttry {\n\t\tfs.moveSync(package_path, new_path);\n\t}catch(err) {return false}\n\n\treturn true;\n}\n\nmodule.exports = packages;\n"
  },
  {
    "path": "src/modules/protocol.js",
    "content": "const { app } = require(\"electron\");\n\nconst win = require(\"../win\");\nconst version = require(\"./version\");\n\nmodule.exports = async (argv) => {\n\tif (version.northstar() == \"unknown\")\n\t\treturn;\n\n\tconst args = argv || process.argv;\n\n\tfor (const key of args) {\n\t\tif (key.startsWith(\"ror2mm://\")) {\n\t\t\tlet fragments = key.slice(9).split(\"/\");\n\t\t\n\t\t\tif (fragments.length < 6)\n\t\t\t\treturn;\n\n\t\t\tconst ver = fragments[0];\n\t\t\tconst term = fragments[1];\n\t\t\tconst domain = fragments[2];\n\t\t\tconst author = fragments[3];\n\t\t\tconst package_name = fragments[4];\n\t\t\tconst version = fragments[5];\n\n\t\t\t// There is only v1\n\t\t\tif (ver != \"v1\")\n\t\t\t\tcontinue;\n\n\t\t\t// No support for custom thunderstore instances\n\t\t\tif (domain != \"thunderstore.io\")\n\t\t\t\tcontinue;\n\n\t\t\tif (term == \"install\") {\n\t\t\t\twin().send(\"protocol-install-mod\", [domain, author, package_name, version]);\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/modules/releases.js",
    "content": "const win = require(\"../win\");\nconst ipcMain = require(\"electron\").ipcMain;\n\nconst requests = require(\"./requests\");\n\nlet releases = {\n\tnotes: {},\n\tlatest: {}\n}\n\n// returns release notes to renderer\nipcMain.on(\"get-ns-notes\", async () => {\n\twin().send(\"ns-notes\", await releases.notes.northstar());\n})\n\nipcMain.on(\"get-vp-notes\", async () => {\n\twin().send(\"vp-notes\", await releases.notes.viper());\n})\n\n// gets and returns the release notes of a GitHub repo\nasync function github_releases(repo) {\n\tlet request = false;\n\n\t// attempt to perform the request, while caching it\n\ttry {\n\t\trequest = JSON.parse(await requests.get(\n\t\t\t\"api.github.com\", `/repos/${repo}/releases`,\n\t\t\t\"release-notes-\" + repo\n\t\t))\n\t}catch(err) {\n\t\t// request or parsing failed, return `false`\n\t\treturn false;\n\t}\n\n\t// request is somehow falsy, return `false`\n\tif (! request) {\n\t\treturn false;\n\t}\n\n\t// return the actual request as parsed JSON\n\treturn request;\n}\n\n// returns release notes for Viper\nreleases.notes.viper = async () => {\n\treturn await github_releases(\"0neGal/viper\");\n}\n\n// returns release notes for Northstar\nreleases.notes.northstar = async () => {\n\treturn await github_releases(\"R2Northstar/Northstar\");\n}\n\n// gets and returns some details of the latest release of a GitHub repo\nasync function github_latest(repo) {\n\tlet request = false;\n\n\t// attempt to perform the request, while caching it\n\ttry {\n\t\trequest = JSON.parse(await requests.get(\n\t\t\t\"api.github.com\", `/repos/${repo}/releases/latest`,\n\t\t\t\"latest-release-\" + repo\n\t\t))\n\t}catch(err) {\n\t\t// request or parsing failed, return `false`\n\t\treturn false;\n\t}\n\n\t// request is somehow falsy, return `false`\n\tif (! request) {\n\t\treturn false;\n\t}\n\n\t// return the actual request as parsed JSON\n\treturn {\n\t\tnotes: request.body,\n\t\tversion: request.tag_name,\n\t\tdownload_link: request.assets[0].browser_download_url\n\t}\n}\n\n// returns latest release for Viper\nreleases.latest.viper = async () => {\n\treturn await github_latest(\"0neGal/viper\");\n}\n\n// returns latest release for Northstar\nreleases.latest.northstar = async () => {\n\treturn await github_latest(\"R2Northstar/Northstar\");\n}\n\nmodule.exports = releases;\n"
  },
  {
    "path": "src/modules/requests.js",
    "content": "const fs = require(\"fs\");\nconst path = require(\"path\");\nconst { app, ipcMain } = require(\"electron\");\nconst https = require(\"follow-redirects\").https;\n\nconst json = require(\"./json\");\nconst version = require(\"./version\");\n\nvar cache_dir = app.getPath(\"userData\");\nvar cache_file = path.join(cache_dir, \"cached-requests.json\");\n\n// lets renderer delete request cache\nipcMain.on(\"delete-request-cache\", () => {\n\trequests.cache.delete.all();\n})\n\n// lets renderer use `requests.get()`\nipcMain.handle(\"request\", async (e, ...args) => {\n\tlet res = false;\n\n\ttry {\n\t\tres = await requests.get(...args);\n\t}catch(err) {}\n\n\treturn res;\n})\n\nipcMain.handle(\"request-check\", async (_, ...args) => {\n\tlet res = false;\n\n\ttry {\n\t\tres = await requests.check(...args);\n\t}catch(err) {}\n\n\treturn res;\n})\n\n// updates `cache_dir` and `cache_file`\nfunction set_paths() {\n\tcache_dir = app.getPath(\"userData\");\n\tcache_file = path.join(cache_dir, \"cached-requests.json\");\n}\n\nlet requests = {\n\tcache: {}\n}\n\n// verifies and ensures `cache_dir` exists\nfunction ensure_dir() {\n\tset_paths();\n\n\t// does the folder exist?\n\tlet exists = fs.existsSync(cache_dir);\n\n\t// shorthand for creating folder\n\tlet mkdir = () => {fs.mkdirSync(cache_dir)};\n\n\t// if folder doesn't exist at all, create it\n\tif (! exists) {\n\t\tmkdir();\n\t\treturn;\n\t}\n\n\t// if it does exist, but somehow is a file, remove it, then recreate\n\t// it as an actual folder, wait how did this even happen?\n\tif (exists && fs.statSync(cache_dir).isFile()) {\n\t\tfs.rmSync(cache_dir);\n\t\tmkdir();\n\t}\n}\n\n// check `cache_file` and optionally check for the existence of\n// `cache_key`, and if it exists, return it as is\nlet check_file = (cache_key) => {\n\t// if `cache_file` doesn't exist, or isn't even a file, somehow,\n\t// simply return `false`, and if it wasn't a file, we'll also remove\n\t// the non-file item.\n\tif (! fs.existsSync(cache_file)\n\t\t|| ! fs.statSync(cache_file).isFile()) {\n\n\t\tif (fs.existsSync(cache_file)) {\n\t\t\tfs.rmSync(cache_file, {recursive: true});\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t// attempt to read and parse `cache_file` as JSON\n\tlet file = json(cache_file);\n\n\t// if parsing failed, remove file, and return `false`\n\tif (! file) {\n\t\tfs.rmSync(cache_file);\n\t\treturn false;\n\t}\n\n\tif (! cache_key) {\n\t\treturn file;\n\t}\n\n\t// if `cache_key` isn't found, return `false`\n\tif (! file[cache_key]) {\n\t\treturn false;\n\t}\n\n\treturn file[cache_key];\n}\n\n// attempts to get a `cache_key`'s value, unless it's been set more than\n// `max_time_min` ago, set it to a falsy value to disable\nrequests.cache.get = (cache_key, max_time_min = 5) => {\n\tensure_dir();\n\n\tlet key = check_file(cache_key);\n\n\t// something went wrong with the config file or the key doesn't\n\t// exist, return `false`\n\tif (! key) {\n\t\treturn false;\n\t}\n\n\t// if the key is missing `.data` or `.time`, return `false`\n\tif (! key.data || ! key.time) {\n\t\treturn false;\n\t}\n\n\t// convert from minutes to milliseconds\n\tmax_time_min = max_time_min * 1000 * 60;\n\n\tlet now = new Date().getTime();\n\n\t// check if `key.time` is more than `max_time_min` since it got set\n\tif (now - key.time > max_time_min && max_time_min) {\n\t\treturn false;\n\t}\n\n\treturn key.data;\n}\n\n// attempt to delete `cache_key` from `cache_file`\nrequests.cache.delete = (cache_key) => {\n\tensure_dir();\n\tlet file = check_file();\n\n\t// if something went wrong when checking the `cache_file`, simply\n\t// set the file to an empty Object\n\tif (! file) {\n\t\tfile = {};\n\t}\n\n\tdelete file[cache_key];\n\tfs.writeFileSync(cache_file, JSON.stringify(file));\n}\n\n// deletes all cached keys\nrequests.cache.delete.all = () => {\n\t// paths to files we'll be deleting\n\tlet files = [\n\t\tcache_file,\n\t\tpath.join(app.getPath(\"cache\"), \"viper-requests.json\")\n\t]\n\n\t// run through list of files, and attempt to delete each of them\n\tfor (let i = 0; i < files.length; i++) {\n\t\t// if the file actually exists, delete it!\n\t\tif (fs.existsSync(files[i])) {\n\t\t\tfs.rmSync(files[i], {recursive: true});\n\t\t}\n\t}\n}\n\n// sets `cache_key` to `data` and updates its timestamp\nrequests.cache.set = (cache_key, data) => {\n\tensure_dir();\n\tlet file = check_file();\n\n\t// if something went wrong when checking the `cache_file`, simply\n\t// set the file to an empty Object\n\tif (! file) {\n\t\tfile = {};\n\t}\n\n\tfile[cache_key] = {\n\t\tdata: data,\n\t\ttime: new Date().getTime()\n\t}\n\n\tfs.writeFileSync(cache_file, JSON.stringify(file));\n}\n\n// attempts to `GET` `https://<host>/<path>`, and then returns the\n// result or if it fails it'll reject with `false`\n//\n// if `cache_key` is set, we'll first attempt to check if any valid\n// cache with that key exists, and then return it directly if its still\n// valid cache.\nrequests.get = (host, path, cache_key, ignore_max_time_when_offline = true, max_time_min) => {\n\tlet cached = requests.cache.get(cache_key, max_time_min);\n\tif (cached) {\n\t\treturn cached;\n\t}\n\n\t// we'll use this as the `User-Agent` header for the request\n\tlet user_agent = \"viper/\" + version.viper();\n\n\treturn new Promise((resolve, reject) => {\n\t\t// start `GET` request\n\t\thttps.get({\n\t\t\thost: host,\n\t\t\tport: 443,\n\t\t\tpath: path,\n\t\t\tmethod: \"GET\",\n\t\t\theaders: { \"User-Agent\": user_agent }\n\t\t},\n\n\t\t// on data response\n\t\tresponse => {\n\t\t\t// set correct encoding\n\t\t\tresponse.setEncoding(\"utf8\");\n\n\t\t\t// this'll be filled with incoming data\n\t\t\tlet res_data = \"\";\n\n\t\t\t// data has arrived, add it on `res_data`\n\t\t\tresponse.on(\"data\", data => {\n\t\t\t\tres_data += data;\n\t\t\t})\n\n\t\t\t// request is done, return result\n\t\t\tresponse.on(\"end\", _ => {\n\t\t\t\tresolve(res_data);\n\t\t\t\tif (cache_key) {\n\t\t\t\t\trequests.cache.set(cache_key, res_data);\n\t\t\t\t}\n\t\t\t})\n\t\t})\n\t\t\n\t\t// an error occured\n\t\t.on(\"error\", () => {\n\t\t\tif (ignore_max_time_when_offline) {\n\t\t\t\t// check if the request has been cached before, at all, not\n\t\t\t\t// caring about how long time ago it was, and if it was, we\n\t\t\t\t// simply return that, as a last resort.\n\t\t\t\tcached = requests.cache.get(cache_key, false);\n\n\t\t\t\tif (cached) {\n\t\t\t\t\treturn resolve(cached);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treject(false);\n\t\t})\n\t})\n}\n\n// checks whether a list of `endpoints` can be contacted\nrequests.check = async (endpoints) => {\n\t// turn `endpoints` into an array, if it isn't already\n\tif (typeof endpoints == \"string\") {\n\t\tendpoints = [endpoints];\n\t}\n\n\t// list of what failed and succeeded, will be returned later\n\tlet res = {\n\t\tfailed: [],\n\t\tsucceeded: []\n\t}\n\n\t// run through all the endpoints\n\tfor (let endpoint of endpoints) {\n\t\tlet req;\n\n\t\t// attempt to do a request\n\t\ttry {\n\t\t\treq = await fetch(endpoint);\n\t\t} catch(err) { // something went wrong!\n\t\t\tres.failed.push(endpoint);\n\t\t\tcontinue;\n\t\t}\n\n\t\t// if we're within the `200-299` response code range, we\n\t\t// consider it a success\n\t\tif (req.status < 300 && req.status >= 200) {\n\t\t\tres.succeeded.push(endpoint);\n\t\t\tcontinue;\n\t\t}\n\n\t\t// we failed!\n\t\tres.failed.push(endpoint);\n\t}\n\n\treturn res;\n}\n\nmodule.exports = requests;\n"
  },
  {
    "path": "src/modules/settings.js",
    "content": "const fs = require(\"fs\");\nconst path = require(\"path\");\nconst { app, ipcMain } = require(\"electron\");\n\nconst win = require(\"../win\");\nconst lang = require(\"../lang\");\n\nconsole = require(\"./console\");\nconst json = require(\"./json\");\n\nvar invalid_settings = false;\n\nipcMain.on(\"save-settings\", (event, obj) => {\n\tsave(obj, false);\n})\n\nipcMain.on(\"reset-config\", async () => {\n\tlet confirmation = await win().confirm(\n\t\tlang(\"gui.settings.miscbuttons.reset_config_alert\")\n\t)\n\n\tif (confirmation) {\n\t\tfs.rmSync(\"viper.json\");\n\n\t\tapp.relaunch({\n\t\t\targs: process.argv.slice(1)\n\t\t})\n\n\t\tapp.exit(0);\n\t}\n})\n\nipcMain.on(\"setlang\", (event, lang) => {\n\tset(\"lang\", lang);\n\tsave();\n})\n\n// base settings\nvar settings = {\n\tgamepath: \"\",\n\tlang: \"en-US\",\n\tnsupdate: true,\n\tautolang: true,\n\tforcedlang: \"en\",\n\tautoupdate: true,\n\toriginkill: false,\n\tnsargs: \"-multiple\",\n\tzip: \"/northstar.zip\",\n\n\tlinux_launch_cmd_ns: \"\",\n\tlinux_launch_cmd_vanilla: \"\",\n\tlinux_launch_method: \"steam_auto\",\n\n\t// these files won't be overwritten when installing/updating\n\t// Northstar, useful for config files\n\texcludes: [\n\t\t\"ns_startup_args.txt\",\n\t\t\"ns_startup_args_dedi.txt\"\n\t]\n}\n\n// creates the settings file with the base settings if it doesn't exist.\nif (fs.existsSync(\"viper.json\")) {\n\tlet conf = json(\"viper.json\");\n\n\t// validates viper.json\n\tif (! conf) {\n\t\tinvalid_settings = true;\n\t}\n\n\tsettings = {\n\t\t...settings, ...conf\n\t}\n\n\tsettings.zip = path.join(app.getPath(\"userData\"), \"Temp/northstar.zip\");\n\n\tlet args = path.join(settings.gamepath, \"ns_startup_args.txt\");\n\tif (! settings.nsargs && fs.existsSync(args)) {\n\t\tsettings.nsargs = fs.readFileSync(args, \"utf8\");\n\t}\n} else {\n\tconsole.error(lang(\"general.missing_path\"));\n}\n\n// as to not have to do the same one liner a million times, this\n// function exists, as the name suggests, it simply writes the current\n// settings to the disk.\n//\n// you can also pass a settings object to the function and it'll try and\n// merge it together with the already existing settings\nlet save = (obj = {}, notify_renderer = true) => {\n\t// refuse to save if settings aren't valid\n\tif (invalid_settings) {\n\t\tsettings = {};\n\t}\n\n\tlet settings_content = {\n\t\t...settings, ...obj\n\t}\n\n\tlet stringified_settings = JSON.stringify({\n\t\t...settings, ...obj\n\t})\n\n\tlet settings_file = app.getPath(\"appData\") + \"/viper.json\";\n\n\t// write the settings file\n\tfs.writeFileSync(settings_file, stringified_settings);\n\n\t// set the settings obj for the main process\n\tsettings = settings_content;\n\t\n\tif (notify_renderer) {\n\t\tsend(\"changed-settings\", obj);\n\t}\n}\n\n// sets `key` in `settings` to `value`\nlet set = (key, value) => {\n\tsettings[key] = value;\n}\n\n// returns up-to-date `settings`, along with `set()` and `save()`\nlet export_func = () => {\n\treturn {\n\t\t...settings,\n\n\t\tset,\n\t\tsave\n\t}\n}\n\n// add properties from `settings` to `export_func`, this is just for\n// backwards compatibility, and they really shouldn't be relied on,\n// this'll likely be removed at some point\nfor (let i in settings) {\n\texport_func[i] = settings[i];\n}\n\nmodule.exports = export_func;\n"
  },
  {
    "path": "src/modules/update.js",
    "content": "const path = require(\"path\");\nconst fs = require(\"fs-extra\");\nconst { autoUpdater } = require(\"electron-updater\");\nconst { app, ipcMain, Notification } = require(\"electron\");\n\nconst win = require(\"../win\");\nconst cli = require(\"../cli\");\nconst lang = require(\"../lang\");\n\nconst version = require(\"./version\");\nconst settings = require(\"./settings\");\nconst releases = require(\"./releases\");\nconst gamepath = require(\"./gamepath\");\nconst is_running = require(\"./is_running\");\n\nconsole = require(\"./console\");\n\nconst unzip = require(\"unzip-stream\");\nconst https = require(\"follow-redirects\").https;\n\nlet update = {};\n\nipcMain.on(\"delete-install-cache\", () => {\n\tlet delete_dirs = [\n\t\tpath.join(app.getPath(\"userData\"), \"Temp\"),\n\t\tpath.join(app.getPath(\"cache\"), \"vipertmp\"),\n\t\tpath.join(settings().gamepath, \"northstar.zip\")\n\t]\n\n\tfor (let i = 0; i < delete_dirs.length; i++) {\n\t\tif (fs.existsSync(delete_dirs[i])) {\n\t\t\tfs.rmSync(delete_dirs[i], {recursive: true});\n\t\t}\n\t}\n})\n\nipcMain.on(\"update-northstar\", async (e, force_install) => {\n\tif (await is_running.game()) {\n\t\treturn win().alert(lang(\"general.auto_updates.game_running\"));\n\t}\n\n\tupdate.northstar(force_install);\n})\n\n// inform renderer that an update has been downloaded\nautoUpdater.on(\"update-downloaded\", () => {\n\twin().send(\"update-available\");\n})\n\n// updates and restarts Viper, if user says yes to do so.\n// otherwise it'll do it on the next start up.\nipcMain.on(\"update-now\", () => {\n\tautoUpdater.quitAndInstall();\n})\n\nlet update_active;\n\n// renderer requested a check for whether we can auto updates\nipcMain.on(\"can-autoupdate\", () => {\n\t// is this the first time we're checking?\n\tif (typeof update_active == \"undefined\") {\n\t\t// save auto updater status\n\t\tupdate_active = autoUpdater.isUpdaterActive();\n\t}\n\n\t// if `update_active` is falsy or `--no-vp-updates` is set,\n\t// inform the renderer that auto updates aren't possible\n\tif (! update_active || cli.hasParam(\"no-vp-updates\")) {\n\t\twin().send(\"cant-autoupdate\");\n\t}\n})\n\n\n// renames excluded files to their original name\nfunction restore_excluded_files() {\n\tif (! gamepath.exists()) {return}\n\n\tfor (let i = 0; i < settings().excludes.length; i++) {\n\t\tlet exclude = path.join(settings().gamepath + \"/\" + settings().excludes[i]);\n\t\tif (fs.existsSync(exclude + \".excluded\")) {\n\t\t\tfs.renameSync(exclude + \".excluded\", exclude);\n\t\t}\n\t}\n}; restore_excluded_files();\n\n// renames excluded files to <file>.excluded, the list of files to be\n// exluded is set in the settings (settings().excludes)\nfunction exclude_files() {\n\tfor (let i = 0; i < settings().excludes.length; i++) {\n\t\tlet exclude = path.join(settings().gamepath + \"/\" + settings().excludes[i]);\n\t\tif (fs.existsSync(exclude)) {\n\t\t\tfs.renameSync(exclude, exclude + \".excluded\");\n\t\t}\n\t}\n}\n\n// whether update.northstar_auto_update() has already been run before\nlet is_auto_updating = false;\n\n// handles auto updating Northstar.\n//\n// it uses isGameRunning() to ensure it doesn't run while the game is\n// running, as that may have all kinds of issues.\nupdate.northstar_autoupdate = () => {\n\tif (! settings().nsupdate || ! fs.existsSync(\"viper.json\") || settings().gamepath.length === 0) {\n\t\treturn;\n\t}\n\n\tif (is_auto_updating) {return}\n\n\tasync function _checkForUpdates() {\n\t\tis_auto_updating = true;\n\n\t\tconsole.info(lang(\"cli.auto_updates.checking\"));\n\n\t\t// checks if NS is outdated\n\t\tif (await northstar_update_available()) {\n\t\t\tconsole.ok(lang(\"cli.auto_updates.available\"));\n\t\t\tif (await is_running.game()) {\n\t\t\t\tconsole.error(lang(\"general.auto_updates.game_running\"));\n\t\t\t\tnew Notification({\n\t\t\t\t\ttitle: lang(\"gui.nsupdate.gaming.title\"),\n\t\t\t\t\tbody: lang(\"gui.nsupdate.gaming.body\")\n\t\t\t\t}).show();\n\t\t\t} else {\n\t\t\t\tconsole.info(lang(\"cli.auto_updates.updating_ns\"));\n\t\t\t\tupdate.northstar();\n\t\t\t}\n\t\t} else {\n\t\t\tconsole.info(lang(\"cli.auto_updates.no_update\"));\n\t\t}\n\n\t\tsetTimeout(\n\t\t\t_checkForUpdates,\n\t\t\t15 * 60 * 1000\n\t\t\t// interval in between each update check\n\t\t\t// by default 15 minutes.\n\t\t);\n\t}\n\n\t_checkForUpdates();\n}\n\n// returns whether an update is available for Northstar\nasync function northstar_update_available() {\n\tlet local = version.northstar();\n\tlet distant = (await releases.latest.northstar()).version;\n\n\tif (distant == false) {\n\t\treturn false;\n\t}\n\n\t// checks if NS is outdated\n\tif (local !== distant) {\n\t\treturn true;\n\t} else {\n\t\treturn false;\n\t}\n}\n\n// updates Viper itself\n//\n// this uses electron updater to easily update and publish releases, it\n// simply fetches it from GitHub and updates if it's outdated, very\n// useful. Not much we have to do on our side.\nupdate.viper = (autoinstall) => {\n\t// stop if we're already in the process of updating\n\tif (update.viper.updating) {\n\t\treturn;\n\t}\n\n\tupdate.viper.updating = true;\n\n\tconst { autoUpdater } = require(\"electron-updater\");\n\n\tif (! autoUpdater.isUpdaterActive()) {\n\t\tupdate.viper.updating = false;\n\n\t\tif (settings().nsupdate) {\n\t\t\tupdate.northstar_autoupdate();\n\t\t}\n\n\t\treturn cli.exit();\n\t}\n\n\tif (autoinstall) {\n\t\tautoUpdater.on(\"update-downloaded\", (info) => {\n\t\t\tautoUpdater.quitAndInstall();\n\t\t\tupdate.viper.updating = false;\n\t\t});\n\t}\n\n\tautoUpdater.on(\"error\", (info) => {\n\t\tupdate.viper.updating = false;\n\t\tcli.exit(1)\n\t});\n\n\tautoUpdater.on(\"update-not-available\", (info) => {\n\t\tupdate.viper.updating = false;\n\n\t\t// only check for NS updates if Viper itself has no updates and\n\t\t// if NS auto updates is enabled.\n\t\tif (settings().nsupdate || cli.hasArgs()) {\n\t\t\tupdate.northstar_autoupdate();\n\t\t}\n\n\t\tcli.exit();\n\t});\n\n\tautoUpdater.checkForUpdatesAndNotify();\n}\n\nupdate.viper.updating = false;\n\n// removes all mods in \"R2Northstar/mods\" starting with \"Northstar.\"\nfunction remove_core_mods() {\n\tif (! gamepath.exists()) {return}\n\n\t// make sure the \"R2Northstar/mods\" folder exists, on top of making\n\t// sure that, it is in fact a folder\n\tlet mod_dir = path.join(settings().gamepath, \"R2Northstar/mods\");\n\tif (! fs.existsSync(mod_dir) || fs.statSync(mod_dir).isFile()) {\n\t\treturn;\n\t}\n\n\tlet mods = [];\n\tlet deleted_core_mods = false;\n\n\ttry {\n\t\t// try to get list of items in `mod_dir`\n\t\tmods = fs.readdirSync(mod_dir);\n\t}catch(err) {}\n\n\t// run through list\n\tfor (let i = 0; i < mods.length; i++) {\n\t\t// does the item starts with \"Northstar.\"?\n\t\tif (mods[i].match(\"Northstar\\..*\")) {\n\t\t\t// remove the item\n\t\t\tfs.rmSync(path.join(mod_dir, mods[i]), {\n\t\t\t\trecursive: true\n\t\t\t})\n\n\t\t\tdeleted_core_mods = true;\n\t\t}\n\t}\n\n\t// display message, if we even deleted any mods\n\tif (deleted_core_mods) {\n\t\tconsole.ok(\"Removed existing core mods!\");\n\t}\n}\n\n// installs/Updates Northstar\n//\n// if Northstar is already installed it'll be an update, otherwise it'll\n// install it. It simply downloads the Northstar archive from GitHub, if\n// it's outdated, then extracts it into the game path.\n//\n// as to handle not overwriting files we rename certain files to\n// <file>.excluded, then rename them back after the extraction. The\n// unzip module does not support excluding files directly.\n//\n// `force_install` makes this function not care about whether or not\n// we're already up-to-date, forcing the install\nupdate.northstar = async (force_install) => {\n\t// stop if we're already in the process of updating\n\tif (update.northstar.updating) {\n\t\treturn;\n\t}\n\n\tupdate.northstar.updating = true;\n\n\tif (await is_running.game()) {\n\t\tupdate.northstar.updating = false;\n\t\tconsole.error(lang(\"general.auto_updates.game_running\"));\n\t\treturn false;\n\t}\n\n\tif (! gamepath.exists()) {\n\t\tupdate.northstar.updating = false;\n\t\treturn;\n\t}\n\n\twin().send(\"ns-update-event\", \"cli.update.checking\");\n\tconsole.info(lang(\"cli.update.checking\"));\n\tlet ns_version = version.northstar();\n\n\tlet latest = await releases.latest.northstar();\n\n\tif (latest && latest.version == false) {\n\t\tupdate.northstar.updating = false;\n\t\twin().send(\"ns-update-event\", \"cli.update.noInternet\");\n\t\treturn;\n\t}\n\n\t// Makes sure it is not already the latest version\n\tif (! force_install && ! await northstar_update_available()) {\n\t\twin().send(\"ns-update-event\", \"cli.update.uptodate_short\");\n\t\tconsole.ok(lang(\"cli.update.uptodate\").replace(\"%s\", ns_version));\n\n\t\twin().log(lang(\"gui.update.uptodate\"));\n\n\t\tupdate.northstar.updating = false;\n\n\t\tcli.exit();\n\t\treturn;\n\t} else {\n\t\tif (ns_version != \"unknown\") {\n\t\t\tconsole.info(lang(\"cli.update.current\"), ns_version);\n\t\t}\n\t}\n\n\texclude_files();\n\n\t// start the download of the zip\n\thttps.get(latest.download_link, (res) => {\n\t\t// cancel out if zip can't be retrieved and or found\n\t\tif (res.statusCode !== 200) {\n\t\t\twin().send(\"ns-update-event\", \"cli.update.uptodate_short\");\n\t\t\tconsole.ok(lang(\"cli.update.uptodate\"), ns_version);\n\t\t\tupdate.northstar.updating = false;\n\t\t\treturn false;\n\t\t}\n\n\t\tlet unknown_size = false;\n\t\tlet content_length_mb = 0.0;\n\t\tlet content_length = res.headers[\"content-length\"];\n\n\t\t// if content_length is `undefined`, we can't get the complete\n\t\t// size (not all servers send the content-length)\n\t\tif (content_length == undefined) {\n\t\t\tunknown_size = true;\n\t\t} else {\n\t\t\tcontent_length = parseInt(content_length);\n\n\t\t\tif (isNaN(content_length)) {\n\t\t\t\tunknown_size = true;\n\t\t\t} else {\n\t\t\t\tcontent_length_mb = (content_length / 1024 / 1024).toFixed(1);\n\t\t\t}\n\t\t}\n\n\t\tconsole.info(lang(\"cli.update.downloading\") + \":\", latest.version);\n\t\twin().send(\"ns-update-event\", {\n\t\t\tprogress: 0,\n\t\t\tbtn_text: \"1/2\",\n\t\t\tkey: \"cli.update.downloading\",\n\t\t})\n\n\t\tlet tmp = path.dirname(settings().zip);\n\n\t\tif (fs.existsSync(tmp)) {\n\t\t\tif (! fs.statSync(tmp).isDirectory()) {\n\t\t\t\tfs.rmSync(tmp);\n\t\t\t}\n\t\t} else {\n\t\t\tfs.mkdirSync(tmp);\n\t\t\tif (fs.existsSync(settings().zip)) {\n\t\t\t\tfs.rmSync(settings().zip);\n\t\t\t}\n\t\t}\n\n\t\tlet stream = fs.createWriteStream(settings().zip);\n\t\tres.pipe(stream);\n\n\t\tlet received = 0;\n\t\tres.on(\"data\", (chunk) => {\n\t\t\treceived += chunk.length;\n\t\t\tlet received_mb = (received / 1024 / 1024).toFixed(1);\n\n\t\t\tlet percentage_str = \"\";\n\t\t\tlet current_percentage = 0;\n\n\t\t\tlet key = lang(\"gui.update.downloading\") + \" \" + received_mb + \"mb\";\n\n\t\t\tif (unknown_size === false) {\n\t\t\t\tkey += \" / \" + content_length_mb + \"mb\";\n\t\t\t\tcurrent_percentage = Math.floor(received_mb / content_length_mb * 100);\n\t\t\t\tpercentage_str = \" - \" + current_percentage + \"%\";\n\t\t\t}\n\n\t\t\twin().send(\"ns-update-event\", {\n\t\t\t\tkey: key,\n\t\t\t\tprogress: current_percentage,\n\t\t\t\tbtn_text: \"1/2\" + percentage_str\n\t\t\t});\n\t\t})\n\n\t\tstream.on(\"finish\", () => {\n\t\t\tremove_core_mods();\n\n\t\t\tstream.close();\n\t\t\tlet extract = fs.createReadStream(settings().zip);\n\n\t\t\twin().log(lang(\"gui.update.extracting\"));\n\t\t\twin().send(\"ns-update-event\", {\n\t\t\t\tprogress: 0,\n\t\t\t\tbtn_text: \"2/2 - 0%\",\n\t\t\t\tkey: lang(\"gui.update.extracting\")\n\t\t\t});\n\n\t\t\tconsole.ok(lang(\"cli.update.download_done\"));\n\n\t\t\tlet destination = unzip.Extract({path: settings().gamepath});\n\n\t\t\t// If we receive multiple errors of the same type we ignore them\n\t\t\tlet received_errors = [];\n\t\t\tdestination.on(\"error\", (err) => {\n\t\t\t\tif (received_errors.indexOf(err.code) >= 0)\n\t\t\t\t\treturn;\n\n\t\t\t\treceived_errors.push(err.code);\n\t\t\t\textract.close();\n\t\t\t\tupdate.northstar.updating = false;\n\n\t\t\t\tlet description = lang(\"gui.toast.desc.unknown_error\") + \" (\" + err.code + \")\";\n\n\t\t\t\twin().toast({\n\t\t\t\t\tscheme: \"error\",\n\t\t\t\t\ttitle: lang(\"gui.toast.title.failed\"),\n\t\t\t\t\tdescription: description\n\t\t\t\t})\n\n\t\t\t\twin().send(\"ns-update-event\", \"cli.update.failed\");\n\t\t\t})\n\n\t\t\t// extracts the zip, this is the part where we're actually\n\t\t\t// installing Northstar.\n\t\t\textract.pipe(destination)\n\n\t\t\tlet extracted = 0;\n\t\t\tlet size = received;\n\t\t\tlet size_mb = (size / 1024 / 1024).toFixed(1);\n\n\t\t\textract.on(\"data\", (chunk) => {\n\t\t\t\textracted += chunk.length;\n\t\t\t\tlet percent = Math.floor(extracted / size * 100);\n\t\t\t\tlet extracted_mb = (extracted / 1024 / 1024).toFixed(1);\n\n\t\t\t\twin().send(\"ns-update-event\", {\n\t\t\t\t\tprogress: percent,\n\t\t\t\t\tbtn_text: \"2/2 - \" + percent + \"%\",\n\t\t\t\t\tkey: lang(\"gui.update.extracting\") +\n\t\t\t\t\t\t\" \" + extracted_mb + \"mb / \" + size_mb + \"mb\"\n\t\t\t\t});\n\t\t\t})\n\n\t\t\textract.on(\"end\", () => {\n\t\t\t\textract.close();\n\t\t\t\tipcMain.emit(\"getversion\");\n\n\t\t\t\trestore_excluded_files();\n\n\t\t\t\tipcMain.emit(\"gui-getmods\");\n\t\t\t\tipcMain.emit(\"get-version\");\n\t\t\t\twin().send(\"ns-update-event\", \"cli.update.uptodate_short\");\n\t\t\t\twin().log(lang(\"gui.update.finished\"));\n\t\t\t\tconsole.ok(lang(\"cli.update.finished\"));\n\n\t\t\t\tupdate.northstar.updating = false;\n\n\t\t\t\tcli.exit();\n\t\t\t})\n\t\t})\n\t})\n}\n\nupdate.northstar.updating = false;\n\nmodule.exports = update;\n"
  },
  {
    "path": "src/modules/version.js",
    "content": "const path = require(\"path\");\nconst fs = require(\"fs-extra\");\nconst ipcMain = require(\"electron\").ipcMain;\n\nconst win = require(\"../win\");\n\nconst json = require(\"./json\");\nconst settings = require(\"./settings\");\n\nlet version = {};\n\n// sends the version info back to the renderer\nipcMain.on(\"get-version\", () => {\n\tversion.send_info();\n})\n\n// retrieves various local version numbers and sends them to the renderer\nversion.send_info = () => {\n\twin().send(\"version\", {\n\t\tns: version.northstar(),\n\t\ttf2: version.titanfall(),\n\t\tvp: \"v\" + version.viper()\n\t})\n}\n\n// returns the current Northstar version\n// if not installed it'll return \"unknown\"\nversion.northstar = () => {\n\t// if NorthstarLauncher.exe doesn't exist, always return \"unknown\"\n\tif (! fs.existsSync(path.join(settings().gamepath, \"NorthstarLauncher.exe\"))) {\n\t\treturn \"unknown\";\n\t}\n\n\t// mods to check version of\n\tvar versionFiles = [\n\t\t\"Northstar.Client\",\n\t\t\"Northstar.Custom\",\n\t\t\"Northstar.CustomServers\"\n\t]\n\n\tvar versions = [];\n\n\n\tlet add = (version) => {\n\t\tversions.push(version)\n\t}\n\n\t// checks version of mods\n\tfor (let i = 0; i < versionFiles.length; i++) {\n\t\tvar versionFile = path.join(settings().gamepath, \"R2Northstar/mods/\", versionFiles[i],\"/mod.json\");\n\t\tif (fs.existsSync(versionFile)) {\n\t\t\tif (! fs.statSync(versionFile).isFile()) {\n\t\t\t\tadd(\"unknown\");\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tadd(\"v\" + json(versionFile).Version);\n\t\t\t}catch(err) {\n\t\t\t\tadd(\"unknown\");\n\t\t\t}\n\t\t} else {\n\t\t\tadd(\"unknown\");\n\t\t}\n\t}\n\n\tif (versions.includes(\"unknown\")) {return \"unknown\"}\n\n\t// verifies all mods have the same version number\n\tlet mismatch = false;\n\tlet baseVersion = versions[0];\n\tfor (let i = 0; i < versions.length; i++) {\n\t\tif (versions[i] != baseVersion) {\n\t\t\tmismatch = true;\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif (mismatch) {return \"unknown\"}\n\treturn baseVersion;\n}\n\n// returns the Titanfall 2 version from gameversion.txt file.\n// If it fails it simply returns \"unknown\"\n//\n// TODO: This file is present on Origin install, should check if it's\n// present with Steam install as well.\nversion.titanfall = () => {\n\tvar versionFilePath = path.join(settings().gamepath, \"gameversion.txt\");\n\tif (fs.existsSync(versionFilePath)) {\n\t\treturn fs.readFileSync(versionFilePath, \"utf8\").trim();\n\t} else {\n\t\treturn \"unknown\";\n\t}\n}\n\n// returns Viper's current version, taken from `package.json`\nversion.viper = () => {\n\treturn json(\n\t\tpath.join(__dirname, \"../../package.json\")\n\t).version;\n}\n\nmodule.exports = version;\n"
  },
  {
    "path": "src/win.js",
    "content": "const ipcMain = require(\"electron\").ipcMain;\n\n// logs into the dev tools of the renderer\nlog = (...args) => {\n\twin.send(\"log\", ...args);\n}\n\n// this increments for every alert that's created, the ID is used to\n// keep track of popups being opened or closed.\nlet alert_id = 0;\n\n// sends an alert to the renderer\nalert = async (msg) => {\n\talert_id++;\n\n\treturn new Promise((resolve) => {\n\t\tipcMain.once(`alert-closed-${alert_id}`, () => {\n\t\t\tresolve();\n\t\t})\n\n\t\twin.send(\"alert\", {\n\t\t\tid: alert_id,\n\t\t\tmessage: msg\n\t\t})\n\t})\n}\n\n// sends an toast to the renderer\ntoast = (properties) => {\n\twin.send(\"toast\", properties);\n}\n\n// this increments for every confirm alert that's created, the ID is\n// used to keep track of popups being opened or closed.\nlet confirm_id = 0;\n\n// sends an alert to the renderer\nconfirm = async (msg) => {\n\tconfirm_id++;\n\n\treturn new Promise((resolve) => {\n\t\tipcMain.once(`confirm-closed-${confirm_id}`, (event, confirmed) => {\n\t\t\tresolve(confirmed);\n\t\t})\n\n\t\twin.send(\"confirm\", {\n\t\t\tmessage: msg,\n\t\t\tid: confirm_id\n\t\t})\n\t})\n}\n\nlet win = {\n\tsend: () => {},\n\n\tlog: log,\n\ttoast: toast,\n\talert: alert,\n\tconfirm: confirm\n}\n\nlet func = () => {\n\treturn win;\n}\n\nfunc.set = (main_window) => {\n\twin = main_window;\n\n\twin.log = log;\n\twin.toast = toast;\n\twin.alert = alert;\n\twin.confirm = confirm;\n}\n\nmodule.exports = func;\n"
  }
]