diff --git a/app/components/media_asset_gallery_component.rb b/app/components/media_asset_gallery_component.rb
new file mode 100644
index 000000000..b3d7c27ec
--- /dev/null
+++ b/app/components/media_asset_gallery_component.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class MediaAssetGalleryComponent < ApplicationComponent
+ DEFAULT_SIZE = 180
+
+ attr_reader :inline, :size, :options
+
+ renders_many :media_assets, MediaAssetPreviewComponent
+ renders_one :footer
+
+ def initialize(inline: false, size: DEFAULT_SIZE, **options)
+ super
+ @inline = inline
+ @size = size
+ @option = options
+ end
+end
diff --git a/app/components/media_asset_gallery_component/media_asset_gallery_component.html.erb b/app/components/media_asset_gallery_component/media_asset_gallery_component.html.erb
new file mode 100644
index 000000000..b94be1dc5
--- /dev/null
+++ b/app/components/media_asset_gallery_component/media_asset_gallery_component.html.erb
@@ -0,0 +1,13 @@
+
diff --git a/app/components/media_asset_gallery_component/media_asset_gallery_component.scss b/app/components/media_asset_gallery_component/media_asset_gallery_component.scss
new file mode 100644
index 000000000..267dde096
--- /dev/null
+++ b/app/components/media_asset_gallery_component/media_asset_gallery_component.scss
@@ -0,0 +1,34 @@
+.media-asset-gallery {
+ .media-assets-container {
+ gap: 0.5rem;
+ }
+
+ @media screen and (min-width: 660px) {
+ &.media-asset-gallery-150 .post-preview-container { height: 150px; }
+ &.media-asset-gallery-180 .post-preview-container { height: 180px; }
+ &.media-asset-gallery-225 .post-preview-container { height: 225px; }
+ &.media-asset-gallery-270 .post-preview-container { height: 270px; }
+ &.media-asset-gallery-360 .post-preview-container { height: 360px; }
+ &.media-asset-gallery-720 .post-preview-container { height: 720px; }
+ }
+
+ &.media-asset-gallery-150 .media-assets-container { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
+ &.media-asset-gallery-180 .media-assets-container { grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); }
+ &.media-asset-gallery-225 .media-assets-container { grid-template-columns: repeat(auto-fill, minmax(225px, 1fr)); }
+ &.media-asset-gallery-270 .media-assets-container { grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); }
+ &.media-asset-gallery-360 .media-assets-container { grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); }
+ &.media-asset-gallery-720 .media-assets-container { grid-template-columns: repeat(auto-fill, minmax(720px, 1fr)); }
+
+ @media screen and (max-width: 660px) {
+ .media-assets-container {
+ gap: 0.25rem;
+ }
+
+ &.media-asset-gallery-150 .media-assets-container { grid-template-columns: repeat(3, minmax(0, 150px)); }
+ &.media-asset-gallery-180 .media-assets-container { grid-template-columns: repeat(2, auto); }
+ &.media-asset-gallery-225 .media-assets-container { grid-template-columns: repeat(2, auto); }
+ &.media-asset-gallery-270 .media-assets-container { grid-template-columns: repeat(2, auto); }
+ &.media-asset-gallery-360 .media-assets-container { grid-template-columns: repeat(1, auto); }
+ &.media-asset-gallery-720 .media-assets-container { grid-template-columns: repeat(1, auto); }
+ }
+}
diff --git a/app/components/media_asset_preview_component.rb b/app/components/media_asset_preview_component.rb
index 3bb91e2d2..243b76f48 100644
--- a/app/components/media_asset_preview_component.rb
+++ b/app/components/media_asset_preview_component.rb
@@ -8,6 +8,8 @@ class MediaAssetPreviewComponent < ApplicationComponent
attr_reader :media_asset, :size, :fit, :link_target, :shrink_to_fit, :save_data
delegate :duration_to_hhmmss, :sound_icon, to: :helpers
+ renders_one :footer
+
# @param media_asset [MediaAsset] The media asset to show the thumbnail for.
# @param size [String] The size of the thumbnail. One of 150, 180, 225, 270, or 360.
# @param link_target [ApplicationRecord] What the thumbnail links to (default: the media asset).
diff --git a/app/components/media_asset_preview_component/media_asset_preview_component.html.erb b/app/components/media_asset_preview_component/media_asset_preview_component.html.erb
index cf5c56820..b13dd9777 100644
--- a/app/components/media_asset_preview_component/media_asset_preview_component.html.erb
+++ b/app/components/media_asset_preview_component/media_asset_preview_component.html.erb
@@ -1,5 +1,5 @@
<%= tag.article class: "media-asset-preview media-asset-preview-#{size}" do -%>
- <%= link_to link_target, class: "inline-block relative", draggable: "false" do -%>
+ <%= link_to link_target, class: "inline-block relative flex justify-center", draggable: "false" do -%>
<% if media_asset.is_animated? %>
@@ -21,4 +21,8 @@
<%= tag.img src: variant.file_url, width: variant.width, height: variant.height, class: "media-asset-preview-image w-auto h-auto max-h-#{size}px #{"max-w-full" if shrink_to_fit}", crossorigin: "anonymous", draggable: "false" -%>
<% end %>
+
+
+ <%= footer %>
+
<% end %>
diff --git a/app/javascript/src/styles/common/utilities.scss b/app/javascript/src/styles/common/utilities.scss
index 385d394db..8aaa0b5be 100644
--- a/app/javascript/src/styles/common/utilities.scss
+++ b/app/javascript/src/styles/common/utilities.scss
@@ -113,7 +113,9 @@ $spacer: 0.25rem; /* 4px */
.h-auto { height: auto; }
.h-1 { height: 1 * $spacer; }
.h-3 { height: 3 * $spacer; }
+.h-8 { height: 8 * $spacer; }
.h-10 { height: 10 * $spacer; }
+.h-12 { height: 12 * $spacer; }
.max-h-150px { max-height: 150px; }
.max-h-180px { max-height: 180px; }
@@ -143,7 +145,12 @@ $spacer: 0.25rem; /* 4px */
.flex-initial { flex: 0 1 auto; }
.flex-grow-1 { flex-grow: 1; }
.flex-col { flex-direction: column; }
+
+.items-start { align-items: flex-start; }
.items-center { align-items: center; }
+.items-end { align-items: flex-end; }
+
+.justify-items-center { justify-items: center; }
.justify-center { justify-content: center; }
.float-right { float: right; }
diff --git a/app/models/media_asset.rb b/app/models/media_asset.rb
index a2703832f..b0f602a35 100644
--- a/app/models/media_asset.rb
+++ b/app/models/media_asset.rb
@@ -15,6 +15,9 @@ class MediaAsset < ApplicationRecord
has_one :post, foreign_key: :md5, primary_key: :md5
has_one :media_metadata, dependent: :destroy
has_one :pixiv_ugoira_frame_data, class_name: "PixivUgoiraFrameData", foreign_key: :md5, primary_key: :md5
+ has_many :upload_media_assets, dependent: :destroy
+ has_many :uploads, through: :upload_media_assets
+ has_many :uploaders, through: :uploads, class_name: "User", foreign_key: :uploader_id
delegate :metadata, to: :media_metadata
delegate :is_non_repeating_animation?, :is_greyscale?, :is_rotated?, to: :metadata
diff --git a/app/views/iqdb_queries/show.html.erb b/app/views/iqdb_queries/show.html.erb
index 00f4ade6c..3c1c848d1 100644
--- a/app/views/iqdb_queries/show.html.erb
+++ b/app/views/iqdb_queries/show.html.erb
@@ -3,7 +3,7 @@
-
Similar Images Search
+
Reverse Images Search
Paste a URL or upload a file to perform a reverse image search on <%= Danbooru.config.app_name %>.
@@ -19,3 +19,5 @@
<%= render "iqdb_queries/matches" %>
+
+<%= render "uploads/secondary_links" %>
diff --git a/app/views/media_assets/_gallery.html.erb b/app/views/media_assets/_gallery.html.erb
new file mode 100644
index 000000000..422338953
--- /dev/null
+++ b/app/views/media_assets/_gallery.html.erb
@@ -0,0 +1,15 @@
+<%= render(MediaAssetGalleryComponent.new) do |gallery| %>
+ <% @media_assets.each do |media_asset| %>
+ <% if policy(media_asset).can_see_image? %>
+ <% gallery.media_asset(media_asset: media_asset, size: gallery.size, link_target: media_asset) do |preview| %>
+ <% preview.footer do %>
+
+ <% if media_asset.post.present? %>
+ <%= link_to "post ##{media_asset.post.id}", media_asset.post %>
+ <% end %>
+
+ <% end %>
+ <% end %>
+ <% end %>
+ <% end %>
+<% end %>
diff --git a/app/views/media_assets/_table.html.erb b/app/views/media_assets/_table.html.erb
new file mode 100644
index 000000000..eb444b80b
--- /dev/null
+++ b/app/views/media_assets/_table.html.erb
@@ -0,0 +1,20 @@
+<%= table_for @media_assets, class: "striped autofit" do |t| %>
+ <% t.column "File", td: { class: "text-center" } do |media_asset| %>
+ <% if policy(media_asset).can_see_image? %>
+ <%= render MediaAssetPreviewComponent.new(media_asset: media_asset, save_data: CurrentUser.save_data, shrink_to_fit: false) %>
+ <% end %>
+ <% end %>
+
+ <% t.column :image_width %>
+ <% t.column :image_height %>
+ <% t.column :file_size %>
+ <% t.column :file_ext %>
+
+ <% t.column "Metadata" do |media_asset| %>
+ <%= link_to pluralize(media_asset.metadata.size, "tags"), media_asset %>
+ <% end %>
+
+ <% t.column "Created" do |media_asset| %>
+ <%= time_ago_in_words_tagged(media_asset.created_at) %>
+ <% end %>
+<% end %>
diff --git a/app/views/media_assets/index.html.erb b/app/views/media_assets/index.html.erb
index 4fe1239e2..508ffb8a3 100644
--- a/app/views/media_assets/index.html.erb
+++ b/app/views/media_assets/index.html.erb
@@ -1,42 +1,27 @@
+
+<%= render "uploads/secondary_links" %>
diff --git a/app/views/media_assets/show.html.erb b/app/views/media_assets/show.html.erb
index 956937475..98a7a6516 100644
--- a/app/views/media_assets/show.html.erb
+++ b/app/views/media_assets/show.html.erb
@@ -30,3 +30,5 @@
+
+<%= render "uploads/secondary_links" %>
diff --git a/app/views/uploads/_secondary_links.html.erb b/app/views/uploads/_secondary_links.html.erb
index 34d793277..f20f86e4e 100644
--- a/app/views/uploads/_secondary_links.html.erb
+++ b/app/views/uploads/_secondary_links.html.erb
@@ -1,7 +1,8 @@
<% content_for(:secondary_links) do %>
- <%= subnav_link_to "My Uploads", uploads_path %>
<%= subnav_link_to "New Upload", new_upload_path %>
- <%= subnav_link_to "Batch Upload", batch_uploads_path %>
- <%= subnav_link_to "Reverse Image Search", iqdb_queries_path %>
+ <%= subnav_link_to "Batch Upload", batch_uploads_path %> |
+ <%= subnav_link_to "My Uploads", uploads_path %>
+ <%= subnav_link_to "All Uploads", media_assets_path %>
+ <%= subnav_link_to "Reverse Image Search", iqdb_queries_path %> |
<%= subnav_link_to "Help", wiki_page_path("help:upload") %>
<% end %>