refactor: use swr

splashcat-exporter-v2
spacemeowx2 2024-01-30 18:54:51 +08:00 committed by imspace
parent 32079a50e7
commit bd569c1c80
24 changed files with 531 additions and 3014 deletions

12
gui/.eslintrc.cjs Normal file
View File

@ -0,0 +1,12 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
}

View File

@ -12,28 +12,32 @@
}, },
"dependencies": { "dependencies": {
"@tauri-apps/api": "^1.5.3", "@tauri-apps/api": "^1.5.3",
"classnames": "^2.5.1", "clsx": "^2.1.0",
"daisyui": "^4.6.1", "daisyui": "^4.6.1",
"i18next": "^23.8.1", "i18next": "^23.8.1",
"i18next-browser-languagedetector": "^7.2.0", "i18next-browser-languagedetector": "^7.2.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-error-boundary": "^4.0.12",
"react-i18next": "^14.0.1", "react-i18next": "^14.0.1",
"react-icons": "^5.0.1", "react-icons": "^5.0.1",
"react-router-dom": "^6.21.3", "react-router-dom": "^6.21.3",
"react-use": "^17.5.0" "react-use": "^17.5.0",
"swr": "^2.2.4"
}, },
"devDependencies": { "devDependencies": {
"@tauri-apps/cli": "^1.5.9", "@tauri-apps/cli": "^1.5.9",
"@types/node": "^20.11.10", "@types/node": "^20.11.10",
"@types/react": "^18.0.15", "@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6", "@types/react-dom": "^18.0.6",
"@typescript-eslint/eslint-plugin": "^6.20.0",
"@typescript-eslint/parser": "^6.20.0", "@typescript-eslint/parser": "^6.20.0",
"@typescript-eslint/typescript-estree": "^6.20.0", "@typescript-eslint/typescript-estree": "^6.20.0",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.17", "autoprefixer": "^10.4.17",
"eslint": "^8.56.0", "eslint": "^8.56.0",
"eslint-config-react-app": "^7.0.1", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"i18next-http-backend": "^2.4.2", "i18next-http-backend": "^2.4.2",
"postcss": "^8.4.33", "postcss": "^8.4.33",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
@ -41,22 +45,5 @@
"vite": "^5.0.12", "vite": "^5.0.12",
"vite-plugin-eslint": "^1.8.1", "vite-plugin-eslint": "^1.8.1",
"vite-tsconfig-paths": "^4.3.1" "vite-tsconfig-paths": "^4.3.1"
},
"eslintConfig": {
"extends": "react-app"
},
"pnpm": {
"packageExtensions": {
"eslint-plugin-flowtype": {
"peerDependenciesMeta": {
"@babel/plugin-syntax-flow": {
"optional": true
},
"@babel/plugin-transform-react-jsx": {
"optional": true
}
}
}
}
} }
} }

File diff suppressed because it is too large Load Diff

252
gui/src-tauri/Cargo.lock generated
View File

