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 = {
env: {
browser: true,
es2021: true
},
extends: [
'standard'
],
overrides: [
{
env: {
node: true
},
files: [
'.eslintrc.{js,cjs}'
],
parserOptions: {
sourceType: 'script'
}
}
],
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module'
},
plugins: [],
rules: {
}
'eslint:recommended',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'@electron-toolkit',
'@electron-toolkit/eslint-config-prettier'
]
}

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ out/
.vscode/
*.code-workspace
.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')
module.exports = {
packagerConfig: {
packageName: "altarik-launcher",
name: "Altarik Launcher",
productName: "altarik-launcher",
icon: path.resolve(__dirname, 'icon.ico'),
asar: true,
},
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: ''
}
}
packagerConfig: {
packageName: 'altarik-launcher',
name: 'Altarik Launcher',
productName: 'altarik-launcher',
icon: path.resolve(__dirname, 'icon.ico'),
asar: true,
ignore: [
/^\/src/,
/(.eslintrc.json)|(.gitignore)|(electron.vite.config.ts)|(forge.config.cjs)|(tsconfig.*)/
]
},
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",
"version": "2.1.4",
"description": "Altarik Launcher",
"main": "src/server/main.js",
"main": "./dist/main/index.js",
"type": "module",
"homepage": "https://altarik.fr/",
"private": true,
@ -23,11 +23,11 @@
],
"license": "BSD-3-Clause",
"scripts": {
"start": "electron-forge start",
"test": "electron-forge start --enable-logging --inspect-electron",
"package": "electron-forge package",
"make": "electron-forge make",
"publish": "electron-forge publish"
"start": "electron-vite preview --outDir=dist",
"dev": "electron-vite dev --outDir=dist",
"package": "electron-vite build --outDir=dist && electron-forge package",
"make": "electron-vite build --outDir=dist && electron-forge make",
"publish": "electron-vite build --outDir=dist && electron-forge publish"
},
"devDependencies": {
"@electron-forge/cli": "^7.1.0",
@ -35,25 +35,38 @@
"@electron-forge/maker-zip": "^7.2.0",
"@electron-forge/plugin-auto-unpack-natives": "^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",
"eslint": "^8.56.0",
"eslint-config-standard": "^17.1.0",
"eslint-plugin-import": "^2.29.1",
"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": {
"@electron-toolkit/preload": "^3.0.0",
"@electron-toolkit/utils": "^3.0.0",
"@vitejs/plugin-react": "^4.2.1",
"decompress": "^4.2.1",
"electron-is-dev": "^3.0.1",
"electron-log": "^5.0.3",
"electron-squirrel-startup": "^1.0.0",
"electron-vite": "^2.0.0",
"eslint-plugin-react": "^7.33.2",
"extract-zip": "^2.0.1",
"hasha": "^6.0.0",
"izitoast": "^1.4.0",
"minecraft-launcher-core": "^3.17.3",
"msmc": "^4.1.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": {
"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 mlc from 'minecraft-launcher-core'
import fetch from 'node-fetch'
import { hashFile } from 'hasha'
import fs from 'fs'
import { join } from 'path'
import zip from 'extract-zip'
import logger from 'electron-log'
import { Auth, lst } from 'msmc'
import decompress from 'decompress'
import decompressTar from 'decompress-targz'
const { Authenticator, Client } = mlc
export default class Minecraft {
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')
minecraftpath = join(this.appdata, '.altarik')
launcher = new Client()
auth = null
modsList = undefined
showNotification = undefined
modsInformationsEndpoint = 'https://launcher.altarik.fr'
setShowNotification (showNotification) {
this.showNotification = showNotification
}
/**
* @deprecated Mojang removed this method of authentification
* Used to login through Mojang account
*/
login (event, win, username, password) {
this.auth = null
if (isDev || password.trim() !== '') {
this.auth = Authenticator.getAuth(username, password)
this.auth.then(v => {
win.loadFile('src/client/index.html')
}).catch(() => {
event.sender.send('loginError')
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')
})
} else {
this.showNotification('Erreur de connexion', 'Veuillez renseignez un mot de passe', 'warning')
}
}
/**
* Used to login through a Microsoft account
*/
microsoftLogin (event, win) {
const authManager = new Auth('select_account')
authManager.launch('electron').then(async xboxManager => {
xboxManager.getMinecraft().then(async token => {
if (!token.isDemo()) {
this.auth = token.mclc()
logger.info('[MS login] User has been connected successfully to them account')
win.loadFile('src/client/index.html')
} else {
event.sender.send('loginError')
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')
}
}).catch(err => {
event.sender.send('loginError')
this.showNotification('Erreur de connexion à Mojang', err, 'error')
logger.error('[MS login] ' + lst(err))
})
}).catch(err => {
event.sender.send('loginError')
if (err !== 'error.gui.closed') {
this.showNotification('Une erreur de connexion à Xbox est survenue', err, 'error')
logger.error('[MS login] ' + lst(err))
}
})
}
launch (event, args) {
this.extractJava(Number(args.chapter), event).then((javaPath) => {
this.extractMods(Number(args.chapter), event).then((chapter) => {
this.launcher.launch({
authorization: this.auth,
root: this.minecraftpath,
javaPath,
version: {
number: chapter.minecraftVersion,
type: chapter.type | 'release',
custom: chapter.customVersion
},
memory: {
max: args.maxMem,
min: args.minMem
}
}).then(v => {
if (v === null) {
this.close(event, -1)
}
})
this.launcher.on('debug', (e) => logger.info(`debug: ${e}`))
this.launcher.on('data', (e) => logger.info(`data: ${e}`))
this.launcher.on('progress', (e) => {
event.sender.send('progress', e)
logger.info(`progress ${e.type} :${e.task} / ${e.total}`)
})
this.launcher.on('arguments', (e) => {
event.sender.send('launch', e)
logger.info('launching the game')
logger.info(e)
})
this.launcher.on('close', (e) => {
this.close(event, e)
})
}).catch((err) => {
this.showNotification('Impossible de lancer le jeu', 'Erreur inconnue', 'error')
event.sender.send('close', 1)
logger.error('Unable to launch the game')
logger.error(err)
})
}).catch(err => {
this.showNotification('Impossible de lancer le jeu', "Impossible d'installer Java pour votre configuration", 'error')
event.sender.send('close', 1)
logger.warn('Unable to install java')
logger.warn(err)
})
}
close (event, code) {
event.sender.send('close', code)
if (code !== 0) {
logger.warn("Minecraft didn't close properly")
logger.warn(code)
this.showNotification('Une erreur est survenue', "Minecraft ne s'est pas fermé correctement", 'error')
}
}
getModsInformations (event) {
fetch(this.modsInformationsEndpoint).then(response => {
if (response.ok) {
response.json().then(data => {
const folder = join(this.localappdata, 'altarik-launcher', 'data')
if (!fs.existsSync(folder)) { fs.mkdirSync(folder, { recursive: true }) }
const file = join(folder, 'launcher.json')
if (fs.existsSync(file)) { fs.rmSync(file) }
fs.writeFileSync(file, JSON.stringify(data))
event.sender.send('modsInformations', this.extractModsInformations(data))
}).catch(err => {
event.sender.send('modsInformations', this.extractModsFromFileSystem())
logger.warn(err)
logger.warn('An error occured while trying to connect to server')
})
} else {
logger.warn('Unable to connect to server')
logger.warn(response.status)
event.sender.send('modsInformations', this.extractModsFromFileSystem())
}
}).catch(err => {
logger.warn('Unable to connect to server')
logger.warn(err)
event.sender.send('modsInformations', this.extractModsFromFileSystem())
})
}
extractModsFromFileSystem () {
const filepath = join(this.localappdata, 'altarik-launcher/data/launcher.json')
if (fs.existsSync(filepath)) {
const content = fs.readFileSync(filepath)
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')
return this.extractModsInformations(JSON.parse(content))
} else {
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')
return null
}
} else {
return null
}
}
extractModsInformations (json) {
this.modsList = json.chapters
return this.modsList
}
async extractMods (chapterId, event) {
return new Promise((resolve, reject) => {
const modsFolder = join(this.minecraftpath, 'mods')
// const shaderFolder = join(this.minecraftpath, 'shaderpacks')
if (fs.existsSync(modsFolder)) { fs.rmSync(modsFolder, { recursive: true }) }
// if (fs.existsSync(shaderFolder)) { fs.rmSync(shaderFolder, { recursive: true }) }
for (const i in this.modsList) {
if (Number(i) === chapterId) {
const chapter = this.modsList[i]
for (const j in chapter.modspack.mods) {
event.sender.send('progress', { type: 'mods', task: 0, total: chapter.modspack.mods.length })
const modpackFolder = join(this.minecraftpath, 'modpack', chapter.title)
if (!fs.existsSync(modpackFolder)) { fs.mkdirSync(modpackFolder, { recursive: true }) }
const path = join(modpackFolder, `modpack${j}.zip`)
try {
fs.accessSync(path, fs.W_OK)
hashFile(path, { algorithm: 'sha1' }).then(sha1 => {
if (sha1 === chapter.modspack.sha1sum[j]) {
this.unzipMods(path).catch(err => {
reject(err)
})
} else {
logger.warn(`sha1sum ${sha1} don't correspond to ${chapter.modspack.sha1sum[j]} of mods ${path}`)
this.downloadAndExtractMods(chapter.modspack.mods[j], path).catch(err => {
reject(err)
})
}
}).catch(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 })
} catch (err) {
this.downloadAndExtractMods(chapter.modspack.mods[j], path).catch(err => {
reject(err)
})
}
}
resolve(chapter)
return
}
}
reject(new Error("didn't found the correct chapter" + chapterId))
})
}
downloadMods (link, path) {
return new Promise((resolve, reject) => {
fetch(link).then(response => {
if (response.ok) {
if (fs.existsSync(path)) { fs.rmSync(path) }
const dest = fs.createWriteStream(path)
response.body.pipe(dest)
response.body.on('end', () => {
logger.log('download completed')
resolve('download completed')
})
dest.on('error', () => {
reject(new Error('An error appenned when using stream'))
})
} else {
reject(response.status)
}
}).catch(err => {
reject(err)
})
})
}
async unzipMods (zipLocation, outLocation = this.minecraftpath) {
return new Promise((resolve, reject) => {
logger.info(`unzipping ${zipLocation} file to ${outLocation}`)
zip(zipLocation, { dir: outLocation }).then(() => {
resolve()
}).catch(err => {
logger.error('failed to unzip file')
reject(err)
})
})
}
async extractTar (tarLocation, outLocation = this.microsoftpath) {
return new Promise((resolve, reject) => {
logger.info(`Extracting targz ${tarLocation} file to ${outLocation}`)
decompress(tarLocation, outLocation, {
plugins: [
decompressTar()
]
}).then(() => {
resolve()
}).catch((e) => {
logger.error('Failed to extract targz file')
reject(e)
})
})
}
async downloadAndExtractMods (link, path) {
return new Promise((resolve, reject) => {
this.downloadMods(link, path).then(() => {
this.unzipMods(path).then(() => {
resolve()
}).catch(err => {
reject(err)
})
}).catch(err => {
reject(err)
})
})
}
async extractJava (chapterId, event) {
return new Promise((resolve, reject) => {
const runtime = join(this.minecraftpath, 'runtime')
if (this.modsList[chapterId].java.platform[process.platform] !== undefined &&
this.modsList[chapterId].java.platform[process.platform][process.arch] !== undefined) {
event.sender.send('progress', { type: 'java', task: 0, total: 1 })
const infos = this.modsList[chapterId].java.platform[process.platform][process.arch]
const jre = join(runtime, infos.name)
const downloadFolder = join(runtime, 'download')
const downloadFile = join(downloadFolder, `${infos.name}.zip`)
if (fs.existsSync(jre)) { fs.rmSync(jre, { recursive: true }) }
if (!fs.existsSync(downloadFolder)) { fs.mkdirSync(downloadFolder, { recursive: true }) }
if (fs.existsSync(downloadFile)) {
hashFile(downloadFile, { algorithm: 'sha256' }).then(sha1 => {
if (sha1 === infos.sha256sum) {
this.extractJavaArchive(downloadFile, runtime).then(() => {
const filename = process.platform === 'win32' ? 'java.exe' : 'java'
resolve(join(jre, 'bin', filename))
}).catch(err => {
reject(err)
})
} else {
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))
}
}).catch(err => {
reject(err)
})
} else {
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 })
} else {
reject(new Error('There is not available version for this system'))
}
})
}
async downloadAndExtractJava (infos, downloadFolder, runtimeFolder) {
return new Promise((resolve, reject) => {
logger.info(`Downloading ${infos.name}`)
this.downloadMods(infos.link, join(downloadFolder, `${infos.name}.zip`)).then(() => {
logger.info('download completed')
this.extractJavaArchive(join(downloadFolder, `${infos.name}.zip`), runtimeFolder).then(() => {
logger.info('File unzipped')
resolve()
}).catch(err => {
const joinS = join(downloadFolder, `${infos.name}.zip`)
logger.info(`Failed to unzip ${joinS}`)
reject(err)
})
}).catch(err => {
logger.err(`Failed to download ${infos.link} to ${infos.name}.zip`)
reject(err)
})
})
}
async extractJavaArchive (zipLocation, outLocation) {
if (process.platform === 'win32') {
await this.unzipMods(zipLocation, outLocation)
} else {
await this.extractTar(zipLocation, outLocation)
}
}
invalidateData (event) {
logger.info('invalidate game data...')
const assets = join(this.minecraftpath, 'assets')
const librairies = join(this.minecraftpath, 'libraries')
const natives = join(this.minecraftpath, 'natives')
const versions = join(this.minecraftpath, 'versions')
if (fs.existsSync(assets)) { fs.rmSync(assets, { recursive: true }) }
if (fs.existsSync(librairies)) { fs.rmSync(librairies, { recursive: true }) }
if (fs.existsSync(natives)) { fs.rmSync(natives, { recursive: true }) }
if (fs.existsSync(versions)) { fs.rmSync(versions, { recursive: true }) }
logger.info('Game data invalidated')
event.sender.send('invalidated')
}
}
import { is } from '@electron-toolkit/utils'
import mlc from 'minecraft-launcher-core'
import fetch from 'node-fetch'
import { hashFile } from 'hasha'
import fs from 'fs'
import { join } from 'path'
import zip from 'extract-zip'
import logger from 'electron-log'
import { Auth, lst } from 'msmc'
import decompress from 'decompress'
import decompressTar from 'decompress-targz'
const { Authenticator, Client } = mlc
export default class Minecraft {
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')
minecraftpath = join(this.appdata, '.altarik')
launcher = new Client()
auth = null
modsList = undefined
showNotification = undefined
modsInformationsEndpoint = 'https://launcher.altarik.fr'
setShowNotification (showNotification) {
this.showNotification = showNotification
}
/**
* @deprecated Mojang removed this method of authentification
* Used to login through Mojang account
*/
login (event, win, username, password) {
this.auth = null
if (is.dev || password.trim() !== '') {
this.auth = Authenticator.getAuth(username, password)
this.auth.then(v => {
win.loadFile('src/client/index.html')
}).catch(() => {
event.sender.send('loginError')
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')
})
} else {
this.showNotification('Erreur de connexion', 'Veuillez renseignez un mot de passe', 'warning')
}
}
/**
* Used to login through a Microsoft account
*/
microsoftLogin (event, win) {
const authManager = new Auth('select_account')
authManager.launch('electron').then(async xboxManager => {
xboxManager.getMinecraft().then(async token => {
if (!token.isDemo()) {
this.auth = token.mclc()
logger.info('[MS login] User has been connected successfully to them account')
win.loadFile('src/client/index.html')
} else {
event.sender.send('loginError')
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')
}
}).catch(err => {
event.sender.send('loginError')
this.showNotification('Erreur de connexion à Mojang', err, 'error')
logger.error('[MS login] ' + lst(err))
})
}).catch(err => {
event.sender.send('loginError')
if (err !== 'error.gui.closed') {
this.showNotification('Une erreur de connexion à Xbox est survenue', err, 'error')
logger.error('[MS login] ' + lst(err))
}
})
}
launch (event, args) {
this.extractJava(Number(args.chapter), event).then((javaPath) => {
this.extractMods(Number(args.chapter), event).then((chapter) => {
this.launcher.launch({
authorization: this.auth,
root: this.minecraftpath,
javaPath,
version: {
number: chapter.minecraftVersion,
type: chapter.type | 'release',
custom: chapter.customVersion
},
memory: {
max: args.maxMem,
min: args.minMem
}
}).then(v => {
if (v === null) {
this.close(event, -1)
}
})
this.launcher.on('debug', (e) => logger.info(`debug: ${e}`))
this.launcher.on('data', (e) => logger.info(`data: ${e}`))
this.launcher.on('progress', (e) => {
event.sender.send('progress', e)
logger.info(`progress ${e.type} :${e.task} / ${e.total}`)
})
this.launcher.on('arguments', (e) => {
event.sender.send('launch', e)
logger.info('launching the game')
logger.info(e)
})
this.launcher.on('close', (e) => {
this.close(event, e)
})
}).catch((err) => {
this.showNotification('Impossible de lancer le jeu', 'Erreur inconnue', 'error')
event.sender.send('close', 1)
logger.error('Unable to launch the game')
logger.error(err)
})
}).catch(err => {
this.showNotification('Impossible de lancer le jeu', "Impossible d'installer Java pour votre configuration", 'error')
event.sender.send('close', 1)
logger.warn('Unable to install java')
logger.warn(err)
})
}
close (event, code) {
event.sender.send('close', code)
if (code !== 0) {
logger.warn("Minecraft didn't close properly")
logger.warn(code)
this.showNotification('Une erreur est survenue', "Minecraft ne s'est pas fermé correctement", 'error')
}
}
getModsInformations (event) {
fetch(this.modsInformationsEndpoint).then(response => {
if (response.ok) {
response.json().then(data => {
const folder = join(this.localappdata, 'altarik-launcher', 'data')
if (!fs.existsSync(folder)) { fs.mkdirSync(folder, { recursive: true }) }
const file = join(folder, 'launcher.json')
if (fs.existsSync(file)) { fs.rmSync(file) }
fs.writeFileSync(file, JSON.stringify(data))
event.sender.send('modsInformations', this.extractModsInformations(data))
}).catch(err => {
event.sender.send('modsInformations', this.extractModsFromFileSystem())
logger.warn(err)
logger.warn('An error occured while trying to connect to server')
})
} else {
logger.warn('Unable to connect to server')
logger.warn(response.status)
event.sender.send('modsInformations', this.extractModsFromFileSystem())
}
}).catch(err => {
logger.warn('Unable to connect to server')
logger.warn(err)
event.sender.send('modsInformations', this.extractModsFromFileSystem())
})
}
extractModsFromFileSystem () {
const filepath = join(this.localappdata, 'altarik-launcher/data/launcher.json')
if (fs.existsSync(filepath)) {
const content = fs.readFileSync(filepath)
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')
return this.extractModsInformations(JSON.parse(content))
} else {
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')
return null
}
} else {
return null
}
}
extractModsInformations (json) {
this.modsList = json.chapters
return this.modsList
}
async extractMods (chapterId, event) {
return new Promise((resolve, reject) => {
const modsFolder = join(this.minecraftpath, 'mods')
// const shaderFolder = join(this.minecraftpath, 'shaderpacks')
if (fs.existsSync(modsFolder)) { fs.rmSync(modsFolder, { recursive: true }) }
// if (fs.existsSync(shaderFolder)) { fs.rmSync(shaderFolder, { recursive: true }) }
for (const i in this.modsList) {
if (Number(i) === chapterId) {
const chapter = this.modsList[i]
for (const j in chapter.modspack.mods) {
event.sender.send('progress', { type: 'mods', task: 0, total: chapter.modspack.mods.length })
const modpackFolder = join(this.minecraftpath, 'modpack', chapter.title)
if (!fs.existsSync(modpackFolder)) { fs.mkdirSync(modpackFolder, { recursive: true }) }
const path = join(modpackFolder, `modpack${j}.zip`)
try {
fs.accessSync(path, fs.W_OK)
hashFile(path, { algorithm: 'sha1' }).then(sha1 => {
if (sha1 === chapter.modspack.sha1sum[j]) {
this.unzipMods(path).catch(err => {
reject(err)
})
} else {
logger.warn(`sha1sum ${sha1} don't correspond to ${chapter.modspack.sha1sum[j]} of mods ${path}`)
this.downloadAndExtractMods(chapter.modspack.mods[j], path).catch(err => {
reject(err)
})
}
}).catch(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 })
} catch (err) {
this.downloadAndExtractMods(chapter.modspack.mods[j], path).catch(err => {
reject(err)
})
}
}
resolve(chapter)
return
}
}
reject(new Error("didn't found the correct chapter" + chapterId))
})
}
downloadMods (link, path) {
return new Promise((resolve, reject) => {
fetch(link).then(response => {
if (response.ok) {
if (fs.existsSync(path)) { fs.rmSync(path) }
const dest = fs.createWriteStream(path)
response.body.pipe(dest)
response.body.on('end', () => {
logger.log('download completed')
resolve('download completed')
})
dest.on('error', () => {
reject(new Error('An error appenned when using stream'))
})
} else {
reject(response.status)
}
}).catch(err => {
reject(err)
})
})
}
async unzipMods (zipLocation, outLocation = this.minecraftpath) {
return new Promise((resolve, reject) => {
logger.info(`unzipping ${zipLocation} file to ${outLocation}`)
zip(zipLocation, { dir: outLocation }).then(() => {
resolve()
}).catch(err => {
logger.error('failed to unzip file')
reject(err)
})
})
}
async extractTar (tarLocation, outLocation = this.microsoftpath) {
return new Promise((resolve, reject) => {
logger.info(`Extracting targz ${tarLocation} file to ${outLocation}`)
decompress(tarLocation, outLocation, {
plugins: [
decompressTar()
]
}).then(() => {
resolve()
}).catch((e) => {
logger.error('Failed to extract targz file')
reject(e)
})
})
}
async downloadAndExtractMods (link, path) {
return new Promise((resolve, reject) => {
this.downloadMods(link, path).then(() => {
this.unzipMods(path).then(() => {
resolve()
}).catch(err => {
reject(err)
})
}).catch(err => {
reject(err)
})
})
}
async extractJava (chapterId, event) {
return new Promise((resolve, reject) => {
const runtime = join(this.minecraftpath, 'runtime')
if (this.modsList[chapterId].java.platform[process.platform] !== undefined &&
this.modsList[chapterId].java.platform[process.platform][process.arch] !== undefined) {
event.sender.send('progress', { type: 'java', task: 0, total: 1 })
const infos = this.modsList[chapterId].java.platform[process.platform][process.arch]
const jre = join(runtime, infos.name)
const downloadFolder = join(runtime, 'download')
const downloadFile = join(downloadFolder, `${infos.name}.zip`)
if (fs.existsSync(jre)) { fs.rmSync(jre, { recursive: true }) }
if (!fs.existsSync(downloadFolder)) { fs.mkdirSync(downloadFolder, { recursive: true }) }
if (fs.existsSync(downloadFile)) {
hashFile(downloadFile, { algorithm: 'sha256' }).then(sha1 => {
if (sha1 === infos.sha256sum) {
this.extractJavaArchive(downloadFile, runtime).then(() => {
const filename = process.platform === 'win32' ? 'java.exe' : 'java'
resolve(join(jre, 'bin', filename))
}).catch(err => {
reject(err)
})
} else {
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))
}
}).catch(err => {
reject(err)
})
} else {
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 })
} else {
reject(new Error('There is not available version for this system'))
}
})
}
async downloadAndExtractJava (infos, downloadFolder, runtimeFolder) {
return new Promise((resolve, reject) => {
logger.info(`Downloading ${infos.name}`)
this.downloadMods(infos.link, join(downloadFolder, `${infos.name}.zip`)).then(() => {
logger.info('download completed')
this.extractJavaArchive(join(downloadFolder, `${infos.name}.zip`), runtimeFolder).then(() => {
logger.info('File unzipped')
resolve()
}).catch(err => {
const joinS = join(downloadFolder, `${infos.name}.zip`)
logger.info(`Failed to unzip ${joinS}`)
reject(err)
})
}).catch(err => {
logger.err(`Failed to download ${infos.link} to ${infos.name}.zip`)
reject(err)
})
})
}
async extractJavaArchive (zipLocation, outLocation) {
if (process.platform === 'win32') {
await this.unzipMods(zipLocation, outLocation)
} else {
await this.extractTar(zipLocation, outLocation)
}
}
invalidateData (event) {
logger.info('invalidate game data...')
const assets = join(this.minecraftpath, 'assets')
const librairies = join(this.minecraftpath, 'libraries')
const natives = join(this.minecraftpath, 'natives')
const versions = join(this.minecraftpath, 'versions')
if (fs.existsSync(assets)) { fs.rmSync(assets, { recursive: true }) }
if (fs.existsSync(librairies)) { fs.rmSync(librairies, { recursive: true }) }
if (fs.existsSync(natives)) { fs.rmSync(natives, { recursive: true }) }
if (fs.existsSync(versions)) { fs.rmSync(versions, { recursive: true }) }
logger.info('Game data 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 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(`platform: ${process.platform}`)
this.logger.info(`arch: ${process.arch}`)
if (isDev) {
if (is.dev) {
this.logger.info(`developpement version ${this.app.getVersion()}`)
return
}
@ -33,7 +33,7 @@ export default class Updater {
}
checkForUpdates (win, showNotification) {
if (isDev) {
if (is.dev) {
win.loadFile('src/client/login.html')
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";
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