mirror of
https://github.com/ApfelTeeSaft/LiveSplitOne.git
synced 2026-08-27 03:33:40 +00:00
Introduce a Tauri based desktop version (#940)
This introduces a Tauri based desktop version of LiveSplit One. For now very few changes have been done. It's mostly global hotkeys that work now. It's unclear if Tauri is the long term solution for LiveSplit One, but it gets us global hotkeys and auto splitting with very little effort, so we'll use it until something better comes along.
This commit is contained in:
+164
-90
@@ -4,115 +4,189 @@ on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
- "master"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
label:
|
||||
- Web
|
||||
- Tauri on Windows
|
||||
- Tauri on Linux
|
||||
- Tauri on macOS ARM
|
||||
|
||||
include:
|
||||
- label: Web
|
||||
platform: web
|
||||
dist_path: dist
|
||||
os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-musl
|
||||
binaryen: x86_64-linux
|
||||
cargo_bin: /home/runner/.cargo/bin
|
||||
|
||||
- label: Tauri on Windows
|
||||
platform: tauri
|
||||
dist_path: src-tauri/target/dist
|
||||
os: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
binaryen: x86_64-windows
|
||||
cargo_bin: C:/Users/runneradmin/.cargo/bin
|
||||
|
||||
- label: Tauri on Linux
|
||||
platform: tauri
|
||||
dist_path: src-tauri/target/dist
|
||||
os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-musl
|
||||
binaryen: x86_64-linux
|
||||
cargo_bin: /home/runner/.cargo/bin
|
||||
|
||||
- label: Tauri on macOS ARM
|
||||
platform: tauri
|
||||
dist_path: src-tauri/target/dist
|
||||
os: macos-latest
|
||||
target: aarch64-apple-darwin
|
||||
binaryen: arm64-macos
|
||||
cargo_bin: /Users/runner/.cargo/bin
|
||||
|
||||
steps:
|
||||
- name: Checkout commit
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
# This forces the entire history to be cloned, which is necessary for
|
||||
# the changelog generation to work correctly.
|
||||
fetch-depth: 0
|
||||
- name: Checkout commit
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
# This forces the entire history to be cloned, which is necessary for
|
||||
# the changelog generation to work correctly.
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
|
||||
- name: Install Rust
|
||||
uses: hecrj/setup-rust-action@v2
|
||||
with:
|
||||
rust-version: nightly
|
||||
components: rust-src
|
||||
targets: wasm32-unknown-unknown
|
||||
- name: Install Rust
|
||||
uses: hecrj/setup-rust-action@v2
|
||||
with:
|
||||
rust-version: nightly
|
||||
components: rust-src
|
||||
targets: wasm32-unknown-unknown
|
||||
|
||||
- name: Download binaryen
|
||||
if: github.repository == 'LiveSplit/LiveSplitOne' && github.ref == 'refs/heads/master'
|
||||
uses: robinraju/[email protected]
|
||||
with:
|
||||
repository: "WebAssembly/binaryen"
|
||||
latest: true
|
||||
fileName: "binaryen-*-x86_64-linux.tar.gz"
|
||||
out-file-path: "/home/runner/.cargo/bin"
|
||||
- name: Download binaryen
|
||||
if: github.repository == 'LiveSplit/LiveSplitOne' && github.ref == 'refs/heads/master'
|
||||
uses: robinraju/[email protected]
|
||||
with:
|
||||
repository: "WebAssembly/binaryen"
|
||||
latest: true
|
||||
fileName: "binaryen-*-${{ matrix.binaryen }}.tar.gz"
|
||||
out-file-path: ${{ matrix.cargo_bin }}
|
||||
|
||||
- name: Install binaryen
|
||||
if: github.repository == 'LiveSplit/LiveSplitOne' && github.ref == 'refs/heads/master'
|
||||
run: |
|
||||
cd ~/.cargo/bin
|
||||
tar -xzf binaryen-*-x86_64-linux.tar.gz
|
||||
mv binaryen*/bin/wasm* .
|
||||
- name: Install binaryen
|
||||
if: github.repository == 'LiveSplit/LiveSplitOne' && github.ref == 'refs/heads/master'
|
||||
run: |
|
||||
cd ~/.cargo/bin
|
||||
tar -xzf binaryen-*-${{ matrix.binaryen }}.tar.gz
|
||||
mv binaryen*/bin/wasm* .
|
||||
|
||||
- name: Choose wasm-bindgen-cli version
|
||||
run: echo "version=$(cd livesplit-core && cargo tree -i wasm-bindgen --features wasm-web --target wasm32-unknown-unknown --depth 0 | sed 's/.* v//g')" >> $GITHUB_OUTPUT
|
||||
id: wasm-bindgen
|
||||
- name: Choose wasm-bindgen-cli version
|
||||
shell: bash
|
||||
run: echo "version=$(cd livesplit-core && cargo tree -i wasm-bindgen --features wasm-web --target wasm32-unknown-unknown --depth 0 | sed 's/.* v//g')" >> $GITHUB_OUTPUT
|
||||
id: wasm-bindgen
|
||||
|
||||
- name: Download wasm-bindgen-cli
|
||||
uses: robinraju/[email protected]
|
||||
with:
|
||||
repository: "rustwasm/wasm-bindgen"
|
||||
tag: ${{ steps.wasm-bindgen.outputs.version }}
|
||||
fileName: "wasm-bindgen-${{ steps.wasm-bindgen.outputs.version }}-x86_64-unknown-linux-musl.tar.gz"
|
||||
out-file-path: "/home/runner/.cargo/bin"
|
||||
- name: Download wasm-bindgen-cli
|
||||
uses: robinraju/[email protected]
|
||||
with:
|
||||
repository: "rustwasm/wasm-bindgen"
|
||||
tag: ${{ steps.wasm-bindgen.outputs.version }}
|
||||
fileName: "wasm-bindgen-${{ steps.wasm-bindgen.outputs.version }}-${{ matrix.target }}.tar.gz"
|
||||
out-file-path: ${{ matrix.cargo_bin }}
|
||||
|
||||
- name: Install wasm-bindgen-cli
|
||||
run: |
|
||||
cd ~/.cargo/bin
|
||||
tar -xzf wasm-bindgen-${{ steps.wasm-bindgen.outputs.version }}-x86_64-unknown-linux-musl.tar.gz
|
||||
mv wasm-bindgen-${{ steps.wasm-bindgen.outputs.version }}-x86_64-unknown-linux-musl/wasm* .
|
||||
- name: Install wasm-bindgen-cli
|
||||
shell: bash
|
||||
run: |
|
||||
cd ${{ matrix.cargo_bin }}
|
||||
tar -xzf wasm-bindgen-${{ steps.wasm-bindgen.outputs.version }}-${{ matrix.target }}.tar.gz
|
||||
mv wasm-bindgen-${{ steps.wasm-bindgen.outputs.version }}-${{ matrix.target }}/wasm* .
|
||||
|
||||
- name: Install npm packages
|
||||
run: npm ci -f
|
||||
env:
|
||||
DETECT_CHROMEDRIVER_VERSION: true
|
||||
- name: Install npm packages
|
||||
run: npm ci -f
|
||||
env:
|
||||
DETECT_CHROMEDRIVER_VERSION: true
|
||||
|
||||
- name: Build Core
|
||||
run: npm run build:core:deploy
|
||||
- name: Install native dependencies (Tauri)
|
||||
if: matrix.platform == 'tauri' && matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install libwebkit2gtk-4.0-dev \
|
||||
build-essential \
|
||||
curl \
|
||||
wget \
|
||||
file \
|
||||
libssl-dev \
|
||||
libgtk-3-dev \
|
||||
libayatana-appindicator3-dev \
|
||||
librsvg2-dev
|
||||
|
||||
- name: Run eslint
|
||||
run: npm run lint
|
||||
- name: Build Core
|
||||
run: npm run build:core:deploy
|
||||
|
||||
- name: Build Frontend
|
||||
run: npm run publish
|
||||
- name: Run eslint (Web)
|
||||
if: matrix.platform == 'web'
|
||||
run: npm run lint
|
||||
|
||||
- name: Cache screenshots
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: test/screenshots
|
||||
key: ${{ runner.os }}-screenshots-${{ hashFiles('test/rendering-test.js') }}
|
||||
restore-keys: ${{ runner.os }}-screenshots-
|
||||
- name: Build Frontend (Web)
|
||||
if: matrix.platform == 'web'
|
||||
run: npm run publish
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
echo "::add-matcher::.github/workflows/test-failures.json"
|
||||
npm run test
|
||||
- name: Build Frontend (Tauri)
|
||||
if: matrix.platform == 'tauri'
|
||||
run: npm run tauri:build-html
|
||||
|
||||
- name: Upload screenshots
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Screenshots
|
||||
path: test/screenshots
|
||||
- name: Generate Icons (Tauri)
|
||||
if: matrix.platform == 'tauri'
|
||||
run: npm run tauri:icons
|
||||
|
||||
- name: Optimize
|
||||
if: github.repository == 'LiveSplit/LiveSplitOne' && github.ref == 'refs/heads/master'
|
||||
run: |
|
||||
WASM_FILE=$(ls dist/*.wasm)
|
||||
wasm-opt --all-features -O4 "$WASM_FILE" -o "$WASM_FILE"
|
||||
- name: Cache screenshots (Web)
|
||||
if: matrix.platform == 'web'
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: test/screenshots
|
||||
key: ${{ runner.os }}-screenshots-${{ hashFiles('test/rendering-test.js') }}
|
||||
restore-keys: ${{ runner.os }}-screenshots-
|
||||
|
||||
- name: Add CNAME file
|
||||
if: github.repository == 'LiveSplit/LiveSplitOne' && github.ref == 'refs/heads/master'
|
||||
run: cp ./.github/workflows/CNAME ./dist/CNAME
|
||||
- name: Run tests (Web)
|
||||
if: matrix.platform == 'web'
|
||||
run: |
|
||||
echo "::add-matcher::.github/workflows/test-failures.json"
|
||||
npm run test
|
||||
|
||||
- name: Deploy
|
||||
if: github.repository == 'LiveSplit/LiveSplitOne' && github.ref == 'refs/heads/master'
|
||||
uses: peaceiris/actions-gh-pages@v4
|
||||
with:
|
||||
deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }}
|
||||
publish_branch: gh-pages
|
||||
publish_dir: ./dist
|
||||
force_orphan: true
|
||||
- name: Upload screenshots (Web)
|
||||
if: matrix.platform == 'web' && (success() || failure())
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Screenshots
|
||||
path: test/screenshots
|
||||
|
||||
- name: Optimize
|
||||
if: github.repository == 'LiveSplit/LiveSplitOne' && github.ref == 'refs/heads/master'
|
||||
shell: bash
|
||||
run: |
|
||||
WASM_FILE=$(ls ${{ matrix.dist_path }}/*.wasm)
|
||||
wasm-opt --all-features -O4 "$WASM_FILE" -o "$WASM_FILE"
|
||||
|
||||
- name: Build (Tauri)
|
||||
if: matrix.platform == 'tauri'
|
||||
run: npm run tauri:publish
|
||||
|
||||
- name: Add CNAME file (Web)
|
||||
if: matrix.platform == 'web' && github.repository == 'LiveSplit/LiveSplitOne' && github.ref == 'refs/heads/master'
|
||||
run: cp ./.github/workflows/CNAME ./${{ matrix.dist_path }}/CNAME
|
||||
|
||||
- name: Deploy (Web)
|
||||
if: matrix.platform == 'web' && github.repository == 'LiveSplit/LiveSplitOne' && github.ref == 'refs/heads/master'
|
||||
uses: peaceiris/actions-gh-pages@v4
|
||||
with:
|
||||
deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }}
|
||||
publish_branch: gh-pages
|
||||
publish_dir: ./${{ matrix.dist_path }}
|
||||
force_orphan: true
|
||||
|
||||
+1
-1
Submodule livesplit-core updated: 922cc84754...3c3762bec2
Generated
+189
@@ -33,6 +33,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.13",
|
||||
"@tauri-apps/cli": "^1.6.0",
|
||||
"@types/node": "^20.12.7",
|
||||
"@types/selenium-webdriver": "^4.1.21",
|
||||
"@typescript-eslint/eslint-plugin": "^7.7.0",
|
||||
@@ -2897,6 +2898,194 @@
|
||||
"string.prototype.matchall": "^4.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-1.6.0.tgz",
|
||||
"integrity": "sha512-DBBpBl6GhTzm8ImMbKkfaZ4fDTykWrC7Q5OXP4XqD91recmDEn2LExuvuiiS3HYe7uP8Eb5B9NPHhqJb+Zo7qQ==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tauri": "tauri.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/tauri"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tauri-apps/cli-darwin-arm64": "1.6.0",
|
||||
"@tauri-apps/cli-darwin-x64": "1.6.0",
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf": "1.6.0",
|
||||
"@tauri-apps/cli-linux-arm64-gnu": "1.6.0",
|
||||
"@tauri-apps/cli-linux-arm64-musl": "1.6.0",
|
||||
"@tauri-apps/cli-linux-x64-gnu": "1.6.0",
|
||||
"@tauri-apps/cli-linux-x64-musl": "1.6.0",
|
||||
"@tauri-apps/cli-win32-arm64-msvc": "1.6.0",
|
||||
"@tauri-apps/cli-win32-ia32-msvc": "1.6.0",
|
||||
"@tauri-apps/cli-win32-x64-msvc": "1.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-darwin-arm64": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-1.6.0.tgz",
|
||||
"integrity": "sha512-SNRwUD9nqGxY47mbY1CGTt/jqyQOU7Ps7Mx/mpgahL0FVUDiCEY/5L9QfEPPhEgccgcelEVn7i6aQHIkHyUtCA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-darwin-x64": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-1.6.0.tgz",
|
||||
"integrity": "sha512-g2/uDR/eeH2arvuawA4WwaEOqv/7jDO/ZLNI3JlBjP5Pk8GGb3Kdy0ro1xQzF94mtk2mOnOXa4dMgAet4sUJ1A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-1.6.0.tgz",
|
||||
"integrity": "sha512-EVwf4oRkQyG8BpSrk0gqO7oA0sDM2MdNDtJpMfleYFEgCxLIOGZKNqaOW3M7U+0Y4qikmG3TtRK+ngc8Ymtrjg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm64-gnu": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-1.6.0.tgz",
|
||||
"integrity": "sha512-YdpY17cAySrhK9dX4BUVEmhAxE2o+6skIEFg8iN/xrDwRxhaNPI9I80YXPatUTX54Kx55T5++25VJG9+3iw83A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm64-musl": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.6.0.tgz",
|
||||
"integrity": "sha512-4U628tuf2U8pMr4tIBJhEkrFwt+46dwhXrDlpdyWSZtnop5RJAVKHODm0KbWns4xGKfTW1F3r6sSv+2ZxLcISA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-x64-gnu": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-1.6.0.tgz",
|
||||
"integrity": "sha512-AKRzp76fVUaJyXj5KRJT9bJyhwZyUnRQU0RqIRqOtZCT5yr6qGP8rjtQ7YhCIzWrseBlOllc3Qvbgw3Yl0VQcA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-x64-musl": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-1.6.0.tgz",
|
||||
"integrity": "sha512-0edIdq6aMBTaRMIXddHfyAFL361JqulLLd2Wi2aoOie7DkQ2MYh6gv3hA7NB9gqFwNIGE+xtJ4BkXIP2tSGPlg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-arm64-msvc": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-1.6.0.tgz",
|
||||
"integrity": "sha512-QwWpWk4ubcwJ1rljsRAmINgB2AwkyzZhpYbalA+MmzyYMREcdXWGkyixWbRZgqc6fEWEBmq5UG73qz5eBJiIKg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-ia32-msvc": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-1.6.0.tgz",
|
||||
"integrity": "sha512-Vtw0yxO9+aEFuhuxQ57ALG43tjECopRimRuKGbtZYDCriB/ty5TrT3QWMdy0dxBkpDTu3Rqsz30sbDzw6tlP3Q==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-1.6.0.tgz",
|
||||
"integrity": "sha512-h54FHOvGi7+LIfRchzgZYSCHB1HDlP599vWXQQJ/XnwJY+6Rwr2E5bOe/EhqoG8rbGkfK0xX3KPAvXPbUlmggg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@testim/chrome-version": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@testim/chrome-version/-/chrome-version-1.1.4.tgz",
|
||||
|
||||
+6
-1
@@ -13,7 +13,11 @@
|
||||
"publish": "webpack --mode production",
|
||||
"serve": "webpack serve",
|
||||
"start": "webpack serve --open",
|
||||
"test": "mocha --exit test/**/*.js"
|
||||
"test": "mocha --exit test/**/*.js",
|
||||
"tauri:icons": "tauri icon src/assets/icon.png",
|
||||
"tauri:build-html": "webpack --mode production --env TAURI=true",
|
||||
"tauri:watch": "tauri dev",
|
||||
"tauri:publish": "tauri build"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
@@ -42,6 +46,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.13",
|
||||
"@tauri-apps/cli": "^1.6.0",
|
||||
"@types/node": "^20.12.7",
|
||||
"@types/selenium-webdriver": "^4.1.21",
|
||||
"@typescript-eslint/eslint-plugin": "^7.7.0",
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
||||
# Generated by Tauri
|
||||
# will have schema files for capabilities auto-completion
|
||||
/gen/schemas
|
||||
|
||||
/icons
|
||||
Generated
+4636
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "livesplit-one"
|
||||
version = "0.1.0"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "1", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "1", features = [ "http-all"] }
|
||||
serde = { version = "1" }
|
||||
serde_derive = { version = "1" }
|
||||
serde_json = "1"
|
||||
livesplit-core = { path = "../livesplit-core" }
|
||||
|
||||
[features]
|
||||
# This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!!
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
panic = "abort"
|
||||
codegen-units = 1
|
||||
strip = true
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
future::Future,
|
||||
str::FromStr,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
use livesplit_core::{
|
||||
event::{CommandSink, Event, Result},
|
||||
hotkey::KeyCode,
|
||||
networking::server_protocol::Command,
|
||||
HotkeyConfig, HotkeySystem, TimeSpan, TimingMethod,
|
||||
};
|
||||
use tauri::{Manager, Window};
|
||||
|
||||
struct State {
|
||||
hotkey_system: RwLock<Option<HotkeySystem<TauriCommandSink>>>,
|
||||
window: RwLock<Option<Window>>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn set_hotkey_config(state: tauri::State<'_, State>, config: HotkeyConfig) -> bool {
|
||||
if let Some(hotkey_system) = &mut *state.hotkey_system.write().unwrap() {
|
||||
hotkey_system.set_config(config).is_ok()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn set_hotkey_activation(state: tauri::State<'_, State>, active: bool) -> bool {
|
||||
if let Some(hotkey_system) = &mut *state.hotkey_system.write().unwrap() {
|
||||
if active {
|
||||
hotkey_system.activate()
|
||||
} else {
|
||||
hotkey_system.deactivate()
|
||||
}
|
||||
.is_ok()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_hotkey_config(state: tauri::State<'_, State>) -> HotkeyConfig {
|
||||
if let Some(hotkey_system) = &*state.hotkey_system.read().unwrap() {
|
||||
hotkey_system.config()
|
||||
} else {
|
||||
HotkeyConfig::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn resolve_hotkey(state: tauri::State<'_, State>, key_code: String) -> Cow<'static, str> {
|
||||
if let Some(hotkey_system) = &*state.hotkey_system.read().unwrap() {
|
||||
if let Ok(key_code) = KeyCode::from_str(&key_code) {
|
||||
return hotkey_system.resolve(key_code);
|
||||
}
|
||||
}
|
||||
key_code.into()
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TauriCommandSink(Arc<RwLock<Option<Window>>>);
|
||||
|
||||
impl TauriCommandSink {
|
||||
fn send(&self, command: Command) {
|
||||
self.0
|
||||
.read()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.emit("command", command)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl CommandSink for TauriCommandSink {
|
||||
fn start(&self) -> impl Future<Output = Result> + 'static {
|
||||
self.send(Command::Start);
|
||||
async { Ok(Event::Unknown) }
|
||||
}
|
||||
|
||||
fn split(&self) -> impl Future<Output = Result> + 'static {
|
||||
self.send(Command::Split);
|
||||
async { Ok(Event::Unknown) }
|
||||
}
|
||||
|
||||
fn split_or_start(&self) -> impl Future<Output = Result> + 'static {
|
||||
self.send(Command::SplitOrStart);
|
||||
async { Ok(Event::Unknown) }
|
||||
}
|
||||
|
||||
fn reset(&self, save_attempt: Option<bool>) -> impl Future<Output = Result> + 'static {
|
||||
self.send(Command::Reset { save_attempt });
|
||||
async { Ok(Event::Unknown) }
|
||||
}
|
||||
|
||||
fn undo_split(&self) -> impl Future<Output = Result> + 'static {
|
||||
self.send(Command::UndoSplit);
|
||||
async { Ok(Event::Unknown) }
|
||||
}
|
||||
|
||||
fn skip_split(&self) -> impl Future<Output = Result> + 'static {
|
||||
self.send(Command::SkipSplit);
|
||||
async { Ok(Event::Unknown) }
|
||||
}
|
||||
|
||||
fn toggle_pause_or_start(&self) -> impl Future<Output = Result> + 'static {
|
||||
self.send(Command::TogglePauseOrStart);
|
||||
async { Ok(Event::Unknown) }
|
||||
}
|
||||
|
||||
fn pause(&self) -> impl Future<Output = Result> + 'static {
|
||||
self.send(Command::Pause);
|
||||
async { Ok(Event::Unknown) }
|
||||
}
|
||||
|
||||
fn resume(&self) -> impl Future<Output = Result> + 'static {
|
||||
self.send(Command::Resume);
|
||||
async { Ok(Event::Unknown) }
|
||||
}
|
||||
|
||||
fn undo_all_pauses(&self) -> impl Future<Output = Result> + 'static {
|
||||
self.send(Command::UndoAllPauses);
|
||||
async { Ok(Event::Unknown) }
|
||||
}
|
||||
|
||||
fn switch_to_previous_comparison(&self) -> impl Future<Output = Result> + 'static {
|
||||
self.send(Command::SwitchToPreviousComparison);
|
||||
async { Ok(Event::Unknown) }
|
||||
}
|
||||
|
||||
fn switch_to_next_comparison(&self) -> impl Future<Output = Result> + 'static {
|
||||
self.send(Command::SwitchToNextComparison);
|
||||
async { Ok(Event::Unknown) }
|
||||
}
|
||||
|
||||
fn set_current_comparison(
|
||||
&self,
|
||||
comparison: Cow<'_, str>,
|
||||
) -> impl Future<Output = Result> + 'static {
|
||||
self.send(Command::SetCurrentComparison { comparison });
|
||||
async { Ok(Event::Unknown) }
|
||||
}
|
||||
|
||||
fn toggle_timing_method(&self) -> impl Future<Output = Result> + 'static {
|
||||
self.send(Command::ToggleTimingMethod);
|
||||
async { Ok(Event::Unknown) }
|
||||
}
|
||||
|
||||
fn set_current_timing_method(
|
||||
&self,
|
||||
method: TimingMethod,
|
||||
) -> impl Future<Output = Result> + 'static {
|
||||
self.send(Command::SetCurrentTimingMethod {
|
||||
timing_method: method,
|
||||
});
|
||||
async { Ok(Event::Unknown) }
|
||||
}
|
||||
|
||||
fn initialize_game_time(&self) -> impl Future<Output = Result> + 'static {
|
||||
self.send(Command::InitializeGameTime);
|
||||
async { Ok(Event::Unknown) }
|
||||
}
|
||||
|
||||
fn set_game_time(&self, time: TimeSpan) -> impl Future<Output = Result> + 'static {
|
||||
self.send(Command::SetGameTime { time });
|
||||
async { Ok(Event::Unknown) }
|
||||
}
|
||||
|
||||
fn pause_game_time(&self) -> impl Future<Output = Result> + 'static {
|
||||
self.send(Command::PauseGameTime);
|
||||
async { Ok(Event::Unknown) }
|
||||
}
|
||||
|
||||
fn resume_game_time(&self) -> impl Future<Output = Result> + 'static {
|
||||
self.send(Command::ResumeGameTime);
|
||||
async { Ok(Event::Unknown) }
|
||||
}
|
||||
|
||||
fn set_loading_times(&self, time: TimeSpan) -> impl Future<Output = Result> + 'static {
|
||||
self.send(Command::SetLoadingTimes { time });
|
||||
async { Ok(Event::Unknown) }
|
||||
}
|
||||
|
||||
fn set_custom_variable(
|
||||
&self,
|
||||
key: Cow<'_, str>,
|
||||
value: Cow<'_, str>,
|
||||
) -> impl Future<Output = Result> + 'static {
|
||||
self.send(Command::SetCustomVariable { key, value });
|
||||
async { Ok(Event::Unknown) }
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let sink = TauriCommandSink(Arc::new(RwLock::new(None)));
|
||||
let hotkey_system = RwLock::new(HotkeySystem::new(sink.clone()).ok());
|
||||
tauri::Builder::default()
|
||||
.manage(State {
|
||||
hotkey_system,
|
||||
window: RwLock::new(None),
|
||||
})
|
||||
.setup(move |app| {
|
||||
let main_window = app.windows().values().next().unwrap().clone();
|
||||
app.state::<State>()
|
||||
.window
|
||||
.write()
|
||||
.unwrap()
|
||||
.replace(main_window.clone());
|
||||
*sink.0.write().unwrap() = Some(main_window);
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
set_hotkey_config,
|
||||
set_hotkey_activation,
|
||||
get_hotkey_config,
|
||||
resolve_hotkey,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"build": {
|
||||
"devPath": "target/dist",
|
||||
"distDir": "target/dist",
|
||||
"withGlobalTauri": true
|
||||
},
|
||||
"package": {
|
||||
"productName": "LiveSplit One",
|
||||
"version": "0.1.0"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
"all": false,
|
||||
"http": {
|
||||
"all": true,
|
||||
"request": true,
|
||||
"scope": ["https://www.speedrun.com/static/*"]
|
||||
}
|
||||
},
|
||||
"windows": [
|
||||
{
|
||||
"title": "LiveSplit One",
|
||||
"width": 850,
|
||||
"height": 750
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"identifier": "org.livesplit.one",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/[email protected]",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 106 KiB |
+1
-1
@@ -18,7 +18,7 @@ if (typeof Symbol.asyncDispose !== "symbol") {
|
||||
});
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
|
||||
if (process.env.NODE_ENV === "production" && window.__TAURI__ == null && "serviceWorker" in navigator) {
|
||||
navigator.serviceWorker.register("/service-worker.js");
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { WebRenderer } from "../livesplit-core/livesplit_core";
|
||||
import AutoRefresh from "../util/AutoRefresh";
|
||||
import { UrlCache } from "../util/UrlCache";
|
||||
import "../css/Layout.scss";
|
||||
import { GeneralSettings } from "../ui/SettingsEditor";
|
||||
import { GeneralSettings } from "../ui/MainSettings";
|
||||
|
||||
export interface Props {
|
||||
getState: () => LayoutStateRef,
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
export async function corsBustingFetch(url: string, signal?: AbortSignal): Promise<ArrayBuffer> {
|
||||
if (window.__TAURI__ != null) {
|
||||
const response = await window.__TAURI__.http.fetch(
|
||||
url,
|
||||
{ responseType: 3 },
|
||||
);
|
||||
if (signal != null) {
|
||||
signal.throwIfAborted();
|
||||
}
|
||||
return new Uint8Array(response.data);
|
||||
} else {
|
||||
const response = await fetch(url, { signal });
|
||||
return response.arrayBuffer();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { CommandSinkRef, HotkeyConfig, HotkeySystem } from "../livesplit-core";
|
||||
import { expect } from "../util/OptionUtil";
|
||||
|
||||
export interface HotkeyImplementation {
|
||||
config(): Promise<HotkeyConfig> | HotkeyConfig,
|
||||
setConfig(config: HotkeyConfig): void,
|
||||
activate(): void,
|
||||
deactivate(): void,
|
||||
resolve(keyCode: string): Promise<string> | string,
|
||||
}
|
||||
|
||||
class GlobalHotkeys implements HotkeyImplementation {
|
||||
constructor(private hotkeySystem?: HotkeySystem) { }
|
||||
|
||||
public async config(): Promise<HotkeyConfig> {
|
||||
return expect(HotkeyConfig.parseJson(
|
||||
await window.__TAURI__!.tauri.invoke("get_hotkey_config"),
|
||||
), "Couldn't parse the hotkey config.");
|
||||
}
|
||||
|
||||
public setConfig(config: HotkeyConfig): void {
|
||||
window.__TAURI__!.tauri.invoke("set_hotkey_config", { config: config.asJson() });
|
||||
if (this.hotkeySystem != null) {
|
||||
this.hotkeySystem.setConfig(config);
|
||||
} else {
|
||||
config[Symbol.dispose]();
|
||||
}
|
||||
}
|
||||
|
||||
setConfigJson(configJson: unknown): void {
|
||||
window.__TAURI__!.tauri.invoke("set_hotkey_config", { config: configJson });
|
||||
if (this.hotkeySystem != null) {
|
||||
const config = HotkeyConfig.parseJson(configJson);
|
||||
if (config != null) {
|
||||
this.hotkeySystem.setConfig(config);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public activate(): void {
|
||||
window.__TAURI__!.tauri.invoke("set_hotkey_activation", { active: true });
|
||||
this.hotkeySystem?.activate();
|
||||
}
|
||||
|
||||
public deactivate(): void {
|
||||
window.__TAURI__!.tauri.invoke("set_hotkey_activation", { active: false });
|
||||
this.hotkeySystem?.deactivate();
|
||||
}
|
||||
|
||||
public resolve(keyCode: string): Promise<string> {
|
||||
return window.__TAURI__!.tauri.invoke("resolve_hotkey", { keyCode });
|
||||
}
|
||||
}
|
||||
|
||||
export function createHotkeys(commandSink: CommandSinkRef, configJson: unknown): HotkeyImplementation {
|
||||
let hotkeySystem: HotkeySystem | null = null;
|
||||
|
||||
const tauri = window.__TAURI__ != null;
|
||||
|
||||
if (!tauri || navigator.platform === "Win32") {
|
||||
try {
|
||||
const config = HotkeyConfig.parseJson(configJson);
|
||||
if (config !== null) {
|
||||
hotkeySystem = HotkeySystem.withConfig(commandSink, config);
|
||||
}
|
||||
} catch (_) { /* Looks like the storage has no valid data */ }
|
||||
|
||||
if (hotkeySystem == null) {
|
||||
hotkeySystem = expect(
|
||||
HotkeySystem.new(commandSink),
|
||||
"Couldn't initialize the hotkeys",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (tauri) {
|
||||
const globalHotkeys = new GlobalHotkeys(hotkeySystem ?? undefined);
|
||||
if (configJson != null) {
|
||||
globalHotkeys.setConfigJson(configJson);
|
||||
}
|
||||
return globalHotkeys;
|
||||
} else {
|
||||
return hotkeySystem!;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { openDB, IDBPDatabase } from "idb";
|
||||
import { Option, assert } from "../util/OptionUtil";
|
||||
import { RunRef, Run, TimingMethod } from "../livesplit-core";
|
||||
import { GeneralSettings } from "../ui/SettingsEditor";
|
||||
import { GeneralSettings } from "../ui/MainSettings";
|
||||
import { FRAME_RATE_AUTOMATIC } from "../util/FrameRate";
|
||||
|
||||
export type HotkeyConfigSettings = unknown;
|
||||
@@ -247,9 +247,11 @@ export async function loadGeneralSettings(): Promise<GeneralSettings> {
|
||||
|
||||
const generalSettings = await db.get("settings", "generalSettings") ?? {};
|
||||
|
||||
const isTauri = window.__TAURI__ != null;
|
||||
|
||||
return {
|
||||
frameRate: generalSettings.frameRate ?? FRAME_RATE_AUTOMATIC,
|
||||
showControlButtons: generalSettings.showControlButtons ?? true,
|
||||
showControlButtons: generalSettings.showControlButtons ?? !isTauri,
|
||||
showManualGameTime: generalSettings.showManualGameTime ?? false,
|
||||
saveOnReset: generalSettings.saveOnReset ?? false,
|
||||
speedrunComIntegration: generalSettings.speedrunComIntegration ?? true,
|
||||
|
||||
Vendored
+46
@@ -0,0 +1,46 @@
|
||||
declare var __TAURI__: GlobalTauri | undefined;
|
||||
|
||||
declare interface GlobalTauri {
|
||||
tauri: TauriModule;
|
||||
event: TauriEventModule;
|
||||
notification: TauriNotificationModule;
|
||||
http: TauriHttpModule;
|
||||
}
|
||||
|
||||
declare interface TauriModule {
|
||||
invoke<T>(cmd: string, args?: Record<string, unknown>): Promise<T>;
|
||||
}
|
||||
|
||||
declare interface TauriEventModule {
|
||||
listen(eventName: string, callback: (event: TauriEvent) => void): Promise<ListenHandle>;
|
||||
}
|
||||
|
||||
declare interface TauriNotificationModule {
|
||||
isPermissionGranted(): Promise<boolean>;
|
||||
requestPermission(): Promise<string>;
|
||||
sendNotification(notification: TauriNotification): void;
|
||||
}
|
||||
|
||||
declare interface TauriNotification {
|
||||
title: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
declare interface TauriHttpModule {
|
||||
fetch(url: string, options: { responseType: 3 }): Promise<TauriResponse<Array<number>>>;
|
||||
}
|
||||
|
||||
declare interface TauriResponse<T> {
|
||||
data: T,
|
||||
}
|
||||
|
||||
declare interface TauriHttpOptions {
|
||||
responseType: number;
|
||||
}
|
||||
|
||||
declare interface TauriEvent {
|
||||
event: string;
|
||||
payload: unknown;
|
||||
}
|
||||
|
||||
declare interface ListenHandle { }
|
||||
+22
-6
@@ -4,7 +4,7 @@ import { hotkeySystem } from "./LiveSplit";
|
||||
|
||||
import "../css/HotkeyButton.scss";
|
||||
|
||||
function resolveKey(keyCode: string): string {
|
||||
function resolveKey(keyCode: string): Promise<string> | string {
|
||||
return expect(hotkeySystem, "The Hotkey System should always be initialized.").resolve(keyCode);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface Props {
|
||||
export interface State {
|
||||
listener: Option<EventListenerObject>,
|
||||
intervalHandle: Option<number>,
|
||||
resolvedKey: Option<string>,
|
||||
}
|
||||
|
||||
export default class HotkeyButton extends React.Component<Props, State> {
|
||||
@@ -25,19 +26,34 @@ export default class HotkeyButton extends React.Component<Props, State> {
|
||||
this.state = {
|
||||
listener: null,
|
||||
intervalHandle: null,
|
||||
resolvedKey: null,
|
||||
};
|
||||
|
||||
this.updateResolvedKey(props.value);
|
||||
}
|
||||
public componentDidUpdate(previousProps: Props) {
|
||||
if (previousProps.value !== this.props.value) {
|
||||
this.updateResolvedKey(this.props.value);
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const value = this.props.value;
|
||||
let buttonText = null;
|
||||
private async updateResolvedKey(value: Option<string>): Promise<void> {
|
||||
let resolvedKey = "";
|
||||
if (value != null) {
|
||||
const matches = value.match(/(.+)\+\s*(.+)$/);
|
||||
if (matches != null) {
|
||||
buttonText = `${matches[1]}+ ${resolveKey(matches[2])}`;
|
||||
resolvedKey = `${matches[1]}+ ${await resolveKey(matches[2])}`;
|
||||
} else {
|
||||
buttonText = resolveKey(value);
|
||||
resolvedKey = await resolveKey(value);
|
||||
}
|
||||
}
|
||||
this.setState({ resolvedKey });
|
||||
}
|
||||
|
||||
public render() {
|
||||
let buttonText: Option<string> | JSX.Element = null;
|
||||
if (this.props.value != null) {
|
||||
buttonText = this.state.resolvedKey;
|
||||
} else if (this.state.listener != null) {
|
||||
buttonText = <i className="fa fa-circle" aria-hidden="true" />;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { SettingsComponent } from "./Settings";
|
||||
import { UrlCache } from "../util/UrlCache";
|
||||
import Layout from "../layout/Layout";
|
||||
import { WebRenderer } from "../livesplit-core/livesplit_core";
|
||||
import { GeneralSettings } from "./SettingsEditor";
|
||||
import { GeneralSettings } from "./MainSettings";
|
||||
import { LSOCommandSink } from "./LSOCommandSink";
|
||||
|
||||
import "../css/LayoutEditor.scss";
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Layout, LayoutStateRefMut, TimerPhase, TimingMethod } from "../livespli
|
||||
import { TimerView } from "./TimerView";
|
||||
import { UrlCache } from "../util/UrlCache";
|
||||
import { WebRenderer } from "../livesplit-core/livesplit_core";
|
||||
import { GeneralSettings } from "./SettingsEditor";
|
||||
import { GeneralSettings } from "./MainSettings";
|
||||
import { LiveSplitServer } from "../api/LiveSplitServer";
|
||||
import { Option } from "../util/OptionUtil";
|
||||
import { LSOCommandSink } from "./LSOCommandSink";
|
||||
@@ -42,7 +42,7 @@ interface Callbacks {
|
||||
openLayoutEditor(): void,
|
||||
openLayoutView(): void,
|
||||
openSplitsView(): void,
|
||||
openSettingsEditor(): void,
|
||||
openMainSettings(): void,
|
||||
openTimerView(): void,
|
||||
renderViewWithSidebar(renderedView: JSX.Element, sidebarContent: JSX.Element): JSX.Element,
|
||||
saveLayout(): void,
|
||||
|
||||
+24
-31
@@ -1,7 +1,7 @@
|
||||
import * as React from "react";
|
||||
import Sidebar from "react-sidebar";
|
||||
import {
|
||||
HotkeySystem, Layout, LayoutEditor, Run, RunEditor, Segment,
|
||||
Layout, LayoutEditor, Run, RunEditor, Segment,
|
||||
Timer, HotkeyConfig, LayoutState, LayoutStateJson,
|
||||
TimingMethod, TimerPhase,
|
||||
Event,
|
||||
@@ -11,7 +11,7 @@ import { Option, assertNull, expect, maybeDisposeAndThen, panic } from "../util/
|
||||
import * as SplitsIO from "../util/SplitsIO";
|
||||
import { LayoutEditor as LayoutEditorComponent } from "./LayoutEditor";
|
||||
import { RunEditor as RunEditorComponent } from "./RunEditor";
|
||||
import { GeneralSettings, SettingsEditor as SettingsEditorComponent } from "./SettingsEditor";
|
||||
import { GeneralSettings, MainSettings as SettingsEditorComponent } from "./MainSettings";
|
||||
import { TimerView } from "./TimerView";
|
||||
import { About } from "./About";
|
||||
import { SplitsSelection, EditingInfo } from "./SplitsSelection";
|
||||
@@ -19,10 +19,11 @@ import { LayoutView } from "./LayoutView";
|
||||
import { ToastContainer, toast } from "react-toastify";
|
||||
import * as Storage from "../storage";
|
||||
import { UrlCache } from "../util/UrlCache";
|
||||
import { WebRenderer } from "../livesplit-core/livesplit_core";
|
||||
import { ServerProtocol, WebRenderer } from "../livesplit-core/livesplit_core";
|
||||
import { LiveSplitServer } from "../api/LiveSplitServer";
|
||||
import { LSOCommandSink } from "./LSOCommandSink";
|
||||
import DialogContainer from "./Dialog";
|
||||
import { createHotkeys, HotkeyImplementation } from "../platform/Hotkeys";
|
||||
|
||||
import variables from "../css/variables.scss";
|
||||
|
||||
@@ -39,7 +40,7 @@ export enum MenuKind {
|
||||
RunEditor,
|
||||
Layout,
|
||||
LayoutEditor,
|
||||
SettingsEditor,
|
||||
MainSettings,
|
||||
About,
|
||||
}
|
||||
|
||||
@@ -59,7 +60,7 @@ type Menu =
|
||||
{ kind: MenuKind.RunEditor, editor: RunEditor, splitsKey?: number } |
|
||||
{ kind: MenuKind.Layout } |
|
||||
{ kind: MenuKind.LayoutEditor, editor: LayoutEditor } |
|
||||
{ kind: MenuKind.SettingsEditor, config: HotkeyConfig } |
|
||||
{ kind: MenuKind.MainSettings, config: HotkeyConfig } |
|
||||
{ kind: MenuKind.About };
|
||||
|
||||
export interface Props {
|
||||
@@ -75,7 +76,7 @@ export interface Props {
|
||||
}
|
||||
|
||||
export interface State {
|
||||
hotkeySystem: HotkeySystem,
|
||||
hotkeySystem: HotkeyImplementation,
|
||||
isBrowserSource: boolean,
|
||||
isDesktop: boolean,
|
||||
layout: Layout,
|
||||
@@ -104,7 +105,7 @@ export interface State {
|
||||
layoutModified: boolean,
|
||||
}
|
||||
|
||||
export let hotkeySystem: Option<HotkeySystem> = null;
|
||||
export let hotkeySystem: Option<HotkeyImplementation> = null;
|
||||
|
||||
export class LiveSplit extends React.Component<Props, State> {
|
||||
public static async loadStoredData() {
|
||||
@@ -150,21 +151,7 @@ export class LiveSplit extends React.Component<Props, State> {
|
||||
this,
|
||||
);
|
||||
|
||||
const hotkeys = props.hotkeys;
|
||||
try {
|
||||
if (hotkeys !== undefined) {
|
||||
const config = HotkeyConfig.parseJson(hotkeys);
|
||||
if (config !== null) {
|
||||
hotkeySystem = HotkeySystem.withConfig(commandSink.getCommandSink(), config);
|
||||
}
|
||||
}
|
||||
} catch (_) { /* Looks like the storage has no valid data */ }
|
||||
if (hotkeySystem == null) {
|
||||
hotkeySystem = expect(
|
||||
HotkeySystem.new(commandSink.getCommandSink()),
|
||||
"Couldn't initialize the hotkeys",
|
||||
);
|
||||
}
|
||||
hotkeySystem = createHotkeys(commandSink.getCommandSink(), props.hotkeys);
|
||||
|
||||
if (window.location.hash.indexOf("#/splits-io/") === 0) {
|
||||
const loadingRun = Run.new();
|
||||
@@ -236,6 +223,13 @@ export class LiveSplit extends React.Component<Props, State> {
|
||||
layoutModified: false,
|
||||
};
|
||||
|
||||
if (window.__TAURI__ != null) {
|
||||
window.__TAURI__.event.listen("command", (event) => {
|
||||
const payloadString = JSON.stringify(event.payload);
|
||||
ServerProtocol.handleCommand(payloadString, commandSink.getCommandSink().ptr);
|
||||
});
|
||||
}
|
||||
|
||||
this.updateBadge();
|
||||
|
||||
this.mediaQueryChanged = this.mediaQueryChanged.bind(this);
|
||||
@@ -308,7 +302,6 @@ export class LiveSplit extends React.Component<Props, State> {
|
||||
this.state.commandSink[Symbol.dispose]();
|
||||
this.state.layout[Symbol.dispose]();
|
||||
this.state.layoutState[Symbol.dispose]();
|
||||
this.state.hotkeySystem?.[Symbol.dispose]();
|
||||
|
||||
// This is bound in the constructor
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
@@ -345,7 +338,7 @@ export class LiveSplit extends React.Component<Props, State> {
|
||||
renderer={this.state.renderer}
|
||||
callbacks={this}
|
||||
/>;
|
||||
} else if (this.state.menu.kind === MenuKind.SettingsEditor) {
|
||||
} else if (this.state.menu.kind === MenuKind.MainSettings) {
|
||||
view = <SettingsEditorComponent
|
||||
generalSettings={this.state.generalSettings}
|
||||
hotkeyConfig={this.state.menu.config}
|
||||
@@ -635,24 +628,24 @@ export class LiveSplit extends React.Component<Props, State> {
|
||||
this.openTimerView();
|
||||
}
|
||||
|
||||
public openSettingsEditor() {
|
||||
public async openMainSettings() {
|
||||
this.changeMenu({
|
||||
kind: MenuKind.SettingsEditor,
|
||||
config: this.state.hotkeySystem.config(),
|
||||
kind: MenuKind.MainSettings,
|
||||
config: await this.state.hotkeySystem.config(),
|
||||
});
|
||||
}
|
||||
|
||||
public async closeSettingsEditor(save: boolean, generalSettings: GeneralSettings) {
|
||||
public async closeMainSettings(save: boolean, generalSettings: GeneralSettings) {
|
||||
const menu = this.state.menu;
|
||||
|
||||
if (menu.kind !== MenuKind.SettingsEditor) {
|
||||
if (menu.kind !== MenuKind.MainSettings) {
|
||||
panic("No Settings Editor to close.");
|
||||
}
|
||||
|
||||
if (save) {
|
||||
try {
|
||||
const hotkeys = menu.config.asJson();
|
||||
await Storage.storeHotkeys(hotkeys);
|
||||
const config = menu.config.asJson();
|
||||
await Storage.storeHotkeys(config);
|
||||
} catch {
|
||||
toast.error("Failed to save the hotkey settings.");
|
||||
}
|
||||
|
||||
@@ -38,13 +38,13 @@ export interface State {
|
||||
|
||||
interface Callbacks {
|
||||
renderViewWithSidebar(renderedView: JSX.Element, sidebarContent: JSX.Element): JSX.Element,
|
||||
closeSettingsEditor(save: boolean, newGeneralSettings: GeneralSettings): void,
|
||||
closeMainSettings(save: boolean, newGeneralSettings: GeneralSettings): void,
|
||||
onServerConnectionOpened(serverConnection: LiveSplitServer): void,
|
||||
onServerConnectionClosed(): void,
|
||||
forceUpdate(): void,
|
||||
}
|
||||
|
||||
export class SettingsEditor extends React.Component<Props, State> {
|
||||
export class MainSettings extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
@@ -257,13 +257,13 @@ export class SettingsEditor extends React.Component<Props, State> {
|
||||
<div className="small">
|
||||
<button
|
||||
className="toggle-left"
|
||||
onClick={(_) => this.props.callbacks.closeSettingsEditor(true, this.state.generalSettings)}
|
||||
onClick={(_) => this.props.callbacks.closeMainSettings(true, this.state.generalSettings)}
|
||||
>
|
||||
<i className="fa fa-check" aria-hidden="true" /> OK
|
||||
</button>
|
||||
<button
|
||||
className="toggle-right"
|
||||
onClick={(_) => this.props.callbacks.closeSettingsEditor(false, this.state.generalSettings)}
|
||||
onClick={(_) => this.props.callbacks.closeMainSettings(false, this.state.generalSettings)}
|
||||
>
|
||||
<i className="fa fa-times" aria-hidden="true" /> Cancel
|
||||
</button>
|
||||
@@ -23,8 +23,9 @@ import {
|
||||
} from "./Settings";
|
||||
import { renderMarkdown, replaceFlag } from "../util/Markdown";
|
||||
import { UrlCache } from "../util/UrlCache";
|
||||
import { GeneralSettings } from "./SettingsEditor";
|
||||
import { GeneralSettings } from "./MainSettings";
|
||||
import { showDialog } from "./Dialog";
|
||||
import { corsBustingFetch } from "../platform/CORS";
|
||||
|
||||
import "../css/RunEditor.scss";
|
||||
|
||||
@@ -2020,8 +2021,7 @@ export class RunEditor extends React.Component<Props, State> {
|
||||
if (game !== undefined) {
|
||||
const uri = game.assets["cover-medium"].uri;
|
||||
if (uri.startsWith("https://") && uri !== "https://www.speedrun.com/images/blankcover.png") {
|
||||
const response = await fetch(uri, { signal });
|
||||
const buffer = await response.arrayBuffer();
|
||||
const buffer = await corsBustingFetch(uri, signal);
|
||||
if (this.props.editor.ptr === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -2050,8 +2050,7 @@ export class RunEditor extends React.Component<Props, State> {
|
||||
if (game !== undefined) {
|
||||
const uri = game.assets.icon.uri;
|
||||
if (uri.startsWith("https://") && uri !== "https://www.speedrun.com/images/1st.png") {
|
||||
const response = await fetch(uri, { signal });
|
||||
const buffer = await response.arrayBuffer();
|
||||
const buffer = await corsBustingFetch(uri, signal);
|
||||
if (this.props.editor.ptr === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { openFileAsArrayBuffer, exportFile, convertFileToArrayBuffer, FILE_EXT_S
|
||||
import { Option, bug, maybeDisposeAndThen } from "../util/OptionUtil";
|
||||
import DragUpload from "./DragUpload";
|
||||
import { ContextMenuTrigger, ContextMenu, MenuItem } from "react-contextmenu";
|
||||
import { GeneralSettings } from "./SettingsEditor";
|
||||
import { GeneralSettings } from "./MainSettings";
|
||||
import { LSOCommandSink } from "./LSOCommandSink";
|
||||
import { showDialog } from "./Dialog";
|
||||
|
||||
@@ -187,7 +187,7 @@ export class SplitsSelection extends React.Component<Props, State> {
|
||||
<hr />
|
||||
<button onClick={(_) => {
|
||||
if (this.props.commandSink.currentPhase() !== TimerPhase.NotRunning) {
|
||||
toast.error("You can't edit your run while the timer is running.");
|
||||
toast.error("You can't edit your splits while the timer is running.");
|
||||
return;
|
||||
}
|
||||
const run = this.props.commandSink.getRun().clone();
|
||||
|
||||
@@ -6,7 +6,7 @@ import DragUpload from "./DragUpload";
|
||||
import Layout from "../layout/Layout";
|
||||
import { UrlCache } from "../util/UrlCache";
|
||||
import { WebRenderer } from "../livesplit-core/livesplit_core";
|
||||
import { GeneralSettings } from "./SettingsEditor";
|
||||
import { GeneralSettings } from "./MainSettings";
|
||||
import { LiveSplitServer } from "../api/LiveSplitServer";
|
||||
import { LSOCommandSink } from "./LSOCommandSink";
|
||||
|
||||
@@ -47,7 +47,7 @@ interface Callbacks {
|
||||
openAboutView(): void,
|
||||
openLayoutView(): void,
|
||||
openSplitsView(): void,
|
||||
openSettingsEditor(): void,
|
||||
openMainSettings(): void,
|
||||
renderViewWithSidebar(renderedView: JSX.Element, sidebarContent: JSX.Element): JSX.Element,
|
||||
onServerConnectionClosed(): void,
|
||||
onServerConnectionOpened(serverConnection: LiveSplitServer): void,
|
||||
@@ -262,7 +262,7 @@ export class TimerView extends React.Component<Props, State> {
|
||||
</button>
|
||||
</div>
|
||||
<hr />
|
||||
<button onClick={() => this.props.callbacks.openSettingsEditor()}>
|
||||
<button onClick={() => this.props.callbacks.openMainSettings()}>
|
||||
<i className="fa fa-cog" aria-hidden="true" /> Settings
|
||||
</button>
|
||||
<button onClick={(_) => this.props.callbacks.openAboutView()}>
|
||||
|
||||
+14
-7
@@ -83,7 +83,11 @@ export default async (env, argv) => {
|
||||
const basePath = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const isProduction = argv.mode === "production";
|
||||
const distPath = path.join(basePath, "dist");
|
||||
const isTauri = env.TAURI === "true";
|
||||
const distPath = path.join(...[
|
||||
basePath,
|
||||
...(isTauri ? ["src-tauri", "target", "dist"] : ["dist"]),
|
||||
]);
|
||||
|
||||
return {
|
||||
entry: {
|
||||
@@ -107,8 +111,11 @@ export default async (env, argv) => {
|
||||
},
|
||||
|
||||
plugins: [
|
||||
...(isProduction ? [new CleanWebpackPlugin()] : []),
|
||||
new FaviconsWebpackPlugin({
|
||||
...(isProduction ? [new CleanWebpackPlugin({
|
||||
protectWebpackAssets: false,
|
||||
cleanAfterEveryBuildPatterns: ['*.LICENSE.txt'],
|
||||
})] : []),
|
||||
...(isTauri ? [] : [new FaviconsWebpackPlugin({
|
||||
logo: path.resolve("src/assets/icon.svg"),
|
||||
inject: true,
|
||||
logoMaskable: path.resolve("src/assets/maskable.svg"),
|
||||
@@ -133,7 +140,7 @@ export default async (env, argv) => {
|
||||
},
|
||||
start_url: "/",
|
||||
},
|
||||
}),
|
||||
})]),
|
||||
new HtmlWebpackPlugin({
|
||||
template: "./src/index.html",
|
||||
}),
|
||||
@@ -147,7 +154,7 @@ export default async (env, argv) => {
|
||||
new HtmlInlineScriptPlugin({
|
||||
scriptMatchPattern: ['^bundle.js$'],
|
||||
}),
|
||||
new WorkboxPlugin.GenerateSW({
|
||||
...(isTauri ? [] : [new WorkboxPlugin.GenerateSW({
|
||||
clientsClaim: true,
|
||||
skipWaiting: true,
|
||||
maximumFileSizeToCacheInBytes: 100 * 1024 * 1024,
|
||||
@@ -163,9 +170,9 @@ export default async (env, argv) => {
|
||||
},
|
||||
handler: "CacheFirst",
|
||||
}],
|
||||
})
|
||||
})])
|
||||
] : []),
|
||||
...(!isProduction ? [new ReactRefreshWebpackPlugin()] : [])
|
||||
...(!isProduction ? [new ReactRefreshWebpackPlugin()] : []),
|
||||
],
|
||||
|
||||
module: {
|
||||
|
||||
Reference in New Issue
Block a user