@ -459,12 +459,12 @@ dependencies = [
[[package]] [[package]]
name = "ctor" name = "ctor"
version = "0.1.26" version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" checksum = "30d2b3721e861707777e3195b0158f950ae6dc4a27e4d02ff9f67e3eb3de199e"
dependencies = [ dependencies = [
"quote", "quote",
"syn 1.0.109", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -494,7 +494,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"strsim", "strsim",
"syn 2.0.23", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -505,7 +505,7 @@ checksum = "29a358ff9f12ec09c3e61fef9b5a9902623a695a46a917b07f269bff1445611a"
dependencies = [ dependencies = [
"darling_core", "darling_core",
"quote", "quote",
"syn 2.0.23", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -1128,9 +1128,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]] [[package]]
name = "html5ever" name = "html5ever"
version = "0.25.2" version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5c13fb08e5d4dfc151ee5e88bae63f7773d61852f3bdc73c9f4b9e1bde03148" checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7"
dependencies = [ dependencies = [
"log", "log",
"mac", "mac",
@ -1260,9 +1260,9 @@ dependencies = [
[[package]] [[package]]
name = "infer" name = "infer"
version = "0.12.0" version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a898e4b7951673fce96614ce5751d13c40fc5674bc2d759288e46c3ab62598b3" checksum = "f551f8c3a39f68f986517db0d1759de85881894fdc7db798bd2a9df9cb04b7fc"
dependencies = [ dependencies = [
"cfb", "cfb",
] ]
@ -1352,9 +1352,9 @@ dependencies = [
[[package]] [[package]]
name = "json-patch" name = "json-patch"
version = "1.0.0" version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f54898088ccb91df1b492cc80029a6fdf1c48ca0db7c6822a8babad69c94658" checksum = "55ff1e1486799e3f64129f8ccad108b38290df9cd7015cd31bed17239f0789d6"
dependencies = [ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
@ -1363,13 +1363,14 @@ dependencies = [
] ]
[[package]] [[package]]
name = "kuchiki" name = "kuchikiki"
version = "0.8.1" version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ea8e9c6e031377cff82ee3001dc8026cdf431ed4e2e6b51f98ab8c73484a358" checksum = "f29e4755b7b995046f510a7520c42b2fed58b77bd94d5a87a8eb43d2fd126da8"
dependencies = [ dependencies = [
"cssparser", "cssparser",
"html5ever", "html5ever",
"indexmap 1.9.2",
"matches", "matches",
"selectors", "selectors",
] ]
@ -1413,12 +1414,9 @@ dependencies = [
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.17" version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
dependencies = [
"cfg-if",
]
[[package]] [[package]]
name = "loom" name = "loom"
@ -1452,13 +1450,13 @@ dependencies = [
[[package]] [[package]]
name = "markup5ever" name = "markup5ever"
version = "0.10.1" version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a24f40fb03852d1cdd84330cddcaf98e9ec08a7b7768e952fad3b4cf048ec8fd" checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016"
dependencies = [ dependencies = [
"log", "log",
"phf 0.8.0", "phf 0.10.1",
"phf_codegen", "phf_codegen 0.10.0",
"string_cache", "string_cache",
"string_cache_codegen", "string_cache_codegen",
"tendril", "tendril",
@ -1779,9 +1777,17 @@ version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259"
dependencies = [ dependencies = [
"phf_macros 0.10.0",
"phf_shared 0.10.0", "phf_shared 0.10.0",
"proc-macro-hack", ]
[[package]]
name = "phf"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
dependencies = [
"phf_macros 0.11.2",
"phf_shared 0.11.2",
] ]
[[package]] [[package]]
@ -1794,6 +1800,16 @@ dependencies = [
"phf_shared 0.8.0", "phf_shared 0.8.0",
] ]
[[package]]
name = "phf_codegen"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd"
dependencies = [
"phf_generator 0.10.0",
"phf_shared 0.10.0",
]
[[package]] [[package]]
name = "phf_generator" name = "phf_generator"
version = "0.8.0" version = "0.8.0"
@ -1814,6 +1830,16 @@ dependencies = [
"rand 0.8.5", "rand 0.8.5",
] ]
[[package]]
name = "phf_generator"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0"
dependencies = [
"phf_shared 0.11.2",
"rand 0.8.5",
]
[[package]] [[package]]
name = "phf_macros" name = "phf_macros"
version = "0.8.0" version = "0.8.0"
@ -1830,16 +1856,15 @@ dependencies = [
[[package]] [[package]]
name = "phf_macros" name = "phf_macros"
version = "0.10.0" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b"
dependencies = [ dependencies = [
"phf_generator 0.10.0", "phf_generator 0.11.2",
"phf_shared 0.10.0", "phf_shared 0.11.2",
"proc-macro-hack",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 1.0.109", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -1861,10 +1886,19 @@ dependencies = [
] ]
[[package]] [[package]]
name = "pin-project-lite" name = "phf_shared"
version = "0.2.9" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b"
dependencies = [
"siphasher",
]
[[package]]
name = "pin-project-lite"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
[[package]] [[package]]
name = "pin-utils" name = "pin-utils"
@ -1958,9 +1992,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.63" version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb" checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -1976,9 +2010,9 @@ dependencies = [
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.29" version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@ -2220,7 +2254,7 @@ dependencies = [
"log", "log",
"matches", "matches",
"phf 0.8.0", "phf 0.8.0",
"phf_codegen", "phf_codegen 0.8.0",
"precomputed-hash", "precomputed-hash",
"servo_arc", "servo_arc",
"smallvec", "smallvec",
@ -2256,29 +2290,29 @@ dependencies = [
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.166" version = "1.0.196"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d01b7404f9d441d3ad40e6a636a7782c377d2abdbe4fa2440e2edcc2f4f10db8" checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.166" version = "1.0.196"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dd83d6dde2b6b2d466e14d9d1acce8816dedee94f735eac6395808b3483c6d6" checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.23", "syn 2.0.48",
] ]
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.99" version = "1.0.113"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46266871c240a00b8f503b877622fe33430b3c7d963bdc0f2adc511e54a1eae3" checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79"
dependencies = [ dependencies = [
"itoa 1.0.5", "itoa 1.0.5",
"ryu", "ryu",
@ -2330,7 +2364,7 @@ dependencies = [
"darling", "darling",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.23", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -2504,9 +2538,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.23" version = "2.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59fb7d6d8281a51045d62b8eb3a7d1ce347b76f312af50cd3dc0af39c87c1737" checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2610,9 +2644,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri" name = "tauri"
version = "1.4.1" version = "1.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fbe522898e35407a8e60dc3870f7579fea2fc262a6a6072eccdd37ae1e1d91e" checksum = "fd27c04b9543776a972c86ccf70660b517ecabbeced9fb58d8b961a13ad129af"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"cocoa", "cocoa",
@ -2659,12 +2693,13 @@ dependencies = [
[[package]] [[package]]
name = "tauri-build" name = "tauri-build"
version = "1.4.0" version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d2edd6a259b5591c8efdeb9d5702cb53515b82a6affebd55c7fd6d3a27b7d1b" checksum = "e9914a4715e0b75d9f387a285c7e26b5bbfeb1249ad9f842675a82481565c532"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"cargo_toml", "cargo_toml",
"dirs-next",
"heck 0.4.1", "heck 0.4.1",
"json-patch", "json-patch",
"semver 1.0.16", "semver 1.0.16",
@ -2672,13 +2707,14 @@ dependencies = [
"serde_json", "serde_json",
"tauri-utils", "tauri-utils",
"tauri-winres", "tauri-winres",
"walkdir",
] ]
[[package]] [[package]]
name = "tauri-codegen" name = "tauri-codegen"
version = "1.4.0" version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54ad2d49fdeab4a08717f5b49a163bdc72efc3b1950b6758245fcde79b645e1a" checksum = "a1554c5857f65dbc377cefb6b97c8ac77b1cb2a90d30d3448114d5d6b48a77fc"
dependencies = [ dependencies = [
"base64 0.21.0", "base64 0.21.0",
"brotli", "brotli",
@ -2702,9 +2738,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-macros" name = "tauri-macros"
version = "1.4.0" version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eb12a2454e747896929338d93b0642144bb51e0dddbb36e579035731f0d76b7" checksum = "277abf361a3a6993ec16bcbb179de0d6518009b851090a01adfea12ac89fa875"
dependencies = [ dependencies = [
"heck 0.4.1", "heck 0.4.1",
"proc-macro2", "proc-macro2",
@ -2716,9 +2752,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-runtime" name = "tauri-runtime"
version = "0.14.0" version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "108683199cb18f96d2d4134187bb789964143c845d2d154848dda209191fd769" checksum = "cf2d0652aa2891ff3e9caa2401405257ea29ab8372cce01f186a5825f1bd0e76"
dependencies = [ dependencies = [
"gtk", "gtk",
"http", "http",
@ -2737,9 +2773,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-runtime-wry" name = "tauri-runtime-wry"
version = "0.14.0" version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7aa256a1407a3a091b5d843eccc1a5042289baf0a43d1179d9f0fcfea37c1b" checksum = "6cae61fbc731f690a4899681c9052dde6d05b159b44563ace8186fc1bfb7d158"
dependencies = [ dependencies = [
"cocoa", "cocoa",
"gtk", "gtk",
@ -2757,9 +2793,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-utils" name = "tauri-utils"
version = "1.4.0" version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03fc02bb6072bb397e1d473c6f76c953cda48b4a2d0cce605df284aa74a12e84" checksum = "ece74810b1d3d44f29f732a7ae09a63183d63949bbdd59c61f8ed2a1b70150db"
dependencies = [ dependencies = [
"brotli", "brotli",
"ctor", "ctor",
@ -2769,9 +2805,10 @@ dependencies = [
"html5ever", "html5ever",
"infer", "infer",
"json-patch", "json-patch",
"kuchiki", "kuchikiki",
"log",
"memchr", "memchr",
"phf 0.10.1", "phf 0.11.2",
"proc-macro2", "proc-macro2",
"quote", "quote",
"semver 1.0.16", "semver 1.0.16",
@ -2781,7 +2818,7 @@ dependencies = [
"thiserror", "thiserror",
"url", "url",
"walkdir", "walkdir",
"windows 0.39.0", "windows-version",
] ]
[[package]] [[package]]
@ -2841,7 +2878,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.23", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -2898,11 +2935,10 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.29.1" version = "1.35.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104"
dependencies = [ dependencies = [
"autocfg",
"backtrace", "backtrace",
"bytes", "bytes",
"num_cpus", "num_cpus",
@ -3177,7 +3213,7 @@ dependencies = [
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.23", "syn 2.0.48",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -3199,7 +3235,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.23", "syn 2.0.48",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -3438,12 +3474,36 @@ dependencies = [
"windows_x86_64_msvc 0.48.0", "windows_x86_64_msvc 0.48.0",
] ]
[[package]]
name = "windows-targets"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd"
dependencies = [
"windows_aarch64_gnullvm 0.52.0",
"windows_aarch64_msvc 0.52.0",
"windows_i686_gnu 0.52.0",
"windows_i686_msvc 0.52.0",
"windows_x86_64_gnu 0.52.0",
"windows_x86_64_gnullvm 0.52.0",
"windows_x86_64_msvc 0.52.0",
]
[[package]] [[package]]
name = "windows-tokens" name = "windows-tokens"
version = "0.39.0" version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f838de2fe15fe6bac988e74b798f26499a8b21a9d97edec321e79b28d1d7f597" checksum = "f838de2fe15fe6bac988e74b798f26499a8b21a9d97edec321e79b28d1d7f597"
[[package]]
name = "windows-version"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75aa004c988e080ad34aff5739c39d0312f4684699d6d71fc8a198d057b8b9b4"
dependencies = [
"windows-targets 0.52.0",
]
[[package]] [[package]]
name = "windows_aarch64_gnullvm" name = "windows_aarch64_gnullvm"
version = "0.42.1" version = "0.42.1"
@ -3456,6 +3516,12 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea"
[[package]] [[package]]
name = "windows_aarch64_msvc" name = "windows_aarch64_msvc"
version = "0.39.0" version = "0.39.0"
@ -3474,6 +3540,12 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef"
[[package]] [[package]]
name = "windows_i686_gnu" name = "windows_i686_gnu"
version = "0.39.0" version = "0.39.0"
@ -3492,6 +3564,12 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
[[package]]
name = "windows_i686_gnu"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313"
[[package]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
version = "0.39.0" version = "0.39.0"
@ -3510,6 +3588,12 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
[[package]]
name = "windows_i686_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a"
[[package]] [[package]]
name = "windows_x86_64_gnu" name = "windows_x86_64_gnu"
version = "0.39.0" version = "0.39.0"
@ -3528,6 +3612,12 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd"
[[package]] [[package]]
name = "windows_x86_64_gnullvm" name = "windows_x86_64_gnullvm"
version = "0.42.1" version = "0.42.1"
@ -3540,6 +3630,12 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e"
[[package]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
version = "0.39.0" version = "0.39.0"
@ -3558,6 +3654,12 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.4.7" version = "0.4.7"
@ -3579,9 +3681,9 @@ dependencies = [
[[package]] [[package]]
name = "wry" name = "wry"
version = "0.24.3" version = "0.24.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33748f35413c8a98d45f7a08832d848c0c5915501803d1faade5a4ebcd258cea" checksum = "6ad85d0e067359e409fcb88903c3eac817c392e5d638258abfb3da5ad8ba6fc4"
dependencies = [ dependencies = [
"base64 0.13.1", "base64 0.13.1",
"block", "block",
@ -3595,7 +3697,7 @@ dependencies = [
"gtk", "gtk",
"html5ever", "html5ever",
"http", "http",
"kuchiki", "kuchikiki",
"libc", "libc",
"log", "log",
"objc", "objc",

View File

@ -10,10 +10,10 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies] [build-dependencies]
tauri-build = { version = "1.4.0", features = [] } tauri-build = { version = "1.5.1", features = [] }
[dependencies] [dependencies]
tauri = { version = "1.4.1", features = [ tauri = { version = "1.5.4", features = [
"fs-all", "fs-all",
"path-all", "path-all",
"process-relaunch", "process-relaunch",
@ -22,9 +22,9 @@ tauri = { version = "1.4.1", features = [
"shell-sidecar", "shell-sidecar",
"window-all", "window-all",
] } ] }
serde = { version = "1.0.164", features = ["derive"] } serde = { version = "1.0.196", features = ["derive"] }
serde_json = "1.0.97" serde_json = "1.0.113"
tokio = { version = "1.28.2", features = ["time"] } tokio = { version = "1.35.1", features = ["time"] }
[features] [features]
# this feature is used for production builds or when `devPath` points to the filesystem # this feature is used for production builds or when `devPath` points to the filesystem

View File

@ -5,17 +5,20 @@ import { Home } from "pages/Home";
import { Settings } from "pages/Settings"; import { Settings } from "pages/Settings";
import { Guide } from 'pages/Guide'; import { Guide } from 'pages/Guide';
import { useShowWindow } from 'hooks/useShowWindow'; import { useShowWindow } from 'hooks/useShowWindow';
import { AppContextProvider } from 'context/app';
function App() { function App() {
useShowWindow(); useShowWindow();
return ( return (
<Routes> <AppContextProvider>
<Route path='/' element={<Layout />}> <Routes>
<Route index element={<Home />} /> <Route path='/' element={<Layout />}>
<Route path='/settings' element={<Settings />} /> <Route index element={<Home />} />
<Route path='/guide' element={<Guide />} /> <Route path='/settings' element={<Settings />} />
</Route> <Route path='/guide' element={<Guide />} />
</Routes> </Route>
</Routes>
</AppContextProvider>
); );
} }

View File

@ -1,9 +1,10 @@
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { AiOutlineWarning } from 'react-icons/ai' import { AiOutlineWarning } from 'react-icons/ai'
import { FallbackProps } from 'react-error-boundary'
type ErrorContentProps = { type ErrorContentProps = {
error: any error: unknown
retry?: () => void retry?: () => void
} }
@ -24,3 +25,8 @@ export const ErrorContent: React.FC<ErrorContentProps> = ({ error, retry }) => {
</span> </span>
</div> </div>
} }
export const FallbackComponent: React.FC<FallbackProps> = ({ error, resetErrorBoundary }) => {
console.error('FallbackComponent', error)
return <ErrorContent error={error} retry={resetErrorBoundary} />
}

View File

@ -1,10 +1,8 @@
import { invoke } from '@tauri-apps/api'; import { invoke } from '@tauri-apps/api';
import classNames from 'classnames'; import clsx from 'clsx';
import { usePromise } from 'hooks/usePromise'; import { useService, useServiceMutation } from 'services/useService';
import React, { useState } from 'react' import React, { useState } from 'react'
import { getConfig, getProfile, setProfile } from 'services/config';
import { ensureTokenValid } from 'services/s3si'; import { ensureTokenValid } from 'services/s3si';
import { composeLoadable } from 'utils/composeLoadable';
import { ErrorContent } from './ErrorContent'; import { ErrorContent } from './ErrorContent';
type OpenSplatnetProps = { type OpenSplatnetProps = {
@ -12,30 +10,29 @@ type OpenSplatnetProps = {
} }
export const OpenSplatnet: React.FC<OpenSplatnetProps> = ({ children }) => { export const OpenSplatnet: React.FC<OpenSplatnetProps> = ({ children }) => {
let { loading, error, retry, result } = composeLoadable({ const profileResult = useService('profile', 0)
config: usePromise(getConfig), const { trigger: setProfile } = useServiceMutation('profile', 0)
profile: usePromise(() => getProfile(0)),
});
const [doing, setDoing] = useState(false); const [doing, setDoing] = useState(false);
const [err, setError] = useState<any>(); const [err, setError] = useState<unknown>();
const onClick = async () => { const onClick = async () => {
setDoing(true); setDoing(true);
try { try {
if (!result) { if (!profileResult.data) {
return; return;
} }
const state = result.profile.state; const state = profileResult.data.state;
const newState = await ensureTokenValid(state); const newState = await ensureTokenValid(state);
await setProfile(0, { await setProfile({
...result.profile, ...profileResult.data,
state: newState, state: newState,
}); });
retry?.();
const gtoken = newState.loginState?.gToken; const gtoken = newState.loginState?.gToken;
await invoke('open_splatnet', { await invoke('open_splatnet', {
gtoken, gtoken,
lang: result.profile.state.userLang, lang: profileResult.data.state.userLang,
}); });
} catch (e) { } catch (e) {
setError(e); setError(e);
@ -45,18 +42,18 @@ export const OpenSplatnet: React.FC<OpenSplatnetProps> = ({ children }) => {
}; };
if (error || err) { if (err) {
return <> return <>
<ErrorContent error={error || err} retry={retry} /> <ErrorContent error={err} />
</> </>
} }
const btnLoading = loading || doing; const btnLoading = profileResult.isLoading || doing;
return <> return <>
<button <button
type='button' type='button'
className={classNames('btn w-full', { className={clsx('btn w-full', {
'btn-disabled': !result?.profile.state.loginState?.sessionToken, 'btn-disabled': !profileResult.data?.state?.loginState?.sessionToken,
})} })}
onClick={onClick} onClick={onClick}
disabled={btnLoading} disabled={btnLoading}

View File

@ -1,72 +1,39 @@
import classNames from 'classnames'; import clsx from 'clsx';
import { usePromise } from 'hooks/usePromise';
import React, { useEffect, useRef, useState } from 'react' import React, { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { canExport, getProfile, setProfile } from 'services/config'; import { useLog } from 'services/s3si';
import { addLog, run, useLog } from 'services/s3si';
import { Checkbox } from './Checkbox'; import { Checkbox } from './Checkbox';
import { Loading } from './Loading'; import { Loading } from './Loading';
import { useService } from 'services/useService';
import { useAppContext } from 'context/app'
type RunPanelProps = { type RunPanelProps = Record<string, never>
}
export const RunPanel: React.FC<RunPanelProps> = () => { export const RunPanel: React.FC<RunPanelProps> = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { result } = usePromise(() => getProfile(0)); const { data: result } = useService('profile', 0)
const [exportBattle, setExportBattle] = useState(true); const [exportBattle, setExportBattle] = useState(true);
const [exportCoop, setExportCoop] = useState(true); const [exportCoop, setExportCoop] = useState(true);
const [loading, setLoading] = useState(false); const { exports } = useAppContext()
const disabled = !exports
const isExporting = exports?.isExporting ?? false
if (!result) { if (!result) {
return <Loading /> return <Loading />
} }
const onClick = async () => {
setLoading(true);
try {
addLog({
level: 'log',
msg: ['Export started at', new Date().toLocaleString()],
})
const { state } = result;
const newState = await run(state, {
exporter: "stat.ink",
monitor: false,
withSummary: false,
skipMode: exportBattle === false ? 'vs' : exportCoop === false ? 'coop' : undefined,
});
await setProfile(0, {
...result,
state: newState,
})
} catch (e) {
console.error(e)
addLog({
level: 'error',
msg: [e],
})
} finally {
addLog({
level: 'log',
msg: ['Export ended at', new Date().toLocaleString()],
})
setLoading(false);
}
}
const disabled = !canExport(result);
return <> return <>
<div className="tooltip" data-tip={disabled ? t('请先在设置中完成Nintendo Account登录和stat.ink的API密钥') : undefined}> <div className="tooltip" data-tip={disabled ? t('请先在设置中完成Nintendo Account登录和stat.ink的API密钥') : undefined}>
<Checkbox disabled={disabled || loading} value={exportBattle} onChange={setExportBattle}>{t('导出对战数据')}</Checkbox> <Checkbox disabled={disabled || isExporting} value={exportBattle} onChange={setExportBattle}>{t('导出对战数据')}</Checkbox>
<Checkbox disabled={disabled || loading} value={exportCoop} onChange={setExportCoop}>{t('导出打工数据')}</Checkbox> <Checkbox disabled={disabled || isExporting} value={exportCoop} onChange={setExportCoop}>{t('导出打工数据')}</Checkbox>
<button <button
type='button' type='button'
onClick={onClick} onClick={() => exports?.trigger({ exportBattle, exportCoop })}
className={classNames('btn btn-primary w-full', { className={clsx('btn btn-primary w-full', {
'btn-disabled': disabled || (!exportBattle && !exportCoop), 'btn-disabled': disabled || (!exportBattle && !exportCoop),
})} })}
disabled={loading} disabled={isExporting}
>{loading ? <span className='loading' /> : t('导出')}</button> >{isExporting ? <span className='loading' /> : t('导出')}</button>
</div> </div>
</> </>
} }

81
gui/src/context/app.tsx Normal file
View File

@ -0,0 +1,81 @@
import { ReactNode, createContext, useContext } from 'react'
import { canExport } from 'services/config';
import { addLog, run } from 'services/s3si';
import { useService, useServiceMutation } from 'services/useService';
import useSWRMutation from 'swr/mutation';
export type ExportArgs = {
exportBattle: boolean,
exportCoop: boolean,
}
const APP_CONTEXT = createContext<{
exports?: {
isExporting: boolean
trigger: (args: ExportArgs) => Promise<void>
}
}>({})
export const useAppContext = () => {
return useContext(APP_CONTEXT)
}
export const AppContextProvider: React.FC<{ children?: ReactNode }> = ({ children }) => {
const { data: result } = useService('profile', 0)
const { trigger: setProfile } = useServiceMutation('profile', 0)
const { trigger: doExport, isMutating } = useSWRMutation<
unknown,
Error,
string,
{
exportBattle: boolean,
exportCoop: boolean,
}
>('export', async (_, { arg: {
exportBattle, exportCoop,
} }) => {
try {
if (!result) {
return
}
addLog({
level: 'log',
msg: ['Export started at', new Date().toLocaleString()],
})
const { state } = result;
const newState = await run(state, {
exporter: "stat.ink",
monitor: false,
withSummary: false,
skipMode: exportBattle === false ? 'vs' : exportCoop === false ? 'coop' : undefined,
});
await setProfile({
...result,
state: newState,
})
} catch (e) {
console.error(e)
addLog({
level: 'error',
msg: [e],
})
} finally {
addLog({
level: 'log',
msg: ['Export ended at', new Date().toLocaleString()],
})
}
})
return <APP_CONTEXT.Provider value={{
exports: result && canExport(result) ? {
isExporting: isMutating,
trigger: async (args: ExportArgs) => {
await doExport(args)
},
} : undefined,
}}>
{children}
</APP_CONTEXT.Provider>
}

View File

@ -1,84 +0,0 @@
import { useState } from "react";
/**
* A hook that returns a promise and its state.
*
* @param factory A function that returns a promise.
* @returns An object containing the promise's state and result.
* @example
* const { loading, result, error } = usePromise(() => fetch('https://example.com')
* .then(response => response.text())
* );
* if (loading) {
* return <p>Loading...</p>;
* }
* if (error) {
* return <p>Error: {error.message}</p>;
* }
* return <p>Result: {result}</p>;
*/
export function usePromise<T>(factory: () => Promise<T>) {
const init = () => {
const promise = factory();
if (!promise || typeof promise.then !== "function") {
throw new Error("The factory function must return a promise.");
}
return promise
.then(r => {
setResult(r);
setLoading(false);
return r;
})
.catch(e => {
setError(e);
setLoading(false);
throw e;
});
}
const [loading, setLoading] = useState(true);
const [result, setResult] = useState<T | undefined>(undefined);
const [error, setError] = useState<any | undefined>(undefined);
const [promise, setPromise] = useState(init);
const retry = () => {
setLoading(true);
setResult(undefined);
setError(undefined);
setPromise(init);
}
return { loading, result, error, promise, retry };
}
/**
* A hook that returns a promise and its state.
*/
export function usePromiseLazy<T, Args extends any[]>(factory: (...args: Args) => Promise<T>) {
const init = (promise: Promise<T>) => {
if (!promise || typeof promise.then !== "function") {
throw new Error("The factory function must return a promise.");
}
return promise
.then(r => {
setResult(r);
setLoading(false);
return r;
})
.catch(e => {
setError(e);
setLoading(false);
throw e;
});
}
const [loading, setLoading] = useState(false);
const [result, setResult] = useState<T | undefined>(undefined);
const [error, setError] = useState<any | undefined>(undefined);
const [promise, setPromise] = useState<Promise<T> | undefined>(undefined);
const execute = (...args: Args) => {
setLoading(true);
setResult(undefined);
setError(undefined);
setPromise(init(factory(...args)));
}
return [execute, { loading, result, error, promise }] as const;
}

View File

@ -1,3 +1,5 @@
/* eslint-disable @typescript-eslint/ban-types */
/* eslint-disable @typescript-eslint/no-explicit-any */
type Maybe<T> = T | null | undefined; type Maybe<T> = T | null | undefined;
type KeyOf<T extends Record<string, any>, K = keyof T> = K extends string ? (T[K] extends Function ? never : K) : never; type KeyOf<T extends Record<string, any>, K = keyof T> = K extends string ? (T[K] extends Function ? never : K) : never;
type DotField<T extends Maybe<Record<string, any>>, K = KeyOf<NonNullable<T>>> = K extends string type DotField<T extends Maybe<Record<string, any>>, K = KeyOf<NonNullable<T>>> = K extends string

View File

@ -21,7 +21,7 @@ export class JSONRPCClient<S extends Service> {
protected transport: Transport; protected transport: Transport;
protected requestMap: Map< protected requestMap: Map<
ID, ID,
(result: RPCResult<any, ResponseError>) => void (result: RPCResult<unknown, ResponseError>) => void
> = new Map(); > = new Map();
protected fatal: unknown = undefined; protected fatal: unknown = undefined;
protected task: Promise<void>; protected task: Promise<void>;
@ -55,6 +55,7 @@ export class JSONRPCClient<S extends Service> {
// receive response from server // receive response from server
protected async run() { protected async run() {
try { try {
// eslint-disable-next-line no-constant-condition
while (true) { while (true) {
const data = await this.transport.recv(); const data = await this.transport.recv();
if (data === undefined) { if (data === undefined) {
@ -111,7 +112,7 @@ export class JSONRPCClient<S extends Service> {
if (result.error) { if (result.error) {
rej(new JSONRPCError(result.error)); rej(new JSONRPCError(result.error));
} else { } else {
res(result.result); res(result.result as R);
} }
}); });
}); });
@ -120,6 +121,7 @@ export class JSONRPCClient<S extends Service> {
getProxy(): S { getProxy(): S {
const proxy = new Proxy({}, { const proxy = new Proxy({}, {
get: (_, method: string) => { get: (_, method: string) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (...params: unknown[]) => this.call(method, ...params as any); return (...params: unknown[]) => this.call(method, ...params as any);
}, },
}); });

View File

@ -1,4 +1,4 @@
import classNames from 'classnames'; import clsx from 'clsx';
import { Header } from 'components/Header'; import { Header } from 'components/Header';
import { useState } from 'react'; import { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -32,14 +32,14 @@ const Steps: React.FC<{ steps: Step[], className?: string }> = ({ className, ste
<button <button
type='button' type='button'
onClick={() => setStep(s => s - 1)} onClick={() => setStep(s => s - 1)}
className={classNames('btn', { className={clsx('btn', {
'btn-disabled': !hasPrev || !state.prev, 'btn-disabled': !hasPrev || !state.prev,
})} })}
>{t('上一步')}</button> >{t('上一步')}</button>
<button <button
type='button' type='button'
onClick={() => setStep(s => s + 1)} onClick={() => setStep(s => s + 1)}
className={classNames('btn', { className={clsx('btn', {
'btn-disabled': !hasNext || !state.next, 'btn-disabled': !hasNext || !state.next,
})} })}
>{t('下一步')}</button> >{t('下一步')}</button>

View File

@ -1,25 +1,32 @@
import { OpenSplatnet } from 'components/OpenSplatnet'; import { OpenSplatnet } from 'components/OpenSplatnet';
import { LogPanel, RunPanel } from 'components/RunPanel'; import { LogPanel, RunPanel } from 'components/RunPanel';
import { STAT_INK } from 'constant'; import { STAT_INK } from 'constant';
import React from 'react' import React, { Suspense } from 'react'
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Link } from "react-router-dom"; import { Link } from 'react-router-dom';
import { ErrorBoundary } from 'react-error-boundary';
import { FallbackComponent } from 'components/ErrorContent';
import { Loading } from 'components/Loading';
export const Home: React.FC = () => { export const Home: React.FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
return <div className='flex p-2 w-full h-full gap-2'> return <ErrorBoundary FallbackComponent={FallbackComponent}>
<div className='max-w-full h-full md:max-w-sm flex-auto'> <Suspense fallback={<Loading />}>
<div className='flex flex-col gap-2 h-full'> <div className='flex p-2 w-full h-full gap-2'>
<LogPanel className='sm:hidden flex-auto' /> <div className='max-w-full h-full md:max-w-sm flex-auto'>
<RunPanel /> <div className='flex flex-col gap-2 h-full'>
<Link to='/settings' className='btn'>{t('设置')}</Link> <LogPanel className='sm:hidden flex-auto' />
<div className='flex gap-2 flex-auto-all'> <RunPanel />
<OpenSplatnet>{t('打开鱿鱼圈3')}</OpenSplatnet> <Link to='/settings' className='btn'>{t('设置')}</Link>
<a className='btn w-full' href={STAT_INK} target='_blank' rel='noreferrer'>{t('前往 stat.ink')}</a> <div className='flex gap-2 flex-auto-all'>
<OpenSplatnet>{t('打开鱿鱼圈3')}</OpenSplatnet>
<a className='btn w-full' href={STAT_INK} target='_blank' rel='noreferrer'>{t('前往 stat.ink')}</a>
</div>
</div>
</div> </div>
<LogPanel className='hidden sm:block flex-1' />
</div> </div>
</div> </Suspense>
<LogPanel className='hidden sm:block flex-1' /> </ErrorBoundary>
</div>
} }

View File

@ -1,16 +1,17 @@
import { ErrorContent } from 'components/ErrorContent'; import { ErrorContent, FallbackComponent } from 'components/ErrorContent';
import { Loading } from 'components/Loading'; import { Loading } from 'components/Loading';
import { usePromise, usePromiseLazy } from 'hooks/usePromise'; import React, { Suspense, useState } from 'react'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Config, getConfig, getProfile, Profile, setConfig, setProfile } from 'services/config'; import { Config, Profile } from 'services/config';
import { composeLoadable } from 'utils/composeLoadable'; import clsx from 'clsx';
import classNames from 'classnames';
import { useLogin } from 'services/s3si'; import { useLogin } from 'services/s3si';
import { STAT_INK } from 'constant'; import { STAT_INK } from 'constant';
import { Header } from 'components/Header'; import { Header } from 'components/Header';
import { useSubField } from 'hooks/useSubField'; import { useSubField } from 'hooks/useSubField';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import useSWRMutation from 'swr/mutation'
import { useService, useServiceMutation } from 'services/useService';
import { ErrorBoundary } from 'react-error-boundary';
const STAT_INK_KEY_LENGTH = 43; const STAT_INK_KEY_LENGTH = 43;
@ -57,6 +58,8 @@ const Form: React.FC<{
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const [value, setValue] = useState(oldValue); const [value, setValue] = useState(oldValue);
const { subField } = useSubField({ value, onChange: setValue }); const { subField } = useSubField({ value, onChange: setValue });
const { trigger: setProfile } = useServiceMutation('profile', 0)
const { trigger: setConfig } = useServiceMutation('config')
const changed = JSON.stringify(value) !== JSON.stringify(oldValue); const changed = JSON.stringify(value) !== JSON.stringify(oldValue);
@ -64,12 +67,12 @@ const Form: React.FC<{
const statInkApiKey = subField('profile.state.statInkApiKey') const statInkApiKey = subField('profile.state.statInkApiKey')
const splatnet3Lang = subField('profile.state.userLang') const splatnet3Lang = subField('profile.state.userLang')
const [onSave, { loading, error }] = usePromiseLazy(async () => { const { trigger: onSave, isMutating: loading, error } = useSWRMutation('saveSettings', async () => {
await setProfile(0, value.profile); await setProfile(value.profile);
await setConfig(value.config); await setConfig(value.config);
onSaved?.(); onSaved?.();
}) })
const [onLogin, loginState] = usePromiseLazy(async () => { const loginState = useSWRMutation('login', async () => {
const result = await login(); const result = await login();
if (!result) { if (!result) {
return; return;
@ -86,11 +89,11 @@ const Form: React.FC<{
<span className="label-text">{t('Nintendo Account 会话令牌')}</span> <span className="label-text">{t('Nintendo Account 会话令牌')}</span>
<span className="label-text-alt"><button <span className="label-text-alt"><button
type='button' type='button'
className={classNames('link', { className={clsx('link', {
loading: loginState.loading, loading: loginState.isMutating,
})} })}
onClick={onLogin} onClick={() => loginState.trigger()}
disabled={loginState.loading} disabled={loginState.isMutating}
>{t('网页登录')}</button></span> >{t('网页登录')}</button></span>
</label> </label>
<input <input
@ -114,7 +117,7 @@ const Form: React.FC<{
</label> </label>
<div className='tooltip' data-tip={statInkKeyError ? t('密钥的长度应该为{{length}}, 请检查', { length: STAT_INK_KEY_LENGTH }) : null}> <div className='tooltip' data-tip={statInkKeyError ? t('密钥的长度应该为{{length}}, 请检查', { length: STAT_INK_KEY_LENGTH }) : null}>
<input <input
className={classNames("input input-bordered w-full", { className={clsx("input input-bordered w-full", {
'input-error': statInkKeyError, 'input-error': statInkKeyError,
})} })}
type="text" type="text"
@ -149,19 +152,19 @@ const Form: React.FC<{
</div> </div>
<ErrorContent error={error} /> <ErrorContent error={error} />
<div className='flex gap-4 max-w-md justify-between flex-auto-all'> <div className='flex gap-4 max-w-md justify-between flex-auto-all'>
<div className="tooltip" data-tip={changed ? undefined : t('没有更改')}> <div className='tooltip' data-tip={changed ? undefined : t('没有更改')}>
<button <button
type='button' type='button'
className={classNames('btn btn-primary w-full', { className={clsx('btn btn-primary w-full', {
loading, loading,
})} })}
onClick={onSave} onClick={() => onSave()}
disabled={!changed || statInkKeyError} disabled={!changed || statInkKeyError}
>{t('保存')}</button> >{t('保存')}</button>
</div> </div>
<button <button
type='button' type='button'
className={classNames('btn', { className={clsx('btn', {
loading, loading,
})} })}
onClick={() => setValue(oldValue)} onClick={() => setValue(oldValue)}
@ -170,26 +173,28 @@ const Form: React.FC<{
</> </>
} }
export const Settings: React.FC = () => { const SettingsLoader: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
let { loading, error, retry, result } = composeLoadable({ const { data: config } = useService('config')
config: usePromise(getConfig), const { data: profile } = useService('profile', 0)
profile: usePromise(() => getProfile(0)),
});
if (loading) { if (!config || !profile) {
return <Page> return <>
<div className='h-full flex items-center justify-center'><Loading /></div> Error
</Page> </>
} }
if (error) { return <>
return <Page> <Form oldValue={{ config, profile }} onSaved={() => navigate(-1)} />
<ErrorContent error={error} retry={retry} /> </>
</Page> }
}
export const Settings: React.FC = () => {
return <Page> return <Page>
{result && <Form oldValue={result} onSaved={() => navigate(-1)} />} <ErrorBoundary FallbackComponent={FallbackComponent}>
<Suspense fallback={<div className='h-full flex items-center justify-center'><Loading /></div>}>
<SettingsLoader />
</Suspense>
</ErrorBoundary>
</Page> </Page>
} }

View File

@ -1,6 +1,6 @@
import { fs } from "@tauri-apps/api" import { fs } from "@tauri-apps/api"
import { appConfigDir, join } from '@tauri-apps/api/path' import { appConfigDir, join } from '@tauri-apps/api/path'
import { State } from '../../../src/state'; import type { State } from '../../../src/state';
const configFile = appConfigDir().then(c => join(c, 'config.json')); const configFile = appConfigDir().then(c => join(c, 'config.json'));
const profileDir = appConfigDir().then(c => join(c, 'profile')); const profileDir = appConfigDir().then(c => join(c, 'profile'));
@ -9,8 +9,7 @@ export type Profile = {
state: State, state: State,
} }
export type Config = { export type Config = Record<string, never>
}
// TODO: import from state.ts. // TODO: import from state.ts.
const DEFAULT_STATE: State = { const DEFAULT_STATE: State = {

View File

@ -9,6 +9,7 @@ const client = new JSONRPCClient<S3SIService>({
const LOG_SUB = new Set<(logs: Log[]) => void>(); const LOG_SUB = new Set<(logs: Log[]) => void>();
async function getLogs() { async function getLogs() {
// eslint-disable-next-line no-constant-condition
while (true) { while (true) {
const r = await client.getLogs() const r = await client.getLogs()
@ -57,7 +58,7 @@ export const useLog = () => {
return useContext(LOG_CONTEXT); return useContext(LOG_CONTEXT);
} }
function renderMsg(i: any) { function renderMsg(i: unknown) {
if (i instanceof Error) { if (i instanceof Error) {
return i.message return i.message
} }

View File

@ -0,0 +1,37 @@
import useSWR, { Key, SWRResponse } from 'swr'
import useSWRMutation, { SWRMutationResponse } from 'swr/mutation'
import { getConfig, getProfile, setConfig, setProfile } from './config'
const SERVICES = {
profile: {
fetcher: getProfile,
updater: setProfile,
},
config: {
fetcher: getConfig,
updater: setConfig,
},
} as const
export type Services = keyof typeof SERVICES
export const useService = <S extends Services>(service: S, ...args: Parameters<(typeof SERVICES)[S]['fetcher']>): SWRResponse<
Awaited<ReturnType<(typeof SERVICES)[S]['fetcher']>>
> => {
// @ts-expect-error TypeScript can not infer type here
return useSWR(['service', service, ...args], () => SERVICES[service].fetcher(...args), { suspense: true })
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type RemoveLastParamters<T extends (...args: any) => any> = T extends (...args: [...infer P, any]) => any ? P : never;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type LastParamter<T extends (...args: any) => any> = T extends (...args: [...infer _, infer P]) => any ? P : never;
export const useServiceMutation = <S extends Services>(service: S, ...args: RemoveLastParamters<(typeof SERVICES)[S]['updater']>): SWRMutationResponse<
Awaited<ReturnType<(typeof SERVICES)[S]['updater']>>,
Error,
Key,
LastParamter<(typeof SERVICES)[S]['updater']>
> => {
// @ts-expect-error TypeScript can not infer type here
return useSWRMutation(['service', service, ...args], (_, { arg }) => SERVICES[service].updater(...args, arg))
}

View File

@ -1,19 +0,0 @@
export type Loadable<T> = {
loading: boolean;
result?: T;
error?: any;
retry?: () => void;
}
export function composeLoadable<T extends Record<string, Loadable<any>>>(map: T): Loadable<{
[P in keyof T]: T[P] extends Loadable<infer R> ? R : never
}> {
const values = Object.values(map)
const loading = values.some(v => v.loading);
const error = values.find(v => v.error)?.error;
const result = loading || error ? undefined : Object.fromEntries(Object.entries(map).map(([k, v]) => [k, v.result])) as any;
const retry = values.some(i => !!i.retry) ? () => Object.values(map).forEach(v => v.retry?.()) : undefined;
return { loading, result, error, retry };
}

View File

@ -1 +0,0 @@
export { IPC } from "./stdio.ts";

View File

@ -1,40 +0,0 @@
/// <reference lib="deno.ns" />
import { io, writeAll } from "../../deps.ts";
import type { ExtractType } from "./types.ts";
export class IPC<T extends { type: string }> {
lines: AsyncIterableIterator<string>;
writer: Deno.Writer;
constructor({ reader, writer }: {
reader: Deno.Reader;
writer: Deno.Writer;
}) {
this.lines = io.readLines(reader);
this.writer = writer;
}
async recvType<K extends T["type"]>(
type: K,
): Promise<ExtractType<T, K>> {
const data = await this.recv();
if (data.type !== type) {
throw new Error(`Unexpected type: ${data.type}`);
}
return data as ExtractType<T, K>;
}
async recv(): Promise<T> {
const result = await this.lines.next();
if (!result.done) {
return JSON.parse(result.value);
}
throw new Error("EOF");
}
async send(data: T) {
await writeAll(
this.writer,
new TextEncoder().encode(JSON.stringify(data) + "\n"),
);
}
}

View File

@ -1,10 +0,0 @@
export type Command = {
type: "hello";
data: string;
};
export type ExtractType<T extends { type: string }, K extends T["type"]> =
Extract<
T,
{ type: K }
>;

View File

@ -3,13 +3,13 @@ import { Transport } from "./types.ts";
export class DenoIO implements Transport { export class DenoIO implements Transport {
lines: AsyncIterableIterator<string>; lines: AsyncIterableIterator<string>;
writer: WritableStream<Uint8Array>; writer: WritableStreamDefaultWriter<Uint8Array>;
constructor({ reader, writer }: { constructor({ reader, writer }: {
reader: ReadableStream<Uint8Array>; reader: ReadableStream<Uint8Array>;
writer: WritableStream<Uint8Array>; writer: WritableStream<Uint8Array>;
}) { }) {
this.lines = readLines(reader); this.lines = readLines(reader);
this.writer = writer; this.writer = writer.getWriter();
} }
async recv(): Promise<string | undefined> { async recv(): Promise<string | undefined> {
const result = await this.lines.next(); const result = await this.lines.next();
@ -21,13 +21,8 @@ export class DenoIO implements Transport {
return undefined; return undefined;
} }
async send(data: string) { async send(data: string) {
const writer = this.writer.getWriter(); await this.writer.ready;
try { await this.writer.write(new TextEncoder().encode(data + "\n"));
await writer.ready;
await writer.write(new TextEncoder().encode(data + "\n"));
} finally {
writer.releaseLock();
}
} }
async close() { async close() {
await this.writer.close(); await this.writer.close();