settings page

This commit is contained in:
byte[] 2019-11-30 17:40:53 -05:00
parent 72121ca057
commit 64d1e817d1
25 changed files with 516 additions and 115 deletions

View file

@ -1,10 +1,9 @@
defmodule Philomena.Filters.Filter do defmodule Philomena.Filters.Filter do
use Ecto.Schema use Ecto.Schema
import Philomena.Schema.TagList
import Philomena.Schema.Search
import Ecto.Changeset import Ecto.Changeset
import Ecto.Query
alias Philomena.Tags.Tag
alias Philomena.Images.Query
alias Philomena.Users.User alias Philomena.Users.User
alias Philomena.Repo alias Philomena.Repo
@ -29,67 +28,21 @@ defmodule Philomena.Filters.Filter do
@doc false @doc false
def changeset(filter, attrs) do def changeset(filter, attrs) do
user =
filter
|> Repo.preload(:user)
|> Map.get(:user)
filter filter
|> cast(attrs, [:spoilered_tag_list, :hidden_tag_list, :description, :name, :spoilered_complex_str, :hidden_complex_str]) |> cast(attrs, [:spoilered_tag_list, :hidden_tag_list, :description, :name, :spoilered_complex_str, :hidden_complex_str])
|> propagate_tag_lists() |> propagate_tag_list(:spoilered_tag_list, :spoilered_tag_ids)
|> propagate_tag_list(:hidden_tag_list, :hidden_tag_ids)
|> validate_required([:name, :description]) |> validate_required([:name, :description])
|> unsafe_validate_unique([:user_id, :name], Repo)
|> validate_my_downvotes(:spoilered_complex_str) |> validate_my_downvotes(:spoilered_complex_str)
|> validate_my_downvotes(:hidden_complex_str) |> validate_my_downvotes(:hidden_complex_str)
|> validate_search(:spoilered_complex_str) |> validate_search(:spoilered_complex_str, user)
|> validate_search(:hidden_complex_str) |> validate_search(:hidden_complex_str, user)
end |> unsafe_validate_unique([:user_id, :name], Repo)
def assign_tag_lists(filter) do
tags = Enum.uniq(filter.spoilered_tag_ids ++ filter.hidden_tag_ids)
lookup =
Tag
|> where([t], t.id in ^tags)
|> Repo.all()
|> Map.new(fn t -> {t.id, t.name} end)
spoilered_tag_list =
filter.spoilered_tag_ids
|> Enum.map(&lookup[&1])
|> Enum.filter(& &1 != nil)
|> Enum.sort()
|> Enum.join(", ")
hidden_tag_list =
filter.hidden_tag_ids
|> Enum.map(&lookup[&1])
|> Enum.filter(& &1 != nil)
|> Enum.sort()
|> Enum.join(", ")
%{filter | hidden_tag_list: hidden_tag_list, spoilered_tag_list: spoilered_tag_list}
end
defp propagate_tag_lists(changeset) do
spoilers = get_field(changeset, :spoilered_tag_list) |> parse_tag_list
filters = get_field(changeset, :hidden_tag_list) |> parse_tag_list
tags = Enum.uniq(spoilers ++ filters)
lookup =
Tag
|> where([t], t.name in ^tags)
|> Repo.all()
|> Map.new(fn t -> {t.name, t.id} end)
spoilered_tag_ids =
spoilers
|> Enum.map(&lookup[&1])
|> Enum.filter(& &1 != nil)
hidden_tag_ids =
filters
|> Enum.map(&lookup[&1])
|> Enum.filter(& &1 != nil)
changeset
|> put_change(:spoilered_tag_ids, spoilered_tag_ids)
|> put_change(:hidden_tag_ids, hidden_tag_ids)
end end
defp validate_my_downvotes(changeset, field) do defp validate_my_downvotes(changeset, field) do
@ -102,25 +55,4 @@ defmodule Philomena.Filters.Filter do
changeset changeset
end end
end end
defp validate_search(changeset, field) do
user_id = get_field(changeset, :user_id)
user = if user_id, do: User |> Repo.get!(user_id)
output = Query.compile(user, get_field(changeset, field))
case output do
{:ok, _} -> changeset
_ ->
changeset
|> add_error(field, "is invalid")
end
end
defp parse_tag_list(list) do
(list || "")
|> String.split(",")
|> Enum.map(&String.trim(&1))
|> Enum.filter(& &1 != "")
end
end end

View file

