chore: refactored settings_editor.ex
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Living document. Each section lists **status**, then **open work in priority order**, then a brief notes block. Completed work is listed compactly at the bottom (`## Changelog`).
|
Living document. Each section lists **status**, then **open work in priority order**, then a brief notes block. Completed work is listed compactly at the bottom (`## Changelog`).
|
||||||
|
|
||||||
Last refreshed: 2026-05-06.
|
Last refreshed: 2026-05-07.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -14,7 +14,6 @@ Last refreshed: 2026-05-06.
|
|||||||
|
|
||||||
| # | Module | Current lines | Target | Strategy |
|
| # | Module | Current lines | Target | Strategy |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| 7 | `BDS.Desktop.ShellLive.SettingsEditor` | 872 | ≤ 350 | Extract `ProjectSettings` (~140), `AISettings` (~150), `PublishingSettings` (~80), `ManagedCategories` (~140), `StyleEditor` (~80), `MCPConfig` (~60). |
|
|
||||||
| 8 | `BDS.Desktop.ShellLive.ChatEditor` | 972 | ≤ 400 | Extract `ToolSurfaces` (~280), `ToolTracking` (~140), `MessageBuild` (~160), `ModelSelection` (~100). Defer — highest internal coupling. |
|
| 8 | `BDS.Desktop.ShellLive.ChatEditor` | 972 | ≤ 400 | Extract `ToolSurfaces` (~280), `ToolTracking` (~140), `MessageBuild` (~160), `ModelSelection` (~100). Defer — highest internal coupling. |
|
||||||
| 9 | `BDS.MCP` | 677 | ≤ 350 | Split tools / resources / proposals / serialization clusters. (Carried over from original priority list.) |
|
| 9 | `BDS.MCP` | 677 | ≤ 350 | Split tools / resources / proposals / serialization clusters. (Carried over from original priority list.) |
|
||||||
|
|
||||||
@@ -33,6 +32,7 @@ Last refreshed: 2026-05-06.
|
|||||||
- `BDS.Rendering` 838 → 33 (96 %)
|
- `BDS.Rendering` 838 → 33 (96 %)
|
||||||
- `BDS.Desktop.ShellLive.MenuEditor` 871 → 335 (62 %)
|
- `BDS.Desktop.ShellLive.MenuEditor` 871 → 335 (62 %)
|
||||||
- `BDS.Desktop.ShellLive.PostEditor` 963 → 506 (47 %)
|
- `BDS.Desktop.ShellLive.PostEditor` 963 → 506 (47 %)
|
||||||
|
- `BDS.Desktop.ShellLive.SettingsEditor` 872 → 226 (74 %)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -166,6 +166,11 @@ Most tests share the SQLite repo and named GenServers (`BDS.Tasks`, `BDS.Search`
|
|||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
|
### 2026-05-07
|
||||||
|
|
||||||
|
- **God modules**:
|
||||||
|
- `BDS.Desktop.ShellLive.SettingsEditor` 872 → 226 (74 %). Submodules under `lib/bds/desktop/shell_live/settings_editor/`: `StyleEditor` (103, build_style + select_style_theme + change_style_preview_mode + apply_style_theme + theme_display_name + current_theme + style_theme + @themes), `MCPConfig` (100, mcp_rows + toggle_mcp_agent + find_mcp_agent + mcp_configured? + mcp_config_path + mcp_server_present? + @mcp_agents), `PublishingSettings` (89, publishing_form + update_publishing_draft + save_publishing + clear_publishing + publishing_attrs + normalize_publishing_params), `EditorSettings` (93, editor_form + update_editor_draft + save_editor + get_global_setting + put_global_setting + editor_attrs + normalize_editor_params + boolean_string), `ProjectSettings` (112, project_metadata + project_form + technology_form + update_project_draft + save_project + project_attrs + normalize_project_params), `ManagedCategories` (194, protected_categories + protected_category? + category_rows + update_new_category + add_category + reset_categories + save_category + remove_category + ensure_default_categories + reset_default_category_settings + @protected_categories + @default_category_settings), `AISettings` (203, ai_form + endpoint_model_options + update_ai_draft + refresh_ai_models + save_ai + reset_ai_prompt + ai_attrs + normalize_ai_params + get_model_preference + maybe_put_model_preference + put_endpoint_preferences + endpoint_refresh_attrs + normalize_endpoint_result). Coordinator keeps `embed_templates "settings_editor_html/*"` (HEEx components `settings_editor` + `style_editor`), `assign_socket/1`, `update_search/3`, the `build_settings/1` aggregator, the `current_settings_section`/`current_tab_meta`/`visible_settings_sections`/`section_matches?`/`template_options` helpers, the `translated/1,2` HEEx render-time helper, and `defdelegate` entries for the 21 public event handlers + the HEEx-callable `theme_display_name/1` (delegated to `StyleEditor`) and `protected_category?/1` (delegated to `ManagedCategories`). Cross-submodule deps: `AISettings` calls `EditorSettings.{get_global_setting/1, put_global_setting/2}` (the only intra-submodule dependency); all other submodules are leaves. Each submodule duplicates the small `translated/2`, `truthy?/1`, `blank_to_nil/1`, `parse_integer/2`, `boolean_string/1` helpers locally per the established convention. Submodules use `Phoenix.Component.assign/3` directly via `use Phoenix.Component`. Validates clean: `mix compile --warnings-as-errors`, `mix dialyzer --format short` (Total errors: 0), `mix test` (342 tests, 0 failures, 4 skipped).
|
||||||
|
|
||||||
### 2026-05-06
|
### 2026-05-06
|
||||||
|
|
||||||
- **God modules**:
|
- **God modules**:
|
||||||
|
|||||||
@@ -5,59 +5,45 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do
|
|||||||
|
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
||||||
alias BDS.AI
|
|
||||||
alias BDS.Metadata
|
|
||||||
alias BDS.Desktop.ShellData
|
alias BDS.Desktop.ShellData
|
||||||
alias BDS.MCP.AgentConfig
|
|
||||||
alias BDS.Persistence
|
|
||||||
alias BDS.Repo
|
alias BDS.Repo
|
||||||
alias BDS.Settings.Setting
|
|
||||||
alias BDS.Templates.Template
|
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/*"
|
embed_templates "settings_editor_html/*"
|
||||||
|
|
||||||
@settings_sections ~w(project editor content ai technology publishing data mcp)
|
@settings_sections ~w(project editor content ai technology publishing data mcp)
|
||||||
|
|
||||||
@themes [
|
|
||||||
"default",
|
|
||||||
"amber",
|
|
||||||
"blue",
|
|
||||||
"cyan",
|
|
||||||
"fuchsia",
|
|
||||||
"green",
|
|
||||||
"grey",
|
|
||||||
"indigo",
|
|
||||||
"jade",
|
|
||||||
"lime",
|
|
||||||
"orange",
|
|
||||||
"pink",
|
|
||||||
"pumpkin",
|
|
||||||
"purple",
|
|
||||||
"red",
|
|
||||||
"sand",
|
|
||||||
"slate",
|
|
||||||
"violet",
|
|
||||||
"yellow",
|
|
||||||
"zinc"
|
|
||||||
]
|
|
||||||
|
|
||||||
@supported_languages ["en", "de", "fr", "it", "es"]
|
@supported_languages ["en", "de", "fr", "it", "es"]
|
||||||
@protected_categories MapSet.new(["article", "aside", "page", "picture"])
|
|
||||||
@default_category_settings %{
|
defdelegate update_project_draft(socket, params, reload), to: ProjectSettings
|
||||||
"article" => %{title: "article", render_in_lists: true, show_title: true},
|
defdelegate save_project(socket, reload, append_output), to: ProjectSettings
|
||||||
"picture" => %{title: "picture", render_in_lists: true, show_title: true},
|
defdelegate update_editor_draft(socket, params, reload), to: EditorSettings
|
||||||
"aside" => %{title: "aside", render_in_lists: true, show_title: false},
|
defdelegate save_editor(socket, reload, append_output), to: EditorSettings
|
||||||
"page" => %{title: "page", render_in_lists: false, show_title: true}
|
defdelegate update_publishing_draft(socket, params, reload), to: PublishingSettings
|
||||||
}
|
defdelegate save_publishing(socket, reload, append_output), to: PublishingSettings
|
||||||
@mcp_agents [
|
defdelegate clear_publishing(socket, reload, append_output), to: PublishingSettings
|
||||||
%{id: :claude_code, label: "Claude Code", supported?: true},
|
defdelegate update_ai_draft(socket, params, reload), to: AISettings
|
||||||
%{id: :claude_desktop, label: "Claude Desktop", supported?: false},
|
defdelegate refresh_ai_models(socket, endpoint_key, reload, append_output), to: AISettings
|
||||||
%{id: :github_copilot, label: "GitHub Copilot", supported?: true},
|
defdelegate save_ai(socket, reload, append_output), to: AISettings
|
||||||
%{id: :gemini_cli, label: "Gemini CLI", supported?: false},
|
defdelegate reset_ai_prompt(socket, reload, append_output), to: AISettings
|
||||||
%{id: :opencode, label: "OpenCode", supported?: false},
|
defdelegate update_new_category(socket, name, reload), to: ManagedCategories
|
||||||
%{id: :mistral_vibe, label: "Mistral Vibe", supported?: false},
|
defdelegate add_category(socket, reload, append_output), to: ManagedCategories
|
||||||
%{id: :openai_codex, label: "OpenAI Codex", supported?: false}
|
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
|
||||||
|
|
||||||
def assign_socket(socket) do
|
def assign_socket(socket) do
|
||||||
case socket.assigns[:current_tab] do
|
case socket.assigns[:current_tab] do
|
||||||
@@ -69,7 +55,7 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do
|
|||||||
%{type: :style} ->
|
%{type: :style} ->
|
||||||
socket
|
socket
|
||||||
|> assign(:settings_editor, nil)
|
|> assign(:settings_editor, nil)
|
||||||
|> assign(:style_editor, build_style(socket.assigns))
|
|> assign(:style_editor, StyleEditor.build_style(socket.assigns))
|
||||||
|
|
||||||
_other ->
|
_other ->
|
||||||
socket
|
socket
|
||||||
@@ -84,305 +70,29 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do
|
|||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_project_draft(socket, params, reload) do
|
|
||||||
socket
|
|
||||||
|> assign(:settings_editor_project_draft, normalize_project_params(params))
|
|
||||||
|> reload.(socket.assigns.workbench)
|
|
||||||
end
|
|
||||||
|
|
||||||
def update_editor_draft(socket, params, reload) do
|
|
||||||
socket
|
|
||||||
|> assign(:settings_editor_editor_draft, normalize_editor_params(params))
|
|
||||||
|> reload.(socket.assigns.workbench)
|
|
||||||
end
|
|
||||||
|
|
||||||
def save_editor(socket, reload, append_output) do
|
|
||||||
attrs = editor_attrs(socket.assigns)
|
|
||||||
|
|
||||||
with :ok <- put_global_setting("ui.preferred_editor_mode", attrs.default_mode),
|
|
||||||
:ok <- put_global_setting("ui.git_diff_view_style", attrs.diff_view_style),
|
|
||||||
:ok <- put_global_setting("ui.git_diff_word_wrap", boolean_string(attrs.wrap_long_lines)),
|
|
||||||
:ok <- put_global_setting("ui.git_diff_hide_unchanged_regions", boolean_string(attrs.hide_unchanged_regions)) do
|
|
||||||
socket
|
|
||||||
|> assign(:settings_editor_editor_draft, %{})
|
|
||||||
|> reload.(socket.assigns.workbench)
|
|
||||||
else
|
|
||||||
{:error, reason} ->
|
|
||||||
socket
|
|
||||||
|> append_output.(translated("Editor Settings"), inspect(reason), nil, "error")
|
|
||||||
|> reload.(socket.assigns.workbench)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def save_project(socket, reload, append_output) do
|
|
||||||
project_id = socket.assigns.projects.active_project_id
|
|
||||||
|
|
||||||
case Metadata.update_project_metadata(project_id, project_attrs(socket.assigns)) do
|
|
||||||
{:ok, _metadata} ->
|
|
||||||
socket
|
|
||||||
|> assign(:settings_editor_project_draft, %{})
|
|
||||||
|> reload.(socket.assigns.workbench)
|
|
||||||
|
|
||||||
{:error, reason} ->
|
|
||||||
socket
|
|
||||||
|> append_output.(translated("Settings"), inspect(reason), nil, "error")
|
|
||||||
|> reload.(socket.assigns.workbench)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def update_publishing_draft(socket, params, reload) do
|
|
||||||
socket
|
|
||||||
|> assign(:settings_editor_publishing_draft, normalize_publishing_params(params))
|
|
||||||
|> reload.(socket.assigns.workbench)
|
|
||||||
end
|
|
||||||
|
|
||||||
def update_ai_draft(socket, params, reload) do
|
|
||||||
socket
|
|
||||||
|> assign(:settings_editor_ai_draft, normalize_ai_params(params))
|
|
||||||
|> reload.(socket.assigns.workbench)
|
|
||||||
end
|
|
||||||
|
|
||||||
def refresh_ai_models(socket, endpoint_key, reload, append_output) do
|
|
||||||
attrs = ai_attrs(socket.assigns)
|
|
||||||
|
|
||||||
with {:ok, endpoint} <- endpoint_refresh_attrs(endpoint_key, attrs),
|
|
||||||
{:ok, models} <- AI.list_endpoint_models(endpoint) do
|
|
||||||
socket
|
|
||||||
|> assign(:settings_editor_endpoint_models, Map.put(socket.assigns[:settings_editor_endpoint_models] || %{}, endpoint_key, models))
|
|
||||||
|> reload.(socket.assigns.workbench)
|
|
||||||
else
|
|
||||||
{:error, reason} ->
|
|
||||||
socket
|
|
||||||
|> append_output.(translated("AI Settings"), inspect(reason), nil, "error")
|
|
||||||
|> reload.(socket.assigns.workbench)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def save_ai(socket, reload, append_output) do
|
|
||||||
attrs = ai_attrs(socket.assigns)
|
|
||||||
|
|
||||||
with :ok <- put_endpoint_preferences(:online, attrs.online_url, attrs.online_api_key, attrs.online_chat_model),
|
|
||||||
:ok <- put_endpoint_preferences(:airplane, attrs.offline_url, attrs.offline_api_key, attrs.offline_chat_model),
|
|
||||||
:ok <- AI.delete_endpoint(:mistral),
|
|
||||||
:ok <- AI.set_airplane_mode(attrs.offline_mode),
|
|
||||||
:ok <- maybe_put_model_preference(:chat, attrs.online_chat_model),
|
|
||||||
:ok <- maybe_put_model_preference(:title, attrs.online_title_model),
|
|
||||||
:ok <- maybe_put_model_preference(:image_analysis, attrs.online_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, %{})
|
|
||||||
|> assign(:offline_mode, attrs.offline_mode)
|
|
||||||
|> reload.(socket.assigns.workbench)
|
|
||||||
else
|
|
||||||
{:error, reason} ->
|
|
||||||
socket
|
|
||||||
|> append_output.(translated("AI Settings"), inspect(reason), nil, "error")
|
|
||||||
|> reload.(socket.assigns.workbench)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def reset_ai_prompt(socket, reload, append_output) do
|
|
||||||
case put_global_setting("ai.system_prompt", "") do
|
|
||||||
:ok ->
|
|
||||||
socket
|
|
||||||
|> assign(:settings_editor_ai_draft, %{})
|
|
||||||
|> reload.(socket.assigns.workbench)
|
|
||||||
|
|
||||||
{:error, reason} ->
|
|
||||||
socket
|
|
||||||
|> append_output.(translated("AI Settings"), inspect(reason), nil, "error")
|
|
||||||
|> reload.(socket.assigns.workbench)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def save_publishing(socket, reload, append_output) do
|
|
||||||
project_id = socket.assigns.projects.active_project_id
|
|
||||||
|
|
||||||
case Metadata.set_publishing_preferences(project_id, publishing_attrs(socket.assigns)) do
|
|
||||||
{:ok, _metadata} ->
|
|
||||||
socket
|
|
||||||
|> assign(:settings_editor_publishing_draft, %{})
|
|
||||||
|> reload.(socket.assigns.workbench)
|
|
||||||
|
|
||||||
{:error, reason} ->
|
|
||||||
socket
|
|
||||||
|> append_output.(translated("Publishing"), inspect(reason), nil, "error")
|
|
||||||
|> reload.(socket.assigns.workbench)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def clear_publishing(socket, reload, append_output) do
|
|
||||||
project_id = socket.assigns.projects.active_project_id
|
|
||||||
|
|
||||||
case Metadata.set_publishing_preferences(project_id, %{}) do
|
|
||||||
{:ok, _metadata} ->
|
|
||||||
socket
|
|
||||||
|> assign(:settings_editor_publishing_draft, %{})
|
|
||||||
|> reload.(socket.assigns.workbench)
|
|
||||||
|
|
||||||
{:error, reason} ->
|
|
||||||
socket
|
|
||||||
|> append_output.(translated("Publishing"), inspect(reason), nil, "error")
|
|
||||||
|> reload.(socket.assigns.workbench)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def update_new_category(socket, name, reload) do
|
|
||||||
socket
|
|
||||||
|> assign(:settings_editor_new_category, to_string(name || ""))
|
|
||||||
|> reload.(socket.assigns.workbench)
|
|
||||||
end
|
|
||||||
|
|
||||||
def add_category(socket, reload, append_output) do
|
|
||||||
project_id = socket.assigns.projects.active_project_id
|
|
||||||
name = socket.assigns[:settings_editor_new_category] |> to_string() |> String.trim()
|
|
||||||
|
|
||||||
cond do
|
|
||||||
name == "" ->
|
|
||||||
socket
|
|
||||||
|> append_output.(translated("Categories"), translated("Category name is required"), nil, "error")
|
|
||||||
|> reload.(socket.assigns.workbench)
|
|
||||||
|
|
||||||
true ->
|
|
||||||
case Metadata.add_category(project_id, name) do
|
|
||||||
{:ok, _metadata} ->
|
|
||||||
socket
|
|
||||||
|> assign(:settings_editor_new_category, "")
|
|
||||||
|> reload.(socket.assigns.workbench)
|
|
||||||
|
|
||||||
{:error, reason} ->
|
|
||||||
socket
|
|
||||||
|> append_output.(translated("Categories"), inspect(reason), nil, "error")
|
|
||||||
|> reload.(socket.assigns.workbench)
|
|
||||||
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
|
|
||||||
project_id = socket.assigns.projects.active_project_id
|
|
||||||
category = Map.get(params, "category", "")
|
|
||||||
|
|
||||||
settings = %{
|
|
||||||
title: blank_to_nil(Map.get(params, "title")),
|
|
||||||
render_in_lists: truthy?(Map.get(params, "render_in_lists")),
|
|
||||||
show_title: truthy?(Map.get(params, "show_title")),
|
|
||||||
post_template_slug: blank_to_nil(Map.get(params, "post_template_slug")),
|
|
||||||
list_template_slug: blank_to_nil(Map.get(params, "list_template_slug"))
|
|
||||||
}
|
|
||||||
|
|
||||||
case Metadata.update_category_settings(project_id, category, settings) do
|
|
||||||
{:ok, _metadata} -> reload.(socket, socket.assigns.workbench)
|
|
||||||
{:error, reason} ->
|
|
||||||
socket
|
|
||||||
|> append_output.(translated("Categories"), inspect(reason), nil, "error")
|
|
||||||
|> reload.(socket.assigns.workbench)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def remove_category(socket, category, reload, append_output) do
|
|
||||||
project_id = socket.assigns.projects.active_project_id
|
|
||||||
|
|
||||||
cond do
|
|
||||||
MapSet.member?(@protected_categories, category) ->
|
|
||||||
socket
|
|
||||||
|> append_output.(translated("Categories"), translated("Protected categories cannot be removed"), nil, "error")
|
|
||||||
|> reload.(socket.assigns.workbench)
|
|
||||||
|
|
||||||
true ->
|
|
||||||
case Metadata.remove_category(project_id, category) do
|
|
||||||
{:ok, _metadata} -> reload.(socket, socket.assigns.workbench)
|
|
||||||
{:error, reason} ->
|
|
||||||
socket
|
|
||||||
|> append_output.(translated("Categories"), inspect(reason), nil, "error")
|
|
||||||
|> reload.(socket.assigns.workbench)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def select_style_theme(socket, theme, reload) do
|
|
||||||
socket
|
|
||||||
|> assign(:style_editor_theme, to_string(theme || "default"))
|
|
||||||
|> reload.(socket.assigns.workbench)
|
|
||||||
end
|
|
||||||
|
|
||||||
def change_style_preview_mode(socket, mode, reload) do
|
|
||||||
socket
|
|
||||||
|> assign(:style_editor_preview_mode, to_string(mode || "auto"))
|
|
||||||
|> reload.(socket.assigns.workbench)
|
|
||||||
end
|
|
||||||
|
|
||||||
def apply_style_theme(socket, reload, append_output) do
|
|
||||||
project_id = socket.assigns.projects.active_project_id
|
|
||||||
theme = socket.assigns[:style_editor_theme] || current_theme(socket.assigns)
|
|
||||||
|
|
||||||
case Metadata.update_project_metadata(project_id, %{pico_theme: theme}) do
|
|
||||||
{:ok, _metadata} -> reload.(socket, socket.assigns.workbench)
|
|
||||||
{:error, reason} ->
|
|
||||||
socket
|
|
||||||
|> append_output.(translated("Style"), inspect(reason), nil, "error")
|
|
||||||
|> reload.(socket.assigns.workbench)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_settings(%{projects: %{active_project_id: nil}}), do: nil
|
def build_settings(%{projects: %{active_project_id: nil}}), do: nil
|
||||||
|
|
||||||
def build_settings(assigns) do
|
def build_settings(assigns) do
|
||||||
metadata = project_metadata(assigns)
|
metadata = ProjectSettings.project_metadata(assigns)
|
||||||
project_form = Map.merge(project_form(metadata), Map.get(assigns, :settings_editor_project_draft, %{}))
|
|
||||||
editor_form = Map.merge(editor_form(), Map.get(assigns, :settings_editor_editor_draft, %{}))
|
project_form =
|
||||||
ai_form = Map.merge(ai_form(assigns), Map.get(assigns, :settings_editor_ai_draft, %{}))
|
Map.merge(
|
||||||
publishing_form = Map.merge(publishing_form(metadata), Map.get(assigns, :settings_editor_publishing_draft, %{}))
|
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, "")
|
query = Map.get(assigns, :settings_editor_search, "")
|
||||||
selected_section = current_settings_section(assigns)
|
selected_section = current_settings_section(assigns)
|
||||||
visible_sections = visible_settings_sections(query)
|
visible_sections = visible_settings_sections(query)
|
||||||
@@ -393,259 +103,47 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do
|
|||||||
active_sections: visible_sections,
|
active_sections: visible_sections,
|
||||||
project: project_form,
|
project: project_form,
|
||||||
editor: editor_form,
|
editor: editor_form,
|
||||||
categories: category_rows(metadata),
|
categories: ManagedCategories.category_rows(metadata),
|
||||||
ai: ai_form,
|
ai: ai_form,
|
||||||
technology: technology_form(project_form),
|
technology: ProjectSettings.technology_form(project_form),
|
||||||
publishing: publishing_form,
|
publishing: publishing_form,
|
||||||
mcp: mcp_rows(),
|
mcp: MCPConfig.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_path: Map.get(assigns.current_project || %{}, :data_path) || "",
|
||||||
project_data_default_path: Map.get(assigns.current_project || %{}, :project_path) || "",
|
project_data_default_path: Map.get(assigns.current_project || %{}, :project_path) || "",
|
||||||
template_options: template_options(assigns.projects.active_project_id),
|
template_options: template_options(assigns.projects.active_project_id),
|
||||||
online_endpoint_models: endpoint_model_options(assigns, :online),
|
online_endpoint_models: AISettings.endpoint_model_options(assigns, :online),
|
||||||
offline_endpoint_models: endpoint_model_options(assigns, :airplane),
|
offline_endpoint_models: AISettings.endpoint_model_options(assigns, :airplane),
|
||||||
project_visible?: section_matches?(query, ~w(project name description url language author category posts bookmarklet)),
|
project_visible?:
|
||||||
editor_visible?: section_matches?(query, ~w(editor mode markdown preview diff wrap unchanged)),
|
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 airplane offline online endpoint url api key chat title image)),
|
ai_visible?:
|
||||||
technology_visible?: section_matches?(query, ~w(technology runtime semantic similarity embedding scripting)),
|
section_matches?(
|
||||||
publishing_visible?: section_matches?(query, ~w(publishing ssh scp rsync host user remote path)),
|
query,
|
||||||
mcp_visible?: section_matches?(query, ~w(mcp claude copilot gemini opencode mistral codex agent server)),
|
~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)),
|
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: ManagedCategories.protected_categories()
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_style(%{projects: %{active_project_id: nil}}), do: nil
|
def translated(text, bindings \\ %{}),
|
||||||
|
do: ShellData.translate(text, bindings, Process.get(:bds_ui_locale))
|
||||||
def build_style(assigns) do
|
|
||||||
selected_theme = Map.get(assigns, :style_editor_theme) || current_theme(assigns)
|
|
||||||
preview_mode = Map.get(assigns, :style_editor_preview_mode, "auto")
|
|
||||||
|
|
||||||
%{
|
|
||||||
themes: Enum.map(@themes, &style_theme/1),
|
|
||||||
selected_theme: selected_theme,
|
|
||||||
applied_theme: current_theme(assigns),
|
|
||||||
preview_mode: preview_mode,
|
|
||||||
preview_url: "http://127.0.0.1:4123/__style-preview?theme=#{selected_theme}&mode=#{preview_mode}"
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
def translated(text, bindings \\ %{}), do: ShellData.translate(text, bindings, Process.get(:bds_ui_locale))
|
|
||||||
|
|
||||||
def protected_category?(category), do: MapSet.member?(@protected_categories, category)
|
|
||||||
|
|
||||||
def theme_display_name(theme) do
|
|
||||||
theme
|
|
||||||
|> to_string()
|
|
||||||
|> String.replace("-", " ")
|
|
||||||
|> String.capitalize()
|
|
||||||
end
|
|
||||||
|
|
||||||
defp project_attrs(assigns) do
|
|
||||||
draft = Map.get(assigns, :settings_editor_project_draft, %{})
|
|
||||||
|
|
||||||
%{
|
|
||||||
name: blank_to_nil(Map.get(draft, "name")),
|
|
||||||
description: blank_to_nil(Map.get(draft, "description")),
|
|
||||||
public_url: blank_to_nil(Map.get(draft, "public_url")),
|
|
||||||
main_language: blank_to_nil(Map.get(draft, "main_language")),
|
|
||||||
default_author: blank_to_nil(Map.get(draft, "default_author")),
|
|
||||||
max_posts_per_page: parse_integer(Map.get(draft, "max_posts_per_page"), 50),
|
|
||||||
blogmark_category: blank_to_nil(Map.get(draft, "blogmark_category")),
|
|
||||||
blog_languages: Map.get(draft, "blog_languages", []),
|
|
||||||
semantic_similarity_enabled: truthy?(Map.get(draft, "semantic_similarity_enabled"))
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp editor_attrs(assigns) do
|
|
||||||
draft = Map.get(assigns, :settings_editor_editor_draft, %{})
|
|
||||||
|
|
||||||
%{
|
|
||||||
default_mode: Map.get(draft, "default_mode", "markdown"),
|
|
||||||
diff_view_style: Map.get(draft, "diff_view_style", "inline"),
|
|
||||||
wrap_long_lines: truthy?(Map.get(draft, "wrap_long_lines")),
|
|
||||||
hide_unchanged_regions: truthy?(Map.get(draft, "hide_unchanged_regions"))
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp publishing_attrs(assigns) do
|
|
||||||
draft = Map.get(assigns, :settings_editor_publishing_draft, %{})
|
|
||||||
|
|
||||||
%{
|
|
||||||
ssh_host: blank_to_nil(Map.get(draft, "ssh_host")),
|
|
||||||
ssh_user: blank_to_nil(Map.get(draft, "ssh_user")),
|
|
||||||
ssh_remote_path: blank_to_nil(Map.get(draft, "ssh_remote_path")),
|
|
||||||
ssh_mode: Map.get(draft, "ssh_mode", "scp")
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp ai_attrs(assigns) do
|
|
||||||
draft = Map.get(assigns, :settings_editor_ai_draft, %{})
|
|
||||||
|
|
||||||
%{
|
|
||||||
online_url: blank_to_nil(Map.get(draft, "online_url")),
|
|
||||||
online_api_key: blank_to_nil(Map.get(draft, "online_api_key")),
|
|
||||||
online_chat_model: blank_to_nil(Map.get(draft, "online_chat_model")),
|
|
||||||
online_title_model: blank_to_nil(Map.get(draft, "online_title_model")),
|
|
||||||
online_image_analysis_model: blank_to_nil(Map.get(draft, "online_image_analysis_model")),
|
|
||||||
offline_url: blank_to_nil(Map.get(draft, "offline_url")),
|
|
||||||
offline_api_key: blank_to_nil(Map.get(draft, "offline_api_key")),
|
|
||||||
offline_mode: truthy?(Map.get(draft, "offline_mode")),
|
|
||||||
offline_chat_model: blank_to_nil(Map.get(draft, "offline_chat_model")),
|
|
||||||
offline_title_model: blank_to_nil(Map.get(draft, "offline_title_model")),
|
|
||||||
offline_image_analysis_model: blank_to_nil(Map.get(draft, "offline_image_analysis_model")),
|
|
||||||
system_prompt: Map.get(draft, "system_prompt", "")
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp project_metadata(assigns) do
|
|
||||||
case Metadata.get_project_metadata(assigns.projects.active_project_id) do
|
|
||||||
{:ok, metadata} -> metadata
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp project_form(metadata) do
|
|
||||||
%{
|
|
||||||
"name" => Map.get(metadata, :name, ""),
|
|
||||||
"description" => Map.get(metadata, :description, ""),
|
|
||||||
"public_url" => Map.get(metadata, :public_url, ""),
|
|
||||||
"main_language" => Map.get(metadata, :main_language) || "en",
|
|
||||||
"default_author" => Map.get(metadata, :default_author, ""),
|
|
||||||
"max_posts_per_page" => Integer.to_string(Map.get(metadata, :max_posts_per_page, 50)),
|
|
||||||
"blogmark_category" => Map.get(metadata, :blogmark_category) || List.first(Map.get(metadata, :categories, [])) || "article",
|
|
||||||
"blog_languages" => Map.get(metadata, :blog_languages, []),
|
|
||||||
"semantic_similarity_enabled" => Map.get(metadata, :semantic_similarity_enabled, false)
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp editor_form do
|
|
||||||
%{
|
|
||||||
"default_mode" => get_global_setting("ui.preferred_editor_mode") || "markdown",
|
|
||||||
"diff_view_style" => get_global_setting("ui.git_diff_view_style") || "inline",
|
|
||||||
"wrap_long_lines" => get_global_setting("ui.git_diff_word_wrap") == "true",
|
|
||||||
"hide_unchanged_regions" => get_global_setting("ui.git_diff_hide_unchanged_regions") == "true"
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp publishing_form(metadata) do
|
|
||||||
prefs = Map.get(metadata, :publishing_preferences, %{})
|
|
||||||
|
|
||||||
%{
|
|
||||||
"ssh_host" => Map.get(prefs, "ssh_host", ""),
|
|
||||||
"ssh_user" => Map.get(prefs, "ssh_user", ""),
|
|
||||||
"ssh_remote_path" => Map.get(prefs, "ssh_remote_path", ""),
|
|
||||||
"ssh_mode" => Map.get(prefs, "ssh_mode", "scp")
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp ai_form(assigns) do
|
|
||||||
{:ok, online_endpoint} = AI.get_endpoint(:online)
|
|
||||||
{:ok, airplane_endpoint} = AI.get_endpoint(:airplane)
|
|
||||||
|
|
||||||
%{
|
|
||||||
"online_url" => Map.get(online_endpoint || %{}, :url, ""),
|
|
||||||
"online_api_key" => Map.get(online_endpoint || %{}, :api_key, ""),
|
|
||||||
"online_chat_model" => get_model_preference(:chat) || Map.get(online_endpoint || %{}, :model, ""),
|
|
||||||
"online_title_model" => get_model_preference(:title),
|
|
||||||
"online_image_analysis_model" => get_model_preference(:image_analysis),
|
|
||||||
"offline_url" => Map.get(airplane_endpoint || %{}, :url, ""),
|
|
||||||
"offline_api_key" => Map.get(airplane_endpoint || %{}, :api_key, ""),
|
|
||||||
"offline_mode" => Map.get(assigns, :offline_mode, AI.airplane_mode?(true)),
|
|
||||||
"offline_chat_model" => get_model_preference(:airplane_chat) || Map.get(airplane_endpoint || %{}, :model, ""),
|
|
||||||
"offline_title_model" => get_model_preference(:airplane_title),
|
|
||||||
"offline_image_analysis_model" => get_model_preference(:airplane_image_analysis),
|
|
||||||
"system_prompt" => get_global_setting("ai.system_prompt") || ""
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp technology_form(project_form) do
|
|
||||||
%{
|
|
||||||
"semantic_similarity_enabled" => Map.get(project_form, "semantic_similarity_enabled", false)
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp current_theme(assigns) do
|
|
||||||
assigns
|
|
||||||
|> project_metadata()
|
|
||||||
|> Map.get(:pico_theme)
|
|
||||||
|> case do
|
|
||||||
nil -> "default"
|
|
||||||
"" -> "default"
|
|
||||||
theme -> theme
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp category_rows(metadata) do
|
|
||||||
categories = Map.get(metadata, :categories, [])
|
|
||||||
settings = Map.get(metadata, :category_settings, %{})
|
|
||||||
|
|
||||||
Enum.map(categories, fn category ->
|
|
||||||
category_settings = Map.get(settings, category, %{})
|
|
||||||
|
|
||||||
%{
|
|
||||||
name: category,
|
|
||||||
title: Map.get(category_settings, "title") || category,
|
|
||||||
render_in_lists: Map.get(category_settings, "render_in_lists", true),
|
|
||||||
show_title: Map.get(category_settings, "show_title", true),
|
|
||||||
post_template_slug: Map.get(category_settings, "post_template_slug", ""),
|
|
||||||
list_template_slug: Map.get(category_settings, "list_template_slug", ""),
|
|
||||||
protected?: protected_category?(category)
|
|
||||||
}
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp category_names(metadata), do: Map.get(metadata, :categories, [])
|
|
||||||
|
|
||||||
defp normalize_project_params(params) do
|
|
||||||
%{
|
|
||||||
"name" => Map.get(params, "name", ""),
|
|
||||||
"description" => Map.get(params, "description", ""),
|
|
||||||
"public_url" => Map.get(params, "public_url", ""),
|
|
||||||
"main_language" => Map.get(params, "main_language", "en"),
|
|
||||||
"default_author" => Map.get(params, "default_author", ""),
|
|
||||||
"max_posts_per_page" => Map.get(params, "max_posts_per_page", "50"),
|
|
||||||
"blogmark_category" => Map.get(params, "blogmark_category", "article"),
|
|
||||||
"blog_languages" => List.wrap(Map.get(params, "blog_languages", [])),
|
|
||||||
"semantic_similarity_enabled" => truthy?(Map.get(params, "semantic_similarity_enabled"))
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp normalize_editor_params(params) do
|
|
||||||
%{
|
|
||||||
"default_mode" => Map.get(params, "default_mode", "markdown"),
|
|
||||||
"diff_view_style" => Map.get(params, "diff_view_style", "inline"),
|
|
||||||
"wrap_long_lines" => truthy?(Map.get(params, "wrap_long_lines")),
|
|
||||||
"hide_unchanged_regions" => truthy?(Map.get(params, "hide_unchanged_regions"))
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp normalize_publishing_params(params) do
|
|
||||||
%{
|
|
||||||
"ssh_host" => Map.get(params, "ssh_host", ""),
|
|
||||||
"ssh_user" => Map.get(params, "ssh_user", ""),
|
|
||||||
"ssh_remote_path" => Map.get(params, "ssh_remote_path", ""),
|
|
||||||
"ssh_mode" => Map.get(params, "ssh_mode", "scp")
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp normalize_ai_params(params) do
|
|
||||||
%{
|
|
||||||
"online_url" => Map.get(params, "online_url", ""),
|
|
||||||
"online_api_key" => Map.get(params, "online_api_key", ""),
|
|
||||||
"online_chat_model" => Map.get(params, "online_chat_model", ""),
|
|
||||||
"online_title_model" => Map.get(params, "online_title_model", ""),
|
|
||||||
"online_image_analysis_model" => Map.get(params, "online_image_analysis_model", ""),
|
|
||||||
"offline_url" => Map.get(params, "offline_url", ""),
|
|
||||||
"offline_api_key" => Map.get(params, "offline_api_key", ""),
|
|
||||||
"offline_mode" => truthy?(Map.get(params, "offline_mode")),
|
|
||||||
"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
|
defp current_settings_section(assigns) do
|
||||||
meta = current_tab_meta(assigns)
|
meta = current_tab_meta(assigns)
|
||||||
@@ -672,14 +170,32 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do
|
|||||||
defp visible_settings_sections(query) do
|
defp visible_settings_sections(query) do
|
||||||
Enum.filter(@settings_sections, fn section ->
|
Enum.filter(@settings_sections, fn section ->
|
||||||
case section do
|
case section do
|
||||||
"project" -> section_matches?(query, ~w(project name description data url language author bookmarklet))
|
"project" ->
|
||||||
"editor" -> section_matches?(query, ~w(editor mode markdown preview diff wrap unchanged))
|
section_matches?(query, ~w(project name description data url language author bookmarklet))
|
||||||
"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))
|
"editor" ->
|
||||||
"technology" -> section_matches?(query, ~w(technology semantic similarity runtime scripting embedding))
|
section_matches?(query, ~w(editor mode markdown preview diff wrap unchanged))
|
||||||
"publishing" -> section_matches?(query, ~w(publishing ssh scp rsync host user remote path))
|
|
||||||
"data" -> section_matches?(query, ~w(data rebuild maintenance links thumbnails filesystem))
|
"content" ->
|
||||||
"mcp" -> section_matches?(query, ~w(mcp claude copilot gemini opencode mistral codex agent server))
|
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)
|
end)
|
||||||
end
|
end
|
||||||
@@ -703,170 +219,8 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do
|
|||||||
}
|
}
|
||||||
end
|
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 put_endpoint_preferences(kind, url, api_key, primary_model) do
|
|
||||||
if Enum.all?([url, api_key, primary_model], &(blank_to_nil(&1) == nil)) do
|
|
||||||
AI.delete_endpoint(kind)
|
|
||||||
else
|
|
||||||
AI.put_endpoint(kind, %{url: url, api_key: api_key, model: primary_model}) |> normalize_endpoint_result()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp endpoint_model_options(assigns, endpoint_key) do
|
|
||||||
assigns
|
|
||||||
|> Map.get(:settings_editor_endpoint_models, %{})
|
|
||||||
|> Map.get(endpoint_key, [])
|
|
||||||
end
|
|
||||||
|
|
||||||
defp endpoint_refresh_attrs(:online, attrs) do
|
|
||||||
endpoint_refresh_attrs(attrs.online_url, attrs.online_api_key)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp endpoint_refresh_attrs(:airplane, attrs) do
|
|
||||||
endpoint_refresh_attrs(attrs.offline_url, attrs.offline_api_key)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp endpoint_refresh_attrs(url, api_key) do
|
|
||||||
case blank_to_nil(url) do
|
|
||||||
nil -> {:error, :endpoint_not_configured}
|
|
||||||
loaded_url -> {:ok, %{url: loaded_url, api_key: api_key}}
|
|
||||||
end
|
|
||||||
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 style_theme(name) do
|
defp section_matches?(query, keywords),
|
||||||
%{
|
do: Enum.any?(keywords, &String.contains?(&1, String.downcase(query)))
|
||||||
name: name,
|
|
||||||
accent_color: "#4f46e5",
|
|
||||||
light_bg_color: "#f8fafc",
|
|
||||||
dark_bg_color: "#0f172a"
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp truthy?(value), do: value in [true, "true", "on", "1", 1]
|
|
||||||
defp boolean_string(true), do: "true"
|
|
||||||
defp boolean_string(false), do: "false"
|
|
||||||
defp parse_integer(nil, fallback), do: fallback
|
|
||||||
defp parse_integer(value, _fallback) when is_integer(value), do: value
|
|
||||||
defp parse_integer(value, fallback) do
|
|
||||||
case Integer.parse(to_string(value)) do
|
|
||||||
{parsed, _rest} -> parsed
|
|
||||||
:error -> fallback
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp blank_to_nil(nil), do: nil
|
|
||||||
defp blank_to_nil(value) do
|
|
||||||
case String.trim(to_string(value)) do
|
|
||||||
"" -> nil
|
|
||||||
trimmed -> trimmed
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
203
lib/bds/desktop/shell_live/settings_editor/ai_settings.ex
Normal file
203
lib/bds/desktop/shell_live/settings_editor/ai_settings.ex
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
defmodule BDS.Desktop.ShellLive.SettingsEditor.AISettings do
|
||||||
|
@moduledoc false
|
||||||
|
|
||||||
|
use Phoenix.Component
|
||||||
|
|
||||||
|
alias BDS.AI
|
||||||
|
alias BDS.Desktop.ShellData
|
||||||
|
alias BDS.Desktop.ShellLive.SettingsEditor.EditorSettings
|
||||||
|
|
||||||
|
def ai_form(assigns) do
|
||||||
|
{:ok, online_endpoint} = AI.get_endpoint(:online)
|
||||||
|
{:ok, airplane_endpoint} = AI.get_endpoint(:airplane)
|
||||||
|
|
||||||
|
%{
|
||||||
|
"online_url" => Map.get(online_endpoint || %{}, :url, ""),
|
||||||
|
"online_api_key" => Map.get(online_endpoint || %{}, :api_key, ""),
|
||||||
|
"online_chat_model" =>
|
||||||
|
get_model_preference(:chat) || Map.get(online_endpoint || %{}, :model, ""),
|
||||||
|
"online_title_model" => get_model_preference(:title),
|
||||||
|
"online_image_analysis_model" => get_model_preference(:image_analysis),
|
||||||
|
"offline_url" => Map.get(airplane_endpoint || %{}, :url, ""),
|
||||||
|
"offline_api_key" => Map.get(airplane_endpoint || %{}, :api_key, ""),
|
||||||
|
"offline_mode" => Map.get(assigns, :offline_mode, AI.airplane_mode?(true)),
|
||||||
|
"offline_chat_model" =>
|
||||||
|
get_model_preference(:airplane_chat) ||
|
||||||
|
Map.get(airplane_endpoint || %{}, :model, ""),
|
||||||
|
"offline_title_model" => get_model_preference(:airplane_title),
|
||||||
|
"offline_image_analysis_model" => get_model_preference(:airplane_image_analysis),
|
||||||
|
"system_prompt" => EditorSettings.get_global_setting("ai.system_prompt") || ""
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def endpoint_model_options(assigns, endpoint_key) do
|
||||||
|
assigns
|
||||||
|
|> Map.get(:settings_editor_endpoint_models, %{})
|
||||||
|
|> Map.get(endpoint_key, [])
|
||||||
|
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 refresh_ai_models(socket, endpoint_key, reload, append_output) do
|
||||||
|
attrs = ai_attrs(socket.assigns)
|
||||||
|
|
||||||
|
with {:ok, endpoint} <- endpoint_refresh_attrs(endpoint_key, attrs),
|
||||||
|
{:ok, models} <- AI.list_endpoint_models(endpoint) do
|
||||||
|
socket
|
||||||
|
|> assign(
|
||||||
|
:settings_editor_endpoint_models,
|
||||||
|
Map.put(
|
||||||
|
socket.assigns[:settings_editor_endpoint_models] || %{},
|
||||||
|
endpoint_key,
|
||||||
|
models
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|> reload.(socket.assigns.workbench)
|
||||||
|
else
|
||||||
|
{:error, reason} ->
|
||||||
|
socket
|
||||||
|
|> append_output.(translated("AI Settings"), inspect(reason), nil, "error")
|
||||||
|
|> reload.(socket.assigns.workbench)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def save_ai(socket, reload, append_output) do
|
||||||
|
attrs = ai_attrs(socket.assigns)
|
||||||
|
|
||||||
|
with :ok <-
|
||||||
|
put_endpoint_preferences(:online, attrs.online_url, attrs.online_api_key, attrs.online_chat_model),
|
||||||
|
:ok <-
|
||||||
|
put_endpoint_preferences(
|
||||||
|
:airplane,
|
||||||
|
attrs.offline_url,
|
||||||
|
attrs.offline_api_key,
|
||||||
|
attrs.offline_chat_model
|
||||||
|
),
|
||||||
|
:ok <- AI.delete_endpoint(:mistral),
|
||||||
|
:ok <- AI.set_airplane_mode(attrs.offline_mode),
|
||||||
|
:ok <- maybe_put_model_preference(:chat, attrs.online_chat_model),
|
||||||
|
:ok <- maybe_put_model_preference(:title, attrs.online_title_model),
|
||||||
|
:ok <- maybe_put_model_preference(:image_analysis, attrs.online_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 <- EditorSettings.put_global_setting("ai.system_prompt", attrs.system_prompt) do
|
||||||
|
socket
|
||||||
|
|> assign(:settings_editor_ai_draft, %{})
|
||||||
|
|> assign(:offline_mode, attrs.offline_mode)
|
||||||
|
|> 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 EditorSettings.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
|
||||||
|
|
||||||
|
defp ai_attrs(assigns) do
|
||||||
|
draft = Map.get(assigns, :settings_editor_ai_draft, %{})
|
||||||
|
|
||||||
|
%{
|
||||||
|
online_url: blank_to_nil(Map.get(draft, "online_url")),
|
||||||
|
online_api_key: blank_to_nil(Map.get(draft, "online_api_key")),
|
||||||
|
online_chat_model: blank_to_nil(Map.get(draft, "online_chat_model")),
|
||||||
|
online_title_model: blank_to_nil(Map.get(draft, "online_title_model")),
|
||||||
|
online_image_analysis_model: blank_to_nil(Map.get(draft, "online_image_analysis_model")),
|
||||||
|
offline_url: blank_to_nil(Map.get(draft, "offline_url")),
|
||||||
|
offline_api_key: blank_to_nil(Map.get(draft, "offline_api_key")),
|
||||||
|
offline_mode: truthy?(Map.get(draft, "offline_mode")),
|
||||||
|
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 normalize_ai_params(params) do
|
||||||
|
%{
|
||||||
|
"online_url" => Map.get(params, "online_url", ""),
|
||||||
|
"online_api_key" => Map.get(params, "online_api_key", ""),
|
||||||
|
"online_chat_model" => Map.get(params, "online_chat_model", ""),
|
||||||
|
"online_title_model" => Map.get(params, "online_title_model", ""),
|
||||||
|
"online_image_analysis_model" => Map.get(params, "online_image_analysis_model", ""),
|
||||||
|
"offline_url" => Map.get(params, "offline_url", ""),
|
||||||
|
"offline_api_key" => Map.get(params, "offline_api_key", ""),
|
||||||
|
"offline_mode" => truthy?(Map.get(params, "offline_mode")),
|
||||||
|
"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 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 put_endpoint_preferences(kind, url, api_key, primary_model) do
|
||||||
|
if Enum.all?([url, api_key, primary_model], &(blank_to_nil(&1) == nil)) do
|
||||||
|
AI.delete_endpoint(kind)
|
||||||
|
else
|
||||||
|
AI.put_endpoint(kind, %{url: url, api_key: api_key, model: primary_model})
|
||||||
|
|> normalize_endpoint_result()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp endpoint_refresh_attrs(:online, attrs) do
|
||||||
|
endpoint_refresh_attrs(attrs.online_url, attrs.online_api_key)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp endpoint_refresh_attrs(:airplane, attrs) do
|
||||||
|
endpoint_refresh_attrs(attrs.offline_url, attrs.offline_api_key)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp endpoint_refresh_attrs(url, api_key) do
|
||||||
|
case blank_to_nil(url) do
|
||||||
|
nil -> {:error, :endpoint_not_configured}
|
||||||
|
loaded_url -> {:ok, %{url: loaded_url, api_key: api_key}}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp normalize_endpoint_result({:ok, _endpoint}), do: :ok
|
||||||
|
defp normalize_endpoint_result({:error, reason}), do: {:error, reason}
|
||||||
|
|
||||||
|
defp truthy?(value), do: value in [true, "true", "on", "1", 1]
|
||||||
|
|
||||||
|
defp blank_to_nil(nil), do: nil
|
||||||
|
|
||||||
|
defp blank_to_nil(value) do
|
||||||
|
case String.trim(to_string(value)) do
|
||||||
|
"" -> nil
|
||||||
|
trimmed -> trimmed
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp translated(text, bindings \\ %{}),
|
||||||
|
do: ShellData.translate(text, bindings, Process.get(:bds_ui_locale))
|
||||||
|
end
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
defmodule BDS.Desktop.ShellLive.SettingsEditor.EditorSettings do
|
||||||
|
@moduledoc false
|
||||||
|
|
||||||
|
use Phoenix.Component
|
||||||
|
|
||||||
|
alias BDS.Persistence
|
||||||
|
alias BDS.Repo
|
||||||
|
alias BDS.Settings.Setting
|
||||||
|
alias BDS.Desktop.ShellData
|
||||||
|
|
||||||
|
def 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
|
||||||
|
|
||||||
|
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 get_global_setting(key) do
|
||||||
|
case Repo.get(Setting, key) do
|
||||||
|
%Setting{value: value} -> value
|
||||||
|
_other -> nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def 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 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 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 truthy?(value), do: value in [true, "true", "on", "1", 1]
|
||||||
|
defp boolean_string(true), do: "true"
|
||||||
|
defp boolean_string(false), do: "false"
|
||||||
|
|
||||||
|
defp translated(text, bindings \\ %{}),
|
||||||
|
do: ShellData.translate(text, bindings, Process.get(:bds_ui_locale))
|
||||||
|
end
|
||||||
194
lib/bds/desktop/shell_live/settings_editor/managed_categories.ex
Normal file
194
lib/bds/desktop/shell_live/settings_editor/managed_categories.ex
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
defmodule BDS.Desktop.ShellLive.SettingsEditor.ManagedCategories do
|
||||||
|
@moduledoc false
|
||||||
|
|
||||||
|
use Phoenix.Component
|
||||||
|
|
||||||
|
alias BDS.Metadata
|
||||||
|
alias BDS.Desktop.ShellData
|
||||||
|
|
||||||
|
@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}
|
||||||
|
}
|
||||||
|
|
||||||
|
def protected_categories, do: @protected_categories
|
||||||
|
|
||||||
|
def protected_category?(category), do: MapSet.member?(@protected_categories, category)
|
||||||
|
|
||||||
|
def category_rows(metadata) do
|
||||||
|
categories = Map.get(metadata, :categories, [])
|
||||||
|
settings = Map.get(metadata, :category_settings, %{})
|
||||||
|
|
||||||
|
Enum.map(categories, fn category ->
|
||||||
|
category_settings = Map.get(settings, category, %{})
|
||||||
|
|
||||||
|
%{
|
||||||
|
name: category,
|
||||||
|
title: Map.get(category_settings, "title") || category,
|
||||||
|
render_in_lists: Map.get(category_settings, "render_in_lists", true),
|
||||||
|
show_title: Map.get(category_settings, "show_title", true),
|
||||||
|
post_template_slug: Map.get(category_settings, "post_template_slug", ""),
|
||||||
|
list_template_slug: Map.get(category_settings, "list_template_slug", ""),
|
||||||
|
protected?: protected_category?(category)
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_new_category(socket, name, reload) do
|
||||||
|
socket
|
||||||
|
|> assign(:settings_editor_new_category, to_string(name || ""))
|
||||||
|
|> reload.(socket.assigns.workbench)
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_category(socket, reload, append_output) do
|
||||||
|
project_id = socket.assigns.projects.active_project_id
|
||||||
|
name = socket.assigns[:settings_editor_new_category] |> to_string() |> String.trim()
|
||||||
|
|
||||||
|
cond do
|
||||||
|
name == "" ->
|
||||||
|
socket
|
||||||
|
|> append_output.(
|
||||||
|
translated("Categories"),
|
||||||
|
translated("Category name is required"),
|
||||||
|
nil,
|
||||||
|
"error"
|
||||||
|
)
|
||||||
|
|> reload.(socket.assigns.workbench)
|
||||||
|
|
||||||
|
true ->
|
||||||
|
case Metadata.add_category(project_id, name) do
|
||||||
|
{:ok, _metadata} ->
|
||||||
|
socket
|
||||||
|
|> assign(:settings_editor_new_category, "")
|
||||||
|
|> reload.(socket.assigns.workbench)
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
socket
|
||||||
|
|> append_output.(translated("Categories"), inspect(reason), nil, "error")
|
||||||
|
|> reload.(socket.assigns.workbench)
|
||||||
|
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 save_category(socket, params, reload, append_output) do
|
||||||
|
project_id = socket.assigns.projects.active_project_id
|
||||||
|
category = Map.get(params, "category", "")
|
||||||
|
|
||||||
|
settings = %{
|
||||||
|
title: blank_to_nil(Map.get(params, "title")),
|
||||||
|
render_in_lists: truthy?(Map.get(params, "render_in_lists")),
|
||||||
|
show_title: truthy?(Map.get(params, "show_title")),
|
||||||
|
post_template_slug: blank_to_nil(Map.get(params, "post_template_slug")),
|
||||||
|
list_template_slug: blank_to_nil(Map.get(params, "list_template_slug"))
|
||||||
|
}
|
||||||
|
|
||||||
|
case Metadata.update_category_settings(project_id, category, settings) do
|
||||||
|
{:ok, _metadata} ->
|
||||||
|
reload.(socket, socket.assigns.workbench)
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
socket
|
||||||
|
|> append_output.(translated("Categories"), inspect(reason), nil, "error")
|
||||||
|
|> reload.(socket.assigns.workbench)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_category(socket, category, reload, append_output) do
|
||||||
|
project_id = socket.assigns.projects.active_project_id
|
||||||
|
|
||||||
|
cond do
|
||||||
|
MapSet.member?(@protected_categories, category) ->
|
||||||
|
socket
|
||||||
|
|> append_output.(
|
||||||
|
translated("Categories"),
|
||||||
|
translated("Protected categories cannot be removed"),
|
||||||
|
nil,
|
||||||
|
"error"
|
||||||
|
)
|
||||||
|
|> reload.(socket.assigns.workbench)
|
||||||
|
|
||||||
|
true ->
|
||||||
|
case Metadata.remove_category(project_id, category) do
|
||||||
|
{:ok, _metadata} ->
|
||||||
|
reload.(socket, socket.assigns.workbench)
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
socket
|
||||||
|
|> append_output.(translated("Categories"), inspect(reason), nil, "error")
|
||||||
|
|> reload.(socket.assigns.workbench)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp project_metadata(assigns) do
|
||||||
|
case Metadata.get_project_metadata(assigns.projects.active_project_id) do
|
||||||
|
{:ok, metadata} -> metadata
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp category_names(metadata), do: Map.get(metadata, :categories, [])
|
||||||
|
|
||||||
|
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 truthy?(value), do: value in [true, "true", "on", "1", 1]
|
||||||
|
|
||||||
|
defp blank_to_nil(nil), do: nil
|
||||||
|
|
||||||
|
defp blank_to_nil(value) do
|
||||||
|
case String.trim(to_string(value)) do
|
||||||
|
"" -> nil
|
||||||
|
trimmed -> trimmed
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp translated(text, bindings \\ %{}),
|
||||||
|
do: ShellData.translate(text, bindings, Process.get(:bds_ui_locale))
|
||||||
|
end
|
||||||
100
lib/bds/desktop/shell_live/settings_editor/mcp_config.ex
Normal file
100
lib/bds/desktop/shell_live/settings_editor/mcp_config.ex
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
defmodule BDS.Desktop.ShellLive.SettingsEditor.MCPConfig do
|
||||||
|
@moduledoc false
|
||||||
|
|
||||||
|
use Phoenix.Component
|
||||||
|
|
||||||
|
alias BDS.Desktop.ShellData
|
||||||
|
alias BDS.MCP.AgentConfig
|
||||||
|
|
||||||
|
@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 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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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 translated(text, bindings \\ %{}),
|
||||||
|
do: ShellData.translate(text, bindings, Process.get(:bds_ui_locale))
|
||||||
|
end
|
||||||
112
lib/bds/desktop/shell_live/settings_editor/project_settings.ex
Normal file
112
lib/bds/desktop/shell_live/settings_editor/project_settings.ex
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
defmodule BDS.Desktop.ShellLive.SettingsEditor.ProjectSettings do
|
||||||
|
@moduledoc false
|
||||||
|
|
||||||
|
use Phoenix.Component
|
||||||
|
|
||||||
|
alias BDS.Metadata
|
||||||
|
alias BDS.Desktop.ShellData
|
||||||
|
|
||||||
|
def project_metadata(assigns) do
|
||||||
|
case Metadata.get_project_metadata(assigns.projects.active_project_id) do
|
||||||
|
{:ok, metadata} -> metadata
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def project_form(metadata) do
|
||||||
|
%{
|
||||||
|
"name" => Map.get(metadata, :name, ""),
|
||||||
|
"description" => Map.get(metadata, :description, ""),
|
||||||
|
"public_url" => Map.get(metadata, :public_url, ""),
|
||||||
|
"main_language" => Map.get(metadata, :main_language) || "en",
|
||||||
|
"default_author" => Map.get(metadata, :default_author, ""),
|
||||||
|
"max_posts_per_page" => Integer.to_string(Map.get(metadata, :max_posts_per_page, 50)),
|
||||||
|
"blogmark_category" =>
|
||||||
|
Map.get(metadata, :blogmark_category) ||
|
||||||
|
List.first(Map.get(metadata, :categories, [])) || "article",
|
||||||
|
"blog_languages" => Map.get(metadata, :blog_languages, []),
|
||||||
|
"semantic_similarity_enabled" => Map.get(metadata, :semantic_similarity_enabled, false)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def technology_form(project_form) do
|
||||||
|
%{
|
||||||
|
"semantic_similarity_enabled" => Map.get(project_form, "semantic_similarity_enabled", false)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_project_draft(socket, params, reload) do
|
||||||
|
socket
|
||||||
|
|> assign(:settings_editor_project_draft, normalize_project_params(params))
|
||||||
|
|> reload.(socket.assigns.workbench)
|
||||||
|
end
|
||||||
|
|
||||||
|
def save_project(socket, reload, append_output) do
|
||||||
|
project_id = socket.assigns.projects.active_project_id
|
||||||
|
|
||||||
|
case Metadata.update_project_metadata(project_id, project_attrs(socket.assigns)) do
|
||||||
|
{:ok, _metadata} ->
|
||||||
|
socket
|
||||||
|
|> assign(:settings_editor_project_draft, %{})
|
||||||
|
|> reload.(socket.assigns.workbench)
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
socket
|
||||||
|
|> append_output.(translated("Settings"), inspect(reason), nil, "error")
|
||||||
|
|> reload.(socket.assigns.workbench)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp project_attrs(assigns) do
|
||||||
|
draft = Map.get(assigns, :settings_editor_project_draft, %{})
|
||||||
|
|
||||||
|
%{
|
||||||
|
name: blank_to_nil(Map.get(draft, "name")),
|
||||||
|
description: blank_to_nil(Map.get(draft, "description")),
|
||||||
|
public_url: blank_to_nil(Map.get(draft, "public_url")),
|
||||||
|
main_language: blank_to_nil(Map.get(draft, "main_language")),
|
||||||
|
default_author: blank_to_nil(Map.get(draft, "default_author")),
|
||||||
|
max_posts_per_page: parse_integer(Map.get(draft, "max_posts_per_page"), 50),
|
||||||
|
blogmark_category: blank_to_nil(Map.get(draft, "blogmark_category")),
|
||||||
|
blog_languages: Map.get(draft, "blog_languages", []),
|
||||||
|
semantic_similarity_enabled: truthy?(Map.get(draft, "semantic_similarity_enabled"))
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp normalize_project_params(params) do
|
||||||
|
%{
|
||||||
|
"name" => Map.get(params, "name", ""),
|
||||||
|
"description" => Map.get(params, "description", ""),
|
||||||
|
"public_url" => Map.get(params, "public_url", ""),
|
||||||
|
"main_language" => Map.get(params, "main_language", "en"),
|
||||||
|
"default_author" => Map.get(params, "default_author", ""),
|
||||||
|
"max_posts_per_page" => Map.get(params, "max_posts_per_page", "50"),
|
||||||
|
"blogmark_category" => Map.get(params, "blogmark_category", "article"),
|
||||||
|
"blog_languages" => List.wrap(Map.get(params, "blog_languages", [])),
|
||||||
|
"semantic_similarity_enabled" => truthy?(Map.get(params, "semantic_similarity_enabled"))
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp truthy?(value), do: value in [true, "true", "on", "1", 1]
|
||||||
|
|
||||||
|
defp parse_integer(nil, fallback), do: fallback
|
||||||
|
defp parse_integer(value, _fallback) when is_integer(value), do: value
|
||||||
|
|
||||||
|
defp parse_integer(value, fallback) do
|
||||||
|
case Integer.parse(to_string(value)) do
|
||||||
|
{parsed, _rest} -> parsed
|
||||||
|
:error -> fallback
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp blank_to_nil(nil), do: nil
|
||||||
|
|
||||||
|
defp blank_to_nil(value) do
|
||||||
|
case String.trim(to_string(value)) do
|
||||||
|
"" -> nil
|
||||||
|
trimmed -> trimmed
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp translated(text, bindings \\ %{}),
|
||||||
|
do: ShellData.translate(text, bindings, Process.get(:bds_ui_locale))
|
||||||
|
end
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
defmodule BDS.Desktop.ShellLive.SettingsEditor.PublishingSettings do
|
||||||
|
@moduledoc false
|
||||||
|
|
||||||
|
use Phoenix.Component
|
||||||
|
|
||||||
|
alias BDS.Metadata
|
||||||
|
alias BDS.Desktop.ShellData
|
||||||
|
|
||||||
|
def publishing_form(metadata) do
|
||||||
|
prefs = Map.get(metadata, :publishing_preferences, %{})
|
||||||
|
|
||||||
|
%{
|
||||||
|
"ssh_host" => Map.get(prefs, "ssh_host", ""),
|
||||||
|
"ssh_user" => Map.get(prefs, "ssh_user", ""),
|
||||||
|
"ssh_remote_path" => Map.get(prefs, "ssh_remote_path", ""),
|
||||||
|
"ssh_mode" => Map.get(prefs, "ssh_mode", "scp")
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_publishing_draft(socket, params, reload) do
|
||||||
|
socket
|
||||||
|
|> assign(:settings_editor_publishing_draft, normalize_publishing_params(params))
|
||||||
|
|> reload.(socket.assigns.workbench)
|
||||||
|
end
|
||||||
|
|
||||||
|
def save_publishing(socket, reload, append_output) do
|
||||||
|
project_id = socket.assigns.projects.active_project_id
|
||||||
|
|
||||||
|
case Metadata.set_publishing_preferences(project_id, publishing_attrs(socket.assigns)) do
|
||||||
|
{:ok, _metadata} ->
|
||||||
|
socket
|
||||||
|
|> assign(:settings_editor_publishing_draft, %{})
|
||||||
|
|> reload.(socket.assigns.workbench)
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
socket
|
||||||
|
|> append_output.(translated("Publishing"), inspect(reason), nil, "error")
|
||||||
|
|> reload.(socket.assigns.workbench)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def clear_publishing(socket, reload, append_output) do
|
||||||
|
project_id = socket.assigns.projects.active_project_id
|
||||||
|
|
||||||
|
case Metadata.set_publishing_preferences(project_id, %{}) do
|
||||||
|
{:ok, _metadata} ->
|
||||||
|
socket
|
||||||
|
|> assign(:settings_editor_publishing_draft, %{})
|
||||||
|
|> reload.(socket.assigns.workbench)
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
socket
|
||||||
|
|> append_output.(translated("Publishing"), inspect(reason), nil, "error")
|
||||||
|
|> reload.(socket.assigns.workbench)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp publishing_attrs(assigns) do
|
||||||
|
draft = Map.get(assigns, :settings_editor_publishing_draft, %{})
|
||||||
|
|
||||||
|
%{
|
||||||
|
ssh_host: blank_to_nil(Map.get(draft, "ssh_host")),
|
||||||
|
ssh_user: blank_to_nil(Map.get(draft, "ssh_user")),
|
||||||
|
ssh_remote_path: blank_to_nil(Map.get(draft, "ssh_remote_path")),
|
||||||
|
ssh_mode: Map.get(draft, "ssh_mode", "scp")
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp normalize_publishing_params(params) do
|
||||||
|
%{
|
||||||
|
"ssh_host" => Map.get(params, "ssh_host", ""),
|
||||||
|
"ssh_user" => Map.get(params, "ssh_user", ""),
|
||||||
|
"ssh_remote_path" => Map.get(params, "ssh_remote_path", ""),
|
||||||
|
"ssh_mode" => Map.get(params, "ssh_mode", "scp")
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp blank_to_nil(nil), do: nil
|
||||||
|
|
||||||
|
defp blank_to_nil(value) do
|
||||||
|
case String.trim(to_string(value)) do
|
||||||
|
"" -> nil
|
||||||
|
trimmed -> trimmed
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp translated(text, bindings \\ %{}),
|
||||||
|
do: ShellData.translate(text, bindings, Process.get(:bds_ui_locale))
|
||||||
|
end
|
||||||
103
lib/bds/desktop/shell_live/settings_editor/style_editor.ex
Normal file
103
lib/bds/desktop/shell_live/settings_editor/style_editor.ex
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
defmodule BDS.Desktop.ShellLive.SettingsEditor.StyleEditor do
|
||||||
|
@moduledoc false
|
||||||
|
|
||||||
|
use Phoenix.Component
|
||||||
|
|
||||||
|
alias BDS.Metadata
|
||||||
|
alias BDS.Desktop.ShellData
|
||||||
|
|
||||||
|
@themes [
|
||||||
|
"default",
|
||||||
|
"amber",
|
||||||
|
"blue",
|
||||||
|
"cyan",
|
||||||
|
"fuchsia",
|
||||||
|
"green",
|
||||||
|
"grey",
|
||||||
|
"indigo",
|
||||||
|
"jade",
|
||||||
|
"lime",
|
||||||
|
"orange",
|
||||||
|
"pink",
|
||||||
|
"pumpkin",
|
||||||
|
"purple",
|
||||||
|
"red",
|
||||||
|
"sand",
|
||||||
|
"slate",
|
||||||
|
"violet",
|
||||||
|
"yellow",
|
||||||
|
"zinc"
|
||||||
|
]
|
||||||
|
|
||||||
|
def build_style(%{projects: %{active_project_id: nil}}), do: nil
|
||||||
|
|
||||||
|
def build_style(assigns) do
|
||||||
|
selected_theme = Map.get(assigns, :style_editor_theme) || current_theme(assigns)
|
||||||
|
preview_mode = Map.get(assigns, :style_editor_preview_mode, "auto")
|
||||||
|
|
||||||
|
%{
|
||||||
|
themes: Enum.map(@themes, &style_theme/1),
|
||||||
|
selected_theme: selected_theme,
|
||||||
|
applied_theme: current_theme(assigns),
|
||||||
|
preview_mode: preview_mode,
|
||||||
|
preview_url: "http://127.0.0.1:4123/__style-preview?theme=#{selected_theme}&mode=#{preview_mode}"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def select_style_theme(socket, theme, reload) do
|
||||||
|
socket
|
||||||
|
|> assign(:style_editor_theme, to_string(theme || "default"))
|
||||||
|
|> reload.(socket.assigns.workbench)
|
||||||
|
end
|
||||||
|
|
||||||
|
def change_style_preview_mode(socket, mode, reload) do
|
||||||
|
socket
|
||||||
|
|> assign(:style_editor_preview_mode, to_string(mode || "auto"))
|
||||||
|
|> reload.(socket.assigns.workbench)
|
||||||
|
end
|
||||||
|
|
||||||
|
def apply_style_theme(socket, reload, append_output) do
|
||||||
|
project_id = socket.assigns.projects.active_project_id
|
||||||
|
theme = socket.assigns[:style_editor_theme] || current_theme(socket.assigns)
|
||||||
|
|
||||||
|
case Metadata.update_project_metadata(project_id, %{pico_theme: theme}) do
|
||||||
|
{:ok, _metadata} ->
|
||||||
|
reload.(socket, socket.assigns.workbench)
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
socket
|
||||||
|
|> append_output.(translated("Style"), inspect(reason), nil, "error")
|
||||||
|
|> reload.(socket.assigns.workbench)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def theme_display_name(theme) do
|
||||||
|
theme
|
||||||
|
|> to_string()
|
||||||
|
|> String.replace("-", " ")
|
||||||
|
|> String.capitalize()
|
||||||
|
end
|
||||||
|
|
||||||
|
def current_theme(assigns) do
|
||||||
|
case Metadata.get_project_metadata(assigns.projects.active_project_id) do
|
||||||
|
{:ok, metadata} ->
|
||||||
|
case Map.get(metadata, :pico_theme) do
|
||||||
|
nil -> "default"
|
||||||
|
"" -> "default"
|
||||||
|
theme -> theme
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp style_theme(name) do
|
||||||
|
%{
|
||||||
|
name: name,
|
||||||
|
accent_color: "#4f46e5",
|
||||||
|
light_bg_color: "#f8fafc",
|
||||||
|
dark_bg_color: "#0f172a"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp translated(text, bindings \\ %{}),
|
||||||
|
do: ShellData.translate(text, bindings, Process.get(:bds_ui_locale))
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user