Merge branch 'blazor'

This commit is contained in:
Pat Hartl 2023-03-03 19:42:33 -06:00
commit f6dbd013be
104 changed files with 2892 additions and 4388 deletions

BIN
Docs/AddGame.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

BIN
Docs/ArchiveUploading.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

BIN
Docs/ChangeKey.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 647 KiB

BIN
Docs/Dashboard.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

BIN
Docs/EditingScript.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

BIN
Docs/GamesList.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

BIN
Docs/InstallingGames.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

BIN
Docs/KeyManagement.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

27
LANCommander/App.razor Normal file
View file

@ -0,0 +1,27 @@
@using Microsoft.AspNetCore.Components.Authorization
<AntContainer />
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
<NotAuthorized>
@if (context.User.Identity.IsAuthenticated == false)
{
<RedirectToLogin />
}
else
{
<audio autoplay>
<source src="~/static/access-denied.mp3" type="audio/mp3" />
</audio>
}
</NotAuthorized>
</AuthorizeRouteView>
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>

View file

@ -7,46 +7,84 @@
ViewData["Title"] = "First Time Setup";
}
<div class="page page-center">
<form asp-route-returnUrl="@Model.ReturnUrl" class="container-tight py-4">
<div class="text-center mb-4">
<h2>LANCommander</h2>
</div>
<div class="card card-md">
<div class="card-body text-center py-4 p-sm-5">
<h1>Welcome to LANCommander!</h1>
<p class="text-muted">LANCommander is your one stop shop for distributing games on your LAN. Start your adventure with LANCommander and take control of your local multiplayer gaming!</p>
</div>
<div class="hr-text hr-text-center hr-text-spaceless">registration</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">Register your admin account</label>
<input asp-for="Input.UserName" type="text" class="form-control ps-1" autocomplete="off" placeholder="Username" />
<div class="form-hint">For first-time setup, an admin user is required. This user will be able to manage all aspects of the application.</div>
</div>
<div class="mb-3">
<label asp-for="Input.Password" class="form-label"></label>
<input asp-for="Input.Password" type="password" class="form-control ps-1" autocomplete="new-password" />
<span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Input.ConfirmPassword" class="form-label"></label>
<input asp-for="Input.ConfirmPassword" type="password" class="form-control ps-1" autocomplete="new-password" />
<span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
</div>
</div>
<div class="ant-row ant-row-middle ant-row-space-around" style="position: absolute; top:0; left: 0; right: 0; bottom: 0;">
<div class="ant-col ant-col-10">
<div style="text-align: center; margin-bottom: 24px;">
<img src="~/static/logo.svg" />
</div>
<div class="row align-items-center mt-3">
<div class="col">
<div class="btn-list justify-content-end">
<button type="submit" class="btn btn-primary">Continue</button>
<div class="ant-card ant-card-bordered">
<div class="ant-card-head">
<div class="ant-card-head-wrapper">
<div class="ant-card-head-title">First Time Setup</div>
</div>
</div>
</div>
</form>
</div>
<form asp-route-returnUrl="@Model.ReturnUrl" class="ant-card-body" autocomplete="off">
<div class="ant-form ant-form-vertical">
<div class="ant-form-item">
<p>LANCommander is your one stop shop for distributing games on your LAN. Start your adventure with LANCommander and take control of your local multiplayer gaming!</p>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}
<div class="ant-form-item">
<div class="ant-form-item-row ant-row">
<div class="ant-form-item-label ant-col">
<label class="form-label">Register your admin account</label>
</div>
<div class="ant-form-item-control ant-col">
<div class="ant-form-item-control-input">
<div class="ant-form-item-control-input-content">
<input asp-for="Input.UserName" class="ant-input" autocomplete="username" aria-required="true" placeholder="Username" />
</div>
</div>
</div>
</div>
</div>
<div class="ant-form-item">
<div class="ant-form-item-row ant-row">
<div class="ant-form-item-label ant-col">
<label asp-for="Input.Password" class="form-label"></label>
</div>
<div class="ant-form-item-control ant-col">
<div class="ant-form-item-control-input">
<div class="ant-form-item-control-input-content">
<input asp-for="Input.Password" class="ant-input" autocomplete="current-password" aria-required="true" />
</div>
</div>
</div>
</div>
</div>
<div class="ant-form-item">
<div class="ant-form-item-row ant-row">
<div class="ant-form-item-label ant-col">
<label asp-for="Input.ConfirmPassword" class="form-label"></label>
</div>
<div class="ant-form-item-control ant-col">
<div class="ant-form-item-control-input">
<div class="ant-form-item-control-input-content">
<input asp-for="Input.ConfirmPassword" class="ant-input" autocomplete="new-password" aria-required="true" />
</div>
</div>
</div>
</div>
</div>
<div class="ant-form-item" style="margin-bottom: 0;">
<div class="ant-form-item-row ant-row">
<button type="submit" class="ant-btn ant-btn-primary ant-btn-block">Continue</button>
</div>
</div>
</div>
</form>
</div>
<div style="text-align: center; margin-top: 16px;">
Don't have account yet? <a asp-page="./Register" asp-route-returnUrl="@Model.ReturnUrl" tabindex="-1">Register</a>
</div>
</div>
</div>

View file

@ -6,47 +6,84 @@
ViewData["Title"] = "Log in";
}
<div class="page page-center">
<div class="container-tight py-4">
<div class="text-center mb-4">
<div class="ant-row ant-row-middle ant-row-space-around" style="position: absolute; top:0; left: 0; right: 0; bottom: 0;">
<div class="ant-col ant-col-10">
<div style="text-align: center; margin-bottom: 24px;">
<img src="~/static/logo.svg" />
</div>
<form id="account" method="post" class="card card-md" autocomplete="off">
<div class="card-body">
<h2 class="card-title text-center mb-4">Login to your account</h2>
<div class="mb-3">
<label asp-for="Input.UserName" class="form-label"></label>
<input asp-for="Input.UserName" class="form-control" autocomplete="username" aria-required="true" />
<span asp-validation-for="Input.UserName" class="text-danger"></span>
</div>
<div class="mb-2">
<label asp-for="Input.Password" class="form-label"></label>
<input asp-for="Input.Password" class="form-control" autocomplete="current-password" aria-required="true" />
<span asp-validation-for="Input.Password" class="text-danger"></span>
</div>
<div class="mb-2">
<label asp-for="Input.RememberMe" class="form-check">
<input class="form-check-input" asp-for="Input.RememberMe" />
<span class="form-check-label">@Html.DisplayNameFor(m => m.Input.RememberMe)</span>
</label>
</div>
<div class="form-footer">
<button id="login-submit" type="submit" class="btn btn-primary w-100">Sign in</button>
<div class="ant-card ant-card-bordered">
<div class="ant-card-head">
<div class="ant-card-head-wrapper">
<div class="ant-card-head-title">Login</div>
</div>
</div>
</form>
<form id="account" method="post" class="ant-card-body" autocomplete="off">
<div class="ant-form ant-form-vertical">
<div class="ant-form-item">
<div class="ant-form-item-row ant-row">
<div class="ant-form-item-label ant-col">
<label asp-for="Input.UserName" class="form-label"></label>
</div>
<div class="text-center text-muted mt-3">
Don't have account yet? <a asp-page="./Register" asp-route-returnUrl="@Model.ReturnUrl" tabindex="-1">Register</a>
<div class="ant-form-item-control ant-col">
<div class="ant-form-item-control-input">
<div class="ant-form-item-control-input-content">
<input asp-for="Input.UserName" class="ant-input" autocomplete="username" aria-required="true" />
</div>
</div>
</div>
</div>
</div>
<div class="ant-form-item">
<div class="ant-form-item-row ant-row">
<div class="ant-form-item-label ant-col">
<label asp-for="Input.Password" class="form-label"></label>
</div>
<div class="ant-form-item-control ant-col">
<div class="ant-form-item-control-input">
<div class="ant-form-item-control-input-content">
<input asp-for="Input.Password" class="ant-input" autocomplete="current-password" aria-required="true" />
</div>
</div>
</div>
</div>
</div>
<div class="ant-form-item">
<div class="ant-form-item-row ant-row">
<div class="ant-form-item-control ant-col">
<div class="ant-form-item-control-input">
<div class="ant-form-item-control-input-content">
<label class="ant-checkbox-wrapper">
<span class="ant-checkbox">
<input class="ant-checkbox-input" asp-for="Input.RememberMe" />
<span class="ant-checkbox-inner"></span>
</span>
<span>
@Html.DisplayNameFor(m => m.Input.RememberMe)
</span>
</label>
</div>
</div>
</div>
</div>
</div>
<div class="ant-form-item" style="margin-bottom: 0;">
<div class="ant-form-item-row ant-row">
<button id="login-submit" type="submit" class="ant-btn ant-btn-primary ant-btn-block">Sign in</button>
</div>
</div>
</div>
</form>
</div>
<div style="text-align: center; margin-top: 16px;">
Don't have account yet? <a asp-page="./Register" asp-route-returnUrl="@Model.ReturnUrl" tabindex="-1">Register</a>
</div>
</div>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}
</div>

View file

@ -5,46 +5,80 @@
ViewData["Title"] = "Register";
}
<div class="page page-center">
<div class="container-tight py-4">
<div class="text-center mb-4">
<div class="ant-row ant-row-middle ant-row-space-around" style="position: absolute; top:0; left: 0; right: 0; bottom: 0;">
<div class="ant-col ant-col-10">
<div style="text-align: center; margin-bottom: 24px;">
<img src="~/static/logo.svg" />
</div>
<form id="registerForm" method="post" class="card card-md" autocomplete="off">
<div class="card-body">
<h2 class="card-title text-center mb-4">Create a new account</h2>
<div class="mb-3">
<label asp-for="Input.UserName" class="form-label"></label>
<input asp-for="Input.UserName" class="form-control" autocomplete="username" aria-required="true" />
<span asp-validation-for="Input.UserName" class="text-danger"></span>
</div>
<div class="mb-2">
<label asp-for="Input.Password" class="form-label"></label>
<input asp-for="Input.Password" class="form-control" autocomplete="current-password" aria-required="true" />
<span asp-validation-for="Input.Password" class="text-danger"></span>
</div>
<div class="mb-2">
<label asp-for="Input.ConfirmPassword" class="form-label"></label>
<input asp-for="Input.ConfirmPassword" class="form-control" autocomplete="new-password" aria-required="true" />
<span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
</div>
<div class="form-footer">
<button id="register-submit" type="submit" class="btn btn-primary w-100">Register</button>
<div class="ant-card ant-card-bordered">
<div class="ant-card-head">
<div class="ant-card-head-wrapper">
<div class="ant-card-head-title">Create An Account</div>
</div>
</div>
</form>
<form id="registerForm" method="post" class="ant-card-body" autocomplete="off">
<div class="ant-form ant-form-vertical">
<div class="ant-form-item">
<div class="ant-form-item-row ant-row">
<div class="ant-form-item-label ant-col">
<label asp-for="Input.UserName" class="form-label"></label>
</div>
<div class="text-center text-muted mt-3">
Already have an account? <a asp-page="./Login" asp-route-returnUrl="@Model.ReturnUrl" tabindex="-1">Login</a>
<div class="ant-form-item-control ant-col">
<div class="ant-form-item-control-input">
<div class="ant-form-item-control-input-content">
<input asp-for="Input.UserName" class="ant-input" autocomplete="username" aria-required="true" />
</div>
</div>
</div>
</div>
</div>
<div class="ant-form-item">
<div class="ant-form-item-row ant-row">
<div class="ant-form-item-label ant-col">
<label asp-for="Input.Password" class="form-label"></label>
</div>
<div class="ant-form-item-control ant-col">
<div class="ant-form-item-control-input">
<div class="ant-form-item-control-input-content">
<input asp-for="Input.Password" class="ant-input" autocomplete="current-password" aria-required="true" />
</div>
</div>
</div>
</div>
</div>
<div class="ant-form-item">
<div class="ant-form-item-row ant-row">
<div class="ant-form-item-label ant-col">
<label asp-for="Input.ConfirmPassword" class="form-label"></label>
</div>
<div class="ant-form-item-control ant-col">
<div class="ant-form-item-control-input">
<div class="ant-form-item-control-input-content">
<input asp-for="Input.ConfirmPassword" class="ant-input" autocomplete="new-password" aria-required="true" />
</div>
</div>
</div>
</div>
</div>
<div class="ant-form-item" style="margin-bottom: 0;">
<div class="ant-form-item-row ant-row">
<button id="register-submit" type="submit" class="ant-btn ant-btn-primary ant-btn-block">Register</button>
</div>
</div>
</div>
</form>
</div>
<div style="text-align: center; margin-top: 16px;">
Already have an account? <a asp-page="./Login" asp-route-returnUrl="@Model.ReturnUrl" tabindex="-1">Login</a>
</div>
</div>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}
</div>

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

