Partial implementation of game edit page using Blazor components and Razor Pages

This commit is contained in:
Pat Hartl 2023-02-04 12:18:10 -06:00
parent c564c8f62a
commit efda7bf03d
14 changed files with 495 additions and 11 deletions

10
LANCommander/App.razor Normal file
View file

@ -0,0 +1,10 @@
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>

View file

@ -3,3 +3,4 @@
@using LANCommander.Areas.Identity.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using LANCommander.Data.Models
@using LANCommander.Components

View file

@ -0,0 +1,198 @@
@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>
<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>
@code {
[CascadingParameter] MudDialogInstance MudDialog { get; set; }
[Parameter] public string GameTitle { get; set; }
private IEnumerable<Game> Results { get; set; }
private PCGamingWikiClient PCGamingWikiClient { get; set; }
protected override async Task OnInitializedAsync()
{
PCGamingWikiClient = new PCGamingWikiClient();
await SearchForGame(GameTitle);
}
private void Cancel()
{
MudDialog.Cancel();
}
public async Task SearchForGame(string title)
{
var results = await IGDBService.Search(GameTitle, "involved_companies.*", "involved_companies.company.*");
if (results == null)
Results = new List<Game>();
else
{
Results = results.Select(r =>
{
var result = new Game()
{
IGDBId = r.Id.GetValueOrDefault(),
Title = r.Name,
ReleasedOn = r.FirstReleaseDate.GetValueOrDefault().UtcDateTime,
Developers = new List<Company>()
};
if (r.InvolvedCompanies != null && r.InvolvedCompanies.Values != null)
{
result.Developers = r.InvolvedCompanies.Values.Where(c => c.Developer.HasValue && c.Developer.GetValueOrDefault() && c.Company != null && c.Company.Value != null).Select(c => new Company()
{
Name = c.Company.Value.Name
}).ToList();
}
return result;
});
}
}
private async Task SelectGame(Game game)
{
Results = null;
var result = await IGDBService.Get(game.IGDBId.GetValueOrDefault(), "genres.*", "game_modes.*", "multiplayer_modes.*", "release_dates.*", "platforms.*", "keywords.*", "involved_companies.*", "involved_companies.company.*", "cover.*");
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>();
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));
}
private async Task<ICollection<MultiplayerMode>> GetMultiplayerModes(string gameTitle)
{
var multiplayerModes = new List<MultiplayerMode>();
var playerCounts = await PCGamingWikiClient.GetMultiplayerPlayerCounts(gameTitle);
if (playerCounts != null)
{
foreach (var playerCount in playerCounts)
{
MultiplayerType type;
switch (playerCount.Key)
{
case "Local Play":
type = MultiplayerType.Local;
break;
case "LAN Play":
type = MultiplayerType.Lan;
break;
case "Online Play":
type = MultiplayerType.Online;
break;
default:
continue;
}
multiplayerModes.Add(new MultiplayerMode()
{
Type = type,
MaxPlayers = playerCount.Value,
MinPlayers = 2
});
}
}
return multiplayerModes;
}
}

View file

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

View file

@ -0,0 +1,18 @@
using Castle.DynamicProxy.Generators.Emitters.SimpleAST;
using System.ComponentModel.DataAnnotations;
using System.Linq.Expressions;
using System.Reflection;
namespace LANCommander.Helpers
{
public static class DisplayName
{
public static string For<T>(Expression<Func<T>> accessor)
{
var expression = (MemberExpression)accessor.Body;
var value = expression.Member.GetCustomAttribute(typeof(DisplayAttribute)) as DisplayAttribute;
return value?.Name ?? expression.Member.Name;
}
}
}

View file

@ -38,12 +38,14 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="6.0.11" />
<PackageReference Include="MudBlazor" Version="6.1.8" />
<PackageReference Include="rix0rrr.BeaconLib" Version="1.0.2" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.3.0" />
<PackageReference Include="YamlDotNet" Version="12.3.1" />
</ItemGroup>
<ItemGroup>
<Folder Include="Areas\Archive\Pages\" />
<Folder Include="bin\Debug\net6.0\" />
<Folder Include="Data\Migrations\" />
<Folder Include="Migrations\" />

View file

@ -0,0 +1,23 @@
@page "/Games/{id:guid}/Browse"
@inject GameService GameService
<Card Title="Archive Browser" SubTitle="@Game.Title">
<Table>
<ArchiveBrowser ArchiveId="@Archive.Id" />
</Table>
</Card>
@code {
[Parameter] public Guid Id { get; set; }
private Game Game { get; set; }
private Archive Archive { get; set; }
protected override async Task OnInitializedAsync()
{
Game = await GameService.Get(Id);
if (Game.Archives != null && Game.Archives.Count > 0)
Archive = Game.Archives.OrderByDescending(a => a.CreatedOn).First();
}
}

View file

