├── bun.lockb ├── resources ├── icon.ico ├── installerHeader.bmp ├── installerSidebar.bmp ├── uninstallerHeader.bmp └── uninstallerSidebar.bmp ├── web ├── assets │ ├── vk.png │ ├── elyby.webp │ ├── github.png │ ├── icon.png │ ├── icon_2.png │ ├── icon_3.png │ ├── icon_4.png │ ├── idle1.wav │ ├── modIcon.webp │ ├── telegram.png │ ├── microsoft.webp │ ├── fonts │ │ ├── Manrope.ttf │ │ ├── FiraCode.ttf │ │ └── MaterialSymbols │ │ │ ├── font.woff2 │ │ │ └── font.css │ ├── posters │ │ ├── 1.11.webp │ │ ├── 1.12.webp │ │ ├── 1.13.webp │ │ ├── 1.14.webp │ │ ├── 1.15.webp │ │ ├── 1.16.webp │ │ ├── 1.17.webp │ │ ├── 1.18.webp │ │ ├── 1.19.webp │ │ ├── 1.20.webp │ │ └── 1.21.webp │ ├── sounds │ │ ├── start.mp3 │ │ └── sweep.mp3 │ ├── backgrounds │ │ ├── 1.webp │ │ ├── 2.webp │ │ ├── 3.webp │ │ ├── 4.webp │ │ ├── 5.webp │ │ └── min │ │ │ ├── 1.webp │ │ │ ├── 2.webp │ │ │ ├── 3.webp │ │ │ ├── 4.webp │ │ │ └── 5.webp │ ├── versions │ │ ├── fabric.webp │ │ ├── forge.webp │ │ ├── pack.webp │ │ ├── quilt.webp │ │ ├── 3rdparty.webp │ │ ├── neoforge.webp │ │ ├── vanilla.webp │ │ └── forgeOptiFine.webp │ ├── multiplayer_disabled.jpg │ ├── multiplayer_enabled.jpg │ └── flags │ │ ├── de.svg │ │ ├── ru.svg │ │ ├── ua.svg │ │ ├── en.svg │ │ └── cn.svg ├── css │ ├── link.css │ ├── themes │ │ ├── dark.css │ │ └── light.css │ ├── resets.css │ ├── scrollbar.css │ ├── input.css │ ├── link_openInNew.css │ ├── slider.css │ ├── chips.css │ ├── misc.css │ ├── checksList.css │ ├── checkbox.css │ ├── colorPicker.css │ ├── switch.css │ ├── progress.css │ ├── spinner.css │ ├── tabs.css │ ├── toast.css │ ├── alert.css │ ├── custom-select.css │ ├── modal.css │ ├── button.css │ ├── globals.css │ └── layout.css ├── sections │ ├── settings │ │ ├── startup.css │ │ ├── personalization.css │ │ ├── tabs.ejs │ │ ├── about.css │ │ ├── game.ejs │ │ └── java.ejs │ ├── packManager │ │ ├── updateTab.css │ │ ├── layoutTabs.ejs │ │ └── tabs.ejs │ └── packsAndMods │ │ ├── wrappedModsList.ejs │ │ └── filters.ejs ├── views │ ├── startAnimation.ejs │ ├── updateNotify.css │ ├── updateNotify.ejs │ ├── titleWrapped.ejs │ ├── news.ejs │ ├── versionsPosters.ejs │ ├── flymenu.css │ ├── flyout.css │ ├── news.css │ ├── flymenu.ejs │ ├── versionsPosters.css │ ├── flyout.ejs │ └── startAnimation.css ├── modals │ ├── createPack.css │ ├── content.ejs │ ├── frogLogin.css │ ├── elybyLogin.css │ ├── frogRegister.css │ ├── eula.ejs │ ├── eula.css │ ├── eula.js │ ├── changelog.css │ ├── content.css │ ├── changelog.ejs │ ├── selectWorld.ejs │ ├── createPack.ejs │ ├── packs.js │ ├── elybyLogin.ejs │ ├── console.css │ ├── frogLogin.ejs │ ├── installMods.ejs │ ├── frogRegister.ejs │ ├── selectWorld.css │ ├── packs.ejs │ ├── frogSkin.css │ ├── packs.css │ ├── packManager.ejs │ ├── console.ejs │ ├── settings.ejs │ ├── installMods.js │ ├── changelog.js │ ├── servers.ejs │ ├── elybyLogin.js │ ├── versions.css │ ├── versions.ejs │ ├── frogSkin.ejs │ ├── accounts.ejs │ ├── packManager.css │ ├── servers.css │ ├── accounts.css │ └── settings.css └── js │ ├── FrogRequests.js │ ├── FrogAlerts.js │ ├── FrogConfig.js │ ├── FrogNews.js │ ├── FrogUpdater.js │ ├── FrogToasts.js │ ├── FrogPlayStats.js │ ├── FrogErrorsParser.js │ ├── renderer.js │ ├── FrogServersUI.js │ ├── colors.js │ ├── FrogModals.js │ ├── FrogDownloader.js │ ├── FrogFlyout.js │ ├── FrogElybyManager.js │ ├── FrogAccountsUI.js │ └── FrogCollector.js ├── .gitignore ├── windows └── mainWindow.js ├── verifyTranslations.js ├── README.md ├── package.json ├── PRIVACY_POLICY.md └── EULA.md /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/bun.lockb -------------------------------------------------------------------------------- /resources/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/resources/icon.ico -------------------------------------------------------------------------------- /web/assets/vk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/web/assets/vk.png -------------------------------------------------------------------------------- /web/assets/elyby.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/web/assets/elyby.webp -------------------------------------------------------------------------------- /web/assets/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/web/assets/github.png -------------------------------------------------------------------------------- /web/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/web/assets/icon.png -------------------------------------------------------------------------------- /web/assets/icon_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/web/assets/icon_2.png -------------------------------------------------------------------------------- /web/assets/icon_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/web/assets/icon_3.png -------------------------------------------------------------------------------- /web/assets/icon_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/web/assets/icon_4.png -------------------------------------------------------------------------------- /web/assets/idle1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/web/assets/idle1.wav -------------------------------------------------------------------------------- /web/assets/modIcon.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/web/assets/modIcon.webp -------------------------------------------------------------------------------- /web/assets/telegram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/web/assets/telegram.png -------------------------------------------------------------------------------- /web/assets/microsoft.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/web/assets/microsoft.webp -------------------------------------------------------------------------------- /web/assets/fonts/Manrope.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/web/assets/fonts/Manrope.ttf -------------------------------------------------------------------------------- /web/assets/posters/1.11.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/web/assets/posters/1.11.webp -------------------------------------------------------------------------------- /web/assets/posters/1.12.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/web/assets/posters/1.12.webp -------------------------------------------------------------------------------- /web/assets/posters/1.13.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/web/assets/posters/1.13.webp -------------------------------------------------------------------------------- /web/assets/posters/1.14.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/web/assets/posters/1.14.webp -------------------------------------------------------------------------------- /web/assets/posters/1.15.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/web/assets/posters/1.15.webp -------------------------------------------------------------------------------- /web/assets/posters/1.16.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/web/assets/posters/1.16.webp -------------------------------------------------------------------------------- /web/assets/posters/1.17.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/web/assets/posters/1.17.webp -------------------------------------------------------------------------------- /web/assets/posters/1.18.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/web/assets/posters/1.18.webp -------------------------------------------------------------------------------- /web/assets/posters/1.19.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/web/assets/posters/1.19.webp -------------------------------------------------------------------------------- /web/assets/posters/1.20.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/web/assets/posters/1.20.webp -------------------------------------------------------------------------------- /web/assets/posters/1.21.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/web/assets/posters/1.21.webp -------------------------------------------------------------------------------- /web/assets/sounds/start.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/web/assets/sounds/start.mp3 -------------------------------------------------------------------------------- /web/assets/sounds/sweep.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/web/assets/sounds/sweep.mp3 -------------------------------------------------------------------------------- /resources/installerHeader.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/resources/installerHeader.bmp -------------------------------------------------------------------------------- /resources/installerSidebar.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/resources/installerSidebar.bmp -------------------------------------------------------------------------------- /resources/uninstallerHeader.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/resources/uninstallerHeader.bmp -------------------------------------------------------------------------------- /web/assets/backgrounds/1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/web/assets/backgrounds/1.webp -------------------------------------------------------------------------------- /web/assets/backgrounds/2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/web/assets/backgrounds/2.webp -------------------------------------------------------------------------------- /web/assets/backgrounds/3.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/web/assets/backgrounds/3.webp -------------------------------------------------------------------------------- /web/assets/backgrounds/4.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/web/assets/backgrounds/4.webp -------------------------------------------------------------------------------- /web/assets/backgrounds/5.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/web/assets/backgrounds/5.webp -------------------------------------------------------------------------------- /web/assets/fonts/FiraCode.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/web/assets/fonts/FiraCode.ttf -------------------------------------------------------------------------------- /web/assets/versions/fabric.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/web/assets/versions/fabric.webp -------------------------------------------------------------------------------- /web/assets/versions/forge.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/web/assets/versions/forge.webp -------------------------------------------------------------------------------- /web/assets/versions/pack.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/web/assets/versions/pack.webp -------------------------------------------------------------------------------- /web/assets/versions/quilt.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/web/assets/versions/quilt.webp -------------------------------------------------------------------------------- /resources/uninstallerSidebar.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/resources/uninstallerSidebar.bmp -------------------------------------------------------------------------------- /web/assets/backgrounds/min/1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/web/assets/backgrounds/min/1.webp -------------------------------------------------------------------------------- /web/assets/backgrounds/min/2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/web/assets/backgrounds/min/2.webp -------------------------------------------------------------------------------- /web/assets/backgrounds/min/3.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/web/assets/backgrounds/min/3.webp -------------------------------------------------------------------------------- /web/assets/backgrounds/min/4.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/web/assets/backgrounds/min/4.webp -------------------------------------------------------------------------------- /web/assets/backgrounds/min/5.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/web/assets/backgrounds/min/5.webp -------------------------------------------------------------------------------- /web/assets/versions/3rdparty.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/web/assets/versions/3rdparty.webp -------------------------------------------------------------------------------- /web/assets/versions/neoforge.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/web/assets/versions/neoforge.webp -------------------------------------------------------------------------------- /web/assets/versions/vanilla.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/web/assets/versions/vanilla.webp -------------------------------------------------------------------------------- /web/assets/multiplayer_disabled.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/web/assets/multiplayer_disabled.jpg -------------------------------------------------------------------------------- /web/assets/multiplayer_enabled.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/web/assets/multiplayer_enabled.jpg -------------------------------------------------------------------------------- /web/css/link.css: -------------------------------------------------------------------------------- 1 | a { 2 | color: var(--theme-primary) !important; 3 | font-size: 16pt; 4 | font-weight: 600; 5 | } -------------------------------------------------------------------------------- /web/assets/versions/forgeOptiFine.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/web/assets/versions/forgeOptiFine.webp -------------------------------------------------------------------------------- /web/assets/fonts/MaterialSymbols/font.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeroy/FrogLauncher2/HEAD/web/assets/fonts/MaterialSymbols/font.woff2 -------------------------------------------------------------------------------- /web/css/themes/dark.css: -------------------------------------------------------------------------------- 1 | html.dark { 2 | --bg-main: #171717; 3 | --bg-sub: #404040; 4 | --bg-transparent: rgba(23, 23, 23, 0.5); 5 | --text-color: white; 6 | --text-sub: #d1d1d1; 7 | } -------------------------------------------------------------------------------- /web/sections/settings/startup.css: -------------------------------------------------------------------------------- 1 | #modal-settings:has(label[data-setting="fullySeparatedStorage"] input:checked) .settings-item:has(label[data-setting="separatedStorage"]) { 2 | display: none !important; 3 | } -------------------------------------------------------------------------------- /web/assets/flags/de.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /web/assets/flags/ru.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /web/assets/flags/ua.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /web/css/themes/light.css: -------------------------------------------------------------------------------- 1 | html.light { 2 | --bg-main: #faf9f9; 3 | --bg-sub: #b8b8b8; 4 | --bg-transparent: rgba(250, 249, 249, 0.5); 5 | --text-color: #000000; 6 | --text-sub: #616161; 7 | } 8 | 9 | html.light .flymenu .item:not(.active) * { 10 | color: black; 11 | } -------------------------------------------------------------------------------- /web/css/resets.css: -------------------------------------------------------------------------------- 1 | button, input[type="text"], select, input[type="number"], input[type="color"], input[type="password"], input[type="email"], textarea, dialog { 2 | background: none; 3 | border: none; 4 | appearance: none; 5 | outline: none; 6 | padding: 0; 7 | margin: 0; 8 | border-radius: 0; 9 | } -------------------------------------------------------------------------------- /web/views/startAnimation.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | FrogLauncher Logo 6 | FrogLauncher 7 | {{commons.starting}} 8 |
-------------------------------------------------------------------------------- /web/modals/createPack.css: -------------------------------------------------------------------------------- 1 | /* Common stylings */ 2 | #modal-createPack { 3 | display: flex; 4 | align-items: center; 5 | justify-content: center; 6 | flex-direction: column; 7 | 8 | width: 40vw; 9 | height: max-content; 10 | gap: 8px; 11 | } 12 | 13 | #modal-createPack > * { 14 | width: 100%; 15 | } -------------------------------------------------------------------------------- /web/css/scrollbar.css: -------------------------------------------------------------------------------- 1 | *:not(.slider)::-webkit-scrollbar-thumb { 2 | background: var(--theme-primary); 3 | border-radius: 8px; 4 | } 5 | 6 | *:not(.slider)::-webkit-scrollbar-track { 7 | background: var(--theme-primaryBgLight); 8 | border-radius: 8px; 9 | } 10 | 11 | *:not(.slider)::-webkit-scrollbar { 12 | width: 6px; 13 | } -------------------------------------------------------------------------------- /web/modals/content.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /web/modals/frogLogin.css: -------------------------------------------------------------------------------- 1 | #modal-frogLogin { 2 | padding: 18px; 3 | 4 | width: 36vw; 5 | height: max-content; 6 | 7 | background: var(--theme-primaryBg); 8 | 9 | display: flex; 10 | flex-direction: column; 11 | gap: 12px; 12 | } 13 | 14 | #modal-frogLogin button.loginBtn { 15 | border-radius: 9999px; 16 | width: 100%; 17 | } -------------------------------------------------------------------------------- /web/modals/elybyLogin.css: -------------------------------------------------------------------------------- 1 | #modal-elybyLogin { 2 | padding: 18px; 3 | 4 | width: 36vw; 5 | height: max-content; 6 | 7 | background: var(--theme-primaryBg); 8 | 9 | display: flex; 10 | flex-direction: column; 11 | gap: 12px; 12 | } 13 | 14 | #modal-elybyLogin button.loginBtn { 15 | border-radius: 9999px; 16 | width: 100%; 17 | } -------------------------------------------------------------------------------- /web/modals/frogRegister.css: -------------------------------------------------------------------------------- 1 | #modal-frogRegister { 2 | padding: 18px; 3 | 4 | width: 36vw; 5 | height: max-content; 6 | 7 | background: var(--theme-primaryBg); 8 | 9 | display: flex; 10 | flex-direction: column; 11 | gap: 12px; 12 | } 13 | 14 | #modal-frogRegister button.loginBtn { 15 | border-radius: 9999px; 16 | width: 100%; 17 | } -------------------------------------------------------------------------------- /web/modals/eula.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | .idea/ 10 | pids 11 | *.pid 12 | *.seed 13 | *.pid.lock 14 | node_modules/ 15 | .npm 16 | .eslintcache 17 | .stylelintcache 18 | .node_repl_history 19 | *.tgz 20 | .env 21 | .env.development.local 22 | .env.test.local 23 | .env.production.local 24 | .env.local 25 | .cache 26 | .parcel-cache 27 | out 28 | dist 29 | .cache/ -------------------------------------------------------------------------------- /web/modals/eula.css: -------------------------------------------------------------------------------- 1 | #modal-eula { 2 | max-width: 90vw; 3 | max-height: 90vh; 4 | 5 | display: flex; 6 | flex-direction: column; 7 | align-items: center; 8 | gap: 24px; 9 | } 10 | 11 | #modal-eula .data { 12 | overflow-y: auto; 13 | max-height: 70vh; 14 | } 15 | 16 | #modal-eula .buttons { 17 | display: flex; 18 | gap: 8px; 19 | height: max-content; 20 | } 21 | 22 | #modal-eula .buttons button { 23 | min-width: 20vw; 24 | } -------------------------------------------------------------------------------- /web/css/input.css: -------------------------------------------------------------------------------- 1 | input[type="text"], input[type="password"], input[type="email"], input[type="number"] { 2 | appearance: none; 3 | border: 0; 4 | outline: none; 5 | 6 | background: var(--theme-primaryBgLight); 7 | padding: 8px; 8 | border-radius: 8px; 9 | color: var(--text-color); 10 | font-size: 14pt; 11 | 12 | min-width: 128px; 13 | max-width: 100vw; 14 | } 15 | 16 | input.lighter { 17 | background: var(--theme-primaryBgLighter); 18 | } -------------------------------------------------------------------------------- /web/css/link_openInNew.css: -------------------------------------------------------------------------------- 1 | .link { 2 | font-size: 14pt; 3 | 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | flex-direction: row; 8 | cursor: pointer; 9 | gap: 4px; 10 | } 11 | 12 | .link * { 13 | display: inline-block !important; 14 | } 15 | 16 | .link, 17 | .link * { 18 | text-decoration: none !important; 19 | color: var(--theme-primaryLighter) !important; 20 | } 21 | 22 | .link:hover * { 23 | color: var(--theme-primary) !important; 24 | } -------------------------------------------------------------------------------- /web/modals/eula.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | if(FrogConfig.read("eulaConfirmed", false) !== true){ 3 | $("#modal-eula").show(); 4 | $("#modal-eula .data").load("https://froglauncher.ru/privacy-policy"); 5 | } 6 | }) 7 | 8 | // При подтверждении 9 | $("#modal-eula .confirm").click(() => { 10 | $("#modal-eula").hide(); 11 | FrogConfig.write("eulaConfirmed", true); 12 | }) 13 | 14 | // При отклонении 15 | $("#modal-eula .decline").click(() => { 16 | FrogUI.closeMainWindow(); 17 | }) -------------------------------------------------------------------------------- /web/modals/changelog.css: -------------------------------------------------------------------------------- 1 | #modal-changelog .list .item { 2 | width: 100%; 3 | height: max-content; 4 | padding: 12px; 5 | border-radius: 8px; 6 | display: flex; 7 | flex-direction: column; 8 | gap: 12px; 9 | justify-content: center; 10 | background: var(--theme-primaryBgLight); 11 | } 12 | 13 | #modal-changelog .list { 14 | display: flex; 15 | flex-direction: column; 16 | gap: 12px; 17 | height: max-content; 18 | } 19 | 20 | #modal-changelog .wrapper { 21 | overflow: auto; 22 | height: 60vh; 23 | padding: 4px 12px; 24 | } -------------------------------------------------------------------------------- /web/modals/content.css: -------------------------------------------------------------------------------- 1 | #modal-content { 2 | max-height: 100%; 3 | background: var(--theme-primaryBg); 4 | padding: 12px; 5 | border-radius: 12px; 6 | z-index: 1; 7 | } 8 | 9 | #modal-content .scroll-wrap { 10 | overflow-y: auto; 11 | max-height: 73vh; 12 | gap: 12px; 13 | padding: 10px; 14 | } 15 | 16 | #modal-content .scroll-wrap > *:not(:first-child) { 17 | margin: 16px 0; 18 | } 19 | 20 | #modal-content::-webkit-scrollbar-track { 21 | background: transparent; 22 | } 23 | 24 | #modal-content::-webkit-scrollbar { 25 | width: 8px; 26 | } -------------------------------------------------------------------------------- /web/views/updateNotify.css: -------------------------------------------------------------------------------- 1 | #updateNotify { 2 | width: 100%; 3 | border-radius: 10px; 4 | gap: 12px; 5 | } 6 | 7 | #updateNotify .caption { 8 | font-size: 15pt; 9 | font-weight: 700; 10 | } 11 | 12 | #updateNotify .description { 13 | font-size: 13pt; 14 | font-weight: 500; 15 | color: var(--text-sub); 16 | } 17 | 18 | #updateNotify button { 19 | font-size: 14pt; 20 | padding: 8px 21px !important; 21 | border-radius: 32px !important; 22 | height: max-content !important; 23 | } 24 | 25 | #updateNotify .link * { 26 | font-size: 13pt; 27 | } -------------------------------------------------------------------------------- /web/modals/changelog.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | -------------------------------------------------------------------------------- /web/modals/selectWorld.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /web/assets/fonts/MaterialSymbols/font.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Material Symbols Outlined'; 3 | font-style: normal; 4 | font-weight: 400; 5 | src: url("font.woff2") format('woff2'); 6 | } 7 | 8 | .material-symbols-outlined { 9 | font-family: 'Material Symbols Outlined'; 10 | font-weight: normal; 11 | font-style: normal; 12 | font-size: 24px; 13 | line-height: 1; 14 | letter-spacing: normal; 15 | text-transform: none; 16 | display: inline-block; 17 | white-space: nowrap; 18 | word-wrap: normal; 19 | direction: ltr; 20 | -webkit-font-feature-settings: 'liga'; 21 | -webkit-font-smoothing: antialiased; 22 | } -------------------------------------------------------------------------------- /web/assets/flags/en.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /web/css/slider.css: -------------------------------------------------------------------------------- 1 | input[type="range"] { 2 | -webkit-appearance: none; 3 | width: 100%; 4 | height: 32px; 5 | background: transparent; 6 | outline: none; 7 | -webkit-transition: .2s; 8 | transition: opacity .2s; 9 | } 10 | 11 | input[type="range"]::-webkit-slider-runnable-track { 12 | height: 7px !important; 13 | background: var(--theme-primaryBgLighter); 14 | border-radius: 16px; 15 | } 16 | 17 | input[type="range"]::-webkit-slider-thumb { 18 | -webkit-appearance: none; 19 | appearance: none; 20 | width: 21px; 21 | height: 21px; 22 | margin-top: -6px; 23 | background: var(--theme-primary); 24 | border-radius: 9999px; 25 | cursor: pointer; 26 | } -------------------------------------------------------------------------------- /web/modals/createPack.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /web/assets/flags/cn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /web/sections/settings/personalization.css: -------------------------------------------------------------------------------- 1 | #languages-list { 2 | display: grid; 3 | grid-template-columns: 1fr 1fr; 4 | grid-gap: 0.5em; 5 | } 6 | 7 | #languages-list .item .icon { 8 | width: 28px; 9 | height: 28px; 10 | border-radius: 9999px; 11 | } 12 | 13 | #languages-list .item .text > span:first-child { 14 | font-weight: 600; 15 | } 16 | 17 | #languages-list .item .text > span:last-child, 18 | #languages-list .item .text sup { 19 | color: var(--text-sub); 20 | } 21 | 22 | #languages-list .item .text sup { 23 | opacity: 0.88; 24 | font-weight: 400 !important; 25 | } 26 | 27 | #modal-settings .settings-item .wallpapers-list-wrapper { 28 | display: flex; 29 | flex-wrap: wrap; 30 | gap: 10px; 31 | justify-content: end; 32 | align-items: center; 33 | max-width: 50%; 34 | } -------------------------------------------------------------------------------- /web/views/updateNotify.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /web/modals/packs.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | $(document).on("showModalEvent", (e) => { 3 | if (e.originalEvent.detail.modal === "packs") { 4 | FrogPacksUI.refreshDirectorySelect(); 5 | } 6 | }) 7 | 8 | $("#modal-packs .item.add button.install").click(function () { 9 | FrogPacksUI.setCurrentMode("modpacks"); 10 | FrogPacksUI.reloadAll(true, true, true); 11 | FrogModals.hideModal("packManager"); 12 | FrogModals.switchModal("installMods"); 13 | }) 14 | 15 | $("#modal-packs .item.add button.create").click(function () { 16 | FrogPacksUI.setCurrentMode("modpacks"); 17 | FrogPacksUI.reloadAll(true, true, true); 18 | FrogModals.showModal("createPack"); 19 | }) 20 | 21 | $("#modal-packs .item.add button.import").click(function () { 22 | FrogPacks.importModpack(); 23 | }) 24 | }) -------------------------------------------------------------------------------- /windows/mainWindow.js: -------------------------------------------------------------------------------- 1 | let path = require("path"); 2 | const {BrowserWindow} = require("electron"); 3 | const ejs = require('ejs-electron'); 4 | 5 | const MW_OPTIONS = { 6 | width: 1400, 7 | height: 900, 8 | minWidth: 1150, 9 | minHeight: 750, 10 | show: false, 11 | icon: path.join(__dirname, "../web/assets/icon.png"), 12 | resizable: true, 13 | maximizable: true, 14 | autoHideMenuBar: true, 15 | frame: false, 16 | transparent: true, 17 | webPreferences: { 18 | nodeIntegration: true, 19 | contextIsolation: false, 20 | }, 21 | }; 22 | 23 | exports.create = (cb) => { 24 | let winObj = new BrowserWindow(MW_OPTIONS); 25 | 26 | ejs.listen(); 27 | winObj.loadURL("file://" + __dirname + "/../web/index.ejs"); 28 | 29 | winObj.once("ready-to-show", () => { 30 | winObj.show(); 31 | cb(winObj); 32 | }); 33 | }; 34 | -------------------------------------------------------------------------------- /web/css/chips.css: -------------------------------------------------------------------------------- 1 | .chip { 2 | background: var(--theme-primaryBgLighter); 3 | width: max-content; 4 | height: max-content; 5 | 6 | padding: 4px 10px; 7 | border-radius: 16px; 8 | 9 | display: flex; 10 | align-items: center; 11 | justify-content: center; 12 | gap: 6px; 13 | } 14 | 15 | .chip.small { 16 | font-size: 10pt; 17 | padding: 4px 8px; 18 | } 19 | 20 | .chip.small * { 21 | font-size: 10pt; 22 | } 23 | 24 | .chip.large { 25 | font-size: 17pt; 26 | padding: 6px 16px; 27 | gap: 8px; 28 | } 29 | 30 | .chip.pill { 31 | border-radius: 9999px; 32 | } 33 | 34 | .chip.square { 35 | border-radius: 6px; 36 | } 37 | 38 | .chip.bgLight { 39 | background: var(--theme-primaryBgLight); 40 | } 41 | 42 | .chip.primary { 43 | background: var(--theme-primary); 44 | } 45 | 46 | .chip.primaryDarker { 47 | background: var(--theme-primaryDarker); 48 | } -------------------------------------------------------------------------------- /web/modals/elybyLogin.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 19 | 20 | -------------------------------------------------------------------------------- /web/modals/console.css: -------------------------------------------------------------------------------- 1 | #modal-console { 2 | padding-left: 7vw; 3 | padding-right: 7vw; 4 | } 5 | 6 | #modal-console .wrapper { 7 | background: var(--theme-primaryBgLight); 8 | overflow: auto !important; 9 | width: max-content; 10 | height: 82%; 11 | padding: 8px; 12 | margin: 16px 0; 13 | border-radius: 8px; 14 | } 15 | 16 | #modal-console .wrapper .console-textarea { 17 | width: 66vw; 18 | height: max-content; 19 | min-height: 100%; 20 | user-select: text; 21 | font-family: "Fira Code"; 22 | font-size: 11pt; 23 | } 24 | 25 | #modal-console .wrapper::-webkit-scrollbar { 26 | width: 4px; 27 | } 28 | 29 | #modal-console button { 30 | font-size: 14pt; 31 | padding: 6px 12px !important; 32 | height: max-content; 33 | width: max-content; 34 | border-radius: 8px !important; 35 | } 36 | 37 | #modal-console button:last-child { 38 | background: var(--success); 39 | color: white !important; 40 | } -------------------------------------------------------------------------------- /web/js/FrogRequests.js: -------------------------------------------------------------------------------- 1 | class FrogRequests { 2 | static get = (url) => { 3 | return new Promise(resolve => { 4 | $.get(url, (response) => { 5 | resolve([true, response]); 6 | }).fail(e => { 7 | resolve([false, e]); 8 | }) 9 | }) 10 | } 11 | 12 | static post = (url, data = {}, contentType = false, processData = false) => { 13 | return new Promise(resolve => { 14 | $.ajax({ 15 | url: url, 16 | type: "POST", 17 | data: data, 18 | success: (response) => { 19 | resolve([true, response]); 20 | }, 21 | error: (e) => { 22 | console.error(e); 23 | resolve([false, e]); 24 | }, 25 | processData: processData, 26 | contentType: contentType 27 | }); 28 | }) 29 | } 30 | } -------------------------------------------------------------------------------- /web/modals/frogLogin.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /web/views/titleWrapped.ejs: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | FrogLauncher 5 | FrogLauncher 6 |
7 | 8 |
9 | 12 | 15 | 18 |
19 |
-------------------------------------------------------------------------------- /web/views/news.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | newsmode 5 | {{commons.news}} 6 |
7 |
8 |
9 |
10 |
11 | {{loading.progress}} 12 |
13 | 14 | 23 |
24 |
-------------------------------------------------------------------------------- /web/css/misc.css: -------------------------------------------------------------------------------- 1 | .flex { 2 | display: flex; 3 | } 4 | 5 | .flex-center { 6 | align-items: center; 7 | justify-content: center; 8 | } 9 | 10 | .flex-align-center { 11 | align-items: center; 12 | } 13 | 14 | .flex-justify-center { 15 | justify-content: center; 16 | } 17 | 18 | .flex-col { 19 | flex-direction: column; 20 | } 21 | 22 | .flex-wrap { 23 | flex-wrap: wrap; 24 | } 25 | 26 | .flex-grow { 27 | flex-grow: 1; 28 | } 29 | 30 | .flex-row { 31 | flex-direction: row; 32 | } 33 | 34 | .flex-gap-8 { 35 | gap: 8px 36 | } 37 | 38 | .flex-gap-4 { 39 | gap: 4px 40 | } 41 | 42 | .flex-gap-v-8 { 43 | gap: 0 8px; 44 | } 45 | 46 | .flex-gap-h-8 { 47 | gap: 0 8px; 48 | } 49 | 50 | /* Microdot */ 51 | .microdot { 52 | display: flex; 53 | align-items: center; 54 | justify-content: center; 55 | width: 10px; 56 | height: 10px; 57 | } 58 | 59 | .microdot .dot { 60 | width: 4px; 61 | height: 4px; 62 | border-radius: 9999px; 63 | background: var(--text-sub); 64 | opacity: 0.6; 65 | } -------------------------------------------------------------------------------- /web/modals/installMods.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 28 | 29 | -------------------------------------------------------------------------------- /web/css/checksList.css: -------------------------------------------------------------------------------- 1 | .checksList { 2 | display: flex; 3 | padding: 8px; 4 | flex-direction: column; 5 | overflow-y: auto; 6 | overflow-x: hidden; 7 | min-width: 320px; 8 | } 9 | 10 | .checksList .item { 11 | display: flex; 12 | border-radius: 8px; 13 | background: var(--theme-primaryBg); 14 | border: 3px solid var(--theme-primaryBgLighter); 15 | padding: 14px 16px; 16 | align-items: center; 17 | margin: 4px 0; 18 | cursor: pointer; 19 | } 20 | 21 | .checksList .item:not(.active) * { 22 | color: var(--text-color); 23 | } 24 | 25 | .checksList .item:not(.active):hover { 26 | filter: brightness(120%); 27 | } 28 | 29 | .checksList .item:not(.active) .check { 30 | display: none !important; 31 | } 32 | 33 | .checksList .item.active { 34 | border-color: var(--theme-primary); 35 | } 36 | 37 | .checksList .item.active .material-symbols-outlined { 38 | color: var(--theme-primary); 39 | } 40 | 41 | .checksList .item .text { 42 | flex-grow: 1; 43 | } 44 | 45 | .checksList .item .check { 46 | font-size: 16px; 47 | } -------------------------------------------------------------------------------- /web/modals/frogRegister.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /web/css/checkbox.css: -------------------------------------------------------------------------------- 1 | .checkbox { 2 | position: absolute; 3 | z-index: -1; 4 | opacity: 0; 5 | margin-bottom: 1em; 6 | } 7 | 8 | .checkbox + label { 9 | display: inline-flex; 10 | align-items: center; 11 | user-select: none; 12 | cursor: pointer; 13 | } 14 | 15 | .checkbox + label::before { 16 | content: ''; 17 | display: inline-block; 18 | width: 22px; 19 | height: 22px; 20 | flex-shrink: 0; 21 | flex-grow: 0; 22 | border: 1px solid #adb5bd; 23 | border-radius: 0.25em; 24 | margin-right: 0.5em; 25 | background-repeat: no-repeat; 26 | background-position: center center; 27 | background-size: 50% 50%; 28 | } 29 | 30 | .checkbox:checked + label::before { 31 | border-color: var(--theme-primary); 32 | background-color: var(--theme-primary); 33 | background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3e%3c/svg%3e"); 34 | } 35 | 36 | .checkbox:disabled + label::before { 37 | background-color: var(--theme-primaryDarker); 38 | } -------------------------------------------------------------------------------- /web/css/colorPicker.css: -------------------------------------------------------------------------------- 1 | .color-picker { 2 | width: 32px; 3 | height: 32px; 4 | border-radius: 8px; 5 | 6 | display: flex; 7 | align-items: center; 8 | justify-content: center; 9 | cursor: pointer; 10 | } 11 | 12 | input.color-picker { 13 | opacity: 0; 14 | pointer-events: none; 15 | position: absolute; 16 | } 17 | 18 | .color-edit { 19 | background: var(--theme-primaryBgLighter); 20 | padding: 0; 21 | margin: 0; 22 | width: 38px; 23 | height: 38px; 24 | border-radius: 8px; 25 | min-width: 32px; 26 | } 27 | 28 | .color-edit:hover { 29 | filter: brightness(118%); 30 | background: var(--theme-primaryBgLighter); 31 | } 32 | 33 | .color-edit * { 34 | color: var(--text-color); 35 | font-size: 10pt; 36 | } 37 | 38 | .color-picker:hover { 39 | filter: brightness(86%); 40 | } 41 | 42 | .color-picker .material-symbols-outlined { 43 | color: white; 44 | font-size: 16px; 45 | } 46 | 47 | .color-picker:not(.active) .material-symbols-outlined { 48 | display: none; 49 | } 50 | 51 | .color-picker.active .material-symbols-outlined { 52 | display: initial; 53 | } -------------------------------------------------------------------------------- /web/modals/selectWorld.css: -------------------------------------------------------------------------------- 1 | #modal-selectWorld { 2 | width: max-content; 3 | min-width: 32vw; 4 | height: max-content; 5 | background: var(--theme-primaryBg); 6 | } 7 | 8 | #modal-selectWorld .worlds-list-wrapper { 9 | background: var(--theme-primaryBgDark); 10 | padding: 8px; 11 | overflow-y: auto; 12 | max-height: 50vh; 13 | border-radius: 8px; 14 | } 15 | 16 | #modal-selectWorld .worlds-list { 17 | display: flex; 18 | flex-direction: column; 19 | align-items: end; 20 | width: 100%; 21 | height: max-content; 22 | gap: 8px; 23 | } 24 | 25 | #modal-selectWorld .worlds-list .item { 26 | width: 100%; 27 | height: max-content; 28 | background: var(--theme-primaryBg); 29 | padding: 12px; 30 | border-radius: 8px; 31 | transition: 0s; 32 | } 33 | 34 | #modal-selectWorld .worlds-list .item.active { 35 | border: 2px solid var(--theme-primary); 36 | } 37 | 38 | #modal-selectWorld .worlds-list .item.sub { 39 | padding-left: 32px; 40 | width: 90%; 41 | cursor: pointer; 42 | } 43 | 44 | #modal-selectWorld .select { 45 | width: 100%; 46 | margin-top: 10px; 47 | } -------------------------------------------------------------------------------- /web/modals/packs.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 26 | 27 | -------------------------------------------------------------------------------- /web/sections/settings/tabs.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | format_paint 5 |
6 | {{settings.sections.personalization}} 7 |
8 | 9 |
10 |
11 | sports_esports 12 |
13 | {{settings.sections.game}} 14 |
15 | 16 |
17 |
18 | restart_alt 19 |
20 | {{settings.sections.startup}} 21 |
22 | 23 |
24 |
25 | web_asset 26 |
27 | {{settings.sections.java}} 28 |
29 | 30 |
31 |
32 | info 33 |
34 | {{settings.sections.about}} 35 |
36 |
-------------------------------------------------------------------------------- /web/sections/packManager/updateTab.css: -------------------------------------------------------------------------------- 1 | #modal-packManager .updateTab, 2 | #modal-packManager .updateTab .updateMode, 3 | #modal-packManager .updateTab .foundUpdateMode { 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | } 8 | 9 | #modal-packManager .updateTab > div, 10 | #modal-packManager .updateTab .scroll-wrap { 11 | width: 100%; 12 | } 13 | 14 | #modal-packManager .updateTab .scroll-wrap { 15 | max-height: 47vh; 16 | } 17 | 18 | #modal-packManager .updateTab .updateMode, 19 | #modal-packManager .updateTab .foundUpdateMode { 20 | flex-direction: column; 21 | gap: 12px; 22 | } 23 | 24 | /* Updates list mode */ 25 | #modal-packManager .updateTab .foundUpdateMode .updatesListWrapper { 26 | width: 100%; 27 | overflow: auto; 28 | } 29 | 30 | #modal-packManager .updateTab .foundUpdateMode .updatesList { 31 | display: flex; 32 | flex-direction: column; 33 | gap: 6px; 34 | width: 100%; 35 | height: max-content; 36 | } 37 | 38 | #modal-packManager .updateTab .foundUpdateMode .updatesList .item { 39 | display: flex; 40 | flex-direction: column; 41 | justify-content: center; 42 | 43 | gap: 4px; 44 | border-radius: 8px; 45 | 46 | width: 100%; 47 | height: max-content; 48 | padding: 12px; 49 | 50 | background: var(--theme-primaryBgLight); 51 | } -------------------------------------------------------------------------------- /web/css/switch.css: -------------------------------------------------------------------------------- 1 | .switch { 2 | position: relative; 3 | display: inline-block; 4 | width: 64px; 5 | height: 34px; 6 | } 7 | 8 | .switch input { 9 | opacity: 0; 10 | width: 0; 11 | height: 0; 12 | } 13 | 14 | .switch .inner { 15 | cursor: pointer; 16 | position: absolute; 17 | top: 0; 18 | left: 0; 19 | right: 0; 20 | bottom: 0; 21 | background-image: linear-gradient(270deg, var(--theme-primaryBgLighter) 0%, var(--theme-primaryBgLight) 50%, var(--theme-primary) 50%, var(--theme-primaryDarker) 100%); 22 | background-size: 132px; 23 | background-position: -67px; 24 | -webkit-transition: .4s; 25 | transition: .4s; 26 | border-radius: 34px; 27 | } 28 | 29 | .switch .inner:before { 30 | position: absolute; 31 | content: ""; 32 | height: 26px; 33 | width: 26px; 34 | left: 3px; 35 | bottom: 4px; 36 | background-color: var(--theme-text); 37 | -webkit-transition: .4s; 38 | transition: .4s; 39 | border-radius: 50%; 40 | } 41 | 42 | input:checked + .inner { 43 | background-position: 0; 44 | } 45 | 46 | input:focus + .inner { 47 | box-shadow: 0 0 1px var(--theme-primary); 48 | } 49 | 50 | input:checked + .inner:before { 51 | -webkit-transform: translateX(32px); 52 | -ms-transform: translateX(32px); 53 | transform: translateX(32px); 54 | } -------------------------------------------------------------------------------- /web/js/FrogAlerts.js: -------------------------------------------------------------------------------- 1 | const FROG_ALERT_PLACEHOLDER = 2 | '
$4
$1
$2
$6
'; 3 | 4 | class FrogAlerts { 5 | static create(caption, text, buttonText, icon, cb = () => { 6 | }, additionalElements = "") { 7 | icon = `${icon}`; 8 | let randomID = "alert-" + Math.floor(Math.random() * (1000 - 10 + 1)) + 10; 9 | $("body").append( 10 | FROG_ALERT_PLACEHOLDER.replaceAll(/\$1/gim, caption) 11 | .replaceAll(/\$2/gim, text) 12 | .replaceAll(/\$3/gim, buttonText) 13 | .replaceAll(/\$4/gim, icon) 14 | .replaceAll(/\$5/gim, randomID) 15 | .replaceAll(/\$6/gim, additionalElements) 16 | ); 17 | animateCSSNode($("#" + randomID)[0], "fadeIn", true); 18 | $("#cmbtn-" + randomID) 19 | .click(function () { 20 | animateCSSNode($("#" + randomID)[0], "fadeOut", true).then(() => { 21 | $(this).parent().parent().remove(); 22 | }); 23 | cb(); 24 | }); 25 | } 26 | } -------------------------------------------------------------------------------- /web/js/FrogConfig.js: -------------------------------------------------------------------------------- 1 | let mainConfig; 2 | let defaultConfig = { 3 | accounts: [], 4 | theme: { 5 | mode: 'dark', 6 | color: "rgb(10, 115, 255)" 7 | } 8 | }; 9 | 10 | class FrogConfig { 11 | // Прочитать конфиг-файл 12 | static readConfig() { 13 | if (fs.existsSync(CONFIG_PATH)) { 14 | return JSON.parse(fs.readFileSync(CONFIG_PATH)); 15 | } else { 16 | this.writeConfig(defaultConfig); 17 | return defaultConfig; 18 | } 19 | } 20 | 21 | // Записать конфиг-файл 22 | static writeConfig(config) { 23 | fs.writeFileSync( 24 | CONFIG_PATH, 25 | JSON.stringify(config, null, "\t") 26 | ); 27 | return true; 28 | } 29 | 30 | // Прочитать переменную из конфига 31 | static read = (key, defaultValue = true) => { 32 | if (!this.isKeyExists(key)) { 33 | this.write(key, defaultValue); 34 | return defaultValue; 35 | } 36 | return mainConfig[key]; 37 | }; 38 | 39 | // Записать переменную в конфиг 40 | static write = (key, value) => { 41 | mainConfig[key] = value; 42 | return this.writeConfig(mainConfig); 43 | } 44 | 45 | // Существует ли ключ в конфиге 46 | static isKeyExists = (key) => { 47 | if (typeof mainConfig === "undefined") { 48 | return false; 49 | } 50 | return typeof mainConfig[key] !== "undefined"; 51 | } 52 | } 53 | 54 | mainConfig = FrogConfig.readConfig(); -------------------------------------------------------------------------------- /web/modals/frogSkin.css: -------------------------------------------------------------------------------- 1 | #modal-frogSkin { 2 | height: 65vh; 3 | width: 65vw; 4 | } 5 | 6 | #modal-frogSkin .layout { 7 | display: grid; 8 | width: 100%; 9 | height: 75%; 10 | 11 | grid-template-columns: 1.2fr 1fr 1.2fr; 12 | grid-template-rows: 1fr; 13 | gap: 2px; 14 | } 15 | 16 | #modal-frogSkin .error { 17 | width: 100%; 18 | color: var(--error); 19 | text-align: center; 20 | } 21 | 22 | #modal-frogSkin .layout > div { 23 | display: flex; 24 | flex-direction: column; 25 | gap: 6px; 26 | align-items: center; 27 | justify-content: center; 28 | } 29 | 30 | #modal-frogSkin .layout .dragzone { 31 | width: 60%; 32 | height: 80%; 33 | 34 | border: 4px dashed var(--theme-primaryBgLight); 35 | cursor: pointer; 36 | 37 | display: flex; 38 | flex-direction: column; 39 | align-items: center; 40 | justify-content: center; 41 | padding: 16px; 42 | } 43 | 44 | #modal-frogSkin .layout .dragzone.dragged h4 { 45 | display: none; 46 | } 47 | 48 | #modal-frogSkin .layout .dragzone h4 { 49 | text-align: center; 50 | } 51 | 52 | #modal-frogSkin .layout .dragzone:hover, 53 | #modal-frogSkin .layout .dragzone.dragged { 54 | border: 4px dashed var(--theme-primaryBgLighter); 55 | background: var(--theme-primaryBgLight); 56 | } 57 | 58 | #skin_container { 59 | align-self: center; 60 | justify-self: center; 61 | align-content: center; 62 | justify-content: center; 63 | pointer-events: none; 64 | 65 | width: 300% !important; 66 | height: 100% !important; 67 | } -------------------------------------------------------------------------------- /web/css/progress.css: -------------------------------------------------------------------------------- 1 | .progress-pill { 2 | background: var(--theme-primaryBgLight); 3 | border-radius: 8px; 4 | } 5 | 6 | .progress-pill, .progress-pill * { 7 | transition: 0s !important; 8 | } 9 | 10 | /* Внутренняя часть */ 11 | .progress-pill .inner { 12 | background: var(--theme-primary); 13 | overflow: hidden; 14 | height: 100%; 15 | } 16 | 17 | .progress-pill .inner.indeterminate { 18 | animation: indeterminateAnimation 2s infinite linear; 19 | transform-origin: 0 50%; 20 | width: 100% !important; 21 | } 22 | 23 | /* Текст и враппер */ 24 | .progress-pill .title, 25 | .progress-pill .description, 26 | .progress-pill .percent { 27 | color: var(--theme-text); 28 | } 29 | 30 | .progress-pill .title { 31 | font-size: 16pt; 32 | font-weight: 600; 33 | } 34 | 35 | .progress-pill .description { 36 | font-size: 11pt; 37 | } 38 | 39 | .progress-pill .wrapper { 40 | display: flex; 41 | flex-direction: column; 42 | align-items: start; 43 | justify-content: center; 44 | position: relative; 45 | top: -54px; 46 | left: 16px; 47 | height: 89%; 48 | } 49 | 50 | .progress-pill .percent { 51 | position: relative; 52 | font-size: 18pt; 53 | font-weight: 800; 54 | top: -50px; 55 | right: 16px; 56 | } 57 | 58 | /* Анимация */ 59 | @keyframes indeterminateAnimation { 60 | 0% { 61 | transform: translateX(0) scaleX(0); 62 | } 63 | 40% { 64 | transform: translateX(0) scaleX(0.4); 65 | } 66 | 100% { 67 | transform: translateX(100%) scaleX(0.5); 68 | } 69 | } -------------------------------------------------------------------------------- /web/sections/settings/about.css: -------------------------------------------------------------------------------- 1 | /* Clickable */ 2 | .settings-item.clickable:hover { 3 | cursor: pointer; 4 | filter: brightness(119%); 5 | } 6 | 7 | #aboutItem { 8 | background-image: linear-gradient(90deg, var(--theme-primaryBgLight) 0%, var(--theme-primaryBgLighter) 50%, var(--theme-primaryBgLight) 100%) !important; 9 | background-position: 0 0; 10 | animation: gradAnim 5s linear infinite normal; 11 | } 12 | 13 | /* Описание */ 14 | #aboutItem .description { 15 | display: flex; 16 | gap: 2px; 17 | align-items: center; 18 | max-width: 100vw !important; 19 | } 20 | 21 | #aboutItem .description h3 { 22 | white-space: nowrap; 23 | text-overflow: ellipsis; 24 | max-width: 13vw; 25 | } 26 | 27 | #aboutItem .description, 28 | #aboutItem .description * { 29 | font-weight: 500; 30 | } 31 | 32 | #aboutItem * { 33 | color: var(--text-color) !important; 34 | } 35 | 36 | /* Разные текста */ 37 | #about_operatingSystem, 38 | #about_username { 39 | color: var(--theme-primaryBgLighter); 40 | } 41 | 42 | /* Заголовок */ 43 | #aboutItem .name { 44 | font-size: 16pt !important; 45 | font-weight: 700 !important; 46 | } 47 | 48 | #modal-settings .grid-wrapper { 49 | display: grid; 50 | grid-template-columns: 1fr 1fr; 51 | gap: 0.4em; 52 | width: 100%; 53 | height: 100%; 54 | } 55 | 56 | @keyframes gradAnim { 57 | 0% { 58 | background-position: 0; 59 | } 60 | 61 | 50% { 62 | background-position: 30vw; 63 | } 64 | 65 | 100% { 66 | background-position: 62vw; 67 | } 68 | } -------------------------------------------------------------------------------- /web/sections/packManager/layoutTabs.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 | 7 |
8 |
9 |
10 |
11 |
12 | 13 |
14 |
15 |
16 |
17 |
18 | 19 |
20 |
21 |
22 |
23 |
24 | 25 |
26 |
27 |
28 |
29 |
30 | 31 |
32 | 37 | 38 | 45 |
46 | 47 | -------------------------------------------------------------------------------- /web/modals/packs.css: -------------------------------------------------------------------------------- 1 | #modal-packs .wrapper .grid { 2 | margin-top: 8px; 3 | display: grid; 4 | grid-template-columns: repeat(auto-fill, minmax(100px, 256px)); 5 | gap: 12px; 6 | justify-content: center; 7 | } 8 | 9 | #modal-packs > div:nth-child(1) { 10 | margin-bottom: 0; 11 | } 12 | 13 | #modal-packs .wrapper { 14 | padding: 16px 0; 15 | } 16 | 17 | #modal-packs .wrapper .grid .item { 18 | width: 100%; 19 | height: 100%; 20 | 21 | display: flex; 22 | flex-direction: column; 23 | gap: 8px; 24 | background: var(--theme-primaryBgLight); 25 | padding: 16px; 26 | border-radius: 10px; 27 | cursor: pointer; 28 | } 29 | 30 | #modal-packs .wrapper .grid .item.add { 31 | align-items: center; 32 | justify-content: center; 33 | cursor: default; 34 | } 35 | 36 | #modal-packs .wrapper .grid .item.add button { 37 | width: 100%; 38 | } 39 | 40 | #modal-packs .wrapper .grid .item .version { 41 | font-size: 12pt; 42 | color: var(--text-sub); 43 | } 44 | 45 | #modal-packs .wrapper .grid .item:not(.add):hover { 46 | filter: brightness(113%); 47 | } 48 | 49 | #modal-packs .wrapper .grid .item .img-wrap img { 50 | width: 96px; 51 | height: 96px; 52 | border-radius: 18px; 53 | } 54 | 55 | #modal-packs .wrapper .grid .item .img-wrap { 56 | width: 100%; 57 | height: 145px; 58 | display: flex; 59 | align-items: center; 60 | justify-content: center; 61 | background: var(--theme-primaryBgLighter); 62 | border-radius: 16px; 63 | } 64 | 65 | #modal-packs .wrapper { 66 | height: 98%; 67 | overflow-y: auto; 68 | } -------------------------------------------------------------------------------- /web/modals/packManager.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /web/js/FrogNews.js: -------------------------------------------------------------------------------- 1 | class FrogNews { 2 | // Получить новости 3 | static getNews = async () => { 4 | return await FrogRequests.get(NEWS_URL); 5 | } 6 | 7 | // Загрузить новости в UI 8 | static loadNewsToUI = async () => { 9 | $(".news .preloader").show(); 10 | $(".news .news-list").hide(); 11 | $(".news .news-list").html(""); 12 | 13 | let [isSuccess, news] = await FrogNews.getNews(); 14 | if(!isSuccess || !news){ 15 | return false; 16 | } 17 | let placeholder = $(".news .placeholder")[0].outerHTML; 18 | placeholder = placeholder.replace(' placeholder"', ""); 19 | // По placeholder`у добавляем новые элементы 20 | news.forEach((item) => { 21 | let preparedPlaceholder = placeholder.replaceAll("$1", item.title).replaceAll("$2", item.description).replaceAll("$3", item.date); 22 | if (typeof item.url !== "undefined") { 23 | preparedPlaceholder = preparedPlaceholder.replaceAll("$4", item.url).replace('class="link" style="display: none;"', 'class="link"'); 24 | } 25 | let backgroundImageCSS = `linear-gradient(rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.82), rgb(0, 0, 0)), url("${item.picture}")`; 26 | $(".news .news-list").append(preparedPlaceholder); 27 | $(".news .news-list .news-item:last-child").css("background-image", backgroundImageCSS); 28 | }) 29 | $(".news .preloader").hide(); 30 | $(".news .news-list").show(); 31 | animateCSSNode($(".news .news-list")[0], "fadeIn"); 32 | return true; 33 | } 34 | } -------------------------------------------------------------------------------- /web/css/spinner.css: -------------------------------------------------------------------------------- 1 | .spinner { 2 | border: 6px solid var(--theme-primaryBgLight); 3 | border-top: 6px solid var(--theme-primary); 4 | border-radius: 50%; 5 | width: 48px; 6 | height: 48px; 7 | animation: spin 1.3s cubic-bezier(0.5, 0, 0.2, 1) infinite; 8 | } 9 | 10 | /* Размеры спиннера */ 11 | .spinner.small { 12 | width: 38px; 13 | height: 38px; 14 | border-width: 3px; 15 | } 16 | 17 | .spinner.large { 18 | width: 80px; 19 | height: 80px; 20 | border-width: 9px; 21 | } 22 | 23 | /* Виды спиннера */ 24 | .spinner.transparent { 25 | border: 6px solid transparent; 26 | border-top: 6px solid var(--theme-primary); 27 | } 28 | 29 | .spinner.mode { 30 | border: 6px solid var(--bg-sub); 31 | border-top: 6px solid var(--text-sub); 32 | } 33 | 34 | @keyframes spin { 35 | 0% { 36 | transform: rotate(0deg); 37 | } 38 | 100% { 39 | transform: rotate(360deg); 40 | } 41 | } 42 | 43 | .line-spinner { 44 | display: inline-block; 45 | width: 100%; 46 | height: 16px; 47 | border-radius: 9999px; 48 | 49 | background: var(--theme-primaryBgLight); 50 | 51 | background-image: linear-gradient(90deg, var(--theme-primaryBgLight) 0%, var(--theme-primaryBgLighter) 50%, var(--theme-primaryBgLight) 100%) !important; 52 | filter: brightness(115%); 53 | background-position: 0 0; 54 | animation: gradAnim 3s linear infinite normal; 55 | } 56 | 57 | .line-spinner.custom-select { 58 | height: 100%; 59 | width: max-content; 60 | flex-grow: 1; 61 | pointer-events: none; 62 | } 63 | 64 | .line-spinner.small { 65 | height: 10px; 66 | } -------------------------------------------------------------------------------- /verifyTranslations.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | let reference = JSON.parse(fs.readFileSync('./web/languages/ru.json').toString()); 3 | let referenceKeys = Object.keys(flatten(reference)); 4 | 5 | const languagesList = fs.readdirSync("./web/languages"); 6 | 7 | languagesList.forEach((lang) => { 8 | if(lang !== "ru.json"){ 9 | let langData = JSON.parse(fs.readFileSync(`./web/languages/${lang}`).toString()); 10 | let flattenLang = Object.keys(flatten(langData)); 11 | let difference = referenceKeys.filter(x => !flattenLang.includes(x)); 12 | if(difference.length === 0){ 13 | console.log(`Язык ${lang} не имеет проблем`); 14 | } else { 15 | console.log(`Язык ${lang} имеет ${difference.length} проблем:`); 16 | console.log(difference); 17 | } 18 | } 19 | }) 20 | 21 | function traverseAndFlatten(currentNode, target, flattenedKey) { 22 | for (let key in currentNode) { 23 | if (currentNode.hasOwnProperty(key)) { 24 | let newKey; 25 | if (flattenedKey === undefined) { 26 | newKey = key; 27 | } else { 28 | newKey = flattenedKey + '.' + key; 29 | } 30 | 31 | let value = currentNode[key]; 32 | if (typeof value === "object") { 33 | traverseAndFlatten(value, target, newKey); 34 | } else { 35 | target[newKey] = value; 36 | } 37 | } 38 | } 39 | } 40 | 41 | function flatten(obj) { 42 | let flattenedObject = {}; 43 | traverseAndFlatten(obj, flattenedObject); 44 | return flattenedObject; 45 | } -------------------------------------------------------------------------------- /web/views/versionsPosters.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | bolt 5 | {{commons.bigUpdates}} 6 |
7 | 8 |
9 |
10 | 18 |
19 |
20 | 21 | -------------------------------------------------------------------------------- /web/css/tabs.css: -------------------------------------------------------------------------------- 1 | .tabs { 2 | display: flex; 3 | gap: 10px; 4 | align-items: center; 5 | overflow: visible; 6 | } 7 | 8 | .tabs.bg { 9 | background: var(--theme-primaryBgShadow); 10 | } 11 | 12 | .tabs.vertical { 13 | flex-direction: column; 14 | } 15 | 16 | .tabs.vertical .tab { 17 | width: 100%; 18 | justify-content: start; 19 | } 20 | 21 | .tabs .tab { 22 | background: var(--theme-primaryBgLight); 23 | width: max-content; 24 | height: 40px; 25 | border-radius: 8px; 26 | 27 | display: flex; 28 | align-items: center; 29 | justify-content: center; 30 | gap: 8px; 31 | 32 | padding: 24px 16px; 33 | font-size: 14pt; 34 | } 35 | 36 | .tabs .tab .icon { 37 | display: flex; 38 | align-items: center; 39 | justify-content: center; 40 | 41 | width: 24px; 42 | height: 24px; 43 | background: var(--theme-primaryBgLighter); 44 | border-radius: 8px; 45 | } 46 | 47 | .tabs .tab.active .icon { 48 | background: var(--theme-primaryLighter); 49 | } 50 | 51 | .tabs .tab .icon * { 52 | font-size: 16px; 53 | } 54 | 55 | .tabs .tab:not(.active):hover { 56 | cursor: pointer; 57 | filter: brightness(105%); 58 | } 59 | 60 | html.light .tabs .tab:not(.active):hover { 61 | filter: brightness(95%); 62 | } 63 | 64 | .tabs .tab * { 65 | color: var(--text-color); 66 | } 67 | 68 | .tabs .tab:not(.active):hover { 69 | filter: brightness(135%); 70 | } 71 | 72 | .tabs .tab.active { 73 | background: var(--theme-primary); 74 | border: none; 75 | box-shadow: var(--theme-primaryBgLight) 0 2px 16px; 76 | } 77 | 78 | .tabs .tab.active * { 79 | color: var(--theme-text); 80 | } -------------------------------------------------------------------------------- /web/js/FrogUpdater.js: -------------------------------------------------------------------------------- 1 | class FrogUpdater { 2 | // Забиндить эвенты обновлений 3 | static bindUpdate = () => { 4 | ipcRenderer.once("update-available", () => { 5 | FrogUpdater.onUpdateAvailable(); 6 | }); 7 | ipcRenderer.once("update-downloaded", () => { 8 | FrogUpdater.onUpdateDownloaded(); 9 | }); 10 | } 11 | 12 | // При нахождении обновления - показать уведомление 13 | static onUpdateAvailable = () => { 14 | let $notifyElem = $("#updateNotify"); 15 | let $notifyElemBtn = $notifyElem.find("button:not(.transparent)"); 16 | let $notifyElemText = $notifyElem.find(".description"); 17 | FrogCollector.writeLog(`Updater: New version found`); 18 | $notifyElem.show(); 19 | $notifyElem.addClass("animate__animated animate__fadeIn"); 20 | $notifyElemBtn.hide(); 21 | $notifyElemText.text(); 22 | setTimeout(() => { 23 | $notifyElem.removeClass("animate__animated animate__fadeIn"); 24 | }, 1000); 25 | } 26 | 27 | // После скачивания обновления - изменить уведомление 28 | static onUpdateDownloaded = () => { 29 | let $notifyElem = $("#updateNotify"); 30 | let $notifyElemBtn = $notifyElem.find("button:not(.transparent)"); 31 | let $notifyElemText = $notifyElem.find(".description"); 32 | FrogCollector.writeLog(`Updater: New version downloaded, ready to restart`); 33 | $notifyElemBtn.show(); 34 | $notifyElemText.text(MESSAGES.updater.ready); 35 | } 36 | 37 | // Установить обновление 38 | static installUpdate = () => { 39 | ipcRenderer.send("install-update"); 40 | } 41 | } -------------------------------------------------------------------------------- /web/views/flymenu.css: -------------------------------------------------------------------------------- 1 | /* Flyout menu */ 2 | .flymenu { 3 | width: 78px !important; 4 | height: 100% !important; 5 | 6 | display: flex; 7 | align-items: center; 8 | justify-content: center; 9 | flex-direction: column; 10 | 11 | gap: 16px; 12 | } 13 | 14 | .flymenu .menu-wrapped { 15 | display: flex; 16 | flex-direction: column; 17 | align-items: center; 18 | justify-content: center; 19 | overflow: visible; 20 | gap: 12px; 21 | flex-grow: 1; 22 | } 23 | 24 | .flymenu .item { 25 | width: 56px; 26 | height: 56px; 27 | border-radius: 16px; 28 | 29 | display: flex; 30 | align-items: center; 31 | justify-content: center; 32 | flex-direction: column; 33 | 34 | background: var(--theme-primaryBgLight); 35 | color: var(--theme-text); 36 | } 37 | 38 | .flymenu .item:not(.active):hover { 39 | cursor: pointer; 40 | filter: brightness(112%); 41 | box-shadow: var(--theme-primaryBgLight) 0 0 24px; 42 | } 43 | 44 | html.light .flymenu .item:not(.active):hover { 45 | filter: brightness(90%); 46 | } 47 | 48 | .flymenu .item.active { 49 | background: var(--theme-primary); 50 | box-shadow: var(--theme-primaryDarker) 0 0 48px; 51 | } 52 | 53 | .flymenu .item.active, 54 | .flymenu .item.active * { 55 | color: var(--theme-text); 56 | font-size: 24px; 57 | } 58 | 59 | .flymenu-tooltip { 60 | z-index: 9999; 61 | position: absolute; 62 | left: 0; 63 | right: 0; 64 | bottom: 0; 65 | top: 0; 66 | border-radius: 16px; 67 | 68 | width: max-content; 69 | height: max-content; 70 | transition: top 0s, left 0s !important; 71 | pointer-events: none !important; 72 | } -------------------------------------------------------------------------------- /web/sections/settings/game.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 | work_update 4 |
5 | {{settings.game.autoInstallDeps}} 6 | 10 |
11 | 12 |
13 |
14 | terminal 15 |
16 | {{settings.game.consoleOnStart}} 17 | 21 |
22 | 23 |
24 |
25 | bug_report 26 |
27 | {{settings.game.consoleOnCrash}} 28 | 32 |
33 | 34 |
35 |
36 | hide 37 |
38 | {{settings.game.hideOnStart}} 39 | 43 |
-------------------------------------------------------------------------------- /web/views/flyout.css: -------------------------------------------------------------------------------- 1 | .flyout, .flymenu { 2 | background: var(--theme-primaryBg); 3 | width: 80%; 4 | height: 78px; 5 | padding: 10px; 6 | border-radius: 21px; 7 | 8 | display: flex; 9 | align-items: center; 10 | justify-content: center; 11 | gap: 10px; 12 | 13 | box-shadow: var(--theme-primaryBgShadow) 0 2px 24px; 14 | } 15 | 16 | .flyout > div, button { 17 | height: 56px; 18 | border-radius: 11px !important; 19 | } 20 | 21 | .flyout #playButton { 22 | width: 15vw; 23 | min-width: 160px; 24 | } 25 | 26 | .flyout #accountSelect { 27 | width: 20vw; 28 | } 29 | 30 | .flyout .flyout-mode { 31 | animation-duration: .24s !important; 32 | } 33 | 34 | .flyout #stopGameButton { 35 | background: var(--error); 36 | } 37 | 38 | .flyout #stopGameButton, 39 | .flyout #stopGameButton * { 40 | color: white !important; 41 | } 42 | 43 | .flyout #stopGameButton:hover { 44 | filter: brightness(87%); 45 | } 46 | 47 | /* Hide divs */ 48 | .flyout > div.hidden { 49 | display: none; 50 | } 51 | 52 | .flyout .progress-pill, 53 | .flyout .spinner-wrapper, 54 | .flyout #versionSelect { 55 | flex-grow: 1; 56 | } 57 | 58 | /* Spinner wrapper */ 59 | .flyout .spinner-wrapper { 60 | display: flex; 61 | align-items: center; 62 | justify-content: start; 63 | gap: 12px; 64 | flex-direction: row; 65 | } 66 | 67 | .flyout .spinner-wrapper .title { 68 | font-weight: 400; 69 | font-size: 14pt; 70 | } 71 | 72 | /* Flyout start mode */ 73 | .flyout.start-mode #accountSelect, 74 | .flyout.start-mode #versionSelect { 75 | pointer-events: none !important; 76 | cursor: not-allowed; 77 | } 78 | 79 | .flyout.start-mode #playButton { 80 | display: none; 81 | } -------------------------------------------------------------------------------- /web/css/toast.css: -------------------------------------------------------------------------------- 1 | .toast { 2 | min-width: 18vw; 3 | max-height: 40vh; 4 | max-width: 40vw; 5 | width: 100%; 6 | height: max-content; 7 | background: var(--theme-primaryBg); 8 | border-radius: 8px; 9 | padding: 10px 16px; 10 | display: flex; 11 | align-items: center; 12 | justify-content: start; 13 | flex-direction: row; 14 | cursor: pointer; 15 | } 16 | 17 | .toast .icon-bg { 18 | width: 38px; 19 | height: 38px; 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | background: var(--theme-primaryBgLight); 24 | border-radius: 8px; 25 | margin-right: 16px; 26 | } 27 | 28 | .toast .content { 29 | display: flex; 30 | flex-direction: column; 31 | } 32 | 33 | .toast .content .description { 34 | margin-top: 2px; 35 | } 36 | 37 | .toast .icon-bg * { 38 | color: var(--text-color); 39 | font-size: 21pt; 40 | } 41 | 42 | .toast .icon-bg.circle { 43 | border-radius: 50%; 44 | } 45 | 46 | .toast .icon-bg { 47 | background: var(--theme-primaryBgLighter); 48 | } 49 | 50 | .toast .icon-bg.colored * { 51 | color: var(--theme-primary); 52 | } 53 | 54 | .toast .caption { 55 | font-size: 16pt; 56 | font-weight: 500; 57 | color: var(--text-color); 58 | } 59 | 60 | .toast .description { 61 | font-size: 13pt; 62 | color: #9d9d9d; 63 | } 64 | 65 | #toasts-pool { 66 | display: flex; 67 | flex-direction: column; 68 | justify-content: center; 69 | 70 | position: absolute; 71 | z-index: 3000000; 72 | 73 | max-height: 100vh; 74 | max-width: 45vw; 75 | height: max-content; 76 | width: max-content; 77 | 78 | overflow: hidden; 79 | gap: 12px; 80 | 81 | top: 24px; 82 | left: 24px; 83 | } -------------------------------------------------------------------------------- /web/views/news.css: -------------------------------------------------------------------------------- 1 | .news { 2 | display: flex; 3 | align-items: center; 4 | overflow-x: auto; 5 | width: 100%; 6 | height: 28vh; 7 | } 8 | 9 | .news .preloader { 10 | width: 100%; 11 | height: 100%; 12 | display: flex; 13 | align-items: center; 14 | justify-content: center; 15 | flex-direction: column; 16 | gap: 12px; 17 | } 18 | 19 | .news-list { 20 | display: flex; 21 | width: 82vw; 22 | height: 100%; 23 | gap: 12px; 24 | scroll-snap-align: center; 25 | scroll-snap-type: x mandatory; 26 | 27 | overflow: auto; 28 | } 29 | 30 | .news-list .news-item { 31 | flex: 0 0 auto; 32 | scroll-snap-align: start; 33 | } 34 | 35 | .news-list::-webkit-scrollbar-thumb, 36 | .news-list::-webkit-scrollbar, 37 | .news-list::-webkit-scrollbar-track { 38 | height: 8px !important; 39 | } 40 | 41 | .news-list::-webkit-scrollbar-thumb { 42 | background: var(--theme-primaryBgLighter); 43 | } 44 | 45 | .news-list::-webkit-scrollbar-track { 46 | background: transparent; 47 | } 48 | 49 | /* News item */ 50 | .news .news-item { 51 | padding: 18px; 52 | border-radius: 12px; 53 | 54 | background-size: cover; 55 | background-repeat: no-repeat; 56 | background-position: center; 57 | 58 | display: flex; 59 | flex-direction: column; 60 | justify-content: end; 61 | 62 | width: 32vw; 63 | height: 27vh; 64 | } 65 | 66 | .news .news-item .title { 67 | font-size: 22pt; 68 | font-weight: 700; 69 | color: white; 70 | } 71 | 72 | .news .news-item .description { 73 | font-size: 13pt; 74 | color: rgba(235, 235, 235); 75 | } 76 | 77 | .news .news-item .date { 78 | font-size: 11pt; 79 | font-weight: 400; 80 | color: rgba(180, 180, 180); 81 | text-align: end; 82 | } -------------------------------------------------------------------------------- /web/css/alert.css: -------------------------------------------------------------------------------- 1 | .alert-modal { 2 | width: 100%; 3 | height: 100%; 4 | z-index: 40; 5 | display: block; 6 | 7 | position: absolute; 8 | left: 0; 9 | right: 0; 10 | top: 0; 11 | bottom: 0; 12 | backdrop-filter: blur(8px); 13 | border-radius: 12px; 14 | } 15 | 16 | .alert-modal .alert-window { 17 | position: absolute; 18 | left: 0; 19 | right: 0; 20 | top: 0; 21 | bottom: 0; 22 | width: max-content; 23 | max-width: 48vw; 24 | min-width: 23vw; 25 | height: max-content; 26 | padding: 32px; 27 | border-radius: 16px; 28 | z-index: 41; 29 | background: var(--theme-primaryBg); 30 | box-shadow: rgba(0, 0, 0, 0.25) 0 54px 55px, 31 | rgba(0, 0, 0, 0.12) 0 -12px 30px, rgba(0, 0, 0, 0.12) 0 4px 6px, 32 | rgba(0, 0, 0, 0.17) 0 12px 13px, rgba(0, 0, 0, 0.09) 0 -3px 5px; 33 | 34 | display: flex; 35 | flex-direction: column; 36 | align-items: center; 37 | justify-content: center; 38 | gap: 16px; 39 | margin: auto; 40 | } 41 | 42 | .alert-modal .alert-window .alert-icon { 43 | display: flex; 44 | align-items: center; 45 | justify-content: center; 46 | 47 | border-radius: 8px; 48 | padding: 8px; 49 | background: var(--theme-primaryBgLight); 50 | height: 64px; 51 | width: 64px; 52 | } 53 | 54 | .alert-modal .alert-window .alert-icon .material-symbols-outlined { 55 | font-size: 26pt; 56 | color: var(--text-color); 57 | } 58 | 59 | .alert-modal .alert-window .alert-caption { 60 | font-weight: 700; 61 | font-size: 15pt; 62 | color: var(--text-color); 63 | text-align: center; 64 | } 65 | 66 | .alert-modal .alert-window .alert-description { 67 | font-weight: 500; 68 | font-size: 14pt; 69 | color: var(--text-sub); 70 | text-align: center; 71 | } 72 | -------------------------------------------------------------------------------- /web/modals/console.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 23 | 24 | -------------------------------------------------------------------------------- /web/modals/settings.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 47 | 48 | -------------------------------------------------------------------------------- /web/js/FrogToasts.js: -------------------------------------------------------------------------------- 1 | class FrogToasts { 2 | // Функция отправки нового уведомления 3 | static create(text, icon = "info", description = "", duration = 5000, iconClasses = "", callback = () => { 4 | }) { 5 | let toastsPoolElement = $("#toasts-pool"); 6 | let newID = this.generateID(); 7 | let toastCode = "
"; 8 | if (iconClasses !== "") { 9 | toastCode = toastCode + "
" + icon + "
"; 10 | } else { 11 | toastCode = toastCode + "
" + icon + "
"; 12 | } 13 | if (description !== "") { 14 | toastCode = toastCode + "
" + text + "
" + description + "
"; 15 | } else { 16 | toastCode = toastCode + "
" + text + "
"; 17 | } 18 | toastCode = toastCode + "
"; 19 | toastsPoolElement.append(toastCode); 20 | $("#toast-" + newID).on("click", function () { 21 | $(this).remove(); 22 | callback(); 23 | }); 24 | if (duration > 0) { 25 | $("#toast-" + newID) 26 | .delay(duration) 27 | .queue(function () { 28 | animateCSSNode($(this)[0], "fadeOut", false).then(() => { 29 | $(this).remove(); 30 | }); 31 | }); 32 | } 33 | } 34 | 35 | // Получить ID для нового уведомления 36 | static generateID() { 37 | return $("#toasts-pool .toast").length; 38 | } 39 | 40 | // Удалить все уведомления 41 | static removeAll() { 42 | $("#toasts-pool").html(""); 43 | } 44 | } -------------------------------------------------------------------------------- /web/css/custom-select.css: -------------------------------------------------------------------------------- 1 | .custom-select { 2 | background: var(--theme-primaryBgLight); 3 | cursor: pointer; 4 | 5 | border-radius: 6px; 6 | padding: 0 6px; 7 | height: 56px; 8 | } 9 | 10 | .custom-select.lighter { 11 | background: var(--theme-primaryBgLighter); 12 | } 13 | 14 | select.custom-select { 15 | color: var(--text-color); 16 | padding: 6px 12px; 17 | font-size: 14pt; 18 | font-weight: 600; 19 | } 20 | 21 | .custom-select:hover { 22 | filter: brightness(110%); 23 | background: var(--theme-primaryBgLight) !important; 24 | } 25 | 26 | /* Icon and description mode */ 27 | .custom-select.icon-and-description { 28 | display: grid; 29 | grid-template-columns: 40px 1fr max-content; 30 | grid-template-rows: max-content max-content; 31 | grid-auto-columns: 1fr; 32 | gap: 0 12px; 33 | grid-auto-flow: row; 34 | align-content: center; 35 | grid-template-areas: 36 | "icon title button" 37 | "icon description button"; 38 | } 39 | 40 | .custom-select.icon-and-description .icon { 41 | justify-self: center; 42 | align-self: center; 43 | grid-area: icon; 44 | 45 | width: 40px; 46 | height: 40px; 47 | border-radius: 8px; 48 | } 49 | 50 | .custom-select.icon-and-description .title { 51 | grid-area: title; 52 | font-size: 14pt; 53 | font-weight: 500; 54 | } 55 | 56 | .custom-select.icon-and-description .button { 57 | grid-area: button; 58 | justify-self: center; 59 | align-self: center; 60 | padding: 2px; 61 | border-radius: 9999px; 62 | background: var(--theme-primaryBgLighter); 63 | 64 | display: flex; 65 | align-items: center; 66 | justify-content: center; 67 | } 68 | 69 | .custom-select.icon-and-description .description { 70 | align-self: start; 71 | grid-area: description; 72 | 73 | color: rgb(150, 150, 150); 74 | text-transform: uppercase; 75 | font-size: 9pt; 76 | font-weight: 700; 77 | } -------------------------------------------------------------------------------- /web/sections/packManager/tabs.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | deployed_code_update 5 |
6 | {{packs.filters.mods}} 7 |
8 |
9 |
10 | globe_asia 11 |
12 | {{packManager.tabs.worlds}} 13 |
14 |
15 |
16 | landscape 17 |
18 | {{packs.filters.rp}} 19 |
20 |
21 |
22 | light_mode 23 |
24 | {{packs.filters.shaders}} 25 |
26 | 32 |
33 |
34 | update 35 |
36 | {{packManager.tabs.updates}} 37 |
38 |
39 | 43 | 44 | 48 |
-------------------------------------------------------------------------------- /web/sections/packsAndMods/wrappedModsList.ejs: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 33 |
34 |
35 |
36 | {{loading.progress}} 37 |
38 |
39 | 40 | -------------------------------------------------------------------------------- /web/modals/installMods.js: -------------------------------------------------------------------------------- 1 | let packs__firstAppear = true; 2 | 3 | $(function () { 4 | $(document).on("showModalEvent", (e) => { 5 | if (e.originalEvent.detail.modal === "installMods") { 6 | if (packs__firstAppear === true) { 7 | packs__firstAppear = false; 8 | FrogPacksUI.reloadAll(true); 9 | } else { 10 | FrogPacksUI.refreshDirectorySelect(); 11 | } 12 | } 13 | }) 14 | 15 | $("#modal-installMods input.search").on("change", function () { 16 | FrogPacksUI.reloadAll(true); 17 | }) 18 | 19 | $("#modal-installMods #packs_dirList").on("change", function () { 20 | FrogPacksUI.reloadAll(true); 21 | }) 22 | 23 | // Прогрузка при скролле 24 | $("#modal-installMods .packs-wrapper").scroll(function () { 25 | let wrapper = $(this)[0]; 26 | if (wrapper.offsetHeight === wrapper.scrollHeight || packs_scrollIsLoading) { 27 | return false; 28 | } 29 | if (wrapper.offsetHeight + wrapper.scrollTop >= wrapper.scrollHeight) { 30 | packs_scrollIsLoading = true; 31 | FrogPacksUI.loadMore(); 32 | return true; 33 | } 34 | }); 35 | }) 36 | 37 | // Переключение табов 38 | $("#modal-installMods .tabs.sub .tab").click(function () { 39 | if ($(this).hasClass("active")) { 40 | return; 41 | } 42 | 43 | $("#modal-installMods .tabs.sub .tab.active").removeClass("active"); 44 | $(this).addClass("active"); 45 | let tabName = $(this).data("tab"); 46 | 47 | let tabIcon = $(this).find(".material-symbols-outlined").html(); 48 | let tabText = $(this).find("span:not(.material-symbols-outlined)").text(); 49 | 50 | $("#packs_currentSection span:not(.material-symbols-outlined)").text(tabText); 51 | $("#packs_currentSection .material-symbols-outlined").html(tabIcon); 52 | 53 | FrogPacksUI.setCurrentMode(tabName); 54 | FrogPacksUI.reloadAll(true); 55 | }) -------------------------------------------------------------------------------- /web/views/flymenu.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 21 |
22 | folder 23 |
24 |
25 | 26 | -------------------------------------------------------------------------------- /web/css/modal.css: -------------------------------------------------------------------------------- 1 | .modal { 2 | position: relative; 3 | width: 100%; 4 | height: 100%; 5 | background: var(--theme-primaryBg); 6 | z-index: 3; 7 | 8 | border-radius: 16px; 9 | box-shadow: rgba(0, 0, 0, 0.16) 0 10px 36px 0, rgba(0, 0, 0, 0.06) 0 0 0 1px; 10 | padding: 3%; 11 | 12 | max-height: 73vh; 13 | } 14 | 15 | .modal-overlay { 16 | position: absolute; 17 | left: 0; 18 | right: 0; 19 | bottom: 0; 20 | top: 0; 21 | z-index: 4; 22 | width: 100vw; 23 | height: 100vh; 24 | backdrop-filter: blur(2px) brightness(85%); 25 | display: none; 26 | } 27 | 28 | body:has(.modal.overlay:not([style*="display: none;"])) > .modal-overlay { 29 | display: block; 30 | } 31 | 32 | .modal.centered { 33 | position: absolute; 34 | left: 0; 35 | right: 0; 36 | bottom: 0; 37 | top: 0; 38 | margin: auto; 39 | z-index: 10; 40 | } 41 | 42 | .modal.overlay:not(.centered) { 43 | z-index: 11 !important; 44 | } 45 | 46 | .modal .content { 47 | padding: 1.5% 0 0 0; 48 | } 49 | 50 | .modal h1, .modal h2, .modal h3, .modal h4, .modal h5, .modal h6 { 51 | padding: 0; 52 | margin: 0; 53 | } 54 | 55 | /* Title chip */ 56 | .modal .title-chip { 57 | width: max-content; 58 | height: max-content; 59 | display: flex; 60 | align-items: center; 61 | justify-content: center; 62 | 63 | gap: 6px; 64 | background: var(--theme-primaryBgLight); 65 | padding: 8px; 66 | border-radius: 24px; 67 | 68 | font-size: 12pt; 69 | } 70 | 71 | .modal .title-chip .icon { 72 | background: var(--theme-primaryBgLighter); 73 | border-radius: 9999px; 74 | 75 | display: flex; 76 | align-items: center; 77 | justify-content: center; 78 | width: 24px; 79 | height: 24px; 80 | font-size: 18px; 81 | } 82 | 83 | /* Icon */ 84 | .modal > div:nth-child(1) > .material-symbols-outlined { 85 | background: var(--theme-primaryBgLight); 86 | padding: 10px; 87 | border-radius: 8px; 88 | } 89 | 90 | .modal > div:nth-child(1) { 91 | margin-bottom: 6px; 92 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FrogLauncher 2 | 3 | FrogLauncher - это Minecraft-лаунчер, который позволяет запускать любые версии игры, устанавливать моды и создавать модпаки. Поддерживает как пиратские, так и официальные аккаунты, а также имеет различные настройки оформления. 4 | 5 | ## Важные ссылки 6 | - [Сайт лаунчера](https://froglauncher.ru) 7 | - [Последний релиз лаунчера](https://github.com/Seeroy/FrogLauncher2/releases/latest) 8 | - [Плагин на сервер для поддержки системы скинов лаунчера](https://github.com/Seeroy/FrogLauncher-Skins-Plugin) 9 | 10 | ## Особенности 11 | 12 | - **Запуск любых (почти :D) версий Minecraft** 13 | - **Установка модов**: Простая установка модов с сервиса Modrinth 14 | - **Создание модпаков**: Доступно создание своих модпаков, а так же установка их с Modrinth 15 | - **Поддержка аккаунтов**: Используйте как пиратские, так и официальные аккаунты Minecraft 16 | - **Широкие возможности кастомизации**: Возможно изменение главного цвета, изображения фона лаунчера, тёмный и светлый режим 17 | 18 | ## Технологии 19 | 20 | FrogLauncher разработан с использованием следующих технологий: 21 | 22 | - **Electron** 23 | - **EJS** 24 | - **CSS** 25 | - **jQuery** 26 | - **[minecraft-launcher-core](https://github.com/Pierce01/MinecraftLauncher-core)** 27 | - **[tomate-loaders](https://github.com/doublekekse/tomate-loaders)** 28 | - **[MSMC](https://github.com/Hanro50/MSMC)** 29 | 30 | ## Установка 31 | 32 | 1. Клонируйте репозиторий: 33 | ```bash 34 | git clone https://github.com/Seeroy/FrogLauncher2.git 35 | cd FrogLauncher2 36 | ``` 37 | 38 | 2. Установите зависимости: 39 | ```bash 40 | npm install 41 | ``` 42 | 43 | 3. Запустите лаунчер: 44 | ```bash 45 | npm start 46 | ``` 47 | 48 | 4. Так же вы можете использовать режим разработчика: 49 | ```bash 50 | npm run dev 51 | ``` 52 | 53 | ## Лицензия 54 | 55 | Этот проект лицензируется под лицензией GPL-3.0. Подробности можно найти в файле [LICENSE](LICENSE). 56 | 57 | --- 58 | 59 | **FrogLauncher** - отличный выбор для тех, кто хочет разнообразить и улучшить свой Minecraft-опыт! 60 | -------------------------------------------------------------------------------- /web/sections/settings/java.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 | motion_photos_auto 4 |
5 |
6 | {{settings.java.auto.title}} 7 |
{{settings.java.auto.description}}
8 |
9 | 13 |
14 | 15 |
16 |
17 | folder 18 |
19 |
20 | {{settings.java.user}} 21 |
22 | 23 | 26 |
27 |
28 |
29 | 30 | 35 | 36 | -------------------------------------------------------------------------------- /web/css/button.css: -------------------------------------------------------------------------------- 1 | /* Обычная кнопка */ 2 | button { 3 | padding: 8px 18px; 4 | border-radius: 6px; 5 | 6 | display: flex; 7 | align-items: center; 8 | justify-content: center; 9 | gap: 8px; 10 | flex-direction: row; 11 | font-size: 18pt; 12 | cursor: pointer; 13 | 14 | height: max-content; 15 | width: max-content; 16 | 17 | background: var(--theme-primary); 18 | } 19 | 20 | button, button * { 21 | color: var(--theme-text); 22 | } 23 | 24 | button:hover { 25 | background: var(--theme-primaryDarker); 26 | } 27 | 28 | .button.pill { 29 | border-radius: 24px !important; 30 | padding: 6px 18px; 31 | } 32 | 33 | .button.pill.small { 34 | padding: 4px 26px; 35 | font-size: 15pt; 36 | } 37 | 38 | /* Квадратная кнопка */ 39 | button.square { 40 | width: 64px; 41 | height: 64px; 42 | 43 | padding: 0 !important; 44 | margin: 0 !important; 45 | display: flex; 46 | align-items: center; 47 | justify-content: center; 48 | } 49 | 50 | button.square:not(.small) .material-symbols-outlined { 51 | font-size: 24pt; 52 | } 53 | 54 | button.square.small { 55 | width: 48px; 56 | height: 48px; 57 | } 58 | 59 | /* Прозрачная кнопка */ 60 | button.transparent { 61 | background: transparent; 62 | color: var(--text-color); 63 | } 64 | 65 | button.transparent:hover { 66 | background: var(--theme-primaryBgLight); 67 | } 68 | 69 | /* Кнопка sub-цвета */ 70 | button.sub { 71 | background: var(--bg-sub); 72 | color: var(--text-color); 73 | } 74 | 75 | button.sub:hover { 76 | filter: brightness(90%); 77 | } 78 | 79 | /* Outline-кнопка */ 80 | button.outline { 81 | color: var(--text-sub); 82 | background: transparent; 83 | border: 2px solid var(--text-sub) !important; 84 | opacity: 0.77; 85 | } 86 | 87 | button.outline, 88 | button.outline * { 89 | color: var(--text-sub); 90 | } 91 | 92 | button.outline:hover { 93 | border-color: var(--text-color); 94 | opacity: 1; 95 | } 96 | 97 | button.outline:hover, 98 | button.outline:hover * { 99 | color: var(--text-color); 100 | } -------------------------------------------------------------------------------- /web/views/versionsPosters.css: -------------------------------------------------------------------------------- 1 | .versionsPostersWrapper { 2 | display: flex; 3 | align-items: center; 4 | overflow-y: auto; 5 | width: 100%; 6 | height: max-content; 7 | } 8 | 9 | .versionsPosters { 10 | display: grid; 11 | grid-auto-columns: 1fr; 12 | grid-template-columns: 1fr 1fr; 13 | grid-auto-rows: max-content; 14 | gap: 12px; 15 | grid-template-areas: 16 | ". ."; 17 | justify-content: center; 18 | align-content: center; 19 | 20 | grid-auto-flow: dense; 21 | 22 | width: 82vw; 23 | height: max-content; 24 | scroll-snap-align: center; 25 | scroll-snap-type: x mandatory; 26 | 27 | overflow: auto; 28 | } 29 | 30 | .versionsPosters.start-mode button { 31 | display: none !important; 32 | } 33 | 34 | .versionsPostersWrapper *::-webkit-scrollbar-thumb, 35 | .versionsPostersWrapper *::-webkit-scrollbar, 36 | .versionsPostersWrapper *::-webkit-scrollbar-track { 37 | height: 8px !important; 38 | } 39 | 40 | .versionsPostersWrapper *::-webkit-scrollbar-thumb { 41 | background: var(--theme-primaryBgLighter); 42 | } 43 | 44 | .versionsPostersWrapper *::-webkit-scrollbar-track { 45 | background: transparent; 46 | } 47 | 48 | /* Version item */ 49 | .versionsPosters .item { 50 | display: grid; 51 | grid-auto-columns: 1fr; 52 | grid-template-columns: max-content 1fr max-content; 53 | grid-template-rows: 1fr max-content max-content; 54 | gap: 2px; 55 | grid-template-areas: 56 | ". . ." 57 | "title title button" 58 | "description description button"; 59 | justify-content: center; 60 | align-content: center; 61 | 62 | padding: 18px; 63 | border-radius: 12px; 64 | 65 | background-size: cover; 66 | background-repeat: no-repeat; 67 | background-position: center; 68 | 69 | width: 100%; 70 | height: 24vh; 71 | 72 | flex: 0 0 auto; 73 | scroll-snap-align: start; 74 | } 75 | 76 | .versionsPosters .item .title { 77 | grid-area: title; 78 | font-size: 22pt; 79 | font-weight: 700; 80 | color: white; 81 | } 82 | 83 | .versionsPosters .item .description { 84 | grid-area: description; 85 | font-size: 13pt; 86 | color: rgba(235, 235, 235); 87 | } 88 | 89 | .versionsPosters .item button { 90 | grid-area: button; 91 | width: 48px; 92 | height: 48px; 93 | 94 | align-self: end; 95 | } -------------------------------------------------------------------------------- /web/modals/changelog.js: -------------------------------------------------------------------------------- 1 | const changelog_pageLimit = 25; 2 | let changelog_currentPage = 1; 3 | let changelog_endOfPage = false; 4 | 5 | const CHANGELOG_PRELOADER_PLACEHOLDER = `

${MESSAGES.loading.progress}

`; 6 | const CHANGELOG_FAILED_PLACEHOLDER = `

${MESSAGES.loading.failed}

`; 7 | const CHANGELOG_ITEM_PLACEHOLDER = `

$1

$2

$3
`; 8 | 9 | $(function () { 10 | $(document).on("showModalEvent", (e) => { 11 | if (e.originalEvent.detail.modal === "changelog" && $(".list .item").length === 0) { 12 | FrogChangelogUI.loadReleases(); 13 | } 14 | }) 15 | }) 16 | 17 | $("#modal-changelog .wrapper").scroll(function () { 18 | let wrapper = $("#modal-changelog .wrapper")[0]; 19 | if (wrapper.offsetHeight + wrapper.scrollTop >= wrapper.scrollHeight) { 20 | changelog_currentPage++; 21 | FrogChangelogUI.loadReleases(); 22 | } 23 | }); 24 | 25 | class FrogChangelogUI { 26 | static getReleases = (cb) => { 27 | $.get(`https://api.github.com/repos/${REPO_NAME}/releases?per_page=${changelog_pageLimit}&page=${changelog_currentPage}`, cb).fail(() => { 28 | $(".list").html(CHANGELOG_FAILED_PLACEHOLDER); 29 | }); 30 | } 31 | 32 | static loadReleases = () => { 33 | if (changelog_endOfPage === true) { 34 | return; 35 | } 36 | 37 | FrogChangelogUI.addPlaceholder(); 38 | FrogChangelogUI.getReleases((releases) => { 39 | if (releases !== false && Array.isArray(releases)) { 40 | releases.forEach((release) => { 41 | $("#modal-changelog .list").append(CHANGELOG_ITEM_PLACEHOLDER.replaceAll("$1", release.name).replaceAll("$2", release.body.replaceAll("\n", "
")).replaceAll("$3", release.published_at)); 42 | if (release.tag_name === "v2.0.0") { 43 | changelog_endOfPage = true; 44 | } 45 | }); 46 | } 47 | FrogChangelogUI.removePlaceholder(); 48 | }) 49 | } 50 | 51 | static addPlaceholder = () => { 52 | return $("#modal-changelog .list").append(CHANGELOG_PRELOADER_PLACEHOLDER); 53 | } 54 | 55 | static removePlaceholder = () => { 56 | return $("#modal-changelog .list .placeholder").remove(); 57 | } 58 | } -------------------------------------------------------------------------------- /web/js/FrogPlayStats.js: -------------------------------------------------------------------------------- 1 | let gameStatsData; 2 | let currentGameCounter; 3 | 4 | class FrogPlayStats { 5 | // Загрузить всю сохранённую статистику из конфига 6 | static getAllStats = () => { 7 | gameStatsData = FrogConfig.read("gameStatistics", {}); 8 | return gameStatsData; 9 | } 10 | 11 | // Получить статистику по версии 12 | static getByID = (versionId) => { 13 | if (typeof gameStatsData[versionId] === "undefined") { 14 | return false; 15 | } 16 | 17 | return gameStatsData[versionId]; 18 | } 19 | 20 | // Получить список версий отсортированный по последнему времени запуска 21 | static getByLastStarted = () => { 22 | if (Object.values(gameStatsData).length === 0) return []; 23 | 24 | return Object.values(Object.entries(gameStatsData) 25 | .sort(([, a], [, b]) => b.lastStart - a.lastStart) 26 | .reduce((r, [k, v]) => ({...r, [k]: v}), {})); 27 | } 28 | 29 | // Получить список версий отсортированный по времени в игре 30 | static getByIngameTime = () => { 31 | if (Object.values(gameStatsData).length === 0) return []; 32 | 33 | return Object.values(Object.entries(gameStatsData) 34 | .sort(([, a], [, b]) => b.inGame - a.inGame) 35 | .reduce((r, [k, v]) => ({...r, [k]: v}), {})); 36 | } 37 | 38 | // Сохранить статистику 39 | static saveStats = () => { 40 | return FrogConfig.write("gameStatistics", gameStatsData); 41 | } 42 | 43 | // Вызывается при запуске игры 44 | static onGameLaunch = (versionId) => { 45 | if (typeof gameStatsData[versionId] === "undefined") { 46 | gameStatsData[versionId] = { 47 | id: versionId, 48 | parsed: FrogVersionsManager.parseVersionID(versionId), 49 | starts: 0, 50 | inGame: 0, 51 | lastStart: 0 52 | } 53 | } 54 | gameStatsData[versionId].starts++; 55 | gameStatsData[versionId].lastStart = Date.now(); 56 | currentGameCounter = setInterval(() => { 57 | gameStatsData[versionId].inGame++; 58 | }, 1000); 59 | FrogPlayStats.saveStats(); 60 | return true; 61 | } 62 | 63 | // Вызывается при завершении игры 64 | static onGameClose = () => { 65 | clearInterval(currentGameCounter); 66 | FrogPlayStats.saveStats(); 67 | return true; 68 | } 69 | } -------------------------------------------------------------------------------- /web/css/globals.css: -------------------------------------------------------------------------------- 1 | /* Элементы */ 2 | @import "./resets.css"; 3 | @import "./layout.css"; 4 | @import "./progress.css"; 5 | @import "./slider.css"; 6 | @import "./misc.css"; 7 | @import "./button.css"; 8 | @import "./tabs.css"; 9 | @import "./custom-select.css"; 10 | @import "./checkbox.css"; 11 | @import "./checksList.css"; 12 | @import "./spinner.css"; 13 | @import "./modal.css"; 14 | @import "./colorPicker.css"; 15 | @import "./switch.css"; 16 | @import "./scrollbar.css"; 17 | @import "./chips.css"; 18 | @import "./input.css"; 19 | @import "./toast.css"; 20 | @import "./alert.css"; 21 | @import "./link.css"; 22 | @import "./link_openInNew.css"; 23 | 24 | @import "./themes/dark.css"; 25 | @import "./themes/light.css"; 26 | 27 | /* Шрифты */ 28 | @import "../assets/fonts/MaterialSymbols/font.css"; 29 | 30 | @font-face { 31 | font-family: "Manrope"; 32 | src: url("../assets/fonts/Manrope.ttf") format("truetype"); 33 | } 34 | 35 | @font-face { 36 | font-family: "Fira Code"; 37 | src: url("../assets/fonts/FiraCode.ttf") format("truetype"); 38 | } 39 | 40 | body, 41 | html { 42 | padding: 0; 43 | margin: 0; 44 | background: transparent; 45 | overflow: hidden; 46 | width: 100%; 47 | height: 100%; 48 | border-radius: 32px; 49 | color: var(--text-color); 50 | } 51 | 52 | html.noEffects, 53 | html.noEffects *:not(.icon img):not(.progress-pill):not(.progress-pill *):not(.line-spinner) { 54 | filter: none !important; 55 | box-shadow: none !important; 56 | } 57 | 58 | html.noAnimations, 59 | html.noAnimations *:not(.spinner):not(.progress-pill):not(.progress-pill *):not(.line-spinner) { 60 | transition: 0s !important; 61 | animation-duration: 0.001s !important; 62 | } 63 | 64 | html.noAnimations #aboutItem { 65 | animation: none; 66 | animation-duration: 0s; 67 | } 68 | 69 | * { 70 | transition: 0.2s; 71 | font-family: Manrope, sans-serif; 72 | box-sizing: border-box; 73 | -webkit-user-select: none; 74 | -webkit-app-region: no-drag; 75 | overflow: hidden; 76 | } 77 | 78 | .material-symbols-rounded, 79 | .material-symbols-outlined, 80 | .material-symbols-sharp { 81 | margin: 0; 82 | padding: 0; 83 | height: max-content; 84 | width: max-content; 85 | } 86 | 87 | :root { 88 | --bradius: 8px; 89 | --success: #29ae6b; 90 | --error: #f04438; 91 | --warning: #fdb022; 92 | --info: #53b1fd; 93 | } 94 | 95 | dialog::backdrop { 96 | backdrop-filter: blur(2px); 97 | } -------------------------------------------------------------------------------- /web/sections/packsAndMods/filters.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | filter_alt 5 |

{{packs.filters.title}}

6 |
7 | 8 | 9 |
10 | merge_type 11 | 12 |

{{packs.filters.type}}

13 |
14 |
15 |
16 |
17 | deployed_code_update 18 |
19 | {{packs.filters.mods}} 20 |
21 |
22 |
23 | package_2 24 |
25 | {{packs.filters.packs}} 26 |
27 |
28 |
29 | landscape 30 |
31 | {{packs.filters.rp}} 32 |
33 |
34 |
35 | light_mode 36 |
37 | {{packs.filters.shaders}} 38 |
39 |
40 | 41 |
42 | electric_bolt 43 | 44 |

{{packs.filters.loaders}}

45 |
46 |
47 |
48 |
49 | 50 |
51 | conversion_path 52 | 53 |

{{packs.filters.versions}}

54 |
55 |
56 |
57 |
58 | 59 |
60 |
-------------------------------------------------------------------------------- /web/modals/servers.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /web/views/flyout.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
6 | 7 | 16 | 17 |
18 | 19 | 28 | 29 | 37 | 38 | 42 | 43 | 47 | 48 | 49 | 53 |
-------------------------------------------------------------------------------- /web/views/startAnimation.css: -------------------------------------------------------------------------------- 1 | .loading-screen { 2 | position: absolute; 3 | left: 0; 4 | right: 0; 5 | top: 0; 6 | bottom: 0; 7 | z-index: 2; 8 | width: 100vw; 9 | height: 100vh; 10 | display: flex; 11 | align-items: center; 12 | justify-content: center; 13 | flex-direction: column; 14 | gap: 16px; 15 | backdrop-filter: blur(24px); 16 | } 17 | 18 | .loading-screen .caption { 19 | font-weight: 800; 20 | font-size: 48pt; 21 | 22 | overflow: hidden; 23 | white-space: nowrap; 24 | color: white; 25 | 26 | animation-timing-function: ease-in; 27 | animation: fade 1.2s; 28 | } 29 | 30 | .loading-screen .description { 31 | font-weight: 600; 32 | font-size: 16pt; 33 | 34 | overflow: hidden; 35 | white-space: nowrap; 36 | color: white; 37 | margin-top: 16px; 38 | 39 | animation-timing-function: ease-in; 40 | animation: fade 1.2s; 41 | } 42 | 43 | .loading-screen img { 44 | animation-timing-function: ease-in-out; 45 | width: 384px; 46 | transition: 1.2s !important; 47 | clip-path: circle(1%); 48 | } 49 | 50 | .loading-screen img.uncircle { 51 | clip-path: circle(80%); 52 | } 53 | 54 | /* Animate in */ 55 | .loading-screen.animateIn .caption { 56 | animation: fadeIn 1.2s; 57 | } 58 | 59 | /* Animate out */ 60 | .loading-screen.animateOut img { 61 | animation: zoomFadeOutBig 1.06s; 62 | } 63 | 64 | .loading-screen.animateOut .caption { 65 | animation: fadeOut 1.06s; 66 | } 67 | 68 | .loading-screen.animateOut { 69 | animation: fadeOutLinear 1s; 70 | } 71 | 72 | .hide-on-loading { 73 | display: none !important; 74 | opacity: 0 !important; 75 | } 76 | 77 | @keyframes zoomFadeIn { 78 | 0% { 79 | opacity: 0; 80 | transform: scale(0); 81 | } 82 | 83 | 100% { 84 | opacity: 1; 85 | transform: scale(1); 86 | } 87 | } 88 | 89 | @keyframes zoomFadeOutBig { 90 | 0% { 91 | opacity: 1; 92 | transform: scale(1); 93 | } 94 | 95 | 100% { 96 | opacity: 0; 97 | transform: scale(2.2); 98 | } 99 | } 100 | 101 | @keyframes fadeIn { 102 | 0% { 103 | opacity: 0; 104 | } 105 | 100% { 106 | opacity: 1; 107 | } 108 | } 109 | 110 | @keyframes fadeOut { 111 | 0% { 112 | opacity: 1; 113 | } 114 | 50% { 115 | opacity: 0.01; 116 | } 117 | 100% { 118 | opacity: 0; 119 | } 120 | } 121 | 122 | @keyframes fadeOutLinear { 123 | 0% { 124 | opacity: 1; 125 | } 126 | 100% { 127 | opacity: 0; 128 | } 129 | } -------------------------------------------------------------------------------- /web/modals/elybyLogin.js: -------------------------------------------------------------------------------- 1 | class FrogElybyLoginUI { 2 | static login = () => { 3 | let login = $("#modal-elybyLogin input.login").val(); 4 | let password = $("#modal-elybyLogin input.password").val(); 5 | let totp = $("#modal-elybyLogin input.totp").val(); 6 | 7 | let $error = $("#modal-elybyLogin .error"); 8 | 9 | if (login !== "" && password !== "") { 10 | $("#modal-elybyLogin .loginBtn").hide(); 11 | $error.hide(); 12 | 13 | FrogElybyManager.loginAccount(login, password, totp).then((data) => { 14 | $("#modal-elybyLogin .loginBtn").show(); 15 | $("#modal-elybyLogin input.totp").val(""); 16 | 17 | let isSuccess = data[0]; 18 | let clientToken = data[2]; 19 | data = data[1]; 20 | 21 | if (!isSuccess) { 22 | $error.show(); 23 | // Показываем ошибку 24 | if (typeof MESSAGES.elyby[data] !== "undefined") { 25 | $error.text(MESSAGES.elyby[data]); 26 | } else { 27 | $error.text(data); 28 | } 29 | return false; 30 | } else { 31 | if (FrogAccountsManager.isAccountExistsByNickname(data.selectedProfile.name, "elyby")) { 32 | FrogToasts.create(MESSAGES.accounts.alreadyExists); 33 | return false; 34 | } 35 | 36 | // Создаём аккаунт 37 | let accountData = { 38 | type: "elyby", 39 | nickname: data.selectedProfile.name, 40 | added: Date.now(), 41 | clientToken: clientToken, 42 | accessToken: data.accessToken, 43 | uuid: data.selectedProfile.id, 44 | } 45 | 46 | let accountsList = FrogAccountsManager.getAccounts(); 47 | accountsList[data.selectedProfile.id] = accountData; 48 | FrogElybyManager.getHeadURLByPlayerNickname(data.selectedProfile.name).then(() => { 49 | FrogAccountsManager.saveAccounts(accountsList); 50 | }); 51 | 52 | $("#modal-elybyLogin input").val(""); 53 | FrogModals.hideModal("elybyLogin").then(() => { 54 | FrogToasts.create(data.selectedProfile.name, "person", MESSAGES.commons.newAccountAdded); 55 | }); 56 | return true; 57 | } 58 | }); 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /web/modals/versions.css: -------------------------------------------------------------------------------- 1 | #modal-versions { 2 | position: absolute; 3 | z-index: 3; 4 | 5 | padding: 12px; 6 | 7 | width: 56vw; 8 | height: 52vh; 9 | 10 | bottom: calc(12% + 24px); 11 | left: calc(24% + 6px); 12 | 13 | background: var(--theme-primaryBg); 14 | 15 | display: flex; 16 | flex-direction: column; 17 | gap: 12px; 18 | } 19 | 20 | /* Versions list */ 21 | #modal-versions .versions-list { 22 | padding: 4px; 23 | flex-grow: 1; 24 | display: flex; 25 | flex-direction: column; 26 | align-items: center; 27 | gap: 8px; 28 | height: max-content; 29 | 30 | background: var(--theme-primaryBg); 31 | border-radius: 8px; 32 | } 33 | 34 | #modal-versions .ver-list-wrapper { 35 | flex-grow: 1; 36 | overflow: auto; 37 | height: 77%; 38 | } 39 | 40 | /* Versions list item */ 41 | #modal-versions .versions-list .item { 42 | display: flex; 43 | align-items: center; 44 | justify-content: center; 45 | gap: 6px; 46 | 47 | height: max-content; 48 | padding: 6px 12px; 49 | 50 | width: 100%; 51 | } 52 | 53 | #modal-versions .versions-list .item .favorite { 54 | cursor: pointer; 55 | } 56 | 57 | #modal-versions .versions-list .item .favorite:hover { 58 | filter: brightness(75%); 59 | } 60 | 61 | #modal-versions .versions-list .item .favorite.active { 62 | color: var(--warning); 63 | } 64 | 65 | #modal-versions .versions-list .item .title { 66 | flex-grow: 1; 67 | } 68 | 69 | #modal-versions .versions-list .item .icon { 70 | position: relative; 71 | display: inline-block; 72 | } 73 | 74 | #modal-versions .versions-list .item .icon { 75 | object-fit: cover; 76 | 77 | width: 44px; 78 | height: 44px; 79 | } 80 | 81 | #modal-versions .versions-list .item .installed { 82 | grid-area: check; 83 | align-self: center; 84 | justify-self: center; 85 | color: var(--success); 86 | } 87 | 88 | #modal-versions .versions-list .item:not([data-installed="true"]) .installed { 89 | display: none !important; 90 | } 91 | 92 | #modal-versions .versions-list .item.active { 93 | outline: 3px solid var(--theme-primary); 94 | } 95 | 96 | #modal-versions .preloader { 97 | width: 100%; 98 | height: 100%; 99 | display: flex; 100 | align-items: center; 101 | justify-content: center; 102 | flex-direction: column; 103 | gap: 12px; 104 | } 105 | 106 | /* Version types selector */ 107 | #modal-versions #versionTypeSelect .chip { 108 | cursor: pointer; 109 | } 110 | 111 | #modal-versions #versionTypeSelect .chip.active { 112 | color: var(--theme-text); 113 | background: var(--theme-primary); 114 | } -------------------------------------------------------------------------------- /web/css/layout.css: -------------------------------------------------------------------------------- 1 | .background { 2 | position: absolute; 3 | left: 0; 4 | right: 0; 5 | top: 0; 6 | bottom: 0; 7 | width: 100vw; 8 | height: 100vh; 9 | z-index: -1000; 10 | } 11 | 12 | .background.img { 13 | background-position: center; 14 | background-repeat: no-repeat; 15 | background-size: cover; 16 | } 17 | 18 | .background.img-1 { 19 | background-image: url("../assets/backgrounds/1.webp"); 20 | } 21 | 22 | .background.img-2 { 23 | background-image: url("../assets/backgrounds/2.webp"); 24 | } 25 | 26 | .background.img-3 { 27 | background-image: url("../assets/backgrounds/3.webp"); 28 | } 29 | 30 | .background.img-4 { 31 | background-image: url("../assets/backgrounds/4.webp"); 32 | } 33 | 34 | .background.img-5 { 35 | background-image: url("../assets/backgrounds/5.webp"); 36 | } 37 | 38 | .background::after { 39 | content: ""; 40 | position: absolute; 41 | width: 100%; 42 | height: 100%; 43 | backdrop-filter: blur(4px) brightness(75%); 44 | pointer-events: none; 45 | } 46 | 47 | body > .layout { 48 | display: grid; 49 | height: 100%; 50 | width: 100%; 51 | padding: 2% 3%; 52 | grid-template-columns: max-content 1fr; 53 | grid-template-rows: max-content 1fr max-content; 54 | gap: 24px 8px; 55 | grid-auto-flow: row; 56 | grid-template-areas: 57 | "title-wrapper title-wrapper" 58 | "flymenu content" 59 | "flyout flyout"; 60 | } 61 | 62 | body > .layout > .title-wrapper { 63 | grid-area: title-wrapper; 64 | display: flex; 65 | } 66 | 67 | body > .layout > .title-wrapper .title, 68 | body > .layout > .title-wrapper .title *:not(img) { 69 | -webkit-app-region: drag; 70 | color: white; 71 | } 72 | 73 | body > .layout > .title-wrapper .title { 74 | flex-grow: 1; 75 | } 76 | 77 | body > .layout > .title-wrapper .title span { 78 | font-size: 21pt; 79 | font-weight: 700; 80 | } 81 | 82 | body > .layout .flymenu { 83 | align-self: center; 84 | grid-area: flymenu; 85 | overflow: visible; 86 | } 87 | 88 | body > .layout .content { 89 | grid-area: content; 90 | overflow: visible; 91 | min-height: 73vh; 92 | } 93 | 94 | body > .layout .windowButtons { 95 | justify-self: end; 96 | align-self: center; 97 | grid-area: windowButtons; 98 | } 99 | 100 | body > .layout .flyout { 101 | width: 100%; 102 | justify-self: center; 103 | align-self: center; 104 | grid-area: flyout; 105 | } 106 | 107 | .global-wrapper, .layout, .background { 108 | border-radius: 16px !important; 109 | overflow: hidden; 110 | } 111 | 112 | .global-wrapper { 113 | border: 6px solid var(--theme-primaryBgShadow); 114 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frogLauncher2", 3 | "version": "2.1.14", 4 | "description": "Minecraft-лаунчер. Быстрый. Индивидуальный. Твой", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "electron .", 8 | "verify": "node ./verifyTranslations.js", 9 | "dev": "cross-env LAUNCHER_IN_DEV=yes electron .", 10 | "build-win": "electron-builder build --win --publish never", 11 | "build-linux": "electron-builder build --linux --publish never", 12 | "test": "cross-env LAUNCHER_IN_DEV=yes LAUNCHER_IN_TEST=yes electron ." 13 | }, 14 | "author": "Seeroy", 15 | "license": "GPL-3.0", 16 | "homepage": "https://froglauncher.ru", 17 | "bugs": "https://github.com/Seeroy/FrogLauncher2/issues", 18 | "repository": "https://github.com/Seeroy/FrogLauncher2", 19 | "devDependencies": { 20 | "cross-env": "^7.0.3", 21 | "electron": "^34.2.0", 22 | "electron-builder": "^25.1.8", 23 | "electron-reloader": "^1.2.3" 24 | }, 25 | "dependencies": { 26 | "colors": "^1.4.0", 27 | "compressing": "^1.10.1", 28 | "console-stamp": "^3.1.2", 29 | "ejs-electron": "^3.0.0", 30 | "electron-updater": "^6.3.9", 31 | "fs-extra": "^11.3.0", 32 | "gamedig": "^5.2.0", 33 | "jimp": "^0.22.12", 34 | "jquery": "^3.7.1", 35 | "machine-uuid": "^1.2.0", 36 | "mcnbt": "^2.0.3", 37 | "minecraft-launcher-core": "^3.18.2", 38 | "msmc": "^5.0.5", 39 | "open": "^8.4.2", 40 | "request": "^2.88.2", 41 | "tomate-loaders": "^2.0.2", 42 | "toml": "^3.0.0", 43 | "tree-kill": "^1.2.2" 44 | }, 45 | "build": { 46 | "productName": "FrogLauncher", 47 | "appId": "ru.seeroy.froglauncher", 48 | "copyright": "© 2024 Seeroy", 49 | "directories": { 50 | "buildResources": "resources" 51 | }, 52 | "linux": { 53 | "target": [ 54 | "deb", 55 | "rpm", 56 | "tar.gz" 57 | ], 58 | "maintainer": "Seeroy", 59 | "vendor": "Seeroy", 60 | "icon": "web/assets/icon.png" 61 | }, 62 | "win": { 63 | "asar": true, 64 | "target": "nsis", 65 | "icon": "resources/icon.ico" 66 | }, 67 | "publish": { 68 | "provider": "generic", 69 | "url": "https://cdn.froglauncher.ru/updates" 70 | }, 71 | "nsis": { 72 | "installerIcon": "resources/icon.ico", 73 | "installerHeaderIcon": "resources/icon.ico", 74 | "deleteAppDataOnUninstall": false, 75 | "oneClick": false, 76 | "allowToChangeInstallationDirectory": true 77 | }, 78 | "files": [ 79 | "**/*", 80 | "node_modules/**/*", 81 | "!logs{/*}", 82 | "!logs/", 83 | "!logs", 84 | "!cache{/*}", 85 | "!config.json", 86 | "!frog_accounts.json", 87 | "!LICENSE", 88 | "!README.md", 89 | "!.gitignore", 90 | "!.git{/*}" 91 | ] 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /web/modals/versions.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 38 | 39 | -------------------------------------------------------------------------------- /web/js/FrogErrorsParser.js: -------------------------------------------------------------------------------- 1 | let isMCErrorShown = false; 2 | 3 | const ERRORS_DESCRIPTIONS = MESSAGES.errors.list; 4 | 5 | const ERRORS_MESSAGES = { 6 | "java.lang.ClassCastException: class jdk.internal.loader": 7 | ERRORS_DESCRIPTIONS[0], 8 | "java.lang.NoSuchMethodError: sun.security.util.ManifestEntryVerifier": 9 | ERRORS_DESCRIPTIONS[1], 10 | "java.lang.UnsupportedClassVersionError": ERRORS_DESCRIPTIONS[0], 11 | "Could not reserve enough space": ERRORS_DESCRIPTIONS[2], 12 | "Main has been compiled by a more recent": ERRORS_DESCRIPTIONS[0], 13 | "The system cannot find the path specified": ERRORS_DESCRIPTIONS[4], 14 | "at java.base/java.io.Reader.": ERRORS_DESCRIPTIONS[5], 15 | "requires version": ERRORS_DESCRIPTIONS[0], 16 | "java.io.IOException: error reading": ERRORS_DESCRIPTIONS[5], 17 | "ava.lang.NoClassDefFoundError: com/mojang/authlib/properties/PropertyMap": ERRORS_DESCRIPTIONS[6], 18 | "Failed to start due to Error: ENOENT: no such file or directory": ERRORS_DESCRIPTIONS[6], 19 | "java.lang.NoSuchMethodError": ERRORS_DESCRIPTIONS[7], 20 | "[authlib-injector] [ERROR] Failed to fetch metadata": ERRORS_DESCRIPTIONS[8] 21 | }; 22 | 23 | class FrogErrorsParser { 24 | static parse(line = "", exitCode = 0) { 25 | let errorHappend = false; 26 | if (line === "" && exitCode) { 27 | if (exitCode > 0 && exitCode !== 127 && exitCode !== 255) { 28 | FrogCollector.writeLog(`Crash: Exit code ${exitCode}`); 29 | if (isMCErrorShown === false) { 30 | FrogAlerts.create( 31 | MESSAGES.errors.title, 32 | `${MESSAGES.errors.exitCode}: ${exitCode}`, 33 | MESSAGES.commons.close, 34 | "error", 35 | () => { 36 | isMCErrorShown = false; 37 | } 38 | ); 39 | isMCErrorShown = true; 40 | } 41 | } else { 42 | FrogCollector.writeLog(`Crash: Force terminated with code ${exitCode}`); 43 | } 44 | } else { 45 | for (const [key, value] of Object.entries(ERRORS_MESSAGES)) { 46 | let nreg = new RegExp(key, "gmi"); 47 | if (line.match(nreg) != null && isMCErrorShown === false) { 48 | isMCErrorShown = true; 49 | FrogAlerts.create( 50 | MESSAGES.errors.title, 51 | value, 52 | MESSAGES.commons.close, 53 | "warning", 54 | () => { 55 | isMCErrorShown = false; 56 | } 57 | ); 58 | errorHappend = true; 59 | } 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /web/modals/frogSkin.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 33 | 34 | -------------------------------------------------------------------------------- /web/modals/accounts.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 50 | 51 | -------------------------------------------------------------------------------- /web/modals/packManager.css: -------------------------------------------------------------------------------- 1 | /* Main window */ 2 | #modal-packManager { 3 | width: 94vw; 4 | height: 94vh; 5 | padding: 21px; 6 | 7 | display: flex; 8 | flex-direction: column; 9 | gap: 12px; 10 | } 11 | 12 | #modal-packManager .title-wrapper img.icon { 13 | width: 64px; 14 | height: 64px; 15 | border-radius: 8px; 16 | } 17 | 18 | #modal-packManager .title-wrapper .description { 19 | font-size: 12pt; 20 | color: var(--text-sub); 21 | } 22 | 23 | #modal-packManager .title-wrapper .delete { 24 | background: var(--error); 25 | } 26 | 27 | #modal-packManager .title-wrapper .play { 28 | background: var(--success); 29 | } 30 | 31 | #modal-packManager .title-wrapper .export { 32 | background: var(--info); 33 | } 34 | 35 | #modal-packManager .title-wrapper button.icon { 36 | background: var(--theme-primaryBgLighter); 37 | } 38 | 39 | /* Tabs */ 40 | #modal-packManager .tabs .tab { 41 | padding: 12px; 42 | } 43 | 44 | #modal-packManager .tabs button { 45 | padding: 0 10px !important; 46 | height: 40px; 47 | width: max-content; 48 | min-width: max-content; 49 | } 50 | 51 | #modal-packManager .tabs button span { 52 | font-size: 13pt; 53 | } 54 | 55 | /* Layout */ 56 | #modal-packManager .layout-tab .scroll-wrap { 57 | padding: 12px; 58 | overflow-y: auto; 59 | max-height: 50vh; 60 | border-radius: 8px; 61 | } 62 | 63 | #modal-packManager .layout-tab .scroll-wrap > div { 64 | display: flex; 65 | flex-direction: column; 66 | width: 100%; 67 | height: max-content; 68 | gap: 8px; 69 | } 70 | 71 | #modal-packManager .layout-tab .scroll-wrap > div .item { 72 | background: var(--theme-primaryBgLight); 73 | padding: 14px; 74 | gap: 4px 14px; 75 | height: max-content; 76 | } 77 | 78 | #modal-packManager .layout-tab .scroll-wrap > div .item .description { 79 | color: var(--text-sub); 80 | opacity: 0.9; 81 | text-transform: none; 82 | font-size: 13pt; 83 | font-weight: 300; 84 | } 85 | 86 | #modal-packManager .layout-tab:not(.active) { 87 | display: none; 88 | } 89 | 90 | /* Item */ 91 | #modal-packManager .layout-tab .custom-select.icon-and-description:hover { 92 | filter: none; 93 | cursor: default; 94 | } 95 | 96 | #modal-packManager .layout-tab .custom-select.icon-and-description button:hover { 97 | filter: brightness(115%); 98 | } 99 | 100 | #modal-packManager .layout-tab .custom-select.icon-and-description { 101 | grid-template-columns: max-content 40px 1fr max-content; 102 | grid-template-areas: 103 | "switch icon title button" 104 | "switch icon description button"; 105 | } 106 | 107 | #modal-packManager .layout-tab .custom-select.icon-and-description .switch { 108 | justify-self: center; 109 | align-self: center; 110 | grid-area: switch; 111 | 112 | width: 48px; 113 | height: 28px; 114 | } 115 | 116 | #modal-packManager .layout-tab .custom-select.icon-and-description input:not(:checked) + .inner:before { 117 | left: 0; 118 | } 119 | 120 | #modal-packManager .layout-tab .custom-select.icon-and-description .switch .inner:before { 121 | height: 21px; 122 | width: 21px; 123 | left: -8px; 124 | bottom: 3px; 125 | } -------------------------------------------------------------------------------- /web/modals/servers.css: -------------------------------------------------------------------------------- 1 | #modal-servers { 2 | display: flex; 3 | flex-direction: column; 4 | gap: 6px; 5 | } 6 | 7 | #modal-servers .serversListWrapper { 8 | width: 100%; 9 | height: 80%; 10 | overflow: auto; 11 | 12 | display: flex; 13 | justify-content: center; 14 | flex-grow: 1; 15 | } 16 | 17 | #modal-servers .serversList { 18 | display: flex; 19 | flex-direction: column; 20 | width: 70%; 21 | height: max-content; 22 | 23 | align-items: center; 24 | justify-content: center; 25 | gap: 8px; 26 | } 27 | 28 | #modal-servers .serversList .item { 29 | background: var(--theme-primaryBgLight); 30 | border-radius: 16px; 31 | 32 | padding: 6px 20px; 33 | 34 | width: 100%; 35 | height: max-content; 36 | display: flex; 37 | align-items: center; 38 | justify-content: center; 39 | gap: 16px; 40 | } 41 | 42 | #modal-servers .serversList .item .caption { 43 | font-weight: 700; 44 | font-size: 16pt; 45 | color: var(--text-color); 46 | } 47 | 48 | #modal-servers .serversList .item .description { 49 | font-weight: 500; 50 | font-size: 11pt; 51 | color: var(--text-sub); 52 | } 53 | 54 | /* Кнопки и чипы */ 55 | #modal-servers .serversList .item .ip, 56 | #modal-servers .serversList .item .version { 57 | width: max-content; 58 | height: 32px; 59 | background: var(--theme-primaryBgLighter); 60 | padding: 6px; 61 | border-radius: 4px; 62 | display: flex; 63 | align-items: center; 64 | justify-content: center; 65 | margin: 4px 0; 66 | 67 | gap: 8px; 68 | } 69 | 70 | #modal-servers .serversList .item .ip *, 71 | #modal-servers .serversList .item .version * { 72 | font-size: 11pt; 73 | color: var(--text-sub); 74 | } 75 | 76 | #modal-servers .serversList .item .copy-btn, 77 | #modal-servers .serversList .item .play-btn { 78 | margin: 0 8px; 79 | } 80 | 81 | #modal-servers .serversList .item .copy-btn .material-symbols-outlined, 82 | #modal-servers .serversList .item .play-btn .material-symbols-outlined { 83 | font-size: 21pt; 84 | color: var(--theme-primaryLighter); 85 | cursor: pointer; 86 | } 87 | 88 | #modal-servers .serversList .item .copy-btn .material-symbols-outlined:hover, 89 | #modal-servers .serversList .item .play-btn .material-symbols-outlined:hover { 90 | color: var(--text-sub); 91 | } 92 | 93 | #modal-servers .disclaimer { 94 | color: var(--text-sub); 95 | font-size: 11pt; 96 | text-align: center; 97 | } 98 | 99 | /* Иконка со статусом */ 100 | #modal-servers .serversList .item .icon { 101 | position: relative; 102 | display: inline-block; 103 | } 104 | 105 | #modal-servers .serversList .item .icon img { 106 | object-fit: cover; 107 | } 108 | 109 | #modal-servers .serversList .item .icon .status { 110 | border: 2px solid var(--theme-primaryBgLight); 111 | position: absolute; 112 | bottom: 2%; 113 | right: -1%; 114 | border-radius: 99999px; 115 | 116 | width: 18px; 117 | height: 18px; 118 | 119 | background: var(--warning); 120 | } 121 | 122 | #modal-servers .serversList .item .icon .status.offline { 123 | background: var(--error); 124 | } 125 | 126 | #modal-servers .serversList .item .icon .status.online { 127 | background: var(--success); 128 | } -------------------------------------------------------------------------------- /web/js/renderer.js: -------------------------------------------------------------------------------- 1 | window.$ = window.jQuery = require("jquery"); 2 | const fs = require("fs"); 3 | const fsPromises = require("fs/promises"); 4 | const fsExtra = require("fs-extra"); 5 | const path = require("path"); 6 | const os = require("os"); 7 | const {ipcRenderer} = require("electron"); 8 | const crypto = require("crypto"); 9 | const openExternal = require("open"); 10 | const request = require("request"); 11 | const {Client, Authenticator} = require("minecraft-launcher-core"); 12 | const {Auth} = require("msmc"); 13 | const {forge, neoforge, fabric, quilt, vanilla, liner} = require("tomate-loaders"); 14 | const machineUuid = require("machine-uuid"); 15 | const packageJson = require("./../package.json"); 16 | const Jimp = require("jimp"); 17 | const compressing = require('compressing'); 18 | const treeKill = require("tree-kill"); 19 | const NBT = require('mcnbt'); 20 | const {exec} = require('node:child_process'); 21 | let toml = require('toml'); 22 | const {GameDig} = require('gamedig'); 23 | 24 | // Версия лаунчера 25 | global.LAUNCHER_VERSION = packageJson.version; 26 | 27 | // Ссылки на сайт 28 | global.SITE_URL = "https://froglauncher.ru"; 29 | global.CDN_URL = "https://cdn.froglauncher.ru"; 30 | global.SKINS_API_URL = "https://skins.froglauncher.ru"; 31 | global.STATS_URL = "https://statscol.seeeroy.ru/save_fl?savedata="; 32 | //global.NEWS_URL = CDN_URL + "/news.json"; // Перемещено в index.ejs 33 | global.SERVERS_URL = CDN_URL + `/servers.json?_=${Date.now()}`; 34 | global.AUTHLIB_INJECTOR_URL = CDN_URL + "/authlib-injector.jar"; 35 | global.REPO_NAME = "Seeroy/FrogLauncher2"; 36 | 37 | // Ссылка на Java 38 | global.JAVA_LIST_URL = "https://api.adoptium.net/v3/info/available_releases"; 39 | 40 | // Последние логи лаунчера 41 | global.LAST_LOG = ""; 42 | 43 | // Лаунчер в dev mode? test mode? 44 | global.IS_APP_IN_DEV = ipcRenderer.sendSync("isAppInDev"); 45 | global.IS_APP_IN_TEST = ipcRenderer.sendSync("isAppInTest"); 46 | 47 | if (IS_APP_IN_DEV) { 48 | //global.SKINS_API_URL = "http://localhost:58883"; 49 | } 50 | 51 | // Пути к файлам 52 | global.USERDATA_PATH = path.normalize(ipcRenderer.sendSync("get-userdata-path")); 53 | global.CONFIG_PATH = path.join(global.USERDATA_PATH, "config.json"); 54 | if (process.platform === "win32") { 55 | global.DOT_MC_PATH = path.join(os.homedir(), "AppData", "Roaming", ".minecraft"); 56 | } else { 57 | global.DOT_MC_PATH = path.join(os.homedir(), ".minecraft"); 58 | } 59 | 60 | const animateCSS = (element, animation, fast = true, prefix = "animate__") => { 61 | const animationName = `${prefix}${animation}`; 62 | const node = document.querySelector(element); 63 | 64 | return animateCSSNode(node); 65 | }; 66 | 67 | const animateCSSNode = (node, animation, fast = true, prefix = "animate__") => { 68 | return new Promise((resolve, reject) => { 69 | const animationName = `${prefix}${animation}`; 70 | 71 | if (fast === true) { 72 | node.classList.add(`${prefix}animated`, animationName, `${prefix}faster`); 73 | } else { 74 | node.classList.add(`${prefix}animated`, animationName); 75 | } 76 | 77 | function handleAnimationEnd(event) { 78 | event.stopPropagation(); 79 | node.classList.remove( 80 | `${prefix}animated`, 81 | animationName, 82 | `${prefix}faster` 83 | ); 84 | resolve("Animation ended"); 85 | } 86 | 87 | node.addEventListener("animationend", handleAnimationEnd, {once: true}); 88 | }); 89 | } -------------------------------------------------------------------------------- /web/js/FrogServersUI.js: -------------------------------------------------------------------------------- 1 | class FrogServersUI { 2 | // Загрузить список серверов 3 | static loadList = async () => { 4 | let [isSuccess, servers] = await FrogRequests.get(SERVERS_URL); 5 | if (!isSuccess) { 6 | return false; 7 | } 8 | 9 | // Очищаем список 10 | $("#modal-servers .serversList .item:not(.placeholder)").remove(); 11 | 12 | // Получаем код placeholder`а 13 | let placeholder = $("#modal-servers .serversList .item.placeholder")[0].outerHTML; 14 | placeholder = placeholder.replace(' placeholder', ""); 15 | // По placeholder`у добавляем новые элементы 16 | servers.servers.forEach((srv) => { 17 | let preparedPlaceholder = placeholder.replaceAll(/\$1/gim, srv.name) 18 | .replaceAll(/\$2/gim, srv.description) 19 | .replaceAll(/\$3/gim, srv.version) 20 | .replaceAll(/\$4/gim, srv.ip) 21 | .replaceAll(/\$5/gim, srv.icon) 22 | .replaceAll(/\$6/gim, srv.ip + ":" + srv.port) 23 | .replaceAll(/\$7/gim, srv.flVersion); 24 | $("#modal-servers .serversList").append(preparedPlaceholder); 25 | }) 26 | 27 | // Показываем все в списке 28 | $("#modal-servers .serversList .item").each(function () { 29 | if (!$(this).hasClass("placeholder")) { 30 | $(this).show(); 31 | } 32 | }) 33 | FrogServersUI.refreshServersInfo(); 34 | return true; 35 | } 36 | 37 | // Скопировать IP сервера 38 | static copyServerIP(ip) { 39 | navigator.clipboard.writeText(ip); 40 | FrogToasts.create(MESSAGES.servers.ipCopied, "share"); 41 | } 42 | 43 | // Получить статус и информацию о сервере 44 | static queryServer = async (ip) => { 45 | let port = 25565; 46 | if(ip.split(":").length === 2){ 47 | port = ip.split(":")[1]; 48 | ip = ip.split(":")[0]; 49 | } 50 | try { 51 | let query = await GameDig.query({type: "minecraft", host: ip, port: port}); 52 | return { 53 | online: true, 54 | data: query 55 | } 56 | } catch(e) { 57 | return { 58 | online: false 59 | }; 60 | } 61 | } 62 | 63 | // Обновить информацию о серверах в списке 64 | static refreshServersInfo = () => { 65 | $("#modal-servers .serversList .item:not(.placeholder)").each(async function () { 66 | let $status = $(this).find("div.status"); 67 | let $onlineChip = $(this).find("div.cap-div div.chip"); 68 | let ip = $(this).data("ip"); 69 | 70 | $status.removeClass("online offline"); 71 | $onlineChip.text("..."); 72 | 73 | let serverQuery = await FrogServersUI.queryServer(ip); 74 | if(!serverQuery.online){ 75 | $status.addClass("offline"); 76 | $onlineChip.hide(); 77 | } else { 78 | $status.addClass("online"); 79 | $onlineChip.show(); 80 | $onlineChip.text(`${serverQuery.data.numplayers} / ${serverQuery.data.maxplayers}`); 81 | } 82 | }) 83 | } 84 | 85 | // Играть на сервере 86 | static playOnServer = async (ip, versionId) => { 87 | selectedServerFromList = ip; 88 | FrogVersionsManager.setActiveVersion(versionId); 89 | FrogFlyout.startSelectedVersion(); 90 | return true; 91 | } 92 | } -------------------------------------------------------------------------------- /web/js/colors.js: -------------------------------------------------------------------------------- 1 | class colors { 2 | // Цвет светлый или тёмный (для цвета текста) 3 | static isColorLight = (color) => { 4 | const rgb = [ 5 | parseInt(color.substring(1, 3), 16), 6 | parseInt(color.substring(3, 5), 16), 7 | parseInt(color.substring(5), 16), 8 | ]; 9 | const luminance = 10 | (0.2126 * rgb[0]) / 255 + 11 | (0.7152 * rgb[1]) / 255 + 12 | (0.0722 * rgb[2]) / 255; 13 | return luminance > 7; 14 | } 15 | 16 | // Цифра цвета RGB в двузначный HEX 17 | static componentToHex = (c) => { 18 | let hex = c.toString(16); 19 | return hex.length === 1 ? "0" + hex : hex; 20 | } 21 | 22 | // rgb() to #hex 23 | static rgbToHex = (r, g, b) => { 24 | return "#" + colors.componentToHex(r) + colors.componentToHex(g) + colors.componentToHex(b); 25 | } 26 | 27 | // rgb() to hsl() 28 | static RGBToHSL = (r, g, b) => { 29 | // Make r, g, and b fractions of 1 30 | r /= 255; 31 | g /= 255; 32 | b /= 255; 33 | 34 | // Find greatest and smallest channel values 35 | let cmin = Math.min(r, g, b), 36 | cmax = Math.max(r, g, b), 37 | delta = cmax - cmin, 38 | h = 0, 39 | s = 0, 40 | l = 0; 41 | 42 | if (delta === 0) 43 | h = 0; 44 | // Red is max 45 | else if (cmax === r) 46 | h = ((g - b) / delta) % 6; 47 | // Green is max 48 | else if (cmax === g) 49 | h = (b - r) / delta + 2; 50 | // Blue is max 51 | else 52 | h = (r - g) / delta + 4; 53 | 54 | h = Math.round(h * 60); 55 | 56 | // Make negative hues positive behind 360° 57 | if (h < 0) 58 | h += 360; 59 | 60 | // Calculate lightness 61 | l = (cmax + cmin) / 2; 62 | 63 | // Calculate saturation 64 | s = delta === 0 ? 0 : delta / (1 - Math.abs(2 * l - 1)); 65 | 66 | // Multiply l and s by 100 67 | s = +(s * 100).toFixed(0); 68 | l = +(l * 100).toFixed(0); 69 | 70 | return { 71 | h: h, 72 | s: s, 73 | l: l 74 | }; 75 | } 76 | 77 | // Вычесть значение из цвета RGB 78 | static rgbMinus = (color, minus) => { 79 | let matched = color.match(colorsMatchRegex); 80 | if (matched === null || matched.length !== 4) { 81 | return false; 82 | } 83 | 84 | let r = matched[1]; 85 | let g = matched[2]; 86 | let b = matched[3]; 87 | 88 | r = parseInt(r) + parseInt(minus); 89 | g = parseInt(g) + parseInt(minus); 90 | b = parseInt(b) + parseInt(minus); 91 | 92 | if (r > 255) { 93 | r = 255; 94 | } else if (r < 0) { 95 | r = 0; 96 | } 97 | if (g > 255) { 98 | g = 255; 99 | } else if (g < 0) { 100 | g = 0; 101 | } 102 | if (b > 255) { 103 | b = 255; 104 | } else if (b < 0) { 105 | b = 0; 106 | } 107 | return "rgb(" + r + ", " + g + ", " + b + ")"; 108 | } 109 | 110 | // #hex to rgb() 111 | static hexToRgb = (hex) => { 112 | hex = hex.toLowerCase(); 113 | let result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); 114 | return result ? { 115 | r: parseInt(result[1], 16), 116 | g: parseInt(result[2], 16), 117 | b: parseInt(result[3], 16) 118 | } : null; 119 | } 120 | 121 | } -------------------------------------------------------------------------------- /web/modals/accounts.css: -------------------------------------------------------------------------------- 1 | #modal-accounts { 2 | position: absolute; 3 | z-index: 2; 4 | 5 | padding: 12px; 6 | 7 | width: 25vw; 8 | height: max-content; 9 | min-height: 50vh; 10 | max-height: 80vh; 11 | 12 | bottom: calc(12% + 24px); 13 | left: calc(3% + 6px); 14 | 15 | background: var(--theme-primaryBg); 16 | 17 | display: flex; 18 | flex-direction: column; 19 | gap: 12px; 20 | } 21 | 22 | #modal-accounts .acc-btns { 23 | display: flex; 24 | gap: 6px; 25 | } 26 | 27 | #modal-accounts .acc-btns button, 28 | #modal-accounts .addFrog { 29 | width: 100%; 30 | padding: 0; 31 | font-size: 14pt; 32 | height: 40px; 33 | } 34 | 35 | /* Accounts list */ 36 | #modal-accounts .accounts-list { 37 | padding: 4px; 38 | flex-grow: 1; 39 | display: flex; 40 | flex-direction: column; 41 | align-items: center; 42 | gap: 8px; 43 | height: max-content; 44 | 45 | background: var(--theme-primaryBg); 46 | border-radius: 8px; 47 | } 48 | 49 | #modal-accounts .acc-list-wrapper { 50 | flex-grow: 1; 51 | overflow: auto; 52 | height: max-content; 53 | } 54 | 55 | /* Accounts list item */ 56 | #modal-accounts .accounts-list .item { 57 | grid-template-columns: max-content 1fr max-content max-content; 58 | grid-template-rows: max-content max-content; 59 | gap: 6px; 60 | grid-template-areas: 61 | "icon title buttons check" 62 | "icon description buttons check"; 63 | 64 | height: max-content; 65 | padding: 6px 12px; 66 | 67 | width: 100%; 68 | } 69 | 70 | #modal-accounts .accounts-list .item .buttons { 71 | grid-area: buttons; 72 | align-self: center; 73 | justify-self: center; 74 | 75 | display: flex; 76 | align-items: center; 77 | justify-content: center; 78 | gap: 2px; 79 | } 80 | 81 | #modal-accounts .accounts-list .item .icon, 82 | #accountSelect .icon { 83 | position: relative; 84 | display: inline-block; 85 | } 86 | 87 | #modal-accounts .accounts-list .item .icon .avatar, 88 | #accountSelect .icon .avatar { 89 | object-fit: cover; 90 | 91 | width: 40px; 92 | height: 40px; 93 | } 94 | 95 | #modal-accounts .accounts-list .item .icon .type, 96 | #accountSelect .icon .type { 97 | border: 2px solid var(--theme-primaryBgLight); 98 | position: absolute; 99 | bottom: -6%; 100 | right: -4%; 101 | border-radius: 4px; 102 | 103 | width: 18px; 104 | height: 18px; 105 | 106 | background: var(--theme-primaryBgLight) !important; 107 | } 108 | 109 | #modal-accounts .accounts-list .item .check { 110 | grid-area: check; 111 | align-self: center; 112 | justify-self: center; 113 | color: var(--theme-primary); 114 | } 115 | 116 | #modal-accounts .accounts-list .item:not(.active) .check { 117 | display: none; 118 | } 119 | 120 | #modal-accounts .accounts-list .item .buttons button { 121 | cursor: pointer; 122 | 123 | width: 32px; 124 | height: 32px; 125 | padding: 0; 126 | display: none; 127 | } 128 | 129 | #modal-accounts .accounts-list .item .delete:hover * { 130 | color: var(--error); 131 | } 132 | 133 | #modal-accounts .accounts-list .item .edit:hover * { 134 | color: var(--theme-primary); 135 | } 136 | 137 | #modal-accounts .accounts-list .item:not(.active) .delete { 138 | display: inherit; 139 | } 140 | 141 | #modal-accounts .accounts-list .item.active { 142 | outline: 3px solid var(--theme-primary); 143 | } 144 | 145 | /* Add local item */ 146 | #modal-accounts .add-local { 147 | background: transparent; 148 | display: flex; 149 | align-items: center; 150 | justify-content: center; 151 | width: 100%; 152 | height: max-content; 153 | padding: 6px; 154 | border-radius: 8px; 155 | gap: 4px; 156 | clip-path: inset(0 0 0 0); 157 | transition: clip-path 1s; 158 | } 159 | 160 | #modal-accounts .add-local.hidden { 161 | clip-path: inset(0 0 99px 0); 162 | } 163 | 164 | #modal-accounts .add-local input { 165 | flex-grow: 1; 166 | } -------------------------------------------------------------------------------- /web/js/FrogModals.js: -------------------------------------------------------------------------------- 1 | class FrogModals { 2 | // Показать модальное окно 3 | static showModal = async (modalName) => { 4 | FrogCollector.writeLog(`Modal: Show "${modalName}"`); 5 | 6 | document.dispatchEvent(new CustomEvent("showModalEvent", { 7 | detail: {modal: modalName} 8 | })); 9 | if (FrogModals.isModalShown(modalName)) { 10 | return false; 11 | } 12 | let modalElem = $(`.modal#modal-${modalName}`); 13 | modalElem.show(); 14 | if (modalElem[0].tagName === "DIALOG") { 15 | modalElem[0].showModal(); 16 | } 17 | await animateCSSNode(modalElem[0], "fadeInUp"); 18 | return true; 19 | } 20 | 21 | // Скрыть модальное окно 22 | static hideModal = async (modalName) => { 23 | FrogCollector.writeLog(`Modal: Hide "${modalName}"`); 24 | 25 | document.dispatchEvent(new CustomEvent("hideModalEvent", { 26 | detail: {modal: modalName} 27 | })); 28 | if (!FrogModals.isModalShown(modalName)) { 29 | return false; 30 | } 31 | let modalElem = $(`.modal#modal-${modalName}`); 32 | await animateCSSNode(modalElem[0], "fadeOutDown"); 33 | modalElem.hide(); 34 | if (modalElem[0].tagName === "DIALOG") { 35 | modalElem[0].close(); 36 | } 37 | return true; 38 | } 39 | 40 | // Переключить окно (если сейчас показано какое-либо) 41 | static switchModal = async (modalName) => { 42 | FrogFlyout.lockFlymenu(); 43 | $(`.flymenu .item[data-modal="${FrogModals.currentModalName()}"]`).removeClass("active"); 44 | $(`.flymenu .item[data-modal="${modalName}"]`).addClass("active"); 45 | await FrogModals.hideCurrentModal(); 46 | await FrogModals.showModal(modalName); 47 | FrogFlyout.unlockFlymenu(); 48 | return true; 49 | } 50 | 51 | // Скрыть модальное окно, которое сейчас открыто 52 | static hideCurrentModal = async () => { 53 | let currentModalElement = $(`.modal[style!="display: none;"]:not(.overlay)`); 54 | let currentModalName = $(currentModalElement).attr("id").replace("modal-", ""); 55 | await FrogModals.hideModal(currentModalName); 56 | return true; 57 | } 58 | 59 | // Скрыть модальное overlay окно, которое сейчас открыто 60 | static hideCurrentOverlay = async () => { 61 | let currentModalElement = $(`.modal.overlay[style!="display: none;"]`); 62 | if (currentModalElement.length === 0) return false; 63 | let currentModalName = $(currentModalElement).attr("id").replace("modal-", ""); 64 | await FrogModals.hideModal(currentModalName); 65 | return true; 66 | } 67 | 68 | // Получить название modal на переднем плане 69 | static currentModalName = () => { 70 | let $currentModal = $(`.modal[style!="display: none;"]:not(.overlay)`); 71 | return $currentModal[0].id.replace("modal-", ""); 72 | } 73 | 74 | // Показано ли модальное окно (если "", то любое из окон) 75 | static isModalShown = (modalName = "") => { 76 | if (modalName !== "") { 77 | return $(`.modal#modal-${modalName}`).css("display") !== "none"; 78 | } else { 79 | return $(`.modal[style!="display: none;"]:not(.overlay)`).length > 0; 80 | } 81 | } 82 | 83 | // Показано ли overlay модальное окно (если "", то любое из окон) 84 | static isOverlayModalShown = (modalName = "") => { 85 | if (modalName !== "") { 86 | return FrogModals.isModalShown(modalName); 87 | } else { 88 | return $(`.modal.overlay[style!="display: none;"]`).length > 0; 89 | } 90 | } 91 | 92 | // Скрыть все и переключиться на контент 93 | static switchToContent = () => { 94 | $(".modal.overlay").each(function () { 95 | FrogModals.hideModal($(this)[0].id.replace("modal-", "")); 96 | }) 97 | if ($(`.modal[style!="display: none;"]:not(.overlay)`)[0].id !== "modal-content") { 98 | FrogModals.switchModal("content"); 99 | } 100 | } 101 | } -------------------------------------------------------------------------------- /PRIVACY_POLICY.md: -------------------------------------------------------------------------------- 1 | # Политика конфиденциальности FrogLauncher 2 | 3 | **Последнее обновление:** 25 февраля 2025 года 4 | 5 | Настоящая Политика конфиденциальности (далее — «Политика») описывает, как разработчик программного обеспечения FrogLauncher, seeeroy (далее — «Разработчик»), собирает, использует, хранит и защищает персональные данные пользователей (далее — «Пользователь»). 6 | 7 | --- 8 | 9 | ### **1. Типы собираемых данных** 10 | 1.1. При каждом запуске Лаунчера автоматически собираются следующие данные: 11 | - Название операционной системы (ОС); 12 | - Название процессора; 13 | - Количество оперативной памяти; 14 | - Уникальный идентификатор пользователя (ID), генерируемый с использованием библиотеки [machine-uuid](https://www.npmjs.com/package/machine-uuid); 15 | - Версия Лаунчера; 16 | - Установленные самим Лаунчером версии Java; 17 | - Количество созданных сборок; 18 | - Количество установленных версий; 19 | - Версии используемого в Лаунчере программного обеспечения. 20 | 21 | --- 22 | 23 | ### **2. Методы сбора данных** 24 | 2.1. Сбор данных осуществляется Лаунчером при его запуске путем отправки данных на сервер по защищенному протоколу HTTPS. 25 | 26 | --- 27 | 28 | ### **3. Использование данных** 29 | 3.1. Собранные данные используются для следующих целей: 30 | - Диагностика проблем пользователей: уникальный идентификатор (ID) позволяет связать данные об аппаратном обеспечении, версии Лаунчера и Java с конкретным пользователем для оказания технической поддержки; 31 | - Получение обратной связи от Пользователей; 32 | - Улучшение общего пользовательского опыта; 33 | - Исправление возможных ошибок и багов в Лаунчере. 34 | 35 | 3.2. Данные не передаются третьим лицам и используются исключительно Разработчиком для указанных выше целей. 36 | 37 | --- 38 | 39 | ### **4. Правовые основания обработки данных** 40 | 4.1. Обработка данных осуществляется на основании: 41 | - Согласия Пользователя, предоставляемого при первом запуске Лаунчера; 42 | - Законного интереса Разработчика в улучшении работы Лаунчера и оказании технической поддержки. 43 | 44 | --- 45 | 46 | ### **5. Права пользователей** 47 | 5.1. Пользователь имеет следующие права в отношении своих данных: 48 | - Право на доступ к данным, отправляемым Лаунчером; 49 | - Право на исправление неточных данных; 50 | - Право на удаление своих данных; 51 | - Право на ограничение обработки данных; 52 | - Право на отзыв согласия на обработку данных. 53 | 54 | 5.2. Для реализации своих прав Пользователь может направить запрос на любые из контактов Разработчика, расположенные на его официальном сайте. 55 | 56 | --- 57 | 58 | ### **6. Хранение и защита данных** 59 | 6.1. Разработчик принимает все необходимые меры для защиты данных Пользователей от несанкционированного доступа, изменения, раскрытия или уничтожения. 60 | 61 | 6.2. Данные хранятся на серверах, подконтрольных Разработчику, и защищены с использованием современных средств защиты информации. 62 | 63 | 6.3. Срок хранения данных составляет **3 года**. По истечении этого периода данные будут удалены. 64 | 65 | --- 66 | 67 | ### **7. Международная передача данных** 68 | 7.1. Данные Пользователей хранятся и обрабатываются на территории страны, где находится Разработчик. Международная передача данных не осуществляется. 69 | 70 | --- 71 | 72 | ### **8. Изменения в Политике** 73 | 8.1. Разработчик оставляет за собой право вносить изменения в настоящую Политику в одностороннем порядке. Изменения вступают в силу с момента их публикации на официальном сайте Разработчика. 74 | 75 | 8.2. Пользователи будут уведомлены об изменениях через новости в Лаунчере. 76 | 77 | 8.3. Продолжение использования Лаунчера после внесения изменений в Политику считается согласием Пользователя с новыми условиями. 78 | 79 | --- 80 | 81 | ### **9. Согласие с Политикой** 82 | 9.1. При первом запуске Лаунчера Пользователь будет уведомлен о необходимости ознакомиться с настоящей Политикой и дать согласие на обработку данных. Согласие предоставляется путем нажатия кнопки «Согласен» во всплывающем окне. 83 | 84 | 9.2. Пользователь может отозвать свое согласие в любой момент, направив запрос на контакты Разработчика, и приостановив использование продукта. 85 | 86 | --- 87 | 88 | ### **10. Контактная информация** 89 | 10.1. По всем вопросам, связанным с обработкой данных, Пользователь может обратиться к Разработчику через контактные данные, указанные на официальном сайте. 90 | 91 | Используя FrogLauncher, вы подтверждаете, что ознакомились с условиями данной Политики конфиденциальности и согласны с ними. -------------------------------------------------------------------------------- /web/modals/settings.css: -------------------------------------------------------------------------------- 1 | #modal-settings .settings-layout > h3 { 2 | font-weight: 600; 3 | margin: 21px 0; 4 | padding: 0; 5 | } 6 | 7 | #modal-settings .content { 8 | display: flex; 9 | gap: 16px; 10 | } 11 | 12 | #modal-settings .content .tabs { 13 | width: 26vw; 14 | } 15 | 16 | #modal-settings .content .tabs .tab { 17 | height: 54px; 18 | } 19 | 20 | #modal-settings .settings-layout .splitter { 21 | margin: 8px 0; 22 | width: 100%; 23 | height: 2px; 24 | background: rgba(190, 190, 190, 0.25); 25 | } 26 | 27 | #modal-settings .settings-item { 28 | background: var(--theme-primaryBgLight); 29 | padding: 12px; 30 | border-radius: 8px; 31 | } 32 | 33 | #modal-settings > h2 { 34 | font-weight: 400; 35 | margin: 16px 0; 36 | } 37 | 38 | #modal-settings .settings-item .name { 39 | font-weight: 600; 40 | font-size: 14pt; 41 | } 42 | 43 | #modal-settings .settings-item .description { 44 | font-size: 11pt; 45 | font-weight: 400; 46 | color: var(--text-sub); 47 | max-width: 40vw; 48 | } 49 | 50 | #modal-settings .settings-item .icon { 51 | background: var(--theme-primaryBgLighter); 52 | width: 44px; 53 | height: 44px; 54 | aspect-ratio: 1/1; 55 | display: flex; 56 | align-items: center; 57 | justify-content: center; 58 | border-radius: 8px; 59 | margin-right: 6px; 60 | } 61 | 62 | #modal-settings .settings-item .icon img { 63 | width: 26px; 64 | height: 26px; 65 | } 66 | 67 | html.dark #modal-settings .settings-item .icon img { 68 | filter: invert(100%) sepia(0%) saturate(0%) hue-rotate(184deg) brightness(103%) contrast(101%); 69 | } 70 | 71 | html.light #modal-settings .settings-item img.icon { 72 | filter: none; 73 | } 74 | 75 | #modal-settings .settings-item .icon * { 76 | color: var(--text-color); 77 | } 78 | 79 | #modal-settings .settings-layout { 80 | overflow-x: hidden; 81 | overflow-y: auto; 82 | height: 60vh; 83 | width: 100%; 84 | padding-right: 8px; 85 | } 86 | 87 | /* Micro placeholder for dark mode switch */ 88 | #modal-settings .placeholder.active, 89 | #modal-settings .settings-item .wp-item.active { 90 | border: 4px solid var(--theme-primary); 91 | } 92 | 93 | #modal-settings .placeholder:not(.active):hover, 94 | #modal-settings .settings-item .wp-item:not(.active):hover { 95 | cursor: pointer; 96 | filter: brightness(120%); 97 | } 98 | 99 | #modal-settings .placeholder { 100 | width: 96px; 101 | height: 64px; 102 | display: grid; 103 | grid-template-columns: 12px 1fr 24px; 104 | grid-template-rows: 1fr 1fr 8px; 105 | gap: 4px 4px; 106 | grid-auto-flow: row; 107 | grid-template-areas: 108 | "left main main" 109 | "left main main" 110 | "bottom-1 bottom-1 bottom-2"; 111 | padding: 6px; 112 | border-radius: 8px; 113 | background: #3c3c3c; 114 | } 115 | 116 | #modal-settings .placeholder.light { 117 | background: #f1f1f1; 118 | } 119 | 120 | #modal-settings .placeholder .left { 121 | border-radius: 4px; 122 | grid-area: left; 123 | background: #bdbdbd; 124 | } 125 | 126 | #modal-settings .placeholder.light .left { 127 | background: #606060; 128 | } 129 | 130 | #modal-settings .placeholder .bottom-1 { 131 | grid-area: bottom-1; 132 | background: #818181; 133 | border-radius: 4px; 134 | } 135 | 136 | #modal-settings .placeholder.light .bottom-1 { 137 | background: #757575; 138 | } 139 | 140 | #modal-settings .placeholder .bottom-2 { 141 | grid-area: bottom-2; 142 | background: #0a73ff; 143 | border-radius: 4px; 144 | } 145 | 146 | #modal-settings .placeholder .main { 147 | grid-area: main; 148 | background: #818181; 149 | border-radius: 4px; 150 | } 151 | 152 | /* Wallpapers changer */ 153 | #modal-settings .settings-item .wp-item { 154 | height: 50px; 155 | border-radius: 6px; 156 | } 157 | 158 | #modal-settings .settings-item .wp-item.placeholder { 159 | display: flex; 160 | align-items: center; 161 | justify-content: center; 162 | background: var(--theme-primaryBgLighter); 163 | width: 78px; 164 | } 165 | 166 | #modal-settings .settings-item .wp-item.placeholder * { 167 | color: var(--text-color); 168 | } 169 | 170 | /* Tabs */ 171 | #modal-settings .layout-tab { 172 | display: flex; 173 | flex-direction: column; 174 | gap: 16px 0; 175 | } 176 | 177 | #modal-settings .layout-tab:not(.active) { 178 | display: none; 179 | } 180 | 181 | #modal-settings .packs-layout { 182 | display: flex; 183 | flex-direction: column; 184 | gap: 8px; 185 | } -------------------------------------------------------------------------------- /EULA.md: -------------------------------------------------------------------------------- 1 | # Пользовательское соглашение FrogLauncher 2 | 3 | Последнее обновление: 25 июля 2024 года 4 | 5 | Настоящее 6 | Пользовательское соглашение (далее — «Соглашение») регулирует отношения между разработчиком программного 7 | обеспечения FrogLauncher, seeeroy (далее — «Разработчик»), и пользователем данного программного обеспечения 8 | (далее — «Пользователь»). Используя FrogLauncher, Пользователь соглашается с условиями данного Соглашения. 9 | 10 | ### 1. Общие положения 11 | 1.1. FrogLauncher (далее — «Лаунчер») предназначен для запуска игры Minecraft с 12 | использованием официальных аккаунтов, установки модификаций (модов) и сборок. 13 | 14 | 1.2. Лаунчер распространяется по лицензии GNU General Public License версии 3.0 (GPL-3.0), что позволяет пользователям 15 | использовать, изменять и распространять Лаунчер в соответствии с условиями данной лицензии. 16 | 17 | 1.3. Исходный код Лаунчера является открытым и доступен для ознакомления, использования и модификации. Пользователь 18 | может получить доступ к исходному коду на официальном репозитории Разработчика. 19 | 20 | ### 2. Сбор и использование данных 21 | 2.1. При каждом запуске Лаунчера автоматически собираются следующие данные: 22 | - Название операционной системы (ОС), установленной на устройстве Пользователя. 23 | - Название и модель процессора, установленного в устройстве Пользователя. 24 | - Количество установленной оперативной памяти на устройстве Пользователя. 25 | - Уникальный идентификатор пользователя (ID), генерируемый для идентификации Пользователя. 26 | - Версия Лаунчера, используемая Пользователем. 27 | - Версии Java, установленные Лаунчером. 28 | - Количество созданных Пользователем сборок в Лаунчере. 29 | - Количество версий Minecraft, установленных через Лаунчер. 30 | - Версии программного обеспечения, используемые в Лаунчере. 31 | 32 | 2.2. Собранные данные передаются на сервера, подконтрольные Разработчику, и используются для следующих целей: 33 | - Получение обратной связи от Пользователей. 34 | - Улучшение общего пользовательского опыта. 35 | - Исправление возможных ошибок и багов в Лаунчере. 36 | 37 | 2.3. Лаунчер предоставляет возможность сбора более полных данных только с 38 | явного согласия Пользователя. Это осуществляется путем нажатия клавиши F12 в окне Лаунчера. Такие данные 39 | остаются на устройстве Пользователя и не передаются на сервера Разработчика. Пользователь вправе самостоятельно 40 | отправить эти данные Разработчику в случае возникновения технических проблем. 41 | 42 | ### 3. Ограничение ответственности 43 | 3.1. Разработчик не несет ответственности за любые повреждения, потерю данных или другие убытки, возникающие в результате использования Лаунчера, программного обеспечения Java или игры 44 | Minecraft. 45 | 46 | 3.2. Разработчик не несет ответственности за любые изменения или удаления файлов Пользователя, связанные с использованием Лаунчера и игры Minecraft. 47 | 48 | 3.3. Разработчик предоставляет Лаунчер как есть (as is), без каких-либо гарантий, явных или подразумеваемых, включая, но не ограничиваясь, 49 | подразумеваемые гарантии товарной пригодности или соответствия определенной цели. 50 | 51 | ### 4. Лицензия и права 52 | 4.1. Лаунчер распространяется по лицензии GNU General Public License версии 3.0 (GPL-3.0). Пользователь имеет право 53 | использовать, копировать, изменять и распространять Лаунчер в соответствии с условиями данной 54 | лицензии. 55 | 56 | 4.2. Исходный код Лаунчера доступен для свободного использования и модификации. Пользователь может вносить изменения в исходный код при условии соблюдения условий лицензии 57 | GPL-3.0. 58 | 59 | 4.3. Любые модификации Лаунчера, распространяемые Пользователем, должны также распространяться под лицензией GPL-3.0. 60 | 61 | ### 5. Прочие условия 62 | 5.1. Разработчик оставляет за собой право вносить изменения в настоящее Соглашение в одностороннем порядке. Изменения вступают в силу с момента публикации новой версии Соглашения на официальном сайте Разработчика. 63 | 64 | 5.2. В случае несогласия Пользователя с изменениями, он обязан прекратить использование Лаунчера. Продолжение использования Лаунчера после внесения изменений считается согласием Пользователя с новыми условиями Соглашения. 65 | 66 | ### 6. Заключительные положения 67 | 6.1. В случае признания какого-либо положения настоящего Соглашения 68 | недействительным или неисполнимым, остальные положения остаются в силе и продолжают действовать в полной 69 | мере. 70 | 71 | 6.2. Настоящее Соглашение является полным и исчерпывающим соглашением между Пользователем и 72 | Разработчиком относительно использования Лаунчера и заменяет собой все предыдущие соглашения, договоренности и 73 | переговоры, как в устной, так и в письменной форме. 74 | 75 | Скачивая с официального сайта и используя продукт FrogLauncher вы подтверждаете, что ознакомились с условиями данного Соглашения и согласны с ними. -------------------------------------------------------------------------------- /web/js/FrogDownloader.js: -------------------------------------------------------------------------------- 1 | let downloadsProcesses = {}; 2 | 3 | class FrogDownloader { 4 | // Подготовить UI к отображению загрузки файла 5 | static prepareUIToDownload(displayName) { 6 | return new Promise(resolve => { 7 | FrogFlyout.setProgress(0); 8 | FrogFlyout.setText(`${MESSAGES.commons.downloading} ${displayName}`, MESSAGES.commons.plsWait); 9 | FrogFlyout.changeMode("progress").then(resolve); 10 | }) 11 | } 12 | 13 | // Скачивается ли что-то в данный момент 14 | static isAnythingDownloading = () => { 15 | return Object.keys(downloadsProcesses).length; 16 | } 17 | 18 | // Обновить UI загрузки 19 | static updateDownloadUI = (displayName, progress, current, total) => { 20 | let currentHuman = FrogUtils.humanFileSize(current); 21 | let totalHuman = FrogUtils.humanFileSize(total); 22 | 23 | FrogFlyout.setProgress(progress); 24 | FrogFlyout.setText(`${MESSAGES.commons.downloading} ${displayName}`, `${currentHuman} / ${totalHuman} (${progress}%)`); 25 | return true; 26 | } 27 | 28 | // Обновить UI загрузки (использовать текст) 29 | static updateDownloadUIText = (displayName, progress, current, total) => { 30 | FrogFlyout.setProgress(progress); 31 | FrogFlyout.setText(`${MESSAGES.commons.downloading} ${displayName}`, `${current} / ${total} (${progress}%)`); 32 | return true; 33 | } 34 | 35 | // Скачать файл 36 | static downloadFile = (url, filePath, displayName = null, updateProgress = true) => { 37 | FrogCollector.writeLog(`New download [url=${url}] [displayName=${displayName}]`); 38 | if (displayName === null) { 39 | displayName = path.basename(filePath); 40 | } 41 | let dirname = path.dirname(filePath); 42 | if (!fs.existsSync(dirname)) { 43 | fs.mkdirSync(dirname, { 44 | recursive: true, 45 | }); 46 | } 47 | 48 | let received_bytes = 0; 49 | let total_bytes = 0; 50 | let percent = ""; 51 | 52 | return new Promise((resolve, reject) => { 53 | FrogDownloader.prepareUIToDownload(displayName).then(() => { 54 | FrogCollector.writeLog(`UI prepared for download`); 55 | request 56 | .get(url) 57 | .on("error", function (error) { 58 | reject(error); 59 | }) 60 | .on("response", function (data) { 61 | total_bytes = parseInt(data.headers["content-length"]); 62 | data.pipe(fs.createWriteStream(filePath)); 63 | }) 64 | .on("data", function (chunk) { 65 | received_bytes += chunk.length; 66 | if (updateProgress) { 67 | percent = Math.round((received_bytes * 100) / total_bytes); 68 | FrogDownloader.updateDownloadUI(displayName, percent, received_bytes, total_bytes); 69 | } 70 | }) 71 | .on("end", function () { 72 | FrogCollector.writeLog(`Download completed [displayname=${displayName}]`); 73 | FrogFlyout.setProgress(0); 74 | resolve(true); 75 | }); 76 | }) 77 | }); 78 | } 79 | 80 | // Скачать файл в фоновом режиме 81 | static quietDownloadFile = (url, filePath) => { 82 | FrogCollector.writeLog(`New quiet download [url=${url}]`); 83 | let dirname = path.dirname(filePath); 84 | if (!fs.existsSync(dirname)) { 85 | fs.mkdirSync(dirname, { 86 | recursive: true, 87 | }); 88 | } 89 | 90 | let received_bytes = 0; 91 | let total_bytes = 0; 92 | 93 | return new Promise((resolve, reject) => { 94 | request 95 | .get(url) 96 | .on("error", function (error) { 97 | reject(error); 98 | }) 99 | .on("response", function (data) { 100 | total_bytes = parseInt(data.headers["content-length"]); 101 | data.pipe(fs.createWriteStream(filePath)); 102 | }) 103 | .on("data", function (chunk) { 104 | received_bytes += chunk.length; 105 | }) 106 | .on("end", function () { 107 | FrogCollector.writeLog(`Quiet download completed`); 108 | resolve(true); 109 | }); 110 | }) 111 | } 112 | } -------------------------------------------------------------------------------- /web/js/FrogFlyout.js: -------------------------------------------------------------------------------- 1 | const TOOLTIP_PLACEHOLDER = `
$1
`; 2 | let tooltipShowed = false; 3 | 4 | class FrogFlyout { 5 | // Сменить режим панели 6 | static changeMode = (mode) => { 7 | return new Promise((resolve) => { 8 | if (FrogFlyout.getCurrentMode() === mode) { 9 | return resolve(false); 10 | } 11 | if (FrogModals.isModalShown("accounts")) { 12 | FrogModals.hideModal("accounts"); 13 | } 14 | if (FrogModals.isModalShown("versions")) { 15 | FrogModals.hideModal("versions"); 16 | } 17 | FrogCollector.writeLog(`Flyout: New mode - "${mode}"`); 18 | let allowedModes = ["idle", "progress", "spinner"]; 19 | if (!allowedModes.includes(mode)) { 20 | return resolve(false); 21 | } 22 | 23 | let visibleFlyoutElems = $(".flyout .flyout-mode:not(.hidden)"); 24 | visibleFlyoutElems.each(function (i, elem) { 25 | animateCSSNode($(elem)[0], "fadeOut").then(() => { 26 | $(`#${$(elem).attr("id")}`).addClass("hidden"); 27 | let newModeElem = $(`.flyout > div[data-mode="${mode}"]`); 28 | newModeElem.removeClass("hidden"); 29 | animateCSSNode(newModeElem[0], "fadeIn").then(resolve); 30 | }) 31 | }) 32 | }) 33 | } 34 | 35 | // Получить режим UI в настоящее время 36 | static getCurrentMode = () => { 37 | return $(".flyout .flyout-mode:not(.hidden)").data("mode"); 38 | } 39 | 40 | // Перевести UI в режим запуска игры (скрыть/заблокировать ненужные элементы) 41 | static setUIStartMode = (startMode) => { 42 | FrogModals.switchToContent(); 43 | // Скрываем все кнопки Play в списке версий на главном экране 44 | startMode ? $(".versionsPosters").addClass("start-mode") : $(".versionsPosters").removeClass("start-mode"); 45 | FrogCollector.writeLog(`Flyout: UI in start mode: ${startMode}`); 46 | startMode ? $(".flyout").addClass("start-mode") : $(".flyout").removeClass("start-mode"); 47 | } 48 | 49 | // Задать текст (задаётся во всех режимах сразу) 50 | static setText = (text = "", description = "") => { 51 | if (text !== "") { 52 | $(".flyout .flyout-mode:not(#versionSelect) .title").text(text); 53 | } 54 | if (description !== "") { 55 | $(".flyout .flyout-mode:not(#versionSelect) .description").show().text(description); 56 | } else { 57 | $(".flyout .flyout-mode:not(#versionSelect) .description").hide(); 58 | } 59 | }; 60 | 61 | // Задать значение прогресс бара (-1 - бесконечный) 62 | static setProgress = (progress) => { 63 | let progressElem = $(".flyout .progress-pill .inner"); 64 | if (progress < -1 || progress > 100) { 65 | return false; 66 | } 67 | 68 | if (progress === -1) { 69 | progressElem.addClass("indeterminate"); 70 | return true; 71 | } 72 | 73 | progressElem.removeClass("indeterminate"); 74 | progressElem.css("width", `${progress}%`); 75 | return true; 76 | } 77 | 78 | // Показать tooltip 79 | static showTooltip = (menuElement, text) => { 80 | if (tooltipShowed === true) { 81 | return false; 82 | } 83 | 84 | tooltipShowed = true; 85 | let elementPos = $(menuElement)[0].getBoundingClientRect(); 86 | $("html").append(TOOLTIP_PLACEHOLDER.replaceAll("$1", text)) 87 | let tooltipElement = $(".flymenu-tooltip"); 88 | let tooltipPos = $(tooltipElement)[0].getBoundingClientRect(); 89 | 90 | let left = elementPos.left + elementPos.width + 18; 91 | let top = elementPos.top + (elementPos.height / 2 - tooltipPos.height / 2); 92 | 93 | $(tooltipElement).css("top", top); 94 | $(tooltipElement).css("left", left); 95 | return true; 96 | } 97 | 98 | // Скрыть tooltip 99 | static hideTooltip = () => { 100 | tooltipShowed = false; 101 | return $(".flymenu-tooltip").remove(); 102 | } 103 | 104 | // Заблокировать для взаимодействия FlyMenu 105 | static lockFlymenu = () => { 106 | $(".flymenu").css("pointer-events", "none"); 107 | } 108 | 109 | // Разблокировать для взаимодействия FlyMenu 110 | static unlockFlymenu = () => { 111 | $(".flymenu").css("pointer-events", "initial"); 112 | } 113 | 114 | // Запустить выбранную версию 115 | static startSelectedVersion = () => { 116 | if (FrogAccountsManager.getActiveAccount() === "none") { 117 | return false; 118 | } 119 | let activeVersion = FrogVersionsManager.getActiveVersion(); 120 | FrogStarter.simpleStart(activeVersion); 121 | return true; 122 | } 123 | } -------------------------------------------------------------------------------- /web/js/FrogElybyManager.js: -------------------------------------------------------------------------------- 1 | const ELYBY_ENDPOINT = "https://authserver.ely.by"; 2 | const ELYBY_AUTH_URL = "/auth/authenticate"; 3 | const ELYBY_REFRESH_URL = "/auth/refresh"; 4 | const ELYBY_VALIDATE_URL = "/auth/validate"; 5 | 6 | class FrogElybyManager { 7 | // Войти в аккаунт (возвращает success, data/error) 8 | static loginAccount = async (login, password, totpToken = "") => { 9 | let clientToken = await machineUuid(); 10 | if (totpToken !== "") { 11 | password = password + ":" + totpToken; 12 | } 13 | let [isSuccess, response] = await FrogRequests.post(ELYBY_ENDPOINT + ELYBY_AUTH_URL, { 14 | username: login, 15 | password: password, 16 | clientToken: clientToken, 17 | requestUser: true 18 | }, "json", true); 19 | if(isSuccess){ 20 | return [true, response, clientToken]; 21 | } else { 22 | if (response.responseJSON.errorMessage.match(/Invalid credentials/gim) !== null) { 23 | return [false, "INVALID_CREDENTIALS", clientToken]; 24 | } else if (response.responseJSON.errorMessage.match(/two factor auth/gim) !== null) { 25 | return [false, "REQUIRES_TOTP", clientToken]; 26 | } else { 27 | return [false, response.responseJSON.errorMessage, clientToken]; 28 | } 29 | } 30 | } 31 | 32 | // Обновить access token 33 | static refreshAccessToken = async (authToken) => { 34 | let clientToken = await machineUuid(); 35 | let [isSuccess, result] = await FrogRequests.post(ELYBY_ENDPOINT + ELYBY_REFRESH_URL, { 36 | accessToken: authToken, 37 | clientToken: clientToken, 38 | requestUser: true, 39 | }, "json", true); 40 | if(!isSuccess){ 41 | return [false, result.toString(), clientToken]; 42 | } 43 | return [true, result, clientToken]; 44 | } 45 | 46 | // Валидировать access token 47 | static validateAccessToken = async (accessToken) => { 48 | $.post(ELYBY_ENDPOINT + ELYBY_VALIDATE_URL, { 49 | accessToken: accessToken, 50 | }).done(() => { 51 | return true; 52 | }).fail(() => { 53 | return false; 54 | }); 55 | } 56 | 57 | // Получить данные аккаунта для авторизации 58 | static getAccountForAuth = async (uuid) => { 59 | let accountData = FrogAccountsManager.getAccount(uuid); 60 | // Проверяем токен 61 | let validationResult = await FrogElybyManager.validateAccessToken(accountData.accessToken); 62 | if (!validationResult) { 63 | // Заново генерируем токен 64 | let [isRefreshSuccess, refreshResult] = await FrogElybyManager.refreshAccessToken(accountData.accessToken); 65 | if (!isRefreshSuccess) { 66 | // Если ошибка генерации - удаляем акк и просим добавить заново 67 | $("#modal-elybyLogin .error").text(MESSAGES.elyby.repeat); 68 | await FrogModals.showModal("elybyLogin"); 69 | FrogAccountsManager.deleteAccount(uuid); 70 | FrogFlyout.setUIStartMode(false); 71 | await FrogFlyout.changeMode("idle"); 72 | return false; 73 | } else { 74 | // Сохраняем данные после рефреша 75 | let accountsData = FrogAccountsManager.getAccounts(); 76 | accountData.accessToken = refreshResult.accessToken; 77 | accountData.nickname = refreshResult.selectedProfile.name; 78 | accountsData[uuid] = accountData; 79 | await FrogAccountsManager.saveAccounts(accountsData); 80 | 81 | // Возвращаем данные 82 | return { 83 | access_token: accountData.accessToken, 84 | client_token: accountData.clientToken, 85 | uuid: accountData.uuid, 86 | name: accountData.nickname, 87 | }; 88 | } 89 | } else { 90 | return { 91 | access_token: accountData.accessToken, 92 | client_token: accountData.clientToken, 93 | uuid: accountData.uuid, 94 | name: accountData.nickname, 95 | }; 96 | } 97 | } 98 | 99 | // Получить текстуру головы скина Ely.by по нику 100 | static getHeadURLByPlayerNickname = (nickname) => { 101 | return new Promise(resolve => { 102 | let fileUrl = `http://skinsystem.ely.by/skins/${nickname}.png`; 103 | let filePath = path.join(USERDATA_PATH, "elybySkins", `${nickname}.png`); 104 | if (!fs.existsSync(path.dirname(filePath))) { 105 | fs.mkdirSync(path.dirname(filePath)); 106 | } 107 | Jimp.read(fileUrl, (err, image) => { 108 | let imgWidth = image.bitmap.width; 109 | let headWidth = imgWidth / 8; 110 | let headResizeSize; 111 | headWidth > 40 ? headResizeSize = headWidth : headResizeSize = 40; 112 | image.crop(headWidth, headWidth, headWidth, headWidth).resize(headResizeSize, headResizeSize, Jimp.RESIZE_NEAREST_NEIGHBOR).writeAsync(filePath).then(() => { 113 | return resolve(filePath); 114 | }); 115 | }); 116 | }); 117 | } 118 | } -------------------------------------------------------------------------------- /web/js/FrogAccountsUI.js: -------------------------------------------------------------------------------- 1 | class FrogAccountsUI { 2 | // Перезагрузить список доступных аккаунтов 3 | static reloadAccountsManager = () => { 4 | $("#modal-accounts .accounts-list .item").unbind("click"); 5 | // Получаем список и активный аккаунт 6 | let accountsList = FrogAccountsManager.getAccounts(); 7 | let activeAccount = FrogAccountsManager.getActiveAccount(); 8 | $("#modal-accounts .accounts-list .item:not(.placeholder)").remove(); 9 | 10 | // Получаем код placeholder`а 11 | let placeholder = $("#modal-accounts .accounts-list .item.placeholder")[0].outerHTML; 12 | placeholder = placeholder.replace(' placeholder', ""); 13 | // По placeholder`у добавляем новые элементы 14 | Object.values(accountsList).forEach((acc) => { 15 | let accountType = FrogAccountsUI.typeDisplayName(acc.type); 16 | let imageUrl = `https://minotar.net/avatar/${acc.nickname}/44`; 17 | let editButtonVisible = "none"; 18 | if (acc.type === "elyby") { 19 | imageUrl = path.join(USERDATA_PATH, "elybySkins", `${acc.nickname}.png`); 20 | } 21 | if (acc.type === "frog") { 22 | imageUrl = acc.textures.head || imageUrl; 23 | editButtonVisible = "inherit"; 24 | } 25 | let preparedPlaceholder = placeholder.replaceAll("$1", acc.nickname).replaceAll("$2", accountType).replaceAll("$3", acc.uuid).replaceAll("$4", imageUrl).replaceAll("$5", acc.type).replaceAll("$6", editButtonVisible); 26 | $("#modal-accounts .accounts-list").append(preparedPlaceholder); 27 | }) 28 | 29 | // Помечаем нужные аккаунты в списке активными 30 | $("#modal-accounts .accounts-list .item").each(function () { 31 | if (!$(this).hasClass("placeholder")) { 32 | if ($(this).data("uuid") === activeAccount) { 33 | $(this).addClass("active"); 34 | } 35 | if ($(this).data("type") === "microsoft") { 36 | $(this).find(".type.microsoft").show(); 37 | } 38 | if ($(this).data("type") === "elyby") { 39 | $(this).find(".type.elyby").show(); 40 | } 41 | if ($(this).data("type") === "frog") { 42 | $(this).find(".type.frog").show(); 43 | } 44 | $(this).show(); 45 | } 46 | }) 47 | 48 | // Биндим клик на смену аккаунта 49 | $("#modal-accounts .accounts-list .item, #modal-accounts .accounts-list .item *:not(.delete)").click(function () { 50 | FrogAccountsManager.setActiveAccount($(this).data("uuid")); 51 | FrogModals.hideModal("accounts"); 52 | }) 53 | return true; 54 | } 55 | 56 | // Перезагрузить кнопку для открытия менеджера аккаунтов 57 | static reloadAccountSelect = () => { 58 | $(".flyout #accountSelectPlaceholder").show(); 59 | $(".flyout #accountSelect").hide(); 60 | let activeAccount = FrogAccountsManager.getActiveAccount(); 61 | let $activeAccount = $("#modal-accounts .accounts-list .item.active"); 62 | 63 | if (activeAccount === "none") { 64 | $("#accountSelect .title").text(MESSAGES.commons.notSelected); 65 | $("#accountSelect .icon").hide(); 66 | $("#accountSelect .description").hide(); 67 | } else { 68 | $("#accountSelect")[0].innerHTML = ""; 69 | $("#accountSelect")[0].innerHTML = $activeAccount.html(); 70 | $("#accountSelect .check, #accountSelect .buttons").remove(); 71 | } 72 | $(".flyout #accountSelectPlaceholder").hide(); 73 | $(".flyout #accountSelect").show(); 74 | return true; 75 | } 76 | 77 | // Получить текст из типа аккаунта 78 | static typeDisplayName = (type) => { 79 | if (typeof MESSAGES.accounts[type] !== "undefined") { 80 | return MESSAGES.accounts[type]; 81 | } else { 82 | return MESSAGES.accounts.unknown; 83 | } 84 | } 85 | 86 | // Показать UI для добавления локального аккаунта 87 | static showAddLocalAccountUI = () => { 88 | $("#modal-accounts .add-local").show(); 89 | $("#modal-accounts .add-local").removeClass("hidden"); 90 | } 91 | 92 | // Добавить локальный аккаунт из UI 93 | static addLocalAccount = () => { 94 | let txt = $("#modal-accounts .add-local input").val().trim(); 95 | FrogAccountsManager.addLocalAccount(txt); 96 | $("#modal-accounts .add-local").addClass("hidden"); 97 | $("#modal-accounts .add-local").one("transitionend webkitTransitionEnd oTransitionEnd", () => { 98 | $("#modal-accounts .add-local").hide(); 99 | }) 100 | $("#modal-accounts .add-local input").val(""); 101 | } 102 | 103 | // Добавить Microsoft аккаунт из UI 104 | static addMicrosoftAccount = () => { 105 | FrogModals.hideModal("accounts"); 106 | FrogFlyout.setUIStartMode(true); 107 | FrogFlyout.setText(MESSAGES.accounts.addingMs); 108 | FrogFlyout.changeMode("spinner").then(() => { 109 | FrogAccountsManager.addMicrosoftAccount().then(() => { 110 | FrogFlyout.changeMode("idle"); 111 | FrogFlyout.setUIStartMode(false); 112 | }); 113 | }) 114 | } 115 | 116 | // Открыть UI аккаунтов 117 | static open = () => { 118 | if (FrogModals.isModalShown("accounts")) { 119 | return FrogModals.hideModal("accounts"); 120 | } 121 | return FrogModals.showModal("accounts"); 122 | } 123 | } -------------------------------------------------------------------------------- /web/js/FrogCollector.js: -------------------------------------------------------------------------------- 1 | class FrogCollector { 2 | // Собрать архив отладки 3 | static collectDebugData = () => { 4 | FrogCollector.writeLog("Collecting debug archive"); 5 | machineUuid().then((uniqueId) => { 6 | let archiveName = `FL_debug-${Date.now()}-${uniqueId}`; 7 | let archiveDirPath = path.join(USERDATA_PATH, archiveName); 8 | let filesList = [ 9 | path.join(archiveDirPath, "last.log"), 10 | path.join(archiveDirPath, "timestamp"), 11 | path.join(archiveDirPath, "localStorage"), 12 | path.join(archiveDirPath, "data.json") 13 | ]; 14 | fs.mkdirSync(archiveDirPath); 15 | fs.writeFileSync(filesList[0], LAST_LOG); 16 | fs.writeFileSync(filesList[1], Date.now().toString()); 17 | fs.writeFileSync(filesList[2], JSON.stringify(window.localStorage)); 18 | fs.writeFileSync(filesList[3], JSON.stringify({ 19 | cpu: os.cpus()[0], 20 | ram: Math.round(os.totalmem() / 1024 / 1024), 21 | arch: os.arch(), 22 | win: os.release(), 23 | hostName: os.hostname(), 24 | homeDir: os.homedir(), 25 | appDataDir: USERDATA_PATH, 26 | resolvedRoot: path.resolve("./"), 27 | uniqueId: uniqueId, 28 | version: LAUNCHER_VERSION, 29 | processVersions: process.versions, 30 | javas: FrogJavaManager.getLocal(), 31 | packsCount: FrogPacks.getPacksList().length, 32 | settings: FrogConfig.readConfig() 33 | })); 34 | const outputPath = path.join(USERDATA_PATH, archiveName + ".zip"); 35 | FrogUtils.compressDirectory(outputPath, archiveDirPath).then(() => { 36 | filesList.forEach(file => { 37 | fs.unlinkSync(file); 38 | }) 39 | fs.rmdirSync(archiveDirPath); 40 | openExternal(USERDATA_PATH); 41 | }) 42 | }); 43 | } 44 | 45 | static writeLog = (...data) => { 46 | let logDate = new Date().toISOString(); 47 | let newPart = "\n" + "[" + logDate + "] " + data.join(" ") 48 | global.LAST_LOG = global.LAST_LOG + newPart; 49 | ipcRenderer.send("console-log", newPart); 50 | updateConsole(); 51 | } 52 | 53 | static collectAndSendStats = async () => { 54 | FrogCollector.writeLog("Collecting stats"); 55 | machineUuid().then((uniqueId) => { 56 | let platformInfo = { 57 | name: os.type(), 58 | release: os.release(), 59 | arch: process.arch, 60 | version: os.version(), 61 | }; 62 | 63 | let cpuSummary = os.cpus(); 64 | 65 | let cpuInfo = { 66 | model: cpuSummary[0].model, 67 | speed: cpuSummary[0].speed, 68 | cores: cpuSummary.length, 69 | }; 70 | 71 | let collectedStats = { 72 | platform: platformInfo, 73 | totalRAM: Math.round(os.totalmem() / 1024 / 1024), 74 | cpu: cpuInfo, 75 | uniqueID: uniqueId, 76 | version: LAUNCHER_VERSION, 77 | javas: FrogJavaManager.getLocal(), 78 | packsCount: FrogPacks.getPacksList().length, 79 | versionsInstalledCount: FrogVersionsManager.getInstalledVersionsList().length, 80 | versions: process.versions 81 | }; 82 | if (FrogConfig.read("lessDataCollection", false) === true) { 83 | // Меньше собираем данные 84 | collectedStats = { 85 | uniqueID: uniqueId, 86 | version: LAUNCHER_VERSION 87 | } 88 | } 89 | $.get( 90 | STATS_URL + encodeURIComponent(JSON.stringify(collectedStats)), 91 | () => { 92 | FrogCollector.writeLog("Stats sent successfully"); 93 | return true; 94 | } 95 | ); 96 | }); 97 | } 98 | 99 | static setupLogHijacking = () => { 100 | // Добавляем хэндлинг ошибок 101 | window.onerror = (errorMsg, url, lineNumber) => { 102 | FrogCollector.writeLog("========================"); 103 | FrogCollector.writeLog("window.onerror triggered at", Date.now()); 104 | FrogCollector.writeLog("Message:", errorMsg); 105 | FrogCollector.writeLog("URL:", url); 106 | FrogCollector.writeLog("Line number:", lineNumber); 107 | FrogCollector.writeLog("========================"); 108 | }; 109 | 110 | window.addEventListener("error", function (e) { 111 | FrogCollector.writeLog("========================"); 112 | FrogCollector.writeLog("error event triggered at", Date.now()); 113 | FrogCollector.writeLog("Message:", e.error.message); 114 | FrogCollector.writeLog("Full JSON:", JSON.stringify(e)); 115 | FrogCollector.writeLog("========================"); 116 | }); 117 | 118 | // Заменяем функции консоли 119 | let oldConsole = console; 120 | let newConsole = {}; 121 | newConsole.log = function (...log) { 122 | oldConsole.log(...log); 123 | FrogCollector.writeLog("BROWSER:", ...log); 124 | }; 125 | 126 | newConsole.info = function (...log) { 127 | oldConsole.info(...log); 128 | FrogCollector.writeLog("BROWSER:", ...log); 129 | }; 130 | 131 | newConsole.warn = function (...log) { 132 | oldConsole.warn(...log); 133 | FrogCollector.writeLog("BROWSER:", ...log); 134 | }; 135 | 136 | newConsole.error = function (...log) { 137 | oldConsole.error(...log); 138 | FrogCollector.writeLog("BROWSER:", ...log); 139 | }; 140 | window.console = newConsole; 141 | return true; 142 | } 143 | } --------------------------------------------------------------------------------