From ed15a50545ae58d9453e43d66eca9b082e472ce2 Mon Sep 17 00:00:00 2001 From: Pat Hartl Date: Fri, 7 Apr 2023 13:46:12 -0500 Subject: [PATCH] Revert "Remove deprecated MVC routes, views, and models" This reverts commit 1158886c52de6ceb36316ea02bb346e78cdea4fb. --- .../Controllers/Api/UploadController.cs | 51 ++ .../Controllers/ArchivesController.cs | 140 +++++ .../Controllers/CompaniesController.cs | 166 +++++ LANCommander/Controllers/GamesController.cs | 577 ++++++++++++++++++ LANCommander/Controllers/KeysController.cs | 110 ++++ LANCommander/Controllers/ScriptsController.cs | 102 ++++ LANCommander/Controllers/TagsController.cs | 166 +++++ LANCommander/Controllers/UploadController.cs | 56 ++ LANCommander/LANCommander.csproj | 6 + LANCommander/Models/ChunkUpload.cs | 11 + LANCommander/Models/EditKeysViewModel.cs | 10 + .../Models/GameLookupResultsViewModel.cs | 10 + LANCommander/Models/GameViewModel.cs | 21 + LANCommander/Pages/Games/Browse.razor | 23 + LANCommander/Views/Archives/Add.cshtml | 127 ++++ LANCommander/Views/Archives/Browse.cshtml | 32 + LANCommander/Views/Companies/Create.cshtml | 43 ++ LANCommander/Views/Companies/Delete.cshtml | 39 ++ LANCommander/Views/Companies/Details.cshtml | 36 ++ LANCommander/Views/Companies/Edit.cshtml | 44 ++ LANCommander/Views/Companies/Index.cshtml | 47 ++ LANCommander/Views/Games/Add.cshtml | 125 ++++ LANCommander/Views/Games/Delete.cshtml | 65 ++ LANCommander/Views/Games/Edit.cshtml | 303 +++++++++ LANCommander/Views/Games/Index.cshtml | 110 ++++ LANCommander/Views/Games/Lookup.cshtml | 99 +++ LANCommander/Views/Keys/Details.cshtml | 106 ++++ LANCommander/Views/Keys/Edit.cshtml | 73 +++ LANCommander/Views/Scripts/Add.cshtml | 125 ++++ LANCommander/Views/Scripts/Edit.cshtml | 126 ++++ LANCommander/Views/Tags/Create.cshtml | 43 ++ LANCommander/Views/Tags/Delete.cshtml | 39 ++ LANCommander/Views/Tags/Details.cshtml | 36 ++ LANCommander/Views/Tags/Edit.cshtml | 44 ++ LANCommander/Views/Tags/Index.cshtml | 47 ++ LANCommander/wwwroot/js/Modal.js | 16 + LANCommander/wwwroot/js/Modal.js.map | 1 + LANCommander/wwwroot/js/Modal.ts | 22 + LANCommander/wwwroot/js/Upload.js | 118 ++++ LANCommander/wwwroot/js/Upload.js.map | 1 + LANCommander/wwwroot/js/Upload.ts | 161 +++++ 41 files changed, 3477 insertions(+) create mode 100644 LANCommander/Controllers/Api/UploadController.cs create mode 100644 LANCommander/Controllers/ArchivesController.cs create mode 100644 LANCommander/Controllers/CompaniesController.cs create mode 100644 LANCommander/Controllers/GamesController.cs create mode 100644 LANCommander/Controllers/KeysController.cs create mode 100644 LANCommander/Controllers/ScriptsController.cs create mode 100644 LANCommander/Controllers/TagsController.cs create mode 100644 LANCommander/Controllers/UploadController.cs create mode 100644 LANCommander/Models/ChunkUpload.cs create mode 100644 LANCommander/Models/EditKeysViewModel.cs create mode 100644 LANCommander/Models/GameLookupResultsViewModel.cs create mode 100644 LANCommander/Models/GameViewModel.cs create mode 100644 LANCommander/Pages/Games/Browse.razor create mode 100644 LANCommander/Views/Archives/Add.cshtml create mode 100644 LANCommander/Views/Archives/Browse.cshtml create mode 100644 LANCommander/Views/Companies/Create.cshtml create mode 100644 LANCommander/Views/Companies/Delete.cshtml create mode 100644 LANCommander/Views/Companies/Details.cshtml create mode 100644 LANCommander/Views/Companies/Edit.cshtml create mode 100644 LANCommander/Views/Companies/Index.cshtml create mode 100644 LANCommander/Views/Games/Add.cshtml create mode 100644 LANCommander/Views/Games/Delete.cshtml create mode 100644 LANCommander/Views/Games/Edit.cshtml create mode 100644 LANCommander/Views/Games/Index.cshtml create mode 100644 LANCommander/Views/Games/Lookup.cshtml create mode 100644 LANCommander/Views/Keys/Details.cshtml create mode 100644 LANCommander/Views/Keys/Edit.cshtml create mode 100644 LANCommander/Views/Scripts/Add.cshtml create mode 100644 LANCommander/Views/Scripts/Edit.cshtml create mode 100644 LANCommander/Views/Tags/Create.cshtml create mode 100644 LANCommander/Views/Tags/Delete.cshtml create mode 100644 LANCommander/Views/Tags/Details.cshtml create mode 100644 LANCommander/Views/Tags/Edit.cshtml create mode 100644 LANCommander/Views/Tags/Index.cshtml create mode 100644 LANCommander/wwwroot/js/Modal.js create mode 100644 LANCommander/wwwroot/js/Modal.js.map create mode 100644 LANCommander/wwwroot/js/Modal.ts create mode 100644 LANCommander/wwwroot/js/Upload.js create mode 100644 LANCommander/wwwroot/js/Upload.js.map create mode 100644 LANCommander/wwwroot/js/Upload.ts diff --git a/LANCommander/Controllers/Api/UploadController.cs b/LANCommander/Controllers/Api/UploadController.cs new file mode 100644 index 0000000..2469a97 --- /dev/null +++ b/LANCommander/Controllers/Api/UploadController.cs @@ -0,0 +1,51 @@ +using LANCommander.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace LANCommander.Controllers.Api +{ + [Route("api/[controller]")] + [ApiController] + public class UploadController : ControllerBase + { + private const string UploadDirectory = "Upload"; + + [HttpPost("Init")] + public string 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 key; + } + + [HttpPost("Chunk")] + public async Task Chunk([FromForm] ChunkUpload chunk) + { + var filePath = Path.Combine(UploadDirectory, chunk.Key.ToString()); + + if (!System.IO.File.Exists(filePath)) + throw new Exception("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); + } + } + } + } +} diff --git a/LANCommander/Controllers/ArchivesController.cs b/LANCommander/Controllers/ArchivesController.cs new file mode 100644 index 0000000..f81e94d --- /dev/null +++ b/LANCommander/Controllers/ArchivesController.cs @@ -0,0 +1,140 @@ +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 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 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 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 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 Browse(Guid id) + { + var archive = await ArchiveService.Get(id); + + return View(archive); + } + + public async Task 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, + }); + } + } +} diff --git a/LANCommander/Controllers/CompaniesController.cs b/LANCommander/Controllers/CompaniesController.cs new file mode 100644 index 0000000..7892372 --- /dev/null +++ b/LANCommander/Controllers/CompaniesController.cs @@ -0,0 +1,166 @@ +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 Index() + { + return _context.Companies != null ? + View(await _context.Companies.ToListAsync()) : + Problem("Entity set 'DatabaseContext.Companies' is null."); + } + + // GET: Companies/Details/5 + public async Task 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 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 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 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 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 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(); + } + } +} diff --git a/LANCommander/Controllers/GamesController.cs b/LANCommander/Controllers/GamesController.cs new file mode 100644 index 0000000..3417eba --- /dev/null +++ b/LANCommander/Controllers/GamesController.cs @@ -0,0 +1,577 @@ +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 Index() + { + return View(GameService.Get()); + } + + public async Task Add(long? igdbid) + { + var viewModel = new GameViewModel() + { + Game = new Game(), + Developers = new List(), + Publishers = new List(), + Genres = new List(), + Tags = new List(), + }; + + if (igdbid == null) + { + viewModel.Game = new Game() + { + Actions = new List(), + MultiplayerModes = new List() + }; + + 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(), + MultiplayerModes = new List() + }; + + + 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 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 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 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 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 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 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()); + + viewModel.Results = results.Select(r => + { + var result = new Game() + { + IGDBId = r.Id.GetValueOrDefault(), + Title = r.Name, + ReleasedOn = r.FirstReleaseDate.GetValueOrDefault().UtcDateTime, + Developers = new List() + }; + + 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); + } + + /// + /// Provides a list of possible games based on the given name + /// + /// Name of the game to lookup against IGDB + /// + public async Task 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 GetIcon(Guid id) + { + try + { + var game = await GameService.Get(id); + + return File(GameService.GetIcon(game), "image/png"); + } + catch (FileNotFoundException ex) + { + return NotFound(); + } + } + } +} diff --git a/LANCommander/Controllers/KeysController.cs b/LANCommander/Controllers/KeysController.cs new file mode 100644 index 0000000..b62f42f --- /dev/null +++ b/LANCommander/Controllers/KeysController.cs @@ -0,0 +1,110 @@ +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 Details(Guid? id) + { + using (var repo = new Repository(Context, HttpContext)) + { + var game = await repo.Find(id.GetValueOrDefault()); + + if (game == null) + return NotFound(); + + return View(game); + } + } + + public async Task Edit(Guid? id) + { + using (var repo = new Repository(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 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(Context, HttpContext)) + { + var game = await gameRepo.Find(id); + + if (game == null) + return NotFound(); + + using (var keyRepo = new Repository(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 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 }); + } + } +} diff --git a/LANCommander/Controllers/ScriptsController.cs b/LANCommander/Controllers/ScriptsController.cs new file mode 100644 index 0000000..39f59f2 --- /dev/null +++ b/LANCommander/Controllers/ScriptsController.cs @@ -0,0 +1,102 @@ +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 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 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 Edit(Guid? id) + { + var script = await ScriptService.Get(id.GetValueOrDefault()); + + if (script == null) + return NotFound(); + + return View(script); + } + + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task 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 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 }); + } + } +} diff --git a/LANCommander/Controllers/TagsController.cs b/LANCommander/Controllers/TagsController.cs new file mode 100644 index 0000000..76dec8b --- /dev/null +++ b/LANCommander/Controllers/TagsController.cs @@ -0,0 +1,166 @@ +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 Index() + { + return _context.Tags != null ? + View(await _context.Tags.ToListAsync()) : + Problem("Entity set 'DatabaseContext.Tags' is null."); + } + + // GET: Tags/Details/5 + public async Task 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 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 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 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 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 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(); + } + } +} diff --git a/LANCommander/Controllers/UploadController.cs b/LANCommander/Controllers/UploadController.cs new file mode 100644 index 0000000..e8dddf8 --- /dev/null +++ b/LANCommander/Controllers/UploadController.cs @@ -0,0 +1,56 @@ +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 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!"); + } + } +} diff --git a/LANCommander/LANCommander.csproj b/LANCommander/LANCommander.csproj index 6676dc6..aa09e68 100644 --- a/LANCommander/LANCommander.csproj +++ b/LANCommander/LANCommander.csproj @@ -45,6 +45,12 @@ + + + Never + + + diff --git a/LANCommander/Models/ChunkUpload.cs b/LANCommander/Models/ChunkUpload.cs new file mode 100644 index 0000000..36589ac --- /dev/null +++ b/LANCommander/Models/ChunkUpload.cs @@ -0,0 +1,11 @@ +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; } + } +} diff --git a/LANCommander/Models/EditKeysViewModel.cs b/LANCommander/Models/EditKeysViewModel.cs new file mode 100644 index 0000000..fc53512 --- /dev/null +++ b/LANCommander/Models/EditKeysViewModel.cs @@ -0,0 +1,10 @@ +using LANCommander.Data.Models; + +namespace LANCommander.Models +{ + public class EditKeysViewModel + { + public Game Game { get; set; } + public string Keys { get; set; } + } +} diff --git a/LANCommander/Models/GameLookupResultsViewModel.cs b/LANCommander/Models/GameLookupResultsViewModel.cs new file mode 100644 index 0000000..cd9ce18 --- /dev/null +++ b/LANCommander/Models/GameLookupResultsViewModel.cs @@ -0,0 +1,10 @@ +using LANCommander.Data.Models; + +namespace LANCommander.Models +{ + public class GameLookupResultsViewModel + { + public string Search { get; set; } + public IEnumerable Results { get; set; } + } +} diff --git a/LANCommander/Models/GameViewModel.cs b/LANCommander/Models/GameViewModel.cs new file mode 100644 index 0000000..80c57e2 --- /dev/null +++ b/LANCommander/Models/GameViewModel.cs @@ -0,0 +1,21 @@ +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? Genres { get; set; } + public List? Tags { get; set; } + public List? Categories { get; set; } + public List? Developers { get; set; } + public List? 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; } + } +} diff --git a/LANCommander/Pages/Games/Browse.razor b/LANCommander/Pages/Games/Browse.razor new file mode 100644 index 0000000..cbeea7a --- /dev/null +++ b/LANCommander/Pages/Games/Browse.razor @@ -0,0 +1,23 @@ +@page "/Games/{id:guid}/Browse" +@inject GameService GameService + + + + +
+
+ +@code { + [Parameter] public Guid Id { get; set; } + + private Game Game { get; set; } + private Archive Archive { get; set; } + + protected override async Task OnInitializedAsync() + { + Game = await GameService.Get(Id); + + if (Game.Archives != null && Game.Archives.Count > 0) + Archive = Game.Archives.OrderByDescending(a => a.CreatedOn).First(); + } +} diff --git a/LANCommander/Views/Archives/Add.cshtml b/LANCommander/Views/Archives/Add.cshtml new file mode 100644 index 0000000..97bd5eb --- /dev/null +++ b/LANCommander/Views/Archives/Add.cshtml @@ -0,0 +1,127 @@ +@model LANCommander.Data.Models.Archive + +@{ + ViewData["Title"] = "Add Archive"; +} + +
+ + +
+ +
+
+
+
+
+
+
+
+
+
+ +
+ + + + @if (Model.LastVersion != null && !String.IsNullOrWhiteSpace(Model.LastVersion.Version)) + { + Last version: @Model.LastVersion.Version + } +
+
+ + + +
+
+ + +
+
+
+
+
+
+ + + + +
+
+
+ + +
+
+
+
+
+
+ +@section Scripts { + @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} + + + +} diff --git a/LANCommander/Views/Archives/Browse.cshtml b/LANCommander/Views/Archives/Browse.cshtml new file mode 100644 index 0000000..6d08108 --- /dev/null +++ b/LANCommander/Views/Archives/Browse.cshtml @@ -0,0 +1,32 @@ +@using LANCommander.Components; +@model LANCommander.Data.Models.Archive + +@{ + ViewData["Title"] = "Browse Archive"; +} + +
+ + +
+ +
+
+
+
+
+ +
+
+
+
+
diff --git a/LANCommander/Views/Companies/Create.cshtml b/LANCommander/Views/Companies/Create.cshtml new file mode 100644 index 0000000..a63e57c --- /dev/null +++ b/LANCommander/Views/Companies/Create.cshtml @@ -0,0 +1,43 @@ +@model LANCommander.Data.Models.Company + +@{ + ViewData["Title"] = "Create"; +} + +