@ -1,141 +1,153 @@
@using LANCommander.Data.Models
@using LANCommander.Extensions
@using LANCommander.Models;
@using System.IO.Compression;
@inject ModalService ModalService
@{
int i = 0;
}
<div class="table-responsive">
<table class="table mb-0">
<thead>
<tr>
<th>Name</th>
<th>Path</th>
<th>Arguments</th>
<th>Working Dir</th>
<th>Primary</th>
<th></th>
</tr>
</thead>
<Space Direction="DirectionVHType.Vertical" Size="@("large")" Style="width: 100%">
<SpaceItem>
<Table TItem="Data.Models.Action" DataSource="@OrderedActions" HidePagination="true" Style="border: 1px solid #f0f0f0">
<PropertyColumn Property="a => a.Name">
<Input Type="text" @bind-Value="context.Name" />
</PropertyColumn>
<PropertyColumn Property="a => a.Path">
<Space Style="display: flex">
<SpaceItem Style="flex-grow: 1">
<Input Type="text" @bind-Value="context.Path" />
</SpaceItem>
<SpaceItem>
<Button OnClick="() => BrowseForActionPath(context)" Type="@ButtonType.Primary" Icon="@IconType.Outline.FolderOpen" />
</SpaceItem>
</Space>
</PropertyColumn>
<PropertyColumn Property="a => a.Arguments">
<Input Type="text" @bind-Value="context.Arguments" />
</PropertyColumn>
<PropertyColumn Property="a => a.WorkingDirectory" Title="Working Dir">
<Input Type="text" @bind-Value="context.WorkingDirectory" />
</PropertyColumn>
<PropertyColumn Property="a => a.PrimaryAction" Title="Primary" Style="text-align: center">
<Checkbox @bind-Checked="context.PrimaryAction" />
</PropertyColumn>
<ActionColumn>
<Space Style="display: flex; justify-content: end">
<SpaceItem>
<Button OnClick="() => MoveUp(context)" Icon="@IconType.Outline.Up" Type="@ButtonType.Text" />
<Button OnClick="() => MoveDown(context)" Icon="@IconType.Outline.Down" Type="@ButtonType.Text" />
<tbody>
@if (Actions == null || Actions.Count == 0)
{
<tr><td colspan="6">Actions are used to start the game or launch other executables. It is recommended to have at least one action to launch the game.</td></tr>
}
<Popconfirm OnConfirm="() => RemoveAction(context)" Title="Are you sure you want to remove this action?">
<Button Icon="@IconType.Outline.Close" Type="@ButtonType.Text" Danger />
</Popconfirm>
</SpaceItem>
</Space>
</ActionColumn>
</Table>
</SpaceItem>
@foreach (var action in Actions.OrderBy(a => a.SortOrder))
{
var index = i;
<tr>
<td><input @bind="action.Name" name="Game.Actions[@i].Name" class="form-control" placeholder="Play" /></td>
<td><input @bind="action.Path" name="Game.Actions[@i].Path" class="form-control" placeholder="Game.exe" /></td>
<td><input @bind="action.Arguments" name="Game.Actions[@i].Arguments" class="form-control" placeholder="Launch Arguments" /></td>
<td><input @bind="action.WorkingDirectory" name="Game.Actions[@i].WorkingDirectory" class="form-control" placeholder="Working Directory" /></td>
<td class="align-middle">
<div class="form-check form-check-inline mb-0">
<input name="Game.Actions[@i].PrimaryAction" class="form-check-input" type="checkbox" checked="@action.PrimaryAction" value="true" />
</div>
</td>
<td>
<input name="Game.Actions[@i].Id" type="hidden" value="@action.Id" />
<input name="Game.Actions[@i].GameId" type="hidden" value="@GameId" />
<input name="Game.Actions[@i].SortOrder" type="hidden" value="@i" />
<div class="btn-list flex-nowrap justify-content-end">
<button class="btn btn-ghost-secondary btn-icon" @onclick="() => MoveUp(index)" type="button">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-chevron-up" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<polyline points="6 15 12 9 18 15"></polyline>
</svg>
</button>
<button class="btn btn-ghost-secondary btn-icon" @onclick="() => MoveDown(index)" type="button">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-chevron-down" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</button>
<button class="btn btn-ghost-danger btn-icon" @onclick="() => RemoveAction(index)" type="button">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-x" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
</td>
</tr>
i++;
}
<tr>
<td colspan="6">
<div class="btn-list flex-nowrap justify-content-end">
<button class="btn btn-ghost-primary" @onclick="AddAction" type="button">Add Action</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<SpaceItem>
<GridRow Justify="end">
<GridCol>
<Button OnClick="AddAction" Type="@ButtonType.Primary">Add Action</Button>
</GridCol>
</GridRow>
</SpaceItem>
</Space>
@code {
[Parameter] public List<Data.Models.Action> Actions { get; set; }
[Parameter] public Guid GameId { get; set; }
[Parameter] public Game Game { get; set; }
protected override void OnInitialized()
private List<Data.Models.Action> OrderedActions { get; set; }
protected override async Task OnInitializedAsync()
{
Actions = Actions.OrderBy(a => a.SortOrder).ToList();
if (Game.Actions == null)
Game.Actions = new List<Data.Models.Action>();
FixSortOrders();
base.OnInitialized();
OrderedActions = Game.Actions.OrderBy(a => a.SortOrder).ToList();
FixSortOrder();
}
private void AddAction()
private async Task AddAction()
{
if (Actions == null)
Actions = new List<Data.Models.Action>();
if (OrderedActions == null)
OrderedActions = new List<Data.Models.Action>();
Actions.Add(new Data.Models.Action()
OrderedActions.Add(new Data.Models.Action()
{
PrimaryAction = Actions.Count == 0,
SortOrder = Actions.Count
PrimaryAction = OrderedActions.Count == 0,
SortOrder = OrderedActions.Count
});
}
private void RemoveAction(int index)
private async Task RemoveAction(Data.Models.Action action)
{
Actions.Remove(Actions.ElementAt(index));
FixSortOrders();
OrderedActions.Remove(action);
}
private void MoveUp(int index)
private async Task MoveUp(Data.Models.Action action)
{
if (index == 0)
return;
if (action.SortOrder > 0)
OrderedActions.Move(action, action.SortOrder - 1);
Actions.Move(Actions.ElementAt(index), index - 1);
FixSortOrders();
FixSortOrder();
}
private void MoveDown(int index)
private async Task MoveDown(Data.Models.Action action)
{
if (index == Actions.Count - 1)
return;
if (action.SortOrder < OrderedActions.Count + 1)
OrderedActions.Move(action, action.SortOrder + 1);
Actions.Move(Actions.ElementAt(index), index + 1);
FixSortOrders();
FixSortOrder();
}
private void FixSortOrders() {
for (int i = 0; i < Actions.Count; i++)
private async void BrowseForActionPath(Data.Models.Action action)
{
var modalOptions = new ModalOptions()
{
Title = "Choose Action Executable",
Maximizable = false,
DefaultMaximized = true,
Closable = true,
OkText = "Select File"
};
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) =>
{
Actions.ElementAt(i).SortOrder = i;
action.Path = results.FirstOrDefault().FullName;
var parts = action.Path.Split('/');
if (parts.Length > 1)
{
action.Path = parts.Last();
action.WorkingDirectory = "{InstallDir}/" + String.Join('/', parts.Take(parts.Length - 1));
}
StateHasChanged();
return Task.CompletedTask;
};
}
private void FixSortOrder()
{
int i = 0;
foreach (var action in OrderedActions)
{
action.SortOrder = i;
i++;
}
Game.Actions = OrderedActions;
}
}

View file

@ -1,151 +1,131 @@
@using ByteSizeLib;
@using AntDesign.TableModels;
@using ByteSizeLib;
@using LANCommander.Services;
@using System.IO.Compression;
@inject ArchiveService ArchiveService;
<div class="card-body">
<div class="row">
<div class="col">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
@if (BreadCrumbs.Length == 0)
{
<li class="breadcrumb-item active">Root</li>
}
else
{
<li class="breadcrumb-item" @onclick="() => GoToRoot()">Root</li>
}
<GridRow Style="position: fixed; height: calc(100vh - 55px - 53px); top: 55px; left: 0; width: 100%">
<GridCol Span="6" Style="height: 100%; overflow-y: scroll; padding: 24px">
<Tree TItem="ArchiveDirectory"
DataSource="Directories"
TitleExpression="x => x.DataItem.Name"
ChildrenExpression="x => x.DataItem.Children"
IsLeafExpression="x => !x.DataItem.HasChildren"
OnClick="(args) => ChangeDirectory(args.Node.DataItem)">
</Tree>
</GridCol>
@for (int i = 0; i < BreadCrumbs.Length; i++)
{
var path = String.Join('/', BreadCrumbs.Take(i + 1)) + '/';
<GridCol Span="18" Style="height: 100%">
<Table
@ref="FileTable"
TItem="ZipArchiveEntry"
DataSource="CurrentPathEntries"
HidePagination="true"
Loading="Entries == null"
RowSelectable="@(x => x.FullName != null && !x.FullName.EndsWith('/'))"
OnRowClick="OnRowClicked"
SelectedRowsChanged="SelectedFilesChanged"
ScrollY="calc(100vh - 55px - 55px - 53px)">
if (i == BreadCrumbs.Length - 1)
{
<li class="breadcrumb-item active">@BreadCrumbs[i]</li>
}
else
{
<li class="breadcrumb-item" @onclick="() => GoToPath(path)">@BreadCrumbs[i]</li>
}
}
</ol>
</nav>
</div>
</div>
</div>
<div class="table-responsive">
<table class="table table-vcenter table-striped table-hover card-table" id="ArchiveBrowser">
<thead>
<tr>
<th></th>
<th>Name</th>
<th>Size</th>
<th>Modified</th>
</tr>
</thead>
<tbody>
@if (CurrentPath != "")
@if (Select)
{
<tr @ondblclick="GoUpLevel">
<td></td>
<td colspan="3">..</td>
</tr>
<Selection Key="@context.FullName" Type="@(Multiple ? "checkbox" : "radio")" Disabled="@(context.FullName != null && context.FullName.EndsWith('/'))" />
}
<Column TData="string" Width="32">
<Icon Type="@GetIcon(context)" Theme="outline" />
</Column>
<PropertyColumn Property="e => e.FullName" Sortable Title="Name">
@GetFileName(context)
</PropertyColumn>
<PropertyColumn Property="e => e.Length" Sortable Title="Size">
@ByteSize.FromBytes(context.Length)
</PropertyColumn>
<PropertyColumn Property="e => e.LastWriteTime" Format="MM/dd/yyyy hh:mm tt" Sortable Title="Modified" />
@foreach (var entry in CurrentPathEntries.OrderBy(e => !e.FullName.EndsWith('/')).ThenBy(e => e.FullName))
{
@if (entry.FullName.EndsWith('/'))
{
<tr @ondblclick="() => GoToPath(entry.FullName)">
<td><i class="ti ti-@GetIcon(entry.FullName.ToLower())"></i></td>
<td>@entry.FullName.Remove(0, CurrentPath.Length)</td>
<td></td>
<td>@entry.LastWriteTime</td>
</tr>
}
else
{
<tr>
<td><i class="ti ti-@GetIcon(entry.FullName.ToLower())"></i></td>
<td>@entry.Name</td>
<td class="text-end">@ByteSize.FromBytes(entry.Length)</td>
<td>@entry.LastWriteTime</td>
</tr>
}
}
</tbody>
</table>
</div>
</Table>
</GridCol>
</GridRow>
<style>
.breadcrumb-item:not(.active) {
cursor: pointer;
.select-file-button {
opacity: 0;
transition: .1s opacity;
}
#ArchiveBrowser tr {
cursor: pointer;
}
#ArchiveBrowser tr td:first-child {
padding: 0;
padding-left: .75rem;
font-size: 1.5rem;
width: .75rem;
.archive-browser tr:hover .select-file-button {
opacity: 1;
}
</style>
@code {
[Parameter] public Guid ArchiveId { get; set; }
[Parameter] public bool Select { get; set; }
[Parameter] public bool Multiple { get; set; }
[Parameter] public IEnumerable<ZipArchiveEntry> SelectedFiles { get; set; }
[Parameter] public EventCallback<IEnumerable<ZipArchiveEntry>> SelectedFilesChanged { get; set; }
ITable? FileTable;
private IEnumerable<ZipArchiveEntry> Entries { get; set; }
private IEnumerable<ZipArchiveEntry> CurrentPathEntries { get; set; }
private string CurrentPath { get; set; }
private string[] BreadCrumbs { get { return CurrentPath.TrimEnd('/').Split('/'); } }
private HashSet<ArchiveDirectory> Directories { get; set; }
private ArchiveDirectory SelectedDirectory { get; set; }
protected override async Task OnInitializedAsync()
{
Entries = await ArchiveService.GetContents(ArchiveId);
Directories = new HashSet<ArchiveDirectory>();
GoToRoot();
}
private void GoToRoot()
{
CurrentPath = "";
CurrentPathEntries = Entries.Where(e => e.FullName.TrimEnd('/').Split('/').Length == 1);
}
private void GoUpLevel()
{
var parts = CurrentPath.TrimEnd('/').Split('/');
if (parts.Length == 1)
GoToRoot();
else
var root = new ArchiveDirectory()
{
GoToPath(String.Join('/', parts.Take(parts.Length - 1)) + "/");
Name = "/",
FullName = "",
IsExpanded = true
};
root.PopulateChildren(Entries);
Directories.Add(root);
ChangeDirectory(root);
}
private void OnRowClicked(RowData<ZipArchiveEntry> row)
{
FileTable.SetSelection(new string[] { row.Data.FullName });
}
private void ChangeDirectory(ArchiveDirectory selectedDirectory)
{
SelectedDirectory = selectedDirectory;
if (SelectedDirectory.FullName == "")
CurrentPathEntries = Entries.Where(e => !e.FullName.TrimEnd('/').Contains('/'));
else
CurrentPathEntries = Entries.Where(e => e.FullName.StartsWith(SelectedDirectory.FullName) && e.FullName != SelectedDirectory.FullName);
}
private string GetFileName(ZipArchiveEntry entry)
{
if (String.IsNullOrWhiteSpace(entry.Name) && entry.Length == 0)
{
return entry.FullName.TrimEnd('/').Split('/').Last();
}
else
return entry.Name;
}
private void GoToPath(string path)
private string GetIcon(ZipArchiveEntry entry)
{
CurrentPath = path;
CurrentPathEntries = Entries.Where(e => e.FullName.StartsWith(CurrentPath) && e.FullName != CurrentPath && e.FullName.Remove(0, path.Length).TrimEnd('/').Split('/').Length == 1);
}
private string GetIcon(string path)
{
switch (Path.GetExtension(path))
switch (Path.GetExtension(entry.FullName))
{
case "":
return "folder";
case ".exe":
return "terminal-2";
return "code";
case ".zip":
case ".rar":
@ -158,7 +138,7 @@
case ".pk3":
case ".pak":
case ".cab":
return "archive";
return "file-zip";
case ".txt":
case ".cfg":
@ -174,7 +154,7 @@
case ".bat":
case ".ps1":
case ".json":
return "file-code";
return "code";
case ".bik":
case ".avi":
@ -186,26 +166,51 @@
case ".mpg":
case ".mpeg":
case ".flv":
return "movie";
return "video-camera";
case ".dll":
return "package";
case ".scm":
return "map";
return "api";
case ".hlp":
return "help";
return "file-unknown";
case ".png":
case ".bmp":
case ".jpeg":
case ".jpg":
case ".gif":
return "photo";
return "file-image";
default:
return "file";
}
}
public class ArchiveDirectory
{
public string Name { get; set; }
public string FullName { get; set; }
public bool IsExpanded { get; set; } = false;
public bool HasChildren => Children != null && Children.Count > 0;
public HashSet<ArchiveDirectory> Children { get; set; } = new HashSet<ArchiveDirectory>();
public void PopulateChildren(IEnumerable<ZipArchiveEntry> entries)
{
var childPaths = entries.Where(e => e.FullName.StartsWith(FullName) && e.FullName.EndsWith('/'));
var directChildren = childPaths.Where(p => p.FullName != FullName && p.FullName.Substring(FullName.Length + 1).TrimEnd('/').Split('/').Length == 1);
foreach (var directChild in directChildren)
{
var child = new ArchiveDirectory()
{
FullName = directChild.FullName,
Name = directChild.FullName.Substring(FullName.Length).TrimEnd('/')
};
child.PopulateChildren(entries);
Children.Add(child);
}
}
}
}

View file

@ -0,0 +1,14 @@
@inherits FeedbackComponent<ArchiveBrowserOptions, IEnumerable<ZipArchiveEntry>>
@using System.IO.Compression;
@using LANCommander.Models;
<ArchiveBrowser ArchiveId="Options.ArchiveId" @bind-SelectedFiles="SelectedFiles" Select="Options.Select" Multiple="Options.Multiple" />
@code {
private IEnumerable<ZipArchiveEntry> SelectedFiles { get; set; }
public override async Task OnFeedbackOkAsync(ModalClosingEventArgs args)
{
await base.OkCancelRefWithResult!.OnOk(SelectedFiles);
}
}

View file

@ -0,0 +1,227 @@
@using System.Net;
@using System.Diagnostics;
@inject HttpClient HttpClient
@inject NavigationManager Navigator
@inject ArchiveService ArchiveService
@inject IMessageService MessageService
<Space Direction="DirectionVHType.Vertical" Style="width: 100%">
<SpaceItem>
<Table TItem="Archive" DataSource="@Game.Archives.OrderByDescending(a => a.CreatedOn)" HidePagination="true">
<PropertyColumn Property="a => a.Version" />
<PropertyColumn Property="a => a.CompressedSize">
@ByteSizeLib.ByteSize.FromBytes(context.CompressedSize)
</PropertyColumn>
<PropertyColumn Property="a => a.CreatedBy">
@context.CreatedBy?.UserName
</PropertyColumn>
<PropertyColumn Property="a => a.CreatedOn" Format="MM/dd/yyyy hh:mm tt" />
<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>
<SpaceItem>
<GridRow Justify="end">
<GridCol>
<Button OnClick="AddArchive" Type="@ButtonType.Primary">Upload Archive</Button>
</GridCol>
</GridRow>
</SpaceItem>
</Space>
@{
RenderFragment Footer =
@<Template>
<Button OnClick="UploadArchive" Disabled="@(File == null || Uploading)" Type="@ButtonType.Primary">Upload</Button>
<Button OnClick="Clear" Disabled="File == null || Uploading" Danger>Clear</Button>
<Button OnClick="Cancel">Cancel</Button>
</Template>;
}
<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>
<FormItem Label="Changelog">
<TextArea @bind-Value="@context.Changelog" MaxLength=500 ShowCount />
</FormItem>
<FormItem>
<InputFile id="FileInput" OnChange="FileSelected" hidden />
<Upload Name="files" FileList="FileList">
<label class="ant-btn" for="FileInput">
<Icon Type="upload" />
Select Archive
</label>
</Upload>
</FormItem>
<FormItem>
<Progress Percent="Progress" />
<Text>@ByteSizeLib.ByteSize.FromBytes(Speed)/s</Text>
</FormItem>
</Form>
</Modal>
@code {
[Parameter] public Game Game { get; set; }
Archive Archive;
IBrowserFile File { get; set; }
List<UploadFileItem> FileList = new List<UploadFileItem>();
bool IsValid = false;
bool ModalVisible = false;
private static string DefaultDragClass = "relative rounded-lg border-2 border-dashed pa-4 mt-4 mud-width-full mud-height-full z-10";
private string DragClass = DefaultDragClass;
const int ChunkSize = 1024 * 1024 * 10;
int Progress = 0;
bool Uploading = false;
double Speed = 0;
Stopwatch Watch;
long WatchBytesTransferred = 0;
protected override async Task OnInitializedAsync()
{
if (Game.Archives == null)
Game.Archives = new List<Archive>();
HttpClient.BaseAddress = new Uri(Navigator.BaseUri);
Archive = new Archive()
{
GameId = Game.Id,
Id = Guid.NewGuid()
};
}
private void AddArchive()
{
Archive = new Archive()
{
GameId = Game.Id,
Id = Guid.NewGuid()
};
ModalVisible = true;
}
private async Task Delete(Archive archive)
{
try
{
await ArchiveService.Delete(archive);
await MessageService.Success("Archive deleted!");
}
catch
{
await MessageService.Error("Archive could not be deleted.");
}
}
private void Clear()
{
File = null;
}
private void Cancel()
{
File = null;
ModalVisible = false;
}
private void FileSelected(InputFileChangeEventArgs args)
{
File = args.File;
}
private async Task UploadArchive()
{
long uploadedBytes = 0;
long totalBytes = File.Size;
Watch = new Stopwatch();
using (var stream = File.OpenReadStream(long.MaxValue))
{
Uploading = true;
Watch.Start();
while (Uploading)
{
byte[] chunk;
if (totalBytes - uploadedBytes < ChunkSize)
chunk = new byte[totalBytes - uploadedBytes];
else
chunk = new byte[ChunkSize];
int bytesRead = 0;
// This feels hacky, why do we need to do this?
// Only 32256 bytes of the file get read unless we
// loop through like this. Probably kills performance.
while (bytesRead < chunk.Length)
{
bytesRead += await stream.ReadAsync(chunk, bytesRead, chunk.Length - bytesRead);
}
using (FileStream fs = new FileStream(Path.Combine("Upload", Archive.Id.ToString()), FileMode.Append))
{
await fs.WriteAsync(chunk);
}
uploadedBytes += chunk.Length;
WatchBytesTransferred += chunk.Length;
Progress = (int)(uploadedBytes * 100 / totalBytes);
if (Watch.Elapsed.TotalSeconds >= 1)
{
Speed = WatchBytesTransferred * (1 / Watch.Elapsed.TotalSeconds);
WatchBytesTransferred = 0;
Watch.Restart();
}
if (Progress >= 100)
{
Watch.Stop();
Uploading = false;
await UploadComplete();
}
await InvokeAsync(StateHasChanged);
}
}
}
private async Task UploadComplete()
{
Archive.ObjectKey = Archive.Id.ToString();
Archive.CompressedSize = File.Size;
await ArchiveService.Add(Archive);
ModalVisible = false;
await MessageService.Success("Archive uploaded!");
}
}

View file

@ -0,0 +1,145 @@
@using AntDesign.TableModels;
@using LANCommander.Data.Enums
@using LANCommander.Models
@using LANCommander.PCGamingWiki
@inject IGDBService IGDBService
@inject CompanyService CompanyService
@inject GenreService GenreService
@inject TagService TagService
@{
RenderFragment Footer =
@<Template>
<Button OnClick="SelectGame" Disabled="@(Results == null || Results.Count() == 0)" Type="@ButtonType.Primary">Select</Button>
<Button OnClick="() => ModalVisible= false">Cancel</Button>
</Template>;
}
<Modal Visible="ModalVisible" Title="Game Metadata Lookup" Footer="@Footer">
<Table
@ref="ResultsTable"
TItem="Game"
DataSource="Results"
HidePagination="true"
Loading="Results == null"
OnRowClick="OnRowClicked"
@bind-SelectedRows="SelectedResults"
ScrollY="calc(100vh - 55px - 55px - 53px)">
<Selection Key="@context.IGDBId.ToString()" Type="radio" />
<PropertyColumn Property="g => g.Title" Title="Title" />
<PropertyColumn Property="g => g.ReleasedOn" Format="MM/dd/yyyy" Title="Released" />
<PropertyColumn Property="g => g.Developers">
@String.Join(", ", context.Developers?.Select(d => d.Name))
</PropertyColumn>
</Table>
</Modal>
@code {
[Parameter] public EventCallback<GameLookupResult> OnResultSelected { get; set; }
ITable? ResultsTable;
IEnumerable<Game> Results { get; set; }
IEnumerable<Game> SelectedResults { get; set; }
PCGamingWikiClient PCGamingWikiClient { get; set; }
bool ModalVisible { get; set; } = false;
protected override async Task OnInitializedAsync()
{
PCGamingWikiClient = new PCGamingWikiClient();
}
private void OnRowClicked(RowData<Game> row)
{
ResultsTable.SetSelection(new string[] { row.Data.IGDBId.ToString() });
}
public async Task SearchForGame(string title)
{
ModalVisible = true;
Results = null;
var results = await IGDBService.Search(title, "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()
{
var result = new GameLookupResult();
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);
await OnResultSelected.InvokeAsync(result);
ModalVisible = false;
}
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,126 @@
@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 tt" 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()
{
if (Game.Keys == null)
Game.Keys = new List<Key>();
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,85 +1,59 @@
@using LANCommander.Data.Enums
@using LANCommander.Data.Models
@{
int i = 0;
}
<div class="table-responsive">
<table class="table mb-0">
<thead>
<tr>
<th>Type</th>
<th>Min Players</th>
<th>Max Players</th>
<th>Description</th>
<th></th>
</tr>
</thead>
<Space Direction="DirectionVHType.Vertical" Size="@("large")" Style="width: 100%">
<SpaceItem>
<Table TItem="MultiplayerMode" DataSource="@Game.MultiplayerModes" HidePagination="true">
<PropertyColumn Property="m => m.Type">
<Select @bind-Value="context.Type" TItem="MultiplayerType" TItemValue="MultiplayerType" DataSource="Enum.GetValues<MultiplayerType>()" />
</PropertyColumn>
<PropertyColumn Property="m => m.MinPlayers">
<AntDesign.InputNumber @bind-Value="context.MinPlayers" DefaultValue="2" Min="2" />
</PropertyColumn>
<PropertyColumn Property="m => m.MaxPlayers">
<AntDesign.InputNumber @bind-Value="context.MaxPlayers" DefaultValue="2" Min="2" />
</PropertyColumn>
<PropertyColumn Property="m => m.Description">
<Input Type="text" @bind-Value="context.Description" />
</PropertyColumn>
<ActionColumn>
<Space Style="display: flex; justify-content: end">
<SpaceItem>
<Button OnClick="() => RemoveMode(context)" Type="@ButtonType.Text" Danger Icon="@IconType.Outline.Close" />
</SpaceItem>
</Space>
</ActionColumn>
</Table>
</SpaceItem>
<tbody>
@if (MultiplayerModes.Count == 0)
{
<tr><td colspan="5">If the game has any multiplayer modes you can add them here to provide metadata to clients.</td></tr>
}
@foreach (var multiplayerMode in MultiplayerModes)
{
var index = i;
<tr>
<td>
<select @bind="multiplayerMode.Type" name="Game.MultiplayerModes[@i].Type" class="form-control">
@foreach (var type in Enum.GetValues(typeof(MultiplayerType)))
{
<option value="@type">@type</option>
}
</select>
</td>
<td><input @bind="multiplayerMode.MinPlayers" name="Game.MultiplayerModes[@i].MinPlayers" class="form-control" /></td>
<td><input @bind="multiplayerMode.MaxPlayers" name="Game.MultiplayerModes[@i].MaxPlayers" class="form-control" /></td>
<td><input @bind="multiplayerMode.Description" name="Game.MultiplayerModes[@i].Description" class="form-control" /></td>
<td>
<input name="Game.MultiplayerModes[@i].GameId" type="hidden" value="@GameId" />
<div class="btn-list flex-nowrap justify-content-end">
<button class="btn btn-ghost-danger btn-icon" @onclick="() => RemoveMode(index)" type="button">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-x" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
</td>
</tr>
i++;
}
<tr>
<td colspan="5">
<div class="btn-list flex-nowrap justify-content-end">
<button class="btn btn-ghost-primary" @onclick="AddMode" type="button">Add Mode</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<SpaceItem>
<GridRow Justify="end">
<GridCol>
<Button OnClick="AddMode" Type="@ButtonType.Primary">Add Mode</Button>
</GridCol>
</GridRow>
</SpaceItem>
</Space>
@code {
[Parameter] public ICollection<MultiplayerMode> MultiplayerModes { get; set; }
[Parameter] public Guid GameId { get; set; }
[Parameter] public Game Game { get; set; }
protected override async Task OnInitializedAsync()
{
if (Game.MultiplayerModes == null)
Game.MultiplayerModes = new List<MultiplayerMode>();
}
private void AddMode()
{
if (MultiplayerModes == null)
MultiplayerModes = new List<MultiplayerMode>();
if (Game.MultiplayerModes == null)
Game.MultiplayerModes = new List<MultiplayerMode>();
MultiplayerModes.Add(new MultiplayerMode());
Game.MultiplayerModes.Add(new MultiplayerMode());
}
private void RemoveMode(int index)
private void RemoveMode(MultiplayerMode mode)
{
MultiplayerModes.Remove(MultiplayerModes.ElementAt(index));
Game.MultiplayerModes.Remove(mode);
}
}

View file

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

View file

@ -0,0 +1,193 @@
@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 tt" />
<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()
{
if (Game.Scripts == null)
Game.Scripts = new List<Script>();
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,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,42 @@
@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)).ToList();
var toRemove = SelectedEntities.Where(e => !values.Any(v => v == e.Id)).ToList();
foreach (var value in toAdd)
{
SelectedEntities.Add(Entities.First(e => e.Id == value));
}
foreach (var value in toRemove)
{
SelectedEntities.Remove(value);
}
SelectedValues = SelectedEntities.Select(e => e.Id);
}
}

View file

@ -1,140 +0,0 @@
using LANCommander.Data;
using LANCommander.Data.Models;
using LANCommander.Extensions;
using LANCommander.Models;
using LANCommander.SDK;
using LANCommander.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.IO.Compression;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
namespace LANCommander.Controllers
{
[Authorize(Roles = "Administrator")]
public class ArchivesController : Controller
{
private readonly GameService GameService;
private readonly ArchiveService ArchiveService;
public ArchivesController(GameService gameService, ArchiveService archiveService)
{
GameService = gameService;
ArchiveService = archiveService;
}
public async Task<IActionResult> Add(Guid? id)
{
if (id == null)
return NotFound();
var game = await GameService.Get(id.GetValueOrDefault());
if (game == null)
return NotFound();
Archive lastVersion = null;
if (game.Archives != null && game.Archives.Count > 0)
lastVersion = game.Archives.OrderByDescending(a => a.CreatedOn).First();
return View(new Archive()
{
Game = game,
GameId = game.Id,
LastVersion = lastVersion,
});
}
[HttpPost]
public async Task<IActionResult> Add(Guid? id, Archive archive)
{
archive.Id = Guid.Empty;
var game = await GameService.Get(id.GetValueOrDefault());
if (game == null)
return NotFound();
archive.Game = game;
archive.GameId = game.Id;
if (game.Archives != null && game.Archives.Any(a => a.Version == archive.Version))
ModelState.AddModelError("Version", "An archive for this game is already using that version.");
if (ModelState.IsValid)
{
await ArchiveService.Update(archive);
return RedirectToAction("Edit", "Games", new { id = id });
}
return View(archive);
}
public async Task<IActionResult> Download(Guid id)
{
var archive = await ArchiveService.Get(id);
var content = new FileStream($"Upload/{archive.ObjectKey}".ToPath(), FileMode.Open, FileAccess.Read, FileShare.Read);
return File(content, "application/octet-stream", $"{archive.Game.Title.SanitizeFilename()}.zip");
}
public async Task<IActionResult> Delete(Guid? id)
{
var archive = await ArchiveService.Get(id.GetValueOrDefault());
var gameId = archive.Game.Id;
await ArchiveService.Delete(archive);
return RedirectToAction("Edit", "Games", new { id = gameId });
}
public async Task<IActionResult> Browse(Guid id)
{
var archive = await ArchiveService.Get(id);
return View(archive);
}
public async Task<IActionResult> Validate(Guid id, Archive archive)
{
var path = $"Upload/{id}".ToPath();
string manifestContents = String.Empty;
long compressedSize = 0;
long uncompressedSize = 0;
if (!System.IO.File.Exists(path))
return BadRequest("Specified object does not exist");
var game = await GameService.Get(archive.GameId);
if (game == null)
return BadRequest("The related game is missing or corrupt.");
archive.GameId = game.Id;
archive.Id = Guid.Empty;
archive.CompressedSize = compressedSize;
archive.UncompressedSize = uncompressedSize;
archive.ObjectKey = id.ToString();
try
{
archive = await ArchiveService.Add(archive);
}
catch (Exception ex)
{
}
return Json(new
{
Id = archive.Id,
ObjectKey = archive.ObjectKey,
});
}
}
}

View file

@ -1,166 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using LANCommander.Data;
using LANCommander.Data.Models;
using Microsoft.AspNetCore.Authorization;
namespace LANCommander.Controllers
{
[Authorize(Roles = "Administrator")]
public class CompaniesController : Controller
{
private readonly DatabaseContext _context;
public CompaniesController(DatabaseContext context)
{
_context = context;
}
// GET: Companies
public async Task<IActionResult> Index()
{
return _context.Companies != null ?
View(await _context.Companies.ToListAsync()) :
Problem("Entity set 'DatabaseContext.Companies' is null.");
}
// GET: Companies/Details/5
public async Task<IActionResult> Details(Guid? id)
{
if (id == null || _context.Companies == null)
{
return NotFound();
}
var company = await _context.Companies
.FirstOrDefaultAsync(m => m.Id == id);
if (company == null)
{
return NotFound();
}
return View(company);
}
// GET: Companies/Create
public IActionResult Create()
{
return View();
}
// POST: Companies/Create
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("Name,Id,CreatedOn,CreatedById,UpdatedOn,UpdatedById")] Company company)
{
if (ModelState.IsValid)
{
company.Id = Guid.NewGuid();
_context.Add(company);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
return View(company);
}
// GET: Companies/Edit/5
public async Task<IActionResult> Edit(Guid? id)
{
if (id == null || _context.Companies == null)
{
return NotFound();
}
var company = await _context.Companies.FindAsync(id);
if (company == null)
{
return NotFound();
}
return View(company);
}
// POST: Companies/Edit/5
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(Guid id, [Bind("Name,Id,CreatedOn,CreatedById,UpdatedOn,UpdatedById")] Company company)
{
if (id != company.Id)
{
return NotFound();
}
if (ModelState.IsValid)
{
try
{
_context.Update(company);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!CompanyExists(company.Id))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction(nameof(Index));
}
return View(company);
}
// GET: Companies/Delete/5
public async Task<IActionResult> Delete(Guid? id)
{
if (id == null || _context.Companies == null)
{
return NotFound();
}
var company = await _context.Companies
.FirstOrDefaultAsync(m => m.Id == id);
if (company == null)
{
return NotFound();
}
return View(company);
}
// POST: Companies/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(Guid id)
{
if (_context.Companies == null)
{
return Problem("Entity set 'DatabaseContext.Companies' is null.");
}
var company = await _context.Companies.FindAsync(id);
if (company != null)
{
_context.Companies.Remove(company);
}
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
private bool CompanyExists(Guid id)
{
return (_context.Companies?.Any(e => e.Id == id)).GetValueOrDefault();
}
}
}

View file

@ -1,577 +0,0 @@
using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using LANCommander.Data;
using LANCommander.Data.Models;
using Microsoft.AspNetCore.Authorization;
using LANCommander.Services;
using System.Drawing;
using LANCommander.Models;
using LANCommander.Data.Enums;
using LANCommander.PCGamingWiki;
namespace LANCommander.Controllers
{
[Authorize(Roles = "Administrator")]
public class GamesController : Controller
{
private readonly GameService GameService;
private readonly ArchiveService ArchiveService;
private readonly CategoryService CategoryService;
private readonly TagService TagService;
private readonly GenreService GenreService;
private readonly CompanyService CompanyService;
private readonly IGDBService IGDBService;
private readonly PCGamingWikiClient PCGamingWikiClient;
public GamesController(GameService gameService, ArchiveService archiveService, CategoryService categoryService, TagService tagService, GenreService genreService, CompanyService companyService, IGDBService igdbService)
{
GameService = gameService;
ArchiveService = archiveService;
CategoryService = categoryService;
TagService = tagService;
GenreService = genreService;
CompanyService = companyService;
IGDBService = igdbService;
PCGamingWikiClient = new PCGamingWikiClient();
}
// GET: Games
public async Task<IActionResult> Index()
{
return View(GameService.Get());
}
public async Task<IActionResult> Add(long? igdbid)
{
var viewModel = new GameViewModel()
{
Game = new Game(),
Developers = new List<SelectListItem>(),
Publishers = new List<SelectListItem>(),
Genres = new List<SelectListItem>(),
Tags = new List<SelectListItem>(),
};
if (igdbid == null)
{
viewModel.Game = new Game()
{
Actions = new List<Data.Models.Action>(),
MultiplayerModes = new List<Data.Models.MultiplayerMode>()
};
viewModel.Developers = CompanyService.Get().OrderBy(c => c.Name).Select(c => new SelectListItem() { Text = c.Name, Value = c.Name }).ToList();
viewModel.Publishers = CompanyService.Get().OrderBy(c => c.Name).Select(c => new SelectListItem() { Text = c.Name, Value = c.Name }).ToList();
viewModel.Genres = GenreService.Get().OrderBy(g => g.Name).Select(g => new SelectListItem() { Text = g.Name, Value = g.Name }).ToList();
viewModel.Tags = TagService.Get().OrderBy(t => t.Name).Select(t => new SelectListItem() { Text = t.Name, Value = t.Name }).ToList();
return View(viewModel);
}
var result = await IGDBService.Get(igdbid.Value, "genres.*", "game_modes.*", "multiplayer_modes.*", "release_dates.*", "platforms.*", "keywords.*", "involved_companies.*", "involved_companies.company.*", "cover.*");
viewModel.Game = new Game()
{
IGDBId = result.Id.GetValueOrDefault(),
Title = result.Name,
Description = result.Summary,
ReleasedOn = result.FirstReleaseDate.GetValueOrDefault().UtcDateTime,
Actions = new List<Data.Models.Action>(),
MultiplayerModes = new List<MultiplayerMode>()
};
var playerCounts = await PCGamingWikiClient.GetMultiplayerPlayerCounts(result.Name);
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;
}
viewModel.Game.MultiplayerModes.Add(new MultiplayerMode()
{
Type = type,
MaxPlayers = playerCount.Value,
MinPlayers = 2
});
}
}
if (result.GameModes != null && result.GameModes.Values != null)
viewModel.Game.Singleplayer = result.GameModes.Values.Any(gm => gm.Name == "Singleplayer");
#region Multiplayer Modes
if (result.MultiplayerModes != null && result.MultiplayerModes.Values != null)
{
var lan = result.MultiplayerModes.Values.Where(mm => mm.LanCoop.GetValueOrDefault()).OrderByDescending(mm => mm.OnlineMax).FirstOrDefault();
var online = result.MultiplayerModes.Values.Where(mm => mm.OnlineCoop.GetValueOrDefault()).OrderByDescending(mm => mm.OnlineMax).FirstOrDefault();
var offline = result.MultiplayerModes.Values.Where(mm => mm.OfflineCoop.GetValueOrDefault()).OrderByDescending(mm => mm.OnlineMax).FirstOrDefault();
if (lan != null)
{
viewModel.Game.MultiplayerModes.Add(new MultiplayerMode()
{
Type = MultiplayerType.Lan,
MaxPlayers = lan.OnlineMax.GetValueOrDefault(),
});
}
if (online != null)
{
viewModel.Game.MultiplayerModes.Add(new MultiplayerMode()
{
Type = MultiplayerType.Online,
MaxPlayers = online.OnlineMax.GetValueOrDefault(),
});
}
if (offline != null)
{
viewModel.Game.MultiplayerModes.Add(new MultiplayerMode()
{
Type = MultiplayerType.Local,
MaxPlayers = offline.OfflineMax.GetValueOrDefault(),
});
}
}
#endregion
#region Publishers & Developers
var companies = CompanyService.Get();
if (result.InvolvedCompanies != null && result.InvolvedCompanies.Values != null)
{
// Make sure companie
var developerNames = result.InvolvedCompanies.Values.Where(c => c.Developer.GetValueOrDefault()).Select(c => c.Company.Value.Name);
var publisherNames = result.InvolvedCompanies.Values.Where(c => c.Publisher.GetValueOrDefault()).Select(c => c.Company.Value.Name);
viewModel.Developers.AddRange(companies.Select(c => new SelectListItem()
{
Text = c.Name,
Value = c.Name,
Selected = developerNames.Contains(c.Name),
}));
viewModel.Publishers.AddRange(companies.Select(c => new SelectListItem()
{
Text = c.Name,
Value = c.Name,
Selected = publisherNames.Contains(c.Name),
}));
foreach (var developer in developerNames)
{
if (!viewModel.Developers.Any(d => d.Value == developer))
{
viewModel.Developers.Add(new SelectListItem()
{
Text = developer,
Value = developer,
Selected = true
});
}
}
foreach (var publisher in publisherNames)
{
if (!viewModel.Publishers.Any(d => d.Value == publisher))
{
viewModel.Publishers.Add(new SelectListItem()
{
Text = publisher,
Value = publisher,
Selected = true
});
}
}
viewModel.Developers = viewModel.Developers.OrderBy(d => d.Value).ToList();
viewModel.Publishers = viewModel.Publishers.OrderBy(d => d.Value).ToList();
}
#endregion
#region Genres
var genres = GenreService.Get();
if (result.Genres != null && result.Genres.Values != null)
{
var genreNames = result.Genres.Values.Select(g => g.Name);
viewModel.Genres.AddRange(genres.Select(g => new SelectListItem()
{
Text = g.Name,
Value = g.Name,
Selected = genreNames.Contains(g.Name),
}));
foreach (var genre in genreNames)
{
if (!viewModel.Genres.Any(g => g.Value == genre))
{
viewModel.Genres.Add(new SelectListItem()
{
Text = genre,
Value = genre,
Selected = true
});
}
}
viewModel.Genres = viewModel.Genres.OrderBy(g => g.Value).ToList();
}
#endregion
#region Tags
var tags = TagService.Get();
if (result.Keywords != null && result.Keywords.Values != null)
{
var tagNames = result.Keywords.Values.Select(t => t.Name).Take(20);
viewModel.Tags.AddRange(genres.Select(t => new SelectListItem()
{
Text = t.Name,
Value = t.Name,
Selected = tagNames.Contains(t.Name),
}));
foreach (var tag in tagNames)
{
if (!viewModel.Tags.Any(t => t.Value == tag))
{
viewModel.Tags.Add(new SelectListItem()
{
Text = tag,
Value = tag,
Selected = true
});
}
}
}
#endregion
return View(viewModel);
}
// POST: Games/Create
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Add(GameViewModel viewModel)
{
if (ModelState.IsValid)
{
var game = await GameService.Add(viewModel.Game);
if (viewModel.SelectedDevelopers != null && viewModel.SelectedDevelopers.Length > 0)
game.Developers = viewModel.SelectedDevelopers.Select(async d => await CompanyService.AddMissing(x => x.Name == d, new Company() { Name = d })).Select(t => t.Result).ToList();
if (viewModel.SelectedPublishers != null && viewModel.SelectedPublishers.Length > 0)
game.Publishers = viewModel.SelectedPublishers.Select(async p => await CompanyService.AddMissing(x => x.Name == p, new Company() { Name = p })).Select(t => t.Result).ToList();
if (viewModel.SelectedGenres != null && viewModel.SelectedGenres.Length > 0)
game.Genres = viewModel.SelectedGenres.Select(async g => await GenreService.AddMissing(x => x.Name == g, new Genre() { Name = g })).Select(t => t.Result).ToList();
if (viewModel.SelectedTags != null && viewModel.SelectedTags.Length > 0)
game.Tags = viewModel.SelectedTags.Select(async t => await TagService.AddMissing(x => x.Name == t, new Tag() { Name = t })).Select(t => t.Result).ToList();
await GameService.Update(game);
return RedirectToAction(nameof(Edit), new { id = game.Id });
}
return View(viewModel.Game);
}
// GET: Games/Edit/5
public async Task<IActionResult> Edit(Guid? id)
{
var viewModel = new GameViewModel();
viewModel.Game = await GameService.Get(id.GetValueOrDefault());
if (viewModel.Game == null)
return NotFound();
viewModel.Developers = CompanyService.Get()
.OrderBy(c => c.Name)
.Select(c => new SelectListItem() { Text = c.Name, Value = c.Name, Selected = viewModel.Game.Developers.Any(d => d.Id == c.Id) })
.ToList();
viewModel.Publishers = CompanyService.Get()
.OrderBy(c => c.Name)
.Select(c => new SelectListItem() { Text = c.Name, Value = c.Name, Selected = viewModel.Game.Publishers.Any(d => d.Id == c.Id) })
.ToList();
viewModel.Genres = GenreService.Get()
.OrderBy(g => g.Name)
.Select(g => new SelectListItem() { Text = g.Name, Value = g.Name, Selected = viewModel.Game.Genres.Any(x => x.Id == g.Id) })
.ToList();
viewModel.Tags = TagService.Get()
.OrderBy(t => t.Name)
.Select(t => new SelectListItem() { Text = t.Name, Value = t.Name, Selected = viewModel.Game.Tags.Any(x => x.Id == t.Id) })
.ToList();
return View(viewModel);
}
// POST: Games/Edit/5
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(Guid id, GameViewModel viewModel)
{
if (id != viewModel.Game.Id)
{
return NotFound();
}
if (ModelState.IsValid)
{
var game = GameService.Get(g => g.Id == viewModel.Game.Id).FirstOrDefault();
game.Title = viewModel.Game.Title;
game.SortTitle = viewModel.Game.SortTitle;
game.DirectoryName = viewModel.Game.DirectoryName;
game.Icon = viewModel.Game.Icon;
game.Description = viewModel.Game.Description;
game.ReleasedOn = viewModel.Game.ReleasedOn;
game.Singleplayer = viewModel.Game.Singleplayer;
#region Update Developers
if (viewModel.SelectedDevelopers == null)
viewModel.SelectedDevelopers = new string[0];
foreach (var developer in game.Developers)
{
if (!viewModel.SelectedDevelopers.Any(d => d == developer.Name))
game.Developers.Remove(developer);
}
foreach (var newDeveloper in viewModel.SelectedDevelopers.Where(sd => !game.Developers.Any(d => d.Name == sd)))
{
game.Developers.Add(new Company()
{
Name = newDeveloper
});
}
#endregion
#region Update Publishers
if (viewModel.SelectedPublishers == null)
viewModel.SelectedPublishers = new string[0];
foreach (var publisher in game.Publishers)
{
if (!viewModel.SelectedPublishers.Any(p => p == publisher.Name))
game.Publishers.Remove(publisher);
}
foreach (var newPublisher in viewModel.SelectedPublishers.Where(sp => !game.Publishers.Any(p => p.Name == sp)))
{
game.Publishers.Add(new Company()
{
Name = newPublisher
});
}
#endregion
#region Update Genres
if (viewModel.SelectedGenres == null)
viewModel.SelectedGenres = new string[0];
foreach (var genre in game.Genres)
{
if (!viewModel.SelectedGenres.Any(g => g == genre.Name))
game.Genres.Remove(genre);
}
foreach (var newGenre in viewModel.SelectedGenres.Where(sg => !game.Genres.Any(g => g.Name == sg)))
{
game.Genres.Add(new Genre()
{
Name = newGenre
});
}
#endregion
#region Update Tags
if (viewModel.SelectedTags == null)
viewModel.SelectedTags = new string[0];
foreach (var tag in game.Tags)
{
if (!viewModel.SelectedTags.Any(t => t == tag.Name))
game.Tags.Remove(tag);
}
foreach (var newTag in viewModel.SelectedTags.Where(st => !game.Tags.Any(t => t.Name == st)))
{
game.Tags.Add(new Tag()
{
Name = newTag
});
}
#endregion
#region Update Actions
if (game.Actions != null)
{
game.Actions.Clear();
if (viewModel.Game.Actions != null)
{
foreach (var action in viewModel.Game.Actions)
{
game.Actions.Add(action);
}
}
}
#endregion
#region Update MultiplayerModes
if (game.MultiplayerModes != null)
{
game.MultiplayerModes.Clear();
if (viewModel.Game.MultiplayerModes != null)
{
foreach (var multiplayerMode in viewModel.Game.MultiplayerModes)
{
game.MultiplayerModes.Add(multiplayerMode);
}
}
}
#endregion
await GameService.Update(game);
return RedirectToAction(nameof(Edit), new { id = id });
}
return View(viewModel);
}
// GET: Games/Delete/5
public async Task<IActionResult> Delete(Guid? id)
{
var game = await GameService.Get(id.GetValueOrDefault());
if (game == null)
{
return NotFound();
}
return View(game);
}
// POST: Games/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(Guid id)
{
var game = await GameService.Get(id);
if (game == null)
return NotFound();
await GameService.Delete(game);
return RedirectToAction(nameof(Index));
}
[HttpPost]
public async Task<IActionResult> Lookup(Game game)
{
var viewModel = new GameLookupResultsViewModel()
{
Search = game.Title
};
var results = await IGDBService.Search(game.Title, "involved_companies.*", "involved_companies.company.*");
if (results == null)
return View(new List<Game>());
viewModel.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;
});
return View(viewModel);
}
/// <summary>
/// Provides a list of possible games based on the given name
/// </summary>
/// <param name="name">Name of the game to lookup against IGDB</param>
/// <returns></returns>
public async Task<IActionResult> SearchMetadata(string name)
{
var metadata = await IGDBService.Search(name, "genres.*", "multiplayer_modes.*", "release_dates.*", "platforms.*", "keywords.*", "involved_companies.*", "involved_companies.company.*", "cover.*");
if (metadata == null)
return NotFound();
return Json(metadata);
}
public async Task<IActionResult> GetIcon(Guid id)
{
try
{
var game = await GameService.Get(id);
return File(GameService.GetIcon(game), "image/png");
}
catch (FileNotFoundException ex)
{
return NotFound();
}
}
}
}

