Skip to content

Commit

Permalink
[Enhancement] Add pagination for a source's media (#190)
Browse files Browse the repository at this point in the history
* [WIP] added first attempt at pagination component

* Hooked up pagination for downloaded media
  • Loading branch information
kieraneglin authored Apr 17, 2024
1 parent a4d5f45 commit aea40a3
Show file tree
Hide file tree
Showing 7 changed files with 247 additions and 72 deletions.
16 changes: 16 additions & 0 deletions lib/pinchflat/utils/number_utils.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
defmodule Pinchflat.Utils.NumberUtils do
@moduledoc """
Utility methods for working with numbers
"""

@doc """
Clamps a number between a minimum and maximum value
Returns integer() | float()
"""
def clamp(num, minimum, maximum) do
num
|> max(minimum)
|> min(maximum)
end
end
50 changes: 50 additions & 0 deletions lib/pinchflat_web/components/custom_components/table_components.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ defmodule PinchflatWeb.CustomComponents.TableComponents do
@moduledoc false
use Phoenix.Component

alias PinchflatWeb.CoreComponents

@doc """
Renders a table component with the given rows and columns.
Expand Down Expand Up @@ -50,4 +52,52 @@ defmodule PinchflatWeb.CustomComponents.TableComponents do
</table>
"""
end

@doc """
Renders simple pagination controls for a table in a liveview.
## Examples
<.live_pagination_controls page_number={@page} total_pages={@total_pages} />
"""
attr :page_number, :integer, default: 1
attr :total_pages, :integer, default: 1

def live_pagination_controls(assigns) do
~H"""
<nav>
<ul class="flex flex-wrap items-center">
<li>
<span
class={[
"flex h-8 w-8 items-center justify-center rounded",
@page_number != 1 && "cursor-pointer hover:bg-primary hover:text-white",
@page_number == 1 && "cursor-not-allowed"
]}
phx-click={@page_number != 1 && "page_change"}
phx-value-direction="dec"
>
<CoreComponents.icon name="hero-chevron-left" />
</span>
</li>
<li>
<span class="mx-2">Page <%= @page_number %> of <%= @total_pages %></span>
</li>
<li>
<span
class={[
"flex h-8 w-8 items-center justify-center rounded",
@page_number != @total_pages && "cursor-pointer hover:bg-primary hover:text-white",
@page_number == @total_pages && "cursor-not-allowed"
]}
phx-click={@page_number != @total_pages && "page_change"}
phx-value-direction="inc"
>
<CoreComponents.icon name="hero-chevron-right" />
</span>
</li>
</ul>
</nav>
"""
end
end
33 changes: 1 addition & 32 deletions lib/pinchflat_web/controllers/sources/source_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ defmodule PinchflatWeb.Sources.SourceController do
alias Pinchflat.Repo
alias Pinchflat.Tasks
alias Pinchflat.Sources
alias Pinchflat.MediaQuery
alias Pinchflat.Sources.Source
alias Pinchflat.Media.MediaQuery
alias Pinchflat.Profiles.MediaProfile
alias Pinchflat.Downloading.DownloadingHelpers
alias Pinchflat.SlowIndexing.SlowIndexingHelpers
Expand Down Expand Up @@ -60,29 +58,7 @@ defmodule PinchflatWeb.Sources.SourceController do
|> Tasks.list_tasks_for(nil, [:executing, :available, :scheduled, :retryable])
|> Repo.preload(:job)

pending_media =
MediaQuery.new()
|> MediaQuery.for_source(source)
|> MediaQuery.where_pending_download()
|> order_by(desc: :id)
|> limit(100)
|> Repo.all()

downloaded_media =
MediaQuery.new()
|> MediaQuery.for_source(source)
|> MediaQuery.with_media_filepath()
|> order_by(desc: :id)
|> limit(100)
|> Repo.all()

render(conn, :show,
source: source,
pending_tasks: pending_tasks,
pending_media: pending_media,
downloaded_media: downloaded_media,
total_downloaded: total_downloaded_for(source)
)
render(conn, :show, source: source, pending_tasks: pending_tasks)
end

def edit(conn, %{"id" => id}) do
Expand Down Expand Up @@ -151,13 +127,6 @@ defmodule PinchflatWeb.Sources.SourceController do
|> Repo.all()
end

