mirror of
https://github.com/an-anime-team/an-anime-game-launcher.git
synced 2025-03-17 21:50:11 +03:00
Merge request !5 implementation and more
- added Lutris 6.21-5 runner - added `recommendable` field to the runners (for future feature) - removed PrefixSelector class which functionality now partially moved as an anonymous class in the `constants.prefixDir` property - fixed temp winetricks and winecfg buttons disabling - made `LauncherLib.getGameVersion` method to gather game version from its files
This commit is contained in:
parent
1cdc553c25
commit
b89eccd863
8 changed files with 155 additions and 118 deletions
20
entry.js
20
entry.js
|
@ -121,14 +121,24 @@ app.whenReady().then(() => {
|
|||
mainWindow.webContents.send('change-voicepack');
|
||||
});
|
||||
|
||||
ipcMain.on('prefix-con', async () => {
|
||||
const result = await dialog.showOpenDialog({ properties: ['openDirectory'] });
|
||||
if(result.filePaths.length == 0) return;
|
||||
mainWindow.webContents.send('change-prefix', { 'type': 'change', 'dir': result.filePaths[0] });
|
||||
ipcMain.on('prefix-select', async () => {
|
||||
const result = await dialog.showOpenDialog({
|
||||
properties: ['openDirectory']
|
||||
});
|
||||
|
||||
if (result.filePaths.length > 0)
|
||||
{
|
||||
mainWindow.webContents.send('change-prefix', {
|
||||
'type': 'change',
|
||||
'dir': result.filePaths[0]
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on('prefix-reset', async () => {
|
||||
mainWindow.webContents.send('change-prefix', { 'type': 'reset' });
|
||||
mainWindow.webContents.send('change-prefix', {
|
||||
'type': 'reset'
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on('prefix-changed', async () => {
|
||||
|
|
|
@ -54,6 +54,6 @@
|
|||
"LauncherUpdateTitle": "Доступно обновление лаунчера: ",
|
||||
"LauncherUpdateBody": "Вы можете скачать новую версию лаунчера с репозитория проекта: {uri.launcher}",
|
||||
"TelemetryNotDisabled": "Серверы сбора телеметрии {placeholders.uppercase.company} не отключены!",
|
||||
"DefPrefix": "Reset to Default",
|
||||
"ChangePrefix": "Change Prefix"
|
||||
"DefPrefix": "Сбросить до умолчания",
|
||||
"ChangePrefix": "Изменить префикс"
|
||||
}
|
|
@ -2,6 +2,16 @@
|
|||
{
|
||||
"title": "Lutris",
|
||||
"runners": [
|
||||
{
|
||||
"name": "Lutris 6.21-5",
|
||||
"version": "6.21-5",
|
||||
"uri": "https://github.com/lutris/wine/releases/download/lutris-6.21-5/wine-lutris-6.21-5-x86_64.tar.xz",
|
||||
"archive": "tar",
|
||||
"folder": "lutris-6.21-5-x86_64",
|
||||
"makeFolder": false,
|
||||
"executable": "bin/wine64",
|
||||
"recommendable": false
|
||||
},
|
||||
{
|
||||
"name": "Lutris 6.21-4",
|
||||
"version": "6.21-4",
|
||||
|
@ -9,7 +19,8 @@
|
|||
"archive": "tar",
|
||||
"folder": "lutris-6.21-4-x86_64",
|
||||
"makeFolder": false,
|
||||
"executable": "bin/wine64"
|
||||
"executable": "bin/wine64",
|
||||
"recommendable": false
|
||||
},
|
||||
{
|
||||
"name": "Lutris 6.21-3",
|
||||
|
@ -18,7 +29,8 @@
|
|||
"archive": "tar",
|
||||
"folder": "lutris-6.21-3-x86_64",
|
||||
"makeFolder": false,
|
||||
"executable": "bin/wine64"
|
||||
"executable": "bin/wine64",
|
||||
"recommendable": false
|
||||
},
|
||||
{
|
||||
"name": "Lutris 6.21-2",
|
||||
|
@ -27,7 +39,8 @@
|
|||
"archive": "tar",
|
||||
"folder": "lutris-6.21-2-x86_64",
|
||||
"makeFolder": false,
|
||||
"executable": "bin/wine64"
|
||||
"executable": "bin/wine64",
|
||||
"recommendable": false
|
||||
},
|
||||
{
|
||||
"name": "Lutris 6.21",
|
||||
|
@ -36,7 +49,8 @@
|
|||
"archive": "tar",
|
||||
"folder": "lutris-6.21-x86_64",
|
||||
"makeFolder": false,
|
||||
"executable": "bin/wine64"
|
||||
"executable": "bin/wine64",
|
||||
"recommendable": false
|
||||
},
|
||||
{
|
||||
"name": "Lutris 6.14-4",
|
||||
|
@ -45,7 +59,8 @@
|
|||
"archive": "tar",
|
||||
"folder": "lutris-6.14-4-x86_64",
|
||||
"makeFolder": false,
|
||||
"executable": "bin/wine64"
|
||||
"executable": "bin/wine64",
|
||||
"recommendable": true
|
||||
},
|
||||
{
|
||||
"name": "Lutris 6.14-3",
|
||||
|
@ -54,7 +69,8 @@
|
|||
"archive": "tar",
|
||||
"folder": "lutris-6.14-3-x86_64",
|
||||
"makeFolder": false,
|
||||
"executable": "bin/wine64"
|
||||
"executable": "bin/wine64",
|
||||
"recommendable": true
|
||||
},
|
||||
{
|
||||
"name": "Lutris 6.14-2",
|
||||
|
@ -63,7 +79,8 @@
|
|||
"archive": "tar",
|
||||
"folder": "lutris-6.14-2-x86_64",
|
||||
"makeFolder": false,
|
||||
"executable": "bin/wine64"
|
||||
"executable": "bin/wine64",
|
||||
"recommendable": true
|
||||
},
|
||||
{
|
||||
"name": "Lutris 6.14",
|
||||
|
@ -72,7 +89,8 @@
|
|||
"archive": "tar",
|
||||
"folder": "lutris-6.14-x86_64",
|
||||
"makeFolder": false,
|
||||
"executable": "bin/wine64"
|
||||
"executable": "bin/wine64",
|
||||
"recommendable": true
|
||||
},
|
||||
{
|
||||
"name": "Lutris 6.13-3",
|
||||
|
@ -81,7 +99,8 @@
|
|||
"archive": "tar",
|
||||
"folder": "lutris-6.13-3-x86_64",
|
||||
"makeFolder": false,
|
||||
"executable": "bin/wine64"
|
||||
"executable": "bin/wine64",
|
||||
"recommendable": false
|
||||
},
|
||||
{
|
||||
"name": "Lutris 6.13-2",
|
||||
|
@ -90,7 +109,8 @@
|
|||
"archive": "tar",
|
||||
"folder": "lutris-6.13-2-x86_64",
|
||||
"makeFolder": false,
|
||||
"executable": "bin/wine64"
|
||||
"executable": "bin/wine64",
|
||||
"recommendable": false
|
||||
},
|
||||
{
|
||||
"name": "Lutris 6.13",
|
||||
|
@ -99,7 +119,8 @@
|
|||
"archive": "tar",
|
||||
"folder": "lutris-6.13-x86_64",
|
||||
"makeFolder": false,
|
||||
"executable": "bin/wine64"
|
||||
"executable": "bin/wine64",
|
||||
"recommendable": false
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -113,7 +134,8 @@
|
|||
"archive": "tar",
|
||||
"folder": "Proton-6.21-GE-2",
|
||||
"makeFolder": false,
|
||||
"executable": "files/bin/wine64"
|
||||
"executable": "files/bin/wine64",
|
||||
"recommendable": false
|
||||
},
|
||||
{
|
||||
"name": "Proton-6.20-GE-1",
|
||||
|
@ -122,7 +144,8 @@
|
|||
"archive": "tar",
|
||||
"folder": "Proton-6.20-GE-1",
|
||||
"makeFolder": false,
|
||||
"executable": "files/bin/wine64"
|
||||
"executable": "files/bin/wine64",
|
||||
"recommendable": true
|
||||
},
|
||||
{
|
||||
"name": "Proton-6.19-GE-2",
|
||||
|
@ -131,7 +154,8 @@
|
|||
"archive": "tar",
|
||||
"folder": "Proton-6.19-GE-2",
|
||||
"makeFolder": false,
|
||||
"executable": "files/bin/wine64"
|
||||
"executable": "files/bin/wine64",
|
||||
"recommendable": false
|
||||
},
|
||||
{
|
||||
"name": "Proton-6.19-GE-1",
|
||||
|
@ -140,7 +164,8 @@
|
|||
"archive": "tar",
|
||||
"folder": "Proton-6.19-GE-1",
|
||||
"makeFolder": false,
|
||||
"executable": "files/bin/wine64"
|
||||
"executable": "files/bin/wine64",
|
||||
"recommendable": false
|
||||
},
|
||||
{
|
||||
"name": "Proton-6.18-GE-2",
|
||||
|
@ -149,7 +174,8 @@
|
|||
"archive": "tar",
|
||||
"folder": "Proton-6.18-GE-2",
|
||||
"makeFolder": false,
|
||||
"executable": "files/bin/wine64"
|
||||
"executable": "files/bin/wine64",
|
||||
"recommendable": false
|
||||
},
|
||||
{
|
||||
"name": "Proton-6.18-GE-1",
|
||||
|
@ -158,7 +184,8 @@
|
|||
"archive": "tar",
|
||||
"folder": "Proton-6.18-GE-1",
|
||||
"makeFolder": false,
|
||||
"executable": "files/bin/wine64"
|
||||
"executable": "files/bin/wine64",
|
||||
"recommendable": false
|
||||
},
|
||||
{
|
||||
"name": "Proton-6.16-GE-1",
|
||||
|
@ -167,7 +194,8 @@
|
|||
"archive": "tar",
|
||||
"folder": "Proton-6.16-GE-1",
|
||||
"makeFolder": false,
|
||||
"executable": "files/bin/wine64"
|
||||
"executable": "files/bin/wine64",
|
||||
"recommendable": false
|
||||
},
|
||||
{
|
||||
"name": "Proton-6.15-GE-2",
|
||||
|
@ -176,7 +204,8 @@
|
|||
"archive": "tar",
|
||||
"folder": "Proton-6.15-GE-2",
|
||||
"makeFolder": false,
|
||||
"executable": "files/bin/wine64"
|
||||
"executable": "files/bin/wine64",
|
||||
"recommendable": false
|
||||
},
|
||||
{
|
||||
"name": "Proton-6.15-GE-1",
|
||||
|
@ -185,7 +214,8 @@
|
|||
"archive": "tar",
|
||||
"folder": "Proton-6.15-GE-1",
|
||||
"makeFolder": false,
|
||||
"executable": "files/bin/wine64"
|
||||
"executable": "files/bin/wine64",
|
||||
"recommendable": false
|
||||
},
|
||||
{
|
||||
"name": "Proton-6.14-GE-2",
|
||||
|
@ -194,7 +224,8 @@
|
|||
"archive": "tar",
|
||||
"folder": "Proton-6.14-GE-2",
|
||||
"makeFolder": false,
|
||||
"executable": "files/bin/wine64"
|
||||
"executable": "files/bin/wine64",
|
||||
"recommendable": false
|
||||
},
|
||||
{
|
||||
"name": "Proton-6.14-GE-1",
|
||||
|
@ -203,7 +234,8 @@
|
|||
"archive": "tar",
|
||||
"folder": "Proton-6.14-GE-1",
|
||||
"makeFolder": false,
|
||||
"executable": "files/bin/wine64"
|
||||
"executable": "files/bin/wine64",
|
||||
"recommendable": false
|
||||
},
|
||||
{
|
||||
"name": "Proton-5.21-GE-1",
|
||||
|
@ -212,7 +244,8 @@
|
|||
"archive": "tar",
|
||||
"folder": "Proton-5.21-GE-1",
|
||||
"makeFolder": false,
|
||||
"executable": "files/bin/wine64"
|
||||
"executable": "files/bin/wine64",
|
||||
"recommendable": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -13,13 +13,12 @@ import LauncherLib from './lib/LauncherLib';
|
|||
import LauncherUI from './lib/LauncherUI';
|
||||
import Tools from './lib/Tools';
|
||||
import DiscordRPC from './lib/DiscordRPC';
|
||||
import PrefixSelector from './lib/PrefixSelector';
|
||||
import SwitcherooControl from './lib/SwitcherooControl';
|
||||
|
||||
const launcher_version = require('../../package.json').version;
|
||||
|
||||
if (!fs.existsSync(LauncherLib.getConfig('prefix')))
|
||||
fs.mkdirSync(LauncherLib.getConfig('prefix'), { recursive: true });
|
||||
if (!fs.existsSync(constants.prefixDir.get()))
|
||||
fs.mkdirSync(constants.prefixDir.get(), { recursive: true });
|
||||
|
||||
if (!fs.existsSync(constants.runnersDir))
|
||||
fs.mkdirSync(constants.runnersDir, { recursive: true });
|
||||
|
@ -61,9 +60,21 @@ $(() => {
|
|||
});
|
||||
|
||||
ipcRenderer.on('change-prefix', (event: void, data: any) => {
|
||||
if(data.type == 'change') PrefixSelector.set(data.dir);
|
||||
if(data.type == 'reset') PrefixSelector.Default();
|
||||
switch (data.type)
|
||||
{
|
||||
case 'change':
|
||||
constants.prefixDir.set(data.dir);
|
||||
|
||||
break;
|
||||
|
||||
case 'reset':
|
||||
constants.prefixDir.set(constants.prefixDir.getDefault());
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
LauncherUI.updateLauncherState();
|
||||
|
||||
ipcRenderer.send('prefix-changed');
|
||||
});
|
||||
|
||||
|
@ -149,14 +160,14 @@ $(() => {
|
|||
}
|
||||
|
||||
// Creating wine prefix
|
||||
if (!LauncherLib.isPrefixInstalled(constants.prefixDir))
|
||||
if (!LauncherLib.isPrefixInstalled(constants.prefixDir.get()))
|
||||
{
|
||||
console.log(`%c> Creating wineprefix...`, 'font-size: 16px');
|
||||
|
||||
$('#launch').css('display', 'none');
|
||||
$('#downloader-panel').css('display', 'block');
|
||||
|
||||
await LauncherLib.installPrefix(constants.prefixDir, (output: string, current: number, total: number) => {
|
||||
await LauncherLib.installPrefix(constants.prefixDir.get(), (output: string, current: number, total: number) => {
|
||||
output = output.trim();
|
||||
|
||||
console.log(output);
|
||||
|
|
|
@ -227,6 +227,28 @@ export default class LauncherLib
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param dataLocation path to the [An Anime Game]_Data folder
|
||||
*/
|
||||
public static getGameVersion(dataLocation: string): string|null
|
||||
{
|
||||
const persistentPath = path.join(dataLocation, 'Persistent');
|
||||
const globalGameManagersPath = path.join(dataLocation, 'globalgamemanagers');
|
||||
|
||||
if (fs.existsSync(persistentPath))
|
||||
return fs.readFileSync(path.join(persistentPath, 'ScriptVersion'), { encoding: 'UTF-8' }).toString();
|
||||
|
||||
else if (fs.existsSync(globalGameManagersPath))
|
||||
{
|
||||
const config = fs.readFileSync(globalGameManagersPath, { encoding: 'ascii' });
|
||||
const version = /([1-9]+\.[0-9]+\.[0-9]+)_[\d]+_[\d]+/.exec(config);
|
||||
|
||||
return version !== null ? version[1] : null;
|
||||
}
|
||||
|
||||
else return null;
|
||||
}
|
||||
|
||||
// WINEPREFIX='...../wineprefix' winetricks corefonts usetakefocus=n
|
||||
public static async installPrefix (prefixPath: string, progress: (output: string, current: number, total: number) => void): Promise<void>
|
||||
{
|
||||
|
|
|
@ -1,71 +0,0 @@
|
|||
const fs = require('fs');
|
||||
import LauncherLib from "./LauncherLib";
|
||||
import constants from "./constants";
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
export default class PrefixSelector
|
||||
{
|
||||
protected static prefix: string = LauncherLib.getConfig('prefix');
|
||||
|
||||
public static set(location: string) {
|
||||
if (this.prefix == location) return console.log('Can\'t set already selected prefix as new prefix');
|
||||
|
||||
if (fs.existsSync(path.join(location, 'drive_c', 'Program Files', 'Genshin Impact', 'GenshinImpact_Data', 'Persistent'))) {
|
||||
const version = fs.readFileSync(path.join(location, 'drive_c', 'Program Files', 'Genshin Impact', 'GenshinImpact_Data', 'Persistent', 'ScriptVersion'), { encoding: 'UTF-8' }).toString();
|
||||
|
||||
LauncherLib.updateConfig('version', version);
|
||||
LauncherLib.updateConfig('prefix', location);
|
||||
constants.prefixDir = location;
|
||||
this.prefix = location;
|
||||
} else if (fs.existsSync(path.join(location, 'drive_c', 'Program Files', 'Genshin Impact', 'GenshinImpact_Data', 'globalgamemanagers'))) {
|
||||
const config = fs.readFileSync(path.join(location, 'drive_c', 'Program Files', 'Genshin Impact', 'GenshinImpact_Data', 'globalgamemanagers'), { encoding: 'ascii' });
|
||||
const version = /([1-9]+\.[0-9]+\.[0-9]+)_[\d]+_[\d]+/.exec(config)![1];
|
||||
|
||||
LauncherLib.updateConfig('version', version);
|
||||
LauncherLib.updateConfig('prefix', location);
|
||||
constants.prefixDir = location;
|
||||
this.prefix = location;
|
||||
} else {
|
||||
console.log('Game not found.');
|
||||
|
||||
// Unset version if game is not found.
|
||||
LauncherLib.updateConfig('version', null);
|
||||
LauncherLib.updateConfig('prefix', location);
|
||||
constants.prefixDir = location;
|
||||
this.prefix = location;
|
||||
}
|
||||
}
|
||||
|
||||
public static Default() {
|
||||
const dp = path.join(os.homedir(), '.local', 'share', 'anime-game-launcher', 'game');
|
||||
|
||||
if (this.prefix == dp) return console.log('Can\'t set already selected prefix as new prefix');
|
||||
|
||||
if (fs.existsSync(path.join(dp, 'drive_c', 'Program Files', 'Genshin Impact', 'GenshinImpact_Data', 'Persistent'))) {
|
||||
const version = fs.readFileSync(path.join(dp, 'drive_c', 'Program Files', 'Genshin Impact', 'GenshinImpact_Data', 'Persistent', 'ScriptVersion'), { encoding: 'UTF-8' }).toString();
|
||||
|
||||
LauncherLib.updateConfig('version', version);
|
||||
LauncherLib.updateConfig('prefix', dp);
|
||||
constants.prefixDir = dp;
|
||||
this.prefix = dp;
|
||||
} else if (fs.existsSync(path.join(dp, 'drive_c', 'Program Files', 'Genshin Impact', 'GenshinImpact_Data', 'globalgamemanagers'))) {
|
||||
const config = fs.readFileSync(path.join(dp, 'drive_c', 'Program Files', 'Genshin Impact', 'GenshinImpact_Data', 'globalgamemanagers'), { encoding: 'ascii' });
|
||||
const version = /([1-9]+\.[0-9]+\.[0-9]+)_[\d]+_[\d]+/.exec(config)![1];
|
||||
|
||||
LauncherLib.updateConfig('version', version);
|
||||
LauncherLib.updateConfig('prefix', dp);
|
||||
constants.prefixDir = dp;
|
||||
this.prefix = dp;
|
||||
} else {
|
||||
console.log('Game not found.');
|
||||
|
||||
// Unset version if game is not found.
|
||||
LauncherLib.updateConfig('version', null);
|
||||
LauncherLib.updateConfig('prefix', dp);
|
||||
constants.prefixDir = dp;
|
||||
this.prefix = dp;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
const path = require('path');
|
||||
const os = require('os');
|
||||
const fs = require('fs');
|
||||
|
||||
import LauncherLib from "./LauncherLib";
|
||||
|
||||
export default class constants
|
||||
|
@ -45,10 +47,6 @@ export default class constants
|
|||
|
||||
public static readonly launcherDir: string = path.join(os.homedir(), '.local', 'share', 'anime-game-launcher');
|
||||
|
||||
public static prefixDir: string = LauncherLib.getConfig('prefix');
|
||||
public static readonly gameDir: string = path.join(this.prefixDir, 'drive_c', 'Program Files', this.placeholders.uppercase.full);
|
||||
public static readonly voiceDir: string = path.join(this.gameDir, `${this.placeholders.uppercase.first + this.placeholders.uppercase.second}_Data`, 'StreamingAssets', 'Audio', 'GeneratedSoundBanks', 'Windows');
|
||||
|
||||
public static readonly runnersDir: string = path.join(this.launcherDir, 'runners');
|
||||
public static readonly dxvksDir: string = path.join(this.launcherDir, 'dxvks');
|
||||
|
||||
|
@ -59,4 +57,38 @@ export default class constants
|
|||
|
||||
public static readonly runnersUri: string = `${this.uri.launcher}/raw/main/runners.json`;
|
||||
public static readonly dxvksUri: string = `${this.uri.launcher}/raw/main/dxvks.json`;
|
||||
|
||||
public static prefixDir = new class
|
||||
{
|
||||
public get(): string
|
||||
{
|
||||
return LauncherLib.getConfig('prefix');
|
||||
}
|
||||
|
||||
public getDefault(): string
|
||||
{
|
||||
return path.join(os.homedir(), '.local', 'share', 'anime-game-launcher', 'game');
|
||||
}
|
||||
|
||||
public set(location: string)
|
||||
{
|
||||
if (path.relative(LauncherLib.getConfig('prefix'), location) === '')
|
||||
return console.log('Can\'t set already selected prefix as new prefix');
|
||||
|
||||
const dataPath = path.join(location, 'drive_c', 'Program Files', constants.placeholders.uppercase.full, `${constants.placeholders.uppercase.first + constants.placeholders.uppercase.second}_Data`);
|
||||
|
||||
LauncherLib.updateConfig('prefix', location);
|
||||
LauncherLib.updateConfig('version', LauncherLib.getGameVersion(dataPath));
|
||||
}
|
||||
}
|
||||
|
||||
public static get gameDir(): string
|
||||
{
|
||||
return path.join(this.prefixDir.get(), 'drive_c', 'Program Files', this.placeholders.uppercase.full);
|
||||
}
|
||||
|
||||
public static get voiceDir(): string
|
||||
{
|
||||
return path.join(this.gameDir, `${this.placeholders.uppercase.first + this.placeholders.uppercase.second}_Data`, 'StreamingAssets', 'Audio', 'GeneratedSoundBanks', 'Windows');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,15 +62,15 @@ $(() => {
|
|||
|
||||
ipcRenderer.on('prefix-changed', () => {
|
||||
$('#prefixloc #currentprefix').text(LauncherLib.getConfig('prefix'));
|
||||
})
|
||||
});
|
||||
|
||||
$('#prefixloc #prefixdir').on('click', () => {
|
||||
ipcRenderer.send('prefix-con');
|
||||
})
|
||||
ipcRenderer.send('prefix-select');
|
||||
});
|
||||
|
||||
$('#prefixloc #defprefix').on('click', () => {
|
||||
ipcRenderer.send('prefix-reset');
|
||||
})
|
||||
});
|
||||
|
||||
/**
|
||||
* Game voice language
|
||||
|
@ -121,7 +121,7 @@ $(() => {
|
|||
/**
|
||||
* winetricks button
|
||||
*/
|
||||
if (!commandExists('winetricks'))
|
||||
if (commandExists('winetricks'))
|
||||
{
|
||||
$('#general-action-buttons #winetricks').on('click', () => {
|
||||
exec('winetricks', {
|
||||
|
@ -144,7 +144,7 @@ $(() => {
|
|||
/**
|
||||
* winecfg button
|
||||
*/
|
||||
if (!commandExists('winecfg'))
|
||||
if (commandExists('winecfg'))
|
||||
{
|
||||
$('#general-action-buttons #winecfg').on('click', () => {
|
||||
exec('winecfg', {
|
||||
|
|
Loading…
Add table
Reference in a new issue