Create

+ +

Company

+
+
+
+
+
+
+ + + +
+
+ + + +
+
+ + + +
+
+ +
+
+
+
+ + + +@section Scripts { + @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} +} diff --git a/LANCommander/Views/Companies/Delete.cshtml b/LANCommander/Views/Companies/Delete.cshtml new file mode 100644 index 0000000..0528677 --- /dev/null +++ b/LANCommander/Views/Companies/Delete.cshtml @@ -0,0 +1,39 @@ +@model LANCommander.Data.Models.Company + +@{ + ViewData["Title"] = "Delete"; +} + +

Delete

+ +

Are you sure you want to delete this?

+
+

Company

+
+
+
+ @Html.DisplayNameFor(model => model.Name) +
+
+ @Html.DisplayFor(model => model.Name) +
+
+ @Html.DisplayNameFor(model => model.CreatedOn) +
+
+ @Html.DisplayFor(model => model.CreatedOn) +
+
+ @Html.DisplayNameFor(model => model.UpdatedOn) +
+
+ @Html.DisplayFor(model => model.UpdatedOn) +
+
+ +
+ + | + Back to List +
+
diff --git a/LANCommander/Views/Companies/Details.cshtml b/LANCommander/Views/Companies/Details.cshtml new file mode 100644 index 0000000..ab17f90 --- /dev/null +++ b/LANCommander/Views/Companies/Details.cshtml @@ -0,0 +1,36 @@ +@model LANCommander.Data.Models.Company + +@{ + ViewData["Title"] = "Details"; +} + +

