diff --git a/lib/pinchflat/utils/number_utils.ex b/lib/pinchflat/utils/number_utils.ex new file mode 100644 index 0000000..4a8404b --- /dev/null +++ b/lib/pinchflat/utils/number_utils.ex @@ -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 diff --git a/lib/pinchflat_web/components/custom_components/table_components.ex b/lib/pinchflat_web/components/custom_components/table_components.ex index fbe897c..532fba1 100644 --- a/lib/pinchflat_web/components/custom_components/table_components.ex +++ b/lib/pinchflat_web/components/custom_components/table_components.ex @@ -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. @@ -50,4 +52,52 @@ defmodule PinchflatWeb.CustomComponents.TableComponents do """ 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""" + + """ + end end diff --git a/lib/pinchflat_web/controllers/sources/source_controller.ex b/lib/pinchflat_web/controllers/sources/source_controller.ex index ff12534..6e4b215 100644 --- a/lib/pinchflat_web/controllers/sources/source_controller.ex +++ b/lib/pinchflat_web/controllers/sources/source_controller.ex @@ -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 @@ -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 @@ -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} diff --git a/lib/pinchflat_web/controllers/sources/source_html/media_item_table_live.ex b/lib/pinchflat_web/controllers/sources/source_html/media_item_table_live.ex new file mode 100644 index 0000000..bc2705c --- /dev/null +++ b/lib/pinchflat_web/controllers/sources/source_html/media_item_table_live.ex @@ -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""" +

Nothing Here!

+ """ + end + + def render(assigns) do + ~H""" +
+ + Showing <%= length(@records) %> of <%= @total_record_count %> + + <.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) %> + + + <: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" /> + + +
+ <.live_pagination_controls page_number={@page} total_pages={@total_pages} /> +
+
+ """ + 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 diff --git a/lib/pinchflat_web/controllers/sources/source_html/show.html.heex b/lib/pinchflat_web/controllers/sources/source_html/show.html.heex index d9687a1..3093bcf 100644 --- a/lib/pinchflat_web/controllers/sources/source_html/show.html.heex +++ b/lib/pinchflat_web/controllers/sources/source_html/show.html.heex @@ -37,48 +37,18 @@ <:tab title="Pending Media"> - <%= if match?([_|_], @pending_media) do %> -

Shows a maximum of 100 media items

- <.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) %> - - - <: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" - /> - - - <% else %> -

Nothing Here!

- <% end %> + <%= live_render( + @conn, + Pinchflat.Sources.MediaItemTableLive, + session: %{"source_id" => @source.id, "media_state" => "pending"} + ) %> <:tab title="Downloaded Media"> - <%= if match?([_|_], @downloaded_media) do %> -

Shows a maximum of 100 media items (<%= @total_downloaded %> total)

- <.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) %> - - - <: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" - /> - - - <% else %> -

Nothing Here!

- <% end %> + <%= live_render( + @conn, + Pinchflat.Sources.MediaItemTableLive, + session: %{"source_id" => @source.id, "media_state" => "downloaded"} + ) %> <:tab title="Pending Tasks"> <%= if match?([_|_], @pending_tasks) do %> diff --git a/test/pinchflat/utils/number_utils_test.exs b/test/pinchflat/utils/number_utils_test.exs new file mode 100644 index 0000000..48efe41 --- /dev/null +++ b/test/pinchflat/utils/number_utils_test.exs @@ -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 diff --git a/test/pinchflat_web/controllers/sources/media_item_table_live_test.exs b/test/pinchflat_web/controllers/sources/media_item_table_live_test.exs new file mode 100644 index 0000000..555738f --- /dev/null +++ b/test/pinchflat_web/controllers/sources/media_item_table_live_test.exs @@ -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