defmodule BDS.Desktop.ShellLive.OverlayComponents do @moduledoc false use Phoenix.Component import Ecto.Query alias BDS.Desktop.ShellData alias BDS.{I18n, Metadata, Repo} alias BDS.Media.Media alias BDS.Media.Translation, as: MediaTranslation alias BDS.Posts.{Post, PostMedia, Translation} alias BDS.Tags.Tag embed_templates "overlay_html/*" def context(assigns, tab_title, tab_subtitle) do project_id = assigns.projects.active_project_id metadata = project_metadata(project_id) current_tab = assigns.current_tab page_language = assigns.page_language posts = posts(project_id) media = media(project_id) %{ current_tab: %{type: current_tab.type, id: current_tab.id, title: tab_title, subtitle: tab_subtitle}, current_post_language: source_language(current_tab, metadata), current_media_language: source_language(current_tab, metadata), posts: posts, media: media, post_media_ids: post_media_ids(current_tab), blog_languages: blog_languages(metadata), language_names: language_names(), language_flags: language_flags(), existing_translations: existing_translations(current_tab), ai_title: ShellData.translate("AI Suggestions", %{}, page_language), insert_link_title: ShellData.translate("Insert Link", %{}, page_language), insert_media_title: ShellData.translate("Insert Media", %{}, page_language), language_picker_title: ShellData.translate("Translate", %{}, page_language), gallery_title: tab_title, ai_fields: ai_fields(current_tab, tab_title, tab_subtitle, page_language), delete_details: delete_details(current_tab, page_language), merge_details: merge_details(project_id, page_language) } end def kind("ai_suggestions"), do: :ai_suggestions def kind("insert_link"), do: :insert_link def kind("insert_media"), do: :insert_media def kind("language_picker"), do: :language_picker def kind("confirm_delete"), do: :confirm_delete def kind("confirm_merge"), do: :confirm_merge def kind("gallery"), do: :gallery def kind(_kind), do: nil def tab("internal"), do: :internal def tab("external"), do: :external def tab(_tab), do: :internal def markdown_link(text, url), do: "[#{text}](#{url})" def translated(text, bindings \\ %{}), do: ShellData.translate(text, bindings, Process.get(:bds_ui_locale)) def project_metadata(nil), do: %{main_language: "en", blog_languages: []} def project_metadata(project_id) do {:ok, metadata} = Metadata.get_project_metadata(project_id) metadata rescue _error -> %{main_language: "en", blog_languages: []} end defp posts(nil), do: [] defp posts(project_id) do Repo.all( from post in Post, where: post.project_id == ^project_id, order_by: [desc: post.updated_at, desc: post.created_at], select: %{id: post.id, title: post.title, slug: post.slug, status: post.status, published_at: post.published_at, updated_at: post.updated_at, language: post.language} ) |> Enum.map(fn post -> %{ id: post.id, title: post.title || post.slug || post.id, status: Atom.to_string(post.status || :draft), canonical_url: canonical_post_url(post) } end) end defp media(nil), do: [] defp media(project_id) do Repo.all( from media in Media, where: media.project_id == ^project_id, order_by: [desc: media.updated_at, desc: media.created_at], select: %{id: media.id, title: media.title, original_name: media.original_name, mime_type: media.mime_type, alt: media.alt, caption: media.caption} ) |> Enum.map(fn media -> %{ id: media.id, title: media.title || media.original_name || media.id, original_name: media.original_name || media.id, is_image: String.starts_with?(to_string(media.mime_type || ""), "image/"), thumbnail_url: "/media-thumbnail/#{media.id}", image_url: "/media-thumbnail/#{media.id}?size=large", alt_text: media.alt || media.caption || media.title } end) end defp post_media_ids(%{type: :post, id: post_id}) do Repo.all( from pm in PostMedia, where: pm.post_id == ^post_id, order_by: [asc: pm.sort_order, asc: pm.media_id], select: pm.media_id ) rescue _error -> [] end defp post_media_ids(_tab), do: [] defp existing_translations(%{type: :post, id: post_id}) do Repo.all( from translation in Translation, where: translation.translation_for == ^post_id, select: {translation.language, translation.status} ) |> Map.new(fn {language, status} -> {language, Atom.to_string(status || :draft)} end) rescue _error -> %{} end defp existing_translations(%{type: :media, id: media_id}) do Repo.all( from translation in MediaTranslation, where: translation.translation_for == ^media_id, select: {translation.language, "draft"} ) |> Map.new(fn {language, status} -> {language, status} end) rescue _error -> %{} end defp existing_translations(_tab), do: %{} defp blog_languages(metadata) do ([metadata.main_language || "en"] ++ (metadata.blog_languages || []) ++ Enum.map(I18n.supported_languages(), & &1.code)) |> Enum.reject(&is_nil/1) |> Enum.uniq() end defp source_language(%{type: :post, id: post_id}, metadata) do case Repo.get(Post, post_id) do %Post{language: language} when is_binary(language) and language != "" -> language _other -> metadata.main_language || "en" end rescue _error -> metadata.main_language || "en" end defp source_language(%{type: :media, id: media_id}, metadata) do case Repo.get(Media, media_id) do %Media{language: language} when is_binary(language) and language != "" -> language _other -> metadata.main_language || "en" end rescue _error -> metadata.main_language || "en" end defp source_language(_tab, metadata), do: metadata.main_language || "en" defp language_names do %{ "en" => "English", "de" => "Deutsch", "fr" => "Francais", "it" => "Italiano", "es" => "Espanol" } end defp language_flags do I18n.supported_languages() |> Enum.into(%{}, fn language -> {language.code, I18n.flag(language.code)} end) end defp ai_fields(%{type: :post, id: post_id}, title, subtitle, page_language) do case Repo.get(Post, post_id) do %Post{} = post -> [ %{key: "title", label: ShellData.translate("Title", %{}, page_language), current_value: post.title || title, suggested_value: refine_title(post.title || title), locked: false}, %{key: "excerpt", label: ShellData.translate("Excerpt", %{}, page_language), current_value: post.excerpt || subtitle, suggested_value: refine_excerpt(post.title || title, post.excerpt || subtitle), locked: false}, %{key: "slug", label: ShellData.translate("Slug", %{}, page_language), current_value: post.slug || slugify(post.title || title), suggested_value: refine_slug(post.slug || slugify(post.title || title)), locked: post.status == :published} ] _other -> [] end rescue _error -> [] end defp ai_fields(%{type: :media, id: media_id}, title, _subtitle, page_language) do case Repo.get(Media, media_id) do %Media{} = media -> [ %{key: "title", label: ShellData.translate("Title", %{}, page_language), current_value: media.title || title, suggested_value: refine_title(media.title || title), locked: false}, %{key: "alt", label: ShellData.translate("Alt Text", %{}, page_language), current_value: media.alt || "", suggested_value: media.alt || title, locked: false}, %{key: "caption", label: ShellData.translate("Caption", %{}, page_language), current_value: media.caption || "", suggested_value: refine_excerpt(title, media.caption || title), locked: false} ] _other -> [] end rescue _error -> [] end defp ai_fields(_tab, _title, _subtitle, _page_language), do: [] defp delete_details(%{type: :media, id: media_id}, page_language) do entity_name = case Repo.get(Media, media_id) do %Media{} = media -> media.title || media.original_name || media.id _other -> media_id end reference_list = Repo.all( from post in Post, join: pm in PostMedia, on: pm.post_id == post.id, where: pm.media_id == ^media_id, order_by: [asc: pm.sort_order, desc: post.updated_at], select: post.title ) |> Enum.map(&(&1 || media_id)) %{ title: ShellData.translate("Delete Media", %{}, page_language), entity_name: entity_name, entity_type: "media", reference_list: reference_list } rescue _error -> %{title: ShellData.translate("Delete Media", %{}, page_language), entity_name: media_id, entity_type: "media", reference_list: []} end defp delete_details(%{type: :tags}, page_language) do tag_name = Repo.one(from tag in Tag, order_by: [asc: tag.name], limit: 1, select: tag.name) |> Kernel.||("tag") %{ title: ShellData.translate("Delete Tag", %{}, page_language), entity_name: tag_name, entity_type: "tag", reference_list: [] } rescue _error -> %{title: ShellData.translate("Delete Tag", %{}, page_language), entity_name: "tag", entity_type: "tag", reference_list: []} end defp delete_details(_tab, page_language) do %{title: ShellData.translate("Delete", %{}, page_language), entity_name: "", entity_type: "item", reference_list: []} end defp merge_details(project_id, page_language) do tags = Repo.all(from tag in Tag, where: tag.project_id == ^project_id, order_by: [asc: tag.name], limit: 3, select: tag.name) target = List.first(tags) || "tag" %{ target: target, count: max(length(tags), 1), title: ShellData.translate("Merge Tags", %{}, page_language), message: ShellData.translate("Cannot be undone.", %{}, page_language) } rescue _error -> %{target: "tag", count: 1, title: ShellData.translate("Merge Tags", %{}, page_language), message: ShellData.translate("Cannot be undone.", %{}, page_language)} end defp canonical_post_url(post) do timestamp = post.published_at || post.updated_at || System.system_time(:millisecond) date = DateTime.from_unix!(timestamp, :millisecond) "/#{date.year}/#{pad2(date.month)}/#{pad2(date.day)}/#{post.slug || post.id}" end defp pad2(value), do: value |> Integer.to_string() |> String.pad_leading(2, "0") defp refine_title(nil), do: "" defp refine_title(title), do: String.trim(title <> " Notes") defp refine_excerpt(title, excerpt) do base = excerpt |> to_string() |> String.trim() if base == "", do: "#{title} overview", else: base <> "." end defp refine_slug(slug), do: slug |> to_string() |> String.trim_trailing("-") |> Kernel.<>("-updated") defp slugify(value) do value |> to_string() |> String.downcase() |> String.replace(~r/[^a-z0-9]+/u, "-") |> String.trim("-") end end