chore: convert media to live component

This commit is contained in:
2026-05-03 13:09:27 +02:00
parent 9f17954ce3
commit 5bc2b4a338
7 changed files with 528 additions and 637 deletions

View File

@@ -161,13 +161,7 @@ defmodule BDS.Desktop.ShellLive do
|> assign(:tab_meta, %{}) |> assign(:tab_meta, %{})
|> assign(:project_menu_open, false) |> assign(:project_menu_open, false)
|> assign(:sidebar_filters_by_view, %{}) |> assign(:sidebar_filters_by_view, %{})
|> assign(:sidebar_filter_panels, %{}) |> assign(:sidebar_filter_panels, %{})
|> assign(:media_editor_drafts, %{})
|> assign(:media_editor_quick_actions_open, %{})
|> assign(:media_editor_post_pickers_open, %{})
|> assign(:media_editor_post_picker_queries, %{})
|> assign(:media_editor_save_states, %{})
|> assign(:media_editor_translation_forms, %{})
|> assign(:chat_editor_inputs, %{}) |> assign(:chat_editor_inputs, %{})
|> assign(:chat_model_selectors_open, %{}) |> assign(:chat_model_selectors_open, %{})
|> assign(:chat_editor_requests, %{}) |> assign(:chat_editor_requests, %{})
@@ -331,116 +325,6 @@ defmodule BDS.Desktop.ShellLive do
{:noreply, reload_shell(socket, workbench)} {:noreply, reload_shell(socket, workbench)}
end end
def handle_event("change_media_editor", %{"media_editor" => params}, socket) do
{:noreply, MediaEditor.update(socket, params, &reload_shell/2)}
end
def handle_event("save_media_editor", %{"id" => media_id}, socket) do
{:noreply,
MediaEditor.persist_socket(socket, media_id, &reload_shell/2, &append_output_entry/5)}
end
def handle_event("toggle_media_editor_quick_actions", %{"id" => media_id}, socket) do
{:noreply, MediaEditor.toggle_quick_actions(socket, media_id, &reload_shell/2)}
end
def handle_event("replace_media_editor_file", %{"id" => media_id}, socket) do
{:noreply,
MediaEditor.replace_file(socket, media_id, &reload_shell/2, &append_output_entry/5)}
end
def handle_event("detect_media_editor_language", %{"id" => media_id}, socket) do
{:noreply,
MediaEditor.detect_language(socket, media_id, &reload_shell/2, &append_output_entry/5)}
end
def handle_event("toggle_media_post_picker", %{"id" => media_id}, socket) do
{:noreply, MediaEditor.toggle_post_picker(socket, media_id, &reload_shell/2)}
end
def handle_event(
"change_media_post_picker",
%{"id" => media_id, "media_post_picker" => %{"query" => query}},
socket
) do
{:noreply, MediaEditor.set_post_picker_query(socket, media_id, query, &reload_shell/2)}
end
def handle_event("link_media_to_post", %{"id" => media_id, "post-id" => post_id}, socket) do
{:noreply,
MediaEditor.link_post(socket, media_id, post_id, &reload_shell/2, &append_output_entry/5)}
end
def handle_event("unlink_media_from_post", %{"id" => media_id, "post-id" => post_id}, socket) do
{:noreply,
MediaEditor.unlink_post(socket, media_id, post_id, &reload_shell/2, &append_output_entry/5)}
end
def handle_event("edit_media_translation", %{"id" => media_id, "language" => language}, socket) do
{:noreply, MediaEditor.edit_translation(socket, media_id, language, &reload_shell/2)}
end
def handle_event("change_media_translation", %{"media_translation" => params}, socket) do
case socket.assigns.current_tab do
%{type: :media, id: media_id} ->
{:noreply, MediaEditor.update_translation(socket, media_id, params, &reload_shell/2)}
_other ->
{:noreply, socket}
end
end
def handle_event("save_media_translation", %{"id" => media_id}, socket) do
{:noreply,
MediaEditor.save_translation(socket, media_id, &reload_shell/2, &append_output_entry/5)}
end
def handle_event(
"refresh_media_translation",
%{"id" => media_id, "language" => language},
socket
) do
{:noreply,
MediaEditor.refresh_translation(
socket,
media_id,
language,
&reload_shell/2,
&append_output_entry/5
)}
end
def handle_event(
"delete_media_translation",
%{"id" => media_id, "language" => language},
socket
) do
{:noreply,
MediaEditor.delete_translation(
socket,
media_id,
language,
&reload_shell/2,
&append_output_entry/5
)}
end
def handle_event("close_media_translation_editor", _params, socket) do
case socket.assigns.current_tab do
%{type: :media, id: media_id} ->
{:noreply,
socket
|> assign(
:media_editor_translation_forms,
Map.delete(socket.assigns.media_editor_translation_forms, media_id)
)
|> reload_shell(socket.assigns.workbench)}
_other ->
{:noreply, socket}
end
end
def handle_event("settings_shell_command", %{"action" => action}, socket) do def handle_event("settings_shell_command", %{"action" => action}, socket) do
{:noreply, apply_shell_command(socket, action)} {:noreply, apply_shell_command(socket, action)}
end end
@@ -693,11 +577,8 @@ defmodule BDS.Desktop.ShellLive do
%{type: :media, id: media_id} %{type: :media, id: media_id}
when kind in ["ai_suggestions", "language_picker", "confirm_delete"] -> when kind in ["ai_suggestions", "language_picker", "confirm_delete"] ->
assign( send_update(__MODULE__.MediaEditor, id: "media-editor-#{media_id}", action: :close_quick_actions)
socket, socket
:media_editor_quick_actions_open,
Map.put(socket.assigns.media_editor_quick_actions_open, media_id, false)
)
_other -> _other ->
socket socket
@@ -868,7 +749,8 @@ defmodule BDS.Desktop.ShellLive do
socket socket
{%{kind: :language_picker}, %{type: :media, id: media_id}} -> {%{kind: :language_picker}, %{type: :media, id: media_id}} ->
MediaEditor.translate(socket, media_id, code, &reload_shell/2, &append_output_entry/5) send_update(MediaEditor, id: "media-editor-#{media_id}", action: :translate, language: code)
socket
_other -> _other ->
socket socket
@@ -890,16 +772,29 @@ defmodule BDS.Desktop.ShellLive do
socket socket
{%{kind: :ai_suggestions} = overlay, %{type: :media, id: media_id}} -> {%{kind: :ai_suggestions} = overlay, %{type: :media, id: media_id}} ->
MediaEditor.apply_ai_suggestions( send_update(MediaEditor, id: "media-editor-#{media_id}",
socket, action: :apply_ai_suggestions,
media_id, fields: Overlay.selected_ai_fields(overlay)
Overlay.selected_ai_fields(overlay),
&reload_shell/2,
&append_output_entry/5
) )
socket
{%{kind: :confirm_delete}, %{type: :media, id: media_id}} -> {%{kind: :confirm_delete}, %{type: :media, id: media_id}} ->
MediaEditor.delete_socket(socket, media_id, &reload_shell/2, &append_output_entry/5) case Media.delete_media(media_id) do
{:ok, :deleted} ->
workbench = Workbench.close_tab(socket.assigns.workbench, :media, media_id)
socket
|> assign(:shell_overlay, nil)
|> assign(:tab_meta, Map.delete(socket.assigns.tab_meta, {:media, media_id}))
|> reload_shell(workbench)
{:error, reason} ->
socket
|> assign(:shell_overlay, nil)
|> append_output_entry(translated("Delete Media"), inspect(reason), nil, "error")
|> reload_shell(socket.assigns.workbench)
end
{%{kind: :confirm_delete, title: title, entity_name: entity_name}, _tab} -> {%{kind: :confirm_delete, title: title, entity_name: entity_name}, _tab} ->
close_overlay_with_output(socket, title, entity_name) close_overlay_with_output(socket, title, entity_name)
@@ -1304,6 +1199,28 @@ defmodule BDS.Desktop.ShellLive do
{:noreply, socket} {:noreply, socket}
end end
def handle_info({:media_editor_output, title, message, level}, socket) do
{:noreply, append_output_entry(socket, title, message, nil, level)}
end
def handle_info({:media_editor_dirty, media_id, dirty?}, socket) do
workbench =
if dirty? do
Workbench.mark_dirty(socket.assigns.workbench, :media, media_id)
else
Workbench.clear_dirty(socket.assigns.workbench, :media, media_id)
end
{:noreply, assign(socket, :workbench, workbench)}
end
def handle_info({:media_editor_tab_meta, media_id, title, subtitle}, socket) do
tab_meta =
Map.put(socket.assigns.tab_meta, {:media, media_id}, %{title: title, subtitle: subtitle})
{:noreply, assign(socket, :tab_meta, tab_meta)}
end
def handle_info(:reload_shell, socket) do def handle_info(:reload_shell, socket) do
{:noreply, reload_shell(socket, socket.assigns.workbench)} {:noreply, reload_shell(socket, socket.assigns.workbench)}
end end
@@ -1380,7 +1297,6 @@ defmodule BDS.Desktop.ShellLive do
|> assign(:menu_groups, socket.assigns[:menu_groups] || TitlebarMenu.groups()) |> assign(:menu_groups, socket.assigns[:menu_groups] || TitlebarMenu.groups())
|> assign(:titlebar_menu_item_index, socket.assigns[:titlebar_menu_item_index]) |> assign(:titlebar_menu_item_index, socket.assigns[:titlebar_menu_item_index])
|> assign(:current_tab, current_tab(workbench)) |> assign(:current_tab, current_tab(workbench))
|> assign_media_editor()
|> assign_chat_editor() |> assign_chat_editor()
|> assign_import_editor() |> assign_import_editor()
|> assign_misc_editor() |> assign_misc_editor()
@@ -1425,10 +1341,6 @@ defmodule BDS.Desktop.ShellLive do
Enum.find(tabs, &(&1.type == type and &1.id == id)) Enum.find(tabs, &(&1.type == type and &1.id == id))
end end
defp assign_media_editor(socket) do
MediaEditor.assign_socket(socket)
end
defp assign_chat_editor(socket) do defp assign_chat_editor(socket) do
ChatEditor.assign_socket(socket) ChatEditor.assign_socket(socket)
end end
@@ -1594,7 +1506,8 @@ defmodule BDS.Desktop.ShellLive do
end end
defp save_current_tab(%{assigns: %{current_tab: %{type: :media, id: media_id}}} = socket) do defp save_current_tab(%{assigns: %{current_tab: %{type: :media, id: media_id}}} = socket) do
MediaEditor.persist_socket(socket, media_id, &reload_shell/2, &append_output_entry/5) send_update(MediaEditor, id: "media-editor-#{media_id}", action: :save)
socket
end end
defp save_current_tab(%{assigns: %{current_tab: %{type: :settings}}} = socket) do defp save_current_tab(%{assigns: %{current_tab: %{type: :settings}}} = socket) do
@@ -1740,9 +1653,21 @@ defmodule BDS.Desktop.ShellLive do
end end
"media" -> "media" ->
socket case Media.delete_media(id) do
|> assign(:shell_overlay, nil) {:ok, :deleted} ->
|> MediaEditor.delete_socket(id, &reload_shell/2, &append_output_entry/5) workbench = Workbench.close_tab(socket.assigns.workbench, :media, id)
socket
|> assign(:shell_overlay, nil)
|> assign(:tab_meta, Map.delete(socket.assigns.tab_meta, {:media, id}))
|> reload_shell(workbench)
{:error, reason} ->
socket
|> assign(:shell_overlay, nil)
|> append_output_entry(translated("Delete Media"), inspect(reason), nil, "error")
|> reload_shell(socket.assigns.workbench)
end
"scripts" -> "scripts" ->
delete_sidebar_script(socket, id) delete_sidebar_script(socket, id)