Details

+ +
+

Company

+
+
+
+ @Html.DisplayNameFor(model => model.Name) +
+
+ @Html.DisplayFor(model => model.Name) +
+
+ @Html.DisplayNameFor(model => model.CreatedOn) +
+
+ @Html.DisplayFor(model => model.CreatedOn) +
+
+ @Html.DisplayNameFor(model => model.UpdatedOn) +
+
+ @Html.DisplayFor(model => model.UpdatedOn) +
+
+
+ diff --git a/LANCommander/Views/Companies/Edit.cshtml b/LANCommander/Views/Companies/Edit.cshtml new file mode 100644 index 0000000..6d5f879 --- /dev/null +++ b/LANCommander/Views/Companies/Edit.cshtml @@ -0,0 +1,44 @@ +@model LANCommander.Data.Models.Company + +@{ + ViewData["Title"] = "Edit"; +} + +

Edit

+ +

Company

+
+
+
+
+
+
+ + + +
+ +
+ + + +
+
+ + + +
+
+ +
+
+
+
+ + + +@section Scripts { + @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} +} diff --git a/LANCommander/Views/Companies/Index.cshtml b/LANCommander/Views/Companies/Index.cshtml new file mode 100644 index 0000000..bdf83b8 --- /dev/null +++ b/LANCommander/Views/Companies/Index.cshtml @@ -0,0 +1,47 @@ +@model IEnumerable + +@{ + ViewData["Title"] = "Index"; +} + +

