mirror of
https://github.com/decompme/decomp.me.git
synced 2026-02-14 01:29:24 -06:00
Co-authored-by: Mark Street <22226349+mkst@users.noreply.github.com>
This commit is contained in:
1
.github/workflows/pr.yml
vendored
1
.github/workflows/pr.yml
vendored
@@ -4,7 +4,6 @@ on:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
jobs:
|
||||
reviewdog:
|
||||
name: reviewdog
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -12,6 +12,7 @@ sandbox/
|
||||
*.log
|
||||
/frontend/.next
|
||||
/frontend/.cache
|
||||
/frontend/cache
|
||||
/frontend/public/sw.*
|
||||
/frontend/public/workbox-*
|
||||
/frontend/storybook-static
|
||||
|
||||
@@ -2,6 +2,9 @@ runner:
|
||||
eslint:
|
||||
cmd: cd frontend && yarn -s run eslint src --ext .js,.jsx,.ts,.tsx -f=rdjson
|
||||
format: rdjson
|
||||
stylelint:
|
||||
cmd: cd frontend && yarn -s run stylelint 'src/**/*.css' 'src/**/*.scss'
|
||||
format: stylelint
|
||||
tsc:
|
||||
cmd: cd frontend && yarn -s run tsc
|
||||
format: tsc
|
||||
|
||||
7
.vscode/extensions.json
vendored
Normal file
7
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"EditorConfig.EditorConfig",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"stylelint.vscode-stylelint",
|
||||
]
|
||||
}
|
||||
15
.vscode/settings.json
vendored
15
.vscode/settings.json
vendored
@@ -14,4 +14,19 @@
|
||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
|
||||
"editor.formatOnSave": true,
|
||||
},
|
||||
|
||||
// stylelint
|
||||
"[css]": {
|
||||
"editor.defaultFormatter": "stylelint.vscode-stylelint",
|
||||
"editor.formatOnSave": true,
|
||||
},
|
||||
"[scss]": {
|
||||
"editor.defaultFormatter": "stylelint.vscode-stylelint",
|
||||
"editor.formatOnSave": true,
|
||||
},
|
||||
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "vscode.json-language-features",
|
||||
"editor.formatOnSave": true,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -36,6 +36,13 @@ RUN python3 -m pip install -r /backend/requirements.txt --no-cache-dir
|
||||
|
||||
COPY --from=nsjail /bin/nsjail /bin/nsjail
|
||||
|
||||
# nsjail wants to mount this so ensure it exists
|
||||
RUN mkdir -p /lib32
|
||||
|
||||
COPY compilers/download.sh /compilers/
|
||||
|
||||
RUN bash /compilers/download.sh
|
||||
|
||||
WORKDIR /backend
|
||||
|
||||
ENTRYPOINT ["/backend/docker_entrypoint.sh"]
|
||||
|
||||
6
backend/compilers/.gitignore
vendored
6
backend/compilers/.gitignore
vendored
@@ -1,4 +1,4 @@
|
||||
*
|
||||
!gcc2.8.1/
|
||||
!ido5.3/
|
||||
!ido7.1/
|
||||
!gcc2.8.1
|
||||
!ido5.3
|
||||
!ido7.1
|
||||
|
||||
@@ -13,11 +13,14 @@ else
|
||||
fi
|
||||
|
||||
# gcc2.8.1
|
||||
mkdir -p "$compiler_dir/gcc2.8.1"
|
||||
curl -L "https://github.com/pmret/gcc-papermario/releases/download/master/$os.tar.gz" | tar zx -C "$compiler_dir/gcc2.8.1"
|
||||
curl -L "https://github.com/pmret/binutils-papermario/releases/download/master/$os.tar.gz" | tar zx -C "$compiler_dir/gcc2.8.1"
|
||||
|
||||
# ido5.3
|
||||
curl -L "https://github.com/ethteck/ido-static-recomp/releases/download/master/ido-5.3-recomp-$ido_os-latest.tar.gz" | tar zx -C "$compiler_dir/ido5.3/"
|
||||
mkdir -p "$compiler_dir/ido5.3"
|
||||
curl -L "https://github.com/ethteck/ido-static-recomp/releases/download/master/ido-5.3-recomp-$ido_os-latest.tar.gz" | tar zx -C "$compiler_dir/ido5.3"
|
||||
|
||||
# ido7.1
|
||||
curl -L "https://github.com/ethteck/ido-static-recomp/releases/download/master/ido-7.1-recomp-$ido_os-latest.tar.gz" | tar zx -C "$compiler_dir/ido7.1/"
|
||||
mkdir -p "$compiler_dir/ido7.1"
|
||||
curl -L "https://github.com/ethteck/ido-static-recomp/releases/download/master/ido-7.1-recomp-$ido_os-latest.tar.gz" | tar zx -C "$compiler_dir/ido7.1"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from typing import Dict, List, Optional, Set, Tuple
|
||||
from collections import OrderedDict
|
||||
from coreapp.models import Asm, Assembly, Compilation
|
||||
from coreapp import util
|
||||
from coreapp.sandbox import Sandbox
|
||||
@@ -38,9 +39,10 @@ else:
|
||||
def load_compilers() -> Dict[str, Dict[str, str]]:
|
||||
ret = {}
|
||||
|
||||
compiler_dirs = next(os.walk(settings.COMPILER_BASE_PATH))
|
||||
compilers_base = settings.BASE_DIR / "compilers"
|
||||
compiler_dirs = next(os.walk(compilers_base))
|
||||
for compiler_id in compiler_dirs[1]:
|
||||
config_path = Path(settings.COMPILER_BASE_PATH / compiler_id / "config.json")
|
||||
config_path = Path(compilers_base / compiler_id / "config.json")
|
||||
if config_path.exists():
|
||||
with open(config_path) as f:
|
||||
ret[compiler_id] = json.load(f)
|
||||
@@ -51,6 +53,7 @@ def load_compilers() -> Dict[str, Dict[str, str]]:
|
||||
@dataclass
|
||||
class Arch:
|
||||
name: str
|
||||
description: str
|
||||
assemble_cmd: Optional[str] = None
|
||||
objdump_cmd: Optional[str] = None
|
||||
nm_cmd: Optional[str] = None
|
||||
@@ -59,13 +62,15 @@ class Arch:
|
||||
def load_arches() -> Dict[str, Arch]:
|
||||
return {
|
||||
"mips": Arch(
|
||||
"MIPS (Nintendo 64)",
|
||||
"Nintendo 64",
|
||||
"MIPS (big-endian)",
|
||||
assemble_cmd='mips-linux-gnu-as -march=vr4300 -mabi=32 -o "$OUTPUT" "$INPUT"',
|
||||
objdump_cmd="mips-linux-gnu-objdump",
|
||||
nm_cmd="mips-linux-gnu-nm",
|
||||
),
|
||||
"mipsel": Arch(
|
||||
"MIPS (LE)",
|
||||
"PlayStation 2",
|
||||
"MIPS (little-endian)",
|
||||
assemble_cmd='mips-linux-gnu-as -march=mips64 -mabi=64 -o "$OUTPUT" "$INPUT"',
|
||||
objdump_cmd="mips-linux-gnu-objdump",
|
||||
nm_cmd="mips-linux-gnu-nm",
|
||||
@@ -120,17 +125,18 @@ class CompilerWrapper:
|
||||
return {k: {"arch": CompilerWrapper.arch_from_compiler(k)} for k in CompilerWrapper.available_compiler_ids()}
|
||||
|
||||
@staticmethod
|
||||
def available_arches() -> List[Tuple[str, str]]:
|
||||
def available_arches() -> OrderedDict[str, Dict[str, str]]:
|
||||
a_set: Set[str] = set()
|
||||
ret = []
|
||||
ret = OrderedDict()
|
||||
|
||||
for id in CompilerWrapper.available_compiler_ids():
|
||||
a_set.add(_compilers[id]["arch"])
|
||||
|
||||
for a in a_set:
|
||||
ret.append((a, _arches[a].name))
|
||||
|
||||
ret.sort(key=lambda x: x[0])
|
||||
for a in sorted(a_set):
|
||||
ret[a] = {
|
||||
"name": _arches[a].name,
|
||||
"description": _arches[a].description,
|
||||
}
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ class Sandbox(contextlib.AbstractContextManager["Sandbox"]):
|
||||
"--bindmount_ro", "/lib32",
|
||||
"--bindmount_ro", "/lib64",
|
||||
"--bindmount_ro", "/usr",
|
||||
"--bindmount_ro", str(settings.COMPILER_BASE_PATH),
|
||||
"--env", "PATH=/usr/bin:/bin",
|
||||
"--disable_proc", # Needed for running inside Docker
|
||||
"--time_limit", "30", # seconds
|
||||
|
||||
@@ -25,6 +25,7 @@ env = environ.Env(
|
||||
SESSION_COOKIE_SECURE=(bool, True),
|
||||
GITHUB_CLIENT_ID=(str, ""),
|
||||
GITHUB_CLIENT_SECRET=(str, ""),
|
||||
COMPILER_BASE_PATH=(str, BASE_DIR / "compilers")
|
||||
)
|
||||
|
||||
for stem in [".env.local", ".env"]:
|
||||
@@ -150,7 +151,7 @@ if DEBUG:
|
||||
else:
|
||||
SESSION_COOKIE_SAMESITE = "Lax"
|
||||
|
||||
COMPILER_BASE_PATH = BASE_DIR / "compilers"
|
||||
COMPILER_BASE_PATH = Path(env("COMPILER_BASE_PATH"))
|
||||
LOCAL_FILE_DIR = BASE_DIR / "local_files"
|
||||
|
||||
USE_SANDBOX_JAIL = env("USE_SANDBOX_JAIL")
|
||||
|
||||
@@ -21,8 +21,9 @@ services:
|
||||
DATABASE_URL: psql://decompme:decompme@postgres:5432/decompme
|
||||
SECRET_KEY: "django-insecure-nm#!8%z$$hc0wwi#m_*l9l)=m*6gs4&o_^-e5b5vj*k05&yaqc1"
|
||||
DEBUG: "on"
|
||||
ALLOWED_HOSTS: "localhost,127.0.0.1"
|
||||
ALLOWED_HOSTS: "backend,localhost,127.0.0.1"
|
||||
USE_SANDBOX_JAIL: "on"
|
||||
COMPILER_BASE_PATH: /compilers
|
||||
ports:
|
||||
- "8000:8000"
|
||||
security_opt:
|
||||
@@ -38,6 +39,8 @@ services:
|
||||
size: 64M
|
||||
frontend:
|
||||
build: frontend
|
||||
environment:
|
||||
INTERNAL_API_BASE: http://backend:8000/api
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
|
||||
3
frontend/.dockerignore
Normal file
3
frontend/.dockerignore
Normal file
@@ -0,0 +1,3 @@
|
||||
.next/
|
||||
cache/
|
||||
node_modules/
|
||||
@@ -1,18 +1,55 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals",
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"css-modules"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"next/core-web-vitals",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"semi": ["error", "never", { "beforeStatementContinuationChars": "always" }],
|
||||
"indent": ["error", 4],
|
||||
"quotes": ["error", "double"],
|
||||
"quote-props": ["error", "consistent"],
|
||||
"brace-style": "error",
|
||||
"brace-style": "off",
|
||||
"@typescript-eslint/brace-style": ["error"],
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"array-bracket-spacing": ["error", "never"],
|
||||
"no-else-return": "off",
|
||||
"no-trailing-spaces": "error",
|
||||
"no-multi-spaces": "error",
|
||||
"no-multiple-empty-lines": "error",
|
||||
"comma-dangle": "off",
|
||||
"@typescript-eslint/comma-dangle": ["error", "always-multiline"],
|
||||
"comma-spacing": "off",
|
||||
"@typescript-eslint/comma-spacing": ["error"],
|
||||
"prefer-const": ["warn", { "destructuring": "all" }],
|
||||
"arrow-parens": ["error", "as-needed"],
|
||||
"no-confusing-arrow": ["error", { "allowParens": true }],
|
||||
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
|
||||
"no-extra-semi": "off",
|
||||
"@typescript-eslint/no-extra-semi": ["error"],
|
||||
"no-empty-function": "off",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"no-unused-expressions": "off",
|
||||
"@typescript-eslint/no-unused-expressions": ["warn"],
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"keyword-spacing": "off",
|
||||
"@typescript-eslint/keyword-spacing": ["error"],
|
||||
"@typescript-eslint/member-delimiter-style": ["error", {
|
||||
"multiline": {
|
||||
"delimiter": "none",
|
||||
"requireLast": true
|
||||
},
|
||||
"singleline": {
|
||||
"delimiter": "comma",
|
||||
"requireLast": false
|
||||
},
|
||||
"multilineDetection": "brackets"
|
||||
}],
|
||||
"import/newline-after-import": "error",
|
||||
"import/no-duplicates": "error",
|
||||
"import/first": "error",
|
||||
@@ -42,7 +79,14 @@
|
||||
"pathGroupsExcludedImportTypes": ["react"],
|
||||
"alphabetize": { "order": "asc", "caseInsensitive": true }
|
||||
}
|
||||
]
|
||||
],
|
||||
"import/no-unresolved": "error",
|
||||
"import/export": "warn",
|
||||
"import/no-named-as-default": "warn",
|
||||
"import/no-named-as-default-member": "warn",
|
||||
"import/no-unused-modules": "warn",
|
||||
"css-modules/no-unused-class": "warn",
|
||||
"css-modules/no-undef-class": "warn"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
|
||||
12
frontend/.stylelintrc.json
Normal file
12
frontend/.stylelintrc.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": [
|
||||
"stylelint-config-standard",
|
||||
"stylelint-config-css-modules"
|
||||
],
|
||||
"rules": {
|
||||
"indentation": 4,
|
||||
"declaration-empty-line-before": null,
|
||||
"at-rule-no-unknown": null,
|
||||
"no-descending-specificity": null
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,14 @@ for (const envFile of [".env.local", ".env"]) {
|
||||
|
||||
process.env.NEXT_PUBLIC_API_BASE = process.env.API_BASE
|
||||
process.env.NEXT_PUBLIC_GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID
|
||||
process.env.NEXT_PUBLIC_COMMIT_HASH = execSync("git rev-parse HEAD").toString().trim()
|
||||
let git_hash
|
||||
try {
|
||||
git_hash = execSync("git rev-parse HEAD").toString().trim()
|
||||
} catch (error) {
|
||||
console.log("Unable to get git hash, assume running inside Docker")
|
||||
git_hash = "abc123"
|
||||
}
|
||||
process.env.NEXT_PUBLIC_COMMIT_HASH = git_hash
|
||||
|
||||
const withPWA = require('next-pwa')
|
||||
const runtimeCaching = require('next-pwa/cache')
|
||||
@@ -22,9 +29,14 @@ module.exports = removeImports(withPWA({
|
||||
return [
|
||||
{
|
||||
source: "/",
|
||||
destination: "/scratch",
|
||||
destination: "/scratch/new",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/scratch",
|
||||
destination: "/scratch/new",
|
||||
permanent: true,
|
||||
},
|
||||
]
|
||||
},
|
||||
async rewrites() {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"dev": "next dev --port 8080",
|
||||
"build": "next build",
|
||||
"start": "next start --port 8080",
|
||||
"lint": "next lint",
|
||||
"lint": "next lint && yarn stylelint 'src/**/*.css' 'src/**/*.scss'",
|
||||
"postinstall": "next telemetry disable > /dev/null",
|
||||
"storybook": "start-storybook -p 6006 --ci",
|
||||
"build-storybook": "build-storybook"
|
||||
@@ -43,6 +43,8 @@
|
||||
"@storybook/preset-scss": "^1.0.3",
|
||||
"@storybook/react": "^6.3.10",
|
||||
"@svgr/webpack": "^5.5.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||
"@typescript-eslint/parser": "^5.0.0",
|
||||
"autoprefixer": "^10.3.1",
|
||||
"babel-loader": "^8.2.2",
|
||||
"css-loader": "^6.4.0",
|
||||
@@ -51,6 +53,7 @@
|
||||
"eslint": "7",
|
||||
"eslint-config-next": "11.1.2",
|
||||
"eslint-formatter-rdjson": "^1.0.5",
|
||||
"eslint-plugin-css-modules": "^2.11.0",
|
||||
"eslint-plugin-react": "^7.26.1",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"monaco-editor-webpack-plugin": "^5.0.0",
|
||||
@@ -61,6 +64,9 @@
|
||||
"sass-loader": "^12.1.0",
|
||||
"storybook-dark-mode": "^1.0.8",
|
||||
"style-loader": "^3.3.0",
|
||||
"stylelint": "^13.13.1",
|
||||
"stylelint-config-css-modules": "^2.2.0",
|
||||
"stylelint-config-standard": "^22.0.0",
|
||||
"typescript": "^4.4.2",
|
||||
"webpack": "5"
|
||||
}
|
||||
|
||||
68
frontend/src/components/ArchSelect/ArchSelect.module.scss
Normal file
68
frontend/src/components/ArchSelect/ArchSelect.module.scss
Normal file
@@ -0,0 +1,68 @@
|
||||
.container {
|
||||
list-style: none;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.arch {
|
||||
flex: 1;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
padding: 16px;
|
||||
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
background: var(--g300);
|
||||
color: var(--g1200);
|
||||
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
white-space: nowrap;
|
||||
|
||||
transition: border-color 0.1s, background 0.15s, color 0.2s;
|
||||
|
||||
svg {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
||||
will-change: filter;
|
||||
filter: grayscale(100%);
|
||||
transition: filter 0.2s;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.selected {
|
||||
border-color: var(--g400);
|
||||
color: var(--g2000);
|
||||
|
||||
svg {
|
||||
filter: grayscale(0%);
|
||||
}
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: var(--g400);
|
||||
}
|
||||
}
|
||||
|
||||
.labelContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.consoleName {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.archName {
|
||||
font-size: 0.8em;
|
||||
opacity: 0.6;
|
||||
}
|
||||
42
frontend/src/components/ArchSelect/ArchSelect.tsx
Normal file
42
frontend/src/components/ArchSelect/ArchSelect.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import classNames from "classnames"
|
||||
|
||||
import styles from "./ArchSelect.module.scss"
|
||||
import LogoN64 from "./n64.svg"
|
||||
import LogoPS2 from "./ps2.svg"
|
||||
|
||||
const ICONS = {
|
||||
"mips": <LogoN64 />,
|
||||
"mipsel": <LogoPS2 />,
|
||||
}
|
||||
|
||||
export type Props = {
|
||||
arches: {
|
||||
[key: string]: {
|
||||
name: string
|
||||
description: string
|
||||
}
|
||||
}
|
||||
value: string
|
||||
className?: string
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
|
||||
export default function ArchSelect({ arches, value, onChange, className }: Props) {
|
||||
if (!value)
|
||||
onChange("mips")
|
||||
|
||||
|
||||
return <ul className={classNames(styles.container, className)}>
|
||||
{Object.entries(arches).map(([key, arch]) => <li
|
||||
key={key}
|
||||
className={classNames(styles.arch, { [styles.selected]: value === key })}
|
||||
onClick={() => onChange(key)}
|
||||
>
|
||||
{ICONS[key]}
|
||||
<div className={styles.labelContainer}>
|
||||
<div className={styles.consoleName}>{arch.name}</div>
|
||||
<div className={styles.archName}>{arch.description}</div>
|
||||
</div>
|
||||
</li>)}
|
||||
</ul>
|
||||
}
|
||||
4
frontend/src/components/ArchSelect/index.ts
Normal file
4
frontend/src/components/ArchSelect/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import ArchSelect, { Props as ArchSelectProps } from "./ArchSelect"
|
||||
|
||||
export type Props = ArchSelectProps
|
||||
export default ArchSelect
|
||||
19
frontend/src/components/ArchSelect/n64.svg
Normal file
19
frontend/src/components/ArchSelect/n64.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg version="1.1" viewBox="0 0 240 240" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(-142.51 -9.4602)" stroke-width=".46815">
|
||||
<path d="m318.38 63.882-14.986 23.662 43.806 62.326v-74.89l-28.819-11.099z" color="#000000" fill="#069330" style="-inkscape-stroke:none"/>
|
||||
<path d="m226.95 82.877-11.174-22.255-31.32 12.39 32.183 61.844v-47.206z" color="#000000" fill="#fe2015" style="-inkscape-stroke:none"/>
|
||||
<g fill="#ffc001">
|
||||
<path d="m318.38 56.631 32.148 11.826 31.821-11.827-29.506-11.827-34.463 11.827z" color="#000000" style="-inkscape-stroke:none"/>
|
||||
<path d="m300.65 21.922-30.971-12.308-30.969 12.157 30.971 12.308z" color="#000000" style="-inkscape-stroke:none"/>
|
||||
<path d="m175.43 40.375-32.766 13.162 36.995 12.858 30.479-12.858z" color="#000000" style="-inkscape-stroke:none"/>
|
||||
<path d="m262.74 104.58 34.725-13.448-34.725-14.403-34.725 15.358z" color="#000000" style="-inkscape-stroke:none"/>
|
||||
</g>
|
||||
<path d="m244.5 76.038 19.291-36.262-29.506-12.471-14.177 26.232 12.991 27.215z" color="#000000" fill="#069330" style="-inkscape-stroke:none"/>
|
||||
<path d="m262.74 68.877 34.917 13.531 7.989-13.531v-41.316l-32.329 12.288-10.576 19.304v9.7234z" color="#000000" fill="#011da9" style="-inkscape-stroke:none"/>
|
||||
<path d="m346.53 215.52-40.778-53.751v22.406l15.34 20.983 25.438 10.362z" color="#000000" fill="#fe2015" style="-inkscape-stroke:none"/>
|
||||
<path d="m257.42 249.31v-136.24l-33.787-13.602v67.877l-49.048-93.951-31.927-12.509v139.25l31.79 13.734v-75.901l49.055 96.825z" color="#000000" fill="#069330" style="-inkscape-stroke:none"/>
|
||||
<path d="m200.48 204.94-18.37-34.309v43.239z" color="#000000" fill="#011da9" style="-inkscape-stroke:none"/>
|
||||
<path d="m350.85 211.61-51.2-68.088v90.06l-32.486 15.723v-136.24l34.637-13.602 52.511 70.095v-94.581l28.036-10.359v134.25l-31.499 12.738z" color="#000000" fill="#011da9" style="-inkscape-stroke:none"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
44
frontend/src/components/ArchSelect/ps2.svg
Normal file
44
frontend/src/components/ArchSelect/ps2.svg
Normal file
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg data-name="Layer 1" version="1.1" viewBox="0 0 235 235" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<style>.cls-1, .cls-2, .cls-3, .cls-4 {
|
||||
fill-rule: evenodd;
|
||||
}
|
||||
|
||||
.cls-1 {
|
||||
fill: url(#linear-gradient);
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: url(#linear-gradient-2);
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: url(#linear-gradient-3);
|
||||
}</style>
|
||||
<linearGradient id="linear-gradient" x1="116.84" x2="116.84" y1="129.72" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#23bcee" offset="0"/>
|
||||
<stop stop-color="#24b0e5" offset=".07"/>
|
||||
<stop stop-color="#2785c3" offset=".36"/>
|
||||
<stop stop-color="#2965aa" offset=".63"/>
|
||||
<stop stop-color="#2b529b" offset=".85"/>
|
||||
<stop stop-color="#2b4b96" offset="1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear-gradient-2" x1="634.51" x2="634.51" y1="125.33" y2="3.46" gradientTransform="translate(.245 52.64)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#09b2cf" offset="0"/>
|
||||
<stop stop-color="#304e97" offset="1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear-gradient-3" x1="358" x2="358" y1="129.72" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#09b2cf" offset="0"/>
|
||||
<stop stop-color="#178fbb" offset=".2"/>
|
||||
<stop stop-color="#2273ab" offset=".4"/>
|
||||
<stop stop-color="#2a5ea0" offset=".6"/>
|
||||
<stop stop-color="#2e5299" offset=".8"/>
|
||||
<stop stop-color="#304e97" offset="1"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<title>PlayStation 2 logo</title>
|
||||
<g transform="translate(-517.25)">
|
||||
<path class="cls-2" d="m517.5 52.64v9.15h225.37v50.72h-225.37v69.85h234.51v-9.14h-224.52v-51.56h224.52v-69.02z" fill="url(#linear-gradient-2)" fill-rule="evenodd"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -1,6 +1,6 @@
|
||||
.errorPopup {
|
||||
font-size: 0.8rem;
|
||||
background: #bb4444;
|
||||
background: #b44;
|
||||
color: var(--a800);
|
||||
padding: 1rem;
|
||||
border-radius: 0.4rem;
|
||||
@@ -20,11 +20,11 @@
|
||||
.label {
|
||||
opacity: 1;
|
||||
transform: initial;
|
||||
transition: opacity .1s ease, transform .1s ease;
|
||||
transition: opacity 0.1s ease, transform 0.1s ease;
|
||||
|
||||
.isLoading & {
|
||||
opacity: 0;
|
||||
transform:scale(0.9);
|
||||
transform: scale(0.9);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
transition: opacity .1s ease, transform .2s ease;
|
||||
transition: opacity 0.1s ease, transform 0.2s ease;
|
||||
|
||||
.isLoading & {
|
||||
opacity: initial;
|
||||
|
||||
@@ -9,10 +9,10 @@ import Button, { Props as ButtonProps } from "./Button"
|
||||
import LoadingSpinner from "./loading.svg"
|
||||
|
||||
export interface Props extends ButtonProps {
|
||||
onClick: () => Promise<unknown>,
|
||||
forceLoading?: boolean,
|
||||
errorPlacement?: import("react-laag/dist/PlacementType").PlacementType,
|
||||
children: ReactNode,
|
||||
onClick: () => Promise<unknown>
|
||||
forceLoading?: boolean
|
||||
errorPlacement?: import("react-laag/dist/PlacementType").PlacementType
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export default function AsyncButton(props: Props) {
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
.btn {
|
||||
border-radius: 4px;
|
||||
padding: .6em 1em;
|
||||
padding: 0.6em 1em;
|
||||
|
||||
font-size: .8rem;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
|
||||
user-select: none;
|
||||
appearance: none;
|
||||
|
||||
display: inline-flex;
|
||||
gap: .5em;
|
||||
gap: 0.5em;
|
||||
align-items: center;
|
||||
|
||||
background: transparent;
|
||||
color: var(--a800);
|
||||
border: 1px solid var(--g500);
|
||||
|
||||
transition: color .2s ease, background .15s ease, border-color .15s ease;
|
||||
transition: color 0.2s ease, background 0.15s ease, border-color 0.15s ease;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.7;
|
||||
|
||||
@@ -28,11 +28,11 @@ const Button = forwardRef(function Button({
|
||||
})
|
||||
|
||||
export type Props = {
|
||||
children: React.ReactNode,
|
||||
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void,
|
||||
className?: string,
|
||||
disabled?: boolean,
|
||||
primary?: boolean,
|
||||
children: React.ReactNode
|
||||
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void
|
||||
className?: string
|
||||
disabled?: boolean
|
||||
primary?: boolean
|
||||
}
|
||||
|
||||
export default Button
|
||||
|
||||
60
frontend/src/components/Diff/Diff.module.css
Normal file
60
frontend/src/components/Diff/Diff.module.css
Normal file
@@ -0,0 +1,60 @@
|
||||
.container {
|
||||
padding: 8px;
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
|
||||
scrollbar-color: #fff3 transparent;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.diff {
|
||||
border-spacing: 1em 0;
|
||||
}
|
||||
|
||||
.diff,
|
||||
.log {
|
||||
display: block;
|
||||
user-select: text;
|
||||
border: none;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
white-space: pre;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.lineNumber {
|
||||
color: grey;
|
||||
font-size: 0.7em;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.immediate { color: #6d6dff; }
|
||||
.stack { color: #e3fc45; }
|
||||
.register { color: #e3fc45; }
|
||||
|
||||
.delay_slot {
|
||||
font-weight: bold;
|
||||
color: #969896;
|
||||
}
|
||||
|
||||
.diff_change { color: #6d6dff; }
|
||||
.diff_add { color: #45bd00; }
|
||||
.diff_remove { color: #c82829; }
|
||||
.source_filename { font-weight: bold; }
|
||||
|
||||
.source_function {
|
||||
font-weight: bold;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.source_other { font-style: italic; }
|
||||
.source_line_num { font-style: italic; }
|
||||
.rotation0 { color: magenta; }
|
||||
.rotation1 { color: cyan; }
|
||||
.rotation2 { color: rgb(0, 212, 0); }
|
||||
.rotation3 { color: red; }
|
||||
.rotation4 { color: lightyellow; }
|
||||
.rotation5 { color: lightpink; }
|
||||
.rotation6 { color: lightcyan; }
|
||||
.rotation7 { color: lightgreen; }
|
||||
.rotation8 { color: grey; }
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint css-modules/no-unused-class: off */
|
||||
|
||||
import * as api from "../../lib/api"
|
||||
|
||||
@@ -18,7 +19,7 @@ function FormatDiffText({ texts }: { texts: api.DiffText[] }) {
|
||||
}
|
||||
|
||||
export type Props = {
|
||||
compilation: api.Compilation,
|
||||
compilation: api.Compilation
|
||||
}
|
||||
|
||||
export default function Diff({ compilation }: Props) {
|
||||
4
frontend/src/components/Diff/index.ts
Normal file
4
frontend/src/components/Diff/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import Diff, { Props as DiffProps } from "./Diff"
|
||||
|
||||
export type Props = DiffProps
|
||||
export default Diff
|
||||
@@ -14,8 +14,8 @@ import getTheme from "./monacoTheme"
|
||||
const isMobile = mobile()
|
||||
|
||||
interface Props extends MonacoEditorProps {
|
||||
bubbleSuspense?: boolean,
|
||||
useLoadingSpinner?: boolean,
|
||||
bubbleSuspense?: boolean
|
||||
useLoadingSpinner?: boolean
|
||||
}
|
||||
|
||||
const MonacoEditor = isMobile ? null : dynamic(() => import("./MonacoEditor"), {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
.container {
|
||||
flex-grow: 1;
|
||||
user-select: initial;
|
||||
|
||||
/* fix border-radius overflow */
|
||||
overflow: hidden;
|
||||
|
||||
& :global(.monaco-editor),
|
||||
& :global(.monaco-editor-background),
|
||||
& :global(.margin),
|
||||
@@ -15,6 +15,14 @@
|
||||
& :global(.monaco-hover) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& :global(.mac .monaco-mouse-cursor-text) {
|
||||
cursor: text !important;
|
||||
}
|
||||
|
||||
& :global(.view-lines) {
|
||||
user-select: initial;
|
||||
}
|
||||
}
|
||||
|
||||
.readonly {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useEffect, useState, useRef } from "react"
|
||||
import { useEffect, useState, useRef, MutableRefObject } from "react"
|
||||
|
||||
import classNames from "classnames"
|
||||
import * as monaco from "monaco-editor"
|
||||
import { editor } from "monaco-editor"
|
||||
import { editor, languages } from "monaco-editor"
|
||||
|
||||
import * as c from "./language/c"
|
||||
import * as mips from "./language/mips"
|
||||
@@ -15,13 +14,13 @@ if (typeof window === "undefined") {
|
||||
throw new Error("Editor component does not work with SSR, use next/dynamic with { ssr: false }")
|
||||
}
|
||||
|
||||
monaco.languages.register({ id: "decompme_c" })
|
||||
monaco.languages.setLanguageConfiguration("decompme_c", c.conf)
|
||||
monaco.languages.setMonarchTokensProvider("decompme_c", c.language)
|
||||
languages.register({ id: "decompme_c" })
|
||||
languages.setLanguageConfiguration("decompme_c", c.conf)
|
||||
languages.setMonarchTokensProvider("decompme_c", c.language)
|
||||
|
||||
monaco.languages.register({ id: "decompme_mips" })
|
||||
monaco.languages.setLanguageConfiguration("decompme_mips", mips.conf)
|
||||
monaco.languages.setMonarchTokensProvider("decompme_mips", mips.language)
|
||||
languages.register({ id: "decompme_mips" })
|
||||
languages.setLanguageConfiguration("decompme_mips", mips.conf)
|
||||
languages.setMonarchTokensProvider("decompme_mips", mips.language)
|
||||
|
||||
function convertLanguage(language: string) {
|
||||
if (language === "c")
|
||||
@@ -32,30 +31,34 @@ function convertLanguage(language: string) {
|
||||
return "plaintext"
|
||||
}
|
||||
|
||||
export type EditorInstance = editor.IStandaloneCodeEditor;
|
||||
|
||||
export type Props = {
|
||||
className?: string,
|
||||
className?: string
|
||||
|
||||
// This is a controlled component
|
||||
value: string,
|
||||
onChange?: (value: string) => void,
|
||||
value: string
|
||||
onChange?: (value: string) => void
|
||||
|
||||
instanceRef?: MutableRefObject<editor.IStandaloneCodeEditor>
|
||||
|
||||
// Options
|
||||
language: "c" | "mips",
|
||||
lineNumbers?: boolean,
|
||||
showMargin?: boolean,
|
||||
padding?: number, // css
|
||||
language: "c" | "mips"
|
||||
lineNumbers?: boolean
|
||||
showMargin?: boolean
|
||||
padding?: number // css
|
||||
}
|
||||
|
||||
export default function Editor({ value, onChange, className, showMargin, padding, language, lineNumbers }: Props) {
|
||||
export default function Editor({ value, onChange, className, showMargin, padding, language, lineNumbers, instanceRef }: Props) {
|
||||
const isReadOnly = typeof onChange === "undefined"
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const [editor, setEditor] = useState<editor.IStandaloneCodeEditor | null>(null)
|
||||
const [editorInstance, setEditorInstance] = useState<editor.IStandaloneCodeEditor | null>(null)
|
||||
|
||||
// Effect to set up the editor. This is run once when the component is mounted.
|
||||
useEffect(() => {
|
||||
monaco.editor.defineTheme("custom", monacoTheme())
|
||||
editor.defineTheme("custom", monacoTheme())
|
||||
|
||||
const editor = monaco.editor.create(containerRef.current, {
|
||||
const editorInstance = editor.create(containerRef.current, {
|
||||
language: convertLanguage(language),
|
||||
value,
|
||||
theme: "custom",
|
||||
@@ -83,12 +86,14 @@ export default function Editor({ value, onChange, className, showMargin, padding
|
||||
glyphMargin: !!showMargin,
|
||||
folding: !!showMargin,
|
||||
lineDecorationsWidth: padding ?? (showMargin ? 10 : 0),
|
||||
lineNumbersMinChars: showMargin ? 5 : 0,
|
||||
lineNumbersMinChars: showMargin ? 2 : 0,
|
||||
automaticLayout: true,
|
||||
})
|
||||
setEditor(editor)
|
||||
setEditorInstance(editorInstance)
|
||||
if (instanceRef)
|
||||
instanceRef.current = editorInstance
|
||||
|
||||
const model = editor.getModel()
|
||||
const model = editorInstance.getModel()
|
||||
if (model) {
|
||||
model.onDidChangeContent(() => {
|
||||
if (onChange)
|
||||
@@ -98,33 +103,42 @@ export default function Editor({ value, onChange, className, showMargin, padding
|
||||
console.error("monaco editor has no model")
|
||||
}
|
||||
|
||||
return () => editor.dispose()
|
||||
return () => editorInstance.dispose()
|
||||
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// Update value.
|
||||
useEffect(() => {
|
||||
const model = editor?.getModel()
|
||||
const model = editorInstance?.getModel()
|
||||
|
||||
// Only update the model value if it is different; otherwise, the
|
||||
// model state will be reset every time the user types!
|
||||
if (model && model.getValue() !== value) {
|
||||
console.log("editor value reset")
|
||||
console.warn("editor value reset")
|
||||
model.setValue(value)
|
||||
}
|
||||
}, [editor, value])
|
||||
}, [editorInstance, value])
|
||||
|
||||
// Update language.
|
||||
useEffect(() => {
|
||||
const model = editor?.getModel()
|
||||
const model = editorInstance?.getModel()
|
||||
|
||||
if (model) {
|
||||
monaco.editor.setModelLanguage(model, convertLanguage(language))
|
||||
editor.setModelLanguage(model, convertLanguage(language))
|
||||
}
|
||||
}, [editor, language])
|
||||
}, [editorInstance, language])
|
||||
|
||||
useEffect(() => {
|
||||
editor?.updateOptions({ lineNumbers: lineNumbers ? "on" : "off" })
|
||||
}, [editor, lineNumbers])
|
||||
editorInstance?.updateOptions({ lineNumbers: lineNumbers ? "on" : "off" })
|
||||
}, [editorInstance, lineNumbers])
|
||||
|
||||
useEffect(() => {
|
||||
editorInstance?.updateOptions({
|
||||
glyphMargin: !!showMargin,
|
||||
folding: !!showMargin,
|
||||
lineDecorationsWidth: padding ?? (showMargin ? 10 : 0),
|
||||
lineNumbersMinChars: showMargin ? 2 : 0,
|
||||
})
|
||||
}, [editorInstance, padding, showMargin])
|
||||
|
||||
return <div
|
||||
ref={containerRef}
|
||||
@@ -154,7 +168,7 @@ export default function Editor({ value, onChange, className, showMargin, padding
|
||||
e.stopPropagation()
|
||||
|
||||
if (e.shiftKey)
|
||||
editor?.trigger("", "editor.action.quickCommand", "")
|
||||
editorInstance?.trigger("", "editor.action.quickCommand", "")
|
||||
//console.log(editor.getSupportedActions())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { languages } from "monaco-editor"
|
||||
export const conf: languages.LanguageConfiguration = {
|
||||
comments: {
|
||||
lineComment: "//",
|
||||
blockComment: ["/*", "*/"]
|
||||
blockComment: ["/*", "*/"],
|
||||
},
|
||||
brackets: [
|
||||
["{", "}"],
|
||||
@@ -15,20 +15,20 @@ export const conf: languages.LanguageConfiguration = {
|
||||
{ open: "{", close: "}" },
|
||||
{ open: "(", close: ")" },
|
||||
{ open: "'", close: "'", notIn: ["string", "comment"] },
|
||||
{ open: "\"", close: "\"", notIn: ["string"] }
|
||||
{ open: "\"", close: "\"", notIn: ["string"] },
|
||||
],
|
||||
surroundingPairs: [
|
||||
{ open: "{", close: "}" },
|
||||
{ open: "[", close: "]" },
|
||||
{ open: "(", close: ")" },
|
||||
{ open: "\"", close: "\"" },
|
||||
{ open: "'", close: "'" }
|
||||
{ open: "'", close: "'" },
|
||||
],
|
||||
folding: {
|
||||
markers: {
|
||||
start: new RegExp("^\\s*#pragma\\s+region\\b"),
|
||||
end: new RegExp("^\\s*#pragma\\s+endregion\\b")
|
||||
}
|
||||
end: new RegExp("^\\s*#pragma\\s+endregion\\b"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ export const language: languages.IMonarchLanguage = {
|
||||
{ token: "delimiter.curly", open: "{", close: "}" },
|
||||
{ token: "delimiter.parenthesis", open: "(", close: ")" },
|
||||
{ token: "delimiter.square", open: "[", close: "]" },
|
||||
{ token: "delimiter.angle", open: "<", close: ">" }
|
||||
{ token: "delimiter.angle", open: "<", close: ">" },
|
||||
],
|
||||
|
||||
keywords: [
|
||||
@@ -309,9 +309,9 @@ export const language: languages.IMonarchLanguage = {
|
||||
cases: {
|
||||
"@keywords": { token: "keyword.$0" },
|
||||
"@types": { token: "storage.type.$0" },
|
||||
"@default": "identifier"
|
||||
}
|
||||
}
|
||||
"@default": "identifier",
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
// The preprocessor checks must be before whitespace as they check /^\s*#/ which
|
||||
@@ -337,9 +337,9 @@ export const language: languages.IMonarchLanguage = {
|
||||
cases: {
|
||||
"@comparisonOperators": { token: "operator.comparison" },
|
||||
"@operators": { token: "operator" },
|
||||
"@default": { token: "delimiter" }
|
||||
}
|
||||
}
|
||||
"@default": { token: "delimiter" },
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
// numbers
|
||||
@@ -361,7 +361,7 @@ export const language: languages.IMonarchLanguage = {
|
||||
// characters
|
||||
[/'[^\\']'/, "string"],
|
||||
[/(')(@escapes)(')/, ["string", "string.escape", "string"]],
|
||||
[/'/, "string.invalid"]
|
||||
[/'/, "string.invalid"],
|
||||
],
|
||||
|
||||
whitespace: [
|
||||
@@ -369,33 +369,33 @@ export const language: languages.IMonarchLanguage = {
|
||||
[/\/\*\*(?!\/)/, "comment.doc", "@doccomment"],
|
||||
[/\/\*/, "comment", "@comment"],
|
||||
[/\/\/.*\\$/, "comment", "@linecomment"],
|
||||
[/\/\/.*$/, "comment"]
|
||||
[/\/\/.*$/, "comment"],
|
||||
],
|
||||
|
||||
comment: [
|
||||
[/[^/*]+/, "comment"],
|
||||
[/\*\//, "comment", "@pop"],
|
||||
[/[/*]/, "comment"]
|
||||
[/[/*]/, "comment"],
|
||||
],
|
||||
|
||||
//For use with continuous line comments
|
||||
linecomment: [
|
||||
[/.*[^\\]$/, "comment", "@pop"],
|
||||
[/[^]+/, "comment"]
|
||||
[/[^]+/, "comment"],
|
||||
],
|
||||
|
||||
//Identical copy of comment above, except for the addition of .doc
|
||||
doccomment: [
|
||||
[/[^/*]+/, "comment.doc"],
|
||||
[/\*\//, "comment.doc", "@pop"],
|
||||
[/[/*]/, "comment.doc"]
|
||||
[/[/*]/, "comment.doc"],
|
||||
],
|
||||
|
||||
string: [
|
||||
[/[^\\"]+/, "string"],
|
||||
[/@escapes/, "string.escape"],
|
||||
[/\\./, "string.escape.invalid"],
|
||||
[/"/, "string", "@pop"]
|
||||
[/"/, "string", "@pop"],
|
||||
],
|
||||
|
||||
raw: [
|
||||
@@ -407,13 +407,13 @@ export const language: languages.IMonarchLanguage = {
|
||||
"string.raw",
|
||||
"string.raw.end",
|
||||
"string.raw.end",
|
||||
{ token: "string.raw.end", next: "@pop" }
|
||||
{ token: "string.raw.end", next: "@pop" },
|
||||
],
|
||||
"@default": ["string.raw", "string.raw", "string.raw", "string.raw"]
|
||||
}
|
||||
}
|
||||
"@default": ["string.raw", "string.raw", "string.raw", "string.raw"],
|
||||
},
|
||||
},
|
||||
],
|
||||
[/.*/, "string.raw"]
|
||||
[/.*/, "string.raw"],
|
||||
],
|
||||
|
||||
annotation: [
|
||||
@@ -422,7 +422,7 @@ export const language: languages.IMonarchLanguage = {
|
||||
[/[a-zA-Z0-9_]+/, "annotation"],
|
||||
[/[,:]/, "delimiter"],
|
||||
[/[()]/, "@brackets"],
|
||||
[/\]\s*\]/, { token: "annotation", next: "@pop" }]
|
||||
[/\]\s*\]/, { token: "annotation", next: "@pop" }],
|
||||
],
|
||||
|
||||
include: [
|
||||
@@ -432,8 +432,8 @@ export const language: languages.IMonarchLanguage = {
|
||||
"",
|
||||
"keyword.directive.include.begin",
|
||||
"string.include.identifier",
|
||||
{ token: "keyword.directive.include.end", next: "@pop" }
|
||||
] as languages.IMonarchLanguageAction
|
||||
{ token: "keyword.directive.include.end", next: "@pop" },
|
||||
] as languages.IMonarchLanguageAction,
|
||||
],
|
||||
[
|
||||
/(\s*)(")([^"]*)(")/,
|
||||
@@ -441,9 +441,9 @@ export const language: languages.IMonarchLanguage = {
|
||||
"",
|
||||
"keyword.directive.include.begin",
|
||||
"string.include.identifier",
|
||||
{ token: "keyword.directive.include.end", next: "@pop" }
|
||||
] as languages.IMonarchLanguageAction
|
||||
]
|
||||
]
|
||||
}
|
||||
{ token: "keyword.directive.include.end", next: "@pop" },
|
||||
] as languages.IMonarchLanguageAction,
|
||||
],
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -3,14 +3,14 @@ import type { languages } from "monaco-editor"
|
||||
export const conf: languages.LanguageConfiguration = {
|
||||
comments: {
|
||||
lineComment: "#",
|
||||
blockComment: ["/*", "*/"]
|
||||
blockComment: ["/*", "*/"],
|
||||
},
|
||||
brackets: [
|
||||
["(", ")"],
|
||||
],
|
||||
autoClosingPairs: [
|
||||
{ open: "(", close: ")" },
|
||||
{ open: "\"", close: "\"", notIn: ["string"] }
|
||||
{ open: "\"", close: "\"", notIn: ["string"] },
|
||||
],
|
||||
surroundingPairs: [
|
||||
{ open: "(", close: ")" },
|
||||
@@ -32,7 +32,7 @@ export const language: languages.IMonarchLanguage = {
|
||||
|
||||
instructions: [
|
||||
"lb", "lbu", "ld", "ldl", "ldr", "lh", "lhu", "ll", "lld", "lw", "lwl", "lwr", "lwu", "sb", "sc", "scd", "sd", "sdl", "sdr", "sh", "sw", "swl", "swr", "sync", "add", "addi", "addiu", "addu", "and", "andi", "dadd", "daddi", "daddiu", "daddu", "ddiv", "ddivu", "div", "divu", "dmult", "dmultu", "dsll", "dsll32", "dsllv", "dsra", "dsra32", "dsrav", "dsrl", "dsrl32", "dsrlv", "dsub", "dsubu", "lui", "mfhi", "mflo", "mthi", "mtlo", "mult", "multu", "nor", "or", "ori", "sll", "sllv", "slt", "slti", "sltiu", "sltu", "sra", "srav", "srl", "srlv", "sub", "subu", "xor", "xori", "beq", "beql", "bgez", "bgezal", "bgezall", "bgezl", "bgtz", "bgtzl", "blez", "blezl", "bltz", "bltzal", "bltzall", "bltzl", "bne", "bnel", "j", "jal", "jalr", "jr", "break", "syscall", "teq", "teqi", "tge", "tgei", "tgeiu", "tgeu", "tlt", "tlti", "tltiu", "tltu", "tne", "tnei", "cache", "dmfc0", "dmtc0", "eret", "mfc0", "mtc0", "tlbp", "tlbr", "tlbwi", "tlbwr", "bc1f", "bc1fl", "bc1t", "bc1tl", "cfc1", "ctc1", "dmfc1", "dmtc1", "ldc1", "lwc1", "mfc1", "mtc1", "sdc1", "swc1",
|
||||
"beqz", "bnez", "negu",
|
||||
"beqz", "bnez", "negu", "nop",
|
||||
],
|
||||
|
||||
registers: [
|
||||
@@ -58,18 +58,18 @@ export const language: languages.IMonarchLanguage = {
|
||||
"jal": { token: "function" },
|
||||
"@instructions": { token: "support.function.$0" },
|
||||
"@keywords": { token: "keyword.$0" },
|
||||
"@default": "identifier"
|
||||
}
|
||||
}
|
||||
"@default": "identifier",
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
/\$\w+/,
|
||||
{
|
||||
cases: {
|
||||
"@registers": { token: "entity.name.register.$0" },
|
||||
"@default": "identifier"
|
||||
}
|
||||
}
|
||||
"@default": "identifier",
|
||||
},
|
||||
},
|
||||
],
|
||||
[/%(hi|lo)/, "macro"],
|
||||
[/\.\w+/, { token: "keyword.directive" }],
|
||||
@@ -96,33 +96,33 @@ export const language: languages.IMonarchLanguage = {
|
||||
// characters
|
||||
[/'[^\\']'/, "string"],
|
||||
[/(')(@escapes)(')/, ["string", "string.escape", "string"]],
|
||||
[/'/, "string.invalid"]
|
||||
[/'/, "string.invalid"],
|
||||
],
|
||||
|
||||
whitespace: [
|
||||
[/[ \t\r\n]+/, ""],
|
||||
[/\/\*/, "comment", "@comment"],
|
||||
[/#.*\\$/, "comment", "@linecomment"],
|
||||
[/#.*$/, "comment"]
|
||||
[/#.*$/, "comment"],
|
||||
],
|
||||
|
||||
comment: [
|
||||
[/[^/*]+/, "comment"],
|
||||
[/\*\//, "comment", "@pop"],
|
||||
[/[/*]/, "comment"]
|
||||
[/[/*]/, "comment"],
|
||||
],
|
||||
|
||||
//For use with continuous line comments
|
||||
linecomment: [
|
||||
[/.*[^#]$/, "comment", "@pop"],
|
||||
[/[^]+/, "comment"]
|
||||
[/[^]+/, "comment"],
|
||||
],
|
||||
|
||||
string: [
|
||||
[/[^\\"]+/, "string"],
|
||||
[/@escapes/, "string.escape"],
|
||||
[/\\./, "string.escape.invalid"],
|
||||
[/"/, "string", "@pop"]
|
||||
[/"/, "string", "@pop"],
|
||||
],
|
||||
|
||||
raw: [
|
||||
@@ -134,13 +134,13 @@ export const language: languages.IMonarchLanguage = {
|
||||
"string.raw",
|
||||
"string.raw.end",
|
||||
"string.raw.end",
|
||||
{ token: "string.raw.end", next: "@pop" }
|
||||
{ token: "string.raw.end", next: "@pop" },
|
||||
],
|
||||
"@default": ["string.raw", "string.raw", "string.raw", "string.raw"]
|
||||
}
|
||||
}
|
||||
"@default": ["string.raw", "string.raw", "string.raw", "string.raw"],
|
||||
},
|
||||
},
|
||||
],
|
||||
[/.*/, "string.raw"]
|
||||
[/.*/, "string.raw"],
|
||||
],
|
||||
|
||||
include: [
|
||||
@@ -150,8 +150,8 @@ export const language: languages.IMonarchLanguage = {
|
||||
"",
|
||||
"keyword.directive.include.begin",
|
||||
"string.include.identifier",
|
||||
{ token: "keyword.directive.include.end", next: "@pop" }
|
||||
] as languages.IMonarchLanguageAction
|
||||
{ token: "keyword.directive.include.end", next: "@pop" },
|
||||
] as languages.IMonarchLanguageAction,
|
||||
],
|
||||
[
|
||||
/(\s*)(")([^"]*)(")/,
|
||||
@@ -159,9 +159,9 @@ export const language: languages.IMonarchLanguage = {
|
||||
"",
|
||||
"keyword.directive.include.begin",
|
||||
"string.include.identifier",
|
||||
{ token: "keyword.directive.include.end", next: "@pop" }
|
||||
] as languages.IMonarchLanguageAction
|
||||
]
|
||||
]
|
||||
}
|
||||
{ token: "keyword.directive.include.end", next: "@pop" },
|
||||
] as languages.IMonarchLanguageAction,
|
||||
],
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export default function getTheme(): editor.IStandaloneThemeData {
|
||||
{ "token": "entity.other.attribute-name", "foreground": "FF4A98" },
|
||||
{ "token": "support.function", "foreground": "45B8FF" },
|
||||
{ "token": "storage", "foreground": "45B8FF" },
|
||||
{ "token": "macro", "foreground": "3bff6c" }
|
||||
{ "token": "macro", "foreground": "3bff6c" },
|
||||
],
|
||||
"colors": {
|
||||
"editor.foreground": "#8599b3",
|
||||
@@ -32,7 +32,7 @@ export default function getTheme(): editor.IStandaloneThemeData {
|
||||
"editor.lineHighlightBackground": "#ccccff07",
|
||||
"editorCursor.foreground": "#c9cbfc",
|
||||
"editorWhitespace.foreground": "#c9cbfc11",
|
||||
"editorLineNumber.foreground":"#ccccff31"
|
||||
}
|
||||
"editorLineNumber.foreground":"#ccccff31",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,12 +19,12 @@
|
||||
|
||||
.links {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.link {
|
||||
display: inline-flex;
|
||||
gap: .5em;
|
||||
gap: 0.5em;
|
||||
align-items: center;
|
||||
padding: 1em;
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
|
||||
.commitHash {
|
||||
text-align: center;
|
||||
font-size: .7em;
|
||||
font-size: 0.7em;
|
||||
|
||||
background: var(--g200);
|
||||
color: var(--g600);
|
||||
|
||||
@@ -31,6 +31,12 @@ export default function Footer() {
|
||||
Chat on Discord
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
<Link href="/credits">
|
||||
<a className={styles.link}>
|
||||
Credits
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className={styles.commitHash}>
|
||||
|
||||
@@ -8,7 +8,3 @@
|
||||
.avatar {
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.loginContainer, .user {
|
||||
margin-right: .5em;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import GitHubLoginButton from "../GitHubLoginButton"
|
||||
import styles from "./LoginState.module.scss"
|
||||
|
||||
export type Props = {
|
||||
onChange: (user: api.AnonymousUser | api.User) => void,
|
||||
onChange: (user: api.AnonymousUser | api.User) => void
|
||||
}
|
||||
|
||||
export default function LoginState({ onChange }: Props) {
|
||||
@@ -39,11 +39,11 @@ export default function LoginState({ onChange }: Props) {
|
||||
height={24}
|
||||
priority
|
||||
/>}
|
||||
<span>{user.name}</span>
|
||||
{/*<span>{user.username}</span>*/}
|
||||
</a>
|
||||
</Link>
|
||||
} else {
|
||||
return <div className={styles.loginContainer}>
|
||||
return <div>
|
||||
<GitHubLoginButton label="Sign in" />
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -8,15 +8,19 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: .5em;
|
||||
gap: 0.5em;
|
||||
|
||||
padding: 1vh 0.5vw;
|
||||
padding: 4px;
|
||||
|
||||
background: var(--g400);
|
||||
border-bottom: 1px solid var(--g600);
|
||||
|
||||
font-size: 90%;
|
||||
font-weight: 600;
|
||||
|
||||
@media screen and (min-width: 800px) and (min-height: 800px) {
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.item {
|
||||
@@ -26,7 +30,7 @@
|
||||
height: 100%;
|
||||
padding: 0 8px;
|
||||
|
||||
opacity: 1.0;
|
||||
opacity: 1;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
@@ -36,7 +40,7 @@
|
||||
.logo {
|
||||
@extend .item;
|
||||
|
||||
margin: 0 .5vw;
|
||||
margin: 0 0.5vw;
|
||||
padding: 0;
|
||||
|
||||
svg {
|
||||
@@ -46,11 +50,30 @@
|
||||
/* Frog blinks when you load the page or stop hovering */
|
||||
&:not(:hover) {
|
||||
@keyframes blink {
|
||||
0% { transform: scaleY(1.0); opacity: 1 }
|
||||
25% { transform: scaleY(0.1); opacity: 0 }
|
||||
50% { transform: scaleY(1.0); opacity: 1 }
|
||||
75% { transform: scaleY(0.1); opacity: 0 }
|
||||
100% { transform: scaleY(1.0); opacity: 1 }
|
||||
0% {
|
||||
transform: scaleY(1);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: scaleY(0.2);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scaleY(1);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: scaleY(0.2);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scaleY(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.frog_svg__pupilR),
|
||||
@@ -58,7 +81,7 @@
|
||||
:global(.frog_svg__eyeR),
|
||||
:global(.frog_svg__eyeL) {
|
||||
transform-origin: 0 7px;
|
||||
animation: blink .4s 2s ease;
|
||||
animation: blink 0.4s 2s ease;
|
||||
|
||||
@media (prefers-reduced-motion) {
|
||||
animation: none;
|
||||
@@ -70,7 +93,3 @@
|
||||
.grow {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.link:hover, .linkActive {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@ import Frog from "./frog.svg"
|
||||
import LoginState, { Props as LoginStateProps } from "./LoginState"
|
||||
import styles from "./Nav.module.scss"
|
||||
|
||||
const onUserChangeNop = (_user: any) => {}
|
||||
const onUserChangeNop: LoginStateProps["onChange"] = _user => {}
|
||||
|
||||
export type Props = {
|
||||
children?: React.ReactNode,
|
||||
onUserChange?: LoginStateProps["onChange"],
|
||||
children?: React.ReactNode
|
||||
onUserChange?: LoginStateProps["onChange"]
|
||||
}
|
||||
|
||||
export default function Nav({ children, onUserChange }: Props) {
|
||||
@@ -19,7 +19,7 @@ export default function Nav({ children, onUserChange }: Props) {
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
{children ?? <Link href="/scratch">
|
||||
{children ?? <Link href="/scratch/new">
|
||||
<a className={styles.item}>New scratch</a>
|
||||
</Link>}
|
||||
|
||||
|
||||
36
frontend/src/components/ScoreBadge.module.scss
Normal file
36
frontend/src/components/ScoreBadge.module.scss
Normal file
@@ -0,0 +1,36 @@
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5em;
|
||||
|
||||
min-width: 2em;
|
||||
height: 2em;
|
||||
|
||||
font-size: 0.8em;
|
||||
font-weight: 400;
|
||||
padding: 0.4em 0.8em;
|
||||
|
||||
border-radius: 999px;
|
||||
color: var(--a800);
|
||||
background: var(--a100);
|
||||
|
||||
// circle when using icons
|
||||
&.error,
|
||||
&.match {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&.error {
|
||||
background: #b93636;
|
||||
}
|
||||
|
||||
&.match {
|
||||
background: var(--accent);
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 1.2em;
|
||||
}
|
||||
24
frontend/src/components/ScoreBadge.tsx
Normal file
24
frontend/src/components/ScoreBadge.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { AlertIcon, CheckIcon } from "@primer/octicons-react"
|
||||
import classNames from "classnames"
|
||||
|
||||
import styles from "./ScoreBadge.module.scss"
|
||||
|
||||
export type Props = {
|
||||
score: number
|
||||
}
|
||||
|
||||
export default function ScoreBadge({ score }: Props) {
|
||||
if (score === -1) {
|
||||
return <div className={classNames(styles.badge, { [styles.error]: true })}>
|
||||
<AlertIcon className={styles.icon} />
|
||||
</div>
|
||||
} else if (score === 0) {
|
||||
return <div className={classNames(styles.badge, { [styles.match]: true })}>
|
||||
<CheckIcon className={styles.icon} />
|
||||
</div>
|
||||
} else {
|
||||
return <div className={styles.badge} aria-label="Score">
|
||||
{score}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
100
frontend/src/components/Scratch/AboutScratch.module.scss
Normal file
100
frontend/src/components/Scratch/AboutScratch.module.scss
Normal file
@@ -0,0 +1,100 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1em;
|
||||
cursor: default;
|
||||
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.rule {
|
||||
border: 0;
|
||||
background: var(--g300);
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
|
||||
color: var(--g1000);
|
||||
font-size: 0.9em;
|
||||
font-weight: 500;
|
||||
|
||||
small {
|
||||
color: var(--g1000);
|
||||
font-size: 0.8em;
|
||||
}
|
||||
}
|
||||
|
||||
.horizontalField {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.label {
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.scratchLinkContainer {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
|
||||
color: var(--g800);
|
||||
}
|
||||
|
||||
.scratchLink {
|
||||
cursor: pointer;
|
||||
color: var(--g1400);
|
||||
|
||||
&:hover {
|
||||
color: var(--g1700);
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.scratchLinkByText {
|
||||
padding-left: 6px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.textArea {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
outline: none !important;
|
||||
|
||||
color: var(--g1800);
|
||||
resize: none;
|
||||
|
||||
width: 100%;
|
||||
|
||||
cursor: auto;
|
||||
user-select: text;
|
||||
|
||||
&::-webkit-input-placeholder {
|
||||
color: var(--g500);
|
||||
}
|
||||
|
||||
&:not(:disabled) {
|
||||
cursor: auto;
|
||||
transition: border-color 0.15s;
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
border-color: var(--g300);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.grow {
|
||||
display: flex;
|
||||
flex-direction: inherit;
|
||||
flex: 1;
|
||||
}
|
||||
54
frontend/src/components/Scratch/AboutScratch.tsx
Normal file
54
frontend/src/components/Scratch/AboutScratch.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import Link from "next/link"
|
||||
|
||||
import * as api from "../../lib/api"
|
||||
import UserLink from "../user/UserLink"
|
||||
|
||||
import styles from "./AboutScratch.module.scss"
|
||||
|
||||
function ScratchLink({ slug }: { slug: string }) {
|
||||
const { scratch } = api.useScratch(slug)
|
||||
|
||||
return <span className={styles.scratchLinkContainer}>
|
||||
<Link href={`/scratch/${scratch.slug}`}>
|
||||
<a className={styles.scratchLink}>
|
||||
{scratch.name || "Untitled scratch"}
|
||||
</a>
|
||||
</Link>
|
||||
<span className={styles.scratchLinkByText}>by</span>
|
||||
<UserLink user={scratch.owner} />
|
||||
</span>
|
||||
}
|
||||
|
||||
export type Props = {
|
||||
scratch: api.Scratch
|
||||
setScratch?: (scratch: Partial<api.Scratch>) => void
|
||||
}
|
||||
|
||||
export default function AboutScratch({ scratch, setScratch }: Props) {
|
||||
return <div className={styles.container}>
|
||||
<div>
|
||||
<div className={styles.horizontalField}>
|
||||
<p className={styles.label}>Owner</p>
|
||||
<UserLink user={scratch.owner} />
|
||||
</div>
|
||||
{scratch.parent &&<div className={styles.horizontalField}>
|
||||
<p className={styles.label}>Fork of</p>
|
||||
<ScratchLink slug={scratch.parent} />
|
||||
</div>}
|
||||
</div>
|
||||
|
||||
<hr className={styles.rule} />
|
||||
|
||||
{setScratch || scratch.description ? <div className={styles.grow}>
|
||||
<p className={styles.label}>Description</p>
|
||||
<textarea
|
||||
className={styles.textArea}
|
||||
value={scratch.description}
|
||||
disabled={!setScratch}
|
||||
onChange={event => setScratch && setScratch({ description: event.target.value })}
|
||||
maxLength={5000}
|
||||
placeholder="Add any notes about the scratch here"
|
||||
/>
|
||||
</div> : <div />}
|
||||
</div>
|
||||
}
|
||||
95
frontend/src/components/Scratch/Scratch.module.scss
Normal file
95
frontend/src/components/Scratch/Scratch.module.scss
Normal file
@@ -0,0 +1,95 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.resizer {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.context {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.diffSection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
scrollbar-color: #fff3 transparent;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.chooseACompiler {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chooseACompilerActions {
|
||||
padding: 1em;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.editor {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: initial;
|
||||
|
||||
gap: 4px;
|
||||
padding: 4px;
|
||||
|
||||
background: var(--g300);
|
||||
border-bottom: 1px solid var(--a100);
|
||||
|
||||
@media screen and (min-width: 800px) and (min-height: 800px) {
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.scratchName {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
padding: 4px 8px;
|
||||
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
outline: none !important;
|
||||
|
||||
color: var(--g1800);
|
||||
|
||||
text-overflow: ellipsis;
|
||||
|
||||
transition: border-color 0.15s;
|
||||
|
||||
&::-webkit-input-placeholder {
|
||||
color: var(--g800);
|
||||
}
|
||||
|
||||
&:not(:disabled) {
|
||||
&:hover,
|
||||
&:active {
|
||||
border-color: var(--g400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.diffTab {
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.about {
|
||||
padding: 16px;
|
||||
max-width: 500px;
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
import { useState, useEffect, useCallback, useRef } from "react"
|
||||
|
||||
import Link from "next/link"
|
||||
import { useState, useEffect, useRef } from "react"
|
||||
|
||||
import { RepoForkedIcon, SyncIcon, UploadIcon, ArrowRightIcon } from "@primer/octicons-react"
|
||||
import * as resizer from "react-simple-resizer"
|
||||
@@ -10,14 +8,15 @@ import * as api from "../../lib/api"
|
||||
import { useSize, useWarnBeforeUnload } from "../../lib/hooks"
|
||||
import AsyncButton from "../AsyncButton"
|
||||
import Button from "../Button"
|
||||
import CompilerButton from "../compiler/CompilerButton"
|
||||
import CompilerOpts, { CompilerOptsT } from "../compiler/CompilerOpts"
|
||||
import Diff from "../diff/Diff"
|
||||
import Diff from "../Diff"
|
||||
import Editor from "../Editor"
|
||||
import { EditorInstance } from "../Editor/MonacoEditor"
|
||||
import ScoreBadge from "../ScoreBadge"
|
||||
import Tabs, { Tab } from "../Tabs"
|
||||
import UserLink from "../user/UserLink"
|
||||
|
||||
import styles from "./Scratch.module.css"
|
||||
import AboutScratch from "./AboutScratch"
|
||||
import styles from "./Scratch.module.scss"
|
||||
|
||||
const LEFT_PANE_MIN_WIDTH = 400
|
||||
const RIGHT_PANE_MIN_WIDTH = 400
|
||||
@@ -25,8 +24,8 @@ const RIGHT_PANE_MIN_WIDTH = 400
|
||||
let isClaiming = false
|
||||
|
||||
function ChooseACompiler({ arch, onCommit }: {
|
||||
arch: string,
|
||||
onCommit: (opts: CompilerOptsT) => void,
|
||||
arch: string
|
||||
onCommit: (opts: CompilerOptsT) => void
|
||||
}) {
|
||||
const [compiler, setCompiler] = useState<CompilerOptsT>()
|
||||
|
||||
@@ -47,26 +46,18 @@ function ChooseACompiler({ arch, onCommit }: {
|
||||
</div>
|
||||
}
|
||||
|
||||
function ScratchLink({ slug }: { slug: string }) {
|
||||
const { scratch } = api.useScratch(slug)
|
||||
|
||||
if (!scratch) {
|
||||
return <span />
|
||||
}
|
||||
|
||||
return <Link href={`/scratch/${scratch.slug}`}>
|
||||
<a className={styles.scratchLink}>
|
||||
{nameScratch(scratch)}
|
||||
</a>
|
||||
</Link>
|
||||
}
|
||||
|
||||
function renderRightTabs({ compilation }: {
|
||||
compilation?: api.Compilation,
|
||||
compilation?: api.Compilation
|
||||
}): React.ReactElement<typeof Tab>[] {
|
||||
console.log(compilation)
|
||||
return [
|
||||
<Tab key="diff" label="Diff">
|
||||
<Tab
|
||||
key="diff"
|
||||
label={<>
|
||||
Diff
|
||||
{compilation && <ScoreBadge score={compilation?.diff_output?.current_score ?? -1} />}
|
||||
</>}
|
||||
className={styles.diffTab}
|
||||
>
|
||||
{compilation && <Diff compilation={compilation} />}
|
||||
</Tab>,
|
||||
/*<Tab key="options" label="Options">
|
||||
@@ -75,17 +66,27 @@ function renderRightTabs({ compilation }: {
|
||||
]
|
||||
}
|
||||
|
||||
function renderLeftTabs({ scratch, isSaved, setScratch, saveScratch, forkScratch, compile }: {
|
||||
scratch: api.Scratch,
|
||||
isSaved: boolean,
|
||||
setScratch: (s: Partial<api.Scratch>) => void,
|
||||
saveScratch: () => Promise<void>,
|
||||
forkScratch: () => Promise<void>,
|
||||
compile: () => Promise<void>,
|
||||
function renderLeftTabs({ scratch, setScratch }: {
|
||||
scratch: api.Scratch
|
||||
setScratch: (s: Partial<api.Scratch>) => void
|
||||
}): React.ReactElement<typeof Tab>[] {
|
||||
const sourceEditor = useRef<EditorInstance>() // eslint-disable-line react-hooks/rules-of-hooks
|
||||
const contextEditor = useRef<EditorInstance>() // eslint-disable-line react-hooks/rules-of-hooks
|
||||
|
||||
return [
|
||||
<Tab key="source" label="Source code">
|
||||
<Tab key="about" label="About" className={styles.about}>
|
||||
<AboutScratch
|
||||
scratch={scratch}
|
||||
setScratch={scratch.owner?.is_you ? setScratch : null}
|
||||
/>
|
||||
</Tab>,
|
||||
<Tab
|
||||
key="source"
|
||||
label="Source code"
|
||||
onSelect={() => sourceEditor.current.focus()}
|
||||
>
|
||||
<Editor
|
||||
instanceRef={sourceEditor}
|
||||
className={styles.editor}
|
||||
language="c"
|
||||
value={scratch.source_code}
|
||||
@@ -97,8 +98,14 @@ function renderLeftTabs({ scratch, isSaved, setScratch, saveScratch, forkScratch
|
||||
bubbleSuspense
|
||||
/>
|
||||
</Tab>,
|
||||
<Tab key="context" label="Context" className={styles.context}>
|
||||
<Tab
|
||||
key="context"
|
||||
label="Context"
|
||||
className={styles.context}
|
||||
onSelect={() => contextEditor.current.focus()}
|
||||
>
|
||||
<Editor
|
||||
instanceRef={contextEditor}
|
||||
className={styles.editor}
|
||||
language="c"
|
||||
value={scratch.context}
|
||||
@@ -110,50 +117,38 @@ function renderLeftTabs({ scratch, isSaved, setScratch, saveScratch, forkScratch
|
||||
bubbleSuspense
|
||||
/>
|
||||
</Tab>,
|
||||
<Tab key="about" label={scratch.owner.is_you ? "Scratch settings" : "About this scratch" }>
|
||||
<div className={styles.metadata}>
|
||||
{scratch.owner && <div>
|
||||
Owner
|
||||
<UserLink user={scratch.owner} />
|
||||
</div>}
|
||||
|
||||
{scratch.parent && <div>
|
||||
Fork of <ScratchLink slug={scratch.parent} />
|
||||
</div>}
|
||||
</div>
|
||||
<Tab key="settings" label="Scratch settings">
|
||||
<CompilerOpts
|
||||
arch={scratch.arch}
|
||||
value={scratch}
|
||||
onChange={setScratch}
|
||||
/>
|
||||
</Tab>,
|
||||
]
|
||||
}
|
||||
|
||||
export function nameScratch({ slug, owner }: api.Scratch): string {
|
||||
if (owner?.is_you) {
|
||||
return "Your scratch"
|
||||
} else if (owner && !api.isAnonUser(owner) && owner?.name) {
|
||||
return `${owner?.name}'s scratch`
|
||||
} else {
|
||||
return "Untitled scratch"
|
||||
}
|
||||
}
|
||||
|
||||
export type Props = {
|
||||
slug: string,
|
||||
tryClaim?: boolean, // note: causes page reload after claiming
|
||||
slug: string
|
||||
tryClaim?: boolean // note: causes page reload after claiming
|
||||
}
|
||||
|
||||
export default function Scratch({ slug, tryClaim }: Props) {
|
||||
const container = useSize<HTMLDivElement>()
|
||||
const { scratch, savedScratch, isSaved, setScratch, saveScratch } = api.useScratch(slug)
|
||||
const { compilation, isCompiling, compile } = api.useCompilation(scratch, savedScratch, true)
|
||||
const forkScratch = api.useForkScratchAndGo(scratch)
|
||||
const forkScratch = api.useForkScratchAndGo(savedScratch, scratch)
|
||||
const [leftTab, setLeftTab] = useState("source")
|
||||
const [rightTab, setRightTab] = useState("diff")
|
||||
const [isForking, setIsForking] = useState(false)
|
||||
|
||||
// TODO: remove once scratch.compiler is no longer nullable
|
||||
const setCompilerOpts = ({ compiler, cc_opts }: CompilerOptsT) => {
|
||||
setScratch({
|
||||
compiler,
|
||||
cc_opts,
|
||||
})
|
||||
saveScratch()
|
||||
if (scratch.owner?.is_you)
|
||||
saveScratch()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@@ -173,7 +168,7 @@ export default function Scratch({ slug, tryClaim }: Props) {
|
||||
|
||||
useDeepCompareEffect(() => {
|
||||
if (scratch) {
|
||||
document.title = nameScratch(scratch)
|
||||
document.title = scratch.name || "Untitled scratch"
|
||||
|
||||
if (!isSaved) {
|
||||
document.title += " (unsaved changes)"
|
||||
@@ -183,7 +178,12 @@ export default function Scratch({ slug, tryClaim }: Props) {
|
||||
}
|
||||
}, [scratch || {}, isSaved])
|
||||
|
||||
useWarnBeforeUnload(!isSaved, "You have unsaved changes. Are you sure you want to leave?")
|
||||
useWarnBeforeUnload(
|
||||
!isSaved && !isForking,
|
||||
scratch.owner?.is_you
|
||||
? "You have not saved your changes to this scratch. Discard changes?"
|
||||
: "You have edited this scratch but not saved it in a fork. Discard changes?",
|
||||
)
|
||||
|
||||
// Claim the scratch
|
||||
if (tryClaim && !savedScratch?.owner && typeof window !== "undefined") {
|
||||
@@ -202,18 +202,13 @@ export default function Scratch({ slug, tryClaim }: Props) {
|
||||
})
|
||||
.catch(console.error)
|
||||
.then(() => {
|
||||
// Reload the entire page
|
||||
window.location.href = window.location.href
|
||||
window.location.reload()
|
||||
})
|
||||
}
|
||||
|
||||
const leftTabs = renderLeftTabs({
|
||||
scratch,
|
||||
isSaved,
|
||||
setScratch,
|
||||
saveScratch,
|
||||
forkScratch,
|
||||
compile,
|
||||
})
|
||||
|
||||
const rightTabs = renderRightTabs({
|
||||
@@ -222,11 +217,16 @@ export default function Scratch({ slug, tryClaim }: Props) {
|
||||
|
||||
return <div ref={container.ref} className={styles.container}>
|
||||
<div className={styles.toolbar}>
|
||||
<div className={styles.scratchName}>
|
||||
{nameScratch(scratch)}
|
||||
</div>
|
||||
|
||||
<span className={styles.grow} />
|
||||
<input
|
||||
className={styles.scratchName}
|
||||
type="text"
|
||||
value={scratch.name}
|
||||
onChange={event => setScratch({ name: event.target.value })}
|
||||
disabled={!scratch.owner?.is_you}
|
||||
spellCheck="false"
|
||||
maxLength={100}
|
||||
placeholder={"Untitled scratch"}
|
||||
/>
|
||||
|
||||
<AsyncButton onClick={compile} forceLoading={isCompiling}>
|
||||
<SyncIcon size={16} /> Compile
|
||||
@@ -234,12 +234,15 @@ export default function Scratch({ slug, tryClaim }: Props) {
|
||||
{scratch.owner?.is_you && <AsyncButton onClick={() => {
|
||||
return Promise.all([
|
||||
saveScratch(),
|
||||
compile(),
|
||||
compile().catch(() => {}), // Ignore errors
|
||||
])
|
||||
}} disabled={isSaved}>
|
||||
<UploadIcon size={16} /> Save
|
||||
</AsyncButton>}
|
||||
<AsyncButton onClick={forkScratch}>
|
||||
<AsyncButton onClick={() => {
|
||||
setIsForking(true)
|
||||
return forkScratch()
|
||||
}} primary={!isSaved && !scratch.owner?.is_you}>
|
||||
<RepoForkedIcon size={16} /> Fork
|
||||
</AsyncButton>
|
||||
</div>
|
||||
4
frontend/src/components/Scratch/index.ts
Normal file
4
frontend/src/components/Scratch/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import Scratch, { Props as ScratchProps } from "./Scratch"
|
||||
|
||||
export type Props = ScratchProps
|
||||
export default Scratch
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
padding: 8px 10px;
|
||||
|
||||
font-size: .8rem;
|
||||
font-size: 0.8rem;
|
||||
|
||||
user-select: none;
|
||||
}
|
||||
@@ -24,7 +24,8 @@
|
||||
padding-right: 2em;
|
||||
}
|
||||
|
||||
.group option, .group optgroup {
|
||||
.group option,
|
||||
.group optgroup {
|
||||
color: var(--g1600);
|
||||
background: var(--g200);
|
||||
}
|
||||
@@ -32,7 +33,7 @@
|
||||
.icon {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
right: .6em;
|
||||
right: 0.6em;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ import { ChevronDownIcon } from "@primer/octicons-react"
|
||||
import styles from "./Select.module.scss"
|
||||
|
||||
export type Props = {
|
||||
className?: string,
|
||||
onChange: ChangeEventHandler<HTMLSelectElement>,
|
||||
children: ReactNode,
|
||||
className?: string
|
||||
onChange: ChangeEventHandler<HTMLSelectElement>
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export default function Select({ onChange, children, className }: Props) {
|
||||
|
||||
@@ -3,10 +3,10 @@ import { ChevronDownIcon } from "@primer/octicons-react"
|
||||
import styles from "./Select.module.scss"
|
||||
|
||||
export type Props = {
|
||||
options: { [key: string]: string },
|
||||
value: string,
|
||||
className?: string,
|
||||
onChange: (value: string) => void,
|
||||
options: { [key: string]: string }
|
||||
value: string
|
||||
className?: string
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
|
||||
export default function Select({ options, value, onChange, className }: Props) {
|
||||
|
||||
@@ -3,11 +3,14 @@
|
||||
flex-direction: column;
|
||||
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.tabButtons {
|
||||
position: relative;
|
||||
height: 48px;
|
||||
min-height: 48px;
|
||||
white-space: nowrap;
|
||||
overflow-x: auto;
|
||||
|
||||
border-bottom: 1px solid var(--a100);
|
||||
}
|
||||
@@ -16,7 +19,7 @@
|
||||
position: relative;
|
||||
|
||||
display: inline-flex;
|
||||
gap: .5em;
|
||||
gap: 0.5em;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@@ -24,7 +27,7 @@
|
||||
min-width: 60px;
|
||||
height: 100%;
|
||||
|
||||
font-size: .8rem;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
|
||||
user-select: none;
|
||||
@@ -34,7 +37,7 @@
|
||||
background: transparent;
|
||||
border: 0;
|
||||
|
||||
transition: color .2s ease;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.7;
|
||||
@@ -44,7 +47,8 @@
|
||||
&:not(:disabled) {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover, &[aria-selected="true"] {
|
||||
&:hover,
|
||||
&[aria-selected="true"] {
|
||||
color: var(--a900);
|
||||
}
|
||||
}
|
||||
@@ -80,7 +84,7 @@
|
||||
|
||||
transition: transform, width;
|
||||
|
||||
// left, width, transition-duration, opacity set by JS
|
||||
// transform, width, transition-duration, opacity set by JS
|
||||
|
||||
// must be separate element for a different transition-duration
|
||||
&::after {
|
||||
@@ -95,12 +99,13 @@
|
||||
border-radius: 4px;
|
||||
|
||||
opacity: inherit;
|
||||
transition: opacity .1s;
|
||||
transition: opacity 0.1s;
|
||||
}
|
||||
}
|
||||
|
||||
.tabPanel {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
|
||||
display: none;
|
||||
|
||||
@@ -111,4 +116,5 @@
|
||||
|
||||
.tabPanelContent {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -5,21 +5,22 @@ import classNames from "classnames"
|
||||
import styles from "./Tabs.module.scss"
|
||||
|
||||
type Context = {
|
||||
activeTab: string | undefined,
|
||||
setActive: (tab: string | undefined) => void,
|
||||
hover: string | undefined,
|
||||
setHover: (tab: string | undefined) => void,
|
||||
setTabRef: (tab: string, ref: RefObject<HTMLButtonElement>) => void,
|
||||
activeTab: string | undefined
|
||||
setActive: (tab: string | undefined) => void
|
||||
hover: string | undefined
|
||||
setHover: (tab: string | undefined) => void
|
||||
setTabRef: (tab: string, ref: RefObject<HTMLButtonElement>) => void
|
||||
}
|
||||
|
||||
const TABS_CTX = createContext<Context>(null)
|
||||
|
||||
export type TabProps = {
|
||||
children: ReactNode,
|
||||
className?: string,
|
||||
key: string, // react doesn't actually give us this as a prop, but this forces ts into requiring it
|
||||
label?: ReactNode,
|
||||
disabled?: boolean,
|
||||
children: ReactNode
|
||||
className?: string
|
||||
key: string // react doesn't actually give us this as a prop, but this forces ts into requiring it
|
||||
label?: ReactNode
|
||||
disabled?: boolean
|
||||
onSelect?: () => void
|
||||
}
|
||||
|
||||
export class Tab extends Component<TabProps> {
|
||||
@@ -42,7 +43,16 @@ export class Tab extends Component<TabProps> {
|
||||
aria-selected={ctx.activeTab === key}
|
||||
className={styles.tabButton}
|
||||
disabled={this.props.disabled}
|
||||
onClick={() => ctx.setActive(key)}
|
||||
onClick={() => {
|
||||
ctx.setActive(key)
|
||||
|
||||
if (this.props.onSelect) {
|
||||
// run after layout
|
||||
setTimeout(() => {
|
||||
this.props.onSelect()
|
||||
}, 0)
|
||||
}
|
||||
}}
|
||||
onMouseMove={event => {
|
||||
ctx.setHover(key)
|
||||
event.stopPropagation()
|
||||
@@ -57,18 +67,23 @@ export class Tab extends Component<TabProps> {
|
||||
}
|
||||
|
||||
export type Props = {
|
||||
className?: string,
|
||||
children: ReactElement<typeof Tab> | ReactElement<typeof Tab>[] | ReactElement<typeof Tab>[][];
|
||||
activeTab: string | undefined;
|
||||
onChange: (tab: string) => void;
|
||||
className?: string
|
||||
children: ReactElement<typeof Tab> | ReactElement<typeof Tab>[] | ReactElement<typeof Tab>[][]
|
||||
activeTab: string | undefined
|
||||
onChange: (tab: string) => void
|
||||
}
|
||||
|
||||
export default function Tabs<Key>({ children, activeTab, onChange, className }: Props) {
|
||||
export default function Tabs({ children, activeTab, onChange, className }: Props) {
|
||||
const [hover, _setHover] = useState<string>()
|
||||
const bgRef = useRef<HTMLDivElement>()
|
||||
const isMovingBetweenButtons = useRef(false)
|
||||
|
||||
const tabs: { [key: string]: { el: any, ref?: RefObject<HTMLButtonElement> } } = {}
|
||||
const tabs: {
|
||||
[key: string]: {
|
||||
el: ReactElement<typeof Tab>
|
||||
ref?: RefObject<HTMLButtonElement>
|
||||
}
|
||||
} = {}
|
||||
|
||||
if (Array.isArray(children)) {
|
||||
for (const child of children) {
|
||||
@@ -97,7 +112,7 @@ export default function Tabs<Key>({ children, activeTab, onChange, className }:
|
||||
if (button) {
|
||||
Object.assign(bgRef.current.style, {
|
||||
opacity: 1,
|
||||
transform: `translateX(${button.offsetLeft}px)`,
|
||||
transform: `translate(${button.offsetLeft}px, ${button.offsetTop}px)`,
|
||||
width: `${button.offsetWidth}px`,
|
||||
})
|
||||
} else {
|
||||
@@ -138,19 +153,21 @@ export default function Tabs<Key>({ children, activeTab, onChange, className }:
|
||||
className={styles.tabButtonsBackground}
|
||||
/>
|
||||
</div>
|
||||
{Object.entries(tabs).map(([key, { el }]) => (
|
||||
<div
|
||||
{Object.entries(tabs).map(([key, { el }]) => {
|
||||
const props = el.props as unknown as TabProps
|
||||
|
||||
return <div
|
||||
role="tabpanel"
|
||||
className={classNames(styles.tabPanel, {
|
||||
[styles.active]: key === activeTab,
|
||||
})}
|
||||
key={key}
|
||||
>
|
||||
<div className={classNames(styles.tabPanelContent, el.props.className)}>
|
||||
{el.props.children}
|
||||
<div className={classNames(styles.tabPanelContent, props.className)}>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
})}
|
||||
</div>
|
||||
</TABS_CTX.Provider>
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.popover {
|
||||
border: 1px solid var(--g500);
|
||||
border-radius: 1em;
|
||||
box-shadow: 0 2px 8px 0 #00000088;
|
||||
box-shadow: 0 2px 8px 0 #0008;
|
||||
max-width: 50em;
|
||||
|
||||
z-index: 999;
|
||||
|
||||
@@ -11,10 +11,10 @@ import styles from "./CompilerButton.module.css"
|
||||
import CompilerOpts, { Props as CompilerOptsProps } from "./CompilerOpts"
|
||||
|
||||
export type Props = {
|
||||
arch: CompilerOptsProps["arch"],
|
||||
value: CompilerOptsProps["value"],
|
||||
onChange: CompilerOptsProps["onChange"],
|
||||
disabled?: boolean,
|
||||
arch: CompilerOptsProps["arch"]
|
||||
value: CompilerOptsProps["value"]
|
||||
onChange: CompilerOptsProps["onChange"]
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export default function CompilerButton({ arch, value, onChange, disabled }: Props) {
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.container {
|
||||
@@ -20,10 +22,12 @@
|
||||
background: var(--g300);
|
||||
}
|
||||
|
||||
.header:not([data-is-popup]), .container:not([data-is-popup]) {
|
||||
.header:not([data-is-popup]),
|
||||
.container:not([data-is-popup]) {
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
/*border-bottom: 1px solid var(--g400);*/
|
||||
|
||||
/* border-bottom: 1px solid var(--g400); */
|
||||
}
|
||||
|
||||
.row {
|
||||
@@ -47,20 +51,26 @@
|
||||
color: var(--frog-secondary);
|
||||
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
|
||||
font-family: monospace;
|
||||
font-size: .8rem;
|
||||
font-size: 0.8rem;
|
||||
|
||||
padding: .6em 1em;
|
||||
padding: 0.6em 1em;
|
||||
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.textbox::-webkit-input-placeholder {
|
||||
color: var(--g500);
|
||||
}
|
||||
|
||||
.flags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: .5em;
|
||||
gap: 0.5em;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
@@ -68,17 +78,20 @@
|
||||
flex-grow: 1;
|
||||
display: inline-block;
|
||||
cursor: default;
|
||||
padding: .75em;
|
||||
padding: 0.75em;
|
||||
border-radius: 12px;
|
||||
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.flag:hover {
|
||||
background: var(--g400);
|
||||
background: var(--g250);
|
||||
border-color: var(--g500);
|
||||
}
|
||||
|
||||
.flag label {
|
||||
font-size: .9rem;
|
||||
margin-left: .5em;
|
||||
font-size: 0.9rem;
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
.flag input {
|
||||
@@ -88,7 +101,7 @@
|
||||
.flagDescription {
|
||||
display: block;
|
||||
font-size: 0.8em;
|
||||
opacity: 0.5;
|
||||
color: var(--g800);
|
||||
}
|
||||
|
||||
.flagSet {
|
||||
@@ -98,13 +111,13 @@
|
||||
flex-direction: column;
|
||||
|
||||
width: 20em;
|
||||
border-radius: .5em;
|
||||
border-radius: 0.5em;
|
||||
}
|
||||
|
||||
.flagSetName {
|
||||
cursor: default;
|
||||
font-size: .8rem;
|
||||
padding: .6em 1em;
|
||||
padding-bottom: .2em;
|
||||
opacity: 0.5;
|
||||
font-size: 0.8rem;
|
||||
padding: 0.6em 1em;
|
||||
padding-bottom: 0.2em;
|
||||
color: var(--g800);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { createContext, useState, useContext, useEffect } from "react"
|
||||
import { createContext, useContext } from "react"
|
||||
|
||||
import Select from "../Select"
|
||||
|
||||
import styles from "./CompilerOpts.module.css"
|
||||
import { useCompilersForArch } from "./compilers"
|
||||
import PresetSelect, { PRESETS } from "./PresetSelect"
|
||||
import PresetSelect from "./PresetSelect"
|
||||
|
||||
interface IOptsContext {
|
||||
checkFlag(flag: string): boolean,
|
||||
setFlag(flag: string, value: boolean): void,
|
||||
checkFlag(flag: string): boolean
|
||||
setFlag(flag: string, value: boolean): void
|
||||
}
|
||||
|
||||
const OptsContext = createContext<IOptsContext>(undefined)
|
||||
@@ -56,28 +56,35 @@ export function FlagOption({ flag, description }: { flag: string, description?:
|
||||
}
|
||||
|
||||
export type CompilerOptsT = {
|
||||
compiler: string,
|
||||
cc_opts: string,
|
||||
compiler: string
|
||||
cc_opts: string
|
||||
}
|
||||
|
||||
export type Props = {
|
||||
arch?: string,
|
||||
value?: CompilerOptsT,
|
||||
onChange: (value: CompilerOptsT) => void,
|
||||
title?: string,
|
||||
isPopup?: boolean,
|
||||
arch?: string
|
||||
value: CompilerOptsT
|
||||
onChange: (value: CompilerOptsT) => void
|
||||
title?: string
|
||||
isPopup?: boolean
|
||||
}
|
||||
|
||||
export default function CompilerOpts({ arch, value, onChange, title, isPopup }: Props) {
|
||||
const [compiler, setCompiler] = useState((value && value.compiler) || PRESETS[0].compiler)
|
||||
let [opts, setOpts] = useState((value && value.cc_opts) || PRESETS[0].opts)
|
||||
const compiler = value.compiler
|
||||
let opts = value.cc_opts
|
||||
|
||||
useEffect(() => {
|
||||
const setCompiler = (compiler: string) => {
|
||||
onChange({
|
||||
compiler,
|
||||
cc_opts: opts,
|
||||
})
|
||||
}, [compiler, opts]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}
|
||||
|
||||
const setOpts = (opts: string) => {
|
||||
onChange({
|
||||
compiler,
|
||||
cc_opts: opts,
|
||||
})
|
||||
}
|
||||
|
||||
return <OptsContext.Provider value={{
|
||||
checkFlag(flag: string) {
|
||||
@@ -108,11 +115,11 @@ export default function CompilerOpts({ arch, value, onChange, title, isPopup }:
|
||||
}
|
||||
|
||||
export function OptsEditor({ arch, compiler, setCompiler, opts, setOpts }: {
|
||||
arch?: string,
|
||||
compiler: string,
|
||||
setCompiler: (compiler: string) => void,
|
||||
opts: string,
|
||||
setOpts: (opts: string) => void,
|
||||
arch?: string
|
||||
compiler: string
|
||||
setCompiler: (compiler: string) => void
|
||||
opts: string
|
||||
setOpts: (opts: string) => void
|
||||
}) {
|
||||
const compilers = useCompilersForArch(arch)
|
||||
const compilerModule = compilers?.find(c => c.id === compiler)
|
||||
|
||||
@@ -4,41 +4,43 @@ import Select from "../Select"
|
||||
import { useCompilersForArch } from "./compilers"
|
||||
|
||||
export const PRESETS = [
|
||||
{
|
||||
name: "Super Mario 64",
|
||||
compiler: "ido5.3",
|
||||
opts: "-O1 -g -mips2",
|
||||
},
|
||||
{
|
||||
name: "Paper Mario",
|
||||
compiler: "gcc2.8.1",
|
||||
opts: "-O2 -fforce-addr",
|
||||
},
|
||||
{
|
||||
name: "OOT",
|
||||
name: "Ocarina of Time",
|
||||
compiler: "ido7.1",
|
||||
opts: "-O2 -mips2",
|
||||
},
|
||||
{
|
||||
name: "MM",
|
||||
name: "Majora's Mask",
|
||||
compiler: "ido7.1",
|
||||
opts: "-O2 -g3 -mips2",
|
||||
},
|
||||
{
|
||||
name: "SM64",
|
||||
compiler: "ido5.3",
|
||||
opts: "-O1 -g -mips2",
|
||||
},
|
||||
]
|
||||
|
||||
export default function PresetSelect({ arch, compiler, opts, setCompiler, setOpts }: {
|
||||
arch: string,
|
||||
compiler: string,
|
||||
opts: string,
|
||||
setCompiler: (compiler: string) => void,
|
||||
setOpts: (opts: string) => void,
|
||||
export default function PresetSelect({ className, arch, compiler, opts, setCompiler, setOpts, serverCompilers }: {
|
||||
className?: string
|
||||
arch: string
|
||||
compiler: string
|
||||
opts: string
|
||||
setCompiler: (compiler: string) => void
|
||||
setOpts: (opts: string) => void
|
||||
serverCompilers?: Record<string, { arch: string | null }>
|
||||
}) {
|
||||
const compilers = useCompilersForArch(arch)
|
||||
const compilers = useCompilersForArch(arch, serverCompilers)
|
||||
|
||||
const presets = PRESETS.filter(p => compilers?.find(c => c.id === p.compiler) !== undefined)
|
||||
const selectedPreset = PRESETS.find(p => p.compiler === compiler && p.opts === opts)
|
||||
|
||||
return <Select onChange={e => {
|
||||
return <Select className={className} onChange={e => {
|
||||
if ((e.target as HTMLSelectElement).value === "custom") {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -18,11 +18,9 @@ export type CompilerModule = { id: string, name: string, Flags: FunctionComponen
|
||||
|
||||
export default COMPILERS
|
||||
|
||||
export function useCompilersForArch(arch?: string) {
|
||||
const serverCompilers = api.useCompilers()
|
||||
|
||||
export function useCompilersForArch(arch?: string, serverCompilers?: Record<string, { arch: string | null }>) {
|
||||
if (!serverCompilers)
|
||||
return null
|
||||
serverCompilers = api.useCompilers()
|
||||
|
||||
if (arch)
|
||||
return COMPILERS.filter(compiler => serverCompilers[compiler.id]?.arch === arch) // compiler supports this arch
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
.container {
|
||||
padding: 1rem;
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.diff {
|
||||
border-spacing: 1em 0;
|
||||
}
|
||||
|
||||
.diff, .log {
|
||||
display: block;
|
||||
user-select: text;
|
||||
border: none;
|
||||
font-family: monospace;
|
||||
font-size: var(--s-1);
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.lineNumber {
|
||||
color: grey;
|
||||
font-size: 0.7em;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.immediate { color: #6D6DFF }
|
||||
.stack { color: #e3fc45 }
|
||||
.register { color: #e3fc45 }
|
||||
.delay_slot { font-weight: bold; color: #969896 }
|
||||
.diff_change { color: #6D6DFF }
|
||||
.diff_add { color: #45bd00 }
|
||||
.diff_remove { color: #c82829 }
|
||||
.source_filename { font-weight: bold }
|
||||
.source_function { font-weight: bold; text-decoration: underline }
|
||||
.source_other { font-style: italic }
|
||||
.source_line_num { font-style: italic }
|
||||
.rotation0 { color: magenta}
|
||||
.rotation1 { color: cyan }
|
||||
.rotation2 { color: rgb(0, 212, 0) }
|
||||
.rotation3 { color: red }
|
||||
.rotation4 { color: lightyellow }
|
||||
.rotation5 { color: lightpink }
|
||||
.rotation6 { color: lightcyan }
|
||||
.rotation7 { color: lightgreen }
|
||||
.rotation8 { color: grey }
|
||||
@@ -1,102 +0,0 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
padding-bottom: var(--s1);
|
||||
padding-top: var(--s1);
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.resizer {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.context, .sourceCode {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.grow { flex: 1 }
|
||||
|
||||
.diffSection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
scrollbar-color: #ffffff33 transparent;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.diffExplanation {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.output {
|
||||
padding: 1em;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.metadata {
|
||||
font-size: 0.8em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem;
|
||||
padding-left: 1rem;
|
||||
gap: 1em;
|
||||
|
||||
color: var(--g1200);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.metadata > div {
|
||||
display: flex;
|
||||
gap: 1ch;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chooseACompiler {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chooseACompilerActions {
|
||||
padding: 1em;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.scratchLink {
|
||||
color: var(--g1400);
|
||||
}
|
||||
|
||||
.editor {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: initial;
|
||||
gap: .25em;
|
||||
|
||||
height: 48px;
|
||||
padding: 0 14px;
|
||||
|
||||
background: var(--g300);
|
||||
border-bottom: 1px solid var(--a100);
|
||||
}
|
||||
|
||||
.scratchName {
|
||||
user-select: initial;
|
||||
}
|
||||
@@ -1,19 +1,23 @@
|
||||
.user {
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: .5em;
|
||||
gap: 6px;
|
||||
|
||||
padding: .6em;
|
||||
padding-right: .9em;
|
||||
padding-left: 3px;
|
||||
|
||||
color: var(--g1400);
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.user:any-link {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.user:any-link:hover {
|
||||
color: var(--g1700);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
border-radius: 999px;
|
||||
width: 1.5em;
|
||||
|
||||
@@ -5,20 +5,30 @@ import * as api from "../../lib/api"
|
||||
|
||||
import styles from "./UserLink.module.css"
|
||||
|
||||
export function GitHubUserLink({ user }: { user: { login: string, avatar_url?: string } }) {
|
||||
return <Link href={`https://github.com/${user.login}`}>
|
||||
<a className={styles.user}>
|
||||
{user.avatar_url && <Image className={styles.avatar} src={user.avatar_url} alt="User avatar" width={24} height={24} />}
|
||||
<span>{user.login}</span>
|
||||
</a>
|
||||
</Link>
|
||||
}
|
||||
|
||||
|
||||
export type Props = {
|
||||
user: api.User | api.AnonymousUser,
|
||||
user: api.User | api.AnonymousUser
|
||||
}
|
||||
|
||||
export default function UserLink({ user }: Props) {
|
||||
if (api.isAnonUser(user)) {
|
||||
return <a className={styles.user}>
|
||||
<span>{user.is_you ? "you" : "anon" }</span>
|
||||
<span>{user.is_you ? "You" : "Anonymous" }</span>
|
||||
</a>
|
||||
} else {
|
||||
return <Link href={`/u/${user.username}`}>
|
||||
<a title={`@${user.username}`} className={styles.user}>
|
||||
<a className={styles.user}>
|
||||
{user.avatar_url && <Image className={styles.avatar} src={user.avatar_url} alt="User avatar" width={24} height={24} />}
|
||||
<span>{user.name} {user.is_you && <i>(you)</i>}</span>
|
||||
<span>{user.username}</span>
|
||||
</a>
|
||||
</Link>
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { useState, useCallback, useEffect, useTransition, useRef } from "react"
|
||||
import { useState, useCallback, useEffect } from "react"
|
||||
|
||||
import { useRouter } from "next/router"
|
||||
|
||||
import { dequal } from "dequal/lite"
|
||||
import useSWR, { Revalidator, RevalidatorOptions } from "swr"
|
||||
import { useDebouncedCallback } from "use-debounce"
|
||||
import useDeepCompareEffect from "use-deep-compare-effect"
|
||||
|
||||
const API_BASE = process.env.NEXT_PUBLIC_API_BASE ?? process.env.STORYBOOK_API_BASE
|
||||
const API_BASE = process.env.INTERNAL_API_BASE ?? process.env.NEXT_PUBLIC_API_BASE ?? process.env.STORYBOOK_API_BASE
|
||||
|
||||
type Json = Record<string, unknown>
|
||||
|
||||
@@ -130,76 +128,79 @@ export async function patch(url: string, json: Json) {
|
||||
}
|
||||
|
||||
export interface AnonymousUser {
|
||||
is_you: boolean,
|
||||
is_anonymous: true,
|
||||
is_you: boolean
|
||||
is_anonymous: true
|
||||
}
|
||||
|
||||
export interface User {
|
||||
is_you: boolean,
|
||||
is_anonymous: false,
|
||||
is_you: boolean
|
||||
is_anonymous: false
|
||||
|
||||
id: number,
|
||||
username: string,
|
||||
name: string,
|
||||
avatar_url: string | null,
|
||||
github_api_url: string | null,
|
||||
github_html_url: string | null,
|
||||
id: number
|
||||
username: string
|
||||
name: string
|
||||
avatar_url: string | null
|
||||
github_api_url: string | null
|
||||
github_html_url: string | null
|
||||
}
|
||||
|
||||
export type Scratch = {
|
||||
slug: string,
|
||||
compiler: string,
|
||||
arch: string,
|
||||
cc_opts: string,
|
||||
source_code: string,
|
||||
context: string,
|
||||
owner: AnonymousUser | User | null,
|
||||
parent: string | null, // URL
|
||||
diff_label: string | null,
|
||||
name: string
|
||||
description: string
|
||||
slug: string
|
||||
compiler: string
|
||||
arch: string
|
||||
cc_opts: string
|
||||
source_code: string
|
||||
context: string
|
||||
owner: AnonymousUser | User | null // null means unclaimed
|
||||
parent: string | null // URL
|
||||
diff_label: string | null
|
||||
score: number // -1 = doesn't compile
|
||||
}
|
||||
|
||||
export type Compilation = {
|
||||
errors: string,
|
||||
diff_output: DiffOutput,
|
||||
errors: string
|
||||
diff_output: DiffOutput
|
||||
}
|
||||
|
||||
export type DiffOutput = {
|
||||
arch_str: string,
|
||||
current_score: number,
|
||||
error: string | null,
|
||||
header: DiffHeader,
|
||||
rows: DiffRow[],
|
||||
arch_str: string
|
||||
current_score: number
|
||||
error: string | null
|
||||
header: DiffHeader
|
||||
rows: DiffRow[]
|
||||
}
|
||||
|
||||
export type DiffHeader = {
|
||||
base: DiffText[],
|
||||
current: DiffText[],
|
||||
previous?: DiffText[],
|
||||
base: DiffText[]
|
||||
current: DiffText[]
|
||||
previous?: DiffText[]
|
||||
}
|
||||
|
||||
export type DiffRow = {
|
||||
key: string,
|
||||
base?: DiffCell,
|
||||
current?: DiffCell,
|
||||
previous?: DiffCell,
|
||||
key: string
|
||||
base?: DiffCell
|
||||
current?: DiffCell
|
||||
previous?: DiffCell
|
||||
}
|
||||
|
||||
export type DiffCell = {
|
||||
text: DiffText[],
|
||||
line?: number,
|
||||
branch?: number,
|
||||
src?: string,
|
||||
src_comment?: string,
|
||||
src_line?: number,
|
||||
src_path?: string,
|
||||
text: DiffText[]
|
||||
line?: number
|
||||
branch?: number
|
||||
src?: string
|
||||
src_comment?: string
|
||||
src_line?: number
|
||||
src_path?: string
|
||||
}
|
||||
|
||||
export type DiffText = {
|
||||
text: string,
|
||||
format?: string,
|
||||
group?: string,
|
||||
index?: number,
|
||||
key?: string,
|
||||
text: string
|
||||
format?: string
|
||||
group?: string
|
||||
index?: number
|
||||
key?: string
|
||||
}
|
||||
|
||||
|
||||
@@ -208,29 +209,39 @@ export function isAnonUser(user: User | AnonymousUser): user is AnonymousUser {
|
||||
}
|
||||
|
||||
export function useScratch(slugOrUrl: string): {
|
||||
scratch: Readonly<Scratch>,
|
||||
savedScratch: Readonly<Scratch> | null,
|
||||
setScratch: (scratch: Partial<Scratch>) => void, // Update the scratch, but only locally
|
||||
saveScratch: () => Promise<void>, // Persist the scratch to the server
|
||||
isSaved: boolean,
|
||||
scratch: Readonly<Scratch>
|
||||
savedScratch: Readonly<Scratch> | null
|
||||
setScratch: (scratch: Partial<Scratch>) => void // Update the scratch, but only locally
|
||||
saveScratch: () => Promise<void> // Persist the scratch to the server
|
||||
isSaved: boolean
|
||||
} {
|
||||
const url = isAbsoluteUrl(slugOrUrl) ?slugOrUrl : `/scratch/${slugOrUrl}`
|
||||
const [isSaved, setIsSaved] = useState(true)
|
||||
const localScratch = useRef<Scratch>()
|
||||
const [localScratch, setLocalScratch] = useState<Scratch>()
|
||||
const shouldGetScratchFromServer = useCallback(() => {
|
||||
// This is in a useCallback so useSWR's onSuccess doesn't capture the values
|
||||
|
||||
if (!localScratch)
|
||||
return true
|
||||
|
||||
// Only update localScratch if there aren't unsaved changes (otherwise, data loss could occur)
|
||||
if (isSaved)
|
||||
return true
|
||||
|
||||
return false
|
||||
}, [localScratch, isSaved])
|
||||
const { data, mutate } = useSWR<Scratch>(url, get, {
|
||||
suspense: true,
|
||||
refreshInterval: isSaved ? 5000 : 0,
|
||||
refreshInterval: 5000,
|
||||
onSuccess: scratch => {
|
||||
if (!scratch.source_code) {
|
||||
throw new Error("Scratch returned from API has no source_code (is the API misbehaving?)")
|
||||
}
|
||||
|
||||
// Only update localScratch if there aren't unsaved changes (otherwise, data loss could occur)
|
||||
// TODO: display onscreen prompt if there are scratch updates but they arent displayed
|
||||
// because they could overwrite your own changes
|
||||
if (!localScratch || isSaved) {
|
||||
if (shouldGetScratchFromServer()) {
|
||||
console.info("Got updated scratch from server", scratch)
|
||||
localScratch.current = scratch
|
||||
setLocalScratch(scratch)
|
||||
setIsSaved(true)
|
||||
}
|
||||
},
|
||||
onErrorRetry,
|
||||
@@ -239,73 +250,72 @@ export function useScratch(slugOrUrl: string): {
|
||||
|
||||
// If the slug changes, forget the local scratch
|
||||
useEffect(() => {
|
||||
localScratch.current = undefined
|
||||
setLocalScratch(undefined)
|
||||
mutate()
|
||||
setIsSaved(true)
|
||||
}, [mutate])
|
||||
}, [slugOrUrl, mutate])
|
||||
|
||||
const [, forceUpdate] = useState<{}>({})
|
||||
const setScratch = useCallback((partial: Partial<Scratch>) => {
|
||||
Object.assign(localScratch.current, partial)
|
||||
setIsSaved(dequal(localScratch.current, savedScratch))
|
||||
forceUpdate({}) // we changed localScratch
|
||||
}, [localScratch, savedScratch])
|
||||
const updateLocalScratch = useCallback((partial: Partial<Scratch>) => {
|
||||
setLocalScratch(Object.assign({}, localScratch, partial))
|
||||
setIsSaved(false)
|
||||
}, [localScratch])
|
||||
|
||||
const saveScratch = useCallback(() => {
|
||||
if (!localScratch.current) {
|
||||
if (!localScratch) {
|
||||
throw new Error("Cannot save scratch before it is loaded")
|
||||
}
|
||||
if (!localScratch.current.owner.is_you) {
|
||||
if (!localScratch.owner.is_you) {
|
||||
throw new Error("Cannot save scratch which you do not own")
|
||||
}
|
||||
|
||||
return patch(`/scratch/${savedScratch.slug}`, {
|
||||
// TODO: api should support undefinedIfUnchanged on all fields
|
||||
source_code: localScratch.current.source_code,
|
||||
context: localScratch.current.context, //undefinedIfUnchanged("context"),
|
||||
compiler: localScratch.current.compiler,
|
||||
cc_opts: localScratch.current.cc_opts,
|
||||
source_code: undefinedIfUnchanged(savedScratch, localScratch, "source_code"),
|
||||
context: undefinedIfUnchanged(savedScratch, localScratch, "context"),
|
||||
compiler: undefinedIfUnchanged(savedScratch, localScratch, "compiler"),
|
||||
cc_opts: undefinedIfUnchanged(savedScratch, localScratch, "cc_opts"),
|
||||
name: undefinedIfUnchanged(savedScratch, localScratch, "name"),
|
||||
description: undefinedIfUnchanged(savedScratch, localScratch, "description"),
|
||||
}).then(() => {
|
||||
setIsSaved(true)
|
||||
mutate(localScratch.current, true)
|
||||
mutate(localScratch, true)
|
||||
}).catch(error => {
|
||||
console.error(error)
|
||||
})
|
||||
}, [localScratch, savedScratch, mutate])
|
||||
|
||||
if (!localScratch.current) {
|
||||
if (!localScratch) {
|
||||
setIsSaved(true)
|
||||
localScratch.current = savedScratch
|
||||
setLocalScratch(savedScratch)
|
||||
}
|
||||
|
||||
return {
|
||||
scratch: localScratch.current,
|
||||
scratch: localScratch ?? savedScratch,
|
||||
savedScratch,
|
||||
setScratch,
|
||||
setScratch: updateLocalScratch,
|
||||
saveScratch,
|
||||
isSaved,
|
||||
}
|
||||
}
|
||||
|
||||
export async function forkScratch(parent: Scratch): Promise<Scratch> {
|
||||
const scratch = await post(`/scratch/${parent.slug}/fork`, parent)
|
||||
export async function forkScratch(parent: Scratch, localScratch: Partial<Scratch> = {}): Promise<Scratch> {
|
||||
const scratch = await post(`/scratch/${parent.slug}/fork`, Object.assign({}, parent, localScratch))
|
||||
return scratch
|
||||
}
|
||||
|
||||
export function useForkScratchAndGo(parent: Scratch): () => Promise<void> {
|
||||
export function useForkScratchAndGo(parent: Scratch, localScratch: Partial<Scratch> = {}): () => Promise<void> {
|
||||
const router = useRouter()
|
||||
|
||||
return async () => {
|
||||
const fork = await forkScratch(parent)
|
||||
const fork = await forkScratch(parent, localScratch)
|
||||
router.push(`/scratch/${fork.slug}`)
|
||||
}
|
||||
}
|
||||
|
||||
export function useCompilation(scratch: Scratch | null, savedScratch?: Scratch, autoRecompile = true): {
|
||||
compilation: Readonly<Compilation> | null,
|
||||
compile: () => Promise<void>, // no debounce
|
||||
debouncedCompile: () => Promise<void>, // with debounce
|
||||
isCompiling: boolean,
|
||||
compilation: Readonly<Compilation> | null
|
||||
compile: () => Promise<void> // no debounce
|
||||
debouncedCompile: () => Promise<void> // with debounce
|
||||
isCompiling: boolean
|
||||
} {
|
||||
const [compileRequestPromise, setCompileRequestPromise] = useState<Promise<void>>(null)
|
||||
const [compilation, setCompilation] = useState<Compilation>(null)
|
||||
@@ -344,20 +354,27 @@ export function useCompilation(scratch: Scratch | null, savedScratch?: Scratch,
|
||||
|
||||
const debouncedCompile = useDebouncedCallback(compile, 500, { leading: false, trailing: true })
|
||||
|
||||
useDeepCompareEffect(() => {
|
||||
useEffect(() => {
|
||||
if (autoRecompile) {
|
||||
if (scratch && scratch.compiler !== "")
|
||||
debouncedCompile()
|
||||
else
|
||||
setCompilation(null)
|
||||
}
|
||||
}, [debouncedCompile, scratch, autoRecompile])
|
||||
}, [ // eslint-disable-line react-hooks/exhaustive-deps
|
||||
debouncedCompile,
|
||||
autoRecompile,
|
||||
|
||||
// fields passed to compilations
|
||||
scratch.compiler, scratch.cc_opts,
|
||||
scratch.source_code, scratch.context,
|
||||
])
|
||||
|
||||
return {
|
||||
compilation,
|
||||
compile,
|
||||
debouncedCompile,
|
||||
isCompiling: !!compileRequestPromise,
|
||||
isCompiling: !!compileRequestPromise || debouncedCompile.isPending(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -372,7 +389,7 @@ export function useArches(): Record<string, string> {
|
||||
return data?.arches
|
||||
}
|
||||
|
||||
export function useCompilers(): Record<string, { arch: string | null }> | null {
|
||||
export function useCompilers(): Record<string, { arch: string | null }> {
|
||||
const { data } = useSWR("/compilers", get, {
|
||||
refreshInterval: 0,
|
||||
suspense: true,
|
||||
|
||||
@@ -5,9 +5,9 @@ import Router from "next/router"
|
||||
import useResizeObserver from "@react-hook/resize-observer"
|
||||
|
||||
export function useSize<T extends HTMLElement>(): {
|
||||
width: number | undefined,
|
||||
height: number | undefined,
|
||||
ref: RefObject<T>,
|
||||
width: number | undefined
|
||||
height: number | undefined
|
||||
ref: RefObject<T>
|
||||
} {
|
||||
const ref = useRef<T>()
|
||||
const [size, setSize] = useState({ width: undefined, height: undefined })
|
||||
@@ -22,7 +22,7 @@ export function useSize<T extends HTMLElement>(): {
|
||||
return { width: size.width, height: size.height, ref }
|
||||
}
|
||||
|
||||
export function useWarnBeforeUnload(enabled: boolean, message: string = "Are you sure you want to leave this page?") {
|
||||
export function useWarnBeforeUnload(enabled: boolean, message = "Are you sure you want to leave this page?") {
|
||||
const enabledRef = useRef(enabled)
|
||||
const messageRef = useRef(message)
|
||||
|
||||
|
||||
@@ -10,28 +10,15 @@
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji;
|
||||
line-height: 1.5;
|
||||
text-rendering: optimizeQuality;
|
||||
overflow: hidden;
|
||||
|
||||
--ratio: 1.25;
|
||||
--s-5: calc(var(--s-4) / var(--ratio));
|
||||
--s-4: calc(var(--s-3) / var(--ratio));
|
||||
--s-3: calc(var(--s-2) / var(--ratio));
|
||||
--s-2: calc(var(--s-1) / var(--ratio));
|
||||
--s-1: calc(var(--s0) / var(--ratio));
|
||||
--s0: 1rem;
|
||||
--s1: calc(var(--s0) * var(--ratio));
|
||||
--s2: calc(var(--s1) * var(--ratio));
|
||||
--s3: calc(var(--s2) * var(--ratio));
|
||||
--s4: calc(var(--s3) * var(--ratio));
|
||||
--s5: calc(var(--s4) * var(--ratio));
|
||||
--s6: calc(var(--s5) * var(--ratio));
|
||||
--s7: calc(var(--s6) * var(--ratio));
|
||||
}
|
||||
|
||||
*, *::before, *::after {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
@@ -48,13 +35,14 @@ body {
|
||||
background: var(--g300);
|
||||
color: var(--a800);
|
||||
|
||||
line-height: var(--ratio);
|
||||
line-height: 1.25;
|
||||
overflow-x: hidden;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
html, body {
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -76,7 +64,7 @@ main {
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: #ffffff22;
|
||||
background: var(--a100);
|
||||
}
|
||||
|
||||
.routerProgressBar {
|
||||
|
||||
57
frontend/src/pages/credits.module.scss
Normal file
57
frontend/src/pages/credits.module.scss
Normal file
@@ -0,0 +1,57 @@
|
||||
.container {
|
||||
padding: 32px;
|
||||
max-width: 700px;
|
||||
|
||||
user-select: auto;
|
||||
cursor: auto;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 21px;
|
||||
|
||||
line-height: 1.5;
|
||||
|
||||
p,
|
||||
ul {
|
||||
color: var(--g1700);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--g1400);
|
||||
|
||||
&:hover {
|
||||
color: var(--g1700);
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.heading {
|
||||
font-size: 3rem;
|
||||
font-weight: 400;
|
||||
color: var(--g1800);
|
||||
}
|
||||
|
||||
.subheading {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--g1800);
|
||||
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.contributors {
|
||||
list-style: none;
|
||||
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.rule {
|
||||
border: 0;
|
||||
background: var(--g400);
|
||||
height: 1px;
|
||||
}
|
||||
140
frontend/src/pages/credits.tsx
Normal file
140
frontend/src/pages/credits.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import { GetStaticProps } from "next"
|
||||
|
||||
import Link from "next/link"
|
||||
|
||||
import Nav from "../components/Nav"
|
||||
import UserLink, { GitHubUserLink } from "../components/user/UserLink"
|
||||
import * as api from "../lib/api"
|
||||
|
||||
import styles from "./credits.module.scss"
|
||||
|
||||
const MAINTAINER_USERNAMES = ["nanaian", "ethteck"]
|
||||
const CONTRIBUTOR_USERNAMES = [
|
||||
"zbanks",
|
||||
"simonlindholm",
|
||||
"TGEnigma",
|
||||
"octorock",
|
||||
"mkst",
|
||||
"JoshDuMan",
|
||||
"Henny022",
|
||||
"AngheloAlf",
|
||||
]
|
||||
|
||||
type Contributor = {
|
||||
type: "decompme"
|
||||
user: api.User
|
||||
} | {
|
||||
type: "github"
|
||||
user: { login: string, avatar_url?: string }
|
||||
}
|
||||
|
||||
async function getContributor(username: string): Promise<Contributor> {
|
||||
try {
|
||||
// Try and get decomp.me information if they have an account
|
||||
const user: api.User = await api.get(`/users/${username}`)
|
||||
return {
|
||||
type: "decompme",
|
||||
user,
|
||||
}
|
||||
} catch (error) {
|
||||
// Fall back to GitHub information
|
||||
const req = await fetch(`https://api.github.com/users/${username}`)
|
||||
const user = await req.json()
|
||||
|
||||
if (user.message) {
|
||||
// Rate limit :(
|
||||
return {
|
||||
type: "github",
|
||||
user: { login: username },
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: "github",
|
||||
user,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Contributor({ contributor }: { contributor: Contributor }) {
|
||||
if (contributor.type === "decompme")
|
||||
return <UserLink user={contributor.user} />
|
||||
else
|
||||
return <GitHubUserLink user={contributor.user} />
|
||||
}
|
||||
|
||||
export const getStaticProps: GetStaticProps = async _context => {
|
||||
return {
|
||||
props: {
|
||||
maintainers: await Promise.all(MAINTAINER_USERNAMES.map(getContributor)),
|
||||
contributors: await Promise.all(CONTRIBUTOR_USERNAMES.map(getContributor)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default function CreditsPage({ maintainers, contributors }: {
|
||||
maintainers: Contributor[]
|
||||
contributors: Contributor[]
|
||||
}) {
|
||||
return <>
|
||||
<Nav />
|
||||
<main className={styles.container}>
|
||||
<h1 className={styles.heading}>
|
||||
Credits
|
||||
</h1>
|
||||
|
||||
<p>
|
||||
decomp.me is developed by <Contributor contributor={maintainers[0]} /> and <Contributor contributor={maintainers[1]} />.
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<h3 className={styles.subheading}>
|
||||
Contributors
|
||||
</h3>
|
||||
<ul className={styles.contributors}>
|
||||
{contributors.map(contributor => {
|
||||
return <li key={contributor.type === "decompme" ? contributor.user.username : contributor.user.login}>
|
||||
<Contributor contributor={contributor} />
|
||||
</li>
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<hr className={styles.rule} />
|
||||
|
||||
<div>
|
||||
<h3 className={styles.subheading}>
|
||||
Projects
|
||||
</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<Link href="https://github.com/simonlindholm/asm-differ">
|
||||
<a>simonlindholm/asm-differ</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="https://github.com/matt-kempster/mips_to_c">
|
||||
<a>matt-kempster/mips_to_c</a>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<hr className={styles.rule} />
|
||||
|
||||
<div>
|
||||
<h3 className={styles.subheading}>
|
||||
Icons
|
||||
</h3>
|
||||
<ul>
|
||||
<li>Octicons by GitHub</li>
|
||||
<li>
|
||||
<Link href="https://github.com/file-icons/icons">
|
||||
<a>file-icons/icons</a>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
}
|
||||
@@ -18,5 +18,5 @@
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #ff8866;
|
||||
color: #f86;
|
||||
}
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1em;
|
||||
|
||||
max-width: 40em;
|
||||
padding: 1em;
|
||||
|
||||
user-select: auto;
|
||||
}
|
||||
|
||||
.rule {
|
||||
border: 0;
|
||||
background: var(--g400);
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.heading {
|
||||
padding: 10px;
|
||||
|
||||
> h1 {
|
||||
color: var(--g1900);
|
||||
font-size: 1.25em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
> p {
|
||||
padding-top: 4px;
|
||||
color: var(--g1000);
|
||||
font-size: .9em;
|
||||
max-width: 50ch;
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
|
||||
color: var(--g1700);
|
||||
font-size: .9em;
|
||||
font-weight: 600;
|
||||
|
||||
small {
|
||||
color: var(--g1000);
|
||||
font-size: .8em;
|
||||
}
|
||||
}
|
||||
|
||||
.textInput {
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
|
||||
color: var(--g1200);
|
||||
background: var(--g200);
|
||||
font: .8em monospace;
|
||||
border: 1px solid var(--g500);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.editorContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.editor {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
|
||||
background: var(--g200);
|
||||
|
||||
border: 1px solid var(--g500);
|
||||
border-radius: 4px;
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
import { useEffect, useState, useMemo } from "react"
|
||||
|
||||
import { GetStaticProps } from "next"
|
||||
|
||||
import Head from "next/head"
|
||||
import { useRouter } from "next/router"
|
||||
|
||||
import AsyncButton from "../components/AsyncButton"
|
||||
import Editor from "../components/Editor"
|
||||
import Footer from "../components/Footer"
|
||||
import Nav from "../components/Nav"
|
||||
import Select from "../components/Select2"
|
||||
import * as api from "../lib/api"
|
||||
|
||||
import styles from "./scratch.module.scss"
|
||||
|
||||
function getLabels(asm: string): string[] {
|
||||
const lines = asm.split("\n")
|
||||
const labels = []
|
||||
|
||||
for (const line of lines) {
|
||||
const match = line.match(/^\s*glabel\s+([a-zA-Z0-9_]+)\s*$/)
|
||||
if (match) {
|
||||
labels.push(match[1])
|
||||
}
|
||||
}
|
||||
|
||||
return labels
|
||||
}
|
||||
|
||||
export const getStaticProps: GetStaticProps = async _context => {
|
||||
const data = await api.get("/compilers")
|
||||
|
||||
return {
|
||||
props: {
|
||||
arches: data.arches,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default function NewScratch({ arches }: { arches: { [key: string]: string } }) {
|
||||
const [asm, setAsm] = useState("")
|
||||
const [context, setContext] = useState("")
|
||||
const [arch, setArch] = useState("")
|
||||
const router = useRouter()
|
||||
|
||||
const defaultLabel = useMemo(() => {
|
||||
const labels = getLabels(asm)
|
||||
return labels.length > 0 ? labels[labels.length - 1] : null
|
||||
}, [asm])
|
||||
const [label, setLabel] = useState<string>("")
|
||||
|
||||
// Load fields from localStorage
|
||||
useEffect(() => {
|
||||
try {
|
||||
setLabel(JSON.parse(localStorage["NewScratch.label"] ?? "\"\""))
|
||||
setAsm(JSON.parse(localStorage["NewScratch.asm"] ?? "\"\""))
|
||||
setContext(JSON.parse(localStorage["NewScratch.context"] ?? "\"\""))
|
||||
setArch(JSON.parse(localStorage["NewScratch.arch"] ?? "\"\""))
|
||||
} catch (error) {
|
||||
console.warn("bad localStorage", error)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Update localStorage
|
||||
useEffect(() => {
|
||||
localStorage["NewScratch.label"] = JSON.stringify(label)
|
||||
localStorage["NewScratch.asm"] = JSON.stringify(asm)
|
||||
localStorage["NewScratch.context"] = JSON.stringify(context)
|
||||
localStorage["NewScratch.arch"] = JSON.stringify(arch)
|
||||
}, [label, asm, context, arch])
|
||||
|
||||
if (!arch) {
|
||||
setArch(Object.keys(arches)[0])
|
||||
}
|
||||
|
||||
const submit = async () => {
|
||||
try {
|
||||
const scratch: api.Scratch = await api.post("/scratch", {
|
||||
target_asm: asm,
|
||||
context: context || "",
|
||||
arch,
|
||||
diff_label: label || defaultLabel || "",
|
||||
})
|
||||
|
||||
localStorage["NewScratch.label"] = ""
|
||||
localStorage["NewScratch.asm"] = ""
|
||||
|
||||
router.push(`/scratch/${scratch.slug}`)
|
||||
} catch (error) {
|
||||
if (error?.responseJSON?.as_errors) {
|
||||
throw new Error(error.responseJSON.as_errors.join("\n"))
|
||||
} else {
|
||||
console.error(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return <>
|
||||
<Head>
|
||||
<title>New scratch | decomp.me</title>
|
||||
</Head>
|
||||
<Nav />
|
||||
<main className={styles.container}>
|
||||
<div className={styles.heading}>
|
||||
<h1>Create a new scratch</h1>
|
||||
<p>
|
||||
A scratch is a playground where you can work on matching
|
||||
a given target function using any compiler options you like.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<hr className={styles.rule} />
|
||||
|
||||
<div>
|
||||
<p className={styles.label}>
|
||||
Architecture
|
||||
</p>
|
||||
{/* TODO: custom horizontal <options> */}
|
||||
<Select
|
||||
options={arches}
|
||||
value={arch}
|
||||
onChange={a => setArch(a)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<hr className={styles.rule} />
|
||||
|
||||
<div>
|
||||
<label className={styles.label} htmlFor="label">
|
||||
Function name <small>(label as it appears in the target asm)</small>
|
||||
</label>
|
||||
<input
|
||||
name="label"
|
||||
type="text"
|
||||
value={label}
|
||||
placeholder={defaultLabel}
|
||||
onChange={e => setLabel((e.target as HTMLInputElement).value)}
|
||||
className={styles.textInput}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.editorContainer}>
|
||||
<p className={styles.label}>Target assembly <small>(required)</small></p>
|
||||
<Editor
|
||||
className={styles.editor}
|
||||
language="mips"
|
||||
value={asm}
|
||||
onChange={setAsm}
|
||||
padding={10}
|
||||
showMargin
|
||||
lineNumbers
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.editorContainer}>
|
||||
<p className={styles.label}>
|
||||
Context <small>(typically generated with m2ctx.py)</small>
|
||||
</p>
|
||||
<Editor
|
||||
className={styles.editor}
|
||||
language="c"
|
||||
value={context}
|
||||
onChange={setContext}
|
||||
padding={10}
|
||||
showMargin
|
||||
lineNumbers
|
||||
/>
|
||||
</div>
|
||||
|
||||
<hr className={styles.rule} />
|
||||
|
||||
<div>
|
||||
<AsyncButton
|
||||
primary
|
||||
disabled={asm.length == 0}
|
||||
onClick={submit}
|
||||
errorPlacement="right-center"
|
||||
>
|
||||
Create scratch
|
||||
</AsyncButton>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import Head from "next/head"
|
||||
|
||||
import LoadingSpinner from "../../components/loading.svg"
|
||||
import Nav from "../../components/Nav"
|
||||
import Scratch, { nameScratch } from "../../components/scratch/Scratch"
|
||||
import Scratch from "../../components/Scratch"
|
||||
import * as api from "../../lib/api"
|
||||
|
||||
import styles from "./[slug].module.scss"
|
||||
@@ -42,7 +42,7 @@ export const getStaticProps: GetStaticProps = async context => {
|
||||
export default function ScratchPage({ scratch }: { scratch?: api.Scratch }) {
|
||||
return <>
|
||||
<Head>
|
||||
<title>{scratch ? nameScratch(scratch) : "Loading scratch"} | decomp.me</title>
|
||||
<title>{scratch ? scratch.name : "Scratch"} | decomp.me</title>
|
||||
</Head>
|
||||
<Nav />
|
||||
<main className={styles.container}>
|
||||
|
||||
129
frontend/src/pages/scratch/new.module.scss
Normal file
129
frontend/src/pages/scratch/new.module.scss
Normal file
@@ -0,0 +1,129 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1em;
|
||||
|
||||
max-width: 40em;
|
||||
padding: 1em;
|
||||
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.rule {
|
||||
border: 0;
|
||||
background: var(--g400);
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.heading {
|
||||
padding: 10px;
|
||||
user-select: auto;
|
||||
|
||||
> h1 {
|
||||
color: var(--g1900);
|
||||
font-size: 1.25em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
> p {
|
||||
padding-top: 4px;
|
||||
color: var(--g1000);
|
||||
font-size: 0.9em;
|
||||
max-width: 50ch;
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
|
||||
color: var(--g1700);
|
||||
font-size: 0.9em;
|
||||
font-weight: 600;
|
||||
|
||||
small {
|
||||
color: var(--g800);
|
||||
font-size: 0.8em;
|
||||
font-weight: 400;
|
||||
padding-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.textInput {
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
|
||||
color: var(--g1200);
|
||||
background: var(--g200);
|
||||
font: 0.8em monospace;
|
||||
border: 1px solid var(--g500);
|
||||
border-radius: 4px;
|
||||
|
||||
outline: none !important;
|
||||
|
||||
&::-webkit-input-placeholder {
|
||||
color: var(--g700);
|
||||
}
|
||||
}
|
||||
|
||||
.editorContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.editor {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
|
||||
background: var(--g200);
|
||||
|
||||
border: 1px solid var(--g500);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.compilerContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
|
||||
@media screen and (max-width: 400px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
& > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
|
||||
&.compilerChoiceOr {
|
||||
display: block;
|
||||
flex: 0;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
|
||||
color: var(--g500);
|
||||
font-size: 0.8rem;
|
||||
|
||||
@media screen and (min-width: 400px) {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.compilerChoiceSelect {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.compilerChoiceHeading {
|
||||
font-size: 0.8rem;
|
||||
padding: 2px 10px;
|
||||
color: var(--g800);
|
||||
}
|
||||
272
frontend/src/pages/scratch/new.tsx
Normal file
272
frontend/src/pages/scratch/new.tsx
Normal file
@@ -0,0 +1,272 @@
|
||||
import { useEffect, useState, useMemo } from "react"
|
||||
|
||||
import { GetStaticProps } from "next"
|
||||
|
||||
import Head from "next/head"
|
||||
import { useRouter } from "next/router"
|
||||
|
||||
import ArchSelect from "../../components/ArchSelect"
|
||||
import AsyncButton from "../../components/AsyncButton"
|
||||
import { useCompilersForArch } from "../../components/compiler/compilers"
|
||||
import PresetSelect, { PRESETS } from "../../components/compiler/PresetSelect"
|
||||
import Editor from "../../components/Editor"
|
||||
import Footer from "../../components/Footer"
|
||||
import Nav from "../../components/Nav"
|
||||
import Select from "../../components/Select2"
|
||||
import * as api from "../../lib/api"
|
||||
|
||||
import styles from "./new.module.scss"
|
||||
|
||||
function getLabels(asm: string): string[] {
|
||||
const lines = asm.split("\n")
|
||||
const labels = []
|
||||
|
||||
for (const line of lines) {
|
||||
const match = line.match(/^\s*glabel\s+([a-zA-Z0-9_]+)\s*$/)
|
||||
if (match) {
|
||||
labels.push(match[1])
|
||||
}
|
||||
}
|
||||
|
||||
return labels
|
||||
}
|
||||
|
||||
export const getStaticProps: GetStaticProps = async _context => {
|
||||
const data = await api.get("/compilers")
|
||||
|
||||
return {
|
||||
props: {
|
||||
serverCompilers: data,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default function NewScratch({ serverCompilers }: {
|
||||
serverCompilers: {
|
||||
arches: {
|
||||
[key: string]: {
|
||||
name: string
|
||||
description: string
|
||||
}
|
||||
}
|
||||
compilers: {
|
||||
[key: string]: {
|
||||
arch: string
|
||||
}
|
||||
}
|
||||
}
|
||||
}) {
|
||||
const [asm, setAsm] = useState("")
|
||||
const [context, setContext] = useState("")
|
||||
const [arch, setArch] = useState("")
|
||||
const [compiler, setCompiler] = useState<string>()
|
||||
const [compilerOpts, setCompilerOpts] = useState<string>("")
|
||||
const router = useRouter()
|
||||
|
||||
const defaultLabel = useMemo(() => {
|
||||
const labels = getLabels(asm)
|
||||
return labels.length > 0 ? labels[labels.length - 1] : null
|
||||
}, [asm])
|
||||
const [label, setLabel] = useState<string>("")
|
||||
|
||||
const [lineNumbers, setLineNumbers] = useState(false)
|
||||
|
||||
// Load fields from localStorage
|
||||
useEffect(() => {
|
||||
try {
|
||||
setLabel(localStorage["new_scratch_label"] ?? "")
|
||||
setAsm(localStorage["new_scratch_asm"] ?? "")
|
||||
setContext(localStorage["new_scratch_context"] ?? "")
|
||||
setArch(localStorage["new_scratch_arch"] ?? "")
|
||||
setCompiler(localStorage["new_scratch_compiler"] ?? undefined)
|
||||
setCompilerOpts(localStorage["new_scratch_compilerOpts"] ?? "")
|
||||
} catch (error) {
|
||||
console.warn("bad localStorage", error)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Update localStorage
|
||||
useEffect(() => {
|
||||
localStorage["new_scratch_label"] = label
|
||||
localStorage["new_scratch_asm"] = asm
|
||||
localStorage["new_scratch_context"] = context
|
||||
localStorage["new_scratch_arch"] = arch
|
||||
localStorage["new_scratch_compiler"] = compiler
|
||||
localStorage["new_scratch_compilerOpts"] = compilerOpts
|
||||
}, [label, asm, context, arch, compiler, compilerOpts])
|
||||
|
||||
const compilers = useCompilersForArch(arch, serverCompilers.compilers)
|
||||
const compilerModule = compilers?.find(c => c.id === compiler)
|
||||
|
||||
if (!arch || Object.keys(serverCompilers.arches).indexOf(arch) === -1) {
|
||||
setArch(Object.keys(serverCompilers.arches)[0])
|
||||
}
|
||||
|
||||
if (!compilerModule) { // We just changed architectures, probably
|
||||
if (compilers.length === 0) {
|
||||
console.warn("No compilers supported for arch", arch)
|
||||
} else {
|
||||
// Fall back to the first supported compiler and no opts
|
||||
setCompiler(compilers[0].id)
|
||||
setCompilerOpts("")
|
||||
|
||||
// If there is a preset that uses a supported compiler, default to it
|
||||
for (const preset of PRESETS) {
|
||||
if (compilers.find(c => c.id === preset.compiler)) {
|
||||
setCompiler(preset.compiler)
|
||||
setCompilerOpts(preset.opts)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const submit = async () => {
|
||||
try {
|
||||
const scratch: api.Scratch = await api.post("/scratch", {
|
||||
target_asm: asm,
|
||||
context: context || "",
|
||||
arch,
|
||||
compiler,
|
||||
compiler_flags: compilerOpts,
|
||||
diff_label: label || defaultLabel || "",
|
||||
})
|
||||
|
||||
localStorage["new_scratch_label"] = ""
|
||||
localStorage["new_scratch_asm"] = ""
|
||||
|
||||
router.push(`/scratch/${scratch.slug}`)
|
||||
} catch (error) {
|
||||
setLineNumbers(true) // line numbers are likely relevant to the error
|
||||
if (error?.responseJSON?.as_errors) {
|
||||
throw new Error(error.responseJSON.as_errors.join("\n"))
|
||||
} else {
|
||||
console.error(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return <>
|
||||
<Head>
|
||||
<title>New scratch | decomp.me</title>
|
||||
</Head>
|
||||
<Nav />
|
||||
<main className={styles.container}>
|
||||
<div className={styles.heading}>
|
||||
<h1>Create a new scratch</h1>
|
||||
<p>
|
||||
A scratch is a playground where you can work on matching
|
||||
a given target function using any compiler options you like.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<hr className={styles.rule} />
|
||||
|
||||
<div>
|
||||
<p className={styles.label}>
|
||||
Architecture
|
||||
</p>
|
||||
<ArchSelect
|
||||
arches={serverCompilers.arches}
|
||||
value={arch}
|
||||
onChange={a => setArch(a)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className={styles.label}>
|
||||
Compiler
|
||||
</p>
|
||||
<div className={styles.compilerContainer}>
|
||||
<div>
|
||||
<span className={styles.compilerChoiceHeading}>Select a compiler</span>
|
||||
<Select
|
||||
className={styles.compilerChoiceSelect}
|
||||
options={compilers.reduce((options, compiler) => {
|
||||
return {
|
||||
...options,
|
||||
[compiler.id]: compiler.name,
|
||||
}
|
||||
}, {})}
|
||||
value={compiler}
|
||||
onChange={c => {
|
||||
setCompiler(c)
|
||||
setCompilerOpts("")
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.compilerChoiceOr}>or</div>
|
||||
<div>
|
||||
<span className={styles.compilerChoiceHeading}>Select a preset</span>
|
||||
<PresetSelect
|
||||
className={styles.compilerChoiceSelect}
|
||||
arch={arch}
|
||||
compiler={compiler}
|
||||
opts={compilerOpts}
|
||||
setCompiler={setCompiler}
|
||||
setOpts={setCompilerOpts}
|
||||
serverCompilers={serverCompilers.compilers}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr className={styles.rule} />
|
||||
|
||||
<div>
|
||||
<label className={styles.label} htmlFor="label">
|
||||
Function name <small>(label as it appears in the target asm)</small>
|
||||
</label>
|
||||
<input
|
||||
name="label"
|
||||
type="text"
|
||||
value={label}
|
||||
placeholder={defaultLabel}
|
||||
onChange={e => setLabel((e.target as HTMLInputElement).value)}
|
||||
className={styles.textInput}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.editorContainer}>
|
||||
<p className={styles.label}>Target assembly <small>(required)</small></p>
|
||||
<Editor
|
||||
className={styles.editor}
|
||||
language="mips"
|
||||
value={asm}
|
||||
onChange={setAsm}
|
||||
padding={10}
|
||||
showMargin={lineNumbers}
|
||||
lineNumbers={lineNumbers}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.editorContainer}>
|
||||
<p className={styles.label}>
|
||||
Context <small>(typically generated with m2ctx.py)</small>
|
||||
</p>
|
||||
<Editor
|
||||
className={styles.editor}
|
||||
language="c"
|
||||
value={context}
|
||||
onChange={setContext}
|
||||
padding={10}
|
||||
showMargin={lineNumbers}
|
||||
lineNumbers={lineNumbers}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<hr className={styles.rule} />
|
||||
|
||||
<div>
|
||||
<AsyncButton
|
||||
primary
|
||||
disabled={asm.length == 0}
|
||||
onClick={submit}
|
||||
errorPlacement="right-center"
|
||||
>
|
||||
Create scratch
|
||||
</AsyncButton>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
}
|
||||
@@ -6,23 +6,24 @@
|
||||
$accent-dark: color.scale($accent, $lightness: -40%, $saturation: -20%);
|
||||
|
||||
@if color.saturation($accent) == 0% {
|
||||
$accent-light: color.change($accent-light, $saturation: 0%)
|
||||
$accent-light: color.change($accent-light, $saturation: 0%);
|
||||
}
|
||||
|
||||
--frog-pupil: #292f33;
|
||||
--frog-primary: #{$accent};
|
||||
--frog-secondary: #{$accent-light};
|
||||
--frog-nose: #{$accent-dark};
|
||||
|
||||
--accent: #{$accent};
|
||||
--complement: #{color.complement($accent)};
|
||||
|
||||
@for $i from 1 through 40 {
|
||||
$multiplier: 2.5%;
|
||||
|
||||
--g#{$i * 50}: #{hsl(color.hue($bg), color.saturation($bg), ($i - 1) * $multiplier)};
|
||||
}
|
||||
|
||||
$peak: 0;
|
||||
|
||||
@if color.blackness($bg) > 50% {
|
||||
$peak: 255;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
.avatar {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background: #ffffff44;
|
||||
background: #fff4;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ export default function UserPage({ user: initialUser }: { user: api.User }) {
|
||||
height={64}
|
||||
/>}
|
||||
<h1 className={styles.name}>
|
||||
<div>{user.name} {user.is_you && <i>(you)</i>}</div>
|
||||
<div>{user.name}</div>
|
||||
<div className={styles.username}>
|
||||
@{user.username}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user