View file

@ -1,51 +0,0 @@
using ByteSizeLib;
using LANCommander.Data;
using LANCommander.Data.Models;
using LANCommander.Models;
using LANCommander.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Diagnostics;
namespace LANCommander.Controllers
{
[Authorize]
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly GameService GameService;
public HomeController(ILogger<HomeController> logger, GameService gameService)
{
GameService = gameService;
_logger = logger;
}
public IActionResult Index()
{
var drives = DriveInfo.GetDrives();
var root = Path.GetPathRoot(System.Reflection.Assembly.GetExecutingAssembly().Location);
var model = new DashboardViewModel();
model.TotalStorageSize = ByteSize.FromBytes(drives.Where(d => d.IsReady && d.Name == root).Sum(d => d.TotalSize));
model.TotalAvailableFreeSpace = ByteSize.FromBytes(drives.Where(d => d.IsReady && d.Name == root).Sum(d => d.AvailableFreeSpace));
model.TotalUploadDirectorySize = ByteSize.FromBytes(new DirectoryInfo("Upload").EnumerateFiles().Sum(f => f.Length));
model.TotalSaveDirectorySize = ByteSize.FromBytes(new DirectoryInfo("Save").EnumerateFiles().Sum(f => f.Length));
model.GameCount = GameService.Get().Count;
return View(model);
}
public IActionResult Privacy()
{
return View();
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
}

View file

@ -1,110 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using LANCommander.Data;
using LANCommander.Data.Models;
using Microsoft.AspNetCore.Authorization;
using LANCommander.Models;
using LANCommander.Services;
namespace LANCommander.Controllers
{
[Authorize(Roles = "Administrator")]
public class KeysController : Controller
{
private readonly DatabaseContext Context;
private readonly KeyService KeyService;
public KeysController(DatabaseContext context, KeyService keyService)
{
Context = context;
KeyService = keyService;
}
public async Task<IActionResult> Details(Guid? id)
{
using (var repo = new Repository<Game>(Context, HttpContext))
{
var game = await repo.Find(id.GetValueOrDefault());
if (game == null)
return NotFound();
return View(game);
}
}
public async Task<IActionResult> Edit(Guid? id)
{
using (var repo = new Repository<Game>(Context, HttpContext))
{
var game = await repo.Find(id.GetValueOrDefault());
if (game == null)
return NotFound();
var viewModel = new EditKeysViewModel()
{
Game = game,
Keys = String.Join("\n", game.Keys.OrderByDescending(k => k.ClaimedOn).Select(k => k.Value))
};
return View(viewModel);
}
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(Guid id, EditKeysViewModel viewModel)
{
var keys = viewModel.Keys.Split("\n").Select(k => k.Trim()).Where(k => !String.IsNullOrWhiteSpace(k));
using (var gameRepo = new Repository<Game>(Context, HttpContext))
{
var game = await gameRepo.Find(id);
if (game == null)
return NotFound();
using (var keyRepo = new Repository<Key>(Context, HttpContext))
{
var existingKeys = keyRepo.Get(k => k.Game.Id == id).ToList();
var keysDeleted = existingKeys.Where(k => !keys.Contains(k.Value));
var keysAdded = keys.Where(k => !existingKeys.Any(e => e.Value == k));
foreach (var key in keysDeleted)
keyRepo.Delete(key);
foreach (var key in keysAdded)
await keyRepo.Add(new Key()
{
Game = game,
Value = key,
});
await keyRepo.SaveChanges();
}
}
return RedirectToAction("Edit", "Games", new { id = id });
}
public async Task<IActionResult> Release(Guid id)
{
var existing = await KeyService.Get(id);
if (existing == null)
return NotFound();
await KeyService.Release(id);
return RedirectToAction("Details", "Keys", new { id = existing.Game.Id });
}
}
}

View file

@ -1,102 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using LANCommander.Data;
using LANCommander.Data.Models;
using Microsoft.AspNetCore.Authorization;
using LANCommander.Models;
using LANCommander.Services;
namespace LANCommander.Controllers
{
[Authorize(Roles = "Administrator")]
public class ScriptsController : BaseController
{
private readonly GameService GameService;
private readonly ScriptService ScriptService;
public ScriptsController(GameService gameService, ScriptService scriptService)
{
GameService = gameService;
ScriptService = scriptService;
}
public async Task<IActionResult> Add(Guid? id)
{
var game = await GameService.Get(id.GetValueOrDefault());
if (game == null)
return NotFound();
var script = new Script()
{
GameId = game.Id,
Game = game
};
return View(script);
}
[HttpPost]
public async Task<IActionResult> Add(Script script)
{
script.Id = Guid.Empty;
if (ModelState.IsValid)
{
script = await ScriptService.Add(script);
return RedirectToAction("Edit", "Games", new { id = script.GameId });
}
return View(script);
}
public async Task<IActionResult> Edit(Guid? id)
{
var script = await ScriptService.Get(id.GetValueOrDefault());
if (script == null)
return NotFound();
return View(script);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(Guid id, Script script)
{
if (ModelState.IsValid)
{
await ScriptService.Update(script);
Alert("The script has been saved!", "success");
return RedirectToAction("Edit", "Games", new { id = script.GameId });
}
script.Game = await GameService.Get(script.GameId.GetValueOrDefault());
return View(script);
}
public async Task<IActionResult> Delete(Guid? id)
{
var script = await ScriptService.Get(id.GetValueOrDefault());
if (script == null)
return NotFound();
var gameId = script.GameId;
await ScriptService.Delete(script);
return RedirectToAction("Edit", "Games", new { id = gameId });
}
}
}

View file

@ -1,149 +0,0 @@
using LANCommander.Data.Models;
using LANCommander.Models;
using LANCommander.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
namespace LANCommander.Controllers
{
[Authorize(Roles = "Administrator")]
public class SettingsController : BaseController
{
private readonly SettingService SettingService;
private readonly UserManager<User> UserManager;
public SettingsController(SettingService settingService, UserManager<User> userManager)
{
SettingService = settingService;
UserManager = userManager;
}
public IActionResult Index()
{
return RedirectToAction(nameof(General));
}
public IActionResult General()
{
var settings = SettingService.GetSettings();
return View(settings);
}
[HttpPost]
public IActionResult General(LANCommanderSettings settings)
{
SettingService.SaveSettings(settings);
return RedirectToAction(nameof(General));
}
public async Task<IActionResult> Users()
{
var users = new List<UserViewModel>();
foreach (var user in UserManager.Users)
{
var savePath = Path.Combine("Save", user.Id.ToString());
long saveSize = 0;
if (Directory.Exists(savePath))
saveSize = new DirectoryInfo(savePath).EnumerateFiles().Sum(f => f.Length);
users.Add(new UserViewModel()
{
Id = user.Id,
UserName = user.UserName,
Roles = await UserManager.GetRolesAsync(user),
SavesSize = saveSize
});
}
return View(users);
}
public async Task<IActionResult> DeleteUser(Guid id)
{
var user = await UserManager.FindByIdAsync(id.ToString());
var admins = await UserManager.GetUsersInRoleAsync("Administrator");
if (user.UserName == HttpContext.User.Identity.Name)
{
Alert("You cannot delete yourself!", "danger");
return RedirectToAction(nameof(Users));
}
if (admins.Count == 1 && admins.First().Id == id)
{
Alert("You cannot delete the only admin user!", "danger");
return RedirectToAction(nameof(Users));
}
try
{
await UserManager.DeleteAsync(user);
Alert("User successfully deleted!", "success");
return RedirectToAction(nameof(Users));
}
catch
{
Alert("User could not be deleted!", "danger");
return RedirectToAction(nameof(Users));
}
}
public async Task<IActionResult> PromoteUser(Guid id)
{
var user = await UserManager.FindByIdAsync(id.ToString());
try
{
await UserManager.AddToRoleAsync(user, "Administrator");
Alert("User promoted to administrator!", "success");
return RedirectToAction(nameof(Users));
}
catch (Exception ex)
{
Alert("User could not be promoted!", "danger");
return RedirectToAction(nameof(Users));
}
}
public async Task<IActionResult> DemoteUser(Guid id)
{
var user = await UserManager.FindByIdAsync(id.ToString());
var admins = await UserManager.GetUsersInRoleAsync("Administrator");
if (user.UserName == HttpContext.User.Identity.Name)
{
Alert("You cannot demote yourself!", "danger");
return RedirectToAction(nameof(Users));
}
try
{
await UserManager.RemoveFromRoleAsync(user, "Administrator");
Alert("User successfully demoted!", "success");
return RedirectToAction(nameof(Users));
}
catch
{
Alert("User could not be demoted!", "danger");
return RedirectToAction(nameof(Users));
}
}
}
}

View file

@ -1,166 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using LANCommander.Data;
using LANCommander.Data.Models;
using Microsoft.AspNetCore.Authorization;
namespace LANCommander.Controllers
{
[Authorize(Roles = "Administrator")]
public class TagsController : Controller
{
private readonly DatabaseContext _context;
public TagsController(DatabaseContext context)
{
_context = context;
}
// GET: Tags
public async Task<IActionResult> Index()
{
return _context.Tags != null ?
View(await _context.Tags.ToListAsync()) :
Problem("Entity set 'DatabaseContext.Tags' is null.");
}
// GET: Tags/Details/5
public async Task<IActionResult> Details(Guid? id)
{
if (id == null || _context.Tags == null)
{
return NotFound();
}
var tag = await _context.Tags
.FirstOrDefaultAsync(m => m.Id == id);
if (tag == null)
{
return NotFound();
}
return View(tag);
}
// GET: Tags/Create
public IActionResult Create()
{
return View();
}
// POST: Tags/Create
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("Name,Id,CreatedOn,CreatedById,UpdatedOn,UpdatedById")] Tag tag)
{
if (ModelState.IsValid)
{
tag.Id = Guid.NewGuid();
_context.Add(tag);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
return View(tag);
}
// GET: Tags/Edit/5
public async Task<IActionResult> Edit(Guid? id)
{
if (id == null || _context.Tags == null)
{
return NotFound();
}
var tag = await _context.Tags.FindAsync(id);
if (tag == null)
{
return NotFound();
}
return View(tag);
}
// POST: Tags/Edit/5
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(Guid id, [Bind("Name,Id,CreatedOn,CreatedById,UpdatedOn,UpdatedById")] Tag tag)
{
if (id != tag.Id)
{
return NotFound();
}
if (ModelState.IsValid)
{
try
{
_context.Update(tag);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!TagExists(tag.Id))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction(nameof(Index));
}
return View(tag);
}
// GET: Tags/Delete/5
public async Task<IActionResult> Delete(Guid? id)
{
if (id == null || _context.Tags == null)
{
return NotFound();
}
var tag = await _context.Tags
.FirstOrDefaultAsync(m => m.Id == id);
if (tag == null)
{
return NotFound();
}
return View(tag);
}
// POST: Tags/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(Guid id)
{
if (_context.Tags == null)
{
return Problem("Entity set 'DatabaseContext.Tags' is null.");
}
var tag = await _context.Tags.FindAsync(id);
if (tag != null)
{
_context.Tags.Remove(tag);
}
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
private bool TagExists(Guid id)
{
return (_context.Tags?.Any(e => e.Id == id)).GetValueOrDefault();
}
}
}

View file

@ -1,56 +0,0 @@
using LANCommander.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace LANCommander.Controllers
{
[Authorize(Roles = "Administrator")]
public class UploadController : Controller
{
private const string UploadDirectory = "Upload";
public JsonResult Init()
{
var key = Guid.NewGuid().ToString();
if (!Directory.Exists(UploadDirectory))
Directory.CreateDirectory(UploadDirectory);
if (!System.IO.File.Exists(Path.Combine(UploadDirectory, key)))
System.IO.File.Create(Path.Combine(UploadDirectory, key)).Close();
return Json(new
{
Key = key
});
}
public async Task<IActionResult> Chunk([FromForm] ChunkUpload chunk)
{
var filePath = Path.Combine(UploadDirectory, chunk.Key.ToString());
if (!System.IO.File.Exists(filePath))
return BadRequest("Destination file not initialized.");
Request.EnableBuffering();
using (var ms = new MemoryStream())
{
await chunk.File.CopyToAsync(ms);
var data = ms.ToArray();
using (var fs = new FileStream(filePath, FileMode.Append, FileAccess.Write, FileShare.None))
{
fs.Position = chunk.Start;
fs.Write(data, 0, data.Length);
}
}
Thread.Sleep(100);
return Json("Done!");
}
}
}

View file

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

View file

@ -0,0 +1,19 @@
namespace LANCommander.Extensions
{
public static class ArrayExtensions
{
public static T[] ShiftArrayAndInsert<T>(this T[] array, T input, int max)
{
if (array == null || array.Length < max)
{
array = new T[max];
}
Array.Copy(array, 1, array, 0, array.Length - 1);
array[array.Length - 1] = input;
return array;
}
}
}

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

@ -20,6 +20,10 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AntDesign" Version="0.14.3" />
<PackageReference Include="AntDesign.Charts" Version="0.3.0" />
<PackageReference Include="Blazor-ApexCharts" Version="0.9.18-beta" />
<PackageReference Include="BlazorMonaco" Version="3.0.0" />
<PackageReference Include="ByteSize" Version="2.1.1" />
<PackageReference Include="IGDB" Version="2.3.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.12" />
@ -38,8 +42,11 @@
<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" Version="5.6.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.3.0" />
<PackageReference Include="System.Diagnostics.PerformanceCounter" Version="7.0.0" />
<PackageReference Include="YamlDotNet" Version="12.3.1" />
</ItemGroup>
@ -47,6 +54,8 @@
<Folder Include="bin\Debug\net6.0\" />
<Folder Include="Data\Migrations\" />
<Folder Include="Migrations\" />
<Folder Include="Pages\Games\Archives\" />
<Folder Include="Pages\Settings\" />
</ItemGroup>
<ItemGroup>
@ -54,12 +63,6 @@
<ProjectReference Include="..\LANCommander.SDK\LANCommander.SDK.csproj" />
</ItemGroup>
<ItemGroup>
<TypeScriptCompile Update="wwwroot\js\Upload.ts">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</TypeScriptCompile>
</ItemGroup>
<ProjectExtensions><VisualStudio><UserProperties /></VisualStudio></ProjectExtensions>
</Project>

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

@ -1,11 +0,0 @@
namespace LANCommander.Models
{
public class ChunkUpload
{
public long Start { get; set; }
public long End { get; set; }
public long Total { get; set; }
public Guid Key { get; set; }
public IFormFile File { get; set; }
}
}

View file

@ -1,10 +0,0 @@
using LANCommander.Data.Models;
namespace LANCommander.Models
{
public class EditKeysViewModel
{
public Game Game { get; set; }
public string Keys { 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,10 +0,0 @@
using LANCommander.Data.Models;
namespace LANCommander.Models
{
public class GameLookupResultsViewModel
{
public string Search { get; set; }
public IEnumerable<Game> Results { get; set; }
}
}

View file

@ -1,21 +0,0 @@
using LANCommander.Data.Models;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace LANCommander.Models
{
public class GameViewModel
{
public long? IgdbId { get; set; }
public Game Game { get; set; }
public List<SelectListItem>? Genres { get; set; }
public List<SelectListItem>? Tags { get; set; }
public List<SelectListItem>? Categories { get; set; }
public List<SelectListItem>? Developers { get; set; }
public List<SelectListItem>? Publishers { get; set; }
public string[]? SelectedGenres { get; set; }
public string[]? SelectedTags { get; set; }
public string[]? SelectedCategories { get; set; }
public string[]? SelectedDevelopers { get; set; }
public string[]? SelectedPublishers { get; set; }
}
}

View file

@ -0,0 +1,35 @@
using MudBlazor;
using System.Diagnostics;
namespace LANCommander.Models
{
public class PerformanceChartData
{
public PerformanceCounterData ProcessorUtilization { get; set; }
public Dictionary<string, PerformanceCounterData> NetworkUploadRate { get; set; }
public Dictionary<string, PerformanceCounterData> NetworkDownloadRate { get; set; }
}
public class PerformanceCounterData
{
public PerformanceCounter PerformanceCounter { get; set; }
public double[] Data { get; set; }
public ChartSeries ToSeries(string name)
{
return new ChartSeries
{
Name = name,
Data = Data
};
}
public List<ChartSeries> ToSeriesList(string name)
{
return new List<ChartSeries>
{
ToSeries(name)
};
}
}
}

View file

@ -0,0 +1,83 @@
@using System.Diagnostics;
@using LANCommander.Extensions;
@using AntDesign.Charts;
@using System.Collections.Concurrent;
<Area @ref="Chart" Config="Config" />
@code {
[Parameter] public int TimerHistory { get; set; }
[Parameter] public int TimerInterval { get; set; }
IChartComponent? Chart;
System.Timers.Timer Timer;
Dictionary<string, double[]> Data = new Dictionary<string, double[]>();
ConcurrentDictionary<string, PerformanceCounter> PerformanceCounters = new ConcurrentDictionary<string, PerformanceCounter>();
string JsConfig = @"{
meta: {
value: {
alias: 'Speed',
formatter: (v) => humanFileSize(v, true) + '/s'
}
}
}";
AreaConfig Config = new AreaConfig
{
Name = "Network Download Rate",
Padding = "auto",
SeriesField = "series",
YField = "value",
XField = "index",
Animation = false,
XAxis = new ValueCatTimeAxis
{
Visible = false
}
};
protected override async Task OnInitializedAsync()
{
if (Timer == null)
{
Timer = new System.Timers.Timer();
Timer.Interval = TimerInterval;
Timer.Elapsed += async (s, e) =>
{
await RefreshData();
};
}
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await Chart.UpdateChart(Config, null, null, JsConfig);
Timer.Start();
}
}
private async Task RefreshData()
{
var category = new PerformanceCounterCategory("Network Interface");
foreach (var instance in category.GetInstanceNames())
{
if (!Data.ContainsKey(instance))
Data[instance] = new double[TimerHistory];
if (!PerformanceCounters.ContainsKey(instance))
PerformanceCounters[instance] = new PerformanceCounter("Network Interface", "Bytes Received/sec", instance);
Data[instance] = Data[instance].ShiftArrayAndInsert((double)PerformanceCounters[instance].NextValue(), TimerHistory);
}
await Chart.ChangeData(Data.SelectMany(x => x.Value.Select((y, i) => new { value = y, index = i, series = x.Key })), true);
}
}

View file

@ -0,0 +1,83 @@
@using System.Diagnostics;
@using LANCommander.Extensions;
@using AntDesign.Charts;
@using System.Collections.Concurrent;
<Area @ref="Chart" Config="Config" />
@code {
[Parameter] public int TimerHistory { get; set; }
[Parameter] public int TimerInterval { get; set; }
IChartComponent? Chart;
System.Timers.Timer Timer;
Dictionary<string, double[]> Data = new Dictionary<string, double[]>();
ConcurrentDictionary<string, PerformanceCounter> PerformanceCounters = new ConcurrentDictionary<string, PerformanceCounter>();
string JsConfig = @"{
meta: {
value: {
alias: 'Speed',
formatter: (v) => humanFileSize(v, true) + '/s'
}
}
}";
AreaConfig Config = new AreaConfig
{
Name = "Network Upload Rate",
Padding = "auto",
SeriesField = "series",
YField = "value",
XField = "index",
Animation = false,
XAxis = new ValueCatTimeAxis
{
Visible = false
}
};
protected override async Task OnInitializedAsync()
{
if (Timer == null)
{
Timer = new System.Timers.Timer();
Timer.Interval = TimerInterval;
Timer.Elapsed += async (s, e) =>
{
await RefreshData();
};
}
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await Chart.UpdateChart(Config, null, null, JsConfig);
Timer.Start();
}
}
private async Task RefreshData()
{
var category = new PerformanceCounterCategory("Network Interface");
foreach (var instance in category.GetInstanceNames())
{
if (!Data.ContainsKey(instance))
Data[instance] = new double[TimerHistory];
if (!PerformanceCounters.ContainsKey(instance))
PerformanceCounters[instance] = new PerformanceCounter("Network Interface", "Bytes Sent/sec", instance);
Data[instance] = Data[instance].ShiftArrayAndInsert((double)PerformanceCounters[instance].NextValue(), TimerHistory);
}
await Chart.ChangeData(Data.SelectMany(x => x.Value.Select((y, i) => new { value = y, index = i, series = x.Key })), true);
}
}

View file

@ -0,0 +1,75 @@
@using System.Diagnostics;
@using LANCommander.Extensions;
@using AntDesign.Charts;
<Area @ref="Chart" Config="Config" />
@code {
[Parameter] public int TimerHistory { get; set; }
[Parameter] public int TimerInterval { get; set; }
IChartComponent? Chart;
System.Timers.Timer Timer;
double[] Data;
PerformanceCounter PerformanceCounter = new PerformanceCounter("Processor", "% Processor Time", "_Total");
string JsConfig = @"{
meta: {
value: {
alias: '% Usage',
formatter: (v) => v + '%'
}
}
}";
AreaConfig Config = new AreaConfig
{
Name = "Processor Utilization",
Padding = "auto",
YField = "value",
XField = "index",
Animation = false,
IsPercent = true,
YAxis = new ValueAxis
{
Min = 0,
Max = 100
},
XAxis = new ValueCatTimeAxis
{
Visible = false
}
};
protected override async Task OnInitializedAsync()
{
if (Timer == null)
{
Timer = new System.Timers.Timer();
Timer.Interval = TimerInterval;
Timer.Elapsed += async (s, e) =>
{
await RefreshData();
};
}
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await Chart.UpdateChart(Config, null, null, JsConfig);
Timer.Start();
}
}
private async Task RefreshData()
{
Data = Data.ShiftArrayAndInsert((double)Math.Ceiling(PerformanceCounter.NextValue()), TimerHistory);
await Chart.ChangeData(Data.Select((x, i) => new { value = x, index = i }), true);
}
}

