summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJake Mannens <jake@asger.xyz>2023-08-20 22:59:09 +1000
committerJake Mannens <jake@asger.xyz>2023-08-20 22:59:09 +1000
commitc29bdd4a9ec782411f57e3c798e1bb01ca7d417d (patch)
treefdb2046af3454a96a9733e156b9c86f538eea21c
parentdfff79815acc86c02dd94716098d409d3fe46ab7 (diff)
NSFW tagging and tag editing
-rw-r--r--.config/dotnet-tools.json12
-rw-r--r--Controllers/MediaController.cs2
-rw-r--r--HBContext.cs7
-rw-r--r--MainLayout.razor5
-rw-r--r--MainLayout.razor.css13
-rw-r--r--Media.cs2
-rw-r--r--Pages/Component/MediaTagTable.razor7
-rw-r--r--Pages/Component/Switch.razor15
-rw-r--r--Pages/Component/Switch.razor.css24
-rw-r--r--Pages/TagDefinitions.razor54
-rw-r--r--Pages/TagDefinitions.razor.css4
-rw-r--r--Pages/ViewMedia.razor97
-rw-r--r--Pages/ViewMedia.razor.css2
-rw-r--r--Services/MediaService.cs2
-rw-r--r--Services/TagService.cs19
-rw-r--r--wwwroot/styles/global.css10
16 files changed, 212 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%;
}
diff --git a/Media.cs b/Media.cs
index 2ff10bf..3f2021d 100644
--- a/Media.cs
+++ b/Media.cs
@@ -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..076b5d1
--- /dev/null
+++ b/Pages/Component/Switch.razor.css
@@ -0,0 +1,24 @@
+div.switch-outer {
+ background: var(--col-switch-bg);
+ border-radius: 20px;
+ border: 1px solid var(--col-switch-fg);
+ 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%;