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 + 12 files changed, 463 insertions(+), 16 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 (limited to 'apps') 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' -- cgit v1.2.3