4
0
mirror of https://github.com/AltarikMC/Launcher synced 2024-11-24 06:39:50 +01:00

Move renderer to Vite + React

This commit is contained in:
Quentin Legot 2024-01-23 19:16:32 +01:00
parent c0f6ace012
commit b9f6dba214
48 changed files with 2530 additions and 1221 deletions

View File

@ -1,29 +1,9 @@
module.exports = { module.exports = {
env: {
browser: true,
es2021: true
},
extends: [ extends: [
'standard' 'eslint:recommended',
], 'plugin:react/recommended',
overrides: [ 'plugin:react/jsx-runtime',
{ '@electron-toolkit',
env: { '@electron-toolkit/eslint-config-prettier'
node: true ]
},
files: [
'.eslintrc.{js,cjs}'
],
parserOptions: {
sourceType: 'script'
}
}
],
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module'
},
plugins: [],
rules: {
}
} }

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ out/
.vscode/ .vscode/
*.code-workspace *.code-workspace
.idea .idea
dist

6
.prettierignore Normal file
View File

@ -0,0 +1,6 @@
out
dist
pnpm-lock.yaml
LICENSE.md
tsconfig.json
tsconfig.*.json

5
.prettierrc.yaml Normal file
View File

@ -0,0 +1,5 @@
singleQuote: true
semi: false
printWidth: 100
trailingComma: none
endOfLine: auto

View File

@ -1,49 +1,53 @@
const path = require('path'); const path = require('path')
const pkg = require('./package.json') const pkg = require('./package.json')
module.exports = { module.exports = {
packagerConfig: { packagerConfig: {
packageName: "altarik-launcher", packageName: 'altarik-launcher',
name: "Altarik Launcher", name: 'Altarik Launcher',
productName: "altarik-launcher", productName: 'altarik-launcher',
icon: path.resolve(__dirname, 'icon.ico'), icon: path.resolve(__dirname, 'icon.ico'),
asar: true, asar: true,
}, ignore: [
plugins: [ /^\/src/,
{ /(.eslintrc.json)|(.gitignore)|(electron.vite.config.ts)|(forge.config.cjs)|(tsconfig.*)/
name: '@electron-forge/plugin-auto-unpack-natives',
config: {}
}
],
makers: [
{
name: "@electron-forge/maker-squirrel",
platforms: ['darwin', 'win32'],
config: {
name: pkg.name,
iconUrl: path.resolve(__dirname, 'icon.ico'),
//loadingGif: path.resolve(__dirname, 'src/assets/loading.gif'),
setupIcon: path.resolve(__dirname, 'icon.ico'),
setupExe: `${pkg.name}-${pkg.version}-win32-x64.exe`
}
},
{
name: '@electron-forge/maker-zip',
platforms: ['linux']
}
],
publishers: [
{
name: '@electron-forge/publisher-github',
config: {
repository: {
owner: 'AltarikMC',
name: 'Launcher'
},
preRelease: false,
draft: true,
tagPrefix: ''
}
}
] ]
},
plugins: [
{
name: '@electron-forge/plugin-auto-unpack-natives',
config: {}
}
],
makers: [
{
name: '@electron-forge/maker-squirrel',
platforms: ['darwin', 'win32'],
config: {
name: pkg.name,
iconUrl: path.resolve(__dirname, 'icon.ico'),
// loadingGif: path.resolve(__dirname, 'src/assets/loading.gif'),
setupIcon: path.resolve(__dirname, 'icon.ico'),
setupExe: `${pkg.name}-${pkg.version}-win32-x64.exe`
}
},
{
name: '@electron-forge/maker-zip',
platforms: ['linux']
}
],
publishers: [
{
name: '@electron-forge/publisher-github',
config: {
repository: {
owner: 'AltarikMC',
name: 'Launcher'
},
preRelease: false,
draft: true,
tagPrefix: ''
}
}
]
} }

View File

@ -3,7 +3,7 @@
"author": "Altarik", "author": "Altarik",
"version": "2.1.4", "version": "2.1.4",
"description": "Altarik Launcher", "description": "Altarik Launcher",
"main": "src/server/main.js", "main": "./dist/main/index.js",
"type": "module", "type": "module",
"homepage": "https://altarik.fr/", "homepage": "https://altarik.fr/",
"private": true, "private": true,
@ -23,11 +23,11 @@
], ],
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"scripts": { "scripts": {
"start": "electron-forge start", "start": "electron-vite preview --outDir=dist",
"test": "electron-forge start --enable-logging --inspect-electron", "dev": "electron-vite dev --outDir=dist",
"package": "electron-forge package", "package": "electron-vite build --outDir=dist && electron-forge package",
"make": "electron-forge make", "make": "electron-vite build --outDir=dist && electron-forge make",
"publish": "electron-forge publish" "publish": "electron-vite build --outDir=dist && electron-forge publish"
}, },
"devDependencies": { "devDependencies": {
"@electron-forge/cli": "^7.1.0", "@electron-forge/cli": "^7.1.0",
@ -35,25 +35,38 @@
"@electron-forge/maker-zip": "^7.2.0", "@electron-forge/maker-zip": "^7.2.0",
"@electron-forge/plugin-auto-unpack-natives": "^7.2.0", "@electron-forge/plugin-auto-unpack-natives": "^7.2.0",
"@electron-forge/publisher-github": "^7.2.0", "@electron-forge/publisher-github": "^7.2.0",
"@electron-toolkit/eslint-config": "^1.0.2",
"@electron-toolkit/eslint-config-prettier": "^2.0.0",
"autoprefixer": "^10.4.17",
"cssnano": "^6.0.3",
"electron": "28.1.3", "electron": "28.1.3",
"eslint": "^8.56.0", "eslint": "^8.56.0",
"eslint-config-standard": "^17.1.0", "eslint-config-standard": "^17.1.0",
"eslint-plugin-import": "^2.29.1", "eslint-plugin-import": "^2.29.1",
"eslint-plugin-n": "^16.6.2", "eslint-plugin-n": "^16.6.2",
"eslint-plugin-promise": "^6.0.0" "eslint-plugin-promise": "^6.0.0",
"postcss": "^8.4.33",
"tailwindcss": "^3.4.1"
}, },
"dependencies": { "dependencies": {
"@electron-toolkit/preload": "^3.0.0",
"@electron-toolkit/utils": "^3.0.0",
"@vitejs/plugin-react": "^4.2.1",
"decompress": "^4.2.1", "decompress": "^4.2.1",
"electron-is-dev": "^3.0.1",
"electron-log": "^5.0.3", "electron-log": "^5.0.3",
"electron-squirrel-startup": "^1.0.0", "electron-squirrel-startup": "^1.0.0",
"electron-vite": "^2.0.0",
"eslint-plugin-react": "^7.33.2",
"extract-zip": "^2.0.1", "extract-zip": "^2.0.1",
"hasha": "^6.0.0", "hasha": "^6.0.0",
"izitoast": "^1.4.0", "izitoast": "^1.4.0",
"minecraft-launcher-core": "^3.17.3", "minecraft-launcher-core": "^3.17.3",
"msmc": "^4.1.0", "msmc": "^4.1.0",
"node-fetch": "^3.0.0", "node-fetch": "^3.0.0",
"vue": "^3.4.11" "prettier": "^3.2.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"vite": "^5.0.12"
}, },
"config": { "config": {
"forge": "./config.forge.cjs" "forge": "./config.forge.cjs"

7
postcss.config.js Normal file
View File

@ -0,0 +1,7 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
...(process.env.NODE_ENV === 'production' ? { cssnano: {} } : {})
}
}

View File

@ -1,32 +0,0 @@
const vue = require('vue/dist/vue.cjs.js')
app = vue.createApp({
data () {
return {
displayFullscreen: 'block',
fullscreenText: 'Recherche de mise à jour...',
downloadLink: null
}
},
mounted () {
this.sendCheckingUpdate()
},
methods: {
sendCheckingUpdate () {
ipcRenderer.send('checking-update')
},
openLinkExternal () {
shell.openExternal(this.downloadLink)
}
}
})
const root = app.mount('#vue')
ipcRenderer.on('update-available', (event, arg) => {
root.fullscreenText = 'Mise à jour disponible, téléchargement...'
})
ipcRenderer.on('please-download-update', (event, args) => {
root.fullscreenText = 'Veuillez télécharger la mise à jour en cliquant sur le lien suivant :'
root.downloadLink = `${args.url}`
})

View File

@ -1,177 +0,0 @@
const os = require('os')
const totalMem = os.totalmem() / (1.049 * Math.pow(10, 6))
const vue = require('vue/dist/vue.cjs.js')
app = vue.createApp({
data () {
return {
minMemValue: localStorage.getItem('minMem') != null ? localStorage.getItem('minMem') : 1024,
maxMemValue: localStorage.getItem('maxMem') != null ? localStorage.getItem('maxMem') : 2048,
memStep: 128,
memMax: totalMem,
invalidateButtonText: 'Supprimer et retélécharger les bibliothèques',
invalidateButtonDisabled: false,
displayFullscreen: 'none',
displaySettings: 'none',
displayCredits: 'none',
nick: 'Chargement',
launchBtnText: 'Selectionnez un chapitre',
launchBtnDisable: true,
launchBtnHidden: false,
loadingMessageHidden: true,
loadingMessageText: 'Téléchargement de Minecraft en cours...',
fullprogressbarHidden: true,
progressbarWidth: 0,
sidebarContent: '<hr><p>Chargement en cours</p>',
modsInformations: [],
modsInformationsLoaded: true,
selectedChapter: -1,
gameLaunching: false
}
},
mounted () {
iziToast.settings({
close: false,
closeOnClick: true,
timeout: 5000,
position: 'topRight',
resetOnHover: true
})
setInterval(() => {
ipcRenderer.send('pageReady')
}, 500)
},
methods: {
invalidateData () {
this.invalidateButtonDisabled = true
this.invalidateButtonText = 'Opération en cours'
this.showInfo('Opération en cours', 'Suppression des données du jeu en cours')
ipcRenderer.send('invalidateData')
},
launchBtnClick () {
this.launchBtnHidden = true
this.fullprogressbarHidden = false
this.loadingMessageHidden = false
if (Number(this.minMemValue) <= Number(this.maxMemValue)) {
ipcRenderer.send('launch', {
minMem: this.minMemValue + 'M',
maxMem: this.maxMemValue + 'M',
chapter: this.selectedChapter
})
this.launchBtnDisable = true
localStorage.setItem('minMem', this.minMemValue)
localStorage.setItem('maxMem', this.maxMemValue)
this.gameLaunching = true
} else {
this.showError('Erreur de lancement', 'La mémoire minimale doit être inférieure ou égale à la mémoire maximale.')
}
},
changeSelectedChapter (index) {
this.selectedChapter = parseInt(index)
root.launchBtnText = 'JOUER'
root.launchBtnDisable = false
},
disconnectBtn () {
ipcRenderer.send('disconnect')
},
options () {
if (!this.gameLaunching) {
this.displayFullscreen = 'block'
this.displaySettings = 'block'
this.displayCredits = 'none'
}
},
discord () {
shell.openExternal('https://discord.gg/p3EnE6Jumg')
},
web () {
shell.openExternal('https://altarik.fr')
},
closeFullscreen () {
this.displayFullscreen = 'none'
this.displaySettings = 'none'
this.displayCredits = 'none'
},
credits () {
this.displayFullscreen = 'block'
this.displaySettings = 'none'
this.displayCredits = 'block'
},
updateModsInformations (content) {
this.modsInformations = content
},
getModsInformations () {
return this.modsInformations
},
showInfo (title, body) {
iziToast.info({
title,
message: body,
color: 'blue'
})
},
showError (title, body) {
iziToast.error({
title,
message: body,
color: 'red'
})
},
showWarning (title, body) {
iziToast.warning({
title,
message: body,
color: 'yellow'
})
},
showSuccess (title, body) {
iziToast.success({
title,
message: body,
color: 'green'
})
},
isSelected (index) {
return this.selectedChapter === index
}
}
})
const root = app.mount('#vue')
ipcRenderer.on('invalidated', () => {
root.invalidateButtonDisabled = false
root.invalidateButtonText = 'Supprimer et retélécharger les bibliothèques'
root.showSuccess('Opération terminée', 'Les données du jeu ont été supprimé avec succès')
})
ipcRenderer.on('progress', (e, args) => {
root.progressbarWidth = (args.task / Math.max(args.total, args.task)) * 100
root.loadingMessageText = 'Téléchargement de ' + args.type + ': ' + args.task + ' sur ' + Math.max(args.total, args.task)
})
ipcRenderer.on('close', (_e, _args) => {
root.launchBtnHidden = false
root.fullprogressbarHidden = true
root.loadingMessageHidden = true
root.loadingMessageText = 'Chargement de Minecraft en cours...'
root.progressbarWidth = 0
root.launchBtnDisable = false
root.gameLaunching = false
})
ipcRenderer.on('launch', (_e, _args) => {
root.fullprogressbarHidden = true
root.loadingMessageHidden = true
})
ipcRenderer.on('modsInformations', (_e, args) => {
if (args === null) {
root.modsInformationsLoaded = false
} else {
root.modsInformationsLoaded = true
}
root.updateModsInformations(args)
})
ipcRenderer.on('nick', (_e, args) => root.nick = args.name)

View File

@ -1,87 +0,0 @@
const vue = require('vue/dist/vue.cjs.js')
app = vue.createApp({
data () {
return {
login: 'Connexion',
email: 'Email',
password: 'Mot de passe',
send_credentials: 'Se connecter',
microsoft_button: 'Connexion avec un compte Microsoft'
}
},
mounted () {
iziToast.settings({
close: false,
closeOnClick: true,
timeout: 5000,
position: 'topRight',
resetOnHover: true
})
},
methods: {
formSubmit (e) {
e.preventDefault()
if (!microsoftButton.disabled) {
form.disabled = true
if (user.value) {
ipcRenderer.send('login', {
user: user.value,
pass: password.value
})
} else {
this.showWarning('Erreur de connexion', 'Veuillez entrer des identifiants')
form.disabled = false
}
}
},
microsoftButton (e) {
e.preventDefault()
if (!form.disabled) {
microsoftButton.disabled = true
form.disabled = true
ipcRenderer.send('microsoft-login')
}
},
showInfo (title, body) {
iziToast.info({
title,
message: body,
color: 'blue'
})
},
showError (title, body) {
iziToast.error({
title,
message: body,
color: 'red'
})
},
showWarning (title, body) {
iziToast.warning({
title,
message: body,
color: 'yellow'
})
},
showSuccess (title, body) {
iziToast.success({
title,
message: body,
color: 'green'
})
}
}
})
app.mount('#vue')
// theirs const are declared after vue cause vue modify them when declaring new vue instance
const form = document.querySelector('#login-form')
const user = document.querySelector('#nickname')
const password = document.querySelector('#password')
const microsoftButton = document.querySelector('#microsoft-button')
ipcRenderer.on('loginError', () => {
form.disabled = false
microsoftButton.disabled = false
})

