mirror of
https://github.com/He4eT/elseifplayer.git
synced 2026-05-05 01:17:22 +00:00
Rearrange components
This commit is contained in:
parent
e44831db08
commit
90f630f277
13 changed files with 10 additions and 8 deletions
92
src/components/Player/InputBox.jsx
Normal file
92
src/components/Player/InputBox.jsx
Normal 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' />
|
||||
)
|
||||
}
|
||||
120
src/components/Player/Player.jsx
Normal file
120
src/components/Player/Player.jsx
Normal 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>)
|
||||
}
|
||||
76
src/components/Player/TextBuffer.jsx
Normal file
76
src/components/Player/TextBuffer.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
14
src/components/Player/TextMessage.jsx
Normal file
14
src/components/Player/TextMessage.jsx
Normal 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'>> {text}</span>),
|
||||
subheader: (<strong>{text}</strong>),
|
||||
endOfLine: (<br />)
|
||||
})[style] || defaultContent
|
||||
}
|
||||
51
src/components/Player/UrlPlayer.jsx
Normal file
51
src/components/Player/UrlPlayer.jsx
Normal 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>)
|
||||
}
|
||||
38
src/components/Player/common/engines.js
Normal file
38
src/components/Player/common/engines.js
Normal 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}`)
|
||||
}
|
||||
}
|
||||
21
src/components/Player/common/if.js
Normal file
21
src/components/Player/common/if.js
Normal 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)
|
||||
52
src/components/Player/player.css
Normal file
52
src/components/Player/player.css
Normal 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;
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue