summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.config/dotnet-tools.json12
-rw-r--r--ApiRecords.cs53
-rw-r--r--App.razor11
-rw-r--r--Controllers/MediaController.cs (renamed from MediaController.cs)44
-rw-r--r--DbMedia.cs46
-rw-r--r--DbObject.cs15
-rw-r--r--DbTag.cs23
-rw-r--r--DebugController.cs22
-rw-r--r--Enum.cs13
-rw-r--r--HBContext.cs41
-rw-r--r--HBObject.cs14
-rw-r--r--HyperBooruContext.cs37
-rw-r--r--MainLayout.razor15
-rw-r--r--MainLayout.razor.css (renamed from Pages/Shared/_Layout.cshtml.css)7
-rw-r--r--Media.cs43
-rw-r--r--Pages/Component/Dialog.razor38
-rw-r--r--Pages/Component/Dialog.razor.css21
-rw-r--r--Pages/Component/MediaTagTable.razor69
-rw-r--r--Pages/Component/MediaTagTable.razor.css3
-rw-r--r--Pages/Component/TabContainer.razor35
-rw-r--r--Pages/Component/TabContainer.razor.css19
-rw-r--r--Pages/Component/TabPane.razor29
-rw-r--r--Pages/Component/TabPane.razor.css1
-rw-r--r--Pages/Component/TagSelectDialog.razor59
-rw-r--r--Pages/Component/TagSelectDialog.razor.css31
-rw-r--r--Pages/Index.cshtml.cs43
-rw-r--r--Pages/Index.razor (renamed from Pages/Index.cshtml)12
-rw-r--r--Pages/Index.razor.css (renamed from Pages/Index.cshtml.css)3
-rw-r--r--Pages/Shared/_Layout.cshtml26
-rw-r--r--Pages/TagDefinitions.cshtml108
-rw-r--r--Pages/TagDefinitions.cshtml.cs16
-rw-r--r--Pages/TagDefinitions.razor92
-rw-r--r--Pages/TagDefinitions.razor.css (renamed from Pages/TagDefinitions.cshtml.css)0
-rw-r--r--Pages/ViewMedia.cshtml181
-rw-r--r--Pages/ViewMedia.cshtml.cs43
-rw-r--r--Pages/ViewMedia.cshtml.css97
-rw-r--r--Pages/ViewMedia.razor106
-rw-r--r--Pages/ViewMedia.razor.css30
-rw-r--r--Pages/_Host.cshtml33
-rw-r--r--Pages/_ViewStart.cshtml3
-rw-r--r--Program.cs (renamed from HyperBooru.cs)32
-rw-r--r--Properties/launchSettings.json26
-rw-r--r--Server.csproj16
-rw-r--r--Services/ConfigService.cs (renamed from ConfigService.cs)15
-rw-r--r--Services/MediaService.cs28
-rw-r--r--Services/TagService.cs97
-rw-r--r--Tag.cs22
-rw-r--r--TagController.cs129
-rw-r--r--_Imports.razor7
-rw-r--r--appsettings.Development.json1
-rw-r--r--wwwroot/css/site.css28
-rw-r--r--wwwroot/favicon.icobin0 -> 3262 bytes
-rw-r--r--wwwroot/icon-192.pngbin0 -> 31523 bytes
-rw-r--r--wwwroot/icon-512.pngbin0 -> 136487 bytes
-rw-r--r--wwwroot/manifest.webmanifest6
-rw-r--r--wwwroot/styles/data-table.css21
-rw-r--r--wwwroot/styles/global.css63
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
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<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
new file mode 100644
index 0000000..a1be4cc
--- /dev/null
+++ b/wwwroot/favicon.ico
Binary files differ
diff --git a/wwwroot/icon-192.png b/wwwroot/icon-192.png
new file mode 100644
index 0000000..28ce06d
--- /dev/null
+++ b/wwwroot/icon-192.png
Binary files differ
diff --git a/wwwroot/icon-512.png b/wwwroot/icon-512.png
new file mode 100644
index 0000000..8c28696
--- /dev/null
+++ b/wwwroot/icon-512.png
Binary files 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;