diff options
Diffstat (limited to 'apps')
-rw-r--r-- | apps/Player/components/App.js | 82 | ||||
-rw-r--r-- | apps/Player/components/Buttons.js | 1 | ||||
-rw-r--r-- | apps/Player/components/Video.js | 63 | ||||
-rw-r--r-- | apps/Player/styles/_player.scss | 121 | ||||
-rw-r--r-- | apps/Youtube/components/App.js | 128 |
5 files changed, 284 insertions, 111 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)}><</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; + } + } + } } diff --git a/apps/Youtube/components/App.js b/apps/Youtube/components/App.js index d902346..6130473 100644 --- a/apps/Youtube/components/App.js +++ b/apps/Youtube/components/App.js @@ -7,56 +7,64 @@ import { open } from 'helpers/windowActions' import appList from 'configs/appList' import Splash from 'components/Splash' -const time = t => new Date(t * 1000).toISOString().substr(11, 8) +const time = t => t ? new Date(t * 1000).toISOString().substr(11, 8) : '-' const App = () => { const { apps, setApps } = useApps() const [results, setResults] = useState() - const [searching, setSearching] = useState(false) - const [sending, setSending] = useState(false) + const [loading, setLoading] = useState(false) const [enqueue, setEnqueue] = useState(false) - const [type, setType] = useState('youtube_videos') + const [type, setType] = useState('yt_video') const { t } = useSettings() - const handleSearch = async e => { + const handleSearch = e => { e.preventDefault() e.stopPropagation() - setSearching(true) - const quote = e.currentTarget.quote.value + setLoading(true) + const quote = [ + e.currentTarget.quote.value, + { type: type === 'yt_music' ? 'music' : type.split('_')[1] } + ] - const results = await fetchJson('/api/youtube/search', { + fetchJson('/api/youtube/search', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ quote }) }) - - results?.videos && setResults(results.videos) - setSearching(false) + .then(results => { + if (type === 'yt_live') { + results?.streams && setResults(results.streams) + } else if (type === 'yt_playlist') { + results?.playlists && setResults(results.playlists) + } else { + results?.videos && setResults(results.videos) + } + }) + .catch(() => { console.log('Could not fetch results') }) + .finally(() => { setLoading(false) }) } - const handlePlay = async url => { - setSending(true) - const data = await fetchJson('/api/youtube/player', { + const fetchPlaylist = (id) => fetchJson( + '/api/youtube/playlist', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ url }) - }) - - const list = { - items: [{ - id: data.videoDetails.videoId, - title: data.videoDetails.title, - type: 'youtube-video', - sources: data.formats.filter(v => v.hasAudio).sort((a, b) => a.bitrate > b.bitrate) - }], - enqueue + body: JSON.stringify({ id }) } + ) - apps && apps.length > 0 && apps.some(a => a && a.name === 'Player') - ? setApps(prev => prev.map(a => a.name === 'Player' ? { ...a, props: { list } } : a)) - : open({ appName: 'Player', ...appList.Player }, setApps, { list }) + const handlePlay = async (e, title, id) => { + e.stopPropagation() + setLoading(true) + const items = type === 'yt_playlist' + ? await fetchPlaylist(id) + .then(data => data.items.map(i => ({ title: i.title, id: i.id, type: i.isLive ? 'yt_live' : 'yt_video' }))) + .catch(() => { console.log('Could not fetch playlist') }) + : [{ title, id, type }] - setSending(false) + apps && apps.length > 0 && apps.some(a => a && a.name === 'Player') + ? setApps(prev => prev.map(a => a.name === 'Player' ? { ...a, props: { list: { items, enqueue } } } : a)) + : open({ appName: 'Player', ...appList.Player }, setApps, { list: { items, enqueue } }) + setLoading(false) } return ( @@ -64,11 +72,9 @@ const App = () => { <div className='window__submenu'> <div> {[ - 'youtube_videos', - 'youtube_music', - 'youtube_live', - 'youtube_channels', - 'youtube_playlists' + 'yt_video', + 'yt_live', + 'yt_playlist' ].map(y => ( <div className={y === type ? 'active' : ''} @@ -83,7 +89,7 @@ const App = () => { className={enqueue ? '' : 'off'} onClick={() => { setEnqueue(e => !e) }} > - {t('youtube_enqueue')} + {t('yt_enqueue')} </div> </div> </div> @@ -93,30 +99,34 @@ const App = () => { <input type='submit' className='window__button' value={t('search')} /> </form> <div className='window__scroll'> - { - searching - ? ( - <Splash /> - ) - : ( - <ul> - { - results && results.length > 0 && results.map(r => ( - <li key={r.id} onClick={() => handlePlay(r.link)}> - <img loading='lazy' src={r.thumbnail} width={96} height={72} /> - <p>{time(r.duration)}</p> - <div> - <p>{r.title}</p> - <p>Views: {r.views}, uploaded: {r.uploaded || '-'}</p> - <p>{r.channel?.name}</p> - </div> - </li> - )) - } - {sending && <Splash fixed />} - </ul> - ) - } + <ul> + { + results && results.length > 0 && results.map(r => ( + <li + key={r.id} + onClick={e => handlePlay(e, r.title, r.id)} + > + <img loading='lazy' src={r.thumbnail} width={96} height={72} /> + {r.duration && <p>{time(r.duration)}</p>} + <div> + <p>{r.title}</p> + <p>{r.views + ? r.views + ' ' + t('yt_views') + : r.watching + ? r.watching + ' ' + t('yt_watching') + : r.videoCount + ? r.videoCount + t('yt_video_count') + : '-'} + {(r.views || r.watching) && r.uploaded && ', '} + {r.uploaded || ''} + </p> + <p>{r.channel?.name}</p> + </div> + </li> + )) + } + </ul> + {loading && <Splash fixed />} </div> </div> </> |