View file

@ -0,0 +1,63 @@
@using AntDesign.Charts
@using ByteSizeLib
<Pie Data="Data" Config="Config" JsConfig="@JsConfig" />
@code {
object[] Data;
string JsConfig = @"{
meta: {
value: {
alias: 'Data Usage',
formatter: (v) => humanFileSize(v, true)
}
},
label: {
visible: true,
type: 'outer-center'
}
}";
PieConfig Config = new PieConfig
{
Radius = 0.8,
AngleField = "value",
ColorField = "type",
};
protected override async Task OnInitializedAsync()
{
var drives = DriveInfo.GetDrives();
var root = Path.GetPathRoot(System.Reflection.Assembly.GetExecutingAssembly().Location);
var totalStorageSize = drives.Where(d => d.IsReady && d.Name == root).Sum(d => d.TotalSize);
var totalAvailableFreeSpace = drives.Where(d => d.IsReady && d.Name == root).Sum(d => d.AvailableFreeSpace);
var totalUploadDirectorySize = new DirectoryInfo("Upload").EnumerateFiles().Sum(f => f.Length);
var totalSaveDirectorySize = new DirectoryInfo("Save").EnumerateFiles().Sum(f => f.Length);
Data = new object[]
{
new {
type = "Free",
value = totalAvailableFreeSpace
},
new {
type = "Games",
value = totalUploadDirectorySize
},
new
{
type = "Saves",
value = totalSaveDirectorySize
},
new
{
type = "Other",
value = totalStorageSize - totalAvailableFreeSpace - totalUploadDirectorySize - totalSaveDirectorySize
}
};
StateHasChanged();
}
}