Index

+ +

+ Create New +

+ + + + + + + + + + +@foreach (var item in Model) { + + + + + + +} + +
+ @Html.DisplayNameFor(model => model.Name) + + @Html.DisplayNameFor(model => model.CreatedOn) + + @Html.DisplayNameFor(model => model.UpdatedOn) +
+ @Html.DisplayFor(modelItem => item.Name) + + @Html.DisplayFor(modelItem => item.CreatedOn) + + @Html.DisplayFor(modelItem => item.UpdatedOn) + + Edit | + Details | + Delete +
diff --git a/LANCommander/Views/Games/Add.cshtml b/LANCommander/Views/Games/Add.cshtml new file mode 100644 index 0000000..ef0490c --- /dev/null +++ b/LANCommander/Views/Games/Add.cshtml @@ -0,0 +1,125 @@ +@using LANCommander.Components +@model LANCommander.Models.GameViewModel + +@{ + ViewData["Title"] = "Add Game"; +} + +
+ + +
+ +
+
+
+
+
+
+
+
+
+ +
+ +
+ + +
+ +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ +
+
+ + + +
+
+ + + +
+ +
+ + + +
+ +
+ + + +
+
+
+
+ +
+

Actions

+
+ + + +
+

Multiplayer Modes

+
+ + + + + +
+
+
+
+ +@section Scripts { + @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } + + +} diff --git a/LANCommander/Views/Games/Delete.cshtml b/LANCommander/Views/Games/Delete.cshtml new file mode 100644 index 0000000..80a8f7b --- /dev/null +++ b/LANCommander/Views/Games/Delete.cshtml @@ -0,0 +1,65 @@ +@model LANCommander.Data.Models.Game + +@{ + ViewData["Title"] = "Delete"; +} + +
+ +
+
+

Are you sure you want to delete this game? + @if (Model.Archives != null && Model.Archives.Count > 0) + { + It will also delete the following archives: + } +

+
+ + @if (Model.Archives != null && Model.Archives.Count > 0) + { +
+ + + + + + + + + + + + @foreach (var archive in Model.Archives.OrderByDescending(a => a.CreatedOn)) + { + + + + + + + } + +
VersionUploaded ByUploaded OnSize
@Html.DisplayFor(m => archive.Version)@Html.DisplayFor(m => archive.CreatedBy.UserName)@Html.DisplayFor(m => archive.CreatedOn)@ByteSizeLib.ByteSize.FromBytes(archive.CompressedSize)
+
+ } + + + +
+
\ No newline at end of file diff --git a/LANCommander/Views/Games/Edit.cshtml b/LANCommander/Views/Games/Edit.cshtml new file mode 100644 index 0000000..f2c9801 --- /dev/null +++ b/LANCommander/Views/Games/Edit.cshtml @@ -0,0 +1,303 @@ +@using LANCommander.Components +@using LANCommander.Data.Models +@model LANCommander.Models.GameViewModel + +@{ + ViewData["Title"] = "Edit"; +} + +
+ + +
+ +
+
+
+
+
+
+
+
+
+ +
+ +
+ + +
+ +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ +
+
+ + + +
+
+ + + +
+ +
+ + + +
+ +
+ + + +
+ + +
+
+
+ +
+

Actions

+
+ + + +
+

Multiplayer Modes

