diff --git a/assets/js/app.js b/assets/js/app.js
index 6f074e3..6abc8e1 100644
--- a/assets/js/app.js
+++ b/assets/js/app.js
@@ -135,7 +135,7 @@ Hooks.VideoPlayer = {
},
});
- window.addEventListener("js:play_video", ({ detail }) => {
+ const playVideo = ({ detail }) => {
const { player } = detail;
this.player.options({
techOrder: [player.type === "video/youtube" ? "youtube" : "html5"],
@@ -145,7 +145,10 @@ Hooks.VideoPlayer = {
this.player.el().parentElement.classList.remove("hidden");
this.player.el().parentElement.classList.add("flex");
this.player.el().scrollIntoView();
- });
+ };
+
+ window.addEventListener("js:play_video", playVideo);
+ this.handleEvent("js:play_video", playVideo);
this.handleEvent("join_chat", Chat.join);
},
diff --git a/lib/algora/library.ex b/lib/algora/library.ex
index 24ade1a..fbe4ff8 100644
--- a/lib/algora/library.ex
+++ b/lib/algora/library.ex
@@ -7,9 +7,8 @@ defmodule Algora.Library do
import Ecto.Query, warn: false
import Ecto.Changeset
alias Algora.Accounts.User
- alias Algora.Storage
- alias Algora.{Repo, Accounts}
- alias Algora.Library.{Channel, Video, Events}
+ alias Algora.{Repo, Accounts, Storage}
+ alias Algora.Library.{Channel, Video, Events, Subtitle}
@pubsub Algora.PubSub
@@ -291,4 +290,31 @@ defmodule Algora.Library do
defp topic(user_id) when is_integer(user_id), do: "channel:#{user_id}"
def topic_livestreams(), do: "livestreams"
+
+ def list_subtitles(%Video{} = video) do
+ from(s in Subtitle, where: s.video_id == ^video.id, order_by: [asc: s.start])
+ |> Repo.replica().all()
+ end
+
+ def get_subtitle!(id), do: Repo.get!(Subtitle, id)
+
+ def create_subtitle(%Video{} = video, attrs \\ %{}) do
+ %Subtitle{video_id: video.id}
+ |> Subtitle.changeset(attrs)
+ |> Repo.insert()
+ end
+
+ def update_subtitle(%Subtitle{} = subtitle, attrs) do
+ subtitle
+ |> Subtitle.changeset(attrs)
+ |> Repo.update()
+ end
+
+ def delete_subtitle(%Subtitle{} = subtitle) do
+ Repo.delete(subtitle)
+ end
+
+ def change_subtitle(%Subtitle{} = subtitle, attrs \\ %{}) do
+ Subtitle.changeset(subtitle, attrs)
+ end
end
diff --git a/lib/algora/library/subtitle.ex b/lib/algora/library/subtitle.ex
new file mode 100644
index 0000000..e0e62a9
--- /dev/null
+++ b/lib/algora/library/subtitle.ex
@@ -0,0 +1,21 @@
+defmodule Algora.Library.Subtitle do
+ alias Algora.Library
+ use Ecto.Schema
+ import Ecto.Changeset
+
+ schema "subtitles" do
+ field :start, :float
+ field :end, :float
+ field :body, :string
+ belongs_to :video, Library.Video
+
+ timestamps()
+ end
+
+ @doc false
+ def changeset(subtitle, attrs) do
+ subtitle
+ |> cast(attrs, [:body, :start, :end])
+ |> validate_required([:body, :start, :end])
+ end
+end
diff --git a/lib/algora_web/live/subtitle_live/form_component.ex b/lib/algora_web/live/subtitle_live/form_component.ex
new file mode 100644
index 0000000..82e2efc
--- /dev/null
+++ b/lib/algora_web/live/subtitle_live/form_component.ex
@@ -0,0 +1,92 @@
+defmodule AlgoraWeb.SubtitleLive.FormComponent do
+ use AlgoraWeb, :live_component
+
+ alias Algora.Library
+
+ @impl true
+ def render(assigns) do
+ ~H"""
+
+ <.header class="pb-6">
+ <%= @title %>
+ <:subtitle>Use this form to manage subtitle records in your database.
+
+
+ <.simple_form
+ for={@form}
+ id="subtitle-form"
+ phx-target={@myself}
+ phx-change="validate"
+ phx-submit="save"
+ >
+ <.input field={@form[:body]} type="text" label="Body" />
+ <.input field={@form[:start]} type="number" label="Start" step="any" />
+ <.input field={@form[:end]} type="number" label="End" step="any" />
+ <:actions>
+ <.button phx-disable-with="Saving...">Save Subtitle
+
+
+
+ """
+ end
+
+ @impl true
+ def update(%{subtitle: subtitle} = assigns, socket) do
+ changeset = Library.change_subtitle(subtitle)
+
+ {:ok,
+ socket
+ |> assign(assigns)
+ |> assign_form(changeset)}
+ end
+
+ @impl true
+ def handle_event("validate", %{"subtitle" => subtitle_params}, socket) do
+ changeset =
+ socket.assigns.subtitle
+ |> Library.change_subtitle(subtitle_params)
+ |> Map.put(:action, :validate)
+
+ {:noreply, assign_form(socket, changeset)}
+ end
+
+ def handle_event("save", %{"subtitle" => subtitle_params}, socket) do
+ save_subtitle(socket, socket.assigns.action, subtitle_params)
+ end
+
+ defp save_subtitle(socket, :edit, subtitle_params) do
+ case Library.update_subtitle(socket.assigns.subtitle, subtitle_params) do
+ {:ok, subtitle} ->
+ notify_parent({:saved, subtitle})
+
+ {:noreply,
+ socket
+ |> put_flash(:info, "Subtitle updated successfully")
+ |> push_patch(to: socket.assigns.patch)}
+
+ {:error, %Ecto.Changeset{} = changeset} ->
+ {:noreply, assign_form(socket, changeset)}
+ end
+ end
+
+ defp save_subtitle(socket, :new, subtitle_params) do
+ case Library.create_subtitle(socket.assigns.video, subtitle_params) do
+ {:ok, subtitle} ->
+ notify_parent({:saved, subtitle})
+
+ {:noreply,
+ socket
+ |> put_flash(:info, "Subtitle created successfully")
+ |> push_patch(to: socket.assigns.patch)}
+
+ {:error, %Ecto.Changeset{} = changeset} ->
+ {:noreply, assign_form(socket, changeset)}
+ end
+ end
+
+ defp assign_form(socket, %Ecto.Changeset{} = changeset) do
+ assign(socket, :form, to_form(changeset))
+ end
+
+ defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
+end
diff --git a/lib/algora_web/live/subtitle_live/index.ex b/lib/algora_web/live/subtitle_live/index.ex
new file mode 100644
index 0000000..cd6be29
--- /dev/null
+++ b/lib/algora_web/live/subtitle_live/index.ex
@@ -0,0 +1,65 @@
+defmodule AlgoraWeb.SubtitleLive.Index do
+ use AlgoraWeb, :live_view
+
+ alias Algora.Library
+ alias Algora.Library.Subtitle
+
+ @impl true
+ def mount(%{"video_id" => video_id}, _session, socket) do
+ video = Library.get_video!(video_id)
+
+ if connected?(socket), do: send(self(), :play_video)
+
+ {:ok,
+ socket
+ |> assign(:video, video)
+ |> stream(:subtitles, Library.list_subtitles(video))}
+ end
+
+ @impl true
+ def handle_params(params, _url, socket) do
+ {:noreply, apply_action(socket, socket.assigns.live_action, params)}
+ end
+
+ defp apply_action(socket, :edit, %{"id" => id}) do
+ socket
+ |> assign(:page_title, "Edit Subtitle")
+ |> assign(:subtitle, Library.get_subtitle!(id))
+ end
+
+ defp apply_action(socket, :new, _params) do
+ socket
+ |> assign(:page_title, "New Subtitle")
+ |> assign(:subtitle, %Subtitle{})
+ end
+
+ defp apply_action(socket, :index, _params) do
+ socket
+ |> assign(:page_title, "Listing Subtitles")
+ |> assign(:subtitle, nil)
+ end
+
+ @impl true
+ def handle_info(:play_video, socket) do
+ video = socket.assigns.video
+
+ {:noreply,
+ socket
+ |> push_event("js:play_video", %{
+ detail: %{player: %{src: video.url, type: Library.player_type(video)}}
+ })}
+ end
+
+ @impl true
+ def handle_info({AlgoraWeb.SubtitleLive.FormComponent, {:saved, subtitle}}, socket) do
+ {:noreply, stream_insert(socket, :subtitles, subtitle)}
+ end
+
+ @impl true
+ def handle_event("delete", %{"id" => id}, socket) do
+ subtitle = Library.get_subtitle!(id)
+ {:ok, _} = Library.delete_subtitle(subtitle)
+
+ {:noreply, stream_delete(socket, :subtitles, subtitle)}
+ end
+end
diff --git a/lib/algora_web/live/subtitle_live/index.html.heex b/lib/algora_web/live/subtitle_live/index.html.heex
new file mode 100644
index 0000000..aa31079
--- /dev/null
+++ b/lib/algora_web/live/subtitle_live/index.html.heex
@@ -0,0 +1,51 @@
+<.header class="p-4 sm:p-6 lg:p-8">
+ Subtitles
+ <:actions>
+ <.link patch={~p"/videos/#{@video.id}/subtitles/new"}>
+ <.button>New Subtitle
+
+
+
+
+<.table
+ id="subtitles"
+ rows={@streams.subtitles}
+ row_click={
+ fn {_id, subtitle} -> JS.navigate(~p"/videos/#{@video.id}/subtitles/#{subtitle}") end
+ }
+>
+ <:col :let={{_id, subtitle}} label="Start"><%= subtitle.start %>
+ <:col :let={{_id, subtitle}} label="End"><%= subtitle.end %>
+ <:col :let={{_id, subtitle}} label="Body"><%= subtitle.body %>
+ <:action :let={{_id, subtitle}}>
+
+ <.link navigate={~p"/videos/#{@video.id}/subtitles/#{subtitle}"}>Show
+
+ <.link patch={~p"/videos/#{@video.id}/subtitles/#{subtitle}/edit"}>Edit
+
+ <:action :let={{id, subtitle}}>
+ <.link
+ phx-click={JS.push("delete", value: %{id: subtitle.id}) |> hide("##{id}")}
+ data-confirm="Are you sure?"
+ >
+ Delete
+
+
+
+
+<.modal
+ :if={@live_action in [:new, :edit]}
+ id="subtitle-modal"
+ show
+ on_cancel={JS.navigate(~p"/videos/#{@video.id}/subtitles")}
+>
+ <.live_component
+ module={AlgoraWeb.SubtitleLive.FormComponent}
+ id={@subtitle.id || :new}
+ title={@page_title}
+ action={@live_action}
+ subtitle={@subtitle}
+ video={@video}
+ patch={~p"/videos/#{@video.id}/subtitles"}
+ />
+
diff --git a/lib/algora_web/live/subtitle_live/show.ex b/lib/algora_web/live/subtitle_live/show.ex
new file mode 100644
index 0000000..8151662
--- /dev/null
+++ b/lib/algora_web/live/subtitle_live/show.ex
@@ -0,0 +1,36 @@
+defmodule AlgoraWeb.SubtitleLive.Show do
+ use AlgoraWeb, :live_view
+
+ alias Algora.Library
+
+ @impl true
+ def mount(%{"video_id" => video_id}, _session, socket) do
+ video = Library.get_video!(video_id)
+
+ if connected?(socket), do: send(self(), :play_video)
+
+ {:ok, socket |> assign(:video, video)}
+ end
+
+ @impl true
+ def handle_info(:play_video, socket) do
+ video = socket.assigns.video
+
+ {:noreply,
+ socket
+ |> push_event("js:play_video", %{
+ detail: %{player: %{src: video.url, type: Library.player_type(video)}}
+ })}
+ end
+
+ @impl true
+ def handle_params(%{"id" => id}, _, socket) do
+ {:noreply,
+ socket
+ |> assign(:page_title, page_title(socket.assigns.live_action))
+ |> assign(:subtitle, Library.get_subtitle!(id))}
+ end
+
+ defp page_title(:show), do: "Show Subtitle"
+ defp page_title(:edit), do: "Edit Subtitle"
+end
diff --git a/lib/algora_web/live/subtitle_live/show.html.heex b/lib/algora_web/live/subtitle_live/show.html.heex
new file mode 100644
index 0000000..715cc13
--- /dev/null
+++ b/lib/algora_web/live/subtitle_live/show.html.heex
@@ -0,0 +1,38 @@
+
+ <.header class="py-4 sm:py-6 lg:py-8">
+ Subtitle <%= @subtitle.id %>
+ <:subtitle>This is a subtitle record from your database.
+ <:actions>
+ <.link
+ patch={~p"/videos/#{@video.id}/subtitles/#{@subtitle}/show/edit"}
+ phx-click={JS.push_focus()}
+ >
+ <.button>Edit subtitle
+
+
+
+
+ <.list>
+ <:item title="Body"><%= @subtitle.body %>
+ <:item title="Start"><%= @subtitle.start %>
+ <:item title="End"><%= @subtitle.end %>
+
+
+ <.back navigate={~p"/videos/#{@video.id}/subtitles"}>Back to subtitles
+
+ <.modal
+ :if={@live_action == :edit}
+ id="subtitle-modal"
+ show
+ on_cancel={JS.patch(~p"/videos/#{@video.id}/subtitles/#{@subtitle}")}
+ >
+ <.live_component
+ module={AlgoraWeb.SubtitleLive.FormComponent}
+ id={@subtitle.id}
+ title={@page_title}
+ action={@live_action}
+ subtitle={@subtitle}
+ patch={~p"/videos/#{@video.id}/subtitles/#{@subtitle}"}
+ />
+
+
diff --git a/lib/algora_web/router.ex b/lib/algora_web/router.ex
index d4546e6..14b759c 100644
--- a/lib/algora_web/router.ex
+++ b/lib/algora_web/router.ex
@@ -50,6 +50,12 @@ defmodule AlgoraWeb.Router do
on_mount: [{AlgoraWeb.UserAuth, :ensure_authenticated}, AlgoraWeb.Nav] do
live "/channel/settings", SettingsLive, :edit
live "/:channel_handle/stream", ChannelLive, :stream
+
+ live "/videos/:video_id/subtitles", SubtitleLive.Index, :index
+ live "/videos/:video_id/subtitles/new", SubtitleLive.Index, :new
+ live "/videos/:video_id/subtitles/:id", SubtitleLive.Show, :show
+ live "/videos/:video_id/subtitles/:id/edit", SubtitleLive.Index, :edit
+ live "/videos/:video_id/subtitles/:id/show/edit", SubtitleLive.Show, :edit
end
live_session :default, on_mount: [{AlgoraWeb.UserAuth, :current_user}, AlgoraWeb.Nav] do
diff --git a/priv/repo/migrations/20240304213317_create_subtitles.exs b/priv/repo/migrations/20240304213317_create_subtitles.exs
new file mode 100644
index 0000000..a2f1e49
--- /dev/null
+++ b/priv/repo/migrations/20240304213317_create_subtitles.exs
@@ -0,0 +1,16 @@
+defmodule Algora.Repo.Migrations.CreateSubtitles do
+ use Ecto.Migration
+
+ def change do
+ create table(:subtitles) do
+ add :body, :text
+ add :start, :float
+ add :end, :float
+ add :video_id, references(:videos, on_delete: :nothing), null: false
+
+ timestamps()
+ end
+
+ create index(:subtitles, [:video_id])
+ end
+end