View File

@@ -58,27 +58,6 @@ defmodule BDS.Desktop.ShellLive.CliSync do
|> assign(:workbench, workbench) |> assign(:workbench, workbench)
|> assign(:shell_overlay, nil) |> assign(:shell_overlay, nil)
|> assign(:tab_meta, Map.delete(socket.assigns.tab_meta, {:media, media_id})) |> assign(:tab_meta, Map.delete(socket.assigns.tab_meta, {:media, media_id}))
|> assign(:media_editor_drafts, Map.delete(socket.assigns.media_editor_drafts, media_id))
|> assign(
:media_editor_quick_actions_open,
Map.delete(socket.assigns.media_editor_quick_actions_open, media_id)
)
|> assign(
:media_editor_post_pickers_open,
Map.delete(socket.assigns.media_editor_post_pickers_open, media_id)
)
|> assign(
:media_editor_post_picker_queries,
Map.delete(socket.assigns.media_editor_post_picker_queries, media_id)
)
|> assign(
:media_editor_save_states,
Map.delete(socket.assigns.media_editor_save_states, media_id)
)
|> assign(
:media_editor_translation_forms,
Map.delete(socket.assigns.media_editor_translation_forms, media_id)
)
{socket, workbench} {socket, workbench}
end end

View File

@@ -385,8 +385,8 @@
<% @current_tab.type == :post -> %> <% @current_tab.type == :post -> %>
<.live_component module={PostEditor} id={"post-editor-#{@current_tab.id}"} current_tab={@current_tab} offline_mode={@offline_mode} /> <.live_component module={PostEditor} id={"post-editor-#{@current_tab.id}"} current_tab={@current_tab} offline_mode={@offline_mode} />
<% @current_tab.type == :media and @media_editor -> %> <% @current_tab.type == :media -> %>
<MediaEditor.media_editor media_editor={@media_editor} /> <.live_component module={MediaEditor} id={"media-editor-#{@current_tab.id}"} current_tab={@current_tab} offline_mode={@offline_mode} />
<% @current_tab.type in [:settings, :style] and @current_project -> %> <% @current_tab.type in [:settings, :style] and @current_project -> %>
<.live_component module={SettingsEditor} id="settings-editor" <.live_component module={SettingsEditor} id="settings-editor"

File diff suppressed because it is too large Load Diff

View File

@@ -23,7 +23,7 @@
class="secondary quick-actions-btn" class="secondary quick-actions-btn"
type="button" type="button"
phx-click="toggle_media_editor_quick_actions" phx-click="toggle_media_editor_quick_actions"
phx-value-id={@media_editor.id} phx-target={@myself}
> >
<span class="quick-actions-btn-icon">⚡</span> <span class="quick-actions-btn-icon">⚡</span>
<span class="quick-actions-btn-label"><%= translated("Quick Actions") %></span> <span class="quick-actions-btn-label"><%= translated("Quick Actions") %></span>
@@ -53,7 +53,7 @@
class="quick-action-item" class="quick-action-item"
type="button" type="button"
phx-click="detect_media_editor_language" phx-click="detect_media_editor_language"
phx-value-id={@media_editor.id} phx-target={@myself}
disabled={not @media_editor.can_detect_language?} disabled={not @media_editor.can_detect_language?}
> >
<span class="quick-action-text"> <span class="quick-action-text">
@@ -83,10 +83,10 @@
<% end %> <% end %>
</div> </div>
<button class="secondary" type="button" phx-click="replace_media_editor_file" phx-value-id={@media_editor.id}> <button class="secondary" type="button" phx-click="replace_media_editor_file" phx-target={@myself}>
<%= translated("Replace File") %> <%= translated("Replace File") %>
</button> </button>
<button data-testid="media-save-button" type="button" phx-click="save_media_editor" phx-value-id={@media_editor.id}> <button data-testid="media-save-button" type="button" phx-click="save_media_editor" phx-target={@myself}>
<%= translated("Save") %> <%= translated("Save") %>
</button> </button>
<button <button
@@ -118,7 +118,7 @@
</div> </div>
<div class="media-details"> <div class="media-details">
<form class="media-editor-details-form" data-testid="media-editor-form" phx-change="change_media_editor"> <form class="media-editor-details-form" data-testid="media-editor-form" phx-change="change_media_editor" phx-target={@myself}>
<div class="editor-field"> <div class="editor-field">
<label><%= translated("File Name") %></label> <label><%= translated("File Name") %></label>
<input class="post-editor-input disabled" type="text" value={@media_editor.original_name} disabled /> <input class="post-editor-input disabled" type="text" value={@media_editor.original_name} disabled />
@@ -193,15 +193,15 @@
class="linked-post-title linked-post-link" class="linked-post-title linked-post-link"
type="button" type="button"
phx-click="edit_media_translation" phx-click="edit_media_translation"
phx-value-id={@media_editor.id} phx-target={@myself}
phx-value-language={translation.language} phx-value-language={translation.language}
> >
<%= translation.flag %> <%= language_label(translation.language) %><%= if translation.title, do: " — #{translation.title}" %> <%= translation.flag %> <%= language_label(translation.language) %><%= if translation.title, do: " — #{translation.title}" %>
</button> </button>
<button class="secondary compact" type="button" phx-click="refresh_media_translation" phx-value-id={@media_editor.id} phx-value-language={translation.language}> <button class="secondary compact" type="button" phx-click="refresh_media_translation" phx-target={@myself} phx-value-language={translation.language}>
<%= translated("Refresh") %> <%= translated("Refresh") %>
</button> </button>
<button class="unlink-btn" type="button" phx-click="delete_media_translation" phx-value-id={@media_editor.id} phx-value-language={translation.language}>×</button> <button class="unlink-btn" type="button" phx-click="delete_media_translation" phx-target={@myself} phx-value-language={translation.language}>×</button>
</div> </div>
<% end %> <% end %>
</div> </div>
@@ -212,7 +212,7 @@
<div class="editor-field linked-posts-section"> <div class="editor-field linked-posts-section">
<label> <label>
<%= translated("Linked Posts") %> <%= translated("Linked Posts") %>
<button class="add-link-btn" type="button" phx-click="toggle_media_post_picker" phx-value-id={@media_editor.id}> <button class="add-link-btn" type="button" phx-click="toggle_media_post_picker" phx-target={@myself}>
<%= translated("Link to Post") %> <%= translated("Link to Post") %>
</button> </button>
</label> </label>
@@ -226,7 +226,7 @@
value={@media_editor.post_picker_query} value={@media_editor.post_picker_query}
placeholder={translated("Search posts")} placeholder={translated("Search posts")}
phx-change="change_media_post_picker" phx-change="change_media_post_picker"
phx-value-id={@media_editor.id} phx-target={@myself}
/> />
</div> </div>
@@ -235,7 +235,7 @@
<% else %> <% else %>
<div class="post-picker-list"> <div class="post-picker-list">
<%= for result <- @media_editor.post_picker_results do %> <%= for result <- @media_editor.post_picker_results do %>
<button class="post-picker-item" type="button" phx-click="link_media_to_post" phx-value-id={@media_editor.id} phx-value-post-id={result.post_id}> <button class="post-picker-item" type="button" phx-click="link_media_to_post" phx-target={@myself} phx-value-post-id={result.post_id}>
<%= result.title %> <%= result.title %>
</button> </button>
<% end %> <% end %>
@@ -264,7 +264,7 @@
> >
📄 <%= linked_post.title %> 📄 <%= linked_post.title %>
</button> </button>
<button class="unlink-btn" type="button" phx-click="unlink_media_from_post" phx-value-id={@media_editor.id} phx-value-post-id={linked_post.post_id}>×</button> <button class="unlink-btn" type="button" phx-click="unlink_media_from_post" phx-target={@myself} phx-value-post-id={linked_post.post_id}>×</button>
</div> </div>
<% end %> <% end %>
</div> </div>
@@ -278,9 +278,9 @@
<div class="translation-modal"> <div class="translation-modal">
<div class="translation-modal-header"> <div class="translation-modal-header">
<h2><%= translated("Edit Translation") %></h2> <h2><%= translated("Edit Translation") %></h2>
<button class="translation-modal-close" type="button" phx-click="close_media_translation_editor">×</button> <button class="translation-modal-close" type="button" phx-click="close_media_translation_editor" phx-target={@myself}>×</button>
</div> </div>
<form class="translation-modal-body" phx-change="change_media_translation"> <form class="translation-modal-body" phx-change="change_media_translation" phx-target={@myself}>
<input type="hidden" name="media_translation[language]" value={@media_editor.editing_translation["language"]} /> <input type="hidden" name="media_translation[language]" value={@media_editor.editing_translation["language"]} />
<div class="editor-field"> <div class="editor-field">
<label><%= translated("Title") %></label> <label><%= translated("Title") %></label>
@@ -296,10 +296,10 @@
</div> </div>
</form> </form>
<div class="translation-modal-footer"> <div class="translation-modal-footer">
<button class="secondary" type="button" phx-click="close_media_translation_editor"><%= translated("Cancel") %></button> <button class="secondary" type="button" phx-click="close_media_translation_editor" phx-target={@myself}><%= translated("Cancel") %></button>
<button type="button" phx-click="save_media_translation" phx-value-id={@media_editor.id}><%= translated("Save") %></button> <button type="button" phx-click="save_media_translation" phx-target={@myself}><%= translated("Save") %></button>
</div> </div>
</div> </div>
</div> </div>
<% end %> <% end %>
</div> </div>