@ -1,16 +1,16 @@
defmodule Philomena.ImageScope do defmodule Philomena.ImageScope do
def scope(conn) do def scope(conn) do
[] []
|> scope(conn, "q") |> scope(conn, "q", :q)
|> scope(conn, "sf") |> scope(conn, "sf", :sf)
|> scope(conn, "sd") |> scope(conn, "sd", :sf)
end end
defp scope(list, conn, key) do defp scope(list, conn, key, key_atom) do
case conn.params[key] do case conn.params[key] do
nil -> list nil -> list
"" -> list "" -> list
val -> [{key, val} | list] val -> [{key_atom, val} | list]
end end
end end
end end

View file

@ -0,0 +1,18 @@
defmodule Philomena.Schema.Search do
alias Philomena.Images.Query
import Search.String
import Ecto.Changeset
def validate_search(changeset, field, user, watched \\ false) do
query = changeset |> get_field(field) |> normalize()
output = Query.compile(user, query, watched)
case output do
{:ok, _} ->
changeset
_ ->
add_error(changeset, field, "is invalid")
end
end
end

View file

@ -0,0 +1,52 @@
defmodule Philomena.Schema.TagList do
# TODO: remove this in favor of normalized relations
alias Philomena.Tags.Tag
alias Philomena.Repo
import Ecto.Changeset
import Ecto.Query
def assign_tag_list(model, field, target_field) do
tags = model |> Map.get(field) |> Enum.uniq()
lookup =
Tag
|> where([t], t.id in ^tags)
|> order_by(asc: :name)
|> Repo.all()
|> Map.new(fn t -> {t.id, t.name} end)
tag_list =
model
|> Map.get(field)
|> Enum.map(&lookup[&1])
|> Enum.reject(&is_nil/1)
|> Enum.join(", ")
%{model | target_field => tag_list}
end
def propagate_tag_list(changeset, field, target_field) do
tag_list = changeset |> get_field(field) |> parse_tag_list()
lookup =
Tag
|> where([t], t.name in ^tag_list)
|> Repo.all()
|> Map.new(fn t -> {t.name, t.id} end)
tag_ids =
tag_list
|> Enum.map(&lookup[&1])
|> Enum.reject(&is_nil/1)
changeset
|> put_change(target_field, tag_ids)
end
defp parse_tag_list(list) do
(list || "")
|> String.split(",")
|> Enum.map(&String.trim(&1))
|> Enum.filter(& &1 != "")
end
end

View file

@ -83,6 +83,12 @@ defmodule Philomena.Users do
|> Repo.update() |> Repo.update()
end end
def update_settings(%User{} = user, attrs) do
user
|> User.settings_changeset(attrs)
|> Repo.update()
end
@doc """ @doc """
Returns an `%Ecto.Changeset{}` for tracking user changes. Returns an `%Ecto.Changeset{}` for tracking user changes.

View file

@ -12,6 +12,8 @@ defmodule Philomena.Users.User do
extensions: [PowResetPassword, PowLockout] extensions: [PowResetPassword, PowLockout]
import Ecto.Changeset import Ecto.Changeset
import Philomena.Schema.TagList
import Philomena.Schema.Search
alias Philomena.Filters.Filter alias Philomena.Filters.Filter
alias Philomena.UserLinks.UserLink alias Philomena.UserLinks.UserLink
@ -106,6 +108,7 @@ defmodule Philomena.Users.User do
# Poorly denormalized associations # Poorly denormalized associations
field :recent_filter_ids, {:array, :integer}, default: [] field :recent_filter_ids, {:array, :integer}, default: []
field :watched_tag_ids, {:array, :integer}, default: [] field :watched_tag_ids, {:array, :integer}, default: []
field :watched_tag_list, :string, virtual: true
# Other stuff # Other stuff
field :last_donation_at, :naive_datetime field :last_donation_at, :naive_datetime
@ -158,6 +161,28 @@ defmodule Philomena.Users.User do
|> validate_inclusion(:spoiler_type, ~W(static click hover off)) |> validate_inclusion(:spoiler_type, ~W(static click hover off))
end end
def settings_changeset(user, attrs) do
user
|> cast(attrs, [
:watched_tag_list, :images_per_page, :fancy_tag_field_on_upload,
:fancy_tag_field_on_edit, :anonymous_by_default, :scale_large_images,
:comments_per_page, :theme, :watched_images_query_str,
:no_spoilered_in_watched, :watched_images_exclude_str,
:use_centered_layout, :hide_vote_counts
])
|> validate_required([
:images_per_page, :fancy_tag_field_on_upload, :fancy_tag_field_on_edit,
:anonymous_by_default, :scale_large_images, :comments_per_page, :theme,
:no_spoilered_in_watched, :use_centered_layout, :hide_vote_counts
])
|> propagate_tag_list(:watched_tag_list, :watched_tag_ids)
|> validate_inclusion(:theme, ~W(default dark red))
|> validate_inclusion(:images_per_page, 15..50)
|> validate_inclusion(:comments_per_page, 15..50)
|> validate_search(:watched_images_query_str, user, true)
|> validate_search(:watched_images_exclude_str, user, true)
end
def create_totp_secret_changeset(user) do def create_totp_secret_changeset(user) do
secret = :crypto.strong_rand_bytes(15) |> Base.encode32() secret = :crypto.strong_rand_bytes(15) |> Base.encode32()
data = Philomena.Users.Encryptor.encrypt_model(secret) data = Philomena.Users.Encryptor.encrypt_model(secret)
@ -288,8 +313,15 @@ defmodule Philomena.Users.User do
|> put_change(:slug, Slug.slug(name)) |> put_change(:slug, Slug.slug(name))
end end
defp totp_valid?(user, token), defp totp_valid?(user, token) do
do: :pot.valid_totp(token, totp_secret(user), window: 1) case Integer.parse(token) do
{int_token, _rest} ->
int_token != user.consumed_timestep and :pot.valid_totp(token, totp_secret(user), window: 1)
_error ->
false
end
end
defp backup_code_valid?(user, token), defp backup_code_valid?(user, token),
do: Enum.any?(user.otp_backup_codes, &Password.verify_pass(token, &1)) do: Enum.any?(user.otp_backup_codes, &Password.verify_pass(token, &1))

View file

@ -26,7 +26,7 @@ defmodule PhilomenaWeb.ActivityController do
}, },
sort: %{created_at: :desc} sort: %{created_at: :desc}
}, },
%{page_number: 1, page_size: 25}, %{conn.assigns.image_pagination | page_number: 1},
Image |> preload([:tags]) Image |> preload([:tags])
) )
@ -84,7 +84,7 @@ defmodule PhilomenaWeb.ActivityController do
}, },
sort: %{created_at: :desc} sort: %{created_at: :desc}
}, },
%{page_number: 1, page_size: 25}, %{conn.assigns.image_pagination | page_number: 1},
Image |> preload([:tags]) Image |> preload([:tags])
) )
end end

View file

@ -9,9 +9,11 @@ defmodule PhilomenaWeb.ChannelController do
plug :load_resource, model: Channel plug :load_resource, model: Channel
def index(conn, _params) do def index(conn, _params) do
show_nsfw? = conn.cookies["chan_nsfw"] == "true"
channels = channels =
Channel Channel
|> where([c], c.nsfw == false and not is_nil(c.last_fetched_at)) |> maybe_show_nsfw(show_nsfw?)
|> where([c], not is_nil(c.last_fetched_at))
|> order_by(desc: :is_live, asc: :title) |> order_by(desc: :is_live, asc: :title)
|> preload(:associated_artist_tag) |> preload(:associated_artist_tag)
|> Repo.paginate(conn.assigns.scrivener) |> Repo.paginate(conn.assigns.scrivener)
@ -28,6 +30,9 @@ defmodule PhilomenaWeb.ChannelController do
redirect(conn, external: url(channel)) redirect(conn, external: url(channel))
end end
defp maybe_show_nsfw(query, true), do: query
defp maybe_show_nsfw(query, _falsy), do: where(query, [c], c.nsfw == false)
defp url(%{type: "LivestreamChannel", short_name: short_name}), defp url(%{type: "LivestreamChannel", short_name: short_name}),
do: "http://www.livestream.com/#{short_name}" do: "http://www.livestream.com/#{short_name}"
defp url(%{type: "PicartoChannel", short_name: short_name}), defp url(%{type: "PicartoChannel", short_name: short_name}),

View file

@ -2,6 +2,7 @@ defmodule PhilomenaWeb.FilterController do
use PhilomenaWeb, :controller use PhilomenaWeb, :controller
alias Philomena.{Filters, Filters.Filter, Tags.Tag} alias Philomena.{Filters, Filters.Filter, Tags.Tag}
alias Philomena.Schema.TagList
alias Philomena.Repo alias Philomena.Repo
import Ecto.Query import Ecto.Query
@ -67,7 +68,11 @@ defmodule PhilomenaWeb.FilterController do
end end
def edit(conn, _params) do def edit(conn, _params) do
filter = conn.assigns.filter |> Filter.assign_tag_lists() filter =
conn.assigns.filter
|> TagList.assign_tag_list(:spoilered_tag_ids, :spoilered_tag_list)
|> TagList.assign_tag_list(:hidden_tag_ids, :hidden_tag_list)
changeset = Filters.change_filter(filter) changeset = Filters.change_filter(filter)
render(conn, "edit.html", filter: filter, changeset: changeset) render(conn, "edit.html", filter: filter, changeset: changeset)