+
+ + + + + +
+ +
+
+ @if (Model.Game.Keys != null && Model.Game.Keys.Count > 0) + { +
+

Keys

+ +
+ + var keysAvailable = Model.Game.Keys.Count(k => + { + return (k.AllocationMethod == KeyAllocationMethod.MacAddress && String.IsNullOrWhiteSpace(k.ClaimedByMacAddress)) || + (k.AllocationMethod == KeyAllocationMethod.UserAccount && k.ClaimedByUser == null); + }); + +
+
+
+
Available
+
+ @keysAvailable +
+
+
+
Claimed
+
+ @(Model.Game.Keys.Count - keysAvailable) +
+
+
+
Total
+
+ @Model.Game.Keys.Count +
+
+
+
+ } + else + { +
+

No Keys

+

There have been no keys added for this game.

+
+ Edit Keys +
+
+ } +
+
+ +
+
+ @if (Model.Game.Archives != null && Model.Game.Archives.Count > 0) + { +
+

Archives

+ +
+ +
+ + + + + + + + + + + + + @foreach (var archive in Model.Game.Archives.OrderByDescending(a => a.CreatedOn)) + { + + + + + + + + } + +
VersionUploaded ByUploaded OnSize
@Html.DisplayFor(m => archive.Version)@Html.DisplayFor(m => archive.CreatedBy.UserName)@Html.DisplayFor(m => archive.CreatedOn)@ByteSizeLib.ByteSize.FromBytes(new FileInfo(System.IO.Path.Combine("Upload", archive.ObjectKey)).Length) + +
+
+ } + else + { +
+

No Archives

+

There have been no archives uploaded for this game.

+ +
+ } +
+
+ +
+
+ @if (Model.Game.Scripts != null && Model.Game.Scripts.Count > 0) + { +
+

Scripts

+ +
+ +
+ + + + + + + + + + + + + @foreach (var script in Model.Game.Scripts.OrderBy(s => s.Type).ThenByDescending(s => s.CreatedOn)) + { + + + + + + + + } + +
TypeNameCreated OnCreated By
@Html.DisplayFor(m => script.Type)@Html.DisplayFor(m => script.Name)@Html.DisplayFor(m => script.CreatedOn)@Html.DisplayFor(m => script.CreatedBy.UserName) +
+ Edit + Delete +
+
+
+ } + else + { +
+

No Scripts

+

There have been no scripts added for this game.

+ +
+ } +
+
+ +
+
+
+ +@section Scripts { + @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } + + +} diff --git a/LANCommander/Views/Games/Index.cshtml b/LANCommander/Views/Games/Index.cshtml new file mode 100644 index 0000000..9051197 --- /dev/null +++ b/LANCommander/Views/Games/Index.cshtml @@ -0,0 +1,110 @@ +@model IEnumerable + +@{ + ViewData["Title"] = "Games"; +} +
+ +
+ +
+
+
+
+
+
+ + + + + + + + + + + + + + + + + @foreach (var item in Model.OrderBy(g => !String.IsNullOrWhiteSpace(g.SortTitle) ? g.SortTitle : g.Title)) + { + + + + + + + + + + + + } + +
+ @Html.DisplayNameFor(model => model.Title) + + @Html.DisplayNameFor(model => model.SortTitle) + + @Html.DisplayNameFor(model => model.ReleasedOn) + + @Html.DisplayNameFor(model => model.CreatedOn) + + @Html.DisplayNameFor(model => model.CreatedBy) + + @Html.DisplayNameFor(model => model.UpdatedOn) + + @Html.DisplayNameFor(model => model.UpdatedBy) +
+ @if (!String.IsNullOrWhiteSpace(item.Icon)) { + + } + + @Html.DisplayFor(modelItem => item.Title) + + @Html.DisplayFor(modelItem => item.SortTitle) + + @if (item.ReleasedOn.HasValue) + { + @item.ReleasedOn.Value.ToString("MM/dd/yyyy") + } + + @Html.DisplayFor(modelItem => item.CreatedOn) + + @Html.DisplayFor(modelItem => item.CreatedBy.UserName) + + @Html.DisplayFor(modelItem => item.UpdatedOn) + + @Html.DisplayFor(modelItem => item.UpdatedBy.UserName) + +
+ Edit + Delete +
+
+
+
+
+
+
+
\ No newline at end of file diff --git a/LANCommander/Views/Games/Lookup.cshtml b/LANCommander/Views/Games/Lookup.cshtml new file mode 100644 index 0000000..329fceb --- /dev/null +++ b/LANCommander/Views/Games/Lookup.cshtml @@ -0,0 +1,99 @@ +@model LANCommander.Models.GameLookupResultsViewModel + +@{ + ViewData["Title"] = "Games"; +} +
+ +
+ +
+
+
+
+
+
+

Results

