Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/video player #112

Merged
merged 14 commits into from
Oct 2, 2023
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
213 changes: 110 additions & 103 deletions assets/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
})();
11 changes: 11 additions & 0 deletions assets/js/youtube/api.js
Original file line number Diff line number Diff line change
@@ -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)
})
}
66 changes: 66 additions & 0 deletions assets/js/youtube/hook.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import initPlayer from './player'

export default {
mounted() {
/**
* on_container_mounted
*
* Received when the player DOM has been mounted.
*/
this.handleEvent('on_container_mounted', ({container_id}) => {
console.debug(
'[Player :: on_container_mounted]', `container_id=${container_id}`)

const onPlayerReady = player => {
console.debug('[Player :: Ready]', player)
player.g.classList.add("rounded-lg")

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
}
29 changes: 29 additions & 0 deletions assets/js/youtube/player.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export default (container, onReady) => {
return new Promise((resolve) => {
new YT.Player(container, {
events: {
onError: (error) => {
console.error('[YT :: On Player Error] ', error)
},
onReady: async event => {
console.debug("[YT :: On Player Ready]")
await onReady(event.target)
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%"
})
})
}
15 changes: 12 additions & 3 deletions lib/livedj_web.ex
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,15 @@ defmodule LivedjWeb do
end
end

def live_view do
def live_view(opts \\ []) do
quote do
use Phoenix.LiveView,
layout: {LivedjWeb.Layouts, :app}
@opts Keyword.merge(
[
layout: {LivedjWeb.Layouts, :app}
],
unquote(opts)
)
use Phoenix.LiveView, @opts

unquote(html_helpers())
end
Expand Down Expand Up @@ -107,6 +112,10 @@ defmodule LivedjWeb do
@doc """
When used, dispatch to the appropriate controller/view/etc.
"""
defmacro __using__({which, opts}) when is_atom(which) do
apply(__MODULE__, which, [opts])
end

defmacro __using__(which) when is_atom(which) do
apply(__MODULE__, which, [])
end
Expand Down
2 changes: 1 addition & 1 deletion lib/livedj_web/components/core_components.ex
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ defmodule LivedjWeb.CoreComponents do
def add_media_form(assigns) do
~H"""
<.form :let={f} for={@for} as={@as} {@rest}>
<div class="space-y-8 bg-white w-full px-2">
<div class="space-y-8 bg-white w-full">
<div class="flex flex-row">
<div class="w-full">
<%= render_slot(@field, f) %>
Expand Down
6 changes: 6 additions & 0 deletions lib/livedj_web/components/layouts/flash.html.heex
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<main class="">
<div class="mx-1 sm:mx-auto max-w-xl">
<.flash_group flash={@flash} />
<%= @inner_content %>
</div>
</main>
Loading
Loading