You've already forked algora-tv
mirror of
https://github.com/algora-io/tv.git
synced 2025-11-23 21:34:58 +02:00
add subtitle crud (#2)
* init subs * fix styling issues in core components * update layout * show video on subtitles page * update spacing * list subtitles by video * misc * reorder routes * clean up module
This commit is contained in:
@@ -135,7 +135,7 @@ Hooks.VideoPlayer = {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener("js:play_video", ({ detail }) => {
|
const playVideo = ({ detail }) => {
|
||||||
const { player } = detail;
|
const { player } = detail;
|
||||||
this.player.options({
|
this.player.options({
|
||||||
techOrder: [player.type === "video/youtube" ? "youtube" : "html5"],
|
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.remove("hidden");
|
||||||
this.player.el().parentElement.classList.add("flex");
|
this.player.el().parentElement.classList.add("flex");
|
||||||
this.player.el().scrollIntoView();
|
this.player.el().scrollIntoView();
|
||||||
});
|
};
|
||||||
|
|
||||||
|
window.addEventListener("js:play_video", playVideo);
|
||||||
|
this.handleEvent("js:play_video", playVideo);
|
||||||
|
|
||||||
this.handleEvent("join_chat", Chat.join);
|
this.handleEvent("join_chat", Chat.join);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,9 +7,8 @@ defmodule Algora.Library do
|
|||||||
import Ecto.Query, warn: false
|
import Ecto.Query, warn: false
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
alias Algora.Accounts.User
|
alias Algora.Accounts.User
|
||||||
alias Algora.Storage
|
alias Algora.{Repo, Accounts, Storage}
|
||||||
alias Algora.{Repo, Accounts}
|
alias Algora.Library.{Channel, Video, Events, Subtitle}
|
||||||
alias Algora.Library.{Channel, Video, Events}
|
|
||||||
|
|
||||||
@pubsub Algora.PubSub
|
@pubsub Algora.PubSub
|
||||||
|
|
||||||
@@ -291,4 +290,31 @@ defmodule Algora.Library do
|
|||||||
defp topic(user_id) when is_integer(user_id), do: "channel:#{user_id}"
|
defp topic(user_id) when is_integer(user_id), do: "channel:#{user_id}"
|
||||||
|
|
||||||
def topic_livestreams(), do: "livestreams"
|
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
|
end
|
||||||
|
|||||||
21
lib/algora/library/subtitle.ex
Normal file
21
lib/algora/library/subtitle.ex
Normal file
@@ -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
|
||||||
92
lib/algora_web/live/subtitle_live/form_component.ex
Normal file
92
lib/algora_web/live/subtitle_live/form_component.ex
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
defmodule AlgoraWeb.SubtitleLive.FormComponent do
|
||||||
|
use AlgoraWeb, :live_component
|
||||||
|
|
||||||
|
alias Algora.Library
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def render(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div>
|
||||||
|
<.header class="pb-6">
|
||||||
|
<%= @title %>
|
||||||
|
<:subtitle>Use this form to manage subtitle records in your database.</:subtitle>
|
||||||
|
</.header>
|
||||||
|
|
||||||
|
<.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</.button>
|
||||||
|
</:actions>
|
||||||
|
</.simple_form>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
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
|
||||||
65
lib/algora_web/live/subtitle_live/index.ex
Normal file
65
lib/algora_web/live/subtitle_live/index.ex
Normal file
@@ -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
|
||||||
51
lib/algora_web/live/subtitle_live/index.html.heex
Normal file
51
lib/algora_web/live/subtitle_live/index.html.heex
Normal file
@@ -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</.button>
|
||||||
|
</.link>
|
||||||
|
</:actions>
|
||||||
|
</.header>
|
||||||
|
|
||||||
|
<.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>
|
||||||
|
<:col :let={{_id, subtitle}} label="End"><%= subtitle.end %></:col>
|
||||||
|
<:col :let={{_id, subtitle}} label="Body"><%= subtitle.body %></:col>
|
||||||
|
<:action :let={{_id, subtitle}}>
|
||||||
|
<div class="sr-only">
|
||||||
|
<.link navigate={~p"/videos/#{@video.id}/subtitles/#{subtitle}"}>Show</.link>
|
||||||
|
</div>
|
||||||
|
<.link patch={~p"/videos/#{@video.id}/subtitles/#{subtitle}/edit"}>Edit</.link>
|
||||||
|
</:action>
|
||||||
|
<:action :let={{id, subtitle}}>
|
||||||
|
<.link
|
||||||
|
phx-click={JS.push("delete", value: %{id: subtitle.id}) |> hide("##{id}")}
|
||||||
|
data-confirm="Are you sure?"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</.link>
|
||||||
|
</:action>
|
||||||
|
</.table>
|
||||||
|
|
||||||
|
<.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"}
|
||||||
|
/>
|
||||||
|
</.modal>
|
||||||
36
lib/algora_web/live/subtitle_live/show.ex
Normal file
36
lib/algora_web/live/subtitle_live/show.ex
Normal file
@@ -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
|
||||||
38
lib/algora_web/live/subtitle_live/show.html.heex
Normal file
38
lib/algora_web/live/subtitle_live/show.html.heex
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<div class="px-4 sm:px-6 lg:px-8">
|
||||||
|
<.header class="py-4 sm:py-6 lg:py-8">
|
||||||
|
Subtitle <%= @subtitle.id %>
|
||||||
|
<:subtitle>This is a subtitle record from your database.</:subtitle>
|
||||||
|
<:actions>
|
||||||
|
<.link
|
||||||
|
patch={~p"/videos/#{@video.id}/subtitles/#{@subtitle}/show/edit"}
|
||||||
|
phx-click={JS.push_focus()}
|
||||||
|
>
|
||||||
|
<.button>Edit subtitle</.button>
|
||||||
|
</.link>
|
||||||
|
</:actions>
|
||||||
|
</.header>
|
||||||
|
|
||||||
|
<.list>
|
||||||
|
<:item title="Body"><%= @subtitle.body %></:item>
|
||||||
|
<:item title="Start"><%= @subtitle.start %></:item>
|
||||||
|
<:item title="End"><%= @subtitle.end %></:item>
|
||||||
|
</.list>
|
||||||
|
|
||||||
|
<.back navigate={~p"/videos/#{@video.id}/subtitles"}>Back to subtitles</.back>
|
||||||
|
|
||||||
|
<.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}"}
|
||||||
|
/>
|
||||||
|
</.modal>
|
||||||
|
</div>
|
||||||
@@ -50,6 +50,12 @@ defmodule AlgoraWeb.Router do
|
|||||||
on_mount: [{AlgoraWeb.UserAuth, :ensure_authenticated}, AlgoraWeb.Nav] do
|
on_mount: [{AlgoraWeb.UserAuth, :ensure_authenticated}, AlgoraWeb.Nav] do
|
||||||
live "/channel/settings", SettingsLive, :edit
|
live "/channel/settings", SettingsLive, :edit
|
||||||
live "/:channel_handle/stream", ChannelLive, :stream
|
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
|
end
|
||||||
|
|
||||||
live_session :default, on_mount: [{AlgoraWeb.UserAuth, :current_user}, AlgoraWeb.Nav] do
|
live_session :default, on_mount: [{AlgoraWeb.UserAuth, :current_user}, AlgoraWeb.Nav] do
|
||||||
|
|||||||
16
priv/repo/migrations/20240304213317_create_subtitles.exs
Normal file
16
priv/repo/migrations/20240304213317_create_subtitles.exs
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user