+
+ @if (Model.Results.Count() == 0) + { +
+

No games could be found with the search "@Model.Search".

+
+ } + else + { +
+ @if (Model.Results.Count() > 1) + { +

There was a total of @Model.Results.Count() games that matched the search "@Model.Search" in IGDB's database.

+ } + else + { +

Only one game matched the search "@Model.Search" in IGDB's database.

+ } +
+
+
+ + + + + + + + + + + + @foreach (var item in Model.Results) + { + + + + + + + } + +
+ Title + + Release Date + + Developers +
+ @Html.DisplayFor(modelItem => item.Title) + + @Html.DisplayFor(modelItem => item.ReleasedOn) + + @String.Join(", ", item.Developers.Select(d => d.Name)) + +
+ Select +
+
+
+
+ + } + + +
+
+
+
+
\ No newline at end of file diff --git a/LANCommander/Views/Keys/Details.cshtml b/LANCommander/Views/Keys/Details.cshtml new file mode 100644 index 0000000..936d504 --- /dev/null +++ b/LANCommander/Views/Keys/Details.cshtml @@ -0,0 +1,106 @@ +@using LANCommander.Data.Models +@model LANCommander.Data.Models.Game + +@{ + ViewData["Title"] = "Keys | " + Model.Title; +} + +
+ + +
+ +
+
+
+
+
+ @if (Model.Keys != null && Model.Keys.Count > 0) + { +
+ + + + + + + + + + + + + @foreach (var key in Model.Keys.OrderByDescending(k => k.ClaimedOn)) + { + + + + + + + + } + +
KeyAllocation MethodClaimed ByClaimed On
@Html.DisplayFor(m => key.Value)@Html.DisplayFor(m => key.AllocationMethod) + @switch (key.AllocationMethod) + { + case KeyAllocationMethod.MacAddress: + @key.ClaimedByMacAddress + break; + + case KeyAllocationMethod.UserAccount: + @key.ClaimedByUser?.UserName + break; + } + @key.ClaimedOn +
+ @if ((key.AllocationMethod == KeyAllocationMethod.MacAddress && !String.IsNullOrWhiteSpace(key.ClaimedByMacAddress)) || (key.AllocationMethod == KeyAllocationMethod.UserAccount && key.ClaimedByUser != null)) + { + Release + } + Delete +
+
+
+ } + else + { +
+

No Key

+

There have been no keys added for this game.

+
+ Edit Keys +
+
+ } +
+
+
+
+
+ + + +@section Scripts { + @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } +} diff --git a/LANCommander/Views/Keys/Edit.cshtml b/LANCommander/Views/Keys/Edit.cshtml new file mode 100644 index 0000000..1387711 --- /dev/null +++ b/LANCommander/Views/Keys/Edit.cshtml @@ -0,0 +1,73 @@ +@model LANCommander.Models.EditKeysViewModel + +@{ + ViewData["Title"] = "Edit Keys | " + Model.Game.Title; +} + +
+ + +
+ +
+
+
+
+
+
+ + + + +
+ + +
+
+
+
+
+ +@section Scripts { + @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } + + + + +} diff --git a/LANCommander/Views/Scripts/Add.cshtml b/LANCommander/Views/Scripts/Add.cshtml new file mode 100644 index 0000000..abfdbc0 --- /dev/null +++ b/LANCommander/Views/Scripts/Add.cshtml @@ -0,0 +1,125 @@ +@using LANCommander.Components; +@using LANCommander.Data.Enums +@model LANCommander.Data.Models.Script + +@{ + ViewData["Title"] = "Add Script | " + Model.Game.Title; +} + +
+ + +
+ +
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+ + + +
+
+
+
+ + + +
+
+
+ +
+
+
+ + + +
+ +
+ +
+ + + +
+
+ +
+
+ +
+
+
+ +
+ + +
+
+
+
+
+ +@section Scripts { + @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } + + + + +} diff --git a/LANCommander/Views/Scripts/Edit.cshtml b/LANCommander/Views/Scripts/Edit.cshtml new file mode 100644 index 0000000..9865827 --- /dev/null +++ b/LANCommander/Views/Scripts/Edit.cshtml @@ -0,0 +1,126 @@ +@using LANCommander.Components; +@using LANCommander.Data.Enums +@model LANCommander.Data.Models.Script + +@{ + ViewData["Title"] = "Edit Script | " + Model.Game.Title; +} + +
+ + +
+ +
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+ + + +
+
+
+
+ + + +
+
+
+ +
+
+
+ + + +
+ +
+ +
+ + + + +
+
+ +
+
+ +
+
+
+ +
+ + +
+
+
+
+
+ +@section Scripts { + @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } + + + + +} diff --git a/LANCommander/Views/Tags/Create.cshtml b/LANCommander/Views/Tags/Create.cshtml new file mode 100644 index 0000000..e8d8412 --- /dev/null +++ b/LANCommander/Views/Tags/Create.cshtml @@ -0,0 +1,43 @@ +@model LANCommander.Data.Models.Tag + +@{ + ViewData["Title"] = "Create"; +} + +

Create

+ +

Tag

+
+
+
+
+
+
+ + + +
+
+ + + +
+
+ + + +
+
+ +
+
+
+
+ + + +@section Scripts { + @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} +} diff --git a/LANCommander/Views/Tags/Delete.cshtml b/LANCommander/Views/Tags/Delete.cshtml new file mode 100644 index 0000000..a9581d1 --- /dev/null +++ b/LANCommander/Views/Tags/Delete.cshtml @@ -0,0 +1,39 @@ +@model LANCommander.Data.Models.Tag + +@{ + ViewData["Title"] = "Delete"; +} + +

Delete

+ +

Are you sure you want to delete this?

+
+

Tag

