diff options
-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 | ||||
-rw-r--r-- | components/App.js | 6 | ||||
-rw-r--r-- | components/Header.js | 17 | ||||
-rw-r--r-- | configs/translations.js | 52 | ||||
-rw-r--r-- | helpers/windowActions.js | 6 | ||||
-rw-r--r-- | package-lock.json | 31 | ||||
-rw-r--r-- | package.json | 3 | ||||
-rw-r--r-- | pages/api/youtube/playlist.js | 26 | ||||
-rw-r--r-- | pages/api/youtube/search.js | 9 | ||||
-rw-r--r-- | pages/api/youtube/video.js (renamed from pages/api/youtube/player.js) | 12 | ||||
-rw-r--r-- | pages/index.js | 9 | ||||
-rw-r--r-- | styles/global.scss | 5 | ||||
-rw-r--r-- | styles/global/_themes.scss | 6 | ||||
-rw-r--r-- | styles/global/_window.scss | 5 | ||||
-rw-r--r-- | styles/main/_header.scss | 29 |
19 files changed, 434 insertions, 177 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> </> diff --git a/components/App.js b/components/App.js index 9faa9c9..f6a297b 100644 --- a/components/App.js +++ b/components/App.js @@ -1,11 +1,13 @@ import { useEffect, useRef } from 'react' +import useApps from 'hooks/useApps' import useSettings from 'hooks/useSettings' import useMediaQuery from 'hooks/useMediaQuery' import { close, toggleMin, toggleMax, move, focus } from 'helpers/windowActions' import { faArrowUp, faExpandAlt, faTimes, faCompressAlt } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -const App = ({ children, app, setApps }) => { +const App = ({ children, app }) => { + const { apps, setApps } = useApps() const { t } = useSettings() const winRef = useRef(null) const forceMax = useMediaQuery(`(max-width: ${app.width}), (max-height: ${app.height})`) @@ -36,7 +38,7 @@ const App = ({ children, app, setApps }) => { <div className='window__content'>{children}</div> <div className='window__title-buttons'> {app.buttons.includes('min') && ( - <span onClick={e => { e.preventDefault(); e.stopPropagation(); toggleMin(app.name, setApps) }}> + <span onClick={e => { e.preventDefault(); e.stopPropagation(); toggleMin(app.name, apps, setApps) }}> <FontAwesomeIcon icon={faArrowUp} /> </span> )} diff --git a/components/Header.js b/components/Header.js index 87c31e8..5e4ff12 100644 --- a/components/Header.js +++ b/components/Header.js @@ -8,7 +8,7 @@ import { focus, toggleMin, open, close } from 'helpers/windowActions' import appList from 'configs/appList' import useSettings from 'hooks/useSettings' import useApps from 'hooks/useApps' -import { faTimes } from '@fortawesome/free-solid-svg-icons' +import { faArrowUp, faTimes } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' const Header = () => { @@ -30,7 +30,7 @@ const Header = () => { const handleClick = (app, setApps) => { if (app.min) { - toggleMin(app.name, setApps) + toggleMin(app.name, apps, setApps) } focus(app.name, setApps) } @@ -54,13 +54,18 @@ const Header = () => { onClick={() => handleClick(app, setApps)} > <span + className={ + apps.findIndex(a => a && a.name === app.name) === apps.length - 1 ? 'active' : '' + } style={{ - ...apps.findIndex(a => a && a.name === app.name) === apps.length - 1 ? { fontWeight: 600 } : {}, ...app.min ? { color: '#888' } : {} }} > - {t(app.name)} - <span className='mobile-only' onClick={() => close(app.name, setApps)}> + <span>{t(app.name)}</span> + <span className='mobile-only' onClick={e => { e.stopPropagation(); toggleMin(app.name, apps, setApps) }}> + <FontAwesomeIcon icon={faArrowUp} /> + </span> + <span className='mobile-only' onClick={e => { e.stopPropagation(); close(app.name, setApps) }}> <FontAwesomeIcon icon={faTimes} /> </span> </span> @@ -69,7 +74,7 @@ const Header = () => { }) } <li className='mobile-only'> - <span onClick={() => setShowApps(false)} className='window__button'>{t('close')}</span> + <div onClick={() => setShowApps(false)} className='window__button'>{t('close')}</div> </li> </ul> </div> diff --git a/configs/translations.js b/configs/translations.js index ff7de3c..30d6432 100644 --- a/configs/translations.js +++ b/configs/translations.js @@ -76,12 +76,13 @@ const translations = { notes_remove_confirm: 'Do you want to remove note?', player_playlist_empty: 'Your playlist is empty', player_playlist_default: 'Default playlist', - youtube_videos: 'Videos', - youtube_music: 'Music', - youtube_live: 'Live', - youtube_channels: 'Channels', - youtube_playlists: 'Playlists', - youtube_enqueue: 'Enqueue' + yt_video: 'Videos', + yt_video_count: ' videos', + yt_live: 'Live', + yt_playlist: 'Playlists', + yt_enqueue: 'Enqueue', + yt_views: 'views', + yt_watching: 'watching' }, pl: { register_user: 'Zarejestruj użytkownika', @@ -160,12 +161,13 @@ const translations = { notes_remove_confirm: 'Czy na pewno chcesz usunąć notatkę?', player_playlist_empty: 'Lista otwarzania jest pusta', player_playlist_default: 'Domyślna lista', - youtube_videos: 'Wideo', - youtube_music: 'Muzyka', - youtube_live: 'Na żywo', - youtube_channels: 'Kanały', - youtube_playlists: 'Playlisty', - youtube_enqueue: 'Dodaj do listy' + yt_video: 'Wideo', + yt_video_count: ' klipów', + yt_live: 'Na żywo', + yt_playlist: 'Playlisty', + yt_enqueue: 'Dodaj do listy', + yt_views: 'wyświetleń', + yt_watching: 'oglądających' }, es: { register_user: 'Registrar al usuario', @@ -244,12 +246,13 @@ const translations = { notes_remove_confirm: '¿Estás seguro de que deseas eliminar la nota?', player_playlist_empty: 'La lista está vacía', player_playlist_default: 'Lista predeterminada', - youtube_videos: 'Video', - youtube_music: 'Música', - youtube_live: 'En vivo', - youtube_channels: 'Canales', - youtube_playlists: 'Listas', - youtube_enqueue: 'Agregar' + yt_video: 'Video', + yt_video_count: ' clips', + yt_live: 'En vivo', + yt_playlist: 'Listas', + yt_enqueue: 'Agregar', + yt_views: 'visitas', + yt_watching: 'espectadores' }, de: { register_user: 'Registrieren Sie den Benutzer', @@ -328,12 +331,13 @@ const translations = { notes_remove_confirm: 'Möchten Sie die Notiz wirklich löschen? ', player_playlist_empty: 'Playlist ist leer', player_playlist_default: 'Standardliste', - youtube_videos: 'Video', - youtube_music: 'Musik', - youtube_live: 'Live', - youtube_channels: 'Kanäle', - youtube_playlists: 'Playlists', - youtube_enqueue: 'Hinzufügen' + yt_video: 'Video', + yt_video_count: ' Clips', + yt_live: 'Live', + yt_playlist: 'Playlists', + yt_enqueue: 'Hinzufügen', + yt_views: 'Aufrufe', + yt_watching: 'Zuschauer' } } diff --git a/helpers/windowActions.js b/helpers/windowActions.js index ac86ea5..7cc09be 100644 --- a/helpers/windowActions.js +++ b/helpers/windowActions.js @@ -30,11 +30,13 @@ export const unfocus = (appName, setApps) => { }) } -export const toggleMin = (appName, setApps) => { +export const toggleMin = (appName, apps, setApps) => { setApps(apps => ([ ...apps.map(a => a.name === appName ? { ...a, min: !a.min } : a) ])) - unfocus(appName, setApps) + apps.find(a => a.name === appName).min + ? focus(appName, setApps) + : unfocus(appName, setApps) } export const toggleMax = (appName, setApps) => setApps(apps => ([ diff --git a/package-lock.json b/package-lock.json index f34d1e2..68e2b95 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,6 +5,7 @@ "requires": true, "packages": { "": { + "name": "myapps", "version": "1.0.0", "license": "MIT", "dependencies": { @@ -23,7 +24,8 @@ "sass": "^1.35.1", "scrape-youtube": "^2.1.9", "swr": "^1.0.0", - "ytdl-core": "^4.9.1" + "ytdl-core": "^4.9.1", + "ytpl": "^2.2.3" }, "devDependencies": { "@next/eslint-plugin-next": "^11.1.2", @@ -1717,7 +1719,6 @@ "dependencies": { "anymatch": "~3.1.1", "braces": "~3.0.2", - "fsevents": "~2.3.1", "glob-parent": "~5.1.0", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", @@ -4047,8 +4048,7 @@ "bson": "^1.1.4", "denque": "^1.4.1", "optional-require": "^1.0.3", - "safe-buffer": "^5.1.2", - "saslprep": "^1.0.0" + "safe-buffer": "^5.1.2" }, "engines": { "node": ">=4" @@ -4195,10 +4195,6 @@ "@next/polyfill-module": "11.1.2", "@next/react-dev-overlay": "11.1.2", "@next/react-refresh-utils": "11.1.2", - "@next/swc-darwin-arm64": "11.1.2", - "@next/swc-darwin-x64": "11.1.2", - "@next/swc-linux-x64-gnu": "11.1.2", - "@next/swc-win32-x64-msvc": "11.1.2", "@node-rs/helper": "1.2.1", "assert": "2.0.0", "ast-types": "0.13.2", @@ -6286,6 +6282,17 @@ "engines": { "node": ">=10" } + }, + "node_modules/ytpl": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/ytpl/-/ytpl-2.2.3.tgz", + "integrity": "sha512-d18HibT8wzEWzWsXFS6u6kUKCCFL20roFVwpjyGCzl+nP8sOAz5xSLafxLflkkoeXJU8AFuO7BEsJXIREASvFQ==", + "dependencies": { + "miniget": "^4.2.1" + }, + "engines": { + "node": ">=8" + } } }, "dependencies": { @@ -11072,6 +11079,14 @@ "miniget": "^4.0.0", "sax": "^1.1.3" } + }, + "ytpl": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/ytpl/-/ytpl-2.2.3.tgz", + "integrity": "sha512-d18HibT8wzEWzWsXFS6u6kUKCCFL20roFVwpjyGCzl+nP8sOAz5xSLafxLflkkoeXJU8AFuO7BEsJXIREASvFQ==", + "requires": { + "miniget": "^4.2.1" + } } } } diff --git a/package.json b/package.json index a331983..0cf9415 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "sass": "^1.35.1", "scrape-youtube": "^2.1.9", "swr": "^1.0.0", - "ytdl-core": "^4.9.1" + "ytdl-core": "^4.9.1", + "ytpl": "^2.2.3" }, "devDependencies": { "@next/eslint-plugin-next": "^11.1.2", diff --git a/pages/api/youtube/playlist.js b/pages/api/youtube/playlist.js new file mode 100644 index 0000000..e75e00d --- /dev/null +++ b/pages/api/youtube/playlist.js @@ -0,0 +1,26 @@ +import withSession from 'hocs/withSession' +import ytpl from 'ytpl' + +export default withSession(async (req, res) => { + switch (req.method) { + case 'POST': + try { + const user = req.session.get('user') + const { id } = req.body + + if (!user || !user?.isVerified || !id) { + throw new Error('Something went wrong') + } + + const info = await ytpl(id, { gl: user.language || 'en' }) + + res.status(200).json(info) + } catch (error) { + res.status(400).json([]) + } + break + default: + res.status(400).send() + break + } +}) diff --git a/pages/api/youtube/search.js b/pages/api/youtube/search.js index b17d008..f1b461f 100644 --- a/pages/api/youtube/search.js +++ b/pages/api/youtube/search.js @@ -12,9 +12,14 @@ export default withSession(async (req, res) => { throw new Error('Something went wrong') } - const video = await youtube.search(quote) + const results = await youtube.search(quote[0], { + ...quote[1], + requestOptions: { + headers: { 'Accept-Language': user.language || 'en' } + } + }) - res.status(200).json(video) + res.status(200).json(results) } catch (error) { res.status(400).json([]) } diff --git a/pages/api/youtube/player.js b/pages/api/youtube/video.js index a6d3a7e..aeb2e8b 100644 --- a/pages/api/youtube/player.js +++ b/pages/api/youtube/video.js @@ -1,24 +1,18 @@ import withSession from 'hocs/withSession' import ytdl from 'ytdl-core' -const getId = url => { - const regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/ - const match = url.match(regExp) - return (match && match[7].length === 11) ? match[7] : false -} - export default withSession(async (req, res) => { switch (req.method) { case 'POST': try { const user = req.session.get('user') - const { url } = req.body + const { id } = req.body - if (!user || !user?.isVerified || !url) { + if (!user || !user?.isVerified || !id) { throw new Error('Something went wrong') } - const info = await ytdl.getInfo(getId(url)) + const info = await ytdl.getInfo(id, { lang: user.language || 'en' }) res.status(200).json(info) } catch (error) { diff --git a/pages/index.js b/pages/index.js index af25f7b..a13a840 100644 --- a/pages/index.js +++ b/pages/index.js @@ -1,4 +1,5 @@ import styles from 'styles/Main.module.scss' +import { useMemo } from 'react' import Image from 'next/image' import useUser from 'hooks/useUser' import useSettings from 'hooks/useSettings' @@ -70,7 +71,7 @@ const Home = () => { app={app} setApps={setApps} > - <AppComponent {...app.props} /> + <AppContainer AppComponent={AppComponent} appProps={app.props} /> </App> ) })} @@ -79,4 +80,10 @@ const Home = () => { ) } +const AppContainer = ({ AppComponent, appProps }) => { + const Component = useMemo(() => <AppComponent {...appProps} />, [appProps]) + + return Component +} + export default Home diff --git a/styles/global.scss b/styles/global.scss index f72a66b..ad4a51a 100644 --- a/styles/global.scss +++ b/styles/global.scss @@ -53,3 +53,8 @@ textarea, input { display: none!important; } } + +.active { + color: var(--color-text-alt)!important; + font-weight: 600; +} diff --git a/styles/global/_themes.scss b/styles/global/_themes.scss index e0b146f..4127c4c 100644 --- a/styles/global/_themes.scss +++ b/styles/global/_themes.scss @@ -2,7 +2,7 @@ --color-bg: #50a3a2; --color-bg-alt: #53e3a6; --color-text: #333; - --color-text-alt: #111; + --color-text-alt: #000; --color-decor: #ccc; --color-glass: rgb(151, 215, 200, .9); --color-glass-alt: rgba(255, 255, 255, 0.5); @@ -31,7 +31,7 @@ --color-bg: #71b0d7; --color-bg-alt: #a4e4fd; --color-text: #333; - --color-text-alt: #111; + --color-text-alt: #000; --color-decor: #ccc; --color-glass: rgba(151, 192, 230, 0.9); --color-glass-alt: rgba(255, 255, 255, 0.5); @@ -59,7 +59,7 @@ .black { --color-bg: #000; --color-bg-alt: #222; - --color-text: #eee; + --color-text: #ddd; --color-text-alt: #fff; --color-decor: #ccc; --color-glass: rgb(20, 20, 20, .9); diff --git a/styles/global/_window.scss b/styles/global/_window.scss index cf78752..9f224a9 100644 --- a/styles/global/_window.scss +++ b/styles/global/_window.scss @@ -137,11 +137,6 @@ pointer-events: none; } - .active { - color: var(--color-text-alt); - font-weight: 600; - } - @media(max-width: 40em) { height: 3em; overflow: auto; diff --git a/styles/main/_header.scss b/styles/main/_header.scss index e6ac70f..ea75f5e 100644 --- a/styles/main/_header.scss +++ b/styles/main/_header.scss @@ -28,10 +28,33 @@ & > li { display: inline-block; - & > span > span { - padding: .75em; + @media(max-width: 40em) { + & > span:first-of-type { + display: flex!important; + width: 100%; + } + + & > span > span:first-of-type { + flex-grow: 1; + } + } + + & > span > span:nth-of-type(2), + & > span > span:nth-of-type(3) { + margin: .25em .25em .25em .5em; + padding: .5em; display: inline-block; - margin-left: .5em; + border-top: 1px solid var(--color-window-border-top); + border-bottom: 1px solid var(--color-window-border-bottom); + border-radius: .5em; + background-color: var(--color-window-content); + } + + & > span > span:nth-of-type(2) { + margin-left: 1em; + } + + & > span > span:nth-of-type(3) { color: var(--color-error); } |