aboutsummaryrefslogtreecommitdiffstats
path: root/apps
diff options
context:
space:
mode:
Diffstat (limited to 'apps')
-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
12 files changed, 463 insertions, 16 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'