media assets: redesign show page.

Redesign the media assets show page to:

* Include sidebar with AI tags and image metadata.
* Include next and previous image buttons.
* Make the image use 100% of the available screen space and to scroll with the window.
This commit is contained in:
evazion
2022-11-27 21:19:05 -06:00
parent 8a2f59172b
commit a5d4af332d
8 changed files with 258 additions and 60 deletions

View File

@@ -2,12 +2,19 @@
# A component for showing a full-sized image or video for a media asset.
class MediaAssetComponent < ApplicationComponent
attr_reader :media_asset
attr_reader :media_asset, :current_user, :outer_classes, :inner_classes, :dynamic_height
delegate :image_width, :image_height, :variant, :is_image?, :is_video?, :is_ugoira?, :is_flash?, to: :media_asset
def initialize(media_asset:)
renders_one :header
renders_one :footer
def initialize(media_asset:, current_user:, outer_classes: "", inner_classes: "", dynamic_height: false)
super
@media_asset = media_asset
@current_user = current_user
@outer_classes = outer_classes
@inner_classes = inner_classes
@dynamic_height = dynamic_height
end
end

View File

@@ -1,15 +1,23 @@
<div class="media-asset-component relative fit-screen">
<div class="media-asset-zoom-level hidden absolute top-0.5 left-0.5 p-1 m-0.5 leading-none rounded text-xs font-arial font-bold pointer-events-none">
100%
<div class="media-asset-container media-asset-container-fit-height flex flex-col <%= outer_classes %>" data-dynamic-height="<%= dynamic_height %>" style="--header-visible-height: 0px">
<%= header %>
<div class="media-asset-component relative max-h-inherit overflow-hidden <%= inner_classes %>">
<div class="media-asset-zoom-level hidden absolute top-0.5 left-0.5 p-1 m-0.5 leading-none rounded text-xs font-arial font-bold pointer-events-none transition-opacity">
100%
</div>
<% if !policy(media_asset).can_see_image? %>
<p>Image unavailable.</p>
<% elsif is_image? %>
<%= tag.img src: variant(:original).file_url, width: image_width, height: image_height, draggable: "false", class: "media-asset-image max-h-inherit max-w-full h-full w-auto select-none" -%>
<% elsif is_video? %>
<%= tag.video src: variant(:original).file_url, width: image_width, height: image_height, autoplay: true, loop: true, controls: "controls" %>
<% elsif is_ugoira? %>
<%= tag.video src: variant(:sample).file_url, width: image_width, height: image_height, autoplay: true, loop: true, controls: "controls" %>
<% elsif is_flash? %>
<%= tag.div class: "ruffle-container", "data-swf": variant(:original).file_url %>
<% end %>
</div>
<% if is_image? %>
<%= tag.img src: variant(:original).file_url, width: image_width, height: image_height, draggable: "false", class: "media-asset-image select-none" -%>
<% elsif is_video? %>
<%= tag.video src: variant(:original).file_url, width: image_width, height: image_height, autoplay: true, loop: true, controls: "controls" %>
<% elsif is_ugoira? %>
<%= tag.video src: variant(:sample).file_url, width: image_width, height: image_height, autoplay: true, loop: true, controls: "controls" %>
<% elsif is_flash? %>
<%= tag.div class: "ruffle-container", "data-swf": variant(:original).file_url %>
<% end %>
<%= footer %>
</div>

View File

