mirror of
https://github.com/He4eT/elseifplayer.git
synced 2026-05-05 01:17:22 +00:00
Compare commits
120 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3ef278fa5f | |||
| 6cba8066c1 | |||
| c3074d66f8 | |||
| 483c8ec806 | |||
|
312a959202 |
|||
|
2c2c6cded4 |
|||
| 3943ca9ea3 | |||
| 61cbb65067 | |||
| 898d2d8aa6 | |||
| db61461cb5 | |||
| 7280dffd37 | |||
| 3c6315e033 | |||
| 9f3e1d8c9a | |||
| 9a26ed1822 | |||
| 6585eee27f | |||
| 229500b861 | |||
| ade6f5d2c3 | |||
| ae9456d6dd | |||
| 40a6e7eb3a | |||
| 47d61f9b35 | |||
| 8f9fddb73c | |||
| 3062140352 | |||
| ae9d57fe44 | |||
| 36efea1d09 | |||
| ceafed4a5c | |||
| 0d0e42a26a | |||
| ee752b91e2 | |||
| 500b156968 | |||
| e0a90005e6 | |||
| 54512df52c | |||
| 832fd2faa7 | |||
| cce894176f | |||
| 759d221c40 | |||
| d5e89b9bdc | |||
| 12e894b41c | |||
| 69460a7f55 | |||
| 982da41acb | |||
| d01dd59598 | |||
| 3931685b08 | |||
| 22cc737f8e | |||
| 674c6c7c51 | |||
| daa26965cd | |||
| 39e8b8a526 | |||
| 92e874e263 | |||
| 921b24195d | |||
| a5c4386a80 | |||
| 17ffeab680 | |||
| d5b176087e | |||
| 4173349481 | |||
| 2b344f7cd9 | |||
| f614deca5a | |||
| f4e94a63b1 | |||
| ae4bdf5983 | |||
| a3fdb236d2 | |||
| 2fdc39c1f0 | |||
| 8a79c5dd3d | |||
| f84da5ba79 | |||
| 5863134c3e | |||
| 12d34f3a40 | |||
| 2ae3d64039 | |||
| 6b03a1ea21 | |||
| b22a2802b2 | |||
| fd49342e12 | |||
| f85fadef2d | |||
| 78ad3d5657 | |||
| 1afbb95e05 | |||
| 91456a8bc9 | |||
| 7a58f92434 | |||
| e7c1384436 | |||
| 4caf912ff1 | |||
| 9de3d951e5 | |||
| e1267730bb | |||
| 8b6a805c36 | |||
| fdd49f8621 | |||
| 6eb20f8db3 | |||
| 393d15a64f | |||
| b08d9725d4 | |||
| c817d72827 | |||
| 22c90ef633 | |||
| dfa4057ebd | |||
| a8d48ce6b8 | |||
| 7a96d99055 | |||
| 3288234f36 | |||
| b6687463e8 | |||
| 10126a988b | |||
| 89593dd0bd | |||
| 99631a7cb1 | |||
| 0c7ca4abdb | |||
| 4746f9dcb5 | |||
| dc22a4782f | |||
| 9fc2265e41 | |||
| d4b0abda46 | |||
| e5c96709d8 | |||
| ef8aca3476 | |||
| 42a6f25078 | |||
| c1058a8034 | |||
| fb182f0364 | |||
| 8699868c42 | |||
| fa2635525c | |||
| 71162ec2c6 | |||
| 1fe588af6e | |||
| 1e0b16361f | |||
| 1e1614706a | |||
| cb73eb430d | |||
| cc1df287be | |||
| 35e8f2ce6f | |||
| 2bd264659c | |||
| 0c5a763414 | |||
| 18da26dc20 | |||
| 7240be1120 | |||
| d95ee172bb | |||
| 9db390c76c | |||
| 26cebc2596 | |||
| fae4b09483 | |||
| 30d8cfec1a | |||
| 752cb60b56 | |||
| 4747a3396c | |||
| 393b1f1dd6 | |||
| f4939aaf55 | |||
| 5d92e1e3dd |
59 changed files with 8766 additions and 8650 deletions
56
.eslintrc.js
56
.eslintrc.js
|
|
@ -1,26 +1,46 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
env: {
|
'env': {
|
||||||
browser: true,
|
'browser': true,
|
||||||
es2021: true
|
'es2021': true
|
||||||
},
|
},
|
||||||
extends: [
|
'extends': [
|
||||||
'standard',
|
'eslint:recommended',
|
||||||
'standard-preact'
|
'preact',
|
||||||
],
|
],
|
||||||
overrides: [
|
'overrides': [
|
||||||
{
|
{
|
||||||
files: ['*.jsx', '*.js']
|
files: ['*.js', '*.jsx'],
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
parserOptions: {
|
'parserOptions': {
|
||||||
ecmaVersion: 12,
|
'ecmaVersion': 'latest',
|
||||||
sourceType: 'module'
|
'sourceType': 'module'
|
||||||
},
|
},
|
||||||
rules: {
|
'rules': {
|
||||||
},
|
'jest/no-deprecated-functions': 0,
|
||||||
settings: {
|
|
||||||
react: {
|
'arrow-parens': ['error', 'always'],
|
||||||
version: 'latest'
|
'comma-dangle': ['error', 'always-multiline'],
|
||||||
}
|
|
||||||
|
'indent': [
|
||||||
|
'error',
|
||||||
|
2
|
||||||
|
],
|
||||||
|
'linebreak-style': [
|
||||||
|
'error',
|
||||||
|
'unix'
|
||||||
|
],
|
||||||
|
'object-curly-spacing': [
|
||||||
|
'error',
|
||||||
|
'always'
|
||||||
|
],
|
||||||
|
'quotes': [
|
||||||
|
'error',
|
||||||
|
'single'
|
||||||
|
],
|
||||||
|
'semi': [
|
||||||
|
'error',
|
||||||
|
'never'
|
||||||
|
],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,4 +1,4 @@
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
dist/
|
dist/
|
||||||
.cache/
|
.parcel-cache/
|
||||||
|
|
|
||||||
4
.parcelrc
Normal file
4
.parcelrc
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"extends": ["@parcel/config-default"],
|
||||||
|
"reporters": ["...", "parcel-reporter-static-files-copy"]
|
||||||
|
}
|
||||||
28
CHANGELOG.md
Normal file
28
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
# 0.2.0
|
||||||
|
|
||||||
|
## Breaking Changes
|
||||||
|
|
||||||
|
- Changed direct links format
|
||||||
|
- Changed savefiles format
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Added over 150 new interface themes
|
||||||
|
- Added themes preview page
|
||||||
|
- Implemented smooth scrolling when a new message appears
|
||||||
|
- Added an in-game menu
|
||||||
|
- Improved mobile user experience
|
||||||
|
- Upgraded Emglken to version 0.5.2
|
||||||
|
- Upgraded cheap-glkote to version 0.5.1
|
||||||
|
- Added support for ADRIFT 4
|
||||||
|
- Fixed broken links
|
||||||
|
- Enhanced error handling
|
||||||
|
- Refactored styles for improved code structure
|
||||||
|
|
||||||
|
# 0.1.0
|
||||||
|
|
||||||
|
- Added support for multiple output buffers
|
||||||
|
|
||||||
|
# 0.0.0
|
||||||
|
|
||||||
|
- Initial release
|
||||||
69
README.md
69
README.md
|
|
@ -1,18 +1,65 @@
|
||||||
# ifplayer
|
# ElseIFPlayer
|
||||||
|
|
||||||
Interactive Fiction player for the web.
|
ElseIFPlayer is an interactive fiction player for the web.
|
||||||
Powered by [cheap-glkote](https://github.com/He4eT/cheap-glkote) and [Emglken](https://github.com/curiousdannii/emglken).
|
It's powered by [cheap-glkote](https://github.com/He4eT/cheap-glkote) and [Emglken](https://github.com/curiousdannii/emglken).
|
||||||
|
|
||||||
To see a live demo, check out [https://he4et.github.io/ifplayer/](https://he4et.github.io/ifplayer/).
|
Player available here: [https://he4et.github.io/elseifplayer/](https://he4et.github.io/elseifplayer/).
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
- Ensure that you have Node.js and NPM installed on your system.
|
||||||
|
- Install the required packages by running the command `npm install` in your project directory.
|
||||||
|
- Launch the local development server using `npm run dev`.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
To create a production build, use the following command:
|
||||||
|
```
|
||||||
|
npm run build <public-url>
|
||||||
|
```
|
||||||
|
|
||||||
|
- If you intend to host the player on `https://your.domain/`, use:
|
||||||
|
```
|
||||||
|
npm run build /
|
||||||
|
```
|
||||||
|
- For hosting it in a specific directory like `https://your.domain/some-directory/`, use:
|
||||||
|
```
|
||||||
|
npm run build /some-directory
|
||||||
|
```
|
||||||
|
|
||||||
|
The finalized production bundle will be generated and stored in the `/docs` directory.
|
||||||
|
|
||||||
## Direct links
|
## Direct links
|
||||||
|
|
||||||
You can provide the direct link to your game:
|
You can provide a direct link to a specific game using the following URL format:
|
||||||
|
```
|
||||||
|
/#/<mode>/<encodedURL>/[theme]/
|
||||||
|
```
|
||||||
|
|
||||||
`/#/play/encodedURL/[theme]/`
|
- `mode` specifies the player interface mode:
|
||||||
- `encodedURL` - storyfile location encoded with `encodeURIComponent`;
|
- `play`: the default multi-window mode
|
||||||
- `theme` - [UI theme](https://github.com/He4eT/ifplayer/blob/master/src/themes/themes.js), optional;
|
- `focus`: the single-window mode without additional windows, such as the status bar
|
||||||
|
- `encodedURL` represents the location of the storyfile encoded with `encodeURIComponent`.
|
||||||
|
- `theme` is optional and allows you to choose a specific UI theme.
|
||||||
|
|
||||||
### Examples
|
### CORS
|
||||||
- [Play "Lost Pig" with default or last used theme](https://he4et.github.io/ifplayer/#/play/https%3A%2F%2Fmirror.ifarchive.org%2Fif-archive%2Fgames%2Fzcode%2FLostPig.z8/);
|
|
||||||
- [Play "Lost Pig" with Nord theme](https://he4et.github.io/ifplayer/#/play/https%3A%2F%2Fmirror.ifarchive.org%2Fif-archive%2Fgames%2Fzcode%2FLostPig.z8/nord/);
|
If the player and your storyfile are located on different domains,
|
||||||
|
you need to use appropriate [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) settings.
|
||||||
|
|
||||||
|
If you cannot modify the server settings, you can use the [Parchment Proxy](https://iplayif.com/proxy/) as an alternative.
|
||||||
|
|
||||||
|
### Direct Link Examples
|
||||||
|
|
||||||
|
- [Play "Lost Pig" with default or last used theme](https://he4et.github.io/elseifplayer/#/play/https%3A%2F%2Fmirror.ifarchive.org%2Fif-archive%2Fgames%2Fzcode%2FLostPig.z8/)
|
||||||
|
- [Play "Lost Pig" without statusbar with default or last used theme](https://he4et.github.io/elseifplayer/#/focus/https%3A%2F%2Fmirror.ifarchive.org%2Fif-archive%2Fgames%2Fzcode%2FLostPig.z8/)
|
||||||
|
- [Play "Lost Pig" with Nord theme](https://he4et.github.io/elseifplayer/#/play/https%3A%2F%2Fmirror.ifarchive.org%2Fif-archive%2Fgames%2Fzcode%2FLostPig.z8/nord/)
|
||||||
|
- [Play "Lost Pig" without statusbar with Dim theme](https://he4et.github.io/elseifplayer/#/focus/https%3A%2F%2Fmirror.ifarchive.org%2Fif-archive%2Fgames%2Fzcode%2FLostPig.z8/dim/)
|
||||||
|
- [Play "Lost Pig" loaded with Parchment Proxy](https://he4et.github.io/elseifplayer/#/play/https%3A%2F%2Fiplayif.com%2Fproxy%2F%3Furl%3Dhttps%3A%2F%2Fifarchive.org%2Fif-archive%2Fgames%2Fzcode%2FLostPig.z8)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
ElseIFPlayer is distributed under the MIT License.
|
||||||
|
However, please remember to respect the licenses of the interpreters
|
||||||
|
listed on the
|
||||||
|
[Emglken page](https://github.com/curiousdannii/emglken#included-projects).
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
CURRENT_TIMESTAMP=`date +"%Y-%m-%d-%H%M%S"`
|
CURRENT_TIMESTAMP=`date +"%Y-%m-%d-%H%M%S"`
|
||||||
|
|
||||||
GH_REPO_NAME='ifplayer'
|
GH_REPO_NAME='elseifplayer'
|
||||||
RELEASE_BRANCH='release'
|
RELEASE_BRANCH='release'
|
||||||
BUILD_DIR='docs'
|
BUILD_DIR='docs'
|
||||||
|
|
||||||
|
|
|
||||||
22
index.html
22
index.html
|
|
@ -4,33 +4,25 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta
|
<meta
|
||||||
name="viewport"
|
name="viewport"
|
||||||
content="width=device-width, initial-scale=1.0">
|
content="width=device-width, initial-scale=1.0, interactive-widget=resizes-content">
|
||||||
<title>
|
<title>
|
||||||
IFPlayer
|
ElseIFPlayer
|
||||||
</title>
|
</title>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="Interactive Fiction player for the web.">
|
content="Interactive Fiction player for the web">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root">
|
<div id="root"></div>
|
||||||
|
|
||||||
<div class="app play">
|
<script type="module" src="./src/index.js"></script>
|
||||||
<main>
|
|
||||||
<div class="status loading">
|
|
||||||
<div>Loading</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="./src/index.js"></script>
|
|
||||||
|
|
||||||
<!-- <goatcounter> -->
|
<!-- <goatcounter> -->
|
||||||
<script>
|
<script>
|
||||||
window.goatcounter = { no_onload: true }
|
window.goatcounter = { no_onload: true }
|
||||||
window.addEventListener('hashchange', _ => {
|
window.addEventListener('hashchange', _ => {
|
||||||
|
if (!window.goatcounter.count) return void null
|
||||||
|
|
||||||
window.goatcounter.count({
|
window.goatcounter.count({
|
||||||
path: location.pathname + location.hash
|
path: location.pathname + location.hash
|
||||||
})
|
})
|
||||||
|
|
|
||||||
13873
package-lock.json
generated
13873
package-lock.json
generated
File diff suppressed because it is too large
Load diff
45
package.json
45
package.json
|
|
@ -1,35 +1,44 @@
|
||||||
{
|
{
|
||||||
"name": "ifplayer",
|
"name": "elseifplayer",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"description": "Play interactive fiction games in your browser",
|
"description": "Play interactive fiction games in your browser",
|
||||||
"main": "index.js",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "parcel index.html",
|
"dev": "parcel index.html",
|
||||||
"build": "parcel build index.html --out-dir docs --public-url",
|
"build": "parcel build index.html --dist-dir docs --public-url",
|
||||||
"lint": "eslint --fix src"
|
"lint": "eslint --fix src"
|
||||||
},
|
},
|
||||||
"author": "He4eT",
|
"author": "He4eT",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"browserslist": "defaults",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"alias": {
|
||||||
|
"preact/jsx-dev-runtime": "preact/jsx-runtime"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^7.20.0",
|
"@parcel/transformer-sass": "^2.9.3",
|
||||||
"eslint-config-standard": "^16.0.2",
|
"buffer": "^6.0.3",
|
||||||
"eslint-config-standard-preact": "^1.1.6",
|
"crypto-browserify": "^3.12.0",
|
||||||
"eslint-plugin-import": "^2.22.1",
|
"eslint": "^8.44.0",
|
||||||
"eslint-plugin-node": "^11.1.0",
|
"eslint-config-preact": "^1.3.0",
|
||||||
"eslint-plugin-promise": "^4.3.1",
|
"events": "^3.3.0",
|
||||||
"parcel-bundler": "^1.12.4",
|
"parcel": "^2.9.3",
|
||||||
"parcel-plugin-static-files-copy": "^2.5.1"
|
"parcel-reporter-static-files-copy": "^1.5.0",
|
||||||
|
"path-browserify": "^1.0.1",
|
||||||
|
"process": "^0.11.10",
|
||||||
|
"stream-browserify": "^3.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/open-sans": "^4.2.1",
|
"@fontsource/open-sans": "^5.0.3",
|
||||||
"cheap-glkote": "^0.2.5",
|
"base32768": "^3.0.1",
|
||||||
"emglken": "^0.3.3",
|
"cheap-glkote": "^0.5.1",
|
||||||
"lz-string": "^1.4.4",
|
"emglken": "^0.5.2",
|
||||||
"preact": "^10.5.12",
|
"preact": "^10.15.1",
|
||||||
"wouter-preact": "^2.7.3"
|
"wouter-preact": "^2.7.3"
|
||||||
},
|
},
|
||||||
"staticFiles": {
|
"staticFiles": {
|
||||||
"staticPath": "node_modules/emglken/build",
|
"staticPath": "node_modules/emglken/build",
|
||||||
"excludeGlob": "*.js"
|
"staticOutPath": "emglken"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
76
src/App.jsx
Normal file
76
src/App.jsx
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
import { Route, Router, Switch } from 'wouter-preact'
|
||||||
|
|
||||||
|
import {
|
||||||
|
useHashLocation,
|
||||||
|
extractView,
|
||||||
|
} from './routing'
|
||||||
|
import {
|
||||||
|
useThemeEngine,
|
||||||
|
} from '~/src/themes/themes'
|
||||||
|
|
||||||
|
import HomeView from '~/src/views/HomeView/HomeView'
|
||||||
|
import GamesView from '~/src/views/GamesView/GamesView'
|
||||||
|
import ThemesView from '~/src/views/ThemesView/ThemesView'
|
||||||
|
import PlayerView from '~/src/views/PlayerView/PlayerView'
|
||||||
|
import NotFoundView from '~/src/views/NotFoundView/NotFoundView'
|
||||||
|
|
||||||
|
import * as s from './style/App.module.scss'
|
||||||
|
|
||||||
|
export default function App () {
|
||||||
|
const themeEngine = useThemeEngine()
|
||||||
|
const [currentLocation] = useHashLocation()
|
||||||
|
|
||||||
|
const playerView = (themeEngine, singleWindow) =>
|
||||||
|
function view (params) {
|
||||||
|
return (<PlayerView {...{
|
||||||
|
themeEngine,
|
||||||
|
singleWindow,
|
||||||
|
...params,
|
||||||
|
}} />)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Router hook={useHashLocation}>
|
||||||
|
<div className={[
|
||||||
|
s.app,
|
||||||
|
s[extractView(currentLocation)],
|
||||||
|
themeEngine.currentTheme,
|
||||||
|
].join(' ')}>
|
||||||
|
|
||||||
|
<Switch>
|
||||||
|
<Route path='/'>
|
||||||
|
<HomeView {...{
|
||||||
|
themeEngine,
|
||||||
|
}} />
|
||||||
|
</Route>
|
||||||
|
<Route path='/games/'>
|
||||||
|
<GamesView />
|
||||||
|
</Route>
|
||||||
|
<Route path='/themes/'>
|
||||||
|
<ThemesView {...{
|
||||||
|
themeEngine,
|
||||||
|
}} />
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
<Route path='/play/:encodedUrl'>
|
||||||
|
{ playerView(themeEngine, false) }
|
||||||
|
</Route>
|
||||||
|
<Route path='/play/:encodedUrl/:theme'>
|
||||||
|
{ playerView(themeEngine, false) }
|
||||||
|
</Route>
|
||||||
|
<Route path='/focus/:encodedUrl'>
|
||||||
|
{ playerView(themeEngine, true) }
|
||||||
|
</Route>
|
||||||
|
<Route path='/focus/:encodedUrl/:theme'>
|
||||||
|
{ playerView(themeEngine, true) }
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
<Route>
|
||||||
|
<NotFoundView />
|
||||||
|
</Route>
|
||||||
|
</Switch>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</Router>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
import { h } from 'preact'
|
export default function LocalFileSelector ({ theme, setLocation, buildLink }) {
|
||||||
|
|
||||||
export default function ({ theme, setLocation, buildLink }) {
|
|
||||||
const fileInputHandler = ({ target }) => {
|
const fileInputHandler = ({ target }) => {
|
||||||
const file = target.files[0]
|
const file = target.files[0]
|
||||||
const url = `${URL.createObjectURL(file)}#${file.name}`
|
const url = `${URL.createObjectURL(file)}#${file.name}`
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
import { h } from 'preact'
|
export default function TargetURLSelector ({ theme, setLocation, buildLink }) {
|
||||||
|
|
||||||
export default function ({ theme, setLocation, buildLink }) {
|
|
||||||
const urlRE = /^(http|https):\/\/[^ "]+$/
|
const urlRE = /^(http|https):\/\/[^ "]+$/
|
||||||
|
|
||||||
const onKeyPress = ({ keyCode, target }) => {
|
const onKeyPress = ({ keyCode, target }) => {
|
||||||
if (keyCode !== 13) return
|
if (keyCode !== 13) return
|
||||||
|
|
||||||
const url = target.value
|
const url = encodeURI(target.value)
|
||||||
|
|
||||||
if (urlRE.test(url)) {
|
if (urlRE.test(url)) {
|
||||||
setLocation(buildLink({ url, theme }))
|
setLocation(buildLink({ url, theme }))
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,24 @@
|
||||||
import { h } from 'preact'
|
|
||||||
import { Link } from 'wouter-preact'
|
import { Link } from 'wouter-preact'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
buildPlayLinkHref
|
buildPlayLinkHref,
|
||||||
} from '~/src/utils/utils.routing'
|
} from '~/src/routing'
|
||||||
|
|
||||||
export default ({ name, ifdb, url }) => (
|
export default function GameEntry ({ name, ifdb, url }) {
|
||||||
<div>
|
return (
|
||||||
<h4>{name}</h4>
|
<div>
|
||||||
<a
|
<h4>{name}</h4>
|
||||||
target='_blank'
|
<a
|
||||||
rel='noopener'
|
target='_blank'
|
||||||
href={ifdb}>
|
rel='noopener noreferrer'
|
||||||
IFDB page
|
href={ifdb}>
|
||||||
</a>
|
IFDB page
|
||||||
<span> | </span>
|
</a>
|
||||||
<Link
|
<span> | </span>
|
||||||
href={buildPlayLinkHref({ url })}>
|
<Link
|
||||||
Play
|
href={buildPlayLinkHref({ url })}>
|
||||||
</Link>
|
Play
|
||||||
</div>
|
</Link>
|
||||||
)
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,108 +0,0 @@
|
||||||
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 [lastInput, setLastInput] = useState('')
|
|
||||||
const inputEl = useRef(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setInputText('')
|
|
||||||
inputEl.current && inputEl.current.focus()
|
|
||||||
}, [inputType])
|
|
||||||
|
|
||||||
const send = x => {
|
|
||||||
sendMessage(x, currentWindow)
|
|
||||||
setLastInput(x)
|
|
||||||
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 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: {
|
|
||||||
placeholder: 'Press any key here',
|
|
||||||
onKeyDown: charHandler
|
|
||||||
},
|
|
||||||
line: {
|
|
||||||
placeholder: ' > ',
|
|
||||||
onKeyDown: lineArrowHandler,
|
|
||||||
onKeyPress: lineHandler
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const enterFullscreen = _ =>
|
|
||||||
document.documentElement.requestFullscreen()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<input {...inputHandlers[inputType]}
|
|
||||||
className='inputBox'
|
|
||||||
ref={inputEl}
|
|
||||||
value={inputText}
|
|
||||||
autofocus
|
|
||||||
autocomplete='off'
|
|
||||||
onDblClick={enterFullscreen}
|
|
||||||
onInput={({ target: { value } }) => setInputText(value)}
|
|
||||||
type='search' />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
169
src/components/Player/InputBox/InputBox.jsx
Normal file
169
src/components/Player/InputBox/InputBox.jsx
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
import { useEffect, useRef, useState } from 'preact/hooks'
|
||||||
|
|
||||||
|
import MenuButton from './MenuButton/MenuButton'
|
||||||
|
|
||||||
|
import * as s from './InputBox.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 */
|
||||||
|
|
||||||
|
const hasModifier = (event) => {
|
||||||
|
const modifiers = [
|
||||||
|
event.altKey,
|
||||||
|
event.ctrlKey,
|
||||||
|
event.metaKey,
|
||||||
|
event.shiftKey,
|
||||||
|
]
|
||||||
|
|
||||||
|
return modifiers.some((modifier) => modifier === true)
|
||||||
|
}
|
||||||
|
|
||||||
|
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(() => {
|
||||||
|
let setFocus = () => {
|
||||||
|
inputEl.current && inputEl.current.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
setInputText('')
|
||||||
|
setFocus()
|
||||||
|
|
||||||
|
document.addEventListener('fullscreenchange', setFocus)
|
||||||
|
return () => document.removeEventListener('fullscreenchange', setFocus)
|
||||||
|
}, [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) => {
|
||||||
|
if (hasModifier(event)) { return undefined }
|
||||||
|
|
||||||
|
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={s.inputControls}>
|
||||||
|
<input {...inputHandlers[inputType]}
|
||||||
|
className={s.inputBox}
|
||||||
|
ref={inputEl}
|
||||||
|
value={inputText}
|
||||||
|
autofocus
|
||||||
|
autocomplete='off'
|
||||||
|
spellCheck='false'
|
||||||
|
autocapitalize='off'
|
||||||
|
autocorrect='off'
|
||||||
|
onDblClick={onFullscreenRequest}
|
||||||
|
onInput={({ target: { value } }) => setInputText(value)}
|
||||||
|
type='search' />
|
||||||
|
<MenuButton
|
||||||
|
onClick={() => setMenuOpen(true)} />
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
31
src/components/Player/InputBox/InputBox.module.scss
Normal file
31
src/components/Player/InputBox/InputBox.module.scss
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
.inputControls {
|
||||||
|
position: relative;
|
||||||
|
margin-top: var(--input-box-margin);
|
||||||
|
|
||||||
|
.inputBox {
|
||||||
|
font: inherit;
|
||||||
|
color: inherit;
|
||||||
|
outline: 0;
|
||||||
|
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
border: var(--border-width) solid var(--main-color);
|
||||||
|
border-top: var(--separator-width) solid var(--main-color);
|
||||||
|
padding: var(--inner-padding);
|
||||||
|
padding-right: calc(4 * var(--inner-padding));
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--main-color);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus::placeholder {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-search-cancel-button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/components/Player/InputBox/MenuButton/MenuButton.jsx
Normal file
21
src/components/Player/InputBox/MenuButton/MenuButton.jsx
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import * as s from './MenuButton.module.scss'
|
||||||
|
|
||||||
|
export default function MenuButton ({ onClick }) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
aria-label='Menu'
|
||||||
|
className={s.menuButton}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class={s.menuIcon}
|
||||||
|
viewBox='0 0 28 32'
|
||||||
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
|
>
|
||||||
|
<rect x='12' y='4' width='4' height='4' />
|
||||||
|
<rect x='12' y='14' width='4' height='4' />
|
||||||
|
<rect x='12' y='24' width='4' height='4' />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
.menuButton {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
outline-offset: -8px;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
@media (max-width: 800px) {
|
||||||
|
padding: 0 calc(0.5 * var(--inner-padding));
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline-offset: -4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menuIcon {
|
||||||
|
height: 32px;
|
||||||
|
fill: none;
|
||||||
|
stroke: currentColor;
|
||||||
|
stroke-width: 2px;
|
||||||
|
|
||||||
|
@media (max-width: 800px) {
|
||||||
|
transform: scaleX(2) scaleY(0.5);
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
79
src/components/Player/MenuOverlay/MenuOverlay.jsx
Normal file
79
src/components/Player/MenuOverlay/MenuOverlay.jsx
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
import { useEffect, useRef } from 'preact/hooks'
|
||||||
|
import { Link } from 'wouter-preact'
|
||||||
|
|
||||||
|
import ThemeSelector from
|
||||||
|
'~/src/components/ThemeSelector/ThemeSelector'
|
||||||
|
|
||||||
|
import * as s from './MenuOverlay.module.scss'
|
||||||
|
|
||||||
|
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 currentDialog = dialog.current
|
||||||
|
const closeHandler = () => {
|
||||||
|
setMenuOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentDialog.addEventListener('close', closeHandler)
|
||||||
|
return () => currentDialog.removeEventListener('close', closeHandler)
|
||||||
|
}, [dialog, setMenuOpen])
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<dialog ref={dialog} className={s.menu}>
|
||||||
|
<section>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => dialog.current.close()}
|
||||||
|
>
|
||||||
|
Close this menu
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={s.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={s.navigation}>
|
||||||
|
<Link href="/" tabIndex={0}>
|
||||||
|
ElseIfPlayer
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
41
src/components/Player/MenuOverlay/MenuOverlay.module.scss
Normal file
41
src/components/Player/MenuOverlay/MenuOverlay.module.scss
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);
|
||||||
|
|
||||||
|
&::backdrop {
|
||||||
|
background: none;
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
& > section {
|
||||||
|
margin: 32px auto 40px;
|
||||||
|
gap: 32px;
|
||||||
|
max-width: 270px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigation {
|
||||||
|
color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.appearance {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
select,
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
79
src/components/Player/OutputBox/GridBuffer/GridBuffer.jsx
Normal file
79
src/components/Player/OutputBox/GridBuffer/GridBuffer.jsx
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
import { useEffect, useState } from 'preact/hooks'
|
||||||
|
|
||||||
|
import TextMessage from '../TextMessage/TextMessage'
|
||||||
|
|
||||||
|
import * as s from '../../Player.module.scss'
|
||||||
|
|
||||||
|
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'
|
||||||
|
return style || 'grid'
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages =
|
||||||
|
rawMessagesContent
|
||||||
|
.map((message) => ({
|
||||||
|
style: getGridStyle(message),
|
||||||
|
text: message.text.replace(' ', ' / '),
|
||||||
|
}))
|
||||||
|
|
||||||
|
setMessages(isEmpty ? [] : messages)
|
||||||
|
}, [inbox, currentWindow, prevMessages])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={[s.buffer, s.gridBuffer].join(' ')}>
|
||||||
|
{messages.map(TextMessage)}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
97
src/components/Player/OutputBox/TextBuffer/TextBuffer.jsx
Normal file
97
src/components/Player/OutputBox/TextBuffer/TextBuffer.jsx
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
import { useEffect, useRef, useState } from 'preact/hooks'
|
||||||
|
|
||||||
|
import TextMessage from '../TextMessage/TextMessage'
|
||||||
|
|
||||||
|
import * as s from '../../Player.module.scss'
|
||||||
|
|
||||||
|
const eol = { style: 'endOfLine' }
|
||||||
|
const scrollTarget = { style: 'scrollTarget' }
|
||||||
|
|
||||||
|
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: [scrollTarget],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { text: inboxMessagesRaw } =
|
||||||
|
currentInbox
|
||||||
|
|
||||||
|
const incoming =
|
||||||
|
inboxMessagesRaw
|
||||||
|
/* Normalize. */
|
||||||
|
.map(({ content }) =>
|
||||||
|
content
|
||||||
|
? [...trimInputPrompt(content), eol]
|
||||||
|
: [eol])
|
||||||
|
/* Flatten. */
|
||||||
|
.reduce((acc, x) =>
|
||||||
|
acc.concat(x), [scrollTarget])
|
||||||
|
|
||||||
|
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 scrollTargets =
|
||||||
|
textBufferEl.current.querySelectorAll(`.${scrollTarget.style}`)
|
||||||
|
const freshScrollTarget =
|
||||||
|
scrollTargets[scrollTargets.length - 1]
|
||||||
|
|
||||||
|
freshScrollTarget
|
||||||
|
? freshScrollTarget.scrollIntoView()
|
||||||
|
: textBufferEl.current.scrollTo({
|
||||||
|
top: textBufferEl.current.scrollHeight,
|
||||||
|
behavior: 'smooth',
|
||||||
|
})
|
||||||
|
}, 0)
|
||||||
|
}, [currentWindow, inbox])
|
||||||
|
|
||||||
|
const classes = () => [
|
||||||
|
s.buffer,
|
||||||
|
isFakeStatus(currentWindow)
|
||||||
|
? s.gridBuffer
|
||||||
|
: s.textBuffer,
|
||||||
|
].join(' ')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
tabindex='0'
|
||||||
|
ref={textBufferEl}
|
||||||
|
className={classes()}
|
||||||
|
>
|
||||||
|
{messages.map(TextMessage)}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
23
src/components/Player/OutputBox/TextMessage/TextMessage.jsx
Normal file
23
src/components/Player/OutputBox/TextMessage/TextMessage.jsx
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import * as s from './TextMessage.module.scss'
|
||||||
|
|
||||||
|
export default function TextMessage ({ style, text }) {
|
||||||
|
const defaultContent = (
|
||||||
|
<span className={[s.message, s[style]].join(' ')}>
|
||||||
|
{text}
|
||||||
|
</span>)
|
||||||
|
|
||||||
|
return ({
|
||||||
|
grid:
|
||||||
|
(text?.length > 0 ? <div>{text}</div> : <br />),
|
||||||
|
input:
|
||||||
|
(<span className={[s.message, s.input].join(' ')}>> {text}</span>),
|
||||||
|
subheader:
|
||||||
|
(<strong className={[s.message, s.subheader].join(' ')}>{text}</strong>),
|
||||||
|
emphasized:
|
||||||
|
(<em className={[s.message, s.emphasized].join(' ')}>{text}</em>),
|
||||||
|
scrollTarget:
|
||||||
|
(<div className={[s.scrollTarget, style].join(' ')}></div>),
|
||||||
|
endOfLine:
|
||||||
|
(<br />),
|
||||||
|
})[style] || defaultContent
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
.message {
|
||||||
|
&.input {
|
||||||
|
color: var(--input-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.emphasized,
|
||||||
|
&.subheader {
|
||||||
|
color: var(--accent-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollTarget {
|
||||||
|
scroll-margin-block-start: var(--inner-padding);
|
||||||
|
}
|
||||||
|
|
@ -1,71 +1,51 @@
|
||||||
import { h } from 'preact'
|
|
||||||
import { useState, useEffect } from 'preact/hooks'
|
import { useState, useEffect } from 'preact/hooks'
|
||||||
import {
|
|
||||||
compressToUTF16 as encode,
|
|
||||||
decompressFromUTF16 as decode
|
|
||||||
} from 'lz-string'
|
|
||||||
|
|
||||||
import CheapGlkOte from 'cheap-glkote'
|
import CheapGlkOte from 'cheap-glkote'
|
||||||
|
|
||||||
import TextBuffer from './TextBuffer'
|
import TextBuffer from './OutputBox/TextBuffer/TextBuffer'
|
||||||
import InputBox from './InputBox'
|
import GridBuffer from './OutputBox/GridBuffer/GridBuffer'
|
||||||
import Status from './Status'
|
|
||||||
|
|
||||||
import './player.css'
|
import InputBox from './InputBox/InputBox'
|
||||||
|
import Status from './Status/Status'
|
||||||
|
|
||||||
|
import {
|
||||||
|
Handlers,
|
||||||
|
unhandledRejectionHandler,
|
||||||
|
} from './common/playerHandlers'
|
||||||
|
|
||||||
|
import * as s from './Player.module.scss'
|
||||||
|
|
||||||
const INITIAL_STATUS = {
|
const INITIAL_STATUS = {
|
||||||
stage: 'loading',
|
stage: 'loading',
|
||||||
details: ['Preparing']
|
details: ['Preparing'],
|
||||||
}
|
}
|
||||||
|
|
||||||
const runMachine = ({ engine: Engine, file, handlers }) => {
|
const runMachine = ({ engine: Engine, wasmBinary, storyfile, handlers }) => {
|
||||||
const vm = new Engine()
|
const { Dialog, GlkOte, send } = CheapGlkOte(handlers)
|
||||||
const { glkInterface, sendFn } = CheapGlkOte(handlers)
|
const instance = new Engine()
|
||||||
|
|
||||||
vm.prepare(file, glkInterface)
|
instance.init(storyfile, {
|
||||||
vm.start()
|
Dialog,
|
||||||
|
GlkOte,
|
||||||
|
Glk: {},
|
||||||
|
wasmBinary,
|
||||||
|
arguments: ['storyfile'],
|
||||||
|
})
|
||||||
|
instance.start()
|
||||||
|
|
||||||
return { sendFn, instance: vm }
|
return { send, instance }
|
||||||
}
|
}
|
||||||
|
|
||||||
const Handlers = ({
|
export default function Player ({
|
||||||
setStatus,
|
vmParts: { storyfile, engine, wasmBinary },
|
||||||
setCurrentWindow,
|
onFullscreenRequest,
|
||||||
setInputType,
|
setMenuOpen,
|
||||||
setInbox
|
singleWindow,
|
||||||
}) => ({
|
}) {
|
||||||
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: 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(null)
|
|
||||||
})
|
|
||||||
|
|
||||||
export default function ({ vmParts: { file, engine } }) {
|
|
||||||
const [status, setStatus] = useState(INITIAL_STATUS)
|
const [status, setStatus] = useState(INITIAL_STATUS)
|
||||||
|
|
||||||
const [currentWindow, setCurrentWindow] = useState(null)
|
const [windows, setWindows] = useState([])
|
||||||
|
const [currentWindowId, setCurrentWindowId] = useState(null)
|
||||||
const [inputType, setInputType] = useState(null)
|
const [inputType, setInputType] = useState(null)
|
||||||
const [inbox, setInbox] = useState([])
|
const [inbox, setInbox] = useState([])
|
||||||
|
|
||||||
|
|
@ -73,39 +53,75 @@ export default function ({ vmParts: { file, engine } }) {
|
||||||
const [sendMessage, setSendMessage] = useState(null)
|
const [sendMessage, setSendMessage] = useState(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const vm = runMachine({
|
const handlers = Handlers({
|
||||||
engine,
|
setStatus,
|
||||||
file,
|
setWindows,
|
||||||
handlers: Handlers({
|
setCurrentWindowId,
|
||||||
setStatus,
|
setInputType,
|
||||||
setCurrentWindow,
|
setInbox,
|
||||||
setInputType,
|
|
||||||
setInbox
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
setVm(vm)
|
setVm(runMachine({
|
||||||
}, [file, engine])
|
engine,
|
||||||
|
wasmBinary,
|
||||||
|
storyfile,
|
||||||
|
handlers,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const rejectionHandler =
|
||||||
|
unhandledRejectionHandler(handlers.onExit)
|
||||||
|
|
||||||
|
window.addEventListener('unhandledrejection', rejectionHandler)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
setVm(null)
|
||||||
|
window.removeEventListener('unhandledrejection', rejectionHandler)
|
||||||
|
}
|
||||||
|
}, [storyfile, engine, wasmBinary])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSendMessage(_ => vm
|
setSendMessage(() => vm
|
||||||
? vm.sendFn
|
? vm.send
|
||||||
: null)
|
: null)
|
||||||
|
|
||||||
|
return () => setSendMessage(null)
|
||||||
}, [vm])
|
}, [vm])
|
||||||
|
|
||||||
|
const textWindow = (inbox) => (currentWindow) => {
|
||||||
|
const props = {
|
||||||
|
inbox,
|
||||||
|
currentWindow,
|
||||||
|
}
|
||||||
|
|
||||||
|
return ({
|
||||||
|
buffer: <TextBuffer {...props} />,
|
||||||
|
grid: <GridBuffer {...props} />,
|
||||||
|
})[currentWindow.type]
|
||||||
|
}
|
||||||
|
|
||||||
|
const byTop = (a, b) =>
|
||||||
|
a.top - b.top
|
||||||
|
|
||||||
return status.stage !== 'ready'
|
return status.stage !== 'ready'
|
||||||
? (<Status {...status} />)
|
? (<Status {...status} />)
|
||||||
: (
|
: (<section className={s.elseifplayer}>
|
||||||
<section className='ifplayer'>
|
<section className={s.output}>
|
||||||
<TextBuffer {...{
|
{
|
||||||
inbox,
|
windows
|
||||||
currentWindow
|
.sort(byTop)
|
||||||
}} />
|
.filter(singleWindow
|
||||||
<InputBox {...{
|
? ({ id }) => id === currentWindowId
|
||||||
currentWindow,
|
: () => true)
|
||||||
inputType,
|
.map(textWindow(inbox))
|
||||||
sendMessage
|
}
|
||||||
}} />
|
|
||||||
</section>
|
</section>
|
||||||
)
|
<InputBox {...{
|
||||||
|
inputType,
|
||||||
|
windows,
|
||||||
|
currentWindowId,
|
||||||
|
sendMessage,
|
||||||
|
onFullscreenRequest,
|
||||||
|
setMenuOpen,
|
||||||
|
}} />
|
||||||
|
</section>)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
49
src/components/Player/Player.module.scss
Normal file
49
src/components/Player/Player.module.scss
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
.elseifplayer {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
color: var(--main-color);
|
||||||
|
padding: var(--outer-padding);
|
||||||
|
|
||||||
|
.output {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 2;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow-y: hidden;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
|
||||||
|
border: var(--border-width) solid var(--main-color);
|
||||||
|
|
||||||
|
.buffer {
|
||||||
|
overflow-y: scroll;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
padding: var(--inner-padding);
|
||||||
|
|
||||||
|
&:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > br:first-child,
|
||||||
|
& > br:last-child,
|
||||||
|
& > br + br + br {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.gridBuffer {
|
||||||
|
flex-shrink: 0;
|
||||||
|
max-height: 100%;
|
||||||
|
border-bottom: var(--separator-width) solid var(--main-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.textBuffer {
|
||||||
|
flex: 2 1;
|
||||||
|
outline: none;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
import { h } from 'preact'
|
|
||||||
import { Link } from 'wouter-preact'
|
|
||||||
|
|
||||||
const fail = details => (
|
|
||||||
<div class='status fail'>
|
|
||||||
<h1>
|
|
||||||
Error
|
|
||||||
</h1>
|
|
||||||
{details.map(x => (<p>{x}</p>))}
|
|
||||||
<hr />
|
|
||||||
<Link href='/'>
|
|
||||||
Home
|
|
||||||
</Link>
|
|
||||||
|
|
|
||||||
<a
|
|
||||||
target='_blank'
|
|
||||||
rel='noopener'
|
|
||||||
href='https://github.com/He4eT/ifplayer/issues'>
|
|
||||||
Report bug
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
const loading = details => (
|
|
||||||
<div class='status loading'>
|
|
||||||
{details.map(x => (<div>{x}</div>))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default ({ stage, details }) =>
|
|
||||||
({ fail, loading })[stage](details)
|
|
||||||
33
src/components/Player/Status/Status.jsx
Normal file
33
src/components/Player/Status/Status.jsx
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { Link } from 'wouter-preact'
|
||||||
|
|
||||||
|
import * as s from './Status.module.scss'
|
||||||
|
|
||||||
|
const fail = (details) => (
|
||||||
|
<div className={[s.status].join(' ')}>
|
||||||
|
<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={[s.status, s.loading].join(' ')}>
|
||||||
|
{details.map((x) => (<div key={x}>{x}</div>))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default ({ stage, details }) =>
|
||||||
|
({ fail, loading })[stage](details)
|
||||||
21
src/components/Player/Status/Status.module.scss
Normal file
21
src/components/Player/Status/Status.module.scss
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
@keyframes dots0123 {
|
||||||
|
0% { content: ''; }
|
||||||
|
33% { content: '.'; }
|
||||||
|
66% { content: '..'; }
|
||||||
|
100% { content: '...'; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
word-break: break-word;
|
||||||
|
padding-block: var(--inner-padding);
|
||||||
|
|
||||||
|
@media (max-width: 800px) {
|
||||||
|
padding: var(--inner-padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.loading > div:after {
|
||||||
|
animation: dots0123 1s infinite;
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
import { h } from 'preact'
|
|
||||||
import { useEffect, useRef, useState } from 'preact/hooks'
|
|
||||||
|
|
||||||
import TextMessage from './TextMessage'
|
|
||||||
|
|
||||||
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 { clear, 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 { 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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
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>),
|
|
||||||
emphasized: (<em>{text}</em>),
|
|
||||||
endOfLine: (<br />)
|
|
||||||
})[style] || defaultContent
|
|
||||||
}
|
|
||||||
|
|
@ -1,41 +1,64 @@
|
||||||
import { h } from 'preact'
|
|
||||||
import { useState, useEffect } from 'preact/hooks'
|
import { useState, useEffect } from 'preact/hooks'
|
||||||
|
|
||||||
import { engineByFilename } from './common/engines'
|
import { engineByFilename } from './common/engines'
|
||||||
|
|
||||||
import Player from './Player'
|
import Player from './Player'
|
||||||
import Status from './Status'
|
import Status from './Status/Status'
|
||||||
|
|
||||||
const INITIAL_STATUS = {
|
const INITIAL_STATUS = {
|
||||||
stage: 'loading',
|
stage: 'loading',
|
||||||
details: ['Loading']
|
details: ['Loading'],
|
||||||
}
|
}
|
||||||
|
|
||||||
const prepareVM = ({ url, setStatus, setParts }) => {
|
const prepareVM = ({ url, setStatus, setParts }) => {
|
||||||
const st = (stage, details) => args => {
|
const st = (stage, details) => (args) => {
|
||||||
setStatus({ stage, details: [details] })
|
setStatus({ stage, details: [details] })
|
||||||
return args
|
return args
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve()
|
const cleanUrl = (url) =>
|
||||||
|
url.startsWith('blob:')
|
||||||
|
? url.replace(/#(.*)$/g, '')
|
||||||
|
: url
|
||||||
|
|
||||||
|
const fetchWasm = (wasmBinaryName) =>
|
||||||
|
fetch(wasmBinaryName)
|
||||||
|
.then((response) => response.arrayBuffer())
|
||||||
|
|
||||||
|
const checkResponse = (response) => {
|
||||||
|
if (response.ok) return response
|
||||||
|
throw new Error(response.statusText)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(url)
|
||||||
.then(st('loading', 'Downloading file'))
|
.then(st('loading', 'Downloading file'))
|
||||||
.then(_ => fetch(url))
|
.then(cleanUrl)
|
||||||
|
.then(fetch)
|
||||||
|
.then(checkResponse)
|
||||||
.then(st('loading', 'Processing file'))
|
.then(st('loading', 'Processing file'))
|
||||||
.then(response => response.arrayBuffer())
|
.then((response) => response.arrayBuffer())
|
||||||
.then(arrayBuffer => new Uint8Array(arrayBuffer))
|
.then((arrayBuffer) => new Uint8Array(arrayBuffer))
|
||||||
.then(st('loading', 'Downloading engine'))
|
.then(st('loading', 'Downloading engine'))
|
||||||
.then(file => setParts({
|
.then((storyfile) => {
|
||||||
file,
|
let parts = engineByFilename(url)
|
||||||
engine: engineByFilename(url)
|
return [storyfile, parts.engine, parts.wasmBinaryName]
|
||||||
|
})
|
||||||
|
.then(([storyfile, engine, wasmBinaryName]) => Promise.all([
|
||||||
|
storyfile, engine, fetchWasm(wasmBinaryName),
|
||||||
|
]))
|
||||||
|
.then(([storyfile, engine, wasmBinary]) => setParts({
|
||||||
|
storyfile, engine, wasmBinary,
|
||||||
}))
|
}))
|
||||||
.then(st('loading', 'Running'))
|
.then(st('loading', 'Running'))
|
||||||
.catch(e => {
|
.catch((e) => {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
setStatus({ stage: 'fail', details: [e.message, url] })
|
setStatus({ stage: 'fail', details: [e.message, url] })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ({ url }) {
|
export default function UrlPlayer ({
|
||||||
|
url, singleWindow, onFullscreenRequest, setMenuOpen,
|
||||||
|
}) {
|
||||||
const [status, setStatus] = useState(INITIAL_STATUS)
|
const [status, setStatus] = useState(INITIAL_STATUS)
|
||||||
const [vmParts, setParts] = useState(null)
|
const [vmParts, setParts] = useState(null)
|
||||||
|
|
||||||
|
|
@ -44,9 +67,16 @@ export default function ({ url }) {
|
||||||
setParts(null)
|
setParts(null)
|
||||||
|
|
||||||
prepareVM({ url, setStatus, setParts })
|
prepareVM({ url, setStatus, setParts })
|
||||||
|
|
||||||
|
return () => setParts(null)
|
||||||
}, [url])
|
}, [url])
|
||||||
|
|
||||||
return vmParts
|
return vmParts
|
||||||
? (<Player vmParts={vmParts} />)
|
? (<Player {...{
|
||||||
|
vmParts,
|
||||||
|
onFullscreenRequest,
|
||||||
|
setMenuOpen,
|
||||||
|
singleWindow,
|
||||||
|
}} />)
|
||||||
: (<Status {...status} />)
|
: (<Status {...status} />)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,37 +2,46 @@ import bocfel from 'emglken/src/bocfel.js'
|
||||||
import git from 'emglken/src/git.js'
|
import git from 'emglken/src/git.js'
|
||||||
import hugo from 'emglken/src/hugo.js'
|
import hugo from 'emglken/src/hugo.js'
|
||||||
import tads from 'emglken/src/tads.js'
|
import tads from 'emglken/src/tads.js'
|
||||||
|
import scare from 'emglken/src/scare.js'
|
||||||
|
|
||||||
const formats = [
|
const formats = [
|
||||||
{
|
{
|
||||||
id: 'bocfel',
|
id: 'bocfel',
|
||||||
extensions: /z([3458]|blorb)$/,
|
extensions: /z([3458]|blorb)$/,
|
||||||
engine: bocfel
|
engine: bocfel,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'git',
|
id: 'git',
|
||||||
extensions: /(gblorb|ulx)$/,
|
extensions: /(gblorb|ulx)$/,
|
||||||
engine: git
|
engine: git,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'hugo',
|
id: 'hugo',
|
||||||
extensions: /hex$/,
|
extensions: /hex$/,
|
||||||
engine: hugo
|
engine: hugo,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'scare',
|
||||||
|
extensions: /taf$/,
|
||||||
|
engine: scare,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'tads',
|
id: 'tads',
|
||||||
extensions: /(gam|t3)$/,
|
extensions: /(gam|t3)$/,
|
||||||
engine: tads
|
engine: tads,
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export const engineByFilename = filename => {
|
export const engineByFilename = (filename) => {
|
||||||
const format = formats.find(x =>
|
const format = formats.find((x) =>
|
||||||
x.extensions.test(filename))
|
x.extensions.test(filename))
|
||||||
|
|
||||||
if (format) {
|
if (format) {
|
||||||
return format.engine
|
return {
|
||||||
} else {
|
...format,
|
||||||
throw new Error('Unsupported file type')
|
/* @see staticFiles in package.json */
|
||||||
|
wasmBinaryName: `emglken/${format.id}-core.wasm`,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
throw new Error('Unsupported file type')
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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()
|
||||||
|
}
|
||||||
|
|
@ -1,80 +0,0 @@
|
||||||
.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);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ifplayer .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);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ifplayer .inputBox::placeholder {
|
|
||||||
color: var(--main-color);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ifplayer .inputBox:focus::placeholder {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ifplayer .inputBox::-webkit-search-cancel-button {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ifplayer .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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ifplayer .textBuffer::-webkit-scrollbar {
|
|
||||||
width: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ifplayer .textBuffer::-webkit-scrollbar-thumb {
|
|
||||||
background-color: var(--main-color);
|
|
||||||
border: 4px solid var(--bg-color);
|
|
||||||
border-left-width: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ifplayer .textBuffer > br:first-child,
|
|
||||||
.ifplayer .textBuffer > br:last-child,
|
|
||||||
.ifplayer .textBuffer > br + br + br {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status {
|
|
||||||
padding: var(--inner-padding);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status.loading > div:after {
|
|
||||||
animation: dots0123 1s infinite;
|
|
||||||
content: '';
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes dots0123 {
|
|
||||||
0% { content: ''; }
|
|
||||||
33% { content: '.'; }
|
|
||||||
66% { content: '..'; }
|
|
||||||
100% { content: '...'; }
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { h } from 'preact'
|
export default function ThemeSelector ({ themeEngine }) {
|
||||||
|
const options = themeEngine.themes.map((theme) => (
|
||||||
export default function ({ themeEngine }) {
|
<option
|
||||||
const options = themeEngine.themes.map(theme => (
|
key={theme}
|
||||||
<option value={theme}>
|
value={theme}>
|
||||||
{theme}
|
{theme}
|
||||||
</option>))
|
</option>))
|
||||||
|
|
||||||
|
|
|
||||||
60
src/index.js
60
src/index.js
|
|
@ -1,62 +1,10 @@
|
||||||
import { h, render } from 'preact'
|
import { render } from 'preact'
|
||||||
import { Route, Router, Switch } from 'wouter-preact'
|
|
||||||
|
|
||||||
import {
|
|
||||||
useHashLocation,
|
|
||||||
extractView
|
|
||||||
} from '~/src/utils/utils.routing'
|
|
||||||
import {
|
|
||||||
useThemeEngine
|
|
||||||
} from '~/src/themes/themes'
|
|
||||||
|
|
||||||
import HomeView from '~/src/views/HomeView/HomeView'
|
|
||||||
import GamesView from '~/src/views/GamesView/GamesView'
|
|
||||||
import PlayerView from '~/src/views/PlayerView/PlayerView'
|
|
||||||
import NotFoundView from '~/src/views/NotFoundView'
|
|
||||||
|
|
||||||
import '@fontsource/open-sans'
|
import '@fontsource/open-sans'
|
||||||
import '~/src/style/base.css'
|
|
||||||
|
|
||||||
function App () {
|
import './style/base.scss'
|
||||||
const themeEngine = useThemeEngine()
|
import './style/controls.scss'
|
||||||
const [location] = useHashLocation()
|
|
||||||
|
|
||||||
return (
|
import App from './App'
|
||||||
<Router hook={useHashLocation}>
|
|
||||||
<div className={[
|
|
||||||
'app',
|
|
||||||
extractView(location),
|
|
||||||
themeEngine.currentTheme].join(' ')}>
|
|
||||||
|
|
||||||
<Switch>
|
|
||||||
<Route path='/'>
|
|
||||||
<HomeView {...{
|
|
||||||
themeEngine
|
|
||||||
}} />
|
|
||||||
</Route>
|
|
||||||
<Route path='/games/'>
|
|
||||||
<GamesView />
|
|
||||||
</Route>
|
|
||||||
<Route path='/play/:encodedUrl'>
|
|
||||||
{params => <PlayerView {...{
|
|
||||||
...themeEngine,
|
|
||||||
...params
|
|
||||||
}} />}
|
|
||||||
</Route>
|
|
||||||
<Route path='/play/:encodedUrl/:theme'>
|
|
||||||
{params => <PlayerView {...{
|
|
||||||
...themeEngine,
|
|
||||||
...params
|
|
||||||
}} />}
|
|
||||||
</Route>
|
|
||||||
<Route>
|
|
||||||
<NotFoundView />
|
|
||||||
</Route>
|
|
||||||
</Switch>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</Router>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
render(<App />, document.getElementById('root'))
|
render(<App />, document.getElementById('root'))
|
||||||
|
|
|
||||||
42
src/routing.js
Normal file
42
src/routing.js
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
import {
|
||||||
|
useCallback, useEffect, useState,
|
||||||
|
} from 'preact/hooks'
|
||||||
|
|
||||||
|
const windowLocation = () =>
|
||||||
|
window.location.hash.replace('#', '') || '/'
|
||||||
|
|
||||||
|
export const buildPlayLinkHref = ({ url }) =>
|
||||||
|
`/#/play/${encodeURIComponent(url)}`
|
||||||
|
|
||||||
|
export const extractView = (location) => {
|
||||||
|
if (location === '/') return 'home'
|
||||||
|
|
||||||
|
const currentView = location.split('/').filter(Boolean)[0]
|
||||||
|
|
||||||
|
return currentView || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useHashLocation = () => {
|
||||||
|
const [currentLocation, setCurrentLocation] =
|
||||||
|
useState(windowLocation())
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onHashChange = () => {
|
||||||
|
let newLocation = windowLocation()
|
||||||
|
if (newLocation !== currentLocation) {
|
||||||
|
setCurrentLocation(newLocation)
|
||||||
|
window.scrollTo(0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onHashChange()
|
||||||
|
window.addEventListener('hashchange', onHashChange)
|
||||||
|
return () => window.removeEventListener('hashchange', onHashChange)
|
||||||
|
}, [currentLocation, setCurrentLocation])
|
||||||
|
|
||||||
|
const navigate = useCallback((to) => {
|
||||||
|
window.location.hash = to.replace('#/', '')
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return [currentLocation, navigate]
|
||||||
|
}
|
||||||
49
src/style/App.module.scss
Normal file
49
src/style/App.module.scss
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
.app {
|
||||||
|
min-height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
color: var(--main-color);
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
|
||||||
|
/* */
|
||||||
|
&.home, &.games, &.themes {
|
||||||
|
padding: var(--inner-padding);
|
||||||
|
|
||||||
|
/* Fix for Jumping Scrollbar Issue */
|
||||||
|
@media (min-width: 800px) {
|
||||||
|
padding-left: calc(100vw - 100% + var(--inner-padding));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Player view */
|
||||||
|
&.play, &.focus {
|
||||||
|
height: 100%;
|
||||||
|
max-height: 100dvh;
|
||||||
|
|
||||||
|
@media (min-width: 800px) {
|
||||||
|
& > main {
|
||||||
|
max-height: 90%;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* */
|
||||||
|
& > main {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
@media (min-width: 800px) {
|
||||||
|
margin: 5vh 0;
|
||||||
|
max-width: 800px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,92 +0,0 @@
|
||||||
/* Layout */
|
|
||||||
|
|
||||||
html, body {
|
|
||||||
margin: 0;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
font-family: 'Open Sans', sans-serif;
|
|
||||||
font-size: 18px;
|
|
||||||
line-height: 27px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#root {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app {
|
|
||||||
min-height: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
background-color: var(--bg-color);
|
|
||||||
color: var(--main-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.app > main {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
@media (min-width: 800px) {
|
|
||||||
.app > main {
|
|
||||||
margin: 5vh 0;
|
|
||||||
max-width: 800px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Common */
|
|
||||||
|
|
||||||
a,
|
|
||||||
summary {
|
|
||||||
display: inline;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
border-bottom: 2px solid currentColor;
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:focus,
|
|
||||||
a:hover,
|
|
||||||
summary:focus,
|
|
||||||
summary:hover {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
*:focus {
|
|
||||||
outline: 1px solid var(--main-color);
|
|
||||||
outline-offset: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
*::selection {
|
|
||||||
color: var(--bg-color);
|
|
||||||
background: var(--main-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
input::placeholder {
|
|
||||||
color: var(--main-color);
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
list-style: square;
|
|
||||||
}
|
|
||||||
|
|
||||||
hr {
|
|
||||||
border: 0;
|
|
||||||
height: 0;
|
|
||||||
border-top: 2px solid var(--main-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* */
|
|
||||||
|
|
||||||
.status {
|
|
||||||
padding: 8px;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
17
src/style/base.scss
Normal file
17
src/style/base.scss
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
/* Layout */
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
font-family: 'Open Sans', sans-serif;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 27px;
|
||||||
|
|
||||||
|
/* To prevent the white flash */
|
||||||
|
background: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
128
src/style/controls.scss
Normal file
128
src/style/controls.scss
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
/* Scrollbars */
|
||||||
|
|
||||||
|
body {
|
||||||
|
* {
|
||||||
|
scrollbar-color: var(--main-color) var(--bg-color);
|
||||||
|
scrollbar-width: thin;
|
||||||
|
}
|
||||||
|
|
||||||
|
*::-webkit-scrollbar {
|
||||||
|
width: var(--inner-padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
*::-webkit-scrollbar-thumb {
|
||||||
|
background-color: var(--main-color);
|
||||||
|
border: calc(0.5 * var(--inner-padding)) solid var(--bg-color);
|
||||||
|
border-left-width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Focus */
|
||||||
|
|
||||||
|
*:focus-visible {
|
||||||
|
outline: calc(0.5 * var(--separator-width)) solid var(--main-color);
|
||||||
|
outline-offset: calc(1px + var(--separator-width));
|
||||||
|
}
|
||||||
|
|
||||||
|
*::selection {
|
||||||
|
color: var(--bg-color);
|
||||||
|
background: var(--main-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Links */
|
||||||
|
|
||||||
|
a,
|
||||||
|
summary {
|
||||||
|
display: inline;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
border-bottom: var(--separator-width) solid currentColor;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:focus,
|
||||||
|
a:hover,
|
||||||
|
summary:focus,
|
||||||
|
summary:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Separators */
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: 0;
|
||||||
|
height: 0;
|
||||||
|
border-top: var(--separator-width) solid var(--main-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Lists */
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: square;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
|
||||||
|
button {
|
||||||
|
border: var(--separator-width) solid var(--main-color);
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
color: var(--main-color);
|
||||||
|
padding: var(--inner-padding) calc(4 * var(--inner-padding));
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inputs */
|
||||||
|
|
||||||
|
input::placeholder {
|
||||||
|
color: var(--main-color);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
select {
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
padding: calc(0.5 * var(--inner-padding)) var(--inner-padding);
|
||||||
|
color: var(--main-color);
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
border: var(--separator-width) solid var(--main-color);
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
width: 250px;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
border-radius: 0;
|
||||||
|
background-image: repeating-linear-gradient(
|
||||||
|
315deg,
|
||||||
|
var(--bg-color),
|
||||||
|
var(--bg-color) var(--inner-padding),
|
||||||
|
var(--main-color) var(--inner-padding),
|
||||||
|
var(--main-color) calc(var(--inner-padding) + var(--separator-width)),
|
||||||
|
var(--bg-color) calc(var(--inner-padding) + var(--separator-width)),
|
||||||
|
var(--bg-color) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='file'] {
|
||||||
|
position: relative;
|
||||||
|
font-size: 0;
|
||||||
|
text-indent: -100%;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
27
src/themes/_generator.js
Normal file
27
src/themes/_generator.js
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
/* @see https://github.com/monkeytypegame/monkeytype/blob/master/frontend/static/themes/_list.json */
|
||||||
|
const monkeyTypesThemes = [
|
||||||
|
/* Paste json here! */
|
||||||
|
].map((theme) => theme)
|
||||||
|
.filter((theme) => ![
|
||||||
|
'dark', 'solarized_dark', 'solarized_light',
|
||||||
|
].includes(theme.name))
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
|
||||||
|
const names = monkeyTypesThemes
|
||||||
|
.map(({ name }) => `'${name}',`).join('\n')
|
||||||
|
|
||||||
|
const css = monkeyTypesThemes
|
||||||
|
.map((theme) => [
|
||||||
|
`.${theme.name} {`,
|
||||||
|
` --bg-color: ${theme.bgColor};`,
|
||||||
|
` --main-color: ${theme.textColor};`,
|
||||||
|
` --accent-color: ${theme.mainColor};`,
|
||||||
|
` --input-color: ${theme.subColor};`,
|
||||||
|
'}\n'].join('\n'))
|
||||||
|
.join('\n')
|
||||||
|
|
||||||
|
console.log('/* List*/')
|
||||||
|
console.log(names)
|
||||||
|
|
||||||
|
console.log('/* CSS */')
|
||||||
|
console.log(css)
|
||||||
179
src/themes/themeList.js
Normal file
179
src/themes/themeList.js
Normal file
|
|
@ -0,0 +1,179 @@
|
||||||
|
export const themes = [
|
||||||
|
'light',
|
||||||
|
'dim',
|
||||||
|
'dark',
|
||||||
|
/* Solarized */
|
||||||
|
'solarized-light',
|
||||||
|
'solarized-dark',
|
||||||
|
/* Original */
|
||||||
|
'emo',
|
||||||
|
'redrum',
|
||||||
|
'toxin',
|
||||||
|
'valve',
|
||||||
|
'wasp',
|
||||||
|
/* Monkeytype */
|
||||||
|
/* @see https://github.com/monkeytypegame/monkeytype/blob/master/frontend/static/themes/_list.json */
|
||||||
|
'a8008',
|
||||||
|
'a80s_after_dark',
|
||||||
|
'a9009',
|
||||||
|
'aether',
|
||||||
|
'alduin',
|
||||||
|
'alpine',
|
||||||
|
'arch',
|
||||||
|
'aurora',
|
||||||
|
'beach',
|
||||||
|
'bento',
|
||||||
|
'bingsu',
|
||||||
|
'bliss',
|
||||||
|
'blue_dolphin',
|
||||||
|
'blueberry_dark',
|
||||||
|
'blueberry_light',
|
||||||
|
'botanical',
|
||||||
|
'bouquet',
|
||||||
|
'breeze',
|
||||||
|
'bushido',
|
||||||
|
'cafe',
|
||||||
|
'camping',
|
||||||
|
'carbon',
|
||||||
|
'catppuccin',
|
||||||
|
'chaos_theory',
|
||||||
|
'cheesecake',
|
||||||
|
'cherry_blossom',
|
||||||
|
'comfy',
|
||||||
|
'copper',
|
||||||
|
'creamsicle',
|
||||||
|
'cyberspace',
|
||||||
|
'dark_magic_girl',
|
||||||
|
'dark_note',
|
||||||
|
'darling',
|
||||||
|
'deku',
|
||||||
|
'desert_oasis',
|
||||||
|
'dev',
|
||||||
|
'diner',
|
||||||
|
'dino',
|
||||||
|
'dmg',
|
||||||
|
'dollar',
|
||||||
|
'dots',
|
||||||
|
'dracula',
|
||||||
|
'drowning',
|
||||||
|
'dualshot',
|
||||||
|
'earthsong',
|
||||||
|
'everblush',
|
||||||
|
'evil_eye',
|
||||||
|
'ez_mode',
|
||||||
|
'fire',
|
||||||
|
'fledgling',
|
||||||
|
'fleuriste',
|
||||||
|
'froyo',
|
||||||
|
'frozen_llama',
|
||||||
|
'fruit_chew',
|
||||||
|
'fundamentals',
|
||||||
|
'future_funk',
|
||||||
|
'godspeed',
|
||||||
|
'graen',
|
||||||
|
'grand_prix',
|
||||||
|
'gruvbox_dark',
|
||||||
|
'gruvbox_light',
|
||||||
|
'hammerhead',
|
||||||
|
'hanok',
|
||||||
|
'hedge',
|
||||||
|
'honey',
|
||||||
|
'horizon',
|
||||||
|
'husqy',
|
||||||
|
'iceberg_dark',
|
||||||
|
'iceberg_light',
|
||||||
|
'ishtar',
|
||||||
|
'iv_clover',
|
||||||
|
'iv_spade',
|
||||||
|
'joker',
|
||||||
|
'laser',
|
||||||
|
'lavender',
|
||||||
|
'leather',
|
||||||
|
'lil_dragon',
|
||||||
|
'lime',
|
||||||
|
'luna',
|
||||||
|
'magic_girl',
|
||||||
|
'mashu',
|
||||||
|
'matcha_moccha',
|
||||||
|
'material',
|
||||||
|
'matrix',
|
||||||
|
'menthol',
|
||||||
|
'metaverse',
|
||||||
|
'metropolis',
|
||||||
|
'mexican',
|
||||||
|
'miami',
|
||||||
|
'miami_nights',
|
||||||
|
'midnight',
|
||||||
|
'milkshake',
|
||||||
|
'mint',
|
||||||
|
'mizu',
|
||||||
|
'modern_dolch',
|
||||||
|
'modern_dolch_light',
|
||||||
|
'modern_ink',
|
||||||
|
'monokai',
|
||||||
|
'moonlight',
|
||||||
|
'mountain',
|
||||||
|
'mr_sleeves',
|
||||||
|
'ms_cupcakes',
|
||||||
|
'muted',
|
||||||
|
'nautilus',
|
||||||
|
'nebula',
|
||||||
|
'night_runner',
|
||||||
|
'nord',
|
||||||
|
'nord_light',
|
||||||
|
'norse',
|
||||||
|
'oblivion',
|
||||||
|
'olive',
|
||||||
|
'olivia',
|
||||||
|
'onedark',
|
||||||
|
'our_theme',
|
||||||
|
'paper',
|
||||||
|
'passion_fruit',
|
||||||
|
'pastel',
|
||||||
|
'peach_blossom',
|
||||||
|
'peaches',
|
||||||
|
'pink_lemonade',
|
||||||
|
'pulse',
|
||||||
|
'purpurite',
|
||||||
|
'red_dragon',
|
||||||
|
'red_samurai',
|
||||||
|
'repose_dark',
|
||||||
|
'repose_light',
|
||||||
|
'retro',
|
||||||
|
'retrocast',
|
||||||
|
'rose_pine',
|
||||||
|
'rose_pine_dawn',
|
||||||
|
'rose_pine_moon',
|
||||||
|
'rudy',
|
||||||
|
'ryujinscales',
|
||||||
|
'serika',
|
||||||
|
'serika_dark',
|
||||||
|
'sewing_tin',
|
||||||
|
'sewing_tin_light',
|
||||||
|
'shadow',
|
||||||
|
'shoko',
|
||||||
|
'slambook',
|
||||||
|
'snes',
|
||||||
|
'soaring_skies',
|
||||||
|
'sonokai',
|
||||||
|
'stealth',
|
||||||
|
'strawberry',
|
||||||
|
'striker',
|
||||||
|
'superuser',
|
||||||
|
'sweden',
|
||||||
|
'taro',
|
||||||
|
'terminal',
|
||||||
|
'terra',
|
||||||
|
'terror_below',
|
||||||
|
'tiramisu',
|
||||||
|
'trackday',
|
||||||
|
'trance',
|
||||||
|
'tron_orange',
|
||||||
|
'vaporwave',
|
||||||
|
'viridescent',
|
||||||
|
'voc',
|
||||||
|
'vscode',
|
||||||
|
'watermelon',
|
||||||
|
'wavez',
|
||||||
|
'witch_girl',
|
||||||
|
]
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,22 +1,9 @@
|
||||||
import { useState } from 'preact/hooks'
|
import { useState } from 'preact/hooks'
|
||||||
|
|
||||||
|
import { themes } from './themeList.js'
|
||||||
import './themes.css'
|
import './themes.css'
|
||||||
|
|
||||||
const themes = [
|
const LS_THEME_KEY = 'elseifplayer/theme'
|
||||||
'light',
|
|
||||||
'dim',
|
|
||||||
'dark',
|
|
||||||
'solarized-light',
|
|
||||||
'solarized-dark',
|
|
||||||
'emo',
|
|
||||||
'nord',
|
|
||||||
'redrum',
|
|
||||||
'toxin',
|
|
||||||
'valve',
|
|
||||||
'wasp'
|
|
||||||
]
|
|
||||||
|
|
||||||
const LS_THEME_KEY = 'ifplayer/theme'
|
|
||||||
const DEFAULT_THEME = themes[0]
|
const DEFAULT_THEME = themes[0]
|
||||||
|
|
||||||
const getSavedTheme = () => {
|
const getSavedTheme = () => {
|
||||||
|
|
@ -24,7 +11,7 @@ const getSavedTheme = () => {
|
||||||
return savedTheme || DEFAULT_THEME
|
return savedTheme || DEFAULT_THEME
|
||||||
}
|
}
|
||||||
|
|
||||||
const assertTheme = theme =>
|
const assertTheme = (theme) =>
|
||||||
themes.includes(theme)
|
themes.includes(theme)
|
||||||
? theme
|
? theme
|
||||||
: getSavedTheme()
|
: getSavedTheme()
|
||||||
|
|
@ -33,12 +20,17 @@ export const useThemeEngine = (initialTheme = getSavedTheme()) => {
|
||||||
const [currentTheme, setCurrentTheme] =
|
const [currentTheme, setCurrentTheme] =
|
||||||
useState(initialTheme)
|
useState(initialTheme)
|
||||||
|
|
||||||
const setTheme = theme => {
|
const setTheme = (theme) => {
|
||||||
const newTheme = assertTheme(theme)
|
const newTheme = assertTheme(theme)
|
||||||
|
|
||||||
setCurrentTheme(newTheme)
|
setCurrentTheme(newTheme)
|
||||||
localStorage.setItem(LS_THEME_KEY, newTheme)
|
localStorage.setItem(LS_THEME_KEY, newTheme)
|
||||||
}
|
}
|
||||||
|
|
||||||
return { currentTheme, setTheme, themes }
|
const setRandomTheme = () => {
|
||||||
|
const randomTheme = themes[Math.floor(Math.random() * themes.length)]
|
||||||
|
setTheme(randomTheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { currentTheme, setTheme, setRandomTheme, themes }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
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.addEventListener('hashchange', handler)
|
|
||||||
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 || ''
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
.app > .view.games {
|
|
||||||
padding: var(--inner-padding);
|
|
||||||
}
|
|
||||||
|
|
||||||
.view.games h4 {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.view.games li {
|
|
||||||
margin-bottom: 1em;
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { h } from 'preact'
|
|
||||||
import { Link } from 'wouter-preact'
|
import { Link } from 'wouter-preact'
|
||||||
|
|
||||||
import GameEntry from
|
import GameEntry from
|
||||||
|
|
@ -6,26 +5,25 @@ import GameEntry from
|
||||||
|
|
||||||
import top2019 from './top2019'
|
import top2019 from './top2019'
|
||||||
|
|
||||||
import './GamesView.css'
|
import * as s from './GamesView.module.scss'
|
||||||
|
|
||||||
const tutorialGame = {
|
const tutorialGame = {
|
||||||
name: 'The Dreamhold',
|
name: 'The Dreamhold',
|
||||||
ifdb: 'https://ifdb.org/viewgame?id=3myqnrs64nbtwdaz',
|
ifdb: 'https://ifdb.org/viewgame?id=3myqnrs64nbtwdaz',
|
||||||
url: 'https://mirror.ifarchive.org/if-archive/games/zcode/dreamhold.z8'
|
url: 'https://mirror.ifarchive.org/if-archive/games/zcode/dreamhold.z8',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function () {
|
export default function GamesView () {
|
||||||
return (
|
return (
|
||||||
<main className='view games'>
|
<main className={s.games}>
|
||||||
|
|
||||||
<h1>
|
<h1>
|
||||||
<a
|
<a
|
||||||
target='_blank'
|
target='_blank'
|
||||||
rel='noopener'
|
rel='noopener noreferrer'
|
||||||
href='https://ifdb.org/'
|
href='https://ifdb.org/'
|
||||||
title='The Interactive Fiction Database'>
|
title='The Interactive Fiction Database'>
|
||||||
IFDB
|
IFDB
|
||||||
</a> games
|
</a> Games
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
|
@ -33,25 +31,25 @@ export default function () {
|
||||||
go back</Link>.
|
go back</Link>.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>
|
<section className={s.tutorial}>
|
||||||
Tutorial
|
<h2>
|
||||||
</h2>
|
Tutorial
|
||||||
|
</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
If you are not familiar with Interactive Fiction,
|
If you are not familiar with Interactive Fiction,
|
||||||
you should start with this tutorial game
|
you should start with this tutorial game
|
||||||
by Andrew Plotkin:
|
by Andrew Plotkin:
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<GameEntry {...{
|
<GameEntry {...{
|
||||||
...tutorialGame
|
...tutorialGame,
|
||||||
}} />
|
}} />
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
</section>
|
||||||
<br />
|
|
||||||
|
|
||||||
<h2>
|
<h2>
|
||||||
Interactive Fiction Top 50 of All Time
|
Interactive Fiction Top 50 of All Time
|
||||||
|
|
@ -60,23 +58,23 @@ export default function () {
|
||||||
<p>
|
<p>
|
||||||
<a
|
<a
|
||||||
target='_blank'
|
target='_blank'
|
||||||
rel='noopener'
|
rel='noopener noreferrer'
|
||||||
href='https://ifdb.org/search?comp&sortby=awn&searchfor=series%3AInteractive+Fiction+Top+50+of+All+Time'>
|
href='https://ifdb.org/search?comp&sortby=awn&searchfor=series%3AInteractive+Fiction+Top+50+of+All+Time'>
|
||||||
Every four years </a>, Victor Gijsbers puts
|
Every four years </a>, Victor Gijsbers puts
|
||||||
together a list of the top 50 IF games of all time.
|
together a list of the top 50 IF games of all time.
|
||||||
|
|
||||||
Here is an almost complete version of the <a
|
Here is an almost complete and slightly rearranged version of the <a
|
||||||
target='_blank'
|
target='_blank'
|
||||||
rel='noopener'
|
rel='noopener noreferrer'
|
||||||
href='https://ifdb.org/viewcomp?id=1lv599reviaxvwo7'>
|
href='https://ifdb.org/viewcomp?id=1lv599reviaxvwo7'>
|
||||||
list for 2019</a>:
|
list from 2019</a>:
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ol>
|
<ol>
|
||||||
{top2019.map(game => (
|
{top2019.map((game) => (
|
||||||
<li>
|
<li key={game.name}>
|
||||||
<GameEntry {...{
|
<GameEntry {...{
|
||||||
...game
|
...game,
|
||||||
}} />
|
}} />
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
13
src/views/GamesView/GamesView.module.scss
Normal file
13
src/views/GamesView/GamesView.module.scss
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
.games {
|
||||||
|
.tutorial {
|
||||||
|
margin-block: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,20 +1,20 @@
|
||||||
export default [
|
export default [
|
||||||
|
[
|
||||||
|
'Lost Pig',
|
||||||
|
'https://ifdb.org/viewgame?id=mohwfk47yjzii14w',
|
||||||
|
'https://mirror.ifarchive.org/if-archive/games/zcode/LostPig.z8',
|
||||||
|
],
|
||||||
[
|
[
|
||||||
/* Check with cheap-glk */
|
/* Check with cheap-glk */
|
||||||
'Counterfeit Monkey',
|
'Counterfeit Monkey',
|
||||||
'https://ifdb.org/viewgame?id=aearuuxv83plclpl',
|
'https://ifdb.org/viewgame?id=aearuuxv83plclpl',
|
||||||
'https://mirror.ifarchive.org/if-archive/games/glulx/CounterfeitMonkey.gblorb'
|
'https://mirror.ifarchive.org/if-archive/games/glulx/CounterfeitMonkey.gblorb',
|
||||||
],
|
|
||||||
[
|
|
||||||
'Lost Pig',
|
|
||||||
'https://ifdb.org/viewgame?id=mohwfk47yjzii14w',
|
|
||||||
'https://mirror.ifarchive.org/if-archive/games/zcode/LostPig.z8'
|
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
/* Works. Check inputs */
|
/* Works. Check inputs */
|
||||||
'Anchorhead',
|
'Anchorhead',
|
||||||
'https://ifdb.org/viewgame?id=op0uw1gn1tjqmjt7',
|
'https://ifdb.org/viewgame?id=op0uw1gn1tjqmjt7',
|
||||||
'https://ifarchive.org/if-archive/games/zcode/anchor.z8'
|
'https://ifarchive.org/if-archive/games/zcode/anchor.z8',
|
||||||
],
|
],
|
||||||
/* [
|
/* [
|
||||||
'80 DAYS',
|
'80 DAYS',
|
||||||
|
|
@ -24,18 +24,18 @@ export default [
|
||||||
[
|
[
|
||||||
'Galatea',
|
'Galatea',
|
||||||
'https://ifdb.org/viewgame?id=urxrv27t7qtu52lb',
|
'https://ifdb.org/viewgame?id=urxrv27t7qtu52lb',
|
||||||
'https://mirror.ifarchive.org/if-archive/games/zcode/Galatea.zblorb'
|
'https://mirror.ifarchive.org/if-archive/games/zcode/Galatea.zblorb',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
/* Works. Check inputs */
|
/* Works. Check inputs */
|
||||||
'Photopia',
|
'Photopia',
|
||||||
'https://ifdb.org/viewgame?id=ju778uv5xaswnlpl',
|
'https://ifdb.org/viewgame?id=ju778uv5xaswnlpl',
|
||||||
'https://mirror.ifarchive.org/if-archive/games/zcode/photopia.z5'
|
'https://mirror.ifarchive.org/if-archive/games/zcode/photopia.z5',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'Spider and Web',
|
'Spider and Web',
|
||||||
'https://ifdb.org/viewgame?id=2xyccw3pe0uovfad',
|
'https://ifdb.org/viewgame?id=2xyccw3pe0uovfad',
|
||||||
'https://mirror.ifarchive.org/if-archive/games/zcode/Tangle.z5'
|
'https://mirror.ifarchive.org/if-archive/games/zcode/Tangle.z5',
|
||||||
],
|
],
|
||||||
/* [
|
/* [
|
||||||
'Trinity',
|
'Trinity',
|
||||||
|
|
@ -60,12 +60,12 @@ export default [
|
||||||
[
|
[
|
||||||
'Slouching Towards Bedlam',
|
'Slouching Towards Bedlam',
|
||||||
'https://ifdb.org/viewgame?id=032krqe6bjn5au78',
|
'https://ifdb.org/viewgame?id=032krqe6bjn5au78',
|
||||||
'https://mirror.ifarchive.org/if-archive/games/competition2003/zcode/slouch/slouch.z5'
|
'https://mirror.ifarchive.org/if-archive/games/competition2003/zcode/slouch/slouch.z5',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'Curses!',
|
'Curses!',
|
||||||
'https://ifdb.org/viewgame?id=plvzam05bmz3enh8',
|
'https://ifdb.org/viewgame?id=plvzam05bmz3enh8',
|
||||||
'https://mirror.ifarchive.org/if-archive/games/zcode/curses.z5'
|
'https://mirror.ifarchive.org/if-archive/games/zcode/curses.z5',
|
||||||
],
|
],
|
||||||
/* [
|
/* [
|
||||||
'howling dogs',
|
'howling dogs',
|
||||||
|
|
@ -75,12 +75,12 @@ export default [
|
||||||
[
|
[
|
||||||
'Violet',
|
'Violet',
|
||||||
'https://ifdb.org/viewgame?id=4glrrfh7wrp9zz7b',
|
'https://ifdb.org/viewgame?id=4glrrfh7wrp9zz7b',
|
||||||
'https://mirror.ifarchive.org/if-archive/games/zcode/Violet.zblorb'
|
'https://mirror.ifarchive.org/if-archive/games/zcode/Violet.zblorb',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'The Wizard Sniffer',
|
'The Wizard Sniffer',
|
||||||
'https://ifdb.org/viewgame?id=uq18rw9gt8j58da',
|
'https://ifdb.org/viewgame?id=uq18rw9gt8j58da',
|
||||||
'https://ifarchive.org/if-archive/games/competition2017/The%20Wizard%20Sniffer/The_Wizard_Sniffer.gblorb'
|
'https://ifarchive.org/if-archive/games/competition2017/The%20Wizard%20Sniffer/The_Wizard_Sniffer.gblorb',
|
||||||
],
|
],
|
||||||
/* [
|
/* [
|
||||||
'Eat Me',
|
'Eat Me',
|
||||||
|
|
@ -100,12 +100,12 @@ export default [
|
||||||
[
|
[
|
||||||
'Shade',
|
'Shade',
|
||||||
'https://ifdb.org/viewgame?id=hsfc7fnl40k4a30q',
|
'https://ifdb.org/viewgame?id=hsfc7fnl40k4a30q',
|
||||||
'https://mirror.ifarchive.org/if-archive/games/zcode/shade.z5'
|
'https://mirror.ifarchive.org/if-archive/games/zcode/shade.z5',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'Vespers',
|
'Vespers',
|
||||||
'https://ifdb.org/viewgame?id=6dj2vguyiagrhvc2',
|
'https://ifdb.org/viewgame?id=6dj2vguyiagrhvc2',
|
||||||
'https://mirror.ifarchive.org/if-archive/games/zcode/vespers.z8'
|
'https://mirror.ifarchive.org/if-archive/games/zcode/vespers.z8',
|
||||||
],
|
],
|
||||||
/* [
|
/* [
|
||||||
'Will Not Let Me Go',
|
'Will Not Let Me Go',
|
||||||
|
|
@ -135,7 +135,7 @@ export default [
|
||||||
[
|
[
|
||||||
'Savoir-Faire',
|
'Savoir-Faire',
|
||||||
'https://ifdb.org/viewgame?id=p0cizeb3kiwzlm2p',
|
'https://ifdb.org/viewgame?id=p0cizeb3kiwzlm2p',
|
||||||
'https://mirror.ifarchive.org/if-archive/games/zcode/Savoir-Faire.zblorb'
|
'https://mirror.ifarchive.org/if-archive/games/zcode/Savoir-Faire.zblorb',
|
||||||
],
|
],
|
||||||
/* [
|
/* [
|
||||||
'With Those We Love Alive',
|
'With Those We Love Alive',
|
||||||
|
|
@ -145,7 +145,7 @@ export default [
|
||||||
[
|
[
|
||||||
'Aisle',
|
'Aisle',
|
||||||
'https://ifdb.org/viewgame?id=j49crlvd62mhwuzu',
|
'https://ifdb.org/viewgame?id=j49crlvd62mhwuzu',
|
||||||
'https://mirror.ifarchive.org/if-archive/games/zcode/Aisle.z5'
|
'https://mirror.ifarchive.org/if-archive/games/zcode/Aisle.z5',
|
||||||
],
|
],
|
||||||
/* [
|
/* [
|
||||||
'Blue Lacuna',
|
'Blue Lacuna',
|
||||||
|
|
@ -155,7 +155,7 @@ export default [
|
||||||
[
|
[
|
||||||
'Gun Mute',
|
'Gun Mute',
|
||||||
'https://ifdb.org/viewgame?id=xwedbibfksczn7eq',
|
'https://ifdb.org/viewgame?id=xwedbibfksczn7eq',
|
||||||
'https://mirror.ifarchive.org/if-archive/games/tads/GunMute.t3'
|
'https://mirror.ifarchive.org/if-archive/games/tads/GunMute.t3',
|
||||||
],
|
],
|
||||||
/* [
|
/* [
|
||||||
'The King of Shreds and Patches',
|
'The King of Shreds and Patches',
|
||||||
|
|
@ -180,7 +180,7 @@ export default [
|
||||||
[
|
[
|
||||||
'A Beauty Cold and Austere',
|
'A Beauty Cold and Austere',
|
||||||
'https://ifdb.org/viewgame?id=y9y7jozi0l76bb82',
|
'https://ifdb.org/viewgame?id=y9y7jozi0l76bb82',
|
||||||
'https://ifarchive.org/if-archive/games/competition2017/A%20Beauty%20Cold%20and%20Austere/A_Beauty_Cold_and_Austere.gblorb'
|
'https://ifarchive.org/if-archive/games/competition2017/A%20Beauty%20Cold%20and%20Austere/A_Beauty_Cold_and_Austere.gblorb',
|
||||||
],
|
],
|
||||||
/* [
|
/* [
|
||||||
'Cactus Blue Motel',
|
'Cactus Blue Motel',
|
||||||
|
|
@ -190,7 +190,7 @@ export default [
|
||||||
[
|
[
|
||||||
'Coloratura',
|
'Coloratura',
|
||||||
'https://ifdb.org/viewgame?id=g0fl99ovcrq2sqzk',
|
'https://ifdb.org/viewgame?id=g0fl99ovcrq2sqzk',
|
||||||
'https://mirror.ifarchive.org/if-archive/games/competition2013/glulx/coloratura/Coloratura.gblorb'
|
'https://mirror.ifarchive.org/if-archive/games/competition2013/glulx/coloratura/Coloratura.gblorb',
|
||||||
],
|
],
|
||||||
/* [
|
/* [
|
||||||
'Harmonia',
|
'Harmonia',
|
||||||
|
|
@ -200,12 +200,12 @@ export default [
|
||||||
[
|
[
|
||||||
'Lime Ergot',
|
'Lime Ergot',
|
||||||
'https://ifdb.org/viewgame?id=b8mb4fcwmf1hrxl',
|
'https://ifdb.org/viewgame?id=b8mb4fcwmf1hrxl',
|
||||||
'https://mirror.ifarchive.org/if-archive/games/glulx/Lime_Ergot.gblorb'
|
'https://mirror.ifarchive.org/if-archive/games/glulx/Lime_Ergot.gblorb',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'Rameses',
|
'Rameses',
|
||||||
'https://ifdb.org/viewgame?id=0stz0hr7a98bp9mp',
|
'https://ifdb.org/viewgame?id=0stz0hr7a98bp9mp',
|
||||||
'https://mirror.ifarchive.org/if-archive/games/zcode/rameses.zblorb'
|
'https://mirror.ifarchive.org/if-archive/games/zcode/rameses.zblorb',
|
||||||
],
|
],
|
||||||
/* [
|
/* [
|
||||||
'Spellbreaker',
|
'Spellbreaker',
|
||||||
|
|
@ -220,7 +220,7 @@ export default [
|
||||||
[
|
[
|
||||||
'The Wand',
|
'The Wand',
|
||||||
'https://ifdb.org/viewgame?id=2jil5vbxmbv8riv1',
|
'https://ifdb.org/viewgame?id=2jil5vbxmbv8riv1',
|
||||||
'https://ifarchive.org/if-archive/games/glulx/Wand.ulx'
|
'https://ifarchive.org/if-archive/games/glulx/Wand.ulx',
|
||||||
],
|
],
|
||||||
/* [
|
/* [
|
||||||
'Zork I',
|
'Zork I',
|
||||||
|
|
@ -230,17 +230,17 @@ export default [
|
||||||
[
|
[
|
||||||
'1893: A World\'s Fair Mystery',
|
'1893: A World\'s Fair Mystery',
|
||||||
'https://ifdb.org/viewgame?id=00e0t7swrris5pg6',
|
'https://ifdb.org/viewgame?id=00e0t7swrris5pg6',
|
||||||
'https://mirror.ifarchive.org/if-archive/games/tads/1893.gam'
|
'https://mirror.ifarchive.org/if-archive/games/tads/1893.gam',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'Adventure',
|
'Adventure',
|
||||||
'https://ifdb.org/viewgame?id=fft6pu91j85y4acv',
|
'https://ifdb.org/viewgame?id=fft6pu91j85y4acv',
|
||||||
'https://mirror.ifarchive.org/if-archive/games/zcode/Advent.z5'
|
'https://mirror.ifarchive.org/if-archive/games/zcode/Advent.z5',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'Alias \'The Magpie\'',
|
'Alias \'The Magpie\'',
|
||||||
'https://ifdb.org/viewgame?id=yspn49v69hzc8rtb',
|
'https://ifdb.org/viewgame?id=yspn49v69hzc8rtb',
|
||||||
'https://ifarchive.org/if-archive/games/competition2018/Alias%20The%20Magpie/Alias%20%27The%20Magpie%27.gblorb'
|
'https://ifarchive.org/if-archive/games/competition2018/Alias%20The%20Magpie/Alias%20%27The%20Magpie%27.gblorb',
|
||||||
],
|
],
|
||||||
/* [
|
/* [
|
||||||
'De Baron',
|
'De Baron',
|
||||||
|
|
@ -255,22 +255,22 @@ export default [
|
||||||
[
|
[
|
||||||
'Cragne Manor',
|
'Cragne Manor',
|
||||||
'https://ifdb.org/viewgame?id=4x7nltu8p851tn4x',
|
'https://ifdb.org/viewgame?id=4x7nltu8p851tn4x',
|
||||||
'https://mirror.ifarchive.org/if-archive/games/glulx/cragne.gblorb'
|
'https://mirror.ifarchive.org/if-archive/games/glulx/cragne.gblorb',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'The Edifice',
|
'The Edifice',
|
||||||
'https://ifdb.org/viewgame?id=4tb9soabrb4apqzd',
|
'https://ifdb.org/viewgame?id=4tb9soabrb4apqzd',
|
||||||
'https://mirror.ifarchive.org/if-archive/games/zcode/edifice.z5'
|
'https://mirror.ifarchive.org/if-archive/games/zcode/edifice.z5',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'Endless, Nameless',
|
'Endless, Nameless',
|
||||||
'https://ifdb.org/viewgame?id=7vtm1rq16hh3xch',
|
'https://ifdb.org/viewgame?id=7vtm1rq16hh3xch',
|
||||||
'https://ifarchive.org/if-archive/games/zcode/nameless.z8'
|
'https://ifarchive.org/if-archive/games/zcode/nameless.z8',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'Everybody Dies',
|
'Everybody Dies',
|
||||||
'https://ifdb.org/viewgame?id=lyblvftb8xtlo0a1',
|
'https://ifdb.org/viewgame?id=lyblvftb8xtlo0a1',
|
||||||
'https://mirror.ifarchive.org/if-archive/games/competition2008/glulx/everybodydies/EverybodyDies.gblorb'
|
'https://mirror.ifarchive.org/if-archive/games/competition2008/glulx/everybodydies/EverybodyDies.gblorb',
|
||||||
],
|
],
|
||||||
/* [
|
/* [
|
||||||
'Fallen London',
|
'Fallen London',
|
||||||
|
|
@ -280,12 +280,12 @@ export default [
|
||||||
[
|
[
|
||||||
'Foo Foo',
|
'Foo Foo',
|
||||||
'https://ifdb.org/viewgame?id=ec6x9y8qcmsrxob9',
|
'https://ifdb.org/viewgame?id=ec6x9y8qcmsrxob9',
|
||||||
'https://ifarchive.org/if-archive/games/springthing/2016/FooFoo.gblorb'
|
'https://ifarchive.org/if-archive/games/springthing/2016/FooFoo.gblorb',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'The Gostak',
|
'The Gostak',
|
||||||
'https://ifdb.org/viewgame?id=w5s3sv43s3p98v45',
|
'https://ifdb.org/viewgame?id=w5s3sv43s3p98v45',
|
||||||
'https://mirror.ifarchive.org/if-archive/games/zcode/gostak.z5'
|
'https://mirror.ifarchive.org/if-archive/games/zcode/gostak.z5',
|
||||||
],
|
],
|
||||||
/* [
|
/* [
|
||||||
'The Hitchhiker\'s Guide to the Galaxy',
|
'The Hitchhiker\'s Guide to the Galaxy',
|
||||||
|
|
@ -305,27 +305,27 @@ export default [
|
||||||
[
|
[
|
||||||
'Inside the Facility',
|
'Inside the Facility',
|
||||||
'https://ifdb.org/viewgame?id=stsdri5zh7a4i5my',
|
'https://ifdb.org/viewgame?id=stsdri5zh7a4i5my',
|
||||||
'https://ifarchive.org/if-archive/games/competition2016/Inside%20the%20Facility/Facility.z8'
|
'https://ifarchive.org/if-archive/games/competition2016/Inside%20the%20Facility/Facility.z8',
|
||||||
],
|
],
|
||||||
[
|
/* [
|
||||||
'Junior Arithmancer',
|
'Junior Arithmancer',
|
||||||
'https://ifdb.org/viewgame?id=pw1rbjt1t4n4n87s',
|
'https://ifdb.org/viewgame?id=pw1rbjt1t4n4n87s',
|
||||||
'https://ifarchive.org/if-archive/games/competition2018/Junior%20Arithmancer/Junior_Arithmancer.gblorb'
|
'https://ifarchive.org/if-archive/games/competition2018/Junior%20Arithmancer/Junior_Arithmancer.gblorb',
|
||||||
],
|
], */
|
||||||
[
|
[
|
||||||
'Make It Good',
|
'Make It Good',
|
||||||
'https://ifdb.org/viewgame?id=jdrbw1htq4ah8q57',
|
'https://ifdb.org/viewgame?id=jdrbw1htq4ah8q57',
|
||||||
'https://mirror.ifarchive.org/if-archive/games/zcode/MakeItGood.zblorb'
|
'https://mirror.ifarchive.org/if-archive/games/zcode/MakeItGood.z8',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'Sub Rosa',
|
'Sub Rosa',
|
||||||
'https://ifdb.org/viewgame?id=73nvz9yui87ub3sd',
|
'https://ifdb.org/viewgame?id=73nvz9yui87ub3sd',
|
||||||
'https://mirror.ifarchive.org/if-archive/games/glulx/Sub_Rosa.gblorb'
|
'https://mirror.ifarchive.org/if-archive/games/glulx/Sub_Rosa.gblorb',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'Suveh Nux',
|
'Suveh Nux',
|
||||||
'https://ifdb.org/viewgame?id=xkai23ry99qdxce3',
|
'https://ifdb.org/viewgame?id=xkai23ry99qdxce3',
|
||||||
'https://mirror.ifarchive.org/if-archive/games/zcode/suvehnux.z5'
|
'https://mirror.ifarchive.org/if-archive/games/zcode/suvehnux.z5',
|
||||||
],
|
],
|
||||||
/* [
|
/* [
|
||||||
'their angelical understanding',
|
'their angelical understanding',
|
||||||
|
|
@ -340,6 +340,6 @@ export default [
|
||||||
[
|
[
|
||||||
'Varicella',
|
'Varicella',
|
||||||
'https://ifdb.org/viewgame?id=ywwlr3tpxnktjasd',
|
'https://ifdb.org/viewgame?id=ywwlr3tpxnktjasd',
|
||||||
'https://mirror.ifarchive.org/if-archive/games/zcode/vgame.z8'
|
'https://mirror.ifarchive.org/if-archive/games/zcode/vgame.z8',
|
||||||
]
|
],
|
||||||
].map(([name, ifdb, url]) => ({ name, ifdb, url }))
|
].map(([name, ifdb, url]) => ({ name, ifdb, url }))
|
||||||
|
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
.app > .view.home {
|
|
||||||
padding: var(--inner-padding);
|
|
||||||
}
|
|
||||||
|
|
||||||
.view.home input,
|
|
||||||
.view.home select {
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
padding: 4px 8px;
|
|
||||||
color: var(--main-color);
|
|
||||||
background-color: var(--bg-color);
|
|
||||||
border: 2px solid var(--main-color);
|
|
||||||
outline-offset: 0;
|
|
||||||
font: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.view.home select {
|
|
||||||
cursor: pointer;
|
|
||||||
appearance: none;
|
|
||||||
width: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.view.home label {
|
|
||||||
display: inline-block;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.view.home label input {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.view.home input[type='file'] {
|
|
||||||
position: relative;
|
|
||||||
font-size: 0;
|
|
||||||
text-indent: -100%;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import { h } from 'preact'
|
|
||||||
import { Link } from 'wouter-preact'
|
import { Link } from 'wouter-preact'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useHashLocation,
|
useHashLocation,
|
||||||
buildPlayLinkHref
|
buildPlayLinkHref,
|
||||||
} from '~/src/utils/utils.routing'
|
} from '~/src/routing'
|
||||||
|
|
||||||
import LocalFileSelector from
|
import LocalFileSelector from
|
||||||
'~/src/components/FileSelector/LocalFileSelector'
|
'~/src/components/FileSelector/LocalFileSelector'
|
||||||
|
|
@ -13,15 +12,13 @@ import TargetURLSelector from
|
||||||
import ThemeSelector from
|
import ThemeSelector from
|
||||||
'~/src/components/ThemeSelector/ThemeSelector'
|
'~/src/components/ThemeSelector/ThemeSelector'
|
||||||
|
|
||||||
import './HomeView.css'
|
export default function HomeView ({ themeEngine }) {
|
||||||
|
|
||||||
export default function ({ themeEngine }) {
|
|
||||||
const setLocation = useHashLocation()[1]
|
const setLocation = useHashLocation()[1]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className='view home'>
|
<main>
|
||||||
<h1>
|
<h1>
|
||||||
ifplayer
|
ElseIFPlayer
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
|
|
@ -31,8 +28,8 @@ export default function ({ themeEngine }) {
|
||||||
<br />
|
<br />
|
||||||
Source code can be found in this <a
|
Source code can be found in this <a
|
||||||
target='_blank'
|
target='_blank'
|
||||||
rel='noopener'
|
rel='noopener noreferrer'
|
||||||
href='https://github.com/He4eT/ifplayer'>
|
href='https://github.com/He4eT/elseifplayer'>
|
||||||
repository
|
repository
|
||||||
</a>.
|
</a>.
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -42,13 +39,18 @@ export default function ({ themeEngine }) {
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2>
|
<h2>
|
||||||
Interface theme
|
Interface Theme
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<ThemeSelector {...{
|
<ThemeSelector {...{
|
||||||
themeEngine
|
themeEngine,
|
||||||
}} />
|
}} />
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Preview and choose from available themes on the <Link href={'/#/themes/'}>
|
||||||
|
themes page
|
||||||
|
</Link>.
|
||||||
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<small>
|
<small>
|
||||||
Double-click the input field during the game
|
Double-click the input field during the game
|
||||||
|
|
@ -61,7 +63,7 @@ export default function ({ themeEngine }) {
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2>
|
<h2>
|
||||||
Play a game from the list
|
Play a Game from the List
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
|
@ -75,18 +77,19 @@ export default function ({ themeEngine }) {
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2>
|
<h2>
|
||||||
Play the game from a file
|
Play the Game from a File
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<details>
|
<details>
|
||||||
<summary>Supported formats</summary>
|
<summary>Supported formats</summary>
|
||||||
|
<p>Text-only games are supported:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>TADS games (.t3, .gam);</li>
|
<li>ADRIFT 4 (.taf)</li>
|
||||||
<li>Z-machine games (.z3, .z4, .z5, .z8, .blorb);</li>
|
<li>Glulx (.gblorb, .ulx)</li>
|
||||||
<li>Glulx VM games (.gblorb, .ulx);</li>
|
<li>Hugo (.hex)</li>
|
||||||
<li>Hugo games (.hex);</li>
|
<li>TADS 2/3 (.gam, .t3)</li>
|
||||||
<li>Text-only games are supported;</li>
|
<li>Z-code (.z3, .z4, .z5, .z8, .blorb)</li>
|
||||||
</ul>
|
</ul>
|
||||||
</details>
|
</details>
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -97,7 +100,7 @@ export default function ({ themeEngine }) {
|
||||||
<LocalFileSelector {...{
|
<LocalFileSelector {...{
|
||||||
setLocation,
|
setLocation,
|
||||||
buildLink: buildPlayLinkHref,
|
buildLink: buildPlayLinkHref,
|
||||||
theme: themeEngine.currentTheme
|
theme: themeEngine.currentTheme,
|
||||||
}} />
|
}} />
|
||||||
</label>
|
</label>
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -108,7 +111,7 @@ export default function ({ themeEngine }) {
|
||||||
<TargetURLSelector {...{
|
<TargetURLSelector {...{
|
||||||
setLocation,
|
setLocation,
|
||||||
buildLink: buildPlayLinkHref,
|
buildLink: buildPlayLinkHref,
|
||||||
theme: themeEngine.currentTheme
|
theme: themeEngine.currentTheme,
|
||||||
}} />
|
}} />
|
||||||
</label>
|
</label>
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
import { h } from 'preact'
|
|
||||||
import { Link } from 'wouter-preact'
|
|
||||||
|
|
||||||
export default () => (
|
|
||||||
<main>
|
|
||||||
<div class='status'>
|
|
||||||
<h1>
|
|
||||||
404
|
|
||||||
</h1>
|
|
||||||
<p>
|
|
||||||
Page not found
|
|
||||||
</p>
|
|
||||||
<hr />
|
|
||||||
<Link href='/'>
|
|
||||||
Home
|
|
||||||
</Link>
|
|
||||||
|
|
|
||||||
<a
|
|
||||||
target='_blank'
|
|
||||||
rel='noopener'
|
|
||||||
href='https://github.com/He4eT/ifplayer/issues'>
|
|
||||||
Report bug
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
)
|
|
||||||
10
src/views/NotFoundView/NotFoundView.jsx
Normal file
10
src/views/NotFoundView/NotFoundView.jsx
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import Status from '~/src/components/Player/Status/Status'
|
||||||
|
|
||||||
|
export default function NotFoundView () {
|
||||||
|
return <main>
|
||||||
|
<Status
|
||||||
|
stage='fail'
|
||||||
|
details={['404', 'Page Not Found']}
|
||||||
|
/>
|
||||||
|
</main>
|
||||||
|
}
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
.app.play {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 800px) {
|
|
||||||
.app.play main {
|
|
||||||
max-height: 90%;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,14 +1,16 @@
|
||||||
import { h } from 'preact'
|
|
||||||
import { useState, useEffect } from 'preact/hooks'
|
import { useState, useEffect } from 'preact/hooks'
|
||||||
|
|
||||||
import UrlPlayer from '~/src/components/Player/UrlPlayer'
|
import UrlPlayer from '~/src/components/Player/UrlPlayer'
|
||||||
|
import MenuOverlay from '~/src/components/Player/MenuOverlay/MenuOverlay'
|
||||||
|
|
||||||
import './PlayerView.css'
|
const decode = (encodedUrl) => decodeURIComponent(encodedUrl)
|
||||||
|
|
||||||
const decode = encodedUrl => decodeURIComponent(encodedUrl)
|
export default function PlayerView ({
|
||||||
|
theme, themeEngine, encodedUrl, singleWindow,
|
||||||
export default function ({ setTheme, theme, encodedUrl }) {
|
}) {
|
||||||
useEffect(() => setTheme(theme), [theme])
|
useEffect(() => {
|
||||||
|
themeEngine.setTheme(theme)
|
||||||
|
}, [theme, themeEngine])
|
||||||
|
|
||||||
const [targetUrl, setTargetUrl] = useState(decode(encodedUrl))
|
const [targetUrl, setTargetUrl] = useState(decode(encodedUrl))
|
||||||
|
|
||||||
|
|
@ -16,9 +18,26 @@ export default function ({ setTheme, theme, encodedUrl }) {
|
||||||
setTargetUrl(decode(encodedUrl))
|
setTargetUrl(decode(encodedUrl))
|
||||||
}, [encodedUrl])
|
}, [encodedUrl])
|
||||||
|
|
||||||
|
const [menuOpen, setMenuOpen] = useState(false)
|
||||||
|
|
||||||
|
const onFullscreenRequest = () => {
|
||||||
|
document.documentElement.requestFullscreen()
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<UrlPlayer url={targetUrl} />
|
<MenuOverlay {...{
|
||||||
|
themeEngine,
|
||||||
|
onFullscreenRequest,
|
||||||
|
menuOpen,
|
||||||
|
setMenuOpen,
|
||||||
|
}} />
|
||||||
|
<UrlPlayer {...{
|
||||||
|
url: targetUrl,
|
||||||
|
onFullscreenRequest,
|
||||||
|
setMenuOpen,
|
||||||
|
singleWindow,
|
||||||
|
}} />
|
||||||
</main>
|
</main>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
73
src/views/ThemesView/ThemesView.jsx
Normal file
73
src/views/ThemesView/ThemesView.jsx
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { Link } from 'wouter-preact'
|
||||||
|
|
||||||
|
import * as s from './ThemesView.module.scss'
|
||||||
|
|
||||||
|
const Preview = (themeEngine, theme) =>
|
||||||
|
<section key={theme} className={[s.themePreview, theme].join(' ')}>
|
||||||
|
<div className={s.output}>
|
||||||
|
<div className={[s.message, s.input].join(' ')}>
|
||||||
|
> look
|
||||||
|
</div>
|
||||||
|
<div><br /></div>
|
||||||
|
<div className={[s.message, s.subheader].join(' ')}>
|
||||||
|
{theme}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Observe a vibrant demonstration of colors at work,
|
||||||
|
showcasing their versatile usage right before your eyes.
|
||||||
|
</div>
|
||||||
|
<div><br /></div>
|
||||||
|
</div>
|
||||||
|
<button onClick={() => themeEngine.setTheme(theme)}>
|
||||||
|
Apply this colors
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
export default function ThemesView ({ themeEngine }) {
|
||||||
|
const themes = themeEngine
|
||||||
|
.themes
|
||||||
|
.map((theme) => Preview(themeEngine, theme))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className={s.themes}>
|
||||||
|
<h1>
|
||||||
|
Themes Page
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Choose one or <Link href='/'>
|
||||||
|
go back</Link>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<section className={[s.themePreview, s.current].join(' ')}>
|
||||||
|
<h2>
|
||||||
|
Current Theme
|
||||||
|
</h2>
|
||||||
|
<div className={s.output}>
|
||||||
|
<div className={[s.message, s.input].join(' ')}>
|
||||||
|
> look
|
||||||
|
</div>
|
||||||
|
<div><br /></div>
|
||||||
|
<div className={[s.message, s.subheader].join(' ')}>
|
||||||
|
Selected: {themeEngine.currentTheme}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
You can set random one with the button below
|
||||||
|
or choose any theme from the list.
|
||||||
|
</div>
|
||||||
|
<div><br /></div>
|
||||||
|
</div>
|
||||||
|
<button onClick={() => themeEngine.setRandomTheme()}>
|
||||||
|
Set a random theme
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<h2>
|
||||||
|
Theme List
|
||||||
|
</h2>
|
||||||
|
<section>
|
||||||
|
{themes}
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
38
src/views/ThemesView/ThemesView.module.scss
Normal file
38
src/views/ThemesView/ThemesView.module.scss
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
.themes {
|
||||||
|
--current-border: var(--main-color);
|
||||||
|
|
||||||
|
.themePreview {
|
||||||
|
border: 2px solid var(--current-border);
|
||||||
|
padding: calc(2 * var(--inner-padding));
|
||||||
|
margin-bottom: 32px;
|
||||||
|
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
color: var(--main-color);
|
||||||
|
|
||||||
|
&.current {
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
margin-block: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.output {
|
||||||
|
border: 2px solid var(--main-color);
|
||||||
|
padding: var(--inner-padding);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
.message.subheader {
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--accent-color);
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.input {
|
||||||
|
color: var(--input-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue