diff options
Diffstat (limited to 'Pages')
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 |
