diff options
| author | Jake Mannens <jake@asger.xyz> | 2023-08-15 15:49:14 +1000 |
|---|---|---|
| committer | Jake Mannens <jake@asger.xyz> | 2025-08-18 10:59:32 +1000 |
| commit | 38c60cee378b9c2ad42fc9dc79bc492b919a68f5 (patch) | |
| tree | 6b62f84aab4b7866432e5da8ae8fcb889795d58b | |
| parent | 07a4c7ead01514bd3f304f00abc38140a1d73634 (diff) | |
Convert Razor pages to Blazor
57 files changed, 993 insertions, 992 deletions
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 @@ +<Router AppAssembly="@typeof(App).Assembly"> + <Found Context="routeData"> + <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" /> + </Found> + <NotFound> + <PageTitle>Not found</PageTitle> + <LayoutView Layout="@typeof(MainLayout)"> + <p role="alert">Sorry, there's nothing at this address.</p> + </LayoutView> + </NotFound> +</Router> diff --git a/MediaController.cs b/Controllers/MediaController.cs index 620b3ee..3f36064 100644 --- a/MediaController.cs +++ b/Controllers/MediaController.cs @@ -1,20 +1,20 @@ -using HyperBooru.ApiRecords; +using HyperBooru.Services; using ImageMagick; using Microsoft.AspNetCore.Mvc; using MimeDetective; using System.Security.Cryptography; -namespace HyperBooru; +namespace HyperBooru.Controllers; [ApiController] [Route("/media")] public class MediaController : Controller { private IConfigService config; - private HyperBooruDbContext db; + private HBContext db; private ContentInspector inspector; - public MediaController(IConfigService config, HyperBooruDbContext db) { + public MediaController(IConfigService config, HBContext db) { this.config = config; this.db = db; @@ -64,8 +64,8 @@ public class MediaController : Controller { 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); + 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); @@ -92,36 +92,6 @@ public class MediaController : Controller { 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, @@ -146,7 +116,7 @@ public class MediaController : Controller { if(checksum is not null && hash != checksum.ToLower()) return BadRequest("Checksum does not match"); - var fileRecord = new DbUploadedFile() { + var fileRecord = new UploadedFile() { Filename = formFile.FileName, OriginalChecksum = hash, UploadTime = DateTime.Now, 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<DbUploadedFile> 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<DbTag> 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<DbTagDefinition> 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<HBObject> Objects { get; set; } + public DbSet<TagDefinition> TagDefinitions { get; set; } + public DbSet<Tag> Tags { get; set; } + public DbSet<Media> Media { get; set; } + public DbSet<UploadedFile> UploadedFiles { get; set; } + + private IConfigService config; + + public HBContext(DbContextOptions<HBContext> 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<HBObject>().ToTable("Objects"); + modelBuilder.Entity<TagDefinition>().ToTable("TagDefinitions"); + modelBuilder.Entity<Tag>().ToTable("Tags"); + modelBuilder.Entity<Media>().ToTable("Media"); + modelBuilder.Entity<UploadedFile>().ToTable("UploadedFiles"); + + modelBuilder.Entity<TagDefinition>().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<Tag> Tags { get; set; } = new(); +}
\ 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<DbObject> Objects { get; set; } - public DbSet<DbTagDefinition> TagDefinitions { get; set; } - public DbSet<DbTag> Tags { get; set; } - public DbSet<DbMedia> Media { get; set; } - public DbSet<DbUploadedFile> 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<DbObject>().ToTable("Objects"); - modelBuilder.Entity<DbTagDefinition>().ToTable("TagDefinitions"); - modelBuilder.Entity<DbTag>().ToTable("Tags"); - modelBuilder.Entity<DbMedia>().ToTable("Media"); - modelBuilder.Entity<DbUploadedFile>().ToTable("UploadedFiles"); - - modelBuilder.Entity<DbTagDefinition>().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 + +<link href="@(nameof(HyperBooru)).styles.css" rel="stylesheet" /> + +<main> + <div id="navbar"> + <a href="/">Home</a> + <a href="/TagDefinitions">Tags</a> + <input type="text" placeholder="Search"/> + </div> + @* <div id="content" style="overflow-y:@(ViewBag.ContentScroll ? "auto" : "hidden");padding:@(ViewBag.ContentMargin ?? "0");"> *@ + <div id="content"> + @Body + </div> +</main> diff --git a/Pages/Shared/_Layout.cshtml.css b/MainLayout.razor.css index 5b2f26e..e82e72e 100644 --- a/Pages/Shared/_Layout.cshtml.css +++ b/MainLayout.razor.css @@ -32,5 +32,8 @@ div#navbar > input { } #content { - flex: 1 1 calc(100vh - 119px); -}
\ No newline at end of file + 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<UploadedFile> 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/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 @@ +<div style="@style" class="@(visible ? "visible" : "")"> + @if(Title is not null) { + <p>@Title</p> + <hr/> + } + @ChildContent +</div> + +@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<HBContext> dbFactory +@inject ITagService tagService + +<link rel="stylesheet" href="@(nameof(HyperBooru)).styles.css"/> + +<table class="data-table"> + <tr> + <th>Namespace</th> + <th>Tag Name</th> + <th></th> + </tr> + @foreach(var tag in userTags) { + bool isImplicit = IsImplicit(tag); + <tr> + <td> + @if(isImplicit) { + <i>@tag.Namespace</i> + } else { + @tag.Namespace + } + </td> + <td> + @if(isImplicit) { + <i>@tag.Name</i> + } else { + @tag.Name + } + </td> + <td><a href="javascript:;" @onclick=@(() => Delete(tag))>Delete</a></td> + </tr> + } +</table> + +@code { + [Parameter] + public Media Media { get; set; } + + private IEnumerable<TagDefinition> 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<TagDefinition> GetTagRecursive(IEnumerable<TagDefinition> 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 @@ +<link rel="stylesheet" href="@(nameof(HyperBooru)).styles.css"/> + +<div class="tabs"> + @foreach(var pane in Panes) { + <a href="javascript:;" @onclick=@(() => ActivePane = pane) class="@(pane == ActivePane ? "selected" : "")"> + @pane.Title + </a> + } +</div> + +<CascadingValue Value="this"> + @ChildContent +</CascadingValue> + +@code { + [Parameter] + public RenderFragment ChildContent { get; set; } + + public TabPane? ActivePane { get; set; } + List<TabPane> 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 + +<link rel="stylesheet" href="@(nameof(HyperBooru)).styles.css"/> + +@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 + +<link rel="stylesheet" href="@(nameof(HyperBooru)).styles.css"/> + +<Dialog Title=@(Title ?? "Select one or more tag(s)") @ref=dialog> + <input type="text" placeholder="Search"/> + <div class="tag-definitions"> + @foreach(var tagDef in tagDefinitions) { + <input type="checkbox" id="tagDef-@tagDef.Guid" @onchange=@(e => Checked(tagDef, e.Value))/> + <label for="tagDef-@tagDef.Guid">@tagDef.Name</label> + } + </div> + <div class="button-container"> + <button @onclick=@(() => dialog.Hide()) class="secondary">Cancel</button> + <button @onclick=@(() => Submit())>Accept</button> + </div> +</Dialog> + +@code { + [Parameter] + public string? Title { get; set; } + + [Parameter] + public EventCallback<TagDefinition[]> OnSubmit { get; set; } + + public bool Visible { + get => visible; + set => visible = dialog.Visible = value; + } + + private bool visible; + + private Dialog dialog; + + private IEnumerable<TagDefinition> tagDefinitions => db.TagDefinitions + .Where(td => td.Source == TagSource.UserTag) + .OrderBy(td => td.Name); + + private List<TagDefinition> 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.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<DbMedia> Media { get; private set; } - - private HyperBooruDbContext db; - - public IndexModel(HyperBooruDbContext db) => - this.db = db; - - public void OnGet([FromQuery(Name = "q")] string? query) { - IEnumerable<DbMedia> 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 b/Pages/Index.razor index 80e05d9..a69d69c 100644 --- a/Pages/Index.cshtml +++ b/Pages/Index.razor @@ -1,15 +1,17 @@ -@page -@model HyperBooru.Pages.IndexModel +@page "/" +@inject HBContext db; -<link rel="stylesheet" type="text/css" href="@(nameof(HyperBooru)).styles.css"/> +<PageTitle>Gallery</PageTitle> + +<link rel="stylesheet" href="@(nameof(HyperBooru)).styles.css"/> <form id="upload" action="/media" method="post" enctype="multipart/form-data"> <input type="file" id="myFile" name="filename"/> <input type="submit" /> </form> -@foreach(var media in Model.Media) { +@foreach(var media in db.Media) { <a href="/ViewMedia?m=@(media.Guid)"> <img src="/media/thumb/@(media.Guid)?h=200" /> </a> -}
\ No newline at end of file +} diff --git a/Pages/Index.cshtml.css b/Pages/Index.razor.css index f573988..d1750b4 100644 --- a/Pages/Index.cshtml.css +++ b/Pages/Index.razor.css @@ -1,5 +1,6 @@ img { - max-height: 200px; + margin-right: 5px; + max-height: 200px; } form#upload { 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"; -} - -<!DOCTYPE html> - -<html> -<head> - <meta name="viewport" content="width=device-width"/> - <link rel="stylesheet" type="text/css" href="/styles/global.css"/> - <link rel="stylesheet" type="text/css" href="@(nameof(HyperBooru)).styles.css"/> - <title>@ViewBag.Title</title> -</head> - <body> - <div id="navbar"> - <a href="/">Home</a> - <a href="/TagDefinitions">Tags</a> - <input type="text" placeholder="Search"/> - </div> - <div id="content" style="overflow-y:@(ViewBag.ContentScroll ? "auto" : "hidden");padding:@(ViewBag.ContentMargin ?? "0");"> - @RenderBody() - </div> - </body> -</html>
\ 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"; -} - -<script type="text/javascript"> - async function createDefinition(e) { - var form = new FormData(); - - form.append('name', e.querySelector('#name').value); - form.append('namespace', e.querySelector('#namespace').value); - - var resp = await fetch('/api/tag/def', { - method: 'post', - body: form - }); - - if(!resp.ok) { - alert('Error creating tag definition!'); - showCreateDialog(false); - } else { - window.location.reload(); - } - } - - async function deleteTagDefinition() { - var dialog = document.getElementById('delete-dialog'); - - var tagDefId = dialog.dataset.guid; - - var resp = await fetch(`/api/tag/def/${tagDefId}`, { - method: 'delete' - }); - - if(!resp.ok) { - alert('Error deleting tag definition!'); - showDeleteDialog(false); - } else { - var rows = Array.from(document.getElementsByTagName('tr')); - rows.find(r => r.dataset.guid == tagDefId).remove(); - showDeleteDialog(false); - } - } - - function showCreateDialog(visible) { - document.getElementById('create-dialog').classList.toggle('visible', visible); - } - - function showDeleteDialog(visible) { - var dialog = document.getElementById('delete-dialog'); - if(visible == false) { - dialog.classList.toggle('visible', false); - } else { - dialog.classList.toggle('visible', true); - dialog.dataset.guid = visible; - } - } -</script> - -<link rel="stylesheet" type="text/css" href="@(nameof(HyperBooru)).styles.css"/> - -<table id="tag-definitions" class="data-table"> - <tr> - <th>Guid</th> - <th>Source</th> - <th>Namespace</th> - <th>Name</th> - <th></th> - </tr> - @foreach(var tagDef in Model.TagDefinitions) { - <tr data-guid="@tagDef.Guid"> - <td>@tagDef.Guid</td> - <td>@tagDef.Source</td> - <td>@tagDef.Namespace</td> - <td>@tagDef.Name</td> - <td><a href="javascript:showDeleteDialog('@tagDef.Guid');">Delete</a></td> - </tr> - } -</table> - -<div class="button-container"> - <button onclick="showCreateDialog(true)">Create</button> -</div> - -<div id="create-dialog" class="dialog"> - <p>Create a new tag definition</p> - <hr/> - <form onsubmit="createDefinition(this)"> - <label>Name</label> - <input id="name" type="text" required/> - <label>Namespace</label> - <input id="namespace" type="text"/> - <div class="button-container"> - <button class="secondary" onclick="showCreateDialog(false)">Cancel</button> - <button type="submit">Create</button> - </div> - </form> -</div> - -<div id="delete-dialog" class="dialog"> - <p>Are you sure you want to delete this tag definition?</p> - <hr/> - <div class="button-container"> - <button onclick="showDeleteDialog(false)" class="secondary">Cancel</button> - <button onclick="deleteTagDefinition()">Confirm</button> - </div> -</div> 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<DbTagDefinition> 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.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<HBContext> dbFactory +@inject ITagService tagService + +<PageTitle>Tag Definitions</PageTitle> + +<link rel="stylesheet" type="text/css" href="@(nameof(HyperBooru)).styles.css"/> + +<table id="tag-definitions" class="data-table"> + <tr> + <th>Guid</th> + <th>Source</th> + <th>Namespace</th> + <th>Name</th> + <th></th> + </tr> + @foreach(var tagDef in tagDefinitions) { + <tr data-guid="@tagDef.Guid"> + <td>@tagDef.Guid</td> + <td>@tagDef.Source</td> + <td>@tagDef.Namespace</td> + <td>@tagDef.Name</td> + <td> + <a href="javascript:showDeleteDialog('@tagDef.Guid');" @onclick=@(() => PromptToDelete(tagDef))> + Delete + </a> + </td> + </tr> + } +</table> + +<div class="button-container"> + <button @onclick=@(() => createTagDialog.Show())>Create</button> +</div> + +<Dialog Title="Create a new tag definition" @ref=createTagDialog> + <form @onsubmit=CreateTagDefinition> + <label>Name</label> + <input type="text" @bind=tagName required/> + <label>Namespace</label> + <input type="text" @bind=tagNamespace/> + <div class="button-container"> + <button class="secondary" @onclick=@(() => createTagDialog.Hide())>Cancel</button> + <button type="submit">Create</button> + </div> + </form> +</Dialog> + +<Dialog Title="Are you sure you want to delete this tag definition?" @ref=deleteTagDialog> + <div class="button-container"> + <button @onclick=@(() => deleteTagDialog.Hide()) class="secondary">Cancel</button> + <button @onclick=@(() => DeleteTagDefinition()) class="warning">Confirm</button> + </div> +</Dialog> + +@code { + private Dialog createTagDialog; + private Dialog deleteTagDialog; + + private string tagName; + private string? tagNamespace; + + private TagDefinition? toDelete; + + private IEnumerable<TagDefinition> 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.cshtml.css b/Pages/TagDefinitions.razor.css index 93001c7..93001c7 100644 --- a/Pages/TagDefinitions.cshtml.css +++ b/Pages/TagDefinitions.razor.css 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; -} - -<link rel="stylesheet" type="text/css" href="@(nameof(HyperBooru)).styles.css"/> - -<script> - var mediaId = new URL(window.location.href).searchParams.get('m'); - - async function deleteMedia() { - var resp = await fetch('/media/' + mediaId, { method: 'delete' }); - if(resp.ok) { - window.location.href = '/'; - } else { - alert('Failed to delete media object!'); - } - } - - async function applyTags() { - var checkboxes = Array.from(document - .getElementById('tag-definitions') - .getElementsByTagName('input')); - - var tagDefIds = checkboxes - .filter(cb => cb.checked) - .map(cb => cb.id.replace(/^tagdef-/, '')); - - var pendingRequests = tagDefIds - .map(id => fetch(`/api/tag/${mediaId}/${id}`, { method: 'POST' })); - - var responses = await Promise.all(pendingRequests); - - if(responses.some(r => !r.ok && r.status != 400)) { - alert('Error setting tags!'); - } - showTagDialog(false); - } - - async function removeTag(e, tagDefId) { - var resp = await fetch(`/api/tag/${mediaId}/${tagDefId}`, { method: 'DELETE' }); - if(!resp.ok && resp.status != 400) { - alert('Error removing tag!'); - } else { - e.closest('tr').remove(); - } - } - - function showDeleteDialog(visible) { - document.getElementById('delete-dialog').classList.toggle('visible', visible); - } - - function showTagDialog(visible) { - document.getElementById('tag-dialog').classList.toggle('visible', visible); - document.querySelector('div#tag-dialog input').focus(); - } - - function selectPane(tab) { - var tabs = Array.from(document.querySelectorAll('div#metadata-header > a')); - - var panes = Array.from(document.querySelectorAll('div#metadata > div')) - .filter(x => x.id != 'metadata-header'); - var pane = panes.filter(x => x.id == tab.dataset.pane)[0]; - - for(var t of tabs) { - if(t == tab) - t.classList.add('selected'); - else - t.classList.remove('selected'); - } - - for(var p of panes) { - if(p == pane) - p.classList.add('selected'); - else - p.classList.remove('selected'); - } - } -</script> - -<div id="content"> - <img src="/media/@(Model.Media.Guid)"/> - <div id="metadata"> - <div id="metadata-header"> - <a href="javascript:;" onclick="selectPane(this);" data-pane="metadata-fileinfo" class="selected">File Info</a> - <a href="javascript:;" onclick="selectPane(this);" data-pane="metadata-tags">Tags</a> - </div> -@* <form method="post"> - <label for="shortDescription">Short Description</label> - <input type="text" name="shortDescription" placeholder="@Model.Media.ShortDescription"/> - <label for="longDescription">Long Description</label> - <input type="text" name="longDescription" placeholder="@Model.Media.LongDescription"/> - <input type="submit" value="Update"/> - </form>*@ - <div id="metadata-fileinfo" class="selected"> - <p>Upload history</p> - <hr /> - <table class="data-table"> - <tr> - <th>Created On</th> - <th>Last Write</th> - <th>Uploaded On</th> - <th>Filename</th> - <th>Original Checksum</th> - </tr> - @foreach(var file in Model.Media.UploadedFiles) { - <tr> - <td>@(file.CreateTime?.ToString() ?? "N/A")</td> - <td>@(file.LastWriteTime?.ToString() ?? "N/A")</td> - <td>@file.UploadTime</td> - <td>@file.Filename</td> - <td>@file.OriginalChecksum</td> - </tr> - } - </table> - <div class="button-container"> - <button onclick="showDeleteDialog(true)" id="delete-button">Delete</button> - <button>Apply</button> - </div> - </div> - <div id="metadata-tags"> - <table class="data-table"> - <tr> - <th>Namespace</th> - <th>Tag Name</th> - <th></th> - </tr> - @foreach(var tag in Model.UserTags) { - bool isImplicit = Model.IsImplicit(tag); - <tr> - <td> - @if(isImplicit) { - <i>@tag.Namespace</i> - } else { - @tag.Namespace - } - </td> - <td> - @if(isImplicit) { - <i>@tag.Name</i> - } else { - @tag.Name - } - </td> - <td><a href="javascript:;" onclick="removeTag(this, '@tag.Guid')">Delete</a></td> - </tr> - } - </table> - <div class="button-container"> - <button onclick="showTagDialog(true)" class="secondary">Add Tag</button> - <button onclick="show">Tagging Complete</button> - </div> - </div> - </div> -</div> - -<div id="delete-dialog" class="dialog"> - <p>Delete this media?</p> - <hr/> - <div class="button-container"> - <button class="secondary" onclick="showDeleteDialog(false)">Cancel</button> - <button onclick="deleteMedia()">Confirm</button> - </div> -</div> - -<div id="tag-dialog" class="dialog"> - <p>Select one or more tag(s) to add</p> - <hr/> - <input type="text" placeholder="Search"/> - <div id="tag-definitions"> - @foreach(var tagdef in Model.TagDefinitions) { - <input type="checkbox" id="tagdef-@tagdef.Guid"/> - <label for="tagdef-@tagdef.Guid">@tagdef.Name</label> - } - </div> - <div class="button-container"> - <button onclick="showTagDialog(false)" class="secondary">Cancel</button> - <button onclick="applyTags()">Accept</button> - </div> -</div> 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<DbTagDefinition> 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<DbTagDefinition> GetTagRecursive(IEnumerable<DbTagDefinition> 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<HBContext> dbFactory +@inject ITagService tagService + +<PageTitle>@title</PageTitle> + +<link rel="stylesheet" type="text/css" href="@(nameof(HyperBooru)).styles.css"/> + +<div id="content"> + <img src="/media/@(media.Guid)"/> + <div id="metadata"> + <TabContainer> + <TabPane Title="Media Info"> + <div id="metadata-fileinfo"> + <p>Title: <i>@(@media.ShortDescription ?? "None")</i></p> + <p>Description: <i>@(media.LongDescription ?? "None")</i></p> + <p>Upload history</p> + <hr /> + <table class="data-table"> + <tr> + <th>Created On</th> + <th>Last Write</th> + <th>Uploaded On</th> + <th>Filename</th> + <th>Original Checksum</th> + </tr> + @foreach(var file in media.UploadedFiles) { + <tr> + <td>@(file.CreateTime?.ToString() ?? "N/A")</td> + <td>@(file.LastWriteTime?.ToString() ?? "N/A")</td> + <td>@file.UploadTime</td> + <td>@file.Filename</td> + <td>@file.OriginalChecksum</td> + </tr> + } + </table> + <div class="button-container"> + <button @onclick=@(() => deleteDialog.Show()) class="warning">Delete</button> + <button>Apply</button> + </div> + </div> + </TabPane> + <TabPane Title="Tags"> + <div id="metadata-tags"> + <MediaTagTable Media=media @ref=mediaTagTable/> + <div class="button-container"> + <button @onclick=@(() => tagDialog.Show()) class="secondary">Add Tag</button> + @if(media.IsIngest) { + <button @onclick=@(() => SetIngest(false))>Mark Tagging Complete</button> + } else { + <button class="secondary" @onclick=@(() => SetIngest(true))>Mark Tagging Incomplete</button> + } + </div> + </div> + </TabPane> + </TabContainer> + </div> +</div> + +<Dialog Title="Delete this media?" @ref=deleteDialog> + <div class="button-container"> + <button class="secondary" @onclick=@(() => deleteDialog.Hide())>Cancel</button> + <button onclick="deleteMedia()" class="warning">Confirm</button> + </div> +</Dialog> + +<TagSelectDialog + Title="Select one or more tag(s) to add" + OnSubmit=AddTags + @ref=tagDialog/> + +@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 + +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8" /> + <base href="~/" /> + <link href="css/site.css" rel="stylesheet" /> + <link href="/styles/global.css" rel="stylesheet" /> + <link href="/favicon.ico" rel="icon" /> + <link href="/manifest.webmanifest" rel="manifest" /> + <component type="typeof(HeadOutlet)" render-mode="ServerPrerendered" /> +</head> +<body> + <component type="typeof(App)" render-mode="ServerPrerendered" /> + + <div id="blazor-error-ui"> + <environment include="Staging,Production"> + An error has occurred. This application may no longer respond until reloaded. + </environment> + <environment include="Development"> + An unhandled exception has occurred. See browser dev tools for details. + </environment> + <a href="" class="reload">Reload</a> + <a class="dismiss">🗙</a> + </div> + + <script src="_framework/blazor.server.js"></script> +</body> +</html> 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/HyperBooru.cs b/Program.cs index e0e1b2d..dfc635c 100644 --- a/HyperBooru.cs +++ b/Program.cs @@ -1,37 +1,39 @@ using Microsoft.EntityFrameworkCore; using System.Text.Json.Serialization; +using HyperBooru.Services; namespace HyperBooru; -public class HyperBooru { +public class Program { public static void Main(string[] args) { - var builder = WebApplication.CreateBuilder(); - builder.Services.AddEndpointsApiExplorer(); - builder.Services.AddSwaggerGen(); + 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<IConfigService, ConfigService>(); - builder.Services.AddScoped<HyperBooruDbContext>(p => - new HyperBooruDbContext(p.GetRequiredService<IConfigService>())); + builder.Services.AddDbContextFactory<HBContext>(); + builder.Services.AddScoped<ITagService, TagService>(); 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<HyperBooruDbContext>(); + using var db = scope.ServiceProvider.GetRequiredService<HBContext>(); db.Database.Migrate(); - #if DEBUG - app.UseSwagger(); - app.UseSwaggerUI(); - #endif - - app.MapRazorPages(); - app.UseStaticFiles(); + app.UseHsts(); app.UseHttpsRedirection(); + app.UseStaticFiles(); + app.UseRouting(); + app.MapBlazorHub(); app.MapControllers(); + app.MapFallbackToPage("/_Host"); + app.Run(); } -}
\ No newline at end of file +} 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 @@ <ImplicitUsings>enable</ImplicitUsings> <AssemblyName>HyperBooru</AssemblyName> <RootNamespace>HyperBooru</RootNamespace> + <AssemblyVersion>0.1.0.0</AssemblyVersion> + <FileVersion>$(AssemblyVersion)</FileVersion> + <Version>$(AssemblyVersion)</Version> + </PropertyGroup> + + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> + <NoWarn>1701;1702;8618</NoWarn> + </PropertyGroup> + + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'"> + <NoWarn>1701;1702;8618</NoWarn> </PropertyGroup> <ItemGroup> @@ -20,4 +31,9 @@ <PackageReference Include="Mime-Detective" Version="23.6.1" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" /> </ItemGroup> + + <ItemGroup> + <Folder Include="Data\" /> + </ItemGroup> + </Project> diff --git a/ConfigService.cs b/Services/ConfigService.cs index 0ee4113..6ba3936 100644 --- a/ConfigService.cs +++ b/Services/ConfigService.cs @@ -1,11 +1,12 @@ -namespace HyperBooru; +namespace HyperBooru.Services; public interface IConfigService { - public string DataPath { get; } - public string DbPath { get; } + 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 string GetPath(Media media); + public string GetPath(Media media, int width, int height); } public class ConfigService : IConfigService { @@ -51,7 +52,7 @@ public class ConfigService : IConfigService { InitDirectoryStructure(); } - public string GetPath(DbMedia media) { + public string GetPath(Media media) { var fileInfo = new FileInfo(Path.Join( MediaBasePath, media.Guid.ToString().Substring(0, 2), @@ -62,7 +63,7 @@ public class ConfigService : IConfigService { return fileInfo.FullName; } - public string GetPath(DbMedia media, int width, int height) { + public string GetPath(Media media, int width, int height) { var fileInfo = new FileInfo(Path.Join( ThumbnailBasePath, media.Guid.ToString().Substring(0, 2), 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<HBContext> dbFactory; + + public MediaService(IDbContextFactory<HBContext> 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<HBContext> dbFactory; + + public TagService(IDbContextFactory<HBContext> 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 @@ -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<TagDefinition> 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 Binary files differnew file mode 100644 index 0000000..a1be4cc --- /dev/null +++ b/wwwroot/favicon.ico diff --git a/wwwroot/icon-192.png b/wwwroot/icon-192.png Binary files differnew file mode 100644 index 0000000..28ce06d --- /dev/null +++ b/wwwroot/icon-192.png diff --git a/wwwroot/icon-512.png b/wwwroot/icon-512.png Binary files differnew file mode 100644 index 0000000..8c28696 --- /dev/null +++ b/wwwroot/icon-512.png 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; |
