summaryrefslogtreecommitdiff
path: root/Pages
diff options
context:
space:
mode:
authorJake Mannens <jake@asger.xyz>2023-08-15 15:49:14 +1000
committerJake Mannens <jake@asger.xyz>2025-08-18 10:59:32 +1000
commit38c60cee378b9c2ad42fc9dc79bc492b919a68f5 (patch)
tree6b62f84aab4b7866432e5da8ae8fcb889795d58b /Pages
parent07a4c7ead01514bd3f304f00abc38140a1d73634 (diff)
Convert Razor pages to Blazor
Diffstat (limited to 'Pages')
-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/Shared/_Layout.cshtml.css36
-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
26 files changed, 575 insertions, 559 deletions
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/Shared/_Layout.cshtml.css b/Pages/Shared/_Layout.cshtml.css
deleted file mode 100644
index 5b2f26e..0000000
--- a/Pages/Shared/_Layout.cshtml.css
+++ /dev/null
@@ -1,36 +0,0 @@
-div#navbar {
- background: var(--col-navbar-bg);
- box-shadow: rgba(0, 0, 0, 0.5) 0px 10px 10px;
- display: flex;
-}
-
-div#navbar > a {
- color: white;
- display: inline-block;
- padding: 20px 20px 20px 20px;
-}
-
-div#navbar > a:hover {
- background: rgba(255, 255, 255, 0.4);
- filter: none;
-}
-
-div#navbar > a:active {
- background: #fff;
- color: var(--col-navbar-bg);
-}
-
-div#navbar > input {
- align-self: center;
- background: var(--col-bg);
- border-radius: 0;
- color: white;
- height: 40px !important;
- margin: 0 20px 0 auto;
- font-size: 12pt;
- min-width: 30%;
-}
-
-#content {
- flex: 1 1 calc(100vh - 119px);
-} \ 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