2246 lines
74 KiB
Elixir
2246 lines
74 KiB
Elixir
defmodule BDS.Desktop.ShellLive do
|
|
@moduledoc false
|
|
|
|
use Phoenix.LiveView
|
|
|
|
import Phoenix.HTML
|
|
|
|
alias BDS.{AI, BoundedAtoms, ImportDefinitions, Media, Posts, Scripts}
|
|
alias BDS.CliSync.Watcher
|
|
alias BDS.Desktop.{FolderPicker, Overlay, ShellData, UILocale}
|
|
|
|
alias BDS.Desktop.ShellLive.{
|
|
ChatEditor,
|
|
CodeEntityEditor,
|
|
ImportEditor,
|
|
MediaEditor,
|
|
MenuEditor,
|
|
MiscEditor,
|
|
SettingsEditor,
|
|
TagsEditor
|
|
}
|
|
|
|
alias BDS.Desktop.ShellLive.OverlayComponents, as: ShellOverlayComponents
|
|
alias BDS.Desktop.ShellLive.PostEditor
|
|
alias BDS.Desktop.ShellLive.SidebarComponents, as: ShellSidebarComponents
|
|
alias BDS.Desktop.ShellLive.SidebarState, as: ShellSidebarState
|
|
|
|
alias BDS.Desktop.ShellLive.{
|
|
ChatSurface,
|
|
CliSync,
|
|
Layout,
|
|
SessionUtil,
|
|
ShellCommandRunner,
|
|
SidebarCreate,
|
|
TabHelpers,
|
|
TaskLocalization,
|
|
TitlebarMenu
|
|
}
|
|
|
|
import TaskLocalization,
|
|
only: [
|
|
localize_task_status: 2,
|
|
translate_for_socket: 2
|
|
]
|
|
|
|
import TabHelpers,
|
|
only: [
|
|
tab_title: 2,
|
|
tab_subtitle: 2,
|
|
tab_id_for_route: 2,
|
|
tab_intent: 2,
|
|
sidebar_route_atom: 1,
|
|
parse_integer: 1
|
|
]
|
|
|
|
alias BDS.Projects
|
|
alias BDS.Templates
|
|
alias BDS.UI.{Commands, MenuBar, Session, Workbench}
|
|
alias Desktop.OS
|
|
alias BDS.Desktop.Shutdown
|
|
|
|
@refresh_interval 1_500
|
|
@output_entry_limit 20
|
|
@local_menu_actions MapSet.new([
|
|
:toggle_sidebar,
|
|
:toggle_panel,
|
|
:toggle_assistant_sidebar,
|
|
:view_posts,
|
|
:view_media,
|
|
:edit_preferences,
|
|
:edit_menu,
|
|
:documentation,
|
|
:api_documentation,
|
|
:close_tab
|
|
])
|
|
@socket_menu_actions MapSet.new([
|
|
:new_post,
|
|
:import_media,
|
|
:save,
|
|
:publish_selected,
|
|
:quit,
|
|
:view_on_github,
|
|
:report_issue,
|
|
:about
|
|
])
|
|
@runtime_menu_actions MapSet.new([
|
|
:undo,
|
|
:redo,
|
|
:cut,
|
|
:copy,
|
|
:paste,
|
|
:delete,
|
|
:select_all,
|
|
:find,
|
|
:replace,
|
|
:reload,
|
|
:force_reload,
|
|
:reset_zoom,
|
|
:zoom_in,
|
|
:zoom_out,
|
|
:toggle_full_screen
|
|
])
|
|
|
|
def supported_menu_actions do
|
|
@local_menu_actions
|
|
|> MapSet.union(@socket_menu_actions)
|
|
|> MapSet.union(@runtime_menu_actions)
|
|
|> MapSet.union(MapSet.new([:open_in_browser, :open_data_folder]))
|
|
|> MapSet.union(MapSet.new([:preview_post, :rebuild_database, :reindex_text]))
|
|
|> MapSet.union(MapSet.new([:rebuild_embedding_index, :metadata_diff, :regenerate_calendar]))
|
|
|> MapSet.union(MapSet.new([:validate_translations, :fill_missing_translations, :find_duplicates]))
|
|
|> MapSet.union(MapSet.new([:generate_sitemap, :validate_site, :upload_site]))
|
|
end
|
|
|
|
embed_templates("shell_live/*")
|
|
|
|
@impl true
|
|
def mount(_params, _session, socket) do
|
|
connected = connected?(socket)
|
|
|
|
if connected do
|
|
Phoenix.PubSub.subscribe(BDS.PubSub, Watcher.topic())
|
|
:timer.send_interval(@refresh_interval, :refresh_task_status)
|
|
end
|
|
|
|
workbench = Workbench.new()
|
|
|
|
{:ok,
|
|
socket
|
|
|> assign(:page_title, ShellData.title())
|
|
|> assign(:page_language, ShellData.ui_language())
|
|
|> assign(:client_shortcuts, Commands.client_shortcuts())
|
|
|> assign(:offline_mode, if(connected, do: AI.airplane_mode?(true), else: true))
|
|
|> assign(:handled_task_results, SessionUtil.initial_handled_task_results())
|
|
|> assign(:assistant_prompt, "")
|
|
|> assign(:assistant_messages, [])
|
|
|> assign(:is_mac_ui, mac_ui?())
|
|
|> assign(:menu_groups, TitlebarMenu.groups())
|
|
|> assign(:titlebar_menu_group, nil)
|
|
|> assign(:titlebar_menu_item_index, nil)
|
|
|> assign(:tab_meta, %{})
|
|
|> assign(:project_menu_open, false)
|
|
|> assign(:sidebar_filters_by_view, %{})
|
|
|> assign(:sidebar_filter_panels, %{})
|
|
|> assign(:post_editor_drafts, %{})
|
|
|> assign(:post_editor_active_languages, %{})
|
|
|> assign(:post_editor_tag_queries, %{})
|
|
|> assign(:post_editor_category_queries, %{})
|
|
|> assign(:post_editor_quick_actions_open, %{})
|
|
|> assign(:post_editor_modes, %{})
|
|
|> assign(:post_editor_expanded, %{})
|
|
|> assign(:post_editor_save_states, %{})
|
|
|> assign(:media_editor_drafts, %{})
|
|
|> assign(:media_editor_quick_actions_open, %{})
|
|
|> assign(:media_editor_post_pickers_open, %{})
|
|
|> assign(:media_editor_post_picker_queries, %{})
|
|
|> assign(:media_editor_save_states, %{})
|
|
|> assign(:media_editor_translation_forms, %{})
|
|
|> assign(:settings_editor_search, "")
|
|
|> assign(:settings_editor_project_draft, %{})
|
|
|> assign(:settings_editor_endpoint_models, %{})
|
|
|> assign(:settings_editor_publishing_draft, %{})
|
|
|> assign(:settings_editor_new_category, "")
|
|
|> assign(:style_editor_theme, nil)
|
|
|> assign(:style_editor_preview_mode, "auto")
|
|
|> assign(:tags_editor_selected, [])
|
|
|> assign(:tags_editor_new_tag, %{"name" => "", "color" => ""})
|
|
|> assign(:tags_editor_edit_draft, %{})
|
|
|> assign(:tags_editor_merge_target, "")
|
|
|> assign(:script_editor_drafts, %{})
|
|
|> assign(:template_editor_drafts, %{})
|
|
|> assign(:chat_editor_inputs, %{})
|
|
|> assign(:chat_model_selectors_open, %{})
|
|
|> assign(:chat_editor_requests, %{})
|
|
|> assign(:chat_editor_request_refs, %{})
|
|
|> assign(:chat_editor_surface_data, %{})
|
|
|> assign(:chat_editor_surface_tabs, %{})
|
|
|> assign(:chat_editor_dismissed_surfaces, MapSet.new())
|
|
|> assign(:chat_editor_action_errors, %{})
|
|
|> assign(:import_editor_analysis_states, %{})
|
|
|> assign(:import_editor_analysis_task_refs, %{})
|
|
|> assign(:import_editor_execution_states, %{})
|
|
|> assign(:import_editor_execution_task_refs, %{})
|
|
|> assign(:import_editor_sections, %{})
|
|
|> assign(:import_editor_taxonomy_edits, %{})
|
|
|> assign(:import_editor_model_selectors_open, %{})
|
|
|> assign(:import_editor_selected_models, %{})
|
|
|> assign(:misc_editor_selected_pairs, %{})
|
|
|> assign(:misc_editor_git_selected_files, %{})
|
|
|> assign(:metadata_diff_active_tabs, %{})
|
|
|> assign(:metadata_diff_field_filters, %{})
|
|
|> assign(:shell_overlay, nil)
|
|
|> assign(:output_entries, [])
|
|
|> reload_shell(workbench)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("toggle_sidebar", _params, socket) do
|
|
{:noreply, reload_shell(socket, Workbench.toggle_sidebar(socket.assigns.workbench))}
|
|
end
|
|
|
|
def handle_event("toggle_panel", _params, socket) do
|
|
{:noreply, reload_shell(socket, Workbench.toggle_panel(socket.assigns.workbench))}
|
|
end
|
|
|
|
def handle_event("toggle_assistant_sidebar", _params, socket) do
|
|
{:noreply, reload_shell(socket, Workbench.toggle_assistant_sidebar(socket.assigns.workbench))}
|
|
end
|
|
|
|
def handle_event("select_view", %{"view" => view_id}, socket) do
|
|
workbench =
|
|
Workbench.click_activity(
|
|
socket.assigns.workbench,
|
|
BoundedAtoms.sidebar_view(view_id, :posts)
|
|
)
|
|
|
|
{:noreply, reload_shell(socket, workbench)}
|
|
end
|
|
|
|
def handle_event("select_panel_tab", %{"tab" => tab}, socket) do
|
|
workbench =
|
|
socket.assigns.workbench
|
|
|> Workbench.set_panel_visible(true)
|
|
|> Workbench.set_panel_tab(BoundedAtoms.panel_tab(tab, :tasks))
|
|
|
|
{:noreply, reload_shell(socket, workbench)}
|
|
end
|
|
|
|
def handle_event("open_sidebar_item", %{"route" => _route, "id" => _id} = params, socket) do
|
|
{:noreply, open_sidebar_item(socket, params, :preview)}
|
|
end
|
|
|
|
def handle_event("pin_sidebar_item", %{"route" => _route, "id" => _id} = params, socket) do
|
|
{:noreply, open_sidebar_item(socket, params, :pin)}
|
|
end
|
|
|
|
def handle_event("sync_layout", params, socket) do
|
|
{:noreply, reload_shell(socket, Layout.sync(socket.assigns.workbench, params))}
|
|
end
|
|
|
|
def handle_event("resize_panel", %{"target" => target, "width" => width}, socket) do
|
|
{:noreply, reload_shell(socket, Layout.resize(socket.assigns.workbench, target, width))}
|
|
end
|
|
|
|
def handle_event("toggle_sidebar_filters", _params, socket) do
|
|
socket =
|
|
ShellSidebarState.put_filter_panel_state(socket, fn state ->
|
|
if state.visible do
|
|
%{state | visible: false}
|
|
else
|
|
%{
|
|
visible: true,
|
|
archive_collapsed: true,
|
|
tags_collapsed: true,
|
|
categories_collapsed: true,
|
|
expanded_year: nil
|
|
}
|
|
end
|
|
end)
|
|
|
|
{:noreply,
|
|
socket
|
|
|> reload_shell(socket.assigns.workbench)}
|
|
end
|
|
|
|
def handle_event("toggle_sidebar_archive", _params, socket) do
|
|
{:noreply,
|
|
socket
|
|
|> ShellSidebarState.put_filter_panel_state(fn state ->
|
|
%{state | archive_collapsed: not state.archive_collapsed}
|
|
end)
|
|
|> reload_shell(socket.assigns.workbench)}
|
|
end
|
|
|
|
def handle_event("toggle_sidebar_tags", _params, socket) do
|
|
{:noreply,
|
|
socket
|
|
|> ShellSidebarState.put_filter_panel_state(fn state ->
|
|
%{state | tags_collapsed: not state.tags_collapsed}
|
|
end)
|
|
|> reload_shell(socket.assigns.workbench)}
|
|
end
|
|
|
|
def handle_event("toggle_sidebar_categories", _params, socket) do
|
|
{:noreply,
|
|
socket
|
|
|> ShellSidebarState.put_filter_panel_state(fn state ->
|
|
%{state | categories_collapsed: not state.categories_collapsed}
|
|
end)
|
|
|> reload_shell(socket.assigns.workbench)}
|
|
end
|
|
|
|
def handle_event("update_sidebar_search", %{"sidebar_filters" => params}, socket) do
|
|
{:noreply,
|
|
socket
|
|
|> ShellSidebarState.put_filters(fn filters ->
|
|
Map.put(
|
|
filters,
|
|
:search,
|
|
ShellSidebarState.normalize_filter_string(Map.get(params, "search"))
|
|
)
|
|
end)
|
|
|> reload_shell(socket.assigns.workbench)}
|
|
end
|
|
|
|
def handle_event("clear_sidebar_search", _params, socket) do
|
|
{:noreply,
|
|
socket
|
|
|> ShellSidebarState.put_filters(fn filters -> Map.put(filters, :search, nil) end)
|
|
|> reload_shell(socket.assigns.workbench)}
|
|
end
|
|
|
|
def handle_event("clear_sidebar_tags", _params, socket) do
|
|
{:noreply,
|
|
socket
|
|
|> ShellSidebarState.put_filters(fn filters -> Map.put(filters, :tags, []) end)
|
|
|> reload_shell(socket.assigns.workbench)}
|
|
end
|
|
|
|
def handle_event("clear_sidebar_categories", _params, socket) do
|
|
{:noreply,
|
|
socket
|
|
|> ShellSidebarState.put_filters(fn filters -> Map.put(filters, :categories, []) end)
|
|
|> reload_shell(socket.assigns.workbench)}
|
|
end
|
|
|
|
def handle_event("toggle_sidebar_tag", %{"tag" => tag}, socket) do
|
|
{:noreply,
|
|
socket
|
|
|> ShellSidebarState.put_filters(fn filters ->
|
|
ShellSidebarState.toggle_filter_value(filters, :tags, tag)
|
|
end)
|
|
|> reload_shell(socket.assigns.workbench)}
|
|
end
|
|
|
|
def handle_event("toggle_sidebar_category", %{"category" => category}, socket) do
|
|
{:noreply,
|
|
socket
|
|
|> ShellSidebarState.put_filters(fn filters ->
|
|
ShellSidebarState.toggle_filter_value(filters, :categories, category)
|
|
end)
|
|
|> reload_shell(socket.assigns.workbench)}
|
|
end
|
|
|
|
def handle_event("select_sidebar_year", %{"year" => year}, socket) do
|
|
parsed_year = ShellSidebarState.parse_optional_integer(year)
|
|
|
|
{:noreply,
|
|
socket
|
|
|> ShellSidebarState.put_filter_panel_state(fn state ->
|
|
%{
|
|
state
|
|
| archive_collapsed: false,
|
|
expanded_year: if(state.expanded_year == parsed_year, do: nil, else: parsed_year)
|
|
}
|
|
end)
|
|
|> ShellSidebarState.put_filters(fn filters ->
|
|
filters
|
|
|> Map.put(:year, parsed_year)
|
|
|> Map.put(:month, nil)
|
|
end)
|
|
|> reload_shell(socket.assigns.workbench)}
|
|
end
|
|
|
|
def handle_event("select_sidebar_month", %{"year" => year, "month" => month}, socket) do
|
|
{:noreply,
|
|
socket
|
|
|> ShellSidebarState.put_filter_panel_state(fn state ->
|
|
%{
|
|
state
|
|
| archive_collapsed: false,
|
|
expanded_year: ShellSidebarState.parse_optional_integer(year)
|
|
}
|
|
end)
|
|
|> ShellSidebarState.put_filters(fn filters ->
|
|
filters
|
|
|> Map.put(:year, ShellSidebarState.parse_optional_integer(year))
|
|
|> Map.put(:month, ShellSidebarState.parse_optional_integer(month))
|
|
end)
|
|
|> reload_shell(socket.assigns.workbench)}
|
|
end
|
|
|
|
def handle_event("clear_sidebar_month", _params, socket) do
|
|
{:noreply,
|
|
socket
|
|
|> ShellSidebarState.put_filter_panel_state(fn state ->
|
|
%{state | archive_collapsed: false}
|
|
end)
|
|
|> ShellSidebarState.put_filters(fn filters ->
|
|
filters |> Map.put(:year, nil) |> Map.put(:month, nil)
|
|
end)
|
|
|> reload_shell(socket.assigns.workbench)}
|
|
end
|
|
|
|
def handle_event("clear_sidebar_filters", _params, socket) do
|
|
{:noreply,
|
|
socket
|
|
|> ShellSidebarState.put_filters(fn filters ->
|
|
filters
|
|
|> Map.put(:search, nil)
|
|
|> Map.put(:year, nil)
|
|
|> Map.put(:month, nil)
|
|
|> Map.put(:tags, [])
|
|
|> Map.put(:categories, [])
|
|
|> Map.put(
|
|
:display_limit,
|
|
ShellSidebarState.sidebar_page_size(socket.assigns.sidebar_data)
|
|
)
|
|
end)
|
|
|> reload_shell(socket.assigns.workbench)}
|
|
end
|
|
|
|
def handle_event("load_more_sidebar", _params, socket) do
|
|
{:noreply,
|
|
socket
|
|
|> ShellSidebarState.put_filters(fn filters ->
|
|
Map.update(
|
|
filters,
|
|
:display_limit,
|
|
ShellSidebarState.sidebar_page_size(socket.assigns.sidebar_data),
|
|
&(&1 + ShellSidebarState.sidebar_page_size(socket.assigns.sidebar_data))
|
|
)
|
|
end)
|
|
|> reload_shell(socket.assigns.workbench)}
|
|
end
|
|
|
|
def handle_event("create_sidebar_item", %{"kind" => kind}, socket) do
|
|
{:noreply, create_sidebar_item(socket, kind)}
|
|
end
|
|
|
|
def handle_event("shortcut", params, socket) do
|
|
if Layout.ignore_shortcut?(params) do
|
|
{:noreply, socket}
|
|
else
|
|
case Commands.command_for_shortcut(params) do
|
|
nil -> {:noreply, socket}
|
|
action -> {:noreply, handle_menu_action(socket, action)}
|
|
end
|
|
end
|
|
end
|
|
|
|
def handle_event("select_tab", %{"type" => type, "id" => id}, socket) do
|
|
workbench =
|
|
Workbench.open_tab(
|
|
socket.assigns.workbench,
|
|
BoundedAtoms.editor_route(type, :post),
|
|
id,
|
|
:preview
|
|
)
|
|
|
|
{:noreply, reload_shell(socket, workbench)}
|
|
end
|
|
|
|
def handle_event("close_tab", %{"type" => type, "id" => id}, socket) do
|
|
type_atom = BoundedAtoms.editor_route(type, :post)
|
|
workbench = Workbench.close_tab(socket.assigns.workbench, type_atom, id)
|
|
tab_meta = Map.delete(socket.assigns.tab_meta, {type_atom, id})
|
|
|
|
{:noreply,
|
|
socket
|
|
|> assign(:tab_meta, tab_meta)
|
|
|> reload_shell(workbench)}
|
|
end
|
|
|
|
def handle_event(
|
|
"confirm_sidebar_delete",
|
|
%{"route" => route, "id" => id} = params,
|
|
socket
|
|
) do
|
|
{:noreply, request_sidebar_delete(socket, route, id, Map.get(params, "title"))}
|
|
end
|
|
|
|
def handle_event("toggle_offline_mode", _params, socket) do
|
|
next_mode = not socket.assigns.offline_mode
|
|
|
|
:ok = AI.set_airplane_mode(next_mode)
|
|
socket = assign(socket, :offline_mode, next_mode)
|
|
|
|
{:noreply, reload_shell(socket, socket.assigns.workbench)}
|
|
end
|
|
|
|
def handle_event("update_assistant_prompt", %{"assistant" => %{"prompt" => prompt}}, socket) do
|
|
{:noreply, assign(socket, :assistant_prompt, prompt)}
|
|
end
|
|
|
|
def handle_event("submit_assistant_prompt", %{"assistant" => %{"prompt" => prompt}}, socket) do
|
|
prompt = prompt |> to_string() |> String.trim()
|
|
|
|
socket =
|
|
if prompt == "" do
|
|
assign(socket, :assistant_prompt, "")
|
|
else
|
|
socket
|
|
|> assign(:assistant_prompt, "")
|
|
|> assign(
|
|
:assistant_messages,
|
|
socket.assigns.assistant_messages ++ ChatSurface.assistant_turn(prompt, socket)
|
|
)
|
|
end
|
|
|
|
{:noreply, socket}
|
|
end
|
|
|
|
def handle_event("open_tasks_panel", _params, socket) do
|
|
workbench =
|
|
socket.assigns.workbench
|
|
|> Workbench.set_panel_visible(true)
|
|
|> Workbench.set_panel_tab(:tasks)
|
|
|
|
{:noreply, reload_shell(socket, workbench)}
|
|
end
|
|
|
|
def handle_event("change_post_editor", %{"post_editor" => params}, socket) do
|
|
{:noreply, PostEditor.update(socket, params, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("save_post_editor", %{"id" => post_id}, socket) do
|
|
{:noreply,
|
|
PostEditor.persist_socket(socket, post_id, :save, &reload_shell/2, &append_output_entry/5)}
|
|
end
|
|
|
|
def handle_event("publish_post_editor", %{"id" => post_id}, socket) do
|
|
{:noreply,
|
|
PostEditor.persist_socket(socket, post_id, :publish, &reload_shell/2, &append_output_entry/5)}
|
|
end
|
|
|
|
def handle_event("discard_post_editor", %{"id" => post_id}, socket) do
|
|
{:noreply,
|
|
PostEditor.discard_socket(socket, post_id, &reload_shell/2, &append_output_entry/5)}
|
|
end
|
|
|
|
def handle_event("delete_post_editor", %{"id" => post_id}, socket) do
|
|
{:noreply, PostEditor.delete_socket(socket, post_id, &reload_shell/2, &append_output_entry/5)}
|
|
end
|
|
|
|
def handle_event("set_post_editor_mode", %{"id" => post_id, "mode" => mode}, socket) do
|
|
{:noreply, PostEditor.set_mode(socket, post_id, mode, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("toggle_post_metadata", %{"id" => post_id}, socket) do
|
|
{:noreply, PostEditor.toggle_section(socket, post_id, :metadata, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("toggle_post_excerpt", %{"id" => post_id}, socket) do
|
|
{:noreply, PostEditor.toggle_section(socket, post_id, :excerpt, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event(
|
|
"select_post_editor_language",
|
|
%{"id" => post_id, "language" => language},
|
|
socket
|
|
) do
|
|
{:noreply, PostEditor.select_language(socket, post_id, language, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("toggle_post_editor_quick_actions", %{"id" => post_id}, socket) do
|
|
{:noreply, PostEditor.toggle_quick_actions(socket, post_id, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("detect_post_editor_language", %{"id" => post_id}, socket) do
|
|
{:noreply,
|
|
PostEditor.detect_language(socket, post_id, &reload_shell/2, &append_output_entry/5)}
|
|
end
|
|
|
|
def handle_event("add_post_editor_tag", %{"id" => post_id, "tag" => tag}, socket) do
|
|
{:noreply, PostEditor.add_list_value(socket, post_id, :tags, tag, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("remove_post_editor_tag", %{"id" => post_id, "tag" => tag}, socket) do
|
|
{:noreply, PostEditor.remove_list_value(socket, post_id, :tags, tag, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("add_post_editor_category", %{"id" => post_id, "category" => category}, socket) do
|
|
{:noreply, PostEditor.add_list_value(socket, post_id, :categories, category, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event(
|
|
"remove_post_editor_category",
|
|
%{"id" => post_id, "category" => category},
|
|
socket
|
|
) do
|
|
{:noreply,
|
|
PostEditor.remove_list_value(socket, post_id, :categories, category, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("change_media_editor", %{"media_editor" => params}, socket) do
|
|
{:noreply, MediaEditor.update(socket, params, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("save_media_editor", %{"id" => media_id}, socket) do
|
|
{:noreply,
|
|
MediaEditor.persist_socket(socket, media_id, &reload_shell/2, &append_output_entry/5)}
|
|
end
|
|
|
|
def handle_event("toggle_media_editor_quick_actions", %{"id" => media_id}, socket) do
|
|
{:noreply, MediaEditor.toggle_quick_actions(socket, media_id, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("replace_media_editor_file", %{"id" => media_id}, socket) do
|
|
{:noreply,
|
|
MediaEditor.replace_file(socket, media_id, &reload_shell/2, &append_output_entry/5)}
|
|
end
|
|
|
|
def handle_event("detect_media_editor_language", %{"id" => media_id}, socket) do
|
|
{:noreply,
|
|
MediaEditor.detect_language(socket, media_id, &reload_shell/2, &append_output_entry/5)}
|
|
end
|
|
|
|
def handle_event("toggle_media_post_picker", %{"id" => media_id}, socket) do
|
|
{:noreply, MediaEditor.toggle_post_picker(socket, media_id, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event(
|
|
"change_media_post_picker",
|
|
%{"id" => media_id, "media_post_picker" => %{"query" => query}},
|
|
socket
|
|
) do
|
|
{:noreply, MediaEditor.set_post_picker_query(socket, media_id, query, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("link_media_to_post", %{"id" => media_id, "post-id" => post_id}, socket) do
|
|
{:noreply,
|
|
MediaEditor.link_post(socket, media_id, post_id, &reload_shell/2, &append_output_entry/5)}
|
|
end
|
|
|
|
def handle_event("unlink_media_from_post", %{"id" => media_id, "post-id" => post_id}, socket) do
|
|
{:noreply,
|
|
MediaEditor.unlink_post(socket, media_id, post_id, &reload_shell/2, &append_output_entry/5)}
|
|
end
|
|
|
|
def handle_event("edit_media_translation", %{"id" => media_id, "language" => language}, socket) do
|
|
{:noreply, MediaEditor.edit_translation(socket, media_id, language, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("change_media_translation", %{"media_translation" => params}, socket) do
|
|
case socket.assigns.current_tab do
|
|
%{type: :media, id: media_id} ->
|
|
{:noreply, MediaEditor.update_translation(socket, media_id, params, &reload_shell/2)}
|
|
|
|
_other ->
|
|
{:noreply, socket}
|
|
end
|
|
end
|
|
|
|
def handle_event("save_media_translation", %{"id" => media_id}, socket) do
|
|
{:noreply,
|
|
MediaEditor.save_translation(socket, media_id, &reload_shell/2, &append_output_entry/5)}
|
|
end
|
|
|
|
def handle_event(
|
|
"refresh_media_translation",
|
|
%{"id" => media_id, "language" => language},
|
|
socket
|
|
) do
|
|
{:noreply,
|
|
MediaEditor.refresh_translation(
|
|
socket,
|
|
media_id,
|
|
language,
|
|
&reload_shell/2,
|
|
&append_output_entry/5
|
|
)}
|
|
end
|
|
|
|
def handle_event(
|
|
"delete_media_translation",
|
|
%{"id" => media_id, "language" => language},
|
|
socket
|
|
) do
|
|
{:noreply,
|
|
MediaEditor.delete_translation(
|
|
socket,
|
|
media_id,
|
|
language,
|
|
&reload_shell/2,
|
|
&append_output_entry/5
|
|
)}
|
|
end
|
|
|
|
def handle_event("close_media_translation_editor", _params, socket) do
|
|
case socket.assigns.current_tab do
|
|
%{type: :media, id: media_id} ->
|
|
{:noreply,
|
|
socket
|
|
|> assign(
|
|
:media_editor_translation_forms,
|
|
Map.delete(socket.assigns.media_editor_translation_forms, media_id)
|
|
)
|
|
|> reload_shell(socket.assigns.workbench)}
|
|
|
|
_other ->
|
|
{:noreply, socket}
|
|
end
|
|
end
|
|
|
|
def handle_event("change_settings_search", %{"query" => query}, socket) do
|
|
{:noreply, SettingsEditor.update_search(socket, query, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("change_settings_project", %{"settings_project" => params}, socket) do
|
|
{:noreply, SettingsEditor.update_project_draft(socket, params, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("change_settings_editor", %{"settings_editor" => params}, socket) do
|
|
{:noreply, SettingsEditor.update_editor_draft(socket, params, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("save_settings_editor", _params, socket) do
|
|
{:noreply, SettingsEditor.save_editor(socket, &reload_shell/2, &append_output_entry/5)}
|
|
end
|
|
|
|
def handle_event("save_settings_project", _params, socket) do
|
|
{:noreply, SettingsEditor.save_project(socket, &reload_shell/2, &append_output_entry/5)}
|
|
end
|
|
|
|
def handle_event("change_settings_publishing", %{"settings_publishing" => params}, socket) do
|
|
{:noreply, SettingsEditor.update_publishing_draft(socket, params, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("change_settings_ai", %{"settings_ai" => params}, socket) do
|
|
{:noreply, SettingsEditor.update_ai_draft(socket, params, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("refresh_settings_ai_models", %{"endpoint" => endpoint}, socket) do
|
|
case BoundedAtoms.ai_endpoint(endpoint) do
|
|
nil ->
|
|
{:noreply, reload_shell(socket, socket.assigns.workbench)}
|
|
|
|
endpoint_key ->
|
|
{:noreply,
|
|
SettingsEditor.refresh_ai_models(
|
|
socket,
|
|
endpoint_key,
|
|
&reload_shell/2,
|
|
&append_output_entry/5
|
|
)}
|
|
end
|
|
end
|
|
|
|
def handle_event("save_settings_ai", _params, socket) do
|
|
{:noreply, SettingsEditor.save_ai(socket, &reload_shell/2, &append_output_entry/5)}
|
|
end
|
|
|
|
def handle_event("reset_settings_ai_prompt", _params, socket) do
|
|
{:noreply, SettingsEditor.reset_ai_prompt(socket, &reload_shell/2, &append_output_entry/5)}
|
|
end
|
|
|
|
def handle_event("save_settings_publishing", _params, socket) do
|
|
{:noreply, SettingsEditor.save_publishing(socket, &reload_shell/2, &append_output_entry/5)}
|
|
end
|
|
|
|
def handle_event("clear_settings_publishing", _params, socket) do
|
|
{:noreply, SettingsEditor.clear_publishing(socket, &reload_shell/2, &append_output_entry/5)}
|
|
end
|
|
|
|
def handle_event("change_settings_new_category", %{"name" => name}, socket) do
|
|
{:noreply, SettingsEditor.update_new_category(socket, name, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("add_settings_category", _params, socket) do
|
|
{:noreply, SettingsEditor.add_category(socket, &reload_shell/2, &append_output_entry/5)}
|
|
end
|
|
|
|
def handle_event("reset_settings_categories", _params, socket) do
|
|
{:noreply, SettingsEditor.reset_categories(socket, &reload_shell/2, &append_output_entry/5)}
|
|
end
|
|
|
|
def handle_event("save_settings_category", %{"category_settings" => params}, socket) do
|
|
{:noreply,
|
|
SettingsEditor.save_category(socket, params, &reload_shell/2, &append_output_entry/5)}
|
|
end
|
|
|
|
def handle_event("remove_settings_category", %{"category" => category}, socket) do
|
|
{:noreply,
|
|
SettingsEditor.remove_category(socket, category, &reload_shell/2, &append_output_entry/5)}
|
|
end
|
|
|
|
def handle_event("settings_shell_command", %{"action" => action}, socket) do
|
|
{:noreply, apply_shell_command(socket, action)}
|
|
end
|
|
|
|
def handle_event("toggle_settings_mcp_agent", %{"agent" => agent}, socket) do
|
|
{:noreply,
|
|
SettingsEditor.toggle_mcp_agent(socket, agent, &reload_shell/2, &append_output_entry/5)}
|
|
end
|
|
|
|
def handle_event("select_style_theme", %{"theme" => theme}, socket) do
|
|
{:noreply, SettingsEditor.select_style_theme(socket, theme, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("change_style_preview_mode", %{"mode" => mode}, socket) do
|
|
{:noreply, SettingsEditor.change_style_preview_mode(socket, mode, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("apply_style_theme", _params, socket) do
|
|
{:noreply, SettingsEditor.apply_style_theme(socket, &reload_shell/2, &append_output_entry/5)}
|
|
end
|
|
|
|
def handle_event("menu_editor_select_item", %{"item_id" => item_id}, socket) do
|
|
{:noreply, MenuEditor.select_item(socket, item_id, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("change_menu_editor_entry", %{"menu_editor_entry" => params}, socket) do
|
|
{:noreply, MenuEditor.change_entry(socket, params, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("submit_menu_editor_entry", _params, socket) do
|
|
{:noreply, MenuEditor.submit_entry(socket, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("cancel_menu_editor_entry", _params, socket) do
|
|
{:noreply, MenuEditor.cancel_entry(socket, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("select_menu_editor_page", %{"post_id" => post_id}, socket) do
|
|
{:noreply, MenuEditor.select_page(socket, post_id, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("select_menu_editor_category", %{"name" => name}, socket) do
|
|
{:noreply, MenuEditor.select_category(socket, name, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("menu_editor_toolbar_action", %{"action" => action}, socket) do
|
|
{:noreply, MenuEditor.toolbar_action(socket, action, &reload_shell/2, &append_output_entry/5)}
|
|
end
|
|
|
|
def handle_event(
|
|
"menu_editor_drop_item",
|
|
%{
|
|
"drag_item_id" => drag_item_id,
|
|
"target_item_id" => target_item_id,
|
|
"position" => position
|
|
},
|
|
socket
|
|
) do
|
|
{:noreply,
|
|
MenuEditor.drop_item(socket, drag_item_id, target_item_id, position, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("menu_editor_keydown", %{"key" => key}, socket) do
|
|
{:noreply, MenuEditor.handle_keydown(socket, key, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("toggle_tag_selection", %{"name" => tag_name}, socket) do
|
|
{:noreply, TagsEditor.toggle_selection(socket, tag_name, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("change_new_tag_editor", %{"new_tag" => params}, socket) do
|
|
{:noreply, TagsEditor.update_new_tag(socket, params, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("create_tag_editor", _params, socket) do
|
|
{:noreply, TagsEditor.create_tag(socket, &reload_shell/2, &append_output_entry/5)}
|
|
end
|
|
|
|
def handle_event("change_edit_tag_editor", %{"edit_tag" => params}, socket) do
|
|
{:noreply, TagsEditor.update_edit_tag(socket, params, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("save_tag_editor", _params, socket) do
|
|
{:noreply, TagsEditor.save_tag(socket, &reload_shell/2, &append_output_entry/5)}
|
|
end
|
|
|
|
def handle_event("delete_tag_editor", _params, socket) do
|
|
{:noreply, TagsEditor.delete_selected(socket, &reload_shell/2, &append_output_entry/5)}
|
|
end
|
|
|
|
def handle_event("change_merge_target", %{"target" => target}, socket) do
|
|
{:noreply, TagsEditor.update_merge_target(socket, target, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("merge_tags_editor", _params, socket) do
|
|
{:noreply, TagsEditor.merge_selected(socket, &reload_shell/2, &append_output_entry/5)}
|
|
end
|
|
|
|
def handle_event("sync_tags_editor", _params, socket) do
|
|
{:noreply, TagsEditor.sync(socket, &reload_shell/2, &append_output_entry/5)}
|
|
end
|
|
|
|
def handle_event("change_script_editor", %{"script_editor" => params}, socket) do
|
|
{:noreply, CodeEntityEditor.update_script(socket, params, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("save_script_editor", _params, socket) do
|
|
{:noreply, CodeEntityEditor.save_script(socket, &reload_shell/2, &append_output_entry/5)}
|
|
end
|
|
|
|
def handle_event("run_script_editor", _params, socket) do
|
|
{:noreply, CodeEntityEditor.run_script(socket, &reload_shell/2, &append_output_entry/5)}
|
|
end
|
|
|
|
def handle_event("check_script_editor", _params, socket) do
|
|
{:noreply, CodeEntityEditor.check_script(socket, &reload_shell/2, &append_output_entry/5)}
|
|
end
|
|
|
|
def handle_event("delete_script_editor", _params, socket) do
|
|
{:noreply, CodeEntityEditor.delete_script(socket, &reload_shell/2, &append_output_entry/5)}
|
|
end
|
|
|
|
def handle_event("change_template_editor", %{"template_editor" => params}, socket) do
|
|
{:noreply, CodeEntityEditor.update_template(socket, params, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("save_template_editor", _params, socket) do
|
|
{:noreply, CodeEntityEditor.save_template(socket, &reload_shell/2, &append_output_entry/5)}
|
|
end
|
|
|
|
def handle_event("validate_template_editor", _params, socket) do
|
|
{:noreply,
|
|
CodeEntityEditor.validate_template(socket, &reload_shell/2, &append_output_entry/5)}
|
|
end
|
|
|
|
def handle_event("delete_template_editor", _params, socket) do
|
|
{:noreply, CodeEntityEditor.delete_template(socket, &reload_shell/2, &append_output_entry/5)}
|
|
end
|
|
|
|
def handle_event("change_chat_editor_input", %{"message" => message}, socket) do
|
|
{:noreply, ChatEditor.update_input(socket, message, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("toggle_chat_model_selector", _params, socket) do
|
|
{:noreply, ChatEditor.toggle_model_selector(socket, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("select_chat_model", %{"model" => model_id}, socket) do
|
|
{:noreply, ChatEditor.set_model(socket, model_id, &reload_shell/2, &append_output_entry/5)}
|
|
end
|
|
|
|
def handle_event("send_chat_editor_message", _params, socket) do
|
|
{:noreply, ChatEditor.send_message(socket, &reload_shell/2, &append_output_entry/5)}
|
|
end
|
|
|
|
def handle_event("abort_chat_editor_message", _params, socket) do
|
|
{:noreply, ChatEditor.abort_message(socket, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("open_chat_settings", _params, socket) do
|
|
{:noreply,
|
|
socket
|
|
|> ChatSurface.clear_action_error()
|
|
|> open_sidebar_item(
|
|
%{"route" => "settings", "id" => "settings-ai", "title" => "Settings", "subtitle" => "AI"},
|
|
:pin
|
|
)}
|
|
end
|
|
|
|
def handle_event(
|
|
"change_chat_surface_form",
|
|
%{"surface" => %{"id" => surface_id, "fields" => fields}},
|
|
socket
|
|
) do
|
|
{:noreply, ChatEditor.update_surface_form(socket, surface_id, fields, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event(
|
|
"select_chat_surface_tab",
|
|
%{"surface-id" => surface_id, "index" => index},
|
|
socket
|
|
) do
|
|
{:noreply,
|
|
ChatEditor.select_surface_tab(socket, surface_id, parse_integer(index), &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("dismiss_chat_surface", %{"surface-id" => surface_id}, socket) do
|
|
{:noreply, ChatEditor.dismiss_surface(socket, surface_id, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("chat_surface_action", params, socket) do
|
|
{:noreply,
|
|
ChatSurface.handle_action(socket, params, %{
|
|
reload: &reload_shell/2,
|
|
open_sidebar: &open_sidebar_item/3
|
|
})}
|
|
end
|
|
|
|
def handle_event("change_import_editor_definition", %{"import_definition" => params}, socket) do
|
|
{:noreply, ImportEditor.change_definition(socket, params, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("select_import_uploads_folder", _params, socket) do
|
|
{:noreply,
|
|
ImportEditor.select_uploads_folder(socket, &reload_shell/2, &append_output_entry/5)}
|
|
end
|
|
|
|
def handle_event("select_import_wxr_file", _params, socket) do
|
|
{:noreply, ImportEditor.select_and_analyze(socket, &reload_shell/2, &append_output_entry/5)}
|
|
end
|
|
|
|
def handle_event("execute_import_editor", _params, socket) do
|
|
{:noreply, ImportEditor.execute_import(socket, &reload_shell/2, &append_output_entry/5)}
|
|
end
|
|
|
|
def handle_event("change_import_conflict_resolution", params, socket) do
|
|
{:noreply, ImportEditor.change_conflict_resolution(socket, params, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("start_import_taxonomy_edit", params, socket) do
|
|
{:noreply, ImportEditor.start_taxonomy_edit(socket, params, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("save_import_taxonomy_edit", params, socket) do
|
|
{:noreply, ImportEditor.save_taxonomy_edit(socket, params, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("cancel_import_taxonomy_edit", _params, socket) do
|
|
{:noreply, ImportEditor.cancel_taxonomy_edit(socket, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("clear_import_taxonomy_mapping", params, socket) do
|
|
{:noreply, ImportEditor.clear_taxonomy_mapping(socket, params, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("toggle_import_section", %{"section" => section}, socket) do
|
|
{:noreply, ImportEditor.toggle_section(socket, section, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("toggle_import_ai_model_selector", _params, socket) do
|
|
{:noreply, ImportEditor.toggle_model_selector(socket, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("select_import_ai_model", %{"model" => model_id}, socket) do
|
|
{:noreply, ImportEditor.select_ai_model(socket, model_id, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event("analyze_import_taxonomy_ai", _params, socket) do
|
|
{:noreply, ImportEditor.analyze_taxonomy_ai(socket, &reload_shell/2, &append_output_entry/5)}
|
|
end
|
|
|
|
def handle_event("rerun_misc_editor", _params, socket) do
|
|
case MiscEditor.rerun(socket) do
|
|
{:command, action} -> {:noreply, apply_shell_command(socket, action)}
|
|
{:noop, next_socket} -> {:noreply, next_socket}
|
|
end
|
|
end
|
|
|
|
def handle_event("apply_site_validation", _params, socket) do
|
|
case MiscEditor.apply_site_validation(socket, &append_output_entry/5) do
|
|
{:rerun, next_socket} -> {:noreply, apply_shell_command(next_socket, "validate_site")}
|
|
{:socket, next_socket} -> {:noreply, next_socket}
|
|
end
|
|
end
|
|
|
|
def handle_event("fix_translation_validation", _params, socket) do
|
|
case MiscEditor.fix_translation_validation(socket, &append_output_entry/5) do
|
|
{:rerun, next_socket} ->
|
|
{:noreply, apply_shell_command(next_socket, "validate_translations")}
|
|
|
|
{:socket, next_socket} ->
|
|
{:noreply, next_socket}
|
|
end
|
|
end
|
|
|
|
def handle_event("select_git_diff_file", %{"path" => path}, socket) do
|
|
{:noreply, socket |> MiscEditor.select_git_diff_file(path) |> assign_misc_editor()}
|
|
end
|
|
|
|
def handle_event("toggle_duplicate_pair", %{"pair-id" => pair_id}, socket) do
|
|
{:noreply, MiscEditor.toggle_duplicate(socket, pair_id, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_event(
|
|
"dismiss_duplicate_pair",
|
|
%{"post-id-a" => post_id_a, "post-id-b" => post_id_b},
|
|
socket
|
|
) do
|
|
{:noreply,
|
|
MiscEditor.dismiss_duplicate(
|
|
socket,
|
|
post_id_a,
|
|
post_id_b,
|
|
&reload_shell/2,
|
|
&append_output_entry/5
|
|
)}
|
|
end
|
|
|
|
def handle_event("dismiss_selected_duplicates", _params, socket) do
|
|
{:noreply, MiscEditor.dismiss_selected(socket, &reload_shell/2, &append_output_entry/5)}
|
|
end
|
|
|
|
def handle_event("repair_metadata_diff", %{"field" => field, "direction" => direction}, socket) do
|
|
case MiscEditor.metadata_diff_repair_request(socket, field, direction) do
|
|
{:ok, params} ->
|
|
{:noreply, apply_shell_command(socket, "repair_metadata_diff", params)}
|
|
|
|
{:error, message} ->
|
|
{:noreply,
|
|
append_output_entry(
|
|
socket,
|
|
translate_for_socket(socket, "Metadata Diff"),
|
|
message,
|
|
nil,
|
|
"error"
|
|
)}
|
|
end
|
|
end
|
|
|
|
def handle_event("import_metadata_diff_orphans", _params, socket) do
|
|
case MiscEditor.metadata_diff_orphan_import_request(socket) do
|
|
{:ok, params} ->
|
|
{:noreply, apply_shell_command(socket, "import_metadata_diff_orphans", params)}
|
|
|
|
{:error, message} ->
|
|
{:noreply,
|
|
append_output_entry(
|
|
socket,
|
|
translate_for_socket(socket, "Metadata Diff"),
|
|
message,
|
|
nil,
|
|
"error"
|
|
)}
|
|
end
|
|
end
|
|
|
|
def handle_event("select_metadata_diff_tab", %{"tab" => tab}, socket) do
|
|
tab_id = socket.assigns.current_tab.id
|
|
|
|
socket =
|
|
socket
|
|
|> assign(
|
|
:metadata_diff_active_tabs,
|
|
Map.put(socket.assigns.metadata_diff_active_tabs, tab_id, tab)
|
|
)
|
|
|> assign(
|
|
:metadata_diff_field_filters,
|
|
Map.delete(socket.assigns.metadata_diff_field_filters, tab_id)
|
|
)
|
|
|> assign_misc_editor()
|
|
|
|
{:noreply, socket}
|
|
end
|
|
|
|
def handle_event("toggle_metadata_diff_field", %{"field" => field}, socket) do
|
|
tab_id = socket.assigns.current_tab.id
|
|
current = Map.get(socket.assigns.metadata_diff_field_filters, tab_id)
|
|
|
|
next_filters =
|
|
if current == field do
|
|
Map.delete(socket.assigns.metadata_diff_field_filters, tab_id)
|
|
else
|
|
Map.put(socket.assigns.metadata_diff_field_filters, tab_id, field)
|
|
end
|
|
|
|
{:noreply,
|
|
socket |> assign(:metadata_diff_field_filters, next_filters) |> assign_misc_editor()}
|
|
end
|
|
|
|
def handle_event("open_duplicate_post", %{"id" => id, "title" => title}, socket) do
|
|
{:noreply,
|
|
open_sidebar_item(
|
|
socket,
|
|
%{"route" => "post", "id" => id, "title" => title, "subtitle" => "draft"},
|
|
:preview
|
|
)}
|
|
end
|
|
|
|
def handle_event("open_overlay", %{"kind" => kind}, socket) do
|
|
socket =
|
|
case socket.assigns[:current_tab] do
|
|
%{type: :post, id: post_id} when kind in ["ai_suggestions", "language_picker"] ->
|
|
assign(
|
|
socket,
|
|
:post_editor_quick_actions_open,
|
|
Map.put(socket.assigns.post_editor_quick_actions_open, post_id, false)
|
|
)
|
|
|
|
%{type: :media, id: media_id}
|
|
when kind in ["ai_suggestions", "language_picker", "confirm_delete"] ->
|
|
assign(
|
|
socket,
|
|
:media_editor_quick_actions_open,
|
|
Map.put(socket.assigns.media_editor_quick_actions_open, media_id, false)
|
|
)
|
|
|
|
_other ->
|
|
socket
|
|
end
|
|
|
|
overlay =
|
|
with overlay_kind when not is_nil(overlay_kind) <- ShellOverlayComponents.kind(kind),
|
|
%{type: route} <- socket.assigns[:current_tab] do
|
|
tab = socket.assigns.current_tab
|
|
title = tab_title(tab, socket.assigns.tab_meta)
|
|
subtitle = tab_subtitle(tab, socket.assigns.tab_meta)
|
|
|
|
Overlay.open(
|
|
route,
|
|
overlay_kind,
|
|
ShellOverlayComponents.context(socket.assigns, title, subtitle)
|
|
)
|
|
end
|
|
|
|
{:noreply, assign(socket, :shell_overlay, overlay)}
|
|
end
|
|
|
|
def handle_event("close_overlay", _params, socket) do
|
|
{:noreply, assign(socket, :shell_overlay, nil)}
|
|
end
|
|
|
|
def handle_event("overlay_keydown", %{"key" => key}, socket) do
|
|
socket =
|
|
case {socket.assigns[:shell_overlay], key} do
|
|
{nil, _other} ->
|
|
socket
|
|
|
|
{_overlay, "Escape"} ->
|
|
assign(socket, :shell_overlay, nil)
|
|
|
|
{%{kind: :gallery} = overlay, "ArrowLeft"} ->
|
|
assign(socket, :shell_overlay, Overlay.lightbox_previous(overlay))
|
|
|
|
{%{kind: :gallery} = overlay, "ArrowRight"} ->
|
|
assign(socket, :shell_overlay, Overlay.lightbox_next(overlay))
|
|
|
|
_other ->
|
|
socket
|
|
end
|
|
|
|
{:noreply, socket}
|
|
end
|
|
|
|
def handle_event("overlay_toggle_ai_field", %{"key" => key}, socket) do
|
|
{:noreply, update_shell_overlay(socket, &Overlay.toggle_ai_field(&1, key))}
|
|
end
|
|
|
|
def handle_event("overlay_set_search", %{"overlay" => %{"query" => query}}, socket) do
|
|
{:noreply, update_shell_overlay(socket, &Overlay.set_search_query(&1, query))}
|
|
end
|
|
|
|
def handle_event("overlay_set_tab", %{"tab" => tab}, socket) do
|
|
{:noreply,
|
|
update_shell_overlay(socket, &Overlay.set_active_tab(&1, ShellOverlayComponents.tab(tab)))}
|
|
end
|
|
|
|
def handle_event("overlay_update_form", %{"overlay" => params}, socket) do
|
|
socket =
|
|
socket
|
|
|> update_shell_overlay(
|
|
&Overlay.update_form_value(&1, :external_url, Map.get(params, "url", ""))
|
|
)
|
|
|> update_shell_overlay(
|
|
&Overlay.update_form_value(&1, :external_text, Map.get(params, "text", ""))
|
|
)
|
|
|
|
{:noreply, socket}
|
|
end
|
|
|
|
def handle_event("overlay_select_result", %{"id" => id}, socket) do
|
|
overlay = socket.assigns[:shell_overlay]
|
|
current_tab = socket.assigns[:current_tab]
|
|
|
|
socket =
|
|
case {overlay, current_tab} do
|
|
{%{kind: :insert_link}, %{type: :post, id: post_id}} ->
|
|
case Overlay.insert_link_result(overlay, id) do
|
|
nil ->
|
|
socket
|
|
|
|
result ->
|
|
PostEditor.insert_content(
|
|
socket,
|
|
post_id,
|
|
ShellOverlayComponents.markdown_link(result.title, result.canonical_url),
|
|
&reload_shell/2
|
|
)
|
|
end
|
|
|
|
{%{kind: :insert_media}, %{type: :post, id: post_id}} ->
|
|
case Overlay.insert_media_result(overlay, id) do
|
|
nil ->
|
|
socket
|
|
|
|
result ->
|
|
syntax =
|
|
if result.is_image do
|
|
""
|
|
else
|
|
"[#{result.original_name}](bds-media://#{result.media_id})"
|
|
end
|
|
|
|
PostEditor.insert_content(socket, post_id, syntax, &reload_shell/2)
|
|
end
|
|
|
|
_other ->
|
|
socket
|
|
end
|
|
|
|
{:noreply, socket}
|
|
end
|
|
|
|
def handle_event("overlay_insert_external", _params, socket) do
|
|
current_tab = socket.assigns[:current_tab]
|
|
|
|
socket =
|
|
case {socket.assigns[:shell_overlay], current_tab} do
|
|
{%{kind: :insert_link} = overlay, %{type: :post, id: post_id}} ->
|
|
details =
|
|
case {overlay.external_url, String.trim(overlay.external_text || "")} do
|
|
{"", _text} -> nil
|
|
{url, ""} -> url
|
|
{url, text} -> ShellOverlayComponents.markdown_link(text, url)
|
|
end
|
|
|
|
if details do
|
|
PostEditor.insert_content(socket, post_id, details, &reload_shell/2)
|
|
else
|
|
socket
|
|
end
|
|
|
|
_other ->
|
|
socket
|
|
end
|
|
|
|
{:noreply, socket}
|
|
end
|
|
|
|
def handle_event("overlay_select_language", %{"code" => code}, socket) do
|
|
current_tab = socket.assigns[:current_tab]
|
|
|
|
socket =
|
|
case {socket.assigns[:shell_overlay], current_tab} do
|
|
{%{kind: :language_picker}, %{type: :post, id: post_id}} ->
|
|
PostEditor.translate(socket, post_id, code, &reload_shell/2, &append_output_entry/5)
|
|
|
|
{%{kind: :language_picker}, %{type: :media, id: media_id}} ->
|
|
MediaEditor.translate(socket, media_id, code, &reload_shell/2, &append_output_entry/5)
|
|
|
|
_other ->
|
|
socket
|
|
end
|
|
|
|
{:noreply, socket}
|
|
end
|
|
|
|
def handle_event("overlay_confirm", _params, socket) do
|
|
current_tab = socket.assigns[:current_tab]
|
|
|
|
socket =
|
|
case {socket.assigns[:shell_overlay], current_tab} do
|
|
{%{kind: :confirm_delete, delete_action: %{source: :sidebar, route: route, id: id}}, _tab} ->
|
|
execute_sidebar_delete(socket, route, id)
|
|
|
|
{%{kind: :ai_suggestions} = overlay, %{type: :post, id: post_id}} ->
|
|
PostEditor.apply_ai_suggestions(
|
|
socket,
|
|
post_id,
|
|
Overlay.selected_ai_fields(overlay),
|
|
&reload_shell/2,
|
|
&append_output_entry/5
|
|
)
|
|
|
|
{%{kind: :ai_suggestions} = overlay, %{type: :media, id: media_id}} ->
|
|
MediaEditor.apply_ai_suggestions(
|
|
socket,
|
|
media_id,
|
|
Overlay.selected_ai_fields(overlay),
|
|
&reload_shell/2,
|
|
&append_output_entry/5
|
|
)
|
|
|
|
{%{kind: :confirm_delete}, %{type: :media, id: media_id}} ->
|
|
MediaEditor.delete_socket(socket, media_id, &reload_shell/2, &append_output_entry/5)
|
|
|
|
{%{kind: :confirm_delete, title: title, entity_name: entity_name}, _tab} ->
|
|
close_overlay_with_output(socket, title, entity_name)
|
|
|
|
{%{kind: :confirm_dialog, title: title, message: message}, _tab} ->
|
|
close_overlay_with_output(socket, title, message)
|
|
|
|
_other ->
|
|
socket
|
|
end
|
|
|
|
{:noreply, socket}
|
|
end
|
|
|
|
def handle_event("overlay_select_gallery_image", %{"id" => id}, socket) do
|
|
{:noreply, update_shell_overlay(socket, &Overlay.select_gallery_image(&1, id))}
|
|
end
|
|
|
|
def handle_event("overlay_close_lightbox", _params, socket) do
|
|
{:noreply, update_shell_overlay(socket, &Overlay.close_lightbox/1)}
|
|
end
|
|
|
|
def handle_event("overlay_lightbox_previous", _params, socket) do
|
|
{:noreply, update_shell_overlay(socket, &Overlay.lightbox_previous/1)}
|
|
end
|
|
|
|
def handle_event("overlay_lightbox_next", _params, socket) do
|
|
{:noreply, update_shell_overlay(socket, &Overlay.lightbox_next/1)}
|
|
end
|
|
|
|
def handle_event("toggle_project_menu", _params, socket) do
|
|
{:noreply, assign(socket, :project_menu_open, not socket.assigns.project_menu_open)}
|
|
end
|
|
|
|
def handle_event("close_project_menu", _params, socket) do
|
|
{:noreply, assign(socket, :project_menu_open, false)}
|
|
end
|
|
|
|
def handle_event("select_project", %{"project_id" => project_id}, socket) do
|
|
{:noreply,
|
|
activate_project(socket, project_id, "Select Project", fn project ->
|
|
"Activated #{project.name}"
|
|
end)}
|
|
end
|
|
|
|
def handle_event("create_project", _params, socket) do
|
|
attrs = %{name: SessionUtil.next_project_name(socket.assigns.projects.projects)}
|
|
|
|
socket =
|
|
case Projects.create_project(attrs) do
|
|
{:ok, project} ->
|
|
activate_project(socket, project.id, "New Project", fn created ->
|
|
"Activated #{created.name}"
|
|
end)
|
|
|
|
{:error, reason} ->
|
|
append_output_entry(socket, "New Project", inspect(reason), nil, "error")
|
|
end
|
|
|
|
{:noreply, socket}
|
|
end
|
|
|
|
def handle_event("import_project", _params, socket) do
|
|
socket =
|
|
case FolderPicker.choose_directory("Open Existing Blog") do
|
|
{:ok, path} ->
|
|
name =
|
|
path
|
|
|> Path.basename()
|
|
|> String.trim()
|
|
|> case do
|
|
"" -> "Imported Blog"
|
|
value -> value
|
|
end
|
|
|
|
case Projects.create_project(%{name: name, data_path: path}) do
|
|
{:ok, project} ->
|
|
activate_project(socket, project.id, "Open Existing Blog", fn imported ->
|
|
"Activated #{imported.name}"
|
|
end)
|
|
|
|
{:error, reason} ->
|
|
append_output_entry(socket, "Open Existing Blog", inspect(reason), nil, "error")
|
|
end
|
|
|
|
:cancel ->
|
|
assign(socket, :project_menu_open, false)
|
|
|
|
{:error, %{message: message}} ->
|
|
append_output_entry(socket, "Open Existing Blog", message, nil, "error")
|
|
end
|
|
|
|
{:noreply, socket}
|
|
end
|
|
|
|
def handle_event("change_ui_language", %{"ui_language" => language}, socket) do
|
|
{:noreply, set_page_language(socket, language)}
|
|
end
|
|
|
|
def handle_event("sync_ui_language", %{"language" => language}, socket) do
|
|
{:noreply, set_page_language(socket, language)}
|
|
end
|
|
|
|
def handle_event("restore_workbench_session", %{"session" => session_payload}, socket)
|
|
when is_map(session_payload) do
|
|
{:noreply, reload_shell(socket, SessionUtil.restore_workbench_session(session_payload))}
|
|
end
|
|
|
|
def handle_event("native_menu_action", %{"action" => action}, socket) do
|
|
{:noreply, handle_native_menu_action(socket, action)}
|
|
end
|
|
|
|
def handle_event("titlebar_menu_keydown", %{"key" => key}, socket) do
|
|
{:noreply, TitlebarMenu.handle_keydown(socket, key, &handle_native_menu_action/2)}
|
|
end
|
|
|
|
def handle_event("toggle_titlebar_menu", %{"group" => group}, socket) do
|
|
{:noreply, TitlebarMenu.toggle(socket, group)}
|
|
end
|
|
|
|
def handle_event("hover_titlebar_menu", %{"group" => group}, socket) do
|
|
{:noreply, TitlebarMenu.hover(socket, group)}
|
|
end
|
|
|
|
def handle_event("close_titlebar_menu", _params, socket) do
|
|
{:noreply, TitlebarMenu.close(socket)}
|
|
end
|
|
|
|
def handle_event("titlebar_menu_action", %{"action" => action}, socket) do
|
|
{:noreply,
|
|
socket
|
|
|> TitlebarMenu.close()
|
|
|> handle_native_menu_action(action)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_info({ref, result}, socket) when is_reference(ref) do
|
|
Process.demonitor(ref, [:flush])
|
|
|
|
cond do
|
|
Map.has_key?(socket.assigns.import_editor_analysis_task_refs, ref) ->
|
|
{:noreply,
|
|
ImportEditor.finish_analysis(
|
|
socket,
|
|
ref,
|
|
result,
|
|
&reload_shell/2,
|
|
&append_output_entry/5
|
|
)}
|
|
|
|
Map.has_key?(socket.assigns.import_editor_execution_task_refs, ref) ->
|
|
{:noreply,
|
|
ImportEditor.finish_execution(
|
|
socket,
|
|
ref,
|
|
result,
|
|
&reload_shell/2,
|
|
&append_output_entry/5
|
|
)}
|
|
|
|
true ->
|
|
{:noreply,
|
|
ChatEditor.finish_request(socket, ref, result, &reload_shell/2, &append_output_entry/5)}
|
|
end
|
|
end
|
|
|
|
def handle_info({:DOWN, ref, :process, _pid, reason}, socket) when is_reference(ref) do
|
|
next_socket =
|
|
cond do
|
|
Map.has_key?(socket.assigns.import_editor_analysis_task_refs, ref) ->
|
|
ImportEditor.handle_task_down(
|
|
socket,
|
|
:analysis,
|
|
ref,
|
|
reason,
|
|
&reload_shell/2,
|
|
&append_output_entry/5
|
|
)
|
|
|
|
Map.has_key?(socket.assigns.import_editor_execution_task_refs, ref) ->
|
|
ImportEditor.handle_task_down(
|
|
socket,
|
|
:execution,
|
|
ref,
|
|
reason,
|
|
&reload_shell/2,
|
|
&append_output_entry/5
|
|
)
|
|
|
|
true ->
|
|
case reason do
|
|
:normal ->
|
|
socket
|
|
|
|
_other ->
|
|
ChatEditor.finish_request(
|
|
socket,
|
|
ref,
|
|
{:error, :cancelled},
|
|
&reload_shell/2,
|
|
&append_output_entry/5
|
|
)
|
|
end
|
|
end
|
|
|
|
{:noreply, next_socket}
|
|
end
|
|
|
|
def handle_info({:import_analysis_progress, definition_id, step, detail}, socket) do
|
|
{:noreply,
|
|
ImportEditor.note_analysis_progress(socket, definition_id, step, detail, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_info(
|
|
{:import_execution_progress, definition_id, phase, current, total, detail},
|
|
socket
|
|
) do
|
|
{:noreply,
|
|
ImportEditor.note_execution_progress(
|
|
socket,
|
|
definition_id,
|
|
phase,
|
|
current,
|
|
total,
|
|
detail,
|
|
&reload_shell/2
|
|
)}
|
|
end
|
|
|
|
def handle_info({:chat_tool_call, conversation_id, tool_call}, socket) do
|
|
{:noreply, ChatEditor.note_tool_call(socket, conversation_id, tool_call, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_info({:chat_tool_result, conversation_id, name}, socket) do
|
|
{:noreply, ChatEditor.note_tool_result(socket, conversation_id, name, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_info({:chat_streaming_content, conversation_id, content}, socket) do
|
|
{:noreply,
|
|
ChatEditor.note_streaming_content(socket, conversation_id, content, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_info({:entity_changed, payload}, socket) when is_map(payload) do
|
|
{:noreply, CliSync.apply_entity_change(socket, payload, &reload_shell/2)}
|
|
end
|
|
|
|
def handle_info(:refresh_task_status, socket) do
|
|
raw_task_status = BDS.Tasks.status_snapshot()
|
|
|
|
case SessionUtil.next_completed_task_result(socket, raw_task_status) do
|
|
nil ->
|
|
task_status = localize_task_status(raw_task_status, socket.assigns.page_language)
|
|
|
|
{:noreply,
|
|
socket
|
|
|> assign(:task_status, task_status)
|
|
|> assign(:editor_meta, ShellData.editor_meta(task_status))
|
|
|> assign(
|
|
:status,
|
|
ShellData.status_bar(socket.assigns.workbench, task_status, socket.assigns.dashboard,
|
|
ui_language: socket.assigns.page_language,
|
|
offline_mode: socket.assigns.offline_mode
|
|
)
|
|
)}
|
|
|
|
task ->
|
|
{:noreply,
|
|
socket
|
|
|> SessionUtil.mark_task_result_handled(task.id)
|
|
|> apply_shell_command_result(task.result)}
|
|
end
|
|
end
|
|
|
|
@impl true
|
|
def render(assigns) do
|
|
UILocale.put(assigns.page_language)
|
|
index(assigns)
|
|
end
|
|
|
|
defp reload_shell(socket, workbench) do
|
|
projects = ShellData.project_snapshot()
|
|
dashboard = ShellData.dashboard(projects.active_project_id)
|
|
git_badge_count = ShellData.git_badge_count(projects.active_project_id)
|
|
active_view_id = Atom.to_string(workbench.active_view)
|
|
tab_meta = TabHelpers.sync_tab_meta(workbench, socket.assigns[:tab_meta] || %{})
|
|
|
|
sidebar_data =
|
|
ShellData.sidebar_view(
|
|
projects.active_project_id,
|
|
active_view_id,
|
|
ShellSidebarState.current_filters(socket, active_view_id)
|
|
)
|
|
|
|
sidebar_data = ShellSidebarState.merge_ui_state(socket, active_view_id, sidebar_data)
|
|
raw_task_status = BDS.Tasks.status_snapshot()
|
|
activity_buttons = Workbench.activity_buttons(workbench, git_badge_count)
|
|
page_language = socket.assigns[:page_language] || ShellData.ui_language()
|
|
|
|
offline_mode =
|
|
if connected?(socket) do
|
|
Map.get(socket.assigns, :offline_mode, AI.airplane_mode?(true))
|
|
else
|
|
Map.get(socket.assigns, :offline_mode, true)
|
|
end
|
|
|
|
task_status = localize_task_status(raw_task_status, page_language)
|
|
|
|
socket
|
|
|> assign(:tab_meta, tab_meta)
|
|
|> assign(:workbench, workbench)
|
|
|> assign(:projects, projects)
|
|
|> assign(:current_project, ShellData.current_project(projects))
|
|
|> assign(:dashboard, dashboard)
|
|
|> assign(:dashboard_timeline_entries, Map.get(dashboard, :timeline_entries, []))
|
|
|> assign(:dashboard_category_counts, Map.get(dashboard, :category_counts, []))
|
|
|> assign(:dashboard_recent_posts, Map.get(dashboard, :recent_posts, []))
|
|
|> assign(
|
|
:dashboard_tag_cloud_items,
|
|
ShellData.dashboard_tag_cloud_items(Map.get(dashboard, :tag_cloud_items, []))
|
|
)
|
|
|> assign(:sidebar_data, sidebar_data)
|
|
|> assign(
|
|
:sidebar_header,
|
|
active_sidebar_label(activity_buttons, workbench.active_view, sidebar_data)
|
|
)
|
|
|> assign(:assistant_cards, ShellData.assistant_cards())
|
|
|> assign(:editor_meta, ShellData.editor_meta(task_status))
|
|
|> assign(:task_status, task_status)
|
|
|> assign(
|
|
:status,
|
|
ShellData.status_bar(workbench, task_status, dashboard,
|
|
ui_language: page_language,
|
|
offline_mode: offline_mode
|
|
)
|
|
)
|
|
|> assign(:activity_buttons, activity_buttons)
|
|
|> assign(:panel_tabs, ShellData.panel_tabs(workbench))
|
|
|> assign(:supported_ui_languages, ShellData.supported_ui_languages())
|
|
|> assign(:menu_groups, socket.assigns[:menu_groups] || TitlebarMenu.groups())
|
|
|> assign(:titlebar_menu_item_index, socket.assigns[:titlebar_menu_item_index])
|
|
|> assign(:current_tab, current_tab(workbench))
|
|
|> assign_post_editor()
|
|
|> assign_media_editor()
|
|
|> assign_settings_editor()
|
|
|> assign_menu_editor()
|
|
|> assign_tags_editor()
|
|
|> assign_code_entity_editor()
|
|
|> assign_chat_editor()
|
|
|> assign_import_editor()
|
|
|> assign_misc_editor()
|
|
end
|
|
|
|
defp translated(text, bindings \\ %{}),
|
|
do: ShellData.translate(text, bindings, UILocale.current())
|
|
|
|
defp encoded_shortcuts(shortcuts), do: Jason.encode!(shortcuts)
|
|
|
|
defp encoded_workbench_session(workbench), do: Jason.encode!(Session.serialize(workbench))
|
|
|
|
defp panel_tab_label(:tasks), do: translated("Tasks")
|
|
defp panel_tab_label(:output), do: translated("Output")
|
|
defp panel_tab_label(:git_log), do: translated("Git Log")
|
|
defp panel_tab_label(tab), do: ShellData.route_label(tab)
|
|
|
|
defp activity_label("AI Assistant"), do: "Chat"
|
|
defp activity_label("Source Control"), do: "Git"
|
|
defp activity_label(label), do: translated(label)
|
|
|
|
defp active_sidebar_label(activity_buttons, active_view, sidebar_data) do
|
|
Enum.find_value(activity_buttons, translated(Map.get(sidebar_data, :title, "")), fn button ->
|
|
if button.id == active_view, do: activity_label(button.label), else: nil
|
|
end)
|
|
end
|
|
|
|
defp sidebar_header_label(label), do: translated(label)
|
|
|
|
defp timeline_height(entry, entries) do
|
|
max_count =
|
|
entries
|
|
|> Enum.map(&(&1.count || 0))
|
|
|> Enum.max(fn -> 1 end)
|
|
|
|
max(4, (entry.count || 0) / max_count * 100)
|
|
end
|
|
|
|
defp current_tab(%{active_tab: nil}), do: nil
|
|
|
|
defp current_tab(%{tabs: tabs, active_tab: {type, id}}) do
|
|
Enum.find(tabs, &(&1.type == type and &1.id == id))
|
|
end
|
|
|
|
defp assign_post_editor(socket) do
|
|
PostEditor.assign_socket(socket)
|
|
end
|
|
|
|
defp assign_media_editor(socket) do
|
|
MediaEditor.assign_socket(socket)
|
|
end
|
|
|
|
defp assign_settings_editor(socket) do
|
|
SettingsEditor.assign_socket(socket)
|
|
end
|
|
|
|
defp assign_menu_editor(socket) do
|
|
MenuEditor.assign_socket(socket)
|
|
end
|
|
|
|
defp assign_tags_editor(socket) do
|
|
TagsEditor.assign_socket(socket)
|
|
end
|
|
|
|
defp assign_code_entity_editor(socket) do
|
|
CodeEntityEditor.assign_socket(socket)
|
|
end
|
|
|
|
defp assign_chat_editor(socket) do
|
|
ChatEditor.assign_socket(socket)
|
|
end
|
|
|
|
defp assign_import_editor(socket) do
|
|
ImportEditor.assign_socket(socket)
|
|
end
|
|
|
|
defp assign_misc_editor(socket) do
|
|
MiscEditor.assign_socket(socket)
|
|
end
|
|
|
|
defp create_sidebar_item(socket, kind),
|
|
do: SidebarCreate.create(socket, kind, sidebar_create_callbacks())
|
|
|
|
defp sidebar_create_callbacks do
|
|
%{
|
|
reload: &reload_shell/2,
|
|
open_sidebar: &open_sidebar_item/3,
|
|
append_output: &append_output_entry/5
|
|
}
|
|
end
|
|
|
|
defp open_sidebar_item(socket, params, intent) do
|
|
route_atom = sidebar_route_atom(Map.fetch!(params, "route"))
|
|
tab_id = tab_id_for_route(route_atom, Map.fetch!(params, "id"))
|
|
|
|
workbench =
|
|
Workbench.open_tab(
|
|
socket.assigns.workbench,
|
|
route_atom,
|
|
tab_id,
|
|
tab_intent(route_atom, intent)
|
|
)
|
|
|
|
tab_meta =
|
|
Map.put(socket.assigns.tab_meta, {route_atom, tab_id}, %{
|
|
sidebar_item_id: Map.get(params, "id"),
|
|
title: Map.get(params, "title", ""),
|
|
subtitle: Map.get(params, "subtitle", "")
|
|
})
|
|
|
|
socket
|
|
|> assign(:tab_meta, tab_meta)
|
|
|> reload_shell(workbench)
|
|
end
|
|
|
|
defp sidebar_create_action(view), do: SidebarCreate.action(view)
|
|
|
|
defp set_page_language(socket, language) do
|
|
codes =
|
|
Enum.map(
|
|
socket.assigns[:supported_ui_languages] || ShellData.supported_ui_languages(),
|
|
& &1.code
|
|
)
|
|
|
|
normalized =
|
|
language
|
|
|> to_string()
|
|
|> String.trim()
|
|
|> case do
|
|
value -> if(value in codes, do: value, else: socket.assigns.page_language)
|
|
end
|
|
|
|
if normalized == socket.assigns.page_language do
|
|
socket
|
|
else
|
|
socket
|
|
|> assign(:page_language, normalized)
|
|
|> reload_shell(socket.assigns.workbench)
|
|
end
|
|
end
|
|
|
|
defp activate_project(socket, project_id, title, message_fun) do
|
|
cond do
|
|
project_id == socket.assigns.projects.active_project_id ->
|
|
assign(socket, :project_menu_open, false)
|
|
|
|
true ->
|
|
case Projects.set_active_project(project_id) do
|
|
{:ok, project} ->
|
|
socket
|
|
|> assign(:project_menu_open, false)
|
|
|> assign(:sidebar_filters_by_view, %{})
|
|
|> append_output_entry(title, message_fun.(project))
|
|
|> reload_shell(Workbench.new())
|
|
|
|
{:error, reason} ->
|
|
socket
|
|
|> assign(:project_menu_open, false)
|
|
|> append_output_entry(title, inspect(reason), nil, "error")
|
|
end
|
|
end
|
|
end
|
|
|
|
defp append_output_entry(socket, title, message, details \\ nil, level \\ "info") do
|
|
entry = %{title: title, message: message, details: details, level: level}
|
|
entries = [entry | socket.assigns.output_entries] |> Enum.take(@output_entry_limit)
|
|
assign(socket, :output_entries, entries)
|
|
end
|
|
|
|
defp handle_native_menu_action(socket, action) do
|
|
case BoundedAtoms.menu_action(action) do
|
|
nil -> append_output_entry(socket, "Menu", "Unsupported shell command", action, "error")
|
|
action_atom -> handle_menu_action(socket, action_atom)
|
|
end
|
|
end
|
|
|
|
defp handle_menu_action(socket, action) when is_atom(action) do
|
|
cond do
|
|
MapSet.member?(@local_menu_actions, action) ->
|
|
reload_shell(socket, MenuBar.execute(socket.assigns.workbench, action))
|
|
|
|
MapSet.member?(@socket_menu_actions, action) ->
|
|
handle_socket_menu_action(socket, action)
|
|
|
|
MapSet.member?(@runtime_menu_actions, action) ->
|
|
push_event(socket, "menu-runtime-command", %{action: Atom.to_string(action)})
|
|
|
|
shell_command?(action) ->
|
|
apply_shell_command(socket, Atom.to_string(action))
|
|
|
|
true ->
|
|
append_output_entry(socket, "Menu", "Unsupported shell command", Atom.to_string(action), "error")
|
|
end
|
|
end
|
|
|
|
defp handle_socket_menu_action(socket, :new_post), do: create_sidebar_item(socket, "post")
|
|
defp handle_socket_menu_action(socket, :import_media), do: create_sidebar_item(socket, "media")
|
|
defp handle_socket_menu_action(socket, :save), do: save_current_tab(socket)
|
|
defp handle_socket_menu_action(socket, :publish_selected), do: publish_current_tab(socket)
|
|
|
|
defp handle_socket_menu_action(socket, :quit) do
|
|
Shutdown.request_quit()
|
|
socket
|
|
end
|
|
|
|
defp handle_socket_menu_action(socket, :view_on_github) do
|
|
OS.launch_default_browser("https://github.com/rfc1437/bDS")
|
|
socket
|
|
end
|
|
|
|
defp handle_socket_menu_action(socket, :report_issue) do
|
|
OS.launch_default_browser("https://github.com/rfc1437/bDS/issues")
|
|
socket
|
|
end
|
|
|
|
defp handle_socket_menu_action(socket, :about) do
|
|
append_output_entry(
|
|
socket,
|
|
"About",
|
|
"Blogging Desktop Server",
|
|
"Version #{Application.spec(:bds, :vsn) |> to_string()}",
|
|
"info"
|
|
)
|
|
end
|
|
|
|
defp shell_command?(action), do: not is_nil(shell_command_atom(action))
|
|
|
|
defp save_current_tab(%{assigns: %{current_tab: %{type: :post, id: post_id}}} = socket) do
|
|
PostEditor.persist_socket(socket, post_id, :save, &reload_shell/2, &append_output_entry/5)
|
|
end
|
|
|
|
defp save_current_tab(%{assigns: %{current_tab: %{type: :media, id: media_id}}} = socket) do
|
|
MediaEditor.persist_socket(socket, media_id, &reload_shell/2, &append_output_entry/5)
|
|
end
|
|
|
|
defp save_current_tab(%{assigns: %{current_tab: %{type: :settings}}} = socket) do
|
|
SettingsEditor.save_project(socket, &reload_shell/2, &append_output_entry/5)
|
|
end
|
|
|
|
defp save_current_tab(%{assigns: %{current_tab: %{type: :menu_editor}}} = socket) do
|
|
MenuEditor.toolbar_action(socket, "save", &reload_shell/2, &append_output_entry/5)
|
|
end
|
|
|
|
defp save_current_tab(%{assigns: %{current_tab: %{type: :tags}}} = socket) do
|
|
TagsEditor.save_tag(socket, &reload_shell/2, &append_output_entry/5)
|
|
end
|
|
|
|
defp save_current_tab(%{assigns: %{current_tab: %{type: :scripts}}} = socket) do
|
|
CodeEntityEditor.save_script(socket, &reload_shell/2, &append_output_entry/5)
|
|
end
|
|
|
|
defp save_current_tab(%{assigns: %{current_tab: %{type: :templates}}} = socket) do
|
|
CodeEntityEditor.save_template(socket, &reload_shell/2, &append_output_entry/5)
|
|
end
|
|
|
|
defp save_current_tab(socket), do: reload_shell(socket, socket.assigns.workbench)
|
|
|
|
defp publish_current_tab(%{assigns: %{current_tab: %{type: :post, id: post_id}}} = socket) do
|
|
PostEditor.persist_socket(socket, post_id, :publish, &reload_shell/2, &append_output_entry/5)
|
|
end
|
|
|
|
defp publish_current_tab(socket), do: reload_shell(socket, socket.assigns.workbench)
|
|
|
|
defp apply_shell_command(socket, action, params \\ %{}),
|
|
do: ShellCommandRunner.execute(socket, action, params, shell_command_callbacks())
|
|
|
|
defp apply_shell_command_result(socket, result),
|
|
do: ShellCommandRunner.apply_result(socket, result, shell_command_callbacks())
|
|
|
|
defp shell_command_callbacks do
|
|
%{
|
|
reload: &reload_shell/2,
|
|
append_output: &append_output_entry/5
|
|
}
|
|
end
|
|
|
|
defp shell_command_atom(action), do: ShellCommandRunner.shell_command_atom(action)
|
|
|
|
defp mac_ui? do
|
|
case Application.get_env(:bds, :shell_platform) do
|
|
nil -> match?({:unix, :darwin}, :os.type())
|
|
platform -> match?({:unix, :darwin}, platform)
|
|
end
|
|
end
|
|
|
|
defp update_shell_overlay(socket, updater),
|
|
do: ChatSurface.update_shell_overlay(socket, updater)
|
|
|
|
defp close_overlay_with_output(socket, title, details) do
|
|
socket
|
|
|> append_output_entry(title, translated("Command completed"), details)
|
|
|> assign(:shell_overlay, nil)
|
|
end
|
|
|
|
defp request_sidebar_delete(socket, route, id, fallback_title) do
|
|
case sidebar_delete_target(socket, route, id, fallback_title) do
|
|
{:ok, entity_name} ->
|
|
assign(socket, :shell_overlay, %{
|
|
kind: :confirm_delete,
|
|
title: sidebar_delete_title(route),
|
|
entity_name: entity_name,
|
|
entity_type: route,
|
|
reference_count: 0,
|
|
reference_list: [],
|
|
delete_action: %{source: :sidebar, route: route, id: id}
|
|
})
|
|
|
|
{:error, reason} ->
|
|
socket
|
|
|> assign(:shell_overlay, nil)
|
|
|> append_output_entry(sidebar_delete_title(route), inspect(reason), nil, "error")
|
|
|> reload_shell(socket.assigns.workbench)
|
|
end
|
|
end
|
|
|
|
defp execute_sidebar_delete(socket, route, id) do
|
|
case route do
|
|
"post" ->
|
|
socket
|
|
|> assign(:shell_overlay, nil)
|
|
|> PostEditor.delete_socket(id, &reload_shell/2, &append_output_entry/5)
|
|
|
|
"media" ->
|
|
socket
|
|
|> assign(:shell_overlay, nil)
|
|
|> MediaEditor.delete_socket(id, &reload_shell/2, &append_output_entry/5)
|
|
|
|
"scripts" ->
|
|
delete_sidebar_script(socket, id)
|
|
|
|
"templates" ->
|
|
delete_sidebar_template(socket, id)
|
|
|
|
"chat" ->
|
|
delete_sidebar_chat(socket, id)
|
|
|
|
"import" ->
|
|
delete_sidebar_import(socket, id)
|
|
|
|
_other ->
|
|
socket
|
|
|> assign(:shell_overlay, nil)
|
|
|> append_output_entry(translated("Delete"), inspect(:unsupported_route), nil, "error")
|
|
|> reload_shell(socket.assigns.workbench)
|
|
end
|
|
end
|
|
|
|
defp delete_sidebar_script(socket, script_id) do
|
|
case Scripts.delete_script(script_id) do
|
|
{:ok, :deleted} ->
|
|
workbench = Workbench.close_tab(socket.assigns.workbench, :scripts, script_id)
|
|
|
|
socket
|
|
|> assign(:shell_overlay, nil)
|
|
|> assign(:tab_meta, Map.delete(socket.assigns.tab_meta, {:scripts, script_id}))
|
|
|> assign(:script_editor_drafts, Map.delete(socket.assigns.script_editor_drafts, script_id))
|
|
|> reload_shell(workbench)
|
|
|
|
{:error, reason} ->
|
|
socket
|
|
|> assign(:shell_overlay, nil)
|
|
|> append_output_entry(sidebar_delete_title("scripts"), inspect(reason), nil, "error")
|
|
|> reload_shell(socket.assigns.workbench)
|
|
end
|
|
end
|
|
|
|
defp delete_sidebar_template(socket, template_id) do
|
|
case Templates.delete_template(template_id, force: true) do
|
|
{:ok, :deleted} ->
|
|
workbench = Workbench.close_tab(socket.assigns.workbench, :templates, template_id)
|
|
|
|
socket
|
|
|> assign(:shell_overlay, nil)
|
|
|> assign(:tab_meta, Map.delete(socket.assigns.tab_meta, {:templates, template_id}))
|
|
|> assign(
|
|
:template_editor_drafts,
|
|
Map.delete(socket.assigns.template_editor_drafts, template_id)
|
|
)
|
|
|> reload_shell(workbench)
|
|
|
|
{:error, reason} ->
|
|
socket
|
|
|> assign(:shell_overlay, nil)
|
|
|> append_output_entry(sidebar_delete_title("templates"), inspect(reason), nil, "error")
|
|
|> reload_shell(socket.assigns.workbench)
|
|
end
|
|
end
|
|
|
|
defp delete_sidebar_chat(socket, conversation_id) do
|
|
case AI.delete_chat_conversation(conversation_id) do
|
|
{:ok, :deleted} ->
|
|
workbench = Workbench.close_tab(socket.assigns.workbench, :chat, conversation_id)
|
|
|
|
socket
|
|
|> assign(:shell_overlay, nil)
|
|
|> assign(:tab_meta, Map.delete(socket.assigns.tab_meta, {:chat, conversation_id}))
|
|
|> reload_shell(workbench)
|
|
|
|
{:error, reason} ->
|
|
socket
|
|
|> assign(:shell_overlay, nil)
|
|
|> append_output_entry(sidebar_delete_title("chat"), inspect(reason), nil, "error")
|
|
|> reload_shell(socket.assigns.workbench)
|
|
end
|
|
end
|
|
|
|
defp delete_sidebar_import(socket, definition_id) do
|
|
case ImportDefinitions.delete_definition(definition_id) do
|
|
{:ok, :deleted} ->
|
|
workbench = Workbench.close_tab(socket.assigns.workbench, :import, definition_id)
|
|
|
|
socket
|
|
|> assign(:shell_overlay, nil)
|
|
|> assign(:tab_meta, Map.delete(socket.assigns.tab_meta, {:import, definition_id}))
|
|
|> clear_import_editor_state(definition_id)
|
|
|> reload_shell(workbench)
|
|
|
|
{:error, reason} ->
|
|
socket
|
|
|> assign(:shell_overlay, nil)
|
|
|> append_output_entry(sidebar_delete_title("import"), inspect(reason), nil, "error")
|
|
|> reload_shell(socket.assigns.workbench)
|
|
end
|
|
end
|
|
|
|
defp clear_import_editor_state(socket, definition_id) do
|
|
socket
|
|
|> assign(
|
|
:import_editor_analysis_states,
|
|
Map.delete(socket.assigns.import_editor_analysis_states, definition_id)
|
|
)
|
|
|> assign(
|
|
:import_editor_analysis_task_refs,
|
|
Map.delete(socket.assigns.import_editor_analysis_task_refs, definition_id)
|
|
)
|
|
|> assign(
|
|
:import_editor_execution_states,
|
|
Map.delete(socket.assigns.import_editor_execution_states, definition_id)
|
|
)
|
|
|> assign(
|
|
:import_editor_execution_task_refs,
|
|
Map.delete(socket.assigns.import_editor_execution_task_refs, definition_id)
|
|
)
|
|
|> assign(:import_editor_sections, Map.delete(socket.assigns.import_editor_sections, definition_id))
|
|
|> assign(
|
|
:import_editor_taxonomy_edits,
|
|
Map.delete(socket.assigns.import_editor_taxonomy_edits, definition_id)
|
|
)
|
|
|> assign(
|
|
:import_editor_model_selectors_open,
|
|
Map.delete(socket.assigns.import_editor_model_selectors_open, definition_id)
|
|
)
|
|
|> assign(
|
|
:import_editor_selected_models,
|
|
Map.delete(socket.assigns.import_editor_selected_models, definition_id)
|
|
)
|
|
end
|
|
|
|
defp sidebar_delete_target(socket, route, id, fallback_title) do
|
|
active_project_id = socket.assigns.projects.active_project_id
|
|
|
|
case route do
|
|
"post" ->
|
|
case Posts.get_post(id) do
|
|
%{project_id: ^active_project_id} = post ->
|
|
{:ok, present_title(fallback_title) || present_title(post.title) || present_title(post.slug) || id}
|
|
|
|
_other ->
|
|
{:error, :not_found}
|
|
end
|
|
|
|
"media" ->
|
|
case Media.get_media(id) do
|
|
%{project_id: ^active_project_id} = media ->
|
|
{:ok,
|
|
present_title(fallback_title) || present_title(media.title) ||
|
|
present_title(media.original_name) || id}
|
|
|
|
_other ->
|
|
{:error, :not_found}
|
|
end
|
|
|
|
"scripts" ->
|
|
case Scripts.get_script(id) do
|
|
%{project_id: ^active_project_id} = script ->
|
|
{:ok, present_title(fallback_title) || present_title(script.title) || id}
|
|
|
|
_other ->
|
|
{:error, :not_found}
|
|
end
|
|
|
|
"templates" ->
|
|
case Templates.get_template(id) do
|
|
%{project_id: ^active_project_id} = template ->
|
|
{:ok, present_title(fallback_title) || present_title(template.title) || id}
|
|
|
|
_other ->
|
|
{:error, :not_found}
|
|
end
|
|
|
|
"chat" ->
|
|
case AI.get_chat_conversation(id) do
|
|
%{title: title} -> {:ok, present_title(fallback_title) || present_title(title) || id}
|
|
_other -> {:error, :not_found}
|
|
end
|
|
|
|
"import" ->
|
|
case ImportDefinitions.get_definition(id) do
|
|
%{project_id: ^active_project_id} = definition ->
|
|
{:ok, present_title(fallback_title) || present_title(definition.name) || id}
|
|
|
|
_other ->
|
|
{:error, :not_found}
|
|
end
|
|
|
|
_other ->
|
|
{:error, :unsupported_route}
|
|
end
|
|
end
|
|
|
|
defp sidebar_delete_title("chat"), do: translated("sidebar.chat.deleteConversation")
|
|
defp sidebar_delete_title("post"), do: translated("Delete") <> " " <> translated("Post")
|
|
defp sidebar_delete_title("media"), do: translated("Delete") <> " " <> translated("Media")
|
|
defp sidebar_delete_title("scripts"), do: translated("Delete") <> " " <> translated("Script")
|
|
defp sidebar_delete_title("templates"), do: translated("Delete") <> " " <> translated("Template")
|
|
defp sidebar_delete_title("import"), do: translated("Delete") <> " " <> translated("Import")
|
|
defp sidebar_delete_title(_route), do: translated("Delete")
|
|
|
|
defp present_title(value) when is_binary(value) do
|
|
case String.trim(value) do
|
|
"" -> nil
|
|
trimmed -> trimmed
|
|
end
|
|
end
|
|
|
|
defp present_title(_value), do: nil
|
|
end
|