Converted most of the application over to AntBlazor 🙃

This commit is contained in:
Pat Hartl 2023-03-02 18:50:24 -06:00
parent 73a9468c37
commit 99a638b64d
33 changed files with 1060 additions and 1176 deletions

View file

@ -1,6 +1,23 @@
<Router AppAssembly="@typeof(Program).Assembly">
@using Microsoft.AspNetCore.Components.Authorization
<AntContainer />
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
<NotAuthorized>
@if (context.User.Identity.IsAuthenticated == false)
{
<RedirectToLogin />
}
else
{
<audio autoplay>
<source src="~/static/access-denied.mp3" type="audio/mp3" />
</audio>
}
</NotAuthorized>
</AuthorizeRouteView>
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">

View file

@ -1,48 +1,70 @@
@using LANCommander.Data.Models
@using LANCommander.Extensions
@inject IDialogService DialogService
@using LANCommander.Models;
@using System.IO.Compression;
@inject ModalService ModalService
<MudTable Items="@OrderedActions" Elevation="0" Dense="true">
<HeaderContent>
<MudTh>Name</MudTh>
<MudTh>Path</MudTh>
<MudTh>Arguments</MudTh>
<MudTh>Working Dir</MudTh>
<MudTh>Primary</MudTh>
<MudTh></MudTh>
</HeaderContent>
<Space Direction="DirectionVHType.Vertical" Size="@("large")" Style="width: 100%">
<SpaceItem>
<Table TItem="Data.Models.Action" DataSource="@OrderedActions" HidePagination="true" Style="border: 1px solid #f0f0f0">
<PropertyColumn Property="a => a.Name">
<Input Type="text" @bind-Value="context.Name" />
</PropertyColumn>
<PropertyColumn Property="a => a.Path">
<Space Style="display: flex">
<SpaceItem Style="flex-grow: 1">
<Input Type="text" @bind-Value="context.Path" />
</SpaceItem>
<SpaceItem>
<Button OnClick="() => BrowseForActionPath(context)" Type="@ButtonType.Primary" Icon="@IconType.Outline.FolderOpen" />
</SpaceItem>
</Space>
</PropertyColumn>
<PropertyColumn Property="a => a.Arguments">
<Input Type="text" @bind-Value="context.Arguments" />
</PropertyColumn>
<PropertyColumn Property="a => a.WorkingDirectory" Title="Working Dir">
<Input Type="text" @bind-Value="context.WorkingDirectory" />
</PropertyColumn>
<PropertyColumn Property="a => a.PrimaryAction" Title="Primary" Style="text-align: center">
<Checkbox @bind-Checked="context.PrimaryAction" />
</PropertyColumn>
<ActionColumn>
<Space Style="display: flex; justify-content: end">
<SpaceItem>
<Button OnClick="() => MoveUp(context)" Icon="@IconType.Outline.Up" Type="@ButtonType.Text" />
<Button OnClick="() => MoveDown(context)" Icon="@IconType.Outline.Down" Type="@ButtonType.Text" />
<RowTemplate>
<MudTd><MudTextField @bind-Value="context.Name" /></MudTd>
<MudTd><MudTextField @bind-Value="context.Path" Adornment="Adornment.End" AdornmentIcon="@Icons.Material.Filled.Folder" OnAdornmentClick="() => BrowseForActionPath(context)" /></MudTd>
<MudTd><MudTextField @bind-Value="context.Arguments" /></MudTd>
<MudTd><MudTextField @bind-Value="context.WorkingDirectory" /></MudTd>
<MudTd><MudCheckBox @bind-Checked="context.PrimaryAction" Color="Color.Primary" /></MudTd>
<MudTd Class="d-flex flex-nowrap justify-end">
<MudIconButton OnClick="() => MoveUp(context)" Icon="@Icons.Material.Filled.ArrowUpward"></MudIconButton>
<MudIconButton OnClick="() => MoveDown(context)" Icon="@Icons.Material.Filled.ArrowDownward"></MudIconButton>
<MudIconButton OnClick="() => RemoveAction(context)" Color="Color.Error" Icon="@Icons.Material.Filled.Close"></MudIconButton>
</MudTd>
</RowTemplate>
</MudTable>
<Popconfirm OnConfirm="() => RemoveAction(context)" Title="Are you sure you want to remove this action?">
<Button Icon="@IconType.Outline.Close" Type="@ButtonType.Text" Danger />
</Popconfirm>
</SpaceItem>
</Space>
</ActionColumn>
</Table>
</SpaceItem>
<MudPaper Elevation="0" Class="d-flex justify-end mt-3">
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add" OnClick="AddAction">Add</MudButton>
</MudPaper>
<SpaceItem>
<GridRow Justify="end">
<GridCol>
<Button OnClick="AddAction" Type="@ButtonType.Primary">Add Action</Button>
</GridCol>
</GridRow>
</SpaceItem>
</Space>
@code {
[Parameter] public IEnumerable<Data.Models.Action> Actions { get; set; }
[Parameter] public Guid ArchiveId { get; set; }
[Parameter] public Game Game { get; set; }
private List<Data.Models.Action> OrderedActions { get; set; }
protected override async Task OnInitializedAsync()
{
OrderedActions = Actions.OrderBy(a => a.SortOrder).ToList();
OrderedActions = Game.Actions.OrderBy(a => a.SortOrder).ToList();
FixSortOrder();
}
private void AddAction()
private async Task AddAction()
{
if (OrderedActions == null)
OrderedActions = new List<Data.Models.Action>();
@ -54,12 +76,12 @@
});
}
private void RemoveAction(Data.Models.Action action)
private async Task RemoveAction(Data.Models.Action action)
{
OrderedActions.Remove(action);
}
private void MoveUp(Data.Models.Action action)
private async Task MoveUp(Data.Models.Action action)
{
if (action.SortOrder > 0)
OrderedActions.Move(action, action.SortOrder - 1);
@ -67,7 +89,7 @@
FixSortOrder();
}
private void MoveDown(Data.Models.Action action)
private async Task MoveDown(Data.Models.Action action)
{
if (action.SortOrder < OrderedActions.Count + 1)
OrderedActions.Move(action, action.SortOrder + 1);
@ -77,17 +99,27 @@
private async void BrowseForActionPath(Data.Models.Action action)
{
var parameters = new DialogParameters
{
["ArchiveId"] = ArchiveId
};
var modalOptions = new ModalOptions()
{
Title = "Choose Action Executable",
Maximizable = false,
DefaultMaximized = true,
Closable = true,
OkText = "Select File"
};
var dialog = await DialogService.ShowAsync<ArchiveFileSelectorDialog>("File Selector", parameters);
var result = await dialog.Result;
var browserOptions = new ArchiveBrowserOptions()
{
ArchiveId = Game.Archives.FirstOrDefault().Id,
Select = true,
Multiple = false
};
if (!result.Canceled)
var modalRef = await ModalService.CreateModalAsync<ArchiveBrowserDialog, ArchiveBrowserOptions, IEnumerable<ZipArchiveEntry>>(modalOptions, browserOptions);
modalRef.OnOk = (results) =>
{
action.Path = result.Data as string;
action.Path = results.FirstOrDefault().FullName;
var parts = action.Path.Split('/');
@ -98,7 +130,8 @@
}
StateHasChanged();
}
return Task.CompletedTask;
};
}
private void FixSortOrder()
@ -112,6 +145,6 @@
i++;
}
Actions = OrderedActions;
Game.Actions = OrderedActions;
}
}

View file

