This commit is contained in:
2026-06-20 22:15:54 +08:00
parent 2fa9d7041b
commit ce28c24066
49 changed files with 2224 additions and 325 deletions
+1
View File
@@ -0,0 +1 @@
dW50cnVzdGVkIGNvbW1lbnQ6IHJzaWduIGVuY3J5cHRlZCBzZWNyZXQga2V5ClJXUlRZMEl5b29vWWcwSnZkbzlYQWNNQzJuVEZ2dlJJd3A0MUhTcEpCYmZubFdlVmVvY0FBQkFBQUFBQUFBQUFBQUlBQUFBQXEveUNRTnFXUEE5Q3M5Und2YTFlQWdRUVh1L3hudGJaTnZlRlRHM3FwZkVHQXBhNlpQWmF3bkcwTGJ5N1MwbEFIRFVXM3lCc3QxdUVOeDJqeVdVK29lZkllRmxWZ2FxcW1CK2cvYTFwMUptQ0hMZEpjZXhpdU1Ndm9CVWcrTk91aXBiSHB4MVBKdU09Cg==
+1
View File
@@ -0,0 +1 @@
dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEYxRDZBQjc3QUQxNDBGNDUKUldSRkR4U3RkNnZXOGNtdHE4TS80d3p4UHlhaEVVMDFUTFRrQmhySnFVNWdZWHNXOWUraFFhRTcK
+6
View File
@@ -0,0 +1,6 @@
@echo off
call "C:\Program Files\Microsoft Visual Studio\18\Community\VC\Auxiliary\Build\vcvarsall.bat" arm64
set PATH=C:\Users\OseasyVM\scoop\apps\llvm\current\bin;%PATH%
set CC=clang
cd /d "%~dp0"
pnpm dev:t
+7 -1
View File
@@ -12,15 +12,20 @@
"build:beta": "node scripts/switch-icon.js beta && tsc && vite build --mode beta",
"preview": "vite preview",
"tauri": "tauri",
"dev:t": "node scripts/switch-icon.js dev && tauri dev"
"dev:t": "node scripts/switch-icon.js dev && tauri dev",
"dev:vs": "./dev-vs.cmd"
},
"dependencies": {
"@base-ui/react": "^1.6.0",
"@fontsource-variable/geist": "^5.2.9",
"@fontsource-variable/inter": "^5.2.8",
"@react-three/fiber": "^9.6.1",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-opener": "^2",
"@tauri-apps/plugin-process": "^2.3.1",
"@tauri-apps/plugin-shell": "^2.3.5",
"@tauri-apps/plugin-updater": "^2.10.1",
"@types/three": "^0.184.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^1.21.0",
@@ -28,6 +33,7 @@
"react-dom": "^19.1.0",
"shadcn": "^4.11.0",
"tailwind-merge": "^3.6.0",
"three": "^0.184.0",
"tw-animate-css": "^1.4.0",
"zustand": "^5.0.14"
},
+184
View File
@@ -17,15 +17,27 @@ importers:
'@fontsource-variable/inter':
specifier: ^5.2.8
version: 5.2.8
'@react-three/fiber':
specifier: ^9.6.1
version: 9.6.1(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(three@0.184.0)
'@tauri-apps/api':
specifier: ^2
version: 2.11.1
'@tauri-apps/plugin-opener':
specifier: ^2
version: 2.5.4
'@tauri-apps/plugin-process':
specifier: ^2.3.1
version: 2.3.1
'@tauri-apps/plugin-shell':
specifier: ^2.3.5
version: 2.3.5
'@tauri-apps/plugin-updater':
specifier: ^2.10.1
version: 2.10.1
'@types/three':
specifier: ^0.184.1
version: 0.184.1
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
@@ -47,6 +59,9 @@ importers:
tailwind-merge:
specifier: ^3.6.0
version: 3.6.0
three:
specifier: ^0.184.0
version: 0.184.0
tw-animate-css:
specifier: ^1.4.0
version: 1.4.0
@@ -253,6 +268,9 @@ packages:
'@types/react':
optional: true
'@dimforge/rapier3d-compat@0.12.0':
resolution: {integrity: sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==}
'@dotenvx/dotenvx@1.74.2':
resolution: {integrity: sha512-UucMF9L95sVr/feRojWB/NIc5R+CzoiiXq7rd7sWppYA7j6CIH5bu5bQCuoJ0HJxQARR0Ecg4bYot3ilo100lw==}
hasBin: true
@@ -496,6 +514,31 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
'@react-three/fiber@9.6.1':
resolution: {integrity: sha512-zF0rsKcVYpcJwbFEnv2HkHX9cvOEgsfQo/X8lwmR2dn13S4qEQJXir9fxf5js2LQFoXqxOY7MDkOkYx2uZ4gSg==}
peerDependencies:
expo: '>=43.0'
expo-asset: '>=8.4'
expo-file-system: '>=11.0'
expo-gl: '>=11.0'
react: '>=19 <19.3'
react-dom: '>=19 <19.3'
react-native: '>=0.78'
three: '>=0.156'
peerDependenciesMeta:
expo:
optional: true
expo-asset:
optional: true
expo-file-system:
optional: true
expo-gl:
optional: true
react-dom:
optional: true
react-native:
optional: true
'@rolldown/pluginutils@1.0.0-beta.27':
resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==}
@@ -820,12 +863,21 @@ packages:
'@tauri-apps/plugin-opener@2.5.4':
resolution: {integrity: sha512-1HnPkb+AmgO29HBazm4uPLKB+r7zzcTBW1d0fyYp1uP+jwtpoiNDGKMMzz58SFp49nOIrxdE3aUJtT57lfO9CQ==}
'@tauri-apps/plugin-process@2.3.1':
resolution: {integrity: sha512-nCa4fGVaDL/B9ai03VyPOjfAHRHSBz5v6F/ObsB73r/dA3MHHhZtldaDMIc0V/pnUw9ehzr2iEG+XkSEyC0JJA==}
'@tauri-apps/plugin-shell@2.3.5':
resolution: {integrity: sha512-jewtULhiQ7lI7+owCKAjc8tYLJr92U16bPOeAa472LHJdgaibLP83NcfAF2e+wkEcA53FxKQAZ7byDzs2eeizg==}
'@tauri-apps/plugin-updater@2.10.1':
resolution: {integrity: sha512-NFYMg+tWOZPJdzE/PpFj2qfqwAWwNS3kXrb1tm1gnBJ9mYzZ4WDRrwy8udzWoAnfGCHLuePNLY1WVCNHnh3eRA==}
'@ts-morph/common@0.27.0':
resolution: {integrity: sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==}
'@tweenjs/tween.js@23.1.3':
resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==}
'@types/babel__core@7.20.5':
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
@@ -846,12 +898,26 @@ packages:
peerDependencies:
'@types/react': ^19.2.0
'@types/react-reconciler@0.28.9':
resolution: {integrity: sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==}
peerDependencies:
'@types/react': '*'
'@types/react@19.2.17':
resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==}
'@types/stats.js@0.17.4':
resolution: {integrity: sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==}
'@types/three@0.184.1':
resolution: {integrity: sha512-6q4VdiqVsrTRqmk62/BnlcAvIrnDM0zf2ZDVKI5kZiniWrSaOHaQzmbp+BNzoggc/8tgW412pL//wZIxu2PPTA==}
'@types/validate-npm-package-name@4.0.2':
resolution: {integrity: sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==}
'@types/webxr@0.5.24':
resolution: {integrity: sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==}
'@vitejs/plugin-react@4.7.0':
resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==}
engines: {node: ^14.18.0 || >=16.0.0}
@@ -912,6 +978,9 @@ packages:
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
engines: {node: 18 || 20 || >=22}
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
baseline-browser-mapping@2.10.38:
resolution: {integrity: sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==}
engines: {node: '>=6.0.0'}
@@ -934,6 +1003,9 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
buffer@6.0.3:
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
bundle-name@4.1.0:
resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==}
engines: {node: '>=18'}
@@ -1225,6 +1297,9 @@ packages:
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
engines: {node: ^12.20 || >= 14.13}
fflate@0.8.3:
resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==}
figures@6.1.0:
resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==}
engines: {node: '>=18'}
@@ -1339,6 +1414,9 @@ packages:
resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==}
engines: {node: '>=0.10.0'}
ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
@@ -1446,6 +1524,11 @@ packages:
resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==}
engines: {node: '>=18'}
its-fine@2.0.0:
resolution: {integrity: sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==}
peerDependencies:
react: ^19.0.0
jiti@2.7.0:
resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
hasBin: true
@@ -1608,6 +1691,9 @@ packages:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
meshoptimizer@1.1.1:
resolution: {integrity: sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==}
micromatch@4.0.8:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'}
@@ -1824,6 +1910,15 @@ packages:
resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
engines: {node: '>=0.10.0'}
react-use-measure@2.1.7:
resolution: {integrity: sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==}
peerDependencies:
react: '>=16.13'
react-dom: '>=16.13'
peerDependenciesMeta:
react-dom:
optional: true
react@19.2.7:
resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==}
engines: {node: '>=0.10.0'}
@@ -1975,6 +2070,11 @@ packages:
resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==}
engines: {node: '>=18'}
suspend-react@0.1.3:
resolution: {integrity: sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==}
peerDependencies:
react: '>=17.0'
systeminformation@5.31.7:
resolution: {integrity: sha512-/8NC53e5nP9nmhn42/ncdOkyJnOoue/Vy+tJOyUGd1Yv66G069wK4rrziwhrqDETgk78CudTQupw5z19S5uoZw==}
engines: {node: '>=8.0.0'}
@@ -1991,6 +2091,9 @@ packages:
resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==}
engines: {node: '>=6'}
three@0.184.0:
resolution: {integrity: sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg==}
tiny-invariant@1.3.3:
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
@@ -2387,6 +2490,8 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.17
'@dimforge/rapier3d-compat@0.12.0': {}
'@dotenvx/dotenvx@1.74.2':
dependencies:
commander: 11.1.0
@@ -2574,6 +2679,26 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.20.1
'@react-three/fiber@9.6.1(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(three@0.184.0)':
dependencies:
'@babel/runtime': 7.29.7
'@types/webxr': 0.5.24
base64-js: 1.5.1
buffer: 6.0.3
its-fine: 2.0.0(@types/react@19.2.17)(react@19.2.7)
react: 19.2.7
react-use-measure: 2.1.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
scheduler: 0.27.0
suspend-react: 0.1.3(react@19.2.7)
three: 0.184.0
use-sync-external-store: 1.6.0(react@19.2.7)
zustand: 5.0.14(@types/react@19.2.17)(react@19.2.7)(use-sync-external-store@1.6.0(react@19.2.7))
optionalDependencies:
react-dom: 19.2.7(react@19.2.7)
transitivePeerDependencies:
- '@types/react'
- immer
'@rolldown/pluginutils@1.0.0-beta.27': {}
'@rollup/rollup-android-arm-eabi@4.62.0':
@@ -2776,16 +2901,26 @@ snapshots:
dependencies:
'@tauri-apps/api': 2.11.1
'@tauri-apps/plugin-process@2.3.1':
dependencies:
'@tauri-apps/api': 2.11.1
'@tauri-apps/plugin-shell@2.3.5':
dependencies:
'@tauri-apps/api': 2.11.1
'@tauri-apps/plugin-updater@2.10.1':
dependencies:
'@tauri-apps/api': 2.11.1
'@ts-morph/common@0.27.0':
dependencies:
fast-glob: 3.3.3
minimatch: 10.2.5
path-browserify: 1.0.1
'@tweenjs/tween.js@23.1.3': {}
'@types/babel__core@7.20.5':
dependencies:
'@babel/parser': 7.29.7
@@ -2813,12 +2948,29 @@ snapshots:
dependencies:
'@types/react': 19.2.17
'@types/react-reconciler@0.28.9(@types/react@19.2.17)':
dependencies:
'@types/react': 19.2.17
'@types/react@19.2.17':
dependencies:
csstype: 3.2.3
'@types/stats.js@0.17.4': {}
'@types/three@0.184.1':
dependencies:
'@dimforge/rapier3d-compat': 0.12.0
'@tweenjs/tween.js': 23.1.3
'@types/stats.js': 0.17.4
'@types/webxr': 0.5.24
fflate: 0.8.3
meshoptimizer: 1.1.1
'@types/validate-npm-package-name@4.0.2': {}
'@types/webxr@0.5.24': {}
'@vitejs/plugin-react@4.7.0(vite@7.3.5(jiti@2.7.0)(lightningcss@1.32.0))':
dependencies:
'@babel/core': 7.29.7
@@ -2869,6 +3021,8 @@ snapshots:
balanced-match@4.0.4: {}
base64-js@1.5.1: {}
baseline-browser-mapping@2.10.38: {}
body-parser@2.3.0:
@@ -2901,6 +3055,11 @@ snapshots:
node-releases: 2.0.48
update-browserslist-db: 1.2.3(browserslist@4.28.2)
buffer@6.0.3:
dependencies:
base64-js: 1.5.1
ieee754: 1.2.1
bundle-name@4.1.0:
dependencies:
run-applescript: 7.1.0
@@ -3205,6 +3364,8 @@ snapshots:
node-domexception: 1.0.0
web-streams-polyfill: 3.3.3
fflate@0.8.3: {}
figures@6.1.0:
dependencies:
is-unicode-supported: 2.1.0
@@ -3319,6 +3480,8 @@ snapshots:
dependencies:
safer-buffer: 2.1.2
ieee754@1.2.1: {}
ignore@5.3.2: {}
import-fresh@3.3.1:
@@ -3384,6 +3547,13 @@ snapshots:
isexe@3.1.5: {}
its-fine@2.0.0(@types/react@19.2.17)(react@19.2.7):
dependencies:
'@types/react-reconciler': 0.28.9(@types/react@19.2.17)
react: 19.2.7
transitivePeerDependencies:
- '@types/react'
jiti@2.7.0: {}
jose@6.2.3: {}
@@ -3499,6 +3669,8 @@ snapshots:
merge2@1.4.1: {}
meshoptimizer@1.1.1: {}
micromatch@4.0.8:
dependencies:
braces: 3.0.3
@@ -3692,6 +3864,12 @@ snapshots:
react-refresh@0.17.0: {}
react-use-measure@2.1.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
dependencies:
react: 19.2.7
optionalDependencies:
react-dom: 19.2.7(react@19.2.7)
react@19.2.7: {}
recast@0.23.11:
@@ -3912,6 +4090,10 @@ snapshots:
strip-final-newline@4.0.0: {}
suspend-react@0.1.3(react@19.2.7):
dependencies:
react: 19.2.7
systeminformation@5.31.7: {}
tailwind-merge@3.6.0: {}
@@ -3920,6 +4102,8 @@ snapshots:
tapable@2.3.3: {}
three@0.184.0: {}
tiny-invariant@1.3.3: {}
tinyglobby@0.2.17:
+1 -1
View File
@@ -78,7 +78,7 @@
</div>
<div class="bottom-bar">
<span>Provided by Lingke Koring Studio</span>
<span>v0.1.0</span>
<span>UI预览版本</span>
</div>
</div>
</div>
+385 -7
View File
@@ -47,6 +47,15 @@ version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
dependencies = [
"derive_arbitrary",
]
[[package]]
name = "async-broadcast"
version = "0.7.2"
@@ -412,9 +421,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.64"
version = "1.2.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f"
checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96"
dependencies = [
"find-msvc-tools",
"shlex",
@@ -670,6 +679,17 @@ dependencies = [
"serde_core",
]
[[package]]
name = "derive_arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.118",
]
[[package]]
name = "derive_more"
version = "2.1.1"
@@ -963,6 +983,16 @@ dependencies = [
"rustc_version",
]
[[package]]
name = "filetime"
version = "0.2.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
dependencies = [
"cfg-if",
"libc",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
@@ -1507,6 +1537,21 @@ dependencies = [
"want",
]
[[package]]
name = "hyper-rustls"
version = "0.27.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
dependencies = [
"http",
"hyper",
"hyper-util",
"rustls",
"tokio",
"tokio-rustls",
"tower-service",
]
[[package]]
name = "hyper-util"
version = "0.1.20"
@@ -1775,6 +1820,36 @@ dependencies = [
"windows-sys 0.45.0",
]
[[package]]
name = "jni"
version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
dependencies = [
"cfg-if",
"combine",
"jni-macros",
"jni-sys 0.4.1",
"log",
"simd_cesu8",
"thiserror 2.0.18",
"walkdir",
"windows-link 0.2.1",
]
[[package]]
name = "jni-macros"
version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
dependencies = [
"proc-macro2",
"quote",
"rustc_version",
"simd_cesu8",
"syn 2.0.118",
]
[[package]]
name = "jni-sys"
version = "0.3.1"
@@ -1856,7 +1931,9 @@ dependencies = [
"tauri",
"tauri-build",
"tauri-plugin-opener",
"tauri-plugin-process",
"tauri-plugin-shell",
"tauri-plugin-updater",
"tokio",
"uuid",
]
@@ -1978,6 +2055,12 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "minisign-verify"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e"
[[package]]
name = "miniz_oxide"
version = "0.8.9"
@@ -2210,6 +2293,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
dependencies = [
"bitflags 2.13.0",
"block2",
"libc",
"objc2",
"objc2-core-foundation",
]
@@ -2225,6 +2309,18 @@ dependencies = [
"objc2-core-foundation",
]
[[package]]
name = "objc2-osa-kit"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0"
dependencies = [
"bitflags 2.13.0",
"objc2",
"objc2-app-kit",
"objc2-foundation",
]
[[package]]
name = "objc2-quartz-core"
version = "0.3.2"
@@ -2300,6 +2396,12 @@ dependencies = [
"pathdiff",
]
[[package]]
name = "openssl-probe"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
[[package]]
name = "option-ext"
version = "0.2.0"
@@ -2326,6 +2428,20 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "osakit"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b"
dependencies = [
"objc2",
"objc2-foundation",
"objc2-osa-kit",
"serde",
"serde_json",
"thiserror 2.0.18",
]
[[package]]
name = "pango"
version = "0.18.3"
@@ -2723,15 +2839,20 @@ dependencies = [
"http-body",
"http-body-util",
"hyper",
"hyper-rustls",
"hyper-util",
"js-sys",
"log",
"percent-encoding",
"pin-project-lite",
"rustls",
"rustls-pki-types",
"rustls-platform-verifier",
"serde",
"serde_json",
"sync_wrapper",
"tokio",
"tokio-rustls",
"tokio-util",
"tower",
"tower-http",
@@ -2743,6 +2864,20 @@ dependencies = [
"web-sys",
]
[[package]]
name = "ring"
version = "0.17.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [
"cc",
"cfg-if",
"getrandom 0.2.17",
"libc",
"untrusted",
"windows-sys 0.52.0",
]
[[package]]
name = "rustc-hash"
version = "2.1.2"
@@ -2771,6 +2906,79 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "rustls"
version = "0.23.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"
dependencies = [
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki",
"subtle",
"zeroize",
]
[[package]]
name = "rustls-native-certs"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"
dependencies = [
"openssl-probe",
"rustls-pki-types",
"schannel",
"security-framework",
]
[[package]]
name = "rustls-pki-types"
version = "1.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
dependencies = [
"zeroize",
]
[[package]]
name = "rustls-platform-verifier"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
dependencies = [
"core-foundation",
"core-foundation-sys",
"jni 0.22.4",
"log",
"once_cell",
"rustls",
"rustls-native-certs",
"rustls-platform-verifier-android",
"rustls-webpki",
"security-framework",
"security-framework-sys",
"webpki-root-certs",
"windows-sys 0.61.2",
]
[[package]]
name = "rustls-platform-verifier-android"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]]
name = "rustls-webpki"
version = "0.103.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
dependencies = [
"ring",
"rustls-pki-types",
"untrusted",
]
[[package]]
name = "rustversion"
version = "1.0.22"
@@ -2786,6 +2994,15 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "schannel"
version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "schemars"
version = "0.8.22"
@@ -2843,6 +3060,29 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "security-framework"
version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
dependencies = [
"bitflags 2.13.0",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "selectors"
version = "0.36.1"
@@ -3095,6 +3335,22 @@ version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
[[package]]
name = "simd_cesu8"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33"
dependencies = [
"rustc_version",
"simdutf8",
]
[[package]]
name = "simdutf8"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]]
name = "siphasher"
version = "1.0.3"
@@ -3207,6 +3463,12 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "swift-rs"
version = "1.0.7"
@@ -3290,7 +3552,7 @@ dependencies = [
"gdkwayland-sys",
"gdkx11-sys",
"gtk",
"jni",
"jni 0.21.1",
"libc",
"log",
"ndk",
@@ -3323,6 +3585,17 @@ dependencies = [
"syn 2.0.118",
]
[[package]]
name = "tar"
version = "0.4.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840"
dependencies = [
"filetime",
"libc",
"xattr",
]
[[package]]
name = "target-lexicon"
version = "0.12.16"
@@ -3346,7 +3619,7 @@ dependencies = [
"gtk",
"heck 0.5.0",
"http",
"jni",
"jni 0.21.1",
"libc",
"log",
"mime",
@@ -3480,6 +3753,16 @@ dependencies = [
"zbus",
]
[[package]]
name = "tauri-plugin-process"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d55511a7bf6cd70c8767b02c97bf8134fa434daf3926cfc1be0a0f94132d165a"
dependencies = [
"tauri",
"tauri-plugin",
]
[[package]]
name = "tauri-plugin-shell"
version = "2.3.5"
@@ -3501,6 +3784,39 @@ dependencies = [
"tokio",
]
[[package]]
name = "tauri-plugin-updater"
version = "2.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "806d9dac662c2e4594ff03c647a552f2c9bd544e7d0f683ec58f872f952ce4af"
dependencies = [
"base64 0.22.1",
"dirs",
"flate2",
"futures-util",
"http",
"infer",
"log",
"minisign-verify",
"osakit",
"percent-encoding",
"reqwest",
"rustls",
"semver",
"serde",
"serde_json",
"tar",
"tauri",
"tauri-plugin",
"tempfile",
"thiserror 2.0.18",
"time",
"tokio",
"url",
"windows-sys 0.60.2",
"zip",
]
[[package]]
name = "tauri-runtime"
version = "2.11.3"
@@ -3511,7 +3827,7 @@ dependencies = [
"dpi",
"gtk",
"http",
"jni",
"jni 0.21.1",
"objc2",
"objc2-ui-kit",
"objc2-web-kit",
@@ -3534,7 +3850,7 @@ checksum = "fe41e015bf8fc4d6477ff4926a0ef769dc64ff34c7b0038b6f7cacae892acb5c"
dependencies = [
"gtk",
"http",
"jni",
"jni 0.21.1",
"log",
"objc2",
"objc2-app-kit",
@@ -3733,6 +4049,16 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "tokio-rustls"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
dependencies = [
"rustls",
"tokio",
]
[[package]]
name = "tokio-util"
version = "0.7.18"
@@ -4046,6 +4372,12 @@ version = "1.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "url"
version = "2.5.8"
@@ -4295,6 +4627,15 @@ dependencies = [
"system-deps",
]
[[package]]
name = "webpki-root-certs"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "webview2-com"
version = "0.38.2"
@@ -4525,6 +4866,15 @@ dependencies = [
"windows-targets 0.42.2",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
@@ -4820,7 +5170,7 @@ dependencies = [
"gtk",
"http",
"javascriptcore-rs",
"jni",
"jni 0.21.1",
"libc",
"ndk",
"objc2",
@@ -4867,6 +5217,16 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "xattr"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
dependencies = [
"libc",
"rustix",
]
[[package]]
name = "yoke"
version = "0.8.3"
@@ -4972,6 +5332,12 @@ dependencies = [
"synstructure",
]
[[package]]
name = "zeroize"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
[[package]]
name = "zerotrie"
version = "0.2.4"
@@ -5005,6 +5371,18 @@ dependencies = [
"syn 2.0.118",
]
[[package]]
name = "zip"
version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1"
dependencies = [
"arbitrary",
"crc32fast",
"indexmap 2.14.0",
"memchr",
]
[[package]]
name = "zmij"
version = "1.0.21"
+2
View File
@@ -20,7 +20,9 @@ tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-opener = "2"
tauri-plugin-process = "2"
tauri-plugin-shell = "2"
tauri-plugin-updater = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
uuid = { version = "1", features = ["v4"] }
+2
View File
@@ -17,7 +17,9 @@
"core:window:allow-set-focus",
"core:webview:allow-create-webview-window",
"opener:default",
"process:default",
"shell:allow-execute",
"updater:default",
{
"identifier": "shell:allow-execute",
"allow": [
Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 KiB

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 393 KiB

After

Width:  |  Height:  |  Size: 365 KiB

+2
View File
@@ -9,7 +9,9 @@ use tauri::Manager;
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_updater::Builder::new().build())
.manage(Mutex::new(SidecarManager::new()))
.invoke_handler(tauri::generate_handler![
commands::sidecar_request,
+10 -1
View File
@@ -52,6 +52,15 @@
],
"externalBin": [
"binaries/koring-sidecar"
]
],
"createUpdaterArtifacts": true
},
"plugins": {
"updater": {
"endpoints": [
"https://cdn.crabnebula.app/update/lingke-network/koring-launcher/{{target}}-{{arch}}/{{current_version}}"
],
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEYxRDZBQjc3QUQxNDBGNDUKUldSRkR4U3RkNnZXOGNtdHE4TS80d3p4UHlhaEVVMDFUTFRrQmhySnFVNWdZWHNXOWUraFFhRTcK"
}
}
}
+7 -1
View File
@@ -6,7 +6,10 @@ import { Store } from "./pages/store";
import { Today } from "./pages/today";
import { PlayLink } from "./pages/play-link";
import { Setting } from "./pages/setting";
import { Debug } from "./pages/setting/debug";
import { Debug } from "./pages/debug";
import { SplashDebug } from "./pages/debug/splash-debug";
import { DisplayDebug } from "./pages/debug/display-debug";
import { VersionCardDebug } from "./pages/debug/version-card-debug";
const pageMap = {
home: Home,
@@ -15,6 +18,9 @@ const pageMap = {
"play-link": PlayLink,
setting: Setting,
debug: Debug,
"debug-splash": SplashDebug,
"debug-display": DisplayDebug,
"debug-version-card": VersionCardDebug,
} as const;
function App() {
+43
View File
@@ -0,0 +1,43 @@
import { check, type Update } from "@tauri-apps/plugin-updater";
import { relaunch } from "@tauri-apps/plugin-process";
export interface DownloadProgress {
downloaded: number;
contentLength: number;
percent: number;
}
export async function checkForUpdates(): Promise<Update | null> {
const update = await check();
return update;
}
export async function downloadAndInstall(
update: Update,
onProgress?: (progress: DownloadProgress) => void,
): Promise<void> {
let downloaded = 0;
let contentLength = 0;
await update.downloadAndInstall((event) => {
switch (event.event) {
case "Started":
contentLength = event.data.contentLength ?? 0;
break;
case "Progress":
downloaded += event.data.chunkLength;
onProgress?.({
downloaded,
contentLength,
percent: contentLength > 0 ? Math.round((downloaded / contentLength) * 100) : 0,
});
break;
case "Finished":
break;
}
});
}
export async function relaunchApp(): Promise<void> {
await relaunch();
}
+150
View File
@@ -0,0 +1,150 @@
import { BUILD_MODE } from "@/lib/mode";
import { useUpdateStore } from "@/stores/updateStore";
import { relaunchApp } from "@/api/update";
import Silk from "@/components/silk/Silk";
import { Button } from "@/components/ui/button";
import clsx from "clsx";
const modeColors: Record<string, string> = {
dev: "#F59E0B",
beta: "#10B981",
run: "#3B82F6",
};
const modeLabels: Record<string, string> = {
dev: "开发版",
beta: "测试版",
run: "正式版",
};
const VERSION = "0.1.0";
type UpdateState = "latest" | "hasUpdate" | "installed";
interface VersionCardProps {
className?: string;
/** 开发者模式:覆盖颜色 */
overrideMode?: string | null;
/** 开发者模式:覆盖更新状态 */
overrideState?: UpdateState | null;
/** 开发者模式:遮罩透明度 (0-100) */
overlayOpacity?: number;
/** 开发者模式:模糊强度 (px) */
blurAmount?: number;
}
export function VersionCard({
className,
overrideMode,
overrideState,
overlayOpacity = 30,
blurAmount = 12,
}: VersionCardProps) {
const color = modeColors[overrideMode ?? BUILD_MODE] ?? modeColors.run;
const label = modeLabels[overrideMode ?? BUILD_MODE] ?? modeLabels.run;
const { checking, downloading, installed, update, check, install } = useUpdateStore();
const effectiveState: UpdateState = overrideState ?? (installed ? "installed" : update ? "hasUpdate" : "latest");
return (
<div
className={clsx(
"relative overflow-hidden rounded-xl border border-white/10",
className,
)}
>
{/* Silk 背景 */}
<div className="absolute inset-0 z-0">
<Silk speed={3} scale={1.2} color={color} noiseIntensity={1.2} rotation={0.3} />
</div>
{/* 磨砂层 */}
<div
className="absolute inset-0 z-[1]"
style={{
background: `rgba(0,0,0,${overlayOpacity / 100})`,
backdropFilter: `blur(${blurAmount}px)`,
}}
/>
{/* 内容层 */}
<div className="relative z-10 flex flex-col items-center justify-center px-6 py-10 gap-4">
{/* 版本类型 */}
<span className="text-[11px] font-bold tracking-wider text-white/70">
{label}
</span>
{/* Logo */}
<img src="/koring-licon.svg" alt="Koring" className="w-50 h-20 drop-shadow-lg" style={{ filter: "brightness(0) invert(1)" }} />
{/* 版本号 */}
<p className="text-sm text-white/70 font-medium">v{VERSION}</p>
{/* 按钮组 */}
<div className="flex items-center gap-2 mt-1">
{effectiveState === "latest" && (
<>
<Button
size="sm"
variant="outline"
className="bg-white/10 border-white/20 text-white hover:bg-white/20 hover:text-white"
onClick={check}
disabled={checking}
>
{checking ? "检查中..." : "检查更新"}
</Button>
<Button
size="sm"
variant="outline"
className="bg-white/10 border-white/20 text-white hover:bg-white/20 hover:text-white"
>
</Button>
</>
)}
{effectiveState === "hasUpdate" && (
<>
<span className="text-[12px] text-white/80 mr-1">
</span>
<Button
size="sm"
variant="outline"
className="bg-white/10 border-white/20 text-white hover:bg-white/20 hover:text-white"
onClick={install}
disabled={downloading}
>
{downloading ? "下载中..." : "下载更新"}
</Button>
<Button
size="sm"
variant="outline"
className="bg-white/10 border-white/20 text-white hover:bg-white/20 hover:text-white"
>
</Button>
</>
)}
{effectiveState === "installed" && (
<>
<span className="text-[12px] text-white/80 mr-1">
</span>
<Button
size="sm"
variant="outline"
className="bg-white/10 border-white/20 text-white hover:bg-white/20 hover:text-white"
onClick={relaunchApp}
>
</Button>
</>
)}
</div>
</div>
</div>
);
}
@@ -9,7 +9,7 @@ export function BackgroundLayer() {
const { type, image, color, blur, opacity, animationSpeed, fetchConfig } = useBackgroundStore();
const route = useRouteStore((s) => s.current);
const forceDisableContentBlur = useDevStore((s) => s.forceDisableContentBlur);
const inSetting = route === "setting" || route === "debug";
const showContentBlur = route !== "home";
useEffect(() => {
fetchConfig();
@@ -82,7 +82,10 @@ export function BackgroundLayer() {
}
`}</style>
<div style={getBackgroundStyle()} />
{inSetting && !forceDisableContentBlur && <div className="content-blur-overlay" />}
<div
className="content-blur-overlay"
style={{ opacity: showContentBlur && !forceDisableContentBlur ? 1 : 0 }}
/>
<div className="dark:block hidden fixed inset-0 z-0 pointer-events-none bg-black/35" />
</>
);
+130
View File
@@ -0,0 +1,130 @@
/* eslint-disable react/no-unknown-property */
import { Canvas, useFrame, useThree } from "@react-three/fiber";
import { forwardRef, useRef, useMemo, useLayoutEffect } from "react";
import { Color, type Mesh, type ShaderMaterial } from "three";
const hexToNormalizedRGB = (hex: string) => {
hex = hex.replace("#", "");
return [
parseInt(hex.slice(0, 2), 16) / 255,
parseInt(hex.slice(2, 4), 16) / 255,
parseInt(hex.slice(4, 6), 16) / 255,
];
};
const vertexShader = `
varying vec2 vUv;
varying vec3 vPosition;
void main() {
vPosition = position;
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const fragmentShader = `
varying vec2 vUv;
varying vec3 vPosition;
uniform float uTime;
uniform vec3 uColor;
uniform float uSpeed;
uniform float uScale;
uniform float uRotation;
uniform float uNoiseIntensity;
const float e = 2.71828182845904523536;
float noise(vec2 texCoord) {
float G = e;
vec2 r = (G * sin(G * texCoord));
return fract(r.x * r.y * (1.0 + texCoord.x));
}
vec2 rotateUvs(vec2 uv, float angle) {
float c = cos(angle);
float s = sin(angle);
mat2 rot = mat2(c, -s, s, c);
return rot * uv;
}
void main() {
float rnd = noise(gl_FragCoord.xy);
vec2 uv = rotateUvs(vUv * uScale, uRotation);
vec2 tex = uv * uScale;
float tOffset = uSpeed * uTime;
tex.y += 0.03 * sin(8.0 * tex.x - tOffset);
float pattern = 0.6 +
0.4 * sin(5.0 * (tex.x + tex.y +
cos(3.0 * tex.x + 5.0 * tex.y) +
0.02 * tOffset) +
sin(20.0 * (tex.x + tex.y - 0.1 * tOffset)));
vec4 col = vec4(uColor, 1.0) * vec4(pattern) - rnd / 15.0 * uNoiseIntensity;
col.a = 1.0;
gl_FragColor = col;
}
`;
interface SilkPlaneProps {
uniforms: Record<string, { value: number | Color }>;
}
const SilkPlane = forwardRef(function SilkPlane({ uniforms }: SilkPlaneProps, ref: React.Ref<Mesh>) {
const { viewport } = useThree();
useLayoutEffect(() => {
if (ref && typeof ref === "object" && ref.current) {
ref.current.scale.set(viewport.width, viewport.height, 1);
}
}, [ref, viewport]);
useFrame((_, delta) => {
if (ref && typeof ref === "object" && ref.current) {
(ref.current.material as ShaderMaterial).uniforms.uTime.value += 0.1 * delta;
}
});
return (
<mesh ref={ref}>
<planeGeometry args={[1, 1, 1, 1]} />
<shaderMaterial uniforms={uniforms} vertexShader={vertexShader} fragmentShader={fragmentShader} />
</mesh>
);
});
SilkPlane.displayName = "SilkPlane";
interface SilkProps {
speed?: number;
scale?: number;
color?: string;
noiseIntensity?: number;
rotation?: number;
}
const Silk = ({ speed = 5, scale = 1, color = "#7B7481", noiseIntensity = 1.5, rotation = 0 }: SilkProps) => {
const meshRef = useRef<Mesh>(null);
const uniforms = useMemo(
() => ({
uSpeed: { value: speed },
uScale: { value: scale },
uNoiseIntensity: { value: noiseIntensity },
uColor: { value: new Color(...hexToNormalizedRGB(color)) },
uRotation: { value: rotation },
uTime: { value: 0 },
}),
[speed, scale, noiseIntensity, color, rotation],
);
return (
<Canvas dpr={[1, 2]} frameloop="always">
<SilkPlane ref={meshRef} uniforms={uniforms} />
</Canvas>
);
};
export default Silk;
+17
View File
@@ -1,5 +1,7 @@
import { useState, useEffect } from "react";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { BUILD_MODE } from "@/lib/mode";
import clsx from "clsx";
interface WindowControlsProps {
showMinimize?: boolean;
@@ -30,8 +32,23 @@ export function WindowControls({
const btnClass = "flex items-center justify-center w-[25px] h-[25px] rounded transition-colors cursor-default hover:bg-black/10 dark:hover:bg-white/15 text-black/70 dark:text-white/70 hover:text-black dark:hover:text-white";
const closeBtnClass = "flex items-center justify-center w-[25px] h-[25px] rounded transition-colors cursor-default hover:bg-red-500 text-black/70 dark:text-white/70 hover:text-white";
const showBadge = BUILD_MODE !== "run";
const badgeLabel = BUILD_MODE === "dev" ? "DEV" : "BETA";
return (
<div className="flex items-center gap-0.5 pr-2" data-no-drag>
{showBadge && (
<span
className={clsx(
"inline-flex items-center justify-center h-[18px] px-1.5 rounded text-[10px] font-bold tracking-wider mr-1",
BUILD_MODE === "dev"
? "bg-amber-500/65 text-amber-700 dark:text-amber-400 border border-amber-500/20"
: "bg-emerald-500/65 text-emerald-800 dark:text-emerald-400 border border-emerald-500/20",
)}
>
{badgeLabel}
</span>
)}
{showMinimize && (
<div onClick={handleMinimize} className={btnClass}>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.5">
+17 -3
View File
@@ -92,7 +92,7 @@
--sidebar-ring: oklch(0.708 0 0);
/* 标题栏 & 导航 - 亮色 */
--titlebar-bg: rgba(255, 255, 255, 0.1);
--titlebar-bg: rgba(255, 255, 255, 0.3);
--titlebar-border: rgba(0, 0, 0, 0.08);
--indicator-bg: rgba(0, 0, 0, 0.1);
--indicator-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
@@ -200,6 +200,8 @@
animation: 0.3s cubic-bezier(0.4, 0, 0.2, 1) 0.05s both page-in-forward;
}
/* 设置子页面转场(已弃用 View Transitions,改用 keyframe */
/* 内容区转场:后退方向 */
:root[data-transition-dir="backward"] ::view-transition-old(content) {
animation-name: page-out-backward;
@@ -226,6 +228,17 @@
to { opacity: 1; transform: scale(1); }
}
/* ============================================
设置子页面切换 — keyframe 动画(仅内容区)
============================================ */
.setting-page-enter {
animation: setting-page-in 0.25s cubic-bezier(0.4, 0, 0.2, 1) both;
}
@keyframes setting-page-in {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: translateY(0); }
}
/* ============================================
磨砂卡片 — 复用 --titlebar-bg / --titlebar-border
============================================ */
@@ -251,10 +264,11 @@
-webkit-backdrop-filter: blur(80px) saturate(120%);
z-index: 0;
pointer-events: none;
background: rgba(255, 255, 255, 0.5);
background: rgba(255, 255, 255, var(--content-blur-opacity, 0.5));
transition: opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
:root.dark .content-blur-overlay {
background: rgba(0, 0, 0, 0.5);
background: rgba(0, 0, 0, var(--content-blur-opacity, 0.5));
}
/* ============================================
+4 -1
View File
@@ -17,7 +17,9 @@ export function RootLayout({
showMaximize = true,
showClose = true,
}: RootLayoutProps) {
const { reduceMotion, reduceTransparency, highContrast } = useA11yStore();
const { reduceMotion, reduceTransparency, highContrast, contentBlurOpacity } = useA11yStore();
const blurOpacity = reduceTransparency ? 0 : contentBlurOpacity;
return (
<div
@@ -27,6 +29,7 @@ export function RootLayout({
reduceTransparency && "reduce-transparency",
highContrast && "high-contrast",
)}
style={{ "--content-blur-opacity": `${blurOpacity / 100}` } as React.CSSProperties}
>
{/* Layer 0: Background */}
<BackgroundLayer />
+3 -8
View File
@@ -1,10 +1,5 @@
import { UnderConstruction } from "@/components/UnderConstruction";
export function PlayLink() {
return (
<div className="flex items-center justify-center h-full">
<div className="text-center">
<h1 className="text-4xl font-bold mb-4 text-foreground"></h1>
<p className="text-muted-foreground"></p>
</div>
</div>
);
return <UnderConstruction pageName="联机" description="多人联机与服务器,敬请期待" />;
}
-59
View File
@@ -1,59 +0,0 @@
import { Button } from "@/components/ui/button";
import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
export function Debug() {
const openSplash = async () => {
try {
const existing = await WebviewWindow.getByLabel("splashscreen");
if (existing) {
await existing.show();
await existing.setFocus();
return;
}
const splash = new WebviewWindow("splashscreen", {
url: "/splash.html",
width: 480,
height: 320,
decorations: false,
transparent: true,
center: true,
visible: true,
resizable: false,
minWidth: 480,
maxWidth: 480,
minHeight: 320,
maxHeight: 320,
} as any);
splash.once("tauri://error", (e) => {
console.error("Splash window error:", e);
});
} catch (err) {
console.error("Failed to open splash:", err);
}
};
const closeSplash = async () => {
try {
const splash = await WebviewWindow.getByLabel("splashscreen");
if (splash) {
await splash.close();
}
} catch (err) {
console.error("Failed to close splash:", err);
}
};
return (
<div className="flex items-center justify-center h-full">
<div className="text-center space-y-4">
<h1 className="text-2xl font-bold mb-6 text-foreground">Debug</h1>
<div className="flex gap-3">
<Button onClick={openSplash}>Open Splash</Button>
<Button variant="destructive" onClick={closeSplash}>Close Splash</Button>
</div>
</div>
</div>
);
}
+162 -3
View File
@@ -1,8 +1,167 @@
export function AdvancedSetting() {
import { useState } from "react";
import { Switch } from "@/components/ui/switch";
function GlassCard({ children }: { children: React.ReactNode }) {
return <div className="glass-card px-5 py-4">{children}</div>;
}
function SettingRow({ label, desc, children }: { label: string; desc?: string; children: React.ReactNode }) {
return (
<div className="flex items-center justify-between gap-4">
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-foreground">{label}</p>
{desc && <p className="text-[13px] text-muted-foreground mt-0.5">{desc}</p>}
</div>
<div className="shrink-0">{children}</div>
</div>
);
}
const launcherBehavior = [
{ value: "close", label: "关闭启动器" },
{ value: "minimize", label: "最小化到任务栏" },
{ value: "keep", label: "保持不变" },
];
const windowSize = [
{ value: "default", label: "默认窗口大小" },
{ value: "fullscreen", label: "全屏启动" },
{ value: "custom", label: "自定义尺寸" },
];
export function AdvancedSetting() {
const [afterLaunch, setAfterLaunch] = useState("close");
const [winMode, setWinMode] = useState("default");
const [customWidth, setCustomWidth] = useState("854");
const [customHeight, setCustomHeight] = useState("480");
const [gameArgs, setGameArgs] = useState("");
const [preLaunchCmd, setPreLaunchCmd] = useState("");
const [debugMode, setDebugMode] = useState(false);
return (
<div>
<h2 className="text-xl font-bold text-foreground mb-4"></h2>
<p className="text-sm text-muted-foreground"></p>
<h2 className="text-xl font-bold text-foreground mb-1"></h2>
<p className="text-sm text-muted-foreground mb-6"></p>
<div className="space-y-6">
{/* ===== 启动行为 ===== */}
<div>
<h3 className="text-lg font-bold text-foreground mb-3"></h3>
<div className="space-y-3">
<GlassCard>
<div className="space-y-2">
<p className="text-sm font-medium text-foreground"></p>
<p className="text-[13px] text-muted-foreground"></p>
<div className="space-y-2 mt-2">
{launcherBehavior.map((opt) => (
<label key={opt.value} className="flex items-center gap-2 cursor-pointer">
<input type="radio" name="afterLaunch" checked={afterLaunch === opt.value} onChange={() => setAfterLaunch(opt.value)} className="accent-primary" />
<span className="text-sm text-foreground">{opt.label}</span>
</label>
))}
</div>
</div>
</GlassCard>
</div>
</div>
{/* ===== 窗口设置 ===== */}
<div>
<h3 className="text-lg font-bold text-foreground mb-3"></h3>
<div className="space-y-3">
<GlassCard>
<div className="space-y-3">
<div className="space-y-2">
<p className="text-sm font-medium text-foreground"></p>
<div className="space-y-2">
{windowSize.map((opt) => (
<label key={opt.value} className="flex items-center gap-2 cursor-pointer">
<input type="radio" name="winMode" checked={winMode === opt.value} onChange={() => setWinMode(opt.value)} className="accent-primary" />
<span className="text-sm text-foreground">{opt.label}</span>
</label>
))}
</div>
</div>
{winMode === "custom" && (
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
<span className="text-[13px] text-muted-foreground"></span>
<input
type="number"
value={customWidth}
onChange={(e) => setCustomWidth(e.target.value)}
className="w-20 h-8 px-2 rounded-md border border-input bg-background text-sm text-center font-mono focus:outline-none focus:ring-1 focus:ring-ring"
/>
</div>
<span className="text-muted-foreground">×</span>
<div className="flex items-center gap-2">
<span className="text-[13px] text-muted-foreground"></span>
<input
type="number"
value={customHeight}
onChange={(e) => setCustomHeight(e.target.value)}
className="w-20 h-8 px-2 rounded-md border border-input bg-background text-sm text-center font-mono focus:outline-none focus:ring-1 focus:ring-ring"
/>
</div>
</div>
)}
</div>
</GlassCard>
</div>
</div>
{/* ===== 游戏参数 ===== */}
<div>
<h3 className="text-lg font-bold text-foreground mb-3"></h3>
<div className="space-y-3">
<GlassCard>
<div className="space-y-2">
<p className="text-sm font-medium text-foreground"></p>
<p className="text-[13px] text-muted-foreground"></p>
<input
type="text"
value={gameArgs}
onChange={(e) => setGameArgs(e.target.value)}
placeholder="可选,例如 --demo"
className="w-full h-8 px-3 rounded-md border border-input bg-background text-sm font-mono placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring"
/>
</div>
</GlassCard>
</div>
</div>
{/* ===== 启动命令 ===== */}
<div>
<h3 className="text-lg font-bold text-foreground mb-3"></h3>
<div className="space-y-3">
<GlassCard>
<div className="space-y-2">
<p className="text-sm font-medium text-foreground"></p>
<p className="text-[13px] text-muted-foreground"></p>
<input
type="text"
value={preLaunchCmd}
onChange={(e) => setPreLaunchCmd(e.target.value)}
placeholder="可选,例如 D:\scripts\pre-launch.bat"
className="w-full h-8 px-3 rounded-md border border-input bg-background text-sm font-mono placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring"
/>
</div>
</GlassCard>
</div>
</div>
{/* ===== 调试 ===== */}
<div>
<h3 className="text-lg font-bold text-foreground mb-3"></h3>
<div className="space-y-3">
<GlassCard>
<SettingRow label="调试模式" desc="启用后将在控制台输出详细日志,可能影响性能">
<Switch checked={debugMode} onCheckedChange={setDebugMode} />
</SettingRow>
</GlassCard>
</div>
</div>
</div>
</div>
);
}
+2 -2
View File
@@ -1,8 +1,8 @@
export function GameAccountSetting() {
return (
<div>
<h2 className="text-xl font-bold text-foreground mb-4">&</h2>
<p className="text-sm text-muted-foreground"> Minecraft </p>
<h2 className="text-xl font-bold text-foreground mb-1">&</h2>
<p className="text-sm text-muted-foreground mb-6"> Minecraft </p>
</div>
);
}
+89 -2
View File
@@ -1,8 +1,95 @@
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { RefreshCw } from "lucide-react";
function GlassCard({ children }: { children: React.ReactNode }) {
return <div className="glass-card px-5 py-4">{children}</div>;
}
function PathInput({ label, desc, value, onChange }: { label: string; desc: string; value: string; onChange: (v: string) => void }) {
return (
<GlassCard>
<div className="space-y-2">
<div>
<p className="text-sm font-medium text-foreground">{label}</p>
<p className="text-[13px] text-muted-foreground mt-0.5">{desc}</p>
</div>
<div className="flex items-center gap-2">
<input
type="text"
value={value}
onChange={(e) => onChange(e.target.value)}
className="flex-1 h-8 px-3 rounded-md border border-input bg-background text-sm font-mono placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring"
/>
<Button size="sm" variant="outline"></Button>
</div>
</div>
</GlassCard>
);
}
export function GameDirSetting() {
const [gameDir, setGameDir] = useState(".minecraft");
const [resourceDir, setResourceDir] = useState("");
const [savesDir, setSavesDir] = useState("");
return (
<div>
<h2 className="text-xl font-bold text-foreground mb-4"></h2>
<p className="text-sm text-muted-foreground"></p>
<h2 className="text-xl font-bold text-foreground mb-1"></h2>
<p className="text-sm text-muted-foreground mb-6"> Minecraft </p>
<div className="space-y-6">
{/* ===== 游戏路径 ===== */}
<div>
<h3 className="text-lg font-bold text-foreground mb-3"></h3>
<div className="space-y-3">
<PathInput
label="游戏安装根目录"
desc="Minecraft 游戏文件的根目录,包含 versions、saves、resourcepacks 等文件夹"
value={gameDir}
onChange={setGameDir}
/>
<GlassCard>
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-foreground"></p>
<p className="text-[13px] text-muted-foreground mt-0.5"></p>
</div>
<Button size="sm" variant="outline">
<RefreshCw className="w-3.5 h-3.5 mr-1.5" />
</Button>
</div>
</GlassCard>
</div>
</div>
{/* ===== 资源包 ===== */}
<div>
<h3 className="text-lg font-bold text-foreground mb-3"></h3>
<div className="space-y-3">
<PathInput
label="资源包路径"
desc="自定义资源包(材质包)的存放目录,留空使用默认路径"
value={resourceDir}
onChange={setResourceDir}
/>
</div>
</div>
{/* ===== 存档 ===== */}
<div>
<h3 className="text-lg font-bold text-foreground mb-3"></h3>
<div className="space-y-3">
<PathInput
label="存档路径"
desc="自定义世界存档的存放目录,留空使用默认路径"
value={savesDir}
onChange={setSavesDir}
/>
</div>
</div>
</div>
</div>
);
}
+180 -3
View File
@@ -1,8 +1,185 @@
export function JavaMemSetting() {
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Slider } from "@/components/ui/slider";
import { Cpu, FolderSearch } from "lucide-react";
function GlassCard({ children }: { children: React.ReactNode }) {
return <div className="glass-card px-5 py-4">{children}</div>;
}
function SettingRow({ label, desc, children }: { label: string; desc?: string; children: React.ReactNode }) {
return (
<div className="flex items-center justify-between gap-4">
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-foreground">{label}</p>
{desc && <p className="text-[13px] text-muted-foreground mt-0.5">{desc}</p>}
</div>
<div className="shrink-0">{children}</div>
</div>
);
}
interface JavaInfo {
path: string;
version: string;
vendor: string;
}
const mockJavaList: JavaInfo[] = [
{ path: "C:\\Program Files\\Java\\jdk-21\\bin\\javaw.exe", version: "21.0.3", vendor: "Oracle OpenJDK" },
{ path: "C:\\Program Files\\Eclipse Adoptium\\jdk-17.0.10.7-hotspot\\bin\\javaw.exe", version: "17.0.10", vendor: "Eclipse Temurin" },
];
const gcOptions = [
{ value: "auto", label: "不指定(由 Java 自动选择)" },
{ value: "zgc", label: "ZGC(低延迟,推荐 Java 21+" },
{ value: "g1", label: "G1GC(标准,兼容性好)" },
];
export function JavaMemSetting() {
const [javaPath, setJavaPath] = useState("");
const [memMode, setMemMode] = useState<"auto" | "custom">("auto");
const [memGB, setMemGB] = useState(4);
const [gc, setGc] = useState("auto");
const [jvmArgs, setJvmArgs] = useState("");
return (
<div>
<h2 className="text-xl font-bold text-foreground mb-4">Java虚拟机与内存</h2>
<p className="text-sm text-muted-foreground"> Java </p>
<h2 className="text-xl font-bold text-foreground mb-1">Java </h2>
<p className="text-sm text-muted-foreground mb-6"> Java JVM </p>
<div className="space-y-6">
{/* ===== Java 环境 ===== */}
<div>
<h3 className="text-lg font-bold text-foreground mb-3">Java </h3>
<div className="space-y-3">
<GlassCard>
<SettingRow label="自动检测" desc="扫描系统中已安装的 Java 版本">
<Button size="sm" variant="outline">
<FolderSearch className="w-3.5 h-3.5 mr-1.5" />
</Button>
</SettingRow>
</GlassCard>
{/* 检测结果列表 */}
{mockJavaList.map((java) => (
<GlassCard key={java.path}>
<div className="flex items-center gap-3">
<Cpu className="w-4 h-4 text-muted-foreground shrink-0" />
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-foreground">
{java.vendor} {java.version}
</p>
<p className="text-[11px] text-muted-foreground/60 mt-0.5 font-mono truncate">
{java.path}
</p>
</div>
<Button size="sm" variant="outline" className="shrink-0">
使
</Button>
</div>
</GlassCard>
))}
<GlassCard>
<div className="space-y-2">
<p className="text-sm font-medium text-foreground"></p>
<div className="flex items-center gap-2">
<input
type="text"
value={javaPath}
onChange={(e) => setJavaPath(e.target.value)}
placeholder="输入 javaw.exe 完整路径"
className="flex-1 h-8 px-3 rounded-md border border-input bg-background text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring"
/>
<Button size="sm" variant="outline"></Button>
</div>
</div>
</GlassCard>
</div>
</div>
{/* ===== 内存分配 ===== */}
<div>
<h3 className="text-lg font-bold text-foreground mb-3"></h3>
<div className="space-y-3">
<GlassCard>
<div className="space-y-3">
<div className="flex items-center gap-4">
<label className="flex items-center gap-2 cursor-pointer">
<input type="radio" name="mem" checked={memMode === "auto"} onChange={() => setMemMode("auto")} className="accent-primary" />
<span className="text-sm text-foreground"></span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input type="radio" name="mem" checked={memMode === "custom"} onChange={() => setMemMode("custom")} className="accent-primary" />
<span className="text-sm text-foreground"></span>
</label>
</div>
{memMode === "custom" && (
<div>
<div className="flex items-center justify-between mb-1.5">
<span className="text-[13px] text-muted-foreground"></span>
<span className="text-[13px] text-muted-foreground tabular-nums">{memGB} GB</span>
</div>
<Slider
value={[memGB]}
onValueChange={(v) => setMemGB(Array.isArray(v) ? v[0] : v)}
min={1}
max={16}
step={1}
/>
<div className="flex justify-between mt-1">
<span className="text-[11px] text-muted-foreground/50">1 GB</span>
<span className="text-[11px] text-muted-foreground/50">16 GB</span>
</div>
</div>
)}
</div>
</GlassCard>
</div>
</div>
{/* ===== JVM 参数 ===== */}
<div>
<h3 className="text-lg font-bold text-foreground mb-3">JVM </h3>
<div className="space-y-3">
<GlassCard>
<div className="space-y-2">
<p className="text-sm font-medium text-foreground"> JVM </p>
<p className="text-[13px] text-muted-foreground"> -XX:+UseZGC</p>
<textarea
value={jvmArgs}
onChange={(e) => setJvmArgs(e.target.value)}
placeholder="可选,留空使用默认参数"
rows={3}
className="w-full px-3 py-2 rounded-md border border-input bg-background text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring resize-none font-mono"
/>
</div>
</GlassCard>
</div>
</div>
{/* ===== 垃圾回收 ===== */}
<div>
<h3 className="text-lg font-bold text-foreground mb-3"></h3>
<div className="space-y-3">
<GlassCard>
<div className="space-y-2">
<p className="text-sm font-medium text-foreground">GC </p>
<div className="space-y-2">
{gcOptions.map((opt) => (
<label key={opt.value} className="flex items-center gap-2 cursor-pointer">
<input type="radio" name="gc" checked={gc === opt.value} onChange={() => setGc(opt.value)} className="accent-primary" />
<span className="text-sm text-foreground">{opt.label}</span>
</label>
))}
</div>
</div>
</GlassCard>
</div>
</div>
</div>
</div>
);
}
+96 -3
View File
@@ -1,8 +1,101 @@
export function AboutSetting() {
import { VersionCard } from "@/components/VersionCard";
import { BUILD_MODE } from "@/lib/mode";
import { ExternalLink, GitFork } from "lucide-react";
import { open } from "@tauri-apps/plugin-shell";
function GlassCard({ children }: { children: React.ReactNode }) {
return <div className="glass-card px-5 py-4">{children}</div>;
}
function SettingRow({ label, desc, children }: { label: string; desc?: string; children: React.ReactNode }) {
return (
<div className="flex items-center justify-between gap-4">
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-foreground">{label}</p>
{desc && <p className="text-[13px] text-muted-foreground mt-0.5">{desc}</p>}
</div>
<div className="shrink-0">{children}</div>
</div>
);
}
const modeLabels: Record<string, string> = {
dev: "开发版",
beta: "测试版",
run: "正式版",
};
const GITHUB_URL = "https://github.com/koring-launcher/koring-launcher";
const OFFICIAL_URL = "https://koring.app";
export function AboutSetting() {
const openLink = (url: string) => {
open(url).catch(() => {
window.open(url, "_blank");
});
};
return (
<div>
<h2 className="text-xl font-bold text-foreground mb-4"></h2>
<p className="text-sm text-muted-foreground">Koring Launcher </p>
<h2 className="text-xl font-bold text-foreground mb-1"></h2>
<p className="text-sm text-muted-foreground mb-6"></p>
<div className="space-y-6">
{/* 版本卡片 */}
<VersionCard />
{/* 项目信息 */}
<div>
<h3 className="text-lg font-bold text-foreground mb-3"></h3>
<div className="space-y-3">
<GlassCard>
<SettingRow label="应用名称" desc="Koring Launcher">
<span className="text-[13px] text-muted-foreground">Lingke Network </span>
</SettingRow>
</GlassCard>
<GlassCard>
<SettingRow label="构建模式">
<span className="text-[13px] text-muted-foreground">{modeLabels[BUILD_MODE] ?? BUILD_MODE}</span>
</SettingRow>
</GlassCard>
<GlassCard>
<SettingRow label="技术栈" desc="Tauri 2 + React 19 + TypeScript + @xmcl">
<span className="text-[13px] text-muted-foreground">Rust · Node.js</span>
</SettingRow>
</GlassCard>
</div>
</div>
{/* 链接 */}
<div>
<h3 className="text-lg font-bold text-foreground mb-3"></h3>
<div className="space-y-3">
<GlassCard>
<SettingRow label="GitHub 仓库" desc="查看源代码、提交 Issue">
<button
onClick={() => openLink(GITHUB_URL)}
className="inline-flex items-center gap-1.5 text-[13px] text-primary hover:underline"
>
<GitFork className="w-4 h-4" />
<ExternalLink className="w-3 h-3" />
</button>
</SettingRow>
</GlassCard>
<GlassCard>
<SettingRow label="官方网站" desc="了解更多功能与文档">
<button
onClick={() => openLink(OFFICIAL_URL)}
className="inline-flex items-center gap-1.5 text-[13px] text-primary hover:underline"
>
访
<ExternalLink className="w-3 h-3" />
</button>
</SettingRow>
</GlassCard>
</div>
</div>
</div>
</div>
);
}
+106 -3
View File
@@ -1,8 +1,111 @@
export function AccountSetting() {
import { useAuthStore } from "@/stores/authStore";
import { Button } from "@/components/ui/button";
import { UserCircle, LogOut, RefreshCw } from "lucide-react";
function GlassCard({ children }: { children: React.ReactNode }) {
return <div className="glass-card px-5 py-4">{children}</div>;
}
function SettingRow({ label, desc, children }: { label: string; desc?: string; children: React.ReactNode }) {
return (
<div className="flex items-center justify-between gap-4">
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-foreground">{label}</p>
{desc && <p className="text-[13px] text-muted-foreground mt-0.5">{desc}</p>}
</div>
<div className="shrink-0">{children}</div>
</div>
);
}
export function AccountSetting() {
const { user, loading, logout, startMicrosoftLogin } = useAuthStore();
return (
<div>
<h2 className="text-xl font-bold text-foreground mb-4">Koring Account</h2>
<p className="text-sm text-muted-foreground"> Koring </p>
<h2 className="text-xl font-bold text-foreground mb-1">Koring </h2>
<p className="text-sm text-muted-foreground mb-6"> Koring </p>
<div className="space-y-6">
{/* ===== 账户信息 ===== */}
<div>
<h3 className="text-lg font-bold text-foreground mb-3"></h3>
<GlassCard>
{user ? (
<div className="flex items-center gap-4">
<div className="w-14 h-14 rounded-full bg-foreground/[0.06] flex items-center justify-center shrink-0">
<UserCircle className="w-8 h-8 text-foreground/40" />
</div>
<div className="flex-1 min-w-0">
<p className="text-base font-medium text-foreground truncate">{user.username}</p>
<p className="text-[13px] text-muted-foreground mt-0.5">
{user.xboxProfile ? "Microsoft 账户" : "本地账户"}
</p>
{user.uuid && (
<p className="text-[11px] text-muted-foreground/60 mt-0.5 font-mono">
UUID: {user.uuid}
</p>
)}
</div>
</div>
) : (
<div className="flex items-center gap-4">
<div className="w-14 h-14 rounded-full bg-foreground/[0.06] flex items-center justify-center shrink-0">
<UserCircle className="w-8 h-8 text-foreground/30" />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm text-muted-foreground"></p>
<p className="text-[12px] text-muted-foreground/60 mt-0.5"></p>
</div>
</div>
)}
</GlassCard>
</div>
{/* ===== 账户操作 ===== */}
<div>
<h3 className="text-lg font-bold text-foreground mb-3"></h3>
<div className="space-y-3">
{user ? (
<>
<GlassCard>
<SettingRow
label="同步数据"
desc="立即拉取最新的云端配置与数据"
>
<Button variant="outline" size="sm" disabled={loading}>
<RefreshCw className="w-3.5 h-3.5 mr-1.5" />
</Button>
</SettingRow>
</GlassCard>
<GlassCard>
<SettingRow
label="退出登录"
desc="退出当前账户,数据将保留在本地"
>
<Button variant="destructive" size="sm" onClick={logout} disabled={loading}>
<LogOut className="w-3.5 h-3.5 mr-1.5" />
退
</Button>
</SettingRow>
</GlassCard>
</>
) : (
<GlassCard>
<SettingRow
label="登录 Koring 账户"
desc="通过 Microsoft 账户登录以同步数据、皮肤与个人配置"
>
<Button size="sm" onClick={() => startMicrosoftLogin("00000000-0000-0000-0000-000000000000")} disabled={loading}>
</Button>
</SettingRow>
</GlassCard>
)}
</div>
</div>
</div>
</div>
);
}
+98 -3
View File
@@ -1,8 +1,103 @@
export function CopyrightSetting() {
import { ExternalLink } from "lucide-react";
import { open } from "@tauri-apps/plugin-shell";
function GlassCard({ children }: { children: React.ReactNode }) {
return <div className="glass-card px-5 py-4">{children}</div>;
}
function SettingRow({ label, desc, children }: { label: string; desc?: string; children: React.ReactNode }) {
return (
<div className="flex items-center justify-between gap-4">
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-foreground">{label}</p>
{desc && <p className="text-[13px] text-muted-foreground mt-0.5">{desc}</p>}
</div>
<div className="shrink-0">{children}</div>
</div>
);
}
const openLink = (url: string) => {
open(url).catch(() => {
window.open(url, "_blank");
});
};
const licenses = [
{ name: "Tauri", license: "MIT / Apache-2.0", url: "https://github.com/tauri-apps/tauri" },
{ name: "React", license: "MIT", url: "https://github.com/facebook/react" },
{ name: "@xmcl/core", license: "MIT", url: "https://github.com/VoxelCogs/xmcl" },
{ name: "@xmcl/installer", license: "MIT", url: "https://github.com/VoxelCogs/xmcl" },
{ name: "@xmcl/user", license: "MIT", url: "https://github.com/VoxelCogs/xmcl" },
{ name: "Zustand", license: "MIT", url: "https://github.com/pmndrs/zustand" },
{ name: "Tailwind CSS", license: "MIT", url: "https://github.com/tailwindlabs/tailwindcss" },
{ name: "shadcn/ui", license: "MIT", url: "https://github.com/shadcn-ui/ui" },
{ name: "Vite", license: "MIT", url: "https://github.com/vitejs/vite" },
{ name: "Lucide React", license: "ISC", url: "https://github.com/lucide-icons/lucide" },
];
const fonts = [
{ name: "Alimama ShuHeiTi", license: "商用免费授权", note: "主标题字体" },
{ name: "Geist", license: "OFL-1.1", note: "界面字体" },
{ name: "Inter", license: "OFL-1.1", note: "界面字体" },
];
export function CopyrightSetting() {
return (
<div>
<h2 className="text-xl font-bold text-foreground mb-4"></h2>
<p className="text-sm text-muted-foreground"></p>
<h2 className="text-xl font-bold text-foreground mb-1"></h2>
<p className="text-sm text-muted-foreground mb-6"></p>
<div className="space-y-6">
{/* 项目协议 */}
<div>
<h3 className="text-lg font-bold text-foreground mb-3"></h3>
<GlassCard>
<SettingRow label="MIT License" desc="Copyright © 2024 Koring Launcher Contributors">
<button
onClick={() => openLink("https://opensource.org/licenses/MIT")}
className="inline-flex items-center gap-1.5 text-[13px] text-primary hover:underline"
>
<ExternalLink className="w-3 h-3" />
</button>
</SettingRow>
</GlassCard>
</div>
{/* 开源依赖 */}
<div>
<h3 className="text-lg font-bold text-foreground mb-3"></h3>
<div className="space-y-2">
{licenses.map((dep) => (
<GlassCard key={dep.name}>
<SettingRow label={dep.name} desc={dep.license}>
<button
onClick={() => openLink(dep.url)}
className="inline-flex items-center gap-1 text-[13px] text-primary hover:underline"
>
<ExternalLink className="w-3 h-3" />
</button>
</SettingRow>
</GlassCard>
))}
</div>
</div>
{/* 字体 */}
<div>
<h3 className="text-lg font-bold text-foreground mb-3"></h3>
<div className="space-y-2">
{fonts.map((font) => (
<GlassCard key={font.name}>
<SettingRow label={font.name} desc={font.license}>
<span className="text-[12px] text-muted-foreground">{font.note}</span>
</SettingRow>
</GlassCard>
))}
</div>
</div>
</div>
</div>
);
}
+116 -3
View File
@@ -1,8 +1,121 @@
export function HomeSetting() {
import { useAuthStore } from "@/stores/authStore";
import { BUILD_MODE } from "@/lib/mode";
import { useUpdateStore } from "@/stores/updateStore";
import {
UserCircle,
Gamepad2,
Palette,
Download,
Globe,
Cpu,
Info,
ChevronRight,
} from "lucide-react";
function GlassCard({ children, className }: { children: React.ReactNode; className?: string }) {
return <div className={`glass-card px-5 py-4 ${className ?? ""}`}>{children}</div>;
}
interface ShortcutItem {
icon: React.ReactNode;
label: string;
desc: string;
navKey: string;
}
function ShortcutTile({ icon, label, desc, navKey, onClick }: ShortcutItem & { onClick: (key: string) => void }) {
return (
<button
onClick={() => onClick(navKey)}
className="glass-card flex items-center gap-3.5 px-4 py-3.5 text-left transition-all duration-150 hover:bg-foreground/[0.04] group"
>
<span className="flex items-center justify-center w-9 h-9 rounded-lg bg-foreground/[0.06] text-foreground/60 group-hover:text-foreground/80 shrink-0">
{icon}
</span>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-foreground truncate">{label}</p>
<p className="text-[12px] text-muted-foreground truncate">{desc}</p>
</div>
<ChevronRight className="w-4 h-4 text-foreground/25 shrink-0" />
</button>
);
}
const modeLabels: Record<string, string> = {
dev: "开发版",
beta: "测试版",
run: "正式版",
};
const shortcuts: ShortcutItem[] = [
{ icon: <UserCircle className="w-[18px] h-[18px]" />, label: "Koring 账户", desc: "同步数据、皮肤与个人配置", navKey: "account" },
{ icon: <Gamepad2 className="w-[18px] h-[18px]" />, label: "游戏账户与档案", desc: "管理游戏内账户和档案配置", navKey: "game-account" },
{ icon: <Palette className="w-[18px] h-[18px]" />, label: "主题与背景", desc: "深色模式、背景图片与视差", navKey: "theme-bg" },
{ icon: <Download className="w-[18px] h-[18px]" />, label: "下载设置", desc: "下载线程数与存储路径", navKey: "download" },
{ icon: <Globe className="w-[18px] h-[18px]" />, label: "联机功能", desc: "以太联机与陶瓦联机", navKey: "ether-online" },
{ icon: <Cpu className="w-[18px] h-[18px]" />, label: "Java 虚拟机与内存", desc: "Java 路径与内存分配", navKey: "java-mem" },
{ icon: <Info className="w-[18px] h-[18px]" />, label: "关于 Koring Launcher", desc: "版本信息与更新", navKey: "about" },
];
interface HomeSettingProps {
onNavigate: (key: string) => void;
}
export function HomeSetting({ onNavigate }: HomeSettingProps) {
const user = useAuthStore((s) => s.user);
const { update } = useUpdateStore();
return (
<div>
<h2 className="text-xl font-bold text-foreground mb-4"></h2>
<p className="text-sm text-muted-foreground"></p>
<h2 className="text-xl font-bold text-foreground mb-1"></h2>
<p className="text-sm text-muted-foreground mb-6"></p>
<div className="space-y-6">
{/* ===== 搜索栏占位 ===== */}
<div className="glass-card flex items-center gap-3 px-4 py-3">
<svg className="w-4 h-4 text-muted-foreground shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.3-4.3" />
</svg>
<span className="text-sm text-muted-foreground"></span>
</div>
{/* ===== 快捷状态 ===== */}
<div>
<h3 className="text-lg font-bold text-foreground mb-3"></h3>
<GlassCard>
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-full bg-foreground/[0.06] flex items-center justify-center shrink-0">
<UserCircle className="w-7 h-7 text-foreground/40" />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-foreground truncate">
{user?.username ?? "未登录"}
</p>
<p className="text-[12px] text-muted-foreground mt-0.5">
{user
? `${user.xboxProfile ? "Microsoft" : "离线"} 账户`
: "登录以同步数据和皮肤"}
</p>
</div>
<span className="text-[11px] text-muted-foreground shrink-0">
{modeLabels[BUILD_MODE] ?? BUILD_MODE}
{update ? " · 有更新" : ""}
</span>
</div>
</GlassCard>
</div>
{/* ===== 快捷入口 ===== */}
<div>
<h3 className="text-lg font-bold text-foreground mb-3"></h3>
<div className="grid grid-cols-1 gap-2">
{shortcuts.map((s) => (
<ShortcutTile key={s.navKey} {...s} onClick={onNavigate} />
))}
</div>
</div>
</div>
</div>
);
}
+80 -56
View File
@@ -1,4 +1,4 @@
import { useState, type ReactNode } from "react";
import { useState, useCallback, type ReactNode } from "react";
import {
Home,
UserCircle,
@@ -20,6 +20,7 @@ import {
Heart,
Code,
} from "lucide-react";
import { useRouteStore } from "@/stores/routeStore";
import { HomeSetting } from "./general/home";
import { AccountSetting } from "./general/account";
@@ -39,13 +40,13 @@ import { TawaOnlineSetting } from "./network/tawa-online";
import { SecurityIdSetting } from "./network/security-id";
import { FeedbackSetting } from "./other/feedback";
import { SponsorSetting } from "./other/sponsor";
import { DeveloperSetting } from "./other/developer";
interface MenuItem {
key: string;
label: string;
icon: ReactNode;
component: ReactNode;
component?: ReactNode;
onSelect?: () => void;
}
interface MenuGroup {
@@ -55,63 +56,84 @@ interface MenuGroup {
const iconCls = "w-[18px] h-[18px] shrink-0";
const menuData: MenuGroup[] = [
{
title: "通用",
items: [
{ key: "home", label: "主页", icon: <Home className={iconCls} />, component: <HomeSetting /> },
{ key: "account", label: "Koring Account", icon: <UserCircle className={iconCls} />, component: <AccountSetting /> },
{ key: "about", label: "关于", icon: <Info className={iconCls} />, component: <AboutSetting /> },
{ key: "copyright", label: "版权", icon: <Copyright className={iconCls} />, component: <CopyrightSetting /> },
],
},
{
title: "游戏",
items: [
{ key: "game-account", label: "游戏账户&档案", icon: <Gamepad2 className={iconCls} />, component: <GameAccountSetting /> },
{ key: "java-mem", label: "Java虚拟机与内存", icon: <Cpu className={iconCls} />, component: <JavaMemSetting /> },
{ key: "game-dir", label: "游戏目录", icon: <FolderOpen className={iconCls} />, component: <GameDirSetting /> },
{ key: "advanced", label: "高级设置", icon: <Settings className={iconCls} />, component: <AdvancedSetting /> },
],
},
{
title: "个性化",
items: [
{ key: "theme-bg", label: "主题与背景", icon: <Palette className={iconCls} />, component: <ThemeBgSetting /> },
{ key: "ui", label: "主界面", icon: <Monitor className={iconCls} />, component: <UiSetting /> },
{ key: "lang", label: "语言", icon: <Languages className={iconCls} />, component: <LangSetting /> },
{ key: "a11y", label: "辅助功能", icon: <Accessibility className={iconCls} />, component: <A11ySetting /> },
],
},
{
title: "网络",
items: [
{ key: "download", label: "下载", icon: <Download className={iconCls} />, component: <DownloadSetting /> },
{ key: "ether-online", label: "以太联机", icon: <Globe className={iconCls} />, component: <EtherOnlineSetting /> },
{ key: "tawa-online", label: "陶瓦联机", icon: <Network className={iconCls} />, component: <TawaOnlineSetting /> },
{ key: "security-id", label: "安全识别服务", icon: <Shield className={iconCls} />, component: <SecurityIdSetting /> },
],
},
{
title: "其他",
items: [
{ key: "feedback", label: "服务与反馈", icon: <MessageSquareHeart className={iconCls} />, component: <FeedbackSetting /> },
{ key: "sponsor", label: "赞助我们", icon: <Heart className={iconCls} />, component: <SponsorSetting /> },
{ key: "developer", label: "开发者选项", icon: <Code className={iconCls} />, component: <DeveloperSetting /> },
],
},
];
const allItems = menuData.flatMap((g) => g.items);
function buildMenuData(
switchPage: (key: string) => void,
routeNavigate: (key: any) => void,
): MenuGroup[] {
return [
{
title: "通用",
items: [
{ key: "home", label: "主页", icon: <Home className={iconCls} />, component: <HomeSetting onNavigate={switchPage} /> },
{ key: "account", label: "Koring 账户", icon: <UserCircle className={iconCls} />, component: <AccountSetting /> },
{ key: "about", label: "关于", icon: <Info className={iconCls} />, component: <AboutSetting /> },
{ key: "copyright", label: "版权", icon: <Copyright className={iconCls} />, component: <CopyrightSetting /> },
],
},
{
title: "游戏",
items: [
{ key: "game-account", label: "游戏账户&档案", icon: <Gamepad2 className={iconCls} />, component: <GameAccountSetting /> },
{ key: "java-mem", label: "Java虚拟机与内存", icon: <Cpu className={iconCls} />, component: <JavaMemSetting /> },
{ key: "game-dir", label: "游戏目录", icon: <FolderOpen className={iconCls} />, component: <GameDirSetting /> },
{ key: "advanced", label: "高级设置", icon: <Settings className={iconCls} />, component: <AdvancedSetting /> },
],
},
{
title: "个性化",
items: [
{ key: "theme-bg", label: "主题与背景", icon: <Palette className={iconCls} />, component: <ThemeBgSetting /> },
{ key: "ui", label: "主界面", icon: <Monitor className={iconCls} />, component: <UiSetting /> },
{ key: "lang", label: "语言", icon: <Languages className={iconCls} />, component: <LangSetting /> },
{ key: "a11y", label: "辅助功能", icon: <Accessibility className={iconCls} />, component: <A11ySetting /> },
],
},
{
title: "网络",
items: [
{ key: "download", label: "下载", icon: <Download className={iconCls} />, component: <DownloadSetting /> },
{ key: "ether-online", label: "以太联机", icon: <Globe className={iconCls} />, component: <EtherOnlineSetting /> },
{ key: "tawa-online", label: "陶瓦联机", icon: <Network className={iconCls} />, component: <TawaOnlineSetting /> },
{ key: "security-id", label: "安全识别服务", icon: <Shield className={iconCls} />, component: <SecurityIdSetting /> },
],
},
{
title: "其他",
items: [
{ key: "feedback", label: "服务与反馈", icon: <MessageSquareHeart className={iconCls} />, component: <FeedbackSetting /> },
{ key: "sponsor", label: "赞助我们", icon: <Heart className={iconCls} />, component: <SponsorSetting /> },
{ key: "developer", label: "开发者选项", icon: <Code className={iconCls} />, onSelect: () => routeNavigate("debug") },
],
},
];
}
export function Setting() {
const [selected, setSelected] = useState("home");
const [animKey, setAnimKey] = useState(0);
const routeNavigate = useRouteStore((s) => s.navigate);
const switchPage = useCallback((key: string) => {
if (key === selected) return;
setSelected(key);
setAnimKey((k) => k + 1);
}, [selected]);
const handleItemClick = useCallback((item: MenuItem) => {
if (item.onSelect) {
item.onSelect();
} else {
switchPage(item.key);
}
}, [switchPage]);
const menuData = buildMenuData(switchPage, routeNavigate);
const allItems = menuData.flatMap((g) => g.items);
const current = allItems.find((i) => i.key === selected);
return (
<div className="flex h-full">
{/* 侧边栏 */}
{/* 侧边栏 — 无动画 */}
<aside className="scroll-area w-[240px] shrink-0 h-full overflow-y-auto py-5 pl-5 pr-2">
<nav className="space-y-5">
{menuData.map((group) => (
@@ -121,11 +143,11 @@ export function Setting() {
</h3>
<div className="space-y-0.5">
{group.items.map((item) => {
const active = selected === item.key;
const active = selected === item.key && !item.onSelect;
return (
<button
key={item.key}
onClick={() => setSelected(item.key)}
onClick={() => handleItemClick(item)}
className={[
"w-full flex items-center gap-2.5 px-2.5 py-2 rounded-lg text-[14px] transition-all duration-150",
active
@@ -146,9 +168,11 @@ export function Setting() {
</nav>
</aside>
{/* 内容区 */}
{/* 内容区 — 仅内容有 fade 动画 */}
<main className="scroll-area flex-1 h-full overflow-y-auto p-8">
{current?.component}
<div key={animKey} className="setting-page-enter">
{current?.component}
</div>
</main>
</div>
);
+110 -2
View File
@@ -1,8 +1,116 @@
import { useState } from "react";
import { Slider } from "@/components/ui/slider";
function GlassCard({ children }: { children: React.ReactNode }) {
return <div className="glass-card px-5 py-4">{children}</div>;
}
const downloadSources = [
{ value: "mirror", label: "尽量使用镜像源(推荐国内用户)" },
{ value: "official", label: "优先使用官方源" },
{ value: "official-only", label: "仅使用官方源" },
];
export function DownloadSetting() {
const [fileSource, setFileSource] = useState("mirror");
const [versionSource, setVersionSource] = useState("mirror");
const [threads, setThreads] = useState(16);
const [speedLimit, setSpeedLimit] = useState("0");
return (
<div>
<h2 className="text-xl font-bold text-foreground mb-4"></h2>
<p className="text-sm text-muted-foreground"></p>
<h2 className="text-xl font-bold text-foreground mb-1"></h2>
<p className="text-sm text-muted-foreground mb-6"></p>
<div className="space-y-6">
{/* ===== 下载源 ===== */}
<div>
<h3 className="text-lg font-bold text-foreground mb-3"></h3>
<div className="space-y-3">
<GlassCard>
<div className="space-y-2">
<p className="text-sm font-medium text-foreground"></p>
<p className="text-[13px] text-muted-foreground">jarlib</p>
<div className="space-y-2 mt-2">
{downloadSources.map((opt) => (
<label key={opt.value} className="flex items-center gap-2 cursor-pointer">
<input type="radio" name="fileSource" checked={fileSource === opt.value} onChange={() => setFileSource(opt.value)} className="accent-primary" />
<span className="text-sm text-foreground">{opt.label}</span>
</label>
))}
</div>
</div>
</GlassCard>
<GlassCard>
<div className="space-y-2">
<p className="text-sm font-medium text-foreground"></p>
<p className="text-[13px] text-muted-foreground"></p>
<div className="space-y-2 mt-2">
{downloadSources.map((opt) => (
<label key={opt.value} className="flex items-center gap-2 cursor-pointer">
<input type="radio" name="versionSource" checked={versionSource === opt.value} onChange={() => setVersionSource(opt.value)} className="accent-primary" />
<span className="text-sm text-foreground">{opt.label}</span>
</label>
))}
</div>
</div>
</GlassCard>
</div>
</div>
{/* ===== 并发控制 ===== */}
<div>
<h3 className="text-lg font-bold text-foreground mb-3"></h3>
<div className="space-y-3">
<GlassCard>
<div>
<div className="flex items-center justify-between mb-1.5">
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-foreground">线</p>
<p className="text-[13px] text-muted-foreground mt-0.5"></p>
</div>
<span className="text-[13px] text-muted-foreground tabular-nums shrink-0 ml-4">{threads}</span>
</div>
<Slider
value={[threads]}
onValueChange={(v) => setThreads(Array.isArray(v) ? v[0] : v)}
min={1}
max={64}
step={1}
/>
<div className="flex justify-between mt-1">
<span className="text-[11px] text-muted-foreground/50">1</span>
<span className="text-[11px] text-muted-foreground/50">64</span>
</div>
</div>
</GlassCard>
</div>
</div>
{/* ===== 速度限制 ===== */}
<div>
<h3 className="text-lg font-bold text-foreground mb-3"></h3>
<div className="space-y-3">
<GlassCard>
<div className="space-y-2">
<p className="text-sm font-medium text-foreground"></p>
<p className="text-[13px] text-muted-foreground"> KB/s0 </p>
<div className="flex items-center gap-2">
<input
type="number"
value={speedLimit}
onChange={(e) => setSpeedLimit(e.target.value)}
min={0}
className="w-28 h-8 px-3 rounded-md border border-input bg-background text-sm font-mono placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring"
/>
<span className="text-[13px] text-muted-foreground">KB/s</span>
</div>
</div>
</GlassCard>
</div>
</div>
</div>
</div>
);
}
+2 -2
View File
@@ -1,8 +1,8 @@
export function EtherOnlineSetting() {
return (
<div>
<h2 className="text-xl font-bold text-foreground mb-4"></h2>
<p className="text-sm text-muted-foreground"></p>
<h2 className="text-xl font-bold text-foreground mb-1"></h2>
<p className="text-sm text-muted-foreground mb-6"></p>
</div>
);
}
+62 -3
View File
@@ -1,8 +1,67 @@
export function SecurityIdSetting() {
import { useState } from "react";
import { Switch } from "@/components/ui/switch";
import { ShieldCheck } from "lucide-react";
function GlassCard({ children }: { children: React.ReactNode }) {
return <div className="glass-card px-5 py-4">{children}</div>;
}
function SettingRow({ label, desc, children }: { label: string; desc?: string; children: React.ReactNode }) {
return (
<div className="flex items-center justify-between gap-4">
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-foreground">{label}</p>
{desc && <p className="text-[13px] text-muted-foreground mt-0.5">{desc}</p>}
</div>
<div className="shrink-0">{children}</div>
</div>
);
}
export function SecurityIdSetting() {
const [enabled, setEnabled] = useState(false);
const [authUrl, setAuthUrl] = useState("");
return (
<div>
<h2 className="text-xl font-bold text-foreground mb-4"></h2>
<p className="text-sm text-muted-foreground"></p>
<h2 className="text-xl font-bold text-foreground mb-1"></h2>
<p className="text-sm text-muted-foreground mb-6"></p>
<div className="space-y-6">
{/* ===== 认证服务 ===== */}
<div>
<h3 className="text-lg font-bold text-foreground mb-3"></h3>
<div className="space-y-3">
<GlassCard>
<SettingRow
label="启用第三方认证"
desc="使用自定义认证服务器替代 Microsoft 认证(适用于离线服务器)"
>
<Switch checked={enabled} onCheckedChange={setEnabled} />
</SettingRow>
</GlassCard>
{enabled && (
<GlassCard>
<div className="space-y-2">
<p className="text-sm font-medium text-foreground"> URL</p>
<p className="text-[13px] text-muted-foreground"></p>
<div className="flex items-center gap-2">
<ShieldCheck className="w-4 h-4 text-muted-foreground shrink-0" />
<input
type="text"
value={authUrl}
onChange={(e) => setAuthUrl(e.target.value)}
placeholder="https://auth.example.com"
className="flex-1 h-8 px-3 rounded-md border border-input bg-background text-sm font-mono placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring"
/>
</div>
</div>
</GlassCard>
)}
</div>
</div>
</div>
</div>
);
}
+2 -2
View File
@@ -1,8 +1,8 @@
export function TawaOnlineSetting() {
return (
<div>
<h2 className="text-xl font-bold text-foreground mb-4"></h2>
<p className="text-sm text-muted-foreground"></p>
<h2 className="text-xl font-bold text-foreground mb-1"></h2>
<p className="text-sm text-muted-foreground mb-6"></p>
</div>
);
}
-122
View File
@@ -1,122 +0,0 @@
import { useDevStore } from "@/stores/devStore";
import { Button } from "@/components/ui/button";
import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
function GlassCard({ children }: { children: React.ReactNode }) {
return <div className="glass-card px-5 py-4">{children}</div>;
}
function SettingRow({ label, desc, children }: { label: string; desc: string; children: React.ReactNode }) {
return (
<div className="flex items-center justify-between gap-4">
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-foreground">{label}</p>
<p className="text-[13px] text-muted-foreground mt-0.5">{desc}</p>
</div>
<div className="shrink-0">{children}</div>
</div>
);
}
const openSplash = async () => {
try {
const existing = await WebviewWindow.getByLabel("splashscreen");
if (existing) {
await existing.show();
await existing.setFocus();
return;
}
const splash = new WebviewWindow("splashscreen", {
url: "/splash.html",
width: 480,
height: 320,
decorations: false,
transparent: true,
center: true,
visible: true,
resizable: false,
minWidth: 480,
maxWidth: 480,
minHeight: 320,
maxHeight: 320,
} as any);
splash.once("tauri://error", (e) => {
console.error("Splash window error:", e);
});
} catch (err) {
console.error("Failed to open splash:", err);
}
};
const closeSplash = async () => {
try {
const splash = await WebviewWindow.getByLabel("splashscreen");
if (splash) {
await splash.close();
}
} catch (err) {
console.error("Failed to close splash:", err);
}
};
export function DeveloperSetting() {
const { forceDisableContentBlur, setForceDisableContentBlur } = useDevStore();
return (
<div>
<h2 className="text-xl font-bold text-foreground mb-6"></h2>
<div className="space-y-6">
{/* ===== 启动画面 ===== */}
<div>
<h3 className="text-lg font-bold text-foreground mb-3"></h3>
<div className="space-y-3">
<GlassCard>
<SettingRow
label="打开启动画面"
desc="立即创建并显示 Splash Screen 窗口(480×320"
>
<Button size="sm" onClick={openSplash}>
</Button>
</SettingRow>
</GlassCard>
<GlassCard>
<SettingRow
label="关闭启动画面"
desc="立即关闭当前显示的 Splash Screen 窗口"
>
<Button variant="destructive" size="sm" onClick={closeSplash}>
</Button>
</SettingRow>
</GlassCard>
</div>
</div>
{/* ===== 显示效果测试 ===== */}
<div>
<h3 className="text-lg font-bold text-foreground mb-3"></h3>
<div className="space-y-3">
<GlassCard>
<SettingRow
label="强制关闭背景「强内容模式」"
desc="覆盖系统设置,在设置页面也禁用背景模糊遮罩,用于对比测试"
>
<Button
variant={forceDisableContentBlur ? "default" : "outline"}
size="sm"
onClick={() => setForceDisableContentBlur(!forceDisableContentBlur)}
>
{forceDisableContentBlur ? "已开启" : "已关闭"}
</Button>
</SettingRow>
</GlassCard>
</div>
</div>
</div>
</div>
);
}
+2 -2
View File
@@ -1,8 +1,8 @@
export function FeedbackSetting() {
return (
<div>
<h2 className="text-xl font-bold text-foreground mb-4"></h2>
<p className="text-sm text-muted-foreground"></p>
<h2 className="text-xl font-bold text-foreground mb-1"></h2>
<p className="text-sm text-muted-foreground mb-6"></p>
</div>
);
}
+2 -2
View File
@@ -1,8 +1,8 @@
export function SponsorSetting() {
return (
<div>
<h2 className="text-xl font-bold text-foreground mb-4"></h2>
<p className="text-sm text-muted-foreground"> Koring Launcher </p>
<h2 className="text-xl font-bold text-foreground mb-1"></h2>
<p className="text-sm text-muted-foreground mb-6"> Koring Launcher </p>
</div>
);
}
+2 -2
View File
@@ -22,10 +22,10 @@ export function A11ySetting() {
return (
<div>
<h2 className="text-xl font-bold text-foreground mb-6"></h2>
<h2 className="text-xl font-bold text-foreground mb-1"></h2>
<p className="text-sm text-muted-foreground mb-6">使</p>
<div className="space-y-6">
{/* ===== 显示 ===== */}
<div>
<h3 className="text-lg font-bold text-foreground mb-3"></h3>
<div className="space-y-3">
+2 -2
View File
@@ -1,8 +1,8 @@
export function LangSetting() {
return (
<div>
<h2 className="text-xl font-bold text-foreground mb-4"></h2>
<p className="text-sm text-muted-foreground"></p>
<h2 className="text-xl font-bold text-foreground mb-1"></h2>
<p className="text-sm text-muted-foreground mb-6"></p>
</div>
);
}
+21 -1
View File
@@ -1,5 +1,6 @@
import { useThemeStore, type DarkMode } from "@/stores/themeStore";
import { useBackgroundStore } from "@/stores/backgroundStore";
import { useA11yStore } from "@/stores/a11yStore";
import { Switch } from "@/components/ui/switch";
import { Slider } from "@/components/ui/slider";
import { Button } from "@/components/ui/button";
@@ -107,6 +108,7 @@ function ThemePreviewCard({ mode, selected, onClick }: { mode: DarkMode; selecte
export function ThemeBgSetting() {
const { darkMode, setDarkMode, parallax, setParallax } = useThemeStore();
const { opacity, setOpacity, blur, setBlur, reset } = useBackgroundStore();
const { contentBlurOpacity, setContentBlurOpacity } = useA11yStore();
const handlePickImage = async () => {
// TODO: 打开文件选择器
@@ -118,7 +120,8 @@ export function ThemeBgSetting() {
return (
<div>
<h2 className="text-xl font-bold text-foreground mb-6"></h2>
<h2 className="text-xl font-bold text-foreground mb-1"></h2>
<p className="text-sm text-muted-foreground mb-6"></p>
<div className="space-y-6">
{/* ===== 深色模式 ===== */}
@@ -180,6 +183,23 @@ export function ThemeBgSetting() {
</SettingRow>
</GlassCard>
<GlassCard>
<div className="flex items-center justify-between gap-4">
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-foreground"></p>
<p className="text-[13px] text-muted-foreground mt-0.5"> {contentBlurOpacity}%</p>
</div>
<Slider
className="w-[180px] shrink-0"
value={[contentBlurOpacity]}
min={0}
max={100}
step={1}
onValueChange={(v) => setContentBlurOpacity(Array.isArray(v) ? v[0] : v)}
/>
</div>
</GlassCard>
<GlassCard>
<SettingRow label="恢复默认" desc="重置所有背景设置为初始状态">
<Button variant="destructive" size="sm" onClick={handleReset}>
+2 -2
View File
@@ -1,8 +1,8 @@
export function UiSetting() {
return (
<div>
<h2 className="text-xl font-bold text-foreground mb-4"></h2>
<p className="text-sm text-muted-foreground"></p>
<h2 className="text-xl font-bold text-foreground mb-1"></h2>
<p className="text-sm text-muted-foreground mb-6"></p>
</div>
);
}
+3 -8
View File
@@ -1,10 +1,5 @@
import { UnderConstruction } from "@/components/UnderConstruction";
export function Store() {
return (
<div className="flex items-center justify-center h-full">
<div className="text-center">
<h1 className="text-4xl font-bold mb-4 text-foreground"></h1>
<p className="text-muted-foreground">Mod / / </p>
</div>
</div>
);
return <UnderConstruction pageName="资源" description="Mod / 整合包 / 材质包,敬请期待" />;
}
+3 -8
View File
@@ -1,10 +1,5 @@
import { UnderConstruction } from "@/components/UnderConstruction";
export function Today() {
return (
<div className="flex items-center justify-center h-full">
<div className="text-center">
<h1 className="text-4xl font-bold mb-4 text-foreground"></h1>
<p className="text-muted-foreground"></p>
</div>
</div>
);
return <UnderConstruction pageName="资讯" description="Minecraft 新闻与社区动态" />;
}
+4
View File
@@ -4,17 +4,21 @@ interface A11yState {
reduceMotion: boolean;
reduceTransparency: boolean;
highContrast: boolean;
contentBlurOpacity: number;
setReduceMotion: (v: boolean) => void;
setReduceTransparency: (v: boolean) => void;
setHighContrast: (v: boolean) => void;
setContentBlurOpacity: (v: number) => void;
}
export const useA11yStore = create<A11yState>((set) => ({
reduceMotion: false,
reduceTransparency: false,
highContrast: false,
contentBlurOpacity: 50,
setReduceMotion: (v) => set({ reduceMotion: v }),
setReduceTransparency: (v) => set({ reduceTransparency: v }),
setHighContrast: (v) => set({ highContrast: v }),
setContentBlurOpacity: (v) => set({ contentBlurOpacity: v }),
}));
+26 -1
View File
@@ -1,12 +1,37 @@
import { create } from "zustand";
export type PreviewUpdateState = "latest" | "hasUpdate" | "installed";
interface DevState {
forceDisableContentBlur: boolean;
setForceDisableContentBlur: (v: boolean) => void;
previewMode: string | null;
setPreviewMode: (v: string | null) => void;
previewUpdateState: PreviewUpdateState | null;
setPreviewUpdateState: (v: PreviewUpdateState | null) => void;
overlayOpacity: number;
setOverlayOpacity: (v: number) => void;
blurAmount: number;
setBlurAmount: (v: number) => void;
}
export const useDevStore = create<DevState>((set) => ({
forceDisableContentBlur: false,
setForceDisableContentBlur: (v) => set({ forceDisableContentBlur: v }),
previewMode: null,
setPreviewMode: (v) => set({ previewMode: v }),
previewUpdateState: null,
setPreviewUpdateState: (v) => set({ previewUpdateState: v }),
overlayOpacity: 30,
setOverlayOpacity: (v) => set({ overlayOpacity: v }),
blurAmount: 12,
setBlurAmount: (v) => set({ blurAmount: v }),
}));
+20 -4
View File
@@ -1,6 +1,15 @@
import { create } from "zustand";
export type RouteKey = "home" | "store" | "today" | "play-link" | "setting" | "debug";
export type RouteKey =
| "home"
| "store"
| "today"
| "play-link"
| "setting"
| "debug"
| "debug-splash"
| "debug-display"
| "debug-version-card";
export type TitleBarMode = "default" | "sub" | "window";
@@ -24,12 +33,20 @@ export const routes: RouteItem[] = [
export const allRoutes: RouteItem[] = [
...routes,
{ key: "debug", label: "调试", path: "/debug", hidden: true },
{ key: "debug-splash", label: "启动动画调试", path: "/debug/splash", hidden: true },
{ key: "debug-display", label: "显示效果调试", path: "/debug/display", hidden: true },
{ key: "debug-version-card", label: "版本卡片调试", path: "/debug/version-card", hidden: true },
];
const parentMap: Partial<Record<RouteKey, RouteKey>> = {
debug: "setting",
"debug-splash": "debug",
"debug-display": "debug",
"debug-version-card": "debug",
};
const topLevelKeys = new Set(routes.map((r) => r.key));
function startTransition(dir: TransitionDirection, callback: () => void) {
document.documentElement.dataset.transitionDir = dir;
if (document.startViewTransition) {
@@ -54,11 +71,10 @@ export const useRouteStore = create<RouteState>((set, get) => ({
direction: "forward",
navigate: (key) => {
const parent = parentMap[key];
const isForward = parent !== undefined;
startTransition("forward", () => {
set({
current: key,
titleBarMode: isForward ? "sub" : "default",
titleBarMode: parent && !topLevelKeys.has(key) ? "sub" : "default",
direction: "forward",
});
});
@@ -71,7 +87,7 @@ export const useRouteStore = create<RouteState>((set, get) => ({
startTransition("backward", () => {
set({
current: parent,
titleBarMode: "default",
titleBarMode: topLevelKeys.has(parent) ? "default" : "sub",
direction: "backward",
});
});
+55
View File
@@ -0,0 +1,55 @@
import { create } from "zustand";
import {
checkForUpdates,
downloadAndInstall,
type DownloadProgress,
} from "../api/update";
import type { Update } from "@tauri-apps/plugin-updater";
interface UpdateState {
checking: boolean;
downloading: boolean;
installed: boolean;
progress: DownloadProgress | null;
update: Update | null;
error: string | null;
check: () => Promise<void>;
install: () => Promise<void>;
reset: () => void;
}
export const useUpdateStore = create<UpdateState>((set, get) => ({
checking: false,
downloading: false,
installed: false,
progress: null,
update: null,
error: null,
check: async () => {
set({ checking: true, error: null });
try {
const update = await checkForUpdates();
set({ update, checking: false });
} catch (e: any) {
set({ error: e.message ?? String(e), checking: false });
}
},
install: async () => {
const { update } = get();
if (!update) return;
set({ downloading: true, error: null, progress: null });
try {
await downloadAndInstall(update, (progress) => {
set({ progress });
});
set({ downloading: false, installed: true });
} catch (e: any) {
set({ error: e.message ?? String(e), downloading: false });
}
},
reset: () => set({ update: null, error: null, progress: null, installed: false }),
}));