diff --git a/README.md b/README.md index 9c5a223f..3385d52e 100644 --- a/README.md +++ b/README.md @@ -1,216 +1,216 @@ -

aventium softworks

- -

Helios Launcher

- -
(formerly Electron Launcher)
- -[

travis](https://travis-ci.org/dscalzi/HeliosLauncher) [downloads](https://github.com/dscalzi/HeliosLauncher/releases) stark

- -

Join modded servers without worrying about installing Java, Forge, or other mods. We'll handle that for you.

- -![Screenshot 1](https://i.imgur.com/6o7SmH6.png) -![Screenshot 2](https://i.imgur.com/x3B34n1.png) - -## Features - -* 🔒 Full account management. - * Add multiple accounts and easily switch between them. - * Credentials are never stored and transmitted directly to Mojang. -* 📂 Efficient asset management. - * Receive client updates as soon as we release them. - * Files are validated before launch. Corrupt or incorrect files will be redownloaded. -* ☕ **Automatic Java validation.** - * If you have an incompatible version of Java installed, we'll install the right one *for you*. - * You do not need to have Java installed to run the launcher. -* 📰 News feed natively built into the launcher. -* ⚙️ Intuitive settings management, including a Java control panel. -* Supports all of our servers. - * Switch between server configurations with ease. - * View the player count of the selected server. -* Automatic updates. That's right, the launcher updates itself. -* View the status of Mojang's services. - -This is not an exhaustive list. Download and install the launcher to gauge all it can do! - -#### Need Help? [Check the wiki.][wiki] - -#### Like the project? Leave a ⭐ star on the repository! - -## Downloads - -You can download from [GitHub Releases](https://github.com/dscalzi/HeliosLauncher/releases) - -#### Latest Release - -[![](https://img.shields.io/github/release/dscalzi/HeliosLauncher.svg?style=flat-square)](https://github.com/dscalzi/HeliosLauncher/releases/latest) - -#### Latest Pre-Release -[![](https://img.shields.io/github/release/dscalzi/HeliosLauncher/all.svg?style=flat-square)](https://github.com/dscalzi/HeliosLauncher/releases) - -**Supported Platforms** - -If you download from the [Releases](https://github.com/dscalzi/HeliosLauncher/releases) tab, select the installer for your system. - -| Platform | File | -| -------- | ---- | -| Windows x64 | `helioslauncher-setup-VERSION.exe` | -| macOS | `helioslauncher-VERSION.dmg` | -| Linux x64 | `helioslauncher-VERSION-x86_64.AppImage` | - -## Console - -To open the console, use the following keybind. - -```console -ctrl + shift + i -``` - -Ensure that you have the console tab selected. Do not paste anything into the console unless you are 100% sure of what it will do. Pasting the wrong thing can expose sensitive information. - -#### Export Output to a File - -If you want to export the console output, simply right click anywhere on the console and click **Save as..** - -![console example](https://i.imgur.com/T5e73jP.png) - - -## Development - -### Getting Started - -**System Requirements** - -* [Node.js][nodejs] v12 - ---- - -**Clone and Install Dependencies** - -```console -> git clone https://github.com/dscalzi/HeliosLauncher.git -> cd HeliosLauncher -> npm install -``` - ---- - -**Launch Application** - -```console -> npm start -``` - ---- - -**Build Installers** - -To build for your current platform. - -```console -> npm run dist -``` - -Build for a specific platform. - -| Platform | Command | -| ----------- | -------------------- | -| Windows x64 | `npm run dist:win` | -| macOS | `npm run dist:mac` | -| Linux x64 | `npm run dist:linux` | - -Builds for macOS may not work on Windows/Linux and vice-versa. - ---- - -### Visual Studio Code - -All development of the launcher should be done using [Visual Studio Code][vscode]. - -Paste the following into `.vscode/launch.json` - -```JSON -{ - "version": "0.2.0", - "configurations": [ - { - "name": "Debug Main Process", - "type": "node", - "request": "launch", - "cwd": "${workspaceFolder}", - "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron", - "windows": { - "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd" - }, - "args": ["."], - "console": "integratedTerminal", - "protocol": "inspector" - }, - { - "name": "Debug Renderer Process", - "type": "chrome", - "request": "launch", - "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron", - "windows": { - "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd" - }, - "runtimeArgs": [ - "${workspaceFolder}/.", - "--remote-debugging-port=9222" - ], - "webRoot": "${workspaceFolder}" - } - ] -} -``` - -This adds two debug configurations. - -#### Debug Main Process - -This allows you to debug Electron's [main process][mainprocess]. You can debug scripts in the [renderer process][rendererprocess] by opening the DevTools Window. - -#### Debug Renderer Process - -This allows you to debug Electron's [renderer process][rendererprocess]. This requires you to install the [Debugger for Chrome][chromedebugger] extension. - -Note that you **cannot** open the DevTools window while using this debug configuration. Chromium only allows one debugger, opening another will crash the program. - ---- - -### Note on Third-Party Usage - -You may use this software in your own project so long as the following conditions are met. - -* Credit is expressly given to the original authors (Daniel Scalzi). - * Include a link to the original source on the launcher's About page. - * Credit the authors and provide a link to the original source in any publications or download pages. -* The source code remain **public** as a fork of this repository. - -We reserve the right to update these conditions at any time, please check back periodically. - ---- - -## Resources - -* [Wiki][wiki] -* [Nebula (Create Distribution.json)][nebula] -* [v2 Rewrite Branch (WIP)][v2branch] - -The best way to contact the developers is on Discord. - -[![discord](https://discordapp.com/api/guilds/211524927831015424/embed.png?style=banner3)][discord] - ---- - -### See you ingame. - - -[nodejs]: https://nodejs.org/en/ 'Node.js' -[vscode]: https://code.visualstudio.com/ 'Visual Studio Code' -[mainprocess]: https://electronjs.org/docs/tutorial/application-architecture#main-and-renderer-processes 'Main Process' -[rendererprocess]: https://electronjs.org/docs/tutorial/application-architecture#main-and-renderer-processes 'Renderer Process' -[chromedebugger]: https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome 'Debugger for Chrome' -[discord]: https://discord.gg/zNWUXdt 'Discord' -[wiki]: https://github.com/dscalzi/HeliosLauncher/wiki 'wiki' -[nebula]: https://github.com/dscalzi/Nebula 'dscalzi/Nebula' -[v2branch]: https://github.com/dscalzi/HeliosLauncher/tree/ts-refactor 'v2 branch' +

aventium softworks

+ +

Creeponnia Launcher

+ +
(formerly Electron Launcher)
+ +[

travis](https://travis-ci.org/dscalzi/HeliosLauncher) [downloads](https://github.com/dscalzi/HeliosLauncher/releases) stark

+ +

Join modded servers without worrying about installing Java, Forge, or other mods. We'll handle that for you.

+ +![Screenshot 1](https://i.imgur.com/6o7SmH6.png) +![Screenshot 2](https://i.imgur.com/x3B34n1.png) + +## Features + +* 🔒 Full account management. + * Add multiple accounts and easily switch between them. + * Credentials are never stored and transmitted directly to Mojang. +* 📂 Efficient asset management. + * Receive client updates as soon as we release them. + * Files are validated before launch. Corrupt or incorrect files will be redownloaded. +* ☕ **Automatic Java validation.** + * If you have an incompatible version of Java installed, we'll install the right one *for you*. + * You do not need to have Java installed to run the launcher. +* 📰 News feed natively built into the launcher. +* ⚙️ Intuitive settings management, including a Java control panel. +* Supports all of our servers. + * Switch between server configurations with ease. + * View the player count of the selected server. +* Automatic updates. That's right, the launcher updates itself. +* View the status of Mojang's services. + +This is not an exhaustive list. Download and install the launcher to gauge all it can do! + +#### Need Help? [Check the wiki.][wiki] + +#### Like the project? Leave a ⭐ star on the repository! + +## Downloads + +You can download from [GitHub Releases](https://github.com/dscalzi/HeliosLauncher/releases) + +#### Latest Release + +[![](https://img.shields.io/github/release/dscalzi/HeliosLauncher.svg?style=flat-square)](https://github.com/dscalzi/HeliosLauncher/releases/latest) + +#### Latest Pre-Release +[![](https://img.shields.io/github/release/dscalzi/HeliosLauncher/all.svg?style=flat-square)](https://github.com/dscalzi/HeliosLauncher/releases) + +**Supported Platforms** + +If you download from the [Releases](https://github.com/dscalzi/HeliosLauncher/releases) tab, select the installer for your system. + +| Platform | File | +| -------- | ---- | +| Windows x64 | `helioslauncher-setup-VERSION.exe` | +| macOS | `helioslauncher-VERSION.dmg` | +| Linux x64 | `helioslauncher-VERSION-x86_64.AppImage` | + +## Console + +To open the console, use the following keybind. + +```console +ctrl + shift + i +``` + +Ensure that you have the console tab selected. Do not paste anything into the console unless you are 100% sure of what it will do. Pasting the wrong thing can expose sensitive information. + +#### Export Output to a File + +If you want to export the console output, simply right click anywhere on the console and click **Save as..** + +![console example](https://i.imgur.com/T5e73jP.png) + + +## Development + +### Getting Started + +**System Requirements** + +* [Node.js][nodejs] v12 + +--- + +**Clone and Install Dependencies** + +```console +> git clone https://github.com/dscalzi/HeliosLauncher.git +> cd HeliosLauncher +> npm install +``` + +--- + +**Launch Application** + +```console +> npm start +``` + +--- + +**Build Installers** + +To build for your current platform. + +```console +> npm run dist +``` + +Build for a specific platform. + +| Platform | Command | +| ----------- | -------------------- | +| Windows x64 | `npm run dist:win` | +| macOS | `npm run dist:mac` | +| Linux x64 | `npm run dist:linux` | + +Builds for macOS may not work on Windows/Linux and vice-versa. + +--- + +### Visual Studio Code + +All development of the launcher should be done using [Visual Studio Code][vscode]. + +Paste the following into `.vscode/launch.json` + +```JSON +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug Main Process", + "type": "node", + "request": "launch", + "cwd": "${workspaceFolder}", + "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron", + "windows": { + "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd" + }, + "args": ["."], + "console": "integratedTerminal", + "protocol": "inspector" + }, + { + "name": "Debug Renderer Process", + "type": "chrome", + "request": "launch", + "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron", + "windows": { + "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd" + }, + "runtimeArgs": [ + "${workspaceFolder}/.", + "--remote-debugging-port=9222" + ], + "webRoot": "${workspaceFolder}" + } + ] +} +``` + +This adds two debug configurations. + +#### Debug Main Process + +This allows you to debug Electron's [main process][mainprocess]. You can debug scripts in the [renderer process][rendererprocess] by opening the DevTools Window. + +#### Debug Renderer Process + +This allows you to debug Electron's [renderer process][rendererprocess]. This requires you to install the [Debugger for Chrome][chromedebugger] extension. + +Note that you **cannot** open the DevTools window while using this debug configuration. Chromium only allows one debugger, opening another will crash the program. + +--- + +### Note on Third-Party Usage + +You may use this software in your own project so long as the following conditions are met. + +* Credit is expressly given to the original authors (Daniel Scalzi). + * Include a link to the original source on the launcher's About page. + * Credit the authors and provide a link to the original source in any publications or download pages. +* The source code remain **public** as a fork of this repository. + +We reserve the right to update these conditions at any time, please check back periodically. + +--- + +## Resources + +* [Wiki][wiki] +* [Nebula (Create Distribution.json)][nebula] +* [v2 Rewrite Branch (WIP)][v2branch] + +The best way to contact the developers is on Discord. + +[![discord](https://discordapp.com/api/guilds/211524927831015424/embed.png?style=banner3)][discord] + +--- + +### See you ingame. + + +[nodejs]: https://nodejs.org/en/ 'Node.js' +[vscode]: https://code.visualstudio.com/ 'Visual Studio Code' +[mainprocess]: https://electronjs.org/docs/tutorial/application-architecture#main-and-renderer-processes 'Main Process' +[rendererprocess]: https://electronjs.org/docs/tutorial/application-architecture#main-and-renderer-processes 'Renderer Process' +[chromedebugger]: https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome 'Debugger for Chrome' +[discord]: https://discord.gg/zNWUXdt 'Discord' +[wiki]: https://github.com/dscalzi/HeliosLauncher/wiki 'wiki' +[nebula]: https://github.com/dscalzi/Nebula 'dscalzi/Nebula' +[v2branch]: https://github.com/dscalzi/HeliosLauncher/tree/ts-refactor 'v2 branch' diff --git a/app/app.ejs b/app/app.ejs index 499c10d5..b5412c90 100644 --- a/app/app.ejs +++ b/app/app.ejs @@ -1,53 +1,53 @@ - - - - Helios Launcher - - - - - - - <%- include('frame') %> -
- <%- include('welcome') %> - <%- include('login') %> - <%- include('settings') %> - <%- include('landing') %> -
- <%- include('overlay') %> -
-
-
- - -
-
-
- - + + + + Creeponnia Launcher + + + + + + + <%- include('frame') %> +
+ <%- include('welcome') %> + <%- include('login') %> + <%- include('settings') %> + <%- include('landing') %> +
+ <%- include('overlay') %> +
+
+
+ + +
+
+
+ + \ No newline at end of file diff --git a/app/assets/css/launcher.css b/app/assets/css/launcher.css index 56816997..08ee4714 100644 --- a/app/assets/css/launcher.css +++ b/app/assets/css/launcher.css @@ -1,3777 +1,3777 @@ -/* Github Code Highlighting. */ -@import "../../../node_modules/github-syntax-dark/lib/github-dark.css"; - -/******************************************************************************* - * * - * Fonts * - * * - ******************************************************************************/ - -@font-face { - font-family: 'Avenir Book'; - src: url('../fonts/Avenir-Book.ttf'); -} - -@font-face { - font-family: 'Avenir Medium'; - src: url('../fonts/Avenir-Medium.ttf'); -} - -@font-face { - font-family: 'Ringbearer'; - src: url('../fonts/Ringbearer.ttf'); -} - -/******************************************************************************* - * * - * Element Styles * - * * - ******************************************************************************/ - -/* Reset body, html, and div presets. */ -body, html, div { - margin: 0px; - padding: 0px; -} - -/* Reset p presets. */ -p { - -webkit-margin-before: 0em; - -webkit-margin-after: 0em; -} - -/* Set default font and color. */ -body, button { - font-family: 'Avenir Book'; - color: white; -} - -/*body { - background: url('./../images/backgrounds/0.jpg') no-repeat center center fixed; - background-size: cover; -}*/ - -/******************************************************************************* - * * - * Frame Styles (frame.ejs) * - * * - ******************************************************************************/ - -/* Frame Bar */ -#frameBar { - position: relative; - z-index: 100; - display: flex; - flex-direction: column; - transition: background-color 1s ease; - /*background-color: rgba(0, 0, 0, 0.5);*/ - -webkit-user-select: none; -} - -/* Undraggable region on the top of the frame. */ -#frameResizableTop { - height: 2px; - width: 100%; - -webkit-app-region: no-drag; -} - -/* Flexbox to wrap the main frame content. */ -#frameMain { - display: flex; - height: 20px -} - -/* Undraggable region on the left and right of the frame. */ -.frameResizableVert { - width: 2px; - -webkit-app-region: no-drag; -} - -/* Main frame content for windows. */ -#frameContentWin { - display: flex; - justify-content: space-between; - width: 100%; - -webkit-app-region: drag; -} - -/* Main frame content for darwin. */ -#frameContentDarwin { - display: flex; - justify-content: flex-start; - align-items: center; - width: 100%; - -webkit-app-region: drag; -} - -/* Frame logo (windows only). */ -#frameTitleDock { - padding: 0px 10px; -} -#frameTitleText { - font-size: 14px; - font-family: 'Avenir Medium'; - letter-spacing: 0.5px; -} - -/* Windows frame button dock. */ -#frameButtonDockWin { - -webkit-app-region: no-drag !important; - position: relative; - top: -2px; - right: -2px; - height: 22px; -} -#frameButtonDockWin > .frameButton:not(:first-child) { - margin-left: -4px; -} - -/* Darwin frame button dock: NaN; */ -#frameButtonDockDarwin { - -webkit-app-region: no-drag !important; - position: relative; - top: -1px; - right: -1px; -} - -/* Windows Frame Button Styles. */ -.frameButton { - background: none; - border: none; - height: 22px; - width: 39px; - cursor: pointer; -} -.frameButton:hover, -.frameButton:focus { - background: rgba(189, 189, 189, 0.43); -} -.frameButton:active { - background: rgba(156, 156, 156, 0.43); -} -.frameButton:focus { - outline: 0px; -} - -/* Close button is red. */ -#frameButton_close:hover, -#frameButton_close:focus { - background: rgba(255, 53, 53, 0.61) !important; -} -#frameButton_close:active { - background: rgba(235, 0, 0, 0.61) !important; -} - -/* Darwin Frame Button Styles. */ -.frameButtonDarwin { - height: 12px; - width: 12px; - border-radius: 50%; - border: 0px; - margin-left: 5px; - -webkit-app-region: no-drag !important; - cursor: pointer; -} -.frameButtonDarwin:focus { - outline: 0px; -} - -#frameButtonDarwin_close { - background-color: #e74c32; -} -#frameButtonDarwin_close:hover, -#frameButtonDarwin_close:focus { - background-color: #FF9A8A; -} -#frameButtonDarwin_close:active { - background-color: #ff8d7b; -} - -#frameButtonDarwin_minimize { - background-color: #fed045; -} -#frameButtonDarwin_minimize:hover, -#frameButtonDarwin_minimize:focus { - background-color: #FFE9A9; -} -#frameButtonDarwin_minimize:active { - background-color: #ffde7b; -} - -#frameButtonDarwin_restoredown { - background-color: #96e734; -} -#frameButtonDarwin_restoredown:hover, -#frameButtonDarwin_restoredown:focus { - background-color: #D6FFA6; -} -#frameButtonDarwin_restoredown:active { - background-color: #bfff76; -} - -/******************************************************************************* - * * - * Welcome View (welcome.ejs) * - * * - ******************************************************************************/ - -#welcomeContainer { - position: relative; - display: flex; - justify-content: center; - align-items: center; - height: 100%; - width: 100%; -} - -#welcomeContent { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - width: 50%; - top: -10%; - position: relative; -} - -/* -.cloudDiv { - position: absolute; - height: 100%; - width: 100%; - display: flex; - flex-direction: column; -} - -.cloudTop { - height: 50%; - width: 100%; - background-image: url('../images/cloudTrans.png'); - animation: clouds1 80s linear infinite; - background-size: cover; -} - -.cloudBottom { - height: 50%; - width: 100%; - background-image: url('../images/cloudTrans2.png'); - animation: clouds2 70s linear infinite; - background-size: cover; -} - -@keyframes clouds1 { - to { - background-position: 200%; - } -} -@keyframes clouds2 { - to { - background-position: 230%; - } -} -*/ - -#welcomeImageSeal { - border-radius: 50%; - border: 2px solid #cad7e1; - background: rgba(1, 2, 1, 0.5); - height: 125px; - width: 125px; - box-shadow: 0px 0px 10px 0px rgb(0, 0, 0); - margin-bottom: 5%; - margin-top: 10%; -} - -#welcomeHeader { - font-family: 'Avenir Medium'; - text-align: center; - color: white; - margin-bottom: 25px; - letter-spacing: 1px; - font-size: 20px; - text-shadow: white 0px 0px 0px; -} - -#welcomeDescription { - text-align: justify; - font-size: 13px; - font-weight: 100; - text-shadow: rgba(255, 255, 255, 0.75) 0px 0px 20px -} - -#welcomeDescCTA { - text-align: center; - font-size: 14px; - font-weight: 100; - text-shadow: rgba(255, 255, 255, 0.75) 0px 0px 20px -} - -/* Login button styles. */ -#welcomeButton { - background: none; - font-weight: bold; - letter-spacing: 2px; - border: none; - padding: 15px 5px; - margin: 10px 0px; - cursor: pointer; - position: relative; - right: -20px; - transition: 0.5s ease; - margin-top: 5%; - margin-bottom: -5%; -} -#welcomeButton:disabled { - color: rgba(255, 255, 255, 0.75); - pointer-events: none; -} -#welcomeButton:hover, -#welcomeButton:focus { - text-shadow: 0px 0px 20px #fff; - outline: none; -} -#welcomeButton:active { - color: #c7c7c7; - text-shadow: 0px 0px 20px #c7c7c7; -} -#welcomeSVG { - -webkit-transform: translate3d(0, 0, 0); - overflow: visible; - transform: rotate(90deg); - margin-left: 20px; - transition: 0.25s ease; - width: 20px; - height: 20px; -} -#welcomeButton:hover #welcomeSVG, -#welcomeButton:focus #welcomeSVG { - -webkit-filter: drop-shadow(0px 0px 2px #fff); -} -#welcomeButton:active #welcomeSVG .arrowLine { - stroke: #c7c7c7; -} -#welcomeButton:active #welcomeSVG { - -webkit-filter: drop-shadow(0px 0px 2px #c7c7c7); -} -#welcomeButton:disabled #welcomeSVG .arrowLine { - stroke: rgba(255, 255, 255, 0.75); -} - -#welcomeButtonContent { - display: flex; - align-items: center; -} - -/******************************************************************************* - * * - * Login View (login.ejs) * - * * - ******************************************************************************/ - -/* Styles for dimmer login span. */ -.loginSpanDim { - font-size: 12px; - color: #848484; - font-weight: bold; -} - -/* Main login container. */ -#loginContainer { - position: relative; - display: flex; - justify-content: center; - align-items: center; - height: 100%; - width: 100%; - transition: filter 0.25s ease; - background: rgba(0, 0, 0, 0.50); -} - -/* Login cancel button styles. */ -#loginCancelContainer { - position: absolute; - top: 5%; - right: 5%; -} - -/* Login cancel button styles. */ -#loginCancelButton { - background: none; - border: none; - outline: none; - cursor: pointer; - transition: 0.25s ease; -} -#loginCancelButton:hover #loginCancelIcon, -#loginCancelButton:hover #loginCancelText, -#loginCancelButton:focus #loginCancelIcon, -#loginCancelButton:focus #loginCancelText { - text-shadow: 0px 0px 20px white; -} -#loginCancelButton:hover #loginCancelIcon, -#loginCancelButton:focus #loginCancelIcon { - box-shadow: 0px 0px 20px white; -} -#loginCancelButton:active #loginCancelIcon, -#loginCancelButton:active #loginCancelText { - text-shadow: 0px 0px 20px rgba(255, 255, 255, 0.75); - color: rgba(255, 255, 255, 0.75); - border-color: rgba(255, 255, 255, 0.75); -} -#loginCancelButton:active #loginCancelIcon { - box-shadow: 0px 0px 20px rgba(255, 255, 255, 0.75); -} -#loginCancelButton:disabled { - pointer-events: none; -} -#loginCancelButton:disabled #loginCancelIcon, -#loginCancelButton:disabled #loginCancelText { - color: rgba(255, 255, 255, 0.75); - border-color: rgba(255, 255, 255, 0.75); -} - -/* The X in a circle icon for the cancel button. */ -#loginCancelIcon { - border-radius: 50%; - border: 1px solid white; - box-sizing: border-box; - height: 30px; - width: 30px; - font-size: 19px; - line-height: 30px; - margin: 0 auto; - margin-bottom: 5px; - transition: 0.25s ease; -} -/* Text for the login cancel button. */ -#loginCancelText { - font-size: 15px; - transition: 0.25s ease; -} - -/* Login content wrapper. */ -#loginContent { - display: flex; - justify-content: center; - align-items: center; - height: 100%; - padding: 0px 25px; -} - -/* Login form. */ -#loginForm { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; -} - -/* Login form anchor styles. */ -#loginForm a { - font-size: 12px; - color: #848484; - font-weight: bold; - text-decoration: none; - transition: 0.25s ease; -} -#loginForm a:hover, -#loginForm a:focus { - color: #a2a2a2; - outline: none; -} -#loginForm a:active { - color: #8b8b8b; -} - -/* Logo on login form. */ -#loginImageSeal { - border-radius: 50%; - border: 2px solid #cad7e1; - background: rgba(1, 2, 1, 0.5); - height: 125px; - width: 125px; - box-shadow: 0px 0px 10px 0px rgb(0, 0, 0); - margin-bottom: 20px; -} - -/* Header on login view. */ -#loginSubheader { - font-family: 'Avenir Medium'; - margin-bottom: 25px; - font-size: 12px; - letter-spacing: 1px; - font-weight: bold; -} - -/* Container to organize login field elements. */ -.loginFieldContainer { - position: relative; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; -} - -/* SVG icons on the login view. */ -.loginSVG { - fill: #fff; - height: 20px; - width: 20px; -} - -/* Span which displays errors related to login field content. */ -.loginErrorSpan { - font-family: 'Avenir Medium'; - font-weight: bold; - font-size: 8px; - color: #ff1b0c; - width: 100%; - text-align: right; - position: absolute; - top: 7px; - opacity: 0; - transition: 0.25s ease; -} - -.shake { - animation: shake 0.82s cubic-bezier(.36,.07,.19,.97) both; -} - -@keyframes shake { - 10%, 90% { - transform: translate3d(-1px, 0, 0); - } - - 20%, 80% { - transform: translate3d(2px, 0, 0); - } - - 30%, 50%, 70% { - transform: translate3d(-4px, 0, 0); - } - - 40%, 60% { - transform: translate3d(4px, 0, 0); - } -} - -/* Login text input styles. */ -.loginField { - font-family: 'Avenir Book'; - background: none; - border-width: 1.5px 0px 0px 0px; - border-style: solid; - width: 250px; - margin-bottom: 20px; - border-color: #fff; - color: rgba(255, 255, 255, 0.75); - font-weight: bold; - text-align: center; - box-sizing: border-box; - padding: 7.5px; - font-size: 10px; - letter-spacing: 1px; -} -.loginField:focus { - outline: none; -} -.loginField:disabled { - color: rgba(255, 255, 255, 0.50); -} -.loginField::-webkit-input-placeholder { - color: rgba(255, 255, 255, 0.75); - font-size: 10px; - letter-spacing: 1px; - text-align: center; - font-weight: bold; -} -.loginField:focus::-webkit-input-placeholder { - color: transparent; -} - -/* Add spacing between password field and options bar. */ -#labelPassword { - margin-bottom: 13px; -} - -/* Container which contains the forgot and remember options. */ -#loginOptions { - display: flex; - justify-content: space-between; - width: 100%; -} - -/* Remember option text. */ -#loginRememberText { - padding-right: 10px; - transition: 0.25s ease; -} - -/* Login button styles. */ -#loginButton { - background: none; - font-weight: bold; - letter-spacing: 2px; - border: none; - padding: 15px 5px; - margin: 10px 0px; - cursor: pointer; - position: relative; - right: -20px; - transition: 0.5s ease; -} -#loginButton:disabled { - color: rgba(255, 255, 255, 0.75); - pointer-events: none; -} -#loginButton[loading] { - color: #fff; -} -#loginButton:hover, -#loginButton:focus { - text-shadow: 0px 0px 20px #fff; - outline: none; -} -#loginButton:active { - color: #c7c7c7; - text-shadow: 0px 0px 20px #c7c7c7; -} -#loginSVG { - -webkit-transform: translate3d(0, 0, 0); - overflow: visible; - transform: rotate(90deg); - margin-left: 20px; - transition: 0.25s ease; - width: 20px; - height: 20px; -} -#loginButton:hover #loginSVG, -#loginButton:focus #loginSVG { - -webkit-filter: drop-shadow(0px 0px 2px #fff); -} -#loginButton:active #loginSVG .arrowLine { - stroke: #c7c7c7; -} -#loginButton:active #loginSVG { - -webkit-filter: drop-shadow(0px 0px 2px #c7c7c7); -} -#loginButton:disabled #loginSVG .arrowLine { - stroke: rgba(255, 255, 255, 0.75); -} - -#loginButtonContent { - display: flex; - align-items: center; -} - -#loginButton .circle-loader, -#loginButton[loading] #loginSVG { - display: none; -} -#loginButton[loading] .circle-loader, -#loginButton #loginSVG { - display: initial; -} - - -.circle-loader { - margin-left: 20px; - border: 2px solid rgba(255, 255, 255, 0.5); - border-left-color: #ffffff; - animation-name: loader-spin; - animation-duration: 1s; - animation-iteration-count: infinite; - animation-timing-function: linear; - position: relative; - display: inline-block; - vertical-align: top; - border-radius: 50%; - width: 16px; - height: 16px; -} -.load-complete { - animation: none; - border-color: #ffffff; - transition: border 500ms ease-out; -} -.checkmark { - display: none; -} -.checkmark.draw:after { - animation-duration: 800ms; - animation-timing-function: ease; - animation-name: checkmark; - transform: scaleX(-1) rotate(135deg); -} -.checkmark:after { - opacity: 1; - height: 8px; - width: 4px; - transform-origin: left top; - border-right: 2px solid #ffffff; - border-top: 2px solid #ffffff; - content: ''; - left: 2px; - top: 8px; - position: absolute; -} -@keyframes loader-spin { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } -} -@keyframes checkmark { - 0% { - height: 0; - width: 0; - opacity: 1; - } - 20% { - height: 0; - width: 4px; - opacity: 1; - } - 40% { - height: 8px; - width: 4px; - opacity: 1; - } - 100% { - height: 8px; - width: 4px; - opacity: 1; - } -} - - - -/*.spinningCircle { - margin-left: 20px; - height: 16px; - width: 16px; - border-radius: 50%; - border: 2px solid rgba(255,255,255,0); - border-top-color: #ffffff; - border-right-color: #ffffff; - border-left-color: rgba(255, 255, 255, 0.50); - border-bottom-color: rgba(255, 255, 255, 0.50); - animation: single2 4s infinite linear; -} - -@keyframes single2 { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(720deg); - } -}*/ - -/* Disclaimer container. */ -#loginDisclaimer { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; -} - -/* Add spacing between register anchor and disclaimer. */ -#loginRegisterSpan { - margin-bottom: 5px; -} - -/* Disclaimer text styles. */ -.loginDisclaimerText { - font-size: 7px; - color: #848484; - font-weight: bold; - text-align: center; -} - -/* * * -* Login View | Custom Checkbox -* * */ - -/* Checkbox container. */ -#checkmarkContainer { - display: flex; - justify-content: flex-end; - align-items: center; - position: relative; - cursor: pointer; - font-size: 22px; - -webkit-user-select: none; -} - -/* Hide the default checkbox. */ -#checkmarkContainer input { - opacity: 0; - cursor: pointer; - position: absolute; -} - -/* Create a custom checkbox. */ -.loginCheckmark { - position: relative; - height: 10px; - width: 10px; - border: 1px solid #848484; - border-radius: 1px; - background: none; - transition: 0.25s ease; -} -/* On hover and focus, add a grey border color. */ -#checkmarkContainer:hover input ~ *, -#checkmarkContainer input:focus ~ * { - color: #a2a2a2; - border-color: #a2a2a2; -} -/* On keydown, darken the checkbox a bit. */ -#checkmarkContainer input:active ~ *:not(#loginRememberText) { - color: #8d8d8d; - border-color: #8d8d8d; -} -#checkmarkContainer[disabled] { - pointer-events: none; -} -/* For checked -> #checkmarkContainer input:checked ~ * */ -/* Create the checkmark/indicator (hidden when not checked). */ -.loginCheckmark:after { - content: ""; - display: none; -} -/* Show the checkmark when checked. */ -#checkmarkContainer input:checked ~ .loginCheckmark:after { - display: block; -} -/* Style the checkmark/indicator. */ -#checkmarkContainer .loginCheckmark:after { - position: absolute; - left: 3.5px; - top: 0.5px; - width: 2px; - height: 6px; - border: solid #a2a2a2; - border-width: 0 2px 2px 0; - transform: rotate(45deg); -} - -/* -#login_filter { - height: calc(100% - 22px); - width: 100%; - z-index: 9000; - position: absolute; - filter: blur(8px) contrast(0.9) brightness(1.0); - background: url('./../images/backgrounds/0.jpg') no-repeat center center fixed; - transform: scale(1.2); - background-size: cover; -} -*/ - -/******************************************************************************* - * * - * Settings View (sttings.ejs) * - * * - ******************************************************************************/ - -/* Main settings container. */ -#settingsContainer { - position: relative; - height: 100%; - display: flex; - background-color: rgba(0, 0, 0, 0.50); - transition: background-color 0.25s cubic-bezier(.02, .01, .47, 1); -} - -/* Drop shadow displayed when content is scrolled out of view. */ -#settingsContainer:before { - content: ''; - background: linear-gradient(rgba(0, 0, 0, 0.25), transparent); - width: 100%; - height: 5px; - position: absolute; - opacity: 0; - transition: opacity 0.25s ease; -} -#settingsContainer[scrolled]:before { - opacity: 1; -} - -/* Left hand side of the settings UI, for navigation. */ -#settingsContainerLeft { - padding-top: 4%; - height: 100%; - width: 25%; - box-sizing: border-box; -} - -/* Settings navigation container. */ -#settingsNavContainer { - height: 100%; - display: flex; - flex-direction: column; -} - -/* Navigation header styles. */ -#settingsNavHeader { - height: 15%; - display: flex; - justify-content: center; -} -#settingsNavHeaderText { - font-size: 20px; -} - -/* Navigation items outer container. */ -#settingsNavItemsContainer { - height: 85%; - display: flex; - justify-content: center; - box-sizing: border-box; -} - -/* Navigation items content container. */ -#settingsNavItemsContent { - height: 100%; - display: flex; - flex-direction: column; - position: relative; -} - -/* Navigation item shared styles. */ -.settingsNavItem { - background: none; - border: none; - text-align: left; - margin: 5px 0px; - padding: 0px 20px; - color: grey; - cursor: pointer; - outline: none; - transition: 0.25s ease; -} -.settingsNavItem:hover, -.settingsNavItem:focus { - color: #c1c1c1; - text-shadow: 0px 0px 20px #c1c1c1; -} -.settingsNavItem[selected] { - cursor: default; - color: white; - text-shadow: none; -} - -/* Div to add some space between nav items. */ -.settingsNavSpacer { - height: 25px; -} - -/* Content container for the done button. */ -#settingsNavContentBottom { - position: absolute; - top: 65%; -} - -/* Settings navigational divider. */ -.settingsNavDivider { - width: 75%; - height: 1px; - background: rgba(126, 126, 126, 0.57); - margin-left: auto; - margin-bottom: 25px; -} - -/* Settings done button styles. */ -#settingsNavDone { - background: none; - border: none; - text-align: left; - margin: 5px 0px; - padding: 0px 20px; - color: white; - cursor: pointer; - outline: none; - transition: 0.25s ease; -} -#settingsNavDone:hover, -#settingsNavDone:focus { - text-shadow: 0px 0px 20px white, 0px 0px 20px white, 0px 0px 20px white; -} -#settingsNavDone:active { - text-shadow: 0px 0px 20px rgba(255, 255, 255, 0.75), 0px 0px 20px rgba(255, 255, 255, 0.75), 0px 0px 20px rgba(255, 255, 255, 0.75); - color: rgba(255, 255, 255, 0.75); -} -#settingsNavDone:disabled { - color: rgba(255, 255, 255, 0.75); - pointer-events: none; -} - -/* Right hand side of the settings container, for tabs. */ -#settingsContainerRight { - height: 100%; - width: 75%; - box-sizing: border-box; -} - -/* Settings tab shared styles. */ -.settingsTab { - width: 100%; - height: 100%; - overflow-y: auto; -} -.settingsTab::-webkit-scrollbar { - width: 2px; -} -.settingsTab::-webkit-scrollbar-track { - display: none; -} -.settingsTab::-webkit-scrollbar-thumb { - border-radius: 10px; - box-shadow: inset 0 0 10px rgba(255, 255, 255, 0.50); -} - -/* Add spacing to the top of each settings tab. */ -.settingsTab > *:first-child { - margin-top: 5%; -} - -/* Add spacing to the bottom of each settings tab. */ -.settingsTab > *:last-child { - margin-bottom: 20%; -} - -/* Tab header shared styles. */ -.settingsTabHeader { - display: flex; - flex-direction: column; - margin-bottom: 20px; -} -.settingsTabHeaderText { - font-size: 20px; - font-family: 'Avenir Medium'; -} -.settingsTabHeaderDesc { - font-size: 12px; -} - -/* Remove spin button from number inputs. */ -#settingsContainer input[type=number]::-webkit-inner-spin-button { - -webkit-appearance: none; -} - -/* Default styles for text/number inputs. */ -#settingsContainer input[type=number], -#settingsContainer input[type=text] { - color: white; - background: rgba(0, 0, 0, 0.25); - border-radius: 3px; - border: 1px solid rgba(126, 126, 126, 0.57); - font-family: 'Avenir Book'; - transition: 0.25s ease; -} -#settingsContainer input[type=number]:focus, -#settingsContainer input[type=text]:focus { - outline: none; - border-color: rgba(126, 126, 126, 0.87); -} -#settingsContainer input[type=number][error] { - border-color: rgb(255, 27, 12); - background: rgba(236, 0, 0, 0.25); - color: rgb(255, 27, 12); -} - -/* Styles for a generic settings entry. */ -.settingsFieldContainer { - display: flex; - align-items: center; - justify-content: space-between; - padding: 20px 0px; - width: 75%; - border-bottom: 1px solid rgba(255, 255, 255, 0.50); -} -.settingsFieldLeft { - display: flex; - flex-direction: column; -} -.settingsFieldTitle { - font-size: 14px; - font-family: 'Avenir Medium'; - color: rgba(255, 255, 255, 0.95); -} -.settingsFieldDesc { - font-size: 12px; - color: rgba(255, 255, 255, .95); - margin-top: 5px; -} -.settingsDivider { - height: 1px; - width: 75%; - background: rgba(255, 255, 255, 0.25); -} - -/* Toggle Switch */ -.toggleSwitch { - position: relative; - display: inline-block; - width: 40px; - height: 20px; - border-radius: 50px; - box-sizing: border-box; -} -.toggleSwitch input { - display:none; -} -.toggleSwitchSlider { - position: absolute; - cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(255, 255, 255, 0.35); - transition: .4s; - border-radius: 50px; - border: 1px solid rgba(126, 126, 126, 0.57); -} -.toggleSwitchSlider:before { - position: absolute; - content: ""; - height: 13px; - width: 16px; - left: 3px; - bottom: 3px; - background-color: white; - box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.75); - border-radius: 50px; - transition: .4s; -} -input:checked + .toggleSwitchSlider { - background-color: rgb(31, 140, 11); - /* box-shadow: inset 2px 1px 20px black; */ - border: 1px solid rgb(31, 140, 11); -} -input:checked + .toggleSwitchSlider:before { - transform: translateX(15px); -} - -/* Range Slider styles. */ -.rangeSlider { - width: 35%; - height: 5px; - margin: 15px 0px; - background: grey; - border-radius: 3px; - position: relative; -} -.rangeSliderBar { - position: absolute; - background: #8be88b; - width: 50%; - height: 5px; - border-radius: 3px 0px 0px 3px; - transition: background 0.25s ease; -} -.rangeSliderTrack { - position: absolute; - top: -7.5px; - width: 7px; - height: 20px; - background: white; - border-radius: 3px; - left: 50%; - cursor: ew-resize; -} - -/* File selectors */ - -/* Main container for File selectors. */ -.settingsFileSelContainer { - display: flex; - flex-direction: column; - border-bottom: 1px solid rgba(255, 255, 255, 0.50); - margin-bottom: 20px; - margin-top: 20px; - width: 75%; -} - -/* File selector title. */ -.settingsFileSelTitle { - margin-bottom: 10px; -} - -/* Wrapper container for the actionable elements. */ -.settingsFileSelActions { - display: flex; - width: 90%; -} - -/* File selector icon settings. */ -.settingsFileSelIcon { - display: flex; - align-items: center; - background: rgba(126, 126, 126, 0.57); - border-radius: 3px 0px 0px 3px; - padding: 5px; - transition: 0.25s ease; -} -.settingsFileSelSVG { - width: 20px; - height: 20px; - fill: white; -} - -/* Disabled text field which stores the selected file path. */ -.settingsFileSelVal { - border-radius: 0px !important; - width: 100%; - padding: 5px 10px; - font-size: 12px; - height: 30px; -} - -/* File selection button. */ -.settingsFileSelButton { - border: 0px; - border-radius: 0px 3px 3px 0px; - font-size: 12px; - padding: 0px 5px; - cursor: pointer; - background: rgba(126, 126, 126, 0.57); - transition: 0.25s ease; - white-space: nowrap; - outline: none; -} -.settingsFileSelButton:hover, -.settingsFileSelButton:focus { - text-shadow: 0px 0px 20px white; -} -.settingsFileSelButton:active { - text-shadow: 0px 0px 20px rgba(255, 255, 255, 0.75); - color: rgba(255, 255, 255, 0.75); -} - -/* Description for the file selector. */ -.settingsFileSelDesc { - font-size: 10px; - margin: 20px 0px; - color: lightgrey; - width: 89%; -} -.settingsFileSelDesc strong { - font-family: 'Avenir Medium'; -} - -/* * * -* Settings View (Account Tab) -* * */ - -/* Add account button styles. */ -#settingsAddAccount { - background: rgba(0, 0, 0, 0.25); - border: 1px solid rgba(126, 126, 126, 0.57); - border-radius: 3px; - height: 50px; - width: 75%; - text-align: left; - padding: 0px 50px; - cursor: pointer; - outline: none; - transition: 0.25s ease; -} -#settingsAddAccount:hover, -#settingsAddAccount:focus { - background: rgba(54, 54, 54, 0.25); - text-shadow: 0px 0px 20px white; -} - -/* Settings auth accounts header. */ -#settingsCurrentAccountsHeader { - margin: 20px 0px; -} - -/* Auth account list container styles. */ -#settingsCurrentAccounts { - margin-bottom: 5%; -} -#settingsCurrentAccounts > .settingsAuthAccount:not(:last-child) { - margin-bottom: 10px; -} -#settingsCurrentAccounts > .settingsAuthAccount:not(:first-child) { - margin-top: 10px; -} - -/* Auth account shared styles. */ -.settingsAuthAccount { - display: flex; - width: 75%; - background: rgba(0, 0, 0, 0.25); - border-radius: 3px; - border: 1px solid rgba(126, 126, 126, 0.57); -} - -/* Left hand side of an auth account element, for the skin image. */ -.settingsAuthAccountLeft { - padding: 5px 5px 5px 20px; -} - -/* Image of the auth account's skin. */ -.settingsAuthAccountImage { - height: 115px; -} - -/* Right hand side of the auth account, for info + actions. */ -.settingsAuthAccountRight { - display: flex; - width: 100%; -} - -/* Account details container. */ -.settingsAuthAccountDetails { - display: flex; - flex-direction: column; - justify-content: center; - margin-left: 20px; - width: 100%; -} -.settingsAuthAccountDetails > *:not(:last-child) { - margin-bottom: 20px; -} - -/* Account detail element styles. */ -.settingsAuthAccountDetailPane { - display: flex; - flex-direction: column; -} -.settingsAuthAccountDetailTitle { - font-size: 12px; - color: grey; - font-weight: bold; - font-family: 'Avenir Medium'; -} -.settingsAuthAccountDetailValue { - font-size: 14px; - -webkit-user-select: initial; -} - -/* Account actions container. */ -.settingsAuthAccountActions { - display: flex; - flex-direction: column; - justify-content: space-between; - align-items: flex-end; - padding: 10px; -} - -/* Account select button shared styles. */ -.settingsAuthAccountSelect { - opacity: 0; - border: none; - white-space: nowrap; - background: none; - font-family: 'Avenir Medium'; - outline: none; - transition: 0.25s ease; -} -.settingsAuthAccountSelect:hover:not([selected]), -.settingsAuthAccountSelect:focus:not([selected]) { - text-shadow: 0px 0px 20px white, 0px 0px 20px white; - cursor: pointer; -} -.settingsAuthAccount:hover .settingsAuthAccountSelect:not([selected]), -.settingsAuthAccountSelect[selected] { - opacity: 1; -} -.settingsAuthAccountSelect[selected] { - pointer-events: none; -} - -/* Account logout button shared styles. */ -.settingsAuthAccountLogOut { - opacity: 0; - border: 1px solid rgb(241, 55, 55); - color: rgb(241, 55, 55); - background: none; - font-size: 12px; - border-radius: 3px; - font-family: 'Avenir Medium'; - transition: 0.25s ease; - cursor: pointer; - outline: none; -} -.settingsAuthAccountLogOut:hover, -.settingsAuthAccountLogOut:focus { - box-shadow: 0px 0px 20px rgb(241, 55, 55); - background: rgba(241, 55, 55, 0.25); -} -.settingsAuthAccountLogOut:active { - box-shadow: 0px 0px 20px rgb(185, 47, 47); - background: rgba(185, 47, 47, 0.25); - border: 1px solid rgb(185, 47, 47); - color: rgb(185, 47, 47); -} -.settingsAuthAccount:hover .settingsAuthAccountLogOut { - opacity: 1; -} - -/* * * -* Settings View (Minecraft Tab) -* * */ - -/* Game resolution UI elements. */ -#settingsGameResolutionContainer { - display: flex; - flex-direction: column; - padding-bottom: 20px; - border-bottom: 1px solid rgba(255, 255, 255, 0.50); - width: 75%; -} -#settingsGameResolutionContent { - display: flex; - align-items: center; - padding-top: 10px; -} -#settingsGameResolutionCross { - color: grey; - padding: 0px 15px; -} -#settingsGameWidth, -#settingsGameHeight { - padding: 7.5px 5px; - width: 75px; -} - -/* * * -* Settings View (Mods Tab) -* * */ - -/* Selected server content container */ -#settingsSelServContainer { - background: rgba(0, 0, 0, 0.25); - width: 75%; - border-radius: 3px; - display: flex; - justify-content: space-between; - margin: 15px 0px; -} - -/* Div which will be populated with the selected server's information. */ -#settingsSelServContent { - display: flex; - align-items: center; - justify-content: flex-start; - padding: 5px 0px; -} - -/* Wrapper container for the switch server button. */ -#settingsSwitchServerContainer { - display: flex; - align-items: center; - padding: 15px; -} - -/* Button to switch server configurations on the mods tab. */ -#settingsSwitchServerButton { - opacity: 0; - border: 1px solid rgb(255, 255, 255); - color: rgb(255, 255, 255); - background: none; - font-size: 12px; - border-radius: 3px; - font-family: 'Avenir Medium'; - transition: 0.25s ease; - cursor: pointer; - outline: none; -} -#settingsSwitchServerButton:hover, -#settingsSwitchServerButton:focus { - box-shadow: 0px 0px 20px rgb(255, 255, 255); - background: rgba(255, 255, 255, 0.25); -} -#settingsSwitchServerButton:active { - box-shadow: 0px 0px 20px rgb(187, 187, 187); - background: rgba(187, 187, 187, 0.25); - border: 1px solid rgb(187, 187, 187); - color: rgb(187, 187, 187); -} -#settingsSelServContainer:hover #settingsSwitchServerButton { - opacity: 1; -} - -/* Main content container for the mod elements. */ -#settingsModsContainer { - width: 75%; -} - -/* Mod sub-container header text. */ -.settingsModsHeader { - padding-bottom: 10px; - border-bottom: 1px solid rgba(255, 255, 255, 0.5); - margin-bottom: 10px; -} - -/* Mod elements sub-containers. */ -#settingsReqModsContainer, -#settingsOptModsContainer, -#settingsDropinModsContainer { - padding-bottom: 25px; -} - -/* Main content containers for mod elements. */ -#settingsReqModsContent, -#settingsOptModsContent, -#settingsDropinModsContent { - font-size: 12px; - background: rgba(0, 0, 0, 0.25); - border-radius: 3px; - color: white; -} - -/* Mod elements. */ -.settingsMod, -.settingsDropinMod { - padding: 10px; -} -.settingsSubMod { - padding: 10px 0px 10px 15px; - margin-left: 20px; - border-left: 1px solid rgba(255, 255, 255, 0.5); -} - -/* Main content container for mod element information. */ -.settingsModContent { - display: flex; - align-items: center; - justify-content: space-between; - transition: opacity 0.25s ease; -} - -/* Wrapper container for the left side of a mod element. */ -.settingsModMainWrapper { - display: flex; - align-items: center; -} - -/* Mod enabled/disabled status. */ -.settingsModStatus { - width: 7px; - height: 7px; - border-radius: 50%; - background-color: #c32625; - margin-right: 15px; - transition: 0.25s ease; -} - -/* Mod details container. */ -.settingsModDetails { - display: flex; - flex-direction: column; -} - -/* The version of the mod. */ -.settingsModVersion { - color: grey; - font-size: 10px; -} - -/* Disabled toggleswitch for required mods. */ -.toggleSwitch[reqmod] { - filter: grayscale(49%) brightness(60%); - pointer-events: none; -} - -/* Set the status color of an enabled mod. */ -.settingsBaseMod[enabled] > .settingsModContent > .settingsModMainWrapper > .settingsModStatus { - background-color: rgb(165, 195, 37); -} - -/* Add opacity to submods of a disabled mod. */ -.settingsBaseMod:not([enabled]) > .settingsSubModContainer .settingsModContent { - opacity: 0.5; -} - -/* Curve the left border for submods. */ -.settingsSubModContainer > .settingsSubMod:first-child { - border-top-left-radius: 10px; -} -.settingsSubModContainer > .settingsSubMod:last-child { - border-bottom-left-radius: 10px; -} -.settingsSubModContainer > .settingsSubMod:only-child { - border-top-left-radius: 10px; - border-bottom-left-radius: 10px; -} - -/* Wrapper container for all submods. */ -.settingsSubModContainer { - margin-top: 10px; -} - -/* Button to open the mods folder for drop-in mods. */ -#settingsDropinFileSystemButton { - background: rgba(0, 0, 0, 0.25); - border: 1px solid rgba(126, 126, 126, 0.57); - border-radius: 3px; - height: 50px; - width: 100%; - text-align: left; - padding: 0px 50px; - cursor: pointer; - outline: none; - transition: 0.25s ease; - margin-bottom: 10px; -} -#settingsDropinFileSystemButton:hover, -#settingsDropinFileSystemButton:focus, -#settingsDropinFileSystemButton[drag] { - background: rgba(54, 54, 54, 0.25); - text-shadow: 0px 0px 20px white; -} -/* Refresh instructions on the file system button. */ -#settingsDropinRefreshNote { - font-size: 10px; - pointer-events: none; -} - -/* Button to remove drop-in mods. */ -.settingsDropinRemoveButton { - background: none; - border: none; - font-size: 10px; - text-align: left; - padding: 0px; - color: #c32625; - font-weight: bold; - cursor: pointer; - outline: none; - transition: 0.25s ease; -} -.settingsDropinRemoveButton:hover, -.settingsDropinRemoveButton:focus { - text-shadow: 0px 0px 20px #c32625, 0px 0px 20px #c32625, 0px 0px 20px #c32625; -} -.settingsDropinRemoveButton:active { - color: #9b1f1f; - text-shadow: 0px 0px 20px #9b1f1f, 0px 0px 20px #9b1f1f, 0px 0px 20px #9b1f1f; -} - -/* Shaderpack settings description. */ -#settingsShaderpackDesc { - font-size: 10px; - margin: 10px 0px; - color: lightgrey; - font-weight: bold; - width: 89%; -} - -/* Wrapper container. */ -#settingsShaderpackWrapper { - display: flex; -} - -/* Button to add shaderpacks. */ -#settingsShaderpackButton { - background: rgba(0, 0, 0, 0.25); - border: 1px solid rgba(126, 126, 126, 0.57); - border-radius: 3px; - cursor: pointer; - outline: none; - transition: 0.25s ease; - font-size: 14px; - padding: 6px 11px; - margin-right: 5px; -} -#settingsShaderpackButton:hover, -#settingsShaderpackButton:focus, -#settingsShaderpackButton[drag] { - background: rgba(54, 54, 54, 0.25); - text-shadow: 0px 0px 20px white; -} - -/* Main select container. */ -.settingsSelectContainer { - position: relative; - width: 50%; -} - -/* Div which displays the selected option. */ -.settingsSelectSelected { - border-radius: 3px; - border-width: 1px; - font-size: 14px; - padding: 6px 16px; -} - -/* Style the arrow inside the select element. */ -.settingsSelectSelected:after { - position: absolute; - content: ""; - top: calc(50% - 3px); - right: 10px; - width: 0; - height: 0; - border: 6px solid transparent; - border-color: rgba(126, 126, 126, 0.57) transparent transparent transparent; -} - -/* Point the arrow upwards when the select box is open (active). */ -.settingsSelectSelected.select-arrow-active:after { - border-color: transparent transparent rgba(126, 126, 126, 0.57) transparent; - top: 7px; -} -.settingsSelectSelected.select-arrow-active { - border-radius: 3px 3px 0px 0px; -} - -/* Options content container. */ -.settingsSelectOptions { - position: absolute; - top: 100%; - left: 0; - right: 0; - z-index: 99; - max-height: 300%; - overflow-y: scroll; - border: 1px solid rgba(126, 126, 126, 0.57); - border-top: none; - border-radius: 0px 0px 3px 3px; -} -/* Hide the items when the select box is closed. */ -.settingsSelectOptions[hidden] { - display: none; -} -.settingsSelectOptions::-webkit-scrollbar { - width: 2px; -} -.settingsSelectOptions::-webkit-scrollbar-track { - display: none; -} -.settingsSelectOptions::-webkit-scrollbar-thumb { - border-radius: 10px; - box-shadow: inset 0 0 10px rgba(255, 255, 255, 0.50); -} - -/* Shared styles between options and selection div. */ -.settingsSelectOptions div, -.settingsSelectSelected { - background: rgba(0, 0, 0, 0.25); - border-style: solid; - border-color: rgba(126, 126, 126, 0.57); - color: #ffffff; - cursor: pointer; -} -.settingsSelectOptions div { - border-width: 0px 0px 1px 0px; - font-size: 12px; - padding: 4px 16px; -} -.settingsSelectOptions div:last-child { - border-bottom: none; -} - -/* Hover + selected styles. */ -.settingsSelectOptions div:hover, .settingsSelectOptions div[selected] { - background-color: rgba(255, 255, 255, 0.25) !important; -} - -/* * * -* Settings View (Java Tab) -* * */ - -/* Style links on the Java tab. */ -#settingsTabJava a, -.settingsChangelogText a { - color: rgba(202, 202, 202, 0.75); - transition: 0.25s ease; - outline: none; -} -#settingsTabJava a:hover, -#settingsTabJava a:focus, -.settingsChangelogText a:hover, -.settingsChangelogText a:focus { - color: rgba(255, 255, 255, 0.75); -} -#settingsTabJava a:active, -.settingsChangelogText a:active { - color: rgba(165, 165, 165, 0.75); -} - -/* Main container for memory management. */ -#settingsMemoryContainer { - width: 75%; - display: flex; - flex-direction: column; - border-bottom: 1px solid rgba(255, 255, 255, 0.50); - margin-bottom: 20px; -} - -/* Memory management title. */ -#settingsMemoryTitle { - margin-bottom: 10px; - padding-bottom: 5px; - border-bottom: 1px solid rgba(255, 255, 255, 0.5); -} - -/* Memory management content. */ -#settingsMemoryContent { - display: flex; - justify-content: space-between; - width: 100%; -} -#settingsMemoryContentLeft { - width: 69%; -} -#settingsMemoryContentRight { - display: flex; - align-items: center; - margin-right: 10%; -} - -/* Header for memory sliders. */ -.settingsMemoryHeader { - font-size: 14px; -} - -/* Wrapper container for a memory slider and label. */ -.settingsMemoryActionContainer { - display: flex; - align-items: center; - justify-content: space-between; -} - -/* Label which displays a memory slider's value. */ -.settingsMemoryLabel { - font-size: 14px; - margin-right: 2%; -} - -/* Range sliders for min and max memory settings. */ -#settingsMaxRAMRange, -#settingsMinRAMRange { - width: 85%; -} - -/* Memory status elements. */ -#settingsMemoryStatus { - display: flex; - flex-direction: column; -} -#settingsMemoryStatus > .settingsMemoryStatusContainer:not(:last-child){ - margin-bottom: 50%; -} -.settingsMemoryStatusContainer { - display: flex; - flex-direction: column; - align-items: center; -} -.settingsMemoryStatusTitle { - font-size: 12px; - color: grey; - font-weight: bold; -} -.settingsMemoryStatusValue { - color: lightgrey; - font-size: 16px; -} - -/* Description for memory management. */ -#settingsMemoryDesc { - font-size: 10px; - margin: 20px 0px; - color: lightgrey; - font-weight: bold; -} - -/* Status text which displays details on the selected executable. */ -#settingsJavaExecDetails { - font-weight: bold; - color: grey; - font-size: 12px; -} - -/* Main container for the JVM options setting. */ -#settingsJVMOptsContainer { - width: 75%; -} - -/* JVM options title. */ -#settingsJVMOptsTitle { - margin-bottom: 10px; -} - -/* Wrapper container for the actionable elements. */ -#settingsJVMOptsContent { - display: flex; - width: 90%; -} - -/* Text field to input the JVM options. */ -#settingsJVMOptsVal { - border-radius: 0px 3px 3px 0px !important; - width: 100%; - padding: 5px 10px; - font-size: 12px; -} -#settingsJVMOptsContent:focus-within > .settingsJavaIcon { - background: rgba(126, 126, 126, 0.87); -} - -/* Description for the JVM options setting. */ -#settingsJVMOptsDesc { - font-size: 10px; - margin: 20px 0px; - color: lightgrey; - font-weight: bold; - width: 89%; -} - -/* * * -* Settings View (Launcher Tab) -* * */ - -/* Tailored style for the data directory header. */ -#settingsDataDirTitle { - margin-bottom: 10px; -} - -/* * * -* Settings View (About Tab) -* * */ - -/* Main about content container. */ -#settingsAboutCurrentContainer { - display: flex; - flex-direction: column; - background: rgba(0, 0, 0, 0.25); - border: 1px solid rgba(126, 126, 126, 0.57); - border-radius: 3px; - width: 75%; - margin-bottom: 20px; -} - -/* About content. */ -#settingsAboutCurrentContent { - display: flex; - flex-direction: column; - padding: 15px; -} - -/* About header elements. */ -#settingsAboutCurrentHeadline { - display: flex; - align-items: center; - padding-bottom: 5px; - border-bottom: 1px solid rgba(126, 126, 126, 0.57); -} -#settingsAboutLogo { - width: 30px; - height: 30px; - padding: 5px; -} -#settingsAboutTitle { - font-size: 23px; - padding-left: 10px; -} - -/* Current version container. */ -#settingsAboutCurrentVersion { - display: flex; - align-items: center; - padding-top: 10px; -} - -/* Checkmark next to the version information. */ -#settingsAboutCurrentVersionCheck { - border-radius: 50%; - background: #23aa23; - text-align: center; - font-weight: bold; - margin: 11px 12px; - color: white; - height: 15px; - width: 15px; - font-size: 12px; - line-height: 17px; -} - -/* Current version details container. */ -#settingsAboutCurrentVersionDetails { - margin-left: 10px; -} - -/* Release type text. */ -#settingsAboutCurrentVersionTitle { - font-size: 12px; - font-family: 'Avenir Medium'; - color: #23aa23; - font-weight: bold; -} - -/* Current version text. */ -#settingsAboutCurrentVersionLine { - font-size: 10px; - color: grey; - font-weight: bold; -} - -/* About information links. */ -#settingsAboutButtons { - display: flex; - padding: 0px 15px; - margin-bottom: 5px; -} -.settingsAboutButton { - background: none; - border: none; - font-size: 10px; - color: grey; - padding: 0px 5px; - transition: 0.25s ease; - outline: none; - text-decoration: none; -} -.settingsAboutButton:hover, -.settingsAboutButton:focus { - color: rgb(165, 165, 165); -} -.settingsAboutButton:active { - color: rgba(124, 124, 124, 0.75); -} - -/* Main changelog container. */ -.settingsChangelogContainer { - display: flex; - flex-direction: column; - background: rgba(0, 0, 0, 0.25); - border: 1px solid rgba(126, 126, 126, 0.57); - border-radius: 3px; - width: 75%; - margin-bottom: 20px; -} - -/* Changelog content container. */ -.settingsChangelogContent { - display: flex; - flex-direction: column; - padding: 15px; -} - -/* Changelog header container. */ -.settingsChangelogHeadline { - padding-bottom: 10px; - margin-bottom: 10px; - border-bottom: 1px solid rgba(126, 126, 126, 0.57); -} -/* Changelog header label. */ -.settingsChangelogLabel { - font-size: 12px; - color: grey; - font-weight: bold; -} - -/* Changelog text content container. */ -.settingsChangelogText { - font-size: 12px; -} - -/* Styles for the changelog elements. */ -.settingsChangelogText p { - margin-bottom: 16px; - line-height: 1.5; -} -.settingsChangelogText blockquote { - border-left: 0.25em solid rgba(126, 126, 126, 0.95); - margin: 0px; - padding: 0 0 0 1em; - color: rgba(255, 255, 255, 0.85); -} -.settingsChangelogText code { - padding: 0.1em 0.4em; - font-size: 85%; - background-color: rgba(255, 255, 255, 0.25); - color: white; - border-radius: 3px; - font-family: 'Avenir Book'; -} -.settingsChangelogText li+li { - margin-top: .25em; -} -.settingsChangelogText a.commit-link { - font-weight: 400; - color: #ffffff; - text-decoration: none; -} -.settingsChangelogText a.commit-link:hover { - text-decoration: underline !important; - text-decoration-color: black; -} -.settingsChangelogText tt { - padding: 0.1em 0.4em; - font-size: 86%; - background-color: white; - border-radius: 3px; - color: black; - font-weight: bold; -} -.settingsChangelogText a.commit-link:hover tt { - text-decoration: underline; - text-decoration-color: black; -} -.settingsChangelogText .highlight { - background: rgba(0, 0, 0, 0.30); - user-select: initial; - padding: 5px 10px; -} -.settingsChangelogText .highlight pre { - margin: 0px; -} - -/* Container for the changelog button. */ -.settingsChangelogActions { - padding: 0px 15px 5px 15px; -} - -/* Open changelog on GitHub. */ -.settingsChangelogButton { - padding: 0px; -} - -/* * * -* Settings View (Updates Tab) -* * */ - -/* Main about content container. */ -#settingsUpdateStatusContainer { - display: flex; - flex-direction: column; - background: rgba(0, 0, 0, 0.25); - border: 1px solid rgba(126, 126, 126, 0.57); - border-radius: 3px; - width: 75%; - margin-bottom: 20px; -} - -/* Update content. */ -#settingsUpdateStatusContent { - display: flex; - flex-direction: column; - padding: 15px; -} - -/* Update header elements. */ -#settingsUpdateStatusHeadline { - display: flex; - align-items: center; - padding-bottom: 5px; - border-bottom: 1px solid rgba(126, 126, 126, 0.57); -} -#settingsUpdateTitle { - font-size: 16px; - padding-left: 10px; - font-weight: bold; -} - -/* Update version container. */ -#settingsUpdateVersion { - display: flex; - align-items: center; - padding: 10px 0px; - border-bottom: 1px solid rgba(126, 126, 126, 0.57); -} - -/* Checkmark next to the version information. */ -#settingsUpdateVersionCheck { - border-radius: 50%; - background: #23aa23; - text-align: center; - font-weight: bold; - margin: 11px 12px; - color: white; - height: 15px; - width: 15px; - font-size: 12px; - line-height: 17px; -} - -/* Update version details container. */ -#settingsUpdateVersionDetails { - margin-left: 10px; -} - -/* Release type text. */ -#settingsUpdateVersionTitle { - font-size: 12px; - font-family: 'Avenir Medium'; - color: #23aa23; - font-weight: bold; -} - -/* Current version text. */ -#settingsUpdateVersionLine { - font-size: 10px; - color: grey; - font-weight: bold; -} - -/* Update action container. */ -#settingsUpdateActionContainer { - padding-top: 10px; - font-size: 14px; - font-weight: bold; -} - -/* Update action button styles. */ -#settingsUpdateActionButton { - display: flex; - flex-direction: column; - padding-left: 10px; - background: none; - border: none; - font-size: 14px; - font-weight: bold; - cursor: pointer; - outline: none; - text-align: left; - transition: 0.25s ease; -} -#settingsUpdateActionButton:hover, -#settingsUpdateActionButton:focus { - text-shadow: 0px 0px 20px white, 0px 0px 20px white, 0px 0px 20px white; -} -#settingsUpdateActionButton:active { - text-shadow: 0px 0px 20px #c7c7c7, 0px 0px 20px #c7c7c7, 0px 0px 20px #c7c7c7; - color: #c7c7c7; -} -#settingsUpdateActionButton:disabled { - pointer-events: none; -} - -/******************************************************************************* - * * - * Landing View (Structural Styles) * - * * - ******************************************************************************/ - -/* Main content container. */ -#landingContainer { - height: 100%; - position: relative; - transition: background 2s ease; - overflow-y: hidden; -} - -/* Upper content container. */ -#landingContainer > #upper { - position: relative; - transition: top 2s ease; - top: 0px; - height: 77%; - display: flex; -} -#landingContainer > #upper > #left { - display: inline-flex; - width: 15%; - height: 100%; - justify-content: flex-end; -} -#landingContainer > #upper > #content { - display: inline-flex; - width: 70%; - height: 100%; -} -#landingContainer > #upper > #right { - display: inline-flex; - width: 15%; - height: 100%; -} - -/* Lower content container. */ -#landingContainer > #lower { - height: 23%; - display: flex; - background: linear-gradient(to top, rgba(0, 0, 0, 0.75), rgba(0, 0, 0, 0)); -} -#landingContainer > #lower > #left { - position: relative; - transition: top 2s ease; - top: 0px; - height: 100%; - width: 33%; - display: inline-flex; - justify-content: center; -} -#landingContainer > #lower > #left #content { - position: relative; - top: 25px; - display: inline-flex; - line-height: 24px; - left: 50px; -} -#landingContainer > #lower > #center { - position: relative; - transition: top 2s ease; - top: 0px; - height: 100%; - width: 34%; - display: inline-flex; - justify-content: center; -} -#landingContainer > #lower > #center #content { - position: relative; - z-index: 500; - transition: top 2s ease; - top: 10px; -} -#landingContainer > #lower > #right { - position: relative; - transition: top 2s ease; - top: 0px; - height: 100%; - width: 33%; - display: inline-flex; -} - -/******************************************************************************* - * * - * Landing View (News Styles) * - * * - ******************************************************************************/ - -/* Main container. */ -#newsContainer { - position: absolute; - top: 100%; - height: 100%; - width: 100%; - transition: top 2s ease; - display: flex; - align-items: flex-end; - justify-content: center; -} - -/* News content container. */ -#newsContent { - height: 82vh; - width: 100%; - display: flex; - -webkit-user-select: initial; - position: relative; -} - -/* Drop shadow displayed when content is scrolled out of view. */ -#newsContent:before { - content: ''; - background: linear-gradient(rgba(0, 0, 0, 0.25), transparent); - width: 100%; - height: 5px; - position: absolute; - opacity: 0; - transition: opacity 0.25s ease; -} -#newsContent[scrolled]:before { - opacity: 1; -} - -/* News article status container (left). */ -#newsStatusContainer { - width: calc(30% - 60px); - height: calc(100% - 30px); - padding: 15px 15px 15px 45px; - display: flex; - flex-direction: column; - justify-content: space-between; - position: relative; -} - -/* News status content. */ -#newsStatusContent { - display: flex; - flex-direction: column; - align-items: flex-end; -} - -/* News title wrapper. */ -#newsTitleContainer { - display: flex; - max-width: 90%; -} - -/* News article title styles. */ -#newsArticleTitle { - font-size: 18px; - font-weight: bold; - font-family: 'Avenir Medium'; - color: white; - text-decoration: none; - transition: 0.25s ease; - outline: none; - text-align: right; -} -#newsArticleTitle:hover, -#newsArticleTitle:focus { - text-shadow: 0px 0px 20px white; -} -#newsArticleTitle:active { - color: #c7c7c7; - text-shadow: 0px 0px 20px #c7c7c7; -} - -/* News meta container. */ -#newsMetaContainer { - display: flex; - flex-direction: column; -} - -/* Date and author wrappers. */ -#newsArticleDateWrapper, -#newsArticleAuthorWrapper { - display: flex; - justify-content: flex-end; -} - -/* Date and author shared styles. */ -#newsArticleDate, -#newsArticleAuthor { - display: inline-block; - font-size: 10px; - padding: 0px 5px; - font-weight: bold; - border-radius: 2px; -} - -/* Date styles. */ -#newsArticleDate { - background: white; - color: black; - margin-top: 5px; -} - -/* Author styles. */ -#newsArticleAuthor { - background: #a02d2a; -} - -/* News article comments styles. */ -#newsArticleComments { - margin-top: 5px; - display: inline-block; - font-size: 10px; - color: #ffffff; - text-decoration: none; - transition: 0.25s ease; - outline: none; - text-align: right; -} -#newsArticleComments:focus, -#newsArticleComments:hover { - color: #e0e0e0; -} -#newsArticleComments:active { - color: #c7c7c7; -} - -/* Article content container (right). */ -#newsArticleContainer { - width: calc(100% - 25px); - height: 100%; - margin: 0px 0px 0px 25px; -} - -/* Article content styles. */ -#newsArticleContentScrollable { - font-size: 12px; - overflow-y: scroll; - height: 100%; - padding: 0px 15px 0px 15px; -} -#newsArticleContentScrollable img, -#newsArticleContentScrollable iframe { - max-width: 95%; - display: block; - margin: 0 auto; -} -#newsArticleContentScrollable a { - color: rgba(202, 202, 202, 0.75); - transition: 0.25s ease; - outline: none; -} -#newsArticleContentScrollable a:hover, -#newsArticleContentScrollable a:focus { - color: rgba(255, 255, 255, 0.75); -} -#newsArticleContentScrollable a:active { - color: rgba(165, 165, 165, 0.75); -} -#newsArticleContentScrollable::-webkit-scrollbar { - width: 2px; -} -#newsArticleContentScrollable::-webkit-scrollbar-track { - display: none; -} -#newsArticleContentScrollable::-webkit-scrollbar-thumb { - border-radius: 10px; - box-shadow: inset 0 0 10px rgba(255, 255, 255, 0.50); -} -.bbCodeSpoilerButton { - background: none; - border: none; - outline: none; - cursor: pointer; - font-size: 16px; - transition: 0.25s ease; - width: 100%; - border-bottom: 1px solid white; - padding-bottom: 15px; -} -.bbCodeSpoilerButton:hover, -.bbCodeSpoilerButton:focus { - text-shadow: 0px 0px 20px #ffffff, 0px 0px 20px #ffffff, 0px 0px 20px #ffffff; -} -.bbCodeSpoilerButton:active { - color: #c7c7c7; - text-shadow: 0px 0px 20px #c7c7c7, 0px 0px 20px #c7c7c7, 0px 0px 20px #c7c7c7; -} -.bbCodeSpoilerText { - display: none; - padding: 15px 0px; - border-bottom: 1px solid white; -} - - -#newsArticleContentWrapper { - width: 80%; -} - -.newsArticleSpacerTop { - height: 15px; -} - -/* Div to add spacing at the end of a news article. */ -.newsArticleSpacerBot { - height: 30px; -} - -/* News navigation container. */ -#newsNavigationContainer { - display: flex; - justify-content: center; - align-items: center; - margin-bottom: 10px; - -webkit-user-select: none; - position: absolute; - bottom: 15px; - right: 0px; -} - -/* Navigation status span. */ -#newsNavigationStatus { - font-size: 12px; - margin: 0px 15px; -} - -/* Left and right navigation button styles. */ -#newsNavigateLeft, -#newsNavigateRight { - background: none; - border: none; - outline: none; - height: 20px; - cursor: pointer; -} -#newsNavigateLeft:hover #newsNavigationLeftSVG, -#newsNavigateLeft:focus #newsNavigationLeftSVG, -#newsNavigateRight:hover #newsNavigationRightSVG, -#newsNavigateRight:focus #newsNavigationRightSVG { - -webkit-filter: drop-shadow(0px 0px 2px #fff); -} -#newsNavigateLeft:active #newsNavigationLeftSVG .arrowLine, -#newsNavigateRight:active #newsNavigationRightSVG .arrowLine { - stroke: #c7c7c7; -} -#newsNavigateLeft:active #newsNavigationLeftSVG, -#newsNavigateRight:active #newsNavigationRightSVG { - -webkit-filter: drop-shadow(0px 0px 2px #c7c7c7); -} -#newsNavigateLeft:disabled #newsNavigationLeftSVG .arrowLine, -#newsNavigateRight:disabled #newsNavigationRightSVG .arrowLine { - stroke: rgba(255, 255, 255, 0.75); -} -#newsNavigationLeftSVG { - transform: rotate(-90deg); - width: 15px; -} -#newsNavigationRightSVG { - transform: rotate(90deg); - width: 15px; -} - -/* News error (message) container. */ -#newsErrorContainer { - height: 100%; - display: flex; - align-items: center; - flex-direction: column; - justify-content: center; -} -#newsErrorFailed { - display: flex; - align-items: center; - flex-direction: column; - justify-content: center; -} - -/* News error content (message). */ -.newsErrorContent { - font-size: 20px; -} -#newsErrorLoading { - display: flex; - width: 168.92px; -} -#nELoadSpan { - white-space: pre; -} -/* News error retry button styles. */ -#newsErrorRetry { - font-size: 12px; - font-weight: bold; - cursor: pointer; - background: none; - border: none; - outline: none; - transition: 0.25s ease; -} -#newsErrorRetry:focus, -#newsErrorRetry:hover { - text-shadow: 0px 0px 20px white; -} -#newsErrorRetry:active { - color: #c7c7c7; - text-shadow: 0px 0px 20px #c7c7c7; -} - -/******************************************************************************* - * * - * Landing View (Top Styles) * - * * - ******************************************************************************/ - -/* * * -* Landing View (Top Styles) | Left Content -* * */ - -/* Logo image. */ -#image_seal { - height: 70px; - width: auto; - position: relative; - border: 2px solid white; - box-sizing: border-box; - border-radius: 50%; -} - -/* Logo container styles. */ -#image_seal_container { - position: relative; - height: 70px; - width: 70px; - border-radius: 50%; - margin-top: 50px; -} - -/* Logo container styles w/ update. */ -#image_seal_container[update]{ - cursor: pointer -} -#image_seal_container[update]:before, -#image_seal_container[update]:after { - cursor: pointer; - position: absolute; - content: ''; - height: 100%; - width: 100%; - top: 0%; - left: 0%; - border-radius: 50%; - box-shadow: 0 0 15px #43c628; - animation: glow-grow 4s ease-out infinite; - background: rgba(0, 0, 0, 0.15); -} -#image_seal_container[update]:before { - animation-delay: 2s; -} - -/* Update available tooltip styles. */ -#updateAvailableTooltip { - cursor: pointer; - visibility: hidden; - opacity: 0; - width: 100px; - height: 15px; - background-color: rgb(0, 0, 0); - color: #fff; - text-align: center; - border-radius: 4px; - padding: 2px; - position: absolute; - z-index: 1; - top: 115%; - left: -17.5px; - font-family: 'Avenir Medium'; - font-size: 12px; - transition: visibility 0s linear 0.25s, opacity 0.25s ease; -} -#updateAvailableTooltip::after { - content: " "; - position: absolute; - left: 50%; - bottom: 100%; - margin-left: -5px; - border-width: 5px; - border-style: solid; - border-color: transparent transparent rgb(0, 0, 0) transparent; -} -#image_seal_container[update]:hover #updateAvailableTooltip { - visibility: visible; - opacity: 1; - transition-delay: 0s; -} - -/* Update available animation. */ -@keyframes glow-grow { - 0% { - opacity: 0; - transform: scale(1); - } - 80% { - opacity: 1; - } - 100% { - transform: scale(1.5); - opacity: 0; - } -} - -/* * * -* Landing View (Bottom Styles) | Right Content -* * */ - -/* Wrapper container for top, right content. */ -#rightContainer { - display: flex; - flex-direction: column; - position: relative; - top: 50px; - align-items: flex-start; - height: calc(100% - 50px); -} - -/* Right hand user content container. */ -#user_content { - display: flex; - align-items: center; - justify-content: center; - box-sizing: border-box; - position: relative; -} - -/* User profile avatar container. */ -#avatarContainer { - border-radius: 50%; - border: 2px solid #cad7e1; - box-sizing: border-box; - background: rgba(1, 2, 1, 0.5); - height: 70px; - width: 70px; - box-shadow: 0px 0px 10px 0px rgb(0, 0, 0); - overflow: hidden; - position: relative; - background-position: center; - background-repeat: no-repeat; - background-size: contain; -} - -/* Avatar edit overlay. */ -#avatarOverlay { - opacity: 0; - position: absolute; - z-index: 1; - display: flex; - justify-content: center; - align-items: center; - transition: 0.25s ease; - font-weight: bold; - letter-spacing: 2px; - background-color: rgba(0, 0, 0, 0.35); - -webkit-user-select: none; - border: none; - cursor: pointer; - width: 100%; - height: 100%; - border-radius: 50%; -} -#avatarOverlay:hover, -#avatarOverlay:focus { - opacity: 1; -} -#avatarOverlay:active { - background-color: rgba(0, 0, 0, 0.45); -} - -/* User profile name text. */ -#user_text { - font-size: 12px; - min-width: 135px; - font-weight: 900; - letter-spacing: 1px; - text-shadow: 0px 0px 20px black; - position: absolute; - right: 95px; - text-align: right; - -webkit-user-select: initial; -} - -/* Social media icon content container. */ -#mediaContent { - position: relative; - display: flex; - flex-direction: column; - margin-top: 25px; - height: calc(100% - 95px); - width: 70px; - align-items: center; -} - -/* Social Media Icon division containers. */ -#internalMedia, #externalMedia { - display: flex; - flex-direction: column; -} - -/* Container object which wraps an icon to ensure fluid transitions. */ -.mediaContainer { - display: flex; - justify-content: center; - align-items: center; - height: 27px; -} - -/* Divider bar between the external and internal icons. */ -.mediaDivider { - height: 1px; - width: 14px; - background: rgb(255, 255, 255); - margin: 10px 0px; -} - -/* Social media icon shared styles. */ -.mediaSVG { - fill: #ffffff; - height: 12px; - transition: 0.25s ease; - cursor: pointer; - height: 12px; - width: 25px; -} -.mediaSVG:hover, -.mediaURL:focus .mediaSVG, -.mediaSVG:active { - height: 20px; -} - -/* Social media URL shared styles. */ -.mediaURL { - outline: none; -} - -/* Internal media button shared styles. */ -.mediaButton { - background: none; - border: none; - padding: 0px; - display: flex; - align-items: center; - outline: none; -} - -#settingsMediaContainer { - position: relative; -} - -/* Settings icon colors. */ -#settingsSVG { - stroke: #ffffff; - height: 15px; -} -.mediaButton:hover #settingsSVG, -.mediaButton:focus #settingsSVG, -.mediaButton:active #settingsSVG { - height: 23px; -} - -/* Settings tooltip styles. */ -#settingsTooltip { - visibility: hidden; - opacity: 0; - width: 75px; - height: 20px; - background-color: rgba(0, 0, 0, 0.75); - text-align: center; - border-radius: 4px; - position: absolute; - z-index: 1; - right: 130%; - font-size: 12px; - line-height: 20px; - transition: visibility 0s linear 0.25s, opacity 0.25s ease; -} -#settingsTooltip::after { - content: " "; - position: absolute; - top: 50%; - left: 100%; - margin-top: -5px; - border-width: 5px; - border-style: solid; - border-color: transparent transparent transparent rgba(0, 0, 0, 0.75); -} -.mediaButton:hover #settingsTooltip, -.mediaButton:focus #settingsTooltip, -.mediaButton:active #settingsTooltip { - visibility: visible; - opacity: 1; - transition-delay:0s; -} - -/* Twitter icon colors. */ -#twitterSVG:hover, -#twitterURL:focus #twitterSVG { - fill: #1da1f2; -} -#twitterSVG:active { - fill: #1b8dd4; -} - -/* Instagram icon colors. */ -#instagramSVG:hover, -#instagramURL:focus #instagramSVG { - fill: url('#instaFill') - /*fill: radial-gradient(circle at 30% 107%, #fdf497 0%, #fdf497 5%, #fd5949 45%, #d6249f 60%, #285AEB 90%); */ -} -#instagramSVG:active { - fill: url('#instaFill') -} - -/* Youtube icon colors. */ -#youtubeSVG:hover, -#youtubeURL:focus #youtubeSVG { - fill: #f00; -} -#youtubeSVG:active { - fill: #ea0202; -} - -/* Discord icon colors. */ -#discordSVG:hover, -#discordURL:focus #discordSVG { - fill: #7288d9; -} -#discordSVG:active { - fill: #657ac4; -} - -/******************************************************************************* - * * - * Landing View (Bottom Styles) * - * * - ******************************************************************************/ - -/* Style for a general label on the bottom of the landing view. */ -.bot_label { - font-size: 9px; - letter-spacing: 1px; - font-weight: bold; - text-shadow: 0px 0px 0px #bebcbb; -} - -/* Divider used on the bottom of the landing view. */ -.bot_divider { - height: 25px; - width: 2px; - background: rgba(107, 105, 105, 0.7); - margin-left: 20px; - margin-right: 20px; -} - -/* * * -* Landing View (Bottom Styles) | Left Content -* * */ - -/* Maintains maximum width on the status bar. */ -#server_status_wrapper { - display: inline-flex; - width: 75px; -} - -/* Span which displays the player count of the selected server. */ -#player_count { - color: #949494; - font-size: 8px; - font-weight: 900; - text-shadow: 0px 0px 20px #949494; - margin-left: 10px; -} - -/* Wrapper container for the mojang status bar. */ -#mojangStatusWrapper { - position: relative; - display: flex; - cursor: pointer; -} - -/* Icon which displays the status of the mojang services. */ -#mojang_status_icon { - font-size: 30px; - color: #848484; - margin-left: 15px; - font-family: 'sans-serif'; -} - -/* Tooltip which displays more details about the mojang statuses. */ -#mojangStatusTooltip { - position: absolute; - visibility: hidden; - opacity: 0; - width: 145px; - min-height: 150px; - background-color: rgba(0, 0, 0, 0.75); - color: #fff; - border-radius: 4px; - padding: 5px 10px; - z-index: 1; - font-family: 'Avenir Medium'; - font-size: 12px; - transition: visibility 0s linear 0.25s, opacity 0.25s ease; - bottom: calc(100% + 15px); - transform: translateX(-50%); - margin-left: 50%; - box-shadow: 0px 0px 20px rgb(0, 0, 0); - cursor: default; -} -#mojangStatusTooltip:after { - content: " "; - position: absolute; - left: 50%; - top: 100%; - margin-left: -5px; - border-width: 5px; - border-style: solid; - border-color: rgba(0, 0, 0, 0.75) transparent transparent transparent; -} -#mojangStatusWrapper:hover #mojangStatusTooltip { - visibility: visible; - opacity: 1; - transition-delay: 0s; -} - -/* Tooltip title for the mojang statuses. */ -#mojangStatusTooltipTitle { - width: 100%; - text-align: center; - margin-bottom: 5px; - letter-spacing: 1px; -} - -/* Wrapper container for the non essential services title. */ -#mojangStatusNEContainer { - display: flex; - align-items: center; - margin: 10px 0px; -} - -/* White bar which surrounds the non essential service title. */ -.mojangStatusNEBar { - height: 1px; - width: 100%; - background: white; -} - -/* Non essential service title text. */ -#mojangStatusNETitle { - font-size: 10px; - padding: 0px 3px; - text-align: center; - letter-spacing: 1px; -} - -/* Wrapper container for mojang service information. */ -.mojangStatusContainer { - display: flex; -} - -/* Displays the name of the mojang service. */ -.mojangStatusName { - width: 100%; - font-size: 10px; - letter-spacing: 1px; - line-height: 12px; - padding: 6px 0px; -} - -/* Displays the status of the mojang service. */ -.mojangStatusIcon { - margin-right: 10px; - font-size: 18.5px; - color: #848484; -} - -/* * * -* Landing View (Bottom Styles) | Center Content -* * */ - -/* Button which opens the news view. */ -#newsButton { - background: none; - border: none; - cursor: pointer; - outline: none; -} -#newsButton:hover #newsButtonText, -#newsButton:focus #newsButtonText { - text-shadow: 0px 0px 20px #fff, 0px 0px 20px #fff; -} -#newsButton:active { - color: #c7c7c7; - text-shadow: 0px 0px 20px #c7c7c7, 0px 0px 20px #c7c7c7; -} - -#newsButton:hover #newsButtonSVG, -#newsButton:focus #newsButtonSVG { - -webkit-filter: drop-shadow(0px 0px 2px #fff); -} -#newsButton:active #newsButtonSVG .arrowLine { - stroke: #c7c7c7; -} -#newsButton:active #newsButtonSVG { - -webkit-filter: drop-shadow(0px 0px 2px #c7c7c7); -} -#newsButton:disabled #newsButtonSVG .arrowLine { - stroke: rgba(255, 255, 255, 0.75); -} - -/* Icon which indicates there is new news. */ -#newsButtonAlert { - width: 5px; - height: 5px; - position: absolute; - border-radius: 50%; - background: red; - right: -1px; - top: 50%; -} - -/* Arrow image which floats above the news button. */ -#newsButtonSVG { - height: 11px; - margin-left: -2px; - transition: 0.25s ease; -} - -/* Span which contains the news button text. */ -#newsButtonText { - color: white; - font-weight: 900; - letter-spacing: 2px; - text-shadow: 0px 0px 0px #bebcbb; - font-size: 11px; - line-height: 30px; - display: flex; - transition: 0.25s ease; -} - -/* * * -* Landing View (Bottom Styles) | Right Content -* * */ - -/* Main launch content container. */ -#landingContainer > #lower > #right #launch_content { - position: relative; - top: 25px; - display: inline-flex; -} - -/* The launch button. */ -#launch_button { - background: none; - border: none; - cursor: pointer; - font-weight: 900; - letter-spacing: 2px; - text-shadow: 0px 0px 0px #bebcbb; - font-size: 20px; - padding: 0px; - transition: 0.25s ease; - outline: none; -} -#launch_button:hover, -#launch_button:focus { - text-shadow: 0px 0px 20px #fff, 0px 0px 20px #fff; -} -#launch_button:active { - color: #c7c7c7; - text-shadow: 0px 0px 20px #c7c7c7, 0px 0px 20px #c7c7c7; -} -#launch_button:disabled { - color: #c7c7c7; - cursor: default; - pointer-events: none; -} - -/* Launch details main container, hidden until launch processing begins. */ -#launch_details { - position: relative; - top: 25px; - display: none; -} - -/* Left side of launch details container, displays percentage and a divider. */ -#launch_details_left { - display: flex; -} - -/* Span which displays percentage complete. */ -#launch_progress_label { - font-weight: 900; - letter-spacing: 1px; - text-shadow: 0px 0px 0px #bebcbb; - font-size: 20px; - min-width: 53.21px; - max-width: 53.21px; - text-align: right; -} - -/* Right side of launch details container, displays progress bar and details. */ -#launch_details_right { - display: flex; - flex-direction: column; - justify-content: center; -} - -/* Button which opens the server selection view. */ -#server_selection_button { - background: none; - border: none; - outline: none; - cursor: pointer; - line-height: 24px; - padding: 0px; - transition: 0.25s ease; -} -#server_selection_button:hover, -#server_selection_button:focus { - text-shadow: 0px 0px 20px #fff, 0px 0px 20px #fff, 0px 0px 20px #fff; -} -#server_selection_button:active { - color: #c7c7c7; - text-shadow: 0px 0px 20px #c7c7c7, 0px 0px 20px #c7c7c7, 0px 0px 20px #c7c7c7; -} - -/* Progress bar styles. */ -#launch_progress[value] { - height: 3px; - width: 265px; - -webkit-appearance: none; -} -#launch_progress[value]::-webkit-progress-bar { - background-color: transparent; -} -#launch_progress[value]::-webkit-progress-value { - background-color: #fff; -} - -/* Span which displays information about the status of the launch process. */ -#launch_details_text { - font-size: 11px; - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; -} - -/******************************************************************************* - * * - * Overlay View (overlay.ejs) * - * * - ******************************************************************************/ - -/* * * -* Overlay View (Main Content) -* * */ - -/* Overlay container, placed over the main div. */ -#overlayContainer { - position: absolute; - z-index: 500; - top: 22px; - display: flex; - align-items: center; - justify-content: center; - width: 100%; - height: calc(100% - 22px); - background: rgba(0, 0, 0, 0.50); -} - -/* Main overlay content. */ -#overlayContent { - position: relative; - display: flex; - flex-direction: column; - align-items: center; - /*justify-content: space-between;*/ - width: 300px; - /*height: 35%;*/ - box-sizing: border-box; - padding: 15px 0px; - /* background-color: #424242; */ - text-align: center; -} - -/* Main overlay content anchor styles. */ -#overlayContent a, -#overlayDismiss { - color: rgba(202, 202, 202, 0.75); - transition: 0.25s ease; -} -#overlayContent a:hover, -#overlayContent a:focus, -#overlayDismiss:focus { - color: rgba(255, 255, 255, 0.75); -} -#overlayContent a:active, -#overlayDismiss:active { - color: rgba(165, 165, 165, 0.75); -} - -/* Add spacing between overlay content elements. */ -#overlayContent > *:first-child { - margin-top: 0px !important; -} -#overlayContent > *:last-child { - margin-bottom: 0px !important; -} -#overlayContent > * { - margin: 8px 0px; -} - -/* Overlay title styles. */ -#overlayTitle { - font-family: 'Avenir Medium'; - font-size: 20px; - font-weight: bold; - letter-spacing: 1px; - -webkit-user-select: initial; -} - -/* Overlay description styles. */ -#overlayDesc { - font-size: 12px; - font-weight: bold; - -webkit-user-select: initial; -} - -/* Div which contains action buttons. */ -#overlayActionContainer { - display: flex; - flex-direction: column; - justify-content: center; -} - -/* Overlay acknowledge button styles. */ -#overlayAcknowledge { - background: none; - border: 1px solid #ffffff; - color: white; - font-family: 'Avenir Medium'; - font-weight: bold; - border-radius: 2px; - padding: 0px 8.1px; - cursor: pointer; - transition: 0.25s ease; -} -#overlayAcknowledge:hover, -#overlayAcknowledge:focus { - box-shadow: 0px 0px 10px 0px #fff; - outline: none; -} -#overlayAcknowledge:active { - border-color: rgba(255, 255, 255, 0.75); - color: rgba(255, 255, 255, 0.75); -} - -/* Overlay dismiss option styles. */ -#overlayDismiss { - font-weight: bold; - font-size: 10px; - text-decoration: none; - padding-top: 2.5px; - background: none; - border: none; - outline: none; - cursor: pointer; -} -#overlayDismiss:hover { - color: rgba(255, 255, 255, 0.75); -} -#overlayDismiss:active { - color: rgba(165, 165, 165, 0.75); -} - -/* * * -* Overlay View (Server + Account Selection Content) -* * */ - -/* Server selection content container. */ -#serverSelectContent, -#accountSelectContent { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - height: 75%; -} - -/* Server selection header. */ -#serverSelectHeader, -#accountSelectHeader { - font-family: 'Avenir Medium'; - font-size: 20px; - font-weight: bold; - color: #fff; - margin-bottom: 25px; -} - -/* Wrapper div for the list of available servers. */ -#serverSelectList, -#accountSelectList { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - max-height: 65%; - min-height: 40%; -} - -/* Scrollable div which lists the available servers. */ -#serverSelectListScrollable, -#accountSelectListScrollable { - padding: 0px 5px; - overflow-y: scroll; -} -#serverSelectListScrollable::-webkit-scrollbar, -#accountSelectListScrollable::-webkit-scrollbar { - width: 2px; -} -#serverSelectListScrollable::-webkit-scrollbar-track, -#accountSelectListScrollable::-webkit-scrollbar-track { - display: none; -} -#serverSelectListScrollable::-webkit-scrollbar-thumb, -#accountSelectListScrollable::-webkit-scrollbar-thumb { - border-radius: 10px; - box-shadow: inset 0 0 10px rgba(255, 255, 255, 0.50); -} - -/* Content container for a server listing. */ -.serverListing { - border: none; - padding: 0px; - width: 375px; - min-height: 60px; - display: flex; - justify-content: flex-start; - align-items: center; - opacity: 0.6; - transition: 0.25s ease; - cursor: pointer; - position: relative; - background: rgba(131, 131, 131, 0.25); -} -.serverListing[selected] { - cursor: default; - opacity: 1.0; -} -.serverListing:hover, -.serverListing:focus { - outline: none; - opacity: 1.0; -} - -.accountListing { - color: white; - border: 1px solid rgba(126, 126, 126, 0.57); - border-radius: 3px; - padding: 5px 45px; - width: 250px; - display: flex; - justify-content: flex-start; - align-items: center; - opacity: 0.6; - transition: 0.25s ease; - cursor: pointer; - position: relative; - background: rgba(0, 0, 0, 0.25); -} -.accountListing[selected] { - cursor: default; - opacity: 1.0; -} -.accountListing:hover, -.accountListing:focus { - outline: none; - opacity: 1.0; -} - -.accountListingName { - display: flex; - height: 100%; - width: 100%; - padding-left: 10px; -} - -/* Add spacing between server listings. */ -#serverSelectListScrollable > .serverListing:not(:first-child):not(:last-child), -#accountSelectListScrollable > .accountListing:not(:first-child):not(:last-child) { - margin: 5px 0px; -} -#serverSelectListScrollable > .serverListing:first-child, -#accountSelectListScrollable > .accountListing:first-child { - margin-bottom: 5px; -} -#serverSelectListScrollable > .serverListing:last-child, -#accountSelectListScrollable > .accountListing:last-child { - margin-top: 5px; -} - -/* Server listing image. */ -.serverListingImg { - margin: 0px 10px 0px 5px; - border: 1px solid #fff; - height: 50px; - width: 50px; -} - -/* Content container for the server listing's details. */ -.serverListingDetails { - display: flex; - flex-direction: column; - align-items: flex-start; - justify-content: space-between; - height: 50px; -} - -/* The name of the server listing. */ -.serverListingName { - font-size: 14px; - font-weight: bold; -} - -/* Description for the server listing. */ -.serverListingDescription { - font-size: 10px; - line-height: 10px; - font-weight: bold; -} - -/* Content container for the server listing's information. */ -.serverListingInfo { - width: 100%; - display: flex; - justify-content: flex-start; -} - -/* The minecraft version of the server listing. */ -.serverListingVersion { - font-size: 10px; - text-align: center; - display: flex; - justify-content: center; - align-items: center; - line-height: 12px; - height: 12px; - border-radius: 2px; - background: rgba(31, 140, 11, 0.8); - padding: 0px 2px; -} - -/* The revision version of the server's manifest. */ -.serverListingRevision { - color: #969696; - font-size: 10px; - line-height: 12px; - padding: 0px 5px; -} - -/* Star which indicates the default (main) server. */ -.serverListingStarWrapper { - display: flex; - align-items: center; - cursor: pointer; - height: 12px; - position: relative; -} -/* Tooltip which displays when hovering over the star. */ -.serverListingStarTooltip { - visibility: hidden; - opacity: 0; - width: 65px; - background-color: rgba(0, 0, 0, 0.40); - text-align: center; - border-radius: 4px; - position: absolute; - z-index: 1; - left: 130%; - font-size: 10px; - transition: visibility 0s linear 0.25s, opacity 0.25s ease; -} -.serverListingStarTooltip::after { - content: " "; - position: absolute; - top: 50%; - right: 100%; /* To the left of the tooltip */ - margin-top: -5px; - border-width: 5px; - border-style: solid; - border-color: transparent rgba(0, 0, 0, 0.40) transparent transparent; -} -.serverListingStarWrapper:hover .serverListingStarTooltip { - visibility: visible; - opacity: 1; - transition-delay:0s; -} - -/* Content container which contains the server select actions. */ -#serverSelectActions, -#accountSelectActions { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - margin-top: 25px; -} - -/* Server selection confirm button styles. */ -#serverSelectConfirm, -#accountSelectConfirm { - background: none; - border: 1px solid #ffffff; - color: white; - font-family: 'Avenir Medium'; - font-weight: bold; - border-radius: 2px; - padding: 0px 8.1px; - cursor: pointer; - transition: 0.25s ease; - min-height: 20.67px; -} -#serverSelectConfirm:hover, -#serverSelectConfirm:focus, -#accountSelectConfirm:hover, -#accountSelectConfirm:focus { - box-shadow: 0px 0px 10px 0px #fff; - outline: none; -} -#serverSelectConfirm:active, -#accountSelectConfirm:active { - border-color: rgba(255, 255, 255, 0.75); - color: rgba(255, 255, 255, 0.75); -} - -/* Server selection cancel button styles. */ -#serverSelectCancel, -#accountSelectCancel { - font-weight: bold; - font-size: 10px; - text-decoration: none; - padding-top: 2.5px; - color: rgba(202, 202, 202, 0.75); - transition: 0.25s ease; - background: none; - border: none; - outline: none; - cursor: pointer; -} -#serverSelectCancel:hover, -#serverSelectCancel:focus, -#accountSelectCancel:hover, -#accountSelectCancel:focus { - color: rgba(255, 255, 255, 0.75); -} -#serverSelectCancel:active, -#accountSelectCancel:active { - color: rgba(165, 165, 165, 0.75); -} - -/******************************************************************************* - * * - * Loading Element (app.ejs) * - * * - ******************************************************************************/ - -/* Loading container, placed above everything. */ -#loadingContainer { - position: absolute; - z-index: 400; - display: flex; - align-items: center; - justify-content: center; - width: 100%; - height: calc(100% - 22px); -} - -/* Loading content container. */ -#loadingContent { - position: relative; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; -} - -/* Spinner container. */ -#loadSpinnerContainer { - position: relative; - display: flex; - align-items: center; - justify-content: center; -} - -/* Stationary image for the spinner. */ -#loadCenterImage { - position: absolute; - width: 277px; - height: auto; -} - -/* Rotating image for the spinner. */ -#loadSpinnerImage { - width: 280px; - height: auto; - z-index: 400; -} - -/* Rotating animation for the spinner. */ -@keyframes rotating { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -/* Class which is applied when the spinner image is spinning. */ -.rotating { - animation: rotating 10s linear infinite; +/* Github Code Highlighting. */ +@import "../../../node_modules/github-syntax-dark/lib/github-dark.css"; + +/******************************************************************************* + * * + * Fonts * + * * + ******************************************************************************/ + +@font-face { + font-family: 'Avenir Book'; + src: url('../fonts/Avenir-Book.ttf'); +} + +@font-face { + font-family: 'Avenir Medium'; + src: url('../fonts/Avenir-Medium.ttf'); +} + +@font-face { + font-family: 'Ringbearer'; + src: url('../fonts/Ringbearer.ttf'); +} + +/******************************************************************************* + * * + * Element Styles * + * * + ******************************************************************************/ + +/* Reset body, html, and div presets. */ +body, html, div { + margin: 0px; + padding: 0px; +} + +/* Reset p presets. */ +p { + -webkit-margin-before: 0em; + -webkit-margin-after: 0em; +} + +/* Set default font and color. */ +body, button { + font-family: 'Avenir Book'; + color: white; +} + +/*body { + background: url('./../images/backgrounds/0.jpg') no-repeat center center fixed; + background-size: cover; +}*/ + +/******************************************************************************* + * * + * Frame Styles (frame.ejs) * + * * + ******************************************************************************/ + +/* Frame Bar */ +#frameBar { + position: relative; + z-index: 100; + display: flex; + flex-direction: column; + transition: background-color 1s ease; + /*background-color: rgba(0, 0, 0, 0.5);*/ + -webkit-user-select: none; +} + +/* Undraggable region on the top of the frame. */ +#frameResizableTop { + height: 2px; + width: 100%; + -webkit-app-region: no-drag; +} + +/* Flexbox to wrap the main frame content. */ +#frameMain { + display: flex; + height: 20px +} + +/* Undraggable region on the left and right of the frame. */ +.frameResizableVert { + width: 2px; + -webkit-app-region: no-drag; +} + +/* Main frame content for windows. */ +#frameContentWin { + display: flex; + justify-content: space-between; + width: 100%; + -webkit-app-region: drag; +} + +/* Main frame content for darwin. */ +#frameContentDarwin { + display: flex; + justify-content: flex-start; + align-items: center; + width: 100%; + -webkit-app-region: drag; +} + +/* Frame logo (windows only). */ +#frameTitleDock { + padding: 0px 10px; +} +#frameTitleText { + font-size: 14px; + font-family: 'Avenir Medium'; + letter-spacing: 0.5px; +} + +/* Windows frame button dock. */ +#frameButtonDockWin { + -webkit-app-region: no-drag !important; + position: relative; + top: -2px; + right: -2px; + height: 22px; +} +#frameButtonDockWin > .frameButton:not(:first-child) { + margin-left: -4px; +} + +/* Darwin frame button dock: NaN; */ +#frameButtonDockDarwin { + -webkit-app-region: no-drag !important; + position: relative; + top: -1px; + right: -1px; +} + +/* Windows Frame Button Styles. */ +.frameButton { + background: none; + border: none; + height: 22px; + width: 39px; + cursor: pointer; +} +.frameButton:hover, +.frameButton:focus { + background: rgba(189, 189, 189, 0.43); +} +.frameButton:active { + background: rgba(156, 156, 156, 0.43); +} +.frameButton:focus { + outline: 0px; +} + +/* Close button is red. */ +#frameButton_close:hover, +#frameButton_close:focus { + background: rgba(255, 53, 53, 0.61) !important; +} +#frameButton_close:active { + background: rgba(235, 0, 0, 0.61) !important; +} + +/* Darwin Frame Button Styles. */ +.frameButtonDarwin { + height: 12px; + width: 12px; + border-radius: 50%; + border: 0px; + margin-left: 5px; + -webkit-app-region: no-drag !important; + cursor: pointer; +} +.frameButtonDarwin:focus { + outline: 0px; +} + +#frameButtonDarwin_close { + background-color: #e74c32; +} +#frameButtonDarwin_close:hover, +#frameButtonDarwin_close:focus { + background-color: #FF9A8A; +} +#frameButtonDarwin_close:active { + background-color: #ff8d7b; +} + +#frameButtonDarwin_minimize { + background-color: #fed045; +} +#frameButtonDarwin_minimize:hover, +#frameButtonDarwin_minimize:focus { + background-color: #FFE9A9; +} +#frameButtonDarwin_minimize:active { + background-color: #ffde7b; +} + +#frameButtonDarwin_restoredown { + background-color: #96e734; +} +#frameButtonDarwin_restoredown:hover, +#frameButtonDarwin_restoredown:focus { + background-color: #D6FFA6; +} +#frameButtonDarwin_restoredown:active { + background-color: #bfff76; +} + +/******************************************************************************* + * * + * Welcome View (welcome.ejs) * + * * + ******************************************************************************/ + +#welcomeContainer { + position: relative; + display: flex; + justify-content: center; + align-items: center; + height: 100%; + width: 100%; +} + +#welcomeContent { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + width: 50%; + top: -10%; + position: relative; +} + +/* +.cloudDiv { + position: absolute; + height: 100%; + width: 100%; + display: flex; + flex-direction: column; +} + +.cloudTop { + height: 50%; + width: 100%; + background-image: url('../images/cloudTrans.png'); + animation: clouds1 80s linear infinite; + background-size: cover; +} + +.cloudBottom { + height: 50%; + width: 100%; + background-image: url('../images/cloudTrans2.png'); + animation: clouds2 70s linear infinite; + background-size: cover; +} + +@keyframes clouds1 { + to { + background-position: 200%; + } +} +@keyframes clouds2 { + to { + background-position: 230%; + } +} +*/ + +#welcomeImageSeal { + border-radius: 50%; + border: 2px solid #cad7e1; + background: rgba(1, 2, 1, 0.5); + height: 125px; + width: 125px; + box-shadow: 0px 0px 10px 0px rgb(0, 0, 0); + margin-bottom: 5%; + margin-top: 10%; +} + +#welcomeHeader { + font-family: 'Avenir Medium'; + text-align: center; + color: white; + margin-bottom: 25px; + letter-spacing: 1px; + font-size: 20px; + text-shadow: white 0px 0px 0px; +} + +#welcomeDescription { + text-align: justify; + font-size: 13px; + font-weight: 100; + text-shadow: rgba(255, 255, 255, 0.75) 0px 0px 20px +} + +#welcomeDescCTA { + text-align: center; + font-size: 14px; + font-weight: 100; + text-shadow: rgba(255, 255, 255, 0.75) 0px 0px 20px +} + +/* Login button styles. */ +#welcomeButton { + background: none; + font-weight: bold; + letter-spacing: 2px; + border: none; + padding: 15px 5px; + margin: 10px 0px; + cursor: pointer; + position: relative; + right: -20px; + transition: 0.5s ease; + margin-top: 5%; + margin-bottom: -5%; +} +#welcomeButton:disabled { + color: rgba(255, 255, 255, 0.75); + pointer-events: none; +} +#welcomeButton:hover, +#welcomeButton:focus { + text-shadow: 0px 0px 20px #fff; + outline: none; +} +#welcomeButton:active { + color: #c7c7c7; + text-shadow: 0px 0px 20px #c7c7c7; +} +#welcomeSVG { + -webkit-transform: translate3d(0, 0, 0); + overflow: visible; + transform: rotate(90deg); + margin-left: 20px; + transition: 0.25s ease; + width: 20px; + height: 20px; +} +#welcomeButton:hover #welcomeSVG, +#welcomeButton:focus #welcomeSVG { + -webkit-filter: drop-shadow(0px 0px 2px #fff); +} +#welcomeButton:active #welcomeSVG .arrowLine { + stroke: #c7c7c7; +} +#welcomeButton:active #welcomeSVG { + -webkit-filter: drop-shadow(0px 0px 2px #c7c7c7); +} +#welcomeButton:disabled #welcomeSVG .arrowLine { + stroke: rgba(255, 255, 255, 0.75); +} + +#welcomeButtonContent { + display: flex; + align-items: center; +} + +/******************************************************************************* + * * + * Login View (login.ejs) * + * * + ******************************************************************************/ + +/* Styles for dimmer login span. */ +.loginSpanDim { + font-size: 12px; + color: #848484; + font-weight: bold; +} + +/* Main login container. */ +#loginContainer { + position: relative; + display: flex; + justify-content: center; + align-items: center; + height: 100%; + width: 100%; + transition: filter 0.25s ease; + background: rgba(0, 0, 0, 0.50); +} + +/* Login cancel button styles. */ +#loginCancelContainer { + position: absolute; + top: 5%; + right: 5%; +} + +/* Login cancel button styles. */ +#loginCancelButton { + background: none; + border: none; + outline: none; + cursor: pointer; + transition: 0.25s ease; +} +#loginCancelButton:hover #loginCancelIcon, +#loginCancelButton:hover #loginCancelText, +#loginCancelButton:focus #loginCancelIcon, +#loginCancelButton:focus #loginCancelText { + text-shadow: 0px 0px 20px white; +} +#loginCancelButton:hover #loginCancelIcon, +#loginCancelButton:focus #loginCancelIcon { + box-shadow: 0px 0px 20px white; +} +#loginCancelButton:active #loginCancelIcon, +#loginCancelButton:active #loginCancelText { + text-shadow: 0px 0px 20px rgba(255, 255, 255, 0.75); + color: rgba(255, 255, 255, 0.75); + border-color: rgba(255, 255, 255, 0.75); +} +#loginCancelButton:active #loginCancelIcon { + box-shadow: 0px 0px 20px rgba(255, 255, 255, 0.75); +} +#loginCancelButton:disabled { + pointer-events: none; +} +#loginCancelButton:disabled #loginCancelIcon, +#loginCancelButton:disabled #loginCancelText { + color: rgba(255, 255, 255, 0.75); + border-color: rgba(255, 255, 255, 0.75); +} + +/* The X in a circle icon for the cancel button. */ +#loginCancelIcon { + border-radius: 50%; + border: 1px solid white; + box-sizing: border-box; + height: 30px; + width: 30px; + font-size: 19px; + line-height: 30px; + margin: 0 auto; + margin-bottom: 5px; + transition: 0.25s ease; +} +/* Text for the login cancel button. */ +#loginCancelText { + font-size: 15px; + transition: 0.25s ease; +} + +/* Login content wrapper. */ +#loginContent { + display: flex; + justify-content: center; + align-items: center; + height: 100%; + padding: 0px 25px; +} + +/* Login form. */ +#loginForm { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +/* Login form anchor styles. */ +#loginForm a { + font-size: 12px; + color: #848484; + font-weight: bold; + text-decoration: none; + transition: 0.25s ease; +} +#loginForm a:hover, +#loginForm a:focus { + color: #a2a2a2; + outline: none; +} +#loginForm a:active { + color: #8b8b8b; +} + +/* Logo on login form. */ +#loginImageSeal { + border-radius: 50%; + border: 2px solid #cad7e1; + background: rgba(1, 2, 1, 0.5); + height: 125px; + width: 125px; + box-shadow: 0px 0px 10px 0px rgb(0, 0, 0); + margin-bottom: 20px; +} + +/* Header on login view. */ +#loginSubheader { + font-family: 'Avenir Medium'; + margin-bottom: 25px; + font-size: 12px; + letter-spacing: 1px; + font-weight: bold; +} + +/* Container to organize login field elements. */ +.loginFieldContainer { + position: relative; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +/* SVG icons on the login view. */ +.loginSVG { + fill: #fff; + height: 20px; + width: 20px; +} + +/* Span which displays errors related to login field content. */ +.loginErrorSpan { + font-family: 'Avenir Medium'; + font-weight: bold; + font-size: 8px; + color: #ff1b0c; + width: 100%; + text-align: right; + position: absolute; + top: 7px; + opacity: 0; + transition: 0.25s ease; +} + +.shake { + animation: shake 0.82s cubic-bezier(.36,.07,.19,.97) both; +} + +@keyframes shake { + 10%, 90% { + transform: translate3d(-1px, 0, 0); + } + + 20%, 80% { + transform: translate3d(2px, 0, 0); + } + + 30%, 50%, 70% { + transform: translate3d(-4px, 0, 0); + } + + 40%, 60% { + transform: translate3d(4px, 0, 0); + } +} + +/* Login text input styles. */ +.loginField { + font-family: 'Avenir Book'; + background: none; + border-width: 1.5px 0px 0px 0px; + border-style: solid; + width: 250px; + margin-bottom: 20px; + border-color: #fff; + color: rgba(255, 255, 255, 0.75); + font-weight: bold; + text-align: center; + box-sizing: border-box; + padding: 7.5px; + font-size: 10px; + letter-spacing: 1px; +} +.loginField:focus { + outline: none; +} +.loginField:disabled { + color: rgba(255, 255, 255, 0.50); +} +.loginField::-webkit-input-placeholder { + color: rgba(255, 255, 255, 0.75); + font-size: 10px; + letter-spacing: 1px; + text-align: center; + font-weight: bold; +} +.loginField:focus::-webkit-input-placeholder { + color: transparent; +} + +/* Add spacing between password field and options bar. */ +#labelPassword { + margin-bottom: 13px; +} + +/* Container which contains the forgot and remember options. */ +#loginOptions { + display: flex; + justify-content: space-between; + width: 100%; +} + +/* Remember option text. */ +#loginRememberText { + padding-right: 10px; + transition: 0.25s ease; +} + +/* Login button styles. */ +#loginButton { + background: none; + font-weight: bold; + letter-spacing: 2px; + border: none; + padding: 15px 5px; + margin: 10px 0px; + cursor: pointer; + position: relative; + right: -20px; + transition: 0.5s ease; +} +#loginButton:disabled { + color: rgba(255, 255, 255, 0.75); + pointer-events: none; +} +#loginButton[loading] { + color: #fff; +} +#loginButton:hover, +#loginButton:focus { + text-shadow: 0px 0px 20px #fff; + outline: none; +} +#loginButton:active { + color: #c7c7c7; + text-shadow: 0px 0px 20px #c7c7c7; +} +#loginSVG { + -webkit-transform: translate3d(0, 0, 0); + overflow: visible; + transform: rotate(90deg); + margin-left: 20px; + transition: 0.25s ease; + width: 20px; + height: 20px; +} +#loginButton:hover #loginSVG, +#loginButton:focus #loginSVG { + -webkit-filter: drop-shadow(0px 0px 2px #fff); +} +#loginButton:active #loginSVG .arrowLine { + stroke: #c7c7c7; +} +#loginButton:active #loginSVG { + -webkit-filter: drop-shadow(0px 0px 2px #c7c7c7); +} +#loginButton:disabled #loginSVG .arrowLine { + stroke: rgba(255, 255, 255, 0.75); +} + +#loginButtonContent { + display: flex; + align-items: center; +} + +#loginButton .circle-loader, +#loginButton[loading] #loginSVG { + display: none; +} +#loginButton[loading] .circle-loader, +#loginButton #loginSVG { + display: initial; +} + + +.circle-loader { + margin-left: 20px; + border: 2px solid rgba(255, 255, 255, 0.5); + border-left-color: #ffffff; + animation-name: loader-spin; + animation-duration: 1s; + animation-iteration-count: infinite; + animation-timing-function: linear; + position: relative; + display: inline-block; + vertical-align: top; + border-radius: 50%; + width: 16px; + height: 16px; +} +.load-complete { + animation: none; + border-color: #ffffff; + transition: border 500ms ease-out; +} +.checkmark { + display: none; +} +.checkmark.draw:after { + animation-duration: 800ms; + animation-timing-function: ease; + animation-name: checkmark; + transform: scaleX(-1) rotate(135deg); +} +.checkmark:after { + opacity: 1; + height: 8px; + width: 4px; + transform-origin: left top; + border-right: 2px solid #ffffff; + border-top: 2px solid #ffffff; + content: ''; + left: 2px; + top: 8px; + position: absolute; +} +@keyframes loader-spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} +@keyframes checkmark { + 0% { + height: 0; + width: 0; + opacity: 1; + } + 20% { + height: 0; + width: 4px; + opacity: 1; + } + 40% { + height: 8px; + width: 4px; + opacity: 1; + } + 100% { + height: 8px; + width: 4px; + opacity: 1; + } +} + + + +/*.spinningCircle { + margin-left: 20px; + height: 16px; + width: 16px; + border-radius: 50%; + border: 2px solid rgba(255,255,255,0); + border-top-color: #ffffff; + border-right-color: #ffffff; + border-left-color: rgba(255, 255, 255, 0.50); + border-bottom-color: rgba(255, 255, 255, 0.50); + animation: single2 4s infinite linear; +} + +@keyframes single2 { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(720deg); + } +}*/ + +/* Disclaimer container. */ +#loginDisclaimer { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +/* Add spacing between register anchor and disclaimer. */ +#loginRegisterSpan { + margin-bottom: 5px; +} + +/* Disclaimer text styles. */ +.loginDisclaimerText { + font-size: 7px; + color: #848484; + font-weight: bold; + text-align: center; +} + +/* * * +* Login View | Custom Checkbox +* * */ + +/* Checkbox container. */ +#checkmarkContainer { + display: flex; + justify-content: flex-end; + align-items: center; + position: relative; + cursor: pointer; + font-size: 22px; + -webkit-user-select: none; +} + +/* Hide the default checkbox. */ +#checkmarkContainer input { + opacity: 0; + cursor: pointer; + position: absolute; +} + +/* Create a custom checkbox. */ +.loginCheckmark { + position: relative; + height: 10px; + width: 10px; + border: 1px solid #848484; + border-radius: 1px; + background: none; + transition: 0.25s ease; +} +/* On hover and focus, add a grey border color. */ +#checkmarkContainer:hover input ~ *, +#checkmarkContainer input:focus ~ * { + color: #a2a2a2; + border-color: #a2a2a2; +} +/* On keydown, darken the checkbox a bit. */ +#checkmarkContainer input:active ~ *:not(#loginRememberText) { + color: #8d8d8d; + border-color: #8d8d8d; +} +#checkmarkContainer[disabled] { + pointer-events: none; +} +/* For checked -> #checkmarkContainer input:checked ~ * */ +/* Create the checkmark/indicator (hidden when not checked). */ +.loginCheckmark:after { + content: ""; + display: none; +} +/* Show the checkmark when checked. */ +#checkmarkContainer input:checked ~ .loginCheckmark:after { + display: block; +} +/* Style the checkmark/indicator. */ +#checkmarkContainer .loginCheckmark:after { + position: absolute; + left: 3.5px; + top: 0.5px; + width: 2px; + height: 6px; + border: solid #a2a2a2; + border-width: 0 2px 2px 0; + transform: rotate(45deg); +} + +/* +#login_filter { + height: calc(100% - 22px); + width: 100%; + z-index: 9000; + position: absolute; + filter: blur(8px) contrast(0.9) brightness(1.0); + background: url('./../images/backgrounds/0.jpg') no-repeat center center fixed; + transform: scale(1.2); + background-size: cover; +} +*/ + +/******************************************************************************* + * * + * Settings View (sttings.ejs) * + * * + ******************************************************************************/ + +/* Main settings container. */ +#settingsContainer { + position: relative; + height: 100%; + display: flex; + background-color: rgba(0, 0, 0, 0.50); + transition: background-color 0.25s cubic-bezier(.02, .01, .47, 1); +} + +/* Drop shadow displayed when content is scrolled out of view. */ +#settingsContainer:before { + content: ''; + background: linear-gradient(rgba(0, 0, 0, 0.25), transparent); + width: 100%; + height: 5px; + position: absolute; + opacity: 0; + transition: opacity 0.25s ease; +} +#settingsContainer[scrolled]:before { + opacity: 1; +} + +/* Left hand side of the settings UI, for navigation. */ +#settingsContainerLeft { + padding-top: 4%; + height: 100%; + width: 25%; + box-sizing: border-box; +} + +/* Settings navigation container. */ +#settingsNavContainer { + height: 100%; + display: flex; + flex-direction: column; +} + +/* Navigation header styles. */ +#settingsNavHeader { + height: 15%; + display: flex; + justify-content: center; +} +#settingsNavHeaderText { + font-size: 20px; +} + +/* Navigation items outer container. */ +#settingsNavItemsContainer { + height: 85%; + display: flex; + justify-content: center; + box-sizing: border-box; +} + +/* Navigation items content container. */ +#settingsNavItemsContent { + height: 100%; + display: flex; + flex-direction: column; + position: relative; +} + +/* Navigation item shared styles. */ +.settingsNavItem { + background: none; + border: none; + text-align: left; + margin: 5px 0px; + padding: 0px 20px; + color: grey; + cursor: pointer; + outline: none; + transition: 0.25s ease; +} +.settingsNavItem:hover, +.settingsNavItem:focus { + color: #c1c1c1; + text-shadow: 0px 0px 20px #c1c1c1; +} +.settingsNavItem[selected] { + cursor: default; + color: white; + text-shadow: none; +} + +/* Div to add some space between nav items. */ +.settingsNavSpacer { + height: 25px; +} + +/* Content container for the done button. */ +#settingsNavContentBottom { + position: absolute; + top: 65%; +} + +/* Settings navigational divider. */ +.settingsNavDivider { + width: 75%; + height: 1px; + background: rgba(126, 126, 126, 0.57); + margin-left: auto; + margin-bottom: 25px; +} + +/* Settings done button styles. */ +#settingsNavDone { + background: none; + border: none; + text-align: left; + margin: 5px 0px; + padding: 0px 20px; + color: white; + cursor: pointer; + outline: none; + transition: 0.25s ease; +} +#settingsNavDone:hover, +#settingsNavDone:focus { + text-shadow: 0px 0px 20px white, 0px 0px 20px white, 0px 0px 20px white; +} +#settingsNavDone:active { + text-shadow: 0px 0px 20px rgba(255, 255, 255, 0.75), 0px 0px 20px rgba(255, 255, 255, 0.75), 0px 0px 20px rgba(255, 255, 255, 0.75); + color: rgba(255, 255, 255, 0.75); +} +#settingsNavDone:disabled { + color: rgba(255, 255, 255, 0.75); + pointer-events: none; +} + +/* Right hand side of the settings container, for tabs. */ +#settingsContainerRight { + height: 100%; + width: 75%; + box-sizing: border-box; +} + +/* Settings tab shared styles. */ +.settingsTab { + width: 100%; + height: 100%; + overflow-y: auto; +} +.settingsTab::-webkit-scrollbar { + width: 2px; +} +.settingsTab::-webkit-scrollbar-track { + display: none; +} +.settingsTab::-webkit-scrollbar-thumb { + border-radius: 10px; + box-shadow: inset 0 0 10px rgba(255, 255, 255, 0.50); +} + +/* Add spacing to the top of each settings tab. */ +.settingsTab > *:first-child { + margin-top: 5%; +} + +/* Add spacing to the bottom of each settings tab. */ +.settingsTab > *:last-child { + margin-bottom: 20%; +} + +/* Tab header shared styles. */ +.settingsTabHeader { + display: flex; + flex-direction: column; + margin-bottom: 20px; +} +.settingsTabHeaderText { + font-size: 20px; + font-family: 'Avenir Medium'; +} +.settingsTabHeaderDesc { + font-size: 12px; +} + +/* Remove spin button from number inputs. */ +#settingsContainer input[type=number]::-webkit-inner-spin-button { + -webkit-appearance: none; +} + +/* Default styles for text/number inputs. */ +#settingsContainer input[type=number], +#settingsContainer input[type=text] { + color: white; + background: rgba(0, 0, 0, 0.25); + border-radius: 3px; + border: 1px solid rgba(126, 126, 126, 0.57); + font-family: 'Avenir Book'; + transition: 0.25s ease; +} +#settingsContainer input[type=number]:focus, +#settingsContainer input[type=text]:focus { + outline: none; + border-color: rgba(126, 126, 126, 0.87); +} +#settingsContainer input[type=number][error] { + border-color: rgb(255, 27, 12); + background: rgba(236, 0, 0, 0.25); + color: rgb(255, 27, 12); +} + +/* Styles for a generic settings entry. */ +.settingsFieldContainer { + display: flex; + align-items: center; + justify-content: space-between; + padding: 20px 0px; + width: 75%; + border-bottom: 1px solid rgba(255, 255, 255, 0.50); +} +.settingsFieldLeft { + display: flex; + flex-direction: column; +} +.settingsFieldTitle { + font-size: 14px; + font-family: 'Avenir Medium'; + color: rgba(255, 255, 255, 0.95); +} +.settingsFieldDesc { + font-size: 12px; + color: rgba(255, 255, 255, .95); + margin-top: 5px; +} +.settingsDivider { + height: 1px; + width: 75%; + background: rgba(255, 255, 255, 0.25); +} + +/* Toggle Switch */ +.toggleSwitch { + position: relative; + display: inline-block; + width: 40px; + height: 20px; + border-radius: 50px; + box-sizing: border-box; +} +.toggleSwitch input { + display:none; +} +.toggleSwitchSlider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(255, 255, 255, 0.35); + transition: .4s; + border-radius: 50px; + border: 1px solid rgba(126, 126, 126, 0.57); +} +.toggleSwitchSlider:before { + position: absolute; + content: ""; + height: 13px; + width: 16px; + left: 3px; + bottom: 3px; + background-color: white; + box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.75); + border-radius: 50px; + transition: .4s; +} +input:checked + .toggleSwitchSlider { + background-color: rgb(31, 140, 11); + /* box-shadow: inset 2px 1px 20px black; */ + border: 1px solid rgb(31, 140, 11); +} +input:checked + .toggleSwitchSlider:before { + transform: translateX(15px); +} + +/* Range Slider styles. */ +.rangeSlider { + width: 35%; + height: 5px; + margin: 15px 0px; + background: grey; + border-radius: 3px; + position: relative; +} +.rangeSliderBar { + position: absolute; + background: #8be88b; + width: 50%; + height: 5px; + border-radius: 3px 0px 0px 3px; + transition: background 0.25s ease; +} +.rangeSliderTrack { + position: absolute; + top: -7.5px; + width: 7px; + height: 20px; + background: white; + border-radius: 3px; + left: 50%; + cursor: ew-resize; +} + +/* File selectors */ + +/* Main container for File selectors. */ +.settingsFileSelContainer { + display: flex; + flex-direction: column; + border-bottom: 1px solid rgba(255, 255, 255, 0.50); + margin-bottom: 20px; + margin-top: 20px; + width: 75%; +} + +/* File selector title. */ +.settingsFileSelTitle { + margin-bottom: 10px; +} + +/* Wrapper container for the actionable elements. */ +.settingsFileSelActions { + display: flex; + width: 90%; +} + +/* File selector icon settings. */ +.settingsFileSelIcon { + display: flex; + align-items: center; + background: rgba(126, 126, 126, 0.57); + border-radius: 3px 0px 0px 3px; + padding: 5px; + transition: 0.25s ease; +} +.settingsFileSelSVG { + width: 20px; + height: 20px; + fill: white; +} + +/* Disabled text field which stores the selected file path. */ +.settingsFileSelVal { + border-radius: 0px !important; + width: 100%; + padding: 5px 10px; + font-size: 12px; + height: 30px; +} + +/* File selection button. */ +.settingsFileSelButton { + border: 0px; + border-radius: 0px 3px 3px 0px; + font-size: 12px; + padding: 0px 5px; + cursor: pointer; + background: rgba(126, 126, 126, 0.57); + transition: 0.25s ease; + white-space: nowrap; + outline: none; +} +.settingsFileSelButton:hover, +.settingsFileSelButton:focus { + text-shadow: 0px 0px 20px white; +} +.settingsFileSelButton:active { + text-shadow: 0px 0px 20px rgba(255, 255, 255, 0.75); + color: rgba(255, 255, 255, 0.75); +} + +/* Description for the file selector. */ +.settingsFileSelDesc { + font-size: 10px; + margin: 20px 0px; + color: lightgrey; + width: 89%; +} +.settingsFileSelDesc strong { + font-family: 'Avenir Medium'; +} + +/* * * +* Settings View (Account Tab) +* * */ + +/* Add account button styles. */ +#settingsAddAccount { + background: rgba(0, 0, 0, 0.25); + border: 1px solid rgba(126, 126, 126, 0.57); + border-radius: 3px; + height: 50px; + width: 75%; + text-align: left; + padding: 0px 50px; + cursor: pointer; + outline: none; + transition: 0.25s ease; +} +#settingsAddAccount:hover, +#settingsAddAccount:focus { + background: rgba(54, 54, 54, 0.25); + text-shadow: 0px 0px 20px white; +} + +/* Settings auth accounts header. */ +#settingsCurrentAccountsHeader { + margin: 20px 0px; +} + +/* Auth account list container styles. */ +#settingsCurrentAccounts { + margin-bottom: 5%; +} +#settingsCurrentAccounts > .settingsAuthAccount:not(:last-child) { + margin-bottom: 10px; +} +#settingsCurrentAccounts > .settingsAuthAccount:not(:first-child) { + margin-top: 10px; +} + +/* Auth account shared styles. */ +.settingsAuthAccount { + display: flex; + width: 75%; + background: rgba(0, 0, 0, 0.25); + border-radius: 3px; + border: 1px solid rgba(126, 126, 126, 0.57); +} + +/* Left hand side of an auth account element, for the skin image. */ +.settingsAuthAccountLeft { + padding: 5px 5px 5px 20px; +} + +/* Image of the auth account's skin. */ +.settingsAuthAccountImage { + height: 115px; +} + +/* Right hand side of the auth account, for info + actions. */ +.settingsAuthAccountRight { + display: flex; + width: 100%; +} + +/* Account details container. */ +.settingsAuthAccountDetails { + display: flex; + flex-direction: column; + justify-content: center; + margin-left: 20px; + width: 100%; +} +.settingsAuthAccountDetails > *:not(:last-child) { + margin-bottom: 20px; +} + +/* Account detail element styles. */ +.settingsAuthAccountDetailPane { + display: flex; + flex-direction: column; +} +.settingsAuthAccountDetailTitle { + font-size: 12px; + color: grey; + font-weight: bold; + font-family: 'Avenir Medium'; +} +.settingsAuthAccountDetailValue { + font-size: 14px; + -webkit-user-select: initial; +} + +/* Account actions container. */ +.settingsAuthAccountActions { + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: flex-end; + padding: 10px; +} + +/* Account select button shared styles. */ +.settingsAuthAccountSelect { + opacity: 0; + border: none; + white-space: nowrap; + background: none; + font-family: 'Avenir Medium'; + outline: none; + transition: 0.25s ease; +} +.settingsAuthAccountSelect:hover:not([selected]), +.settingsAuthAccountSelect:focus:not([selected]) { + text-shadow: 0px 0px 20px white, 0px 0px 20px white; + cursor: pointer; +} +.settingsAuthAccount:hover .settingsAuthAccountSelect:not([selected]), +.settingsAuthAccountSelect[selected] { + opacity: 1; +} +.settingsAuthAccountSelect[selected] { + pointer-events: none; +} + +/* Account logout button shared styles. */ +.settingsAuthAccountLogOut { + opacity: 0; + border: 1px solid rgb(241, 55, 55); + color: rgb(241, 55, 55); + background: none; + font-size: 12px; + border-radius: 3px; + font-family: 'Avenir Medium'; + transition: 0.25s ease; + cursor: pointer; + outline: none; +} +.settingsAuthAccountLogOut:hover, +.settingsAuthAccountLogOut:focus { + box-shadow: 0px 0px 20px rgb(241, 55, 55); + background: rgba(241, 55, 55, 0.25); +} +.settingsAuthAccountLogOut:active { + box-shadow: 0px 0px 20px rgb(185, 47, 47); + background: rgba(185, 47, 47, 0.25); + border: 1px solid rgb(185, 47, 47); + color: rgb(185, 47, 47); +} +.settingsAuthAccount:hover .settingsAuthAccountLogOut { + opacity: 1; +} + +/* * * +* Settings View (Minecraft Tab) +* * */ + +/* Game resolution UI elements. */ +#settingsGameResolutionContainer { + display: flex; + flex-direction: column; + padding-bottom: 20px; + border-bottom: 1px solid rgba(255, 255, 255, 0.50); + width: 75%; +} +#settingsGameResolutionContent { + display: flex; + align-items: center; + padding-top: 10px; +} +#settingsGameResolutionCross { + color: grey; + padding: 0px 15px; +} +#settingsGameWidth, +#settingsGameHeight { + padding: 7.5px 5px; + width: 75px; +} + +/* * * +* Settings View (Mods Tab) +* * */ + +/* Selected server content container */ +#settingsSelServContainer { + background: rgba(0, 0, 0, 0.25); + width: 75%; + border-radius: 3px; + display: flex; + justify-content: space-between; + margin: 15px 0px; +} + +/* Div which will be populated with the selected server's information. */ +#settingsSelServContent { + display: flex; + align-items: center; + justify-content: flex-start; + padding: 5px 0px; +} + +/* Wrapper container for the switch server button. */ +#settingsSwitchServerContainer { + display: flex; + align-items: center; + padding: 15px; +} + +/* Button to switch server configurations on the mods tab. */ +#settingsSwitchServerButton { + opacity: 0; + border: 1px solid rgb(255, 255, 255); + color: rgb(255, 255, 255); + background: none; + font-size: 12px; + border-radius: 3px; + font-family: 'Avenir Medium'; + transition: 0.25s ease; + cursor: pointer; + outline: none; +} +#settingsSwitchServerButton:hover, +#settingsSwitchServerButton:focus { + box-shadow: 0px 0px 20px rgb(255, 255, 255); + background: rgba(255, 255, 255, 0.25); +} +#settingsSwitchServerButton:active { + box-shadow: 0px 0px 20px rgb(187, 187, 187); + background: rgba(187, 187, 187, 0.25); + border: 1px solid rgb(187, 187, 187); + color: rgb(187, 187, 187); +} +#settingsSelServContainer:hover #settingsSwitchServerButton { + opacity: 1; +} + +/* Main content container for the mod elements. */ +#settingsModsContainer { + width: 75%; +} + +/* Mod sub-container header text. */ +.settingsModsHeader { + padding-bottom: 10px; + border-bottom: 1px solid rgba(255, 255, 255, 0.5); + margin-bottom: 10px; +} + +/* Mod elements sub-containers. */ +#settingsReqModsContainer, +#settingsOptModsContainer, +#settingsDropinModsContainer { + padding-bottom: 25px; +} + +/* Main content containers for mod elements. */ +#settingsReqModsContent, +#settingsOptModsContent, +#settingsDropinModsContent { + font-size: 12px; + background: rgba(0, 0, 0, 0.25); + border-radius: 3px; + color: white; +} + +/* Mod elements. */ +.settingsMod, +.settingsDropinMod { + padding: 10px; +} +.settingsSubMod { + padding: 10px 0px 10px 15px; + margin-left: 20px; + border-left: 1px solid rgba(255, 255, 255, 0.5); +} + +/* Main content container for mod element information. */ +.settingsModContent { + display: flex; + align-items: center; + justify-content: space-between; + transition: opacity 0.25s ease; +} + +/* Wrapper container for the left side of a mod element. */ +.settingsModMainWrapper { + display: flex; + align-items: center; +} + +/* Mod enabled/disabled status. */ +.settingsModStatus { + width: 7px; + height: 7px; + border-radius: 50%; + background-color: #c32625; + margin-right: 15px; + transition: 0.25s ease; +} + +/* Mod details container. */ +.settingsModDetails { + display: flex; + flex-direction: column; +} + +/* The version of the mod. */ +.settingsModVersion { + color: grey; + font-size: 10px; +} + +/* Disabled toggleswitch for required mods. */ +.toggleSwitch[reqmod] { + filter: grayscale(49%) brightness(60%); + pointer-events: none; +} + +/* Set the status color of an enabled mod. */ +.settingsBaseMod[enabled] > .settingsModContent > .settingsModMainWrapper > .settingsModStatus { + background-color: rgb(165, 195, 37); +} + +/* Add opacity to submods of a disabled mod. */ +.settingsBaseMod:not([enabled]) > .settingsSubModContainer .settingsModContent { + opacity: 0.5; +} + +/* Curve the left border for submods. */ +.settingsSubModContainer > .settingsSubMod:first-child { + border-top-left-radius: 10px; +} +.settingsSubModContainer > .settingsSubMod:last-child { + border-bottom-left-radius: 10px; +} +.settingsSubModContainer > .settingsSubMod:only-child { + border-top-left-radius: 10px; + border-bottom-left-radius: 10px; +} + +/* Wrapper container for all submods. */ +.settingsSubModContainer { + margin-top: 10px; +} + +/* Button to open the mods folder for drop-in mods. */ +#settingsDropinFileSystemButton { + background: rgba(0, 0, 0, 0.25); + border: 1px solid rgba(126, 126, 126, 0.57); + border-radius: 3px; + height: 50px; + width: 100%; + text-align: left; + padding: 0px 50px; + cursor: pointer; + outline: none; + transition: 0.25s ease; + margin-bottom: 10px; +} +#settingsDropinFileSystemButton:hover, +#settingsDropinFileSystemButton:focus, +#settingsDropinFileSystemButton[drag] { + background: rgba(54, 54, 54, 0.25); + text-shadow: 0px 0px 20px white; +} +/* Refresh instructions on the file system button. */ +#settingsDropinRefreshNote { + font-size: 10px; + pointer-events: none; +} + +/* Button to remove drop-in mods. */ +.settingsDropinRemoveButton { + background: none; + border: none; + font-size: 10px; + text-align: left; + padding: 0px; + color: #c32625; + font-weight: bold; + cursor: pointer; + outline: none; + transition: 0.25s ease; +} +.settingsDropinRemoveButton:hover, +.settingsDropinRemoveButton:focus { + text-shadow: 0px 0px 20px #c32625, 0px 0px 20px #c32625, 0px 0px 20px #c32625; +} +.settingsDropinRemoveButton:active { + color: #9b1f1f; + text-shadow: 0px 0px 20px #9b1f1f, 0px 0px 20px #9b1f1f, 0px 0px 20px #9b1f1f; +} + +/* Shaderpack settings description. */ +#settingsShaderpackDesc { + font-size: 10px; + margin: 10px 0px; + color: lightgrey; + font-weight: bold; + width: 89%; +} + +/* Wrapper container. */ +#settingsShaderpackWrapper { + display: flex; +} + +/* Button to add shaderpacks. */ +#settingsShaderpackButton { + background: rgba(0, 0, 0, 0.25); + border: 1px solid rgba(126, 126, 126, 0.57); + border-radius: 3px; + cursor: pointer; + outline: none; + transition: 0.25s ease; + font-size: 14px; + padding: 6px 11px; + margin-right: 5px; +} +#settingsShaderpackButton:hover, +#settingsShaderpackButton:focus, +#settingsShaderpackButton[drag] { + background: rgba(54, 54, 54, 0.25); + text-shadow: 0px 0px 20px white; +} + +/* Main select container. */ +.settingsSelectContainer { + position: relative; + width: 50%; +} + +/* Div which displays the selected option. */ +.settingsSelectSelected { + border-radius: 3px; + border-width: 1px; + font-size: 14px; + padding: 6px 16px; +} + +/* Style the arrow inside the select element. */ +.settingsSelectSelected:after { + position: absolute; + content: ""; + top: calc(50% - 3px); + right: 10px; + width: 0; + height: 0; + border: 6px solid transparent; + border-color: rgba(126, 126, 126, 0.57) transparent transparent transparent; +} + +/* Point the arrow upwards when the select box is open (active). */ +.settingsSelectSelected.select-arrow-active:after { + border-color: transparent transparent rgba(126, 126, 126, 0.57) transparent; + top: 7px; +} +.settingsSelectSelected.select-arrow-active { + border-radius: 3px 3px 0px 0px; +} + +/* Options content container. */ +.settingsSelectOptions { + position: absolute; + top: 100%; + left: 0; + right: 0; + z-index: 99; + max-height: 300%; + overflow-y: scroll; + border: 1px solid rgba(126, 126, 126, 0.57); + border-top: none; + border-radius: 0px 0px 3px 3px; +} +/* Hide the items when the select box is closed. */ +.settingsSelectOptions[hidden] { + display: none; +} +.settingsSelectOptions::-webkit-scrollbar { + width: 2px; +} +.settingsSelectOptions::-webkit-scrollbar-track { + display: none; +} +.settingsSelectOptions::-webkit-scrollbar-thumb { + border-radius: 10px; + box-shadow: inset 0 0 10px rgba(255, 255, 255, 0.50); +} + +/* Shared styles between options and selection div. */ +.settingsSelectOptions div, +.settingsSelectSelected { + background: rgba(0, 0, 0, 0.25); + border-style: solid; + border-color: rgba(126, 126, 126, 0.57); + color: #ffffff; + cursor: pointer; +} +.settingsSelectOptions div { + border-width: 0px 0px 1px 0px; + font-size: 12px; + padding: 4px 16px; +} +.settingsSelectOptions div:last-child { + border-bottom: none; +} + +/* Hover + selected styles. */ +.settingsSelectOptions div:hover, .settingsSelectOptions div[selected] { + background-color: rgba(255, 255, 255, 0.25) !important; +} + +/* * * +* Settings View (Java Tab) +* * */ + +/* Style links on the Java tab. */ +#settingsTabJava a, +.settingsChangelogText a { + color: rgba(202, 202, 202, 0.75); + transition: 0.25s ease; + outline: none; +} +#settingsTabJava a:hover, +#settingsTabJava a:focus, +.settingsChangelogText a:hover, +.settingsChangelogText a:focus { + color: rgba(255, 255, 255, 0.75); +} +#settingsTabJava a:active, +.settingsChangelogText a:active { + color: rgba(165, 165, 165, 0.75); +} + +/* Main container for memory management. */ +#settingsMemoryContainer { + width: 75%; + display: flex; + flex-direction: column; + border-bottom: 1px solid rgba(255, 255, 255, 0.50); + margin-bottom: 20px; +} + +/* Memory management title. */ +#settingsMemoryTitle { + margin-bottom: 10px; + padding-bottom: 5px; + border-bottom: 1px solid rgba(255, 255, 255, 0.5); +} + +/* Memory management content. */ +#settingsMemoryContent { + display: flex; + justify-content: space-between; + width: 100%; +} +#settingsMemoryContentLeft { + width: 69%; +} +#settingsMemoryContentRight { + display: flex; + align-items: center; + margin-right: 10%; +} + +/* Header for memory sliders. */ +.settingsMemoryHeader { + font-size: 14px; +} + +/* Wrapper container for a memory slider and label. */ +.settingsMemoryActionContainer { + display: flex; + align-items: center; + justify-content: space-between; +} + +/* Label which displays a memory slider's value. */ +.settingsMemoryLabel { + font-size: 14px; + margin-right: 2%; +} + +/* Range sliders for min and max memory settings. */ +#settingsMaxRAMRange, +#settingsMinRAMRange { + width: 85%; +} + +/* Memory status elements. */ +#settingsMemoryStatus { + display: flex; + flex-direction: column; +} +#settingsMemoryStatus > .settingsMemoryStatusContainer:not(:last-child){ + margin-bottom: 50%; +} +.settingsMemoryStatusContainer { + display: flex; + flex-direction: column; + align-items: center; +} +.settingsMemoryStatusTitle { + font-size: 12px; + color: grey; + font-weight: bold; +} +.settingsMemoryStatusValue { + color: lightgrey; + font-size: 16px; +} + +/* Description for memory management. */ +#settingsMemoryDesc { + font-size: 10px; + margin: 20px 0px; + color: lightgrey; + font-weight: bold; +} + +/* Status text which displays details on the selected executable. */ +#settingsJavaExecDetails { + font-weight: bold; + color: grey; + font-size: 12px; +} + +/* Main container for the JVM options setting. */ +#settingsJVMOptsContainer { + width: 75%; +} + +/* JVM options title. */ +#settingsJVMOptsTitle { + margin-bottom: 10px; +} + +/* Wrapper container for the actionable elements. */ +#settingsJVMOptsContent { + display: flex; + width: 90%; +} + +/* Text field to input the JVM options. */ +#settingsJVMOptsVal { + border-radius: 0px 3px 3px 0px !important; + width: 100%; + padding: 5px 10px; + font-size: 12px; +} +#settingsJVMOptsContent:focus-within > .settingsJavaIcon { + background: rgba(126, 126, 126, 0.87); +} + +/* Description for the JVM options setting. */ +#settingsJVMOptsDesc { + font-size: 10px; + margin: 20px 0px; + color: lightgrey; + font-weight: bold; + width: 89%; +} + +/* * * +* Settings View (Launcher Tab) +* * */ + +/* Tailored style for the data directory header. */ +#settingsDataDirTitle { + margin-bottom: 10px; +} + +/* * * +* Settings View (About Tab) +* * */ + +/* Main about content container. */ +#settingsAboutCurrentContainer { + display: flex; + flex-direction: column; + background: rgba(0, 0, 0, 0.25); + border: 1px solid rgba(126, 126, 126, 0.57); + border-radius: 3px; + width: 75%; + margin-bottom: 20px; +} + +/* About content. */ +#settingsAboutCurrentContent { + display: flex; + flex-direction: column; + padding: 15px; +} + +/* About header elements. */ +#settingsAboutCurrentHeadline { + display: flex; + align-items: center; + padding-bottom: 5px; + border-bottom: 1px solid rgba(126, 126, 126, 0.57); +} +#settingsAboutLogo { + width: 30px; + height: 30px; + padding: 5px; +} +#settingsAboutTitle { + font-size: 23px; + padding-left: 10px; +} + +/* Current version container. */ +#settingsAboutCurrentVersion { + display: flex; + align-items: center; + padding-top: 10px; +} + +/* Checkmark next to the version information. */ +#settingsAboutCurrentVersionCheck { + border-radius: 50%; + background: #23aa23; + text-align: center; + font-weight: bold; + margin: 11px 12px; + color: white; + height: 15px; + width: 15px; + font-size: 12px; + line-height: 17px; +} + +/* Current version details container. */ +#settingsAboutCurrentVersionDetails { + margin-left: 10px; +} + +/* Release type text. */ +#settingsAboutCurrentVersionTitle { + font-size: 12px; + font-family: 'Avenir Medium'; + color: #23aa23; + font-weight: bold; +} + +/* Current version text. */ +#settingsAboutCurrentVersionLine { + font-size: 10px; + color: grey; + font-weight: bold; +} + +/* About information links. */ +#settingsAboutButtons { + display: flex; + padding: 0px 15px; + margin-bottom: 5px; +} +.settingsAboutButton { + background: none; + border: none; + font-size: 10px; + color: grey; + padding: 0px 5px; + transition: 0.25s ease; + outline: none; + text-decoration: none; +} +.settingsAboutButton:hover, +.settingsAboutButton:focus { + color: rgb(165, 165, 165); +} +.settingsAboutButton:active { + color: rgba(124, 124, 124, 0.75); +} + +/* Main changelog container. */ +.settingsChangelogContainer { + display: flex; + flex-direction: column; + background: rgba(0, 0, 0, 0.25); + border: 1px solid rgba(126, 126, 126, 0.57); + border-radius: 3px; + width: 75%; + margin-bottom: 20px; +} + +/* Changelog content container. */ +.settingsChangelogContent { + display: flex; + flex-direction: column; + padding: 15px; +} + +/* Changelog header container. */ +.settingsChangelogHeadline { + padding-bottom: 10px; + margin-bottom: 10px; + border-bottom: 1px solid rgba(126, 126, 126, 0.57); +} +/* Changelog header label. */ +.settingsChangelogLabel { + font-size: 12px; + color: grey; + font-weight: bold; +} + +/* Changelog text content container. */ +.settingsChangelogText { + font-size: 12px; +} + +/* Styles for the changelog elements. */ +.settingsChangelogText p { + margin-bottom: 16px; + line-height: 1.5; +} +.settingsChangelogText blockquote { + border-left: 0.25em solid rgba(126, 126, 126, 0.95); + margin: 0px; + padding: 0 0 0 1em; + color: rgba(255, 255, 255, 0.85); +} +.settingsChangelogText code { + padding: 0.1em 0.4em; + font-size: 85%; + background-color: rgba(255, 255, 255, 0.25); + color: white; + border-radius: 3px; + font-family: 'Avenir Book'; +} +.settingsChangelogText li+li { + margin-top: .25em; +} +.settingsChangelogText a.commit-link { + font-weight: 400; + color: #ffffff; + text-decoration: none; +} +.settingsChangelogText a.commit-link:hover { + text-decoration: underline !important; + text-decoration-color: black; +} +.settingsChangelogText tt { + padding: 0.1em 0.4em; + font-size: 86%; + background-color: white; + border-radius: 3px; + color: black; + font-weight: bold; +} +.settingsChangelogText a.commit-link:hover tt { + text-decoration: underline; + text-decoration-color: black; +} +.settingsChangelogText .highlight { + background: rgba(0, 0, 0, 0.30); + user-select: initial; + padding: 5px 10px; +} +.settingsChangelogText .highlight pre { + margin: 0px; +} + +/* Container for the changelog button. */ +.settingsChangelogActions { + padding: 0px 15px 5px 15px; +} + +/* Open changelog on GitHub. */ +.settingsChangelogButton { + padding: 0px; +} + +/* * * +* Settings View (Updates Tab) +* * */ + +/* Main about content container. */ +#settingsUpdateStatusContainer { + display: flex; + flex-direction: column; + background: rgba(0, 0, 0, 0.25); + border: 1px solid rgba(126, 126, 126, 0.57); + border-radius: 3px; + width: 75%; + margin-bottom: 20px; +} + +/* Update content. */ +#settingsUpdateStatusContent { + display: flex; + flex-direction: column; + padding: 15px; +} + +/* Update header elements. */ +#settingsUpdateStatusHeadline { + display: flex; + align-items: center; + padding-bottom: 5px; + border-bottom: 1px solid rgba(126, 126, 126, 0.57); +} +#settingsUpdateTitle { + font-size: 16px; + padding-left: 10px; + font-weight: bold; +} + +/* Update version container. */ +#settingsUpdateVersion { + display: flex; + align-items: center; + padding: 10px 0px; + border-bottom: 1px solid rgba(126, 126, 126, 0.57); +} + +/* Checkmark next to the version information. */ +#settingsUpdateVersionCheck { + border-radius: 50%; + background: #23aa23; + text-align: center; + font-weight: bold; + margin: 11px 12px; + color: white; + height: 15px; + width: 15px; + font-size: 12px; + line-height: 17px; +} + +/* Update version details container. */ +#settingsUpdateVersionDetails { + margin-left: 10px; +} + +/* Release type text. */ +#settingsUpdateVersionTitle { + font-size: 12px; + font-family: 'Avenir Medium'; + color: #23aa23; + font-weight: bold; +} + +/* Current version text. */ +#settingsUpdateVersionLine { + font-size: 10px; + color: grey; + font-weight: bold; +} + +/* Update action container. */ +#settingsUpdateActionContainer { + padding-top: 10px; + font-size: 14px; + font-weight: bold; +} + +/* Update action button styles. */ +#settingsUpdateActionButton { + display: flex; + flex-direction: column; + padding-left: 10px; + background: none; + border: none; + font-size: 14px; + font-weight: bold; + cursor: pointer; + outline: none; + text-align: left; + transition: 0.25s ease; +} +#settingsUpdateActionButton:hover, +#settingsUpdateActionButton:focus { + text-shadow: 0px 0px 20px white, 0px 0px 20px white, 0px 0px 20px white; +} +#settingsUpdateActionButton:active { + text-shadow: 0px 0px 20px #c7c7c7, 0px 0px 20px #c7c7c7, 0px 0px 20px #c7c7c7; + color: #c7c7c7; +} +#settingsUpdateActionButton:disabled { + pointer-events: none; +} + +/******************************************************************************* + * * + * Landing View (Structural Styles) * + * * + ******************************************************************************/ + +/* Main content container. */ +#landingContainer { + height: 100%; + position: relative; + transition: background 2s ease; + overflow-y: hidden; +} + +/* Upper content container. */ +#landingContainer > #upper { + position: relative; + transition: top 2s ease; + top: 0px; + height: 77%; + display: flex; +} +#landingContainer > #upper > #left { + display: inline-flex; + width: 15%; + height: 100%; + justify-content: flex-end; +} +#landingContainer > #upper > #content { + display: inline-flex; + width: 70%; + height: 100%; +} +#landingContainer > #upper > #right { + display: inline-flex; + width: 15%; + height: 100%; +} + +/* Lower content container. */ +#landingContainer > #lower { + height: 23%; + display: flex; + background: linear-gradient(to top, rgba(0, 0, 0, 0.75), rgba(0, 0, 0, 0)); +} +#landingContainer > #lower > #left { + position: relative; + transition: top 2s ease; + top: 0px; + height: 100%; + width: 33%; + display: inline-flex; + justify-content: center; +} +#landingContainer > #lower > #left #content { + position: relative; + top: 25px; + display: inline-flex; + line-height: 24px; + left: 50px; +} +#landingContainer > #lower > #center { + position: relative; + transition: top 2s ease; + top: 0px; + height: 100%; + width: 34%; + display: inline-flex; + justify-content: center; +} +#landingContainer > #lower > #center #content { + position: relative; + z-index: 500; + transition: top 2s ease; + top: 10px; +} +#landingContainer > #lower > #right { + position: relative; + transition: top 2s ease; + top: 0px; + height: 100%; + width: 33%; + display: inline-flex; +} + +/******************************************************************************* + * * + * Landing View (News Styles) * + * * + ******************************************************************************/ + +/* Main container. */ +#newsContainer { + position: absolute; + top: 100%; + height: 100%; + width: 100%; + transition: top 2s ease; + display: flex; + align-items: flex-end; + justify-content: center; +} + +/* News content container. */ +#newsContent { + height: 82vh; + width: 100%; + display: flex; + -webkit-user-select: initial; + position: relative; +} + +/* Drop shadow displayed when content is scrolled out of view. */ +#newsContent:before { + content: ''; + background: linear-gradient(rgba(0, 0, 0, 0.25), transparent); + width: 100%; + height: 5px; + position: absolute; + opacity: 0; + transition: opacity 0.25s ease; +} +#newsContent[scrolled]:before { + opacity: 1; +} + +/* News article status container (left). */ +#newsStatusContainer { + width: calc(30% - 60px); + height: calc(100% - 30px); + padding: 15px 15px 15px 45px; + display: flex; + flex-direction: column; + justify-content: space-between; + position: relative; +} + +/* News status content. */ +#newsStatusContent { + display: flex; + flex-direction: column; + align-items: flex-end; +} + +/* News title wrapper. */ +#newsTitleContainer { + display: flex; + max-width: 90%; +} + +/* News article title styles. */ +#newsArticleTitle { + font-size: 18px; + font-weight: bold; + font-family: 'Avenir Medium'; + color: white; + text-decoration: none; + transition: 0.25s ease; + outline: none; + text-align: right; +} +#newsArticleTitle:hover, +#newsArticleTitle:focus { + text-shadow: 0px 0px 20px white; +} +#newsArticleTitle:active { + color: #c7c7c7; + text-shadow: 0px 0px 20px #c7c7c7; +} + +/* News meta container. */ +#newsMetaContainer { + display: flex; + flex-direction: column; +} + +/* Date and author wrappers. */ +#newsArticleDateWrapper, +#newsArticleAuthorWrapper { + display: flex; + justify-content: flex-end; +} + +/* Date and author shared styles. */ +#newsArticleDate, +#newsArticleAuthor { + display: inline-block; + font-size: 10px; + padding: 0px 5px; + font-weight: bold; + border-radius: 2px; +} + +/* Date styles. */ +#newsArticleDate { + background: white; + color: black; + margin-top: 5px; +} + +/* Author styles. */ +#newsArticleAuthor { + background: #a02d2a; +} + +/* News article comments styles. */ +#newsArticleComments { + margin-top: 5px; + display: inline-block; + font-size: 10px; + color: #ffffff; + text-decoration: none; + transition: 0.25s ease; + outline: none; + text-align: right; +} +#newsArticleComments:focus, +#newsArticleComments:hover { + color: #e0e0e0; +} +#newsArticleComments:active { + color: #c7c7c7; +} + +/* Article content container (right). */ +#newsArticleContainer { + width: calc(100% - 25px); + height: 100%; + margin: 0px 0px 0px 25px; +} + +/* Article content styles. */ +#newsArticleContentScrollable { + font-size: 12px; + overflow-y: scroll; + height: 100%; + padding: 0px 15px 0px 15px; +} +#newsArticleContentScrollable img, +#newsArticleContentScrollable iframe { + max-width: 95%; + display: block; + margin: 0 auto; +} +#newsArticleContentScrollable a { + color: rgba(202, 202, 202, 0.75); + transition: 0.25s ease; + outline: none; +} +#newsArticleContentScrollable a:hover, +#newsArticleContentScrollable a:focus { + color: rgba(255, 255, 255, 0.75); +} +#newsArticleContentScrollable a:active { + color: rgba(165, 165, 165, 0.75); +} +#newsArticleContentScrollable::-webkit-scrollbar { + width: 2px; +} +#newsArticleContentScrollable::-webkit-scrollbar-track { + display: none; +} +#newsArticleContentScrollable::-webkit-scrollbar-thumb { + border-radius: 10px; + box-shadow: inset 0 0 10px rgba(255, 255, 255, 0.50); +} +.bbCodeSpoilerButton { + background: none; + border: none; + outline: none; + cursor: pointer; + font-size: 16px; + transition: 0.25s ease; + width: 100%; + border-bottom: 1px solid white; + padding-bottom: 15px; +} +.bbCodeSpoilerButton:hover, +.bbCodeSpoilerButton:focus { + text-shadow: 0px 0px 20px #ffffff, 0px 0px 20px #ffffff, 0px 0px 20px #ffffff; +} +.bbCodeSpoilerButton:active { + color: #c7c7c7; + text-shadow: 0px 0px 20px #c7c7c7, 0px 0px 20px #c7c7c7, 0px 0px 20px #c7c7c7; +} +.bbCodeSpoilerText { + display: none; + padding: 15px 0px; + border-bottom: 1px solid white; +} + + +#newsArticleContentWrapper { + width: 80%; +} + +.newsArticleSpacerTop { + height: 15px; +} + +/* Div to add spacing at the end of a news article. */ +.newsArticleSpacerBot { + height: 30px; +} + +/* News navigation container. */ +#newsNavigationContainer { + display: flex; + justify-content: center; + align-items: center; + margin-bottom: 10px; + -webkit-user-select: none; + position: absolute; + bottom: 15px; + right: 0px; +} + +/* Navigation status span. */ +#newsNavigationStatus { + font-size: 12px; + margin: 0px 15px; +} + +/* Left and right navigation button styles. */ +#newsNavigateLeft, +#newsNavigateRight { + background: none; + border: none; + outline: none; + height: 20px; + cursor: pointer; +} +#newsNavigateLeft:hover #newsNavigationLeftSVG, +#newsNavigateLeft:focus #newsNavigationLeftSVG, +#newsNavigateRight:hover #newsNavigationRightSVG, +#newsNavigateRight:focus #newsNavigationRightSVG { + -webkit-filter: drop-shadow(0px 0px 2px #fff); +} +#newsNavigateLeft:active #newsNavigationLeftSVG .arrowLine, +#newsNavigateRight:active #newsNavigationRightSVG .arrowLine { + stroke: #c7c7c7; +} +#newsNavigateLeft:active #newsNavigationLeftSVG, +#newsNavigateRight:active #newsNavigationRightSVG { + -webkit-filter: drop-shadow(0px 0px 2px #c7c7c7); +} +#newsNavigateLeft:disabled #newsNavigationLeftSVG .arrowLine, +#newsNavigateRight:disabled #newsNavigationRightSVG .arrowLine { + stroke: rgba(255, 255, 255, 0.75); +} +#newsNavigationLeftSVG { + transform: rotate(-90deg); + width: 15px; +} +#newsNavigationRightSVG { + transform: rotate(90deg); + width: 15px; +} + +/* News error (message) container. */ +#newsErrorContainer { + height: 100%; + display: flex; + align-items: center; + flex-direction: column; + justify-content: center; +} +#newsErrorFailed { + display: flex; + align-items: center; + flex-direction: column; + justify-content: center; +} + +/* News error content (message). */ +.newsErrorContent { + font-size: 20px; +} +#newsErrorLoading { + display: flex; + width: 168.92px; +} +#nELoadSpan { + white-space: pre; +} +/* News error retry button styles. */ +#newsErrorRetry { + font-size: 12px; + font-weight: bold; + cursor: pointer; + background: none; + border: none; + outline: none; + transition: 0.25s ease; +} +#newsErrorRetry:focus, +#newsErrorRetry:hover { + text-shadow: 0px 0px 20px white; +} +#newsErrorRetry:active { + color: #c7c7c7; + text-shadow: 0px 0px 20px #c7c7c7; +} + +/******************************************************************************* + * * + * Landing View (Top Styles) * + * * + ******************************************************************************/ + +/* * * +* Landing View (Top Styles) | Left Content +* * */ + +/* Logo image. */ +#image_seal { + height: 70px; + width: auto; + position: relative; + border: 2px solid white; + box-sizing: border-box; + border-radius: 50%; +} + +/* Logo container styles. */ +#image_seal_container { + position: relative; + height: 70px; + width: 70px; + border-radius: 50%; + margin-top: 50px; +} + +/* Logo container styles w/ update. */ +#image_seal_container[update]{ + cursor: pointer +} +#image_seal_container[update]:before, +#image_seal_container[update]:after { + cursor: pointer; + position: absolute; + content: ''; + height: 100%; + width: 100%; + top: 0%; + left: 0%; + border-radius: 50%; + box-shadow: 0 0 15px #43c628; + animation: glow-grow 4s ease-out infinite; + background: rgba(0, 0, 0, 0.15); +} +#image_seal_container[update]:before { + animation-delay: 2s; +} + +/* Update available tooltip styles. */ +#updateAvailableTooltip { + cursor: pointer; + visibility: hidden; + opacity: 0; + width: 100px; + height: 15px; + background-color: rgb(0, 0, 0); + color: #fff; + text-align: center; + border-radius: 4px; + padding: 2px; + position: absolute; + z-index: 1; + top: 115%; + left: -17.5px; + font-family: 'Avenir Medium'; + font-size: 12px; + transition: visibility 0s linear 0.25s, opacity 0.25s ease; +} +#updateAvailableTooltip::after { + content: " "; + position: absolute; + left: 50%; + bottom: 100%; + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: transparent transparent rgb(0, 0, 0) transparent; +} +#image_seal_container[update]:hover #updateAvailableTooltip { + visibility: visible; + opacity: 1; + transition-delay: 0s; +} + +/* Update available animation. */ +@keyframes glow-grow { + 0% { + opacity: 0; + transform: scale(1); + } + 80% { + opacity: 1; + } + 100% { + transform: scale(1.5); + opacity: 0; + } +} + +/* * * +* Landing View (Bottom Styles) | Right Content +* * */ + +/* Wrapper container for top, right content. */ +#rightContainer { + display: flex; + flex-direction: column; + position: relative; + top: 50px; + align-items: flex-start; + height: calc(100% - 50px); +} + +/* Right hand user content container. */ +#user_content { + display: flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + position: relative; +} + +/* User profile avatar container. */ +#avatarContainer { + border-radius: 50%; + border: 2px solid #cad7e1; + box-sizing: border-box; + background: rgba(1, 2, 1, 0.5); + height: 70px; + width: 70px; + box-shadow: 0px 0px 10px 0px rgb(0, 0, 0); + overflow: hidden; + position: relative; + background-position: center; + background-repeat: no-repeat; + background-size: contain; +} + +/* Avatar edit overlay. */ +#avatarOverlay { + opacity: 0; + position: absolute; + z-index: 1; + display: flex; + justify-content: center; + align-items: center; + transition: 0.25s ease; + font-weight: bold; + letter-spacing: 2px; + background-color: rgba(0, 0, 0, 0.35); + -webkit-user-select: none; + border: none; + cursor: pointer; + width: 100%; + height: 100%; + border-radius: 50%; +} +#avatarOverlay:hover, +#avatarOverlay:focus { + opacity: 1; +} +#avatarOverlay:active { + background-color: rgba(0, 0, 0, 0.45); +} + +/* User profile name text. */ +#user_text { + font-size: 12px; + min-width: 135px; + font-weight: 900; + letter-spacing: 1px; + text-shadow: 0px 0px 20px black; + position: absolute; + right: 95px; + text-align: right; + -webkit-user-select: initial; +} + +/* Social media icon content container. */ +#mediaContent { + position: relative; + display: flex; + flex-direction: column; + margin-top: 25px; + height: calc(100% - 95px); + width: 70px; + align-items: center; +} + +/* Social Media Icon division containers. */ +#internalMedia, #externalMedia { + display: flex; + flex-direction: column; +} + +/* Container object which wraps an icon to ensure fluid transitions. */ +.mediaContainer { + display: flex; + justify-content: center; + align-items: center; + height: 27px; +} + +/* Divider bar between the external and internal icons. */ +.mediaDivider { + height: 1px; + width: 14px; + background: rgb(255, 255, 255); + margin: 10px 0px; +} + +/* Social media icon shared styles. */ +.mediaSVG { + fill: #ffffff; + height: 12px; + transition: 0.25s ease; + cursor: pointer; + height: 12px; + width: 25px; +} +.mediaSVG:hover, +.mediaURL:focus .mediaSVG, +.mediaSVG:active { + height: 20px; +} + +/* Social media URL shared styles. */ +.mediaURL { + outline: none; +} + +/* Internal media button shared styles. */ +.mediaButton { + background: none; + border: none; + padding: 0px; + display: flex; + align-items: center; + outline: none; +} + +#settingsMediaContainer { + position: relative; +} + +/* Settings icon colors. */ +#settingsSVG { + stroke: #ffffff; + height: 15px; +} +.mediaButton:hover #settingsSVG, +.mediaButton:focus #settingsSVG, +.mediaButton:active #settingsSVG { + height: 23px; +} + +/* Settings tooltip styles. */ +#settingsTooltip { + visibility: hidden; + opacity: 0; + width: 75px; + height: 20px; + background-color: rgba(0, 0, 0, 0.75); + text-align: center; + border-radius: 4px; + position: absolute; + z-index: 1; + right: 130%; + font-size: 12px; + line-height: 20px; + transition: visibility 0s linear 0.25s, opacity 0.25s ease; +} +#settingsTooltip::after { + content: " "; + position: absolute; + top: 50%; + left: 100%; + margin-top: -5px; + border-width: 5px; + border-style: solid; + border-color: transparent transparent transparent rgba(0, 0, 0, 0.75); +} +.mediaButton:hover #settingsTooltip, +.mediaButton:focus #settingsTooltip, +.mediaButton:active #settingsTooltip { + visibility: visible; + opacity: 1; + transition-delay:0s; +} + +/* Twitter icon colors. */ +#twitterSVG:hover, +#twitterURL:focus #twitterSVG { + fill: #1da1f2; +} +#twitterSVG:active { + fill: #1b8dd4; +} + +/* Instagram icon colors. */ +#instagramSVG:hover, +#instagramURL:focus #instagramSVG { + fill: url('#instaFill') + /*fill: radial-gradient(circle at 30% 107%, #fdf497 0%, #fdf497 5%, #fd5949 45%, #d6249f 60%, #285AEB 90%); */ +} +#instagramSVG:active { + fill: url('#instaFill') +} + +/* Youtube icon colors. */ +#youtubeSVG:hover, +#youtubeURL:focus #youtubeSVG { + fill: #f00; +} +#youtubeSVG:active { + fill: #ea0202; +} + +/* Discord icon colors. */ +#discordSVG:hover, +#discordURL:focus #discordSVG { + fill: #7288d9; +} +#discordSVG:active { + fill: #657ac4; +} + +/******************************************************************************* + * * + * Landing View (Bottom Styles) * + * * + ******************************************************************************/ + +/* Style for a general label on the bottom of the landing view. */ +.bot_label { + font-size: 9px; + letter-spacing: 1px; + font-weight: bold; + text-shadow: 0px 0px 0px #bebcbb; +} + +/* Divider used on the bottom of the landing view. */ +.bot_divider { + height: 25px; + width: 2px; + background: rgba(107, 105, 105, 0.7); + margin-left: 20px; + margin-right: 20px; +} + +/* * * +* Landing View (Bottom Styles) | Left Content +* * */ + +/* Maintains maximum width on the status bar. */ +#server_status_wrapper { + display: inline-flex; + width: 75px; +} + +/* Span which displays the player count of the selected server. */ +#player_count { + color: #949494; + font-size: 8px; + font-weight: 900; + text-shadow: 0px 0px 20px #949494; + margin-left: 10px; +} + +/* Wrapper container for the mojang status bar. */ +#mojangStatusWrapper { + position: relative; + display: flex; + cursor: pointer; +} + +/* Icon which displays the status of the mojang services. */ +#mojang_status_icon { + font-size: 30px; + color: #848484; + margin-left: 15px; + font-family: 'sans-serif'; +} + +/* Tooltip which displays more details about the mojang statuses. */ +#mojangStatusTooltip { + position: absolute; + visibility: hidden; + opacity: 0; + width: 145px; + min-height: 150px; + background-color: rgba(0, 0, 0, 0.75); + color: #fff; + border-radius: 4px; + padding: 5px 10px; + z-index: 1; + font-family: 'Avenir Medium'; + font-size: 12px; + transition: visibility 0s linear 0.25s, opacity 0.25s ease; + bottom: calc(100% + 15px); + transform: translateX(-50%); + margin-left: 50%; + box-shadow: 0px 0px 20px rgb(0, 0, 0); + cursor: default; +} +#mojangStatusTooltip:after { + content: " "; + position: absolute; + left: 50%; + top: 100%; + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: rgba(0, 0, 0, 0.75) transparent transparent transparent; +} +#mojangStatusWrapper:hover #mojangStatusTooltip { + visibility: visible; + opacity: 1; + transition-delay: 0s; +} + +/* Tooltip title for the mojang statuses. */ +#mojangStatusTooltipTitle { + width: 100%; + text-align: center; + margin-bottom: 5px; + letter-spacing: 1px; +} + +/* Wrapper container for the non essential services title. */ +#mojangStatusNEContainer { + display: flex; + align-items: center; + margin: 10px 0px; +} + +/* White bar which surrounds the non essential service title. */ +.mojangStatusNEBar { + height: 1px; + width: 100%; + background: white; +} + +/* Non essential service title text. */ +#mojangStatusNETitle { + font-size: 10px; + padding: 0px 3px; + text-align: center; + letter-spacing: 1px; +} + +/* Wrapper container for mojang service information. */ +.mojangStatusContainer { + display: flex; +} + +/* Displays the name of the mojang service. */ +.mojangStatusName { + width: 100%; + font-size: 10px; + letter-spacing: 1px; + line-height: 12px; + padding: 6px 0px; +} + +/* Displays the status of the mojang service. */ +.mojangStatusIcon { + margin-right: 10px; + font-size: 18.5px; + color: #848484; +} + +/* * * +* Landing View (Bottom Styles) | Center Content +* * */ + +/* Button which opens the news view. */ +#newsButton { + background: none; + border: none; + cursor: pointer; + outline: none; +} +#newsButton:hover #newsButtonText, +#newsButton:focus #newsButtonText { + text-shadow: 0px 0px 20px #fff, 0px 0px 20px #fff; +} +#newsButton:active { + color: #c7c7c7; + text-shadow: 0px 0px 20px #c7c7c7, 0px 0px 20px #c7c7c7; +} + +#newsButton:hover #newsButtonSVG, +#newsButton:focus #newsButtonSVG { + -webkit-filter: drop-shadow(0px 0px 2px #fff); +} +#newsButton:active #newsButtonSVG .arrowLine { + stroke: #c7c7c7; +} +#newsButton:active #newsButtonSVG { + -webkit-filter: drop-shadow(0px 0px 2px #c7c7c7); +} +#newsButton:disabled #newsButtonSVG .arrowLine { + stroke: rgba(255, 255, 255, 0.75); +} + +/* Icon which indicates there is new news. */ +#newsButtonAlert { + width: 5px; + height: 5px; + position: absolute; + border-radius: 50%; + background: red; + right: -1px; + top: 50%; +} + +/* Arrow image which floats above the news button. */ +#newsButtonSVG { + height: 11px; + margin-left: -2px; + transition: 0.25s ease; +} + +/* Span which contains the news button text. */ +#newsButtonText { + color: white; + font-weight: 900; + letter-spacing: 2px; + text-shadow: 0px 0px 0px #bebcbb; + font-size: 11px; + line-height: 30px; + display: flex; + transition: 0.25s ease; +} + +/* * * +* Landing View (Bottom Styles) | Right Content +* * */ + +/* Main launch content container. */ +#landingContainer > #lower > #right #launch_content { + position: relative; + top: 25px; + display: inline-flex; +} + +/* The launch button. */ +#launch_button { + background: none; + border: none; + cursor: pointer; + font-weight: 900; + letter-spacing: 2px; + text-shadow: 0px 0px 0px #bebcbb; + font-size: 20px; + padding: 0px; + transition: 0.25s ease; + outline: none; +} +#launch_button:hover, +#launch_button:focus { + text-shadow: 0px 0px 20px #fff, 0px 0px 20px #fff; +} +#launch_button:active { + color: #c7c7c7; + text-shadow: 0px 0px 20px #c7c7c7, 0px 0px 20px #c7c7c7; +} +#launch_button:disabled { + color: #c7c7c7; + cursor: default; + pointer-events: none; +} + +/* Launch details main container, hidden until launch processing begins. */ +#launch_details { + position: relative; + top: 25px; + display: none; +} + +/* Left side of launch details container, displays percentage and a divider. */ +#launch_details_left { + display: flex; +} + +/* Span which displays percentage complete. */ +#launch_progress_label { + font-weight: 900; + letter-spacing: 1px; + text-shadow: 0px 0px 0px #bebcbb; + font-size: 20px; + min-width: 53.21px; + max-width: 53.21px; + text-align: right; +} + +/* Right side of launch details container, displays progress bar and details. */ +#launch_details_right { + display: flex; + flex-direction: column; + justify-content: center; +} + +/* Button which opens the server selection view. */ +#server_selection_button { + background: none; + border: none; + outline: none; + cursor: pointer; + line-height: 24px; + padding: 0px; + transition: 0.25s ease; +} +#server_selection_button:hover, +#server_selection_button:focus { + text-shadow: 0px 0px 20px #fff, 0px 0px 20px #fff, 0px 0px 20px #fff; +} +#server_selection_button:active { + color: #c7c7c7; + text-shadow: 0px 0px 20px #c7c7c7, 0px 0px 20px #c7c7c7, 0px 0px 20px #c7c7c7; +} + +/* Progress bar styles. */ +#launch_progress[value] { + height: 3px; + width: 265px; + -webkit-appearance: none; +} +#launch_progress[value]::-webkit-progress-bar { + background-color: transparent; +} +#launch_progress[value]::-webkit-progress-value { + background-color: #fff; +} + +/* Span which displays information about the status of the launch process. */ +#launch_details_text { + font-size: 11px; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + +/******************************************************************************* + * * + * Overlay View (overlay.ejs) * + * * + ******************************************************************************/ + +/* * * +* Overlay View (Main Content) +* * */ + +/* Overlay container, placed over the main div. */ +#overlayContainer { + position: absolute; + z-index: 500; + top: 22px; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: calc(100% - 22px); + background: rgba(0, 0, 0, 0.50); +} + +/* Main overlay content. */ +#overlayContent { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + /*justify-content: space-between;*/ + width: 300px; + /*height: 35%;*/ + box-sizing: border-box; + padding: 15px 0px; + /* background-color: #424242; */ + text-align: center; +} + +/* Main overlay content anchor styles. */ +#overlayContent a, +#overlayDismiss { + color: rgba(202, 202, 202, 0.75); + transition: 0.25s ease; +} +#overlayContent a:hover, +#overlayContent a:focus, +#overlayDismiss:focus { + color: rgba(255, 255, 255, 0.75); +} +#overlayContent a:active, +#overlayDismiss:active { + color: rgba(165, 165, 165, 0.75); +} + +/* Add spacing between overlay content elements. */ +#overlayContent > *:first-child { + margin-top: 0px !important; +} +#overlayContent > *:last-child { + margin-bottom: 0px !important; +} +#overlayContent > * { + margin: 8px 0px; +} + +/* Overlay title styles. */ +#overlayTitle { + font-family: 'Avenir Medium'; + font-size: 20px; + font-weight: bold; + letter-spacing: 1px; + -webkit-user-select: initial; +} + +/* Overlay description styles. */ +#overlayDesc { + font-size: 12px; + font-weight: bold; + -webkit-user-select: initial; +} + +/* Div which contains action buttons. */ +#overlayActionContainer { + display: flex; + flex-direction: column; + justify-content: center; +} + +/* Overlay acknowledge button styles. */ +#overlayAcknowledge { + background: none; + border: 1px solid #ffffff; + color: white; + font-family: 'Avenir Medium'; + font-weight: bold; + border-radius: 2px; + padding: 0px 8.1px; + cursor: pointer; + transition: 0.25s ease; +} +#overlayAcknowledge:hover, +#overlayAcknowledge:focus { + box-shadow: 0px 0px 10px 0px #fff; + outline: none; +} +#overlayAcknowledge:active { + border-color: rgba(255, 255, 255, 0.75); + color: rgba(255, 255, 255, 0.75); +} + +/* Overlay dismiss option styles. */ +#overlayDismiss { + font-weight: bold; + font-size: 10px; + text-decoration: none; + padding-top: 2.5px; + background: none; + border: none; + outline: none; + cursor: pointer; +} +#overlayDismiss:hover { + color: rgba(255, 255, 255, 0.75); +} +#overlayDismiss:active { + color: rgba(165, 165, 165, 0.75); +} + +/* * * +* Overlay View (Server + Account Selection Content) +* * */ + +/* Server selection content container. */ +#serverSelectContent, +#accountSelectContent { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 75%; +} + +/* Server selection header. */ +#serverSelectHeader, +#accountSelectHeader { + font-family: 'Avenir Medium'; + font-size: 20px; + font-weight: bold; + color: #fff; + margin-bottom: 25px; +} + +/* Wrapper div for the list of available servers. */ +#serverSelectList, +#accountSelectList { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + max-height: 65%; + min-height: 40%; +} + +/* Scrollable div which lists the available servers. */ +#serverSelectListScrollable, +#accountSelectListScrollable { + padding: 0px 5px; + overflow-y: scroll; +} +#serverSelectListScrollable::-webkit-scrollbar, +#accountSelectListScrollable::-webkit-scrollbar { + width: 2px; +} +#serverSelectListScrollable::-webkit-scrollbar-track, +#accountSelectListScrollable::-webkit-scrollbar-track { + display: none; +} +#serverSelectListScrollable::-webkit-scrollbar-thumb, +#accountSelectListScrollable::-webkit-scrollbar-thumb { + border-radius: 10px; + box-shadow: inset 0 0 10px rgba(255, 255, 255, 0.50); +} + +/* Content container for a server listing. */ +.serverListing { + border: none; + padding: 0px; + width: 375px; + min-height: 60px; + display: flex; + justify-content: flex-start; + align-items: center; + opacity: 0.6; + transition: 0.25s ease; + cursor: pointer; + position: relative; + background: rgba(131, 131, 131, 0.25); +} +.serverListing[selected] { + cursor: default; + opacity: 1.0; +} +.serverListing:hover, +.serverListing:focus { + outline: none; + opacity: 1.0; +} + +.accountListing { + color: white; + border: 1px solid rgba(126, 126, 126, 0.57); + border-radius: 3px; + padding: 5px 45px; + width: 250px; + display: flex; + justify-content: flex-start; + align-items: center; + opacity: 0.6; + transition: 0.25s ease; + cursor: pointer; + position: relative; + background: rgba(0, 0, 0, 0.25); +} +.accountListing[selected] { + cursor: default; + opacity: 1.0; +} +.accountListing:hover, +.accountListing:focus { + outline: none; + opacity: 1.0; +} + +.accountListingName { + display: flex; + height: 100%; + width: 100%; + padding-left: 10px; +} + +/* Add spacing between server listings. */ +#serverSelectListScrollable > .serverListing:not(:first-child):not(:last-child), +#accountSelectListScrollable > .accountListing:not(:first-child):not(:last-child) { + margin: 5px 0px; +} +#serverSelectListScrollable > .serverListing:first-child, +#accountSelectListScrollable > .accountListing:first-child { + margin-bottom: 5px; +} +#serverSelectListScrollable > .serverListing:last-child, +#accountSelectListScrollable > .accountListing:last-child { + margin-top: 5px; +} + +/* Server listing image. */ +.serverListingImg { + margin: 0px 10px 0px 5px; + border: 1px solid #fff; + height: 50px; + width: 50px; +} + +/* Content container for the server listing's details. */ +.serverListingDetails { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: space-between; + height: 50px; +} + +/* The name of the server listing. */ +.serverListingName { + font-size: 14px; + font-weight: bold; +} + +/* Description for the server listing. */ +.serverListingDescription { + font-size: 10px; + line-height: 10px; + font-weight: bold; +} + +/* Content container for the server listing's information. */ +.serverListingInfo { + width: 100%; + display: flex; + justify-content: flex-start; +} + +/* The minecraft version of the server listing. */ +.serverListingVersion { + font-size: 10px; + text-align: center; + display: flex; + justify-content: center; + align-items: center; + line-height: 12px; + height: 12px; + border-radius: 2px; + background: rgba(31, 140, 11, 0.8); + padding: 0px 2px; +} + +/* The revision version of the server's manifest. */ +.serverListingRevision { + color: #969696; + font-size: 10px; + line-height: 12px; + padding: 0px 5px; +} + +/* Star which indicates the default (main) server. */ +.serverListingStarWrapper { + display: flex; + align-items: center; + cursor: pointer; + height: 12px; + position: relative; +} +/* Tooltip which displays when hovering over the star. */ +.serverListingStarTooltip { + visibility: hidden; + opacity: 0; + width: 65px; + background-color: rgba(0, 0, 0, 0.40); + text-align: center; + border-radius: 4px; + position: absolute; + z-index: 1; + left: 130%; + font-size: 10px; + transition: visibility 0s linear 0.25s, opacity 0.25s ease; +} +.serverListingStarTooltip::after { + content: " "; + position: absolute; + top: 50%; + right: 100%; /* To the left of the tooltip */ + margin-top: -5px; + border-width: 5px; + border-style: solid; + border-color: transparent rgba(0, 0, 0, 0.40) transparent transparent; +} +.serverListingStarWrapper:hover .serverListingStarTooltip { + visibility: visible; + opacity: 1; + transition-delay:0s; +} + +/* Content container which contains the server select actions. */ +#serverSelectActions, +#accountSelectActions { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + margin-top: 25px; +} + +/* Server selection confirm button styles. */ +#serverSelectConfirm, +#accountSelectConfirm { + background: none; + border: 1px solid #ffffff; + color: white; + font-family: 'Avenir Medium'; + font-weight: bold; + border-radius: 2px; + padding: 0px 8.1px; + cursor: pointer; + transition: 0.25s ease; + min-height: 20.67px; +} +#serverSelectConfirm:hover, +#serverSelectConfirm:focus, +#accountSelectConfirm:hover, +#accountSelectConfirm:focus { + box-shadow: 0px 0px 10px 0px #fff; + outline: none; +} +#serverSelectConfirm:active, +#accountSelectConfirm:active { + border-color: rgba(255, 255, 255, 0.75); + color: rgba(255, 255, 255, 0.75); +} + +/* Server selection cancel button styles. */ +#serverSelectCancel, +#accountSelectCancel { + font-weight: bold; + font-size: 10px; + text-decoration: none; + padding-top: 2.5px; + color: rgba(202, 202, 202, 0.75); + transition: 0.25s ease; + background: none; + border: none; + outline: none; + cursor: pointer; +} +#serverSelectCancel:hover, +#serverSelectCancel:focus, +#accountSelectCancel:hover, +#accountSelectCancel:focus { + color: rgba(255, 255, 255, 0.75); +} +#serverSelectCancel:active, +#accountSelectCancel:active { + color: rgba(165, 165, 165, 0.75); +} + +/******************************************************************************* + * * + * Loading Element (app.ejs) * + * * + ******************************************************************************/ + +/* Loading container, placed above everything. */ +#loadingContainer { + position: absolute; + z-index: 400; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: calc(100% - 22px); +} + +/* Loading content container. */ +#loadingContent { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +/* Spinner container. */ +#loadSpinnerContainer { + position: relative; + display: flex; + align-items: center; + justify-content: center; +} + +/* Stationary image for the spinner. */ +#loadCenterImage { + position: absolute; + width: 277px; + height: auto; +} + +/* Rotating image for the spinner. */ +#loadSpinnerImage { + width: 280px; + height: auto; + z-index: 400; +} + +/* Rotating animation for the spinner. */ +@keyframes rotating { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +/* Class which is applied when the spinner image is spinning. */ +.rotating { + animation: rotating 10s linear infinite; } \ No newline at end of file diff --git a/app/assets/images/icons/arrow.svg b/app/assets/images/icons/arrow.svg index 93326051..a2d2f129 100644 --- a/app/assets/images/icons/arrow.svg +++ b/app/assets/images/icons/arrow.svg @@ -1,7 +1,7 @@ - - - - - arrow - + + + + + arrow + \ No newline at end of file diff --git a/app/assets/images/icons/discord.svg b/app/assets/images/icons/discord.svg index 87271281..7c5d7d7d 100644 --- a/app/assets/images/icons/discord.svg +++ b/app/assets/images/icons/discord.svg @@ -1,10 +1,10 @@ - - - - - - discord - - - + + + + + + discord + + + \ No newline at end of file diff --git a/app/assets/images/icons/instagram.svg b/app/assets/images/icons/instagram.svg index 00c70bda..027f863e 100644 --- a/app/assets/images/icons/instagram.svg +++ b/app/assets/images/icons/instagram.svg @@ -1,9 +1,9 @@ - - - - - - - - + + + + + + + + \ No newline at end of file diff --git a/app/assets/images/icons/link.svg b/app/assets/images/icons/link.svg index df151d4a..c9d6c9ae 100644 --- a/app/assets/images/icons/link.svg +++ b/app/assets/images/icons/link.svg @@ -1,11 +1,11 @@ - - - - - - link - - - - + + + + + + link + + + + \ No newline at end of file diff --git a/app/assets/images/icons/lock.svg b/app/assets/images/icons/lock.svg index 47a5343e..24b2deb2 100644 --- a/app/assets/images/icons/lock.svg +++ b/app/assets/images/icons/lock.svg @@ -1,12 +1,12 @@ - - - - - - - - Lock - - - + + + + + + + + Lock + + + \ No newline at end of file diff --git a/app/assets/images/icons/news.svg b/app/assets/images/icons/news.svg index 775578d4..00bcfc21 100644 --- a/app/assets/images/icons/news.svg +++ b/app/assets/images/icons/news.svg @@ -1,14 +1,14 @@ - - - - - News - - - - - - - - + + + + + News + + + + + + + + \ No newline at end of file diff --git a/app/assets/images/icons/profile.svg b/app/assets/images/icons/profile.svg index 6526c65f..c4c9da79 100644 --- a/app/assets/images/icons/profile.svg +++ b/app/assets/images/icons/profile.svg @@ -1,10 +1,10 @@ - - - - - - Profile - - - + + + + + + Profile + + + \ No newline at end of file diff --git a/app/assets/images/icons/settings.svg b/app/assets/images/icons/settings.svg index 1a0ec766..c1deff3f 100644 --- a/app/assets/images/icons/settings.svg +++ b/app/assets/images/icons/settings.svg @@ -1,10 +1,10 @@ - - - - - - settings - - - + + + + + + settings + + + \ No newline at end of file diff --git a/app/assets/images/icons/sevenstar.svg b/app/assets/images/icons/sevenstar.svg index 4f55ef40..ff3c72ed 100644 --- a/app/assets/images/icons/sevenstar.svg +++ b/app/assets/images/icons/sevenstar.svg @@ -1,13 +1,13 @@ - - - - - Seven Pointed Star - - - - - - - + + + + + Seven Pointed Star + + + + + + + \ No newline at end of file diff --git a/app/assets/images/icons/sevenstar_circle.svg b/app/assets/images/icons/sevenstar_circle.svg index 9e8c8a8f..78bd4524 100644 --- a/app/assets/images/icons/sevenstar_circle.svg +++ b/app/assets/images/icons/sevenstar_circle.svg @@ -1,14 +1,14 @@ - - - - - Seven Pointed Star with Circle - - - - - - - - + + + + + Seven Pointed Star with Circle + + + + + + + + \ No newline at end of file diff --git a/app/assets/images/icons/sevenstar_circle_extended.svg b/app/assets/images/icons/sevenstar_circle_extended.svg index 8651baa3..1aa3954a 100644 --- a/app/assets/images/icons/sevenstar_circle_extended.svg +++ b/app/assets/images/icons/sevenstar_circle_extended.svg @@ -1,8 +1,8 @@ - - - - - Seven Pointed Star Extended with Circle - - + + + + + Seven Pointed Star Extended with Circle + + \ No newline at end of file diff --git a/app/assets/images/icons/sevenstar_circle_hole.svg b/app/assets/images/icons/sevenstar_circle_hole.svg index 65250d4a..86d90966 100644 --- a/app/assets/images/icons/sevenstar_circle_hole.svg +++ b/app/assets/images/icons/sevenstar_circle_hole.svg @@ -1,15 +1,15 @@ - - - - - Seven Pointed Star with Circle and Hole - - - - - - - - - + + + + + Seven Pointed Star with Circle and Hole + + + + + + + + + \ No newline at end of file diff --git a/app/assets/images/icons/sevenstar_circle_hole_extended.svg b/app/assets/images/icons/sevenstar_circle_hole_extended.svg index e549b4d4..323fa01e 100644 --- a/app/assets/images/icons/sevenstar_circle_hole_extended.svg +++ b/app/assets/images/icons/sevenstar_circle_hole_extended.svg @@ -1,9 +1,9 @@ - - - - - Seven Pointed Star Extended with Circle and Hole - - - + + + + + Seven Pointed Star Extended with Circle and Hole + + + \ No newline at end of file diff --git a/app/assets/images/icons/sevenstar_extended.svg b/app/assets/images/icons/sevenstar_extended.svg index b8e28241..5b64a439 100644 --- a/app/assets/images/icons/sevenstar_extended.svg +++ b/app/assets/images/icons/sevenstar_extended.svg @@ -1,7 +1,7 @@ - - - - - Seven Pointed Star Extended - + + + + + Seven Pointed Star Extended + \ No newline at end of file diff --git a/app/assets/images/icons/twitter.svg b/app/assets/images/icons/twitter.svg index bad6d42c..a15bbfd5 100644 --- a/app/assets/images/icons/twitter.svg +++ b/app/assets/images/icons/twitter.svg @@ -1,13 +1,13 @@ - - - - - - - - - - - twitter> - + + + + + + + + + + + twitter> + \ No newline at end of file diff --git a/app/assets/images/icons/youtube.svg b/app/assets/images/icons/youtube.svg index 38a2e8ba..46d4ae5d 100644 --- a/app/assets/images/icons/youtube.svg +++ b/app/assets/images/icons/youtube.svg @@ -1,10 +1,10 @@ - - - - - - youtube - - - + + + + + + youtube + + + \ No newline at end of file diff --git a/app/assets/js/assetexec.js b/app/assets/js/assetexec.js index 9d39449d..8e32b924 100644 --- a/app/assets/js/assetexec.js +++ b/app/assets/js/assetexec.js @@ -1,73 +1,73 @@ -let target = require('./assetguard')[process.argv[2]] -if(target == null){ - process.send({context: 'error', data: null, error: 'Invalid class name'}) - console.error('Invalid class name passed to argv[2], cannot continue.') - process.exit(1) -} -let tracker = new target(...(process.argv.splice(3))) - -process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' - -//const tracker = new AssetGuard(process.argv[2], process.argv[3]) -console.log('AssetExec Started') - -// Temporary for debug purposes. -process.on('unhandledRejection', r => console.log(r)) - -let percent = 0 -function assignListeners(){ - tracker.on('validate', (data) => { - process.send({context: 'validate', data}) - }) - tracker.on('progress', (data, acc, total) => { - const currPercent = parseInt((acc/total) * 100) - if (currPercent !== percent) { - percent = currPercent - process.send({context: 'progress', data, value: acc, total, percent}) - } - }) - tracker.on('complete', (data, ...args) => { - process.send({context: 'complete', data, args}) - }) - tracker.on('error', (data, error) => { - process.send({context: 'error', data, error}) - }) -} - -assignListeners() - -process.on('message', (msg) => { - if(msg.task === 'execute'){ - const func = msg.function - let nS = tracker[func] // Nonstatic context - let iS = target[func] // Static context - if(typeof nS === 'function' || typeof iS === 'function'){ - const f = typeof nS === 'function' ? nS : iS - const res = f.apply(f === nS ? tracker : null, msg.argsArr) - if(res instanceof Promise){ - res.then((v) => { - process.send({result: v, context: func}) - }).catch((err) => { - process.send({result: err.message || err, context: func}) - }) - } else { - process.send({result: res, context: func}) - } - } else { - process.send({context: 'error', data: null, error: `Function ${func} not found on ${process.argv[2]}`}) - } - } else if(msg.task === 'changeContext'){ - target = require('./assetguard')[msg.class] - if(target == null){ - process.send({context: 'error', data: null, error: `Invalid class ${msg.class}`}) - } else { - tracker = new target(...(msg.args)) - assignListeners() - } - } -}) - -process.on('disconnect', () => { - console.log('AssetExec Disconnected') - process.exit(0) +let target = require('./assetguard')[process.argv[2]] +if(target == null){ + process.send({context: 'error', data: null, error: 'Invalid class name'}) + console.error('Invalid class name passed to argv[2], cannot continue.') + process.exit(1) +} +let tracker = new target(...(process.argv.splice(3))) + +process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' + +//const tracker = new AssetGuard(process.argv[2], process.argv[3]) +console.log('AssetExec Started') + +// Temporary for debug purposes. +process.on('unhandledRejection', r => console.log(r)) + +let percent = 0 +function assignListeners(){ + tracker.on('validate', (data) => { + process.send({context: 'validate', data}) + }) + tracker.on('progress', (data, acc, total) => { + const currPercent = parseInt((acc/total) * 100) + if (currPercent !== percent) { + percent = currPercent + process.send({context: 'progress', data, value: acc, total, percent}) + } + }) + tracker.on('complete', (data, ...args) => { + process.send({context: 'complete', data, args}) + }) + tracker.on('error', (data, error) => { + process.send({context: 'error', data, error}) + }) +} + +assignListeners() + +process.on('message', (msg) => { + if(msg.task === 'execute'){ + const func = msg.function + let nS = tracker[func] // Nonstatic context + let iS = target[func] // Static context + if(typeof nS === 'function' || typeof iS === 'function'){ + const f = typeof nS === 'function' ? nS : iS + const res = f.apply(f === nS ? tracker : null, msg.argsArr) + if(res instanceof Promise){ + res.then((v) => { + process.send({result: v, context: func}) + }).catch((err) => { + process.send({result: err.message || err, context: func}) + }) + } else { + process.send({result: res, context: func}) + } + } else { + process.send({context: 'error', data: null, error: `Function ${func} not found on ${process.argv[2]}`}) + } + } else if(msg.task === 'changeContext'){ + target = require('./assetguard')[msg.class] + if(target == null){ + process.send({context: 'error', data: null, error: `Invalid class ${msg.class}`}) + } else { + tracker = new target(...(msg.args)) + assignListeners() + } + } +}) + +process.on('disconnect', () => { + console.log('AssetExec Disconnected') + process.exit(0) }) \ No newline at end of file diff --git a/app/assets/js/assetguard.js b/app/assets/js/assetguard.js index 0db05e56..15291209 100644 --- a/app/assets/js/assetguard.js +++ b/app/assets/js/assetguard.js @@ -1,1913 +1,1913 @@ -// Requirements -const AdmZip = require('adm-zip') -const async = require('async') -const child_process = require('child_process') -const crypto = require('crypto') -const EventEmitter = require('events') -const fs = require('fs-extra') -const path = require('path') -const Registry = require('winreg') -const request = require('request') -const tar = require('tar-fs') -const zlib = require('zlib') - -const ConfigManager = require('./configmanager') -const DistroManager = require('./distromanager') -const isDev = require('./isdev') - -// Constants -// const PLATFORM_MAP = { -// win32: '-windows-x64.tar.gz', -// darwin: '-macosx-x64.tar.gz', -// linux: '-linux-x64.tar.gz' -// } - -// Classes - -/** Class representing a base asset. */ -class Asset { - /** - * Create an asset. - * - * @param {any} id The id of the asset. - * @param {string} hash The hash value of the asset. - * @param {number} size The size in bytes of the asset. - * @param {string} from The url where the asset can be found. - * @param {string} to The absolute local file path of the asset. - */ - constructor(id, hash, size, from, to){ - this.id = id - this.hash = hash - this.size = size - this.from = from - this.to = to - } -} - -/** Class representing a mojang library. */ -class Library extends Asset { - - /** - * Converts the process.platform OS names to match mojang's OS names. - */ - static mojangFriendlyOS(){ - const opSys = process.platform - if (opSys === 'darwin') { - return 'osx' - } else if (opSys === 'win32'){ - return 'windows' - } else if (opSys === 'linux'){ - return 'linux' - } else { - return 'unknown_os' - } - } - - /** - * Checks whether or not a library is valid for download on a particular OS, following - * the rule format specified in the mojang version data index. If the allow property has - * an OS specified, then the library can ONLY be downloaded on that OS. If the disallow - * property has instead specified an OS, the library can be downloaded on any OS EXCLUDING - * the one specified. - * - * If the rules are undefined, the natives property will be checked for a matching entry - * for the current OS. - * - * @param {Array.} rules The Library's download rules. - * @param {Object} natives The Library's natives object. - * @returns {boolean} True if the Library follows the specified rules, otherwise false. - */ - static validateRules(rules, natives){ - if(rules == null) { - if(natives == null) { - return true - } else { - return natives[Library.mojangFriendlyOS()] != null - } - } - - for(let rule of rules){ - const action = rule.action - const osProp = rule.os - if(action != null && osProp != null){ - const osName = osProp.name - const osMoj = Library.mojangFriendlyOS() - if(action === 'allow'){ - return osName === osMoj - } else if(action === 'disallow'){ - return osName !== osMoj - } - } - } - return true - } -} - -class DistroModule extends Asset { - - /** - * Create a DistroModule. This is for processing, - * not equivalent to the module objects in the - * distro index. - * - * @param {any} id The id of the asset. - * @param {string} hash The hash value of the asset. - * @param {number} size The size in bytes of the asset. - * @param {string} from The url where the asset can be found. - * @param {string} to The absolute local file path of the asset. - * @param {string} type The the module type. - */ - constructor(id, hash, size, from, to, type){ - super(id, hash, size, from, to) - this.type = type - } - -} - -/** - * Class representing a download tracker. This is used to store meta data - * about a download queue, including the queue itself. - */ -class DLTracker { - - /** - * Create a DLTracker - * - * @param {Array.} dlqueue An array containing assets queued for download. - * @param {number} dlsize The combined size of each asset in the download queue array. - * @param {function(Asset)} callback Optional callback which is called when an asset finishes downloading. - */ - constructor(dlqueue, dlsize, callback = null){ - this.dlqueue = dlqueue - this.dlsize = dlsize - this.callback = callback - } - -} - -class Util { - - /** - * Returns true if the actual version is greater than - * or equal to the desired version. - * - * @param {string} desired The desired version. - * @param {string} actual The actual version. - */ - static mcVersionAtLeast(desired, actual){ - const des = desired.split('.') - const act = actual.split('.') - - for(let i=0; i= parseInt(des[i]))){ - return false - } - } - return true - } - - static isForgeGradle3(mcVersion, forgeVersion) { - - if(Util.mcVersionAtLeast('1.13', mcVersion)) { - return true - } - - try { - - const forgeVer = forgeVersion.split('-')[1] - - const maxFG2 = [14, 23, 5, 2847] - const verSplit = forgeVer.split('.').map(v => Number(v)) - - for(let i=0; i maxFG2[i]) { - return true - } else if(verSplit[i] < maxFG2[i]) { - return false - } - } - - return false - - } catch(err) { - throw new Error('Forge version is complex (changed).. launcher requires a patch.') - } - } - - static isAutoconnectBroken(forgeVersion) { - - const minWorking = [31, 2, 15] - const verSplit = forgeVersion.split('.').map(v => Number(v)) - - if(verSplit[0] === 31) { - for(let i=0; i minWorking[i]) { - return false - } else if(verSplit[i] < minWorking[i]) { - return true - } - } - } - - return false - } - -} - - -class JavaGuard extends EventEmitter { - - constructor(mcVersion){ - super() - this.mcVersion = mcVersion - } - - // /** - // * @typedef OracleJREData - // * @property {string} uri The base uri of the JRE. - // * @property {{major: string, update: string, build: string}} version Object containing version information. - // */ - - // /** - // * Resolves the latest version of Oracle's JRE and parses its download link. - // * - // * @returns {Promise.} Promise which resolved to an object containing the JRE download data. - // */ - // static _latestJREOracle(){ - - // const url = 'https://www.oracle.com/technetwork/java/javase/downloads/jre8-downloads-2133155.html' - // const regex = /https:\/\/.+?(?=\/java)\/java\/jdk\/([0-9]+u[0-9]+)-(b[0-9]+)\/([a-f0-9]{32})?\/jre-\1/ - - // return new Promise((resolve, reject) => { - // request(url, (err, resp, body) => { - // if(!err){ - // const arr = body.match(regex) - // const verSplit = arr[1].split('u') - // resolve({ - // uri: arr[0], - // version: { - // major: verSplit[0], - // update: verSplit[1], - // build: arr[2] - // } - // }) - // } else { - // resolve(null) - // } - // }) - // }) - // } - - /** - * @typedef OpenJDKData - * @property {string} uri The base uri of the JRE. - * @property {number} size The size of the download. - * @property {string} name The name of the artifact. - */ - - /** - * Fetch the last open JDK binary. Uses https://api.adoptopenjdk.net/ - * - * @param {string} major The major version of Java to fetch. - * - * @returns {Promise.} Promise which resolved to an object containing the JRE download data. - */ - static _latestOpenJDK(major = '8'){ - - const sanitizedOS = process.platform === 'win32' ? 'windows' : (process.platform === 'darwin' ? 'mac' : process.platform) - - const url = `https://api.adoptopenjdk.net/v2/latestAssets/nightly/openjdk${major}?os=${sanitizedOS}&arch=x64&heap_size=normal&openjdk_impl=hotspot&type=jre` - - return new Promise((resolve, reject) => { - request({url, json: true}, (err, resp, body) => { - if(!err && body.length > 0){ - resolve({ - uri: body[0].binary_link, - size: body[0].binary_size, - name: body[0].binary_name - }) - } else { - resolve(null) - } - }) - }) - } - - /** - * Returns the path of the OS-specific executable for the given Java - * installation. Supported OS's are win32, darwin, linux. - * - * @param {string} rootDir The root directory of the Java installation. - * @returns {string} The path to the Java executable. - */ - static javaExecFromRoot(rootDir){ - if(process.platform === 'win32'){ - return path.join(rootDir, 'bin', 'javaw.exe') - } else if(process.platform === 'darwin'){ - return path.join(rootDir, 'Contents', 'Home', 'bin', 'java') - } else if(process.platform === 'linux'){ - return path.join(rootDir, 'bin', 'java') - } - return rootDir - } - - /** - * Check to see if the given path points to a Java executable. - * - * @param {string} pth The path to check against. - * @returns {boolean} True if the path points to a Java executable, otherwise false. - */ - static isJavaExecPath(pth){ - if(process.platform === 'win32'){ - return pth.endsWith(path.join('bin', 'javaw.exe')) - } else if(process.platform === 'darwin'){ - return pth.endsWith(path.join('bin', 'java')) - } else if(process.platform === 'linux'){ - return pth.endsWith(path.join('bin', 'java')) - } - return false - } - - /** - * Load Mojang's launcher.json file. - * - * @returns {Promise.} Promise which resolves to Mojang's launcher.json object. - */ - static loadMojangLauncherData(){ - return new Promise((resolve, reject) => { - request.get('https://launchermeta.mojang.com/mc/launcher.json', (err, resp, body) => { - if(err){ - resolve(null) - } else { - resolve(JSON.parse(body)) - } - }) - }) - } - - /** - * Parses a **full** Java Runtime version string and resolves - * the version information. Dynamically detects the formatting - * to use. - * - * @param {string} verString Full version string to parse. - * @returns Object containing the version information. - */ - static parseJavaRuntimeVersion(verString){ - const major = verString.split('.')[0] - if(major == 1){ - return JavaGuard._parseJavaRuntimeVersion_8(verString) - } else { - return JavaGuard._parseJavaRuntimeVersion_9(verString) - } - } - - /** - * Parses a **full** Java Runtime version string and resolves - * the version information. Uses Java 8 formatting. - * - * @param {string} verString Full version string to parse. - * @returns Object containing the version information. - */ - static _parseJavaRuntimeVersion_8(verString){ - // 1.{major}.0_{update}-b{build} - // ex. 1.8.0_152-b16 - const ret = {} - let pts = verString.split('-') - ret.build = parseInt(pts[1].substring(1)) - pts = pts[0].split('_') - ret.update = parseInt(pts[1]) - ret.major = parseInt(pts[0].split('.')[1]) - return ret - } - - /** - * Parses a **full** Java Runtime version string and resolves - * the version information. Uses Java 9+ formatting. - * - * @param {string} verString Full version string to parse. - * @returns Object containing the version information. - */ - static _parseJavaRuntimeVersion_9(verString){ - // {major}.{minor}.{revision}+{build} - // ex. 10.0.2+13 - const ret = {} - let pts = verString.split('+') - ret.build = parseInt(pts[1]) - pts = pts[0].split('.') - ret.major = parseInt(pts[0]) - ret.minor = parseInt(pts[1]) - ret.revision = parseInt(pts[2]) - return ret - } - - /** - * Validates the output of a JVM's properties. Currently validates that a JRE is x64 - * and that the major = 8, update > 52. - * - * @param {string} stderr The output to validate. - * - * @returns {Promise.} A promise which resolves to a meta object about the JVM. - * The validity is stored inside the `valid` property. - */ - _validateJVMProperties(stderr){ - const res = stderr - const props = res.split('\n') - - const goal = 2 - let checksum = 0 - - const meta = {} - - for(let i=0; i -1){ - let arch = props[i].split('=')[1].trim() - arch = parseInt(arch) - console.log(props[i].trim()) - if(arch === 64){ - meta.arch = arch - ++checksum - if(checksum === goal){ - break - } - } - } else if(props[i].indexOf('java.runtime.version') > -1){ - let verString = props[i].split('=')[1].trim() - console.log(props[i].trim()) - const verOb = JavaGuard.parseJavaRuntimeVersion(verString) - if(verOb.major < 9){ - // Java 8 - if(verOb.major === 8 && verOb.update > 52){ - meta.version = verOb - ++checksum - if(checksum === goal){ - break - } - } - } else { - // Java 9+ - if(Util.mcVersionAtLeast('1.13', this.mcVersion)){ - console.log('Java 9+ not yet tested.') - /* meta.version = verOb - ++checksum - if(checksum === goal){ - break - } */ - } - } - } - } - - meta.valid = checksum === goal - - return meta - } - - /** - * Validates that a Java binary is at least 64 bit. This makes use of the non-standard - * command line option -XshowSettings:properties. The output of this contains a property, - * sun.arch.data.model = ARCH, in which ARCH is either 32 or 64. This option is supported - * in Java 8 and 9. Since this is a non-standard option. This will resolve to true if - * the function's code throws errors. That would indicate that the option is changed or - * removed. - * - * @param {string} binaryExecPath Path to the java executable we wish to validate. - * - * @returns {Promise.} A promise which resolves to a meta object about the JVM. - * The validity is stored inside the `valid` property. - */ - _validateJavaBinary(binaryExecPath){ - - return new Promise((resolve, reject) => { - if(!JavaGuard.isJavaExecPath(binaryExecPath)){ - resolve({valid: false}) - } else if(fs.existsSync(binaryExecPath)){ - // Workaround (javaw.exe no longer outputs this information.) - console.log(typeof binaryExecPath) - if(binaryExecPath.indexOf('javaw.exe') > -1) { - binaryExecPath.replace('javaw.exe', 'java.exe') - } - child_process.exec('"' + binaryExecPath + '" -XshowSettings:properties', (err, stdout, stderr) => { - try { - // Output is stored in stderr? - resolve(this._validateJVMProperties(stderr)) - } catch (err){ - // Output format might have changed, validation cannot be completed. - resolve({valid: false}) - } - }) - } else { - resolve({valid: false}) - } - }) - - } - - /** - * Checks for the presence of the environment variable JAVA_HOME. If it exits, we will check - * to see if the value points to a path which exists. If the path exits, the path is returned. - * - * @returns {string} The path defined by JAVA_HOME, if it exists. Otherwise null. - */ - static _scanJavaHome(){ - const jHome = process.env.JAVA_HOME - try { - let res = fs.existsSync(jHome) - return res ? jHome : null - } catch (err) { - // Malformed JAVA_HOME property. - return null - } - } - - /** - * Scans the registry for 64-bit Java entries. The paths of each entry are added to - * a set and returned. Currently, only Java 8 (1.8) is supported. - * - * @returns {Promise.>} A promise which resolves to a set of 64-bit Java root - * paths found in the registry. - */ - static _scanRegistry(){ - - return new Promise((resolve, reject) => { - // Keys for Java v9.0.0 and later: - // 'SOFTWARE\\JavaSoft\\JRE' - // 'SOFTWARE\\JavaSoft\\JDK' - // Forge does not yet support Java 9, therefore we do not. - - // Keys for Java 1.8 and prior: - const regKeys = [ - '\\SOFTWARE\\JavaSoft\\Java Runtime Environment', - '\\SOFTWARE\\JavaSoft\\Java Development Kit' - ] - - let keysDone = 0 - - const candidates = new Set() - - for(let i=0; i { - if(exists) { - key.keys((err, javaVers) => { - if(err){ - keysDone++ - console.error(err) - - // REG KEY DONE - // DUE TO ERROR - if(keysDone === regKeys.length){ - resolve(candidates) - } - } else { - if(javaVers.length === 0){ - // REG KEY DONE - // NO SUBKEYS - keysDone++ - if(keysDone === regKeys.length){ - resolve(candidates) - } - } else { - - let numDone = 0 - - for(let j=0; j { - const jHome = res.value - if(jHome.indexOf('(x86)') === -1){ - candidates.add(jHome) - } - - // SUBKEY DONE - - numDone++ - if(numDone === javaVers.length){ - keysDone++ - if(keysDone === regKeys.length){ - resolve(candidates) - } - } - }) - } else { - - // SUBKEY DONE - // NOT JAVA 8 - - numDone++ - if(numDone === javaVers.length){ - keysDone++ - if(keysDone === regKeys.length){ - resolve(candidates) - } - } - } - } - } - } - }) - } else { - - // REG KEY DONE - // DUE TO NON-EXISTANCE - - keysDone++ - if(keysDone === regKeys.length){ - resolve(candidates) - } - } - }) - } - - }) - - } - - /** - * See if JRE exists in the Internet Plug-Ins folder. - * - * @returns {string} The path of the JRE if found, otherwise null. - */ - static _scanInternetPlugins(){ - // /Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java - const pth = '/Library/Internet Plug-Ins/JavaAppletPlugin.plugin' - const res = fs.existsSync(JavaGuard.javaExecFromRoot(pth)) - return res ? pth : null - } - - /** - * Scan a directory for root JVM folders. - * - * @param {string} scanDir The directory to scan. - * @returns {Promise.>} A promise which resolves to a set of the discovered - * root JVM folders. - */ - static _scanFileSystem(scanDir){ - return new Promise((resolve, reject) => { - - fs.exists(scanDir, (e) => { - - let res = new Set() - - if(e){ - fs.readdir(scanDir, (err, files) => { - if(err){ - resolve(res) - console.log(err) - } else { - let pathsDone = 0 - - for(let i=0; i { - - if(v){ - res.add(combinedPath) - } - - ++pathsDone - - if(pathsDone === files.length){ - resolve(res) - } - - }) - } - if(pathsDone === files.length){ - resolve(res) - } - } - }) - } else { - resolve(res) - } - }) - - }) - } - - /** - * - * @param {Set.} rootSet A set of JVM root strings to validate. - * @returns {Promise.} A promise which resolves to an array of meta objects - * for each valid JVM root directory. - */ - async _validateJavaRootSet(rootSet){ - - const rootArr = Array.from(rootSet) - const validArr = [] - - for(let i=0; i { - - if(a.version.major === b.version.major){ - - if(a.version.major < 9){ - // Java 8 - if(a.version.update === b.version.update){ - if(a.version.build === b.version.build){ - - // Same version, give priority to JRE. - if(a.execPath.toLowerCase().indexOf('jdk') > -1){ - return b.execPath.toLowerCase().indexOf('jdk') > -1 ? 0 : 1 - } else { - return -1 - } - - } else { - return a.version.build > b.version.build ? -1 : 1 - } - } else { - return a.version.update > b.version.update ? -1 : 1 - } - } else { - // Java 9+ - if(a.version.minor === b.version.minor){ - if(a.version.revision === b.version.revision){ - - // Same version, give priority to JRE. - if(a.execPath.toLowerCase().indexOf('jdk') > -1){ - return b.execPath.toLowerCase().indexOf('jdk') > -1 ? 0 : 1 - } else { - return -1 - } - - } else { - return a.version.revision > b.version.revision ? -1 : 1 - } - } else { - return a.version.minor > b.version.minor ? -1 : 1 - } - } - - } else { - return a.version.major > b.version.major ? -1 : 1 - } - }) - - return retArr - } - - /** - * Attempts to find a valid x64 installation of Java on Windows machines. - * Possible paths will be pulled from the registry and the JAVA_HOME environment - * variable. The paths will be sorted with higher versions preceeding lower, and - * JREs preceeding JDKs. The binaries at the sorted paths will then be validated. - * The first validated is returned. - * - * Higher versions > Lower versions - * If versions are equal, JRE > JDK. - * - * @param {string} dataDir The base launcher directory. - * @returns {Promise.} A Promise which resolves to the executable path of a valid - * x64 Java installation. If none are found, null is returned. - */ - async _win32JavaValidate(dataDir){ - - // Get possible paths from the registry. - let pathSet1 = await JavaGuard._scanRegistry() - if(pathSet1.length === 0){ - // Do a manual file system scan of program files. - pathSet1 = JavaGuard._scanFileSystem('C:\\Program Files\\Java') - } - - // Get possible paths from the data directory. - const pathSet2 = await JavaGuard._scanFileSystem(path.join(dataDir, 'runtime', 'x64')) - - // Merge the results. - const uberSet = new Set([...pathSet1, ...pathSet2]) - - // Validate JAVA_HOME. - const jHome = JavaGuard._scanJavaHome() - if(jHome != null && jHome.indexOf('(x86)') === -1){ - uberSet.add(jHome) - } - - let pathArr = await this._validateJavaRootSet(uberSet) - pathArr = JavaGuard._sortValidJavaArray(pathArr) - - if(pathArr.length > 0){ - return pathArr[0].execPath - } else { - return null - } - - } - - /** - * Attempts to find a valid x64 installation of Java on MacOS. - * The system JVM directory is scanned for possible installations. - * The JAVA_HOME enviroment variable and internet plugins directory - * are also scanned and validated. - * - * Higher versions > Lower versions - * If versions are equal, JRE > JDK. - * - * @param {string} dataDir The base launcher directory. - * @returns {Promise.} A Promise which resolves to the executable path of a valid - * x64 Java installation. If none are found, null is returned. - */ - async _darwinJavaValidate(dataDir){ - - const pathSet1 = await JavaGuard._scanFileSystem('/Library/Java/JavaVirtualMachines') - const pathSet2 = await JavaGuard._scanFileSystem(path.join(dataDir, 'runtime', 'x64')) - - const uberSet = new Set([...pathSet1, ...pathSet2]) - - // Check Internet Plugins folder. - const iPPath = JavaGuard._scanInternetPlugins() - if(iPPath != null){ - uberSet.add(iPPath) - } - - // Check the JAVA_HOME environment variable. - let jHome = JavaGuard._scanJavaHome() - if(jHome != null){ - // Ensure we are at the absolute root. - if(jHome.contains('/Contents/Home')){ - jHome = jHome.substring(0, jHome.indexOf('/Contents/Home')) - } - uberSet.add(jHome) - } - - let pathArr = await this._validateJavaRootSet(uberSet) - pathArr = JavaGuard._sortValidJavaArray(pathArr) - - if(pathArr.length > 0){ - return pathArr[0].execPath - } else { - return null - } - } - - /** - * Attempts to find a valid x64 installation of Java on Linux. - * The system JVM directory is scanned for possible installations. - * The JAVA_HOME enviroment variable is also scanned and validated. - * - * Higher versions > Lower versions - * If versions are equal, JRE > JDK. - * - * @param {string} dataDir The base launcher directory. - * @returns {Promise.} A Promise which resolves to the executable path of a valid - * x64 Java installation. If none are found, null is returned. - */ - async _linuxJavaValidate(dataDir){ - - const pathSet1 = await JavaGuard._scanFileSystem('/usr/lib/jvm') - const pathSet2 = await JavaGuard._scanFileSystem(path.join(dataDir, 'runtime', 'x64')) - - const uberSet = new Set([...pathSet1, ...pathSet2]) - - // Validate JAVA_HOME - const jHome = JavaGuard._scanJavaHome() - if(jHome != null){ - uberSet.add(jHome) - } - - let pathArr = await this._validateJavaRootSet(uberSet) - pathArr = JavaGuard._sortValidJavaArray(pathArr) - - if(pathArr.length > 0){ - return pathArr[0].execPath - } else { - return null - } - } - - /** - * Retrieve the path of a valid x64 Java installation. - * - * @param {string} dataDir The base launcher directory. - * @returns {string} A path to a valid x64 Java installation, null if none found. - */ - async validateJava(dataDir){ - return await this['_' + process.platform + 'JavaValidate'](dataDir) - } - -} - - - - -/** - * Central object class used for control flow. This object stores data about - * categories of downloads. Each category is assigned an identifier with a - * DLTracker object as its value. Combined information is also stored, such as - * the total size of all the queued files in each category. This event is used - * to emit events so that external modules can listen into processing done in - * this module. - */ -class AssetGuard extends EventEmitter { - - /** - * Create an instance of AssetGuard. - * On creation the object's properties are never-null default - * values. Each identifier is resolved to an empty DLTracker. - * - * @param {string} commonPath The common path for shared game files. - * @param {string} javaexec The path to a java executable which will be used - * to finalize installation. - */ - constructor(commonPath, javaexec){ - super() - this.totaldlsize = 0 - this.progress = 0 - this.assets = new DLTracker([], 0) - this.libraries = new DLTracker([], 0) - this.files = new DLTracker([], 0) - this.forge = new DLTracker([], 0) - this.java = new DLTracker([], 0) - this.extractQueue = [] - this.commonPath = commonPath - this.javaexec = javaexec - } - - // Static Utility Functions - // #region - - // Static Hash Validation Functions - // #region - - /** - * Calculates the hash for a file using the specified algorithm. - * - * @param {Buffer} buf The buffer containing file data. - * @param {string} algo The hash algorithm. - * @returns {string} The calculated hash in hex. - */ - static _calculateHash(buf, algo){ - return crypto.createHash(algo).update(buf).digest('hex') - } - - /** - * Used to parse a checksums file. This is specifically designed for - * the checksums.sha1 files found inside the forge scala dependencies. - * - * @param {string} content The string content of the checksums file. - * @returns {Object} An object with keys being the file names, and values being the hashes. - */ - static _parseChecksumsFile(content){ - let finalContent = {} - let lines = content.split('\n') - for(let i=0; i} checksums The checksums listed in the forge version index. - * @returns {boolean} True if the file exists and the hashes match, otherwise false. - */ - static _validateForgeChecksum(filePath, checksums){ - if(fs.existsSync(filePath)){ - if(checksums == null || checksums.length === 0){ - return true - } - let buf = fs.readFileSync(filePath) - let calcdhash = AssetGuard._calculateHash(buf, 'sha1') - let valid = checksums.includes(calcdhash) - if(!valid && filePath.endsWith('.jar')){ - valid = AssetGuard._validateForgeJar(filePath, checksums) - } - return valid - } - return false - } - - /** - * Validates a forge jar file dependency who declares a checksums.sha1 file. - * This can be an expensive task as it usually requires that we calculate thousands - * of hashes. - * - * @param {Buffer} buf The buffer of the jar file. - * @param {Array.} checksums The checksums listed in the forge version index. - * @returns {boolean} True if all hashes declared in the checksums.sha1 file match the actual hashes. - */ - static _validateForgeJar(buf, checksums){ - // Double pass method was the quickest I found. I tried a version where we store data - // to only require a single pass, plus some quick cleanup but that seemed to take slightly more time. - - const hashes = {} - let expected = {} - - const zip = new AdmZip(buf) - const zipEntries = zip.getEntries() - - //First pass - for(let i=0; i} filePaths The paths of the files to be extracted and unpacked. - * @returns {Promise.} An empty promise to indicate the extraction has completed. - */ - static _extractPackXZ(filePaths, javaExecutable){ - console.log('[PackXZExtract] Starting') - return new Promise((resolve, reject) => { - - let libPath - if(isDev){ - libPath = path.join(process.cwd(), 'libraries', 'java', 'PackXZExtract.jar') - } else { - if(process.platform === 'darwin'){ - libPath = path.join(process.cwd(),'Contents', 'Resources', 'libraries', 'java', 'PackXZExtract.jar') - } else { - libPath = path.join(process.cwd(), 'resources', 'libraries', 'java', 'PackXZExtract.jar') - } - } - - const filePath = filePaths.join(',') - const child = child_process.spawn(javaExecutable, ['-jar', libPath, '-packxz', filePath]) - child.stdout.on('data', (data) => { - console.log('[PackXZExtract]', data.toString('utf8')) - }) - child.stderr.on('data', (data) => { - console.log('[PackXZExtract]', data.toString('utf8')) - }) - child.on('close', (code, signal) => { - console.log('[PackXZExtract]', 'Exited with code', code) - resolve() - }) - }) - } - - /** - * Function which finalizes the forge installation process. This creates a 'version' - * instance for forge and saves its version.json file into that instance. If that - * instance already exists, the contents of the version.json file are read and returned - * in a promise. - * - * @param {Asset} asset The Asset object representing Forge. - * @param {string} commonPath The common path for shared game files. - * @returns {Promise.} A promise which resolves to the contents of forge's version.json. - */ - static _finalizeForgeAsset(asset, commonPath){ - return new Promise((resolve, reject) => { - fs.readFile(asset.to, (err, data) => { - const zip = new AdmZip(data) - const zipEntries = zip.getEntries() - - for(let i=0; i} Promise which resolves to the version data object. - */ - loadVersionData(version, force = false){ - const self = this - return new Promise(async (resolve, reject) => { - const versionPath = path.join(self.commonPath, 'versions', version) - const versionFile = path.join(versionPath, version + '.json') - if(!fs.existsSync(versionFile) || force){ - const url = await self._getVersionDataUrl(version) - //This download will never be tracked as it's essential and trivial. - console.log('Preparing download of ' + version + ' assets.') - fs.ensureDirSync(versionPath) - const stream = request(url).pipe(fs.createWriteStream(versionFile)) - stream.on('finish', () => { - resolve(JSON.parse(fs.readFileSync(versionFile))) - }) - } else { - resolve(JSON.parse(fs.readFileSync(versionFile))) - } - }) - } - - /** - * Parses Mojang's version manifest and retrieves the url of the version - * data index. - * - * @param {string} version The version to lookup. - * @returns {Promise.} Promise which resolves to the url of the version data index. - * If the version could not be found, resolves to null. - */ - _getVersionDataUrl(version){ - return new Promise((resolve, reject) => { - request('https://launchermeta.mojang.com/mc/game/version_manifest.json', (error, resp, body) => { - if(error){ - reject(error) - } else { - const manifest = JSON.parse(body) - - for(let v of manifest.versions){ - if(v.id === version){ - resolve(v.url) - } - } - - resolve(null) - } - }) - }) - } - - - // Asset (Category=''') Validation Functions - // #region - - /** - * Public asset validation function. This function will handle the validation of assets. - * It will parse the asset index specified in the version data, analyzing each - * asset entry. In this analysis it will check to see if the local file exists and is valid. - * If not, it will be added to the download queue for the 'assets' identifier. - * - * @param {Object} versionData The version data for the assets. - * @param {boolean} force Optional. If true, the asset index will be downloaded even if it exists locally. Defaults to false. - * @returns {Promise.} An empty promise to indicate the async processing has completed. - */ - validateAssets(versionData, force = false){ - const self = this - return new Promise((resolve, reject) => { - self._assetChainIndexData(versionData, force).then(() => { - resolve() - }) - }) - } - - //Chain the asset tasks to provide full async. The below functions are private. - /** - * Private function used to chain the asset validation process. This function retrieves - * the index data. - * @param {Object} versionData - * @param {boolean} force - * @returns {Promise.} An empty promise to indicate the async processing has completed. - */ - _assetChainIndexData(versionData, force = false){ - const self = this - return new Promise((resolve, reject) => { - //Asset index constants. - const assetIndex = versionData.assetIndex - const name = assetIndex.id + '.json' - const indexPath = path.join(self.commonPath, 'assets', 'indexes') - const assetIndexLoc = path.join(indexPath, name) - - let data = null - if(!fs.existsSync(assetIndexLoc) || force){ - console.log('Downloading ' + versionData.id + ' asset index.') - fs.ensureDirSync(indexPath) - const stream = request(assetIndex.url).pipe(fs.createWriteStream(assetIndexLoc)) - stream.on('finish', () => { - data = JSON.parse(fs.readFileSync(assetIndexLoc, 'utf-8')) - self._assetChainValidateAssets(versionData, data).then(() => { - resolve() - }) - }) - } else { - data = JSON.parse(fs.readFileSync(assetIndexLoc, 'utf-8')) - self._assetChainValidateAssets(versionData, data).then(() => { - resolve() - }) - } - }) - } - - /** - * Private function used to chain the asset validation process. This function processes - * the assets and enqueues missing or invalid files. - * @param {Object} versionData - * @param {boolean} force - * @returns {Promise.} An empty promise to indicate the async processing has completed. - */ - _assetChainValidateAssets(versionData, indexData){ - const self = this - return new Promise((resolve, reject) => { - - //Asset constants - const resourceURL = 'http://resources.download.minecraft.net/' - const localPath = path.join(self.commonPath, 'assets') - const objectPath = path.join(localPath, 'objects') - - const assetDlQueue = [] - let dlSize = 0 - let acc = 0 - const total = Object.keys(indexData.objects).length - //const objKeys = Object.keys(data.objects) - async.forEachOfLimit(indexData.objects, 10, (value, key, cb) => { - acc++ - self.emit('progress', 'assets', acc, total) - const hash = value.hash - const assetName = path.join(hash.substring(0, 2), hash) - const urlName = hash.substring(0, 2) + '/' + hash - const ast = new Asset(key, hash, value.size, resourceURL + urlName, path.join(objectPath, assetName)) - if(!AssetGuard._validateLocal(ast.to, 'sha1', ast.hash)){ - dlSize += (ast.size*1) - assetDlQueue.push(ast) - } - cb() - }, (err) => { - self.assets = new DLTracker(assetDlQueue, dlSize) - resolve() - }) - }) - } - - // #endregion - - // Library (Category=''') Validation Functions - // #region - - /** - * Public library validation function. This function will handle the validation of libraries. - * It will parse the version data, analyzing each library entry. In this analysis, it will - * check to see if the local file exists and is valid. If not, it will be added to the download - * queue for the 'libraries' identifier. - * - * @param {Object} versionData The version data for the assets. - * @returns {Promise.} An empty promise to indicate the async processing has completed. - */ - validateLibraries(versionData){ - const self = this - return new Promise((resolve, reject) => { - - const libArr = versionData.libraries - const libPath = path.join(self.commonPath, 'libraries') - - const libDlQueue = [] - let dlSize = 0 - - //Check validity of each library. If the hashs don't match, download the library. - async.eachLimit(libArr, 5, (lib, cb) => { - if(Library.validateRules(lib.rules, lib.natives)){ - let artifact = (lib.natives == null) ? lib.downloads.artifact : lib.downloads.classifiers[lib.natives[Library.mojangFriendlyOS()].replace('${arch}', process.arch.replace('x', ''))] - const libItm = new Library(lib.name, artifact.sha1, artifact.size, artifact.url, path.join(libPath, artifact.path)) - if(!AssetGuard._validateLocal(libItm.to, 'sha1', libItm.hash)){ - dlSize += (libItm.size*1) - libDlQueue.push(libItm) - } - } - cb() - }, (err) => { - self.libraries = new DLTracker(libDlQueue, dlSize) - resolve() - }) - }) - } - - // #endregion - - // Miscellaneous (Category=files) Validation Functions - // #region - - /** - * Public miscellaneous mojang file validation function. These files will be enqueued under - * the 'files' identifier. - * - * @param {Object} versionData The version data for the assets. - * @returns {Promise.} An empty promise to indicate the async processing has completed. - */ - validateMiscellaneous(versionData){ - const self = this - return new Promise(async (resolve, reject) => { - await self.validateClient(versionData) - await self.validateLogConfig(versionData) - resolve() - }) - } - - /** - * Validate client file - artifact renamed from client.jar to '{version}'.jar. - * - * @param {Object} versionData The version data for the assets. - * @param {boolean} force Optional. If true, the asset index will be downloaded even if it exists locally. Defaults to false. - * @returns {Promise.} An empty promise to indicate the async processing has completed. - */ - validateClient(versionData, force = false){ - const self = this - return new Promise((resolve, reject) => { - const clientData = versionData.downloads.client - const version = versionData.id - const targetPath = path.join(self.commonPath, 'versions', version) - const targetFile = version + '.jar' - - let client = new Asset(version + ' client', clientData.sha1, clientData.size, clientData.url, path.join(targetPath, targetFile)) - - if(!AssetGuard._validateLocal(client.to, 'sha1', client.hash) || force){ - self.files.dlqueue.push(client) - self.files.dlsize += client.size*1 - resolve() - } else { - resolve() - } - }) - } - - /** - * Validate log config. - * - * @param {Object} versionData The version data for the assets. - * @param {boolean} force Optional. If true, the asset index will be downloaded even if it exists locally. Defaults to false. - * @returns {Promise.} An empty promise to indicate the async processing has completed. - */ - validateLogConfig(versionData){ - const self = this - return new Promise((resolve, reject) => { - const client = versionData.logging.client - const file = client.file - const targetPath = path.join(self.commonPath, 'assets', 'log_configs') - - let logConfig = new Asset(file.id, file.sha1, file.size, file.url, path.join(targetPath, file.id)) - - if(!AssetGuard._validateLocal(logConfig.to, 'sha1', logConfig.hash)){ - self.files.dlqueue.push(logConfig) - self.files.dlsize += logConfig.size*1 - resolve() - } else { - resolve() - } - }) - } - - // #endregion - - // Distribution (Category=forge) Validation Functions - // #region - - /** - * Validate the distribution. - * - * @param {Server} server The Server to validate. - * @returns {Promise.} A promise which resolves to the server distribution object. - */ - validateDistribution(server){ - const self = this - return new Promise((resolve, reject) => { - self.forge = self._parseDistroModules(server.getModules(), server.getMinecraftVersion(), server.getID()) - resolve(server) - }) - } - - _parseDistroModules(modules, version, servid){ - let alist = [] - let asize = 0 - for(let ob of modules){ - let obArtifact = ob.getArtifact() - let obPath = obArtifact.getPath() - let artifact = new DistroModule(ob.getIdentifier(), obArtifact.getHash(), obArtifact.getSize(), obArtifact.getURL(), obPath, ob.getType()) - const validationPath = obPath.toLowerCase().endsWith('.pack.xz') ? obPath.substring(0, obPath.toLowerCase().lastIndexOf('.pack.xz')) : obPath - if(!AssetGuard._validateLocal(validationPath, 'MD5', artifact.hash)){ - asize += artifact.size*1 - alist.push(artifact) - if(validationPath !== obPath) this.extractQueue.push(obPath) - } - //Recursively process the submodules then combine the results. - if(ob.getSubModules() != null){ - let dltrack = this._parseDistroModules(ob.getSubModules(), version, servid) - asize += dltrack.dlsize*1 - alist = alist.concat(dltrack.dlqueue) - } - } - - return new DLTracker(alist, asize) - } - - /** - * Loads Forge's version.json data into memory for the specified server id. - * - * @param {string} server The Server to load Forge data for. - * @returns {Promise.} A promise which resolves to Forge's version.json data. - */ - loadForgeData(server){ - const self = this - return new Promise(async (resolve, reject) => { - const modules = server.getModules() - for(let ob of modules){ - const type = ob.getType() - if(type === DistroManager.Types.ForgeHosted || type === DistroManager.Types.Forge){ - if(Util.isForgeGradle3(server.getMinecraftVersion(), ob.getVersion())){ - // Read Manifest - for(let sub of ob.getSubModules()){ - if(sub.getType() === DistroManager.Types.VersionManifest){ - resolve(JSON.parse(fs.readFileSync(sub.getArtifact().getPath(), 'utf-8'))) - return - } - } - reject('No forge version manifest found!') - return - } else { - let obArtifact = ob.getArtifact() - let obPath = obArtifact.getPath() - let asset = new DistroModule(ob.getIdentifier(), obArtifact.getHash(), obArtifact.getSize(), obArtifact.getURL(), obPath, type) - try { - let forgeData = await AssetGuard._finalizeForgeAsset(asset, self.commonPath) - resolve(forgeData) - } catch (err){ - reject(err) - } - return - } - } - } - reject('No forge module found!') - }) - } - - _parseForgeLibraries(){ - /* TODO - * Forge asset validations are already implemented. When there's nothing much - * to work on, implement forge downloads using forge's version.json. This is to - * have the code on standby if we ever need it (since it's half implemented already). - */ - } - - // #endregion - - // Java (Category=''') Validation (download) Functions - // #region - - _enqueueOpenJDK(dataDir){ - return new Promise((resolve, reject) => { - JavaGuard._latestOpenJDK('8').then(verData => { - if(verData != null){ - - dataDir = path.join(dataDir, 'runtime', 'x64') - const fDir = path.join(dataDir, verData.name) - const jre = new Asset(verData.name, null, verData.size, verData.uri, fDir) - this.java = new DLTracker([jre], jre.size, (a, self) => { - if(verData.name.endsWith('zip')){ - - const zip = new AdmZip(a.to) - const pos = path.join(dataDir, zip.getEntries()[0].entryName) - zip.extractAllToAsync(dataDir, true, (err) => { - if(err){ - console.log(err) - self.emit('complete', 'java', JavaGuard.javaExecFromRoot(pos)) - } else { - fs.unlink(a.to, err => { - if(err){ - console.log(err) - } - self.emit('complete', 'java', JavaGuard.javaExecFromRoot(pos)) - }) - } - }) - - } else { - // Tar.gz - let h = null - fs.createReadStream(a.to) - .on('error', err => console.log(err)) - .pipe(zlib.createGunzip()) - .on('error', err => console.log(err)) - .pipe(tar.extract(dataDir, { - map: (header) => { - if(h == null){ - h = header.name - } - } - })) - .on('error', err => console.log(err)) - .on('finish', () => { - fs.unlink(a.to, err => { - if(err){ - console.log(err) - } - if(h.indexOf('/') > -1){ - h = h.substring(0, h.indexOf('/')) - } - const pos = path.join(dataDir, h) - self.emit('complete', 'java', JavaGuard.javaExecFromRoot(pos)) - }) - }) - } - }) - resolve(true) - - } else { - resolve(false) - } - }) - }) - - } - - // _enqueueOracleJRE(dataDir){ - // return new Promise((resolve, reject) => { - // JavaGuard._latestJREOracle().then(verData => { - // if(verData != null){ - - // const combined = verData.uri + PLATFORM_MAP[process.platform] - - // const opts = { - // url: combined, - // headers: { - // 'Cookie': 'oraclelicense=accept-securebackup-cookie' - // } - // } - - // request.head(opts, (err, resp, body) => { - // if(err){ - // resolve(false) - // } else { - // dataDir = path.join(dataDir, 'runtime', 'x64') - // const name = combined.substring(combined.lastIndexOf('/')+1) - // const fDir = path.join(dataDir, name) - // const jre = new Asset(name, null, parseInt(resp.headers['content-length']), opts, fDir) - // this.java = new DLTracker([jre], jre.size, (a, self) => { - // let h = null - // fs.createReadStream(a.to) - // .on('error', err => console.log(err)) - // .pipe(zlib.createGunzip()) - // .on('error', err => console.log(err)) - // .pipe(tar.extract(dataDir, { - // map: (header) => { - // if(h == null){ - // h = header.name - // } - // } - // })) - // .on('error', err => console.log(err)) - // .on('finish', () => { - // fs.unlink(a.to, err => { - // if(err){ - // console.log(err) - // } - // if(h.indexOf('/') > -1){ - // h = h.substring(0, h.indexOf('/')) - // } - // const pos = path.join(dataDir, h) - // self.emit('complete', 'java', JavaGuard.javaExecFromRoot(pos)) - // }) - // }) - - // }) - // resolve(true) - // } - // }) - - // } else { - // resolve(false) - // } - // }) - // }) - - // } - - // _enqueueMojangJRE(dir){ - // return new Promise((resolve, reject) => { - // // Mojang does not host the JRE for linux. - // if(process.platform === 'linux'){ - // resolve(false) - // } - // AssetGuard.loadMojangLauncherData().then(data => { - // if(data != null) { - - // try { - // const mJRE = data[Library.mojangFriendlyOS()]['64'].jre - // const url = mJRE.url - - // request.head(url, (err, resp, body) => { - // if(err){ - // resolve(false) - // } else { - // const name = url.substring(url.lastIndexOf('/')+1) - // const fDir = path.join(dir, name) - // const jre = new Asset('jre' + mJRE.version, mJRE.sha1, resp.headers['content-length'], url, fDir) - // this.java = new DLTracker([jre], jre.size, a => { - // fs.readFile(a.to, (err, data) => { - // // Data buffer needs to be decompressed from lzma, - // // not really possible using node.js - // }) - // }) - // } - // }) - // } catch (err){ - // resolve(false) - // } - - // } - // }) - // }) - // } - - - // #endregion - - // #endregion - - // Control Flow Functions - // #region - - /** - * Initiate an async download process for an AssetGuard DLTracker. - * - * @param {string} identifier The identifier of the AssetGuard DLTracker. - * @param {number} limit Optional. The number of async processes to run in parallel. - * @returns {boolean} True if the process began, otherwise false. - */ - startAsyncProcess(identifier, limit = 5){ - - const self = this - const dlTracker = this[identifier] - const dlQueue = dlTracker.dlqueue - - if(dlQueue.length > 0){ - console.log('DLQueue', dlQueue) - - async.eachLimit(dlQueue, limit, (asset, cb) => { - - fs.ensureDirSync(path.join(asset.to, '..')) - - let req = request(asset.from) - req.pause() - - req.on('response', (resp) => { - - if(resp.statusCode === 200){ - - let doHashCheck = false - const contentLength = parseInt(resp.headers['content-length']) - - if(contentLength !== asset.size){ - console.log(`WARN: Got ${contentLength} bytes for ${asset.id}: Expected ${asset.size}`) - doHashCheck = true - - // Adjust download - this.totaldlsize -= asset.size - this.totaldlsize += contentLength - } - - let writeStream = fs.createWriteStream(asset.to) - writeStream.on('close', () => { - if(dlTracker.callback != null){ - dlTracker.callback.apply(dlTracker, [asset, self]) - } - - if(doHashCheck){ - const v = AssetGuard._validateLocal(asset.to, asset.type != null ? 'md5' : 'sha1', asset.hash) - if(v){ - console.log(`Hashes match for ${asset.id}, byte mismatch is an issue in the distro index.`) - } else { - console.error(`Hashes do not match, ${asset.id} may be corrupted.`) - } - } - - cb() - }) - req.pipe(writeStream) - req.resume() - - } else { - - req.abort() - console.log(`Failed to download ${asset.id}(${typeof asset.from === 'object' ? asset.from.url : asset.from}). Response code ${resp.statusCode}`) - self.progress += asset.size*1 - self.emit('progress', 'download', self.progress, self.totaldlsize) - cb() - - } - - }) - - req.on('error', (err) => { - self.emit('error', 'download', err) - }) - - req.on('data', (chunk) => { - self.progress += chunk.length - self.emit('progress', 'download', self.progress, self.totaldlsize) - }) - - }, (err) => { - - if(err){ - console.log('An item in ' + identifier + ' failed to process') - } else { - console.log('All ' + identifier + ' have been processed successfully') - } - - //self.totaldlsize -= dlTracker.dlsize - //self.progress -= dlTracker.dlsize - self[identifier] = new DLTracker([], 0) - - if(self.progress >= self.totaldlsize) { - if(self.extractQueue.length > 0){ - self.emit('progress', 'extract', 1, 1) - //self.emit('extracting') - AssetGuard._extractPackXZ(self.extractQueue, self.javaexec).then(() => { - self.extractQueue = [] - self.emit('complete', 'download') - }) - } else { - self.emit('complete', 'download') - } - } - - }) - - return true - - } else { - return false - } - } - - /** - * This function will initiate the download processed for the specified identifiers. If no argument is - * given, all identifiers will be initiated. Note that in order for files to be processed you need to run - * the processing function corresponding to that identifier. If you run this function without processing - * the files, it is likely nothing will be enqueued in the object and processing will complete - * immediately. Once all downloads are complete, this function will fire the 'complete' event on the - * global object instance. - * - * @param {Array.<{id: string, limit: number}>} identifiers Optional. The identifiers to process and corresponding parallel async task limit. - */ - processDlQueues(identifiers = [{id:'assets', limit:20}, {id:'libraries', limit:5}, {id:'files', limit:5}, {id:'forge', limit:5}]){ - return new Promise((resolve, reject) => { - let shouldFire = true - - // Assign dltracking variables. - this.totaldlsize = 0 - this.progress = 0 - - for(let iden of identifiers){ - this.totaldlsize += this[iden.id].dlsize - } - - this.once('complete', (data) => { - resolve() - }) - - for(let iden of identifiers){ - let r = this.startAsyncProcess(iden.id, iden.limit) - if(r) shouldFire = false - } - - if(shouldFire){ - this.emit('complete', 'download') - } - }) - } - - async validateEverything(serverid, dev = false){ - - try { - if(!ConfigManager.isLoaded()){ - ConfigManager.load() - } - DistroManager.setDevMode(dev) - const dI = await DistroManager.pullLocal() - - const server = dI.getServer(serverid) - - // Validate Everything - - await this.validateDistribution(server) - this.emit('validate', 'distribution') - const versionData = await this.loadVersionData(server.getMinecraftVersion()) - this.emit('validate', 'version') - await this.validateAssets(versionData) - this.emit('validate', 'assets') - await this.validateLibraries(versionData) - this.emit('validate', 'libraries') - await this.validateMiscellaneous(versionData) - this.emit('validate', 'files') - await this.processDlQueues() - //this.emit('complete', 'download') - const forgeData = await this.loadForgeData(server) - - return { - versionData, - forgeData - } - - } catch (err){ - return { - versionData: null, - forgeData: null, - error: err - } - } - - - } - - // #endregion - -} - -module.exports = { - Util, - AssetGuard, - JavaGuard, - Asset, - Library +// Requirements +const AdmZip = require('adm-zip') +const async = require('async') +const child_process = require('child_process') +const crypto = require('crypto') +const EventEmitter = require('events') +const fs = require('fs-extra') +const path = require('path') +const Registry = require('winreg') +const request = require('request') +const tar = require('tar-fs') +const zlib = require('zlib') + +const ConfigManager = require('./configmanager') +const DistroManager = require('./distromanager') +const isDev = require('./isdev') + +// Constants +// const PLATFORM_MAP = { +// win32: '-windows-x64.tar.gz', +// darwin: '-macosx-x64.tar.gz', +// linux: '-linux-x64.tar.gz' +// } + +// Classes + +/** Class representing a base asset. */ +class Asset { + /** + * Create an asset. + * + * @param {any} id The id of the asset. + * @param {string} hash The hash value of the asset. + * @param {number} size The size in bytes of the asset. + * @param {string} from The url where the asset can be found. + * @param {string} to The absolute local file path of the asset. + */ + constructor(id, hash, size, from, to){ + this.id = id + this.hash = hash + this.size = size + this.from = from + this.to = to + } +} + +/** Class representing a mojang library. */ +class Library extends Asset { + + /** + * Converts the process.platform OS names to match mojang's OS names. + */ + static mojangFriendlyOS(){ + const opSys = process.platform + if (opSys === 'darwin') { + return 'osx' + } else if (opSys === 'win32'){ + return 'windows' + } else if (opSys === 'linux'){ + return 'linux' + } else { + return 'unknown_os' + } + } + + /** + * Checks whether or not a library is valid for download on a particular OS, following + * the rule format specified in the mojang version data index. If the allow property has + * an OS specified, then the library can ONLY be downloaded on that OS. If the disallow + * property has instead specified an OS, the library can be downloaded on any OS EXCLUDING + * the one specified. + * + * If the rules are undefined, the natives property will be checked for a matching entry + * for the current OS. + * + * @param {Array.} rules The Library's download rules. + * @param {Object} natives The Library's natives object. + * @returns {boolean} True if the Library follows the specified rules, otherwise false. + */ + static validateRules(rules, natives){ + if(rules == null) { + if(natives == null) { + return true + } else { + return natives[Library.mojangFriendlyOS()] != null + } + } + + for(let rule of rules){ + const action = rule.action + const osProp = rule.os + if(action != null && osProp != null){ + const osName = osProp.name + const osMoj = Library.mojangFriendlyOS() + if(action === 'allow'){ + return osName === osMoj + } else if(action === 'disallow'){ + return osName !== osMoj + } + } + } + return true + } +} + +class DistroModule extends Asset { + + /** + * Create a DistroModule. This is for processing, + * not equivalent to the module objects in the + * distro index. + * + * @param {any} id The id of the asset. + * @param {string} hash The hash value of the asset. + * @param {number} size The size in bytes of the asset. + * @param {string} from The url where the asset can be found. + * @param {string} to The absolute local file path of the asset. + * @param {string} type The the module type. + */ + constructor(id, hash, size, from, to, type){ + super(id, hash, size, from, to) + this.type = type + } + +} + +/** + * Class representing a download tracker. This is used to store meta data + * about a download queue, including the queue itself. + */ +class DLTracker { + + /** + * Create a DLTracker + * + * @param {Array.} dlqueue An array containing assets queued for download. + * @param {number} dlsize The combined size of each asset in the download queue array. + * @param {function(Asset)} callback Optional callback which is called when an asset finishes downloading. + */ + constructor(dlqueue, dlsize, callback = null){ + this.dlqueue = dlqueue + this.dlsize = dlsize + this.callback = callback + } + +} + +class Util { + + /** + * Returns true if the actual version is greater than + * or equal to the desired version. + * + * @param {string} desired The desired version. + * @param {string} actual The actual version. + */ + static mcVersionAtLeast(desired, actual){ + const des = desired.split('.') + const act = actual.split('.') + + for(let i=0; i= parseInt(des[i]))){ + return false + } + } + return true + } + + static isForgeGradle3(mcVersion, forgeVersion) { + + if(Util.mcVersionAtLeast('1.13', mcVersion)) { + return true + } + + try { + + const forgeVer = forgeVersion.split('-')[1] + + const maxFG2 = [14, 23, 5, 2847] + const verSplit = forgeVer.split('.').map(v => Number(v)) + + for(let i=0; i maxFG2[i]) { + return true + } else if(verSplit[i] < maxFG2[i]) { + return false + } + } + + return false + + } catch(err) { + throw new Error('Forge version is complex (changed).. launcher requires a patch.') + } + } + + static isAutoconnectBroken(forgeVersion) { + + const minWorking = [31, 2, 15] + const verSplit = forgeVersion.split('.').map(v => Number(v)) + + if(verSplit[0] === 31) { + for(let i=0; i minWorking[i]) { + return false + } else if(verSplit[i] < minWorking[i]) { + return true + } + } + } + + return false + } + +} + + +class JavaGuard extends EventEmitter { + + constructor(mcVersion){ + super() + this.mcVersion = mcVersion + } + + // /** + // * @typedef OracleJREData + // * @property {string} uri The base uri of the JRE. + // * @property {{major: string, update: string, build: string}} version Object containing version information. + // */ + + // /** + // * Resolves the latest version of Oracle's JRE and parses its download link. + // * + // * @returns {Promise.} Promise which resolved to an object containing the JRE download data. + // */ + // static _latestJREOracle(){ + + // const url = 'https://www.oracle.com/technetwork/java/javase/downloads/jre8-downloads-2133155.html' + // const regex = /https:\/\/.+?(?=\/java)\/java\/jdk\/([0-9]+u[0-9]+)-(b[0-9]+)\/([a-f0-9]{32})?\/jre-\1/ + + // return new Promise((resolve, reject) => { + // request(url, (err, resp, body) => { + // if(!err){ + // const arr = body.match(regex) + // const verSplit = arr[1].split('u') + // resolve({ + // uri: arr[0], + // version: { + // major: verSplit[0], + // update: verSplit[1], + // build: arr[2] + // } + // }) + // } else { + // resolve(null) + // } + // }) + // }) + // } + + /** + * @typedef OpenJDKData + * @property {string} uri The base uri of the JRE. + * @property {number} size The size of the download. + * @property {string} name The name of the artifact. + */ + + /** + * Fetch the last open JDK binary. Uses https://api.adoptopenjdk.net/ + * + * @param {string} major The major version of Java to fetch. + * + * @returns {Promise.} Promise which resolved to an object containing the JRE download data. + */ + static _latestOpenJDK(major = '8'){ + + const sanitizedOS = process.platform === 'win32' ? 'windows' : (process.platform === 'darwin' ? 'mac' : process.platform) + + const url = `https://api.adoptopenjdk.net/v2/latestAssets/nightly/openjdk${major}?os=${sanitizedOS}&arch=x64&heap_size=normal&openjdk_impl=hotspot&type=jre` + + return new Promise((resolve, reject) => { + request({url, json: true}, (err, resp, body) => { + if(!err && body.length > 0){ + resolve({ + uri: body[0].binary_link, + size: body[0].binary_size, + name: body[0].binary_name + }) + } else { + resolve(null) + } + }) + }) + } + + /** + * Returns the path of the OS-specific executable for the given Java + * installation. Supported OS's are win32, darwin, linux. + * + * @param {string} rootDir The root directory of the Java installation. + * @returns {string} The path to the Java executable. + */ + static javaExecFromRoot(rootDir){ + if(process.platform === 'win32'){ + return path.join(rootDir, 'bin', 'javaw.exe') + } else if(process.platform === 'darwin'){ + return path.join(rootDir, 'Contents', 'Home', 'bin', 'java') + } else if(process.platform === 'linux'){ + return path.join(rootDir, 'bin', 'java') + } + return rootDir + } + + /** + * Check to see if the given path points to a Java executable. + * + * @param {string} pth The path to check against. + * @returns {boolean} True if the path points to a Java executable, otherwise false. + */ + static isJavaExecPath(pth){ + if(process.platform === 'win32'){ + return pth.endsWith(path.join('bin', 'javaw.exe')) + } else if(process.platform === 'darwin'){ + return pth.endsWith(path.join('bin', 'java')) + } else if(process.platform === 'linux'){ + return pth.endsWith(path.join('bin', 'java')) + } + return false + } + + /** + * Load Mojang's launcher.json file. + * + * @returns {Promise.} Promise which resolves to Mojang's launcher.json object. + */ + static loadMojangLauncherData(){ + return new Promise((resolve, reject) => { + request.get('https://launchermeta.mojang.com/mc/launcher.json', (err, resp, body) => { + if(err){ + resolve(null) + } else { + resolve(JSON.parse(body)) + } + }) + }) + } + + /** + * Parses a **full** Java Runtime version string and resolves + * the version information. Dynamically detects the formatting + * to use. + * + * @param {string} verString Full version string to parse. + * @returns Object containing the version information. + */ + static parseJavaRuntimeVersion(verString){ + const major = verString.split('.')[0] + if(major == 1){ + return JavaGuard._parseJavaRuntimeVersion_8(verString) + } else { + return JavaGuard._parseJavaRuntimeVersion_9(verString) + } + } + + /** + * Parses a **full** Java Runtime version string and resolves + * the version information. Uses Java 8 formatting. + * + * @param {string} verString Full version string to parse. + * @returns Object containing the version information. + */ + static _parseJavaRuntimeVersion_8(verString){ + // 1.{major}.0_{update}-b{build} + // ex. 1.8.0_152-b16 + const ret = {} + let pts = verString.split('-') + ret.build = parseInt(pts[1].substring(1)) + pts = pts[0].split('_') + ret.update = parseInt(pts[1]) + ret.major = parseInt(pts[0].split('.')[1]) + return ret + } + + /** + * Parses a **full** Java Runtime version string and resolves + * the version information. Uses Java 9+ formatting. + * + * @param {string} verString Full version string to parse. + * @returns Object containing the version information. + */ + static _parseJavaRuntimeVersion_9(verString){ + // {major}.{minor}.{revision}+{build} + // ex. 10.0.2+13 + const ret = {} + let pts = verString.split('+') + ret.build = parseInt(pts[1]) + pts = pts[0].split('.') + ret.major = parseInt(pts[0]) + ret.minor = parseInt(pts[1]) + ret.revision = parseInt(pts[2]) + return ret + } + + /** + * Validates the output of a JVM's properties. Currently validates that a JRE is x64 + * and that the major = 8, update > 52. + * + * @param {string} stderr The output to validate. + * + * @returns {Promise.} A promise which resolves to a meta object about the JVM. + * The validity is stored inside the `valid` property. + */ + _validateJVMProperties(stderr){ + const res = stderr + const props = res.split('\n') + + const goal = 2 + let checksum = 0 + + const meta = {} + + for(let i=0; i -1){ + let arch = props[i].split('=')[1].trim() + arch = parseInt(arch) + console.log(props[i].trim()) + if(arch === 64){ + meta.arch = arch + ++checksum + if(checksum === goal){ + break + } + } + } else if(props[i].indexOf('java.runtime.version') > -1){ + let verString = props[i].split('=')[1].trim() + console.log(props[i].trim()) + const verOb = JavaGuard.parseJavaRuntimeVersion(verString) + if(verOb.major < 9){ + // Java 8 + if(verOb.major === 8 && verOb.update > 52){ + meta.version = verOb + ++checksum + if(checksum === goal){ + break + } + } + } else { + // Java 9+ + if(Util.mcVersionAtLeast('1.13', this.mcVersion)){ + console.log('Java 9+ not yet tested.') + /* meta.version = verOb + ++checksum + if(checksum === goal){ + break + } */ + } + } + } + } + + meta.valid = checksum === goal + + return meta + } + + /** + * Validates that a Java binary is at least 64 bit. This makes use of the non-standard + * command line option -XshowSettings:properties. The output of this contains a property, + * sun.arch.data.model = ARCH, in which ARCH is either 32 or 64. This option is supported + * in Java 8 and 9. Since this is a non-standard option. This will resolve to true if + * the function's code throws errors. That would indicate that the option is changed or + * removed. + * + * @param {string} binaryExecPath Path to the java executable we wish to validate. + * + * @returns {Promise.} A promise which resolves to a meta object about the JVM. + * The validity is stored inside the `valid` property. + */ + _validateJavaBinary(binaryExecPath){ + + return new Promise((resolve, reject) => { + if(!JavaGuard.isJavaExecPath(binaryExecPath)){ + resolve({valid: false}) + } else if(fs.existsSync(binaryExecPath)){ + // Workaround (javaw.exe no longer outputs this information.) + console.log(typeof binaryExecPath) + if(binaryExecPath.indexOf('javaw.exe') > -1) { + binaryExecPath.replace('javaw.exe', 'java.exe') + } + child_process.exec('"' + binaryExecPath + '" -XshowSettings:properties', (err, stdout, stderr) => { + try { + // Output is stored in stderr? + resolve(this._validateJVMProperties(stderr)) + } catch (err){ + // Output format might have changed, validation cannot be completed. + resolve({valid: false}) + } + }) + } else { + resolve({valid: false}) + } + }) + + } + + /** + * Checks for the presence of the environment variable JAVA_HOME. If it exits, we will check + * to see if the value points to a path which exists. If the path exits, the path is returned. + * + * @returns {string} The path defined by JAVA_HOME, if it exists. Otherwise null. + */ + static _scanJavaHome(){ + const jHome = process.env.JAVA_HOME + try { + let res = fs.existsSync(jHome) + return res ? jHome : null + } catch (err) { + // Malformed JAVA_HOME property. + return null + } + } + + /** + * Scans the registry for 64-bit Java entries. The paths of each entry are added to + * a set and returned. Currently, only Java 8 (1.8) is supported. + * + * @returns {Promise.>} A promise which resolves to a set of 64-bit Java root + * paths found in the registry. + */ + static _scanRegistry(){ + + return new Promise((resolve, reject) => { + // Keys for Java v9.0.0 and later: + // 'SOFTWARE\\JavaSoft\\JRE' + // 'SOFTWARE\\JavaSoft\\JDK' + // Forge does not yet support Java 9, therefore we do not. + + // Keys for Java 1.8 and prior: + const regKeys = [ + '\\SOFTWARE\\JavaSoft\\Java Runtime Environment', + '\\SOFTWARE\\JavaSoft\\Java Development Kit' + ] + + let keysDone = 0 + + const candidates = new Set() + + for(let i=0; i { + if(exists) { + key.keys((err, javaVers) => { + if(err){ + keysDone++ + console.error(err) + + // REG KEY DONE + // DUE TO ERROR + if(keysDone === regKeys.length){ + resolve(candidates) + } + } else { + if(javaVers.length === 0){ + // REG KEY DONE + // NO SUBKEYS + keysDone++ + if(keysDone === regKeys.length){ + resolve(candidates) + } + } else { + + let numDone = 0 + + for(let j=0; j { + const jHome = res.value + if(jHome.indexOf('(x86)') === -1){ + candidates.add(jHome) + } + + // SUBKEY DONE + + numDone++ + if(numDone === javaVers.length){ + keysDone++ + if(keysDone === regKeys.length){ + resolve(candidates) + } + } + }) + } else { + + // SUBKEY DONE + // NOT JAVA 8 + + numDone++ + if(numDone === javaVers.length){ + keysDone++ + if(keysDone === regKeys.length){ + resolve(candidates) + } + } + } + } + } + } + }) + } else { + + // REG KEY DONE + // DUE TO NON-EXISTANCE + + keysDone++ + if(keysDone === regKeys.length){ + resolve(candidates) + } + } + }) + } + + }) + + } + + /** + * See if JRE exists in the Internet Plug-Ins folder. + * + * @returns {string} The path of the JRE if found, otherwise null. + */ + static _scanInternetPlugins(){ + // /Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java + const pth = '/Library/Internet Plug-Ins/JavaAppletPlugin.plugin' + const res = fs.existsSync(JavaGuard.javaExecFromRoot(pth)) + return res ? pth : null + } + + /** + * Scan a directory for root JVM folders. + * + * @param {string} scanDir The directory to scan. + * @returns {Promise.>} A promise which resolves to a set of the discovered + * root JVM folders. + */ + static _scanFileSystem(scanDir){ + return new Promise((resolve, reject) => { + + fs.exists(scanDir, (e) => { + + let res = new Set() + + if(e){ + fs.readdir(scanDir, (err, files) => { + if(err){ + resolve(res) + console.log(err) + } else { + let pathsDone = 0 + + for(let i=0; i { + + if(v){ + res.add(combinedPath) + } + + ++pathsDone + + if(pathsDone === files.length){ + resolve(res) + } + + }) + } + if(pathsDone === files.length){ + resolve(res) + } + } + }) + } else { + resolve(res) + } + }) + + }) + } + + /** + * + * @param {Set.} rootSet A set of JVM root strings to validate. + * @returns {Promise.} A promise which resolves to an array of meta objects + * for each valid JVM root directory. + */ + async _validateJavaRootSet(rootSet){ + + const rootArr = Array.from(rootSet) + const validArr = [] + + for(let i=0; i { + + if(a.version.major === b.version.major){ + + if(a.version.major < 9){ + // Java 8 + if(a.version.update === b.version.update){ + if(a.version.build === b.version.build){ + + // Same version, give priority to JRE. + if(a.execPath.toLowerCase().indexOf('jdk') > -1){ + return b.execPath.toLowerCase().indexOf('jdk') > -1 ? 0 : 1 + } else { + return -1 + } + + } else { + return a.version.build > b.version.build ? -1 : 1 + } + } else { + return a.version.update > b.version.update ? -1 : 1 + } + } else { + // Java 9+ + if(a.version.minor === b.version.minor){ + if(a.version.revision === b.version.revision){ + + // Same version, give priority to JRE. + if(a.execPath.toLowerCase().indexOf('jdk') > -1){ + return b.execPath.toLowerCase().indexOf('jdk') > -1 ? 0 : 1 + } else { + return -1 + } + + } else { + return a.version.revision > b.version.revision ? -1 : 1 + } + } else { + return a.version.minor > b.version.minor ? -1 : 1 + } + } + + } else { + return a.version.major > b.version.major ? -1 : 1 + } + }) + + return retArr + } + + /** + * Attempts to find a valid x64 installation of Java on Windows machines. + * Possible paths will be pulled from the registry and the JAVA_HOME environment + * variable. The paths will be sorted with higher versions preceeding lower, and + * JREs preceeding JDKs. The binaries at the sorted paths will then be validated. + * The first validated is returned. + * + * Higher versions > Lower versions + * If versions are equal, JRE > JDK. + * + * @param {string} dataDir The base launcher directory. + * @returns {Promise.} A Promise which resolves to the executable path of a valid + * x64 Java installation. If none are found, null is returned. + */ + async _win32JavaValidate(dataDir){ + + // Get possible paths from the registry. + let pathSet1 = await JavaGuard._scanRegistry() + if(pathSet1.length === 0){ + // Do a manual file system scan of program files. + pathSet1 = JavaGuard._scanFileSystem('C:\\Program Files\\Java') + } + + // Get possible paths from the data directory. + const pathSet2 = await JavaGuard._scanFileSystem(path.join(dataDir, 'runtime', 'x64')) + + // Merge the results. + const uberSet = new Set([...pathSet1, ...pathSet2]) + + // Validate JAVA_HOME. + const jHome = JavaGuard._scanJavaHome() + if(jHome != null && jHome.indexOf('(x86)') === -1){ + uberSet.add(jHome) + } + + let pathArr = await this._validateJavaRootSet(uberSet) + pathArr = JavaGuard._sortValidJavaArray(pathArr) + + if(pathArr.length > 0){ + return pathArr[0].execPath + } else { + return null + } + + } + + /** + * Attempts to find a valid x64 installation of Java on MacOS. + * The system JVM directory is scanned for possible installations. + * The JAVA_HOME enviroment variable and internet plugins directory + * are also scanned and validated. + * + * Higher versions > Lower versions + * If versions are equal, JRE > JDK. + * + * @param {string} dataDir The base launcher directory. + * @returns {Promise.} A Promise which resolves to the executable path of a valid + * x64 Java installation. If none are found, null is returned. + */ + async _darwinJavaValidate(dataDir){ + + const pathSet1 = await JavaGuard._scanFileSystem('/Library/Java/JavaVirtualMachines') + const pathSet2 = await JavaGuard._scanFileSystem(path.join(dataDir, 'runtime', 'x64')) + + const uberSet = new Set([...pathSet1, ...pathSet2]) + + // Check Internet Plugins folder. + const iPPath = JavaGuard._scanInternetPlugins() + if(iPPath != null){ + uberSet.add(iPPath) + } + + // Check the JAVA_HOME environment variable. + let jHome = JavaGuard._scanJavaHome() + if(jHome != null){ + // Ensure we are at the absolute root. + if(jHome.contains('/Contents/Home')){ + jHome = jHome.substring(0, jHome.indexOf('/Contents/Home')) + } + uberSet.add(jHome) + } + + let pathArr = await this._validateJavaRootSet(uberSet) + pathArr = JavaGuard._sortValidJavaArray(pathArr) + + if(pathArr.length > 0){ + return pathArr[0].execPath + } else { + return null + } + } + + /** + * Attempts to find a valid x64 installation of Java on Linux. + * The system JVM directory is scanned for possible installations. + * The JAVA_HOME enviroment variable is also scanned and validated. + * + * Higher versions > Lower versions + * If versions are equal, JRE > JDK. + * + * @param {string} dataDir The base launcher directory. + * @returns {Promise.} A Promise which resolves to the executable path of a valid + * x64 Java installation. If none are found, null is returned. + */ + async _linuxJavaValidate(dataDir){ + + const pathSet1 = await JavaGuard._scanFileSystem('/usr/lib/jvm') + const pathSet2 = await JavaGuard._scanFileSystem(path.join(dataDir, 'runtime', 'x64')) + + const uberSet = new Set([...pathSet1, ...pathSet2]) + + // Validate JAVA_HOME + const jHome = JavaGuard._scanJavaHome() + if(jHome != null){ + uberSet.add(jHome) + } + + let pathArr = await this._validateJavaRootSet(uberSet) + pathArr = JavaGuard._sortValidJavaArray(pathArr) + + if(pathArr.length > 0){ + return pathArr[0].execPath + } else { + return null + } + } + + /** + * Retrieve the path of a valid x64 Java installation. + * + * @param {string} dataDir The base launcher directory. + * @returns {string} A path to a valid x64 Java installation, null if none found. + */ + async validateJava(dataDir){ + return await this['_' + process.platform + 'JavaValidate'](dataDir) + } + +} + + + + +/** + * Central object class used for control flow. This object stores data about + * categories of downloads. Each category is assigned an identifier with a + * DLTracker object as its value. Combined information is also stored, such as + * the total size of all the queued files in each category. This event is used + * to emit events so that external modules can listen into processing done in + * this module. + */ +class AssetGuard extends EventEmitter { + + /** + * Create an instance of AssetGuard. + * On creation the object's properties are never-null default + * values. Each identifier is resolved to an empty DLTracker. + * + * @param {string} commonPath The common path for shared game files. + * @param {string} javaexec The path to a java executable which will be used + * to finalize installation. + */ + constructor(commonPath, javaexec){ + super() + this.totaldlsize = 0 + this.progress = 0 + this.assets = new DLTracker([], 0) + this.libraries = new DLTracker([], 0) + this.files = new DLTracker([], 0) + this.forge = new DLTracker([], 0) + this.java = new DLTracker([], 0) + this.extractQueue = [] + this.commonPath = commonPath + this.javaexec = javaexec + } + + // Static Utility Functions + // #region + + // Static Hash Validation Functions + // #region + + /** + * Calculates the hash for a file using the specified algorithm. + * + * @param {Buffer} buf The buffer containing file data. + * @param {string} algo The hash algorithm. + * @returns {string} The calculated hash in hex. + */ + static _calculateHash(buf, algo){ + return crypto.createHash(algo).update(buf).digest('hex') + } + + /** + * Used to parse a checksums file. This is specifically designed for + * the checksums.sha1 files found inside the forge scala dependencies. + * + * @param {string} content The string content of the checksums file. + * @returns {Object} An object with keys being the file names, and values being the hashes. + */ + static _parseChecksumsFile(content){ + let finalContent = {} + let lines = content.split('\n') + for(let i=0; i} checksums The checksums listed in the forge version index. + * @returns {boolean} True if the file exists and the hashes match, otherwise false. + */ + static _validateForgeChecksum(filePath, checksums){ + if(fs.existsSync(filePath)){ + if(checksums == null || checksums.length === 0){ + return true + } + let buf = fs.readFileSync(filePath) + let calcdhash = AssetGuard._calculateHash(buf, 'sha1') + let valid = checksums.includes(calcdhash) + if(!valid && filePath.endsWith('.jar')){ + valid = AssetGuard._validateForgeJar(filePath, checksums) + } + return valid + } + return false + } + + /** + * Validates a forge jar file dependency who declares a checksums.sha1 file. + * This can be an expensive task as it usually requires that we calculate thousands + * of hashes. + * + * @param {Buffer} buf The buffer of the jar file. + * @param {Array.} checksums The checksums listed in the forge version index. + * @returns {boolean} True if all hashes declared in the checksums.sha1 file match the actual hashes. + */ + static _validateForgeJar(buf, checksums){ + // Double pass method was the quickest I found. I tried a version where we store data + // to only require a single pass, plus some quick cleanup but that seemed to take slightly more time. + + const hashes = {} + let expected = {} + + const zip = new AdmZip(buf) + const zipEntries = zip.getEntries() + + //First pass + for(let i=0; i} filePaths The paths of the files to be extracted and unpacked. + * @returns {Promise.} An empty promise to indicate the extraction has completed. + */ + static _extractPackXZ(filePaths, javaExecutable){ + console.log('[PackXZExtract] Starting') + return new Promise((resolve, reject) => { + + let libPath + if(isDev){ + libPath = path.join(process.cwd(), 'libraries', 'java', 'PackXZExtract.jar') + } else { + if(process.platform === 'darwin'){ + libPath = path.join(process.cwd(),'Contents', 'Resources', 'libraries', 'java', 'PackXZExtract.jar') + } else { + libPath = path.join(process.cwd(), 'resources', 'libraries', 'java', 'PackXZExtract.jar') + } + } + + const filePath = filePaths.join(',') + const child = child_process.spawn(javaExecutable, ['-jar', libPath, '-packxz', filePath]) + child.stdout.on('data', (data) => { + console.log('[PackXZExtract]', data.toString('utf8')) + }) + child.stderr.on('data', (data) => { + console.log('[PackXZExtract]', data.toString('utf8')) + }) + child.on('close', (code, signal) => { + console.log('[PackXZExtract]', 'Exited with code', code) + resolve() + }) + }) + } + + /** + * Function which finalizes the forge installation process. This creates a 'version' + * instance for forge and saves its version.json file into that instance. If that + * instance already exists, the contents of the version.json file are read and returned + * in a promise. + * + * @param {Asset} asset The Asset object representing Forge. + * @param {string} commonPath The common path for shared game files. + * @returns {Promise.} A promise which resolves to the contents of forge's version.json. + */ + static _finalizeForgeAsset(asset, commonPath){ + return new Promise((resolve, reject) => { + fs.readFile(asset.to, (err, data) => { + const zip = new AdmZip(data) + const zipEntries = zip.getEntries() + + for(let i=0; i} Promise which resolves to the version data object. + */ + loadVersionData(version, force = false){ + const self = this + return new Promise(async (resolve, reject) => { + const versionPath = path.join(self.commonPath, 'versions', version) + const versionFile = path.join(versionPath, version + '.json') + if(!fs.existsSync(versionFile) || force){ + const url = await self._getVersionDataUrl(version) + //This download will never be tracked as it's essential and trivial. + console.log('Preparing download of ' + version + ' assets.') + fs.ensureDirSync(versionPath) + const stream = request(url).pipe(fs.createWriteStream(versionFile)) + stream.on('finish', () => { + resolve(JSON.parse(fs.readFileSync(versionFile))) + }) + } else { + resolve(JSON.parse(fs.readFileSync(versionFile))) + } + }) + } + + /** + * Parses Mojang's version manifest and retrieves the url of the version + * data index. + * + * @param {string} version The version to lookup. + * @returns {Promise.} Promise which resolves to the url of the version data index. + * If the version could not be found, resolves to null. + */ + _getVersionDataUrl(version){ + return new Promise((resolve, reject) => { + request('https://launchermeta.mojang.com/mc/game/version_manifest.json', (error, resp, body) => { + if(error){ + reject(error) + } else { + const manifest = JSON.parse(body) + + for(let v of manifest.versions){ + if(v.id === version){ + resolve(v.url) + } + } + + resolve(null) + } + }) + }) + } + + + // Asset (Category=''') Validation Functions + // #region + + /** + * Public asset validation function. This function will handle the validation of assets. + * It will parse the asset index specified in the version data, analyzing each + * asset entry. In this analysis it will check to see if the local file exists and is valid. + * If not, it will be added to the download queue for the 'assets' identifier. + * + * @param {Object} versionData The version data for the assets. + * @param {boolean} force Optional. If true, the asset index will be downloaded even if it exists locally. Defaults to false. + * @returns {Promise.} An empty promise to indicate the async processing has completed. + */ + validateAssets(versionData, force = false){ + const self = this + return new Promise((resolve, reject) => { + self._assetChainIndexData(versionData, force).then(() => { + resolve() + }) + }) + } + + //Chain the asset tasks to provide full async. The below functions are private. + /** + * Private function used to chain the asset validation process. This function retrieves + * the index data. + * @param {Object} versionData + * @param {boolean} force + * @returns {Promise.} An empty promise to indicate the async processing has completed. + */ + _assetChainIndexData(versionData, force = false){ + const self = this + return new Promise((resolve, reject) => { + //Asset index constants. + const assetIndex = versionData.assetIndex + const name = assetIndex.id + '.json' + const indexPath = path.join(self.commonPath, 'assets', 'indexes') + const assetIndexLoc = path.join(indexPath, name) + + let data = null + if(!fs.existsSync(assetIndexLoc) || force){ + console.log('Downloading ' + versionData.id + ' asset index.') + fs.ensureDirSync(indexPath) + const stream = request(assetIndex.url).pipe(fs.createWriteStream(assetIndexLoc)) + stream.on('finish', () => { + data = JSON.parse(fs.readFileSync(assetIndexLoc, 'utf-8')) + self._assetChainValidateAssets(versionData, data).then(() => { + resolve() + }) + }) + } else { + data = JSON.parse(fs.readFileSync(assetIndexLoc, 'utf-8')) + self._assetChainValidateAssets(versionData, data).then(() => { + resolve() + }) + } + }) + } + + /** + * Private function used to chain the asset validation process. This function processes + * the assets and enqueues missing or invalid files. + * @param {Object} versionData + * @param {boolean} force + * @returns {Promise.} An empty promise to indicate the async processing has completed. + */ + _assetChainValidateAssets(versionData, indexData){ + const self = this + return new Promise((resolve, reject) => { + + //Asset constants + const resourceURL = 'http://resources.download.minecraft.net/' + const localPath = path.join(self.commonPath, 'assets') + const objectPath = path.join(localPath, 'objects') + + const assetDlQueue = [] + let dlSize = 0 + let acc = 0 + const total = Object.keys(indexData.objects).length + //const objKeys = Object.keys(data.objects) + async.forEachOfLimit(indexData.objects, 10, (value, key, cb) => { + acc++ + self.emit('progress', 'assets', acc, total) + const hash = value.hash + const assetName = path.join(hash.substring(0, 2), hash) + const urlName = hash.substring(0, 2) + '/' + hash + const ast = new Asset(key, hash, value.size, resourceURL + urlName, path.join(objectPath, assetName)) + if(!AssetGuard._validateLocal(ast.to, 'sha1', ast.hash)){ + dlSize += (ast.size*1) + assetDlQueue.push(ast) + } + cb() + }, (err) => { + self.assets = new DLTracker(assetDlQueue, dlSize) + resolve() + }) + }) + } + + // #endregion + + // Library (Category=''') Validation Functions + // #region + + /** + * Public library validation function. This function will handle the validation of libraries. + * It will parse the version data, analyzing each library entry. In this analysis, it will + * check to see if the local file exists and is valid. If not, it will be added to the download + * queue for the 'libraries' identifier. + * + * @param {Object} versionData The version data for the assets. + * @returns {Promise.} An empty promise to indicate the async processing has completed. + */ + validateLibraries(versionData){ + const self = this + return new Promise((resolve, reject) => { + + const libArr = versionData.libraries + const libPath = path.join(self.commonPath, 'libraries') + + const libDlQueue = [] + let dlSize = 0 + + //Check validity of each library. If the hashs don't match, download the library. + async.eachLimit(libArr, 5, (lib, cb) => { + if(Library.validateRules(lib.rules, lib.natives)){ + let artifact = (lib.natives == null) ? lib.downloads.artifact : lib.downloads.classifiers[lib.natives[Library.mojangFriendlyOS()].replace('${arch}', process.arch.replace('x', ''))] + const libItm = new Library(lib.name, artifact.sha1, artifact.size, artifact.url, path.join(libPath, artifact.path)) + if(!AssetGuard._validateLocal(libItm.to, 'sha1', libItm.hash)){ + dlSize += (libItm.size*1) + libDlQueue.push(libItm) + } + } + cb() + }, (err) => { + self.libraries = new DLTracker(libDlQueue, dlSize) + resolve() + }) + }) + } + + // #endregion + + // Miscellaneous (Category=files) Validation Functions + // #region + + /** + * Public miscellaneous mojang file validation function. These files will be enqueued under + * the 'files' identifier. + * + * @param {Object} versionData The version data for the assets. + * @returns {Promise.} An empty promise to indicate the async processing has completed. + */ + validateMiscellaneous(versionData){ + const self = this + return new Promise(async (resolve, reject) => { + await self.validateClient(versionData) + await self.validateLogConfig(versionData) + resolve() + }) + } + + /** + * Validate client file - artifact renamed from client.jar to '{version}'.jar. + * + * @param {Object} versionData The version data for the assets. + * @param {boolean} force Optional. If true, the asset index will be downloaded even if it exists locally. Defaults to false. + * @returns {Promise.} An empty promise to indicate the async processing has completed. + */ + validateClient(versionData, force = false){ + const self = this + return new Promise((resolve, reject) => { + const clientData = versionData.downloads.client + const version = versionData.id + const targetPath = path.join(self.commonPath, 'versions', version) + const targetFile = version + '.jar' + + let client = new Asset(version + ' client', clientData.sha1, clientData.size, clientData.url, path.join(targetPath, targetFile)) + + if(!AssetGuard._validateLocal(client.to, 'sha1', client.hash) || force){ + self.files.dlqueue.push(client) + self.files.dlsize += client.size*1 + resolve() + } else { + resolve() + } + }) + } + + /** + * Validate log config. + * + * @param {Object} versionData The version data for the assets. + * @param {boolean} force Optional. If true, the asset index will be downloaded even if it exists locally. Defaults to false. + * @returns {Promise.} An empty promise to indicate the async processing has completed. + */ + validateLogConfig(versionData){ + const self = this + return new Promise((resolve, reject) => { + const client = versionData.logging.client + const file = client.file + const targetPath = path.join(self.commonPath, 'assets', 'log_configs') + + let logConfig = new Asset(file.id, file.sha1, file.size, file.url, path.join(targetPath, file.id)) + + if(!AssetGuard._validateLocal(logConfig.to, 'sha1', logConfig.hash)){ + self.files.dlqueue.push(logConfig) + self.files.dlsize += logConfig.size*1 + resolve() + } else { + resolve() + } + }) + } + + // #endregion + + // Distribution (Category=forge) Validation Functions + // #region + + /** + * Validate the distribution. + * + * @param {Server} server The Server to validate. + * @returns {Promise.} A promise which resolves to the server distribution object. + */ + validateDistribution(server){ + const self = this + return new Promise((resolve, reject) => { + self.forge = self._parseDistroModules(server.getModules(), server.getMinecraftVersion(), server.getID()) + resolve(server) + }) + } + + _parseDistroModules(modules, version, servid){ + let alist = [] + let asize = 0 + for(let ob of modules){ + let obArtifact = ob.getArtifact() + let obPath = obArtifact.getPath() + let artifact = new DistroModule(ob.getIdentifier(), obArtifact.getHash(), obArtifact.getSize(), obArtifact.getURL(), obPath, ob.getType()) + const validationPath = obPath.toLowerCase().endsWith('.pack.xz') ? obPath.substring(0, obPath.toLowerCase().lastIndexOf('.pack.xz')) : obPath + if(!AssetGuard._validateLocal(validationPath, 'MD5', artifact.hash)){ + asize += artifact.size*1 + alist.push(artifact) + if(validationPath !== obPath) this.extractQueue.push(obPath) + } + //Recursively process the submodules then combine the results. + if(ob.getSubModules() != null){ + let dltrack = this._parseDistroModules(ob.getSubModules(), version, servid) + asize += dltrack.dlsize*1 + alist = alist.concat(dltrack.dlqueue) + } + } + + return new DLTracker(alist, asize) + } + + /** + * Loads Forge's version.json data into memory for the specified server id. + * + * @param {string} server The Server to load Forge data for. + * @returns {Promise.} A promise which resolves to Forge's version.json data. + */ + loadForgeData(server){ + const self = this + return new Promise(async (resolve, reject) => { + const modules = server.getModules() + for(let ob of modules){ + const type = ob.getType() + if(type === DistroManager.Types.ForgeHosted || type === DistroManager.Types.Forge){ + if(Util.isForgeGradle3(server.getMinecraftVersion(), ob.getVersion())){ + // Read Manifest + for(let sub of ob.getSubModules()){ + if(sub.getType() === DistroManager.Types.VersionManifest){ + resolve(JSON.parse(fs.readFileSync(sub.getArtifact().getPath(), 'utf-8'))) + return + } + } + reject('No forge version manifest found!') + return + } else { + let obArtifact = ob.getArtifact() + let obPath = obArtifact.getPath() + let asset = new DistroModule(ob.getIdentifier(), obArtifact.getHash(), obArtifact.getSize(), obArtifact.getURL(), obPath, type) + try { + let forgeData = await AssetGuard._finalizeForgeAsset(asset, self.commonPath) + resolve(forgeData) + } catch (err){ + reject(err) + } + return + } + } + } + reject('No forge module found!') + }) + } + + _parseForgeLibraries(){ + /* TODO + * Forge asset validations are already implemented. When there's nothing much + * to work on, implement forge downloads using forge's version.json. This is to + * have the code on standby if we ever need it (since it's half implemented already). + */ + } + + // #endregion + + // Java (Category=''') Validation (download) Functions + // #region + + _enqueueOpenJDK(dataDir){ + return new Promise((resolve, reject) => { + JavaGuard._latestOpenJDK('8').then(verData => { + if(verData != null){ + + dataDir = path.join(dataDir, 'runtime', 'x64') + const fDir = path.join(dataDir, verData.name) + const jre = new Asset(verData.name, null, verData.size, verData.uri, fDir) + this.java = new DLTracker([jre], jre.size, (a, self) => { + if(verData.name.endsWith('zip')){ + + const zip = new AdmZip(a.to) + const pos = path.join(dataDir, zip.getEntries()[0].entryName) + zip.extractAllToAsync(dataDir, true, (err) => { + if(err){ + console.log(err) + self.emit('complete', 'java', JavaGuard.javaExecFromRoot(pos)) + } else { + fs.unlink(a.to, err => { + if(err){ + console.log(err) + } + self.emit('complete', 'java', JavaGuard.javaExecFromRoot(pos)) + }) + } + }) + + } else { + // Tar.gz + let h = null + fs.createReadStream(a.to) + .on('error', err => console.log(err)) + .pipe(zlib.createGunzip()) + .on('error', err => console.log(err)) + .pipe(tar.extract(dataDir, { + map: (header) => { + if(h == null){ + h = header.name + } + } + })) + .on('error', err => console.log(err)) + .on('finish', () => { + fs.unlink(a.to, err => { + if(err){ + console.log(err) + } + if(h.indexOf('/') > -1){ + h = h.substring(0, h.indexOf('/')) + } + const pos = path.join(dataDir, h) + self.emit('complete', 'java', JavaGuard.javaExecFromRoot(pos)) + }) + }) + } + }) + resolve(true) + + } else { + resolve(false) + } + }) + }) + + } + + // _enqueueOracleJRE(dataDir){ + // return new Promise((resolve, reject) => { + // JavaGuard._latestJREOracle().then(verData => { + // if(verData != null){ + + // const combined = verData.uri + PLATFORM_MAP[process.platform] + + // const opts = { + // url: combined, + // headers: { + // 'Cookie': 'oraclelicense=accept-securebackup-cookie' + // } + // } + + // request.head(opts, (err, resp, body) => { + // if(err){ + // resolve(false) + // } else { + // dataDir = path.join(dataDir, 'runtime', 'x64') + // const name = combined.substring(combined.lastIndexOf('/')+1) + // const fDir = path.join(dataDir, name) + // const jre = new Asset(name, null, parseInt(resp.headers['content-length']), opts, fDir) + // this.java = new DLTracker([jre], jre.size, (a, self) => { + // let h = null + // fs.createReadStream(a.to) + // .on('error', err => console.log(err)) + // .pipe(zlib.createGunzip()) + // .on('error', err => console.log(err)) + // .pipe(tar.extract(dataDir, { + // map: (header) => { + // if(h == null){ + // h = header.name + // } + // } + // })) + // .on('error', err => console.log(err)) + // .on('finish', () => { + // fs.unlink(a.to, err => { + // if(err){ + // console.log(err) + // } + // if(h.indexOf('/') > -1){ + // h = h.substring(0, h.indexOf('/')) + // } + // const pos = path.join(dataDir, h) + // self.emit('complete', 'java', JavaGuard.javaExecFromRoot(pos)) + // }) + // }) + + // }) + // resolve(true) + // } + // }) + + // } else { + // resolve(false) + // } + // }) + // }) + + // } + + // _enqueueMojangJRE(dir){ + // return new Promise((resolve, reject) => { + // // Mojang does not host the JRE for linux. + // if(process.platform === 'linux'){ + // resolve(false) + // } + // AssetGuard.loadMojangLauncherData().then(data => { + // if(data != null) { + + // try { + // const mJRE = data[Library.mojangFriendlyOS()]['64'].jre + // const url = mJRE.url + + // request.head(url, (err, resp, body) => { + // if(err){ + // resolve(false) + // } else { + // const name = url.substring(url.lastIndexOf('/')+1) + // const fDir = path.join(dir, name) + // const jre = new Asset('jre' + mJRE.version, mJRE.sha1, resp.headers['content-length'], url, fDir) + // this.java = new DLTracker([jre], jre.size, a => { + // fs.readFile(a.to, (err, data) => { + // // Data buffer needs to be decompressed from lzma, + // // not really possible using node.js + // }) + // }) + // } + // }) + // } catch (err){ + // resolve(false) + // } + + // } + // }) + // }) + // } + + + // #endregion + + // #endregion + + // Control Flow Functions + // #region + + /** + * Initiate an async download process for an AssetGuard DLTracker. + * + * @param {string} identifier The identifier of the AssetGuard DLTracker. + * @param {number} limit Optional. The number of async processes to run in parallel. + * @returns {boolean} True if the process began, otherwise false. + */ + startAsyncProcess(identifier, limit = 5){ + + const self = this + const dlTracker = this[identifier] + const dlQueue = dlTracker.dlqueue + + if(dlQueue.length > 0){ + console.log('DLQueue', dlQueue) + + async.eachLimit(dlQueue, limit, (asset, cb) => { + + fs.ensureDirSync(path.join(asset.to, '..')) + + let req = request(asset.from) + req.pause() + + req.on('response', (resp) => { + + if(resp.statusCode === 200){ + + let doHashCheck = false + const contentLength = parseInt(resp.headers['content-length']) + + if(contentLength !== asset.size){ + console.log(`WARN: Got ${contentLength} bytes for ${asset.id}: Expected ${asset.size}`) + doHashCheck = true + + // Adjust download + this.totaldlsize -= asset.size + this.totaldlsize += contentLength + } + + let writeStream = fs.createWriteStream(asset.to) + writeStream.on('close', () => { + if(dlTracker.callback != null){ + dlTracker.callback.apply(dlTracker, [asset, self]) + } + + if(doHashCheck){ + const v = AssetGuard._validateLocal(asset.to, asset.type != null ? 'md5' : 'sha1', asset.hash) + if(v){ + console.log(`Hashes match for ${asset.id}, byte mismatch is an issue in the distro index.`) + } else { + console.error(`Hashes do not match, ${asset.id} may be corrupted.`) + } + } + + cb() + }) + req.pipe(writeStream) + req.resume() + + } else { + + req.abort() + console.log(`Failed to download ${asset.id}(${typeof asset.from === 'object' ? asset.from.url : asset.from}). Response code ${resp.statusCode}`) + self.progress += asset.size*1 + self.emit('progress', 'download', self.progress, self.totaldlsize) + cb() + + } + + }) + + req.on('error', (err) => { + self.emit('error', 'download', err) + }) + + req.on('data', (chunk) => { + self.progress += chunk.length + self.emit('progress', 'download', self.progress, self.totaldlsize) + }) + + }, (err) => { + + if(err){ + console.log('An item in ' + identifier + ' failed to process') + } else { + console.log('All ' + identifier + ' have been processed successfully') + } + + //self.totaldlsize -= dlTracker.dlsize + //self.progress -= dlTracker.dlsize + self[identifier] = new DLTracker([], 0) + + if(self.progress >= self.totaldlsize) { + if(self.extractQueue.length > 0){ + self.emit('progress', 'extract', 1, 1) + //self.emit('extracting') + AssetGuard._extractPackXZ(self.extractQueue, self.javaexec).then(() => { + self.extractQueue = [] + self.emit('complete', 'download') + }) + } else { + self.emit('complete', 'download') + } + } + + }) + + return true + + } else { + return false + } + } + + /** + * This function will initiate the download processed for the specified identifiers. If no argument is + * given, all identifiers will be initiated. Note that in order for files to be processed you need to run + * the processing function corresponding to that identifier. If you run this function without processing + * the files, it is likely nothing will be enqueued in the object and processing will complete + * immediately. Once all downloads are complete, this function will fire the 'complete' event on the + * global object instance. + * + * @param {Array.<{id: string, limit: number}>} identifiers Optional. The identifiers to process and corresponding parallel async task limit. + */ + processDlQueues(identifiers = [{id:'assets', limit:20}, {id:'libraries', limit:5}, {id:'files', limit:5}, {id:'forge', limit:5}]){ + return new Promise((resolve, reject) => { + let shouldFire = true + + // Assign dltracking variables. + this.totaldlsize = 0 + this.progress = 0 + + for(let iden of identifiers){ + this.totaldlsize += this[iden.id].dlsize + } + + this.once('complete', (data) => { + resolve() + }) + + for(let iden of identifiers){ + let r = this.startAsyncProcess(iden.id, iden.limit) + if(r) shouldFire = false + } + + if(shouldFire){ + this.emit('complete', 'download') + } + }) + } + + async validateEverything(serverid, dev = false){ + + try { + if(!ConfigManager.isLoaded()){ + ConfigManager.load() + } + DistroManager.setDevMode(dev) + const dI = await DistroManager.pullLocal() + + const server = dI.getServer(serverid) + + // Validate Everything + + await this.validateDistribution(server) + this.emit('validate', 'distribution') + const versionData = await this.loadVersionData(server.getMinecraftVersion()) + this.emit('validate', 'version') + await this.validateAssets(versionData) + this.emit('validate', 'assets') + await this.validateLibraries(versionData) + this.emit('validate', 'libraries') + await this.validateMiscellaneous(versionData) + this.emit('validate', 'files') + await this.processDlQueues() + //this.emit('complete', 'download') + const forgeData = await this.loadForgeData(server) + + return { + versionData, + forgeData + } + + } catch (err){ + return { + versionData: null, + forgeData: null, + error: err + } + } + + + } + + // #endregion + +} + +module.exports = { + Util, + AssetGuard, + JavaGuard, + Asset, + Library } \ No newline at end of file diff --git a/app/assets/js/authmanager.js b/app/assets/js/authmanager.js index 22b2fed9..28761b3e 100644 --- a/app/assets/js/authmanager.js +++ b/app/assets/js/authmanager.js @@ -1,99 +1,99 @@ -/** - * AuthManager - * - * This module aims to abstract login procedures. Results from Mojang's REST api - * are retrieved through our Mojang module. These results are processed and stored, - * if applicable, in the config using the ConfigManager. All login procedures should - * be made through this module. - * - * @module authmanager - */ -// Requirements -const ConfigManager = require('./configmanager') -const LoggerUtil = require('./loggerutil') -const Mojang = require('./mojang') -const logger = LoggerUtil('%c[AuthManager]', 'color: #a02d2a; font-weight: bold') -const loggerSuccess = LoggerUtil('%c[AuthManager]', 'color: #209b07; font-weight: bold') - -// Functions - -/** - * Add an account. This will authenticate the given credentials with Mojang's - * authserver. The resultant data will be stored as an auth account in the - * configuration database. - * - * @param {string} username The account username (email if migrated). - * @param {string} password The account password. - * @returns {Promise.} Promise which resolves the resolved authenticated account object. - */ -exports.addAccount = async function(username, password){ - try { - const session = await Mojang.authenticate(username, password, ConfigManager.getClientToken()) - if(session.selectedProfile != null){ - const ret = ConfigManager.addAuthAccount(session.selectedProfile.id, session.accessToken, username, session.selectedProfile.name) - if(ConfigManager.getClientToken() == null){ - ConfigManager.setClientToken(session.clientToken) - } - ConfigManager.save() - return ret - } else { - throw new Error('NotPaidAccount') - } - - } catch (err){ - return Promise.reject(err) - } -} - -/** - * Remove an account. This will invalidate the access token associated - * with the account and then remove it from the database. - * - * @param {string} uuid The UUID of the account to be removed. - * @returns {Promise.} Promise which resolves to void when the action is complete. - */ -exports.removeAccount = async function(uuid){ - try { - const authAcc = ConfigManager.getAuthAccount(uuid) - await Mojang.invalidate(authAcc.accessToken, ConfigManager.getClientToken()) - ConfigManager.removeAuthAccount(uuid) - ConfigManager.save() - return Promise.resolve() - } catch (err){ - return Promise.reject(err) - } -} - -/** - * Validate the selected account with Mojang's authserver. If the account is not valid, - * we will attempt to refresh the access token and update that value. If that fails, a - * new login will be required. - * - * **Function is WIP** - * - * @returns {Promise.} Promise which resolves to true if the access token is valid, - * otherwise false. - */ -exports.validateSelected = async function(){ - const current = ConfigManager.getSelectedAccount() - const isValid = await Mojang.validate(current.accessToken, ConfigManager.getClientToken()) - if(!isValid){ - try { - const session = await Mojang.refresh(current.accessToken, ConfigManager.getClientToken()) - ConfigManager.updateAuthAccount(current.uuid, session.accessToken) - ConfigManager.save() - } catch(err) { - logger.debug('Error while validating selected profile:', err) - if(err && err.error === 'ForbiddenOperationException'){ - // What do we do? - } - logger.log('Account access token is invalid.') - return false - } - loggerSuccess.log('Account access token validated.') - return true - } else { - loggerSuccess.log('Account access token validated.') - return true - } +/** + * AuthManager + * + * This module aims to abstract login procedures. Results from Mojang's REST api + * are retrieved through our Mojang module. These results are processed and stored, + * if applicable, in the config using the ConfigManager. All login procedures should + * be made through this module. + * + * @module authmanager + */ +// Requirements +const ConfigManager = require('./configmanager') +const LoggerUtil = require('./loggerutil') +const Mojang = require('./mojang') +const logger = LoggerUtil('%c[AuthManager]', 'color: #a02d2a; font-weight: bold') +const loggerSuccess = LoggerUtil('%c[AuthManager]', 'color: #209b07; font-weight: bold') + +// Functions + +/** + * Add an account. This will authenticate the given credentials with Mojang's + * authserver. The resultant data will be stored as an auth account in the + * configuration database. + * + * @param {string} username The account username (email if migrated). + * @param {string} password The account password. + * @returns {Promise.} Promise which resolves the resolved authenticated account object. + */ +exports.addAccount = async function(username, password){ + try { + const session = await Mojang.authenticate(username, password, ConfigManager.getClientToken()) + if(session.selectedProfile != null){ + const ret = ConfigManager.addAuthAccount(session.selectedProfile.id, session.accessToken, username, session.selectedProfile.name) + if(ConfigManager.getClientToken() == null){ + ConfigManager.setClientToken(session.clientToken) + } + ConfigManager.save() + return ret + } else { + throw new Error('NotPaidAccount') + } + + } catch (err){ + return Promise.reject(err) + } +} + +/** + * Remove an account. This will invalidate the access token associated + * with the account and then remove it from the database. + * + * @param {string} uuid The UUID of the account to be removed. + * @returns {Promise.} Promise which resolves to void when the action is complete. + */ +exports.removeAccount = async function(uuid){ + try { + const authAcc = ConfigManager.getAuthAccount(uuid) + await Mojang.invalidate(authAcc.accessToken, ConfigManager.getClientToken()) + ConfigManager.removeAuthAccount(uuid) + ConfigManager.save() + return Promise.resolve() + } catch (err){ + return Promise.reject(err) + } +} + +/** + * Validate the selected account with Mojang's authserver. If the account is not valid, + * we will attempt to refresh the access token and update that value. If that fails, a + * new login will be required. + * + * **Function is WIP** + * + * @returns {Promise.} Promise which resolves to true if the access token is valid, + * otherwise false. + */ +exports.validateSelected = async function(){ + const current = ConfigManager.getSelectedAccount() + const isValid = await Mojang.validate(current.accessToken, ConfigManager.getClientToken()) + if(!isValid){ + try { + const session = await Mojang.refresh(current.accessToken, ConfigManager.getClientToken()) + ConfigManager.updateAuthAccount(current.uuid, session.accessToken) + ConfigManager.save() + } catch(err) { + logger.debug('Error while validating selected profile:', err) + if(err && err.error === 'ForbiddenOperationException'){ + // What do we do? + } + logger.log('Account access token is invalid.') + return false + } + loggerSuccess.log('Account access token validated.') + return true + } else { + loggerSuccess.log('Account access token validated.') + return true + } } \ No newline at end of file diff --git a/app/assets/js/configmanager.js b/app/assets/js/configmanager.js index 65a73061..f4eb63ff 100644 --- a/app/assets/js/configmanager.js +++ b/app/assets/js/configmanager.js @@ -1,688 +1,688 @@ -const fs = require('fs-extra') -const os = require('os') -const path = require('path') - -const logger = require('./loggerutil')('%c[ConfigManager]', 'color: #a02d2a; font-weight: bold') - -const sysRoot = process.env.APPDATA || (process.platform == 'darwin' ? process.env.HOME + '/Library/Application Support' : process.env.HOME) -// TODO change -const dataPath = path.join(sysRoot, '.helioslauncher') - -// Forked processes do not have access to electron, so we have this workaround. -const launcherDir = process.env.CONFIG_DIRECT_PATH || require('electron').remote.app.getPath('userData') - -/** - * Retrieve the absolute path of the launcher directory. - * - * @returns {string} The absolute path of the launcher directory. - */ -exports.getLauncherDirectory = function(){ - return launcherDir -} - -/** - * Get the launcher's data directory. This is where all files related - * to game launch are installed (common, instances, java, etc). - * - * @returns {string} The absolute path of the launcher's data directory. - */ -exports.getDataDirectory = function(def = false){ - return !def ? config.settings.launcher.dataDirectory : DEFAULT_CONFIG.settings.launcher.dataDirectory -} - -/** - * Set the new data directory. - * - * @param {string} dataDirectory The new data directory. - */ -exports.setDataDirectory = function(dataDirectory){ - config.settings.launcher.dataDirectory = dataDirectory -} - -const configPath = path.join(exports.getLauncherDirectory(), 'config.json') -const configPathLEGACY = path.join(dataPath, 'config.json') -const firstLaunch = !fs.existsSync(configPath) && !fs.existsSync(configPathLEGACY) - -exports.getAbsoluteMinRAM = function(){ - const mem = os.totalmem() - return mem >= 6000000000 ? 3 : 2 -} - -exports.getAbsoluteMaxRAM = function(){ - const mem = os.totalmem() - const gT16 = mem-16000000000 - return Math.floor((mem-1000000000-(gT16 > 0 ? (Number.parseInt(gT16/8) + 16000000000/4) : mem/4))/1000000000) -} - -function resolveMaxRAM(){ - const mem = os.totalmem() - return mem >= 8000000000 ? '4G' : (mem >= 6000000000 ? '3G' : '2G') -} - -function resolveMinRAM(){ - return resolveMaxRAM() -} - -/** - * Three types of values: - * Static = Explicitly declared. - * Dynamic = Calculated by a private function. - * Resolved = Resolved externally, defaults to null. - */ -const DEFAULT_CONFIG = { - settings: { - java: { - minRAM: resolveMinRAM(), - maxRAM: resolveMaxRAM(), // Dynamic - executable: null, - jvmOptions: [ - '-XX:+UseConcMarkSweepGC', - '-XX:+CMSIncrementalMode', - '-XX:-UseAdaptiveSizePolicy', - '-Xmn128M' - ], - }, - game: { - resWidth: 1280, - resHeight: 720, - fullscreen: false, - autoConnect: true, - launchDetached: true - }, - launcher: { - allowPrerelease: false, - dataDirectory: dataPath - } - }, - newsCache: { - date: null, - content: null, - dismissed: false - }, - clientToken: null, - selectedServer: null, // Resolved - selectedAccount: null, - authenticationDatabase: {}, - modConfigurations: [] -} - -let config = null - -// Persistance Utility Functions - -/** - * Save the current configuration to a file. - */ -exports.save = function(){ - fs.writeFileSync(configPath, JSON.stringify(config, null, 4), 'UTF-8') -} - -/** - * Load the configuration into memory. If a configuration file exists, - * that will be read and saved. Otherwise, a default configuration will - * be generated. Note that "resolved" values default to null and will - * need to be externally assigned. - */ -exports.load = function(){ - let doLoad = true - - if(!fs.existsSync(configPath)){ - // Create all parent directories. - fs.ensureDirSync(path.join(configPath, '..')) - if(fs.existsSync(configPathLEGACY)){ - fs.moveSync(configPathLEGACY, configPath) - } else { - doLoad = false - config = DEFAULT_CONFIG - exports.save() - } - } - if(doLoad){ - let doValidate = false - try { - config = JSON.parse(fs.readFileSync(configPath, 'UTF-8')) - doValidate = true - } catch (err){ - logger.error(err) - logger.log('Configuration file contains malformed JSON or is corrupt.') - logger.log('Generating a new configuration file.') - fs.ensureDirSync(path.join(configPath, '..')) - config = DEFAULT_CONFIG - exports.save() - } - if(doValidate){ - config = validateKeySet(DEFAULT_CONFIG, config) - exports.save() - } - } - logger.log('Successfully Loaded') -} - -/** - * @returns {boolean} Whether or not the manager has been loaded. - */ -exports.isLoaded = function(){ - return config != null -} - -/** - * Validate that the destination object has at least every field - * present in the source object. Assign a default value otherwise. - * - * @param {Object} srcObj The source object to reference against. - * @param {Object} destObj The destination object. - * @returns {Object} A validated destination object. - */ -function validateKeySet(srcObj, destObj){ - if(srcObj == null){ - srcObj = {} - } - const validationBlacklist = ['authenticationDatabase'] - const keys = Object.keys(srcObj) - for(let i=0; i} An array of each stored authenticated account. - */ -exports.getAuthAccounts = function(){ - return config.authenticationDatabase -} - -/** - * Returns the authenticated account with the given uuid. Value may - * be null. - * - * @param {string} uuid The uuid of the authenticated account. - * @returns {Object} The authenticated account with the given uuid. - */ -exports.getAuthAccount = function(uuid){ - return config.authenticationDatabase[uuid] -} - -/** - * Update the access token of an authenticated account. - * - * @param {string} uuid The uuid of the authenticated account. - * @param {string} accessToken The new Access Token. - * - * @returns {Object} The authenticated account object created by this action. - */ -exports.updateAuthAccount = function(uuid, accessToken){ - config.authenticationDatabase[uuid].accessToken = accessToken - return config.authenticationDatabase[uuid] -} - -/** - * Adds an authenticated account to the database to be stored. - * - * @param {string} uuid The uuid of the authenticated account. - * @param {string} accessToken The accessToken of the authenticated account. - * @param {string} username The username (usually email) of the authenticated account. - * @param {string} displayName The in game name of the authenticated account. - * - * @returns {Object} The authenticated account object created by this action. - */ -exports.addAuthAccount = function(uuid, accessToken, username, displayName){ - config.selectedAccount = uuid - config.authenticationDatabase[uuid] = { - accessToken, - username: username.trim(), - uuid: uuid.trim(), - displayName: displayName.trim() - } - return config.authenticationDatabase[uuid] -} - -/** - * Remove an authenticated account from the database. If the account - * was also the selected account, a new one will be selected. If there - * are no accounts, the selected account will be null. - * - * @param {string} uuid The uuid of the authenticated account. - * - * @returns {boolean} True if the account was removed, false if it never existed. - */ -exports.removeAuthAccount = function(uuid){ - if(config.authenticationDatabase[uuid] != null){ - delete config.authenticationDatabase[uuid] - if(config.selectedAccount === uuid){ - const keys = Object.keys(config.authenticationDatabase) - if(keys.length > 0){ - config.selectedAccount = keys[0] - } else { - config.selectedAccount = null - config.clientToken = null - } - } - return true - } - return false -} - -/** - * Get the currently selected authenticated account. - * - * @returns {Object} The selected authenticated account. - */ -exports.getSelectedAccount = function(){ - return config.authenticationDatabase[config.selectedAccount] -} - -/** - * Set the selected authenticated account. - * - * @param {string} uuid The UUID of the account which is to be set - * as the selected account. - * - * @returns {Object} The selected authenticated account. - */ -exports.setSelectedAccount = function(uuid){ - const authAcc = config.authenticationDatabase[uuid] - if(authAcc != null) { - config.selectedAccount = uuid - } - return authAcc -} - -/** - * Get an array of each mod configuration currently stored. - * - * @returns {Array.} An array of each stored mod configuration. - */ -exports.getModConfigurations = function(){ - return config.modConfigurations -} - -/** - * Set the array of stored mod configurations. - * - * @param {Array.} configurations An array of mod configurations. - */ -exports.setModConfigurations = function(configurations){ - config.modConfigurations = configurations -} - -/** - * Get the mod configuration for a specific server. - * - * @param {string} serverid The id of the server. - * @returns {Object} The mod configuration for the given server. - */ -exports.getModConfiguration = function(serverid){ - const cfgs = config.modConfigurations - for(let i=0; i} An array of the additional arguments for JVM initialization. - */ -exports.getJVMOptions = function(def = false){ - return !def ? config.settings.java.jvmOptions : DEFAULT_CONFIG.settings.java.jvmOptions -} - -/** - * Set the additional arguments for JVM initialization. Required arguments, - * such as memory allocation, will be dynamically resolved and should not be - * included in this value. - * - * @param {Array.} jvmOptions An array of the new additional arguments for JVM - * initialization. - */ -exports.setJVMOptions = function(jvmOptions){ - config.settings.java.jvmOptions = jvmOptions -} - -// Game Settings - -/** - * Retrieve the width of the game window. - * - * @param {boolean} def Optional. If true, the default value will be returned. - * @returns {number} The width of the game window. - */ -exports.getGameWidth = function(def = false){ - return !def ? config.settings.game.resWidth : DEFAULT_CONFIG.settings.game.resWidth -} - -/** - * Set the width of the game window. - * - * @param {number} resWidth The new width of the game window. - */ -exports.setGameWidth = function(resWidth){ - config.settings.game.resWidth = Number.parseInt(resWidth) -} - -/** - * Validate a potential new width value. - * - * @param {number} resWidth The width value to validate. - * @returns {boolean} Whether or not the value is valid. - */ -exports.validateGameWidth = function(resWidth){ - const nVal = Number.parseInt(resWidth) - return Number.isInteger(nVal) && nVal >= 0 -} - -/** - * Retrieve the height of the game window. - * - * @param {boolean} def Optional. If true, the default value will be returned. - * @returns {number} The height of the game window. - */ -exports.getGameHeight = function(def = false){ - return !def ? config.settings.game.resHeight : DEFAULT_CONFIG.settings.game.resHeight -} - -/** - * Set the height of the game window. - * - * @param {number} resHeight The new height of the game window. - */ -exports.setGameHeight = function(resHeight){ - config.settings.game.resHeight = Number.parseInt(resHeight) -} - -/** - * Validate a potential new height value. - * - * @param {number} resHeight The height value to validate. - * @returns {boolean} Whether or not the value is valid. - */ -exports.validateGameHeight = function(resHeight){ - const nVal = Number.parseInt(resHeight) - return Number.isInteger(nVal) && nVal >= 0 -} - -/** - * Check if the game should be launched in fullscreen mode. - * - * @param {boolean} def Optional. If true, the default value will be returned. - * @returns {boolean} Whether or not the game is set to launch in fullscreen mode. - */ -exports.getFullscreen = function(def = false){ - return !def ? config.settings.game.fullscreen : DEFAULT_CONFIG.settings.game.fullscreen -} - -/** - * Change the status of if the game should be launched in fullscreen mode. - * - * @param {boolean} fullscreen Whether or not the game should launch in fullscreen mode. - */ -exports.setFullscreen = function(fullscreen){ - config.settings.game.fullscreen = fullscreen -} - -/** - * Check if the game should auto connect to servers. - * - * @param {boolean} def Optional. If true, the default value will be returned. - * @returns {boolean} Whether or not the game should auto connect to servers. - */ -exports.getAutoConnect = function(def = false){ - return !def ? config.settings.game.autoConnect : DEFAULT_CONFIG.settings.game.autoConnect -} - -/** - * Change the status of whether or not the game should auto connect to servers. - * - * @param {boolean} autoConnect Whether or not the game should auto connect to servers. - */ -exports.setAutoConnect = function(autoConnect){ - config.settings.game.autoConnect = autoConnect -} - -/** - * Check if the game should launch as a detached process. - * - * @param {boolean} def Optional. If true, the default value will be returned. - * @returns {boolean} Whether or not the game will launch as a detached process. - */ -exports.getLaunchDetached = function(def = false){ - return !def ? config.settings.game.launchDetached : DEFAULT_CONFIG.settings.game.launchDetached -} - -/** - * Change the status of whether or not the game should launch as a detached process. - * - * @param {boolean} launchDetached Whether or not the game should launch as a detached process. - */ -exports.setLaunchDetached = function(launchDetached){ - config.settings.game.launchDetached = launchDetached -} - -// Launcher Settings - -/** - * Check if the launcher should download prerelease versions. - * - * @param {boolean} def Optional. If true, the default value will be returned. - * @returns {boolean} Whether or not the launcher should download prerelease versions. - */ -exports.getAllowPrerelease = function(def = false){ - return !def ? config.settings.launcher.allowPrerelease : DEFAULT_CONFIG.settings.launcher.allowPrerelease -} - -/** - * Change the status of Whether or not the launcher should download prerelease versions. - * - * @param {boolean} launchDetached Whether or not the launcher should download prerelease versions. - */ -exports.setAllowPrerelease = function(allowPrerelease){ - config.settings.launcher.allowPrerelease = allowPrerelease +const fs = require('fs-extra') +const os = require('os') +const path = require('path') + +const logger = require('./loggerutil')('%c[ConfigManager]', 'color: #a02d2a; font-weight: bold') + +const sysRoot = process.env.APPDATA || (process.platform == 'darwin' ? process.env.HOME + '/Library/Application Support' : process.env.HOME) +// TODO change +const dataPath = path.join(sysRoot, '.creeponnia') + +// Forked processes do not have access to electron, so we have this workaround. +const launcherDir = process.env.CONFIG_DIRECT_PATH || require('electron').remote.app.getPath('userData') + +/** + * Retrieve the absolute path of the launcher directory. + * + * @returns {string} The absolute path of the launcher directory. + */ +exports.getLauncherDirectory = function(){ + return launcherDir +} + +/** + * Get the launcher's data directory. This is where all files related + * to game launch are installed (common, instances, java, etc). + * + * @returns {string} The absolute path of the launcher's data directory. + */ +exports.getDataDirectory = function(def = false){ + return !def ? config.settings.launcher.dataDirectory : DEFAULT_CONFIG.settings.launcher.dataDirectory +} + +/** + * Set the new data directory. + * + * @param {string} dataDirectory The new data directory. + */ +exports.setDataDirectory = function(dataDirectory){ + config.settings.launcher.dataDirectory = dataDirectory +} + +const configPath = path.join(exports.getLauncherDirectory(), 'config.json') +const configPathLEGACY = path.join(dataPath, 'config.json') +const firstLaunch = !fs.existsSync(configPath) && !fs.existsSync(configPathLEGACY) + +exports.getAbsoluteMinRAM = function(){ + const mem = os.totalmem() + return mem >= 6000000000 ? 3 : 2 +} + +exports.getAbsoluteMaxRAM = function(){ + const mem = os.totalmem() + const gT16 = mem-16000000000 + return Math.floor((mem-1000000000-(gT16 > 0 ? (Number.parseInt(gT16/8) + 16000000000/4) : mem/4))/1000000000) +} + +function resolveMaxRAM(){ + const mem = os.totalmem() + return mem >= 8000000000 ? '4G' : (mem >= 6000000000 ? '3G' : '2G') +} + +function resolveMinRAM(){ + return resolveMaxRAM() +} + +/** + * Three types of values: + * Static = Explicitly declared. + * Dynamic = Calculated by a private function. + * Resolved = Resolved externally, defaults to null. + */ +const DEFAULT_CONFIG = { + settings: { + java: { + minRAM: resolveMinRAM(), + maxRAM: resolveMaxRAM(), // Dynamic + executable: null, + jvmOptions: [ + '-XX:+UseConcMarkSweepGC', + '-XX:+CMSIncrementalMode', + '-XX:-UseAdaptiveSizePolicy', + '-Xmn128M' + ], + }, + game: { + resWidth: 1280, + resHeight: 720, + fullscreen: false, + autoConnect: true, + launchDetached: true + }, + launcher: { + allowPrerelease: false, + dataDirectory: dataPath + } + }, + newsCache: { + date: null, + content: null, + dismissed: false + }, + clientToken: null, + selectedServer: null, // Resolved + selectedAccount: null, + authenticationDatabase: {}, + modConfigurations: [] +} + +let config = null + +// Persistance Utility Functions + +/** + * Save the current configuration to a file. + */ +exports.save = function(){ + fs.writeFileSync(configPath, JSON.stringify(config, null, 4), 'UTF-8') +} + +/** + * Load the configuration into memory. If a configuration file exists, + * that will be read and saved. Otherwise, a default configuration will + * be generated. Note that "resolved" values default to null and will + * need to be externally assigned. + */ +exports.load = function(){ + let doLoad = true + + if(!fs.existsSync(configPath)){ + // Create all parent directories. + fs.ensureDirSync(path.join(configPath, '..')) + if(fs.existsSync(configPathLEGACY)){ + fs.moveSync(configPathLEGACY, configPath) + } else { + doLoad = false + config = DEFAULT_CONFIG + exports.save() + } + } + if(doLoad){ + let doValidate = false + try { + config = JSON.parse(fs.readFileSync(configPath, 'UTF-8')) + doValidate = true + } catch (err){ + logger.error(err) + logger.log('Configuration file contains malformed JSON or is corrupt.') + logger.log('Generating a new configuration file.') + fs.ensureDirSync(path.join(configPath, '..')) + config = DEFAULT_CONFIG + exports.save() + } + if(doValidate){ + config = validateKeySet(DEFAULT_CONFIG, config) + exports.save() + } + } + logger.log('Successfully Loaded') +} + +/** + * @returns {boolean} Whether or not the manager has been loaded. + */ +exports.isLoaded = function(){ + return config != null +} + +/** + * Validate that the destination object has at least every field + * present in the source object. Assign a default value otherwise. + * + * @param {Object} srcObj The source object to reference against. + * @param {Object} destObj The destination object. + * @returns {Object} A validated destination object. + */ +function validateKeySet(srcObj, destObj){ + if(srcObj == null){ + srcObj = {} + } + const validationBlacklist = ['authenticationDatabase'] + const keys = Object.keys(srcObj) + for(let i=0; i} An array of each stored authenticated account. + */ +exports.getAuthAccounts = function(){ + return config.authenticationDatabase +} + +/** + * Returns the authenticated account with the given uuid. Value may + * be null. + * + * @param {string} uuid The uuid of the authenticated account. + * @returns {Object} The authenticated account with the given uuid. + */ +exports.getAuthAccount = function(uuid){ + return config.authenticationDatabase[uuid] +} + +/** + * Update the access token of an authenticated account. + * + * @param {string} uuid The uuid of the authenticated account. + * @param {string} accessToken The new Access Token. + * + * @returns {Object} The authenticated account object created by this action. + */ +exports.updateAuthAccount = function(uuid, accessToken){ + config.authenticationDatabase[uuid].accessToken = accessToken + return config.authenticationDatabase[uuid] +} + +/** + * Adds an authenticated account to the database to be stored. + * + * @param {string} uuid The uuid of the authenticated account. + * @param {string} accessToken The accessToken of the authenticated account. + * @param {string} username The username (usually email) of the authenticated account. + * @param {string} displayName The in game name of the authenticated account. + * + * @returns {Object} The authenticated account object created by this action. + */ +exports.addAuthAccount = function(uuid, accessToken, username, displayName){ + config.selectedAccount = uuid + config.authenticationDatabase[uuid] = { + accessToken, + username: username.trim(), + uuid: uuid.trim(), + displayName: displayName.trim() + } + return config.authenticationDatabase[uuid] +} + +/** + * Remove an authenticated account from the database. If the account + * was also the selected account, a new one will be selected. If there + * are no accounts, the selected account will be null. + * + * @param {string} uuid The uuid of the authenticated account. + * + * @returns {boolean} True if the account was removed, false if it never existed. + */ +exports.removeAuthAccount = function(uuid){ + if(config.authenticationDatabase[uuid] != null){ + delete config.authenticationDatabase[uuid] + if(config.selectedAccount === uuid){ + const keys = Object.keys(config.authenticationDatabase) + if(keys.length > 0){ + config.selectedAccount = keys[0] + } else { + config.selectedAccount = null + config.clientToken = null + } + } + return true + } + return false +} + +/** + * Get the currently selected authenticated account. + * + * @returns {Object} The selected authenticated account. + */ +exports.getSelectedAccount = function(){ + return config.authenticationDatabase[config.selectedAccount] +} + +/** + * Set the selected authenticated account. + * + * @param {string} uuid The UUID of the account which is to be set + * as the selected account. + * + * @returns {Object} The selected authenticated account. + */ +exports.setSelectedAccount = function(uuid){ + const authAcc = config.authenticationDatabase[uuid] + if(authAcc != null) { + config.selectedAccount = uuid + } + return authAcc +} + +/** + * Get an array of each mod configuration currently stored. + * + * @returns {Array.} An array of each stored mod configuration. + */ +exports.getModConfigurations = function(){ + return config.modConfigurations +} + +/** + * Set the array of stored mod configurations. + * + * @param {Array.} configurations An array of mod configurations. + */ +exports.setModConfigurations = function(configurations){ + config.modConfigurations = configurations +} + +/** + * Get the mod configuration for a specific server. + * + * @param {string} serverid The id of the server. + * @returns {Object} The mod configuration for the given server. + */ +exports.getModConfiguration = function(serverid){ + const cfgs = config.modConfigurations + for(let i=0; i} An array of the additional arguments for JVM initialization. + */ +exports.getJVMOptions = function(def = false){ + return !def ? config.settings.java.jvmOptions : DEFAULT_CONFIG.settings.java.jvmOptions +} + +/** + * Set the additional arguments for JVM initialization. Required arguments, + * such as memory allocation, will be dynamically resolved and should not be + * included in this value. + * + * @param {Array.} jvmOptions An array of the new additional arguments for JVM + * initialization. + */ +exports.setJVMOptions = function(jvmOptions){ + config.settings.java.jvmOptions = jvmOptions +} + +// Game Settings + +/** + * Retrieve the width of the game window. + * + * @param {boolean} def Optional. If true, the default value will be returned. + * @returns {number} The width of the game window. + */ +exports.getGameWidth = function(def = false){ + return !def ? config.settings.game.resWidth : DEFAULT_CONFIG.settings.game.resWidth +} + +/** + * Set the width of the game window. + * + * @param {number} resWidth The new width of the game window. + */ +exports.setGameWidth = function(resWidth){ + config.settings.game.resWidth = Number.parseInt(resWidth) +} + +/** + * Validate a potential new width value. + * + * @param {number} resWidth The width value to validate. + * @returns {boolean} Whether or not the value is valid. + */ +exports.validateGameWidth = function(resWidth){ + const nVal = Number.parseInt(resWidth) + return Number.isInteger(nVal) && nVal >= 0 +} + +/** + * Retrieve the height of the game window. + * + * @param {boolean} def Optional. If true, the default value will be returned. + * @returns {number} The height of the game window. + */ +exports.getGameHeight = function(def = false){ + return !def ? config.settings.game.resHeight : DEFAULT_CONFIG.settings.game.resHeight +} + +/** + * Set the height of the game window. + * + * @param {number} resHeight The new height of the game window. + */ +exports.setGameHeight = function(resHeight){ + config.settings.game.resHeight = Number.parseInt(resHeight) +} + +/** + * Validate a potential new height value. + * + * @param {number} resHeight The height value to validate. + * @returns {boolean} Whether or not the value is valid. + */ +exports.validateGameHeight = function(resHeight){ + const nVal = Number.parseInt(resHeight) + return Number.isInteger(nVal) && nVal >= 0 +} + +/** + * Check if the game should be launched in fullscreen mode. + * + * @param {boolean} def Optional. If true, the default value will be returned. + * @returns {boolean} Whether or not the game is set to launch in fullscreen mode. + */ +exports.getFullscreen = function(def = false){ + return !def ? config.settings.game.fullscreen : DEFAULT_CONFIG.settings.game.fullscreen +} + +/** + * Change the status of if the game should be launched in fullscreen mode. + * + * @param {boolean} fullscreen Whether or not the game should launch in fullscreen mode. + */ +exports.setFullscreen = function(fullscreen){ + config.settings.game.fullscreen = fullscreen +} + +/** + * Check if the game should auto connect to servers. + * + * @param {boolean} def Optional. If true, the default value will be returned. + * @returns {boolean} Whether or not the game should auto connect to servers. + */ +exports.getAutoConnect = function(def = false){ + return !def ? config.settings.game.autoConnect : DEFAULT_CONFIG.settings.game.autoConnect +} + +/** + * Change the status of whether or not the game should auto connect to servers. + * + * @param {boolean} autoConnect Whether or not the game should auto connect to servers. + */ +exports.setAutoConnect = function(autoConnect){ + config.settings.game.autoConnect = autoConnect +} + +/** + * Check if the game should launch as a detached process. + * + * @param {boolean} def Optional. If true, the default value will be returned. + * @returns {boolean} Whether or not the game will launch as a detached process. + */ +exports.getLaunchDetached = function(def = false){ + return !def ? config.settings.game.launchDetached : DEFAULT_CONFIG.settings.game.launchDetached +} + +/** + * Change the status of whether or not the game should launch as a detached process. + * + * @param {boolean} launchDetached Whether or not the game should launch as a detached process. + */ +exports.setLaunchDetached = function(launchDetached){ + config.settings.game.launchDetached = launchDetached +} + +// Launcher Settings + +/** + * Check if the launcher should download prerelease versions. + * + * @param {boolean} def Optional. If true, the default value will be returned. + * @returns {boolean} Whether or not the launcher should download prerelease versions. + */ +exports.getAllowPrerelease = function(def = false){ + return !def ? config.settings.launcher.allowPrerelease : DEFAULT_CONFIG.settings.launcher.allowPrerelease +} + +/** + * Change the status of Whether or not the launcher should download prerelease versions. + * + * @param {boolean} launchDetached Whether or not the launcher should download prerelease versions. + */ +exports.setAllowPrerelease = function(allowPrerelease){ + config.settings.launcher.allowPrerelease = allowPrerelease } \ No newline at end of file diff --git a/app/assets/js/discordwrapper.js b/app/assets/js/discordwrapper.js index 529f17be..76e81ad9 100644 --- a/app/assets/js/discordwrapper.js +++ b/app/assets/js/discordwrapper.js @@ -1,48 +1,48 @@ -// Work in progress -const logger = require('./loggerutil')('%c[DiscordWrapper]', 'color: #7289da; font-weight: bold') - -const {Client} = require('discord-rpc') - -let client -let activity - -exports.initRPC = function(genSettings, servSettings, initialDetails = 'Waiting for Client..'){ - client = new Client({ transport: 'ipc' }) - - activity = { - details: initialDetails, - state: 'Server: ' + servSettings.shortId, - largeImageKey: servSettings.largeImageKey, - largeImageText: servSettings.largeImageText, - smallImageKey: genSettings.smallImageKey, - smallImageText: genSettings.smallImageText, - startTimestamp: new Date().getTime(), - instance: false - } - - client.on('ready', () => { - logger.log('Discord RPC Connected') - client.setActivity(activity) - }) - - client.login({clientId: genSettings.clientId}).catch(error => { - if(error.message.includes('ENOENT')) { - logger.log('Unable to initialize Discord Rich Presence, no client detected.') - } else { - logger.log('Unable to initialize Discord Rich Presence: ' + error.message, error) - } - }) -} - -exports.updateDetails = function(details){ - activity.details = details - client.setActivity(activity) -} - -exports.shutdownRPC = function(){ - if(!client) return - client.clearActivity() - client.destroy() - client = null - activity = null +// Work in progress +const logger = require('./loggerutil')('%c[DiscordWrapper]', 'color: #7289da; font-weight: bold') + +const {Client} = require('discord-rpc') + +let client +let activity + +exports.initRPC = function(genSettings, servSettings, initialDetails = 'Waiting for Client..'){ + client = new Client({ transport: 'ipc' }) + + activity = { + details: initialDetails, + state: 'Server: ' + servSettings.shortId, + largeImageKey: servSettings.largeImageKey, + largeImageText: servSettings.largeImageText, + smallImageKey: genSettings.smallImageKey, + smallImageText: genSettings.smallImageText, + startTimestamp: new Date().getTime(), + instance: false + } + + client.on('ready', () => { + logger.log('Discord RPC Connected') + client.setActivity(activity) + }) + + client.login({clientId: genSettings.clientId}).catch(error => { + if(error.message.includes('ENOENT')) { + logger.log('Unable to initialize Discord Rich Presence, no client detected.') + } else { + logger.log('Unable to initialize Discord Rich Presence: ' + error.message, error) + } + }) +} + +exports.updateDetails = function(details){ + activity.details = details + client.setActivity(activity) +} + +exports.shutdownRPC = function(){ + if(!client) return + client.clearActivity() + client.destroy() + client = null + activity = null } \ No newline at end of file diff --git a/app/assets/js/distromanager.js b/app/assets/js/distromanager.js index a4cafffa..bb106f84 100644 --- a/app/assets/js/distromanager.js +++ b/app/assets/js/distromanager.js @@ -1,605 +1,605 @@ -const fs = require('fs') -const path = require('path') -const request = require('request') - -const ConfigManager = require('./configmanager') -const logger = require('./loggerutil')('%c[DistroManager]', 'color: #a02d2a; font-weight: bold') - -/** - * Represents the download information - * for a specific module. - */ -class Artifact { - - /** - * Parse a JSON object into an Artifact. - * - * @param {Object} json A JSON object representing an Artifact. - * - * @returns {Artifact} The parsed Artifact. - */ - static fromJSON(json){ - return Object.assign(new Artifact(), json) - } - - /** - * Get the MD5 hash of the artifact. This value may - * be undefined for artifacts which are not to be - * validated and updated. - * - * @returns {string} The MD5 hash of the Artifact or undefined. - */ - getHash(){ - return this.MD5 - } - - /** - * @returns {number} The download size of the artifact. - */ - getSize(){ - return this.size - } - - /** - * @returns {string} The download url of the artifact. - */ - getURL(){ - return this.url - } - - /** - * @returns {string} The artifact's destination path. - */ - getPath(){ - return this.path - } - -} -exports.Artifact - -/** - * Represents a the requirement status - * of a module. - */ -class Required { - - /** - * Parse a JSON object into a Required object. - * - * @param {Object} json A JSON object representing a Required object. - * - * @returns {Required} The parsed Required object. - */ - static fromJSON(json){ - if(json == null){ - return new Required(true, true) - } else { - return new Required(json.value == null ? true : json.value, json.def == null ? true : json.def) - } - } - - constructor(value, def){ - this.value = value - this.default = def - } - - /** - * Get the default value for a required object. If a module - * is not required, this value determines whether or not - * it is enabled by default. - * - * @returns {boolean} The default enabled value. - */ - isDefault(){ - return this.default - } - - /** - * @returns {boolean} Whether or not the module is required. - */ - isRequired(){ - return this.value - } - -} -exports.Required - -/** - * Represents a module. - */ -class Module { - - /** - * Parse a JSON object into a Module. - * - * @param {Object} json A JSON object representing a Module. - * @param {string} serverid The ID of the server to which this module belongs. - * - * @returns {Module} The parsed Module. - */ - static fromJSON(json, serverid){ - return new Module(json.id, json.name, json.type, json.required, json.artifact, json.subModules, serverid) - } - - /** - * Resolve the default extension for a specific module type. - * - * @param {string} type The type of the module. - * - * @return {string} The default extension for the given type. - */ - static _resolveDefaultExtension(type){ - switch (type) { - case exports.Types.Library: - case exports.Types.ForgeHosted: - case exports.Types.LiteLoader: - case exports.Types.ForgeMod: - return 'jar' - case exports.Types.LiteMod: - return 'litemod' - case exports.Types.File: - default: - return 'jar' // There is no default extension really. - } - } - - constructor(id, name, type, required, artifact, subModules, serverid) { - this.identifier = id - this.type = type - this._resolveMetaData() - this.name = name - this.required = Required.fromJSON(required) - this.artifact = Artifact.fromJSON(artifact) - this._resolveArtifactPath(artifact.path, serverid) - this._resolveSubModules(subModules, serverid) - } - - _resolveMetaData(){ - try { - - const m0 = this.identifier.split('@') - - this.artifactExt = m0[1] || Module._resolveDefaultExtension(this.type) - - const m1 = m0[0].split(':') - - this.artifactClassifier = m1[3] || undefined - this.artifactVersion = m1[2] || '???' - this.artifactID = m1[1] || '???' - this.artifactGroup = m1[0] || '???' - - } catch (err) { - // Improper identifier - logger.error('Improper ID for module', this.identifier, err) - } - } - - _resolveArtifactPath(artifactPath, serverid){ - const pth = artifactPath == null ? path.join(...this.getGroup().split('.'), this.getID(), this.getVersion(), `${this.getID()}-${this.getVersion()}${this.artifactClassifier != undefined ? `-${this.artifactClassifier}` : ''}.${this.getExtension()}`) : artifactPath - - switch (this.type){ - case exports.Types.Library: - case exports.Types.ForgeHosted: - case exports.Types.LiteLoader: - this.artifact.path = path.join(ConfigManager.getCommonDirectory(), 'libraries', pth) - break - case exports.Types.ForgeMod: - case exports.Types.LiteMod: - this.artifact.path = path.join(ConfigManager.getCommonDirectory(), 'modstore', pth) - break - case exports.Types.VersionManifest: - this.artifact.path = path.join(ConfigManager.getCommonDirectory(), 'versions', this.getIdentifier(), `${this.getIdentifier()}.json`) - break - case exports.Types.File: - default: - this.artifact.path = path.join(ConfigManager.getInstanceDirectory(), serverid, pth) - break - } - - } - - _resolveSubModules(json, serverid){ - const arr = [] - if(json != null){ - for(let sm of json){ - arr.push(Module.fromJSON(sm, serverid)) - } - } - this.subModules = arr.length > 0 ? arr : null - } - - /** - * @returns {string} The full, unparsed module identifier. - */ - getIdentifier(){ - return this.identifier - } - - /** - * @returns {string} The name of the module. - */ - getName(){ - return this.name - } - - /** - * @returns {Required} The required object declared by this module. - */ - getRequired(){ - return this.required - } - - /** - * @returns {Artifact} The artifact declared by this module. - */ - getArtifact(){ - return this.artifact - } - - /** - * @returns {string} The maven identifier of this module's artifact. - */ - getID(){ - return this.artifactID - } - - /** - * @returns {string} The maven group of this module's artifact. - */ - getGroup(){ - return this.artifactGroup - } - - /** - * @returns {string} The identifier without he version or extension. - */ - getVersionlessID(){ - return this.getGroup() + ':' + this.getID() - } - - /** - * @returns {string} The identifier without the extension. - */ - getExtensionlessID(){ - return this.getIdentifier().split('@')[0] - } - - /** - * @returns {string} The version of this module's artifact. - */ - getVersion(){ - return this.artifactVersion - } - - /** - * @returns {string} The classifier of this module's artifact - */ - getClassifier(){ - return this.artifactClassifier - } - - /** - * @returns {string} The extension of this module's artifact. - */ - getExtension(){ - return this.artifactExt - } - - /** - * @returns {boolean} Whether or not this module has sub modules. - */ - hasSubModules(){ - return this.subModules != null - } - - /** - * @returns {Array.} An array of sub modules. - */ - getSubModules(){ - return this.subModules - } - - /** - * @returns {string} The type of the module. - */ - getType(){ - return this.type - } - -} -exports.Module - -/** - * Represents a server configuration. - */ -class Server { - - /** - * Parse a JSON object into a Server. - * - * @param {Object} json A JSON object representing a Server. - * - * @returns {Server} The parsed Server object. - */ - static fromJSON(json){ - - const mdls = json.modules - json.modules = [] - - const serv = Object.assign(new Server(), json) - serv._resolveModules(mdls) - - return serv - } - - _resolveModules(json){ - const arr = [] - for(let m of json){ - arr.push(Module.fromJSON(m, this.getID())) - } - this.modules = arr - } - - /** - * @returns {string} The ID of the server. - */ - getID(){ - return this.id - } - - /** - * @returns {string} The name of the server. - */ - getName(){ - return this.name - } - - /** - * @returns {string} The description of the server. - */ - getDescription(){ - return this.description - } - - /** - * @returns {string} The URL of the server's icon. - */ - getIcon(){ - return this.icon - } - - /** - * @returns {string} The version of the server configuration. - */ - getVersion(){ - return this.version - } - - /** - * @returns {string} The IP address of the server. - */ - getAddress(){ - return this.address - } - - /** - * @returns {string} The minecraft version of the server. - */ - getMinecraftVersion(){ - return this.minecraftVersion - } - - /** - * @returns {boolean} Whether or not this server is the main - * server. The main server is selected by the launcher when - * no valid server is selected. - */ - isMainServer(){ - return this.mainServer - } - - /** - * @returns {boolean} Whether or not the server is autoconnect. - * by default. - */ - isAutoConnect(){ - return this.autoconnect - } - - /** - * @returns {Array.} An array of modules for this server. - */ - getModules(){ - return this.modules - } - -} -exports.Server - -/** - * Represents the Distribution Index. - */ -class DistroIndex { - - /** - * Parse a JSON object into a DistroIndex. - * - * @param {Object} json A JSON object representing a DistroIndex. - * - * @returns {DistroIndex} The parsed Server object. - */ - static fromJSON(json){ - - const servers = json.servers - json.servers = [] - - const distro = Object.assign(new DistroIndex(), json) - distro._resolveServers(servers) - distro._resolveMainServer() - - return distro - } - - _resolveServers(json){ - const arr = [] - for(let s of json){ - arr.push(Server.fromJSON(s)) - } - this.servers = arr - } - - _resolveMainServer(){ - - for(let serv of this.servers){ - if(serv.mainServer){ - this.mainServer = serv.id - return - } - } - - // If no server declares default_selected, default to the first one declared. - this.mainServer = (this.servers.length > 0) ? this.servers[0].getID() : null - } - - /** - * @returns {string} The version of the distribution index. - */ - getVersion(){ - return this.version - } - - /** - * @returns {string} The URL to the news RSS feed. - */ - getRSS(){ - return this.rss - } - - /** - * @returns {Array.} An array of declared server configurations. - */ - getServers(){ - return this.servers - } - - /** - * Get a server configuration by its ID. If it does not - * exist, null will be returned. - * - * @param {string} id The ID of the server. - * - * @returns {Server} The server configuration with the given ID or null. - */ - getServer(id){ - for(let serv of this.servers){ - if(serv.id === id){ - return serv - } - } - return null - } - - /** - * Get the main server. - * - * @returns {Server} The main server. - */ - getMainServer(){ - return this.mainServer != null ? this.getServer(this.mainServer) : null - } - -} -exports.DistroIndex - -exports.Types = { - Library: 'Library', - ForgeHosted: 'ForgeHosted', - Forge: 'Forge', // Unimplemented - LiteLoader: 'LiteLoader', - ForgeMod: 'ForgeMod', - LiteMod: 'LiteMod', - File: 'File', - VersionManifest: 'VersionManifest' -} - -let DEV_MODE = false - -const DISTRO_PATH = path.join(ConfigManager.getLauncherDirectory(), 'distribution.json') -const DEV_PATH = path.join(ConfigManager.getLauncherDirectory(), 'dev_distribution.json') - -let data = null - -/** - * @returns {Promise.} - */ -exports.pullRemote = function(){ - if(DEV_MODE){ - return exports.pullLocal() - } - return new Promise((resolve, reject) => { - const distroURL = 'http://mc.westeroscraft.com/WesterosCraftLauncher/distribution.json' - //const distroURL = 'https://gist.githubusercontent.com/dscalzi/53b1ba7a11d26a5c353f9d5ae484b71b/raw/' - const opts = { - url: distroURL, - timeout: 2500 - } - const distroDest = path.join(ConfigManager.getLauncherDirectory(), 'distribution.json') - request(opts, (error, resp, body) => { - if(!error){ - - try { - data = DistroIndex.fromJSON(JSON.parse(body)) - } catch (e) { - reject(e) - } - - fs.writeFile(distroDest, body, 'utf-8', (err) => { - if(!err){ - resolve(data) - } else { - reject(err) - } - }) - } else { - reject(error) - } - }) - }) -} - -/** - * @returns {Promise.} - */ -exports.pullLocal = function(){ - return new Promise((resolve, reject) => { - fs.readFile(DEV_MODE ? DEV_PATH : DISTRO_PATH, 'utf-8', (err, d) => { - if(!err){ - data = DistroIndex.fromJSON(JSON.parse(d)) - resolve(data) - } else { - reject(err) - } - }) - }) -} - -exports.setDevMode = function(value){ - if(value){ - logger.log('Developer mode enabled.') - logger.log('If you don\'t know what that means, revert immediately.') - } else { - logger.log('Developer mode disabled.') - } - DEV_MODE = value -} - -exports.isDevMode = function(){ - return DEV_MODE -} - -/** - * @returns {DistroIndex} - */ -exports.getDistribution = function(){ - return data +const fs = require('fs') +const path = require('path') +const request = require('request') + +const ConfigManager = require('./configmanager') +const logger = require('./loggerutil')('%c[DistroManager]', 'color: #a02d2a; font-weight: bold') + +/** + * Represents the download information + * for a specific module. + */ +class Artifact { + + /** + * Parse a JSON object into an Artifact. + * + * @param {Object} json A JSON object representing an Artifact. + * + * @returns {Artifact} The parsed Artifact. + */ + static fromJSON(json){ + return Object.assign(new Artifact(), json) + } + + /** + * Get the MD5 hash of the artifact. This value may + * be undefined for artifacts which are not to be + * validated and updated. + * + * @returns {string} The MD5 hash of the Artifact or undefined. + */ + getHash(){ + return this.MD5 + } + + /** + * @returns {number} The download size of the artifact. + */ + getSize(){ + return this.size + } + + /** + * @returns {string} The download url of the artifact. + */ + getURL(){ + return this.url + } + + /** + * @returns {string} The artifact's destination path. + */ + getPath(){ + return this.path + } + +} +exports.Artifact + +/** + * Represents a the requirement status + * of a module. + */ +class Required { + + /** + * Parse a JSON object into a Required object. + * + * @param {Object} json A JSON object representing a Required object. + * + * @returns {Required} The parsed Required object. + */ + static fromJSON(json){ + if(json == null){ + return new Required(true, true) + } else { + return new Required(json.value == null ? true : json.value, json.def == null ? true : json.def) + } + } + + constructor(value, def){ + this.value = value + this.default = def + } + + /** + * Get the default value for a required object. If a module + * is not required, this value determines whether or not + * it is enabled by default. + * + * @returns {boolean} The default enabled value. + */ + isDefault(){ + return this.default + } + + /** + * @returns {boolean} Whether or not the module is required. + */ + isRequired(){ + return this.value + } + +} +exports.Required + +/** + * Represents a module. + */ +class Module { + + /** + * Parse a JSON object into a Module. + * + * @param {Object} json A JSON object representing a Module. + * @param {string} serverid The ID of the server to which this module belongs. + * + * @returns {Module} The parsed Module. + */ + static fromJSON(json, serverid){ + return new Module(json.id, json.name, json.type, json.required, json.artifact, json.subModules, serverid) + } + + /** + * Resolve the default extension for a specific module type. + * + * @param {string} type The type of the module. + * + * @return {string} The default extension for the given type. + */ + static _resolveDefaultExtension(type){ + switch (type) { + case exports.Types.Library: + case exports.Types.ForgeHosted: + case exports.Types.LiteLoader: + case exports.Types.ForgeMod: + return 'jar' + case exports.Types.LiteMod: + return 'litemod' + case exports.Types.File: + default: + return 'jar' // There is no default extension really. + } + } + + constructor(id, name, type, required, artifact, subModules, serverid) { + this.identifier = id + this.type = type + this._resolveMetaData() + this.name = name + this.required = Required.fromJSON(required) + this.artifact = Artifact.fromJSON(artifact) + this._resolveArtifactPath(artifact.path, serverid) + this._resolveSubModules(subModules, serverid) + } + + _resolveMetaData(){ + try { + + const m0 = this.identifier.split('@') + + this.artifactExt = m0[1] || Module._resolveDefaultExtension(this.type) + + const m1 = m0[0].split(':') + + this.artifactClassifier = m1[3] || undefined + this.artifactVersion = m1[2] || '???' + this.artifactID = m1[1] || '???' + this.artifactGroup = m1[0] || '???' + + } catch (err) { + // Improper identifier + logger.error('Improper ID for module', this.identifier, err) + } + } + + _resolveArtifactPath(artifactPath, serverid){ + const pth = artifactPath == null ? path.join(...this.getGroup().split('.'), this.getID(), this.getVersion(), `${this.getID()}-${this.getVersion()}${this.artifactClassifier != undefined ? `-${this.artifactClassifier}` : ''}.${this.getExtension()}`) : artifactPath + + switch (this.type){ + case exports.Types.Library: + case exports.Types.ForgeHosted: + case exports.Types.LiteLoader: + this.artifact.path = path.join(ConfigManager.getCommonDirectory(), 'libraries', pth) + break + case exports.Types.ForgeMod: + case exports.Types.LiteMod: + this.artifact.path = path.join(ConfigManager.getCommonDirectory(), 'modstore', pth) + break + case exports.Types.VersionManifest: + this.artifact.path = path.join(ConfigManager.getCommonDirectory(), 'versions', this.getIdentifier(), `${this.getIdentifier()}.json`) + break + case exports.Types.File: + default: + this.artifact.path = path.join(ConfigManager.getInstanceDirectory(), serverid, pth) + break + } + + } + + _resolveSubModules(json, serverid){ + const arr = [] + if(json != null){ + for(let sm of json){ + arr.push(Module.fromJSON(sm, serverid)) + } + } + this.subModules = arr.length > 0 ? arr : null + } + + /** + * @returns {string} The full, unparsed module identifier. + */ + getIdentifier(){ + return this.identifier + } + + /** + * @returns {string} The name of the module. + */ + getName(){ + return this.name + } + + /** + * @returns {Required} The required object declared by this module. + */ + getRequired(){ + return this.required + } + + /** + * @returns {Artifact} The artifact declared by this module. + */ + getArtifact(){ + return this.artifact + } + + /** + * @returns {string} The maven identifier of this module's artifact. + */ + getID(){ + return this.artifactID + } + + /** + * @returns {string} The maven group of this module's artifact. + */ + getGroup(){ + return this.artifactGroup + } + + /** + * @returns {string} The identifier without he version or extension. + */ + getVersionlessID(){ + return this.getGroup() + ':' + this.getID() + } + + /** + * @returns {string} The identifier without the extension. + */ + getExtensionlessID(){ + return this.getIdentifier().split('@')[0] + } + + /** + * @returns {string} The version of this module's artifact. + */ + getVersion(){ + return this.artifactVersion + } + + /** + * @returns {string} The classifier of this module's artifact + */ + getClassifier(){ + return this.artifactClassifier + } + + /** + * @returns {string} The extension of this module's artifact. + */ + getExtension(){ + return this.artifactExt + } + + /** + * @returns {boolean} Whether or not this module has sub modules. + */ + hasSubModules(){ + return this.subModules != null + } + + /** + * @returns {Array.} An array of sub modules. + */ + getSubModules(){ + return this.subModules + } + + /** + * @returns {string} The type of the module. + */ + getType(){ + return this.type + } + +} +exports.Module + +/** + * Represents a server configuration. + */ +class Server { + + /** + * Parse a JSON object into a Server. + * + * @param {Object} json A JSON object representing a Server. + * + * @returns {Server} The parsed Server object. + */ + static fromJSON(json){ + + const mdls = json.modules + json.modules = [] + + const serv = Object.assign(new Server(), json) + serv._resolveModules(mdls) + + return serv + } + + _resolveModules(json){ + const arr = [] + for(let m of json){ + arr.push(Module.fromJSON(m, this.getID())) + } + this.modules = arr + } + + /** + * @returns {string} The ID of the server. + */ + getID(){ + return this.id + } + + /** + * @returns {string} The name of the server. + */ + getName(){ + return this.name + } + + /** + * @returns {string} The description of the server. + */ + getDescription(){ + return this.description + } + + /** + * @returns {string} The URL of the server's icon. + */ + getIcon(){ + return this.icon + } + + /** + * @returns {string} The version of the server configuration. + */ + getVersion(){ + return this.version + } + + /** + * @returns {string} The IP address of the server. + */ + getAddress(){ + return this.address + } + + /** + * @returns {string} The minecraft version of the server. + */ + getMinecraftVersion(){ + return this.minecraftVersion + } + + /** + * @returns {boolean} Whether or not this server is the main + * server. The main server is selected by the launcher when + * no valid server is selected. + */ + isMainServer(){ + return this.mainServer + } + + /** + * @returns {boolean} Whether or not the server is autoconnect. + * by default. + */ + isAutoConnect(){ + return this.autoconnect + } + + /** + * @returns {Array.} An array of modules for this server. + */ + getModules(){ + return this.modules + } + +} +exports.Server + +/** + * Represents the Distribution Index. + */ +class DistroIndex { + + /** + * Parse a JSON object into a DistroIndex. + * + * @param {Object} json A JSON object representing a DistroIndex. + * + * @returns {DistroIndex} The parsed Server object. + */ + static fromJSON(json){ + + const servers = json.servers + json.servers = [] + + const distro = Object.assign(new DistroIndex(), json) + distro._resolveServers(servers) + distro._resolveMainServer() + + return distro + } + + _resolveServers(json){ + const arr = [] + for(let s of json){ + arr.push(Server.fromJSON(s)) + } + this.servers = arr + } + + _resolveMainServer(){ + + for(let serv of this.servers){ + if(serv.mainServer){ + this.mainServer = serv.id + return + } + } + + // If no server declares default_selected, default to the first one declared. + this.mainServer = (this.servers.length > 0) ? this.servers[0].getID() : null + } + + /** + * @returns {string} The version of the distribution index. + */ + getVersion(){ + return this.version + } + + /** + * @returns {string} The URL to the news RSS feed. + */ + getRSS(){ + return this.rss + } + + /** + * @returns {Array.} An array of declared server configurations. + */ + getServers(){ + return this.servers + } + + /** + * Get a server configuration by its ID. If it does not + * exist, null will be returned. + * + * @param {string} id The ID of the server. + * + * @returns {Server} The server configuration with the given ID or null. + */ + getServer(id){ + for(let serv of this.servers){ + if(serv.id === id){ + return serv + } + } + return null + } + + /** + * Get the main server. + * + * @returns {Server} The main server. + */ + getMainServer(){ + return this.mainServer != null ? this.getServer(this.mainServer) : null + } + +} +exports.DistroIndex + +exports.Types = { + Library: 'Library', + ForgeHosted: 'ForgeHosted', + Forge: 'Forge', // Unimplemented + LiteLoader: 'LiteLoader', + ForgeMod: 'ForgeMod', + LiteMod: 'LiteMod', + File: 'File', + VersionManifest: 'VersionManifest' +} + +let DEV_MODE = false + +const DISTRO_PATH = path.join(ConfigManager.getLauncherDirectory(), 'distribution.json') +const DEV_PATH = path.join(ConfigManager.getLauncherDirectory(), 'dev_distribution.json') + +let data = null + +/** + * @returns {Promise.} + */ +exports.pullRemote = function(){ + if(DEV_MODE){ + return exports.pullLocal() + } + return new Promise((resolve, reject) => { + const distroURL = 'https://creeponnia.ch/launcher/distribution.json' + //const distroURL = 'https://gist.githubusercontent.com/dscalzi/53b1ba7a11d26a5c353f9d5ae484b71b/raw/' + const opts = { + url: distroURL, + timeout: 2500 + } + const distroDest = path.join(ConfigManager.getLauncherDirectory(), 'distribution.json') + request(opts, (error, resp, body) => { + if(!error){ + + try { + data = DistroIndex.fromJSON(JSON.parse(body)) + } catch (e) { + reject(e) + } + + fs.writeFile(distroDest, body, 'utf-8', (err) => { + if(!err){ + resolve(data) + } else { + reject(err) + } + }) + } else { + reject(error) + } + }) + }) +} + +/** + * @returns {Promise.} + */ +exports.pullLocal = function(){ + return new Promise((resolve, reject) => { + fs.readFile(DEV_MODE ? DEV_PATH : DISTRO_PATH, 'utf-8', (err, d) => { + if(!err){ + data = DistroIndex.fromJSON(JSON.parse(d)) + resolve(data) + } else { + reject(err) + } + }) + }) +} + +exports.setDevMode = function(value){ + if(value){ + logger.log('Developer mode enabled.') + logger.log('If you don\'t know what that means, revert immediately.') + } else { + logger.log('Developer mode disabled.') + } + DEV_MODE = value +} + +exports.isDevMode = function(){ + return DEV_MODE +} + +/** + * @returns {DistroIndex} + */ +exports.getDistribution = function(){ + return data } \ No newline at end of file diff --git a/app/assets/js/dropinmodutil.js b/app/assets/js/dropinmodutil.js index 0a61012e..10e5ad3d 100644 --- a/app/assets/js/dropinmodutil.js +++ b/app/assets/js/dropinmodutil.js @@ -1,232 +1,232 @@ -const fs = require('fs-extra') -const path = require('path') -const { shell } = require('electron') - -// Group #1: File Name (without .disabled, if any) -// Group #2: File Extension (jar, zip, or litemod) -// Group #3: If it is disabled (if string 'disabled' is present) -const MOD_REGEX = /^(.+(jar|zip|litemod))(?:\.(disabled))?$/ -const DISABLED_EXT = '.disabled' - -const SHADER_REGEX = /^(.+)\.zip$/ -const SHADER_OPTION = /shaderPack=(.+)/ -const SHADER_DIR = 'shaderpacks' -const SHADER_CONFIG = 'optionsshaders.txt' - -/** - * Validate that the given directory exists. If not, it is - * created. - * - * @param {string} modsDir The path to the mods directory. - */ -exports.validateDir = function(dir) { - fs.ensureDirSync(dir) -} - -/** - * Scan for drop-in mods in both the mods folder and version - * safe mods folder. - * - * @param {string} modsDir The path to the mods directory. - * @param {string} version The minecraft version of the server configuration. - * - * @returns {{fullName: string, name: string, ext: string, disabled: boolean}[]} - * An array of objects storing metadata about each discovered mod. - */ -exports.scanForDropinMods = function(modsDir, version) { - const modsDiscovered = [] - if(fs.existsSync(modsDir)){ - let modCandidates = fs.readdirSync(modsDir) - let verCandidates = [] - const versionDir = path.join(modsDir, version) - if(fs.existsSync(versionDir)){ - verCandidates = fs.readdirSync(versionDir) - } - for(let file of modCandidates){ - const match = MOD_REGEX.exec(file) - if(match != null){ - modsDiscovered.push({ - fullName: match[0], - name: match[1], - ext: match[2], - disabled: match[3] != null - }) - } - } - for(let file of verCandidates){ - const match = MOD_REGEX.exec(file) - if(match != null){ - modsDiscovered.push({ - fullName: path.join(version, match[0]), - name: match[1], - ext: match[2], - disabled: match[3] != null - }) - } - } - } - return modsDiscovered -} - -/** - * Add dropin mods. - * - * @param {FileList} files The files to add. - * @param {string} modsDir The path to the mods directory. - */ -exports.addDropinMods = function(files, modsdir) { - - exports.validateDir(modsdir) - - for(let f of files) { - if(MOD_REGEX.exec(f.name) != null) { - fs.moveSync(f.path, path.join(modsdir, f.name)) - } - } - -} - -/** - * Delete a drop-in mod from the file system. - * - * @param {string} modsDir The path to the mods directory. - * @param {string} fullName The fullName of the discovered mod to delete. - * - * @returns {boolean} True if the mod was deleted, otherwise false. - */ -exports.deleteDropinMod = function(modsDir, fullName){ - const res = shell.moveItemToTrash(path.join(modsDir, fullName)) - if(!res){ - shell.beep() - } - return res -} - -/** - * Toggle a discovered mod on or off. This is achieved by either - * adding or disabling the .disabled extension to the local file. - * - * @param {string} modsDir The path to the mods directory. - * @param {string} fullName The fullName of the discovered mod to toggle. - * @param {boolean} enable Whether to toggle on or off the mod. - * - * @returns {Promise.} A promise which resolves when the mod has - * been toggled. If an IO error occurs the promise will be rejected. - */ -exports.toggleDropinMod = function(modsDir, fullName, enable){ - return new Promise((resolve, reject) => { - const oldPath = path.join(modsDir, fullName) - const newPath = path.join(modsDir, enable ? fullName.substring(0, fullName.indexOf(DISABLED_EXT)) : fullName + DISABLED_EXT) - - fs.rename(oldPath, newPath, (err) => { - if(err){ - reject(err) - } else { - resolve() - } - }) - }) -} - -/** - * Check if a drop-in mod is enabled. - * - * @param {string} fullName The fullName of the discovered mod to toggle. - * @returns {boolean} True if the mod is enabled, otherwise false. - */ -exports.isDropinModEnabled = function(fullName){ - return !fullName.endsWith(DISABLED_EXT) -} - -/** - * Scan for shaderpacks inside the shaderpacks folder. - * - * @param {string} instanceDir The path to the server instance directory. - * - * @returns {{fullName: string, name: string}[]} - * An array of objects storing metadata about each discovered shaderpack. - */ -exports.scanForShaderpacks = function(instanceDir){ - const shaderDir = path.join(instanceDir, SHADER_DIR) - const packsDiscovered = [{ - fullName: 'OFF', - name: 'Off (Default)' - }] - if(fs.existsSync(shaderDir)){ - let modCandidates = fs.readdirSync(shaderDir) - for(let file of modCandidates){ - const match = SHADER_REGEX.exec(file) - if(match != null){ - packsDiscovered.push({ - fullName: match[0], - name: match[1] - }) - } - } - } - return packsDiscovered -} - -/** - * Read the optionsshaders.txt file to locate the current - * enabled pack. If the file does not exist, OFF is returned. - * - * @param {string} instanceDir The path to the server instance directory. - * - * @returns {string} The file name of the enabled shaderpack. - */ -exports.getEnabledShaderpack = function(instanceDir){ - exports.validateDir(instanceDir) - - const optionsShaders = path.join(instanceDir, SHADER_CONFIG) - if(fs.existsSync(optionsShaders)){ - const buf = fs.readFileSync(optionsShaders, {encoding: 'utf-8'}) - const match = SHADER_OPTION.exec(buf) - if(match != null){ - return match[1] - } else { - console.warn('WARNING: Shaderpack regex failed.') - } - } - return 'OFF' -} - -/** - * Set the enabled shaderpack. - * - * @param {string} instanceDir The path to the server instance directory. - * @param {string} pack the file name of the shaderpack. - */ -exports.setEnabledShaderpack = function(instanceDir, pack){ - exports.validateDir(instanceDir) - - const optionsShaders = path.join(instanceDir, SHADER_CONFIG) - let buf - if(fs.existsSync(optionsShaders)){ - buf = fs.readFileSync(optionsShaders, {encoding: 'utf-8'}) - buf = buf.replace(SHADER_OPTION, `shaderPack=${pack}`) - } else { - buf = `shaderPack=${pack}` - } - fs.writeFileSync(optionsShaders, buf, {encoding: 'utf-8'}) -} - -/** - * Add shaderpacks. - * - * @param {FileList} files The files to add. - * @param {string} instanceDir The path to the server instance directory. - */ -exports.addShaderpacks = function(files, instanceDir) { - - const p = path.join(instanceDir, SHADER_DIR) - - exports.validateDir(p) - - for(let f of files) { - if(SHADER_REGEX.exec(f.name) != null) { - fs.moveSync(f.path, path.join(p, f.name)) - } - } - +const fs = require('fs-extra') +const path = require('path') +const { shell } = require('electron') + +// Group #1: File Name (without .disabled, if any) +// Group #2: File Extension (jar, zip, or litemod) +// Group #3: If it is disabled (if string 'disabled' is present) +const MOD_REGEX = /^(.+(jar|zip|litemod))(?:\.(disabled))?$/ +const DISABLED_EXT = '.disabled' + +const SHADER_REGEX = /^(.+)\.zip$/ +const SHADER_OPTION = /shaderPack=(.+)/ +const SHADER_DIR = 'shaderpacks' +const SHADER_CONFIG = 'optionsshaders.txt' + +/** + * Validate that the given directory exists. If not, it is + * created. + * + * @param {string} modsDir The path to the mods directory. + */ +exports.validateDir = function(dir) { + fs.ensureDirSync(dir) +} + +/** + * Scan for drop-in mods in both the mods folder and version + * safe mods folder. + * + * @param {string} modsDir The path to the mods directory. + * @param {string} version The minecraft version of the server configuration. + * + * @returns {{fullName: string, name: string, ext: string, disabled: boolean}[]} + * An array of objects storing metadata about each discovered mod. + */ +exports.scanForDropinMods = function(modsDir, version) { + const modsDiscovered = [] + if(fs.existsSync(modsDir)){ + let modCandidates = fs.readdirSync(modsDir) + let verCandidates = [] + const versionDir = path.join(modsDir, version) + if(fs.existsSync(versionDir)){ + verCandidates = fs.readdirSync(versionDir) + } + for(let file of modCandidates){ + const match = MOD_REGEX.exec(file) + if(match != null){ + modsDiscovered.push({ + fullName: match[0], + name: match[1], + ext: match[2], + disabled: match[3] != null + }) + } + } + for(let file of verCandidates){ + const match = MOD_REGEX.exec(file) + if(match != null){ + modsDiscovered.push({ + fullName: path.join(version, match[0]), + name: match[1], + ext: match[2], + disabled: match[3] != null + }) + } + } + } + return modsDiscovered +} + +/** + * Add dropin mods. + * + * @param {FileList} files The files to add. + * @param {string} modsDir The path to the mods directory. + */ +exports.addDropinMods = function(files, modsdir) { + + exports.validateDir(modsdir) + + for(let f of files) { + if(MOD_REGEX.exec(f.name) != null) { + fs.moveSync(f.path, path.join(modsdir, f.name)) + } + } + +} + +/** + * Delete a drop-in mod from the file system. + * + * @param {string} modsDir The path to the mods directory. + * @param {string} fullName The fullName of the discovered mod to delete. + * + * @returns {boolean} True if the mod was deleted, otherwise false. + */ +exports.deleteDropinMod = function(modsDir, fullName){ + const res = shell.moveItemToTrash(path.join(modsDir, fullName)) + if(!res){ + shell.beep() + } + return res +} + +/** + * Toggle a discovered mod on or off. This is achieved by either + * adding or disabling the .disabled extension to the local file. + * + * @param {string} modsDir The path to the mods directory. + * @param {string} fullName The fullName of the discovered mod to toggle. + * @param {boolean} enable Whether to toggle on or off the mod. + * + * @returns {Promise.} A promise which resolves when the mod has + * been toggled. If an IO error occurs the promise will be rejected. + */ +exports.toggleDropinMod = function(modsDir, fullName, enable){ + return new Promise((resolve, reject) => { + const oldPath = path.join(modsDir, fullName) + const newPath = path.join(modsDir, enable ? fullName.substring(0, fullName.indexOf(DISABLED_EXT)) : fullName + DISABLED_EXT) + + fs.rename(oldPath, newPath, (err) => { + if(err){ + reject(err) + } else { + resolve() + } + }) + }) +} + +/** + * Check if a drop-in mod is enabled. + * + * @param {string} fullName The fullName of the discovered mod to toggle. + * @returns {boolean} True if the mod is enabled, otherwise false. + */ +exports.isDropinModEnabled = function(fullName){ + return !fullName.endsWith(DISABLED_EXT) +} + +/** + * Scan for shaderpacks inside the shaderpacks folder. + * + * @param {string} instanceDir The path to the server instance directory. + * + * @returns {{fullName: string, name: string}[]} + * An array of objects storing metadata about each discovered shaderpack. + */ +exports.scanForShaderpacks = function(instanceDir){ + const shaderDir = path.join(instanceDir, SHADER_DIR) + const packsDiscovered = [{ + fullName: 'OFF', + name: 'Off (Default)' + }] + if(fs.existsSync(shaderDir)){ + let modCandidates = fs.readdirSync(shaderDir) + for(let file of modCandidates){ + const match = SHADER_REGEX.exec(file) + if(match != null){ + packsDiscovered.push({ + fullName: match[0], + name: match[1] + }) + } + } + } + return packsDiscovered +} + +/** + * Read the optionsshaders.txt file to locate the current + * enabled pack. If the file does not exist, OFF is returned. + * + * @param {string} instanceDir The path to the server instance directory. + * + * @returns {string} The file name of the enabled shaderpack. + */ +exports.getEnabledShaderpack = function(instanceDir){ + exports.validateDir(instanceDir) + + const optionsShaders = path.join(instanceDir, SHADER_CONFIG) + if(fs.existsSync(optionsShaders)){ + const buf = fs.readFileSync(optionsShaders, {encoding: 'utf-8'}) + const match = SHADER_OPTION.exec(buf) + if(match != null){ + return match[1] + } else { + console.warn('WARNING: Shaderpack regex failed.') + } + } + return 'OFF' +} + +/** + * Set the enabled shaderpack. + * + * @param {string} instanceDir The path to the server instance directory. + * @param {string} pack the file name of the shaderpack. + */ +exports.setEnabledShaderpack = function(instanceDir, pack){ + exports.validateDir(instanceDir) + + const optionsShaders = path.join(instanceDir, SHADER_CONFIG) + let buf + if(fs.existsSync(optionsShaders)){ + buf = fs.readFileSync(optionsShaders, {encoding: 'utf-8'}) + buf = buf.replace(SHADER_OPTION, `shaderPack=${pack}`) + } else { + buf = `shaderPack=${pack}` + } + fs.writeFileSync(optionsShaders, buf, {encoding: 'utf-8'}) +} + +/** + * Add shaderpacks. + * + * @param {FileList} files The files to add. + * @param {string} instanceDir The path to the server instance directory. + */ +exports.addShaderpacks = function(files, instanceDir) { + + const p = path.join(instanceDir, SHADER_DIR) + + exports.validateDir(p) + + for(let f of files) { + if(SHADER_REGEX.exec(f.name) != null) { + fs.moveSync(f.path, path.join(p, f.name)) + } + } + } \ No newline at end of file diff --git a/app/assets/js/isdev.js b/app/assets/js/isdev.js index 1ed55e5b..e1135334 100644 --- a/app/assets/js/isdev.js +++ b/app/assets/js/isdev.js @@ -1,5 +1,5 @@ -'use strict' -const getFromEnv = parseInt(process.env.ELECTRON_IS_DEV, 10) === 1 -const isEnvSet = 'ELECTRON_IS_DEV' in process.env - +'use strict' +const getFromEnv = parseInt(process.env.ELECTRON_IS_DEV, 10) === 1 +const isEnvSet = 'ELECTRON_IS_DEV' in process.env + module.exports = isEnvSet ? getFromEnv : (process.defaultApp || /node_modules[\\/]electron[\\/]/.test(process.execPath)) \ No newline at end of file diff --git a/app/assets/js/langloader.js b/app/assets/js/langloader.js index 24ab84ae..3d53fb7b 100644 --- a/app/assets/js/langloader.js +++ b/app/assets/js/langloader.js @@ -1,21 +1,21 @@ -const fs = require('fs-extra') -const path = require('path') - -let lang - -exports.loadLanguage = function(id){ - lang = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'lang', `${id}.json`))) || {} -} - -exports.query = function(id){ - let query = id.split('.') - let res = lang - for(let q of query){ - res = res[q] - } - return res === lang ? {} : res -} - -exports.queryJS = function(id){ - return exports.query(`js.${id}`) +const fs = require('fs-extra') +const path = require('path') + +let lang + +exports.loadLanguage = function(id){ + lang = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'lang', `${id}.json`))) || {} +} + +exports.query = function(id){ + let query = id.split('.') + let res = lang + for(let q of query){ + res = res[q] + } + return res === lang ? {} : res +} + +exports.queryJS = function(id){ + return exports.query(`js.${id}`) } \ No newline at end of file diff --git a/app/assets/js/loggerutil.js b/app/assets/js/loggerutil.js index 73899418..014ae6c7 100644 --- a/app/assets/js/loggerutil.js +++ b/app/assets/js/loggerutil.js @@ -1,32 +1,32 @@ -class LoggerUtil { - - constructor(prefix, style){ - this.prefix = prefix - this.style = style - } - - log(){ - console.log.apply(null, [this.prefix, this.style, ...arguments]) - } - - info(){ - console.info.apply(null, [this.prefix, this.style, ...arguments]) - } - - warn(){ - console.warn.apply(null, [this.prefix, this.style, ...arguments]) - } - - debug(){ - console.debug.apply(null, [this.prefix, this.style, ...arguments]) - } - - error(){ - console.error.apply(null, [this.prefix, this.style, ...arguments]) - } - -} - -module.exports = function (prefix, style){ - return new LoggerUtil(prefix, style) +class LoggerUtil { + + constructor(prefix, style){ + this.prefix = prefix + this.style = style + } + + log(){ + console.log.apply(null, [this.prefix, this.style, ...arguments]) + } + + info(){ + console.info.apply(null, [this.prefix, this.style, ...arguments]) + } + + warn(){ + console.warn.apply(null, [this.prefix, this.style, ...arguments]) + } + + debug(){ + console.debug.apply(null, [this.prefix, this.style, ...arguments]) + } + + error(){ + console.error.apply(null, [this.prefix, this.style, ...arguments]) + } + +} + +module.exports = function (prefix, style){ + return new LoggerUtil(prefix, style) } \ No newline at end of file diff --git a/app/assets/js/mojang.js b/app/assets/js/mojang.js index 75143836..16bc729b 100644 --- a/app/assets/js/mojang.js +++ b/app/assets/js/mojang.js @@ -1,271 +1,271 @@ -/** - * Mojang - * - * This module serves as a minimal wrapper for Mojang's REST api. - * - * @module mojang - */ -// Requirements -const request = require('request') -const logger = require('./loggerutil')('%c[Mojang]', 'color: #a02d2a; font-weight: bold') - -// Constants -const minecraftAgent = { - name: 'Minecraft', - version: 1 -} -const authpath = 'https://authserver.mojang.com' -const statuses = [ - { - service: 'sessionserver.mojang.com', - status: 'grey', - name: 'Multiplayer Session Service', - essential: true - }, - { - service: 'authserver.mojang.com', - status: 'grey', - name: 'Authentication Service', - essential: true - }, - { - service: 'textures.minecraft.net', - status: 'grey', - name: 'Minecraft Skins', - essential: false - }, - { - service: 'api.mojang.com', - status: 'grey', - name: 'Public API', - essential: false - }, - { - service: 'minecraft.net', - status: 'grey', - name: 'Minecraft.net', - essential: false - }, - { - service: 'account.mojang.com', - status: 'grey', - name: 'Mojang Accounts Website', - essential: false - } -] - -// Functions - -/** - * Converts a Mojang status color to a hex value. Valid statuses - * are 'green', 'yellow', 'red', and 'grey'. Grey is a custom status - * to our project which represents an unknown status. - * - * @param {string} status A valid status code. - * @returns {string} The hex color of the status code. - */ -exports.statusToHex = function(status){ - switch(status.toLowerCase()){ - case 'green': - return '#a5c325' - case 'yellow': - return '#eac918' - case 'red': - return '#c32625' - case 'grey': - default: - return '#848484' - } -} - -/** - * Retrieves the status of Mojang's services. - * The response is condensed into a single object. Each service is - * a key, where the value is an object containing a status and name - * property. - * - * @see http://wiki.vg/Mojang_API#API_Status - */ -exports.status = function(){ - return new Promise((resolve, reject) => { - request.get('https://status.mojang.com/check', - { - json: true, - timeout: 2500 - }, - function(error, response, body){ - - if(error || response.statusCode !== 200){ - logger.warn('Unable to retrieve Mojang status.') - logger.debug('Error while retrieving Mojang statuses:', error) - //reject(error || response.statusCode) - for(let i=0; i { - - const body = { - agent, - username, - password, - requestUser - } - if(clientToken != null){ - body.clientToken = clientToken - } - - request.post(authpath + '/authenticate', - { - json: true, - body - }, - function(error, response, body){ - if(error){ - logger.error('Error during authentication.', error) - reject(error) - } else { - if(response.statusCode === 200){ - resolve(body) - } else { - reject(body || {code: 'ENOTFOUND'}) - } - } - }) - }) -} - -/** - * Validate an access token. This should always be done before launching. - * The client token should match the one used to create the access token. - * - * @param {string} accessToken The access token to validate. - * @param {string} clientToken The launcher's client token. - * - * @see http://wiki.vg/Authentication#Validate - */ -exports.validate = function(accessToken, clientToken){ - return new Promise((resolve, reject) => { - request.post(authpath + '/validate', - { - json: true, - body: { - accessToken, - clientToken - } - }, - function(error, response, body){ - if(error){ - logger.error('Error during validation.', error) - reject(error) - } else { - if(response.statusCode === 403){ - resolve(false) - } else { - // 204 if valid - resolve(true) - } - } - }) - }) -} - -/** - * Invalidates an access token. The clientToken must match the - * token used to create the provided accessToken. - * - * @param {string} accessToken The access token to invalidate. - * @param {string} clientToken The launcher's client token. - * - * @see http://wiki.vg/Authentication#Invalidate - */ -exports.invalidate = function(accessToken, clientToken){ - return new Promise((resolve, reject) => { - request.post(authpath + '/invalidate', - { - json: true, - body: { - accessToken, - clientToken - } - }, - function(error, response, body){ - if(error){ - logger.error('Error during invalidation.', error) - reject(error) - } else { - if(response.statusCode === 204){ - resolve() - } else { - reject(body) - } - } - }) - }) -} - -/** - * Refresh a user's authentication. This should be used to keep a user logged - * in without asking them for their credentials again. A new access token will - * be generated using a recent invalid access token. See Wiki for more info. - * - * @param {string} accessToken The old access token. - * @param {string} clientToken The launcher's client token. - * @param {boolean} requestUser Optional. Adds user object to the reponse. - * - * @see http://wiki.vg/Authentication#Refresh - */ -exports.refresh = function(accessToken, clientToken, requestUser = true){ - return new Promise((resolve, reject) => { - request.post(authpath + '/refresh', - { - json: true, - body: { - accessToken, - clientToken, - requestUser - } - }, - function(error, response, body){ - if(error){ - logger.error('Error during refresh.', error) - reject(error) - } else { - if(response.statusCode === 200){ - resolve(body) - } else { - reject(body) - } - } - }) - }) +/** + * Mojang + * + * This module serves as a minimal wrapper for Mojang's REST api. + * + * @module mojang + */ +// Requirements +const request = require('request') +const logger = require('./loggerutil')('%c[Mojang]', 'color: #a02d2a; font-weight: bold') + +// Constants +const minecraftAgent = { + name: 'Minecraft', + version: 1 +} +const authpath = 'https://authserver.mojang.com' +const statuses = [ + { + service: 'sessionserver.mojang.com', + status: 'grey', + name: 'Multiplayer Session Service', + essential: true + }, + { + service: 'authserver.mojang.com', + status: 'grey', + name: 'Authentication Service', + essential: true + }, + { + service: 'textures.minecraft.net', + status: 'grey', + name: 'Minecraft Skins', + essential: false + }, + { + service: 'api.mojang.com', + status: 'grey', + name: 'Public API', + essential: false + }, + { + service: 'minecraft.net', + status: 'grey', + name: 'Minecraft.net', + essential: false + }, + { + service: 'account.mojang.com', + status: 'grey', + name: 'Mojang Accounts Website', + essential: false + } +] + +// Functions + +/** + * Converts a Mojang status color to a hex value. Valid statuses + * are 'green', 'yellow', 'red', and 'grey'. Grey is a custom status + * to our project which represents an unknown status. + * + * @param {string} status A valid status code. + * @returns {string} The hex color of the status code. + */ +exports.statusToHex = function(status){ + switch(status.toLowerCase()){ + case 'green': + return '#a5c325' + case 'yellow': + return '#eac918' + case 'red': + return '#c32625' + case 'grey': + default: + return '#848484' + } +} + +/** + * Retrieves the status of Mojang's services. + * The response is condensed into a single object. Each service is + * a key, where the value is an object containing a status and name + * property. + * + * @see http://wiki.vg/Mojang_API#API_Status + */ +exports.status = function(){ + return new Promise((resolve, reject) => { + request.get('https://status.mojang.com/check', + { + json: true, + timeout: 2500 + }, + function(error, response, body){ + + if(error || response.statusCode !== 200){ + logger.warn('Unable to retrieve Mojang status.') + logger.debug('Error while retrieving Mojang statuses:', error) + //reject(error || response.statusCode) + for(let i=0; i { + + const body = { + agent, + username, + password, + requestUser + } + if(clientToken != null){ + body.clientToken = clientToken + } + + request.post(authpath + '/authenticate', + { + json: true, + body + }, + function(error, response, body){ + if(error){ + logger.error('Error during authentication.', error) + reject(error) + } else { + if(response.statusCode === 200){ + resolve(body) + } else { + reject(body || {code: 'ENOTFOUND'}) + } + } + }) + }) +} + +/** + * Validate an access token. This should always be done before launching. + * The client token should match the one used to create the access token. + * + * @param {string} accessToken The access token to validate. + * @param {string} clientToken The launcher's client token. + * + * @see http://wiki.vg/Authentication#Validate + */ +exports.validate = function(accessToken, clientToken){ + return new Promise((resolve, reject) => { + request.post(authpath + '/validate', + { + json: true, + body: { + accessToken, + clientToken + } + }, + function(error, response, body){ + if(error){ + logger.error('Error during validation.', error) + reject(error) + } else { + if(response.statusCode === 403){ + resolve(false) + } else { + // 204 if valid + resolve(true) + } + } + }) + }) +} + +/** + * Invalidates an access token. The clientToken must match the + * token used to create the provided accessToken. + * + * @param {string} accessToken The access token to invalidate. + * @param {string} clientToken The launcher's client token. + * + * @see http://wiki.vg/Authentication#Invalidate + */ +exports.invalidate = function(accessToken, clientToken){ + return new Promise((resolve, reject) => { + request.post(authpath + '/invalidate', + { + json: true, + body: { + accessToken, + clientToken + } + }, + function(error, response, body){ + if(error){ + logger.error('Error during invalidation.', error) + reject(error) + } else { + if(response.statusCode === 204){ + resolve() + } else { + reject(body) + } + } + }) + }) +} + +/** + * Refresh a user's authentication. This should be used to keep a user logged + * in without asking them for their credentials again. A new access token will + * be generated using a recent invalid access token. See Wiki for more info. + * + * @param {string} accessToken The old access token. + * @param {string} clientToken The launcher's client token. + * @param {boolean} requestUser Optional. Adds user object to the reponse. + * + * @see http://wiki.vg/Authentication#Refresh + */ +exports.refresh = function(accessToken, clientToken, requestUser = true){ + return new Promise((resolve, reject) => { + request.post(authpath + '/refresh', + { + json: true, + body: { + accessToken, + clientToken, + requestUser + } + }, + function(error, response, body){ + if(error){ + logger.error('Error during refresh.', error) + reject(error) + } else { + if(response.statusCode === 200){ + resolve(body) + } else { + reject(body) + } + } + }) + }) } \ No newline at end of file diff --git a/app/assets/js/preloader.js b/app/assets/js/preloader.js index 792c5304..20781d0b 100644 --- a/app/assets/js/preloader.js +++ b/app/assets/js/preloader.js @@ -1,69 +1,69 @@ -const {ipcRenderer} = require('electron') -const fs = require('fs-extra') -const os = require('os') -const path = require('path') - -const ConfigManager = require('./configmanager') -const DistroManager = require('./distromanager') -const LangLoader = require('./langloader') -const logger = require('./loggerutil')('%c[Preloader]', 'color: #a02d2a; font-weight: bold') - -logger.log('Loading..') - -// Load ConfigManager -ConfigManager.load() - -// Load Strings -LangLoader.loadLanguage('en_US') - -function onDistroLoad(data){ - if(data != null){ - - // Resolve the selected server if its value has yet to be set. - if(ConfigManager.getSelectedServer() == null || data.getServer(ConfigManager.getSelectedServer()) == null){ - logger.log('Determining default selected server..') - ConfigManager.setSelectedServer(data.getMainServer().getID()) - ConfigManager.save() - } - } - ipcRenderer.send('distributionIndexDone', data != null) -} - -// Ensure Distribution is downloaded and cached. -DistroManager.pullRemote().then((data) => { - logger.log('Loaded distribution index.') - - onDistroLoad(data) - -}).catch((err) => { - logger.log('Failed to load distribution index.') - logger.error(err) - - logger.log('Attempting to load an older version of the distribution index.') - // Try getting a local copy, better than nothing. - DistroManager.pullLocal().then((data) => { - logger.log('Successfully loaded an older version of the distribution index.') - - onDistroLoad(data) - - - }).catch((err) => { - - logger.log('Failed to load an older version of the distribution index.') - logger.log('Application cannot run.') - logger.error(err) - - onDistroLoad(null) - - }) - -}) - -// Clean up temp dir incase previous launches ended unexpectedly. -fs.remove(path.join(os.tmpdir(), ConfigManager.getTempNativeFolder()), (err) => { - if(err){ - logger.warn('Error while cleaning natives directory', err) - } else { - logger.log('Cleaned natives directory.') - } +const {ipcRenderer} = require('electron') +const fs = require('fs-extra') +const os = require('os') +const path = require('path') + +const ConfigManager = require('./configmanager') +const DistroManager = require('./distromanager') +const LangLoader = require('./langloader') +const logger = require('./loggerutil')('%c[Preloader]', 'color: #a02d2a; font-weight: bold') + +logger.log('Loading..') + +// Load ConfigManager +ConfigManager.load() + +// Load Strings +LangLoader.loadLanguage('en_US') + +function onDistroLoad(data){ + if(data != null){ + + // Resolve the selected server if its value has yet to be set. + if(ConfigManager.getSelectedServer() == null || data.getServer(ConfigManager.getSelectedServer()) == null){ + logger.log('Determining default selected server..') + ConfigManager.setSelectedServer(data.getMainServer().getID()) + ConfigManager.save() + } + } + ipcRenderer.send('distributionIndexDone', data != null) +} + +// Ensure Distribution is downloaded and cached. +DistroManager.pullRemote().then((data) => { + logger.log('Loaded distribution index.') + + onDistroLoad(data) + +}).catch((err) => { + logger.log('Failed to load distribution index.') + logger.error(err) + + logger.log('Attempting to load an older version of the distribution index.') + // Try getting a local copy, better than nothing. + DistroManager.pullLocal().then((data) => { + logger.log('Successfully loaded an older version of the distribution index.') + + onDistroLoad(data) + + + }).catch((err) => { + + logger.log('Failed to load an older version of the distribution index.') + logger.log('Application cannot run.') + logger.error(err) + + onDistroLoad(null) + + }) + +}) + +// Clean up temp dir incase previous launches ended unexpectedly. +fs.remove(path.join(os.tmpdir(), ConfigManager.getTempNativeFolder()), (err) => { + if(err){ + logger.warn('Error while cleaning natives directory', err) + } else { + logger.log('Cleaned natives directory.') + } }) \ No newline at end of file diff --git a/app/assets/js/scripts/landing.js b/app/assets/js/scripts/landing.js index 7ea1f2d5..42019632 100644 --- a/app/assets/js/scripts/landing.js +++ b/app/assets/js/scripts/landing.js @@ -1,1144 +1,1144 @@ -/** - * Script for landing.ejs - */ -// Requirements -const cp = require('child_process') -const crypto = require('crypto') -const {URL} = require('url') - -// Internal Requirements -const DiscordWrapper = require('./assets/js/discordwrapper') -const Mojang = require('./assets/js/mojang') -const ProcessBuilder = require('./assets/js/processbuilder') -const ServerStatus = require('./assets/js/serverstatus') - -// Launch Elements -const launch_content = document.getElementById('launch_content') -const launch_details = document.getElementById('launch_details') -const launch_progress = document.getElementById('launch_progress') -const launch_progress_label = document.getElementById('launch_progress_label') -const launch_details_text = document.getElementById('launch_details_text') -const server_selection_button = document.getElementById('server_selection_button') -const user_text = document.getElementById('user_text') - -const loggerLanding = LoggerUtil('%c[Landing]', 'color: #000668; font-weight: bold') - -/* Launch Progress Wrapper Functions */ - -/** - * Show/hide the loading area. - * - * @param {boolean} loading True if the loading area should be shown, otherwise false. - */ -function toggleLaunchArea(loading){ - if(loading){ - launch_details.style.display = 'flex' - launch_content.style.display = 'none' - } else { - launch_details.style.display = 'none' - launch_content.style.display = 'inline-flex' - } -} - -/** - * Set the details text of the loading area. - * - * @param {string} details The new text for the loading details. - */ -function setLaunchDetails(details){ - launch_details_text.innerHTML = details -} - -/** - * Set the value of the loading progress bar and display that value. - * - * @param {number} value The progress value. - * @param {number} max The total size. - * @param {number|string} percent Optional. The percentage to display on the progress label. - */ -function setLaunchPercentage(value, max, percent = ((value/max)*100)){ - launch_progress.setAttribute('max', max) - launch_progress.setAttribute('value', value) - launch_progress_label.innerHTML = percent + '%' -} - -/** - * Set the value of the OS progress bar and display that on the UI. - * - * @param {number} value The progress value. - * @param {number} max The total download size. - * @param {number|string} percent Optional. The percentage to display on the progress label. - */ -function setDownloadPercentage(value, max, percent = ((value/max)*100)){ - remote.getCurrentWindow().setProgressBar(value/max) - setLaunchPercentage(value, max, percent) -} - -/** - * Enable or disable the launch button. - * - * @param {boolean} val True to enable, false to disable. - */ -function setLaunchEnabled(val){ - document.getElementById('launch_button').disabled = !val -} - -// Bind launch button -document.getElementById('launch_button').addEventListener('click', function(e){ - loggerLanding.log('Launching game..') - const mcVersion = DistroManager.getDistribution().getServer(ConfigManager.getSelectedServer()).getMinecraftVersion() - const jExe = ConfigManager.getJavaExecutable() - if(jExe == null){ - asyncSystemScan(mcVersion) - } else { - - setLaunchDetails(Lang.queryJS('landing.launch.pleaseWait')) - toggleLaunchArea(true) - setLaunchPercentage(0, 100) - - const jg = new JavaGuard(mcVersion) - jg._validateJavaBinary(jExe).then((v) => { - loggerLanding.log('Java version meta', v) - if(v.valid){ - dlAsync() - } else { - asyncSystemScan(mcVersion) - } - }) - } -}) - -// Bind settings button -document.getElementById('settingsMediaButton').onclick = (e) => { - prepareSettings() - switchView(getCurrentView(), VIEWS.settings) -} - -// Bind avatar overlay button. -document.getElementById('avatarOverlay').onclick = (e) => { - prepareSettings() - switchView(getCurrentView(), VIEWS.settings, 500, 500, () => { - settingsNavItemListener(document.getElementById('settingsNavAccount'), false) - }) -} - -// Bind selected account -function updateSelectedAccount(authUser){ - let username = 'No Account Selected' - if(authUser != null){ - if(authUser.displayName != null){ - username = authUser.displayName - } - if(authUser.uuid != null){ - document.getElementById('avatarContainer').style.backgroundImage = `url('https://crafatar.com/renders/body/${authUser.uuid}')` - } - } - user_text.innerHTML = username -} -updateSelectedAccount(ConfigManager.getSelectedAccount()) - -// Bind selected server -function updateSelectedServer(serv){ - if(getCurrentView() === VIEWS.settings){ - saveAllModConfigurations() - } - ConfigManager.setSelectedServer(serv != null ? serv.getID() : null) - ConfigManager.save() - server_selection_button.innerHTML = '\u2022 ' + (serv != null ? serv.getName() : 'No Server Selected') - if(getCurrentView() === VIEWS.settings){ - animateModsTabRefresh() - } - setLaunchEnabled(serv != null) -} -// Real text is set in uibinder.js on distributionIndexDone. -server_selection_button.innerHTML = '\u2022 Loading..' -server_selection_button.onclick = (e) => { - e.target.blur() - toggleServerSelection(true) -} - -// Update Mojang Status Color -const refreshMojangStatuses = async function(){ - loggerLanding.log('Refreshing Mojang Statuses..') - - let status = 'grey' - let tooltipEssentialHTML = '' - let tooltipNonEssentialHTML = '' - - try { - const statuses = await Mojang.status() - greenCount = 0 - greyCount = 0 - - for(let i=0; i - - ${service.name} - ` - } else { - tooltipNonEssentialHTML += `
- - ${service.name} -
` - } - - if(service.status === 'yellow' && status !== 'red'){ - status = 'yellow' - } else if(service.status === 'red'){ - status = 'red' - } else { - if(service.status === 'grey'){ - ++greyCount - } - ++greenCount - } - - } - - if(greenCount === statuses.length){ - if(greyCount === statuses.length){ - status = 'grey' - } else { - status = 'green' - } - } - - } catch (err) { - loggerLanding.warn('Unable to refresh Mojang service status.') - loggerLanding.debug(err) - } - - document.getElementById('mojangStatusEssentialContainer').innerHTML = tooltipEssentialHTML - document.getElementById('mojangStatusNonEssentialContainer').innerHTML = tooltipNonEssentialHTML - document.getElementById('mojang_status_icon').style.color = Mojang.statusToHex(status) -} - -const refreshServerStatus = async function(fade = false){ - loggerLanding.log('Refreshing Server Status') - const serv = DistroManager.getDistribution().getServer(ConfigManager.getSelectedServer()) - - let pLabel = 'SERVER' - let pVal = 'OFFLINE' - - try { - const serverURL = new URL('my://' + serv.getAddress()) - const servStat = await ServerStatus.getStatus(serverURL.hostname, serverURL.port) - if(servStat.online){ - pLabel = 'PLAYERS' - pVal = servStat.onlinePlayers + '/' + servStat.maxPlayers - } - - } catch (err) { - loggerLanding.warn('Unable to refresh server status, assuming offline.') - loggerLanding.debug(err) - } - if(fade){ - $('#server_status_wrapper').fadeOut(250, () => { - document.getElementById('landingPlayerLabel').innerHTML = pLabel - document.getElementById('player_count').innerHTML = pVal - $('#server_status_wrapper').fadeIn(500) - }) - } else { - document.getElementById('landingPlayerLabel').innerHTML = pLabel - document.getElementById('player_count').innerHTML = pVal - } - -} - -refreshMojangStatuses() -// Server Status is refreshed in uibinder.js on distributionIndexDone. - -// Set refresh rate to once every 5 minutes. -let mojangStatusListener = setInterval(() => refreshMojangStatuses(true), 300000) -let serverStatusListener = setInterval(() => refreshServerStatus(true), 300000) - -/** - * Shows an error overlay, toggles off the launch area. - * - * @param {string} title The overlay title. - * @param {string} desc The overlay description. - */ -function showLaunchFailure(title, desc){ - setOverlayContent( - title, - desc, - 'Okay' - ) - setOverlayHandler(null) - toggleOverlay(true) - toggleLaunchArea(false) -} - -/* System (Java) Scan */ - -let sysAEx -let scanAt - -let extractListener - -/** - * Asynchronously scan the system for valid Java installations. - * - * @param {string} mcVersion The Minecraft version we are scanning for. - * @param {boolean} launchAfter Whether we should begin to launch after scanning. - */ -function asyncSystemScan(mcVersion, launchAfter = true){ - - setLaunchDetails('Please wait..') - toggleLaunchArea(true) - setLaunchPercentage(0, 100) - - const loggerSysAEx = LoggerUtil('%c[SysAEx]', 'color: #353232; font-weight: bold') - - const forkEnv = JSON.parse(JSON.stringify(process.env)) - forkEnv.CONFIG_DIRECT_PATH = ConfigManager.getLauncherDirectory() - - // Fork a process to run validations. - sysAEx = cp.fork(path.join(__dirname, 'assets', 'js', 'assetexec.js'), [ - 'JavaGuard', - mcVersion - ], { - env: forkEnv, - stdio: 'pipe' - }) - // Stdout - sysAEx.stdio[1].setEncoding('utf8') - sysAEx.stdio[1].on('data', (data) => { - loggerSysAEx.log(data) - }) - // Stderr - sysAEx.stdio[2].setEncoding('utf8') - sysAEx.stdio[2].on('data', (data) => { - loggerSysAEx.log(data) - }) - - sysAEx.on('message', (m) => { - - if(m.context === 'validateJava'){ - if(m.result == null){ - // If the result is null, no valid Java installation was found. - // Show this information to the user. - setOverlayContent( - 'No Compatible
Java Installation Found', - 'In order to join WesterosCraft, you need a 64-bit installation of Java 8. Would you like us to install a copy? By installing, you accept Oracle\'s license agreement.', - 'Install Java', - 'Install Manually' - ) - setOverlayHandler(() => { - setLaunchDetails('Preparing Java Download..') - sysAEx.send({task: 'changeContext', class: 'AssetGuard', args: [ConfigManager.getCommonDirectory(),ConfigManager.getJavaExecutable()]}) - sysAEx.send({task: 'execute', function: '_enqueueOpenJDK', argsArr: [ConfigManager.getDataDirectory()]}) - toggleOverlay(false) - }) - setDismissHandler(() => { - $('#overlayContent').fadeOut(250, () => { - //$('#overlayDismiss').toggle(false) - setOverlayContent( - 'Java is Required
to Launch', - 'A valid x64 installation of Java 8 is required to launch.

Please refer to our Java Management Guide for instructions on how to manually install Java.', - 'I Understand', - 'Go Back' - ) - setOverlayHandler(() => { - toggleLaunchArea(false) - toggleOverlay(false) - }) - setDismissHandler(() => { - toggleOverlay(false, true) - asyncSystemScan() - }) - $('#overlayContent').fadeIn(250) - }) - }) - toggleOverlay(true, true) - - } else { - // Java installation found, use this to launch the game. - ConfigManager.setJavaExecutable(m.result) - ConfigManager.save() - - // We need to make sure that the updated value is on the settings UI. - // Just incase the settings UI is already open. - settingsJavaExecVal.value = m.result - populateJavaExecDetails(settingsJavaExecVal.value) - - if(launchAfter){ - dlAsync() - } - sysAEx.disconnect() - } - } else if(m.context === '_enqueueOpenJDK'){ - - if(m.result === true){ - - // Oracle JRE enqueued successfully, begin download. - setLaunchDetails('Downloading Java..') - sysAEx.send({task: 'execute', function: 'processDlQueues', argsArr: [[{id:'java', limit:1}]]}) - - } else { - - // Oracle JRE enqueue failed. Probably due to a change in their website format. - // User will have to follow the guide to install Java. - setOverlayContent( - 'Unexpected Issue:
Java Download Failed', - 'Unfortunately we\'ve encountered an issue while attempting to install Java. You will need to manually install a copy. Please check out our Troubleshooting Guide for more details and instructions.', - 'I Understand' - ) - setOverlayHandler(() => { - toggleOverlay(false) - toggleLaunchArea(false) - }) - toggleOverlay(true) - sysAEx.disconnect() - - } - - } else if(m.context === 'progress'){ - - switch(m.data){ - case 'download': - // Downloading.. - setDownloadPercentage(m.value, m.total, m.percent) - break - } - - } else if(m.context === 'complete'){ - - switch(m.data){ - case 'download': { - // Show installing progress bar. - remote.getCurrentWindow().setProgressBar(2) - - // Wait for extration to complete. - const eLStr = 'Extracting' - let dotStr = '' - setLaunchDetails(eLStr) - extractListener = setInterval(() => { - if(dotStr.length >= 3){ - dotStr = '' - } else { - dotStr += '.' - } - setLaunchDetails(eLStr + dotStr) - }, 750) - break - } - case 'java': - // Download & extraction complete, remove the loading from the OS progress bar. - remote.getCurrentWindow().setProgressBar(-1) - - // Extraction completed successfully. - ConfigManager.setJavaExecutable(m.args[0]) - ConfigManager.save() - - if(extractListener != null){ - clearInterval(extractListener) - extractListener = null - } - - setLaunchDetails('Java Installed!') - - if(launchAfter){ - dlAsync() - } - - sysAEx.disconnect() - break - } - - } else if(m.context === 'error'){ - console.log(m.error) - } - }) - - // Begin system Java scan. - setLaunchDetails('Checking system info..') - sysAEx.send({task: 'execute', function: 'validateJava', argsArr: [ConfigManager.getDataDirectory()]}) - -} - -// Keep reference to Minecraft Process -let proc -// Is DiscordRPC enabled -let hasRPC = false -// Joined server regex -// Change this if your server uses something different. -const SERVER_JOINED_REGEX = /\[.+\]: \[CHAT\] [a-zA-Z0-9_]{1,16} joined the game/ -const GAME_JOINED_REGEX = /\[.+\]: Sound engine started/ -const GAME_LAUNCH_REGEX = /^\[.+\]: (?:MinecraftForge .+ Initialized|ModLauncher .+ starting: .+)$/ -const MIN_LINGER = 5000 - -let aEx -let serv -let versionData -let forgeData - -let progressListener - -function dlAsync(login = true){ - - // Login parameter is temporary for debug purposes. Allows testing the validation/downloads without - // launching the game. - - if(login) { - if(ConfigManager.getSelectedAccount() == null){ - loggerLanding.error('You must be logged into an account.') - return - } - } - - setLaunchDetails('Please wait..') - toggleLaunchArea(true) - setLaunchPercentage(0, 100) - - const loggerAEx = LoggerUtil('%c[AEx]', 'color: #353232; font-weight: bold') - const loggerLaunchSuite = LoggerUtil('%c[LaunchSuite]', 'color: #000668; font-weight: bold') - - const forkEnv = JSON.parse(JSON.stringify(process.env)) - forkEnv.CONFIG_DIRECT_PATH = ConfigManager.getLauncherDirectory() - - // Start AssetExec to run validations and downloads in a forked process. - aEx = cp.fork(path.join(__dirname, 'assets', 'js', 'assetexec.js'), [ - 'AssetGuard', - ConfigManager.getCommonDirectory(), - ConfigManager.getJavaExecutable() - ], { - env: forkEnv, - stdio: 'pipe' - }) - // Stdout - aEx.stdio[1].setEncoding('utf8') - aEx.stdio[1].on('data', (data) => { - loggerAEx.log(data) - }) - // Stderr - aEx.stdio[2].setEncoding('utf8') - aEx.stdio[2].on('data', (data) => { - loggerAEx.log(data) - }) - aEx.on('error', (err) => { - loggerLaunchSuite.error('Error during launch', err) - showLaunchFailure('Error During Launch', err.message || 'See console (CTRL + Shift + i) for more details.') - }) - aEx.on('close', (code, signal) => { - if(code !== 0){ - loggerLaunchSuite.error(`AssetExec exited with code ${code}, assuming error.`) - showLaunchFailure('Error During Launch', 'See console (CTRL + Shift + i) for more details.') - } - }) - - // Establish communications between the AssetExec and current process. - aEx.on('message', (m) => { - - if(m.context === 'validate'){ - switch(m.data){ - case 'distribution': - setLaunchPercentage(20, 100) - loggerLaunchSuite.log('Validated distibution index.') - setLaunchDetails('Loading version information..') - break - case 'version': - setLaunchPercentage(40, 100) - loggerLaunchSuite.log('Version data loaded.') - setLaunchDetails('Validating asset integrity..') - break - case 'assets': - setLaunchPercentage(60, 100) - loggerLaunchSuite.log('Asset Validation Complete') - setLaunchDetails('Validating library integrity..') - break - case 'libraries': - setLaunchPercentage(80, 100) - loggerLaunchSuite.log('Library validation complete.') - setLaunchDetails('Validating miscellaneous file integrity..') - break - case 'files': - setLaunchPercentage(100, 100) - loggerLaunchSuite.log('File validation complete.') - setLaunchDetails('Downloading files..') - break - } - } else if(m.context === 'progress'){ - switch(m.data){ - case 'assets': { - const perc = (m.value/m.total)*20 - setLaunchPercentage(40+perc, 100, parseInt(40+perc)) - break - } - case 'download': - setDownloadPercentage(m.value, m.total, m.percent) - break - case 'extract': { - // Show installing progress bar. - remote.getCurrentWindow().setProgressBar(2) - - // Download done, extracting. - const eLStr = 'Extracting libraries' - let dotStr = '' - setLaunchDetails(eLStr) - progressListener = setInterval(() => { - if(dotStr.length >= 3){ - dotStr = '' - } else { - dotStr += '.' - } - setLaunchDetails(eLStr + dotStr) - }, 750) - break - } - } - } else if(m.context === 'complete'){ - switch(m.data){ - case 'download': - // Download and extraction complete, remove the loading from the OS progress bar. - remote.getCurrentWindow().setProgressBar(-1) - if(progressListener != null){ - clearInterval(progressListener) - progressListener = null - } - - setLaunchDetails('Preparing to launch..') - break - } - } else if(m.context === 'error'){ - switch(m.data){ - case 'download': - loggerLaunchSuite.error('Error while downloading:', m.error) - - if(m.error.code === 'ENOENT'){ - showLaunchFailure( - 'Download Error', - 'Could not connect to the file server. Ensure that you are connected to the internet and try again.' - ) - } else { - showLaunchFailure( - 'Download Error', - 'Check the console (CTRL + Shift + i) for more details. Please try again.' - ) - } - - remote.getCurrentWindow().setProgressBar(-1) - - // Disconnect from AssetExec - aEx.disconnect() - break - } - } else if(m.context === 'validateEverything'){ - - let allGood = true - - // If these properties are not defined it's likely an error. - if(m.result.forgeData == null || m.result.versionData == null){ - loggerLaunchSuite.error('Error during validation:', m.result) - - loggerLaunchSuite.error('Error during launch', m.result.error) - showLaunchFailure('Error During Launch', 'Please check the console (CTRL + Shift + i) for more details.') - - allGood = false - } - - forgeData = m.result.forgeData - versionData = m.result.versionData - - if(login && allGood) { - const authUser = ConfigManager.getSelectedAccount() - loggerLaunchSuite.log(`Sending selected account (${authUser.displayName}) to ProcessBuilder.`) - let pb = new ProcessBuilder(serv, versionData, forgeData, authUser, remote.app.getVersion()) - setLaunchDetails('Launching game..') - - const onLoadComplete = () => { - toggleLaunchArea(false) - if(hasRPC){ - DiscordWrapper.updateDetails('Loading game..') - } - proc.stdout.on('data', gameStateChange) - proc.stdout.removeListener('data', tempListener) - proc.stderr.removeListener('data', gameErrorListener) - } - const start = Date.now() - - // Attach a temporary listener to the client output. - // Will wait for a certain bit of text meaning that - // the client application has started, and we can hide - // the progress bar stuff. - const tempListener = function(data){ - if(GAME_LAUNCH_REGEX.test(data.trim())){ - const diff = Date.now()-start - if(diff < MIN_LINGER) { - setTimeout(onLoadComplete, MIN_LINGER-diff) - } else { - onLoadComplete() - } - } - } - - // Listener for Discord RPC. - const gameStateChange = function(data){ - data = data.trim() - if(SERVER_JOINED_REGEX.test(data)){ - DiscordWrapper.updateDetails('Exploring the Realm!') - } else if(GAME_JOINED_REGEX.test(data)){ - DiscordWrapper.updateDetails('Sailing to Westeros!') - } - } - - const gameErrorListener = function(data){ - data = data.trim() - if(data.indexOf('Could not find or load main class net.minecraft.launchwrapper.Launch') > -1){ - loggerLaunchSuite.error('Game launch failed, LaunchWrapper was not downloaded properly.') - showLaunchFailure('Error During Launch', 'The main file, LaunchWrapper, failed to download properly. As a result, the game cannot launch.

To fix this issue, temporarily turn off your antivirus software and launch the game again.

If you have time, please submit an issue and let us know what antivirus software you use. We\'ll contact them and try to straighten things out.') - } - } - - try { - // Build Minecraft process. - proc = pb.build() - - // Bind listeners to stdout. - proc.stdout.on('data', tempListener) - proc.stderr.on('data', gameErrorListener) - - setLaunchDetails('Done. Enjoy the server!') - - // Init Discord Hook - const distro = DistroManager.getDistribution() - if(distro.discord != null && serv.discord != null){ - DiscordWrapper.initRPC(distro.discord, serv.discord) - hasRPC = true - proc.on('close', (code, signal) => { - loggerLaunchSuite.log('Shutting down Discord Rich Presence..') - DiscordWrapper.shutdownRPC() - hasRPC = false - proc = null - }) - } - - } catch(err) { - - loggerLaunchSuite.error('Error during launch', err) - showLaunchFailure('Error During Launch', 'Please check the console (CTRL + Shift + i) for more details.') - - } - } - - // Disconnect from AssetExec - aEx.disconnect() - - } - }) - - // Begin Validations - - // Validate Forge files. - setLaunchDetails('Loading server information..') - - refreshDistributionIndex(true, (data) => { - onDistroRefresh(data) - serv = data.getServer(ConfigManager.getSelectedServer()) - aEx.send({task: 'execute', function: 'validateEverything', argsArr: [ConfigManager.getSelectedServer(), DistroManager.isDevMode()]}) - }, (err) => { - loggerLaunchSuite.log('Error while fetching a fresh copy of the distribution index.', err) - refreshDistributionIndex(false, (data) => { - onDistroRefresh(data) - serv = data.getServer(ConfigManager.getSelectedServer()) - aEx.send({task: 'execute', function: 'validateEverything', argsArr: [ConfigManager.getSelectedServer(), DistroManager.isDevMode()]}) - }, (err) => { - loggerLaunchSuite.error('Unable to refresh distribution index.', err) - if(DistroManager.getDistribution() == null){ - showLaunchFailure('Fatal Error', 'Could not load a copy of the distribution index. See the console (CTRL + Shift + i) for more details.') - - // Disconnect from AssetExec - aEx.disconnect() - } else { - serv = data.getServer(ConfigManager.getSelectedServer()) - aEx.send({task: 'execute', function: 'validateEverything', argsArr: [ConfigManager.getSelectedServer(), DistroManager.isDevMode()]}) - } - }) - }) -} - -/** - * News Loading Functions - */ - -// DOM Cache -const newsContent = document.getElementById('newsContent') -const newsArticleTitle = document.getElementById('newsArticleTitle') -const newsArticleDate = document.getElementById('newsArticleDate') -const newsArticleAuthor = document.getElementById('newsArticleAuthor') -const newsArticleComments = document.getElementById('newsArticleComments') -const newsNavigationStatus = document.getElementById('newsNavigationStatus') -const newsArticleContentScrollable = document.getElementById('newsArticleContentScrollable') -const nELoadSpan = document.getElementById('nELoadSpan') - -// News slide caches. -let newsActive = false -let newsGlideCount = 0 - -/** - * Show the news UI via a slide animation. - * - * @param {boolean} up True to slide up, otherwise false. - */ -function slide_(up){ - const lCUpper = document.querySelector('#landingContainer > #upper') - const lCLLeft = document.querySelector('#landingContainer > #lower > #left') - const lCLCenter = document.querySelector('#landingContainer > #lower > #center') - const lCLRight = document.querySelector('#landingContainer > #lower > #right') - const newsBtn = document.querySelector('#landingContainer > #lower > #center #content') - const landingContainer = document.getElementById('landingContainer') - const newsContainer = document.querySelector('#landingContainer > #newsContainer') - - newsGlideCount++ - - if(up){ - lCUpper.style.top = '-200vh' - lCLLeft.style.top = '-200vh' - lCLCenter.style.top = '-200vh' - lCLRight.style.top = '-200vh' - newsBtn.style.top = '130vh' - newsContainer.style.top = '0px' - //date.toLocaleDateString('en-US', {month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: 'numeric'}) - //landingContainer.style.background = 'rgba(29, 29, 29, 0.55)' - landingContainer.style.background = 'rgba(0, 0, 0, 0.50)' - setTimeout(() => { - if(newsGlideCount === 1){ - lCLCenter.style.transition = 'none' - newsBtn.style.transition = 'none' - } - newsGlideCount-- - }, 2000) - } else { - setTimeout(() => { - newsGlideCount-- - }, 2000) - landingContainer.style.background = null - lCLCenter.style.transition = null - newsBtn.style.transition = null - newsContainer.style.top = '100%' - lCUpper.style.top = '0px' - lCLLeft.style.top = '0px' - lCLCenter.style.top = '0px' - lCLRight.style.top = '0px' - newsBtn.style.top = '10px' - } -} - -// Bind news button. -document.getElementById('newsButton').onclick = () => { - // Toggle tabbing. - if(newsActive){ - $('#landingContainer *').removeAttr('tabindex') - $('#newsContainer *').attr('tabindex', '-1') - } else { - $('#landingContainer *').attr('tabindex', '-1') - $('#newsContainer, #newsContainer *, #lower, #lower #center *').removeAttr('tabindex') - if(newsAlertShown){ - $('#newsButtonAlert').fadeOut(2000) - newsAlertShown = false - ConfigManager.setNewsCacheDismissed(true) - ConfigManager.save() - } - } - slide_(!newsActive) - newsActive = !newsActive -} - -// Array to store article meta. -let newsArr = null - -// News load animation listener. -let newsLoadingListener = null - -/** - * Set the news loading animation. - * - * @param {boolean} val True to set loading animation, otherwise false. - */ -function setNewsLoading(val){ - if(val){ - const nLStr = 'Checking for News' - let dotStr = '..' - nELoadSpan.innerHTML = nLStr + dotStr - newsLoadingListener = setInterval(() => { - if(dotStr.length >= 3){ - dotStr = '' - } else { - dotStr += '.' - } - nELoadSpan.innerHTML = nLStr + dotStr - }, 750) - } else { - if(newsLoadingListener != null){ - clearInterval(newsLoadingListener) - newsLoadingListener = null - } - } -} - -// Bind retry button. -newsErrorRetry.onclick = () => { - $('#newsErrorFailed').fadeOut(250, () => { - initNews() - $('#newsErrorLoading').fadeIn(250) - }) -} - -newsArticleContentScrollable.onscroll = (e) => { - if(e.target.scrollTop > Number.parseFloat($('.newsArticleSpacerTop').css('height'))){ - newsContent.setAttribute('scrolled', '') - } else { - newsContent.removeAttribute('scrolled') - } -} - -/** - * Reload the news without restarting. - * - * @returns {Promise.} A promise which resolves when the news - * content has finished loading and transitioning. - */ -function reloadNews(){ - return new Promise((resolve, reject) => { - $('#newsContent').fadeOut(250, () => { - $('#newsErrorLoading').fadeIn(250) - initNews().then(() => { - resolve() - }) - }) - }) -} - -let newsAlertShown = false - -/** - * Show the news alert indicating there is new news. - */ -function showNewsAlert(){ - newsAlertShown = true - $(newsButtonAlert).fadeIn(250) -} - -/** - * Initialize News UI. This will load the news and prepare - * the UI accordingly. - * - * @returns {Promise.} A promise which resolves when the news - * content has finished loading and transitioning. - */ -function initNews(){ - - return new Promise((resolve, reject) => { - setNewsLoading(true) - - let news = {} - loadNews().then(news => { - - newsArr = news.articles || null - - if(newsArr == null){ - // News Loading Failed - setNewsLoading(false) - - $('#newsErrorLoading').fadeOut(250, () => { - $('#newsErrorFailed').fadeIn(250, () => { - resolve() - }) - }) - } else if(newsArr.length === 0) { - // No News Articles - setNewsLoading(false) - - ConfigManager.setNewsCache({ - date: null, - content: null, - dismissed: false - }) - ConfigManager.save() - - $('#newsErrorLoading').fadeOut(250, () => { - $('#newsErrorNone').fadeIn(250, () => { - resolve() - }) - }) - } else { - // Success - setNewsLoading(false) - - const lN = newsArr[0] - const cached = ConfigManager.getNewsCache() - let newHash = crypto.createHash('sha1').update(lN.content).digest('hex') - let newDate = new Date(lN.date) - let isNew = false - - if(cached.date != null && cached.content != null){ - - if(new Date(cached.date) >= newDate){ - - // Compare Content - if(cached.content !== newHash){ - isNew = true - showNewsAlert() - } else { - if(!cached.dismissed){ - isNew = true - showNewsAlert() - } - } - - } else { - isNew = true - showNewsAlert() - } - - } else { - isNew = true - showNewsAlert() - } - - if(isNew){ - ConfigManager.setNewsCache({ - date: newDate.getTime(), - content: newHash, - dismissed: false - }) - ConfigManager.save() - } - - const switchHandler = (forward) => { - let cArt = parseInt(newsContent.getAttribute('article')) - let nxtArt = forward ? (cArt >= newsArr.length-1 ? 0 : cArt + 1) : (cArt <= 0 ? newsArr.length-1 : cArt - 1) - - displayArticle(newsArr[nxtArt], nxtArt+1) - } - - document.getElementById('newsNavigateRight').onclick = () => { switchHandler(true) } - document.getElementById('newsNavigateLeft').onclick = () => { switchHandler(false) } - - $('#newsErrorContainer').fadeOut(250, () => { - displayArticle(newsArr[0], 1) - $('#newsContent').fadeIn(250, () => { - resolve() - }) - }) - } - - }) - - }) -} - -/** - * Add keyboard controls to the news UI. Left and right arrows toggle - * between articles. If you are on the landing page, the up arrow will - * open the news UI. - */ -document.addEventListener('keydown', (e) => { - if(newsActive){ - if(e.key === 'ArrowRight' || e.key === 'ArrowLeft'){ - document.getElementById(e.key === 'ArrowRight' ? 'newsNavigateRight' : 'newsNavigateLeft').click() - } - // Interferes with scrolling an article using the down arrow. - // Not sure of a straight forward solution at this point. - // if(e.key === 'ArrowDown'){ - // document.getElementById('newsButton').click() - // } - } else { - if(getCurrentView() === VIEWS.landing){ - if(e.key === 'ArrowUp'){ - document.getElementById('newsButton').click() - } - } - } -}) - -/** - * Display a news article on the UI. - * - * @param {Object} articleObject The article meta object. - * @param {number} index The article index. - */ -function displayArticle(articleObject, index){ - newsArticleTitle.innerHTML = articleObject.title - newsArticleTitle.href = articleObject.link - newsArticleAuthor.innerHTML = 'by ' + articleObject.author - newsArticleDate.innerHTML = articleObject.date - newsArticleComments.innerHTML = articleObject.comments - newsArticleComments.href = articleObject.commentsLink - newsArticleContentScrollable.innerHTML = '
' + articleObject.content + '
' - Array.from(newsArticleContentScrollable.getElementsByClassName('bbCodeSpoilerButton')).forEach(v => { - v.onclick = () => { - const text = v.parentElement.getElementsByClassName('bbCodeSpoilerText')[0] - text.style.display = text.style.display === 'block' ? 'none' : 'block' - } - }) - newsNavigationStatus.innerHTML = index + ' of ' + newsArr.length - newsContent.setAttribute('article', index-1) -} - -/** - * Load news information from the RSS feed specified in the - * distribution index. - */ -function loadNews(){ - return new Promise((resolve, reject) => { - const distroData = DistroManager.getDistribution() - const newsFeed = distroData.getRSS() - const newsHost = new URL(newsFeed).origin + '/' - $.ajax({ - url: newsFeed, - success: (data) => { - const items = $(data).find('item') - const articles = [] - - for(let i=0; i { - resolve({ - articles: null - }) - }) - }) +/** + * Script for landing.ejs + */ +// Requirements +const cp = require('child_process') +const crypto = require('crypto') +const {URL} = require('url') + +// Internal Requirements +const DiscordWrapper = require('./assets/js/discordwrapper') +const Mojang = require('./assets/js/mojang') +const ProcessBuilder = require('./assets/js/processbuilder') +const ServerStatus = require('./assets/js/serverstatus') + +// Launch Elements +const launch_content = document.getElementById('launch_content') +const launch_details = document.getElementById('launch_details') +const launch_progress = document.getElementById('launch_progress') +const launch_progress_label = document.getElementById('launch_progress_label') +const launch_details_text = document.getElementById('launch_details_text') +const server_selection_button = document.getElementById('server_selection_button') +const user_text = document.getElementById('user_text') + +const loggerLanding = LoggerUtil('%c[Landing]', 'color: #000668; font-weight: bold') + +/* Launch Progress Wrapper Functions */ + +/** + * Show/hide the loading area. + * + * @param {boolean} loading True if the loading area should be shown, otherwise false. + */ +function toggleLaunchArea(loading){ + if(loading){ + launch_details.style.display = 'flex' + launch_content.style.display = 'none' + } else { + launch_details.style.display = 'none' + launch_content.style.display = 'inline-flex' + } +} + +/** + * Set the details text of the loading area. + * + * @param {string} details The new text for the loading details. + */ +function setLaunchDetails(details){ + launch_details_text.innerHTML = details +} + +/** + * Set the value of the loading progress bar and display that value. + * + * @param {number} value The progress value. + * @param {number} max The total size. + * @param {number|string} percent Optional. The percentage to display on the progress label. + */ +function setLaunchPercentage(value, max, percent = ((value/max)*100)){ + launch_progress.setAttribute('max', max) + launch_progress.setAttribute('value', value) + launch_progress_label.innerHTML = percent + '%' +} + +/** + * Set the value of the OS progress bar and display that on the UI. + * + * @param {number} value The progress value. + * @param {number} max The total download size. + * @param {number|string} percent Optional. The percentage to display on the progress label. + */ +function setDownloadPercentage(value, max, percent = ((value/max)*100)){ + remote.getCurrentWindow().setProgressBar(value/max) + setLaunchPercentage(value, max, percent) +} + +/** + * Enable or disable the launch button. + * + * @param {boolean} val True to enable, false to disable. + */ +function setLaunchEnabled(val){ + document.getElementById('launch_button').disabled = !val +} + +// Bind launch button +document.getElementById('launch_button').addEventListener('click', function(e){ + loggerLanding.log('Launching game..') + const mcVersion = DistroManager.getDistribution().getServer(ConfigManager.getSelectedServer()).getMinecraftVersion() + const jExe = ConfigManager.getJavaExecutable() + if(jExe == null){ + asyncSystemScan(mcVersion) + } else { + + setLaunchDetails(Lang.queryJS('landing.launch.pleaseWait')) + toggleLaunchArea(true) + setLaunchPercentage(0, 100) + + const jg = new JavaGuard(mcVersion) + jg._validateJavaBinary(jExe).then((v) => { + loggerLanding.log('Java version meta', v) + if(v.valid){ + dlAsync() + } else { + asyncSystemScan(mcVersion) + } + }) + } +}) + +// Bind settings button +document.getElementById('settingsMediaButton').onclick = (e) => { + prepareSettings() + switchView(getCurrentView(), VIEWS.settings) +} + +// Bind avatar overlay button. +document.getElementById('avatarOverlay').onclick = (e) => { + prepareSettings() + switchView(getCurrentView(), VIEWS.settings, 500, 500, () => { + settingsNavItemListener(document.getElementById('settingsNavAccount'), false) + }) +} + +// Bind selected account +function updateSelectedAccount(authUser){ + let username = 'No Account Selected' + if(authUser != null){ + if(authUser.displayName != null){ + username = authUser.displayName + } + if(authUser.uuid != null){ + document.getElementById('avatarContainer').style.backgroundImage = `url('https://crafatar.com/renders/body/${authUser.uuid}')` + } + } + user_text.innerHTML = username +} +updateSelectedAccount(ConfigManager.getSelectedAccount()) + +// Bind selected server +function updateSelectedServer(serv){ + if(getCurrentView() === VIEWS.settings){ + saveAllModConfigurations() + } + ConfigManager.setSelectedServer(serv != null ? serv.getID() : null) + ConfigManager.save() + server_selection_button.innerHTML = '\u2022 ' + (serv != null ? serv.getName() : 'No Server Selected') + if(getCurrentView() === VIEWS.settings){ + animateModsTabRefresh() + } + setLaunchEnabled(serv != null) +} +// Real text is set in uibinder.js on distributionIndexDone. +server_selection_button.innerHTML = '\u2022 Loading..' +server_selection_button.onclick = (e) => { + e.target.blur() + toggleServerSelection(true) +} + +// Update Mojang Status Color +const refreshMojangStatuses = async function(){ + loggerLanding.log('Refreshing Mojang Statuses..') + + let status = 'grey' + let tooltipEssentialHTML = '' + let tooltipNonEssentialHTML = '' + + try { + const statuses = await Mojang.status() + greenCount = 0 + greyCount = 0 + + for(let i=0; i + + ${service.name} + ` + } else { + tooltipNonEssentialHTML += `
+ + ${service.name} +
` + } + + if(service.status === 'yellow' && status !== 'red'){ + status = 'yellow' + } else if(service.status === 'red'){ + status = 'red' + } else { + if(service.status === 'grey'){ + ++greyCount + } + ++greenCount + } + + } + + if(greenCount === statuses.length){ + if(greyCount === statuses.length){ + status = 'grey' + } else { + status = 'green' + } + } + + } catch (err) { + loggerLanding.warn('Unable to refresh Mojang service status.') + loggerLanding.debug(err) + } + + document.getElementById('mojangStatusEssentialContainer').innerHTML = tooltipEssentialHTML + document.getElementById('mojangStatusNonEssentialContainer').innerHTML = tooltipNonEssentialHTML + document.getElementById('mojang_status_icon').style.color = Mojang.statusToHex(status) +} + +const refreshServerStatus = async function(fade = false){ + loggerLanding.log('Refreshing Server Status') + const serv = DistroManager.getDistribution().getServer(ConfigManager.getSelectedServer()) + + let pLabel = 'SERVER' + let pVal = 'OFFLINE' + + try { + const serverURL = new URL('my://' + serv.getAddress()) + const servStat = await ServerStatus.getStatus(serverURL.hostname, serverURL.port) + if(servStat.online){ + pLabel = 'PLAYERS' + pVal = servStat.onlinePlayers + '/' + servStat.maxPlayers + } + + } catch (err) { + loggerLanding.warn('Unable to refresh server status, assuming offline.') + loggerLanding.debug(err) + } + if(fade){ + $('#server_status_wrapper').fadeOut(250, () => { + document.getElementById('landingPlayerLabel').innerHTML = pLabel + document.getElementById('player_count').innerHTML = pVal + $('#server_status_wrapper').fadeIn(500) + }) + } else { + document.getElementById('landingPlayerLabel').innerHTML = pLabel + document.getElementById('player_count').innerHTML = pVal + } + +} + +refreshMojangStatuses() +// Server Status is refreshed in uibinder.js on distributionIndexDone. + +// Set refresh rate to once every 5 minutes. +let mojangStatusListener = setInterval(() => refreshMojangStatuses(true), 300000) +let serverStatusListener = setInterval(() => refreshServerStatus(true), 300000) + +/** + * Shows an error overlay, toggles off the launch area. + * + * @param {string} title The overlay title. + * @param {string} desc The overlay description. + */ +function showLaunchFailure(title, desc){ + setOverlayContent( + title, + desc, + 'Okay' + ) + setOverlayHandler(null) + toggleOverlay(true) + toggleLaunchArea(false) +} + +/* System (Java) Scan */ + +let sysAEx +let scanAt + +let extractListener + +/** + * Asynchronously scan the system for valid Java installations. + * + * @param {string} mcVersion The Minecraft version we are scanning for. + * @param {boolean} launchAfter Whether we should begin to launch after scanning. + */ +function asyncSystemScan(mcVersion, launchAfter = true){ + + setLaunchDetails('Please wait..') + toggleLaunchArea(true) + setLaunchPercentage(0, 100) + + const loggerSysAEx = LoggerUtil('%c[SysAEx]', 'color: #353232; font-weight: bold') + + const forkEnv = JSON.parse(JSON.stringify(process.env)) + forkEnv.CONFIG_DIRECT_PATH = ConfigManager.getLauncherDirectory() + + // Fork a process to run validations. + sysAEx = cp.fork(path.join(__dirname, 'assets', 'js', 'assetexec.js'), [ + 'JavaGuard', + mcVersion + ], { + env: forkEnv, + stdio: 'pipe' + }) + // Stdout + sysAEx.stdio[1].setEncoding('utf8') + sysAEx.stdio[1].on('data', (data) => { + loggerSysAEx.log(data) + }) + // Stderr + sysAEx.stdio[2].setEncoding('utf8') + sysAEx.stdio[2].on('data', (data) => { + loggerSysAEx.log(data) + }) + + sysAEx.on('message', (m) => { + + if(m.context === 'validateJava'){ + if(m.result == null){ + // If the result is null, no valid Java installation was found. + // Show this information to the user. + setOverlayContent( + 'No Compatible
Java Installation Found', + 'In order to join WesterosCraft, you need a 64-bit installation of Java 8. Would you like us to install a copy? By installing, you accept Oracle\'s license agreement.', + 'Install Java', + 'Install Manually' + ) + setOverlayHandler(() => { + setLaunchDetails('Preparing Java Download..') + sysAEx.send({task: 'changeContext', class: 'AssetGuard', args: [ConfigManager.getCommonDirectory(),ConfigManager.getJavaExecutable()]}) + sysAEx.send({task: 'execute', function: '_enqueueOpenJDK', argsArr: [ConfigManager.getDataDirectory()]}) + toggleOverlay(false) + }) + setDismissHandler(() => { + $('#overlayContent').fadeOut(250, () => { + //$('#overlayDismiss').toggle(false) + setOverlayContent( + 'Java is Required
to Launch', + 'A valid x64 installation of Java 8 is required to launch.

Please refer to our Java Management Guide for instructions on how to manually install Java.', + 'I Understand', + 'Go Back' + ) + setOverlayHandler(() => { + toggleLaunchArea(false) + toggleOverlay(false) + }) + setDismissHandler(() => { + toggleOverlay(false, true) + asyncSystemScan() + }) + $('#overlayContent').fadeIn(250) + }) + }) + toggleOverlay(true, true) + + } else { + // Java installation found, use this to launch the game. + ConfigManager.setJavaExecutable(m.result) + ConfigManager.save() + + // We need to make sure that the updated value is on the settings UI. + // Just incase the settings UI is already open. + settingsJavaExecVal.value = m.result + populateJavaExecDetails(settingsJavaExecVal.value) + + if(launchAfter){ + dlAsync() + } + sysAEx.disconnect() + } + } else if(m.context === '_enqueueOpenJDK'){ + + if(m.result === true){ + + // Oracle JRE enqueued successfully, begin download. + setLaunchDetails('Downloading Java..') + sysAEx.send({task: 'execute', function: 'processDlQueues', argsArr: [[{id:'java', limit:1}]]}) + + } else { + + // Oracle JRE enqueue failed. Probably due to a change in their website format. + // User will have to follow the guide to install Java. + setOverlayContent( + 'Unexpected Issue:
Java Download Failed', + 'Unfortunately we\'ve encountered an issue while attempting to install Java. You will need to manually install a copy. Please check out our Troubleshooting Guide for more details and instructions.', + 'I Understand' + ) + setOverlayHandler(() => { + toggleOverlay(false) + toggleLaunchArea(false) + }) + toggleOverlay(true) + sysAEx.disconnect() + + } + + } else if(m.context === 'progress'){ + + switch(m.data){ + case 'download': + // Downloading.. + setDownloadPercentage(m.value, m.total, m.percent) + break + } + + } else if(m.context === 'complete'){ + + switch(m.data){ + case 'download': { + // Show installing progress bar. + remote.getCurrentWindow().setProgressBar(2) + + // Wait for extration to complete. + const eLStr = 'Extracting' + let dotStr = '' + setLaunchDetails(eLStr) + extractListener = setInterval(() => { + if(dotStr.length >= 3){ + dotStr = '' + } else { + dotStr += '.' + } + setLaunchDetails(eLStr + dotStr) + }, 750) + break + } + case 'java': + // Download & extraction complete, remove the loading from the OS progress bar. + remote.getCurrentWindow().setProgressBar(-1) + + // Extraction completed successfully. + ConfigManager.setJavaExecutable(m.args[0]) + ConfigManager.save() + + if(extractListener != null){ + clearInterval(extractListener) + extractListener = null + } + + setLaunchDetails('Java Installed!') + + if(launchAfter){ + dlAsync() + } + + sysAEx.disconnect() + break + } + + } else if(m.context === 'error'){ + console.log(m.error) + } + }) + + // Begin system Java scan. + setLaunchDetails('Checking system info..') + sysAEx.send({task: 'execute', function: 'validateJava', argsArr: [ConfigManager.getDataDirectory()]}) + +} + +// Keep reference to Minecraft Process +let proc +// Is DiscordRPC enabled +let hasRPC = false +// Joined server regex +// Change this if your server uses something different. +const SERVER_JOINED_REGEX = /\[.+\]: \[CHAT\] [a-zA-Z0-9_]{1,16} joined the game/ +const GAME_JOINED_REGEX = /\[.+\]: Sound engine started/ +const GAME_LAUNCH_REGEX = /^\[.+\]: (?:MinecraftForge .+ Initialized|ModLauncher .+ starting: .+)$/ +const MIN_LINGER = 5000 + +let aEx +let serv +let versionData +let forgeData + +let progressListener + +function dlAsync(login = true){ + + // Login parameter is temporary for debug purposes. Allows testing the validation/downloads without + // launching the game. + + if(login) { + if(ConfigManager.getSelectedAccount() == null){ + loggerLanding.error('You must be logged into an account.') + return + } + } + + setLaunchDetails('Please wait..') + toggleLaunchArea(true) + setLaunchPercentage(0, 100) + + const loggerAEx = LoggerUtil('%c[AEx]', 'color: #353232; font-weight: bold') + const loggerLaunchSuite = LoggerUtil('%c[LaunchSuite]', 'color: #000668; font-weight: bold') + + const forkEnv = JSON.parse(JSON.stringify(process.env)) + forkEnv.CONFIG_DIRECT_PATH = ConfigManager.getLauncherDirectory() + + // Start AssetExec to run validations and downloads in a forked process. + aEx = cp.fork(path.join(__dirname, 'assets', 'js', 'assetexec.js'), [ + 'AssetGuard', + ConfigManager.getCommonDirectory(), + ConfigManager.getJavaExecutable() + ], { + env: forkEnv, + stdio: 'pipe' + }) + // Stdout + aEx.stdio[1].setEncoding('utf8') + aEx.stdio[1].on('data', (data) => { + loggerAEx.log(data) + }) + // Stderr + aEx.stdio[2].setEncoding('utf8') + aEx.stdio[2].on('data', (data) => { + loggerAEx.log(data) + }) + aEx.on('error', (err) => { + loggerLaunchSuite.error('Error during launch', err) + showLaunchFailure('Error During Launch', err.message || 'See console (CTRL + Shift + i) for more details.') + }) + aEx.on('close', (code, signal) => { + if(code !== 0){ + loggerLaunchSuite.error(`AssetExec exited with code ${code}, assuming error.`) + showLaunchFailure('Error During Launch', 'See console (CTRL + Shift + i) for more details.') + } + }) + + // Establish communications between the AssetExec and current process. + aEx.on('message', (m) => { + + if(m.context === 'validate'){ + switch(m.data){ + case 'distribution': + setLaunchPercentage(20, 100) + loggerLaunchSuite.log('Validated distibution index.') + setLaunchDetails('Loading version information..') + break + case 'version': + setLaunchPercentage(40, 100) + loggerLaunchSuite.log('Version data loaded.') + setLaunchDetails('Validating asset integrity..') + break + case 'assets': + setLaunchPercentage(60, 100) + loggerLaunchSuite.log('Asset Validation Complete') + setLaunchDetails('Validating library integrity..') + break + case 'libraries': + setLaunchPercentage(80, 100) + loggerLaunchSuite.log('Library validation complete.') + setLaunchDetails('Validating miscellaneous file integrity..') + break + case 'files': + setLaunchPercentage(100, 100) + loggerLaunchSuite.log('File validation complete.') + setLaunchDetails('Downloading files..') + break + } + } else if(m.context === 'progress'){ + switch(m.data){ + case 'assets': { + const perc = (m.value/m.total)*20 + setLaunchPercentage(40+perc, 100, parseInt(40+perc)) + break + } + case 'download': + setDownloadPercentage(m.value, m.total, m.percent) + break + case 'extract': { + // Show installing progress bar. + remote.getCurrentWindow().setProgressBar(2) + + // Download done, extracting. + const eLStr = 'Extracting libraries' + let dotStr = '' + setLaunchDetails(eLStr) + progressListener = setInterval(() => { + if(dotStr.length >= 3){ + dotStr = '' + } else { + dotStr += '.' + } + setLaunchDetails(eLStr + dotStr) + }, 750) + break + } + } + } else if(m.context === 'complete'){ + switch(m.data){ + case 'download': + // Download and extraction complete, remove the loading from the OS progress bar. + remote.getCurrentWindow().setProgressBar(-1) + if(progressListener != null){ + clearInterval(progressListener) + progressListener = null + } + + setLaunchDetails('Preparing to launch..') + break + } + } else if(m.context === 'error'){ + switch(m.data){ + case 'download': + loggerLaunchSuite.error('Error while downloading:', m.error) + + if(m.error.code === 'ENOENT'){ + showLaunchFailure( + 'Download Error', + 'Could not connect to the file server. Ensure that you are connected to the internet and try again.' + ) + } else { + showLaunchFailure( + 'Download Error', + 'Check the console (CTRL + Shift + i) for more details. Please try again.' + ) + } + + remote.getCurrentWindow().setProgressBar(-1) + + // Disconnect from AssetExec + aEx.disconnect() + break + } + } else if(m.context === 'validateEverything'){ + + let allGood = true + + // If these properties are not defined it's likely an error. + if(m.result.forgeData == null || m.result.versionData == null){ + loggerLaunchSuite.error('Error during validation:', m.result) + + loggerLaunchSuite.error('Error during launch', m.result.error) + showLaunchFailure('Error During Launch', 'Please check the console (CTRL + Shift + i) for more details.') + + allGood = false + } + + forgeData = m.result.forgeData + versionData = m.result.versionData + + if(login && allGood) { + const authUser = ConfigManager.getSelectedAccount() + loggerLaunchSuite.log(`Sending selected account (${authUser.displayName}) to ProcessBuilder.`) + let pb = new ProcessBuilder(serv, versionData, forgeData, authUser, remote.app.getVersion()) + setLaunchDetails('Launching game..') + + const onLoadComplete = () => { + toggleLaunchArea(false) + if(hasRPC){ + DiscordWrapper.updateDetails('Loading game..') + } + proc.stdout.on('data', gameStateChange) + proc.stdout.removeListener('data', tempListener) + proc.stderr.removeListener('data', gameErrorListener) + } + const start = Date.now() + + // Attach a temporary listener to the client output. + // Will wait for a certain bit of text meaning that + // the client application has started, and we can hide + // the progress bar stuff. + const tempListener = function(data){ + if(GAME_LAUNCH_REGEX.test(data.trim())){ + const diff = Date.now()-start + if(diff < MIN_LINGER) { + setTimeout(onLoadComplete, MIN_LINGER-diff) + } else { + onLoadComplete() + } + } + } + + // Listener for Discord RPC. + const gameStateChange = function(data){ + data = data.trim() + if(SERVER_JOINED_REGEX.test(data)){ + DiscordWrapper.updateDetails('Exploring the Realm!') + } else if(GAME_JOINED_REGEX.test(data)){ + DiscordWrapper.updateDetails('Sailing to Westeros!') + } + } + + const gameErrorListener = function(data){ + data = data.trim() + if(data.indexOf('Could not find or load main class net.minecraft.launchwrapper.Launch') > -1){ + loggerLaunchSuite.error('Game launch failed, LaunchWrapper was not downloaded properly.') + showLaunchFailure('Error During Launch', 'The main file, LaunchWrapper, failed to download properly. As a result, the game cannot launch.

To fix this issue, temporarily turn off your antivirus software and launch the game again.

If you have time, please submit an issue and let us know what antivirus software you use. We\'ll contact them and try to straighten things out.') + } + } + + try { + // Build Minecraft process. + proc = pb.build() + + // Bind listeners to stdout. + proc.stdout.on('data', tempListener) + proc.stderr.on('data', gameErrorListener) + + setLaunchDetails('Done. Enjoy the server!') + + // Init Discord Hook + const distro = DistroManager.getDistribution() + if(distro.discord != null && serv.discord != null){ + DiscordWrapper.initRPC(distro.discord, serv.discord) + hasRPC = true + proc.on('close', (code, signal) => { + loggerLaunchSuite.log('Shutting down Discord Rich Presence..') + DiscordWrapper.shutdownRPC() + hasRPC = false + proc = null + }) + } + + } catch(err) { + + loggerLaunchSuite.error('Error during launch', err) + showLaunchFailure('Error During Launch', 'Please check the console (CTRL + Shift + i) for more details.') + + } + } + + // Disconnect from AssetExec + aEx.disconnect() + + } + }) + + // Begin Validations + + // Validate Forge files. + setLaunchDetails('Loading server information..') + + refreshDistributionIndex(true, (data) => { + onDistroRefresh(data) + serv = data.getServer(ConfigManager.getSelectedServer()) + aEx.send({task: 'execute', function: 'validateEverything', argsArr: [ConfigManager.getSelectedServer(), DistroManager.isDevMode()]}) + }, (err) => { + loggerLaunchSuite.log('Error while fetching a fresh copy of the distribution index.', err) + refreshDistributionIndex(false, (data) => { + onDistroRefresh(data) + serv = data.getServer(ConfigManager.getSelectedServer()) + aEx.send({task: 'execute', function: 'validateEverything', argsArr: [ConfigManager.getSelectedServer(), DistroManager.isDevMode()]}) + }, (err) => { + loggerLaunchSuite.error('Unable to refresh distribution index.', err) + if(DistroManager.getDistribution() == null){ + showLaunchFailure('Fatal Error', 'Could not load a copy of the distribution index. See the console (CTRL + Shift + i) for more details.') + + // Disconnect from AssetExec + aEx.disconnect() + } else { + serv = data.getServer(ConfigManager.getSelectedServer()) + aEx.send({task: 'execute', function: 'validateEverything', argsArr: [ConfigManager.getSelectedServer(), DistroManager.isDevMode()]}) + } + }) + }) +} + +/** + * News Loading Functions + */ + +// DOM Cache +const newsContent = document.getElementById('newsContent') +const newsArticleTitle = document.getElementById('newsArticleTitle') +const newsArticleDate = document.getElementById('newsArticleDate') +const newsArticleAuthor = document.getElementById('newsArticleAuthor') +const newsArticleComments = document.getElementById('newsArticleComments') +const newsNavigationStatus = document.getElementById('newsNavigationStatus') +const newsArticleContentScrollable = document.getElementById('newsArticleContentScrollable') +const nELoadSpan = document.getElementById('nELoadSpan') + +// News slide caches. +let newsActive = false +let newsGlideCount = 0 + +/** + * Show the news UI via a slide animation. + * + * @param {boolean} up True to slide up, otherwise false. + */ +function slide_(up){ + const lCUpper = document.querySelector('#landingContainer > #upper') + const lCLLeft = document.querySelector('#landingContainer > #lower > #left') + const lCLCenter = document.querySelector('#landingContainer > #lower > #center') + const lCLRight = document.querySelector('#landingContainer > #lower > #right') + const newsBtn = document.querySelector('#landingContainer > #lower > #center #content') + const landingContainer = document.getElementById('landingContainer') + const newsContainer = document.querySelector('#landingContainer > #newsContainer') + + newsGlideCount++ + + if(up){ + lCUpper.style.top = '-200vh' + lCLLeft.style.top = '-200vh' + lCLCenter.style.top = '-200vh' + lCLRight.style.top = '-200vh' + newsBtn.style.top = '130vh' + newsContainer.style.top = '0px' + //date.toLocaleDateString('en-US', {month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: 'numeric'}) + //landingContainer.style.background = 'rgba(29, 29, 29, 0.55)' + landingContainer.style.background = 'rgba(0, 0, 0, 0.50)' + setTimeout(() => { + if(newsGlideCount === 1){ + lCLCenter.style.transition = 'none' + newsBtn.style.transition = 'none' + } + newsGlideCount-- + }, 2000) + } else { + setTimeout(() => { + newsGlideCount-- + }, 2000) + landingContainer.style.background = null + lCLCenter.style.transition = null + newsBtn.style.transition = null + newsContainer.style.top = '100%' + lCUpper.style.top = '0px' + lCLLeft.style.top = '0px' + lCLCenter.style.top = '0px' + lCLRight.style.top = '0px' + newsBtn.style.top = '10px' + } +} + +// Bind news button. +document.getElementById('newsButton').onclick = () => { + // Toggle tabbing. + if(newsActive){ + $('#landingContainer *').removeAttr('tabindex') + $('#newsContainer *').attr('tabindex', '-1') + } else { + $('#landingContainer *').attr('tabindex', '-1') + $('#newsContainer, #newsContainer *, #lower, #lower #center *').removeAttr('tabindex') + if(newsAlertShown){ + $('#newsButtonAlert').fadeOut(2000) + newsAlertShown = false + ConfigManager.setNewsCacheDismissed(true) + ConfigManager.save() + } + } + slide_(!newsActive) + newsActive = !newsActive +} + +// Array to store article meta. +let newsArr = null + +// News load animation listener. +let newsLoadingListener = null + +/** + * Set the news loading animation. + * + * @param {boolean} val True to set loading animation, otherwise false. + */ +function setNewsLoading(val){ + if(val){ + const nLStr = 'Recherche des Actus' + let dotStr = '..' + nELoadSpan.innerHTML = nLStr + dotStr + newsLoadingListener = setInterval(() => { + if(dotStr.length >= 3){ + dotStr = '' + } else { + dotStr += '.' + } + nELoadSpan.innerHTML = nLStr + dotStr + }, 750) + } else { + if(newsLoadingListener != null){ + clearInterval(newsLoadingListener) + newsLoadingListener = null + } + } +} + +// Bind retry button. +newsErrorRetry.onclick = () => { + $('#newsErrorFailed').fadeOut(250, () => { + initNews() + $('#newsErrorLoading').fadeIn(250) + }) +} + +newsArticleContentScrollable.onscroll = (e) => { + if(e.target.scrollTop > Number.parseFloat($('.newsArticleSpacerTop').css('height'))){ + newsContent.setAttribute('scrolled', '') + } else { + newsContent.removeAttribute('scrolled') + } +} + +/** + * Reload the news without restarting. + * + * @returns {Promise.} A promise which resolves when the news + * content has finished loading and transitioning. + */ +function reloadNews(){ + return new Promise((resolve, reject) => { + $('#newsContent').fadeOut(250, () => { + $('#newsErrorLoading').fadeIn(250) + initNews().then(() => { + resolve() + }) + }) + }) +} + +let newsAlertShown = false + +/** + * Show the news alert indicating there is new news. + */ +function showNewsAlert(){ + newsAlertShown = true + $(newsButtonAlert).fadeIn(250) +} + +/** + * Initialize News UI. This will load the news and prepare + * the UI accordingly. + * + * @returns {Promise.} A promise which resolves when the news + * content has finished loading and transitioning. + */ +function initNews(){ + + return new Promise((resolve, reject) => { + setNewsLoading(true) + + let news = {} + loadNews().then(news => { + + newsArr = news.articles || null + + if(newsArr == null){ + // News Loading Failed + setNewsLoading(false) + + $('#newsErrorLoading').fadeOut(250, () => { + $('#newsErrorFailed').fadeIn(250, () => { + resolve() + }) + }) + } else if(newsArr.length === 0) { + // No News Articles + setNewsLoading(false) + + ConfigManager.setNewsCache({ + date: null, + content: null, + dismissed: false + }) + ConfigManager.save() + + $('#newsErrorLoading').fadeOut(250, () => { + $('#newsErrorNone').fadeIn(250, () => { + resolve() + }) + }) + } else { + // Success + setNewsLoading(false) + + const lN = newsArr[0] + const cached = ConfigManager.getNewsCache() + let newHash = crypto.createHash('sha1').update(lN.content).digest('hex') + let newDate = new Date(lN.date) + let isNew = false + + if(cached.date != null && cached.content != null){ + + if(new Date(cached.date) >= newDate){ + + // Compare Content + if(cached.content !== newHash){ + isNew = true + showNewsAlert() + } else { + if(!cached.dismissed){ + isNew = true + showNewsAlert() + } + } + + } else { + isNew = true + showNewsAlert() + } + + } else { + isNew = true + showNewsAlert() + } + + if(isNew){ + ConfigManager.setNewsCache({ + date: newDate.getTime(), + content: newHash, + dismissed: false + }) + ConfigManager.save() + } + + const switchHandler = (forward) => { + let cArt = parseInt(newsContent.getAttribute('article')) + let nxtArt = forward ? (cArt >= newsArr.length-1 ? 0 : cArt + 1) : (cArt <= 0 ? newsArr.length-1 : cArt - 1) + + displayArticle(newsArr[nxtArt], nxtArt+1) + } + + document.getElementById('newsNavigateRight').onclick = () => { switchHandler(true) } + document.getElementById('newsNavigateLeft').onclick = () => { switchHandler(false) } + + $('#newsErrorContainer').fadeOut(250, () => { + displayArticle(newsArr[0], 1) + $('#newsContent').fadeIn(250, () => { + resolve() + }) + }) + } + + }) + + }) +} + +/** + * Add keyboard controls to the news UI. Left and right arrows toggle + * between articles. If you are on the landing page, the up arrow will + * open the news UI. + */ +document.addEventListener('keydown', (e) => { + if(newsActive){ + if(e.key === 'ArrowRight' || e.key === 'ArrowLeft'){ + document.getElementById(e.key === 'ArrowRight' ? 'newsNavigateRight' : 'newsNavigateLeft').click() + } + // Interferes with scrolling an article using the down arrow. + // Not sure of a straight forward solution at this point. + // if(e.key === 'ArrowDown'){ + // document.getElementById('newsButton').click() + // } + } else { + if(getCurrentView() === VIEWS.landing){ + if(e.key === 'ArrowUp'){ + document.getElementById('newsButton').click() + } + } + } +}) + +/** + * Display a news article on the UI. + * + * @param {Object} articleObject The article meta object. + * @param {number} index The article index. + */ +function displayArticle(articleObject, index){ + newsArticleTitle.innerHTML = articleObject.title + newsArticleTitle.href = articleObject.link + newsArticleAuthor.innerHTML = 'by ' + articleObject.author + newsArticleDate.innerHTML = articleObject.date + newsArticleComments.innerHTML = articleObject.comments + newsArticleComments.href = articleObject.commentsLink + newsArticleContentScrollable.innerHTML = '
' + articleObject.content + '
' + Array.from(newsArticleContentScrollable.getElementsByClassName('bbCodeSpoilerButton')).forEach(v => { + v.onclick = () => { + const text = v.parentElement.getElementsByClassName('bbCodeSpoilerText')[0] + text.style.display = text.style.display === 'block' ? 'none' : 'block' + } + }) + newsNavigationStatus.innerHTML = index + ' of ' + newsArr.length + newsContent.setAttribute('article', index-1) +} + +/** + * Load news information from the RSS feed specified in the + * distribution index. + */ +function loadNews(){ + return new Promise((resolve, reject) => { + const distroData = DistroManager.getDistribution() + const newsFeed = distroData.getRSS() + const newsHost = new URL(newsFeed).origin + '/' + $.ajax({ + url: newsFeed, + success: (data) => { + const items = $(data).find('item') + const articles = [] + + for(let i=0; i { + resolve({ + articles: null + }) + }) + }) } \ No newline at end of file diff --git a/app/assets/js/scripts/login.js b/app/assets/js/scripts/login.js index 34078bd1..06f7425b 100644 --- a/app/assets/js/scripts/login.js +++ b/app/assets/js/scripts/login.js @@ -1,300 +1,300 @@ -/** - * Script for login.ejs - */ -// Validation Regexes. -const validUsername = /^[a-zA-Z0-9_]{1,16}$/ -const basicEmail = /^\S+@\S+\.\S+$/ -//const validEmail = /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i - -// Login Elements -const loginCancelContainer = document.getElementById('loginCancelContainer') -const loginCancelButton = document.getElementById('loginCancelButton') -const loginEmailError = document.getElementById('loginEmailError') -const loginUsername = document.getElementById('loginUsername') -const loginPasswordError = document.getElementById('loginPasswordError') -const loginPassword = document.getElementById('loginPassword') -const checkmarkContainer = document.getElementById('checkmarkContainer') -const loginRememberOption = document.getElementById('loginRememberOption') -const loginButton = document.getElementById('loginButton') -const loginForm = document.getElementById('loginForm') - -// Control variables. -let lu = false, lp = false - -const loggerLogin = LoggerUtil('%c[Login]', 'color: #000668; font-weight: bold') - - -/** - * Show a login error. - * - * @param {HTMLElement} element The element on which to display the error. - * @param {string} value The error text. - */ -function showError(element, value){ - element.innerHTML = value - element.style.opacity = 1 -} - -/** - * Shake a login error to add emphasis. - * - * @param {HTMLElement} element The element to shake. - */ -function shakeError(element){ - if(element.style.opacity == 1){ - element.classList.remove('shake') - void element.offsetWidth - element.classList.add('shake') - } -} - -/** - * Validate that an email field is neither empty nor invalid. - * - * @param {string} value The email value. - */ -function validateEmail(value){ - if(value){ - if(!basicEmail.test(value) && !validUsername.test(value)){ - showError(loginEmailError, Lang.queryJS('login.error.invalidValue')) - loginDisabled(true) - lu = false - } else { - loginEmailError.style.opacity = 0 - lu = true - if(lp){ - loginDisabled(false) - } - } - } else { - lu = false - showError(loginEmailError, Lang.queryJS('login.error.requiredValue')) - loginDisabled(true) - } -} - -/** - * Validate that the password field is not empty. - * - * @param {string} value The password value. - */ -function validatePassword(value){ - if(value){ - loginPasswordError.style.opacity = 0 - lp = true - if(lu){ - loginDisabled(false) - } - } else { - lp = false - showError(loginPasswordError, Lang.queryJS('login.error.invalidValue')) - loginDisabled(true) - } -} - -// Emphasize errors with shake when focus is lost. -loginUsername.addEventListener('focusout', (e) => { - validateEmail(e.target.value) - shakeError(loginEmailError) -}) -loginPassword.addEventListener('focusout', (e) => { - validatePassword(e.target.value) - shakeError(loginPasswordError) -}) - -// Validate input for each field. -loginUsername.addEventListener('input', (e) => { - validateEmail(e.target.value) -}) -loginPassword.addEventListener('input', (e) => { - validatePassword(e.target.value) -}) - -/** - * Enable or disable the login button. - * - * @param {boolean} v True to enable, false to disable. - */ -function loginDisabled(v){ - if(loginButton.disabled !== v){ - loginButton.disabled = v - } -} - -/** - * Enable or disable loading elements. - * - * @param {boolean} v True to enable, false to disable. - */ -function loginLoading(v){ - if(v){ - loginButton.setAttribute('loading', v) - loginButton.innerHTML = loginButton.innerHTML.replace(Lang.queryJS('login.login'), Lang.queryJS('login.loggingIn')) - } else { - loginButton.removeAttribute('loading') - loginButton.innerHTML = loginButton.innerHTML.replace(Lang.queryJS('login.loggingIn'), Lang.queryJS('login.login')) - } -} - -/** - * Enable or disable login form. - * - * @param {boolean} v True to enable, false to disable. - */ -function formDisabled(v){ - loginDisabled(v) - loginCancelButton.disabled = v - loginUsername.disabled = v - loginPassword.disabled = v - if(v){ - checkmarkContainer.setAttribute('disabled', v) - } else { - checkmarkContainer.removeAttribute('disabled') - } - loginRememberOption.disabled = v -} - -/** - * Parses an error and returns a user-friendly title and description - * for our error overlay. - * - * @param {Error | {cause: string, error: string, errorMessage: string}} err A Node.js - * error or Mojang error response. - */ -function resolveError(err){ - // Mojang Response => err.cause | err.error | err.errorMessage - // Node error => err.code | err.message - if(err.cause != null && err.cause === 'UserMigratedException') { - return { - title: Lang.queryJS('login.error.userMigrated.title'), - desc: Lang.queryJS('login.error.userMigrated.desc') - } - } else { - if(err.error != null){ - if(err.error === 'ForbiddenOperationException'){ - if(err.errorMessage != null){ - if(err.errorMessage === 'Invalid credentials. Invalid username or password.'){ - return { - title: Lang.queryJS('login.error.invalidCredentials.title'), - desc: Lang.queryJS('login.error.invalidCredentials.desc') - } - } else if(err.errorMessage === 'Invalid credentials.'){ - return { - title: Lang.queryJS('login.error.rateLimit.title'), - desc: Lang.queryJS('login.error.rateLimit.desc') - } - } - } - } - } else { - // Request errors (from Node). - if(err.code != null){ - if(err.code === 'ENOENT'){ - // No Internet. - return { - title: Lang.queryJS('login.error.noInternet.title'), - desc: Lang.queryJS('login.error.noInternet.desc') - } - } else if(err.code === 'ENOTFOUND'){ - // Could not reach server. - return { - title: Lang.queryJS('login.error.authDown.title'), - desc: Lang.queryJS('login.error.authDown.desc') - } - } - } - } - } - if(err.message != null){ - if(err.message === 'NotPaidAccount'){ - return { - title: Lang.queryJS('login.error.notPaid.title'), - desc: Lang.queryJS('login.error.notPaid.desc') - } - } else { - // Unknown error with request. - return { - title: Lang.queryJS('login.error.unknown.title'), - desc: err.message - } - } - } else { - // Unknown Mojang error. - return { - title: err.error, - desc: err.errorMessage - } - } -} - -let loginViewOnSuccess = VIEWS.landing -let loginViewOnCancel = VIEWS.settings -let loginViewCancelHandler - -function loginCancelEnabled(val){ - if(val){ - $(loginCancelContainer).show() - } else { - $(loginCancelContainer).hide() - } -} - -loginCancelButton.onclick = (e) => { - switchView(getCurrentView(), loginViewOnCancel, 500, 500, () => { - loginUsername.value = '' - loginPassword.value = '' - loginCancelEnabled(false) - if(loginViewCancelHandler != null){ - loginViewCancelHandler() - loginViewCancelHandler = null - } - }) -} - -// Disable default form behavior. -loginForm.onsubmit = () => { return false } - -// Bind login button behavior. -loginButton.addEventListener('click', () => { - // Disable form. - formDisabled(true) - - // Show loading stuff. - loginLoading(true) - - AuthManager.addAccount(loginUsername.value, loginPassword.value).then((value) => { - updateSelectedAccount(value) - loginButton.innerHTML = loginButton.innerHTML.replace(Lang.queryJS('login.loggingIn'), Lang.queryJS('login.success')) - $('.circle-loader').toggleClass('load-complete') - $('.checkmark').toggle() - setTimeout(() => { - switchView(VIEWS.login, loginViewOnSuccess, 500, 500, () => { - // Temporary workaround - if(loginViewOnSuccess === VIEWS.settings){ - prepareSettings() - } - loginViewOnSuccess = VIEWS.landing // Reset this for good measure. - loginCancelEnabled(false) // Reset this for good measure. - loginViewCancelHandler = null // Reset this for good measure. - loginUsername.value = '' - loginPassword.value = '' - $('.circle-loader').toggleClass('load-complete') - $('.checkmark').toggle() - loginLoading(false) - loginButton.innerHTML = loginButton.innerHTML.replace(Lang.queryJS('login.success'), Lang.queryJS('login.login')) - formDisabled(false) - }) - }, 1000) - }).catch((err) => { - loginLoading(false) - const errF = resolveError(err) - setOverlayContent(errF.title, errF.desc, Lang.queryJS('login.tryAgain')) - setOverlayHandler(() => { - formDisabled(false) - toggleOverlay(false) - }) - toggleOverlay(true) - loggerLogin.log('Error while logging in.', err) - }) - +/** + * Script for login.ejs + */ +// Validation Regexes. +const validUsername = /^[a-zA-Z0-9_]{1,16}$/ +const basicEmail = /^\S+@\S+\.\S+$/ +//const validEmail = /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i + +// Login Elements +const loginCancelContainer = document.getElementById('loginCancelContainer') +const loginCancelButton = document.getElementById('loginCancelButton') +const loginEmailError = document.getElementById('loginEmailError') +const loginUsername = document.getElementById('loginUsername') +const loginPasswordError = document.getElementById('loginPasswordError') +const loginPassword = document.getElementById('loginPassword') +const checkmarkContainer = document.getElementById('checkmarkContainer') +const loginRememberOption = document.getElementById('loginRememberOption') +const loginButton = document.getElementById('loginButton') +const loginForm = document.getElementById('loginForm') + +// Control variables. +let lu = false, lp = false + +const loggerLogin = LoggerUtil('%c[Login]', 'color: #000668; font-weight: bold') + + +/** + * Show a login error. + * + * @param {HTMLElement} element The element on which to display the error. + * @param {string} value The error text. + */ +function showError(element, value){ + element.innerHTML = value + element.style.opacity = 1 +} + +/** + * Shake a login error to add emphasis. + * + * @param {HTMLElement} element The element to shake. + */ +function shakeError(element){ + if(element.style.opacity == 1){ + element.classList.remove('shake') + void element.offsetWidth + element.classList.add('shake') + } +} + +/** + * Validate that an email field is neither empty nor invalid. + * + * @param {string} value The email value. + */ +function validateEmail(value){ + if(value){ + if(!basicEmail.test(value) && !validUsername.test(value)){ + showError(loginEmailError, Lang.queryJS('login.error.invalidValue')) + loginDisabled(true) + lu = false + } else { + loginEmailError.style.opacity = 0 + lu = true + if(lp){ + loginDisabled(false) + } + } + } else { + lu = false + showError(loginEmailError, Lang.queryJS('login.error.requiredValue')) + loginDisabled(true) + } +} + +/** + * Validate that the password field is not empty. + * + * @param {string} value The password value. + */ +function validatePassword(value){ + if(value){ + loginPasswordError.style.opacity = 0 + lp = true + if(lu){ + loginDisabled(false) + } + } else { + lp = false + showError(loginPasswordError, Lang.queryJS('login.error.invalidValue')) + loginDisabled(true) + } +} + +// Emphasize errors with shake when focus is lost. +loginUsername.addEventListener('focusout', (e) => { + validateEmail(e.target.value) + shakeError(loginEmailError) +}) +loginPassword.addEventListener('focusout', (e) => { + validatePassword(e.target.value) + shakeError(loginPasswordError) +}) + +// Validate input for each field. +loginUsername.addEventListener('input', (e) => { + validateEmail(e.target.value) +}) +loginPassword.addEventListener('input', (e) => { + validatePassword(e.target.value) +}) + +/** + * Enable or disable the login button. + * + * @param {boolean} v True to enable, false to disable. + */ +function loginDisabled(v){ + if(loginButton.disabled !== v){ + loginButton.disabled = v + } +} + +/** + * Enable or disable loading elements. + * + * @param {boolean} v True to enable, false to disable. + */ +function loginLoading(v){ + if(v){ + loginButton.setAttribute('loading', v) + loginButton.innerHTML = loginButton.innerHTML.replace(Lang.queryJS('login.login'), Lang.queryJS('login.loggingIn')) + } else { + loginButton.removeAttribute('loading') + loginButton.innerHTML = loginButton.innerHTML.replace(Lang.queryJS('login.loggingIn'), Lang.queryJS('login.login')) + } +} + +/** + * Enable or disable login form. + * + * @param {boolean} v True to enable, false to disable. + */ +function formDisabled(v){ + loginDisabled(v) + loginCancelButton.disabled = v + loginUsername.disabled = v + loginPassword.disabled = v + if(v){ + checkmarkContainer.setAttribute('disabled', v) + } else { + checkmarkContainer.removeAttribute('disabled') + } + loginRememberOption.disabled = v +} + +/** + * Parses an error and returns a user-friendly title and description + * for our error overlay. + * + * @param {Error | {cause: string, error: string, errorMessage: string}} err A Node.js + * error or Mojang error response. + */ +function resolveError(err){ + // Mojang Response => err.cause | err.error | err.errorMessage + // Node error => err.code | err.message + if(err.cause != null && err.cause === 'UserMigratedException') { + return { + title: Lang.queryJS('login.error.userMigrated.title'), + desc: Lang.queryJS('login.error.userMigrated.desc') + } + } else { + if(err.error != null){ + if(err.error === 'ForbiddenOperationException'){ + if(err.errorMessage != null){ + if(err.errorMessage === 'Invalid credentials. Invalid username or password.'){ + return { + title: Lang.queryJS('login.error.invalidCredentials.title'), + desc: Lang.queryJS('login.error.invalidCredentials.desc') + } + } else if(err.errorMessage === 'Invalid credentials.'){ + return { + title: Lang.queryJS('login.error.rateLimit.title'), + desc: Lang.queryJS('login.error.rateLimit.desc') + } + } + } + } + } else { + // Request errors (from Node). + if(err.code != null){ + if(err.code === 'ENOENT'){ + // No Internet. + return { + title: Lang.queryJS('login.error.noInternet.title'), + desc: Lang.queryJS('login.error.noInternet.desc') + } + } else if(err.code === 'ENOTFOUND'){ + // Could not reach server. + return { + title: Lang.queryJS('login.error.authDown.title'), + desc: Lang.queryJS('login.error.authDown.desc') + } + } + } + } + } + if(err.message != null){ + if(err.message === 'NotPaidAccount'){ + return { + title: Lang.queryJS('login.error.notPaid.title'), + desc: Lang.queryJS('login.error.notPaid.desc') + } + } else { + // Unknown error with request. + return { + title: Lang.queryJS('login.error.unknown.title'), + desc: err.message + } + } + } else { + // Unknown Mojang error. + return { + title: err.error, + desc: err.errorMessage + } + } +} + +let loginViewOnSuccess = VIEWS.landing +let loginViewOnCancel = VIEWS.settings +let loginViewCancelHandler + +function loginCancelEnabled(val){ + if(val){ + $(loginCancelContainer).show() + } else { + $(loginCancelContainer).hide() + } +} + +loginCancelButton.onclick = (e) => { + switchView(getCurrentView(), loginViewOnCancel, 500, 500, () => { + loginUsername.value = '' + loginPassword.value = '' + loginCancelEnabled(false) + if(loginViewCancelHandler != null){ + loginViewCancelHandler() + loginViewCancelHandler = null + } + }) +} + +// Disable default form behavior. +loginForm.onsubmit = () => { return false } + +// Bind login button behavior. +loginButton.addEventListener('click', () => { + // Disable form. + formDisabled(true) + + // Show loading stuff. + loginLoading(true) + + AuthManager.addAccount(loginUsername.value, loginPassword.value).then((value) => { + updateSelectedAccount(value) + loginButton.innerHTML = loginButton.innerHTML.replace(Lang.queryJS('login.loggingIn'), Lang.queryJS('login.success')) + $('.circle-loader').toggleClass('load-complete') + $('.checkmark').toggle() + setTimeout(() => { + switchView(VIEWS.login, loginViewOnSuccess, 500, 500, () => { + // Temporary workaround + if(loginViewOnSuccess === VIEWS.settings){ + prepareSettings() + } + loginViewOnSuccess = VIEWS.landing // Reset this for good measure. + loginCancelEnabled(false) // Reset this for good measure. + loginViewCancelHandler = null // Reset this for good measure. + loginUsername.value = '' + loginPassword.value = '' + $('.circle-loader').toggleClass('load-complete') + $('.checkmark').toggle() + loginLoading(false) + loginButton.innerHTML = loginButton.innerHTML.replace(Lang.queryJS('login.success'), Lang.queryJS('login.login')) + formDisabled(false) + }) + }, 1000) + }).catch((err) => { + loginLoading(false) + const errF = resolveError(err) + setOverlayContent(errF.title, errF.desc, Lang.queryJS('login.tryAgain')) + setOverlayHandler(() => { + formDisabled(false) + toggleOverlay(false) + }) + toggleOverlay(true) + loggerLogin.log('Error while logging in.', err) + }) + }) \ No newline at end of file diff --git a/app/assets/js/scripts/overlay.js b/app/assets/js/scripts/overlay.js index ed704047..df452d76 100644 --- a/app/assets/js/scripts/overlay.js +++ b/app/assets/js/scripts/overlay.js @@ -1,318 +1,318 @@ -/** - * Script for overlay.ejs - */ - -/* Overlay Wrapper Functions */ - -/** - * Check to see if the overlay is visible. - * - * @returns {boolean} Whether or not the overlay is visible. - */ -function isOverlayVisible(){ - return document.getElementById('main').hasAttribute('overlay') -} - -let overlayHandlerContent - -/** - * Overlay keydown handler for a non-dismissable overlay. - * - * @param {KeyboardEvent} e The keydown event. - */ -function overlayKeyHandler (e){ - if(e.key === 'Enter' || e.key === 'Escape'){ - document.getElementById(overlayHandlerContent).getElementsByClassName('overlayKeybindEnter')[0].click() - } -} -/** - * Overlay keydown handler for a dismissable overlay. - * - * @param {KeyboardEvent} e The keydown event. - */ -function overlayKeyDismissableHandler (e){ - if(e.key === 'Enter'){ - document.getElementById(overlayHandlerContent).getElementsByClassName('overlayKeybindEnter')[0].click() - } else if(e.key === 'Escape'){ - document.getElementById(overlayHandlerContent).getElementsByClassName('overlayKeybindEsc')[0].click() - } -} - -/** - * Bind overlay keydown listeners for escape and exit. - * - * @param {boolean} state Whether or not to add new event listeners. - * @param {string} content The overlay content which will be shown. - * @param {boolean} dismissable Whether or not the overlay is dismissable - */ -function bindOverlayKeys(state, content, dismissable){ - overlayHandlerContent = content - document.removeEventListener('keydown', overlayKeyHandler) - document.removeEventListener('keydown', overlayKeyDismissableHandler) - if(state){ - if(dismissable){ - document.addEventListener('keydown', overlayKeyDismissableHandler) - } else { - document.addEventListener('keydown', overlayKeyHandler) - } - } -} - -/** - * Toggle the visibility of the overlay. - * - * @param {boolean} toggleState True to display, false to hide. - * @param {boolean} dismissable Optional. True to show the dismiss option, otherwise false. - * @param {string} content Optional. The content div to be shown. - */ -function toggleOverlay(toggleState, dismissable = false, content = 'overlayContent'){ - if(toggleState == null){ - toggleState = !document.getElementById('main').hasAttribute('overlay') - } - if(typeof dismissable === 'string'){ - content = dismissable - dismissable = false - } - bindOverlayKeys(toggleState, content, dismissable) - if(toggleState){ - document.getElementById('main').setAttribute('overlay', true) - // Make things untabbable. - $('#main *').attr('tabindex', '-1') - $('#' + content).parent().children().hide() - $('#' + content).show() - if(dismissable){ - $('#overlayDismiss').show() - } else { - $('#overlayDismiss').hide() - } - $('#overlayContainer').fadeIn({ - duration: 250, - start: () => { - if(getCurrentView() === VIEWS.settings){ - document.getElementById('settingsContainer').style.backgroundColor = 'transparent' - } - } - }) - } else { - document.getElementById('main').removeAttribute('overlay') - // Make things tabbable. - $('#main *').removeAttr('tabindex') - $('#overlayContainer').fadeOut({ - duration: 250, - start: () => { - if(getCurrentView() === VIEWS.settings){ - document.getElementById('settingsContainer').style.backgroundColor = 'rgba(0, 0, 0, 0.50)' - } - }, - complete: () => { - $('#' + content).parent().children().hide() - $('#' + content).show() - if(dismissable){ - $('#overlayDismiss').show() - } else { - $('#overlayDismiss').hide() - } - } - }) - } -} - -function toggleServerSelection(toggleState){ - prepareServerSelectionList() - toggleOverlay(toggleState, true, 'serverSelectContent') -} - -/** - * Set the content of the overlay. - * - * @param {string} title Overlay title text. - * @param {string} description Overlay description text. - * @param {string} acknowledge Acknowledge button text. - * @param {string} dismiss Dismiss button text. - */ -function setOverlayContent(title, description, acknowledge, dismiss = 'Dismiss'){ - document.getElementById('overlayTitle').innerHTML = title - document.getElementById('overlayDesc').innerHTML = description - document.getElementById('overlayAcknowledge').innerHTML = acknowledge - document.getElementById('overlayDismiss').innerHTML = dismiss -} - -/** - * Set the onclick handler of the overlay acknowledge button. - * If the handler is null, a default handler will be added. - * - * @param {function} handler - */ -function setOverlayHandler(handler){ - if(handler == null){ - document.getElementById('overlayAcknowledge').onclick = () => { - toggleOverlay(false) - } - } else { - document.getElementById('overlayAcknowledge').onclick = handler - } -} - -/** - * Set the onclick handler of the overlay dismiss button. - * If the handler is null, a default handler will be added. - * - * @param {function} handler - */ -function setDismissHandler(handler){ - if(handler == null){ - document.getElementById('overlayDismiss').onclick = () => { - toggleOverlay(false) - } - } else { - document.getElementById('overlayDismiss').onclick = handler - } -} - -/* Server Select View */ - -document.getElementById('serverSelectConfirm').addEventListener('click', () => { - const listings = document.getElementsByClassName('serverListing') - for(let i=0; i 0){ - const serv = DistroManager.getDistribution().getServer(listings[i].getAttribute('servid')) - updateSelectedServer(serv) - toggleOverlay(false) - } -}) - -document.getElementById('accountSelectConfirm').addEventListener('click', () => { - const listings = document.getElementsByClassName('accountListing') - for(let i=0; i 0){ - const authAcc = ConfigManager.setSelectedAccount(listings[0].getAttribute('uuid')) - ConfigManager.save() - updateSelectedAccount(authAcc) - toggleOverlay(false) - validateSelectedAccount() - } -}) - -// Bind server select cancel button. -document.getElementById('serverSelectCancel').addEventListener('click', () => { - toggleOverlay(false) -}) - -document.getElementById('accountSelectCancel').addEventListener('click', () => { - $('#accountSelectContent').fadeOut(250, () => { - $('#overlayContent').fadeIn(250) - }) -}) - -function setServerListingHandlers(){ - const listings = Array.from(document.getElementsByClassName('serverListing')) - listings.map((val) => { - val.onclick = e => { - if(val.hasAttribute('selected')){ - return - } - const cListings = document.getElementsByClassName('serverListing') - for(let i=0; i { - val.onclick = e => { - if(val.hasAttribute('selected')){ - return - } - const cListings = document.getElementsByClassName('accountListing') - for(let i=0; i - -
- ${serv.getName()} - ${serv.getDescription()} -
-
${serv.getMinecraftVersion()}
-
${serv.getVersion()}
- ${serv.isMainServer() ? `
- - - - - - - - Main Server -
` : ''} -
-
- ` - } - document.getElementById('serverSelectListScrollable').innerHTML = htmlString - -} - -function populateAccountListings(){ - const accountsObj = ConfigManager.getAuthAccounts() - const accounts = Array.from(Object.keys(accountsObj), v=>accountsObj[v]) - let htmlString = '' - for(let i=0; i - -
${accounts[i].displayName}
- ` - } - document.getElementById('accountSelectListScrollable').innerHTML = htmlString - -} - -function prepareServerSelectionList(){ - populateServerListings() - setServerListingHandlers() -} - -function prepareAccountSelectionList(){ - populateAccountListings() - setAccountListingHandlers() +/** + * Script for overlay.ejs + */ + +/* Overlay Wrapper Functions */ + +/** + * Check to see if the overlay is visible. + * + * @returns {boolean} Whether or not the overlay is visible. + */ +function isOverlayVisible(){ + return document.getElementById('main').hasAttribute('overlay') +} + +let overlayHandlerContent + +/** + * Overlay keydown handler for a non-dismissable overlay. + * + * @param {KeyboardEvent} e The keydown event. + */ +function overlayKeyHandler (e){ + if(e.key === 'Enter' || e.key === 'Escape'){ + document.getElementById(overlayHandlerContent).getElementsByClassName('overlayKeybindEnter')[0].click() + } +} +/** + * Overlay keydown handler for a dismissable overlay. + * + * @param {KeyboardEvent} e The keydown event. + */ +function overlayKeyDismissableHandler (e){ + if(e.key === 'Enter'){ + document.getElementById(overlayHandlerContent).getElementsByClassName('overlayKeybindEnter')[0].click() + } else if(e.key === 'Escape'){ + document.getElementById(overlayHandlerContent).getElementsByClassName('overlayKeybindEsc')[0].click() + } +} + +/** + * Bind overlay keydown listeners for escape and exit. + * + * @param {boolean} state Whether or not to add new event listeners. + * @param {string} content The overlay content which will be shown. + * @param {boolean} dismissable Whether or not the overlay is dismissable + */ +function bindOverlayKeys(state, content, dismissable){ + overlayHandlerContent = content + document.removeEventListener('keydown', overlayKeyHandler) + document.removeEventListener('keydown', overlayKeyDismissableHandler) + if(state){ + if(dismissable){ + document.addEventListener('keydown', overlayKeyDismissableHandler) + } else { + document.addEventListener('keydown', overlayKeyHandler) + } + } +} + +/** + * Toggle the visibility of the overlay. + * + * @param {boolean} toggleState True to display, false to hide. + * @param {boolean} dismissable Optional. True to show the dismiss option, otherwise false. + * @param {string} content Optional. The content div to be shown. + */ +function toggleOverlay(toggleState, dismissable = false, content = 'overlayContent'){ + if(toggleState == null){ + toggleState = !document.getElementById('main').hasAttribute('overlay') + } + if(typeof dismissable === 'string'){ + content = dismissable + dismissable = false + } + bindOverlayKeys(toggleState, content, dismissable) + if(toggleState){ + document.getElementById('main').setAttribute('overlay', true) + // Make things untabbable. + $('#main *').attr('tabindex', '-1') + $('#' + content).parent().children().hide() + $('#' + content).show() + if(dismissable){ + $('#overlayDismiss').show() + } else { + $('#overlayDismiss').hide() + } + $('#overlayContainer').fadeIn({ + duration: 250, + start: () => { + if(getCurrentView() === VIEWS.settings){ + document.getElementById('settingsContainer').style.backgroundColor = 'transparent' + } + } + }) + } else { + document.getElementById('main').removeAttribute('overlay') + // Make things tabbable. + $('#main *').removeAttr('tabindex') + $('#overlayContainer').fadeOut({ + duration: 250, + start: () => { + if(getCurrentView() === VIEWS.settings){ + document.getElementById('settingsContainer').style.backgroundColor = 'rgba(0, 0, 0, 0.50)' + } + }, + complete: () => { + $('#' + content).parent().children().hide() + $('#' + content).show() + if(dismissable){ + $('#overlayDismiss').show() + } else { + $('#overlayDismiss').hide() + } + } + }) + } +} + +function toggleServerSelection(toggleState){ + prepareServerSelectionList() + toggleOverlay(toggleState, true, 'serverSelectContent') +} + +/** + * Set the content of the overlay. + * + * @param {string} title Overlay title text. + * @param {string} description Overlay description text. + * @param {string} acknowledge Acknowledge button text. + * @param {string} dismiss Dismiss button text. + */ +function setOverlayContent(title, description, acknowledge, dismiss = 'Dismiss'){ + document.getElementById('overlayTitle').innerHTML = title + document.getElementById('overlayDesc').innerHTML = description + document.getElementById('overlayAcknowledge').innerHTML = acknowledge + document.getElementById('overlayDismiss').innerHTML = dismiss +} + +/** + * Set the onclick handler of the overlay acknowledge button. + * If the handler is null, a default handler will be added. + * + * @param {function} handler + */ +function setOverlayHandler(handler){ + if(handler == null){ + document.getElementById('overlayAcknowledge').onclick = () => { + toggleOverlay(false) + } + } else { + document.getElementById('overlayAcknowledge').onclick = handler + } +} + +/** + * Set the onclick handler of the overlay dismiss button. + * If the handler is null, a default handler will be added. + * + * @param {function} handler + */ +function setDismissHandler(handler){ + if(handler == null){ + document.getElementById('overlayDismiss').onclick = () => { + toggleOverlay(false) + } + } else { + document.getElementById('overlayDismiss').onclick = handler + } +} + +/* Server Select View */ + +document.getElementById('serverSelectConfirm').addEventListener('click', () => { + const listings = document.getElementsByClassName('serverListing') + for(let i=0; i 0){ + const serv = DistroManager.getDistribution().getServer(listings[i].getAttribute('servid')) + updateSelectedServer(serv) + toggleOverlay(false) + } +}) + +document.getElementById('accountSelectConfirm').addEventListener('click', () => { + const listings = document.getElementsByClassName('accountListing') + for(let i=0; i 0){ + const authAcc = ConfigManager.setSelectedAccount(listings[0].getAttribute('uuid')) + ConfigManager.save() + updateSelectedAccount(authAcc) + toggleOverlay(false) + validateSelectedAccount() + } +}) + +// Bind server select cancel button. +document.getElementById('serverSelectCancel').addEventListener('click', () => { + toggleOverlay(false) +}) + +document.getElementById('accountSelectCancel').addEventListener('click', () => { + $('#accountSelectContent').fadeOut(250, () => { + $('#overlayContent').fadeIn(250) + }) +}) + +function setServerListingHandlers(){ + const listings = Array.from(document.getElementsByClassName('serverListing')) + listings.map((val) => { + val.onclick = e => { + if(val.hasAttribute('selected')){ + return + } + const cListings = document.getElementsByClassName('serverListing') + for(let i=0; i { + val.onclick = e => { + if(val.hasAttribute('selected')){ + return + } + const cListings = document.getElementsByClassName('accountListing') + for(let i=0; i + +
+ ${serv.getName()} + ${serv.getDescription()} +
+
${serv.getMinecraftVersion()}
+
${serv.getVersion()}
+ ${serv.isMainServer() ? `
+ + + + + + + + Main Server +
` : ''} +
+
+ ` + } + document.getElementById('serverSelectListScrollable').innerHTML = htmlString + +} + +function populateAccountListings(){ + const accountsObj = ConfigManager.getAuthAccounts() + const accounts = Array.from(Object.keys(accountsObj), v=>accountsObj[v]) + let htmlString = '' + for(let i=0; i + +
${accounts[i].displayName}
+ ` + } + document.getElementById('accountSelectListScrollable').innerHTML = htmlString + +} + +function prepareServerSelectionList(){ + populateServerListings() + setServerListingHandlers() +} + +function prepareAccountSelectionList(){ + populateAccountListings() + setAccountListingHandlers() } \ No newline at end of file diff --git a/app/assets/js/scripts/settings.js b/app/assets/js/scripts/settings.js index 23ef79c0..912b5c1d 100644 --- a/app/assets/js/scripts/settings.js +++ b/app/assets/js/scripts/settings.js @@ -1,1349 +1,1349 @@ -// Requirements -const os = require('os') -const semver = require('semver') - -const { JavaGuard } = require('./assets/js/assetguard') -const DropinModUtil = require('./assets/js/dropinmodutil') - -const settingsState = { - invalid: new Set() -} - -function bindSettingsSelect(){ - for(let ele of document.getElementsByClassName('settingsSelectContainer')) { - const selectedDiv = ele.getElementsByClassName('settingsSelectSelected')[0] - - selectedDiv.onclick = (e) => { - e.stopPropagation() - closeSettingsSelect(e.target) - e.target.nextElementSibling.toggleAttribute('hidden') - e.target.classList.toggle('select-arrow-active') - } - } -} - -function closeSettingsSelect(el){ - for(let ele of document.getElementsByClassName('settingsSelectContainer')) { - const selectedDiv = ele.getElementsByClassName('settingsSelectSelected')[0] - const optionsDiv = ele.getElementsByClassName('settingsSelectOptions')[0] - - if(!(selectedDiv === el)) { - selectedDiv.classList.remove('select-arrow-active') - optionsDiv.setAttribute('hidden', '') - } - } -} - -/* If the user clicks anywhere outside the select box, -then close all select boxes: */ -document.addEventListener('click', closeSettingsSelect) - -bindSettingsSelect() - - -function bindFileSelectors(){ - for(let ele of document.getElementsByClassName('settingsFileSelButton')){ - - ele.onclick = async e => { - const isJavaExecSel = ele.id === 'settingsJavaExecSel' - const directoryDialog = ele.hasAttribute('dialogDirectory') && ele.getAttribute('dialogDirectory') == 'true' - const properties = directoryDialog ? ['openDirectory', 'createDirectory'] : ['openFile'] - - const options = { - properties - } - - if(ele.hasAttribute('dialogTitle')) { - options.title = ele.getAttribute('dialogTitle') - } - - if(isJavaExecSel && process.platform === 'win32') { - options.filters = [ - { name: 'Executables', extensions: ['exe'] }, - { name: 'All Files', extensions: ['*'] } - ] - } - - const res = await remote.dialog.showOpenDialog(remote.getCurrentWindow(), options) - if(!res.canceled) { - ele.previousElementSibling.value = res.filePaths[0] - if(isJavaExecSel) { - populateJavaExecDetails(ele.previousElementSibling.value) - } - } - } - } -} - -bindFileSelectors() - - -/** - * General Settings Functions - */ - -/** - * Bind value validators to the settings UI elements. These will - * validate against the criteria defined in the ConfigManager (if - * and). If the value is invalid, the UI will reflect this and saving - * will be disabled until the value is corrected. This is an automated - * process. More complex UI may need to be bound separately. - */ -function initSettingsValidators(){ - const sEls = document.getElementById('settingsContainer').querySelectorAll('[cValue]') - Array.from(sEls).map((v, index, arr) => { - const vFn = ConfigManager['validate' + v.getAttribute('cValue')] - if(typeof vFn === 'function'){ - if(v.tagName === 'INPUT'){ - if(v.type === 'number' || v.type === 'text'){ - v.addEventListener('keyup', (e) => { - const v = e.target - if(!vFn(v.value)){ - settingsState.invalid.add(v.id) - v.setAttribute('error', '') - settingsSaveDisabled(true) - } else { - if(v.hasAttribute('error')){ - v.removeAttribute('error') - settingsState.invalid.delete(v.id) - if(settingsState.invalid.size === 0){ - settingsSaveDisabled(false) - } - } - } - }) - } - } - } - - }) -} - -/** - * Load configuration values onto the UI. This is an automated process. - */ -function initSettingsValues(){ - const sEls = document.getElementById('settingsContainer').querySelectorAll('[cValue]') - Array.from(sEls).map((v, index, arr) => { - const cVal = v.getAttribute('cValue') - const gFn = ConfigManager['get' + cVal] - if(typeof gFn === 'function'){ - if(v.tagName === 'INPUT'){ - if(v.type === 'number' || v.type === 'text'){ - // Special Conditions - if(cVal === 'JavaExecutable'){ - populateJavaExecDetails(v.value) - v.value = gFn() - } else if (cVal === 'DataDirectory'){ - v.value = gFn() - } else if(cVal === 'JVMOptions'){ - v.value = gFn().join(' ') - } else { - v.value = gFn() - } - } else if(v.type === 'checkbox'){ - v.checked = gFn() - } - } else if(v.tagName === 'DIV'){ - if(v.classList.contains('rangeSlider')){ - // Special Conditions - if(cVal === 'MinRAM' || cVal === 'MaxRAM'){ - let val = gFn() - if(val.endsWith('M')){ - val = Number(val.substring(0, val.length-1))/1000 - } else { - val = Number.parseFloat(val) - } - - v.setAttribute('value', val) - } else { - v.setAttribute('value', Number.parseFloat(gFn())) - } - } - } - } - - }) -} - -/** - * Save the settings values. - */ -function saveSettingsValues(){ - const sEls = document.getElementById('settingsContainer').querySelectorAll('[cValue]') - Array.from(sEls).map((v, index, arr) => { - const cVal = v.getAttribute('cValue') - const sFn = ConfigManager['set' + cVal] - if(typeof sFn === 'function'){ - if(v.tagName === 'INPUT'){ - if(v.type === 'number' || v.type === 'text'){ - // Special Conditions - if(cVal === 'JVMOptions'){ - sFn(v.value.split(' ')) - } else { - sFn(v.value) - } - } else if(v.type === 'checkbox'){ - sFn(v.checked) - // Special Conditions - if(cVal === 'AllowPrerelease'){ - changeAllowPrerelease(v.checked) - } - } - } else if(v.tagName === 'DIV'){ - if(v.classList.contains('rangeSlider')){ - // Special Conditions - if(cVal === 'MinRAM' || cVal === 'MaxRAM'){ - let val = Number(v.getAttribute('value')) - if(val%1 > 0){ - val = val*1000 + 'M' - } else { - val = val + 'G' - } - - sFn(val) - } else { - sFn(v.getAttribute('value')) - } - } - } - } - }) -} - -let selectedSettingsTab = 'settingsTabAccount' - -/** - * Modify the settings container UI when the scroll threshold reaches - * a certain poin. - * - * @param {UIEvent} e The scroll event. - */ -function settingsTabScrollListener(e){ - if(e.target.scrollTop > Number.parseFloat(getComputedStyle(e.target.firstElementChild).marginTop)){ - document.getElementById('settingsContainer').setAttribute('scrolled', '') - } else { - document.getElementById('settingsContainer').removeAttribute('scrolled') - } -} - -/** - * Bind functionality for the settings navigation items. - */ -function setupSettingsTabs(){ - Array.from(document.getElementsByClassName('settingsNavItem')).map((val) => { - if(val.hasAttribute('rSc')){ - val.onclick = () => { - settingsNavItemListener(val) - } - } - }) -} - -/** - * Settings nav item onclick lisener. Function is exposed so that - * other UI elements can quickly toggle to a certain tab from other views. - * - * @param {Element} ele The nav item which has been clicked. - * @param {boolean} fade Optional. True to fade transition. - */ -function settingsNavItemListener(ele, fade = true){ - if(ele.hasAttribute('selected')){ - return - } - const navItems = document.getElementsByClassName('settingsNavItem') - for(let i=0; i { - $(`#${selectedSettingsTab}`).fadeIn({ - duration: 250, - start: () => { - settingsTabScrollListener({ - target: document.getElementById(selectedSettingsTab) - }) - } - }) - }) - } else { - $(`#${prevTab}`).hide(0, () => { - $(`#${selectedSettingsTab}`).show({ - duration: 0, - start: () => { - settingsTabScrollListener({ - target: document.getElementById(selectedSettingsTab) - }) - } - }) - }) - } -} - -const settingsNavDone = document.getElementById('settingsNavDone') - -/** - * Set if the settings save (done) button is disabled. - * - * @param {boolean} v True to disable, false to enable. - */ -function settingsSaveDisabled(v){ - settingsNavDone.disabled = v -} - -/* Closes the settings view and saves all data. */ -settingsNavDone.onclick = () => { - saveSettingsValues() - saveModConfiguration() - ConfigManager.save() - saveDropinModConfiguration() - saveShaderpackSettings() - switchView(getCurrentView(), VIEWS.landing) -} - -/** - * Account Management Tab - */ - -// Bind the add account button. -document.getElementById('settingsAddAccount').onclick = (e) => { - switchView(getCurrentView(), VIEWS.login, 500, 500, () => { - loginViewOnCancel = VIEWS.settings - loginViewOnSuccess = VIEWS.settings - loginCancelEnabled(true) - }) -} - -/** - * Bind functionality for the account selection buttons. If another account - * is selected, the UI of the previously selected account will be updated. - */ -function bindAuthAccountSelect(){ - Array.from(document.getElementsByClassName('settingsAuthAccountSelect')).map((val) => { - val.onclick = (e) => { - if(val.hasAttribute('selected')){ - return - } - const selectBtns = document.getElementsByClassName('settingsAuthAccountSelect') - for(let i=0; i { - val.onclick = (e) => { - let isLastAccount = false - if(Object.keys(ConfigManager.getAuthAccounts()).length === 1){ - isLastAccount = true - setOverlayContent( - 'Warning
This is Your Last Account', - 'In order to use the launcher you must be logged into at least one account. You will need to login again after.

Are you sure you want to log out?', - 'I\'m Sure', - 'Cancel' - ) - setOverlayHandler(() => { - processLogOut(val, isLastAccount) - toggleOverlay(false) - switchView(getCurrentView(), VIEWS.login) - }) - setDismissHandler(() => { - toggleOverlay(false) - }) - toggleOverlay(true, true) - } else { - processLogOut(val, isLastAccount) - } - - } - }) -} - -/** - * Process a log out. - * - * @param {Element} val The log out button element. - * @param {boolean} isLastAccount If this logout is on the last added account. - */ -function processLogOut(val, isLastAccount){ - const parent = val.closest('.settingsAuthAccount') - const uuid = parent.getAttribute('uuid') - const prevSelAcc = ConfigManager.getSelectedAccount() - AuthManager.removeAccount(uuid).then(() => { - if(!isLastAccount && uuid === prevSelAcc.uuid){ - const selAcc = ConfigManager.getSelectedAccount() - refreshAuthAccountSelected(selAcc.uuid) - updateSelectedAccount(selAcc) - validateSelectedAccount() - } - }) - $(parent).fadeOut(250, () => { - parent.remove() - }) -} - -/** - * Refreshes the status of the selected account on the auth account - * elements. - * - * @param {string} uuid The UUID of the new selected account. - */ -function refreshAuthAccountSelected(uuid){ - Array.from(document.getElementsByClassName('settingsAuthAccount')).map((val) => { - const selBtn = val.getElementsByClassName('settingsAuthAccountSelect')[0] - if(uuid === val.getAttribute('uuid')){ - selBtn.setAttribute('selected', '') - selBtn.innerHTML = 'Selected Account ✔' - } else { - if(selBtn.hasAttribute('selected')){ - selBtn.removeAttribute('selected') - } - selBtn.innerHTML = 'Select Account' - } - }) -} - -const settingsCurrentAccounts = document.getElementById('settingsCurrentAccounts') - -/** - * Add auth account elements for each one stored in the authentication database. - */ -function populateAuthAccounts(){ - const authAccounts = ConfigManager.getAuthAccounts() - const authKeys = Object.keys(authAccounts) - if(authKeys.length === 0){ - return - } - const selectedUUID = ConfigManager.getSelectedAccount().uuid - - let authAccountStr = '' - - authKeys.map((val) => { - const acc = authAccounts[val] - authAccountStr += `
-
- ${acc.displayName} -
-
-
-
-
Username
-
${acc.displayName}
-
-
-
UUID
-
${acc.uuid}
-
-
-
- -
- -
-
-
-
` - }) - - settingsCurrentAccounts.innerHTML = authAccountStr -} - -/** - * Prepare the accounts tab for display. - */ -function prepareAccountsTab() { - populateAuthAccounts() - bindAuthAccountSelect() - bindAuthAccountLogOut() -} - -/** - * Minecraft Tab - */ - -/** - * Disable decimals, negative signs, and scientific notation. - */ -document.getElementById('settingsGameWidth').addEventListener('keydown', (e) => { - if(/^[-.eE]$/.test(e.key)){ - e.preventDefault() - } -}) -document.getElementById('settingsGameHeight').addEventListener('keydown', (e) => { - if(/^[-.eE]$/.test(e.key)){ - e.preventDefault() - } -}) - -/** - * Mods Tab - */ - -const settingsModsContainer = document.getElementById('settingsModsContainer') - -/** - * Resolve and update the mods on the UI. - */ -function resolveModsForUI(){ - const serv = ConfigManager.getSelectedServer() - - const distro = DistroManager.getDistribution() - const servConf = ConfigManager.getModConfiguration(serv) - - const modStr = parseModulesForUI(distro.getServer(serv).getModules(), false, servConf.mods) - - document.getElementById('settingsReqModsContent').innerHTML = modStr.reqMods - document.getElementById('settingsOptModsContent').innerHTML = modStr.optMods -} - -/** - * Recursively build the mod UI elements. - * - * @param {Object[]} mdls An array of modules to parse. - * @param {boolean} submodules Whether or not we are parsing submodules. - * @param {Object} servConf The server configuration object for this module level. - */ -function parseModulesForUI(mdls, submodules, servConf){ - - let reqMods = '' - let optMods = '' - - for(const mdl of mdls){ - - if(mdl.getType() === DistroManager.Types.ForgeMod || mdl.getType() === DistroManager.Types.LiteMod || mdl.getType() === DistroManager.Types.LiteLoader){ - - if(mdl.getRequired().isRequired()){ - - reqMods += `
-
-
-
-
- ${mdl.getName()} - v${mdl.getVersion()} -
-
- -
- ${mdl.hasSubModules() ? `
- ${Object.values(parseModulesForUI(mdl.getSubModules(), true, servConf[mdl.getVersionlessID()])).join('')} -
` : ''} -
` - - } else { - - const conf = servConf[mdl.getVersionlessID()] - const val = typeof conf === 'object' ? conf.value : conf - - optMods += `
-
-
-
-
- ${mdl.getName()} - v${mdl.getVersion()} -
-
- -
- ${mdl.hasSubModules() ? `
- ${Object.values(parseModulesForUI(mdl.getSubModules(), true, conf.mods)).join('')} -
` : ''} -
` - - } - } - } - - return { - reqMods, - optMods - } - -} - -/** - * Bind functionality to mod config toggle switches. Switching the value - * will also switch the status color on the left of the mod UI. - */ -function bindModsToggleSwitch(){ - const sEls = settingsModsContainer.querySelectorAll('[formod]') - Array.from(sEls).map((v, index, arr) => { - v.onchange = () => { - if(v.checked) { - document.getElementById(v.getAttribute('formod')).setAttribute('enabled', '') - } else { - document.getElementById(v.getAttribute('formod')).removeAttribute('enabled') - } - } - }) -} - - -/** - * Save the mod configuration based on the UI values. - */ -function saveModConfiguration(){ - const serv = ConfigManager.getSelectedServer() - const modConf = ConfigManager.getModConfiguration(serv) - modConf.mods = _saveModConfiguration(modConf.mods) - ConfigManager.setModConfiguration(serv, modConf) -} - -/** - * Recursively save mod config with submods. - * - * @param {Object} modConf Mod config object to save. - */ -function _saveModConfiguration(modConf){ - for(let m of Object.entries(modConf)){ - const tSwitch = settingsModsContainer.querySelectorAll(`[formod='${m[0]}']`) - if(!tSwitch[0].hasAttribute('dropin')){ - if(typeof m[1] === 'boolean'){ - modConf[m[0]] = tSwitch[0].checked - } else { - if(m[1] != null){ - if(tSwitch.length > 0){ - modConf[m[0]].value = tSwitch[0].checked - } - modConf[m[0]].mods = _saveModConfiguration(modConf[m[0]].mods) - } - } - } - } - return modConf -} - -// Drop-in mod elements. - -let CACHE_SETTINGS_MODS_DIR -let CACHE_DROPIN_MODS - -/** - * Resolve any located drop-in mods for this server and - * populate the results onto the UI. - */ -function resolveDropinModsForUI(){ - const serv = DistroManager.getDistribution().getServer(ConfigManager.getSelectedServer()) - CACHE_SETTINGS_MODS_DIR = path.join(ConfigManager.getInstanceDirectory(), serv.getID(), 'mods') - CACHE_DROPIN_MODS = DropinModUtil.scanForDropinMods(CACHE_SETTINGS_MODS_DIR, serv.getMinecraftVersion()) - - let dropinMods = '' - - for(dropin of CACHE_DROPIN_MODS){ - dropinMods += `
-
-
-
-
- ${dropin.name} -
- -
-
-
- -
-
` - } - - document.getElementById('settingsDropinModsContent').innerHTML = dropinMods -} - -/** - * Bind the remove button for each loaded drop-in mod. - */ -function bindDropinModsRemoveButton(){ - const sEls = settingsModsContainer.querySelectorAll('[remmod]') - Array.from(sEls).map((v, index, arr) => { - v.onclick = () => { - const fullName = v.getAttribute('remmod') - const res = DropinModUtil.deleteDropinMod(CACHE_SETTINGS_MODS_DIR, fullName) - if(res){ - document.getElementById(fullName).remove() - } else { - setOverlayContent( - `Failed to Delete
Drop-in Mod ${fullName}`, - 'Make sure the file is not in use and try again.', - 'Okay' - ) - setOverlayHandler(null) - toggleOverlay(true) - } - } - }) -} - -/** - * Bind functionality to the file system button for the selected - * server configuration. - */ -function bindDropinModFileSystemButton(){ - const fsBtn = document.getElementById('settingsDropinFileSystemButton') - fsBtn.onclick = () => { - DropinModUtil.validateDir(CACHE_SETTINGS_MODS_DIR) - shell.openPath(CACHE_SETTINGS_MODS_DIR) - } - fsBtn.ondragenter = e => { - e.dataTransfer.dropEffect = 'move' - fsBtn.setAttribute('drag', '') - e.preventDefault() - } - fsBtn.ondragover = e => { - e.preventDefault() - } - fsBtn.ondragleave = e => { - fsBtn.removeAttribute('drag') - } - - fsBtn.ondrop = e => { - fsBtn.removeAttribute('drag') - e.preventDefault() - - DropinModUtil.addDropinMods(e.dataTransfer.files, CACHE_SETTINGS_MODS_DIR) - reloadDropinMods() - } -} - -/** - * Save drop-in mod states. Enabling and disabling is just a matter - * of adding/removing the .disabled extension. - */ -function saveDropinModConfiguration(){ - for(dropin of CACHE_DROPIN_MODS){ - const dropinUI = document.getElementById(dropin.fullName) - if(dropinUI != null){ - const dropinUIEnabled = dropinUI.hasAttribute('enabled') - if(DropinModUtil.isDropinModEnabled(dropin.fullName) != dropinUIEnabled){ - DropinModUtil.toggleDropinMod(CACHE_SETTINGS_MODS_DIR, dropin.fullName, dropinUIEnabled).catch(err => { - if(!isOverlayVisible()){ - setOverlayContent( - 'Failed to Toggle
One or More Drop-in Mods', - err.message, - 'Okay' - ) - setOverlayHandler(null) - toggleOverlay(true) - } - }) - } - } - } -} - -// Refresh the drop-in mods when F5 is pressed. -// Only active on the mods tab. -document.addEventListener('keydown', (e) => { - if(getCurrentView() === VIEWS.settings && selectedSettingsTab === 'settingsTabMods'){ - if(e.key === 'F5'){ - reloadDropinMods() - saveShaderpackSettings() - resolveShaderpacksForUI() - } - } -}) - -function reloadDropinMods(){ - resolveDropinModsForUI() - bindDropinModsRemoveButton() - bindDropinModFileSystemButton() - bindModsToggleSwitch() -} - -// Shaderpack - -let CACHE_SETTINGS_INSTANCE_DIR -let CACHE_SHADERPACKS -let CACHE_SELECTED_SHADERPACK - -/** - * Load shaderpack information. - */ -function resolveShaderpacksForUI(){ - const serv = DistroManager.getDistribution().getServer(ConfigManager.getSelectedServer()) - CACHE_SETTINGS_INSTANCE_DIR = path.join(ConfigManager.getInstanceDirectory(), serv.getID()) - CACHE_SHADERPACKS = DropinModUtil.scanForShaderpacks(CACHE_SETTINGS_INSTANCE_DIR) - CACHE_SELECTED_SHADERPACK = DropinModUtil.getEnabledShaderpack(CACHE_SETTINGS_INSTANCE_DIR) - - setShadersOptions(CACHE_SHADERPACKS, CACHE_SELECTED_SHADERPACK) -} - -function setShadersOptions(arr, selected){ - const cont = document.getElementById('settingsShadersOptions') - cont.innerHTML = '' - for(let opt of arr) { - const d = document.createElement('DIV') - d.innerHTML = opt.name - d.setAttribute('value', opt.fullName) - if(opt.fullName === selected) { - d.setAttribute('selected', '') - document.getElementById('settingsShadersSelected').innerHTML = opt.name - } - d.addEventListener('click', function(e) { - this.parentNode.previousElementSibling.innerHTML = this.innerHTML - for(let sib of this.parentNode.children){ - sib.removeAttribute('selected') - } - this.setAttribute('selected', '') - closeSettingsSelect() - }) - cont.appendChild(d) - } -} - -function saveShaderpackSettings(){ - let sel = 'OFF' - for(let opt of document.getElementById('settingsShadersOptions').childNodes){ - if(opt.hasAttribute('selected')){ - sel = opt.getAttribute('value') - } - } - DropinModUtil.setEnabledShaderpack(CACHE_SETTINGS_INSTANCE_DIR, sel) -} - -function bindShaderpackButton() { - const spBtn = document.getElementById('settingsShaderpackButton') - spBtn.onclick = () => { - const p = path.join(CACHE_SETTINGS_INSTANCE_DIR, 'shaderpacks') - DropinModUtil.validateDir(p) - shell.openPath(p) - } - spBtn.ondragenter = e => { - e.dataTransfer.dropEffect = 'move' - spBtn.setAttribute('drag', '') - e.preventDefault() - } - spBtn.ondragover = e => { - e.preventDefault() - } - spBtn.ondragleave = e => { - spBtn.removeAttribute('drag') - } - - spBtn.ondrop = e => { - spBtn.removeAttribute('drag') - e.preventDefault() - - DropinModUtil.addShaderpacks(e.dataTransfer.files, CACHE_SETTINGS_INSTANCE_DIR) - saveShaderpackSettings() - resolveShaderpacksForUI() - } -} - -// Server status bar functions. - -/** - * Load the currently selected server information onto the mods tab. - */ -function loadSelectedServerOnModsTab(){ - const serv = DistroManager.getDistribution().getServer(ConfigManager.getSelectedServer()) - - document.getElementById('settingsSelServContent').innerHTML = ` - -
- ${serv.getName()} - ${serv.getDescription()} -
-
${serv.getMinecraftVersion()}
-
${serv.getVersion()}
- ${serv.isMainServer() ? `
- - - - - - - - Main Server -
` : ''} -
-
- ` -} - -// Bind functionality to the server switch button. -document.getElementById('settingsSwitchServerButton').addEventListener('click', (e) => { - e.target.blur() - toggleServerSelection(true) -}) - -/** - * Save mod configuration for the current selected server. - */ -function saveAllModConfigurations(){ - saveModConfiguration() - ConfigManager.save() - saveDropinModConfiguration() -} - -/** - * Function to refresh the mods tab whenever the selected - * server is changed. - */ -function animateModsTabRefresh(){ - $('#settingsTabMods').fadeOut(500, () => { - prepareModsTab() - $('#settingsTabMods').fadeIn(500) - }) -} - -/** - * Prepare the Mods tab for display. - */ -function prepareModsTab(first){ - resolveModsForUI() - resolveDropinModsForUI() - resolveShaderpacksForUI() - bindDropinModsRemoveButton() - bindDropinModFileSystemButton() - bindShaderpackButton() - bindModsToggleSwitch() - loadSelectedServerOnModsTab() -} - -/** - * Java Tab - */ - -// DOM Cache -const settingsMaxRAMRange = document.getElementById('settingsMaxRAMRange') -const settingsMinRAMRange = document.getElementById('settingsMinRAMRange') -const settingsMaxRAMLabel = document.getElementById('settingsMaxRAMLabel') -const settingsMinRAMLabel = document.getElementById('settingsMinRAMLabel') -const settingsMemoryTotal = document.getElementById('settingsMemoryTotal') -const settingsMemoryAvail = document.getElementById('settingsMemoryAvail') -const settingsJavaExecDetails = document.getElementById('settingsJavaExecDetails') - -// Store maximum memory values. -const SETTINGS_MAX_MEMORY = ConfigManager.getAbsoluteMaxRAM() -const SETTINGS_MIN_MEMORY = ConfigManager.getAbsoluteMinRAM() - -// Set the max and min values for the ranged sliders. -settingsMaxRAMRange.setAttribute('max', SETTINGS_MAX_MEMORY) -settingsMaxRAMRange.setAttribute('min', SETTINGS_MIN_MEMORY) -settingsMinRAMRange.setAttribute('max', SETTINGS_MAX_MEMORY) -settingsMinRAMRange.setAttribute('min', SETTINGS_MIN_MEMORY ) - -// Bind on change event for min memory container. -settingsMinRAMRange.onchange = (e) => { - - // Current range values - const sMaxV = Number(settingsMaxRAMRange.getAttribute('value')) - const sMinV = Number(settingsMinRAMRange.getAttribute('value')) - - // Get reference to range bar. - const bar = e.target.getElementsByClassName('rangeSliderBar')[0] - // Calculate effective total memory. - const max = (os.totalmem()-1000000000)/1000000000 - - // Change range bar color based on the selected value. - if(sMinV >= max/2){ - bar.style.background = '#e86060' - } else if(sMinV >= max/4) { - bar.style.background = '#e8e18b' - } else { - bar.style.background = null - } - - // Increase maximum memory if the minimum exceeds its value. - if(sMaxV < sMinV){ - const sliderMeta = calculateRangeSliderMeta(settingsMaxRAMRange) - updateRangedSlider(settingsMaxRAMRange, sMinV, - ((sMinV-sliderMeta.min)/sliderMeta.step)*sliderMeta.inc) - settingsMaxRAMLabel.innerHTML = sMinV.toFixed(1) + 'G' - } - - // Update label - settingsMinRAMLabel.innerHTML = sMinV.toFixed(1) + 'G' -} - -// Bind on change event for max memory container. -settingsMaxRAMRange.onchange = (e) => { - // Current range values - const sMaxV = Number(settingsMaxRAMRange.getAttribute('value')) - const sMinV = Number(settingsMinRAMRange.getAttribute('value')) - - // Get reference to range bar. - const bar = e.target.getElementsByClassName('rangeSliderBar')[0] - // Calculate effective total memory. - const max = (os.totalmem()-1000000000)/1000000000 - - // Change range bar color based on the selected value. - if(sMaxV >= max/2){ - bar.style.background = '#e86060' - } else if(sMaxV >= max/4) { - bar.style.background = '#e8e18b' - } else { - bar.style.background = null - } - - // Decrease the minimum memory if the maximum value is less. - if(sMaxV < sMinV){ - const sliderMeta = calculateRangeSliderMeta(settingsMaxRAMRange) - updateRangedSlider(settingsMinRAMRange, sMaxV, - ((sMaxV-sliderMeta.min)/sliderMeta.step)*sliderMeta.inc) - settingsMinRAMLabel.innerHTML = sMaxV.toFixed(1) + 'G' - } - settingsMaxRAMLabel.innerHTML = sMaxV.toFixed(1) + 'G' -} - -/** - * Calculate common values for a ranged slider. - * - * @param {Element} v The range slider to calculate against. - * @returns {Object} An object with meta values for the provided ranged slider. - */ -function calculateRangeSliderMeta(v){ - const val = { - max: Number(v.getAttribute('max')), - min: Number(v.getAttribute('min')), - step: Number(v.getAttribute('step')), - } - val.ticks = (val.max-val.min)/val.step - val.inc = 100/val.ticks - return val -} - -/** - * Binds functionality to the ranged sliders. They're more than - * just divs now :'). - */ -function bindRangeSlider(){ - Array.from(document.getElementsByClassName('rangeSlider')).map((v) => { - - // Reference the track (thumb). - const track = v.getElementsByClassName('rangeSliderTrack')[0] - - // Set the initial slider value. - const value = v.getAttribute('value') - const sliderMeta = calculateRangeSliderMeta(v) - - updateRangedSlider(v, value, ((value-sliderMeta.min)/sliderMeta.step)*sliderMeta.inc) - - // The magic happens when we click on the track. - track.onmousedown = (e) => { - - // Stop moving the track on mouse up. - document.onmouseup = (e) => { - document.onmousemove = null - document.onmouseup = null - } - - // Move slider according to the mouse position. - document.onmousemove = (e) => { - - // Distance from the beginning of the bar in pixels. - const diff = e.pageX - v.offsetLeft - track.offsetWidth/2 - - // Don't move the track off the bar. - if(diff >= 0 && diff <= v.offsetWidth-track.offsetWidth/2){ - - // Convert the difference to a percentage. - const perc = (diff/v.offsetWidth)*100 - // Calculate the percentage of the closest notch. - const notch = Number(perc/sliderMeta.inc).toFixed(0)*sliderMeta.inc - - // If we're close to that notch, stick to it. - if(Math.abs(perc-notch) < sliderMeta.inc/2){ - updateRangedSlider(v, sliderMeta.min+(sliderMeta.step*(notch/sliderMeta.inc)), notch) - } - } - } - } - }) -} - -/** - * Update a ranged slider's value and position. - * - * @param {Element} element The ranged slider to update. - * @param {string | number} value The new value for the ranged slider. - * @param {number} notch The notch that the slider should now be at. - */ -function updateRangedSlider(element, value, notch){ - const oldVal = element.getAttribute('value') - const bar = element.getElementsByClassName('rangeSliderBar')[0] - const track = element.getElementsByClassName('rangeSliderTrack')[0] - - element.setAttribute('value', value) - - if(notch < 0){ - notch = 0 - } else if(notch > 100) { - notch = 100 - } - - const event = new MouseEvent('change', { - target: element, - type: 'change', - bubbles: false, - cancelable: true - }) - - let cancelled = !element.dispatchEvent(event) - - if(!cancelled){ - track.style.left = notch + '%' - bar.style.width = notch + '%' - } else { - element.setAttribute('value', oldVal) - } -} - -/** - * Display the total and available RAM. - */ -function populateMemoryStatus(){ - settingsMemoryTotal.innerHTML = Number((os.totalmem()-1000000000)/1000000000).toFixed(1) + 'G' - settingsMemoryAvail.innerHTML = Number(os.freemem()/1000000000).toFixed(1) + 'G' -} - -/** - * Validate the provided executable path and display the data on - * the UI. - * - * @param {string} execPath The executable path to populate against. - */ -function populateJavaExecDetails(execPath){ - const jg = new JavaGuard(DistroManager.getDistribution().getServer(ConfigManager.getSelectedServer()).getMinecraftVersion()) - jg._validateJavaBinary(execPath).then(v => { - if(v.valid){ - if(v.version.major < 9) { - settingsJavaExecDetails.innerHTML = `Selected: Java ${v.version.major} Update ${v.version.update} (x${v.arch})` - } else { - settingsJavaExecDetails.innerHTML = `Selected: Java ${v.version.major}.${v.version.minor}.${v.version.revision} (x${v.arch})` - } - } else { - settingsJavaExecDetails.innerHTML = 'Invalid Selection' - } - }) -} - -/** - * Prepare the Java tab for display. - */ -function prepareJavaTab(){ - bindRangeSlider() - populateMemoryStatus() -} - -/** - * About Tab - */ - -const settingsTabAbout = document.getElementById('settingsTabAbout') -const settingsAboutChangelogTitle = settingsTabAbout.getElementsByClassName('settingsChangelogTitle')[0] -const settingsAboutChangelogText = settingsTabAbout.getElementsByClassName('settingsChangelogText')[0] -const settingsAboutChangelogButton = settingsTabAbout.getElementsByClassName('settingsChangelogButton')[0] - -// Bind the devtools toggle button. -document.getElementById('settingsAboutDevToolsButton').onclick = (e) => { - let window = remote.getCurrentWindow() - window.toggleDevTools() -} - -/** - * Return whether or not the provided version is a prerelease. - * - * @param {string} version The semver version to test. - * @returns {boolean} True if the version is a prerelease, otherwise false. - */ -function isPrerelease(version){ - const preRelComp = semver.prerelease(version) - return preRelComp != null && preRelComp.length > 0 -} - -/** - * Utility method to display version information on the - * About and Update settings tabs. - * - * @param {string} version The semver version to display. - * @param {Element} valueElement The value element. - * @param {Element} titleElement The title element. - * @param {Element} checkElement The check mark element. - */ -function populateVersionInformation(version, valueElement, titleElement, checkElement){ - valueElement.innerHTML = version - if(isPrerelease(version)){ - titleElement.innerHTML = 'Pre-release' - titleElement.style.color = '#ff886d' - checkElement.style.background = '#ff886d' - } else { - titleElement.innerHTML = 'Stable Release' - titleElement.style.color = null - checkElement.style.background = null - } -} - -/** - * Retrieve the version information and display it on the UI. - */ -function populateAboutVersionInformation(){ - populateVersionInformation(remote.app.getVersion(), document.getElementById('settingsAboutCurrentVersionValue'), document.getElementById('settingsAboutCurrentVersionTitle'), document.getElementById('settingsAboutCurrentVersionCheck')) -} - -/** - * Fetches the GitHub atom release feed and parses it for the release notes - * of the current version. This value is displayed on the UI. - */ -function populateReleaseNotes(){ - $.ajax({ - url: 'https://github.com/dscalzi/HeliosLauncher/releases.atom', - success: (data) => { - const version = 'v' + remote.app.getVersion() - const entries = $(data).find('entry') - - for(let i=0; i { - settingsAboutChangelogText.innerHTML = 'Failed to load release notes.' - }) -} - -/** - * Prepare account tab for display. - */ -function prepareAboutTab(){ - populateAboutVersionInformation() - populateReleaseNotes() -} - -/** - * Update Tab - */ - -const settingsTabUpdate = document.getElementById('settingsTabUpdate') -const settingsUpdateTitle = document.getElementById('settingsUpdateTitle') -const settingsUpdateVersionCheck = document.getElementById('settingsUpdateVersionCheck') -const settingsUpdateVersionTitle = document.getElementById('settingsUpdateVersionTitle') -const settingsUpdateVersionValue = document.getElementById('settingsUpdateVersionValue') -const settingsUpdateChangelogTitle = settingsTabUpdate.getElementsByClassName('settingsChangelogTitle')[0] -const settingsUpdateChangelogText = settingsTabUpdate.getElementsByClassName('settingsChangelogText')[0] -const settingsUpdateChangelogCont = settingsTabUpdate.getElementsByClassName('settingsChangelogContainer')[0] -const settingsUpdateActionButton = document.getElementById('settingsUpdateActionButton') - -/** - * Update the properties of the update action button. - * - * @param {string} text The new button text. - * @param {boolean} disabled Optional. Disable or enable the button - * @param {function} handler Optional. New button event handler. - */ -function settingsUpdateButtonStatus(text, disabled = false, handler = null){ - settingsUpdateActionButton.innerHTML = text - settingsUpdateActionButton.disabled = disabled - if(handler != null){ - settingsUpdateActionButton.onclick = handler - } -} - -/** - * Populate the update tab with relevant information. - * - * @param {Object} data The update data. - */ -function populateSettingsUpdateInformation(data){ - if(data != null){ - settingsUpdateTitle.innerHTML = `New ${isPrerelease(data.version) ? 'Pre-release' : 'Release'} Available` - settingsUpdateChangelogCont.style.display = null - settingsUpdateChangelogTitle.innerHTML = data.releaseName - settingsUpdateChangelogText.innerHTML = data.releaseNotes - populateVersionInformation(data.version, settingsUpdateVersionValue, settingsUpdateVersionTitle, settingsUpdateVersionCheck) - - if(process.platform === 'darwin'){ - settingsUpdateButtonStatus('Download from GitHubClose the launcher and run the dmg to update.', false, () => { - shell.openExternal(data.darwindownload) - }) - } else { - settingsUpdateButtonStatus('Downloading..', true) - } - } else { - settingsUpdateTitle.innerHTML = 'You Are Running the Latest Version' - settingsUpdateChangelogCont.style.display = 'none' - populateVersionInformation(remote.app.getVersion(), settingsUpdateVersionValue, settingsUpdateVersionTitle, settingsUpdateVersionCheck) - settingsUpdateButtonStatus('Check for Updates', false, () => { - if(!isDev){ - ipcRenderer.send('autoUpdateAction', 'checkForUpdate') - settingsUpdateButtonStatus('Checking for Updates..', true) - } - }) - } -} - -/** - * Prepare update tab for display. - * - * @param {Object} data The update data. - */ -function prepareUpdateTab(data = null){ - populateSettingsUpdateInformation(data) -} - -/** - * Settings preparation functions. - */ - -/** - * Prepare the entire settings UI. - * - * @param {boolean} first Whether or not it is the first load. - */ -function prepareSettings(first = false) { - if(first){ - setupSettingsTabs() - initSettingsValidators() - prepareUpdateTab() - } else { - prepareModsTab() - } - initSettingsValues() - prepareAccountsTab() - prepareJavaTab() - prepareAboutTab() -} - -// Prepare the settings UI on startup. +// Requirements +const os = require('os') +const semver = require('semver') + +const { JavaGuard } = require('./assets/js/assetguard') +const DropinModUtil = require('./assets/js/dropinmodutil') + +const settingsState = { + invalid: new Set() +} + +function bindSettingsSelect(){ + for(let ele of document.getElementsByClassName('settingsSelectContainer')) { + const selectedDiv = ele.getElementsByClassName('settingsSelectSelected')[0] + + selectedDiv.onclick = (e) => { + e.stopPropagation() + closeSettingsSelect(e.target) + e.target.nextElementSibling.toggleAttribute('hidden') + e.target.classList.toggle('select-arrow-active') + } + } +} + +function closeSettingsSelect(el){ + for(let ele of document.getElementsByClassName('settingsSelectContainer')) { + const selectedDiv = ele.getElementsByClassName('settingsSelectSelected')[0] + const optionsDiv = ele.getElementsByClassName('settingsSelectOptions')[0] + + if(!(selectedDiv === el)) { + selectedDiv.classList.remove('select-arrow-active') + optionsDiv.setAttribute('hidden', '') + } + } +} + +/* If the user clicks anywhere outside the select box, +then close all select boxes: */ +document.addEventListener('click', closeSettingsSelect) + +bindSettingsSelect() + + +function bindFileSelectors(){ + for(let ele of document.getElementsByClassName('settingsFileSelButton')){ + + ele.onclick = async e => { + const isJavaExecSel = ele.id === 'settingsJavaExecSel' + const directoryDialog = ele.hasAttribute('dialogDirectory') && ele.getAttribute('dialogDirectory') == 'true' + const properties = directoryDialog ? ['openDirectory', 'createDirectory'] : ['openFile'] + + const options = { + properties + } + + if(ele.hasAttribute('dialogTitle')) { + options.title = ele.getAttribute('dialogTitle') + } + + if(isJavaExecSel && process.platform === 'win32') { + options.filters = [ + { name: 'Executables', extensions: ['exe'] }, + { name: 'All Files', extensions: ['*'] } + ] + } + + const res = await remote.dialog.showOpenDialog(remote.getCurrentWindow(), options) + if(!res.canceled) { + ele.previousElementSibling.value = res.filePaths[0] + if(isJavaExecSel) { + populateJavaExecDetails(ele.previousElementSibling.value) + } + } + } + } +} + +bindFileSelectors() + + +/** + * General Settings Functions + */ + +/** + * Bind value validators to the settings UI elements. These will + * validate against the criteria defined in the ConfigManager (if + * and). If the value is invalid, the UI will reflect this and saving + * will be disabled until the value is corrected. This is an automated + * process. More complex UI may need to be bound separately. + */ +function initSettingsValidators(){ + const sEls = document.getElementById('settingsContainer').querySelectorAll('[cValue]') + Array.from(sEls).map((v, index, arr) => { + const vFn = ConfigManager['validate' + v.getAttribute('cValue')] + if(typeof vFn === 'function'){ + if(v.tagName === 'INPUT'){ + if(v.type === 'number' || v.type === 'text'){ + v.addEventListener('keyup', (e) => { + const v = e.target + if(!vFn(v.value)){ + settingsState.invalid.add(v.id) + v.setAttribute('error', '') + settingsSaveDisabled(true) + } else { + if(v.hasAttribute('error')){ + v.removeAttribute('error') + settingsState.invalid.delete(v.id) + if(settingsState.invalid.size === 0){ + settingsSaveDisabled(false) + } + } + } + }) + } + } + } + + }) +} + +/** + * Load configuration values onto the UI. This is an automated process. + */ +function initSettingsValues(){ + const sEls = document.getElementById('settingsContainer').querySelectorAll('[cValue]') + Array.from(sEls).map((v, index, arr) => { + const cVal = v.getAttribute('cValue') + const gFn = ConfigManager['get' + cVal] + if(typeof gFn === 'function'){ + if(v.tagName === 'INPUT'){ + if(v.type === 'number' || v.type === 'text'){ + // Special Conditions + if(cVal === 'JavaExecutable'){ + populateJavaExecDetails(v.value) + v.value = gFn() + } else if (cVal === 'DataDirectory'){ + v.value = gFn() + } else if(cVal === 'JVMOptions'){ + v.value = gFn().join(' ') + } else { + v.value = gFn() + } + } else if(v.type === 'checkbox'){ + v.checked = gFn() + } + } else if(v.tagName === 'DIV'){ + if(v.classList.contains('rangeSlider')){ + // Special Conditions + if(cVal === 'MinRAM' || cVal === 'MaxRAM'){ + let val = gFn() + if(val.endsWith('M')){ + val = Number(val.substring(0, val.length-1))/1000 + } else { + val = Number.parseFloat(val) + } + + v.setAttribute('value', val) + } else { + v.setAttribute('value', Number.parseFloat(gFn())) + } + } + } + } + + }) +} + +/** + * Save the settings values. + */ +function saveSettingsValues(){ + const sEls = document.getElementById('settingsContainer').querySelectorAll('[cValue]') + Array.from(sEls).map((v, index, arr) => { + const cVal = v.getAttribute('cValue') + const sFn = ConfigManager['set' + cVal] + if(typeof sFn === 'function'){ + if(v.tagName === 'INPUT'){ + if(v.type === 'number' || v.type === 'text'){ + // Special Conditions + if(cVal === 'JVMOptions'){ + sFn(v.value.split(' ')) + } else { + sFn(v.value) + } + } else if(v.type === 'checkbox'){ + sFn(v.checked) + // Special Conditions + if(cVal === 'AllowPrerelease'){ + changeAllowPrerelease(v.checked) + } + } + } else if(v.tagName === 'DIV'){ + if(v.classList.contains('rangeSlider')){ + // Special Conditions + if(cVal === 'MinRAM' || cVal === 'MaxRAM'){ + let val = Number(v.getAttribute('value')) + if(val%1 > 0){ + val = val*1000 + 'M' + } else { + val = val + 'G' + } + + sFn(val) + } else { + sFn(v.getAttribute('value')) + } + } + } + } + }) +} + +let selectedSettingsTab = 'settingsTabAccount' + +/** + * Modify the settings container UI when the scroll threshold reaches + * a certain poin. + * + * @param {UIEvent} e The scroll event. + */ +function settingsTabScrollListener(e){ + if(e.target.scrollTop > Number.parseFloat(getComputedStyle(e.target.firstElementChild).marginTop)){ + document.getElementById('settingsContainer').setAttribute('scrolled', '') + } else { + document.getElementById('settingsContainer').removeAttribute('scrolled') + } +} + +/** + * Bind functionality for the settings navigation items. + */ +function setupSettingsTabs(){ + Array.from(document.getElementsByClassName('settingsNavItem')).map((val) => { + if(val.hasAttribute('rSc')){ + val.onclick = () => { + settingsNavItemListener(val) + } + } + }) +} + +/** + * Settings nav item onclick lisener. Function is exposed so that + * other UI elements can quickly toggle to a certain tab from other views. + * + * @param {Element} ele The nav item which has been clicked. + * @param {boolean} fade Optional. True to fade transition. + */ +function settingsNavItemListener(ele, fade = true){ + if(ele.hasAttribute('selected')){ + return + } + const navItems = document.getElementsByClassName('settingsNavItem') + for(let i=0; i { + $(`#${selectedSettingsTab}`).fadeIn({ + duration: 250, + start: () => { + settingsTabScrollListener({ + target: document.getElementById(selectedSettingsTab) + }) + } + }) + }) + } else { + $(`#${prevTab}`).hide(0, () => { + $(`#${selectedSettingsTab}`).show({ + duration: 0, + start: () => { + settingsTabScrollListener({ + target: document.getElementById(selectedSettingsTab) + }) + } + }) + }) + } +} + +const settingsNavDone = document.getElementById('settingsNavDone') + +/** + * Set if the settings save (done) button is disabled. + * + * @param {boolean} v True to disable, false to enable. + */ +function settingsSaveDisabled(v){ + settingsNavDone.disabled = v +} + +/* Closes the settings view and saves all data. */ +settingsNavDone.onclick = () => { + saveSettingsValues() + saveModConfiguration() + ConfigManager.save() + saveDropinModConfiguration() + saveShaderpackSettings() + switchView(getCurrentView(), VIEWS.landing) +} + +/** + * Account Management Tab + */ + +// Bind the add account button. +document.getElementById('settingsAddAccount').onclick = (e) => { + switchView(getCurrentView(), VIEWS.login, 500, 500, () => { + loginViewOnCancel = VIEWS.settings + loginViewOnSuccess = VIEWS.settings + loginCancelEnabled(true) + }) +} + +/** + * Bind functionality for the account selection buttons. If another account + * is selected, the UI of the previously selected account will be updated. + */ +function bindAuthAccountSelect(){ + Array.from(document.getElementsByClassName('settingsAuthAccountSelect')).map((val) => { + val.onclick = (e) => { + if(val.hasAttribute('selected')){ + return + } + const selectBtns = document.getElementsByClassName('settingsAuthAccountSelect') + for(let i=0; i { + val.onclick = (e) => { + let isLastAccount = false + if(Object.keys(ConfigManager.getAuthAccounts()).length === 1){ + isLastAccount = true + setOverlayContent( + 'Attention !
Ceci est votre dernier compte Minecraft.', + 'Pour utiliser le launcher, vous devez êtres connecté à au moins un compte Minecraft. Vous devrez vous reconnecter après.

Etes-vous sûr de vouloir vous déconnecter ?', + 'Je suis sûr de moi', + 'Annuler' + ) + setOverlayHandler(() => { + processLogOut(val, isLastAccount) + toggleOverlay(false) + switchView(getCurrentView(), VIEWS.login) + }) + setDismissHandler(() => { + toggleOverlay(false) + }) + toggleOverlay(true, true) + } else { + processLogOut(val, isLastAccount) + } + + } + }) +} + +/** + * Process a log out. + * + * @param {Element} val The log out button element. + * @param {boolean} isLastAccount If this logout is on the last added account. + */ +function processLogOut(val, isLastAccount){ + const parent = val.closest('.settingsAuthAccount') + const uuid = parent.getAttribute('uuid') + const prevSelAcc = ConfigManager.getSelectedAccount() + AuthManager.removeAccount(uuid).then(() => { + if(!isLastAccount && uuid === prevSelAcc.uuid){ + const selAcc = ConfigManager.getSelectedAccount() + refreshAuthAccountSelected(selAcc.uuid) + updateSelectedAccount(selAcc) + validateSelectedAccount() + } + }) + $(parent).fadeOut(250, () => { + parent.remove() + }) +} + +/** + * Refreshes the status of the selected account on the auth account + * elements. + * + * @param {string} uuid The UUID of the new selected account. + */ +function refreshAuthAccountSelected(uuid){ + Array.from(document.getElementsByClassName('settingsAuthAccount')).map((val) => { + const selBtn = val.getElementsByClassName('settingsAuthAccountSelect')[0] + if(uuid === val.getAttribute('uuid')){ + selBtn.setAttribute('selected', '') + selBtn.innerHTML = 'Compte Séléctionné ✔' + } else { + if(selBtn.hasAttribute('selected')){ + selBtn.removeAttribute('selected') + } + selBtn.innerHTML = 'Séléctionner ce compte' + } + }) +} + +const settingsCurrentAccounts = document.getElementById('settingsCurrentAccounts') + +/** + * Add auth account elements for each one stored in the authentication database. + */ +function populateAuthAccounts(){ + const authAccounts = ConfigManager.getAuthAccounts() + const authKeys = Object.keys(authAccounts) + if(authKeys.length === 0){ + return + } + const selectedUUID = ConfigManager.getSelectedAccount().uuid + + let authAccountStr = '' + + authKeys.map((val) => { + const acc = authAccounts[val] + authAccountStr += `
+
+ ${acc.displayName} +
+
+
+
+
Pseudonyme
+
${acc.displayName}
+
+
+
UUID
+
${acc.uuid}
+
+
+
+ +
+ +
+
+
+
` + }) + + settingsCurrentAccounts.innerHTML = authAccountStr +} + +/** + * Prepare the accounts tab for display. + */ +function prepareAccountsTab() { + populateAuthAccounts() + bindAuthAccountSelect() + bindAuthAccountLogOut() +} + +/** + * Minecraft Tab + */ + +/** + * Disable decimals, negative signs, and scientific notation. + */ +document.getElementById('settingsGameWidth').addEventListener('keydown', (e) => { + if(/^[-.eE]$/.test(e.key)){ + e.preventDefault() + } +}) +document.getElementById('settingsGameHeight').addEventListener('keydown', (e) => { + if(/^[-.eE]$/.test(e.key)){ + e.preventDefault() + } +}) + +/** + * Mods Tab + */ + +const settingsModsContainer = document.getElementById('settingsModsContainer') + +/** + * Resolve and update the mods on the UI. + */ +function resolveModsForUI(){ + const serv = ConfigManager.getSelectedServer() + + const distro = DistroManager.getDistribution() + const servConf = ConfigManager.getModConfiguration(serv) + + const modStr = parseModulesForUI(distro.getServer(serv).getModules(), false, servConf.mods) + + document.getElementById('settingsReqModsContent').innerHTML = modStr.reqMods + document.getElementById('settingsOptModsContent').innerHTML = modStr.optMods +} + +/** + * Recursively build the mod UI elements. + * + * @param {Object[]} mdls An array of modules to parse. + * @param {boolean} submodules Whether or not we are parsing submodules. + * @param {Object} servConf The server configuration object for this module level. + */ +function parseModulesForUI(mdls, submodules, servConf){ + + let reqMods = '' + let optMods = '' + + for(const mdl of mdls){ + + if(mdl.getType() === DistroManager.Types.ForgeMod || mdl.getType() === DistroManager.Types.LiteMod || mdl.getType() === DistroManager.Types.LiteLoader){ + + if(mdl.getRequired().isRequired()){ + + reqMods += `
+
+
+
+
+ ${mdl.getName()} + v${mdl.getVersion()} +
+
+ +
+ ${mdl.hasSubModules() ? `
+ ${Object.values(parseModulesForUI(mdl.getSubModules(), true, servConf[mdl.getVersionlessID()])).join('')} +
` : ''} +
` + + } else { + + const conf = servConf[mdl.getVersionlessID()] + const val = typeof conf === 'object' ? conf.value : conf + + optMods += `
+
+
+
+
+ ${mdl.getName()} + v${mdl.getVersion()} +
+
+ +
+ ${mdl.hasSubModules() ? `
+ ${Object.values(parseModulesForUI(mdl.getSubModules(), true, conf.mods)).join('')} +
` : ''} +
` + + } + } + } + + return { + reqMods, + optMods + } + +} + +/** + * Bind functionality to mod config toggle switches. Switching the value + * will also switch the status color on the left of the mod UI. + */ +function bindModsToggleSwitch(){ + const sEls = settingsModsContainer.querySelectorAll('[formod]') + Array.from(sEls).map((v, index, arr) => { + v.onchange = () => { + if(v.checked) { + document.getElementById(v.getAttribute('formod')).setAttribute('enabled', '') + } else { + document.getElementById(v.getAttribute('formod')).removeAttribute('enabled') + } + } + }) +} + + +/** + * Save the mod configuration based on the UI values. + */ +function saveModConfiguration(){ + const serv = ConfigManager.getSelectedServer() + const modConf = ConfigManager.getModConfiguration(serv) + modConf.mods = _saveModConfiguration(modConf.mods) + ConfigManager.setModConfiguration(serv, modConf) +} + +/** + * Recursively save mod config with submods. + * + * @param {Object} modConf Mod config object to save. + */ +function _saveModConfiguration(modConf){ + for(let m of Object.entries(modConf)){ + const tSwitch = settingsModsContainer.querySelectorAll(`[formod='${m[0]}']`) + if(!tSwitch[0].hasAttribute('dropin')){ + if(typeof m[1] === 'boolean'){ + modConf[m[0]] = tSwitch[0].checked + } else { + if(m[1] != null){ + if(tSwitch.length > 0){ + modConf[m[0]].value = tSwitch[0].checked + } + modConf[m[0]].mods = _saveModConfiguration(modConf[m[0]].mods) + } + } + } + } + return modConf +} + +// Drop-in mod elements. + +let CACHE_SETTINGS_MODS_DIR +let CACHE_DROPIN_MODS + +/** + * Resolve any located drop-in mods for this server and + * populate the results onto the UI. + */ +function resolveDropinModsForUI(){ + const serv = DistroManager.getDistribution().getServer(ConfigManager.getSelectedServer()) + CACHE_SETTINGS_MODS_DIR = path.join(ConfigManager.getInstanceDirectory(), serv.getID(), 'mods') + CACHE_DROPIN_MODS = DropinModUtil.scanForDropinMods(CACHE_SETTINGS_MODS_DIR, serv.getMinecraftVersion()) + + let dropinMods = '' + + for(dropin of CACHE_DROPIN_MODS){ + dropinMods += `
+
+
+
+
+ ${dropin.name} +
+ +
+
+
+ +
+
` + } + + document.getElementById('settingsDropinModsContent').innerHTML = dropinMods +} + +/** + * Bind the remove button for each loaded drop-in mod. + */ +function bindDropinModsRemoveButton(){ + const sEls = settingsModsContainer.querySelectorAll('[remmod]') + Array.from(sEls).map((v, index, arr) => { + v.onclick = () => { + const fullName = v.getAttribute('remmod') + const res = DropinModUtil.deleteDropinMod(CACHE_SETTINGS_MODS_DIR, fullName) + if(res){ + document.getElementById(fullName).remove() + } else { + setOverlayContent( + `Failed to Delete
Drop-in Mod ${fullName}`, + 'Make sure the file is not in use and try again.', + 'Okay' + ) + setOverlayHandler(null) + toggleOverlay(true) + } + } + }) +} + +/** + * Bind functionality to the file system button for the selected + * server configuration. + */ +function bindDropinModFileSystemButton(){ + const fsBtn = document.getElementById('settingsDropinFileSystemButton') + fsBtn.onclick = () => { + DropinModUtil.validateDir(CACHE_SETTINGS_MODS_DIR) + shell.openPath(CACHE_SETTINGS_MODS_DIR) + } + fsBtn.ondragenter = e => { + e.dataTransfer.dropEffect = 'move' + fsBtn.setAttribute('drag', '') + e.preventDefault() + } + fsBtn.ondragover = e => { + e.preventDefault() + } + fsBtn.ondragleave = e => { + fsBtn.removeAttribute('drag') + } + + fsBtn.ondrop = e => { + fsBtn.removeAttribute('drag') + e.preventDefault() + + DropinModUtil.addDropinMods(e.dataTransfer.files, CACHE_SETTINGS_MODS_DIR) + reloadDropinMods() + } +} + +/** + * Save drop-in mod states. Enabling and disabling is just a matter + * of adding/removing the .disabled extension. + */ +function saveDropinModConfiguration(){ + for(dropin of CACHE_DROPIN_MODS){ + const dropinUI = document.getElementById(dropin.fullName) + if(dropinUI != null){ + const dropinUIEnabled = dropinUI.hasAttribute('enabled') + if(DropinModUtil.isDropinModEnabled(dropin.fullName) != dropinUIEnabled){ + DropinModUtil.toggleDropinMod(CACHE_SETTINGS_MODS_DIR, dropin.fullName, dropinUIEnabled).catch(err => { + if(!isOverlayVisible()){ + setOverlayContent( + 'Failed to Toggle
One or More Drop-in Mods', + err.message, + 'Okay' + ) + setOverlayHandler(null) + toggleOverlay(true) + } + }) + } + } + } +} + +// Refresh the drop-in mods when F5 is pressed. +// Only active on the mods tab. +document.addEventListener('keydown', (e) => { + if(getCurrentView() === VIEWS.settings && selectedSettingsTab === 'settingsTabMods'){ + if(e.key === 'F5'){ + reloadDropinMods() + saveShaderpackSettings() + resolveShaderpacksForUI() + } + } +}) + +function reloadDropinMods(){ + resolveDropinModsForUI() + bindDropinModsRemoveButton() + bindDropinModFileSystemButton() + bindModsToggleSwitch() +} + +// Shaderpack + +let CACHE_SETTINGS_INSTANCE_DIR +let CACHE_SHADERPACKS +let CACHE_SELECTED_SHADERPACK + +/** + * Load shaderpack information. + */ +function resolveShaderpacksForUI(){ + const serv = DistroManager.getDistribution().getServer(ConfigManager.getSelectedServer()) + CACHE_SETTINGS_INSTANCE_DIR = path.join(ConfigManager.getInstanceDirectory(), serv.getID()) + CACHE_SHADERPACKS = DropinModUtil.scanForShaderpacks(CACHE_SETTINGS_INSTANCE_DIR) + CACHE_SELECTED_SHADERPACK = DropinModUtil.getEnabledShaderpack(CACHE_SETTINGS_INSTANCE_DIR) + + setShadersOptions(CACHE_SHADERPACKS, CACHE_SELECTED_SHADERPACK) +} + +function setShadersOptions(arr, selected){ + const cont = document.getElementById('settingsShadersOptions') + cont.innerHTML = '' + for(let opt of arr) { + const d = document.createElement('DIV') + d.innerHTML = opt.name + d.setAttribute('value', opt.fullName) + if(opt.fullName === selected) { + d.setAttribute('selected', '') + document.getElementById('settingsShadersSelected').innerHTML = opt.name + } + d.addEventListener('click', function(e) { + this.parentNode.previousElementSibling.innerHTML = this.innerHTML + for(let sib of this.parentNode.children){ + sib.removeAttribute('selected') + } + this.setAttribute('selected', '') + closeSettingsSelect() + }) + cont.appendChild(d) + } +} + +function saveShaderpackSettings(){ + let sel = 'OFF' + for(let opt of document.getElementById('settingsShadersOptions').childNodes){ + if(opt.hasAttribute('selected')){ + sel = opt.getAttribute('value') + } + } + DropinModUtil.setEnabledShaderpack(CACHE_SETTINGS_INSTANCE_DIR, sel) +} + +function bindShaderpackButton() { + const spBtn = document.getElementById('settingsShaderpackButton') + spBtn.onclick = () => { + const p = path.join(CACHE_SETTINGS_INSTANCE_DIR, 'shaderpacks') + DropinModUtil.validateDir(p) + shell.openPath(p) + } + spBtn.ondragenter = e => { + e.dataTransfer.dropEffect = 'move' + spBtn.setAttribute('drag', '') + e.preventDefault() + } + spBtn.ondragover = e => { + e.preventDefault() + } + spBtn.ondragleave = e => { + spBtn.removeAttribute('drag') + } + + spBtn.ondrop = e => { + spBtn.removeAttribute('drag') + e.preventDefault() + + DropinModUtil.addShaderpacks(e.dataTransfer.files, CACHE_SETTINGS_INSTANCE_DIR) + saveShaderpackSettings() + resolveShaderpacksForUI() + } +} + +// Server status bar functions. + +/** + * Load the currently selected server information onto the mods tab. + */ +function loadSelectedServerOnModsTab(){ + const serv = DistroManager.getDistribution().getServer(ConfigManager.getSelectedServer()) + + document.getElementById('settingsSelServContent').innerHTML = ` + +
+ ${serv.getName()} + ${serv.getDescription()} +
+
${serv.getMinecraftVersion()}
+
${serv.getVersion()}
+ ${serv.isMainServer() ? `
+ + + + + + + + Main Server +
` : ''} +
+
+ ` +} + +// Bind functionality to the server switch button. +document.getElementById('settingsSwitchServerButton').addEventListener('click', (e) => { + e.target.blur() + toggleServerSelection(true) +}) + +/** + * Save mod configuration for the current selected server. + */ +function saveAllModConfigurations(){ + saveModConfiguration() + ConfigManager.save() + saveDropinModConfiguration() +} + +/** + * Function to refresh the mods tab whenever the selected + * server is changed. + */ +function animateModsTabRefresh(){ + $('#settingsTabMods').fadeOut(500, () => { + prepareModsTab() + $('#settingsTabMods').fadeIn(500) + }) +} + +/** + * Prepare the Mods tab for display. + */ +function prepareModsTab(first){ + resolveModsForUI() + resolveDropinModsForUI() + resolveShaderpacksForUI() + bindDropinModsRemoveButton() + bindDropinModFileSystemButton() + bindShaderpackButton() + bindModsToggleSwitch() + loadSelectedServerOnModsTab() +} + +/** + * Java Tab + */ + +// DOM Cache +const settingsMaxRAMRange = document.getElementById('settingsMaxRAMRange') +const settingsMinRAMRange = document.getElementById('settingsMinRAMRange') +const settingsMaxRAMLabel = document.getElementById('settingsMaxRAMLabel') +const settingsMinRAMLabel = document.getElementById('settingsMinRAMLabel') +const settingsMemoryTotal = document.getElementById('settingsMemoryTotal') +const settingsMemoryAvail = document.getElementById('settingsMemoryAvail') +const settingsJavaExecDetails = document.getElementById('settingsJavaExecDetails') + +// Store maximum memory values. +const SETTINGS_MAX_MEMORY = ConfigManager.getAbsoluteMaxRAM() +const SETTINGS_MIN_MEMORY = ConfigManager.getAbsoluteMinRAM() + +// Set the max and min values for the ranged sliders. +settingsMaxRAMRange.setAttribute('max', SETTINGS_MAX_MEMORY) +settingsMaxRAMRange.setAttribute('min', SETTINGS_MIN_MEMORY) +settingsMinRAMRange.setAttribute('max', SETTINGS_MAX_MEMORY) +settingsMinRAMRange.setAttribute('min', SETTINGS_MIN_MEMORY ) + +// Bind on change event for min memory container. +settingsMinRAMRange.onchange = (e) => { + + // Current range values + const sMaxV = Number(settingsMaxRAMRange.getAttribute('value')) + const sMinV = Number(settingsMinRAMRange.getAttribute('value')) + + // Get reference to range bar. + const bar = e.target.getElementsByClassName('rangeSliderBar')[0] + // Calculate effective total memory. + const max = (os.totalmem()-1000000000)/1000000000 + + // Change range bar color based on the selected value. + if(sMinV >= max/2){ + bar.style.background = '#e86060' + } else if(sMinV >= max/4) { + bar.style.background = '#e8e18b' + } else { + bar.style.background = null + } + + // Increase maximum memory if the minimum exceeds its value. + if(sMaxV < sMinV){ + const sliderMeta = calculateRangeSliderMeta(settingsMaxRAMRange) + updateRangedSlider(settingsMaxRAMRange, sMinV, + ((sMinV-sliderMeta.min)/sliderMeta.step)*sliderMeta.inc) + settingsMaxRAMLabel.innerHTML = sMinV.toFixed(1) + 'G' + } + + // Update label + settingsMinRAMLabel.innerHTML = sMinV.toFixed(1) + 'G' +} + +// Bind on change event for max memory container. +settingsMaxRAMRange.onchange = (e) => { + // Current range values + const sMaxV = Number(settingsMaxRAMRange.getAttribute('value')) + const sMinV = Number(settingsMinRAMRange.getAttribute('value')) + + // Get reference to range bar. + const bar = e.target.getElementsByClassName('rangeSliderBar')[0] + // Calculate effective total memory. + const max = (os.totalmem()-1000000000)/1000000000 + + // Change range bar color based on the selected value. + if(sMaxV >= max/2){ + bar.style.background = '#e86060' + } else if(sMaxV >= max/4) { + bar.style.background = '#e8e18b' + } else { + bar.style.background = null + } + + // Decrease the minimum memory if the maximum value is less. + if(sMaxV < sMinV){ + const sliderMeta = calculateRangeSliderMeta(settingsMaxRAMRange) + updateRangedSlider(settingsMinRAMRange, sMaxV, + ((sMaxV-sliderMeta.min)/sliderMeta.step)*sliderMeta.inc) + settingsMinRAMLabel.innerHTML = sMaxV.toFixed(1) + 'G' + } + settingsMaxRAMLabel.innerHTML = sMaxV.toFixed(1) + 'G' +} + +/** + * Calculate common values for a ranged slider. + * + * @param {Element} v The range slider to calculate against. + * @returns {Object} An object with meta values for the provided ranged slider. + */ +function calculateRangeSliderMeta(v){ + const val = { + max: Number(v.getAttribute('max')), + min: Number(v.getAttribute('min')), + step: Number(v.getAttribute('step')), + } + val.ticks = (val.max-val.min)/val.step + val.inc = 100/val.ticks + return val +} + +/** + * Binds functionality to the ranged sliders. They're more than + * just divs now :'). + */ +function bindRangeSlider(){ + Array.from(document.getElementsByClassName('rangeSlider')).map((v) => { + + // Reference the track (thumb). + const track = v.getElementsByClassName('rangeSliderTrack')[0] + + // Set the initial slider value. + const value = v.getAttribute('value') + const sliderMeta = calculateRangeSliderMeta(v) + + updateRangedSlider(v, value, ((value-sliderMeta.min)/sliderMeta.step)*sliderMeta.inc) + + // The magic happens when we click on the track. + track.onmousedown = (e) => { + + // Stop moving the track on mouse up. + document.onmouseup = (e) => { + document.onmousemove = null + document.onmouseup = null + } + + // Move slider according to the mouse position. + document.onmousemove = (e) => { + + // Distance from the beginning of the bar in pixels. + const diff = e.pageX - v.offsetLeft - track.offsetWidth/2 + + // Don't move the track off the bar. + if(diff >= 0 && diff <= v.offsetWidth-track.offsetWidth/2){ + + // Convert the difference to a percentage. + const perc = (diff/v.offsetWidth)*100 + // Calculate the percentage of the closest notch. + const notch = Number(perc/sliderMeta.inc).toFixed(0)*sliderMeta.inc + + // If we're close to that notch, stick to it. + if(Math.abs(perc-notch) < sliderMeta.inc/2){ + updateRangedSlider(v, sliderMeta.min+(sliderMeta.step*(notch/sliderMeta.inc)), notch) + } + } + } + } + }) +} + +/** + * Update a ranged slider's value and position. + * + * @param {Element} element The ranged slider to update. + * @param {string | number} value The new value for the ranged slider. + * @param {number} notch The notch that the slider should now be at. + */ +function updateRangedSlider(element, value, notch){ + const oldVal = element.getAttribute('value') + const bar = element.getElementsByClassName('rangeSliderBar')[0] + const track = element.getElementsByClassName('rangeSliderTrack')[0] + + element.setAttribute('value', value) + + if(notch < 0){ + notch = 0 + } else if(notch > 100) { + notch = 100 + } + + const event = new MouseEvent('change', { + target: element, + type: 'change', + bubbles: false, + cancelable: true + }) + + let cancelled = !element.dispatchEvent(event) + + if(!cancelled){ + track.style.left = notch + '%' + bar.style.width = notch + '%' + } else { + element.setAttribute('value', oldVal) + } +} + +/** + * Display the total and available RAM. + */ +function populateMemoryStatus(){ + settingsMemoryTotal.innerHTML = Number((os.totalmem()-1000000000)/1000000000).toFixed(1) + 'G' + settingsMemoryAvail.innerHTML = Number(os.freemem()/1000000000).toFixed(1) + 'G' +} + +/** + * Validate the provided executable path and display the data on + * the UI. + * + * @param {string} execPath The executable path to populate against. + */ +function populateJavaExecDetails(execPath){ + const jg = new JavaGuard(DistroManager.getDistribution().getServer(ConfigManager.getSelectedServer()).getMinecraftVersion()) + jg._validateJavaBinary(execPath).then(v => { + if(v.valid){ + if(v.version.major < 9) { + settingsJavaExecDetails.innerHTML = `Selected: Java ${v.version.major} Update ${v.version.update} (x${v.arch})` + } else { + settingsJavaExecDetails.innerHTML = `Selected: Java ${v.version.major}.${v.version.minor}.${v.version.revision} (x${v.arch})` + } + } else { + settingsJavaExecDetails.innerHTML = 'Invalid Selection' + } + }) +} + +/** + * Prepare the Java tab for display. + */ +function prepareJavaTab(){ + bindRangeSlider() + populateMemoryStatus() +} + +/** + * About Tab + */ + +const settingsTabAbout = document.getElementById('settingsTabAbout') +const settingsAboutChangelogTitle = settingsTabAbout.getElementsByClassName('settingsChangelogTitle')[0] +const settingsAboutChangelogText = settingsTabAbout.getElementsByClassName('settingsChangelogText')[0] +const settingsAboutChangelogButton = settingsTabAbout.getElementsByClassName('settingsChangelogButton')[0] + +// Bind the devtools toggle button. +document.getElementById('settingsAboutDevToolsButton').onclick = (e) => { + let window = remote.getCurrentWindow() + window.toggleDevTools() +} + +/** + * Return whether or not the provided version is a prerelease. + * + * @param {string} version The semver version to test. + * @returns {boolean} True if the version is a prerelease, otherwise false. + */ +function isPrerelease(version){ + const preRelComp = semver.prerelease(version) + return preRelComp != null && preRelComp.length > 0 +} + +/** + * Utility method to display version information on the + * About and Update settings tabs. + * + * @param {string} version The semver version to display. + * @param {Element} valueElement The value element. + * @param {Element} titleElement The title element. + * @param {Element} checkElement The check mark element. + */ +function populateVersionInformation(version, valueElement, titleElement, checkElement){ + valueElement.innerHTML = version + if(isPrerelease(version)){ + titleElement.innerHTML = 'Pre-release' + titleElement.style.color = '#ff886d' + checkElement.style.background = '#ff886d' + } else { + titleElement.innerHTML = 'Stable Release' + titleElement.style.color = null + checkElement.style.background = null + } +} + +/** + * Retrieve the version information and display it on the UI. + */ +function populateAboutVersionInformation(){ + populateVersionInformation(remote.app.getVersion(), document.getElementById('settingsAboutCurrentVersionValue'), document.getElementById('settingsAboutCurrentVersionTitle'), document.getElementById('settingsAboutCurrentVersionCheck')) +} + +/** + * Fetches the GitHub atom release feed and parses it for the release notes + * of the current version. This value is displayed on the UI. + */ +function populateReleaseNotes(){ + $.ajax({ + url: 'https://github.com/dscalzi/HeliosLauncher/releases.atom', + success: (data) => { + const version = 'v' + remote.app.getVersion() + const entries = $(data).find('entry') + + for(let i=0; i { + settingsAboutChangelogText.innerHTML = 'Failed to load release notes.' + }) +} + +/** + * Prepare account tab for display. + */ +function prepareAboutTab(){ + populateAboutVersionInformation() + populateReleaseNotes() +} + +/** + * Update Tab + */ + +const settingsTabUpdate = document.getElementById('settingsTabUpdate') +const settingsUpdateTitle = document.getElementById('settingsUpdateTitle') +const settingsUpdateVersionCheck = document.getElementById('settingsUpdateVersionCheck') +const settingsUpdateVersionTitle = document.getElementById('settingsUpdateVersionTitle') +const settingsUpdateVersionValue = document.getElementById('settingsUpdateVersionValue') +const settingsUpdateChangelogTitle = settingsTabUpdate.getElementsByClassName('settingsChangelogTitle')[0] +const settingsUpdateChangelogText = settingsTabUpdate.getElementsByClassName('settingsChangelogText')[0] +const settingsUpdateChangelogCont = settingsTabUpdate.getElementsByClassName('settingsChangelogContainer')[0] +const settingsUpdateActionButton = document.getElementById('settingsUpdateActionButton') + +/** + * Update the properties of the update action button. + * + * @param {string} text The new button text. + * @param {boolean} disabled Optional. Disable or enable the button + * @param {function} handler Optional. New button event handler. + */ +function settingsUpdateButtonStatus(text, disabled = false, handler = null){ + settingsUpdateActionButton.innerHTML = text + settingsUpdateActionButton.disabled = disabled + if(handler != null){ + settingsUpdateActionButton.onclick = handler + } +} + +/** + * Populate the update tab with relevant information. + * + * @param {Object} data The update data. + */ +function populateSettingsUpdateInformation(data){ + if(data != null){ + settingsUpdateTitle.innerHTML = `New ${isPrerelease(data.version) ? 'Pre-release' : 'Release'} Available` + settingsUpdateChangelogCont.style.display = null + settingsUpdateChangelogTitle.innerHTML = data.releaseName + settingsUpdateChangelogText.innerHTML = data.releaseNotes + populateVersionInformation(data.version, settingsUpdateVersionValue, settingsUpdateVersionTitle, settingsUpdateVersionCheck) + + if(process.platform === 'darwin'){ + settingsUpdateButtonStatus('Download from GitHubClose the launcher and run the dmg to update.', false, () => { + shell.openExternal(data.darwindownload) + }) + } else { + settingsUpdateButtonStatus('Downloading..', true) + } + } else { + settingsUpdateTitle.innerHTML = 'You Are Running the Latest Version' + settingsUpdateChangelogCont.style.display = 'none' + populateVersionInformation(remote.app.getVersion(), settingsUpdateVersionValue, settingsUpdateVersionTitle, settingsUpdateVersionCheck) + settingsUpdateButtonStatus('Check for Updates', false, () => { + if(!isDev){ + ipcRenderer.send('autoUpdateAction', 'checkForUpdate') + settingsUpdateButtonStatus('Checking for Updates..', true) + } + }) + } +} + +/** + * Prepare update tab for display. + * + * @param {Object} data The update data. + */ +function prepareUpdateTab(data = null){ + populateSettingsUpdateInformation(data) +} + +/** + * Settings preparation functions. + */ + +/** + * Prepare the entire settings UI. + * + * @param {boolean} first Whether or not it is the first load. + */ +function prepareSettings(first = false) { + if(first){ + setupSettingsTabs() + initSettingsValidators() + prepareUpdateTab() + } else { + prepareModsTab() + } + initSettingsValues() + prepareAccountsTab() + prepareJavaTab() + prepareAboutTab() +} + +// Prepare the settings UI on startup. //prepareSettings(true) \ No newline at end of file diff --git a/app/assets/js/scripts/uibinder.js b/app/assets/js/scripts/uibinder.js index 0b080d1b..f16302d6 100644 --- a/app/assets/js/scripts/uibinder.js +++ b/app/assets/js/scripts/uibinder.js @@ -1,419 +1,419 @@ -/** - * Initialize UI functions which depend on internal modules. - * Loaded after core UI functions are initialized in uicore.js. - */ -// Requirements -const path = require('path') - -const AuthManager = require('./assets/js/authmanager') -const ConfigManager = require('./assets/js/configmanager') -const DistroManager = require('./assets/js/distromanager') -const Lang = require('./assets/js/langloader') - -let rscShouldLoad = false -let fatalStartupError = false - -// Mapping of each view to their container IDs. -const VIEWS = { - landing: '#landingContainer', - login: '#loginContainer', - settings: '#settingsContainer', - welcome: '#welcomeContainer' -} - -// The currently shown view container. -let currentView - -/** - * Switch launcher views. - * - * @param {string} current The ID of the current view container. - * @param {*} next The ID of the next view container. - * @param {*} currentFadeTime Optional. The fade out time for the current view. - * @param {*} nextFadeTime Optional. The fade in time for the next view. - * @param {*} onCurrentFade Optional. Callback function to execute when the current - * view fades out. - * @param {*} onNextFade Optional. Callback function to execute when the next view - * fades in. - */ -function switchView(current, next, currentFadeTime = 500, nextFadeTime = 500, onCurrentFade = () => {}, onNextFade = () => {}){ - currentView = next - $(`${current}`).fadeOut(currentFadeTime, () => { - onCurrentFade() - $(`${next}`).fadeIn(nextFadeTime, () => { - onNextFade() - }) - }) -} - -/** - * Get the currently shown view container. - * - * @returns {string} The currently shown view container. - */ -function getCurrentView(){ - return currentView -} - -function showMainUI(data){ - - if(!isDev){ - loggerAutoUpdater.log('Initializing..') - ipcRenderer.send('autoUpdateAction', 'initAutoUpdater', ConfigManager.getAllowPrerelease()) - } - - prepareSettings(true) - updateSelectedServer(data.getServer(ConfigManager.getSelectedServer())) - refreshServerStatus() - setTimeout(() => { - document.getElementById('frameBar').style.backgroundColor = 'rgba(0, 0, 0, 0.5)' - document.body.style.backgroundImage = `url('assets/images/backgrounds/${document.body.getAttribute('bkid')}.jpg')` - $('#main').show() - - const isLoggedIn = Object.keys(ConfigManager.getAuthAccounts()).length > 0 - - // If this is enabled in a development environment we'll get ratelimited. - // The relaunch frequency is usually far too high. - if(!isDev && isLoggedIn){ - validateSelectedAccount() - } - - if(ConfigManager.isFirstLaunch()){ - currentView = VIEWS.welcome - $(VIEWS.welcome).fadeIn(1000) - } else { - if(isLoggedIn){ - currentView = VIEWS.landing - $(VIEWS.landing).fadeIn(1000) - } else { - currentView = VIEWS.login - $(VIEWS.login).fadeIn(1000) - } - } - - setTimeout(() => { - $('#loadingContainer').fadeOut(500, () => { - $('#loadSpinnerImage').removeClass('rotating') - }) - }, 250) - - }, 750) - // Disable tabbing to the news container. - initNews().then(() => { - $('#newsContainer *').attr('tabindex', '-1') - }) -} - -function showFatalStartupError(){ - setTimeout(() => { - $('#loadingContainer').fadeOut(250, () => { - document.getElementById('overlayContainer').style.background = 'none' - setOverlayContent( - 'Fatal Error: Unable to Load Distribution Index', - 'A connection could not be established to our servers to download the distribution index. No local copies were available to load.

The distribution index is an essential file which provides the latest server information. The launcher is unable to start without it. Ensure you are connected to the internet and relaunch the application.', - 'Close' - ) - setOverlayHandler(() => { - const window = remote.getCurrentWindow() - window.close() - }) - toggleOverlay(true) - }) - }, 750) -} - -/** - * Common functions to perform after refreshing the distro index. - * - * @param {Object} data The distro index object. - */ -function onDistroRefresh(data){ - updateSelectedServer(data.getServer(ConfigManager.getSelectedServer())) - refreshServerStatus() - initNews() - syncModConfigurations(data) -} - -/** - * Sync the mod configurations with the distro index. - * - * @param {Object} data The distro index object. - */ -function syncModConfigurations(data){ - - const syncedCfgs = [] - - for(let serv of data.getServers()){ - - const id = serv.getID() - const mdls = serv.getModules() - const cfg = ConfigManager.getModConfiguration(id) - - if(cfg != null){ - - const modsOld = cfg.mods - const mods = {} - - for(let mdl of mdls){ - const type = mdl.getType() - - if(type === DistroManager.Types.ForgeMod || type === DistroManager.Types.LiteMod || type === DistroManager.Types.LiteLoader){ - if(!mdl.getRequired().isRequired()){ - const mdlID = mdl.getVersionlessID() - if(modsOld[mdlID] == null){ - mods[mdlID] = scanOptionalSubModules(mdl.getSubModules(), mdl) - } else { - mods[mdlID] = mergeModConfiguration(modsOld[mdlID], scanOptionalSubModules(mdl.getSubModules(), mdl), false) - } - } else { - if(mdl.hasSubModules()){ - const mdlID = mdl.getVersionlessID() - const v = scanOptionalSubModules(mdl.getSubModules(), mdl) - if(typeof v === 'object'){ - if(modsOld[mdlID] == null){ - mods[mdlID] = v - } else { - mods[mdlID] = mergeModConfiguration(modsOld[mdlID], v, true) - } - } - } - } - } - } - - syncedCfgs.push({ - id, - mods - }) - - } else { - - const mods = {} - - for(let mdl of mdls){ - const type = mdl.getType() - if(type === DistroManager.Types.ForgeMod || type === DistroManager.Types.LiteMod || type === DistroManager.Types.LiteLoader){ - if(!mdl.getRequired().isRequired()){ - mods[mdl.getVersionlessID()] = scanOptionalSubModules(mdl.getSubModules(), mdl) - } else { - if(mdl.hasSubModules()){ - const v = scanOptionalSubModules(mdl.getSubModules(), mdl) - if(typeof v === 'object'){ - mods[mdl.getVersionlessID()] = v - } - } - } - } - } - - syncedCfgs.push({ - id, - mods - }) - - } - } - - ConfigManager.setModConfigurations(syncedCfgs) - ConfigManager.save() -} - -/** - * Recursively scan for optional sub modules. If none are found, - * this function returns a boolean. If optional sub modules do exist, - * a recursive configuration object is returned. - * - * @returns {boolean | Object} The resolved mod configuration. - */ -function scanOptionalSubModules(mdls, origin){ - if(mdls != null){ - const mods = {} - - for(let mdl of mdls){ - const type = mdl.getType() - // Optional types. - if(type === DistroManager.Types.ForgeMod || type === DistroManager.Types.LiteMod || type === DistroManager.Types.LiteLoader){ - // It is optional. - if(!mdl.getRequired().isRequired()){ - mods[mdl.getVersionlessID()] = scanOptionalSubModules(mdl.getSubModules(), mdl) - } else { - if(mdl.hasSubModules()){ - const v = scanOptionalSubModules(mdl.getSubModules(), mdl) - if(typeof v === 'object'){ - mods[mdl.getVersionlessID()] = v - } - } - } - } - } - - if(Object.keys(mods).length > 0){ - const ret = { - mods - } - if(!origin.getRequired().isRequired()){ - ret.value = origin.getRequired().isDefault() - } - return ret - } - } - return origin.getRequired().isDefault() -} - -/** - * Recursively merge an old configuration into a new configuration. - * - * @param {boolean | Object} o The old configuration value. - * @param {boolean | Object} n The new configuration value. - * @param {boolean} nReq If the new value is a required mod. - * - * @returns {boolean | Object} The merged configuration. - */ -function mergeModConfiguration(o, n, nReq = false){ - if(typeof o === 'boolean'){ - if(typeof n === 'boolean') return o - else if(typeof n === 'object'){ - if(!nReq){ - n.value = o - } - return n - } - } else if(typeof o === 'object'){ - if(typeof n === 'boolean') return typeof o.value !== 'undefined' ? o.value : true - else if(typeof n === 'object'){ - if(!nReq){ - n.value = typeof o.value !== 'undefined' ? o.value : true - } - - const newMods = Object.keys(n.mods) - for(let i=0; i${selectedAcc.displayName}. Please ${accLen > 0 ? 'select another account or ' : ''} login again.`, - 'Login', - 'Select Another Account' - ) - setOverlayHandler(() => { - document.getElementById('loginUsername').value = selectedAcc.username - validateEmail(selectedAcc.username) - loginViewOnSuccess = getCurrentView() - loginViewOnCancel = getCurrentView() - if(accLen > 0){ - loginViewCancelHandler = () => { - ConfigManager.addAuthAccount(selectedAcc.uuid, selectedAcc.accessToken, selectedAcc.username, selectedAcc.displayName) - ConfigManager.save() - validateSelectedAccount() - } - loginCancelEnabled(true) - } - toggleOverlay(false) - switchView(getCurrentView(), VIEWS.login) - }) - setDismissHandler(() => { - if(accLen > 1){ - prepareAccountSelectionList() - $('#overlayContent').fadeOut(250, () => { - bindOverlayKeys(true, 'accountSelectContent', true) - $('#accountSelectContent').fadeIn(250) - }) - } else { - const accountsObj = ConfigManager.getAuthAccounts() - const accounts = Array.from(Object.keys(accountsObj), v => accountsObj[v]) - // This function validates the account switch. - setSelectedAccount(accounts[0].uuid) - toggleOverlay(false) - } - }) - toggleOverlay(true, accLen > 0) - } else { - return true - } - } else { - return true - } -} - -/** - * Temporary function to update the selected account along - * with the relevent UI elements. - * - * @param {string} uuid The UUID of the account. - */ -function setSelectedAccount(uuid){ - const authAcc = ConfigManager.setSelectedAccount(uuid) - ConfigManager.save() - updateSelectedAccount(authAcc) - validateSelectedAccount() -} - -// Synchronous Listener -document.addEventListener('readystatechange', function(){ - - if (document.readyState === 'interactive' || document.readyState === 'complete'){ - if(rscShouldLoad){ - rscShouldLoad = false - if(!fatalStartupError){ - const data = DistroManager.getDistribution() - showMainUI(data) - } else { - showFatalStartupError() - } - } - } - -}, false) - -// Actions that must be performed after the distribution index is downloaded. -ipcRenderer.on('distributionIndexDone', (event, res) => { - if(res) { - const data = DistroManager.getDistribution() - syncModConfigurations(data) - if(document.readyState === 'interactive' || document.readyState === 'complete'){ - showMainUI(data) - } else { - rscShouldLoad = true - } - } else { - fatalStartupError = true - if(document.readyState === 'interactive' || document.readyState === 'complete'){ - showFatalStartupError() - } else { - rscShouldLoad = true - } - } -}) +/** + * Initialize UI functions which depend on internal modules. + * Loaded after core UI functions are initialized in uicore.js. + */ +// Requirements +const path = require('path') + +const AuthManager = require('./assets/js/authmanager') +const ConfigManager = require('./assets/js/configmanager') +const DistroManager = require('./assets/js/distromanager') +const Lang = require('./assets/js/langloader') + +let rscShouldLoad = false +let fatalStartupError = false + +// Mapping of each view to their container IDs. +const VIEWS = { + landing: '#landingContainer', + login: '#loginContainer', + settings: '#settingsContainer', + welcome: '#welcomeContainer' +} + +// The currently shown view container. +let currentView + +/** + * Switch launcher views. + * + * @param {string} current The ID of the current view container. + * @param {*} next The ID of the next view container. + * @param {*} currentFadeTime Optional. The fade out time for the current view. + * @param {*} nextFadeTime Optional. The fade in time for the next view. + * @param {*} onCurrentFade Optional. Callback function to execute when the current + * view fades out. + * @param {*} onNextFade Optional. Callback function to execute when the next view + * fades in. + */ +function switchView(current, next, currentFadeTime = 500, nextFadeTime = 500, onCurrentFade = () => {}, onNextFade = () => {}){ + currentView = next + $(`${current}`).fadeOut(currentFadeTime, () => { + onCurrentFade() + $(`${next}`).fadeIn(nextFadeTime, () => { + onNextFade() + }) + }) +} + +/** + * Get the currently shown view container. + * + * @returns {string} The currently shown view container. + */ +function getCurrentView(){ + return currentView +} + +function showMainUI(data){ + + if(!isDev){ + loggerAutoUpdater.log('Initializing..') + ipcRenderer.send('autoUpdateAction', 'initAutoUpdater', ConfigManager.getAllowPrerelease()) + } + + prepareSettings(true) + updateSelectedServer(data.getServer(ConfigManager.getSelectedServer())) + refreshServerStatus() + setTimeout(() => { + document.getElementById('frameBar').style.backgroundColor = 'rgba(0, 0, 0, 0.5)' + document.body.style.backgroundImage = `url('assets/images/backgrounds/${document.body.getAttribute('bkid')}.jpg')` + $('#main').show() + + const isLoggedIn = Object.keys(ConfigManager.getAuthAccounts()).length > 0 + + // If this is enabled in a development environment we'll get ratelimited. + // The relaunch frequency is usually far too high. + if(!isDev && isLoggedIn){ + validateSelectedAccount() + } + + if(ConfigManager.isFirstLaunch()){ + currentView = VIEWS.welcome + $(VIEWS.welcome).fadeIn(1000) + } else { + if(isLoggedIn){ + currentView = VIEWS.landing + $(VIEWS.landing).fadeIn(1000) + } else { + currentView = VIEWS.login + $(VIEWS.login).fadeIn(1000) + } + } + + setTimeout(() => { + $('#loadingContainer').fadeOut(500, () => { + $('#loadSpinnerImage').removeClass('rotating') + }) + }, 250) + + }, 750) + // Disable tabbing to the news container. + initNews().then(() => { + $('#newsContainer *').attr('tabindex', '-1') + }) +} + +function showFatalStartupError(){ + setTimeout(() => { + $('#loadingContainer').fadeOut(250, () => { + document.getElementById('overlayContainer').style.background = 'none' + setOverlayContent( + 'Fatal Error: Unable to Load Distribution Index', + 'A connection could not be established to our servers to download the distribution index. No local copies were available to load.

The distribution index is an essential file which provides the latest server information. The launcher is unable to start without it. Ensure you are connected to the internet and relaunch the application.', + 'Close' + ) + setOverlayHandler(() => { + const window = remote.getCurrentWindow() + window.close() + }) + toggleOverlay(true) + }) + }, 750) +} + +/** + * Common functions to perform after refreshing the distro index. + * + * @param {Object} data The distro index object. + */ +function onDistroRefresh(data){ + updateSelectedServer(data.getServer(ConfigManager.getSelectedServer())) + refreshServerStatus() + initNews() + syncModConfigurations(data) +} + +/** + * Sync the mod configurations with the distro index. + * + * @param {Object} data The distro index object. + */ +function syncModConfigurations(data){ + + const syncedCfgs = [] + + for(let serv of data.getServers()){ + + const id = serv.getID() + const mdls = serv.getModules() + const cfg = ConfigManager.getModConfiguration(id) + + if(cfg != null){ + + const modsOld = cfg.mods + const mods = {} + + for(let mdl of mdls){ + const type = mdl.getType() + + if(type === DistroManager.Types.ForgeMod || type === DistroManager.Types.LiteMod || type === DistroManager.Types.LiteLoader){ + if(!mdl.getRequired().isRequired()){ + const mdlID = mdl.getVersionlessID() + if(modsOld[mdlID] == null){ + mods[mdlID] = scanOptionalSubModules(mdl.getSubModules(), mdl) + } else { + mods[mdlID] = mergeModConfiguration(modsOld[mdlID], scanOptionalSubModules(mdl.getSubModules(), mdl), false) + } + } else { + if(mdl.hasSubModules()){ + const mdlID = mdl.getVersionlessID() + const v = scanOptionalSubModules(mdl.getSubModules(), mdl) + if(typeof v === 'object'){ + if(modsOld[mdlID] == null){ + mods[mdlID] = v + } else { + mods[mdlID] = mergeModConfiguration(modsOld[mdlID], v, true) + } + } + } + } + } + } + + syncedCfgs.push({ + id, + mods + }) + + } else { + + const mods = {} + + for(let mdl of mdls){ + const type = mdl.getType() + if(type === DistroManager.Types.ForgeMod || type === DistroManager.Types.LiteMod || type === DistroManager.Types.LiteLoader){ + if(!mdl.getRequired().isRequired()){ + mods[mdl.getVersionlessID()] = scanOptionalSubModules(mdl.getSubModules(), mdl) + } else { + if(mdl.hasSubModules()){ + const v = scanOptionalSubModules(mdl.getSubModules(), mdl) + if(typeof v === 'object'){ + mods[mdl.getVersionlessID()] = v + } + } + } + } + } + + syncedCfgs.push({ + id, + mods + }) + + } + } + + ConfigManager.setModConfigurations(syncedCfgs) + ConfigManager.save() +} + +/** + * Recursively scan for optional sub modules. If none are found, + * this function returns a boolean. If optional sub modules do exist, + * a recursive configuration object is returned. + * + * @returns {boolean | Object} The resolved mod configuration. + */ +function scanOptionalSubModules(mdls, origin){ + if(mdls != null){ + const mods = {} + + for(let mdl of mdls){ + const type = mdl.getType() + // Optional types. + if(type === DistroManager.Types.ForgeMod || type === DistroManager.Types.LiteMod || type === DistroManager.Types.LiteLoader){ + // It is optional. + if(!mdl.getRequired().isRequired()){ + mods[mdl.getVersionlessID()] = scanOptionalSubModules(mdl.getSubModules(), mdl) + } else { + if(mdl.hasSubModules()){ + const v = scanOptionalSubModules(mdl.getSubModules(), mdl) + if(typeof v === 'object'){ + mods[mdl.getVersionlessID()] = v + } + } + } + } + } + + if(Object.keys(mods).length > 0){ + const ret = { + mods + } + if(!origin.getRequired().isRequired()){ + ret.value = origin.getRequired().isDefault() + } + return ret + } + } + return origin.getRequired().isDefault() +} + +/** + * Recursively merge an old configuration into a new configuration. + * + * @param {boolean | Object} o The old configuration value. + * @param {boolean | Object} n The new configuration value. + * @param {boolean} nReq If the new value is a required mod. + * + * @returns {boolean | Object} The merged configuration. + */ +function mergeModConfiguration(o, n, nReq = false){ + if(typeof o === 'boolean'){ + if(typeof n === 'boolean') return o + else if(typeof n === 'object'){ + if(!nReq){ + n.value = o + } + return n + } + } else if(typeof o === 'object'){ + if(typeof n === 'boolean') return typeof o.value !== 'undefined' ? o.value : true + else if(typeof n === 'object'){ + if(!nReq){ + n.value = typeof o.value !== 'undefined' ? o.value : true + } + + const newMods = Object.keys(n.mods) + for(let i=0; i${selectedAcc.displayName}. Please ${accLen > 0 ? 'select another account or ' : ''} login again.`, + 'Login', + 'Select Another Account' + ) + setOverlayHandler(() => { + document.getElementById('loginUsername').value = selectedAcc.username + validateEmail(selectedAcc.username) + loginViewOnSuccess = getCurrentView() + loginViewOnCancel = getCurrentView() + if(accLen > 0){ + loginViewCancelHandler = () => { + ConfigManager.addAuthAccount(selectedAcc.uuid, selectedAcc.accessToken, selectedAcc.username, selectedAcc.displayName) + ConfigManager.save() + validateSelectedAccount() + } + loginCancelEnabled(true) + } + toggleOverlay(false) + switchView(getCurrentView(), VIEWS.login) + }) + setDismissHandler(() => { + if(accLen > 1){ + prepareAccountSelectionList() + $('#overlayContent').fadeOut(250, () => { + bindOverlayKeys(true, 'accountSelectContent', true) + $('#accountSelectContent').fadeIn(250) + }) + } else { + const accountsObj = ConfigManager.getAuthAccounts() + const accounts = Array.from(Object.keys(accountsObj), v => accountsObj[v]) + // This function validates the account switch. + setSelectedAccount(accounts[0].uuid) + toggleOverlay(false) + } + }) + toggleOverlay(true, accLen > 0) + } else { + return true + } + } else { + return true + } +} + +/** + * Temporary function to update the selected account along + * with the relevent UI elements. + * + * @param {string} uuid The UUID of the account. + */ +function setSelectedAccount(uuid){ + const authAcc = ConfigManager.setSelectedAccount(uuid) + ConfigManager.save() + updateSelectedAccount(authAcc) + validateSelectedAccount() +} + +// Synchronous Listener +document.addEventListener('readystatechange', function(){ + + if (document.readyState === 'interactive' || document.readyState === 'complete'){ + if(rscShouldLoad){ + rscShouldLoad = false + if(!fatalStartupError){ + const data = DistroManager.getDistribution() + showMainUI(data) + } else { + showFatalStartupError() + } + } + } + +}, false) + +// Actions that must be performed after the distribution index is downloaded. +ipcRenderer.on('distributionIndexDone', (event, res) => { + if(res) { + const data = DistroManager.getDistribution() + syncModConfigurations(data) + if(document.readyState === 'interactive' || document.readyState === 'complete'){ + showMainUI(data) + } else { + rscShouldLoad = true + } + } else { + fatalStartupError = true + if(document.readyState === 'interactive' || document.readyState === 'complete'){ + showFatalStartupError() + } else { + rscShouldLoad = true + } + } +}) diff --git a/app/assets/js/scripts/uicore.js b/app/assets/js/scripts/uicore.js index 73e372aa..5f2b9be6 100644 --- a/app/assets/js/scripts/uicore.js +++ b/app/assets/js/scripts/uicore.js @@ -1,213 +1,213 @@ -/** - * Core UI functions are initialized in this file. This prevents - * unexpected errors from breaking the core features. Specifically, - * actions in this file should not require the usage of any internal - * modules, excluding dependencies. - */ -// Requirements -const $ = require('jquery') -const {ipcRenderer, remote, shell, webFrame} = require('electron') -const isDev = require('./assets/js/isdev') -const LoggerUtil = require('./assets/js/loggerutil') - -const loggerUICore = LoggerUtil('%c[UICore]', 'color: #000668; font-weight: bold') -const loggerAutoUpdater = LoggerUtil('%c[AutoUpdater]', 'color: #000668; font-weight: bold') -const loggerAutoUpdaterSuccess = LoggerUtil('%c[AutoUpdater]', 'color: #209b07; font-weight: bold') - -// Log deprecation and process warnings. -process.traceProcessWarnings = true -process.traceDeprecation = true - -// Disable eval function. -// eslint-disable-next-line -window.eval = global.eval = function () { - throw new Error('Sorry, this app does not support window.eval().') -} - -// Display warning when devtools window is opened. -remote.getCurrentWebContents().on('devtools-opened', () => { - console.log('%cThe console is dark and full of terrors.', 'color: white; -webkit-text-stroke: 4px #a02d2a; font-size: 60px; font-weight: bold') - console.log('%cIf you\'ve been told to paste something here, you\'re being scammed.', 'font-size: 16px') - console.log('%cUnless you know exactly what you\'re doing, close this window.', 'font-size: 16px') -}) - -// Disable zoom, needed for darwin. -webFrame.setZoomLevel(0) -webFrame.setVisualZoomLevelLimits(1, 1) - -// Initialize auto updates in production environments. -let updateCheckListener -if(!isDev){ - ipcRenderer.on('autoUpdateNotification', (event, arg, info) => { - switch(arg){ - case 'checking-for-update': - loggerAutoUpdater.log('Checking for update..') - settingsUpdateButtonStatus('Checking for Updates..', true) - break - case 'update-available': - loggerAutoUpdaterSuccess.log('New update available', info.version) - - if(process.platform === 'darwin'){ - info.darwindownload = `https://github.com/dscalzi/HeliosLauncher/releases/download/v${info.version}/helioslauncher-setup-${info.version}.dmg` - showUpdateUI(info) - } - - populateSettingsUpdateInformation(info) - break - case 'update-downloaded': - loggerAutoUpdaterSuccess.log('Update ' + info.version + ' ready to be installed.') - settingsUpdateButtonStatus('Install Now', false, () => { - if(!isDev){ - ipcRenderer.send('autoUpdateAction', 'installUpdateNow') - } - }) - showUpdateUI(info) - break - case 'update-not-available': - loggerAutoUpdater.log('No new update found.') - settingsUpdateButtonStatus('Check for Updates') - break - case 'ready': - updateCheckListener = setInterval(() => { - ipcRenderer.send('autoUpdateAction', 'checkForUpdate') - }, 1800000) - ipcRenderer.send('autoUpdateAction', 'checkForUpdate') - break - case 'realerror': - if(info != null && info.code != null){ - if(info.code === 'ERR_UPDATER_INVALID_RELEASE_FEED'){ - loggerAutoUpdater.log('No suitable releases found.') - } else if(info.code === 'ERR_XML_MISSED_ELEMENT'){ - loggerAutoUpdater.log('No releases found.') - } else { - loggerAutoUpdater.error('Error during update check..', info) - loggerAutoUpdater.debug('Error Code:', info.code) - } - } - break - default: - loggerAutoUpdater.log('Unknown argument', arg) - break - } - }) -} - -/** - * Send a notification to the main process changing the value of - * allowPrerelease. If we are running a prerelease version, then - * this will always be set to true, regardless of the current value - * of val. - * - * @param {boolean} val The new allow prerelease value. - */ -function changeAllowPrerelease(val){ - ipcRenderer.send('autoUpdateAction', 'allowPrereleaseChange', val) -} - -function showUpdateUI(info){ - //TODO Make this message a bit more informative `${info.version}` - document.getElementById('image_seal_container').setAttribute('update', true) - document.getElementById('image_seal_container').onclick = () => { - /*setOverlayContent('Update Available', 'A new update for the launcher is available. Would you like to install now?', 'Install', 'Later') - setOverlayHandler(() => { - if(!isDev){ - ipcRenderer.send('autoUpdateAction', 'installUpdateNow') - } else { - console.error('Cannot install updates in development environment.') - toggleOverlay(false) - } - }) - setDismissHandler(() => { - toggleOverlay(false) - }) - toggleOverlay(true, true)*/ - switchView(getCurrentView(), VIEWS.settings, 500, 500, () => { - settingsNavItemListener(document.getElementById('settingsNavUpdate'), false) - }) - } -} - -/* jQuery Example -$(function(){ - loggerUICore.log('UICore Initialized'); -})*/ - -document.addEventListener('readystatechange', function () { - if (document.readyState === 'interactive'){ - loggerUICore.log('UICore Initializing..') - - // Bind close button. - Array.from(document.getElementsByClassName('fCb')).map((val) => { - val.addEventListener('click', e => { - const window = remote.getCurrentWindow() - window.close() - }) - }) - - // Bind restore down button. - Array.from(document.getElementsByClassName('fRb')).map((val) => { - val.addEventListener('click', e => { - const window = remote.getCurrentWindow() - if(window.isMaximized()){ - window.unmaximize() - } else { - window.maximize() - } - document.activeElement.blur() - }) - }) - - // Bind minimize button. - Array.from(document.getElementsByClassName('fMb')).map((val) => { - val.addEventListener('click', e => { - const window = remote.getCurrentWindow() - window.minimize() - document.activeElement.blur() - }) - }) - - // Remove focus from social media buttons once they're clicked. - Array.from(document.getElementsByClassName('mediaURL')).map(val => { - val.addEventListener('click', e => { - document.activeElement.blur() - }) - }) - - } else if(document.readyState === 'complete'){ - - //266.01 - //170.8 - //53.21 - // Bind progress bar length to length of bot wrapper - //const targetWidth = document.getElementById("launch_content").getBoundingClientRect().width - //const targetWidth2 = document.getElementById("server_selection").getBoundingClientRect().width - //const targetWidth3 = document.getElementById("launch_button").getBoundingClientRect().width - - document.getElementById('launch_details').style.maxWidth = 266.01 - document.getElementById('launch_progress').style.width = 170.8 - document.getElementById('launch_details_right').style.maxWidth = 170.8 - document.getElementById('launch_progress_label').style.width = 53.21 - - } - -}, false) - -/** - * Open web links in the user's default browser. - */ -$(document).on('click', 'a[href^="http"]', function(event) { - event.preventDefault() - shell.openExternal(this.href) -}) - -/** - * Opens DevTools window if you hold (ctrl + shift + i). - * This will crash the program if you are using multiple - * DevTools, for example the chrome debugger in VS Code. - */ -document.addEventListener('keydown', function (e) { - if((e.key === 'I' || e.key === 'i') && e.ctrlKey && e.shiftKey){ - let window = remote.getCurrentWindow() - window.toggleDevTools() - } +/** + * Core UI functions are initialized in this file. This prevents + * unexpected errors from breaking the core features. Specifically, + * actions in this file should not require the usage of any internal + * modules, excluding dependencies. + */ +// Requirements +const $ = require('jquery') +const {ipcRenderer, remote, shell, webFrame} = require('electron') +const isDev = require('./assets/js/isdev') +const LoggerUtil = require('./assets/js/loggerutil') + +const loggerUICore = LoggerUtil('%c[UICore]', 'color: #000668; font-weight: bold') +const loggerAutoUpdater = LoggerUtil('%c[AutoUpdater]', 'color: #000668; font-weight: bold') +const loggerAutoUpdaterSuccess = LoggerUtil('%c[AutoUpdater]', 'color: #209b07; font-weight: bold') + +// Log deprecation and process warnings. +process.traceProcessWarnings = true +process.traceDeprecation = true + +// Disable eval function. +// eslint-disable-next-line +window.eval = global.eval = function () { + throw new Error('Sorry, this app does not support window.eval().') +} + +// Display warning when devtools window is opened. +remote.getCurrentWebContents().on('devtools-opened', () => { + console.log('%cThe console is dark and full of terrors.', 'color: white; -webkit-text-stroke: 4px #a02d2a; font-size: 60px; font-weight: bold') + console.log('%cIf you\'ve been told to paste something here, you\'re being scammed.', 'font-size: 16px') + console.log('%cUnless you know exactly what you\'re doing, close this window.', 'font-size: 16px') +}) + +// Disable zoom, needed for darwin. +webFrame.setZoomLevel(0) +webFrame.setVisualZoomLevelLimits(1, 1) + +// Initialize auto updates in production environments. +let updateCheckListener +if(!isDev){ + ipcRenderer.on('autoUpdateNotification', (event, arg, info) => { + switch(arg){ + case 'checking-for-update': + loggerAutoUpdater.log('Checking for update..') + settingsUpdateButtonStatus('Checking for Updates..', true) + break + case 'update-available': + loggerAutoUpdaterSuccess.log('New update available', info.version) + + if(process.platform === 'darwin'){ + info.darwindownload = `https://github.com/dscalzi/HeliosLauncher/releases/download/v${info.version}/helioslauncher-setup-${info.version}.dmg` + showUpdateUI(info) + } + + populateSettingsUpdateInformation(info) + break + case 'update-downloaded': + loggerAutoUpdaterSuccess.log('Update ' + info.version + ' ready to be installed.') + settingsUpdateButtonStatus('Install Now', false, () => { + if(!isDev){ + ipcRenderer.send('autoUpdateAction', 'installUpdateNow') + } + }) + showUpdateUI(info) + break + case 'update-not-available': + loggerAutoUpdater.log('No new update found.') + settingsUpdateButtonStatus('Check for Updates') + break + case 'ready': + updateCheckListener = setInterval(() => { + ipcRenderer.send('autoUpdateAction', 'checkForUpdate') + }, 1800000) + ipcRenderer.send('autoUpdateAction', 'checkForUpdate') + break + case 'realerror': + if(info != null && info.code != null){ + if(info.code === 'ERR_UPDATER_INVALID_RELEASE_FEED'){ + loggerAutoUpdater.log('No suitable releases found.') + } else if(info.code === 'ERR_XML_MISSED_ELEMENT'){ + loggerAutoUpdater.log('No releases found.') + } else { + loggerAutoUpdater.error('Error during update check..', info) + loggerAutoUpdater.debug('Error Code:', info.code) + } + } + break + default: + loggerAutoUpdater.log('Unknown argument', arg) + break + } + }) +} + +/** + * Send a notification to the main process changing the value of + * allowPrerelease. If we are running a prerelease version, then + * this will always be set to true, regardless of the current value + * of val. + * + * @param {boolean} val The new allow prerelease value. + */ +function changeAllowPrerelease(val){ + ipcRenderer.send('autoUpdateAction', 'allowPrereleaseChange', val) +} + +function showUpdateUI(info){ + //TODO Make this message a bit more informative `${info.version}` + document.getElementById('image_seal_container').setAttribute('update', true) + document.getElementById('image_seal_container').onclick = () => { + /*setOverlayContent('Update Available', 'A new update for the launcher is available. Would you like to install now?', 'Install', 'Later') + setOverlayHandler(() => { + if(!isDev){ + ipcRenderer.send('autoUpdateAction', 'installUpdateNow') + } else { + console.error('Cannot install updates in development environment.') + toggleOverlay(false) + } + }) + setDismissHandler(() => { + toggleOverlay(false) + }) + toggleOverlay(true, true)*/ + switchView(getCurrentView(), VIEWS.settings, 500, 500, () => { + settingsNavItemListener(document.getElementById('settingsNavUpdate'), false) + }) + } +} + +/* jQuery Example +$(function(){ + loggerUICore.log('UICore Initialized'); +})*/ + +document.addEventListener('readystatechange', function () { + if (document.readyState === 'interactive'){ + loggerUICore.log('UICore Initializing..') + + // Bind close button. + Array.from(document.getElementsByClassName('fCb')).map((val) => { + val.addEventListener('click', e => { + const window = remote.getCurrentWindow() + window.close() + }) + }) + + // Bind restore down button. + Array.from(document.getElementsByClassName('fRb')).map((val) => { + val.addEventListener('click', e => { + const window = remote.getCurrentWindow() + if(window.isMaximized()){ + window.unmaximize() + } else { + window.maximize() + } + document.activeElement.blur() + }) + }) + + // Bind minimize button. + Array.from(document.getElementsByClassName('fMb')).map((val) => { + val.addEventListener('click', e => { + const window = remote.getCurrentWindow() + window.minimize() + document.activeElement.blur() + }) + }) + + // Remove focus from social media buttons once they're clicked. + Array.from(document.getElementsByClassName('mediaURL')).map(val => { + val.addEventListener('click', e => { + document.activeElement.blur() + }) + }) + + } else if(document.readyState === 'complete'){ + + //266.01 + //170.8 + //53.21 + // Bind progress bar length to length of bot wrapper + //const targetWidth = document.getElementById("launch_content").getBoundingClientRect().width + //const targetWidth2 = document.getElementById("server_selection").getBoundingClientRect().width + //const targetWidth3 = document.getElementById("launch_button").getBoundingClientRect().width + + document.getElementById('launch_details').style.maxWidth = 266.01 + document.getElementById('launch_progress').style.width = 170.8 + document.getElementById('launch_details_right').style.maxWidth = 170.8 + document.getElementById('launch_progress_label').style.width = 53.21 + + } + +}, false) + +/** + * Open web links in the user's default browser. + */ +$(document).on('click', 'a[href^="http"]', function(event) { + event.preventDefault() + shell.openExternal(this.href) +}) + +/** + * Opens DevTools window if you hold (ctrl + shift + i). + * This will crash the program if you are using multiple + * DevTools, for example the chrome debugger in VS Code. + */ +document.addEventListener('keydown', function (e) { + if((e.key === 'I' || e.key === 'i') && e.ctrlKey && e.shiftKey){ + let window = remote.getCurrentWindow() + window.toggleDevTools() + } }) \ No newline at end of file diff --git a/app/assets/js/scripts/welcome.js b/app/assets/js/scripts/welcome.js index e6ff6297..e430829a 100644 --- a/app/assets/js/scripts/welcome.js +++ b/app/assets/js/scripts/welcome.js @@ -1,6 +1,6 @@ -/** - * Script for welcome.ejs - */ -document.getElementById('welcomeButton').addEventListener('click', e => { - switchView(VIEWS.welcome, VIEWS.login) +/** + * Script for welcome.ejs + */ +document.getElementById('welcomeButton').addEventListener('click', e => { + switchView(VIEWS.welcome, VIEWS.login) }) \ No newline at end of file diff --git a/app/assets/js/serverstatus.js b/app/assets/js/serverstatus.js index 9729f9c7..bf4e1c44 100644 --- a/app/assets/js/serverstatus.js +++ b/app/assets/js/serverstatus.js @@ -1,65 +1,65 @@ -const net = require('net') - -/** - * Retrieves the status of a minecraft server. - * - * @param {string} address The server address. - * @param {number} port Optional. The port of the server. Defaults to 25565. - * @returns {Promise.} A promise which resolves to an object containing - * status information. - */ -exports.getStatus = function(address, port = 25565){ - - if(port == null || port == ''){ - port = 25565 - } - if(typeof port === 'string'){ - port = parseInt(port) - } - - return new Promise((resolve, reject) => { - const socket = net.connect(port, address, () => { - let buff = Buffer.from([0xFE, 0x01]) - socket.write(buff) - }) - - socket.setTimeout(2500, () => { - socket.end() - reject({ - code: 'ETIMEDOUT', - errno: 'ETIMEDOUT', - address, - port - }) - }) - - socket.on('data', (data) => { - if(data != null && data != ''){ - let server_info = data.toString().split('\x00\x00\x00') - const NUM_FIELDS = 6 - if(server_info != null && server_info.length >= NUM_FIELDS){ - resolve({ - online: true, - version: server_info[2].replace(/\u0000/g, ''), - motd: server_info[3].replace(/\u0000/g, ''), - onlinePlayers: server_info[4].replace(/\u0000/g, ''), - maxPlayers: server_info[5].replace(/\u0000/g,'') - }) - } else { - resolve({ - online: false - }) - } - } - socket.end() - }) - - socket.on('error', (err) => { - socket.destroy() - reject(err) - // ENOTFOUND = Unable to resolve. - // ECONNREFUSED = Unable to connect to port. - }) - }) - +const net = require('net') + +/** + * Retrieves the status of a minecraft server. + * + * @param {string} address The server address. + * @param {number} port Optional. The port of the server. Defaults to 25565. + * @returns {Promise.} A promise which resolves to an object containing + * status information. + */ +exports.getStatus = function(address, port = 25565){ + + if(port == null || port == ''){ + port = 25565 + } + if(typeof port === 'string'){ + port = parseInt(port) + } + + return new Promise((resolve, reject) => { + const socket = net.connect(port, address, () => { + let buff = Buffer.from([0xFE, 0x01]) + socket.write(buff) + }) + + socket.setTimeout(2500, () => { + socket.end() + reject({ + code: 'ETIMEDOUT', + errno: 'ETIMEDOUT', + address, + port + }) + }) + + socket.on('data', (data) => { + if(data != null && data != ''){ + let server_info = data.toString().split('\x00\x00\x00') + const NUM_FIELDS = 6 + if(server_info != null && server_info.length >= NUM_FIELDS){ + resolve({ + online: true, + version: server_info[2].replace(/\u0000/g, ''), + motd: server_info[3].replace(/\u0000/g, ''), + onlinePlayers: server_info[4].replace(/\u0000/g, ''), + maxPlayers: server_info[5].replace(/\u0000/g,'') + }) + } else { + resolve({ + online: false + }) + } + } + socket.end() + }) + + socket.on('error', (err) => { + socket.destroy() + reject(err) + // ENOTFOUND = Unable to resolve. + // ECONNREFUSED = Unable to connect to port. + }) + }) + } \ No newline at end of file diff --git a/app/assets/lang/en_US.json b/app/assets/lang/en_US.json index 25b34c24..9aac94d4 100644 --- a/app/assets/lang/en_US.json +++ b/app/assets/lang/en_US.json @@ -1,49 +1,49 @@ -{ - "html": { - "avatarOverlay": "Edit" - }, - "js": { - "login": { - "error": { - "invalidValue": "* Invalid Value", - "requiredValue": "* Required", - "userMigrated": { - "title": "Error During Login:
Invalid Credentials", - "desc": "You've attempted to login with a migrated account. Try again using the account email as the username." - }, - "invalidCredentials": { - "title": "Error During Login:
Invalid Credentials", - "desc": "The email or password you've entered is incorrect. Please try again." - }, - "rateLimit": { - "title": "Error During Login:
Too Many Attempts", - "desc": "There have been too many login attempts with this account recently. Please try again later." - }, - "noInternet": { - "title": "Error During Login:
No Internet Connection", - "desc": "You must be connected to the internet in order to login. Please connect and try again." - }, - "authDown": { - "title": "Error During Login:
Authentication Server Offline", - "desc": "Mojang's authentication server is currently offline or unreachable. Please wait a bit and try again. You can check the status of the server on Mojang's help portal." - }, - "notPaid": { - "title": "Error During Login:
Game Not Purchased", - "desc": "The account you are trying to login with has not purchased a copy of Minecraft.
You may purchase a copy on Minecraft.net" - }, - "unknown": { - "title": "Error During Login:
Unknown Error" - } - }, - "login": "LOGIN", - "loggingIn": "LOGGING IN", - "success": "SUCCESS", - "tryAgain": "Try Again" - }, - "landing": { - "launch": { - "pleaseWait": "Please wait.." - } - } - } +{ + "html": { + "avatarOverlay": "Modifer" + }, + "js": { + "login": { + "error": { + "invalidValue": "* Invalid Value", + "requiredValue": "* Required", + "userMigrated": { + "title": "Error During Login:
Invalid Credentials", + "desc": "You've attempted to login with a migrated account. Try again using the account email as the username." + }, + "invalidCredentials": { + "title": "Error During Login:
Invalid Credentials", + "desc": "The email or password you've entered is incorrect. Please try again." + }, + "rateLimit": { + "title": "Error During Login:
Too Many Attempts", + "desc": "There have been too many login attempts with this account recently. Please try again later." + }, + "noInternet": { + "title": "Error During Login:
No Internet Connection", + "desc": "You must be connected to the internet in order to login. Please connect and try again." + }, + "authDown": { + "title": "Error During Login:
Authentication Server Offline", + "desc": "Mojang's authentication server is currently offline or unreachable. Please wait a bit and try again. You can check the status of the server on Mojang's help portal." + }, + "notPaid": { + "title": "Error During Login:
Game Not Purchased", + "desc": "The account you are trying to login with has not purchased a copy of Minecraft.
You may purchase a copy on Minecraft.net" + }, + "unknown": { + "title": "Error During Login:
Unknown Error" + } + }, + "login": "LOGIN", + "loggingIn": "LOGGING IN", + "success": "SUCCESS", + "tryAgain": "Try Again" + }, + "landing": { + "launch": { + "pleaseWait": "Please wait.." + } + } + } } \ No newline at end of file diff --git a/app/frame.ejs b/app/frame.ejs index c2aaf337..cfada839 100644 --- a/app/frame.ejs +++ b/app/frame.ejs @@ -1,33 +1,33 @@ -
-
-
-
- <%if (process.platform === 'darwin') { %> -
-
- - - -
-
- <% } else{ %> -
-
- Helios Launcher -
-
- - - -
-
- <% } %> -
-
+
+
+
+
+ <%if (process.platform === 'darwin') { %> +
+
+ + + +
+
+ <% } else{ %> +
+
+ Launcher Creeponnia +
+
+ + + +
+
+ <% } %> +
+
\ No newline at end of file diff --git a/app/landing.ejs b/app/landing.ejs index 2522200d..b480fdc3 100644 --- a/app/landing.ejs +++ b/app/landing.ejs @@ -1,220 +1,220 @@ -