View file

@ -43,7 +43,7 @@ defmodule PhilomenaWeb.Image.CommentController do
|> where(image_id: ^conn.assigns.image.id) |> where(image_id: ^conn.assigns.image.id)
|> order_by(desc: :created_at) |> order_by(desc: :created_at)
|> preload([:image, user: [awards: :badge]]) |> preload([:image, user: [awards: :badge]])
|> Repo.paginate(conn.assigns.scrivener) |> Repo.paginate(conn.assigns.comment_scrivener)
rendered = rendered =
comments.entries comments.entries

View file

@ -26,7 +26,7 @@ defmodule PhilomenaWeb.ImageController do
query: %{bool: %{must_not: [query, %{term: %{hidden_from_users: true}}]}}, query: %{bool: %{must_not: [query, %{term: %{hidden_from_users: true}}]}},
sort: %{created_at: :desc} sort: %{created_at: :desc}
}, },
conn.assigns.pagination, conn.assigns.image_pagination,
Image |> preload([:tags, :user]) Image |> preload([:tags, :user])
) )
@ -45,7 +45,7 @@ defmodule PhilomenaWeb.ImageController do
|> preload([:image, user: [awards: :badge]]) |> preload([:image, user: [awards: :badge]])
|> order_by(desc: :created_at) |> order_by(desc: :created_at)
|> limit(25) |> limit(25)
|> Repo.paginate(conn.assigns.scrivener) |> Repo.paginate(conn.assigns.comment_scrivener)
rendered = rendered =
comments.entries comments.entries

View file

@ -19,7 +19,7 @@ defmodule PhilomenaWeb.SearchController do
query: %{bool: %{must: [query | sort.queries], must_not: [filter, %{term: %{hidden_from_users: true}}]}}, query: %{bool: %{must: [query | sort.queries], must_not: [filter, %{term: %{hidden_from_users: true}}]}},
sort: sort.sorts sort: sort.sorts
}, },
conn.assigns.pagination, conn.assigns.image_pagination,
Image |> preload(:tags) Image |> preload(:tags)
) )

View file

@ -0,0 +1,59 @@
defmodule PhilomenaWeb.SettingController do
use PhilomenaWeb, :controller
alias Philomena.Users
alias Philomena.Users.User
alias Philomena.Schema.TagList
alias Plug.Conn
def edit(conn, _params) do
changeset =
(conn.assigns.current_user || %User{})
|> TagList.assign_tag_list(:watched_tag_ids, :watched_tag_list)
|> Users.change_user()
render(conn, "edit.html", changeset: changeset)
end
def update(conn, %{"user" => user_params}) do
user = conn.assigns.current_user
conn
|> update_local_settings(user_params)
|> maybe_update_user(user, user_params)
|> case do
{:ok, conn} ->
conn
|> put_flash(:info, "Settings updated successfully.")
|> redirect(to: Routes.setting_path(conn, :edit))
{:error, changeset} ->
conn
|> put_flash(:error, "Your settings could not be saved!")
|> render("edit.html", changeset: changeset)
end
end
defp update_local_settings(conn, user_params) do
conn
|> set_cookie(user_params, "hidpi", "hidpi")
|> set_cookie(user_params, "webm", "webm")
|> set_cookie(user_params, "chan_nsfw", "chan_nsfw")
end
defp set_cookie(conn, params, param_name, cookie_name) do
# JS wants access; max-age is set to 25 years from now
Conn.put_resp_cookie(conn, cookie_name, to_string(params[param_name] == "true"), max_age: 788_923_800, http_only: false)
end
defp maybe_update_user(conn, nil, _user_params), do: {:ok, conn}
defp maybe_update_user(conn, user, user_params) do
case Users.update_settings(user, user_params) do
{:ok, _user} ->
{:ok, conn}
error ->
error
end
end
end

View file

@ -50,7 +50,7 @@ defmodule PhilomenaWeb.TagController do
}, },
sort: %{created_at: :desc} sort: %{created_at: :desc}
}, },
conn.assigns.pagination, conn.assigns.image_pagination,
Image |> preload([:tags, :user]) Image |> preload([:tags, :user])
) )

View file

