diff options
| author | Jake Mannens <jake@asger.xyz> | 2023-08-20 22:59:09 +1000 |
|---|---|---|
| committer | Jake Mannens <jake@asger.xyz> | 2025-08-18 17:03:21 +1000 |
| commit | 4558ed9d6cbc59229cd56cc7a6c36b1df0b8199a (patch) | |
| tree | 24b27334ed8dc3cbcdc6cca9def75bca176c8764 | |
| parent | d7bc8b58f750c1a5c629915ed2097e477d204a67 (diff) | |
NSFW tagging and tag editing
| -rw-r--r-- | .config/dotnet-tools.json | 12 | ||||
| -rw-r--r-- | Controllers/MediaController.cs | 2 | ||||
| -rw-r--r-- | HBContext.cs | 7 | ||||
| -rw-r--r-- | MainLayout.razor | 5 | ||||
| -rw-r--r-- | MainLayout.razor.css | 13 | ||||
| -rw-r--r-- | Media.cs | 2 | ||||
| -rw-r--r-- | Pages/Component/MediaTagTable.razor | 7 | ||||
| -rw-r--r-- | Pages/Component/Switch.razor | 15 | ||||
| -rw-r--r-- | Pages/Component/Switch.razor.css | 25 | ||||
| -rw-r--r-- | Pages/TagDefinitions.razor | 54 | ||||
| -rw-r--r-- | Pages/TagDefinitions.razor.css | 4 | ||||
| -rw-r--r-- | Pages/ViewMedia.razor | 97 | ||||
| -rw-r--r-- | Pages/ViewMedia.razor.css | 2 | ||||
| -rw-r--r-- | Services/MediaService.cs | 2 | ||||
| -rw-r--r-- | Services/TagService.cs | 19 | ||||
| -rw-r--r-- | wwwroot/styles/global.css | 10 |
16 files changed, 213 insertions, 63 deletions
diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000..558293e --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "7.0.10", + "commands": [ + "dotnet-ef" + ] + } + } +}
\ No newline at end of file diff --git a/Controllers/MediaController.cs b/Controllers/MediaController.cs index 3fd716e..3ccfc1d 100644 --- a/Controllers/MediaController.cs +++ b/Controllers/MediaController.cs @@ -137,7 +137,7 @@ public class MediaController : Controller { if(media is null) { var ingestTagDef = db.TagDefinitions - .First(td => td.Source == TagSource.Internal && td.Name == "ingest"); + .First(td => td.Guid == HBContext.IngestTag); media = new() { Checksum = hash, diff --git a/HBContext.cs b/HBContext.cs index 45b8852..73ccdcb 100644 --- a/HBContext.cs +++ b/HBContext.cs @@ -4,6 +4,9 @@ using HyperBooru.Services; namespace HyperBooru; public class HBContext : DbContext { + public static readonly Guid NSFWTag = new("EBDAD4F8-455A-4351-8017-1D4854D6FA38"); + public static readonly Guid IngestTag = new("EA212801-5BCC-4C0E-814F-FB9D30DB58BC"); + public DbSet<HBObject> Objects { get; set; } public DbSet<TagDefinition> TagDefinitions { get; set; } public DbSet<Tag> Tags { get; set; } @@ -37,13 +40,13 @@ public class HBContext : DbContext { modelBuilder.Entity<TagDefinition>().HasData(new TagDefinition[] { new() { ObjectId = -1, - Guid = new("EBDAD4F8-455A-4351-8017-1D4854D6FA38"), + Guid = NSFWTag, Source = TagSource.Internal, Name = "nsfw" }, new() { ObjectId = -2, - Guid = new("EA212801-5BCC-4C0E-814F-FB9D30DB58BC"), + Guid = IngestTag, Source = TagSource.Internal, Name = "ingest" } diff --git a/MainLayout.razor b/MainLayout.razor index 111e307..7720fe2 100644 --- a/MainLayout.razor +++ b/MainLayout.razor @@ -7,6 +7,11 @@ <a href="/TagDefinitions">Tags</a> <a href="/Gallery?ingest=true">Ingest</a> <a href="/Upload">Upload</a> + + <p id="nsfw-label">NSFW</p> + <div id="nsfw-switch"> + <Switch/> + </div> <form action="/Gallery" method="get"> <input type="text" name="q" placeholder="Search"/> </form> diff --git a/MainLayout.razor.css b/MainLayout.razor.css index 3d03d61..87b8aae 100644 --- a/MainLayout.razor.css +++ b/MainLayout.razor.css @@ -20,9 +20,20 @@ div#navbar > a:active { color: var(--col-navbar-bg); } +p#nsfw-label { + align-self: center; + font-size: 9pt; + margin-left: auto; +} + +div#nsfw-switch { + align-self: center; + margin-left: 10px; +} + div#navbar form { display: flex; - margin: 0 20px 0 auto; + margin: 0 20px 0 20px; min-width: 30%; } @@ -15,7 +15,7 @@ public class Media : HBObject { public bool IsIngest => Tags .Select(t => t.TagDefinition) - .Any(td => td.Source == TagSource.Internal && td.Name == "ingest"); + .Any(td => td.Guid == HBContext.IngestTag); public string? DisplayName { get { diff --git a/Pages/Component/MediaTagTable.razor b/Pages/Component/MediaTagTable.razor index 278304d..ce42e48 100644 --- a/Pages/Component/MediaTagTable.razor +++ b/Pages/Component/MediaTagTable.razor @@ -28,6 +28,8 @@ <td> @if(!e.isImplicit) { <a href="javascript:;" @onclick=@(() => Delete(e.tagDef))>Delete</a> + } else { + <a href="javascript:;" @onclick=@(() => MakeExplicit(e.tagDef))>Make Explicit</a> } </td> </tr> @@ -60,4 +62,9 @@ .Where(e => e.tagDefinition.Source == TagSource.UserTag) .ToArray(); } + + private void MakeExplicit(TagDefinition tagDef) { + tagService.AddTag(Media, tagDef); + Refresh(); + } } diff --git a/Pages/Component/Switch.razor b/Pages/Component/Switch.razor new file mode 100644 index 0000000..ffb3543 --- /dev/null +++ b/Pages/Component/Switch.razor @@ -0,0 +1,15 @@ +<link rel="stylesheet" href="@(nameof(HyperBooru)).styles.css"/> + +<label> + <input + type="checkbox" + @onchange=@(e => OnToggle.InvokeAsync((e.Value as bool?) ?? false)) + hidden/> + <div class="switch-outer"> + <div class="switch-inner"/> + </div> +</label> + +@code { + public EventCallback<bool> OnToggle { get; set; } +} diff --git a/Pages/Component/Switch.razor.css b/Pages/Component/Switch.razor.css new file mode 100644 index 0000000..6b1f5d5 --- /dev/null +++ b/Pages/Component/Switch.razor.css @@ -0,0 +1,25 @@ +div.switch-outer { + background: var(--col-switch-bg); + border-radius: 20px; + border: 1px solid var(--col-switch-fg); + cursor: pointer; + height: 20px; + transition: background 0.1s linear; + width: 40px; +} + +div.switch-inner { + background: var(--col-switch-fg); + border-radius: 20px; + height: 20px; + transition: margin-left 0.1s linear; + width: 20px; +} + +input:checked + div.switch-outer { + background: var(--col-switch-bg-hl); +} + +input:checked + div.switch-outer > div.switch-inner { + margin-left: 20px; +} diff --git a/Pages/TagDefinitions.razor b/Pages/TagDefinitions.razor index cb0a17c..a9e1ebe 100644 --- a/Pages/TagDefinitions.razor +++ b/Pages/TagDefinitions.razor @@ -6,6 +6,10 @@ <link rel="stylesheet" type="text/css" href="@(nameof(HyperBooru)).styles.css"/> +<div class="button-container"> + <button @onclick=@(() => createTagDialog.Show())>Create</button> +</div> + <table id="tag-definitions" class="data-table"> <tr> <th>Namespace</th> @@ -23,21 +27,24 @@ </i> </td> <td> + <a href="/Gallery?q=@tagDef.Name">Search</a> + <a href="javascript:;" @onclick=@(() => PromptToEdit(tagDef))>Edit</a> <a href="javascript:;" @onclick=@(() => PromptToDelete(tagDef))> Delete </a> <a href="javascript:;" @onclick=@(() => PromptImplicitTags(tagDef))> Implicit Tags </a> + @if(tagDef.ImplicitTags.Select(td => td.Guid).Contains(HBContext.NSFWTag)) { + <a href="javascript:;" @onclick=@(() => SetNsfw(tagDef, false))>Make SFW</a> + } else { + <a href="javascript:;" @onclick=@(() => SetNsfw(tagDef, true))>Make NSFW</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> @@ -51,6 +58,19 @@ </form> </Dialog> +<Dialog Title="Edit tag" @ref=editTagDialog> + <form @onsubmit=EditTagDefinition> + <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=@(() => editTagDialog.Hide())>Cancel</button> + <button type="submit">Apply</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> @@ -65,12 +85,14 @@ @code { private Dialog createTagDialog; private Dialog deleteTagDialog; + private Dialog editTagDialog; private TagSelectDialog implicitTagDialog; private string tagName; private string? tagNamespace; private TagDefinition? toDelete; + private TagDefinition? toEdit; private TagDefinition? toEditImplicit; private TagDefinition[] tagDefinitions => @@ -89,6 +111,21 @@ StateHasChanged(); } + private void PromptToEdit(TagDefinition toEdit) { + this.toEdit = toEdit; + tagName = toEdit.Name; + tagNamespace = toEdit.Namespace; + editTagDialog.Show(); + } + + private void EditTagDefinition() { + if(toEdit is null) + return; + + tagService.UpdateTagDefinition(toEdit, tagName, tagNamespace); + StateHasChanged(); + } + private void PromptToDelete(TagDefinition toDelete) { this.toDelete = toDelete; deleteTagDialog.Show(); @@ -116,4 +153,13 @@ tagService.SetImplicitTags(toEditImplicit, tagDefs); StateHasChanged(); } + + private void SetNsfw(TagDefinition tagDef, bool nsfw) { + if(nsfw) { + tagService.AddTag(tagDef.Guid, HBContext.NSFWTag); + } else { + tagService.RemoveTag(tagDef.Guid, HBContext.NSFWTag); + } + StateHasChanged(); + } } diff --git a/Pages/TagDefinitions.razor.css b/Pages/TagDefinitions.razor.css index 93001c7..933cb4f 100644 --- a/Pages/TagDefinitions.razor.css +++ b/Pages/TagDefinitions.razor.css @@ -5,8 +5,4 @@ div.button-container { display: flex; justify-content: flex-end; -} - -table#tag-definitions td:first-child { - font-family: 'Lucida Console'; }
\ No newline at end of file diff --git a/Pages/ViewMedia.razor b/Pages/ViewMedia.razor index 68cc313..e559460 100644 --- a/Pages/ViewMedia.razor +++ b/Pages/ViewMedia.razor @@ -10,51 +10,56 @@ <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 id="metadata-uploadhistory">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 id="metadata-fileinfo"> + @if(true) { + <label> + Title: + <input type="text" style="width:100%;"/> + </label> + <p>Description:</p> + <textarea/> + } else { + <p>Title: <i>@(@media.ShortDescription ?? "None")</i></p> + <p>Description: <i>@(media.LongDescription ?? "None")</i></p> + } + <p class="heading">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> + <div id="metadata-tags"> + <p class="heading">Tags</p> + <hr/> + <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> </div> </div> @@ -105,7 +110,7 @@ private void SetIngest(bool ingest) { var ingestTag = db.TagDefinitions - .First(td => td.Source == TagSource.Internal && td.Name == "ingest"); + .First(td => td.Guid == HBContext.IngestTag); if(!ingest) media.Tags.RemoveAll(t => t.TagDefinition.Guid == ingestTag.Guid); diff --git a/Pages/ViewMedia.razor.css b/Pages/ViewMedia.razor.css index 8a59041..8a6a95a 100644 --- a/Pages/ViewMedia.razor.css +++ b/Pages/ViewMedia.razor.css @@ -24,7 +24,7 @@ div#metadata-fileinfo > table td { font-size: 8pt; } -p#metadata-uploadhistory { +p.heading { margin-top: 30px; } diff --git a/Services/MediaService.cs b/Services/MediaService.cs index be2657a..05dd5b0 100644 --- a/Services/MediaService.cs +++ b/Services/MediaService.cs @@ -15,7 +15,7 @@ public class MediaService : IMediaService { 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"); + .First(td => td.Guid == HBContext.IngestTag); if(ingest) media.Tags.Add(new() { TagDefinition = ingestTag }); diff --git a/Services/TagService.cs b/Services/TagService.cs index c3b8e14..8de52ff 100644 --- a/Services/TagService.cs +++ b/Services/TagService.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Razor.TagHelpers; using Microsoft.EntityFrameworkCore; +using System.Reflection; using System.Reflection.Metadata; namespace HyperBooru.Services; @@ -15,6 +16,8 @@ public interface ITagService { public void SetImplicitTags(Guid tagDef, Guid[] implicitTagDefs); public void CreateTagDefinition(string name, string? @namespace); public void DeleteTagDefinition(TagDefinition tagDef); + public void UpdateTagDefinition(Guid tagDef, string name, string? @namespace); + public void UpdateTagDefinition(TagDefinition tagDef, string name, string? @namespace); public (TagDefinition tagDefinition, bool isImplicit)[] GetAllTags(Guid obj); public (TagDefinition tagDefinition, bool isImplicit)[] GetAllTags(HBObject obj); } @@ -102,6 +105,22 @@ public class TagService : ITagService { transaction.Commit(); } + public void UpdateTagDefinition(Guid tagDef, string name, string? @namespace) { + using var db = dbFactory.CreateDbContext(); + + if(string.IsNullOrEmpty(@namespace)) + @namespace = null; + + var tag = db.TagDefinitions.First(td => td.Guid == tagDef); + tag.Name = name; + tag.Namespace = @namespace; + + db.SaveChanges(); + } + + public void UpdateTagDefinition(TagDefinition tagDef, string name, string? @namespace) => + UpdateTagDefinition(tagDef.Guid, name, @namespace); + public (TagDefinition tagDefinition, bool isImplicit)[] GetAllTags(Guid obj) { IEnumerable<TagDefinition> GetTagRecursive(IEnumerable<TagDefinition> tagDefs) => tagDefs diff --git a/wwwroot/styles/global.css b/wwwroot/styles/global.css index d79b27c..2757d09 100644 --- a/wwwroot/styles/global.css +++ b/wwwroot/styles/global.css @@ -18,6 +18,9 @@ --col-button-warning-hl: #ff9999; --col-scrollbar: #666666; --col-scrollbar-hover: #aaaaaa; + --col-switch-bg: var(--col-bg); + --col-switch-fg: #fff; + --col-switch-bg-hl: var(--col-accent-pri); } body { @@ -96,16 +99,19 @@ button:active, input[type=submit]:active { color: var(--col-button-pri); } -input { +input, textarea { background: rgba(0, 0, 0, 0); border-radius: 5px; border: 1px solid #aaa; box-sizing: border-box; color: white; - height: 25px !important; margin-bottom: 10px; } +input { + height: 25px !important; +} + /* necessary for use inside flex containers */ hr { width: 100%; |
