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 {
|
[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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user