├── apps ├── proxy │ ├── .gitignore │ ├── Dockerfile │ ├── .env-dist │ ├── README.md │ ├── package.json │ ├── Caddyfile │ └── prod.Caddyfile ├── gif-service │ ├── .gitignore │ ├── src │ │ ├── index.ts │ │ ├── gif-encoder-2.d.ts │ │ ├── tap-file.ts │ │ └── routes │ │ │ └── tape.ts │ ├── tsconfig.json │ └── package.json ├── web │ ├── src │ │ ├── lib │ │ │ ├── jsspeccy │ │ │ │ ├── constants.js │ │ │ │ ├── tape │ │ │ │ │ ├── index.js │ │ │ │ │ ├── segments │ │ │ │ │ │ ├── PulseSequenceSegment.js │ │ │ │ │ │ ├── PauseSegment.js │ │ │ │ │ │ ├── ToneSegment.js │ │ │ │ │ │ ├── DataSegment.js │ │ │ │ │ │ └── PulseGenerator.js │ │ │ │ │ └── TAPFile.js │ │ │ │ ├── ui │ │ │ │ │ ├── icons │ │ │ │ │ │ ├── play.svg │ │ │ │ │ │ ├── pause.svg │ │ │ │ │ │ ├── exitfullscreen.svg │ │ │ │ │ │ ├── fullscreen.svg │ │ │ │ │ │ ├── close.svg │ │ │ │ │ │ ├── open.svg │ │ │ │ │ │ ├── reset.svg │ │ │ │ │ │ ├── tape_play.svg │ │ │ │ │ │ └── tape_pause.svg │ │ │ │ │ ├── index.js │ │ │ │ │ ├── ToolbarButton.js │ │ │ │ │ ├── MenuBar.js │ │ │ │ │ ├── Toolbar.js │ │ │ │ │ └── Menu.js │ │ │ │ ├── generator │ │ │ │ │ ├── opcodes_ed.txt │ │ │ │ │ └── opcodes_dd.txt │ │ │ │ └── DisplayHandler.js │ │ │ ├── lang.js │ │ │ ├── 8bitworker │ │ │ │ ├── defs_files.ts │ │ │ │ ├── shared_vars.ts │ │ │ │ ├── shared_funcs.ts │ │ │ │ ├── defs_build.ts │ │ │ │ ├── worker.ts │ │ │ │ ├── SourceFile.ts │ │ │ │ ├── defs_build_result.ts │ │ │ │ ├── modules.ts │ │ │ │ ├── FileWorkingStore.ts │ │ │ │ └── tools │ │ │ │ │ ├── mcpp.ts │ │ │ │ │ └── z80.ts │ │ │ ├── canvasgui │ │ │ │ ├── ImageManager.js │ │ │ │ ├── Widget.js │ │ │ │ ├── index.js │ │ │ │ ├── ImageButton.js │ │ │ │ ├── Control.js │ │ │ │ ├── TextButton.js │ │ │ │ └── Group.js │ │ │ └── avatar.js │ │ ├── components │ │ │ ├── LoadingScreen.jsx │ │ │ ├── MaxWidth.jsx │ │ │ ├── RequireSubscriber.jsx │ │ │ ├── RenderEmulator.jsx │ │ │ ├── ErrorNotFoundPage.jsx │ │ │ ├── ErrorBoundary.jsx │ │ │ ├── YourProjectsPage.jsx │ │ │ ├── YourProfilePage.jsx │ │ │ ├── LineNumbersToggle.jsx │ │ │ ├── Emulator.jsx │ │ │ ├── TermsOfUsePage.jsx │ │ │ ├── PrivacyPolicyPage.jsx │ │ │ ├── LockScreen.jsx │ │ │ ├── NewProjectPage.jsx │ │ │ ├── DemoAssemblyEditor.jsx │ │ │ ├── ErrorPage.jsx │ │ │ └── DemoSinclairBasicEditor.jsx │ │ ├── redux │ │ │ ├── profile │ │ │ │ └── actions.js │ │ │ ├── window │ │ │ │ ├── actions.js │ │ │ │ ├── reducers.js │ │ │ │ └── sagas.js │ │ │ ├── error │ │ │ │ ├── actions.js │ │ │ │ └── reducers.js │ │ │ ├── identity │ │ │ │ ├── actions.js │ │ │ │ ├── reducers.js │ │ │ │ └── sagas.js │ │ │ ├── demo │ │ │ │ ├── actions.js │ │ │ │ └── sagas.js │ │ │ ├── subscriber │ │ │ │ ├── actions.js │ │ │ │ └── reducers.js │ │ │ ├── app │ │ │ │ ├── actions.js │ │ │ │ └── reducers.js │ │ │ ├── projectList │ │ │ │ ├── actions.js │ │ │ │ └── reducers.js │ │ │ ├── eightbit │ │ │ │ ├── reducers.js │ │ │ │ └── actions.js │ │ │ ├── jsspeccy │ │ │ │ └── actions.js │ │ │ ├── project │ │ │ │ └── actions.js │ │ │ └── social │ │ │ │ └── actions.js │ │ ├── dashboard_lock.js │ │ ├── index.jsx │ │ ├── dashboard_loading.js │ │ ├── constants.js │ │ ├── auth.js │ │ ├── graphql_fetch.js │ │ └── utils │ │ │ └── slug.js │ ├── public │ │ ├── logo.png │ │ ├── favicon.ico │ │ ├── logo150.png │ │ ├── roms │ │ │ ├── 48.rom │ │ │ ├── 128-0.rom │ │ │ ├── 128-1.rom │ │ │ ├── opense.rom │ │ │ ├── trdos.rom │ │ │ └── pentagon-0.rom │ │ ├── keys │ │ │ ├── key0.png │ │ │ ├── key1.png │ │ │ ├── key2.png │ │ │ ├── key3.png │ │ │ ├── key4.png │ │ │ ├── key5.png │ │ │ ├── key6.png │ │ │ ├── key7.png │ │ │ ├── key8.png │ │ │ ├── key9.png │ │ │ ├── keyA.png │ │ │ ├── keyB.png │ │ │ ├── keyC.png │ │ │ ├── keyD.png │ │ │ ├── keyE.png │ │ │ ├── keyF.png │ │ │ ├── keyG.png │ │ │ ├── keyH.png │ │ │ ├── keyI.png │ │ │ ├── keyJ.png │ │ │ ├── keyK.png │ │ │ ├── keyL.png │ │ │ ├── keyM.png │ │ │ ├── keyN.png │ │ │ ├── keyO.png │ │ │ ├── keyP.png │ │ │ ├── keyQ.png │ │ │ ├── keyR.png │ │ │ ├── keyS.png │ │ │ ├── keyT.png │ │ │ ├── keyU.png │ │ │ ├── keyV.png │ │ │ ├── keyW.png │ │ │ ├── keyX.png │ │ │ ├── keyY.png │ │ │ ├── keyZ.png │ │ │ ├── keyCAP.png │ │ │ ├── keyCAP2.png │ │ │ ├── keyENT.png │ │ │ ├── keyNONE.png │ │ │ ├── keySPC.png │ │ │ ├── keySPC2.png │ │ │ └── keySYM.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── mstile-150x150.png │ │ ├── apple-touch-icon.png │ │ ├── tapeloaders │ │ │ ├── tape_48.szx │ │ │ ├── tape_128.szx │ │ │ ├── tape_128_usr0.szx │ │ │ ├── tape_pentagon.szx │ │ │ └── tape_pentagon_usr0.szx │ │ ├── 8bitworker │ │ │ ├── fs │ │ │ │ └── fssdcc.data │ │ │ ├── wasm │ │ │ │ ├── sdcc.wasm │ │ │ │ ├── zmac.wasm │ │ │ │ ├── sdasz80.wasm │ │ │ │ └── sdldz80.wasm │ │ │ └── zx │ │ │ │ ├── crt0.sym │ │ │ │ ├── crt0.rel │ │ │ │ └── crt0.s │ │ ├── android-chrome-96x96.png │ │ ├── assets │ │ │ └── images │ │ │ │ ├── zx-square.png │ │ │ │ └── embed-preview.png │ │ ├── browserconfig.xml │ │ ├── site.webmanifest │ │ ├── safari-pinned-tab.svg │ │ └── index.html │ ├── tsfmt.json │ ├── tsconfig.jsspeccy.json │ ├── Caddyfile │ ├── .dockerignore │ ├── .gitignore │ ├── Dockerfile │ ├── asconfig.json │ └── tsconfig.json └── auth │ ├── package.json │ ├── Auth │ ├── GraphQL │ │ ├── GetUser.graphql │ │ ├── GetUserByEmail.graphql │ │ ├── UpdateUserEmail.graphql │ │ ├── CreateUser.graphql │ │ ├── GetUserRoles.graphql │ │ ├── UpdateSessionTimestamp.graphql │ │ ├── CreateSession.graphql │ │ └── GetSession.graphql │ ├── Model │ │ ├── Role.cs │ │ ├── UserRoles.cs │ │ ├── Session.cs │ │ └── User.cs │ ├── appsettings.Development.json │ ├── Properties │ │ └── launchSettings.json │ ├── appsettings.json │ ├── Tokens │ │ ├── SessionTokenDispenser.cs │ │ ├── HasuraTokenDispenser.cs │ │ ├── TokenDispenser.cs │ │ └── SessionTokenCookieReader.cs │ ├── Codespaces.cs │ ├── Controllers │ │ ├── MeController.cs │ │ ├── TokenDispenserController.cs │ │ ├── AssertionConsumerController.cs │ │ └── LogoutController.cs │ ├── Auth.csproj │ └── Program.cs │ ├── .gitignore │ ├── .dockerignore │ ├── Dockerfile │ ├── Auth.sln │ └── README.md ├── hasura ├── metadata │ ├── allow_list.yaml │ ├── api_limits.yaml │ ├── network.yaml │ ├── backend_configs.yaml │ ├── cron_triggers.yaml │ ├── inherited_roles.yaml │ ├── metrics_config.yaml │ ├── opentelemetry.yaml │ ├── remote_schemas.yaml │ ├── rest_endpoints.yaml │ ├── version.yaml │ ├── query_collections.yaml │ ├── graphql_schema_introspection.yaml │ ├── databases │ │ ├── default │ │ │ └── tables │ │ │ │ ├── public_session.yaml │ │ │ │ ├── public_user_role.yaml │ │ │ │ ├── tables.yaml │ │ │ │ ├── public_role.yaml │ │ │ │ ├── public_text.yaml │ │ │ │ ├── public_user_follows.yaml │ │ │ │ ├── public_project.yaml │ │ │ │ └── public_user.yaml │ │ └── databases.yaml │ ├── actions.graphql │ └── actions.yaml ├── config.yaml └── migrations │ └── default │ ├── 1759630462652_add_avatar_fields │ ├── down.sql │ └── up.sql │ ├── 1759606682000_add_project_display_order │ ├── down.sql │ └── up.sql │ ├── 1759572893000_add_user_profile_fields │ ├── down.sql │ └── up.sql │ ├── 1759594620000_add_social_features │ ├── down.sql │ └── up.sql │ └── 1759572916000_add_slug_constraints │ ├── down.sql │ └── up.sql ├── .gitignore ├── turbo.json ├── .env-dist ├── package.json ├── docker-compose.yaml └── .github └── workflows └── publish-containers.yml /apps/proxy/.gitignore: -------------------------------------------------------------------------------- 1 | /.env 2 | -------------------------------------------------------------------------------- /apps/gif-service/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | -------------------------------------------------------------------------------- /hasura/metadata/allow_list.yaml: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /hasura/metadata/api_limits.yaml: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /hasura/metadata/network.yaml: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /hasura/metadata/backend_configs.yaml: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /hasura/metadata/cron_triggers.yaml: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /hasura/metadata/inherited_roles.yaml: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /hasura/metadata/metrics_config.yaml: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /hasura/metadata/opentelemetry.yaml: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /hasura/metadata/remote_schemas.yaml: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /hasura/metadata/rest_endpoints.yaml: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /hasura/metadata/version.yaml: -------------------------------------------------------------------------------- 1 | version: 3 2 | -------------------------------------------------------------------------------- /hasura/metadata/query_collections.yaml: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /hasura/metadata/graphql_schema_introspection.yaml: -------------------------------------------------------------------------------- 1 | disabled_for_roles: [] 2 | -------------------------------------------------------------------------------- /apps/web/src/lib/jsspeccy/constants.js: -------------------------------------------------------------------------------- 1 | export const FRAME_BUFFER_SIZE = 0x6600; 2 | -------------------------------------------------------------------------------- /apps/web/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/logo.png -------------------------------------------------------------------------------- /apps/proxy/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/stever/caddy-auth:latest 2 | COPY prod.Caddyfile /etc/caddy/Caddyfile -------------------------------------------------------------------------------- /apps/web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/favicon.ico -------------------------------------------------------------------------------- /apps/web/public/logo150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/logo150.png -------------------------------------------------------------------------------- /apps/web/public/roms/48.rom: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/roms/48.rom -------------------------------------------------------------------------------- /apps/web/public/keys/key0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/keys/key0.png -------------------------------------------------------------------------------- /apps/web/public/keys/key1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/keys/key1.png -------------------------------------------------------------------------------- /apps/web/public/keys/key2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/keys/key2.png -------------------------------------------------------------------------------- /apps/web/public/keys/key3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/keys/key3.png -------------------------------------------------------------------------------- /apps/web/public/keys/key4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/keys/key4.png -------------------------------------------------------------------------------- /apps/web/public/keys/key5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/keys/key5.png -------------------------------------------------------------------------------- /apps/web/public/keys/key6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/keys/key6.png -------------------------------------------------------------------------------- /apps/web/public/keys/key7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/keys/key7.png -------------------------------------------------------------------------------- /apps/web/public/keys/key8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/keys/key8.png -------------------------------------------------------------------------------- /apps/web/public/keys/key9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/keys/key9.png -------------------------------------------------------------------------------- /apps/web/public/keys/keyA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/keys/keyA.png -------------------------------------------------------------------------------- /apps/web/public/keys/keyB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/keys/keyB.png -------------------------------------------------------------------------------- /apps/web/public/keys/keyC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/keys/keyC.png -------------------------------------------------------------------------------- /apps/web/public/keys/keyD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/keys/keyD.png -------------------------------------------------------------------------------- /apps/web/public/keys/keyE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/keys/keyE.png -------------------------------------------------------------------------------- /apps/web/public/keys/keyF.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/keys/keyF.png -------------------------------------------------------------------------------- /apps/web/public/keys/keyG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/keys/keyG.png -------------------------------------------------------------------------------- /apps/web/public/keys/keyH.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/keys/keyH.png -------------------------------------------------------------------------------- /apps/web/public/keys/keyI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/keys/keyI.png -------------------------------------------------------------------------------- /apps/web/public/keys/keyJ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/keys/keyJ.png -------------------------------------------------------------------------------- /apps/web/public/keys/keyK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/keys/keyK.png -------------------------------------------------------------------------------- /apps/web/public/keys/keyL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/keys/keyL.png -------------------------------------------------------------------------------- /apps/web/public/keys/keyM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/keys/keyM.png -------------------------------------------------------------------------------- /apps/web/public/keys/keyN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/keys/keyN.png -------------------------------------------------------------------------------- /apps/web/public/keys/keyO.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/keys/keyO.png -------------------------------------------------------------------------------- /apps/web/public/keys/keyP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/keys/keyP.png -------------------------------------------------------------------------------- /apps/web/public/keys/keyQ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/keys/keyQ.png -------------------------------------------------------------------------------- /apps/web/public/keys/keyR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/keys/keyR.png -------------------------------------------------------------------------------- /apps/web/public/keys/keyS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/keys/keyS.png -------------------------------------------------------------------------------- /apps/web/public/keys/keyT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/keys/keyT.png -------------------------------------------------------------------------------- /apps/web/public/keys/keyU.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/keys/keyU.png -------------------------------------------------------------------------------- /apps/web/public/keys/keyV.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/keys/keyV.png -------------------------------------------------------------------------------- /apps/web/public/keys/keyW.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/keys/keyW.png -------------------------------------------------------------------------------- /apps/web/public/keys/keyX.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/keys/keyX.png -------------------------------------------------------------------------------- /apps/web/public/keys/keyY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/keys/keyY.png -------------------------------------------------------------------------------- /apps/web/public/keys/keyZ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/keys/keyZ.png -------------------------------------------------------------------------------- /apps/web/public/keys/keyCAP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/keys/keyCAP.png -------------------------------------------------------------------------------- /apps/web/public/keys/keyCAP2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/keys/keyCAP2.png -------------------------------------------------------------------------------- /apps/web/public/keys/keyENT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/keys/keyENT.png -------------------------------------------------------------------------------- /apps/web/public/keys/keyNONE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/keys/keyNONE.png -------------------------------------------------------------------------------- /apps/web/public/keys/keySPC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/keys/keySPC.png -------------------------------------------------------------------------------- /apps/web/public/keys/keySPC2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/keys/keySPC2.png -------------------------------------------------------------------------------- /apps/web/public/keys/keySYM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/keys/keySYM.png -------------------------------------------------------------------------------- /apps/web/public/roms/128-0.rom: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/roms/128-0.rom -------------------------------------------------------------------------------- /apps/web/public/roms/128-1.rom: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/roms/128-1.rom -------------------------------------------------------------------------------- /apps/web/public/roms/opense.rom: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/roms/opense.rom -------------------------------------------------------------------------------- /apps/web/public/roms/trdos.rom: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/roms/trdos.rom -------------------------------------------------------------------------------- /apps/web/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/favicon-16x16.png -------------------------------------------------------------------------------- /apps/web/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/favicon-32x32.png -------------------------------------------------------------------------------- /apps/web/public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/mstile-150x150.png -------------------------------------------------------------------------------- /apps/web/src/lib/jsspeccy/tape/index.js: -------------------------------------------------------------------------------- 1 | export {TAPFile} from "./TAPFile"; 2 | export {TZXFile} from "./TZXFile"; 3 | -------------------------------------------------------------------------------- /apps/web/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/apple-touch-icon.png -------------------------------------------------------------------------------- /apps/web/public/roms/pentagon-0.rom: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/roms/pentagon-0.rom -------------------------------------------------------------------------------- /apps/web/tsfmt.json: -------------------------------------------------------------------------------- 1 | { 2 | "indentSize": 4, 3 | "newLineCharacter": "\n", 4 | "convertTabsToSpaces": true 5 | } 6 | -------------------------------------------------------------------------------- /apps/proxy/.env-dist: -------------------------------------------------------------------------------- 1 | HASURA_JWT_DEFAULT_ROLE="zxplay-user" 2 | AUTH_SESSION_JWT_SECRET="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" 3 | -------------------------------------------------------------------------------- /apps/web/public/tapeloaders/tape_48.szx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/tapeloaders/tape_48.szx -------------------------------------------------------------------------------- /apps/web/public/8bitworker/fs/fssdcc.data: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/8bitworker/fs/fssdcc.data -------------------------------------------------------------------------------- /apps/web/public/8bitworker/wasm/sdcc.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/8bitworker/wasm/sdcc.wasm -------------------------------------------------------------------------------- /apps/web/public/8bitworker/wasm/zmac.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/8bitworker/wasm/zmac.wasm -------------------------------------------------------------------------------- /apps/web/public/android-chrome-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/android-chrome-96x96.png -------------------------------------------------------------------------------- /apps/web/public/tapeloaders/tape_128.szx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/tapeloaders/tape_128.szx -------------------------------------------------------------------------------- /apps/web/public/8bitworker/wasm/sdasz80.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/8bitworker/wasm/sdasz80.wasm -------------------------------------------------------------------------------- /apps/web/public/8bitworker/wasm/sdldz80.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/8bitworker/wasm/sdldz80.wasm -------------------------------------------------------------------------------- /apps/web/public/assets/images/zx-square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/assets/images/zx-square.png -------------------------------------------------------------------------------- /apps/web/public/assets/images/embed-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/assets/images/embed-preview.png -------------------------------------------------------------------------------- /apps/web/public/tapeloaders/tape_128_usr0.szx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/tapeloaders/tape_128_usr0.szx -------------------------------------------------------------------------------- /apps/web/public/tapeloaders/tape_pentagon.szx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/tapeloaders/tape_pentagon.szx -------------------------------------------------------------------------------- /apps/web/public/tapeloaders/tape_pentagon_usr0.szx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stever/zxcode/HEAD/apps/web/public/tapeloaders/tape_pentagon_usr0.szx -------------------------------------------------------------------------------- /apps/web/tsconfig.jsspeccy.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "assemblyscript/std/assembly.json", 3 | "include": [ 4 | "./build/core.ts" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /apps/web/Caddyfile: -------------------------------------------------------------------------------- 1 | :8080 { 2 | route { 3 | root * /srv 4 | try_files {path} {path}/ /index.html 5 | file_server 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .graphqlconfig 3 | .idea/ 4 | .turbo/ 5 | /apps/auth/appsettings.Development.json 6 | /apps/web/es5/ 7 | CLAUDE.md 8 | node_modules/ 9 | plan_* 10 | -------------------------------------------------------------------------------- /apps/auth/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "auth", 3 | "private": true, 4 | "scripts": { 5 | "dev": "dotnet run --project Auth/Auth.csproj" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /apps/auth/Auth/GraphQL/GetUser.graphql: -------------------------------------------------------------------------------- 1 | query GetUser($username: String!) { 2 | user(where: {username: {_eq: $username}}) { 3 | user_id 4 | username 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /apps/proxy/README.md: -------------------------------------------------------------------------------- 1 | # Proxy 2 | 3 | Add a `.env` file with the following content: 4 | 5 | ```bash 6 | HASURA_JWT_DEFAULT_ROLE="zxplay-user" 7 | AUTH_SESSION_JWT_SECRET="" 8 | ``` 9 | -------------------------------------------------------------------------------- /apps/web/src/components/LoadingScreen.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function LoadingScreen() { 4 | return
5 | } 6 | -------------------------------------------------------------------------------- /hasura/config.yaml: -------------------------------------------------------------------------------- 1 | version: 3 2 | endpoint: http://localhost:4000 3 | metadata_directory: metadata 4 | actions: 5 | kind: synchronous 6 | handler_webhook_baseurl: http://localhost:8080 7 | -------------------------------------------------------------------------------- /apps/web/.dockerignore: -------------------------------------------------------------------------------- 1 | *.interp 2 | *.swp 3 | *.tokens 4 | /.graphqlconfig 5 | /.idea/ 6 | /build/ 7 | /es5/ 8 | /node_modules/ 9 | /public/dist/ 10 | /schema.graphql 11 | /typedocs/ 12 | -------------------------------------------------------------------------------- /apps/web/.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | *.interp 3 | *.swp 4 | *.tokens 5 | /.graphqlconfig 6 | /.idea/ 7 | /build/ 8 | /es5/ 9 | /node_modules/ 10 | /public/dist/ 11 | /schema.graphql 12 | /typedocs/ 13 | -------------------------------------------------------------------------------- /apps/web/src/lib/jsspeccy/ui/icons/play.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/auth/Auth/GraphQL/GetUserByEmail.graphql: -------------------------------------------------------------------------------- 1 | query GetUserByEmail($email_address: String!) { 2 | user(where: {email_address: {_eq: $email_address}}) { 3 | user_id 4 | username 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /apps/auth/Auth/Model/Role.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace SteveR.Auth.Model; 4 | 5 | public class Role 6 | { 7 | [JsonProperty("name")] 8 | public string? Name { get; set; } 9 | } 10 | -------------------------------------------------------------------------------- /hasura/metadata/databases/default/tables/public_session.yaml: -------------------------------------------------------------------------------- 1 | table: 2 | name: session 3 | schema: public 4 | object_relationships: 5 | - name: user 6 | using: 7 | foreign_key_constraint_on: user_id 8 | -------------------------------------------------------------------------------- /apps/auth/Auth/Model/UserRoles.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace SteveR.Auth.Model; 4 | 5 | public class UserRoles 6 | { 7 | [JsonProperty("role")] 8 | public Role? Role { get; set; } 9 | } 10 | -------------------------------------------------------------------------------- /apps/proxy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "proxy", 3 | "private": true, 4 | "scripts": { 5 | "dev": "dotenv caddy run" 6 | }, 7 | "devDependencies": { 8 | "dotenv-cli": "3.0.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /apps/web/src/redux/profile/actions.js: -------------------------------------------------------------------------------- 1 | export const actionTypes = { 2 | showProfile: 'profile/showProfile', 3 | }; 4 | 5 | export const showProfile = () => ({ 6 | type: actionTypes.showProfile 7 | }); 8 | -------------------------------------------------------------------------------- /apps/web/src/lib/jsspeccy/ui/icons/pause.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/web/src/lib/jsspeccy/ui/index.js: -------------------------------------------------------------------------------- 1 | export {Menu} from "./Menu"; 2 | export {MenuBar} from "./MenuBar"; 3 | export {Toolbar} from "./Toolbar"; 4 | export {ToolbarButton} from "./ToolbarButton"; 5 | export {UIController} from "./UIController"; 6 | -------------------------------------------------------------------------------- /apps/web/src/redux/window/actions.js: -------------------------------------------------------------------------------- 1 | export const actionTypes = { 2 | resized: 'window/resized', 3 | }; 4 | 5 | export const resized = (width, height) => ({ 6 | type: actionTypes.resized, 7 | width, 8 | height, 9 | }); 10 | -------------------------------------------------------------------------------- /apps/auth/Auth/GraphQL/UpdateUserEmail.graphql: -------------------------------------------------------------------------------- 1 | mutation UpdateUserEmail($username: String!, $email_address: String) { 2 | update_user(where: {username: {_eq: $username}}, _set: {email_address: $email_address}) { 3 | affected_rows 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /apps/auth/Auth/GraphQL/CreateUser.graphql: -------------------------------------------------------------------------------- 1 | mutation CreateUser($username: String!, $email_address: String, $slug: String!) { 2 | insert_user_one(object: {username: $username, email_address: $email_address, slug: $slug}) { 3 | user_id 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /apps/auth/Auth/GraphQL/GetUserRoles.graphql: -------------------------------------------------------------------------------- 1 | query GetUserRoles($user_id: uuid!) { 2 | user(where: {user_id: {_eq: $user_id}}) { 3 | user_roles { 4 | role { 5 | name 6 | } 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /apps/auth/Auth/GraphQL/UpdateSessionTimestamp.graphql: -------------------------------------------------------------------------------- 1 | mutation UpdateSessionTimestamp($session_id: uuid!, $updated: timestamptz!) { 2 | update_session_by_pk(pk_columns: {session_id: $session_id}, _set: {updated: $updated}) { 3 | updated 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /apps/web/src/components/MaxWidth.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function MaxWidth(props) { 4 | return ( 5 |
6 | {props.children} 7 |
8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /hasura/migrations/default/1759630462652_add_avatar_fields/down.sql: -------------------------------------------------------------------------------- 1 | -- Remove avatar fields from user table 2 | DROP INDEX IF EXISTS idx_user_avatar_variant; 3 | 4 | ALTER TABLE public.user 5 | DROP COLUMN IF EXISTS avatar_variant, 6 | DROP COLUMN IF EXISTS custom_avatar_data; -------------------------------------------------------------------------------- /hasura/migrations/default/1759606682000_add_project_display_order/down.sql: -------------------------------------------------------------------------------- 1 | -- Remove the index 2 | DROP INDEX IF EXISTS public.idx_project_display_order; 3 | 4 | -- Remove display_order column from project table 5 | ALTER TABLE public.project 6 | DROP COLUMN IF EXISTS display_order; -------------------------------------------------------------------------------- /apps/web/src/lib/jsspeccy/ui/icons/exitfullscreen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/web/src/lib/jsspeccy/ui/icons/fullscreen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/auth/.gitignore: -------------------------------------------------------------------------------- 1 | /**/*.swp 2 | /**/*.user 3 | /**/.graphqlconfig 4 | /**/.graphqlrc.json 5 | /**/.idea/ 6 | /**/.vs/ 7 | /**/Generated/ 8 | /**/_ReSharper.Caches/ 9 | /**/bin/ 10 | /**/obj/ 11 | /**/schema.extensions.graphql 12 | /**/schema.graphql 13 | /.config/dotnet-tools.json -------------------------------------------------------------------------------- /hasura/metadata/actions.graphql: -------------------------------------------------------------------------------- 1 | type Mutation { 2 | compile( 3 | basic: String! 4 | ): CompileResult 5 | } 6 | 7 | type Mutation { 8 | compileC( 9 | code: String! 10 | ): CompileResult 11 | } 12 | 13 | type CompileResult { 14 | base64_encoded: String! 15 | } 16 | 17 | -------------------------------------------------------------------------------- /apps/web/src/lib/jsspeccy/ui/icons/close.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/web/src/lib/jsspeccy/ui/icons/open.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hasura/metadata/databases/default/tables/public_user_role.yaml: -------------------------------------------------------------------------------- 1 | table: 2 | name: user_role 3 | schema: public 4 | object_relationships: 5 | - name: role 6 | using: 7 | foreign_key_constraint_on: role_id 8 | - name: user 9 | using: 10 | foreign_key_constraint_on: user_id 11 | -------------------------------------------------------------------------------- /hasura/metadata/databases/default/tables/tables.yaml: -------------------------------------------------------------------------------- 1 | - "!include public_project.yaml" 2 | - "!include public_role.yaml" 3 | - "!include public_session.yaml" 4 | - "!include public_text.yaml" 5 | - "!include public_user.yaml" 6 | - "!include public_user_follows.yaml" 7 | - "!include public_user_role.yaml" 8 | -------------------------------------------------------------------------------- /hasura/migrations/default/1759572893000_add_user_profile_fields/down.sql: -------------------------------------------------------------------------------- 1 | -- Rollback: Remove added columns 2 | ALTER TABLE public.project 3 | DROP COLUMN IF EXISTS slug; 4 | 5 | ALTER TABLE public.user 6 | DROP COLUMN IF EXISTS slug, 7 | DROP COLUMN IF EXISTS profile_is_public, 8 | DROP COLUMN IF EXISTS bio; -------------------------------------------------------------------------------- /apps/auth/Auth/GraphQL/CreateSession.graphql: -------------------------------------------------------------------------------- 1 | mutation CreateSession($user_id: uuid!, $auth_token: String!, $created: timestamptz!, $expires: timestamptz!) { 2 | insert_session_one(object: {user_id: $user_id, auth_token: $auth_token, created: $created, expires: $expires}) { 3 | session_id 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /apps/web/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM caddy:2 AS base 2 | COPY apps/web/Caddyfile /etc/caddy/Caddyfile 3 | 4 | FROM node:22 AS npmbuild 5 | WORKDIR /project 6 | COPY . . 7 | RUN npm install --legacy-peer-deps 8 | RUN npm run build 9 | 10 | FROM base AS final 11 | COPY --from=npmbuild /project/apps/web/public /srv 12 | -------------------------------------------------------------------------------- /apps/web/public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #cccccc 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /hasura/metadata/databases/default/tables/public_role.yaml: -------------------------------------------------------------------------------- 1 | table: 2 | name: role 3 | schema: public 4 | array_relationships: 5 | - name: user_roles 6 | using: 7 | foreign_key_constraint_on: 8 | column: role_id 9 | table: 10 | name: user_role 11 | schema: public 12 | -------------------------------------------------------------------------------- /apps/web/src/lib/lang.js: -------------------------------------------------------------------------------- 1 | export function getLanguageLabel(lang) { 2 | const labels = { 3 | asm: "Pasmo", 4 | basic: "zmakebas", 5 | bas2tap: "bas2tap", 6 | c: "z88dk C", 7 | sdcc: "SDCC", 8 | zmac: "zmac", 9 | zxbasic: "Boriel BASIC", 10 | }; 11 | return labels[lang] || lang; 12 | } 13 | -------------------------------------------------------------------------------- /apps/web/src/lib/8bitworker/defs_files.ts: -------------------------------------------------------------------------------- 1 | export type FileData = string | Uint8Array; 2 | 3 | export type FileEntry = { 4 | path: string 5 | encoding: string 6 | data: FileData 7 | ts: number 8 | }; 9 | 10 | export interface WorkingStore { 11 | getFileData(path: string): FileData 12 | } 13 | -------------------------------------------------------------------------------- /apps/web/src/redux/error/actions.js: -------------------------------------------------------------------------------- 1 | export const actionTypes = { 2 | reset: 'error/reset', 3 | error: 'error/error', 4 | }; 5 | 6 | export const reset = () => ({ 7 | type: actionTypes.reset 8 | }); 9 | 10 | export const error = (msg) => ({ 11 | type: actionTypes.error, 12 | msg 13 | }); 14 | -------------------------------------------------------------------------------- /hasura/migrations/default/1759594620000_add_social_features/down.sql: -------------------------------------------------------------------------------- 1 | -- Drop indexes 2 | DROP INDEX IF EXISTS public.idx_user_follows_created_at; 3 | DROP INDEX IF EXISTS public.idx_user_follows_following; 4 | DROP INDEX IF EXISTS public.idx_user_follows_follower; 5 | 6 | -- Drop user_follows table 7 | DROP TABLE IF EXISTS public.user_follows; -------------------------------------------------------------------------------- /apps/web/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ZX", 3 | "short_name": "ZX", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-96x96.png", 7 | "sizes": "96x96", 8 | "type": "image/png" 9 | } 10 | ], 11 | "theme_color": "#ffffff", 12 | "background_color": "#ffffff", 13 | "display": "standalone" 14 | } 15 | -------------------------------------------------------------------------------- /apps/auth/Auth/GraphQL/GetSession.graphql: -------------------------------------------------------------------------------- 1 | query GetSession($auth_token: String!) { 2 | session(where: {auth_token: {_eq: $auth_token}}) { 3 | session_id 4 | expires 5 | user { 6 | user_id 7 | user_roles { 8 | role { 9 | name 10 | } 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /apps/web/src/lib/8bitworker/shared_vars.ts: -------------------------------------------------------------------------------- 1 | declare function importScripts(path: string); 2 | 3 | const ENVIRONMENT_IS_WEB = typeof window === 'object'; 4 | const ENVIRONMENT_IS_WORKER = typeof importScripts === 'function'; 5 | export const emglobal: any = ENVIRONMENT_IS_WORKER ? self : ENVIRONMENT_IS_WEB ? window : global; 6 | 7 | export const WORKER_RELATIVE_PATH = "../8bitworker/"; 8 | -------------------------------------------------------------------------------- /apps/web/src/redux/identity/actions.js: -------------------------------------------------------------------------------- 1 | export const actionTypes = { 2 | getUserInfo: 'identity/getUserInfo', 3 | setUserInfo: 'identity/setUserInfo', 4 | }; 5 | 6 | export const getUserInfo = () => ({ 7 | type: actionTypes.getUserInfo 8 | }); 9 | 10 | export const setUserInfo = (userInfo) => ({ 11 | type: actionTypes.setUserInfo, 12 | userInfo 13 | }); 14 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turborepo.org/schema.json", 3 | "tasks": { 4 | "build": { 5 | "dependsOn": ["^build"], 6 | "outputs": ["dist/**"], 7 | "cache": false 8 | }, 9 | "lint": { 10 | "outputs": [] 11 | }, 12 | "test": { 13 | "outputs": [] 14 | }, 15 | "dev": { 16 | "cache": false 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /apps/auth/Auth/Model/Session.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace SteveR.Auth.Model; 4 | 5 | public class Session 6 | { 7 | [JsonProperty("session_id")] 8 | public string? SessionId { get; set; } 9 | 10 | [JsonProperty("expires")] 11 | public string? Expires { get; set; } 12 | 13 | [JsonProperty("user")] 14 | public User? User { get; set; } 15 | } 16 | -------------------------------------------------------------------------------- /apps/auth/Auth/Model/User.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace SteveR.Auth.Model; 4 | 5 | public class User 6 | { 7 | [JsonProperty("user_id")] 8 | public string? UserId { get; set; } 9 | 10 | [JsonProperty("username")] 11 | public string? Username { get; set; } 12 | 13 | [JsonProperty("user_roles")] 14 | public UserRoles[]? UserRoles { get; set; } 15 | } 16 | -------------------------------------------------------------------------------- /.env-dist: -------------------------------------------------------------------------------- 1 | POSTGRES_PASSWORD=postgrespassword 2 | POSTGRES_USER=zxplay 3 | POSTGRES_DB=zxplay 4 | 5 | HASURA_DATABASE_URL=postgres://zxplay:postgrespassword@postgres:5432/zxplay 6 | HASURA_ADMIN_SECRET=hasurapassword 7 | HASURA_JWT_SECRET=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 8 | HASURA_JWT_AUDIENCE=hasura 9 | HASURA_JWT_ISSUER=zxplay 10 | HASURA_ENABLE_CONSOLE=true 11 | HASURA_GRAPHQL_DEV_MODE=true 12 | -------------------------------------------------------------------------------- /apps/web/src/components/RequireSubscriber.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {useSelector} from "react-redux"; 3 | 4 | export default function RequireSubscriber({children}) { 5 | const received = useSelector(state => state?.subscriber.subscribeFunctionReceived); 6 | 7 | if (received) { 8 | return <>{children} 9 | } 10 | 11 | return <> 12 | } 13 | -------------------------------------------------------------------------------- /apps/web/src/lib/jsspeccy/tape/segments/PulseSequenceSegment.js: -------------------------------------------------------------------------------- 1 | export class PulseSequenceSegment { 2 | constructor(pulses) { 3 | this.pulses = pulses; 4 | this.index = 0; 5 | } 6 | isFinished() { 7 | return this.index == this.pulses.length; 8 | } 9 | getNextPulseLength() { 10 | return this.pulses[this.index++]; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /apps/web/src/lib/jsspeccy/ui/icons/reset.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hasura/metadata/databases/default/tables/public_text.yaml: -------------------------------------------------------------------------------- 1 | table: 2 | name: text 3 | schema: public 4 | select_permissions: 5 | - role: public 6 | permission: 7 | columns: 8 | - lang 9 | - name 10 | - text 11 | filter: {} 12 | - role: zxplay-user 13 | permission: 14 | columns: 15 | - lang 16 | - name 17 | - text 18 | filter: {} 19 | -------------------------------------------------------------------------------- /apps/auth/.dockerignore: -------------------------------------------------------------------------------- 1 | /**/*.swp 2 | /**/*.user 3 | /**/.dockerignore 4 | /**/.graphqlconfig 5 | /**/.graphqlrc.json 6 | /**/.idea/ 7 | /**/.vs/ 8 | /**/_ReSharper.Caches/ 9 | /**/appsettings.Development.json 10 | /**/bin/ 11 | /**/Dockerfile 12 | /**/Generated/ 13 | /**/local.settings.json 14 | /**/mono_crash.mem.* 15 | /**/obj/ 16 | /**/schema.extensions.graphql 17 | /**/schema.graphql 18 | /**/~$* 19 | /.config/dotnet-tools.json -------------------------------------------------------------------------------- /hasura/migrations/default/1759572916000_add_slug_constraints/down.sql: -------------------------------------------------------------------------------- 1 | -- Rollback: Remove constraints and indexes 2 | DROP INDEX IF EXISTS project_is_public_idx; 3 | DROP INDEX IF EXISTS user_profile_is_public_idx; 4 | DROP INDEX IF EXISTS project_owner_slug_key; 5 | DROP INDEX IF EXISTS user_slug_key; 6 | 7 | ALTER TABLE public.project 8 | ALTER COLUMN slug DROP NOT NULL; 9 | 10 | ALTER TABLE public.user 11 | ALTER COLUMN slug DROP NOT NULL; -------------------------------------------------------------------------------- /apps/gif-service/src/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import tapeRoutes from './routes/tape.js'; 3 | 4 | const app = express(); 5 | const PORT = process.env.PORT || 5001; 6 | 7 | app.use(express.json()); 8 | app.use('/api', tapeRoutes); 9 | 10 | app.get('/health', (req, res) => { 11 | res.json({ status: 'ok' }); 12 | }); 13 | 14 | app.listen(PORT, () => { 15 | console.log(`GIF service listening on port ${PORT}`); 16 | }); 17 | -------------------------------------------------------------------------------- /apps/auth/Auth/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "DebugAutoLoginUsername": "dev", 3 | "AuthRedirect": "http://localhost:8080/", 4 | "GraphQL": { 5 | "Endpoint": "http://localhost:8080/api/v1/graphql", 6 | "AdminSecret": "hasurapassword" 7 | }, 8 | "JWT": { 9 | "SessionToken": { 10 | "Secret": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" 11 | }, 12 | "HasuraToken": { 13 | "Secret": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /apps/auth/Auth/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "Auth": { 5 | "commandName": "Project", 6 | "dotnetRunMessages": true, 7 | "launchBrowser": true, 8 | "launchUrl": "swagger", 9 | "applicationUrl": "http://localhost:5000", 10 | "environmentVariables": { 11 | "ASPNETCORE_ENVIRONMENT": "Development" 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /apps/web/src/lib/jsspeccy/tape/segments/PauseSegment.js: -------------------------------------------------------------------------------- 1 | export class PauseSegment { 2 | constructor(duration) { 3 | this.duration = duration; 4 | this.emitted = false; 5 | } 6 | isFinished() { 7 | return this.emitted; 8 | } 9 | getNextPulseLength() { 10 | // TODO: take level back down to 0 after 1ms if it's currently high 11 | this.emitted = true; 12 | return this.duration * 3500; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /hasura/metadata/databases/databases.yaml: -------------------------------------------------------------------------------- 1 | - name: default 2 | kind: postgres 3 | configuration: 4 | connection_info: 5 | database_url: 6 | from_env: HASURA_GRAPHQL_DATABASE_URL 7 | isolation_level: read-committed 8 | pool_settings: 9 | connection_lifetime: 600 10 | idle_timeout: 180 11 | max_connections: 40 12 | retries: 1 13 | use_prepared_statements: true 14 | tables: "!include default/tables/tables.yaml" 15 | -------------------------------------------------------------------------------- /apps/web/src/lib/jsspeccy/tape/segments/ToneSegment.js: -------------------------------------------------------------------------------- 1 | export class ToneSegment { 2 | constructor(pulseLength, pulseCount) { 3 | this.pulseLength = pulseLength; 4 | this.pulseCount = pulseCount; 5 | this.pulsesGenerated = 0; 6 | } 7 | isFinished() { 8 | return this.pulsesGenerated == this.pulseCount; 9 | } 10 | getNextPulseLength() { 11 | this.pulsesGenerated++; 12 | return this.pulseLength; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /apps/gif-service/src/gif-encoder-2.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'gif-encoder-2' { 2 | export default class GIFEncoder { 3 | constructor(width: number, height: number, algorithm?: string); 4 | setDelay(ms: number): void; 5 | setRepeat(repeat: number): void; 6 | setQuality(quality: number): void; 7 | start(): void; 8 | addFrame(data: Uint8Array | Uint8ClampedArray): void; 9 | finish(): void; 10 | out: { getData(): Uint8Array }; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /apps/web/src/components/RenderEmulator.jsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect} from "react"; 2 | import {useDispatch} from "react-redux"; 3 | import {exit, renderEmulator} from "../redux/jsspeccy/actions"; 4 | 5 | export default function RenderEmulator() { 6 | const dispatch = useDispatch(); 7 | 8 | // NOTE: Using simple component function so emulator is rendered early. 9 | 10 | useEffect(() => { 11 | dispatch(renderEmulator(2)); 12 | return () => {dispatch(exit())} 13 | }, []); 14 | 15 | return <> 16 | } 17 | -------------------------------------------------------------------------------- /apps/web/src/dashboard_lock.js: -------------------------------------------------------------------------------- 1 | export function dashboardLock() { 2 | const elems = document.getElementsByClassName('dashboard-lock-screen'); 3 | for (let i = 0; i < elems.length; i++) { 4 | const elem = elems[i]; 5 | elem.style.display = ''; 6 | } 7 | } 8 | 9 | export function dashboardUnlock() { 10 | const elems = document.getElementsByClassName('dashboard-lock-screen'); 11 | for (let i = 0; i < elems.length; i++) { 12 | const elem = elems[i]; 13 | elem.style.display = 'none'; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /apps/web/asconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "targets": { 3 | "debug": { 4 | "binaryFile": "public/dist/jsspeccy-core.wasm", 5 | "textFile": "build/core.wat", 6 | "sourceMap": true, 7 | "debug": true 8 | }, 9 | "release": { 10 | "binaryFile": "public/dist/jsspeccy-core.wasm", 11 | "textFile": "build/core.wat", 12 | "sourceMap": true, 13 | "optimizeLevel": 3, 14 | "shrinkLevel": 0, 15 | "converge": false, 16 | "noAssert": false 17 | } 18 | }, 19 | "options": { 20 | "memoryBase": 589824 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /hasura/metadata/actions.yaml: -------------------------------------------------------------------------------- 1 | actions: 2 | - name: compile 3 | definition: 4 | kind: synchronous 5 | handler: http://zxbasic/compile/ 6 | permissions: 7 | - role: zxplay-user 8 | - role: public 9 | comment: ZX Basic Compile 10 | - name: compileC 11 | definition: 12 | kind: synchronous 13 | handler: http://z88dk/compile/ 14 | permissions: 15 | - role: public 16 | - role: zxplay-user 17 | custom_types: 18 | enums: [] 19 | input_objects: [] 20 | objects: 21 | - name: CompileResult 22 | scalars: [] 23 | -------------------------------------------------------------------------------- /apps/web/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import {Provider as ReduxProvider} from "react-redux"; 4 | import {ReduxRouter as Router} from "@lagunovsky/redux-react-router"; 5 | import {store, history} from "./redux/store"; 6 | import App from "./components/App"; 7 | 8 | const root = ReactDOM.createRoot(document.getElementById('root')); 9 | root.render( 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | -------------------------------------------------------------------------------- /apps/auth/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base 2 | WORKDIR /app 3 | EXPOSE 80 4 | EXPOSE 443 5 | 6 | FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build 7 | WORKDIR /src 8 | COPY ["Auth/Auth.csproj", "Auth/"] 9 | RUN dotnet restore "Auth/Auth.csproj" 10 | COPY . . 11 | WORKDIR "/src/Auth" 12 | RUN dotnet build "Auth.csproj" -c Release -o /app/build 13 | 14 | FROM build AS publish 15 | RUN dotnet publish "Auth.csproj" -c Release -o /app/publish 16 | 17 | FROM base AS final 18 | WORKDIR /app 19 | COPY --from=publish /app/publish . 20 | ENTRYPOINT ["dotnet", "Auth.dll"] 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zxcoder", 3 | "private": true, 4 | "packageManager": "npm@11.2.0", 5 | "scripts": { 6 | "dev": "turbo run dev", 7 | "test": "turbo run test", 8 | "lint": "turbo run lint", 9 | "build": "turbo run build" 10 | }, 11 | "workspaces": [ 12 | "packages/*", 13 | "apps/*" 14 | ], 15 | "devDependencies": { 16 | "nodemon": "^3.1.10", 17 | "turbo": "^2.5.4" 18 | }, 19 | "dependencies": { 20 | "@dnd-kit/core": "^6.3.1", 21 | "@dnd-kit/sortable": "^10.0.0", 22 | "@dnd-kit/utilities": "^3.2.2" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /apps/web/src/components/ErrorNotFoundPage.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {Card} from "primereact/card"; 3 | import {Titled} from "react-titled"; 4 | import {sep} from "../constants"; 5 | 6 | export default function ErrorNotFoundPage() { 7 | return ( 8 | `Not Found ${sep} ${s}`}> 9 | 10 |

Not Found

11 |

12 | Requested page location not found. 13 |

14 |
15 |
16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /apps/gif-service/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "ESNext", 5 | "lib": ["ES2022", "WebWorker"], 6 | "moduleResolution": "node", 7 | "outDir": "./dist", 8 | "rootDir": "./src", 9 | "strict": false, 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "resolveJsonModule": true, 14 | "declaration": false, 15 | "declarationMap": false, 16 | "sourceMap": true, 17 | "types": ["node"] 18 | }, 19 | "include": ["src/**/*"], 20 | "exclude": ["node_modules", "dist"] 21 | } 22 | -------------------------------------------------------------------------------- /apps/auth/Auth/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "AllowedHosts": "*", 3 | "SAML": { 4 | "AppId": "zxplay", 5 | "DefaultExpirationMinutes": 480, 6 | "AdmitNewUsers": true, 7 | "AuthCookieName": "access_token", 8 | "ReturnUrlCookieName": "redirect_url" 9 | }, 10 | "JWT": { 11 | "DefaultRole": "zxplay-user", 12 | "AddDefaultRole": true, 13 | "SessionToken": { 14 | "ExpirationSeconds": 28800, 15 | "Issuer": "zxplay", 16 | "Audience": "caddy" 17 | }, 18 | "HasuraToken": { 19 | "ExpirationSeconds": 900, 20 | "Issuer": "zxplay", 21 | "Audience": "hasura" 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /hasura/migrations/default/1759630462652_add_avatar_fields/up.sql: -------------------------------------------------------------------------------- 1 | -- Add avatar fields to user table 2 | ALTER TABLE public.user 3 | ADD COLUMN IF NOT EXISTS avatar_variant integer DEFAULT 0, 4 | ADD COLUMN IF NOT EXISTS custom_avatar_data jsonb; 5 | 6 | -- Add comments for documentation 7 | COMMENT ON COLUMN public.user.avatar_variant IS 'Avatar variant number (0-199) or special value for custom avatar'; 8 | COMMENT ON COLUMN public.user.custom_avatar_data IS 'Custom pixel art avatar data as 8x8 grid of color indices'; 9 | 10 | -- Create index on avatar_variant for potential future filtering 11 | CREATE INDEX IF NOT EXISTS idx_user_avatar_variant ON public.user(avatar_variant); -------------------------------------------------------------------------------- /apps/web/src/dashboard_loading.js: -------------------------------------------------------------------------------- 1 | export function showLoading() { 2 | const elems = document.getElementsByClassName('dashboard-loading-screen'); 3 | for (let i = 0; i < elems.length; i++) { 4 | const elem = elems[i]; 5 | elem.style.visibility = 'visible'; 6 | elem.style.opacity = '0.5'; 7 | } 8 | } 9 | 10 | export function hideLoading() { 11 | const elems = document.getElementsByClassName('dashboard-loading-screen'); 12 | for (let i = 0; i < elems.length; i++) { 13 | const elem = elems[i]; 14 | elem.style.opacity = '0'; 15 | setTimeout(() => elem.style.visibility = '', 400); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /apps/gif-service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gif-service", 3 | "version": "1.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "tsx src/index.ts", 8 | "build": "echo 'Build not needed - using tsx'", 9 | "start": "tsx src/index.ts", 10 | "test": "echo 'Tests not implemented yet'" 11 | }, 12 | "dependencies": { 13 | "express": "^4.21.2", 14 | "gif-encoder-2": "^1.0.5", 15 | "multer": "^1.4.5-lts.1" 16 | }, 17 | "devDependencies": { 18 | "@types/express": "^5.0.0", 19 | "@types/multer": "^1.4.12", 20 | "@types/node": "^22.10.5", 21 | "tsx": "^4.19.2", 22 | "typescript": "^5.7.3" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /apps/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src/lib/8bitworker/**/*.ts" 4 | ], 5 | "compilerOptions": { 6 | "outDir": "build", 7 | "allowJs": false, 8 | "checkJs": false, 9 | "sourceMap": true, 10 | "target": "es2017", 11 | "lib": [ 12 | "es2017", 13 | "dom" 14 | ], 15 | "module": "CommonJS", 16 | "esModuleInterop": true, 17 | "isolatedModules": true, 18 | "noImplicitThis": false, 19 | "noImplicitAny": false, 20 | "preserveConstEnums": true, 21 | "alwaysStrict": true, 22 | "strictNullChecks": false, 23 | "experimentalDecorators": true, 24 | "emitDecoratorMetadata": true, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /apps/web/src/lib/canvasgui/ImageManager.js: -------------------------------------------------------------------------------- 1 | export class ImageManager { 2 | constructor(win) { 3 | this.imgtable = {}; 4 | this.win = win; 5 | } 6 | 7 | imageForPath(path) { 8 | if (path in this.imgtable) { 9 | return this.imgtable[path]; 10 | } 11 | 12 | const img = new Image(); 13 | img.src = path; 14 | let that = this; 15 | 16 | img.onload = function (e) { 17 | that.imgtable[path] = img; 18 | if (that.win.ctx == null) return; 19 | that.win.requestRedraw(); 20 | that.win.redrawIfRequested(); 21 | } 22 | 23 | return img; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /apps/web/src/lib/canvasgui/Widget.js: -------------------------------------------------------------------------------- 1 | export class Widget { 2 | constructor(parent, x, y, w, h) { 3 | this.parent = parent; 4 | this.x = x; 5 | this.y = y; 6 | this.w = w; 7 | this.h = h; 8 | 9 | if (this.parent) 10 | this.parent._add(this); 11 | } 12 | 13 | requestRedraw() { 14 | if (this.parent != null) 15 | this.parent.requestRedraw(); 16 | } 17 | 18 | isPointerInside(x, y) { 19 | if (x < this.x) return false; 20 | if (y < this.y) return false; 21 | if (x > this.x + this.w) return false; 22 | if (y > this.y + this.h) return false; 23 | return true; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /apps/web/src/components/ErrorBoundary.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {store} from "../redux/store"; 3 | import {error} from "../redux/error/actions"; 4 | 5 | export default class ErrorBoundary extends React.Component { 6 | 7 | state = {hasError: false}; 8 | 9 | static getDerivedStateFromError(_) { 10 | return {hasError: true}; 11 | } 12 | 13 | componentDidCatch(error, errorInfo) { 14 | console.error(error, errorInfo); 15 | } 16 | 17 | render() { 18 | if (this.state.hasError) { 19 | store.dispatch(error('There was a problem in rendering this content.')); 20 | return <> 21 | } 22 | 23 | return this.props.children; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /apps/web/src/lib/canvasgui/index.js: -------------------------------------------------------------------------------- 1 | export {ImageButton} from "./ImageButton"; 2 | export {TextButton} from "./TextButton"; 3 | export {Control} from "./Control"; 4 | export {SingleWindow} from "./SingleWindow"; 5 | export {ImageManager} from "./ImageManager"; 6 | export {Group} from "./Group"; 7 | export {Widget} from "./Widget"; 8 | 9 | CanvasRenderingContext2D.prototype.roundRect = function (x, y, w, h, r) { 10 | if (w < 2 * r) r = w / 2; 11 | if (h < 2 * r) r = h / 2; 12 | this.beginPath(); 13 | this.moveTo(x + r, y); 14 | this.arcTo(x + w, y, x + w, y + h, r); 15 | this.arcTo(x + w, y + h, x, y + h, r); 16 | this.arcTo(x, y + h, x, y, r); 17 | this.arcTo(x, y, x + w, y, r); 18 | this.closePath(); 19 | return this; 20 | } 21 | -------------------------------------------------------------------------------- /hasura/migrations/default/1759606682000_add_project_display_order/up.sql: -------------------------------------------------------------------------------- 1 | -- Add display_order column to project table for custom ordering 2 | ALTER TABLE public.project 3 | ADD COLUMN display_order integer DEFAULT 0; 4 | 5 | -- Create an index on display_order for efficient sorting 6 | CREATE INDEX idx_project_display_order ON public.project(owner_user_id, display_order); 7 | 8 | -- Update existing projects with initial display order based on created_at 9 | WITH numbered_projects AS ( 10 | SELECT 11 | project_id, 12 | ROW_NUMBER() OVER (PARTITION BY owner_user_id ORDER BY created_at DESC) - 1 as new_order 13 | FROM public.project 14 | ) 15 | UPDATE public.project p 16 | SET display_order = np.new_order 17 | FROM numbered_projects np 18 | WHERE p.project_id = np.project_id; -------------------------------------------------------------------------------- /apps/web/src/components/YourProjectsPage.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {useParams} from "react-router-dom"; 3 | import {Card} from "primereact/card"; 4 | import ProjectList from "./ProjectList"; 5 | import RequireSubscriber from "./RequireSubscriber"; 6 | import {Titled} from "react-titled"; 7 | import {sep} from "../constants"; 8 | 9 | export default function YourProjectsPage() { 10 | const {id} = useParams(); 11 | 12 | return ( 13 | `Your Projects ${sep} ${s}`}> 14 | 15 |

Your Projects

16 | 17 | 18 | 19 |
20 |
21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /apps/web/src/components/YourProfilePage.jsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect} from "react"; 2 | import {useDispatch} from "react-redux"; 3 | import {useParams} from "react-router-dom"; 4 | import {Card} from "primereact/card"; 5 | import {Titled} from "react-titled"; 6 | import {sep} from "../constants"; 7 | 8 | export default function YourProfilePage() { 9 | const {id} = useParams(); 10 | 11 | const dispatch = useDispatch(); 12 | 13 | useEffect(() => { 14 | // dispatch(loadProfile(id)); 15 | // return () => {} 16 | }, [id]); 17 | 18 | return ( 19 | `Your Profile ${sep} ${s}`}> 20 | 21 |

Your Profile

22 |
23 |
24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /apps/web/src/constants.js: -------------------------------------------------------------------------------- 1 | // NOTE: Some constants provided by webpack & defined in webpack.config.js files. 2 | const stagingEnv = STAGING_ENV; 3 | const authBase = AUTH_BASE; 4 | const hostname = HOSTNAME; 5 | const httpProtocol = HTTP_PROTO; 6 | 7 | const isDev = stagingEnv !== 'prod'; 8 | const devLogging = false; 9 | 10 | const hasuraBase = `${hostname}/api`; 11 | 12 | export default { 13 | authBase, 14 | enableBoriel: isDev, 15 | enableZ88dk: isDev, 16 | graphQlEndpoint: `${httpProtocol}://${hasuraBase}/v1/graphql`, 17 | graphQlSubscriptionEndpoint: `${httpProtocol === 'https' ? 'wss' : 'ws'}://${hasuraBase}/v1/graphql`, 18 | isDev, 19 | logActions: isDev && devLogging, 20 | logSubs: isDev && devLogging, 21 | stagingEnv, 22 | } 23 | 24 | export const sep = '|'; 25 | -------------------------------------------------------------------------------- /apps/web/src/lib/jsspeccy/ui/icons/tape_play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /apps/web/src/components/LineNumbersToggle.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useDispatch, useSelector } from "react-redux"; 3 | import { InputSwitch } from "primereact/inputswitch"; 4 | import { toggleLineNumbers } from "../redux/app/actions"; 5 | 6 | export default function LineNumbersToggle() { 7 | const dispatch = useDispatch(); 8 | const lineNumbers = useSelector((state) => state?.app?.lineNumbers || false); 9 | 10 | const handleToggle = (value) => { 11 | dispatch(toggleLineNumbers(value)); 12 | }; 13 | 14 | return ( 15 |
16 | 17 | Line Numbers 18 | handleToggle(e.value)} 21 | /> 22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /apps/web/src/components/Emulator.jsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect} from "react"; 2 | import PropTypes from "prop-types"; 3 | import {useDispatch} from "react-redux"; 4 | import {Keyboard} from "./Keyboard"; 5 | import {loadEmulator} from "../redux/jsspeccy/actions"; 6 | 7 | Emulator.propTypes = { 8 | zoom: PropTypes.number, 9 | width: PropTypes.number 10 | } 11 | 12 | export function Emulator(props) { 13 | const dispatch = useDispatch(); 14 | 15 | const zoom = props.zoom || 3; 16 | const width = props.width || zoom * 320; 17 | 18 | useEffect(() => { 19 | const elem = document.getElementById('jsspeccy-screen'); 20 | dispatch(loadEmulator(elem)); 21 | }, []); 22 | 23 | return ( 24 | <> 25 |
26 | 27 | 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /apps/web/src/redux/demo/actions.js: -------------------------------------------------------------------------------- 1 | export const actionTypes = { 2 | setSelectedTabIndex: 'demo/setSelectedTabIndex', 3 | setAssemblyCode: 'demo/setAssemblyCode', 4 | setSinclairBasicCode: 'demo/setSinclairBasicCode', 5 | runAssembly: 'demo/runAssembly', 6 | runSinclairBasic: 'demo/runSinclairBasic', 7 | }; 8 | 9 | export const setSelectedTabIndex = (index) => ({ 10 | type: actionTypes.setSelectedTabIndex, 11 | index 12 | }) 13 | 14 | export const setAssemblyCode = (code) => ({ 15 | type: actionTypes.setAssemblyCode, 16 | code 17 | }); 18 | 19 | export const setSinclairBasicCode = (code) => ({ 20 | type: actionTypes.setSinclairBasicCode, 21 | code 22 | }); 23 | 24 | export const runAssembly = () => ({ 25 | type: actionTypes.runAssembly 26 | }); 27 | 28 | export const runSinclairBasic = () => ({ 29 | type: actionTypes.runSinclairBasic 30 | }); 31 | -------------------------------------------------------------------------------- /apps/web/src/lib/jsspeccy/ui/icons/tape_pause.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /apps/web/src/lib/8bitworker/shared_funcs.ts: -------------------------------------------------------------------------------- 1 | import {WorkerError, WorkerErrorResult} from "./defs_build_result"; 2 | 3 | export function errorResult(msg: string): WorkerErrorResult { 4 | return {errors: [{line: 0, msg: msg}]}; 5 | } 6 | 7 | export function print_fn(s: string) { 8 | console.log(s); 9 | } 10 | 11 | export function makeErrorMatcher( 12 | errors: WorkerError[], 13 | regex, 14 | iline: number, 15 | imsg: number, 16 | mainpath: string, 17 | ifilename?: number) { 18 | 19 | return function (s) { 20 | const matches = regex.exec(s); 21 | if (matches) { 22 | errors.push({ 23 | line: parseInt(matches[iline]) || 1, 24 | msg: matches[imsg], 25 | path: ifilename ? matches[ifilename] : mainpath 26 | }); 27 | } else { 28 | console.log("??? " + s); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /apps/web/src/lib/jsspeccy/ui/ToolbarButton.js: -------------------------------------------------------------------------------- 1 | export class ToolbarButton { 2 | constructor(icon, opts, onClick) { 3 | this.elem = document.createElement('button'); 4 | this.elem.style.margin = '2px'; 5 | this.setIcon(icon); 6 | if (opts.label) this.setLabel(opts.label); 7 | this.elem.addEventListener('click', onClick); 8 | } 9 | 10 | setIcon(icon) { 11 | this.elem.innerHTML = icon; 12 | this.elem.firstChild.style.height = '20px'; 13 | this.elem.firstChild.style.verticalAlign = 'middle'; 14 | } 15 | 16 | setLabel(label) { 17 | this.elem.title = label; 18 | } 19 | 20 | disable() { 21 | this.elem.disabled = true; 22 | this.elem.firstChild.style.opacity = '0.5'; 23 | } 24 | 25 | enable() { 26 | this.elem.disabled = false; 27 | this.elem.firstChild.style.opacity = '1'; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /apps/web/src/components/TermsOfUsePage.jsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect} from "react"; 2 | import {useDispatch, useSelector} from "react-redux"; 3 | import ReactMarkdown from "react-markdown"; 4 | import {Titled} from "react-titled"; 5 | import {Card} from "primereact/card"; 6 | import {requestTermsOfUse} from "../redux/app/actions"; 7 | import {sep} from "../constants"; 8 | 9 | export default function InfoLegacyTerms() { 10 | const dispatch = useDispatch(); 11 | 12 | const text = useSelector(state => state?.app.termsOfUse); 13 | 14 | useEffect(() => { 15 | if (!text) { 16 | dispatch(requestTermsOfUse()); 17 | } 18 | }, []); 19 | 20 | return ( 21 | `Terms of Use ${sep} ${s}`}> 22 | 23 | 24 | {text} 25 | 26 | 27 | 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /apps/web/src/components/PrivacyPolicyPage.jsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect} from "react"; 2 | import {useDispatch, useSelector} from "react-redux"; 3 | import ReactMarkdown from "react-markdown"; 4 | import {Titled} from "react-titled"; 5 | import {Card} from "primereact/card"; 6 | import {requestPrivacyPolicy} from "../redux/app/actions"; 7 | import {sep} from "../constants"; 8 | 9 | export default function PrivacyPolicyPage() { 10 | const dispatch = useDispatch(); 11 | 12 | const text = useSelector(state => state?.app.privacyPolicy); 13 | 14 | useEffect(() => { 15 | if (!text) { 16 | dispatch(requestPrivacyPolicy()); 17 | } 18 | }, []); 19 | 20 | return ( 21 | `Privacy Policy ${sep} ${s}`}> 22 | 23 | 24 | {text} 25 | 26 | 27 | 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /hasura/migrations/default/1759572916000_add_slug_constraints/up.sql: -------------------------------------------------------------------------------- 1 | -- Add NOT NULL constraints and unique indexes for slugs 2 | -- This is a separate migration to ensure data is populated first 3 | 4 | -- Make user slug required and unique 5 | ALTER TABLE public.user 6 | ALTER COLUMN slug SET NOT NULL; 7 | 8 | CREATE UNIQUE INDEX IF NOT EXISTS user_slug_key 9 | ON public.user(slug); 10 | 11 | -- Make project slug required 12 | ALTER TABLE public.project 13 | ALTER COLUMN slug SET NOT NULL; 14 | 15 | -- Create unique index for project slugs (unique per user) 16 | CREATE UNIQUE INDEX IF NOT EXISTS project_owner_slug_key 17 | ON public.project(owner_user_id, slug); 18 | 19 | -- Add index for profile visibility queries 20 | CREATE INDEX IF NOT EXISTS user_profile_is_public_idx 21 | ON public.user(profile_is_public) 22 | WHERE profile_is_public = true; 23 | 24 | -- Add index for public project queries 25 | CREATE INDEX IF NOT EXISTS project_is_public_idx 26 | ON public.project(is_public) 27 | WHERE is_public = true; -------------------------------------------------------------------------------- /apps/web/src/redux/subscriber/actions.js: -------------------------------------------------------------------------------- 1 | export const actionTypes = { 2 | connectClient: 'subscriber/connectClient', 3 | notifySubscribeFunctionReceived: 'subscriber/notifySubscribeFunctionReceived', 4 | subscribeAction: 'subscriber/subscribeAction', 5 | unsubscribeAction: 'subscriber/unsubscribeAction', 6 | subscribe: 'subscriber/subscribe', 7 | }; 8 | 9 | export const connectClient = () => ({ 10 | type: actionTypes.connectClient 11 | }); 12 | 13 | export const notifySubscribeFunctionReceived = () => ({ 14 | type: actionTypes.notifySubscribeFunctionReceived 15 | }); 16 | 17 | export const subscribeAction = (action) => ({ 18 | type: actionTypes.subscribeAction, 19 | action 20 | }); 21 | 22 | export const unsubscribeAction = (action) => ({ 23 | type: actionTypes.unsubscribeAction, 24 | action 25 | }); 26 | 27 | export const subscribe = (action, query, variables, callback) => ({ 28 | type: actionTypes.subscribe, 29 | action, query, variables, callback 30 | }); 31 | -------------------------------------------------------------------------------- /apps/web/public/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /hasura/metadata/databases/default/tables/public_user_follows.yaml: -------------------------------------------------------------------------------- 1 | table: 2 | name: user_follows 3 | schema: public 4 | object_relationships: 5 | - name: follower 6 | using: 7 | foreign_key_constraint_on: follower_id 8 | - name: following 9 | using: 10 | foreign_key_constraint_on: following_id 11 | insert_permissions: 12 | - role: zxplay-user 13 | permission: 14 | check: 15 | follower_id: 16 | _eq: X-Hasura-User-Id 17 | columns: 18 | - follower_id 19 | - following_id 20 | select_permissions: 21 | - role: public 22 | permission: 23 | columns: 24 | - follower_id 25 | - following_id 26 | - created_at 27 | filter: {} 28 | allow_aggregations: true 29 | - role: zxplay-user 30 | permission: 31 | columns: 32 | - follower_id 33 | - following_id 34 | - created_at 35 | filter: {} 36 | allow_aggregations: true 37 | delete_permissions: 38 | - role: zxplay-user 39 | permission: 40 | filter: 41 | follower_id: 42 | _eq: X-Hasura-User-Id 43 | -------------------------------------------------------------------------------- /apps/web/src/lib/jsspeccy/tape/segments/DataSegment.js: -------------------------------------------------------------------------------- 1 | export class DataSegment { 2 | constructor(data, zeroPulseLength, onePulseLength, lastByteBits) { 3 | this.data = data; 4 | this.zeroPulseLength = zeroPulseLength; 5 | this.onePulseLength = onePulseLength; 6 | this.bitCount = (this.data.length - 1) * 8 + lastByteBits; 7 | this.pulsesOutput = 0; 8 | this.lastPulseLength = null; 9 | } 10 | isFinished() { 11 | return this.pulsesOutput == this.bitCount * 2; 12 | } 13 | getNextPulseLength() { 14 | if (this.pulsesOutput & 0x01) { 15 | this.pulsesOutput++; 16 | return this.lastPulseLength; 17 | } else { 18 | const bitIndex = this.pulsesOutput >> 1; 19 | const byteIndex = bitIndex >> 3; 20 | const bitMask = 1 << (7 - (bitIndex & 0x07)); 21 | this.lastPulseLength = (this.data[byteIndex] & bitMask) ? this.onePulseLength : this.zeroPulseLength; 22 | this.pulsesOutput++; 23 | return this.lastPulseLength; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /apps/auth/Auth/Tokens/SessionTokenDispenser.cs: -------------------------------------------------------------------------------- 1 | using System.IdentityModel.Tokens.Jwt; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Serilog; 4 | using SteveR.Auth.Repositories; 5 | 6 | namespace SteveR.Auth.Tokens; 7 | 8 | internal class SessionTokenDispenser : TokenDispenser 9 | { 10 | public SessionTokenDispenser(IConfiguration configuration) 11 | : base(configuration, "JWT:SessionToken:") 12 | { 13 | 14 | } 15 | 16 | public async Task GenerateAccessToken( 17 | string authToken, string userId, [FromServices] UserRepository userRepository) 18 | { 19 | var token = GetJwtSecurityToken(); 20 | var roles = await userRepository.GetRoles(userId); 21 | 22 | token.Payload["roles"] = roles; 23 | 24 | token.Payload["props"] = new Dictionary 25 | { 26 | ["auth"] = authToken 27 | }; 28 | 29 | var str = new JwtSecurityTokenHandler().WriteToken(token); 30 | Log.Debug($"Session Token Data: {token}"); 31 | Log.Debug($"Session Token Base64: {str}"); 32 | return str; 33 | } 34 | } -------------------------------------------------------------------------------- /apps/web/public/8bitworker/zx/crt0.sym: -------------------------------------------------------------------------------- 1 | ASxxxx Assembler V02.00 + NoICE + SDCC mods (Zilog Z80 / Hitachi HD64180), page 1. 2 | Hexadecimal [16-Bits] 3 | 4 | Symbol Table 5 | 6 | .__.$$$.= 2710 L | .__.ABS.= 0000 G | .__.CPU.= 0000 L 7 | .__.H$L.= 0000 L | 0 _Start 0000 R | ___sdcc_ **** GX 8 | _main **** GX | 3 gsinit 0000 GR | 3 gsinit_s 0025 R 9 | l__DATA **** GX | l__INITI **** GX | s__DATA **** GX 10 | s__INITI **** GX | s__INITI **** GX | 3 zeroed_d 0016 R 11 | 12 | ASxxxx Assembler V02.00 + NoICE + SDCC mods (Zilog Z80 / Hitachi HD64180), page 2. 13 | Hexadecimal [16-Bits] 14 | 15 | Area Table 16 | 17 | 0 _CODE size A flags 0 18 | 1 _INITIAL size 0 flags 0 19 | 2 _HOME size 0 flags 0 20 | 3 _GSINIT size 25 flags 0 21 | 4 _GSFINAL size 1 flags 0 22 | 5 _DATA size 0 flags 0 23 | 6 _INITIAL size 0 flags 0 24 | 7 _BSEG size 0 flags 0 25 | 8 _BSS size 0 flags 0 26 | 9 _HEAP size 0 flags 0 27 | 28 | -------------------------------------------------------------------------------- /apps/auth/Auth.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Auth", "Auth\Auth.csproj", "{0572498D-CE1F-4EAA-8D0A-CD5D290F1E32}" 4 | EndProject 5 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F9ADAAF3-2415-4874-8321-7D18A4D931D3}" 6 | ProjectSection(SolutionItems) = preProject 7 | Dockerfile = Dockerfile 8 | .dockerignore = .dockerignore 9 | README.md = README.md 10 | .gitignore = .gitignore 11 | EndProjectSection 12 | EndProject 13 | Global 14 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 15 | Debug|Any CPU = Debug|Any CPU 16 | Release|Any CPU = Release|Any CPU 17 | EndGlobalSection 18 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 19 | {0572498D-CE1F-4EAA-8D0A-CD5D290F1E32}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 20 | {0572498D-CE1F-4EAA-8D0A-CD5D290F1E32}.Debug|Any CPU.Build.0 = Debug|Any CPU 21 | {0572498D-CE1F-4EAA-8D0A-CD5D290F1E32}.Release|Any CPU.ActiveCfg = Release|Any CPU 22 | {0572498D-CE1F-4EAA-8D0A-CD5D290F1E32}.Release|Any CPU.Build.0 = Release|Any CPU 23 | EndGlobalSection 24 | EndGlobal 25 | -------------------------------------------------------------------------------- /apps/web/public/8bitworker/zx/crt0.rel: -------------------------------------------------------------------------------- 1 | XL2 2 | H A areas 9 global symbols 3 | M crt0 4 | S l__DATA Ref0000 5 | S _main Ref0000 6 | S s__DATA Ref0000 7 | S .__.ABS. Def0000 8 | S s__INITIALIZED Ref0000 9 | S ___sdcc_call_hl Ref0000 10 | S l__INITIALIZER Ref0000 11 | S s__INITIALIZER Ref0000 12 | A _CODE size A flags 0 addr 0 13 | A _INITIALIZER size 0 flags 0 addr 0 14 | A _HOME size 0 flags 0 addr 0 15 | A _GSINIT size 25 flags 0 addr 0 16 | S gsinit Def0000 17 | A _GSFINAL size 1 flags 0 addr 0 18 | A _DATA size 0 flags 0 addr 0 19 | A _INITIALIZED size 0 flags 0 addr 0 20 | A _BSEG size 0 flags 0 addr 0 21 | A _BSS size 0 flags 0 addr 0 22 | A _HEAP size 0 flags 0 addr 0 23 | T 00 00 24 | R 00 00 00 00 25 | T 00 00 F3 ED 56 CD 00 00 CD 00 00 C7 26 | R 00 00 00 00 00 06 03 00 02 09 01 00 27 | T 00 00 28 | R 00 00 03 00 29 | T 00 00 01 00 00 78 B1 28 0F 21 00 00 36 00 0B 78 30 | R 00 00 03 00 02 03 00 00 02 0A 02 00 31 | T 0E 00 B1 28 05 5D 54 13 ED B0 32 | R 00 00 03 00 33 | T 16 00 34 | R 00 00 03 00 35 | T 16 00 01 00 00 78 B1 28 08 11 00 00 21 00 00 ED 36 | R 00 00 03 00 02 03 06 00 02 0A 04 00 02 0D 07 00 37 | T 24 00 B0 38 | R 00 00 03 00 39 | T 25 00 40 | R 00 00 03 00 41 | T 00 00 C9 42 | R 00 00 04 00 43 | -------------------------------------------------------------------------------- /apps/web/src/lib/jsspeccy/generator/opcodes_ed.txt: -------------------------------------------------------------------------------- 1 | 40 IN B,(C) 2 | 41 OUT (C),B 3 | 42 SBC HL,BC 4 | 43 LD (nn),BC 5 | 44 NEG 6 | 45 RETN 7 | 46 IM 0 8 | 47 LD I,A 9 | 48 IN C,(C) 10 | 49 OUT (C),C 11 | 4a ADC HL,BC 12 | 4b LD BC,(nn) 13 | 4c NEG 14 | 4d RETN 15 | 4e IM 0 16 | 4f LD R,A 17 | 18 | 50 IN D,(C) 19 | 51 OUT (C),D 20 | 52 SBC HL,DE 21 | 53 LD (nn),DE 22 | 54 NEG 23 | 55 RETN 24 | 56 IM 1 25 | 57 LD A,I 26 | 58 IN E,(C) 27 | 59 OUT (C),E 28 | 5a ADC HL,DE 29 | 5b LD DE,(nn) 30 | 5c NEG 31 | 5d RETN 32 | 5e IM 2 33 | 5f LD A,R 34 | 35 | 60 IN H,(C) 36 | 61 OUT (C),H 37 | 62 SBC HL,HL 38 | 63 LD (nn),HL 39 | 64 NEG 40 | 65 RETN 41 | 66 IM 0 42 | 67 RRD 43 | 68 IN L,(C) 44 | 69 OUT (C),L 45 | 6a ADC HL,HL 46 | 6b LD HL,(nn) 47 | 6c NEG 48 | 6d RETN 49 | 6e IM 0 50 | 6f RLD 51 | 52 | 70 IN F,(C) 53 | 71 OUT (C),0 54 | 72 SBC HL,SP 55 | 73 LD (nn),SP 56 | 74 NEG 57 | 75 RETN 58 | 76 IM 1 59 | 78 IN A,(C) 60 | 79 OUT (C),A 61 | 7a ADC HL,SP 62 | 7b LD SP,(nn) 63 | 7c NEG 64 | 7d RETN 65 | 7e IM 2 66 | 67 | a0 LDI 68 | a1 CPI 69 | a2 INI 70 | a3 OUTI 71 | a8 LDD 72 | a9 CPD 73 | aa IND 74 | ab OUTD 75 | b0 LDIR 76 | b1 CPIR 77 | b2 INIR 78 | b3 OTIR 79 | b8 LDDR 80 | b9 CPDR 81 | ba INDR 82 | bb OTDR 83 | -------------------------------------------------------------------------------- /apps/web/src/auth.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import Constants from "./constants"; 3 | 4 | let authToken = undefined; 5 | 6 | export function getAuthToken() { 7 | return authToken; 8 | } 9 | 10 | export function login() { 11 | window.location.href = `${Constants.authBase}/login?redirect_url=${window.location.href}`; 12 | } 13 | 14 | export function logout() { 15 | authToken = undefined; 16 | window.location.href = `${Constants.authBase}/logout?redirect_url=${window.location.href}`; 17 | } 18 | 19 | export function isExpired(jwt) { 20 | if (!jwt) return true; 21 | const b = jwt.split('.'); 22 | const a = b[1]; 23 | const o = JSON.parse(atob(a)); 24 | const s = new Date().getTime() / 1000; 25 | const exp = o['exp']; 26 | return exp < s + 30; // NOTE: 30 seconds or more remaining. 27 | } 28 | 29 | export function refreshToken() { 30 | return axios.get(`${Constants.authBase}/token`, { 31 | withCredentials: true 32 | }).then(response => { 33 | const jwt = response.data.token; 34 | if (!jwt) throw new Error('invalid jwt value'); 35 | authToken = jwt; 36 | return jwt; 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /apps/web/src/lib/8bitworker/defs_build.ts: -------------------------------------------------------------------------------- 1 | import {BuildStepResult} from "./defs_build_result"; 2 | import {FileData} from "./defs_files"; 3 | 4 | export interface PlatformParams { 5 | arch: string 6 | code_start: number 7 | rom_size: number 8 | data_start: number 9 | data_size: number 10 | stack_end: number 11 | extra_link_args: string[] 12 | extra_link_files: string[] 13 | } 14 | 15 | export interface WorkerBuildStep { 16 | path?: string 17 | files?: string[] 18 | tool?: string 19 | mainfile?: boolean 20 | } 21 | 22 | export interface BuildStep extends WorkerBuildStep { 23 | args?: string[] 24 | params?: PlatformParams 25 | result?: BuildStepResult 26 | prefix?: string 27 | maxts?: number 28 | } 29 | 30 | export interface WorkerFileUpdate { 31 | path: string 32 | data: FileData 33 | } 34 | 35 | export interface WorkerItemUpdate { 36 | key: string 37 | value: object 38 | } 39 | 40 | export interface WorkerMessage { 41 | preload?: string 42 | updates: WorkerFileUpdate[] 43 | buildsteps: WorkerBuildStep[] 44 | reset?: boolean 45 | setitems?: WorkerItemUpdate[] 46 | } 47 | -------------------------------------------------------------------------------- /apps/auth/Auth/Codespaces.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | 3 | namespace SteveR.Auth; 4 | 5 | public static class Codespaces 6 | { 7 | private static string? GetUrl() 8 | { 9 | var codespaceName = System.Environment.GetEnvironmentVariable("CODESPACE_NAME"); 10 | var forwardingDomain = System.Environment.GetEnvironmentVariable("GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN"); 11 | 12 | if (string.IsNullOrEmpty(codespaceName) || string.IsNullOrEmpty(forwardingDomain)) 13 | { 14 | return null; 15 | } 16 | 17 | const int port = 8080; // NOTE: Port number is hard-coded. 18 | return $"https://{codespaceName}-{port}.{forwardingDomain}"; 19 | } 20 | 21 | public static string? GetAuthRedirect() 22 | { 23 | var url = GetUrl(); 24 | return string.IsNullOrEmpty(url) ? null : $"{url}/"; 25 | } 26 | 27 | public static string? GetCorsOrigin() 28 | { 29 | var url = GetUrl(); 30 | 31 | if (string.IsNullOrEmpty(url)) 32 | { 33 | return url; 34 | } 35 | 36 | var arr = new List(); 37 | arr.Add(url); 38 | return JsonSerializer.Serialize(arr.ToArray()); 39 | } 40 | } -------------------------------------------------------------------------------- /apps/auth/Auth/Tokens/HasuraTokenDispenser.cs: -------------------------------------------------------------------------------- 1 | using System.IdentityModel.Tokens.Jwt; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Serilog; 4 | using SteveR.Auth.Repositories; 5 | 6 | namespace SteveR.Auth.Tokens; 7 | 8 | internal class HasuraTokenDispenser : TokenDispenser 9 | { 10 | public HasuraTokenDispenser(IConfiguration configuration) 11 | : base(configuration, "JWT:HasuraToken:") 12 | { 13 | 14 | } 15 | 16 | public async Task GenerateAccessToken( 17 | string userId, [FromServices] UserRepository userRepository) 18 | { 19 | var token = GetJwtSecurityToken(); 20 | var roles = await userRepository.GetRoles(userId); 21 | 22 | Log.Debug(string.Join(",", roles)); 23 | 24 | token.Payload["https://hasura.io/jwt/claims"] = new Dictionary 25 | { 26 | ["X-Hasura-User-Id"] = userId, 27 | ["X-Hasura-Allowed-Roles"] = roles, 28 | ["X-Hasura-Default-Role"] = GetDefaultRole()! 29 | }; 30 | 31 | var str = new JwtSecurityTokenHandler().WriteToken(token); 32 | Log.Debug($"API Token Data: {token}"); 33 | Log.Debug($"API Token Base64: {str}"); 34 | return str; 35 | } 36 | } -------------------------------------------------------------------------------- /apps/web/src/redux/app/actions.js: -------------------------------------------------------------------------------- 1 | export const actionTypes = { 2 | showActiveEmulator: 'app/showActiveEmulator', 3 | resetEmulator: 'app/resetEmulator', 4 | requestTermsOfUse: 'app/requestTermsOfUse', 5 | receiveTermsOfUse: 'app/receiveTermsOfUse', 6 | requestPrivacyPolicy: 'app/requestPrivacyPolicy', 7 | receivePrivacyPolicy: 'app/receivePrivacyPolicy', 8 | toggleLineNumbers: 'app/toggleLineNumbers', 9 | }; 10 | 11 | export const showActiveEmulator = () => ({ 12 | type: actionTypes.showActiveEmulator 13 | }) 14 | 15 | export const resetEmulator = () => ({ 16 | type: actionTypes.resetEmulator 17 | }) 18 | 19 | export const requestTermsOfUse = () => ({ 20 | type: actionTypes.requestTermsOfUse 21 | }) 22 | 23 | export const receiveTermsOfUse = (text) => ({ 24 | type: actionTypes.receiveTermsOfUse, 25 | text 26 | }) 27 | 28 | export const requestPrivacyPolicy = () => ({ 29 | type: actionTypes.requestPrivacyPolicy 30 | }) 31 | 32 | export const receivePrivacyPolicy = (text) => ({ 33 | type: actionTypes.receivePrivacyPolicy, 34 | text 35 | }) 36 | 37 | export const toggleLineNumbers = (enabled) => ({ 38 | type: actionTypes.toggleLineNumbers, 39 | enabled 40 | }) 41 | -------------------------------------------------------------------------------- /apps/web/src/redux/identity/reducers.js: -------------------------------------------------------------------------------- 1 | import {actionTypes} from "./actions"; 2 | 3 | // ----------------------------------------------------------------------------- 4 | // Initial state 5 | // ----------------------------------------------------------------------------- 6 | 7 | const initialState = { 8 | userId: undefined, 9 | userSlug: undefined, 10 | }; 11 | 12 | // ----------------------------------------------------------------------------- 13 | // Actions 14 | // ----------------------------------------------------------------------------- 15 | 16 | function setUserInfo(state, action) { 17 | return { 18 | ...state, 19 | userId: action.userInfo.userId, 20 | userSlug: action.userInfo.userSlug, 21 | }; 22 | } 23 | 24 | // ----------------------------------------------------------------------------- 25 | // Reducer 26 | // ----------------------------------------------------------------------------- 27 | 28 | const actionsMap = { 29 | [actionTypes.setUserInfo]: setUserInfo, 30 | }; 31 | 32 | export default function reducer(state = initialState, action) { 33 | const reducerFunction = actionsMap[action.type]; 34 | return reducerFunction ? reducerFunction(state, action) : state; 35 | } 36 | -------------------------------------------------------------------------------- /hasura/migrations/default/1759594620000_add_social_features/up.sql: -------------------------------------------------------------------------------- 1 | -- Create user_follows table for tracking follow relationships 2 | CREATE TABLE IF NOT EXISTS public.user_follows ( 3 | follower_id UUID NOT NULL REFERENCES public."user"(user_id) ON DELETE CASCADE, 4 | following_id UUID NOT NULL REFERENCES public."user"(user_id) ON DELETE CASCADE, 5 | created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), 6 | PRIMARY KEY (follower_id, following_id), 7 | -- Ensure users can't follow themselves 8 | CONSTRAINT no_self_follow CHECK (follower_id != following_id) 9 | ); 10 | 11 | -- Create indexes for performance 12 | CREATE INDEX IF NOT EXISTS idx_user_follows_follower ON public.user_follows(follower_id); 13 | CREATE INDEX IF NOT EXISTS idx_user_follows_following ON public.user_follows(following_id); 14 | CREATE INDEX IF NOT EXISTS idx_user_follows_created_at ON public.user_follows(created_at DESC); 15 | 16 | -- Add comment to table 17 | COMMENT ON TABLE public.user_follows IS 'Stores follow relationships between users'; 18 | COMMENT ON COLUMN public.user_follows.follower_id IS 'User who is following'; 19 | COMMENT ON COLUMN public.user_follows.following_id IS 'User who is being followed'; 20 | COMMENT ON COLUMN public.user_follows.created_at IS 'Timestamp when the follow relationship was created'; -------------------------------------------------------------------------------- /apps/web/src/redux/projectList/actions.js: -------------------------------------------------------------------------------- 1 | export const actionTypes = { 2 | reset: 'projectList/reset', 3 | subscribeToProjectList: 'projectList/subscribeToProjectList', 4 | subscribeToProjectListCallback: 'projectList/subscribeToProjectListCallback', 5 | unsubscribeFromProjectList: 'projectList/unsubscribeFromProjectList', 6 | receiveprojectListQueryResult: 'projectList/receiveprojectListQueryResult', 7 | setProjectListPreferences: 'projectList/setProjectListPreferences', 8 | }; 9 | 10 | export const reset = () => ({ 11 | type: actionTypes.reset 12 | }); 13 | 14 | export const subscribeToProjectList = () => ({ 15 | type: actionTypes.subscribeToProjectList 16 | }); 17 | 18 | export const subscribeToProjectListCallback = (error, data) => ({ 19 | type: actionTypes.subscribeToProjectListCallback, 20 | error, data 21 | }); 22 | 23 | export const unsubscribeFromProjectList = () => ({ 24 | type: actionTypes.unsubscribeFromProjectList 25 | }); 26 | 27 | export const receiveprojectListQueryResult = (result) => ({ 28 | type: actionTypes.receiveprojectListQueryResult, 29 | result 30 | }); 31 | 32 | export const setProjectListPreferences = (preferences) => ({ 33 | type: actionTypes.setProjectListPreferences, 34 | preferences 35 | }); 36 | -------------------------------------------------------------------------------- /apps/web/public/8bitworker/zx/crt0.s: -------------------------------------------------------------------------------- 1 | ; crt0.s for ZX Spectrum 2 | 3 | .module crt0 4 | .globl _main 5 | .globl ___sdcc_call_hl 6 | 7 | ; Ordering of segments for the linker - copied from sdcc crt0.s 8 | .area _CODE 9 | .area _INITIALIZER 10 | .area _HOME 11 | .area _GSINIT 12 | .area _GSFINAL 13 | .area _DATA 14 | .area _INITIALIZED 15 | .area _BSEG 16 | .area _BSS 17 | .area _HEAP 18 | 19 | .area _CODE 20 | 21 | _Start: 22 | di 23 | im 1 24 | ; stack pointer already set by BIOS 25 | call gsinit ; Initialize global and static variables. 26 | call _main 27 | rst 0x0 ; Restart when main() returns. 28 | 29 | .area _GSINIT 30 | gsinit:: 31 | 32 | ; Implicitly zeroed global and static variables. 33 | ld bc, #l__DATA 34 | ld a, b 35 | or a, c 36 | jr Z, zeroed_data 37 | ld hl, #s__DATA 38 | ld (hl), #0x00 39 | dec bc 40 | ld a, b 41 | or a, c 42 | jr Z, zeroed_data 43 | ld e, l 44 | ld d, h 45 | inc de 46 | ldir 47 | zeroed_data: 48 | 49 | ; Explicitly initialized global variables. 50 | ld bc, #l__INITIALIZER 51 | ld a, b 52 | or a, c 53 | jr Z, gsinit_static 54 | ld de, #s__INITIALIZED 55 | ld hl, #s__INITIALIZER 56 | ldir 57 | 58 | gsinit_static: 59 | ; Explicitly initialized static variables inserted by compiler here. 60 | 61 | .area _GSFINAL 62 | ret 63 | 64 | .area _HOME 65 | 66 | -------------------------------------------------------------------------------- /apps/web/src/redux/eightbit/reducers.js: -------------------------------------------------------------------------------- 1 | import {actionTypes} from "./actions"; 2 | 3 | // ----------------------------------------------------------------------------- 4 | // Initial state 5 | // ----------------------------------------------------------------------------- 6 | 7 | const initialState = { 8 | followTapAction: undefined 9 | }; 10 | 11 | // ----------------------------------------------------------------------------- 12 | // Actions 13 | // ----------------------------------------------------------------------------- 14 | 15 | function reset() { 16 | return {...initialState}; 17 | } 18 | 19 | function setFollowTapAction(state, action) { 20 | return { 21 | ...state, 22 | followTapAction: action.followTapAction 23 | } 24 | } 25 | 26 | // ----------------------------------------------------------------------------- 27 | // Reducer 28 | // ----------------------------------------------------------------------------- 29 | 30 | const actionsMap = { 31 | [actionTypes.reset]: reset, 32 | [actionTypes.setFollowTapAction]: setFollowTapAction, 33 | }; 34 | 35 | export default function reducer(state = initialState, action) { 36 | const reducerFunction = actionsMap[action.type]; 37 | return reducerFunction ? reducerFunction(state, action) : state; 38 | } 39 | -------------------------------------------------------------------------------- /apps/web/src/redux/window/reducers.js: -------------------------------------------------------------------------------- 1 | import { actionTypes } from './actions'; 2 | 3 | const mobileMaxWidth = 768; 4 | 5 | // ----------------------------------------------------------------------------- 6 | // Initial state 7 | // ----------------------------------------------------------------------------- 8 | 9 | const initialState = { 10 | width: window.innerWidth, 11 | height: window.innerHeight, 12 | isMobile: window.innerWidth <= mobileMaxWidth, 13 | }; 14 | 15 | // ----------------------------------------------------------------------------- 16 | // Actions 17 | // ----------------------------------------------------------------------------- 18 | 19 | function resized(state, action) { 20 | return { 21 | ...state, 22 | width: action.width, 23 | height: action.height, 24 | isMobile: action.width <= mobileMaxWidth, 25 | }; 26 | } 27 | 28 | // ----------------------------------------------------------------------------- 29 | // Reducer 30 | // ----------------------------------------------------------------------------- 31 | 32 | const actionsMap = { 33 | [actionTypes.resized]: resized, 34 | }; 35 | 36 | export default function reducer(state = initialState, action) { 37 | const reducerFunction = actionsMap[action.type]; 38 | return reducerFunction ? reducerFunction(state, action) : state; 39 | } 40 | -------------------------------------------------------------------------------- /apps/web/src/redux/error/reducers.js: -------------------------------------------------------------------------------- 1 | import {actionTypes} from "./actions"; 2 | 3 | // ----------------------------------------------------------------------------- 4 | // Initial state 5 | // ----------------------------------------------------------------------------- 6 | 7 | /** 8 | * @type {{ 9 | * msg: string, 10 | * }} 11 | */ 12 | const initialState = { 13 | msg: undefined 14 | }; 15 | 16 | // ----------------------------------------------------------------------------- 17 | // Actions 18 | // ----------------------------------------------------------------------------- 19 | 20 | function reset() { 21 | return {...initialState}; 22 | } 23 | 24 | function error(state, action) { 25 | return { 26 | ...state, 27 | msg: action.msg 28 | }; 29 | } 30 | 31 | // ----------------------------------------------------------------------------- 32 | // Reducer 33 | // ----------------------------------------------------------------------------- 34 | 35 | const actionsMap = { 36 | [actionTypes.reset]: reset, 37 | [actionTypes.error]: error, 38 | ['@@router/ON_LOCATION_CHANGED']: reset, 39 | }; 40 | 41 | export default function reducer(state = initialState, action) { 42 | const reducerFunction = actionsMap[action.type]; 43 | return reducerFunction ? reducerFunction(state, action) : state; 44 | } 45 | -------------------------------------------------------------------------------- /apps/web/src/lib/canvasgui/ImageButton.js: -------------------------------------------------------------------------------- 1 | import {Control} from "./Control"; 2 | 3 | export class ImageButton extends Control { 4 | constructor(parent, x, y, w, h, imgup, imgdown) { 5 | super(parent, x, y, w, h); 6 | this.imgup = parent.win.imagemgr.imageForPath(imgup); 7 | if (imgdown) { 8 | this.imgdown = parent.win.imagemgr.imageForPath(imgdown); 9 | } else { 10 | this.imgdown = null; 11 | } 12 | } 13 | 14 | onDraw(ctx) { 15 | let x = this.x; 16 | let y = this.y; 17 | let w = this.w; 18 | let h = this.h; 19 | 20 | if (this.pressed) { 21 | if (this.imgdown != null) { 22 | ctx.drawImage(this.imgdown, x, y, w, h); 23 | } else { 24 | ctx.drawImage(this.imgup, x, y, w, h); 25 | ctx.globalAlpha = 0.5; 26 | ctx.fillStyle = 'black'; 27 | ctx.fillRect(x, y, w, h); 28 | ctx.globalAlpha = 1.0; 29 | } 30 | } else { 31 | ctx.drawImage(this.imgup, x, y, w, h); 32 | } 33 | } 34 | 35 | onPointerEvent(type, x, y) { 36 | super.onPointerEvent(type, x, y); 37 | //console.log("ImageButton.onPointerEvent: type = " + type + ", x = " + x + ", y = " + y + ", text = " + this.text); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /apps/web/src/lib/8bitworker/worker.ts: -------------------------------------------------------------------------------- 1 | import {WorkerResult} from "./defs_build_result"; 2 | import {Builder} from "./Builder"; 3 | import {errorResult} from "./shared_funcs"; 4 | import {fsLoaded, loadFilesystem, fileStore} from "./files"; 5 | import {WorkerMessage} from "./defs_build"; 6 | 7 | declare function postMessage(msg); 8 | 9 | const TOOL_PRELOADFS = { 10 | 'sdasz80': 'sdcc', 11 | 'sdcc': 'sdcc', 12 | } 13 | 14 | const builder = new Builder(); 15 | 16 | async function handleMessage(data: WorkerMessage): Promise { 17 | 18 | // preload file system 19 | if (data.preload) { 20 | let fs = TOOL_PRELOADFS[data.preload]; 21 | 22 | if (fs && !fsLoaded[fs]) { 23 | loadFilesystem(fs); 24 | } 25 | 26 | return; 27 | } 28 | 29 | // clear filesystem? 30 | if (data.reset) { 31 | fileStore.reset(); 32 | return; 33 | } 34 | 35 | return builder.handleMessage(data); 36 | } 37 | 38 | let lastpromise = null; 39 | onmessage = async function (e) { 40 | await lastpromise; // wait for previous message to complete 41 | lastpromise = handleMessage(e.data); 42 | const result = await lastpromise; 43 | lastpromise = null; 44 | if (result) { 45 | try { 46 | postMessage(result); 47 | } catch (e) { 48 | console.log(e); 49 | postMessage(errorResult(`${e}`)); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /apps/web/src/lib/canvasgui/Control.js: -------------------------------------------------------------------------------- 1 | import {Widget} from "./Widget"; 2 | 3 | export class Control extends Widget { 4 | constructor(parent, x, y, w, h) { 5 | super(parent, x, y, w, h); 6 | this.disabled = false; 7 | this.pressed = false; 8 | this.checked = false; 9 | this.rcvd_begin = false; 10 | this.on_begin = null; 11 | this.on_enter = null; 12 | this.on_leave = null; 13 | this.on_end = null; 14 | } 15 | 16 | onPointerEvent(type, x, y) { 17 | if (this.disabled) { 18 | return; 19 | } 20 | 21 | if (type == 'begin') { 22 | this.rcvd_begin = true; 23 | if (this.on_begin) this.on_begin(); 24 | return; 25 | } 26 | 27 | if (this.rcvd_begin) { 28 | if (type == 'enter') { 29 | this.pressed = true; 30 | this.requestRedraw(); 31 | if (this.on_enter) this.on_enter(); 32 | return; 33 | } 34 | 35 | if (type == 'leave') { 36 | this.pressed = false; 37 | this.requestRedraw(); 38 | if (this.on_leave) this.on_leave(); 39 | return; 40 | } 41 | 42 | if (type == 'end') { 43 | if (this.on_end) this.on_end.call(this); 44 | return; 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /apps/web/src/lib/jsspeccy/generator/opcodes_dd.txt: -------------------------------------------------------------------------------- 1 | 09 ADD IX,BC 2 | 19 ADD IX,DE 3 | 21 LD IX,nn 4 | 22 LD (nn),IX 5 | 23 INC IX 6 | 24 INC IXH 7 | 25 DEC IXH 8 | 26 LD IXH,n 9 | 29 ADD IX,IX 10 | 2a LD IX,(nn) 11 | 2b DEC IX 12 | 2c INC IXL 13 | 2d DEC IXL 14 | 2e LD IXL,n 15 | 34 INC (IX+n) 16 | 35 DEC (IX+n) 17 | 36 LD (IX+n),n 18 | 39 ADD IX,SP 19 | 44 LD B,IXH 20 | 45 LD B,IXL 21 | 46 LD B,(IX+n) 22 | 4c LD C,IXH 23 | 4d LD C,IXL 24 | 4e LD C,(IX+n) 25 | 54 LD D,IXH 26 | 55 LD D,IXL 27 | 56 LD D,(IX+n) 28 | 5c LD E,IXH 29 | 5d LD E,IXL 30 | 5e LD E,(IX+n) 31 | 60 LD IXH,B 32 | 61 LD IXH,C 33 | 62 LD IXH,D 34 | 63 LD IXH,E 35 | 64 LD IXH,IXH 36 | 65 LD IXH,IXL 37 | 66 LD H,(IX+n) 38 | 67 LD IXH,A 39 | 68 LD IXL,B 40 | 69 LD IXL,C 41 | 6a LD IXL,D 42 | 6b LD IXL,E 43 | 6c LD IXL,IXH 44 | 6d LD IXL,IXL 45 | 6e LD L,(IX+n) 46 | 6f LD IXL,A 47 | 70 LD (IX+n),B 48 | 71 LD (IX+n),C 49 | 72 LD (IX+n),D 50 | 73 LD (IX+n),E 51 | 74 LD (IX+n),H 52 | 75 LD (IX+n),L 53 | 77 LD (IX+n),A 54 | 7c LD A,IXH 55 | 7d LD A,IXL 56 | 7e LD A,(IX+n) 57 | 84 ADD A,IXH 58 | 85 ADD A,IXL 59 | 86 ADD A,(IX+n) 60 | 8c ADC A,IXH 61 | 8d ADC A,IXL 62 | 8e ADC A,(IX+n) 63 | 94 SUB IXH 64 | 95 SUB IXL 65 | 96 SUB (IX+n) 66 | 9c SBC A,IXH 67 | 9d SBC A,IXL 68 | 9e SBC A,(IX+n) 69 | a4 AND IXH 70 | a5 AND IXL 71 | a6 AND (IX+n) 72 | ac XOR IXH 73 | ad XOR IXL 74 | ae XOR (IX+n) 75 | b4 OR IXH 76 | b5 OR IXL 77 | b6 OR (IX+n) 78 | bc CP IXH 79 | bd CP IXL 80 | be CP (IX+n) 81 | cb prefix ddcb 82 | e1 POP IX 83 | e3 EX (SP),IX 84 | e5 PUSH IX 85 | e9 JP (IX) 86 | f9 LD SP,IX 87 | -------------------------------------------------------------------------------- /apps/gif-service/src/tap-file.ts: -------------------------------------------------------------------------------- 1 | export class TAPFile { 2 | private blocks: Uint8Array[] = []; 3 | private nextBlockIndex: number = 0; 4 | 5 | constructor(data: Buffer) { 6 | let i = 0; 7 | const tap = new DataView(data.buffer, data.byteOffset, data.byteLength); 8 | 9 | while ((i + 1) < data.byteLength) { 10 | const blockLength = tap.getUint16(i, true); 11 | i += 2; 12 | this.blocks.push(new Uint8Array(data.buffer, data.byteOffset + i, blockLength)); 13 | i += blockLength; 14 | } 15 | console.log(`TAP file parsed: ${this.blocks.length} blocks, sizes: ${this.blocks.map(b => b.length).join(', ')}`); 16 | } 17 | 18 | getNextLoadableBlock(): Uint8Array | null { 19 | if (this.blocks.length === 0) return null; 20 | const block = this.blocks[this.nextBlockIndex]; 21 | console.log(`getNextLoadableBlock: returning block ${this.nextBlockIndex} of ${this.blocks.length}, size ${block.length}`); 22 | this.nextBlockIndex = (this.nextBlockIndex + 1) % this.blocks.length; 23 | return block; 24 | } 25 | 26 | static isValid(data: Buffer): boolean { 27 | let pos = 0; 28 | const tap = new DataView(data.buffer, data.byteOffset, data.byteLength); 29 | 30 | while (pos < data.byteLength) { 31 | if (pos + 1 >= data.byteLength) return false; 32 | const blockLength = tap.getUint16(pos, true); 33 | pos += blockLength + 2; 34 | } 35 | 36 | return pos === data.byteLength; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /apps/web/src/redux/eightbit/actions.js: -------------------------------------------------------------------------------- 1 | export const actionTypes = { 2 | reset: 'eightbit/reset', 3 | handleWorkerMessage: 'eightbit/handleWorkerMessage', 4 | runProjectCode: 'eightbit/runProjectCode', 5 | downloadProjectTap: 'eightbit/downloadProjectTap', 6 | getProjectTap: 'eightbit/getProjectTap', 7 | getSdccTap: 'eightbit/getSdccTap', 8 | getZmacTap: 'eightbit/getZmacTap', 9 | setFollowTapAction: 'eightbit/setFollowTapAction', 10 | browserTapDownload: 'eightbit/browserTapDownload', 11 | runTap: 'eightbit/runTap', 12 | }; 13 | 14 | export const handleWorkerMessage = (msg) => ({ 15 | type: actionTypes.handleWorkerMessage, 16 | msg 17 | }); 18 | 19 | export const runProjectCode = () => ({ 20 | type: actionTypes.runProjectCode 21 | }); 22 | 23 | export const downloadProjectTap = () => ({ 24 | type: actionTypes.downloadProjectTap 25 | }); 26 | 27 | export const getProjectTap = () => ({ 28 | type: actionTypes.getProjectTap 29 | }); 30 | 31 | export const getSdccTap = () => ({ 32 | type: actionTypes.getSdccTap 33 | }); 34 | 35 | export const getZmacTap = () => ({ 36 | type: actionTypes.getZmacTap 37 | }); 38 | 39 | export const setFollowTapAction = (followTapAction) => ({ 40 | type: actionTypes.setFollowTapAction, 41 | followTapAction 42 | }); 43 | 44 | export const browserTapDownload = (tap) => ({ 45 | type: actionTypes.browserTapDownload, 46 | tap 47 | }); 48 | 49 | export const runTap = (tap) => ({ 50 | type: actionTypes.runTap, 51 | tap 52 | }); 53 | -------------------------------------------------------------------------------- /apps/gif-service/src/routes/tape.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import multer from 'multer'; 3 | import { GIFGenerator } from '../gif-generator.js'; 4 | 5 | const router = Router(); 6 | const upload = multer({ storage: multer.memoryStorage() }); 7 | 8 | router.post('/tape-to-gif', upload.single('tape'), async (req, res) => { 9 | try { 10 | if (!req.file) { 11 | res.status(400).json({ error: 'No tape file uploaded' }); 12 | return; 13 | } 14 | 15 | const maxMinutes = parseInt(req.query.maxMinutes as string) || 3; 16 | const staleThreshold = parseInt(req.query.staleThreshold as string) || 1500; 17 | const machineType = parseInt(req.query.machineType as string) || 128; 18 | 19 | const generator = new GIFGenerator({ 20 | maxDurationMs: maxMinutes * 60 * 1000, 21 | staleFrameThreshold: staleThreshold, 22 | }); 23 | 24 | await generator.initialize(); 25 | 26 | console.log(`Generating GIF from ${req.file.originalname}...`); 27 | const gifBuffer = await generator.generateFromTAP(req.file.buffer, machineType); 28 | console.log(`GIF generated: ${gifBuffer.length} bytes`); 29 | 30 | res.setHeader('Content-Type', 'image/gif'); 31 | res.setHeader('Content-Disposition', `attachment; filename="${req.file.originalname}.gif"`); 32 | res.send(gifBuffer); 33 | } catch (error: any) { 34 | console.error('Error generating GIF:', error); 35 | res.status(500).json({ error: error.message }); 36 | } 37 | }); 38 | 39 | export default router; 40 | -------------------------------------------------------------------------------- /apps/proxy/Caddyfile: -------------------------------------------------------------------------------- 1 | :8080 { 2 | # Strict Content Security Policy 3 | header { 4 | # Remove server identification headers 5 | -Server 6 | -X-Powered-By 7 | 8 | # Security headers 9 | X-Frame-Options "DENY" 10 | X-Content-Type-Options "nosniff" 11 | Referrer-Policy "strict-origin-when-cross-origin" 12 | X-XSS-Protection "1; mode=block" 13 | Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" 14 | 15 | # Strict CSP with nonces for inline scripts/styles 16 | # Note: 'unsafe-inline' for style-src is needed for React inline styles 17 | # 'unsafe-eval' is avoided - using 'wasm-unsafe-eval' for WebAssembly only 18 | Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval' 'sha256-te4o1w3itVxmYQGHyhr0oKSlUohKRzWaI6ZTILb+KYQ='; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' ws://localhost:* wss://localhost:* http://localhost:4000 http://localhost:5000 http://localhost:8000; worker-src 'self' blob:; child-src 'self' blob:; frame-src 'none'; object-src 'none'; base-uri 'self'; form-action 'self'; upgrade-insecure-requests" 19 | 20 | # Report CSP violations (optional - you can set up a reporting endpoint) 21 | # Content-Security-Policy-Report-Only would be useful for testing 22 | } 23 | 24 | redir /auth /auth/ 25 | handle /auth/* { 26 | uri strip_prefix /auth 27 | reverse_proxy localhost:5000 28 | } 29 | 30 | redir /api /api/ 31 | handle /api/* { 32 | uri strip_prefix /api 33 | reverse_proxy localhost:4000 34 | } 35 | 36 | handle { 37 | reverse_proxy localhost:8000 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /apps/web/src/components/LockScreen.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function LockScreen() { 4 | return ( 5 | 6 |
7 |
8 |
9 |
10 | 11 |
12 |
13 | 14 | ) 15 | } 16 | 17 | const styles = { 18 | fullPage: { 19 | position: 'absolute', 20 | top: 0, 21 | left: 0, 22 | margin: 0, 23 | padding: 0, 24 | zIndex: 99999, 25 | width: '100%', 26 | height: '100vh', 27 | display: 'none', 28 | userSelect: 'none' 29 | }, 30 | contentBlanker: { 31 | position: 'fixed', 32 | top: 0, 33 | left: 0, 34 | margin: 0, 35 | padding: 0, 36 | zIndex: 99999, 37 | width: '100%', 38 | height: '100vh', 39 | backgroundColor: 'black', 40 | opacity: 0.05, 41 | display: 'none', 42 | userSelect: 'none' 43 | }, 44 | container: { 45 | position: 'absolute', 46 | top: 0, 47 | left: 0, 48 | margin: 0, 49 | padding: 0, 50 | zIndex: 99999, 51 | width: '100%', 52 | height: '100vh', 53 | display: 'none', 54 | userSelect: 'none' 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /apps/web/src/lib/8bitworker/SourceFile.ts: -------------------------------------------------------------------------------- 1 | import {SourceSnippet} from "./defs_build_result"; 2 | 3 | export class SourceFile { 4 | 5 | lines: SourceSnippet[]; 6 | text: string; 7 | offset2loc: Map; //{[offset:number]:number}; 8 | line2offset: Map; //{[line:number]:number}; 9 | 10 | constructor(lines: SourceSnippet[], text: string) { 11 | lines = lines || []; 12 | 13 | this.lines = lines; 14 | this.text = text; 15 | this.offset2loc = new Map(); 16 | this.line2offset = new Map(); 17 | 18 | for (let info of lines) { 19 | if (info.offset >= 0) { 20 | 21 | // first line wins (is assigned to offset) 22 | if (!this.offset2loc[info.offset]) { 23 | this.offset2loc[info.offset] = info; 24 | } 25 | 26 | if (!this.line2offset[info.line]) { 27 | this.line2offset[info.line] = info.offset; 28 | } 29 | } 30 | } 31 | } 32 | 33 | findLineForOffset(PC: number, lookbehind: number) { 34 | if (this.offset2loc) { 35 | for (let i = 0; i <= lookbehind; i++) { 36 | const loc = this.offset2loc[PC]; 37 | 38 | if (loc) { 39 | return loc; 40 | } 41 | 42 | PC--; 43 | } 44 | } 45 | 46 | return null; 47 | } 48 | 49 | lineCount(): number { 50 | return this.lines.length; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /apps/web/src/components/NewProjectPage.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { useDispatch } from "react-redux"; 4 | import { Titled } from "react-titled"; 5 | import { Card } from "primereact/card"; 6 | import { InputText } from "primereact/inputtext"; 7 | import { Button } from "primereact/button"; 8 | import { createNewProject } from "../redux/project/actions"; 9 | import { getLanguageLabel } from "../lib/lang"; 10 | import { sep } from "../constants"; 11 | 12 | NewProjectPage.propTypes = { 13 | type: PropTypes.string.isRequired, 14 | }; 15 | 16 | export default function NewProjectPage(props) { 17 | const dispatch = useDispatch(); 18 | const [title, setTitle] = useState(""); 19 | const lang = getLanguageLabel(props.type); 20 | 21 | return ( 22 | `New Project ${sep} ${s}`}> 23 | 24 |

New {lang} Project

25 |

Project Name

26 |
27 | setTitle(e.target.value)} 30 | autoFocus={true} 31 | onKeyDown={(e) => { 32 | if (e.code === "Enter") { 33 | dispatch(createNewProject(props.type, title)); 34 | } 35 | }} 36 | /> 37 |
38 |
58 | 59 | 60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /apps/auth/Auth/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using Serilog; 3 | using Serilog.Events; 4 | using SteveR.Auth; 5 | using SteveR.Auth.Repositories; 6 | 7 | // Configure Serilog. 8 | Log.Logger = new LoggerConfiguration() 9 | #if DEBUG 10 | .MinimumLevel.Debug() 11 | #else 12 | .MinimumLevel.Information() 13 | #endif 14 | // Write streamlined request completion events, instead of the more verbose ones from the framework. 15 | .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning) 16 | .Enrich.WithMachineName() // Adds MachineName based on either %COMPUTERNAME% (Windows) or $HOSTNAME (macOS, Linux) 17 | .Enrich.FromLogContext() 18 | .WriteTo.Console() 19 | .CreateLogger(); 20 | 21 | var builder = WebApplication.CreateBuilder(args); 22 | 23 | builder.Host.UseSerilog(); 24 | 25 | builder.Configuration.AddEnvironmentVariables(prefix: "AUTH_"); 26 | 27 | // Add repositories. 28 | builder.Services.AddTransient(); 29 | builder.Services.AddTransient(); 30 | builder.Services.AddTransient(); 31 | 32 | builder.Services.AddCors(); 33 | 34 | builder.Services.AddControllers(); 35 | 36 | // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle 37 | builder.Services.AddEndpointsApiExplorer(); 38 | builder.Services.AddSwaggerGen(); 39 | 40 | var app = builder.Build(); 41 | 42 | // Configure the HTTP request pipeline. 43 | 44 | app.UseSerilogRequestLogging(); 45 | 46 | if (app.Environment.IsDevelopment()) 47 | { 48 | app.UseDeveloperExceptionPage(); 49 | 50 | app.UseSwagger(); 51 | app.UseSwaggerUI(); 52 | } 53 | 54 | // app.UseHttpsRedirection(); 55 | 56 | var corsOrigin = app.Configuration["CorsOrigin"]; 57 | 58 | #if DEBUG 59 | 60 | if (corsOrigin == null) 61 | { 62 | corsOrigin = Codespaces.GetCorsOrigin(); 63 | } 64 | 65 | #endif 66 | 67 | if (corsOrigin != null) 68 | { 69 | app.UseCors( 70 | options => options 71 | .WithOrigins(JsonSerializer.Deserialize>(corsOrigin)!.ToArray()) 72 | .AllowCredentials() 73 | .AllowAnyMethod() 74 | ); 75 | } 76 | 77 | // app.UseAuthorization(); 78 | 79 | app.MapControllers(); 80 | 81 | app.Run(); -------------------------------------------------------------------------------- /apps/web/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Code · ZX Play 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 31 | 32 | 37 | 42 | 43 | 44 |
45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /apps/web/src/lib/8bitworker/FileWorkingStore.ts: -------------------------------------------------------------------------------- 1 | import {FileData, FileEntry, WorkingStore} from "./defs_files"; 2 | 3 | export class FileWorkingStore implements WorkingStore { 4 | workfs: { [path: string]: FileEntry } = {}; 5 | workerseq: number = 0; 6 | items: {}; 7 | 8 | constructor() { 9 | this.reset(); 10 | } 11 | 12 | reset() { 13 | this.workfs = {}; 14 | this.newVersion(); 15 | } 16 | 17 | currentVersion() { 18 | return this.workerseq; 19 | } 20 | 21 | newVersion() { 22 | let ts = new Date().getTime(); 23 | 24 | if (ts <= this.workerseq) { 25 | ts = ++this.workerseq; 26 | } 27 | 28 | return ts; 29 | } 30 | 31 | putFile(path: string, data: FileData): FileEntry { 32 | const encoding = (typeof data === 'string') ? 'utf8' : 'binary'; 33 | let entry = this.workfs[path]; 34 | 35 | if (!entry || !compareData(entry.data, data) || entry.encoding != encoding) { 36 | this.workfs[path] = entry = { 37 | path: path, 38 | data: data, 39 | encoding: encoding, 40 | ts: this.newVersion() 41 | }; 42 | 43 | console.log('+++', entry.path, entry.encoding, entry.data.length, entry.ts); 44 | } 45 | 46 | return entry; 47 | } 48 | 49 | getFileData(path: string): FileData { 50 | return this.workfs[path] && this.workfs[path].data; 51 | } 52 | 53 | getFileAsString(path: string): string { 54 | let data = this.getFileData(path); 55 | 56 | if (data != null && typeof data !== 'string') { 57 | throw new Error(`${path}: expected string`) 58 | } 59 | 60 | return data as string; 61 | } 62 | 63 | setItem(key: string, value: object) { 64 | this.items[key] = value; 65 | } 66 | } 67 | 68 | function compareData(a: FileData, b: FileData): boolean { 69 | if (a.length != b.length) { 70 | return false; 71 | } 72 | 73 | if (typeof a === 'string' && typeof b === 'string') { 74 | return a == b; 75 | } else { 76 | for (let i = 0; i < a.length; i++) { 77 | if (a[i] != b[i]) return false; 78 | } 79 | 80 | return true; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /hasura/metadata/databases/default/tables/public_user.yaml: -------------------------------------------------------------------------------- 1 | table: 2 | name: user 3 | schema: public 4 | array_relationships: 5 | - name: followers 6 | using: 7 | foreign_key_constraint_on: 8 | column: following_id 9 | table: 10 | name: user_follows 11 | schema: public 12 | - name: following 13 | using: 14 | foreign_key_constraint_on: 15 | column: follower_id 16 | table: 17 | name: user_follows 18 | schema: public 19 | - name: projects 20 | using: 21 | foreign_key_constraint_on: 22 | column: owner_user_id 23 | table: 24 | name: project 25 | schema: public 26 | - name: sessions 27 | using: 28 | foreign_key_constraint_on: 29 | column: user_id 30 | table: 31 | name: session 32 | schema: public 33 | - name: user_roles 34 | using: 35 | foreign_key_constraint_on: 36 | column: user_id 37 | table: 38 | name: user_role 39 | schema: public 40 | select_permissions: 41 | - role: public 42 | permission: 43 | columns: 44 | - user_id 45 | - username 46 | - greeting_name 47 | - created_at 48 | - slug 49 | - bio 50 | - profile_is_public 51 | - avatar_variant 52 | - custom_avatar_data 53 | filter: 54 | profile_is_public: 55 | _eq: true 56 | allow_aggregations: true 57 | - role: zxplay-user 58 | permission: 59 | columns: 60 | - user_id 61 | - username 62 | - greeting_name 63 | - full_name 64 | - email_address 65 | - created_at 66 | - slug 67 | - bio 68 | - profile_is_public 69 | - avatar_variant 70 | - custom_avatar_data 71 | filter: 72 | _or: 73 | - user_id: 74 | _eq: X-Hasura-User-Id 75 | - profile_is_public: 76 | _eq: true 77 | allow_aggregations: true 78 | update_permissions: 79 | - role: zxplay-user 80 | permission: 81 | columns: 82 | - greeting_name 83 | - full_name 84 | - email_address 85 | - bio 86 | - profile_is_public 87 | - slug 88 | - avatar_variant 89 | - custom_avatar_data 90 | filter: 91 | user_id: 92 | _eq: X-Hasura-User-Id 93 | check: null 94 | -------------------------------------------------------------------------------- /apps/web/src/components/DemoSinclairBasicEditor.jsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useRef} from "react"; 2 | import {useDispatch, useSelector} from "react-redux"; 3 | import {Button} from "primereact/button"; 4 | import CodeMirror from "./CodeMirror"; 5 | import {setSinclairBasicCode, runSinclairBasic} from "../redux/demo/actions"; 6 | import "../lib/syntax/zmakebas"; 7 | import {dashboardLock} from "../dashboard_lock"; 8 | import LineNumbersToggle from "./LineNumbersToggle"; 9 | import {Divider} from "primereact/divider"; 10 | 11 | export function DemoSinclairBasicEditor() { 12 | const dispatch = useDispatch(); 13 | const cmRef = useRef(null); 14 | const code = useSelector(state => state?.demo.sinclairBasicCode); 15 | const lineNumbers = useSelector((state) => state?.app?.lineNumbers || false); 16 | 17 | const options = { 18 | mode: 'text/x-zmakebas', 19 | theme: 'mbo', 20 | readOnly: false, 21 | lineWrapping: false, 22 | lineNumbers: lineNumbers, 23 | matchBrackets: true, 24 | tabSize: 4, 25 | indentAuto: true 26 | }; 27 | 28 | useEffect(() => { 29 | const cm = cmRef.current.getCodeMirror(); 30 | cm.setValue(code || ''); 31 | dispatch(setSinclairBasicCode(cm.getValue())) 32 | }, []); 33 | 34 | useEffect(() => { 35 | if (cmRef.current) { 36 | const cm = cmRef.current.getCodeMirror(); 37 | cm.setOption("lineNumbers", lineNumbers); 38 | } 39 | }, [lineNumbers]); 40 | 41 | return ( 42 | <> 43 | dispatch(setSinclairBasicCode(cm.getValue()))} 47 | /> 48 |