From 334ffe6f6ab9167495265bb59c56cb972d2a76e6 Mon Sep 17 00:00:00 2001 From: Chili Palmer Date: Sun, 26 Apr 2026 21:50:31 +0200 Subject: [PATCH] feat: better parity in layout for media and preferences --- lib/bds/desktop/shell_commands.ex | 50 ++ lib/bds/desktop/shell_live.ex | 29 + .../media_editor_html/media_editor.html.heex | 263 ++++----- lib/bds/desktop/shell_live/misc_editor.ex | 2 - lib/bds/desktop/shell_live/settings_editor.ex | 449 ++++++++++++++- .../settings_editor.html.heex | 266 ++++++++- priv/i18n/locales/de.json | 91 ++- priv/i18n/locales/en.json | 91 ++- priv/i18n/locales/es.json | 91 ++- priv/i18n/locales/fr.json | 91 ++- priv/i18n/locales/it.json | 91 ++- priv/ui/app.css | 518 ++++++++++++------ test/bds/desktop/shell_live_test.exs | 73 +++ test/bds/ui/shell_test.exs | 21 + 14 files changed, 1786 insertions(+), 340 deletions(-) diff --git a/lib/bds/desktop/shell_commands.ex b/lib/bds/desktop/shell_commands.ex index aa12845..2a02d8a 100644 --- a/lib/bds/desktop/shell_commands.ex +++ b/lib/bds/desktop/shell_commands.ex @@ -117,6 +117,56 @@ defmodule BDS.Desktop.ShellCommands do end) end + defp dispatch("rebuild_posts_from_files", project, _params) do + queue_task(project, "rebuild_posts_from_files", "Rebuild Posts From Files", "Maintenance", fn report -> + {:ok, posts} = Maintenance.rebuild_from_filesystem(project.id, "post", on_progress: report, rebuild_embeddings: false) + report.(1.0, "Post rebuild complete") + %{project_id: project.id, counts: %{posts: length(posts)}} + end) + end + + defp dispatch("rebuild_media_from_files", project, _params) do + queue_task(project, "rebuild_media_from_files", "Rebuild Media From Files", "Maintenance", fn report -> + {:ok, media} = Maintenance.rebuild_from_filesystem(project.id, "media", on_progress: report) + report.(1.0, "Media rebuild complete") + %{project_id: project.id, counts: %{media: length(media)}} + end) + end + + defp dispatch("rebuild_scripts_from_files", project, _params) do + queue_task(project, "rebuild_scripts_from_files", "Rebuild Scripts From Files", "Maintenance", fn report -> + {:ok, scripts} = Maintenance.rebuild_from_filesystem(project.id, "script", on_progress: report) + report.(1.0, "Script rebuild complete") + %{project_id: project.id, counts: %{scripts: length(scripts)}} + end) + end + + defp dispatch("rebuild_templates_from_files", project, _params) do + queue_task(project, "rebuild_templates_from_files", "Rebuild Templates From Files", "Maintenance", fn report -> + {:ok, templates} = Maintenance.rebuild_from_filesystem(project.id, "template", on_progress: report) + report.(1.0, "Template rebuild complete") + %{project_id: project.id, counts: %{templates: length(templates)}} + end) + end + + defp dispatch("rebuild_post_links", project, _params) do + queue_task(project, "rebuild_post_links", "Rebuild Post Links", "Maintenance", fn report -> + report.(0.0, "Rebuilding link graph") + :ok = Posts.rebuild_post_links(project.id) + report.(1.0, "Post links rebuilt") + %{project_id: project.id} + end) + end + + defp dispatch("regenerate_missing_thumbnails", project, _params) do + queue_task(project, "regenerate_missing_thumbnails", "Regenerate Missing Thumbnails", "Maintenance", fn report -> + report.(0.0, "Checking missing thumbnails") + result = BDS.Media.regenerate_missing_thumbnails(project.id) + report.(1.0, "Missing thumbnails regenerated") + Map.put(result, :project_id, project.id) + end) + end + defp dispatch("rebuild_database", project, _params) do group_id = task_group_id("rebuild_database") attrs = %{group_id: group_id, group_name: "Maintenance"} diff --git a/lib/bds/desktop/shell_live.ex b/lib/bds/desktop/shell_live.ex index 210da96..a9d8c05 100644 --- a/lib/bds/desktop/shell_live.ex +++ b/lib/bds/desktop/shell_live.ex @@ -509,6 +509,14 @@ defmodule BDS.Desktop.ShellLive do {:noreply, SettingsEditor.update_project_draft(socket, params, &reload_shell/2)} end + def handle_event("change_settings_editor", %{"settings_editor" => params}, socket) do + {:noreply, SettingsEditor.update_editor_draft(socket, params, &reload_shell/2)} + end + + def handle_event("save_settings_editor", _params, socket) do + {:noreply, SettingsEditor.save_editor(socket, &reload_shell/2, &append_output_entry/5)} + end + def handle_event("save_settings_project", _params, socket) do {:noreply, SettingsEditor.save_project(socket, &reload_shell/2, &append_output_entry/5)} end @@ -517,6 +525,18 @@ defmodule BDS.Desktop.ShellLive do {:noreply, SettingsEditor.update_publishing_draft(socket, params, &reload_shell/2)} end + def handle_event("change_settings_ai", %{"settings_ai" => params}, socket) do + {:noreply, SettingsEditor.update_ai_draft(socket, params, &reload_shell/2)} + end + + def handle_event("save_settings_ai", _params, socket) do + {:noreply, SettingsEditor.save_ai(socket, &reload_shell/2, &append_output_entry/5)} + end + + def handle_event("reset_settings_ai_prompt", _params, socket) do + {:noreply, SettingsEditor.reset_ai_prompt(socket, &reload_shell/2, &append_output_entry/5)} + end + def handle_event("save_settings_publishing", _params, socket) do {:noreply, SettingsEditor.save_publishing(socket, &reload_shell/2, &append_output_entry/5)} end @@ -533,6 +553,10 @@ defmodule BDS.Desktop.ShellLive do {:noreply, SettingsEditor.add_category(socket, &reload_shell/2, &append_output_entry/5)} end + def handle_event("reset_settings_categories", _params, socket) do + {:noreply, SettingsEditor.reset_categories(socket, &reload_shell/2, &append_output_entry/5)} + end + def handle_event("save_settings_category", %{"category_settings" => params}, socket) do {:noreply, SettingsEditor.save_category(socket, params, &reload_shell/2, &append_output_entry/5)} end @@ -545,6 +569,10 @@ defmodule BDS.Desktop.ShellLive do {:noreply, apply_shell_command(socket, action)} end + def handle_event("toggle_settings_mcp_agent", %{"agent" => agent}, socket) do + {:noreply, SettingsEditor.toggle_mcp_agent(socket, agent, &reload_shell/2, &append_output_entry/5)} + end + def handle_event("select_style_theme", %{"theme" => theme}, socket) do {:noreply, SettingsEditor.select_style_theme(socket, theme, &reload_shell/2)} end @@ -1326,6 +1354,7 @@ defmodule BDS.Desktop.ShellLive do tab_meta = Map.put(socket.assigns.tab_meta, {route_atom, tab_id}, %{ + sidebar_item_id: Map.get(params, "id"), title: Map.get(params, "title", ""), subtitle: Map.get(params, "subtitle", "") }) diff --git a/lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex b/lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex index a85edc0..19881c3 100644 --- a/lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex +++ b/lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex @@ -116,159 +116,160 @@ <% end %> -
-
- - -
- -
- - -
- -
+
+
- - + +
- <%= if @media_editor.dimensions do %> +
+ + +
+ +
- - + +
- <% end %> -
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - +
<% end %> - -
- -
+ - <%= if @media_editor.form["language"] not in [nil, ""] do %> -
- +
+ + +
- <%= if Enum.empty?(@media_editor.translations) do %> -
<%= translated("No translations") %>
- <% else %> -
- <%= for translation <- @media_editor.translations do %> -
- - - +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + <%= if @media_editor.form["language"] not in [nil, ""] do %> +
+ + + <%= if Enum.empty?(@media_editor.translations) do %> +
<%= translated("No translations") %>
+ <% else %> +
+ <%= for translation <- @media_editor.translations do %> +
+ + + +
+ <% end %>
<% end %>
<% end %> -
- <% end %> +
+ -
- + <%= if @media_editor.post_picker_open? do %> +
+
+ +
- <%= if @media_editor.post_picker_open? do %> -
-
- -
+ <%= if Enum.empty?(@media_editor.post_picker_results) do %> +
<%= translated("No posts to link") %>
+ <% else %> +
+ <%= for result <- @media_editor.post_picker_results do %> + + <% end %> + <%= if @media_editor.post_picker_overflow_count > 0 do %> +
<%= translated("and %{count} more", %{count: @media_editor.post_picker_overflow_count}) %>
+ <% end %> +
+ <% end %> +
+ <% end %> - <%= if Enum.empty?(@media_editor.post_picker_results) do %> -
<%= translated("No posts to link") %>
+ <%= if Enum.empty?(@media_editor.linked_posts) do %> +
<%= translated("Not linked to any posts") %>
<% else %> -
- <%= for result <- @media_editor.post_picker_results do %> - - <% end %> - <%= if @media_editor.post_picker_overflow_count > 0 do %> -
<%= translated("and %{count} more", %{count: @media_editor.post_picker_overflow_count}) %>
+
+ <%= for linked_post <- @media_editor.linked_posts do %> +
+ + +
<% end %>
<% end %>
- <% end %> - - <%= if Enum.empty?(@media_editor.linked_posts) do %> -
<%= translated("Not linked to any posts") %>
- <% else %> -
- <%= for linked_post <- @media_editor.linked_posts do %> -
- - -
- <% end %> -
- <% end %> +
<%= if @media_editor.editing_translation do %> diff --git a/lib/bds/desktop/shell_live/misc_editor.ex b/lib/bds/desktop/shell_live/misc_editor.ex index 5cbdb98..c0396c5 100644 --- a/lib/bds/desktop/shell_live/misc_editor.ex +++ b/lib/bds/desktop/shell_live/misc_editor.ex @@ -32,8 +32,6 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do {:rerun, socket |> append_output.(translated("Site Validation"), translated("Validation changes applied"), inspect(result))} - - {:error, reason} -> {:socket, append_output.(socket, translated("Site Validation"), inspect(reason), nil, "error")} end rescue error -> {:socket, append_output.(socket, translated("Site Validation"), inspect(error), nil, "error")} diff --git a/lib/bds/desktop/shell_live/settings_editor.ex b/lib/bds/desktop/shell_live/settings_editor.ex index 2b784cd..f93924b 100644 --- a/lib/bds/desktop/shell_live/settings_editor.ex +++ b/lib/bds/desktop/shell_live/settings_editor.ex @@ -3,11 +3,22 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do use Phoenix.Component + import Ecto.Query + + alias BDS.AI + alias BDS.AI.Model alias BDS.Metadata alias BDS.Desktop.ShellData + alias BDS.MCP.AgentConfig + alias BDS.Persistence + alias BDS.Repo + alias BDS.Settings.Setting + alias BDS.Templates.Template embed_templates "settings_editor_html/*" + @settings_sections ~w(project editor content ai technology publishing data mcp) + @themes [ "default", "amber", @@ -33,6 +44,21 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do @supported_languages ["en", "de", "fr", "it", "es"] @protected_categories MapSet.new(["article", "aside", "page", "picture"]) + @default_category_settings %{ + "article" => %{title: "article", render_in_lists: true, show_title: true}, + "picture" => %{title: "picture", render_in_lists: true, show_title: true}, + "aside" => %{title: "aside", render_in_lists: true, show_title: false}, + "page" => %{title: "page", render_in_lists: false, show_title: true} + } + @mcp_agents [ + %{id: :claude_code, label: "Claude Code", supported?: true}, + %{id: :claude_desktop, label: "Claude Desktop", supported?: false}, + %{id: :github_copilot, label: "GitHub Copilot", supported?: true}, + %{id: :gemini_cli, label: "Gemini CLI", supported?: false}, + %{id: :opencode, label: "OpenCode", supported?: false}, + %{id: :mistral_vibe, label: "Mistral Vibe", supported?: false}, + %{id: :openai_codex, label: "OpenAI Codex", supported?: false} + ] def assign_socket(socket) do case socket.assigns[:current_tab] do @@ -65,6 +91,30 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do |> reload.(socket.assigns.workbench) end + def update_editor_draft(socket, params, reload) do + socket + |> assign(:settings_editor_editor_draft, normalize_editor_params(params)) + |> reload.(socket.assigns.workbench) + end + + def save_editor(socket, reload, append_output) do + attrs = editor_attrs(socket.assigns) + + with :ok <- put_global_setting("ui.preferred_editor_mode", attrs.default_mode), + :ok <- put_global_setting("ui.git_diff_view_style", attrs.diff_view_style), + :ok <- put_global_setting("ui.git_diff_word_wrap", boolean_string(attrs.wrap_long_lines)), + :ok <- put_global_setting("ui.git_diff_hide_unchanged_regions", boolean_string(attrs.hide_unchanged_regions)) do + socket + |> assign(:settings_editor_editor_draft, %{}) + |> reload.(socket.assigns.workbench) + else + {:error, reason} -> + socket + |> append_output.(translated("Editor Settings"), inspect(reason), nil, "error") + |> reload.(socket.assigns.workbench) + end + end + def save_project(socket, reload, append_output) do project_id = socket.assigns.projects.active_project_id @@ -87,6 +137,50 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do |> reload.(socket.assigns.workbench) end + def update_ai_draft(socket, params, reload) do + socket + |> assign(:settings_editor_ai_draft, normalize_ai_params(params)) + |> reload.(socket.assigns.workbench) + end + + def save_ai(socket, reload, append_output) do + attrs = ai_attrs(socket.assigns) + + with :ok <- maybe_put_endpoint(:online, attrs.online_api_key, attrs.default_model), + :ok <- maybe_put_endpoint(:mistral, attrs.mistral_api_key, attrs.default_model), + :ok <- AI.set_airplane_mode(attrs.offline_mode), + :ok <- maybe_put_model_preference(:default, attrs.default_model), + :ok <- maybe_put_model_preference(:title, attrs.title_model), + :ok <- maybe_put_model_preference(:image_analysis, attrs.image_analysis_model), + :ok <- maybe_put_model_preference(:airplane_chat, attrs.offline_chat_model), + :ok <- maybe_put_model_preference(:airplane_title, attrs.offline_title_model), + :ok <- maybe_put_model_preference(:airplane_image_analysis, attrs.offline_image_analysis_model), + :ok <- put_global_setting("ai.system_prompt", attrs.system_prompt) do + socket + |> assign(:settings_editor_ai_draft, %{}) + |> reload.(socket.assigns.workbench) + else + {:error, reason} -> + socket + |> append_output.(translated("AI Settings"), inspect(reason), nil, "error") + |> reload.(socket.assigns.workbench) + end + end + + def reset_ai_prompt(socket, reload, append_output) do + case put_global_setting("ai.system_prompt", "") do + :ok -> + socket + |> assign(:settings_editor_ai_draft, %{}) + |> reload.(socket.assigns.workbench) + + {:error, reason} -> + socket + |> append_output.(translated("AI Settings"), inspect(reason), nil, "error") + |> reload.(socket.assigns.workbench) + end + end + def save_publishing(socket, reload, append_output) do project_id = socket.assigns.projects.active_project_id @@ -150,6 +244,54 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do end end + def reset_categories(socket, reload, append_output) do + project_id = socket.assigns.projects.active_project_id + + result = + Enum.reduce_while(category_names(project_metadata(socket.assigns)), :ok, fn category, _acc -> + if MapSet.member?(@protected_categories, category) do + {:cont, :ok} + else + case Metadata.remove_category(project_id, category) do + {:ok, _metadata} -> {:cont, :ok} + {:error, reason} -> {:halt, {:error, reason}} + end + end + end) + + with :ok <- result, + :ok <- ensure_default_categories(project_id), + :ok <- reset_default_category_settings(project_id) do + socket + |> assign(:settings_editor_new_category, "") + |> reload.(socket.assigns.workbench) + else + {:error, reason} -> + socket + |> append_output.(translated("Categories"), inspect(reason), nil, "error") + |> reload.(socket.assigns.workbench) + end + end + + def toggle_mcp_agent(socket, agent, reload, append_output) do + case find_mcp_agent(agent) do + %{id: agent_id, supported?: true} = config -> + if mcp_configured?(config) do + {:ok, _payload} = AgentConfig.remove_from_config(agent_id) + reload.(socket, socket.assigns.workbench) + else + install_root = Application.app_dir(:bds) + {:ok, _payload} = AgentConfig.add_to_config(agent_id, install_root: install_root) + reload.(socket, socket.assigns.workbench) + end + + _other -> + socket + |> append_output.(translated("MCP"), translated("This MCP agent is not supported in the rewrite yet"), nil, "error") + |> reload.(socket.assigns.workbench) + end + end + def save_category(socket, params, reload, append_output) do project_id = socket.assigns.projects.active_project_id category = Map.get(params, "category", "") @@ -221,18 +363,37 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do def build_settings(assigns) do metadata = project_metadata(assigns) project_form = Map.merge(project_form(metadata), Map.get(assigns, :settings_editor_project_draft, %{})) + editor_form = Map.merge(editor_form(), Map.get(assigns, :settings_editor_editor_draft, %{})) + ai_form = Map.merge(ai_form(), Map.get(assigns, :settings_editor_ai_draft, %{})) publishing_form = Map.merge(publishing_form(metadata), Map.get(assigns, :settings_editor_publishing_draft, %{})) query = Map.get(assigns, :settings_editor_search, "") + selected_section = current_settings_section(assigns) + visible_sections = visible_settings_sections(query) %{ search_query: query, + selected_section: selected_section, + active_sections: visible_sections, project: project_form, + editor: editor_form, categories: category_rows(metadata), + ai: ai_form, + technology: technology_form(project_form), publishing: publishing_form, + mcp: mcp_rows(), new_category: Map.get(assigns, :settings_editor_new_category, ""), + project_data_path: Map.get(assigns.current_project || %{}, :data_path) || "", + project_data_default_path: Map.get(assigns.current_project || %{}, :project_path) || "", + template_options: template_options(assigns.projects.active_project_id), + model_options: model_options(), + image_model_options: image_model_options(), project_visible?: section_matches?(query, ~w(project name description url language author category posts bookmarklet)), + editor_visible?: section_matches?(query, ~w(editor mode markdown preview diff wrap unchanged)), content_visible?: section_matches?(query, ~w(content categories templates lists blogmark)), + ai_visible?: section_matches?(query, ~w(ai assistant model prompt offline endpoint api key title image mistral anthropic)), + technology_visible?: section_matches?(query, ~w(technology runtime semantic similarity embedding scripting)), publishing_visible?: section_matches?(query, ~w(publishing ssh scp rsync host user remote path)), + mcp_visible?: section_matches?(query, ~w(mcp claude copilot gemini opencode mistral codex agent server)), data_visible?: section_matches?(query, ~w(data rebuild maintenance folder filesystem)), supported_languages: @supported_languages, protected_categories: @protected_categories @@ -281,6 +442,17 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do } end + defp editor_attrs(assigns) do + draft = Map.get(assigns, :settings_editor_editor_draft, %{}) + + %{ + default_mode: Map.get(draft, "default_mode", "markdown"), + diff_view_style: Map.get(draft, "diff_view_style", "inline"), + wrap_long_lines: truthy?(Map.get(draft, "wrap_long_lines")), + hide_unchanged_regions: truthy?(Map.get(draft, "hide_unchanged_regions")) + } + end + defp publishing_attrs(assigns) do draft = Map.get(assigns, :settings_editor_publishing_draft, %{}) @@ -292,10 +464,26 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do } end + defp ai_attrs(assigns) do + draft = Map.get(assigns, :settings_editor_ai_draft, %{}) + + %{ + online_api_key: blank_to_nil(Map.get(draft, "online_api_key")), + mistral_api_key: blank_to_nil(Map.get(draft, "mistral_api_key")), + offline_mode: truthy?(Map.get(draft, "offline_mode")), + default_model: blank_to_nil(Map.get(draft, "default_model")), + title_model: blank_to_nil(Map.get(draft, "title_model")), + image_analysis_model: blank_to_nil(Map.get(draft, "image_analysis_model")), + offline_chat_model: blank_to_nil(Map.get(draft, "offline_chat_model")), + offline_title_model: blank_to_nil(Map.get(draft, "offline_title_model")), + offline_image_analysis_model: blank_to_nil(Map.get(draft, "offline_image_analysis_model")), + system_prompt: Map.get(draft, "system_prompt", "") + } + end + defp project_metadata(assigns) do case Metadata.get_project_metadata(assigns.projects.active_project_id) do {:ok, metadata} -> metadata - _other -> %{} end end @@ -313,6 +501,15 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do } end + defp editor_form do + %{ + "default_mode" => get_global_setting("ui.preferred_editor_mode") || "markdown", + "diff_view_style" => get_global_setting("ui.git_diff_view_style") || "inline", + "wrap_long_lines" => get_global_setting("ui.git_diff_word_wrap") == "true", + "hide_unchanged_regions" => get_global_setting("ui.git_diff_hide_unchanged_regions") == "true" + } + end + defp publishing_form(metadata) do prefs = Map.get(metadata, :publishing_preferences, %{}) @@ -324,6 +521,30 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do } end + defp ai_form do + {:ok, online_endpoint} = AI.get_endpoint(:online) + {:ok, mistral_endpoint} = AI.get_endpoint(:mistral) + + %{ + "online_api_key" => Map.get(online_endpoint || %{}, :api_key, ""), + "mistral_api_key" => Map.get(mistral_endpoint || %{}, :api_key, ""), + "offline_mode" => AI.airplane_mode?(), + "default_model" => get_model_preference(:default), + "title_model" => get_model_preference(:title), + "image_analysis_model" => get_model_preference(:image_analysis), + "offline_chat_model" => get_model_preference(:airplane_chat), + "offline_title_model" => get_model_preference(:airplane_title), + "offline_image_analysis_model" => get_model_preference(:airplane_image_analysis), + "system_prompt" => get_global_setting("ai.system_prompt") || "" + } + end + + defp technology_form(project_form) do + %{ + "semantic_similarity_enabled" => Map.get(project_form, "semantic_similarity_enabled", false) + } + end + defp current_theme(assigns) do assigns |> project_metadata() @@ -354,6 +575,8 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do end) end + defp category_names(metadata), do: Map.get(metadata, :categories, []) + defp normalize_project_params(params) do %{ "name" => Map.get(params, "name", ""), @@ -368,6 +591,15 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do } end + defp normalize_editor_params(params) do + %{ + "default_mode" => Map.get(params, "default_mode", "markdown"), + "diff_view_style" => Map.get(params, "diff_view_style", "inline"), + "wrap_long_lines" => truthy?(Map.get(params, "wrap_long_lines")), + "hide_unchanged_regions" => truthy?(Map.get(params, "hide_unchanged_regions")) + } + end + defp normalize_publishing_params(params) do %{ "ssh_host" => Map.get(params, "ssh_host", ""), @@ -377,6 +609,217 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do } end + defp normalize_ai_params(params) do + %{ + "online_api_key" => Map.get(params, "online_api_key", ""), + "mistral_api_key" => Map.get(params, "mistral_api_key", ""), + "offline_mode" => truthy?(Map.get(params, "offline_mode")), + "default_model" => Map.get(params, "default_model", ""), + "title_model" => Map.get(params, "title_model", ""), + "image_analysis_model" => Map.get(params, "image_analysis_model", ""), + "offline_chat_model" => Map.get(params, "offline_chat_model", ""), + "offline_title_model" => Map.get(params, "offline_title_model", ""), + "offline_image_analysis_model" => Map.get(params, "offline_image_analysis_model", ""), + "system_prompt" => Map.get(params, "system_prompt", "") + } + end + + defp current_settings_section(assigns) do + meta = current_tab_meta(assigns) + + meta + |> Map.get(:sidebar_item_id, "settings-project") + |> to_string() + |> String.replace_prefix("settings-", "") + |> case do + section when section in @settings_sections -> section + _other -> "project" + end + end + + defp current_tab_meta(assigns) do + current_tab = Map.get(assigns, :current_tab) + + case current_tab do + %{type: type, id: id} -> Map.get(assigns[:tab_meta] || %{}, {type, id}, %{}) + _other -> %{} + end + end + + defp visible_settings_sections(query) do + Enum.filter(@settings_sections, fn section -> + case section do + "project" -> section_matches?(query, ~w(project name description data url language author bookmarklet)) + "editor" -> section_matches?(query, ~w(editor mode markdown preview diff wrap unchanged)) + "content" -> section_matches?(query, ~w(content categories templates lists blogmark)) + "ai" -> section_matches?(query, ~w(ai assistant model prompt offline endpoint api key title image mistral anthropic)) + "technology" -> section_matches?(query, ~w(technology semantic similarity runtime scripting embedding)) + "publishing" -> section_matches?(query, ~w(publishing ssh scp rsync host user remote path)) + "data" -> section_matches?(query, ~w(data rebuild maintenance links thumbnails filesystem)) + "mcp" -> section_matches?(query, ~w(mcp claude copilot gemini opencode mistral codex agent server)) + end + end) + end + + defp template_options(project_id) do + %{ + post: + Repo.all( + from template in Template, + where: template.project_id == ^project_id and template.kind == :post, + order_by: [asc: template.title], + select: %{slug: template.slug, title: template.title} + ), + list: + Repo.all( + from template in Template, + where: template.project_id == ^project_id and template.kind == :list, + order_by: [asc: template.title], + select: %{slug: template.slug, title: template.title} + ) + } + end + + defp model_options do + Repo.all( + from model in Model, + order_by: [asc: model.provider, asc: model.name], + select: %{ + id: model.model_id, + provider: model.provider, + name: model.name, + context_window: model.context_window, + max_output_tokens: model.max_output_tokens, + supports_attachment: model.supports_attachment + } + ) + |> Enum.map(fn model -> + Map.put(model, :label, model.provider <> " / " <> model.name) + end) + end + + defp image_model_options do + Enum.filter(model_options(), & &1.supports_attachment) + end + + defp mcp_rows do + Enum.map(@mcp_agents, fn agent -> + %{ + id: agent.id, + label: agent.label, + supported?: agent.supported?, + configured?: mcp_configured?(agent), + config_path: mcp_config_path(agent) + } + end) + end + + defp find_mcp_agent(agent) do + normalized = + agent + |> to_string() + |> String.to_existing_atom() + + Enum.find(@mcp_agents, &(&1.id == normalized)) + rescue + _error -> nil + end + + defp mcp_configured?(%{supported?: false}), do: false + + defp mcp_configured?(%{id: agent_id}) do + path = AgentConfig.config_path(agent_id, System.user_home!()) + + if File.exists?(path) do + path + |> File.read!() + |> Jason.decode!() + |> mcp_server_present?(agent_id) + else + false + end + rescue + _error -> false + end + + defp mcp_config_path(%{supported?: false}), do: nil + defp mcp_config_path(%{id: agent_id}), do: AgentConfig.config_path(agent_id, System.user_home!()) + + defp mcp_server_present?(config, :github_copilot) do + config + |> Map.get("servers", %{}) + |> Map.has_key?("bDS") + end + + defp mcp_server_present?(config, _agent_id) do + config + |> Map.get("mcpServers", %{}) + |> Map.has_key?("bDS") + end + + defp get_model_preference(key) do + case AI.get_model_preference(key) do + {:ok, value} -> value || "" + _other -> "" + end + end + + defp maybe_put_model_preference(_key, nil), do: :ok + defp maybe_put_model_preference(_key, ""), do: :ok + defp maybe_put_model_preference(key, value), do: AI.put_model_preference(key, value) + + defp maybe_put_endpoint(kind, nil, model) do + case model do + nil -> :ok + "" -> :ok + _other -> AI.put_endpoint(kind, %{model: model}) |> normalize_endpoint_result() + end + end + + defp maybe_put_endpoint(kind, api_key, model) do + AI.put_endpoint(kind, %{api_key: api_key, model: model}) |> normalize_endpoint_result() + end + + defp normalize_endpoint_result({:ok, _endpoint}), do: :ok + defp normalize_endpoint_result({:error, reason}), do: {:error, reason} + + defp ensure_default_categories(project_id) do + Enum.reduce_while(Map.keys(@default_category_settings), :ok, fn category, _acc -> + case Metadata.add_category(project_id, category) do + {:ok, _metadata} -> {:cont, :ok} + {:error, reason} -> {:halt, {:error, reason}} + end + end) + end + + defp reset_default_category_settings(project_id) do + Enum.reduce_while(@default_category_settings, :ok, fn {category, settings}, _acc -> + case Metadata.update_category_settings(project_id, category, settings) do + {:ok, _metadata} -> {:cont, :ok} + {:error, reason} -> {:halt, {:error, reason}} + end + end) + end + + defp get_global_setting(key) do + case Repo.get(Setting, key) do + %Setting{value: value} -> value + _other -> nil + end + end + + defp put_global_setting(key, value) do + setting = Repo.get(Setting, key) || %Setting{} + + setting + |> Setting.changeset(%{key: key, value: to_string(value || ""), updated_at: Persistence.now_ms()}) + |> Repo.insert_or_update() + |> case do + {:ok, _setting} -> :ok + {:error, reason} -> {:error, reason} + end + end + defp section_matches?("", _keywords), do: true defp section_matches?(query, keywords), do: Enum.any?(keywords, &String.contains?(&1, String.downcase(query))) @@ -390,6 +833,8 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do end defp truthy?(value), do: value in [true, "true", "on", "1", 1] + defp boolean_string(true), do: "true" + defp boolean_string(false), do: "false" defp parse_integer(nil, fallback), do: fallback defp parse_integer(value, _fallback) when is_integer(value), do: value defp parse_integer(value, fallback) do @@ -406,4 +851,4 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do trimmed -> trimmed end end -end \ No newline at end of file +end diff --git a/lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex b/lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex index 95631ad..218b168 100644 --- a/lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex +++ b/lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex @@ -1,4 +1,4 @@ -
+

