posts: show length of videos and animations in thumbnails.

Show the length of videos and animated posts in the thumbnail. The
length is shown the top left corner in MM:SS format. This replaces the
play button icon.

Show a speaker icon instead of a music note icon for posts with sound.

Doing this requires doing `.includes(:media_asset)` in a bunch of
places to avoid N+1 queries when we access the post's duration.
This commit is contained in:
evazion
2021-10-25 01:31:47 -05:00
parent be505920d1
commit f1b5c34b4d
31 changed files with 111 additions and 81 deletions

View File

@@ -5,7 +5,9 @@ class PostPreviewComponent < ApplicationComponent
attr_reader :post, :tags, :show_deleted, :show_cropped, :link_target, :pool, :similarity, :recommended, :compact, :size, :current_user, :options
delegate :external_link_to, :time_ago_in_words_tagged, :empty_heart_icon, to: :helpers
delegate :external_link_to, :time_ago_in_words_tagged, :duration_to_hhmmss, :empty_heart_icon, :sound_icon, to: :helpers
delegate :image_width, :image_height, :file_ext, :file_size, :duration, :is_animated?, to: :media_asset
delegate :media_asset, to: :post
def initialize(post:, tags: "", show_deleted: false, show_cropped: true, link_target: post, pool: nil, similarity: nil, recommended: nil, compact: nil, size: nil, current_user: CurrentUser.user, **options)
super
@@ -67,7 +69,7 @@ class PostPreviewComponent < ApplicationComponent
def data_attributes
attributes = {
"data-id" => post.id,
"data-has-sound" => post.has_tag?("sound"),
"data-has-sound" => has_sound?,
"data-tags" => post.tag_string,
"data-approver-id" => post.approver_id,
"data-rating" => post.rating,
@@ -96,4 +98,8 @@ class PostPreviewComponent < ApplicationComponent
attributes
end
def has_sound?
post.has_tag?("sound")
end
end

View File

@@ -1,5 +1,18 @@
<%= tag.article id: "post_#{post.id}", **article_attrs do -%>
<%= link_to polymorphic_path(link_target, q: tags) do -%>
<% if is_animated? || has_sound? %>
<div class="post-animation-icon absolute top-0.5 left-0.5 p-0.5 leading-none text-xs">
<% if is_animated? %>
<span class="post-duration align-middle">
<%= duration_to_hhmmss(duration) %>
</span>
<% end %>
<% if has_sound? %>
<%= sound_icon(class: "h-3 mx-0.5") -%>
<% end %>
</div>
<% end %>
<%= tag.picture do -%>
<%= tag.source media: "(max-width: 660px)", srcset: cropped_url -%>
<%= tag.source media: "(min-width: 660px)", srcset: post.preview_file_url -%>

View File

@@ -11,6 +11,12 @@ article.post-preview {
a {
display: inline-block;
position: relative;
.post-animation-icon {
color: var(--preview-icon-color);
background: var(--preview-icon-background);
}
}
&.captioned {
@@ -31,14 +37,6 @@ article.post-preview {
img {
margin: auto;
}
&[data-tags~=animated]::before, &[data-file-ext=swf]::before, &[data-file-ext=webm]::before, &[data-file-ext=mp4]::before, &[data-file-ext=zip]::before {
@include animated-icon;
}
&[data-has-sound=true]::before {
@include sound-icon;
}
}
/* Avoid dead space around thumbnails in tables. */

View File

@@ -3,7 +3,7 @@ class ArtistCommentariesController < ApplicationController
def index
@commentaries = authorize ArtistCommentary.paginated_search(params)
@commentaries = @commentaries.includes(post: :uploader) if request.format.html?
@commentaries = @commentaries.includes(post: [:uploader, :media_asset]) if request.format.html?
respond_with(@commentaries)
end

View File

@@ -4,7 +4,7 @@ class ArtistCommentaryVersionsController < ApplicationController
def index
set_version_comparison
@commentary_versions = ArtistCommentaryVersion.paginated_search(params)
@commentary_versions = @commentary_versions.includes(:updater, post: :uploader) if request.format.html?
@commentary_versions = @commentary_versions.includes(:updater, post: [:uploader, :media_asset]) if request.format.html?
respond_with(@commentary_versions)
end

View File

@@ -3,7 +3,7 @@ class CommentVotesController < ApplicationController
def index
@comment_votes = authorize CommentVote.visible(CurrentUser.user).paginated_search(params, count_pages: true)
@comment_votes = @comment_votes.includes(:user, comment: [:creator, { post: [:uploader] }]) if request.format.html?
@comment_votes = @comment_votes.includes(:user, comment: [:creator, { post: [:uploader, :media_asset] }]) if request.format.html?
respond_with(@comment_votes)
end

View File

@@ -99,7 +99,7 @@ class CommentsController < ApplicationController
@comments = @comments.includes(:creator, :post)
@comments = @comments.select { |comment| comment.post.visible? }
elsif request.format.html?
@comments = @comments.includes(:creator, :updater, post: :uploader)
@comments = @comments.includes(:creator, :updater, post: [:uploader, :media_asset])
@comments = @comments.includes(:votes) if CurrentUser.is_member?
end

View File

@@ -4,7 +4,7 @@ class ModqueueController < ApplicationController
def index
authorize :modqueue
@posts = Post.includes(:appeals, :disapprovals, :uploader, flags: [:creator]).in_modqueue.available_for_moderation(CurrentUser.user, hidden: search_params[:hidden])
@posts = Post.includes(:appeals, :disapprovals, :uploader, :media_asset, flags: [:creator]).in_modqueue.available_for_moderation(CurrentUser.user, hidden: search_params[:hidden])
@modqueue_posts = @posts.reselect(nil).reorder(nil).offset(nil).limit(nil)
@posts = @posts.paginated_search(params, count_pages: true, count: @modqueue_posts.to_a.size, defaults: { order: "modqueue" })

View File

@@ -10,7 +10,7 @@ class PostAppealsController < ApplicationController
@post_appeals = authorize PostAppeal.paginated_search(params)
if request.format.html?
@post_appeals = @post_appeals.includes(:creator, post: [:appeals, :uploader, :approver])
@post_appeals = @post_appeals.includes(:creator, post: [:appeals, :uploader, :approver, :media_asset])
else
@post_appeals = @post_appeals.includes(:post)
end

View File

@@ -9,7 +9,7 @@ class PostApprovalsController < ApplicationController
def index
@post_approvals = authorize PostApproval.paginated_search(params)
@post_approvals = @post_approvals.includes(:user, post: :uploader) if request.format.html?
@post_approvals = @post_approvals.includes(:user, post: [:uploader, :media_asset]) if request.format.html?
respond_with(@post_approvals)
end

View File

@@ -10,7 +10,7 @@ class PostFlagsController < ApplicationController
@post_flags = authorize PostFlag.paginated_search(params)
if request.format.html?
@post_flags = @post_flags.includes(:creator, post: [:flags, :uploader, :approver])
@post_flags = @post_flags.includes(:creator, post: [:flags, :uploader, :approver, :media_asset])
else
@post_flags = @post_flags.includes(:post)
end

View File

@@ -24,7 +24,7 @@ class PostReplacementsController < ApplicationController
def index
params[:search][:post_id] = params.delete(:post_id) if params.key?(:post_id)
@post_replacements = authorize PostReplacement.paginated_search(params)
@post_replacements = @post_replacements.includes(:creator, post: :uploader) if request.format.html?
@post_replacements = @post_replacements.includes(:creator, post: [:uploader, :media_asset]) if request.format.html?
respond_with(@post_replacements)
end

View File

@@ -9,7 +9,7 @@ class PostVersionsController < ApplicationController
@post_versions = authorize PostVersion.paginated_search(params)
if request.format.html?
@post_versions = @post_versions.includes(:updater, post: [:uploader, :versions])
@post_versions = @post_versions.includes(:updater, post: [:uploader, :media_asset, :versions])
else
@post_versions = @post_versions.includes(post: :versions)
end

View File

@@ -3,7 +3,7 @@ class PostVotesController < ApplicationController
def index
@post_votes = authorize PostVote.visible(CurrentUser.user).paginated_search(params, count_pages: true)
@post_votes = @post_votes.includes(:user, post: :uploader) if request.format.html?
@post_votes = @post_votes.includes(:user, post: [:uploader, :media_asset]) if request.format.html?
respond_with(@post_votes)
end

View File

@@ -26,9 +26,11 @@ class PostsController < ApplicationController
include_deleted = @post.is_deleted? || (@post.parent_id.present? && @post.parent.is_deleted?) || CurrentUser.user.show_deleted_children?
@sibling_posts = @post.parent.present? ? @post.parent.children : Post.none
@sibling_posts = @sibling_posts.undeleted unless include_deleted
@sibling_posts = @sibling_posts.includes(:media_asset)
@child_posts = @post.children
@child_posts = @child_posts.undeleted unless include_deleted
@sibling_posts = @sibling_posts.includes(:media_asset)
end
respond_with(@post) do |format|

View File

@@ -26,7 +26,7 @@ class UploadsController < ApplicationController
def index
@uploads = authorize Upload.visible(CurrentUser.user).paginated_search(params, count_pages: true)
@uploads = @uploads.includes(:uploader, post: :uploader) if request.format.html?
@uploads = @uploads.includes(:uploader, post: [:media_asset, :uploader]) if request.format.html?
respond_with(@uploads)
end

View File

@@ -102,6 +102,20 @@ module ApplicationHelper
end
end
def duration_to_hhmmss(seconds)
hh = seconds.div(1.hour).to_s
mm = seconds.div(1.minute).to_s
ss = "%.2d" % (seconds.round % 1.minute)
if seconds >= 1.hour
"#{hh}:#{mm}:#{ss}"
elsif seconds >= 1.second
"#{mm}:#{ss}"
else
"0:01"
end
end
def humanized_number(number)
if number >= 1_000_000
format("%.1fM", number / 1_000_000.0)

View File

@@ -4,6 +4,7 @@ module ComponentsHelper
end
def post_previews_html(posts, **options)
posts = posts.includes(:media_asset) if posts.is_a?(ActiveRecord::Relation)
render PostPreviewComponent.with_collection(posts, **options)
end

View File

@@ -4,9 +4,9 @@ module IconHelper
tag.i(class: "icon #{icon_class} #{klass}", **options)
end
def svg_icon_tag(type, path, class: nil, **options)
def svg_icon_tag(type, path, class: nil, viewbox: "0 0 448 512", **options)
klass = binding.local_variable_get(:class)
tag.svg(class: "icon svg-icon #{type} #{klass}", role: "img", xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 448 512", **options) do
tag.svg(class: "icon svg-icon #{type} #{klass}", role: "img", xmlns: "http://www.w3.org/2000/svg", viewBox: viewbox, **options) do
tag.path(fill: "currentColor", d: path)
end
end
@@ -158,6 +158,11 @@ module IconHelper
icon_tag("fas fa-plus", **options)
end
# https://fontawesome.com/v6.0/icons/volume-high
def sound_icon(**options)
svg_icon_tag("sound-icon", "M412.6 182c-10.28-8.334-25.41-6.867-33.75 3.402c-8.406 10.24-6.906 25.35 3.375 33.74C393.5 228.4 400 241.8 400 255.1c0 14.17-6.5 27.59-17.81 36.83c-10.28 8.396-11.78 23.5-3.375 33.74c4.719 5.806 11.62 8.802 18.56 8.802c5.344 0 10.75-1.779 15.19-5.399C435.1 311.5 448 284.6 448 255.1S435.1 200.4 412.6 182zM473.1 108.2c-10.22-8.334-25.34-6.898-33.78 3.34c-8.406 10.24-6.906 25.35 3.344 33.74C476.6 172.1 496 213.3 496 255.1s-19.44 82.1-53.31 110.7c-10.25 8.396-11.75 23.5-3.344 33.74c4.75 5.775 11.62 8.771 18.56 8.771c5.375 0 10.75-1.779 15.22-5.431C518.2 366.9 544 313 544 255.1S518.2 145 473.1 108.2zM534.4 33.4c-10.22-8.334-25.34-6.867-33.78 3.34c-8.406 10.24-6.906 25.35 3.344 33.74C559.9 116.3 592 183.9 592 255.1s-32.09 139.7-88.06 185.5c-10.25 8.396-11.75 23.5-3.344 33.74C505.3 481 512.2 484 519.2 484c5.375 0 10.75-1.779 15.22-5.431C601.5 423.6 640 342.5 640 255.1S601.5 88.34 534.4 33.4zM301.2 34.98c-11.5-5.181-25.01-3.076-34.43 5.29L131.8 160.1H48c-26.51 0-48 21.48-48 47.96v95.92c0 26.48 21.49 47.96 48 47.96h83.84l134.9 119.8C272.7 477 280.3 479.8 288 479.8c4.438 0 8.959-.9314 13.16-2.835C312.7 471.8 320 460.4 320 447.9V64.12C320 51.55 312.7 40.13 301.2 34.98z", viewbox: "0 0 640 512", **options)
end
def globe_icon(**options)
icon_tag("fas fa-globe", **options)
end

View File

@@ -1,5 +1,5 @@
:root {
--text-xs: 0.8em;
--text-xs: 0.643em; // 9px
--text-sm: 0.9em;
--text-md: 1em;
--text-lg: 1.16667em;
@@ -15,28 +15,6 @@ $h2_padding: 0.8em 0 0.25em 0;
$h3_padding: 0.8em 0 0.25em 0;
$h4_padding: 0.8em 0 0.25em 0;
@mixin animated-icon {
content: "";
position: absolute;
width: 20px;
height: 20px;
color: var(--preview-icon-color);
background: var(--preview-icon-background);
margin: 2px;
text-align: center;
}
@mixin sound-icon {
content: "";
position: absolute;
width: 20px;
height: 20px;
color: var(--preview-icon-color);
background: var(--preview-icon-background);
margin: 2px;
text-align: center;
}
// https://fontawesome.com/how-to-use/on-the-web/advanced/css-pseudo-elements
@mixin fa-solid-icon($content) {
display: inline-block;

View File

@@ -160,7 +160,7 @@ html {
--preview-selected-color: rgba(0, 0, 0, 0.15);
--preview-icon-color: var(--inverse-text-color);
--preview-icon-background: rgba(0, 0, 0, 0.5);
--preview-icon-background: rgba(0, 0, 0, 0.7);
--modqueue-tag-warning-color: var(--red-1);

View File

@@ -17,8 +17,18 @@ $spacer: 0.25rem; /* 4px */
.text-center { text-align: center; }
.text-muted { color: var(--muted-text-color); }
.text-xs { font-size: var(--text-xs); }
.leading-none { line-height: 1; }
.absolute { position: absolute; }
.top-0\.5 { top: 0.5 * $spacer; }
.left-0\.5 { left: 0.5 * $spacer; }
.mx-auto { margin-left: auto; margin-right: auto; }
.mx-2 { margin-left: 2 * $spacer; margin-right: 2 * $spacer; }
.mx-0\.5 { margin-left: 0.5 * $spacer; margin-right: 0.5 * $spacer; }
.mx-2 { margin-left: 2 * $spacer; margin-right: 2 * $spacer; }
.mt-2 { margin-top: 2 * $spacer; }
.mt-4 { margin-top: 4 * $spacer; }
@@ -30,11 +40,13 @@ $spacer: 0.25rem; /* 4px */
.ml-4 { margin-left: 4 * $spacer; }
.p-0\.5 { padding: 0.5 * $spacer; }
.p-4 { padding: 4 * $spacer; }
.w-1\/4 { width: 25%; }
.w-full { width: 100%; }
.h-3 { height: 3 * $spacer; }
.h-10 { height: 10 * $spacer; }
.space-x-1 > * + * { margin-left: 1 * $spacer; }
@@ -45,6 +57,7 @@ $spacer: 0.25rem; /* 4px */
.divide-y-1 > * + * { border-top: 1px solid var(--divider-border-color); }
.align-top { vertical-align: top; }
.align-middle { vertical-align: middle; }
.flex-auto { flex: 1 1 auto; }
.items-center { align-items: center; }

View File

@@ -14,28 +14,6 @@ div#c-comments {
}
}
div.post-preview {
&[data-tags~=animated], &[data-file-ext=swf], &[data-file-ext=webm], &[data-file-ext=zip], &[data-file-ext=mp4] {
div.preview {
position: relative;
&::before {
@include animated-icon;
}
}
}
&[data-has-sound=true] {
div.preview {
position: relative;
&::before {
@include sound-icon;
}
}
}
}
div.post {
display: flex;
margin-bottom: 4em;

View File

@@ -66,7 +66,7 @@ class IqdbClient
def process_results(matches, low_similarity, high_similarity)
matches = matches.select { |result| result["score"] >= low_similarity }
post_ids = matches.map { |match| match["post_id"] }
posts = Post.where(id: post_ids).group_by(&:id).transform_values(&:first)
posts = Post.includes(:media_asset).where(id: post_ids).group_by(&:id).transform_values(&:first)
matches = matches.map do |match|
post = posts.fetch(match["post_id"], nil)

View File

@@ -489,10 +489,10 @@ class PostQueryBuilder
relation
end
def build
def build(includes: nil)
validate!
relation = Post.all
relation = Post.includes(includes)
relation = add_joins(relation)
relation = metatags_match(metatags, relation)
relation = tags_match(tags, relation)
@@ -514,8 +514,8 @@ class PostQueryBuilder
relation
end
def paginated_posts(page, small_search_threshold: Danbooru.config.small_search_threshold.to_i, **options)
posts = build.paginate(page, **options)
def paginated_posts(page, small_search_threshold: Danbooru.config.small_search_threshold.to_i, includes: nil, **options)
posts = build(includes: includes).paginate(page, **options)
posts = optimize_search(posts, small_search_threshold)
posts.load
end

View File

@@ -107,7 +107,7 @@ module PostSets
if is_random?
get_random_posts.paginate(page, search_count: false, limit: per_page, max_limit: max_per_page).load
else
normalized_query.paginated_posts(page, count: post_count, search_count: !post_count.nil?, limit: per_page, max_limit: max_per_page).load
normalized_query.paginated_posts(page, includes: :media_asset, count: post_count, search_count: !post_count.nil?, limit: per_page, max_limit: max_per_page).load
end
end
end

View File

@@ -59,7 +59,7 @@ module RecommenderService
# Process a set of recommendations to filter out posts the user uploaded
# themselves, or has already favorited, or that don't match a tag search.
def process_recs(recs, post: nil, uploader: nil, favoriter: nil, tags: nil)
posts = Post.where(id: recs.map(&:first))
posts = Post.includes(:media_asset).where(id: recs.map(&:first))
posts = posts.where.not(id: post.id) if post
posts = posts.where.not(uploader_id: uploader.id) if uploader
posts = posts.where.not(id: favoriter.favorites.select(:post_id)) if favoriter

View File

@@ -37,7 +37,7 @@ class Post < ApplicationRecord
belongs_to :approver, class_name: "User", optional: true
belongs_to :uploader, :class_name => "User", :counter_cache => "post_upload_count"
belongs_to :parent, class_name: "Post", optional: true
has_one :media_asset, foreign_key: :md5, primary_key: :md5
has_one :media_asset, -> { active }, foreign_key: :md5, primary_key: :md5
has_one :upload, :dependent => :destroy
has_one :artist_commentary, :dependent => :destroy
has_one :pixiv_ugoira_frame_data, class_name: "PixivUgoiraFrameData", foreign_key: :md5, primary_key: :md5

View File

@@ -6,7 +6,7 @@
<%= render "posts/partials/common/inline_blacklist" %>
<ul id="sortable">
<% @favorite_group.posts.limit(1_000).each do |post| %>
<% @favorite_group.posts.includes(:media_asset).limit(1_000).each do |post| %>
<%= tag.li id: "favorite_group[post_ids]_#{post.id}" do -%>
<%= post_preview(post, show_deleted: true).presence || "Hidden: Post ##{post.id}" -%>
<% end -%>

View File

@@ -12,7 +12,7 @@
<%= render "posts/partials/common/inline_blacklist" %>
<ul id="sortable">
<% @pool.posts.each do |post| %>
<% @pool.posts.includes(:media_asset).each do |post| %>
<%= tag.li id: "pool[post_ids]_#{post.id}" do -%>
<%= post_preview(post, show_deleted: true).presence || "Hidden: Post ##{post.id}" -%>
<% end -%>