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/add videos update #118

Merged
merged 16 commits into from
Jan 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 1 addition & 90 deletions assets/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,11 @@

// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
import "phoenix_html"
import Hooks from './hooks'
// Establish Phoenix Socket and LiveView configuration.
import {LiveSocket} from "phoenix_live_view"
import {Socket} from "phoenix"
import Sortable from "../vendor/sortable"
import ThemeHook from './theme/hook'
import YoutubeAPI from './youtube/api'
import YoutubeHook from './youtube/hook'
import topbar from "../vendor/topbar"

// On page load or when changing themes, best to add inline in `head` to avoid
Expand All @@ -44,93 +42,6 @@ if (
// eslint-disable-next-line no-unexpected-multiline
(async () => {
await YoutubeAPI()

const Hooks = {}
Hooks.Theme = ThemeHook
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: 300,
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"}
}

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.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)
})
}
}

const csrfToken =
document.querySelector("meta[name='csrf-token']")
Expand Down
11 changes: 11 additions & 0 deletions assets/js/hooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import SearchBar from './search-bar/hook'
import Sortable from './sortable/hook'
import Theme from './theme/hook'
import Youtube from './youtube/hook'

export default {
SearchBar,
Sortable,
Theme,
Youtube
}
38 changes: 38 additions & 0 deletions assets/js/search-bar/hook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// This is optional phoenix client hook. It allows to use key down and up to select results.

export default {
mounted() {
const searchBarContainer = (this as any).el as HTMLDivElement
document.addEventListener('keydown', (event) => {
if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') {
return
}

const focusElemnt = document.querySelector(':focus') as HTMLElement

if (!focusElemnt) {
return
}

if (!searchBarContainer.contains(focusElemnt)) {
return
}

event.preventDefault()

const tabElements = document.querySelectorAll(
'#search-input, #searchbox__results_list a',
) as NodeListOf<HTMLElement>
const focusIndex = Array.from(tabElements).indexOf(focusElemnt)
const tabElementsCount = tabElements.length - 1

if (event.key === 'ArrowUp') {
tabElements[focusIndex > 0 ? focusIndex - 1 : tabElementsCount].focus()
}

if (event.key === 'ArrowDown') {
tabElements[focusIndex < tabElementsCount ? focusIndex + 1 : 0].focus()
}
})
},
}
85 changes: 85 additions & 0 deletions assets/js/sortable/hook.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import Sortable from '../../vendor/sortable'

export default {
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: 300,
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"}
}

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.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)
})
}
}
27 changes: 27 additions & 0 deletions lib/livedj/media.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ defmodule Livedj.Media do

require Logger

@type tubex_video :: %Tubex.Video{}

@doc """
Given a metadata reponse returns a success tuple when the result contains
media metadata.
Expand All @@ -33,6 +35,31 @@ defmodule Livedj.Media do
end
end

@doc """
Given a search reponse returns a success tuple when the result contains
search results.
"""
@spec from_tubex_search([tubex_video()]) :: [map()]
def from_tubex_search(media_search) do
Enum.map(media_search, fn %Tubex.Video{
etag: etag,
video_id: external_id,
published_at: published_at,
thumbnails: %{"high" => %{"url" => url}},
title: title,
channel_title: channel
} ->
%{
etag: etag,
external_id: external_id,
published_at: published_at,
thumbnail_url: url,
title: HtmlEntities.decode(title),
channel: HtmlEntities.decode(channel)
}
end)
end

@doc """
Returns the list of videos.

Expand Down
1 change: 0 additions & 1 deletion lib/livedj/media/video.ex
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ defmodule Livedj.Media.Video do
:title,
:thumbnail_url,
:external_id,
:etag,
:published_at
])
|> unique_constraint(:external_id,
Expand Down
33 changes: 33 additions & 0 deletions lib/livedj/sessions.ex
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,39 @@ defmodule Livedj.Sessions do
end
end

@doc """
Given a query, searches for videos through the Tubex api.
"""
@spec search_by_query(String.t()) ::
{:ok, [Media.Video.t()]} | {:error, :service_unavailable}
def search_by_query(query) do
opts = [maxResults: 20]

case Tubex.Video.search_by_query(query, opts) do
{:ok, search_result, _pag_opts} ->
with medias <- Media.from_tubex_search(search_result),
:ok <- create_videos(medias) do
{:ok, medias}
end

{:error, %{"error" => %{"errors" => errors}}} ->
for error <- errors do
Logger.error(error["message"])
end

{:error, :service_unavailable}
end
end

@spec create_videos([Media.Video.t()]) :: :ok
defp create_videos(medias) do
Enum.each(medias, fn media ->
# TODO: handle constraints to the external_id to update those entities,
# and cache the result.
Media.create_video(media)
end)
end

# ----------------------------------------------------------------------------
# Redis management
#
Expand Down
4 changes: 3 additions & 1 deletion lib/livedj_web/components/core_components.ex
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,8 @@ defmodule LivedjWeb.CoreComponents do
default: false,
doc: "the multiple flag for select inputs"

attr :container_class, :string, default: ""

attr :rest, :global,
include:
~w(accept autocomplete capture cols disabled form list max maxlength min minlength
Expand Down Expand Up @@ -482,7 +484,7 @@ defmodule LivedjWeb.CoreComponents do
# here...
def input(assigns) do
~H"""
<div phx-feedback-for={@name}>
<div phx-feedback-for={@name} class={@container_class}>
<.label for={@id}><%= @label %></.label>
<input
type={@type}
Expand Down
2 changes: 1 addition & 1 deletion lib/livedj_web/components/layouts/session.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<div class="flex items-center border-b border-zinc-300 dark:border-zinc-700 py-3 text-sm justify-between">
<div class="h-5 w-5" />
<div class="flex items-center gap-4">
<a href="/">
<a href="/" data-confirm={gettext("Exit session?")}>
<.livedj_logo theme={@theme} />
</a>
</div>
Expand Down
7 changes: 4 additions & 3 deletions lib/livedj_web/components/list_component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,9 @@ defmodule LivedjWeb.ListComponent do
</div>
<div class="absolute top-6 -left-1 h-4 w-4 rounded-full">
<p class={"
rounded-full
text-center text-xs
rounded-full h-4 w-4
flex justify-center items-center
text-center text-[0.5rem]
text-zinc-100 dark:text-zinc-900
#{classes_by_media(
item.external_id,
Expand All @@ -78,7 +79,7 @@ defmodule LivedjWeb.ListComponent do
<div class="
flex-auto block
text-xs leading-6 font-semibold
p-1 px-1 h-8
p-1 px-1 h-8 w-5/6
text-ellipsis overflow-hidden
">
<%= item.title %>
Expand Down
Loading
Loading