<%= translated("Settings") %>

@@ -8,7 +8,7 @@
- <%= if not @settings_editor.project_visible? and not @settings_editor.content_visible? and not @settings_editor.publishing_visible? and not @settings_editor.data_visible? do %> + <%= if Enum.empty?(@settings_editor.active_sections) do %>

<%= translated("No settings match the current search") %>

@@ -18,6 +18,7 @@

<%= translated("Project") %>

+

<%= translated("Blog identity, URLs, authoring defaults, and bookmarklet setup") %>

@@ -30,6 +31,15 @@
+
+
+
+
+ + +
+
+
@@ -76,34 +86,107 @@
-
-
- -
+
+

<%= translated("Bookmarklet copy support is wired through the desktop runtime and project public URL.") %>

<% end %> + <%= if @settings_editor.editor_visible? do %> +
+
+

<%= translated("Editor") %>

+

<%= translated("Default editing mode and diff presentation") %>

+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+ <% end %> + <%= if @settings_editor.content_visible? do %>
-

<%= translated("Content Categories") %>

+

<%= translated("Content Categories") %>

<%= translated("Category defaults, rendering flags, and template wiring") %>

- <%= for category <- @settings_editor.categories do %> -
- -
-
-
- - - - -
-
-
- <% end %> + + + + + + + + + + + + + + <%= for category <- @settings_editor.categories do %> + + + + + + + + + + <% end %> + +
<%= translated("Category") %><%= translated("Title") %><%= translated("Render in Lists") %><%= translated("Show Titles") %><%= translated("Post Template") %><%= translated("List Template") %><%= translated("Actions") %>
<%= category.name %> + + + + + + +
+
+ +
+ + +
+
@@ -113,13 +196,122 @@
+
<% end %> + <%= if @settings_editor.ai_visible? do %> +
+