@@ -1,11 +1,41 @@
.media-asset-component.fit-screen img, .media-asset-component.fit-screen video {
max-width: 100%;
max-height: 90vh;
width: auto;
height: auto;
.media-asset-component:not(:hover) .media-asset-zoom-level {
opacity: 0;
}
.media-asset-zoom-level {
color: var(--preview-icon-color);
background: var(--preview-icon-background);
}
.media-asset-container {
&:not(.media-asset-container-fit-height) .paginator {
position: sticky;
}
&:hover {
.paginator-prev, .paginator-next {
opacity: 0.8;
}
}
.paginator-prev, .paginator-next {
opacity: 0;
width: 48px;
height: 96px;
top: calc((100vh - var(--header-visible-height) - 96px) / 2);
transition: opacity 0.125s, background-color 0.125s;
color: var(--asset-paginator-link-color);
background-color: var(--asset-paginator-background-color);
&:hover {
color: var(--asset-paginator-link-hover-color);
background-color: var(--asset-paginator-background-hover-color);
}
}
&.media-asset-container-fit-height {
max-height: calc(100vh - var(--header-visible-height));
justify-content: center;
}
}

View File

@@ -1,47 +1,72 @@
export default class MediaAssetComponent {
static initialize() {
$(".media-asset-component").toArray().forEach(element => {
$(".media-asset-container").toArray().forEach(element => {
new MediaAssetComponent(element);
});
}
constructor(element) {
this.$component = $(element);
this.$container = $(element);
this.$component = this.$container.find(".media-asset-component");
if (this.$container.attr("data-dynamic-height") === "true") {
this.updateHeight();
$(window).on("scroll.danbooru", element => {
this.updateHeight();
});
}
if (this.$image.length) {
this.$image.on("click.danbooru", e => this.toggleFit());
this.$image.on("load.danbooru", e => this.updateZoom());
this.$image.on("load.danbooru", e => this.updateHeight());
new ResizeObserver(() => this.updateZoom()).observe(this.$image.get(0));
this.updateZoom();
}
}
toggleFit() {
this.$component.toggleClass("fit-screen");
if (this.canZoom) {
this.$container.toggleClass("media-asset-container-fit-height");
}
this.updateZoom();
}
updateZoom() {
this.$image.removeClass("cursor-zoom-in cursor-zoom-out");
this.$zoomLevel.addClass("hidden").text(`${Math.round(100 * this.zoomLevel)}%`);
this.$zoomLevel.removeClass("hidden").text(`${Math.round(100 * this.zoomLevel)}%`);
if (this.isDownscaled) {
this.$image.addClass("cursor-zoom-out");
this.$zoomLevel.removeClass("hidden");
} else if (this.isTooBig) {
if (this.canZoomIn) {
this.$image.addClass("cursor-zoom-in");
} else if (this.canZoomOut) {
this.$image.addClass("cursor-zoom-out");
}
}
updateHeight() {
// XXX 115 = header height (hardcoded to prevent height glitches as page loads)
this.$container.css("--header-visible-height", Math.min(115, Math.max(0, this.$container.offset().top - $(window).scrollTop())) + "px");
}
get zoomLevel() {
return this.$image.width() / Number(this.$image.attr("width"));
}
get isDownscaled() {
return this.$image.width() < Number(this.$image.attr("width"));
get canZoom() {
return this.canZoomIn || this.canZoomOut;
}
get isTooBig() {
return this.$image.width() > this.$component.width();
get canZoomIn() {
return !this.isZoomed && this.$image.height() < this.$image.get(0).naturalHeight && Math.round(this.$image.width()) < Math.round(this.$container.width());
}
get canZoomOut() {
return this.isZoomed;
}
get isZoomed() {
return !this.$container.is(".media-asset-container-fit-height");
}
get $image() {

View File

@@ -116,6 +116,9 @@ html, body[data-current-user-theme="light"] {
--default-border-color: var(--grey-1);
--success-color: var(--green-5);
--error-color: var(--red-5);
--error-background-color: var(--red-1);
--success-background-color: var(--green-0);
--target-background: var(--yellow-0);
@@ -287,6 +290,11 @@ html, body[data-current-user-theme="light"] {
--paginator-arrow-background-color: var(--inverse-text-color);
--paginator-arrow-color: var(--link-color);
--asset-paginator-link-color: var(--link-color);
--asset-paginator-link-hover-color: var(--link-color);
--asset-paginator-background-color: var(--white);
--asset-paginator-background-hover-color: var(--blue-0);
--artist-tag-color: var(--red-6);
--artist-tag-hover-color: var(--red-5);
--copyright-tag-color: var(--magenta-6);
@@ -334,6 +342,9 @@ html, body[data-current-user-theme="light"] {
--link-color: var(--azure-4);
--link-hover-color: var(--azure-3);
--success-color: var(--green-3);
--error-color: var(--red-3);
--default-border-color: var(--grey-7);
--error-background-color: var(--red-9);
@@ -494,6 +505,11 @@ html, body[data-current-user-theme="light"] {
--paginator-arrow-background-color: var(--grey-0);
--paginator-arrow-color: var(--link-color);
--asset-paginator-link-color: var(--white);
--asset-paginator-link-hover-color: var(--white);
--asset-paginator-background-color: var(--grey-9);
--asset-paginator-background-hover-color: var(--grey-8);
--artist-tag-color: var(--red-3);
--artist-tag-hover-color: var(--red-2);
--copyright-tag-color: var(--purple-3);

View File

@@ -46,6 +46,7 @@ $spacer: 0.25rem; /* 4px */
}
.overflow-auto { overflow: auto; }
.overflow-hidden { overflow: hidden; }
.break-all { word-break: break-all; }
.whitespace-nowrap { white-space: nowrap; }
@@ -62,11 +63,21 @@ $spacer: 0.25rem; /* 4px */
.relative { position: relative; }
.sticky { position: sticky; }
.top-0 { top: 0; }
.bottom-0 { bottom: 0; }
.left-0 { left: 0; }
.right-0 { right: 0; }
.top-0\.5 { top: 0.5 * $spacer; }
.bottom-0\.5 { bottom: 0.5 * $spacer; }
.left-0\.5 { left: 0.5 * $spacer; }
.right-0\.5 { right: 0.5 * $spacer; }
.top-4 { top: 4 * $spacer; }
.bottom-4 { bottom: 4 * $spacer; }
.left-4 { left: 4 * $spacer; }
.right-4 { right: 4 * $spacer; }
.inset-0 { top: 0; right: 0; bottom: 0; left: 0 }
.border, %border { border-width: 1px; }
@@ -138,6 +149,7 @@ $spacer: 0.25rem; /* 4px */
.w-auto { width: auto; }
.w-min { width: min-content; }
.w-max { width: max-content; }
.w-fit { width: fit-content; }
.w-sm { width: 24rem; }
.w-md { width: 28rem; }
.w-1\/4 { width: 25%; }
@@ -149,15 +161,20 @@ $spacer: 0.25rem; /* 4px */
.w-225px { width: 225px; }
.w-270px { width: 270px; }
.w-360px { width: 360px; }
.w-600px { width: 600px; }
.min-w-0 { min-width: 0; }
.max-w-full { max-width: 100%; }
.max-h-full { max-height: 100%; }
.h-auto { height: auto; }
.h-inherit { height: inherit; }
.h-fit { height: fit-content; }
.h-1 { height: 1 * $spacer; }
.h-3 { height: 3 * $spacer; }
.h-4 { height: 4 * $spacer; }
.h-6 { height: 6 * $spacer; }
.h-8 { height: 8 * $spacer; }
.h-10 { height: 10 * $spacer; }
.h-12 { height: 12 * $spacer; }
@@ -176,6 +193,7 @@ $spacer: 0.25rem; /* 4px */
.max-h-360px { max-height: 360px; }
.max-h-720px { max-height: 720px; }
.max-h-screen { max-height: 100vh; }
.max-h-inherit { max-height: inherit; }
.space-x-1 > * + * { margin-left: 1 * $spacer; }
.space-x-2 > * + * { margin-left: 2 * $spacer; }
@@ -195,6 +213,7 @@ $spacer: 0.25rem; /* 4px */
.flex-1 { flex: 1 1 0%; }
.flex-auto { flex: 1 1 auto; }
.flex-initial { flex: 0 1 auto; }
.flex-none { flex: none; }
.flex-grow-1 { flex-grow: 1; }
.flex-col { flex-direction: column; }
.flex-wrap { flex-wrap: wrap; }
@@ -212,6 +231,7 @@ $spacer: 0.25rem; /* 4px */
.self-start { align-self: flex-start; }
.self-center { align-self: center; }
.self-stretch { align-self: stretch; }
.self-end { align-self: flex-end; }
.float-right { float: right; }
@@ -223,8 +243,19 @@ $spacer: 0.25rem; /* 4px */
.grid-cols-8 { grid-template-columns: repeat(8, minmax(0, 1fr)); }
.grid-cols-12 { grid-template-columns: repeat(12, minmax(0, 1fr)); }
.z-0 { z-index: 0; }
.z-10 { z-index: 10; }
.z-20 { z-index: 20; }
.link-color { color: var(--link-color); }
.text-success { color: var(--success-color); }
.text-error { color: var(--error-color); }
.transition-opacity {
transition: opacity 0.15s ease;
}
.card {
@extend %border;
@extend %rounded-lg;
@@ -232,10 +263,9 @@ $spacer: 0.25rem; /* 4px */
}
.thin-scrollbar {
overflow-x: hidden;
overflow-y: auto;
overflow: auto;
padding-right: 2 * $spacer;
overscroll-behavior: contain; // https://caniuse.com/css-overscroll-behavior
overscroll-behavior-x: contain; // https://caniuse.com/css-overscroll-behavior
// Firefox only
// https://caniuse.com/?search=scrollbar-width
@@ -304,8 +334,11 @@ $spacer: 0.25rem; /* 4px */
.md\:block { display: block; }
.md\:flex { display: flex; }
.md\:grid { display: grid; }
.md\:sticky { position: sticky; }
.md\:space-x-4 > * + * { margin-left: 4 * $spacer; }
.md\:space-x-8 > * + * { margin-left: 8 * $spacer; }
.md\:space-y-0 > * + * { margin-top: 0; }
.md\:w-360px { width: 360px; }
}

View File

@@ -1,33 +1,112 @@
<div id="c-media-assets">
<div id="a-show" class="fixed-width-container">
<h1 class="mb-4">Media Asset</h1>
<div id="a-show">
<div class="md:flex flex-row gap-4">
<div class="flex-1 w-full">
<%= render MediaAssetComponent.new(media_asset: @media_asset, current_user: CurrentUser.user, outer_classes: "sticky h-full relative top-0", inner_classes: "mx-auto", dynamic_height: true) do |component| %>
<% component.with_header do %>
<div class="paginator top-0 w-full z-10">
<%= link_to chevron_left_icon, media_assets_path(search: { id: ">#{@media_asset.id}", order: "id_asc" }, limit: 1, redirect: true), class: "paginator-prev flex items-center justify-center text-xl absolute left-0 z-10", "data-shortcut": "a left" %>
<%= link_to chevron_right_icon, media_assets_path(search: { id: "<#{@media_asset.id}", order: "id_desc" }, limit: 1, redirect: true), class: "paginator-next flex items-center justify-center text-xl absolute right-0 z-10", "data-shortcut": "d right" %>
</div>
<% end %>
<% if policy(@media_asset).can_see_image? %>
<%= render MediaAssetComponent.new(media_asset: @media_asset) %>
<% end %>
<% if policy(@media_asset).can_see_image? %>
<% component.with_footer do %>
<div class="flex flex-none h-6 items-center justify-center text-xs">
<%= link_to @media_asset.original.file_url do %>
<%= number_to_human_size(@media_asset.file_size) %> .<%= @media_asset.file_ext %>,
<%= @media_asset.image_width %>x<%= @media_asset.image_height %>
<table class="striped aligned-vertical">
<% if @post.present? %>
<tr>
<th>Post</th>
<td><%= link_to "##{@post.id}", @post %></td>
</tr>
<% end %>
<% if @media_asset.duration.present? %>
(<%= duration_to_hhmmss(@media_asset.duration) %>)
<% end %>
<% end %>
</div>
<% end %>
<% end %>
<% end %>
</div>
<% if policy(@media_asset).can_see_image? %>
<tr>
<th>MD5</th>
<td><%= @media_asset.md5 %></td>
</tr>
<% end %>
<div class="media-asset-sidebar md:w-360px">
<div>
<h3>Tags</h3>
<% @media_asset.metadata.sort.each do |key, value| %>
<tr>
<th><%= key %></th>
<td><%= link_to value, media_assets_path(search: { metadata: { key => value }}) %></td>
</tr>
<% end %>
</table>
<ul class="tag-list categorized-tag-list">
<% @media_asset.ai_tags.undeprecated.sort_by { |tag| [TagCategory.split_header_list.index(tag.category_name.downcase), tag.name.starts_with?("rating:") ? 0 : 1, tag.name] }.each do |ai_tag| %>
<%= tag.li class: "tag-type-#{ai_tag.category} space-x-1 text-sm", "data-tag-name": ai_tag.name do %>
<% if ai_tag.tag.artist? %>
<%= link_to "?", show_or_new_artists_path(name: ai_tag.name) %>
<% elsif ai_tag.name =~ /\A\d+\z/ %>
<%= link_to "?", wiki_page_path("~#{ai_tag.name}") %>
<% else %>
<%= link_to "?", wiki_page_path(ai_tag.name) %>
<% end %>
<%= link_to ai_tag.pretty_name, media_assets_path(search: { ai_tags_match: ai_tag.name }), class: "break-word" %>
<%= tag.span "#{ai_tag.score}%", class: ["text-muted text-xs"] %>
<% end %>
<% end %>
</ul>
</div>
<div>
<table class="striped text-sm w-full">
<tr>
<th colspan="2" class="text-center">Metadata</th>
</tr>
<% if @post.present? %>
<tr>
<th class="text-left">Post</th>
<td class="break-all"><%= link_to "##{@post.id}", @post %></td>
</tr>
<% end %>
<% if policy(@media_asset).can_see_image? %>
<tr>
<th class="text-left">MD5</th>
<td class="break-all"><%= @media_asset.md5 %></td>
</tr>
<% end %>
<tr>
<th class="text-left">File Type</th>
<td class="break-all"><%= @media_asset.mime_type %></td>
</tr>
<tr>
<th class="text-left">File Size</th>
<td class="break-all"><%= number_to_human_size(@media_asset.file_size) %></td>
</tr>
<tr>
<th class="text-left">Resolution</th>
<td class="break-all"><%= @media_asset.image_width %>x<%= @media_asset.image_height %></td>
</tr>
<% if @media_asset.duration.present? %>
<tr>
<th class="text-left">Duration</th>
<td class="break-all"><%= duration_to_hhmmss(@media_asset.duration) %></td>
</tr>
<% end %>
<% @media_asset.metadata.group_by { |key, value| key.split(":").first }.sort.each do |group, pairs| %>
<tr>
<th colspan="2" class="text-center"><%= group.split(/[ _-]/).map(&:upcase_first).join(" ") %></th>
</tr>
<% pairs.sort.each do |key, value| %>
<tr>
<th class="text-left w-1/4"><%= key.split(":").second.scan(/(?:[A-Z][a-z]+|[A-Z]+(?![a-z]))/).join(" ") %></th>
<td class="break-all"><%= link_to value, media_assets_path(search: { metadata: { key => value }}) %></td>
</tr>
<% end %>
<% end %>
</table>
</div>
</div>
</div>
</div>
</div>

View File

@@ -23,7 +23,7 @@
<div id="client-errors" class="error-messages ui-state-error ui-corner-all" style="display:none"></div>
<div id="upload-image">
<%= render MediaAssetComponent.new(media_asset: media_asset) %>
<%= render MediaAssetComponent.new(media_asset: media_asset, current_user: CurrentUser.user) %>
<p>
<strong>Size</strong>