@ -1,38 +1,50 @@
@using ByteSizeLib;
@using AntDesign.TableModels;
@using ByteSizeLib;
@using LANCommander.Services;
@using System.IO.Compression;
@inject ArchiveService ArchiveService;
<MudStack Row="true" Style="max-height: 100%">
<MudTreeView Items="Directories" Hover="true" @bind-SelectedValue="SelectedDirectory" T="ArchiveDirectory" Style="min-width: 18%">
<ItemTemplate>
<MudTreeViewItem Expanded="@context.IsExpanded" Value="@context" Items="@context.Children" Text="@context.Name" T="ArchiveDirectory" OnClick="() => ChangeDirectory(context)"></MudTreeViewItem>
</ItemTemplate>
</MudTreeView>
<GridRow Style="position: fixed; height: calc(100vh - 55px - 53px); top: 55px; left: 0; width: 100%">
<GridCol Span="6" Style="height: 100%; overflow-y: scroll; padding: 24px">
<Tree TItem="ArchiveDirectory"
DataSource="Directories"
TitleExpression="x => x.DataItem.Name"
ChildrenExpression="x => x.DataItem.Children"
IsLeafExpression="x => !x.DataItem.HasChildren"
OnClick="(args) => ChangeDirectory(args.Node.DataItem)">
</Tree>
</GridCol>
<MudTable Items="@CurrentPathEntries" Hover="true" Class="flex-grow-1 archive-browser" FixedHeader="true" Elevation="0" Height="calc(100vh - 64px)">
<HeaderContent>
<MudTh></MudTh>
<MudTh>Name</MudTh>
<MudTh>Size</MudTh>
<MudTh>Modified</MudTh>
@if (OnFileSelected.HasDelegate)
<GridCol Span="18" Style="height: 100%">
<Table
@ref="FileTable"
TItem="ZipArchiveEntry"
DataSource="CurrentPathEntries"
HidePagination="true"
Loading="Entries == null"
RowSelectable="@(x => x.FullName != null && !x.FullName.EndsWith('/'))"
OnRowClick="OnRowClicked"
SelectedRowsChanged="SelectedFilesChanged"
ScrollY="calc(100vh - 55px - 55px - 53px)">
@if (Select)
{
<MudTh></MudTh>
<Selection Key="@context.FullName" Type="@(Multiple ? "checkbox" : "radio")" Disabled="@(context.FullName != null && context.FullName.EndsWith('/'))" />
}
</HeaderContent>
<RowTemplate>
<MudTd><MudIcon Icon="@GetIcon(context)" /></MudTd>
<MudTd>@GetFileName(context)</MudTd>
<MudTd>@ByteSize.FromBytes(context.Length)</MudTd>
<MudTd>@context.LastWriteTime</MudTd>
@if (OnFileSelected.HasDelegate)
{
<MudTd><MudButton Class="select-file-button" Color="Color.Primary" Variant="Variant.Filled" OnClick="() => OnFileSelected.InvokeAsync(context.FullName)">Select</MudButton></MudTd>
}
</RowTemplate>
</MudTable>
</MudStack>
<Column TData="string" Width="32">
<Icon Type="@GetIcon(context)" Theme="outline" />
</Column>
<PropertyColumn Property="e => e.FullName" Sortable Title="Name">
@GetFileName(context)
</PropertyColumn>
<PropertyColumn Property="e => e.Length" Sortable Title="Size">
@ByteSize.FromBytes(context.Length)
</PropertyColumn>
<PropertyColumn Property="e => e.LastWriteTime" Format="MM/dd/yyyy hh:mm" Sortable Title="Modified" />
</Table>
</GridCol>
</GridRow>
<style>
.select-file-button {
@ -47,9 +59,13 @@
@code {
[Parameter] public Guid ArchiveId { get; set; }
[Parameter] public Guid Archive { get; set; }
[Parameter] public EventCallback<string> OnFileSelected { get; set; }
[Parameter] public bool Select { get; set; }
[Parameter] public bool Multiple { get; set; }
[Parameter] public IEnumerable<ZipArchiveEntry> SelectedFiles { get; set; }
[Parameter] public EventCallback<IEnumerable<ZipArchiveEntry>> SelectedFilesChanged { get; set; }
ITable? FileTable;
private IEnumerable<ZipArchiveEntry> Entries { get; set; }
private IEnumerable<ZipArchiveEntry> CurrentPathEntries { get; set; }
@ -76,10 +92,14 @@
ChangeDirectory(root);
}
private void OnRowClicked(RowData<ZipArchiveEntry> row)
{
FileTable.SetSelection(new string[] { row.Data.FullName });
}
private void ChangeDirectory(ArchiveDirectory selectedDirectory)
{
if (SelectedDirectory == null)
SelectedDirectory = selectedDirectory;
SelectedDirectory = selectedDirectory;
if (SelectedDirectory.FullName == "")
CurrentPathEntries = Entries.Where(e => !e.FullName.TrimEnd('/').Contains('/'));
@ -102,23 +122,23 @@
switch (Path.GetExtension(entry.FullName))
{
case "":
return Icons.Material.Filled.Folder;
return "folder";
case ".exe":
return Icons.Material.Filled.Terminal;
return "code";
case ".zip":
case ".rar":
case ".7z":
case ".gz":
case ".tar":
return Icons.Material.Filled.FolderZip;
return "file-zip";
case ".wad":
case ".pk3":
case ".pak":
case ".cab":
return Icons.Material.Filled.Token;
return "file-zip";
case ".txt":
case ".cfg":
@ -129,12 +149,12 @@
case ".log":
case ".doc":
case ".nfo":
return Icons.Custom.FileFormats.FileDocument;
return "file-text";
case ".bat":
case ".ps1":
case ".json":
return Icons.Custom.FileFormats.FileCode;
return "code";
case ".bik":
case ".avi":
@ -146,26 +166,23 @@
case ".mpg":
case ".mpeg":
case ".flv":
return Icons.Custom.FileFormats.FileVideo;
return "video-camera";
case ".dll":
return Icons.Material.Filled.SettingsApplications;
case ".scm":
return Icons.Material.Filled.Map;
return "api";
case ".hlp":
return Icons.Material.Filled.Help;
return "file-unknown";
case ".png":
case ".bmp":
case ".jpeg":
case ".jpg":
case ".gif":
return Icons.Custom.FileFormats.FileImage;
return "file-image";
default:
return Icons.Material.Filled.InsertDriveFile;
return "file";
}
}

View file

@ -1,32 +1,14 @@
<MudDialog>
<TitleContent>
<MudText Typo="Typo.h6">
Browse Archive
</MudText>
</TitleContent>
@inherits FeedbackComponent<ArchiveBrowserOptions, IEnumerable<ZipArchiveEntry>>
@using System.IO.Compression;
@using LANCommander.Models;
<DialogContent>
<ArchiveBrowser ArchiveId="ArchiveId" />
</DialogContent>
</MudDialog>
<ArchiveBrowser ArchiveId="Options.ArchiveId" @bind-SelectedFiles="SelectedFiles" Select="Options.Select" Multiple="Options.Multiple" />
@code {
[CascadingParameter] MudDialogInstance MudDialog { get; set; }
[Parameter] public Guid ArchiveId { get; set; }
private IEnumerable<ZipArchiveEntry> SelectedFiles { get; set; }
protected override async Task OnInitializedAsync()
public override async Task OnFeedbackOkAsync(ModalClosingEventArgs args)
{
MudDialog.Options.MaxWidth = MaxWidth.Large;
MudDialog.Options.FullWidth = true;
MudDialog.Options.FullScreen = true;
MudDialog.Options.CloseButton = true;
MudDialog.Options.CloseOnEscapeKey = true;
MudDialog.SetOptions(MudDialog.Options);
await base.OkCancelRefWithResult!.OnOk(SelectedFiles);
}
private void Cancel()
{
MudDialog.Cancel();
}
}
}

View file

@ -1,39 +0,0 @@
<MudDialog>
<TitleContent>
<MudText Typo="Typo.h6">
Select a File
</MudText>
</TitleContent>
<DialogContent>
<ArchiveBrowser ArchiveId="ArchiveId" OnFileSelected="FileSelected" />
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel">Cancel</MudButton>
</DialogActions>
</MudDialog>
@code {
[CascadingParameter] MudDialogInstance MudDialog { get; set; }
[Parameter] public Guid ArchiveId { get; set; }
protected override async Task OnInitializedAsync()
{
MudDialog.Options.MaxWidth = MaxWidth.Large;
MudDialog.Options.FullWidth = true;
MudDialog.Options.FullScreen = true;
MudDialog.SetOptions(MudDialog.Options);
}
private void Cancel()
{
MudDialog.Cancel();
}
private void FileSelected(string fileName)
{
MudDialog.Close(DialogResult.Ok(fileName));
}
}

View file

@ -2,52 +2,88 @@
@using System.Diagnostics;
@inject HttpClient HttpClient
@inject NavigationManager Navigator
@inject ISnackbar Snackbar
@inject ArchiveService ArchiveService
@inject IMessageService MessageService
<MudDialog>
<DialogContent>
<MudForm @bind-IsValid="@IsValid">
<MudTextField T="string" @bind-Value="Archive.Version" Label="Version" Required="true" Disabled="Uploading" RequiredError="Version is required" />
<MudTextField T="string" @bind-Value="Archive.Changelog" Label="Changelog" Required="false" Disabled="Uploading" Lines="6" />
<Space Direction="DirectionVHType.Vertical" Style="width: 100%">
<SpaceItem>
<Table TItem="Archive" DataSource="@Game.Archives.OrderByDescending(a => a.CreatedOn)" HidePagination="true">
<PropertyColumn Property="a => a.Version" />
<PropertyColumn Property="a => a.CompressedSize">
@ByteSizeLib.ByteSize.FromBytes(context.CompressedSize)
</PropertyColumn>
<PropertyColumn Property="a => a.CreatedBy">
@context.CreatedBy?.UserName
</PropertyColumn>
<PropertyColumn Property="a => a.CreatedOn" Format="MM/dd/yyyy hh:mm" />
<ActionColumn Title="">
<Space Style="display: flex; justify-content: end">
<SpaceItem>
<Popconfirm Title="Are you sure you want to delete this archive?" OnConfirm="() => Delete(context)">
<Button Icon="@IconType.Outline.Close" Type="@ButtonType.Text" Danger />
</Popconfirm>
</SpaceItem>
</Space>
</ActionColumn>
</Table>
</SpaceItem>
<MudFileUpload T="IBrowserFile" OnFilesChanged="FileSelected" Class="flex-1"
InputClass="absolute mud-width-full mud-height-full overflow-hidden z-20 d-block" InputStyle="opacity: 0;"
@ondragenter="@SetDragClass" @ondragleave="@ClearDragClass" @ondragend="@ClearDragClass">
<ButtonTemplate>
<MudPaper Height="200px" Outlined="true" Class="@DragClass">
<MudText Typo="Typo.h6">Drop files here or click to browse</MudText>
<SpaceItem>
<GridRow Justify="end">
<GridCol>
<Button OnClick="AddArchive" Type="@ButtonType.Primary">Upload Archive</Button>
</GridCol>
</GridRow>
</SpaceItem>
</Space>
@if (File != null)
{
<MudChip Color="Color.Dark" Text="@File.Name" />
}
</MudPaper>
</ButtonTemplate>
</MudFileUpload>
@{
RenderFragment Footer =
@<Template>
<Button OnClick="UploadArchive" Disabled="@(File == null || Uploading)" Type="@ButtonType.Primary">Upload</Button>
<Button OnClick="Clear" Disabled="File == null || Uploading" Danger>Clear</Button>
<Button OnClick="Cancel">Cancel</Button>
</Template>;
}
<MudProgressLinear Color="Color.Primary" Striped="Uploading" Size="Size.Large" Value="Progress" Class="mt-4" />
<Modal Visible="@ModalVisible" Title="Upload Archive" OnOk="UploadArchive" OnCancel="Cancel" Footer="@Footer">
<Form Model="@Archive" Layout="@FormLayout.Vertical">
<FormItem Label="Version">
<Input @bind-Value="@context.Version" />
</FormItem>
<MudText>@ByteSizeLib.ByteSize.FromBytes(Speed)/s</MudText>
<FormItem Label="Changelog">
<TextArea @bind-Value="@context.Changelog" MaxLength=500 ShowCount />
</FormItem>
<MudToolBar DisableGutters="true" Class="gap-4">
<MudButton OnClick="UploadArchive" Disabled="@(!IsValid || File == null || Uploading)" Color="Color.Primary" Variant="Variant.Filled">Upload</MudButton>
<MudButton OnClick="Clear" Disabled="File == null || Uploading" Color="Color.Error" Variant="Variant.Filled">Clear</MudButton>
<MudButton OnClick="Cancel">Cancel</MudButton>
</MudToolBar>
</MudForm>
</DialogContent>
</MudDialog>
<FormItem>
<InputFile id="FileInput" OnChange="FileSelected" hidden />
<Upload Name="files" FileList="FileList">
<label class="ant-btn" for="FileInput">
<Icon Type="upload" />
Select Archive
</label>
</Upload>
</FormItem>
<FormItem>
<Progress Percent="Progress" />
<Text>@ByteSizeLib.ByteSize.FromBytes(Speed)/s</Text>
</FormItem>
</Form>
</Modal>
@code {
[CascadingParameter] MudDialogInstance MudDialog { get; set; }
[Parameter] public Guid GameId { get; set; }
[Parameter] public Game Game { get; set; }
Archive Archive;
IBrowserFile File { get; set; }
List<UploadFileItem> FileList = new List<UploadFileItem>();
bool IsValid = false;
bool ModalVisible = false;
private static string DefaultDragClass = "relative rounded-lg border-2 border-dashed pa-4 mt-4 mud-width-full mud-height-full z-10";
private string DragClass = DefaultDragClass;
@ -63,42 +99,49 @@
protected override async Task OnInitializedAsync()
{
MudDialog.Options.MaxWidth = MaxWidth.Large;
MudDialog.Options.FullWidth = true;
MudDialog.Options.CloseButton = false;
MudDialog.Options.CloseOnEscapeKey = false;
MudDialog.Options.DisableBackdropClick = true;
MudDialog.SetOptions(MudDialog.Options);
HttpClient.BaseAddress = new Uri(Navigator.BaseUri);
Archive = new Archive()
{
GameId = GameId,
GameId = Game.Id,
Id = Guid.NewGuid()
};
}
private void SetDragClass()
private void AddArchive()
{
DragClass = $"{DefaultDragClass} mud-border-primary";
Archive = new Archive()
{
GameId = Game.Id,
Id = Guid.NewGuid()
};
ModalVisible = true;
}
private void ClearDragClass()
private async Task Delete(Archive archive)
{
DragClass = DefaultDragClass;
try
{
await ArchiveService.Delete(archive);
await MessageService.Success("Archive deleted!");
}
catch
{
await MessageService.Error("Archive could not be deleted.");
}
}
private void Clear()
{
File = null;
ClearDragClass();
}
private void Cancel()
{
MudDialog.Cancel();
File = null;
ModalVisible = false;
}
private void FileSelected(InputFileChangeEventArgs args)
@ -159,7 +202,7 @@
{
Watch.Stop();
Uploading = false;
UploadComplete();
await UploadComplete();
}
await InvokeAsync(StateHasChanged);
@ -172,9 +215,10 @@
Archive.ObjectKey = Archive.Id.ToString();
Archive.CompressedSize = File.Size;
ArchiveService.Add(Archive);
await ArchiveService.Add(Archive);
MudDialog.Close();
Snackbar.Add("Archive uploaded!", Severity.Success);
ModalVisible = false;
await MessageService.Success("Archive uploaded!");
}
}