<%= translated("AI") %>

<%= translated("Provider keys, model preferences, airplane mode, and system prompt") %>

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+ <% end %> + + <%= if @settings_editor.technology_visible? do %> +
+

<%= translated("Technology") %>

<%= translated("Application-level runtime behavior and semantic indexing") %>

+
+
+
+
+
+
+
+

<%= translated("Scripting capabilities are configured at the application layer in the rewrite and do not expose runtime switching here.") %>

+
+
+
+
+ <% end %> + <%= if @settings_editor.publishing_visible? do %>
-

<%= translated("Publishing") %>

+

<%= translated("Publishing") %>

<%= translated("Deployment credentials for upload tasks") %>

@@ -130,11 +322,37 @@
<% end %> + <%= if @settings_editor.mcp_visible? do %> +
+

<%= translated("MCP") %>

<%= translated("Agent configuration files for the built-in bDS MCP server") %>

+
+ <%= for agent <- @settings_editor.mcp do %> +
+
+ +

<%= agent.config_path || translated("Not supported in the rewrite yet") %>

+
+
+ +
+
+ <% end %> +
+
+ <% end %> + <%= if @settings_editor.data_visible? do %>
-

<%= translated("Data Maintenance") %>

+

<%= translated("Data Maintenance") %>

<%= translated("Rebuild filesystem-backed records and thumbnails") %>