View File

@@ -2444,7 +2444,11 @@ defmodule BDS.Desktop.ShellLiveTest do
assert html =~ ~s(data-testid="media-editor") assert html =~ ~s(data-testid="media-editor")
html = render_click(view, "toggle_media_editor_quick_actions", %{"id" => media.id}) html =
view
|> element("[data-testid='media-editor'] .quick-actions-btn")
|> render_click()
assert html =~ "quick-actions-menu" assert html =~ "quick-actions-menu"
html = html =
@@ -2705,7 +2709,10 @@ defmodule BDS.Desktop.ShellLiveTest do
assert html =~ "Titelbild" assert html =~ "Titelbild"
refute html =~ "Desktop workbench content routed through the Elixir shell." refute html =~ "Desktop workbench content routed through the Elixir shell."
html = render_click(view, "toggle_media_editor_quick_actions", %{"id" => media.id}) html =
view
|> element("[data-testid='media-editor'] .quick-actions-btn")
|> render_click()
assert html =~ "quick-actions-menu" assert html =~ "quick-actions-menu"
assert html =~ "Detect Language" assert html =~ "Detect Language"
@@ -2727,7 +2734,10 @@ defmodule BDS.Desktop.ShellLiveTest do
assert html =~ "Updated Cover" assert html =~ "Updated Cover"
_html = render_click(view, "save_media_editor", %{"id" => media.id}) _html =
view
|> element("[data-testid='media-save-button']")
|> render_click()
saved_media = Repo.get!(BDS.Media.Media, media.id) saved_media = Repo.get!(BDS.Media.Media, media.id)
assert saved_media.title == "Updated Cover" assert saved_media.title == "Updated Cover"
@@ -2791,7 +2801,10 @@ defmodule BDS.Desktop.ShellLiveTest do
"[data-testid='media-editor'] .editor-content.media-editor .media-details .linked-posts-section" "[data-testid='media-editor'] .editor-content.media-editor .media-details .linked-posts-section"
) )
html = render_click(view, "edit_media_translation", %{"id" => media.id, "language" => "de"}) html =
view
|> element("[phx-click='edit_media_translation'][phx-value-language='de']")
|> render_click()
assert html =~ ~s(class="translation-modal-backdrop") assert html =~ ~s(class="translation-modal-backdrop")
assert html =~ ~s(class="translation-modal") assert html =~ ~s(class="translation-modal")

View File

@@ -401,8 +401,8 @@ defmodule BDS.UI.ShellTest do
media_editor_ex = media_editor_ex =
File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/media_editor.ex") File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/media_editor.ex")
assert template =~ "<MediaEditor.media_editor" assert template =~ "<.live_component module={MediaEditor}"
assert media_editor_ex =~ "def build(%{current_tab: %{type: :media, id: media_id}} = assigns)" assert media_editor_ex =~ "defp build_data(socket)"
refute live_ex =~ "defp update_media_editor(" refute live_ex =~ "defp update_media_editor("
refute live_ex =~ "defp persist_media_editor(" refute live_ex =~ "defp persist_media_editor("