mirror of
https://github.com/algora-io/tv.git
synced 2025-01-05 01:20:24 +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:
parent
31c6c1adcf
commit
1b10930700
@ -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);
|
||||
},
|
||||
|
@ -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
|
||||
|
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
|
||||
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
|
||||
|
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
|
Loading…
Reference in New Issue
Block a user