- + + + + + +
diff --git a/priv/i18n/locales/de.json b/priv/i18n/locales/de.json index fb0f71c..b4978e8 100644 --- a/priv/i18n/locales/de.json +++ b/priv/i18n/locales/de.json @@ -249,5 +249,94 @@ "Ask the assistant about the active project or editor.": "Frage den Assistenten zum aktiven Projekt oder Editor.", "Start chat": "Chat starten", "You": "Du", - "The assistant sidebar chat surface is ready, but model execution is not connected yet.": "Die Chat-Oberfläche der Assistenten-Seitenleiste ist bereit, aber die Modellausführung ist noch nicht verbunden." + "The assistant sidebar chat surface is ready, but model execution is not connected yet.": "Die Chat-Oberfläche der Assistenten-Seitenleiste ist bereit, aber die Modellausführung ist noch nicht verbunden.", + "Search settings": "Einstellungen durchsuchen", + "No settings match the current search": "Keine Einstellungen entsprechen der aktuellen Suche", + "Blog identity, URLs, authoring defaults, and bookmarklet setup": "Blog-Identität, URLs, Autorenvorgaben und Bookmarklet-Einrichtung", + "Project Name": "Projektname", + "Description": "Beschreibung", + "Data Path": "Datenpfad", + "Public URL": "Öffentliche URL", + "Blog Languages": "Blog-Sprachen", + "Default Author": "Standardautor", + "Max Posts Per Page": "Maximale Beiträge pro Seite", + "Blogmark Category": "Blogmark-Kategorie", + "Blogmark Bookmarklet": "Blogmark-Bookmarklet", + "Bookmarklet copy support is wired through the desktop runtime and project public URL.": "Die Bookmarklet-Kopierfunktion ist über die Desktop-Laufzeit und die öffentliche Projekt-URL verdrahtet.", + "Default editing mode and diff presentation": "Standard-Bearbeitungsmodus und Diff-Darstellung", + "Default Editor Mode": "Standard-Bearbeitungsmodus", + "Diff View Style": "Diff-Ansicht", + "Inline": "Inline", + "Side by Side": "Nebeneinander", + "Wrap Long Lines": "Lange Zeilen umbrechen", + "Enable line wrapping in diffs": "Zeilenumbruch in Diffs aktivieren", + "Hide Unchanged Regions": "Unveränderte Bereiche ausblenden", + "Collapse unchanged diff hunks": "Unveränderte Diff-Blöcke einklappen", + "Content Categories": "Inhaltskategorien", + "Category defaults, rendering flags, and template wiring": "Kategorie-Standards, Render-Flags und Template-Zuordnung", + "Category": "Kategorie", + "Render in Lists": "In Listen rendern", + "Show Titles": "Titel anzeigen", + "Post Template": "Beitragsvorlage", + "List Template": "Listen-Vorlage", + "Default": "Standard", + "Add Category": "Kategorie hinzufügen", + "Reset to Defaults": "Auf Standard zurücksetzen", + "Provider keys, model preferences, airplane mode, and system prompt": "Provider-Schlüssel, Modellvorgaben, Flugmodus und System-Prompt", + "Anthropic / Online API Key": "Anthropic-/Online-API-Schlüssel", + "Mistral API Key": "Mistral-API-Schlüssel", + "Offline Mode": "Offline-Modus", + "Route AI tasks through the airplane endpoint": "KI-Aufgaben über den Flugmodus-Endpunkt leiten", + "Default Model": "Standardmodell", + "Title Model": "Titelmodell", + "Image Analysis Model": "Bildanalysemodell", + "Offline Chat Model": "Offline-Chatmodell", + "Offline Title Model": "Offline-Titelmodell", + "Offline Image Analysis Model": "Offline-Bildanalysemodell", + "System Prompt": "System-Prompt", + "Reset to Default": "Auf Standard zurücksetzen", + "Application-level runtime behavior and semantic indexing": "Anwendungsverhalten zur Laufzeit und semantische Indizierung", + "Semantic Similarity": "Semantische Ähnlichkeit", + "Enable duplicate search and related-post embeddings": "Duplikatsuche und Embeddings für verwandte Beiträge aktivieren", + "Scripting Runtime": "Scripting-Laufzeit", + "Scripting capabilities are configured at the application layer in the rewrite and do not expose runtime switching here.": "Scripting-Funktionen werden in der Neufassung auf Anwendungsebene konfiguriert und bieten hier keine Umschaltung der Laufzeit.", + "Deployment credentials for upload tasks": "Bereitstellungszugangsdaten für Upload-Aufgaben", + "SSH Mode": "SSH-Modus", + "Host": "Host", + "Username": "Benutzername", + "Remote Path": "Remote-Pfad", + "Agent configuration files for the built-in bDS MCP server": "Agent-Konfigurationsdateien für den integrierten bDS-MCP-Server", + "Not supported in the rewrite yet": "In der Neufassung noch nicht unterstützt", + "Remove": "Entfernen", + "Add": "Hinzufügen", + "Data Maintenance": "Datenwartung", + "Rebuild filesystem-backed records and thumbnails": "Dateisystemgestützte Datensätze und Vorschaubilder neu aufbauen", + "Rebuild Posts From Files": "Beiträge aus Dateien neu aufbauen", + "Rebuild Media From Files": "Medien aus Dateien neu aufbauen", + "Rebuild Scripts From Files": "Skripte aus Dateien neu aufbauen", + "Rebuild Templates From Files": "Vorlagen aus Dateien neu aufbauen", + "Rebuild Links": "Links neu aufbauen", + "Regenerate Missing Thumbnails": "Fehlende Vorschaubilder neu erzeugen", + "Rebuild Embedding Index": "Embedding-Index neu aufbauen", + "Quick Actions": "Schnellaktionen", + "Review title, alt text, and caption suggestions": "Titel-, Alt-Text- und Beschriftungsvorschläge prüfen", + "Detect Language": "Sprache erkennen", + "Persist the detected language for this media item": "Die erkannte Sprache für dieses Medium speichern", + "Select a target language for this media item": "Eine Zielsprache für dieses Medium auswählen", + "Replace File": "Datei ersetzen", + "File Name": "Dateiname", + "MIME Type": "MIME-Typ", + "Size": "Größe", + "Dimensions": "Abmessungen", + "Author": "Autor", + "Language": "Sprache", + "None": "Keine", + "No translations": "Keine Übersetzungen", + "Refresh": "Aktualisieren", + "Linked Posts": "Verknüpfte Beiträge", + "Link to Post": "Mit Beitrag verknüpfen", + "Search posts": "Beiträge durchsuchen", + "No posts to link": "Keine Beiträge zum Verknüpfen", + "and %{count} more": "und %{count} weitere", + "Not linked to any posts": "Mit keinen Beiträgen verknüpft" } \ No newline at end of file diff --git a/priv/i18n/locales/en.json b/priv/i18n/locales/en.json index d9fd0bc..008cf69 100644 --- a/priv/i18n/locales/en.json +++ b/priv/i18n/locales/en.json @@ -249,5 +249,94 @@ "Ask the assistant about the active project or editor.": "Ask the assistant about the active project or editor.", "Start chat": "Start chat", "You": "You", - "The assistant sidebar chat surface is ready, but model execution is not connected yet.": "The assistant sidebar chat surface is ready, but model execution is not connected yet." + "The assistant sidebar chat surface is ready, but model execution is not connected yet.": "The assistant sidebar chat surface is ready, but model execution is not connected yet.", + "Search settings": "Search settings", + "No settings match the current search": "No settings match the current search", + "Blog identity, URLs, authoring defaults, and bookmarklet setup": "Blog identity, URLs, authoring defaults, and bookmarklet setup", + "Project Name": "Project Name", + "Description": "Description", + "Data Path": "Data Path", + "Public URL": "Public URL", + "Blog Languages": "Blog Languages", + "Default Author": "Default Author", + "Max Posts Per Page": "Max Posts Per Page", + "Blogmark Category": "Blogmark Category", + "Blogmark Bookmarklet": "Blogmark Bookmarklet", + "Bookmarklet copy support is wired through the desktop runtime and project public URL.": "Bookmarklet copy support is wired through the desktop runtime and project public URL.", + "Default editing mode and diff presentation": "Default editing mode and diff presentation", + "Default Editor Mode": "Default Editor Mode", + "Diff View Style": "Diff View Style", + "Inline": "Inline", + "Side by Side": "Side by Side", + "Wrap Long Lines": "Wrap Long Lines", + "Enable line wrapping in diffs": "Enable line wrapping in diffs", + "Hide Unchanged Regions": "Hide Unchanged Regions", + "Collapse unchanged diff hunks": "Collapse unchanged diff hunks", + "Content Categories": "Content Categories", + "Category defaults, rendering flags, and template wiring": "Category defaults, rendering flags, and template wiring", + "Category": "Category", + "Render in Lists": "Render in Lists", + "Show Titles": "Show Titles", + "Post Template": "Post Template", + "List Template": "List Template", + "Default": "Default", + "Add Category": "Add Category", + "Reset to Defaults": "Reset to Defaults", + "Provider keys, model preferences, airplane mode, and system prompt": "Provider keys, model preferences, airplane mode, and system prompt", + "Anthropic / Online API Key": "Anthropic / Online API Key", + "Mistral API Key": "Mistral API Key", + "Offline Mode": "Offline Mode", + "Route AI tasks through the airplane endpoint": "Route AI tasks through the airplane endpoint", + "Default Model": "Default Model", + "Title Model": "Title Model", + "Image Analysis Model": "Image Analysis Model", + "Offline Chat Model": "Offline Chat Model", + "Offline Title Model": "Offline Title Model", + "Offline Image Analysis Model": "Offline Image Analysis Model", + "System Prompt": "System Prompt", + "Reset to Default": "Reset to Default", + "Application-level runtime behavior and semantic indexing": "Application-level runtime behavior and semantic indexing", + "Semantic Similarity": "Semantic Similarity", + "Enable duplicate search and related-post embeddings": "Enable duplicate search and related-post embeddings", + "Scripting Runtime": "Scripting Runtime", + "Scripting capabilities are configured at the application layer in the rewrite and do not expose runtime switching here.": "Scripting capabilities are configured at the application layer in the rewrite and do not expose runtime switching here.", + "Deployment credentials for upload tasks": "Deployment credentials for upload tasks", + "SSH Mode": "SSH Mode", + "Host": "Host", + "Username": "Username", + "Remote Path": "Remote Path", + "Agent configuration files for the built-in bDS MCP server": "Agent configuration files for the built-in bDS MCP server", + "Not supported in the rewrite yet": "Not supported in the rewrite yet", + "Remove": "Remove", + "Add": "Add", + "Data Maintenance": "Data Maintenance", + "Rebuild filesystem-backed records and thumbnails": "Rebuild filesystem-backed records and thumbnails", + "Rebuild Posts From Files": "Rebuild Posts From Files", + "Rebuild Media From Files": "Rebuild Media From Files", + "Rebuild Scripts From Files": "Rebuild Scripts From Files", + "Rebuild Templates From Files": "Rebuild Templates From Files", + "Rebuild Links": "Rebuild Links", + "Regenerate Missing Thumbnails": "Regenerate Missing Thumbnails", + "Rebuild Embedding Index": "Rebuild Embedding Index", + "Quick Actions": "Quick Actions", + "Review title, alt text, and caption suggestions": "Review title, alt text, and caption suggestions", + "Detect Language": "Detect Language", + "Persist the detected language for this media item": "Persist the detected language for this media item", + "Select a target language for this media item": "Select a target language for this media item", + "Replace File": "Replace File", + "File Name": "File Name", + "MIME Type": "MIME Type", + "Size": "Size", + "Dimensions": "Dimensions", + "Author": "Author", + "Language": "Language", + "None": "None", + "No translations": "No translations", + "Refresh": "Refresh", + "Linked Posts": "Linked Posts", + "Link to Post": "Link to Post", + "Search posts": "Search posts", + "No posts to link": "No posts to link", + "and %{count} more": "and %{count} more", + "Not linked to any posts": "Not linked to any posts" } \ No newline at end of file diff --git a/priv/i18n/locales/es.json b/priv/i18n/locales/es.json index c5665ed..865f252 100644 --- a/priv/i18n/locales/es.json +++ b/priv/i18n/locales/es.json @@ -249,5 +249,94 @@ "Ask the assistant about the active project or editor.": "Pregunta al asistente sobre el proyecto o editor activo.", "Start chat": "Iniciar chat", "You": "Tú", - "The assistant sidebar chat surface is ready, but model execution is not connected yet.": "La superficie de chat de la barra lateral del asistente está lista, pero la ejecución del modelo aún no está conectada." + "The assistant sidebar chat surface is ready, but model execution is not connected yet.": "La superficie de chat de la barra lateral del asistente está lista, pero la ejecución del modelo aún no está conectada.", + "Search settings": "Buscar en la configuración", + "No settings match the current search": "Ninguna configuración coincide con la búsqueda actual", + "Blog identity, URLs, authoring defaults, and bookmarklet setup": "Identidad del blog, URL, valores predeterminados de autoría y configuración del bookmarklet", + "Project Name": "Nombre del proyecto", + "Description": "Descripción", + "Data Path": "Ruta de datos", + "Public URL": "URL pública", + "Blog Languages": "Idiomas del blog", + "Default Author": "Autor predeterminado", + "Max Posts Per Page": "Máximo de publicaciones por página", + "Blogmark Category": "Categoría de blogmark", + "Blogmark Bookmarklet": "Bookmarklet de blogmark", + "Bookmarklet copy support is wired through the desktop runtime and project public URL.": "La copia del bookmarklet está conectada mediante el entorno de escritorio y la URL pública del proyecto.", + "Default editing mode and diff presentation": "Modo de edición predeterminado y presentación de diff", + "Default Editor Mode": "Modo de editor predeterminado", + "Diff View Style": "Estilo de vista diff", + "Inline": "En línea", + "Side by Side": "Lado a lado", + "Wrap Long Lines": "Ajustar líneas largas", + "Enable line wrapping in diffs": "Activar ajuste de línea en diffs", + "Hide Unchanged Regions": "Ocultar regiones sin cambios", + "Collapse unchanged diff hunks": "Contraer bloques de diff sin cambios", + "Content Categories": "Categorías de contenido", + "Category defaults, rendering flags, and template wiring": "Valores predeterminados de categoría, opciones de renderizado y conexión de plantillas", + "Category": "Categoría", + "Render in Lists": "Mostrar en listas", + "Show Titles": "Mostrar títulos", + "Post Template": "Plantilla de publicación", + "List Template": "Plantilla de lista", + "Default": "Predeterminado", + "Add Category": "Agregar categoría", + "Reset to Defaults": "Restablecer valores predeterminados", + "Provider keys, model preferences, airplane mode, and system prompt": "Claves del proveedor, preferencias de modelo, modo avión y prompt del sistema", + "Anthropic / Online API Key": "Clave API de Anthropic / en línea", + "Mistral API Key": "Clave API de Mistral", + "Offline Mode": "Modo sin conexión", + "Route AI tasks through the airplane endpoint": "Enviar tareas de IA mediante el endpoint de modo avión", + "Default Model": "Modelo predeterminado", + "Title Model": "Modelo de título", + "Image Analysis Model": "Modelo de análisis de imágenes", + "Offline Chat Model": "Modelo de chat sin conexión", + "Offline Title Model": "Modelo de título sin conexión", + "Offline Image Analysis Model": "Modelo de análisis de imágenes sin conexión", + "System Prompt": "Prompt del sistema", + "Reset to Default": "Restablecer al predeterminado", + "Application-level runtime behavior and semantic indexing": "Comportamiento de ejecución a nivel de aplicación e indexación semántica", + "Semantic Similarity": "Similitud semántica", + "Enable duplicate search and related-post embeddings": "Activar búsqueda de duplicados y embeddings de publicaciones relacionadas", + "Scripting Runtime": "Entorno de scripts", + "Scripting capabilities are configured at the application layer in the rewrite and do not expose runtime switching here.": "Las capacidades de scripts se configuran en la capa de aplicación en la reescritura y no exponen aquí el cambio de entorno.", + "Deployment credentials for upload tasks": "Credenciales de despliegue para tareas de subida", + "SSH Mode": "Modo SSH", + "Host": "Host", + "Username": "Nombre de usuario", + "Remote Path": "Ruta remota", + "Agent configuration files for the built-in bDS MCP server": "Archivos de configuración de agentes para el servidor MCP integrado de bDS", + "Not supported in the rewrite yet": "Todavía no compatible en la reescritura", + "Remove": "Quitar", + "Add": "Agregar", + "Data Maintenance": "Mantenimiento de datos", + "Rebuild filesystem-backed records and thumbnails": "Reconstruir registros respaldados por el sistema de archivos y miniaturas", + "Rebuild Posts From Files": "Reconstruir publicaciones desde archivos", + "Rebuild Media From Files": "Reconstruir medios desde archivos", + "Rebuild Scripts From Files": "Reconstruir scripts desde archivos", + "Rebuild Templates From Files": "Reconstruir plantillas desde archivos", + "Rebuild Links": "Reconstruir enlaces", + "Regenerate Missing Thumbnails": "Regenerar miniaturas faltantes", + "Rebuild Embedding Index": "Reconstruir índice de embeddings", + "Quick Actions": "Acciones rápidas", + "Review title, alt text, and caption suggestions": "Revisar sugerencias de título, texto alternativo y leyenda", + "Detect Language": "Detectar idioma", + "Persist the detected language for this media item": "Guardar el idioma detectado para este medio", + "Select a target language for this media item": "Seleccionar un idioma de destino para este medio", + "Replace File": "Reemplazar archivo", + "File Name": "Nombre del archivo", + "MIME Type": "Tipo MIME", + "Size": "Tamaño", + "Dimensions": "Dimensiones", + "Author": "Autor", + "Language": "Idioma", + "None": "Ninguno", + "No translations": "Sin traducciones", + "Refresh": "Actualizar", + "Linked Posts": "Publicaciones enlazadas", + "Link to Post": "Enlazar con publicación", + "Search posts": "Buscar publicaciones", + "No posts to link": "No hay publicaciones para enlazar", + "and %{count} more": "y %{count} más", + "Not linked to any posts": "No está enlazado a ninguna publicación" } \ No newline at end of file diff --git a/priv/i18n/locales/fr.json b/priv/i18n/locales/fr.json index 1dd153e..efed1bc 100644 --- a/priv/i18n/locales/fr.json +++ b/priv/i18n/locales/fr.json @@ -249,5 +249,94 @@ "Ask the assistant about the active project or editor.": "Interrogez l’assistant sur le projet ou l’éditeur actif.", "Start chat": "Démarrer la conversation", "You": "Vous", - "The assistant sidebar chat surface is ready, but model execution is not connected yet.": "La surface de discussion de la barre latérale de l’assistant est prête, mais l’exécution du modèle n’est pas encore connectée." + "The assistant sidebar chat surface is ready, but model execution is not connected yet.": "La surface de discussion de la barre latérale de l’assistant est prête, mais l’exécution du modèle n’est pas encore connectée.", + "Search settings": "Rechercher dans les paramètres", + "No settings match the current search": "Aucun paramètre ne correspond à la recherche actuelle", + "Blog identity, URLs, authoring defaults, and bookmarklet setup": "Identité du blog, URLs, valeurs d’auteur par défaut et configuration du bookmarklet", + "Project Name": "Nom du projet", + "Description": "Description", + "Data Path": "Chemin des données", + "Public URL": "URL publique", + "Blog Languages": "Langues du blog", + "Default Author": "Auteur par défaut", + "Max Posts Per Page": "Nombre maximal d’articles par page", + "Blogmark Category": "Catégorie de blogmark", + "Blogmark Bookmarklet": "Bookmarklet blogmark", + "Bookmarklet copy support is wired through the desktop runtime and project public URL.": "La copie du bookmarklet est reliée via l’environnement desktop et l’URL publique du projet.", + "Default editing mode and diff presentation": "Mode d’édition par défaut et présentation des diffs", + "Default Editor Mode": "Mode d’édition par défaut", + "Diff View Style": "Style de vue diff", + "Inline": "En ligne", + "Side by Side": "Côte à côte", + "Wrap Long Lines": "Renvoyer les longues lignes", + "Enable line wrapping in diffs": "Activer le retour à la ligne dans les diffs", + "Hide Unchanged Regions": "Masquer les zones inchangées", + "Collapse unchanged diff hunks": "Réduire les blocs de diff inchangés", + "Content Categories": "Catégories de contenu", + "Category defaults, rendering flags, and template wiring": "Valeurs par défaut des catégories, options de rendu et liaison des modèles", + "Category": "Catégorie", + "Render in Lists": "Afficher dans les listes", + "Show Titles": "Afficher les titres", + "Post Template": "Modèle d’article", + "List Template": "Modèle de liste", + "Default": "Par défaut", + "Add Category": "Ajouter une catégorie", + "Reset to Defaults": "Réinitialiser par défaut", + "Provider keys, model preferences, airplane mode, and system prompt": "Clés fournisseur, préférences de modèle, mode avion et prompt système", + "Anthropic / Online API Key": "Clé API Anthropic / en ligne", + "Mistral API Key": "Clé API Mistral", + "Offline Mode": "Mode hors ligne", + "Route AI tasks through the airplane endpoint": "Acheminer les tâches IA via le point d’accès du mode avion", + "Default Model": "Modèle par défaut", + "Title Model": "Modèle pour les titres", + "Image Analysis Model": "Modèle d’analyse d’image", + "Offline Chat Model": "Modèle de chat hors ligne", + "Offline Title Model": "Modèle de titre hors ligne", + "Offline Image Analysis Model": "Modèle d’analyse d’image hors ligne", + "System Prompt": "Prompt système", + "Reset to Default": "Réinitialiser par défaut", + "Application-level runtime behavior and semantic indexing": "Comportement d’exécution applicatif et indexation sémantique", + "Semantic Similarity": "Similarité sémantique", + "Enable duplicate search and related-post embeddings": "Activer la recherche de doublons et les embeddings d’articles liés", + "Scripting Runtime": "Environnement de script", + "Scripting capabilities are configured at the application layer in the rewrite and do not expose runtime switching here.": "Les capacités de script sont configurées au niveau de l’application dans la réécriture et n’exposent pas de changement d’environnement ici.", + "Deployment credentials for upload tasks": "Identifiants de déploiement pour les tâches d’envoi", + "SSH Mode": "Mode SSH", + "Host": "Hôte", + "Username": "Nom d’utilisateur", + "Remote Path": "Chemin distant", + "Agent configuration files for the built-in bDS MCP server": "Fichiers de configuration d’agent pour le serveur MCP bDS intégré", + "Not supported in the rewrite yet": "Pas encore pris en charge dans la réécriture", + "Remove": "Retirer", + "Add": "Ajouter", + "Data Maintenance": "Maintenance des données", + "Rebuild filesystem-backed records and thumbnails": "Reconstruire les enregistrements basés sur le système de fichiers et les vignettes", + "Rebuild Posts From Files": "Reconstruire les articles depuis les fichiers", + "Rebuild Media From Files": "Reconstruire les médias depuis les fichiers", + "Rebuild Scripts From Files": "Reconstruire les scripts depuis les fichiers", + "Rebuild Templates From Files": "Reconstruire les modèles depuis les fichiers", + "Rebuild Links": "Reconstruire les liens", + "Regenerate Missing Thumbnails": "Régénérer les vignettes manquantes", + "Rebuild Embedding Index": "Reconstruire l’index d’embeddings", + "Quick Actions": "Actions rapides", + "Review title, alt text, and caption suggestions": "Vérifier les suggestions de titre, texte alternatif et légende", + "Detect Language": "Détecter la langue", + "Persist the detected language for this media item": "Enregistrer la langue détectée pour ce média", + "Select a target language for this media item": "Sélectionner une langue cible pour ce média", + "Replace File": "Remplacer le fichier", + "File Name": "Nom du fichier", + "MIME Type": "Type MIME", + "Size": "Taille", + "Dimensions": "Dimensions", + "Author": "Auteur", + "Language": "Langue", + "None": "Aucune", + "No translations": "Aucune traduction", + "Refresh": "Actualiser", + "Linked Posts": "Articles liés", + "Link to Post": "Lier à un article", + "Search posts": "Rechercher des articles", + "No posts to link": "Aucun article à lier", + "and %{count} more": "et %{count} de plus", + "Not linked to any posts": "Lié à aucun article" } \ No newline at end of file diff --git a/priv/i18n/locales/it.json b/priv/i18n/locales/it.json index ec1d92d..613b740 100644 --- a/priv/i18n/locales/it.json +++ b/priv/i18n/locales/it.json @@ -249,5 +249,94 @@ "Ask the assistant about the active project or editor.": "Chiedi all’assistente del progetto o editor attivo.", "Start chat": "Avvia chat", "You": "Tu", - "The assistant sidebar chat surface is ready, but model execution is not connected yet.": "La superficie chat della barra laterale assistente è pronta, ma l’esecuzione del modello non è ancora collegata." + "The assistant sidebar chat surface is ready, but model execution is not connected yet.": "La superficie chat della barra laterale assistente è pronta, ma l’esecuzione del modello non è ancora collegata.", + "Search settings": "Cerca nelle impostazioni", + "No settings match the current search": "Nessuna impostazione corrisponde alla ricerca corrente", + "Blog identity, URLs, authoring defaults, and bookmarklet setup": "Identità del blog, URL, valori di autore predefiniti e configurazione del bookmarklet", + "Project Name": "Nome progetto", + "Description": "Descrizione", + "Data Path": "Percorso dati", + "Public URL": "URL pubblica", + "Blog Languages": "Lingue del blog", + "Default Author": "Autore predefinito", + "Max Posts Per Page": "Numero massimo di post per pagina", + "Blogmark Category": "Categoria blogmark", + "Blogmark Bookmarklet": "Bookmarklet blogmark", + "Bookmarklet copy support is wired through the desktop runtime and project public URL.": "La copia del bookmarklet è collegata tramite il runtime desktop e l’URL pubblica del progetto.", + "Default editing mode and diff presentation": "Modalità di modifica predefinita e presentazione dei diff", + "Default Editor Mode": "Modalità editor predefinita", + "Diff View Style": "Stile vista diff", + "Inline": "In linea", + "Side by Side": "Affiancato", + "Wrap Long Lines": "A capo per linee lunghe", + "Enable line wrapping in diffs": "Abilita il ritorno a capo nei diff", + "Hide Unchanged Regions": "Nascondi regioni invariate", + "Collapse unchanged diff hunks": "Comprimi i blocchi diff invariati", + "Content Categories": "Categorie di contenuto", + "Category defaults, rendering flags, and template wiring": "Valori predefiniti delle categorie, opzioni di rendering e collegamento dei template", + "Category": "Categoria", + "Render in Lists": "Mostra nelle liste", + "Show Titles": "Mostra titoli", + "Post Template": "Template del post", + "List Template": "Template della lista", + "Default": "Predefinito", + "Add Category": "Aggiungi categoria", + "Reset to Defaults": "Ripristina valori predefiniti", + "Provider keys, model preferences, airplane mode, and system prompt": "Chiavi provider, preferenze modello, modalità aereo e prompt di sistema", + "Anthropic / Online API Key": "Chiave API Anthropic / online", + "Mistral API Key": "Chiave API Mistral", + "Offline Mode": "Modalità offline", + "Route AI tasks through the airplane endpoint": "Instrada i task IA tramite l’endpoint modalità aereo", + "Default Model": "Modello predefinito", + "Title Model": "Modello titolo", + "Image Analysis Model": "Modello analisi immagini", + "Offline Chat Model": "Modello chat offline", + "Offline Title Model": "Modello titolo offline", + "Offline Image Analysis Model": "Modello analisi immagini offline", + "System Prompt": "Prompt di sistema", + "Reset to Default": "Ripristina predefinito", + "Application-level runtime behavior and semantic indexing": "Comportamento runtime a livello applicativo e indicizzazione semantica", + "Semantic Similarity": "Somiglianza semantica", + "Enable duplicate search and related-post embeddings": "Abilita ricerca duplicati ed embeddings per post correlati", + "Scripting Runtime": "Runtime scripting", + "Scripting capabilities are configured at the application layer in the rewrite and do not expose runtime switching here.": "Le capacità di scripting sono configurate a livello applicativo nella riscrittura e non espongono qui il cambio di runtime.", + "Deployment credentials for upload tasks": "Credenziali di distribuzione per i task di upload", + "SSH Mode": "Modalità SSH", + "Host": "Host", + "Username": "Nome utente", + "Remote Path": "Percorso remoto", + "Agent configuration files for the built-in bDS MCP server": "File di configurazione degli agenti per il server MCP bDS integrato", + "Not supported in the rewrite yet": "Non ancora supportato nella riscrittura", + "Remove": "Rimuovi", + "Add": "Aggiungi", + "Data Maintenance": "Manutenzione dati", + "Rebuild filesystem-backed records and thumbnails": "Ricostruisci record basati su filesystem e miniature", + "Rebuild Posts From Files": "Ricostruisci i post dai file", + "Rebuild Media From Files": "Ricostruisci i media dai file", + "Rebuild Scripts From Files": "Ricostruisci gli script dai file", + "Rebuild Templates From Files": "Ricostruisci i template dai file", + "Rebuild Links": "Ricostruisci i link", + "Regenerate Missing Thumbnails": "Rigenera miniature mancanti", + "Rebuild Embedding Index": "Ricostruisci indice embeddings", + "Quick Actions": "Azioni rapide", + "Review title, alt text, and caption suggestions": "Rivedi i suggerimenti per titolo, testo alternativo e didascalia", + "Detect Language": "Rileva lingua", + "Persist the detected language for this media item": "Salva la lingua rilevata per questo media", + "Select a target language for this media item": "Seleziona una lingua di destinazione per questo media", + "Replace File": "Sostituisci file", + "File Name": "Nome file", + "MIME Type": "Tipo MIME", + "Size": "Dimensione", + "Dimensions": "Dimensioni", + "Author": "Autore", + "Language": "Lingua", + "None": "Nessuno", + "No translations": "Nessuna traduzione", + "Refresh": "Aggiorna", + "Linked Posts": "Post collegati", + "Link to Post": "Collega al post", + "Search posts": "Cerca post", + "No posts to link": "Nessun post da collegare", + "and %{count} more": "e altri %{count}", + "Not linked to any posts": "Non collegato ad alcun post" } \ No newline at end of file diff --git a/priv/ui/app.css b/priv/ui/app.css index 505aef7..035a290 100644 --- a/priv/ui/app.css +++ b/priv/ui/app.css @@ -2598,18 +2598,339 @@ button svg * { font: inherit; } -.media-editor { - display: grid; - grid-template-columns: minmax(320px, 0.95fr) minmax(360px, 1.05fr); - gap: 20px; - padding: 20px; - align-items: start; -} - -.media-editor-details-form { +[data-testid="media-editor"] { + flex: 1; display: flex; flex-direction: column; - gap: 14px; + background-color: var(--vscode-editor-background); + overflow: hidden; +} + +[data-testid="media-editor"] .editor-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 0 12px; + min-height: 35px; + background-color: var(--vscode-tab-activeBackground); + border-bottom: 1px solid var(--vscode-panel-border); +} + +[data-testid="media-editor"] .editor-tabs { + display: flex; + align-items: center; + gap: 2px; + min-width: 0; +} + +[data-testid="media-editor"] .editor-tab { + display: flex; + align-items: center; + gap: 6px; + min-width: 0; + padding: 6px 12px; + background-color: var(--vscode-tab-inactiveBackground); + color: var(--vscode-tab-inactiveForeground); + font-size: 13px; + border-radius: 4px 4px 0 0; +} + +[data-testid="media-editor"] .editor-tab.active { + background-color: var(--vscode-tab-activeBackground); + color: var(--vscode-tab-activeForeground); +} + +[data-testid="media-editor"] .editor-tab-title { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +[data-testid="media-editor"] .editor-tab-dirty { + color: var(--vscode-notificationsWarningIcon-foreground, var(--vscode-editorWarning-foreground)); + font-size: 10px; +} + +[data-testid="media-editor"] .editor-actions { + display: flex; + align-items: center; + gap: 8px; +} + +[data-testid="media-editor"] .editor-actions button { + padding: 4px 10px; + font-size: 12px; +} + +[data-testid="media-editor"] .editor-actions button.danger:hover { + background-color: var(--vscode-notificationsErrorIcon-foreground); +} + +[data-testid="media-editor"] .auto-save-indicator { + font-size: 11px; + color: var(--vscode-descriptionForeground); + font-style: italic; +} + +[data-testid="media-editor"] .editor-content { + flex: 1; + display: flex; + flex-direction: column; + padding: 16px; + overflow-y: auto; + gap: 16px; +} + +[data-testid="media-editor"] > .editor-content.media-editor { + flex-direction: row; + align-items: stretch; + gap: 24px; +} + +[data-testid="media-editor"] .editor-field { + display: flex; + flex-direction: column; + gap: 4px; + flex: 1; + min-width: 0; +} + +[data-testid="media-editor"] .editor-field label { + font-size: 11px; + font-weight: 500; + color: var(--vscode-descriptionForeground); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +[data-testid="media-editor"] .editor-field-row { + display: flex; + gap: 12px; + width: 100%; + margin-bottom: 0; +} + +[data-testid="media-editor"] .post-editor-input, +[data-testid="media-editor"] .post-editor-textarea { + width: 100%; + padding: 8px 10px; + border: 1px solid var(--vscode-input-border, var(--vscode-panel-border)); + border-radius: 4px; + background: var(--vscode-input-background, rgba(255, 255, 255, 0.06)); + color: var(--vscode-input-foreground, var(--vscode-foreground)); + font: inherit; +} + +[data-testid="media-editor"] .post-editor-input.disabled, +[data-testid="media-editor"] .post-editor-input:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +[data-testid="media-editor"] .post-editor-textarea { + line-height: 1.5; + resize: vertical; +} + +[data-testid="media-editor"] .media-preview { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + background-color: var(--vscode-input-background); + border-radius: 8px; + min-height: 300px; +} + +[data-testid="media-editor"] .media-preview-placeholder { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + color: var(--vscode-descriptionForeground); +} + +[data-testid="media-editor"] .media-preview-image { + max-width: 100%; + max-height: 100%; + display: flex; + align-items: center; + justify-content: center; + padding: 16px; +} + +[data-testid="media-editor"] .media-preview-image img { + max-width: 100%; + max-height: 100%; + object-fit: contain; + border-radius: 4px; +} + +[data-testid="media-editor"] .media-details { + width: 320px; + display: flex; + flex-direction: column; + gap: 12px; + flex-shrink: 0; +} + +[data-testid="media-editor"] .media-editor-details-form { + display: flex; + flex-direction: column; + gap: 12px; +} + +[data-testid="media-editor"] .media-details textarea { + resize: vertical; +} + +[data-testid="media-editor"] .linked-posts-section label { + display: flex; + justify-content: space-between; + align-items: center; +} + +[data-testid="media-editor"] .add-link-btn { + background: var(--vscode-button-secondaryBackground); + border: none; + color: var(--vscode-button-secondaryForeground); + padding: 2px 8px; + border-radius: 3px; + cursor: pointer; + font-size: 11px; +} + +[data-testid="media-editor"] .add-link-btn:hover { + background: var(--vscode-button-secondaryHoverBackground); +} + +[data-testid="media-editor"] .post-picker { + background: var(--vscode-dropdown-background); + border: 1px solid var(--vscode-dropdown-border); + border-radius: 4px; + margin-top: 8px; + max-height: 250px; + overflow-y: auto; +} + +[data-testid="media-editor"] .post-picker-search { + padding: 8px; + border-bottom: 1px solid var(--vscode-dropdown-border); + position: sticky; + top: 0; + background: var(--vscode-dropdown-background); +} + +[data-testid="media-editor"] .post-picker-search input { + width: 100%; + padding: 6px 10px; + background: var(--vscode-input-background); + border: 1px solid var(--vscode-input-border); + border-radius: 3px; + color: var(--vscode-input-foreground); + font-size: 12px; +} + +[data-testid="media-editor"] .post-picker-search input:focus { + outline: none; + border-color: var(--vscode-focusBorder); +} + +[data-testid="media-editor"] .post-picker-list { + padding: 4px; +} + +[data-testid="media-editor"] .post-picker-item { + width: 100%; + padding: 6px 8px; + cursor: pointer; + border: none; + border-radius: 3px; + background: transparent; + color: inherit; + font-size: 12px; + text-align: left; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +[data-testid="media-editor"] .post-picker-item:hover { + background: var(--vscode-list-hoverBackground); +} + +[data-testid="media-editor"] .post-picker-more { + padding: 6px 8px; + color: var(--vscode-descriptionForeground); + font-size: 11px; + font-style: italic; +} + +[data-testid="media-editor"] .no-posts, +[data-testid="media-editor"] .no-linked-posts { + padding: 12px 8px; + color: var(--vscode-descriptionForeground); + font-size: 12px; + font-style: italic; +} + +[data-testid="media-editor"] .linked-posts-list { + margin-top: 8px; + display: flex; + flex-direction: column; + gap: 4px; +} + +[data-testid="media-editor"] .linked-post-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px 8px; + background: var(--vscode-sideBar-background); + border-radius: 4px; +} + +[data-testid="media-editor"] .linked-post-title, +[data-testid="media-editor"] .linked-post-link { + flex: 1; + min-width: 0; + border: none; + background: transparent; + padding: 0; + color: inherit; + text-align: left; + cursor: pointer; + font-size: 12px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +[data-testid="media-editor"] .linked-post-title:hover, +[data-testid="media-editor"] .linked-post-link:hover { + color: var(--vscode-textLink-foreground); + text-decoration: underline; +} + +[data-testid="media-editor"] .linked-post-item .unlink-btn { + background: none; + border: none; + color: var(--vscode-descriptionForeground); + cursor: pointer; + padding: 0 4px; + font-size: 14px; + opacity: 0; + transition: opacity 0.1s; +} + +[data-testid="media-editor"] .linked-post-item:hover .unlink-btn { + opacity: 1; +} + +[data-testid="media-editor"] .linked-post-item .unlink-btn:hover { + color: var(--vscode-errorForeground); } .translation-modal-backdrop { @@ -3091,7 +3412,7 @@ button svg * { } @media (max-width: 1100px) { - .media-editor, + [data-testid="media-editor"] > .editor-content.media-editor, .setting-row, .tag-form-row, .editor-field-row, @@ -3100,6 +3421,16 @@ button svg * { grid-template-columns: 1fr; } + [data-testid="media-editor"] > .editor-content.media-editor, + [data-testid="media-editor"] .editor-field-row { + display: flex; + flex-direction: column; + } + + [data-testid="media-editor"] .media-details { + width: 100%; + } + .style-theme-picker { grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); } @@ -3340,171 +3671,6 @@ button svg * { .lightbox-image-container { max-width: 90%; - -.media-editor { - display: flex; - flex-direction: column; - gap: 18px; -} - -.media-editor-form { - display: grid; - grid-template-columns: minmax(240px, 0.9fr) minmax(0, 1.1fr); - gap: 20px; - align-items: start; -} - -.media-preview, -.media-translations-section, -.linked-posts-section { - border: 1px solid rgba(148, 163, 184, 0.24); - border-radius: 12px; - background: rgba(255, 255, 255, 0.84); -} - -.media-preview { - min-height: 260px; - display: flex; - align-items: center; - justify-content: center; - padding: 16px; -} - -.media-preview-image, -.media-preview-image img { - width: 100%; -} - -.media-preview-image img { - display: block; - max-height: 460px; - object-fit: contain; - border-radius: 10px; -} - -.media-preview-placeholder { - min-height: 220px; - width: 100%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 10px; - color: #64748b; -} - -.media-details { - display: flex; - flex-direction: column; - gap: 14px; -} - -.media-translations-section, -.linked-posts-section { - margin-top: 2px; - padding: 14px 16px; -} - -.linked-posts-list { - display: flex; - flex-direction: column; - gap: 8px; -} - -.linked-post-item { - display: flex; - align-items: center; - justify-content: space-between; - gap: 10px; -} - -.linked-post-link { - border: 0; - background: transparent; - padding: 0; - color: #0f172a; - text-align: left; - cursor: pointer; -} - -.unlink-btn, -.add-link-btn { - border: 0; - background: transparent; - cursor: pointer; -} - -.add-link-btn { - margin-left: 10px; - color: #2563eb; - font-weight: 600; -} - -.unlink-btn { - color: #b91c1c; - font-size: 1rem; -} - -.post-picker { - margin-top: 10px; - padding: 12px; - border-radius: 10px; - border: 1px solid rgba(148, 163, 184, 0.22); - background: rgba(248, 250, 252, 0.9); -} - -.post-picker-search input, -.translation-inline-form .post-editor-input, -.translation-inline-form .post-editor-textarea { - width: 100%; -} - -.post-picker-list { - margin-top: 10px; - display: flex; - flex-direction: column; - gap: 6px; -} - -.post-picker-item, -.post-picker-more, -.no-linked-posts, -.no-posts { - padding: 8px 10px; - border-radius: 8px; -} - -.post-picker-item { - border: 0; - background: rgba(37, 99, 235, 0.08); - text-align: left; - cursor: pointer; -} - -.post-picker-more, -.no-linked-posts, -.no-posts { - color: #64748b; - background: rgba(226, 232, 240, 0.4); -} - -.translation-inline-form { - margin-top: 12px; - display: flex; - flex-direction: column; - gap: 10px; -} - -.translation-inline-actions { - display: flex; - justify-content: flex-end; -} - -@media (max-width: 1100px) { - .media-editor-form { - grid-template-columns: 1fr; - } -} max-height: 78%; } diff --git a/test/bds/desktop/shell_live_test.exs b/test/bds/desktop/shell_live_test.exs index 6f7c699..da050fe 100644 --- a/test/bds/desktop/shell_live_test.exs +++ b/test/bds/desktop/shell_live_test.exs @@ -860,6 +860,9 @@ defmodule BDS.Desktop.ShellLiveTest do assert html =~ ~s(class="editor-content media-editor") assert html =~ ~s(class="quick-actions-wrapper") refute html =~ ~s(class="media-editor-form") + assert has_element?(view, "[data-testid='media-editor'] .editor-content.media-editor .media-details") + assert has_element?(view, "[data-testid='media-editor'] .editor-content.media-editor .media-details .media-translations-section") + assert has_element?(view, "[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"}) @@ -870,6 +873,54 @@ defmodule BDS.Desktop.ShellLiveTest do assert html =~ ~s(name="media_translation[caption]") end + test "settings and media editors render localized labels when the UI language changes", %{project: project, temp_dir: temp_dir} do + source_path = Path.join(temp_dir, "localized-hero.txt") + File.write!(source_path, "media body") + + assert {:ok, media} = + Media.import_media(%{ + project_id: project.id, + source_path: source_path, + title: "Lokales Bild", + alt: "Alt", + caption: "Beschriftung", + language: "de" + }) + + {:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive) + + html = + view + |> form("[data-testid='status-language-form']", %{ui_language: "de"}) + |> render_change() + + assert html =~ "Beiträge durchsuchen..." + + settings_html = + render_click(view, "pin_sidebar_item", %{ + "route" => "settings", + "id" => "settings-editor", + "title" => "Editor", + "subtitle" => "Editor settings" + }) + + assert settings_html =~ "Standard-Bearbeitungsmodus" + refute settings_html =~ "Default Editor Mode" + + media_html = + render_click(view, "pin_sidebar_item", %{ + "route" => "media", + "id" => media.id, + "title" => media.title, + "subtitle" => media.original_name + }) + + assert media_html =~ "Dateiname" + assert media_html =~ "Verknüpfte Beiträge" + refute media_html =~ "File Name" + refute media_html =~ "Linked Posts" + end + test "remaining step-5 routes render dedicated editors instead of the generic shell placeholder", %{project: project} do assert {:ok, script} = Scripts.create_script(%{ @@ -965,6 +1016,28 @@ defmodule BDS.Desktop.ShellLiveTest do refute chat_html =~ "Desktop workbench content routed through the Elixir shell." end + test "settings sidebar categories render the full old-app section model and target the requested section" do + {:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive) + + html = + render_click(view, "pin_sidebar_item", %{ + "route" => "settings", + "id" => "settings-ai", + "title" => "AI", + "subtitle" => "Assistant settings" + }) + + assert html =~ ~s(id="settings-section-project") + assert html =~ ~s(id="settings-section-editor") + assert html =~ ~s(id="settings-section-content") + assert html =~ ~s(id="settings-section-ai") + assert html =~ ~s(id="settings-section-technology") + assert html =~ ~s(id="settings-section-publishing") + assert html =~ ~s(id="settings-section-data") + assert html =~ ~s(id="settings-section-mcp") + assert html =~ ~s(data-selected-settings-section="ai") + end + test "template sidebar exposes old-app style delete control and removes template rows", %{project: project} do assert {:ok, template} = BDS.Templates.create_template(%{ diff --git a/test/bds/ui/shell_test.exs b/test/bds/ui/shell_test.exs index ceac1ec..e3596b2 100644 --- a/test/bds/ui/shell_test.exs +++ b/test/bds/ui/shell_test.exs @@ -218,6 +218,27 @@ defmodule BDS.UI.ShellTest do assert live_ex =~ "titlebar_menu_item_index" end + test "desktop shell css keeps the old media editor layout contract" do + css = File.read!("/Users/gb/Projects/bDS2/priv/ui/app.css") + + assert css =~ ".media-preview {" + assert css =~ "min-height: 300px;" + assert css =~ ".media-details {" + assert css =~ "width: 320px;" + assert css =~ ".media-details textarea {" + assert css =~ "resize: vertical;" + assert css =~ ".linked-posts-section label {" + assert css =~ "justify-content: space-between;" + assert css =~ ".add-link-btn {" + assert css =~ "font-size: 11px;" + assert css =~ ".post-picker {" + assert css =~ "max-height: 250px;" + assert css =~ ".post-picker-search input {" + assert css =~ "padding: 6px 10px;" + assert css =~ ".linked-post-item:hover .unlink-btn {" + assert css =~ "opacity: 1;" + end + test "desktop shell status task area keeps the compact running-task markup" do template = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/index.html.heex")