defp total_downloaded_for(source) do
MediaQuery.new()
|> MediaQuery.for_source(source)
|> MediaQuery.with_media_filepath()
|> Repo.aggregate(:count, :id)
end

defp get_onboarding_layout do
if Settings.get!(:onboarding) do
{Layouts, :onboarding}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
defmodule Pinchflat.Sources.MediaItemTableLive do
use PinchflatWeb, :live_view
import Ecto.Query, warn: false

alias Pinchflat.Repo
alias Pinchflat.Sources
alias Pinchflat.Media.MediaQuery
alias Pinchflat.Utils.NumberUtils

@limit 10

def render(%{records: []} = assigns) do
~H"""
<p class="text-black dark:text-white">Nothing Here!</p>
"""
end

def render(assigns) do
~H"""
<div>
<span class="mb-4 inline-block">
Showing <%= length(@records) %> of <%= @total_record_count %>
</span>
<.table rows={@records} table_class="text-black dark:text-white">
<:col :let={media_item} label="Title">
<.subtle_link href={~p"/sources/#{@source.id}/media/#{media_item.id}"}>
<%= StringUtils.truncate(media_item.title, 50) %>
</.subtle_link>
</:col>
<:col :let={media_item} label="" class="flex place-content-evenly">
<.icon_link href={~p"/sources/#{@source.id}/media/#{media_item.id}"} icon="hero-eye" class="mx-1" />
<.icon_link href={~p"/sources/#{@source.id}/media/#{media_item.id}/edit"} icon="hero-pencil-square" class="mx-1" />
</:col>
</.table>
<section class="flex justify-center mt-5">
<.live_pagination_controls page_number={@page} total_pages={@total_pages} />
</section>
</div>
"""
end

def mount(_params, session, socket) do
page = 1
media_state = session["media_state"]
source = Sources.get_source!(session["source_id"])
base_query = generate_base_query(source, media_state)
pagination_attrs = fetch_pagination_attributes(base_query, page)

{:ok, assign(socket, Map.merge(pagination_attrs, %{base_query: base_query, source: source}))}
end

def handle_event("page_change", %{"direction" => direction}, %{assigns: assigns} = socket) do
direction = if direction == "inc", do: 1, else: -1
new_page = assigns.page + direction
new_assigns = fetch_pagination_attributes(assigns.base_query, new_page)

{:noreply, assign(socket, new_assigns)}
end

defp fetch_pagination_attributes(base_query, page) do
total_record_count = Repo.aggregate(base_query, :count, :id)
total_pages = max(ceil(total_record_count / @limit), 1)
page = NumberUtils.clamp(page, 1, total_pages)
records = fetch_records(base_query, page)

%{page: page, total_pages: total_pages, records: records, total_record_count: total_record_count}
end

defp fetch_records(base_query, page) do
offset = (page - 1) * @limit

base_query
|> limit(^@limit)
|> offset(^offset)
|> Repo.all()
end

defp generate_base_query(source, "pending") do
MediaQuery.new()
|> MediaQuery.for_source(source)
|> MediaQuery.where_pending_download()
|> order_by(desc: :id)
end