@ -1,12 +1,14 @@
defmodule PhilomenaWeb.PaginationPlug do defmodule PhilomenaWeb.PaginationPlug do
import Plug.Conn import Plug.Conn
alias Pow.Plug
# No options # No options
def init([]), do: false def init([]), do: []
# Assign pagination info # Assign pagination info
def call(conn, _opts) do def call(conn, _opts) do
conn = conn |> fetch_query_params() conn = conn |> fetch_query_params()
user = conn |> Plug.current_user()
params = conn.params params = conn.params
page_number = page_number =
@ -31,6 +33,14 @@ defmodule PhilomenaWeb.PaginationPlug do
conn conn
|> assign(:pagination, %{page_number: page_number, page_size: page_size}) |> assign(:pagination, %{page_number: page_number, page_size: page_size})
|> assign(:image_pagination, %{page_number: page_number, page_size: image_page_size(user)})
|> assign(:scrivener, [page: page_number, page_size: page_size]) |> assign(:scrivener, [page: page_number, page_size: page_size])
|> assign(:comment_scrivener, [page: page_number, page_size: comment_page_size(user)])
end end
defp image_page_size(%{images_per_page: x}), do: x
defp image_page_size(_user), do: 15
defp comment_page_size(%{comments_per_page: x}), do: x
defp comment_page_size(_user), do: 25
end end

View file

@ -136,6 +136,7 @@ defmodule PhilomenaWeb.Router do
resources "/staff", StaffController, only: [:index] resources "/staff", StaffController, only: [:index]
resources "/stats", StatController, only: [:index] resources "/stats", StatController, only: [:index]
resources "/channels", ChannelController, only: [:index, :show] resources "/channels", ChannelController, only: [:index, :show]
resources "/settings", SettingController, only: [:edit, :update], singleton: true
get "/:id", ImageController, :show get "/:id", ImageController, :show
# get "/:forum_id", ForumController, :show # impossible to do without constraints # get "/:forum_id", ForumController, :show # impossible to do without constraints

View file

@ -16,19 +16,28 @@
.media-box__overlay.js-spoiler-info-overlay .media-box__overlay.js-spoiler-info-overlay
a href=link a href=link
= if @image.thumbnails_generated do = case render_intent(@conn, @image, @size) do
- uris = thumb_urls(@image, false) - {:hidpi, small_url, medium_url, hover_text} ->
- vid = @image.image_mime_type == "video/webm"
- tags = Enum.map(@image.tags, & &1.name) |> Enum.sort() |> Enum.join(", ")
- alt = "Size: #{@image.image_width}x#{@image.image_height} | Tagged: #{tags}"
= if vid do
video alt=alt autoplay="autoplay" muted="muted" loop="loop" playsinline="playsinline"
source src=uris[@size] type="video/webm"
source src=String.replace(uris[@size], ".webm", ".mp4") type="video/mp4"
- else
picture picture
img alt=alt src=thumb_url(@image, false, @size) srcset="#{uris[@size]} 1x, #{uris[:medium]} 2x" img src=small_url srcset="#{small_url} 1x, #{medium_url} 2x" alt=hover_text
- else - {:image, small_url, hover_text} ->
| Thumbnails not yet generated picture
img src=small_url alt=hover_text
- {:video, webm, mp4, 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} ->
picture
img alt=hover_text
- {:filtered_video, hover_text}
video autoplay="autoplay" muted="muted" loop="loop" playsinline="playsinline"
img alt=hover_text
- :not_rendered ->
' Thumbnails not yet generated

View file

@ -15,6 +15,7 @@
span.fave-span title="Fave!" span.fave-span title="Fave!"
i.fa.fa-star i.fa.fa-star
a.interaction--upvote href="#" rel="nofollow" data-image-id=@image.id a.interaction--upvote href="#" rel="nofollow" data-image-id=@image.id
= if show_vote_counts?(@conn.assigns.current_user) do
span.upvotes> title="Upvotes" data-image-id=@image.id = @image.upvotes_count span.upvotes> title="Upvotes" data-image-id=@image.id = @image.upvotes_count
span.upvote-span title="Yay!" span.upvote-span title="Yay!"
i.fa.fa-arrow-up i.fa.fa-arrow-up
@ -22,6 +23,7 @@
a.interaction--downvote href="#" rel="nofollow" data-image-id=@image.id a.interaction--downvote href="#" rel="nofollow" data-image-id=@image.id
span.downvote-span title="Neigh!" span.downvote-span title="Neigh!"
i.fa.fa-arrow-down i.fa.fa-arrow-down
= if show_vote_counts?(@conn.assigns.current_user) do
span.downvotes< title="Downvotes" data-image-id=@image.id = @image.downvotes_count span.downvotes< title="Downvotes" data-image-id=@image.id = @image.downvotes_count
a.interaction--comments href="#comments" title="Comments" a.interaction--comments href="#comments" title="Comments"
i.fa.fa-comments i.fa.fa-comments

