Rearrange components

This commit is contained in:
He4eT 2023-06-03 22:50:37 +03:00 • committed by Alexey
commit daa26965cd
16 changed files with 14 additions and 11 deletions

View file

@ -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 (

View 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>
)
}

View file

@ -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;
}
}

View 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%;
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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'>&gt; {text}</span>),
subheader: (<strong className='message subheader'>{text}</strong>),
emphasized: (<em className='message emphasized'>{text}</em>),
endOfLine: (<br />),
})[style] || defaultContent
}

View file

@ -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'

View 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)

View file

@ -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',

View 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()
}

View file

@ -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
View 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 || ''
}

View file

@ -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'

View file

@ -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'