fix: issue #5 now using full size images in editor

This commit is contained in:
2026-07-06 08:36:49 +02:00
parent c735c1afa7
commit a772e3abea
6 changed files with 121 additions and 12 deletions

View File

@@ -106,8 +106,8 @@
} }
[data-testid="media-editor"] .media-preview-image img { [data-testid="media-editor"] .media-preview-image img {
width: 100%; max-width: 100%;
height: 100%; max-height: 100%;
object-fit: contain; object-fit: contain;
border-radius: 4px; border-radius: 4px;
} }

View File

@@ -8,16 +8,45 @@ defmodule BDS.Desktop.MediaController do
alias BDS.Projects alias BDS.Projects
alias BDS.Repo alias BDS.Repo
@web_image_types ~w(image/jpeg image/png image/webp image/gif image/svg+xml image/avif)
def thumbnail(conn, %{"media_id" => media_id} = params) do def thumbnail(conn, %{"media_id" => media_id} = params) do
case active_media_thumbnail(media_id, Map.get(params, "size")) do send_media(conn, active_media_thumbnail(media_id, Map.get(params, "size")))
{:ok, content_type, path} -> end
def file(conn, %{"media_id" => media_id}) do
send_media(conn, active_media_file(media_id))
end
defp send_media(conn, {:ok, content_type, path}) do
conn conn
|> Plug.Conn.put_resp_content_type(content_type) |> Plug.Conn.put_resp_content_type(content_type)
|> Plug.Conn.send_file(200, path) |> Plug.Conn.send_file(200, path)
:error ->
send_resp(conn, 404, "not found")
end end
defp send_media(conn, :error), do: send_resp(conn, 404, "not found")
# Serves the original image at full resolution; formats browsers cannot
# render inline fall back to the large thumbnail.
defp active_media_file(media_id) do
with %{} = project <- Projects.get_active_project(),
%MediaRecord{} = media <- Repo.get(MediaRecord, media_id),
true <- media.project_id == project.id,
true <- media.mime_type in @web_image_types,
absolute_path = Path.join(Projects.project_data_dir(project), media.file_path),
true <- File.exists?(absolute_path) do
{:ok, media.mime_type, absolute_path}
else
_other -> active_media_thumbnail(media_id, "large")
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 end
defp active_media_thumbnail(media_id, size) do defp active_media_thumbnail(media_id, size) do

View File

@@ -19,6 +19,7 @@ defmodule BDS.Desktop.Router do
get("/health", HealthController, :show) get("/health", HealthController, :show)
get("/media-thumbnail/:media_id", MediaController, :thumbnail) get("/media-thumbnail/:media_id", MediaController, :thumbnail)
get("/media-file/:media_id", MediaController, :file)
live_session :desktop_shell, live_session :desktop_shell,
root_layout: {BDS.Desktop.Layouts, :root} do root_layout: {BDS.Desktop.Layouts, :root} do

View File

@@ -645,7 +645,7 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
defp preview_url(%MediaRecord{} = media) do defp preview_url(%MediaRecord{} = media) do
if image?(media), if image?(media),
do: "/media-thumbnail/#{media.id}?size=large&t=#{media.updated_at}", do: "/media-file/#{media.id}?t=#{media.updated_at}",
else: nil else: nil
end end

View File

@@ -116,7 +116,11 @@ surface MediaEditorSurface {
-- Translate Metadata (globe icon). -- Translate Metadata (globe icon).
@guarantee PreviewArea @guarantee PreviewArea
-- Images: rendered via bds-media:// protocol with cache-busting timestamp. -- Images: rendered at full resolution via the /media-file/:id route
-- (serves the original file; falls back to the large thumbnail for
-- formats browsers cannot display inline) with cache-busting
-- timestamp. Never upscaled beyond natural size. The sidebar keeps
-- using small thumbnails.
-- Non-images: SVG file icon placeholder + original filename text. -- Non-images: SVG file icon placeholder + original filename text.
@guarantee MetadataForm @guarantee MetadataForm

View File

@@ -492,6 +492,81 @@ defmodule BDS.DesktopTest do
assert byte_size(conn.resp_body) > 0 assert byte_size(conn.resp_body) > 0
end end
test "desktop endpoint serves the original media file for the editor preview" do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
temp_dir =
Path.join(System.tmp_dir!(), "bds-desktop-media-file-#{System.unique_integer([:positive])}")
File.mkdir_p!(temp_dir)
on_exit(fn ->
File.rm_rf(temp_dir)
end)
{:ok, project} =
BDS.Projects.create_project(%{name: "Desktop Media File", data_path: temp_dir})
{:ok, _active} = BDS.Projects.set_active_project(project.id)
source_path = Path.join(temp_dir, "sample.jpg")
original_bytes = tiny_jpeg_binary()
File.write!(source_path, original_bytes)
assert {:ok, media} =
BDS.Media.import_media(%{project_id: project.id, source_path: source_path})
conn = conn(:get, "/media-file/#{media.id}?k=#{Desktop.Auth.login_key()}")
conn = BDS.Desktop.Endpoint.call(conn, BDS.Desktop.Endpoint.init([]))
assert conn.status == 200
assert [content_type] = Plug.Conn.get_resp_header(conn, "content-type")
assert String.starts_with?(content_type, "image/jpeg")
assert conn.resp_body == original_bytes
end
test "desktop endpoint falls back to the large thumbnail for non-web-displayable media" do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
temp_dir =
Path.join(
System.tmp_dir!(),
"bds-desktop-media-fallback-#{System.unique_integer([:positive])}"
)
File.mkdir_p!(temp_dir)
on_exit(fn ->
File.rm_rf(temp_dir)
end)
{:ok, project} =
BDS.Projects.create_project(%{name: "Desktop Media Fallback", data_path: temp_dir})
{:ok, _active} = BDS.Projects.set_active_project(project.id)
source_path = Path.join(temp_dir, "sample.jpg")
File.write!(source_path, tiny_jpeg_binary())
assert {:ok, media} =
BDS.Media.import_media(%{project_id: project.id, source_path: source_path})
# Simulate a format browsers cannot render inline (e.g. TIFF): thumbnails
# exist from import, but the original must not be served directly.
{:ok, _media} =
media
|> Ecto.Changeset.change(mime_type: "image/tiff")
|> BDS.Repo.update()
conn = conn(:get, "/media-file/#{media.id}?k=#{Desktop.Auth.login_key()}")
conn = BDS.Desktop.Endpoint.call(conn, BDS.Desktop.Endpoint.init([]))
assert conn.status == 200
assert [content_type] = Plug.Conn.get_resp_header(conn, "content-type")
assert String.starts_with?(content_type, "image/webp")
assert byte_size(conn.resp_body) > 0
end
defp menu_item(groups, id) do defp menu_item(groups, id) do
groups groups
|> Enum.flat_map(& &1.items) |> Enum.flat_map(& &1.items)