View file

@ -21,7 +21,7 @@ html lang="en"
script type="text/javascript" src=Routes.static_path(@conn, "/js/app.js") async="async" script type="text/javascript" src=Routes.static_path(@conn, "/js/app.js") async="async"
body data-theme="default" body data-theme="default"
= render PhilomenaWeb.LayoutView, "_burger.html", assigns = render PhilomenaWeb.LayoutView, "_burger.html", assigns
#container #container class=container_class(@current_user)
= render PhilomenaWeb.LayoutView, "_header.html", assigns = render PhilomenaWeb.LayoutView, "_header.html", assigns
= render PhilomenaWeb.LayoutView, "_flash_warnings.html", assigns = render PhilomenaWeb.LayoutView, "_flash_warnings.html", assigns
main#content class=layout_class(@conn) main#content class=layout_class(@conn)

View file

@ -1,6 +1,6 @@
= cond do = cond do
- Enum.any?(@images) -> - Enum.any?(@images) ->
= 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"]] = render PhilomenaWeb.ImageView, "index.html", conn: @conn, images: @images, route: fn p -> Routes.search_path(@conn, :index, p) end, scope: scope(@conn)
- assigns[:error] -> - assigns[:error] ->
p p
' Oops, there was an error evaluating your query: ' Oops, there was an error evaluating your query:

View file

@ -0,0 +1,118 @@
h1 Content Settings
= form_for @changeset, Routes.setting_path(@conn, :update), [method: "put"], fn f ->
= if @changeset.action do
.alert.alert-danger
p Oops, something went wrong! Please check the errors below.
#js-setting-table.block
.block__header.block__header--js-tabbed
= if @conn.assigns.current_user do
= link "Watch List", to: "#", class: "selected", data: [click_tab: "watched"]
= link "Display", to: "#", data: [click_tab: "display"]
= link "Metadata", to: "#", data: [click_tab: "metadata"]
= link "Local", to: "#", data: [click_tab: "local"]
- else
= link "Local", to: "#", class: "selected", data: [click_tab: "local"]
= link "More settings", to: "#", data: [click_tab: "join-the-herd"]
= if @conn.assigns.current_user do
.block__tab data-tab="watched"
h4 Tags
.field
= label f, :watched_tag_list, "Tags to watch"
= render PhilomenaWeb.TagView, "_tag_editor.html", f: f, name: :watched_tag_list, type: :edit, conn: @conn
h4 Watchlist queries and filtering
p
' The following two areas are for search queries to control what other images show up in your watch list. Lines are ORed together. See
=> link "the syntax guide", to: "/pages/search_syntax"
' for how to write queries.
.field
= label f, :watched_images_query_str, "Watch list search string (images found by this search are added to your watched images list)"
= textarea f, :watched_images_query_str, class: "input input--wide", autocapitalize: "none"
.field
= label f, :watched_images_exclude_str, "Watch list filter string (any images found by this search are removed from your watched images list)"
= textarea f, :watched_images_exclude_str, class: "input input--wide", autocapitalize: "none"
.field
=> checkbox f, :no_spoilered_in_watched, class: "checkbox"
=> label f, :no_spoilered_in_watched, "Hide images spoilered by filter in watchlist"
.block__tab.hidden.flex.flex--maybe-wrap data-tab="display"
div
.field
=> label f, :use_centered_layout
=> checkbox f, :use_centered_layout, class: "checkbox"
.fieldlabel: i Align content to the center of the page - try this option out if you browse the site on a tablet or a fairly wide screen.
.field
=> label f, :hide_vote_counts
=> checkbox f, :hide_vote_counts, class: "checkbox"
.fieldlabel: i Hide upvote and downvote counts on images, showing only the overall score
.field
=> label f, :images_per_page
=> number_input f, :images_per_page, min: 15, max: 50, step: 1, class: "input"
.fieldlabel
i
' This is the number of images per page that are displayed on image listings and searches, up to a maximum of 50.
' For 1080p monitors, try 24.
.field
=> label f, :comments_per_page
=> number_input f, :comments_per_page, min: 15, max: 50, step: 1, class: "input"
.fieldlabel: i This is the number of comments per page that are displayed on image pages.
.field
=> label f, :scale_large_images
=> checkbox f, :scale_large_images, class: "checkbox"
.fieldlabel: i Scale large images down to fit your monitor (approximately) server-side before downloading. Disabling this will load full images immediately on image pages.
.field
=> label f, :theme
=> select f, :theme, theme_options(@conn), class: "input"
.fieldlabel: i Preview themes by selecting one from the dropdown. Saving sets the currently selected theme.
.block__tab.hidden.flex.flex--maybe-wrap data-tab="metadata"
div
.field
=> label f, :fancy_tag_field_on_upload, "Fancy tags - uploads"
=> checkbox f, :fancy_tag_field_on_upload, class: "checkbox"
.field
=> label f, :fancy_tag_field_on_edit, "Fancy tags - edits"
=> checkbox f, :fancy_tag_field_on_edit, class: "checkbox"
.fieldlabel: i The fancy tag editor gives you autosuggestions and visual representations of the tags, but is sometimes not desired - for instance when dealing with batch uploads where you might want to copy-paste tags. You can choose which type of editor to use by default here.
.field
=> label f, :anonymous_by_default
=> checkbox f, :anonymous_by_default, class: "checkbox"
.fieldlabel: i Check this box to post images and comments as anonymous by default, even if logged in.
.block__tab class=local_tab_class(@conn) data-tab="local"
.block.block--fixed.block--warning Settings on this tab are saved in the current browser. They are independent of your login.
.field
=> label f, :hidpi, "Serve HiDPI thumbnails"
=> checkbox f, :hidpi
.fieldlabel: i Use high quality thumbnails on displays with a high pixel density. Requires more data than regular thumbnails.
.field
=> label f, :serve_webm, "Serve WebM"
=> checkbox f, :serve_webm
.fieldlabel: i Serve WebM/MP4 versions of GIF images when available. Good for lower-bandwidth connections, but the video versions may have missing start/end frames, and do not support transparency.
.field
=> label f, :webm, "Use video thumbnails"
=> checkbox f, :webm
.fieldlabel: i Use video thumbnails for WebM videos. Does not apply to GIF images.
.field
=> label f, :hide_uploader
=> checkbox f, :hide_uploader
.fieldlabel: i Hide the uploader and date posted information on image pages.
.field
=> label f, :chan_nsfw, "Show NSFW channels"
=> checkbox f, :chan_nsfw
.fieldlabel: i Show streams marked as NSFW on the channels page.
= if !@conn.assigns.current_user do
.block__tab.hidden data-tab="join-the-herd"
p
' Consider
=> link "creating an account!", to: Routes.pow_registration_path(@conn, :new)
br
' You will be able to customize the number of images and comments you get on a single page, as well as change the appearance of the site with custom themes.
br
= submit "Save My Settings", class: "button"
br

