feat: better parity in layout for media and preferences
This commit is contained in:
@@ -117,6 +117,56 @@ defmodule BDS.Desktop.ShellCommands do
|
|||||||
end)
|
end)
|
||||||
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
|
defp dispatch("rebuild_database", project, _params) do
|
||||||
group_id = task_group_id("rebuild_database")
|
group_id = task_group_id("rebuild_database")
|
||||||
attrs = %{group_id: group_id, group_name: "Maintenance"}
|
attrs = %{group_id: group_id, group_name: "Maintenance"}
|
||||||
|
|||||||
@@ -509,6 +509,14 @@ defmodule BDS.Desktop.ShellLive do
|
|||||||
{:noreply, SettingsEditor.update_project_draft(socket, params, &reload_shell/2)}
|
{:noreply, SettingsEditor.update_project_draft(socket, params, &reload_shell/2)}
|
||||||
end
|
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
|
def handle_event("save_settings_project", _params, socket) do
|
||||||
{:noreply, SettingsEditor.save_project(socket, &reload_shell/2, &append_output_entry/5)}
|
{:noreply, SettingsEditor.save_project(socket, &reload_shell/2, &append_output_entry/5)}
|
||||||
end
|
end
|
||||||
@@ -517,6 +525,18 @@ defmodule BDS.Desktop.ShellLive do
|
|||||||
{:noreply, SettingsEditor.update_publishing_draft(socket, params, &reload_shell/2)}
|
{:noreply, SettingsEditor.update_publishing_draft(socket, params, &reload_shell/2)}
|
||||||
end
|
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
|
def handle_event("save_settings_publishing", _params, socket) do
|
||||||
{:noreply, SettingsEditor.save_publishing(socket, &reload_shell/2, &append_output_entry/5)}
|
{:noreply, SettingsEditor.save_publishing(socket, &reload_shell/2, &append_output_entry/5)}
|
||||||
end
|
end
|
||||||
@@ -533,6 +553,10 @@ defmodule BDS.Desktop.ShellLive do
|
|||||||
{:noreply, SettingsEditor.add_category(socket, &reload_shell/2, &append_output_entry/5)}
|
{:noreply, SettingsEditor.add_category(socket, &reload_shell/2, &append_output_entry/5)}
|
||||||
end
|
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
|
def handle_event("save_settings_category", %{"category_settings" => params}, socket) do
|
||||||
{:noreply, SettingsEditor.save_category(socket, params, &reload_shell/2, &append_output_entry/5)}
|
{:noreply, SettingsEditor.save_category(socket, params, &reload_shell/2, &append_output_entry/5)}
|
||||||
end
|
end
|
||||||
@@ -545,6 +569,10 @@ defmodule BDS.Desktop.ShellLive do
|
|||||||
{:noreply, apply_shell_command(socket, action)}
|
{:noreply, apply_shell_command(socket, action)}
|
||||||
end
|
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
|
def handle_event("select_style_theme", %{"theme" => theme}, socket) do
|
||||||
{:noreply, SettingsEditor.select_style_theme(socket, theme, &reload_shell/2)}
|
{:noreply, SettingsEditor.select_style_theme(socket, theme, &reload_shell/2)}
|
||||||
end
|
end
|
||||||
@@ -1326,6 +1354,7 @@ defmodule BDS.Desktop.ShellLive do
|
|||||||
|
|
||||||
tab_meta =
|
tab_meta =
|
||||||
Map.put(socket.assigns.tab_meta, {route_atom, tab_id}, %{
|
Map.put(socket.assigns.tab_meta, {route_atom, tab_id}, %{
|
||||||
|
sidebar_item_id: Map.get(params, "id"),
|
||||||
title: Map.get(params, "title", ""),
|
title: Map.get(params, "title", ""),
|
||||||
subtitle: Map.get(params, "subtitle", "")
|
subtitle: Map.get(params, "subtitle", "")
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -116,159 +116,160 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form class="media-details media-editor-details-form" data-testid="media-editor-form" phx-change="change_media_editor">
|
<div class="media-details">
|
||||||
<div class="editor-field">
|
<form class="media-editor-details-form" data-testid="media-editor-form" phx-change="change_media_editor">
|
||||||
<label><%= translated("File Name") %></label>
|
|
||||||
<input class="post-editor-input is-readonly" type="text" value={@media_editor.original_name} readonly />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="editor-field">
|
|
||||||
<label><%= translated("MIME Type") %></label>
|
|
||||||
<input class="post-editor-input is-readonly" type="text" value={@media_editor.mime_type} readonly />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="editor-field-row">
|
|
||||||
<div class="editor-field">
|
<div class="editor-field">
|
||||||
<label><%= translated("Size") %></label>
|
<label><%= translated("File Name") %></label>
|
||||||
<input class="post-editor-input is-readonly" type="text" value={@media_editor.file_size} readonly />
|
<input class="post-editor-input disabled" type="text" value={@media_editor.original_name} disabled />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%= if @media_editor.dimensions do %>
|
<div class="editor-field">
|
||||||
|
<label><%= translated("MIME Type") %></label>
|
||||||
|
<input class="post-editor-input disabled" type="text" value={@media_editor.mime_type} disabled />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="editor-field-row">
|
||||||
<div class="editor-field">
|
<div class="editor-field">
|
||||||
<label><%= translated("Dimensions") %></label>
|
<label><%= translated("Size") %></label>
|
||||||
<input class="post-editor-input is-readonly" type="text" value={@media_editor.dimensions} readonly />
|
<input class="post-editor-input disabled" type="text" value={@media_editor.file_size} disabled />
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="editor-field">
|
<%= if @media_editor.dimensions do %>
|
||||||
<label><%= translated("Title") %></label>
|
<div class="editor-field">
|
||||||
<input class="post-editor-input" type="text" name="media_editor[title]" value={@media_editor.form["title"]} />
|
<label><%= translated("Dimensions") %></label>
|
||||||
</div>
|
<input class="post-editor-input disabled" type="text" value={@media_editor.dimensions} disabled />
|
||||||
|
</div>
|
||||||
<div class="editor-field">
|
|
||||||
<label><%= translated("Alt Text") %></label>
|
|
||||||
<input class="post-editor-input" type="text" name="media_editor[alt]" value={@media_editor.form["alt"]} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="editor-field">
|
|
||||||
<label><%= translated("Caption") %></label>
|
|
||||||
<textarea class="post-editor-textarea" name="media_editor[caption]" rows="3"><%= @media_editor.form["caption"] %></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="editor-field">
|
|
||||||
<label><%= translated("Tags") %></label>
|
|
||||||
<input class="post-editor-input" type="text" name="media_editor[tags]" value={@media_editor.form["tags"]} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="editor-field">
|
|
||||||
<label><%= translated("Author") %></label>
|
|
||||||
<input class="post-editor-input" type="text" name="media_editor[author]" value={@media_editor.form["author"]} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="editor-field">
|
|
||||||
<label><%= translated("Language") %></label>
|
|
||||||
<select class="post-editor-input" name="media_editor[language]">
|
|
||||||
<option value=""><%= translated("None") %></option>
|
|
||||||
<%= for language <- @media_editor.languages do %>
|
|
||||||
<option value={language} selected={language == @media_editor.form["language"]}><%= language_label(language) %></option>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</select>
|
</div>
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<%= if @media_editor.form["language"] not in [nil, ""] do %>
|
<div class="editor-field">
|
||||||
<div class="editor-field media-translations-section">
|
<label><%= translated("Title") %></label>
|
||||||
<label><%= translated("Translations") %></label>
|
<input class="post-editor-input" type="text" name="media_editor[title]" value={@media_editor.form["title"]} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<%= if Enum.empty?(@media_editor.translations) do %>
|
<div class="editor-field">
|
||||||
<div class="no-linked-posts"><%= translated("No translations") %></div>
|
<label><%= translated("Alt Text") %></label>
|
||||||
<% else %>
|
<input class="post-editor-input" type="text" name="media_editor[alt]" value={@media_editor.form["alt"]} />
|
||||||
<div class="linked-posts-list">
|
</div>
|
||||||
<%= for translation <- @media_editor.translations do %>
|
|
||||||
<div class="linked-post-item">
|
<div class="editor-field">
|
||||||
<button
|
<label><%= translated("Caption") %></label>
|
||||||
class="linked-post-title linked-post-link"
|
<textarea class="post-editor-textarea" name="media_editor[caption]" rows="3"><%= @media_editor.form["caption"] %></textarea>
|
||||||
type="button"
|
</div>
|
||||||
phx-click="edit_media_translation"
|
|
||||||
phx-value-id={@media_editor.id}
|
<div class="editor-field">
|
||||||
phx-value-language={translation.language}
|
<label><%= translated("Tags") %></label>
|
||||||
>
|
<input class="post-editor-input" type="text" name="media_editor[tags]" value={@media_editor.form["tags"]} />
|
||||||
<%= translation.flag %> <%= language_label(translation.language) %><%= if translation.title, do: " - #{translation.title}" %>
|
</div>
|
||||||
</button>
|
|
||||||
<button class="secondary compact" type="button" phx-click="refresh_media_translation" phx-value-id={@media_editor.id} phx-value-language={translation.language}>
|
<div class="editor-field">
|
||||||
<%= translated("Refresh") %>
|
<label><%= translated("Author") %></label>
|
||||||
</button>
|
<input class="post-editor-input" type="text" name="media_editor[author]" value={@media_editor.form["author"]} />
|
||||||
<button class="unlink-btn" type="button" phx-click="delete_media_translation" phx-value-id={@media_editor.id} phx-value-language={translation.language}>×</button>
|
</div>
|
||||||
|
|
||||||
|
<div class="editor-field">
|
||||||
|
<label><%= translated("Language") %></label>
|
||||||
|
<select class="post-editor-input" name="media_editor[language]">
|
||||||
|
<option value=""><%= translated("None") %></option>
|
||||||
|
<%= for language <- @media_editor.languages do %>
|
||||||
|
<option value={language} selected={language == @media_editor.form["language"]}><%= language_label(language) %></option>
|
||||||
|
<% end %>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<%= if @media_editor.form["language"] not in [nil, ""] do %>
|
||||||
|
<div class="editor-field media-translations-section">
|
||||||
|
<label><%= translated("Translations") %></label>
|
||||||
|
|
||||||
|
<%= if Enum.empty?(@media_editor.translations) do %>
|
||||||
|
<div class="no-linked-posts"><%= translated("No translations") %></div>
|
||||||
|
<% else %>
|
||||||
|
<div class="linked-posts-list">
|
||||||
|
<%= for translation <- @media_editor.translations do %>
|
||||||
|
<div class="linked-post-item">
|
||||||
|
<button
|
||||||
|
class="linked-post-title linked-post-link"
|
||||||
|
type="button"
|
||||||
|
phx-click="edit_media_translation"
|
||||||
|
phx-value-id={@media_editor.id}
|
||||||
|
phx-value-language={translation.language}
|
||||||
|
>
|
||||||
|
<%= translation.flag %> <%= language_label(translation.language) %><%= if translation.title, do: " — #{translation.title}" %>
|
||||||
|
</button>
|
||||||
|
<button class="secondary compact" type="button" phx-click="refresh_media_translation" phx-value-id={@media_editor.id} phx-value-language={translation.language}>
|
||||||
|
<%= translated("Refresh") %>
|
||||||
|
</button>
|
||||||
|
<button class="unlink-btn" type="button" phx-click="delete_media_translation" phx-value-id={@media_editor.id} phx-value-language={translation.language}>×</button>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
</div>
|
<div class="editor-field linked-posts-section">
|
||||||
<% end %>
|
<label>
|
||||||
|
<%= translated("Linked Posts") %>
|
||||||
|
<button class="add-link-btn" type="button" phx-click="toggle_media_post_picker" phx-value-id={@media_editor.id}>
|
||||||
|
<%= translated("Link to Post") %>
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
|
|
||||||
<div class="editor-field linked-posts-section">
|
<%= if @media_editor.post_picker_open? do %>
|
||||||
<label>
|
<div class="post-picker">
|
||||||
<%= translated("Linked Posts") %>
|
<div class="post-picker-search">
|
||||||
<button class="add-link-btn" type="button" phx-click="toggle_media_post_picker" phx-value-id={@media_editor.id}>
|
<input
|
||||||
<%= translated("Link to Post") %>
|
type="text"
|
||||||
</button>
|
name="media_post_picker[query]"
|
||||||
</label>
|
value={@media_editor.post_picker_query}
|
||||||
|
placeholder={translated("Search posts")}
|
||||||
|
phx-change="change_media_post_picker"
|
||||||
|
phx-value-id={@media_editor.id}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<%= if @media_editor.post_picker_open? do %>
|
<%= if Enum.empty?(@media_editor.post_picker_results) do %>
|
||||||
<div class="post-picker">
|
<div class="no-posts"><%= translated("No posts to link") %></div>
|
||||||
<div class="post-picker-search">
|
<% else %>
|
||||||
<input
|
<div class="post-picker-list">
|
||||||
type="text"
|
<%= for result <- @media_editor.post_picker_results do %>
|
||||||
name="media_post_picker[query]"
|
<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}>
|
||||||
value={@media_editor.post_picker_query}
|
<%= result.title %>
|
||||||
placeholder={translated("Search posts")}
|
</button>
|
||||||
phx-change="change_media_post_picker"
|
<% end %>
|
||||||
phx-value-id={@media_editor.id}
|
<%= if @media_editor.post_picker_overflow_count > 0 do %>
|
||||||
/>
|
<div class="post-picker-more"><%= translated("and %{count} more", %{count: @media_editor.post_picker_overflow_count}) %></div>
|
||||||
</div>
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<%= if Enum.empty?(@media_editor.post_picker_results) do %>
|
<%= if Enum.empty?(@media_editor.linked_posts) do %>
|
||||||
<div class="no-posts"><%= translated("No posts to link") %></div>
|
<div class="no-linked-posts"><%= translated("Not linked to any posts") %></div>
|
||||||
<% else %>
|
<% else %>
|
||||||
<div class="post-picker-list">
|
<div class="linked-posts-list">
|
||||||
<%= for result <- @media_editor.post_picker_results do %>
|
<%= for linked_post <- @media_editor.linked_posts 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}>
|
<div class="linked-post-item">
|
||||||
<%= result.title %>
|
<button
|
||||||
</button>
|
class="linked-post-title linked-post-link"
|
||||||
<% end %>
|
type="button"
|
||||||
<%= if @media_editor.post_picker_overflow_count > 0 do %>
|
phx-click="pin_sidebar_item"
|
||||||
<div class="post-picker-more"><%= translated("and %{count} more", %{count: @media_editor.post_picker_overflow_count}) %></div>
|
phx-value-route="post"
|
||||||
|
phx-value-id={linked_post.post_id}
|
||||||
|
phx-value-title={linked_post.title}
|
||||||
|
phx-value-subtitle="linked post"
|
||||||
|
>
|
||||||
|
📄 <%= linked_post.title %>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
</div>
|
||||||
|
|
||||||
<%= if Enum.empty?(@media_editor.linked_posts) do %>
|
|
||||||
<div class="no-linked-posts"><%= translated("Not linked to any posts") %></div>
|
|
||||||
<% else %>
|
|
||||||
<div class="linked-posts-list">
|
|
||||||
<%= for linked_post <- @media_editor.linked_posts do %>
|
|
||||||
<div class="linked-post-item">
|
|
||||||
<button
|
|
||||||
class="linked-post-title linked-post-link"
|
|
||||||
type="button"
|
|
||||||
phx-click="pin_sidebar_item"
|
|
||||||
phx-value-route="post"
|
|
||||||
phx-value-id={linked_post.post_id}
|
|
||||||
phx-value-title={linked_post.title}
|
|
||||||
phx-value-subtitle="linked post"
|
|
||||||
>
|
|
||||||
📄 <%= linked_post.title %>
|
|
||||||
</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>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%= if @media_editor.editing_translation do %>
|
<%= if @media_editor.editing_translation do %>
|
||||||
|
|||||||
@@ -32,8 +32,6 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
|
|||||||
{:rerun,
|
{:rerun,
|
||||||
socket
|
socket
|
||||||
|> append_output.(translated("Site Validation"), translated("Validation changes applied"), inspect(result))}
|
|> 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
|
end
|
||||||
rescue
|
rescue
|
||||||
error -> {:socket, append_output.(socket, translated("Site Validation"), inspect(error), nil, "error")}
|
error -> {:socket, append_output.(socket, translated("Site Validation"), inspect(error), nil, "error")}
|
||||||
|
|||||||
@@ -3,11 +3,22 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do
|
|||||||
|
|
||||||
use Phoenix.Component
|
use Phoenix.Component
|
||||||
|
|
||||||
|
import Ecto.Query
|
||||||
|
|
||||||
|
alias BDS.AI
|
||||||
|
alias BDS.AI.Model
|
||||||
alias BDS.Metadata
|
alias BDS.Metadata
|
||||||
alias BDS.Desktop.ShellData
|
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/*"
|
embed_templates "settings_editor_html/*"
|
||||||
|
|
||||||
|
@settings_sections ~w(project editor content ai technology publishing data mcp)
|
||||||
|
|
||||||
@themes [
|
@themes [
|
||||||
"default",
|
"default",
|
||||||
"amber",
|
"amber",
|
||||||
@@ -33,6 +44,21 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do
|
|||||||
|
|
||||||
@supported_languages ["en", "de", "fr", "it", "es"]
|
@supported_languages ["en", "de", "fr", "it", "es"]
|
||||||
@protected_categories MapSet.new(["article", "aside", "page", "picture"])
|
@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
|
def assign_socket(socket) do
|
||||||
case socket.assigns[:current_tab] do
|
case socket.assigns[:current_tab] do
|
||||||
@@ -65,6 +91,30 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do
|
|||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
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
|
def save_project(socket, reload, append_output) do
|
||||||
project_id = socket.assigns.projects.active_project_id
|
project_id = socket.assigns.projects.active_project_id
|
||||||
|
|
||||||
@@ -87,6 +137,50 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do
|
|||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
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
|
def save_publishing(socket, reload, append_output) do
|
||||||
project_id = socket.assigns.projects.active_project_id
|
project_id = socket.assigns.projects.active_project_id
|
||||||
|
|
||||||
@@ -150,6 +244,54 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do
|
|||||||
end
|
end
|
||||||
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
|
def save_category(socket, params, reload, append_output) do
|
||||||
project_id = socket.assigns.projects.active_project_id
|
project_id = socket.assigns.projects.active_project_id
|
||||||
category = Map.get(params, "category", "")
|
category = Map.get(params, "category", "")
|
||||||
@@ -221,18 +363,37 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do
|
|||||||
def build_settings(assigns) do
|
def build_settings(assigns) do
|
||||||
metadata = project_metadata(assigns)
|
metadata = project_metadata(assigns)
|
||||||
project_form = Map.merge(project_form(metadata), Map.get(assigns, :settings_editor_project_draft, %{}))
|
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, %{}))
|
publishing_form = Map.merge(publishing_form(metadata), Map.get(assigns, :settings_editor_publishing_draft, %{}))
|
||||||
query = Map.get(assigns, :settings_editor_search, "")
|
query = Map.get(assigns, :settings_editor_search, "")
|
||||||
|
selected_section = current_settings_section(assigns)
|
||||||
|
visible_sections = visible_settings_sections(query)
|
||||||
|
|
||||||
%{
|
%{
|
||||||
search_query: query,
|
search_query: query,
|
||||||
|
selected_section: selected_section,
|
||||||
|
active_sections: visible_sections,
|
||||||
project: project_form,
|
project: project_form,
|
||||||
|
editor: editor_form,
|
||||||
categories: category_rows(metadata),
|
categories: category_rows(metadata),
|
||||||
|
ai: ai_form,
|
||||||
|
technology: technology_form(project_form),
|
||||||
publishing: publishing_form,
|
publishing: publishing_form,
|
||||||
|
mcp: mcp_rows(),
|
||||||
new_category: Map.get(assigns, :settings_editor_new_category, ""),
|
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)),
|
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)),
|
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)),
|
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)),
|
data_visible?: section_matches?(query, ~w(data rebuild maintenance folder filesystem)),
|
||||||
supported_languages: @supported_languages,
|
supported_languages: @supported_languages,
|
||||||
protected_categories: @protected_categories
|
protected_categories: @protected_categories
|
||||||
@@ -281,6 +442,17 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do
|
|||||||
}
|
}
|
||||||
end
|
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
|
defp publishing_attrs(assigns) do
|
||||||
draft = Map.get(assigns, :settings_editor_publishing_draft, %{})
|
draft = Map.get(assigns, :settings_editor_publishing_draft, %{})
|
||||||
|
|
||||||
@@ -292,10 +464,26 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do
|
|||||||
}
|
}
|
||||||
end
|
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
|
defp project_metadata(assigns) do
|
||||||
case Metadata.get_project_metadata(assigns.projects.active_project_id) do
|
case Metadata.get_project_metadata(assigns.projects.active_project_id) do
|
||||||
{:ok, metadata} -> metadata
|
{:ok, metadata} -> metadata
|
||||||
_other -> %{}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -313,6 +501,15 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do
|
|||||||
}
|
}
|
||||||
end
|
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
|
defp publishing_form(metadata) do
|
||||||
prefs = Map.get(metadata, :publishing_preferences, %{})
|
prefs = Map.get(metadata, :publishing_preferences, %{})
|
||||||
|
|
||||||
@@ -324,6 +521,30 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do
|
|||||||
}
|
}
|
||||||
end
|
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
|
defp current_theme(assigns) do
|
||||||
assigns
|
assigns
|
||||||
|> project_metadata()
|
|> project_metadata()
|
||||||
@@ -354,6 +575,8 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp category_names(metadata), do: Map.get(metadata, :categories, [])
|
||||||
|
|
||||||
defp normalize_project_params(params) do
|
defp normalize_project_params(params) do
|
||||||
%{
|
%{
|
||||||
"name" => Map.get(params, "name", ""),
|
"name" => Map.get(params, "name", ""),
|
||||||
@@ -368,6 +591,15 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do
|
|||||||
}
|
}
|
||||||
end
|
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
|
defp normalize_publishing_params(params) do
|
||||||
%{
|
%{
|
||||||
"ssh_host" => Map.get(params, "ssh_host", ""),
|
"ssh_host" => Map.get(params, "ssh_host", ""),
|
||||||
@@ -377,6 +609,217 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do
|
|||||||
}
|
}
|
||||||
end
|
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?("", _keywords), do: true
|
||||||
defp section_matches?(query, keywords), do: Enum.any?(keywords, &String.contains?(&1, String.downcase(query)))
|
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
|
end
|
||||||
|
|
||||||
defp truthy?(value), do: value in [true, "true", "on", "1", 1]
|
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(nil, fallback), do: fallback
|
||||||
defp parse_integer(value, _fallback) when is_integer(value), do: value
|
defp parse_integer(value, _fallback) when is_integer(value), do: value
|
||||||
defp parse_integer(value, fallback) do
|
defp parse_integer(value, fallback) do
|
||||||
@@ -406,4 +851,4 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do
|
|||||||
trimmed -> trimmed
|
trimmed -> trimmed
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<div class="settings-view-shell" data-testid="settings-editor">
|
<div class="settings-view-shell" data-testid="settings-editor" data-selected-settings-section={@settings_editor.selected_section}>
|
||||||
<div class="settings-view">
|
<div class="settings-view">
|
||||||
<div class="settings-header">
|
<div class="settings-header">
|
||||||
<h2 data-testid="editor-title"><%= translated("Settings") %></h2>
|
<h2 data-testid="editor-title"><%= translated("Settings") %></h2>
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-content">
|
<div class="settings-content">
|
||||||
<%= 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 %>
|
||||||
<div class="settings-no-results">
|
<div class="settings-no-results">
|
||||||
<p><%= translated("No settings match the current search") %></p>
|
<p><%= translated("No settings match the current search") %></p>
|
||||||
</div>
|
</div>
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
<div class="setting-section" id="settings-section-project">
|
<div class="setting-section" id="settings-section-project">
|
||||||
<div class="setting-section-header">
|
<div class="setting-section-header">
|
||||||
<h3><%= translated("Project") %></h3>
|
<h3><%= translated("Project") %></h3>
|
||||||
|
<p class="setting-section-description"><%= translated("Blog identity, URLs, authoring defaults, and bookmarklet setup") %></p>
|
||||||
</div>
|
</div>
|
||||||
<form class="setting-section-content" phx-change="change_settings_project">
|
<form class="setting-section-content" phx-change="change_settings_project">
|
||||||
<div class="setting-row">
|
<div class="setting-row">
|
||||||
@@ -30,6 +31,15 @@
|
|||||||
<div class="setting-info"><label class="setting-label"><%= translated("Description") %></label></div>
|
<div class="setting-info"><label class="setting-label"><%= translated("Description") %></label></div>
|
||||||
<div class="setting-control"><textarea name="settings_project[description]" rows="3"><%= @settings_editor.project["description"] %></textarea></div>
|
<div class="setting-control"><textarea name="settings_project[description]" rows="3"><%= @settings_editor.project["description"] %></textarea></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="setting-row">
|
||||||
|
<div class="setting-info"><label class="setting-label"><%= translated("Data Path") %></label></div>
|
||||||
|
<div class="setting-control">
|
||||||
|
<div class="setting-input-group">
|
||||||
|
<input type="text" value={@settings_editor.project_data_path} readonly />
|
||||||
|
<button class="secondary" type="button" phx-click="settings_shell_command" phx-value-action="open_data_folder"><%= translated("Open") %></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="setting-row">
|
<div class="setting-row">
|
||||||
<div class="setting-info"><label class="setting-label"><%= translated("Public URL") %></label></div>
|
<div class="setting-info"><label class="setting-label"><%= translated("Public URL") %></label></div>
|
||||||
<div class="setting-control"><input type="url" name="settings_project[public_url]" value={@settings_editor.project["public_url"]} /></div>
|
<div class="setting-control"><input type="url" name="settings_project[public_url]" value={@settings_editor.project["public_url"]} /></div>
|
||||||
@@ -76,34 +86,107 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-row">
|
<div class="setting-row">
|
||||||
<div class="setting-info"><label class="setting-label"><%= translated("Semantic Similarity") %></label></div>
|
<div class="setting-info"><label class="setting-label"><%= translated("Blogmark Bookmarklet") %></label></div>
|
||||||
<div class="setting-control">
|
<div class="setting-control"><p class="setting-description"><%= translated("Bookmarklet copy support is wired through the desktop runtime and project public URL.") %></p></div>
|
||||||
<label><input type="checkbox" name="settings_project[semantic_similarity_enabled]" checked={@settings_editor.project["semantic_similarity_enabled"]} /> <%= translated("Enable semantic similarity") %></label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<div class="setting-actions"><button class="primary" type="button" phx-click="save_settings_project"><%= translated("Save") %></button></div>
|
<div class="setting-actions"><button class="primary" type="button" phx-click="save_settings_project"><%= translated("Save") %></button></div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
<%= if @settings_editor.editor_visible? do %>
|
||||||
|
<div class="setting-section" id="settings-section-editor">
|
||||||
|
<div class="setting-section-header">
|
||||||
|
<h3><%= translated("Editor") %></h3>
|
||||||
|
<p class="setting-section-description"><%= translated("Default editing mode and diff presentation") %></p>
|
||||||
|
</div>
|
||||||
|
<form class="setting-section-content" phx-change="change_settings_editor">
|
||||||
|
<div class="setting-row">
|
||||||
|
<div class="setting-info"><label class="setting-label"><%= translated("Default Editor Mode") %></label></div>
|
||||||
|
<div class="setting-control">
|
||||||
|
<select name="settings_editor[default_mode]">
|
||||||
|
<option value="wysiwyg" selected={@settings_editor.editor["default_mode"] == "wysiwyg"}><%= translated("WYSIWYG") %></option>
|
||||||
|
<option value="markdown" selected={@settings_editor.editor["default_mode"] == "markdown"}><%= translated("Markdown") %></option>
|
||||||
|
<option value="preview" selected={@settings_editor.editor["default_mode"] == "preview"}><%= translated("Preview") %></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="setting-row">
|
||||||
|
<div class="setting-info"><label class="setting-label"><%= translated("Diff View Style") %></label></div>
|
||||||
|
<div class="setting-control">
|
||||||
|
<select name="settings_editor[diff_view_style]">
|
||||||
|
<option value="inline" selected={@settings_editor.editor["diff_view_style"] == "inline"}><%= translated("Inline") %></option>
|
||||||
|
<option value="side-by-side" selected={@settings_editor.editor["diff_view_style"] == "side-by-side"}><%= translated("Side by Side") %></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="setting-row">
|
||||||
|
<div class="setting-info"><label class="setting-label"><%= translated("Wrap Long Lines") %></label></div>
|
||||||
|
<div class="setting-control"><label><input type="checkbox" name="settings_editor[wrap_long_lines]" checked={@settings_editor.editor["wrap_long_lines"]} /> <%= translated("Enable line wrapping in diffs") %></label></div>
|
||||||
|
</div>
|
||||||
|
<div class="setting-row">
|
||||||
|
<div class="setting-info"><label class="setting-label"><%= translated("Hide Unchanged Regions") %></label></div>
|
||||||
|
<div class="setting-control"><label><input type="checkbox" name="settings_editor[hide_unchanged_regions]" checked={@settings_editor.editor["hide_unchanged_regions"]} /> <%= translated("Collapse unchanged diff hunks") %></label></div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="setting-actions"><button class="primary" type="button" phx-click="save_settings_editor"><%= translated("Save") %></button></div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<%= if @settings_editor.content_visible? do %>
|
<%= if @settings_editor.content_visible? do %>
|
||||||
<div class="setting-section" id="settings-section-content">
|
<div class="setting-section" id="settings-section-content">
|
||||||
<div class="setting-section-header"><h3><%= translated("Content Categories") %></h3></div>
|
<div class="setting-section-header"><h3><%= translated("Content Categories") %></h3><p class="setting-section-description"><%= translated("Category defaults, rendering flags, and template wiring") %></p></div>
|
||||||
<div class="setting-section-content">
|
<div class="setting-section-content">
|
||||||
<%= for category <- @settings_editor.categories do %>
|
<table class="categories-table">
|
||||||
<form class="setting-row" phx-change="save_settings_category">
|
<thead>
|
||||||
<input type="hidden" name="category_settings[category]" value={category.name} />
|
<tr>
|
||||||
<div class="setting-info"><label class="setting-label"><%= category.name %></label></div>
|
<th><%= translated("Category") %></th>
|
||||||
<div class="setting-control">
|
<th><%= translated("Title") %></th>
|
||||||
<div class="setting-input-group">
|
<th><%= translated("Render in Lists") %></th>
|
||||||
<input type="text" name="category_settings[title]" value={category.title} />
|
<th><%= translated("Show Titles") %></th>
|
||||||
<label><input type="checkbox" name="category_settings[render_in_lists]" checked={category.render_in_lists} /> <%= translated("Render in Lists") %></label>
|
<th><%= translated("Post Template") %></th>
|
||||||
<label><input type="checkbox" name="category_settings[show_title]" checked={category.show_title} /> <%= translated("Show Titles") %></label>
|
<th><%= translated("List Template") %></th>
|
||||||
<button class="secondary" type="button" phx-click="remove_settings_category" phx-value-category={category.name} disabled={category.protected?}><%= translated("Remove") %></button>
|
<th><%= translated("Actions") %></th>
|
||||||
</div>
|
</tr>
|
||||||
</div>
|
</thead>
|
||||||
</form>
|
<tbody>
|
||||||
<% end %>
|
<%= for category <- @settings_editor.categories do %>
|
||||||
|
<tr>
|
||||||
|
<td><%= category.name %></td>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="category_settings[title]" value={category.title} form={"category-form-#{category.name}"} />
|
||||||
|
</td>
|
||||||
|
<td><input type="checkbox" name="category_settings[render_in_lists]" checked={category.render_in_lists} form={"category-form-#{category.name}"} /></td>
|
||||||
|
<td><input type="checkbox" name="category_settings[show_title]" checked={category.show_title} form={"category-form-#{category.name}"} /></td>
|
||||||
|
<td>
|
||||||
|
<select name="category_settings[post_template_slug]" form={"category-form-#{category.name}"}>
|
||||||
|
<option value=""><%= translated("Default") %></option>
|
||||||
|
<%= for template <- @settings_editor.template_options.post do %>
|
||||||
|
<option value={template.slug} selected={template.slug == category.post_template_slug}><%= template.title %></option>
|
||||||
|
<% end %>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<select name="category_settings[list_template_slug]" form={"category-form-#{category.name}"}>
|
||||||
|
<option value=""><%= translated("Default") %></option>
|
||||||
|
<%= for template <- @settings_editor.template_options.list do %>
|
||||||
|
<option value={template.slug} selected={template.slug == category.list_template_slug}><%= template.title %></option>
|
||||||
|
<% end %>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="setting-input-group">
|
||||||
|
<form id={"category-form-#{category.name}"} phx-submit="save_settings_category">
|
||||||
|
<input type="hidden" name="category_settings[category]" value={category.name} />
|
||||||
|
</form>
|
||||||
|
<button class="secondary" type="submit" form={"category-form-#{category.name}"}><%= translated("Save") %></button>
|
||||||
|
<button class="secondary" type="button" phx-click="remove_settings_category" phx-value-category={category.name} disabled={category.protected?}><%= translated("Remove") %></button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
<div class="setting-row">
|
<div class="setting-row">
|
||||||
<div class="setting-info"><label class="setting-label"><%= translated("Add Category") %></label></div>
|
<div class="setting-info"><label class="setting-label"><%= translated("Add Category") %></label></div>
|
||||||
<div class="setting-control">
|
<div class="setting-control">
|
||||||
@@ -113,13 +196,122 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="setting-actions"><button class="secondary" type="button" phx-click="reset_settings_categories"><%= translated("Reset to Defaults") %></button></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
<%= if @settings_editor.ai_visible? do %>
|
||||||
|
<div class="setting-section" id="settings-section-ai">
|
||||||
|
<div class="setting-section-header"><h3><%= translated("AI") %></h3><p class="setting-section-description"><%= translated("Provider keys, model preferences, airplane mode, and system prompt") %></p></div>
|
||||||
|
<form class="setting-section-content" phx-change="change_settings_ai">
|
||||||
|
<div class="setting-row">
|
||||||
|
<div class="setting-info"><label class="setting-label"><%= translated("Anthropic / Online API Key") %></label></div>
|
||||||
|
<div class="setting-control"><input type="password" name="settings_ai[online_api_key]" value={@settings_editor.ai["online_api_key"]} /></div>
|
||||||
|
</div>
|
||||||
|
<div class="setting-row">
|
||||||
|
<div class="setting-info"><label class="setting-label"><%= translated("Mistral API Key") %></label></div>
|
||||||
|
<div class="setting-control"><input type="password" name="settings_ai[mistral_api_key]" value={@settings_editor.ai["mistral_api_key"]} /></div>
|
||||||
|
</div>
|
||||||
|
<div class="setting-row">
|
||||||
|
<div class="setting-info"><label class="setting-label"><%= translated("Offline Mode") %></label></div>
|
||||||
|
<div class="setting-control"><label><input type="checkbox" name="settings_ai[offline_mode]" checked={@settings_editor.ai["offline_mode"]} /> <%= translated("Route AI tasks through the airplane endpoint") %></label></div>
|
||||||
|
</div>
|
||||||
|
<div class="setting-row">
|
||||||
|
<div class="setting-info"><label class="setting-label"><%= translated("Default Model") %></label></div>
|
||||||
|
<div class="setting-control">
|
||||||
|
<select name="settings_ai[default_model]">
|
||||||
|
<option value=""></option>
|
||||||
|
<%= for model <- @settings_editor.model_options do %>
|
||||||
|
<option value={model.id} selected={model.id == @settings_editor.ai["default_model"]}><%= model.label %></option>
|
||||||
|
<% end %>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="setting-row">
|
||||||
|
<div class="setting-info"><label class="setting-label"><%= translated("Title Model") %></label></div>
|
||||||
|
<div class="setting-control">
|
||||||
|
<select name="settings_ai[title_model]">
|
||||||
|
<option value=""></option>
|
||||||
|
<%= for model <- @settings_editor.model_options do %>
|
||||||
|
<option value={model.id} selected={model.id == @settings_editor.ai["title_model"]}><%= model.label %></option>
|
||||||
|
<% end %>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="setting-row">
|
||||||
|
<div class="setting-info"><label class="setting-label"><%= translated("Image Analysis Model") %></label></div>
|
||||||
|
<div class="setting-control">
|
||||||
|
<select name="settings_ai[image_analysis_model]">
|
||||||
|
<option value=""></option>
|
||||||
|
<%= for model <- @settings_editor.image_model_options do %>
|
||||||
|
<option value={model.id} selected={model.id == @settings_editor.ai["image_analysis_model"]}><%= model.label %></option>
|
||||||
|
<% end %>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="setting-row">
|
||||||
|
<div class="setting-info"><label class="setting-label"><%= translated("Offline Chat Model") %></label></div>
|
||||||
|
<div class="setting-control">
|
||||||
|
<select name="settings_ai[offline_chat_model]">
|
||||||
|
<option value=""></option>
|
||||||
|
<%= for model <- @settings_editor.model_options do %>
|
||||||
|
<option value={model.id} selected={model.id == @settings_editor.ai["offline_chat_model"]}><%= model.label %></option>
|
||||||
|
<% end %>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="setting-row">
|
||||||
|
<div class="setting-info"><label class="setting-label"><%= translated("Offline Title Model") %></label></div>
|
||||||
|
<div class="setting-control">
|
||||||
|
<select name="settings_ai[offline_title_model]">
|
||||||
|
<option value=""></option>
|
||||||
|
<%= for model <- @settings_editor.model_options do %>
|
||||||
|
<option value={model.id} selected={model.id == @settings_editor.ai["offline_title_model"]}><%= model.label %></option>
|
||||||
|
<% end %>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="setting-row">
|
||||||
|
<div class="setting-info"><label class="setting-label"><%= translated("Offline Image Analysis Model") %></label></div>
|
||||||
|
<div class="setting-control">
|
||||||
|
<select name="settings_ai[offline_image_analysis_model]">
|
||||||
|
<option value=""></option>
|
||||||
|
<%= for model <- @settings_editor.image_model_options do %>
|
||||||
|
<option value={model.id} selected={model.id == @settings_editor.ai["offline_image_analysis_model"]}><%= model.label %></option>
|
||||||
|
<% end %>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="setting-row">
|
||||||
|
<div class="setting-info"><label class="setting-label"><%= translated("System Prompt") %></label></div>
|
||||||
|
<div class="setting-control"><textarea name="settings_ai[system_prompt]" rows="12"><%= @settings_editor.ai["system_prompt"] %></textarea></div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="setting-actions"><button class="primary" type="button" phx-click="save_settings_ai"><%= translated("Save") %></button><button class="secondary" type="button" phx-click="reset_settings_ai_prompt"><%= translated("Reset to Default") %></button></div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%= if @settings_editor.technology_visible? do %>
|
||||||
|
<div class="setting-section" id="settings-section-technology">
|
||||||
|
<div class="setting-section-header"><h3><%= translated("Technology") %></h3><p class="setting-section-description"><%= translated("Application-level runtime behavior and semantic indexing") %></p></div>
|
||||||
|
<form class="setting-section-content" phx-change="change_settings_project">
|
||||||
|
<div class="setting-row">
|
||||||
|
<div class="setting-info"><label class="setting-label"><%= translated("Semantic Similarity") %></label></div>
|
||||||
|
<div class="setting-control"><label><input type="checkbox" name="settings_project[semantic_similarity_enabled]" checked={@settings_editor.technology["semantic_similarity_enabled"]} /> <%= translated("Enable duplicate search and related-post embeddings") %></label></div>
|
||||||
|
</div>
|
||||||
|
<div class="setting-row">
|
||||||
|
<div class="setting-info"><label class="setting-label"><%= translated("Scripting Runtime") %></label></div>
|
||||||
|
<div class="setting-control"><p class="setting-description"><%= translated("Scripting capabilities are configured at the application layer in the rewrite and do not expose runtime switching here.") %></p></div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="setting-actions"><button class="primary" type="button" phx-click="save_settings_project"><%= translated("Save") %></button></div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<%= if @settings_editor.publishing_visible? do %>
|
<%= if @settings_editor.publishing_visible? do %>
|
||||||
<div class="setting-section" id="settings-section-publishing">
|
<div class="setting-section" id="settings-section-publishing">
|
||||||
<div class="setting-section-header"><h3><%= translated("Publishing") %></h3></div>
|
<div class="setting-section-header"><h3><%= translated("Publishing") %></h3><p class="setting-section-description"><%= translated("Deployment credentials for upload tasks") %></p></div>
|
||||||
<form class="setting-section-content" phx-change="change_settings_publishing">
|
<form class="setting-section-content" phx-change="change_settings_publishing">
|
||||||
<div class="setting-row"><div class="setting-info"><label class="setting-label"><%= translated("SSH Mode") %></label></div><div class="setting-control"><select name="settings_publishing[ssh_mode]"><option value="scp" selected={@settings_editor.publishing["ssh_mode"] == "scp"}>scp</option><option value="rsync" selected={@settings_editor.publishing["ssh_mode"] == "rsync"}>rsync</option></select></div></div>
|
<div class="setting-row"><div class="setting-info"><label class="setting-label"><%= translated("SSH Mode") %></label></div><div class="setting-control"><select name="settings_publishing[ssh_mode]"><option value="scp" selected={@settings_editor.publishing["ssh_mode"] == "scp"}>scp</option><option value="rsync" selected={@settings_editor.publishing["ssh_mode"] == "rsync"}>rsync</option></select></div></div>
|
||||||
<div class="setting-row"><div class="setting-info"><label class="setting-label"><%= translated("Host") %></label></div><div class="setting-control"><input type="text" name="settings_publishing[ssh_host]" value={@settings_editor.publishing["ssh_host"]} /></div></div>
|
<div class="setting-row"><div class="setting-info"><label class="setting-label"><%= translated("Host") %></label></div><div class="setting-control"><input type="text" name="settings_publishing[ssh_host]" value={@settings_editor.publishing["ssh_host"]} /></div></div>
|
||||||
@@ -130,11 +322,37 @@
|
|||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
<%= if @settings_editor.mcp_visible? do %>
|
||||||
|
<div class="setting-section" id="settings-section-mcp">
|
||||||
|
<div class="setting-section-header"><h3><%= translated("MCP") %></h3><p class="setting-section-description"><%= translated("Agent configuration files for the built-in bDS MCP server") %></p></div>
|
||||||
|
<div class="setting-section-content">
|
||||||
|
<%= for agent <- @settings_editor.mcp do %>
|
||||||
|
<div class="setting-row">
|
||||||
|
<div class="setting-info">
|
||||||
|
<label class="setting-label"><%= agent.label %></label>
|
||||||
|
<p class="setting-description"><%= agent.config_path || translated("Not supported in the rewrite yet") %></p>
|
||||||
|
</div>
|
||||||
|
<div class="setting-control">
|
||||||
|
<button class="secondary" type="button" phx-click="toggle_settings_mcp_agent" phx-value-agent={agent.id} disabled={not agent.supported?}>
|
||||||
|
<%= if agent.configured?, do: translated("Remove"), else: translated("Add") %>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<%= if @settings_editor.data_visible? do %>
|
<%= if @settings_editor.data_visible? do %>
|
||||||
<div class="setting-section" id="settings-section-data">
|
<div class="setting-section" id="settings-section-data">
|
||||||
<div class="setting-section-header"><h3><%= translated("Data Maintenance") %></h3></div>
|
<div class="setting-section-header"><h3><%= translated("Data Maintenance") %></h3><p class="setting-section-description"><%= translated("Rebuild filesystem-backed records and thumbnails") %></p></div>
|
||||||
<div class="setting-actions">
|
<div class="setting-actions">
|
||||||
<button class="secondary" type="button" phx-click="settings_shell_command" phx-value-action="rebuild_database"><%= translated("Rebuild Database") %></button>
|
<button class="secondary" type="button" phx-click="settings_shell_command" phx-value-action="rebuild_posts_from_files"><%= translated("Rebuild Posts From Files") %></button>
|
||||||
|
<button class="secondary" type="button" phx-click="settings_shell_command" phx-value-action="rebuild_media_from_files"><%= translated("Rebuild Media From Files") %></button>
|
||||||
|
<button class="secondary" type="button" phx-click="settings_shell_command" phx-value-action="rebuild_scripts_from_files"><%= translated("Rebuild Scripts From Files") %></button>
|
||||||
|
<button class="secondary" type="button" phx-click="settings_shell_command" phx-value-action="rebuild_templates_from_files"><%= translated("Rebuild Templates From Files") %></button>
|
||||||
|
<button class="secondary" type="button" phx-click="settings_shell_command" phx-value-action="rebuild_post_links"><%= translated("Rebuild Links") %></button>
|
||||||
|
<button class="secondary" type="button" phx-click="settings_shell_command" phx-value-action="regenerate_missing_thumbnails"><%= translated("Regenerate Missing Thumbnails") %></button>
|
||||||
<button class="secondary" type="button" phx-click="settings_shell_command" phx-value-action="rebuild_embedding_index"><%= translated("Rebuild Embedding Index") %></button>
|
<button class="secondary" type="button" phx-click="settings_shell_command" phx-value-action="rebuild_embedding_index"><%= translated("Rebuild Embedding Index") %></button>
|
||||||
<button class="secondary" type="button" phx-click="settings_shell_command" phx-value-action="open_data_folder"><%= translated("Open Data Folder") %></button>
|
<button class="secondary" type="button" phx-click="settings_shell_command" phx-value-action="open_data_folder"><%= translated("Open Data Folder") %></button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -249,5 +249,94 @@
|
|||||||
"Ask the assistant about the active project or editor.": "Frage den Assistenten zum aktiven Projekt oder Editor.",
|
"Ask the assistant about the active project or editor.": "Frage den Assistenten zum aktiven Projekt oder Editor.",
|
||||||
"Start chat": "Chat starten",
|
"Start chat": "Chat starten",
|
||||||
"You": "Du",
|
"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"
|
||||||
}
|
}
|
||||||
@@ -249,5 +249,94 @@
|
|||||||
"Ask the assistant about the active project or editor.": "Ask the assistant about the active project or editor.",
|
"Ask the assistant about the active project or editor.": "Ask the assistant about the active project or editor.",
|
||||||
"Start chat": "Start chat",
|
"Start chat": "Start chat",
|
||||||
"You": "You",
|
"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"
|
||||||
}
|
}
|
||||||
@@ -249,5 +249,94 @@
|
|||||||
"Ask the assistant about the active project or editor.": "Pregunta al asistente sobre el proyecto o editor activo.",
|
"Ask the assistant about the active project or editor.": "Pregunta al asistente sobre el proyecto o editor activo.",
|
||||||
"Start chat": "Iniciar chat",
|
"Start chat": "Iniciar chat",
|
||||||
"You": "Tú",
|
"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"
|
||||||
}
|
}
|
||||||
@@ -249,5 +249,94 @@
|
|||||||
"Ask the assistant about the active project or editor.": "Interrogez l’assistant sur le projet ou l’éditeur actif.",
|
"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",
|
"Start chat": "Démarrer la conversation",
|
||||||
"You": "Vous",
|
"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"
|
||||||
}
|
}
|
||||||
@@ -249,5 +249,94 @@
|
|||||||
"Ask the assistant about the active project or editor.": "Chiedi all’assistente del progetto o editor attivo.",
|
"Ask the assistant about the active project or editor.": "Chiedi all’assistente del progetto o editor attivo.",
|
||||||
"Start chat": "Avvia chat",
|
"Start chat": "Avvia chat",
|
||||||
"You": "Tu",
|
"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"
|
||||||
}
|
}
|
||||||
518
priv/ui/app.css
518
priv/ui/app.css
@@ -2598,18 +2598,339 @@ button svg * {
|
|||||||
font: inherit;
|
font: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-editor {
|
[data-testid="media-editor"] {
|
||||||
display: grid;
|
flex: 1;
|
||||||
grid-template-columns: minmax(320px, 0.95fr) minmax(360px, 1.05fr);
|
|
||||||
gap: 20px;
|
|
||||||
padding: 20px;
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-editor-details-form {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
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 {
|
.translation-modal-backdrop {
|
||||||
@@ -3091,7 +3412,7 @@ button svg * {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1100px) {
|
@media (max-width: 1100px) {
|
||||||
.media-editor,
|
[data-testid="media-editor"] > .editor-content.media-editor,
|
||||||
.setting-row,
|
.setting-row,
|
||||||
.tag-form-row,
|
.tag-form-row,
|
||||||
.editor-field-row,
|
.editor-field-row,
|
||||||
@@ -3100,6 +3421,16 @@ button svg * {
|
|||||||
grid-template-columns: 1fr;
|
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 {
|
.style-theme-picker {
|
||||||
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
|
||||||
}
|
}
|
||||||
@@ -3340,171 +3671,6 @@ button svg * {
|
|||||||
|
|
||||||
.lightbox-image-container {
|
.lightbox-image-container {
|
||||||
max-width: 90%;
|
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%;
|
max-height: 78%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -860,6 +860,9 @@ defmodule BDS.Desktop.ShellLiveTest do
|
|||||||
assert html =~ ~s(class="editor-content media-editor")
|
assert html =~ ~s(class="editor-content media-editor")
|
||||||
assert html =~ ~s(class="quick-actions-wrapper")
|
assert html =~ ~s(class="quick-actions-wrapper")
|
||||||
refute html =~ ~s(class="media-editor-form")
|
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"})
|
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]")
|
assert html =~ ~s(name="media_translation[caption]")
|
||||||
end
|
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
|
test "remaining step-5 routes render dedicated editors instead of the generic shell placeholder", %{project: project} do
|
||||||
assert {:ok, script} =
|
assert {:ok, script} =
|
||||||
Scripts.create_script(%{
|
Scripts.create_script(%{
|
||||||
@@ -965,6 +1016,28 @@ defmodule BDS.Desktop.ShellLiveTest do
|
|||||||
refute chat_html =~ "Desktop workbench content routed through the Elixir shell."
|
refute chat_html =~ "Desktop workbench content routed through the Elixir shell."
|
||||||
end
|
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
|
test "template sidebar exposes old-app style delete control and removes template rows", %{project: project} do
|
||||||
assert {:ok, template} =
|
assert {:ok, template} =
|
||||||
BDS.Templates.create_template(%{
|
BDS.Templates.create_template(%{
|
||||||
|
|||||||
@@ -218,6 +218,27 @@ defmodule BDS.UI.ShellTest do
|
|||||||
assert live_ex =~ "titlebar_menu_item_index"
|
assert live_ex =~ "titlebar_menu_item_index"
|
||||||
end
|
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
|
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")
|
template = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/index.html.heex")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user