View File

@ -1,29 +0,0 @@
'use strict'
const { ipcRenderer, shell } = require('electron')
let app
window.addEventListener('DOMContentLoaded', () => {
const minimizeButton = document.getElementById('minimize-btn')
const closeButton = document.getElementById('close-btn')
minimizeButton.addEventListener('click', () => ipcRenderer.send('minimizeWindow'))
closeButton.addEventListener('click', () => ipcRenderer.send('closeWindow'))
})
ipcRenderer.on('notification', (_e, args) => {
app.notificationTitle = args.title
app.notificationMessage = args.body
switch (args.class) {
case 'success':
app._component.methods.showSuccess(args.title, args.body)
break
case 'warning':
app._component.methods.showWarning(args.title, args.body)
break
case 'error':
app._component.methods.showError(args.title, args.body)
break
case 'info':default:
app._component.methods.showInfo(args.title, args.body)
}
})

View File

@ -1,35 +0,0 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Altarik Launcher</title>
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline' 'unsafe-eval';" />
<link rel="stylesheet" href="../../node_modules/izitoast/dist/css/iziToast.min.css">
<link href="assets/css/fonts.css" rel="stylesheet" />
<link href="assets/css/checkingUpdate.css" rel="stylesheet" />
<link href="assets/css/menubar.css" rel="stylesheet" />
<!-- <link rel="shortcut icon" type="image/png" href="assets/images/icon.png"/> -->
</head>
<body>
<div id="vue">
<div id="menubar">
<ul class="left">
<img src="../../icon.ico">
</ul>
<ul class="right">
<!-- Mettre ce code en ligne pour éviter que chrome ne met un espace automatiquement entre les éléments -->
<li id="minimize-btn"><i class="material-icons">minimize</i></li><!--<li id="max-unmax-btn"><i class="material-icons">crop_square</i></li>--><li id="close-btn"><i class="material-icons">close</i></li>
</ul>
</div>
<div id="fullscreen" :style="{ display: displayFullscreen }">
<div id="fullscreen-content">
<p>{{ fullscreenText }} <br /><a @click="openLinkExternal" v-if="downloadLink !== null" href="#">{{ downloadLink }}</a></p>
<div class="dots-3"></div>
</div>
</div>
</div>
<script src="../../node_modules/izitoast/dist/js/iziToast.js" type="text/javascript"></script>
<script src="assets/js/preload.js"></script>
<script src="assets/js/checkingUpdate.js"></script>
</body>
</html>

View File

@ -1,112 +0,0 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Altarik Launcher</title>
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline' 'unsafe-eval';" />
<link rel="stylesheet" href="../../node_modules/izitoast/dist/css/iziToast.min.css">
<link href="assets/css/fonts.css" rel="stylesheet" />
<link href="assets/css/index.css" rel="stylesheet" />
<link href="assets/css/menubar.css" rel="stylesheet" />
<!--<link rel="shortcut icon" type="image/png" href="assets/images/icon.png"/> -->
</head>
<body>
<div id="vue">
<div id="menubar">
<ul class="left">
<img src="../../icon.ico">
</ul>
<ul class="right">
<!-- Mettre ce code en ligne pour éviter que chrome ne met un espace automatiquement entre les éléments -->
<li id="minimize-btn"><i class="material-icons">minimize</i></li><!--<li id="max-unmax-btn"><i class="material-icons">crop_square</i></li>--><li id="close-btn"><i class="material-icons">close</i></li>
</ul>
</div>
<div id="fullscreen" :style="{ display: displayFullscreen }">
<div @click="closeFullscreen" id="close"><i class="material-icons">close</i></div>
<div id="settings" :style="{ display: displaySettings }">
<h2>Paramètres</h2>
<span href="" id="disconnect-btn" @click="disconnectBtn">Se déconnecter</span>
<h4>Allocation mémoire</h4>
<label for="minMem">mémoire minimale : <span id="outputMinMem">{{ minMemValue }}</span></label><br />
<input type="number" min="1024" :max="memMax" :step="memStep" v-model="minMemValue" class="slider" id="minMem"><br />
<label for="maxMem">mémoire maximale : <span id="outputMaxMem">{{ maxMemValue }}</span></label><br />
<input type="number" min="1024" :max="memMax" :step="memStep" v-model="maxMemValue" class="slider" id="maxMem"><br />
<h4>Au secours, mon jeu ne démarre pas</h4>
<button @click="invalidateData" :disabled="invalidateButtonDisabled">{{ invalidateButtonText }}</button><br />
<span @click="credits">Voir crédits</span>
</div>
<div id="credits" :style="{ display: displayCredits }">
<div class="content">
<p>BSD 3-Clause License</p>
<p>Copyright (c) 2021, Altarik<br />
All rights reserved.</p>
<p>Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:</p>
<ol>
<li>Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.</li>
<li>Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.</li>
<li>Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.</li>
</ol>
<p>THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.</p>
</div>
</div>
</div>
<div id="content">
<div id="sidebar">
<h2>Chapitres</h2>
<div id="sidebar-content" @change="modsInformations">
<div v-if="modsInformationsLoaded === false">Une erreur est survenue lors de la récupération des informations, vérifiez votre connexion internet puis cliquez sur réessayez</div>
<div v-for="(item, index) in modsInformations" v-else-if="modsInformations.length !== 0" v-on:click="changeSelectedChapter(index)" :class="{ selected: isSelected(index) }">
<h3>{{ item.title }}</h3>
<p>{{ item.description}}</p>
</div>
<div v-else>Chargement en cours</div>
</div>
</div>
<div id="media">
<div @click="options" title="Paramètres">
<img src="assets/images/settings.png">
</div>
<div @click="discord" title="Rejoingnez notre Discord">
<img src="assets/images/discord.png">
</div>
<div @click="web"title="Visitez notre site web">
<img src="assets/images/web.png">
</div>
</div>
<div id="main">
<div id="account">
<div id="nick">{{ nick }}</div><!-- <img src=""> Head du joueur -->
</div>
<button @click="launchBtnClick" id="launch-btn" :disabled="launchBtnDisable">
<div id="launch-text" :class="[{hidden: launchBtnHidden}]">{{ launchBtnText }}</div>
<div id="loading-message" :class="[{hidden: loadingMessageHidden}]">{{ loadingMessageText }}</div>
<div id="fullprogressbar" :class="[{hidden: fullprogressbarHidden}]"><div id="progressbar" :style="{ width: progressbarWidth + '%' }"></div></div>
</button>
</div>
</div>
</div>
<script src="../../node_modules/izitoast/dist/js/iziToast.js" type="text/javascript"></script>
<script src="assets/js/preload.js"></script>
<script src="assets/js/index.js"></script>
</body>
</html>

View File

@ -1,44 +0,0 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Altarik Launcher</title>
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline' 'unsafe-eval';" />
<link rel="stylesheet" href="../../node_modules/izitoast/dist/css/iziToast.min.css">
<link href="assets/css/fonts.css" rel="stylesheet" />
<link href="assets/css/login.css" rel="stylesheet" />
<link href="assets/css/menubar.css" rel="stylesheet" />
<!-- <link rel="shortcut icon" type="image/png" href="assets/images/icon.png"/> -->
</head>
<body>
<div id="vue">
<div id="menubar">
<ul class="left">
<img src="../../icon.ico">
</ul>
<ul class="right">
<!-- Mettre ce code en ligne pour éviter que chrome ne met un espace automatiquement entre les éléments -->
<li id="minimize-btn"><i class="material-icons">minimize</i></li><!--<li id="max-unmax-btn"><i class="material-icons">crop_square</i></li>--><li id="close-btn"><i class="material-icons">close</i></li>
</ul>
</div>
<div id="content">
<div id="login">
<h3>{{ login }}</h3>
<hr>
<form v-on:submit="formSubmit" id="login-form">
<label for="nickname">{{ email }}:</label><br />
<input type="text" name="nickname" id="nickname"><br />
<label for="password">{{ password }}:</label><br />
<input type="password" name="password" id="password"><br />
<input type="submit" v-bind:value="send_credentials">
</form>
</div>
<button id="microsoft-button" v-on:click="microsoftButton">{{ microsoft_button }}</button>
</div>
</div>
<script src="../../node_modules/izitoast/dist/js/iziToast.js" type="text/javascript"></script>
<script src="assets/js/preload.js"></script>
<script src="assets/js/login.js"></script>
</body>
</html>

107
src/main/index.js Normal file
View File

