Rearrange components

This commit is contained in:
He4eT 2021-03-02 16:48:24 +05:00
commit 90f630f277
13 changed files with 10 additions and 8 deletions

View file

@ -0,0 +1,92 @@
import { h } from 'preact'
import { useEffect, useRef, useState } from 'preact/hooks'
/* 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 ({ currentWindow, inputType, sendMessage }) {
const [inputText, setInputText] = useState('')
const inputEl = useRef(null)
useEffect(() => {
inputEl.current && inputEl.current.focus()
}, [inputType])
const send = x => {
sendMessage(x, currentWindow)
setInputText('')
}
const charHandler = event => {
event.preventDefault()
const key =
keyNames[event.keyCode] ||
event.key
send(key)
}
const lineHandler = ({ keyCode, target: { value } }) => {
if (keyCode === keyCodes.KEY_RETURN) {
send(value)
}
}
const inputHandlers = {
char: {
onKeyDown: charHandler
},
line: {
onKeyPress: lineHandler
}
}
const placeholder = {
char: 'Press any key',
line: ' > '
}
const enterFullscreen = _ =>
document.documentElement.requestFullscreen()
return (
<input {...inputHandlers[inputType]}
className='inputBox'
ref={inputEl}
value={inputText}
placeholder={placeholder[inputType]}
autofocus
autocomplete='off'
onDblClick={enterFullscreen}
onInput={({ target: { value } }) => setInputText(value)}
type='search' />
)
}

View file

@ -0,0 +1,120 @@
import { h } from 'preact'
import { useState, useEffect } from 'preact/hooks'
import {
compressToUTF16 as encode,
decompressFromUTF16 as decode
} from 'lz-string'
import CheapGlkOte from 'cheap-glkote'
import TextBuffer from './TextBuffer'
import InputBox from './InputBox'
import './player.css'
const INITIAL_STATUS = {
stage: 'loading',
details: 'Preparing...'
}
const runMachine = ({ Engine, file, handlers }) => {
console.log('runMachine')
const vm = new Engine()
const { glkInterface, sendFn } = CheapGlkOte(handlers)
vm.prepare(file, glkInterface)
vm.start()
return { sendFn, instance: vm }
}
const Handlers = ({
setStatus,
setCurrentWindow,
setInputType,
setInbox
}) => ({
onInit: _ => setStatus({ stage: 'ready' }),
/* */
onUpdateWindows: windows => {
setCurrentWindow(windows
.filter(x => x.type === 'buffer')
.slice(-1)[0])
},
onUpdateInputs: setInputType,
onUpdateContent: setInbox,
onDisable: _ => setInputType(null),
/* */
onFileNameRequest: (tosave, usage, _, setFileName) => {
setFileName({
usage,
filename: tosave ? 'save' : 'load'
})
},
onFileRead: ({ filename }) => {
if (filename === 'save') return null
const lsName = prompt('Enter the name of the saved file:')
const record = localStorage.getItem(`save-${lsName}`)
return decode(record)
},
onFileWrite: ({ filename }, content) => {
const lsName = prompt('Select a name for the saved file:')
const record = encode(content)
localStorage.setItem(`save-${lsName}`, record)
},
/* */
onExit: _ => setInputType(null)
})
export default function ({ vmParts: { file, engine } }) {
const [status, setStatus] = useState(INITIAL_STATUS)
const [currentWindow, setCurrentWindow] = useState(null)
const [inputType, setInputType] = useState(null)
const [inbox, setInbox] = useState([])
const [vm, setVm] = useState(null)
const [sendMessage, setSendMessage] = useState(null)
useEffect(() => {
const handlers = Handlers({
setStatus,
setCurrentWindow,
setInputType,
setInbox
})
const vm = runMachine({
Engine: engine,
file,
handlers
})
setVm(vm)
}, [file, engine])
useEffect(() => {
setSendMessage(_ => vm
? vm.sendFn
: null)
}, [vm])
return status.stage !== 'ready'
? (<div>{status.details}</div>)
: (
<section className='ifplayer'>
<TextBuffer {...{
inbox,
currentWindow
}} />
<InputBox {...{
currentWindow,
inputType,
sendMessage
}} />
</section>)
}

View file

@ -0,0 +1,76 @@
import { h } from 'preact'
import { useEffect, useRef, useState } from 'preact/hooks'
import TextMessage from './TextMessage'
const trimImputPrompt = 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 { clear, text: inboxMessagesRaw } =
currentInbox
const eol = { style: 'endOfLine' }
const incoming =
inboxMessagesRaw
/* Normalize. */
.map(({ content }) =>
content
? [...trimImputPrompt(content), eol]
: [eol])
/* Flatten. */
.reduce((acc, x) =>
acc.concat(x), [])
return { clear, incoming }
}
export default function ({ inbox, currentWindow }) {
const [messages, setMessages] = useState([])
const textBufferEl = useRef(null)
useEffect(() => {
const { incoming, clear } =
parseInbox(inbox, currentWindow)
setMessages(clear
? incoming
: messages.concat(incoming))
setTimeout(() => {
const inputs =
textBufferEl.current.querySelectorAll('.message.input')
const lastInput =
inputs[inputs.length - 1]
textBufferEl.current.scrollTop =
lastInput
? lastInput.offsetTop
: textBufferEl.current.scrollHeight * 2
}, 0)
}, [inbox])
return (
<section
ref={textBufferEl}
className='textBuffer'>
{messages.map(TextMessage)}
</section>
)
}

