Files
bDS2/lib/bds/desktop/shell_live/settings_editor.ex
2026-05-01 17:49:50 +02:00

240 lines
8.8 KiB
Elixir

defmodule BDS.Desktop.ShellLive.SettingsEditor do
@moduledoc false
use Phoenix.Component
import Ecto.Query
alias BDS.Desktop.ShellData
alias BDS.Repo
alias BDS.Templates.Template
alias BDS.Desktop.ShellLive.SettingsEditor.AISettings
alias BDS.Desktop.ShellLive.SettingsEditor.EditorSettings
alias BDS.Desktop.ShellLive.SettingsEditor.ManagedCategories
alias BDS.Desktop.ShellLive.SettingsEditor.MCPConfig
alias BDS.Desktop.ShellLive.SettingsEditor.ProjectSettings
alias BDS.Desktop.ShellLive.SettingsEditor.PublishingSettings
alias BDS.Desktop.ShellLive.SettingsEditor.StyleEditor
embed_templates("settings_editor_html/*")
@settings_sections ~w(project editor content ai technology publishing data mcp)
@supported_languages ["en", "de", "fr", "it", "es"]
defdelegate update_project_draft(socket, params, reload), to: ProjectSettings
defdelegate save_project(socket, reload, append_output), to: ProjectSettings
defdelegate update_editor_draft(socket, params, reload), to: EditorSettings
defdelegate save_editor(socket, reload, append_output), to: EditorSettings
defdelegate update_publishing_draft(socket, params, reload), to: PublishingSettings
defdelegate save_publishing(socket, reload, append_output), to: PublishingSettings
defdelegate clear_publishing(socket, reload, append_output), to: PublishingSettings
defdelegate update_ai_draft(socket, params, reload), to: AISettings
defdelegate refresh_ai_models(socket, endpoint_key, reload, append_output), to: AISettings
defdelegate save_ai(socket, reload, append_output), to: AISettings
defdelegate reset_ai_prompt(socket, reload, append_output), to: AISettings
defdelegate update_new_category(socket, name, reload), to: ManagedCategories
defdelegate add_category(socket, reload, append_output), to: ManagedCategories
defdelegate reset_categories(socket, reload, append_output), to: ManagedCategories
defdelegate save_category(socket, params, reload, append_output), to: ManagedCategories
defdelegate remove_category(socket, category, reload, append_output), to: ManagedCategories
defdelegate toggle_mcp_agent(socket, agent, reload, append_output), to: MCPConfig
defdelegate select_style_theme(socket, theme, reload), to: StyleEditor
defdelegate change_style_preview_mode(socket, mode, reload), to: StyleEditor
defdelegate apply_style_theme(socket, reload, append_output), to: StyleEditor
defdelegate theme_display_name(theme), to: StyleEditor
defdelegate protected_category?(category), to: ManagedCategories
@spec assign_socket(term()) :: term()
def assign_socket(socket) do
case socket.assigns[:current_tab] do
%{type: :settings} ->
socket
|> assign(:settings_editor, build_settings(socket.assigns))
|> assign(:style_editor, nil)
%{type: :style} ->
socket
|> assign(:settings_editor, nil)
|> assign(:style_editor, StyleEditor.build_style(socket.assigns))
_other ->
socket
|> assign(:settings_editor, nil)
|> assign(:style_editor, nil)
end
end
@spec update_search(term(), term(), term()) :: term()
def update_search(socket, query, reload) do
socket
|> assign(:settings_editor_search, to_string(query || ""))
|> reload.(socket.assigns.workbench)
end
@spec build_settings(term()) :: term()
def build_settings(%{projects: %{active_project_id: nil}}), do: nil
def build_settings(assigns) do
metadata = ProjectSettings.project_metadata(assigns)
project_form =
Map.merge(
ProjectSettings.project_form(metadata),
Map.get(assigns, :settings_editor_project_draft, %{})
)
editor_form =
Map.merge(
EditorSettings.editor_form(),
Map.get(assigns, :settings_editor_editor_draft, %{})
)
ai_form =
Map.merge(AISettings.ai_form(assigns), Map.get(assigns, :settings_editor_ai_draft, %{}))
publishing_form =
Map.merge(
PublishingSettings.publishing_form(metadata),
Map.get(assigns, :settings_editor_publishing_draft, %{})
)
query = Map.get(assigns, :settings_editor_search, "")
selected_section = current_settings_section(assigns)
visible_sections = visible_settings_sections(query)
%{
search_query: query,
selected_section: selected_section,
active_sections: visible_sections,
project: project_form,
editor: editor_form,
categories: ManagedCategories.category_rows(metadata),
ai: ai_form,
technology: ProjectSettings.technology_form(project_form),
publishing: publishing_form,
mcp: MCPConfig.mcp_rows(),
new_category: Map.get(assigns, :settings_editor_new_category, ""),
project_data_path: Map.get(assigns.current_project || %{}, :data_path) || "",
project_data_default_path: Map.get(assigns.current_project || %{}, :project_path) || "",
template_options: template_options(assigns.projects.active_project_id),
online_endpoint_models: AISettings.endpoint_model_options(assigns, :online),
offline_endpoint_models: AISettings.endpoint_model_options(assigns, :airplane),
project_visible?:
section_matches?(
query,
~w(project name description url language author category posts bookmarklet)
),
editor_visible?:
section_matches?(query, ~w(editor mode markdown preview diff wrap unchanged)),
content_visible?: section_matches?(query, ~w(content categories templates lists blogmark)),
ai_visible?:
section_matches?(
query,
~w(ai assistant model prompt airplane offline online endpoint url api key chat title image)
),
technology_visible?:
section_matches?(query, ~w(technology runtime semantic similarity embedding scripting)),
publishing_visible?:
section_matches?(query, ~w(publishing ssh scp rsync host user remote path)),
mcp_visible?:
section_matches?(
query,
~w(mcp claude copilot gemini opencode mistral codex agent server)
),
data_visible?: section_matches?(query, ~w(data rebuild maintenance folder filesystem)),
supported_languages: @supported_languages,
protected_categories: ManagedCategories.protected_categories()
}
end
@spec translated(term(), term()) :: term()
def translated(text, bindings \\ %{}),
do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current())
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 airplane offline online endpoint url api key chat title image)
)
"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 section_matches?("", _keywords), do: true
defp section_matches?(query, keywords),
do: Enum.any?(keywords, &String.contains?(&1, String.downcase(query)))
end