@ -0,0 +1,107 @@
import { app, shell, BrowserWindow, autoUpdater, dialog, Menu, ipcMain } from 'electron'
import { dirname, join } from 'path'
import { optimizer, is } from '@electron-toolkit/utils'
import logger from 'electron-log'
import Updater from './updater.js'
import electronStartup from 'electron-squirrel-startup'
import install from './install.js'
import Mc from './minecraft.js'
import { minimizeWindow, closeWindow } from './menubar.js'
// import icon from '../../resources/icon.png?asset'
const updaterInstance = new Updater(app, autoUpdater, dialog, logger, showNotification)
updaterInstance.configUpdater()
const minecraft = new Mc()
minecraft.showNotification = showNotification
const __dirname = dirname(__filename)
const iconPath = join(__dirname, 'icon.ico')
let mainWindow
function createWindow() {
// Create the browser window.
mainWindow = new BrowserWindow({
width: 1000,
height: 600,
resizable: false,
autoHideMenuBar: true,
icon: iconPath,
// ...(process.platform === 'linux' ? { icon } : {}),
webPreferences: {
preload: join(__dirname, '../preload/index.mjs'),
sandbox: false,
nodeIntegration: true,
contextIsolation: false
},
frame: false
})
if (!is.dev) {
Menu.setApplicationMenu(null)
}
mainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url)
return { action: 'deny' }
})
mainWindow.on('close', () => {
app.quit()
})
if (is.dev && process.env.ELECTRON_RENDERER_URL) {
mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL)
} else {
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
}
}
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
function showNotification(title, body = '', clazz = 'info') {
mainWindow.webContents.send('notification', { title, body, class: clazz })
}
ipcMain.on('minimizeWindow', () => {
minimizeWindow(mainWindow)
})
ipcMain.on('closeWindow', () => {
closeWindow(mainWindow)
})
ipcMain.on('microsoft-login', (event) => {
minecraft.microsoftLogin(event, mainWindow)
})
ipcMain.on('invalidateData', (event) => {
minecraft.invalidateData(event)
})
ipcMain.on('launch', (event, args) => {
minecraft.launch(event, args)
})
function main() {
if (electronStartup) {
install(app)
app.quit()
return null
}
app.whenReady().then(() => {
app.on('browser-window-created', (_, window) => {
optimizer.watchWindowShortcuts(window)
})
mainWindow = createWindow()
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
}
main()

View File

@ -1,375 +1,375 @@
import isDev from 'electron-is-dev' import { is } from '@electron-toolkit/utils'
import mlc from 'minecraft-launcher-core' import mlc from 'minecraft-launcher-core'
import fetch from 'node-fetch' import fetch from 'node-fetch'
import { hashFile } from 'hasha' import { hashFile } from 'hasha'
import fs from 'fs' import fs from 'fs'
import { join } from 'path' import { join } from 'path'
import zip from 'extract-zip' import zip from 'extract-zip'
import logger from 'electron-log' import logger from 'electron-log'
import { Auth, lst } from 'msmc' import { Auth, lst } from 'msmc'
import decompress from 'decompress' import decompress from 'decompress'
import decompressTar from 'decompress-targz' import decompressTar from 'decompress-targz'
const { Authenticator, Client } = mlc const { Authenticator, Client } = mlc
export default class Minecraft { export default class Minecraft {
appdata = process.env.APPDATA || (process.platform === 'darwin' ? process.env.HOME + '/Library/Preferences' : process.env.HOME + '/.local/share') appdata = process.env.APPDATA || (process.platform === 'darwin' ? process.env.HOME + '/Library/Preferences' : process.env.HOME + '/.local/share')
localappdata = process.env.LOCALAPPDATA || (process.platform === 'darwin' ? process.env.HOME + '/Library/Application Support/' : process.env.HOME + '/.config') localappdata = process.env.LOCALAPPDATA || (process.platform === 'darwin' ? process.env.HOME + '/Library/Application Support/' : process.env.HOME + '/.config')
minecraftpath = join(this.appdata, '.altarik') minecraftpath = join(this.appdata, '.altarik')
launcher = new Client() launcher = new Client()
auth = null auth = null
modsList = undefined modsList = undefined
showNotification = undefined showNotification = undefined
modsInformationsEndpoint = 'https://launcher.altarik.fr' modsInformationsEndpoint = 'https://launcher.altarik.fr'
setShowNotification (showNotification) { setShowNotification (showNotification) {
this.showNotification = showNotification this.showNotification = showNotification
} }
/** /**
* @deprecated Mojang removed this method of authentification * @deprecated Mojang removed this method of authentification
* Used to login through Mojang account * Used to login through Mojang account
*/ */
login (event, win, username, password) { login (event, win, username, password) {
this.auth = null this.auth = null
if (isDev || password.trim() !== '') { if (is.dev || password.trim() !== '') {
this.auth = Authenticator.getAuth(username, password) this.auth = Authenticator.getAuth(username, password)
this.auth.then(v => { this.auth.then(v => {
win.loadFile('src/client/index.html') win.loadFile('src/client/index.html')
}).catch(() => { }).catch(() => {
event.sender.send('loginError') event.sender.send('loginError')
logger.error("[MJ login] User haven't purchase the game") logger.error("[MJ login] User haven't purchase the game")
this.showNotification('Erreur de connexion', 'Vous ne possèdez pas de licence Minecraft sur ce compte', 'error') this.showNotification('Erreur de connexion', 'Vous ne possèdez pas de licence Minecraft sur ce compte', 'error')
}) })
} else { } else {
this.showNotification('Erreur de connexion', 'Veuillez renseignez un mot de passe', 'warning') this.showNotification('Erreur de connexion', 'Veuillez renseignez un mot de passe', 'warning')
} }
} }
/** /**
* Used to login through a Microsoft account * Used to login through a Microsoft account
*/ */
microsoftLogin (event, win) { microsoftLogin (event, win) {
const authManager = new Auth('select_account') const authManager = new Auth('select_account')
authManager.launch('electron').then(async xboxManager => { authManager.launch('electron').then(async xboxManager => {
xboxManager.getMinecraft().then(async token => { xboxManager.getMinecraft().then(async token => {
if (!token.isDemo()) { if (!token.isDemo()) {
this.auth = token.mclc() this.auth = token.mclc()
logger.info('[MS login] User has been connected successfully to them account') logger.info('[MS login] User has been connected successfully to them account')
win.loadFile('src/client/index.html') win.loadFile('src/client/index.html')
} else { } else {
event.sender.send('loginError') event.sender.send('loginError')
logger.error("[MS login] User haven't purchase the game") logger.error("[MS login] User haven't purchase the game")
this.showNotification('Erreur de connexion', 'Vous ne possèdez pas de licence Minecraft sur ce compte', 'error') this.showNotification('Erreur de connexion', 'Vous ne possèdez pas de licence Minecraft sur ce compte', 'error')
} }
}).catch(err => { }).catch(err => {
event.sender.send('loginError') event.sender.send('loginError')
this.showNotification('Erreur de connexion à Mojang', err, 'error') this.showNotification('Erreur de connexion à Mojang', err, 'error')
logger.error('[MS login] ' + lst(err)) logger.error('[MS login] ' + lst(err))
}) })
}).catch(err => { }).catch(err => {
event.sender.send('loginError') event.sender.send('loginError')
if (err !== 'error.gui.closed') { if (err !== 'error.gui.closed') {
this.showNotification('Une erreur de connexion à Xbox est survenue', err, 'error') this.showNotification('Une erreur de connexion à Xbox est survenue', err, 'error')
logger.error('[MS login] ' + lst(err)) logger.error('[MS login] ' + lst(err))
} }
}) })
} }
launch (event, args) { launch (event, args) {
this.extractJava(Number(args.chapter), event).then((javaPath) => { this.extractJava(Number(args.chapter), event).then((javaPath) => {
this.extractMods(Number(args.chapter), event).then((chapter) => { this.extractMods(Number(args.chapter), event).then((chapter) => {
this.launcher.launch({ this.launcher.launch({
authorization: this.auth, authorization: this.auth,
root: this.minecraftpath, root: this.minecraftpath,
javaPath, javaPath,
version: { version: {
number: chapter.minecraftVersion, number: chapter.minecraftVersion,
type: chapter.type | 'release', type: chapter.type | 'release',
custom: chapter.customVersion custom: chapter.customVersion
}, },
memory: { memory: {
max: args.maxMem, max: args.maxMem,
min: args.minMem min: args.minMem
} }
}).then(v => { }).then(v => {
if (v === null) { if (v === null) {
this.close(event, -1) this.close(event, -1)
} }
}) })
this.launcher.on('debug', (e) => logger.info(`debug: ${e}`)) this.launcher.on('debug', (e) => logger.info(`debug: ${e}`))
this.launcher.on('data', (e) => logger.info(`data: ${e}`)) this.launcher.on('data', (e) => logger.info(`data: ${e}`))
this.launcher.on('progress', (e) => { this.launcher.on('progress', (e) => {
event.sender.send('progress', e) event.sender.send('progress', e)
logger.info(`progress ${e.type} :${e.task} / ${e.total}`) logger.info(`progress ${e.type} :${e.task} / ${e.total}`)
}) })
this.launcher.on('arguments', (e) => { this.launcher.on('arguments', (e) => {
event.sender.send('launch', e) event.sender.send('launch', e)
logger.info('launching the game') logger.info('launching the game')
logger.info(e) logger.info(e)
}) })
this.launcher.on('close', (e) => { this.launcher.on('close', (e) => {
this.close(event, e) this.close(event, e)
}) })
}).catch((err) => { }).catch((err) => {
this.showNotification('Impossible de lancer le jeu', 'Erreur inconnue', 'error') this.showNotification('Impossible de lancer le jeu', 'Erreur inconnue', 'error')
event.sender.send('close', 1) event.sender.send('close', 1)
logger.error('Unable to launch the game') logger.error('Unable to launch the game')
logger.error(err) logger.error(err)
}) })
}).catch(err => { }).catch(err => {
this.showNotification('Impossible de lancer le jeu', "Impossible d'installer Java pour votre configuration", 'error') this.showNotification('Impossible de lancer le jeu', "Impossible d'installer Java pour votre configuration", 'error')
event.sender.send('close', 1) event.sender.send('close', 1)
logger.warn('Unable to install java') logger.warn('Unable to install java')
logger.warn(err) logger.warn(err)
}) })
} }
close (event, code) { close (event, code) {
event.sender.send('close', code) event.sender.send('close', code)
if (code !== 0) { if (code !== 0) {
logger.warn("Minecraft didn't close properly") logger.warn("Minecraft didn't close properly")
logger.warn(code) logger.warn(code)
this.showNotification('Une erreur est survenue', "Minecraft ne s'est pas fermé correctement", 'error') this.showNotification('Une erreur est survenue', "Minecraft ne s'est pas fermé correctement", 'error')
} }
} }
getModsInformations (event) { getModsInformations (event) {
fetch(this.modsInformationsEndpoint).then(response => { fetch(this.modsInformationsEndpoint).then(response => {
if (response.ok) { if (response.ok) {
response.json().then(data => { response.json().then(data => {
const folder = join(this.localappdata, 'altarik-launcher', 'data') const folder = join(this.localappdata, 'altarik-launcher', 'data')
if (!fs.existsSync(folder)) { fs.mkdirSync(folder, { recursive: true }) } if (!fs.existsSync(folder)) { fs.mkdirSync(folder, { recursive: true }) }
const file = join(folder, 'launcher.json') const file = join(folder, 'launcher.json')
if (fs.existsSync(file)) { fs.rmSync(file) } if (fs.existsSync(file)) { fs.rmSync(file) }
fs.writeFileSync(file, JSON.stringify(data)) fs.writeFileSync(file, JSON.stringify(data))
event.sender.send('modsInformations', this.extractModsInformations(data)) event.sender.send('modsInformations', this.extractModsInformations(data))
}).catch(err => { }).catch(err => {
event.sender.send('modsInformations', this.extractModsFromFileSystem()) event.sender.send('modsInformations', this.extractModsFromFileSystem())
logger.warn(err) logger.warn(err)
logger.warn('An error occured while trying to connect to server') logger.warn('An error occured while trying to connect to server')
}) })
} else { } else {
logger.warn('Unable to connect to server') logger.warn('Unable to connect to server')
logger.warn(response.status) logger.warn(response.status)
event.sender.send('modsInformations', this.extractModsFromFileSystem()) event.sender.send('modsInformations', this.extractModsFromFileSystem())
} }
}).catch(err => { }).catch(err => {
logger.warn('Unable to connect to server') logger.warn('Unable to connect to server')
logger.warn(err) logger.warn(err)
event.sender.send('modsInformations', this.extractModsFromFileSystem()) event.sender.send('modsInformations', this.extractModsFromFileSystem())
}) })
} }
extractModsFromFileSystem () { extractModsFromFileSystem () {
const filepath = join(this.localappdata, 'altarik-launcher/data/launcher.json') const filepath = join(this.localappdata, 'altarik-launcher/data/launcher.json')
if (fs.existsSync(filepath)) { if (fs.existsSync(filepath)) {
const content = fs.readFileSync(filepath) const content = fs.readFileSync(filepath)
if (content !== null) { if (content !== null) {
this.showNotification('Impossible de récupérer certaines informations en ligne', 'utilisation des dernières données récupérées', 'warning') this.showNotification('Impossible de récupérer certaines informations en ligne', 'utilisation des dernières données récupérées', 'warning')
return this.extractModsInformations(JSON.parse(content)) return this.extractModsInformations(JSON.parse(content))
} else { } else {
this.showNotification('Impossible de récupérer certaines informations en ligne', 'Veuillez réessayez en cliquant sur le bouton', 'warning') this.showNotification('Impossible de récupérer certaines informations en ligne', 'Veuillez réessayez en cliquant sur le bouton', 'warning')
logger.error('Unable to get chapters informations from server or filesystem') logger.error('Unable to get chapters informations from server or filesystem')
return null return null
} }
} else { } else {
return null return null
} }
} }
extractModsInformations (json) { extractModsInformations (json) {
this.modsList = json.chapters this.modsList = json.chapters
return this.modsList return this.modsList
} }
async extractMods (chapterId, event) { async extractMods (chapterId, event) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const modsFolder = join(this.minecraftpath, 'mods') const modsFolder = join(this.minecraftpath, 'mods')
// const shaderFolder = join(this.minecraftpath, 'shaderpacks') // const shaderFolder = join(this.minecraftpath, 'shaderpacks')
if (fs.existsSync(modsFolder)) { fs.rmSync(modsFolder, { recursive: true }) } if (fs.existsSync(modsFolder)) { fs.rmSync(modsFolder, { recursive: true }) }
// if (fs.existsSync(shaderFolder)) { fs.rmSync(shaderFolder, { recursive: true }) } // if (fs.existsSync(shaderFolder)) { fs.rmSync(shaderFolder, { recursive: true }) }
for (const i in this.modsList) { for (const i in this.modsList) {
if (Number(i) === chapterId) { if (Number(i) === chapterId) {
const chapter = this.modsList[i] const chapter = this.modsList[i]
for (const j in chapter.modspack.mods) { for (const j in chapter.modspack.mods) {
event.sender.send('progress', { type: 'mods', task: 0, total: chapter.modspack.mods.length }) event.sender.send('progress', { type: 'mods', task: 0, total: chapter.modspack.mods.length })
const modpackFolder = join(this.minecraftpath, 'modpack', chapter.title) const modpackFolder = join(this.minecraftpath, 'modpack', chapter.title)
if (!fs.existsSync(modpackFolder)) { fs.mkdirSync(modpackFolder, { recursive: true }) } if (!fs.existsSync(modpackFolder)) { fs.mkdirSync(modpackFolder, { recursive: true }) }
const path = join(modpackFolder, `modpack${j}.zip`) const path = join(modpackFolder, `modpack${j}.zip`)
try { try {
fs.accessSync(path, fs.W_OK) fs.accessSync(path, fs.W_OK)
hashFile(path, { algorithm: 'sha1' }).then(sha1 => { hashFile(path, { algorithm: 'sha1' }).then(sha1 => {
if (sha1 === chapter.modspack.sha1sum[j]) { if (sha1 === chapter.modspack.sha1sum[j]) {
this.unzipMods(path).catch(err => { this.unzipMods(path).catch(err => {
reject(err) reject(err)
}) })
} else { } else {
logger.warn(`sha1sum ${sha1} don't correspond to ${chapter.modspack.sha1sum[j]} of mods ${path}`) logger.warn(`sha1sum ${sha1} don't correspond to ${chapter.modspack.sha1sum[j]} of mods ${path}`)
this.downloadAndExtractMods(chapter.modspack.mods[j], path).catch(err => { this.downloadAndExtractMods(chapter.modspack.mods[j], path).catch(err => {
reject(err) reject(err)
}) })
} }
}).catch(err => { }).catch(err => {
reject(new Error('Can obtain md5 hash of file ' + path + ': ' + err)) reject(new Error('Can obtain md5 hash of file ' + path + ': ' + err))
}) })
event.sender.send('progress', { type: 'mods', task: Number(j) + 1, total: chapter.modspack.mods.length }) event.sender.send('progress', { type: 'mods', task: Number(j) + 1, total: chapter.modspack.mods.length })
} catch (err) { } catch (err) {
this.downloadAndExtractMods(chapter.modspack.mods[j], path).catch(err => { this.downloadAndExtractMods(chapter.modspack.mods[j], path).catch(err => {
reject(err) reject(err)
}) })
} }
} }
resolve(chapter) resolve(chapter)
return return
} }
} }
reject(new Error("didn't found the correct chapter" + chapterId)) reject(new Error("didn't found the correct chapter" + chapterId))
}) })
} }
downloadMods (link, path) { downloadMods (link, path) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fetch(link).then(response => { fetch(link).then(response => {
if (response.ok) { if (response.ok) {
if (fs.existsSync(path)) { fs.rmSync(path) } if (fs.existsSync(path)) { fs.rmSync(path) }
const dest = fs.createWriteStream(path) const dest = fs.createWriteStream(path)
response.body.pipe(dest) response.body.pipe(dest)
response.body.on('end', () => { response.body.on('end', () => {
logger.log('download completed') logger.log('download completed')
resolve('download completed') resolve('download completed')
}) })
dest.on('error', () => { dest.on('error', () => {
reject(new Error('An error appenned when using stream')) reject(new Error('An error appenned when using stream'))
}) })
} else { } else {
reject(response.status) reject(response.status)
} }
}).catch(err => { }).catch(err => {
reject(err) reject(err)
}) })
}) })
} }
async unzipMods (zipLocation, outLocation = this.minecraftpath) { async unzipMods (zipLocation, outLocation = this.minecraftpath) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
logger.info(`unzipping ${zipLocation} file to ${outLocation}`) logger.info(`unzipping ${zipLocation} file to ${outLocation}`)
zip(zipLocation, { dir: outLocation }).then(() => { zip(zipLocation, { dir: outLocation }).then(() => {
resolve() resolve()
}).catch(err => { }).catch(err => {
logger.error('failed to unzip file') logger.error('failed to unzip file')
reject(err) reject(err)
}) })
}) })
} }
async extractTar (tarLocation, outLocation = this.microsoftpath) { async extractTar (tarLocation, outLocation = this.microsoftpath) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
logger.info(`Extracting targz ${tarLocation} file to ${outLocation}`) logger.info(`Extracting targz ${tarLocation} file to ${outLocation}`)
decompress(tarLocation, outLocation, { decompress(tarLocation, outLocation, {
plugins: [ plugins: [
decompressTar() decompressTar()
] ]
}).then(() => { }).then(() => {
resolve() resolve()
}).catch((e) => { }).catch((e) => {
logger.error('Failed to extract targz file') logger.error('Failed to extract targz file')
reject(e) reject(e)
}) })
}) })
} }
async downloadAndExtractMods (link, path) { async downloadAndExtractMods (link, path) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.downloadMods(link, path).then(() => { this.downloadMods(link, path).then(() => {
this.unzipMods(path).then(() => { this.unzipMods(path).then(() => {
resolve() resolve()
}).catch(err => { }).catch(err => {
reject(err) reject(err)
}) })
}).catch(err => { }).catch(err => {
reject(err) reject(err)
}) })
}) })
} }
async extractJava (chapterId, event) { async extractJava (chapterId, event) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const runtime = join(this.minecraftpath, 'runtime') const runtime = join(this.minecraftpath, 'runtime')
if (this.modsList[chapterId].java.platform[process.platform] !== undefined && if (this.modsList[chapterId].java.platform[process.platform] !== undefined &&
this.modsList[chapterId].java.platform[process.platform][process.arch] !== undefined) { this.modsList[chapterId].java.platform[process.platform][process.arch] !== undefined) {
event.sender.send('progress', { type: 'java', task: 0, total: 1 }) event.sender.send('progress', { type: 'java', task: 0, total: 1 })
const infos = this.modsList[chapterId].java.platform[process.platform][process.arch] const infos = this.modsList[chapterId].java.platform[process.platform][process.arch]
const jre = join(runtime, infos.name) const jre = join(runtime, infos.name)
const downloadFolder = join(runtime, 'download') const downloadFolder = join(runtime, 'download')
const downloadFile = join(downloadFolder, `${infos.name}.zip`) const downloadFile = join(downloadFolder, `${infos.name}.zip`)
if (fs.existsSync(jre)) { fs.rmSync(jre, { recursive: true }) } if (fs.existsSync(jre)) { fs.rmSync(jre, { recursive: true }) }
if (!fs.existsSync(downloadFolder)) { fs.mkdirSync(downloadFolder, { recursive: true }) } if (!fs.existsSync(downloadFolder)) { fs.mkdirSync(downloadFolder, { recursive: true }) }
if (fs.existsSync(downloadFile)) { if (fs.existsSync(downloadFile)) {
hashFile(downloadFile, { algorithm: 'sha256' }).then(sha1 => { hashFile(downloadFile, { algorithm: 'sha256' }).then(sha1 => {
if (sha1 === infos.sha256sum) { if (sha1 === infos.sha256sum) {
this.extractJavaArchive(downloadFile, runtime).then(() => { this.extractJavaArchive(downloadFile, runtime).then(() => {
const filename = process.platform === 'win32' ? 'java.exe' : 'java' const filename = process.platform === 'win32' ? 'java.exe' : 'java'
resolve(join(jre, 'bin', filename)) resolve(join(jre, 'bin', filename))
}).catch(err => { }).catch(err => {
reject(err) reject(err)
}) })
} else { } else {
logger.warn(`java sha256sum ${sha1} don't correspond to ${infos.sha256sum}`) logger.warn(`java sha256sum ${sha1} don't correspond to ${infos.sha256sum}`)
this.downloadAndExtractJava(infos, downloadFolder, runtime).then(() => resolve(join(jre, 'bin', process.platform === 'win32' ? 'java.exe' : 'java'))).catch(err => reject(err)) this.downloadAndExtractJava(infos, downloadFolder, runtime).then(() => resolve(join(jre, 'bin', process.platform === 'win32' ? 'java.exe' : 'java'))).catch(err => reject(err))
} }
}).catch(err => { }).catch(err => {
reject(err) reject(err)
}) })
} else { } else {
this.downloadAndExtractJava(infos, downloadFolder, runtime).then(() => resolve(join(jre, 'bin', process.platform === 'win32' ? 'java.exe' : 'java'))).catch(err => reject(err)) this.downloadAndExtractJava(infos, downloadFolder, runtime).then(() => resolve(join(jre, 'bin', process.platform === 'win32' ? 'java.exe' : 'java'))).catch(err => reject(err))
} }
event.sender.send('progress', { type: 'java', task: 1, total: 1 }) event.sender.send('progress', { type: 'java', task: 1, total: 1 })
} else { } else {
reject(new Error('There is not available version for this system')) reject(new Error('There is not available version for this system'))
} }
}) })
} }
async downloadAndExtractJava (infos, downloadFolder, runtimeFolder) { async downloadAndExtractJava (infos, downloadFolder, runtimeFolder) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
logger.info(`Downloading ${infos.name}`) logger.info(`Downloading ${infos.name}`)
this.downloadMods(infos.link, join(downloadFolder, `${infos.name}.zip`)).then(() => { this.downloadMods(infos.link, join(downloadFolder, `${infos.name}.zip`)).then(() => {
logger.info('download completed') logger.info('download completed')
this.extractJavaArchive(join(downloadFolder, `${infos.name}.zip`), runtimeFolder).then(() => { this.extractJavaArchive(join(downloadFolder, `${infos.name}.zip`), runtimeFolder).then(() => {
logger.info('File unzipped') logger.info('File unzipped')
resolve() resolve()
}).catch(err => { }).catch(err => {
const joinS = join(downloadFolder, `${infos.name}.zip`) const joinS = join(downloadFolder, `${infos.name}.zip`)
logger.info(`Failed to unzip ${joinS}`) logger.info(`Failed to unzip ${joinS}`)
reject(err) reject(err)
}) })
}).catch(err => { }).catch(err => {
logger.err(`Failed to download ${infos.link} to ${infos.name}.zip`) logger.err(`Failed to download ${infos.link} to ${infos.name}.zip`)
reject(err) reject(err)
}) })
}) })
} }
async extractJavaArchive (zipLocation, outLocation) { async extractJavaArchive (zipLocation, outLocation) {
if (process.platform === 'win32') { if (process.platform === 'win32') {
await this.unzipMods(zipLocation, outLocation) await this.unzipMods(zipLocation, outLocation)
} else { } else {
await this.extractTar(zipLocation, outLocation) await this.extractTar(zipLocation, outLocation)
} }
} }
invalidateData (event) { invalidateData (event) {
logger.info('invalidate game data...') logger.info('invalidate game data...')
const assets = join(this.minecraftpath, 'assets') const assets = join(this.minecraftpath, 'assets')
const librairies = join(this.minecraftpath, 'libraries') const librairies = join(this.minecraftpath, 'libraries')
const natives = join(this.minecraftpath, 'natives') const natives = join(this.minecraftpath, 'natives')
const versions = join(this.minecraftpath, 'versions') const versions = join(this.minecraftpath, 'versions')
if (fs.existsSync(assets)) { fs.rmSync(assets, { recursive: true }) } if (fs.existsSync(assets)) { fs.rmSync(assets, { recursive: true }) }
if (fs.existsSync(librairies)) { fs.rmSync(librairies, { recursive: true }) } if (fs.existsSync(librairies)) { fs.rmSync(librairies, { recursive: true }) }
if (fs.existsSync(natives)) { fs.rmSync(natives, { recursive: true }) } if (fs.existsSync(natives)) { fs.rmSync(natives, { recursive: true }) }
if (fs.existsSync(versions)) { fs.rmSync(versions, { recursive: true }) } if (fs.existsSync(versions)) { fs.rmSync(versions, { recursive: true }) }
logger.info('Game data invalidated') logger.info('Game data invalidated')
event.sender.send('invalidated') event.sender.send('invalidated')
} }
} }

View File

@ -1,4 +1,4 @@
import isDev from 'electron-is-dev' import { is } from '@electron-toolkit/utils'
import fetch from 'node-fetch' import fetch from 'node-fetch'
import pkg from '../../package.json' assert {type: 'json'} import pkg from '../../package.json' assert {type: 'json'}
@ -19,7 +19,7 @@ export default class Updater {
this.logger.info(`Node version: ${process.versions.node}`) this.logger.info(`Node version: ${process.versions.node}`)
this.logger.info(`platform: ${process.platform}`) this.logger.info(`platform: ${process.platform}`)
this.logger.info(`arch: ${process.arch}`) this.logger.info(`arch: ${process.arch}`)
if (isDev) { if (is.dev) {
this.logger.info(`developpement version ${this.app.getVersion()}`) this.logger.info(`developpement version ${this.app.getVersion()}`)
return return
} }
@ -33,7 +33,7 @@ export default class Updater {
} }
checkForUpdates (win, showNotification) { checkForUpdates (win, showNotification) {
if (isDev) { if (is.dev) {
win.loadFile('src/client/login.html') win.loadFile('src/client/login.html')
return return
} }

20
src/preload/index.js Normal file
View File

@ -0,0 +1,20 @@
import { contextBridge } from 'electron'
import { electronAPI } from '@electron-toolkit/preload'
// Custom APIs for renderer
const api = {}
// Use `contextBridge` APIs to expose Electron APIs to
// renderer only if context isolation is enabled, otherwise
// just add to the DOM global.
if (process.contextIsolated) {
try {
contextBridge.exposeInMainWorld('electron', electronAPI)
contextBridge.exposeInMainWorld('api', api)
} catch (error) {
console.error(error)
}
} else {
window.electron = electronAPI
window.api = api
}

17
src/renderer/index.html Normal file
View File

@ -0,0 +1,17 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>Electron</title>
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"
/>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

17
src/renderer/src/App.jsx Normal file
View File

@ -0,0 +1,17 @@
/* eslint-disable no-unused-vars */
import React, { useState } from 'react'
import CheckingUpdate from './components/CheckingUpdate'
import Login from './components/Login'
import Main from './components/Main'
import Menubar from './components/Menubar'
export default function App() {
const [checkingUpdate, setCheckingUpdate] = useState(true)
const [login, setLogin] = useState(false)
return (
<>
<Menubar />
{checkingUpdate ? <CheckingUpdate /> : login ? <Login /> : <Main />}
</>
)
}

View File

@ -32,3 +32,8 @@
font-family: "French-Press"; font-family: "French-Press";
src: url("../fonts/Frenchpress.otf") format("OpenType"); src: url("../fonts/Frenchpress.otf") format("OpenType");
} }
.material-icons{
position: relative;
top: 2px;
}