+
+
+
+ @Html.DisplayNameFor(model => model.Name) +
+
+ @Html.DisplayFor(model => model.Name) +
+
+ @Html.DisplayNameFor(model => model.CreatedOn) +
+
+ @Html.DisplayFor(model => model.CreatedOn) +
+
+ @Html.DisplayNameFor(model => model.UpdatedOn) +
+
+ @Html.DisplayFor(model => model.UpdatedOn) +
+
+ +
+ + | + Back to List +
+
diff --git a/LANCommander/Views/Tags/Details.cshtml b/LANCommander/Views/Tags/Details.cshtml new file mode 100644 index 0000000..0315ae3 --- /dev/null +++ b/LANCommander/Views/Tags/Details.cshtml @@ -0,0 +1,36 @@ +@model LANCommander.Data.Models.Tag + +@{ + ViewData["Title"] = "Details"; +} + +

Details

+ +
+

Tag

+
+
+
+ @Html.DisplayNameFor(model => model.Name) +
+
+ @Html.DisplayFor(model => model.Name) +
+
+ @Html.DisplayNameFor(model => model.CreatedOn) +
+
+ @Html.DisplayFor(model => model.CreatedOn) +
+
+ @Html.DisplayNameFor(model => model.UpdatedOn) +
+
+ @Html.DisplayFor(model => model.UpdatedOn) +
+
+
+ diff --git a/LANCommander/Views/Tags/Edit.cshtml b/LANCommander/Views/Tags/Edit.cshtml new file mode 100644 index 0000000..41b8833 --- /dev/null +++ b/LANCommander/Views/Tags/Edit.cshtml @@ -0,0 +1,44 @@ +@model LANCommander.Data.Models.Tag + +@{ + ViewData["Title"] = "Edit"; +} + +

Edit

+ +

Tag

+
+
+
+
+
+
+ + + +
+ +
+ + + +
+
+ + + +
+
+ +
+
+
+
+ + + +@section Scripts { + @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} +} diff --git a/LANCommander/Views/Tags/Index.cshtml b/LANCommander/Views/Tags/Index.cshtml new file mode 100644 index 0000000..c967417 --- /dev/null +++ b/LANCommander/Views/Tags/Index.cshtml @@ -0,0 +1,47 @@ +@model IEnumerable + +@{ + ViewData["Title"] = "Index"; +} + +

Index

+ +

+ Create New +

+ + + + + + + + + + +@foreach (var item in Model) { + + + + + + +} + +
+ @Html.DisplayNameFor(model => model.Name) + + @Html.DisplayNameFor(model => model.CreatedOn) + + @Html.DisplayNameFor(model => model.UpdatedOn) +
+ @Html.DisplayFor(modelItem => item.Name) + + @Html.DisplayFor(modelItem => item.CreatedOn) + + @Html.DisplayFor(modelItem => item.UpdatedOn) + + Edit | + Details | + Delete +
diff --git a/LANCommander/wwwroot/js/Modal.js b/LANCommander/wwwroot/js/Modal.js new file mode 100644 index 0000000..fe80049 --- /dev/null +++ b/LANCommander/wwwroot/js/Modal.js @@ -0,0 +1,16 @@ +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 \ No newline at end of file diff --git a/LANCommander/wwwroot/js/Modal.js.map b/LANCommander/wwwroot/js/Modal.js.map new file mode 100644 index 0000000..74e6593 --- /dev/null +++ b/LANCommander/wwwroot/js/Modal.js.map @@ -0,0 +1 @@ +{"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"} \ No newline at end of file diff --git a/LANCommander/wwwroot/js/Modal.ts b/LANCommander/wwwroot/js/Modal.ts new file mode 100644 index 0000000..ec1a0b5 --- /dev/null +++ b/LANCommander/wwwroot/js/Modal.ts @@ -0,0 +1,22 @@ +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'); \ No newline at end of file diff --git a/LANCommander/wwwroot/js/Upload.js b/LANCommander/wwwroot/js/Upload.js new file mode 100644 index 0000000..5f0167e --- /dev/null +++ b/LANCommander/wwwroot/js/Upload.js @@ -0,0 +1,118 @@ +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 \ No newline at end of file diff --git a/LANCommander/wwwroot/js/Upload.js.map b/LANCommander/wwwroot/js/Upload.js.map new file mode 100644 index 0000000..4e560e5 --- /dev/null +++ b/LANCommander/wwwroot/js/Upload.js.map @@ -0,0 +1 @@ +{"version":3,"file":"Upload.js","sourceRoot":"","sources":["Upload.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/LANCommander/wwwroot/js/Upload.ts b/LANCommander/wwwroot/js/Upload.ts new file mode 100644 index 0000000..450845b --- /dev/null +++ b/LANCommander/wwwroot/js/Upload.ts @@ -0,0 +1,161 @@ +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 { + 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; +} \ No newline at end of file