aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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
-rw-r--r--apps/Youtube/components/App.js128
-rw-r--r--components/App.js6
-rw-r--r--components/Header.js17
-rw-r--r--configs/translations.js52
-rw-r--r--helpers/windowActions.js6
-rw-r--r--package-lock.json31
-rw-r--r--package.json3
-rw-r--r--pages/api/youtube/playlist.js26
-rw-r--r--pages/api/youtube/search.js9
-rw-r--r--pages/api/youtube/video.js (renamed from pages/api/youtube/player.js)12
-rw-r--r--pages/index.js9
-rw-r--r--styles/global.scss5
-rw-r--r--styles/global/_themes.scss6
-rw-r--r--styles/global/_window.scss5
-rw-r--r--styles/main/_header.scss29
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)}>&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;
+ }
+ }
+ }
}
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);
}