View file

@ -0,0 +1,39 @@
@page "/"
@page "/Dashboard"
@using LANCommander.Pages.Dashboard.Charts
<PageHeader Title="Dashboard" Style="margin-bottom: 24px" />
<GridRow Gutter="(16, 16)">
<GridCol Sm="24" Md="12">
<Card Title="Network Upload Rate">
<Body>
<NetworkDownloadRate TimerHistory="60" TimerInterval="1000" />
</Body>
</Card>
</GridCol>
<GridCol Sm="24" Md="12">
<Card Title="Network Download Rate">
<Body>
<NetworkUploadRate TimerHistory="60" TimerInterval="1000" />
</Body>
</Card>
</GridCol>
<GridCol Sm="24" Md="12">
<Card Title="CPU Usage (%)">
<Body>
<ProcessorUtilization TimerHistory="60" TimerInterval="1000" />
</Body>
</Card>
</GridCol>
<GridCol Sm="24" Md="12">
<Card Title="Storage Usage">
<Body>
<StorageUsage />
</Body>
</Card>
</GridCol>
</GridRow>

View file

@ -0,0 +1,275 @@
@page "/Games/{id:guid}/Edit"
@page "/Games/Add"
@using LANCommander.Models;
@using System.IO.Compression;
@attribute [Authorize(Roles = "Administrator")]
@inject GameService GameService
@inject CompanyService CompanyService
@inject GenreService GenreService
@inject TagService TagService
@inject ArchiveService ArchiveService
@inject ScriptService ScriptService
@inject IMessageService MessageService
@inject ModalService ModalService
<Space Direction="DirectionVHType.Vertical" Size="@("large")" Style="width: 100%;">
<SpaceItem>
<Card Title="Game Details">
<Body>
<Form Model="@Game" Layout="@FormLayout.Vertical">
<FormItem Label="Title">
<GameMetadataLookup @ref="GameMetadataLookup" OnResultSelected="OnGameLookupResultSelected" />
<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" Disabled="@(String.IsNullOrWhiteSpace(context.Title))">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>
@if (context.Archives != null && context.Archives.Count > 0)
{
<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>
@if (Game.Id != Guid.Empty)
{
<SpaceItem>
<Card Title="Actions">
<Body>
<ActionEditor Game="Game" />
</Body>
</Card>
</SpaceItem>
<SpaceItem>
<Card Title="Multiplayer Modes">
<Body>
<MultiplayerModeEditor Game="Game" />
</Body>
</Card>
</SpaceItem>
<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>
<SpaceItem>
<Card Title="Scripts">
<Body>
<ScriptEditor Game="Game" />
</Body>
</Card>
</SpaceItem>
<SpaceItem>
<Card Title="Archives">
<ArchiveUploader Game="Game" />
</Card>
</SpaceItem>
}
</Space>
@code {
[Parameter] public Guid Id { get; set; }
bool Success;
string[] Errors = { };
IEnumerable<Company> Companies;
IEnumerable<Genre> Genres;
IEnumerable<Data.Models.Tag> Tags;
ArchiveBrowserDialog ArchiveBrowserDialog;
Modal FileSelectorModal;
private string value = "blazor";
private ConfirmRef _confirmRef;
private Game Game;
private KeysEditor? KeysEditor;
private GameMetadataLookup? GameMetadataLookup;
private int KeysAvailable { get {
return Game.Keys.Count(k =>
{
return (k.AllocationMethod == KeyAllocationMethod.MacAddress && String.IsNullOrWhiteSpace(k.ClaimedByMacAddress))
||
(k.AllocationMethod == KeyAllocationMethod.UserAccount && k.ClaimedByUser == null);
});
} }
protected override async Task OnInitializedAsync()
{
if (Id == Guid.Empty)
Game = new Game();
else
Game = await GameService.Get(Id);
Companies = CompanyService.Get();
Genres = GenreService.Get();
Tags = TagService.Get();
}
private async Task Save()
{
try
{
if (Game.Id != Guid.Empty)
{
Game = await GameService.Update(Game);
await MessageService.Success("Game updated!");
}
else
{
Game = await GameService.Add(Game);
await MessageService.Success("Game added!");
}
}
catch (Exception ex)
{
await MessageService.Error("Could not save!");
}
}
private async Task BrowseForIcon()
{
var modalOptions = new ModalOptions()
{
Title = "Choose Icon",
Maximizable = false,
DefaultMaximized = true,
Closable = true,
OkText = "Select File"
};
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) =>
{
Game.Icon = results.FirstOrDefault().FullName;
StateHasChanged();
return Task.CompletedTask;
};
}
private async Task OnGameLookupResultSelected(GameLookupResult result)
{
Game.Title = result.IGDBMetadata.Name;
Game.Description = result.IGDBMetadata.Summary;
Game.ReleasedOn = result.IGDBMetadata.FirstReleaseDate.GetValueOrDefault().UtcDateTime;
Game.MultiplayerModes = result.MultiplayerModes.ToList();
Game.Developers = new List<Company>();
Game.Publishers = new List<Company>();
Game.Genres = new List<Genre>();
Game.Tags = new List<Data.Models.Tag>();
if (result.IGDBMetadata.GameModes != null && result.IGDBMetadata.GameModes.Values != null)
Game.Singleplayer = result.IGDBMetadata.GameModes.Values.Any(gm => gm.Name == "Singleplayer");
if (result.IGDBMetadata.InvolvedCompanies != null && result.IGDBMetadata.InvolvedCompanies.Values != null)
{
// Make sure companie
var developers = result.IGDBMetadata.InvolvedCompanies.Values.Where(c => c.Developer.GetValueOrDefault()).Select(c => c.Company.Value.Name);
var publishers = result.IGDBMetadata.InvolvedCompanies.Values.Where(c => c.Publisher.GetValueOrDefault()).Select(c => c.Company.Value.Name);
foreach (var developer in developers)
{
Game.Developers.Add(await CompanyService.AddMissing(c => c.Name == developer, new Company { Name = developer }));
}
foreach (var publisher in publishers)
{
Game.Publishers.Add(await CompanyService.AddMissing(c => c.Name == publisher, new Company { Name = publisher }));
}
}
if (result.IGDBMetadata.Genres != null && result.IGDBMetadata.Genres.Values != null)
{
var genres = result.IGDBMetadata.Genres.Values.Select(g => g.Name);
foreach (var genre in genres)
{
Game.Genres.Add(await GenreService.AddMissing(g => g.Name == genre, new Genre { Name = genre }));
}
}
if (result.IGDBMetadata.Keywords != null && result.IGDBMetadata.Keywords.Values != null)
{
var tags = result.IGDBMetadata.Keywords.Values.Select(t => t.Name).Take(20);
foreach (var tag in tags)
{
Game.Tags.Add(await TagService.AddMissing(t => t.Name == tag, new Data.Models.Tag { Name = tag }));
}
}
}
private async Task CloseModal()
{
if (_confirmRef != null)
{
await _confirmRef.CloseAsync();
}
}
}

View file

@ -0,0 +1,67 @@
@page "/Games"
@attribute [Authorize]
@inject GameService GameService
@inject NavigationManager NavigationManager
<PageHeader Title="Games">
<PageHeaderExtra>
<Button OnClick="() => Add()" Type="@ButtonType.Primary">Add Game</Button>
</PageHeaderExtra>
</PageHeader>
<Table TItem="Game" DataSource="@Games" Loading="@Loading">
<Column TData="string">
<Image Src="@GetIcon(context)" Height="32" Width="32" Preview="false"></Image>
</Column>
<PropertyColumn Property="g => g.Title" Sortable />
<PropertyColumn Property="g => g.SortTitle" Sortable />
<PropertyColumn Property="g => g.ReleasedOn" Format="MM/dd/yyyy" Sortable />
<PropertyColumn Property="g => g.CreatedOn" Format="MM/dd/yyyy hh:mm tt" Sortable />
<PropertyColumn Property="g => g.CreatedBy" Sortable>
@context.CreatedBy?.UserName
</PropertyColumn>
<PropertyColumn Property="g => g.UpdatedOn" Format="MM/dd/yyyy hh:mm tt" Sortable />
<PropertyColumn Property="g => g.UpdatedBy" Sortable>
@context.UpdatedBy?.UserName
</PropertyColumn>
<ActionColumn Title="">
<Space>
<SpaceItem>
<Button OnClick="() => Edit(context)">Edit</Button>
</SpaceItem>
</Space>
</ActionColumn>
</Table>
@code {
IEnumerable<Game> Games { get; set; } = new List<Game>();
bool Loading = true;
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
Games = GameService.Get().OrderBy(g => String.IsNullOrWhiteSpace(g.SortTitle) ? g.Title : g.SortTitle).ToList();
Loading = false;
StateHasChanged();
}
}
private string GetIcon(Game game)
{
return $"/api/Games/{game.Id}/Icon.png";
}
private void Add()
{
NavigationManager.NavigateTo("/Games/Add");
}
private void Edit(Game game)
{
NavigationManager.NavigateTo($"/Games/{game.Id}/Edit");
}
}

View file

@ -0,0 +1,47 @@
@page "/Settings"
@page "/Settings/General"
@using LANCommander.Models;
@layout SettingsLayout
@inject SettingService SettingService
@inject IMessageService MessageService
<PageHeader Title="General" />
<div style="padding: 0 24px;">
<Text>IGDB Credentials</Text>
<Text>In order to use IGDB metadata, you need a Twitch developer account. <a href="https://api-docs.igdb.com/#account-creation" target="_blank">Click here</a> for more details.</Text>
<Form Model="Settings" Layout="@FormLayout.Vertical">
<FormItem Label="Client ID">
<Input @bind-Value="context.IGDBClientId" />
</FormItem>
<FormItem Label="Client Secret">
<InputPassword @bind-Value="context.IGDBClientSecret" />
</FormItem>
<FormItem>
<Button OnClick="Save" Icon="@IconType.Fill.Save">Save</Button>
</FormItem>
</Form>
</div>
@code {
private LANCommanderSettings Settings;
protected override async Task OnInitializedAsync()
{
Settings = SettingService.GetSettings();
}
private void Save()
{
try
{
SettingService.SaveSettings(Settings);
MessageService.Success("Settings saved!");
}
catch
{
MessageService.Error("An unknown error occurred.");
}
}
}

View file

@ -0,0 +1,21 @@
@inherits LayoutComponentBase
@layout MainLayout
<Layout Class="site-layout-background" Style="padding: 24px 0;">
<Sider Class="site-layout-background" Width="200">
<Menu Mode=@MenuMode.Inline Style="height: 100%">
<MenuItem RouterLink="/Settings/General">General</MenuItem>
<MenuItem RouterLink="/Settings/Users">Users</MenuItem>
</Menu>
</Sider>
<Content>
@Body
</Content>
</Layout>
<style>
.site-layout-background {
background: #fff;
}
</style>

View file

@ -0,0 +1,98 @@
@page "/Settings/Users"
@using LANCommander.Models;
@layout SettingsLayout
@inject UserManager<User> UserManager
@inject RoleManager<Role> RoleManager
@inject IMessageService MessageService
<PageHeader Title="Users" />
<div style="padding: 0 24px;">
<Table TItem="UserViewModel" DataSource="@UserList" Loading="@(Loading)">
<PropertyColumn Property="u => u.UserName" Title="Username" />
<PropertyColumn Property="u => u.Roles">
@String.Join(", ", context.Roles)
</PropertyColumn>
<PropertyColumn Property="u => u.SavesSize" Title="Saves">
@ByteSizeLib.ByteSize.FromBytes(context.SavesSize)
</PropertyColumn>
<ActionColumn>
<Space Style="display: flex; justify-content: end">
<SpaceItem>
@if (!context.Roles.Any(r => r == "Administrator"))
{
<Button OnClick="() => PromoteUser(context)" Type="@ButtonType.Primary">Promote</Button>
}
else
{
<Button OnClick="() => DemoteUser(context)" Danger>Demote</Button>
}
</SpaceItem>
</Space>
</ActionColumn>
</Table>
</div>
@code {
ICollection<UserViewModel> UserList { get; set; }
bool Loading = true;
protected override async Task OnInitializedAsync()
{
UserList = new List<UserViewModel>();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
await RefreshUserList();
}
private async Task RefreshUserList()
{
UserList = new List<UserViewModel>();
foreach (var user in UserManager.Users)
{
var savePath = Path.Combine("Save", user.Id.ToString());
long saveSize = 0;
if (Directory.Exists(savePath))
saveSize = new DirectoryInfo(savePath).EnumerateFiles().Sum(f => f.Length);
UserList.Add(new UserViewModel()
{
Id = user.Id,
UserName = user.UserName,
Roles = await UserManager.GetRolesAsync(user),
SavesSize = saveSize
});
}
Loading = false;
StateHasChanged();
}
private async Task PromoteUser(UserViewModel user)
{
await UserManager.AddToRoleAsync(UserManager.Users.First(u => u.UserName == user.UserName), "Administrator");
await RefreshUserList();
await MessageService.Success($"Promoted {user.UserName}!");
}
private async Task DemoteUser(UserViewModel user)
{
if (UserList.SelectMany(u => u.Roles).Count(r => r == "Administrator") == 1)
{
await MessageService.Error("Cannot demote the only administrator!");
}
else
{
await UserManager.RemoveFromRoleAsync(UserManager.Users.First(u => u.UserName == user.UserName), "Administrator");
await RefreshUserList();
}
}
}

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,34 @@
@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" />
<link href="_content/AntDesign/css/ant-design-blazor.css" rel="stylesheet" />
<link href="~/css/site.css" rel="stylesheet" />
</head>
<body>
<div class="page-wrapper">
@RenderBody()
</div>
<script src="~/_content/MudBlazor/MudBlazor.min.js"></script>
<script src="~/lib/antv/g2plot/dist/g2plot.js"></script>
<script src="~/_content/AntDesign/js/ant-design-blazor.js"></script>
<script src="~/_content/AntDesign.Charts/ant-design-charts-blazor.js"></script>
<script src="~/_framework/blazor.server.js"></script>
<script src="~/_content/BlazorMonaco/jsInterop.js"></script>
<script src="~/_content/BlazorMonaco/lib/monaco-editor/min/vs/loader.js"></script>
<script src="~/_content/BlazorMonaco/lib/monaco-editor/min/vs/editor/editor.main.js"></script>
<script src="~/js/site.js"></script>
</body>
</html>

View file

@ -7,6 +7,8 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using MudBlazor;
using MudBlazor.Services;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
@ -17,6 +19,16 @@ ConfigurationManager configuration = builder.Configuration;
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
var settings = SettingService.GetSettings();
builder.Services.AddMvc(options => options.EnableEndpointRouting = false);
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor().AddCircuitOptions(option =>
{
option.DetailedErrors = true;
}).AddHubOptions(option =>
{
option.MaximumReceiveMessageSize = 1024 * 1024 * 11;
});
builder.WebHost.ConfigureKestrel(options =>
{
// Configure as HTTP only
@ -61,11 +73,30 @@ builder.Services.AddAuthentication(options =>
};
});
builder.Services.AddControllersWithViews().AddJsonOptions(x =>
builder.Services.AddControllers().AddJsonOptions(x =>
{
x.JsonSerializerOptions.ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles;
});
builder.Services.AddServerSideBlazor();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddAntDesign();
builder.Services.AddMudServices(config =>
{
config.SnackbarConfiguration.PositionClass = Defaults.Classes.Position.BottomLeft;
config.SnackbarConfiguration.PreventDuplicates = false;
config.SnackbarConfiguration.NewestOnTop = false;
config.SnackbarConfiguration.ShowCloseIcon = true;
config.SnackbarConfiguration.VisibleStateDuration = 10000;
config.SnackbarConfiguration.HideTransitionDuration = 500;
config.SnackbarConfiguration.ShowTransitionDuration = 500;
config.SnackbarConfiguration.SnackbarVariant = Variant.Filled;
});
builder.Services.AddHttpClient();
builder.Services.AddScoped<SettingService>();
builder.Services.AddScoped<ArchiveService>();
@ -81,12 +112,16 @@ builder.Services.AddScoped<IGDBService>();
if (settings.Beacon)
builder.Services.AddHostedService<BeaconService>();
builder.WebHost.UseStaticWebAssets();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
app.UseSwagger();
app.UseSwaggerUI();
}
else
{
@ -103,18 +138,15 @@ app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.UseMvcWithDefaultRoute();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
endpoints.MapControllers();
});
app.MapRazorPages();
if (!Directory.Exists("Upload"))
Directory.CreateDirectory("Upload");

View file

@ -29,13 +29,18 @@ namespace LANCommander.Services
return key;
}
public async Task Release(Guid id)
public async Task<Key> Release(Guid id)
{
var key = await Get(id);
if (key == null)
return;
return null;
return await Release(key);
}
public async Task<Key> Release(Key key)
{
switch (key.AllocationMethod)
{
case KeyAllocationMethod.UserAccount:
@ -49,7 +54,7 @@ namespace LANCommander.Services
break;
}
await Update(key);
return await Update(key);
}
}
}

View file

@ -0,0 +1,17 @@
@inherits LayoutComponentBase
<Layout Class="layout">
<Header>
<div class="logo" style="background: url('/static/logo-dark.svg'); width: 143px; height: 31px; margin: 16px 24px 16px 0; float: left; background-size: contain;" />
<Menu Theme="MenuTheme.Dark" Mode="MenuMode.Horizontal">
<MenuItem RouterLink="/Dashboard">Dashboard</MenuItem>
<MenuItem RouterLink="/Games">Games</MenuItem>
<MenuItem RouterLink="/Settings">Settings</MenuItem>
</Menu>
</Header>
<Content Style="padding: 24px;">
@Body
</Content>
</Layout>

View file

@ -1,127 +0,0 @@
@model LANCommander.Data.Models.Archive
@{
ViewData["Title"] = "Add Archive";
}
<div class="container-xl">
<!-- Page title -->
<div class="page-header d-print-none">
<div class="row align-items-center">
<div class="col">
<div class="page-pretitle">@Model.Game.Title</div>
<h2 class="page-title">
Add Archive
</h2>
</div>
</div>
</div>
</div>
<div class="page-body">
<div class="container-xl">
<div class="row row-cards">
<div class="col-12">
<form asp-action="Add" enctype="multipart/form-data" class="card">
<fieldset>
<div class="card-body">
<div class="row">
<div class="col-12">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="mb-3">
<label asp-for="Version" class="control-label"></label>
<input asp-for="Version" class="form-control" />
<span asp-validation-for="Version" class="text-danger"></span>
@if (Model.LastVersion != null && !String.IsNullOrWhiteSpace(Model.LastVersion.Version))
{
<small class="form-hint">Last version: @Model.LastVersion.Version</small>
}
</div>
<div class="mb-3">
<label asp-for="Changelog" class="control-label"></label>
<textarea asp-for="Changelog" class="form-control"></textarea>
<span asp-validation-for="Changelog" class="text-danger"></span>
</div>
<div class="mb-3">
<label for="File" class="control-label">File</label>
<input type="file" id="File" class="form-control" />
</div>
<div>
<div class="progress h-4">
<div class="progress-bar" role="progressbar" style="width: 0%"></div>
</div>
</div>
<input type="hidden" asp-for="GameId" />
<input type="hidden" asp-for="LastVersion.Id" />
<input type="hidden" asp-for="ObjectKey" />
</div>
</div>
</div>
<div class="card-footer">
<div class="d-flex">
<a asp-action="Edit" asp-controller="Games" asp-route-id="@Model.Game.Id" class="btn btn-ghost-primary">Cancel</a>
<button class="btn btn-primary ms-auto" id="UploadButton">Upload</button>
</div>
</div>
</fieldset>
</form>
</div>
</div>
</div>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
<script src="~/js/Upload.js"></script>
<script>
var uploader = new Uploader();
uploader.Init('File', 'UploadButton');
uploader.OnStart = () => {
$('fieldset').prop('disabled', true);
$('.progress-bar')
.css('width', '0%')
.removeClass('bg-success')
.removeClass('bg-danger')
.addClass('progress-bar-striped')
.addClass('progress-bar-animated')
.text('0%');
};
uploader.OnComplete = (id, key) => {
$('#Id').val(id);
$('#ObjectKey').val(key);
$('.progress-bar')
.css('width', '100%')
.removeClass('progress-bar-striped')
.removeClass('progress-bar-animated')
.addClass('bg-success')
.text('Upload Complete!');
setTimeout(() => {
window.location.href = '@Url.Action("Edit", "Games", new { id = Model.Game.Id })';
}, 2000);
};
uploader.OnProgress = (percent) => {
$('.progress-bar')
.css('width', `${percent * 100}%`)
.text(`${Math.round(percent * 100)}%`);
};
uploader.OnError = () => {
$('fieldset').prop('disabled', false);
$('.progress-bar')
.css('width', '100%')
.removeClass('progress-bar-striped')
.removeClass('progress-bar-animated')
.addClass('bg-danger')
.text('Upload Error!');
};
</script>
}

