From 38c60cee378b9c2ad42fc9dc79bc492b919a68f5 Mon Sep 17 00:00:00 2001 From: Jake Mannens Date: Tue, 15 Aug 2023 15:49:14 +1000 Subject: Convert Razor pages to Blazor --- .config/dotnet-tools.json | 12 -- ApiRecords.cs | 53 -------- App.razor | 11 ++ ConfigService.cs | 80 ------------ Controllers/MediaController.cs | 170 +++++++++++++++++++++++++ DbMedia.cs | 46 ------- DbObject.cs | 15 --- DbTag.cs | 23 ---- DebugController.cs | 22 ---- Enum.cs | 13 -- HBContext.cs | 41 ++++++ HBObject.cs | 14 +++ HyperBooru.cs | 37 ------ HyperBooruContext.cs | 37 ------ MainLayout.razor | 15 +++ MainLayout.razor.css | 39 ++++++ Media.cs | 43 +++++++ MediaController.cs | 200 ------------------------------ Pages/Component/Dialog.razor | 38 ++++++ Pages/Component/Dialog.razor.css | 21 ++++ Pages/Component/MediaTagTable.razor | 69 +++++++++++ Pages/Component/MediaTagTable.razor.css | 3 + Pages/Component/TabContainer.razor | 35 ++++++ Pages/Component/TabContainer.razor.css | 19 +++ Pages/Component/TabPane.razor | 29 +++++ Pages/Component/TabPane.razor.css | 1 + Pages/Component/TagSelectDialog.razor | 59 +++++++++ Pages/Component/TagSelectDialog.razor.css | 31 +++++ Pages/Index.cshtml | 15 --- Pages/Index.cshtml.cs | 43 ------- Pages/Index.cshtml.css | 7 -- Pages/Index.razor | 17 +++ Pages/Index.razor.css | 8 ++ Pages/Shared/_Layout.cshtml | 26 ---- Pages/Shared/_Layout.cshtml.css | 36 ------ Pages/TagDefinitions.cshtml | 108 ---------------- Pages/TagDefinitions.cshtml.cs | 16 --- Pages/TagDefinitions.cshtml.css | 12 -- Pages/TagDefinitions.razor | 92 ++++++++++++++ Pages/TagDefinitions.razor.css | 12 ++ Pages/ViewMedia.cshtml | 181 --------------------------- Pages/ViewMedia.cshtml.cs | 43 ------- Pages/ViewMedia.cshtml.css | 97 --------------- Pages/ViewMedia.razor | 106 ++++++++++++++++ Pages/ViewMedia.razor.css | 30 +++++ Pages/_Host.cshtml | 33 +++++ Pages/_ViewStart.cshtml | 3 - Program.cs | 39 ++++++ Properties/launchSettings.json | 26 ++-- Server.csproj | 16 +++ Services/ConfigService.cs | 81 ++++++++++++ Services/MediaService.cs | 28 +++++ Services/TagService.cs | 97 +++++++++++++++ Tag.cs | 22 ++++ TagController.cs | 129 ------------------- _Imports.razor | 7 ++ appsettings.Development.json | 1 + wwwroot/css/site.css | 28 +++++ wwwroot/favicon.ico | Bin 0 -> 3262 bytes wwwroot/icon-192.png | Bin 0 -> 31523 bytes wwwroot/icon-512.png | Bin 0 -> 136487 bytes wwwroot/manifest.webmanifest | 6 + wwwroot/styles/data-table.css | 21 ++++ wwwroot/styles/global.css | 63 +++------- 64 files changed, 1313 insertions(+), 1312 deletions(-) delete mode 100644 .config/dotnet-tools.json delete mode 100644 ApiRecords.cs create mode 100644 App.razor delete mode 100644 ConfigService.cs create mode 100644 Controllers/MediaController.cs delete mode 100644 DbMedia.cs delete mode 100644 DbObject.cs delete mode 100644 DbTag.cs delete mode 100644 DebugController.cs delete mode 100644 Enum.cs create mode 100644 HBContext.cs create mode 100644 HBObject.cs delete mode 100644 HyperBooru.cs delete mode 100644 HyperBooruContext.cs create mode 100644 MainLayout.razor create mode 100644 MainLayout.razor.css create mode 100644 Media.cs delete mode 100644 MediaController.cs create mode 100644 Pages/Component/Dialog.razor create mode 100644 Pages/Component/Dialog.razor.css create mode 100644 Pages/Component/MediaTagTable.razor create mode 100644 Pages/Component/MediaTagTable.razor.css create mode 100644 Pages/Component/TabContainer.razor create mode 100644 Pages/Component/TabContainer.razor.css create mode 100644 Pages/Component/TabPane.razor create mode 100644 Pages/Component/TabPane.razor.css create mode 100644 Pages/Component/TagSelectDialog.razor create mode 100644 Pages/Component/TagSelectDialog.razor.css delete mode 100644 Pages/Index.cshtml delete mode 100644 Pages/Index.cshtml.cs delete mode 100644 Pages/Index.cshtml.css create mode 100644 Pages/Index.razor create mode 100644 Pages/Index.razor.css delete mode 100644 Pages/Shared/_Layout.cshtml delete mode 100644 Pages/Shared/_Layout.cshtml.css delete mode 100644 Pages/TagDefinitions.cshtml delete mode 100644 Pages/TagDefinitions.cshtml.cs delete mode 100644 Pages/TagDefinitions.cshtml.css create mode 100644 Pages/TagDefinitions.razor create mode 100644 Pages/TagDefinitions.razor.css delete mode 100644 Pages/ViewMedia.cshtml delete mode 100644 Pages/ViewMedia.cshtml.cs delete mode 100644 Pages/ViewMedia.cshtml.css create mode 100644 Pages/ViewMedia.razor create mode 100644 Pages/ViewMedia.razor.css create mode 100644 Pages/_Host.cshtml delete mode 100644 Pages/_ViewStart.cshtml create mode 100644 Program.cs create mode 100644 Services/ConfigService.cs create mode 100644 Services/MediaService.cs create mode 100644 Services/TagService.cs create mode 100644 Tag.cs delete mode 100644 TagController.cs create mode 100644 _Imports.razor create mode 100644 wwwroot/css/site.css create mode 100644 wwwroot/favicon.ico create mode 100644 wwwroot/icon-192.png create mode 100644 wwwroot/icon-512.png create mode 100644 wwwroot/manifest.webmanifest create mode 100644 wwwroot/styles/data-table.css diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json deleted file mode 100644 index 34c3e19..0000000 --- a/.config/dotnet-tools.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "version": 1, - "isRoot": true, - "tools": { - "dotnet-ef": { - "version": "7.0.5", - "commands": [ - "dotnet-ef" - ] - } - } -} \ No newline at end of file diff --git a/ApiRecords.cs b/ApiRecords.cs deleted file mode 100644 index 69160c5..0000000 --- a/ApiRecords.cs +++ /dev/null @@ -1,53 +0,0 @@ -namespace HyperBooru.ApiRecords; - -public class TagInfo { - public string Id { get; init; } - public string? Namespace { get; init; } - public string Name { get; init; } - public bool IsImplicit { get; init; } -} - -public class MediaInfo { - public string Id { get; init; } - public string Checksum { get; init; } - public string MimeType { get; init; } - public string? ShortDescription { get; init; } - public string? LongDescription { get; init; } - - public UploadedFileInfo[] UploadedFileHistory { get; init; } - - public MediaInfo(DbMedia media) { - Id = media.Guid.ToString(); - Checksum = media.Checksum; - MimeType = media.MimeType; - ShortDescription = media.ShortDescription; - LongDescription = media.LongDescription; - - UploadedFileHistory = media.UploadedFiles - .Select(uf => new UploadedFileInfo(uf)) - .ToArray(); - } -} - -public record MediaUpdateInfo { - public string? ShortDescription { get; init; } - public string? LongDescription { get; init; } -} - -public class UploadedFileInfo { - public string Checksum { get; init; } - public string? Filename { get; init; } - public DateTime UploadTime { get; init; } - public DateTime? LastAccessTime { get; init; } - public DateTime? LastWriteTime { get; init; } - public DateTime? CreateTime { get; init; } - - public UploadedFileInfo(DbUploadedFile uploadedFile) { - Checksum = uploadedFile.OriginalChecksum; - Filename = uploadedFile.Filename; - UploadTime = uploadedFile.UploadTime; - LastAccessTime = uploadedFile.LastAccessTime; - LastWriteTime = uploadedFile.LastWriteTime; - CreateTime = uploadedFile.CreateTime; - } -} \ No newline at end of file diff --git a/App.razor b/App.razor new file mode 100644 index 0000000..d414dad --- /dev/null +++ b/App.razor @@ -0,0 +1,11 @@ + + + + + + Not found + +

Sorry, there's nothing at this address.

+
+
+
diff --git a/ConfigService.cs b/ConfigService.cs deleted file mode 100644 index 0ee4113..0000000 --- a/ConfigService.cs +++ /dev/null @@ -1,80 +0,0 @@ -namespace HyperBooru; - -public interface IConfigService { - public string DataPath { get; } - public string DbPath { get; } - public string MediaBasePath { get; } - public string GetPath(DbMedia media); - public string GetPath(DbMedia media, int width, int height); -} - -public class ConfigService : IConfigService { - private IConfiguration config; - - private const string AppName = "HyperBooru"; - - public string DataPath { - get { - #if DEBUG - return "Data"; - #else - string? path = config["DataPath"]; - if(path is not null) - return path; - - switch(Environment.OSVersion.Platform) { - case PlatformID.Win32NT: - return Path.Join( - Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), - AppName); - case PlatformID.Unix: - return $"/var/lib/{AppName.ToLower()}"; - default: - throw new NotImplementedException( - $"Unknown Operating System: {Environment.OSVersion.Platform}"); - } - #endif - } - } - - public string DbPath => - Path.Join(DataPath, $"{AppName}.db"); - - public string MediaBasePath => - Path.Join(DataPath, "media"); - - public string ThumbnailBasePath => - Path.Join(DataPath, "thumb"); - - public ConfigService(IConfiguration config) { - this.config = config; - InitDirectoryStructure(); - } - - public string GetPath(DbMedia media) { - var fileInfo = new FileInfo(Path.Join( - MediaBasePath, - media.Guid.ToString().Substring(0, 2), - media.Guid.ToString().Substring(2, 2), - media.Guid.ToString())); - - Directory.CreateDirectory(fileInfo.Directory.FullName); - return fileInfo.FullName; - } - - public string GetPath(DbMedia media, int width, int height) { - var fileInfo = new FileInfo(Path.Join( - ThumbnailBasePath, - media.Guid.ToString().Substring(0, 2), - media.Guid.ToString().Substring(2, 2), - $"{media.Guid.ToString()}-{width}-{height}")); - - Directory.CreateDirectory(fileInfo.Directory.FullName); - return fileInfo.FullName; - } - - private void InitDirectoryStructure() { - Directory.CreateDirectory(DataPath); - Directory.CreateDirectory(MediaBasePath); - } -} \ No newline at end of file diff --git a/Controllers/MediaController.cs b/Controllers/MediaController.cs new file mode 100644 index 0000000..3f36064 --- /dev/null +++ b/Controllers/MediaController.cs @@ -0,0 +1,170 @@ +using HyperBooru.Services; +using ImageMagick; +using Microsoft.AspNetCore.Mvc; +using MimeDetective; +using System.Security.Cryptography; + +namespace HyperBooru.Controllers; + +[ApiController] +[Route("/media")] +public class MediaController : Controller { + private IConfigService config; + private HBContext db; + + private ContentInspector inspector; + + public MediaController(IConfigService config, HBContext db) { + this.config = config; + this.db = db; + + ContentInspectorBuilder inspectorBuilder = new() { + Definitions = + MimeDetective.Definitions.Default.FileTypes.Images.All() + .Union(MimeDetective.Definitions.Default.FileTypes.Video.All()) + .ToList() + }; + + inspector = inspectorBuilder.Build(); + } + + [HttpGet("list")] + public IActionResult EnumerateMedia() => + Ok(db.Media.Select(m => m.ObjectId).ToArray()); + + [HttpGet("{mediaId}")] + public IActionResult Fetch([FromRoute] Guid mediaId) { + var media = db.Media.First(m => m.Guid == mediaId); + if(media is null) + return NotFound(); + + var fs = System.IO.File.OpenRead(config.GetPath(media)); + + return new FileStreamResult(fs, media.MimeType); + } + + [HttpGet("thumb/{mediaId}")] + public IActionResult Thumbnail( + [FromRoute] Guid mediaId, + [FromQuery] int? w, + [FromQuery] int? h) { + + var media = db.Media.First(m => m.Guid == mediaId); + if(media is null) + return NotFound(); + + if(media.MimeType.Split("/")[0] != "image") + return BadRequest("Media object not an image"); + + using var image = new MagickImage(config.GetPath(media)); + + if(w is null && h is null) + return BadRequest("Both width and height cannot be null!"); + + if(w > image.Width || h > image.Height) + return BadRequest("Requested thumbnail size is larger than original media"); + + int width = (int)(w is not null ? w : image.Width * h / image.Height); + int height = (int)(h is not null ? h : image.Height * w / image.Width); + + var thumbPath = config.GetPath(media, width, height); + + if(!System.IO.File.Exists(thumbPath)) { + image.Resize(new MagickGeometry(width, height)); + image.Write(thumbPath); + } + + var fs = System.IO.File.OpenRead(thumbPath); + + return new FileStreamResult(fs, "image/jpeg"); + } + + [HttpDelete("{mediaId}")] + public IActionResult Delete([FromRoute] Guid mediaId) { + var media = db.Media.First(m => m.Guid == mediaId); + if(media is null) + return NotFound(); + + System.IO.File.Delete(config.GetPath(media)); + + db.Media.Remove(media); + db.SaveChanges(); + return Ok(); + } + + [HttpPost] + public IActionResult Upload( + [FromForm] string? checksum, + [FromForm] DateTime? lastAccessTime, + [FromForm] DateTime? lastWriteTime, + [FromForm] DateTime? createTime) { + + using var transaction = db.Database.BeginTransaction(); + + var formFile = Request.Form.Files[0]; + if(formFile.Length < 1) + return BadRequest("Empty file"); + + var formStream = formFile.OpenReadStream(); + + // Calculate the checksum using the in-memory file contents + var hash = BitConverter + .ToString(MD5.Create().ComputeHash(formStream)) + .Replace("-", "") + .ToLower(); + + if(checksum is not null && hash != checksum.ToLower()) + return BadRequest("Checksum does not match"); + + var fileRecord = new UploadedFile() { + Filename = formFile.FileName, + OriginalChecksum = hash, + UploadTime = DateTime.Now, + LastAccessTime = lastAccessTime, + LastWriteTime = lastWriteTime, + CreateTime = createTime + }; + + formStream.Seek(0, SeekOrigin.Begin); + var defs = inspector.Inspect(formStream); + + var mime = defs.ByMimeType().FirstOrDefault()?.MimeType; + if(mime is null) + return BadRequest("Unsupported file type"); + + var media = db.Media + .FirstOrDefault(m => m.Checksum == hash); + + if(media is null) { + var ingestTagDef = db.TagDefinitions + .First(td => td.Source == TagSource.Internal && td.Name == "ingest"); + + media = new() { + Checksum = hash, + MimeType = mime, + UploadedFiles = new() { + fileRecord + }, + Tags = new() { + new() { TagDefinition = ingestTagDef } + } + }; + + using var newFile = System.IO.File.Create(config.GetPath(media)); + + formStream.Seek(0, SeekOrigin.Begin); + formStream.CopyTo(newFile); + newFile.Flush(); + + db.Media.Add(media); + } else { + media.UploadedFiles.Add(fileRecord); + db.Update(media); + } + + db.SaveChanges(); + transaction.Commit(); + + return Ok(media.Guid); + } +} \ No newline at end of file diff --git a/DbMedia.cs b/DbMedia.cs deleted file mode 100644 index 016e7f7..0000000 --- a/DbMedia.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Microsoft.AspNetCore.Mvc.ModelBinding.Binders; -using Microsoft.EntityFrameworkCore; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using System.Net.NetworkInformation; - -namespace HyperBooru; - -public class DbMedia : DbObject { - public string Checksum { get; set; } - public string MimeType { get; set; } - public string? ShortDescription { get; set; } - public string? LongDescription { get; set; } - public virtual List UploadedFiles { get; set; } = new(); - - public bool IsIngest => Tags - .Select(t => t.TagDefinition) - .Any(td => td.Source == TagSource.Internal && td.Name == "ingest"); - - public DbMedia() => - base.ObjectType = ObjectType.Media; - - public string? DisplayName { - get { - if(ShortDescription is not null) - return ShortDescription; - - return UploadedFiles - .OrderBy(f => f.UploadTime) - .First()?.Filename; - } - } -} - -public record DbUploadedFile { - [Key] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int UploadedFileId { get; set; } - public string OriginalChecksum { get; set; } - public string? Filename { get; set; } - public DateTime UploadTime { get; set; } = DateTime.Now; - public DateTime? LastAccessTime { get; set; } - public DateTime? LastWriteTime { get; set; } - public DateTime? CreateTime { get; set; } - public virtual DbMedia Media { get; set; } -} \ No newline at end of file diff --git a/DbObject.cs b/DbObject.cs deleted file mode 100644 index f07b866..0000000 --- a/DbObject.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace HyperBooru; - -[Index(nameof(Guid))] -public class DbObject { - [Key] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int ObjectId { get; set; } - public Guid Guid { get; set; } = Guid.NewGuid(); - public ObjectType ObjectType { get; set; } - public virtual List Tags { get; set; } -} \ No newline at end of file diff --git a/DbTag.cs b/DbTag.cs deleted file mode 100644 index 4647f8e..0000000 --- a/DbTag.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace HyperBooru; - -public class DbTagDefinition : DbObject { - public TagSource Source { get; set; } = TagSource.Internal; - public string? Namespace { get; set; } - public string Name { get; set; } - public virtual List ImplicitTags { get; set; } = new(); - - public DbTagDefinition() => - base.ObjectType = ObjectType.TagDefinition; -} - -public class DbTag : DbObject { - public virtual DbTagDefinition TagDefinition { get; set; } - public DateTime CreateTime { get; set; } = DateTime.Now; - public virtual DbObject Target { get; set; } - - public DbTag() => - base.ObjectType = ObjectType.Tag; -} \ No newline at end of file diff --git a/DebugController.cs b/DebugController.cs deleted file mode 100644 index 7a87729..0000000 --- a/DebugController.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -namespace HyperBooru; - -#if DEBUG -[ApiController] -[Route("/debug")] -public class DebugController : Controller { - private HyperBooruDbContext db; - - public DebugController(HyperBooruDbContext db) => - this.db = db; - - [HttpGet("media")] - public IActionResult Media() => - Ok(db.Media.ToList()); - - [HttpGet("tags")] - public IActionResult TagDefinitions() => - Ok(db.Tags.ToList()); -} -#endif \ No newline at end of file diff --git a/Enum.cs b/Enum.cs deleted file mode 100644 index a8fc5b5..0000000 --- a/Enum.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace HyperBooru; - -public enum ObjectType { - TagDefinition, - Tag, - Media, - Collection -} - -public enum TagSource { - Internal, - UserTag -} \ No newline at end of file diff --git a/HBContext.cs b/HBContext.cs new file mode 100644 index 0000000..17b8b3d --- /dev/null +++ b/HBContext.cs @@ -0,0 +1,41 @@ +using Microsoft.EntityFrameworkCore; +using HyperBooru.Services; + +namespace HyperBooru; + +public class HBContext : DbContext { + public DbSet Objects { get; set; } + public DbSet TagDefinitions { get; set; } + public DbSet Tags { get; set; } + public DbSet Media { get; set; } + public DbSet UploadedFiles { get; set; } + + private IConfigService config; + + public HBContext(DbContextOptions options, IConfigService config) : base(options) => + this.config = config; + + protected override void OnConfiguring(DbContextOptionsBuilder options) { + options.UseLazyLoadingProxies(); + + var path = Path.Join(config.DataPath, "HyperBooru.db"); + options.UseSqlite($"Data Source = {config.DbPath}"); + + #if DEBUG + options.EnableSensitiveDataLogging(); + #endif + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) { + modelBuilder.Entity().ToTable("Objects"); + modelBuilder.Entity().ToTable("TagDefinitions"); + modelBuilder.Entity().ToTable("Tags"); + modelBuilder.Entity().ToTable("Media"); + modelBuilder.Entity().ToTable("UploadedFiles"); + + modelBuilder.Entity().HasData(new TagDefinition[] { + new() { ObjectId = -1, Source = TagSource.Internal, Name = "nsfw" }, + new() { ObjectId = -2, Source = TagSource.Internal, Name = "ingest" } + }); + } +} \ No newline at end of file diff --git a/HBObject.cs b/HBObject.cs new file mode 100644 index 0000000..8001ea3 --- /dev/null +++ b/HBObject.cs @@ -0,0 +1,14 @@ +using Microsoft.EntityFrameworkCore; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace HyperBooru; + +[Index(nameof(Guid))] +public class HBObject { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int ObjectId { get; set; } + public Guid Guid { get; set; } = Guid.NewGuid(); + public virtual List Tags { get; set; } = new(); +} \ No newline at end of file diff --git a/HyperBooru.cs b/HyperBooru.cs deleted file mode 100644 index e0e1b2d..0000000 --- a/HyperBooru.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using System.Text.Json.Serialization; - -namespace HyperBooru; - -public class HyperBooru { - public static void Main(string[] args) { - var builder = WebApplication.CreateBuilder(); - builder.Services.AddEndpointsApiExplorer(); - builder.Services.AddSwaggerGen(); - builder.Services.AddControllers().AddJsonOptions(o => { - var converter = new JsonStringEnumConverter(); - o.JsonSerializerOptions.Converters.Add(converter); - }); - builder.Services.AddRazorPages(); - builder.Services.AddSingleton(); - builder.Services.AddScoped(p => - new HyperBooruDbContext(p.GetRequiredService())); - - var app = builder.Build(); - - using var scope = app.Services.CreateScope(); - using var db = scope.ServiceProvider.GetRequiredService(); - db.Database.Migrate(); - - #if DEBUG - app.UseSwagger(); - app.UseSwaggerUI(); - #endif - - app.MapRazorPages(); - app.UseStaticFiles(); - app.UseHttpsRedirection(); - app.MapControllers(); - app.Run(); - } -} \ No newline at end of file diff --git a/HyperBooruContext.cs b/HyperBooruContext.cs deleted file mode 100644 index 3a9aa0f..0000000 --- a/HyperBooruContext.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using System.Text.Json; - -namespace HyperBooru; - -public class HyperBooruDbContext : DbContext { - public DbSet Objects { get; set; } - public DbSet TagDefinitions { get; set; } - public DbSet Tags { get; set; } - public DbSet Media { get; set; } - public DbSet UploadedFiles { get; set; } - - private IConfigService config; - - public HyperBooruDbContext(IConfigService config) => - this.config = config; - - protected override void OnConfiguring(DbContextOptionsBuilder options) { - options.UseLazyLoadingProxies(); - - var path = Path.Join(config.DataPath, "HyperBooru.db"); - options.UseSqlite($"Data Source = {config.DbPath}"); - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) { - modelBuilder.Entity().ToTable("Objects"); - modelBuilder.Entity().ToTable("TagDefinitions"); - modelBuilder.Entity().ToTable("Tags"); - modelBuilder.Entity().ToTable("Media"); - modelBuilder.Entity().ToTable("UploadedFiles"); - - modelBuilder.Entity().HasData(new DbTagDefinition[] { - new() { ObjectId = -1, Source = TagSource.Internal, Name = "nsfw" }, - new() { ObjectId = -2, Source = TagSource.Internal, Name = "ingest" } - }); - } -} \ No newline at end of file diff --git a/MainLayout.razor b/MainLayout.razor new file mode 100644 index 0000000..b34bf64 --- /dev/null +++ b/MainLayout.razor @@ -0,0 +1,15 @@ +@inherits LayoutComponentBase + + + +
+ + @*
*@ +
+ @Body +
+
diff --git a/MainLayout.razor.css b/MainLayout.razor.css new file mode 100644 index 0000000..e82e72e --- /dev/null +++ b/MainLayout.razor.css @@ -0,0 +1,39 @@ +div#navbar { + background: var(--col-navbar-bg); + box-shadow: rgba(0, 0, 0, 0.5) 0px 10px 10px; + display: flex; +} + +div#navbar > a { + color: white; + display: inline-block; + padding: 20px 20px 20px 20px; +} + +div#navbar > a:hover { + background: rgba(255, 255, 255, 0.4); + filter: none; +} + +div#navbar > a:active { + background: #fff; + color: var(--col-navbar-bg); +} + +div#navbar > input { + align-self: center; + background: var(--col-bg); + border-radius: 0; + color: white; + height: 40px !important; + margin: 0 20px 0 auto; + font-size: 12pt; + min-width: 30%; +} + +#content { + flex: 1 1 calc(100vh - 119px); + overflow-x: hidden; + overflow-y: auto; + padding: 30px; +} diff --git a/Media.cs b/Media.cs new file mode 100644 index 0000000..68ae4c4 --- /dev/null +++ b/Media.cs @@ -0,0 +1,43 @@ +using Microsoft.AspNetCore.Mvc.ModelBinding.Binders; +using Microsoft.EntityFrameworkCore; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Net.NetworkInformation; + +namespace HyperBooru; + +public class Media : HBObject { + public string Checksum { get; set; } + public string MimeType { get; set; } + public string? ShortDescription { get; set; } + public string? LongDescription { get; set; } + public virtual List UploadedFiles { get; set; } = new(); + + public bool IsIngest => Tags + .Select(t => t.TagDefinition) + .Any(td => td.Source == TagSource.Internal && td.Name == "ingest"); + + public string? DisplayName { + get { + if(ShortDescription is not null) + return ShortDescription; + + return UploadedFiles + .OrderBy(f => f.UploadTime) + .First()?.Filename; + } + } +} + +public record UploadedFile { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int UploadedFileId { get; set; } + public string OriginalChecksum { get; set; } + public string? Filename { get; set; } + public DateTime UploadTime { get; set; } = DateTime.Now; + public DateTime? LastAccessTime { get; set; } + public DateTime? LastWriteTime { get; set; } + public DateTime? CreateTime { get; set; } + public virtual Media Media { get; set; } +} \ No newline at end of file diff --git a/MediaController.cs b/MediaController.cs deleted file mode 100644 index 620b3ee..0000000 --- a/MediaController.cs +++ /dev/null @@ -1,200 +0,0 @@ -using HyperBooru.ApiRecords; -using ImageMagick; -using Microsoft.AspNetCore.Mvc; -using MimeDetective; -using System.Security.Cryptography; - -namespace HyperBooru; - -[ApiController] -[Route("/media")] -public class MediaController : Controller { - private IConfigService config; - private HyperBooruDbContext db; - - private ContentInspector inspector; - - public MediaController(IConfigService config, HyperBooruDbContext db) { - this.config = config; - this.db = db; - - ContentInspectorBuilder inspectorBuilder = new() { - Definitions = - MimeDetective.Definitions.Default.FileTypes.Images.All() - .Union(MimeDetective.Definitions.Default.FileTypes.Video.All()) - .ToList() - }; - - inspector = inspectorBuilder.Build(); - } - - [HttpGet("list")] - public IActionResult EnumerateMedia() => - Ok(db.Media.Select(m => m.ObjectId).ToArray()); - - [HttpGet("{mediaId}")] - public IActionResult Fetch([FromRoute] Guid mediaId) { - var media = db.Media.First(m => m.Guid == mediaId); - if(media is null) - return NotFound(); - - var fs = System.IO.File.OpenRead(config.GetPath(media)); - - return new FileStreamResult(fs, media.MimeType); - } - - [HttpGet("thumb/{mediaId}")] - public IActionResult Thumbnail( - [FromRoute] Guid mediaId, - [FromQuery] int? w, - [FromQuery] int? h) { - - var media = db.Media.First(m => m.Guid == mediaId); - if(media is null) - return NotFound(); - - if(media.MimeType.Split("/")[0] != "image") - return BadRequest("Media object not an image"); - - using var image = new MagickImage(config.GetPath(media)); - - if(w is null && h is null) - return BadRequest("Both width and height cannot be null!"); - - if(w > image.Width || h > image.Height) - return BadRequest("Requested thumbnail size is larger than original media"); - - int width = (int) (w is not null ? w : image.Width * h / image.Height); - int height = (int) (h is not null ? h : image.Height * w / image.Width); - - var thumbPath = config.GetPath(media, width, height); - - if(!System.IO.File.Exists(thumbPath)) { - image.Resize(new MagickGeometry(width, height)); - image.Write(thumbPath); - } - - var fs = System.IO.File.OpenRead(thumbPath); - - return new FileStreamResult(fs, "image/jpeg"); - } - - [HttpDelete("{mediaId}")] - public IActionResult Delete([FromRoute] Guid mediaId) { - var media = db.Media.First(m => m.Guid == mediaId); - if(media is null) - return NotFound(); - - System.IO.File.Delete(config.GetPath(media)); - - db.Media.Remove(media); - db.SaveChanges(); - return Ok(); - } - - [HttpGet("{mediaId}/info")] - public IActionResult GetInfo([FromRoute] Guid mediaId) { - var media = db.Media.First(m => m.Guid == mediaId); - if(media is null) - return NotFound(); - - return Ok(new MediaInfo(media)); - } - - [HttpPatch("{mediaId}/info")] - public IActionResult UpdateInfo( - [FromRoute] Guid mediaId, - [FromBody] MediaUpdateInfo updateInfo) { - - var media = db.Media.First(m => m.Guid == mediaId); - if(media is null) - return NotFound(); - - if(updateInfo.ShortDescription is not null) - media.ShortDescription = updateInfo.ShortDescription; - if(updateInfo.LongDescription is not null) - media.LongDescription = updateInfo.LongDescription; - - db.Update(media); - - db.SaveChanges(); - - return Ok(); - } - - [HttpPost] - public IActionResult Upload( - [FromForm] string? checksum, - [FromForm] DateTime? lastAccessTime, - [FromForm] DateTime? lastWriteTime, - [FromForm] DateTime? createTime) { - - using var transaction = db.Database.BeginTransaction(); - - var formFile = Request.Form.Files[0]; - if(formFile.Length < 1) - return BadRequest("Empty file"); - - var formStream = formFile.OpenReadStream(); - - // Calculate the checksum using the in-memory file contents - var hash = BitConverter - .ToString(MD5.Create().ComputeHash(formStream)) - .Replace("-", "") - .ToLower(); - - if(checksum is not null && hash != checksum.ToLower()) - return BadRequest("Checksum does not match"); - - var fileRecord = new DbUploadedFile() { - Filename = formFile.FileName, - OriginalChecksum = hash, - UploadTime = DateTime.Now, - LastAccessTime = lastAccessTime, - LastWriteTime = lastWriteTime, - CreateTime = createTime - }; - - formStream.Seek(0, SeekOrigin.Begin); - var defs = inspector.Inspect(formStream); - - var mime = defs.ByMimeType().FirstOrDefault()?.MimeType; - if(mime is null) - return BadRequest("Unsupported file type"); - - var media = db.Media - .FirstOrDefault(m => m.Checksum == hash); - - if(media is null) { - var ingestTagDef = db.TagDefinitions - .First(td => td.Source == TagSource.Internal && td.Name == "ingest"); - - media = new() { - Checksum = hash, - MimeType = mime, - UploadedFiles = new() { - fileRecord - }, - Tags = new() { - new() { TagDefinition = ingestTagDef } - } - }; - - using var newFile = System.IO.File.Create(config.GetPath(media)); - - formStream.Seek(0, SeekOrigin.Begin); - formStream.CopyTo(newFile); - newFile.Flush(); - - db.Media.Add(media); - } else { - media.UploadedFiles.Add(fileRecord); - db.Update(media); - } - - db.SaveChanges(); - transaction.Commit(); - - return Ok(media.Guid); - } -} \ No newline at end of file diff --git a/Pages/Component/Dialog.razor b/Pages/Component/Dialog.razor new file mode 100644 index 0000000..1e2929a --- /dev/null +++ b/Pages/Component/Dialog.razor @@ -0,0 +1,38 @@ +
+ @if(Title is not null) { +

@Title

+
+ } + @ChildContent +
+ +@code { + [Parameter] + public string? Title { get; set; } + + [Parameter] + public RenderFragment ChildContent { get; set; } + + public bool Visible { + get => visible; + set { + visible = value; + StateHasChanged(); + } + } + + [Parameter] + public int HeightPixels { set => height = $"{value}px"; } + [Parameter] + public int HeightPercent { set => height = $"{value}%"; } + + public void Show() => Visible = true; + public void Hide() => Visible = false; + + private bool visible = false; + + private string? height; + + private string style => + $"{(height is null ? "" : $"max-height:{height};")}"; +} diff --git a/Pages/Component/Dialog.razor.css b/Pages/Component/Dialog.razor.css new file mode 100644 index 0000000..ff34843 --- /dev/null +++ b/Pages/Component/Dialog.razor.css @@ -0,0 +1,21 @@ +div { + background: var(--col-dialog-bg); + border-radius: 20px; + box-shadow: 0px 5px 10px 10px rgb(0 0 0 / 25%); + display: flex; + flex-direction: column; + left: 50%; + opacity: 0; + padding: 20px; + position: absolute; + top: 50%; + transform: translate(-50%, -50%); + transition: visibility 0.1s, opacity 0.1s linear; + visibility: hidden; + width: 450px; +} + +div.visible { + opacity: 1; + visibility: visible; +} diff --git a/Pages/Component/MediaTagTable.razor b/Pages/Component/MediaTagTable.razor new file mode 100644 index 0000000..1bd4de4 --- /dev/null +++ b/Pages/Component/MediaTagTable.razor @@ -0,0 +1,69 @@ +@inject IDbContextFactory dbFactory +@inject ITagService tagService + + + + + + + + + + @foreach(var tag in userTags) { + bool isImplicit = IsImplicit(tag); + + + + + + } +
NamespaceTag Name
+ @if(isImplicit) { + @tag.Namespace + } else { + @tag.Namespace + } + + @if(isImplicit) { + @tag.Name + } else { + @tag.Name + } + Delete(tag))>Delete
+ +@code { + [Parameter] + public Media Media { get; set; } + + private IEnumerable userTags { + get { + using var db = dbFactory.CreateDbContext(); + if(db.Entry(Media).State == EntityState.Detached) + db.Attach(Media); + return GetTagRecursive( + Media.Tags + .Select(t => t.TagDefinition)) + .Where(td => td.Source == TagSource.UserTag) + .OrderBy(td => td.Namespace) + .ThenBy(td => td.Name) + .ToArray(); + } + } + + public void Refresh() => StateHasChanged(); + + private void Delete(TagDefinition tagDef) { + tagService.RemoveTag(Media, tagDef); + StateHasChanged(); + } + + private bool IsImplicit(TagDefinition tagDef) => + !Media.Tags + .Select(t => t.TagDefinition.Guid) + .Contains(tagDef.Guid); + + private IEnumerable GetTagRecursive(IEnumerable tagDefs) => + tagDefs + .Concat(tagDefs.SelectMany(td => GetTagRecursive(td.ImplicitTags))) + .DistinctBy(td => td.Guid); +} diff --git a/Pages/Component/MediaTagTable.razor.css b/Pages/Component/MediaTagTable.razor.css new file mode 100644 index 0000000..dcf5e09 --- /dev/null +++ b/Pages/Component/MediaTagTable.razor.css @@ -0,0 +1,3 @@ +td { + font-size: 8pt; +} diff --git a/Pages/Component/TabContainer.razor b/Pages/Component/TabContainer.razor new file mode 100644 index 0000000..3caab0b --- /dev/null +++ b/Pages/Component/TabContainer.razor @@ -0,0 +1,35 @@ + + + + + + @ChildContent + + +@code { + [Parameter] + public RenderFragment ChildContent { get; set; } + + public TabPane? ActivePane { get; set; } + List Panes = new(); + + public void AddPane(TabPane tabPane) { + Panes.Add(tabPane); + if(Panes.Count == 1) + ActivePane = tabPane; + StateHasChanged(); + } + + public void RemovePane(TabPane tabPane) { + if(ActivePane == tabPane) + ActivePane = Panes.ElementAtOrDefault(0); + Panes.Remove(tabPane); + StateHasChanged(); + } +} \ No newline at end of file diff --git a/Pages/Component/TabContainer.razor.css b/Pages/Component/TabContainer.razor.css new file mode 100644 index 0000000..6a56021 --- /dev/null +++ b/Pages/Component/TabContainer.razor.css @@ -0,0 +1,19 @@ +div.tabs { + display: inherit !important; + border-bottom: 1px solid white; +} + +div.tabs > a { + display: inline-block; + padding: 10px 10px 9px 10px; +} + +div.tabs > a.selected { + border-bottom: 4px solid white; + padding-bottom: 5px; +} + +div.tabs > a:hover { + background: rgba(255, 255, 255, 0.4); + filter: none; +} diff --git a/Pages/Component/TabPane.razor b/Pages/Component/TabPane.razor new file mode 100644 index 0000000..ba4a13a --- /dev/null +++ b/Pages/Component/TabPane.razor @@ -0,0 +1,29 @@ +@implements IDisposable + + + +@if(Parent.ActivePane == this) { + @ChildContent +} + +@code { + [CascadingParameter] + private TabContainer Parent { get; set; } + + [Parameter] + public string Title { get; set; } + + [Parameter] + public RenderFragment ChildContent { get; set; } + + protected override void OnInitialized() { + if (Parent is null) + throw new ArgumentNullException(nameof(Parent), "TabPane must exist within a TabContainer"); + + Parent.AddPane(this); + } + + public void Dispose() { + Parent.RemovePane(this); + } +} \ No newline at end of file diff --git a/Pages/Component/TabPane.razor.css b/Pages/Component/TabPane.razor.css new file mode 100644 index 0000000..5f28270 --- /dev/null +++ b/Pages/Component/TabPane.razor.css @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Pages/Component/TagSelectDialog.razor b/Pages/Component/TagSelectDialog.razor new file mode 100644 index 0000000..590c8f2 --- /dev/null +++ b/Pages/Component/TagSelectDialog.razor @@ -0,0 +1,59 @@ +@inject HBContext db + + + + + +
+ @foreach(var tagDef in tagDefinitions) { + Checked(tagDef, e.Value))/> + + } +
+
+ + +
+
+ +@code { + [Parameter] + public string? Title { get; set; } + + [Parameter] + public EventCallback OnSubmit { get; set; } + + public bool Visible { + get => visible; + set => visible = dialog.Visible = value; + } + + private bool visible; + + private Dialog dialog; + + private IEnumerable tagDefinitions => db.TagDefinitions + .Where(td => td.Source == TagSource.UserTag) + .OrderBy(td => td.Name); + + private List selected = new(); + + public void Show() => Visible = true; + public void Hide() => Visible = false; + + private async void Submit() { + await OnSubmit.InvokeAsync(selected.ToArray()); + selected.Clear(); + Hide(); + StateHasChanged(); + } + + private void Checked(TagDefinition tagDef, object? isChecked) { + if(isChecked is bool && (bool) isChecked == true) + if (!selected.Contains(tagDef)) + selected.Add(tagDef); + else + if (selected.Contains(tagDef)) + selected.Remove(tagDef); + } +} diff --git a/Pages/Component/TagSelectDialog.razor.css b/Pages/Component/TagSelectDialog.razor.css new file mode 100644 index 0000000..f6f704e --- /dev/null +++ b/Pages/Component/TagSelectDialog.razor.css @@ -0,0 +1,31 @@ +div.button-container { + display: flex; + justify-content: flex-end; +} + +div.tag-definitions { + overflow-y: auto; + user-select: none; +} + +div.tag-definitions label { + background: #555; + border-radius: 10px; + display: inline-block; + font-size: 10pt; + margin: 0 5px 5px 0; + padding: 5px 7px 5px 7px; + transition: background 0.1s linear; +} + +div.tag-definitions label:hover { + background: #777; +} + +div.tag-definitions input:checked + label { + background: #aaa; +} + +div.tag-definitions input { + display: none; +} diff --git a/Pages/Index.cshtml b/Pages/Index.cshtml deleted file mode 100644 index 80e05d9..0000000 --- a/Pages/Index.cshtml +++ /dev/null @@ -1,15 +0,0 @@ -@page -@model HyperBooru.Pages.IndexModel - - - -
- - -
- -@foreach(var media in Model.Media) { - - - -} \ No newline at end of file diff --git a/Pages/Index.cshtml.cs b/Pages/Index.cshtml.cs deleted file mode 100644 index 07d24f0..0000000 --- a/Pages/Index.cshtml.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.RazorPages; - -namespace HyperBooru.Pages; - -public class IndexModel : PageModel { - public IEnumerable Media { get; private set; } - - private HyperBooruDbContext db; - - public IndexModel(HyperBooruDbContext db) => - this.db = db; - - public void OnGet([FromQuery(Name = "q")] string? query) { - IEnumerable media; - - if(query is null) { - media = db.UploadedFiles - .OrderByDescending(uf => uf.UploadTime) - .Select(uf => uf.Media) - .Distinct(); - } else { - query = query.ToLower(); - - var matchingTags = db.TagDefinitions - .Where(td => td.Name.ToLower().Contains(query)) - .Select(td => td.Guid); - - media = db.Media - .Where(m => - m.Tags - .Select(t => t.TagDefinition.Guid) - .Intersect(matchingTags) - .Any()); - } - - Media = media.OrderByDescending(m => - m.UploadedFiles - .Select(uf => uf.UploadTime) - .Order() - .Last()); - } -} \ No newline at end of file diff --git a/Pages/Index.cshtml.css b/Pages/Index.cshtml.css deleted file mode 100644 index f573988..0000000 --- a/Pages/Index.cshtml.css +++ /dev/null @@ -1,7 +0,0 @@ -img { - max-height: 200px; -} - -form#upload { - padding-bottom: 30px; -} \ No newline at end of file diff --git a/Pages/Index.razor b/Pages/Index.razor new file mode 100644 index 0000000..a69d69c --- /dev/null +++ b/Pages/Index.razor @@ -0,0 +1,17 @@ +@page "/" +@inject HBContext db; + +Gallery + + + +
+ + +
+ +@foreach(var media in db.Media) { + + + +} diff --git a/Pages/Index.razor.css b/Pages/Index.razor.css new file mode 100644 index 0000000..d1750b4 --- /dev/null +++ b/Pages/Index.razor.css @@ -0,0 +1,8 @@ +img { + margin-right: 5px; + max-height: 200px; +} + +form#upload { + padding-bottom: 30px; +} \ No newline at end of file diff --git a/Pages/Shared/_Layout.cshtml b/Pages/Shared/_Layout.cshtml deleted file mode 100644 index 9d6d382..0000000 --- a/Pages/Shared/_Layout.cshtml +++ /dev/null @@ -1,26 +0,0 @@ -@{ - ViewBag.Title ??= "HyperBooru"; - ViewBag.ContentScroll ??= true; - ViewBag.ContentMargin ??= "30px"; -} - - - - - - - - - @ViewBag.Title - - - -
- @RenderBody() -
- - \ No newline at end of file diff --git a/Pages/Shared/_Layout.cshtml.css b/Pages/Shared/_Layout.cshtml.css deleted file mode 100644 index 5b2f26e..0000000 --- a/Pages/Shared/_Layout.cshtml.css +++ /dev/null @@ -1,36 +0,0 @@ -div#navbar { - background: var(--col-navbar-bg); - box-shadow: rgba(0, 0, 0, 0.5) 0px 10px 10px; - display: flex; -} - -div#navbar > a { - color: white; - display: inline-block; - padding: 20px 20px 20px 20px; -} - -div#navbar > a:hover { - background: rgba(255, 255, 255, 0.4); - filter: none; -} - -div#navbar > a:active { - background: #fff; - color: var(--col-navbar-bg); -} - -div#navbar > input { - align-self: center; - background: var(--col-bg); - border-radius: 0; - color: white; - height: 40px !important; - margin: 0 20px 0 auto; - font-size: 12pt; - min-width: 30%; -} - -#content { - flex: 1 1 calc(100vh - 119px); -} \ No newline at end of file diff --git a/Pages/TagDefinitions.cshtml b/Pages/TagDefinitions.cshtml deleted file mode 100644 index 7bf1790..0000000 --- a/Pages/TagDefinitions.cshtml +++ /dev/null @@ -1,108 +0,0 @@ -@page -@model HyperBooru.Pages.TagDefinitionsModel -@{ - ViewBag.Title = "Tag Definitions"; -} - - - - - - - - - - - - - - @foreach(var tagDef in Model.TagDefinitions) { - - - - - - - - } -
GuidSourceNamespaceName
@tagDef.Guid@tagDef.Source@tagDef.Namespace@tagDef.NameDelete
- -
- -
- -
-

Create a new tag definition

-
-
- - - - -
- - -
-
-
- -
-

Are you sure you want to delete this tag definition?

-
-
- - -
-
diff --git a/Pages/TagDefinitions.cshtml.cs b/Pages/TagDefinitions.cshtml.cs deleted file mode 100644 index afbad87..0000000 --- a/Pages/TagDefinitions.cshtml.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.RazorPages; - -namespace HyperBooru.Pages; - -public class TagDefinitionsModel : PageModel { - public IEnumerable TagDefinitions => - db.TagDefinitions.Where(td => td.Source == TagSource.UserTag); - - private HyperBooruDbContext db; - - public TagDefinitionsModel(HyperBooruDbContext db) => - this.db = db; - - public void OnGet() {} -} diff --git a/Pages/TagDefinitions.cshtml.css b/Pages/TagDefinitions.cshtml.css deleted file mode 100644 index 93001c7..0000000 --- a/Pages/TagDefinitions.cshtml.css +++ /dev/null @@ -1,12 +0,0 @@ -form > input { - width: 100%; -} - -div.button-container { - display: flex; - justify-content: flex-end; -} - -table#tag-definitions td:first-child { - font-family: 'Lucida Console'; -} \ No newline at end of file diff --git a/Pages/TagDefinitions.razor b/Pages/TagDefinitions.razor new file mode 100644 index 0000000..e48402b --- /dev/null +++ b/Pages/TagDefinitions.razor @@ -0,0 +1,92 @@ +@page "/TagDefinitions" +@inject IDbContextFactory dbFactory +@inject ITagService tagService + +Tag Definitions + + + + + + + + + + + + @foreach(var tagDef in tagDefinitions) { + + + + + + + + } +
GuidSourceNamespaceName
@tagDef.Guid@tagDef.Source@tagDef.Namespace@tagDef.Name + PromptToDelete(tagDef))> + Delete + +
+ +
+ +
+ + +
+ + + + +
+ + +
+
+
+ + +
+ + +
+
+ +@code { + private Dialog createTagDialog; + private Dialog deleteTagDialog; + + private string tagName; + private string? tagNamespace; + + private TagDefinition? toDelete; + + private IEnumerable tagDefinitions => + dbFactory.CreateDbContext().TagDefinitions + .Where(td => td.Source == TagSource.UserTag) + .OrderBy(td => td.Namespace) + .ThenBy(td => td.Name); + + private void CreateTagDefinition() { + if(string.IsNullOrEmpty(tagNamespace)) + tagNamespace = null; + + tagService.CreateTagDefinition(tagName, tagNamespace); + createTagDialog.Hide(); + StateHasChanged(); + } + + private void PromptToDelete(TagDefinition toDelete) { + this.toDelete = toDelete; + deleteTagDialog.Show(); + } + + private void DeleteTagDefinition() { + if(toDelete is null) + return; + + tagService.DeleteTagDefinition(toDelete); + deleteTagDialog.Hide(); + } +} diff --git a/Pages/TagDefinitions.razor.css b/Pages/TagDefinitions.razor.css new file mode 100644 index 0000000..93001c7 --- /dev/null +++ b/Pages/TagDefinitions.razor.css @@ -0,0 +1,12 @@ +form > input { + width: 100%; +} + +div.button-container { + display: flex; + justify-content: flex-end; +} + +table#tag-definitions td:first-child { + font-family: 'Lucida Console'; +} \ No newline at end of file diff --git a/Pages/ViewMedia.cshtml b/Pages/ViewMedia.cshtml deleted file mode 100644 index e77ec22..0000000 --- a/Pages/ViewMedia.cshtml +++ /dev/null @@ -1,181 +0,0 @@ -@page -@model HyperBooru.Pages.ViewMediaModel -@{ - ViewBag.ContentScroll = false; -} - - - - - -
- -
-
- File Info - Tags -
-@*
- - - - - -
*@ -
-

Upload history

-
- - - - - - - - - @foreach(var file in Model.Media.UploadedFiles) { - - - - - - - - } -
Created OnLast WriteUploaded OnFilenameOriginal Checksum
@(file.CreateTime?.ToString() ?? "N/A")@(file.LastWriteTime?.ToString() ?? "N/A")@file.UploadTime@file.Filename@file.OriginalChecksum
-
- - -
-
-
- - - - - - - @foreach(var tag in Model.UserTags) { - bool isImplicit = Model.IsImplicit(tag); - - - - - - } -
NamespaceTag Name
- @if(isImplicit) { - @tag.Namespace - } else { - @tag.Namespace - } - - @if(isImplicit) { - @tag.Name - } else { - @tag.Name - } - Delete
-
- - -
-
-
-
- -
-

Delete this media?

-
-
- - -
-
- -
-

Select one or more tag(s) to add

-
- -
- @foreach(var tagdef in Model.TagDefinitions) { - - - } -
-
- - -
-
diff --git a/Pages/ViewMedia.cshtml.cs b/Pages/ViewMedia.cshtml.cs deleted file mode 100644 index 76c515b..0000000 --- a/Pages/ViewMedia.cshtml.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.RazorPages; - -namespace HyperBooru.Pages; - -public class ViewMediaModel : PageModel { - public DbMedia Media { get; private set; } - - public DbTagDefinition[] UserTags { get; private set; } - - public IEnumerable TagDefinitions => - db.TagDefinitions.Where(td => td.Source == TagSource.UserTag); - - private HyperBooruDbContext db; - - public ViewMediaModel(HyperBooruDbContext db) => - this.db = db; - - public IActionResult OnGet([FromQuery(Name = "m")] Guid mediaId) { - Media = db.Media.First(m => m.Guid == mediaId); - if(Media is null) - return NotFound(); - - UserTags = GetTagRecursive( - Media.Tags - .Select(t => t.TagDefinition)) - .OrderBy(td => td.Namespace) - .ThenBy(td => td.Name) - .ToArray(); - - return Page(); - } - - public bool IsImplicit(DbTagDefinition tagDef) => - !Media.Tags - .Select(t => t.TagDefinition.Guid) - .Contains(tagDef.Guid); - - private IEnumerable GetTagRecursive(IEnumerable tagDefs) => - tagDefs - .Concat(tagDefs.SelectMany(td => GetTagRecursive(td.ImplicitTags))) - .DistinctBy(td => td.Guid); -} \ No newline at end of file diff --git a/Pages/ViewMedia.cshtml.css b/Pages/ViewMedia.cshtml.css deleted file mode 100644 index 29094b8..0000000 --- a/Pages/ViewMedia.cshtml.css +++ /dev/null @@ -1,97 +0,0 @@ -div#content { - display: flex; - align-items: start; - height: 100%; -} - -div#content > img { - max-width: 60%; - height: 100%; - object-fit: contain; -} - -div#metadata { - margin-left: 15px; - width: 100%; -} - -div#metadata > div { - display: none; -} - -div#metadata > div.selected { - display: inherit !important; -} - -div#metadata-header { - display: inherit !important; - border-bottom: 1px solid white; -} - -div#metadata-header > a { - display: inline-block; - padding: 10px 10px 9px 10px; -} - -div#metadata-header > a.selected { - border-bottom: 4px solid white; - padding-bottom: 5px; -} - -div#metadata-header > a:hover { - background: rgba(255, 255, 255, 0.4); - filter: none; -} - -div#metadata-fileinfo > table th { - font-size: 8pt; -} - -div#metadata-fileinfo > table td { - font-family: 'Lucida Console'; - font-size: 8pt; -} - -div#metadata-fileinfo button#delete-button { - background: #ff4848; -} - -div#metadata-tags > table td { - font-size: 8pt; -} - -div.button-container { - display: flex; - justify-content: flex-end; -} - -div#tag-dialog { - max-height: 400px; -} - -div#tag-dialog div#tag-definitions { - overflow-y: auto; - user-select: none; -} - -div#tag-dialog div#tag-definitions label { - background: #555; - border-radius: 10px; - display: inline-block; - font-size: 10pt; - margin: 0 5px 5px 0; - padding: 5px 7px 5px 7px; - transition: background 0.1s linear; -} - -div#tag-dialog div#tag-definitions label:hover { - background: #777; -} - -div#tag-dialog div#tag-definitions input:checked + label { - background: #aaa; -} - -div#tag-dialog div#tag-definitions input { - display: none; -} diff --git a/Pages/ViewMedia.razor b/Pages/ViewMedia.razor new file mode 100644 index 0000000..8436159 --- /dev/null +++ b/Pages/ViewMedia.razor @@ -0,0 +1,106 @@ +@page "/ViewMedia" +@inject IDbContextFactory dbFactory +@inject ITagService tagService + +@title + + + +
+ +
+ + +
+

Title: @(@media.ShortDescription ?? "None")

+

Description: @(media.LongDescription ?? "None")

+

Upload history

+
+ + + + + + + + + @foreach(var file in media.UploadedFiles) { + + + + + + + + } +
Created OnLast WriteUploaded OnFilenameOriginal Checksum
@(file.CreateTime?.ToString() ?? "N/A")@(file.LastWriteTime?.ToString() ?? "N/A")@file.UploadTime@file.Filename@file.OriginalChecksum
+
+ + +
+
+
+ +
+ +
+ + @if(media.IsIngest) { + + } else { + + } +
+
+
+
+
+
+ + +
+ + +
+
+ + + +@code { + [Parameter] + [SupplyParameterFromQuery(Name = "m")] + public Guid MediaId { get; set; } + + private Media media; + + private string title; + + private bool infoEditMode = false; + + private Dialog deleteDialog; + private TagSelectDialog tagDialog; + private MediaTagTable mediaTagTable; + + protected override void OnInitialized() { + using var db = dbFactory.CreateDbContext(); + media = db.Media.AsNoTracking().First(m => m.Guid == MediaId); + if(media is null) + throw new ArgumentException("Media not found!"); + + title = media.DisplayName ?? "Media View"; + } + + private void AddTags(TagDefinition[] tagDefs) { + Console.WriteLine($"Adding tags: {string.Join(", ", tagDefs.Select(td => td.Name))}"); + foreach(var tagDef in tagDefs) + tagService.AddTag(media, tagDef); + mediaTagTable.Refresh(); + } + + private void SetIngest(bool ingest) { + StateHasChanged(); + } +} diff --git a/Pages/ViewMedia.razor.css b/Pages/ViewMedia.razor.css new file mode 100644 index 0000000..abf8e08 --- /dev/null +++ b/Pages/ViewMedia.razor.css @@ -0,0 +1,30 @@ +div#content { + display: flex; + align-items: start; + height: 100%; +} + +div#content > img { + max-width: 60%; + height: 100%; + object-fit: contain; +} + +div#metadata { + margin-left: 15px; + width: 100%; +} + +div#metadata-fileinfo > table th { + font-size: 8pt; +} + +div#metadata-fileinfo > table td { + font-family: 'Lucida Console'; + font-size: 8pt; +} + +div.button-container { + display: flex; + justify-content: flex-end; +} diff --git a/Pages/_Host.cshtml b/Pages/_Host.cshtml new file mode 100644 index 0000000..e01b94d --- /dev/null +++ b/Pages/_Host.cshtml @@ -0,0 +1,33 @@ +@page "/" +@using Microsoft.AspNetCore.Components.Web +@namespace HyperBooru.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers + + + + + + + + + + + + + + + +
+ + An error has occurred. This application may no longer respond until reloaded. + + + An unhandled exception has occurred. See browser dev tools for details. + + Reload + 🗙 +
+ + + + diff --git a/Pages/_ViewStart.cshtml b/Pages/_ViewStart.cshtml deleted file mode 100644 index 1af6e49..0000000 --- a/Pages/_ViewStart.cshtml +++ /dev/null @@ -1,3 +0,0 @@ -@{ - Layout = "_Layout"; -} \ No newline at end of file diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..dfc635c --- /dev/null +++ b/Program.cs @@ -0,0 +1,39 @@ +using Microsoft.EntityFrameworkCore; +using System.Text.Json.Serialization; +using HyperBooru.Services; + +namespace HyperBooru; + +public class Program { + public static void Main(string[] args) { + var builder = WebApplication.CreateBuilder(args); + builder.Services.AddControllers().AddJsonOptions(o => { + var converter = new JsonStringEnumConverter(); + o.JsonSerializerOptions.Converters.Add(converter); + }); + builder.Services.AddRazorPages(); + builder.Services.AddServerSideBlazor(); + + // Add out custom services + builder.Services.AddSingleton(); + builder.Services.AddDbContextFactory(); + builder.Services.AddScoped(); + + var app = builder.Build(); + + // Ensure database is created and it's schema is up to date + using var scope = app.Services.CreateScope(); + using var db = scope.ServiceProvider.GetRequiredService(); + db.Database.Migrate(); + + app.UseHsts(); + app.UseHttpsRedirection(); + app.UseStaticFiles(); + app.UseRouting(); + app.MapBlazorHub(); + app.MapControllers(); + app.MapFallbackToPage("/_Host"); + + app.Run(); + } +} diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json index 696a6ea..9f4966c 100644 --- a/Properties/launchSettings.json +++ b/Properties/launchSettings.json @@ -1,31 +1,31 @@ { "profiles": { - "HyperBooru": { - "commandName": "Project", + "WSL": { + "commandName": "WSL2", "launchBrowser": true, + "launchUrl": "https://localhost:7132", "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" + "ASPNETCORE_ENVIRONMENT": "Development", + "ASPNETCORE_URLS": "https://localhost:7132;http://localhost:5186" }, - "applicationUrl": "https://localhost:7284", - "dotnetRunMessages": true, - "nativeDebugging": false + "distributionName": "" }, - "IIS Express": { - "commandName": "IISExpress", + "HyperBooru": { + "commandName": "Project", "launchBrowser": true, - "launchUrl": "swagger", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" - } + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:7132;http://localhost:5186" } }, - "$schema": "https://json.schemastore.org/launchsettings.json", "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { - "applicationUrl": "http://localhost:62281", - "sslPort": 44394 + "applicationUrl": "http://localhost:1922", + "sslPort": 44354 } } } \ No newline at end of file diff --git a/Server.csproj b/Server.csproj index f7bf989..5eb5f1d 100644 --- a/Server.csproj +++ b/Server.csproj @@ -6,6 +6,17 @@ enable HyperBooru HyperBooru + 0.1.0.0 + $(AssemblyVersion) + $(AssemblyVersion) + + + + 1701;1702;8618 + + + + 1701;1702;8618 @@ -20,4 +31,9 @@ + + + + + diff --git a/Services/ConfigService.cs b/Services/ConfigService.cs new file mode 100644 index 0000000..6ba3936 --- /dev/null +++ b/Services/ConfigService.cs @@ -0,0 +1,81 @@ +namespace HyperBooru.Services; + +public interface IConfigService { + public string DataPath { get; } + public string DbPath { get; } + public string MediaBasePath { get; } + + public string GetPath(Media media); + public string GetPath(Media media, int width, int height); +} + +public class ConfigService : IConfigService { + private IConfiguration config; + + private const string AppName = "HyperBooru"; + + public string DataPath { + get { + #if DEBUG + return "Data"; + #else + string? path = config["DataPath"]; + if(path is not null) + return path; + + switch(Environment.OSVersion.Platform) { + case PlatformID.Win32NT: + return Path.Join( + Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), + AppName); + case PlatformID.Unix: + return $"/var/lib/{AppName.ToLower()}"; + default: + throw new NotImplementedException( + $"Unknown Operating System: {Environment.OSVersion.Platform}"); + } + #endif + } + } + + public string DbPath => + Path.Join(DataPath, $"{AppName}.db"); + + public string MediaBasePath => + Path.Join(DataPath, "media"); + + public string ThumbnailBasePath => + Path.Join(DataPath, "thumb"); + + public ConfigService(IConfiguration config) { + this.config = config; + InitDirectoryStructure(); + } + + public string GetPath(Media media) { + var fileInfo = new FileInfo(Path.Join( + MediaBasePath, + media.Guid.ToString().Substring(0, 2), + media.Guid.ToString().Substring(2, 2), + media.Guid.ToString())); + + Directory.CreateDirectory(fileInfo.Directory.FullName); + return fileInfo.FullName; + } + + public string GetPath(Media media, int width, int height) { + var fileInfo = new FileInfo(Path.Join( + ThumbnailBasePath, + media.Guid.ToString().Substring(0, 2), + media.Guid.ToString().Substring(2, 2), + $"{media.Guid.ToString()}-{width}-{height}")); + + Directory.CreateDirectory(fileInfo.Directory.FullName); + return fileInfo.FullName; + } + + private void InitDirectoryStructure() { + Directory.CreateDirectory(DataPath); + Directory.CreateDirectory(MediaBasePath); + } +} \ No newline at end of file diff --git a/Services/MediaService.cs b/Services/MediaService.cs new file mode 100644 index 0000000..be2657a --- /dev/null +++ b/Services/MediaService.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore; + +namespace HyperBooru.Services; + +public interface IMediaService { + public void SetIngest(Media media, bool ingest); +} + +public class MediaService : IMediaService { + private IDbContextFactory dbFactory; + + public MediaService(IDbContextFactory dbFactory) => + this.dbFactory = dbFactory; + + public void SetIngest(Media media, bool ingest) { + using var db = dbFactory.CreateDbContext(); + var ingestTag = db.TagDefinitions + .First(td => td.Source == TagSource.Internal && td.Name == "ingest"); + + if(ingest) + media.Tags.Add(new() { TagDefinition = ingestTag }); + else + media.Tags.Remove( + media.Tags.First(t => t.TagDefinition.Guid == ingestTag.Guid)); + + db.SaveChanges(); + } +} diff --git a/Services/TagService.cs b/Services/TagService.cs new file mode 100644 index 0000000..e0dcf64 --- /dev/null +++ b/Services/TagService.cs @@ -0,0 +1,97 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace HyperBooru.Services; + +public interface ITagService { + public void AddTag(HBObject obj, TagDefinition tagDef); + public void RemoveTag(HBObject obj, TagDefinition tagDef); + public void AddImplicitTag(TagDefinition tagDef, TagDefinition implicitTagDef); + public void RemoveImplicitTag(TagDefinition tagDef, TagDefinition implicitTagDef); + public void CreateTagDefinition(string name, string? @namespace); + public void DeleteTagDefinition(TagDefinition tagDef); +} + +public class TagService : ITagService { + private IDbContextFactory dbFactory; + + public TagService(IDbContextFactory dbFactory) => + this.dbFactory = dbFactory; + + public void AddTag(HBObject obj, TagDefinition tagDef) { + using var db = dbFactory.CreateDbContext(); + db.Entry(obj).State = EntityState.Unchanged; + + bool alreadyTagged = obj.Tags + .Select(t => t.TagDefinition.Guid) + .Contains(tagDef.Guid); + + obj.Tags.Add(new() { + TagDefinition = tagDef, + Target = obj + }); + + db.SaveChanges(); + } + + public void RemoveTag(Guid obj, Guid tagDef) { + using var db = dbFactory.CreateDbContext(); + + db.Objects.First(o => o.Guid == obj).Tags + .RemoveAll(t => t.TagDefinition.Guid == tagDef); + + db.SaveChanges(); + } + + public void RemoveTag(HBObject obj, TagDefinition tagDef) => + RemoveTag(obj.Guid, tagDef.Guid); + + public void AddImplicitTag(TagDefinition tagDef, TagDefinition implicitTagDef) { + using var db = dbFactory.CreateDbContext(); + db.Entry(tagDef).State = EntityState.Unchanged; + + if(tagDef.ImplicitTags.Select(td => td.Guid).Contains(implicitTagDef.Guid)) + throw new ArgumentException("Tag definition already contains implicit tag"); + + tagDef.ImplicitTags.Add(implicitTagDef); + db.SaveChanges(); + } + + public void RemoveImplicitTag(TagDefinition tagDef, TagDefinition implicitTagDef) { + using var db = dbFactory.CreateDbContext(); + db.Entry(tagDef).State = EntityState.Unchanged; + + if(!tagDef.ImplicitTags.Select(td => td.Guid).Contains(implicitTagDef.Guid)) + throw new ArgumentException("Tag definition doesn't contain implicit tag"); + + tagDef.ImplicitTags.Remove(implicitTagDef); + db.SaveChanges(); + } + + public void CreateTagDefinition(string name, string? @namespace) { + using var db = dbFactory.CreateDbContext(); + + TagDefinition tagdef = new() { + Source = TagSource.UserTag, + Namespace = @namespace, + Name = name + }; + if(!db.TagDefinitions.Contains(tagdef)) + db.TagDefinitions.Add(tagdef); + db.SaveChanges(); + } + + public void DeleteTagDefinition(TagDefinition tagDef) { + using var db = dbFactory.CreateDbContext(); + db.Entry(tagDef).State = EntityState.Unchanged; + + using var transaction = db.Database.BeginTransaction(); + + db.Tags.RemoveRange( + db.Tags.Where(t => t.TagDefinition == tagDef)); + db.TagDefinitions.Remove(tagDef); + db.SaveChanges(); + + transaction.Commit(); + } +} \ No newline at end of file diff --git a/Tag.cs b/Tag.cs new file mode 100644 index 0000000..c662c42 --- /dev/null +++ b/Tag.cs @@ -0,0 +1,22 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace HyperBooru; + +public enum TagSource { + Internal, + UserTag +} + +public class TagDefinition : HBObject { + public TagSource Source { get; set; } = TagSource.Internal; + public string? Namespace { get; set; } + public string Name { get; set; } + public virtual List ImplicitTags { get; set; } = new(); +} + +public class Tag : HBObject { + public virtual TagDefinition TagDefinition { get; set; } + public DateTime CreateTime { get; set; } = DateTime.Now; + public virtual HBObject Target { get; set; } +} \ No newline at end of file diff --git a/TagController.cs b/TagController.cs deleted file mode 100644 index 6972da2..0000000 --- a/TagController.cs +++ /dev/null @@ -1,129 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -namespace HyperBooru; - -[ApiController] -[Route("/api/tag")] -public class TagController : Controller { - private HyperBooruDbContext db; - - public TagController(HyperBooruDbContext db) => - this.db = db; - - [HttpPost("{objectId}/{tagId}")] - public IActionResult AddTag([FromRoute] Guid objectId, [FromRoute] Guid tagId) { - var obj = db.Objects.First(o => o.Guid == objectId); - var def = db.TagDefinitions.First(d => d.Guid == tagId); - - if(obj is null || def is null) - return NotFound(); - - bool alreadyTagged = obj.Tags - .Select(t => t.TagDefinition.Guid) - .Contains(def.Guid); - - if(alreadyTagged) - return BadRequest(); - - obj.Tags.Add(new() { - TagDefinition = def, - Target = obj - }); - - db.SaveChanges(); - - return Ok(); - } - - [HttpDelete("{objectId}/{tagId}")] - public IActionResult RemoveTag([FromRoute] Guid objectId, [FromRoute] Guid tagId) { - var obj = db.Objects.First(o => o.Guid == objectId); - - if(obj is null) - return NotFound(); - - var tag = obj.Tags - .First(t => t.Guid == tagId || t.TagDefinition.Guid == tagId); - - if(tag is null) - return NotFound(); - - obj.Tags.Remove(tag); - db.SaveChanges(); - - return Ok(); - } - - [HttpPost("{tagId}/implicit/{implicitTagId}")] - public IActionResult AddImplicitTag( - [FromRoute] Guid tagId, - [FromRoute] Guid implicitTagId) { - - var tagDef = db.TagDefinitions.First(td => td.Guid == tagId); - var implicitTagDef = db.TagDefinitions.First(td => td.Guid == implicitTagId); - - if(tagDef is null || implicitTagDef is null) - return NotFound(); - - if(tagDef.ImplicitTags.Select(td => td.Guid).Contains(implicitTagId)) - return BadRequest(); - - tagDef.ImplicitTags.Add(implicitTagDef); - db.SaveChanges(); - - return Ok(); - } - - [HttpDelete("{tagId}/implicit/{implicitTagId}")] - public IActionResult RemoveImplicitTag( - [FromRoute] Guid tagId, - [FromRoute] Guid implicitTagId) { - - var tagDef = db.TagDefinitions.First(td => td.Guid == tagId); - var implicitTagDef = db.TagDefinitions.First(td => td.Guid == implicitTagId); - - if(tagDef is null || implicitTagDef is null) - return NotFound(); - - if(!tagDef.ImplicitTags.Select(td => td.Guid).Contains(implicitTagId)) - return BadRequest(); - - tagDef.ImplicitTags.Remove(implicitTagDef); - db.SaveChanges(); - - return Ok(); - } - - [HttpPost("def")] - public void CreateTagDefinition( - [FromForm] string name, - [FromForm] string? @namespace) { - - DbTagDefinition tagdef = new() { - Source = TagSource.UserTag, - Namespace = @namespace, - Name = name - }; - if(!db.TagDefinitions.Contains(tagdef)) - db.TagDefinitions.Add(tagdef); - db.SaveChanges(); - } - - [HttpDelete("def/{tagId}")] - public IActionResult DeleteTagDefinition([FromRoute] Guid tagId) { - var tagdef = db.TagDefinitions.First(td => td.Guid == tagId); - if(tagdef is null) - return NotFound(); - - using var transaction = db.Database.BeginTransaction(); - - db.Tags.RemoveRange( - db.Tags.Where(t => t.TagDefinition == tagdef)); - db.TagDefinitions.Remove(tagdef); - db.SaveChanges(); - - transaction.Commit(); - - return Ok(); - } -} \ No newline at end of file diff --git a/_Imports.razor b/_Imports.razor new file mode 100644 index 0000000..d20a491 --- /dev/null +++ b/_Imports.razor @@ -0,0 +1,7 @@ +@using HyperBooru +@using HyperBooru.Pages.Component +@using HyperBooru.Services +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.EntityFrameworkCore +@using Microsoft.JSInterop diff --git a/appsettings.Development.json b/appsettings.Development.json index 0c208ae..770d3e9 100644 --- a/appsettings.Development.json +++ b/appsettings.Development.json @@ -1,4 +1,5 @@ { + "DetailedErrors": true, "Logging": { "LogLevel": { "Default": "Information", diff --git a/wwwroot/css/site.css b/wwwroot/css/site.css new file mode 100644 index 0000000..65147c2 --- /dev/null +++ b/wwwroot/css/site.css @@ -0,0 +1,28 @@ +#blazor-error-ui { + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + +#blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 3.5rem; + top: 0.5rem; +} + +.blazor-error-boundary { + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; + color: white; + padding: 1rem 1rem 1rem 3.7rem; +} + +.blazor-error-boundary::after { + content: "An error has occurred." +} diff --git a/wwwroot/favicon.ico b/wwwroot/favicon.ico new file mode 100644 index 0000000..a1be4cc Binary files /dev/null and b/wwwroot/favicon.ico differ diff --git a/wwwroot/icon-192.png b/wwwroot/icon-192.png new file mode 100644 index 0000000..28ce06d Binary files /dev/null and b/wwwroot/icon-192.png differ diff --git a/wwwroot/icon-512.png b/wwwroot/icon-512.png new file mode 100644 index 0000000..8c28696 Binary files /dev/null and b/wwwroot/icon-512.png differ diff --git a/wwwroot/manifest.webmanifest b/wwwroot/manifest.webmanifest new file mode 100644 index 0000000..f150f98 --- /dev/null +++ b/wwwroot/manifest.webmanifest @@ -0,0 +1,6 @@ +{ + "icons": [ + { "src": "/icon-192.png", "type": "images/png", "sizes": "192x192" }, + { "src": "/icon-512.png", "type": "images/png", "sizes": "512x512" } + ] +} \ No newline at end of file diff --git a/wwwroot/styles/data-table.css b/wwwroot/styles/data-table.css new file mode 100644 index 0000000..6256fc7 --- /dev/null +++ b/wwwroot/styles/data-table.css @@ -0,0 +1,21 @@ +table.data-table { + border-collapse: collapse; + width: 100%; +} + +table.data-table > tr > th { + border-bottom: 1px solid white; + padding: 4px; +} + +table.data-table > tr > td { + padding: 4px; +} + +table.data-table > tr:nth-child(2n) { + background: rgba(255, 255, 255, 0.2); +} + +table.data-table > tr > td:not(:last-child) { + border-right: 1px solid white; +} diff --git a/wwwroot/styles/global.css b/wwwroot/styles/global.css index 83d76ec..d79b27c 100644 --- a/wwwroot/styles/global.css +++ b/wwwroot/styles/global.css @@ -1,4 +1,6 @@ -:root { +@import url('data-table.css'); + +:root { --col-accent-pri: #0aa; --col-accent-pri-hl: #0cc; --col-bg: #222; @@ -12,6 +14,8 @@ --col-button-sec-hl: #777; --col-button-sec-disabled: #555; --col-button-sec-disabled-bg: #000; + --col-button-warning: #ff4848; + --col-button-warning-hl: #ff9999; --col-scrollbar: #666666; --col-scrollbar-hover: #aaaaaa; } @@ -52,6 +56,19 @@ button:disabled { background: var(--col-button-disabled-bg) !important; } +button.warning { + background: var(--col-button-warning); +} + +button.warning:hover { + background: var(--col-button-warning-hl); +} + +button.warning:active { + color: var(--col-button-warning); + background: white; +} + button.secondary { background: var(--col-button-sec); } @@ -94,50 +111,6 @@ hr { width: 100%; } -table.data-table { - border-collapse: collapse; - width: 100%; -} - -table.data-table > tbody > tr > th { - border-bottom: 1px solid white; - padding: 4px; -} - -table.data-table > tbody > tr > td { - padding: 4px; -} - -table.data-table > tbody > tr:nth-child(2n) { - background: rgba(255, 255, 255, 0.2); -} - -table.data-table > tbody > tr > td:not(:last-child) { - border-right: 1px solid white; -} - -div.dialog { - background: var(--col-dialog-bg); - border-radius: 20px; - box-shadow: 0px 5px 10px 10px rgb(0 0 0 / 25%); - display: flex; - flex-direction: column; - left: 50%; - opacity: 0; - padding: 20px; - position: absolute; - top: 50%; - transform: translate(-50%, -50%); - transition: visibility 0.1s, opacity 0.1s linear; - visibility: hidden; - width: 450px; -} - -div.dialog.visible { - opacity: 1; - visibility: visible; -} - ::-webkit-scrollbar { width: 10px; height: 10px; -- cgit v1.3