View file

@ -2,6 +2,35 @@ defmodule PhilomenaWeb.ImageView do
use PhilomenaWeb, :view use PhilomenaWeb, :view
alias Philomena.Tags.Tag alias Philomena.Tags.Tag
alias Plug.Conn
def show_vote_counts?(%{hide_vote_counts: true}), do: false
def show_vote_counts?(_user), do: true
# this is a bit ridculous
def render_intent(conn, %{thumbnails_generated: false}, _size), do: :not_rendered
def render_intent(conn, image, size) do
uris = thumb_urls(image, false)
vid? = image.image_mime_type == "video/webm"
gif? = image.image_mime_type == "image/gif"
tags = Tag.display_order(image.tags) |> Enum.map_join(", ", & &1.name)
alt = "Size: #{image.image_width}x#{image.image_height} | Tagged: #{tags}"
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
cond do
hidpi? and not (gif? or vid?) ->
{:hidpi, uris[size], uris[:medium], alt}
not vid? or use_gif? ->
{:image, String.replace(uris[size], ".webm", ".gif"), alt}
true ->
{:video, uris[size], String.replace(uris[size], ".webm", ".mp4"), alt}
end
end
def thumb_urls(image, show_hidden) do def thumb_urls(image, show_hidden) do
%{ %{
@ -14,8 +43,22 @@ defmodule PhilomenaWeb.ImageView do
tall: thumb_url(image, show_hidden, :tall), tall: thumb_url(image, show_hidden, :tall),
full: pretty_url(image, true, false) full: pretty_url(image, true, false)
} }
|> append_gif_urls(image, show_hidden)
end end
defp append_gif_urls(urls, %{image_mime_type: "image/gif"} = image, show_hidden) do
full_url = thumb_url(image, show_hidden, :full)
Map.merge(
urls,
%{
webm: String.replace(full_url, ".gif", ".webm"),
mp4: String.replace(full_url, ".gif", ".mp4")
}
)
end
defp append_gif_urls(urls, _image, _show_hidden), do: urls
def thumb_url(image, show_hidden, name) do def thumb_url(image, show_hidden, name) do
%{year: year, month: month, day: day} = image.created_at %{year: year, month: month, day: day} = image.created_at
deleted = image.hidden_from_users deleted = image.hidden_from_users

