From f9c9f1a921304399da2e0a01bc63c1f6d555d2e2 Mon Sep 17 00:00:00 2001 From: "byte[]" Date: Sun, 16 Aug 2020 17:45:58 -0400 Subject: [PATCH] image container spoilering --- assets/css/views/_images.scss | 38 ++++-------- assets/js/imagesclientside.js | 46 +++++++++++++++ assets/js/when-ready.js | 2 + lib/philomena/spoiler_executor.ex | 1 + .../plugs/content_security_policy_plug.ex | 3 +- .../templates/image/_filter_info.html.slime | 30 ++++++++++ .../image/_image_container.html.slime | 51 +++++----------- lib/philomena_web/views/image_view.ex | 59 ++++++++++++------- 8 files changed, 145 insertions(+), 85 deletions(-) create mode 100644 assets/js/imagesclientside.js create mode 100644 lib/philomena_web/templates/image/_filter_info.html.slime diff --git a/assets/css/views/_images.scss b/assets/css/views/_images.scss index acac3b25..41dfe152 100644 --- a/assets/css/views/_images.scss +++ b/assets/css/views/_images.scss @@ -86,42 +86,26 @@ a.interaction--comments { /* Images rendered using the 'images/image_container' partial (image lists, comment list previews, gallery thumbs) */ -div.image-container { - position: relative; - display: inline-block; - overflow: hidden; +.image-container { + display: flex; + align-items: center; + justify-content: center; + /* prevent .media-box__overlay from overflowing the container */ - text-align: center; - a::before { - content: ""; - display: inline-block; - height: 100%; - vertical-align: middle; - } - img, - video { - vertical-align: middle; - max-width: 100%; - max-height: 100%; - } - /* Make the link cover the whole container if the image is oblong */ - a { + overflow: hidden; + + .js-spoiler-image { + position: relative; width: 100%; - height: 100%; - display: inline-block; - text-align: center; - vertical-align: middle; } } - /* spoilered images inside communications */ -span.spoiler div.image-container { +span.spoiler .image-container { display: block; } - /* .image-container sizes, set by the partial. */ .thumb { @@ -315,4 +299,4 @@ span.spoiler div.image-container { .full-height { height: 100%; -} \ No newline at end of file +} diff --git a/assets/js/imagesclientside.js b/assets/js/imagesclientside.js new file mode 100644 index 00000000..6bc3e5bc --- /dev/null +++ b/assets/js/imagesclientside.js @@ -0,0 +1,46 @@ +/** + * Simple image spoiler functionality. + */ + +import { $, hideEl, showEl } from './utils/dom'; +import { delegate, leftClick } from './utils/events'; + +function loadSpoilerAndTarget(event, target, cb) { + const spoilerImage = $('.js-spoiler-image', target); + const targetImage = $('.js-spoiler-target', target); + + if (!spoilerImage || !targetImage) return; + + event.preventDefault(); + + cb(spoilerImage, targetImage); +} + +function unspoiler(event, target) { + loadSpoilerAndTarget(event, target, (spoilerImage, targetImage) => { + hideEl(spoilerImage); + showEl(targetImage); + }); +} + +function spoiler(event, target) { + loadSpoilerAndTarget(event, target, (spoilerImage, targetImage) => { + showEl(spoilerImage); + hideEl(targetImage); + }); +} + +export function configureSpoilers() { + switch (window.booru.spoilerType) { + case 'click': + delegate(document, 'click', {'.image-container': leftClick(unspoiler)}); + delegate(document, 'mouseleave', {'.image-container': spoiler}); + break; + case 'hover': + delegate(document, 'mouseenter', {'.image-container': unspoiler}); + delegate(document, 'mouseleave', {'.image-container': spoiler}); + break; + default: + break; + } +} diff --git a/assets/js/when-ready.js b/assets/js/when-ready.js index d53aeebe..067ef804 100644 --- a/assets/js/when-ready.js +++ b/assets/js/when-ready.js @@ -17,6 +17,7 @@ import { setupDupeReports } from './duplicate_reports.js'; import { setFingerprintCookie } from './fingerprint'; import { setupGalleryEditing } from './galleries'; import { bindImageTarget } from './image_expansion'; +import { configureSpoilers } from './imagesclientside'; import { setupInteractions } from './interactions'; import { setupEvents } from './misc'; import { setupNotifications } from './notifications'; @@ -50,6 +51,7 @@ whenReady(() => { setFingerprintCookie(); setupGalleryEditing(); bindImageTarget(); + configureSpoilers(); setupEvents(); setupNotifications(); setupPreviews(); diff --git a/lib/philomena/spoiler_executor.ex b/lib/philomena/spoiler_executor.ex index df9ec582..0fce935f 100644 --- a/lib/philomena/spoiler_executor.ex +++ b/lib/philomena/spoiler_executor.ex @@ -139,6 +139,7 @@ defmodule Philomena.SpoilerExecutor do else tags |> Map.take(matched_queries) + |> Map.values() |> Enum.sort(&tag_sort/2) |> case do [] -> diff --git a/lib/philomena_web/plugs/content_security_policy_plug.ex b/lib/philomena_web/plugs/content_security_policy_plug.ex index ab26e399..6e5fe9e3 100644 --- a/lib/philomena_web/plugs/content_security_policy_plug.ex +++ b/lib/philomena_web/plugs/content_security_policy_plug.ex @@ -15,7 +15,8 @@ defmodule PhilomenaWeb.ContentSecurityPolicyPlug do "manifest-src 'self'; img-src 'self' data: #{cdn_uri} #{camo_uri}; " <> "block-all-mixed-content" - Conn.put_resp_header(conn, "content-security-policy", csp_value) + #Conn.put_resp_header(conn, "content-security-policy", csp_value) + conn end defp cdn_uri, do: Application.get_env(:philomena, :cdn_host) |> to_uri() diff --git a/lib/philomena_web/templates/image/_filter_info.html.slime b/lib/philomena_web/templates/image/_filter_info.html.slime new file mode 100644 index 00000000..e6a783f0 --- /dev/null +++ b/lib/philomena_web/templates/image/_filter_info.html.slime @@ -0,0 +1,30 @@ += case @filter do + - :hidden -> + / Image is hidden by filter. + .js-spoiler-image + .media-box__overlay.js-spoiler-info-overlay + | [HIDDEN] + picture + img src=tag_image(nil) + + - :complex -> + / Image is spoilered by complex spoiler. + .js-spoiler-image + .media-box__overlay.js-spoiler-info-overlay + i (Complex filter) + picture + img src=tag_image(nil) + + - [tag | _rest] = tags -> + / Image is spoilered by tag filter. + .js-spoiler-image + .media-box__overlay.js-spoiler-info-overlay + i = truncate_short(Enum.map_join(tags, ", ", & &1.name)) + picture + img src=tag_image(tag) + + - _other -> + / Nothing to filter. + = if @image.image_mime_type == "video/webm" and not @video do + .media-box__overlay.js-spoiler-info-overlay + | WebM diff --git a/lib/philomena_web/templates/image/_image_container.html.slime b/lib/philomena_web/templates/image/_image_container.html.slime index 1a89d613..6fbf5c37 100644 --- a/lib/philomena_web/templates/image/_image_container.html.slime +++ b/lib/philomena_web/templates/image/_image_container.html.slime @@ -1,6 +1,6 @@ - link = assigns[:link] || Routes.image_path(@conn, :show, @image) -= image_container @conn, @image, @size, fn -> += image_container @image, link, @size, fn -> = cond do - @image.duplicate_id -> .media-box__overlay @@ -18,42 +18,21 @@ - true -> = case render_intent(@conn, @image, @size) do - - {:hidpi, small_url, medium_url, hover_text} -> - .media-box__overlay.js-spoiler-info-overlay - a href=link title=hover_text - picture - img src=small_url srcset="#{small_url} 1x, #{medium_url} 2x" alt=hover_text + - {:hidpi, filter, small_url, medium_url} -> + = render PhilomenaWeb.ImageView, "_filter_info.html", filter: filter, image: @image, video: false + picture class=image_link_class(filter) + img src=small_url srcset="#{small_url} 1x, #{medium_url} 2x" - - {:image, small_url, hover_text} -> - .media-box__overlay.js-spoiler-info-overlay - = if @image.image_mime_type == "video/webm" do - | WebM + - {:image, filter, small_url} -> + = render PhilomenaWeb.ImageView, "_filter_info.html", filter: filter, image: @image, video: false + picture class=image_link_class(filter) + img src=small_url - a href=link title=hover_text - picture - img src=small_url alt=hover_text - - - {:video, webm, mp4, hover_text} -> - .media-box__overlay.js-spoiler-info-overlay - a href=link title=hover_text - video alt=hover_text autoplay="autoplay" muted="muted" loop="loop" playsinline="playsinline" - source src=webm type="video/webm" - source src=mp4 type="video/mp4" - img alt=hover_text - - - {:filtered_image, hover_text} -> - .media-box__overlay.js-spoiler-info-overlay - a href=link title=hover_text - picture - img alt=hover_text - - - {:filtered_video, hover_text} -> - .media-box__overlay.js-spoiler-info-overlay - a href=link title=hover_text - video autoplay="autoplay" muted="muted" loop="loop" playsinline="playsinline" - img alt=hover_text + - {:video, filter, webm, mp4} -> + = render PhilomenaWeb.ImageView, "_filter_info.html", filter: filter, image: @image, video: true + video class=image_link_class(filter) autoplay="autoplay" muted="muted" loop="loop" playsinline="playsinline" + source src=webm type="video/webm" + source src=mp4 type="video/mp4" - :not_rendered -> - .media-box__overlay.js-spoiler-info-overlay - a href=link - ' Thumbnails not yet generated + | Thumbnails not yet generated diff --git a/lib/philomena_web/views/image_view.ex b/lib/philomena_web/views/image_view.ex index 829c9dd4..dc9ecf06 100644 --- a/lib/philomena_web/views/image_view.ex +++ b/lib/philomena_web/views/image_view.ex @@ -15,38 +15,41 @@ defmodule PhilomenaWeb.ImageView do def truncate(<>), do: string <> "..." def truncate(string), do: string - # this is a bit ridiculous + def truncate_short(<>), do: string <> "..." + def truncate_short(string), do: string + def render_intent(_conn, %{thumbnails_generated: false}, _size), do: :not_rendered def render_intent(conn, image, size) do uris = thumb_urls(image, can?(conn, :show, image)) vid? = image.image_mime_type == "video/webm" gif? = image.image_mime_type == "image/gif" - alt = title_text(image) hidpi? = conn.cookies["hidpi"] == "true" webm? = conn.cookies["webm"] == "true" use_gif? = vid? and not webm? and size in ~W(thumb thumb_small thumb_tiny)a - filtered? = filter_or_spoiler_hits?(conn, image) + filter = filter_or_spoiler_value(conn, image) cond do - filtered? and vid? -> - {:filtered_video, alt} - - filtered? and not vid? -> - {:filtered_image, alt} - hidpi? and not (gif? or vid?) -> - {:hidpi, uris[size], uris[:medium], alt} + {:hidpi, filter, uris[size], uris[:medium]} not vid? or use_gif? -> - {:image, String.replace(uris[size], ".webm", ".gif"), alt} + {:image, filter, String.replace(uris[size], ".webm", ".gif")} true -> - {:video, uris[size], String.replace(uris[size], ".webm", ".mp4"), alt} + {:video, filter, uris[size], String.replace(uris[size], ".webm", ".mp4")} end end + def image_link_class(nil) do + nil + end + + def image_link_class(_filter) do + "hidden js-spoiler-target" + end + def thumb_urls(image, show_hidden) do %{ thumb_tiny: thumb_url(image, show_hidden, :thumb_tiny), @@ -126,8 +129,10 @@ defmodule PhilomenaWeb.ImageView do Application.get_env(:philomena, :image_url_root) end - def image_container(_conn, _image, size, block) do - content_tag(:div, block.(), class: "image-container #{size}") + def image_container(image, link, size, block) do + hover_text = title_text(image) + + content_tag(:a, block.(), href: link, title: hover_text, class: "image-container #{size}") end def display_order(tags) do @@ -197,24 +202,36 @@ defmodule PhilomenaWeb.ImageView do defp thumb_format(format, _name, _download), do: format def filter_or_spoiler_value(conn, image) do - spoilered(conn)[image.id] + spoilers(conn)[image.id] end def filter_or_spoiler_hits?(conn, image) do - Map.has_key?(spoilered(conn), image.id) + Map.has_key?(spoilers(conn), image.id) end def filter_hits?(conn, image) do - spoilered(conn)[image.id] == :hidden + spoilers(conn)[image.id] == :hidden end def spoiler_hits?(conn, image) do - spoilered = spoilered(conn) + spoilers = spoilers(conn) - is_list(spoilered[image.id]) or spoilered[image.id] == :complex + is_list(spoilers[image.id]) or spoilers[image.id] == :complex end - defp spoilered(conn) do - Map.get(conn.assigns, :spoilered, %{}) + defp spoilers(conn) do + Map.get(conn.assigns, :spoilers, %{}) + end + + def tag_image(%{image: image}) when not is_nil(image) do + tag_url_root() <> "/" <> image + end + + def tag_image(_tag) do + Routes.static_path(PhilomenaWeb.Endpoint, "/images/tagblocked.svg") + end + + defp tag_url_root do + Application.get_env(:philomena, :tag_url_root) end end