aboutsummaryrefslogtreecommitdiffstats
path: root/apps/Player
diff options
context:
space:
mode:
Diffstat (limited to 'apps/Player')
-rw-r--r--apps/Player/components/App.js82
-rw-r--r--apps/Player/components/Buttons.js1
-rw-r--r--apps/Player/components/Video.js63
-rw-r--r--apps/Player/styles/_player.scss121
4 files changed, 215 insertions, 52 deletions
diff --git a/apps/Player/components/App.js b/apps/Player/components/App.js
index f7b0557..6633bc8 100644
--- a/apps/Player/components/App.js
+++ b/apps/Player/components/App.js
@@ -1,15 +1,20 @@
import styles from '../styles/Player.module.scss'
import { useState, useEffect } from 'react'
import useSettings from 'hooks/useSettings'
+import useMediaQuery from 'hooks/useMediaQuery'
import Video from './Video'
import Buttons from './Buttons'
import { Splash } from 'components'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import { faList, faTrashAlt, faCaretSquareRight, faInfinity, faAlignJustify } from '@fortawesome/free-solid-svg-icons'
const App = ({ list }) => {
const { t } = useSettings()
+ const smallDevice = useMediaQuery('(max-width: 40em)')
const [current, setCurrent] = useState(null)
const [playlist, setPlaylist] = useState(null)
const [showPlaylist, setShowPlaylist] = useState(false)
+ const [details, setDetails] = useState({ show: false })
useEffect(() => {
if (list) {
@@ -40,16 +45,14 @@ const App = ({ list }) => {
useEffect(() => {
if (playlist) {
- const { items, enqueue } = playlist
-
if (typeof window !== 'undefined' && playlist && playlist.length > 0) {
- window.localStorage.setItem('playlist', JSON.stringify(enqueue ? [...playlist, ...items] : items))
+ window.localStorage.setItem('playlist', JSON.stringify(playlist))
setShowPlaylist(true)
}
if (current === null && list) {
setCurrent(
- enqueue
+ list.enqueue
? playlist && playlist.length > 1
? playlist.length - 1
: 0
@@ -59,7 +62,35 @@ const App = ({ list }) => {
}
}, [playlist])
- if (!playlist) return <Splash />
+ useEffect(() => {
+ smallDevice && showPlaylist && setDetails(d => ({ ...d, show: false }))
+ }, [showPlaylist])
+
+ useEffect(() => {
+ smallDevice && details && details.show && setShowPlaylist(false)
+ }, [details && details.show])
+
+ const remove = (e, i) => {
+ e.stopPropagation()
+ if (current === i) {
+ setCurrent(null)
+ }
+ setPlaylist(p => p.filter((_, j) => j !== i))
+ if (current > i) {
+ setCurrent(c => c - 1)
+ }
+ }
+
+ if (!playlist) {
+ return (
+ <>
+ <div className='window__submenu' />
+ <div className={styles.player}>
+ <Splash />
+ </div>
+ </>
+ )
+ }
return (
<>
@@ -69,16 +100,24 @@ const App = ({ list }) => {
onClick={() => { setShowPlaylist(p => !p) }}
className={current ? 'active' : null}
>
- {t('player_playlist_default')}
+ <FontAwesomeIcon icon={faList} />
</div>
- <div onClick={() => {}}>+</div>
+ <span />
<Buttons current={current} setCurrent={setCurrent} playlist={playlist} />
+ <div onClick={() => { setDetails(d => ({ ...d, show: !d.show })) }}>
+ <FontAwesomeIcon icon={faAlignJustify} />
+ </div>
</div>
</div>
<div className={styles.player}>
<div>
- {current !== null && (
- <Video playlist={playlist} current={current} setCurrent={setCurrent} />
+ {playlist && current !== null && playlist[current] && setDetails && (
+ <Video
+ playlist={playlist}
+ current={current}
+ setCurrent={setCurrent}
+ setDetails={setDetails}
+ />
)}
</div>
<div style={showPlaylist ? {} : { transform: 'translateX(-110%)' }}>
@@ -89,11 +128,19 @@ const App = ({ list }) => {
playlist.map((item, i) => (
<li
onClick={() => { setCurrent(i) }}
- className={current === i ? styles.activeItem : ''}
+ className={current === i ? 'active' : ''}
key={item.id}
>
+ <FontAwesomeIcon
+ icon={item.type.split('_')[1] === 'live'
+ ? faInfinity
+ : faCaretSquareRight}
+ />
<span>{(i + 1) + '.'}</span>
- {item.title}
+ <span>{item.title}</span>
+ <span onClick={e => remove(e, i)}>
+ <FontAwesomeIcon icon={faTrashAlt} />
+ </span>
</li>
))
)
@@ -102,7 +149,18 @@ const App = ({ list }) => {
)
}
</ul>
- <div onClick={() => setShowPlaylist(false)}>&lt;</div>
+ </div>
+ <div style={details.show ? {} : { transform: 'translateX(110%)' }}>
+ {details && (
+ <div>
+ <pre>
+ {details.title}
+ </pre>
+ <pre>
+ {details.description}
+ </pre>
+ </div>
+ )}
</div>
</div>
</>
diff --git a/apps/Player/components/Buttons.js b/apps/Player/components/Buttons.js
index 14452a0..efc8555 100644
--- a/apps/Player/components/Buttons.js
+++ b/apps/Player/components/Buttons.js
@@ -3,7 +3,6 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
const Buttons = ({ current, setCurrent, playlist }) => (
<>
- <span />
<div
className={current !== null && current > 0 ? '' : 'iconOff'}
onClick={() => { current !== null && current > 0 && setCurrent(c => c - 1) }}
diff --git a/apps/Player/components/Video.js b/apps/Player/components/Video.js
index 7152967..7028f1f 100644
--- a/apps/Player/components/Video.js
+++ b/apps/Player/components/Video.js
@@ -1,25 +1,66 @@
-import { useRef } from 'react'
+import { useState, useEffect, useRef } from 'react'
import Splash from 'components/Splash'
+import fetchJson from 'helpers/fetchJson'
-const Video = ({ playlist, current, setCurrent }) => {
- if (!playlist) return null
-
+const Video = ({ playlist, current, setCurrent, audioOnly = false, setDetails }) => {
+ const [data, setData] = useState(null)
+ const [loading, setLoading] = useState(null)
const videoEl = useRef()
- const sources = playlist[current]?.sources
const handleEnd = () => {
setCurrent(current === playlist.length - 1 ? null : current + 1)
}
+ useEffect(() => {
+ setLoading(true)
+ if (current === null) {
+ setData(null)
+ setDetails(d => ({ ...d, show: false }))
+ }
+
+ switch (playlist[current].type.split('_')[0]) {
+ case 'yt':
+ fetchJson('/api/youtube/video', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ id: playlist[current].id })
+ })
+ .then(v => {
+ setData({
+ id: v.videoDetails.videoId,
+ formats: v.formats
+ .filter(v => !v.isHLS && v.hasAudio && (audioOnly ? !v.hasVideo : v.hasVideo))
+ .sort((a, b) => audioOnly ? a.audioBitrate < b.audioBitrate : a.bitrate < b.bitrate)
+ })
+ setDetails(d => ({
+ ...d,
+ title: v.videoDetails.title,
+ description: v.videoDetails.description
+ }))
+ })
+ .catch(() => console.log('error fetching video'))
+ .finally(() => setLoading(false))
+ break
+ default:
+ }
+ }, [playlist && playlist[current].id])
+
return (
- sources
+ data && !loading
? (
- <video onEnded={handleEnd} ref={videoEl} key={playlist[current]?.id} controls autoPlay>
+ <video
+ onEnded={handleEnd}
+ ref={videoEl}
+ key={data.id}
+ controls
+ autoPlay
+ style={audioOnly ? { backgroundImage: 'url(' + playlist[current].thumbnail + ')' } : {}}
+ >
{
- sources.map(s => (
- <source src={s.url} type={s.mimeType} key={s.url} />
- ))
- }
+ data.formats.map(s => (
+ <source src={s.url} type={s.mimeType} key={s.url} />
+ ))
+ }
Your browser does not support the video tag.
</video>
)
diff --git a/apps/Player/styles/_player.scss b/apps/Player/styles/_player.scss
index 8cf37cd..0d73b59 100644
--- a/apps/Player/styles/_player.scss
+++ b/apps/Player/styles/_player.scss
@@ -16,55 +16,120 @@
}
}
- & > div:nth-of-type(2) {
- width: auto;
- min-width: 25em;
+
+ & > div:nth-of-type(2) {
+ overflow-y: auto;
+ }
+
+ & > div:nth-of-type(2),
+ & > div:nth-of-type(3) {
+ width: 48%;
position: absolute;
top: 0;
- left: 0;
bottom: 2em;
background-color: var(--color-glass);
transition: .3s transform;
- border-top-right-radius: 1em;
- border-bottom-right-radius: 1em;
- border-right: 1px solid var(--color-window-border-bottom);
+
+ @media(max-width: 40em) {
+ width: 96%;
+ }
+
+ ul {
+ width: 100%;
+ padding-top: .5em;
+ }
li {
- padding: .5em 2em .5em .5em;
- white-space: nowrap;
+ display: flex;
+ align-items: center;
+ padding: 0 1em;
+
+ & > span:first-of-type {
+ margin: 0 .5em;
+ }
+
+ & > span:nth-of-type(2) {
+ flex-grow: 1;
+ overflow-x: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+
+ & > span:nth-of-type(3) {
+ padding: .5em;
- span {
- padding-right: 1em;
+ svg {
+ transition: .3s opacity linear .3s, .3s color;
+
+ @media(min-width: 40em) {
+ visibility: hidden;
+ opacity: 0;
+ }
+ }
}
+ & > span:nth-of-type(3) {
+ & > svg {
+ color: var(--color-error);
+ }
+
+
+ @media(hover: hover) {
+ &:hover > svg {
+ color: #f00;
+ }
+ }
+ }
+
+
@media(hover: hover) {
&:hover {
background-color: var(--color-glass-alt);
}
+
+ &:hover > span:nth-of-type(3) > svg {
+ visibility: visible;
+ opacity: 1;
+ }
}
}
+ }
+
+ & > div:nth-of-type(2) {
+ left: 0;
+ border-top-right-radius: 1em;
+ border-bottom-right-radius: 1em;
+ border-right: 1px solid var(--color-window-border-bottom);
& > div {
- position: absolute;
- top: 50%;
right: -0.5em;
- padding: 2em .5em;
- border-radius: .5em;
- transform: translateY(-50%);
- background-color: var(--color-window-menu-alt);
- color: var(--color-text-alt);
- border: 1px solid var(--color-window-border-bottom);
- transition: .3s background;
-
- &:hover {
- background-color: var(--color-window-menu);
- }
}
}
-}
+ & > div:nth-of-type(3) {
+ right: 0;
+ border-top-left-radius: 1em;
+ border-bottom-left-radius: 1em;
+ border-left: 1px solid var(--color-window-border-bottom);
+
+ & > div:last-of-type {
+ left: -0.5em;
+ }
-.activeItem {
- color: var(--color-text-alt);
- font-weight: 600;
+ & > div:first-of-type {
+ overflow-y: auto;
+ padding: 1em;
+ height: 100%;
+ }
+
+ pre {
+ line-height: 1.25;
+ white-space: break-spaces;
+
+ &:first-of-type {
+ font-weight: 600;
+ padding-bottom: 1em;
+ }
+ }
+ }
}