View file

@ -1,73 +1,66 @@
@using LANCommander.Data.Enums
@using AntDesign.TableModels;
@using LANCommander.Data.Enums
@using LANCommander.Models
@using LANCommander.PCGamingWiki
@inject IGDBService IGDBService
@inject CompanyService CompanyService
@inject GenreService GenreService
@inject TagService TagService
@inject ISnackbar Snackbar
<MudDialog>
<TitleContent>
<MudText Typo="Typo.h6">
<MudIcon Icon="@Icons.Material.Filled.DeleteForever" Class="mr-3 mb-n1" />
Results for @GameTitle
</MudText>
</TitleContent>
<DialogContent>
@if (Results == null)
{
<MudProgressCircular Color="Color.Primary" Indeterminate="true" />
}
else
{
<MudTable Items="@Results" Hover="true">
<HeaderContent>
<MudTh>Title</MudTh>
<MudTh>Released</MudTh>
<MudTh>Developers</MudTh>
<MudTh></MudTh>
</HeaderContent>
@{
RenderFragment Footer =
@<Template>
<Button OnClick="SelectGame" Disabled="@(Results == null || Results.Count() == 0)" Type="@ButtonType.Primary">Select</Button>
<Button OnClick="() => ModalVisible= false">Cancel</Button>
</Template>;
}
<RowTemplate>
<MudTh>@context.Title</MudTh>
<MudTh>@context.ReleasedOn?.ToString("MM/dd/yyyy")</MudTh>
<MudTh>@String.Join(", ", context.Developers?.Select(d => d.Name))</MudTh>
<MudTh>
<MudButton OnClick="() => SelectGame(context)">Select</MudButton>
</MudTh>
</RowTemplate>
</MudTable>
}
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel">Cancel</MudButton>
</DialogActions>
</MudDialog>
<Modal Visible="ModalVisible" Title="Game Metadata Lookup" Footer="@Footer">
<Table
@ref="ResultsTable"
TItem="Game"
DataSource="Results"
HidePagination="true"
Loading="Results == null"
OnRowClick="OnRowClicked"
@bind-SelectedRows="SelectedResults"
ScrollY="calc(100vh - 55px - 55px - 53px)">
<Selection Key="@context.IGDBId.ToString()" Type="radio" />
<PropertyColumn Property="g => g.Title" Title="Title" />
<PropertyColumn Property="g => g.ReleasedOn" Format="MM/dd/yyyy" Title="Released" />
<PropertyColumn Property="g => g.Developers">
@String.Join(", ", context.Developers?.Select(d => d.Name))
</PropertyColumn>
</Table>
</Modal>
@code {
[CascadingParameter] MudDialogInstance MudDialog { get; set; }
[Parameter] public EventCallback<GameLookupResult> OnResultSelected { get; set; }
[Parameter] public string GameTitle { get; set; }
ITable? ResultsTable;
private IEnumerable<Game> Results { get; set; }
private PCGamingWikiClient PCGamingWikiClient { get; set; }
IEnumerable<Game> Results { get; set; }
IEnumerable<Game> SelectedResults { get; set; }
PCGamingWikiClient PCGamingWikiClient { get; set; }
bool ModalVisible { get; set; } = false;
protected override async Task OnInitializedAsync()
{
PCGamingWikiClient = new PCGamingWikiClient();
await SearchForGame(GameTitle);
}
private void Cancel()
private void OnRowClicked(RowData<Game> row)
{
MudDialog.Cancel();
ResultsTable.SetSelection(new string[] { row.Data.IGDBId.ToString() });
}
public async Task SearchForGame(string title)
{
var results = await IGDBService.Search(GameTitle, "involved_companies.*", "involved_companies.company.*");
ModalVisible = true;
Results = null;
var results = await IGDBService.Search(title, "involved_companies.*", "involved_companies.company.*");
if (results == null)
Results = new List<Game>();
@ -96,62 +89,16 @@
}
}
private async Task SelectGame(Game game)
private async Task SelectGame()
{
Results = null;
var result = new GameLookupResult();
var result = await IGDBService.Get(game.IGDBId.GetValueOrDefault(), "genres.*", "game_modes.*", "multiplayer_modes.*", "release_dates.*", "platforms.*", "keywords.*", "involved_companies.*", "involved_companies.company.*", "cover.*");
result.IGDBMetadata = await IGDBService.Get(SelectedResults.First().IGDBId.GetValueOrDefault(), "genres.*", "game_modes.*", "multiplayer_modes.*", "release_dates.*", "platforms.*", "keywords.*", "involved_companies.*", "involved_companies.company.*", "cover.*");
result.MultiplayerModes = await GetMultiplayerModes(result.IGDBMetadata.Name);
game.Title = result.Name;
game.Description = result.Summary;
game.ReleasedOn = result.FirstReleaseDate.GetValueOrDefault().UtcDateTime;
game.MultiplayerModes = await GetMultiplayerModes(result.Name);
game.Developers = new List<Company>();
game.Publishers = new List<Company>();
game.Genres = new List<Genre>();
game.Tags = new List<Tag>();
await OnResultSelected.InvokeAsync(result);
if (result.GameModes != null && result.GameModes.Values != null)
game.Singleplayer = result.GameModes.Values.Any(gm => gm.Name == "Singleplayer");
if (result.InvolvedCompanies != null && result.InvolvedCompanies.Values != null)
{
// Make sure companie
var developers = result.InvolvedCompanies.Values.Where(c => c.Developer.GetValueOrDefault()).Select(c => c.Company.Value.Name);
var publishers = result.InvolvedCompanies.Values.Where(c => c.Publisher.GetValueOrDefault()).Select(c => c.Company.Value.Name);
foreach (var developer in developers)
{
game.Developers.Add(await CompanyService.AddMissing(c => c.Name == developer, new Company { Name = developer }));
}
foreach (var publisher in publishers)
{
game.Publishers.Add(await CompanyService.AddMissing(c => c.Name == publisher, new Company { Name = publisher }));
}
}
if (result.Genres != null && result.Genres.Values != null)
{
var genres = result.Genres.Values.Select(g => g.Name);
foreach (var genre in genres)
{
game.Genres.Add(await GenreService.AddMissing(g => g.Name == genre, new Genre { Name = genre }));
}
}
if (result.Keywords != null && result.Keywords.Values != null)
{
var tags = result.Keywords.Values.Select(t => t.Name).Take(20);
foreach (var tag in tags)
{
game.Tags.Add(await TagService.AddMissing(t => t.Name == tag, new Tag { Name = tag }));
}
}
MudDialog.Close(DialogResult.Ok(game));
ModalVisible = false;
}
private async Task<ICollection<MultiplayerMode>> GetMultiplayerModes(string gameTitle)

View file

@ -0,0 +1,123 @@
@inject KeyService KeyService
@inject IMessageService MessageService
<Row>
<Col Span="8">
<Statistic Title="Available" Value="Game.Keys.Count - AllocatedKeys" Style="text-align: center;" />
</Col>
<Col Span="8">
<Statistic Title="Allocated" Value="AllocatedKeys" Style="text-align: center;" />
</Col>
<Col Span="8">
<Statistic Title="Total" Value="Game.Keys.Count" Style="text-align: center;" />
</Col>
</Row>
<Modal Title="View Keys" Visible="ViewModalVisible" Maximizable="false" DefaultMaximized="true" OnCancel="() => ViewModalVisible = false" OnOk="() => ViewModalVisible = false">
<Table TItem="Key" DataSource="@Game.Keys" Bordered>
<PropertyColumn Property="k => k.Value">
<InputPassword @bind-Value="@context.Value" />
</PropertyColumn>
<PropertyColumn Property="k => k.AllocationMethod" />
<Column TData="string">
@switch (context.AllocationMethod)
{
case KeyAllocationMethod.MacAddress:
<text>@context.ClaimedByMacAddress</text>
break;
case KeyAllocationMethod.UserAccount:
<text>@context.ClaimedByUser?.UserName</text>
break;
}
</Column>
<PropertyColumn Property="g => g.ClaimedOn" Format="MM/dd/yyyy hh:mm" Sortable />
<ActionColumn Title="">
<Space>
<SpaceItem>
@if (context.IsAllocated())
{
<Button OnClick="() => Release(context)">Release</Button>
}
</SpaceItem>
</Space>
</ActionColumn>
</Table>
</Modal>
<Modal Title="Edit Keys" Visible="EditModalVisible" Maximizable="false" DefaultMaximized="true" OnCancel="() => EditModalVisible = false" OnOk="Save">
<StandaloneCodeEditor @ref="Editor" Id="editor" ConstructionOptions="EditorConstructionOptions" />
</Modal>
<style>
.monaco-editor-container {
height: 600px;
}
</style>
@code {
[Parameter] public Game Game { get; set; }
int AllocatedKeys;
bool ViewModalVisible = false;
bool EditModalVisible = false;
private StandaloneCodeEditor? Editor;
private StandaloneEditorConstructionOptions EditorConstructionOptions(StandaloneCodeEditor editor)
{
return new StandaloneEditorConstructionOptions
{
AutomaticLayout = true,
Language = "text",
Value = String.Join('\n', Game.Keys.Select(k => k.Value)),
Theme = "vs-dark",
};
}
protected override async Task OnInitializedAsync()
{
AllocatedKeys = Game.Keys.Count(k => k.IsAllocated());
}
public void Edit()
{
EditModalVisible = true;
}
public void View()
{
ViewModalVisible = true;
}
private async Task Release(Key key)
{
key = await KeyService.Release(key);
await MessageService.Success("Key was unallocated!");
}
private async Task Save()
{
var value = await Editor.GetValue();
var keys = value.Split("\n").Select(k => k.Trim()).Where(k => !String.IsNullOrWhiteSpace(k));
var keysDeleted = Game.Keys.Where(k => !keys.Contains(k.Value));
var keysAdded = keys.Where(k => !Game.Keys.Any(gk => gk.Value == k));
foreach (var key in keysDeleted)
KeyService.Delete(key);
foreach (var key in keysAdded)
await KeyService.Add(new Key()
{
Game = Game,
Value = key
});
EditModalVisible = false;
await MessageService.Success("Keys updated!");
}
}

View file