@ -0,0 +1,103 @@
@page "/Games/{id:guid}/Edit"
@inject GameService GameService
@inject IDialogService DialogService
<MudGrid>
<MudItem xs="12">
<MudPaper Class="pa-4">
<MudForm>
<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>
<MudChipSet>
@foreach (var developer in Game.Developers)
{
<MudChip>@developer.Name</MudChip>
}
</MudChipSet>
<MudChipSet>
@foreach (var publisher in Game.Publishers)
{
<MudChip>@publisher.Name</MudChip>
}
</MudChipSet>
<MudChipSet>
@foreach (var genre in Game.Genres)
{
<MudChip>@genre.Name</MudChip>
}
</MudChipSet>
<MudChipSet>
@foreach (var tags in Game.Tags)
{
<MudChip>@tags.Name</MudChip>
}
</MudChipSet>
<div class="d-flex align-center justify-space-between">
<MudButton Variant="Variant.Filled" Color="Color.Primary" Disabled="@(!Success)">Save</MudButton>
</div>
</MudForm>
</MudPaper>
</MudItem>
</MudGrid>
@code {
[Parameter] public Guid Id { get; set; }
bool Success;
string[] Errors = { };
MudForm Form;
private Game Game { get; set; }
protected override async Task OnInitializedAsync()
{
Game = await GameService.Get(Id);
}
private void Save()
{
}
private void BrowseForIcon()
{
}
private async void LookupGameMetadata()
{
var parameters = new DialogParameters
{
["GameTitle"] = Game.Title
};
var dialog = await DialogService.ShowAsync<GameMetadataLookup>("Game Lookup", parameters);
var result = await dialog.Result;
if (!result.Canceled)
{
var info = result.Data as Game;
Game.Title = info.Title;
Game.Description = info.Description;
Game.ReleasedOn = info.ReleasedOn;
Game.MultiplayerModes = info.MultiplayerModes;
Game.Developers = info.Developers;
Game.Publishers = info.Publishers;
Game.Genres = info.Genres;
Game.Tags = info.Tags;
Game.Singleplayer = info.Singleplayer;
StateHasChanged();
}
}
}

View file

@ -0,0 +1,51 @@
@page "/Games"
@inject GameService GameService
<MudTable Items="@Games" 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="@($"~/Icon/{context.Id}.png")" />
</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>
<MudButton Href="@($"/Games/{context.Id}/Edit")">Edit</MudButton>
</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager />
</PagerContent>
</MudTable>
@code {
private ICollection<Game> Games { get; set; }
private string Search { get; set; }
protected override async Task OnInitializedAsync()
{
Games = GameService.Get();
}
}

View file

@ -0,0 +1,8 @@
@page "/"
@namespace LANCommander.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
Layout = "_Layout";
}
<component type="typeof(App)" render-mode="ServerPrerendered" />

View file

@ -0,0 +1,25 @@
@using Microsoft.AspNetCore.Components.Web
@using LANCommander.Components
@namespace LANCommander.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<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" />
</head>
<body>
<div class="page-wrapper">
@RenderBody()
</div>
<script src="~/_content/MudBlazor/MudBlazor.min.js"></script>
<script src="~/_framework/blazor.server.js"></script>
</body>
</html>

View file

@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using MudBlazor.Services;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
@ -17,6 +18,9 @@ ConfigurationManager configuration = builder.Configuration;
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
var settings = SettingService.GetSettings();
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.WebHost.ConfigureKestrel(options =>
{
// Configure as HTTP only
@ -65,7 +69,8 @@ builder.Services.AddControllersWithViews().AddJsonOptions(x =>
{
x.JsonSerializerOptions.ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles;
});
builder.Services.AddServerSideBlazor();
builder.Services.AddMudServices();
builder.Services.AddScoped<SettingService>();
builder.Services.AddScoped<ArchiveService>();
@ -81,6 +86,8 @@ builder.Services.AddScoped<IGDBService>();
if (settings.Beacon)
builder.Services.AddHostedService<BeaconService>();
builder.WebHost.UseStaticWebAssets();
var app = builder.Build();
// Configure the HTTP request pipeline.
@ -104,16 +111,8 @@ app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
endpoints.MapBlazorHub();
});
app.MapRazorPages();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");
if (!Directory.Exists("Upload"))
Directory.CreateDirectory("Upload");

View file

@ -0,0 +1,31 @@
@inherits LayoutComponentBase
<MudThemeProvider />
<MudDialogProvider />
<MudSnackbarProvider />
<MudLayout>
<MudAppBar>
<MudIconButton Icon="@Icons.Material.Filled.Menu" Color="Color.Inherit" Edge="Edge.Start" OnClick="@((e) => DrawerToggle())" />
LANCommander
</MudAppBar>
<MudDrawer @bind-Open="@_drawerOpen">
<NavMenu />
</MudDrawer>
<MudMainContent>
<MudContainer MaxWidth="MaxWidth.Large">
@Body
</MudContainer>
</MudMainContent>
</MudLayout>
@code {
bool _drawerOpen = true;
void DrawerToggle()
{
_drawerOpen = !_drawerOpen;
}
}

View file

@ -0,0 +1,11 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.JSInterop
@using MudBlazor
@using LANCommander.Components
@using LANCommander.Shared
@using LANCommander.Services
@using LANCommander.Data.Models