View file

@ -0,0 +1,14 @@
import { h } from 'preact'
export default function ({ style, text }) {
const defaultContent = (
<span class={['message', style].join(' ')}>
{text}
</span>)
return ({
input: (<span class='message input'>&gt; {text}</span>),
subheader: (<strong>{text}</strong>),
endOfLine: (<br />)
})[style] || defaultContent
}

View file

@ -0,0 +1,51 @@
import { h } from 'preact'
import { useState, useEffect } from 'preact/hooks'
import { engineByFilename } from './common/engines'
import Player from './Player'
const INITIAL_STATUS = {
stage: 'loading',
details: 'Loading...'
}
const prepareVM = ({ url, setStatus, setParts }) => {
const st = (stage, details) => args => {
setStatus({ stage, details })
return args
}
return Promise.resolve()
.then(st('loading', 'Downloading file...'))
.then(_ => fetch(url))
.then(st('loading', 'Processing file...'))
.then(response => response.arrayBuffer())
.then(arrayBuffer => new Uint8Array(arrayBuffer))
.then(st('loading', 'Downloading engine...'))
.then(file => setParts({
file,
engine: engineByFilename(url)
}))
.then(st('loading', 'Running...'))
.catch(e => {
console.error(e)
setStatus({ stage: 'fail', details: e.message })
})
}
export default function ({ url }) {
const [status, setStatus] = useState(INITIAL_STATUS)
const [vmParts, setParts] = useState(null)
useEffect(() => {
setStatus(INITIAL_STATUS)
setParts(null)
prepareVM({ url, setStatus, setParts })
}, [url])
return vmParts
? (<Player vmParts={vmParts} />)
: (<div>{status.details}</div>)
}

View file

@ -0,0 +1,38 @@
import bocfel from 'emglken/src/bocfel.js'
import git from 'emglken/src/git.js'
import hugo from 'emglken/src/hugo.js'
import tads from 'emglken/src/tads.js'
const formats = [
{
id: 'bocfel',
extensions: /z([3458]|blorb)$/,
engine: bocfel
},
{
id: 'git',
extensions: /(gblorb|ulx)$/,
engine: git
},
{
id: 'hugo',
extensions: /hex$/,
engine: hugo
},
{
id: 'tads',
extensions: /(gam|t3)$/,
engine: tads
}
]
export const engineByFilename = filename => {
const format = formats.find(x =>
x.extensions.test(filename))
if (format) {
return format.engine
} else {
throw new Error(`Unsupported file type: ${filename}`)
}
}

View file

@ -0,0 +1,21 @@
// import CheapGlkOte from 'cheap-glkote'
// import engine from 'emglken/src/tads.js'
// import { engineByFilename } from './engines'
// export const fetchGameFile = url => fetch(url)
// .then(response => (console.log(response), response))
// .then(response => response.blob())
// .then(blob => new Response(blob).arrayBuffer())
// .then(buffer => new Uint8Array(buffer))
// .then(file => {
// const { glkInterface, sendFn } = CheapGlkOte({
// onUpdateContent: messages => console.log(messages)
// })
// window.send = sendFn
// const vm = new engine()
// vm.prepare(file, glkInterface)
// vm.start()
// })
// .catch(console.log)

View file

@ -0,0 +1,52 @@
.ifplayer {
height: 100%;
display: flex;
flex-direction: column;
box-sizing: border-box;
background-color: var(--bg-color);
color: var(--main-color);
padding: var(--outer-padding);
}
.inputBox {
flex: 0 1 auto;
font: inherit;
color: inherit;
outline: 0;
background-color: var(--bg-color);
border: var(--border-width) solid var(--main-color);
padding: var(--inner-padding);
margin-top: var(--input-box-margin);
}
.textBuffer {
flex: 2 1 auto;
overflow-y: scroll;
box-sizing: border-box;
border: var(--border-width) solid var(--main-color);
padding: var(--inner-padding);
scrollbar-color: var(--main-color) var(--bg-color);
scrollbar-width: thin;
}
.textBuffer::-webkit-scrollbar {
width: 8px;
}
.textBuffer::-webkit-scrollbar-thumb {
background-color: var(--main-color);
border: 4px solid var(--bg-color);
border-left-width: 0px;
}
.textBuffer > br:first-child,
.textBuffer > br:last-child,
.textBuffer > br + br + br {
display: none;
}