Riorganizzazione BasePage con inheritance

This commit is contained in:
Samuele Locatelli
2024-02-26 20:05:47 +01:00
parent f3c3452a16
commit 70414dc00f
19 changed files with 208 additions and 470 deletions
+7 -3
View File
@@ -6,11 +6,15 @@
<div class="d-flex justify-content-center flex-wrap align-items-center">
@if(infosIob.iType == MP.Data.Objects.Enums.IobType.rPi)
{
<img src="/images/linuxLogo.png" style="height: 70px; width: 70px" />
<img src="/images/linuxLogo.png" style="height: 7em; width: 7em;" />
}
else if (infosIob.iType == MP.Data.Objects.Enums.IobType.WIN || infosIob.iType == MP.Data.Objects.Enums.IobType.ND)
else if (infosIob.iType == MP.Data.Objects.Enums.IobType.WIN)
{
<img src="/images/winLogo.png" style="height: 70px; width: 70px" />
<img src="/images/winLogo.png" style="height: 7em; width: 7em;" />
}
else if (infosIob.iType == MP.Data.Objects.Enums.IobType.ND)
{
<img src="/images/pythonLogo.png" style="height: 7em; width: 7em;" />
}
</div>
</div>
+8 -12
View File
@@ -7,13 +7,11 @@
<span class="placeholder col-7"></span>
<span class="placeholder col-12 pholderHeight"></span>
<span class="placeholder col-12"></span>
<span class="placeholder col-4"></span>
<span class="placeholder offset-3 col-4"></span>
<span class="placeholder col-4"></span>
<span class="placeholder col-3"></span>
<span class="placeholder col-8"></span>
<span class="placeholder col-4"></span>
<span class="placeholder col-8"></span>
<span class="placeholder col-4"></span>
<span class="placeholder col-3"></span>
<span class="placeholder col-7"></span>
<span class="placeholder col-3"></span>
<span class="placeholder col-8"></span>
</div>
@@ -32,13 +30,11 @@ else
<span class="placeholder col-7"></span>
<span class="placeholder col-12 pholderHeight"></span>
<span class="placeholder col-12"></span>
<span class="placeholder col-4"></span>
<span class="placeholder offset-3 col-4"></span>
<span class="placeholder col-4"></span>
<span class="placeholder col-3"></span>
<span class="placeholder col-8"></span>
<span class="placeholder col-4"></span>
<span class="placeholder col-8"></span>
<span class="placeholder col-4"></span>
<span class="placeholder col-3"></span>
<span class="placeholder col-7"></span>
<span class="placeholder col-3"></span>
<span class="placeholder col-8"></span>
</div>
</div>
+1 -1
View File
@@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<Version>6.16.2402.2618</Version>
<Version>6.16.2402.2620</Version>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>MP_TAB3</RootNamespace>
</PropertyGroup>
+2
View File
@@ -0,0 +1,2 @@
+160
View File
@@ -0,0 +1,160 @@
using Microsoft.AspNetCore.Components;
using MP.Data.DatabaseModels;
using MP.Data;
using MP.Data.Services;
using Newtonsoft.Json;
using NLog.Fluent;
namespace MP_TAB3.Pages
{
/// <summary>
/// Pagina base da cui ereditare metodi x refresh dati da RedisChannel (MessagePipe)
/// </summary>
public partial class BasePage : IDisposable
{
#region Public Properties
/// <summary>
/// Dato di dettaglio MSE per macchina
/// </summary>
public MappaStatoExpl? CurrMSE { get; set; } = null;
/// <summary>
/// Macchina selezionata
/// </summary>
public string IdxMacc { get; set; } = "";
[Inject]
public MessageService MsgServ { get; set; } = null!;
[Inject]
public TabDataFeeder TDFeeder { get; set; } = null!;
#endregion Public Properties
#region Public Methods
/// <summary>
/// Metodo dispose
/// </summary>
public virtual void Dispose()
{
IdxMacc = "";
CurrMSE = null;
TDFeeder.dataPipe.EA_NewMessage -= DataPipe_EA_NewMessage;
}
#endregion Public Methods
#region Protected Methods
/// <summary>
/// Override Oninit x aggancio evento update da MessagePipe
/// </summary>
/// <returns></returns>
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
await ReloadData();
TDFeeder.dataPipe.EA_NewMessage += DataPipe_EA_NewMessage;
}
/// <summary>
/// Metodo refresh con controllo salvataggio in LocalStorage ogni 5 sec
/// </summary>
/// <param name="ListMSE"></param>
/// <returns></returns>
protected async Task RefreshData(List<MappaStatoExpl> ListMSE)
{
// se vecchio salvo sul browser...
if (DateTime.Now.Subtract(lastSave).TotalSeconds > 5)
{
// salvo in LocalStorage...
await MsgServ.SaveMse(ListMSE);
lastSave = DateTime.Now;
}
if (!string.IsNullOrEmpty(IdxMacc))
{
var rawData = ListMSE.Find(x => x.IdxMacchina == IdxMacc);
if (rawData != null)
{
CurrMSE = rawData;
}
else
{
await RefreshMBlock();
}
}
}
/// <summary>
/// Refresh con rilettura da LocalStorage
/// </summary>
/// <returns></returns>
protected async Task RefreshMBlock()
{
// recupero MSE macchina....
if (!string.IsNullOrEmpty(IdxMacc))
{
CurrMSE = await MsgServ.GetMachineMse(IdxMacc);
}
}
#endregion Protected Methods
#region Private Fields
/// <summary>
/// DateTime ultimo salvataggio in LocalStorage
/// </summary>
private DateTime lastSave = DateTime.Now;
#endregion Private Fields
#region Private Methods
/// <summary>
/// Ricevuto nuovi dati da mostrare!
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <exception cref="NotImplementedException"></exception>
private void DataPipe_EA_NewMessage(object? sender, EventArgs e)
{
PubSubEventArgs currArgs = (PubSubEventArgs)e;
// conversione on-the-fly List<string> --> allarmi
if (!string.IsNullOrEmpty(currArgs.newMessage))
{
try
{
List<MappaStatoExpl>? dataList = JsonConvert.DeserializeObject<List<MappaStatoExpl>>(currArgs.newMessage);
if (dataList != null)
{
InvokeAsync(() => RefreshData(dataList));
}
}
catch
{ }
}
InvokeAsync(() =>
{
StateHasChanged();
});
}
protected virtual async Task ReloadData()
{
if (string.IsNullOrEmpty(IdxMacc))
{
IdxMacc = await MsgServ.IdxMaccGet();
}
// recupero MSE macchina....
if (!string.IsNullOrEmpty(IdxMacc))
{
CurrMSE = await MsgServ.GetMachineMse(IdxMacc);
}
}
#endregion Private Methods
}
}
+2
View File
@@ -1,5 +1,7 @@
@page "/machine-detail"
@inherits BasePage
@if (string.IsNullOrEmpty(IdxMacc) || CurrMSE == null)
{
<EgwCoreLib.Razor.LoadingData></EgwCoreLib.Razor.LoadingData>
+6 -103
View File
@@ -7,77 +7,31 @@ using NLog;
namespace MP_TAB3.Pages
{
public partial class MachineDetail : IDisposable
public partial class MachineDetail
{
#region Public Methods
public void Dispose()
{
IdxMacc = "";
CurrMSE = null;
TDFeeder.dataPipe.EA_NewMessage -= DataPipe_EA_NewMessage;
}
#endregion Public Methods
#region Protected Properties
protected MappaStatoExpl? CurrMSE { get; set; } = null;
protected string IdxMacc { get; set; } = "";
[Inject]
protected MessageService MsgServ { get; set; } = null!;
[Inject]
protected SharedMemService MStor { get; set; } = null!;
[Inject]
protected TabDataService TabServ { get; set; } = null!;
[Inject]
protected TabDataFeeder TDFeeder { get; set; } = null!;
#endregion Protected Properties
#region Protected Methods
protected override async Task OnInitializedAsync()
{
await ReloadData();
TDFeeder.dataPipe.EA_NewMessage += DataPipe_EA_NewMessage;
await base.OnInitializedAsync();
// recupero parametri configurazione...
await setupConf();
if (enableSchedaTecnica)
{
checkLottiOdl();
}
}
protected async Task RefreshData(List<MappaStatoExpl> newList)
{
var ListMSE = newList;
if (!string.IsNullOrEmpty(IdxMacc))
{
var rawData = ListMSE.Find(x => x.IdxMacchina == IdxMacc);
if (rawData != null)
{
CurrMSE = rawData;
}
else
{
await RefreshMBlock();
}
}
}
protected async Task RefreshMBlock()
{
// recupero MSE macchina....
if (!string.IsNullOrEmpty(IdxMacc))
{
CurrMSE = await MsgServ.GetMachineMse(IdxMacc);
}
}
protected void SetMacc(string selIdxMacc)
{
IdxMaccSubSel = selIdxMacc;
@@ -88,12 +42,10 @@ namespace MP_TAB3.Pages
#region Private Fields
private static Logger Log = LogManager.GetCurrentClassLogger();
private bool enableMagLotti = false;
private bool enableSchedaTecnica = false;
private string IdxMaccSubSel = "";
private DateTime lastSave = DateTime.Now;
#endregion Private Fields
@@ -123,56 +75,7 @@ namespace MP_TAB3.Pages
#endif
}
/// <summary>
/// Ricevuto nuovi dati da mostrare!
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <exception cref="NotImplementedException"></exception>
private void DataPipe_EA_NewMessage(object? sender, EventArgs e)
{
PubSubEventArgs currArgs = (PubSubEventArgs)e;
// conversione on-the-fly List<string> --> allarmi
if (!string.IsNullOrEmpty(currArgs.newMessage))
{
try
{
List<MappaStatoExpl>? dataList = JsonConvert.DeserializeObject<List<MappaStatoExpl>>(currArgs.newMessage);
if (dataList != null)
{
InvokeAsync(() => RefreshData(dataList));
}
}
catch
{ }
}
InvokeAsync(() =>
{
StateHasChanged();
});
}
private async Task ReloadData()
{
try
{
if (string.IsNullOrEmpty(IdxMacc))
{
IdxMacc = await MsgServ.IdxMaccGet();
}
// recupero MSE macchina....
if (!string.IsNullOrEmpty(IdxMacc))
{
CurrMSE = await MsgServ.GetMachineMse(IdxMacc);
}
}
catch (Exception exc)
{
Log.Error($"Eccezione in ReloadData{Environment.NewLine}{exc}");
}
// recupero parametri configurazione...
await setupConf();
}
private async Task setupConf()
{
+1
View File
@@ -1,4 +1,5 @@
@page "/odl"
@inherits BasePage
@if (string.IsNullOrEmpty(IdxMacc) || CurrMSE == null)
{
+2 -109
View File
@@ -7,65 +7,13 @@ using NLog;
namespace MP_TAB3.Pages
{
public partial class ODL : IDisposable
public partial class ODL
{
#region Public Methods
public void Dispose()
{
IdxMacc = "";
CurrMSE = null;
TDFeeder.dataPipe.EA_NewMessage -= DataPipe_EA_NewMessage;
}
#endregion Public Methods
#region Protected Properties
protected MappaStatoExpl? CurrMSE { get; set; } = null;
protected string IdxMacc { get; set; } = "";
[Inject]
protected MessageService MsgServ { get; set; } = null!;
[Inject]
protected TabDataFeeder TDFeeder { get; set; } = null!;
#endregion Protected Properties
#region Protected Methods
protected override async Task OnInitializedAsync()
{
await ReloadData();
TDFeeder.dataPipe.EA_NewMessage += DataPipe_EA_NewMessage;
}
protected async Task RefreshData(List<MappaStatoExpl> newList)
{
var ListMSE = newList;
if (!string.IsNullOrEmpty(IdxMacc))
{
var rawData = ListMSE.Find(x => x.IdxMacchina == IdxMacc);
if (rawData != null)
{
CurrMSE = rawData;
}
else
{
await RefreshMBlock();
}
}
}
protected async Task RefreshMBlock()
{
// recupero MSE macchina....
if (!string.IsNullOrEmpty(IdxMacc))
{
CurrMSE = await MsgServ.GetMachineMse(IdxMacc);
}
await base.OnInitializedAsync();
}
protected void SetMacc(string selIdxMacc)
@@ -77,63 +25,8 @@ namespace MP_TAB3.Pages
#region Private Fields
private static Logger Log = LogManager.GetCurrentClassLogger();
private string IdxMaccSubSel = "";
#endregion Private Fields
#region Private Methods
/// <summary>
/// Ricevuto nuovi dati da mostrare!
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <exception cref="NotImplementedException"></exception>
private void DataPipe_EA_NewMessage(object? sender, EventArgs e)
{
PubSubEventArgs currArgs = (PubSubEventArgs)e;
// conversione on-the-fly List<string> --> allarmi
if (!string.IsNullOrEmpty(currArgs.newMessage))
{
try
{
List<MappaStatoExpl>? dataList = JsonConvert.DeserializeObject<List<MappaStatoExpl>>(currArgs.newMessage);
if (dataList != null)
{
InvokeAsync(() => RefreshData(dataList));
}
}
catch
{ }
}
InvokeAsync(() =>
{
StateHasChanged();
});
}
private async Task ReloadData()
{
try
{
if (string.IsNullOrEmpty(IdxMacc))
{
IdxMacc = await MsgServ.IdxMaccGet();
}
// recupero MSE macchina....
if (!string.IsNullOrEmpty(IdxMacc))
{
CurrMSE = await MsgServ.GetMachineMse(IdxMacc);
}
}
catch (Exception exc)
{
Log.Error($"Eccezione in ReloadData{Environment.NewLine}{exc}");
}
}
#endregion Private Methods
}
}
+1
View File
@@ -1,4 +1,5 @@
@page "/parameters"
@inherits BasePage
@if (string.IsNullOrEmpty(IdxMacc) || CurrMSE == null)
{
+2 -93
View File
@@ -4,108 +4,17 @@ using MP.Data.DatabaseModels;
using MP.Data.Services;
using Newtonsoft.Json;
namespace MP_TAB3.Pages
{
public partial class Parameters : IDisposable
public partial class Parameters
{
#region Public Methods
public void Dispose()
{
IdxMacc = "";
CurrMSE = null;
TDFeeder.dataPipe.EA_NewMessage -= DataPipe_EA_NewMessage;
}
#endregion Public Methods
#region Protected Properties
protected MappaStatoExpl? CurrMSE { get; set; } = null;
protected string IdxMacc { get; set; } = "";
[Inject]
protected MessageService MsgServ { get; set; } = null!;
[Inject]
protected TabDataFeeder TDFeeder { get; set; } = null!;
#endregion Protected Properties
#region Protected Methods
protected override async Task OnInitializedAsync()
{
await ReloadData();
TDFeeder.dataPipe.EA_NewMessage += DataPipe_EA_NewMessage;
}
protected async Task RefreshData(List<MappaStatoExpl> newList)
{
var ListMSE = newList;
if (!string.IsNullOrEmpty(IdxMacc))
{
var rawData = ListMSE.Find(x => x.IdxMacchina == IdxMacc);
if (rawData != null)
{
CurrMSE = rawData;
}
else
{
await ReloadData();
}
}
await InvokeAsync(StateHasChanged);
await base.OnInitializedAsync();
}
#endregion Protected Methods
#region Private Methods
/// <summary>
/// Ricevuto nuovi dati da mostrare!
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <exception cref="NotImplementedException"></exception>
private void DataPipe_EA_NewMessage(object? sender, EventArgs e)
{
PubSubEventArgs currArgs = (PubSubEventArgs)e;
// conversione on-the-fly List<string> --> allarmi
if (!string.IsNullOrEmpty(currArgs.newMessage))
{
try
{
List<MappaStatoExpl>? dataList = JsonConvert.DeserializeObject<List<MappaStatoExpl>>(currArgs.newMessage);
if (dataList != null)
{
InvokeAsync(() => RefreshData(dataList));
}
}
catch
{ }
}
InvokeAsync(() =>
{
StateHasChanged();
});
}
private async Task ReloadData()
{
if (string.IsNullOrEmpty(IdxMacc))
{
IdxMacc = await MsgServ.IdxMaccGet();
// recupero MSE macchina....
if (!string.IsNullOrEmpty(IdxMacc))
{
CurrMSE = await MsgServ.GetMachineMse(IdxMacc);
}
}
}
#endregion Private Methods
}
}
+1
View File
@@ -1,4 +1,5 @@
@page "/prod-stop"
@inherits BasePage
@if (string.IsNullOrEmpty(IdxMacc) || CurrMSE == null)
{
+7 -65
View File
@@ -8,17 +8,10 @@ using NLog;
namespace MP_TAB3.Pages
{
public partial class ProdStop : IDisposable
public partial class ProdStop
{
#region Public Methods
public void Dispose()
{
IdxMacc = "";
CurrMSE = null;
TDFeeder.dataPipe.EA_NewMessage -= DataPipe_EA_NewMessage;
}
#endregion Public Methods
#region Protected Fields
@@ -41,13 +34,11 @@ namespace MP_TAB3.Pages
protected string alertCss { get; set; } = "alert-danger";
protected MappaStatoExpl? CurrMSE { get; set; } = null;
protected DateTime DtRif { get; set; } = DateTime.Now;
protected List<vSelEventiBCodeModel> events2show { get; set; } = new List<vSelEventiBCodeModel>();
protected string IdxMacc { get; set; } = "";
/// <summary>
/// Determina se insert sia Realtime o batch con DataOra (in base a diff tra DataOra
@@ -85,9 +76,6 @@ namespace MP_TAB3.Pages
[Inject]
protected TabDataService TabDServ { get; set; } = null!;
[Inject]
protected TabDataFeeder TDFeeder { get; set; } = null!;
#endregion Protected Properties
#region Protected Methods
@@ -233,6 +221,7 @@ namespace MP_TAB3.Pages
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
dltMinRealtime = SMServ.GetConfInt("dltMinRealtime");
minAnticipoRicalcolo = SMServ.GetConfInt("minAnticipoRicalcolo");
rdm_nEvStep = SMServ.GetConfInt("rdm_nEvStep");
@@ -240,25 +229,9 @@ namespace MP_TAB3.Pages
rdm_ChkOnly = SMServ.GetConfBool("rdm_ChkOnly");
// leggo gli altri dati
await ReloadData();
TDFeeder.dataPipe.EA_NewMessage += DataPipe_EA_NewMessage;
}
protected async Task RefreshData(List<MappaStatoExpl> newList)
{
var ListMSE = newList;
if (!string.IsNullOrEmpty(IdxMacc))
{
var rawData = ListMSE.Find(x => x.IdxMacchina == IdxMacc);
if (rawData != null)
{
CurrMSE = rawData;
}
else
{
await ReloadData();
}
}
}
protected void SetDate(DateTime newDate)
{
@@ -284,47 +257,16 @@ namespace MP_TAB3.Pages
#region Private Methods
/// <summary>
/// Ricevuto nuovi dati da mostrare!
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <exception cref="NotImplementedException"></exception>
private void DataPipe_EA_NewMessage(object? sender, EventArgs e)
{
PubSubEventArgs currArgs = (PubSubEventArgs)e;
// conversione on-the-fly List<string> --> allarmi
if (!string.IsNullOrEmpty(currArgs.newMessage))
{
try
{
List<MappaStatoExpl>? dataList = JsonConvert.DeserializeObject<List<MappaStatoExpl>>(currArgs.newMessage);
if (dataList != null)
{
InvokeAsync(() => RefreshData(dataList));
}
}
catch
{ }
}
InvokeAsync(() =>
{
StateHasChanged();
});
}
private async Task ReloadData()
protected override async Task ReloadData()
{
if (string.IsNullOrEmpty(IdxMacc))
{
try
{
IdxMacc = await MServ.IdxMaccGet();
// recupero MSE macchina....
if (!string.IsNullOrEmpty(IdxMacc))
{
CurrMSE = await MServ.GetMachineMse(IdxMacc);
}
await base.ReloadData();
// recupero eventi
var eventsAll = await TabDServ.AnagEventiGetByMacch(IdxMacc);
if (eventsAll != null)
{
+1
View File
@@ -1,4 +1,5 @@
@page "/tech-sheet"
@inherits BasePage
@if (string.IsNullOrEmpty(IdxMacc) || CurrMSE == null)
{
+4 -81
View File
@@ -6,65 +6,30 @@ using Newtonsoft.Json;
namespace MP_TAB3.Pages
{
public partial class TechSheet : IDisposable
public partial class TechSheet
{
#region Public Methods
public void Dispose()
{
IdxMacc = "";
CurrMSE = null;
TDFeeder.dataPipe.EA_NewMessage -= DataPipe_EA_NewMessage;
}
#endregion Public Methods
#region Protected Properties
protected MappaStatoExpl? CurrMSE { get; set; } = null;
protected string IdxMacc { get; set; } = "";
[Inject]
protected MessageService MsgServ { get; set; } = null!;
[Inject]
protected TabDataService TabDServ { get; set; } = null!;
[Inject]
protected TabDataFeeder TDFeeder { get; set; } = null!;
#endregion Protected Properties
#region Protected Methods
protected override async Task OnInitializedAsync()
{
await ReloadData();
TDFeeder.dataPipe.EA_NewMessage += DataPipe_EA_NewMessage;
await base.OnInitializedAsync();
SetupConf();
}
protected async Task RefreshData(List<MappaStatoExpl> newList)
{
var ListMSE = newList;
if (!string.IsNullOrEmpty(IdxMacc))
{
var rawData = ListMSE.Find(x => x.IdxMacchina == IdxMacc);
if (rawData != null)
{
CurrMSE = rawData;
}
}
await InvokeAsync(StateHasChanged);
}
#endregion Protected Methods
#region Private Fields
private string disMessage = "Gestione schede tecniche di attrezzaggio, collaudo e verifica procedure di setup. Il modulo opzionale attivabile su richiesta.";
private string disMessage = "Gestione schede tecniche di attrezzaggio, collaudo e verifica procedure di setup. Il modulo opzionale &grave; attivabile su richiesta.";
private string disSubtitle = "Funzionalit disattivata";
private string disSubtitle = "Funzionalit&agrave; disattivata";
private string disTitle = "Scheda Tecnica";
@@ -74,48 +39,6 @@ namespace MP_TAB3.Pages
#region Private Methods
/// <summary>
/// Ricevuto nuovi dati da mostrare!
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <exception cref="NotImplementedException"></exception>
private void DataPipe_EA_NewMessage(object? sender, EventArgs e)
{
PubSubEventArgs currArgs = (PubSubEventArgs)e;
// conversione on-the-fly List<string> --> allarmi
if (!string.IsNullOrEmpty(currArgs.newMessage))
{
try
{
List<MappaStatoExpl>? dataList = JsonConvert.DeserializeObject<List<MappaStatoExpl>>(currArgs.newMessage);
if (dataList != null)
{
InvokeAsync(() => RefreshData(dataList));
}
}
catch
{ }
}
InvokeAsync(() =>
{
StateHasChanged();
});
}
private async Task ReloadData()
{
if (string.IsNullOrEmpty(IdxMacc))
{
IdxMacc = await MsgServ.IdxMaccGet();
// recupero MSE macchina....
if (!string.IsNullOrEmpty(IdxMacc))
{
CurrMSE = await MsgServ.GetMachineMse(IdxMacc);
}
}
}
private void SetupConf()
{
TabDServ.ConfigGetVal("enableSchedaTecnica", ref enableSchedaTecnica);
+1 -1
View File
@@ -1,6 +1,6 @@
<body>
<i>Modulo MAPOSPEC </i>
<h4>Versione: 6.16.2402.2618</h4>
<h4>Versione: 6.16.2402.2620</h4>
<br /> Note di rilascio:
<ul>
<li>
+1 -1
View File
@@ -1 +1 @@
6.16.2402.2618
6.16.2402.2620
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<item>
<version>6.16.2402.2618</version>
<version>6.16.2402.2620</version>
<url>https://nexus.steamware.net/repository/SWS/MP-TAB3/stable/LAST/MP-TAB3.zip</url>
<changelog>https://nexus.steamware.net/repository/SWS/MP-TAB3/stable/LAST/ChangeLog.html</changelog>
<mandatory>false</mandatory>
Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB