2.0.0 beta 3

- made Discord RPC preparations
  made `discord-rpc` cli utility
  made `DiscordRPC` class to manage it
- changed `'` to `"` in every command execution
- `Voice.current` field changed to `Voice.installed`
- `Voice.selected` field now returns list of selected voice packages
- added `Voice.delete()` method to delete voice package
- `Voice.isUpdatePredownloaded()` method now can check list of voice packages
- fixed ProgressBar's `showTotals` property work
- decreased maximal wine prefix creation log size in progress bar
- improved `Process.run` command which now finds correct process id
- made DropdownCheckboxes component
- made voice packages selection system
This commit is contained in:
Observer KRypt0n_ 2022-01-03 18:51:32 +02:00
parent d90f339eb1
commit db6c219776
No known key found for this signature in database
GPG key ID: DC5D4EC1303465DA
35 changed files with 478 additions and 173 deletions

View file

@ -1,6 +1,6 @@
{
"applicationId": "com.krypt0nn.an-anime-game-launcher",
"version": "2.0.0-beta-2",
"version": "2.0.0-beta-3",
"defaultMode": "window",
"port": 0,
"documentRoot": "/bundle/",

View file

@ -1,6 +1,6 @@
{
"name": "an-anime-game-launcher",
"version": "2.0.0-beta-2",
"version": "2.0.0-beta-3",
"license": "GPL-3.0",
"type": "module",
"scripts": {

BIN
public/discord-rpc/discord-rpc Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

View file

@ -39,7 +39,7 @@ const bundler = new Bundler({
output: path.join(__dirname, '../dist/An Anime Game Launcher.AppImage'),
// Application version
version: '2.0.0-beta-2'
version: '2.0.0-beta-3'
});
// Bundle project

View file

@ -0,0 +1,81 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
import Configs from '../ts/Configs';
export let prop: string = '';
export let lang: string = '';
export let tooltip: string = '';
export let selected: string|undefined;
export let items = {};
export let selectionUpdated: (property: string, value: boolean, list: object) => void = () => {};
import Arrow from '../assets/svgs/arrow.svg';
import Checkmark from '../assets/svgs/checkmark.svg';
let selectionOpen = false;
let selectedValue = selected;
let selectedValues = {};
Object.keys(items).forEach((key) => selectedValues[key] = false);
Configs.get(prop).then((values) => {
(values as string[]).forEach((key) => selectedValues[key] = true);
});
const updateCheckbox = (value: string) => {
selectedValues[value] = !selectedValues[value];
let activeVoices: string[] = [];
Object.keys(selectedValues).forEach((key) => {
if (selectedValues[key])
activeVoices.push(key);
});
Configs.set(prop, activeVoices);
selectionUpdated(value, selectedValues[value], selectedValues);
};
</script>
<div class="select dropdown-checkboxes" class:select-active={selectionOpen}>
<span>{ $_(lang) }</span>
<div class="select-options">
<ul>
{#each Object.keys(items) as value}
<li>
<div class="checkbox" class:checkbox-active={selectedValues[value]}>
<span>{ $_(items[value]) }</span>
<div class="checkbox-mark" on:click={() => updateCheckbox(value)}>
<!-- svelte-ignore a11y-missing-attribute -->
<img src={Checkmark} />
</div>
</div>
</li>
{/each}
</ul>
</div>
<div
class="selected-item"
class:hint--left={tooltip !== ''}
class:hint--medium={tooltip !== ''}
aria-label={$_(tooltip)}
on:click={() => selectionOpen = !selectionOpen}
>
<span>{ selectedValue ? $_(items[selectedValue]) : '' }</span>
<!-- svelte-ignore a11y-missing-attribute -->
<img src={Arrow} class:selection-empty={selectedValue === undefined} />
</div>
</div>
<style>
.dropdown-checkboxes .selected-item img.selection-empty
{
margin-left: 0;
}
</style>

View file

@ -6,7 +6,9 @@ promisify(async () => {
Configs.defaults({
lang: {
launcher: 'en-us',
voice: 'en-us'
voice: [
'en-us'
]
},
/**

View file

@ -15,6 +15,7 @@
import Gear from './assets/images/gear.png';
import GearActive from './assets/images/gear-active.png';
import Download from './assets/images/cloud-download.png';
import DiscordRPC from './ts/core/DiscordRPC';
Neutralino.events.on('ready', () => {
Window.open('splash', {
@ -42,6 +43,22 @@
Window.current.setTitle(`${constants.placeholders.uppercase.full} Linux Launcher - ${game.version}`);
});
/*const rpc = new DiscordRPC({
id: '901534333360304168',
details: 'Aboba',
state: 'Amogus',
icon: {
large: 'kleegame'
},
time: {
start: Math.round(Date.now() / 1000)
}
});
setTimeout(() => {
rpc.stop(true);
}, 10000);*/
/**
* Add some events to some elements
*/

View file

@ -1,22 +1,10 @@
import '../i18n';
import Debug from '../ts/core/Debug';
import App from '../settings.svelte';
import IPC from '../ts/core/IPC';
declare const Neutralino;
Neutralino.init();
Neutralino.events.on('windowClose', async () => {
await IPC.write({
type: 'log',
records: Debug.getRecords()
});
Neutralino.app.exit();
});
const app = new App({
target: document.getElementById('app')!
});

View file

@ -2,6 +2,7 @@
@import "components/checkbox"
@import "components/selectionBox"
@import "components/dropdownCheckboxes"
@import "components/selectionList"
@mixin themable($theme-name, $theme-map)

View file

@ -0,0 +1,17 @@
@use "sass:map"
@mixin themable($theme-name, $theme-map)
body[data-theme=#{$theme-name}]
.dropdown-checkboxes
.select-options
.checkbox
height: 28px
span
margin-right: 24px
@import "src/sass/themes/light"
@import "src/sass/themes/dark"
@include themable(light, $light)
@include themable(dark, $dark)

View file

@ -1,18 +1,24 @@
<script context="module" lang="ts">
declare const Neutralino;
</script>
<script lang="ts">
import { onMount } from 'svelte';
import { _, locale, locales } from 'svelte-i18n';
import Configs from './ts/Configs';
import FPSUnlock from './ts/FPSUnlock';
import Window from './ts/neutralino/Window';
import Debug from './ts/core/Debug';
import IPC from './ts/core/IPC';
import Checkbox from './components/Checkbox.svelte';
import SelectionBox from './components/SelectionBox.svelte';
import DropdownCheckboxes from './components/DropdownCheckboxes.svelte';
import DXVKSelectionList from './components/DXVKSelectionList.svelte';
import RunnerSelectionList from './components/RunnerSelectionList.svelte';
import ShadersSelection from './components/ShadersSelection.svelte';
import Window from './ts/neutralino/Window';
// TODO: somehow simplify all this variables definitions
/**
@ -104,7 +110,8 @@
let dxvkRecommendable = true,
runnersRecommendable = true,
fpsUnlockerAvailable = true;
fpsUnlockerAvailable = true,
voiceUpdateRequired = false;
// Auto theme switcher
Configs.get('theme').then((theme) => switchTheme(theme as string));
@ -113,6 +120,18 @@
onMount(() => {
Window.current.show();
});
Neutralino.events.on('windowClose', async () => {
await IPC.write({
type: 'log',
records: Debug.getRecords()
});
if (voiceUpdateRequired)
await IPC.write('voice-update-required');
Neutralino.app.exit();
});
</script>
{#if typeof $locale === 'string'}
@ -134,11 +153,13 @@
valueChanged={(value) => $locale = value}
/>
<SelectionBox
<DropdownCheckboxes
lang="settings.general.items.lang.voice.title"
tooltip="settings.general.items.lang.voice.tooltip"
prop="lang.voice"
selected={undefined}
items={voiceLocales}
selectionUpdated={() => voiceUpdateRequired = true}
/>
<SelectionBox

View file

@ -35,7 +35,7 @@ export default class FPSUnlock
Downloader.download(constants.uri.fpsunlock.bat, fpsunlockBat).then((stream) => {
stream.finish(async () => {
// sed -i 's/start ..\/GI_FPSUnlocker\/unlockfps.exe \%\*/start ..\/fpsunlock\/unlockfps.exe \%\*/g' unlockfps.bat
Neutralino.os.execCommand(`sed -i 's/start ..\\/GI_FPSUnlocker\\/unlockfps.exe \\%\\*/start ..\\/fpsunlock\\/unlockfps.exe \\%\\*/g' '${Process.addSlashes(fpsunlockBat)}'`)
Neutralino.os.execCommand(`sed -i 's/start ..\\/GI_FPSUnlocker\\/unlockfps.exe \\%\\*/start ..\\/fpsunlock\\/unlockfps.exe \\%\\*/g' "${Process.addSlashes(fpsunlockBat)}"`)
.then(() => resolve());
});
});

View file

@ -9,8 +9,6 @@ import State from './launcher/State';
import Debug from './core/Debug';
import IPC from './core/IPC';
declare const Neutralino;
export default class Launcher
{
public state?: State;
@ -40,7 +38,7 @@ export default class Launcher
title: 'Settings',
width: 900,
height: 600,
// enableInspector: true,
enableInspector: true,
exitProcessOnClose: false
});
@ -55,9 +53,12 @@ export default class Launcher
records.forEach((record) => {
if (record.data.type !== undefined && record.data.type === 'log')
Debug.merge(record.pop().data.records);
else if (record.data === 'voice-update-required')
this.state!.set('game-voice-update-required');
});
});
Window.current.show();
})

View file

@ -47,34 +47,34 @@ class Stream extends AbstractInstaller
/**
* Remove test version restrictions from the main patch
*/
() => Neutralino.os.execCommand(`cd '${patchDir}' && sed -i '/^echo "If you would like to test this patch, modify this script and remove the line below this one."/,+5d' patch.sh`),
() => Neutralino.os.execCommand(`cd "${patchDir}" && sed -i '/^echo "If you would like to test this patch, modify this script and remove the line below this one."/,+5d' patch.sh`),
/**
* Remove /etc/hosts editing due to sudo permissions
*/
() => Neutralino.os.execCommand(`cd '${patchDir}' && sed -i '/^# ===========================================================/,+68d' patch.sh`),
() => Neutralino.os.execCommand(`cd "${patchDir}" && sed -i '/^# ===========================================================/,+68d' patch.sh`),
/**
* Remove test version restrictions from the anti-login crash patch
*/
() => Neutralino.os.execCommand(`cd '${patchDir}' && sed -i '/^echo " necessary afterwards (Friday?). If that's the case, comment the line below."/,+2d' patch_anti_logincrash.sh`),
() => Neutralino.os.execCommand(`cd "${patchDir}" && sed -i '/^echo " necessary afterwards (Friday?). If that's the case, comment the line below."/,+2d' patch_anti_logincrash.sh`),
/**
* Make the main patch executable
*/
() => Neutralino.os.execCommand(`chmod +x '${patchDir}/patch.sh'`),
() => Neutralino.os.execCommand(`chmod +x "${patchDir}/patch.sh"`),
/**
* Make the anti-login crash patch executable
*/
() => Neutralino.os.execCommand(`chmod +x '${patchDir}/patch_anti_logincrash.sh'`),
() => Neutralino.os.execCommand(`chmod +x "${patchDir}/patch_anti_logincrash.sh"`),
/**
* Execute the main patch installation script
*/
(): Promise<void> => {
return new Promise(async (resolve) => {
Process.run(`yes yes | bash '${patchDir}/patch.sh'`, {
Process.run(`yes yes | bash "${patchDir}/patch.sh"`, {
cwd: await constants.paths.gameDir
}).then((process) => {
process.finish(() => resolve());
@ -87,7 +87,7 @@ class Stream extends AbstractInstaller
*/
(): Promise<void> => {
return new Promise(async (resolve) => {
Process.run(`yes | bash '${patchDir}/patch_anti_logincrash.sh'`, {
Process.run(`yes | bash "${patchDir}/patch_anti_logincrash.sh"`, {
cwd: await constants.paths.gameDir
}).then((process) => {
process.finish(() => resolve());

View file

@ -1,5 +1,5 @@
import type { VoicePack } from './types/GameData';
import type { VoiceInfo, InstalledVoiceInfo, VoiceLang } from './types/Voice';
import type { VoiceLang, InstalledVoice } from './types/Voice';
import constants from './Constants';
import Game from './Game';
@ -7,6 +7,7 @@ import AbstractInstaller from './core/AbstractInstaller';
import Configs from './Configs';
import Debug, { DebugThread } from './core/Debug';
import Downloader, { Stream as DownloadingStream } from './core/Downloader';
import Process from './neutralino/Process';
declare const Neutralino;
@ -18,28 +19,26 @@ class Stream extends AbstractInstaller
}
}
export default class Voice
{
protected static readonly langs = {
'en-us': 'English(US)',
'ja-jp': 'Japanese',
'ko-kr': 'Korean',
'zn-cn': 'Chinese'
};
/**
* Get current installed voice data info
* Get the list of the installed voice packages
*/
public static get current(): Promise<VoiceInfo>
public static get installed(): Promise<InstalledVoice[]>
{
return new Promise(async (resolve) => {
const persistentPath = `${await constants.paths.gameDataDir}/Persistent/audio_lang_14`;
const voiceDir = await constants.paths.voiceDir;
const langs = {
'English(US)': 'en-us',
'Japanese': 'ja-jp',
'Korean': 'ko-kr',
'Chinese': 'zn-cn'
};
let installedVoice: VoiceInfo = {
installed: [],
active: null
};
let installedVoices: InstalledVoice[] = [];
// Parse installed voice packages
Neutralino.filesystem.readDirectory(voiceDir)
@ -47,57 +46,41 @@ export default class Voice
files = files.filter((file) => file.type == 'DIRECTORY')
.map((file) => file.entry);
for (const folder of Object.keys(langs))
for (const folder of Object.values(this.langs))
if (files.includes(folder))
{
const voiceFiles: { entry: string, type: string }[] = await Neutralino.filesystem.readDirectory(`${voiceDir}/${folder}`);
const latestVoiceFile = voiceFiles.sort((a, b) => a.entry < b.entry ? -1 : 1).pop();
installedVoice.installed.push({
lang: langs[folder],
installedVoices.push({
lang: Object.keys(this.langs).find((lang) => this.langs[lang] === folder),
version: latestVoiceFile ? `${/_([\d]*\.[\d]*)_/.exec(latestVoiceFile.entry)![1]}.0` : null
} as InstalledVoiceInfo);
} as InstalledVoice);
}
parseActiveVoice();
resolveVoices();
})
.catch(() => parseActiveVoice());
.catch(() => resolveVoices());
// Parse active voice package
const parseActiveVoice = () => {
Neutralino.filesystem.readFile(persistentPath)
.then(async (lang) => {
const voiceFiles: { entry: string, type: string }[] = await Neutralino.filesystem.readDirectory(`${voiceDir}/${lang}`);
const resolveVoices = () => {
Debug.log({
function: 'Voice.current',
message: `Installed voices: ${installedVoices.map((voice) => `${voice.lang} (${voice.version})`).join(', ')}`
});
const latestVoiceFile = voiceFiles.sort((a, b) => a.entry < b.entry ? -1 : 1).pop();
installedVoice.active = {
lang: langs[lang] ?? null,
version: latestVoiceFile ? `${/_([\d]*\.[\d]*)_/.exec(latestVoiceFile.entry)![1]}.0` : null
} as InstalledVoiceInfo;
Debug.log({
function: 'Voice.current',
message: {
'active voice': `${installedVoice.active.lang} (${installedVoice.active.version})`,
'installed voices': installedVoice.installed.map((voice) => `${voice.lang} (${voice.version})`).join(', ')
}
});
resolve(installedVoice);
})
.catch(() => resolve(installedVoice));
resolve(installedVoices);
};
});
}
/**
* Get currently selected voice package language according to the config file
* Get currently selected voice packages according to the config file
*/
public static get selected(): Promise<VoiceLang>
public static get selected(): Promise<VoiceLang[]>
{
return Configs.get('lang.voice') as Promise<VoiceLang>;
return Configs.get('lang.voice') as Promise<VoiceLang[]>;
}
/**
@ -135,13 +118,13 @@ export default class Voice
* @returns null if the language or the version can't be found
* @returns rejects Error object if company's servers are unreachable or they responded with an error
*/
public static update(lang: string, version: string|null = null): Promise<Stream|null>
public static update(lang: VoiceLang, version: string|null = null): Promise<Stream|null>
{
Debug.log({
function: 'Voice.update',
message: version !== null ?
`Updating the voice package from the ${version} version` :
'Installing the voice package'
`Updating ${lang} voice package from the ${version} version` :
`Installing ${lang} voice package`
});
return new Promise((resolve, reject) => {
@ -176,6 +159,25 @@ export default class Voice
});
}
/**
* Delete specified voice package
*/
public static delete(lang: VoiceLang): Promise<void>
{
const debugThread = new DebugThread('Voice.delete', `Deleting ${this.langs[lang]} (${lang}) voice package`);
return new Promise(async (resolve) => {
Process.run(`rm -rf "${Process.addSlashes(await constants.paths.voiceDir + '/' + this.langs[lang])}"`)
.then((process) => {
process.finish(() => {
debugThread.log('Voice package deleted');
resolve();
});
});
});
}
/**
* Pre-download the game's voice update
*
@ -186,7 +188,7 @@ export default class Voice
*/
public static predownloadUpdate(lang: string, version: string|null = null): Promise<DownloadingStream|null>
{
const debugThread = new DebugThread('Voice.predownloadUpdate', 'Predownloading game voice data...')
const debugThread = new DebugThread('Voice.predownloadUpdate', `Predownloading ${lang} game voice data...`)
return new Promise((resolve) => {
Game.getLatestData()
@ -226,12 +228,25 @@ export default class Voice
/**
* Checks whether the update was downloaded or not
*/
public static isUpdatePredownloaded(lang: string): Promise<boolean>
public static isUpdatePredownloaded(lang: VoiceLang|VoiceLang[]): Promise<boolean>
{
return new Promise(async (resolve) => {
Neutralino.filesystem.getStats(`${await constants.paths.launcherDir}/voice-${lang}-predownloaded.zip`)
.then(() => resolve(true))
.catch(() => resolve(false));
if (typeof lang === 'string')
{
Neutralino.filesystem.getStats(`${await constants.paths.launcherDir}/voice-${lang}-predownloaded.zip`)
.then(() => resolve(true))
.catch(() => resolve(false));
}
else
{
let predownloaded = true;
for (const voiceLang of lang)
predownloaded &&= await this.isUpdatePredownloaded(voiceLang);
resolve(predownloaded);
}
});
}
}

View file

@ -78,12 +78,12 @@ class Stream
this.archive = info;
let command = {
tar: `tar -xvf '${Process.addSlashes(path)}'${unpackDir ? ` -C '${Process.addSlashes(unpackDir)}'` : ''}`,
zip: `unzip -o '${Process.addSlashes(path)}'${unpackDir ? ` -d '${Process.addSlashes(unpackDir)}'` : ''}`
tar: `tar -xvf "${Process.addSlashes(path)}"${unpackDir ? ` -C "${Process.addSlashes(unpackDir)}"` : ''}`,
zip: `unzip -o "${Process.addSlashes(path)}"${unpackDir ? ` -d "${Process.addSlashes(unpackDir)}"` : ''}`
}[this.archive.type!];
if (unpackDir)
command = `mkdir -p '${Process.addSlashes(unpackDir)}' && ${command}`;
command = `mkdir -p "${Process.addSlashes(unpackDir)}" && ${command}`;
let remainedFiles = this.archive.files;

View file

@ -143,7 +143,7 @@ export default class DXVK
const version = typeof dxvk !== 'string' ?
dxvk.version : dxvk;
Process.run(`rm -rf '${Process.addSlashes(await constants.paths.dxvksDir + '/dxvk-' + version)}'`)
Process.run(`rm -rf "${Process.addSlashes(await constants.paths.dxvksDir + '/dxvk-' + version)}"`)
.then((process) => {
process.finish(() => {
debugThread.log('Deletion completed');
@ -179,15 +179,15 @@ export default class DXVK
/**
* Make the installation script executable
*/
() => Neutralino.os.execCommand(`chmod +x '${dxvkDir}/setup_dxvk.sh'`),
() => Neutralino.os.execCommand(`chmod +x "${dxvkDir}/setup_dxvk.sh"`),
/**
* And then run it
*/
(): Promise<void> => new Promise(async (resolve) => {
const alias = runner ? `alias winecfg=\\'${runnerDir}/${runner.files.winecfg}\\'\\n` : '';
const alias = runner ? `alias winecfg=\\"${runnerDir}/${runner.files.winecfg}\\"\\n` : '';
Process.run(`eval $'${alias ? alias : ''}./setup_dxvk.sh install'`, {
Process.run(`eval $"${alias ? alias : ''}./setup_dxvk.sh install"`, {
cwd: dxvkDir,
env: {
WINE: runner ? `${runnerDir}/${runner.files.wine}` : 'wine',

56
src/ts/core/DiscordRPC.ts Normal file
View file

@ -0,0 +1,56 @@
import Process from '../neutralino/Process';
import type { Params } from '../types/DiscordRPC';
declare const NL_CWD;
export default class DiscordRPC
{
protected params: Params;
protected process?: Process;
public constructor(params: Params)
{
this.params = params;
let exec = [
`${NL_CWD}/public/discord-rpc/discord-rpc`,
`-a ${params.id}`
];
if (params.details)
exec = [...exec, `-d "${Process.addSlashes(params.details)}"`];
if (params.state)
exec = [...exec, `-s "${Process.addSlashes(params.state)}"`];
if (params.icon)
{
if (params.icon.large)
exec = [...exec, `-li "${params.icon.large}"`];
if (params.icon.small)
exec = [...exec, `-si "${params.icon.small}"`];
}
if (params.time)
{
if (params.time.start)
exec = [...exec, `-st ${params.time.start}`];
if (params.time.end)
exec = [...exec, `-et ${params.time.end}`];
}
Process.run(exec.join(' ')).then((process) => this.process = process);
}
/**
* Stop the discord rpc
*/
public stop(forced: boolean = false): Promise<void>
{
console.log(this.process);
return this.process!.kill(forced);
}
};

View file

@ -49,7 +49,7 @@ class Stream
if (this.onStart)
this.onStart();
const command = `curl -s -L -N -o '${Process.addSlashes(output)}' '${uri}'`;
const command = `curl -s -L -N -o "${Process.addSlashes(output)}" "${uri}"`;
Neutralino.os.execCommand(command, {
background: true

View file

@ -11,11 +11,11 @@ export default class Notifications
*/
public static show(options: NotificationsOptions)
{
let command = `notify-send '${Process.addSlashes(options.title)}' '${Process.addSlashes(options.body)}'`;
let command = `notify-send "${Process.addSlashes(options.title)}" "${Process.addSlashes(options.body)}"`;
// Specify notification icon
if (options.icon)
command += ` -i '${Process.addSlashes(options.icon)}'`;
command += ` -i "${Process.addSlashes(options.icon)}"`;
// Specify notification duration
if (options.duration)

View file

@ -52,7 +52,7 @@ export default class Prefix
.catch(() => {
Downloader.download(constants.uri.winetricks, winetricksPath).then((stream) => {
stream.finish(async () => {
await Neutralino.os.execCommand(`chmod +x '${Process.addSlashes(winetricksPath)}'`);
await Neutralino.os.execCommand(`chmod +x "${Process.addSlashes(winetricksPath)}"`);
resolve(winetricksPath);
});
@ -108,7 +108,7 @@ export default class Prefix
this.getWinetricks().then(async (winetricks) => {
let installationProgress = 0;
const process = await Process.run(`'${Process.addSlashes(winetricks)}' corefonts usetakefocus=n`, {
const process = await Process.run(`"${Process.addSlashes(winetricks)}" corefonts usetakefocus=n`, {
env: {
WINE: `${await constants.paths.runnersDir}/${runner.name}/${runner.files.wine}`,
WINESERVER: `${await constants.paths.runnersDir}/${runner.name}/${runner.files.wineserver}`,

View file

@ -149,7 +149,7 @@ class Runners
const name = typeof runner !== 'string' ?
runner.name : runner;
Process.run(`rm -rf '${Process.addSlashes(await constants.paths.runnersDir + '/' + name)}'`)
Process.run(`rm -rf "${Process.addSlashes(await constants.paths.runnersDir + '/' + name)}"`)
.then((process) => {
process.finish(() => {
debugThread.log('Runner deleted');

View file

@ -95,7 +95,7 @@ export default class ProgressBar
this.downloadedLabelElement.textContent = this.options!.label(current, total, difference);
// Otherwise update percents and totals if we should
else if (this.options!.showPercents || this.options!.showPercents)
else if (this.options!.showPercents || this.options!.showTotals)
{
this.downloadedLabelElement.textContent = this.options!.label;

View file

@ -253,7 +253,22 @@ export default class State
const gameCurrent = await Game.current;
const gameLatest = await Game.getLatestData();
const patch = await Patch.latest;
const voiceData = await Voice.current;
const installedVoices = await Voice.installed;
const selectedVoices = await Voice.selected;
let voiceUpdateRequired = installedVoices.length != selectedVoices.length || installedVoices.length === 0;
if (!voiceUpdateRequired)
{
for (const installedVoice of installedVoices)
if (installedVoice.version != gameCurrent || !selectedVoices.includes(installedVoice.lang))
{
voiceUpdateRequired = true;
break;
}
}
if (gameCurrent === null)
state = 'game-installation-available';
@ -261,8 +276,8 @@ export default class State
else if (gameCurrent != gameLatest.game.latest.version)
state = 'game-update-available';
// TODO: update this thing if the user selected another voice language
else if (voiceData.installed.length === 0)
// TODO: download default voice language if user removed all of them
else if (voiceUpdateRequired)
state = 'game-voice-update-required';
else if (!patch.applied)

View file

@ -28,8 +28,8 @@ export default (launcher: Launcher): Promise<void> => {
Prefix.create(prefixDir, (output, current, total) => {
progressLabel = output;
if (progressLabel.length > 80)
progressLabel = progressLabel.substring(0, 80) + '...';
if (progressLabel.length > 70)
progressLabel = progressLabel.substring(0, 70) + '...';
launcher.progressBar!.update(current, total, 1);
})

View file

@ -54,7 +54,7 @@ export default (launcher: Launcher): Promise<void> => {
stream?.unpackFinish(() => {
// Download voice package when the game itself has been installed
import('./InstallVoice').then((module) => {
module.default(launcher, prevGameVersion).then(() => resolve());
module.default(launcher).then(() => resolve());
});
});
});

View file

@ -1,43 +1,94 @@
import type Launcher from '../../Launcher';
import type { VoiceLang } from '../../types/Voice';
import Voice from '../../Voice';
import promisify from '../../core/promisify';
import Game from '../../Game';
export default (launcher: Launcher, prevGameVersion: string|null = null): Promise<void> => {
export default (launcher: Launcher): Promise<void> => {
return new Promise(async (resolve) => {
Voice.update(await Voice.selected, prevGameVersion).then((stream) => {
launcher.progressBar?.init({
label: 'Downloading voice package...',
showSpeed: true,
showEta: true,
showPercents: true,
showTotals: true
});
Voice.selected.then(async (selected: VoiceLang[]) => {
const installedVoices = await Voice.installed;
const currentVersion = await Game.current;
stream?.downloadStart(() => launcher.progressBar?.show());
let packagesToDelete: VoiceLang[] = [],
packagesVersions = {};
stream?.downloadProgress((current: number, total: number, difference: number) => {
launcher.progressBar?.update(current, total, difference);
});
for (const installedVoice of installedVoices)
{
packagesVersions[installedVoice.lang] = installedVoice.version;
stream?.unpackStart(() => {
if (!selected.includes(installedVoice.lang))
packagesToDelete.push(installedVoice.lang);
}
if (packagesToDelete.length > 0)
{
launcher.progressBar?.init({
label: 'Unpacking voice package...',
showSpeed: true,
showEta: true,
showPercents: true,
label: `Deleting voice packages...`,
showSpeed: false,
showEta: false,
showPercents: false,
showTotals: true
});
launcher.progressBar?.show();
for (let i = 0; i < packagesToDelete.length; ++i)
{
await Voice.delete(packagesToDelete[i]);
launcher.progressBar?.update(i + 1, packagesToDelete.length, 1);
}
}
const updateVoices = promisify({
callbacks: selected.map((selectedVoice: VoiceLang) => {
return (): Promise<void> => new Promise((resolve) => {
if (packagesVersions[selectedVoice] === currentVersion)
resolve();
else Voice.update(selectedVoice, packagesVersions[selectedVoice] ?? null).then((stream) => {
launcher.progressBar?.init({
label: `Downloading ${selectedVoice} voice package...`,
showSpeed: true,
showEta: true,
showPercents: true,
showTotals: true
});
stream?.downloadStart(() => launcher.progressBar?.show());
stream?.downloadProgress((current: number, total: number, difference: number) => {
launcher.progressBar?.update(current, total, difference);
});
stream?.unpackStart(() => {
launcher.progressBar?.init({
label: `Unpacking ${selectedVoice} voice package...`,
showSpeed: true,
showEta: true,
showPercents: true,
showTotals: true
});
});
stream?.unpackProgress((current: number, total: number, difference: number) => {
launcher.progressBar?.update(current, total, difference);
});
stream?.unpackFinish(() => {
launcher.progressBar?.hide();
resolve();
});
});
});
}),
interval: 3000
});
stream?.unpackProgress((current: number, total: number, difference: number) => {
launcher.progressBar?.update(current, total, difference);
});
stream?.unpackFinish(() => {
launcher.progressBar?.hide();
resolve();
});
updateVoices.then(() => resolve());
});
});
};

View file

@ -114,7 +114,7 @@ export default (): Promise<void> => {
else console.warn(`GPU ${LauncherLib.getConfig('gpu')} not found. Launching on the default GPU`);
}*/
let command = `'${Process.addSlashes(wineExeutable)}' ${await Configs.get('fps_unlocker') ? 'unlockfps.bat' : 'launcher.bat'}`;
let command = `"${Process.addSlashes(wineExeutable)}" ${await Configs.get('fps_unlocker') ? 'unlockfps.bat' : 'launcher.bat'}`;
/**
* Gamemode integration

View file

@ -21,7 +21,7 @@ export default (launcher: Launcher): Promise<void> => {
Game.predownloadUpdate(prevGameVersion).then((stream) => {
launcher.progressBar?.init({
label: 'Downloading game...',
label: 'Pre-downloading game...',
showSpeed: true,
showEta: true,
showPercents: true,
@ -37,7 +37,7 @@ export default (launcher: Launcher): Promise<void> => {
stream?.finish(() => {
// Predownload voice package when the game itself has been downloaded
import('./PredownloadVoice').then((module) => {
module.default(launcher, prevGameVersion).then(() => resolve());
module.default(launcher).then(() => resolve());
});
});
});

View file

@ -1,32 +1,47 @@
import type Launcher from '../../Launcher';
import type { VoiceLang } from '../../types/Voice';
import Voice from '../../Voice';
import Game from '../../Game';
import promisify from '../../core/promisify';
export default (launcher: Launcher, prevGameVersion: string|null = null): Promise<void> => {
export default (launcher: Launcher): Promise<void> => {
return new Promise(async (resolve) => {
prevGameVersion ??= await Game.current;
let packagesVersions = {};
Voice.predownloadUpdate(await Voice.selected, prevGameVersion).then((stream) => {
launcher.progressBar?.init({
label: 'Downloading voice package...',
showSpeed: true,
showEta: true,
showPercents: true,
showTotals: true
for (const installedVoice of await Voice.installed)
packagesVersions[installedVoice.lang] = installedVoice.version;
Voice.selected.then(async (selected: VoiceLang[]) => {
const updateVoices = promisify({
callbacks: selected.map((selectedVoice: VoiceLang) => {
return (): Promise<void> => new Promise((resolve) => {
Voice.predownloadUpdate(selectedVoice, packagesVersions[selectedVoice] ?? null).then((stream) => {
launcher.progressBar?.init({
label: `Pre-downloading ${selectedVoice} voice package...`,
showSpeed: true,
showEta: true,
showPercents: true,
showTotals: true
});
stream?.start(() => launcher.progressBar?.show());
stream?.progress((current: number, total: number, difference: number) => {
launcher.progressBar?.update(current, total, difference);
});
stream?.finish(() => {
launcher.progressBar?.hide();
resolve();
});
});
});
}),
interval: 3000
});
stream?.start(() => launcher.progressBar?.show());
stream?.progress((current: number, total: number, difference: number) => {
launcher.progressBar?.update(current, total, difference);
});
stream?.finish(() => {
launcher.progressBar?.hide();
resolve();
});
updateVoices.then(() => resolve());
});
});
};

View file

@ -180,26 +180,40 @@ class Process
{
return new Promise(async (resolve) => {
const tmpFile = `${await constants.paths.launcherDir}/${10000 + Math.round(Math.random() * 89999)}.tmp`;
const originalCommand = command.replaceAll(/\\|"|'/gm, '');
// Set env variables
if (options.env)
{
Object.keys(options.env).forEach((key) => {
command = `${key}='${this.addSlashes(options.env![key].toString())}' ${command}`;
command = `${key}="${this.addSlashes(options.env![key].toString())}" ${command}`;
});
}
// Set output redirection to the temp file
command = `${command} > '${this.addSlashes(tmpFile)}' 2>&1`;
command = `${command} > "${this.addSlashes(tmpFile)}" 2>&1 </dev/null &`;
// Set current working directory
if (options.cwd)
command = `cd '${this.addSlashes(options.cwd)}' && ${command} && cd -`;
command = `cd "${this.addSlashes(options.cwd)}" && ${command}`;
// And run the command
const process = await Neutralino.os.execCommand(command, {
background: true
});
const process = await Neutralino.os.execCommand(command);
// Because we're redirecting process output to the file
// it creates another process and our process.pid is not correct
// so we need to find real process id
const processes = ((await Neutralino.os.execCommand('ps -a -S')).stdOut as string).split(/\r\n|\r|\n/);
let processId = process.pid;
for (const line of processes)
if (line.replaceAll(/\\|"|'/gm, '').includes(originalCommand))
{
processId = parseInt(line.split(' ').filter((word) => word != '')[0]);
break;
}
Debug.log({
function: 'Process.run',
@ -210,7 +224,7 @@ class Process
}
});
resolve(new Process(process.pid, tmpFile));
resolve(new Process(processId, tmpFile));
});
}
@ -227,7 +241,7 @@ class Process
*/
public static addSlashes(str: string): string
{
return str.replaceAll('\\', '\\\\').replaceAll('\'', '\\\'');
return str.replaceAll('\\', '\\\\').replaceAll('"', '\\"');
}
}

17
src/ts/types/DiscordRPC.d.ts vendored Normal file
View file

@ -0,0 +1,17 @@
type Params = {
id: string;
details?: string;
state?: string;
icon?: {
large?: string;
small?: string;
};
time?: {
start?: number;
end?: number;
};
};
export type { Params };

View file

@ -4,18 +4,12 @@ type VoiceLang =
| 'ja-jp'
| 'ko-kr';
type InstalledVoiceInfo = {
type InstalledVoice = {
lang: VoiceLang;
version: string|null;
};
type VoiceInfo = {
installed: InstalledVoiceInfo[];
active: InstalledVoiceInfo|null;
};
export type {
VoiceLang,
InstalledVoiceInfo,
VoiceInfo
InstalledVoice
};