mirror of
https://github.com/He4eT/elseifplayer.git
synced 2026-11-01 16:58:29 +00:00
Rearrange components
This commit is contained in:
parent
39e8b8a526
commit
daa26965cd
16 changed files with 14 additions and 11 deletions
|
|
@ -3,7 +3,7 @@ import { Link } from 'wouter-preact'
|
|||
|
||||
import {
|
||||
buildPlayLinkHref,
|
||||
} from '~/src/utils/utils.routing'
|
||||
} from '~/src/routing'
|
||||
|
||||
export default function GameEntry ({ name, ifdb, url }) {
|
||||
return (
|
||||
|
|
|
|||
153
src/components/Player/InputBox/InputBox.jsx
Normal file
153
src/components/Player/InputBox/InputBox.jsx
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
import { h } from 'preact'
|
||||
import { useEffect, useRef, useState } from 'preact/hooks'
|
||||
|
||||
import s from './MenuButton/MenuButton.module.scss'
|
||||
|
||||
/* eslint-disable */
|
||||
const keyCodes = {
|
||||
KEY_BACKSPACE: 8,
|
||||
KEY_TAB: 9,
|
||||
KEY_RETURN: 13,
|
||||
KEY_ESC: 27,
|
||||
KEY_PAGEUP: 33,
|
||||
KEY_PAGEDOWN: 34,
|
||||
KEY_END: 35,
|
||||
KEY_HOME: 36,
|
||||
KEY_LEFT: 37,
|
||||
KEY_UP: 38,
|
||||
KEY_RIGHT: 39,
|
||||
KEY_DOWN: 40
|
||||
}
|
||||
|
||||
const keyNames = {
|
||||
[keyCodes.KEY_BACKSPACE]: 'delete',
|
||||
[keyCodes.KEY_TAB]: 'tab',
|
||||
[keyCodes.KEY_RETURN]: 'return',
|
||||
[keyCodes.KEY_ESC]: 'escape',
|
||||
[keyCodes.KEY_PAGEUP]: 'pageup',
|
||||
[keyCodes.KEY_PAGEDOWN]: 'pagedown',
|
||||
[keyCodes.KEY_END]: 'end',
|
||||
[keyCodes.KEY_HOME]: 'home',
|
||||
[keyCodes.KEY_LEFT]: 'left',
|
||||
[keyCodes.KEY_UP]: 'up',
|
||||
[keyCodes.KEY_RIGHT]: 'right',
|
||||
[keyCodes.KEY_DOWN]: 'down'
|
||||
}
|
||||
/* eslint-enable */
|
||||
|
||||
export default function InputBox ({
|
||||
inputType,
|
||||
windows,
|
||||
currentWindowId,
|
||||
sendMessage,
|
||||
onFullscreenRequest,
|
||||
setMenuOpen,
|
||||
}) {
|
||||
const [targetWindow, setTargetWindow] = useState(null)
|
||||
const [inputText, setInputText] = useState('')
|
||||
const [lastInput, setLastInput] = useState('')
|
||||
const inputEl = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
setInputText('')
|
||||
inputEl.current && inputEl.current.focus()
|
||||
}, [inputType])
|
||||
|
||||
useEffect(() => {
|
||||
setTargetWindow(
|
||||
windows
|
||||
.find(({ id }) =>
|
||||
id === currentWindowId))
|
||||
}, [currentWindowId, windows])
|
||||
|
||||
const send = (message) => {
|
||||
sendMessage(
|
||||
message,
|
||||
inputType,
|
||||
targetWindow)
|
||||
setLastInput(message)
|
||||
setInputText('')
|
||||
}
|
||||
|
||||
const charHandler = (event) =>
|
||||
(event.keyCode === 229
|
||||
? charHandlerMobile
|
||||
: charHandlerDefault)(event)
|
||||
|
||||
const charHandlerDefault = (event) => {
|
||||
event.preventDefault()
|
||||
|
||||
const key =
|
||||
keyNames[event.keyCode] ||
|
||||
event.key
|
||||
|
||||
send(key)
|
||||
}
|
||||
|
||||
const charHandlerMobile = (event) =>
|
||||
setTimeout(() => {
|
||||
send(event.target.value.slice(-1).toUpperCase())
|
||||
setInputText('')
|
||||
})
|
||||
|
||||
const lineHandler = ({ keyCode, target: { value } }) => {
|
||||
if (keyCode === keyCodes.KEY_RETURN) {
|
||||
send(value)
|
||||
}
|
||||
}
|
||||
|
||||
const lineArrowHandler = ({ keyCode }) => {
|
||||
if (keyCode === keyCodes.KEY_UP) {
|
||||
setInputText(lastInput)
|
||||
|
||||
setTimeout(() => {
|
||||
const end = lastInput.length
|
||||
inputEl.current.setSelectionRange(end, end)
|
||||
}, 0)
|
||||
}
|
||||
if (keyCode === keyCodes.KEY_DOWN) {
|
||||
setInputText('')
|
||||
}
|
||||
}
|
||||
|
||||
const inputHandlers = {
|
||||
char: {
|
||||
maxlength: '1',
|
||||
placeholder: 'Press any key here',
|
||||
onKeyDown: charHandler,
|
||||
},
|
||||
line: {
|
||||
placeholder: ' > ',
|
||||
onKeyDown: lineArrowHandler,
|
||||
onKeyPress: lineHandler,
|
||||
},
|
||||
finished: {
|
||||
placeholder: 'The program has finished',
|
||||
disabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
return (
|
||||
<section className='inputControls'>
|
||||
<input {...inputHandlers[inputType]}
|
||||
className='inputBox'
|
||||
ref={inputEl}
|
||||
value={inputText}
|
||||
autofocus
|
||||
autocomplete='off'
|
||||
spellCheck='false'
|
||||
autocapitalize='off'
|
||||
autocorrect='off'
|
||||
onDblClick={onFullscreenRequest}
|
||||
onInput={({ target: { value } }) => setInputText(value)}
|
||||
type='search' />
|
||||
<button
|
||||
aria-label='Menu'
|
||||
className={s.menuButton}
|
||||
onClick={() => setMenuOpen(true)}
|
||||
>
|
||||
=
|
||||
</button>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
.menuButton {
|
||||
border-left: none;
|
||||
outline-offset: -8px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
aspect-ratio: 1;
|
||||
|
||||
&:focus-visible {
|
||||
outline-offset: -4px;
|
||||
}
|
||||
}
|
||||
41
src/components/Player/MenuOverlay/MenuOverlay.css
Normal file
41
src/components/Player/MenuOverlay/MenuOverlay.css
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
.menu {
|
||||
width: 100%;
|
||||
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
text-align: center;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
|
||||
background-color: var(--bg-color);
|
||||
border-color: var(--main-color);
|
||||
color: var(--main-color);
|
||||
}
|
||||
|
||||
.menu::backdrop {
|
||||
background: none;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.menu > section {
|
||||
margin: 32px auto 40px;
|
||||
gap: 32px;
|
||||
max-width: 250px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.menu .navigation {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.menu .appearance {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.menu select,
|
||||
.menu button {
|
||||
width: 100%;
|
||||
}
|
||||
78
src/components/Player/MenuOverlay/MenuOverlay.jsx
Normal file
78
src/components/Player/MenuOverlay/MenuOverlay.jsx
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import { useEffect, useRef } from 'preact/hooks'
|
||||
import { Link } from 'wouter-preact'
|
||||
|
||||
import ThemeSelector from
|
||||
'~/src/components/ThemeSelector/ThemeSelector'
|
||||
|
||||
import './MenuOverlay.css'
|
||||
|
||||
export default function MenuOverlay ({
|
||||
themeEngine, onFullscreenRequest, menuOpen, setMenuOpen,
|
||||
}) {
|
||||
const dialog = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
const dialogOpen = dialog.current.open
|
||||
|
||||
if (menuOpen && !dialogOpen) {
|
||||
dialog.current.showModal()
|
||||
}
|
||||
|
||||
if (!menuOpen && dialogOpen) {
|
||||
dialog.current.close()
|
||||
}
|
||||
}, [menuOpen])
|
||||
|
||||
useEffect(() => {
|
||||
const closeHandler = () => {
|
||||
setMenuOpen(false)
|
||||
}
|
||||
|
||||
dialog.current.addEventListener('close', closeHandler)
|
||||
return () => dialog.current.removeEventListener('close', closeHandler)
|
||||
}, [dialog])
|
||||
|
||||
|
||||
return (
|
||||
<dialog ref={dialog} className='menu'>
|
||||
<section>
|
||||
<div>
|
||||
<button
|
||||
tabIndex={0}
|
||||
onClick={() => dialog.current.close()}
|
||||
>
|
||||
Close this menu
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className='appearance'>
|
||||
<button
|
||||
onClick={() => {
|
||||
dialog.current.close()
|
||||
onFullscreenRequest()
|
||||
}}
|
||||
>
|
||||
Full screen
|
||||
</button>
|
||||
<button
|
||||
onClick={() => themeEngine.setRandomTheme()}
|
||||
>
|
||||
Set a random theme
|
||||
</button>
|
||||
<label>
|
||||
Current theme:
|
||||
<ThemeSelector {...{
|
||||
themeEngine,
|
||||
}} />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className='navigation'>
|
||||
<Link href="/" tabIndex={0}>
|
||||
ElseIfPlayer
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</dialog>
|
||||
)
|
||||
}
|
||||
79
src/components/Player/OutputBox/GridBuffer.jsx
Normal file
79
src/components/Player/OutputBox/GridBuffer.jsx
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import { h } from 'preact'
|
||||
import { useEffect, useState } from 'preact/hooks'
|
||||
|
||||
import TextMessage from './TextMessage'
|
||||
|
||||
export default function GridBuffer ({ inbox, currentWindow }) {
|
||||
const [prevMessages, setPrevMessages] = useState([])
|
||||
const [messages, setMessages] = useState([])
|
||||
|
||||
useEffect(() => {
|
||||
const currentInboxObj =
|
||||
inbox.find(({ id }) =>
|
||||
id === currentWindow.id)
|
||||
|
||||
const currentInbox = currentInboxObj?.lines ?? []
|
||||
|
||||
const newOrPrev = (cur, prev) => (i) => {
|
||||
const byId = (list, i) =>
|
||||
list.find(({ line }) => line === i)
|
||||
|
||||
return byId(cur, i) || byId(prev, i)
|
||||
}
|
||||
|
||||
const rawMessages =
|
||||
Array(currentWindow.gridheight)
|
||||
.fill(null)
|
||||
.map((_, i) => i)
|
||||
.map(newOrPrev(currentInbox, prevMessages))
|
||||
|
||||
/* */
|
||||
|
||||
const shouldUpdatePrev = (rawMessages, prevMessages) => {
|
||||
const serialize = JSON.stringify
|
||||
return serialize(rawMessages) !== serialize(prevMessages)
|
||||
}
|
||||
|
||||
if (shouldUpdatePrev(rawMessages, prevMessages)) {
|
||||
setPrevMessages(rawMessages)
|
||||
}
|
||||
|
||||
/* */
|
||||
|
||||
|
||||
const rawMessagesContent =
|
||||
rawMessages
|
||||
.map((x) => x.content)
|
||||
.flat()
|
||||
.map((message) => ({
|
||||
...message,
|
||||
text: message.text.trim(),
|
||||
}))
|
||||
|
||||
const isEmpty =
|
||||
rawMessagesContent
|
||||
.map(({text}) => text.length)
|
||||
.every((l) => l === 0)
|
||||
|
||||
const getGridStyle = ({style}) => {
|
||||
if (['alert', 'normal'].includes(style)) return 'grid'
|
||||
else return style || 'grid'
|
||||
}
|
||||
|
||||
const messages =
|
||||
rawMessagesContent
|
||||
.map((message) => ({
|
||||
style: getGridStyle(message),
|
||||
text: message.text.replace(' ', ' / '),
|
||||
}))
|
||||
|
||||
setMessages(isEmpty ? [] : messages)
|
||||
}, [inbox, currentWindow, prevMessages])
|
||||
|
||||
return (
|
||||
<section
|
||||
className='buffer gridBuffer'>
|
||||
{messages.map(TextMessage)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
93
src/components/Player/OutputBox/TextBuffer.jsx
Normal file
93
src/components/Player/OutputBox/TextBuffer.jsx
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import { h } from 'preact'
|
||||
import { useEffect, useRef, useState } from 'preact/hooks'
|
||||
|
||||
import TextMessage from './TextMessage'
|
||||
|
||||
const isFakeStatus = (w) =>
|
||||
w.height < 5
|
||||
|
||||
const trimInputPrompt = (messages) =>
|
||||
messages.length < 1
|
||||
? messages
|
||||
: messages.slice(-1)[0].text === '>'
|
||||
? messages.slice(0, messages.length - 1)
|
||||
: messages
|
||||
|
||||
const parseInbox = (inbox, currentWindow) => {
|
||||
const currentInbox =
|
||||
inbox.find(({ id }) =>
|
||||
id === currentWindow.id)
|
||||
|
||||
if (!currentInbox) {
|
||||
return {
|
||||
clear: false,
|
||||
incoming: [],
|
||||
}
|
||||
}
|
||||
|
||||
const { text: inboxMessagesRaw } =
|
||||
currentInbox
|
||||
|
||||
const eol = { style: 'endOfLine' }
|
||||
|
||||
const incoming =
|
||||
inboxMessagesRaw
|
||||
/* Normalize. */
|
||||
.map(({ content }) =>
|
||||
content
|
||||
? [...trimInputPrompt(content), eol]
|
||||
: [eol])
|
||||
/* Flatten. */
|
||||
.reduce((acc, x) =>
|
||||
acc.concat(x), [])
|
||||
|
||||
return {
|
||||
incoming,
|
||||
clear: isFakeStatus(currentWindow)
|
||||
? true
|
||||
: currentInbox.clear,
|
||||
}
|
||||
}
|
||||
|
||||
export default function TextBuffer ({ inbox, currentWindow }) {
|
||||
const [messages, setMessages] = useState([])
|
||||
const textBufferEl = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
const { incoming, clear } =
|
||||
parseInbox(inbox, currentWindow)
|
||||
|
||||
setMessages((messages) => clear
|
||||
? incoming
|
||||
: messages.concat(incoming))
|
||||
|
||||
setTimeout(() => {
|
||||
const inputs =
|
||||
textBufferEl.current.querySelectorAll('.message.input')
|
||||
const lastInput =
|
||||
inputs[inputs.length - 1]
|
||||
|
||||
textBufferEl.current.scrollTo({
|
||||
top: lastInput
|
||||
? lastInput.offsetTop
|
||||
: textBufferEl.current.scrollHeight,
|
||||
behavior: 'smooth',
|
||||
})
|
||||
}, 0)
|
||||
}, [currentWindow, inbox])
|
||||
|
||||
const classes = [
|
||||
isFakeStatus(currentWindow)
|
||||
? 'gridBuffer'
|
||||
: 'textBuffer',
|
||||
'buffer'].join(' ')
|
||||
|
||||
return (
|
||||
<section
|
||||
tabindex='0'
|
||||
ref={textBufferEl}
|
||||
className={classes}>
|
||||
{messages.map(TextMessage)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
16
src/components/Player/OutputBox/TextMessage.jsx
Normal file
16
src/components/Player/OutputBox/TextMessage.jsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { h } from 'preact'
|
||||
|
||||
export default function TextMessage ({ style, text }) {
|
||||
const defaultContent = (
|
||||
<span className={['message', style].join(' ')}>
|
||||
{text}
|
||||
</span>)
|
||||
|
||||
return ({
|
||||
grid: (text?.length > 0 ? <div>{text}</div> : <br />),
|
||||
input: (<span className='message input'>> {text}</span>),
|
||||
subheader: (<strong className='message subheader'>{text}</strong>),
|
||||
emphasized: (<em className='message emphasized'>{text}</em>),
|
||||
endOfLine: (<br />),
|
||||
})[style] || defaultContent
|
||||
}
|
||||
|
|
@ -3,13 +3,16 @@ import { useState, useEffect } from 'preact/hooks'
|
|||
|
||||
import CheapGlkOte from 'cheap-glkote'
|
||||
|
||||
import TextBuffer from './TextBuffer'
|
||||
import GridBuffer from './GridBuffer'
|
||||
import TextBuffer from './OutputBox/TextBuffer'
|
||||
import GridBuffer from './OutputBox/GridBuffer'
|
||||
|
||||
import InputBox from './InputBox'
|
||||
import Status from './Status'
|
||||
import InputBox from './InputBox/InputBox'
|
||||
import Status from './Status/Status'
|
||||
|
||||
import { Handlers, unhandledRejectionHandler } from './playerHandlers'
|
||||
import {
|
||||
Handlers,
|
||||
unhandledRejectionHandler
|
||||
} from './common/playerHandlers'
|
||||
|
||||
import './player.css'
|
||||
|
||||
|
|
|
|||
31
src/components/Player/Status/Status.jsx
Normal file
31
src/components/Player/Status/Status.jsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { h } from 'preact'
|
||||
import { Link } from 'wouter-preact'
|
||||
|
||||
const fail = (details) => (
|
||||
<div className='status fail'>
|
||||
<h1>
|
||||
Error
|
||||
</h1>
|
||||
{details.map((x) => (<p key={x}>{x}</p>))}
|
||||
<hr />
|
||||
<Link href='/'>
|
||||
Home
|
||||
</Link>
|
||||
|
|
||||
<a
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
href='https://github.com/He4eT/elseifplayer/issues'>
|
||||
Report bug
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
|
||||
const loading = (details) => (
|
||||
<div className='status loading'>
|
||||
{details.map((x) => (<div key={x}>{x}</div>))}
|
||||
</div>
|
||||
)
|
||||
|
||||
export default ({ stage, details }) =>
|
||||
({ fail, loading })[stage](details)
|
||||
|
|
@ -4,7 +4,7 @@ import { useState, useEffect } from 'preact/hooks'
|
|||
import { engineByFilename } from './common/engines'
|
||||
|
||||
import Player from './Player'
|
||||
import Status from './Status'
|
||||
import Status from './Status/Status'
|
||||
|
||||
const INITIAL_STATUS = {
|
||||
stage: 'loading',
|
||||
|
|
|
|||
57
src/components/Player/common/playerHandlers.js
Normal file
57
src/components/Player/common/playerHandlers.js
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { encode, decode } from 'base32768'
|
||||
|
||||
export const Handlers = ({
|
||||
setStatus,
|
||||
setWindows,
|
||||
setCurrentWindowId,
|
||||
setInputType,
|
||||
setInbox,
|
||||
}) => ({
|
||||
onInit: () => {
|
||||
setStatus({ stage: 'ready' })
|
||||
},
|
||||
/* */
|
||||
onUpdateWindows: (windows) => {
|
||||
setWindows(windows)
|
||||
},
|
||||
onUpdateInputs: (data) => {
|
||||
if (data.length === 0) return null
|
||||
|
||||
const { type, id } = data[0]
|
||||
setCurrentWindowId(id)
|
||||
setInputType(type)
|
||||
},
|
||||
onUpdateContent: (inbox) => {
|
||||
setInbox(inbox)
|
||||
},
|
||||
onDisable: () => {
|
||||
setInputType(null)
|
||||
},
|
||||
/* */
|
||||
onFileNameRequest: (_tosave, usage, _gameId, setFileName) => {
|
||||
setFileName({
|
||||
usage,
|
||||
filename: prompt('Enter the filename'),
|
||||
})
|
||||
},
|
||||
onFileRead: ({ filename }) => {
|
||||
const content = localStorage.getItem(`fake-fs/${filename}`)
|
||||
return decode(content)
|
||||
},
|
||||
onFileWrite: ({ filename }, content) => {
|
||||
localStorage.setItem(`fake-fs/${filename}`, encode(content))
|
||||
},
|
||||
/* */
|
||||
onExit: () => {
|
||||
setInputType('finished')
|
||||
},
|
||||
})
|
||||
|
||||
export const unhandledRejectionHandler = (onExit) => (event) => {
|
||||
if (event.reason.name === 'ExitStatus' || event.reason.message === 'Program terminated with exit(0)') {
|
||||
onExit()
|
||||
} else {
|
||||
console.error('Unhandled rejection (promise: ', event.promise, ', reason: ', event.reason, ').')
|
||||
}
|
||||
event.preventDefault()
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ import { Route, Router, Switch } from 'wouter-preact'
|
|||
import {
|
||||
useHashLocation,
|
||||
extractView,
|
||||
} from '~/src/utils/utils.routing'
|
||||
} from './routing'
|
||||
import {
|
||||
useThemeEngine,
|
||||
} from '~/src/themes/themes'
|
||||
|
|
|
|||
33
src/routing.js
Normal file
33
src/routing.js
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import {
|
||||
useState, useEffect, useCallback,
|
||||
} from 'preact/hooks'
|
||||
|
||||
export const useHashLocation = () => {
|
||||
const currentLoc = () =>
|
||||
window.location.hash.replace('#', '') || '/'
|
||||
|
||||
const [loc, setLoc] = useState(currentLoc())
|
||||
|
||||
useEffect(() => {
|
||||
const handler = () => {
|
||||
setLoc(currentLoc())
|
||||
window.scrollTo(0, 0)
|
||||
}
|
||||
|
||||
handler()
|
||||
window.addEventListener('hashchange', handler)
|
||||
return () => window.removeEventListener('hashchange', handler)
|
||||
}, [])
|
||||
|
||||
const navigate = useCallback((to) =>
|
||||
(window.location.hash = to.replace('#/', '')), [])
|
||||
return [loc, navigate]
|
||||
}
|
||||
|
||||
export const buildPlayLinkHref = ({ url }) =>
|
||||
`/#/play/${encodeURIComponent(url)}`
|
||||
|
||||
export const extractView = (location) => {
|
||||
const currentView = location.split('/').filter(Boolean)[0]
|
||||
return currentView || ''
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ import { Link } from 'wouter-preact'
|
|||
import {
|
||||
useHashLocation,
|
||||
buildPlayLinkHref,
|
||||
} from '~/src/utils/utils.routing'
|
||||
} from '~/src/routing'
|
||||
|
||||
import LocalFileSelector from
|
||||
'~/src/components/FileSelector/LocalFileSelector'
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { h } from 'preact'
|
|||
import { useState, useEffect } from 'preact/hooks'
|
||||
|
||||
import UrlPlayer from '~/src/components/Player/UrlPlayer'
|
||||
import MenuOverlay from '~/src/components/Player/MenuOverlay'
|
||||
import MenuOverlay from '~/src/components/Player/MenuOverlay/MenuOverlay'
|
||||
|
||||
import './PlayerView.css'
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue