v3.4.2.0
- Implemented Paradox Launcher support for Epic games - Minor refactoring
This commit is contained in:
parent
124dea6fc2
commit
90dc9853fa
4 changed files with 86 additions and 71 deletions
|
@ -5,7 +5,7 @@
|
|||
<UseWindowsForms>True</UseWindowsForms>
|
||||
<ApplicationIcon>Resources\ini.ico</ApplicationIcon>
|
||||
<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
|
||||
<Version>3.4.1.1</Version>
|
||||
<Version>3.4.2.0</Version>
|
||||
<PackageIcon>Resources\ini.ico</PackageIcon>
|
||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
||||
<Copyright>2021, pointfeev (https://github.com/pointfeev)</Copyright>
|
||||
|
|
|
@ -4,6 +4,7 @@ using System.Drawing;
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
|
@ -51,11 +52,13 @@ internal partial class InstallForm : CustomForm
|
|||
if (logTextBox.Text.Length > 0) logTextBox.AppendText(Environment.NewLine, color);
|
||||
logTextBox.AppendText(text, color);
|
||||
});
|
||||
Thread.Sleep(0);
|
||||
}
|
||||
}
|
||||
|
||||
internal static void WriteCreamConfiguration(StreamWriter writer, string appId, string name, SortedList<string, (DlcType type, string name, string icon)> dlc, InstallForm installForm = null)
|
||||
{
|
||||
Thread.Sleep(0);
|
||||
writer.WriteLine($"; {name}");
|
||||
writer.WriteLine("[steam]");
|
||||
writer.WriteLine($"appid = {appId}");
|
||||
|
@ -65,6 +68,7 @@ internal partial class InstallForm : CustomForm
|
|||
installForm.UpdateUser($"Added game to cream_api.ini with appid {appId} ({name})", InstallationLog.Action, info: false);
|
||||
foreach (KeyValuePair<string, (DlcType type, string name, string icon)> pair in dlc)
|
||||
{
|
||||
Thread.Sleep(0);
|
||||
string dlcId = pair.Key;
|
||||
(_, string dlcName, _) = pair.Value;
|
||||
writer.WriteLine($"{dlcId} = {dlcName}");
|
||||
|
@ -141,21 +145,24 @@ internal partial class InstallForm : CustomForm
|
|||
StreamWriter writer = new(config, true, Encoding.UTF8);
|
||||
if (selection.Id != "ParadoxLauncher")
|
||||
WriteCreamConfiguration(writer, selection.Id, selection.Name, selection.SelectedDlc, installForm);
|
||||
foreach (Tuple<string, string, SortedList<string, (DlcType type, string name, string icon)>> extraAppDlc in selection.ExtraDlc)
|
||||
WriteCreamConfiguration(writer, extraAppDlc.Item1, extraAppDlc.Item2, extraAppDlc.Item3, installForm);
|
||||
foreach ((string id, string name, SortedList<string, (DlcType type, string name, string icon)> dlc) in selection.ExtraDlc)
|
||||
WriteCreamConfiguration(writer, id, name, dlc, installForm);
|
||||
writer.Flush();
|
||||
writer.Close();
|
||||
});
|
||||
|
||||
internal static void WriteScreamConfiguration(StreamWriter writer, SortedList<string, (DlcType type, string name, string icon)> dlc, InstallForm installForm = null)
|
||||
internal static void WriteScreamConfiguration(StreamWriter writer, SortedList<string, (DlcType type, string name, string icon)> dlc, List<(string id, string name, SortedList<string, (DlcType type, string name, string icon)> dlc)> extraDlc, InstallForm installForm = null)
|
||||
{
|
||||
Thread.Sleep(0);
|
||||
writer.WriteLine("{");
|
||||
writer.WriteLine(" \"version\": 2,");
|
||||
writer.WriteLine(" \"logging\": false,");
|
||||
writer.WriteLine(" \"eos_logging\": false,");
|
||||
writer.WriteLine(" \"block_metrics\": false,");
|
||||
writer.WriteLine(" \"catalog_items\": {");
|
||||
IEnumerable<KeyValuePair<string, (DlcType type, string name, string icon)>> catalogItems = dlc.Where(pair => pair.Value.type == DlcType.CatalogItem);
|
||||
IEnumerable<KeyValuePair<string, (DlcType type, string name, string icon)>> catalogItems = dlc.Where(pair => pair.Value.type == DlcType.EpicCatalogItem);
|
||||
foreach ((string id, string name, SortedList<string, (DlcType type, string name, string icon)> _dlc) in extraDlc)
|
||||
catalogItems = catalogItems.Concat(_dlc.Where(pair => pair.Value.type == DlcType.EpicCatalogItem));
|
||||
if (catalogItems.Any())
|
||||
{
|
||||
writer.WriteLine(" \"unlock_all\": false,");
|
||||
|
@ -163,6 +170,7 @@ internal partial class InstallForm : CustomForm
|
|||
KeyValuePair<string, (DlcType type, string name, string icon)> lastCatalogItem = catalogItems.Last();
|
||||
foreach (KeyValuePair<string, (DlcType type, string name, string icon)> pair in catalogItems)
|
||||
{
|
||||
Thread.Sleep(0);
|
||||
string id = pair.Key;
|
||||
(_, string name, _) = pair.Value;
|
||||
writer.WriteLine($" \"{id}\"{(pair.Equals(lastCatalogItem) ? "" : ",")}");
|
||||
|
@ -178,7 +186,9 @@ internal partial class InstallForm : CustomForm
|
|||
}
|
||||
writer.WriteLine(" },");
|
||||
writer.WriteLine(" \"entitlements\": {");
|
||||
IEnumerable<KeyValuePair<string, (DlcType type, string name, string icon)>> entitlements = dlc.Where(pair => pair.Value.type == DlcType.Entitlement);
|
||||
IEnumerable<KeyValuePair<string, (DlcType type, string name, string icon)>> entitlements = dlc.Where(pair => pair.Value.type == DlcType.EpicEntitlement);
|
||||
foreach ((string id, string name, SortedList<string, (DlcType type, string name, string icon)> _dlc) in extraDlc)
|
||||
entitlements = entitlements.Concat(_dlc.Where(pair => pair.Value.type == DlcType.EpicEntitlement));
|
||||
if (entitlements.Any())
|
||||
{
|
||||
writer.WriteLine(" \"unlock_all\": false,");
|
||||
|
@ -187,6 +197,7 @@ internal partial class InstallForm : CustomForm
|
|||
KeyValuePair<string, (DlcType type, string name, string icon)> lastEntitlement = entitlements.Last();
|
||||
foreach (KeyValuePair<string, (DlcType type, string name, string icon)> pair in entitlements)
|
||||
{
|
||||
Thread.Sleep(0);
|
||||
string id = pair.Key;
|
||||
(_, string name, _) = pair.Value;
|
||||
writer.WriteLine($" \"{id}\"{(pair.Equals(lastEntitlement) ? "" : ",")}");
|
||||
|
@ -271,10 +282,7 @@ internal partial class InstallForm : CustomForm
|
|||
installForm.UpdateUser("Generating ScreamAPI configuration for " + selection.Name + $" in directory \"{directory}\" . . . ", InstallationLog.Operation);
|
||||
File.Create(config).Close();
|
||||
StreamWriter writer = new(config, true, Encoding.UTF8);
|
||||
if (selection.Id != "ParadoxLauncher")
|
||||
WriteScreamConfiguration(writer, selection.SelectedDlc, installForm);
|
||||
foreach (Tuple<string, string, SortedList<string, (DlcType type, string name, string icon)>> extraAppDlc in selection.ExtraDlc)
|
||||
WriteScreamConfiguration(writer, extraAppDlc.Item3, installForm);
|
||||
WriteScreamConfiguration(writer, selection.SelectedDlc, selection.ExtraDlc, installForm);
|
||||
writer.Flush();
|
||||
writer.Close();
|
||||
});
|
||||
|
@ -306,27 +314,36 @@ internal partial class InstallForm : CustomForm
|
|||
}
|
||||
}
|
||||
if (code < 0) throw new CustomMessageException("Repair failed!");
|
||||
string platform = selection.Platform == Platform.Steam ? "CreamAPI"
|
||||
: selection.Platform == Platform.Epic ? "ScreamAPI"
|
||||
: throw new InvalidPlatformException(selection.Platform);
|
||||
foreach (string directory in selection.DllDirectories)
|
||||
{
|
||||
UpdateUser($"{(Uninstalling ? "Uninstalling" : "Installing")} {platform}" +
|
||||
$" {(Uninstalling ? "from" : "for")} " + selection.Name + $" in directory \"{directory}\" . . . ", InstallationLog.Operation);
|
||||
if (Program.Canceled || !Program.IsProgramRunningDialog(this, selection)) throw new CustomMessageException("The operation was canceled.");
|
||||
if (platform == "CreamAPI")
|
||||
Thread.Sleep(0);
|
||||
if (selection.IsSteam && selection.SelectedDlc.Any(d => d.Value.type is DlcType.Steam)
|
||||
|| selection.ExtraDlc.Any(item => item.dlc.Any(dlc => dlc.Value.type is DlcType.Steam)))
|
||||
{
|
||||
if (Uninstalling)
|
||||
await UninstallCreamAPI(directory, this);
|
||||
else
|
||||
await InstallCreamAPI(directory, selection, this);
|
||||
directory.GetCreamApiComponents(out string sdk32, out string sdk32_o, out string sdk64, out string sdk64_o, out string config);
|
||||
if (File.Exists(sdk32) || File.Exists(sdk32_o) || File.Exists(sdk64) || File.Exists(sdk64_o) || File.Exists(config))
|
||||
{
|
||||
UpdateUser($"{(Uninstalling ? "Uninstalling" : "Installing")} CreamAPI" +
|
||||
$" {(Uninstalling ? "from" : "for")} " + selection.Name + $" in directory \"{directory}\" . . . ", InstallationLog.Operation);
|
||||
if (Uninstalling)
|
||||
await UninstallCreamAPI(directory, this);
|
||||
else
|
||||
await InstallCreamAPI(directory, selection, this);
|
||||
}
|
||||
}
|
||||
else if (platform == "ScreamAPI")
|
||||
if (selection.IsEpic && selection.SelectedDlc.Any(d => d.Value.type is DlcType.EpicCatalogItem or DlcType.EpicEntitlement)
|
||||
|| selection.ExtraDlc.Any(item => item.dlc.Any(dlc => dlc.Value.type is DlcType.EpicCatalogItem or DlcType.EpicEntitlement)))
|
||||
{
|
||||
if (Uninstalling)
|
||||
await UninstallScreamAPI(directory, this);
|
||||
else
|
||||
await InstallScreamAPI(directory, selection, this);
|
||||
directory.GetScreamApiComponents(out string sdk32, out string sdk32_o, out string sdk64, out string sdk64_o, out string config);
|
||||
if (File.Exists(sdk32) || File.Exists(sdk32_o) || File.Exists(sdk64) || File.Exists(sdk64_o) || File.Exists(config))
|
||||
{
|
||||
UpdateUser($"{(Uninstalling ? "Uninstalling" : "Installing")} ScreamAPI" +
|
||||
$" {(Uninstalling ? "from" : "for")} " + selection.Name + $" in directory \"{directory}\" . . . ", InstallationLog.Operation);
|
||||
if (Uninstalling)
|
||||
await UninstallScreamAPI(directory, this);
|
||||
else
|
||||
await InstallScreamAPI(directory, selection, this);
|
||||
}
|
||||
}
|
||||
UpdateProgress(++cur / count * 100);
|
||||
}
|
||||
|
@ -342,6 +359,7 @@ internal partial class InstallForm : CustomForm
|
|||
foreach (ProgramSelection selection in programSelections)
|
||||
{
|
||||
if (Program.Canceled || !Program.IsProgramRunningDialog(this, selection)) throw new CustomMessageException("The operation was canceled.");
|
||||
Thread.Sleep(0);
|
||||
try
|
||||
{
|
||||
await OperateFor(selection);
|
||||
|
|
|
@ -103,9 +103,7 @@ internal partial class SelectForm : CustomForm
|
|||
if (Directory.Exists(ParadoxLauncher.InstallPath) && ProgramsToScan.Any(c => c.platform == "Paradox" && c.id == "ParadoxLauncher"))
|
||||
{
|
||||
List<string> steamDllDirectories = await SteamLibrary.GetDllDirectoriesFromGameDirectory(ParadoxLauncher.InstallPath);
|
||||
List<string> epicDllDirectories = null;
|
||||
if (steamDllDirectories is null)
|
||||
epicDllDirectories = await EpicLibrary.GetDllDirectoriesFromGameDirectory(ParadoxLauncher.InstallPath);
|
||||
List<string> epicDllDirectories = await EpicLibrary.GetDllDirectoriesFromGameDirectory(ParadoxLauncher.InstallPath);
|
||||
if (steamDllDirectories is not null || epicDllDirectories is not null)
|
||||
{
|
||||
ProgramSelection selection = ProgramSelection.FromId("ParadoxLauncher");
|
||||
|
@ -115,7 +113,8 @@ internal partial class SelectForm : CustomForm
|
|||
selection.Name = "Paradox Launcher";
|
||||
selection.RootDirectory = ParadoxLauncher.InstallPath;
|
||||
selection.DllDirectories = steamDllDirectories ?? epicDllDirectories;
|
||||
selection.Platform = steamDllDirectories is not null ? Platform.Steam : Platform.Epic;
|
||||
selection.IsSteam = steamDllDirectories is not null;
|
||||
selection.IsEpic = epicDllDirectories is not null;
|
||||
|
||||
TreeNode programNode = treeNodes.Find(s => s.Name == selection.Id) ?? new();
|
||||
programNode.Name = selection.Id;
|
||||
|
@ -196,7 +195,7 @@ internal partial class SelectForm : CustomForm
|
|||
}
|
||||
if (Program.Canceled) return;
|
||||
if (!string.IsNullOrWhiteSpace(dlcName))
|
||||
dlc[dlcAppId] = (DlcType.Default, dlcName, dlcIcon);
|
||||
dlc[dlcAppId] = (DlcType.Steam, dlcName, dlcIcon);
|
||||
RemoveFromRemainingDLCs(dlcAppId);
|
||||
});
|
||||
dlcTasks.Add(task);
|
||||
|
@ -217,11 +216,11 @@ internal partial class SelectForm : CustomForm
|
|||
|
||||
ProgramSelection selection = ProgramSelection.FromId(appId) ?? new();
|
||||
selection.Enabled = allCheckBox.Checked || selection.SelectedDlc.Any() || selection.ExtraDlc.Any();
|
||||
selection.Platform = Platform.Steam;
|
||||
selection.Id = appId;
|
||||
selection.Name = appData?.name ?? name;
|
||||
selection.RootDirectory = directory;
|
||||
selection.DllDirectories = dllDirectories;
|
||||
selection.IsSteam = true;
|
||||
selection.ProductUrl = "https://store.steampowered.com/app/" + appId;
|
||||
selection.IconUrl = IconGrabber.SteamAppImagesPath + @$"\{appId}\{appInfo?.Value?.GetChild("common")?.GetChild("icon")?.ToString()}.jpg";
|
||||
selection.SubIconUrl = appData?.header_image ?? IconGrabber.SteamAppImagesPath + @$"\{appId}\{appInfo?.Value?.GetChild("common")?.GetChild("clienticon")?.ToString()}.ico";
|
||||
|
@ -318,11 +317,11 @@ internal partial class SelectForm : CustomForm
|
|||
|
||||
ProgramSelection selection = ProgramSelection.FromId(@namespace) ?? new();
|
||||
selection.Enabled = allCheckBox.Checked || selection.SelectedDlc.Any() || selection.ExtraDlc.Any();
|
||||
selection.Platform = Platform.Epic;
|
||||
selection.Id = @namespace;
|
||||
selection.Name = name;
|
||||
selection.RootDirectory = directory;
|
||||
selection.DllDirectories = dllDirectories;
|
||||
selection.IsEpic = true;
|
||||
foreach (KeyValuePair<string, (string name, string product, string icon, string developer)> pair in entitlements)
|
||||
{
|
||||
Thread.Sleep(0);
|
||||
|
@ -364,7 +363,7 @@ internal partial class SelectForm : CustomForm
|
|||
if (programNode is null/* || entitlementsNode is null*/) return;
|
||||
Thread.Sleep(0);
|
||||
string dlcId = pair.Key;
|
||||
(DlcType type, string name, string icon) dlcApp = (DlcType.Entitlement, pair.Value.name, pair.Value.icon);
|
||||
(DlcType type, string name, string icon) dlcApp = (DlcType.EpicEntitlement, pair.Value.name, pair.Value.icon);
|
||||
selection.AllDlc[dlcId] = dlcApp;
|
||||
if (allCheckBox.Checked) selection.SelectedDlc[dlcId] = dlcApp;
|
||||
TreeNode dlcNode = treeNodes.Find(s => s.Name == dlcId) ?? new();
|
||||
|
@ -614,10 +613,8 @@ internal partial class SelectForm : CustomForm
|
|||
List<ContextMenuItem> queries = new();
|
||||
if (File.Exists(appInfoJSON))
|
||||
{
|
||||
string platform = (selection is null || selection.Platform == Platform.Steam) ? "Steam Store"
|
||||
: selection.Platform == Platform.Epic ? "Epic GraphQL"
|
||||
: throw new InvalidPlatformException(selection.Platform);
|
||||
queries.Add(new ContextMenuItem($"Open {platform} Query", "Notepad",
|
||||
string platform = (selection is null || selection.IsSteam) ? "Steam Store " : selection.IsEpic ? "Epic GraphQL " : "";
|
||||
queries.Add(new ContextMenuItem($"Open {platform}Query", "Notepad",
|
||||
new EventHandler((sender, e) => Diagnostics.OpenFileInNotepad(appInfoJSON))));
|
||||
}
|
||||
if (File.Exists(appInfoVDF))
|
||||
|
@ -662,20 +659,32 @@ internal partial class SelectForm : CustomForm
|
|||
contextMenuStrip.Items.Add(new ContextMenuItem("Open Root Directory", "File Explorer",
|
||||
new EventHandler((sender, e) => Diagnostics.OpenDirectoryInFileExplorer(selection.RootDirectory))));
|
||||
List<string> directories = selection.DllDirectories.ToList();
|
||||
string platform = selection.Platform == Platform.Steam ? "Steamworks"
|
||||
: selection.Platform == Platform.Epic ? "Epic Online Services"
|
||||
: throw new InvalidPlatformException(selection.Platform);
|
||||
for (int i = 0; i < directories.Count; i++)
|
||||
{
|
||||
string directory = directories[i];
|
||||
contextMenuStrip.Items.Add(new ContextMenuItem($"Open {platform} SDK Directory #{i + 1}", "File Explorer",
|
||||
new EventHandler((sender, e) => Diagnostics.OpenDirectoryInFileExplorer(directory))));
|
||||
}
|
||||
if (selection.IsSteam)
|
||||
for (int i = 0; i < directories.Count; i++)
|
||||
{
|
||||
string directory = directories[i];
|
||||
directory.GetCreamApiComponents(out string sdk32, out string sdk32_o, out string sdk64, out string sdk64_o, out string config);
|
||||
if (File.Exists(sdk32) || File.Exists(sdk32_o) || File.Exists(sdk64) || File.Exists(sdk64_o) || File.Exists(config))
|
||||
{
|
||||
contextMenuStrip.Items.Add(new ContextMenuItem($"Open Steamworks SDK Directory #{i + 1}", "File Explorer",
|
||||
new EventHandler((sender, e) => Diagnostics.OpenDirectoryInFileExplorer(directory))));
|
||||
}
|
||||
}
|
||||
if (selection.IsEpic)
|
||||
for (int i = 0; i < directories.Count; i++)
|
||||
{
|
||||
string directory = directories[i];
|
||||
directory.GetScreamApiComponents(out string sdk32, out string sdk32_o, out string sdk64, out string sdk64_o, out string config);
|
||||
if (File.Exists(sdk32) || File.Exists(sdk32_o) || File.Exists(sdk64) || File.Exists(sdk64_o) || File.Exists(config))
|
||||
{
|
||||
contextMenuStrip.Items.Add(new ContextMenuItem($"Open Epic Online Services SDK Directory #{i + 1}", "File Explorer",
|
||||
new EventHandler((sender, e) => Diagnostics.OpenDirectoryInFileExplorer(directory))));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (id != "ParadoxLauncher")
|
||||
{
|
||||
if (selection is not null && selection.Platform == Platform.Steam
|
||||
|| dlcParentSelection is not null && dlcParentSelection.Platform == Platform.Steam)
|
||||
if (selection is not null && selection.IsSteam || dlcParentSelection is not null && dlcParentSelection.IsSteam)
|
||||
{
|
||||
contextMenuStrip.Items.Add(new ToolStripSeparator());
|
||||
contextMenuStrip.Items.Add(new ContextMenuItem("Open SteamDB", "SteamDB",
|
||||
|
@ -683,14 +692,14 @@ internal partial class SelectForm : CustomForm
|
|||
}
|
||||
if (selection is not null)
|
||||
{
|
||||
if (selection.Platform == Platform.Steam)
|
||||
if (selection.IsSteam)
|
||||
{
|
||||
contextMenuStrip.Items.Add(new ContextMenuItem("Open Steam Store", "Steam Store",
|
||||
new EventHandler((sender, e) => Diagnostics.OpenUrlInInternetBrowser(selection.ProductUrl))));
|
||||
contextMenuStrip.Items.Add(new ContextMenuItem("Open Steam Community", (id, selection.SubIconUrl, true), "Steam Community",
|
||||
new EventHandler((sender, e) => Diagnostics.OpenUrlInInternetBrowser("https://steamcommunity.com/app/" + id))));
|
||||
}
|
||||
else if (selection.Platform == Platform.Epic)
|
||||
else if (selection.IsEpic)
|
||||
{
|
||||
contextMenuStrip.Items.Add(new ToolStripSeparator());
|
||||
contextMenuStrip.Items.Add(new ContextMenuItem("Open ScreamDB", "ScreamDB",
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
|
@ -7,30 +6,16 @@ using CreamInstaller.Components;
|
|||
|
||||
namespace CreamInstaller;
|
||||
|
||||
public enum Platform
|
||||
{
|
||||
Steam = 0,
|
||||
Epic = 1
|
||||
}
|
||||
public class InvalidPlatformException : Exception
|
||||
{
|
||||
public InvalidPlatformException() : base("Invalid platform!") { }
|
||||
public InvalidPlatformException(Platform platform) : base($"Invalid platform ({platform})!") { }
|
||||
public InvalidPlatformException(string message) : base(message) { }
|
||||
public InvalidPlatformException(string message, Exception innerException) : base(message, innerException) { }
|
||||
}
|
||||
|
||||
public enum DlcType
|
||||
{
|
||||
Default = 0,
|
||||
CatalogItem = 1,
|
||||
Entitlement = 2
|
||||
Steam = 0,
|
||||
EpicCatalogItem = 1,
|
||||
EpicEntitlement = 2
|
||||
}
|
||||
|
||||
internal class ProgramSelection
|
||||
{
|
||||
internal bool Enabled;
|
||||
internal Platform Platform = (Platform)(-1);
|
||||
|
||||
internal string Id = "0";
|
||||
internal string Name = "Program";
|
||||
|
@ -44,9 +29,12 @@ internal class ProgramSelection
|
|||
internal string RootDirectory;
|
||||
internal List<string> DllDirectories;
|
||||
|
||||
internal bool IsSteam;
|
||||
internal bool IsEpic;
|
||||
|
||||
internal readonly SortedList<string, (DlcType type, string name, string icon)> AllDlc = new(AppIdComparer.Comparer);
|
||||
internal readonly SortedList<string, (DlcType type, string name, string icon)> SelectedDlc = new(AppIdComparer.Comparer);
|
||||
internal readonly List<Tuple<string, string, SortedList<string, (DlcType type, string name, string icon)>>> ExtraDlc = new(); // for Paradox Launcher
|
||||
internal readonly List<(string id, string name, SortedList<string, (DlcType type, string name, string icon)> dlc)> ExtraDlc = new(); // for Paradox Launcher
|
||||
|
||||
internal bool AreDllsLocked
|
||||
{
|
||||
|
|
Loading…
Reference in a new issue