Context Menu, CSS Modules and React 19 (#1015)

This replaces the context menu with our own. By doing that we no longer
need to force install our npm packages, as there are no longer any
conflicts. In fact, as part of this change, I updated us to the latest
React 19 and even enabled the React compiler.

I also explored CSS modules for the first time, which are going to allow
us to reduce conflicts between CSS rules and possibly even allow us to
fully remove SASS, as the need for it is going to be reduced.

I also found out that the latest Rust nightly is broken due to their
update to LLVM 20. So I pinned the version to the last working nightly
version.
This commit is contained in:
Christopher Serr
2025-02-20 21:40:55 +00:00
committed by GitHub
parent 55162c047a
commit 349d95ebb0
50 changed files with 1287 additions and 1062 deletions
+2 -2
View File
@@ -70,7 +70,7 @@ jobs:
- name: Install Rust
uses: hecrj/setup-rust-action@v2
with:
rust-version: nightly
rust-version: nightly-2025-02-17
components: rust-src
targets: wasm32-unknown-unknown
@@ -114,7 +114,7 @@ jobs:
mv wasm-bindgen-${{ steps.wasm-bindgen.outputs.version }}-${{ matrix.target }}/wasm* .
- name: Install npm packages
run: npm ci -f
run: npm ci
env:
DETECT_CHROMEDRIVER_VERSION: true
+1 -1
View File
@@ -40,7 +40,7 @@ cargo install wasm-bindgen-cli
You need to set up some npm modules before compiling the project:
```bash
npm install -f
npm install
```
You are now ready to build livesplit-core, which powers LiveSplit One:
+7 -1
View File
@@ -28,11 +28,17 @@ if (process.argv.some((v) => v === "--max-opt")) {
if (process.argv.some((v) => v === "--unstable")) {
// Relaxed SIMD is not supported by Firefox and Safari yet.
rustFlags += ",+relaxed-simd";
// Not supported by any browser yet.
rustFlags += ",+wide-arithmetic";
}
// Use the nightly toolchain, which enables some more optimizations.
if (process.argv.some((v) => v === "--nightly")) {
toolchain = "+nightly";
// FIXME: Nightly is broken since the LLVM 20 upgrade. We need to wait for
// stdarch (which has the fix already) to be updated:
// https://github.com/rust-lang/stdarch/pull/1719
toolchain = "+nightly-2025-02-17";
cargoFlags +=
" -Z build-std=std,panic_abort -Z build-std-features=panic_immediate_abort";
rustFlags += " -Z wasm-c-abi=spec";
+215 -178
View File
@@ -8,38 +8,39 @@
"dependencies": {
"idb": "^8.0.0",
"markdown-it": "^14.1.0",
"react": "^18.3.1",
"react-contextmenu": "^2.14.0",
"react-dom": "^18.3.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-resizable": "^3.0.4",
"react-sidebar": "^3.0.2",
"react-toastify": "11.0.3"
"react-toastify": "^11.0.3"
},
"devDependencies": {
"@babel/plugin-proposal-explicit-resource-management": "^7.25.9",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.13",
"@tauri-apps/cli": "^1.6.0",
"@types/markdown-it": "^14.1.2",
"@types/node": "22.13.1",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/node": "^22.13.1",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@types/react-resizable": "^3.0.3",
"@types/react-sidebar": "^3.0.2",
"clean-webpack-plugin": "^4.0.0",
"css-loader": "7.1.2",
"css-loader": "^7.1.2",
"eslint": "^9.20.1",
"favicons-webpack-plugin": "^6.0.0",
"html-inline-script-webpack-plugin": "^3.1.0",
"html-webpack-plugin": "^5.6.0",
"react-compiler-webpack": "^0.1.2",
"react-refresh-typescript": "^2.0.7",
"sass": "^1.77.2",
"sass-loader": "16.0.4",
"sass-loader": "^16.0.4",
"style-loader": "^4.0.0",
"ts-loader": "^9.4.2",
"typescript": "^5.7.3",
"typescript-eslint": "^8.24.0",
"webpack": "^5.91.0",
"webpack-cli": "6.0.1",
"webpack-dev-server": "5.2.0",
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.0",
"workbox-webpack-plugin": "^7.0.0"
}
},
@@ -535,6 +536,22 @@
"@babel/core": "^7.0.0"
}
},
"node_modules/@babel/plugin-proposal-explicit-resource-management": {
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-explicit-resource-management/-/plugin-proposal-explicit-resource-management-7.25.9.tgz",
"integrity": "sha512-EbtfSvb6s4lZwef1nH52nw4DTUAvHY6bl1mbLgEHUkR6L8j4WY70mM/InB8Ozgdlok/7etbiSW+Wc88ZebZAKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-proposal-private-property-in-object": {
"version": "7.21.0-placeholder-for-preset-env.2",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz",
@@ -580,6 +597,38 @@
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-syntax-jsx": {
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz",
"integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-syntax-typescript": {
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz",
"integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-syntax-unicode-sets-regex": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz",
@@ -1760,32 +1809,19 @@
}
},
"node_modules/@eslint/plugin-kit": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz",
"integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==",
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.6.tgz",
"integrity": "sha512-+0TjwR1eAUdZtvv/ir1mGX+v0tUoR3VEPB8Up0LLJC+whRW0GgBBtpbOkg/a/U4Dxa6l5a3l9AJ1aWIQVyoWJA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@eslint/core": "^0.10.0",
"@eslint/core": "^0.11.0",
"levn": "^0.4.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@eslint/plugin-kit/node_modules/@eslint/core": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz",
"integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@types/json-schema": "^7.0.15"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@@ -1839,9 +1875,9 @@
}
},
"node_modules/@humanwhocodes/retry": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz",
"integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==",
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz",
"integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
@@ -3337,9 +3373,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "22.13.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.1.tgz",
"integrity": "sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew==",
"version": "22.13.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.4.tgz",
"integrity": "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3356,13 +3392,6 @@
"@types/node": "*"
}
},
"node_modules/@types/prop-types": {
"version": "15.7.14",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
"integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/qs": {
"version": "6.9.18",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz",
@@ -3378,24 +3407,23 @@
"license": "MIT"
},
"node_modules/@types/react": {
"version": "18.3.18",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz",
"integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==",
"version": "19.0.10",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.10.tgz",
"integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
}
},
"node_modules/@types/react-dom": {
"version": "18.3.5",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.5.tgz",
"integrity": "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==",
"version": "19.0.4",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.4.tgz",
"integrity": "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^18.0.0"
"@types/react": "^19.0.0"
}
},
"node_modules/@types/react-resizable": {
@@ -3493,17 +3521,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.24.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.24.0.tgz",
"integrity": "sha512-aFcXEJJCI4gUdXgoo/j9udUYIHgF23MFkg09LFz2dzEmU0+1Plk4rQWv/IYKvPHAtlkkGoB3m5e6oUp+JPsNaQ==",
"version": "8.24.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.24.1.tgz",
"integrity": "sha512-ll1StnKtBigWIGqvYDVuDmXJHVH4zLVot1yQ4fJtLpL7qacwkxJc1T0bptqw+miBQ/QfUbhl1TcQ4accW5KUyA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.24.0",
"@typescript-eslint/type-utils": "8.24.0",
"@typescript-eslint/utils": "8.24.0",
"@typescript-eslint/visitor-keys": "8.24.0",
"@typescript-eslint/scope-manager": "8.24.1",
"@typescript-eslint/type-utils": "8.24.1",
"@typescript-eslint/utils": "8.24.1",
"@typescript-eslint/visitor-keys": "8.24.1",
"graphemer": "^1.4.0",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
@@ -3523,16 +3551,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.24.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.24.0.tgz",
"integrity": "sha512-MFDaO9CYiard9j9VepMNa9MTcqVvSny2N4hkY6roquzj8pdCBRENhErrteaQuu7Yjn1ppk0v1/ZF9CG3KIlrTA==",
"version": "8.24.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.24.1.tgz",
"integrity": "sha512-Tqoa05bu+t5s8CTZFaGpCH2ub3QeT9YDkXbPd3uQ4SfsLoh1/vv2GEYAioPoxCWJJNsenXlC88tRjwoHNts1oQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.24.0",
"@typescript-eslint/types": "8.24.0",
"@typescript-eslint/typescript-estree": "8.24.0",
"@typescript-eslint/visitor-keys": "8.24.0",
"@typescript-eslint/scope-manager": "8.24.1",
"@typescript-eslint/types": "8.24.1",
"@typescript-eslint/typescript-estree": "8.24.1",
"@typescript-eslint/visitor-keys": "8.24.1",
"debug": "^4.3.4"
},
"engines": {
@@ -3548,14 +3576,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.24.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.24.0.tgz",
"integrity": "sha512-HZIX0UByphEtdVBKaQBgTDdn9z16l4aTUz8e8zPQnyxwHBtf5vtl1L+OhH+m1FGV9DrRmoDuYKqzVrvWDcDozw==",
"version": "8.24.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.24.1.tgz",
"integrity": "sha512-OdQr6BNBzwRjNEXMQyaGyZzgg7wzjYKfX2ZBV3E04hUCBDv3GQCHiz9RpqdUIiVrMgJGkXm3tcEh4vFSHreS2Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.24.0",
"@typescript-eslint/visitor-keys": "8.24.0"
"@typescript-eslint/types": "8.24.1",
"@typescript-eslint/visitor-keys": "8.24.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3566,14 +3594,14 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.24.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.24.0.tgz",
"integrity": "sha512-8fitJudrnY8aq0F1wMiPM1UUgiXQRJ5i8tFjq9kGfRajU+dbPyOuHbl0qRopLEidy0MwqgTHDt6CnSeXanNIwA==",
"version": "8.24.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.24.1.tgz",
"integrity": "sha512-/Do9fmNgCsQ+K4rCz0STI7lYB4phTtEXqqCAs3gZW0pnK7lWNkvWd5iW545GSmApm4AzmQXmSqXPO565B4WVrw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "8.24.0",
"@typescript-eslint/utils": "8.24.0",
"@typescript-eslint/typescript-estree": "8.24.1",
"@typescript-eslint/utils": "8.24.1",
"debug": "^4.3.4",
"ts-api-utils": "^2.0.1"
},
@@ -3590,9 +3618,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.24.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.24.0.tgz",
"integrity": "sha512-VacJCBTyje7HGAw7xp11q439A+zeGG0p0/p2zsZwpnMzjPB5WteaWqt4g2iysgGFafrqvyLWqq6ZPZAOCoefCw==",
"version": "8.24.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.24.1.tgz",
"integrity": "sha512-9kqJ+2DkUXiuhoiYIUvIYjGcwle8pcPpdlfkemGvTObzgmYfJ5d0Qm6jwb4NBXP9W1I5tss0VIAnWFumz3mC5A==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3604,14 +3632,14 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.24.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.24.0.tgz",
"integrity": "sha512-ITjYcP0+8kbsvT9bysygfIfb+hBj6koDsu37JZG7xrCiy3fPJyNmfVtaGsgTUSEuTzcvME5YI5uyL5LD1EV5ZQ==",
"version": "8.24.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.24.1.tgz",
"integrity": "sha512-UPyy4MJ/0RE648DSKQe9g0VDSehPINiejjA6ElqnFaFIhI6ZEiZAkUI0D5MCk0bQcTf/LVqZStvQ6K4lPn/BRg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.24.0",
"@typescript-eslint/visitor-keys": "8.24.0",
"@typescript-eslint/types": "8.24.1",
"@typescript-eslint/visitor-keys": "8.24.1",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@@ -3657,16 +3685,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.24.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.24.0.tgz",
"integrity": "sha512-07rLuUBElvvEb1ICnafYWr4hk8/U7X9RDCOqd9JcAMtjh/9oRmcfN4yGzbPVirgMR0+HLVHehmu19CWeh7fsmQ==",
"version": "8.24.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.24.1.tgz",
"integrity": "sha512-OOcg3PMMQx9EXspId5iktsI3eMaXVwlhC8BvNnX6B5w9a4dVgpkQZuU8Hy67TolKcl+iFWq0XX+jbDGN4xWxjQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "8.24.0",
"@typescript-eslint/types": "8.24.0",
"@typescript-eslint/typescript-estree": "8.24.0"
"@typescript-eslint/scope-manager": "8.24.1",
"@typescript-eslint/types": "8.24.1",
"@typescript-eslint/typescript-estree": "8.24.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3681,13 +3709,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.24.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.24.0.tgz",
"integrity": "sha512-kArLq83QxGLbuHrTMoOEWO+l2MwsNS2TGISEdx8xgqpkbytB07XmlQyQdNDrCc1ecSqx0cnmhGvpX+VBwqqSkg==",
"version": "8.24.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.24.1.tgz",
"integrity": "sha512-EwVHlp5l+2vp8CoqJm9KikPZgi3gbdZAtabKT9KPShGeOcJhsv4Zdo3oc8T8I0uKEmYoU4ItyxbptjF08enaxg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.24.0",
"@typescript-eslint/types": "8.24.1",
"eslint-visitor-keys": "^4.2.0"
},
"engines": {
@@ -4272,6 +4300,17 @@
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
}
},
"node_modules/babel-plugin-react-compiler": {
"version": "19.0.0-beta-21e868a-20250216",
"resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-19.0.0-beta-21e868a-20250216.tgz",
"integrity": "sha512-WDOBsm9t9P0RADm8CSlav5OqWvs+3mZFvrBo/qf3vuNtdz78OG5TFxOy7De8ePR3rA6qg1Qmcjjae6nR1pOpCA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/types": "^7.19.0"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -4531,9 +4570,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001699",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001699.tgz",
"integrity": "sha512-b+uH5BakXZ9Do9iK+CkDmctUSEqZl+SP056vc5usa0PL+ev5OHw003rZXcnjNDv3L8P5j6rwT6C0BPKSikW08w==",
"version": "1.0.30001700",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001700.tgz",
"integrity": "sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ==",
"dev": true,
"funding": [
{
@@ -4594,12 +4633,6 @@
"node": ">=6.0"
}
},
"node_modules/classnames": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
"license": "MIT"
},
"node_modules/clean-css": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz",
@@ -4655,9 +4688,9 @@
}
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
"license": "MIT",
"engines": {
"node": ">=6"
@@ -5365,9 +5398,9 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.101",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.101.tgz",
"integrity": "sha512-L0ISiQrP/56Acgu4/i/kfPwWSgrzYZUnQrC0+QPFuhqlLP1Ir7qzPPDVS9BcKIyWTRU8+o6CC8dKw38tSWhYIA==",
"version": "1.5.102",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.102.tgz",
"integrity": "sha512-eHhqaja8tE/FNpIiBrvBjFV/SSKpyWHLvxuR9dPTdo+3V9ppdLmFB7ZZQ98qNovcngPLYIz0oOBF9P0FfZef5Q==",
"dev": true,
"license": "ISC"
},
@@ -6145,9 +6178,9 @@
}
},
"node_modules/flatted": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz",
"integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==",
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
"dev": true,
"license": "ISC"
},
@@ -7762,9 +7795,9 @@
}
},
"node_modules/launch-editor": {
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.9.1.tgz",
"integrity": "sha512-Gcnl4Bd+hRO9P9icCP/RVVT2o8SFlPXofuCxvA2SaZuH45whSvf5p8x5oih5ftLiVhEI4sp5xDY+R+b3zJBh5w==",
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.10.0.tgz",
"integrity": "sha512-D7dBRJo/qcGX9xlvt/6wUYzQxjh5G1RvZPgPv8vi4KRU99DVQL/oW7tnVOCCTm2HGeo3C5HvGE5Yrh6UBoZ0vA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -8682,9 +8715,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.2",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.2.tgz",
"integrity": "sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==",
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
"dev": true,
"funding": [
{
@@ -8963,43 +8996,64 @@
}
},
"node_modules/react": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
"integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-contextmenu": {
"version": "2.14.0",
"resolved": "https://registry.npmjs.org/react-contextmenu/-/react-contextmenu-2.14.0.tgz",
"integrity": "sha512-ktqMOuad6sCFNJs/ltEwppN8F0YeXmqoZfwycgtZR/MxOXMYx1xgYC44SzWH259HdGyshk1/7sXGuIRwj9hzbw==",
"node_modules/react-compiler-webpack": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/react-compiler-webpack/-/react-compiler-webpack-0.1.2.tgz",
"integrity": "sha512-NaT5Ft4FRqiDAllr8ywjmXWcv0+ouL/4GsYiLHj783C2HbTp4SGUtUC/6vpWT/UQ5LggV+PUMLUrdEdua+LOaQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"classnames": "^2.2.5",
"object-assign": "^4.1.0"
"@babel/core": "^7.24.6",
"@babel/plugin-syntax-jsx": "^7.24.6",
"@babel/plugin-syntax-typescript": "^7.24.6",
"loader-utils": "^3.2.2"
},
"peerDependencies": {
"prop-types": "^15.0.0",
"react": "^0.14.0 || ^15.0.0 || ^16.0.1",
"react-dom": "^0.14.0 || ^15.0.0 || ^16.0.1"
"babel-plugin-react-compiler": "*"
}
},
"node_modules/react-compiler-webpack/node_modules/loader-utils": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz",
"integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 12.13.0"
}
},
"node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz",
"integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
"scheduler": "^0.25.0"
},
"peerDependencies": {
"react": "^18.3.1"
"react": "^19.0.0"
}
},
"node_modules/react-draggable": {
"version": "4.4.6",
"resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.6.tgz",
"integrity": "sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==",
"license": "MIT",
"dependencies": {
"clsx": "^1.1.1",
"prop-types": "^15.8.1"
},
"peerDependencies": {
"react": ">= 16.3.0",
"react-dom": ">= 16.3.0"
}
},
"node_modules/react-is": {
@@ -9043,29 +9097,6 @@
"react": ">= 16.3"
}
},
"node_modules/react-resizable/node_modules/clsx": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/react-resizable/node_modules/react-draggable": {
"version": "4.4.6",
"resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.6.tgz",
"integrity": "sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==",
"license": "MIT",
"dependencies": {
"clsx": "^1.1.1",
"prop-types": "^15.8.1"
},
"peerDependencies": {
"react": ">= 16.3.0",
"react-dom": ">= 16.3.0"
}
},
"node_modules/react-sidebar": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/react-sidebar/-/react-sidebar-3.0.2.tgz",
@@ -9093,6 +9124,15 @@
"react-dom": "^18 || ^19"
}
},
"node_modules/react-toastify/node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
@@ -9554,9 +9594,9 @@
}
},
"node_modules/sass-loader": {
"version": "16.0.4",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.4.tgz",
"integrity": "sha512-LavLbgbBGUt3wCiYzhuLLu65+fWXaXLmq7YxivLhEqmiupCFZ5sKUAipK3do6V80YSU0jvSxNhEdT13IXNr3rg==",
"version": "16.0.5",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.5.tgz",
"integrity": "sha512-oL+CMBXrj6BZ/zOq4os+UECPL+bWqt6OAC6DWS8Ln8GZRcMDjlJ4JC3FBDuHJdYaFWIdKNIBYmtZtK2MaMkNIw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -9603,13 +9643,10 @@
"peer": true
},
"node_modules/scheduler": {
"version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
}
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",
"integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==",
"license": "MIT"
},
"node_modules/schema-utils": {
"version": "4.3.0",
@@ -10663,9 +10700,9 @@
}
},
"node_modules/type-fest": {
"version": "4.34.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.34.1.tgz",
"integrity": "sha512-6kSc32kT0rbwxD6QL1CYe8IqdzN/J/ILMrNK+HMQCKH3insCDRY/3ITb0vcBss0a3t72fzh2YSzj8ko1HgwT3g==",
"version": "4.35.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.35.0.tgz",
"integrity": "sha512-2/AwEFQDFEy30iOLjrvHDIH7e4HEWH+f1Yl1bI5XMqzuoCUqwYCdxachgsgv0og/JdVZUhbfjcJAoHj5L1753A==",
"dev": true,
"license": "(MIT OR CC0-1.0)",
"optional": true,
@@ -10784,15 +10821,15 @@
}
},
"node_modules/typescript-eslint": {
"version": "8.24.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.24.0.tgz",
"integrity": "sha512-/lmv4366en/qbB32Vz5+kCNZEMf6xYHwh1z48suBwZvAtnXKbP+YhGe8OLE2BqC67LMqKkCNLtjejdwsdW6uOQ==",
"version": "8.24.1",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.24.1.tgz",
"integrity": "sha512-cw3rEdzDqBs70TIcb0Gdzbt6h11BSs2pS0yaq7hDWDBtCCSei1pPSUXE9qUdQ/Wm9NgFg8mKtMt1b8fTHIl1jA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.24.0",
"@typescript-eslint/parser": "8.24.0",
"@typescript-eslint/utils": "8.24.0"
"@typescript-eslint/eslint-plugin": "8.24.1",
"@typescript-eslint/parser": "8.24.1",
"@typescript-eslint/utils": "8.24.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+12 -11
View File
@@ -17,38 +17,39 @@
"dependencies": {
"idb": "^8.0.0",
"markdown-it": "^14.1.0",
"react": "^18.3.1",
"react-contextmenu": "^2.14.0",
"react-dom": "^18.3.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-resizable": "^3.0.4",
"react-sidebar": "^3.0.2",
"react-toastify": "11.0.3"
"react-toastify": "^11.0.3"
},
"devDependencies": {
"@babel/plugin-proposal-explicit-resource-management": "^7.25.9",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.13",
"@tauri-apps/cli": "^1.6.0",
"@types/markdown-it": "^14.1.2",
"@types/node": "22.13.1",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/node": "^22.13.1",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@types/react-resizable": "^3.0.3",
"@types/react-sidebar": "^3.0.2",
"clean-webpack-plugin": "^4.0.0",
"css-loader": "7.1.2",
"css-loader": "^7.1.2",
"eslint": "^9.20.1",
"favicons-webpack-plugin": "^6.0.0",
"html-inline-script-webpack-plugin": "^3.1.0",
"html-webpack-plugin": "^5.6.0",
"react-compiler-webpack": "^0.1.2",
"react-refresh-typescript": "^2.0.7",
"sass": "^1.77.2",
"sass-loader": "16.0.4",
"sass-loader": "^16.0.4",
"style-loader": "^4.0.0",
"ts-loader": "^9.4.2",
"typescript": "^5.7.3",
"typescript-eslint": "^8.24.0",
"webpack": "^5.91.0",
"webpack-cli": "6.0.1",
"webpack-dev-server": "5.2.0",
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.0",
"workbox-webpack-plugin": "^7.0.0"
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
@use 'mobile';
@use 'variables';
@use 'variables.icss';
$icon-size: 40px;
$title-font-size: 40px;
+225
View File
@@ -0,0 +1,225 @@
@use 'variables.icss';
.colorPickerButton {
border: 2px solid white;
border-radius: 2px;
box-shadow: 0 0 0 1px rgba(0,0,0,.1);
cursor: pointer;
box-sizing: border-box;
height: variables.$settings-row-height;
}
.colorPickerDialogPositioning {
margin: 0 auto;
width: 0;
}
.overlay {
inset: 0;
position: fixed;
z-index: 1;
}
.glassPanel {
background-color: rgba(28, 28, 28, 0.8);
backdrop-filter: blur(5px);
z-index: 1;
position: absolute;
margin-top: 5px;
margin-left: -113.5px;
border: 1px solid rgba(255, 255, 255, 0.25);
box-shadow: 0 5px 10px 0px rgba(28, 28, 28, 0.8);
}
.hr {
margin: 0;
height: 1px;
border-width: 0px;
background: rgba(255, 255, 255, 0.25);
}
.gradientSelector {
overflow: hidden;
position: relative;
.whiteGradient {
background: linear-gradient(to right, white, transparent);
.cursor {
pointer-events: none;
position: absolute;
width: 12px;
height: 12px;
border-radius: 6px;
box-shadow: black 0 0 0 2px inset;
>div {
width: 100%;
height: 100%;
border-radius: 6px;
box-shadow: white 0 0 0 1px inset;
}
}
.blackGradient {
width: 225px;
height: 125px;
background: linear-gradient(to top, black, transparent);
}
}
}
.controlPanel {
margin: 10px;
display: flex;
flex-direction: column;
gap: 10px;
.controlPanelTop {
display: flex;
gap: 10px;
align-items: center;
.checker {
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAADFJREFUOE9jZGBgEGHAD97gk2YcNYBhmIQBgWSAP52AwoAQwJvQRg1gACckQoC2gQgAIF8IscwEtKYAAAAASUVORK5CYII=") left center;
}
.sliders {
flex-grow: 1;
display: flex;
flex-direction: column;
gap: 10px;
.hueSlider {
background: linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%);
height: 18px;
position: relative;
border-radius: 9px;
input {
margin: 0;
background: none;
height: 16px;
position: absolute;
top: 1px;
left: 1px;
}
}
.alphaSlider {
background: white;
border-radius: 9px;
overflow: hidden;
height: 18px;
position: relative;
input {
margin: 0;
background: none;
height: 16px;
position: absolute;
top: 1px;
left: 1px;
}
}
}
}
.controlPanelBottom {
display: flex;
gap: 10px;
cursor: ew-resize;
}
input[type="range"] {
/* Hack for LSO to prevent global style */
all: revert;
appearance: none;
accent-color: white;
width: calc(100% - 2px);
}
}
.colorPreview {
border: 1px solid rgba(255, 255, 255, 0.25);
border-radius: 50%;
background: white;
position: relative;
overflow: hidden;
.colorPreviewInner {
width: 35px;
height: 35px;
display: flex;
justify-content: center;
align-items: center;
}
}
.colorInput {
display: flex;
flex-direction: column;
gap: 5px;
font-size: 12px;
color: rgba(255, 255, 255, 0.7);
align-items: center;
flex: 1;
width: 0;
user-select: none;
>input {
width: 100%;
/* Hack for LSO to prevent global input height style */
height: 25px !important;
font-family: inherit;
font-size: 14px;
text-align: center;
color: white;
font-variant-numeric: tabular-nums;
background: transparent;
border: 1px solid rgba(255, 255, 255, 0.25);
border-radius: 5px;
padding: 0;
}
}
.predefinedColors {
margin: 10px;
display: flex;
flex-direction: column;
gap: 10px;
.predefinedColorsRow {
display: flex;
justify-content: space-between;
}
}
.predefinedColor {
/* Hack for LSO to prevent global button style */
all: revert;
border: 3px solid transparent;
border-radius: 50%;
width: 20px;
height: 20px;
cursor: pointer;
transition: all .05s, border-color .25s;
/* Fix for iOS minimum button width */
font-size: 0;
&:hover {
/* Hack for LSO to prevent global button style */
background: revert;
border: 3px solid white;
}
&:active {
margin-top: 3px;
height: 17px;
}
&:active:hover {
/* Hack for LSO to prevent global button style */
background: revert;
}
}
-88
View File
@@ -1,88 +0,0 @@
@use 'variables';
.color-picker-button {
border: 2px solid white;
border-radius: 2px;
box-shadow: 0 0 0 1px rgba(0,0,0,.1);
cursor: pointer;
box-sizing: border-box;
height: variables.$settings-row-height;
}
.color-picker {
.blur {
background-color: rgba(28, 28, 28, 0.8);
backdrop-filter: blur(5px);
}
input[type="range"] {
/* Hack for LSO to prevent global style */
all: revert;
appearance: none;
accent-color: white;
width: calc(100% - 2px);
}
.predefined-color {
/* Hack for LSO to prevent global button style */
all: revert;
border: 3px solid transparent;
border-radius: 50%;
width: 20px;
height: 20px;
cursor: pointer;
transition: all .05s, border-color .25s;
/* Fix for iOS minimum button width */
font-size: 0;
&:hover {
border: 3px solid white;
}
&:active {
margin-top: 3px;
height: 17px;
}
&:active:hover {
/* Hack for LSO to prevent global button style */
background: revert;
}
}
.color-input {
display: flex;
flex-direction: column;
gap: 5px;
font-size: 12px;
color: rgba(255, 255, 255, 0.7);
align-items: center;
flex: 1;
width: 0;
user-select: none;
>input {
width: 100%;
/* Hack for LSO to prevent global input height style */
height: 25px !important;
font-size: 14px;
text-align: center;
color: white;
font-variant-numeric: tabular-nums;
background: transparent;
border: 1px solid rgba(255, 255, 255, 0.25);
border-radius: 5px;
padding: 0;
}
}
.checker {
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAADFJREFUOE9jZGBgEGHAD97gk2YcNYBhmIQBgWSAP52AwoAQwJvQRg1gACckQoC2gQgAIF8IscwEtKYAAAAASUVORK5CYII=") left center;
}
.overlay {
inset: 0;
position: fixed;
z-index: 1;
}
}
+45
View File
@@ -0,0 +1,45 @@
.contextMenu {
position: fixed;
z-index: 3;
}
.panel {
z-index: 3;
background-color: rgba(28, 28, 28, .8);
backdrop-filter: blur(5px);
min-width: 100px;
padding: 4px 0;
border: 1px solid rgba(255, 255, 255, .25);
border-radius: 9px;
box-shadow: 0 5px 10px 0px rgba(28, 28, 28, .8);
display: flex;
flex-direction: column;
user-select: none;
text-align: left;
font-weight: initial;
}
.overlay {
inset: 0;
position: fixed;
}
.entry {
padding: 4px;
cursor: pointer;
font-size: 16px;
transition: background-color .25s;
border-radius: 5px;
margin: 0 4px;
&:hover {
background-color: rgba(255, 255, 255, .25);
}
}
.hr {
height: 1px;
border-width: 0px;
background: rgba(255, 255, 255, .25);
margin: 4px 0;
}
-48
View File
@@ -1,48 +0,0 @@
@use 'sass:math';
@use 'variables';
@mixin context-menu {
.react-contextmenu {
min-width: 160px;
padding: math.div(variables.$ui-margin, 2) 0;
margin: 2px 0 0;
font-size: 16px;
text-align: left;
background-color: variables.$dark-row-color;
background-clip: padding-box;
border: 1px solid variables.$border-color;
border-radius: 0.25rem;
outline: none;
opacity: 0;
pointer-events: none;
transition: opacity 250ms ease !important;
z-index: 3;
&.react-contextmenu--visible {
opacity: 1;
pointer-events: auto;
}
.react-contextmenu-item {
padding: math.div(variables.$ui-margin, 2) variables.$ui-margin;
font-weight: 400;
text-align: inherit;
white-space: nowrap;
background: 0 0;
border: 0;
cursor: pointer;
}
.react-contextmenu-item--divider {
margin-bottom: math.div(variables.$ui-margin, 2);
padding: math.div(variables.$ui-margin, 2) 0 0 0;
border-bottom: 1px solid variables.$border-color;
cursor: inherit;
}
.react-contextmenu-item--selected {
background: variables.$selected-row-color;
}
}
}
+5 -4
View File
@@ -1,4 +1,4 @@
@use 'variables';
@use 'variables.icss';
.is-mobile+dialog>.dialog {
min-width: auto;
@@ -15,8 +15,9 @@ dialog {
.dialog {
color: #eee;
background: variables.$main-background-color;
border: 2px solid variables.$border-color;
background-color: rgba(28, 28, 28, .8);
backdrop-filter: blur(5px);
border: 2px solid rgba(255, 255, 255, .25);
border-radius: 10px;
min-width: 225px;
@@ -50,7 +51,7 @@ dialog {
input {
width: 100%;
border: none;
border-bottom: 1px solid hsla(0, 0%, 100%, 0.25);
border-bottom: 1px solid rgba(255, 255, 255, .25);
background: transparent;
color: white;
text-overflow: ellipsis;
+1 -1
View File
@@ -1,5 +1,5 @@
@use 'mobile';
@use 'variables';
@use 'variables.icss';
#upload-drop-zone {
position: absolute;
+1 -1
View File
@@ -1,4 +1,4 @@
@use 'variables';
@use 'variables.icss';
.hotkey-box {
button {
+1 -3
View File
@@ -1,13 +1,11 @@
@use 'mobile';
@use 'ContextMenu';
@use 'Table';
@use 'Toggle';
@use "variables";
@use "variables.icss";
.layout-editor-outer {
display: inline-flex;
@include ContextMenu.context-menu;
@include Table.table;
@include Toggle.toggle;
+1 -1
View File
@@ -1,6 +1,6 @@
@use 'mobile';
@use 'Sidebar';
@use 'variables';
@use 'variables.icss';
$sidebar-button-height: 42px;
+1 -1
View File
@@ -1,4 +1,4 @@
@use 'variables';
@use 'variables.icss';
.livesplit-server-button {
margin: 0;
+1 -1
View File
@@ -1,4 +1,4 @@
@use 'variables';
@use 'variables.icss';
@mixin markdown {
.flag {
+4 -14
View File
@@ -1,11 +1,10 @@
@use 'sass:math';
@use "ContextMenu";
@use "Markdown";
@use "mobile";
@use "Table";
@use "Toggle";
@use "variables";
@use "variables.icss";
$tab-bar-height: 30px + math.div(variables.$ui-margin, 2);
$tab-width: 625px;
@@ -18,7 +17,6 @@ $small-font-size: 13px;
$small-button-padding: 1px 3px 1px 3px;
.run-editor {
@include ContextMenu.context-menu;
@include Markdown.markdown;
@include Table.table;
@include Toggle.toggle;
@@ -205,18 +203,10 @@ $small-button-padding: 1px 3px 1px 3px;
}
&.segment-icon-container {
width: $segment-icon-size;
height: $segment-icon-size;
cursor: pointer;
div {
height: $segment-icon-size;
.segment-icon {
width: 100%;
height: 100%;
object-fit: contain;
}
}
display: flex;
justify-content: center;
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
@use 'sass:math';
@use 'Toggle';
@use 'variables';
@use 'variables.icss';
$h1-font-size: 24px;
$h2-font-size: 22px;
+1 -4
View File
@@ -1,9 +1,8 @@
@use 'sass:math';
@use 'mobile';
@use 'variables';
@use 'variables.icss';
@use 'Table';
@use 'ContextMenu';
$loading-text-font-size: 40px;
$splits-row-width: 500px;
@@ -86,8 +85,6 @@ $splits-row-height: 40px;
flex-shrink: 0;
margin-left: variables.$ui-large-margin;
@include ContextMenu.context-menu;
button {
background: transparent;
border: 0;
@@ -1,3 +1,10 @@
.label {
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
}
.switch {
position: relative;
width: 35px;
+7 -7
View File
@@ -1,7 +1,7 @@
@use 'sass:math';
@use 'mobile';
@use 'variables';
@use 'variables.icss';
@mixin table {
.table {
@@ -74,14 +74,14 @@
}
}
input {
font-size: 15px;
border: none;
border-bottom: 1px solid hsla(0, 0%, 100%, 0.25);
input.text-box {
font-family: inherit;
background: transparent;
color: white;
text-overflow: ellipsis;
font-family: "fira", sans-serif;
font-size: 15px;
border: none;
border-bottom: 1px solid hsla(0, 0%, 100%, 0.25);
}
input[Type="text"] {
@@ -94,7 +94,7 @@
border: 1px solid variables.$border-color;
color: white;
text-overflow: ellipsis;
font-family: "fira", sans-serif;
font-family: inherit;
}
.settings-table {
+1 -1
View File
@@ -1,7 +1,7 @@
@use 'sass:math';
@use 'mobile';
@use 'variables';
@use 'variables.icss';
.buttons {
margin-top: variables.$ui-large-margin - math.div(variables.$ui-margin, 2);
+1 -1
View File
@@ -1,4 +1,4 @@
@use 'variables';
@use 'variables.icss';
@mixin toggle {
.toggle-left {
+5 -4
View File
@@ -1,5 +1,5 @@
@use 'mobile';
@use 'variables';
@use 'variables.icss';
.tooltip {
position: relative;
@@ -8,8 +8,9 @@
.tooltip .tooltip-text {
visibility: hidden;
width: 300px;
background-color: variables.$sidebar-background-color;
border: 1px solid variables.$border-color;
background-color: rgba(28, 28, 28, .8);
backdrop-filter: blur(5px);
border: 1px solid rgba(255, 255, 255, .25);
color: #fff;
text-align: center;
border-radius: 6px;
@@ -31,7 +32,7 @@
}
}
.react-contextmenu-item.tooltip .tooltip-text {
.contextmenu-item.tooltip .tooltip-text {
bottom: initial;
left: calc(100% + variables.$ui-margin);
margin-left: 0;
+4 -3
View File
@@ -1,7 +1,7 @@
@use 'sass:math';
@use 'browser_source';
@use 'variables';
@use 'variables.icss';
@font-face {
font-family: timer;
@@ -25,8 +25,9 @@ body {
.Toastify {
.toast-class {
font-family: "fira", sans-serif;
background: variables.$sidebar-background-color !important;
border: 2px solid variables.$border-color !important;
background: rgba(28, 28, 28, .8) !important;
backdrop-filter: blur(5px);
border: 2px solid rgba(255, 255, 255, .25) !important;
border-radius: 10px !important;
min-height: 48px !important;
+1 -1
View File
@@ -36,7 +36,7 @@ export default class Layout extends React.Component<Props, unknown> {
<div className="layout" style={{ width: this.props.width, height: this.props.height }}>
<div
style={{ width: "inherit", height: "inherit" }}
ref={(element) => element?.appendChild(this.props.renderer.element())}
ref={(element) => { element?.appendChild(this.props.renderer.element()); }}
/>
{
this.props.allowResize && <div className="resizable-layout">
-12
View File
@@ -1,12 +0,0 @@
declare module "commonmark-react-renderer" {
class ReactRenderer {
constructor(options?: Options);
render(ast: any): JSX.Element;
}
interface Options {
escapeHtml?: boolean,
linkTarget?: string,
softBreak?: string,
}
export = ReactRenderer;
}
-10
View File
@@ -1,10 +0,0 @@
declare module "emojilib" {
export const lib: {
[emoji_name: string]: Emoji;
}
export interface Emoji {
char: string,
keywords: string[],
}
}
-15
View File
@@ -1,15 +0,0 @@
declare module "react-contextmenu" {
import { ReactNode } from "react";
export interface ContextMenuProps {
children?: ReactNode,
}
export interface ContextMenuTriggerProps {
children?: ReactNode,
}
export interface MenuItemProps {
children?: ReactNode,
}
}
-10
View File
@@ -1,10 +0,0 @@
declare module "react-linkifier" {
import { Component, ReactNode } from "react";
export interface Props {
target?: string,
children: ReactNode,
}
export default class Linkifier extends Component<Props> { }
}
+2 -2
View File
@@ -2,7 +2,7 @@ import * as React from "react";
import LiveSplitIcon from "../assets/icon.svg";
import { Markdown } from "../util/Markdown";
import * as variables from "../css/variables.scss";
import * as variables from "../css/variables.icss.scss";
import "../css/About.scss";
@@ -11,7 +11,7 @@ export interface Props {
}
interface Callbacks {
renderViewWithSidebar(renderedView: JSX.Element, sidebarContent: JSX.Element): JSX.Element,
renderViewWithSidebar(renderedView: React.JSX.Element, sidebarContent: React.JSX.Element): React.JSX.Element,
openTimerView(): void,
}
+88 -176
View File
@@ -1,9 +1,9 @@
import * as React from "react";
import React, { ChangeEvent, MouseEvent } from "react";
import { useEffect, useState } from "react";
import { colorToCss } from "../util/ColorUtil";
import { Color } from "../livesplit-core";
import "../css/ColorPicker.scss";
import * as classes from "../css/ColorPicker.module.scss";
const EyeDropper = (window as any).EyeDropper;
const hasEyeDropper = !!EyeDropper;
@@ -13,14 +13,11 @@ export default function ColorPicker({ color, setColor }: { color: Color, setColo
return (
<div>
<div
className="color-picker-button"
className={classes.colorPickerButton}
style={{ background: colorToCss(color) }}
onClick={() => setIsShowing(true)}
/>
<div style={{
margin: "0 auto",
width: "0",
}}>
<div className={classes.colorPickerDialogPositioning}>
{isShowing && <ColorPickerDialog color={color} setColor={setColor} close={() => setIsShowing(false)} />}
</div>
</div>
@@ -29,67 +26,48 @@ export default function ColorPicker({ color, setColor }: { color: Color, setColo
function ColorPickerDialog({ color, setColor, close }: { color: Color, setColor: (color: Color) => void, close: () => void }) {
return (
<div style={{ margin: "0 auto", width: 0 }} className="color-picker">
<div className="overlay" onClick={() => close()}></div>
<div
className="blur"
style={{
zIndex: 1,
position: "absolute",
marginTop: "5px",
marginLeft: "-113.5px",
border: "1px solid rgba(255, 255, 255, 0.25)",
boxShadow: "0 5px 10px 0px rgba(28, 28, 28, 0.8)",
}}
>
<>
<div className={classes.overlay} onClick={close} />
<div className={classes.glassPanel} >
<GradientSelector color={color} setColor={setColor} />
<Hr />
<ControlPanel color={color} setColor={setColor} />
<Hr />
<PredefinedColors setColor={setColor} />
</div>
</div>
</>
);
}
function Hr() {
return (
<hr
style={{
margin: 0,
height: "1px",
borderWidth: "0px",
background: "rgba(255, 255, 255, 0.25)",
}}
/>
);
return <hr className={classes.hr} />;
}
function GradientSelector({ color, setColor }: { color: Color, setColor: (color: Color) => void }) {
const [r, g, b, a] = color;
const [h, s, v] = rgbToHsv(r, g, b);
const [r1, g1, b1] = hsvToRgb(h, 1.0, 1.0);
const [r1, g1, b1] = hsvToRgb(h, 1, 1);
const [mouseDown, setMouseDown] = useState(false);
const handleMouseDown = (e: React.MouseEvent) => {
const handleMouseDown = (e: MouseEvent) => {
e.preventDefault();
(document.activeElement as any)?.blur();
(document.activeElement as any)?.blur?.();
setMouseDown(true);
updateColor(e);
};
const handleMouseUp = (e: React.MouseEvent) => {
const handleMouseUp = (e: MouseEvent) => {
e.preventDefault();
setMouseDown(false);
};
const handleMouseMove = (e: React.MouseEvent) => {
const handleMouseMove = (e: MouseEvent) => {
e.preventDefault();
if (!mouseDown) return;
updateColor(e);
};
const updateColor = (e: React.MouseEvent) => {
const updateColor = (e: MouseEvent) => {
const pos = e.currentTarget.getBoundingClientRect();
const x = Math.min(Math.max(e.clientX - pos.left, 0), 225);
const y = Math.min(Math.max(e.clientY - pos.top, 0), 125);
@@ -104,41 +82,19 @@ function GradientSelector({ color, setColor }: { color: Color, setColor: (color:
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onMouseMove={handleMouseMove}
className={classes.gradientSelector}
style={{
background: `rgb(${255 * r1}, ${255 * g1}, ${255 * b1})`,
overflow: "hidden",
position: "relative",
cursor: mouseDown ? "grabbing" : "grab",
}}
>
<div style={{ background: "linear-gradient(to right, white, transparent)" }}>
<div
style={{
pointerEvents: "none",
position: "absolute",
transform: `translate(${s * 225 - 6}px, ${125 - v * 125 - 6}px)`,
width: "12px",
height: "12px",
borderRadius: "6px",
boxShadow: "black 0 0 0 2px inset",
}}
>
<div
style={{
width: "100%",
height: "100%",
borderRadius: "6px",
boxShadow: "white 0 0 0 1px inset",
}}
/>
<div className={classes.whiteGradient}>
<div className={classes.cursor} style={{
transform: `translate(${s * 225 - 6}px, ${125 - v * 125 - 6}px)`,
}}>
<div />
</div>
<div
style={{
width: "225px",
height: "125px",
background: "linear-gradient(to top, black, transparent)",
}}
/>
<div className={classes.blackGradient} />
</div>
</div>
);
@@ -155,31 +111,23 @@ function ControlPanel({ color, setColor }: { color: Color, setColor: (color: Col
const [h, s, v] = rgbToHsv(r, g, b);
const [mode, setMode] = useState<Mode>("Hex");
const handleHueChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const handleHueChange = (e: ChangeEvent<HTMLInputElement>) => {
const newH = parseFloat(e.target.value);
const [newR, newG, newB] = hsvToRgb(newH, s, v);
setColor([newR, newG, newB, a]);
};
const handleAlphaChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const handleAlphaChange = (e: ChangeEvent<HTMLInputElement>) => {
const newA = parseFloat(e.target.value);
setColor([r, g, b, newA]);
};
return (
<div style={{ margin: "10px", display: "flex", flexDirection: "column", gap: "10px" }}>
<div style={{ display: "flex", gap: "10px", alignItems: "center" }}>
<div className={classes.controlPanel}>
<div className={classes.controlPanelTop}>
<ColorPreview color={color} setColor={setColor} />
<div style={{ flexGrow: 1, display: "flex", flexDirection: "column", gap: "10px" }}>
<div
style={{
background: "linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%)",
height: "18px",
position: "relative",
borderRadius: "9px",
}}
title="Hue"
>
<div className={classes.sliders}>
<div className={classes.hueSlider} title="Hue">
<input
type="range"
min="0"
@@ -187,27 +135,10 @@ function ControlPanel({ color, setColor }: { color: Color, setColor: (color: Col
step="1"
value={h}
onChange={handleHueChange}
style={{
margin: 0,
background: "none",
height: "16px",
position: "absolute",
top: "1px",
left: "1px",
}}
/>
</div>
<div
style={{
background: "white",
borderRadius: "9px",
overflow: "hidden",
height: "18px",
position: "relative",
}}
title="Alpha"
>
<div className="checker">
<div className={classes.alphaSlider} title="Alpha">
<div className={classes.checker}>
<div
style={{
height: "18px",
@@ -222,20 +153,12 @@ function ControlPanel({ color, setColor }: { color: Color, setColor: (color: Col
step="0.01"
value={a}
onChange={handleAlphaChange}
style={{
margin: 0,
background: "none",
height: "16px",
position: "absolute",
top: "1px",
left: "1px",
}}
/>
</div>
</div>
</div>
<div
style={{ display: "flex", gap: "10px", cursor: "ew-resize" }}
className={classes.controlPanelBottom}
onClick={() => setMode(nextMode(mode))}
>
{mode === "Rgb" ? (
@@ -296,23 +219,14 @@ function ColorPreview({ color, setColor }: { color: Color, setColor: (color: Col
/>
)}
<div
style={{
border: "1px solid rgba(255, 255, 255, 0.25)",
borderRadius: "50%",
background: "white",
position: "relative",
overflow: "hidden",
}}
className={classes.colorPreview}
title="Color Preview"
>
<div className="checker">
<div className={classes.checker}>
<div
className={classes.colorPreviewInner}
style={{
width: "35px",
height: "35px",
backgroundColor: `rgba(${255 * r}, ${255 * g}, ${255 * b}, ${a})`,
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
/>
</div>
@@ -332,7 +246,7 @@ function Hsva({ color, setColor }: { color: Color, setColor: (color: Color) => v
short="H"
kind="Degree"
value={h}
setValue={(newH: number) => {
setValue={(newH) => {
const [newR, newG, newB] = hsvToRgb(newH, s, v);
setColor([newR, newG, newB, a]);
}}
@@ -342,7 +256,7 @@ function Hsva({ color, setColor }: { color: Color, setColor: (color: Color) => v
short="S"
kind="Percent"
value={s}
setValue={(newS: number) => {
setValue={(newS) => {
const [newR, newG, newB] = hsvToRgb(h, newS, v);
setColor([newR, newG, newB, a]);
}}
@@ -352,7 +266,7 @@ function Hsva({ color, setColor }: { color: Color, setColor: (color: Color) => v
short="V"
kind="Percent"
value={v}
setValue={(newV: number) => {
setValue={(newV) => {
const [newR, newG, newB] = hsvToRgb(h, s, newV);
setColor([newR, newG, newB, a]);
}}
@@ -362,7 +276,7 @@ function Hsva({ color, setColor }: { color: Color, setColor: (color: Color) => v
short="A"
kind="Percent"
value={a}
setValue={(newA: number) => {
setValue={(newA) => {
setColor([r, g, b, newA]);
}}
/>
@@ -380,7 +294,7 @@ function Rgba({ color, setColor }: { color: Color, setColor: (color: Color) => v
short="R"
kind="Byte"
value={r}
setValue={(newR: number) => {
setValue={(newR) => {
setColor([newR, g, b, a]);
}}
/>
@@ -389,7 +303,7 @@ function Rgba({ color, setColor }: { color: Color, setColor: (color: Color) => v
short="G"
kind="Byte"
value={g}
setValue={(newG: number) => {
setValue={(newG) => {
setColor([r, newG, b, a]);
}}
/>
@@ -398,7 +312,7 @@ function Rgba({ color, setColor }: { color: Color, setColor: (color: Color) => v
short="B"
kind="Byte"
value={b}
setValue={(newB: number) => {
setValue={(newB) => {
setColor([r, g, newB, a]);
}}
/>
@@ -407,7 +321,7 @@ function Rgba({ color, setColor }: { color: Color, setColor: (color: Color) => v
short="A"
kind="Percent"
value={a}
setValue={(newA: number) => {
setValue={(newA) => {
setColor([r, g, b, newA]);
}}
/>
@@ -420,11 +334,11 @@ function Hex({ color, setColor }: { color: Color, setColor: (color: Color) => vo
return (
<>
<div className="color-input" title="Hexadecimal">
<div className={classes.colorInput} title="Hexadecimal">
<FormattedInput
value={color}
format={([r, g, b, _]: Color) => `#${Math.round(255 * r).toString(16).padStart(2, '0')}${Math.round(255 * g).toString(16).padStart(2, '0')}${Math.round(255 * b).toString(16).padStart(2, '0')}`.toUpperCase()}
parse={(value: string) => {
format={([r, g, b, _]) => `#${Math.round(255 * r).toString(16).padStart(2, '0')}${Math.round(255 * g).toString(16).padStart(2, '0')}${Math.round(255 * b).toString(16).padStart(2, '0')}`.toUpperCase()}
parse={(value) => {
const parsed = parseHex(value);
if (parsed) {
return [...parsed, a];
@@ -440,7 +354,7 @@ function Hex({ color, setColor }: { color: Color, setColor: (color: Color) => vo
short="A"
kind="Percent"
value={a}
setValue={(newA: number) => {
setValue={(newA) => {
setColor([r, g, b, newA]);
}}
/>
@@ -465,7 +379,7 @@ function FormattedInput<T>({ value, format, parse, setValue }: {
}
}, [value]);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
const formatted = e.target.value;
setFormatted(formatted);
const newValue = parse(formatted);
@@ -496,7 +410,7 @@ type Format = "Degree" | "Percent" | "Byte";
function ColorComponent({ title, short, kind, value, setValue }: { title: string, short: string, kind: Format, value: number, setValue: (value: number) => void }) {
return (
<div className="color-input" title={title}>
<div className={classes.colorInput} title={title}>
<FormattedInput
value={value}
format={(value) => kind === "Degree" ? `${value.toFixed(0)}°` : kind === "Percent" ? `${(100 * value).toFixed(0)}%` : `${(255 * value).toFixed(0)}`}
@@ -517,40 +431,40 @@ function ColorComponent({ title, short, kind, value, setValue }: { title: string
function PredefinedColors({ setColor }: { setColor: (color: Color) => void }) {
const predefinedColors = [
[
[4.0, 0.79, 0.96],
[340.0, 0.86, 0.91],
[291.0, 0.78, 0.69],
[262.0, 0.68, 0.71],
[231.0, 0.65, 0.71],
[207.0, 0.87, 0.95],
[199.0, 0.99, 0.95],
["Red", 244, 67, 54],
["Pink", 233, 30, 99],
["Purple", 156, 39, 176],
["Deep Purple", 103, 58, 183],
["Indigo", 63, 81, 181],
["Blue", 33, 150, 243],
["Light Blue", 3, 169, 244],
],
[
[187.0, 1.0, 0.84],
[174.0, 1.0, 0.58],
[122.0, 0.56, 0.68],
[88.0, 0.61, 0.77],
[66.0, 0.75, 0.86],
[54.0, 0.76, 1.0],
[45.0, 0.98, 1.0],
["Cyan", 0, 188, 212],
["Teal", 0, 150, 136],
["Green", 76, 175, 80],
["Light Green", 139, 195, 74],
["Lime", 205, 220, 57],
["Yellow", 255, 235, 59],
["Amber", 255, 193, 7],
],
[
[36.0, 1.0, 1.0],
[14.0, 0.86, 1.0],
[16.0, 0.40, 0.48],
[0.0, 0.0, 0.62],
[200.0, 0.31, 0.54],
[0.0, 0.0, 0.0],
[0.0, 0.0, 1.0],
["Orange", 255, 152, 0],
["Deep Orange", 255, 87, 34],
["Brown", 121, 85, 72],
["Grey", 158, 158, 158],
["Blue Grey", 96, 125, 139],
["Black", 0, 0, 0],
["White", 255, 255, 255],
],
] as unknown as [[[number, number, number]]];
] as unknown as [[[string, number, number, number]]];
return (
<div style={{ margin: "10px", display: "flex", flexDirection: "column", gap: "10px" }}>
<div className={classes.predefinedColors}>
{predefinedColors.map((hsv, index) => (
<div key={index} style={{ display: "flex", justifyContent: "space-between" }}>
<div key={index} className={classes.predefinedColorsRow}>
{hsv.map((color, i) => (
<PredefinedColor key={i} hsv={color} setColor={setColor} />
<PredefinedColor key={i} color={color} setColor={setColor} />
))}
</div>
))}
@@ -558,36 +472,34 @@ function PredefinedColors({ setColor }: { setColor: (color: Color) => void }) {
);
}
function PredefinedColor({ hsv, setColor }: { hsv: [number, number, number], setColor: (color: Color) => void }) {
const [h, s, v] = hsv;
const [r, g, b] = hsvToRgb(h, s, v);
function PredefinedColor({ color: [title, r, g, b], setColor }: { color: [string, number, number, number], setColor: (color: Color) => void }) {
return (
<button
className="predefined-color"
style={{ backgroundColor: `rgb(${255 * r}, ${255 * g}, ${255 * b})` }}
onClick={() => setColor([r, g, b, 1.0])}
className={classes.predefinedColor}
style={{ backgroundColor: `rgb(${r}, ${g}, ${b})` }}
onClick={() => setColor([r / 255, g / 255, b / 255, 1])}
title={title}
/>
);
}
function hsvToRgb(h: number, s: number, v: number) {
const RECIP_60 = 1 / 60;
const x_div_c = 1 - Math.abs((h * RECIP_60) % 2 - 1);
const xDivC = 1 - Math.abs((h / 60) % 2 - 1);
const [rc, rx, gc, gx, bc, bx] = h < 60 ? [1, 0, 0, 1, 0, 0] :
h < 120 ? [0, 1, 1, 0, 0, 0] :
h < 180 ? [0, 0, 1, 0, 0, 1] :
h < 240 ? [0, 0, 0, 1, 1, 0] :
h < 300 ? [0, 1, 0, 0, 1, 0] :
[1, 0, 0, 0, 0, 1];
const value_times_255 = v;
const c_times_255 = value_times_255 * s;
const x_times_255 = x_div_c * c_times_255;
const m_times_255 = value_times_255 - c_times_255;
const r = rc * c_times_255 + rx * x_times_255 + m_times_255;
const g = gc * c_times_255 + gx * x_times_255 + m_times_255;
const b = bc * c_times_255 + bx * x_times_255 + m_times_255;
const c = v * s;
const x = xDivC * c;
const m = v - c;
const r = rc * c + rx * x + m;
const g = gc * c + gx * x + m;
const b = bc * c + bx * x + m;
return [r, g, b];
}
+68
View File
@@ -0,0 +1,68 @@
import React, { createContext, useContext, ReactNode } from "react";
import { expect } from "../util/OptionUtil";
import * as classes from "../css/ContextMenu.module.css";
export interface Position {
x: number;
y: number;
}
const InfoContext = createContext<{ onClose: () => void } | undefined>(undefined);
export function ContextMenu({
position,
onClose,
children,
}: {
position: Position;
onClose: () => void;
children: ReactNode;
}) {
return (
<InfoContext.Provider value={{ onClose }}>
<nav
role="menu"
className={classes.contextMenu}
style={{
top: `${position.y}px`,
left: `${position.x}px`,
}}
>
<div className={classes.overlay} onClick={onClose} />
<div className={classes.panel}>
{children}
</div>
</nav>
</InfoContext.Provider>
);
}
export function MenuItem({
children,
onClick,
className,
}: {
children: ReactNode;
onClick: () => void;
className?: string;
}) {
const { onClose } = expect(useContext(InfoContext), "MenuItem must be used within a ContextMenu");
return (
<div
className={`${classes.entry} ${className}`}
role="menuitem"
onClick={() => {
onClick();
onClose();
}}
>
{children}
</div>
);
}
export function Separator() {
return <hr className={classes.hr} />;
}
+3 -3
View File
@@ -8,8 +8,8 @@ export interface Props {
}
export interface Options {
title: string | JSX.Element,
description: string | JSX.Element,
title: string | React.JSX.Element,
description: string | React.JSX.Element,
textInput?: boolean,
defaultText?: string,
buttons: string[],
@@ -71,7 +71,7 @@ export default class DialogContainer extends React.Component<Props, State> {
public render() {
return <dialog
ref={(element) => dialogElement = element}
ref={(element) => { dialogElement = element; }}
onKeyDown={(e) => {
if (e?.key === "ArrowLeft") {
e.preventDefault();
+6 -6
View File
@@ -1,7 +1,7 @@
import * as React from "react";
import { Option } from "../util/OptionUtil";
export function resolveEmbed(uri: string): Option<JSX.Element> {
export function resolveEmbed(uri: string): Option<React.JSX.Element> {
const youtube = tryYoutubeFromUri(uri);
if (youtube != null) {
return youtube;
@@ -13,7 +13,7 @@ export function resolveEmbed(uri: string): Option<JSX.Element> {
return <p><a href={uri} target="_blank">{uri}</a></p>;
}
function tryYoutubeFromUri(uri: string): Option<JSX.Element> {
function tryYoutubeFromUri(uri: string): Option<React.JSX.Element> {
const youtubeBase = /https?:\/\/www\.youtube\.com\/watch\?v=([A-z0-9-]+)&?/;
let result = youtubeBase.exec(uri);
if (result === null) {
@@ -26,7 +26,7 @@ function tryYoutubeFromUri(uri: string): Option<JSX.Element> {
return null;
}
function tryTwitchFromUri(uri: string): Option<JSX.Element> {
function tryTwitchFromUri(uri: string): Option<React.JSX.Element> {
const twitchBase = /https?:\/\/(www\.)?(go\.)?twitch\.tv\/.*\/v\/(\w+)&?/;
const twitchBase2 = /https?:\/\/(www\.)?(go\.)?twitch\.tv\/videos\/(\w+)&?/;
let result = twitchBase.exec(uri);
@@ -39,7 +39,7 @@ function tryTwitchFromUri(uri: string): Option<JSX.Element> {
return null;
}
function videoIframe(videoSource: string): JSX.Element {
function videoIframe(videoSource: string): React.JSX.Element {
return (
<div className="video-outer-container">
<div className="video-inner-container">
@@ -55,11 +55,11 @@ function videoIframe(videoSource: string): JSX.Element {
);
}
function resolveYoutube(videoId: string): JSX.Element {
function resolveYoutube(videoId: string): React.JSX.Element {
return videoIframe(`https://www.youtube.com/embed/${videoId}?wmode=transparent`);
}
function resolveTwitch(videoId: string): JSX.Element {
function resolveTwitch(videoId: string): React.JSX.Element {
const domain = window.location.hostname;
return videoIframe(`https://player.twitch.tv/?video=${videoId}&parent=${domain}&autoplay=false`);
}
+1 -1
View File
@@ -51,7 +51,7 @@ export default class HotkeyButton extends React.Component<Props, State> {
}
public render() {
let buttonText: Option<string> | JSX.Element = null;
let buttonText: Option<string> | React.JSX.Element = null;
if (this.props.value != null) {
buttonText = this.state.resolvedKey;
} else if (this.state.listener != null) {
+155 -140
View File
@@ -1,5 +1,4 @@
import * as React from "react";
import { ContextMenu, ContextMenuTrigger, MenuItem } from "react-contextmenu";
import React, { useState } from "react";
import * as LiveSplit from "../livesplit-core";
import { SettingsComponent } from "./Settings";
import { UrlCache } from "../util/UrlCache";
@@ -7,6 +6,7 @@ import Layout from "../layout/Layout";
import { WebRenderer } from "../livesplit-core/livesplit_core";
import { GeneralSettings } from "./MainSettings";
import { LSOCommandSink } from "./LSOCommandSink";
import { ContextMenu, MenuItem, Separator, Position } from "./ContextMenu";
import "../css/LayoutEditor.scss";
@@ -32,7 +32,7 @@ export interface State {
interface Callbacks {
onResize(width: number, height: number): void,
renderViewWithSidebar(renderedView: JSX.Element, sidebarContent: JSX.Element): JSX.Element,
renderViewWithSidebar(renderedView: React.JSX.Element, sidebarContent: React.JSX.Element): React.JSX.Element,
closeLayoutEditor(save: boolean): void,
}
@@ -91,13 +91,6 @@ export class LayoutEditor extends React.Component<Props, State> {
);
});
let contextTrigger: any = null;
const toggleMenu = (e: any) => {
if (contextTrigger) {
contextTrigger.handleContextClick(e);
}
};
const settings = this.state.showComponentSettings
? (
<SettingsComponent
@@ -136,136 +129,11 @@ export class LayoutEditor extends React.Component<Props, State> {
<div className="layout-editor-inner-container">
<div className="layout-editor-inner">
<div className="btn-group">
<ContextMenuTrigger id="add-button-context-menu" ref={(c) => contextTrigger = c}>
<button
aria-label="Add Component"
onClick={toggleMenu}
>
<i className="fa fa-plus" aria-hidden="true"></i>
</button>
</ContextMenuTrigger>
<ContextMenu id="add-button-context-menu">
<MenuItem className="tooltip" onClick={(_) => this.addComponent(LiveSplit.TitleComponent)}>
Title
<span className="tooltip-text">
Shows the name of the game and the category that is being run. Additionally, the game icon, the attempt count, and the total number of successfully finished runs can be shown.
</span>
</MenuItem>
<MenuItem className="tooltip" onClick={(_) => this.addComponent(LiveSplit.GraphComponent)}>
Graph
<span className="tooltip-text">
Visualizes how far the current run has been ahead or behind the chosen comparison throughout the whole run. All the individual deltas are shown as points on the graph.
</span>
</MenuItem>
<MenuItem className="tooltip" onClick={(_) => this.addComponent(LiveSplit.SplitsComponent)}>
Splits
<span className="tooltip-text">
The main component for visualizing all the split times. Each segment is shown in a tabular fashion showing the segment icon, segment name, the delta compared to the chosen comparison, and the split time. The list provides scrolling functionality, so not every segment needs to be shown all the time.
</span>
</MenuItem>
<MenuItem divider />
<MenuItem className="tooltip" onClick={(_) => this.addComponent(LiveSplit.DetailedTimerComponent)}>
Detailed Timer
<span className="tooltip-text">
Shows two timers, one for the total time of the current run and one showing the time of just the current segment. Other information, like segment times of up to two comparisons, the segment icon, and the segment name, can also be shown.
</span>
</MenuItem>
<MenuItem className="tooltip" onClick={(_) => this.addComponent(LiveSplit.TimerComponent)}>
Timer
<span className="tooltip-text">
Shows the total time of the current run as a digital clock. The color of the time shown is based on a how well the current run is doing compared to the chosen comparison.
</span>
</MenuItem>
<MenuItem divider />
<MenuItem className="tooltip" onClick={(_) => this.addComponent(LiveSplit.CurrentComparisonComponent)}>
Current Comparison
<span className="tooltip-text">
Shows the name of the comparison that the timer is currently comparing against.
</span>
</MenuItem>
<MenuItem className="tooltip" onClick={(_) => this.addComponent(LiveSplit.CurrentPaceComponent)}>
Current Pace
<span className="tooltip-text">
Shows a prediction for the current run's final time. The remainder of the run is predicted based on the chosen comparison for the component. For example, the "Best Segments" comparison can be chosen to show the best possible final time for the current run based on the Sum of Best Segments.
</span>
</MenuItem>
<MenuItem className="tooltip" onClick={(_) => this.addComponent(LiveSplit.DeltaComponent)}>
Delta
<span className="tooltip-text">
Shows how far ahead or behind the current run is compared to the chosen comparison.
</span>
</MenuItem>
<MenuItem className="tooltip" onClick={(_) => this.addComponent(LiveSplit.PbChanceComponent)}>
PB Chance
<span className="tooltip-text">
Shows how likely it is for the active run to beat the personal best. If there is no active run, it shows the general chance of beating the personal best. During a run, it actively changes based on how well the run is going.
</span>
</MenuItem>
<MenuItem className="tooltip" onClick={(_) => this.addComponent(LiveSplit.PossibleTimeSaveComponent)}>
Possible Time Save
<span className="tooltip-text">
Shows how much time you can save on the current segment compared to the chosen comparison, based on the best segment time of the segment. This component also allows showing the "Total Possible Time Save" for the remainder of the current run.
</span>
</MenuItem>
<MenuItem className="tooltip" onClick={(_) => this.addComponent(LiveSplit.PreviousSegmentComponent)}>
Previous Segment
<span className="tooltip-text">
Shows how much time was saved or lost during the previous segment based on the chosen comparison. Additionally, the potential time save for the previous segment can be displayed. This component switches to a "Live Segment" view that shows the active time loss whenever you are losing time on the current segment.
</span>
</MenuItem>
<MenuItem className="tooltip" onClick={(_) => this.addComponent(LiveSplit.SegmentTimeComponent)}>
Segment Time
<span className="tooltip-text">
Shows the time for the current segment for the chosen comparison. If no comparison is specified it uses the timer's current comparison.
</span>
</MenuItem>
<MenuItem className="tooltip" onClick={(_) => this.addComponent(LiveSplit.SumOfBestComponent)}>
Sum of Best Segments
<span className="tooltip-text">
Shows the fastest possible time to complete a run of the current category, based on information collected from all the previous runs. This often matches up with the sum of the best segment times of all the segments, but that may not always be the case, as skipped segments may introduce combined segments that may be faster than the actual sum of their best segment times. The name is therefore a bit misleading, but sticks around for historical reasons.
</span>
</MenuItem>
<MenuItem className="tooltip" onClick={(_) => this.addComponent(LiveSplit.TextComponent)}>
Text
<span className="tooltip-text">
Shows the text that you specify. This can either be a single centered text, or split up into a left and right text, which is suitable for a situation where you have a label and a value. There is also the option of showing a custom variable that you specify in the splits editor.
</span>
</MenuItem>
<MenuItem className="tooltip" onClick={(_) => this.addComponent(LiveSplit.TotalPlaytimeComponent)}>
Total Playtime
<span className="tooltip-text">
Shows the total amount of time that the current category has been played for.
</span>
</MenuItem>
{
this.props.allVariables.size > 0 && <MenuItem divider />
}
{
this.props.allVariables.size > 0 && Array.from(this.props.allVariables).map((name) => {
return (
<MenuItem className="tooltip" key={name} onClick={(_) => this.addVariable(name)}>
{name}
<span className="tooltip-text">
Creates a text component that shows the value of the custom variable "{name}".
</span>
</MenuItem>
);
})
}
<MenuItem divider />
<MenuItem className="tooltip" onClick={(_) => this.addComponent(LiveSplit.BlankSpaceComponent)}>
Blank Space
<span className="tooltip-text">
An empty component that doesn't show anything other than a background. It mostly serves as padding between other components.
</span>
</MenuItem>
<MenuItem className="tooltip" onClick={(_) => this.addComponent(LiveSplit.SeparatorComponent)}>
Separator
<span className="tooltip-text">
A simple component that just renders a separator between components.
</span>
</MenuItem>
</ContextMenu>
<AddComponentButton
allVariables={this.props.allVariables}
addVariable={(v) => this.addVariable(v)}
addComponent={(v) => this.addComponent(v)}
/>
<button
aria-label="Remove Component"
onClick={(_) => this.removeComponent()}
@@ -426,3 +294,150 @@ export class LayoutEditor extends React.Component<Props, State> {
this.update();
}
}
function AddComponentButton({
allVariables,
addVariable,
addComponent,
}: {
allVariables: Set<string>;
addVariable: (name: string) => void;
addComponent: (componentClass: any) => void;
}) {
const [position, setPosition] = useState<Position | null>(null);
return (
<>
<button
aria-label="Add Component"
onClick={(e) => setPosition({ x: e.clientX, y: e.clientY })}
>
<i className="fa fa-plus" aria-hidden="true"></i>
</button>
{position && (
<ContextMenu position={position} onClose={() => setPosition(null)}>
<MenuItem className="contextmenu-item tooltip" onClick={() => addComponent(LiveSplit.TitleComponent)}>
Title
<span className="tooltip-text">
Shows the name of the game and the category that is being run. Additionally, the game icon, the attempt count, and the total number of successfully finished runs can be shown.
</span>
</MenuItem>
<MenuItem className="contextmenu-item tooltip" onClick={() => addComponent(LiveSplit.GraphComponent)}>
Graph
<span className="tooltip-text">
Visualizes how far the current run has been ahead or behind the chosen comparison throughout the whole run. All the individual deltas are shown as points on the graph.
</span>
</MenuItem>
<MenuItem className="contextmenu-item tooltip" onClick={() => addComponent(LiveSplit.SplitsComponent)}>
Splits
<span className="tooltip-text">
The main component for visualizing all the split times. Each segment is shown in a tabular fashion showing the segment icon, segment name, the delta compared to the chosen comparison, and the split time. The list provides scrolling functionality, so not every segment needs to be shown all the time.
</span>
</MenuItem>
<Separator />
<MenuItem className="contextmenu-item tooltip" onClick={() => addComponent(LiveSplit.DetailedTimerComponent)}>
Detailed Timer
<span className="tooltip-text">
Shows two timers, one for the total time of the current run and one showing the time of just the current segment. Other information, like segment times of up to two comparisons, the segment icon, and the segment name, can also be shown.
</span>
</MenuItem>
<MenuItem className="contextmenu-item tooltip" onClick={() => addComponent(LiveSplit.TimerComponent)}>
Timer
<span className="tooltip-text">
Shows the total time of the current run as a digital clock. The color of the time shown is based on a how well the current run is doing compared to the chosen comparison.
</span>
</MenuItem>
<Separator />
<MenuItem className="contextmenu-item tooltip" onClick={() => addComponent(LiveSplit.CurrentComparisonComponent)}>
Current Comparison
<span className="tooltip-text">
Shows the name of the comparison that the timer is currently comparing against.
</span>
</MenuItem>
<MenuItem className="contextmenu-item tooltip" onClick={() => addComponent(LiveSplit.CurrentPaceComponent)}>
Current Pace
<span className="tooltip-text">
Shows a prediction for the current run's final time. The remainder of the run is predicted based on the chosen comparison for the component. For example, the "Best Segments" comparison can be chosen to show the best possible final time for the current run based on the Sum of Best Segments.
</span>
</MenuItem>
<MenuItem className="contextmenu-item tooltip" onClick={() => addComponent(LiveSplit.DeltaComponent)}>
Delta
<span className="tooltip-text">
Shows how far ahead or behind the current run is compared to the chosen comparison.
</span>
</MenuItem>
<MenuItem className="contextmenu-item tooltip" onClick={() => addComponent(LiveSplit.PbChanceComponent)}>
PB Chance
<span className="tooltip-text">
Shows how likely it is for the active run to beat the personal best. If there is no active run, it shows the general chance of beating the personal best. During a run, it actively changes based on how well the run is going.
</span>
</MenuItem>
<MenuItem className="contextmenu-item tooltip" onClick={() => addComponent(LiveSplit.PossibleTimeSaveComponent)}>
Possible Time Save
<span className="tooltip-text">
Shows how much time you can save on the current segment compared to the chosen comparison, based on the best segment time of the segment. This component also allows showing the "Total Possible Time Save" for the remainder of the current run.
</span>
</MenuItem>
<MenuItem className="contextmenu-item tooltip" onClick={() => addComponent(LiveSplit.PreviousSegmentComponent)}>
Previous Segment
<span className="tooltip-text">
Shows how much time was saved or lost during the previous segment based on the chosen comparison. Additionally, the potential time save for the previous segment can be displayed. This component switches to a "Live Segment" view that shows the active time loss whenever you are losing time on the current segment.
</span>
</MenuItem>
<MenuItem className="contextmenu-item tooltip" onClick={() => addComponent(LiveSplit.SegmentTimeComponent)}>
Segment Time
<span className="tooltip-text">
Shows the time for the current segment for the chosen comparison. If no comparison is specified it uses the timer's current comparison.
</span>
</MenuItem>
<MenuItem className="contextmenu-item tooltip" onClick={() => addComponent(LiveSplit.SumOfBestComponent)}>
Sum of Best Segments
<span className="tooltip-text">
Shows the fastest possible time to complete a run of the current category, based on information collected from all the previous runs. This often matches up with the sum of the best segment times of all the segments, but that may not always be the case, as skipped segments may introduce combined segments that may be faster than the actual sum of their best segment times. The name is therefore a bit misleading, but sticks around for historical reasons.
</span>
</MenuItem>
<MenuItem className="contextmenu-item tooltip" onClick={() => addComponent(LiveSplit.TextComponent)}>
Text
<span className="tooltip-text">
Shows the text that you specify. This can either be a single centered text, or split up into a left and right text, which is suitable for a situation where you have a label and a value. There is also the option of showing a custom variable that you specify in the splits editor.
</span>
</MenuItem>
<MenuItem className="contextmenu-item tooltip" onClick={() => addComponent(LiveSplit.TotalPlaytimeComponent)}>
Total Playtime
<span className="tooltip-text">
Shows the total amount of time that the current category has been played for.
</span>
</MenuItem>
{
allVariables.size > 0 && <Separator />
}
{
allVariables.size > 0 && Array.from(allVariables).map((name) => {
return (
<MenuItem className="contextmenu-item tooltip" key={name} onClick={() => addVariable(name)}>
{name}
<span className="tooltip-text">
Creates a text component that shows the value of the custom variable "{name}".
</span>
</MenuItem>
);
})
}
<Separator />
<MenuItem className="contextmenu-item tooltip" onClick={() => addComponent(LiveSplit.BlankSpaceComponent)}>
Blank Space
<span className="tooltip-text">
An empty component that doesn't show anything other than a background. It mostly serves as padding between other components.
</span>
</MenuItem>
<MenuItem className="contextmenu-item tooltip" onClick={() => addComponent(LiveSplit.SeparatorComponent)}>
Separator
<span className="tooltip-text">
A simple component that just renders a separator between components.
</span>
</MenuItem>
</ContextMenu>
)}
</>
);
}
+1 -1
View File
@@ -44,7 +44,7 @@ interface Callbacks {
openSplitsView(): void,
openMainSettings(): void,
openTimerView(): void,
renderViewWithSidebar(renderedView: JSX.Element, sidebarContent: JSX.Element): JSX.Element,
renderViewWithSidebar(renderedView: React.JSX.Element, sidebarContent: React.JSX.Element): React.JSX.Element,
saveLayout(): void,
onServerConnectionOpened(serverConnection: LiveSplitServer): void,
onServerConnectionClosed(): void,
+2 -2
View File
@@ -25,7 +25,7 @@ import { LSOCommandSink } from "./LSOCommandSink";
import DialogContainer from "./Dialog";
import { createHotkeys, HotkeyImplementation } from "../platform/Hotkeys";
import * as variables from "../css/variables.scss";
import * as variables from "../css/variables.icss.scss";
import "react-toastify/dist/ReactToastify.css";
import "../css/LiveSplit.scss";
@@ -426,7 +426,7 @@ export class LiveSplit extends React.Component<Props, State> {
</>;
}
public renderViewWithSidebar(renderedView: JSX.Element, sidebarContent: JSX.Element) {
public renderViewWithSidebar(renderedView: React.JSX.Element, sidebarContent: React.JSX.Element) {
return (
<div className={this.state.isDesktop ? "" : "is-mobile"}>
<Sidebar
+1 -1
View File
@@ -49,7 +49,7 @@ export interface State {
}
interface Callbacks {
renderViewWithSidebar(renderedView: JSX.Element, sidebarContent: JSX.Element): JSX.Element,
renderViewWithSidebar(renderedView: React.JSX.Element, sidebarContent: React.JSX.Element): React.JSX.Element,
closeMainSettings(save: boolean, newGeneralSettings: GeneralSettings): void,
onServerConnectionOpened(serverConnection: LiveSplitServer): void,
onServerConnectionClosed(): void,
+328 -253
View File
@@ -1,5 +1,4 @@
import * as React from "react";
import { ContextMenu, ContextMenuTrigger, MenuItem } from "react-contextmenu";
import * as LiveSplit from "../livesplit-core";
import { FILE_EXT_IMAGES, FILE_EXT_SPLITS, openFileAsArrayBuffer } from "../util/FileUtil";
import { TextBox } from "./TextBox";
@@ -26,6 +25,7 @@ import { UrlCache } from "../util/UrlCache";
import { GeneralSettings } from "./MainSettings";
import { showDialog } from "./Dialog";
import { corsBustingFetch } from "../platform/CORS";
import { ContextMenu, MenuItem, Position } from "./ContextMenu";
import "../css/RunEditor.scss";
@@ -48,7 +48,7 @@ export interface State {
}
interface Callbacks {
renderViewWithSidebar(renderedView: JSX.Element, sidebarContent: JSX.Element): JSX.Element,
renderViewWithSidebar(renderedView: React.JSX.Element, sidebarContent: React.JSX.Element): React.JSX.Element,
closeRunEditor(save: boolean): void,
}
@@ -131,13 +131,6 @@ export class RunEditor extends React.Component<Props, State> {
private renderView() {
const gameIcon = this.getGameIcon();
let gameIconContextTrigger: any = null;
const gameIconToggleMenu = (e: any) => {
if (gameIconContextTrigger) {
gameIconContextTrigger.handleContextClick(e);
}
};
const tab = this.getTab();
const { category, categoryNames } = this.getCurrentCategoriesInfo();
@@ -145,58 +138,14 @@ export class RunEditor extends React.Component<Props, State> {
return (
<div className="run-editor">
<div className="run-editor-info">
<div
className="game-icon-container"
onClick={(e) => {
gameIconToggleMenu(e);
}}
>
<ContextMenuTrigger
id="game-icon-context-menu"
ref={(c) => gameIconContextTrigger = c}
>
{
gameIcon !== undefined &&
<img
src={gameIcon}
className="game-icon-image"
/>
}
</ContextMenuTrigger>
</div>
<ContextMenu id="game-icon-context-menu">
<MenuItem className="tooltip" onClick={(_) => this.changeGameIcon()}>
Set Icon
<span className="tooltip-text">
Allows you to choose an image file to set as the game's icon. Certain file formats may not work everywhere.
</span>
</MenuItem>
{
this.props.generalSettings.speedrunComIntegration && <>
<MenuItem className="tooltip" onClick={(_) => this.downloadBoxArt()}>
Download Box Art
<span className="tooltip-text">
Attempts to download the box art of the game from speedrun.com, to set as the game's icon.
</span>
</MenuItem>
<MenuItem className="tooltip" onClick={(_) => this.downloadIcon()}>
Download Icon
<span className="tooltip-text">
Attempts to download the icon of the game from speedrun.com.
</span>
</MenuItem>
</>
}
{
gameIcon !== undefined &&
<MenuItem className="tooltip" onClick={(_) => this.removeGameIcon()}>
Remove Icon
<span className="tooltip-text">
Removes the icon of the game.
</span>
</MenuItem>
}
</ContextMenu>
<GameIcon
gameIcon={gameIcon}
speedrunComIntegration={this.props.generalSettings.speedrunComIntegration}
changeGameIcon={() => this.changeGameIcon()}
downloadBoxArt={() => this.downloadBoxArt()}
downloadIcon={() => this.downloadIcon()}
removeGameIcon={() => this.removeGameIcon()}
/>
<div className="run-editor-info-table">
<div className="info-table-row">
<div className="info-table-cell">
@@ -322,21 +271,7 @@ export class RunEditor extends React.Component<Props, State> {
});
}
private renderSegmentListButtons(): JSX.Element {
let otherButtonContextTrigger: any = null;
const otherButtonToggleMenu = (e: any) => {
if (otherButtonContextTrigger) {
otherButtonContextTrigger.handleContextClick(e);
}
};
let comparisonsButtonContextTrigger: any = null;
const comparisonsButtonToggleMenu = (e: any) => {
if (comparisonsButtonContextTrigger) {
comparisonsButtonContextTrigger.handleContextClick(e);
}
};
private renderSegmentListButtons(): React.JSX.Element {
return (
<div className="btn-group">
<button onClick={(_) => this.insertSegmentAbove()}>
@@ -363,73 +298,22 @@ export class RunEditor extends React.Component<Props, State> {
>
Move Down
</button>
<button onClick={(e) => comparisonsButtonToggleMenu(e)}>
<ContextMenuTrigger
id="comparisons-button-context-menu"
ref={(c) => comparisonsButtonContextTrigger = c}
>
Comparisons
</ContextMenuTrigger>
</button>
<button onClick={(e) => otherButtonToggleMenu(e)}>
<ContextMenuTrigger
id="other-button-context-menu"
ref={(c) => otherButtonContextTrigger = c}
>
Cleaning
</ContextMenuTrigger>
</button>
<ContextMenu id="comparisons-button-context-menu">
<MenuItem className="tooltip" onClick={(_) => this.addComparison()}>
Add Comparison
<span className="tooltip-text">
Adds a new custom comparison where you can store any times that you would like.
</span>
</MenuItem>
<MenuItem className="tooltip" onClick={(_) => this.importComparison()}>
Import Comparison
<span className="tooltip-text">
Imports the Personal Best of a splits file you provide as a comparison.
</span>
</MenuItem>
<MenuItem className="tooltip" onClick={(_) => this.generateGoalComparison()}>
Generate Goal Comparison
<span className="tooltip-text">
Generates a custom goal comparison based on a goal time that you can specify. The comparison's times are automatically balanced based on the segment history such that it roughly represents what the split times for the goal time would look like. Since it is populated by the segment history, the goal times are capped to a range between the sum of the best segments and the sum of the worst segments. The comparison is only populated for the selected timing method. The other timing method's comparison times are not modified by this, so you can generate it again with the other timing method to generate the comparison times for both timing methods.
</span>
</MenuItem>
<MenuItem className="tooltip" onClick={(_) => this.copyComparison()}>
Copy Comparison
<span className="tooltip-text">
Copies any existing comparison, including the Personal Best or even any other automatically provided comparison as a new custom comparison. You could for example use this to keep the Latest Run around as a comparison that exists for as long as you want it to.
</span>
</MenuItem>
</ContextMenu>
<ContextMenu id="other-button-context-menu">
<MenuItem className="tooltip" onClick={(_) => this.clearHistory()}>
Clear Only History
<span className="tooltip-text">
Splits store the entire history of all runs, including every segment time. This information is used by various components. You can clear the history with this. The personal best, the best segment times, and the comparisons will not be affected.
</span>
</MenuItem>
<MenuItem className="tooltip" onClick={(_) => this.clearTimes()}>
Clear All Times
<span className="tooltip-text">
This removes all the times from the splits, including all the history, such that the splits are completely empty, as if they were just created.
</span>
</MenuItem>
<MenuItem className="tooltip" onClick={(_) => this.cleanSumOfBest()}>
Clean Sum of Best
<span className="tooltip-text">
Allows you to interactively remove potential issues in the segment history that lead to an inaccurate Sum of Best. If you skip a split, whenever you will do the next split, the combined segment time might be faster than the sum of the individual best segments. This will point out all such occurrences and allow you to delete them individually if any of them seem wrong.
</span>
</MenuItem>
</ContextMenu>
<ComparisonsButton
addComparison={() => this.addComparison()}
importComparison={() => this.importComparison()}
generateGoalComparison={() => this.generateGoalComparison()}
copyComparison={() => this.copyComparison()}
/>
<CleaningButton
clearHistory={() => this.clearHistory()}
clearTimes={() => this.clearTimes()}
cleanSumOfBest={() => this.cleanSumOfBest()}
/>
</div>
);
}
private renderAssociateRunButton(): JSX.Element {
private renderAssociateRunButton(): React.JSX.Element {
if (this.props.generalSettings.speedrunComIntegration) {
return (
<button onClick={(_) => this.interactiveAssociateRunOrOpenPage()}>
@@ -441,7 +325,7 @@ export class RunEditor extends React.Component<Props, State> {
}
}
private renderRulesButtons(): JSX.Element {
private renderRulesButtons(): React.JSX.Element {
return (
<div className="btn-group">
{this.renderAssociateRunButton()}
@@ -449,7 +333,7 @@ export class RunEditor extends React.Component<Props, State> {
);
}
private renderVariablesButtons(): JSX.Element {
private renderVariablesButtons(): React.JSX.Element {
return (
<div className="btn-group">
{this.renderAssociateRunButton()}
@@ -473,7 +357,7 @@ export class RunEditor extends React.Component<Props, State> {
}
}
private renderSideButtons(tab: Tab, category: Option<Category>): JSX.Element {
private renderSideButtons(tab: Tab, category: Option<Category>): React.JSX.Element {
switch (tab) {
case Tab.RealTime:
case Tab.GameTime:
@@ -487,7 +371,7 @@ export class RunEditor extends React.Component<Props, State> {
}
}
private renderTab(tab: Tab, category: Option<Category>): JSX.Element {
private renderTab(tab: Tab, category: Option<Category>): React.JSX.Element {
switch (tab) {
case Tab.RealTime:
case Tab.GameTime:
@@ -501,7 +385,7 @@ export class RunEditor extends React.Component<Props, State> {
}
}
private renderLeaderboardButtons(category: Option<Category>): JSX.Element {
private renderLeaderboardButtons(category: Option<Category>): React.JSX.Element {
const gameInfo = getGameInfo(this.state.editor.game);
if (gameInfo === undefined) {
return this.renderRulesButtons();
@@ -745,7 +629,7 @@ export class RunEditor extends React.Component<Props, State> {
this.update();
}
private renderVariablesTab(category: Option<Category>): JSX.Element {
private renderVariablesTab(category: Option<Category>): React.JSX.Element {
const metadata = this.state.editor.metadata;
const gameInfo = getGameInfo(this.state.editor.game);
const fields: ExtendedSettingsDescriptionFieldJson[] = [];
@@ -927,7 +811,7 @@ export class RunEditor extends React.Component<Props, State> {
);
}
private renderLeaderboard(category: Option<Category>): JSX.Element {
private renderLeaderboard(category: Option<Category>): React.JSX.Element {
const leaderboard = this.getCurrentLeaderboard(category);
if (leaderboard === undefined) {
return <div />;
@@ -1177,7 +1061,7 @@ export class RunEditor extends React.Component<Props, State> {
);
}
private renderRulesTab(category: Option<Category>): JSX.Element {
private renderRulesTab(category: Option<Category>): React.JSX.Element {
let rules = null;
if (category != null && category.rules != null) {
rules = <Markdown markdown={category.rules} />;
@@ -1223,7 +1107,7 @@ export class RunEditor extends React.Component<Props, State> {
);
}
private renderSegmentsTable(): JSX.Element {
private renderSegmentsTable(): React.JSX.Element {
return (
<table className="table run-editor-tab run-editor-table">
<thead className="table-header">
@@ -1235,30 +1119,13 @@ export class RunEditor extends React.Component<Props, State> {
<th>Best Segment</th>
{
this.state.editor.comparison_names.map((comparison, comparisonIndex) => {
let contextTrigger: any = null;
const toggleMenu = (e: any) => {
if (contextTrigger) {
contextTrigger.handleContextClick(e);
}
};
const id = `comparison-${comparisonIndex}-context-menu`;
return (
<th
style={{
cursor: "pointer",
}}
onClick={(e) => toggleMenu(e)}
draggable
<CustomComparison
comparison={comparison}
onDragStart={(e) => {
e.dataTransfer.setData("text/plain", "");
this.dragIndex = comparisonIndex;
}}
onDragOver={(e) => {
if (e.preventDefault) {
e.preventDefault();
}
e.dataTransfer.dropEffect = "move";
}}
onDragEnd={(_) => this.update()}
onDrop={(e) => {
if (e.stopPropagation) {
@@ -1267,40 +1134,10 @@ export class RunEditor extends React.Component<Props, State> {
this.props.editor.moveComparison(this.dragIndex, comparisonIndex);
return false;
}}
>
<ContextMenuTrigger
id={id}
ref={(c) => contextTrigger = c}
>
{comparison}
</ContextMenuTrigger>
<ContextMenu id={id}>
<MenuItem className="tooltip" onClick={(_) =>
this.renameComparison(comparison)
}>
Rename
<span className="tooltip-text">
Choose a new name for the custom comparison. There are reserved names that can't be used. You also can't have duplicate names.
</span>
</MenuItem>
<MenuItem className="tooltip" onClick={(_) =>
this.copyComparison(comparison)
}>
Copy
<span className="tooltip-text">
Creates a copy of the custom comparison.
</span>
</MenuItem>
<MenuItem className="tooltip" onClick={(_) =>
this.removeComparison(comparison)
}>
Remove
<span className="tooltip-text">
Removes the custom comparison.
</span>
</MenuItem>
</ContextMenu>
</th>
renameComparison={() => this.renameComparison(comparison)}
copyComparison={() => this.copyComparison(comparison)}
removeComparison={() => this.removeComparison(comparison)}
/>
);
})
}
@@ -1310,15 +1147,7 @@ export class RunEditor extends React.Component<Props, State> {
{
this.state.editor.segments.map((s, segmentIndex) => {
const segmentIcon = this.getSegmentIconUrl(segmentIndex);
const segmentIconContextMenuId = `segment-icon-${segmentIndex}-context-menu`;
let segmentIconContextTrigger: any = null;
const segmentIconToggleMenu = (e: any) => {
if (segmentIconContextTrigger) {
segmentIconContextTrigger.handleContextClick(e);
this.props.editor.selectOnly(segmentIndex);
this.update();
}
};
return (
<tr
key={segmentIndex.toString()}
@@ -1329,46 +1158,14 @@ export class RunEditor extends React.Component<Props, State> {
}
onClick={(e) => this.changeSegmentSelection(e, segmentIndex)}
>
<td
className="segment-icon-container"
onClick={(e) => {
if (segmentIcon !== undefined) {
segmentIconToggleMenu(e);
} else {
this.changeSegmentIcon(segmentIndex);
}
}}
>
<ContextMenuTrigger
id={segmentIconContextMenuId}
ref={(c) => segmentIconContextTrigger = c}
>
{
segmentIcon !== undefined &&
<img
className="segment-icon"
src={segmentIcon}
/>
}
</ContextMenuTrigger>
<ContextMenu id={segmentIconContextMenuId}>
<MenuItem className="tooltip" onClick={(_) => this.changeSegmentIcon(segmentIndex)}>
Set Icon
<span className="tooltip-text">
Allows you to choose an image file to set as the segment's icon. Certain file formats may not work everywhere.
</span>
</MenuItem>
<MenuItem className="tooltip" onClick={(_) => this.removeSegmentIcon(segmentIndex)}>
Remove Icon
<span className="tooltip-text">
Removes the segment's icon.
</span>
</MenuItem>
</ContextMenu>
</td>
<SegmentIcon
segmentIcon={segmentIcon}
changeSegmentIcon={() => this.changeSegmentIcon(segmentIndex)}
removeSegmentIcon={() => this.removeSegmentIcon(segmentIndex)}
/>
<td>
<input
className="name"
className="name text-box"
type="text"
value={s.name}
onFocus={(_) => this.focusSegment(segmentIndex)}
@@ -1377,7 +1174,7 @@ export class RunEditor extends React.Component<Props, State> {
</td>
<td>
<input
className="number"
className="number text-box"
type="text"
value={segmentIndex === this.state.rowState.index &&
this.state.rowState.splitTimeChanged
@@ -1394,8 +1191,8 @@ export class RunEditor extends React.Component<Props, State> {
((segmentIndex !== this.state.rowState.index ||
!this.state.rowState.segmentTimeChanged) &&
s.segment_time === s.best_segment_time)
? "number best-segment-time"
: "number"
? "number text-box best-segment-time"
: "number text-box"
}
type="text"
value={segmentIndex === this.state.rowState.index &&
@@ -1409,7 +1206,7 @@ export class RunEditor extends React.Component<Props, State> {
</td>
<td>
<input
className="number"
className="number text-box"
type="text"
value={segmentIndex === this.state.rowState.index &&
this.state.rowState.bestSegmentTimeChanged
@@ -1429,7 +1226,7 @@ export class RunEditor extends React.Component<Props, State> {
.map((comparisonTime, comparisonIndex) => (
<td>
<input
className="number"
className="number text-box"
type="text"
value={segmentIndex === this.state.rowState.index &&
this.state.rowState.comparisonTimesChanged[comparisonIndex]
@@ -2286,3 +2083,281 @@ function associateRun<T>(
// Needs to be set last in order for it not to dissociate again
editor.setRunId(apiRun.id);
}
function GameIcon({
gameIcon,
speedrunComIntegration,
changeGameIcon,
downloadBoxArt,
downloadIcon,
removeGameIcon,
}: {
gameIcon: string | undefined;
speedrunComIntegration: boolean;
changeGameIcon: () => void;
downloadBoxArt: () => void;
downloadIcon: () => void;
removeGameIcon: () => void;
}) {
const [position, setPosition] = React.useState<Position | null>(null);
return (
<>
<div
className="game-icon-container"
onClick={(e) => setPosition({ x: e.clientX, y: e.clientY })}
>
{
gameIcon !== undefined &&
<img
src={gameIcon}
className="game-icon-image"
/>
}
</div>
{position && (
<ContextMenu position={position} onClose={() => setPosition(null)}>
<MenuItem className="contextmenu-item tooltip" onClick={changeGameIcon}>
Set Icon
<span className="tooltip-text">
Allows you to choose an image file to set as the game's icon. Certain file formats may not work everywhere.
</span>
</MenuItem>
{
speedrunComIntegration && <>
<MenuItem className="contextmenu-item tooltip" onClick={downloadBoxArt}>
Download Box Art
<span className="tooltip-text">
Attempts to download the box art of the game from speedrun.com, to set as the game's icon.
</span>
</MenuItem>
<MenuItem className="contextmenu-item tooltip" onClick={downloadIcon}>
Download Icon
<span className="tooltip-text">
Attempts to download the icon of the game from speedrun.com.
</span>
</MenuItem>
</>
}
{
gameIcon !== undefined &&
<MenuItem className="contextmenu-item tooltip" onClick={removeGameIcon}>
Remove Icon
<span className="tooltip-text">
Removes the icon of the game.
</span>
</MenuItem>
}
</ContextMenu>
)}
</>
);
}
function CleaningButton({
clearHistory,
clearTimes,
cleanSumOfBest,
}: {
clearHistory: () => void;
clearTimes: () => void;
cleanSumOfBest: () => void;
}) {
const [position, setPosition] = React.useState<Position | null>(null);
return (
<>
<button onClick={(e) => setPosition({ x: e.clientX, y: e.clientY })}>
Cleaning
</button>
{position && (
<ContextMenu position={position} onClose={() => setPosition(null)}>
<MenuItem className="contextmenu-item tooltip" onClick={clearHistory}>
Clear Only History
<span className="tooltip-text">
Splits store the entire history of all runs, including every segment time. This information is used by various components. You can clear the history with this. The personal best, the best segment times, and the comparisons will not be affected.
</span>
</MenuItem>
<MenuItem className="contextmenu-item tooltip" onClick={clearTimes}>
Clear All Times
<span className="tooltip-text">
This removes all the times from the splits, including all the history, such that the splits are completely empty, as if they were just created.
</span>
</MenuItem>
<MenuItem className="contextmenu-item tooltip" onClick={cleanSumOfBest}>
Clean Sum of Best
<span className="tooltip-text">
Allows you to interactively remove potential issues in the segment history that lead to an inaccurate Sum of Best. If you skip a split, whenever you will do the next split, the combined segment time might be faster than the sum of the individual best segments. This will point out all such occurrences and allow you to delete them individually if any of them seem wrong.
</span>
</MenuItem>
</ContextMenu>
)}
</>
);
}
function ComparisonsButton({
addComparison,
importComparison,
generateGoalComparison,
copyComparison,
}: {
addComparison: () => void;
importComparison: () => void;
generateGoalComparison: () => void;
copyComparison: () => void;
}) {
const [position, setPosition] = React.useState<Position | null>(null);
return (
<>
<button onClick={(e) => setPosition({ x: e.clientX, y: e.clientY })}>
Comparisons
</button>
{position && (
<ContextMenu position={position} onClose={() => setPosition(null)}>
<MenuItem className="contextmenu-item tooltip" onClick={addComparison}>
Add Comparison
<span className="tooltip-text">
Adds a new custom comparison where you can store any times that you would like.
</span>
</MenuItem>
<MenuItem className="contextmenu-item tooltip" onClick={importComparison}>
Import Comparison
<span className="tooltip-text">
Imports the Personal Best of a splits file you provide as a comparison.
</span>
</MenuItem>
<MenuItem className="contextmenu-item tooltip" onClick={generateGoalComparison}>
Generate Goal Comparison
<span className="tooltip-text">
Generates a custom goal comparison based on a goal time that you can specify. The comparison's times are automatically balanced based on the segment history such that it roughly represents what the split times for the goal time would look like. Since it is populated by the segment history, the goal times are capped to a range between the sum of the best segments and the sum of the worst segments. The comparison is only populated for the selected timing method. The other timing method's comparison times are not modified by this, so you can generate it again with the other timing method to generate the comparison times for both timing methods.
</span>
</MenuItem>
<MenuItem className="contextmenu-item tooltip" onClick={copyComparison}>
Copy Comparison
<span className="tooltip-text">
Copies any existing comparison, including the Personal Best or even any other automatically provided comparison as a new custom comparison. You could for example use this to keep the Latest Run around as a comparison that exists for as long as you want it to.
</span>
</MenuItem>
</ContextMenu>
)}
</>
);
}
function SegmentIcon({
segmentIcon,
changeSegmentIcon,
removeSegmentIcon,
}: {
segmentIcon: string | undefined;
changeSegmentIcon: () => void;
removeSegmentIcon: () => void;
}) {
const [position, setPosition] = React.useState<Position | null>(null);
return (
<td
className="segment-icon-container"
onClick={(e) => {
if (position !== null) {
return;
}
if (segmentIcon !== undefined) {
setPosition({ x: e.clientX, y: e.clientY });
} else {
changeSegmentIcon();
}
}}
>
{
segmentIcon !== undefined &&
<img src={segmentIcon} />
}
{position && (
<ContextMenu position={position} onClose={() => setPosition(null)}>
<MenuItem className="contextmenu-item tooltip" onClick={changeSegmentIcon}>
Set Icon
<span className="tooltip-text">
Allows you to choose an image file to set as the segment's icon. Certain file formats may not work everywhere.
</span>
</MenuItem>
<MenuItem className="contextmenu-item tooltip" onClick={removeSegmentIcon}>
Remove Icon
<span className="tooltip-text">
Removes the segment's icon.
</span>
</MenuItem>
</ContextMenu>
)}
</td>
);
}
function CustomComparison({
comparison,
onDragStart,
onDragEnd,
onDrop,
renameComparison,
copyComparison,
removeComparison,
}: {
comparison: string;
onDragStart: (e: React.DragEvent) => void;
onDragEnd: (e: React.DragEvent) => void;
onDrop: (e: React.DragEvent) => void;
renameComparison: () => void;
copyComparison: () => void;
removeComparison: () => void;
}) {
const [position, setPosition] = React.useState<Position | null>(null);
return (
<th
style={{
cursor: "pointer",
}}
onClick={(e) => {
if (position === null) {
setPosition({ x: e.clientX, y: e.clientY });
}
}}
draggable
onDragStart={onDragStart}
onDragOver={(e) => {
if (e.preventDefault) {
e.preventDefault();
}
e.dataTransfer.dropEffect = "move";
}}
onDragEnd={onDragEnd}
onDrop={onDrop}
>
{comparison}
{position && (
<ContextMenu position={position} onClose={() => setPosition(null)}>
<MenuItem className="contextmenu-item tooltip" onClick={renameComparison}>
Rename
<span className="tooltip-text">
Choose a new name for the custom comparison. There are reserved names that can't be used. You also can't have duplicate names.
</span>
</MenuItem>
<MenuItem className="contextmenu-item tooltip" onClick={copyComparison}>
Copy
<span className="tooltip-text">
Creates a copy of the custom comparison.
</span>
</MenuItem>
<MenuItem className="contextmenu-item tooltip" onClick={removeComparison}>
Remove
<span className="tooltip-text">
Removes the custom comparison.
</span>
</MenuItem>
</ContextMenu>
)}
</th>
);
}
+13 -9
View File
@@ -29,8 +29,8 @@ export interface ExtendedSettingsDescriptionJson {
}
export interface ExtendedSettingsDescriptionFieldJson {
text: string | JSX.Element,
tooltip: string | JSX.Element,
text: string | React.JSX.Element,
tooltip: string | React.JSX.Element,
value: ExtendedSettingsDescriptionValueJson,
}
@@ -193,7 +193,7 @@ export class JsonSettingValueFactory implements SettingValueFactory<ExtendedSett
export class SettingsComponent<T> extends React.Component<Props<T>> {
public render() {
const settingsRows: JSX.Element[] = [];
const settingsRows: React.JSX.Element[] = [];
const { factory } = this.props;
this.props.state.fields.forEach((field, valueIndex) => {
@@ -218,7 +218,7 @@ export class SettingsComponent<T> extends React.Component<Props<T>> {
<div className="settings-value-box">
<input
type="number"
className="number"
className="number text-box"
value={value.UInt}
min="0"
onChange={(e) => {
@@ -235,7 +235,7 @@ export class SettingsComponent<T> extends React.Component<Props<T>> {
<div className="settings-value-box">
<input
type="number"
className="number"
className="number text-box"
value={value.Int}
onChange={(e) => {
this.props.setValue(
@@ -283,6 +283,7 @@ export class SettingsComponent<T> extends React.Component<Props<T>> {
component = (
<div className="settings-value-box">
<input
className="text-box"
value={value.String}
onChange={(e) => {
this.props.setValue(
@@ -344,6 +345,7 @@ export class SettingsComponent<T> extends React.Component<Props<T>> {
if (value.OptionalString !== null) {
children.push(
<input
className="text-box"
value={value.OptionalString}
onChange={(e) => {
this.props.setValue(
@@ -365,6 +367,7 @@ export class SettingsComponent<T> extends React.Component<Props<T>> {
component = (
<div className="settings-value-box removable-string">
<input
className="text-box"
value={value.RemovableString || ""}
onChange={(e) => {
if (factory.fromRemovableString) {
@@ -551,7 +554,7 @@ export class SettingsComponent<T> extends React.Component<Props<T>> {
}
};
const children: JSX.Element[] = [
const children: React.JSX.Element[] = [
<select
value={type}
onChange={(e) => {
@@ -676,7 +679,7 @@ export class SettingsComponent<T> extends React.Component<Props<T>> {
}
};
const children: JSX.Element[] = [
const children: React.JSX.Element[] = [
<select
value={type}
onChange={(e) => {
@@ -1020,6 +1023,7 @@ export class SettingsComponent<T> extends React.Component<Props<T>> {
} else {
children.push(
<input
className="text-box"
value={family}
onChange={(e) => {
this.props.setValue(
@@ -1154,7 +1158,7 @@ export class SettingsComponent<T> extends React.Component<Props<T>> {
}
};
const children: JSX.Element[] = [
const children: React.JSX.Element[] = [
<select
value={type}
onChange={(e) => {
@@ -1231,7 +1235,7 @@ export class SettingsComponent<T> extends React.Component<Props<T>> {
let blur = 0;
const gradient = value.LayoutBackground;
const children: JSX.Element[] = [];
const children: React.JSX.Element[] = [];
const colorsToValue = (
type: string,
+47 -35
View File
@@ -1,4 +1,4 @@
import * as React from "react";
import React, { useState } from "react";
import {
getSplitsInfos, SplitsInfo, deleteSplits, copySplits, loadSplits,
storeRunWithoutDisposing, storeSplitsKey,
@@ -9,10 +9,10 @@ import { toast } from "react-toastify";
import { openFileAsArrayBuffer, exportFile, convertFileToArrayBuffer, FILE_EXT_SPLITS } from "../util/FileUtil";
import { Option, bug, maybeDisposeAndThen } from "../util/OptionUtil";
import DragUpload from "./DragUpload";
import { ContextMenuTrigger, ContextMenu, MenuItem } from "react-contextmenu";
import { GeneralSettings } from "./MainSettings";
import { LSOCommandSink } from "./LSOCommandSink";
import { showDialog } from "./Dialog";
import { ContextMenu, MenuItem, Position } from "./ContextMenu";
import "../css/SplitsSelection.scss";
@@ -37,7 +37,7 @@ interface Callbacks {
openRunEditor(editingInfo: EditingInfo): void,
setSplitsKey(newKey?: number): void,
openTimerView(): void,
renderViewWithSidebar(renderedView: JSX.Element, sidebarContent: JSX.Element): JSX.Element,
renderViewWithSidebar(renderedView: React.JSX.Element, sidebarContent: React.JSX.Element): React.JSX.Element,
saveSplits(): Promise<void>,
}
@@ -112,14 +112,6 @@ export class SplitsSelection extends React.Component<Props, State> {
private renderSavedSplitsRow(key: number, info: SplitsInfo) {
const isOpened = key === this.props.openedSplitsKey;
const segmentIconContextMenuId = `splits-${key}-context-menu`;
let exportContextTrigger: any = null;
const exportButtonToggleMenu = (e: any) => {
if (exportContextTrigger) {
exportContextTrigger.handleContextClick(e);
}
};
return (
<div className={isOpened ? "splits-row selected" : "splits-row"} key={key}>
{this.splitsTitle(info)}
@@ -134,30 +126,11 @@ export class SplitsSelection extends React.Component<Props, State> {
<button aria-label="Edit Splits" onClick={() => this.editSplits(key)}>
<i className="fa fa-edit" aria-hidden="true" />
</button>
<button aria-label="Export Splits" onClick={(e) => exportButtonToggleMenu(e)}>
<ContextMenuTrigger
id={segmentIconContextMenuId}
ref={(c) => exportContextTrigger = c}
>
<i className="fa fa-upload" aria-hidden="true" />
</ContextMenuTrigger>
</button>
<ContextMenu id={segmentIconContextMenuId}>
<MenuItem className="tooltip" onClick={(_) => this.exportSplits(key, info)}>
Export to File
<span className="tooltip-text">
Export the splits to a file on your computer.
</span>
</MenuItem>
{
this.props.generalSettings.splitsIoIntegration && <MenuItem className="tooltip" onClick={(_) => this.uploadSplitsToSplitsIO(key)}>
Upload to Splits.io
<span className="tooltip-text">
Upload the splits to splits.io.
</span>
</MenuItem>
}
</ContextMenu>
<ExportButton
splitsIoIntegration={this.props.generalSettings.splitsIoIntegration}
exportSplits={() => this.exportSplits(key, info)}
uploadSplitsToSplitsIO={() => this.uploadSplitsToSplitsIO(key)}
/>
</>
}
<button aria-label="Copy Splits" onClick={() => this.copySplits(key)}>
@@ -428,3 +401,42 @@ export class SplitsSelection extends React.Component<Props, State> {
}
}
}
function ExportButton({
splitsIoIntegration,
exportSplits,
uploadSplitsToSplitsIO,
}: {
splitsIoIntegration: boolean;
exportSplits: () => void;
uploadSplitsToSplitsIO: () => void;
}) {
const [position, setPosition] = useState<Position | null>(null);
return (
<>
<button aria-label="Export Splits" onClick={(e) => setPosition({ x: e.clientX, y: e.clientY })}>
<i className="fa fa-upload" aria-hidden="true" />
</button>
{position && (
<ContextMenu position={position} onClose={() => setPosition(null)}>
<MenuItem className="contextmenu-item tooltip" onClick={exportSplits}>
Export to File
<span className="tooltip-text">
Export the splits to a file on your computer.
</span>
</MenuItem>
{splitsIoIntegration && (
<MenuItem className="contextmenu-item tooltip" onClick={uploadSplitsToSplitsIO}>
Upload to Splits.io
<span className="tooltip-text">
Upload the splits to splits.io.
</span>
</MenuItem>
)}
</ContextMenu>
)}
</>
);
}
+3 -3
View File
@@ -1,14 +1,14 @@
import React from "react";
import "../css/Switch.scss";
import * as classes from "../css/Switch.module.scss";
export default function Switch({ checked, setIsChecked }: {
checked: boolean,
setIsChecked: (checked: boolean) => void,
}) {
return (
<label style={{ cursor: "pointer", display: "flex", justifyContent: "center", alignItems: "center" }}>
<div className="switch">
<label className={classes.label}>
<div className={classes.switch}>
<input type="checkbox" checked={checked} onChange={(e) => setIsChecked(e.target.checked)} />
<span />
</div>
+1 -1
View File
@@ -33,7 +33,7 @@ export class TextBox extends React.Component<Props> {
<div className={className}>
<input
list={name}
type="text"
type="text text-box"
required
className={this.props.className}
value={this.props.value}
+1 -1
View File
@@ -48,7 +48,7 @@ interface Callbacks {
openLayoutView(): void,
openSplitsView(): void,
openMainSettings(): void,
renderViewWithSidebar(renderedView: JSX.Element, sidebarContent: JSX.Element): JSX.Element,
renderViewWithSidebar(renderedView: React.JSX.Element, sidebarContent: React.JSX.Element): React.JSX.Element,
onServerConnectionClosed(): void,
onServerConnectionOpened(serverConnection: LiveSplitServer): void,
}
+2 -2
View File
@@ -33,7 +33,7 @@ function replaceTwitchEmotes(text: string): string {
});
}
export function replaceFlag(countryCode: string): JSX.Element {
export function replaceFlag(countryCode: string): React.JSX.Element {
const url = `https://www.speedrun.com/images/flags/${countryCode}.png`;
return <img className="flag" src={url} alt={countryCode} />;
@@ -43,7 +43,7 @@ export const Markdown = React.memo(renderMarkdown);
export function renderMarkdown({ markdown, unsafe }: { markdown: string,
unsafe?: boolean,
}): JSX.Element {
}): React.JSX.Element {
const markdownWithEmotes = replaceTwitchEmotes(markdown);
const html = (unsafe ? UNSAFE : SAFE).render(markdownWithEmotes);
+14 -1
View File
@@ -9,6 +9,7 @@ import webpack from "webpack";
import { execSync } from "child_process";
import path from "path";
import { fileURLToPath } from 'url';
import { defineReactCompilerLoaderOption, reactCompilerLoader } from "react-compiler-webpack";
function parseChangelog() {
return execSync("git log --grep \"^Changelog: \" -10")
@@ -182,6 +183,16 @@ export default async (env, argv) => {
{
test: /\.tsx?$/,
use: [
...(isProduction ? [{
loader: reactCompilerLoader,
options: defineReactCompilerLoaderOption({
babelTransFormOpt: {
plugins: [
"@babel/plugin-proposal-explicit-resource-management",
],
},
}),
}] : []),
{
loader: "ts-loader",
options: {
@@ -202,7 +213,9 @@ export default async (env, argv) => {
loader: "css-loader",
options: {
importLoaders: 1,
modules: "icss",
modules: {
auto: true,
},
},
},
"sass-loader",