├── .babelrc ├── .eslintignore ├── .eslintrc.json ├── .github ├── actions │ └── upload │ │ ├── action.yml │ │ └── index.js └── workflows │ ├── release.yml │ └── translation.yml ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── app ├── main.js └── package.json ├── build ├── icons │ ├── 256x256.png │ ├── icon.icns │ └── icon.ico ├── installer.nsh └── sync-version.js ├── index.html ├── langs └── zh-cn.json ├── package-lock.json ├── package.json ├── packages ├── babel-plugin-dollar │ ├── index.js │ └── package.json ├── language-tools │ ├── cli.js │ └── index.js ├── minimize-three │ └── index.js ├── parcel-plugin-replacer │ ├── Asset.js │ ├── JSAsset.js │ ├── JSONAsset.js │ ├── TypeScriptAsset.js │ ├── index.js │ └── package.json ├── parcel-plugin-v8-cache │ ├── JSPackager.js │ ├── index.js │ ├── package-lock.json │ └── package.json ├── plugin │ ├── cli.js │ ├── index.d.ts │ └── index.js └── web-api │ ├── .gitignore │ ├── dev.html │ ├── index.d.ts │ ├── index.js │ ├── package-lock.json │ └── package.json ├── screenshots ├── accounts.png ├── home.png └── versions.png ├── src ├── App.tsx ├── SideBar.tsx ├── assets │ ├── iconfont.css │ ├── iconfont.ttf │ ├── images │ │ ├── bg-grass.png │ │ ├── bg-snow.png │ │ ├── bg-wool-dark-light.png │ │ ├── bg-wool-dark.png │ │ ├── bg-wool-white.png │ │ ├── compass_19.png │ │ ├── icons │ │ │ ├── Bedrock.png │ │ │ ├── Bookshelf.png │ │ │ ├── Brick.png │ │ │ ├── Cake.png │ │ │ ├── Carved_Pumpkin.png │ │ │ ├── Chest.png │ │ │ ├── Clay.png │ │ │ ├── Coal_Block.png │ │ │ ├── Coal_Ore.png │ │ │ ├── Cobblestone.png │ │ │ ├── Crafting_Table.png │ │ │ ├── Creeper_Head.png │ │ │ ├── Diamond_Block.png │ │ │ ├── Diamond_Ore.png │ │ │ ├── Dirt.png │ │ │ ├── Dirt_Podzol.png │ │ │ ├── Dirt_Snow.png │ │ │ ├── Emerald_Block.png │ │ │ ├── Emerald_Ore.png │ │ │ ├── Enchanting_Table.png │ │ │ ├── End_Stone.png │ │ │ ├── Farmland.png │ │ │ ├── Furnace.png │ │ │ ├── Furnace_On.png │ │ │ ├── Glass.png │ │ │ ├── Glazed_Terracotta_Light_Blue.png │ │ │ ├── Glazed_Terracotta_Orange.png │ │ │ ├── Glazed_Terracotta_White.png │ │ │ ├── Glowstone.png │ │ │ ├── Gold_Block.png │ │ │ ├── Gold_Ore.png │ │ │ ├── Grass.png │ │ │ ├── Gravel.png │ │ │ ├── Hardened_Clay.png │ │ │ ├── Ice_Packed.png │ │ │ ├── Iron_Block.png │ │ │ ├── Iron_Ore.png │ │ │ ├── Lapis_Ore.png │ │ │ ├── Leaves_Birch.png │ │ │ ├── Leaves_Jungle.png │ │ │ ├── Leaves_Oak.png │ │ │ ├── Leaves_Spruce.png │ │ │ ├── Lectern_Book.png │ │ │ ├── Log_Acacia.png │ │ │ ├── Log_Birch.png │ │ │ ├── Log_DarkOak.png │ │ │ ├── Log_Jungle.png │ │ │ ├── Log_Oak.png │ │ │ ├── Log_Spruce.png │ │ │ ├── Mycelium.png │ │ │ ├── Nether_Brick.png │ │ │ ├── Netherrack.png │ │ │ ├── Obsidian.png │ │ │ ├── Planks_Acacia.png │ │ │ ├── Planks_Birch.png │ │ │ ├── Planks_DarkOak.png │ │ │ ├── Planks_Jungle.png │ │ │ ├── Planks_Oak.png │ │ │ ├── Planks_Spruce.png │ │ │ ├── Quartz_Ore.png │ │ │ ├── Red_Sand.png │ │ │ ├── Red_Sandstone.png │ │ │ ├── Redstone_Block.png │ │ │ ├── Redstone_Ore.png │ │ │ ├── Sand.png │ │ │ ├── Sandstone.png │ │ │ ├── Skeleton_Skull.png │ │ │ ├── Snow.png │ │ │ ├── Soul_Sand.png │ │ │ ├── Stone.png │ │ │ ├── Stone_Andesite.png │ │ │ ├── Stone_Diorite.png │ │ │ ├── Stone_Granite.png │ │ │ ├── TNT.png │ │ │ ├── Water.png │ │ │ └── Wool.png │ │ ├── logo.png │ │ ├── progress.png │ │ ├── redstone.png │ │ ├── steve-head.png │ │ ├── steve.png │ │ ├── terracotta │ │ │ ├── black_terracotta.png │ │ │ ├── blue_terracotta.png │ │ │ ├── brown_terracotta.png │ │ │ ├── cyan_terracotta.png │ │ │ ├── gray_terracotta.png │ │ │ ├── green_terracotta.png │ │ │ ├── light_blue_terracotta.png │ │ │ ├── light_gray_terracotta.png │ │ │ ├── lime_terracotta.png │ │ │ ├── magenta_terracotta.png │ │ │ ├── orange_terracotta.png │ │ │ ├── pink_terracotta.png │ │ │ ├── purple_terracotta.png │ │ │ ├── red_terracotta.png │ │ │ ├── terracotta.png │ │ │ ├── white_terracotta.png │ │ │ └── yellow_terracotta.png │ │ ├── unknown-server.png │ │ ├── written_book.png │ │ └── zombie-head.png │ ├── minecraft.otf │ └── sounds │ │ ├── click.ogg │ │ └── levelup.ogg ├── components │ ├── Avatar.tsx │ ├── Dots.tsx │ ├── Dropdown.tsx │ ├── Empty.tsx │ ├── ErrorHandler.ts │ ├── IconButton.tsx │ ├── IconPicker.tsx │ ├── InstallList.tsx │ ├── LiveRoute.tsx │ ├── Loading.tsx │ ├── LoginDialog.tsx │ ├── Profile.tsx │ ├── ShowMore.tsx │ ├── Switch.tsx │ ├── VersionSelector.tsx │ ├── VersionSwitch.tsx │ ├── avatar.css │ ├── dots.less │ ├── dropdown.less │ ├── empty.less │ ├── icon-button.css │ ├── icon-picker.css │ ├── install-list.less │ ├── loading.less │ ├── login-dialog.less │ ├── profile.less │ ├── show-more.less │ ├── switch.less │ ├── treebeard │ │ ├── LICENSE │ │ ├── components │ │ │ ├── Decorators │ │ │ │ ├── Container.tsx │ │ │ │ ├── Header.tsx │ │ │ │ ├── Loading.tsx │ │ │ │ ├── Toggle.tsx │ │ │ │ └── index.tsx │ │ │ ├── NodeHeader.tsx │ │ │ ├── TreeNode │ │ │ │ ├── Drawer.tsx │ │ │ │ ├── Loading.tsx │ │ │ │ └── index.tsx │ │ │ ├── common │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ ├── index.ts │ │ ├── themes │ │ │ ├── animations.ts │ │ │ └── default.ts │ │ └── util │ │ │ └── index.ts │ └── version-switch.less ├── constants.ts ├── createServer.ts ├── custom.css ├── globals.d.ts ├── i18n.ts ├── index.css ├── index.tsx ├── main.ts ├── minecraft.css ├── models │ ├── GameStore.ts │ ├── ProfilesStore.ts │ └── index.ts ├── plugin │ ├── Authenticator.ts │ ├── DownloadProviders.ts │ ├── Plugin.ts │ ├── exports.ts │ ├── index.ts │ ├── internal │ │ ├── ResourceInstaller.ts │ │ ├── ResourceResolver.ts │ │ └── index.ts │ └── logins.ts ├── protocol │ ├── check-update.ts │ ├── exporter.tsx │ ├── index.ts │ ├── install-local.ts │ ├── install.ts │ ├── types.ts │ └── uninstaller.ts ├── routes │ ├── CustomServerHome.tsx │ ├── Error.tsx │ ├── Home.tsx │ ├── Manager.tsx │ ├── ServerHome.tsx │ ├── Settings.tsx │ ├── error.css │ ├── home.less │ ├── manager.css │ ├── manager │ │ ├── Extensions.tsx │ │ ├── Mods.tsx │ │ ├── Plugins.tsx │ │ ├── Profiles.tsx │ │ ├── ResourcePacks.tsx │ │ ├── ShaderPacks.tsx │ │ ├── Tasks.tsx │ │ ├── Versions.tsx │ │ ├── Worlds.tsx │ │ └── list.less │ ├── server-home.less │ └── settings.css ├── side-bar.less └── utils │ ├── EventBus.ts │ ├── analytics.ts │ ├── fit-text.tsx │ ├── fs.ts │ ├── hacks.ts │ ├── history.ts │ ├── index.ts │ ├── isDev.ts │ ├── locates.ts │ └── request-reload.ts ├── static └── serverHomePreload.js ├── tsconfig.json └── unpacked ├── createShortcut.js └── mc-logo.ico /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "babel-plugin-dollar", 4 | "@babel/plugin-proposal-optional-chaining", 5 | ["module-resolver", { "alias": { 6 | "reqwq": "reqwq/dist/esnext/index.js", 7 | "skinview3d": "skinview3d/dist/skinview3d.min.js", 8 | "^moment$": "moment/moment.js" 9 | } }] 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "standard", 9 | "standard-react", 10 | "eslint:recommended", 11 | "plugin:@typescript-eslint/eslint-recommended", 12 | "plugin:@typescript-eslint/recommended", 13 | "plugin:react/recommended", 14 | "plugin:jsx-a11y/recommended" 15 | ], 16 | "globals": { 17 | "Atomics": "readonly", 18 | "SharedArrayBuffer": "readonly", 19 | "notice": "readonly", 20 | "$": "readonly", 21 | "pluginMaster": "readonly", 22 | "profilesStore": "readonly", 23 | "openConfirmDialog": "readonly", 24 | "__getStore": "readonly", 25 | "__DEV__": "readonly", 26 | "topBar": "readonly" 27 | }, 28 | "parser": "@typescript-eslint/parser", 29 | "parserOptions": { 30 | "ecmaFeatures": { 31 | "jsx": true 32 | }, 33 | "ecmaVersion": 2018, 34 | "sourceType": "module" 35 | }, 36 | "plugins": [ 37 | "react", 38 | "jsx-a11y", 39 | "@typescript-eslint" 40 | ], 41 | "settings": { 42 | "react": { 43 | "version": "detect" 44 | } 45 | }, 46 | "rules": { 47 | "lines-between-class-members": "off", 48 | "promise/param-names": "off", 49 | "object-curly-newline": "off", 50 | "no-sequences": "off", 51 | "no-eval": "off", 52 | "@typescript-eslint/no-explicit-any": "off", 53 | "@typescript-eslint/explicit-function-return-type": "off", 54 | "@typescript-eslint/no-var-requires": "off", 55 | "no-empty": "off", 56 | "prefer-spread": "off", 57 | "@typescript-eslint/no-empty-function": "off", 58 | "require-atomic-updates": "off", 59 | "@typescript-eslint/no-unused-vars": ["error", { "args": "after-used", "ignoreRestSiblings": true, "argsIgnorePattern": "^_" }], 60 | "react/display-name": "off", 61 | "react/jsx-closing-bracket-location": "off", 62 | "jsx-a11y/anchor-is-valid": "off", 63 | "jsx-a11y/click-events-have-key-events": "off", 64 | "react/jsx-closing-tag-location": "off", 65 | "jsx-a11y/interactive-supports-focus": "off", 66 | "react/jsx-handler-names": "off", 67 | "jsx-a11y/no-onchange": "off", 68 | "react/jsx-indent": "off", 69 | "react/prop-types": "off", 70 | "react/no-children-prop": "off", 71 | "object-property-newline": "off", 72 | "no-void": "off", 73 | "jsx-a11y/no-static-element-interactions": "off", 74 | "@typescript-eslint/member-delimiter-style": ["error", { "multiline": { "delimiter": "none", "requireLast": false }, "singleline": { "delimiter": "comma" } }] 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /.github/actions/upload/action.yml: -------------------------------------------------------------------------------- 1 | name: upload 2 | description: Upload files. 3 | inputs: 4 | files: 5 | description: 'Files to uplad' 6 | required: true 7 | token: 8 | description: 'Github token' 9 | required: true 10 | secretId: 11 | description: 'The secret id of tencent cloud cos' 12 | required: true 13 | secretKey: 14 | description: 'The secret key of tencent cloud cos' 15 | required: true 16 | bucket: 17 | description: 'The bucket of tencent cloud cos' 18 | required: true 19 | final: 20 | description: 'Is this the last step' 21 | runs: 22 | using: 'node12' 23 | main: index.js 24 | -------------------------------------------------------------------------------- /.github/actions/upload/index.js: -------------------------------------------------------------------------------- 1 | const github = require('@actions/github') 2 | const core = require('@actions/core') 3 | const COS = require('cos-nodejs-sdk-v5') 4 | const Refresher = require('tencent-cdn-refresh') 5 | const { extname, basename } = require('path') 6 | const { promises: fs } = require('fs') 7 | const { createHash } = require('crypto') 8 | const { promisify } = require('util') 9 | 10 | ;(async () => { 11 | const octokit = new github.GitHub(core.getInput('token', { required: true })) 12 | 13 | const options = { 14 | SecretId: core.getInput('secretId', { required: true }), 15 | SecretKey: core.getInput('secretKey', { required: true }) 16 | } 17 | const cos = new COS({ ...options, Domain: '{Bucket}.cos.accelerate.myqcloud.com' }) 18 | const putObject = promisify(cos.putObject.bind(cos)) 19 | const Bucket = core.getInput('bucket', { required: true }) 20 | 21 | const names = [] 22 | const tag = github.context.ref.replace('refs/tags/', '') 23 | const files = core.getInput('files', { required: true }) 24 | .split(' ') 25 | .filter(Boolean) 26 | .map(it => { 27 | const [file, name] = it.replace(/{VERSION}/g, tag).split('?', 2) 28 | names.push(basename(name || file)) 29 | return file 30 | }) 31 | 32 | const { data } = await octokit.repos.getReleaseByTag({ ...github.context.repo, tag }) 33 | core.info('Files: ' + JSON.stringify(files)) 34 | 35 | const json = JSON.parse(data.body || `{"version":"${tag}","md5":{}}`) 36 | core.info('Reading files...') 37 | const buffers = await Promise.all(files.map(it => fs.readFile(it))) 38 | core.info('Read files!') 39 | buffers.forEach((it, i) => { 40 | const ext = extname(names[i]).replace(/^\./, '') 41 | json[ext] = createHash('sha1').update(it).digest('hex') 42 | json.md5[ext] = createHash('md5').update(it).digest('hex') 43 | }) 44 | core.info('Files hash: ' + JSON.stringify(json, null, 2)) 45 | 46 | core.info('Uploading files...') 47 | await Promise.all(names.map((name, i) => octokit.repos.uploadReleaseAsset({ 48 | name, 49 | url: data.upload_url, 50 | data: buffers[i], 51 | headers: { 52 | 'content-type': 'binary/octet-stream', 53 | 'content-length': buffers[i].length 54 | } 55 | }))) 56 | await Promise.all(names.map((Key, i) => putObject({ 57 | Key, 58 | Bucket, 59 | Region: 'ap-chengdu', 60 | Body: buffers[i] 61 | }))) 62 | core.info('Uploaded files!') 63 | 64 | if (core.getInput('final') === 'true') { 65 | core.info('Running final actions...') 66 | await octokit.repos.updateRelease({ 67 | ...github.context.repo, 68 | body: JSON.stringify(json, null, 2), 69 | release_id: data.id // eslint-disable-line 70 | }) 71 | const { md5, ...jsonData } = json 72 | const hashesData = JSON.stringify(jsonData) 73 | await octokit.repos.uploadReleaseAsset({ 74 | name: 'latest.json', 75 | url: data.upload_url, 76 | data: hashesData, 77 | headers: { 78 | 'content-type': 'application/json', 79 | 'content-length': hashesData.length 80 | } 81 | }) 82 | await putObject({ 83 | Bucket, 84 | Key: 'latest.json', 85 | Region: 'ap-chengdu', 86 | Body: hashesData 87 | }) 88 | try { 89 | core.info(JSON.stringify(await new Refresher(options).purgeDirsCache('https://dl.pl.apisium.cn/'))) 90 | } catch (e) { 91 | core.warning(e) 92 | } 93 | core.info('Final actions ran successfully!') 94 | } 95 | })().catch(e => { 96 | core.setFailed(e.stack) 97 | }) 98 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | release: 5 | types: 6 | - created 7 | 8 | jobs: 9 | build_windows: 10 | runs-on: windows-latest 11 | 12 | steps: 13 | - name: Check out git repository 14 | uses: actions/checkout@v1 15 | 16 | - name: Install Node.js and npm 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: 12 20 | 21 | - name: Check release tag 22 | run: node -e "if (require('./package.json').version !== '${{ github.ref }}'.replace('refs/tags/', '')) throw new Error('Wrong release tag!')" 23 | 24 | - name: Install dependencies 25 | run: npm install 26 | 27 | - name: Build app 28 | run: npm run build 29 | 30 | - name: Pack app 31 | run: npm run pack 32 | 33 | - name: Upload artifact 34 | uses: actions/upload-artifact@v1 35 | with: 36 | name: dist 37 | path: app/dist 38 | 39 | - name: Upload files 40 | uses: ./.github/actions/upload 41 | with: 42 | token: ${{ secrets.GITHUB_TOKEN }} 43 | secretId: ${{ secrets.SECRET_ID }} 44 | secretKey: ${{ secrets.SECRET_KEY }} 45 | bucket: ${{ secrets.COS_BUCKET }} 46 | files: release/win-ia32-unpacked/resources/app.asar release/nsis-web/PureLauncher.exe release/nsis-web/pure-launcher-{VERSION}-x64.nsis.7z?x64.nsis.7z release/nsis-web/pure-launcher-{VERSION}-ia32.nsis.7z?ia32.nsis.7z 47 | 48 | build_macos: 49 | runs-on: macos-latest 50 | needs: build_windows 51 | 52 | steps: 53 | - name: Check out git repository 54 | uses: actions/checkout@v1 55 | 56 | - name: Install Node.js and npm 57 | uses: actions/setup-node@v1 58 | with: 59 | node-version: 12 60 | 61 | - name: Install dependencies 62 | run: npm install 63 | 64 | - name: Download artifact 65 | uses: actions/download-artifact@v1 66 | with: 67 | name: dist 68 | 69 | - name: Pack app 70 | run: npm run pack 71 | 72 | - name: Upload files 73 | uses: ./.github/actions/upload 74 | with: 75 | token: ${{ secrets.GITHUB_TOKEN }} 76 | secretId: ${{ secrets.SECRET_ID }} 77 | secretKey: ${{ secrets.SECRET_KEY }} 78 | bucket: ${{ secrets.COS_BUCKET }} 79 | files: release/PureLauncher.dmg 80 | 81 | build_linux: 82 | runs-on: ubuntu-latest 83 | needs: build_macos 84 | 85 | steps: 86 | - name: Check out git repository 87 | uses: actions/checkout@v1 88 | 89 | - name: Install Node.js and npm 90 | uses: actions/setup-node@v1 91 | with: 92 | node-version: 12 93 | 94 | - name: Install dependencies 95 | run: npm install 96 | 97 | - name: Download artifact 98 | uses: actions/download-artifact@v1 99 | with: 100 | name: dist 101 | 102 | - name: Pack app 103 | run: npm run pack 104 | 105 | - name: Upload files to Github release 106 | uses: ./.github/actions/upload 107 | with: 108 | token: ${{ secrets.GITHUB_TOKEN }} 109 | secretId: ${{ secrets.SECRET_ID }} 110 | secretKey: ${{ secrets.SECRET_KEY }} 111 | bucket: ${{ secrets.COS_BUCKET }} 112 | final: true 113 | files: release/PureLauncher.tar.gz release/PureLauncher.deb release/PureLauncher.rpm 114 | -------------------------------------------------------------------------------- /.github/workflows/translation.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | paths: 8 | - 'langs/*.json' 9 | 10 | jobs: 11 | lint: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Check out git repository 16 | uses: actions/checkout@v1 17 | 18 | - name: Install Node.js and npm 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: 12 22 | 23 | - name: Install dependencies 24 | run: npm install --production 25 | 26 | - name: Lint translation 27 | run: node packages/translation-tools/cli.js lint $(curl https://api.github.com/repos/Apisium/PureLauncher/pulls/${{ github.event.pull_request.number }}/files | grep '"filename":' | sed -e 's#.*"lang/\(.*\)",\?#"\1"#g') 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .cache 4 | *.log 5 | release 6 | .idea 7 | app/package-lock.json 8 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | build_from_source=true 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Apisium 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | Copyright (c) 2020 Apisium 24 | 25 | Without permission by Apisium, do not use our logo and name outside this 26 | project, modify the copyright, name, logo and author information is not 27 | allowed too. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PureLauncher ![GitHub package.json version](https://img.shields.io/github/package-json/v/Apisium/PureLauncher) ![release](https://github.com/Apisium/PureLauncher/workflows/release/badge.svg) [![codebeat badge](https://codebeat.co/badges/2afd1913-119b-4b47-acb8-dbac54259a4e)](https://codebeat.co/projects/github-com-apisium-purelauncher-master) ![GitHub](https://img.shields.io/github/license/Apisium/PureLauncherOfficialPlugins) ![dependencies](https://david-dm.org/Apisium/PureLauncher/dev-status.svg) [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) 2 | 3 | An awesome Minecraft Launcher. 4 | 5 | [Official Website](https://pl.apisium.cn) 6 | 7 | ## Features 8 | 9 | - 🚀 Fast launching. 10 | - 💘 Beautiful GUI. 11 | - 🎉 Custom protocol. 12 | - 🤔 Powerful extension and rich API. 13 | - 🎁 One-click download and install resources. 14 | - ✨ And more... 15 | 16 | ## Snapshots 17 | 18 | ![Home](./screenshots/home.png) 19 | 20 | ![Accounts](./screenshots/accounts.png) 21 | 22 | ![Settings](./screenshots/versions.png) 23 | 24 | ## Documentation 25 | 26 | For plugins developers, resource creators, testers and advanced users: 27 | 28 | [https://github.com/Apisium/PureLauncher/wiki](https://github.com/Apisium/PureLauncher/wiki) 29 | 30 | ## Usage 31 | 32 | **Please do not use [Yarn](https://yarnpkg.com) !** 33 | 34 | ### Requirements 35 | 36 | - Nodejs v12.0.0+ 37 | 38 | ### Commands 39 | 40 | #### Install dependencies 41 | 42 | ```bash 43 | npm install 44 | ``` 45 | 46 | #### Use taobao registry *(China)* 47 | 48 | ```bash 49 | set REGISTRY=https://registry.npm.taobao.org/ 50 | set dist-url=http://npm.taobao.org/mirrors/atom-shell 51 | set ELECTRON_MIRROR=http://npm.taobao.org/mirrors/electron/ 52 | set ELECTRON_CUSTOM_DIR=8.2.5 53 | ``` 54 | 55 | #### Build 56 | 57 | ```bash 58 | npm run build 59 | 60 | npm run pack 61 | ``` 62 | 63 | #### Development 64 | 65 | ```bash 66 | # Run debugging server: 67 | npm start 68 | 69 | # Run Electron: 70 | npm run run 71 | ``` 72 | 73 | ## Authors 74 | 75 | - Shirasawa 76 | - CI010 77 | 78 | ## License 79 | 80 | [MIT](./LICENSE) 81 | -------------------------------------------------------------------------------- /app/main.js: -------------------------------------------------------------------------------- 1 | const r = require 2 | 3 | if (global.__loadUpdatePack) { 4 | r('./dist/src/main') 5 | return 6 | } 7 | 8 | const { join } = r('path') 9 | 10 | const dir = r('electron').app.getPath('userData') 11 | try { 12 | const json = r(join(dir, 'updates/entry-point.json')) 13 | if (json.version === r('./package.json').version) { 14 | r('./dist/src/main') 15 | return 16 | } 17 | global.__loadUpdatePack = true 18 | r(join(dir, 'updates/asar', json.file)) 19 | } catch (e) { r('./dist/src/main') } 20 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pure-launcher", 3 | "main": "main.js", 4 | "private": true, 5 | "version": "0.0.3", 6 | "author": "Shirasawa", 7 | "description": "An awesome Minecraft Launcher." 8 | } 9 | -------------------------------------------------------------------------------- /build/icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/build/icons/256x256.png -------------------------------------------------------------------------------- /build/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/build/icons/icon.icns -------------------------------------------------------------------------------- /build/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/build/icons/icon.ico -------------------------------------------------------------------------------- /build/installer.nsh: -------------------------------------------------------------------------------- 1 | !macro customInstall 2 | DetailPrint "Register pure-launcher URI Handler" 3 | DeleteRegKey HKCR "pure-launcher" 4 | WriteRegStr HKCR "pure-launcher" "" "URL:pure-launcher" 5 | WriteRegStr HKCR "pure-launcher" "EveHQ NG SSO authentication Protocol" "" 6 | WriteRegStr HKCR "pure-launcher\DefaultIcon" "" "$INSTDIR\${APP_EXECUTABLE_FILENAME}" 7 | WriteRegStr HKCR "pure-launcher\shell" "" "" 8 | WriteRegStr HKCR "pure-launcher\shell\Open" "" "" 9 | WriteRegStr HKCR "pure-launcher\shell\Open\command" "" "$INSTDIR\${APP_EXECUTABLE_FILENAME} %1" 10 | !macroend 11 | -------------------------------------------------------------------------------- /build/sync-version.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path') 2 | const { writeFileSync: w } = require('fs') 3 | 4 | const appPackage = join(__dirname, '../app/package.json') 5 | const { version, description } = require('../package.json') 6 | w(appPackage, `{ 7 | "name": "pure-launcher", 8 | "main": "main.js", 9 | "private": true, 10 | "version": "${version}", 11 | "author": "Shirasawa", 12 | "description": "${description}" 13 | }\n`) 14 | -------------------------------------------------------------------------------- /packages/babel-plugin-dollar/index.js: -------------------------------------------------------------------------------- 1 | module.exports = () => ({ 2 | visitor: { 3 | CallExpression (p) { 4 | const c = p.get('callee') 5 | if (c.isIdentifier() && c.node.name === '$') c.node.name = '__$pli0' 6 | } 7 | } 8 | }) 9 | -------------------------------------------------------------------------------- /packages/babel-plugin-dollar/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "babel-plugin-dollar", 3 | "version": "0.0.0", 4 | "private": true, 5 | "author": "Shirasawa" 6 | } 7 | -------------------------------------------------------------------------------- /packages/language-tools/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fn = require('./index') 4 | const argv = require('minimist')(process.argv.slice(2))._ 5 | 6 | if (argv.length > 1) { 7 | argv 8 | .slice(1) 9 | .filter(it => it !== 'zh-cn' && it !== 'zh-cn.json') 10 | .forEach(it => fn(argv[0], it)) 11 | } else fn() 12 | -------------------------------------------------------------------------------- /packages/language-tools/index.js: -------------------------------------------------------------------------------- 1 | const cn = require('../../langs/zh-cn.json') 2 | const fs = require('fs-extra') 3 | const c = require('chalk') 4 | const glob = require('globby') 5 | const ts = require('typescript') 6 | const { join, basename } = require('path') 7 | 8 | module.exports = async (cmd, langName = '') => { 9 | if (cmd === 'check') { 10 | const files = await glob(langName) 11 | const lang = { } 12 | const visit = (node, root) => { 13 | if (ts.isCallExpression(node)) { 14 | const caller = node.expression 15 | if (ts.isIdentifier(caller) && caller.text === '$' && node.arguments.length) { 16 | const visitKey = expr => { 17 | if (ts.isStringLiteral(expr)) lang[expr.text] = root 18 | else if (ts.isConditionalExpression(expr)) { 19 | visitKey(expr.whenTrue) 20 | visitKey(expr.whenFalse) 21 | } else { 22 | console.error('❌ ' + c.redBright('Unknown expression type: ') + 23 | c.bgYellowBright.black(' ' + expr.kind + ' ')) 24 | process.exit(-1) 25 | } 26 | } 27 | visitKey(node.arguments[0]) 28 | } 29 | } 30 | node.forEachChild(n => visit(n, root)) 31 | } 32 | ;(await Promise.all(files.map(name => fs.readFile(name)))).forEach((it, i) => { 33 | visit(ts.createSourceFile(files[i], it.toString(), ts.ScriptTarget.Latest), files[i]) 34 | }) 35 | 36 | lang.$LanguageName$ = lang.$readmeEn = '' 37 | const missing = Object.keys(lang).filter(it => !(it in cn)) 38 | const redundant = Object.keys(cn).filter(it => !(it in lang)) 39 | 40 | if (missing.length) { 41 | const symbol = c.redBright('-') 42 | console.log('⚠️ ' + c.yellowBright(`Missing entries detected: (${missing.length})`)) 43 | console.log(missing.map(it => ` ${symbol} ${it} : ${c.gray(lang[it])}`).join('\n'), '\n') 44 | } else console.log('✔ ' + c.greenBright('No missing entry!')) 45 | 46 | if (redundant.length) { 47 | const symbol = c.greenBright('+') 48 | console.log('⚠️ ' + c.yellowBright(`Redundant entries detected: (${redundant.length})`)) 49 | console.log(redundant.map(it => ' ' + symbol + ' ' + it).join('\n'), '\n') 50 | } else console.log('✔ ' + c.greenBright('No redundant entry!')) 51 | 52 | if (redundant.length + missing.length) { 53 | console.error('❌ ' + c.redBright('Failed to pass the linting.')) 54 | process.exit(-1) 55 | } 56 | } else { 57 | const commands = ['lint', 'sync'] 58 | 59 | langName = basename(langName, '.json') 60 | if (langName === 'zh-cn') return 61 | if (!commands.includes(cmd) || !langName) { 62 | console.error('❌ ' + c.redBright('Unknown command ' + c.bgYellowBright.black(' ' + cmd + ' ') + ', please ' + 63 | c.bgGreenBright.black(' https://github.com/Apisium/PureLauncher/wiki/Tools_Language ') + 64 | ' To get more information.')) 65 | process.exit(-1) 66 | } 67 | const langFile = join(__dirname, '../../lang', langName + '.json') 68 | 69 | let lang 70 | try { lang = await fs.readJson(langFile) } catch (e) { 71 | console.error(e) 72 | console.error('❌ ' + c.redBright('Unable to read language file: ' + c.bgYellowBright.black(` ${langName}.json `))) 73 | process.exit(-1) 74 | } 75 | 76 | delete cn.$readmeEn 77 | const missing = Object.keys(cn).filter(it => !(it in lang)) 78 | const redundant = Object.keys(lang).filter(it => !(it in cn)) 79 | 80 | if (missing.length) { 81 | const symbol = c.redBright('-') 82 | console.log('⚠️ ' + c.yellowBright(`Missing entries detected: (${missing.length})`)) 83 | console.log(missing.map(it => ' ' + symbol + ' ' + it).join('\n'), '\n') 84 | } else console.log('✔ ' + c.greenBright('No missing entry!')) 85 | 86 | if (redundant.length) { 87 | const symbol = c.greenBright('+') 88 | console.log('⚠️ ' + c.yellowBright(`Redundant entries detected: (${redundant.length})`)) 89 | console.log(redundant.map(it => ' ' + symbol + ' ' + it).join('\n'), '\n') 90 | } else console.log('✔ ' + c.greenBright('No redundant entry!')) 91 | 92 | switch (cmd) { 93 | case 'lint': { 94 | const empty = Object.entries(lang).filter(it => !it[1]).map(it => it[0]) 95 | if (empty.length) { 96 | const symbol = c.yellowBright('!') 97 | console.log('⚠️ ' + c.yellowBright(`Empty entries detected: (${empty.length})`)) 98 | console.log(empty.map(it => ' ' + symbol + ' ' + it).join('\n'), '\n') 99 | } else console.log('✔ ' + c.greenBright('No empty entry!')) 100 | if (empty.length + redundant.length + missing.length) { 101 | console.error('❌ ' + c.redBright('Failed to pass the linting.')) 102 | process.exit(-1) 103 | } 104 | break 105 | } 106 | case 'sync': { 107 | if (missing.length + redundant.length) { 108 | missing.forEach(it => (lang[it] = '')) 109 | redundant.forEach(it => delete lang[it]) 110 | try { 111 | await fs.writeFile(langFile, JSON.stringify(lang, null, 2) + '\n') 112 | console.log('✔ ' + c.greenBright('Save file successfully!')) 113 | } catch (e) { 114 | console.error(e) 115 | console.error('❌ ' + c.redBright('Cannot save file: ' + c.bgYellowBright.black(` ${langName}.json `))) 116 | process.exit(-1) 117 | } 118 | } else console.log('✔ ' + c.greenBright('No need to synchronize!')) 119 | } 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /packages/minimize-three/index.js: -------------------------------------------------------------------------------- 1 | const { MOUSE, DoubleSide, FrontSide, NearestFilter } = require('three/src/constants') 2 | const W = require('three/src/renderers/WebGLRenderer').WebGLRenderer 3 | 4 | Object.defineProperty(W.prototype, 'context', { get () { return this.getContext() } }) 5 | 6 | exports.WebGLRenderer = W 7 | exports.MOUSE = MOUSE 8 | exports.DoubleSide = DoubleSide 9 | exports.FrontSide = FrontSide 10 | exports.NearestFilter = NearestFilter 11 | exports.Object3D = require('three/src/core/Object3D').Object3D 12 | exports.BoxGeometry = require('three/src/geometries/BoxGeometry').BoxGeometry 13 | exports.MeshBasicMaterial = require('three/src/materials/MeshBasicMaterial').MeshBasicMaterial 14 | exports.Vector2 = require('three/src/math/Vector2').Vector2 15 | exports.Group = require('three/src/objects/Group').Group 16 | exports.Mesh = require('three/src/objects/Mesh').Mesh 17 | exports.Clock = require('three/src/core/Clock').Clock 18 | exports.Camera = require('three/src/cameras/Camera').Camera 19 | exports.OrthographicCamera = require('three/src/cameras/OrthographicCamera').OrthographicCamera 20 | exports.PerspectiveCamera = require('three/src/cameras/PerspectiveCamera').PerspectiveCamera 21 | exports.EventDispatcher = require('three/src/core/EventDispatcher').EventDispatcher 22 | exports.Quaternion = require('three/src/math/Quaternion').Quaternion 23 | exports.Spherical = require('three/src/math/Spherical').BoxGeometry 24 | exports.Vector3 = require('three/src/math/Vector3').Vector3 25 | exports.Spherical = require('three/src/math/Spherical').Spherical 26 | exports.Scene = require('three/src/scenes/Scene').Scene 27 | exports.Texture = require('three/src/textures/Texture').Texture 28 | -------------------------------------------------------------------------------- /packages/parcel-plugin-replacer/Asset.js: -------------------------------------------------------------------------------- 1 | const { builtinModules } = require('module') 2 | 3 | // eslint-disable-next-line node/no-deprecated-api 4 | const ms = builtinModules.filter(x => !/^_|^(internal|v8|node-inspect)\/|\//.test(x) && x !== 'sys') 5 | ms.push('electron') 6 | const noBuilds = new Set(ms) 7 | 8 | const DEV = process.env.NODE_ENV !== 'production' 9 | const map = { 10 | 'process.env.NODE_ENV': process.env.NODE_ENV === 'production' ? '"production"' : '$&', 11 | '@(? noBuilds.add(it)) 23 | } 24 | } catch (e) { } 25 | Object.entries(map).forEach(([key, value]) => { 26 | keys.push(new RegExp(key.startsWith('@') ? key.slice(1) : key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')) 27 | values.push(value.toString()) 28 | }) 29 | 30 | const replace = code => keys.reduce((p, key, i) => p.replace(key, values[i]), code) 31 | module.exports = A => class Asset extends A { 32 | generate () { 33 | this.contents = replace(this.contents) 34 | return super.generate() 35 | } 36 | parse (code) { 37 | if (code.includes('forge-site')) console.log(code) 38 | return super.parse(replace(code)) 39 | } 40 | addDependency (name, opts) { 41 | if (!noBuilds.has(name)) super.addDependency(name, opts) 42 | } 43 | load () { 44 | return super.load().then(it => typeof it === 'string' ? replace(it) : it) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/parcel-plugin-replacer/JSAsset.js: -------------------------------------------------------------------------------- 1 | global.JSAsset = module.exports = require('./Asset')(global.JSAsset || require('parcel/lib/assets/JSAsset')) 2 | -------------------------------------------------------------------------------- /packages/parcel-plugin-replacer/JSONAsset.js: -------------------------------------------------------------------------------- 1 | const ParcelJSONAsset = require('parcel/lib/assets/JSONAsset') 2 | 3 | global.JSONAsset = module.exports = class JSONAsset extends ParcelJSONAsset { 4 | generate () { 5 | if (!this.ast && this.contents.includes('pure-launcher')) { 6 | try { 7 | const json = JSON.parse(this.contents) 8 | if (json.author && json.name === 'pure-launcher' && json.author.name === 'Shirasawa') { 9 | return `exports.version='${json.version}'` 10 | } 11 | } catch (e) { console.error(e) } 12 | } 13 | let code 14 | if (this.ast) code = JSON.stringify(this.ast) 15 | else { 16 | try { code = JSON.stringify(JSON.parse(this.contents)) } catch (e) { 17 | console.error(e) 18 | code = this.contents 19 | } 20 | } 21 | return `module.exports=JSON.parse(${JSON.stringify(code)})` 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/parcel-plugin-replacer/TypeScriptAsset.js: -------------------------------------------------------------------------------- 1 | global.TypeScriptAsset = module.exports = require('./Asset')(global.TypeScriptAsset || require('parcel/lib/assets/TypeScriptAsset')) 2 | -------------------------------------------------------------------------------- /packages/parcel-plugin-replacer/index.js: -------------------------------------------------------------------------------- 1 | module.exports = b => { 2 | const js = require.resolve('./JSAsset') 3 | const ts = require.resolve('./TypeScriptAsset') 4 | b.addAssetType('js', js) 5 | b.addAssetType('jsx', js) 6 | b.addAssetType('es6', js) 7 | b.addAssetType('mjs', js) 8 | b.addAssetType('jsm', js) 9 | b.addAssetType('ts', ts) 10 | b.addAssetType('tsx', ts) 11 | b.addAssetType('json', require.resolve('./JSONAsset')) 12 | b.addAssetType('unpack.js', require.resolve('parcel/lib/assets/RawAsset')) 13 | } 14 | -------------------------------------------------------------------------------- /packages/parcel-plugin-replacer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parcel-plugin-replacer", 3 | "version": "0.0.0", 4 | "private": true 5 | } 6 | -------------------------------------------------------------------------------- /packages/parcel-plugin-v8-cache/JSPackager.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra') 2 | const ParcelJSPackager = global.JSPackager || require('parcel/lib/packagers/JSPackager') 3 | const { minify } = require('terser') 4 | const { relative, join, dirname, basename } = require('path') 5 | 6 | const EXCLUDES = [] 7 | 8 | const OPTIONS = { compress: { ecma: 8 }, output: { beautify: false, comments: false, ecma: 8 } } 9 | global.JSPackager = module.exports = class JSPackager extends ParcelJSPackager { 10 | constructor (a, b) { 11 | super(a, b) 12 | this.v8CacheFile = join(b.options.outDir, 'v8-cache.js') 13 | } 14 | async setup () { 15 | await super.setup() 16 | if (!await fs.pathExists(this.v8CacheFile)) { 17 | const data = await fs.readFile(require.resolve('v8-compile-cache')) 18 | await fs.writeFile(this.v8CacheFile, minify(data.toString(), OPTIONS).code) 19 | } 20 | let file = this.bundle.name 21 | if (EXCLUDES.every(it => !this.bundle.name.includes(it))) { 22 | file = file.replace(/js$/, 'c.js') 23 | const cFile = relative(dirname(this.bundle.name), this.v8CacheFile).replace(/\\/g, '/') 24 | this.dest.end(`require('./${cFile}');require('./${basename(file)}')`) 25 | } else this.dest.end('throw new Error("Compile error!")') 26 | let code = '' 27 | this.dest = { 28 | get bytesWritten () { return code.length }, 29 | write (chunk = '') { 30 | code += chunk 31 | return Promise.resolve() 32 | }, 33 | end (chunk = '') { 34 | code += chunk 35 | if (code.includes('Object.defineProperty')) { 36 | code = '_ODP=Object.defineProperty,' + code.replace(/Object\.defineProperty/g, '_ODP') 37 | } 38 | code = minify(code, OPTIONS).code 39 | return fs.writeFile(file, code) 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/parcel-plugin-v8-cache/index.js: -------------------------------------------------------------------------------- 1 | module.exports = b => { 2 | if (process.env.NODE_ENV === 'production') b.addPackager('js', require.resolve('./JSPackager')) 3 | } 4 | -------------------------------------------------------------------------------- /packages/parcel-plugin-v8-cache/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parcel-plugin-v8-cache", 3 | "version": "0.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "buffer-from": { 8 | "version": "1.1.1", 9 | "resolved": "http://registry.npm.taobao.org/buffer-from/download/buffer-from-1.1.1.tgz", 10 | "integrity": "sha1-MnE7wCj3XAL9txDXx7zsHyxgcO8=" 11 | }, 12 | "commander": { 13 | "version": "2.20.3", 14 | "resolved": "https://registry.npm.taobao.org/commander/download/commander-2.20.3.tgz", 15 | "integrity": "sha1-/UhehMA+tIgcIHIrpIA16FMa6zM=" 16 | }, 17 | "source-map": { 18 | "version": "0.6.1", 19 | "resolved": "http://registry.npm.taobao.org/source-map/download/source-map-0.6.1.tgz", 20 | "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=" 21 | }, 22 | "source-map-support": { 23 | "version": "0.5.16", 24 | "resolved": "https://registry.npm.taobao.org/source-map-support/download/source-map-support-0.5.16.tgz?cache=0&sync_timestamp=1572389965235&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsource-map-support%2Fdownload%2Fsource-map-support-0.5.16.tgz", 25 | "integrity": "sha1-CuBp5/47p1OMZMmFFeNTOerFoEI=", 26 | "requires": { 27 | "buffer-from": "^1.0.0", 28 | "source-map": "^0.6.0" 29 | } 30 | }, 31 | "terser": { 32 | "version": "4.4.3", 33 | "resolved": "https://registry.npm.taobao.org/terser/download/terser-4.4.3.tgz", 34 | "integrity": "sha1-QBq8UriIac+QRBJQOx632gk64vA=", 35 | "requires": { 36 | "commander": "^2.20.0", 37 | "source-map": "~0.6.1", 38 | "source-map-support": "~0.5.12" 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/parcel-plugin-v8-cache/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parcel-plugin-v8-cache", 3 | "version": "0.0.0", 4 | "private": true, 5 | "author": "Shirasawa", 6 | "dependencies": { 7 | "terser": "^4.4.3" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/plugin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('v8-compile-cache') 4 | const Bundler = require('parcel') 5 | const c = require('chalk') 6 | const asar = require('asar') 7 | const api = require('../web-api') 8 | const parseArgs = require('minimist') 9 | const os = require('os') 10 | const fs = require('fs-extra') 11 | const replacer = require('../parcel-plugin-replacer') 12 | const { createHash } = require('crypto') 13 | const { resolve, join, dirname } = require('path') 14 | 15 | const argv = parseArgs(process.argv.slice(2), { 16 | alias: { e: 'entry', d: 'dist', p: 'port', f: 'file' }, 17 | default: { replacer: true, remove: true } 18 | }) 19 | 20 | const ENTRY = argv.entry || './src/index.ts' 21 | const DIST = resolve(argv.dist || 'dist') 22 | const DEV_PLUGIN = join(DIST, 'index.js') 23 | 24 | const MESSAGES = { 25 | NOT_DEV: '❌ ' + c.redBright('PureLauncher is not currently in DEV mode. Please set environment variable ' + 26 | c.bgGreenBright.black(' DEV=true ')), 27 | NOT_RUNNING: '❌ ' + c.redBright('PureLauncher has not been ran yet. Please set environment variable ' + 28 | c.bgGreenBright.black(' DEV=true ') + ' Then run PureLauncher.'), 29 | UNKNOWN_COMMAND: '❌ ' + c.redBright('Unknown command, please ' + 30 | c.bgGreenBright.black(' https://github.com/Apisium/PureLauncher/wiki/Tools_Plugin_Development ') + 31 | ' To get more information.'), 32 | REMOVED: '🍀 ' + c.greenBright('Removed files:'), 33 | PACKING: '📦 ' + c.blueBright('Packing...'), 34 | PACKED: '📦 ' + c.greenBright('Packed successfully:'), 35 | WITHOUT_PACKAGE: '⚠️ ' + c.yellowBright('The ' + c.bgGreenBright.black(' package.json ') + 36 | ' is not found or missing the ' + c.bgGreenBright.black(' name ') + ' and ' + 37 | c.bgGreenBright.black(' version ') + ' fields.') 38 | } 39 | 40 | let BUILD = false 41 | switch (argv._[0]) { 42 | case 'build': 43 | BUILD = true 44 | process.env.NODE_ENV = 'production' 45 | break 46 | case '': 47 | case null: 48 | case undefined: 49 | process.env.NODE_ENV = 'development' 50 | break 51 | default: 52 | console.error(MESSAGES.UNKNOWN_COMMAND) 53 | process.exit(-1) 54 | } 55 | 56 | (async () => { 57 | if (BUILD) { 58 | if (argv.remove) { 59 | await fs.remove(DIST) 60 | console.info(MESSAGES.REMOVED, DIST) 61 | } 62 | } else { 63 | if (argv.port) api.setPort(parseInt(argv.port, 10)) 64 | try { 65 | if (!(await api.info()).isDev) { 66 | console.error(MESSAGES.NOT_DEV) 67 | process.exit(-1) 68 | } 69 | } catch (e) { 70 | console.error(e) 71 | console.error(MESSAGES.NOT_RUNNING) 72 | process.exit(-1) 73 | } 74 | } 75 | const bundler = new Bundler(ENTRY, { 76 | watch: !BUILD, 77 | production: BUILD, 78 | outDir: './dist', 79 | outFile: 'index.js', 80 | publicUrl: '.', 81 | target: 'electron', 82 | sourceMaps: false, 83 | bundleNodeModules: true 84 | }) 85 | if (argv.replacer) replacer(bundler) 86 | if (!BUILD) { 87 | bundler.on('bundled', async () => { 88 | try { 89 | const info = await api.info() 90 | if (!info.isDev) { 91 | console.error(MESSAGES.NOT_DEV) 92 | process.exit(-1) 93 | } 94 | if (info.devPlugin !== DEV_PLUGIN) await api.post('setDevPlugin', { path: DEV_PLUGIN }) 95 | await api.reload() 96 | } catch (e) { 97 | console.error(e) 98 | console.error(MESSAGES.NOT_RUNNING) 99 | process.exit(-1) 100 | } 101 | }) 102 | } 103 | await bundler.bundle() 104 | if (BUILD) { 105 | console.info(MESSAGES.PACKING) 106 | const pkg = await fs.readJson('./package.json', { throws: false }) || { } 107 | pkg.main = 'index.js' 108 | if (!pkg.name || !pkg.version) console.warn(MESSAGES.WITHOUT_PACKAGE) 109 | await fs.writeJson(join(DIST, 'package.json'), pkg) 110 | const temp = join(os.tmpdir(), Date.now().toString(36) + Math.random().toString(36)) 111 | await asar.createPackage(DIST, temp) 112 | let file = argv.file 113 | if (!file) { 114 | file = 'build/' + (await new Promise((resolve, e) => { 115 | const s = createHash('sha1').setEncoding('hex') 116 | fs.createReadStream(temp).on('error', e).pipe(s).on('error', e).on('finish', () => resolve(s.read())) 117 | })) + '.asar' 118 | } 119 | if (await fs.pathExists(file)) await fs.unlink(file) 120 | await fs.ensureDir(dirname(file)) 121 | await fs.move(temp, file) 122 | console.log(MESSAGES.PACKED, file) 123 | } 124 | })().catch(e => { 125 | console.error(e) 126 | process.exit(-1) 127 | }) 128 | -------------------------------------------------------------------------------- /packages/plugin/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from '../../src/plugin/exports' 2 | -------------------------------------------------------------------------------- /packages/plugin/index.js: -------------------------------------------------------------------------------- 1 | if (typeof PureLauncherPluginExports !== 'object') throw new Error('Incorrect runtime!') 2 | module.exports = window.PureLauncherPluginExports 3 | -------------------------------------------------------------------------------- /packages/web-api/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /packages/web-api/dev.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /packages/web-api/index.d.ts: -------------------------------------------------------------------------------- 1 | import { queryStatus } from '@xmcl/client' 2 | import * as T from '../../src/protocol/types' 3 | 4 | export * from '../../src/protocol/types' 5 | 6 | /* eslint-disable camelcase */ 7 | export interface PureLauncherInfo { 8 | isDev: boolean 9 | devPlugin: string 10 | arch: string 11 | platform: string 12 | versions: { 13 | node: string 14 | v8: string 15 | uv: string 16 | zlib: string 17 | brotli: string 18 | ares: string 19 | modules: string 20 | nghttp2: string 21 | napi: string 22 | llhttp: string 23 | http_parser: string 24 | openssl: string 25 | icu: string 26 | unicode: string 27 | electron: string 28 | chrome: string 29 | pure_launcher: string 30 | } 31 | } 32 | 33 | export const post: (url: string, body: any) => Promise 34 | export const setPort: (port: number) => void 35 | export const getPort: () => number 36 | export const reload: () => Promise 37 | export const info: () => Promise 38 | export const protocol: (data: T.Protocol) => Promise 39 | export const setDevPlugin: (path: string) => Promise 40 | export const isRunning: () => Promise 41 | export const ensureRunning: (time?: number) => Promise 42 | export const queryMinecraftServer: typeof queryStatus 43 | export const getAccount: () => Promise<{ uuid: string, name: string, type: string, skinUrl?: string }> 44 | -------------------------------------------------------------------------------- /packages/web-api/index.js: -------------------------------------------------------------------------------- 1 | const f = (typeof window !== 'undefined' && window.fetch) || 2 | (typeof global !== 'undefined' && global.fetch) || (typeof self !== 'undefined' && self.fetch) || 3 | eval('require && require("node-fetch")') 4 | if (!f) throw new Error('Can not find the fetch function.') 5 | 6 | let PORT = 46781 7 | export const post = (url, body) => f(`http://127.0.0.1:${PORT}/${url}`, 8 | { method: 'POST', body: JSON.stringify(body), headers: { 'Content-Type': 'application/json' } }) 9 | .then(it => it.json()) 10 | 11 | export const setPort = port => { 12 | if (!port || isNaN(port)) throw new TypeError(`The port ${port} is not an integer!`) 13 | PORT = port 14 | } 15 | export const getPort = () => PORT 16 | export const isRunning = () => f(`http://127.0.0.1:${PORT}/info`).then(() => true, () => false) 17 | export const reload = () => f(`http://127.0.0.1:${PORT}/reload`).then(it => it.json()).then(it => it.success) 18 | export const info = () => f(`http://127.0.0.1:${PORT}/info`).then(it => it.json()) 19 | export const protocol = data => post('protocol', data) 20 | export const setDevPlugin = path => post('setDevPlugin', { path }).then(it => it.success) 21 | export const ensureRunning = time => isRunning().then(r => r ? undefined : new Promise((resolve, reject) => { 22 | location.assign('pure-launcher://') 23 | const timer = setTimeout(reject, time || 15000, new Error('Timeout!')) 24 | const f = () => void isRunning().then(r => r ? (clearTimeout(timer), resolve()) : f()) 25 | f() 26 | })) 27 | 28 | const _global = typeof window === 'object' ? window : {} 29 | export const queryMinecraftServer = _global.queryMinecraftServer 30 | export const getAccount = _global.getAccount 31 | -------------------------------------------------------------------------------- /packages/web-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pure-launcher-api", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "dist/index.js", 6 | "types": "index.d.ts", 7 | "devDependencies": { 8 | "@babel/core": "^7.7.7", 9 | "bili": "^4.8.1" 10 | }, 11 | "scripts": { 12 | "build": "bili index.js --format cjs --format umd-min --format esm --module-name PureLauncherApi --minimal --bundle-node-modules" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /screenshots/accounts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/screenshots/accounts.png -------------------------------------------------------------------------------- /screenshots/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/screenshots/home.png -------------------------------------------------------------------------------- /screenshots/versions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/screenshots/versions.png -------------------------------------------------------------------------------- /src/assets/iconfont.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'iconfont'; 3 | src: url('iconfont.ttf') format('truetype'); 4 | } 5 | 6 | .iconfont { 7 | font-family: 'iconfont' !important; 8 | font-size: 16px; 9 | font-style: normal; 10 | -webkit-font-smoothing: antialiased; 11 | } 12 | 13 | .icon-tuoruwenjian:before { 14 | content: "\e629"; 15 | } 16 | 17 | .icon-minecraft:before { 18 | content: "\e63f"; 19 | } 20 | 21 | .icon-icons-minecraft_pic:before { 22 | content: "\ed9e"; 23 | } 24 | 25 | .icon-shanchu_o:before { 26 | content: "\eb84"; 27 | } 28 | 29 | .icon-shuliang-zengjia_o:before { 30 | content: "\eb83"; 31 | } 32 | 33 | -------------------------------------------------------------------------------- /src/assets/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/iconfont.ttf -------------------------------------------------------------------------------- /src/assets/images/bg-grass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/bg-grass.png -------------------------------------------------------------------------------- /src/assets/images/bg-snow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/bg-snow.png -------------------------------------------------------------------------------- /src/assets/images/bg-wool-dark-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/bg-wool-dark-light.png -------------------------------------------------------------------------------- /src/assets/images/bg-wool-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/bg-wool-dark.png -------------------------------------------------------------------------------- /src/assets/images/bg-wool-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/bg-wool-white.png -------------------------------------------------------------------------------- /src/assets/images/compass_19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/compass_19.png -------------------------------------------------------------------------------- /src/assets/images/icons/Bedrock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Bedrock.png -------------------------------------------------------------------------------- /src/assets/images/icons/Bookshelf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Bookshelf.png -------------------------------------------------------------------------------- /src/assets/images/icons/Brick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Brick.png -------------------------------------------------------------------------------- /src/assets/images/icons/Cake.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Cake.png -------------------------------------------------------------------------------- /src/assets/images/icons/Carved_Pumpkin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Carved_Pumpkin.png -------------------------------------------------------------------------------- /src/assets/images/icons/Chest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Chest.png -------------------------------------------------------------------------------- /src/assets/images/icons/Clay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Clay.png -------------------------------------------------------------------------------- /src/assets/images/icons/Coal_Block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Coal_Block.png -------------------------------------------------------------------------------- /src/assets/images/icons/Coal_Ore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Coal_Ore.png -------------------------------------------------------------------------------- /src/assets/images/icons/Cobblestone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Cobblestone.png -------------------------------------------------------------------------------- /src/assets/images/icons/Crafting_Table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Crafting_Table.png -------------------------------------------------------------------------------- /src/assets/images/icons/Creeper_Head.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Creeper_Head.png -------------------------------------------------------------------------------- /src/assets/images/icons/Diamond_Block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Diamond_Block.png -------------------------------------------------------------------------------- /src/assets/images/icons/Diamond_Ore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Diamond_Ore.png -------------------------------------------------------------------------------- /src/assets/images/icons/Dirt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Dirt.png -------------------------------------------------------------------------------- /src/assets/images/icons/Dirt_Podzol.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Dirt_Podzol.png -------------------------------------------------------------------------------- /src/assets/images/icons/Dirt_Snow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Dirt_Snow.png -------------------------------------------------------------------------------- /src/assets/images/icons/Emerald_Block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Emerald_Block.png -------------------------------------------------------------------------------- /src/assets/images/icons/Emerald_Ore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Emerald_Ore.png -------------------------------------------------------------------------------- /src/assets/images/icons/Enchanting_Table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Enchanting_Table.png -------------------------------------------------------------------------------- /src/assets/images/icons/End_Stone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/End_Stone.png -------------------------------------------------------------------------------- /src/assets/images/icons/Farmland.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Farmland.png -------------------------------------------------------------------------------- /src/assets/images/icons/Furnace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Furnace.png -------------------------------------------------------------------------------- /src/assets/images/icons/Furnace_On.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Furnace_On.png -------------------------------------------------------------------------------- /src/assets/images/icons/Glass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Glass.png -------------------------------------------------------------------------------- /src/assets/images/icons/Glazed_Terracotta_Light_Blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Glazed_Terracotta_Light_Blue.png -------------------------------------------------------------------------------- /src/assets/images/icons/Glazed_Terracotta_Orange.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Glazed_Terracotta_Orange.png -------------------------------------------------------------------------------- /src/assets/images/icons/Glazed_Terracotta_White.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Glazed_Terracotta_White.png -------------------------------------------------------------------------------- /src/assets/images/icons/Glowstone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Glowstone.png -------------------------------------------------------------------------------- /src/assets/images/icons/Gold_Block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Gold_Block.png -------------------------------------------------------------------------------- /src/assets/images/icons/Gold_Ore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Gold_Ore.png -------------------------------------------------------------------------------- /src/assets/images/icons/Grass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Grass.png -------------------------------------------------------------------------------- /src/assets/images/icons/Gravel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Gravel.png -------------------------------------------------------------------------------- /src/assets/images/icons/Hardened_Clay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Hardened_Clay.png -------------------------------------------------------------------------------- /src/assets/images/icons/Ice_Packed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Ice_Packed.png -------------------------------------------------------------------------------- /src/assets/images/icons/Iron_Block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Iron_Block.png -------------------------------------------------------------------------------- /src/assets/images/icons/Iron_Ore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Iron_Ore.png -------------------------------------------------------------------------------- /src/assets/images/icons/Lapis_Ore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Lapis_Ore.png -------------------------------------------------------------------------------- /src/assets/images/icons/Leaves_Birch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Leaves_Birch.png -------------------------------------------------------------------------------- /src/assets/images/icons/Leaves_Jungle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Leaves_Jungle.png -------------------------------------------------------------------------------- /src/assets/images/icons/Leaves_Oak.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Leaves_Oak.png -------------------------------------------------------------------------------- /src/assets/images/icons/Leaves_Spruce.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Leaves_Spruce.png -------------------------------------------------------------------------------- /src/assets/images/icons/Lectern_Book.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Lectern_Book.png -------------------------------------------------------------------------------- /src/assets/images/icons/Log_Acacia.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Log_Acacia.png -------------------------------------------------------------------------------- /src/assets/images/icons/Log_Birch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Log_Birch.png -------------------------------------------------------------------------------- /src/assets/images/icons/Log_DarkOak.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Log_DarkOak.png -------------------------------------------------------------------------------- /src/assets/images/icons/Log_Jungle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Log_Jungle.png -------------------------------------------------------------------------------- /src/assets/images/icons/Log_Oak.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Log_Oak.png -------------------------------------------------------------------------------- /src/assets/images/icons/Log_Spruce.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Log_Spruce.png -------------------------------------------------------------------------------- /src/assets/images/icons/Mycelium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Mycelium.png -------------------------------------------------------------------------------- /src/assets/images/icons/Nether_Brick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Nether_Brick.png -------------------------------------------------------------------------------- /src/assets/images/icons/Netherrack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Netherrack.png -------------------------------------------------------------------------------- /src/assets/images/icons/Obsidian.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Obsidian.png -------------------------------------------------------------------------------- /src/assets/images/icons/Planks_Acacia.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Planks_Acacia.png -------------------------------------------------------------------------------- /src/assets/images/icons/Planks_Birch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Planks_Birch.png -------------------------------------------------------------------------------- /src/assets/images/icons/Planks_DarkOak.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Planks_DarkOak.png -------------------------------------------------------------------------------- /src/assets/images/icons/Planks_Jungle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Planks_Jungle.png -------------------------------------------------------------------------------- /src/assets/images/icons/Planks_Oak.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Planks_Oak.png -------------------------------------------------------------------------------- /src/assets/images/icons/Planks_Spruce.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Planks_Spruce.png -------------------------------------------------------------------------------- /src/assets/images/icons/Quartz_Ore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Quartz_Ore.png -------------------------------------------------------------------------------- /src/assets/images/icons/Red_Sand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Red_Sand.png -------------------------------------------------------------------------------- /src/assets/images/icons/Red_Sandstone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Red_Sandstone.png -------------------------------------------------------------------------------- /src/assets/images/icons/Redstone_Block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Redstone_Block.png -------------------------------------------------------------------------------- /src/assets/images/icons/Redstone_Ore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Redstone_Ore.png -------------------------------------------------------------------------------- /src/assets/images/icons/Sand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Sand.png -------------------------------------------------------------------------------- /src/assets/images/icons/Sandstone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Sandstone.png -------------------------------------------------------------------------------- /src/assets/images/icons/Skeleton_Skull.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Skeleton_Skull.png -------------------------------------------------------------------------------- /src/assets/images/icons/Snow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Snow.png -------------------------------------------------------------------------------- /src/assets/images/icons/Soul_Sand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Soul_Sand.png -------------------------------------------------------------------------------- /src/assets/images/icons/Stone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Stone.png -------------------------------------------------------------------------------- /src/assets/images/icons/Stone_Andesite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Stone_Andesite.png -------------------------------------------------------------------------------- /src/assets/images/icons/Stone_Diorite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Stone_Diorite.png -------------------------------------------------------------------------------- /src/assets/images/icons/Stone_Granite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Stone_Granite.png -------------------------------------------------------------------------------- /src/assets/images/icons/TNT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/TNT.png -------------------------------------------------------------------------------- /src/assets/images/icons/Water.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Water.png -------------------------------------------------------------------------------- /src/assets/images/icons/Wool.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/icons/Wool.png -------------------------------------------------------------------------------- /src/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/logo.png -------------------------------------------------------------------------------- /src/assets/images/progress.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/progress.png -------------------------------------------------------------------------------- /src/assets/images/redstone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/redstone.png -------------------------------------------------------------------------------- /src/assets/images/steve-head.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/steve-head.png -------------------------------------------------------------------------------- /src/assets/images/steve.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/steve.png -------------------------------------------------------------------------------- /src/assets/images/terracotta/black_terracotta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/terracotta/black_terracotta.png -------------------------------------------------------------------------------- /src/assets/images/terracotta/blue_terracotta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/terracotta/blue_terracotta.png -------------------------------------------------------------------------------- /src/assets/images/terracotta/brown_terracotta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/terracotta/brown_terracotta.png -------------------------------------------------------------------------------- /src/assets/images/terracotta/cyan_terracotta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/terracotta/cyan_terracotta.png -------------------------------------------------------------------------------- /src/assets/images/terracotta/gray_terracotta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/terracotta/gray_terracotta.png -------------------------------------------------------------------------------- /src/assets/images/terracotta/green_terracotta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/terracotta/green_terracotta.png -------------------------------------------------------------------------------- /src/assets/images/terracotta/light_blue_terracotta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/terracotta/light_blue_terracotta.png -------------------------------------------------------------------------------- /src/assets/images/terracotta/light_gray_terracotta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/terracotta/light_gray_terracotta.png -------------------------------------------------------------------------------- /src/assets/images/terracotta/lime_terracotta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/terracotta/lime_terracotta.png -------------------------------------------------------------------------------- /src/assets/images/terracotta/magenta_terracotta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/terracotta/magenta_terracotta.png -------------------------------------------------------------------------------- /src/assets/images/terracotta/orange_terracotta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/terracotta/orange_terracotta.png -------------------------------------------------------------------------------- /src/assets/images/terracotta/pink_terracotta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/terracotta/pink_terracotta.png -------------------------------------------------------------------------------- /src/assets/images/terracotta/purple_terracotta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/terracotta/purple_terracotta.png -------------------------------------------------------------------------------- /src/assets/images/terracotta/red_terracotta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/terracotta/red_terracotta.png -------------------------------------------------------------------------------- /src/assets/images/terracotta/terracotta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/terracotta/terracotta.png -------------------------------------------------------------------------------- /src/assets/images/terracotta/white_terracotta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/terracotta/white_terracotta.png -------------------------------------------------------------------------------- /src/assets/images/terracotta/yellow_terracotta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/terracotta/yellow_terracotta.png -------------------------------------------------------------------------------- /src/assets/images/unknown-server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/unknown-server.png -------------------------------------------------------------------------------- /src/assets/images/written_book.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/written_book.png -------------------------------------------------------------------------------- /src/assets/images/zombie-head.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/images/zombie-head.png -------------------------------------------------------------------------------- /src/assets/minecraft.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/minecraft.otf -------------------------------------------------------------------------------- /src/assets/sounds/click.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/sounds/click.ogg -------------------------------------------------------------------------------- /src/assets/sounds/levelup.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/src/assets/sounds/levelup.ogg -------------------------------------------------------------------------------- /src/components/Avatar.tsx: -------------------------------------------------------------------------------- 1 | import './avatar.css' 2 | import React, { forwardRef } from 'react' 3 | import Img from 'react-image' 4 | import { AnimatePresence, motion } from 'framer-motion' 5 | 6 | const steve = require('../assets/images/steve.png') 7 | const Div = motion.div 8 | 9 | const Avatar: React.FC & { src: string | string[] }> = 10 | forwardRef(({ src, className = '', ...props }, ref) => 11 |
20 | {$('Avatar')}} 23 | unloader={{$('Avatar')}} 24 | /> 25 | 26 |
27 |
) 28 | 29 | export default Avatar 30 | -------------------------------------------------------------------------------- /src/components/Dots.tsx: -------------------------------------------------------------------------------- 1 | import './dots.less' 2 | import React from 'react' 3 | import ToolTip from 'rc-tooltip' 4 | 5 | interface P { 6 | count: number 7 | active: number 8 | names: string[] 9 | onChange: (p: number) => void 10 | } 11 | const Dots: React.FC

= props => { 12 | const arr = new Array(props.count) 13 | for (let i = 0; i < props.count; i++) { 14 | const active = i === props.active 15 | arr[i] =

{} : () => props.onChange(i)} 19 | className={active ? 'active' : undefined} 20 | /> 21 | 22 | } 23 | return
24 | {arr} 25 |
26 | } 27 | 28 | export default Dots 29 | -------------------------------------------------------------------------------- /src/components/Dropdown.tsx: -------------------------------------------------------------------------------- 1 | import './dropdown.less' 2 | import React, { useState } from 'react' 3 | import { motion } from 'framer-motion' 4 | 5 | const poses = { 6 | open: { width: 'auto' }, 7 | closed: { width: 0 } 8 | } 9 | const Dropdown: React.FC<{ open: boolean }> = (props) => { 10 | const [hover, setHover] = useState(false) 11 | return setHover(true)} 14 | onMouseLeave={() => setHover(false)} 15 | animate={props.open || hover ? poses.open : poses.closed} 16 | className='dropdown' 17 | > 18 |
{props.children}
19 |
20 | } 21 | 22 | export default Dropdown 23 | -------------------------------------------------------------------------------- /src/components/Empty.tsx: -------------------------------------------------------------------------------- 1 | import './empty.less' 2 | import React from 'react' 3 | 4 | export type Props = React.HTMLAttributes & { text?: string } 5 | const Empty: React.FC = ({ text, className = '', ...props }) => 6 |
7 | 8 |

{text || $('No Data')}

9 |
10 | 11 | export default Empty 12 | -------------------------------------------------------------------------------- /src/components/ErrorHandler.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import history from '../utils/history' 3 | 4 | const errorComponents: Record any> = { } 5 | history.listen(() => { 6 | const f = errorComponents[history.location.pathname] 7 | if (f) f() 8 | }) 9 | 10 | export const AUTO_RELOAD = { autoReload: true } 11 | 12 | export default class ErrorHandler extends React.Component<{ 13 | onError?: (e: any, info: string) => boolean 14 | autoReload?: boolean 15 | }> { 16 | public state = { error: false } 17 | private cachePath: string 18 | public static getDerivedStateFromError () { return { error: true } } 19 | public componentDidCatch (e, info) { 20 | if (this.props.autoReload) { 21 | errorComponents[this.cachePath = history.location.pathname] = () => this.setState({ error: false }) 22 | } 23 | if (!this.props.onError || !this.props.onError(e, info)) history.push('/error') 24 | } 25 | public UNSAFE_componentWillUpdate () { 26 | if (this.cachePath) { 27 | delete errorComponents[this.cachePath] 28 | this.cachePath = null 29 | } 30 | } 31 | public render () { 32 | return this.state.error ? React.createElement('div', {}) : this.props.children 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/components/IconButton.tsx: -------------------------------------------------------------------------------- 1 | import './icon-button.css' 2 | import React, { useMemo, forwardRef } from 'react' 3 | 4 | export const images = Object.values(require('../assets/images/terracotta/*.png')) 5 | .sort(() => 0.5 - Math.random()) as string[] 6 | 7 | export type Props = { title: string, icon?: string, hideFirst?: boolean } & React.HTMLAttributes 8 | 9 | let i = 0 10 | const IconButton: React.FC = forwardRef(({ icon, title, hideFirst, ...props }, ref) => { 11 | const src = useMemo(() => icon || (images[images.length <= i ? (i = 0) : i++]), [icon]) 12 | const t = title[0].toUpperCase() 13 | const c = t.charCodeAt(0) 14 | return 15 | {title} 16 | {(!hideFirst || !icon) && = 65 && c <= 90) || (c >= 48 && c <= 57) ? 'offset' : undefined} 19 | > 20 | {t}} 21 |

{title}

22 |
23 | }) 24 | 25 | export default IconButton 26 | -------------------------------------------------------------------------------- /src/components/IconPicker.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/no-noninteractive-element-to-interactive-role */ 2 | import './icon-picker.css' 3 | import React from 'react' 4 | import Dialog from 'rc-dialog' 5 | 6 | export const icons: Record = require('../assets/images/icons/*.png') 7 | export const resolveIcon = (url: string): string => url in icons ? icons[url] : typeof url === 'string' && 8 | url.startsWith('data:') ? url : icons.Grass 9 | 10 | const IconPicker: React.FC<{ open: boolean, onClose: (icon: string | null) => void }> = p => { 11 | return p.onClose(null)} 17 | visible={p.open} 18 | > 19 |
20 | {Object.entries(icons).map(([key, value]) => 21 | p.onClose(key)} alt={key} src={value} />)} 22 |
23 |
24 | } 25 | export default IconPicker 26 | -------------------------------------------------------------------------------- /src/components/InstallList.tsx: -------------------------------------------------------------------------------- 1 | import './install-list.less' 2 | import React, { useState, useMemo } from 'react' 3 | import Dialog from 'rc-dialog' 4 | import Treebeard from './treebeard/index' 5 | import * as T from '../protocol/types' 6 | 7 | let _setRes: any 8 | let resolve: any 9 | let reject: any 10 | let view: T.InstallView 11 | global.__requestInstallResources = (d: any, _view: any) => { 12 | if (reject) reject() 13 | view = _view 14 | const ret = new Promise(a => { 15 | resolve = () => { 16 | _setRes(null) 17 | resolve = reject = null 18 | a(true) 19 | } 20 | reject = () => { 21 | _setRes(null) 22 | resolve = reject = null 23 | a(false) 24 | } 25 | }) 26 | _setRes(d) 27 | return ret 28 | } 29 | 30 | const InstallList: React.FC = () => { 31 | const [res, setRes] = useState(null) 32 | _setRes = setRes 33 | const [data, setData] = useState({}) 34 | useMemo(() => { 35 | if (res && res.type === 'Version') { 36 | const mods = [] 37 | const servers = [] 38 | const resources = [] 39 | const plugins = [] 40 | const r = res as T.ResourceVersion 41 | if (typeof r.resources === 'object') { 42 | Object.values(r.resources).forEach(it => { 43 | switch (it.type) { 44 | case 'Mod': 45 | mods.push({ id: it.id, name: `${it.title || it.id}@${it.version}` }) 46 | break 47 | case 'Server': 48 | servers.push({ id: it.ip, name: it.title || (it.port ? `${it.ip}:${it.port}` : it.ip) }) 49 | break 50 | case 'ResourcePack': 51 | resources.push({ id: it.id, name: `${it.title || it.id}@${it.version}` }) 52 | break 53 | } 54 | }) 55 | } 56 | const d = { 57 | id: 'root', 58 | name: $('Detailed List'), 59 | toggled: true, 60 | children: [] 61 | } 62 | if (mods.length) d.children.push({ id: 'mods', name: $('Mods'), children: mods }) 63 | if (servers.length) d.children.push({ id: 'servers', name: $('Servers'), children: servers }) 64 | if (resources.length) d.children.push({ id: 'resources', name: $('ResourcePacks'), children: resources }) 65 | if (plugins.length) d.children.push({ id: 'plugins', name: $('Plugins'), children: resources }) 66 | setData(d) 67 | } else setData(res) 68 | }, [res]) 69 | 70 | const [cursor, setCursor] = useState(null) 71 | 72 | if (!res) return 73 | 74 | let name: string 75 | let comp: any 76 | let title: string 77 | switch (res.type) { 78 | case 'Version': { 79 | name = $('Version') 80 | const r = res as any 81 | if (r.$vanilla) title = $('Vanilla Minecraft') 82 | else if (r.$forge) title = 'Forge' 83 | else if (r.$fabric) title = 'Fabric' 84 | const ver = r.$forge?.version || r.$fabric?.version 85 | comp =
86 | {r.mcVersion &&

{$('Minecraft Version')}: {r.mcVersion}

} 87 | {ver &&

{$('VersionId')}: {ver}

} 88 | {data?.children?.length ? { 90 | if (cursor) cursor.active = false 91 | node.active = true 92 | if (node.children) node.toggled = toggled 93 | setCursor(node) 94 | setData(Object.assign({}, data)) 95 | }} 96 | /> : null} 97 |
98 | break 99 | } 100 | case 'Mod': { 101 | name = $('Mods') 102 | const r = res as T.ResourceMod 103 | comp = <> 104 | {r.mcVersion &&

{$('Minecraft Version')}: {r.mcVersion}

} 105 | {r.apis &&

{$('Apis')}: {Object.keys(r.apis).join(', ')}

} 106 | 107 | break 108 | } 109 | case 'ResourcePack': 110 | name = $('ResourcePacks') 111 | break 112 | case 'Server': { 113 | name = $('Servers') 114 | const r = res as T.ResourceServer 115 | comp =

{$('Host')}: {r.ip}{r.port ? ':' + r.port : null}

116 | break 117 | } 118 | case 'Plugin': 119 | name = $('Plugins') 120 | break 121 | } 122 | return 130 |

{$('Name')}: {title || res.title || res.id}

131 | {(res as any).version &&

{$('VersionId')}: {(res as any).version}

} 132 | {res.author &&

{$('Author')}: {res.author}

} 133 | {res.description &&

{$('Description')}: {res.description}

} 134 | {comp}{view && view.render && } 135 |
136 | 137 | 138 |
139 |
140 | } 141 | 142 | export default InstallList 143 | -------------------------------------------------------------------------------- /src/components/LiveRoute.tsx: -------------------------------------------------------------------------------- 1 | import CacheRoute, { CacheRouteProps } from 'react-router-cache-route' 2 | import React from 'react' 3 | import ErrorHandler, { AUTO_RELOAD } from './ErrorHandler' 4 | 5 | const hide = { className: 'route route-hide' } 6 | const show = { className: 'route route-show' } 7 | const behavior = (h: boolean) => h ? hide : show 8 | const LiveRoute: React.FC = p => React.createElement(ErrorHandler, AUTO_RELOAD, 9 | React.createElement(CacheRoute, { ...p, behavior })) 10 | export default LiveRoute 11 | -------------------------------------------------------------------------------- /src/components/Loading.tsx: -------------------------------------------------------------------------------- 1 | import './loading.less' 2 | import React from 'react' 3 | 4 | export type Props = React.HTMLAttributes & { text?: string } 5 | const Loading: React.FC = ({ text, className = '', ...props }) => 6 |
7 |
8 |

{text || $('Loading...')}

9 |
10 | 11 | export default Loading 12 | -------------------------------------------------------------------------------- /src/components/LoginDialog.tsx: -------------------------------------------------------------------------------- 1 | import './login-dialog.less' 2 | import React, { useState, Dispatch, SetStateAction } from 'react' 3 | import Dialog from 'rc-dialog' 4 | import { shell } from 'electron' 5 | import { Link } from 'react-router-dom' 6 | import * as Auth from '../plugin/Authenticator' 7 | 8 | const getObjectLength = (obj: any) => { 9 | let i = 0 10 | /* eslint-disable @typescript-eslint/no-unused-vars */ 11 | for (const _ in obj) i++ 12 | return i 13 | } 14 | 15 | let fn: (type: string) => void 16 | let onClose2: () => void 17 | let defaults: Record | void 18 | let openFn: [boolean, Dispatch>] 19 | export const openLoginDialog = (type: string, defaultValues?: Record, onClose?: () => void) => { 20 | if (!type) return 21 | onClose2 = onClose 22 | defaults = defaultValues 23 | if (type) fn(type) 24 | if (openFn) openFn[1](true) 25 | } 26 | 27 | /* eslint-disable jsx-a11y/no-noninteractive-element-interactions */ 28 | const LoginDialog: React.FC<{ open: boolean, onClose: () => void }> = props => { 29 | const [type, setType] = useState('') 30 | openFn = useState(false) 31 | fn = setType 32 | const [loading, setLoading] = useState(false) 33 | const [submitted, setSubmitted] = useState(false) 34 | const currentLogin = pluginMaster.logins[type] 35 | let width: number 36 | if (!type) { 37 | const len = getObjectLength(pluginMaster.logins) 38 | width = len > 3 ? 570 / len - 30 : 130 39 | } 40 | const close = () => { 41 | props.onClose() 42 | openFn[1](false) 43 | setType('') 44 | if (onClose2) onClose2() 45 | fn = onClose2 = defaults = null 46 | } 47 | const Component: React.ComponentType = currentLogin?.[Auth.COMPONENT] 48 | return !loading && close()} 54 | > 55 | {type === '' 56 | ? <> 57 |

{$('Choose your account login mode. If you don\'t have the online version, please choose the "Offline Login" on the right')}

58 | 59 | {$('Have already logged in? Click here!')} 60 | 61 |
62 | {Object.values(pluginMaster.logins).map(it =>
63 |
{ 68 | setSubmitted(false) 69 | setType(it[Auth.NAME]) 70 | }} 71 | style={{ backgroundImage: `url(${it[Auth.LOGO]})`, width, height: width }} 72 | /> 73 |

{it[Auth.TITLE](it)}

74 |
)} 75 |
76 | 77 | :
setSubmitted(true)} 81 | onSubmit={e => { 82 | setSubmitted(true) 83 | e.preventDefault() 84 | setLoading(true) 85 | const data = { } 86 | new FormData(e.target as HTMLFormElement).forEach((v, k) => (data[k] = v)) 87 | currentLogin 88 | .login(data) 89 | .then(key => profilesStore.setSelectedProfile(key, currentLogin)) 90 | .then(() => { 91 | notice({ content: $('Login succeeded!') }) 92 | close() 93 | }) 94 | .catch(err => notice({ content: err.message, error: true })) // TODO: 95 | .finally(() => setLoading(false)) 96 | }} 97 | > 98 | {(currentLogin[Auth.FIELDS] as Auth.Field[]).map(it => 99 | 100 |
101 | 102 |
103 |
104 |
105 |
106 |
107 | )} 108 | {Component && } 109 |
110 | !loading && setType('')}>{$('Back')} 111 | {currentLogin[Auth.LINK] && 112 | shell.openExternal(currentLogin[Auth.LINK].url())} 115 | > 116 | {currentLogin[Auth.LINK].name()} 117 | } 118 |
119 | 126 | } 127 |
128 | } 129 | 130 | export default LoginDialog 131 | -------------------------------------------------------------------------------- /src/components/ShowMore.tsx: -------------------------------------------------------------------------------- 1 | import './show-more.less' 2 | import { motion } from 'framer-motion' 3 | import React, { useState } from 'react' 4 | 5 | const poses = { 6 | closed: { height: 0 }, 7 | open: { height: 'auto' } 8 | } 9 | 10 | const ShowMore: React.FC = (props) => { 11 | const [show, set] = useState(false) 12 | return
13 |
19 | set(!show)} height='36' viewBox='0 0 16 16'> 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | {props.children} 31 | 32 |
33 | } 34 | 35 | export default ShowMore 36 | -------------------------------------------------------------------------------- /src/components/Switch.tsx: -------------------------------------------------------------------------------- 1 | import './switch.less' 2 | import React from 'react' 3 | 4 | const Switch: React.FC & 5 | { coverStyle?: React.CSSProperties }> = ({ coverStyle, ...props }) => { 6 | return
7 | 8 |
9 |
10 |
11 |
12 | } 13 | 14 | export default Switch 15 | -------------------------------------------------------------------------------- /src/components/VersionSelector.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ProfilesStore from '../models/ProfilesStore' 3 | import { useStore } from 'reqwq' 4 | import { InstallView } from '../protocol/types' 5 | 6 | const css1 = { display: 'flex', alignItems: 'center', marginTop: 4 } 7 | const css2 = { marginLeft: 8, flex: 1, maxWidth: 360 } 8 | export default (o: InstallView, ps?: ProfilesStore, prop: string | number | symbol = 'selectedVersion') => { 9 | const key = (o as any)[prop] = profilesStore.selectedVersion.key 10 | const Render = o.render 11 | return (() => { 12 | const lastRelease = $('last-release') 13 | const lastSnapshot = $('last-snapshot') 14 | if (!ps) ps = useStore(ProfilesStore) 15 | const vers = ps.sortedVersions 16 | const [u, set] = React.useState(key) 17 | return <> 18 | {Render && } 19 |

20 | {$('Target Version')}: 21 | 28 |

29 | 30 | }) as React.FC 31 | } 32 | -------------------------------------------------------------------------------- /src/components/VersionSwitch.tsx: -------------------------------------------------------------------------------- 1 | import './version-switch.less' 2 | import React from 'react' 3 | import Dialog from 'rc-dialog' 4 | import ProfilesStore from '../models/ProfilesStore' 5 | import { useStore } from 'reqwq' 6 | import { resolveIcon } from './IconPicker' 7 | 8 | /* eslint-disable jsx-a11y/no-noninteractive-element-interactions */ 9 | const VersionSwitch: React.FC<{ open: boolean, onClose: () => void }> = props => { 10 | const pm = useStore(ProfilesStore) 11 | const noTitle = $('No Title') 12 | const unknown = $('Unnamed') 13 | const lastPlayed = $('Last played') 14 | const lastRelease = $('last-release') 15 | const lastSnapshot = $('last-snapshot') 16 | return 24 |
    25 | {pm.sortedVersions.map(ver =>
  • { 30 | pm.setSelectedVersion(ver.key) 31 | props.onClose() 32 | }} 33 | > 34 | {ver.icon} 35 |
    36 | {ver.type === 'latest-release' ? lastRelease 37 | : ver.type === 'latest-snapshot' ? lastSnapshot : ver.name || noTitle} 38 | ({ver.lastVersionId}) 39 |
    {lastPlayed}: {ver.lastUsed.valueOf() ? ver.lastUsed.fromNow() : unknown}
    40 |
    41 |
  • )} 42 |
