fix: issue #5 now using full size images in editor
This commit is contained in:
@@ -106,8 +106,8 @@
|
||||
}
|
||||
|
||||
[data-testid="media-editor"] .media-preview-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@@ -8,16 +8,45 @@ defmodule BDS.Desktop.MediaController do
|
||||
alias BDS.Projects
|
||||
alias BDS.Repo
|
||||
|
||||
def thumbnail(conn, %{"media_id" => media_id} = params) do
|
||||
case active_media_thumbnail(media_id, Map.get(params, "size")) do
|
||||
{:ok, content_type, path} ->
|
||||
conn
|
||||
|> Plug.Conn.put_resp_content_type(content_type)
|
||||
|> Plug.Conn.send_file(200, path)
|
||||
@web_image_types ~w(image/jpeg image/png image/webp image/gif image/svg+xml image/avif)
|
||||
|
||||
:error ->
|
||||
send_resp(conn, 404, "not found")
|
||||
def thumbnail(conn, %{"media_id" => media_id} = params) do
|
||||
send_media(conn, active_media_thumbnail(media_id, Map.get(params, "size")))
|
||||
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
|
||||
|> Plug.Conn.put_resp_content_type(content_type)
|
||||
|> Plug.Conn.send_file(200, path)
|
||||
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
|
||||
|
||||
defp active_media_thumbnail(media_id, size) do
|
||||
|
||||
@@ -19,6 +19,7 @@ defmodule BDS.Desktop.Router do
|
||||
|
||||
get("/health", HealthController, :show)
|
||||
get("/media-thumbnail/:media_id", MediaController, :thumbnail)
|
||||
get("/media-file/:media_id", MediaController, :file)
|
||||
|
||||
live_session :desktop_shell,
|
||||
root_layout: {BDS.Desktop.Layouts, :root} do
|
||||
|
||||
@@ -645,7 +645,7 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
|
||||
|
||||
defp preview_url(%MediaRecord{} = media) do
|
||||
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
|
||||
end
|
||||
|
||||
|
||||
@@ -116,7 +116,11 @@ surface MediaEditorSurface {
|
||||
-- Translate Metadata (globe icon).
|
||||
|
||||
@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.
|
||||
|
||||
@guarantee MetadataForm
|
||||
|
||||
@@ -492,6 +492,81 @@ defmodule BDS.DesktopTest do
|
||||
assert byte_size(conn.resp_body) > 0
|
||||
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
|
||||
groups
|
||||
|> Enum.flat_map(& &1.items)
|
||||
|
||||
Reference in New Issue
Block a user