mirror of
https://github.com/an-anime-team/an-anime-game-launcher.git
synced 2025-03-17 21:50:11 +03:00
1.4.1
- added statistics section for now launcher stores time spent in the game - added Korean machine translation - fixed an issue with analytics dialogue - improved new launcher versions finder algorithm
This commit is contained in:
parent
dbf8540946
commit
23d2a9a9f7
24 changed files with 152 additions and 69 deletions
|
@ -10,7 +10,7 @@
|
|||
|
||||
| Game version | Launcher version | Patch version |
|
||||
| :---: | :---: | :---: |
|
||||
| 2.2.0 | 1.4.0 | 2.2.0 stable ✅ |
|
||||
| 2.2.0 | 1.4.1 | 2.2.0 stable ✅ |
|
||||
|
||||
Download from [Releases](https://notabug.org/nobody/an-anime-game-launcher/releases)
|
||||
|
||||
|
@ -89,14 +89,15 @@ npm start
|
|||
* <s>Add installed packages deletion</s> *(1.2.0)*
|
||||
* <s>Add voice packs support</s> (Thank @Maroxy for the developments in the previous versions) *(1.3.0)*
|
||||
* <s>Color variants for progress bar's downloading text dependent on the background picture primary color</s> *(1.4.0)*
|
||||
* <s>Playing statistics</s> *(1.4.1)*
|
||||
* Make force launch button when the launcher's repository is unavailable
|
||||
* Screenshots explorer
|
||||
* Add vkBasalt support and "shaders library
|
||||
* Add vkBasalt support and "shaders library" (the problem is, we don't have vkBasalt binaries, but the source code)
|
||||
* Set default wine version to download so the wine install requirement is no longer needed
|
||||
* Add Patch category in settings menu with
|
||||
- Always participate in patches testing
|
||||
- Applying anti login crash patch
|
||||
- Remove patch
|
||||
* Playing statistics
|
||||
|
||||
And don't forget to change the patch's URI when it will be changed
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "an-anime-game-linux-launcher",
|
||||
"version": "1.4.0",
|
||||
"version": "1.4.1",
|
||||
"description": "An Anime Game Linux Launcher",
|
||||
"author": "Nikita Podvirnyy <suimin.tu.mu.ga.mi@gmail.com>",
|
||||
"contributors": [
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
<div class="menu-item" anchor="runners" i18id="WineVersion">Wine version</div>
|
||||
<div class="menu-item" anchor="dxvks" i18id="DXVK">DXVK</div>
|
||||
<div class="menu-item" anchor="environment" i18id="Environment">Environment</div>
|
||||
<div class="menu-item" anchor="statistics" i18id="Statistics">Statistics</div>
|
||||
</div>
|
||||
|
||||
<div class="settings">
|
||||
|
@ -130,6 +131,16 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-item" id="statistics">
|
||||
<h2 i18id="Statistics">Statistics</h2>
|
||||
|
||||
<div>
|
||||
<span i18id="YouPlayedFor">You've played for</span>
|
||||
<span id="play-hours"></span>
|
||||
<span i18id="hours">hours</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -16,6 +16,9 @@
|
|||
"Value": "Wert",
|
||||
"Add": "Hinzufügen",
|
||||
"Delete": "Löschen",
|
||||
"Statistics": "Statistics",
|
||||
"YouPlayedFor": "You've played for",
|
||||
"hours": "hours",
|
||||
"Downloading": "wird Heruntergeladen",
|
||||
"Unpack": "wird Entpackt",
|
||||
"GameDownloaded": "Spiel würde erfolgreich heruntergeladen",
|
||||
|
|
|
@ -17,6 +17,9 @@
|
|||
"Value": "Value",
|
||||
"Add": "Add",
|
||||
"Delete": "Delete",
|
||||
"Statistics": "Statistics",
|
||||
"YouPlayedFor": "You've played for",
|
||||
"hours": "hours",
|
||||
"Unpack": "Unpacking",
|
||||
"GameDownloaded": "Game was successfully installed",
|
||||
"ApplyPatch": "Applying patch...",
|
||||
|
|
|
@ -16,6 +16,9 @@
|
|||
"Value": "Value",
|
||||
"Add": "Add",
|
||||
"Delete": "Delete",
|
||||
"Statistics": "Statistics",
|
||||
"YouPlayedFor": "You've played for",
|
||||
"hours": "hours",
|
||||
"Downloading": "Downloading",
|
||||
"Unpack": "Unpacking",
|
||||
"GameDownloaded": "Game was successfully installed",
|
||||
|
|
|
@ -16,6 +16,9 @@
|
|||
"Value": "Value",
|
||||
"Add": "Add",
|
||||
"Delete": "Delete",
|
||||
"Statistics": "Statistics",
|
||||
"YouPlayedFor": "You've played for",
|
||||
"hours": "hours",
|
||||
"Downloading": "Downloading",
|
||||
"Unpack": "Unpacking",
|
||||
"GameDownloaded": "Game was successfully installed",
|
||||
|
|
|
@ -16,6 +16,9 @@
|
|||
"Value": "Value",
|
||||
"Add": "Add",
|
||||
"Delete": "Delete",
|
||||
"Statistics": "Statistics",
|
||||
"YouPlayedFor": "You've played for",
|
||||
"hours": "hours",
|
||||
"Downloading": "Downloading",
|
||||
"Unpack": "Unpacking",
|
||||
"GameDownloaded": "Game was successfully installed",
|
||||
|
|
|
@ -16,6 +16,9 @@
|
|||
"Value": "Value",
|
||||
"Add": "Add",
|
||||
"Delete": "Delete",
|
||||
"Statistics": "Statistics",
|
||||
"YouPlayedFor": "You've played for",
|
||||
"hours": "hours",
|
||||
"Downloading": "Downloading",
|
||||
"Unpack": "Unpacking",
|
||||
"GameDownloaded": "Game was successfully installed",
|
||||
|
|
|
@ -17,6 +17,9 @@
|
|||
"Value": "バリュー",
|
||||
"Add": "加える",
|
||||
"Delete": "削除",
|
||||
"Statistics": "統計",
|
||||
"YouPlayedFor": "あなたはのためにプレーしてきました",
|
||||
"hours": "時間",
|
||||
"Unpack": "開梱",
|
||||
"GameDownloaded": "ゲームのインストールに成功しました",
|
||||
"ApplyPatch": "パッチの適用...",
|
||||
|
|
|
@ -1,38 +1,41 @@
|
|||
{
|
||||
"Install": "Install",
|
||||
"Update": "Update",
|
||||
"Launch": "Launch",
|
||||
"Runners": "Runners",
|
||||
"Language": "Language",
|
||||
"Voice": "Voice Pack",
|
||||
"VoiceNotification": "This feature requires you to manually select the new voice pack in the game",
|
||||
"AutoTheme": "Automatic theme switching",
|
||||
"SettingsTitle": "Settings",
|
||||
"GeneralSettings": "General",
|
||||
"WineVersion": "Wine version",
|
||||
"Environment": "Environment",
|
||||
"EnvironmentalVariables": "Environmental variables",
|
||||
"Name": "Name",
|
||||
"Value": "Value",
|
||||
"Add": "Add",
|
||||
"Delete": "Delete",
|
||||
"Downloading": "Downloading",
|
||||
"Unpack": "Unpacking",
|
||||
"GameDownloaded": "Game was successfully installed",
|
||||
"ApplyPatch": "Applying patch...",
|
||||
"PatchRequired": "Patch required",
|
||||
"PatchRequiredHint": "This game version doesn't have an anti-cheat patch. Please, wait a few days and try again",
|
||||
"TestPatch": "Apply test patch",
|
||||
"TestPatchHint": "This game version has an anti-cheat patch, but it is in the testing phase. You can wait a few days until it is stable or apply it at your own risk",
|
||||
"AnalyticsTitle": "Yanfei's commission...",
|
||||
"ParticipateInAnalytics": "Participate in anonymous data collection",
|
||||
"AnalyticsText1": "To count the active user base for Linux, Yanfei would like to collect your IP address everytime the game updates",
|
||||
"AnalyticsText2": "The IP address will be hashed for security purpose",
|
||||
"AnalyticsShareCountry": "Share country",
|
||||
"Participate": "Participate",
|
||||
"Skip": "Skip",
|
||||
"SkipAndDontAsk": "Skip and don't ask again",
|
||||
"LauncherUpdateTitle": "Launcher update available: ",
|
||||
"LauncherUpdateBody": "You can download a new version of the launcher from the project's repository at {uri.launcher}",
|
||||
"TelemetryNotDisabled": "{placeholders.uppercase.company}'s telemetry servers don't disabled!"
|
||||
"Install": "설치",
|
||||
"Update": "업데이트",
|
||||
"Launch": "발사",
|
||||
"Runners": "주자",
|
||||
"Language": "언어",
|
||||
"Voice": "음성 팩",
|
||||
"VoiceNotification": "이 기능을 사용하려면 게임에서 새 음성 팩을 수동으로 선택해야합니다",
|
||||
"AutoTheme": "자동 테마 전환",
|
||||
"SettingsTitle": "설정",
|
||||
"GeneralSettings": "일반",
|
||||
"WineVersion": "와인 버전",
|
||||
"Environment": "환경",
|
||||
"EnvironmentalVariables": "환경 변수",
|
||||
"Name": "이름",
|
||||
"Value": "가치",
|
||||
"Add": "추가",
|
||||
"Delete": "삭제",
|
||||
"Statistics": "통계",
|
||||
"YouPlayedFor": "당신은 위해 연주했습니다",
|
||||
"hours": "시간",
|
||||
"Downloading": "다운로드 중",
|
||||
"Unpack": "풀기",
|
||||
"GameDownloaded": "게임이 성공적으로 설치되었습니다",
|
||||
"ApplyPatch": "패치 적용...",
|
||||
"PatchRequired": "패치 필요",
|
||||
"PatchRequiredHint": "이 게임 버전에는 안티 치트 패치가 없습니다. 며칠 기다렸다가 다시 시도하십시오",
|
||||
"TestPatch": "테스트 패치 적용",
|
||||
"TestPatchHint": "이 게임 버전에는 안티 치트 패치가 있지만 테스트 단계에 있습니다. 안정 될 때까지 며칠을 기다리거나 자신의 위험에 따라 적용 할 수 있습니다",
|
||||
"AnalyticsTitle": "Yanfei 의위원회...",
|
||||
"ParticipateInAnalytics": "익명 데이터 수집에 참여",
|
||||
"AnalyticsText1": "리눅스에 대한 활성 사용자 기반을 계산하려면,Yanfei 는 게임 업데이트마다 IP 주소를 수집하고 싶습니다",
|
||||
"AnalyticsText2": "IP 주소는 보안 목적으로 해시됩니다",
|
||||
"AnalyticsShareCountry": "공유 국가",
|
||||
"Participate": "참여",
|
||||
"Skip": "건너 뛰기",
|
||||
"SkipAndDontAsk": "건너 뛰고 다시 묻지 마십시오",
|
||||
"LauncherUpdateTitle": "실행기 업데이트 가능: ",
|
||||
"LauncherUpdateBody": "에서 프로젝트의 저장소에서 실행기의 새 버전을 다운로드 할 수 있습니다 {uri.launcher}",
|
||||
"TelemetryNotDisabled": "{placeholders.uppercase.company} 의 원격 측정 서버가 비활성화되지 않음!"
|
||||
}
|
|
@ -16,6 +16,9 @@
|
|||
"Value": "Value",
|
||||
"Add": "Add",
|
||||
"Delete": "Delete",
|
||||
"Statistics": "Statistics",
|
||||
"YouPlayedFor": "You've played for",
|
||||
"hours": "hours",
|
||||
"Downloading": "Downloading",
|
||||
"Unpack": "Unpacking",
|
||||
"GameDownloaded": "Game was successfully installed",
|
||||
|
|
|
@ -16,6 +16,9 @@
|
|||
"Value": "Значение",
|
||||
"Add": "Добавить",
|
||||
"Delete": "Удалить",
|
||||
"Statistics": "Статистика",
|
||||
"YouPlayedFor": "Вы играли",
|
||||
"hours": "часов",
|
||||
"Downloading": "Загрузка",
|
||||
"Unpack": "Распаковка",
|
||||
"GameDownloaded": "Игра была успешно установлена",
|
||||
|
|
|
@ -16,6 +16,9 @@
|
|||
"Value": "Value",
|
||||
"Add": "Add",
|
||||
"Delete": "Delete",
|
||||
"Statistics": "Statistics",
|
||||
"YouPlayedFor": "You've played for",
|
||||
"hours": "hours",
|
||||
"Downloading": "Downloading",
|
||||
"Unpack": "Unpacking",
|
||||
"GameDownloaded": "Game was successfully installed",
|
||||
|
|
|
@ -16,6 +16,9 @@
|
|||
"Value": "Value",
|
||||
"Add": "Add",
|
||||
"Delete": "Delete",
|
||||
"Statistics": "Statistics",
|
||||
"YouPlayedFor": "You've played for",
|
||||
"hours": "hours",
|
||||
"Downloading": "Downloading",
|
||||
"Unpack": "Unpacking",
|
||||
"GameDownloaded": "Game was successfully installed",
|
||||
|
|
|
@ -17,6 +17,9 @@
|
|||
"Value": "Value",
|
||||
"Add": "Add",
|
||||
"Delete": "Delete",
|
||||
"Statistics": "Statistics",
|
||||
"YouPlayedFor": "You've played for",
|
||||
"hours": "hours",
|
||||
"Unpack": "开箱",
|
||||
"GameDownloaded": "游戏安装成功",
|
||||
"ApplyPatch": "应用补丁...",
|
||||
|
|
|
@ -17,6 +17,9 @@
|
|||
"Value": "Value",
|
||||
"Add": "Add",
|
||||
"Delete": "Delete",
|
||||
"Statistics": "Statistics",
|
||||
"YouPlayedFor": "You've played for",
|
||||
"hours": "hours",
|
||||
"Unpack": "解壓縮中...",
|
||||
"GameDownloaded": "遊戲安裝成功",
|
||||
"ApplyPatch": "套用補丁...",
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 349 KiB After Width: | Height: | Size: 348 KiB |
|
@ -1,6 +1,7 @@
|
|||
const { ipcRenderer } = require('electron');
|
||||
|
||||
import $ from 'cash-dom';
|
||||
|
||||
import { LauncherLib } from './lib/LauncherLib';
|
||||
import { LauncherUI } from './lib/LauncherUI';
|
||||
|
||||
|
@ -10,13 +11,18 @@ $(() => {
|
|||
$('#participate').on('click', async () => {
|
||||
await fetch(`https://an-anime-game-launcher.000webhostapp.com${ !$('#share-country').hasClass('checkbox-active') ? '/?hide-geo' : '' }`);
|
||||
|
||||
LauncherLib.updateConfig('analytics', LauncherLib.version);
|
||||
// LauncherLib.version can break this property
|
||||
// because analytics can be displayed even with the first
|
||||
// launcher's run and then of course uninstalled game's version
|
||||
// will be "null", which in analytics means that user don't
|
||||
// want to see this dialog anymore
|
||||
LauncherLib.updateConfig('analytics', (await LauncherLib.getData()).game.latest.version);
|
||||
|
||||
ipcRenderer.invoke('hide-analytics-participation');
|
||||
});
|
||||
|
||||
$('#skip').on('click', () => {
|
||||
LauncherLib.updateConfig('analytics', LauncherLib.version);
|
||||
$('#skip').on('click', async () => {
|
||||
LauncherLib.updateConfig('analytics', (await LauncherLib.getData()).game.latest.version);
|
||||
|
||||
ipcRenderer.invoke('hide-analytics-participation');
|
||||
});
|
||||
|
|
|
@ -69,13 +69,14 @@ $(() => {
|
|||
LauncherUI.updateLauncherState();
|
||||
});
|
||||
|
||||
Tools.getGitTags(constants.uri.launcher).then (tags => {
|
||||
if (tags.filter(entry => semver.gt(entry.tag, launcher_version)).length > 0)
|
||||
Tools.getGitTags(constants.uri.launcher).then(tags => {
|
||||
const latestVersion = tags[tags.length - 1].tag;
|
||||
|
||||
if (latestVersion && semver.gt(latestVersion, launcher_version))
|
||||
{
|
||||
ipcRenderer.send('notification', {
|
||||
title: `${LauncherUI.i18n.translate('LauncherUpdateTitle')} (${launcher_version} -> ${tags[tags.length - 1].tag})`,
|
||||
body: LauncherUI.i18n.translate('LauncherUpdateBody'),
|
||||
timeoutType: 'never'
|
||||
title: `${LauncherUI.i18n.translate('LauncherUpdateTitle')} (${launcher_version} -> ${latestVersion})`,
|
||||
body: LauncherUI.i18n.translate('LauncherUpdateBody')
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -117,21 +118,21 @@ $(() => {
|
|||
if (LauncherUI.launcherState == 'game-launch-available')
|
||||
{
|
||||
console.log(`%c> Starting the game...`, 'font-size: 16px');
|
||||
|
||||
|
||||
if (!await LauncherLib.isTelemetryDisabled())
|
||||
{
|
||||
console.log(`${constants.placeholders.uppercase.company}'s telemetry servers doesn't disabled!`);
|
||||
|
||||
|
||||
ipcRenderer.send('notification', {
|
||||
title: document.title,
|
||||
body: LauncherUI.i18n.translate('TelemetryNotDisabled')
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
else
|
||||
{
|
||||
let wineExeutable = 'wine';
|
||||
|
||||
|
||||
if (LauncherLib.getConfig('runner') !== null)
|
||||
{
|
||||
wineExeutable = path.join(
|
||||
|
@ -139,27 +140,29 @@ $(() => {
|
|||
LauncherLib.getConfig('runner.folder'),
|
||||
LauncherLib.getConfig('runner.executable')
|
||||
);
|
||||
|
||||
|
||||
if (!fs.existsSync(wineExeutable))
|
||||
{
|
||||
wineExeutable = 'wine';
|
||||
|
||||
|
||||
LauncherLib.updateConfig('runner', null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
console.log(`Wine executable: ${wineExeutable}`);
|
||||
|
||||
|
||||
if (DiscordRPC.isActive())
|
||||
{
|
||||
DiscordRPC.setActivity({
|
||||
details: 'In-Game',
|
||||
largeImageKey: 'game',
|
||||
largeImageText: 'An Anime Game Launcher',
|
||||
startTimestamp: new Date().setDate(new Date().getDate())
|
||||
startTimestamp: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
exec(`${wineExeutable} launcher.bat`, {
|
||||
cwd: constants.gameDir,
|
||||
env: {
|
||||
|
@ -169,9 +172,13 @@ $(() => {
|
|||
}
|
||||
}, (err: any, stdout: any, stderr: any) => {
|
||||
console.log(`%c> Game closed`, 'font-size: 16px');
|
||||
|
||||
|
||||
const playtime = Date.now() - startTime;
|
||||
|
||||
ipcRenderer.invoke('show-window');
|
||||
|
||||
|
||||
LauncherLib.updateConfig('playtime', LauncherLib.getConfig('playtime') + Math.round(playtime / 1000));
|
||||
|
||||
if (DiscordRPC.isActive())
|
||||
{
|
||||
DiscordRPC.setActivity({
|
||||
|
@ -180,12 +187,12 @@ $(() => {
|
|||
largeImageText: 'An Anime Game Launcher'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
console.log(err);
|
||||
console.log(stdout);
|
||||
console.log(stderr);
|
||||
});
|
||||
|
||||
|
||||
ipcRenderer.invoke('hide-window');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ const fs = require('fs');
|
|||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { spawn, exec } = require('child_process');
|
||||
const dns = require('dns');
|
||||
|
||||
const store = require('electron-store');
|
||||
const https = require('follow-redirects').https;
|
||||
|
@ -43,7 +42,9 @@ const config = new store ({
|
|||
// FidelityFX Super Resolution
|
||||
WINE_FULLSCREEN_FSR: '1',
|
||||
WINE_FULLSCREEN_FSR_STRENGTH: '3'
|
||||
}
|
||||
},
|
||||
|
||||
playtime: 0 // Number of seconds user spent in game
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -118,9 +119,10 @@ export class LauncherLib
|
|||
response.on('data', (chunk: any) => data += chunk);
|
||||
|
||||
response.on('end', () => {
|
||||
let jsondata: GIJSON = JSON.parse(data);
|
||||
const jsonData: GIJSON = JSON.parse(data);
|
||||
|
||||
return jsondata.message === 'OK' ? resolve(jsondata.data) : reject(null);
|
||||
return jsonData.message === 'OK' ?
|
||||
resolve(jsonData.data) : reject(null);
|
||||
});
|
||||
}).on('error', (err: Error) => reject(err));
|
||||
});
|
||||
|
@ -216,7 +218,7 @@ export class LauncherLib
|
|||
// WINEPREFIX='...../wineprefix' winetricks corefonts usetakefocus=n
|
||||
public static async installPrefix (prefixpath: string, progress: (output: string, current: number, total: number) => void): Promise<void>
|
||||
{
|
||||
let installationSteps = [
|
||||
const installationSteps = [
|
||||
// corefonts
|
||||
'Executing w_do_call corefonts',
|
||||
'Executing load_corefonts',
|
||||
|
@ -276,7 +278,7 @@ export class LauncherLib
|
|||
// Delete zip file and assign patch directory.
|
||||
fs.unlinkSync(path.join(constants.launcherDir, 'patch.zip'));
|
||||
|
||||
let patchDir = path.join(constants.launcherDir, 'gi-on-linux', pathInfo.version.replaceAll('.', ''));
|
||||
const patchDir = path.join(constants.launcherDir, 'gi-on-linux', pathInfo.version.replaceAll('.', ''));
|
||||
|
||||
// Patch out the testing phase content from the shell files if active and make sure the shell files are executable.
|
||||
exec(`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`);
|
||||
|
|
|
@ -249,6 +249,7 @@ export class LauncherUI
|
|||
|
||||
sector.forEach(pixel => meanBrightness += pixel.color.r + pixel.color.g + pixel.color.b);
|
||||
|
||||
// TODO: convert RGB mean color to LAB to get real background brightness
|
||||
meanBrightness /= sector.length * 3;
|
||||
|
||||
console.log(`Background's mean brightness is ${meanBrightness}`);
|
||||
|
|
|
@ -58,7 +58,7 @@ export class Tools
|
|||
|
||||
public static async getGitTags (uri: string): Promise<GitTag[]>
|
||||
{
|
||||
return new Promise(resolve => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let git = spawn('git', ['ls-remote', '--tags', uri]),
|
||||
tags: GitTag[] = [];
|
||||
|
||||
|
@ -77,6 +77,8 @@ export class Tools
|
|||
});
|
||||
});
|
||||
|
||||
git.stderr.on('data', (data: string) => reject(data));
|
||||
|
||||
git.on('close', () => resolve(tags));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -132,6 +132,17 @@ $(() => {
|
|||
td.last().find('span').text(value);
|
||||
});
|
||||
|
||||
/**
|
||||
* Statistics
|
||||
*/
|
||||
|
||||
$('#play-hours').text((LauncherLib.getConfig('playtime') / 3600).toFixed(1).toString());
|
||||
|
||||
// Update this once per two minute
|
||||
setInterval(() => {
|
||||
$('#play-hours').text((LauncherLib.getConfig('playtime') / 3600).toFixed(1).toString());
|
||||
}, 120 * 1000);
|
||||
|
||||
/**
|
||||
* Wine versions manager
|
||||
*/
|
||||
|
|
Loading…
Add table
Reference in a new issue