defp generate_base_query(source, "downloaded") do
MediaQuery.new()
|> MediaQuery.for_source(source)
|> MediaQuery.with_media_filepath()
|> order_by(desc: :id)
end
end
50 changes: 10 additions & 40 deletions lib/pinchflat_web/controllers/sources/source_html/show.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -37,48 +37,18 @@
</div>
</:tab>
<:tab title="Pending Media">
<%= if match?([_|_], @pending_media) do %>
<h4 class="text-white text-lg mb-6">Shows a maximum of 100 media items</h4>
<.table rows={@pending_media} table_class="text-black dark:text-white">
<:col :let={media_item} label="Title">
<.subtle_link href={~p"/sources/#{@source.id}/media/#{media_item.id}"}>
<%= StringUtils.truncate(media_item.title, 50) %>
</.subtle_link>
</:col>
<:col :let={media_item} label="" class="flex place-content-evenly">
<.icon_link href={~p"/sources/#{@source.id}/media/#{media_item.id}"} icon="hero-eye" class="mx-1" />
<.icon_link
href={~p"/sources/#{@source.id}/media/#{media_item.id}/edit"}
icon="hero-pencil-square"
class="mx-1"
/>
</:col>
</.table>
<% else %>
<p class="text-black dark:text-white">Nothing Here!</p>
<% end %>
<%= live_render(
@conn,
Pinchflat.Sources.MediaItemTableLive,
session: %{"source_id" => @source.id, "media_state" => "pending"}
) %>
</:tab>
<:tab title="Downloaded Media">
<%= if match?([_|_], @downloaded_media) do %>
<h4 class="text-white text-lg mb-6">Shows a maximum of 100 media items (<%= @total_downloaded %> total)</h4>
<.table rows={@downloaded_media} table_class="text-black dark:text-white">
<:col :let={media_item} label="Title">
<.subtle_link href={~p"/sources/#{@source.id}/media/#{media_item.id}"}>
<%= StringUtils.truncate(media_item.title, 50) %>
</.subtle_link>
</:col>
<:col :let={media_item} label="" class="flex place-content-evenly">
<.icon_link href={~p"/sources/#{@source.id}/media/#{media_item.id}"} icon="hero-eye" class="mx-1" />
<.icon_link
href={~p"/sources/#{@source.id}/media/#{media_item.id}/edit"}
icon="hero-pencil-square"
class="mx-1"
/>
</:col>
</.table>
<% else %>
<p class="text-black dark:text-white">Nothing Here!</p>
<% end %>
<%= live_render(
@conn,
Pinchflat.Sources.MediaItemTableLive,
session: %{"source_id" => @source.id, "media_state" => "downloaded"}
) %>
</:tab>
<:tab title="Pending Tasks">
<%= if match?([_|_], @pending_tasks) do %>
Expand Down
19 changes: 19 additions & 0 deletions test/pinchflat/utils/number_utils_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
defmodule Pinchflat.Utils.NumberUtilsTest do
use ExUnit.Case, async: true

alias Pinchflat.Utils.NumberUtils

describe "clamp/3" do
test "returns the minimum when the number is less than the minimum" do
assert NumberUtils.clamp(1, 2, 3) == 2
end

test "returns the maximum when the number is greater than the maximum" do
assert NumberUtils.clamp(4, 2, 3) == 3
end

test "returns the number when it is between the minimum and maximum" do
assert NumberUtils.clamp(2, 1, 3) == 2
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
defmodule PinchflatWeb.Sources.MediaItemTableLiveTest do
use PinchflatWeb.ConnCase

import Phoenix.LiveViewTest
import Pinchflat.MediaFixtures
import Pinchflat.SourcesFixtures

alias Pinchflat.Sources.MediaItemTableLive

setup do
source = source_fixture()

{:ok, source: source}
end

describe "initial rendering" do
test "shows message when no records", %{conn: conn, source: source} do
{:ok, _view, html} = live_isolated(conn, MediaItemTableLive, session: create_session(source))

assert html =~ "Nothing Here!"
refute html =~ "Showing"
end

test "shows records when present", %{conn: conn, source: source} do
media_item = media_item_fixture(source_id: source.id, media_filepath: nil)

{:ok, _view, html} = live_isolated(conn, MediaItemTableLive, session: create_session(source))

assert html =~ "Showing 1 of 1"
assert html =~ "Title"
assert html =~ media_item.title
end
end

describe "media_state" do
test "shows pending media when pending", %{conn: conn, source: source} do
downloaded_media_item = media_item_fixture(source_id: source.id)
pending_media_item = media_item_fixture(source_id: source.id, media_filepath: nil)

{:ok, _view, html} = live_isolated(conn, MediaItemTableLive, session: create_session(source, "pending"))

assert html =~ pending_media_item.title
refute html =~ downloaded_media_item.title
end

test "shows downloaded media when downloaded", %{conn: conn, source: source} do
downloaded_media_item = media_item_fixture(source_id: source.id)
pending_media_item = media_item_fixture(source_id: source.id, media_filepath: nil)

{:ok, _view, html} = live_isolated(conn, MediaItemTableLive, session: create_session(source, "downloaded"))

assert html =~ downloaded_media_item.title
refute html =~ pending_media_item.title
end
end

defp create_session(source, media_state \\ "pending") do
%{"source_id" => source.id, "media_state" => media_state}
end
end

0 comments on commit aea40a3

Please sign in to comment.