mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-11-27 13:47:58 +01:00
add image navigation
This commit is contained in:
parent
e91154e3c3
commit
d94a09c2d9
14 changed files with 396 additions and 81 deletions
171
lib/philomena/image_navigator.ex
Normal file
171
lib/philomena/image_navigator.ex
Normal file
|
@ -0,0 +1,171 @@
|
|||
defmodule Philomena.ImageNavigator do
|
||||
alias Philomena.ImageSorter
|
||||
alias Philomena.Images.{Image, Elasticsearch}
|
||||
alias Philomena.Repo
|
||||
import Ecto.Query
|
||||
|
||||
# We get consecutive images by finding all images greater than or less than
|
||||
# the current image, and grabbing the FIRST one
|
||||
@range_comparison_for_order %{
|
||||
asc: :gt,
|
||||
desc: :lt
|
||||
}
|
||||
|
||||
# If we didn't reverse for prev, it would be the LAST image, which would
|
||||
# make Elasticsearch choke on deep pagination
|
||||
@order_for_dir %{
|
||||
next: %{"asc" => :asc, "desc" => :desc},
|
||||
prev: %{"asc" => :desc, "desc" => :asc}
|
||||
}
|
||||
|
||||
@range_map %{
|
||||
gt: :gte,
|
||||
lt: :lte
|
||||
}
|
||||
|
||||
def find_consecutive(image, rel, params, compiled_query, compiled_filter) do
|
||||
image_index =
|
||||
Image
|
||||
|> where(id: ^image.id)
|
||||
|> preload(:gallery_interactions)
|
||||
|> Repo.one()
|
||||
|> Map.merge(empty_fields())
|
||||
|> Elasticsearch.as_json()
|
||||
|
||||
sort_data = ImageSorter.parse_sort(params)
|
||||
|
||||
{sorts, filters} =
|
||||
sort_data.sorts
|
||||
|> Enum.map(&extract_filters(&1, image_index, rel))
|
||||
|> Enum.unzip()
|
||||
|
||||
sorts = sortify(sorts, image_index)
|
||||
filters = filterify(filters, image_index)
|
||||
|
||||
Image.search_records(
|
||||
%{
|
||||
query: %{
|
||||
bool: %{
|
||||
must: List.flatten([compiled_query, sort_data.queries, filters]),
|
||||
must_not: [
|
||||
compiled_filter,
|
||||
%{term: %{hidden_from_users: true}}
|
||||
]
|
||||
}
|
||||
},
|
||||
sort: List.flatten(sorts)
|
||||
},
|
||||
%{page_size: 1}
|
||||
)
|
||||
|> Enum.to_list()
|
||||
|> case do
|
||||
[] -> image
|
||||
[next_image] -> next_image
|
||||
end
|
||||
end
|
||||
|
||||
defp extract_filters(%{"galleries.position" => term} = sort, image, rel) do
|
||||
# Extract gallery ID and current position
|
||||
gid = term["nested_filter"]["term"]["galleries.id"]
|
||||
pos = Enum.find(image[:galleries], & &1.id == gid).position
|
||||
|
||||
# Sort in the other direction if we are going backwards
|
||||
sd = term["order"]
|
||||
order = @order_for_dir[rel][sd]
|
||||
term = %{term | "order" => order}
|
||||
sort = %{sort | "galleries.position" => term}
|
||||
|
||||
filter = gallery_range_filter(@range_comparison_for_order[order], pos)
|
||||
|
||||
{[sort], [filter]}
|
||||
end
|
||||
|
||||
defp extract_filters(sort, image, rel) do
|
||||
[{sf, sd}] = Enum.to_list(sort)
|
||||
order = @order_for_dir[rel][sd]
|
||||
sort = %{sort | sf => order}
|
||||
|
||||
field = String.to_existing_atom(sf)
|
||||
filter = range_filter(sf, @range_comparison_for_order[order], image[field])
|
||||
|
||||
cond do
|
||||
sf in [:_random, :_score] ->
|
||||
{[sort], []}
|
||||
|
||||
true ->
|
||||
{[sort], [filter]}
|
||||
end
|
||||
end
|
||||
|
||||
defp sortify(sorts, _image) do
|
||||
List.flatten(sorts)
|
||||
end
|
||||
|
||||
defp filterify(filters, image) do
|
||||
filters = List.flatten(filters)
|
||||
|
||||
filters =
|
||||
filters
|
||||
|> Enum.with_index()
|
||||
|> Enum.map(fn
|
||||
{filter, 0} -> filter.this
|
||||
{filter, i} ->
|
||||
filters_so_far =
|
||||
filters
|
||||
|> Enum.take(i)
|
||||
|> Enum.map(& &1.for_next)
|
||||
|
||||
%{
|
||||
bool: %{
|
||||
must: [filter.this | filters_so_far]
|
||||
}
|
||||
}
|
||||
end)
|
||||
|
||||
%{
|
||||
bool: %{
|
||||
should: filters,
|
||||
must_not: %{term: %{id: image.id}}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp range_filter(sf, dir, val) do
|
||||
%{
|
||||
this: %{range: %{sf => %{dir => parse_val(val)}}},
|
||||
next: %{range: %{sf => %{@range_map[dir] => parse_val(val)}}}
|
||||
}
|
||||
end
|
||||
|
||||
defp gallery_range_filter(dir, val) do
|
||||
%{
|
||||
this: %{
|
||||
nested: %{
|
||||
path: :galleries,
|
||||
query: %{range: %{"galleries.position" => %{dir => val}}}
|
||||
}
|
||||
},
|
||||
next: %{
|
||||
nested: %{
|
||||
path: :galleries,
|
||||
query: %{range: %{"galleries.position" => %{@range_map[dir] => val}}}
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp empty_fields do
|
||||
%{
|
||||
user: nil,
|
||||
deleter: nil,
|
||||
upvoters: [],
|
||||
downvoters: [],
|
||||
favers: [],
|
||||
hiders: [],
|
||||
tags: []
|
||||
}
|
||||
end
|
||||
|
||||
defp parse_val(%NaiveDateTime{} = value), do: NaiveDateTime.to_iso8601(value)
|
||||
defp parse_val(value), do: value
|
||||
end
|
16
lib/philomena/image_scope.ex
Normal file
16
lib/philomena/image_scope.ex
Normal file
|
@ -0,0 +1,16 @@
|
|||
defmodule Philomena.ImageScope do
|
||||
def scope(conn) do
|
||||
[]
|
||||
|> scope(conn, "q")
|
||||
|> scope(conn, "sf")
|
||||
|> scope(conn, "sd")
|
||||
end
|
||||
|
||||
defp scope(list, conn, key) do
|
||||
case conn.params[key] do
|
||||
nil -> list
|
||||
"" -> list
|
||||
val -> [{key, val} | list]
|
||||
end
|
||||
end
|
||||
end
|
70
lib/philomena/image_sorter.ex
Normal file
70
lib/philomena/image_sorter.ex
Normal file
|
@ -0,0 +1,70 @@
|
|||
defmodule Philomena.ImageSorter do
|
||||
def parse_sort(params) do
|
||||
sd = parse_sd(params)
|
||||
|
||||
parse_sf(params, sd)
|
||||
end
|
||||
|
||||
defp parse_sd(%{"sd" => sd}) when sd in ~W(asc desc), do: sd
|
||||
defp parse_sd(_params), do: "desc"
|
||||
|
||||
defp parse_sf(%{"sf" => sf}, sd) when
|
||||
sf in ~W(created_at updated_at first_seen_at width height score comment_count tag_count wilson_score _score)
|
||||
do
|
||||
%{queries: [], sorts: [%{sf => sd}]}
|
||||
end
|
||||
|
||||
defp parse_sf(%{"sf" => "random"}, sd) do
|
||||
random_query(:rand.uniform(4_294_967_296), sd)
|
||||
end
|
||||
|
||||
defp parse_sf(%{"sf" => <<"random:", seed::binary>>}, sd) do
|
||||
case Integer.parse(seed) do
|
||||
{seed, _rest} ->
|
||||
random_query(seed, sd)
|
||||
|
||||
_ ->
|
||||
random_query(:rand.uniform(4_294_967_296), sd)
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_sf(%{"sf" => <<"gallery_id:", gallery::binary>>}, sd) do
|
||||
case Integer.parse(gallery) do
|
||||
{gallery, _rest} ->
|
||||
%{
|
||||
queries: [],
|
||||
sorts: [%{
|
||||
"galleries.position" => %{
|
||||
order: sd,
|
||||
nested_path: :galleries,
|
||||
nested_filter: %{
|
||||
term: %{
|
||||
"galleries.id": gallery
|
||||
}
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
_ ->
|
||||
%{queries: [%{match_none: %{}}], sorts: []}
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_sf(_params, sd) do
|
||||
%{queries: [], sorts: [%{"created_at" => sd}]}
|
||||
end
|
||||
|
||||
defp random_query(seed, sd) do
|
||||
%{
|
||||
queries: [%{
|
||||
function_score: %{
|
||||
query: %{match_all: %{}},
|
||||
random_score: %{seed: seed},
|
||||
boost_mode: :replace
|
||||
}
|
||||
}],
|
||||
sorts: [%{"_score" => sd}]
|
||||
}
|
||||
end
|
||||
end
|
69
lib/philomena_web/controllers/image/navigate_controller.ex
Normal file
69
lib/philomena_web/controllers/image/navigate_controller.ex
Normal file
|
@ -0,0 +1,69 @@
|
|||
defmodule PhilomenaWeb.Image.NavigateController do
|
||||
use PhilomenaWeb, :controller
|
||||
|
||||
alias Philomena.Images.Image
|
||||
alias Philomena.Images.Query
|
||||
alias Philomena.ImageNavigator
|
||||
|
||||
plug PhilomenaWeb.CanaryMapPlug, index: :show
|
||||
plug :load_and_authorize_resource, model: Image, id_name: "image_id", persisted: true
|
||||
|
||||
def index(conn, %{"rel" => rel} = params) when rel in ~W(prev next) do
|
||||
image = conn.assigns.image
|
||||
filter = conn.assigns.compiled_filter
|
||||
rel = String.to_existing_atom(rel)
|
||||
|
||||
next_image = ImageNavigator.find_consecutive(image, rel, params, compile_query(conn), filter)
|
||||
scope = Philomena.ImageScope.scope(conn)
|
||||
|
||||
conn
|
||||
|> redirect(to: Routes.image_path(conn, :show, next_image, scope))
|
||||
end
|
||||
|
||||
def index(conn, %{"rel" => "find"}) do
|
||||
image = conn.assigns.image
|
||||
filter = conn.assigns.compiled_filter
|
||||
pagination = conn.assigns.pagination
|
||||
|
||||
# Global find does not use the current search scope.
|
||||
resp =
|
||||
Image.search(
|
||||
%{
|
||||
query: %{
|
||||
bool: %{
|
||||
must: %{
|
||||
range: %{id: %{gt: image.id}}
|
||||
},
|
||||
must_not: [
|
||||
filter,
|
||||
%{term: %{hidden_from_users: true}}
|
||||
]
|
||||
}
|
||||
},
|
||||
sort: %{created_at: :desc},
|
||||
size: 0
|
||||
}
|
||||
)
|
||||
|
||||
page_num = page_for_offset(pagination.page_size, resp["hits"]["total"])
|
||||
|
||||
conn
|
||||
|> redirect(to: Routes.image_path(conn, :index, page: page_num))
|
||||
end
|
||||
|
||||
defp page_for_offset(_per_page, 0), do: 1
|
||||
defp page_for_offset(per_page, offset) do
|
||||
((offset + 1) / per_page)
|
||||
|> Float.ceil()
|
||||
|> trunc()
|
||||
|> to_string()
|
||||
end
|
||||
|
||||
defp compile_query(conn) do
|
||||
user = conn.assigns.current_user
|
||||
|
||||
{:ok, query} = Query.compile(user, conn.params["q"] || "")
|
||||
|
||||
query
|
||||
end
|
||||
end
|
|
@ -2,6 +2,7 @@ defmodule PhilomenaWeb.SearchController do
|
|||
use PhilomenaWeb, :controller
|
||||
|
||||
alias Philomena.Images.{Image, Query}
|
||||
alias Philomena.ImageSorter
|
||||
alias Philomena.Interactions
|
||||
|
||||
import Ecto.Query
|
||||
|
@ -9,16 +10,14 @@ defmodule PhilomenaWeb.SearchController do
|
|||
def index(conn, params) do
|
||||
filter = conn.assigns.compiled_filter
|
||||
user = conn.assigns.current_user
|
||||
sort = ImageSorter.parse_sort(params)
|
||||
|
||||
with {:ok, query} <- Query.compile(user, params["q"]) do
|
||||
sd = parse_sd(params)
|
||||
sf = parse_sf(params, sd)
|
||||
|
||||
images =
|
||||
Image.search_records(
|
||||
%{
|
||||
query: %{bool: %{must: [query | sf.query], must_not: [filter, %{term: %{hidden_from_users: true}}]}},
|
||||
sort: sf.sort
|
||||
query: %{bool: %{must: [query | sort.queries], must_not: [filter, %{term: %{hidden_from_users: true}}]}},
|
||||
sort: sort.sorts
|
||||
},
|
||||
conn.assigns.pagination,
|
||||
Image |> preload(:tags)
|
||||
|
@ -40,67 +39,4 @@ defmodule PhilomenaWeb.SearchController do
|
|||
end
|
||||
end
|
||||
|
||||
defp parse_sd(%{"sd" => sd}) when sd in ~W(asc desc),
|
||||
do: sd
|
||||
defp parse_sd(_params), do: :desc
|
||||
|
||||
defp parse_sf(%{"sf" => sf}, sd) when
|
||||
sf in ~W(created_at updated_at first_seen_at width height score comment_count tag_count wilson_score _score)
|
||||
do
|
||||
%{query: [], sort: %{sf => sd}}
|
||||
end
|
||||
|
||||
defp parse_sf(%{"sf" => "random"}, sd) do
|
||||
random_query(:rand.uniform(4_294_967_296), sd)
|
||||
end
|
||||
|
||||
defp parse_sf(%{"sf" => <<"random:", seed::binary>>}, sd) do
|
||||
case Integer.parse(seed) do
|
||||
{seed, _rest} ->
|
||||
random_query(seed, sd)
|
||||
|
||||
_ ->
|
||||
random_query(:rand.uniform(4_294_967_296), sd)
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_sf(%{"sf" => <<"gallery_id:", gallery::binary>>}, sd) do
|
||||
case Integer.parse(gallery) do
|
||||
{gallery, _rest} ->
|
||||
%{
|
||||
query: [],
|
||||
sort: %{
|
||||
"galleries.position": %{
|
||||
order: sd,
|
||||
nested_path: :galleries,
|
||||
nested_filter: %{
|
||||
term: %{
|
||||
"galleries.id": gallery
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ ->
|
||||
%{query: [], sort: %{match_none: %{}}}
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_sf(_params, sd) do
|
||||
%{query: [], sort: %{created_at: sd}}
|
||||
end
|
||||
|
||||
defp random_query(seed, sd) do
|
||||
%{
|
||||
query: [%{
|
||||
function_score: %{
|
||||
query: %{match_all: %{}},
|
||||
random_score: %{seed: seed},
|
||||
boost_mode: :replace
|
||||
}
|
||||
}],
|
||||
sort: %{_score: sd}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -66,6 +66,50 @@ defmodule PhilomenaWeb.TagController do
|
|||
dnp_entries =
|
||||
Enum.zip(dnp_bodies, tag.dnp_entries)
|
||||
|
||||
render(conn, "show.html", tag: tag, body: body, dnp_entries: dnp_entries, interactions: interactions, images: images, layout_class: "layout--wide")
|
||||
search_query = escape_name(tag)
|
||||
|
||||
render(
|
||||
conn,
|
||||
"show.html",
|
||||
tag: tag,
|
||||
body: body,
|
||||
search_query: search_query,
|
||||
dnp_entries: dnp_entries,
|
||||
interactions: interactions,
|
||||
images: images,
|
||||
layout_class: "layout--wide"
|
||||
)
|
||||
end
|
||||
|
||||
def escape_name(%{name: name}) do
|
||||
name =
|
||||
name
|
||||
|> String.replace(~r/\s+/, " ")
|
||||
|> String.trim()
|
||||
|> String.downcase()
|
||||
|
||||
cond do
|
||||
String.contains?(name, "(") or String.contains?(name, ")") ->
|
||||
# \ * ? " should be escaped, wrap in quotes so parser doesn't
|
||||
# choke on parens.
|
||||
name =
|
||||
name
|
||||
|> String.replace("\\", "\\\\")
|
||||
|> String.replace("*", "\\*")
|
||||
|> String.replace("?", "\\?")
|
||||
|> String.replace("\"", "\\\"")
|
||||
|
||||
"\"#{name}\""
|
||||
|
||||
true ->
|
||||
# \ * ? - ! " all must be escaped.
|
||||
name
|
||||
|> String.replace(~r/\A-/, "\\-")
|
||||
|> String.replace(~r/\A!/, "\\!")
|
||||
|> String.replace("\\", "\\\\")
|
||||
|> String.replace("*", "\\*")
|
||||
|> String.replace("?", "\\?")
|
||||
|> String.replace("\"", "\\\"")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -99,6 +99,7 @@ defmodule PhilomenaWeb.Router do
|
|||
resources "/tag_changes", Image.TagChangeController, only: [:index]
|
||||
resources "/source_changes", Image.SourceChangeController, only: [:index]
|
||||
resources "/description", Image.DescriptionController, only: [:update], singleton: true
|
||||
resources "/navigate", Image.NavigateController, only: [:index]
|
||||
end
|
||||
scope "/tags", Tag, as: :tag do
|
||||
resources "/autocomplete", AutocompleteController, only: [:show], singleton: true
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
elixir:
|
||||
link = assigns[:link] || Routes.image_path(@conn, :show, @image)
|
||||
size_class =
|
||||
case @size do
|
||||
:thumb ->
|
||||
|
@ -34,4 +35,4 @@ elixir:
|
|||
a.interaction--hide href="#" rel="nofollow" data-image-id=@image.id
|
||||
i.fa.fa-eye-slash title='Hide'
|
||||
.media-box__content.flex.flex--centered.flex--center-distributed class=size_class
|
||||
= render PhilomenaWeb.ImageView, "_image_container.html", image: @image, size: @size, conn: @conn
|
||||
= render PhilomenaWeb.ImageView, "_image_container.html", link: link, image: @image, size: @size, conn: @conn
|
|
@ -1,11 +1,11 @@
|
|||
.block.block__header
|
||||
.flex.flex--wrap.image-metabar.center--layout id="image_meta_#{@image.id}"
|
||||
.stretched-mobile-links
|
||||
a.js-prev href="/" title="Previous Image (j)"
|
||||
a.js-prev href=Routes.image_navigate_path(@conn, :index, @image, [rel: "prev"] ++ scope(@conn)) title="Previous Image (j)"
|
||||
i.fa.fa-chevron-left
|
||||
a.js-up href="/" title="Find this image in the global image list (i)"
|
||||
a.js-up href=Routes.image_navigate_path(@conn, :index, @image, [rel: "find"] ++ scope(@conn)) title="Find this image in the global image list (i)"
|
||||
i.fa.fa-chevron-up
|
||||
a.js-next href="/" title="Next Image (k)"
|
||||
a.js-next href=Routes.image_navigate_path(@conn, :index, @image, [rel: "next"] ++ scope(@conn)) title="Next Image (k)"
|
||||
i.fa.fa-chevron-right
|
||||
a.js-rand href="/" title="Random (r)"
|
||||
i.fa.fa-random
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
- header = assigns[:header] || ""
|
||||
- route = assigns[:route] || fn p -> Routes.image_path(@conn, :index, p) end
|
||||
- params = assigns[:params] || []
|
||||
- pagination = render PhilomenaWeb.PaginationView, "_pagination.html", page: @images, route: route, params: params
|
||||
- info = render PhilomenaWeb.PaginationView, "_pagination_info.html", page: @images
|
||||
elixir:
|
||||
header = assigns[:header] || ""
|
||||
params = assigns[:params] || assigns[:scope] || []
|
||||
scope = assigns[:scope] || []
|
||||
route = assigns[:route] || fn p -> Routes.image_path(@conn, :index, p) end
|
||||
image_url = fn image -> Routes.image_path(@conn, :show, image, scope) end
|
||||
pagination = render PhilomenaWeb.PaginationView, "_pagination.html", page: @images, route: route, params: params
|
||||
info = render PhilomenaWeb.PaginationView, "_pagination_info.html", page: @images
|
||||
|
||||
.block#imagelist-container
|
||||
section.block__header.flex
|
||||
|
@ -12,7 +15,7 @@
|
|||
|
||||
.block__content.js-resizable-media-container
|
||||
= for image <- @images do
|
||||
= render PhilomenaWeb.ImageView, "_image_box.html", image: image, size: assigns[:size] || :thumb, conn: @conn
|
||||
= render PhilomenaWeb.ImageView, "_image_box.html", image: image, link: image_url.(image), size: assigns[:size] || :thumb, conn: @conn
|
||||
|
||||
.block__header.block__header--light.flex
|
||||
= pagination
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
= cond do
|
||||
- Enum.any?(@images) ->
|
||||
= render PhilomenaWeb.ImageView, "index.html", conn: @conn, images: @images, route: fn p -> Routes.search_path(@conn, :index, p) end, params: [q: @search_query]
|
||||
= render PhilomenaWeb.ImageView, "index.html", conn: @conn, images: @images, route: fn p -> Routes.search_path(@conn, :index, p) end, scope: [q: @conn.params["q"], sf: @conn.params["sf"], sd: @conn.params["sd"]]
|
||||
- assigns[:error] ->
|
||||
p
|
||||
' Oops, there was an error evaluating your query:
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
= render PhilomenaWeb.TagView, "_tag_info_row.html", tag: @tag, body: @body, dnp_entries: @dnp_entries, conn: @conn
|
||||
= render PhilomenaWeb.ImageView, "index.html", conn: @conn, images: @images
|
||||
= render PhilomenaWeb.ImageView, "index.html", conn: @conn, images: @images, scope: [q: @search_query]
|
|
@ -81,6 +81,8 @@ defmodule PhilomenaWeb.ImageView do
|
|||
Tag.display_order(tags)
|
||||
end
|
||||
|
||||
def scope(conn), do: Philomena.ImageScope.scope(conn)
|
||||
|
||||
defp thumb_format("svg"), do: "png"
|
||||
defp thumb_format(format), do: format
|
||||
end
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
defmodule PhilomenaWeb.SearchView do
|
||||
use PhilomenaWeb, :view
|
||||
|
||||
def scope(conn), do: Philomena.ImageScope.scope(conn)
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue