Initial Commit
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
defmodule RiverConnectWeb.AudioLive do
|
||||
use RiverConnectWeb, :live_view
|
||||
alias RiverConnect.Audio
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
if connected?(socket) do
|
||||
RiverConnectWeb.Endpoint.subscribe("audio:lobby")
|
||||
end
|
||||
|
||||
# Fetch initial messages
|
||||
messages = Audio.list_messages()
|
||||
|
||||
{:ok, assign(socket, messages: messages, recording: false, page_title: "Voxer Audio")}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("recording_started", %{"id" => _id}, socket) do
|
||||
{:noreply, assign(socket, recording: true)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("recording_stopped", _params, socket) do
|
||||
{:noreply, assign(socket, recording: false)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(%{event: "audio_message_created", payload: %{message: message}}, socket) do
|
||||
# Prepend new message
|
||||
messages = [message | socket.assigns.messages]
|
||||
{:noreply, assign(socket, messages: messages)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(%{event: "audio_message_ready", payload: %{id: _id}}, socket) do
|
||||
# Reload message to get updated status/path
|
||||
# In a real app we'd update just the one message, here we refresh all for simplicity
|
||||
messages = Audio.list_messages()
|
||||
{:noreply, assign(socket, messages: messages)}
|
||||
end
|
||||
|
||||
# Forward other broadcasts (like chunks) which are handled by JS, but if they come here we ignore
|
||||
@impl true
|
||||
def handle_info(_, socket), do: {:noreply, socket}
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="p-4 max-w-md mx-auto h-screen flex flex-col">
|
||||
<h1 class="text-2xl font-bold mb-4 text-center">Voxer Clone</h1>
|
||||
|
||||
<div class="flex-1 overflow-y-auto mb-4 space-y-4 pr-2">
|
||||
<h2 class="text-xl font-semibold sticky top-0 bg-white p-2 border-b">Recent Messages</h2>
|
||||
|
||||
<%= if Enum.empty?(@messages) do %>
|
||||
<p class="text-gray-500 text-center py-8">No messages yet. Start talking!</p>
|
||||
<% end %>
|
||||
|
||||
<%= for message <- @messages do %>
|
||||
<div class="bg-gray-50 p-3 rounded-lg shadow-sm border border-gray-200">
|
||||
<div class="flex justify-between text-xs text-gray-500 mb-2">
|
||||
<span class="font-mono truncate w-1/2" title={message.user_id}>{message.user_id}</span>
|
||||
<span>{Calendar.strftime(message.inserted_at, "%H:%M:%S")}</span>
|
||||
</div>
|
||||
|
||||
<%= if message.status == "ready" do %>
|
||||
<audio controls src={message.file_path} class="w-full h-8"></audio>
|
||||
<div class="text-right text-xs text-gray-400 mt-1">{message.duration_ms}ms</div>
|
||||
<% else %>
|
||||
<div class={[
|
||||
"flex items-center justify-center p-2 text-xs rounded animate-pulse",
|
||||
message.status == "recording" && "bg-red-50 text-red-700",
|
||||
message.status != "recording" && "bg-yellow-50 text-yellow-700"
|
||||
]}>
|
||||
<%= if message.status == "recording" do %>
|
||||
<span class="flex h-2 w-2 mr-2">
|
||||
<span class="animate-ping absolute inline-flex h-2 w-2 rounded-full bg-red-400 opacity-75">
|
||||
</span> <span class="relative inline-flex rounded-full h-2 w-2 bg-red-500"></span>
|
||||
</span>
|
||||
🔴 LIVE
|
||||
<button
|
||||
type="button"
|
||||
phx-click={Phoenix.LiveView.JS.dispatch("play_live", to: "#audio-recorder")}
|
||||
class="ml-3 bg-red-600 hover:bg-red-700 text-white px-2 py-1 rounded text-[10px] font-bold shadow-sm active:scale-95 transition-all"
|
||||
>
|
||||
▶ PLAY
|
||||
</button>
|
||||
<% else %>
|
||||
<svg class="animate-spin h-4 w-4 mr-2" viewBox="0 0 24 24">
|
||||
<circle
|
||||
class="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
>
|
||||
</circle>
|
||||
|
||||
<path
|
||||
class="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
>
|
||||
</path>
|
||||
</svg>
|
||||
Processing...
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="audio-recorder"
|
||||
phx-hook="AudioRecorder"
|
||||
class="pb-8 pt-4 border-t bg-gray-50 -mx-4 px-4 flex flex-col items-center justify-center"
|
||||
>
|
||||
<button
|
||||
id="record-btn"
|
||||
class={"w-24 h-24 rounded-full flex items-center justify-center text-white text-4xl shadow-lg transition-all transform active:scale-95 " <>
|
||||
if(@recording, do: "bg-red-600 animate-pulse ring-4 ring-red-200", else: "bg-blue-600 hover:bg-blue-700 hover:shadow-xl")}
|
||||
>
|
||||
<span class={
|
||||
if @recording,
|
||||
do:
|
||||
"animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75",
|
||||
else: "hidden"
|
||||
}>
|
||||
</span> {if @recording, do: "🎤", else: "🎙️"}
|
||||
</button>
|
||||
<p class={"mt-3 font-medium transition-colors " <> if(@recording, do: "text-red-600", else: "text-gray-600")}>
|
||||
{if @recording, do: "Click to finish", else: "Click to Talk"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user