aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--apps/Notes/components/List.js32
-rw-r--r--apps/Player/components/App.js107
-rw-r--r--apps/Player/components/Buttons.js34
-rw-r--r--apps/Player/components/Video.js32
-rw-r--r--apps/Player/index.js3
-rw-r--r--apps/Player/styles/Player.module.scss1
-rw-r--r--apps/Player/styles/_player.scss70
-rw-r--r--apps/Youtube/components/App.js126
-rw-r--r--apps/Youtube/index.js3
-rw-r--r--apps/Youtube/styles/Youtube.module.scss1
-rw-r--r--apps/Youtube/styles/_results.scss68
-rw-r--r--apps/index.js2
-rw-r--r--components/App.js10
-rw-r--r--components/Header.js6
-rw-r--r--components/Layout.js6
-rw-r--r--configs/appList.js6
-rw-r--r--configs/translations.js54
-rw-r--r--helpers/windowActions.js18
-rw-r--r--hooks/useSettings.js2
-rw-r--r--package-lock.json81
-rw-r--r--package.json4
-rw-r--r--pages/api/youtube/player.js32
-rw-r--r--pages/api/youtube/search.js26
-rw-r--r--pages/index.js2
-rw-r--r--public/icons/notes.svg14
-rw-r--r--public/icons/player.svg10
-rw-r--r--public/icons/youtube.svg7
-rw-r--r--styles/global/_window.scss47
-rw-r--r--styles/main/_icon.scss2
29 files changed, 749 insertions, 57 deletions
diff --git a/apps/Notes/components/List.js b/apps/Notes/components/List.js
index 05d0711..0e6c8fa 100644
--- a/apps/Notes/components/List.js
+++ b/apps/Notes/components/List.js
@@ -60,22 +60,22 @@ const List = () => {
</thead>
<tbody>
{
- notes.length > 0
- ? (notes.sort(sortFn).map(note => (
- <ListItem
- key={note._id}
- note={note}
- setAction={setAction}
- setFetchedNote={setFetchedNote}
- setLoading={setLoading}
- />
- )))
- : (
- <tr>
- <td>{t('notes_list_empty')}</td>
- </tr>
- )
-}
+ notes.length > 0
+ ? (notes.sort(sortFn).map(note => (
+ <ListItem
+ key={note._id}
+ note={note}
+ setAction={setAction}
+ setFetchedNote={setFetchedNote}
+ setLoading={setLoading}
+ />
+ )))
+ : (
+ <tr>
+ <td>{t('notes_list_empty')}</td>
+ </tr>
+ )
+ }
</tbody>
</table>
</div>
diff --git a/apps/Player/components/App.js b/apps/Player/components/App.js
new file mode 100644
index 0000000..5879111
--- /dev/null
+++ b/apps/Player/components/App.js
@@ -0,0 +1,107 @@
+import styles from '../styles/Player.module.scss'
+import { useState, useEffect } from 'react'
+import useSettings from 'hooks/useSettings'
+import Video from './Video'
+import Buttons from './Buttons'
+import { Splash } from 'components'
+
+const App = ({ list }) => {
+ const { t } = useSettings()
+ const [current, setCurrent] = useState(null)
+ const [playlist, setPlaylist] = useState(null)
+ const [showPlaylist, setShowPlaylist] = useState(false)
+
+ useEffect(() => {
+ if (list) {
+ const { items, enqueue } = list
+
+ setPlaylist(
+ p => enqueue && p
+ ? p.some(x => items.some(y => x.id === y.id))
+ ? p
+ : [...p, ...items]
+ : items
+ )
+
+ if (enqueue) {
+ setShowPlaylist(true)
+ }
+ } else {
+ if (typeof window !== 'undefined') {
+ setPlaylist(JSON.parse(window.localStorage.getItem('playlist')))
+ }
+ }
+ }, [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))
+ setShowPlaylist(true)
+ }
+
+ if (current === null && list) {
+ setCurrent(
+ enqueue
+ ? playlist && playlist.length > 1
+ ? playlist.length - 1
+ : 0
+ : 0
+ )
+ }
+ }
+ }, [playlist])
+
+ if (!playlist) return <Splash />
+
+ return (
+ <>
+ <div className='window__submenu'>
+ <div>
+ <div
+ onClick={() => { setShowPlaylist(p => !p) }}
+ className={current ? 'active' : null}
+ >
+ {t('player_playlist_default')}
+ </div>
+ <div onClick={() => {}}>+</div>
+ <Buttons current={current} setCurrent={setCurrent} playlist={playlist} />
+ </div>
+ </div>
+ <div className={styles.player}>
+ <div>
+ {current !== null && (
+ <Video playlist={playlist} current={current} setCurrent={setCurrent} />
+ )}
+ </div>
+ <div style={showPlaylist ? {} : { transform: 'translateX(-110%)' }}>
+ <ul>
+ {
+ playlist && playlist.length > 0
+ ? (
+ playlist.map((item, i) => (
+ <li
+ onClick={() => { setCurrent(i) }}
+ className={current === i ? styles.activeItem : ''}
+ key={item.id}
+ >
+ <span>{(i + 1) + '.'}</span>
+ {item.title}
+ </li>
+ ))
+ )
+ : (
+ <li>{t('player_playlist_empty')}</li>
+ )
+ }
+ </ul>
+ <div onClick={() => setShowPlaylist(false)}>&lt;</div>
+ </div>
+ </div>
+ </>
+ )
+}
+
+export default App
diff --git a/apps/Player/components/Buttons.js b/apps/Player/components/Buttons.js
new file mode 100644
index 0000000..14452a0
--- /dev/null
+++ b/apps/Player/components/Buttons.js
@@ -0,0 +1,34 @@
+import { faStepForward, faStepBackward, faPlay, faStop } from '@fortawesome/free-solid-svg-icons'
+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) }}
+ >
+ <FontAwesomeIcon icon={faStepBackward} />
+ </div>
+ <div
+ className={current === null ? 'iconOff' : ''}
+ onClick={() => { current !== null && setCurrent(null) }}
+ >
+ <FontAwesomeIcon icon={faStop} />
+ </div>
+ <div
+ className={current === null ? '' : 'iconOff'}
+ onClick={() => { current === null && setCurrent(0) }}
+ >
+ <FontAwesomeIcon icon={faPlay} />
+ </div>
+ <div
+ className={current !== null && current < playlist.length - 1 ? '' : 'iconOff'}
+ onClick={() => { current !== null && current < playlist.length - 1 && setCurrent(c => c + 1) }}
+ >
+ <FontAwesomeIcon icon={faStepForward} />
+ </div>
+ </>
+)
+
+export default Buttons
diff --git a/apps/Player/components/Video.js b/apps/Player/components/Video.js
new file mode 100644
index 0000000..7152967
--- /dev/null
+++ b/apps/Player/components/Video.js
@@ -0,0 +1,32 @@
+import { useRef } from 'react'
+import Splash from 'components/Splash'
+
+const Video = ({ playlist, current, setCurrent }) => {
+ if (!playlist) return null
+
+ const videoEl = useRef()
+ const sources = playlist[current]?.sources
+
+ const handleEnd = () => {
+ setCurrent(current === playlist.length - 1 ? null : current + 1)
+ }
+
+ return (
+ sources
+ ? (
+ <video onEnded={handleEnd} ref={videoEl} key={playlist[current]?.id} controls autoPlay>
+ {
+ sources.map(s => (
+ <source src={s.url} type={s.mimeType} key={s.url} />
+ ))
+ }
+ Your browser does not support the video tag.
+ </video>
+ )
+ : (
+ <Splash />
+ )
+ )
+}
+
+export default Video
diff --git a/apps/Player/index.js b/apps/Player/index.js
new file mode 100644
index 0000000..7971cc2
--- /dev/null
+++ b/apps/Player/index.js
@@ -0,0 +1,3 @@
+import Player from './components/App'
+
+export default Player
diff --git a/apps/Player/styles/Player.module.scss b/apps/Player/styles/Player.module.scss
new file mode 100644
index 0000000..b882295
--- /dev/null
+++ b/apps/Player/styles/Player.module.scss
@@ -0,0 +1 @@
+@import "player";
diff --git a/apps/Player/styles/_player.scss b/apps/Player/styles/_player.scss
new file mode 100644
index 0000000..8cf37cd
--- /dev/null
+++ b/apps/Player/styles/_player.scss
@@ -0,0 +1,70 @@
+.player {
+ height: 100%;
+ width: 100%;
+ position: relative;
+ background-color: #000;
+
+ & > div:nth-of-type(1) {
+ height: calc(100% - 2em);
+ width: 100%;
+ padding-bottom: .5em;
+
+ video {
+ height: 100%;
+ width: 100%;
+ object-fit: contain;
+ }
+ }
+
+ & > div:nth-of-type(2) {
+ width: auto;
+ min-width: 25em;
+ 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);
+
+ li {
+ padding: .5em 2em .5em .5em;
+ white-space: nowrap;
+
+ span {
+ padding-right: 1em;
+ }
+
+ @media(hover: hover) {
+ &:hover {
+ background-color: var(--color-glass-alt);
+ }
+ }
+ }
+
+ & > 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);
+ }
+ }
+ }
+
+}
+
+.activeItem {
+ color: var(--color-text-alt);
+ font-weight: 600;
+}
diff --git a/apps/Youtube/components/App.js b/apps/Youtube/components/App.js
new file mode 100644
index 0000000..d902346
--- /dev/null
+++ b/apps/Youtube/components/App.js
@@ -0,0 +1,126 @@
+import styles from '../styles/Youtube.module.scss'
+import { useState } from 'react'
+import fetchJson from 'helpers/fetchJson'
+import useApps from 'hooks/useApps'
+import useSettings from 'hooks/useSettings'
+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 App = () => {
+ const { apps, setApps } = useApps()
+ const [results, setResults] = useState()
+ const [searching, setSearching] = useState(false)
+ const [sending, setSending] = useState(false)
+ const [enqueue, setEnqueue] = useState(false)
+ const [type, setType] = useState('youtube_videos')
+ const { t } = useSettings()
+
+ const handleSearch = async e => {
+ e.preventDefault()
+ e.stopPropagation()
+ setSearching(true)
+ const quote = e.currentTarget.quote.value
+
+ const results = await fetchJson('/api/youtube/search', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ quote })
+ })
+
+ results?.videos && setResults(results.videos)
+ setSearching(false)
+ }
+
+ const handlePlay = async url => {
+ setSending(true)
+ const data = await fetchJson('/api/youtube/player', {
+ 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
+ }
+
+ 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 })
+
+ setSending(false)
+ }
+
+ return (
+ <>
+ <div className='window__submenu'>
+ <div>
+ {[
+ 'youtube_videos',
+ 'youtube_music',
+ 'youtube_live',
+ 'youtube_channels',
+ 'youtube_playlists'
+ ].map(y => (
+ <div
+ className={y === type ? 'active' : ''}
+ onClick={() => { setType(y); setResults() }}
+ key={y}
+ >
+ {t(y)}
+ </div>
+ ))}
+ <span />
+ <div
+ className={enqueue ? '' : 'off'}
+ onClick={() => { setEnqueue(e => !e) }}
+ >
+ {t('youtube_enqueue')}
+ </div>
+ </div>
+ </div>
+ <div className={styles.results}>
+ <form onSubmit={handleSearch}>
+ <input type='text' name='quote' />
+ <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>
+ )
+ }
+ </div>
+ </div>
+ </>
+ )
+}
+
+export default App
diff --git a/apps/Youtube/index.js b/apps/Youtube/index.js
new file mode 100644
index 0000000..8c4906c
--- /dev/null
+++ b/apps/Youtube/index.js
@@ -0,0 +1,3 @@
+import Youtube from './components/App'
+
+export default Youtube
diff --git a/apps/Youtube/styles/Youtube.module.scss b/apps/Youtube/styles/Youtube.module.scss
new file mode 100644
index 0000000..eeeaea9
--- /dev/null
+++ b/apps/Youtube/styles/Youtube.module.scss
@@ -0,0 +1 @@
+@import "results";
diff --git a/apps/Youtube/styles/_results.scss b/apps/Youtube/styles/_results.scss
new file mode 100644
index 0000000..854d1e5
--- /dev/null
+++ b/apps/Youtube/styles/_results.scss
@@ -0,0 +1,68 @@
+.results {
+ height: 100%;
+
+ form {
+ padding: .5em;
+ justify-content: center;
+ align-items: center;
+ display: flex;
+
+ input[type=text] {
+ background-color: var(--color-window-content);
+ color: var(--color-text-alt);
+ margin: 1em .5em 0;
+ height: 2.5rem;
+ border: none;
+ border-radius: .5em;
+ padding: 0.5rem;
+ font-size: 1rem;
+ border: 1px dashed var(--color-window-buttons);
+ flex-shrink: 1;
+ flex-grow: 1;
+
+ &:placeholder {
+ font: inherit;
+ }
+ }
+ }
+
+ ul {
+ display: block;
+
+ & > li {
+ padding: .5em;
+ display: flex;
+ position: relative;
+
+ @media(hover: hover) {
+ &:hover {
+ background-color: var(--color-button-alt);
+ }
+ }
+
+ & > p {
+ position: absolute;
+ font-size: .8em;
+ background-color: #000;
+ color: #fff;
+ border-radius: .5em;
+ padding: .25em;
+ bottom: 0.25em;
+ left: 2em;
+ }
+
+ & > div {
+ margin-left: 1em;
+ display: flex;
+ flex-direction: column;
+ // justify-content: space-between;
+
+ & > p:nth-of-type(2) {
+ margin: .25em 0 .5em;
+ font-size: .8em;
+ color: var(--color-decor);
+ }
+ }
+ }
+ }
+}
diff --git a/apps/index.js b/apps/index.js
index 6e264fd..465761b 100644
--- a/apps/index.js
+++ b/apps/index.js
@@ -1,2 +1,4 @@
export { default as Notes } from './Notes'
+export { default as Player } from './Player'
export { default as Settings } from './Settings'
+export { default as Youtube } from './Youtube'
diff --git a/components/App.js b/components/App.js
index 84268cb..9faa9c9 100644
--- a/components/App.js
+++ b/components/App.js
@@ -10,9 +10,7 @@ const App = ({ children, app, setApps }) => {
const winRef = useRef(null)
const forceMax = useMediaQuery(`(max-width: ${app.width}), (max-height: ${app.height})`)
- useEffect(() => {
- move(app, winRef, setApps)
- }, [app, setApps])
+ useEffect(() => { move(app, winRef, setApps) }, [])
return (
<div
@@ -38,17 +36,17 @@ const App = ({ children, app, setApps }) => {
<div className='window__content'>{children}</div>
<div className='window__title-buttons'>
{app.buttons.includes('min') && (
- <span onClick={() => toggleMin(app.name, setApps)}>
+ <span onClick={e => { e.preventDefault(); e.stopPropagation(); toggleMin(app.name, setApps) }}>
<FontAwesomeIcon icon={faArrowUp} />
</span>
)}
{app.buttons.includes('max') && !forceMax && (
- <span onClick={() => toggleMax(app.name, setApps)}>
+ <span onClick={e => { e.preventDefault(); e.stopPropagation(); toggleMax(app.name, setApps) }}>
<FontAwesomeIcon icon={app.max ? faCompressAlt : faExpandAlt} />
</span>
)}
{app.buttons.includes('close') && (
- <span onClick={() => close(app.name, setApps)}>
+ <span onClick={e => { e.preventDefault(); e.stopPropagation(); close(app.name, setApps) }}>
<FontAwesomeIcon icon={faTimes} />
</span>
)}
diff --git a/components/Header.js b/components/Header.js
index 32ec977..87c31e8 100644
--- a/components/Header.js
+++ b/components/Header.js
@@ -7,13 +7,15 @@ import fetchJson from 'helpers/fetchJson'
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 { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-const Header = ({ apps, setApps }) => {
+const Header = () => {
const [userMenu, setUserMenu] = useState(false)
const [showApps, setShowApps] = useState(false)
const { user, mutateUser } = useUser()
+ const { apps, setApps } = useApps()
const { t } = useSettings()
const router = useRouter()
@@ -53,7 +55,7 @@ const Header = ({ apps, setApps }) => {
>
<span
style={{
- ...apps.findIndex(a => a.name === app.name) === apps.length - 1 ? { fontWeight: 600 } : {},
+ ...apps.findIndex(a => a && a.name === app.name) === apps.length - 1 ? { fontWeight: 600 } : {},
...app.min ? { color: '#888' } : {}
}}
>
diff --git a/components/Layout.js b/components/Layout.js
index c4cc553..572e961 100644
--- a/components/Layout.js
+++ b/components/Layout.js
@@ -5,9 +5,7 @@ import useSettings from 'hooks/useSettings'
import PropTypes from 'prop-types'
const Layout = ({
- children,
- apps,
- setApps
+ children
}) => {
const { settings } = useSettings()
@@ -18,7 +16,7 @@ const Layout = ({
<main>
<div className='container'>{children}</div>
</main>
- <Header apps={apps} setApps={setApps} />
+ <Header />
<Popup />
</section>
)
diff --git a/configs/appList.js b/configs/appList.js
index 6268e15..b5f1eac 100644
--- a/configs/appList.js
+++ b/configs/appList.js
@@ -1,8 +1,10 @@
-import { Notes, Settings } from 'apps'
+import { Notes, Player, Settings, Youtube } from 'apps'
const appList = {
Notes: { component: Notes, icon: true, buttons: ['min', 'max', 'close'], height: '48em', width: '64em' },
- Settings: { component: Settings, icon: false, buttons: ['min'], height: '23em', width: '16em' }
+ Player: { component: Player, icon: true, buttons: ['min', 'max', 'close'], height: '48em', width: '64em' },
+ Settings: { component: Settings, icon: false, buttons: ['min'], height: '23em', width: '16em' },
+ Youtube: { component: Youtube, icon: true, buttons: ['min', 'max', 'close'], height: '48em', width: '64em' }
}
export default appList
diff --git a/configs/translations.js b/configs/translations.js
index 8e086d9..ff7de3c 100644
--- a/configs/translations.js
+++ b/configs/translations.js
@@ -21,6 +21,7 @@ const translations = {
select_all: 'Select all',
import: 'Import',
export: 'Export',
+ search: 'Search',
sort: 'Sort',
sort_by: 'Sort by:',
choose_files: 'Choose files',
@@ -33,7 +34,9 @@ const translations = {
modified: 'Modified',
open_apps: 'Open apps:',
Notes: 'Notes',
+ Player: 'Player',
Settings: 'Settings',
+ Youtube: 'Youtube',
mail_ver_subject: 'Verification of your new My Apps account',
mail_ver_t1: 'Thank you for creating an account in My Apps.',
mail_ver_t2: 'We are sending you the verification code:',
@@ -70,7 +73,15 @@ const translations = {
notes_updated_error: 'Could not update note',
notes_removed: 'Note was removed',
notes_removed_error: 'Could not remove note',
- notes_remove_confirm: 'Do you want to remove note?'
+ 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'
},
pl: {
register_user: 'Zarejestruj użytkownika',
@@ -94,6 +105,7 @@ const translations = {
select_all: 'Wybierz wszystkie',
import: 'Wyślij',
export: 'Pobierz',
+ search: 'Szukaj',
sort: 'Sortuj',
sort_by: 'Sortuj wg:',
choose_files: 'Wybierz pliki',
@@ -106,7 +118,9 @@ const translations = {
modified: 'Zmodyfikowano',
open_apps: 'Otwarte aplikacje:',
Notes: 'Notatki',
+ Player: 'Odtwarzacz',
Settings: 'Ustawienia',
+ Youtube: 'Youtube',
mail_ver_subject: 'Weryfikacja Twojego nowego konta w aplikacji My Apps.',
mail_ver_t1: 'Dziękujemy za założenie konta w My Apps',
mail_ver_t2: 'Przesyłamy Ci kod weryfikacyjny:',
@@ -143,7 +157,15 @@ const translations = {
notes_updated_error: 'Błąd podczas zapisu zmian w notatce',
notes_removed: 'Usunięto notatkę',
notes_removed_error: 'Błąd podczas usuwania notatki',
- notes_remove_confirm: 'Czy na pewno chcesz usunąć notatkę?'
+ 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'
},
es: {
register_user: 'Registrar al usuario',
@@ -151,7 +173,7 @@ const translations = {
password: 'contraseña',
confirm_password: 'confirmar la contraseña',
color_theme: 'Elige un tema: ',
- language: 'Lengua: ',
+ language: 'Idioma: ',
login: 'Acceso',
login_error: 'Error de inicio de sesión',
logout: 'Cerrar sesión',
@@ -167,6 +189,7 @@ const translations = {
select_all: 'Elegir todos',
import: 'Enviar',
export: 'Descargar',
+ search: 'Buscar',
sort: 'Ordenar',
sort_by: 'Ordenar por:',
choose_files: 'Seleccione archivos',
@@ -179,7 +202,9 @@ const translations = {
modified: 'Modificado',
open_apps: 'Aplicaciones abiertas:',
Notes: 'Notas',
+ Player: 'Jugador',
Settings: 'Ajustes',
+ Youtube: 'Youtube',
mail_ver_subject: 'Verificación de su nueva cuenta en la aplicación My Apps.',
mail_ver_t1: 'Gracias por crear una cuenta con Mis aplicaciones',
mail_ver_t2: 'Te enviamos un código de verificación: ',
@@ -216,7 +241,15 @@ const translations = {
notes_updated_error: 'Error al guardar cambios en la nota',
notes_removed: 'Nota eliminada',
notes_removed_error: 'Error al eliminar la nota',
- notes_remove_confirm: '¿Estás seguro de que deseas eliminar la nota?'
+ 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'
},
de: {
register_user: 'Registrieren Sie den Benutzer',
@@ -240,6 +273,7 @@ const translations = {
select_all: 'Alles auswählen',
import: 'Senden',
export: 'Herunterladen',
+ search: 'Suche',
sort: 'Sortieren',
sort_by: 'Sortieren nach:',
choose_files: 'Dateien auswählen',
@@ -252,7 +286,9 @@ const translations = {
modified: 'Geändert',
open_apps: 'Anwendungen öffnen:',
Notes: 'Anmerkungen',
+ Player: 'Spieler',
Settings: 'Einstellungen',
+ Youtube: 'Youtube',
mail_ver_subject: 'Bestätigen Ihres neuen Kontos in der Anwendung „Meine Apps“.',
mail_ver_t1: 'Vielen Dank, dass Sie ein Konto bei My Apps erstellt haben',
mail_ver_t2: 'Wir senden Ihnen einen Bestätigungscode: ',
@@ -289,7 +325,15 @@ const translations = {
notes_updated_error: 'Fehler beim Speichern der Änderungen an der Notiz',
notes_removed: 'Notiz gelöscht',
notes_removed_error: 'Fehler beim Löschen der Notiz',
- notes_remove_confirm: 'Möchten Sie die Notiz wirklich löschen? '
+ 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'
}
}
diff --git a/helpers/windowActions.js b/helpers/windowActions.js
index 090e20a..ac86ea5 100644
--- a/helpers/windowActions.js
+++ b/helpers/windowActions.js
@@ -1,16 +1,20 @@
-export const close = (appName, setApps) => { setApps(apps => apps.filter(a => a && a.name !== appName)) }
+export const close = (appName, setApps) => {
+ setApps(apps => apps.filter(a => a && a.name !== appName))
+}
-export const open = ({ appName, buttons, height, width }, setApps) => {
+export const open = ({ appName, buttons, height, width }, setApps, props = {}) => {
setApps(apps => (
- !apps.some(a => a.name === appName)
- ? [...apps, { name: appName, min: false, max: false, height, width, pos: [], buttons }]
- : apps
+ apps && apps.length > 0
+ ? !apps.some(a => a.name === appName)
+ ? [...apps, { name: appName, min: false, max: false, height, width, pos: [], buttons, props }]
+ : apps
+ : [{ name: appName, min: false, max: false, height, width, pos: [], buttons, props }]
))
}
export const focus = (appName, setApps) => {
setApps(apps => {
- const i = apps.findIndex(a => a.name === appName)
+ const i = apps.length > 0 && apps.findIndex(a => a.name === appName)
return i !== apps.length - 1
? [...apps.filter((_, n) => n !== i), apps[i]]
: apps
@@ -49,7 +53,7 @@ export const move = (app, winRef, setApps) => {
const x = pageX - shiftX
const y = pageY - shiftY - 32
- setApps(apps => ([...apps.map(a => a.name === app.name
+ setApps(apps => ([...apps.map(a => a && a.name === app.name
? { ...a, pos: [x + 'px', y < 0 ? 0 : y + 'px'] }
: a
)]))
diff --git a/hooks/useSettings.js b/hooks/useSettings.js
index 44f5956..74092b2 100644
--- a/hooks/useSettings.js
+++ b/hooks/useSettings.js
@@ -16,7 +16,7 @@ export const SettingsProvider = ({ children }) => {
setData(s)
}
- const t = key => data && data.language && translations[data.language][key]
+ const t = key => data && data.language && translations && translations.en[key]
? translations[data.language][key]
? translations[data.language][key]
: translations.en[key]
diff --git a/package-lock.json b/package-lock.json
index ef1d51d..ba40bee 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -22,7 +22,9 @@
"react": "^17.0.2",
"react-dom": "^17.0.2",
"sass": "^1.35.1",
- "swr": "^1.0.0"
+ "scrape-youtube": "^2.1.9",
+ "swr": "^1.0.0",
+ "ytdl-core": "^4.9.1"
},
"devDependencies": {
"@next/eslint-plugin-next": "^11.1.2",
@@ -4204,6 +4206,18 @@
"node": ">=10"
}
},
+ "node_modules/m3u8stream": {
+ "version": "0.8.4",
+ "resolved": "https://registry.npmjs.org/m3u8stream/-/m3u8stream-0.8.4.tgz",
+ "integrity": "sha512-sco80Db+30RvcaIOndenX6E6oQNgTiBKeJbFPc+yDXwPQIkryfboEbCvXPlBRq3mQTCVPQO93TDVlfRwqpD35w==",
+ "dependencies": {
+ "miniget": "^4.0.0",
+ "sax": "^1.2.4"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/make-dir": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
@@ -4278,6 +4292,14 @@
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
},
+ "node_modules/miniget": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/miniget/-/miniget-4.2.1.tgz",
+ "integrity": "sha512-O/DduzDR6f+oDtVype9S/Qu5hhnx73EDYGyZKwU/qN82lehFZdfhoa4DT51SpsO+8epYrB3gcRmws56ROfTIoQ==",
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
@@ -5685,6 +5707,11 @@
"node": ">=8.9.0"
}
},
+ "node_modules/sax": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
+ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
+ },
"node_modules/scheduler": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz",
@@ -5694,6 +5721,11 @@
"object-assign": "^4.1.1"
}
},
+ "node_modules/scrape-youtube": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/scrape-youtube/-/scrape-youtube-2.1.9.tgz",
+ "integrity": "sha512-+quMZ0qEOhFw3YXkRf1RZh5GLHUn7MSmudEYvt/NHATMT7FvB4NHTg6N/ig9vC2mNiU1BR9MSHjtuv1aTOwauQ=="
+ },
"node_modules/semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
@@ -6590,6 +6622,19 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
+ },
+ "node_modules/ytdl-core": {
+ "version": "4.9.1",
+ "resolved": "https://registry.npmjs.org/ytdl-core/-/ytdl-core-4.9.1.tgz",
+ "integrity": "sha512-6Jbp5RDhUEozlaJQAR+l8oV8AHsx3WUXxSyPxzE6wOIAaLql7Hjiy0ZM58wZoyj1YEenlEPjEqcJIjKYKxvHtQ==",
+ "dependencies": {
+ "m3u8stream": "^0.8.3",
+ "miniget": "^4.0.0",
+ "sax": "^1.1.3"
+ },
+ "engines": {
+ "node": ">=10"
+ }
}
},
"dependencies": {
@@ -9719,6 +9764,15 @@
"yallist": "^4.0.0"
}
},
+ "m3u8stream": {
+ "version": "0.8.4",
+ "resolved": "https://registry.npmjs.org/m3u8stream/-/m3u8stream-0.8.4.tgz",
+ "integrity": "sha512-sco80Db+30RvcaIOndenX6E6oQNgTiBKeJbFPc+yDXwPQIkryfboEbCvXPlBRq3mQTCVPQO93TDVlfRwqpD35w==",
+ "requires": {
+ "miniget": "^4.0.0",
+ "sax": "^1.2.4"
+ }
+ },
"make-dir": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
@@ -9780,6 +9834,11 @@
}
}
},
+ "miniget": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/miniget/-/miniget-4.2.1.tgz",
+ "integrity": "sha512-O/DduzDR6f+oDtVype9S/Qu5hhnx73EDYGyZKwU/qN82lehFZdfhoa4DT51SpsO+8epYrB3gcRmws56ROfTIoQ=="
+ },
"minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
@@ -10845,6 +10904,11 @@
"chokidar": ">=3.0.0 <4.0.0"
}
},
+ "sax": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
+ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
+ },
"scheduler": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz",
@@ -10854,6 +10918,11 @@
"object-assign": "^4.1.1"
}
},
+ "scrape-youtube": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/scrape-youtube/-/scrape-youtube-2.1.9.tgz",
+ "integrity": "sha512-+quMZ0qEOhFw3YXkRf1RZh5GLHUn7MSmudEYvt/NHATMT7FvB4NHTg6N/ig9vC2mNiU1BR9MSHjtuv1aTOwauQ=="
+ },
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
@@ -11577,6 +11646,16 @@
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="
+ },
+ "ytdl-core": {
+ "version": "4.9.1",
+ "resolved": "https://registry.npmjs.org/ytdl-core/-/ytdl-core-4.9.1.tgz",
+ "integrity": "sha512-6Jbp5RDhUEozlaJQAR+l8oV8AHsx3WUXxSyPxzE6wOIAaLql7Hjiy0ZM58wZoyj1YEenlEPjEqcJIjKYKxvHtQ==",
+ "requires": {
+ "m3u8stream": "^0.8.3",
+ "miniget": "^4.0.0",
+ "sax": "^1.1.3"
+ }
}
}
}
diff --git a/package.json b/package.json
index b153b0f..a331983 100644
--- a/package.json
+++ b/package.json
@@ -23,7 +23,9 @@
"react": "^17.0.2",
"react-dom": "^17.0.2",
"sass": "^1.35.1",
- "swr": "^1.0.0"
+ "scrape-youtube": "^2.1.9",
+ "swr": "^1.0.0",
+ "ytdl-core": "^4.9.1"
},
"devDependencies": {
"@next/eslint-plugin-next": "^11.1.2",
diff --git a/pages/api/youtube/player.js b/pages/api/youtube/player.js
new file mode 100644
index 0000000..a6d3a7e
--- /dev/null
+++ b/pages/api/youtube/player.js
@@ -0,0 +1,32 @@
+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
+
+ if (!user || !user?.isVerified || !url) {
+ throw new Error('Something went wrong')
+ }
+
+ const info = await ytdl.getInfo(getId(url))
+
+ 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
new file mode 100644
index 0000000..b17d008
--- /dev/null
+++ b/pages/api/youtube/search.js
@@ -0,0 +1,26 @@
+import withSession from 'hocs/withSession'
+import youtube from 'scrape-youtube'
+
+export default withSession(async (req, res) => {
+ switch (req.method) {
+ case 'POST':
+ try {
+ const user = req.session.get('user')
+ const { quote } = req.body
+
+ if (!user || !user?.isVerified || !quote) {
+ throw new Error('Something went wrong')
+ }
+
+ const video = await youtube.search(quote)
+
+ res.status(200).json(video)
+ } catch (error) {
+ res.status(400).json([])
+ }
+ break
+ default:
+ res.status(400).send()
+ break
+ }
+})
diff --git a/pages/index.js b/pages/index.js
index ac36c3b..af25f7b 100644
--- a/pages/index.js
+++ b/pages/index.js
@@ -70,7 +70,7 @@ const Home = () => {
app={app}
setApps={setApps}
>
- <AppComponent />
+ <AppComponent {...app.props} />
</App>
)
})}
diff --git a/public/icons/notes.svg b/public/icons/notes.svg
index f56da12..c9525d0 100644
--- a/public/icons/notes.svg
+++ b/public/icons/notes.svg
@@ -1 +1,13 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256"><defs><pattern patternUnits="userSpaceOnUse" width="124.249" height="124.249" patternTransform="translate(-9.091 793.763) scale(2.10569)" id="a"><path fill="none" d="M0 0h124.249v124.249H0z"/><path fill="none" d="M5.811 4.423h114.895v115.401H5.811z"/><path d="M104.92 10.289c8.25.023 13.643 3.441 14.285 12.5l-.357 81.785c-.663 12.098-7.175 11.968-12.857 13.572l-51.964.178c-9.844-1.035-14.75-4.883-16.429-14.642l.893-79.822c.4-5.981 2.243-10.784 7.857-12.5l58.572-1.071z" fill="#fff" stroke="#000" stroke-width="3"/><path d="M129.92 124.11l-17.385 80.143c-2.936 11.593-9.448 10.453-15.635 11.046l-37.03-.207.844-77.663c.401-5.982 2.243-10.784 7.857-12.5l61.35-.82z" transform="translate(-22.223 -113.568)" opacity=".262"/><path d="M47.477 106.066l60.357-.252M47.098 96.09l60.357-.252M47.288 85.926l60.356-.252M46.909 75.95l60.357-.252M46.12 65.439l60.356-.253M45.741 55.464l60.357-.253M45.93 45.299l60.357-.252M45.552 35.324l60.356-.253" fill="none" stroke="#646464" stroke-width="2.5"/><path d="M14.92 80.646c-10.843-2.96-7.692-8.898-5.062-14.982l24.531-47.64c1.536-4.244 7.427-7.25 11.245-6.664l60.357-.357c3.71.86 8.788.808 4.286 7.143L82.82 71.84c-1.024 5.808-7.643 8.761-11.497 8.91l-56.403-.104z" fill="#5fd5d4" stroke="#000" stroke-width="3"/><path d="M43.313 27.915l45.713-.253M36.705 36.486l45.714-.252" fill="none" stroke="#171718" stroke-width="4"/><path transform="matrix(2.04511 0 0 1.47368 -149.542 -174.508)" d="M122.986 128.784c0 .662-.396 1.2-.884 1.2s-.884-.538-.884-1.2c0-.663.396-1.2.884-1.2s.884.537.884 1.2z" fill="#26272a"/><path d="M100.098 15.02c-7.037-7.32 3.267-13.747 4.286-6.25" fill="none" stroke="#838382" stroke-width="2"/><path transform="matrix(2.04511 0 0 1.47368 -160.01 -174.045)" d="M122.986 128.784c0 .662-.396 1.2-.884 1.2s-.884-.538-.884-1.2c0-.663.396-1.2.884-1.2s.884.537.884 1.2z" fill="#26272a"/><path d="M89.63 15.484c-7.038-7.322 3.266-13.747 4.285-6.25" fill="none" stroke="#838382" stroke-width="2"/><path transform="matrix(2.04511 0 0 1.47368 -170.746 -174.049)" d="M122.986 128.784c0 .662-.396 1.2-.884 1.2s-.884-.538-.884-1.2c0-.663.396-1.2.884-1.2s.884.537.884 1.2z" fill="#26272a"/><path d="M78.893 15.48C71.856 8.16 82.16 1.733 83.18 9.23" fill="none" stroke="#838382" stroke-width="2"/><g><path transform="matrix(2.04511 0 0 1.47368 -181.652 -173.796)" d="M122.986 128.784c0 .662-.396 1.2-.884 1.2s-.884-.538-.884-1.2c0-.663.396-1.2.884-1.2s.884.537.884 1.2z" fill="#26272a"/><path d="M67.988 15.733c-7.037-7.322 3.266-13.748 4.285-6.25" fill="none" stroke="#838382" stroke-width="2"/></g><g><path transform="matrix(2.04511 0 0 1.47368 -192.222 -173.587)" d="M122.986 128.784c0 .662-.396 1.2-.884 1.2s-.884-.538-.884-1.2c0-.663.396-1.2.884-1.2s.884.537.884 1.2z" fill="#26272a"/><path d="M57.418 15.942c-7.037-7.321 3.266-13.747 4.285-6.25" fill="none" stroke="#838382" stroke-width="2"/></g><g><path transform="matrix(2.04511 0 0 1.47368 -203.636 -173.46)" d="M122.986 128.784c0 .662-.396 1.2-.884 1.2s-.884-.538-.884-1.2c0-.663.396-1.2.884-1.2s.884.537.884 1.2z" fill="#26272a"/><path d="M46.003 16.068c-7.037-7.321 3.267-13.747 4.286-6.25" fill="none" stroke="#838382" stroke-width="2"/></g></pattern></defs><path fill="url(#a)" d="M-9.091 793.763h261.63v261.63H-9.091z" transform="translate(0 -796.362)"/></svg> \ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" version="1">
+ <path style="opacity:0.2" d="M 6,5 C 4.892,5 4,5.892 4,7 V 43 C 4,44.108 4.892,45 6,45 H 28 L 44,29 V 7 C 44,5.892 43.108,5 42,5 Z"/>
+ <path style="fill:#fecd38" d="M 6,4 C 4.892,4 4,4.892 4,6 V 42 C 4,43.108 4.892,44 6,44 H 28 L 44,28 V 6 C 44,4.892 43.108,4 42,4 Z"/>
+ <path style="fill:#ffe69b" d="M 44,28 H 30 C 28.895,28 28,28.895 28,30 V 44 Z"/>
+ <path style="opacity:0.2;fill:#ffffff" d="M 6,4 C 4.892,4 4,4.892 4,6 V 7 C 4,5.892 4.892,5 6,5 H 42 C 43.108,5 44,5.892 44,7 V 6 C 44,4.892 43.108,4 42,4 Z"/>
+ <path style="opacity:0.2;fill:#ffffff" d="M 30,28 C 28.895,28 28,28.895 28,30 V 31 C 28,29.895 28.895,29 30,29 H 43 L 44,28 Z"/>
+ <rect style="opacity:0.1" width="32" height="4" x="8" y="15" rx="2" ry="2"/>
+ <rect style="fill:#4f4f4f" width="32" height="4" x="8" y="14" rx="2" ry="2"/>
+ <rect style="opacity:0.1" width="22" height="4" x="8" y="23" rx="2" ry="2"/>
+ <rect style="fill:#4f4f4f" width="22" height="4" x="8" y="22" rx="2" ry="2"/>
+ <rect style="opacity:0.1" width="14" height="4" x="8" y="31" rx="2" ry="2"/>
+ <rect style="fill:#4f4f4f" width="14" height="4" x="8" y="30" rx="2" ry="2"/>
+</svg>
diff --git a/public/icons/player.svg b/public/icons/player.svg
new file mode 100644
index 0000000..a870027
--- /dev/null
+++ b/public/icons/player.svg
@@ -0,0 +1,10 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" version="1.1">
+ <path fill="#4f4f4f" d="m38 24a14 14 0 0 1 -14 14 14 14 0 0 1 -14 -14 14 14 0 0 1 14 -14 14 14 0 0 1 14 14z"/>
+ <path fill="#e4e4e4" d="m24 4c-11.08 0-20 8.92-20 20s8.92 20 20 20 20-8.92 20-20-8.92-20-20-20zm0 3a6 6 0 0 1 6 6 6 6 0 0 1 -6 6 6 6 0 0 1 -6 -6 6 6 0 0 1 6 -6zm-11 11a6 6 0 0 1 6 6 6 6 0 0 1 -6 6 6 6 0 0 1 -6 -6 6 6 0 0 1 6 -6zm22 0a6 6 0 0 1 6 6 6 6 0 0 1 -6 6 6 6 0 0 1 -6 -6 6 6 0 0 1 6 -6zm-11 11a6 6 0 0 1 6 6 6 6 0 0 1 -6 6 6 6 0 0 1 -6 -6 6 6 0 0 1 6 -6z"/>
+ <path fill="#4f4f4f" d="m23.366 20.9a1 1 0 0 1 -0.366 1.366 1 1 0 0 1 -1.366 -0.366 1 1 0 0 1 0.366 -1.366 1 1 0 0 1 1.366 0.366z"/>
+ <path fill="#4f4f4f" d="m26.366 26.1a1 1 0 0 1 -0.366 1.366 1 1 0 0 1 -1.366 -0.366 1 1 0 0 1 0.366 -1.366 1 1 0 0 1 1.366 0.366z"/>
+ <path fill="#4f4f4f" d="m27.098 23.3a1 1 0 0 1 -1.366 -0.366 1 1 0 0 1 0.366 -1.366 1 1 0 0 1 1.366 0.366 1 1 0 0 1 -0.366 1.366z"/>
+ <path fill="#4f4f4f" d="m21.902 26.3a1 1 0 0 1 -1.366 -0.366 1 1 0 0 1 0.366 -1.366 1 1 0 0 1 1.366 0.366 1 1 0 0 1 -0.366 1.366z"/>
+ <path fill="#fff" opacity=".2" d="m24 4c-11.08 0-20 8.92-20 20 0 0.168 0.0076 0.333 0.0117 0.5 0.2647-10.846 9.0763-19.5 19.988-19.5s19.724 8.654 19.988 19.5c0.004-0.167 0.012-0.332 0.012-0.5 0-11.08-8.92-20-20-20zm-5.9746 9.498a6 6 0 0 0 -0.025 0.502 6 6 0 0 0 6 6 6 6 0 0 0 6 -6 6 6 0 0 0 -0.025 -0.498 6 6 0 0 1 -5.975 5.498 6 6 0 0 1 -5.975 -5.502zm-11 11a6 6 0 0 0 -0.0246 0.502 6 6 0 0 0 6 6 6 6 0 0 0 6 -6 6 6 0 0 0 -0.025 -0.498 6 6 0 0 1 -5.975 5.498 6 6 0 0 1 -5.9742 -5.502zm22 0a6 6 0 0 0 -0.025 0.502 6 6 0 0 0 6 6 6 6 0 0 0 6 -6 6 6 0 0 0 -0.025 -0.498 6 6 0 0 1 -5.975 5.498 6 6 0 0 1 -5.975 -5.502zm-11 11a6 6 0 0 0 -0.025 0.502 6 6 0 0 0 6 6 6 6 0 0 0 6 -6 6 6 0 0 0 -0.025 -0.498 6 6 0 0 1 -5.975 5.498 6 6 0 0 1 -5.975 -5.502z"/>
+ <path opacity=".2" d="m24 7a6 6 0 0 0 -6 6 6 6 0 0 0 0.02539 0.4981 6 6 0 0 1 5.975 -5.498 6 6 0 0 1 5.975 5.502 6 6 0 0 0 0.025 -0.502 6 6 0 0 0 -6 -6zm-11 11a6 6 0 0 0 -6 6 6 6 0 0 0 0.0254 0.498 6 6 0 0 1 5.9746 -5.498 6 6 0 0 1 5.975 5.502 6 6 0 0 0 0.025 -0.502 6 6 0 0 0 -6 -6zm22 0a6 6 0 0 0 -6 6 6 6 0 0 0 0.02539 0.4981 6 6 0 0 1 5.975 -5.498 6 6 0 0 1 5.975 5.502 6 6 0 0 0 0.025 -0.502 6 6 0 0 0 -6 -6zm-30.988 6.5c-0.0041 0.167-0.0117 0.332-0.0117 0.5 0 11.08 8.92 20 20 20s20-8.92 20-20c0-0.1679-0.0076-0.3331-0.01172-0.5-0.264 10.846-9.076 19.5-19.988 19.5s-19.724-8.654-19.988-19.5zm19.988 4.5a6 6 0 0 0 -6 6 6 6 0 0 0 0.02539 0.4981 6 6 0 0 1 5.975 -5.498 6 6 0 0 1 5.975 5.502 6 6 0 0 0 0.025 -0.502 6 6 0 0 0 -6 -6z"/>
+</svg>
diff --git a/public/icons/youtube.svg b/public/icons/youtube.svg
new file mode 100644
index 0000000..fd56bab
--- /dev/null
+++ b/public/icons/youtube.svg
@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" version="1">
+ <path style="opacity:0.2" d="m 20.710958,38.974835 c -6.61137,-0.125659 -9.29483,-0.236612 -10.9357502,-0.452161 -1.20335,-0.158076 -1.79431,-0.312183 -2.46082,-0.641719 -1.10082,-0.544259 -1.78544,-1.303201 -2.27951,-2.526969 -0.45172,-1.118836 -0.63565,-2.007476 -0.80445,-3.886686 -0.19049,-2.120698 -0.20538,-2.504621 -0.22516,-5.80883 -0.0233,-3.88576 0.0236,-5.15909 0.28455,-7.738247 0.13684,-1.352125 0.33628,-2.250726 0.71103,-3.203634 0.34129,-0.867795 0.65288,-1.346721 1.21955,-1.874493 0.7122,-0.663311 1.49437,-1.066538 2.48179,-1.2794 1.4738402,-0.317725 8.1353702,-0.562695 15.3012502,-0.562695 7.165881,0 13.827411,0.24497 15.301251,0.562695 1.06587,0.229777 1.84815,0.654198 2.61141,1.416801 0.59738,0.596864 0.90506,1.139213 1.27121,2.240835 0.37728,1.135093 0.52197,2.139642 0.73635,5.112325 0.1018,1.411685 0.10178,7.910134 0,9.321124 -0.11911,1.650636 -0.26528,3.12057 -0.36261,3.64654 -0.25022,1.352245 -0.74166,2.618321 -1.29573,3.338169 -0.38014,0.493871 -1.13056,1.082328 -1.75807,1.378621 -0.63541,0.300025 -1.2408,0.448768 -2.21675,0.544677 -3.02421,0.297189 -12.259171,0.51418 -17.579501,0.413047 z"/>
+ <path style="fill:#f54b3e" d="m 20.710958,37.974834 c -6.61137,-0.125659 -9.29483,-0.236612 -10.9357502,-0.452161 -1.20335,-0.158076 -1.79431,-0.312183 -2.46082,-0.641719 -1.10082,-0.544259 -1.78544,-1.303201 -2.27951,-2.526969 -0.45172,-1.118836 -0.63565,-2.007476 -0.80445,-3.886686 -0.19049,-2.120698 -0.20538,-2.504621 -0.22516,-5.80883 -0.0233,-3.88576 0.0236,-5.15909 0.28455,-7.738247 0.13684,-1.352125 0.33628,-2.250726 0.71103,-3.203634 0.34129,-0.867795 0.65288,-1.346721 1.21955,-1.874493 0.7122,-0.663311 1.49437,-1.066538 2.48179,-1.2794 C 10.176028,10.24497 16.837558,10 24.003438,10 c 7.165881,0 13.827411,0.24497 15.301251,0.562695 1.06587,0.229777 1.84815,0.654198 2.61141,1.416801 0.59738,0.596864 0.90506,1.139213 1.27121,2.240835 0.37728,1.135093 0.52197,2.139642 0.73635,5.112325 0.1018,1.411685 0.10178,7.910134 0,9.321124 -0.11911,1.650636 -0.26528,3.12057 -0.36261,3.64654 -0.25022,1.352245 -0.74166,2.618321 -1.29573,3.338169 -0.38014,0.493871 -1.13056,1.082328 -1.75807,1.378621 -0.63541,0.300025 -1.2408,0.448768 -2.21675,0.544677 -3.02421,0.297189 -12.259171,0.51418 -17.579501,0.413047 z"/>
+ <path style="opacity:0.2" d="m 24.452263,28.397685 c 5.669439,-3.386921 5.547539,-3.312636 5.547539,-3.380633 0,-0.06363 -9.92357,-6.051366 -9.971956,-6.016909 -0.03268,0.02329 -0.03876,11.951433 -0.0066,11.989072 0.01208,0.01392 0.04644,0.01445 0.0763,0.0011 0.02987,-0.01327 1.98932,-1.17997 4.354226,-2.59276 z"/>
+ <path style="fill:#ffffff" d="m 24.452263,27.397687 c 5.669439,-3.386921 5.547539,-3.312636 5.547539,-3.380633 0,-0.06363 -9.92357,-6.051366 -9.971956,-6.016909 -0.03268,0.02329 -0.03876,11.951433 -0.0066,11.989072 0.01208,0.01392 0.04644,0.01445 0.0763,0.0011 0.02987,-0.01327 1.98932,-1.17997 4.354226,-2.59276 z"/>
+ <path style="fill:#ffffff;opacity:0.2" d="M 24.003906 10 C 16.838026 10 10.176965 10.244775 8.703125 10.5625 C 7.715705 10.775362 6.9329031 11.178486 6.2207031 11.841797 C 5.6540331 12.369569 5.34129 12.849002 5 13.716797 C 4.62525 14.669705 4.4259025 15.567797 4.2890625 16.919922 C 4.0281125 19.499079 3.9825594 20.772443 4.0058594 24.658203 C 4.0069096 24.833636 4.0106691 24.854007 4.0117188 25.013672 C 4.0004811 21.753034 4.0474033 20.308414 4.2890625 17.919922 C 4.4259025 16.567797 4.62525 15.669705 5 14.716797 C 5.34129 13.849002 5.6540331 13.369569 6.2207031 12.841797 C 6.9329031 12.178486 7.715705 11.775362 8.703125 11.5625 C 10.176965 11.244775 16.838026 11 24.003906 11 C 31.169787 11 37.830848 11.244775 39.304688 11.5625 C 40.370557 11.792277 41.152756 12.217866 41.916016 12.980469 C 42.513396 13.577333 42.82135 14.119081 43.1875 15.220703 C 43.56478 16.355796 43.709448 17.359348 43.923828 20.332031 C 43.970197 20.975045 43.991969 22.701878 43.996094 24.494141 C 44.001034 22.351559 43.979262 20.100747 43.923828 19.332031 C 43.709448 16.359348 43.56478 15.355796 43.1875 14.220703 C 42.82135 13.119081 42.513396 12.577333 41.916016 11.980469 C 41.152756 11.217866 40.370557 10.792277 39.304688 10.5625 C 37.830848 10.244775 31.169787 10 24.003906 10 z"/>
+</svg>
diff --git a/styles/global/_window.scss b/styles/global/_window.scss
index c489796..cf78752 100644
--- a/styles/global/_window.scss
+++ b/styles/global/_window.scss
@@ -106,20 +106,41 @@
width: 100%;
background: var(--color-window-menu);
- & > div > div {
- // cursor: pointer;
- display: inline-block;
- padding: .5em;
- transition: .3s background;
- white-space: nowrap;
+ & > div {
+ display: flex;
- @media(hover: hover) {
- &:hover {
- background-color: var(--color-window-menu-alt);
+ & > div {
+ display: inline-block;
+ padding: .5em;
+ transition: .3s background;
+ white-space: nowrap;
+
+ @media(hover: hover) {
+ &:hover {
+ background-color: var(--color-window-menu-alt);
+ }
}
}
+
+ & > span {
+ flex-grow: 1;
+ }
}
+ .off {
+ text-decoration: line-through solid var(--color-decor);
+ color: var(--color-decor);
+ }
+
+ .iconOff {
+ color: var(--color-window-menu-alt);
+ pointer-events: none;
+ }
+
+ .active {
+ color: var(--color-text-alt);
+ font-weight: 600;
+ }
@media(max-width: 40em) {
height: 3em;
@@ -201,5 +222,13 @@
& > .window__content {
top: 0;
}
+
+ .window__submenu > div {
+ @media(min-width: 40em) {
+ &:last-of-type {
+ margin-right: 5.5em;
+ }
+ }
+ }
}
}
diff --git a/styles/main/_icon.scss b/styles/main/_icon.scss
index a7c16ed..efa2ee0 100644
--- a/styles/main/_icon.scss
+++ b/styles/main/_icon.scss
@@ -1,7 +1,7 @@
.icon {
text-decoration: none;
display: inline-block;
- padding: .5em;
+ padding: 1em;
text-align: center;
outline: none;