defmodule BDS.Desktop.ShellData do @moduledoc false use Gettext, backend: BDS.Gettext alias BDS.Git alias BDS.I18n alias BDS.Projects alias BDS.UI.Dashboard alias BDS.UI.Sidebar alias BDS.UI.Workbench def title do Application.get_env(:bds, :desktop)[:title] || "Blogging Desktop Server" end def activity_icon(id) do case to_string(id) do "posts" -> ~s() "pages" -> ~s() "media" -> ~s() "scripts" -> ~s() "templates" -> ~s() "tags" -> ~s() "chat" -> ~s() "import" -> ~s() "git" -> ~s() "settings" -> ~s() _other -> activity_icon("posts") end end def ui_language do I18n.current_ui_locale() end def supported_ui_languages do Enum.map(I18n.supported_languages(), fn language -> %{code: language.code, flag: I18n.flag(language.code)} end) end def project_snapshot do Projects.shell_snapshot() rescue error in [Exqlite.Error, DBConnection.OwnershipError] -> if match?(%Exqlite.Error{}, error) and not String.contains?(Exception.message(error), "no such table: projects") do reraise error, __STACKTRACE__ end default_project_snapshot() end def current_project(projects_snapshot) do Enum.find(projects_snapshot.projects, &(&1.id == projects_snapshot.active_project_id)) || List.first(projects_snapshot.projects) end def dashboard(project_id) do Dashboard.snapshot(project_id) rescue error in [Exqlite.Error, DBConnection.OwnershipError] -> if match?(%Exqlite.Error{}, error) and not String.contains?(Exception.message(error), "no such table") do reraise error, __STACKTRACE__ end Dashboard.empty_snapshot() end def sidebar_view(project_id, view_id, params \\ %{}) do Sidebar.view(project_id, view_id, params) rescue error in [Exqlite.Error, DBConnection.OwnershipError] -> if match?(%Exqlite.Error{}, error) and not String.contains?(Exception.message(error), "no such table") do reraise error, __STACKTRACE__ end Sidebar.view(nil, view_id, params) end def assistant_cards do [ %{label: dgettext("ui", "Offline Gate"), text: dgettext("ui", "Automatic AI actions stay gated by airplane mode.")}, %{ label: dgettext("ui", "Filesystem Sync"), text: dgettext("ui", "Metadata flush, diffing, and rebuild hooks still need editor wiring.") }, %{label: dgettext("ui", "Desktop Runtime"), text: dgettext("ui", "The app window is now served from LiveView state.")} ] end def editor_meta(task_status) do [ %{label: dgettext("ui", "Status"), value: task_status.running_task_message || dgettext("ui", "Idle")}, %{label: dgettext("ui", "Mode"), value: dgettext("ui", "Offline")}, %{label: dgettext("ui", "Main Language"), value: ui_language()} ] end def status_bar(workbench, task_status, dashboard, opts \\ []) do Workbench.status_bar(workbench, post_count: dashboard.post_stats.total_posts, media_count: dashboard.media_stats.media_count, theme_badge: "desktop-shell", ui_language: Keyword.get(opts, :ui_language, ui_language()), offline_mode: Keyword.get(opts, :offline_mode, true), running_task_message: task_status.running_task_message, running_task_overflow: task_status.running_task_overflow, active_post_status: nil ) end def git_badge_count(project_id, opts \\ []) def git_badge_count(nil, _opts), do: 0 def git_badge_count("default", _opts), do: 0 def git_badge_count(project_id, opts) when is_binary(project_id) do provider = Keyword.get(opts, :provider, git_remote_state_provider()) try do case provider.(project_id, []) do {:ok, %{behind: behind}} when is_integer(behind) and behind > 0 -> behind {:ok, %{behind: behind}} when is_binary(behind) -> parse_positive_count(behind) _other -> 0 end rescue error in [DBConnection.OwnershipError, Exqlite.Error] -> if match?(%Exqlite.Error{}, error) and not String.contains?(Exception.message(error), "no such table") do reraise error, __STACKTRACE__ end 0 end end def panel_tabs(workbench) do [:tasks, :output] |> maybe_add_panel_tab(workbench.editor_route, :post_links) |> maybe_add_panel_tab(workbench.editor_route, :git_log) |> Kernel.++([workbench.panel.active_tab]) |> Enum.uniq() end def dashboard_status_label(status) do case to_string(status) do "draft" -> dgettext("ui", "Draft") "published" -> dgettext("ui", "Published") "archived" -> dgettext("ui", "Archived") other -> other |> String.replace("_", " ") |> String.capitalize() end end def dashboard_post_count_label(count) do normalized_count = count || 0 dngettext("ui", "%{count} post", "%{count} posts", normalized_count, count: normalized_count) end def dashboard_tag_cloud_items(items) when is_list(items) do top_items = items |> Enum.sort_by(fn item -> -(item.count || 0) end) |> Enum.take(40) counts = Enum.map(top_items, &(&1.count || 0)) max_count = Enum.max([1 | counts]) min_count = Enum.min([max_count | counts]) range = max(max_count - min_count, 1) top_items |> Enum.map(fn item -> font_size = 11 + ((item.count || 0) - min_count) / range * 11 Map.merge(item, %{font_size: font_size, color: normalize_dashboard_tag_color(item.color)}) end) |> Enum.sort_by(&String.downcase(to_string(&1.tag || ""))) end def render_dashboard_tag_style(item) do declarations = ["font-size: #{Float.round(item.font_size || 11, 1)}px;"] declarations = if item.color do declarations ++ [ "background-color: #{item.color};", "color: #{dashboard_contrast_color(item.color)};" ] else declarations end Enum.join(declarations, " ") end def format_dashboard_month(year, month) do {:ok, date} = Date.new(year, month, 1) Calendar.strftime(date, "%b") end def format_dashboard_date(nil), do: "" def format_dashboard_date(timestamp) do timestamp |> DateTime.from_unix!(:millisecond) |> Calendar.strftime("%x") end def route_label(route) do case to_string(route) do "git_log" -> dgettext("ui", "Git Log") "post_links" -> dgettext("ui", "Post Links") other -> other |> String.replace("_", " ") |> String.split() |> Enum.map_join(" ", &String.capitalize/1) end end def format_bytes(bytes) do normalized_bytes = max(bytes || 0, 0) cond do normalized_bytes == 0 -> "0 B" true -> units = ["B", "KB", "MB", "GB"] unit_index = min(trunc(:math.log(normalized_bytes) / :math.log(1024)), length(units) - 1) value = normalized_bytes / :math.pow(1024, unit_index) decimals = if value >= 10 or unit_index == 0, do: 0, else: 1 unit = Enum.at(units, unit_index) :erlang.float_to_binary(value, decimals: decimals) <> " " <> unit end end defp git_remote_state_provider do Application.get_env(:bds, :git_remote_state_provider, &Git.remote_state/2) end defp parse_positive_count(value) do case Integer.parse(value) do {count, _rest} when count > 0 -> count _other -> 0 end end defp maybe_add_panel_tab(tabs, :post, :post_links), do: tabs ++ [:post_links] defp maybe_add_panel_tab(tabs, route, :git_log) when route in [:post, :media], do: tabs ++ [:git_log] defp maybe_add_panel_tab(tabs, _route, _tab), do: tabs defp default_project_snapshot do %{ active_project_id: "default", projects: [ %{ id: "default", name: "My Blog", slug: "my-blog", data_path: nil, is_active: true } ] } end defp normalize_dashboard_tag_color(nil), do: nil defp normalize_dashboard_tag_color(""), do: nil defp normalize_dashboard_tag_color("#" <> rest = color) when byte_size(rest) == 6 do if String.match?(rest, ~r/\A[0-9a-fA-F]{6}\z/), do: color, else: nil end defp normalize_dashboard_tag_color(_color), do: nil defp dashboard_contrast_color("#" <> rgb) do <> = rgb {red, _} = Integer.parse(r, 16) {green, _} = Integer.parse(g, 16) {blue, _} = Integer.parse(b, 16) luminance = (red * 299 + green * 587 + blue * 114) / 1000 if luminance > 150, do: "#1e1e1e", else: "#ffffff" end defp dashboard_contrast_color(_color), do: "#ffffff" end