43 |
44 | } 45 | 46 | export default VersionSwitch 47 | -------------------------------------------------------------------------------- /src/components/avatar.css: -------------------------------------------------------------------------------- 1 | .avatar { 2 | width: 36px; 3 | height: 36px; 4 | border-radius: 8%; 5 | overflow: hidden; 6 | position: relative; 7 | } 8 | 9 | .avatar img { 10 | display: block; 11 | width: 800%; 12 | margin: -100%; 13 | position: absolute; 14 | image-rendering: pixelated; 15 | } 16 | .avatar .cover { 17 | margin-left: -500%; 18 | filter: drop-shadow(0px 2px 5px #0000005e) brightness(1.1); 19 | } 20 | -------------------------------------------------------------------------------- /src/components/dots.less: -------------------------------------------------------------------------------- 1 | .dots { 2 | left: 50%; 3 | bottom: 50px; 4 | position: fixed; 5 | transform: translateX(50%); 6 | 7 | div { 8 | cursor: pointer; 9 | width: 18px; 10 | height: 3px; 11 | margin: 0 4px; 12 | border-radius: 6px; 13 | transition: background-color .3s; 14 | display: inline-block; 15 | background-color: #fff; 16 | -webkit-app-region: no-drag; 17 | box-shadow: 0px 3px 5px -1px rgba(0, 0, 0, 0.2), 18 | 0px 6px 10px 0px rgba(0, 0, 0, 0.14), 19 | 0px 1px 18px 0px rgba(0, 0, 0, 0.12); 20 | 21 | &.active, &:hover { 22 | background-color: #4be802; 23 | } 24 | &.active { 25 | cursor: default; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/components/dropdown.less: -------------------------------------------------------------------------------- 1 | .dropdown { 2 | z-index: 10; 3 | overflow: hidden; 4 | position: fixed; 5 | transform: translateZ(0); 6 | list-style-type: none; 7 | -webkit-app-region: no-drag; 8 | filter: drop-shadow(0 4px 11px #00000057); 9 | 10 | .cover { 11 | border-radius: 3px; 12 | flex-shrink: 0; 13 | padding: 10px; 14 | position: relative; 15 | background: var(--dropdown-color); 16 | 17 | &:after { 18 | position: absolute; 19 | content: ''; 20 | width: 14px; 21 | height: 14px; 22 | bottom: -8px; 23 | left: 50%; 24 | margin-left: -7px; 25 | overflow: hidden; 26 | pointer-events: none; 27 | transform: rotate(45deg); 28 | bottom: -7px; 29 | background: var(--dropdown-color); 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/components/empty.less: -------------------------------------------------------------------------------- 1 | .pl-empty { 2 | margin: auto; 3 | text-align: center; 4 | 5 | i { 6 | font-size: 60px; 7 | color: #36b030; 8 | filter: drop-shadow(2px 4px 8px rgba(0,0,0,.5)); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/components/icon-button.css: -------------------------------------------------------------------------------- 1 | .icon-button { 2 | margin: 9px; 3 | cursor: pointer; 4 | position: relative; 5 | display: inline-block; 6 | } 7 | .icon-button p { 8 | margin: 0; 9 | text-align: center; 10 | font-size: 13px; 11 | white-space: nowrap; 12 | text-overflow: ellipsis; 13 | overflow: hidden; 14 | } 15 | .icon-button img { 16 | width: 64px; 17 | height: 64px; 18 | border-radius: 4px; 19 | transition: filter .5s; 20 | box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.2), 21 | 0 4px 5px 0 rgba(0, 0, 0, 0.14), 0 1px 10px 0 rgba(0, 0, 0, 0.12); 22 | } 23 | .icon-button:hover img { 24 | filter: brightness(1.3); 25 | } 26 | .icon-button span { 27 | left: 0; 28 | right: 0; 29 | top: 32px; 30 | font-size: 26px; 31 | position: absolute; 32 | color: #ffffffe0; 33 | text-align: center; 34 | font-family: minecraft; 35 | transform: translateY(-50%); 36 | } 37 | .icon-button span.offset { 38 | left: 4px; 39 | } 40 | -------------------------------------------------------------------------------- /src/components/icon-picker.css: -------------------------------------------------------------------------------- 1 | .version-icon { 2 | width: 44px; 3 | height: 44px; 4 | cursor: pointer; 5 | pointer-events: unset; 6 | transition: filter .3s; 7 | filter: drop-shadow(2px 4px 4px rgba(0,0,0,.5)); 8 | } 9 | .version-icon:hover { 10 | filter: drop-shadow(2px 4px 4px rgba(0,0,0,.5)) brightness(1.2); 11 | } 12 | 13 | .icon-picker .icons { max-width: 354px } 14 | .icon-picker .rc-dialog-body { max-height: 260px } 15 | -------------------------------------------------------------------------------- /src/components/install-list.less: -------------------------------------------------------------------------------- 1 | .install-list { 2 | p { 3 | color: #444; 4 | margin: 0; 5 | font-size: 15px; 6 | span { 7 | font-size: 16px; 8 | font-weight: bold; 9 | } 10 | } 11 | .rc-dialog-body { max-width: 500px } 12 | .buttons { 13 | display: flex; 14 | margin-top: 24px; 15 | button { 16 | padding: .75em 1.3em; 17 | } 18 | .btn-primary { 19 | margin-left: auto; 20 | } 21 | .btn-secondary { 22 | margin-right: auto; 23 | } 24 | } 25 | .list { 26 | overflow: auto; 27 | margin-top: 8px; 28 | max-height: 150px; 29 | &::-webkit-scrollbar { 30 | width: 2px; 31 | height: 1px; 32 | } 33 | &::-webkit-scrollbar-thumb { 34 | border-radius: 2px; 35 | background: #aaa; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/components/loading.less: -------------------------------------------------------------------------------- 1 | .pl-loading { 2 | margin: auto; 3 | text-align: center; 4 | .squares { 5 | @w: 25px; 6 | 7 | position: relative; 8 | display: inline-block; 9 | width: @w * 2; 10 | height: @w * 2; 11 | overflow: hidden; 12 | transform-origin: bottom left; 13 | animation: squareShrink 1.5s ease-in-out infinite; 14 | 15 | & > div { 16 | position: absolute; 17 | width: @w; 18 | height: @w; 19 | background: #04ca67; 20 | 21 | &:nth-child(1) { 22 | left: 0; 23 | top: @w; 24 | } 25 | &:nth-child(2) { 26 | left: @w; 27 | top: @w; 28 | animation: drop2 1.5s ease-out infinite; 29 | } 30 | &:nth-child(3) { 31 | left: 0; 32 | top: 0; 33 | animation: drop3 1.5s ease-out infinite; 34 | } 35 | &:nth-child(4) { 36 | left: @w; 37 | top: 0; 38 | animation: drop4 1.5s ease-out infinite; 39 | } 40 | } 41 | } 42 | } 43 | @keyframes squareShrink { 44 | 0% { transform: scale(1) } 45 | 90% { transform: scale(1) } 46 | 100% { transform: scale(0.5) } 47 | } 48 | @keyframes drop2 { 49 | 0% { transform: translateY(-50px) } 50 | 25% { transform: translate(0) } 51 | 100% { transform: translate(0) } 52 | } 53 | @keyframes drop3 { 54 | 0% { transform: translateY(-50px) } 55 | 50% { transform: translate(0) } 56 | 100% { transform: translate(0) } 57 | } 58 | @keyframes drop4 { 59 | 0% { transform: translateY(-50px) } 60 | 75% { transform: translate(0) } 61 | 100% { transform: translate(0) } 62 | } -------------------------------------------------------------------------------- /src/components/login-dialog.less: -------------------------------------------------------------------------------- 1 | .login-dialog { 2 | .rc-dialog-body { 3 | // padding: 60px 40px 40px; 4 | padding: 40px; 5 | } 6 | p { 7 | text-align: center; 8 | } 9 | .title { 10 | margin: 0; 11 | font-size: 14px; 12 | font-weight: 200; 13 | } 14 | .title2 { 15 | color: #36b030; 16 | margin-bottom: 20px; 17 | display: block; 18 | text-align: center; 19 | text-decoration: none; 20 | } 21 | .heads { 22 | display: flex; 23 | & > div { 24 | margin: 0 auto; 25 | } 26 | & > div + div { 27 | margin-left: 30px; 28 | } 29 | } 30 | .head { 31 | max-width: 130px; 32 | max-height: 130px; 33 | cursor: pointer; 34 | border-radius: 10px; 35 | filter: blur(1px) brightness(.7); 36 | background-repeat: no-repeat; 37 | background-size: cover; 38 | image-rendering: pixelated; 39 | box-shadow: 0px 2px 4px -1px rgba(0, 0, 0, 0.2), 40 | 0px 4px 5px 0px rgba(0, 0, 0, 0.14), 41 | 0px 1px 10px 0px rgba(0, 0, 0, 0.12); 42 | transition: .4s ease-in-out; 43 | 44 | &:hover { 45 | filter: blur(0px) brightness(.9); 46 | } 47 | } 48 | .links { 49 | font-size: 12px; 50 | margin-top: 12px; 51 | color: #0000009e; 52 | .right { 53 | float: right; 54 | } 55 | .left::before, .right::after { 56 | content: '< '; 57 | font-size: 13px; 58 | font-family: minecraft; 59 | } 60 | .right::after { 61 | content: ' >'; 62 | } 63 | span { 64 | cursor: pointer; 65 | transition: color .3s; 66 | } 67 | span:hover { 68 | color: #00a354; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/components/profile.less: -------------------------------------------------------------------------------- 1 | .profile { 2 | .skin { 3 | display: inline-block; 4 | background-color: #ffffffde; 5 | border-radius: 3px; 6 | -webkit-app-region: no-drag; 7 | cursor: move; 8 | box-shadow: 0px 2px 1px -1px rgba(0, 0, 0, 0.2), 9 | 0px 1px 1px 0px rgba(0, 0, 0, 0.14), 10 | 0px 1px 3px 0px rgba(0, 0, 0, 0.12); 11 | } 12 | .buttons { 13 | display: flex; 14 | margin-top: 8px; 15 | justify-content: center; 16 | } 17 | .text { 18 | margin: 0 20px 10px 10px; 19 | } 20 | .left { 21 | text-align: center; 22 | } 23 | .right { 24 | margin-left: 22px; 25 | text-align: center; 26 | .buttons { 27 | text-align: center; 28 | margin: -40px 0 30px; 29 | } 30 | } 31 | .rc-dialog-body { 32 | display: flex; 33 | align-items: center; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/components/show-more.less: -------------------------------------------------------------------------------- 1 | .show-more { 2 | width: 100%; 3 | position: relative; 4 | margin-top: 10px; 5 | padding-top: 50px; 6 | 7 | .button { 8 | top: 20px; 9 | width: 100%; 10 | position: absolute; 11 | text-align: center; 12 | transition: 1s; 13 | animation: beat 1s infinite; 14 | will-change: top; 15 | transform: translateZ(0); 16 | } 17 | 18 | .content { overflow: hidden } 19 | 20 | svg { 21 | cursor: pointer; 22 | user-select: none; 23 | -webkit-app-region: no-drag; 24 | } 25 | } 26 | 27 | 28 | @keyframes beat { 29 | 0% { 30 | top: 0px; 31 | } 32 | 33 | 30% { 34 | top: 20px; 35 | } 36 | 37 | 50% { 38 | top: 16px; 39 | // transform: scaleX(0.9); 40 | } 41 | 42 | 70% { 43 | top: 20px; 44 | } 45 | 46 | 100% { 47 | top: 0px; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/components/switch.less: -------------------------------------------------------------------------------- 1 | .switch { 2 | display: inline-block; 3 | width: 44px; 4 | height: 22px; 5 | position: relative; 6 | .background { 7 | position: absolute; 8 | width: 100%; 9 | height: 100%; 10 | top: 0; 11 | left: 0; 12 | background-color: #7f7f7f; 13 | border: 2px solid black; 14 | } 15 | input[type='checkbox'] { 16 | z-index: 1; 17 | margin: 0; 18 | opacity: 0; 19 | left: 0; 20 | cursor: pointer; 21 | user-select: none; 22 | position: absolute; 23 | width: 100%; 24 | height: 100%; 25 | 26 | &:checked + .background { 27 | background-color: #4e8937; 28 | border-color: white; 29 | } 30 | &:checked + .background + .slot { 31 | background-color: #006a00; 32 | } 33 | &:checked + .background + .slot + .thumb { 34 | border-color: white; 35 | background-color: #46a11e; 36 | margin-left: 100%; 37 | box-shadow: inset -2px -2px 0 0 #007200, inset 2px 2px 0 0 #37d71f; 38 | } 39 | } 40 | .slot { 41 | width: 5px; 42 | height: 16px; 43 | background-color: #3e3e3e; 44 | position: absolute; 45 | left: 50%; 46 | top: 50%; 47 | margin: -6px -2px 0 0; 48 | } 49 | .thumb { 50 | transition: all .1s ease-out, margin-left .3s ease-in-out; 51 | width: 14px; 52 | height: calc(100% + 8px); 53 | background-color: #c5c4c5; 54 | top: -4px; 55 | position: absolute; 56 | left: -7px; 57 | border: 2px solid black; 58 | box-shadow: inset -2px -2px 0 0 #565557, inset 2px 2px 0 0 white; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/components/treebeard/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Alex Curtis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/components/treebeard/components/Decorators/Container.tsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react' 2 | import { VelocityComponent } from 'velocity-react' 3 | 4 | class Container extends PureComponent<{ 5 | customStyles?: any 6 | style: any 7 | decorators: any 8 | terminal: any 9 | onClick: any 10 | onSelect?: any 11 | animations: any 12 | node: any 13 | }> { 14 | renderToggle () { 15 | const { animations } = this.props 16 | 17 | if (!animations) { 18 | return this.renderToggleDecorator() 19 | } 20 | 21 | return ( 22 | 23 | {this.renderToggleDecorator()} 24 | 25 | ) 26 | } 27 | 28 | renderToggleDecorator () { 29 | const { style, decorators, onClick } = this.props 30 | return 31 | } 32 | 33 | render () { 34 | const { 35 | style, decorators, terminal, node, onSelect, customStyles 36 | } = this.props 37 | return ( 38 |
39 | {!terminal ? this.renderToggle() : null} 40 | 41 |
42 | ) 43 | } 44 | } 45 | 46 | export default Container 47 | -------------------------------------------------------------------------------- /src/components/treebeard/components/Decorators/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { Div } from '../common' 4 | 5 | /* eslint-disable jsx-a11y/no-noninteractive-element-interactions */ 6 | const Header = ({ onSelect, node, style, customStyles }) => ( 7 |
8 |
11 | {node.name} 12 |
13 |
14 | ) 15 | 16 | export default Header 17 | -------------------------------------------------------------------------------- /src/components/treebeard/components/Decorators/Loading.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from '@emotion/styled' 3 | 4 | const Loading = styled(({ className }) => ( 5 |
loading...
6 | ))(({ style }) => style) 7 | 8 | export default Loading 9 | -------------------------------------------------------------------------------- /src/components/treebeard/components/Decorators/Toggle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from '@emotion/styled' 3 | 4 | import { Div } from '../common' 5 | 6 | const Polygon = styled('polygon', { 7 | shouldForwardProp: prop => ['className', 'children', 'points'].indexOf(prop) !== -1 8 | })(((a: any) => a.style) as any) 9 | 10 | const Toggle = ({ style, onClick }) => { 11 | const { height, width } = style 12 | const midHeight = height * 0.5 13 | const points = `0,0 0,${height} ${width},${midHeight}` 14 | 15 | return ( 16 |
17 |
18 | 19 | 20 | 21 |
22 |
23 | ) 24 | } 25 | 26 | export default Toggle 27 | -------------------------------------------------------------------------------- /src/components/treebeard/components/Decorators/index.tsx: -------------------------------------------------------------------------------- 1 | import Container from './Container' 2 | import Header from './Header' 3 | import Loading from './Loading' 4 | import Toggle from './Toggle' 5 | 6 | export default { 7 | Container, 8 | Header, 9 | Loading, 10 | Toggle 11 | } 12 | -------------------------------------------------------------------------------- /src/components/treebeard/components/NodeHeader.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import deepEqual from 'lodash/isEqual' 3 | import shallowEqual from 'shallowequal' 4 | 5 | class NodeHeader extends Component<{ 6 | style: any 7 | customStyles?: any 8 | decorators: any 9 | animations: boolean | any 10 | node: any 11 | onClick: any 12 | onSelect: any 13 | }> { 14 | shouldComponentUpdate (nextProps) { 15 | const props = this.props 16 | const nextPropKeys = Object.keys(nextProps) 17 | 18 | for (let i = 0; i < nextPropKeys.length; i++) { 19 | const key = nextPropKeys[i] 20 | if (key === 'animations') { 21 | continue 22 | } 23 | 24 | const isEqual = shallowEqual(props[key], nextProps[key]) 25 | if (!isEqual) { 26 | return true 27 | } 28 | } 29 | 30 | return !deepEqual(props.animations, nextProps.animations, { strict: true }) 31 | } 32 | 33 | render () { 34 | const { 35 | animations, decorators, node, onClick, style, onSelect, customStyles = { } 36 | } = this.props 37 | const { active, children } = node 38 | const terminal = !children 39 | let styles 40 | if (active) { 41 | styles = Object.assign(style, { container: { ...style.link, ...style.activeLink } }) 42 | } else { 43 | styles = style 44 | } 45 | return ( 46 | 56 | ) 57 | } 58 | } 59 | 60 | export default NodeHeader 61 | -------------------------------------------------------------------------------- /src/components/treebeard/components/TreeNode/Drawer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { VelocityTransitionGroup } from 'velocity-react' 3 | 4 | const Drawer = ({ restAnimationInfo, children }) => ( 5 | 6 | {children} 7 | 8 | ) 9 | 10 | export default Drawer 11 | -------------------------------------------------------------------------------- /src/components/treebeard/components/TreeNode/Loading.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { Ul } from '../common' 4 | 5 | const Loading = ({ style, decorators }) => ( 6 |
    7 |
  • 8 | 9 |
  • 10 |
11 | ) 12 | 13 | export default Loading 14 | -------------------------------------------------------------------------------- /src/components/treebeard/components/TreeNode/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react' 2 | import styled from '@emotion/styled' 3 | 4 | import defaultAnimations from '../../themes/animations' 5 | import { randomString } from '../../util' 6 | import { Ul } from '../common' 7 | import NodeHeader from '../NodeHeader' 8 | import Drawer from './Drawer' 9 | import Loading from './Loading' 10 | 11 | const Li = styled('li', { 12 | shouldForwardProp: prop => ['className', 'children', 'ref'].includes(prop) 13 | })(((arg: any) => arg.style) as any) 14 | 15 | class TreeNode extends PureComponent<{ 16 | onSelect?: any 17 | onToggle?: any 18 | style: any 19 | customStyles?: any 20 | node: any 21 | decorators: any 22 | animations: any 23 | }> { 24 | onClick () { 25 | const { node, onToggle } = this.props 26 | if (onToggle) { 27 | onToggle(node, !node.toggled) 28 | } 29 | } 30 | 31 | animations () { 32 | const { animations, node } = this.props 33 | if (!animations) { 34 | return { 35 | toggle: defaultAnimations.toggle(this.props, 0) 36 | } 37 | } 38 | const animation = Object.assign({}, animations, node.animations) 39 | return { 40 | toggle: animation.toggle(this.props), 41 | drawer: animation.drawer(this.props) 42 | } 43 | } 44 | 45 | decorators () { 46 | const { decorators, node } = this.props 47 | const nodeDecorators = node.decorators || {} 48 | 49 | return Object.assign({}, decorators, nodeDecorators) 50 | } 51 | 52 | renderChildren (decorators) { 53 | const { 54 | animations, decorators: propDecorators, node, style, onToggle, onSelect, customStyles 55 | } = this.props 56 | 57 | if (node.loading) { 58 | return ( 59 | 60 | ) 61 | } 62 | 63 | let children = node.children 64 | if (!Array.isArray(children)) { 65 | children = children ? [children] : [] 66 | } 67 | 68 | return ( 69 |
    70 | {children.map(child => ( 71 | 81 | ))} 82 |
