gill 0.1.0 init release

- game installation
- game starting
This commit is contained in:
Observer KRypt0n_ 2021-10-14 20:05:44 +02:00
parent d4698f11fa
commit 4ca47f51ca
No known key found for this signature in database
GPG key ID: DC5D4EC1303465DA
11 changed files with 2060 additions and 1587 deletions

3
.gitignore vendored
View file

@ -1,3 +1,4 @@
node_modules
dist
package-lock.json
package-lock.json
wineprefix-installation.log

View file

@ -1,12 +1,17 @@
const { app, BrowserWindow } = require('electron');
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');
let mainWindow;
ipcMain.handle('hide-window', () => mainWindow.hide());
ipcMain.handle('show-window', () => mainWindow.show());
function createWindow ()
{
// https://www.electronjs.org/docs/latest/api/browser-window/#class-browserwindow
const mainWindow = new BrowserWindow ({
width: 800,
height: 600,
mainWindow = new BrowserWindow ({
width: 1280,
height: 728,
webPreferences: {
// Is not safety
// Use it to have access to the node modules inside html files
@ -14,8 +19,8 @@ function createWindow ()
contextIsolation: false
},
icon: path.join(__dirname, 'public', 'images', 'icon64.png'),
// autoHideMenuBar: true,
// resizable: false
autoHideMenuBar: true,
resizable: false
});
mainWindow.loadFile(path.join(__dirname, 'public', 'html', 'index.html'));

View file

@ -1,16 +1,8 @@
{
"name": "electron-blank-app",
"version": "1.0.0",
"description": "Electron Blank Application",
"keywords": [
"some",
"keywords"
],
"author": {
"name" : "your_name",
"email" : "your_email",
"url" : "your_site"
},
"name": "genshin-impact-linux-launcher",
"version": "0.1.0",
"description": "Genshin Impact Linux Launcher",
"author": "Nikita Podvirnyy",
"license": "GPL-3.0",
"main": "entry.js",
"scripts": {
@ -20,9 +12,9 @@
"build:linux": "npm run dev && electron-builder --linux"
},
"build": {
"productName": "Electron Blank Application",
"productName": "Genshin Impact Linux Launcher",
"artifactName": "${productName}-${os}-${arch}-${version}.${ext}",
"appId": "com.electron.blank-app",
"appId": "com.krypt0nn.genshin-impact-linux-launcher",
"directories": {
"output": "dist"
},
@ -75,5 +67,8 @@
"electron-builder": "^22.13.1",
"sass": "^1.41.0",
"typescript": "^4.4.3"
},
"dependencies": {
"cash-dom": "^8.1.0"
}
}

View file

@ -9,12 +9,24 @@
<link rel="stylesheet" href="../css/index.css">
<!-- JS scripts -->
<script src="../js/test.js"></script>
<script>require('../js/index.js');</script>
<title>Hello World</title>
<title>Genshin Impact Linux Launcher</title>
</head>
<body>
<h1 class="greeting"></h1>
<div id="downloader-panel" style="display: none">
<div id="downloader-label">
<span id="downloaded">Downloading...</span>
<span id="speed"></span>
<span id="eta"></span>
</div>
<div class="progress-bar" id="downloader">
<div class="progress"></div>
</div>
</div>
<button class="button" id="launch">Launch</button>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

View file

@ -1,11 +1,83 @@
@mixin center ($width)
body
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif
#info
position: absolute
top: 196px
left: 48px
width: 460px
height: 352px
background-color: rgba(0, 0, 0, .65)
.button
border-radius: 8px
background-color: #ffcb0b
border: none
cursor: pointer
.button:hover:not([disabled])
background-color: #fac60b
box-shadow: 0px 0px 6px 0px rgba(0, 0, 0, .3)
.button:disabled
background-color: rgba(0, 0, 0, .35)
color: rgba(255, 255, 255, .7)
#launch
width: 238px
height: 64px
font-size: 22px
position: absolute
width: $width
left: calc(50% - $width / 2)
top: calc(50% - $width / 2)
right: 128px
bottom: 64px
.greeting
@include center(200px)
.progress-bar
padding: 0
text-align: center
background-color: rgba(0, 0, 0, .1)
border: 1px solid rgba(0, 0, 0, .2)
border-radius: 8px
.progress
width: 0
height: 100%
background-color: rgba(255, 255, 255, .7)
border-radius: 8px
#downloader-panel
user-select: none
#downloader
position: absolute
left: 48px
bottom: 68px
width: 720px
height: 36px
#downloader-label
position: absolute
left: 48px
bottom: 116px
color: white
font-size: 18px
#downloaded
max-width: 720px
display: inline-block
word-break: break-all
#speed, #eta
margin-left: 8px
color: #cccccc
font-weight: 100

210
src/ts/Genshinlib.ts Normal file
View file

@ -0,0 +1,210 @@
const https = require('https');
const fs = require('fs');
const path = require('path');
const os = require('os');
const { spawn } = require('child_process');
type Config = {
lang: {
launcher: 'en-us' | 'ru-ru',
voice: 'en-us' | 'ru-ru'
},
version: string|null
};
export class Genshinlib
{
public static readonly launcherDir: string = path.join(os.homedir(), 'genshin-impact-launcher');
public static readonly launcherJson: string = path.join(this.launcherDir, 'launcher.json');
public static readonly prefixDir: string = path.join(this.launcherDir, 'game');
public static readonly gameDir: string = path.join(this.prefixDir, 'drive_c', 'Program Files', 'Genshin Impact');
protected static uri: string = 'https://sdk-os-static.mihoyo.com/hk4e_global/mdk/launcher/api/resource?key=gcStgarh&launcher_id=10';
public static get version(): string|null
{
return this.getLauncherInfo().version;
}
public static get lang(): { launcher: string, voice: string }
{
return this.getLauncherInfo().lang;
}
public static getLauncherInfo (): Config
{
if (!fs.existsSync(this.launcherJson))
fs.writeFileSync(this.launcherJson, JSON.stringify({
lang: {
launcher: 'en-us',
voice: 'en-us'
},
version: null
}));
return JSON.parse(fs.readFileSync(this.launcherJson));
}
public static setLauncherInfo (info: Config): Genshinlib
{
fs.writeFileSync(this.launcherJson, JSON.stringify(info));
return this;
}
public static async getData (): Promise<any>
{
return new Promise((resolve, reject) => {
https.get(this.uri, (response: any) => {
let data = '';
response.on('data', (chunk: any) => data += chunk);
response.on('end', () => {
data = JSON.parse(data);
// @ts-expect-error
return data.message === 'OK' ? resolve(data.data) : reject(null);
});
}).on('error', (err: Error) => {
reject(err);
});
});
}
public static getBackgroundUri (): string
{
return path.join(__dirname, '..', 'images', 'backgrounds', this.lang.launcher + '.png');
}
public static async downloadFile (uri: string, savePath: string, progress: (current: number, total: number, difference: number) => void): Promise<void|Error>
{
return new Promise((resolve, reject) => {
https.get(uri, (response: any) => {
let length = parseInt(response.headers['content-length'], 10),
total = 0;
response.on('data', (chunk: any) => {
total += chunk.length;
progress(total, length, chunk.length);
fs.appendFileSync(savePath, chunk);
});
response.on('end', () => resolve());
}).on('error', (err: Error) => {
reject(err);
});
});
}
public static async unzip (zipPath: string, unpackedPath: string, progress: (current: number, total: number, difference: number) => void): Promise<void|Error>
{
return new Promise((resolve, reject) => {
let listenerProcess = spawn('unzip', ['-v', zipPath]),
filesList = '';
listenerProcess.stdout.on('data', (data: string) => filesList += data);
listenerProcess.on('close', () => {
let files = filesList.split(/\r\n|\r|\n/).slice(3, -3).map(line => {
line = line.trim();
if (line.slice(-1) == '/')
line = line.slice(0, -1);
let matches = /^(\d+) [a-zA-Z\:]+[ ]+(\d+)[ ]+[0-9\-]+% [0-9\-]+ [0-9\:]+ [a-f0-9]{8} (.+)/.exec(line);
return {
// @ts-expect-error
path: matches[3],
// @ts-expect-error
compressedSize: parseInt(matches[2]),
// @ts-expect-error
uncompressedSize: parseInt(matches[1])
};
});
let total = fs.statSync(zipPath)['size'], current = 0;
let unpackerProcess = spawn('unzip', ['-o', zipPath, '-d', unpackedPath]);
unpackerProcess.stdout.on('data', (data: string) => {
data.toString().split(/\r\n|\r|\n/).forEach(line => {
let items = line.split(': ');
if (items[1] !== undefined)
{
items[1] = path.relative(unpackedPath, items[1].trim());
files.forEach(file => {
if (file.path == items[1])
{
current += file.compressedSize;
progress(current, total, file.compressedSize);
}
});
}
});
});
unpackerProcess.on('close', () => resolve());
});
});
}
// WINEPREFIX='/home/observer/genshin-impact-launcher/wineprefix' winetricks corefonts
public static async installPrefix (path: string, progress: (output: string, current: number, total: number) => void): Promise<void>
{
let installationSteps = [
'Executing w_do_call corefonts',
'Executing load_corefonts',
'Executing load_andale',
'Executing load_arial',
'Executing load_comicsans',
'Executing load_courier',
'Executing load_georgia',
'Executing load_impact',
'Executing load_times',
'Executing load_trebuchet',
'Executing load_verdana',
'Executing load_webdings'
];
return new Promise((resolve) => {
let installationProgress = 0;
let installerProcess = spawn('winetricks', ['corefonts'], {
env: {
...process.env,
WINEPREFIX: path
}
});
installerProcess.stdout.on('data', (data: string) => {
let str = data.toString();
for (let i = 0; i < installationSteps.length; ++i)
if (str.includes(installationSteps[i]))
{
installationProgress = i + 1;
break;
}
progress(str, installationProgress, installationSteps.length);
});
installerProcess.on('close', () => resolve());
});
}
public static isPrefixInstalled (prefixPath: string): boolean
{
return fs.existsSync(path.join(prefixPath, 'drive_c'));
}
}

266
src/ts/index.ts Normal file
View file

@ -0,0 +1,266 @@
const path = require('path');
const fs = require('fs');
const { exec } = require('child_process');
const { ipcRenderer } = require('electron');
import $ from 'cash-dom';
import { Genshinlib } from './Genshinlib';
if (!fs.existsSync(Genshinlib.prefixDir))
fs.mkdirSync(Genshinlib.prefixDir, { recursive: true });
$(() => {
if (Genshinlib.version !== null)
document.title = 'Genshin Impact Linux Launcher - ' + Genshinlib.version;
$('body').css('background-image', `url(${ Genshinlib.getBackgroundUri() })`);
Genshinlib.getData().then(data => {
if (Genshinlib.version != data.game.latest.version)
$('#launch').text(Genshinlib.version === null ? 'Install' : 'Update');
$('#launch').on('click', async () => {
// Creating wine prefix
if (!Genshinlib.isPrefixInstalled(Genshinlib.prefixDir))
{
$('#launch').css('display', 'none');
$('#downloader-panel').css('display', 'block');
await Genshinlib.installPrefix(Genshinlib.prefixDir, (output: string, current: number, total: number) => {
output = output.trim();
console.log(output);
if (!output.includes('\n') && !output.includes('\r'))
$('#downloaded').text(output);
$('#downloader .progress').css('width', `${ Math.round(current / total * 100) }%`);
});
$('#launch').css('display', 'block');
$('#downloader-panel').css('display', 'none');
}
// Launching game
if ($('#launch').text() == 'Launch')
{
exec(`wine "${path.join(Genshinlib.gameDir, 'GenshinImpact.exe')}"`, {
env: {
...process.env,
WINEPREFIX: Genshinlib.prefixDir
}
}, () => {
ipcRenderer.invoke('show-window');
});
ipcRenderer.invoke('hide-window');
}
// Installing game
else
{
$('#launch').css('display', 'none');
$('#downloader-panel').css('display', 'block');
let diff = {
path: data.game.latest.path,
name: `latest-${data.game.latest.version}.zip`,
voice_packs: data.game.latest.voice_packs
};
for (let i = 0; i < data.game.diffs.length; ++i)
if (data.game.diffs[i].version == Genshinlib.version)
{
diff = data.game.diffs[i];
break;
}
if (fs.existsSync(path.join(Genshinlib.gameDir, diff.name)))
fs.unlinkSync(path.join(Genshinlib.gameDir, diff.name));
let beganAt = Date.now(), prevTime = Date.now(), downloaded = 0;
/**
* Downloading game
*/
Genshinlib.downloadFile(diff.path, path.join(Genshinlib.launcherDir, diff.name), (current: number, total: number, difference: number) => {
$('#downloaded').text(`Downloaded: ${ Math.round(current / total * 100) }% (${ (current / 1024 / 1024 / 1024).toFixed(2) } GB / ${ Math.round(total / 1024 / 1024 / 1024).toFixed(2) } GB)`);
downloaded += difference;
if (Date.now() - prevTime > 1000)
{
let eta = Math.round(total / current * (Date.now() - beganAt) / 1000); // seconds
let etaHours = Math.floor(eta / 3600),
etaMinutes = Math.floor((eta - etaHours * 3600) / 60),
etaSeconds = eta - etaHours * 3600 - etaMinutes * 60;
if (etaHours < 10) // @ts-expect-error
etaHours = '0' + etaHours.toString();
if (etaMinutes < 10) // @ts-expect-error
etaMinutes = '0' + etaMinutes.toString();
if (etaSeconds < 10) // @ts-expect-error
etaSeconds = '0' + etaSeconds.toString();
$('#downloader .progress').css('width', `${ Math.round(current / total * 100) }%`);
$('#speed').text(`${ (downloaded / (Date.now() - prevTime) * 1000 / 1024 / 1024).toFixed(2) } MB/s`);
$('#eta').text(`ETA: ${etaHours}:${etaMinutes}:${etaSeconds}`);
prevTime = Date.now();
downloaded = 0;
}
}).then(() => {
/**
* Unpacking downloaded game
*/
$('#speed').text('');
$('#eta').text('');
if (!fs.existsSync(Genshinlib.gameDir))
fs.mkdirSync(Genshinlib.gameDir, { recursive: true });
let beganAt = Date.now(), prevTime = Date.now(), unpacked = 0;
Genshinlib.unzip(path.join(Genshinlib.launcherDir, diff.name), Genshinlib.gameDir, (current: number, total: number, difference: number) => {
$('#downloaded').text(`Unpacking: ${ Math.round(current / total * 100) }% (${ (current / 1024 / 1024 / 1024).toFixed(2) } GB / ${ Math.round(total / 1024 / 1024 / 1024).toFixed(2) } GB)`);
unpacked += difference;
if (Date.now() - prevTime > 1000)
{
let eta = Math.round(total / current * (Date.now() - beganAt) / 1000); // seconds
let etaHours = Math.floor(eta / 3600),
etaMinutes = Math.floor((eta - etaHours * 3600) / 60),
etaSeconds = eta - etaHours * 3600 - etaMinutes * 60;
if (etaHours < 10) // @ts-expect-error
etaHours = '0' + etaHours.toString();
if (etaMinutes < 10) // @ts-expect-error
etaMinutes = '0' + etaMinutes.toString();
if (etaSeconds < 10) // @ts-expect-error
etaSeconds = '0' + etaSeconds.toString();
$('#downloader .progress').css('width', `${ Math.round(current / total * 100) }%`);
$('#speed').text(`${ (unpacked / (Date.now() - prevTime) * 1000 / 1024 / 1024).toFixed(2) } MB/s`);
$('#eta').text(`ETA: ${etaHours}:${etaMinutes}:${etaSeconds}`);
prevTime = Date.now();
unpacked = 0;
}
}).then(() => {
fs.unlinkSync(path.join(Genshinlib.launcherDir, diff.name));
let voicePack = diff.voice_packs[1]; // en-us
for (let i = 0; i < diff.voice_packs.length; ++i)
if (diff.voice_packs[i].language == Genshinlib.lang.voice)
{
voicePack = diff.voice_packs[i];
break;
}
let beganAt = Date.now(), prevTime = Date.now(), downloaded = 0;
/**
* Downloading voice data
*/
Genshinlib.downloadFile(voicePack.path, path.join(Genshinlib.launcherDir, voicePack.name), (current: number, total: number, difference: number) => {
$('#downloaded').text(`Downloaded: ${ Math.round(current / total * 100) }% (${ (current / 1024 / 1024 / 1024).toFixed(2) } GB / ${ Math.round(total / 1024 / 1024 / 1024).toFixed(2) } GB)`);
downloaded += difference;
if (Date.now() - prevTime > 1000)
{
let eta = Math.round(total / current * (Date.now() - beganAt) / 1000); // seconds
let etaHours = Math.floor(eta / 3600),
etaMinutes = Math.floor((eta - etaHours * 3600) / 60),
etaSeconds = eta - etaHours * 3600 - etaMinutes * 60;
if (etaHours < 10) // @ts-expect-error
etaHours = '0' + etaHours.toString();
if (etaMinutes < 10) // @ts-expect-error
etaMinutes = '0' + etaMinutes.toString();
if (etaSeconds < 10) // @ts-expect-error
etaSeconds = '0' + etaSeconds.toString();
$('#downloader .progress').css('width', `${ Math.round(current / total * 100) }%`);
$('#speed').text(`${ (downloaded / (Date.now() - prevTime) * 1000 / 1024 / 1024).toFixed(2) } MB/s`);
$('#eta').text(`ETA: ${etaHours}:${etaMinutes}:${etaSeconds}`);
prevTime = Date.now();
downloaded = 0;
}
}).then(() => {
/**
* Unpacking downloaded game
*/
$('#speed').text('');
$('#eta').text('');
let beganAt = Date.now(), prevTime = Date.now(), unpacked = 0;
Genshinlib.unzip(path.join(Genshinlib.launcherDir, voicePack.name), Genshinlib.gameDir, (current: number, total: number, difference: number) => {
$('#downloaded').text(`Unpacking: ${ Math.round(current / total * 100) }% (${ (current / 1024 / 1024 / 1024).toFixed(2) } GB / ${ Math.round(total / 1024 / 1024 / 1024).toFixed(2) } GB)`);
unpacked += difference;
if (Date.now() - prevTime > 1000)
{
let eta = Math.round(total / current * (Date.now() - beganAt) / 1000); // seconds
let etaHours = Math.floor(eta / 3600),
etaMinutes = Math.floor((eta - etaHours * 3600) / 60),
etaSeconds = eta - etaHours * 3600 - etaMinutes * 60;
if (etaHours < 10) // @ts-expect-error
etaHours = '0' + etaHours.toString();
if (etaMinutes < 10) // @ts-expect-error
etaMinutes = '0' + etaMinutes.toString();
if (etaSeconds < 10) // @ts-expect-error
etaSeconds = '0' + etaSeconds.toString();
$('#downloader .progress').css('width', `${ Math.round(current / total * 100) }%`);
$('#speed').text(`${ (unpacked / (Date.now() - prevTime) * 1000 / 1024 / 1024).toFixed(2) } MB/s`);
$('#eta').text(`ETA: ${etaHours}:${etaMinutes}:${etaSeconds}`);
prevTime = Date.now();
unpacked = 0;
}
}).then(() => {
fs.unlinkSync(path.join(Genshinlib.launcherDir, voicePack.name));
Genshinlib.setLauncherInfo({
...Genshinlib.getLauncherInfo(),
version: data.game.latest.version
});
$('#launch').css('display', 'block');
$('#downloader-panel').css('display', 'none');
$('#launch').text('Launch');
});
}).catch(err => console.log(err));
}).catch(err => console.log(err));
});
}
});
});
});

View file

@ -1,6 +0,0 @@
// When page is fully loaded
window.addEventListener('DOMContentLoaded', () => {
// Change the text inside tags with greeting class
// @ts-ignore
document.querySelector('.greeting').innerText = 'Hello World';
}, false);

3018
yarn.lock

File diff suppressed because it is too large Load diff