summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJake Mannens <jake@asger.xyz>2023-09-05 01:01:24 +1000
committerJake Mannens <jake@asger.xyz>2023-09-05 01:01:24 +1000
commitacb74202f5391272c2e1823dfe04a044c7f7a9a7 (patch)
tree09dc032ba526306d8916035bf92336c095246741
parent1b080819b26e4e84e5e9c88445806e43698b6757 (diff)
Tag names and aliases are now verified to be unique
-rw-r--r--Exception.cs34
-rw-r--r--IDialog.cs8
-rw-r--r--Pages/Component/AboutDialog.razor1
-rw-r--r--Pages/Component/Dialog.razor4
-rw-r--r--Pages/Component/TagEditDialog.razor96
-rw-r--r--Pages/Component/TagEditDialog.razor.css11
-rw-r--r--Pages/Component/TagSelectDialog.razor9
-rw-r--r--Pages/TagDefinitions.razor89
-rw-r--r--Pages/TagDefinitions.razor.css3
-rw-r--r--Services/TagService.cs35
-rw-r--r--Todo.md4
-rw-r--r--wwwroot/styles/global.css1
12 files changed, 206 insertions, 89 deletions
diff --git a/Exception.cs b/Exception.cs
index 2005b1a..1e070eb 100644
--- a/Exception.cs
+++ b/Exception.cs
@@ -10,14 +10,44 @@ public class HBException : Exception {
}
public class ObjectNotFoundException : HBException {
- public Guid Guid { get; set; }
+ public Guid Guid { get; private init; }
public ObjectNotFoundException(Guid guid)
: base($"Object not found: {guid}") {}
}
+public class TagException : HBException {
+ public TagDefinition? TagDefinition { get; private init; }
+
+ public TagException(string message) : base(message) {}
+ public TagException(string message, TagDefinition tagDefinition)
+ : base(message) =>
+ TagDefinition = tagDefinition;
+}
+
+public class TagDuplicateException : TagException {
+ public bool NameExists { get; private init; }
+ public bool AliasExists { get; private init; }
+
+ public TagDuplicateException(bool nameExists, bool aliasExists)
+ : base(GenerateMessage(nameExists, aliasExists)) {
+
+ NameExists = nameExists;
+ AliasExists = aliasExists;
+ }
+
+ private static string GenerateMessage(bool nameExists, bool aliasExists) {
+ if(nameExists && aliasExists)
+ return $"Both tag name and alias already exist!";
+ else if(nameExists)
+ return $"Tag name already exists!";
+ else
+ return $"Tag alias already exists";
+ }
+}
+
public class MediaException : HBException {
- public Media? Media { get; set; }
+ public Media? Media { get; private init; }
public MediaException(string message) : base(message) {}
public MediaException(string message, Media media) : base(message) =>
diff --git a/IDialog.cs b/IDialog.cs
new file mode 100644
index 0000000..41e86a8
--- /dev/null
+++ b/IDialog.cs
@@ -0,0 +1,8 @@
+namespace HyperBooru;
+
+public interface IDialog {
+ public bool Visible { get; set; }
+
+ public void Show();
+ public void Hide();
+}
diff --git a/Pages/Component/AboutDialog.razor b/Pages/Component/AboutDialog.razor
index 9823761..9ffbad4 100644
--- a/Pages/Component/AboutDialog.razor
+++ b/Pages/Component/AboutDialog.razor
@@ -1,6 +1,7 @@
@using System.Reflection
@using Microsoft.AspNetCore.Hosting
@inject IHostingEnvironment hostingEnvironment
+@implements IDialog
<Dialog @ref=dialog>
<p id="title">@Title</p>
diff --git a/Pages/Component/Dialog.razor b/Pages/Component/Dialog.razor
index ded2d2d..f479368 100644
--- a/Pages/Component/Dialog.razor
+++ b/Pages/Component/Dialog.razor
@@ -1,4 +1,6 @@
-<div style="@(heightStyle + visiblilityStyle)">
+@implements IDialog
+
+<div style="@(heightStyle + visiblilityStyle)">
@if(Title is not null) {
<p>@Title</p>
<hr/>
diff --git a/Pages/Component/TagEditDialog.razor b/Pages/Component/TagEditDialog.razor
new file mode 100644
index 0000000..2e443d4
--- /dev/null
+++ b/Pages/Component/TagEditDialog.razor
@@ -0,0 +1,96 @@
+@inject IDbContextFactory<HBContext> dbFactory;
+@inject ITagService tagService
+@implements IDialog
+
+<Dialog Title=@Title @ref=dialog>
+ <label>
+ Name
+ @if(nameExists) {
+ <p class="error">Tag with that name already exists!</p>
+ }
+ </label>
+ <input type="text" @bind=tagName required/>
+ <label>Namespace</label>
+ <input type="text" @bind=tagNamespace/>
+ <label>
+ Alias
+ @if(aliasExists) {
+ <p class="error">Tag with that alias already exists!</p>
+ }
+ </label>
+ <input type="text" @bind=tagAlias/>
+ <ButtonContainer>
+ <button @onclick=Hide class="secondary">Cancel</button>
+ <button @onclick=Submit>@(TagDefinition is null ? "Create" : "Apply")</button>
+ </ButtonContainer>
+</Dialog>
+
+@code {
+ [Parameter]
+ public TagDefinition? TagDefinition { get; set; }
+
+ [Parameter]
+ public EventHandler OnTagUpdate { get; set; }
+
+ public bool Visible {
+ get => visible;
+ set {
+ if(value)
+ Load();
+ visible = dialog.Visible = value;
+ }
+ }
+
+ private string Title =>
+ TagDefinition is null ? "Create a new tag definition" : "Edit tag definition";
+
+ private Dialog dialog;
+
+ private string? tagName;
+ private string? tagNamespace;
+ private string? tagAlias;
+
+ private bool nameExists = false;
+ private bool aliasExists = false;
+
+ private bool visible = false;
+
+ public void Show() => Visible = true;
+ public void Hide() => Visible = false;
+
+ public void Show(TagDefinition? toEdit) {
+ TagDefinition = toEdit;
+ Visible = true;
+ }
+
+ public void Show(string? @namespace) {
+ TagDefinition = null;
+ Visible = true;
+ tagNamespace = @namespace;
+ }
+
+ private void Load() {
+ tagName = TagDefinition?.Name;
+ tagNamespace = TagDefinition?.Namespace;
+ tagAlias = TagDefinition?.Alias;
+ nameExists = false;
+ aliasExists = false;
+ }
+
+ private void Submit() {
+ try {
+ if(TagDefinition is null) {
+ tagService.CreateTagDefinition(tagName, tagNamespace, tagAlias);
+ } else {
+ tagService.UpdateTagDefinition(TagDefinition, tagName, tagNamespace, tagAlias);
+ }
+ } catch(TagDuplicateException e) {
+ nameExists = e.NameExists;
+ aliasExists = e.AliasExists;
+ return;
+ }
+
+ OnTagUpdate.Invoke(this, new EventArgs());
+ Hide();
+ }
+}
diff --git a/Pages/Component/TagEditDialog.razor.css b/Pages/Component/TagEditDialog.razor.css
new file mode 100644
index 0000000..02781c0
--- /dev/null
+++ b/Pages/Component/TagEditDialog.razor.css
@@ -0,0 +1,11 @@
+p.error {
+ color: var(--col-error-pri);
+ display: inline;
+ font-size: 8pt;
+ margin-left: 5px;
+ vertical-align: middle;
+}
+
+input {
+ width: 100%;
+}
diff --git a/Pages/Component/TagSelectDialog.razor b/Pages/Component/TagSelectDialog.razor
index 916a070..88e1471 100644
--- a/Pages/Component/TagSelectDialog.razor
+++ b/Pages/Component/TagSelectDialog.razor
@@ -2,6 +2,7 @@
@inject ITagService tagService
@inject IUserService userService
@implements IDisposable
+@implements IDialog
<link rel="stylesheet" href="@(nameof(HyperBooru)).styles.css"/>
@@ -36,11 +37,11 @@
</Dialog>
@code {
- [Parameter]
- public string? Title { get; set; }
+ [Parameter]
+ public string? Title { get; set; }
- [Parameter]
- public EventCallback<TagDefinition[]> OnSubmit { get; set; }
+ [Parameter]
+ public EventCallback<TagDefinition[]> OnSubmit { get; set; }
public TagDefinition[] SelectedTags { get; set; } =
Array.Empty<TagDefinition>();
diff --git a/Pages/TagDefinitions.razor b/Pages/TagDefinitions.razor
index 75bcba0..95253b7 100644
--- a/Pages/TagDefinitions.razor
+++ b/Pages/TagDefinitions.razor
@@ -8,7 +8,7 @@
<div style="padding:var(--size-default-gap);">
<ButtonContainer>
- <button @onclick=PromptToCreate>Create</button>
+ <button @onclick=PromptTagCreate>Create</button>
</ButtonContainer>
<TabContainer @ref=tabContainer>
@@ -45,7 +45,7 @@
</i>
</td>
<td class="actions">
- <a href="javascript:;" @onclick=@(() => PromptToEdit(tagDef))>Edit</a>
+ <a href="javascript:;" @onclick=@(() => tagEditDialog.Show(tagDef))>Edit</a>
<a href="javascript:;" @onclick=@(() => PromptToDelete(tagDef))>
Delete
</a>
@@ -66,36 +66,6 @@
</TabContainer>
</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/>
- <label>Alias</label>
- <input type="text" @bind=tagAlias/>
- <ButtonContainer>
- <button class="secondary" @onclick=@(() => createTagDialog.Hide())>Cancel</button>
- <button type="submit">Create</button>
- </ButtonContainer>
- </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/>
- <label>Alias</label>
- <input type="text" @bind=tagAlias/>
- <ButtonContainer>
- <button class="secondary" @onclick=@(() => editTagDialog.Hide())>Cancel</button>
- <button type="submit">Apply</button>
- </ButtonContainer>
- </form>
-</Dialog>
-
<Dialog Title="Are you sure you want to delete this tag definition?" @ref=deleteTagDialog>
<ButtonContainer>
<button @onclick=@(() => deleteTagDialog.Hide()) class="secondary">Cancel</button>
@@ -103,23 +73,19 @@
</ButtonContainer>
</Dialog>
+<TagEditDialog OnTagUpdate=TagUpdated @ref=tagEditDialog/>
+
<TagSelectDialog
OnSubmit=SetImplicitTags
@ref=implicitTagDialog />
@code {
private TabContainer tabContainer;
- private Dialog createTagDialog;
private Dialog deleteTagDialog;
- private Dialog editTagDialog;
+ private TagEditDialog tagEditDialog;
private TagSelectDialog implicitTagDialog;
- private string tagName;
- private string? tagNamespace;
- private string? tagAlias;
-
private TagDefinition? toDelete;
- private TagDefinition? toEdit;
private TagDefinition? toEditImplicit;
private TagDefinition[] tagDefinitions;
@@ -157,39 +123,6 @@
.ToArray();
}
- private void PromptToCreate() {
- var curTitle = tabContainer.ActivePane?.Title;
- tagNamespace = curTitle == "Default" ? null : curTitle;
- createTagDialog.Show();
- }
-
- private void CreateTagDefinition() {
- if(string.IsNullOrEmpty(tagNamespace))
- tagNamespace = null;
-
- tagService.CreateTagDefinition(tagName, tagNamespace, tagAlias);
- createTagDialog.Hide();
- LoadTags();
- StateHasChanged();
- }
-
- private void PromptToEdit(TagDefinition toEdit) {
- this.toEdit = toEdit;
- tagName = toEdit.Name;
- tagNamespace = toEdit.Namespace;
- tagAlias = toEdit.Alias;
- editTagDialog.Show();
- }
-
- private void EditTagDefinition() {
- if(toEdit is null)
- return;
-
- tagService.UpdateTagDefinition(toEdit, tagName, tagNamespace, tagAlias);
- LoadTags();
- StateHasChanged();
- }
-
private void PromptToDelete(TagDefinition toDelete) {
this.toDelete = toDelete;
deleteTagDialog.Show();
@@ -205,6 +138,18 @@
StateHasChanged();
}
+ private void PromptTagCreate() {
+ string? @namespace = tabContainer.ActivePane?.Title;
+ if(@namespace == "Default")
+ @namespace = null;
+ tagEditDialog.Show(@namespace);
+ }
+
+ private void TagUpdated(object? sender, EventArgs e) {
+ LoadTags();
+ StateHasChanged();
+ }
+
private void PromptImplicitTags(TagDefinition toEditImplicit) {
this.toEditImplicit = toEditImplicit;
implicitTagDialog.SelectedTags =
diff --git a/Pages/TagDefinitions.razor.css b/Pages/TagDefinitions.razor.css
deleted file mode 100644
index b66d491..0000000
--- a/Pages/TagDefinitions.razor.css
+++ /dev/null
@@ -1,3 +0,0 @@
-form > input {
- width: 100%;
-} \ No newline at end of file
diff --git a/Services/TagService.cs b/Services/TagService.cs
index 2648bb3..a3c4b29 100644
--- a/Services/TagService.cs
+++ b/Services/TagService.cs
@@ -126,14 +126,29 @@ public class TagService : ITagService {
public void CreateTagDefinition(string name, string? @namespace = null, string? alias = null) {
using var db = dbFactory.CreateDbContext();
- TagDefinition tagdef = new() {
+ if(string.IsNullOrEmpty(@namespace))
+ @namespace = null;
+ if(string.IsNullOrEmpty(alias))
+ alias = null;
+
+ TagDefinition tagDef = new() {
Source = TagSource.UserTag,
Namespace = @namespace,
Name = name,
Alias = alias
};
- if(!db.TagDefinitions.Contains(tagdef))
- db.TagDefinitions.Add(tagdef);
+
+ bool nameExists = db.TagDefinitions.Any(td => td.Name.ToLower() == name.ToLower());
+ bool aliasExists = false;
+ if(alias is not null)
+ aliasExists = db.TagDefinitions
+ .Where(td => td.Alias != null)
+ .Any(td => td.Alias!.ToLower() == alias.ToLower());
+ if(nameExists || aliasExists)
+ throw new TagDuplicateException(nameExists, aliasExists);
+
+ if(!db.TagDefinitions.Contains(tagDef))
+ db.TagDefinitions.Add(tagDef);
db.SaveChanges();
}
@@ -162,8 +177,22 @@ public class TagService : ITagService {
if(string.IsNullOrEmpty(@namespace))
@namespace = null;
+ if(string.IsNullOrEmpty(alias))
+ alias = null;
var tag = db.TagDefinitions.First(td => td.Guid == tagDef);
+
+ TagDefinition? nameExisting = db.TagDefinitions.FirstOrDefault(td => td.Name.ToLower() == name.ToLower());
+ TagDefinition? aliasExisting = null;
+ if(alias is not null)
+ aliasExisting = db.TagDefinitions
+ .Where(td => td.Alias != null)
+ .FirstOrDefault(td => td.Alias!.ToLower() == alias.ToLower());
+ bool nameExists = nameExisting is not null && nameExisting != tag;
+ bool aliasExists = aliasExisting is not null && aliasExisting != tag;
+ if(nameExists || aliasExists)
+ throw new TagDuplicateException(nameExists, aliasExists);
+
tag.Name = name;
tag.Namespace = @namespace;
tag.Alias = alias;
diff --git a/Todo.md b/Todo.md
index 003cec2..4d4bb3f 100644
--- a/Todo.md
+++ b/Todo.md
@@ -1,15 +1,11 @@
# Bugs
- Input not focused
- - Tag aliases/names not verified to be unique
- - Tag edit dialog doesn't close when clicking Apply button
- - Tag selection dialog has no max height
- Setting implicit tags removes builtin tags
- UserService listeners don't seem to be removed after disposal
- Cancelling tag creation creates the tag anyway
- Prevent marking tagging complete unless there are actually user tags
# Short-term Features
- - Progressive page loading
- Proper thumbnail generation
- Video support
- User/security support
diff --git a/wwwroot/styles/global.css b/wwwroot/styles/global.css
index 50f20f1..a8c4202 100644
--- a/wwwroot/styles/global.css
+++ b/wwwroot/styles/global.css
@@ -3,6 +3,7 @@
:root {
--col-accent-pri: #0aa;
--col-accent-pri-hl: #0cc;
+ --col-error-pri: #ffaa00;
--col-bg: #222;
--col-dialog-bg: #333;
--col-navbar-bg: var(--col-accent-pri);