83 | ) 84 | } 85 | 86 | render () { 87 | const { 88 | node, style, onSelect, customStyles 89 | } = this.props 90 | const decorators = this.decorators() 91 | const animations = this.animations() 92 | const { ...restAnimationInfo } = animations.drawer 93 | return ( 94 |
  • 95 | this.onClick()} 102 | onSelect={typeof onSelect === 'function' ? () => onSelect(node) : undefined} 103 | /> 104 | 105 | {node.toggled ? this.renderChildren(decorators) : null} 106 | 107 |
  • 108 | ) 109 | } 110 | } 111 | 112 | export default TreeNode 113 | -------------------------------------------------------------------------------- /src/components/treebeard/components/common/index.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled' 2 | 3 | export const Div = styled('div', { 4 | shouldForwardProp: prop => ['className', 'children'].indexOf(prop) !== -1 5 | })(((a: any) => a.style) as any) 6 | 7 | export const Ul = styled('ul', { 8 | shouldForwardProp: prop => ['className', 'children'].indexOf(prop) !== -1 9 | })(((a: any) => a.style) as any) 10 | -------------------------------------------------------------------------------- /src/components/treebeard/components/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import defaultTheme from '../themes/default' 4 | import defaultAnimations from '../themes/animations' 5 | import { randomString } from '../util' 6 | import { Ul } from './common' 7 | import defaultDecorators from './Decorators' 8 | import TreeNode from './TreeNode' 9 | 10 | const TreeBeard = ({ 11 | animations = defaultAnimations, decorators = defaultDecorators, data, onToggle = null, 12 | style = defaultTheme, onSelect = null, customStyles = {} as any 13 | }) => ( 14 |
      15 | {(Array.isArray(data) ? data : [data]).map(node => ( 16 | 26 | ))} 27 |
    28 | ) 29 | 30 | export default TreeBeard 31 | -------------------------------------------------------------------------------- /src/components/treebeard/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './components/index' 2 | -------------------------------------------------------------------------------- /src/components/treebeard/themes/animations.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | toggle: ({ node: { toggled } }, duration = 300) => ({ 3 | animation: { rotateZ: toggled ? 90 : 0 }, 4 | duration: duration 5 | }), 6 | drawer: (/* props */) => ({ 7 | enter: { 8 | animation: 'slideDown', 9 | duration: 300 10 | }, 11 | leave: { 12 | animation: 'slideUp', 13 | duration: 300 14 | } 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /src/components/treebeard/themes/default.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | tree: { 3 | base: { 4 | listStyle: 'none', 5 | margin: 0, 6 | padding: 0, 7 | color: '#444', 8 | fontFamily: 'lucida grande ,tahoma,verdana,arial,sans-serif', 9 | fontSize: '14px' 10 | }, 11 | node: { 12 | base: { 13 | position: 'relative' 14 | }, 15 | link: { 16 | cursor: 'pointer', 17 | position: 'relative', 18 | padding: '0px 5px', 19 | display: 'block' 20 | }, 21 | activeLink: { }, 22 | toggle: { 23 | base: { 24 | position: 'relative', 25 | display: 'inline-block', 26 | verticalAlign: 'top', 27 | marginLeft: -5, 28 | marginTop: 2, 29 | height: 24, 30 | width: 24 31 | }, 32 | wrapper: { 33 | position: 'absolute', 34 | top: '50%', 35 | left: '50%', 36 | margin: '-7px 0 0 -7px', 37 | height: 14 38 | }, 39 | height: 14, 40 | width: 14, 41 | arrow: { 42 | fill: '#777', 43 | strokeWidth: 0, 44 | transform: 'scale(0.7)' 45 | } 46 | }, 47 | header: { 48 | base: { 49 | color: '#444', 50 | display: 'inline-block', 51 | verticalAlign: 'top' 52 | }, 53 | connector: { 54 | width: '2px', 55 | height: '12px', 56 | borderLeft: 'solid 2px black', 57 | borderBottom: 'solid 2px black', 58 | position: 'absolute', 59 | top: '0px', 60 | left: '-21px' 61 | }, 62 | title: { 63 | lineHeight: '24px', 64 | verticalAlign: 'middle' 65 | } 66 | }, 67 | subtree: { 68 | listStyle: 'none', 69 | paddingLeft: '19px' 70 | }, 71 | loading: { 72 | color: '#E2C089' 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/components/treebeard/util/index.ts: -------------------------------------------------------------------------------- 1 | export const randomString = () => Math.random().toString(36).substring(7) 2 | -------------------------------------------------------------------------------- /src/components/version-switch.less: -------------------------------------------------------------------------------- 1 | .version-switch { 2 | width: unset; 3 | max-width: 450px; 4 | min-width: 300px; 5 | ul { 6 | margin: 0; 7 | list-style-type: none; 8 | padding: 0; 9 | font-weight: bold; 10 | border: 1px solid #bbb; 11 | font-size: 15px; 12 | color: #000000be; 13 | border-radius: 3px; 14 | max-height: 350px; 15 | overflow-y: auto; 16 | -webkit-app-region: no-drag; 17 | box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 18 | 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 19 | 0px 1px 5px 0px rgba(0, 0, 0, 0.12); 20 | 21 | &::-webkit-scrollbar { 22 | width: 7px; 23 | height: 1px; 24 | } 25 | 26 | &::-webkit-scrollbar-thumb { 27 | border-radius: 10px; 28 | background: #777; 29 | box-shadow: 0px 3px 3px -2px rgba(0, 0, 0, 0.2), 30 | 0px 3px 4px 0px rgba(0, 0, 0, 0.14), 31 | 0px 1px 8px 0px rgba(0, 0, 0, 0.12); 32 | } 33 | 34 | &::-webkit-scrollbar-thumb:hover { 35 | box-shadow: 0px 4px 5px -2px rgba(0, 0, 0, 0.2), 36 | 0px 7px 10px 1px rgba(0, 0, 0, 0.14), 37 | 0px 2px 16px 1px rgba(0, 0, 0, 0.12); 38 | } 39 | } 40 | li { 41 | letter-spacing: 0.5px; 42 | cursor: pointer; 43 | padding: 10px 20px; 44 | transition: background-color .3s; 45 | 46 | span { 47 | margin-left: 6px; 48 | font-size: 12px; 49 | } 50 | .time { 51 | font-weight: 400; 52 | font-size: 11px; 53 | color: #0000006e; 54 | margin-top: 1px; 55 | } 56 | } 57 | li + li { 58 | border-top: 1px solid #bbb; 59 | } 60 | li:nth-child(odd) { 61 | background-color: #cccccc5e; 62 | } 63 | li:hover { 64 | background-color: #36b030; 65 | } 66 | .rc-dialog-body { 67 | padding: 0; 68 | } 69 | 70 | .version-text { 71 | width: 350px; 72 | overflow: hidden; 73 | white-space: nowrap; 74 | text-overflow: ellipsis; 75 | } 76 | 77 | .version-icon { 78 | width: 44px; 79 | height: 44px; 80 | margin-right: 12px; 81 | image-rendering: pixelated; 82 | filter: drop-shadow(2px 4px 8px rgba(0,0,0,.5)); 83 | } 84 | 85 | .version { 86 | display: flex; 87 | align-items: center; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | import { ensureDirSync } from 'fs-extra' 3 | import { remote } from 'electron' 4 | 5 | export const APP_ROOT = remote.app.getAppPath() 6 | export const UNPACKED_PATH = join(APP_ROOT, 'unpacked') 7 | export const MC_LOGO_PATH = join(UNPACKED_PATH, 'mc-logo.ico') 8 | export const GAME_ROOT = join(process.platform === 'linux' ? remote.app.getPath('home') : remote.app.getPath('appData'), 9 | process.platform === 'darwin' ? 'minecraft' : '.minecraft') 10 | export const APP_PATH = remote.app.getPath('userData') 11 | export const TEMP_PATH = remote.app.getPath('temp') 12 | export const SKINS_PATH = join(APP_PATH, 'skins') 13 | export const PLUGINS_ROOT = join(APP_PATH, 'plugins') 14 | export const DELETES_FILE = join(PLUGINS_ROOT, 'deletes.json') 15 | export const OFFLINE_ACCOUNTS_FILE = join(APP_PATH, 'offline.json') 16 | export const UPDATES_PATH = join(APP_PATH, 'updates') 17 | export const ASAR_PATH = join(UPDATES_PATH, 'asar') 18 | export const ENTRY_POINT_PATH = join(UPDATES_PATH, 'entry-point.json') 19 | ensureDirSync(SKINS_PATH, 1) 20 | 21 | export const LAUNCH_PROFILE_FILE_NAME = 'launcher_profiles.json' 22 | export const LAUNCH_PROFILE_PATH = join(GAME_ROOT, LAUNCH_PROFILE_FILE_NAME) 23 | export const EXTRA_CONFIG_FILE_NAME = 'config.json' 24 | export const EXTRA_CONFIG_PATH = join(APP_PATH, EXTRA_CONFIG_FILE_NAME) 25 | export const MODS_PATH = join(GAME_ROOT, 'mods') 26 | export const WORLDS_PATH = join(GAME_ROOT, 'saves') 27 | export const VERSIONS_PATH = join(GAME_ROOT, 'versions') 28 | export const RESOURCE_PACKS_PATH = join(GAME_ROOT, 'resourcepacks') 29 | export const LIBRARIES_PATH = join(GAME_ROOT, 'libraries') 30 | export const SHADER_PACKS_PATH = join(GAME_ROOT, 'shaderpacks') 31 | export const SERVERS_FILE_NAME = 'servers.dat' 32 | export const SERVERS_PATH = join(GAME_ROOT, SERVERS_FILE_NAME) 33 | 34 | export const RESOURCES_PATH = join(APP_PATH, 'resources') 35 | export const RESOURCES_VERSIONS_PATH = join(RESOURCES_PATH, 'versions') 36 | export const RESOURCES_VERSIONS_INDEX_PATH = join(RESOURCES_PATH, 'versions-index.json') 37 | export const RESOURCES_WORLDS_INDEX_PATH = join(RESOURCES_PATH, 'worlds-index.json') 38 | export const RESOURCES_RESOURCE_PACKS_INDEX_PATH = join(RESOURCES_PATH, 'resource-packs-index.json') 39 | export const RESOURCES_PLUGINS_INDEX = join(RESOURCES_PATH, 'plugins-index.json') 40 | export const RESOURCES_MODS_INDEX_FILE_NAME = 'mods-index.json' 41 | 42 | export const DEFAULT_EXT_FILTER = ['exe', 'com'] 43 | export const ALLOW_PLUGIN_EXTENSIONS = ['.js', '.mjs', '.asar'] 44 | 45 | export const DEFAULT_LOCATE = (navigator.languages[0] || 'zh-cn').toLowerCase() 46 | export const IS_WINDOWS = process.platform === 'win32' 47 | 48 | export const LAUNCHING_IMAGE = join(APP_PATH, 'launching.webp') 49 | export const DOWNLOAD_EXE_URL = 'https://dl.pl.apisium.cn/PureLauncher.exe' 50 | export const DOWNLOAD_ASAR_URL = 'https://dl.pl.apisium.cn/app.asar' 51 | export const LATEST_URL = 'https://dl.pl.apisium.cn/latest.json' 52 | export const LAUNCHER_MANIFEST_URL = 'https://r.pl.apisium.cn/manifest.json' 53 | export const NEWS_URL = 'https://s.pl.apisium.cn/news.json' 54 | -------------------------------------------------------------------------------- /src/createServer.ts: -------------------------------------------------------------------------------- 1 | import isDev from './utils/isDev' 2 | import { BrowserWindow, ipcMain } from 'electron' 3 | import { createServer } from 'http' 4 | import { version } from '../package.json' 5 | 6 | const PORT = (process.env.PORT && parseInt(process.env.PORT, 10)) || 46781 7 | 8 | const HEADERS = { 9 | 'Content-Type': 'application/json', 10 | 'Access-Control-Allow-Origin': '*', 11 | 'Access-Control-Allow-Method': '*', 12 | 'Access-Control-Allow-Headers': 'Content-Type' 13 | } 14 | 15 | const ERROR = '{"error":true}' 16 | const SUCCESS = '{"success":true}' 17 | 18 | ipcMain.on('dev-reset-devPlugin', () => (process.env.DEV_PLUGIN = '')) 19 | 20 | const create = (window: BrowserWindow) => createServer((req, res) => (async () => { 21 | if (__DEV__) console.log(` \u001b[1;33m${req.method}: \u001b[37m${req.url}\u001b[0m`) 22 | let body: string 23 | switch (req.method) { 24 | case 'OPTIONS': 25 | body = SUCCESS 26 | break 27 | case 'GET': 28 | switch (req.url) { 29 | case '/info': 30 | /* eslint-disable @typescript-eslint/camelcase */ 31 | body = JSON.stringify({ 32 | isDev, 33 | devPlugin: process.env.DEV_PLUGIN, 34 | versions: { ...process.versions, pure_launcher: version }, 35 | platform: process.platform, 36 | arch: process.arch 37 | }) 38 | break 39 | case '/reload': 40 | if (isDev) { 41 | window.webContents.reload() 42 | body = SUCCESS 43 | } 44 | break 45 | } 46 | break 47 | case 'POST': { 48 | if (req.headers['content-type'] !== 'application/json') { 49 | body = '{"error":true,"message":"Headers is wrong!"}' 50 | break 51 | } 52 | const data = await new Promise((resolve, reject) => { 53 | let str = '' 54 | req 55 | .setEncoding('utf8') 56 | .on('data', c => { 57 | if (str.length > 10240) { 58 | reject(new Error('The body of request is oversized!')) 59 | req.pause() 60 | } else str += c 61 | }) 62 | .on('end', () => resolve(str)) 63 | .on('error', reject) 64 | }) 65 | switch (req.url) { 66 | case '/close': 67 | body = SUCCESS 68 | console.log(req.headers.referer) 69 | break 70 | case '/protocol': 71 | window.webContents.send('pure-launcher-protocol', data) 72 | body = SUCCESS 73 | break 74 | case '/setDevPlugin': 75 | if (isDev) { 76 | try { 77 | const json = JSON.parse(data) 78 | process.env.DEV_PLUGIN = json.path 79 | body = SUCCESS 80 | } catch (e) { 81 | console.error(e) 82 | body = ERROR 83 | } 84 | } 85 | } 86 | break 87 | } 88 | } 89 | res.writeHead(body ? 200 : 404, HEADERS).end(body) 90 | })().catch(e => { 91 | if (__DEV__) console.error(e) 92 | if (!res.finished) { 93 | if (!res.headersSent) res.writeHead(500, HEADERS) 94 | res.end(ERROR) 95 | } 96 | })).on('error', (e: any) => { 97 | console.error(e) 98 | if (e.code === 'EADDRINUSE') { 99 | // TODO 100 | } 101 | }).listen(PORT) 102 | 103 | export default create 104 | -------------------------------------------------------------------------------- /src/custom.css: -------------------------------------------------------------------------------- 1 | body, input, select, textarea { 2 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 3 | 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 4 | 'Segoe UI Emoji', 'Segoe UI Symbol'; 5 | } 6 | 7 | :root { 8 | --top-color: #6ead3c; 9 | --top-border: #fff483c7; 10 | --top-shadow: rgba(255, 255, 255, 0.62); 11 | 12 | --top-filter: brightness(0.5); 13 | --launch-filter: brightness(0.8); 14 | 15 | --main-color: url(./assets/images/bg-wool-dark-light.png); 16 | --secondary-color: var(--main-color); 17 | --dropdown-color: var(--main-color); 18 | --scroll-color: url(./assets/images/bg-wool-white.png); 19 | 20 | --text-color-0: #ffffffe0; 21 | --text-color-1: #ffffffe0; 22 | --text-color-2: #ffffff80; 23 | --text-color-3: #ffffffc0; 24 | --link-color: #6ead3c; 25 | --link-hover-color: #578830; 26 | 27 | --input-color: #444; 28 | --background: url(./assets/images/bg-wool-dark.png); 29 | } 30 | 31 | .scrollable, .scrollable-dialog .rc-dialog-body { 32 | overflow: auto !important; 33 | } 34 | 35 | .scrollable::-webkit-scrollbar, .scrollable-dialog .rc-dialog-body::-webkit-scrollbar { 36 | width: 7px; 37 | height: 7px; 38 | } 39 | 40 | .scrollable::-webkit-scrollbar-thumb, .scrollable-dialog .rc-dialog-body::-webkit-scrollbar-thumb { 41 | border-radius: 10px; 42 | background: var(--scroll-color); 43 | box-shadow: 0 3px 3px -2px rgba(0, 0, 0, 0.2), 44 | 0 3px 4px 0 rgba(0, 0, 0, 0.14), 45 | 0 1px 8px 0 rgba(0, 0, 0, 0.12); 46 | } 47 | .scrollable-dialog .rc-dialog-body::-webkit-scrollbar-thumb { background: white } 48 | 49 | .scrollable::-webkit-scrollbar-thumb:hover, .scrollable-dialog .rc-dialog-body::-webkit-scrollbar-thumb:hover { 50 | box-shadow: 0 4px 5px -2px rgba(0, 0, 0, 0.2), 51 | 0 7px 10px 1px rgba(0, 0, 0, 0.14), 52 | 0 2px 16px 1px rgba(0, 0, 0, 0.12); 53 | } 54 | -------------------------------------------------------------------------------- /src/globals.d.ts: -------------------------------------------------------------------------------- 1 | import Lang from '../langs/zh-cn.json' 2 | import Master from './plugin/index' 3 | import ProfilesStore from './models/ProfilesStore' 4 | import { GetStore } from 'reqwq' 5 | import { ComponentType } from 'react' 6 | import { PureLauncherTask } from './utils/index' 7 | import { Resources, Resource, InstallView } from './protocol/types' 8 | 9 | type Keys = keyof typeof Lang 10 | type $ = (name: Keys, ...args: string[]) => string 11 | interface Ctx { content: React.ReactNode, duration?: number, error?: boolean } 12 | interface ConfirmCtx { text: string, title?: string, cancelButton?: boolean, component?: ComponentType, ignore?: boolean } 13 | interface TopBar { blocks: Array, colors: Array, containers: HTMLDivElement[] } 14 | declare global { 15 | declare const topBar: TopBar 16 | declare const __DEV__: boolean 17 | declare const $: $ 18 | declare const profilesStore: ProfilesStore 19 | declare const pluginMaster: Master 20 | declare const __getStore: GetStore 21 | declare const __tasks: PureLauncherTask[] 22 | declare const notice: (ctx: Ctx) => void 23 | declare const installResources: (data: Resources) => Promise 24 | declare const __requestInstallResources: (data: Resources, views?: InstallView) => Promise 25 | declare const openConfirmDialog: (data: ConfirmCtx) => Promise 26 | declare const startAnimation: () => void 27 | declare const stopAnimation: () => void 28 | declare const animationStopped: boolean 29 | declare const __updateTasksView: (() => void) | null 30 | declare const quitApp: () => void 31 | declare interface Window { 32 | $: $ 33 | topBar: TopBar 34 | __DEV__: boolean 35 | pluginMaster: Master 36 | profilesStore: ProfilesStore 37 | __getStore: GetStore 38 | animationStopped: boolean 39 | notice: (ctx: Ctx) => void 40 | __tasks: PureLauncherTask[] 41 | installResources: (data: Resources) => Promise 42 | __requestInstallResources: (data: Resource, views?: InstallView) => Promise 43 | openConfirmDialog: (data: ConfirmCtx) => Promise 44 | startAnimation: () => void 45 | stopAnimation: () => void 46 | quitApp: () => void 47 | __updateTasksView: (() => void) | null 48 | } 49 | declare namespace NodeJS { 50 | interface Global { 51 | $: $ 52 | topBar: TopBar 53 | __DEV__: boolean 54 | __getStore: GetStore 55 | pluginMaster: Master 56 | profilesStore: ProfilesStore 57 | animationStopped: boolean 58 | notice: (ctx: Ctx) => void 59 | __tasks: PureLauncherTask[] 60 | installResources: (data: Resources) => Promise 61 | __requestInstallResources: (data: Resource, views?: InstallView) => Promise 62 | openConfirmDialog: (data: ConfirmCtx) => Promise 63 | startAnimation: () => void 64 | stopAnimation: () => void 65 | quitApp: () => void 66 | __updateTasksView: (() => void) | null 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/i18n.ts: -------------------------------------------------------------------------------- 1 | import 'moment/locale/zh-cn' 2 | import zhCN from '../langs/zh-cn.json' 3 | import moment from 'moment' 4 | import forceUpdate from 'react-deep-force-update' 5 | import { RefObject } from 'react' 6 | 7 | export const langs = { 8 | 'zh-cn': zhCN, 9 | 'en-us': { $LanguageName$: 'English' } 10 | } 11 | 12 | let instance: RefObject 13 | export const setInstance = (instance2: RefObject) => (instance = instance2) 14 | 15 | let current = zhCN 16 | export let currentName = 'zh-cn' 17 | 18 | export const applyLocate = (name: string, notUpdate = false) => { 19 | name = name.toLowerCase() 20 | if (!(name in langs)) throw new Error('No such lang: ' + name) 21 | current = langs[name] 22 | moment.locale(name) 23 | currentName = name 24 | if (window.pluginMaster) pluginMaster.emit('changeLanguage', name) 25 | if (!notUpdate) forceUpdate(instance.current) 26 | } 27 | export const replaceArgs = (text: string, ...args: string[]) => text.replace(/{(\d)}/g, (_, i) => args[i]) 28 | ;(window as any).__$pli0 = (text: keyof typeof zhCN, ...args: string[]) => 29 | text in current 30 | ? replaceArgs(current[text], ...args) 31 | : text.startsWith('$') 32 | ? text === '$readme' 33 | ? replaceArgs(zhCN.$readmeEn, ...args) 34 | : null 35 | : replaceArgs(text, ...args) 36 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable node/no-deprecated-api */ 2 | import { join } from 'path' 3 | import { exists } from 'fs' 4 | import { Server } from 'http' 5 | import { app, BrowserWindow, ipcMain, systemPreferences } from 'electron' 6 | import minimist from 'minimist' 7 | import isDev from './utils/isDev' 8 | import createServer from './createServer' 9 | 10 | let server: Server = null 11 | let window: BrowserWindow = null 12 | let launchingWindow: BrowserWindow = null 13 | const webp = join(app.getPath('userData'), 'launching.webp') 14 | ;(process.env as any)['D' + 'EV'] = process.env.NODE_ENV !== 'production' 15 | 16 | const parseArgs = (args: string[]) => { 17 | if (window) { 18 | const arr = minimist(args.slice(1))._ 19 | const data = arr[arr.length - 1] 20 | if (data) window.webContents.send('pure-launcher-protocol', data, arr) 21 | } 22 | } 23 | 24 | if (app.requestSingleInstanceLock()) { 25 | app.on('second-instance', (_, argv) => { 26 | if (window) { 27 | if (window.isMinimized()) { 28 | window.restore() 29 | window.setBounds({ height: 586, width: 816 }) 30 | } 31 | window.focus() 32 | parseArgs(argv) 33 | } else app.exit() 34 | }) 35 | } else app.exit() 36 | 37 | app.setAsDefaultProtocolClient('pure-launcher') 38 | 39 | let launchingDialogOpened = false 40 | const closeLaunchingDialog = () => { 41 | if (!launchingWindow || !launchingDialogOpened) return 42 | launchingDialogOpened = false 43 | launchingWindow.webContents.executeJavaScript('window.img.style.opacity = "0"') 44 | setTimeout(() => { 45 | launchingWindow.hide() 46 | launchingWindow.webContents.executeJavaScript('window.img.src = ""') 47 | }, 3000) 48 | } 49 | const openLaunchingDialog = () => { 50 | if (!launchingWindow || launchingDialogOpened) return 51 | exists(webp, e => { 52 | if (!e) return 53 | launchingDialogOpened = true 54 | launchingWindow.show() 55 | launchingWindow.webContents.executeJavaScript(` 56 | if (!window.img) window.img = document.getElementsByTagName("img")[0] 57 | window.img.src = '${webp.replace(/\\/g, '\\\\')}' 58 | window.img.style.opacity = '1'`) 59 | setTimeout(closeLaunchingDialog, 26000) 60 | }) 61 | } 62 | 63 | const create = () => { 64 | ipcMain 65 | .on('open-launching-dialog', openLaunchingDialog) 66 | .on('close-launching-dialog', closeLaunchingDialog) 67 | window = new BrowserWindow({ 68 | width: 816, 69 | height: 586, 70 | resizable: false, 71 | maximizable: false, 72 | fullscreenable: false, 73 | transparent: process.platform !== 'win32' || systemPreferences.isAeroGlassEnabled(), 74 | frame: false, 75 | show: false, 76 | webPreferences: { webviewTag: true, nodeIntegration: true, nodeIntegrationInWorker: true } 77 | }) 78 | window.loadFile(join(__dirname, '../index.html')) 79 | 80 | launchingWindow = new BrowserWindow({ 81 | width: 500, 82 | height: 441, 83 | resizable: false, 84 | maximizable: false, 85 | fullscreenable: false, 86 | transparent: true, 87 | frame: false, 88 | title: 'Launching...', 89 | show: false, 90 | alwaysOnTop: true, 91 | skipTaskbar: true, 92 | webPreferences: { webSecurity: false } 93 | }) 94 | launchingWindow.webContents.loadURL('data:text/html;charset=UTF-8,' + encodeURIComponent( 95 | '')) 97 | 98 | window 99 | .once('ready-to-show', () => window.show()) 100 | .once('closed', () => (window = null)) 101 | .webContents.on('will-navigate', (e, u) => { 102 | const url = new URL(u) 103 | const url1 = new URL(window.webContents.getURL()) 104 | if (url.origin !== url1.origin || url.pathname !== url1.pathname) e.preventDefault() 105 | }) 106 | if (isDev) window.webContents.openDevTools({ mode: 'detach' }) 107 | parseArgs(process.argv) 108 | 109 | server = createServer(window) 110 | } 111 | 112 | if (process.platform === 'linux') app.commandLine.appendSwitch('enable-transparent-visuals') 113 | 114 | app 115 | .on('ready', process.platform === 'linux' ? () => setTimeout(create, 400) : create) 116 | .on('quit', () => server.close()) 117 | .on('window-all-closed', () => process.platform !== 'darwin' && app.quit()) 118 | .on('activate', () => window == null && create()) 119 | -------------------------------------------------------------------------------- /src/models/index.ts: -------------------------------------------------------------------------------- 1 | import { newInstance } from 'reqwq' 2 | 3 | import ProfilesStore from './ProfilesStore' 4 | import GameStore from './GameStore' 5 | 6 | const P = newInstance(ProfilesStore, GameStore) 7 | export default P 8 | 9 | window.__getStore = P.getStore 10 | window.profilesStore = P.getStore(ProfilesStore) 11 | -------------------------------------------------------------------------------- /src/plugin/Authenticator.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const NAME = Symbol('Authenticator') 4 | export const FIELDS = Symbol('Fields') 5 | export const LINK = Symbol('Link') 6 | export const TITLE = Symbol('Title') 7 | export const LOGO = Symbol('Logo') 8 | export const COMPONENT = Symbol('Component') 9 | export interface Field { 10 | name: string 11 | title: () => string 12 | inputProps?: React.InputHTMLAttributes & React.ClassAttributes 13 | } 14 | export const registerAuthenticator = (args: { 15 | name: string 16 | title: (authenticator: Authenticator, profile?: Profile) => string 17 | logo: string 18 | fields?: Field[] 19 | link?: { name: () => string, url: () => string } 20 | compoent?: React.ComponentType 21 | }) => (C: T) => class RegisteredAuthenticator extends C { 22 | public [NAME] = args.name 23 | public [TITLE] = args.title 24 | public [FIELDS] = args.fields || [] 25 | public [LINK] = args.link 26 | public [LOGO] = args.logo.replace(/\\/g, '/') 27 | public [COMPONENT] = args.compoent 28 | } 29 | 30 | export interface Profile { 31 | clientToken: string 32 | accessToken: string 33 | uuid: string 34 | username: string 35 | type: string 36 | displayName?: string 37 | skinUrl?: string 38 | key: string 39 | } 40 | 41 | export default abstract class Authenticator { 42 | public abstract async login (options: any): Promise 43 | public abstract async logout (key: string): Promise 44 | public abstract async refresh (key: string): Promise 45 | public abstract async validate (key: string, autoRefresh: boolean): Promise 46 | public abstract getData (key: string): Profile 47 | public abstract getAllProfiles (): Profile[] 48 | } 49 | export interface SkinChangeable { 50 | changeSkin (key: string, path: string, slim: boolean): Promise 51 | } 52 | -------------------------------------------------------------------------------- /src/plugin/DownloadProviders.ts: -------------------------------------------------------------------------------- 1 | import urlJoin from 'url-join' 2 | import { HttpDownloader, DownloadOption, Installer } from '@xmcl/installer/index' 3 | import { NOT_PROXY } from 'reqwq' 4 | import { Version } from '@xmcl/installer/minecraft' 5 | 6 | export interface DownloadProvider { 7 | [NOT_PROXY]: true 8 | name (): string 9 | locales?: string[] 10 | launchermeta: string 11 | launcher: string 12 | resources: string | string[] 13 | libraries: string | string[] 14 | forge: string 15 | preference?: boolean 16 | assetsIndex?: (version: import('@xmcl/core').ResolvedVersion) => string | string[] 17 | json?: (version: Pick) => string | string[] 18 | client?: (version: import('@xmcl/core').ResolvedVersion) => string | string[] 19 | optifine?: (mcVersion: string, type: string, version: string) => Promise | string 20 | } 21 | 22 | export const optifine = (mcVersion: string, type: string, version: string) => 23 | `https://bmclapi2.bangbang93.com/optifine/${mcVersion}/${type}/${version}` 24 | 25 | const MCBBSAPI: DownloadProvider = { 26 | [NOT_PROXY]: true, 27 | preference: true, 28 | name: () => 'MCBBSAPI', 29 | locales: ['zh'], 30 | launchermeta: 'https://download.mcbbs.net', 31 | launcher: 'https://download.mcbbs.net', 32 | resources: 'https://download.mcbbs.net/assets', 33 | libraries: 'https://download.mcbbs.net/maven', 34 | forge: 'https://download.mcbbs.net/maven', 35 | assetsIndex: ({ assetIndex }) => 'https://download.mcbbs.net' + new URL(assetIndex.url).pathname, 36 | json: ({ id }) => `https://download.mcbbs.net/version/${id}/json`, 37 | client: ({ id }) => `https://download.mcbbs.net/version/${id}/client`, 38 | optifine: (mcVersion: string, type: string, version: string) => 39 | `https://download.mcbbs.net/optifine/${mcVersion}/${type}/${version}` 40 | } 41 | 42 | const BMCLAPI: DownloadProvider = { 43 | [NOT_PROXY]: true, 44 | name: () => 'BMCLAPI', 45 | locales: ['zh'], 46 | launchermeta: 'https://bmclapi2.bangbang93.com', 47 | launcher: 'https://bmclapi2.bangbang93.com', 48 | resources: 'https://bmclapi2.bangbang93.com/assets', 49 | libraries: 'https://bmclapi2.bangbang93.com/maven', 50 | forge: 'https://bmclapi2.bangbang93.com/maven', 51 | json: ({ id }) => `https://bmclapi2.bangbang93.com/version/${id}/json`, 52 | client: ({ id }) => `https://bmclapi2.bangbang93.com/version/${id}/client`, 53 | assetsIndex: ({ assetIndex }) => 'https://bmclapi2.bangbang93.com' + new URL(assetIndex.url).pathname, 54 | optifine 55 | } 56 | 57 | const TSS_MIRROR: DownloadProvider = { 58 | ...MCBBSAPI, 59 | preference: false, 60 | name: () => 'TSS Mirror', 61 | json: ({ url }) => 'https://mc.mirrors.tmysam.top' + new URL(url).pathname, 62 | client: c => 'https://mc.mirrors.tmysam.top' + new URL(c.downloads.client.url).pathname, 63 | resources: 'https://mcres.mirrors.tmysam.top', 64 | libraries: ['https://mclib.mirrors.tmysam.top', MCBBSAPI.libraries as string] 65 | } 66 | 67 | const OFFICIAL: DownloadProvider = { 68 | [NOT_PROXY]: true, 69 | name: () => $('OFFICIAL'), 70 | launchermeta: 'http://launchermeta.mojang.com', 71 | launcher: 'https://launcher.mojang.com', 72 | resources: 'http://resources.download.minecraft.net', 73 | libraries: 'https://libraries.minecraft.net', 74 | forge: 'https://files.minecraftforge.net/maven', 75 | async optifine (mcVersion: string, type: string, version: string) { 76 | const text = await fetch(`https://optifine.net/adloadx?f=${version.includes('pre') ? 'preview_' : ''}OptiFine_${mcVersion}_${type}_${version}.jar`) 77 | .then(it => it.text()) 78 | if (text) { 79 | const ret = / { 103 | this.bytes += c 104 | if (!Number.isSafeInteger(this.bytes)) this.bytes = 0 105 | __updateTasksView() 106 | if (fn) return fn(c, w, t, u) 107 | } 108 | return super.downloadFile(option) 109 | } 110 | } 111 | 112 | export const downloader = new ProgressDownloader() 113 | 114 | export const getDownloaders = (): Installer.Option => { 115 | let isOffcical = profilesStore.extraJson.downloadProvider === 'OFFICIAL' 116 | const provider: DownloadProvider = DownloadProviders[profilesStore.extraJson.downloadProvider] 117 | if (!provider) isOffcical = true 118 | return { 119 | downloader, 120 | json: provider.json, 121 | client: provider.client, 122 | assetsIndexUrl: provider.assetsIndex, 123 | assetsDownloadConcurrency: profilesStore.extraJson.downloadThreads || 16, 124 | maxConcurrency: profilesStore.extraJson.downloadThreads || 16, 125 | assetsHost: isOffcical ? undefined : typeof provider.resources === 'string' 126 | ? [provider.resources] : provider.resources, 127 | libraryHost: isOffcical && provider?.libraries ? undefined : lib => typeof provider.libraries === 'string' 128 | ? urlJoin(provider.libraries, lib.download.path) 129 | : provider.libraries.map(it => urlJoin(it, lib.download.path)) 130 | } 131 | } 132 | 133 | export default DownloadProviders as Record 134 | -------------------------------------------------------------------------------- /src/plugin/Plugin.ts: -------------------------------------------------------------------------------- 1 | import { ResourcePlugin } from '../protocol/types' 2 | import { INTERRUPTIBLE } from '../utils/EventBus' 3 | 4 | export const EVENTS = Symbol('Events') 5 | export const PLUGIN_INFO = Symbol('PluginInfo') 6 | export interface ExtensionsButton { 7 | title: () => string 8 | key: any 9 | onClick?: () => void 10 | icon?: string 11 | hideFirst?: boolean 12 | } 13 | export type PluginInfo = Pick> & 15 | { title (): string, description? (): string } 16 | & { dependencies?: string[] } 17 | export class Plugin { 18 | public static [PLUGIN_INFO]: PluginInfo = null 19 | public readonly pluginInfo: PluginInfo = null 20 | public onUnload () { } 21 | } 22 | export const event = (name?: string, interruptible = false) => (target: any, key: string, d: PropertyDescriptor) => { 23 | const f = d.value 24 | if (interruptible) f[INTERRUPTIBLE] = true 25 | ;(target[EVENTS] || (target[EVENTS] = {}))[name || key] = f 26 | } 27 | export const plugin = (info: PluginInfo) => (c: T) => { 28 | if (c === Plugin) throw new TypeError('Please extends the Plugin class!') 29 | if (!info.id) throw new Error('this plugin without id!') 30 | c[PLUGIN_INFO] = Object.freeze(info) 31 | return c 32 | } 33 | -------------------------------------------------------------------------------- /src/plugin/exports.ts: -------------------------------------------------------------------------------- 1 | import P from '../models/index' 2 | import GameStore, { STATUS } from '../models/GameStore' 3 | import PluginMaster from './index' 4 | import ProfilesStore from '../models/ProfilesStore' 5 | import * as fs from 'fs-extra' 6 | import * as skinView3d from 'skinview3d' 7 | import * as IconButtonExports from '../components/IconButton' 8 | import * as constants from '../constants' 9 | import * as Reqwq from 'reqwq' 10 | import * as Nbt from '@xmcl/nbt/index' 11 | import * as Core from '@xmcl/core/index' 12 | import * as Task from '@xmcl/task/index' 13 | import * as Unzip from '@xmcl/unzip/index' 14 | import * as Client from '@xmcl/client/index' 15 | import * as Installer from '@xmcl/installer/index' 16 | import * as ResourcePack from '@xmcl/resourcepack/index' 17 | import * as TextComponent from '@xmcl/text-component/index' 18 | import * as Authenticator from './Authenticator' 19 | import * as Yazl from 'yazl' 20 | import * as Yauzl from 'yauzl' 21 | import * as types from '../protocol/types' 22 | import * as ReactRouter from 'react-router-dom' 23 | import * as ofs from '../utils/fs' 24 | 25 | export { version } from '../../package.json' 26 | export { Plugin, plugin, event } from './Plugin' 27 | export { openLoginDialog } from '../components/LoginDialog' 28 | export { default as fitText } from '../utils/fit-text' 29 | export { default as requestReload } from '../utils/request-reload' 30 | export { default as Avatar } from '../components/Avatar' 31 | export { default as Loading } from '../components/Loading' 32 | export { default as Dots } from '../components/Dots' 33 | export { default as locates } from '../utils/locates' 34 | export { default as Dropdown } from '../components/Dropdown' 35 | export { default as Switch } from '../components/Switch' 36 | export { default as ShowMore } from '../components/ShowMore' 37 | export { default as Empty } from '../components/Empty' 38 | export { default as Treebeard } from '../components/treebeard/index' 39 | export { default as DownloadProviders, DownloadProvider } from './DownloadProviders' 40 | export { default as createVersionSelector } from '../components/VersionSelector' 41 | export { default as IconButton } from '../components/IconButton' 42 | export { default as ErrorHandler } from '../components/ErrorHandler' 43 | export { default as LiveRoute } from '../components/LiveRoute' 44 | export { default as protocolFunctions } from '../protocol/index' 45 | export { default as isDev } from '../utils/isDev' 46 | export { default as history } from '../utils/history' 47 | export { default as React } from 'react' 48 | export { default as urlJoin } from 'url-join' 49 | export { default as ReactDOM } from 'react-dom' 50 | export { default as Dialog } from 'rc-dialog' 51 | export { default as Notification } from 'rc-notification' 52 | export { default as ToolTip } from 'rc-tooltip' 53 | export { default as ReactImage } from 'react-image' 54 | export { default as IconPicker, resolveIcon } from '../components/IconPicker' 55 | export { genId, genUUID, genUUIDOrigin, getJson, fetchJson, makeTempDir, cacheSkin, 56 | isX64, createUnzipTask, openServerHome, getJavaVersion, validPath, sha1, md5, replace, createShortcut, 57 | getVersionTypeText, download, getSuitableMemory, unzip, playNoticeSound, removeFormatCodes, reloadPage, 58 | autoNotices, readBuffer, addTask, createDownloadTask, addDirectoryToZipFile } from '../utils/index' 59 | export { STATUS as LAUNCH_STATUS, IconButtonExports, skinView3d, ofs, 60 | constants, Reqwq, ProfilesStore, Authenticator, fs, Yazl, Yauzl, types, ReactRouter } 61 | 62 | export const $: (name: string, ...args: string[]) => string = (window as any).__$pli0 63 | export const pluginMaster: PluginMaster = window.pluginMaster 64 | export const profilesStore: ProfilesStore = window.profilesStore 65 | export const notice: (ctx: { content: React.ReactNode, duration?: number, error?: boolean }) => void = null 66 | export const openConfirmDialog: (data: { text: string, title?: string, cancelButton?: boolean, ignore?: boolean }) => 67 | Promise = null 68 | export const requestInstallResources: (data: T, views?: types.InstallView) => 69 | Promise = null 70 | export const xmcl = { 71 | Nbt, 72 | Core, 73 | Task, 74 | Unzip, 75 | Client, 76 | Installer, 77 | ResourcePack, 78 | TextComponent 79 | } 80 | 81 | const gs = P.getStore(GameStore) 82 | export const launch = gs.launch 83 | export const resolveJavaPath = gs.resolveJavaPath 84 | export const getLaunchStatus = () => gs.status 85 | Object.defineProperty(module.exports, 'notice', { get: () => window.notice }) 86 | Object.defineProperty(module.exports, 'openConfirmDialog', { get: () => window.openConfirmDialog }) 87 | Object.defineProperty(module.exports, 'requestInstallResources', { get: () => window.__requestInstallResources }) 88 | -------------------------------------------------------------------------------- /src/plugin/internal/ResourceResolver.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | import { plugin, Plugin, event } from '../Plugin' 3 | import { version as pluginVersion } from '../../../package.json' 4 | import { isVersion, ResourceVersion } from '../../protocol/types' 5 | import { addTask, download, genId } from '../../utils/index' 6 | import { GAME_ROOT, TEMP_PATH } from '../../constants' 7 | import { getDownloaders, downloader, optifine } from '../../plugin/DownloadProviders' 8 | import { promises as fs } from 'fs' 9 | import * as Installer from '@xmcl/installer/index' 10 | 11 | @plugin({ 12 | version: pluginVersion, 13 | id: '@pure-launcher/resource-resolver', 14 | description: () => $("PureLauncher's built-in plugin"), 15 | title: () => $('ResourcesResolver') 16 | }) 17 | export default class ResourceInstaller extends Plugin { 18 | @event(null, true) 19 | public async processResourceInstallVersion (res: any, obj: { notWriteJson: boolean }) { 20 | if (!isVersion(res) || !res.mcVersion) return 21 | const r: ResourceVersion & { 22 | $fabric?: [string, string] 23 | $forge?: string 24 | $optifine?: [string, string] 25 | $vanilla?: boolean 26 | } = res 27 | if (Array.isArray(r.$fabric)) { 28 | obj.notWriteJson = true 29 | r.version = '0.0.0' 30 | await Installer.FabricInstaller.install(r.$fabric[0], r.$fabric[1], GAME_ROOT, { versionId: r.id }) 31 | } else if (Array.isArray(r.$optifine)) { 32 | obj.notWriteJson = true 33 | r.version = '0.0.0' 34 | const name = r.mcVersion + '-' + r.$optifine[0] + '-' + r.$optifine[1] 35 | const destination = join(TEMP_PATH, genId()) 36 | try { 37 | await download( 38 | { 39 | destination, 40 | url: await (profilesStore.downloadProvider.optifine || optifine)(r.mcVersion, r.$optifine[0], r.$optifine[1]) 41 | }, 42 | $('Download') + ' Optifine', name) 43 | await addTask(Installer.OptifineInstaller.installByInstallerTask(destination, GAME_ROOT, { versionId: r.id }), 44 | $('Install') + ' Optifine', name).wait() 45 | } finally { 46 | await fs.unlink(destination).catch(console.error) 47 | } 48 | } else if (typeof r.$forge === 'string') { 49 | obj.notWriteJson = true 50 | const mv = r.mcVersion 51 | const version = r.$forge 52 | r.version = '0.0.0' 53 | await addTask(Installer.ForgeInstaller.installTask({ 54 | version, 55 | mcversion: mv, 56 | installer: { 57 | path: `/maven/net/minecraftforge/forge/${mv}-${version}/forge-${mv}-${version}-installer.jar` 58 | } 59 | }, GAME_ROOT, { 60 | downloader, 61 | versionId: r.id, 62 | java: profilesStore.extraJson.javaPath, 63 | mavenHost: profilesStore.downloadProvider.forge 64 | }), $('Install') + ' Forge', mv + '-' + version).wait() 65 | } else if (r.$vanilla) { 66 | obj.notWriteJson = true 67 | r.version = '0.0.0' 68 | await profilesStore.ensureVersionManifest() 69 | const data = profilesStore.versionManifest.versions.find(it => it.id === r.mcVersion) 70 | if (!data) throw new Error('No such version: ' + r.mcVersion) 71 | await addTask(Installer.Installer.installTask('client', data, GAME_ROOT, getDownloaders()), 72 | $('Install') + ' Minecraft', r.mcVersion).wait() 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/plugin/internal/index.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from '../Plugin' 2 | import ResourceInstaller from './ResourceInstaller' 3 | import ResourceResolver from './ResourceResolver' 4 | 5 | const map = { 6 | resourceInstaller: ResourceInstaller, 7 | resourceResolver: ResourceResolver 8 | } 9 | 10 | const _plugins: { [k in keyof typeof map]: InstanceType<(typeof map)[k]> } = { } as any 11 | 12 | export default Object.freeze(new Set(Object.entries(map).map(([key, C]) => (_plugins[key] = new C())))) 13 | export const plugins = Object.freeze(_plugins) 14 | -------------------------------------------------------------------------------- /src/protocol/index.ts: -------------------------------------------------------------------------------- 1 | import { ipcRenderer, remote } from 'electron' 2 | import { playNoticeSound, getJson } from '../utils/index' 3 | import * as T from './types' 4 | import fs from 'fs-extra' 5 | import install from './install' 6 | import P from '../models/index' 7 | import GameStore from '../models/GameStore' 8 | import requestReload from '../utils/request-reload' 9 | import { RESOURCES_VERSIONS_INDEX_PATH } from '../constants' 10 | 11 | const gameStore = P.getStore(GameStore) 12 | const currentWindow = remote.getCurrentWindow() 13 | 14 | const mappings = { 15 | Install: (r: T.ProtocolInstall, request?: boolean) => install(r.resource, request, false), 16 | async Launch (data: T.ProtocolLaunch) { 17 | let { version, resource } = data 18 | if (!version && resource) { 19 | if (typeof resource === 'string') resource = await getJson(resource) as T.ResourceVersion 20 | if (!T.isVersion(resource)) return 21 | version = T.resolveVersionId(resource) 22 | if (!(version in (await fs.readJson(RESOURCES_VERSIONS_INDEX_PATH, { throws: false }) || { }))) { 23 | if (data.noAutoInstall) return 24 | await install(resource) 25 | } 26 | } 27 | if (!version) return 28 | if (data.secret !== localStorage.getItem('analyticsToken') && !await openConfirmDialog({ 29 | cancelButton: true, 30 | text: $('Received the request to launch the game. Do you want to launch the game') + ': ' + 31 | (version || profilesStore.selectedVersion.lastVersionId) + '?' 32 | })) return 33 | await gameStore.launch(version) 34 | } 35 | } 36 | 37 | export default mappings 38 | 39 | const INTERRUPTED_MESSAGE = 'interruptedMessage' 40 | const handleMessage = async (data: T.Protocol) => { 41 | console.log(data) 42 | if (!data || typeof data !== 'object') return 43 | try { 44 | if (data.plugins) { 45 | const plugins = Object.keys(data.plugins).filter(it => !(it in pluginMaster.plugins)) 46 | if (plugins.length) { 47 | let needReload = false 48 | let safePluginHashes: string[] 49 | for (const key of plugins) { 50 | const p = data.plugins[key] 51 | const obj: T.InstallView = { safePluginHashes } 52 | await install(p, false, true, T.isPlugin, obj) 53 | if (obj.noDependency) needReload = true 54 | safePluginHashes = obj.safePluginHashes 55 | } 56 | if (needReload) { 57 | const rs = JSON.parse(localStorage.getItem(INTERRUPTED_MESSAGE) || '[]') 58 | rs.push(data) 59 | localStorage.set(INTERRUPTED_MESSAGE, JSON.stringify(rs)) 60 | requestReload() 61 | return 62 | } 63 | } 64 | } 65 | if (data.type in mappings) { 66 | mappings[data.type](data) 67 | playNoticeSound() 68 | currentWindow.flashFrame(true) 69 | currentWindow.restore() 70 | currentWindow.setAlwaysOnTop(true) 71 | currentWindow.setAlwaysOnTop(false) 72 | } else pluginMaster.emit('protocol', data) 73 | } catch (e) { 74 | console.error(e) 75 | } 76 | } 77 | ipcRenderer.on('pure-launcher-protocol', (_, args: any, argv: any) => { 78 | const t = typeof args 79 | if (t === 'string') { 80 | if (args.startsWith('{') && args.endsWith('}')) handleMessage(JSON.parse(args)) 81 | else if (args.includes('://')) { 82 | if (args.startsWith('pure-launcher://')) args = args.replace(/^pure-launcher:\/+/, '') 83 | else pluginMaster.emit('customProtocol', args, argv) 84 | } 85 | } else if (t === 'object' && !Array.isArray(t)) handleMessage(args) 86 | }) 87 | 88 | pluginMaster.once('loaded', () => { 89 | const rs = JSON.parse(localStorage.getItem(INTERRUPTED_MESSAGE) || '[]') 90 | localStorage.removeItem(INTERRUPTED_MESSAGE) 91 | if (rs.length) (async () => { for (const r of rs) await handleMessage(r) })().catch(console.error) 92 | }) 93 | -------------------------------------------------------------------------------- /src/protocol/install.ts: -------------------------------------------------------------------------------- 1 | import * as T from './types' 2 | import user from '../utils/analytics' 3 | import { getJson } from '../utils/index' 4 | import { remote } from 'electron' 5 | 6 | const win = remote.getCurrentWindow() 7 | export default ( 8 | r: R | string, 9 | request = true, 10 | throws = true, 11 | checker: (r: any) => boolean = T.isResource, 12 | obj: T.InstallView = { }, 13 | pluginsNotInstalled = false 14 | ) => { 15 | const p = (async () => { 16 | if (typeof r === 'string') r = await getJson(r) as R 17 | if (!checker(r)) return 18 | obj.request = request 19 | obj.throws = throws 20 | obj.type = r.type 21 | await pluginMaster.emitSync('protocolPreInstallResource', r, obj) 22 | if (request) { 23 | win.flashFrame(true) 24 | win.moveTop() 25 | const req = await global.__requestInstallResources(r, obj) 26 | if (pluginsNotInstalled) return req 27 | if (!req) throw new Error($('canceled')) 28 | user.event('resource', 'install').catch(console.error) 29 | notice({ content: $('Installing resources...') }) 30 | } 31 | await pluginMaster.emitSync('protocolInstallResource', r, obj) 32 | })() 33 | if (!throws) { 34 | p.then(() => notice({ content: $('Successfully installed resources!') }), e => { 35 | console.error(e) 36 | notice({ error: true, content: $('Failed to install resources') + ': ' + (e?.message || $('Unknown')) }) 37 | }) 38 | } 39 | return p 40 | } 41 | -------------------------------------------------------------------------------- /src/protocol/uninstaller.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra' 2 | import { shell } from 'electron' 3 | import { join, basename } from 'path' 4 | import { RESOURCES_VERSIONS_INDEX_PATH, RESOURCES_VERSIONS_PATH, VERSIONS_PATH, 5 | RESOURCES_MODS_INDEX_FILE_NAME, RESOURCES_RESOURCE_PACKS_INDEX_PATH, RESOURCE_PACKS_PATH, DELETES_FILE, RESOURCES_PLUGINS_INDEX } from '../constants' 6 | import { Plugin } from '../plugin/Plugin' 7 | import { FILE } from '../plugin/index' 8 | 9 | export const uninstallVersion = async (id: string) => { 10 | const resolvedId = await profilesStore.resolveVersion(id) 11 | const json = await fs.readJson(RESOURCES_VERSIONS_INDEX_PATH, { throws: false }) || { } 12 | if (resolvedId in json) { 13 | delete json[resolvedId] 14 | await fs.writeJson(RESOURCES_VERSIONS_INDEX_PATH, json) 15 | } 16 | if (id in profilesStore.profiles) { 17 | delete profilesStore.profiles[id] 18 | await profilesStore.saveLaunchProfileJson() 19 | } 20 | await fs.remove(join(RESOURCES_VERSIONS_PATH, resolvedId)).catch(() => {}) 21 | await fs.remove(join(VERSIONS_PATH, resolvedId)).catch(console.error) 22 | } 23 | 24 | export const uninstallMod = async (version: string, id: string, directly = false) => { 25 | const dir = join(VERSIONS_PATH, version, 'mods') 26 | if (directly) { 27 | if (!shell.moveItemToTrash(join(dir, id))) throw new Error('Delete failed!') 28 | } else { 29 | const jsonPath = join(RESOURCES_VERSIONS_PATH, version, RESOURCES_MODS_INDEX_FILE_NAME) 30 | console.log(version, id, directly, jsonPath) 31 | const json = await fs.readJson(jsonPath, { throws: false }) || {} 32 | if (!Array.isArray(json[id]?.hashes)) return 33 | await Promise.all(json[id].hashes.map((it: string) => fs.unlink(join(dir, it + '.jar')).catch(() => {}))) 34 | delete json[id] 35 | await fs.writeJson(jsonPath, json) 36 | } 37 | } 38 | 39 | export const uninstallResourcePack = async (id: string, directly = false) => { 40 | if (directly) { 41 | if (!shell.moveItemToTrash(join(RESOURCE_PACKS_PATH, id))) throw new Error('Delete failed!') 42 | } else { 43 | const json = await fs.readJson(RESOURCES_RESOURCE_PACKS_INDEX_PATH, { throws: false }) || {} 44 | if (!Array.isArray(json[id]?.hashes)) return 45 | await Promise.all(json[id].hashes.map((it: string) => fs.unlink(join(RESOURCE_PACKS_PATH, it + '.zip')) 46 | .catch(() => {}))) 47 | delete json[id] 48 | await fs.writeJson(RESOURCES_RESOURCE_PACKS_INDEX_PATH, json) 49 | } 50 | } 51 | 52 | export const uninstallPlugin = async (p: Plugin) => { 53 | const deletes: string[] = await fs.readJson(DELETES_FILE, { throws: false }) || [] 54 | if (!pluginMaster.isPluginUninstallable(p, deletes)) throw new Error('Plugin cannot be uninstalled!') 55 | pluginMaster.pluginFileMap[p.pluginInfo.id] = p[FILE] 56 | deletes.push(basename(p[FILE])) 57 | const json = await fs.readJson(RESOURCES_PLUGINS_INDEX, { throws: false }) || { } 58 | delete json[p.pluginInfo.id] 59 | await Promise.all([fs.writeJson(DELETES_FILE, deletes), fs.writeJson(RESOURCES_PLUGINS_INDEX, json)]) 60 | return deletes 61 | } 62 | -------------------------------------------------------------------------------- /src/routes/CustomServerHome.tsx: -------------------------------------------------------------------------------- 1 | import React, { useLayoutEffect, useRef } from 'react' 2 | import E from 'electron' 3 | import { useLocation } from 'react-router-dom' 4 | import { queryStatus } from '@xmcl/client/index' 5 | 6 | const style = { 7 | position: 'absolute' as 'absolute', 8 | left: 192, 9 | right: -10, 10 | bottom: 0, 11 | top: 0 12 | } 13 | const ServerHome: React.FC = () => { 14 | let src = useLocation().search.replace(/^\?/, '') 15 | if (!src) return null 16 | src = decodeURIComponent(src) 17 | const ref = useRef() 18 | const w = ref.current 19 | useLayoutEffect(() => { 20 | const elm = document.getElementById('custom-server-home') as any as E.WebviewTag 21 | const cb = () => { 22 | elm.insertCSS(` 23 | ::-webkit-scrollbar { 24 | width: 7px; 25 | height: 7px; 26 | } 27 | 28 | ::-webkit-scrollbar-thumb { 29 | border-radius: 10px; 30 | background: rgba(0, 0, 0, .8); 31 | box-shadow: 0 3px 3px -2px rgba(0, 0, 0, .2), 32 | 0 3px 4px 0 rgba(0, 0, 0, .14), 33 | 0 1px 8px 0 rgba(0, 0, 0, .12); 34 | } 35 | 36 | ::-webkit-scrollbar-thumb:hover { 37 | box-shadow: 0 4px 5px -2px rgba(0, 0, 0, 0.2), 38 | 0 7px 10px 1px rgba(0, 0, 0, 0.14), 39 | 0 2px 16px 1px rgba(0, 0, 0, 0.12); 40 | }`) 41 | } 42 | elm.addEventListener('dom-ready', cb) 43 | const ipcEvent = (e: E.IpcMessageEvent) => { 44 | switch (e.channel) { 45 | case 'get-account': { 46 | const p = profilesStore.getCurrentProfile() 47 | elm.send('account', p.uuid, p.username, p.skinUrl, p.type) 48 | break 49 | } 50 | case 'query-minecraft-server': 51 | queryStatus.apply(null, e.args[1]) 52 | .then(info => elm.send('minecraft-server-data', e.args[0], null, info)) 53 | .catch(e => elm.send('minecraft-server-data', e.args[0], e?.message || '')) 54 | } 55 | } 56 | elm.addEventListener('ipc-message', ipcEvent) 57 | let fn: () => any 58 | if (process.env.DEV_SERVER_HOME) elm.addEventListener('dom-ready', (fn = () => elm.openDevTools())) 59 | return () => { 60 | elm.removeEventListener('dom-ready', cb) 61 | elm.removeEventListener('dom-ready', fn) 62 | elm.removeEventListener('ipc-message', ipcEvent) 63 | } 64 | }, [w]) 65 | return React.createElement('webview', { 66 | src, 67 | ref, 68 | style, 69 | id: 'custom-server-home', 70 | enableremotemodule: 'false', 71 | preload: './serverHomePreload.js' 72 | }) 73 | } 74 | 75 | export default ServerHome 76 | -------------------------------------------------------------------------------- /src/routes/Error.tsx: -------------------------------------------------------------------------------- 1 | import './error.css' 2 | import React from 'react' 3 | import history from '../utils/history' 4 | 5 | const ErrorPage: React.FC = () => { 6 | return
    7 |
    8 | history.goBack()} 12 | /> 13 |

    {$('An unknown error occurred!\nPlease click the Creeper icon above to reload the page.')}

    14 |
    15 |
    16 | } 17 | 18 | export default ErrorPage 19 | -------------------------------------------------------------------------------- /src/routes/Home.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-first-prop-new-line */ 2 | import './home.less' 3 | import Slider from 'react-slick' 4 | import user from '../utils/analytics' 5 | import Loading from '../components/Loading' 6 | import React, { useEffect, useState } from 'react' 7 | import { shell } from 'electron' 8 | import { NEWS_URL } from '../constants' 9 | 10 | interface News { 11 | slides: Array<{ url: string, title: string, img: string }> 12 | news: Array<{ time: string, title: string, classify: string, link: string }> 13 | } 14 | 15 | const NIL = { slides: [], news: [] } 16 | 17 | const openUrl = (url: string) => { 18 | shell.openExternal(url) 19 | user.event('external url', 'open').catch(console.error) 20 | } 21 | const Home: React.FC = () => { 22 | const [data, setData] = useState(NIL) 23 | const [loading, setLoading] = useState(false) 24 | const [fail, setFail] = useState(false) 25 | const load = () => { 26 | if (!navigator.onLine) { 27 | setFail(true) 28 | return 29 | } 30 | const time = +localStorage.getItem('newsTime') || 0 31 | let promise = Promise.resolve() 32 | if (Date.now() - time > 12 * 60 * 60 * 1000) { 33 | setLoading(true) 34 | localStorage.removeItem('news') 35 | promise = fetch(NEWS_URL) 36 | .then(it => it.text()) 37 | .then(it => { 38 | localStorage.setItem('news', it) 39 | localStorage.setItem('newsTime', Date.now().toString()) 40 | }) 41 | .catch(console.error) 42 | } 43 | promise.then(() => { 44 | const v = localStorage.getItem('news') 45 | setLoading(false) 46 | if (v) setTimeout(setData, 100, JSON.parse(v)) 47 | else setFail(true) 48 | }) 49 | } 50 | useEffect(() => { 51 | load() 52 | document.addEventListener('online', load) 53 | return () => document.removeEventListener('online', load) 54 | }, []) 55 | return ( 56 |
    89 | ) 90 | } 91 | 92 | export default Home 93 | -------------------------------------------------------------------------------- /src/routes/Manager.tsx: -------------------------------------------------------------------------------- 1 | import './manager.css' 2 | import React from 'react' 3 | import Dots from '../components/Dots' 4 | import LiveRoute from '../components/LiveRoute' 5 | import history from '../utils/history' 6 | import { useLocation, Route } from 'react-router-dom' 7 | 8 | import Versions from './manager/Versions' 9 | import Tasks from './manager/Tasks' 10 | import Profiles from './manager/Profiles' 11 | import Extensions from './manager/Extensions' 12 | import Plugins from './manager/Plugins' 13 | import Mods from './manager/Mods' 14 | import ResourcePacks from './manager/ResourcePacks' 15 | import Worlds from './manager/Worlds' 16 | import ShaderPacks from './manager/ShaderPacks' 17 | 18 | export const getPages = () => [ 19 | { 20 | name: $('Versions'), 21 | path: '/manager/versions', 22 | component: Versions 23 | }, 24 | { 25 | name: $('Accounts'), 26 | path: '/manager/accounts', 27 | component: Profiles 28 | }, 29 | { 30 | name: $('Tasks'), 31 | path: '/manager/tasks', 32 | component: Tasks 33 | }, 34 | { 35 | name: $('Extensions'), 36 | path: '/manager/extensions', 37 | component: Extensions 38 | } 39 | ] 40 | 41 | const Manager: React.FC = () => { 42 | const pages = getPages() 43 | const pathname = useLocation().pathname 44 | const onChange = (i: number) => history.push(pages[i].path) 45 | 46 | return
    47 |
    48 | {pages.map(it => )} 49 | 50 | 51 | 52 | 53 | 54 |
    55 | it.name)} 59 | active={pages.findIndex(it => it.path === pathname)} 60 | /> 61 |
    62 | } 63 | 64 | export default Manager 65 | -------------------------------------------------------------------------------- /src/routes/ServerHome.tsx: -------------------------------------------------------------------------------- 1 | import './server-home.less' 2 | import React, { useMemo, useState, useEffect } from 'react' 3 | import Img from 'react-image' 4 | import Empty from '../components/Empty' 5 | import Loading from '../components/Loading' 6 | import { AnimatePresence, motion } from 'framer-motion' 7 | import { parse } from 'querystring' 8 | import { useLocation } from 'react-router-dom' 9 | import { queryStatus } from '@xmcl/client/status' 10 | import { fromFormattedString, render } from '@xmcl/text-component/index' 11 | 12 | const Div = motion.div as any 13 | 14 | const LOGO = require('../assets/images/unknown-server.png') 15 | 16 | interface Ret { 17 | description: string 18 | max: number 19 | online: number 20 | logo: string 21 | ping: number 22 | } 23 | 24 | const getStatus = async (host: string, port?: string) => { 25 | if (host) { 26 | const obj: any = { host } 27 | if (port) { 28 | const p = parseInt(port) 29 | if (!isNaN(p) && p > 0 && p < 65536) obj.port = p 30 | } 31 | const data = await queryStatus(obj, { timeout: 10000 }).catch(console.error) 32 | // console.log(data, obj) 33 | return data ? { 34 | description: typeof data.description === 'string' ? data.description : data.description?.text || '', 35 | max: data.players?.max || 0, 36 | online: data.players?.online || 0, 37 | logo: data.favicon, 38 | ping: data.ping 39 | } as Ret : null 40 | } else return null 41 | } 42 | 43 | interface Info { host?: string, port?: string, name?: string, description?: string, logo?: string } 44 | 45 | const ServerHome: React.FC = () => { 46 | const [status, setStatus] = useState() 47 | const args: Info = parse(useLocation().search.replace(/^\?/, '')) 48 | if (!args.host) return 49 | useEffect(() => { if (args.host) getStatus(args.host, args.port).then(setStatus) }, [args.host]) 50 | const desc = (args.description || status?.description || '').toString() 51 | return
    52 | 53 | {status ?
    60 | logo} 63 | unloader={logo} 64 | /> 65 |

    {args.name || args.host}

    66 | {status.online && status.max &&

    {$('Online Players')}: {status.online}/{status.max}

    } 67 | {useMemo(() => { 68 | if (!desc) return 69 | const p = render(fromFormattedString(desc)) 70 | return

    {p.component.text}{p.children?.map((it, i) => 71 | {it.component.text})}

    72 | }, [desc])} 73 |
    :
    } 80 |
    81 |
    82 | } 83 | 84 | export default ServerHome 85 | -------------------------------------------------------------------------------- /src/routes/error.css: -------------------------------------------------------------------------------- 1 | .error { 2 | display: flex; 3 | } 4 | .error .container { 5 | margin: auto; 6 | } 7 | .error i { 8 | cursor: pointer; 9 | color: #f5222d; 10 | transition: color .6s; 11 | } 12 | .error i:hover { 13 | color: #f86d74; 14 | } 15 | .error p { 16 | white-space: pre-wrap; 17 | } 18 | -------------------------------------------------------------------------------- /src/routes/home.less: -------------------------------------------------------------------------------- 1 | .home { 2 | display: flex !important; 3 | flex-wrap: wrap; 4 | justify-content: center; 5 | .slider { 6 | -webkit-app-region: no-drag; 7 | transition: opacity 1s; 8 | margin: 30px 40px; 9 | box-shadow: 0px 8px 11px -5px rgba(0, 0, 0, 0.2), 10 | 0px 17px 26px 2px rgba(0, 0, 0, 0.14), 11 | 0px 6px 32px 5px rgba(0, 0, 0, 0.12); 12 | .slick-slider { 13 | width: 460px; 14 | height: 175px; 15 | } 16 | .cover { 17 | cursor: pointer; 18 | position: relative; 19 | span { 20 | color: #ffffffe6; 21 | background-color: #0000007a; 22 | position: absolute; 23 | bottom: 0; 24 | font-size: 14px; 25 | width: 100%; 26 | text-align: center; 27 | font-weight: 100; 28 | padding: 2px 0; 29 | } 30 | img { 31 | width: 460px; 32 | height: 175px; 33 | } 34 | } 35 | } 36 | 37 | .news { 38 | user-select: none; 39 | -webkit-app-region: no-drag; 40 | transition: .6s; 41 | font-size: 14px; 42 | background: var(--secondary-color); 43 | border-radius: 4px; 44 | overflow: hidden; 45 | padding: 10px 20px; 46 | opacity: .84; 47 | filter: blur(0.4px) brightness(0.96); 48 | box-shadow: 0px 8px 11px -5px rgba(0, 0, 0, 0.2), 49 | 0px 17px 26px 2px rgba(0, 0, 0, 0.14), 50 | 0px 6px 32px 5px rgba(0, 0, 0, 0.12); 51 | p { 52 | margin: 0; 53 | white-space: nowrap; 54 | text-overflow: ellipsis; 55 | overflow: hidden; 56 | } 57 | a { 58 | cursor: pointer; 59 | text-decoration: none; 60 | } 61 | .classify { 62 | font-size: 12px; 63 | color: var(--text-color-2); 64 | } 65 | &:hover { 66 | opacity: 1; 67 | filter: none; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/routes/manager.css: -------------------------------------------------------------------------------- 1 | .manager { 2 | height: 100%; 3 | display: flex; 4 | } 5 | 6 | .manager > .container { 7 | flex: 1; 8 | width: 100%; 9 | border-radius: 4px; 10 | margin: 40px 0 15px; 11 | background: var(--secondary-color); 12 | box-shadow: 0 8px 11px -5px rgba(0, 0, 0, 0.2), 13 | 0 17px 26px 2px rgba(0, 0, 0, 0.14), 14 | 0 6px 32px 5px rgba(0, 0, 0, 0.12); 15 | } 16 | 17 | .manager > .container > div { height: 100% } 18 | -------------------------------------------------------------------------------- /src/routes/manager/Extensions.tsx: -------------------------------------------------------------------------------- 1 | import './list.less' 2 | import React, { useState } from 'react' 3 | import ToolTip from 'rc-tooltip' 4 | import IconButton from '../../components/IconButton' 5 | 6 | const css = { zIndex: 1100 } 7 | const Extensions: React.FC = () => { 8 | (window as any).__extensionsUpdater = useState(false) 9 | const arr = new Array(pluginMaster.extensionsButtons.size) 10 | let i = 0 11 | pluginMaster.extensionsButtons.forEach(it => { 12 | const title = it.title() 13 | arr[i++] = 14 | 15 | 16 | }) 17 | return
    18 |
    19 | {$('Extensions')} 20 |
    21 |
    22 |
    23 |
    {arr}
    24 |
    25 |
    26 |
    27 | } 28 | 29 | export default Extensions 30 | -------------------------------------------------------------------------------- /src/routes/manager/Plugins.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import history from '../../utils/history' 3 | import internal, { plugins } from '../../plugin/internal' 4 | import { uninstallPlugin } from '../../protocol/uninstaller' 5 | import { autoNotices } from '../../utils' 6 | import { clipboard } from 'electron' 7 | 8 | pluginMaster.addExtensionsButton({ 9 | title: () => $('Plugins'), 10 | key: 'plugins', 11 | onClick () { history.push('/manager/plugins') } 12 | }, plugins.resourceInstaller) 13 | 14 | const Plugins: React.FC = () => { 15 | const [deletes, setDeletes] = useState([]) 16 | return
    17 |
    18 | {$('Plugins')} 19 |
    20 |
      21 | {Object.values(pluginMaster.plugins) 22 | .map(p => { 23 | const uninstallable = pluginMaster.isPluginUninstallable(p, deletes) 24 | return
    • {p.pluginInfo.title()} 25 | ({p.pluginInfo.version}) 26 |
      {p.pluginInfo?.description()}
      27 | {!internal.has(p) &&
      28 | {p.pluginInfo.source && } 35 | 42 |
      } 43 |
    • 44 | })} 45 |
    46 |
    47 | } 48 | 49 | export default Plugins 50 | -------------------------------------------------------------------------------- /src/routes/manager/Profiles.tsx: -------------------------------------------------------------------------------- 1 | import './list.less' 2 | import React from 'react' 3 | import Avatar from '../../components/Avatar' 4 | import ProfilesStore from '../../models/ProfilesStore' 5 | import * as Auth from '../../plugin/Authenticator' 6 | import { useStore } from 'reqwq' 7 | import { join } from 'path' 8 | import { SKINS_PATH } from '../../constants' 9 | import { autoNotices } from '../../utils' 10 | 11 | const steve = require('../../assets/images/steve.png') 12 | const Profiles: React.FC = () => { 13 | const pm = useStore(ProfilesStore) 14 | const cp = pm.getCurrentProfile() 15 | return
    16 | 23 |
      {pluginMaster.getAllProfiles().map(it =>
    • 24 | 25 | {it.displayName ? <>{it.username} {it.displayName} : it.username} 26 |
      {pluginMaster.logins[it.type][Auth.TITLE](pluginMaster.logins[it.type], it)}
      27 |
      28 | {(!cp || cp.key !== it.key) && 29 | } 30 | 37 |
      38 |
    • )} 39 |
    40 |
    41 | } 42 | 43 | export default Profiles 44 | -------------------------------------------------------------------------------- /src/routes/manager/ShaderPacks.tsx: -------------------------------------------------------------------------------- 1 | import './list.less' 2 | import fs from 'fs-extra' 3 | import ToolTip from 'rc-tooltip' 4 | import history from '../../utils/history' 5 | import Empty from '../../components/Empty' 6 | import Loading from '../../components/Loading' 7 | import React, { useState, useEffect } from 'react' 8 | import { join, basename } from 'path' 9 | import { plugins } from '../../plugin/internal/index' 10 | import { removeFormatCodes, autoNotices, watchFile } from '../../utils/index' 11 | import { SHADER_PACKS_PATH } from '../../constants' 12 | import { requestPath } from '../../protocol/exporter' 13 | import { remote, shell } from 'electron' 14 | 15 | pluginMaster.addExtensionsButton({ 16 | title: () => $('ShaderPacks'), 17 | key: 'shaderPacks', 18 | onClick () { history.push('/manager/shaderPacks') } 19 | }, plugins.resourceInstaller) 20 | 21 | const getShaderPacks = async () => { 22 | try { 23 | const ret = await Promise.all((await fs.readdir(SHADER_PACKS_PATH)).filter(it => it.endsWith('.zip')) 24 | .map(async it => { 25 | const path = join(SHADER_PACKS_PATH, it) 26 | if (!(await fs.stat(path)).isFile()) return 27 | return [ 28 | basename(removeFormatCodes(it), '.zip'), 29 | path 30 | ] as [string, string] 31 | })) 32 | return ret.filter(Boolean) 33 | } catch (e) { console.error(e) } 34 | return [] 35 | } 36 | 37 | const ShaderPack: React.FC = () => { 38 | const [packs, setPacks] = useState>() 39 | useEffect(() => watchFile(SHADER_PACKS_PATH, () => setPacks(null)), []) 40 | useEffect(() => { if (!packs) getShaderPacks().then(setPacks) }, [packs]) 41 | const requestUninstall = (path: string) => packs && openConfirmDialog({ 42 | cancelButton: true, 43 | title: $('Warning!'), 44 | text: $('Are you sure to delete this {0}? Files can be recovered in the recycle bin.', $('shader pack')) 45 | }).then(ok => { 46 | if (ok) { 47 | notice({ content: $('Deleting...') }) 48 | autoNotices(shell.moveItemToTrash(path)) 49 | setPacks(null) 50 | } 51 | }) 52 | return packs ? packs.length ?
      53 | {packs.map(it =>
    • { 57 | e.preventDefault() 58 | remote.getCurrentWebContents().startDrag({ file: it[0], icon: it[1] }) 59 | }} 60 | > 61 |
      {it[0]}
      62 |
      63 | 67 | 68 |
      69 |
    • )} 70 |
    : :
    71 | } 72 | 73 | const ShaderPacks: React.FC = () => { 74 | return
    75 |
    76 | 77 | shell.openItem(SHADER_PACKS_PATH)}> 78 | {$('ShaderPacks')} 79 | 80 |
    81 | 82 |
    83 | } 84 | 85 | export default ShaderPacks 86 | -------------------------------------------------------------------------------- /src/routes/manager/Tasks.tsx: -------------------------------------------------------------------------------- 1 | import './list.less' 2 | import prettyBytes from 'pretty-bytes' 3 | import React, { Component } from 'react' 4 | import { TaskStatus } from '../../utils/index' 5 | import { downloader } from '../../plugin/DownloadProviders' 6 | 7 | const css = { transform: 'unset' } 8 | export default class Tasks extends Component { 9 | private shouldUpdate = false 10 | private bytes = 0 11 | private prevBytes = 0 12 | private timer = setInterval(() => { 13 | this.prevBytes = this.bytes 14 | this.bytes = downloader.bytes 15 | if (downloader.bytes && this.bytes === this.prevBytes) { 16 | downloader.bytes = this.prevBytes = this.bytes = 0 17 | this.forceUpdate() 18 | } 19 | if (!this.shouldUpdate) return 20 | this.forceUpdate() 21 | this.shouldUpdate = false 22 | }, 500) 23 | constructor (props: any, context: any) { 24 | super(props, context) 25 | window.__updateTasksView = () => { this.shouldUpdate = true } 26 | } 27 | public componentWillUnmount () { 28 | window.__updateTasksView = () => { } 29 | clearInterval(this.timer) 30 | } 31 | public render () { 32 | const cancel = $('Cancel') 33 | return
    34 |
    35 | {$('Tasks')} 36 | {$('Speed')}: {prettyBytes((this.bytes - this.prevBytes) * 2)}/s 37 | 41 | 42 | {$('Clear')} 43 | 44 |
    45 |
      {__tasks.map(it =>
    • 46 |
      {it.name} {it.subName && {it.subName}}
      47 | {it.status === TaskStatus.PENDING && } 48 | {it.status === TaskStatus.PENDING &&
      49 | 50 |
      } 51 |
    • )}
    52 |
    53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/routes/manager/Worlds.tsx: -------------------------------------------------------------------------------- 1 | import './list.less' 2 | import fs from 'fs-extra' 3 | import ToolTip from 'rc-tooltip' 4 | import history from '../../utils/history' 5 | import Empty from '../../components/Empty' 6 | import Loading from '../../components/Loading' 7 | import React, { useState, useEffect } from 'react' 8 | import { join } from 'path' 9 | import { deserialize } from '@xmcl/nbt/index' 10 | import { plugins } from '../../plugin/internal/index' 11 | import { removeFormatCodes, autoNotices, watchFile } from '../../utils/index' 12 | import { WORLDS_PATH } from '../../constants' 13 | import { exportWorld } from '../../protocol/exporter' 14 | import { remote, shell } from 'electron' 15 | 16 | pluginMaster.addExtensionsButton({ 17 | title: () => $('Worlds'), 18 | key: 'worlds', 19 | onClick () { history.push('/manager/worlds') } 20 | }, plugins.resourceInstaller) 21 | 22 | const icon = require('../../assets/images/unknown-server.png') 23 | 24 | const getWorlds = async () => { 25 | try { 26 | const ret = await Promise.all((await fs.readdir(WORLDS_PATH)).map(async it => { 27 | const path = join(WORLDS_PATH, it) 28 | const level = join(path, 'level.dat') 29 | if (!await fs.pathExists(level)) return 30 | const img = join(path, 'icon.png') 31 | return [ 32 | it, 33 | removeFormatCodes((await deserialize<{ Data: { LevelName: string } }>(await fs.readFile(level))).Data.LevelName), 34 | await fs.pathExists(img) ? img.replace(/#/g, '%23') : icon, 35 | (await fs.stat(level)).mtimeMs 36 | ] as [string, string, string, number] 37 | })) 38 | return ret.filter(Boolean).sort((a, b) => b[3] - a[3]) 39 | } catch (e) { console.error(e) } 40 | return [] 41 | } 42 | 43 | const WorldsList: React.FC = () => { 44 | const [worlds, setWorlds] = useState>() 45 | useEffect(() => watchFile(WORLDS_PATH, () => setWorlds(null)), []) 46 | useEffect(() => { if (!worlds) getWorlds().then(setWorlds) }, [worlds]) 47 | const requestUninstall = (id: string) => !worlds && openConfirmDialog({ 48 | cancelButton: true, 49 | title: $('Warning!'), 50 | text: $('Are you sure to delete this {0}? Files can be recovered in the recycle bin.', $('world')) 51 | }).then(ok => { 52 | if (ok) { 53 | notice({ content: $('Deleting...') }) 54 | autoNotices(shell.moveItemToTrash(join(WORLDS_PATH, id))) 55 | setWorlds(null) 56 | } 57 | }) 58 | return worlds ? worlds.length ?
      59 | {worlds.map(it =>
    • { 63 | e.preventDefault() 64 | remote.getCurrentWebContents().startDrag({ file: it[0], icon: it[1] }) 65 | }} 66 | > 67 | {it[0]} 68 |
      {it[1]}
      {it[0]}
      69 |
      70 | 74 | 75 |
      76 |
    • )} 77 |
    : :
    78 | } 79 | 80 | const Worlds: React.FC = () => { 81 | return
    82 |
    83 | 84 | shell.openItem(WORLDS_PATH)}>{$('Worlds')} 85 | 86 |
    87 | 88 |
    89 | } 90 | 91 | export default Worlds 92 | -------------------------------------------------------------------------------- /src/routes/server-home.less: -------------------------------------------------------------------------------- 1 | .server-home { 2 | height: 100%; 3 | display: flex; 4 | justify-content: center; 5 | align-items: center; 6 | & > .container { 7 | padding: 20px; 8 | min-width: 250px; 9 | border-radius: 4px; 10 | text-align: center; 11 | background: var(--secondary-color); 12 | box-shadow: 0 8px 11px -5px rgba(0, 0, 0, 0.2), 13 | 0 17px 26px 2px rgba(0, 0, 0, 0.14), 14 | 0 6px 32px 5px rgba(0, 0, 0, 0.12); 15 | } 16 | img { 17 | width: 120px; 18 | height: 120px; 19 | border-radius: 4px; 20 | box-shadow: 0 8px 11px -5px rgba(0, 0, 0, 0.2), 0 17px 26px 2px rgba(0, 0, 0, 0.14), 21 | 0 6px 32px 5px rgba(0, 0, 0, 0.12); 22 | } 23 | .desc { 24 | white-space: pre-wrap; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/routes/settings.css: -------------------------------------------------------------------------------- 1 | .settings { 2 | background: var(--secondary-color); 3 | margin: 40px 10px 0; 4 | padding: 20px; 5 | border-radius: 4px; 6 | box-shadow: 0px 8px 11px -5px rgba(0, 0, 0, 0.2), 7 | 0px 17px 26px 2px rgba(0, 0, 0, 0.14), 8 | 0px 6px 32px 5px rgba(0, 0, 0, 0.12); 9 | } 10 | -------------------------------------------------------------------------------- /src/side-bar.less: -------------------------------------------------------------------------------- 1 | .side-bar { 2 | flex-shrink: 0; 3 | display: flex; 4 | user-select: none; 5 | flex-direction: column; 6 | align-items: center; 7 | width: 170px; 8 | height: 100%; 9 | z-index: 2; 10 | padding: 0 6px; 11 | background: var(--main-color); 12 | box-shadow: 0px 3px 5px -1px rgba(0, 0, 0, 0.2), 13 | 0px 6px 10px 0px rgba(0, 0, 0, 0.14), 14 | 0px 1px 18px 0px rgba(0, 0, 0, 0.12); 15 | 16 | .avatar { 17 | margin-top: 50px; 18 | width: 80px; 19 | height: 80px; 20 | box-shadow: 0px 2px 4px -1px rgba(0, 0, 0, 0.2), 21 | 0px 4px 5px 0px rgba(0, 0, 0, 0.14), 22 | 0px 1px 10px 0px rgba(0, 0, 0, 0.12); 23 | transition: .4s ease-in-out; 24 | cursor: pointer; 25 | -webkit-app-region: no-drag; 26 | 27 | &:hover { 28 | filter: blur(1px) brightness(0.48); 29 | } 30 | 31 | &::after { 32 | content: ''; 33 | top: 0; 34 | position: absolute; 35 | width: 100%; 36 | height: 100%; 37 | box-shadow: inset 0px 0px 20px 0px #0000005e; 38 | } 39 | } 40 | 41 | .name { 42 | font-size: 18px; 43 | font-family: minecraft; 44 | white-space: nowrap; 45 | text-overflow: ellipsis; 46 | overflow: hidden; 47 | text-shadow: 2px 3px 4px rgba(0, 0, 0, 0.25); 48 | } 49 | 50 | .launch { 51 | width: 160px; 52 | padding: 10px 0; 53 | font-size: 17px; 54 | transition: .3s; 55 | overflow: hidden; 56 | margin-bottom: 8px; 57 | box-shadow: 0 0 16px 0px #36b03064; 58 | &:disabled { 59 | box-shadow: 0 0 16px 0px #ffffff64; 60 | } 61 | &:hover { 62 | box-shadow: 0 0 16px 0px #ffffff44; 63 | } 64 | i { 65 | font-size: 24px; 66 | margin: 2px 6px 0 0; 67 | } 68 | span { 69 | transition: font-size .6s; 70 | line-height: 27px; 71 | } 72 | } 73 | 74 | .version { 75 | font-size: 11px; 76 | color: var(--text-color-1); 77 | margin-bottom: 0; 78 | -webkit-app-region: no-drag; 79 | cursor: pointer; 80 | white-space: nowrap; 81 | text-overflow: ellipsis; 82 | overflow: hidden; 83 | width: 100%; 84 | text-align: center; 85 | 86 | span { font-weight: bold } 87 | } 88 | 89 | .list { 90 | margin-top: 0; 91 | text-shadow: 2px 3px 3px rgba(0, 0, 0, 0.15); 92 | padding-left: 0; 93 | width: 100%; 94 | list-style-type: none; 95 | -webkit-app-region: no-drag; 96 | 97 | img { 98 | width: 26px; 99 | transition: ease-in-out .5s; 100 | image-rendering: pixelated; 101 | } 102 | 103 | a { 104 | display: flex; 105 | align-items: center; 106 | justify-content: center; 107 | text-decoration: none; 108 | } 109 | 110 | span { 111 | color: var(--text-color-1); 112 | font-weight: 300; 113 | font-size: 16px; 114 | margin: 0 auto; 115 | transition: ease-in-out .5s; 116 | } 117 | 118 | li { 119 | padding: 0 20px 0 40px; 120 | transition: ease-in-out .5s; 121 | line-height: 40px; 122 | filter: brightness(0.7); 123 | } 124 | 125 | li.active { filter: brightness(1.3) } 126 | 127 | li.active img { 128 | width: 30px; 129 | margin-left: -2px; 130 | } 131 | 132 | li.active span { 133 | color: var(--text-color-0); 134 | font-size: 17px; 135 | font-weight: 500; 136 | } 137 | } 138 | 139 | .dropdown { 140 | top: 200px; 141 | left: 200px; 142 | display: flex; 143 | align-items: center; 144 | height: calc(100% - 300px); 145 | 146 | .cover { 147 | margin-left: 30px; 148 | } 149 | .cover:after { 150 | left: 0; 151 | bottom: 50%; 152 | } 153 | ul { 154 | margin: 0; 155 | padding: 0; 156 | list-style-type: none; 157 | } 158 | li { 159 | display: block; 160 | background-color: var(--dropdown-color); 161 | } 162 | .active { 163 | font-weight: bold; 164 | } 165 | a { 166 | padding: 4px 12px; 167 | font-size: 15px; 168 | display: block; 169 | font-weight: 200; 170 | text-decoration: none; 171 | } 172 | } 173 | } 174 | 175 | #sidebar-switch { margin: 0 } 176 | -------------------------------------------------------------------------------- /src/utils/EventBus.ts: -------------------------------------------------------------------------------- 1 | export const INTERRUPTIBLE = Symbol('Interruptible') 2 | const ONCE = Symbol('Once') 3 | export default class EventBus { 4 | private map = new Map any>>() 5 | public on (name: string, fn: (...args: any[]) => any) { 6 | if (typeof fn !== 'function') throw new TypeError('fn is not a function!') 7 | let arr = this.map.get(name) 8 | if (!arr) this.map.set(name, arr = []) 9 | arr.push(fn) 10 | } 11 | public once (name: string, fn: (...args: any[]) => any) { 12 | fn[ONCE] = true 13 | this.on(name, fn) 14 | } 15 | public off (name: string, fn: (...args: any[]) => any) { 16 | if (typeof fn !== 'function') throw new TypeError('fn is not a function!') 17 | const arr = this.map.get(name) 18 | if (arr) { 19 | const i = arr.indexOf(fn) 20 | if (i !== -1) { 21 | arr.splice(i, 1) 22 | if (!arr.length) this.map.delete(name) 23 | } 24 | } 25 | } 26 | public async emitSync (name: string, ...args: any[]) { 27 | const arr = this.map.get(name) 28 | if (arr) { 29 | for (let i = 0; i < arr.length;) { 30 | const fn = arr[i] 31 | if (fn[ONCE]) arr.splice(i, 1) 32 | else i++ 33 | if (fn[INTERRUPTIBLE]) await fn.apply(null, args) 34 | else try { await fn.apply(null, args) } catch (e) { console.error(e) } 35 | } 36 | } 37 | } 38 | public async emit (name: string, ...args: any[]) { 39 | const arr = this.map.get(name) 40 | if (!arr) return Promise.resolve() 41 | const promises = [] 42 | for (let i = 0; i < arr.length;) { 43 | const fn = arr[i] 44 | if (fn[ONCE]) arr.splice(i, 1) 45 | else i++ 46 | if (fn[INTERRUPTIBLE]) promises.push(fn.apply(null, args)) 47 | else try { promises.push(fn.apply(null, args)?.catch?.(console.error)) } catch (e) { console.error(e) } 48 | } 49 | await Promise.all(promises) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/utils/analytics.ts: -------------------------------------------------------------------------------- 1 | import uuid from 'uuid-by-string' 2 | import GoogleAnalytics from 'google-analytics-lite/dist/esnext/index' 3 | import { version as av } from '../../package.json' 4 | 5 | export const genUUIDOrigin = (t?: string) => uuid(t || (Math.random().toString() + Math.random().toString())) 6 | export const pagesFilter: Array<(path: string | null, source: string) => string | null> = [ 7 | (_, path) => path.startsWith('/manager/mods/') ? '/manager/mods' : path 8 | ] 9 | 10 | let token = localStorage.getItem('analyticsToken') 11 | if (!token) localStorage.setItem('analyticsToken', (token = genUUIDOrigin())) 12 | const ga = new GoogleAnalytics('UA-155613176-1', token) 13 | Object.assign(ga.defaultValues, { 14 | av, 15 | ds: 'app', 16 | ul: navigator.languages[0], 17 | an: 'PureLauncher', 18 | aid: 'cn.apisium.purelauncher' 19 | }) 20 | const f = ga.pageView 21 | ga.pageView = (dl: string, dh?: string, dt?: string, other?: any) => { 22 | if (dh) { 23 | dh = pagesFilter.reduce((p, fn) => fn(p, dh), dh) 24 | return dh ? f.call(ga, dl, dh, dt, other) : Promise.resolve(true) 25 | } 26 | return f.call(ga, dl, dh, dt, other) 27 | } 28 | 29 | export default ga 30 | -------------------------------------------------------------------------------- /src/utils/fit-text.tsx: -------------------------------------------------------------------------------- 1 | const ctx = new OffscreenCanvas(0, 0).getContext('2d') 2 | 3 | export default (text: string, width: number, max = 100) => { 4 | const font = getComputedStyle(document.body).fontFamily 5 | let size = 20 6 | ctx.font = size + 'px ' + font 7 | if (ctx.measureText(text).width > width) { 8 | do { 9 | ctx.font = (--size) + 'px ' + font 10 | } while (size > 1 && ctx.measureText(text).width > width) 11 | } else { 12 | do { 13 | ctx.font = (++size) + 'px ' + font 14 | } while (size < max && ctx.measureText(text).width < width) 15 | size-- 16 | } 17 | return size 18 | } 19 | -------------------------------------------------------------------------------- /src/utils/fs.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'original-fs' 2 | 3 | export const move = (path: string, dest: string) => fs.rename(path, dest).catch(e => { 4 | if (e?.code !== 'EXDEV') throw e 5 | return fs.copyFile(path, dest).then(() => fs.unlink(path)) 6 | }) 7 | -------------------------------------------------------------------------------- /src/utils/hacks.ts: -------------------------------------------------------------------------------- 1 | import { downloader } from '../plugin/DownloadProviders' 2 | 3 | require('@xmcl/installer/util').resolveDownloader = (opts: any, closure: any) => 4 | closure(opts.downloader ? opts : { ...opts, downloader }) 5 | 6 | const fs = require('fs-extra') 7 | const installer = require('@xmcl/installer/index') 8 | 9 | installer.CurseforgeInstaller.DEFAULT_QUERY = (project: number, file: number) => fetch( 10 | `https://addons-ecs.forgesvc.net/api/v2/addon/${project}/file/${file}/download-url` 11 | ).then(it => it.json()) 12 | 13 | const { readJson } = fs 14 | fs.readJson = (path: string, opts: any) => { 15 | const p = readJson(path, opts) 16 | return opts && opts.throws === false ? p.catch(() => {}) : p 17 | } 18 | -------------------------------------------------------------------------------- /src/utils/history.ts: -------------------------------------------------------------------------------- 1 | import { createHashHistory } from 'history' 2 | import user from './analytics' 3 | 4 | const history = createHashHistory() 5 | history.listen(l => user.pageView(null, l.pathname, '')) 6 | 7 | export default history 8 | -------------------------------------------------------------------------------- /src/utils/isDev.ts: -------------------------------------------------------------------------------- 1 | export default global['__D' + 'EV__'] = Boolean((typeof __DEV__ === 'boolean' && __DEV__) || !!process.env.DEV || process.env.NODE_ENV !== 'production') 2 | process.env['NODE_' + 'ENV'] = process.env.NODE_ENV 3 | -------------------------------------------------------------------------------- /src/utils/locates.ts: -------------------------------------------------------------------------------- 1 | import { replaceArgs } from '../i18n' 2 | 3 | export type Locates = T & { 4 | setCurrentLanguage (name?: string): void 5 | (key: keyof T, ...args: any[]): 233 6 | } 7 | 8 | export default (langs: Record, defaultLanguage = 'zh-cn') => { 9 | let currentLanguage = langs[defaultLanguage] 10 | if (!currentLanguage) throw new Error('The default language name ' + defaultLanguage + ' is not exists!') 11 | const setCurrentLanguage = (name?: string) => { 12 | currentLanguage = langs[name] || langs[this.defaultLanguage] 13 | } 14 | return new Proxy(Function.prototype, { 15 | get: (_, name) => name === 'setCurrentLanguage' ? setCurrentLanguage : currentLanguage[name], 16 | apply (_, __, args) { 17 | args[0] = currentLanguage[args[0]] 18 | return args[0] ? replaceArgs.apply(null, args) : '' 19 | } 20 | }) as any as Locates 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/request-reload.ts: -------------------------------------------------------------------------------- 1 | import P from '../models/index' 2 | import { remote } from 'electron' 3 | import GameStore, { STATUS } from '../models/GameStore' 4 | 5 | export default (isUpdate = false) => { // TODO: need test!! 6 | if (P.getStore(GameStore).status === STATUS.READY) { 7 | notice({ content: $(isUpdate 8 | ? 'A new version has been released, PureLauncher will restart in five seconds for installation.' 9 | : 'Plugins installed! Restarting...' 10 | ) }) 11 | setTimeout(() => { 12 | if (isUpdate) { 13 | remote.app.relaunch() 14 | window.quitApp() 15 | } else location.reload() 16 | }, 5000) 17 | } else { 18 | openConfirmDialog({ text: $(isUpdate 19 | ? 'A new version has been released, but the game is running now. Please manually exit the launcher and game to upgrade.' 20 | : 'Currently, the game is launching. Please restart manually later to install the plugins!' 21 | ) }) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /static/serverHomePreload.js: -------------------------------------------------------------------------------- 1 | const { ipcRenderer } = require('electron') 2 | 3 | const map = { } 4 | ipcRenderer.on('minecraft-server-data', (_, id, err, info) => { 5 | const obj = map[id] 6 | if (!obj) return 7 | if (err) obj[1](new Error(err)) 8 | else obj[0](info) 9 | }) 10 | window.queryMinecraftServer = (...args) => { 11 | const id = Date.now().toString(36) + Math.random().toString(36).slice(2) 12 | const p = new Promise((resolve, reject) => (map[id] = [resolve, reject])) 13 | ipcRenderer.sendToHost('query-minecraft-server', id, args) 14 | return p 15 | } 16 | 17 | window.getAccount = () => { 18 | ipcRenderer.sendToHost('get-account') 19 | return new Promise(resolve => ipcRenderer.once('account', (e, uuid, name, skinUrl, type) => 20 | resolve({ uuid, name, skinUrl, type }))) 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "skipLibCheck": true, 5 | "allowSyntheticDefaultImports": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "module": "esnext", 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "noImplicitAny": false, 11 | "experimentalDecorators": true, 12 | "strictNullChecks": false, 13 | "noEmit": true, 14 | "declaration": true, 15 | "jsx": "react", 16 | "lib": ["webworker", "scripthost", "dom", "es2019"] 17 | }, 18 | "include": [ 19 | "src" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /unpacked/createShortcut.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-var */ 2 | /* global WScript */ 3 | var args = WScript.Arguments 4 | var Shell = WScript.CreateObject('WScript.Shell') 5 | var link = Shell.CreateShortcut(Shell.SpecialFolders('Desktop') + '\\' + args(0) + '.lnk') 6 | link.TargetPath = args(1) 7 | link.Arguments = args(2).replace(/\*\?#!\*/g, '"') 8 | link.Description = args(3) 9 | link.IconLocation = args(4) 10 | link.Save() 11 | -------------------------------------------------------------------------------- /unpacked/mc-logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apisium/PureLauncher/a315aa959df55549f4e9f655fe298b2333761202/unpacked/mc-logo.ico --------------------------------------------------------------------------------