Implement app controls and filters in UpToolEto
This commit is contained in:
parent
5814336814
commit
531a1c1e34
@ -139,7 +139,7 @@ namespace UpTool2
|
||||
args.Graphics.DrawImage(sidebarIcon.BackgroundImage, args.ClipRectangle,
|
||||
new Rectangle(new Point(0, 0), sidebarIcon.BackgroundImage.Size), GraphicsUnit.Pixel);
|
||||
};
|
||||
bool updateable = !app.Local && (app.Status & Status.Updatable) == Status.Updatable;
|
||||
bool updateable = !app.Local && app.Status.Contains(Status.Updatable);
|
||||
sidebarIcon.Click += (sender, e) =>
|
||||
{
|
||||
infoPanel_Title.Text = app.Name;
|
||||
@ -164,7 +164,7 @@ namespace UpTool2
|
||||
else
|
||||
action_update.ResetBackColor();
|
||||
action_run.Tag = app;
|
||||
action_run.Enabled = (app.Status & Status.Installed) == Status.Installed && !app.Local &&
|
||||
action_run.Enabled = app.Status.Contains(Status.Installed) && !app.Local &&
|
||||
app.Runnable && Directory.Exists(app.AppPath);
|
||||
};
|
||||
if (updateable)
|
||||
@ -248,8 +248,7 @@ namespace UpTool2
|
||||
{
|
||||
Panel sidebarIcon = (Panel) sidebarPanel.Controls[i];
|
||||
App app = (App) sidebarIcon.Tag;
|
||||
sidebarIcon.Visible = apps.Contains(app) &&
|
||||
((int) app.Status & (int) (Program.Online ? status : Status.Installed)) != 0;
|
||||
sidebarIcon.Visible = apps.Contains(app) && app.Status.Contains(Program.Online ? status : Status.Installed);
|
||||
}
|
||||
ClearSelection();
|
||||
}
|
||||
|
@ -43,14 +43,14 @@ namespace UpToolCLI
|
||||
private static void List()
|
||||
{
|
||||
Program.Lib.V2.RepoManagement.GetReposFromDisk();
|
||||
Console.WriteLine(Program.Lib.V1.Apps.Where(s => (s.Value.Status & Status.Installed) == Status.Installed)
|
||||
Console.WriteLine(Program.Lib.V1.Apps.Where(s => s.Value.Status.Contains(Status.Installed))
|
||||
.ToStringTable(new[]
|
||||
{
|
||||
"Name", "State", "Guid"
|
||||
},
|
||||
u => u.Value.Name,
|
||||
u => u.Value.Local ? "Local" :
|
||||
(u.Value.Status & Status.Updatable) == Status.Updatable ? "Updatable" : "None",
|
||||
u.Value.Status.Contains(Status.Updatable) ? "Updatable" : "None",
|
||||
u => u.Key));
|
||||
}
|
||||
|
||||
@ -85,7 +85,7 @@ namespace UpToolCLI
|
||||
Program.Lib.V2.RepoManagement.GetReposFromDisk();
|
||||
Console.WriteLine();
|
||||
IEnumerable<App> tmp = Program.Lib.V1.Apps.Where(s =>
|
||||
(s.Value.Status & Status.Updatable) == Status.Updatable).Select(s => s.Value);
|
||||
s.Value.Status.Contains(Status.Updatable)).Select(s => s.Value);
|
||||
IEnumerable<App> apps = tmp as App[] ?? tmp.ToArray();
|
||||
int updatableCount = apps.Count();
|
||||
Console.WriteLine(updatableCount == 0
|
||||
|
@ -86,7 +86,7 @@ namespace UpToolCLI
|
||||
else
|
||||
{
|
||||
App tmp = apps.First();
|
||||
if ((tmp.Status & Status.Installed) == Status.Installed)
|
||||
if (tmp.Status.Contains(Status.Installed))
|
||||
Console.WriteLine("Package is already installed");
|
||||
else
|
||||
{
|
||||
@ -106,7 +106,7 @@ namespace UpToolCLI
|
||||
else
|
||||
{
|
||||
App tmp = apps.First();
|
||||
if ((tmp.Status & Status.Updatable) == Status.Updatable)
|
||||
if (tmp.Status.Contains(Status.Updatable))
|
||||
{
|
||||
Console.WriteLine($"Upgrading {tmp.Name}");
|
||||
Program.Lib.V1.AppExtras.Update(tmp, force);
|
||||
@ -141,7 +141,7 @@ namespace UpToolCLI
|
||||
else
|
||||
{
|
||||
App tmp = apps.First();
|
||||
if ((tmp.Status & Status.Installed) == Status.Installed)
|
||||
if (tmp.Status.Contains(Status.Installed))
|
||||
{
|
||||
Console.WriteLine($"Removing {tmp.Name}");
|
||||
Program.Lib.V1.AppExtras.Remove(tmp, false);
|
||||
@ -161,7 +161,7 @@ namespace UpToolCLI
|
||||
else
|
||||
{
|
||||
App tmp = apps.First();
|
||||
if ((tmp.Status & Status.Installed) == Status.Installed)
|
||||
if (tmp.Status.Contains(Status.Installed))
|
||||
{
|
||||
Console.WriteLine($"Purging {tmp.Name}");
|
||||
Program.Lib.V1.AppExtras.Remove(tmp, true);
|
||||
@ -176,7 +176,7 @@ namespace UpToolCLI
|
||||
{
|
||||
Program.Lib.V2.RepoManagement.GetReposFromDisk();
|
||||
foreach (KeyValuePair<Guid, App> app in Program.Lib.V1.Apps.Where(s =>
|
||||
(s.Value.Status & Status.Updatable) == Status.Updatable))
|
||||
s.Value.Status.Contains(Status.Updatable)))
|
||||
{
|
||||
Console.WriteLine($"Updating {app.Value.Name}");
|
||||
Program.Lib.V1.AppExtras.Update(app.Value, false);
|
||||
|
65
UpToolEto/UpToolEto/Controls/AppControlButton.cs
Normal file
65
UpToolEto/UpToolEto/Controls/AppControlButton.cs
Normal file
@ -0,0 +1,65 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Eto.Drawing;
|
||||
using Eto.Forms;
|
||||
using UpToolLib.v2;
|
||||
using UpToolLib.v2.TaskQueue;
|
||||
|
||||
namespace UpToolEto.Controls
|
||||
{
|
||||
public abstract class AppControlButton : Button
|
||||
{
|
||||
public abstract void ReloadState();
|
||||
public abstract void SetApp(App app);
|
||||
public abstract void Clear();
|
||||
}
|
||||
|
||||
public class AppControlButton<T> : AppControlButton where T : KnownAppTask
|
||||
{
|
||||
private readonly IList<AppTask> _tasks;
|
||||
private readonly Func<App, bool> _enabledCheck;
|
||||
private readonly Color _defaultColor;
|
||||
private App _app;
|
||||
|
||||
public AppControlButton(string text, Func<App, Action?, AppTask> factory, IList<AppTask> tasks, Action reloadState, Func<App, bool> enabledCheck)
|
||||
{
|
||||
_tasks = tasks;
|
||||
_enabledCheck = enabledCheck;
|
||||
_defaultColor = BackgroundColor;
|
||||
Text = text;
|
||||
Click += (_, _) =>
|
||||
{
|
||||
bool found = false;
|
||||
for (var i = tasks.ToArray().Length - 1; i >= 0; i--)
|
||||
{
|
||||
if (tasks[i] is T t && t.App == _app)
|
||||
{
|
||||
found = true;
|
||||
tasks.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
if (!found)
|
||||
tasks.Add(factory(_app, ReloadState));
|
||||
reloadState();
|
||||
};
|
||||
}
|
||||
|
||||
public override void ReloadState()
|
||||
{
|
||||
Enabled = _enabledCheck(_app);
|
||||
BackgroundColor = _tasks.Any(s => s is T t && t.App == _app) ? Colors.Green : _defaultColor;
|
||||
}
|
||||
|
||||
public override void SetApp(App app)
|
||||
{
|
||||
_app = app;
|
||||
ReloadState();
|
||||
}
|
||||
|
||||
public override void Clear()
|
||||
{
|
||||
Enabled = false;
|
||||
}
|
||||
}
|
||||
}
|
48
UpToolEto/UpToolEto/Controls/AppControls.cs
Normal file
48
UpToolEto/UpToolEto/Controls/AppControls.cs
Normal file
@ -0,0 +1,48 @@
|
||||
using System.Collections.Generic;
|
||||
using Eto.Drawing;
|
||||
using Eto.Forms;
|
||||
using UpToolLib.DataStructures;
|
||||
using UpToolLib.v2;
|
||||
using UpToolLib.v2.TaskQueue;
|
||||
|
||||
namespace UpToolEto.Controls
|
||||
{
|
||||
public class AppControls : StackLayout
|
||||
{
|
||||
private readonly List<AppControlButton> _buttons;
|
||||
public AppControls(TaskFactory factory, IList<AppTask> tasks)
|
||||
{
|
||||
_buttons = new List<AppControlButton>
|
||||
{
|
||||
new AppControlButton<InstallTask>("Install", factory.CreateInstall, tasks, ReloadState,
|
||||
app => !app.Status.Contains(Status.Installed)),
|
||||
new AppControlButton<UpdateTask>("Update", factory.CreateUpdate, tasks, ReloadState,
|
||||
app => app.Status.Contains(Status.Updatable)),
|
||||
new AppControlButton<RemoveTask>("Remove", factory.CreateRemove, tasks, ReloadState,
|
||||
app => app.Status.Contains(Status.Installed))
|
||||
};
|
||||
foreach (AppControlButton button in _buttons) Items.Add(button);
|
||||
if (Main.DebugColors)
|
||||
BackgroundColor = Colors.Yellow;
|
||||
Orientation = Orientation.Horizontal;
|
||||
}
|
||||
|
||||
public void SetApp(App app)
|
||||
{
|
||||
Visible = true;
|
||||
foreach (AppControlButton button in _buttons) button.SetApp(app);
|
||||
ReloadState();
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
Visible = false;
|
||||
foreach (AppControlButton button in _buttons) button.Clear();
|
||||
}
|
||||
|
||||
private void ReloadState()
|
||||
{
|
||||
foreach (AppControlButton button in _buttons) button.ReloadState();
|
||||
}
|
||||
}
|
||||
}
|
@ -2,42 +2,54 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using Eto.Drawing;
|
||||
using Eto.Forms;
|
||||
using UpToolLib.v1.Tool;
|
||||
using UpToolLib.v2;
|
||||
|
||||
namespace UpToolEto.Controls
|
||||
{
|
||||
public class AppList : Scrollable
|
||||
public class AppList : StackLayout
|
||||
{
|
||||
private readonly IDictionary<Guid, App> _apps;
|
||||
private readonly AppExtras _extras;
|
||||
private readonly Action<Guid, App> _itemClickEvent;
|
||||
private readonly StackLayout _layout;
|
||||
private readonly AppListSearchProvider _searchProvider;
|
||||
|
||||
public AppList(IDictionary<Guid, App> apps, Action<Guid, App> itemClickEvent)
|
||||
public AppList(AppExtras extras, Action<Guid, App> itemClickEvent, bool online)
|
||||
{
|
||||
_apps = apps;
|
||||
_extras = extras;
|
||||
_itemClickEvent = itemClickEvent;
|
||||
_searchProvider = new AppListSearchProvider(online, Update);
|
||||
Items.Add(_searchProvider);
|
||||
_layout = new StackLayout
|
||||
{
|
||||
Padding = 10,
|
||||
Orientation = Orientation.Vertical,
|
||||
Width = 200
|
||||
};
|
||||
Content = _layout;
|
||||
Scrollable scrollable = new()
|
||||
{
|
||||
Content = _layout
|
||||
};
|
||||
if (Main.DebugColors)
|
||||
scrollable.BackgroundColor = Colors.YellowGreen;
|
||||
Items.Add(new StackLayoutItem(scrollable, VerticalAlignment.Stretch, true));
|
||||
Orientation = Orientation.Vertical;
|
||||
if (Main.DebugColors)
|
||||
BackgroundColor = Colors.Green;
|
||||
Update();
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
_layout.Items.Clear();
|
||||
foreach ((Guid id, App app) in _apps)
|
||||
{
|
||||
_layout.Items.Add(new Button((_, _) => _itemClickEvent(id, app))
|
||||
{
|
||||
Text = app.Name,
|
||||
Image = (Icon)app.Icon,
|
||||
ImagePosition = ButtonImagePosition.Left,
|
||||
});
|
||||
}
|
||||
foreach (App app in _extras.FindApps(_searchProvider.GetSearchTerms()))
|
||||
if (_searchProvider.Matches(app))
|
||||
_layout.Items.Add(new Button((_, _) => _itemClickEvent(app.Id, app))
|
||||
{
|
||||
Text = app.Name,
|
||||
Image = (Icon)app.Icon,
|
||||
ImagePosition = ButtonImagePosition.Left,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
32
UpToolEto/UpToolEto/Controls/AppListSearchProvider.cs
Normal file
32
UpToolEto/UpToolEto/Controls/AppListSearchProvider.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using Eto.Forms;
|
||||
using UpToolLib.DataStructures;
|
||||
using UpToolLib.v2;
|
||||
|
||||
//TODO implement
|
||||
namespace UpToolEto.Controls
|
||||
{
|
||||
public class AppListSearchProvider : StackLayout
|
||||
{
|
||||
private readonly bool _online;
|
||||
private readonly TextBox _search;
|
||||
private readonly EnumDropDown<Status> _state;
|
||||
public AppListSearchProvider(bool online, Action refresh)
|
||||
{
|
||||
_online = online;
|
||||
_search = new SearchBox();
|
||||
_state = new EnumDropDown<Status>();
|
||||
_state.SelectedValue = online ? Status.NotInstalled : Status.Installed;
|
||||
_state.Enabled = online;
|
||||
_search.TextChanged += (_, _) => refresh();
|
||||
_state.SelectedIndexChanged += (_, _) => refresh();
|
||||
Orientation = Orientation.Vertical;
|
||||
Items.Add(new StackLayoutItem(_search, HorizontalAlignment.Stretch));
|
||||
Items.Add(_state);
|
||||
}
|
||||
|
||||
public bool Matches(App app) => app.Status.Contains(_online ? _state.SelectedValue : Status.Installed);
|
||||
|
||||
public string GetSearchTerms() => _search.Text;
|
||||
}
|
||||
}
|
@ -1,38 +1,55 @@
|
||||
using System.Collections.Generic;
|
||||
using Eto.Drawing;
|
||||
using Eto.Forms;
|
||||
using UpToolLib.DataStructures;
|
||||
using UpToolLib.v1.Tool;
|
||||
using UpToolLib.v2;
|
||||
using UpToolLib.v2.TaskQueue;
|
||||
|
||||
namespace UpToolEto.Controls
|
||||
{
|
||||
public class AppPanel : Panel
|
||||
{
|
||||
private readonly Label _appNameLabel;
|
||||
private readonly Button _appNameLabel;
|
||||
private readonly Label _appDescriptionLabel;
|
||||
public AppPanel()
|
||||
private readonly AppControls _appControls;
|
||||
private App _app;
|
||||
public AppPanel(TaskFactory factory, AppExtras extras, IList<AppTask> tasks)
|
||||
{
|
||||
_appDescriptionLabel = new Label();
|
||||
_appNameLabel = new Label();
|
||||
_appNameLabel = new Button((_, _) => extras.RunApp(_app));
|
||||
_appNameLabel.Font = new Font(_appNameLabel.Font.Family, _appNameLabel.Font.Size * 2);
|
||||
_appControls = new AppControls(factory, tasks);
|
||||
Content = new StackLayout
|
||||
{
|
||||
Items =
|
||||
{
|
||||
_appNameLabel,
|
||||
_appDescriptionLabel
|
||||
}
|
||||
new StackLayoutItem(_appNameLabel, HorizontalAlignment.Center),
|
||||
new StackLayoutItem(_appDescriptionLabel, HorizontalAlignment.Center, true),
|
||||
new StackLayoutItem(_appControls, HorizontalAlignment.Stretch)
|
||||
},
|
||||
Orientation = Orientation.Vertical
|
||||
};
|
||||
if (Main.DebugColors)
|
||||
BackgroundColor = Colors.Red;
|
||||
Clear();
|
||||
}
|
||||
|
||||
public void FromApp(App app)
|
||||
public void SetApp(App app)
|
||||
{
|
||||
_app = app;
|
||||
_appNameLabel.Text = app.Name;
|
||||
_appNameLabel.Enabled = app.Status.Contains(Status.Installed) && app.Runnable;
|
||||
_appDescriptionLabel.Text = app.Description;
|
||||
_appControls.SetApp(app);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_appNameLabel.Text = "Welcome to UpTool2";
|
||||
_appNameLabel.Enabled = false;
|
||||
_appDescriptionLabel.Text = "Select an app to get started";
|
||||
_appControls.Clear();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,32 +1,34 @@
|
||||
using System.Collections.Generic;
|
||||
using Eto.Drawing;
|
||||
using Eto.Forms;
|
||||
using UpToolEto.Controls;
|
||||
using UpToolLib;
|
||||
using UpToolLib.DataStructures;
|
||||
using UpToolLib.v2.TaskQueue;
|
||||
|
||||
//TODO implement tasks queue UI
|
||||
//TODO keep track of app state
|
||||
//TODO app filters (search by name/tags)
|
||||
//TODO add control_upload action_run action_update action_remove action_install
|
||||
//TODO implement tasks queue UI and add control to execute queue, also needs to clear AppPanel and reload AppList
|
||||
//TODO add control_upload
|
||||
namespace UpToolEto.Forms
|
||||
{
|
||||
public partial class MainForm : Form
|
||||
{
|
||||
private readonly AppPanel _appPanel;
|
||||
private readonly IList<AppTask> _tasks;
|
||||
public MainForm(InitScreen init, UpToolLibMain lib, IExternalFunctionality platform, bool online)
|
||||
{
|
||||
_tasks = new List<AppTask>();
|
||||
Title = "UpTool2";
|
||||
MinimumSize = new Size(600, 100);
|
||||
AppList appList = new(lib.V1.Apps, (guid, app) => _appPanel.FromApp(app));
|
||||
_appPanel = new AppPanel();
|
||||
AppList appList = new(lib.V1.AppExtras, (_, app) => _appPanel.SetApp(app), online);
|
||||
_appPanel = new AppPanel(lib.V2.TaskFactory, lib.V1.AppExtras, _tasks);
|
||||
|
||||
Content = new StackLayout
|
||||
{
|
||||
Padding = 10,
|
||||
Items =
|
||||
{
|
||||
appList,
|
||||
new StackLayoutItem(_appPanel, true)
|
||||
new StackLayoutItem(appList, VerticalAlignment.Stretch),
|
||||
new StackLayoutItem(_appPanel, VerticalAlignment.Stretch, true)
|
||||
},
|
||||
Orientation = Orientation.Horizontal
|
||||
};
|
||||
|
@ -20,17 +20,17 @@ namespace UpToolEto
|
||||
public class Main
|
||||
{
|
||||
private readonly IExternalFunctionality _platform;
|
||||
private readonly UpToolLibMain _lib;
|
||||
private readonly Application _application;
|
||||
private readonly Action _activityExistsException;
|
||||
private readonly InitScreen _init;
|
||||
private readonly bool _skipFetch;
|
||||
public static bool DebugColors { get; private set; }
|
||||
|
||||
public Main(Application application, Action activityExistsException, string[] args)
|
||||
{
|
||||
_skipFetch = args.Contains("--skip-fetch");
|
||||
DebugColors = args.Contains("--debug-colors");
|
||||
_platform = new UTLibFunctions(application);
|
||||
_lib = new UpToolLibMain(_platform);
|
||||
_application = application;
|
||||
_activityExistsException = activityExistsException;
|
||||
_init = new(application, _platform);
|
||||
@ -44,40 +44,41 @@ namespace UpToolEto
|
||||
|
||||
private void InitThread()
|
||||
{
|
||||
UpToolLibMain lib = null;
|
||||
try
|
||||
{
|
||||
lib = new UpToolLibMain(_platform);
|
||||
_init.SetText("Initializing paths");
|
||||
if (!Directory.Exists(_lib.V1.PathTool.Dir))
|
||||
Directory.CreateDirectory(_lib.V1.PathTool.Dir);
|
||||
FixXml(_lib.V1.XmlTool, _lib.V1.PathTool);
|
||||
if (!Directory.Exists(lib.V1.PathTool.Dir))
|
||||
Directory.CreateDirectory(lib.V1.PathTool.Dir);
|
||||
FixXml(lib.V1.XmlTool, lib.V1.PathTool);
|
||||
_init.SetText("Performing checks");
|
||||
bool online = false;
|
||||
UpdateCheck updateCheck = null;
|
||||
try
|
||||
{
|
||||
updateCheck = _lib.V2.UpdateChecker.Check();
|
||||
updateCheck = lib.V2.UpdateChecker.Check();
|
||||
online = true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
_platform.Log("Could not perform update check, starting offline");
|
||||
}
|
||||
if (online && UpdateCheck(updateCheck, _lib.V1.PathTool, _init))
|
||||
{
|
||||
if (online && UpdateCheck(updateCheck, lib.V1.PathTool, _init))
|
||||
_platform.Log("Quitting");
|
||||
_application.Quit();
|
||||
return;
|
||||
}
|
||||
if (!Directory.Exists(_lib.V1.PathTool.GetRelative("Apps")))
|
||||
Directory.CreateDirectory(_lib.V1.PathTool.GetRelative("Apps"));
|
||||
if (!_skipFetch && online)
|
||||
else
|
||||
{
|
||||
_init.SetText("Fetching repos");
|
||||
_lib.V2.RepoManagement.FetchRepos();
|
||||
if (!Directory.Exists(lib.V1.PathTool.GetRelative("Apps")))
|
||||
Directory.CreateDirectory(lib.V1.PathTool.GetRelative("Apps"));
|
||||
if (!_skipFetch && online)
|
||||
{
|
||||
_init.SetText("Fetching repos");
|
||||
lib.V2.RepoManagement.FetchRepos();
|
||||
}
|
||||
lib.V2.RepoManagement.GetReposFromDisk();
|
||||
_init.SetText("Opening");
|
||||
_application.Invoke(() => _application.Run(new MainForm(_init, lib, _platform, online)));
|
||||
}
|
||||
_lib.V2.RepoManagement.GetReposFromDisk();
|
||||
_init.SetText("Opening");
|
||||
_application.Invoke(() => _application.Run(new MainForm(_init, _lib, _platform, online)));
|
||||
}
|
||||
catch (MutexLockLockedException)
|
||||
{
|
||||
@ -90,11 +91,13 @@ namespace UpToolEto
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
lib?.Dispose();
|
||||
_platform.Log(e.ToString());
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lib?.Dispose();
|
||||
lib?.Dispose();
|
||||
_application.Invoke(() => _application.Quit());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,11 @@ namespace UpToolLib.DataStructures
|
||||
Updatable = 2,
|
||||
Installed = 4,
|
||||
Local = 8,
|
||||
All = 15
|
||||
All = 0
|
||||
}
|
||||
|
||||
public static class StatusExtensions
|
||||
{
|
||||
public static bool Contains(this Status status, Status other) => (status & other) == other;
|
||||
}
|
||||
}
|
@ -108,6 +108,8 @@ namespace UpToolLib.v1.Tool
|
||||
|
||||
public App[] FindApps(string identifier)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(identifier))
|
||||
return _apps.Values.ToArray();
|
||||
IEnumerable<KeyValuePair<Guid, App>> tmp1 = _apps.Where(s => s.Key.ToString().StartsWith(identifier));
|
||||
tmp1 = tmp1.Concat(_apps.Where(s => s.Value.Name.Contains(identifier)));
|
||||
tmp1 = tmp1.Concat(_apps.Where(s => s.Value.Description.Contains(identifier)));
|
||||
|
Reference in New Issue
Block a user