From 0b89f95c2012577ca7c2f120a3d56494cd663e84 Mon Sep 17 00:00:00 2001 From: Santiago Botta Date: Sun, 24 Sep 2023 20:14:08 -0300 Subject: [PATCH 01/14] Implement a simple room list --- .../live/sessions/room_live/index.ex | 28 +++++++++++++++++++ .../live/sessions/room_live/index.html.heex | 20 +++++++++++++ lib/livedj_web/router.ex | 1 + 3 files changed, 49 insertions(+) create mode 100644 lib/livedj_web/live/sessions/room_live/index.ex create mode 100644 lib/livedj_web/live/sessions/room_live/index.html.heex diff --git a/lib/livedj_web/live/sessions/room_live/index.ex b/lib/livedj_web/live/sessions/room_live/index.ex new file mode 100644 index 0000000..858b303 --- /dev/null +++ b/lib/livedj_web/live/sessions/room_live/index.ex @@ -0,0 +1,28 @@ +defmodule LivedjWeb.Sessions.RoomLive.Index do + @moduledoc false + use LivedjWeb, :live_view + + alias Livedj.Sessions + + @impl true + def mount(_params, _session, socket) do + case connected?(socket) do + true -> + {:ok, stream(socket, :rooms, Sessions.list_rooms())} + + false -> + {:ok, socket} + end + 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_title, gettext("Listing Rooms")) + |> assign(:room, nil) + end +end diff --git a/lib/livedj_web/live/sessions/room_live/index.html.heex b/lib/livedj_web/live/sessions/room_live/index.html.heex new file mode 100644 index 0000000..dfcb8dc --- /dev/null +++ b/lib/livedj_web/live/sessions/room_live/index.html.heex @@ -0,0 +1,20 @@ +<%= if connected?(@socket) do %> + <.header> + <%= gettext("Rooms") %> + + + <.table + id="rooms" + rows={@streams.rooms} + row_click={fn {_id, room} -> JS.navigate(~p"/sessions/rooms/#{room}") end} + > + <:col :let={{_id, room}} label={gettext("Name")}><%= room.name %> + <:action :let={{_id, room}}> +
+ <.link navigate={~p"/sessions/rooms/#{room}"}> + <%= gettext("Show") %> + +
+ + +<% end %> diff --git a/lib/livedj_web/router.ex b/lib/livedj_web/router.ex index 40c0ec5..1a2b0c9 100644 --- a/lib/livedj_web/router.ex +++ b/lib/livedj_web/router.ex @@ -27,6 +27,7 @@ defmodule LivedjWeb.Router do on_mount: [{LivedjWeb.UserAuth, :mount_current_user}], root_layout: {LivedjWeb.Layouts, :root_session} do scope "/sessions", Sessions do + live "/rooms", RoomLive.Index, :index live "/rooms/:id", RoomLive.Show, :show end end From e6f725d95eb7339f00bfdd817fffbb031efabaf8 Mon Sep 17 00:00:00 2001 From: Santiago Botta Date: Sun, 24 Sep 2023 20:45:36 -0300 Subject: [PATCH 02/14] Implement a js player module --- assets/js/app.js | 213 +++++++++++++++++++----------------- assets/js/youtube/api.js | 11 ++ assets/js/youtube/hook.js | 12 ++ assets/js/youtube/player.js | 27 +++++ 4 files changed, 160 insertions(+), 103 deletions(-) create mode 100644 assets/js/youtube/api.js create mode 100644 assets/js/youtube/hook.js create mode 100644 assets/js/youtube/player.js diff --git a/assets/js/app.js b/assets/js/app.js index 2f2d09f..cc66db1 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -21,114 +21,121 @@ import "phoenix_html" import {LiveSocket} from "phoenix_live_view" import {Socket} from "phoenix" import Sortable from "../vendor/sortable" +import YoutubeAPI from './youtube/api' +import YoutubeHook from './youtube/hook' import topbar from "../vendor/topbar" -const Hooks = {} - -Hooks.Sortable = { - mounted() { - const noDropCursor = 'cursor-no-drop' - const grabCursor = 'cursor-grab' - const grabbingCursor = 'cursor-grabbing' - - const cancelledPointerHover = `hover:${noDropCursor}` - const grabbablePointerHover = `hover:${grabCursor}` - const grabbingPointerHover = `hover:${grabbingCursor}` - - const sorter = new Sortable(this.el, { - animation: 400, - delay: 20, - dragClass: "drag-item", - forceFallback: true, - ghostClass: "drag-ghost", - onEnd: ({item: item, newIndex: newIndex, oldIndex: oldIndex}) => { - sorter.el.classList.remove(cancelledPointerHover) - sorter.el.classList.remove(grabbingPointerHover) - Array.from(sorter.el.children).forEach(c => { - c.classList.add(grabbablePointerHover) - c.classList.remove(cancelledPointerHover) - }) - - const {dataRelatedInsertedAfter, dataRelatedId} = this - let params - - if ([ - (newIndex !== oldIndex), - (dataRelatedInsertedAfter !== undefined), - (dataRelatedId !== undefined) - ].every(c => c === true)) { - params = { - insertedAfter: dataRelatedInsertedAfter, - new: newIndex, - old: oldIndex, - relatedId: dataRelatedId, - status: "update", - ...item.dataset +// eslint-disable-next-line no-unexpected-multiline +(async () => { + await YoutubeAPI() + + const Hooks = {} + Hooks.Youtube = YoutubeHook + Hooks.Sortable = { + mounted() { + const noDropCursor = 'cursor-no-drop' + const grabCursor = 'cursor-grab' + const grabbingCursor = 'cursor-grabbing' + + const cancelledPointerHover = `hover:${noDropCursor}` + const grabbablePointerHover = `hover:${grabCursor}` + const grabbingPointerHover = `hover:${grabbingCursor}` + + const sorter = new Sortable(this.el, { + animation: 400, + delay: 20, + dragClass: "drag-item", + forceFallback: true, + ghostClass: "drag-ghost", + onEnd: ({item: item, newIndex: newIndex, oldIndex: oldIndex}) => { + sorter.el.classList.remove(cancelledPointerHover) + sorter.el.classList.remove(grabbingPointerHover) + Array.from(sorter.el.children).forEach(c => { + c.classList.add(grabbablePointerHover) + c.classList.remove(cancelledPointerHover) + }) + + const {dataRelatedInsertedAfter, dataRelatedId} = this + let params + + if ([ + (newIndex !== oldIndex), + (dataRelatedInsertedAfter !== undefined), + (dataRelatedId !== undefined) + ].every(c => c === true)) { + params = { + insertedAfter: dataRelatedInsertedAfter, + new: newIndex, + old: oldIndex, + relatedId: dataRelatedId, + status: "update", + ...item.dataset + } + + } else { + params = {status: "noop"} } - - } else { - params = {status: "noop"} + + this.pushEventTo(this.el, "reposition_end", params) + this.dataRelatedInsertedAfter = undefined + this.dataRelatedId = undefined + }, + onMove: event => { + this.dataRelatedId = event.related.id + this.dataRelatedInsertedAfter = event.willInsertAfter + }, + onStart: () => { + Array.from(sorter.el.children).forEach(c => { + c.classList.remove(grabbablePointerHover) + }) + sorter.el.classList.add(grabbingPointerHover) + + this.pushEventTo(this.el, "reposition_start") } - - this.pushEventTo(this.el, "reposition_end", params) - this.dataRelatedInsertedAfter = undefined - this.dataRelatedId = undefined - }, - onMove: event => { - this.dataRelatedId = event.related.id - this.dataRelatedInsertedAfter = event.willInsertAfter - }, - onStart: () => { + }) + + this.handleEvent('disable-drag', () => { + sorter.option("disabled", true) + }) + + this.handleEvent('enable-drag', () => { + sorter.option("disabled", false) + }) + + this.handleEvent('cancel-drag', () => { + sorter.el.classList.remove(grabbingPointerHover) Array.from(sorter.el.children).forEach(c => { - c.classList.remove(grabbablePointerHover) + c.classList.remove(grabbingPointerHover) + c.classList.remove(`drag-ghost:${grabbingCursor}`) + c.classList.add(`drag-ghost:${noDropCursor}`) + c.classList.add(cancelledPointerHover) }) - sorter.el.classList.add(grabbingPointerHover) - - this.pushEventTo(this.el, "reposition_start") - } - }) - - this.handleEvent('disable-drag', () => { - sorter.option("disabled", true) - }) - - this.handleEvent('enable-drag', () => { - sorter.option("disabled", false) - }) - - this.handleEvent('cancel-drag', () => { - sorter.el.classList.remove(grabbingPointerHover) - Array.from(sorter.el.children).forEach(c => { - c.classList.remove(grabbingPointerHover) - c.classList.remove(`drag-ghost:${grabbingCursor}`) - c.classList.add(`drag-ghost:${noDropCursor}`) - c.classList.add(cancelledPointerHover) + sorter.el.classList.add(cancelledPointerHover) }) - sorter.el.classList.add(cancelledPointerHover) - }) + } } -} - -const csrfToken = - document.querySelector("meta[name='csrf-token']") - .getAttribute("content") -const liveSocket = new LiveSocket("/live", Socket, { - hooks: Hooks, - params: {_csrf_token: csrfToken } -}) - -// Show progress bar on live navigation and form submits -topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"}) -window.addEventListener("phx:page-loading-start", () => topbar.show(300)) -window.addEventListener("phx:page-loading-stop", () => topbar.hide()) - -// connect if there are any LiveViews on the page -liveSocket.connect() - -// expose liveSocket on window for web console debug logs and latency -// simulation: -// >> liveSocket.enableDebug() -// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser -// session -// >> liveSocket.disableLatencySim() -window.liveSocket = liveSocket + + const csrfToken = + document.querySelector("meta[name='csrf-token']") + .getAttribute("content") + const liveSocket = new LiveSocket("/live", Socket, { + hooks: Hooks, + params: {_csrf_token: csrfToken } + }) + + // Show progress bar on live navigation and form submits + topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"}) + window.addEventListener("phx:page-loading-start", () => topbar.show(300)) + window.addEventListener("phx:page-loading-stop", () => topbar.hide()) + + // connect if there are any LiveViews on the page + liveSocket.connect() + + // expose liveSocket on window for web console debug logs and latency + // simulation: + // >> liveSocket.enableDebug() + // >> liveSocket.enableLatencySim(1000) // enabled for duration of browser + // session + // >> liveSocket.disableLatencySim() + window.liveSocket = liveSocket +})(); diff --git a/assets/js/youtube/api.js b/assets/js/youtube/api.js new file mode 100644 index 0000000..245f5d3 --- /dev/null +++ b/assets/js/youtube/api.js @@ -0,0 +1,11 @@ +export default () => { + return new Promise(resolve => { + window.onYouTubeIframeAPIReady = () => { + resolve() + console.debug("✅ Youtube API loaded!") + } + const youtubeScriptTag = document.createElement("script") + youtubeScriptTag.src = "//www.youtube.com/iframe_api" + document.head.appendChild(youtubeScriptTag) + }) +} diff --git a/assets/js/youtube/hook.js b/assets/js/youtube/hook.js new file mode 100644 index 0000000..fe584c5 --- /dev/null +++ b/assets/js/youtube/hook.js @@ -0,0 +1,12 @@ +import createPlayer from './player' + +export default { + mounted() { + setTimeout(async () => { + const playerContainer = document.getElementById("video-player") + await createPlayer(playerContainer) + + }, 1800) + + } +} diff --git a/assets/js/youtube/player.js b/assets/js/youtube/player.js new file mode 100644 index 0000000..4a63110 --- /dev/null +++ b/assets/js/youtube/player.js @@ -0,0 +1,27 @@ +export default (container) => { + return new Promise((resolve) => { + new YT.Player(container, { + events: { + onError: (error) => { + console.error('[YT] ', error) + }, + onReady: event => { + const player = event.target + resolve(player) + } + // onStateChange + }, + height: "100%", + playerVars: { + controls: 0, + disablekb: 1, + enablejsapi: 1, + iv_load_policy: 3, + rel: 0, + showinfo: 0 + }, + videoId: 'oCcks-fwq2c', + width: "100%" + }) + }) +} From e8220da170ea9e51e86517a8c3a1786c45bdb4b9 Mon Sep 17 00:00:00 2001 From: Santiago Botta Date: Sun, 24 Sep 2023 20:47:37 -0300 Subject: [PATCH 03/14] Slight style updates --- lib/livedj_web/components/list_component.ex | 4 ++-- lib/livedj_web/live/sessions/room_live/show.html.heex | 9 ++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/livedj_web/components/list_component.ex b/lib/livedj_web/components/list_component.ex index 47e6b2b..f560891 100644 --- a/lib/livedj_web/components/list_component.ex +++ b/lib/livedj_web/components/list_component.ex @@ -4,8 +4,8 @@ defmodule LivedjWeb.ListComponent do def render(assigns) do ~H""" -
-
+
+
-
-
+
+
-
+
<.live_component id={"playlist_#{@room.id}"} module={LivedjWeb.ListComponent} From 300843c772cf340ee551448e71819260da2f5524 Mon Sep 17 00:00:00 2001 From: Santiago Botta Date: Mon, 25 Sep 2023 02:08:48 -0300 Subject: [PATCH 04/14] Update .gitignore --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 8585eef..0adde1c 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,10 @@ livedj-*.tar # Ignore assets that are produced by build tools. /priv/static/assets/ +/priv/static/images/ +/priv/static/favicon-* +/priv/static/robots-* +/priv/static/*.gz # Ignore digested assets cache. /priv/static/cache_manifest.json From b5050acf44ea303c1b0f502f2eade789ab532f85 Mon Sep 17 00:00:00 2001 From: Santiago Botta Date: Mon, 25 Sep 2023 02:10:50 -0300 Subject: [PATCH 05/14] Update js operability and add play/pause functionality --- assets/js/youtube/hook.js | 64 +++++++++++++++++-- assets/js/youtube/player.js | 8 ++- .../components/layouts/session.html.heex | 6 ++ .../live/sessions/room_live/show.ex | 50 ++++++++++++++- .../live/sessions/room_live/show.html.heex | 28 +++++++- 5 files changed, 143 insertions(+), 13 deletions(-) diff --git a/assets/js/youtube/hook.js b/assets/js/youtube/hook.js index fe584c5..ee4739b 100644 --- a/assets/js/youtube/hook.js +++ b/assets/js/youtube/hook.js @@ -1,12 +1,64 @@ -import createPlayer from './player' +import initPlayer from './player' export default { mounted() { - setTimeout(async () => { - const playerContainer = document.getElementById("video-player") - await createPlayer(playerContainer) + /** + * on_container_mounted + * + * Received when the player DOM has been mounted. + */ + this.handleEvent('on_container_mounted', ({container_id}) => { + console.debug( + '[Player :: on_container_mounted]', `contaienr_id=${container_id}`) - }, 1800) + const onPlayerReady = player => { + console.debug('[Player :: Ready]', player) + this.player = player + this.pushEventTo(this.el, 'player_loaded') + } + + const playerContainer = document.getElementById(container_id) + initPlayer(playerContainer, onPlayerReady) + }) - } + /** + * show_player + * + * Received when the player is ready to be displayed + */ + this.handleEvent('show_player', ({loader_container_id}) => { + console.debug('[Player :: show_player]') + + this.player.g.classList.remove('hidden') + + const playerLoader = document.getElementById(loader_container_id) + playerLoader.classList.add('hidden', 'scale-0') + playerLoader.classList.remove('animate-ping') + + this.pushEventTo(this.el, 'player_visible') + }) + + /** + * play_video + * + * Received when the player should play the current track + */ + this.handleEvent('play_video', async () => { + console.debug('[Player :: play_video]') + await this.player.playVideo() + await this.pushEventTo(this.el, 'on_player_playing') + }) + + /** + * pause_video + * + * Received when the player should pause the current track + */ + this.handleEvent('pause_video', async () => { + console.debug('[Player :: pause_video]') + await this.player.pauseVideo() + await this.pushEventTo(this.el, 'on_player_paused') + }) + }, + player: null } diff --git a/assets/js/youtube/player.js b/assets/js/youtube/player.js index 4a63110..801bca7 100644 --- a/assets/js/youtube/player.js +++ b/assets/js/youtube/player.js @@ -1,11 +1,13 @@ -export default (container) => { +export default (container, onReady) => { return new Promise((resolve) => { new YT.Player(container, { events: { onError: (error) => { - console.error('[YT] ', error) + console.error('[YT :: On Player Error] ', error) }, - onReady: event => { + onReady: async event => { + console.debug("[YT :: On Player Ready]") + await onReady(event.target) const player = event.target resolve(player) } diff --git a/lib/livedj_web/components/layouts/session.html.heex b/lib/livedj_web/components/layouts/session.html.heex index 2d0d8fe..9376fad 100644 --- a/lib/livedj_web/components/layouts/session.html.heex +++ b/lib/livedj_web/components/layouts/session.html.heex @@ -60,6 +60,12 @@ <.icon name="hero-chevron-left-solid" class="h-7 w-7" /> + + <.icon + name={if @is_playing, do: "hero-pause", else: "hero-play"} + class="h-7 w-7" + /> + <.icon name="hero-chevron-right-solid" class="h-7 w-7" /> diff --git a/lib/livedj_web/live/sessions/room_live/show.ex b/lib/livedj_web/live/sessions/room_live/show.ex index 87cbdcd..a448aec 100644 --- a/lib/livedj_web/live/sessions/room_live/show.ex +++ b/lib/livedj_web/live/sessions/room_live/show.ex @@ -15,6 +15,9 @@ defmodule LivedjWeb.Sessions.RoomLive.Show do {:ok, assign(socket, + player_container_id: Ecto.UUID.generate(), + spinner_container_id: Ecto.UUID.generate(), + is_playing: false, drag_state: :unlocked, form: to_form(%{}), room: room, @@ -22,7 +25,7 @@ defmodule LivedjWeb.Sessions.RoomLive.Show do )} false -> - {:ok, socket} + {:ok, assign(socket, is_playing: false)} end rescue error in SessionRoomError -> @@ -106,6 +109,51 @@ defmodule LivedjWeb.Sessions.RoomLive.Show do {:noreply, socket} end + def handle_event("play", _params, socket) do + {:noreply, push_event(socket, "play_video", %{})} + end + + def handle_event("on_player_playing", _params, socket) do + {:noreply, assign(socket, is_playing: true)} + end + + def handle_event("pause", _params, socket) do + {:noreply, push_event(socket, "pause_video", %{})} + end + + def handle_event("on_player_paused", _params, socket) do + {:noreply, assign(socket, is_playing: false)} + end + + def handle_event("on_player_container_mount", _params, socket) do + socket = + if connected?(socket), + do: + push_event(socket, "on_container_mounted", %{ + container_id: "player-#{socket.assigns.player_container_id}" + }), + else: socket + + {:noreply, socket} + end + + def handle_event("player_loaded", _params, socket) do + socket = + if connected?(socket), + do: + push_event(socket, "show_player", %{ + loader_container_id: + "spinner-#{socket.assigns.spinner_container_id}" + }), + else: socket + + {:noreply, socket} + end + + def handle_event("player_visible", _params, socket) do + {:noreply, socket} + end + @impl true def handle_info( {:playlist_joined, room_id, payload}, diff --git a/lib/livedj_web/live/sessions/room_live/show.html.heex b/lib/livedj_web/live/sessions/room_live/show.html.heex index eb039dd..0338872 100644 --- a/lib/livedj_web/live/sessions/room_live/show.html.heex +++ b/lib/livedj_web/live/sessions/room_live/show.html.heex @@ -1,9 +1,31 @@ <%= if connected?(@socket) do %>
-
-
+
+