View file

@ -5,6 +5,9 @@ defmodule PhilomenaWeb.LayoutView do
conn.assigns[:layout_class] || "layout--narrow" conn.assigns[:layout_class] || "layout--narrow"
end end
def container_class(%{use_centered_layout: true}), do: "layout--center-aligned"
def container_class(_user), do: nil
def render_time(conn) do def render_time(conn) do
(Time.diff(Time.utc_now(), conn.assigns[:start_time], :microsecond) / 1000.0) (Time.diff(Time.utc_now(), conn.assigns[:start_time], :microsecond) / 1000.0)
|> Float.round(3) |> Float.round(3)

View file

@ -0,0 +1,18 @@
defmodule PhilomenaWeb.SettingView do
use PhilomenaWeb, :view
def theme_options(conn) do
[
[key: "Default", value: "default", data: [theme_path: Routes.static_path(conn, "/css/default.css")]],
[key: "Dark", value: "dark", data: [theme_path: Routes.static_path(conn, "/css/dark.css")]],
[key: "Red", value: "red", data: [theme_path: Routes.static_path(conn, "/css/red.css")]]
]
end
def local_tab_class(conn) do
case conn.assigns.current_user do
nil -> ""
_user -> "hidden"
end
end
end

View file

@ -0,0 +1,88 @@
defmodule PhilomenaWeb.SettingControllerTest do
use PhilomenaWeb.ConnCase
alias Philomena.Settings
@create_attrs %{}
@update_attrs %{}
@invalid_attrs %{}
def fixture(:setting) do
{:ok, setting} = Settings.create_setting(@create_attrs)
setting
end
describe "index" do
test "lists all settings", %{conn: conn} do
conn = get(conn, Routes.setting_path(conn, :index))
assert html_response(conn, 200) =~ "Listing Settings"
end
end
describe "new setting" do
test "renders form", %{conn: conn} do
conn = get(conn, Routes.setting_path(conn, :new))
assert html_response(conn, 200) =~ "New Setting"
end
end
describe "create setting" do
test "redirects to show when data is valid", %{conn: conn} do
conn = post(conn, Routes.setting_path(conn, :create), setting: @create_attrs)
assert %{id: id} = redirected_params(conn)
assert redirected_to(conn) == Routes.setting_path(conn, :show, id)
conn = get(conn, Routes.setting_path(conn, :show, id))
assert html_response(conn, 200) =~ "Show Setting"
end
test "renders errors when data is invalid", %{conn: conn} do
conn = post(conn, Routes.setting_path(conn, :create), setting: @invalid_attrs)
assert html_response(conn, 200) =~ "New Setting"
end
end
describe "edit setting" do
setup [:create_setting]
test "renders form for editing chosen setting", %{conn: conn, setting: setting} do
conn = get(conn, Routes.setting_path(conn, :edit, setting))
assert html_response(conn, 200) =~ "Edit Setting"
end
end
describe "update setting" do
setup [:create_setting]
test "redirects when data is valid", %{conn: conn, setting: setting} do
conn = put(conn, Routes.setting_path(conn, :update, setting), setting: @update_attrs)
assert redirected_to(conn) == Routes.setting_path(conn, :show, setting)
conn = get(conn, Routes.setting_path(conn, :show, setting))
assert html_response(conn, 200)
end
test "renders errors when data is invalid", %{conn: conn, setting: setting} do
conn = put(conn, Routes.setting_path(conn, :update, setting), setting: @invalid_attrs)
assert html_response(conn, 200) =~ "Edit Setting"
end
end
describe "delete setting" do
setup [:create_setting]
test "deletes chosen setting", %{conn: conn, setting: setting} do
conn = delete(conn, Routes.setting_path(conn, :delete, setting))
assert redirected_to(conn) == Routes.setting_path(conn, :index)
assert_error_sent 404, fn ->
get(conn, Routes.setting_path(conn, :show, setting))
end
end
end
defp create_setting(_) do
setting = fixture(:setting)
{:ok, setting: setting}
end
end