From 16dab011c575eaf96630cab406ec2d8086403d0b Mon Sep 17 00:00:00 2001 From: piotrruss Date: Sun, 12 Sep 2021 23:11:25 +0200 Subject: added youtube & player apps --- apps/Notes/components/List.js | 32 ++++---- apps/Player/components/App.js | 107 +++++++++++++++++++++++++++ apps/Player/components/Buttons.js | 34 +++++++++ apps/Player/components/Video.js | 32 ++++++++ apps/Player/index.js | 3 + apps/Player/styles/Player.module.scss | 1 + apps/Player/styles/_player.scss | 70 ++++++++++++++++++ apps/Youtube/components/App.js | 126 ++++++++++++++++++++++++++++++++ apps/Youtube/index.js | 3 + apps/Youtube/styles/Youtube.module.scss | 1 + apps/Youtube/styles/_results.scss | 68 +++++++++++++++++ apps/index.js | 2 + components/App.js | 10 +-- components/Header.js | 6 +- components/Layout.js | 6 +- configs/appList.js | 6 +- configs/translations.js | 54 ++++++++++++-- helpers/windowActions.js | 18 +++-- hooks/useSettings.js | 2 +- package-lock.json | 81 +++++++++++++++++++- package.json | 4 +- pages/api/youtube/player.js | 32 ++++++++ pages/api/youtube/search.js | 26 +++++++ pages/index.js | 2 +- public/icons/notes.svg | 14 +++- public/icons/player.svg | 10 +++ public/icons/youtube.svg | 7 ++ styles/global/_window.scss | 47 +++++++++--- styles/main/_icon.scss | 2 +- 29 files changed, 749 insertions(+), 57 deletions(-) create mode 100644 apps/Player/components/App.js create mode 100644 apps/Player/components/Buttons.js create mode 100644 apps/Player/components/Video.js create mode 100644 apps/Player/index.js create mode 100644 apps/Player/styles/Player.module.scss create mode 100644 apps/Player/styles/_player.scss create mode 100644 apps/Youtube/components/App.js create mode 100644 apps/Youtube/index.js create mode 100644 apps/Youtube/styles/Youtube.module.scss create mode 100644 apps/Youtube/styles/_results.scss create mode 100644 pages/api/youtube/player.js create mode 100644 pages/api/youtube/search.js create mode 100644 public/icons/player.svg create mode 100644 public/icons/youtube.svg 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 = () => { { - notes.length > 0 - ? (notes.sort(sortFn).map(note => ( - - ))) - : ( - - {t('notes_list_empty')} - - ) -} + notes.length > 0 + ? (notes.sort(sortFn).map(note => ( + + ))) + : ( + + {t('notes_list_empty')} + + ) + } 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 + + return ( + <> +
+
+
{ setShowPlaylist(p => !p) }} + className={current ? 'active' : null} + > + {t('player_playlist_default')} +
+
{}}>+
+ +
+
+
+
+ {current !== null && ( +
+
+
    + { + playlist && playlist.length > 0 + ? ( + playlist.map((item, i) => ( +
  • { setCurrent(i) }} + className={current === i ? styles.activeItem : ''} + key={item.id} + > + {(i + 1) + '.'} + {item.title} +
  • + )) + ) + : ( +
  • {t('player_playlist_empty')}
  • + ) + } +
+
setShowPlaylist(false)}><
+
+
+ + ) +} + +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 }) => ( + <> + +
0 ? '' : 'iconOff'} + onClick={() => { current !== null && current > 0 && setCurrent(c => c - 1) }} + > + +
+
{ current !== null && setCurrent(null) }} + > + +
+
{ current === null && setCurrent(0) }} + > + +
+
{ current !== null && current < playlist.length - 1 && setCurrent(c => c + 1) }} + > + +
+ +) + +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 + ? ( + + ) + : ( + + ) + ) +} + +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 ( + <> +
+
+ {[ + 'youtube_videos', + 'youtube_music', + 'youtube_live', + 'youtube_channels', + 'youtube_playlists' + ].map(y => ( +
{ setType(y); setResults() }} + key={y} + > + {t(y)} +
+ ))} + +
{ setEnqueue(e => !e) }} + > + {t('youtube_enqueue')} +
+
+
+
+
+ + +
+
+ { + searching + ? ( + + ) + : ( +
    + { + results && results.length > 0 && results.map(r => ( +
  • handlePlay(r.link)}> + +

    {time(r.duration)}

    +
    +

    {r.title}

    +

    Views: {r.views}, uploaded: {r.uploaded || '-'}

    +

    {r.channel?.name}

    +
    +
  • + )) + } + {sending && } +
+ ) + } +
+
+ + ) +} + +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 (
{
{children}
{app.buttons.includes('min') && ( - toggleMin(app.name, setApps)}> + { e.preventDefault(); e.stopPropagation(); toggleMin(app.name, setApps) }}> )} {app.buttons.includes('max') && !forceMax && ( - toggleMax(app.name, setApps)}> + { e.preventDefault(); e.stopPropagation(); toggleMax(app.name, setApps) }}> )} {app.buttons.includes('close') && ( - close(app.name, setApps)}> + { e.preventDefault(); e.stopPropagation(); close(app.name, setApps) }}> )} 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 }) => { > 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 = ({
{children}
-
+
) 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} > - + ) })} 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 @@ - \ No newline at end of file + + + + + + + + + + + + + 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 @@ + + + + + + + + + + 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 @@ + + + + + + + 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; -- cgit v1.2.3