View file

@ -1,32 +0,0 @@
@using LANCommander.Components;
@model LANCommander.Data.Models.Archive
@{
ViewData["Title"] = "Browse Archive";
}
<div class="container-xl">
<!-- Page title -->
<div class="page-header d-print-none">
<div class="row align-items-center">
<div class="col">
<div class="page-pretitle">@Model.Game.Title</div>
<h2 class="page-title">
Browse Archive
</h2>
</div>
</div>
</div>
</div>
<div class="page-body">
<div class="container-xl">
<div class="row row-cards">
<div class="col-12">
<div class="card">
<component type="typeof(ArchiveBrowser)" render-mode="Server" param-ArchiveId="Model.Id" />
</div>
</div>
</div>
</div>
</div>

View file

@ -1,43 +0,0 @@
@model LANCommander.Data.Models.Company
@{
ViewData["Title"] = "Create";
}
<h1>Create</h1>
<h4>Company</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Create">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Name" class="control-label"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="CreatedOn" class="control-label"></label>
<input asp-for="CreatedOn" class="form-control" />
<span asp-validation-for="CreatedOn" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="UpdatedOn" class="control-label"></label>
<input asp-for="UpdatedOn" class="form-control" />
<span asp-validation-for="UpdatedOn" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-action="Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

View file

@ -1,39 +0,0 @@
@model LANCommander.Data.Models.Company
@{
ViewData["Title"] = "Delete";
}
<h1>Delete</h1>
<h3>Are you sure you want to delete this?</h3>
<div>
<h4>Company</h4>
<hr />
<dl class="row">
<dt class = "col-sm-2">
@Html.DisplayNameFor(model => model.Name)
</dt>
<dd class = "col-sm-10">
@Html.DisplayFor(model => model.Name)
</dd>
<dt class = "col-sm-2">
@Html.DisplayNameFor(model => model.CreatedOn)
</dt>
<dd class = "col-sm-10">
@Html.DisplayFor(model => model.CreatedOn)
</dd>
<dt class = "col-sm-2">
@Html.DisplayNameFor(model => model.UpdatedOn)
</dt>
<dd class = "col-sm-10">
@Html.DisplayFor(model => model.UpdatedOn)
</dd>
</dl>
<form asp-action="Delete">
<input type="hidden" asp-for="Id" />
<input type="submit" value="Delete" class="btn btn-danger" /> |
<a asp-action="Index">Back to List</a>
</form>
</div>

View file

@ -1,36 +0,0 @@
@model LANCommander.Data.Models.Company
@{
ViewData["Title"] = "Details";
}
<h1>Details</h1>
<div>
<h4>Company</h4>
<hr />
<dl class="row">
<dt class = "col-sm-2">
@Html.DisplayNameFor(model => model.Name)
</dt>
<dd class = "col-sm-10">
@Html.DisplayFor(model => model.Name)
</dd>
<dt class = "col-sm-2">
@Html.DisplayNameFor(model => model.CreatedOn)
</dt>
<dd class = "col-sm-10">
@Html.DisplayFor(model => model.CreatedOn)
</dd>
<dt class = "col-sm-2">
@Html.DisplayNameFor(model => model.UpdatedOn)
</dt>
<dd class = "col-sm-10">
@Html.DisplayFor(model => model.UpdatedOn)
</dd>
</dl>
</div>
<div>
<a asp-action="Edit" asp-route-id="@Model?.Id">Edit</a> |
<a asp-action="Index">Back to List</a>
</div>

View file

@ -1,44 +0,0 @@
@model LANCommander.Data.Models.Company
@{
ViewData["Title"] = "Edit";
}
<h1>Edit</h1>
<h4>Company</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Edit">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Name" class="control-label"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<input type="hidden" asp-for="Id" />
<div class="form-group">
<label asp-for="CreatedOn" class="control-label"></label>
<input asp-for="CreatedOn" class="form-control" />
<span asp-validation-for="CreatedOn" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="UpdatedOn" class="control-label"></label>
<input asp-for="UpdatedOn" class="form-control" />
<span asp-validation-for="UpdatedOn" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-action="Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

View file

@ -1,47 +0,0 @@
@model IEnumerable<LANCommander.Data.Models.Company>
@{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Name)
</th>
<th>
@Html.DisplayNameFor(model => model.CreatedOn)
</th>
<th>
@Html.DisplayNameFor(model => model.UpdatedOn)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.CreatedOn)
</td>
<td>
@Html.DisplayFor(modelItem => item.UpdatedOn)
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
<a asp-action="Details" asp-route-id="@item.Id">Details</a> |
<a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
</td>
</tr>
}
</tbody>
</table>

View file

@ -1,125 +0,0 @@
@using LANCommander.Components
@model LANCommander.Models.GameViewModel
@{
ViewData["Title"] = "Add Game";
}
<div class="container-xl">
<!-- Page title -->
<div class="page-header d-print-none">
<div class="row align-items-center">
<div class="col">
<h2 class="page-title">
Add Game
</h2>
</div>
</div>
</div>
</div>
<div class="page-body">
<div class="container-xl">
<div class="row row-cards">
<div class="col-12">
<form asp-action="Add" class="card">
<div class="card-body">
<div class="row">
<div class="col-12">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="mb-3">
<label asp-for="Game.Title" class="control-label"></label>
<div class="input-group">
<input asp-for="Game.Title" class="form-control" />
<button class="btn" type="submit" asp-action="Lookup">Lookup</button>
</div>
<span asp-validation-for="Game.Title" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Game.SortTitle" class="control-label"></label>
<input asp-for="Game.SortTitle" class="form-control" />
<span asp-validation-for="Game.SortTitle" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Game.Icon" class="control-label"></label>
<input asp-for="Game.Icon" class="form-control" />
<span asp-validation-for="Game.Icon" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Game.Description" class="control-label"></label>
<textarea asp-for="Game.Description" class="form-control" data-bs-toggle="autosize"></textarea>
<span asp-validation-for="Game.Description" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Game.ReleasedOn" class="control-label"></label>
<input asp-for="Game.ReleasedOn" class="form-control" />
<span asp-validation-for="Game.ReleasedOn" class="text-danger"></span>
</div>
<div class="mb-3">
<label class="form-check">
<input asp-for="Game.Singleplayer" type="checkbox" class="form-check-input" />
<span class="form-check-label">Singleplayer</span>
<span class="form-check-description">Game has a singleplayer mode</span>
</label>
</div>
<div class="mb-3">
<label asp-for="Developers" class="control-label"></label>
<input type="text" class="developer-select" />
<select asp-for="SelectedDevelopers" class="d-none"></select>
</div>
<div class="mb-3">
<label asp-for="Publishers" class="control-label"></label>
<input type="text" class="publisher-select" />
<select asp-for="SelectedPublishers" class="d-none"></select>
</div>
<div class="mb-3">
<label asp-for="Genres" class="control-label"></label>
<input type="text" class="genre-select" />
<select asp-for="SelectedGenres" class="d-none"></select>
</div>
<div class="mb-3">
<label asp-for="Tags" class="control-label"></label>
<input type="text" class="tag-select" />
<select asp-for="SelectedTags" class="d-none"></select>
</div>
</div>
</div>
</div>
<div class="card-header">
<h3 class="card-title">Actions</h3>
</div>
<component type="typeof(ActionEditor)" render-mode="Server" param-Actions="Model.Game.Actions.ToList()" param-GameId="Model.Game.Id" />
<div class="card-header">
<h3 class="card-title">Multiplayer Modes</h3>
</div>
<component type="typeof(MultiplayerModeEditor)" render-mode="Server" param-MultiplayerModes="Model.Game.MultiplayerModes" param-GameId="Model.Game.Id" />
<div class="card-footer">
<div class="d-flex">
<a asp-action="Index" class="btn btn-ghost-primary">Cancel</a>
<button type="submit" class="btn btn-primary ms-auto">Save</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
@section Scripts {
@{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); }
<script>
new Select('.developer-select', @Html.Raw(Json.Serialize(Model.Developers)));
new Select('.publisher-select', @Html.Raw(Json.Serialize(Model.Publishers)));
new Select('.genre-select', @Html.Raw(Json.Serialize(Model.Genres)));
new Select('.tag-select', @Html.Raw(Json.Serialize(Model.Tags)));
</script>
}

View file

@ -1,65 +0,0 @@
@model LANCommander.Data.Models.Game
@{
ViewData["Title"] = "Delete";
}
<div class="container container-tight py-4">
<div class="page-header">
<div class="row align-items-center">
<div class="col">
<h2 class="page-title">Delete @Model.Title?</h2>
</div>
</div>
</div>
<div class="card">
<div class="card-body">
<p class="text-muted">Are you sure you want to delete this game?
@if (Model.Archives != null && Model.Archives.Count > 0)
{
<span>It will also delete the following archives:</span>
}
</p>
</div>
@if (Model.Archives != null && Model.Archives.Count > 0)
{
<div class="table-responsive">
<table class="table table-vcenter table-mobile-md card-table">
<thead>
<tr>
<th>Version</th>
<th>Uploaded By</th>
<th>Uploaded On</th>
<th>Size</th>
</tr>
</thead>
<tbody>
@foreach (var archive in Model.Archives.OrderByDescending(a => a.CreatedOn))
{
<tr>
<td>@Html.DisplayFor(m => archive.Version)</td>
<td>@Html.DisplayFor(m => archive.CreatedBy.UserName)</td>
<td>@Html.DisplayFor(m => archive.CreatedOn)</td>
<td>@ByteSizeLib.ByteSize.FromBytes(archive.CompressedSize)</td>
</tr>
}
</tbody>
</table>
</div>
}
<div class="card-footer">
<div class="d-flex justify-content-between">
<a asp-action="Index" class="btn btn-ghost-primary">Cancel</a>
<form asp-action="Delete">
<input type="hidden" asp-for="Id" />
<button type="submit" class="btn btn-danger ms-auto">Delete</button>
</form>
</div>
</div>
</div>
</div>

View file

@ -1,303 +0,0 @@
@using LANCommander.Components
@using LANCommander.Data.Models
@model LANCommander.Models.GameViewModel
@{
ViewData["Title"] = "Edit";
}
<div class="container-xl">
<!-- Page title -->
<div class="page-header d-print-none">
<div class="row align-items-center">
<div class="col">
<h2 class="page-title">
Edit Game
</h2>
</div>
</div>
</div>
</div>
<div class="page-body">
<div class="container-xl">
<div class="row row-cards">
<div class="col-12">
<form asp-action="Edit" class="card">
<div class="card-body">
<div class="row">
<div class="col-12">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="mb-3">
<label asp-for="Game.Title" class="control-label"></label>
<div class="input-group">
<input asp-for="Game.Title" class="form-control" />
<button class="btn" type="submit" asp-action="Lookup">Lookup</button>
</div>
<span asp-validation-for="Game.Title" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Game.SortTitle" class="control-label"></label>
<input asp-for="Game.SortTitle" class="form-control" />
<span asp-validation-for="Game.SortTitle" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Game.Icon" class="control-label"></label>
<input asp-for="Game.Icon" class="form-control" />
<span asp-validation-for="Game.Icon" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Game.Description" class="control-label"></label>
<textarea asp-for="Game.Description" class="form-control" data-bs-toggle="autosize"></textarea>
<span asp-validation-for="Game.Description" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Game.ReleasedOn" class="control-label"></label>
<input asp-for="Game.ReleasedOn" class="form-control" />
<span asp-validation-for="Game.ReleasedOn" class="text-danger"></span>
</div>
<div class="mb-3">
<label class="form-check">
<input asp-for="Game.Singleplayer" type="checkbox" class="form-check-input" />
<span class="form-check-label">Singleplayer</span>
<span class="form-check-description">Game has a singleplayer mode</span>
</label>
</div>
<div class="mb-3">
<label asp-for="Developers" class="control-label"></label>
<input type="text" class="developer-select" />
<select asp-for="SelectedDevelopers" class="d-none"></select>
</div>
<div class="mb-3">
<label asp-for="Publishers" class="control-label"></label>
<input type="text" class="publisher-select" />
<select asp-for="SelectedPublishers" class="d-none"></select>
</div>
<div class="mb-3">
<label asp-for="Genres" class="control-label"></label>
<input type="text" class="genre-select" />
<select asp-for="SelectedGenres" class="d-none"></select>
</div>
<div class="mb-3">
<label asp-for="Tags" class="control-label"></label>
<input type="text" class="tag-select" />
<select asp-for="SelectedTags" class="d-none"></select>
</div>
<input type="hidden" asp-for="Game.Id" />
</div>
</div>
</div>
<div class="card-header">
<h3 class="card-title">Actions</h3>
</div>
<component type="typeof(ActionEditor)" render-mode="Server" param-Actions="Model.Game.Actions.ToList()" param-GameId="Model.Game.Id" />
<div class="card-header">
<h3 class="card-title">Multiplayer Modes</h3>
</div>
<component type="typeof(MultiplayerModeEditor)" render-mode="Server" param-MultiplayerModes="Model.Game.MultiplayerModes" param-GameId="Model.Game.Id" />
<div class="card-footer">
<div class="d-flex">
<a asp-action="Index" class="btn btn-ghost-primary">Cancel</a>
<button type="submit" class="btn btn-primary ms-auto">Save</button>
</div>
</div>
</form>
</div>
<div class="col-12">
<div class="card">
@if (Model.Game.Keys != null && Model.Game.Keys.Count > 0)
{
<div class="card-header">
<h3 class="card-title">Keys</h3>
<div class="card-actions">
<a asp-action="Details" asp-controller="Keys" asp-route-id="@Model.Game.Id" class="btn btn-ghost-primary">
Details
</a>
</div>
</div>
var keysAvailable = Model.Game.Keys.Count(k =>
{
return (k.AllocationMethod == KeyAllocationMethod.MacAddress && String.IsNullOrWhiteSpace(k.ClaimedByMacAddress)) ||
(k.AllocationMethod == KeyAllocationMethod.UserAccount && k.ClaimedByUser == null);
});
<div class="card-body">
<div class="datagrid text-center">
<div class="datagrid-item">
<div class="datagrid-title">Available</div>
<div class="datagrid-content">
@keysAvailable
</div>
</div>
<div class="datagrid-item">
<div class="datagrid-title">Claimed</div>
<div class="datagrid-content">
@(Model.Game.Keys.Count - keysAvailable)
</div>
</div>
<div class="datagrid-item">
<div class="datagrid-title">Total</div>
<div class="datagrid-content">
@Model.Game.Keys.Count
</div>
</div>
</div>
</div>
}
else
{
<div class="empty">
<p class="empty-title">No Keys</p>
<p class="empty-subtitle text-muted">There have been no keys added for this game.</p>
<div class="empty-action">
<a asp-action="Edit" asp-controller="Keys" asp-route-id="@Model.Game.Id" class="btn btn-primary">Edit Keys</a>
</div>
</div>
}
</div>
</div>
<div class="col-12">
<div class="card">
@if (Model.Game.Archives != null && Model.Game.Archives.Count > 0)
{
<div class="card-header">
<h3 class="card-title">Archives</h3>
<div class="card-actions">
<a asp-action="Add" asp-controller="Archives" asp-route-id="@Model.Game.Id" class="btn btn-primary">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
Add
</a>
</div>
</div>
<div class="table-responsive">
<table class="table table-vcenter table-mobile-md card-table">
<thead>
<tr>
<th>Version</th>
<th>Uploaded By</th>
<th>Uploaded On</th>
<th>Size</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var archive in Model.Game.Archives.OrderByDescending(a => a.CreatedOn))
{
<tr>
<td>@Html.DisplayFor(m => archive.Version)</td>
<td>@Html.DisplayFor(m => archive.CreatedBy.UserName)</td>
<td>@Html.DisplayFor(m => archive.CreatedOn)</td>
<td>@ByteSizeLib.ByteSize.FromBytes(new FileInfo(System.IO.Path.Combine("Upload", archive.ObjectKey)).Length)</td>
<td>
<div class="btn-list flex-nowrap justify-content-end">
<a asp-action="Download" asp-controller="Archives" asp-route-id="@archive.Id" class="btn">Download</a>
<a asp-action="Browse" asp-controller="Archives" asp-route-id="@archive.Id" class="btn">Browse</a>
<a asp-action="Delete" asp-controller="Archives" asp-route-id="@archive.Id" class="btn btn-danger">Delete</a>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
}
else
{
<div class="empty">
<p class="empty-title">No Archives</p>
<p class="empty-subtitle text-muted">There have been no archives uploaded for this game.</p>
<div class="empty-action">
<a asp-action="Add" asp-controller="Archives" asp-route-id="@Model.Game.Id" class="btn btn-primary">Upload Archive</a>
</div>
</div>
}
</div>
</div>
<div class="col-12">
<div class="card">
@if (Model.Game.Scripts != null && Model.Game.Scripts.Count > 0)
{
<div class="card-header">
<h3 class="card-title">Scripts</h3>
<div class="card-actions">
<a asp-action="Add" asp-controller="Scripts" asp-route-id="@Model.Game.Id" class="btn btn-primary">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
Add
</a>
</div>
</div>
<div class="table-responsive">
<table class="table table-vcenter table-mobile-md card-table">
<thead>
<tr>
<th>Type</th>
<th>Name</th>
<th>Created On</th>
<th>Created By</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var script in Model.Game.Scripts.OrderBy(s => s.Type).ThenByDescending(s => s.CreatedOn))
{
<tr>
<td>@Html.DisplayFor(m => script.Type)</td>
<td>@Html.DisplayFor(m => script.Name)</td>
<td>@Html.DisplayFor(m => script.CreatedOn)</td>
<td>@Html.DisplayFor(m => script.CreatedBy.UserName)</td>
<td>
<div class="btn-list flex-nowrap justify-content-end">
<a asp-action="Edit" asp-controller="Scripts" asp-route-id="@script.Id" class="btn">Edit</a>
<a asp-action="Delete" asp-controller="Scripts" asp-route-id="@script.Id" class="btn btn-danger">Delete</a>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
}
else
{
<div class="empty">
<p class="empty-title">No Scripts</p>
<p class="empty-subtitle text-muted">There have been no scripts added for this game.</p>
<div class="empty-action">
<a asp-action="Add" asp-controller="Scripts" asp-route-id="@Model.Game.Id" class="btn btn-primary">Add Script</a>
</div>
</div>
}
</div>
</div>
</div>
</div>
</div>
@section Scripts {
@{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); }
<script>
new Select('.developer-select', @Html.Raw(Json.Serialize(Model.Developers)));
new Select('.publisher-select', @Html.Raw(Json.Serialize(Model.Publishers)));
new Select('.genre-select', @Html.Raw(Json.Serialize(Model.Genres)));
new Select('.tag-select', @Html.Raw(Json.Serialize(Model.Tags)));
</script>
}

View file

@ -1,110 +0,0 @@
@model IEnumerable<LANCommander.Data.Models.Game>
@{
ViewData["Title"] = "Games";
}
<div class="container-xl">
<div class="page-header d-print-none">
<div class="row align-items-center">
<div class="col">
<h2 class="page-title">
Games
</h2>
</div>
<div class="col-auto ms-auto">
<div class="btn-list">
<a asp-action="Add" class="btn btn-primary d-none d-sm-inline-block">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
Add
</a>
</div>
</div>
</div>
</div>
</div>
<div class="page-body">
<div class="container-xl">
<div class="row row-cards">
<div class="col-12">
<div class="card">
<div class="table-responsive">
<table class="table table-vcenter table-mobile-md card-table">
<thead>
<tr>
<th></th>
<th>
@Html.DisplayNameFor(model => model.Title)
</th>
<th>
@Html.DisplayNameFor(model => model.SortTitle)
</th>
<th>
@Html.DisplayNameFor(model => model.ReleasedOn)
</th>
<th>
@Html.DisplayNameFor(model => model.CreatedOn)
</th>
<th>
@Html.DisplayNameFor(model => model.CreatedBy)
</th>
<th>
@Html.DisplayNameFor(model => model.UpdatedOn)
</th>
<th>
@Html.DisplayNameFor(model => model.UpdatedBy)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.OrderBy(g => !String.IsNullOrWhiteSpace(g.SortTitle) ? g.SortTitle : g.Title))
{
<tr>
<td>
@if (!String.IsNullOrWhiteSpace(item.Icon)) {
<img src="@Url.Action("GetIcon", "Games", new { id = item.Id })" />
}
</td>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.SortTitle)
</td>
<td>
@if (item.ReleasedOn.HasValue)
{
@item.ReleasedOn.Value.ToString("MM/dd/yyyy")
}
</td>
<td>
@Html.DisplayFor(modelItem => item.CreatedOn)
</td>
<td>
@Html.DisplayFor(modelItem => item.CreatedBy.UserName)
</td>
<td>
@Html.DisplayFor(modelItem => item.UpdatedOn)
</td>
<td>
@Html.DisplayFor(modelItem => item.UpdatedBy.UserName)
</td>
<td>
<div class="btn-list flex-nowrap justify-content-end">
<a asp-action="Edit" asp-route-id="@item.Id" class="btn">Edit</a>
<a asp-action="Delete" asp-route-id="@item.Id" class="btn btn-danger">Delete</a>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>