View File

Before

Width:  |  Height:  |  Size: 178 KiB

After

Width:  |  Height:  |  Size: 178 KiB

View File

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 142 KiB

View File

Before

Width:  |  Height:  |  Size: 375 KiB

After

Width:  |  Height:  |  Size: 375 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 83 KiB

View File

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 111 KiB

View File

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@ -0,0 +1,6 @@
/* eslint-disable no-unused-vars */
import React, { useState } from 'react'
export default function CheckingUpdate() {
return <>Bonjour</>
}

View File

@ -0,0 +1,6 @@
/* eslint-disable no-unused-vars */
import React from "react"
export default function Fullscreen() {
return <></>
}

View File

@ -0,0 +1,6 @@
/* eslint-disable no-unused-vars */
import React from 'react'
export default function Login() {
return <></>
}

View File

@ -0,0 +1,6 @@
/* eslint-disable no-unused-vars */
import React from 'react'
export default function Main() {
return <></>
}

View File

@ -0,0 +1,24 @@
/* eslint-disable no-unused-vars */
import React from 'react'
import icon from '../../../../icon.ico'
export default function Menubar() {
return (
<div
className="fixed top-0 left-0 w-full h-7 bg-slate-900 text-white cursor-move select-none z-50 *:m-0 *:p-0"
style={{ 'webkit-app-region': 'drag' }}
>
<ul className="float-left">
<img src={icon} className="pl-4 h-7 w-auto" />
</ul>
<ul className="float-right *:inline-block *:h-7 *:hover:cursor-pointer *:hover:transition-all *:ease-in *:duration-300">
<li className="hover:bg-slate-800">
<i className="material-icons">minimize</i>
</li>
<li className="hover:bg-red-600">
<i className="material-icons">close</i>
</li>
</ul>
</div>
)
}

