defmodule CobblemonUiWeb.DashboardLive do use CobblemonUiWeb, :live_view import CobblemonUiWeb.PokemonComponents import CobblemonUiWeb.BattleComponents @impl true def mount(_params, _session, socket) do if connected?(socket) do :timer.send_interval(1000, self(), :tick) end players = case CobblemonUi.CobblemonFS.list_players() do {:ok, list} -> list end {:ok, assign(socket, page_title: "Cobblemon Dashboard", players: players, selected_player: nil, player_data: nil, battle: nil, selected_pokemon: nil, tier_list: CobblemonUi.TierListScraper.get_tier_list(), species_info: %{}, view_mode: :party, loading: false, error: nil )} end @impl true def handle_params(%{"uuid" => uuid}, _uri, socket) do case CobblemonUi.CobblemonFS.get_player(uuid) do {:ok, data} -> battle = find_player_battle(uuid) socket = assign(socket, selected_player: uuid, player_data: data, battle: battle, selected_pokemon: nil, species_info: %{}, error: nil ) if connected?(socket), do: send(self(), {:load_species_info, data}) {:noreply, socket} {:error, :not_found} -> {:noreply, assign(socket, selected_player: uuid, player_data: nil, error: "Player not found" )} {:error, reason} -> {:noreply, assign(socket, selected_player: uuid, player_data: nil, error: "Error loading player: #{inspect(reason)}" )} end end def handle_params(_params, _uri, socket) do {:noreply, assign(socket, selected_player: nil, player_data: nil, battle: nil, selected_pokemon: nil)} end @impl true def handle_event("select_pokemon", %{"index" => index_str}, socket) do index = String.to_integer(index_str) pokemon = case socket.assigns.view_mode do :party -> Enum.at(socket.assigns.player_data.party, index) :pc -> socket.assigns.player_data.pc |> Enum.flat_map(fn box -> box.pokemon end) |> Enum.at(index) end {:noreply, assign(socket, selected_pokemon: pokemon)} end def handle_event("close_pokemon", _params, socket) do {:noreply, assign(socket, selected_pokemon: nil)} end def handle_event("switch_view", %{"mode" => mode}, socket) do {:noreply, assign(socket, view_mode: String.to_existing_atom(mode), selected_pokemon: nil)} end @impl true def handle_info(:tick, socket) do {:noreply, do_refresh(socket)} end def handle_info({:load_species_info, player_data}, socket) do species_info = compute_species_info(player_data, socket.assigns.tier_list) {:noreply, assign(socket, species_info: species_info)} end defp do_refresh(socket) do players = case CobblemonUi.CobblemonFS.list_players() do {:ok, list} -> list end socket = if uuid = socket.assigns.selected_player do battle = find_player_battle(uuid) case CobblemonUi.CobblemonFS.get_player(uuid) do {:ok, data} -> assign(socket, player_data: data, battle: battle, error: nil) _ -> assign(socket, battle: battle) end else socket end assign(socket, players: players) end @impl true def render(assigns) do ~H"""
<%!-- Player picker --%>
<.icon name="hero-cube-transparent" class="size-6 text-primary" />

Cobblemon

Who are you?

<.icon name="hero-user-group" class="size-10 mx-auto mb-4 text-base-content/20" />

No players found

Check the data directory

<.link :for={player <- @players} patch={~p"/player/#{player.uuid}"} id={"player-#{player.uuid}"} class="group flex items-center gap-3 rounded-xl border border-base-300/40 bg-base-200/20 px-5 py-4 hover:bg-base-200/50 hover:border-primary/30 hover:shadow-md hover:shadow-primary/5 transition-all duration-150" >
<.icon name="hero-user" class="size-5 text-primary/70" />
{player.name || "Unknown"} <.icon name="hero-chevron-right" class="size-4 text-base-content/20 ml-auto shrink-0 group-hover:text-primary/40 transition-colors" />
<%!-- Player view --%>
<%!-- Header --%>
<.link patch={~p"/"} id="back-to-players" class="w-10 h-10 rounded-xl bg-base-200/50 hover:bg-base-200 flex items-center justify-center transition-colors" > <.icon name="hero-arrow-left" class="size-5 text-base-content/50" />

{player_name(@players, @selected_player)}

Player Data Explorer

<%!-- Scrollable content --%>
<.icon name="hero-exclamation-triangle" class="size-5" /> {@error}
<%!-- Stats bar --%>
{party_count(@player_data)} in party
{pc_count(@player_data)} in PC
<%!-- Active battle --%> <.battle_panel :if={@battle} battle={@battle} player_id={@selected_player} tier_list={@tier_list} evolutions={opponent_evolutions(@battle, @selected_player)} /> <%!-- View mode tabs --%>
<%!-- Party view --%>
<%= for {pokemon, idx} <- Enum.with_index(@player_data.party), not is_nil(pokemon) do %> <.pokemon_card pokemon={pokemon} index={idx} tier={Map.get(@tier_list, String.downcase(pokemon.species || ""), nil)} /> <% end %>
<%!-- PC view --%>

Box {box.box + 1}

<%= for {pokemon, idx} <- Enum.with_index(box.pokemon), not is_nil(pokemon) do %> <% skey = String.downcase(pokemon.species || "") %> <% info = Map.get(@species_info, skey, %{}) %> <.pc_pokemon_card pokemon={pokemon} index={pc_global_index(@player_data.pc, box.box, idx)} tier={Map.get(@tier_list, skey, nil)} types={Map.get(info, :types, [])} evo_tiers={Map.get(info, :evo_tiers, [])} /> <% end %>
PC storage is empty
<%!-- Pokemon detail panel --%> <.pokemon_detail :if={@selected_pokemon} pokemon={@selected_pokemon} tier={Map.get(@tier_list, String.downcase(@selected_pokemon.species || ""), nil)} /> <%!-- Type Chart --%> <.type_chart />
""" end # --- Helpers --- defp party_count(%{party: party}), do: Enum.count(party, &(not is_nil(&1))) defp pc_count(%{pc: pc}), do: Enum.sum(Enum.map(pc, fn b -> Enum.count(b.pokemon, &(not is_nil(&1))) end)) defp pc_global_index(boxes, current_box, slot_index) do offset = boxes |> Enum.filter(fn b -> b.box < current_box end) |> Enum.sum_by(fn b -> length(b.pokemon) end) offset + slot_index end defp player_name(players, uuid) do case Enum.find(players, fn p -> p.uuid == uuid end) do %{name: name} when is_binary(name) -> name _ -> "Unknown" end end defp compute_species_info(player_data, tier_list) do all_pokemon = (player_data.party ++ Enum.flat_map(player_data.pc, & &1.pokemon)) |> Enum.reject(&is_nil/1) unique_species = all_pokemon |> Enum.map(&String.downcase(&1.species || "")) |> Enum.reject(&(&1 == "")) |> Enum.uniq() unique_species |> Task.async_stream( fn species -> types = CobblemonUi.PokeApi.get_types(species) evolutions = CobblemonUi.EvolutionApi.get_evolutions(species) # Pre-download sprites for this species and its evolutions CobblemonUi.SpriteCache.ensure_sprite(species) Enum.each(evolutions, &CobblemonUi.SpriteCache.ensure_sprite/1) evo_tiers = Enum.map(evolutions, fn evo -> %{species: evo, tier: Map.get(tier_list, evo)} end) {species, %{types: types, evo_tiers: evo_tiers}} end, max_concurrency: 10, timeout: :infinity ) |> Enum.reduce(%{}, fn {:ok, {species, info}}, acc -> Map.put(acc, species, info) end) end defp opponent_evolutions(nil, _player_id), do: %{} defp opponent_evolutions(battle, player_id) do battle.actors |> Enum.reject(fn actor -> actor.player_id == player_id end) |> Enum.flat_map(fn actor -> actor.active_pokemon end) |> Enum.reduce(%{}, fn poke, acc -> species = String.downcase(poke.species || "") if species != "" do evolutions = CobblemonUi.EvolutionApi.get_evolutions(species) Map.put(acc, species, evolutions) else acc end end) end defp find_player_battle(uuid) do case CobblemonUi.BattlesApi.list_battles() do {:ok, battles} -> Enum.find(battles, fn battle -> Enum.any?(battle.actors, fn actor -> actor.player_id == uuid end) end) _ -> nil end end end