defmodule BDS.Desktop.ShellData do @moduledoc false 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 ui_language do I18n.current_ui_locale() end def translations do I18n.get_ui_translations(ui_language()) end def supported_ui_languages do Enum.map(I18n.supported_languages(), fn language -> %{code: language.code, flag: I18n.flag(language.code)} end) end def translate(key, bindings \\ %{}) do text = Map.get(translations(), to_string(key), to_string(key)) Enum.reduce(bindings, text, fn {binding, value}, acc -> String.replace(acc, "%{#{binding}}", to_string(value)) 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) do Sidebar.view(project_id, view_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 Sidebar.view(nil, view_id, %{}) end def assistant_cards do [ %{label: "Offline Gate", text: "Automatic AI actions stay gated by airplane mode."}, %{label: "Filesystem Sync", text: "Metadata flush, diffing, and rebuild hooks still need editor wiring."}, %{label: "Desktop Runtime", text: "The app window is now served from LiveView state."} ] end def editor_meta(task_status) do [ %{label: "Status", value: task_status.running_task_message || "Idle"}, %{label: "Mode", value: "Offline"}, %{label: "Main Language", value: ui_language()} ] end def status_bar(workbench, task_status, dashboard) 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: ui_language(), 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 panel_tabs(workbench) do [:tasks, :output, :git_log, workbench.panel.active_tab] |> Enum.uniq() 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 dashboard_status_label(status) do case to_string(status) do "draft" -> translate("dashboard.status.draft") "published" -> translate("dashboard.status.published") "archived" -> translate("dashboard.status.archived") other -> other |> String.replace("_", " ") |> String.capitalize() end end def dashboard_post_count_label(count) do normalized_count = count || 0 key = if normalized_count == 1, do: "dashboard.postCount.one", else: "dashboard.postCount.other" translate(key, %{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" -> "Git Log" "post_links" -> "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 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