View file

@ -1,99 +0,0 @@
@model LANCommander.Models.GameLookupResultsViewModel
@{
ViewData["Title"] = "Games";
}
<div class="container-xl">
<div class="page-header d-print-none">
<div class="row align-items-center">
<div class="col">
<div class="page-pretitle">@Model.Search</div>
<h2 class="page-title">
Game Lookup
</h2>
</div>
</div>
</div>
</div>
<div class="page-body">
<div class="container-xl">
<div class="row row-cards">
<div class="col-12">
<div class="card">
<div class="card-header">
<h3 class="card-title">Results</h3>
</div>
@if (Model.Results.Count() == 0)
{
<div class="card-body">
<p>No games could be found with the search "@Model.Search".</p>
</div>
}
else
{
<div class="card-body">
@if (Model.Results.Count() > 1)
{
<p>There was a total of @Model.Results.Count() games that matched the search "@Model.Search" in IGDB's database.</p>
}
else
{
<p>Only one game matched the search "@Model.Search" in IGDB's database.</p>
}
</div>
<form>
<div class="table-responsive">
<table class="table table-vcenter table-mobile-md card-table">
<thead>
<tr>
<th>
Title
</th>
<th>
Release Date
</th>
<th>
Developers
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Results)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.ReleasedOn)
</td>
<td>
@String.Join(", ", item.Developers.Select(d => d.Name))
</td>
<td>
<div class="btn-list flex-nowrap justify-content-end">
<a asp-action="Add" asp-route-igdbid="@item.IGDBId" class="btn btn-ghost-primary">Select</a>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
</form>
}
<div class="card-footer">
<div class="d-flex">
<a class="btn btn-ghost-primary" asp-action="Add" asp-controller="Games">Go Back</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View file

@ -1,66 +0,0 @@
@model LANCommander.Models.DashboardViewModel
@using ByteSizeLib
@{
ViewData["Title"] = "Home Page";
}
<div class="container-xl">
<div class="page-header">
<div class="row align-items-center">
<div class="col">
<div class="page-pretitle">Overview</div>
<h2 class="page-title">Dashboard</h2>
</div>
</div>
</div>
</div>
<div class="page-body">
<div class="container-xl">
<div class="row row-cards">
<div class="col-sm-4">
<div class="card">
<div class="card-body p-2 text-center">
<div class="h1 m-0">@Model.GameCount</div>
<div class="text-muted">Games</div>
</div>
</div>
</div>
<div class="col-sm-8">
<div class="card">
<div class="card-body">
<p class="mb-3">Storage Used: <strong>@(Model.TotalOtherSize + Model.TotalUploadDirectorySize) of @Model.TotalStorageSize</strong></p>
<div class="progress progress-separated mb-3">
<div class="progress-bar bg-primary" role="progressbar" style="width: @Math.Round((Model.TotalUploadDirectorySize.Bytes / Model.TotalStorageSize.Bytes) * 100)%;"></div>
<div class="progress-bar bg-dark" role="progressbar" style="width: @Math.Round((Model.TotalSaveDirectorySize.Bytes / Model.TotalStorageSize.Bytes) * 100)%;"></div>
<div class="progress-bar bg-info" role="progressbar" style="width: @Math.Round((Model.TotalOtherSize.Bytes / Model.TotalStorageSize.Bytes) * 100)%;"></div>
<div class="progress-bar bg-success" role="progressbar" style="width: @Math.Round((Model.TotalAvailableFreeSpace.Bytes / Model.TotalStorageSize.Bytes) * 100)%;"></div>
</div>
<div class="row">
<div class="col-auto d-flex align-items-center pe-2">
<span class="legend me-2 bg-primary"></span>
<span>Games</span>
<span class="d-none d-md-inline d-lg-none d-xxl-inline ms-2 text-muted">@Model.TotalUploadDirectorySize</span>
</div>
<div class="col-auto d-flex align-items-center pe-2">
<span class="legend me-2 bg-dark"></span>
<span>Saves</span>
<span class="d-none d-md-inline d-lg-none d-xxl-inline ms-2 text-muted">@Model.TotalSaveDirectorySize</span>
</div>
<div class="col-auto d-flex align-items-center pe-2">
<span class="legend me-2 bg-info"></span>
<span>Other</span>
<span class="d-none d-md-inline d-lg-none d-xxl-inline ms-2 text-muted">@Model.TotalOtherSize</span>
</div>
<div class="col-auto d-flex align-items-center pe-2">
<span class="legend me-2 bg-success"></span>
<span>Free</span>
<span class="d-none d-md-inline d-lg-none d-xxl-inline ms-2 text-muted">@Model.TotalAvailableFreeSpace</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View file

@ -1,6 +0,0 @@
@{
ViewData["Title"] = "Privacy Policy";
}
<h1>@ViewData["Title"]</h1>
<p>Use this page to detail your site's privacy policy.</p>

View file

@ -1,106 +0,0 @@
@using LANCommander.Data.Models
@model LANCommander.Data.Models.Game
@{
ViewData["Title"] = "Keys | " + Model.Title;
}
<div class="container-xl">
<!-- Page title -->
<div class="page-header d-print-none">
<div class="row align-items-center">
<div class="col">
<div class="page-pretitle">@Model.Title</div>
<h2 class="page-title">
Keys
</h2>
</div>
<div class="col-auto ms-auto">
<div class="btn-list">
<a asp-action="Edit" asp-controller="Games" asp-route-id="@Model.Id" class="btn btn-ghost-primary">Back</a>
<a asp-action="Edit" asp-route-id="@Model.Id" class="btn btn-primary d-none d-sm-inline-block">Edit</a>
</div>
</div>
</div>
</div>
</div>
<div class="page-body">
<div class="container-xl">
<div class="row row-cards">
<div class="col-12">
<form asp-action="Edit" class="card">
@if (Model.Keys != null && Model.Keys.Count > 0)
{
<div class="table-responsive">
<table class="table table-vcenter table-mobile-md card-table">
<thead>
<tr>
<th>Key</th>
<th>Allocation Method</th>
<th>Claimed By</th>
<th>Claimed On</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var key in Model.Keys.OrderByDescending(k => k.ClaimedOn))
{
<tr>
<td class="game-key">@Html.DisplayFor(m => key.Value)</td>
<td>@Html.DisplayFor(m => key.AllocationMethod)</td>
<td>
@switch (key.AllocationMethod)
{
case KeyAllocationMethod.MacAddress:
<text>@key.ClaimedByMacAddress</text>
break;
case KeyAllocationMethod.UserAccount:
<text>@key.ClaimedByUser?.UserName</text>
break;
}
</td>
<td>@key.ClaimedOn</td>
<td>
<div class="btn-list flex-nowrap justify-content-end">
@if ((key.AllocationMethod == KeyAllocationMethod.MacAddress && !String.IsNullOrWhiteSpace(key.ClaimedByMacAddress)) || (key.AllocationMethod == KeyAllocationMethod.UserAccount && key.ClaimedByUser != null))
{
<a asp-action="Release" asp-controller="Keys" asp-route-id="@key.Id" class="btn btn-sm btn-ghost-dark">Release</a>
}
<a asp-action="Delete" asp-controller="Keys" asp-route-id="@key.Id" class="btn btn-sm btn-ghost-danger">Delete</a>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
}
else
{
<div class="empty">
<p class="empty-title">No Key</p>
<p class="empty-subtitle text-muted">There have been no keys added for this game.</p>
<div class="empty-action">
<a asp-action="Edit" asp-controller="Keys" asp-route-id="@Model.Id" class="btn btn-primary">Edit Keys</a>
</div>
</div>
}
</form>
</div>
</div>
</div>
</div>
<style>
.game-key {
font-family: var(--tblr-font-monospace);
}
</style>
@section Scripts {
@{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); }
}

View file

@ -1,73 +0,0 @@
@model LANCommander.Models.EditKeysViewModel
@{
ViewData["Title"] = "Edit Keys | " + Model.Game.Title;
}
<div class="container-xl">
<!-- Page title -->
<div class="page-header d-print-none">
<div class="row align-items-center">
<div class="col">
<div class="page-pretitle">@Model.Game.Title</div>
<h2 class="page-title">
Edit Keys
</h2>
</div>
</div>
</div>
</div>
<div class="page-body">
<div class="container-xl">
<div class="row row-cards">
<div class="col-12">
<form asp-action="Edit" class="card">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="Keys" />
<input type="hidden" asp-for="Game.Id" />
<div id="KeyEditor" style="height: 100%; min-height: 600px;"></div>
<div class="card-footer">
<div class="d-flex">
<a asp-action="Details" asp-route-id="@Model.Game.Id" class="btn btn-ghost-primary">Cancel</a>
<button type="submit" class="btn btn-primary ms-auto">Save</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
@section Scripts {
@{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); }
<script src="~/lib/monaco-editor/min/vs/loader.js"></script>
<script>
require.config({ paths: { vs: '/lib/monaco-editor/min/vs' } });
require(['vs/editor/editor.main'], function () {
var editor = monaco.editor.create(document.getElementById('KeyEditor'), {
value: $('#Keys').val(),
readOnly: false,
theme: 'vs-dark',
automaticLayout: true
});
editor.onDidChangeModelContent(function (e) {
$('#Keys').val(editor.getModel().getValue());
});
});
document.addEventListener('keydown', e => {
if (e.ctrlKey && e.key === 's') {
e.preventDefault();
$('form').submit();
}
});
</script>
}

View file

@ -1,125 +0,0 @@
@using LANCommander.Components;
@using LANCommander.Data.Enums
@model LANCommander.Data.Models.Script
@{
ViewData["Title"] = "Add Script | " + Model.Game.Title;
}
<div class="container-xl">
<!-- Page title -->
<div class="page-header d-print-none">
<div class="row align-items-center">
<div class="col">
<div class="page-pretitle">@Model.Game.Title</div>
<h2 class="page-title">
Add Script
</h2>
</div>
</div>
</div>
</div>
<div class="page-body">
<div class="container-xl">
<div class="row row-cards">
<div class="col-12">
<form asp-action="Add" class="card">
<div class="card-body pb-0">
<div class="row">
<div class="col-12">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
</div>
</div>
<div class="row">
<div class="col-3">
<div class="mb-3">
<label asp-for="Type" class="control-label"></label>
<select asp-for="Type" class="form-control" asp-items="Html.GetEnumSelectList<ScriptType>()"></select>
<span asp-validation-for="Type" class="text-danger"></span>
</div>
</div>
<div class="col-9">
<div class="mb-3">
<label asp-for="Name" class="control-label"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="mb-3">
<label asp-for="Description" class="control-label"></label>
<textarea asp-for="Description" class="form-control"></textarea>
<span asp-validation-for="Description" class="text-danger"></span>
</div>
<div class="mb-3">
<label class="form-check">
<input asp-for="RequiresAdmin" type="checkbox" class="form-check-input" />
<span class="form-check-label">Requires Admin Privileges</span>
<span class="form-check-description">Marks the script as needing admin privileges. Recommended for any changes to the system e.g. Windows Registry.</span>
</label>
</div>
<input type="hidden" asp-for="Contents" />
<input type="hidden" asp-for="GameId" />
</div>
</div>
<div class="row">
<div class="col btn-list mb-3">
<component type="typeof(SnippetBar)" render-mode="Server" />
</div>
</div>
</div>
<div id="ScriptEditor" style="height: 100%; min-height: 600px;"></div>
<div class="card-footer">
<div class="d-flex">
<a asp-action="Edit" asp-controller="Games" asp-route-id="@Model.Game.Id" class="btn btn-ghost-primary">Cancel</a>
<button type="submit" class="btn btn-primary ms-auto">Save</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
@section Scripts {
@{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); }
<script src="~/lib/monaco-editor/min/vs/loader.js"></script>
<script>
window.Editor = {};
require.config({ paths: { vs: '/lib/monaco-editor/min/vs' } });
require(['vs/editor/editor.main'], function () {
window.Editor = monaco.editor.create(document.getElementById('ScriptEditor'), {
value: $('#Contents').val(),
language: 'powershell',
readOnly: false,
theme: 'vs-dark',
automaticLayout: true
});
window.Editor.onDidChangeModelContent(function (e) {
$('#Contents').val(window.Editor.getModel().getValue());
});
});
document.addEventListener('keydown', e => {
if (e.ctrlKey && e.key === 's') {
e.preventDefault();
$('form').submit();
}
});
</script>
}

View file

@ -1,126 +0,0 @@
@using LANCommander.Components;
@using LANCommander.Data.Enums
@model LANCommander.Data.Models.Script
@{
ViewData["Title"] = "Edit Script | " + Model.Game.Title;
}
<div class="container-xl">
<!-- Page title -->
<div class="page-header d-print-none">
<div class="row align-items-center">
<div class="col">
<div class="page-pretitle">@Model.Game.Title</div>
<h2 class="page-title">
Edit Script
</h2>
</div>
</div>
</div>
</div>
<div class="page-body">
<div class="container-xl">
<div class="row row-cards">
<div class="col-12">
<form asp-action="Edit" class="card">
<div class="card-body pb-0">
<div class="row">
<div class="col-12">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
</div>
</div>
<div class="row">
<div class="col-3">
<div class="mb-3">
<label asp-for="Type" class="control-label"></label>
<select asp-for="Type" class="form-control" asp-items="Html.GetEnumSelectList<ScriptType>()"></select>
<span asp-validation-for="Type" class="text-danger"></span>
</div>
</div>
<div class="col-9">
<div class="mb-3">
<label asp-for="Name" class="control-label"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="mb-3">
<label asp-for="Description" class="control-label"></label>
<textarea asp-for="Description" class="form-control"></textarea>
<span asp-validation-for="Description" class="text-danger"></span>
</div>
<div class="mb-3">
<label class="form-check">
<input asp-for="RequiresAdmin" type="checkbox" class="form-check-input" />
<span class="form-check-label">Requires Admin Privileges</span>
<span class="form-check-description">Marks the script as needing admin privileges. Recommended for any changes to the system e.g. Windows Registry.</span>
</label>
</div>
<input type="hidden" asp-for="Contents" />
<input type="hidden" asp-for="GameId" />
<input type="hidden" asp-for="Id" />
</div>
</div>
<div class="row">
<div class="col btn-list mb-3">
<component type="typeof(SnippetBar)" render-mode="Server" />
</div>
</div>
</div>
<div id="ScriptEditor" style="height: 100%; min-height: 600px;"></div>
<div class="card-footer">
<div class="d-flex">
<a asp-action="Edit" asp-controller="Games" asp-route-id="@Model.Game.Id" class="btn btn-ghost-primary">Cancel</a>
<button type="submit" class="btn btn-primary ms-auto">Save</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
@section Scripts {
@{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); }
<script src="~/lib/monaco-editor/min/vs/loader.js"></script>
<script>
window.Editor = {};
require.config({ paths: { vs: '/lib/monaco-editor/min/vs' } });
require(['vs/editor/editor.main'], function () {
window.Editor = monaco.editor.create(document.getElementById('ScriptEditor'), {
value: $('#Contents').val(),
language: 'powershell',
readOnly: false,
theme: 'vs-dark',
automaticLayout: true
});
window.Editor.onDidChangeModelContent(function (e) {
$('#Contents').val(window.Editor.getModel().getValue());
});
});
document.addEventListener('keydown', e => {
if (e.ctrlKey && e.key === 's') {
e.preventDefault();
$('form').submit();
}
});
</script>
}

View file

@ -1,61 +0,0 @@
@model LANCommander.Models.LANCommanderSettings
@{
ViewData["Title"] = "Settings | Users";
}
<div class="container-xl">
<!-- Page title -->
<div class="page-header d-print-none">
<div class="row align-items-center">
<div class="col">
<div class="page-pretitle">Settings</div>
<h2 class="page-title">
General
</h2>
</div>
</div>
</div>
</div>
<div class="page-body">
<div class="container-xl">
<div class="card">
<div class="row g-0">
@{
await Html.RenderPartialAsync("_SidebarPartial");
}
<form method="post" class="col d-flex flex-column">
<div class="card-body">
<h2 class="mb-4">General</h2>
<h3 class="card-title mt-4">IGDB Credentials</h3>
<p class="card-subtitle">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.</p>
<div class="row g-3">
<div class="col-md">
<label asp-for="IGDBClientId" class="form-label"></label>
<input asp-for="IGDBClientId" class="form-control" />
</div>
<div class="col-md">
<label asp-for="IGDBClientSecret" class="form-label"></label>
<input asp-for="IGDBClientSecret" class="form-control" />
</div>
</div>
</div>
<div class="card-footer bg-transparent mt-auto">
<div class="btn-list justify-content-end">
<button type="submit" class="btn btn-primary">Save</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
@section Scripts {
@{
await Html.RenderPartialAsync("_ValidationScriptsPartial");
}
}

View file

@ -1,86 +0,0 @@
@model IEnumerable<LANCommander.Models.UserViewModel>
@{
ViewData["Title"] = "Settings | Users";
}
<div class="container-xl">
<!-- Page title -->
<div class="page-header d-print-none">
<div class="row align-items-center">
<div class="col">
<div class="page-pretitle">Settings</div>
<h2 class="page-title">
Users
</h2>
</div>
</div>
</div>
</div>
<div class="page-body">
<div class="container-xl">
<div class="card">
<div class="row g-0">
@{
await Html.RenderPartialAsync("_SidebarPartial");
}
<div class="col d-flex flex-column">
<div class="card-body">
<h2 class="mb-4">Users</h2>
</div>
<div class="table-responsive">
<table class="table table-vcenter table-mobile-md card-table">
<thead>
<tr>
<th>Username</th>
<th>Role</th>
<th>Saves</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.OrderBy(u => u.UserName))
{
<tr>
<td>
@item.UserName
</td>
<td>
@String.Join(", ", item.Roles)
</td>
<td>
@ByteSizeLib.ByteSize.FromBytes(item.SavesSize)
</td>
<td>
<div class="btn-list flex-nowrap justify-content-end">
@if (!item.Roles.Any(r => r == "Administrator"))
{
<a asp-action="PromoteUser" asp-route-id="@item.Id" class="btn btn-ghost-primary">Promote</a>
}
else
{
<a asp-action="DemoteUser" asp-route-id="@item.Id" class="btn btn-ghost-primary">Demote</a>
}
<a asp-action="DeleteUser" asp-route-id="@item.Id" class="btn btn-danger">Delete</a>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
@section Scripts {
@{
await Html.RenderPartialAsync("_ValidationScriptsPartial");
}
}

View file

@ -1,8 +0,0 @@
<div class="col-3 d-none d-md-block border-end">
<div class="card-body">
<div class="list-group list-group-transparent">
<a asp-action="General" asp-controller="Settings" class="list-group-item list-group-item-action d-flex align-items-center">General</a>
<a asp-action="Users" asp-controller="Settings" class="list-group-item list-group-item-action d-flex align-items-center">Users</a>
</div>
</div>
</div>

View file

@ -4,17 +4,33 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - LANCommander</title>
<link href="~/css/tabler.min.css" rel="stylesheet" />
<link href="~/lib/selectize.js/css/selectize.bootstrap5.min.css" rel="stylesheet" />
<base href="~/" />
<link href="_content/AntDesign/css/ant-design-blazor.css" rel="stylesheet" />
<link href="~/css/site.css" rel="stylesheet" />
</head>
<body>
@RenderBody()
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/selectize.js/js/selectize.min.js"></script>
<script src="~/lib/tabler/core/dist/js/tabler.min.js"></script>
<script src="~/js/Modal.js"></script>
<script src="~/js/Select.js"></script>
<script src="~/lib/antv/g2plot/dist/g2plot.js"></script>
<script src="~/_content/AntDesign/js/ant-design-blazor.js"></script>
<script src="~/_content/AntDesign.Charts/ant-design-charts-blazor.js"></script>
<script src="~/js/site.js"></script>
<script>
$('input[type="checkbox"]').on('change', function() {
var checked = $(this).prop('checked');
if (checked) {
$(this).parents('.ant-checkbox-wrapper').addClass('ant-checkbox-wrapper-checked');
$(this).parents('.ant-checkbox').addClass('ant-checkbox-checked');
}
else {
$(this).parents('.ant-checkbox-wrapper').removeClass('ant-checkbox-wrapper-checked');
$(this).parents('.ant-checkbox').removeClass('ant-checkbox-checked');
}
});
</script>
@await RenderSectionAsync("Scripts", required: false)
</body>

View file

@ -1,43 +0,0 @@
@model LANCommander.Data.Models.Tag
@{
ViewData["Title"] = "Create";
}
<h1>Create</h1>
<h4>Tag</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Create">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Name" class="control-label"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="CreatedOn" class="control-label"></label>
<input asp-for="CreatedOn" class="form-control" />
<span asp-validation-for="CreatedOn" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="UpdatedOn" class="control-label"></label>
<input asp-for="UpdatedOn" class="form-control" />
<span asp-validation-for="UpdatedOn" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-action="Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

View file

@ -1,39 +0,0 @@
@model LANCommander.Data.Models.Tag
@{
ViewData["Title"] = "Delete";
}
<h1>Delete</h1>
<h3>Are you sure you want to delete this?</h3>
<div>
<h4>Tag</h4>
<hr />
<dl class="row">
<dt class = "col-sm-2">
@Html.DisplayNameFor(model => model.Name)
</dt>
<dd class = "col-sm-10">
@Html.DisplayFor(model => model.Name)
</dd>
<dt class = "col-sm-2">
@Html.DisplayNameFor(model => model.CreatedOn)
</dt>
<dd class = "col-sm-10">
@Html.DisplayFor(model => model.CreatedOn)
</dd>
<dt class = "col-sm-2">
@Html.DisplayNameFor(model => model.UpdatedOn)
</dt>
<dd class = "col-sm-10">
@Html.DisplayFor(model => model.UpdatedOn)
</dd>
</dl>
<form asp-action="Delete">
<input type="hidden" asp-for="Id" />
<input type="submit" value="Delete" class="btn btn-danger" /> |
<a asp-action="Index">Back to List</a>
</form>
</div>

View file

@ -1,36 +0,0 @@
@model LANCommander.Data.Models.Tag
@{
ViewData["Title"] = "Details";
}
<h1>Details</h1>
<div>
<h4>Tag</h4>
<hr />
<dl class="row">
<dt class = "col-sm-2">
@Html.DisplayNameFor(model => model.Name)
</dt>
<dd class = "col-sm-10">
@Html.DisplayFor(model => model.Name)
</dd>
<dt class = "col-sm-2">
@Html.DisplayNameFor(model => model.CreatedOn)
</dt>
<dd class = "col-sm-10">
@Html.DisplayFor(model => model.CreatedOn)
</dd>
<dt class = "col-sm-2">
@Html.DisplayNameFor(model => model.UpdatedOn)
</dt>
<dd class = "col-sm-10">
@Html.DisplayFor(model => model.UpdatedOn)
</dd>
</dl>
</div>
<div>
<a asp-action="Edit" asp-route-id="@Model?.Id">Edit</a> |
<a asp-action="Index">Back to List</a>
</div>

View file

@ -1,44 +0,0 @@
@model LANCommander.Data.Models.Tag
@{
ViewData["Title"] = "Edit";
}
<h1>Edit</h1>
<h4>Tag</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Edit">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Name" class="control-label"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<input type="hidden" asp-for="Id" />
<div class="form-group">
<label asp-for="CreatedOn" class="control-label"></label>
<input asp-for="CreatedOn" class="form-control" />
<span asp-validation-for="CreatedOn" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="UpdatedOn" class="control-label"></label>
<input asp-for="UpdatedOn" class="form-control" />
<span asp-validation-for="UpdatedOn" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-action="Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

View file

@ -1,47 +0,0 @@
@model IEnumerable<LANCommander.Data.Models.Tag>
@{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Name)
</th>
<th>
@Html.DisplayNameFor(model => model.CreatedOn)
</th>
<th>
@Html.DisplayNameFor(model => model.UpdatedOn)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.CreatedOn)
</td>
<td>
@Html.DisplayFor(modelItem => item.UpdatedOn)
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
<a asp-action="Details" asp-route-id="@item.Id">Details</a> |
<a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
</td>
</tr>
}
</tbody>
</table>

View file

@ -0,0 +1,15 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Identity
@using Microsoft.JSInterop
@using AntDesign
@using BlazorMonaco
@using BlazorMonaco.Editor
@using LANCommander.Components
@using LANCommander.Shared
@using LANCommander.Services
@using LANCommander.Data.Models

View file

@ -37,19 +37,27 @@
"min/vs/loader.js",
"min/vs/base/browser/ui/codicons/codicon/codicon.ttf"
]
},
{
"provider": "cdnjs",
"library": "tabler-icons@1.35.0",
"destination": "wwwroot/lib/tabler-icons/",
"files": [
"iconfont/tabler-icons.min.css",
"iconfont/fonts/tabler-icons.eot",
"iconfont/fonts/tabler-icons.ttf",
"iconfont/fonts/tabler-icons.woff",
"iconfont/fonts/tabler-icons.woff2"
]
},
{
"provider": "unpkg",
"library": "@antv/g2plot@1.1.28",
"destination": "wwwroot/lib/antv/g2plot/",
"files": [
"dist/g2plot.js",
"dist/g2plot.js.map"
]
}
,
{
"provider": "cdnjs",
"library": "tabler-icons@1.35.0",
"destination": "wwwroot/lib/tabler-icons/",
"files": [
"iconfont/tabler-icons.min.css",
"iconfont/fonts/tabler-icons.eot",
"iconfont/fonts/tabler-icons.ttf",
"iconfont/fonts/tabler-icons.woff",
"iconfont/fonts/tabler-icons.woff2"
]
}
]
}

View file

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

View file

@ -1,18 +0,0 @@
html {
font-size: 14px;
}
@media (min-width: 768px) {
html {
font-size: 16px;
}
}
html {
position: relative;
min-height: 100%;
}
body {
margin-bottom: 60px;
}

View file

@ -1,16 +0,0 @@
class Modal {
constructor(id) {
this.ElementId = id;
// @ts-ignore
this.Instance = new bootstrap.Modal(`#${this.ElementId}`, {
keyboard: false
});
}
Show(header, message) {
document.getElementById(`${this.ElementId}Header`).innerText = header;
document.getElementById(`${this.ElementId}Message`).innerText = message;
this.Instance.show();
}
}
const ErrorModal = new Modal('ErrorModal');
//# sourceMappingURL=Modal.js.map

View file

@ -1 +0,0 @@
{"version":3,"file":"Modal.js","sourceRoot":"","sources":["Modal.ts"],"names":[],"mappings":"AAAA,MAAM,KAAK;IAIP,YAAY,EAAU;QAClB,IAAI,CAAC,SAAS,GAAG,EAAE,CAAC;QAEpB,aAAa;QACb,IAAI,CAAC,QAAQ,GAAG,IAAI,SAAS,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,SAAS,EAAE,EAAE;YACtD,QAAQ,EAAE,KAAK;SAClB,CAAC,CAAC;IACP,CAAC;IAED,IAAI,CAAC,MAAc,EAAE,OAAe;QAChC,QAAQ,CAAC,cAAc,CAAC,GAAG,IAAI,CAAC,SAAS,QAAQ,CAAC,CAAC,SAAS,GAAG,MAAM,CAAC;QACtE,QAAQ,CAAC,cAAc,CAAC,GAAG,IAAI,CAAC,SAAS,SAAS,CAAC,CAAC,SAAS,GAAG,OAAO,CAAC;QAExE,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;IACzB,CAAC;CACJ;AAED,MAAM,UAAU,GAAG,IAAI,KAAK,CAAC,YAAY,CAAC,CAAC"}

View file

@ -1,22 +0,0 @@
class Modal {
Instance: any;
ElementId: string;
constructor(id: string) {
this.ElementId = id;
// @ts-ignore
this.Instance = new bootstrap.Modal(`#${this.ElementId}`, {
keyboard: false
});
}
Show(header: string, message: string) {
document.getElementById(`${this.ElementId}Header`).innerText = header;
document.getElementById(`${this.ElementId}Message`).innerText = message;
this.Instance.show();
}
}
const ErrorModal = new Modal('ErrorModal');

View file

@ -1,118 +0,0 @@
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
class Chunk {
constructor(start, end, index) {
this.Start = start;
this.End = end;
this.Index = index;
}
}
class Uploader {
constructor() {
this.InitRoute = "/Upload/Init";
this.ChunkRoute = "/Upload/Chunk";
this.ValidateRoute = "/Archives/Validate";
this.MaxChunkSize = 1024 * 1024 * 25;
}
Init(fileInputId, uploadButtonId) {
this.FileInput = document.getElementById("File");
this.UploadButton = document.getElementById("UploadButton");
this.VersionInput = document.getElementById("Version");
this.ChangelogTextArea = document.getElementById("Changelog");
this.LastVersionIdInput = document.getElementById("LastVersion_Id");
this.GameIdInput = document.getElementById("GameId");
this.ParentForm = this.FileInput.closest("form");
this.Chunks = [];
this.UploadButton.onclick = (e) => __awaiter(this, void 0, void 0, function* () {
yield this.OnUploadButtonClicked(e);
});
}
OnUploadButtonClicked(e) {
return __awaiter(this, void 0, void 0, function* () {
e.preventDefault();
this.OnStart();
this.File = this.FileInput.files.item(0);
this.TotalChunks = Math.ceil(this.File.size / this.MaxChunkSize);
var response = yield fetch(this.InitRoute, {
method: "POST"
});
const data = yield response.json();
if (response.ok) {
this.Key = data.key;
this.GetChunks();
try {
for (let chunk of this.Chunks) {
yield this.UploadChunk(chunk);
}
var isValid = yield this.Validate();
if (isValid)
this.OnComplete(this.Id, this.Key);
else
this.OnError();
}
catch (ex) {
this.OnError();
}
}
});
}
UploadChunk(chunk) {
return __awaiter(this, void 0, void 0, function* () {
let formData = new FormData();
formData.append('file', this.File.slice(chunk.Start, chunk.End + 1));
formData.append('start', chunk.Start.toString());
formData.append('end', chunk.End.toString());
formData.append('key', this.Key);
formData.append('total', this.File.size.toString());
console.info(`Uploading chunk ${chunk.Index}/${this.TotalChunks}...`);
let chunkResponse = yield fetch(this.ChunkRoute, {
method: "POST",
body: formData
});
if (!chunkResponse)
throw `Error uploading chunk ${chunk.Index}/${this.TotalChunks}`;
this.OnProgress(chunk.Index / this.TotalChunks);
});
}
Validate() {
return __awaiter(this, void 0, void 0, function* () {
let formData = new FormData();
formData.append('Version', this.VersionInput.value);
formData.append('Changelog', this.ChangelogTextArea.value);
formData.append('GameId', this.GameIdInput.value);
formData.append('ObjectKey', this.Key);
let validationResponse = yield fetch(`${this.ValidateRoute}/${this.Key}`, {
method: "POST",
body: formData
});
if (!validationResponse.ok) {
ErrorModal.Show("Archive Invalid", yield validationResponse.text());
return false;
}
let data = yield validationResponse.json();
if (data == null || data.Id === "") {
ErrorModal.Show("Upload Error", "Something interfered with the upload. Try again.");
return false;
}
this.Id = data.Id;
return true;
});
}
GetChunks() {
for (let currentChunk = 1; currentChunk <= this.TotalChunks; currentChunk++) {
let start = (currentChunk - 1) * this.MaxChunkSize;
let end = (currentChunk * this.MaxChunkSize) - 1;
if (currentChunk == this.TotalChunks)
end = this.File.size;
this.Chunks.push(new Chunk(start, end, currentChunk));
}
}
}
//# sourceMappingURL=Upload.js.map

View file

@ -1 +0,0 @@
{"version":3,"file":"Upload.js","sourceRoot":"","sources":["Upload.ts"],"names":[],"mappings":";;;;;;;;;AAAA,MAAM,KAAK;IAKP,YAAY,KAAa,EAAE,GAAW,EAAE,KAAa;QACjD,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;QACf,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACvB,CAAC;CACJ;AAED,MAAM,QAAQ;IAAd;QAaI,cAAS,GAAW,cAAc,CAAC;QACnC,eAAU,GAAW,eAAe,CAAC;QACrC,kBAAa,GAAW,oBAAoB,CAAC;QAE7C,iBAAY,GAAW,IAAI,GAAG,IAAI,GAAG,EAAE,CAAC;IAmI5C,CAAC;IA3HG,IAAI,CAAC,WAAmB,EAAE,cAAsB;QAC5C,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAC,cAAc,CAAC,MAAM,CAAqB,CAAC;QACrE,IAAI,CAAC,YAAY,GAAG,QAAQ,CAAC,cAAc,CAAC,cAAc,CAAsB,CAAC;QACjF,IAAI,CAAC,YAAY,GAAG,QAAQ,CAAC,cAAc,CAAC,SAAS,CAAqB,CAAC;QAC3E,IAAI,CAAC,iBAAiB,GAAG,QAAQ,CAAC,cAAc,CAAC,WAAW,CAAwB,CAAC;QACrF,IAAI,CAAC,kBAAkB,GAAG,QAAQ,CAAC,cAAc,CAAC,gBAAgB,CAAqB,CAAC;QACxF,IAAI,CAAC,WAAW,GAAG,QAAQ,CAAC,cAAc,CAAC,QAAQ,CAAqB,CAAC;QACzE,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAEjD,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC;QAEjB,IAAI,CAAC,YAAY,CAAC,OAAO,GAAG,CAAO,CAAC,EAAE,EAAE;YACpC,MAAM,IAAI,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC;QACxC,CAAC,CAAA,CAAA;IACL,CAAC;IAEK,qBAAqB,CAAC,CAAa;;YACrC,CAAC,CAAC,cAAc,EAAE,CAAC;YAEnB,IAAI,CAAC,OAAO,EAAE,CAAC;YAEf,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACzC,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC;YAEjE,IAAI,QAAQ,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,SAAS,EAAE;gBACvC,MAAM,EAAE,MAAM;aACjB,CAAC,CAAC;YAEH,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;YAEnC,IAAI,QAAQ,CAAC,EAAE,EAAE;gBACb,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC;gBAEpB,IAAI,CAAC,SAAS,EAAE,CAAC;gBAEjB,IAAI;oBACA,KAAK,IAAI,KAAK,IAAI,IAAI,CAAC,MAAM,EAAE;wBAC3B,MAAM,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;qBACjC;oBAED,IAAI,OAAO,GAAG,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAC;oBAEpC,IAAI,OAAO;wBACP,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;;wBAEnC,IAAI,CAAC,OAAO,EAAE,CAAC;iBACtB;gBACD,OAAO,EAAE,EAAE;oBACP,IAAI,CAAC,OAAO,EAAE,CAAC;iBAClB;aACJ;QACL,CAAC;KAAA;IAEK,WAAW,CAAC,KAAY;;YAC1B,IAAI,QAAQ,GAAG,IAAI,QAAQ,EAAE,CAAC;YAE9B,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;YACrE,QAAQ,CAAC,MAAM,CAAC,OAAO,EAAE,KAAK,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC;YACjD,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC;YAC7C,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;YACjC,QAAQ,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;YAEpD,OAAO,CAAC,IAAI,CAAC,mBAAmB,KAAK,CAAC,KAAK,IAAI,IAAI,CAAC,WAAW,KAAK,CAAC,CAAC;YAEtE,IAAI,aAAa,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,UAAU,EAAE;gBAC7C,MAAM,EAAE,MAAM;gBACd,IAAI,EAAE,QAAQ;aACjB,CAAC,CAAC;YAEH,IAAI,CAAC,aAAa;gBACd,MAAM,yBAAyB,KAAK,CAAC,KAAK,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YAErE,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,KAAK,GAAG,IAAI,CAAC,WAAW,CAAC,CAAC;QACpD,CAAC;KAAA;IAEK,QAAQ;;YACV,IAAI,QAAQ,GAAG,IAAI,QAAQ,EAAE,CAAC;YAE9B,QAAQ,CAAC,MAAM,CAAC,SAAS,EAAE,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;YACpD,QAAQ,CAAC,MAAM,CAAC,WAAW,EAAE,IAAI,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC;YAC3D,QAAQ,CAAC,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;YAClD,QAAQ,CAAC,MAAM,CAAC,WAAW,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;YAEvC,IAAI,kBAAkB,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,aAAa,IAAI,IAAI,CAAC,GAAG,EAAE,EAAE;gBACtE,MAAM,EAAE,MAAM;gBACd,IAAI,EAAE,QAAQ;aACjB,CAAC,CAAC;YAEH,IAAI,CAAC,kBAAkB,CAAC,EAAE,EAAE;gBACxB,UAAU,CAAC,IAAI,CAAC,iBAAiB,EAAE,MAAM,kBAAkB,CAAC,IAAI,EAAE,CAAC,CAAA;gBAEnE,OAAO,KAAK,CAAC;aAChB;YAED,IAAI,IAAI,GAAG,MAAM,kBAAkB,CAAC,IAAI,EAAE,CAAC;YAE3C,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE;gBAChC,UAAU,CAAC,IAAI,CAAC,cAAc,EAAE,kDAAkD,CAAC,CAAC;gBAEpF,OAAO,KAAK,CAAC;aAChB;YAED,IAAI,CAAC,EAAE,GAAG,IAAI,CAAC,EAAE,CAAC;YAElB,OAAO,IAAI,CAAC;QAChB,CAAC;KAAA;IAED,SAAS;QACL,KAAK,IAAI,YAAY,GAAG,CAAC,EAAE,YAAY,IAAI,IAAI,CAAC,WAAW,EAAE,YAAY,EAAE,EAAE;YACzE,IAAI,KAAK,GAAG,CAAC,YAAY,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,YAAY,CAAC;YACnD,IAAI,GAAG,GAAG,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC;YAEjD,IAAI,YAAY,IAAI,IAAI,CAAC,WAAW;gBAChC,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;YAEzB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,YAAY,CAAC,CAAC,CAAC;SACzD;IACL,CAAC;CAMJ"}

View file

@ -1,161 +0,0 @@
class Chunk {
Index: number;
Start: number;
End: number;
constructor(start: number, end: number, index: number) {
this.Start = start;
this.End = end;
this.Index = index;
}
}
class Uploader {
ParentForm: HTMLFormElement;
FileInput: HTMLInputElement;
UploadButton: HTMLButtonElement;
VersionInput: HTMLInputElement;
LastVersionIdInput: HTMLInputElement;
GameIdInput: HTMLInputElement;
ChangelogTextArea: HTMLTextAreaElement;
ObjectKeyInput: HTMLInputElement;
IdInput: HTMLInputElement;
File: File;
InitRoute: string = "/Upload/Init";
ChunkRoute: string = "/Upload/Chunk";
ValidateRoute: string = "/Archives/Validate";
MaxChunkSize: number = 1024 * 1024 * 25;
TotalChunks: number;
CurrentChunk: number;
Chunks: Chunk[];
Key: string;
Id: string;
Init(fileInputId: string, uploadButtonId: string) {
this.FileInput = document.getElementById("File") as HTMLInputElement;
this.UploadButton = document.getElementById("UploadButton") as HTMLButtonElement;
this.VersionInput = document.getElementById("Version") as HTMLInputElement;
this.ChangelogTextArea = document.getElementById("Changelog") as HTMLTextAreaElement;
this.LastVersionIdInput = document.getElementById("LastVersion_Id") as HTMLInputElement;
this.GameIdInput = document.getElementById("GameId") as HTMLInputElement;
this.ParentForm = this.FileInput.closest("form");
this.Chunks = [];
this.UploadButton.onclick = async (e) => {
await this.OnUploadButtonClicked(e);
}
}
async OnUploadButtonClicked(e: MouseEvent) {
e.preventDefault();
this.OnStart();
this.File = this.FileInput.files.item(0);
this.TotalChunks = Math.ceil(this.File.size / this.MaxChunkSize);
var response = await fetch(this.InitRoute, {
method: "POST"
});
const data = await response.json();
if (response.ok) {
this.Key = data.key;
this.GetChunks();
try {
for (let chunk of this.Chunks) {
await this.UploadChunk(chunk);
}
var isValid = await this.Validate();
if (isValid)
this.OnComplete(this.Id, this.Key);
else
this.OnError();
}
catch (ex) {
this.OnError();
}
}
}
async UploadChunk(chunk: Chunk) {
let formData = new FormData();
formData.append('file', this.File.slice(chunk.Start, chunk.End + 1));
formData.append('start', chunk.Start.toString());
formData.append('end', chunk.End.toString());
formData.append('key', this.Key);
formData.append('total', this.File.size.toString());
console.info(`Uploading chunk ${chunk.Index}/${this.TotalChunks}...`);
let chunkResponse = await fetch(this.ChunkRoute, {
method: "POST",
body: formData
});
if (!chunkResponse)
throw `Error uploading chunk ${chunk.Index}/${this.TotalChunks}`;
this.OnProgress(chunk.Index / this.TotalChunks);
}
async Validate(): Promise<boolean> {
let formData = new FormData();
formData.append('Version', this.VersionInput.value);
formData.append('Changelog', this.ChangelogTextArea.value);
formData.append('GameId', this.GameIdInput.value);
formData.append('ObjectKey', this.Key);
let validationResponse = await fetch(`${this.ValidateRoute}/${this.Key}`, {
method: "POST",
body: formData
});
if (!validationResponse.ok) {
ErrorModal.Show("Archive Invalid", await validationResponse.text())
return false;
}
let data = await validationResponse.json();
if (data == null || data.Id === "") {
ErrorModal.Show("Upload Error", "Something interfered with the upload. Try again.");
return false;
}
this.Id = data.Id;
return true;
}
GetChunks() {
for (let currentChunk = 1; currentChunk <= this.TotalChunks; currentChunk++) {
let start = (currentChunk - 1) * this.MaxChunkSize;
let end = (currentChunk * this.MaxChunkSize) - 1;
if (currentChunk == this.TotalChunks)
end = this.File.size;
this.Chunks.push(new Chunk(start, end, currentChunk));
}
}
OnStart: () => void;
OnComplete: (id: string, key: string) => void;
OnProgress: (percent: number) => void;
OnError: () => void;
}

View file

@ -2,3 +2,24 @@
// for details on configuring this project to bundle and minify static web assets.
// Write your JavaScript code.
function humanFileSize(bytes, si = false, dp = 1) {
const thresh = si ? 1000 : 1024;
if (Math.abs(bytes) < thresh) {
return bytes + ' B';
}
const units = si
? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
let u = -1;
const r = 10 ** dp;
do {
bytes /= thresh;
++u;
} while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1);
return bytes.toFixed(dp) + ' ' + units[u];
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show more