@ -1,36 +1,39 @@
@using LANCommander.Data.Enums
@using LANCommander.Data.Models
<MudTable Items="@Game.MultiplayerModes" Elevation="0" Dense="true">
<HeaderContent>
<MudTh>Type</MudTh>
<MudTh>Min Players</MudTh>
<MudTh>Max Players</MudTh>
<MudTh>Description</MudTh>
<MudTh></MudTh>
</HeaderContent>
<Space Direction="DirectionVHType.Vertical" Size="@("large")" Style="width: 100%">
<SpaceItem>
<Table TItem="MultiplayerMode" DataSource="@Game.MultiplayerModes" HidePagination="true">
<PropertyColumn Property="m => m.Type">
<Select @bind-Value="context.Type" TItem="MultiplayerType" TItemValue="MultiplayerType" DataSource="Enum.GetValues<MultiplayerType>()" />
</PropertyColumn>
<PropertyColumn Property="m => m.MinPlayers">
<AntDesign.InputNumber @bind-Value="context.MinPlayers" DefaultValue="2" Min="2" />
</PropertyColumn>
<PropertyColumn Property="m => m.MaxPlayers">
<AntDesign.InputNumber @bind-Value="context.MaxPlayers" DefaultValue="2" Min="2" />
</PropertyColumn>
<PropertyColumn Property="m => m.Description">
<Input Type="text" @bind-Value="context.Description" />
</PropertyColumn>
<ActionColumn>
<Space Style="display: flex; justify-content: end">
<SpaceItem>
<Button OnClick="() => RemoveMode(context)" Type="@ButtonType.Text" Danger Icon="@IconType.Outline.Close" />
</SpaceItem>
</Space>
</ActionColumn>
</Table>
</SpaceItem>
<RowTemplate>
<MudTd>
<MudSelect @bind-Value="context.Type" Margin="0">
@foreach (MultiplayerType type in Enum.GetValues(typeof(MultiplayerType)))
{
<MudSelectItem Value="@((MultiplayerType)type)">@type</MudSelectItem>
}
</MudSelect>
</MudTd>
<MudTd><MudNumericField @bind-Value="context.MinPlayers" Margin="0" /></MudTd>
<MudTd><MudNumericField @bind-Value="context.MaxPlayers" Margin="0" /></MudTd>
<MudTd><MudTextField @bind-Value="context.Description" Margin="0" /></MudTd>
<MudTd Class="d-flex justify-end">
<MudIconButton Color="Color.Error" OnClick="() => RemoveMode(context)" Icon="@Icons.Material.Filled.Close"></MudIconButton>
</MudTd>
</RowTemplate>
</MudTable>
<MudPaper Elevation="0" Class="d-flex justify-end mt-3">
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add" OnClick="AddMode">Add</MudButton>
</MudPaper>
<SpaceItem>
<GridRow Justify="end">
<GridCol>
<Button OnClick="AddMode" Type="@ButtonType.Primary">Add Mode</Button>
</GridCol>
</GridRow>
</SpaceItem>
</Space>
@code {
[Parameter] public Game Game { get; set; }

View file

@ -1,4 +0,0 @@
<MudNavMenu>
<MudNavLink Href="/" Match="NavLinkMatch.All">Dashboard</MudNavLink>
<MudNavLink Href="/Games" Match="NavLinkMatch.Prefix">Games</MudNavLink>
</MudNavMenu>

View file

@ -0,0 +1,8 @@
@inject NavigationManager NavigationManager
@code {
protected override void OnInitialized()
{
NavigationManager.NavigateTo("/Identity/Account/Login");
}
}

View file

@ -0,0 +1,190 @@
@using LANCommander.Data.Enums;
@using LANCommander.Models
@using LANCommander.Services
@using System.IO.Compression;
@inject ScriptService ScriptService
@inject ModalService ModalService
@inject IMessageService MessageService
<Modal Visible="ModalVisible" OnOk="Save" OnCancel="() => ModalVisible = false" Title="@(Script == null ? "Add Script" : "Edit Script")" OkText="@("Save")" Maximizable="false" DefaultMaximized="true">
<Form Model="@Script" Layout="@FormLayout.Vertical">
<FormItem>
@foreach (var group in Snippets.Select(s => s.Group).Distinct())
{
<Dropdown>
<Overlay>
<Menu>
@foreach (var snippet in Snippets.Where(s => s.Group == group))
{
<MenuItem OnClick="() => InsertSnippet(snippet)">
@snippet.Name
</MenuItem>
}
</Menu>
</Overlay>
<ChildContent>
<Button Type="@ButtonType.Primary">@group</Button>
</ChildContent>
</Dropdown>
}
<Button Icon="@IconType.Outline.FolderOpen" OnClick="BrowseForPath" Type="@ButtonType.Text">Browse</Button>
</FormItem>
<FormItem>
<StandaloneCodeEditor @ref="Editor" Id="editor" ConstructionOptions="EditorConstructionOptions" />
</FormItem>
<FormItem Label="Type">
<Select @bind-Value="Script.Type" TItem="ScriptType" TItemValue="ScriptType" DataSource="Enum.GetValues<ScriptType>()" />
</FormItem>
<FormItem>
<Checkbox @bind-Checked="Script.RequiresAdmin">Requires Admin</Checkbox>
</FormItem>
<FormItem Label="Description">
<TextArea @bind-Value="Script.Description" MaxLength=500 ShowCount />
</FormItem>
</Form>
</Modal>
<Space Direction="DirectionVHType.Vertical" Size="@("large")" Style="width: 100%">
<SpaceItem>
<Table TItem="Script" DataSource="@Game.Scripts" HidePagination="true">
<PropertyColumn Property="s => s.Type" />
<PropertyColumn Property="s => s.CreatedBy">
@context.CreatedBy?.UserName
</PropertyColumn>
<PropertyColumn Property="s => s.CreatedOn" Format="MM/dd/yyyy hh:mm" />
<ActionColumn Title="">
<Space Style="display: flex; justify-content: end">
<SpaceItem>
<Button OnClick="() => Edit(context)" Icon="@IconType.Outline.Edit" Type="@ButtonType.Text" />
<Popconfirm OnConfirm="() => Delete(context)" Title="Are you sure you want to delete this script?">
<Button Icon="@IconType.Outline.Close" Type="@ButtonType.Text" Danger />
</Popconfirm>
</SpaceItem>
</Space>
</ActionColumn>
</Table>
</SpaceItem>
<SpaceItem>
<GridRow Justify="end">
<GridCol>
<Button OnClick="() => Edit()" Type="@ButtonType.Primary">Add Script</Button>
</GridCol>
</GridRow>
</SpaceItem>
</Space>
<style>
.monaco-editor-container {
height: 600px;
}
</style>
@code {
[Parameter] public Game Game { get; set; }
Script Script;
bool ModalVisible = false;
IEnumerable<Snippet> Snippets { get; set; }
StandaloneCodeEditor Editor;
private StandaloneEditorConstructionOptions EditorConstructionOptions(StandaloneCodeEditor editor)
{
return new StandaloneEditorConstructionOptions
{
AutomaticLayout = true,
Language = "powershell",
Value = Script.Contents,
Theme = "vs-dark",
};
}
protected override async Task OnInitializedAsync()
{
Snippets = ScriptService.GetSnippets();
if (Script == null)
Script = new Script();
}
private async void Edit(Script script = null)
{
if (Script == null)
Script = new Script();
else
Script = script;
if (Editor != null)
await Editor.SetValue(Script.Contents);
ModalVisible = true;
}
private async void Delete(Script script = null)
{
if (script != null)
await ScriptService.Delete(script);
await MessageService.Success("Script deleted!");
}
private async Task Save()
{
var value = await Editor.GetValue();
await ScriptService.Update(Script);
await MessageService.Success("Script saved!");
}
private async void InsertSnippet(Snippet snippet)
{
await Editor.Trigger("keyboard", "type", new
{
text = snippet.Content
});
}
private async void BrowseForPath()
{
var modalOptions = new ModalOptions()
{
Title = "Choose Reference",
Maximizable = false,
DefaultMaximized = true,
Closable = true,
OkText = "Insert File Path"
};
var browserOptions = new ArchiveBrowserOptions()
{
ArchiveId = Game.Archives.FirstOrDefault().Id,
Select = true,
Multiple = false
};
var modalRef = await ModalService.CreateModalAsync<ArchiveBrowserDialog, ArchiveBrowserOptions, IEnumerable<ZipArchiveEntry>>(modalOptions, browserOptions);
modalRef.OnOk = (results) =>
{
var path = results.FirstOrDefault().FullName;
Editor.Trigger("keyboard", "type", new
{
text = $"$InstallDir\\{path.Replace('/', '\\')}"
});
StateHasChanged();
return Task.CompletedTask;
};
}
}

View file

@ -1,125 +0,0 @@
@using LANCommander.Data.Enums;
@using LANCommander.Models
@using LANCommander.Services
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@inject ScriptService ScriptService
<MudDialog>
<DialogContent>
<MudPaper Elevation="0" Class="gap-4 d-flex flex-nowrap justify-content-start mb-2">
@foreach (var group in Snippets.Select(s => s.Group).Distinct())
{
<MudMenu EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="@group" Color="Color.Primary" Variant="Variant.Filled">
@foreach (var snippet in Snippets.Where(s => s.Group == group))
{
<MudMenuItem OnClick="() => InsertSnippet(snippet)">@snippet.Name</MudMenuItem>
}
</MudMenu>
}
<MudTooltip Text="Browse Archive For Path">
<MudIconButton Icon="@Icons.Material.Filled.Folder" OnClick="BrowseForPath" Class="align-self-end" />
</MudTooltip>
</MudPaper>
<StandaloneCodeEditor @ref="Editor" Id="editor" ConstructionOptions="EditorConstructionOptions" />
<MudSelect @bind-Value="Script.Type" Label="Type">
@foreach (ScriptType type in Enum.GetValues(typeof(ScriptType)))
{
<MudSelectItem Value="@((ScriptType)type)">@type</MudSelectItem>
}
</MudSelect>
<MudCheckBox @bind-Checked="Script.RequiresAdmin" Color="Color.Primary" Label="Requires Admin"></MudCheckBox>
<MudTextField @bind-Value="Script.Description" Lines="4" Label="Description" />
</DialogContent>
<DialogActions>
<MudButton Color="Color.Primary" Variant="Variant.Filled" StartIcon="@Icons.Material.Filled.Save" OnClick="Save">Save</MudButton>
</DialogActions>
</MudDialog>
<style>
.monaco-editor-container {
height: 600px;
}
</style>
@code {
[CascadingParameter] MudDialogInstance MudDialog { get; set; }
[Parameter] public Script Script { get; set; }
IEnumerable<Snippet> Snippets { get; set; }
StandaloneCodeEditor Editor;
private StandaloneEditorConstructionOptions EditorConstructionOptions(StandaloneCodeEditor editor)
{
return new StandaloneEditorConstructionOptions
{
AutomaticLayout = true,
Language = "powershell",
Value = Script.Contents,
Theme = "vs-dark",
};
}
protected override async Task OnInitializedAsync()
{
MudDialog.Options.MaxWidth = MaxWidth.ExtraLarge;
MudDialog.Options.FullWidth = true;
MudDialog.Options.CloseButton = true;
MudDialog.Options.CloseOnEscapeKey = false;
MudDialog.Options.DisableBackdropClick = true;
MudDialog.SetOptions(MudDialog.Options);
Snippets = ScriptService.GetSnippets();
if (Script == null)
Script = new Script();
}
private async Task Save()
{
var value = await Editor.GetValue();
await ScriptService.Update(Script);
Snackbar.Add("Script saved!", Severity.Success);
MudDialog.Close();
}
private async void InsertSnippet(Snippet snippet)
{
Editor.Trigger("keyboard", "type", new
{
text = snippet.Content
});
}
private async void BrowseForPath()
{
var parameters = new DialogParameters
{
["ArchiveId"] = Script.Game.Archives.OrderByDescending(a => a.CreatedOn).First().Id
};
var dialog = await DialogService.ShowAsync<ArchiveFileSelectorDialog>("File Selector", parameters);
var result = await dialog.Result;
if (!result.Canceled)
{
var path = result.Data as string;
Editor.Trigger("keyboard", "type", new
{
text = $"$InstallDir\\{path.Replace('/', '\\')}"
});
StateHasChanged();
}
}
}

View file

@ -1,32 +0,0 @@
@using LANCommander.Models;
@using LANCommander.Services;
@inject IJSRuntime JS
@foreach (var group in Snippets.Select(s => s.Group).Distinct())
{
<div class="dropdown">
<button class="btn btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
@group
</button>
<ul class="dropdown-menu">
@foreach (var snippet in Snippets.Where(s => s.Group == group))
{
<li><a class="dropdown-item" @onclick="() => InsertSnippet(snippet)">@snippet.Name</a></li>
}
</ul>
</div>
}
@code {
public IEnumerable<Snippet> Snippets { get; set; }
protected override void OnInitialized()
{
Snippets = ScriptService.GetSnippets();
}
private async Task InsertSnippet(Snippet snippet) {
await JS.InvokeVoidAsync("Editor.trigger", "keyboard", "type", new { text = snippet.Content });
}
}

View file

@ -0,0 +1,40 @@
@typeparam TItem where TItem : BaseModel
<Select Mode="tags" TItem="Guid" TItemValue="Guid" @bind-Values="@SelectedValues" OnSelectedItemsChanged="OnSelectedItemsChanged" EnableSearch>
<SelectOptions>
@foreach (var entity in Entities)
{
<SelectOption TItemValue="Guid" TItem="Guid" Value="@entity.Id" Label="@OptionLabelSelector.Invoke(entity)" />
}
</SelectOptions>
</Select>
@code {
[Parameter] public Func<TItem, string> OptionLabelSelector { get; set; }
[Parameter] public IEnumerable<TItem> Entities { get; set; }
[Parameter] public ICollection<TItem> SelectedEntities { get; set; }
private IEnumerable<Guid> SelectedValues;
protected override void OnInitialized()
{
if (SelectedEntities != null)
SelectedValues = SelectedEntities.Select(e => e.Id);
}
private void OnSelectedItemsChanged(IEnumerable<Guid> values)
{
var toAdd = values.Where(v => !SelectedEntities.Any(e => e.Id == v));
var toRemove = SelectedEntities.Where(e => !values.Any(v => v == e.Id));
foreach (var value in toAdd)
{
SelectedEntities.Add(Entities.First(e => e.Id == value));
}
foreach (var value in toRemove)
{
SelectedEntities.Remove(value);
}
}
}

View file

@ -20,6 +20,17 @@ namespace LANCommander.Data.Models
public string? ClaimedByComputerName { get; set; }
public virtual User? ClaimedByUser { get; set; }
public DateTime? ClaimedOn { get; set; }
public bool IsAllocated()
{
if (AllocationMethod == KeyAllocationMethod.MacAddress && !String.IsNullOrWhiteSpace(ClaimedByMacAddress))
return true;
if (AllocationMethod == KeyAllocationMethod.UserAccount && ClaimedByUser != null)
return true;
return false;
}
}
public enum KeyAllocationMethod

View file

@ -20,6 +20,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AntDesign" Version="0.14.3" />
<PackageReference Include="Blazor-ApexCharts" Version="0.9.18-beta" />
<PackageReference Include="BlazorMonaco" Version="3.0.0" />
<PackageReference Include="ByteSize" Version="2.1.1" />
<PackageReference Include="IGDB" Version="2.3.1" />
@ -48,10 +50,11 @@
</ItemGroup>
<ItemGroup>
<Folder Include="Areas\Archive\Pages\" />
<Folder Include="bin\Debug\net6.0\" />
<Folder Include="Data\Migrations\" />
<Folder Include="Migrations\" />
<Folder Include="Pages\Games\Archives\" />
<Folder Include="Pages\Settings\" />
</ItemGroup>
<ItemGroup>

View file

@ -0,0 +1,9 @@
namespace LANCommander.Models
{
public class ArchiveBrowserOptions
{
public Guid ArchiveId { get; set; }
public bool Select { get; set; }
public bool Multiple { get; set; }
}
}

View file

@ -0,0 +1,10 @@
using LANCommander.Data.Models;
namespace LANCommander.Models
{
public class GameLookupResult
{
public IGDB.Models.Game IGDBMetadata { get; set; }
public IEnumerable<MultiplayerMode> MultiplayerModes { get; set; }
}
}

View file

@ -1,146 +0,0 @@
@page "/Dashboard"
@using System.Diagnostics;
@using LANCommander.Models;
<MudGrid Justify="Justify.Center">
<MudItem>
<MudCard>
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">CPU Utilization (%)</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
@if (PerformanceChartData.ProcessorUtilization != null && PerformanceChartData.ProcessorUtilization.PerformanceCounter != null)
{
<MudChart ChartType="ChartType.Line" ChartSeries="@(PerformanceChartData.ProcessorUtilization.ToSeriesList("CPU %"))" Width="100%" Height="250px"></MudChart>
}
</MudCardContent>
</MudCard>
</MudItem>
<MudItem>
<MudCard>
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">Upload Rate (MB/s)</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudChart ChartType="ChartType.Line" ChartSeries="@PerformanceChartData.NetworkUploadRate.Select(x => x.Value.ToSeries(x.Key)).ToList()" Width="100%" Height="250px"></MudChart>
</MudCardContent>
</MudCard>
</MudItem>
<MudItem>
<MudCard>
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">Download Rate (MB/s)</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudChart ChartType="ChartType.Line" ChartSeries="@PerformanceChartData.NetworkDownloadRate.Select(x => x.Value.ToSeries(x.Key)).ToList()" Width="100%" Height="250px"></MudChart>
</MudCardContent>
</MudCard>
</MudItem>
</MudGrid>
@code {
int cpuTime = 0;
int RecordTime = 60;
private PerformanceChartData PerformanceChartData = new PerformanceChartData()
{
ProcessorUtilization = new PerformanceCounterData(),
NetworkUploadRate = new Dictionary<string, PerformanceCounterData>(),
NetworkDownloadRate = new Dictionary<string, PerformanceCounterData>()
};
protected override async Task OnInitializedAsync()
{
var timer = new System.Timers.Timer();
timer.Interval = 1000;
timer.Elapsed += async (s, e) =>
{
RefreshLiveData();
await InvokeAsync(StateHasChanged);
};
timer.Start();
}
private void RefreshLiveData()
{
RefreshProcessorUtilization();
RefreshNetworkUsage();
}
private void RefreshProcessorUtilization()
{
if (PerformanceChartData.ProcessorUtilization.PerformanceCounter == null)
PerformanceChartData.ProcessorUtilization.PerformanceCounter = new PerformanceCounter("Processor", "% Processor Time", "_Total");
PerformanceChartData.ProcessorUtilization.Data = ShiftArrayAndInsert<double>(PerformanceChartData.ProcessorUtilization.Data, PerformanceChartData.ProcessorUtilization.PerformanceCounter.NextValue());
}
private void RefreshNetworkUsage()
{
var category = new PerformanceCounterCategory("Network Interface");
foreach (var instance in category.GetInstanceNames())
{
if (!PerformanceChartData.NetworkUploadRate.ContainsKey(instance))
PerformanceChartData.NetworkUploadRate[instance] = new PerformanceCounterData()
{
PerformanceCounter = new PerformanceCounter("Network Interface", "Bytes Sent/sec", instance),
Data = new double[RecordTime]
};
if (!PerformanceChartData.NetworkDownloadRate.ContainsKey(instance))
PerformanceChartData.NetworkDownloadRate[instance] = new PerformanceCounterData()
{
PerformanceCounter = new PerformanceCounter("Network Interface", "Bytes Received/sec", instance),
Data = new double[RecordTime]
};
PerformanceChartData.NetworkUploadRate[instance].Data = ShiftArrayAndInsert<double>(PerformanceChartData.NetworkUploadRate[instance].Data, (double)PerformanceChartData.NetworkUploadRate[instance].PerformanceCounter.NextValue() / (1024 * 1024));
PerformanceChartData.NetworkDownloadRate[instance].Data = ShiftArrayAndInsert<double>(PerformanceChartData.NetworkDownloadRate[instance].Data, (double)PerformanceChartData.NetworkDownloadRate[instance].PerformanceCounter.NextValue() / (1024 * 1024));
}
}
private T[] ShiftArrayAndInsert<T>(T[] array, T input)
{
if (array == null || array.Length < RecordTime)
{
array = new T[RecordTime];
}
Array.Copy(array, 1, array, 0, array.Length - 1);
array[array.Length - 1] = input;
return array;
}
private PerformanceCounter[] GetCounters(string categoryName)
{
try
{
var category = PerformanceCounterCategory.GetCategories().First(c => c.CategoryName == categoryName);
var instanceName = Process.GetCurrentProcess().ProcessName;
return category.GetCounters(instanceName);
}
catch
{
return new PerformanceCounter[] { };
}
}
}

View file

@ -1,211 +1,138 @@
@page "/Games/{id:guid}/Edit"
@using LANCommander.Models;
@using System.IO.Compression;
@attribute [Authorize(Roles = "Administrator")]
@inject GameService GameService
@inject CompanyService CompanyService
@inject GenreService GenreService
@inject TagService TagService
@inject ArchiveService ArchiveService
@inject ScriptService ScriptService
@inject IDialogService DialogService
@inject ISnackbar Snackbar
@inject IMessageService MessageService
@inject ModalService ModalService
<MudGrid>
<MudItem xs="12">
<MudPaper Class="pa-4">
<MudForm @bind-IsValid="@Success" @bind-Errors="@Errors">
<MudTextField @bind-Value="Game.Title" Label="Title" For="@(() => Game.Title)" Adornment="Adornment.End" AdornmentIcon="@Icons.Material.Filled.Search" OnAdornmentClick="LookupGameMetadata" />
<MudTextField @bind-Value="Game.SortTitle" Label="Sort Title" For="@(() => Game.SortTitle)" />
<MudTextField @bind-Value="Game.Icon" Label="Icon" Adornment="Adornment.End" AdornmentIcon="@Icons.Material.Filled.Folder" OnAdornmentClick="BrowseForIcon" />
<MudTextField @bind-Value="Game.Description" Label="Description" For="@(() => Game.Description)" Lines="4" />
<MudDatePicker @bind-Date="Game.ReleasedOn" Label="Released" />
<MudCheckBox @bind-Checked="@Game.Singleplayer" Label="Singleplayer" Color="Color.Primary"></MudCheckBox>
<Space Direction="DirectionVHType.Vertical" Size="@("large")" Style="width: 100%;">
<SpaceItem>
<Card Title="Game Details">
<Body>
<Form Model="@Game" Layout="@FormLayout.Vertical">
<FormItem Label="Title">
<GameMetadataLookup @ref="GameMetadataLookup" OnResultSelected="OnGameLookupResultSelected" />
<MudDivider Class="mt-4 mb-4" />
<Space Style="display: flex">
<SpaceItem Style="flex-grow: 1">
<Input @bind-Value="@context.Title" />
</SpaceItem>
<SpaceItem>
<Button OnClick="() => GameMetadataLookup.SearchForGame(context.Title)" Type="@ButtonType.Primary">Lookup</Button>
</SpaceItem>
</Space>
</FormItem>
<FormItem Label="Sort Title">
<Input @bind-Value="@context.SortTitle" />
</FormItem>
<FormItem Label="Icon">
<Space Style="display: flex">
<SpaceItem Style="flex-grow: 1">
<Input @bind-Value="@context.Icon" />
</SpaceItem>
<SpaceItem>
<Button OnClick="BrowseForIcon" Type="@ButtonType.Primary">Browse</Button>
</SpaceItem>
</Space>
</FormItem>
<FormItem Label="Description">
<TextArea @bind-Value="@context.Description" MaxLength=500 ShowCount />
</FormItem>
<FormItem Label="Released On">
<DatePicker TValue="DateTime?" @bind-Value="@context.ReleasedOn" Picker="@DatePickerType.Date" />
</FormItem>
<FormItem Label="Singleplayer">
<Checkbox @bind-Checked="@context.Singleplayer" />
</FormItem>
<FormItem Label="Developers">
<TagsInput Entities="Companies" SelectedEntities="Game.Developers" OptionLabelSelector="c => c.Name" TItem="Company" />
</FormItem>
<FormItem Label="Publishers">
<TagsInput Entities="Companies" SelectedEntities="Game.Publishers" OptionLabelSelector="c => c.Name" TItem="Company" />
</FormItem>
<FormItem Label="Genres">
<TagsInput Entities="Genres" SelectedEntities="Game.Genres" OptionLabelSelector="c => c.Name" TItem="Genre" />
</FormItem>
<FormItem Label="Tags">
<TagsInput Entities="Tags" SelectedEntities="Game.Tags" OptionLabelSelector="c => c.Name" TItem="Data.Models.Tag" />
</FormItem>
<FormItem>
<Button Type="@ButtonType.Primary" OnClick="Save" Icon="@IconType.Fill.Save">Save</Button>
</FormItem>
</Form>
</Body>
</Card>
</SpaceItem>
<MudText Typo="Typo.h6">Developers</MudText>
<SpaceItem>
<Card Title="Actions">
<Body>
<ActionEditor Game="Game" />
</Body>
</Card>
</SpaceItem>
<MudChipSet>
@foreach (var developer in Game.Developers)
{
<MudChip>@developer.Name</MudChip>
}
</MudChipSet>
<SpaceItem>
<Card Title="Multiplayer Modes">
<Body>
<MultiplayerModeEditor Game="Game" />
</Body>
</Card>
</SpaceItem>
<MudDivider Class="mt-4 mb-4" />
<SpaceItem>
<Card Title="Keys">
<Extra>
<Button OnClick="() => KeysEditor.Edit()">Edit</Button>
<Button OnClick="() => KeysEditor.View()" Type="@ButtonType.Primary">View</Button>
</Extra>
<Body>
<KeysEditor @ref="KeysEditor" Game="Game" />
</Body>
</Card>
</SpaceItem>
<MudText Typo="Typo.h6">Publishers</MudText>
<SpaceItem>
<Card Title="Scripts">
<Body>
<ScriptEditor Game="Game" />
</Body>
</Card>
</SpaceItem>
<MudChipSet>
@foreach (var publisher in Game.Publishers)
{
<MudChip>@publisher.Name</MudChip>
}
</MudChipSet>
<MudDivider Class="mt-4 mb-4" />
<MudText Typo="Typo.h6">Genres</MudText>
<MudChipSet>
@foreach (var genre in Game.Genres)
{
<MudChip>@genre.Name</MudChip>
}
</MudChipSet>
<MudDivider Class="mt-4 mb-4" />
<MudText Typo="Typo.h6">Tags</MudText>
<MudChipSet>
@foreach (var tags in Game.Tags)
{
<MudChip>@tags.Name</MudChip>
}
</MudChipSet>
</MudForm>
</MudPaper>
</MudItem>
</MudGrid>
<MudCard Class="mt-4">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">Actions</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<ActionEditor Actions="Game.Actions" ArchiveId="Game.Archives.OrderByDescending(a => a.CreatedOn).First().Id" />
</MudCardContent>
</MudCard>
<MudCard Class="mt-4">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">Multiplayer Modes</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MultiplayerModeEditor Game="Game" />
</MudCardContent>
</MudCard>
<MudCard Class="mt-4">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">Keys</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudGrid>
<MudItem md="4" Class="mud-typography-align-center">
<MudText Typo="Typo.overline">Available</MudText>
<MudText Typo="Typo.body1">@KeysAvailable</MudText>
</MudItem>
<MudItem md="4" Class="mud-typography-align-center">
<MudText Typo="Typo.overline">Claimed</MudText>
<MudText Typo="Typo.body1">@(Game.Keys.Count - KeysAvailable)</MudText>
</MudItem>
<MudItem md="4" Class="mud-typography-align-center">
<MudText Typo="Typo.overline">Total</MudText>
<MudText Typo="Typo.body1">@Game.Keys.Count</MudText>
</MudItem>
</MudGrid>
<MudPaper Class="d-flex justify-end" Elevation="0">
<MudButton Href="@($"/Games/{Id}/Keys")" Color="Color.Primary" Variant="Variant.Filled">View</MudButton>
</MudPaper>
</MudCardContent>
</MudCard>
<MudCard Class="mt-4">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">Archives</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudTable Items="Game.Archives" Elevation="0">
<HeaderContent>
<MudTh>Version</MudTh>
<MudTh>Uploaded By</MudTh>
<MudTh>Uploaded On</MudTh>
<MudTh>Size</MudTh>
<MudTh></MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.Version</MudTd>
<MudTd>@context.CreatedBy?.UserName</MudTd>
<MudTd>@context.CreatedOn</MudTd>
<MudTd>
@{
long size = 0;
var path = Path.Combine("Upload", context.ObjectKey);
if (File.Exists(path))
size = new FileInfo(path).Length;
}
@ByteSizeLib.ByteSize.FromBytes(size)
</MudTd>
<MudTd Class="d-flex flex-nowrap justify-end">
<MudIconButton OnClick="() => BrowseArchive(context)" Icon="@Icons.Material.Filled.Folder"></MudIconButton>
<MudIconButton OnClick="() => DeleteArchive(context)" Color="Color.Error" Icon="@Icons.Material.Filled.Close"></MudIconButton>
</MudTd>
</RowTemplate>
</MudTable>
<MudPaper Class="d-flex justify-end" Elevation="0">
<MudButton OnClick="() => UploadArchive()" StartIcon="@Icons.Material.Filled.Add" Color="Color.Primary" Variant="Variant.Filled">Add</MudButton>
</MudPaper>
</MudCardContent>
</MudCard>
<MudCard Class="mt-4">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">Scripts</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudTable Items="Game.Scripts" Elevation="0">
<HeaderContent>
<MudTh>Type</MudTh>
<MudTh>Created By</MudTh>
<MudTh>Created On</MudTh>
<MudTh></MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.Type</MudTd>
<MudTd>@context.CreatedBy?.UserName</MudTd>
<MudTd>@context.CreatedOn</MudTd>
<MudTd Class="d-flex flex-nowrap justify-end">
<MudIconButton OnClick="() => EditScript(context)" Icon="@Icons.Material.Filled.Edit"></MudIconButton>
<MudIconButton OnClick="() => DeleteScript(context)" Color="Color.Error" Icon="@Icons.Material.Filled.Close"></MudIconButton>
</MudTd>
</RowTemplate>
</MudTable>
<MudPaper Class="d-flex justify-end" Elevation="0">
<MudButton OnClick="() => EditScript()" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add" Variant="Variant.Filled">Add</MudButton>
</MudPaper>
</MudCardContent>
</MudCard>
<MudFab Color="Color.Primary" Disabled="@(!Success)" OnClick="Save" StartIcon="@Icons.Material.Filled.Save" Style="position: fixed; right: 32px; bottom: 32px;" />
<SpaceItem>
<Card Title="Archives">
<ArchiveUploader Game="Game" />
</Card>
</SpaceItem>
</Space>
@code {
[Parameter] public Guid Id { get; set; }
bool Success;
string[] Errors = { };
MudForm Form;
private Game Game { get; set; }
IEnumerable<Company> Companies;
IEnumerable<Genre> Genres;
IEnumerable<Data.Models.Tag> Tags;
ArchiveBrowserDialog ArchiveBrowserDialog;
Modal FileSelectorModal;
private string value = "blazor";
private ConfirmRef _confirmRef;
private Game Game;
private KeysEditor? KeysEditor;
private GameMetadataLookup? GameMetadataLookup;
private int KeysAvailable { get {
return Game.Keys.Count(k =>
@ -219,6 +146,9 @@
protected override async Task OnInitializedAsync()
{
Game = await GameService.Get(Id);
Companies = CompanyService.Get();
Genres = GenreService.Get();
Tags = TagService.Get();
}
private async Task Save()
@ -227,35 +157,105 @@
{
Game = await GameService.Update(Game);
Snackbar.Add("Game updated!", Severity.Success);
await MessageService.Success("Game updated!");
}
catch (Exception ex)
{
Snackbar.Add("An unknown error occurred!", Severity.Error);
await MessageService.Error("Could not save!");
}
}
private async void BrowseForIcon()
private async Task BrowseForIcon()
{
var parameters = new DialogParameters
var modalOptions = new ModalOptions()
{
["ArchiveId"] = Game.Archives.OrderByDescending(a => a.CreatedOn).First().Id
Title = "Choose Icon",
Maximizable = false,
DefaultMaximized = true,
Closable = true,
OkText = "Select File"
};
var dialog = await DialogService.ShowAsync<ArchiveFileSelectorDialog>("File Selector", parameters);
var result = await dialog.Result;
if (!result.Canceled)
var browserOptions = new ArchiveBrowserOptions()
{
Game.Icon = result.Data as string;
ArchiveId = Game.Archives.FirstOrDefault().Id,
Select = true,
Multiple = false
};
var modalRef = await ModalService.CreateModalAsync<ArchiveBrowserDialog, ArchiveBrowserOptions, IEnumerable<ZipArchiveEntry>>(modalOptions, browserOptions);
modalRef.OnOk = (results) =>
{
Game.Icon = results.FirstOrDefault().FullName;
StateHasChanged();
return Task.CompletedTask;
};
}
private async Task OnGameLookupResultSelected(GameLookupResult result)
{
Game.Title = result.IGDBMetadata.Name;
Game.Description = result.IGDBMetadata.Summary;
Game.ReleasedOn = result.IGDBMetadata.FirstReleaseDate.GetValueOrDefault().UtcDateTime;
Game.MultiplayerModes = result.MultiplayerModes.ToList();
Game.Developers = new List<Company>();
Game.Publishers = new List<Company>();
Game.Genres = new List<Genre>();
Game.Tags = new List<Data.Models.Tag>();
if (result.IGDBMetadata.GameModes != null && result.IGDBMetadata.GameModes.Values != null)
Game.Singleplayer = result.IGDBMetadata.GameModes.Values.Any(gm => gm.Name == "Singleplayer");
if (result.IGDBMetadata.InvolvedCompanies != null && result.IGDBMetadata.InvolvedCompanies.Values != null)
{
// Make sure companie
var developers = result.IGDBMetadata.InvolvedCompanies.Values.Where(c => c.Developer.GetValueOrDefault()).Select(c => c.Company.Value.Name);
var publishers = result.IGDBMetadata.InvolvedCompanies.Values.Where(c => c.Publisher.GetValueOrDefault()).Select(c => c.Company.Value.Name);
foreach (var developer in developers)
{
Game.Developers.Add(await CompanyService.AddMissing(c => c.Name == developer, new Company { Name = developer }));
}
foreach (var publisher in publishers)
{
Game.Publishers.Add(await CompanyService.AddMissing(c => c.Name == publisher, new Company { Name = publisher }));
}
}
if (result.IGDBMetadata.Genres != null && result.IGDBMetadata.Genres.Values != null)
{
var genres = result.IGDBMetadata.Genres.Values.Select(g => g.Name);
foreach (var genre in genres)
{
Game.Genres.Add(await GenreService.AddMissing(g => g.Name == genre, new Genre { Name = genre }));
}
}
if (result.IGDBMetadata.Keywords != null && result.IGDBMetadata.Keywords.Values != null)
{
var tags = result.IGDBMetadata.Keywords.Values.Select(t => t.Name).Take(20);
foreach (var tag in tags)
{
Game.Tags.Add(await TagService.AddMissing(t => t.Name == tag, new Data.Models.Tag { Name = tag }));
}
}
}
private async Task CloseModal()
{
if (_confirmRef != null)
{
await _confirmRef.CloseAsync();
}
}
private async void LookupGameMetadata()
{
var parameters = new DialogParameters
/*var parameters = new DialogParameters
{
["GameTitle"] = Game.Title
};
@ -278,12 +278,12 @@
Game.Singleplayer = info.Singleplayer;
StateHasChanged();
}
}*/
}
private async void UploadArchive()
{
var parameters = new DialogParameters
/*var parameters = new DialogParameters
{
["GameId"] = Game.Id
};
@ -294,22 +294,22 @@
await GameService.Context.Entry(Game).Collection(nameof(Game.Archives)).LoadAsync();
StateHasChanged();
StateHasChanged();*/
}
private async void BrowseArchive(Archive archive)
{
var parameters = new DialogParameters
/*var parameters = new DialogParameters
{
["ArchiveId"] = archive.Id
};
var dialog = await DialogService.ShowAsync<ArchiveBrowserDialog>("Archive Browser", parameters);
var dialog = await DialogService.ShowAsync<ArchiveBrowserDialog>("Archive Browser", parameters);*/
}
private async void DeleteArchive(Archive archive)
{
bool? result = await DialogService.ShowMessageBox(
/*bool? result = await DialogService.ShowMessageBox(
"Delete Archive?",
"Do you really want to delete this archive? You will not be able to recover it later.",
"Delete",
@ -319,12 +319,12 @@
if (result == true)
await ArchiveService.Delete(archive);
StateHasChanged();
StateHasChanged();*/
}
private async void EditScript(Script script = null)
{
if (script == null)
/*if (script == null)
script = new Script()
{
GameId = Game.Id,
@ -342,12 +342,12 @@
await GameService.Context.Entry(Game).Collection(nameof(Game.Archives)).LoadAsync();
StateHasChanged();
StateHasChanged();*/
}
private async void DeleteScript(Script script)
{
bool? result = await DialogService.ShowMessageBox(
/*bool? result = await DialogService.ShowMessageBox(
"Delete Script",
"Do you really want to delete this script? You will not be able to recover it later.",
"Delete",
@ -357,6 +357,6 @@
if (result == true)
await ScriptService.Delete(script);
StateHasChanged();
StateHasChanged();*/
}
}

View file

@ -1,56 +1,50 @@
@page "/Games"
@attribute [Authorize]
@inject GameService GameService
@inject NavigationManager NavigationManager
<MudTable Items="@Games.Where(g => String.IsNullOrEmpty(Search) || g.Title.ToLower().Contains(Search.ToLower().Trim()))" RowsPerPage="25" Hover="true">
<ToolBarContent>
<MudText Typo="Typo.h6">Games</MudText>
<MudSpacer />
<MudTextField @bind-Value="Search" Placeholder="Search" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"></MudTextField>
</ToolBarContent>
<HeaderContent>
<MudTh></MudTh>
<MudTh>Title</MudTh>
<MudTh>Sort Title</MudTh>
<MudTh>Released</MudTh>
<MudTh>Created</MudTh>
<MudTh>Created By</MudTh>
<MudTh>Updated</MudTh>
<MudTh>Updated By</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>
<MudImage Src="@GetIcon(context)" Height="32" Width="32" />
</MudTd>
<MudTd DataLabel="Title">@context.Title</MudTd>
<MudTd DataLabel="Sort Title">@context.SortTitle</MudTd>
<MudTd DataLabel="Released">@context.ReleasedOn?.ToString("MM/dd/yyyy")</MudTd>
<MudTd DataLabel="Created">@context.CreatedOn</MudTd>
<MudTd DataLabel="Created By">@context.CreatedBy?.UserName</MudTd>
<MudTd DataLabel="Updated">@context.UpdatedOn</MudTd>
<MudTd DataLabel="Updated By">@context.UpdatedBy?.UserName</MudTd>
<MudTd Class="d-flex justify-end">
<MudButton Href="@($"/Games/{context.Id}/Edit")">Edit</MudButton>
</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager />
</PagerContent>
</MudTable>
<Table TItem="Game" DataSource="@Games">
<Column TData="string">
<Image Src="@GetIcon(context)" Height="32" Width="32" Preview="false"></Image>
</Column>
<PropertyColumn Property="g => g.Title" Sortable />
<PropertyColumn Property="g => g.SortTitle" Sortable />
<PropertyColumn Property="g => g.ReleasedOn" Format="MM/dd/yyyy" Sortable />
<PropertyColumn Property="g => g.CreatedOn" Format="MM/dd/yyyy hh:mm" Sortable />
<PropertyColumn Property="g => g.CreatedBy" Sortable>
@context.CreatedBy?.UserName
</PropertyColumn>
<PropertyColumn Property="g => g.UpdatedOn" Format="MM/dd/yyyy hh:mm" Sortable />
<PropertyColumn Property="g => g.UpdatedBy" Sortable>
@context.UpdatedBy?.UserName
</PropertyColumn>
<ActionColumn Title="">
<Space>
<SpaceItem>
<Button OnClick="() => Edit(context)">Edit</Button>
</SpaceItem>
</Space>
</ActionColumn>
</Table>
@code {
private ICollection<Game> Games { get; set; }
IEnumerable<Game> Games { get; set; } = new List<Game>();
private string Search { get; set; }
protected override async Task OnInitializedAsync()
{
Games = GameService.Get().OrderBy(g => String.IsNullOrWhiteSpace(g.SortTitle) ? g.Title : g.SortTitle).ToList();
StateHasChanged();
}
private string GetIcon(Game game)
{
return $"/api/Games/{game.Id}/Icon.png";
}
private void Edit(Game game)
{
NavigationManager.NavigateTo($"/Games/{game.Id}/Edit");
}
}

View file

@ -1,65 +0,0 @@
@page "/Games/{id:guid}/Keys/Edit"
@inject GameService GameService
@inject KeyService KeyService
@inject NavigationManager NavigationManager
<MudGrid>
<MudItem xs="12">
<MudPaper Class="pa-4">
<StandaloneCodeEditor @ref="Editor" Id="editor" ConstructionOptions="EditorConstructionOptions" />
</MudPaper>
</MudItem>
</MudGrid>
<style>
.monaco-editor-container {
height: 600px;
}
</style>
<MudFab Color="Color.Primary" OnClick="Save" StartIcon="@Icons.Material.Filled.Save" Style="position: fixed; right: 32px; bottom: 32px;" />
@code {
[Parameter] public Guid Id { get; set; }
private Game Game { get; set; }
private StandaloneCodeEditor Editor;
private StandaloneEditorConstructionOptions EditorConstructionOptions(StandaloneCodeEditor editor)
{
return new StandaloneEditorConstructionOptions
{
AutomaticLayout = true,
Language = "text",
Value = String.Join('\n', Game.Keys.Select(k => k.Value)),
Theme = "vs-dark",
};
}
protected override async Task OnInitializedAsync()
{
Game = await GameService.Get(Id);
}
private async Task Save()
{
var value = await Editor.GetValue();
var keys = value.Split("\n").Select(k => k.Trim()).Where(k => !String.IsNullOrWhiteSpace(k));
var keysDeleted = Game.Keys.Where(k => !keys.Contains(k.Value));
var keysAdded = keys.Where(k => !Game.Keys.Any(gk => gk.Value == k));
foreach (var key in keysDeleted)
KeyService.Delete(key);
foreach (var key in keysAdded)
await KeyService.Add(new Key()
{
Game = Game,
Value = key
});
NavigationManager.NavigateTo($"/Games/{Game.Id}/Keys");
}
}

View file

@ -1,61 +0,0 @@
@page "/Games/{id:guid}/Keys"
@inject GameService GameService
@inject KeyService KeyService
<MudTable Items="@Keys" Hover="true" Elevation="0">
<ToolBarContent>
<MudText Typo="Typo.h6">Keys</MudText>
<MudSpacer />
<MudButton Color="Color.Primary" Variant="Variant.Filled" Href="@($"/Games/{Id}/Keys/Edit")">Edit</MudButton>
</ToolBarContent>
<HeaderContent>
<MudTh>Key</MudTh>
<MudTh>Allocation Method</MudTh>
<MudTh>Claimed By</MudTh>
<MudTh>Claimed On</MudTh>
<MudTh></MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.Value</MudTd>
<MudTd>@context.AllocationMethod</MudTd>
<MudTd>
@switch (context.AllocationMethod)
{
case KeyAllocationMethod.MacAddress:
<text>@context.ClaimedByMacAddress</text>
break;
case KeyAllocationMethod.UserAccount:
<text>@context.ClaimedByUser?.UserName</text>
break;
}
</MudTd>
<MudTd>@context.ClaimedOn</MudTd>
<MudTd>
@if ((context.AllocationMethod == KeyAllocationMethod.MacAddress && !String.IsNullOrWhiteSpace(context.ClaimedByMacAddress)) || (context.AllocationMethod == KeyAllocationMethod.UserAccount && context.ClaimedByUser != null))
{
<MudButton OnClick="() => Release(context)">Release</MudButton>
}
</MudTd>
</RowTemplate>
</MudTable>
@code {
[Parameter] public Guid Id { get; set; }
private ICollection<Key> Keys { get; set; }
protected override async Task OnInitializedAsync()
{
var game = await GameService.Get(Id);
Keys = game.Keys;
}
private async Task Release(Key key)
{
key = await KeyService.Release(key);
}
}

View file

@ -2,66 +2,44 @@
@using LANCommander.Models;
@layout SettingsLayout
@inject SettingService SettingService
@inject ISnackbar Snackbar
@inject IMessageService MessageService
<MudCard Elevation="0">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">General</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudText Typo="Typo.subtitle1">IGDB Credentials</MudText>
<MudForm Model="@Settings" @ref="Form">
<MudTextField @bind-Value="Settings.IGDBClientId" For="@(() => Settings.IGDBClientId)" Immediate="true" Label="Client ID" />
<MudTextField @bind-Value="Settings.IGDBClientSecret"
For="@(() => Settings.IGDBClientSecret)"
Immediate="true"
Label="Client Secret"
InputType="@IGDBClientSecretInputType"
Adornment="Adornment.End"
AdornmentIcon="@IGDBClientSecretInputIcon"
OnAdornmentClick="() => ToggleClientSecretInput()" />
<MudText Typo="Typo.caption">In order to use IGDB metadata, you need a Twitch developer account. <MudLink Href="https://api-docs.igdb.com/#account-creation" Typo="Typo.caption" Target="_blank">Click here</MudLink> for more details.</MudText>
</MudForm>
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Save" OnClick="Save">Save</MudButton>
</MudCardActions>
</MudCard>
<Card Title="General">
<Body>
<Text>IGDB Credentials</Text>
<Text>In order to use IGDB metadata, you need a Twitch developer account. <a href="https://api-docs.igdb.com/#account-creation" target="_blank">Click here</a> for more details.</Text>
<Form Model="Settings">
<FormItem Label="Client ID">
<Input @bind-Value="context.IGDBClientId" />
</FormItem>
<FormItem Label="Client Secret">
<InputPassword @bind-Value="context.IGDBClientSecret" />
</FormItem>
<FormItem>
<Button OnClick="Save" Icon="@IconType.Fill.Save">Save</Button>
</FormItem>
</Form>
</Body>
</Card>
@code {
private MudForm Form;
private LANCommanderSettings Settings;
private bool ShowIGDBClientSecret = false;
private InputType IGDBClientSecretInputType = InputType.Password;
private string IGDBClientSecretInputIcon = Icons.Material.Filled.Visibility;
protected override async Task OnInitializedAsync()
{
Settings = SettingService.GetSettings();
}
private void ToggleClientSecretInput()
{
ShowIGDBClientSecret = !ShowIGDBClientSecret;
IGDBClientSecretInputIcon = ShowIGDBClientSecret ? Icons.Material.Filled.VisibilityOff : Icons.Material.Filled.Visibility;
IGDBClientSecretInputType = ShowIGDBClientSecret ? InputType.Text : InputType.Password;
}
private void Save()
{
try
{
SettingService.SaveSettings(Settings);
Snackbar.Add("Settings saved!", Severity.Success);
MessageService.Success("Settings saved!");
}
catch
{
Snackbar.Add("An unknown error occurred", Severity.Error);
MessageService.Error("An unknown error occurred.");
}
}
}

View file

@ -1,19 +1,15 @@
@inherits LayoutComponentBase
@layout MainLayout
<MudPaper>
<MudGrid Style="width: 100%">
<MudItem xs="12" sm="4" md="3" lg="2">
<MudNavMenu Bordered="true">
<MudText Typo="Typo.h6" Class="px-4">Settings</MudText>
<MudDivider Class="my-2" />
<MudNavLink Href="/SettingsNew/General" Match="NavLinkMatch.Prefix">General</MudNavLink>
<MudNavLink Href="/SettingsNew/Users" Match="NavLinkMatch.Prefix">Users</MudNavLink>
</MudNavMenu>
</MudItem>
<GridRow>
<GridCol Span="6">
<Menu Mode=@MenuMode.Vertical>
<MenuItem RouterLink="/Settings/General">General</MenuItem>
<MenuItem RouterLink="/Settings/Users">Users</MenuItem>
</Menu>
</GridCol>
<MudItem xs="12" sm="8" md="9" lg="10">
@Body
</MudItem>
</MudGrid>
</MudPaper>
<GridCol Span="18">
@Body
</GridCol>
</GridRow>

View file

@ -3,38 +3,35 @@
@layout SettingsLayout
@inject UserManager<User> UserManager
@inject RoleManager<Role> RoleManager
@inject ISnackbar Snackbar
@inject IMessageService MessageService
<MudTable Items="@UserList.Where(u => String.IsNullOrEmpty(Search) || u.UserName.ToLower().Contains(Search.ToLower().Trim()))" RowsPerPage="25" Hover="true" Elevation="0">
<ToolBarContent>
<MudText Typo="Typo.h6">Users</MudText>
<MudSpacer />
<MudTextField @bind-Value="Search" Placeholder="Search" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"></MudTextField>
</ToolBarContent>
<HeaderContent>
<MudTh>Username</MudTh>
<MudTh>Roles</MudTh>
<MudTh>Saves</MudTh>
<MudTh></MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.UserName</MudTd>
<MudTd>@String.Join(", ", context.Roles)</MudTd>
<MudTd>@ByteSizeLib.ByteSize.FromBytes(context.SavesSize)</MudTd>
<MudTd>
@if (!context.Roles.Any(r => r == "Administrator"))
{
<MudButton OnClick="() => PromoteUser(context)">Promote</MudButton>
}
else
{
<MudButton OnClick="() => DemoteUser(context)">Demote</MudButton>
}
</MudTd>
</RowTemplate>
</MudTable>
<Card Title="Users">
<Body>
<Table TItem="UserViewModel" DataSource="@UserList">
<PropertyColumn Property="u => u.UserName" />
<PropertyColumn Property="u => u.Roles">
@String.Join(", ", context.Roles)
</PropertyColumn>
<PropertyColumn Property="u => u.SavesSize">
@ByteSizeLib.ByteSize.FromBytes(context.SavesSize)
</PropertyColumn>
<ActionColumn>
<Space>
<SpaceItem>
@if (!context.Roles.Any(r => r == "Administrator"))
{
<Button OnClick="() => PromoteUser(context)">Promote</Button>
}
else
{
<Button OnClick="() => DemoteUser(context)">Demote</Button>
}
</SpaceItem>
</Space>
</ActionColumn>
</Table>
</Body>
</Card>
@code {
private ICollection<UserViewModel> UserList { get; set; }
@ -73,14 +70,14 @@
await UserManager.AddToRoleAsync(UserManager.Users.First(u => u.UserName == user.UserName), "Administrator");
await RefreshUserList();
Snackbar.Add($"Promoted {user.UserName}!", Severity.Success);
await MessageService.Success($"Promoted {user.UserName}!");
}
private async Task DemoteUser(UserViewModel user)
{
if (UserList.SelectMany(u => u.Roles).Count(r => r == "Administrator") == 1)
{
Snackbar.Add("Cannot demote the only administrator!", Severity.Error);
await MessageService.Error("Cannot demote the only administrator!");
}
else
{

View file

@ -11,6 +11,7 @@
<base href="~/" />
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" />
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
<link href="_content/AntDesign/css/ant-design-blazor.css" rel="stylesheet" />
<link href="~/css/site.css" rel="stylesheet" />
</head>
<body>
@ -21,6 +22,7 @@
</div>
<script src="~/_content/MudBlazor/MudBlazor.min.js"></script>
<script src="~/_content/AntDesign/js/ant-design-blazor.js"></script>
<script src="~/_framework/blazor.server.js"></script>
<script src="~/_content/BlazorMonaco/jsInterop.js"></script>
<script src="~/_content/BlazorMonaco/lib/monaco-editor/min/vs/loader.js"></script>

View file

@ -81,6 +81,8 @@ builder.Services.AddControllers().AddJsonOptions(x =>
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddAntDesign();
builder.Services.AddMudServices(config =>
{
config.SnackbarConfiguration.PositionClass = Defaults.Classes.Position.BottomLeft;

View file

@ -1,59 +1,17 @@
@inherits LayoutComponentBase
<MudThemeProvider Theme="DarkTheme" />
<MudDialogProvider />
<MudSnackbarProvider />
<Layout Class="layout">
<Header>
<div class="logo" />
<MudLayout Class="mb-16">
<MudAppBar>
<MudIconButton Icon="@Icons.Material.Filled.Menu" Color="Color.Inherit" Edge="Edge.Start" OnClick="@((e) => DrawerToggle())" />
LANCommander
</MudAppBar>
<Menu Theme="MenuTheme.Dark" Mode="MenuMode.Horizontal">
<MenuItem Key="1">Dashboard</MenuItem>
<MenuItem Key="2">Games</MenuItem>
<MenuItem Key="3">Settings</MenuItem>
</Menu>
</Header>
<MudDrawer @bind-Open="@_drawerOpen">
<NavMenu />
</MudDrawer>
<MudMainContent>
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-8 mb-16">
@Body
</MudContainer>
</MudMainContent>
</MudLayout>
@code {
bool _drawerOpen = true;
private static readonly MudTheme DarkTheme = new MudTheme()
{
Palette = new Palette()
{
Black = "#27272f",
Background = "#32333d",
BackgroundGrey = "#27272f",
Surface = "#373740",
DrawerBackground = "#27272f",
DrawerText = "rgba(255,255,255, 0.50)",
DrawerIcon = "rgba(255,255,255, 0.50)",
AppbarBackground = "#27272f",
AppbarText = "rgba(255,255,255, 0.70)",
TextPrimary = "rgba(255,255,255, 0.70)",
TextSecondary = "rgba(255,255,255, 0.50)",
ActionDefault = "#adadb1",
ActionDisabled = "rgba(255,255,255, 0.26)",
ActionDisabledBackground = "rgba(255,255,255, 0.12)",
Divider = "rgba(255,255,255, 0.12)",
DividerLight = "rgba(255,255,255, 0.06)",
TableLines = "rgba(255,255,255, 0.12)",
LinesDefault = "rgba(255,255,255, 0.12)",
LinesInputs = "rgba(255,255,255, 0.3)",
TextDisabled = "rgba(255,255,255, 0.2)",
TableStriped = "#3f3f45"
}
};
void DrawerToggle()
{
_drawerOpen = !_drawerOpen;
}
}
<Content Style="padding: 24px;">
@Body
</Content>
</Layout>

View file

@ -1,10 +1,12 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Identity
@using Microsoft.JSInterop
@using MudBlazor
@using AntDesign
@using BlazorMonaco
@using BlazorMonaco.Editor
@using LANCommander.Components

View file

@ -7,7 +7,6 @@
"target": "es6"
},
"exclude": [
"node_modules",
"wwwroot/lib"
"node_modules"
]
}

View file

@ -1,9 +0,0 @@
.mud-table-cell .mud-input-control > .mud-input-control-input-container > div.mud-input.mud-input-text,
.mud-table-cell .mud-input-control {
margin-top: 0;
}
.mud-dialog-fullscreen .mud-dialog-title + div > div,
.mud-dialog-fullscreen .mud-dialog-content {
max-height: calc(100vh - 64px);
}