Merge branch 'blazor'
BIN
Docs/AddGame.gif
Normal file
After Width: | Height: | Size: 2.9 MiB |
BIN
Docs/ArchiveUploading.gif
Normal file
After Width: | Height: | Size: 1.7 MiB |
BIN
Docs/ChangeKey.png
Normal file
After Width: | Height: | Size: 647 KiB |
BIN
Docs/Dashboard.gif
Normal file
After Width: | Height: | Size: 178 KiB |
BIN
Docs/EditingScript.gif
Normal file
After Width: | Height: | Size: 1.9 MiB |
BIN
Docs/GamesList.png
Normal file
After Width: | Height: | Size: 84 KiB |
BIN
Docs/InstallingGames.gif
Normal file
After Width: | Height: | Size: 12 MiB |
BIN
Docs/KeyManagement.gif
Normal file
After Width: | Height: | Size: 1.9 MiB |
BIN
Docs/PlayniteAuthentication.png
Normal file
After Width: | Height: | Size: 119 KiB |
27
LANCommander/App.razor
Normal 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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -3,3 +3,4 @@
|
|||
@using LANCommander.Areas.Identity.Pages
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using LANCommander.Data.Models
|
||||
@using LANCommander.Components
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
14
LANCommander/Components/ArchiveBrowserDialog.razor
Normal 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);
|
||||
}
|
||||
}
|
227
LANCommander/Components/ArchiveUploader.razor
Normal 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!");
|
||||
}
|
||||
}
|
145
LANCommander/Components/GameMetadataLookup.razor
Normal 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;
|
||||
}
|
||||
}
|
126
LANCommander/Components/KeysEditor.razor
Normal 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!");
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
8
LANCommander/Components/RedirectToLogin.razor
Normal file
|
@ -0,0 +1,8 @@
|
|||
@inject NavigationManager NavigationManager
|
||||
|
||||
@code {
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
NavigationManager.NavigateTo("/Identity/Account/Login");
|
||||
}
|
||||
}
|
193
LANCommander/Components/ScriptEditor.razor
Normal 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;
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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 });
|
||||
}
|
||||
}
|
42
LANCommander/Components/TagsInput.razor
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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!");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
19
LANCommander/Extensions/ArrayExtensions.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
18
LANCommander/Helpers/RazorHelpers.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
9
LANCommander/Models/ArchiveBrowserOptions.cs
Normal 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; }
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
using LANCommander.Data.Models;
|
||||
|
||||
namespace LANCommander.Models
|
||||
{
|
||||
public class EditKeysViewModel
|
||||
{
|
||||
public Game Game { get; set; }
|
||||
public string Keys { get; set; }
|
||||
}
|
||||
}
|
10
LANCommander/Models/GameLookupResult.cs
Normal 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; }
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
||||
}
|
35
LANCommander/Models/PerformanceChartData.cs
Normal 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)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
83
LANCommander/Pages/Dashboard/Charts/NetworkUploadRate.razor
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
63
LANCommander/Pages/Dashboard/Charts/StorageUsage.razor
Normal 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();
|
||||
}
|
||||
}
|
39
LANCommander/Pages/Dashboard/Index.razor
Normal 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>
|
275
LANCommander/Pages/Games/Edit.razor
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
67
LANCommander/Pages/Games/Index.razor
Normal 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");
|
||||
}
|
||||
}
|
47
LANCommander/Pages/Settings/General.razor
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
21
LANCommander/Pages/Settings/SettingsLayout.razor
Normal 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>
|
98
LANCommander/Pages/Settings/Users.razor
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
8
LANCommander/Pages/_Host.cshtml
Normal file
|
@ -0,0 +1,8 @@
|
|||
@page "/"
|
||||
@namespace LANCommander.Pages
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@{
|
||||
Layout = "_Layout";
|
||||
}
|
||||
|
||||
<component type="typeof(App)" render-mode="ServerPrerendered" />
|
34
LANCommander/Pages/_Layout.cshtml
Normal 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>
|
|
@ -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");
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
17
LANCommander/Shared/MainLayout.razor
Normal 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>
|
|
@ -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>
|
||||
}
|
|
@ -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>
|
|
@ -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");}
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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");}
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -1,6 +0,0 @@
|
|||
@{
|
||||
ViewData["Title"] = "Privacy Policy";
|
||||
}
|
||||
<h1>@ViewData["Title"]</h1>
|
||||
|
||||
<p>Use this page to detail your site's privacy policy.</p>
|
|
@ -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"); }
|
||||
}
|
|
@ -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>
|
||||
}
|
|
@ -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>
|
||||
}
|
|
@ -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>
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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");}
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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");}
|
||||
}
|
|
@ -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>
|
15
LANCommander/_Imports.razor
Normal 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
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -7,7 +7,6 @@
|
|||
"target": "es6"
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"wwwroot/lib"
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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
|
|
@ -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"}
|
|
@ -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');
|
|
@ -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
|
|
@ -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"}
|
|
@ -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;
|
||||
}
|
|
@ -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];
|
||||
}
|