From 8838b104032770311a2de861919a1782bb027284 Mon Sep 17 00:00:00 2001 From: Chili Palmer Date: Sat, 25 Apr 2026 21:08:39 +0200 Subject: [PATCH] feat: sidebar for media now shows thumbnails --- lib/bds/desktop/router.ex | 4 +++ lib/bds/desktop/shell_controller.ex | 53 +++++++++++++++++++++++++++++ priv/ui/app.css | 30 ++++++++++++++++ priv/ui/app.js | 48 +++++++++++++++++++++++++- test/bds/desktop_test.exs | 32 +++++++++++++++++ test/bds/ui/shell_test.exs | 4 +++ 6 files changed, 170 insertions(+), 1 deletion(-) diff --git a/lib/bds/desktop/router.ex b/lib/bds/desktop/router.ex index 7a49184..efd854e 100644 --- a/lib/bds/desktop/router.ex +++ b/lib/bds/desktop/router.ex @@ -42,6 +42,10 @@ defmodule BDS.Desktop.Router do |> Plug.Conn.send_resp(200, BDS.Desktop.ShellController.projects_json()) end + get "/api/media-thumbnail/:media_id" do + BDS.Desktop.ShellController.media_thumbnail(conn, media_id, conn.params) + end + post "/api/sidebar" do {:ok, body, conn} = Plug.Conn.read_body(conn) payload = if body == "", do: %{}, else: Jason.decode!(body) diff --git a/lib/bds/desktop/shell_controller.ex b/lib/bds/desktop/shell_controller.ex index 11b4192..b0f5db7 100644 --- a/lib/bds/desktop/shell_controller.ex +++ b/lib/bds/desktop/shell_controller.ex @@ -1,6 +1,10 @@ defmodule BDS.Desktop.ShellController do @moduledoc false + alias BDS.Media + alias BDS.Media.Media, as: MediaRecord + alias BDS.Projects + alias BDS.Repo alias BDS.UI.Sidebar def index_html do @@ -50,6 +54,18 @@ defmodule BDS.Desktop.ShellController do end end + def media_thumbnail(conn, media_id, params \\ %{}) when is_binary(media_id) do + case active_media_thumbnail(media_id, Map.get(params, "size") || Map.get(params, :size)) do + {:ok, content_type, path} -> + conn + |> Plug.Conn.put_resp_content_type(content_type) + |> Plug.Conn.send_file(200, path) + + :error -> + Plug.Conn.send_resp(conn, 404, "not found") + end + end + def sidebar_json(payload) when is_map(payload) do view = Map.get(payload, "view") || Map.get(payload, :view) filters = Map.get(payload, "filters") || Map.get(payload, :filters) || %{} @@ -187,6 +203,43 @@ defmodule BDS.Desktop.ShellController do defp present?(value) when is_binary(value), do: String.trim(value) != "" defp present?(_value), do: false + defp active_media_thumbnail(media_id, size) do + with %{} = project <- Projects.get_active_project(), + %MediaRecord{} = media <- Repo.get(MediaRecord, media_id), + true <- media.project_id == project.id, + relative_path when is_binary(relative_path) <- Media.thumbnail_paths(media)[thumbnail_size(size)], + absolute_path = Path.join(Projects.project_data_dir(project), relative_path), + true <- File.exists?(absolute_path) do + {:ok, thumbnail_content_type(relative_path), absolute_path} + else + _other -> :error + end + rescue + error in [Exqlite.Error, DBConnection.OwnershipError] -> + if match?(%Exqlite.Error{}, error) and not String.contains?(Exception.message(error), "no such table") do + reraise error, __STACKTRACE__ + end + + :error + end + + defp thumbnail_size(size) do + case to_string(size || "small") do + "medium" -> :medium + "large" -> :large + "ai" -> :ai + _other -> :small + end + end + + defp thumbnail_content_type(path) do + case Path.extname(path) do + ".jpg" -> "image/jpeg" + ".jpeg" -> "image/jpeg" + _other -> "image/webp" + end + end + defp blank_to_nil(value) when is_binary(value) do trimmed = String.trim(value) if trimmed == "", do: nil, else: trimmed diff --git a/priv/ui/app.css b/priv/ui/app.css index d644290..9dabe3b 100644 --- a/priv/ui/app.css +++ b/priv/ui/app.css @@ -1636,6 +1636,36 @@ button { font-size: 20px; } +.media-thumbnail.has-image { + position: relative; +} + +.media-thumbnail-fallback { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; +} + +.media-thumbnail-image { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: cover; + opacity: 0; + transition: opacity 0.15s ease; +} + +.media-thumbnail.is-loaded .media-thumbnail-image { + opacity: 1; +} + +.media-thumbnail.is-loaded .media-thumbnail-fallback { + opacity: 0; +} + .media-item-info { flex: 1; min-width: 0; diff --git a/priv/ui/app.js b/priv/ui/app.js index ebbf8a6..95c0d1e 100644 --- a/priv/ui/app.js +++ b/priv/ui/app.js @@ -439,6 +439,7 @@ function renderSidebarMediaItem(item, view) { const itemRoute = item.route || view.editor_route; const tabId = tabIdForItem(item, itemRoute); const active = tabRef && tabRef.type === itemRoute && tabRef.id === tabId; + const thumbnail = renderMediaThumbnail(item); return `