mirror of
https://github.com/an-anime-team/an-anime-game-launcher.git
synced 2025-03-15 20:51:39 +03:00
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:
parent
d90f339eb1
commit
db6c219776
35 changed files with 478 additions and 173 deletions
|
@ -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/",
|
||||
|
|
|
@ -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
BIN
public/discord-rpc/discord-rpc
Executable file
Binary file not shown.
BIN
public/discord-rpc/libdiscord-rpc.so
Normal file
BIN
public/discord-rpc/libdiscord-rpc.so
Normal file
Binary file not shown.
BIN
repository-pics/logos/2.3.0-2.jpg
Normal file
BIN
repository-pics/logos/2.3.0-2.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 111 KiB |
|
@ -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
|
||||
|
|
81
src/components/DropdownCheckboxes.svelte
Normal file
81
src/components/DropdownCheckboxes.svelte
Normal 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>
|
|
@ -6,7 +6,9 @@ promisify(async () => {
|
|||
Configs.defaults({
|
||||
lang: {
|
||||
launcher: 'en-us',
|
||||
voice: 'en-us'
|
||||
voice: [
|
||||
'en-us'
|
||||
]
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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')!
|
||||
});
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
@import "components/checkbox"
|
||||
@import "components/selectionBox"
|
||||
@import "components/dropdownCheckboxes"
|
||||
@import "components/selectionList"
|
||||
|
||||
@mixin themable($theme-name, $theme-map)
|
||||
|
|
17
src/sass/components/dropdownCheckboxes.sass
Normal file
17
src/sass/components/dropdownCheckboxes.sass
Normal 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)
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
})
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
|
123
src/ts/Voice.ts
123
src/ts/Voice.ts
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
56
src/ts/core/DiscordRPC.ts
Normal 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);
|
||||
}
|
||||
};
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}`,
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
})
|
||||
|
|
|
@ -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());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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());
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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());
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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
17
src/ts/types/DiscordRPC.d.ts
vendored
Normal 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 };
|
10
src/ts/types/Voice.d.ts
vendored
10
src/ts/types/Voice.d.ts
vendored
|
@ -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
|
||||
};
|
||||
|
|
Loading…
Add table
Reference in a new issue