├── .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 6 | 9 | ; 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
7 | 13 | 15 | 16 |
; 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 8 | 13 | ; 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
24 |
25 | 27 | 30 | 31 | { inactive && 33 | 34 | } 35 |
36 | 37 | 38 |
; 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 |