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"> <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> </Found>
<NotFound> <NotFound>
<LayoutView Layout="@typeof(MainLayout)"> <LayoutView Layout="@typeof(MainLayout)">

View file

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

View file

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

View file

@ -1,32 +1,14 @@
<MudDialog> @inherits FeedbackComponent<ArchiveBrowserOptions, IEnumerable<ZipArchiveEntry>>
<TitleContent> @using System.IO.Compression;
<MudText Typo="Typo.h6"> @using LANCommander.Models;
Browse Archive
</MudText>
</TitleContent>
<DialogContent> <ArchiveBrowser ArchiveId="Options.ArchiveId" @bind-SelectedFiles="SelectedFiles" Select="Options.Select" Multiple="Options.Multiple" />
<ArchiveBrowser ArchiveId="ArchiveId" />
</DialogContent>
</MudDialog>
@code { @code {
[CascadingParameter] MudDialogInstance MudDialog { get; set; } private IEnumerable<ZipArchiveEntry> SelectedFiles { get; set; }
[Parameter] public Guid ArchiveId { get; set; }
protected override async Task OnInitializedAsync() public override async Task OnFeedbackOkAsync(ModalClosingEventArgs args)
{ {
MudDialog.Options.MaxWidth = MaxWidth.Large; await base.OkCancelRefWithResult!.OnOk(SelectedFiles);
MudDialog.Options.FullWidth = true;
MudDialog.Options.FullScreen = true;
MudDialog.Options.CloseButton = true;
MudDialog.Options.CloseOnEscapeKey = true;
MudDialog.SetOptions(MudDialog.Options);
}
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; @using System.Diagnostics;
@inject HttpClient HttpClient @inject HttpClient HttpClient
@inject NavigationManager Navigator @inject NavigationManager Navigator
@inject ISnackbar Snackbar
@inject ArchiveService ArchiveService @inject ArchiveService ArchiveService
@inject IMessageService MessageService
<MudDialog> <Space Direction="DirectionVHType.Vertical" Style="width: 100%">
<DialogContent> <SpaceItem>
<MudForm @bind-IsValid="@IsValid"> <Table TItem="Archive" DataSource="@Game.Archives.OrderByDescending(a => a.CreatedOn)" HidePagination="true">
<MudTextField T="string" @bind-Value="Archive.Version" Label="Version" Required="true" Disabled="Uploading" RequiredError="Version is required" /> <PropertyColumn Property="a => a.Version" />
<MudTextField T="string" @bind-Value="Archive.Changelog" Label="Changelog" Required="false" Disabled="Uploading" Lines="6" /> <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" <SpaceItem>
InputClass="absolute mud-width-full mud-height-full overflow-hidden z-20 d-block" InputStyle="opacity: 0;" <GridRow Justify="end">
@ondragenter="@SetDragClass" @ondragleave="@ClearDragClass" @ondragend="@ClearDragClass"> <GridCol>
<ButtonTemplate> <Button OnClick="AddArchive" Type="@ButtonType.Primary">Upload Archive</Button>
<MudPaper Height="200px" Outlined="true" Class="@DragClass"> </GridCol>
<MudText Typo="Typo.h6">Drop files here or click to browse</MudText> </GridRow>
</SpaceItem>
</Space>
@if (File != null) @{
{ RenderFragment Footer =
<MudChip Color="Color.Dark" Text="@File.Name" /> @<Template>
} <Button OnClick="UploadArchive" Disabled="@(File == null || Uploading)" Type="@ButtonType.Primary">Upload</Button>
</MudPaper> <Button OnClick="Clear" Disabled="File == null || Uploading" Danger>Clear</Button>
</ButtonTemplate> <Button OnClick="Cancel">Cancel</Button>
</MudFileUpload> </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"> <FormItem>
<MudButton OnClick="UploadArchive" Disabled="@(!IsValid || File == null || Uploading)" Color="Color.Primary" Variant="Variant.Filled">Upload</MudButton> <InputFile id="FileInput" OnChange="FileSelected" hidden />
<MudButton OnClick="Clear" Disabled="File == null || Uploading" Color="Color.Error" Variant="Variant.Filled">Clear</MudButton>
<MudButton OnClick="Cancel">Cancel</MudButton> <Upload Name="files" FileList="FileList">
</MudToolBar> <label class="ant-btn" for="FileInput">
</MudForm> <Icon Type="upload" />
</DialogContent> Select Archive
</MudDialog> </label>
</Upload>
</FormItem>
<FormItem>
<Progress Percent="Progress" />
<Text>@ByteSizeLib.ByteSize.FromBytes(Speed)/s</Text>
</FormItem>
</Form>
</Modal>
@code { @code {
[CascadingParameter] MudDialogInstance MudDialog { get; set; } [Parameter] public Game Game { get; set; }
[Parameter] public Guid GameId { get; set; }
Archive Archive; Archive Archive;
IBrowserFile File { get; set; } IBrowserFile File { get; set; }
List<UploadFileItem> FileList = new List<UploadFileItem>();
bool IsValid = false; 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 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; private string DragClass = DefaultDragClass;
@ -63,42 +99,49 @@
protected override async Task OnInitializedAsync() 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); HttpClient.BaseAddress = new Uri(Navigator.BaseUri);
Archive = new Archive() Archive = new Archive()
{ {
GameId = GameId, GameId = Game.Id,
Id = Guid.NewGuid() 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() private void Clear()
{ {
File = null; File = null;
ClearDragClass();
} }
private void Cancel() private void Cancel()
{ {
MudDialog.Cancel(); File = null;
ModalVisible = false;
} }
private void FileSelected(InputFileChangeEventArgs args) private void FileSelected(InputFileChangeEventArgs args)
@ -159,7 +202,7 @@
{ {
Watch.Stop(); Watch.Stop();
Uploading = false; Uploading = false;
UploadComplete(); await UploadComplete();
} }
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
@ -172,9 +215,10 @@
Archive.ObjectKey = Archive.Id.ToString(); Archive.ObjectKey = Archive.Id.ToString();
Archive.CompressedSize = File.Size; Archive.CompressedSize = File.Size;
ArchiveService.Add(Archive); await ArchiveService.Add(Archive);
MudDialog.Close(); ModalVisible = false;
Snackbar.Add("Archive uploaded!", Severity.Success);
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.Models
@using LANCommander.PCGamingWiki @using LANCommander.PCGamingWiki
@inject IGDBService IGDBService @inject IGDBService IGDBService
@inject CompanyService CompanyService @inject CompanyService CompanyService
@inject GenreService GenreService @inject GenreService GenreService
@inject TagService TagService @inject TagService TagService
@inject ISnackbar Snackbar
<MudDialog> @{
<TitleContent> RenderFragment Footer =
<MudText Typo="Typo.h6"> @<Template>
<MudIcon Icon="@Icons.Material.Filled.DeleteForever" Class="mr-3 mb-n1" /> <Button OnClick="SelectGame" Disabled="@(Results == null || Results.Count() == 0)" Type="@ButtonType.Primary">Select</Button>
Results for @GameTitle <Button OnClick="() => ModalVisible= false">Cancel</Button>
</MudText> </Template>;
</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>
<RowTemplate> <Modal Visible="ModalVisible" Title="Game Metadata Lookup" Footer="@Footer">
<MudTh>@context.Title</MudTh> <Table
<MudTh>@context.ReleasedOn?.ToString("MM/dd/yyyy")</MudTh> @ref="ResultsTable"
<MudTh>@String.Join(", ", context.Developers?.Select(d => d.Name))</MudTh> TItem="Game"
<MudTh> DataSource="Results"
<MudButton OnClick="() => SelectGame(context)">Select</MudButton> HidePagination="true"
</MudTh> Loading="Results == null"
</RowTemplate> OnRowClick="OnRowClicked"
</MudTable> @bind-SelectedRows="SelectedResults"
} ScrollY="calc(100vh - 55px - 55px - 53px)">
</DialogContent>
<DialogActions> <Selection Key="@context.IGDBId.ToString()" Type="radio" />
<MudButton OnClick="Cancel">Cancel</MudButton> <PropertyColumn Property="g => g.Title" Title="Title" />
</DialogActions> <PropertyColumn Property="g => g.ReleasedOn" Format="MM/dd/yyyy" Title="Released" />
</MudDialog> <PropertyColumn Property="g => g.Developers">
@String.Join(", ", context.Developers?.Select(d => d.Name))
</PropertyColumn>
</Table>
</Modal>
@code { @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; } IEnumerable<Game> Results { get; set; }
private PCGamingWikiClient PCGamingWikiClient { get; set; } IEnumerable<Game> SelectedResults { get; set; }
PCGamingWikiClient PCGamingWikiClient { get; set; }
bool ModalVisible { get; set; } = false;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
PCGamingWikiClient = new PCGamingWikiClient(); 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) 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) if (results == null)
Results = new List<Game>(); 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; await OnResultSelected.InvokeAsync(result);
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>();
if (result.GameModes != null && result.GameModes.Values != null) ModalVisible = false;
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));
} }
private async Task<ICollection<MultiplayerMode>> GetMultiplayerModes(string gameTitle) 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.Enums
@using LANCommander.Data.Models @using LANCommander.Data.Models
<MudTable Items="@Game.MultiplayerModes" Elevation="0" Dense="true"> <Space Direction="DirectionVHType.Vertical" Size="@("large")" Style="width: 100%">
<HeaderContent> <SpaceItem>
<MudTh>Type</MudTh> <Table TItem="MultiplayerMode" DataSource="@Game.MultiplayerModes" HidePagination="true">
<MudTh>Min Players</MudTh> <PropertyColumn Property="m => m.Type">
<MudTh>Max Players</MudTh> <Select @bind-Value="context.Type" TItem="MultiplayerType" TItemValue="MultiplayerType" DataSource="Enum.GetValues<MultiplayerType>()" />
<MudTh>Description</MudTh> </PropertyColumn>
<MudTh></MudTh> <PropertyColumn Property="m => m.MinPlayers">
</HeaderContent> <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> <SpaceItem>
<MudTd> <GridRow Justify="end">
<MudSelect @bind-Value="context.Type" Margin="0"> <GridCol>
@foreach (MultiplayerType type in Enum.GetValues(typeof(MultiplayerType))) <Button OnClick="AddMode" Type="@ButtonType.Primary">Add Mode</Button>
{ </GridCol>
<MudSelectItem Value="@((MultiplayerType)type)">@type</MudSelectItem> </GridRow>
} </SpaceItem>
</MudSelect> </Space>
</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>
@code { @code {
[Parameter] public Game Game { get; set; } [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 string? ClaimedByComputerName { get; set; }
public virtual User? ClaimedByUser { get; set; } public virtual User? ClaimedByUser { get; set; }
public DateTime? ClaimedOn { 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 public enum KeyAllocationMethod

View file

@ -20,6 +20,8 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <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="BlazorMonaco" Version="3.0.0" />
<PackageReference Include="ByteSize" Version="2.1.1" /> <PackageReference Include="ByteSize" Version="2.1.1" />
<PackageReference Include="IGDB" Version="2.3.1" /> <PackageReference Include="IGDB" Version="2.3.1" />
@ -48,10 +50,11 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="Areas\Archive\Pages\" />
<Folder Include="bin\Debug\net6.0\" /> <Folder Include="bin\Debug\net6.0\" />
<Folder Include="Data\Migrations\" /> <Folder Include="Data\Migrations\" />
<Folder Include="Migrations\" /> <Folder Include="Migrations\" />
<Folder Include="Pages\Games\Archives\" />
<Folder Include="Pages\Settings\" />
</ItemGroup> </ItemGroup>
<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" @page "/Games/{id:guid}/Edit"
@using LANCommander.Models;
@using System.IO.Compression;
@attribute [Authorize(Roles = "Administrator")]
@inject GameService GameService @inject GameService GameService
@inject CompanyService CompanyService
@inject GenreService GenreService
@inject TagService TagService
@inject ArchiveService ArchiveService @inject ArchiveService ArchiveService
@inject ScriptService ScriptService @inject ScriptService ScriptService
@inject IDialogService DialogService @inject IMessageService MessageService
@inject ISnackbar Snackbar @inject ModalService ModalService
<MudGrid> <Space Direction="DirectionVHType.Vertical" Size="@("large")" Style="width: 100%;">
<MudItem xs="12"> <SpaceItem>
<MudPaper Class="pa-4"> <Card Title="Game Details">
<MudForm @bind-IsValid="@Success" @bind-Errors="@Errors"> <Body>
<MudTextField @bind-Value="Game.Title" Label="Title" For="@(() => Game.Title)" Adornment="Adornment.End" AdornmentIcon="@Icons.Material.Filled.Search" OnAdornmentClick="LookupGameMetadata" /> <Form Model="@Game" Layout="@FormLayout.Vertical">
<MudTextField @bind-Value="Game.SortTitle" Label="Sort Title" For="@(() => Game.SortTitle)" /> <FormItem Label="Title">
<MudTextField @bind-Value="Game.Icon" Label="Icon" Adornment="Adornment.End" AdornmentIcon="@Icons.Material.Filled.Folder" OnAdornmentClick="BrowseForIcon" /> <GameMetadataLookup @ref="GameMetadataLookup" OnResultSelected="OnGameLookupResultSelected" />
<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>
<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> <SpaceItem>
@foreach (var developer in Game.Developers) <Card Title="Multiplayer Modes">
{ <Body>
<MudChip>@developer.Name</MudChip> <MultiplayerModeEditor Game="Game" />
} </Body>
</MudChipSet> </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> <SpaceItem>
@foreach (var publisher in Game.Publishers) <Card Title="Archives">
{ <ArchiveUploader Game="Game" />
<MudChip>@publisher.Name</MudChip> </Card>
} </SpaceItem>
</MudChipSet> </Space>
<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;" />
@code { @code {
[Parameter] public Guid Id { get; set; } [Parameter] public Guid Id { get; set; }
bool Success; bool Success;
string[] Errors = { }; 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 { private int KeysAvailable { get {
return Game.Keys.Count(k => return Game.Keys.Count(k =>
@ -219,6 +146,9 @@
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
Game = await GameService.Get(Id); Game = await GameService.Get(Id);
Companies = CompanyService.Get();
Genres = GenreService.Get();
Tags = TagService.Get();
} }
private async Task Save() private async Task Save()
@ -227,35 +157,105 @@
{ {
Game = await GameService.Update(Game); Game = await GameService.Update(Game);
Snackbar.Add("Game updated!", Severity.Success); await MessageService.Success("Game updated!");
} }
catch (Exception ex) 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 browserOptions = new ArchiveBrowserOptions()
var result = await dialog.Result;
if (!result.Canceled)
{ {
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(); 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() private async void LookupGameMetadata()
{ {
var parameters = new DialogParameters /*var parameters = new DialogParameters
{ {
["GameTitle"] = Game.Title ["GameTitle"] = Game.Title
}; };
@ -278,12 +278,12 @@
Game.Singleplayer = info.Singleplayer; Game.Singleplayer = info.Singleplayer;
StateHasChanged(); StateHasChanged();
} }*/
} }
private async void UploadArchive() private async void UploadArchive()
{ {
var parameters = new DialogParameters /*var parameters = new DialogParameters
{ {
["GameId"] = Game.Id ["GameId"] = Game.Id
}; };
@ -294,22 +294,22 @@
await GameService.Context.Entry(Game).Collection(nameof(Game.Archives)).LoadAsync(); await GameService.Context.Entry(Game).Collection(nameof(Game.Archives)).LoadAsync();
StateHasChanged(); StateHasChanged();*/
} }
private async void BrowseArchive(Archive archive) private async void BrowseArchive(Archive archive)
{ {
var parameters = new DialogParameters /*var parameters = new DialogParameters
{ {
["ArchiveId"] = archive.Id ["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) private async void DeleteArchive(Archive archive)
{ {
bool? result = await DialogService.ShowMessageBox( /*bool? result = await DialogService.ShowMessageBox(
"Delete Archive?", "Delete Archive?",
"Do you really want to delete this archive? You will not be able to recover it later.", "Do you really want to delete this archive? You will not be able to recover it later.",
"Delete", "Delete",
@ -319,12 +319,12 @@
if (result == true) if (result == true)
await ArchiveService.Delete(archive); await ArchiveService.Delete(archive);
StateHasChanged(); StateHasChanged();*/
} }
private async void EditScript(Script script = null) private async void EditScript(Script script = null)
{ {
if (script == null) /*if (script == null)
script = new Script() script = new Script()
{ {
GameId = Game.Id, GameId = Game.Id,
@ -342,12 +342,12 @@
await GameService.Context.Entry(Game).Collection(nameof(Game.Archives)).LoadAsync(); await GameService.Context.Entry(Game).Collection(nameof(Game.Archives)).LoadAsync();
StateHasChanged(); StateHasChanged();*/
} }
private async void DeleteScript(Script script) private async void DeleteScript(Script script)
{ {
bool? result = await DialogService.ShowMessageBox( /*bool? result = await DialogService.ShowMessageBox(
"Delete Script", "Delete Script",
"Do you really want to delete this script? You will not be able to recover it later.", "Do you really want to delete this script? You will not be able to recover it later.",
"Delete", "Delete",
@ -357,6 +357,6 @@
if (result == true) if (result == true)
await ScriptService.Delete(script); await ScriptService.Delete(script);
StateHasChanged(); StateHasChanged();*/
} }
} }

View file

@ -1,56 +1,50 @@
@page "/Games" @page "/Games"
@attribute [Authorize]
@inject GameService GameService @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"> <Table TItem="Game" DataSource="@Games">
<ToolBarContent> <Column TData="string">
<MudText Typo="Typo.h6">Games</MudText> <Image Src="@GetIcon(context)" Height="32" Width="32" Preview="false"></Image>
<MudSpacer /> </Column>
<MudTextField @bind-Value="Search" Placeholder="Search" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"></MudTextField> <PropertyColumn Property="g => g.Title" Sortable />
</ToolBarContent> <PropertyColumn Property="g => g.SortTitle" Sortable />
<PropertyColumn Property="g => g.ReleasedOn" Format="MM/dd/yyyy" Sortable />
<HeaderContent> <PropertyColumn Property="g => g.CreatedOn" Format="MM/dd/yyyy hh:mm" Sortable />
<MudTh></MudTh> <PropertyColumn Property="g => g.CreatedBy" Sortable>
<MudTh>Title</MudTh> @context.CreatedBy?.UserName
<MudTh>Sort Title</MudTh> </PropertyColumn>
<MudTh>Released</MudTh> <PropertyColumn Property="g => g.UpdatedOn" Format="MM/dd/yyyy hh:mm" Sortable />
<MudTh>Created</MudTh> <PropertyColumn Property="g => g.UpdatedBy" Sortable>
<MudTh>Created By</MudTh> @context.UpdatedBy?.UserName
<MudTh>Updated</MudTh> </PropertyColumn>
<MudTh>Updated By</MudTh> <ActionColumn Title="">
</HeaderContent> <Space>
<SpaceItem>
<RowTemplate> <Button OnClick="() => Edit(context)">Edit</Button>
<MudTd> </SpaceItem>
<MudImage Src="@GetIcon(context)" Height="32" Width="32" /> </Space>
</MudTd> </ActionColumn>
<MudTd DataLabel="Title">@context.Title</MudTd> </Table>
<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>
@code { @code {
private ICollection<Game> Games { get; set; } IEnumerable<Game> Games { get; set; } = new List<Game>();
private string Search { get; set; } private string Search { get; set; }
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
Games = GameService.Get().OrderBy(g => String.IsNullOrWhiteSpace(g.SortTitle) ? g.Title : g.SortTitle).ToList(); Games = GameService.Get().OrderBy(g => String.IsNullOrWhiteSpace(g.SortTitle) ? g.Title : g.SortTitle).ToList();
StateHasChanged();
} }
private string GetIcon(Game game) private string GetIcon(Game game)
{ {
return $"/api/Games/{game.Id}/Icon.png"; 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; @using LANCommander.Models;
@layout SettingsLayout @layout SettingsLayout
@inject SettingService SettingService @inject SettingService SettingService
@inject ISnackbar Snackbar @inject IMessageService MessageService
<MudCard Elevation="0"> <Card Title="General">
<MudCardHeader> <Body>
<CardHeaderContent> <Text>IGDB Credentials</Text>
<MudText Typo="Typo.h6">General</MudText> <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>
</CardHeaderContent> <Form Model="Settings">
</MudCardHeader> <FormItem Label="Client ID">
<Input @bind-Value="context.IGDBClientId" />
<MudCardContent> </FormItem>
<MudText Typo="Typo.subtitle1">IGDB Credentials</MudText> <FormItem Label="Client Secret">
<MudForm Model="@Settings" @ref="Form"> <InputPassword @bind-Value="context.IGDBClientSecret" />
<MudTextField @bind-Value="Settings.IGDBClientId" For="@(() => Settings.IGDBClientId)" Immediate="true" Label="Client ID" /> </FormItem>
<MudTextField @bind-Value="Settings.IGDBClientSecret" <FormItem>
For="@(() => Settings.IGDBClientSecret)" <Button OnClick="Save" Icon="@IconType.Fill.Save">Save</Button>
Immediate="true" </FormItem>
Label="Client Secret" </Form>
InputType="@IGDBClientSecretInputType" </Body>
Adornment="Adornment.End" </Card>
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>
@code { @code {
private MudForm Form;
private LANCommanderSettings Settings; private LANCommanderSettings Settings;
private bool ShowIGDBClientSecret = false;
private InputType IGDBClientSecretInputType = InputType.Password;
private string IGDBClientSecretInputIcon = Icons.Material.Filled.Visibility;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
Settings = SettingService.GetSettings(); 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() private void Save()
{ {
try try
{ {
SettingService.SaveSettings(Settings); SettingService.SaveSettings(Settings);
Snackbar.Add("Settings saved!", Severity.Success); MessageService.Success("Settings saved!");
} }
catch catch
{ {
Snackbar.Add("An unknown error occurred", Severity.Error); MessageService.Error("An unknown error occurred.");
} }
} }
} }

View file

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

View file

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

View file

@ -11,6 +11,7 @@
<base href="~/" /> <base href="~/" />
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" /> <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/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
<link href="_content/AntDesign/css/ant-design-blazor.css" rel="stylesheet" />
<link href="~/css/site.css" rel="stylesheet" /> <link href="~/css/site.css" rel="stylesheet" />
</head> </head>
<body> <body>
@ -21,6 +22,7 @@
</div> </div>
<script src="~/_content/MudBlazor/MudBlazor.min.js"></script> <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="~/_framework/blazor.server.js"></script>
<script src="~/_content/BlazorMonaco/jsInterop.js"></script> <script src="~/_content/BlazorMonaco/jsInterop.js"></script>
<script src="~/_content/BlazorMonaco/lib/monaco-editor/min/vs/loader.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.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(); builder.Services.AddSwaggerGen();
builder.Services.AddAntDesign();
builder.Services.AddMudServices(config => builder.Services.AddMudServices(config =>
{ {
config.SnackbarConfiguration.PositionClass = Defaults.Classes.Position.BottomLeft; config.SnackbarConfiguration.PositionClass = Defaults.Classes.Position.BottomLeft;

View file

@ -1,59 +1,17 @@
@inherits LayoutComponentBase @inherits LayoutComponentBase
<MudThemeProvider Theme="DarkTheme" /> <Layout Class="layout">
<MudDialogProvider /> <Header>
<MudSnackbarProvider /> <div class="logo" />
<MudLayout Class="mb-16"> <Menu Theme="MenuTheme.Dark" Mode="MenuMode.Horizontal">
<MudAppBar> <MenuItem Key="1">Dashboard</MenuItem>
<MudIconButton Icon="@Icons.Material.Filled.Menu" Color="Color.Inherit" Edge="Edge.Start" OnClick="@((e) => DrawerToggle())" /> <MenuItem Key="2">Games</MenuItem>
LANCommander <MenuItem Key="3">Settings</MenuItem>
</MudAppBar> </Menu>
</Header>
<MudDrawer @bind-Open="@_drawerOpen"> <Content Style="padding: 24px;">
<NavMenu /> @Body
</MudDrawer> </Content>
</Layout>
<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;
}
}

View file

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

View file

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

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);
}