├── .gitmodules
├── src
├── layers
│ ├── controls
│ │ ├── layout.ts
│ │ ├── mouse
│ │ │ ├── mouse-not-locked.ts
│ │ │ ├── mouse-swipe.ts
│ │ │ ├── mouse-locked.ts
│ │ │ ├── mouse-nipple.ts
│ │ │ └── mouse-common.ts
│ │ ├── keyboard.ts
│ │ ├── legacy-layers-control.ts
│ │ ├── nipple.ts
│ │ ├── layers-config.ts
│ │ ├── options.ts
│ │ └── grid.ts
│ ├── instance.ts
│ └── dom
│ │ ├── mem-storage.ts
│ │ ├── helpers.ts
│ │ ├── lifecycle.ts
│ │ ├── layers.ts
│ │ └── storage.ts
├── vite-env.d.ts
├── base.css
├── index.css
├── window
│ ├── error-window.tsx
│ ├── file-input.ts
│ ├── dos
│ │ ├── controls
│ │ │ ├── mouse
│ │ │ │ ├── mouse-locked.ts
│ │ │ │ ├── mouse-swipe.ts
│ │ │ │ ├── mouse-default.ts
│ │ │ │ ├── mount.ts
│ │ │ │ └── pointer.ts
│ │ │ ├── mouse.ts
│ │ │ └── keyboard.ts
│ │ ├── render
│ │ │ ├── resize.ts
│ │ │ ├── canvas.ts
│ │ │ └── webgl.ts
│ │ └── sound
│ │ │ └── audio-node.ts
│ ├── window.css
│ ├── loading-window.tsx
│ ├── window.tsx
│ └── select-window.tsx
├── components
│ ├── components.css
│ ├── lock.tsx
│ ├── close-button.tsx
│ ├── error.tsx
│ ├── loading.tsx
│ ├── select.tsx
│ ├── checkbox.tsx
│ ├── dos-option-slider.tsx
│ ├── dos-option-select.tsx
│ ├── dos-option-checkbox.tsx
│ └── slider.tsx
├── store
│ ├── init.ts
│ ├── storage.ts
│ ├── auth.ts
│ └── editor.ts
├── download-file.ts
├── sidebar
│ ├── diskette-icon.tsx
│ ├── network-button.tsx
│ ├── sidebar.css
│ ├── fullscreen-button.tsx
│ ├── sidebar.tsx
│ └── save-buttons.tsx
├── v8
│ ├── config.ts
│ └── changes.ts
├── frame
│ ├── settings-frame.tsx
│ ├── prerun-frame.tsx
│ ├── frame.tsx
│ ├── editor
│ │ ├── editor-frame.css
│ │ └── editor-conf-frame.tsx
│ ├── frame.css
│ ├── stats-frame.tsx
│ └── network-frame.tsx
├── app.css
├── host
│ ├── fullscreen.ts
│ ├── bundle-storage.ts
│ ├── lstorage.ts
│ └── idb.ts
├── store.ts
├── public
│ └── types.ts
├── ui.tsx
├── player-api-load.ts
└── player-api.ts
├── postcss.config.cjs
├── tsconfig.node.json
├── .npmignore
├── .gitignore
├── vite.config.ts
├── .eslintrc.json
├── tsconfig.json
├── README.deployment.md
├── scripts
└── brotli-dist.py
├── package.json
├── tailwind.config.cjs
├── .github
└── workflows
│ └── build.yml
├── ChangeLog.md
├── README.md
└── index.html
/.gitmodules:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/layers/controls/layout.ts:
--------------------------------------------------------------------------------
1 | export interface LayoutPosition {
2 | left?: 1 | 2,
3 | top?: 1 | 2,
4 | right?: 1 | 2,
5 | bottom?: 1 | 2,
6 | }
7 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line
2 | ///
3 |
4 | // eslint-disable-next-line no-unused-vars
5 | declare const JSDOS_VERSION: string;
6 |
--------------------------------------------------------------------------------
/postcss.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | 'postcss-import': {},
4 | 'tailwindcss/nesting': {},
5 | tailwindcss: {},
6 | autoprefixer: {},
7 | },
8 | }
9 |
--------------------------------------------------------------------------------
/src/base.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --sidebar-width: 3rem;
8 | /* w-12 */
9 | }
10 | }
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @import "./base.css";
2 | @import "./app.css";
3 | @import "./components/components.css";
4 | @import "./sidebar/sidebar.css";
5 | @import "./window/window.css";
6 | @import "./frame/frame.css";
7 | @import "./layers/layers.css";
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "module": "ESNext",
5 | "moduleResolution": "Node",
6 | "allowSyntheticDefaultImports": true
7 | },
8 | "include": ["vite.config.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .idea
2 | .vscode
3 | dist.zip
4 | src
5 | .github
6 | .gitmodules
7 | .eslintrc.json
8 | dist/emulators/test/
9 | index.html
10 | postcss.config.cjs
11 | public
12 | tailwind.config.cjs
13 | tsconfig.json
14 | tsconfig.node.json
15 | vite.config.ts
16 | README.deployment.md
--------------------------------------------------------------------------------
/src/window/error-window.tsx:
--------------------------------------------------------------------------------
1 | import { useSelector } from "react-redux";
2 | import { Error } from "../components/error";
3 | import { State } from "../store";
4 |
5 | export function ErrorWindow() {
6 | const error = useSelector((state: State) => state.dos.error);
7 | return ;
8 | }
9 |
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
26 | public/emulators
27 | *.zip
--------------------------------------------------------------------------------
/src/window/file-input.ts:
--------------------------------------------------------------------------------
1 | const fileInput = document.createElement("input");
2 | fileInput.type = "file";
3 |
4 | export function uploadFile(callback: (el: HTMLInputElement) => void) {
5 | const listener = () => {
6 | fileInput.removeEventListener("change", listener);
7 | callback(fileInput);
8 | };
9 | fileInput.addEventListener("change", listener);
10 | fileInput.click();
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/components.css:
--------------------------------------------------------------------------------
1 | .jsdos-rso {
2 | select {
3 | @apply select select-bordered;
4 | }
5 |
6 | .slider {
7 | @apply flex flex-col items-start;
8 |
9 | .touch {
10 | @apply cursor-pointer relative flex;
11 |
12 | .bg-active {
13 | @apply absolute bg-primary;
14 | }
15 |
16 | .point {
17 | @apply absolute h-6 w-6 rounded-full bg-base-content text-base-200;
18 | }
19 | }
20 |
21 | }
22 | }
--------------------------------------------------------------------------------
/src/store/init.ts:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 |
3 | export interface InitState {
4 | uid: string,
5 | };
6 |
7 | let storeUid = -1;
8 | export function createInitSlice() {
9 | storeUid += 1;
10 | return {
11 | storeUid,
12 | slice: createSlice({
13 | name: "init",
14 | initialState: {
15 | uid: storeUid,
16 | },
17 | reducers: {
18 | },
19 | }),
20 | };
21 | }
22 |
23 | export const initSlice = createInitSlice().slice;
24 |
--------------------------------------------------------------------------------
/src/components/lock.tsx:
--------------------------------------------------------------------------------
1 | export function LockBadge(props: {
2 | class?: string,
3 | }) {
4 | return ;
10 | }
11 |
--------------------------------------------------------------------------------
/src/layers/instance.ts:
--------------------------------------------------------------------------------
1 | import { LayersConfig, LegacyLayersConfig } from "./controls/layers-config";
2 | import { Layers } from "./dom/layers";
3 |
4 | export interface LayersInstance {
5 | config: LayersConfig | LegacyLayersConfig | null;
6 | layers: Layers,
7 | autolock: boolean;
8 | sensitivity: number,
9 | mirroredControls: boolean,
10 | scaleControls: number,
11 | activeLayer?: string,
12 | getActiveConfig(): LayersConfig | LegacyLayersConfig | null;
13 | setActiveConfig(config: LayersConfig | LegacyLayersConfig | null, layerName?: string): void;
14 | };
15 |
--------------------------------------------------------------------------------
/src/download-file.ts:
--------------------------------------------------------------------------------
1 | export function downloadUrlToFs(fileName: string, url: string, targetBlank = true) {
2 | const a = document.createElement("a");
3 | a.href = url;
4 | a.target = targetBlank ? "_blank" : "_self";
5 | a.download = fileName;
6 | a.style.display = "none";
7 | document.body.appendChild(a);
8 |
9 | a.click();
10 | a.remove();
11 | }
12 |
13 | export function downloadArrayToFs(fileName: string, data: Uint8Array, type = "application/zip") {
14 | const blob = new Blob([data], {
15 | type,
16 | });
17 | downloadUrlToFs(fileName, URL.createObjectURL(blob));
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/close-button.tsx:
--------------------------------------------------------------------------------
1 | export function CloseButton(props: {
2 | onClose: () => void
3 | class?: string,
4 | }) {
5 | return
;
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/error.tsx:
--------------------------------------------------------------------------------
1 | import { useT } from "../i18n";
2 |
3 | export function Error(props: {
4 | error?: string | null,
5 | onSkip?: () => void,
6 | }) {
7 | const t = useT();
8 | const error = props.error ?? "Unexpected error";
9 |
10 | return
11 |
{t("error")}
12 |
"{error}"
13 |
{t("consult_logs")}
14 | { props.onSkip &&
}
15 |
;
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/loading.tsx:
--------------------------------------------------------------------------------
1 | export function Loading(props: {
2 | head: string,
3 | message: string,
4 | }) {
5 | const { head, message } = props;
6 |
7 | return
8 |
{head}
9 |
{message}
10 |
;
11 | }
12 |
13 | export function formatSize(size: number) {
14 | if (size < 1024) {
15 | return size + "b";
16 | }
17 |
18 | size /= 1024;
19 |
20 | if (size < 1024) {
21 | return Math.round(size) + "kb";
22 | }
23 |
24 | size /= 1024;
25 | return Math.round(size * 10) / 10 + "mb";
26 | }
27 |
--------------------------------------------------------------------------------
/src/sidebar/diskette-icon.tsx:
--------------------------------------------------------------------------------
1 | export function DisketteIcon(props: {
2 | class?: string,
3 | }) {
4 | return ;
14 | }
15 |
--------------------------------------------------------------------------------
/src/v8/config.ts:
--------------------------------------------------------------------------------
1 | export const endpoint = "https://v8.js-dos.com";
2 | export const uploadsS3Bucket = "doszone-uploads";
3 | export const uploadsS3Url = "https://storage.yandexcloud.net";
4 | export const uploadNamspace = "dzapi";
5 |
6 | export const apiEndpoint = "https://d5dn8hh4ivlobv6682ep.apigw.yandexcloud.net";
7 | export const presignPut = apiEndpoint + "/presign-put";
8 | export const presignDelete = apiEndpoint + "/presign-delete";
9 | export const tokenGet = apiEndpoint + "/token/get";
10 |
11 | export const brCdn = "https://br.cdn.dos.zone";
12 |
13 | export const cancelSubscriptionPage = {
14 | en: "https://v8.js-dos.com/cancel-your-subscription/",
15 | ru: "https://v8.js-dos.com/ru/cancel-your-subscription/",
16 | };
17 |
18 | export const actualWsVersion = 5;
19 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import preact from "@preact/preset-vite";
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [preact()],
7 | server: {
8 | port: 3000,
9 | host: "0.0.0.0",
10 | cors: true,
11 | allowedHosts: ["test.js-dos.com"],
12 | },
13 | build: {
14 | rollupOptions: {
15 | output: {
16 | entryFileNames: "js-dos.js",
17 | assetFileNames: (info) => {
18 | return info.name === "index.css" ? "js-dos.css" : info.name;
19 | },
20 | },
21 | },
22 | },
23 | define: {
24 | JSDOS_VERSION: JSON.stringify(process.env.npm_package_version),
25 | },
26 | });
27 |
--------------------------------------------------------------------------------
/src/window/dos/controls/mouse/mouse-locked.ts:
--------------------------------------------------------------------------------
1 | import { pointer } from "./pointer";
2 |
3 | export function mousePointerLock(el: HTMLElement) {
4 | function requestLock() {
5 | if (document.pointerLockElement !== el) {
6 | const requestPointerLock = el.requestPointerLock ||
7 | (el as any).mozRequestPointerLock ||
8 | (el as any).webkitRequestPointerLock;
9 |
10 | requestPointerLock.call(el);
11 |
12 | return;
13 | }
14 | }
15 |
16 | const options = {
17 | capture: true,
18 | };
19 |
20 | for (const next of pointer.starters) {
21 | el.addEventListener(next, requestLock, options);
22 | }
23 |
24 | return () => {
25 | for (const next of pointer.starters) {
26 | el.removeEventListener(next, requestLock, options);
27 | }
28 | };
29 | }
30 |
--------------------------------------------------------------------------------
/src/layers/dom/mem-storage.ts:
--------------------------------------------------------------------------------
1 | export class MemStorage implements Storage {
2 | length = 0;
3 |
4 | private storage: {[key: string]: string} = {};
5 |
6 | setItem(key: string, value: string): void {
7 | this.storage[key] = value;
8 | this.length = Object.keys(this.storage).length;
9 | }
10 |
11 | getItem(key: string): string | null {
12 | const value = this.storage[key];
13 | return value === undefined ? null : value;
14 | }
15 |
16 | removeItem(key: string): void {
17 | delete this.storage[key];
18 | this.length = Object.keys(this.storage).length;
19 | }
20 |
21 | key(index: number): string | null {
22 | const keys = Object.keys(this.storage);
23 | return keys[index] === undefined ? null : keys[index];
24 | }
25 |
26 | clear() {
27 | this.length = 0;
28 | this.storage = {};
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/frame/settings-frame.tsx:
--------------------------------------------------------------------------------
1 | import { MirroredControls, MobileControls,
2 | MouseCapture, SystemCursor, PauseCheckbox } from "../components/dos-option-checkbox";
3 | import { ImageRenderingSelect, RenderAspectSelect, ThemeSelect } from "../components/dos-option-select";
4 | import { MouseSensitiviySlider, ScaleControlsSlider, VolumeSlider } from "../components/dos-option-slider";
5 |
6 | export function SettingsFrame(props: {}) {
7 | return
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
;
20 | }
21 |
--------------------------------------------------------------------------------
/src/app.css:
--------------------------------------------------------------------------------
1 | .jsdos-rso {
2 | @apply h-full;
3 | background: hsl(var(--pc));
4 |
5 | .jsdos-fullscreen-workaround {
6 | position: fixed !important;
7 | left: 0;
8 | top: 0;
9 | bottom: 0;
10 | right: 0;
11 | background: black;
12 | z-index: 999;
13 | }
14 | }
15 |
16 | .jsdos-rso {
17 | canvas, .slider, .soft-keyboard {
18 | -webkit-touch-callout: none;
19 | -webkit-user-select: none;
20 | -khtml-user-select: none;
21 | -moz-user-select: none;
22 | -ms-user-select: none;
23 | user-select: none;
24 |
25 | -ms-touch-action: none;
26 | -ms-content-zooming: none;
27 | touch-action: none;
28 | outline: none;
29 | }
30 |
31 | .cound-down-start > :last-child {
32 | display: none;
33 | }
34 |
35 | .cound-down-start:hover > :first-child {
36 | display: none;
37 | }
38 |
39 | .cound-down-start:hover > :last-child {
40 | display: block;
41 | }
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/src/window/dos/controls/mouse.ts:
--------------------------------------------------------------------------------
1 | import { CommandInterface } from "emulators";
2 | import { mousePointerLock } from "./mouse/mouse-locked";
3 | import { mouseDefault } from "./mouse/mouse-default";
4 | import { mouseSwipe } from "./mouse/mouse-swipe";
5 | import { pointer } from "./mouse/pointer";
6 |
7 | export function mouse(lock: boolean,
8 | sensitivity: number,
9 | pointerButton: number,
10 | el: HTMLElement,
11 | ci: CommandInterface) {
12 | if (lock && !pointer.canLock) {
13 | return mouseSwipe(sensitivity, false, pointerButton, el, ci);
14 | }
15 |
16 | if (lock) {
17 | const unlock = mousePointerLock(el);
18 | const umount = mouseSwipe(sensitivity, true, pointerButton, el, ci);
19 |
20 | return () => {
21 | umount();
22 | unlock();
23 | };
24 | }
25 |
26 | return mouseDefault(pointerButton, el, ci);
27 | }
28 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es2021": true
5 | },
6 | "extends": [
7 | "google"
8 | ],
9 | "parser": "@typescript-eslint/parser",
10 | "parserOptions": {
11 | "ecmaVersion": 12,
12 | "sourceType": "module"
13 | },
14 | "plugins": [
15 | "@typescript-eslint"
16 | ],
17 | "rules": {
18 | "object-curly-spacing": [
19 | "error",
20 | "always"
21 | ],
22 | "require-jsdoc": "off",
23 | "quotes": [
24 | "error",
25 | "double"
26 | ],
27 | "indent": [
28 | "error",
29 | 4,
30 | {
31 | "SwitchCase": 1,
32 | "FunctionDeclaration": {
33 | "parameters": "first"
34 | }
35 | }
36 | ],
37 | "max-len": [
38 | "error",
39 | 120
40 | ]
41 | },
42 | "ignorePatterns": ["dist/**/*"]
43 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "paths": {
4 | "react": ["./node_modules/preact/compat/"],
5 | "react-dom": ["./node_modules/preact/compat/"]
6 | },
7 | "target": "ESNext",
8 | "useDefineForClassFields": true,
9 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
10 | "allowJs": false,
11 | "skipLibCheck": true,
12 | "esModuleInterop": false,
13 | "allowSyntheticDefaultImports": true,
14 | "strict": true,
15 | "forceConsistentCasingInFileNames": true,
16 | "module": "ESNext",
17 | "moduleResolution": "Node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx",
22 | "jsxImportSource": "preact"
23 | },
24 | "include": ["src"],
25 | "exclude": [
26 | "src/sockdrive/js/src/test",
27 | "src/sockdrive/js/src/sockdrive-fat.ts",
28 | "src/sockdrive/js/src/sockdrive-native.ts",
29 | "src/sockdrive/js/src/fatfs"
30 | ],
31 | "references": [{ "path": "./tsconfig.node.json" }]
32 | }
33 |
--------------------------------------------------------------------------------
/src/layers/dom/helpers.ts:
--------------------------------------------------------------------------------
1 | import { pointer } from "../../window/dos/controls/mouse/pointer";
2 |
3 | export function createDiv(className: string, innerHtml?: string) {
4 | const el = document.createElement("div");
5 | el.className = className;
6 | if (innerHtml !== undefined) {
7 | el.innerHTML = innerHtml;
8 | }
9 | return el;
10 | }
11 |
12 | export function stopPropagation(el: HTMLElement, preventDefault = true) {
13 | const onStop = (e: Event) => {
14 | e.stopPropagation();
15 | };
16 | const onPrevent = (e: Event) => {
17 | e.stopPropagation();
18 | if (preventDefault) {
19 | e.preventDefault();
20 | }
21 | };
22 | const options = {
23 | capture: false,
24 | };
25 | for (const next of pointer.starters) {
26 | el.addEventListener(next, onStop, options);
27 | }
28 | for (const next of pointer.enders) {
29 | el.addEventListener(next, onStop, options);
30 | }
31 | for (const next of pointer.prevents) {
32 | el.addEventListener(next, onPrevent, options);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/frame/prerun-frame.tsx:
--------------------------------------------------------------------------------
1 | import { Editor, HardwareCheckbox, MirroredControls, MobileControls,
2 | MouseCapture,
3 | SystemCursor,
4 | WorkerCheckbox } from "../components/dos-option-checkbox";
5 | import { BackendSelect, RenderAspectSelect, RenderSelect, ThemeSelect } from "../components/dos-option-select";
6 | import { MouseSensitiviySlider, ScaleControlsSlider, VolumeSlider } from "../components/dos-option-slider";
7 | import { Play } from "../window/prerun-window";
8 |
9 | export function PreRunFrame(props: {}) {
10 | return
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
;
27 | }
28 |
--------------------------------------------------------------------------------
/src/layers/dom/lifecycle.ts:
--------------------------------------------------------------------------------
1 | import { CommandInterface } from "emulators";
2 |
3 | export function lifecycle(ci: CommandInterface) {
4 | let hidden = "";
5 | let visibilityChange = "";
6 |
7 | if (typeof document.hidden !== "undefined") {
8 | hidden = "hidden";
9 | visibilityChange = "visibilitychange";
10 | } else if (typeof (document as any).mozHidden !== "undefined") {
11 | hidden = "mozHidden";
12 | visibilityChange = "mozvisibilitychange";
13 | } else if (typeof (document as any).msHidden !== "undefined") {
14 | hidden = "msHidden";
15 | visibilityChange = "msvisibilitychange";
16 | } else if (typeof (document as any).webkitHidden !== "undefined") {
17 | hidden = "webkitHidden";
18 | visibilityChange = "webkitvisibilitychange";
19 | }
20 |
21 | function visibilitHandler() {
22 | (document as any)[hidden] ? ci.pause() : ci.resume();
23 | }
24 |
25 | document.addEventListener(visibilityChange as any, visibilitHandler);
26 | ci.events().onExit(() => {
27 | document.removeEventListener(visibilityChange as any, visibilitHandler);
28 | });
29 | }
30 |
--------------------------------------------------------------------------------
/src/store/storage.ts:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 |
3 | const initialState: {
4 | recived: number,
5 | total: number,
6 | changedRecived: number,
7 | changedTotal: number,
8 | ready: boolean,
9 | } = {
10 | recived: 0,
11 | total: 0,
12 | changedRecived: 0,
13 | changedTotal: 0,
14 | ready: false,
15 | };
16 |
17 | export type StorageState = typeof initialState;
18 |
19 | export const storageSlice = createSlice({
20 | name: "storage",
21 | initialState,
22 | reducers: {
23 | reset: (s) => {
24 | s.recived = -1;
25 | s.total = 0;
26 | s.changedRecived = 0;
27 | s.changedTotal = 0;
28 | s.ready = false;
29 | },
30 | progress: (s, a: { payload: [number, number ] }) => {
31 | s.recived = a.payload[0];
32 | s.total = a.payload[1];
33 | },
34 | changedProgress: (s, a: { payload: [number, number ] }) => {
35 | s.changedRecived = a.payload[0];
36 | s.changedTotal = a.payload[1];
37 | },
38 | ready: (s) => {
39 | s.ready = true;
40 | },
41 | },
42 | });
43 |
--------------------------------------------------------------------------------
/src/window/dos/render/resize.ts:
--------------------------------------------------------------------------------
1 | import { FitConstant } from "../../../store/dos";
2 |
3 | export function resizeCanvas(canvas: HTMLCanvasElement,
4 | frameWidth: number,
5 | frameHeight: number,
6 | forceAspect?: number) {
7 | const rect = canvas.parentElement!.getBoundingClientRect();
8 | const containerWidth = rect.width;
9 | const containerHeight = rect.height;
10 |
11 | if (frameHeight === 0) {
12 | return;
13 | }
14 | const aspect =
15 | forceAspect === FitConstant ? (containerWidth / containerHeight) :
16 | (forceAspect ?? (frameWidth / frameHeight));
17 |
18 | let width = containerWidth;
19 | let height = containerWidth / aspect;
20 |
21 | if (height > containerHeight) {
22 | height = containerHeight;
23 | width = containerHeight * aspect;
24 | }
25 |
26 | canvas.style.position = "relative";
27 | canvas.style.top = (containerHeight - height) / 2 + "px";
28 | canvas.style.left = (containerWidth - width) / 2 + "px";
29 | canvas.style.width = width + "px";
30 | canvas.style.height = height + "px";
31 | };
32 |
--------------------------------------------------------------------------------
/src/components/select.tsx:
--------------------------------------------------------------------------------
1 | import { useT } from "../i18n";
2 |
3 | export function Select(props: {
4 | class?: string,
5 | selectClass?: string,
6 | label: string,
7 | selected: string,
8 | values: string[],
9 | onSelect?: (value: string) => void,
10 | disabled?: boolean,
11 | multiline?: boolean,
12 | }) {
13 | const t = useT();
14 | const multiline = props.multiline === true;
15 | function onSelect(e: any) {
16 | if (props.onSelect !== undefined) {
17 | props.onSelect(e.currentTarget.value);
18 | }
19 | }
20 | return
22 |
{props.label}
23 |
24 |
31 |
32 |
;
33 | }
34 |
--------------------------------------------------------------------------------
/src/frame/frame.tsx:
--------------------------------------------------------------------------------
1 | import { useSelector } from "react-redux";
2 | import { State } from "../store";
3 | import { EditorConf } from "./editor/editor-conf-frame";
4 | import { EditorFsFrame } from "./editor/editor-fs-frame";
5 | import { NetworkFrame } from "./network-frame";
6 | import { SettingsFrame } from "./settings-frame";
7 | import { StatsFrame } from "./stats-frame";
8 | import { PreRunFrame } from "./prerun-frame";
9 |
10 | export function Frame(props: {}) {
11 | const frame = useSelector((state: State) => state.ui.frame);
12 | const frameXs = useSelector((state: State) => state.ui.frameXs);
13 | const wideScreen = useSelector((state: State) => state.ui.wideScreen);
14 | if (frame === "none") {
15 | return null;
16 | }
17 |
18 |
19 | return
21 | { frame === "settings" &&
}
22 | { frame === "editor-conf" &&
}
23 | { frame === "editor-fs" &&
}
24 | { frame === "network" &&
}
25 | { frame === "stats" &&
}
26 | { frame === "prerun" &&
}
27 |
;
28 | };
29 |
--------------------------------------------------------------------------------
/README.deployment.md:
--------------------------------------------------------------------------------
1 | # Deployment
2 |
3 | ## Move latest version to named version
4 |
5 | ```sh
6 | VERSION=
7 | mkdir /tmp/$VERSION
8 | aws s3 --endpoint-url=https://storage.yandexcloud.net sync s3://jsdos/latest /tmp/$VERSION
9 | aws s3 --endpoint-url=https://storage.yandexcloud.net sync --acl public-read /tmp/$VERSION s3://jsdos/8.xx/$VERSION
10 | rm -rf /tmp/$VERSION
11 | ```
12 |
13 | ## Release version
14 |
15 | ```
16 | rm -rf build && \
17 | yarn run vite build --base /latest --sourcemap true --minify terser && \
18 | aws s3 --endpoint-url=https://storage.yandexcloud.net sync --acl public-read \
19 | dist s3://jsdos/latest --delete
20 | ```
21 |
22 | Clear the CDN cache (v8.js-dos.com) in dashboard, pattern:
23 | ```
24 | /latest,/latest/*
25 | ```
26 |
27 | ## DOS.Zone (early access) version
28 |
29 | ```
30 | rm -rf build && \
31 | yarn run vite build --base /js-dos/latest --sourcemap true --minify terser && \
32 | python scripts/brotli-dist.py && \
33 | aws s3 --endpoint-url=https://storage.yandexcloud.net sync --acl public-read \
34 | dist s3://br-bundles/js-dos/latest --delete
35 | ```
36 |
37 | Clear the CDN cache (br.cdn.js-dos.com) in dashboard, pattern:
38 | ```
39 | /js-dos/latest,/js-dos/latest/*
40 | ```
--------------------------------------------------------------------------------
/scripts/brotli-dist.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import os
4 | import subprocess
5 | from pathlib import Path
6 |
7 | def brotli_compress_file(file_path):
8 | try:
9 | subprocess.run(['brotli', '-Zf', file_path], check=True)
10 | os.remove(file_path)
11 | if (file_path.endswith(".js") or file_path.endswith(".wasm") or file_path.endswith(".css")):
12 | os.rename(file_path + '.br', file_path + ".ea")
13 | else:
14 | os.rename(file_path + '.br', file_path)
15 | print(f"Compressed {file_path}")
16 | except subprocess.CalledProcessError as e:
17 | print(f"Error compressing {file_path}: {e}")
18 |
19 | def main():
20 | dist_dir = Path('dist')
21 |
22 | if not dist_dir.exists():
23 | print("Error: dist directory not found")
24 | return
25 |
26 | # Walk through all files in dist directory
27 | for root, dirs, files in os.walk(dist_dir):
28 | # Skip types subfolder
29 | if 'types' in root:
30 | continue
31 |
32 | for file in files:
33 | file_path = os.path.join(root, file)
34 | # Skip if file is already brotli compressed
35 | if not file_path.endswith('.br'):
36 | brotli_compress_file(file_path)
37 | if __name__ == '__main__':
38 | main()
39 |
--------------------------------------------------------------------------------
/src/layers/controls/mouse/mouse-not-locked.ts:
--------------------------------------------------------------------------------
1 | import { CommandInterface } from "emulators";
2 | import { Layers } from "../../dom/layers";
3 |
4 | import { mapXY as doMapXY, mount } from "./mouse-common";
5 |
6 | export function mouseNotLocked(layers: Layers, ci: CommandInterface) {
7 | const el = layers.mouseOverlay;
8 | const mapXY = (x: number, y: number) => doMapXY(x, y, ci, layers);
9 |
10 | if (document.pointerLockElement === el) {
11 | document.exitPointerLock();
12 | }
13 |
14 | function onMouseDown(x: number, y: number, button: number) {
15 | const xy = mapXY(x, y);
16 | ci.sendMouseMotion(xy.x, xy.y);
17 | ci.sendMouseButton(button, true);
18 | }
19 |
20 | function onMouseUp(x: number, y: number, button: number) {
21 | const xy = mapXY(x, y);
22 | ci.sendMouseMotion(xy.x, xy.y);
23 | ci.sendMouseButton(button, false);
24 | }
25 |
26 | function onMouseMove(x: number, y: number, mX: number, mY: number) {
27 | const xy = mapXY(x, y);
28 | ci.sendMouseMotion(xy.x, xy.y);
29 | }
30 |
31 | function onMouseLeave(x: number, y: number) {
32 | const xy = mapXY(x, y);
33 | ci.sendMouseMotion(xy.x, xy.y);
34 | }
35 |
36 | return mount(el, layers, 0, false, onMouseDown, onMouseMove, onMouseUp, onMouseLeave);
37 | }
38 |
--------------------------------------------------------------------------------
/src/layers/controls/mouse/mouse-swipe.ts:
--------------------------------------------------------------------------------
1 | import { CommandInterface } from "emulators";
2 | import { Layers } from "../../dom/layers";
3 |
4 | import { mount } from "./mouse-common";
5 |
6 | const clickDelay = 500;
7 | const clickThreshold = 50;
8 |
9 | export function mouseSwipe(sensitivity: number, layers: Layers, ci: CommandInterface) {
10 | const el = layers.mouseOverlay;
11 |
12 | let startedAt = -1;
13 | let acc = 0;
14 |
15 | const onMouseDown = (x: number, y: number) => {
16 | startedAt = Date.now();
17 | acc = 0;
18 | };
19 |
20 | function onMouseMove(x: number, y: number, mX: number, mY: number) {
21 | if (mX === 0 && mY === 0) {
22 | return;
23 | }
24 |
25 | acc += Math.abs(mX) + Math.abs(mY);
26 | (ci as any).sendMouseRelativeMotion(mX, mY);
27 | }
28 |
29 | const onMouseUp = (x: number, y: number) => {
30 | const delay = Date.now() - startedAt;
31 |
32 | if (delay < clickDelay && acc < clickThreshold) {
33 | const button = layers.pointerButton || 0;
34 | ci.sendMouseButton(button, true);
35 | setTimeout(() => ci.sendMouseButton(button, false), 60);
36 | }
37 | };
38 |
39 | const noop = () => {};
40 |
41 | return mount(el, layers, sensitivity, false, onMouseDown, onMouseMove, onMouseUp, noop);
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/checkbox.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from "preact/hooks";
2 |
3 | export function Checkbox(props: {
4 | class?: string,
5 | toggleClass?: string,
6 | label: string,
7 | checked?: boolean,
8 | onChange?: (value: boolean) => void,
9 | disabled?: boolean,
10 | intermediate?: boolean,
11 | }) {
12 | const ref = useRef(null);
13 |
14 | useEffect(() => {
15 | if (ref === null || ref.current === null) {
16 | return;
17 | }
18 |
19 | (ref.current as any).indeterminate = props.intermediate;
20 | }, [ref, props.intermediate]);
21 |
22 | function onChange() {
23 | if (props.onChange) {
24 | props.onChange(!(props.checked === true));
25 | }
26 | }
27 |
28 | return
30 |
40 |
;
41 | }
42 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "js-dos",
3 | "version": "8.3.16",
4 | "description": "Full-featured DOS player with multiple emulator backends",
5 | "type": "module",
6 | "keywords": [
7 | "js-dos",
8 | "dos",
9 | "api",
10 | "browser",
11 | "dosbox",
12 | "emulators",
13 | "webassembly"
14 | ],
15 | "author": "Alexander Guryanov (aka caiiiycuk)",
16 | "license": "GPL-2.0",
17 | "bugs": {
18 | "url": "https://github.com/caiiiycuk/js-dos/issues"
19 | },
20 | "homepage": "https://js-dos.com",
21 | "scripts": {
22 | "dev": "vite",
23 | "build": "tsc && vite build",
24 | "preview": "vite preview"
25 | },
26 | "dependencies": {
27 | "@reduxjs/toolkit": "^1.9.7",
28 | "nipplejs": "^0.10.2",
29 | "preact": "^10.19.3",
30 | "react-checkbox-tree": "^1.8.0",
31 | "react-redux": "^8.1.3"
32 | },
33 | "devDependencies": {
34 | "@preact/preset-vite": "^2.8.1",
35 | "@types/element-resize-detector": "^1.1.6",
36 | "@typescript-eslint/eslint-plugin": "^6.20.0",
37 | "@typescript-eslint/parser": "^6.20.0",
38 | "autoprefixer": "^10.4.17",
39 | "daisyui": "^3.9.3",
40 | "emulators": "8.3.4",
41 | "eslint": "^8.56.0",
42 | "eslint-config-google": "^0.14.0",
43 | "postcss": "^8.4.33",
44 | "postcss-import": "^16.0.0",
45 | "tailwindcss": "^3.4.1",
46 | "terser": "^5.37.0",
47 | "typescript": "^5.3.3",
48 | "vite": "^5.0.12"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/layers/controls/keyboard.ts:
--------------------------------------------------------------------------------
1 | import { CommandInterface } from "emulators";
2 | import { Layers } from "../dom/layers";
3 |
4 | export type Mapper = {[keyCode: number]: number};
5 |
6 | export function keyboard(layers: Layers,
7 | ci: CommandInterface,
8 | mapperOpt?: Mapper) {
9 | const mapper = mapperOpt || {};
10 | function map(keyCode: number) {
11 | if (mapper[keyCode] !== undefined) {
12 | return mapper[keyCode];
13 | }
14 |
15 | return keyCode;
16 | }
17 |
18 | layers.setOnKeyDown((keyCode: number) => {
19 | ci.sendKeyEvent(map(keyCode), true);
20 | });
21 | layers.setOnKeyUp((keyCode: number) => {
22 | ci.sendKeyEvent(map(keyCode), false);
23 | });
24 | layers.setOnKeyPress((keyCode: number) => {
25 | ci.simulateKeyPress(map(keyCode));
26 | });
27 | layers.setOnKeysPress((keyCodes: number[]) => {
28 | ci.simulateKeyPress(...keyCodes);
29 | });
30 |
31 | return () => {
32 | // eslint-disable-next-line
33 | layers.setOnKeyDown((keyCode: number) => { /**/ });
34 | // eslint-disable-next-line
35 | layers.setOnKeyUp((keyCode: number) => { /**/ });
36 | // eslint-disable-next-line
37 | layers.setOnKeyPress((keyCode: number) => { /**/ });
38 | // eslint-disable-next-line
39 | layers.setOnKeysPress((keyCodes: number[]) => { /**/ });
40 | };
41 | }
42 |
--------------------------------------------------------------------------------
/tailwind.config.cjs:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | "./index.html",
5 | "./src/**/*.{js,ts,jsx,tsx}",
6 | ],
7 | theme: {
8 | extend: {},
9 | },
10 | plugins: [require("daisyui")],
11 | safelist: [
12 | "alert-success",
13 | "alert-error",
14 | "alert-warning",
15 | "text-success-content",
16 | "text-error-content",
17 | "text-warning-content",
18 | "input-bordered",
19 | "input-xs",
20 | "bg-blend-multiply",
21 | "bg-opacity-40",
22 | ],
23 | daisyui: {
24 | themes: [
25 | "light",
26 | "dark",
27 | "cupcake",
28 | "bumblebee",
29 | "emerald",
30 | "corporate",
31 | "synthwave",
32 | "retro",
33 | "cyberpunk",
34 | "valentine",
35 | "halloween",
36 | "garden",
37 | "forest",
38 | "aqua",
39 | "lofi",
40 | "pastel",
41 | "fantasy",
42 | "wireframe",
43 | "black",
44 | "luxury",
45 | "dracula",
46 | "cmyk",
47 | "autumn",
48 | "business",
49 | "acid",
50 | "lemonade",
51 | "night",
52 | "coffee",
53 | "winter",
54 | ],
55 | },
56 | };
57 |
--------------------------------------------------------------------------------
/src/frame/editor/editor-frame.css:
--------------------------------------------------------------------------------
1 | .jsdos-rso {
2 | .editor-conf-frame {
3 | @apply w-full overflow-hidden flex-grow flex flex-col items-start justify-center h-full px-4;
4 |
5 | textarea {
6 | @apply w-full textarea;
7 | resize: none;
8 | }
9 | }
10 |
11 | .editor-fs-frame {
12 | @apply h-full;
13 |
14 | .fs-tree-view {
15 | @apply form-control bg-base-100 rounded w-full h-full;
16 |
17 | .fs-tree {
18 |
19 | ol {
20 | @apply ml-2;
21 | }
22 |
23 | li {
24 | @apply my-2;
25 | }
26 |
27 | button {
28 | border: none;
29 | background: none;
30 | filter: none;
31 | min-height: auto;
32 | height: auto;
33 | @apply m-0 p-0;
34 | }
35 |
36 | svg {
37 | @apply text-accent;
38 | }
39 |
40 | input {
41 | @apply checkbox checkbox-accent mr-2 w-4 h-4;
42 | }
43 |
44 | .rct-text,
45 | .rct-bare-label,
46 | label {
47 | display: flex;
48 | flex-direction: row;
49 | justify-content: start;
50 | align-items: center;
51 | }
52 | }
53 | }
54 | }
55 | }
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | push:
5 | branches: [ 8.xx ]
6 | tags:
7 | - "v*.*.*"
8 | pull_request:
9 | branches: [ 8.xx ]
10 |
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 |
15 | strategy:
16 | matrix:
17 | node-version: [18.x]
18 |
19 | steps:
20 | - uses: actions/checkout@v4
21 | with:
22 | submodules: 'recursive'
23 | - name: build js-dos
24 | uses: actions/setup-node@v4
25 | with:
26 | node-version: ${{ matrix.node-version }}
27 | registry-url: 'https://registry.npmjs.org'
28 | cache: 'npm'
29 | - run: npm install -g yarn
30 | - run: yarn
31 | - run: yarn run eslint . --ext ts,tsx --max-warnings 0
32 | - run: mkdir -p public/emulators && cp -rv node_modules/emulators/dist/* public/emulators
33 | - run: NODE_ENV=production yarn run vite build --base /latest --sourcemap true --minify terser
34 | - run: zip -9r release.zip dist/*
35 | - name: publish
36 | if: startsWith(github.ref, 'refs/tags/')
37 | run: npm publish
38 | env:
39 | NODE_AUTH_TOKEN: ${{secrets.NODE_AUTH_TOKEN}}
40 | - name: upload
41 | uses: actions/upload-artifact@v4
42 | with:
43 | name: 'dist'
44 | path: 'dist'
45 | - name: Release
46 | uses: softprops/action-gh-release@v2
47 | if: startsWith(github.ref, 'refs/tags/')
48 | with:
49 | name: ${{ github.ref_name }}
50 | files: |
51 | ${{github.workspace}}/release.zip
52 |
--------------------------------------------------------------------------------
/src/layers/controls/mouse/mouse-locked.ts:
--------------------------------------------------------------------------------
1 | import { CommandInterface } from "emulators";
2 | import { Layers } from "../../dom/layers";
3 | import { mount } from "./mouse-common";
4 |
5 | export function mouseLocked(sensitivity: number, layers: Layers, ci: CommandInterface) {
6 | const el = layers.mouseOverlay;
7 |
8 | function isNotLocked() {
9 | return document.pointerLockElement !== el;
10 | }
11 |
12 | function onMouseDown(x: number, y: number, button: number) {
13 | if (isNotLocked()) {
14 | const requestPointerLock = el.requestPointerLock ||
15 | (el as any).mozRequestPointerLock ||
16 | (el as any).webkitRequestPointerLock;
17 |
18 | requestPointerLock.call(el);
19 |
20 | return;
21 | }
22 |
23 | ci.sendMouseButton(button, true);
24 | }
25 |
26 | function onMouseUp(x: number, y: number, button: number) {
27 | if (isNotLocked()) {
28 | return;
29 | }
30 |
31 | ci.sendMouseButton(button, false);
32 | }
33 |
34 | function onMouseMove(x: number, y: number, mX: number, mY: number) {
35 | if (isNotLocked()) {
36 | return;
37 | }
38 |
39 | if (mX === 0 && mY === 0) {
40 | return;
41 | }
42 |
43 | (ci as any).sendMouseRelativeMotion(mX, mY);
44 | }
45 |
46 | function onMouseLeave(x: number, y: number) {
47 | // nothing to do
48 | }
49 |
50 | return mount(el, layers, sensitivity, true, onMouseDown, onMouseMove, onMouseUp, onMouseLeave);
51 | }
52 |
--------------------------------------------------------------------------------
/ChangeLog.md:
--------------------------------------------------------------------------------
1 | This document describes changes between released js-dos versions.
2 |
3 | Note that version numbers do not necessarily reflect the amount of changes between versions. A version number reflects a release that is known to pass all tests, and versions may be tagged more or less frequently at different times.
4 |
5 | Not all changes are documented here. To examine the full set of changes between versions, you can use git to browse the changes between the tags.
6 |
7 | dev
8 | ---
9 |
10 | * Added keyboard.lock() for "Esc" & "Ctrl+W" keys
11 | * Added UI to download/upload and delete saved games
12 |
13 | 8.3.16 - 30.04.2015
14 | -------------------
15 |
16 | * Added F6/F7 quick save/load support for DOSBox-X
17 | * Changed UI buttons for quick save/load in DOSBox-X mode
18 | * Fixed mouse pointer position calculation
19 | * Changed sliders UI
20 | * Added sensitivity slider when mouse capture mode is enabled
21 | * In fullscreen mode, sidebar becomes thin
22 | * Added click to lock frame if game is running in capture mode
23 |
24 | 8.3.15 - 29.04.2015
25 | -------------------
26 |
27 | * Sockdrive V2 - New version of network drive implementation that improves performance and reliability. Sockdrive v2 is completely backendless and is not compatible with Sockdrive V1. 8.3.14 (https://v8.js-dos.com/8.xx/8.3.14/js-dos.js) is the last version that is compatible with Sockdrive v1.
28 | * Implement `fsDeleteFile` - able to delete files and folders
29 | * Emulators compiled with Emscripten 4.0.2
30 | * js-dos now automatically switch to dark mode if it’s enabled in your system.
31 | * Various UI/UX improvements
--------------------------------------------------------------------------------
/src/components/dos-option-slider.tsx:
--------------------------------------------------------------------------------
1 | import { useDispatch, useSelector } from "react-redux";
2 | import { useT } from "../i18n";
3 | import { dosSlice } from "../store/dos";
4 | import { Slider } from "./slider";
5 | import { State } from "../store";
6 |
7 | export function MouseSensitiviySlider(props: {
8 | class?: string,
9 | }) {
10 | const t = useT();
11 | const sensitivity = useSelector((state: State) => state.dos.mouseSensitivity);
12 | const dispatch = useDispatch();
13 |
14 | return dispatch(dosSlice.actions.mouseSensitivity(value)) }
19 | />;
20 | }
21 |
22 | export function ScaleControlsSlider(props: {
23 | class?: string,
24 | }) {
25 | const t = useT();
26 | const sensitivity = useSelector((state: State) => state.dos.scaleControls);
27 | const dispatch = useDispatch();
28 |
29 | return dispatch(dosSlice.actions.scaleControls(value)) }
34 | />;
35 | }
36 |
37 | export function VolumeSlider(props: {
38 | class?: string,
39 | }) {
40 | const t = useT();
41 | const volume = useSelector((state: State) => state.dos.volume);
42 | const dispatch = useDispatch();
43 |
44 | return dispatch(dosSlice.actions.volume(value)) }
49 | />;
50 | }
51 |
--------------------------------------------------------------------------------
/src/window/window.css:
--------------------------------------------------------------------------------
1 | .jsdos-rso {
2 | .window {
3 | @apply overflow-hidden;
4 |
5 | .background-image {
6 | @apply absolute right-0 h-full pointer-events-none;
7 | background-position: center;
8 | background-size: cover;
9 | background-repeat: no-repeat;
10 | &::after {
11 | position: relative;
12 | content: "";
13 | display: block;
14 | width: 100%;
15 | height: 100%;
16 | background-color: hsl(var(--b1)/var(--tw-bg-opacity));
17 | opacity: 0.75;
18 | }
19 | }
20 |
21 | .play-button {
22 | &:hover {
23 | color: hsl(var(--af));
24 | }
25 | }
26 |
27 | .dhry2-window {
28 | @apply absolute left-0 top-0 w-full h-full flex flex-col items-center justify-center;
29 | @apply bg-black bg-opacity-80 text-2xl px-8 py-4 text-white;
30 |
31 | .title {
32 | @apply mb-4 text-center text-4xl;
33 | }
34 |
35 | .backend {
36 | @apply mb-8 text-center;
37 | }
38 |
39 | .results {
40 | @apply grid grid-cols-2 gap-4;
41 |
42 | div:nth-child(even) {
43 | @apply text-green-300;
44 |
45 | span {
46 | @apply text-white;
47 | }
48 | }
49 |
50 | div:nth-child(2),
51 | div:last-child {
52 | @apply text-yellow-300;
53 | }
54 |
55 | }
56 | }
57 |
58 | .pre-run-window {
59 | @apply overflow-x-hidden overflow-y-auto flex-grow flex flex-col items-center justify-center px-8 mx-auto md:my-auto;
60 | }
61 |
62 | .select-window {
63 | @apply m-auto;
64 | }
65 | }
66 | }
--------------------------------------------------------------------------------
/src/sidebar/network-button.tsx:
--------------------------------------------------------------------------------
1 | import { useDispatch, useSelector } from "react-redux";
2 | import { State } from "../store";
3 | import { uiSlice } from "../store/ui";
4 |
5 | export function NetworkButton(props: {
6 | class?: string,
7 | }) {
8 | const hightlight = useSelector((state: State) => state.ui.frame) === "network";
9 | const inactive = useSelector((state: State) => state.dos.ipx.status !== "connected");
10 | const dispatch = useDispatch();
11 |
12 | function onClick() {
13 | if (hightlight) {
14 | dispatch(uiSlice.actions.frameNone());
15 | } else {
16 | dispatch(uiSlice.actions.frameNetwork());
17 | }
18 | }
19 |
20 | return ;
39 | }
40 |
--------------------------------------------------------------------------------
/src/sidebar/sidebar.css:
--------------------------------------------------------------------------------
1 | .jsdos-rso {
2 | .sidebar-thin {
3 | @apply absolute left-0 top-0 h-full flex flex-col items-center z-10 w-4;
4 | background: linear-gradient(90deg, hsl(var(--b3)) 0%, hsl(var(--b2)) 100%);
5 |
6 | .sidebar-slider {
7 | @apply absolute top-0 bottom-0 left-4;
8 | }
9 | }
10 |
11 | .sidebar {
12 | @apply absolute left-0 top-0 h-full flex flex-col items-center py-2 z-10;
13 | background: linear-gradient(90deg, hsl(var(--b3)) 0%, hsl(var(--b2)) 100%);
14 | width: var(--sidebar-width);
15 |
16 | .sidebar-slider {
17 | @apply absolute top-0 bottom-0;
18 | left: var(--sidebar-width);
19 | }
20 |
21 | .contentbar {
22 | @apply flex-grow;
23 | }
24 |
25 | .sidebar-badge {
26 | @apply absolute right-0 bottom-0 w-3 h-3 rounded-full animate-pulse;
27 | background-color: hsl(var(--p));
28 | }
29 |
30 | .cycles {
31 | @apply text-xs overflow-hidden text-right w-full pr-2 mt-1 -mb-2 whitespace-nowrap opacity-50;
32 | color: hsl(var(--bc));
33 |
34 | &.higlight,
35 | &:hover {
36 | color: hsl(var(--af));
37 | }
38 | }
39 |
40 | .network-button {
41 | &.inactive {
42 | @apply opacity-50;
43 | }
44 | }
45 | }
46 |
47 | .sidebar-button {
48 | @apply cursor-pointer h-8 w-8 my-2 relative;
49 | color: hsl(var(--bc));
50 | }
51 |
52 | .sidebar-highlight,
53 | .sidebar-button:hover {
54 | color: hsl(var(--af));
55 | }
56 |
57 | .animate-led {
58 | animation: pulse 300ms cubic-bezier(0.4, 0, 0.6, 1) infinite;
59 | }
60 |
61 | .save-buttons {
62 | .text-badge {
63 | @apply absolute left-0 top-0 w-3 h-3 font-bold rounded-full flex items-center justify-center;
64 | font-size: 0.5rem;
65 | }
66 | }
67 |
68 | }
--------------------------------------------------------------------------------
/src/frame/editor/editor-conf-frame.tsx:
--------------------------------------------------------------------------------
1 | import { useDispatch, useSelector } from "react-redux";
2 | import { useT } from "../../i18n";
3 | import { State } from "../../store";
4 | import { editorSlice } from "../../store/editor";
5 | import { dosboxconf } from "./defaults";
6 | import { dosSlice } from "../../store/dos";
7 | import { applySockdriveOptionsIfNeeded } from "../../player-api-load";
8 |
9 | export function EditorConf() {
10 | const t = useT();
11 | const bundleConfig = useSelector((state: State) => state.editor.bundleConfig);
12 | const dispatch = useDispatch();
13 |
14 | function changeConfig(contents: string) {
15 | updateDosboxConf(contents);
16 | }
17 |
18 | function updateDosboxConf(newConf: string) {
19 | applySockdriveOptionsIfNeeded(newConf, dispatch);
20 | dispatch(dosSlice.actions.mouseCapture(newConf.indexOf("autolock=true") > 0));
21 | dispatch(editorSlice.actions.dosboxConf(newConf));
22 | }
23 |
24 | if (bundleConfig === null) {
25 | return null;
26 | }
27 |
28 | return
29 |
{t("dosboxconf_template")}
30 |
31 | {dosboxconf
32 | .map(({ name, backend, contents }) => {
33 | return ;
39 | })}
40 |
41 |
;
44 | }
45 |
--------------------------------------------------------------------------------
/src/frame/frame.css:
--------------------------------------------------------------------------------
1 | @import "./editor/editor-frame.css";
2 |
3 | .jsdos-rso {
4 | .frame-root {
5 | @apply flex flex-col;
6 | }
7 |
8 | .frame {
9 | @apply absolute left-0 top-0 pl-12 overflow-auto h-full w-96 py-4;
10 | background-color: hsl(var(--b3));
11 | }
12 |
13 | .frame-md {
14 | width: 100% !important;
15 | }
16 |
17 | .frame-xs {
18 | width: calc(var(--sidebar-width) * 2) !important;
19 | }
20 |
21 | .premium-plan-root {
22 | @apply bg-white rounded-xl px-4 pt-4 pb-2 w-full;
23 | }
24 |
25 | .premium-plan-root.have-premium {
26 | @apply bg-green-200;
27 | }
28 |
29 | .premium-plan-head {
30 | @apply text-blue-400;
31 | }
32 |
33 | .premium-plan-cost {
34 | @apply text-blue-600 text-5xl;
35 | }
36 |
37 | .premium-plan-cost-expl {
38 | @apply text-gray-600 ml-4 flex flex-col;
39 | }
40 |
41 | .premium-plan-highlight {
42 | @apply flex flex-row text-xs items-center py-2 border-b border-b-gray-200;
43 | @apply text-gray-600;
44 | }
45 |
46 | .premium-plan-root.have-premium .premium-plan-highlight {
47 | @apply border-b-green-300;
48 | }
49 |
50 | .settings-frame, .prerun-frame {
51 | @apply px-6 -mt-2;
52 |
53 | .label {
54 | @apply p-0;
55 | }
56 |
57 | .label-text {
58 | font-size: inherit;
59 | }
60 |
61 | .option {
62 | @apply w-full justify-between;
63 | }
64 | }
65 |
66 | .network-frame {
67 | @apply w-full;
68 |
69 | .option {
70 | @apply w-full;
71 | }
72 |
73 | .error {
74 | .label-text {
75 | @apply text-error;
76 | }
77 |
78 | input {
79 | border-color: hsl(var(--er) / var(--tw-border-opacity));
80 | --tw-border-opacity: 0.1;
81 | --tw-bg-opacity: 1;
82 | background-color: hsl(var(--er) / var(--tw-bg-opacity));
83 | --tw-text-opacity: 1;
84 | color: hsl(var(--erc, var(--nc)) / var(--tw-text-opacity));
85 | }
86 | }
87 | }
88 | }
--------------------------------------------------------------------------------
/src/store/auth.ts:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 | import { lStorage } from "../host/lstorage";
3 | import { tokenGet } from "../v8/config";
4 |
5 | const cachedAccount = "cached.jsdos.account";
6 |
7 | export interface Account {
8 | token: string,
9 | name: string,
10 | email: string,
11 | premium: boolean,
12 | };
13 |
14 | const initAccount = (() => {
15 | const json = lStorage.getItem(cachedAccount);
16 | if (json) {
17 | const account = JSON.parse(json);
18 | if (account.email && account.email.length > 0 && account.token && account.token.length === 5) {
19 | return account;
20 | }
21 | }
22 | return null;
23 | })();
24 |
25 | const initialState: {
26 | account: Account | null,
27 | } = {
28 | account: initAccount,
29 | };
30 |
31 | export type AuthState = typeof initialState;
32 |
33 | export const authSlice = createSlice({
34 | name: "auth",
35 | initialState,
36 | reducers: {
37 | setAccount: (state, action: { payload: Account | null }) => {
38 | const account = action.payload;
39 | if (account !== null) {
40 | lStorage.setItem(cachedAccount, JSON.stringify(account));
41 | } else {
42 | lStorage.removeItem(cachedAccount);
43 | }
44 | state.account = account;
45 | },
46 | },
47 | });
48 |
49 | export async function loadAccount(token: string) {
50 | if (!token || token.length !== 5) {
51 | return { token, account: null };
52 | }
53 |
54 | for (let i = 0; i < token.length; ++i) {
55 | const code = token.charCodeAt(i);
56 | if (!(code > 96 && code < 123)) { // lower alpha (a-z)
57 | return { token, account: null };
58 | }
59 | }
60 |
61 | const account = await (await fetch(tokenGet + "?id=" + token)).json();
62 | delete account.success;
63 |
64 | return { token, account: account.email ? account : null };
65 | }
66 |
--------------------------------------------------------------------------------
/src/window/loading-window.tsx:
--------------------------------------------------------------------------------
1 | import { useSelector } from "react-redux";
2 | import { formatSize, Loading } from "../components/loading";
3 | import { useT } from "../i18n";
4 | import { State } from "../store";
5 |
6 | export function LoadingWindow() {
7 | const t = useT();
8 | const step = useSelector((state: State) => state.dos.step);
9 | const received = useSelector((state: State) => state.storage.recived);
10 | const total = useSelector((state: State) => state.storage.total);
11 | const changedRecived = useSelector((state: State) => state.storage.changedRecived);
12 | const changedTotal = useSelector((state: State) => state.storage.changedTotal);
13 |
14 | let head = t("loading");
15 | let message = "100%";
16 | let changesMessage = "";
17 |
18 | switch (step) {
19 | case "bnd-load": {
20 | head = t("bundle_loading");
21 | if (received > 0) {
22 | message = `${formatSize(received)} / ${formatSize(total)}`;
23 |
24 | if (total > 0) {
25 | message += ` (${Math.round(received * 1000 / total) / 10}%)`;
26 | }
27 | }
28 |
29 | if (changedRecived > 0) {
30 | changesMessage = `${formatSize(changedRecived)} / ${formatSize(changedTotal)}`;
31 | if (changedTotal > 0) {
32 | changesMessage += ` (${Math.round(changedRecived * 1000 / changedTotal) / 10}%)`;
33 | }
34 | }
35 | } break;
36 | case "bnd-config": {
37 | head = t("bundle_config");
38 | }
39 |
40 | default: {
41 | }
42 | }
43 |
44 | return
45 |
46 | {changesMessage !== "" &&
47 | <>
48 |
49 |
50 | >
51 | }
52 |
;
53 | }
54 |
--------------------------------------------------------------------------------
/src/store/editor.ts:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 | import { DosConfig } from "emulators";
3 | import { Node } from "react-checkbox-tree";
4 | import { dosboxconf } from "../frame/editor/defaults";
5 |
6 | const initialState: {
7 | // step: "empty" | "extract" | "ready" | "error",
8 | configChanged: boolean,
9 | bundleConfig: DosConfig | null,
10 | errorMessage: string | null,
11 | fs: Node[],
12 | } = {
13 | // step: "empty",
14 | configChanged: false,
15 | bundleConfig: null,
16 | errorMessage: null,
17 | fs: [],
18 | };
19 |
20 | export type EditorState = typeof initialState;
21 |
22 | export const editorSlice = createSlice({
23 | name: "editor",
24 | initialState,
25 | reducers: {
26 | init: (s, a: { payload: DosConfig | null }) => {
27 | if (a.payload === null) {
28 | s.configChanged = true;
29 | s.bundleConfig = {
30 | dosboxConf: dosboxconf[0].contents,
31 | jsdosConf: {
32 | version: "js-dos-v8",
33 | },
34 | };
35 | } else {
36 | s.configChanged = false;
37 | s.bundleConfig = a.payload;
38 | }
39 | },
40 | dosboxConf: (s, a: { payload: string }) => {
41 | s.configChanged = true;
42 | s.bundleConfig!.dosboxConf = a.payload;
43 | },
44 | // empty: (s) => {
45 | // s.step = "empty";
46 | // },
47 | // extract: (s) => {
48 | // s.step = "extract";
49 | // },
50 | // ready: (s) => {
51 | // s.step = "ready";
52 | // },
53 | // config: (s, a: { payload: DosConfig | null }) => {
54 | // s.bundleConfig = a.payload;
55 | // },
56 | // error: (s, a: { payload: string }) => {
57 | // s.step = "error";
58 | // s.errorMessage = a.payload;
59 | // },
60 | },
61 | });
62 |
--------------------------------------------------------------------------------
/src/window/dos/controls/mouse/mouse-swipe.ts:
--------------------------------------------------------------------------------
1 | import { CommandInterface } from "emulators";
2 | import { mount } from "./mount";
3 |
4 | const clickDelay = 500;
5 | const clickThreshold = 50;
6 |
7 | export function mouseSwipe(sensitivity: number,
8 | locked: boolean,
9 | pointerButton: number,
10 | el: HTMLElement,
11 | ci: CommandInterface) {
12 | let startedAt = -1;
13 | let acc = 0;
14 | let prevX = 0;
15 | let prevY = 0;
16 |
17 | const onMouseDown = (x: number, y: number, mouseButton?: number) => {
18 | startedAt = Date.now();
19 | acc = 0;
20 | prevX = x;
21 | prevY = y;
22 |
23 | if (mouseButton !== undefined) {
24 | ci.sendMouseButton(mouseButton, true);
25 | }
26 | };
27 |
28 | function onMouseMove(x: number, y: number, mX: number, mY: number) {
29 | if (mX === undefined) {
30 | mX = x - prevX;
31 | }
32 |
33 | if (mY === undefined) {
34 | mY = y - prevY;
35 | }
36 |
37 | prevX = x;
38 | prevY = y;
39 |
40 | if (mX === 0 && mY === 0) {
41 | return;
42 | }
43 |
44 | acc += Math.abs(mX) + Math.abs(mY);
45 |
46 | ci.sendMouseRelativeMotion(mX, mY);
47 | }
48 |
49 | const onMouseUp = (x: number, y: number, mouseButton?: number) => {
50 | if (mouseButton !== undefined) {
51 | ci.sendMouseButton(mouseButton, false);
52 | } else {
53 | const delay = Date.now() - startedAt;
54 |
55 | if (delay < clickDelay && acc < clickThreshold) {
56 | const button = mouseButton ?? pointerButton;
57 | ci.sendMouseButton(button, true);
58 | setTimeout(() => ci.sendMouseButton(button, false), 60);
59 | }
60 | }
61 | };
62 |
63 | const noop = () => {};
64 |
65 | return mount(el, sensitivity, locked, onMouseDown, onMouseMove, onMouseUp, noop);
66 | }
67 |
--------------------------------------------------------------------------------
/src/v8/changes.ts:
--------------------------------------------------------------------------------
1 | import { uploadsS3Url, uploadsS3Bucket, uploadNamspace, presignPut } from "./config";
2 |
3 | function getPersonalBundleKey(id: string,
4 | bundleUrl: string): string {
5 | const index = bundleUrl.lastIndexOf("/");
6 | const basename = bundleUrl.substring(index + 1);
7 | return "personal-v2/" + uploadNamspace + "/" + id + "/" + basename;
8 | }
9 |
10 | export function getChangesUrlPrefix(id: string): string {
11 | return uploadsS3Url + "/" + uploadsS3Bucket + "/" + "personal-v2" + "/" + uploadNamspace + "/" + id + "/";
12 | }
13 |
14 | export function getChangesUrl(id: string, bundleUrl: string): string {
15 | const personalBundleKey = getPersonalBundleKey(id, bundleUrl);
16 | return uploadsS3Url + "/" + uploadsS3Bucket + "/" + personalBundleKey;
17 | }
18 |
19 | export async function putChanges(bundleUrl: string,
20 | data: Uint8Array): Promise {
21 | let response = await fetch(presignPut + "?bundleUrl=" + encodeURIComponent(bundleUrl));
22 | const result = await response.json();
23 |
24 | if (!result.success) {
25 | throw new Error("Unable to put personal bundle");
26 | }
27 |
28 | const post = result.post as {
29 | url: string,
30 | fields: {
31 | Policy: string,
32 | "X-Amz-Algorithm": string,
33 | "X-Amz-Credential": string,
34 | "X-Amz-Date": string,
35 | "X-Amz-Signature": string,
36 | bucket: string,
37 | key: string,
38 | },
39 | };
40 |
41 | const formData = new FormData();
42 | Object.entries(post.fields).forEach(([k, v]) => {
43 | formData.append(k, v);
44 | });
45 | formData.append("acl", "public-read");
46 | formData.append("file", new Blob([data]));
47 |
48 | response = await fetch(post.url, {
49 | method: "post",
50 | body: formData,
51 | });
52 |
53 | if (response.status !== 200 && response.status !== 204) {
54 | throw new Error("Unable to put changes: " + response.statusText);
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/host/fullscreen.ts:
--------------------------------------------------------------------------------
1 | import { Store, getNonSerializableStore, postJsDosEvent } from "../store";
2 | import { uiSlice } from "../store/ui";
3 |
4 | export function browserSetFullScreen(fullScreen: boolean, store: Store) {
5 | (async () => {
6 | const softFullscreen = store.getState().ui.softFullscreen;
7 | const nsStore = getNonSerializableStore(store);
8 | const root = nsStore.root as any;
9 | if (fullScreen) {
10 | if (softFullscreen) {
11 | root.classList.add("jsdos-fullscreen-workaround");
12 | } else if (root.requestFullscreen) {
13 | await root.requestFullscreen();
14 | } else if (root.webkitRequestFullscreen) {
15 | await root.webkitRequestFullscreen();
16 | } else if (root.mozRequestFullScreen) {
17 | await root.mozRequestFullScreen();
18 | } else if (root.msRequestFullscreen) {
19 | await root.msRequestFullscreen();
20 | } else if (root.webkitEnterFullscreen) {
21 | await root.webkitEnterFullscreen();
22 | } else {
23 | root.classList.add("jsdos-fullscreen-workaround");
24 | }
25 | } else {
26 | if (root.classList.contains("jsdos-fullscreen-workaround")) {
27 | root.classList.remove("jsdos-fullscreen-workaround");
28 | } else if (document.exitFullscreen) {
29 | document.exitFullscreen();
30 | } else if ((document as any).webkitExitFullscreen) {
31 | (document as any).webkitExitFullscreen();
32 | } else if ((document as any).mozCancelFullScreen) {
33 | (document as any).mozCancelFullScreen();
34 | } else if ((document as any).msExitFullscreen) {
35 | (document as any).msExitFullscreen();
36 | }
37 | }
38 |
39 | store.dispatch(uiSlice.actions.setFullScreen(fullScreen));
40 | postJsDosEvent(nsStore, "fullscreen-change", fullScreen);
41 | })().catch((e) => {
42 | console.error("Can't enter fullscreen", e);
43 | });
44 | }
45 |
--------------------------------------------------------------------------------
/src/window/dos/controls/keyboard.ts:
--------------------------------------------------------------------------------
1 | import { CommandInterface } from "emulators";
2 | import { domToKeyCode } from "./keys";
3 | import { Dispatch } from "@reduxjs/toolkit";
4 | import { sendQuickLoadEvent, sendQuickSaveEvent } from "../../../player-api";
5 | import { uiSlice } from "../../../store/ui";
6 | export function keyboard(el: HTMLElement, ci: CommandInterface, handleQuickSaves: boolean, dispatch: Dispatch) {
7 | const pressedKeys = new Set();
8 |
9 | function releaseKeys() {
10 | pressedKeys.forEach((keyCode) => {
11 | ci.sendKeyEvent(keyCode, false);
12 | });
13 | pressedKeys.clear();
14 | }
15 |
16 | function onKeyDown(e: KeyboardEvent) {
17 | if ((e.target as any).type === "text") {
18 | return;
19 | }
20 |
21 | if (handleQuickSaves) {
22 | if (e.key === "F6") {
23 | sendQuickSaveEvent(ci);
24 | dispatch(uiSlice.actions.setHaveQuickSave(true));
25 | }
26 |
27 | if (e.key === "F7") {
28 | sendQuickLoadEvent(ci);
29 | }
30 | }
31 |
32 | const keyCode = domToKeyCode(e.keyCode, e.location);
33 | ci.sendKeyEvent(keyCode, true);
34 | pressedKeys.add(keyCode);
35 | e.stopPropagation();
36 | e.preventDefault();
37 | }
38 |
39 | function onKeyUp(e: KeyboardEvent) {
40 | if ((e.target as any).type === "text") {
41 | return;
42 | }
43 | const keyCode = domToKeyCode(e.keyCode, e.location);
44 | ci.sendKeyEvent(keyCode, false);
45 | pressedKeys.delete(keyCode);
46 | e.stopPropagation();
47 | e.preventDefault();
48 | }
49 |
50 | function onBlur() {
51 | releaseKeys();
52 | }
53 |
54 | el.addEventListener("keydown", onKeyDown);
55 | el.addEventListener("keyup", onKeyUp);
56 | el.addEventListener("blur", onBlur);
57 |
58 | return () => {
59 | releaseKeys();
60 | el.removeEventListener("keydown", onKeyDown);
61 | el.removeEventListener("keyup", onKeyUp);
62 | el.removeEventListener("blur", onBlur);
63 | };
64 | }
65 |
--------------------------------------------------------------------------------
/src/layers/controls/mouse/mouse-nipple.ts:
--------------------------------------------------------------------------------
1 | import nipplejs from "nipplejs";
2 | import { CommandInterface } from "emulators";
3 | import { Layers } from "../../dom/layers";
4 | import { pointer } from "../../../window/dos/controls/mouse/pointer";
5 |
6 | export function mouseNipple(sensitivity: number, layers: Layers, ci: CommandInterface) {
7 | const el = layers.mouseOverlay;
8 | const options = {
9 | capture: true,
10 | };
11 |
12 | let startedAt = -1;
13 | const onStart = () => {
14 | startedAt = Date.now();
15 | };
16 |
17 | const onEnd = () => {
18 | const delay = Date.now() - startedAt;
19 | if (delay < 500) {
20 | const button = layers.pointerButton || 0;
21 | ci.sendMouseButton(button, true);
22 | setTimeout(() => ci.sendMouseButton(button, false), 16);
23 | }
24 | };
25 |
26 | for (const next of pointer.starters) {
27 | el.addEventListener(next, onStart, options);
28 | }
29 | for (const next of pointer.enders) {
30 | el.addEventListener(next, onEnd, options);
31 | }
32 |
33 | const nipple = nipplejs.create({
34 | zone: el,
35 | multitouch: false,
36 | maxNumberOfNipples: 1,
37 | mode: "dynamic",
38 | });
39 |
40 | let dx = 0;
41 | let dy = 0;
42 |
43 | const intervalId = setInterval(() => {
44 | (ci as any).sendMouseRelativeMotion(dx, dy);
45 | }, 16);
46 |
47 | nipple.on("start", () => {
48 | startedAt = Date.now();
49 | dx = 0;
50 | dy = 0;
51 | });
52 |
53 | nipple.on("move", function(evt: any, data: any) {
54 | const { x, y } = data.vector;
55 |
56 | dx = x * data.distance * sensitivity;
57 | dy = -y * data.distance * sensitivity;
58 | });
59 |
60 | nipple.on("end", () => {
61 | dx = 0;
62 | dy = 0;
63 | });
64 |
65 | return () => {
66 | for (const next of pointer.starters) {
67 | el.removeEventListener(next, onStart, options);
68 | }
69 | for (const next of pointer.enders) {
70 | el.removeEventListener(next, onEnd, options);
71 | }
72 | clearInterval(intervalId);
73 | nipple.destroy();
74 | };
75 | }
76 |
--------------------------------------------------------------------------------
/src/window/dos/render/canvas.ts:
--------------------------------------------------------------------------------
1 | import { CommandInterface } from "emulators";
2 | import { resizeCanvas } from "./resize";
3 |
4 | export function canvas(canvas: HTMLCanvasElement,
5 | ci: CommandInterface,
6 | forceAspect?: number) {
7 | const context = canvas.getContext("2d");
8 | if (context === null) {
9 | throw new Error("Unable to create 2d context on given canvas");
10 | }
11 |
12 | let frameWidth = 0;
13 | let frameHeight = 0;
14 |
15 | const onResize = () => {
16 | resizeCanvas(canvas, frameWidth, frameHeight, forceAspect);
17 | };
18 |
19 | let rgba = new Uint8ClampedArray(0);
20 | const onResizeFrame = (w: number, h: number) => {
21 | frameWidth = w;
22 | frameHeight = h;
23 | canvas.width = frameWidth;
24 | canvas.height = frameHeight;
25 | rgba = new Uint8ClampedArray(w * h * 4);
26 | onResize();
27 | };
28 | ci.events().onFrameSize(onResizeFrame);
29 | ci.events().onFrame((frameRgb, frameRgba) => {
30 | if (frameRgb === null && frameRgba === null) {
31 | return;
32 | }
33 |
34 | const frame = (frameRgb !== null ? frameRgb : frameRgba) as Uint8Array;
35 |
36 | let frameOffset = 0;
37 | let rgbaOffset = 0;
38 |
39 | while (rgbaOffset < rgba.length) {
40 | rgba[rgbaOffset++] = frame[frameOffset++];
41 | rgba[rgbaOffset++] = frame[frameOffset++];
42 | rgba[rgbaOffset++] = frame[frameOffset++];
43 | rgba[rgbaOffset++] = 255;
44 |
45 | if (frame.length === rgba.length) {
46 | frameOffset++;
47 | }
48 | }
49 |
50 | context.putImageData(new ImageData(rgba, frameWidth, frameHeight), 0, 0);
51 | });
52 |
53 | onResizeFrame(ci.width(), ci.height());
54 |
55 | const resizeObserver = new ResizeObserver(onResize);
56 | resizeObserver.observe(canvas.parentElement!);
57 | window.addEventListener("resize", onResize);
58 |
59 | return () => {
60 | ci.events().onFrameSize(() => {});
61 | ci.events().onFrame(() => {});
62 | resizeObserver.disconnect();
63 | window.removeEventListener("resize", onResize);
64 | };
65 | }
66 |
--------------------------------------------------------------------------------
/src/layers/controls/legacy-layers-control.ts:
--------------------------------------------------------------------------------
1 | import { LegacyLayersConfig } from "./layers-config";
2 | import { Layers } from "../dom/layers";
3 | import { CommandInterface } from "emulators";
4 | import { deprecatedButton } from "./button";
5 | import { mouse } from "./mouse/mouse-common";
6 | import { nipple } from "./nipple";
7 | import { options } from "./options";
8 | import { keyboard } from "./keyboard";
9 | import { LayersInstance } from "../instance";
10 |
11 | export function initLegacyLayersControl(
12 | dosInstance: LayersInstance,
13 | layers: Layers,
14 | layersConfig: LegacyLayersConfig,
15 | ci: CommandInterface) {
16 | const layersNames = Object.keys(layersConfig);
17 |
18 | const unbind = {
19 | keyboard: () => {/**/},
20 | mouse: () => {/**/},
21 | gestures: () => {/**/},
22 | buttons: () => {/**/},
23 | };
24 |
25 | const changeControlLayer = (layerName: string) => {
26 | unbind.keyboard();
27 | unbind.mouse();
28 | unbind.gestures();
29 | unbind.buttons();
30 |
31 | unbind.keyboard = () => {/**/};
32 | unbind.mouse = () => {/**/};
33 | unbind.gestures = () => {/**/};
34 | unbind.buttons = () => {/**/};
35 |
36 | const layer = layersConfig[layerName];
37 | if (layer === undefined) {
38 | return;
39 | }
40 |
41 | unbind.keyboard = keyboard(layers, ci, layer.mapper);
42 |
43 | if (layer.gestures !== undefined && layer.gestures.length > 0) {
44 | unbind.gestures = nipple(layers, ci, layer.gestures);
45 | } else {
46 | unbind.mouse = mouse(dosInstance.autolock, dosInstance.sensitivity, layers, ci);
47 | }
48 |
49 | if (layer.buttons !== undefined && layer.buttons.length) {
50 | unbind.buttons = deprecatedButton(layers, ci, layer.buttons, 54);
51 | }
52 | };
53 |
54 |
55 | const unbindOptions =
56 | (layers.options.optionControls?.length === 0) ?
57 | () => {/**/} :
58 | options(layers, layersNames, changeControlLayer, 54, 54 / 4, 0);
59 |
60 | changeControlLayer("default");
61 |
62 | return () => {
63 | unbind.gestures();
64 | unbind.buttons();
65 | unbind.mouse();
66 | unbind.keyboard();
67 | unbindOptions();
68 | };
69 | }
70 |
--------------------------------------------------------------------------------
/src/window/window.tsx:
--------------------------------------------------------------------------------
1 | import { useSelector } from "react-redux";
2 | import { State } from "../store";
3 | import { DosWindow } from "./dos/dos-window";
4 | import { ErrorWindow } from "./error-window";
5 | import { LoadingWindow } from "./loading-window";
6 | import { PreRunWindow } from "./prerun-window";
7 | import { SelectWindow } from "./select-window";
8 |
9 | export function Window(props: {}) {
10 | const frameOpened = useSelector((state: State) => state.ui.frame) !== "none";
11 | const frameXs = useSelector((state: State) => state.ui.frameXs);
12 | const window = useSelector((state: State) => state.ui.window);
13 | const background = useSelector((state: State) => state.ui.background);
14 | const kiosk = useSelector((state: State) => state.ui.kiosk);
15 | const sidebarThin = useSelector((state: State) => state.ui.thinSidebar);
16 |
17 | let windowComponent = ;
18 | switch (window) {
19 | case "error": {
20 | windowComponent = ;
21 | } break;
22 | case "loading": {
23 | windowComponent = ;
24 | } break;
25 | case "prerun": {
26 | windowComponent = ;
27 | } break;
28 | case "run": {
29 | windowComponent = ;
30 | } break;
31 | case "select": {
32 | windowComponent = ;
33 | } break;
34 | default: ;
35 | };
36 |
37 | let bgClass = "left-12";
38 | let widthClass = "w-12";
39 | if (sidebarThin && !frameOpened) {
40 | widthClass = "w-4";
41 | bgClass = "left-4";
42 | } else if (frameOpened) {
43 | widthClass = frameXs ? "w-24" : "w-96";
44 | }
45 |
46 | return
47 |
49 |
50 | { !kiosk &&
}
51 | {windowComponent}
52 |
53 |
;
54 | }
55 |
56 | function Loading() {
57 | return ;
60 | }
61 |
--------------------------------------------------------------------------------
/src/layers/controls/nipple.ts:
--------------------------------------------------------------------------------
1 | import nipplejs from "nipplejs";
2 |
3 | import { KBD_NONE } from "../../window/dos/controls/keys";
4 |
5 | import { CommandInterface } from "emulators";
6 | import { Layers } from "../dom/layers";
7 |
8 | export type Event =
9 | "dir:up" | "dir:down" | "dir:left" | "dir:right" |
10 | "plain:up" | "plain:down" | "plain:left" | "plain:right" |
11 | "end:release" | "tap";
12 |
13 | export interface EventMapping {
14 | joystickId: 0 | 1,
15 | event: Event,
16 | mapTo: number,
17 | }
18 |
19 | export function nipple(layers: Layers,
20 | ci: CommandInterface,
21 | mapping: EventMapping[]) {
22 | const manager = nipplejs.create({
23 | zone: layers.mouseOverlay,
24 | multitouch: true,
25 | maxNumberOfNipples: 2,
26 | });
27 |
28 | let pressed = -1;
29 |
30 | const press = (keyCode: number) => {
31 | layers.fireKeyDown(keyCode);
32 | pressed = keyCode;
33 | };
34 |
35 | const release = () => {
36 | if (pressed !== -1) {
37 | layers.fireKeyUp(pressed);
38 | pressed = -1;
39 | }
40 | };
41 |
42 | const releaseOnEnd: {[index: number]: boolean} = {};
43 | const tapJoysticks: {[index: number]: number} = {};
44 | const usedTimes: {[index: number]: number} = {
45 | };
46 | for (const next of mapping) {
47 | if (next.event === "end:release") {
48 | releaseOnEnd[next.joystickId] = true;
49 | } else if (next.mapTo !== KBD_NONE) {
50 | if (next.event === "tap") {
51 | tapJoysticks[next.joystickId] = next.mapTo;
52 | } else {
53 | manager.on(next.event, () => {
54 | usedTimes[next.joystickId] = Date.now();
55 | release();
56 | press(next.mapTo);
57 | });
58 | }
59 | }
60 | }
61 |
62 | const startTimes: {[index: number]: number} = {};
63 | manager.on("start", () => {
64 | const id = manager.ids.length - 1;
65 | startTimes[id] = Date.now();
66 | });
67 |
68 | manager.on("end", () => {
69 | const id = manager.ids.length - 1;
70 | const delay = Date.now() - startTimes[id];
71 |
72 | if (releaseOnEnd[id] === true) {
73 | release();
74 | }
75 |
76 | if (tapJoysticks[id] && delay < 500 && usedTimes[id] < startTimes[id]) {
77 | layers.fireKeyPress(tapJoysticks[id]);
78 | }
79 | });
80 |
81 | return () => manager.destroy();
82 | }
83 |
84 |
--------------------------------------------------------------------------------
/src/window/dos/controls/mouse/mouse-default.ts:
--------------------------------------------------------------------------------
1 | import { CommandInterface } from "emulators";
2 | import { mount } from "./mount";
3 |
4 | const insensitivePadding = 1 / 100;
5 |
6 | export function mouseDefault(pointerButton: number,
7 | el: HTMLElement,
8 | ci: CommandInterface) {
9 | const mapXY = (x: number, y: number) => doMapXY(x, y, el, ci);
10 |
11 | if (document.pointerLockElement === el) {
12 | document.exitPointerLock();
13 | }
14 |
15 | function onMouseDown(x: number, y: number, button?: number) {
16 | const xy = mapXY(x, y);
17 | ci.sendMouseMotion(xy.x, xy.y);
18 | ci.sendMouseButton(button ?? pointerButton, true);
19 | }
20 |
21 | function onMouseUp(x: number, y: number, button?: number) {
22 | const xy = mapXY(x, y);
23 | ci.sendMouseMotion(xy.x, xy.y);
24 | ci.sendMouseButton(button ?? pointerButton, false);
25 | }
26 |
27 | function onMouseMove(x: number, y: number, mX: number, mY: number) {
28 | const xy = mapXY(x, y);
29 | ci.sendMouseMotion(xy.x, xy.y);
30 | }
31 |
32 | function onMouseLeave(x: number, y: number) {
33 | const xy = mapXY(x, y);
34 | ci.sendMouseMotion(xy.x, xy.y);
35 | }
36 |
37 | return mount(el, 0, false, onMouseDown, onMouseMove, onMouseUp, onMouseLeave);
38 | }
39 |
40 | function doMapXY(eX: number,
41 | eY: number,
42 | el: HTMLElement,
43 | ci: CommandInterface) {
44 | const { width: containerWidth, height: containerHeight } = el.getBoundingClientRect();
45 | const frameWidth = ci.width();
46 | const frameHeight = ci.height();
47 |
48 | const aspect = frameWidth / frameHeight;
49 |
50 | let width = containerWidth;
51 | let height = containerWidth / aspect;
52 |
53 | if (height > containerHeight) {
54 | height = containerHeight;
55 | width = containerHeight * aspect;
56 | }
57 |
58 | const top = (containerHeight - height) / 2;
59 | const left = (containerWidth - width) / 2;
60 |
61 | let x = Math.max(0, Math.min(1, (eX - left) / width));
62 | let y = Math.max(0, Math.min(1, (eY - top) / height));
63 |
64 | if (x <= insensitivePadding) {
65 | x = 0;
66 | }
67 |
68 | if (x >= (1 - insensitivePadding)) {
69 | x = 1;
70 | }
71 |
72 | if (y <= insensitivePadding) {
73 | y = 0;
74 | }
75 |
76 | if (y >= (1 - insensitivePadding)) {
77 | y = 1;
78 | }
79 |
80 | return {
81 | x,
82 | y,
83 | };
84 | }
85 |
--------------------------------------------------------------------------------
/src/sidebar/fullscreen-button.tsx:
--------------------------------------------------------------------------------
1 | import { useSelector, useStore } from "react-redux";
2 | import { State, Store } from "../store";
3 | import { browserSetFullScreen } from "../host/fullscreen";
4 |
5 | export function FullscreenButton(props: {
6 | class?: string,
7 | }) {
8 | const fullScreen = useSelector((state: State) => state.ui.fullScreen);
9 | const store = useStore() as Store;
10 |
11 | function onClick() {
12 | browserSetFullScreen(!fullScreen, store);
13 | }
14 | /* eslint-disable max-len */
15 | return ;
43 | /* eslint-enable max-len */
44 | }
45 |
--------------------------------------------------------------------------------
/src/layers/controls/layers-config.ts:
--------------------------------------------------------------------------------
1 | import { Button } from "./button";
2 | import { EventMapping } from "./nipple";
3 | import { Mapper } from "./keyboard";
4 |
5 | import { GridType } from "./grid";
6 |
7 | export type LayerControlType =
8 | "Options" | "Key" | "Keyboard" |
9 | "Switch" | "ScreenMove" |
10 | "PointerButton" | "NippleActivator";
11 |
12 | export interface LayerPosition {
13 | column: number;
14 | row: number;
15 | }
16 |
17 | export interface LayerControl extends LayerPosition {
18 | type: LayerControlType,
19 | symbol: string;
20 | }
21 |
22 | export interface LayerKeyControl extends LayerControl {
23 | mapTo: number[];
24 | }
25 |
26 | export interface LayerSwitchControl extends LayerControl {
27 | layerName: string,
28 | }
29 |
30 | export interface LayerScreenMoveControl extends LayerControl {
31 | direction: "up" | "down" | "left" | "right" |
32 | "up-left" | "up-right" | "down-left" | "down-right";
33 | }
34 |
35 | export interface LayerPointerButtonControl extends LayerControl {
36 | button: 0 | 1;
37 | click: boolean;
38 | }
39 |
40 | // eslint-disable-next-line
41 | export interface LayerNippleActivatorControl extends LayerControl {
42 | }
43 |
44 | // eslint-disable-next-line
45 | export interface LayerPointerResetControl extends LayerControl {
46 | }
47 |
48 | // eslint-disable-next-line
49 | export interface LayerPointerToggleControl extends LayerControl {
50 | }
51 |
52 | export interface LayerPointerMoveControl extends LayerControl {
53 | x: number;
54 | y: number;
55 | }
56 |
57 |
58 | export interface LayerConfig {
59 | grid: GridType,
60 | title: string,
61 | controls: LayerControl[],
62 | }
63 |
64 | export interface LayersConfig {
65 | version: number,
66 | layers: LayerConfig[],
67 | }
68 |
69 |
70 | export interface LegacyLayerConfig {
71 | name: string,
72 | buttons: Button[],
73 | gestures: EventMapping[],
74 | mapper: Mapper,
75 | }
76 |
77 | export type LegacyLayersConfig = {[index: string]: LegacyLayerConfig};
78 |
79 | export function extractLayersConfig(config: any): LayersConfig | LegacyLayersConfig | null {
80 | if (config.layersConfig !== undefined) {
81 | if (config.layersConfig.version === 1) {
82 | migrateV1ToV2(config.layersConfig);
83 | }
84 |
85 | return config.layersConfig;
86 | }
87 |
88 | if (config.layers !== undefined) {
89 | return config.layers;
90 | }
91 |
92 | return null;
93 | }
94 |
95 | function migrateV1ToV2(config: LayersConfig) {
96 | for (const layer of config.layers) {
97 | for (const control of layer.controls) {
98 | if (control.type === "Key") {
99 | const keyControl = control as LayerKeyControl;
100 | if (typeof keyControl.mapTo === "number") {
101 | keyControl.mapTo = [keyControl.mapTo];
102 | }
103 | }
104 | }
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # js-dos v8
2 | [](https://github.com/caiiiycuk/js-dos/actions/workflows/build.yml)
3 |
4 | The simplest API to run **DOS/Win** 9x programs in browser or node. js-dos provides full-featured DOS player that can be easily installed and used to get your DOS program up
5 | and running in browser quickly. js-dos provide many advanced features like multiplayer and cloud storage. All available features are enabled for any integration and free.
6 |
7 | The key features:
8 | * Works in **worker** or render thread
9 | * Support execution in Node and Browsers
10 | * Multiple backends: DOSBox, DOSBox-X
11 | * Mobile support (v8 - WIP, v7 - production)
12 | * Able to run very big games (like Diablo, etc.)
13 | * Multiplayer support
14 | * Cloud storage
15 | * WebAssembly and pure JS versions
16 |
17 | ## Demo
18 |
19 | * [Win 95](https://v8.js-dos.com) - plain Windows 95 with D3D & 3Dfx
20 | * [DOS.Zone](https://dos.zone) - community portal with 1900+ dos games
21 | * [Multiplayer](https://dos.zone/multiplayer) - multiplayer games (Doom, Heroes 2, etc.)
22 | * [Dune 2000](https://dos.zone/dune-2000/)
23 | * [Diablo I](https://dos.zone/diablo-1996/)
24 |
25 | [](https://youtu.be/lhFrAe5YrJE)
26 |
27 | ## Documentation
28 |
29 | * [js-dos 8.xx](https://js-dos.com/overview.html)
30 | * [js-dos 7.xx](https://js-dos.com/v7/build/)
31 | * [js-dos 6.22](https://js-dos.com/index_6.22.html)
32 | * [js-dos 3.xx](https://js-dos.com/index_v3.html)
33 |
34 | ## Support
35 |
36 | If you enjoy using js-dos, we would greatly appreciate your support through subscribing to our [js-dos subscription](https://v8.js-dos.com/key/).
37 | By subscribing, you not only enhance your own experience with exclusive benefits but also contribute to the continued development
38 | and maintenance of our platform. Your subscription helps us grow and provide even better services to all our valued users.
39 |
40 | Alternatively you can do one time donation:
41 |
42 | | [Visa / MasterCard / МИР](https://pay.cloudtips.ru/p/894f907b) | [Buy Me A Coffee!](https://buymeacoffee.com/caiiiycuk) |
43 | |-----------------------------------------------------------------|--------------------------------------------------------|
44 | |  |  |
45 |
46 |
47 | | BTC | ETH |
48 | |--------------------------------------------------------|-------------------------------------------------------|
49 | |  |  |
50 |
51 |
52 | ## Development
53 |
54 | 1. You need to install node dependencies and put `emulators` into `public/emulators`.
55 | ```
56 | yarn
57 | cp -rv node_modules/emulators/dist/* public/emulators
58 | ```
59 | 2. Run `yarn run vite` and open [http://localhost:3000](http://localhost:3000) js-dos is ready!
60 |
61 | ## Community
62 |
63 | * [DOS.Zone](https://dos.zone)
64 | * [Discord](https://discord.com/invite/hMVYEbG)
65 | * [Twitter](https://twitter.com/intent/user?screen_name=doszone_db)
66 | * [Telegram](https://t.me/doszonechat)
67 |
--------------------------------------------------------------------------------
/src/window/dos/controls/mouse/mount.ts:
--------------------------------------------------------------------------------
1 | import { pointer, getPointerState } from "./pointer";
2 |
3 | export function mount(el: HTMLElement,
4 | sensitivity: number,
5 | locked: boolean,
6 | onMouseDown: (x: number, y: number, button?: number) => void,
7 | onMouseMove: (x: number, y: number, mX: number, mY: number) => void,
8 | onMouseUp: (x: number, y: number, button?: number) => void,
9 | onMouseLeave: (x: number, y: number) => void) {
10 | // eslint-disable-next-line
11 | function preventDefaultIfNeeded(e: Event) {
12 | // not needed yet
13 | }
14 |
15 | const onStart = (e: Event) => {
16 | if (e.target !== el) {
17 | return;
18 | }
19 |
20 | const state = getPointerState(e, el, sensitivity, locked);
21 | onMouseDown(state.x, state.y, state.button);
22 |
23 | e.stopPropagation();
24 | preventDefaultIfNeeded(e);
25 | };
26 |
27 | const onChange = (e: Event) => {
28 | if (e.target !== el) {
29 | return;
30 | }
31 |
32 | const state = getPointerState(e, el, sensitivity, locked);
33 | onMouseMove(state.x, state.y, state.mX, state.mY);
34 | e.stopPropagation();
35 | preventDefaultIfNeeded(e);
36 | };
37 |
38 | const onEnd = (e: Event) => {
39 | const state = getPointerState(e, el, sensitivity, locked);
40 | onMouseUp(state.x, state.y, state.button);
41 | e.stopPropagation();
42 | preventDefaultIfNeeded(e);
43 | };
44 |
45 | const onLeave = (e: Event) => {
46 | if (e.target !== el) {
47 | return;
48 | }
49 |
50 | const state = getPointerState(e, el, sensitivity, locked);
51 | onMouseLeave(state.x, state.y);
52 | e.stopPropagation();
53 | preventDefaultIfNeeded(e);
54 | };
55 |
56 | const onPrevent = (e: Event) => {
57 | e.stopPropagation();
58 | preventDefaultIfNeeded(e);
59 | };
60 |
61 | const options = {
62 | capture: false,
63 | };
64 |
65 | for (const next of pointer.starters) {
66 | el.addEventListener(next, onStart, options);
67 | }
68 | for (const next of pointer.changers) {
69 | el.addEventListener(next, onChange, options);
70 | }
71 | for (const next of pointer.enders) {
72 | el.addEventListener(next, onEnd, options);
73 | }
74 | for (const next of pointer.prevents) {
75 | el.addEventListener(next, onPrevent, options);
76 | }
77 | for (const next of pointer.leavers) {
78 | el.addEventListener(next, onLeave, options);
79 | }
80 |
81 | return () => {
82 | for (const next of pointer.starters) {
83 | el.removeEventListener(next, onStart, options);
84 | }
85 | for (const next of pointer.changers) {
86 | el.removeEventListener(next, onChange, options);
87 | }
88 | for (const next of pointer.enders) {
89 | el.removeEventListener(next, onEnd, options);
90 | }
91 | for (const next of pointer.prevents) {
92 | el.removeEventListener(next, onPrevent, options);
93 | }
94 | for (const next of pointer.leavers) {
95 | el.removeEventListener(next, onLeave, options);
96 | }
97 | };
98 | }
99 |
--------------------------------------------------------------------------------
/src/layers/controls/options.ts:
--------------------------------------------------------------------------------
1 | import { Layers } from "../dom/layers";
2 | import { createButton } from "./button";
3 | import { createDiv, stopPropagation } from "../dom/helpers";
4 |
5 | export function options(layers: Layers,
6 | layersNames: string[],
7 | onLayerChange: (layer: string) => void,
8 | size: number,
9 | top: number,
10 | right: number) {
11 | const ident = Math.round(size / 4);
12 |
13 | let controlsVisbile = false;
14 | const keyboardVisible = false;
15 |
16 | const updateVisibility = () => {
17 | const display = controlsVisbile ? "flex" : "none";
18 | for (const next of children) {
19 | if (next == options) {
20 | continue;
21 | }
22 |
23 | next.style.display = display;
24 | }
25 | };
26 |
27 | const toggleOptions = () => {
28 | controlsVisbile = !controlsVisbile;
29 |
30 | if (!controlsVisbile && keyboardVisible) {
31 | layers.toggleKeyboard();
32 | }
33 |
34 | updateVisibility();
35 | };
36 |
37 | const children: HTMLElement[] = [
38 | createSelectForLayers(layersNames, onLayerChange),
39 | createButton("keyboard", {
40 | onClick: () => {
41 | layers.toggleKeyboard();
42 |
43 | if (controlsVisbile && !keyboardVisible) {
44 | controlsVisbile = false;
45 | updateVisibility();
46 | }
47 | },
48 | }, size),
49 | createButton("options", {
50 | onClick: toggleOptions,
51 | }, size),
52 | ];
53 | const options = children[children.length - 1];
54 |
55 | const container = createDiv("emulator-options");
56 | const intialDisplay = keyboardVisible ? "flex" : "none";
57 | for (const next of children) {
58 | if (next !== options) {
59 | next.classList.add("emulator-button-control");
60 | }
61 | next.style.marginRight = ident + "px";
62 | next.style.marginBottom = ident + "px";
63 | if (next !== options) {
64 | next.style.display = intialDisplay;
65 | }
66 | container.appendChild(next);
67 | }
68 |
69 | container.style.position = "absolute";
70 | container.style.right = right + "px";
71 | container.style.top = top + "px";
72 |
73 | layers.mouseOverlay.appendChild(container);
74 |
75 | return () => {
76 | layers.mouseOverlay.removeChild(container);
77 | };
78 | }
79 |
80 | function createSelectForLayers(layers: string[], onChange: (layer: string) => void) {
81 | if (layers.length <= 1) {
82 | return document.createElement("div");
83 | }
84 |
85 | const select = document.createElement("select");
86 | select.classList.add("emulator-control-select");
87 |
88 |
89 | for (const next of layers) {
90 | const option = document.createElement("option");
91 | option.value = next;
92 | option.innerHTML = next;
93 | select.appendChild(option);
94 | }
95 |
96 | select.onchange = (e: any) => {
97 | const layer = e.target.value;
98 | onChange(layer);
99 | };
100 |
101 | stopPropagation(select, false);
102 |
103 | return select;
104 | }
105 |
106 |
--------------------------------------------------------------------------------
/src/layers/dom/layers.ts:
--------------------------------------------------------------------------------
1 | import { createDiv } from "./helpers";
2 |
3 | // eslint-disable-next-line
4 | export interface LayersOptions {
5 | optionControls?: string[];
6 | }
7 |
8 | export class Layers {
9 | options: LayersOptions;
10 | root: HTMLDivElement;
11 | canvas: HTMLCanvasElement;
12 | mouseOverlay: HTMLDivElement;
13 | width: number;
14 | height: number;
15 | keyboardVisible = false;
16 | pointerLock = false;
17 | pointerDisabled = false;
18 | pointerButton: 0 | 1 = 0;
19 |
20 | toggleKeyboard: () => void;
21 |
22 | private onResize: ((width: number, height: number) => void)[];
23 |
24 | private onKeyDown: (keyCode: number) => void;
25 | private onKeyUp: (keyCode: number) => void;
26 | private onKeyPress: (keyCode: number) => void;
27 | private onKeysPress: (keyCodes: number[]) => void;
28 |
29 | // eslint-disable-next-line
30 | constructor(root: HTMLDivElement, canvas: HTMLCanvasElement, toggleKeyboard: () => void, options: LayersOptions) {
31 | this.toggleKeyboard = toggleKeyboard;
32 | this.options = options;
33 | this.root = root;
34 | this.root.classList.add("emulator-root");
35 |
36 | this.canvas = canvas;
37 | this.canvas.className = "emulator-canvas";
38 | this.mouseOverlay = createMouseOverlayLayer();
39 |
40 | this.root.appendChild(this.mouseOverlay);
41 |
42 | this.width = root.offsetWidth;
43 | this.height = root.offsetHeight;
44 |
45 | this.onResize = [];
46 | this.onKeyDown = () => {/**/};
47 | this.onKeyUp = () => {/**/};
48 | this.onKeyPress = () => {/**/};
49 | this.onKeysPress = () => {/**/};
50 |
51 | new ResizeObserver((entries) => {
52 | for (const e of entries) {
53 | if (e.target === root) {
54 | this.width = e.contentRect.width;
55 | this.height = e.contentRect.height;
56 | for (const next of this.onResize) {
57 | next(this.width, this.height);
58 | }
59 | }
60 | }
61 | }).observe(this.root);
62 | }
63 |
64 | addOnResize(handler: (width: number, height: number) => void) {
65 | this.onResize.push(handler);
66 | }
67 |
68 | removeOnResize(handler: (width: number, height: number) => void) {
69 | this.onResize = this.onResize.filter((n) => n !== handler);
70 | }
71 |
72 | setOnKeyDown(handler: (keyCode: number) => void) {
73 | this.onKeyDown = handler;
74 | }
75 |
76 | fireKeyDown(keyCode: number) {
77 | this.onKeyDown(keyCode);
78 | }
79 |
80 | setOnKeyUp(handler: (keyCode: number) => void) {
81 | this.onKeyUp = handler;
82 | }
83 |
84 | fireKeyUp(keyCode: number) {
85 | this.onKeyUp(keyCode);
86 | }
87 |
88 | setOnKeyPress(handler: (keyCode: number) => void) {
89 | this.onKeyPress = handler;
90 | }
91 |
92 | fireKeyPress(keyCode: number) {
93 | this.onKeyPress(keyCode);
94 | }
95 |
96 | setOnKeysPress(handler: (keyCodes: number[]) => void) {
97 | this.onKeysPress = handler;
98 | }
99 |
100 |
101 | fireKeysPress(keyCodes: number[]) {
102 | this.onKeysPress(keyCodes);
103 | }
104 | }
105 |
106 | function createMouseOverlayLayer() {
107 | return createDiv("emulator-mouse-overlay", "");
108 | }
109 |
--------------------------------------------------------------------------------
/src/components/dos-option-select.tsx:
--------------------------------------------------------------------------------
1 | import { useDispatch, useSelector } from "react-redux";
2 | import { useT } from "../i18n";
3 | import { State } from "../store";
4 | import { Select } from "./select";
5 | import { AnyAction } from "@reduxjs/toolkit";
6 | import {
7 | Backend, BackendValues, dosSlice, ImageRendering, ImageRenderingValues, RenderAspect, RenderAspectValues,
8 | RenderBackend, RenderBackendValues,
9 | } from "../store/dos";
10 | import { ThemeValues, Theme, uiSlice } from "../store/ui";
11 | import { lStorage } from "../host/lstorage";
12 |
13 | export function BackendSelect(props: { multiline?: boolean }) {
14 | const locked = useSelector((state: State) => state.dos.backendLocked);
15 | return state.dos.backend}
21 | dispatch={(newValue: Backend) => {
22 | lStorage.setItem("backend", newValue);
23 | return dosSlice.actions.dosBackend(newValue);
24 | }}
25 | />;
26 | }
27 |
28 | export function RenderSelect(props: { multiline?: boolean }) {
29 | const disabled = useSelector((state: State) => state.ui.window) === "run";
30 | return state.dos.renderBackend}
36 | dispatch={(newValue: RenderBackend) => dosSlice.actions.renderBackend(newValue)}
37 | />;
38 | }
39 |
40 | export function RenderAspectSelect(props: { multiline?: boolean }) {
41 | return state.dos.renderAspect}
46 | dispatch={(newValue: RenderAspect) => dosSlice.actions.renderAspect(newValue)}
47 | />;
48 | }
49 |
50 | export function ImageRenderingSelect(props: { multiline?: boolean }) {
51 | return state.dos.imageRendering}
56 | dispatch={(newValue: ImageRendering) => dosSlice.actions.imageRendering(newValue)}
57 | />;
58 | }
59 |
60 | export function ThemeSelect(props: { class?: string, multiline?: boolean }) {
61 | return state.ui.theme}
66 | dispatch={(newValue: Theme) => uiSlice.actions.theme(newValue)}
67 | multiline={props.multiline}
68 | />;
69 | }
70 |
71 | function OptionSelect(props: {
72 | class?: string,
73 | selectClass?: string,
74 | label: string,
75 | values: string[]
76 | selector: (state: State) => T,
77 | dispatch: (newValue: T) => AnyAction;
78 | disabled?: boolean,
79 | multiline?: boolean,
80 | }) {
81 | const t = useT();
82 | const value = useSelector(props.selector);
83 | const dispatch = useDispatch();
84 |
85 | function onBackend(newValue: T) {
86 | dispatch(props.dispatch(newValue));
87 | }
88 | return ;
97 | }
98 |
99 |
--------------------------------------------------------------------------------
/src/host/bundle-storage.ts:
--------------------------------------------------------------------------------
1 | import { storageSlice } from "../store/storage";
2 | import { Account } from "../store/auth";
3 | import { brCdn } from "../v8/config";
4 | import { Store, getNonSerializableStore } from "../store";
5 | import { canDoCloudSave } from "../player-api";
6 |
7 | export function bundleFromFile(file: File, store: Store): Promise {
8 | return new Promise((resolve) => {
9 | store.dispatch(storageSlice.actions.reset());
10 | const reader = new FileReader();
11 | reader.addEventListener("load", async (e) => {
12 | resolve(new Uint8Array(reader.result as ArrayBuffer));
13 | });
14 | reader.addEventListener("progress", (e) => {
15 | store.dispatch(storageSlice.actions.progress([e.loaded, e.total]));
16 | });
17 | reader.readAsArrayBuffer(file);
18 | });
19 | }
20 |
21 |
22 | export async function changesFromUrl(url: string, account: Account | null,
23 | store: Store): Promise {
24 | if (!canDoCloudSave(account, null)) {
25 | return await getNonSerializableStore(store).cache.get(url).catch(() => null);
26 | }
27 |
28 | try {
29 | const response = await fetch(url, {
30 | cache: "no-cache",
31 | });
32 |
33 | if (response.status !== 200) {
34 | throw new Error("Resource not avalible (" + response.status + "): " + response.statusText);
35 | }
36 |
37 | return await readResponseBody(response, url, (bytes, length) => {
38 | store.dispatch(storageSlice.actions.changedProgress([bytes, length]));
39 | });
40 | } catch (e: any) {
41 | return await getNonSerializableStore(store).cache.get(url).catch(() => null);
42 | }
43 | }
44 |
45 | export async function bundleFromUrl(url: string, store: Store): Promise {
46 | try {
47 | return await getNonSerializableStore(store).cache.get(url);
48 | } catch (e: any) {
49 | // ignore
50 | }
51 |
52 | store.dispatch(storageSlice.actions.reset());
53 | const response = await fetch(url, {
54 | cache: "no-store",
55 | });
56 |
57 | if (response.status !== 200) {
58 | throw new Error("Resource not avalible (" + response.status + "): " + response.statusText);
59 | }
60 |
61 | const complete = await readResponseBody(response, url, (bytes, length) => {
62 | store.dispatch(storageSlice.actions.progress([bytes, length]));
63 | });
64 |
65 | getNonSerializableStore(store).cache
66 | .put(url, complete)
67 | .catch(console.error);
68 |
69 | return complete;
70 | };
71 |
72 |
73 | async function readResponseBody(response: Response, url: string,
74 | onProgress: (bytes: number, total: number) => void): Promise {
75 | const lenHeader = response.headers.get("Content-Length");
76 | const length = lenHeader === null ? 0 :
77 | Number.parseInt(lenHeader);
78 | const reader = response.body!.getReader();
79 |
80 | let received = 0;
81 | const chunks: Uint8Array[] = [];
82 | while (true) {
83 | const { done, value } = await reader.read();
84 |
85 | if (done) {
86 | break;
87 | }
88 |
89 | chunks.push(value);
90 | received += value.length;
91 |
92 | const bytes = Math.min(url.startsWith(brCdn) ? received / 2 : received, length);
93 | onProgress(bytes, length);
94 | }
95 |
96 | let offset = 0;
97 | const complete = new Uint8Array(received);
98 | for (const next of chunks) {
99 | complete.set(next, offset);
100 | offset += next.length;
101 | }
102 |
103 | return complete;
104 | }
105 |
--------------------------------------------------------------------------------
/src/layers/dom/storage.ts:
--------------------------------------------------------------------------------
1 | import { MemStorage } from "./mem-storage";
2 |
3 | const MAX_VALUE_SIZE = 1024;
4 | const NEXT_PART_SYMBOL = "@";
5 | const NEXT_PART_SYFFIX = ".";
6 |
7 | export class LStorage implements Storage {
8 | private backend: Storage;
9 | length: number;
10 | prefix: string;
11 |
12 | constructor(backend: Storage | undefined, prefix: string) {
13 | this.prefix = prefix;
14 |
15 | try {
16 | this.backend = backend || localStorage;
17 | this.testBackend();
18 | } catch (e) {
19 | this.backend = new MemStorage();
20 | }
21 |
22 | this.length = this.backend.length;
23 |
24 | if (typeof this.backend.sync === "function") {
25 | (this as any).sync = (callback: any) => {
26 | this.backend.sync(callback);
27 | };
28 | }
29 | }
30 |
31 | testBackend() {
32 | const testKey = this.prefix + ".test.record";
33 | const testValue = "123";
34 | this.backend.setItem(testKey, testValue);
35 | const readedValue = this.backend.getItem(testKey);
36 | this.backend.removeItem(testKey);
37 | const valid = readedValue === testValue && this.backend.getItem(testKey) === null;
38 |
39 | if (!valid) {
40 | throw new Error("Storage backend is not working properly");
41 | }
42 | }
43 |
44 | setLocalStoragePrefix(prefix: string) {
45 | this.prefix = prefix;
46 | }
47 |
48 | clear(): void {
49 | if (!this.backend.length) {
50 | return;
51 | }
52 |
53 | const toRemove: string[] = [];
54 | for (let i = 0; i < this.backend.length; ++i) {
55 | const next = this.backend.key(i);
56 | if (next && next.startsWith(this.prefix)) {
57 | toRemove.push(next);
58 | }
59 | }
60 |
61 | for (const next of toRemove) {
62 | this.backend.removeItem(next);
63 | }
64 | this.length = this.backend.length;
65 | }
66 |
67 | key(index: number): string | null {
68 | return this.backend.key(index);
69 | }
70 |
71 | setItem(key: string, value: string): void {
72 | if (!value || value.length === undefined || value.length === 0) {
73 | this.writeStringToKey(key, "");
74 | return;
75 | }
76 |
77 | let offset = 0;
78 | while (offset < value.length) {
79 | let substr = value.substr(offset, MAX_VALUE_SIZE);
80 | offset += substr.length;
81 |
82 | if (offset < value.length) {
83 | substr += NEXT_PART_SYMBOL;
84 | }
85 |
86 | this.writeStringToKey(key, substr);
87 | key += NEXT_PART_SYFFIX;
88 | }
89 | }
90 |
91 | getItem(key: string): string | null {
92 | let value = this.readStringFromKey(key);
93 | if (value === null) {
94 | return null;
95 | }
96 |
97 | if (value.length === 0) {
98 | return value;
99 | }
100 |
101 | while (value[value.length - 1] === NEXT_PART_SYMBOL) {
102 | value = value.substr(0, value.length - 1);
103 | key += NEXT_PART_SYFFIX;
104 | const next = this.readStringFromKey(key);
105 | value += next === null ? "" : next;
106 | }
107 |
108 | return value;
109 | }
110 |
111 | removeItem(key: string): void {
112 | this.backend.removeItem(this.prefix + key);
113 | this.length = this.backend.length;
114 | }
115 |
116 | private writeStringToKey(key: string, value: string) {
117 | this.backend.setItem(this.prefix + key, value);
118 | this.length = this.backend.length;
119 | }
120 |
121 | private readStringFromKey(key: string) {
122 | return this.backend.getItem(this.prefix + key);
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/src/sidebar/sidebar.tsx:
--------------------------------------------------------------------------------
1 | import { useDispatch, useSelector } from "react-redux";
2 | import { State } from "../store";
3 | import { FullscreenButton } from "./fullscreen-button";
4 | import { NetworkButton } from "./network-button";
5 | import {
6 | DosboxConfButton, SettingsButton, CyclesButton, FsButton,
7 | HddLed,
8 | SoftKeyboardButton,
9 | PreRunButton,
10 | } from "./sidebar-button";
11 | import { SaveButtons } from "./save-buttons";
12 | import { Slider } from "../components/slider";
13 | import { dosSlice } from "../store/dos";
14 | import { uiSlice } from "../store/ui";
15 |
16 | export function SideBar(props: {}) {
17 | const window = useSelector((state: State) => state.ui.window);
18 | const editor = useSelector((state: State) => state.ui.editor);
19 | const kiosk = useSelector((state: State) => state.ui.kiosk);
20 | const networking = !useSelector((state: State) => state.ui.noNetworking);
21 | const frame = useSelector((state: State) => state.ui.frame) !== "none";
22 | const mouseCapture = useSelector((state: State) => state.dos.mouseCapture);
23 | const sidebarThin = useSelector((state: State) => state.ui.thinSidebar);
24 | const dispatch = useDispatch();
25 | if (kiosk) {
26 | return null;
27 | }
28 |
29 | if (sidebarThin) {
30 | return ;
46 | }
47 |
48 | return ;
62 | };
63 |
64 | function SidebarSlider(props: {}) {
65 | const sensitivity = useSelector((state: State) => state.dos.mouseSensitivity);
66 | const dispatch = useDispatch();
67 | return ;
85 | }
86 |
--------------------------------------------------------------------------------
/src/components/dos-option-checkbox.tsx:
--------------------------------------------------------------------------------
1 | import { useDispatch, useSelector } from "react-redux";
2 | import { useT } from "../i18n";
3 | import { State, useNonSerializableStore } from "../store";
4 | import { dosSlice } from "../store/dos";
5 | import { uiSlice } from "../store/ui";
6 | import { Checkbox } from "./checkbox";
7 |
8 | export function Editor() {
9 | const t = useT();
10 | const dispatch = useDispatch();
11 | const editor = useSelector((state: State) => state.ui.editor);
12 | return dispatch(uiSlice.actions.setEditor(e))}
17 | />;
18 | }
19 |
20 | export function MouseCapture() {
21 | const t = useT();
22 | const dispatch = useDispatch();
23 | const lock = useSelector((state: State) => state.dos.mouseCapture);
24 | return dispatch(dosSlice.actions.mouseCapture(l))}
29 | />;
30 | }
31 |
32 | export function SystemCursor() {
33 | const t = useT();
34 | const dispatch = useDispatch();
35 | const lock = useSelector((state: State) => !state.dos.noCursor);
36 | return dispatch(dosSlice.actions.noCursor(!l))}
41 | />;
42 | }
43 |
44 | export function MobileControls() {
45 | const t = useT();
46 | const dispatch = useDispatch();
47 | const lock = useSelector((state: State) => state.dos.mobileControls);
48 | return dispatch(dosSlice.actions.mobileControls(l))}
53 | />;
54 | }
55 |
56 | export function MirroredControls() {
57 | const t = useT();
58 | const dispatch = useDispatch();
59 | const lock = useSelector((state: State) => state.dos.mirroredControls);
60 | return dispatch(dosSlice.actions.mirroredControls(l))}
65 | />;
66 | }
67 |
68 | export function PauseCheckbox() {
69 | const t = useT();
70 | const dispatch = useDispatch();
71 | const paused = useSelector((state: State) => state.dos.paused);
72 | const disabled = useSelector((state: State) => state.ui.window) !== "run";
73 | return dispatch(dosSlice.actions.paused(p))}
79 | />;
80 | }
81 |
82 | export function WorkerCheckbox() {
83 | const t = useT();
84 | const dispatch = useDispatch();
85 | const worker = useSelector((state: State) => state.dos.worker);
86 | const hardware = useSelector((state: State) => state.dos.backendHardware);
87 | const disabled = useSelector((state: State) => state.ui.window) === "run";
88 | const nonSerializableStore = useNonSerializableStore();
89 | return hardware && nonSerializableStore.options.backendHardware ? null : dispatch(dosSlice.actions.dosWorker(w))}
95 | />;
96 | }
97 |
98 | export function HardwareCheckbox() {
99 | const t = useT();
100 | const dispatch = useDispatch();
101 | const hardware = useSelector((state: State) => state.dos.backendHardware);
102 | const nonSerializableStore = useNonSerializableStore();
103 | return nonSerializableStore.options.backendHardware ? dispatch(dosSlice.actions.dosBackendHardware(h))}
108 | /> : null;
109 | }
110 |
--------------------------------------------------------------------------------
/src/store.ts:
--------------------------------------------------------------------------------
1 | import { ThunkMiddleware, configureStore } from "@reduxjs/toolkit";
2 | import { UiState, uiSlice } from "./store/ui";
3 | import { AuthState, authSlice } from "./store/auth";
4 | import { DosState, dosSlice } from "./store/dos";
5 | import { I18NState, i18nSlice } from "./i18n";
6 | import { StorageState, storageSlice } from "./store/storage";
7 | import { EditorState, editorSlice } from "./store/editor";
8 | import { DosEvent, DosOptions } from "./public/types";
9 | import { CommandInterface, InitFs } from "emulators";
10 | import { useStore } from "react-redux";
11 | import { IDB, IDBNoop } from "./host/idb";
12 | import { InitState, createInitSlice } from "./store/init";
13 | import { LayersInstance } from "./layers/instance";
14 |
15 | export interface LoadedBundle {
16 | bundleUrl: string | null,
17 | bundleChangesUrl: string | null,
18 | bundle: InitFs | null,
19 | bundleChanges: Uint8Array | null,
20 | appliedBundleChanges: Uint8Array | null,
21 | initFs: InitFs | null,
22 | }
23 |
24 | export interface NonSerializableStore {
25 | root: HTMLDivElement,
26 | loadedBundle: LoadedBundle | null,
27 | ci: CommandInterface | null,
28 | cache: IDB,
29 | options: Partial,
30 | layers: Promise | null,
31 | gl: WebGLRenderingContext | null,
32 | }
33 |
34 | export interface DosAction {
35 | asyncStore: (cakkback: (store: Store) => void) => void,
36 | }
37 |
38 | const dosMiddleware: ThunkMiddleware = (store) => (next) => (action) => {
39 | function asyncStore(callback: (store: any) => void) {
40 | setTimeout(() => callback(store), 4);
41 | }
42 |
43 | const actionWithAsyncDispatch =
44 | Object.assign({}, action, { asyncStore });
45 |
46 | next(actionWithAsyncDispatch);
47 | };
48 |
49 | const nonSerializableStoreMap: { [uid: string]: NonSerializableStore } = {};
50 |
51 | export function makeNonSerializableStore(options: Partial): NonSerializableStore {
52 | return {
53 | root: null as any,
54 | loadedBundle: null,
55 | ci: null,
56 | cache: new IDBNoop(),
57 | options,
58 | layers: null,
59 | gl: null,
60 | };
61 | }
62 |
63 | export function makeStore(nonSerializableStore: NonSerializableStore, options: Partial) {
64 | const { storeUid, slice } = createInitSlice();
65 | const store = configureStore({
66 | reducer: {
67 | init: slice.reducer,
68 | i18n: i18nSlice.reducer,
69 | auth: authSlice.reducer,
70 | ui: uiSlice.reducer,
71 | dos: dosSlice.reducer,
72 | storage: storageSlice.reducer,
73 | editor: editorSlice.reducer,
74 | },
75 | middleware: (getDefault) => {
76 | const all = getDefault();
77 | all.push(dosMiddleware);
78 | return all;
79 | },
80 | });
81 | nonSerializableStoreMap[storeUid] = nonSerializableStore;
82 | return store;
83 | };
84 |
85 | export interface State {
86 | init: InitState,
87 | ui: UiState,
88 | auth: AuthState,
89 | dos: DosState,
90 | i18n: I18NState,
91 | editor: EditorState,
92 | storage: StorageState,
93 | }
94 |
95 | export type Store = ReturnType;
96 |
97 | export function getNonSerializableStore(storeOrState: any): NonSerializableStore {
98 | if (typeof storeOrState.getState === "function") {
99 | return nonSerializableStoreMap[storeOrState.getState().init.uid];
100 | } else {
101 | return nonSerializableStoreMap[storeOrState.init.uid];
102 | }
103 | }
104 |
105 | export function useNonSerializableStore() {
106 | return getNonSerializableStore(useStore());
107 | }
108 |
109 | export function postJsDosEvent(nonSerializableStore: NonSerializableStore, event: DosEvent,
110 | arg?: CommandInterface | boolean) {
111 | if (nonSerializableStore.options.onEvent) {
112 | setTimeout(() => {
113 | nonSerializableStore.options.onEvent?.(event, arg);
114 | }, 4);
115 | }
116 | }
117 |
118 | export function getState(store: Store): State {
119 | return store.getState() as any;
120 | }
121 |
--------------------------------------------------------------------------------
/src/public/types.ts:
--------------------------------------------------------------------------------
1 | export type DosEvent = "emu-ready" | "ci-ready" | "bnd-play" | "open-key" | "fullscreen-change";
2 | export type ImageRendering = "pixelated" | "smooth";
3 | export type RenderBackend = "webgl" | "canvas";
4 | export type RenderAspect = "AsIs" | "1/1" | "5/4" | "4/3" | "16/10" | "16/9" | "Fit";
5 |
6 |
7 | export type InitBundleEntry = Uint8Array;
8 | export interface InitFileEntry {
9 | path: string;
10 | contents: Uint8Array;
11 | }
12 | export type InitFsEntry = InitBundleEntry | InitFileEntry;
13 | export type InitFs = InitFsEntry | InitFsEntry[];
14 |
15 | export interface NamedHost {
16 | name: string,
17 | host: string,
18 | }
19 |
20 | export interface DosOptions {
21 | url: string,
22 | dosboxConf: string,
23 | jsdosConf: any,
24 | initFs: InitFs,
25 | background: string,
26 | pathPrefix: string,
27 | pathSuffix: string,
28 | theme: "light" | "dark" | "cupcake" | "bumblebee" | "emerald" | "corporate" |
29 | "synthwave" | "retro" | "cyberpunk" | "valentine" | "halloween" | "garden" |
30 | "forest" | "aqua" | "lofi" | "pastel" | "fantasy" | "wireframe" | "black" |
31 | "luxury" | "dracula" | "cmyk" | "autumn" | "business" | "acid" | "lemonade" |
32 | "night" | "coffee" | "winter",
33 | lang: "ru" | "en",
34 | backend: "dosbox" | "dosboxX",
35 | backendLocked: boolean,
36 | backendHardware: ((backend: "dosbox" | "dosboxX") => Promise),
37 | workerThread: boolean,
38 | mouseCapture: boolean,
39 | onEvent: (event: DosEvent, arg?: any /* CommandInterface | boolean */) => void,
40 | ipx: NamedHost[],
41 | ipxBackend: string,
42 | room: string,
43 | server: string,
44 | fullScreen: boolean,
45 | autoStart: boolean,
46 | countDownStart: number,
47 | autoSave: boolean,
48 | kiosk: boolean,
49 | imageRendering: ImageRendering,
50 | renderBackend: RenderBackend,
51 | renderAspect: RenderAspect,
52 | noNetworking: boolean,
53 | noCloud: boolean,
54 | scaleControls: number,
55 | mouseSensitivity: number,
56 | noCursor: boolean,
57 | softKeyboardLayout: string[] | string[][][],
58 | softKeyboardSymbols: {[key: string]: string}[],
59 | volume: number,
60 | key: string,
61 | softFullscreen: boolean,
62 | thinSidebar: boolean,
63 | }
64 |
65 | export interface DosProps {
66 | getVersion(): [string, string];
67 | getToken(): string | null;
68 |
69 | setTheme(theme: DosOptions["theme"]): void;
70 | setLang(lang: DosOptions["lang"]): void;
71 | setBackend(backend: DosOptions["backend"]): void;
72 | setBackendLocked(locked: boolean): void;
73 | setWorkerThread(capture: DosOptions["workerThread"]): void;
74 | setMouseCapture(capture: DosOptions["mouseCapture"]): void;
75 | setIpx(ipx: DosOptions["ipx"]): void;
76 | setIpxBackend(backend: string): void;
77 | setRoom(room: DosOptions["room"]): void;
78 | setFrame(frame: "network"): void;
79 | setBackground(background: string | null): void;
80 | setFullScreen(fullScreen: boolean): void;
81 | setAutoStart(autoStart: boolean): void;
82 | setCountDownStart(countDownStart: number): void;
83 | setAutoSave(autoSave: boolean): void;
84 | setKiosk(kiosk: boolean): void;
85 | setImageRendering(rendering: ImageRendering): void;
86 | setRenderBackend(backend: RenderBackend): void;
87 | setRenderAspect(aspect: RenderAspect): void;
88 | setNoNetworking(noNetworking: boolean): void;
89 | setNoCloud(noCloud: boolean): void;
90 | setPaused(pause: boolean): void;
91 | setScaleControls(scaleControls: number): void;
92 | setMouseSensitivity(mouseSensitivity: number): void;
93 | setNoCursor(noCursor: boolean): void;
94 | setSoftKeyboardLayout(layout: string[] | string[][][]): void;
95 | setSoftKeyboardSymbols(symbols: {[key: string]: string}[]): void;
96 | setVolume(volume: number): void;
97 | setKey(key: string | null): void;
98 | setSoftFullscreen(softFullscreen: boolean): void;
99 | setThinSidebar(thinSidebar: boolean): void;
100 | save(): Promise;
101 | stop(): Promise;
102 | }
103 |
104 | export type DosFn = (element: HTMLDivElement, options: Partial) => DosProps;
105 |
106 | // declare const Dos: DosFn;
107 |
--------------------------------------------------------------------------------
/src/components/slider.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from "preact/hooks";
2 |
3 | export function Slider(props: {
4 | class?: string,
5 | label?: string,
6 | value: number,
7 | vertical?: boolean,
8 | bgClass?: string,
9 | pointClass?: string,
10 | children?: any,
11 | onChange: (value: number) => void,
12 | }) {
13 | const sliderRef = useRef(null);
14 | const pointRef = useRef(null);
15 | const vertical = props.vertical ?? false;
16 | const bgClass = props.bgClass ?? "bg-base-200";
17 | useEffect(() => {
18 | if (sliderRef?.current === null) {
19 | return;
20 | }
21 |
22 | const sliderEl = sliderRef.current;
23 |
24 | let pressed = false;
25 | function updatePercent(e: PointerEvent) {
26 | const boundingRect = sliderEl.getBoundingClientRect();
27 | const percent = vertical ?
28 | 1 - Math.min(1, Math.max(0,
29 | (e.clientY - boundingRect.top) / boundingRect.height)) :
30 | Math.min(1, Math.max(0,
31 | (e.clientX - boundingRect.left) / boundingRect.width));
32 | props.onChange(percent);
33 | }
34 |
35 | function onPointerDown(e: PointerEvent) {
36 | pressed = true;
37 | updatePercent(e);
38 | }
39 | function onPointerMove(e: PointerEvent) {
40 | if (!pressed) {
41 | return;
42 | }
43 |
44 | updatePercent(e);
45 | }
46 |
47 | function onPointerUp(e: PointerEvent) {
48 | pressed = false;
49 | }
50 |
51 |
52 | sliderEl.addEventListener("pointerdown", onPointerDown);
53 | sliderEl.addEventListener("pointermove", onPointerMove);
54 | sliderEl.addEventListener("pointerup", onPointerUp);
55 | sliderEl.addEventListener("pointercancel", onPointerUp);
56 | sliderEl.addEventListener("pointerleave", onPointerUp);
57 |
58 | return () => {
59 | sliderEl.removeEventListener("pointerdown", onPointerDown);
60 | sliderEl.removeEventListener("pointermove", onPointerMove);
61 | sliderEl.removeEventListener("pointerup", onPointerUp);
62 | sliderEl.removeEventListener("pointercancel", onPointerUp);
63 | sliderEl.removeEventListener("pointerleave", onPointerUp);
64 | };
65 | }, [sliderRef, vertical]);
66 |
67 | const rounded = vertical ? "" : "rounded-full";
68 | const touchAlign = vertical ? "items-start" : "items-center";
69 | const percent = Math.min(Math.max(0, props.value * 100), 100);
70 | const flexClass = vertical ? "flex-col" : "flex-row";
71 | const containerSize = vertical ? "h-full" : "w-full";
72 | const touchSize = vertical ? "w-4" : "h-16";
73 | const bgSize = vertical ? "w-2" : "h-2";
74 | const cacluclatedStyle = vertical ? {
75 | active: {
76 | bottom: 0,
77 | height: "calc(" + percent + "%" + " + 12px)",
78 | },
79 | point: {
80 | left: "-8px",
81 | bottom: "calc(" + percent + "%" + " - 12px)",
82 | },
83 | } : {
84 | active: {
85 | left: 0,
86 | width: percent + "%",
87 | },
88 | point: {
89 | left: "calc(" + percent + "%" + " - 12px)",
90 | },
91 | };
92 |
93 | return
94 | {props.label &&
95 |
{props.label}
96 |
{props.value.toFixed(2)}
97 |
}
98 |
99 |
100 |
102 |
104 | {props.children}
105 |
106 |
107 |
;
108 | }
109 |
--------------------------------------------------------------------------------
/src/sidebar/save-buttons.tsx:
--------------------------------------------------------------------------------
1 | import { useDispatch, useSelector, useStore } from "react-redux";
2 | import { DisketteIcon } from "./diskette-icon";
3 | import { getState, State, useNonSerializableStore } from "../store";
4 | import { useState } from "preact/hooks";
5 | import { apiSave, sendQuickLoadEvent, sendQuickSaveEvent } from "../player-api";
6 | import { uiSlice } from "../store/ui";
7 |
8 | export function SaveButtons() {
9 | const showQuickLoad = useSelector((state: State) => state.ui.haveQuickSave);
10 | const dosboxX = useSelector((state: State) => state.dos.backend) === "dosboxX";
11 | return
12 |
13 | {dosboxX && }
14 | {dosboxX && showQuickLoad && }
15 |
;
16 | }
17 |
18 | function QuickSaveButton(props: {
19 | label: number | string,
20 | bgcolor: string,
21 | textcolor: string,
22 | }) {
23 | const nonSerializableStore = useNonSerializableStore();
24 | const dispatch = useDispatch();
25 |
26 | function onClick() {
27 | const ci = nonSerializableStore.ci;
28 | if (ci === null) {
29 | return;
30 | }
31 |
32 | sendQuickSaveEvent(ci);
33 | dispatch(uiSlice.actions.setHaveQuickSave(true));
34 | }
35 |
36 | return ;
48 | }
49 |
50 | function QuickLoadButton(props: {
51 | label: number | string,
52 | bgcolor: string,
53 | }) {
54 | const nonSerializableStore = useNonSerializableStore();
55 | function onClick() {
56 | const ci = nonSerializableStore.ci;
57 | if (ci === null) {
58 | return;
59 | }
60 |
61 | sendQuickLoadEvent(ci);
62 | }
63 |
64 | return ;
74 | }
75 |
76 |
77 | function SaveButton(props: {
78 | class?: string,
79 | }) {
80 | const [busy, setBusy] = useState(false);
81 | const dispatch = useDispatch();
82 | const canSave = useSelector((state: State) => state.ui.canSave);
83 | const nonSerializableStore = useNonSerializableStore();
84 | const store = useStore();
85 |
86 | if (!canSave ||
87 | nonSerializableStore.loadedBundle === null ||
88 | nonSerializableStore.loadedBundle.bundleChangesUrl === null) {
89 | return null;
90 | }
91 |
92 | function onClick() {
93 | if (busy) {
94 | return;
95 | }
96 |
97 |
98 | setBusy(true);
99 | apiSave(getState(store as any), nonSerializableStore, dispatch)
100 | .finally(() => setBusy(false));
101 | }
102 |
103 | return ;
110 | }
111 |
--------------------------------------------------------------------------------
/src/frame/stats-frame.tsx:
--------------------------------------------------------------------------------
1 | import { useSelector } from "react-redux";
2 | import { State, useNonSerializableStore } from "../store";
3 |
4 | export function StatsFrame() {
5 | const nonSerializableStore = useNonSerializableStore();
6 | const backend = useSelector((state: State) => state.dos.backend);
7 | const hardware = useSelector((state: State) => state.dos.backendHardware) &&
8 | nonSerializableStore.options.backendHardware;
9 | const emuVersion = useSelector((state: State) => state.dos.emuVersion);
10 | const startedAt = useSelector((state: State) => state.dos.ciStartedAt);
11 | const stats = useSelector((state: State) => state.dos.stats);
12 | const cycles = Math.round(useSelector((state: State) => state.dos.stats.cyclesPerMs) / 1000);
13 | return
14 |
15 | js-dos/emu: {JSDOS_VERSION}/{emuVersion}
16 |
17 |
18 |
19 |
20 |
21 | | Metric |
22 | Value |
23 |
24 |
25 |
26 |
27 | | Emulation |
28 | {backend + " " + (hardware ? "(WS)" : "(WA)")} |
29 |
30 |
31 | | Uptime |
32 | {Math.round((Date.now() - startedAt) / 100) / 10} s |
33 |
34 |
35 | | Cycles/ms |
36 | {cycles <= 0 && ~ K | }
37 | {cycles > 0 && cycles <= 1000 && {cycles} K | }
38 | {cycles > 1000 && {Math.round(cycles / 1000)} KK | }
39 |
40 |
41 | | NonSkipSleep COUNT/s |
42 | {stats.nonSkippableSleepPreSec} |
43 |
44 |
45 | | Sleep COUNT/s |
46 | {stats.sleepPerSec} |
47 |
48 |
49 | | Sleep TIME/s |
50 | {stats.sleepTimePerSec} |
51 |
52 |
53 | | Msg FRAME/s |
54 | {stats.framePerSec} |
55 |
56 |
57 | | Msg SOUND/s |
58 | {stats.soundPerSec} |
59 |
60 |
61 | | Msg SENT/s |
62 | {stats.msgSentPerSec} |
63 |
64 |
65 | | Msg RECV/s |
66 | {stats.msgRecvPerSec} |
67 |
68 |
69 | | Net SENT/s |
70 | {Math.round(stats.netSent / 1024 * 100) / 100}Kb |
71 |
72 |
73 | | Net RECV/s |
74 | {Math.round(stats.netRecv / 1024 * 100) / 100}Kb |
75 |
76 | {stats.driveIo.map((info, i) => {
77 | return <>
78 |
79 | | HDD {i == 0 ? "C:" : "D:"} |
80 | {info.url.substring(info.url.lastIndexOf("/") + 1)} -
81 | {Math.round(info.read * 100 / info.total)}% |
82 |
83 |
84 | | Size |
85 | {Math.round(info.total / 1024 / 1024)} Mb
86 | {Math.round(info.write / 1024)} Kb |
87 |
88 | >;
89 | })}
90 |
91 |
92 |
93 |
;
94 | }
95 |
--------------------------------------------------------------------------------
/src/window/dos/controls/mouse/pointer.ts:
--------------------------------------------------------------------------------
1 | const MAX_MOVEMENT_REAL_SPEED = 50;
2 |
3 | function initBind() {
4 | const isMobile = /Mobile|mini|Fennec|Android|iP(ad|od|hone)/.test(navigator.appVersion) ||
5 | /Mobile|mini|Fennec|Android|iP(ad|od|hone)/.test(navigator.userAgent) ||
6 | (/MacIntel/.test(navigator.platform) && navigator.maxTouchPoints && navigator.maxTouchPoints > 2);
7 | const isTouch = isMobile && !!("ontouchstart" in window);
8 | const isPointer = isMobile && (window.PointerEvent ? true : false);
9 | const isMSPointer = isMobile && ((window as any).MSPointerEvent ? true : false);
10 | let canLock = !isMobile;
11 |
12 | const starters: string[] = [];
13 | const changers: string[] = [];
14 | const enders: string[] = [];
15 | const leavers: string[] = [];
16 | const prevents: string[] = [];
17 |
18 | if (isPointer) {
19 | starters.push("pointerdown");
20 | enders.push("pointerup", "pointercancel");
21 | changers.push("pointermove");
22 | prevents.push("touchstart", "touchmove", "touchend");
23 | } else if (isMSPointer) {
24 | starters.push("MSPointerDown");
25 | changers.push("MSPointerMove");
26 | enders.push("MSPointerUp");
27 | } else if (isTouch) {
28 | canLock = false;
29 | starters.push("touchstart", "mousedown");
30 | changers.push("touchmove");
31 | enders.push("touchend", "touchcancel", "mouseup");
32 | } else {
33 | starters.push("mousedown");
34 | changers.push("mousemove");
35 | enders.push("mouseup");
36 | leavers.push("mouseleave");
37 | }
38 |
39 | return {
40 | mobile: isMobile,
41 | canLock,
42 | starters,
43 | changers,
44 | enders,
45 | prevents,
46 | leavers,
47 | };
48 | }
49 |
50 | export interface PointerState {
51 | id: string,
52 | x: number,
53 | y: number,
54 | mX: number,
55 | mY: number,
56 | button?: number,
57 | }
58 |
59 | const pointerPositions: {[id: string]: {x: number, y: number}} = {};
60 | export function getPointerState(e: Event, el: HTMLElement, sensitivity: number, locked: boolean): PointerState {
61 | function getState(e: Event): PointerState {
62 | if (e.type.match(/^touch/)) {
63 | const evt = e as TouchEvent;
64 | const rect = el.getBoundingClientRect();
65 | return {
66 | id: "touch-" + evt.targetTouches[0].identifier,
67 | x: evt.targetTouches[0].clientX - rect.x,
68 | y: evt.targetTouches[0].clientY - rect.y,
69 | mX: 0,
70 | mY: 0,
71 | };
72 | } else if (e.type.match(/^pointer/)) {
73 | const evt = e as PointerEvent;
74 | return {
75 | id: "pointer-" + evt.pointerId,
76 | x: evt.offsetX,
77 | y: evt.offsetY,
78 | mX: evt.movementX,
79 | mY: evt.movementY,
80 | };
81 | } else {
82 | const evt = e as MouseEvent;
83 | return {
84 | id: "mouse",
85 | x: evt.offsetX,
86 | y: evt.offsetY,
87 | mX: evt.movementX,
88 | mY: evt.movementY,
89 | button: evt.button === 0 ? 0 : 1,
90 | };
91 | }
92 | }
93 |
94 |
95 | const state = getState(e);
96 | if (!locked) {
97 | if (pointerPositions[state.id]) {
98 | state.mX = state.x - pointerPositions[state.id].x;
99 | state.mY = state.y - pointerPositions[state.id].y;
100 | } else {
101 | state.mX = 0;
102 | state.mY = 0;
103 | }
104 | }
105 |
106 | pointerPositions[state.id] = { x: state.x, y: state.y };
107 | state.mX = calibrateMovement(state.mX, sensitivity);
108 | state.mY = calibrateMovement(state.mY, sensitivity);
109 | return state;
110 | }
111 |
112 | export const pointer = initBind();
113 |
114 | function calibrateMovement(value: number, sensitivity: number) {
115 | if (value > MAX_MOVEMENT_REAL_SPEED) {
116 | value = MAX_MOVEMENT_REAL_SPEED;
117 | } else if (value < -MAX_MOVEMENT_REAL_SPEED) {
118 | value = -MAX_MOVEMENT_REAL_SPEED;
119 | }
120 |
121 | // Map sensitivity (0-1) to logarithmic scale (0.01-5)
122 | // Scale will be 1 when sensitivity = 0.5
123 | const scale = Math.pow(8, sensitivity * 2 - 1);
124 | value = value * scale;
125 |
126 | return value;
127 | }
128 |
129 |
--------------------------------------------------------------------------------
/src/host/lstorage.ts:
--------------------------------------------------------------------------------
1 | const MAX_VALUE_SIZE = 1024;
2 | const NEXT_PART_SYMBOL = "@";
3 | const NEXT_PART_SYFFIX = ".";
4 |
5 | class MemStorage implements Storage {
6 | length = 0;
7 |
8 | private storage: {[key: string]: string} = {};
9 |
10 | setItem(key: string, value: string): void {
11 | this.storage[key] = value;
12 | this.length = Object.keys(this.storage).length;
13 | }
14 |
15 | getItem(key: string): string | null {
16 | const value = this.storage[key];
17 | return value === undefined ? null : value;
18 | }
19 |
20 | removeItem(key: string): void {
21 | delete this.storage[key];
22 | this.length = Object.keys(this.storage).length;
23 | }
24 |
25 | key(index: number): string | null {
26 | const keys = Object.keys(this.storage);
27 | return keys[index] === undefined ? null : keys[index];
28 | }
29 |
30 | clear() {
31 | this.length = 0;
32 | this.storage = {};
33 | }
34 | }
35 |
36 |
37 | class LStorage implements Storage {
38 | private backend: Storage;
39 | length: number;
40 | prefix: string;
41 |
42 | constructor(backend: Storage | undefined, prefix: string) {
43 | this.prefix = prefix;
44 |
45 | try {
46 | this.backend = backend || localStorage;
47 | this.testBackend();
48 | } catch (e) {
49 | this.backend = new MemStorage();
50 | }
51 |
52 | this.length = this.backend.length;
53 |
54 | if (typeof this.backend.sync === "function") {
55 | (this as any).sync = (callback: any) => {
56 | this.backend.sync(callback);
57 | };
58 | }
59 | }
60 |
61 | private testBackend() {
62 | const testKey = this.prefix + ".test.record";
63 | const testValue = "123";
64 | this.backend.setItem(testKey, testValue);
65 | const readedValue = this.backend.getItem(testKey);
66 | this.backend.removeItem(testKey);
67 | const valid = readedValue === testValue && this.backend.getItem(testKey) === null;
68 |
69 | if (!valid) {
70 | throw new Error("Storage backend is not working properly");
71 | }
72 | }
73 |
74 | setLocalStoragePrefix(prefix: string) {
75 | this.prefix = prefix;
76 | }
77 |
78 | clear(): void {
79 | if (!this.backend.length) {
80 | return;
81 | }
82 |
83 | const toRemove: string[] = [];
84 | for (let i = 0; i < this.backend.length; ++i) {
85 | const next = this.backend.key(i);
86 | if (next && next.startsWith(this.prefix)) {
87 | toRemove.push(next);
88 | }
89 | }
90 |
91 | for (const next of toRemove) {
92 | this.backend.removeItem(next);
93 | }
94 | this.length = this.backend.length;
95 | }
96 |
97 | key(index: number): string | null {
98 | return this.backend.key(index);
99 | }
100 |
101 | setItem(key: string, value: string): void {
102 | if (!value || value.length === undefined || value.length === 0) {
103 | this.writeStringToKey(key, "");
104 | return;
105 | }
106 |
107 | let offset = 0;
108 | while (offset < value.length) {
109 | let substr = value.substr(offset, MAX_VALUE_SIZE);
110 | offset += substr.length;
111 |
112 | if (offset < value.length) {
113 | substr += NEXT_PART_SYMBOL;
114 | }
115 |
116 | this.writeStringToKey(key, substr);
117 | key += NEXT_PART_SYFFIX;
118 | }
119 | }
120 |
121 | getItem(key: string): string | null {
122 | let value = this.readStringFromKey(key);
123 | if (value === null) {
124 | return null;
125 | }
126 |
127 | if (value.length === 0) {
128 | return value;
129 | }
130 |
131 | while (value[value.length - 1] === NEXT_PART_SYMBOL) {
132 | value = value.substr(0, value.length - 1);
133 | key += NEXT_PART_SYFFIX;
134 | const next = this.readStringFromKey(key);
135 | value += next === null ? "" : next;
136 | }
137 |
138 | return value;
139 | }
140 |
141 | removeItem(key: string): void {
142 | this.backend.removeItem(this.prefix + key);
143 | this.length = this.backend.length;
144 | }
145 |
146 | private writeStringToKey(key: string, value: string) {
147 | this.backend.setItem(this.prefix + key, value);
148 | this.length = this.backend.length;
149 | }
150 |
151 | private readStringFromKey(key: string) {
152 | return this.backend.getItem(this.prefix + key);
153 | }
154 | }
155 |
156 | export const lStorage = new LStorage(undefined, "jsdos.8.");
157 |
--------------------------------------------------------------------------------
/src/window/dos/sound/audio-node.ts:
--------------------------------------------------------------------------------
1 | import { CommandInterface } from "emulators";
2 |
3 | class SamplesQueue {
4 | private samplesQueue: Float32Array[] = [];
5 |
6 | push(samples: Float32Array) {
7 | this.samplesQueue.push(samples);
8 | }
9 |
10 | length() {
11 | let total = 0;
12 | for (const next of this.samplesQueue) {
13 | total += next.length;
14 | }
15 | return total;
16 | }
17 |
18 | writeTo(dst: Float32Array, bufferSize: number) {
19 | let writeIt = 0;
20 | while (this.samplesQueue.length > 0) {
21 | const src = this.samplesQueue[0];
22 | const toRead = Math.min(bufferSize - writeIt, src.length);
23 | if (toRead === src.length) {
24 | dst.set(src, writeIt);
25 | this.samplesQueue.shift();
26 | } else {
27 | dst.set(src.slice(0, toRead), writeIt);
28 | this.samplesQueue[0] = src.slice(toRead);
29 | }
30 |
31 | writeIt += toRead;
32 |
33 | if (writeIt === bufferSize) {
34 | break;
35 | }
36 | }
37 |
38 | if (writeIt < bufferSize) {
39 | dst.fill(0, writeIt);
40 | }
41 | }
42 | }
43 |
44 | export function audioNode(ci: CommandInterface,
45 | bindVolumeFn?: (fn: (volume: number) => void) => () => void) {
46 | const sampleRate = ci.soundFrequency();
47 | const channels = 1;
48 |
49 | if (sampleRate === 0) {
50 | console.warn("Can't create audio node with sampleRate === 0, ingnoring");
51 | return () => {};
52 | }
53 |
54 | let audioContext: AudioContext | null = null;
55 |
56 | if (typeof AudioContext !== "undefined") {
57 | audioContext = new AudioContext({
58 | sampleRate,
59 | latencyHint: "interactive",
60 | });
61 | } else if (typeof (window as any).webkitAudioContext !== "undefined") {
62 | // eslint-disable-next-line new-cap
63 | audioContext = new (window as any).webkitAudioContext({
64 | sampleRate,
65 | latencyHint: "interactive",
66 | });
67 | }
68 |
69 | if (audioContext == null) {
70 | return () => {};
71 | }
72 |
73 | const samplesQueue = new SamplesQueue();
74 | const bufferSize = 2048;
75 | const preBufferSize = 2048;
76 |
77 | ci.events().onSoundPush((samples) => {
78 | if (samplesQueue.length() < bufferSize * 2 + preBufferSize) {
79 | samplesQueue.push(samples);
80 | }
81 | });
82 |
83 | const audioNode = audioContext.createScriptProcessor(bufferSize, 0, channels);
84 | let started = false;
85 |
86 | const onQueueProcess = (event: AudioProcessingEvent) => {
87 | const numFrames = event.outputBuffer.length;
88 | const numChannels = event.outputBuffer.numberOfChannels;
89 | const samplesCount = samplesQueue.length();
90 |
91 | if (!started) {
92 | started = samplesCount >= preBufferSize;
93 | }
94 |
95 | if (!started) {
96 | return;
97 | }
98 |
99 | for (let channel = 0; channel < numChannels; channel++) {
100 | const channelData = event.outputBuffer.getChannelData(channel);
101 | samplesQueue.writeTo(channelData, numFrames);
102 | }
103 | };
104 |
105 | audioNode.onaudioprocess = onQueueProcess;
106 |
107 | const gainNode = audioContext.createGain();
108 | gainNode.connect(audioContext.destination);
109 | audioNode.connect(gainNode);
110 |
111 | gainNode.gain.value = 1.0;
112 |
113 | let unbindVolumeFn: () => void;
114 | if (bindVolumeFn) {
115 | unbindVolumeFn = bindVolumeFn((volume: number) => {
116 | gainNode.gain.value = volume;
117 | });
118 | }
119 |
120 | const resumeWebAudio = () => {
121 | if (audioContext !== null && audioContext.state === "suspended") {
122 | audioContext.resume();
123 | }
124 | };
125 |
126 | document.addEventListener("pointerdown", resumeWebAudio, { once: true });
127 | document.addEventListener("keydown", resumeWebAudio, { once: true });
128 |
129 | return () => {
130 | ci.events().onSoundPush(() => {});
131 |
132 | if (audioContext !== null) {
133 | audioNode.disconnect();
134 | gainNode.disconnect();
135 | audioContext
136 | .close()
137 | .catch(console.error);
138 | audioContext = null;
139 | }
140 |
141 | if (unbindVolumeFn !== undefined) {
142 | unbindVolumeFn();
143 | }
144 |
145 | document.removeEventListener("pointerdown", resumeWebAudio);
146 | document.removeEventListener("keydown", resumeWebAudio);
147 | };
148 | }
149 |
--------------------------------------------------------------------------------
/src/ui.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from "preact/hooks";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { Frame } from "./frame/frame";
4 | import { SideBar } from "./sidebar/sidebar";
5 | import { State } from "./store";
6 | import { uiSlice } from "./store/ui";
7 | import { Window } from "./window/window";
8 | import { useT } from "./i18n";
9 |
10 | let currentWideScreen = uiSlice.getInitialState().wideScreen;
11 | export function Ui() {
12 | const rootRef = useRef(null);
13 | const hidden = useSelector((state: State) => state.ui.hidden);
14 | const theme = useSelector((state: State) => state.ui.theme);
15 | const dispatch = useDispatch();
16 |
17 | useEffect(() => {
18 | if (hidden || rootRef === null || rootRef.current === null) {
19 | return;
20 | }
21 |
22 | const root = rootRef.current;
23 | function onResize() {
24 | const size = root.getBoundingClientRect().width;
25 | const wide = size > 640;
26 | if (wide !== currentWideScreen) {
27 | currentWideScreen = wide;
28 | dispatch(uiSlice.actions.setWideScreen(currentWideScreen));
29 | }
30 | }
31 |
32 | const resizeObserver = new ResizeObserver(onResize);
33 | resizeObserver.observe(root);
34 | window.addEventListener("resize", onResize);
35 |
36 | return () => {
37 | resizeObserver.disconnect();
38 | window.removeEventListener("resize", onResize);
39 | };
40 | }, [hidden, rootRef, dispatch]);
41 |
42 | if (hidden) {
43 | return null;
44 | }
45 |
46 | return
50 |
51 |
52 |
53 |
54 |
55 |
;
56 | };
57 |
58 | function Toast() {
59 | const toast = useSelector((state: State) => state.ui.toast);
60 | const intent = useSelector((state: State) => state.ui.toastIntent);
61 | const intentClass = intent === "panic" ? "error" : intent;
62 |
63 | if (toast === null) {
64 | return null;
65 | }
66 |
67 | let path = ;
69 |
70 | if (intent === "warning") {
71 | path = ;
74 | }
75 |
76 | if (intent === "error" || intent === "panic") {
77 | path = ;
79 | }
80 |
81 | return
83 |
84 |
91 | {toast}
92 |
93 |
;
94 | }
95 |
96 | function UpdateWsWarning() {
97 | const updateWsWarning = useSelector((state: State) => state.ui.updateWsWarning);
98 | const t = useT();
99 | const dispatch = useDispatch();
100 |
101 | if (!updateWsWarning) {
102 | return null;
103 | }
104 |
105 | function fix() {
106 | window.open("https://dos.zone/download/", "_blank");
107 | dispatch(uiSlice.actions.updateWsWarning(false));
108 | }
109 |
110 | function close() {
111 | dispatch(uiSlice.actions.updateWsWarning(false));
112 | }
113 |
114 | return
115 |
116 |
122 |
{t("ws_outdated")}
123 |
124 |
125 |
126 |
127 |
128 |
;
129 | }
130 |
--------------------------------------------------------------------------------
/src/layers/controls/grid.ts:
--------------------------------------------------------------------------------
1 | export type GridType = "square" | "honeycomb";
2 |
3 | export interface Cell {
4 | centerX: number;
5 | centerY: number;
6 | }
7 |
8 | export interface GridConfiguration {
9 | gridType: GridType,
10 | cells: Cell[][];
11 | columnWidth: number;
12 | rowHeight: number;
13 | columnsPadding: number;
14 | rowsPadding: number;
15 | width: number;
16 | height: number;
17 | }
18 |
19 | export interface Grid {
20 | getConfiguration(width: number, height: number, scale?: number): GridConfiguration;
21 | }
22 |
23 | export function getGrid(gridType: GridType) {
24 | switch (gridType) {
25 | case "square": return getSquareGrid();
26 | case "honeycomb": return getHoneyCombGrid();
27 | }
28 |
29 | throw new Error("Unknown grid type " + gridType);
30 | }
31 |
32 | function getSquareGrid(): Grid {
33 | class SquareGrid implements Grid {
34 | aspect = 200 / 320;
35 |
36 | getConfiguration(width: number, height: number, scale = 1): GridConfiguration {
37 | const cols = this.getCols();
38 | const rows = this.getRows();
39 | const middleCol = Math.floor(cols / 2);
40 | const middleRow = Math.floor(rows / 2);
41 | const columnsPadding = width * 5 / 100 / 2;
42 | const rowsPadding = columnsPadding;
43 | const columnWidth = (width - columnsPadding * 2) / cols * scale;
44 | const rowHeight = (height - rowsPadding * 2) / rows * scale;
45 | const size = Math.min(columnWidth, rowHeight);
46 | const cells: Cell[][] = [];
47 | for (let row = 0; row < rows; ++row) {
48 | const cellRow: Cell[] = [];
49 | for (let col = 0; col < cols; ++col) {
50 | cellRow.push({
51 | centerX: col < middleCol ?
52 | columnsPadding + size * (col + 1 / 2) :
53 | width - columnsPadding - size * ((cols - col - 1) + 1 / 2),
54 | centerY: row < middleRow ?
55 | rowsPadding + size * (row + 1 / 2) :
56 | height - rowsPadding - size * ((rows - row - 1) + 1 / 2),
57 | });
58 | }
59 | cells.push(cellRow);
60 | }
61 | return {
62 | gridType: "square",
63 | cells,
64 | columnWidth: size,
65 | rowHeight: size,
66 | columnsPadding,
67 | rowsPadding,
68 | width,
69 | height,
70 | };
71 | }
72 |
73 | private getCols() {
74 | return 10;
75 | }
76 |
77 | private getRows() {
78 | return Math.floor(this.getCols() * this.aspect) + 1;
79 | }
80 | }
81 |
82 | return new SquareGrid();
83 | }
84 |
85 | function getHoneyCombGrid(): Grid {
86 | class SquareGrid implements Grid {
87 | aspect = 200 / 320;
88 |
89 | getConfiguration(width: number, height: number, scale = 1): GridConfiguration {
90 | const cols = this.getCols();
91 | const rows = this.getRows();
92 | const middleCol = Math.floor(cols / 2);
93 | const middleRow = Math.floor(rows / 2);
94 | const columnsPadding = width * 5 / 100 / 2;
95 | const rowsPadding = columnsPadding;
96 | const columnWidth = (width - columnsPadding * 2) / cols * scale;
97 | const rowHeight = (height - rowsPadding * 2) / rows * scale;
98 | const size = Math.min(columnWidth, rowHeight);
99 | const cells: Cell[][] = [];
100 | for (let row = 0; row < rows; ++row) {
101 | const cellRow: Cell[] = [];
102 | const cellCols = row % 2 == 0 ? cols : cols - 1;
103 | const padding = row % 2 == 0 ? 0 : size / 2;
104 | for (let col = 0; col < cellCols; ++col) {
105 | cellRow.push({
106 | centerX: col < middleCol ?
107 | padding + columnsPadding + size * (col + 1 / 2):
108 | padding + width - columnsPadding - size * ((cols - col - 1) + 1/2),
109 | centerY: row < middleRow ?
110 | rowsPadding + size * (row + 1 / 2) :
111 | height - rowsPadding - size * ((rows - row - 1) + 1 / 2),
112 | });
113 | }
114 | cells.push(cellRow);
115 | }
116 | return {
117 | gridType: "honeycomb",
118 | cells,
119 | columnWidth: size,
120 | rowHeight: size,
121 | columnsPadding,
122 | rowsPadding,
123 | width,
124 | height,
125 | };
126 | }
127 |
128 | getCols() {
129 | return 10;
130 | }
131 |
132 | getRows() {
133 | return Math.floor(this.getCols() * this.aspect) + 1;
134 | }
135 | }
136 |
137 | return new SquareGrid();
138 | }
139 |
--------------------------------------------------------------------------------
/src/player-api-load.ts:
--------------------------------------------------------------------------------
1 | import { Dispatch, Store } from "@reduxjs/toolkit";
2 | import { DosConfig, Emulators, InitFs } from "emulators";
3 | import { dosSlice } from "./store/dos";
4 | import { changesFromUrl, bundleFromFile, bundleFromUrl } from "./host/bundle-storage";
5 | import { uiSlice } from "./store/ui";
6 | import { editorSlice } from "./store/editor";
7 | import { getChangesUrl } from "./v8/changes";
8 | import { storageSlice } from "./store/storage";
9 | import { getNonSerializableStore, getState } from "./store";
10 | import { applySockdriveChanges } from "./player-api";
11 |
12 | declare const emulators: Emulators;
13 |
14 |
15 | export async function loadEmptyBundle(store: Store) {
16 | await doLoadBundle("empty.jsdos",
17 | (async () => {
18 | const bundle = await emulators.bundle();
19 | return bundle.toUint8Array();
20 | })(), null, null, store);
21 |
22 | store.dispatch(uiSlice.actions.frameConf());
23 | store.dispatch(uiSlice.actions.setEditor(true));
24 | }
25 |
26 | export async function loadBundle(bundle: Uint8Array, openConfig: boolean, store: Store) {
27 | await doLoadBundle("bundle.jsdos", Promise.resolve(bundle),
28 | null, null, store);
29 | if (openConfig) {
30 | store.dispatch(uiSlice.actions.frameConf());
31 | }
32 | }
33 |
34 | export function loadBundleFromFile(file: File, store: Store) {
35 | return doLoadBundle(file.name,
36 | bundleFromFile(file, store),
37 | null, null, store);
38 | }
39 |
40 | export async function loadBundleFromConfg(config: DosConfig, initFs: InitFs | null, store: Store) {
41 | const nonSerializableStore = getNonSerializableStore(store);
42 | const dispatch = store.dispatch;
43 | nonSerializableStore.loadedBundle = null;
44 |
45 | dispatch(editorSlice.actions.init(config));
46 | syncWithConfig(config, dispatch);
47 |
48 | nonSerializableStore.loadedBundle = {
49 | bundleUrl: null,
50 | bundleChangesUrl: null,
51 | bundle: config,
52 | bundleChanges: null,
53 | appliedBundleChanges: null,
54 | initFs,
55 | };
56 | dispatch(dosSlice.actions.bndReady({}));
57 | }
58 |
59 | export async function loadBundleFromUrl(url: string, store: Store) {
60 | return doLoadBundle(url,
61 | bundleFromUrl(url, store),
62 | changesProducer(url, store),
63 | url,
64 | store);
65 | }
66 |
67 | async function doLoadBundle(bundleName: string,
68 | bundlePromise: Promise,
69 | bundleChangesPromise: (ReturnType) | null,
70 | bundleUrl: string | null,
71 | store: Store) {
72 | const nonSerializableStore = getNonSerializableStore(store);
73 | const dispatch = store.dispatch;
74 | nonSerializableStore.loadedBundle = null;
75 |
76 |
77 | dispatch(dosSlice.actions.bndLoad(bundleName));
78 |
79 | const bundle = await bundlePromise;
80 | dispatch(storageSlice.actions.ready());
81 | const bundleChanges = await bundleChangesPromise;
82 | dispatch(dosSlice.actions.bndConfig());
83 |
84 | const config = await emulators.bundleConfig(bundle);
85 | dispatch(editorSlice.actions.init(config));
86 | if (config === null) {
87 | dispatch(uiSlice.actions.frameConf());
88 | } else {
89 | syncWithConfig(config, dispatch);
90 | }
91 |
92 | nonSerializableStore.loadedBundle = {
93 | bundleUrl,
94 | bundleChangesUrl: bundleChanges?.url ?? null,
95 | bundle,
96 | bundleChanges: bundleChanges?.bundle ?? null,
97 | appliedBundleChanges: bundleChanges?.appliedBundleChanges ?? null,
98 | initFs: null,
99 | };
100 | dispatch(dosSlice.actions.bndReady({}));
101 | }
102 |
103 | async function changesProducer(bundleUrl: string, store: Store): Promise<{
104 | url: string,
105 | bundle: Uint8Array | null,
106 | appliedBundleChanges: Uint8Array | null,
107 | }> {
108 | const account = getState(store).auth.account;
109 | const owner = account?.email ?? "guest";
110 | const url = getChangesUrl(owner, bundleUrl);
111 | const changes = await changesFromUrl(url, account, store);
112 |
113 | if (changes !== null && changes.length > 1 &&
114 | !(changes[0] === 0x50 && changes[1] === 0x4b)) {
115 | if (!(await applySockdriveChanges(changes))) {
116 | store.dispatch(uiSlice.actions.showToast({
117 | message: "Changes is not a zip file",
118 | intent: "error",
119 | }));
120 | }
121 |
122 | return {
123 | url,
124 | bundle: null,
125 | appliedBundleChanges: changes,
126 | };
127 | }
128 |
129 | return {
130 | url,
131 | bundle: changes,
132 | appliedBundleChanges: null,
133 | };
134 | }
135 |
136 | function syncWithConfig(config: DosConfig, dispatch: Dispatch) {
137 | applySockdriveOptionsIfNeeded(config.dosboxConf, dispatch);
138 | dispatch(dosSlice.actions.mouseCapture(config.dosboxConf.indexOf("autolock=true") >= 0));
139 | }
140 |
141 | export function applySockdriveOptionsIfNeeded(config: string, dispatch: Dispatch) {
142 | if (config.indexOf("sockdrive") >= 0 || config.indexOf(".qcow2") >= 0) {
143 | dispatch(dosSlice.actions.dosBackendLocked(true));
144 | dispatch(dosSlice.actions.dosBackend("dosboxX"));
145 | dispatch(dosSlice.actions.noCursor(true));
146 | dispatch(uiSlice.actions.canSave(config.indexOf(".qcow2") === -1));
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/src/frame/network-frame.tsx:
--------------------------------------------------------------------------------
1 | import { useDispatch, useSelector } from "react-redux";
2 | import { Checkbox } from "../components/checkbox";
3 | import { useT } from "../i18n";
4 | import { State } from "../store";
5 | import { dosSlice } from "../store/dos";
6 | import { Select } from "../components/select";
7 | import { uiSlice } from "../store/ui";
8 | import { Dispatch } from "@reduxjs/toolkit";
9 |
10 | export function NetworkFrame() {
11 | const network = useSelector((state: State) => state.dos.ipx);
12 | const backends = network.backends;
13 | const selected = network.backend;
14 | const room = network.room;
15 | const backend = network.backends.find((v) => v.name === selected) ?? backends[0];
16 | const disabled = network.status !== "disconnected";
17 | const t = useT();
18 | const dispatch = useDispatch();
19 | const ipxLink =
20 | network.status === "connected" ?
21 | location.href + searchSeparator() +
22 | "ipx=1&ipxBackend=" + selected + "&room=" + room :
23 | null;
24 |
25 | function setRoom(room: string) {
26 | dispatch(dosSlice.actions.setRoom(room));
27 | }
28 |
29 | function setIpxBackend(backend: string) {
30 | dispatch(dosSlice.actions.setIpxBackend(backend));
31 | }
32 |
33 | function toggleIpx() {
34 | if (network.status === "connected") {
35 | dispatch(dosSlice.actions.disconnectIpx({}));
36 | } else {
37 | dispatch(dosSlice.actions.connectIpx({
38 | room,
39 | address: backend.host,
40 | }) as any);
41 | }
42 | }
43 |
44 | function copyAndClose() {
45 | if (ipxLink) {
46 | copyToClipBoard(ipxLink, t, dispatch);
47 | dispatch(uiSlice.actions.frameNone());
48 | }
49 | }
50 |
51 | function onServer(newServer: string) {
52 | setIpxBackend(newServer);
53 | }
54 |
55 | return
56 |
57 |
67 |
68 |
71 | setRoom(e.currentTarget.value)}
75 | value={room}>
76 |
77 |
85 |
86 | {ipxLink !== null &&
87 |
{t("copy_net_link")}:
88 |
89 |
104 |
106 |
107 |
}
108 |
;
109 | }
110 |
111 | function searchSeparator() {
112 | if (location.href.endsWith("?") || location.href.endsWith("&")) {
113 | return "";
114 | }
115 | return location.href.indexOf("?") > 0 ? "&" : "?";
116 | }
117 |
118 | async function copyToClipBoard(text: string,
119 | t: (key: string) => string,
120 | dispatch: Dispatch) {
121 | if (!navigator.clipboard) {
122 | return;
123 | }
124 |
125 | try {
126 | await navigator.clipboard.writeText(text);
127 | dispatch(uiSlice.actions.showToast({
128 | message: t("copied"),
129 | intent: "success",
130 | }));
131 | } catch (e: any) {
132 | dispatch(uiSlice.actions.showToast({
133 | message: t("error"),
134 | intent: "error",
135 | }));
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/src/window/select-window.tsx:
--------------------------------------------------------------------------------
1 | import { useDispatch, useStore } from "react-redux";
2 | import { dosSlice } from "../store/dos";
3 | import { useT } from "../i18n";
4 | import { loadBundleFromFile, loadBundleFromUrl, loadEmptyBundle } from "../player-api-load";
5 | import { useState } from "preact/hooks";
6 | import { uiSlice } from "../store/ui";
7 | import { Store } from "../store";
8 | import { uploadFile } from "./file-input";
9 |
10 | export function SelectWindow() {
11 | const t = useT();
12 | const store = useStore() as Store;
13 | const [useUrl, setUseUrl] = useState(false);
14 |
15 | if (useUrl) {
16 | return
17 |
18 |
;
19 | }
20 |
21 | async function createEmpty() {
22 | try {
23 | await loadEmptyBundle(store).catch(console.error);
24 | } catch (e: any) {
25 | store.dispatch(dosSlice.actions.bndError(e.message ?? "unexpected error"));
26 | }
27 | }
28 |
29 | async function onFileChange(fileInput: HTMLInputElement) {
30 | if (fileInput.files === null || fileInput.files.length === 0) {
31 | return;
32 | }
33 |
34 | const file = fileInput.files[0];
35 | try {
36 | await loadBundleFromFile(file, store).catch((e) => store.dispatch(dosSlice.actions.bndError(e.message)));
37 | } catch (e: any) {
38 | store.dispatch(dosSlice.actions.bndError(e.message ?? "unexpected error"));
39 | }
40 | }
41 |
42 | function onUpload() {
43 | uploadFile(onFileChange);
44 | }
45 |
46 | return
47 |
setUseUrl(true)}>{t("load_by_url")}
49 |
50 |
{t("upload_file")}
51 |
52 |
{t("load_archive")}
54 |
{t("create_empty")}
56 |
57 |
{t("sockdrives")}:
58 |
59 | {[
60 | { url: "https://br.cdn.dos.zone/js-dos/system/system-dos7.1-v1.jsdos", label: "DOS v7.1" },
61 | { url: "https://br.cdn.dos.zone/js-dos/system/system-win311-v1.jsdos", label: "Windows 3.11" },
62 | { url: "https://br.cdn.dos.zone/js-dos/system/system-win311-ru.jsdos", label: "Windows 3.11 (RU)" },
63 | { url: "https://br.cdn.dos.zone/js-dos/system/system-win95-v1.jsdos", label: "Windows 95 v1" },
64 | { url: "https://br.cdn.dos.zone/js-dos/system/system-win95-v2.jsdos", label: "Windows 95 v2" },
65 | { url: "https://br.cdn.dos.zone/js-dos/system/system-win95-ru.jsdos", label: "Windows 95 (RU)" },
66 | { url: "https://br.cdn.dos.zone/js-dos/system/system-win98-v1.jsdos", label: "Windows 98" },
67 | ].map(({ url, label }) => (
68 |
70 | {label}
71 |
72 | ))}
73 |
74 |
75 |
;
76 | }
77 |
78 | function Load() {
79 | const t = useT();
80 | const store = useStore();
81 | const dispatch = useDispatch();
82 | const [url, setUrl] = useState("");
83 | const [error, setError] = useState(null);
84 |
85 | async function loadBundle(url: string) {
86 | dispatch(uiSlice.actions.frameNone());
87 |
88 | let validUrl;
89 | try {
90 | validUrl = new URL(url);
91 | } catch (e: any) {
92 | setError(e.message);
93 | return;
94 | }
95 |
96 | try {
97 | await loadBundleFromUrl(validUrl.toString(), store);
98 | } catch (e: any) {
99 | dispatch(dosSlice.actions.bndError(e.message ?? "unexpected error"));
100 | }
101 | }
102 |
103 | return <>
104 |
105 |
108 | setUrl(e.currentTarget.value ?? "")}
111 | value={url}>
112 |
113 | loadBundle(url)}>{t("load")}
115 | {error !== null &&
116 |
117 | {error}
118 |
119 | }
120 | >;
121 | }
122 |
123 | function Upload(props: { onUpload: () => void }) {
124 | return ;
133 | }
134 |
135 |
--------------------------------------------------------------------------------
/src/layers/controls/mouse/mouse-common.ts:
--------------------------------------------------------------------------------
1 | import { CommandInterface } from "emulators";
2 | import { Layers } from "../../dom/layers";
3 | import { pointer, getPointerState } from "../../../window/dos/controls/mouse/pointer";
4 | import { mouseSwipe } from "./mouse-swipe";
5 | import { mouseNotLocked } from "./mouse-not-locked";
6 | import { mouseLocked } from "./mouse-locked";
7 |
8 | const insensitivePadding = 1 / 100;
9 |
10 | export function mapXY(eX: number, eY: number,
11 | ci: CommandInterface, layers: Layers) {
12 | const frameWidth = ci.width();
13 | const frameHeight = ci.height();
14 | const containerWidth = layers.width;
15 | const containerHeight = layers.height;
16 |
17 | const aspect = frameWidth / frameHeight;
18 |
19 | let width = containerWidth;
20 | let height = containerWidth / aspect;
21 |
22 | if (height > containerHeight) {
23 | height = containerHeight;
24 | width = containerHeight * aspect;
25 | }
26 |
27 | const top = (containerHeight - height) / 2;
28 | const left = (containerWidth - width) / 2;
29 |
30 | let x = Math.max(0, Math.min(1, (eX - left) / width));
31 | let y = Math.max(0, Math.min(1, (eY - top) / height));
32 |
33 | if (x <= insensitivePadding) {
34 | x = 0;
35 | }
36 |
37 | if (x >= (1 - insensitivePadding)) {
38 | x = 1;
39 | }
40 |
41 | if (y <= insensitivePadding) {
42 | y = 0;
43 | }
44 |
45 | if (y >= (1 - insensitivePadding)) {
46 | y = 1;
47 | }
48 |
49 | return {
50 | x,
51 | y,
52 | };
53 | }
54 |
55 | export function mount(el: HTMLDivElement, layers: Layers,
56 | sensitivity: number, locked: boolean,
57 | onMouseDown: (x: number, y: number, button: number) => void,
58 | onMouseMove: (x: number, y: number, mX: number, mY: number) => void,
59 | onMouseUp: (x: number, y: number, button: number) => void,
60 | onMouseLeave: (x: number, y: number) => void) {
61 | // eslint-disable-next-line
62 | function preventDefaultIfNeeded(e: Event) {
63 | // not needed yet
64 | }
65 |
66 | let pressedButton = 0;
67 | const onStart = (e: Event) => {
68 | if (e.target !== el) {
69 | return;
70 | }
71 |
72 | if (layers.pointerDisabled) {
73 | e.stopPropagation();
74 | preventDefaultIfNeeded(e);
75 | return;
76 | }
77 |
78 | const state = getPointerState(e, el, sensitivity, locked);
79 | pressedButton = state.button || layers.pointerButton;
80 | onMouseDown(state.x, state.y, pressedButton);
81 |
82 | e.stopPropagation();
83 | preventDefaultIfNeeded(e);
84 | };
85 |
86 | const onChange = (e: Event) => {
87 | if (e.target !== el) {
88 | return;
89 | }
90 |
91 | if (layers.pointerDisabled) {
92 | e.stopPropagation();
93 | preventDefaultIfNeeded(e);
94 | return;
95 | }
96 |
97 | const state = getPointerState(e, el, sensitivity, locked);
98 | onMouseMove(state.x, state.y, state.mX, state.mY);
99 | e.stopPropagation();
100 | preventDefaultIfNeeded(e);
101 | };
102 |
103 | const onEnd = (e: Event) => {
104 | if (layers.pointerDisabled) {
105 | e.stopPropagation();
106 | preventDefaultIfNeeded(e);
107 | return;
108 | }
109 |
110 | const state = getPointerState(e, el, sensitivity, locked);
111 | onMouseUp(state.x, state.y, pressedButton);
112 | e.stopPropagation();
113 | preventDefaultIfNeeded(e);
114 | };
115 |
116 | const onLeave = (e: Event) => {
117 | if (e.target !== el) {
118 | return;
119 | }
120 |
121 | if (layers.pointerDisabled) {
122 | e.stopPropagation();
123 | preventDefaultIfNeeded(e);
124 | return;
125 | }
126 |
127 | const state = getPointerState(e, el, sensitivity, locked);
128 | onMouseLeave(state.x, state.y);
129 | e.stopPropagation();
130 | preventDefaultIfNeeded(e);
131 | };
132 |
133 | const onPrevent = (e: Event) => {
134 | e.stopPropagation();
135 | preventDefaultIfNeeded(e);
136 | };
137 |
138 | const options = {
139 | capture: false,
140 | };
141 |
142 | for (const next of pointer.starters) {
143 | el.addEventListener(next, onStart, options);
144 | }
145 | for (const next of pointer.changers) {
146 | el.addEventListener(next, onChange, options);
147 | }
148 | for (const next of pointer.enders) {
149 | el.addEventListener(next, onEnd, options);
150 | }
151 | for (const next of pointer.prevents) {
152 | el.addEventListener(next, onPrevent, options);
153 | }
154 | for (const next of pointer.leavers) {
155 | el.addEventListener(next, onLeave, options);
156 | }
157 |
158 | return () => {
159 | for (const next of pointer.starters) {
160 | el.removeEventListener(next, onStart, options);
161 | }
162 | for (const next of pointer.changers) {
163 | el.removeEventListener(next, onChange, options);
164 | }
165 | for (const next of pointer.enders) {
166 | el.removeEventListener(next, onEnd, options);
167 | }
168 | for (const next of pointer.prevents) {
169 | el.removeEventListener(next, onPrevent, options);
170 | }
171 | for (const next of pointer.leavers) {
172 | el.removeEventListener(next, onLeave, options);
173 | }
174 | };
175 | }
176 |
177 | export function mouse(autolock: boolean, sensitivity: number, layers: Layers, ci: CommandInterface) {
178 | if (autolock && !pointer.canLock) {
179 | return mouseSwipe(sensitivity, layers, ci);
180 | }
181 |
182 | if (autolock) {
183 | return mouseLocked(sensitivity, layers, ci);
184 | }
185 |
186 | return mouseNotLocked(layers, ci);
187 | }
188 |
--------------------------------------------------------------------------------
/src/window/dos/render/webgl.ts:
--------------------------------------------------------------------------------
1 | import { CommandInterface } from "emulators";
2 | import { resizeCanvas } from "./resize";
3 | import { NonSerializableStore } from "../../../store";
4 |
5 | const vsSource = `
6 | attribute vec4 aVertexPosition;
7 | attribute vec2 aTextureCoord;
8 |
9 | varying highp vec2 vTextureCoord;
10 |
11 | void main(void) {
12 | gl_Position = aVertexPosition;
13 | vTextureCoord = aTextureCoord;
14 | }
15 | `;
16 |
17 | const fsSource = `
18 | varying highp vec2 vTextureCoord;
19 | uniform sampler2D uSampler;
20 |
21 |
22 | void main(void) {
23 | highp vec4 color = texture2D(uSampler, vTextureCoord);
24 | gl_FragColor = vec4(color.r, color.g, color.b, 1.0);
25 | }
26 | `;
27 |
28 | export function webGl(canvas: HTMLCanvasElement,
29 | ci: CommandInterface,
30 | nonSerializableStore: NonSerializableStore,
31 | forceAspect?: number) {
32 | const gl = nonSerializableStore.gl ?? canvas.getContext("webgl");
33 | if (gl === null) {
34 | throw new Error("Unable to create webgl context on given canvas");
35 | }
36 |
37 | nonSerializableStore.gl = gl;
38 |
39 | const shaderProgram = initShaderProgram(gl, vsSource, fsSource);
40 | const vertexPosition = gl.getAttribLocation(shaderProgram, "aVertexPosition");
41 | const textureCoord = gl.getAttribLocation(shaderProgram, "aTextureCoord");
42 | const uSampler = gl.getUniformLocation(shaderProgram, "uSampler");
43 |
44 | initBuffers(gl, vertexPosition, textureCoord);
45 |
46 | const texture = gl.createTexture();
47 | gl.bindTexture(gl.TEXTURE_2D, texture);
48 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
49 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
50 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
51 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
52 |
53 | const pixel = new Uint8Array([0, 0, 0]);
54 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB,
55 | 1, 1, 0, gl.RGB, gl.UNSIGNED_BYTE,
56 | pixel);
57 |
58 | gl.useProgram(shaderProgram);
59 | gl.activeTexture(gl.TEXTURE0);
60 | gl.uniform1i(uSampler, 0);
61 |
62 | let frameWidth = 0;
63 | let frameHeight = 0;
64 |
65 | let requestAnimationFrameId: number | null = null;
66 | let frame: Uint8Array | null = null;
67 | let frameFormat: number = 0;
68 |
69 | const updateTexture = () => {
70 | if (frame !== null) {
71 | gl.texImage2D(gl.TEXTURE_2D, 0, frameFormat,
72 | frameWidth, frameHeight, 0, frameFormat, gl.UNSIGNED_BYTE,
73 | frame);
74 | frame = null;
75 | }
76 | gl.drawArrays(gl.TRIANGLES, 0, 6);
77 |
78 | requestAnimationFrameId = null;
79 | };
80 |
81 | const onResize = () => {
82 | resizeCanvas(canvas, frameWidth, frameHeight, forceAspect);
83 | };
84 |
85 | const onResizeFrame = (w: number, h: number) => {
86 | frameWidth = w;
87 | frameHeight = h;
88 | canvas.width = frameWidth;
89 | canvas.height = frameHeight;
90 | frame = null;
91 | gl.viewport(0, 0, frameWidth, frameHeight);
92 | onResize();
93 | };
94 |
95 | ci.events().onFrameSize(onResizeFrame);
96 | ci.events().onFrame((rgb, rgba) => {
97 | frame = rgb != null ? rgb : rgba;
98 | frameFormat = rgb != null ? gl.RGB : gl.RGBA;
99 | if (requestAnimationFrameId === null) {
100 | requestAnimationFrameId = requestAnimationFrame(updateTexture);
101 | }
102 | });
103 |
104 | onResizeFrame(ci.width(), ci.height());
105 |
106 | const resizeObserver = new ResizeObserver(onResize);
107 | resizeObserver.observe(canvas.parentElement!);
108 | window.addEventListener("resize", onResize);
109 |
110 | return () => {
111 | nonSerializableStore.gl = null;
112 | ci.events().onFrameSize(() => {});
113 | ci.events().onFrame(() => {});
114 | resizeObserver.disconnect();
115 | window.removeEventListener("resize", onResize);
116 | };
117 | }
118 |
119 | function initShaderProgram(gl: WebGLRenderingContext, vsSource: string, fsSource: string) {
120 | const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
121 | const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
122 |
123 | const shaderProgram = gl.createProgram() as WebGLShader;
124 | gl.attachShader(shaderProgram, vertexShader);
125 | gl.attachShader(shaderProgram, fragmentShader);
126 | gl.linkProgram(shaderProgram);
127 |
128 | if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
129 | throw new Error("Unable to initialize the shader program: " + gl.getProgramInfoLog(shaderProgram));
130 | }
131 |
132 | return shaderProgram;
133 | }
134 |
135 | function loadShader(gl: WebGLRenderingContext, shaderType: GLenum, source: string) {
136 | const shader = gl.createShader(shaderType) as WebGLShader;
137 | gl.shaderSource(shader, source);
138 | gl.compileShader(shader);
139 | if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
140 | const info = gl.getShaderInfoLog(shader);
141 | gl.deleteShader(shader);
142 | throw new Error("An error occurred compiling the shaders: " + info);
143 | }
144 |
145 | return shader;
146 | }
147 |
148 | function initBuffers(gl: WebGLRenderingContext, vertexPosition: number, textureCoord: number) {
149 | const positionBuffer = gl.createBuffer();
150 | gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
151 | const positions = [
152 | -1.0, -1.0, 0.0,
153 | 1.0, -1.0, 0.0,
154 | 1.0, 1.0, 0.0,
155 | -1.0, -1.0, 0.0,
156 | 1.0, 1.0, 0.0,
157 | -1.0, 1.0, 0.0,
158 | ];
159 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
160 | gl.vertexAttribPointer(vertexPosition, 3, gl.FLOAT, false, 0, 0);
161 | gl.enableVertexAttribArray(vertexPosition);
162 |
163 | const textureCoordBuffer = gl.createBuffer();
164 | gl.bindBuffer(gl.ARRAY_BUFFER, textureCoordBuffer);
165 | const textureCoordinates = [
166 | 0.0, 1.0,
167 | 1.0, 1.0,
168 | 1.0, 0.0,
169 | 0.0, 1.0,
170 | 1.0, 0.0,
171 | 0.0, 0.0,
172 | ];
173 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textureCoordinates),
174 | gl.STATIC_DRAW);
175 |
176 | gl.vertexAttribPointer(textureCoord, 2, gl.FLOAT, false, 0, 0);
177 | gl.enableVertexAttribArray(textureCoord);
178 | }
179 |
--------------------------------------------------------------------------------
/src/player-api.ts:
--------------------------------------------------------------------------------
1 | import { Dispatch } from "@reduxjs/toolkit";
2 | import { NonSerializableStore, State } from "./store";
3 | import { getT } from "./i18n";
4 | import { putChanges } from "./v8/changes";
5 | import { uiSlice } from "./store/ui";
6 | import { Account } from "./store/auth";
7 | import { PersistedSockdrives } from "emulators";
8 | import { idbSockdrive } from "./host/idb";
9 | import { CommandInterface } from "emulators";
10 |
11 | export async function apiSave(state: State,
12 | nonSerializableStore: NonSerializableStore,
13 | dispatch: Dispatch,
14 | emulationEnded: boolean = false,
15 | encodedChanges: Uint8Array | null = null): Promise {
16 | const ci = nonSerializableStore.ci;
17 | const changesUrl = nonSerializableStore.loadedBundle?.bundleChangesUrl;
18 | if ((ci === null && encodedChanges === null) || !changesUrl || !state.ui.canSave) {
19 | return false;
20 | }
21 |
22 | const t = getT(state);
23 | const account = state.auth.account;
24 | try {
25 | dispatch(uiSlice.actions.showToast({
26 | message: t("saving_game"),
27 | intent: "none",
28 | long: true,
29 | }));
30 |
31 | let savedInIndexedDb = true;
32 | const warnText =
33 | (account === null || account.email === null) ? t("warn_save_no_account") :
34 | (!account.premium) ? t("warn_save_no_premium") :
35 | t("warn_save_big_file");
36 |
37 | let warnAboutSaves = false;
38 | if (encodedChanges === null) {
39 | const changes = await ci!.persist(true);
40 | encodedChanges = encodeChanges(changes);
41 | warnAboutSaves = encodedChanges !== changes && !emulationEnded;
42 | }
43 | if (encodedChanges !== null) {
44 | if (warnAboutSaves) {
45 | dispatch(uiSlice.actions.showToast({
46 | message: t("sockdrive_save_in_the_middle"),
47 | intent: "warning",
48 | long: true,
49 | }));
50 | }
51 |
52 | if (canDoCloudSave(account, encodedChanges)) {
53 | await putChanges(changesUrl, encodedChanges);
54 | savedInIndexedDb = false;
55 | } else {
56 | await nonSerializableStore.cache.put(changesUrl, encodedChanges);
57 | }
58 | }
59 |
60 | if (savedInIndexedDb) {
61 | setTimeout(() => {
62 | dispatch(uiSlice.actions.showToast({
63 | message: warnText,
64 | intent: "success",
65 | long: true,
66 | }));
67 | }, warnAboutSaves ? 3000 : 4);
68 | } else {
69 | dispatch(uiSlice.actions.showToast({
70 | message: t("success_save"),
71 | intent: "success",
72 | long: true,
73 | }));
74 | }
75 |
76 | return true;
77 | } catch (e: any) {
78 | dispatch(uiSlice.actions.showToast({
79 | message: t("unable_to_save"),
80 | intent: "error",
81 | long: true,
82 | }));
83 | console.error(e);
84 |
85 | return false;
86 | }
87 | }
88 |
89 | export function canDoCloudSave(account: Account | null, changes: Uint8Array | null) {
90 | if (account) {
91 | return account.email !== undefined &&
92 | (account.email === "dz.caiiiycuk@gmail.com" || account.premium === true) &&
93 | (changes === null || changes.length <= 25 * 1024 * 1024);
94 | }
95 | return false;
96 | }
97 |
98 | export async function applySockdriveChanges(encoded: Uint8Array): Promise {
99 | return traverseSockdriveChanges(encoded, async (url, persist) => {
100 | const idb = await idbSockdrive(url);
101 | await idb.put(0 as any, persist);
102 | idb.close();
103 | });
104 | }
105 |
106 | export async function traverseSockdriveChanges(encoded: Uint8Array,
107 | callback: (url: string, persist: Uint8Array) => Promise) {
108 | const decoder = new TextDecoder();
109 | let offset = 0;
110 | while (offset < encoded.length) {
111 | const urlLength = readUint32(encoded, offset);
112 | offset += 4;
113 |
114 | if (urlLength > 4096) {
115 | return false;
116 | }
117 |
118 | const url = decoder.decode(encoded.slice(offset, offset + urlLength));
119 |
120 | if (!(url.startsWith("http://") || url.startsWith("https://"))) {
121 | return false;
122 | }
123 |
124 | offset += urlLength;
125 |
126 | const persistLength = readUint32(encoded, offset);
127 | offset += 4;
128 |
129 | const persist = encoded.slice(offset, offset + persistLength);
130 | offset += persistLength;
131 | await callback(url, persist);
132 | }
133 |
134 | return true;
135 | }
136 |
137 | function encodeChanges(changes: Uint8Array | PersistedSockdrives | null) {
138 | if (changes === null || changes instanceof Uint8Array) {
139 | return changes;
140 | }
141 |
142 | const encoder = new TextEncoder();
143 |
144 | const urls = [];
145 | let totalSize = 0;
146 | for (const { url, persist } of changes.drives) {
147 | urls.push(encoder.encode(url));
148 | totalSize += persist.length + urls[urls.length - 1].length + 8;
149 | }
150 |
151 | const result = new Uint8Array(totalSize);
152 | let offset = 0;
153 | for (let i = 0; i < changes.drives.length; i++) {
154 | const url = urls[i];
155 | const persist = changes.drives[i].persist;
156 |
157 | offset = writeUint32(result, url.length, offset);
158 | result.set(url, offset);
159 | offset += url.length;
160 |
161 | offset = writeUint32(result, persist.length, offset);
162 | result.set(persist, offset);
163 | offset += persist.length;
164 | }
165 |
166 | return result;
167 | }
168 |
169 | export function writeUint32(container: Uint8Array, value: number, offset: number) {
170 | container[offset] = value & 0xFF;
171 | container[offset + 1] = (value & 0x0000FF00) >> 8;
172 | container[offset + 2] = (value & 0x00FF0000) >> 16;
173 | container[offset + 3] = (value & 0xFF000000) >> 24;
174 | return offset + 4;
175 | }
176 |
177 | export function readUint32(container: Uint8Array, offset: number) {
178 | return (container[offset] & 0x000000FF) |
179 | ((container[offset + 1] << 8) & 0x0000FF00) |
180 | ((container[offset + 2] << 16) & 0x00FF0000) |
181 | ((container[offset + 3] << 24) & 0xFF000000);
182 | }
183 |
184 | export function sendQuickSaveEvent(ci: CommandInterface) {
185 | ci.sendBackendEvent({
186 | type: "wc-trigger-event",
187 | event: "hand_savestate",
188 | });
189 | }
190 |
191 | export function sendQuickLoadEvent(ci: CommandInterface) {
192 | ci.sendBackendEvent({
193 | type: "wc-trigger-event",
194 | event: "hand_loadstate",
195 | });
196 | }
197 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | js-dos 8.xx
8 |
15 |
16 |
17 |
18 |
19 |
21 |
218 |
219 |
220 |
--------------------------------------------------------------------------------
/src/host/idb.ts:
--------------------------------------------------------------------------------
1 | /* eslint @typescript-eslint/no-unused-vars: 0 */
2 |
3 | export interface IDB {
4 | put: (key: string, data: Uint8Array) => Promise;
5 | get: (key: string, defaultValue?: Uint8Array) => Promise;
6 | del: (key: string) => Promise;
7 | keys: () => Promise;
8 | forEach: (each: (key: string, value: Uint8Array) => void) => Promise;
9 | close: () => void;
10 | }
11 |
12 | export class IDBNoop implements IDB {
13 | public close() {
14 | }
15 |
16 | public put(key: string, data: Uint8Array): Promise {
17 | return Promise.resolve();
18 | }
19 |
20 | public get(key: string, defaultValue?: Uint8Array): Promise {
21 | if (defaultValue !== undefined) {
22 | return Promise.resolve(defaultValue);
23 | }
24 | return Promise.reject(new Error("Cache is not supported on this host"));
25 | }
26 |
27 | public del(key: string): Promise {
28 | return Promise.resolve();
29 | }
30 |
31 | public keys(): Promise {
32 | return Promise.resolve([]);
33 | }
34 |
35 | public forEach(each: (key: string, value: Uint8Array) => void) {
36 | return Promise.resolve();
37 | }
38 | }
39 |
40 | class IDBImpl implements IDB {
41 | private storeName = "files";
42 | private indexedDB: IDBFactory;
43 | private db: IDBDatabase | null = null;
44 |
45 | constructor(dbName: string,
46 | storeName: string,
47 | stores: [string, string, boolean][],
48 | onready: (cache: IDB) => void,
49 | onerror: (msg: string) => void) {
50 | this.storeName = storeName;
51 | this.indexedDB = (typeof window === "undefined" ? undefined : window.indexedDB ||
52 | (window as any).mozIndexedDB ||
53 | (window as any).webkitIndexedDB || (window as any).msIndexedDB) as any;
54 |
55 | if (!this.indexedDB) {
56 | onerror("Indexed db is not supported on this host");
57 | return;
58 | }
59 |
60 | try {
61 | const openRequest = this.indexedDB.open(dbName, 1);
62 | openRequest.onerror = (event) => {
63 | onerror("Can't open cache database: " + openRequest.error?.message);
64 | };
65 | openRequest.onsuccess = (event) => {
66 | this.db = openRequest.result;
67 | onready(this);
68 | };
69 | openRequest.onupgradeneeded = (event) => {
70 | try {
71 | this.db = openRequest.result;
72 | this.db.onerror = (event) => {
73 | onerror("Can't upgrade cache database");
74 | };
75 |
76 | for (const [name, index, unique] of stores) {
77 | this.db.createObjectStore(name)
78 | .createIndex(index, "", {
79 | unique,
80 | multiEntry: false,
81 | });
82 | }
83 | } catch (e) {
84 | onerror("Can't upgrade cache database");
85 | }
86 | };
87 | } catch (e: any) {
88 | onerror("Can't open cache database: " + e.message);
89 | }
90 | }
91 |
92 | private async resultToUint8Array(result: ArrayBuffer | Blob): Promise {
93 | if (result instanceof Blob) {
94 | return new Uint8Array(await result.arrayBuffer());
95 | }
96 | return new Uint8Array(result);
97 | }
98 |
99 | public close() {
100 | if (this.db !== null) {
101 | this.db.close();
102 | this.db = null;
103 | }
104 | }
105 |
106 | public put(key: string, data: Uint8Array): Promise {
107 | return new Promise((resolve, reject) => {
108 | if (this.db === null) {
109 | resolve();
110 | return;
111 | }
112 |
113 | const transaction = this.db.transaction(this.storeName, "readwrite");
114 | const request = transaction.objectStore(this.storeName).put(new Blob([data.buffer]), key);
115 | request.onerror = (e) => {
116 | reject(new Error("Can't put key '" + key + "'"));
117 | console.error(e);
118 | };
119 | request.onsuccess = () => resolve();
120 | });
121 | }
122 |
123 | public del(key: string): Promise {
124 | return new Promise((resolve, reject) => {
125 | if (this.db === null) {
126 | resolve();
127 | return;
128 | }
129 |
130 | const transaction = this.db.transaction(this.storeName, "readwrite");
131 | const request = transaction.objectStore(this.storeName).delete(key);
132 | request.onerror = () => reject;
133 | request.onsuccess = () => resolve();
134 | });
135 | }
136 |
137 | public get(key: string, defaultValue?: Uint8Array): Promise {
138 | return new Promise((resolve, reject) => {
139 | function rejectOrResolve(message: string) {
140 | if (defaultValue === undefined) {
141 | reject(new Error(message));
142 | } else {
143 | resolve(defaultValue);
144 | }
145 | }
146 |
147 |
148 | if (this.db === null) {
149 | rejectOrResolve("db is not initalized");
150 | return;
151 | }
152 |
153 | const transaction = this.db.transaction(this.storeName, "readonly");
154 | const request = transaction.objectStore(this.storeName).get(key) as IDBRequest;
155 | request.onerror = () => reject(new Error("Can't read value for key '" + key + "'"));
156 | request.onsuccess = () => {
157 | if (request.result) {
158 | resolve(this.resultToUint8Array(request.result));
159 | } else {
160 | rejectOrResolve("Result is empty for key '" + key + "', result: " + request.result);
161 | }
162 | };
163 | });
164 | }
165 |
166 | public keys(): Promise {
167 | return new Promise((resolve, reject) => {
168 | if (this.db === null) {
169 | resolve([]);
170 | return;
171 | }
172 |
173 | const transaction = this.db.transaction(this.storeName, "readonly");
174 | const request = transaction.objectStore(this.storeName).getAllKeys();
175 | request.onerror = reject;
176 | request.onsuccess = (event) => {
177 | if (request.result) {
178 | resolve(request.result as string[]);
179 | } else {
180 | resolve([]);
181 | }
182 | };
183 | });
184 | }
185 |
186 | public async forEach(each: (key: string, value: Uint8Array) => void): Promise {
187 | const keys = await this.keys();
188 | for (const key of keys) {
189 | const value = await this.get(key);
190 | if (value) {
191 | each(key, value);
192 | }
193 | }
194 | }
195 | }
196 |
197 | export function idbCache(): Promise {
198 | return new Promise((resolve) => {
199 | new IDBImpl("js-dos-cache (guest)", "files",
200 | [["files", "key", true]], resolve, (msg: string) => {
201 | console.error("Can't open IndexedDB cache", msg);
202 | resolve(new IDBNoop());
203 | });
204 | });
205 | }
206 |
207 | export function idbSockdrive(url: string): Promise {
208 | return new Promise((resolve) => {
209 | new IDBImpl("sockdrive (" + url + ")", "write",
210 | [["raw", "range", false], ["write", "sector", false]], resolve, (msg: string) => {
211 | console.error("Can't open IndexedDB cache", msg);
212 | resolve(new IDBNoop());
213 | });
214 | });
215 | }
216 |
--------------------------------------------------------------------------------