mirror of
https://github.com/Nioux/AideDeJeu.git
synced 2025-10-31 23:45:39 +00:00
Mode dual
This commit is contained in:
parent
06b6a7cdc3
commit
ab4758555b
7 changed files with 113 additions and 83 deletions
|
|
@ -23,8 +23,10 @@ namespace AideDeJeu.Tools
|
||||||
if (string.IsNullOrEmpty(x)) return 1;
|
if (string.IsNullOrEmpty(x)) return 1;
|
||||||
if (string.IsNullOrEmpty(y)) return -1;
|
if (string.IsNullOrEmpty(y)) return -1;
|
||||||
var regex = new Regex(@"\((?<xp>\d*?) (PX|XP)\)");
|
var regex = new Regex(@"\((?<xp>\d*?) (PX|XP)\)");
|
||||||
int xpx = int.Parse(regex.Match(x).Groups["xp"].Value);
|
int xpx;
|
||||||
int xpy = int.Parse(regex.Match(y).Groups["xp"].Value);
|
int.TryParse(regex.Match(x).Groups["xp"].Value, out xpx);
|
||||||
|
int xpy;
|
||||||
|
int.TryParse(regex.Match(y).Groups["xp"].Value, out xpy);
|
||||||
return xpx - xpy;
|
return xpx - xpy;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,11 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.Serialization.Json;
|
using System.Runtime.Serialization.Json;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace AideDeJeu.Tools
|
namespace AideDeJeu.Tools
|
||||||
{
|
{
|
||||||
|
|
@ -20,6 +22,14 @@ namespace AideDeJeu.Tools
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async Task<string> GetStringFromUrl(string url)
|
||||||
|
{
|
||||||
|
using (var client = new HttpClient())
|
||||||
|
{
|
||||||
|
return await client.GetStringAsync(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static string RemoveDiacritics(string text)
|
public static string RemoveDiacritics(string text)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(text))
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,21 @@ namespace AideDeJeu.Tools
|
||||||
{
|
{
|
||||||
public static class MarkdownExtensions
|
public static class MarkdownExtensions
|
||||||
{
|
{
|
||||||
|
public static IEnumerable<Spell> MarkdownToSpells(string md)
|
||||||
|
{
|
||||||
|
var document = Markdig.Parsers.MarkdownParser.Parse(md);
|
||||||
|
return document.ToSpells();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<Monster> MarkdownToMonsters(string md)
|
||||||
|
{
|
||||||
|
var document = Markdig.Parsers.MarkdownParser.Parse(md);
|
||||||
|
return document.ToMonsters();
|
||||||
|
}
|
||||||
|
|
||||||
public static IEnumerable<Spell> ToSpells(this Markdig.Syntax.MarkdownDocument document)
|
public static IEnumerable<Spell> ToSpells(this Markdig.Syntax.MarkdownDocument document)
|
||||||
{
|
{
|
||||||
//var spells = new List<Spell>();
|
var spells = new List<Spell>();
|
||||||
Spell spell = null;
|
Spell spell = null;
|
||||||
foreach (var block in document)
|
foreach (var block in document)
|
||||||
{
|
{
|
||||||
|
|
@ -27,11 +39,11 @@ namespace AideDeJeu.Tools
|
||||||
{
|
{
|
||||||
if (spell != null)
|
if (spell != null)
|
||||||
{
|
{
|
||||||
//spells.Add(spell);
|
spells.Add(spell);
|
||||||
yield return spell;
|
//yield return spell;
|
||||||
}
|
}
|
||||||
spell = new Spell();
|
spell = new Spell();
|
||||||
spell.Name = spell.NamePHB = headingBlock.Inline.ToContainerString();
|
spell.Id = spell.IdVF = spell.IdVO = spell.Name = spell.NamePHB = headingBlock.Inline.ToContainerString();
|
||||||
//Console.WriteLine(spell.Name);
|
//Console.WriteLine(spell.Name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -130,15 +142,15 @@ namespace AideDeJeu.Tools
|
||||||
}
|
}
|
||||||
if (spell != null)
|
if (spell != null)
|
||||||
{
|
{
|
||||||
yield return spell;
|
//yield return spell;
|
||||||
//spells.Add(spell);
|
spells.Add(spell);
|
||||||
}
|
}
|
||||||
//return spells;
|
return spells;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IEnumerable<Monster> ToMonsters(this Markdig.Syntax.MarkdownDocument document)
|
public static IEnumerable<Monster> ToMonsters(this Markdig.Syntax.MarkdownDocument document)
|
||||||
{
|
{
|
||||||
//var monsters = new List<Monster>();
|
var monsters = new List<Monster>();
|
||||||
Monster monster = null;
|
Monster monster = null;
|
||||||
List<string> actions = new List<string>();
|
List<string> actions = new List<string>();
|
||||||
foreach (var block in document)
|
foreach (var block in document)
|
||||||
|
|
@ -152,8 +164,8 @@ namespace AideDeJeu.Tools
|
||||||
{
|
{
|
||||||
if (monster != null)
|
if (monster != null)
|
||||||
{
|
{
|
||||||
//monsters.Add(monster);
|
monsters.Add(monster);
|
||||||
yield return monster;
|
//yield return monster;
|
||||||
}
|
}
|
||||||
monster = new Monster();
|
monster = new Monster();
|
||||||
monster.Name = monster.NamePHB = headingBlock.Inline.ToContainerString();
|
monster.Name = monster.NamePHB = headingBlock.Inline.ToContainerString();
|
||||||
|
|
@ -278,10 +290,10 @@ namespace AideDeJeu.Tools
|
||||||
}
|
}
|
||||||
if (monster != null)
|
if (monster != null)
|
||||||
{
|
{
|
||||||
//monsters.Add(monster);
|
monsters.Add(monster);
|
||||||
yield return monster;
|
//yield return monster;
|
||||||
}
|
}
|
||||||
//return monsters;
|
return monsters;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string ToString(this Markdig.Syntax.SourceSpan span, string md)
|
public static string ToString(this Markdig.Syntax.SourceSpan span, string md)
|
||||||
|
|
@ -293,7 +305,7 @@ namespace AideDeJeu.Tools
|
||||||
var str = string.Empty;
|
var str = string.Empty;
|
||||||
foreach (var inline in inlines)
|
foreach (var inline in inlines)
|
||||||
{
|
{
|
||||||
Debug.WriteLine(inline.GetType());
|
//Debug.WriteLine(inline.GetType());
|
||||||
string add = string.Empty;
|
string add = string.Empty;
|
||||||
if (inline is Markdig.Syntax.Inlines.LineBreakInline)
|
if (inline is Markdig.Syntax.Inlines.LineBreakInline)
|
||||||
{
|
{
|
||||||
|
|
@ -323,7 +335,7 @@ namespace AideDeJeu.Tools
|
||||||
{
|
{
|
||||||
add = inline.ToString();
|
add = inline.ToString();
|
||||||
}
|
}
|
||||||
Debug.WriteLine(add);
|
//Debug.WriteLine(add);
|
||||||
str += add;
|
str += add;
|
||||||
}
|
}
|
||||||
return str;
|
return str;
|
||||||
|
|
@ -425,7 +437,7 @@ namespace AideDeJeu.Tools
|
||||||
}
|
}
|
||||||
public static void Dump(this Markdig.Syntax.ListBlock block)
|
public static void Dump(this Markdig.Syntax.ListBlock block)
|
||||||
{
|
{
|
||||||
Debug.WriteLine(block.BulletType);
|
//Debug.WriteLine(block.BulletType);
|
||||||
foreach (var inblock in block)
|
foreach (var inblock in block)
|
||||||
{
|
{
|
||||||
inblock.Dump();
|
inblock.Dump();
|
||||||
|
|
@ -440,8 +452,8 @@ namespace AideDeJeu.Tools
|
||||||
}
|
}
|
||||||
public static void Dump(this Markdig.Syntax.HeadingBlock block)
|
public static void Dump(this Markdig.Syntax.HeadingBlock block)
|
||||||
{
|
{
|
||||||
Debug.WriteLine(block.HeaderChar);
|
//Debug.WriteLine(block.HeaderChar);
|
||||||
Debug.WriteLine(block.Level);
|
//Debug.WriteLine(block.Level);
|
||||||
//foreach(var line in block.Lines.Lines)
|
//foreach(var line in block.Lines.Lines)
|
||||||
//{
|
//{
|
||||||
// DumpStringLine(line);
|
// DumpStringLine(line);
|
||||||
|
|
@ -453,14 +465,14 @@ namespace AideDeJeu.Tools
|
||||||
}
|
}
|
||||||
public static void Dump(this Markdig.Syntax.Block block)
|
public static void Dump(this Markdig.Syntax.Block block)
|
||||||
{
|
{
|
||||||
Debug.WriteLine(block.Column);
|
//Debug.WriteLine(block.Column);
|
||||||
Debug.WriteLine(block.IsBreakable);
|
//Debug.WriteLine(block.IsBreakable);
|
||||||
Debug.WriteLine(block.IsOpen);
|
//Debug.WriteLine(block.IsOpen);
|
||||||
Debug.WriteLine(block.Line);
|
//Debug.WriteLine(block.Line);
|
||||||
Debug.WriteLine(block.RemoveAfterProcessInlines);
|
//Debug.WriteLine(block.RemoveAfterProcessInlines);
|
||||||
Debug.WriteLine(block.Span.ToString());
|
//Debug.WriteLine(block.Span.ToString());
|
||||||
//Debug.WriteLine(block.Span.ToString(MD));
|
//Debug.WriteLine(block.Span.ToString(MD));
|
||||||
Debug.WriteLine(block.ToString());
|
//Debug.WriteLine(block.ToString());
|
||||||
if (block is Markdig.Syntax.ParagraphBlock)
|
if (block is Markdig.Syntax.ParagraphBlock)
|
||||||
{
|
{
|
||||||
(block as Markdig.Syntax.ParagraphBlock).Dump();
|
(block as Markdig.Syntax.ParagraphBlock).Dump();
|
||||||
|
|
@ -482,7 +494,7 @@ namespace AideDeJeu.Tools
|
||||||
{
|
{
|
||||||
foreach (var block in document)
|
foreach (var block in document)
|
||||||
{
|
{
|
||||||
Debug.WriteLine(block.GetType());
|
//Debug.WriteLine(block.GetType());
|
||||||
//block.Dump();
|
//block.Dump();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -127,12 +127,12 @@ namespace AideDeJeu.ViewModels
|
||||||
return items.Where(item =>
|
return items.Where(item =>
|
||||||
{
|
{
|
||||||
var spell = item as Spell;
|
var spell = item as Spell;
|
||||||
return (int.Parse(spell.Level) >= int.Parse(niveauMin)) &&
|
return //(int.Parse(spell.Level) >= int.Parse(niveauMin)) &&
|
||||||
(int.Parse(spell.Level) <= int.Parse(niveauMax)) &&
|
//(int.Parse(spell.Level) <= int.Parse(niveauMax)) &&
|
||||||
spell.Type.ToLower().Contains(ecole.ToLower()) &&
|
//spell.Type.ToLower().Contains(ecole.ToLower()) &&
|
||||||
spell.Source.Contains(source) &&
|
spell.Source.Contains(source) &&
|
||||||
spell.Source.Contains(classe) &&
|
spell.Source.Contains(classe) &&
|
||||||
spell.Type.Contains(rituel) &&
|
//spell.Type.Contains(rituel) &&
|
||||||
Helpers.RemoveDiacritics(spell.NamePHB).ToLower().Contains(Helpers.RemoveDiacritics(SearchText).ToLower());
|
Helpers.RemoveDiacritics(spell.NamePHB).ToLower().Contains(Helpers.RemoveDiacritics(SearchText).ToLower());
|
||||||
}).OrderBy(spell => spell.NamePHB)
|
}).OrderBy(spell => spell.NamePHB)
|
||||||
.AsEnumerable();
|
.AsEnumerable();
|
||||||
|
|
@ -373,8 +373,9 @@ namespace AideDeJeu.ViewModels
|
||||||
return items.Where(item =>
|
return items.Where(item =>
|
||||||
{
|
{
|
||||||
var monster = item as Monster;
|
var monster = item as Monster;
|
||||||
return monster.Type.Contains(type) &&
|
return
|
||||||
(string.IsNullOrEmpty(size) || monster.Size.Equals(size)) &&
|
//monster.Type.Contains(type) &&
|
||||||
|
//(string.IsNullOrEmpty(size) || monster.Size.Equals(size)) &&
|
||||||
monster.Source.Contains(source) &&
|
monster.Source.Contains(source) &&
|
||||||
powerComparer.Compare(monster.Challenge, minPower) >= 0 &&
|
powerComparer.Compare(monster.Challenge, minPower) >= 0 &&
|
||||||
powerComparer.Compare(monster.Challenge, maxPower) <= 0 &&
|
powerComparer.Compare(monster.Challenge, maxPower) <= 0 &&
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ using System.Threading.Tasks;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace AideDeJeu.ViewModels
|
namespace AideDeJeu.ViewModels
|
||||||
{
|
{
|
||||||
|
|
@ -28,9 +29,7 @@ namespace AideDeJeu.ViewModels
|
||||||
|
|
||||||
|
|
||||||
private IEnumerable<Item> _AllItems = null;
|
private IEnumerable<Item> _AllItems = null;
|
||||||
public IEnumerable<Item> AllItems
|
public async Task<IEnumerable<Item>> GetAllItemsAsync()
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
{
|
||||||
if (_AllItems == null)
|
if (_AllItems == null)
|
||||||
{
|
{
|
||||||
|
|
@ -39,35 +38,38 @@ namespace AideDeJeu.ViewModels
|
||||||
{
|
{
|
||||||
case ItemSourceType.MonsterVF:
|
case ItemSourceType.MonsterVF:
|
||||||
resourceName = "AideDeJeu.Data.monsters_vf.json";
|
resourceName = "AideDeJeu.Data.monsters_vf.json";
|
||||||
|
_AllItems = Tools.Helpers.GetResourceObject<IEnumerable<Monster>>(resourceName);
|
||||||
break;
|
break;
|
||||||
case ItemSourceType.MonsterVO:
|
case ItemSourceType.MonsterVO:
|
||||||
resourceName = "AideDeJeu.Data.monsters_vo.json";
|
resourceName = "AideDeJeu.Data.monsters_vo.json";
|
||||||
|
_AllItems = Tools.Helpers.GetResourceObject<IEnumerable<Monster>>(resourceName);
|
||||||
break;
|
break;
|
||||||
case ItemSourceType.MonsterHD:
|
case ItemSourceType.MonsterHD:
|
||||||
resourceName = "AideDeJeu.Data.monsters_hd.json";
|
resourceName = "AideDeJeu.Data.monsters_hd.json";
|
||||||
|
var mdm = await Tools.Helpers.GetStringFromUrl("https://raw.githubusercontent.com/Nioux/AideDeJeu/master/Data/monsters_hd.md");
|
||||||
|
_AllItems = Tools.MarkdownExtensions.MarkdownToMonsters(mdm);
|
||||||
|
//_AllItems = Tools.Helpers.GetResourceObject<IEnumerable<Monster>>(resourceName);
|
||||||
break;
|
break;
|
||||||
case ItemSourceType.SpellVF:
|
case ItemSourceType.SpellVF:
|
||||||
resourceName = "AideDeJeu.Data.spells_vf.json";
|
resourceName = "AideDeJeu.Data.spells_vf.json";
|
||||||
|
_AllItems = Tools.Helpers.GetResourceObject<IEnumerable<Spell>>(resourceName);
|
||||||
|
//var md2 = await Tools.Helpers.GetStringFromUrl("https://raw.githubusercontent.com/Nioux/AideDeJeu/master/Data/spells_hd.md");
|
||||||
|
//_AllItems = Tools.MarkdownExtensions.MarkdownToSpells(md2).ToList();
|
||||||
break;
|
break;
|
||||||
case ItemSourceType.SpellVO:
|
case ItemSourceType.SpellVO:
|
||||||
resourceName = "AideDeJeu.Data.spells_vo.json";
|
resourceName = "AideDeJeu.Data.spells_vo.json";
|
||||||
|
_AllItems = Tools.Helpers.GetResourceObject<IEnumerable<Spell>>(resourceName);
|
||||||
break;
|
break;
|
||||||
case ItemSourceType.SpellHD:
|
case ItemSourceType.SpellHD:
|
||||||
resourceName = "AideDeJeu.Data.spells_hd.json";
|
resourceName = "AideDeJeu.Data.spells_hd.json";
|
||||||
|
var mds = await Tools.Helpers.GetStringFromUrl("https://raw.githubusercontent.com/Nioux/AideDeJeu/master/Data/spells_hd.md");
|
||||||
|
_AllItems = Tools.MarkdownExtensions.MarkdownToSpells(mds);
|
||||||
|
//_AllItems = Tools.Helpers.GetResourceObject<IEnumerable<Spell>>(resourceName);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (ItemSourceType.HasFlag(ItemSourceType.Spell))
|
|
||||||
{
|
|
||||||
_AllItems = Tools.Helpers.GetResourceObject<IEnumerable<Spell>>(resourceName);
|
|
||||||
}
|
|
||||||
else if (ItemSourceType.HasFlag(ItemSourceType.Monster))
|
|
||||||
{
|
|
||||||
_AllItems = Tools.Helpers.GetResourceObject<IEnumerable<Monster>>(resourceName);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return _AllItems;
|
return _AllItems;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
async Task LoadItemsAsync(CancellationToken token = default)
|
async Task LoadItemsAsync(CancellationToken token = default)
|
||||||
{
|
{
|
||||||
|
|
@ -77,19 +79,19 @@ namespace AideDeJeu.ViewModels
|
||||||
{
|
{
|
||||||
// Yan : c'est pas plutôt cette partie qui devrait être dans une autre Task ?
|
// Yan : c'est pas plutôt cette partie qui devrait être dans une autre Task ?
|
||||||
var filterViewModel = Main.GetFilterViewModel(ItemSourceType);
|
var filterViewModel = Main.GetFilterViewModel(ItemSourceType);
|
||||||
var items = await filterViewModel.FilterItems(AllItems, token);
|
var items = await filterViewModel.FilterItems(await GetAllItemsAsync(), token);
|
||||||
|
Main.Items = items;
|
||||||
await Task.Run(async () => {
|
//await Task.Run(async () => {
|
||||||
// Yan : plus besoin de boucle si on change toute la liste d'un coup ;)
|
// Yan : plus besoin de boucle si on change toute la liste d'un coup ;)
|
||||||
// Yan : indispensable de repasser sur l'ui thread pour la version uwp
|
// Yan : indispensable de repasser sur l'ui thread pour la version uwp
|
||||||
Device.BeginInvokeOnMainThread(() => Main.Items = items);
|
//Device.BeginInvokeOnMainThread(() => Main.Items = items);
|
||||||
//Main.Items.Clear();
|
//Main.Items.Clear();
|
||||||
//foreach (var item in items)
|
//foreach (var item in items)
|
||||||
//{
|
//{
|
||||||
// token.ThrowIfCancellationRequested();
|
// token.ThrowIfCancellationRequested();
|
||||||
// Main.Items.Add(item);
|
// Main.Items.Add(item);
|
||||||
//}
|
//}
|
||||||
}, cancellationToken: token); // Yan : c'est ici qu'il faudrait coller le token non ?
|
//}, cancellationToken: token); // Yan : c'est ici qu'il faudrait coller le token non ?
|
||||||
|
|
||||||
//On arrete le loading ici car on annule toujours avant de lancer une nouvelle opération
|
//On arrete le loading ici car on annule toujours avant de lancer une nouvelle opération
|
||||||
// Yan : ?? du coup le IsLoading repasse pas à false en cas de cancel ou d'autre exception ?
|
// Yan : ?? du coup le IsLoading repasse pas à false en cas de cancel ou d'autre exception ?
|
||||||
|
|
|
||||||
|
|
@ -139,7 +139,10 @@ namespace AideDeJeu.ViewModels
|
||||||
{
|
{
|
||||||
await GetItemsViewModel(ItemSourceType).ExecuteLoadItemsCommandAsync();
|
await GetItemsViewModel(ItemSourceType).ExecuteLoadItemsCommandAsync();
|
||||||
});
|
});
|
||||||
GotoItemCommand = new Command<Item>(async (item) => await GetItemsViewModel(ItemSourceType).ExecuteGotoItemCommandAsync(item));
|
GotoItemCommand = new Command<Item>(async (item) =>
|
||||||
|
{
|
||||||
|
await GetItemsViewModel(ItemSourceType).ExecuteGotoItemCommandAsync(item);
|
||||||
|
});
|
||||||
SwitchToSpells = new Command(() => ItemSourceType = (ItemSourceType & ~ItemSourceType.Monster) | ItemSourceType.Spell);
|
SwitchToSpells = new Command(() => ItemSourceType = (ItemSourceType & ~ItemSourceType.Monster) | ItemSourceType.Spell);
|
||||||
SwitchToMonsters = new Command(() => ItemSourceType = (ItemSourceType & ~ItemSourceType.Spell) | ItemSourceType.Monster);
|
SwitchToMonsters = new Command(() => ItemSourceType = (ItemSourceType & ~ItemSourceType.Spell) | ItemSourceType.Monster);
|
||||||
SwitchToVF = new Command(() => ItemSourceType = (ItemSourceType & ~ItemSourceType.VO & ~ItemSourceType.HD) | ItemSourceType.VF);
|
SwitchToVF = new Command(() => ItemSourceType = (ItemSourceType & ~ItemSourceType.VO & ~ItemSourceType.HD) | ItemSourceType.VF);
|
||||||
|
|
|
||||||
|
|
@ -63,8 +63,8 @@ namespace AideDeJeu.ViewModels
|
||||||
{
|
{
|
||||||
var fd = FormatedTextHelpers.FontData.FromResource("contentital");
|
var fd = FormatedTextHelpers.FontData.FromResource("contentital");
|
||||||
var fs = new FormattedString();
|
var fs = new FormattedString();
|
||||||
var capType = Item.Type.First().ToString().ToUpper() + Item.Type.Substring(1);
|
var capType = Item?.Type?.First().ToString()?.ToUpper() + Item?.Type?.Substring(1);
|
||||||
fs.Spans.Add(new Span() { Text = string.Format("{0} de niveau {1}", capType, Item.Level), FontFamily = fd.FontFamily, FontAttributes = fd.FontAttributes, FontSize = fd.FontSize, ForegroundColor = fd.TextColor});
|
fs.Spans.Add(new Span() { Text = string.Format("{0} de niveau {1}", capType, Item?.Level), FontFamily = fd.FontFamily, FontAttributes = fd.FontAttributes, FontSize = fd.FontSize, ForegroundColor = fd.TextColor});
|
||||||
return fs;
|
return fs;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue