4.9 KiB · text History 9c0fc16
defmodule LambentExWeb.BrightnessLive.Index do
  use LambentExWeb, :live_view

  alias LambentEx.Brightness
  alias LambentEx.Utils.Brightness, as: B

  @pubsub_name LambentEx.PubSub

  @impl true
  def mount(_params, _session, socket) do
    Phoenix.PubSub.subscribe(@pubsub_name, "brightness")
    Phoenix.PubSub.subscribe(@pubsub_name, "scan-82667777")

    snap = Brightness.snapshot()

    {:ok,
     socket
     |> assign(:global, snap.global)
     |> assign(:places, snap.places)
     |> assign(:devices, %{})
     |> assign(:nonew, true)}
  end

  @impl true
  def handle_params(params, _url, socket) do
    {:noreply, apply_action(socket, socket.assigns.live_action, params)}
  end

  defp apply_action(socket, :index, _params) do
    socket
    |> assign(:page_link, "Brightness")
    |> assign(:page_title, "Brightness Clamps")
    |> assign(:source, nil)
  end

  @impl true
  def handle_info({:brightness_pub, %{global: global, places: places}}, socket) do
    {:noreply, socket |> assign(:global, global) |> assign(:places, places)}
  end

  @impl true
  def handle_info({:devices_pub, devices}, socket) do
    {:noreply, socket |> assign(:devices, socket.assigns.devices |> Map.merge(devices))}
  end

  @impl true
  def handle_info(_msg, socket), do: {:noreply, socket}

  @impl true
  def handle_event("brighter", %{"place" => place}, socket) do
    Brightness.brighter(place)
    {:noreply, socket}
  end

  @impl true
  def handle_event("brighter", _params, socket) do
    Brightness.brighter()
    {:noreply, socket}
  end

  @impl true
  def handle_event("dimmer", %{"place" => place}, socket) do
    Brightness.dimmer(place)
    {:noreply, socket}
  end

  @impl true
  def handle_event("dimmer", _params, socket) do
    Brightness.dimmer()
    {:noreply, socket}
  end

  # the slider runs over ladder positions, not raw 0..255, so it can only ever
  # land on one of the named brightness steps
  @impl true
  def handle_event("set", %{"place" => place, "rung" => rung}, socket) do
    Brightness.set_place(place, rung |> to_val() |> B.at())
    {:noreply, socket}
  end

  @impl true
  def handle_event("set", %{"rung" => rung}, socket) do
    Brightness.set_global(rung |> to_val() |> B.at())
    {:noreply, socket}
  end

  @impl true
  def handle_event("rate", %{"place" => place, "rate" => rate}, socket) do
    Brightness.set_place_rate(place, to_val(rate))
    {:noreply, socket}
  end

  @impl true
  def handle_event("rate", %{"rate" => rate}, socket) do
    Brightness.set_global_rate(to_val(rate))
    {:noreply, socket}
  end

  @impl true
  def handle_event("clear", %{"place" => place}, socket) do
    Brightness.clear_place(place)
    {:noreply, socket}
  end

  defp to_val(val) do
    case Integer.parse(val) do
      {v, _rest} -> v
      :error -> 0
    end
  end

  # the named fade durations, quickest first
  defp fades do
    B.fades() |> Enum.sort_by(fn {rate, _name} -> rate end)
  end

  # components

  attr :walk, :map, required: true
  attr :place, :string, default: nil

  @doc """
  The target on top, driven, and the walk's actual position underneath it,
  disabled -- the lower slider chases the upper one at the clamp's fade rate.

  Both run over ladder positions rather than raw brightness: the target snaps
  to a rung, and the rungs get even spacing they do not have in 0..255. The
  lower slider uses a hundred sub-steps per rung so a walk between two of them
  moves smoothly and still lines up with the handle above it.
  """
  def clamp_sliders(assigns) do
    ~H"""
    <form phx-change="set">
      <input :if={@place} type="hidden" name="place" value={@place} />
      <input
        type="range"
        name="rung"
        min="0"
        max={B.rungs() - 1}
        step="1"
        value={B.index(@walk.tgt)}
        phx-debounce="100"
        class="range range-sm"
      />
    </form>
    <input
      type="range"
      min="0"
      max={(B.rungs() - 1) * 100}
      value={(B.fraction(@walk.curr) * 100) |> round}
      disabled
      class="range range-xs opacity-60"
    />
    <div class="text-sm opacity-60">
      <%= @walk.curr |> pct %>&percnt;
      <%= if @walk.curr != @walk.tgt do %>
        <i class="fa-solid fa-fw fa-caret-right"></i>
      <% end %>
      <span class="badge badge-sm"><%= B.name(@walk.tgt) || @walk.tgt %></span>
    </div>
    """
  end

  # functions

  # every place a device claims, plus any that carry a clamp but have no device
  # answering right now
  defp places(devices, clamped) do
    devices
    |> Map.values()
    |> Enum.map(fn d -> d["place"] end)
    |> Enum.concat(Map.keys(clamped))
    |> Enum.reject(&is_nil/1)
    |> Enum.uniq()
    |> Enum.sort()
  end

  defp walk(clamped, place) do
    Map.get(clamped, place, %{tgt: B.full(), curr: B.full(), rate: B.default_fade()})
  end

  defp devices_in(devices, place) do
    devices |> Map.values() |> Enum.count(fn d -> d["place"] == place end)
  end

  defp pct(val), do: B.pct(val)
end