diff options
| author | Jake Mannens <jake@asger.xyz> | 2026-03-17 03:04:36 +1100 |
|---|---|---|
| committer | Jake Mannens <jake@asger.xyz> | 2026-06-07 12:32:37 +1000 |
| commit | c51ff4e755f009ca0bc8e935a92c04e583c4ee8a (patch) | |
| tree | 0a9a311c5404a96495df1047e613dc3aea3d0f15 /Pages/ViewMedia.razor | |
Initial commit
Diffstat (limited to 'Pages/ViewMedia.razor')
| -rw-r--r-- | Pages/ViewMedia.razor | 265 |
1 files changed, 265 insertions, 0 deletions
diff --git a/Pages/ViewMedia.razor b/Pages/ViewMedia.razor new file mode 100644 index 0000000..46cbc45 --- /dev/null +++ b/Pages/ViewMedia.razor @@ -0,0 +1,265 @@ +@page "/ViewMedia" +@using HyperBooru.Util +@inject IJSRuntime jsRuntime +@inject IDbContextFactory<HBContext> dbFactory +@inject ITagService tagService +@inject IMediaService mediaService +@inject ISourceService sourceService +@attribute [Authorize] + +<PageTitle>@title</PageTitle> + +<script suppress-warning="BL9992"> + function toggleSidebar() { + document.getElementById("hcontainer").classList.toggle("hide-metadata"); + } + + function setMobilePane(pane) { + var panes = Array.from(document.querySelectorAll('[class^="mobile-pane-"]')); + + panes.forEach(e => e.classList.remove('visible')); + panes + .filter(e => e.classList.contains(`mobile-pane-${pane}`)) + .forEach(e => e.classList.add('visible')); + } + + function pageKeyDownHandler(e) { + if(!e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey && !e.isComposing) + if(e.key == 's') + toggleSidebar(); + } +</script> + +<div id="vcontainer"> + <div id="hcontainer"> + <div id="image-container" class="mobile-pane-image visible"> + <img + src="/media/@(media.Guid)" + width=@media.CurrentUploadedFile.Width + height=@media.CurrentUploadedFile.Height/> + </div> + <div id="metadata-show-button"> + <a href="javascript:toggleSidebar();" title="Toggle sidebar (S)"></a> + </div> + <div id="metadata" class="mobile-pane-metadata"> + <div id="metadata-container"> + <div id="metadata-fileinfo"> + @if(infoEditMode) { + <form action="javascript:;" @onsubmit=@(() => ApplyInfoEdit(true))> + <table id="edit-metadata"> + <tr> + <td>Title:</td> + <td><input type="text" @bind=shortDescription @ref=shortDescriptionInput/></td> + </tr> + <tr> + <td>Description:</td> + <td><textarea rows="4" @bind=longDescription/></td> + </tr> + </table> + </form> + } else { + <p>Title: <i>@(media.ShortDescription ?? "None")</i></p> + <p class="newlines">Description:<br/><i>@(media.LongDescription ?? "None")</i></p> + } + <p>Resolution: @(media.CurrentUploadedFile.Width)x@(media.CurrentUploadedFile.Height)</p> + <p class="heading">Upload history</p> + <hr/> + <table id="uploaded-files" class="data-table"> + <tr> + <th>Created On</th> + <th>Last Write</th> + <th>Uploaded On</th> + <th>Filename</th> + <th>Size</th> + <th>Original Checksum</th> + </tr> + @foreach(var file in media.UploadedFiles.OrderByDescending(uf => uf.UploadTime)) { + string? sourceUrl = null; + if(file.Filename is not null) + sourceUrl = sourceService.GetUrlFromFilename(file.Filename); + <tr> + <td title=@file.CreateTime?.ToString()> + @(file.CreateTime?.ToString("d") ?? "N/A") + </td> + <td title=@file.LastWriteTime?.ToString()> + @(file.LastWriteTime?.ToString("d") ?? "N/A") + </td> + <td title=@file.UploadTime>@(file.UploadTime.ToString("d"))</td> + <td title=@(file.Path is not null ? $"{file.Path.Replace('\\', '/')}/{file.Filename}" : file.Filename)> + @if(sourceUrl is not null) { + <a class="nondecorated" target="_blank" href=@sourceUrl>@file.Filename</a> + } else { + @file.Filename + } + </td> + <td title=@file.Length>@file.Length.ToBytesSI()</td> + <td + title=@(file.Checksum + (file.ChecksumVerified ? " (verified)" : "")) + class=@(file.ChecksumVerified ? "verified" : null)> + + @file.Checksum.Substring(0, 8) + </td> + </tr> + } + </table> + </div> + <div id="metadata-tags"> + <p class="heading">Tags</p> + <hr/> + <MediaTagTable Media=media @ref=mediaTagTable/> + </div> + </div> + <div id="button-container"> + <ButtonContainer> + <button @onclick=@(() => deleteDialog.Show()) class="warning" data-keyboard-shortcut="d"> + <img src="/images/trash.svg"/> + <p><u>D</u>elete</p> + </button> + <button @onclick=@(() => tagDialog.Show()) class="secondary" data-keyboard-shortcut="t"> + <img src="/images/tag.svg"/> + <p>Add <u>T</u>ag</p> + </button> + <button @onclick=@(() => ocrDialog.Show()) class="secondary" data-keyboard-shortcut="o"> + <img src="/images/book.svg"/> + <p>View <u>O</u>CR</p> + </button> + @if(infoEditMode) { + <button @onclick=@(() => ApplyInfoEdit(false)) class="secondary"> + <img src="/images/cross.svg"/> + <p>Cancel</p> + </button> + <button @onclick=@(() => ApplyInfoEdit(true))> + <img src="/images/checkmark.svg"/> + <p>Apply</p> + </button> + } else { + <button @onclick=@(() => InfoEditMode = true) class="secondary" data-keyboard-shortcut="e"> + <img src="/images/edit.svg"/> + <p><u>E</u>dit Info</p> + </button> + } + @if(media.IsIngest) { + <button @onclick=@(() => SetIngest(false)) data-keyboard-shortcut="c"> + <img src="/images/checkmark.svg"/> + <p>Mark Tagging <u>C</u>omplete</p> + </button> + } else { + <button class="secondary" @onclick=@(() => SetIngest(true)) data-keyboard-shortcut="c"> + <img src="/images/cross.svg"/> + <p>Mark Tagging In<u>c</u>omplete</p> + </button> + } + </ButtonContainer> + </div> + </div> + </div> + <div id="bottom-bar"> + <img onclick="setMobilePane('image');" src="/images/photo.svg" width="25" height="25"/> + <img onclick="setMobilePane('metadata');" src="/images/info.svg" width="25" height="25"/> + </div> +</div> + +<Dialog Title="Delete this media?" @ref=deleteDialog> + <ButtonContainer> + <button @onclick=@(() => deleteDialog.Hide()) class="secondary">Cancel</button> + <button @onclick=DeleteMedia class="warning">Confirm</button> + </ButtonContainer> +</Dialog> + +<Dialog Title="OCR Data" @ref=ocrDialog> + @if(media.OcrData is null) { + <p><center>This media item hasn't been scanned yet!</center></p> + } else { + <code style="max-height:400px;">@media.OcrData?.Text</code> + } + <ButtonContainer> + <button @onclick=@(() => ocrDialog.Hide())>Close</button> + </ButtonContainer> +</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 string? shortDescription; + private string? longDescription; + + private MediaTagTable mediaTagTable; + private Dialog deleteDialog; + private Dialog ocrDialog; + private TagSelectDialog tagDialog; + + private ElementReference shortDescriptionInput; + + protected override void OnInitialized() => + LoadMedia(); + + protected override async void OnAfterRender(bool firstRender) { + if(infoEditMode) + await shortDescriptionInput.FocusAsync(); + } + + private void LoadMedia() { + using var db = dbFactory.CreateDbContext(); + media = db.Media + .Include(m => m.Tags) + .ThenInclude(t => t.TagDefinition) + .Include(m => m.CurrentUploadedFile) + .Include(m => m.UploadedFiles) + .Include(m => m.OcrData) + .First(m => m.Guid == MediaId); + + title = media.DisplayName ?? "Media View"; + } + + private void AddTags(TagDefinition[] tagDefs) { + foreach(var tagDef in tagDefs) + tagService.AddTag(media, tagDef); + mediaTagTable.Refresh(); + } + + private async void SetIngest(bool ingest) { + mediaService.SetIngest(media, ingest); + LoadMedia(); + + if(ingest) + StateHasChanged(); + else + await jsRuntime.InvokeVoidAsync("history.back"); + } + + private bool InfoEditMode { + get => infoEditMode; + set { + shortDescription = media.ShortDescription; + longDescription = media.LongDescription; + infoEditMode = value; + StateHasChanged(); + } + } + + private void ApplyInfoEdit(bool apply) { + if(apply) { + mediaService.SetDescription(media, shortDescription, longDescription); + LoadMedia(); + } + + infoEditMode = false; + } + + private async void DeleteMedia() { + mediaService.Delete(media); + await jsRuntime.InvokeVoidAsync("history.back"); + } +} |