12
src/renderer/src/main.jsx Normal file
View File

@ -0,0 +1,12 @@
/* eslint-disable no-unused-vars */
import React from 'react'
import ReactDOM from 'react-dom/client'
import './assets/index.css'
import './assets/css/fonts.css'
import App from './App'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>
)

View File

@ -1,112 +0,0 @@
import { app, BrowserWindow, Menu, ipcMain, autoUpdater, dialog } from 'electron'
import isDev from 'electron-is-dev'
import logger from 'electron-log'
import { join, dirname } from 'path'
import Updater from './updater.js'
import electronStartup from 'electron-squirrel-startup'
import install from './install.js'
import Mc from './minecraft.js'
import { minimizeWindow, closeWindow } from './menubar.js'
import { fileURLToPath } from 'url'
const updaterInstance = new Updater(app, autoUpdater, dialog, logger, showNotification)
updaterInstance.configUpdater()
const minecraft = new Mc()
minecraft.showNotification = showNotification
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const iconPath = join(__dirname, 'icon.ico')
let win = null
function createWindow () {
win = new BrowserWindow({
width: 1000,
height: 600,
resizable: false,
icon: iconPath,
webPreferences: {
nodeIntegration: true,
contextIsolation: false
},
frame: false
})
if (!isDev) {
Menu.setApplicationMenu(null)
}
win.loadFile('src/client/checkingUpdate.html')
win.on('close', () => {
app.quit()
})
}
function showNotification (title, body = '', clazz = 'info') {
win.webContents.send('notification', { title, body, class: clazz })
}
ipcMain.on('disconnect', () => {
minecraft.auth = null
win.loadFile('src/client/login.html').then(() => showNotification('Déconnecté', 'Vous avez été déconnecté de votre compte', 'success'))
})
ipcMain.on('pageReady', (event) => {
event.sender.send('nick', { name: minecraft.auth.name })
minecraft.getModsInformations(event)
})
ipcMain.on('checking-update', () => {
updaterInstance.checkForUpdates(win, showNotification)
})
function main () {
if (electronStartup) {
install(app)
app.quit()
return
}
app.whenReady().then(() => {
createWindow()
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
ipcMain.on('minimizeWindow', () => {
minimizeWindow(win)
})
ipcMain.on('closeWindow', () => {
closeWindow(win)
})
app.on('activate', () => {
if (win === null) {
createWindow()
}
})
ipcMain.on('login', (event, args) => {
minecraft.login(event, win, args.user, args.pass)
})
ipcMain.on('microsoft-login', (event) => {
minecraft.microsoftLogin(event, win)
})
ipcMain.on('invalidateData', event => {
minecraft.invalidateData(event)
})
ipcMain.on('launch', (event, args) => {
minecraft.launch(event, args)
})
}
main()

9
tailwind.config.js Normal file
View File

@ -0,0 +1,9 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {}
},
plugins: [],
darkMode: 'class'
}

1955
yarn.lock

File diff suppressed because it is too large Load Diff