Merge pull request 'dev-prepare' (#2) from dev-prepare into master

Reviewed-on: #2
This commit was merged in pull request #2.
This commit is contained in:
2025-03-24 02:16:40 +08:00
65 changed files with 9455 additions and 1188 deletions

3
.editorconfig Normal file
View File

@@ -0,0 +1,3 @@
[*.rs]
indent_style = tab
indent_size = 4

View File

@@ -36,7 +36,7 @@
"error",
"always"
],
"react-hooks/exhaustive-deps": "error",
"react-hooks/exhaustive-deps": "warn",
"@typescript-eslint/no-misused-promises": [
"error",
{

View File

@@ -1,24 +0,0 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2
},
"javascript": {
"formatter": {
"semicolons": "asNeeded",
"lineEnding": "lf",
"trailingCommas": "all"
}
}
}

BIN
bun.lockb

Binary file not shown.

View File

@@ -11,61 +11,60 @@
"next-build": "next build",
"tauri": "tauri",
"build": "tauri build",
"build-fast": "tauri build -b nsis -- --profile dev",
"dev": "tauri dev",
"lint": "next lint",
"fix": "next lint --fix",
"prepare": "husky"
"fix": "next lint --fix"
},
"dependencies": {
"@formkit/auto-animate": "^0.8.2",
"@heroui/react": "^2.7.5",
"@icon-park/react": "^1.4.2",
"@reactuses/core": "6.0.1",
"@supabase/ssr": "^0.5.2",
"@tauri-apps/api": "2.1.0",
"@tauri-apps/plugin-clipboard-manager": "2.0.0",
"@supabase/ssr": "0.6.1",
"@tauri-apps/api": "2.4.0",
"@tauri-apps/plugin-autostart": "^2.2.0",
"@tauri-apps/plugin-clipboard-manager": "2.2.2",
"@tauri-apps/plugin-deep-link": "~2.2.0",
"@tauri-apps/plugin-dialog": "~2.2.0",
"@tauri-apps/plugin-fs": "2.0.0",
"@tauri-apps/plugin-global-shortcut": "2.0.0",
"@tauri-apps/plugin-http": "2.0.1",
"@tauri-apps/plugin-notification": "2.0.0",
"@tauri-apps/plugin-os": "2.0.0",
"@tauri-apps/plugin-process": "2.0.0",
"@tauri-apps/plugin-shell": "2.0.1",
"@tauri-apps/plugin-fs": "2.2.0",
"@tauri-apps/plugin-global-shortcut": "2.2.0",
"@tauri-apps/plugin-http": "2.4.2",
"@tauri-apps/plugin-notification": "2.2.2",
"@tauri-apps/plugin-os": "2.2.1",
"@tauri-apps/plugin-process": "2.2.0",
"@tauri-apps/plugin-shell": "2.2.0",
"@tauri-apps/plugin-store": "^2.2.0",
"@tauri-store/valtio": "2.1.0",
"@types/throttle-debounce": "^5.0.2",
"ahooks": "^3.8.4",
"framer-motion": "^12.5.0",
"jotai": "^2.12.2",
"next": "15.2.0",
"next": "15.2.3",
"next-themes": "^0.4.6",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"swr": "^2.3.3",
"tauri-plugin-system-info-api": "^2.0.9",
"tauri-plugin-valtio": "1.1.1",
"throttle-debounce": "^5.0.2",
"zustand": "5.0.1"
"tauri-plugin-system-info-api": "^2.0.10"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@tauri-apps/cli": "^2.3.1",
"@tauri-apps/cli": "^2.4.0",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@testing-library/user-event": "^14.6.1",
"@types/jest": "^29.5.14",
"@types/node": "^22.13.10",
"@types/node": "^22.13.11",
"@types/react": "19.0.10",
"@types/react-dom": "19.0.4",
"@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.26.1",
"@typescript-eslint/eslint-plugin": "^8.27.0",
"@typescript-eslint/parser": "^8.27.0",
"autoprefixer": "^10.4.21",
"clsx": "^2.1.1",
"cross-env": "^7.0.3",
"cssnano": "^7.0.6",
"eslint": "9.14.0",
"eslint-config-next": "15.0.3",
"husky": "^9.1.7",
"lint-staged": "^15.4.3",
"eslint": "9.23.0",
"eslint-config-next": "15.2.3",
"lint-staged": "^15.5.0",
"postcss": "^8.5.3",
"postcss-import": "^16.1.0",
"postcss-nesting": "^13.0.1",
@@ -85,4 +84,4 @@
"last 1 safari version"
]
}
}
}

BIN
public/logo_rounded.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
public/logo_square.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

2093
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,17 +7,32 @@ license = ""
repository = ""
default-run = "CS工具箱"
edition = "2021"
rust-version = "1.66"
rust-version = "1.85"
[profile.release]
panic = "abort" # Strip expensive panic clean-up logic
codegen-units = 1 # Compile crates one after another so the compiler can optimize better
lto = true # Enables link to optimizations
opt-level = "s" # Optimize for binary size
strip = true # Remove debug symbols
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[profile.dev]
opt-level = 0 # 关闭优化
debug = true # 保留调试信息
[build-dependencies]
tauri-build = { version = "2.0.6", features = [] }
tauri-build = { version = "2.1.0", features = [] }
[dependencies]
log = "0.4.26"
base64 = "0.22.1"
walkdir = "2.5.0"
serde_json = "1.0.140"
regex = "1.11.1"
serde = { version = "1.0.219", features = ["derive"] }
tauri = { version = "2.3.1", features = [ "macos-private-api",
reqwest = { version = "0.12.15", features = ["blocking"] }
tauri = { version = "2.4.0", features = [ "macos-private-api",
"tray-icon"
] }
window-vibrancy = "0.6.0"
@@ -27,12 +42,16 @@ tauri-plugin-dialog = "2.2.0"
tauri-plugin-os = "2.2.1"
tauri-plugin-clipboard-manager = "2.2.2"
tauri-plugin-shell = "2.2.0"
tauri-plugin-http = "2.4.0"
tauri-plugin-http = "2.4.2"
tauri-plugin-notification = "2.2.2"
tauri-plugin-valtio = "1.1.1"
tauri-plugin-valtio = "2.1.0"
tauri-plugin-store = "2.2.0"
tauri-plugin-system-info = "2.0.9"
tauri-plugin-theme = "2.1.3"
tauri-plugin-autostart = "2.2.0"
tauri-plugin-single-instance = { version = "2.2.2", features = ["deep-link"] }
tauri-plugin-deep-link = "2.2.0"
anyhow = "1.0.97"
[target.'cfg(windows)'.dependencies] # Windows Only
winreg = "0.55.0"
@@ -46,3 +65,4 @@ custom-protocol = [ "tauri/custom-protocol" ]
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-global-shortcut = "2.2.0"
tauri-plugin-single-instance = "2.2.2"

View File

@@ -5,7 +5,9 @@
"windows",
"linux"
],
"windows": ["main"],
"windows": [
"main"
],
"permissions": [
"global-shortcut:default",
"theme:default",
@@ -19,6 +21,12 @@
"store:allow-save",
"store:allow-load",
"store:allow-reset",
"store:allow-entries"
"store:allow-entries",
"deep-link:default",
"deep-link:allow-register",
"deep-link:allow-get-current",
"autostart:default",
"autostart:allow-enable",
"autostart:allow-disable"
]
}

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
{"desktop-capability":{"identifier":"desktop-capability","description":"","local":true,"windows":["main"],"permissions":["global-shortcut:default","theme:default","store:default","store:allow-set","store:allow-get-store","store:allow-has","store:allow-delete","store:allow-clear","store:allow-values","store:allow-save","store:allow-load","store:allow-reset","store:allow-entries"],"platforms":["macOS","windows","linux"]},"migrated":{"identifier":"migrated","description":"permissions that were migrated from v1","local":true,"windows":["main"],"permissions":["core:default","fs:allow-read-file","fs:allow-write-file","fs:allow-read-dir","fs:allow-copy-file","fs:allow-mkdir","fs:allow-remove","fs:allow-remove","fs:allow-rename","fs:allow-exists","core:window:allow-create","core:window:allow-center","core:window:allow-request-user-attention","core:window:allow-set-resizable","core:window:allow-set-maximizable","core:window:allow-set-minimizable","core:window:allow-set-closable","core:window:allow-set-title","core:window:allow-maximize","core:window:allow-unmaximize","core:window:allow-minimize","core:window:allow-unminimize","core:window:allow-show","core:window:allow-hide","core:window:allow-close","core:window:allow-set-decorations","core:window:allow-set-always-on-top","core:window:allow-set-content-protected","core:window:allow-set-size","core:window:allow-set-min-size","core:window:allow-set-max-size","core:window:allow-set-position","core:window:allow-set-fullscreen","core:window:allow-set-focus","core:window:allow-set-icon","core:window:allow-set-skip-taskbar","core:window:allow-set-cursor-grab","core:window:allow-set-cursor-visible","core:window:allow-set-cursor-icon","core:window:allow-set-cursor-position","core:window:allow-set-ignore-cursor-events","core:window:allow-start-dragging","core:webview:allow-print","shell:allow-execute","shell:allow-open","dialog:allow-open","dialog:allow-save","dialog:allow-message","dialog:allow-ask","dialog:allow-confirm","http:default","notification:default","global-shortcut:allow-is-registered","global-shortcut:allow-register","global-shortcut:allow-register-all","global-shortcut:allow-unregister","global-shortcut:allow-unregister-all","os:allow-platform","os:allow-version","os:allow-os-type","os:allow-family","os:allow-arch","os:allow-exe-extension","os:allow-locale","os:allow-hostname","process:allow-restart","process:allow-exit","clipboard-manager:allow-read-text","clipboard-manager:allow-write-text","core:app:allow-app-show","core:app:allow-app-hide","core:app:allow-set-app-theme","process:default","fs:default","dialog:default","os:default","clipboard-manager:default"]},"system-info":{"identifier":"system-info","description":"","local":true,"windows":["*"],"permissions":["system-info:allow-all"]},"valtio":{"identifier":"valtio","description":"","local":true,"windows":["*"],"permissions":["valtio:default","core:event:default"]}}
{"desktop-capability":{"identifier":"desktop-capability","description":"","local":true,"windows":["main"],"permissions":["global-shortcut:default","theme:default","store:default","store:allow-set","store:allow-get-store","store:allow-has","store:allow-delete","store:allow-clear","store:allow-values","store:allow-save","store:allow-load","store:allow-reset","store:allow-entries","deep-link:default","deep-link:allow-register","deep-link:allow-get-current","autostart:default","autostart:allow-enable","autostart:allow-disable"],"platforms":["macOS","windows","linux"]},"migrated":{"identifier":"migrated","description":"permissions that were migrated from v1","local":true,"windows":["main"],"permissions":["core:default","fs:allow-read-file","fs:allow-write-file","fs:allow-read-dir","fs:allow-copy-file","fs:allow-mkdir","fs:allow-remove","fs:allow-remove","fs:allow-rename","fs:allow-exists","core:window:allow-create","core:window:allow-center","core:window:allow-request-user-attention","core:window:allow-set-resizable","core:window:allow-set-maximizable","core:window:allow-set-minimizable","core:window:allow-set-closable","core:window:allow-set-title","core:window:allow-maximize","core:window:allow-unmaximize","core:window:allow-minimize","core:window:allow-unminimize","core:window:allow-show","core:window:allow-hide","core:window:allow-close","core:window:allow-set-decorations","core:window:allow-set-always-on-top","core:window:allow-set-content-protected","core:window:allow-set-size","core:window:allow-set-min-size","core:window:allow-set-max-size","core:window:allow-set-position","core:window:allow-set-fullscreen","core:window:allow-set-focus","core:window:allow-set-icon","core:window:allow-set-skip-taskbar","core:window:allow-set-cursor-grab","core:window:allow-set-cursor-visible","core:window:allow-set-cursor-icon","core:window:allow-set-cursor-position","core:window:allow-set-ignore-cursor-events","core:window:allow-start-dragging","core:webview:allow-print","shell:allow-execute","shell:allow-open","dialog:allow-open","dialog:allow-save","dialog:allow-message","dialog:allow-ask","dialog:allow-confirm","http:default","notification:default","global-shortcut:allow-is-registered","global-shortcut:allow-register","global-shortcut:allow-register-all","global-shortcut:allow-unregister","global-shortcut:allow-unregister-all","os:allow-platform","os:allow-version","os:allow-os-type","os:allow-family","os:allow-arch","os:allow-exe-extension","os:allow-locale","os:allow-hostname","process:allow-restart","process:allow-exit","clipboard-manager:allow-read-text","clipboard-manager:allow-write-text","core:app:allow-app-show","core:app:allow-app-hide","core:app:allow-set-app-theme","process:default","fs:default","dialog:default","os:default","clipboard-manager:default"]},"system-info":{"identifier":"system-info","description":"","local":true,"windows":["*"],"permissions":["system-info:allow-all"]},"valtio":{"identifier":"valtio","description":"","local":true,"windows":["*"],"permissions":["valtio:default","core:event:default"]}}

View File

@@ -37,7 +37,7 @@
],
"definitions": {
"Capability": {
"description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows fine grained access to the Tauri core, application, or plugin commands. If a window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```",
"description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```",
"type": "object",
"required": [
"identifier",
@@ -70,14 +70,14 @@
"type": "boolean"
},
"windows": {
"description": "List of windows that are affected by this capability. Can be a glob pattern.\n\nOn multiwebview windows, prefer [`Self::webviews`] for a fine grained access control.\n\n## Example\n\n`[\"main\"]`",
"description": "List of windows that are affected by this capability. Can be a glob pattern.\n\nIf a window label matches any of the patterns in this list, the capability will be enabled on all the webviews of that window, regardless of the value of [`Self::webviews`].\n\nOn multiwebview windows, prefer specifying [`Self::webviews`] and omitting [`Self::windows`] for a fine grained access control.\n\n## Example\n\n`[\"main\"]`",
"type": "array",
"items": {
"type": "string"
}
},
"webviews": {
"description": "List of webviews that are affected by this capability. Can be a glob pattern.\n\nThis is only required when using on multiwebview contexts, by default all child webviews of a window that matches [`Self::windows`] are linked.\n\n## Example\n\n`[\"sub-webview-one\", \"sub-webview-two\"]`",
"description": "List of webviews that are affected by this capability. Can be a glob pattern.\n\nThe capability will be enabled on all the webviews whose label matches any of the patterns in this list, regardless of whether the webview's window label matches a pattern in [`Self::windows`].\n\n## Example\n\n`[\"sub-webview-one\", \"sub-webview-two\"]`",
"type": "array",
"items": {
"type": "string"
@@ -2004,6 +2004,41 @@
"Identifier": {
"description": "Permission identifier",
"oneOf": [
{
"description": "This permission set configures if your\napplication can enable or disable auto\nstarting the application on boot.\n\n#### Granted Permissions\n\nIt allows all to check, enable and\ndisable the automatic start on boot.\n\n",
"type": "string",
"const": "autostart:default"
},
{
"description": "Enables the disable command without any pre-configured scope.",
"type": "string",
"const": "autostart:allow-disable"
},
{
"description": "Enables the enable command without any pre-configured scope.",
"type": "string",
"const": "autostart:allow-enable"
},
{
"description": "Enables the is_enabled command without any pre-configured scope.",
"type": "string",
"const": "autostart:allow-is-enabled"
},
{
"description": "Denies the disable command without any pre-configured scope.",
"type": "string",
"const": "autostart:deny-disable"
},
{
"description": "Denies the enable command without any pre-configured scope.",
"type": "string",
"const": "autostart:deny-enable"
},
{
"description": "Denies the is_enabled command without any pre-configured scope.",
"type": "string",
"const": "autostart:deny-is-enabled"
},
{
"description": "No features are enabled by default, as we believe\nthe clipboard can be inherently dangerous and it is \napplication specific if read and/or write access is needed.\n\nClipboard interaction needs to be explicitly enabled.\n",
"type": "string",
@@ -2094,11 +2129,26 @@
"type": "string",
"const": "core:app:allow-default-window-icon"
},
{
"description": "Enables the fetch_data_store_identifiers command without any pre-configured scope.",
"type": "string",
"const": "core:app:allow-fetch-data-store-identifiers"
},
{
"description": "Enables the identifier command without any pre-configured scope.",
"type": "string",
"const": "core:app:allow-identifier"
},
{
"description": "Enables the name command without any pre-configured scope.",
"type": "string",
"const": "core:app:allow-name"
},
{
"description": "Enables the remove_data_store command without any pre-configured scope.",
"type": "string",
"const": "core:app:allow-remove-data-store"
},
{
"description": "Enables the set_app_theme command without any pre-configured scope.",
"type": "string",
@@ -2129,11 +2179,26 @@
"type": "string",
"const": "core:app:deny-default-window-icon"
},
{
"description": "Denies the fetch_data_store_identifiers command without any pre-configured scope.",
"type": "string",
"const": "core:app:deny-fetch-data-store-identifiers"
},
{
"description": "Denies the identifier command without any pre-configured scope.",
"type": "string",
"const": "core:app:deny-identifier"
},
{
"description": "Denies the name command without any pre-configured scope.",
"type": "string",
"const": "core:app:deny-name"
},
{
"description": "Denies the remove_data_store command without any pre-configured scope.",
"type": "string",
"const": "core:app:deny-remove-data-store"
},
{
"description": "Denies the set_app_theme command without any pre-configured scope.",
"type": "string",
@@ -2929,6 +2994,11 @@
"type": "string",
"const": "core:window:allow-internal-toggle-maximize"
},
{
"description": "Enables the is_always_on_top command without any pre-configured scope.",
"type": "string",
"const": "core:window:allow-is-always-on-top"
},
{
"description": "Enables the is_closable command without any pre-configured scope.",
"type": "string",
@@ -3294,6 +3364,11 @@
"type": "string",
"const": "core:window:deny-internal-toggle-maximize"
},
{
"description": "Denies the is_always_on_top command without any pre-configured scope.",
"type": "string",
"const": "core:window:deny-is-always-on-top"
},
{
"description": "Denies the is_closable command without any pre-configured scope.",
"type": "string",
@@ -3599,6 +3674,51 @@
"type": "string",
"const": "core:window:deny-unminimize"
},
{
"description": "Allows reading the opened deep link via the get_current command",
"type": "string",
"const": "deep-link:default"
},
{
"description": "Enables the get_current command without any pre-configured scope.",
"type": "string",
"const": "deep-link:allow-get-current"
},
{
"description": "Enables the is_registered command without any pre-configured scope.",
"type": "string",
"const": "deep-link:allow-is-registered"
},
{
"description": "Enables the register command without any pre-configured scope.",
"type": "string",
"const": "deep-link:allow-register"
},
{
"description": "Enables the unregister command without any pre-configured scope.",
"type": "string",
"const": "deep-link:allow-unregister"
},
{
"description": "Denies the get_current command without any pre-configured scope.",
"type": "string",
"const": "deep-link:deny-get-current"
},
{
"description": "Denies the is_registered command without any pre-configured scope.",
"type": "string",
"const": "deep-link:deny-is-registered"
},
{
"description": "Denies the register command without any pre-configured scope.",
"type": "string",
"const": "deep-link:deny-register"
},
{
"description": "Denies the unregister command without any pre-configured scope.",
"type": "string",
"const": "deep-link:deny-unregister"
},
{
"description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n",
"type": "string",
@@ -5969,6 +6089,11 @@
"type": "string",
"const": "valtio:allow-get-save-strategy"
},
{
"description": "Enables the get_store_collection_path command without any pre-configured scope.",
"type": "string",
"const": "valtio:allow-get-store-collection-path"
},
{
"description": "Enables the get_store_ids command without any pre-configured scope.",
"type": "string",
@@ -5984,11 +6109,6 @@
"type": "string",
"const": "valtio:allow-get-store-state"
},
{
"description": "Enables the get_valtio_path command without any pre-configured scope.",
"type": "string",
"const": "valtio:allow-get-valtio-path"
},
{
"description": "Enables the load command without any pre-configured scope.",
"type": "string",
@@ -6039,16 +6159,16 @@
"type": "string",
"const": "valtio:allow-set-save-strategy"
},
{
"description": "Enables the set_store_collection_path command without any pre-configured scope.",
"type": "string",
"const": "valtio:allow-set-store-collection-path"
},
{
"description": "Enables the set_store_options command without any pre-configured scope.",
"type": "string",
"const": "valtio:allow-set-store-options"
},
{
"description": "Enables the set_valtio_path command without any pre-configured scope.",
"type": "string",
"const": "valtio:allow-set-valtio-path"
},
{
"description": "Enables the unload command without any pre-configured scope.",
"type": "string",
@@ -6069,6 +6189,11 @@
"type": "string",
"const": "valtio:deny-get-save-strategy"
},
{
"description": "Denies the get_store_collection_path command without any pre-configured scope.",
"type": "string",
"const": "valtio:deny-get-store-collection-path"
},
{
"description": "Denies the get_store_ids command without any pre-configured scope.",
"type": "string",
@@ -6084,11 +6209,6 @@
"type": "string",
"const": "valtio:deny-get-store-state"
},
{
"description": "Denies the get_valtio_path command without any pre-configured scope.",
"type": "string",
"const": "valtio:deny-get-valtio-path"
},
{
"description": "Denies the load command without any pre-configured scope.",
"type": "string",
@@ -6139,16 +6259,16 @@
"type": "string",
"const": "valtio:deny-set-save-strategy"
},
{
"description": "Denies the set_store_collection_path command without any pre-configured scope.",
"type": "string",
"const": "valtio:deny-set-store-collection-path"
},
{
"description": "Denies the set_store_options command without any pre-configured scope.",
"type": "string",
"const": "valtio:deny-set-store-options"
},
{
"description": "Denies the set_valtio_path command without any pre-configured scope.",
"type": "string",
"const": "valtio:deny-set-valtio-path"
},
{
"description": "Denies the unload command without any pre-configured scope.",
"type": "string",

View File

@@ -37,7 +37,7 @@
],
"definitions": {
"Capability": {
"description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows fine grained access to the Tauri core, application, or plugin commands. If a window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```",
"description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```",
"type": "object",
"required": [
"identifier",
@@ -70,14 +70,14 @@
"type": "boolean"
},
"windows": {
"description": "List of windows that are affected by this capability. Can be a glob pattern.\n\nOn multiwebview windows, prefer [`Self::webviews`] for a fine grained access control.\n\n## Example\n\n`[\"main\"]`",
"description": "List of windows that are affected by this capability. Can be a glob pattern.\n\nIf a window label matches any of the patterns in this list, the capability will be enabled on all the webviews of that window, regardless of the value of [`Self::webviews`].\n\nOn multiwebview windows, prefer specifying [`Self::webviews`] and omitting [`Self::windows`] for a fine grained access control.\n\n## Example\n\n`[\"main\"]`",
"type": "array",
"items": {
"type": "string"
}
},
"webviews": {
"description": "List of webviews that are affected by this capability. Can be a glob pattern.\n\nThis is only required when using on multiwebview contexts, by default all child webviews of a window that matches [`Self::windows`] are linked.\n\n## Example\n\n`[\"sub-webview-one\", \"sub-webview-two\"]`",
"description": "List of webviews that are affected by this capability. Can be a glob pattern.\n\nThe capability will be enabled on all the webviews whose label matches any of the patterns in this list, regardless of whether the webview's window label matches a pattern in [`Self::windows`].\n\n## Example\n\n`[\"sub-webview-one\", \"sub-webview-two\"]`",
"type": "array",
"items": {
"type": "string"
@@ -2004,6 +2004,41 @@
"Identifier": {
"description": "Permission identifier",
"oneOf": [
{
"description": "This permission set configures if your\napplication can enable or disable auto\nstarting the application on boot.\n\n#### Granted Permissions\n\nIt allows all to check, enable and\ndisable the automatic start on boot.\n\n",
"type": "string",
"const": "autostart:default"
},
{
"description": "Enables the disable command without any pre-configured scope.",
"type": "string",
"const": "autostart:allow-disable"
},
{
"description": "Enables the enable command without any pre-configured scope.",
"type": "string",
"const": "autostart:allow-enable"
},
{
"description": "Enables the is_enabled command without any pre-configured scope.",
"type": "string",
"const": "autostart:allow-is-enabled"
},
{
"description": "Denies the disable command without any pre-configured scope.",
"type": "string",
"const": "autostart:deny-disable"
},
{
"description": "Denies the enable command without any pre-configured scope.",
"type": "string",
"const": "autostart:deny-enable"
},
{
"description": "Denies the is_enabled command without any pre-configured scope.",
"type": "string",
"const": "autostart:deny-is-enabled"
},
{
"description": "No features are enabled by default, as we believe\nthe clipboard can be inherently dangerous and it is \napplication specific if read and/or write access is needed.\n\nClipboard interaction needs to be explicitly enabled.\n",
"type": "string",
@@ -2094,11 +2129,26 @@
"type": "string",
"const": "core:app:allow-default-window-icon"
},
{
"description": "Enables the fetch_data_store_identifiers command without any pre-configured scope.",
"type": "string",
"const": "core:app:allow-fetch-data-store-identifiers"
},
{
"description": "Enables the identifier command without any pre-configured scope.",
"type": "string",
"const": "core:app:allow-identifier"
},
{
"description": "Enables the name command without any pre-configured scope.",
"type": "string",
"const": "core:app:allow-name"
},
{
"description": "Enables the remove_data_store command without any pre-configured scope.",
"type": "string",
"const": "core:app:allow-remove-data-store"
},
{
"description": "Enables the set_app_theme command without any pre-configured scope.",
"type": "string",
@@ -2129,11 +2179,26 @@
"type": "string",
"const": "core:app:deny-default-window-icon"
},
{
"description": "Denies the fetch_data_store_identifiers command without any pre-configured scope.",
"type": "string",
"const": "core:app:deny-fetch-data-store-identifiers"
},
{
"description": "Denies the identifier command without any pre-configured scope.",
"type": "string",
"const": "core:app:deny-identifier"
},
{
"description": "Denies the name command without any pre-configured scope.",
"type": "string",
"const": "core:app:deny-name"
},
{
"description": "Denies the remove_data_store command without any pre-configured scope.",
"type": "string",
"const": "core:app:deny-remove-data-store"
},
{
"description": "Denies the set_app_theme command without any pre-configured scope.",
"type": "string",
@@ -2929,6 +2994,11 @@
"type": "string",
"const": "core:window:allow-internal-toggle-maximize"
},
{
"description": "Enables the is_always_on_top command without any pre-configured scope.",
"type": "string",
"const": "core:window:allow-is-always-on-top"
},
{
"description": "Enables the is_closable command without any pre-configured scope.",
"type": "string",
@@ -3294,6 +3364,11 @@
"type": "string",
"const": "core:window:deny-internal-toggle-maximize"
},
{
"description": "Denies the is_always_on_top command without any pre-configured scope.",
"type": "string",
"const": "core:window:deny-is-always-on-top"
},
{
"description": "Denies the is_closable command without any pre-configured scope.",
"type": "string",
@@ -3599,6 +3674,51 @@
"type": "string",
"const": "core:window:deny-unminimize"
},
{
"description": "Allows reading the opened deep link via the get_current command",
"type": "string",
"const": "deep-link:default"
},
{
"description": "Enables the get_current command without any pre-configured scope.",
"type": "string",
"const": "deep-link:allow-get-current"
},
{
"description": "Enables the is_registered command without any pre-configured scope.",
"type": "string",
"const": "deep-link:allow-is-registered"
},
{
"description": "Enables the register command without any pre-configured scope.",
"type": "string",
"const": "deep-link:allow-register"
},
{
"description": "Enables the unregister command without any pre-configured scope.",
"type": "string",
"const": "deep-link:allow-unregister"
},
{
"description": "Denies the get_current command without any pre-configured scope.",
"type": "string",
"const": "deep-link:deny-get-current"
},
{
"description": "Denies the is_registered command without any pre-configured scope.",
"type": "string",
"const": "deep-link:deny-is-registered"
},
{
"description": "Denies the register command without any pre-configured scope.",
"type": "string",
"const": "deep-link:deny-register"
},
{
"description": "Denies the unregister command without any pre-configured scope.",
"type": "string",
"const": "deep-link:deny-unregister"
},
{
"description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n",
"type": "string",
@@ -5924,6 +6044,31 @@
"type": "string",
"const": "system-info:deny-used-swap"
},
{
"description": "Allow all",
"type": "string",
"const": "theme:default"
},
{
"description": "Enables the get_theme command without any pre-configured scope.",
"type": "string",
"const": "theme:allow-get-theme"
},
{
"description": "Enables the set_theme command without any pre-configured scope.",
"type": "string",
"const": "theme:allow-set-theme"
},
{
"description": "Denies the get_theme command without any pre-configured scope.",
"type": "string",
"const": "theme:deny-get-theme"
},
{
"description": "Denies the set_theme command without any pre-configured scope.",
"type": "string",
"const": "theme:deny-set-theme"
},
{
"description": "Default permissions for tauri-plugin-valtio.",
"type": "string",
@@ -5944,6 +6089,11 @@
"type": "string",
"const": "valtio:allow-get-save-strategy"
},
{
"description": "Enables the get_store_collection_path command without any pre-configured scope.",
"type": "string",
"const": "valtio:allow-get-store-collection-path"
},
{
"description": "Enables the get_store_ids command without any pre-configured scope.",
"type": "string",
@@ -5959,11 +6109,6 @@
"type": "string",
"const": "valtio:allow-get-store-state"
},
{
"description": "Enables the get_valtio_path command without any pre-configured scope.",
"type": "string",
"const": "valtio:allow-get-valtio-path"
},
{
"description": "Enables the load command without any pre-configured scope.",
"type": "string",
@@ -6014,16 +6159,16 @@
"type": "string",
"const": "valtio:allow-set-save-strategy"
},
{
"description": "Enables the set_store_collection_path command without any pre-configured scope.",
"type": "string",
"const": "valtio:allow-set-store-collection-path"
},
{
"description": "Enables the set_store_options command without any pre-configured scope.",
"type": "string",
"const": "valtio:allow-set-store-options"
},
{
"description": "Enables the set_valtio_path command without any pre-configured scope.",
"type": "string",
"const": "valtio:allow-set-valtio-path"
},
{
"description": "Enables the unload command without any pre-configured scope.",
"type": "string",
@@ -6044,6 +6189,11 @@
"type": "string",
"const": "valtio:deny-get-save-strategy"
},
{
"description": "Denies the get_store_collection_path command without any pre-configured scope.",
"type": "string",
"const": "valtio:deny-get-store-collection-path"
},
{
"description": "Denies the get_store_ids command without any pre-configured scope.",
"type": "string",
@@ -6059,11 +6209,6 @@
"type": "string",
"const": "valtio:deny-get-store-state"
},
{
"description": "Denies the get_valtio_path command without any pre-configured scope.",
"type": "string",
"const": "valtio:deny-get-valtio-path"
},
{
"description": "Denies the load command without any pre-configured scope.",
"type": "string",
@@ -6114,16 +6259,16 @@
"type": "string",
"const": "valtio:deny-set-save-strategy"
},
{
"description": "Denies the set_store_collection_path command without any pre-configured scope.",
"type": "string",
"const": "valtio:deny-set-store-collection-path"
},
{
"description": "Denies the set_store_options command without any pre-configured scope.",
"type": "string",
"const": "valtio:deny-set-store-options"
},
{
"description": "Denies the set_valtio_path command without any pre-configured scope.",
"type": "string",
"const": "valtio:deny-set-valtio-path"
},
{
"description": "Denies the unload command without any pre-configured scope.",
"type": "string",

View File

@@ -1,57 +1,84 @@
use crate::steam;
use crate::tool::*;
use crate::vdf::preset;
use crate::wrap_err;
use anyhow::Result;
// pub type Result<T, String = ()> = Result<T, String>;
#[tauri::command]
pub fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
pub fn greet(name: &str) -> Result<String, String> {
Ok(format!("Hello, {}! You've been greeted from Rust!", name))
}
// 工具
#[tauri::command]
pub fn launch_game(steam_path: &str, launch_option: &str, server: &str) -> String {
steam::launch_game(steam_path, launch_option, server).expect("启动失败");
pub fn launch_game(steam_path: &str, launch_option: &str, server: &str) -> Result<String, String> {
wrap_err!(steam::launch_game(steam_path, launch_option, server));
format!(
Ok(format!(
"Launching game on server: {}, with launch Option {}",
server, launch_option
)
))
}
#[tauri::command]
pub fn kill_game() -> String {
common::kill("cs2.exe")
pub fn kill_game() -> Result<String, String> {
Ok(common::kill("cs2.exe"))
}
#[tauri::command]
pub fn kill_steam() -> String {
common::kill("steam.exe")
pub fn kill_steam() -> Result<String, String> {
Ok(common::kill("steam.exe"))
}
// Steam
#[tauri::command]
pub fn get_steam_path() -> String {
steam::path::get_steam_path().expect("获取Steam路径失败")
pub fn get_steam_path() -> Result<String, String> {
wrap_err!(steam::path::get_steam_path())
}
// TODO get_cs_path
// TODO get_powerplan
// TODO set_powerplan
// TODO watch_steam_users
// TODO watch_video_settings
// TODO watch + cancel
// TODO fs_list_dir
// TODO fs_watch_dir
#[tauri::command]
pub fn get_cs_path(name: &str, steam_dir: &str) -> Result<String, String> {
wrap_err!(steam::path::get_cs_path(name, steam_dir))
}
#[tauri::command]
pub fn set_auto_login_user(user: &str) -> String {
pub fn open_path(path: &str) -> Result<(), String> {
wrap_err!(common::open_path(path))
}
#[tauri::command]
pub fn get_powerplan() -> Result<i32, String> {
#[cfg(target_os = "windows")]
steam::reg::set_auto_login_user(user).expect("设置自动登录用户失败");
let powerplan = powerplan::get_powerplan()?;
format!("Set auto login user to {}", user)
Ok(powerplan)
}
#[tauri::command]
pub fn check_file_exists(file: &str) -> bool {
std::path::Path::new(&file).exists()
pub fn set_powerplan(plan: i32) -> Result<(), String> {
#[cfg(target_os = "windows")]
powerplan::set_powerplan(plan)?;
Ok(())
}
#[tauri::command]
pub fn get_steam_users(steam_dir: &str) -> Result<Vec<preset::User>, String> {
wrap_err!(preset::get_users(steam_dir))
}
#[tauri::command]
pub fn set_auto_login_user(user: &str) -> Result<String, String> {
#[cfg(target_os = "windows")]
steam::reg::set_auto_login_user(user)?;
Ok(format!("Set auto login user to {}", user))
}
#[tauri::command]
pub fn check_path(path: &str) -> Result<bool, String> {
Ok(std::path::Path::new(&path).exists())
}

View File

@@ -4,6 +4,9 @@
)]
use tauri::Manager;
use tauri_plugin_deep_link::DeepLinkExt;
use tauri_plugin_autostart::MacosLauncher;
// Window Vibrancy
#[cfg(target_os = "windows")]
use window_vibrancy::apply_mica;
@@ -20,6 +23,7 @@ mod tray;
mod cmds;
mod steam;
mod tool;
mod vdf;
#[tauri::command]
fn on_button_clicked() -> String {
@@ -35,10 +39,18 @@ fn main() {
let mut ctx = tauri::generate_context!();
tauri::Builder::default()
.plugin(tauri_plugin_single_instance::init(|app, _, _| {
let _ = app
.get_webview_window("main")
.expect("no main window")
.set_focus();
}))
.plugin(tauri_plugin_deep_link::init())
.plugin(tauri_plugin_valtio::init())
.plugin(tauri_plugin_store::Builder::new().build())
.plugin(tauri_plugin_theme::init(ctx.config_mut()))
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_autostart::init(MacosLauncher::LaunchAgent, Some(vec![]) /* arbitrary number of args to pass to your app */))
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
@@ -51,6 +63,10 @@ fn main() {
// .plugin(tauri_plugin_store::Builder::default().build())
// .plugin(tauri_plugin_updater::Builder::new().build())
.setup(|app| {
// Deep Link
#[cfg(desktop)]
app.deep_link().register("cstb")?;
// Tray
#[cfg(all(desktop))]
{
@@ -72,7 +88,6 @@ fn main() {
// apply_blur(&window, Some((18, 18, 18, 0)))
// .expect("Unsupported platform! 'apply_blur' is only supported on Windows");
Ok(())
})
.invoke_handler(tauri::generate_handler![
@@ -81,8 +96,13 @@ fn main() {
cmds::kill_game,
cmds::kill_steam,
cmds::get_steam_path,
cmds::get_cs_path,
cmds::open_path,
cmds::get_powerplan,
cmds::set_powerplan,
cmds::get_steam_users,
cmds::set_auto_login_user,
cmds::check_file_exists,
cmds::check_path,
on_button_clicked
])
.run(ctx)

View File

@@ -2,8 +2,10 @@
pub mod id;
pub mod path;
pub mod reg;
pub mod user;
// common steam utils
use anyhow::Result;
use std::error::Error;
use std::process::{Command, Output};

View File

@@ -3,37 +3,105 @@
// - CS2(CS:GO) Path
#![allow(unused)]
use std::fs::{self, exists};
use std::path::{Path, PathBuf};
use crate::tool::common::get_exe_path;
use anyhow::Result;
use super::reg;
pub fn get_steam_path() -> Result<String, String> {
// Running steam.exe
#[cfg(target_os = "windows")]
if let Ok(steam_path) = get_exe_path("steam") {
// 去除结尾的 steam.exe 不区分大小写
if let Some(parent_path) = Path::new(&steam_path).parent() {
return Ok(parent_path.to_string_lossy().to_string());
} else {
return Err("Failed to get parent directory".into());
}
}
pub fn get_steam_path<'a>() -> Result<String, &'a str> {
// Windows Registry
#[cfg(target_os = "windows")]
if let Ok(reg) = reg::SteamReg::get_all() {
return Ok(reg.steam_path);
}
// Running steam.exe
#[cfg(target_os = "windows")]
if let Ok(steam_path) = get_exe_path("steam.exe") {
return Ok(steam_path);
}
Err("no steam path found")
Err("no steam path found".into())
}
pub fn get_cs_path<'a>(name: &str) -> Result<String, &'a str> {
if name != "csgo" || name != "cs2" {
return Err("invalid cs name");
pub fn get_cs_path(name: &str, steam_dir: &str) -> Result<String, String> {
if name != "csgo" && name != "cs2" {
return Err("invalid cs name".into());
}
// 1. 优先检查 steam_dir 参数
let steam_path = if steam_dir.is_empty() || !Path::new(steam_dir).exists() {
// 如果 steam_dir 不满足条件,调用 get_steam_path 获取路径
let p = get_steam_path().map_err(|e| e.to_string())?;
PathBuf::from(p)
} else {
PathBuf::from(steam_dir) // 同样转换为PathBuf
};
let cs_path = steam_path
.join("steamapps\\common\\Counter-Strike Global Offensive\\game\\bin\\win64\\cs2.exe");
if cs_path.exists() {
if let Some(parent) = cs_path.parent() {
return Ok(parent.to_string_lossy().to_string());
}
}
// 2. 通过注册表读取所有磁盘盘符
#[cfg(target_os = "windows")]
if let Ok(cs_path) = get_exe_path(&(name.to_owned() + ".exe")) {
return Ok(cs_path);
{
use winreg::enums::HKEY_LOCAL_MACHINE;
use winreg::RegKey;
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
let mounted_devices = hklm
.open_subkey("SYSTEM\\MountedDevices")
.map_err(|e| e.to_string())?;
// 获取所有盘符
let drives: Vec<String> = mounted_devices
.enum_values()
.filter_map(|v| {
let (name, _) = v.ok()?;
let name = name.to_string();
if name.starts_with("\\DosDevices\\") {
Some(name.replace("\\DosDevices\\", ""))
} else {
None
}
})
.collect();
// 遍历盘符,尝试查找 cs2.exe
let cs_path_suffix = "SteamLibrary\\steamapps\\common\\Counter-Strike Global Offensive\\game\\bin\\win64\\cs2.exe";
for drive in drives {
let cs_path = Path::new(&drive).join(cs_path_suffix);
if cs_path.exists() {
if let Some(parent) = cs_path.parent() {
return Ok(parent.to_string_lossy().to_string());
}
}
}
}
Err("no cs path found")
// 3. 查找正在运行的cs2进程
#[cfg(target_os = "windows")]
if let Ok(cs2_path) = get_exe_path("cs2") {
// 去除结尾的 steam.exe 不区分大小写
if let Some(parent_path) = Path::new(&cs2_path).parent() {
return Ok(parent_path.to_string_lossy().to_string());
} else {
return Err("Failed to get parent directory".into());
}
}
Err("no cs path found".into())
}
#[cfg(test)]
@@ -45,4 +113,17 @@ mod tests {
let path = get_steam_path().unwrap();
println!("{}", path);
}
#[test]
fn test_get_cs_path() {
let result = get_cs_path("cs2", "");
assert!(result.is_ok() || result.is_err());
println!("CS2 Path: {:?}", result);
// 使用get_steam_path给到的路径
let steam_dir = get_steam_path().unwrap();
let result = get_cs_path("cs2", &steam_dir);
assert!(result.is_ok() || result.is_err());
println!("CS2 Path: {:?}", result)
}
}

View File

@@ -29,7 +29,7 @@ impl SteamReg {
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
let cur_ver = hkcu.open_subkey(STEAM_REG).expect("steam reg");
let steam_path: String = cur_ver.get_value("SteamExe").expect("steam path");
let steam_path: String = cur_ver.get_value("SteamPath").expect("steam path");
let auto_login_user: String = cur_ver.get_value("AutoLoginUser").expect("auto login user");
let suppress_auto_run: u32 = cur_ver
.get_value("SuppressAutoRun")
@@ -39,12 +39,12 @@ impl SteamReg {
.expect("remember password");
let language: String = cur_ver.get_value("Language").expect("language");
let users = cur_ver
.open_subkey("Users")
.expect("users")
.enum_keys()
.map(|x| x.unwrap().to_string())
.collect::<Vec<String>>();
// let users = cur_ver
// .open_subkey("Users")
// .expect("users")
// .enum_keys()
// .map(|x| x.unwrap().to_string())
// .collect::<Vec<String>>();
Ok(Self {
steam_path,
@@ -52,7 +52,7 @@ impl SteamReg {
suppress_auto_run,
remember_password,
language,
users: users,
users: vec![],
})
}
}

View File

@@ -0,0 +1,25 @@
use crate::vdf::parse::to_json;
pub fn get_steam_users() -> Result<String, String> {
let vdf_data = r#"
{
"key1"\t\t"value1"
"key2"\t\t"value2"
"subkey" {
"key3"\t\t"value3"
}
}"#;
let json_data = to_json(vdf_data);
Ok(json_data)
}
mod tests {
use super::*;
#[test]
fn test_get_steam_users() {
let result = get_steam_users();
assert!(result.is_ok() || result.is_err());
println!("{}", result.unwrap());
}
}

View File

@@ -1,8 +1,13 @@
use std::os::windows::process::CommandExt;
use std::process::Command;
const CREATE_NO_WINDOW: u32 = 0x08000000;
// const DETACHED_PROCESS: u32 = 0x00000008;
pub fn kill(name: &str) -> String {
Command::new("taskkill")
.args(&["/IM", name, "/F"])
.creation_flags(CREATE_NO_WINDOW)
.output()
.unwrap();
@@ -12,20 +17,33 @@ pub fn kill(name: &str) -> String {
pub fn run_steam() -> std::io::Result<std::process::Output> {
Command::new("cmd")
.args(&["/C", "start", "steam://run"])
.creation_flags(CREATE_NO_WINDOW)
.output()
}
pub fn get_exe_path(name: &str) -> Result<String, std::io::Error> {
let command = format!("/C wmic process where name='{}' get ExecutablePath", name);
// [原理]
// Powershell 运行 Get-Process name | Select-Object path
// 有name.exe运行时返回
// Path
// ----
// 进程路径
let command = format!("Get-Process {} | Select-Object path", name);
let args = command.split_whitespace().collect::<Vec<&str>>();
let output = Command::new("cmd.exe").args(&args).output()?;
let output = Command::new("powershell.exe")
.args(&args)
.creation_flags(CREATE_NO_WINDOW)
.output()?;
let out = String::from_utf8_lossy(&output.stdout).trim().to_string();
let out = String::from_utf8_lossy(&output.stdout).to_string();
if out.contains("ExecutablePath") {
if out.contains("Path") {
let out = out.trim();
let spt: Vec<&str> = out.split("\r\n").collect();
if spt.len() >= 2 {
return Ok(spt[1].to_string());
if spt.len() > 2 {
return Ok(spt[2].to_string());
}
}
Err(std::io::Error::new(
@@ -33,3 +51,40 @@ pub fn get_exe_path(name: &str) -> Result<String, std::io::Error> {
"Failed to get executable path",
))
}
pub fn open_path(path: &str) -> Result<(), std::io::Error> {
// path中所有/ 转换为 \
let path = path.replace("/", "\\");
Command::new("cmd.exe")
.args(["/c", "start", "", &path])
.creation_flags(CREATE_NO_WINDOW)
.spawn()
.unwrap();
Ok(())
}
mod tests {
use super::*;
#[test]
fn test_open_path() {
let path = "D:\\Programs\\Steam";
println!("test open path: {}", path);
open_path(path).unwrap();
let path = "D:\\Programs\\Steam\\steamapps\\common\\Counter-Strike Global Offensive\\game\\bin\\win64";
println!("test open path: {}", path);
open_path(path).unwrap();
let path = "%appdata%/Wmpvp/demo";
println!("test open path: {}", path);
open_path(path).unwrap()
}
#[test]
fn test_get_exe_path() {
let path = get_exe_path("steam").expect("failed");
println!("test get steam path: {}", path);
get_exe_path("not_running").expect("failed");
}
}

View File

@@ -36,9 +36,10 @@ macro_rules! wrap_err {
match $stat {
Ok(a) => Ok(a),
Err(err) => {
log::error!(target: "app", "{}", err.to_string());
Err(format!("{}", err.to_string()))
let err_str = err.to_string();
log::error!(target: "app", "{}", err_str);
Err(err_str)
}
}
};
}
}

View File

@@ -1,2 +1,3 @@
pub mod common;
pub mod macros;
pub mod powerplan;

View File

@@ -0,0 +1,143 @@
use std::collections::HashMap;
use std::process::Command;
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x08000000;
// const DETACHED_PROCESS: u32 = 0x00000008;
pub struct PowerPlan {
power_plan_map: HashMap<i32, String>,
}
impl PowerPlan {
pub fn new() -> Self {
let mut power_plan_map = HashMap::new();
power_plan_map.insert(
PowerPlanMode::PowerSaving as i32,
"a1841308-3541-4fab-bc81-f71556f20b4a".to_string(),
);
power_plan_map.insert(
PowerPlanMode::Balanced as i32,
"381b4222-f694-41f0-9685-ff5bb260df2e".to_string(),
);
power_plan_map.insert(
PowerPlanMode::HighPerformance as i32,
"8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c".to_string(),
);
power_plan_map.insert(
PowerPlanMode::Extreme as i32,
"e9a42b02-d5df-448d-aa00-03f14749eb61".to_string(),
);
PowerPlan { power_plan_map }
}
pub fn set(&self, mode: i32) -> Result<(), String> {
let guid = self
.power_plan_map
.get(&mode)
.ok_or("Invalid power plan number (expect from 1 to 4)")?;
let output = Command::new("powercfg")
.arg("/S")
.arg(guid)
.creation_flags(CREATE_NO_WINDOW)
.output()
.map_err(|e| format!("Failed to execute powercfg command: {}", e))?;
if !output.status.success() {
return Err(format!(
"Powercfg command failed: {}",
String::from_utf8_lossy(&output.stderr)
));
}
Ok(())
}
pub fn get(&self) -> Result<i32, String> {
let output = Command::new("powercfg")
.arg("/L")
.creation_flags(CREATE_NO_WINDOW)
.output()
.map_err(|e| format!("Failed to execute powercfg command: {}", e))?;
let output_str = String::from_utf8_lossy(&output.stdout);
let re = regex::Regex::new(r"GUID:\s+(\S+)\s+\(\S+\)\s+\*")
.map_err(|e| format!("Failed to compile regex: {}", e))?;
let res = re
.captures(&output_str)
.ok_or("No matching power plan found")?;
let guid = res
.get(1)
.ok_or("No GUID found in power plan output")?
.as_str();
for (k, v) in &self.power_plan_map {
if guid == v {
return Ok(*k);
}
}
Err("No matching power plan found in map".to_string())
}
}
#[allow(dead_code)]
pub enum PowerPlanMode {
Other = 0,
PowerSaving = 1,
Balanced = 2,
HighPerformance = 3,
Extreme = 4,
}
pub fn get_powerplan() -> Result<i32, String> {
let power_plan = PowerPlan::new();
let mode = power_plan.get()?;
Ok(mode)
}
pub fn set_powerplan(plan: i32) -> Result<(), String> {
if plan < 0 || plan > 4 {
return Err("Invalid power plan mode".to_string());
}
let power_plan = PowerPlan::new();
power_plan.set(plan)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_set_power_plan() {
let power_plan = PowerPlan::new();
// 测试设置节能模式
let result = power_plan.set(PowerPlanMode::PowerSaving as i32);
assert!(result.is_ok(), "Failed to set power saving plan");
// 测试设置无效模式
let result = power_plan.set(99);
assert!(result.is_err(), "Setting invalid power plan should fail");
}
#[test]
fn test_get_power_plan() {
let power_plan = PowerPlan::new();
// 测试获取当前电源计划
let result = power_plan.get();
assert!(result.is_ok(), "Failed to get current power plan");
// 验证返回的模式是否在有效范围内
let mode = result.unwrap();
assert!((1..=4).contains(&mode), "Invalid power plan mode returned");
println!(
"Mode: {} - {}",
mode,
PowerPlan::new().power_plan_map[&mode]
);
}
}

2
src-tauri/src/vdf/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod parse;
pub mod preset;

View File

@@ -0,0 +1,84 @@
pub fn to_json(vdf_data: &str) -> String {
let linebreak = match std::env::consts::OS {
"macos" => "\r",
"windows" => "\n",
"linux" => "\n",
_ => "\n",
};
let startpoint = vdf_data.find('{').unwrap_or(0);
let vdf_data = &vdf_data[startpoint..];
let lines: Vec<&str> = vdf_data.split(linebreak).collect();
let mut json_data = String::new();
for line in lines {
let mut line = line.trim_end_matches('\r').trim().to_string();
if line.contains("\"\t\t\"") {
line = line.replace("\"\t\t\"", "\": \"");
line.push(',');
} else if line.contains("\" \"") {
line = line.replace("\" \"", "\": \"");
line.push(',');
}
line = line
.replace('{', ": {")
.replace('\t', "")
.replace('}', "},");
json_data.push_str(&line);
}
json_data = json_data
.replace(",}", "}")
.trim_start_matches(": ")
.trim_end_matches(',')
.to_string();
json_data
}
mod tests {
use super::*;
#[test]
fn test_to_json() {
let vdf_data = "\"users\"
{
\"76561198315078806\"
{
\"AccountName\" \"_jerry_dota2\"
\"PersonaName\" \"Rop紫已黑化\"
\"RememberPassword\" \"1\"
\"WantsOfflineMode\" \"0\"
\"SkipOfflineModeWarning\" \"0\"
\"AllowAutoLogin\" \"1\"
\"MostRecent\" \"1\"
\"Timestamp\" \"1742706884\"
}
\"76561198107125441\"
{
\"AccountName\" \"_im_ai_\"
\"PersonaName\" \"Buongiorno\"
\"RememberPassword\" \"1\"
\"WantsOfflineMode\" \"0\"
\"SkipOfflineModeWarning\" \"0\"
\"AllowAutoLogin\" \"1\"
\"MostRecent\" \"0\"
\"Timestamp\" \"1739093763\"
}
}
";
// let expected_json = r#"{"key1": "value1","key2": "value2","subkey": {"key3": "value3"}}"#;
let json_data = to_json(vdf_data);
// 解析json
let json_value: serde_json::Value = serde_json::from_str(&json_data).unwrap();
println!("{}", json_value)
// assert_eq!(to_json(vdf_data), expected_json);
}
}

282
src-tauri/src/vdf/preset.rs Normal file
View File

@@ -0,0 +1,282 @@
use anyhow::Result;
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use tauri_plugin_http::reqwest::blocking::get;
use walkdir::WalkDir;
use crate::steam;
#[derive(Serialize, Deserialize, Debug)]
pub struct User {
steam_id64: u64,
steam_id32: u32,
account_name: String,
persona_name: String,
recent: i32,
avatar: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct LoginUser {
steam_id64: u64,
steam_id32: u32,
account_name: String,
persona_name: String,
remember_password: String,
wants_offline_mode: String,
skip_offline_mode_warning: String,
allow_auto_login: String,
most_recent: String,
timestamp: String,
avatar: String,
avatar_key: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct LocalUser {
steam_id32: u32,
persona_name: String,
avatar_key: String,
}
pub fn parse_login_users(steam_dir: &str) -> Result<Vec<LoginUser>> {
let t_path = Path::new(steam_dir).join("config/loginusers.vdf");
if !t_path.exists() {
return Ok(Vec::new());
}
let data = fs::read_to_string(t_path)?;
let json_data = super::parse::to_json(&data);
let kv: HashMap<String, Value> = serde_json::from_str(&json_data)?;
let mut users = Vec::new();
for (k, v) in kv {
let props = v.as_object().unwrap();
let avatar = if let Some(img) = read_avatar(&steam_dir, &k) {
img
} else {
String::new()
};
let id64 = k.parse::<u64>()?;
let user = LoginUser {
steam_id32: steam::id::id64_to_32(id64),
steam_id64: id64,
account_name: props
.get("AccountName")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
persona_name: props
.get("PersonaName")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
remember_password: props
.get("RememberPassword")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
wants_offline_mode: props
.get("WantsOfflineMode")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
skip_offline_mode_warning: props
.get("SkipOfflineModeWarning")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
allow_auto_login: props
.get("AllowAutoLogin")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
most_recent: props
.get("MostRecent")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
timestamp: props
.get("Timestamp")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
avatar,
avatar_key: String::new(),
};
users.push(user);
}
Ok(users)
}
pub fn parse_local_users(steam_dir: &str) -> Result<Vec<LocalUser>> {
let root = Path::new(steam_dir).join("userdata");
if !root.exists() {
return Ok(Vec::new());
}
let mut local_users = Vec::new();
for entry in WalkDir::new(&root) {
let entry = entry?;
let path = entry.path();
// 跳过根目录
if path == root {
continue;
}
// 只处理目录
if entry.file_type().is_dir() {
let id = path.file_name().unwrap().to_str().unwrap();
// 检查 localconfig.vdf 文件是否存在
let local_config_path = path.join("config/localconfig.vdf");
if !local_config_path.exists() {
continue;
}
// 读取并解析 localconfig.vdf 文件
let data = fs::read_to_string(local_config_path)?;
let json_data = super::parse::to_json(&data);
let kv: HashMap<String, Value> = serde_json::from_str(&json_data)?;
// 获取 friends 节点
let friends = kv.get("friends").and_then(|v| v.as_object());
if friends.is_none() {
continue;
}
let friends = friends.unwrap();
// 获取 PersonaName
let persona_name = friends
.get("PersonaName")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
// 获取 AvatarKey
let avatar_key = friends
.get(id)
.and_then(|v| v.as_object())
.and_then(|props| props.get("avatar"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
// 创建 LocalUser 并加入列表
local_users.push(LocalUser {
steam_id32: id.parse::<u32>().unwrap(),
persona_name,
avatar_key,
});
// 跳过子目录
WalkDir::new(path).max_depth(1).into_iter().next();
}
}
Ok(local_users)
}
pub fn merge_users(login: Vec<LoginUser>, local: Vec<LocalUser>) -> Vec<User> {
let mut users = Vec::new();
for i in login {
let avatar: String;
let mut avatar_key = String::new();
// 匹配获取 avatar_key 在线获取头像使用
let t_usr: Vec<&LocalUser> = local
.iter()
.filter(|j| i.steam_id32 == j.steam_id32)
.collect();
if let Some(usr) = t_usr.first() {
avatar_key = usr.avatar_key.clone();
}
// 获取头像的base64 本地头像文件不存在时使用在线api获取
if i.avatar.is_empty() && !avatar_key.is_empty() {
avatar = download_avatar(&avatar_key).unwrap_or_default();
} else {
avatar = i.avatar;
}
users.push(User {
steam_id64: i.steam_id64,
steam_id32: i.steam_id32,
account_name: i.account_name,
persona_name: i.persona_name,
recent: i.most_recent.parse().unwrap_or(0),
avatar,
});
}
// 把第一个recent=1的放在头部
for (id, usr) in users.iter_mut().enumerate() {
if usr.recent == 1 {
let tmp = users.remove(id);
users.insert(0, tmp);
break;
}
}
users
}
pub fn get_users(steam_dir: &str) -> Result<Vec<User>> {
let login_users = parse_login_users(steam_dir)?;
let local_users = parse_local_users(steam_dir)?;
let users = merge_users(login_users, local_users);
Ok(users)
}
fn download_avatar(avatar_key: &str) -> Result<String> {
// 下载并转换成Base64 "https://avatars.cloudflare.steamstatic.com/" + avatarKey + "_full.jpg"
let url = format!(
"https://avatars.cloudflare.steamstatic.com/{}_full.jpg",
avatar_key
);
if let Ok(resp) = get(&url) {
if let Ok(bytes) = resp.bytes() {
return Ok(STANDARD.encode(bytes));
}
}
return Err(anyhow::anyhow!("Failed to download avatar"));
}
fn read_avatar(steam_dir: &str, steam_id64: &str) -> Option<String> {
let t_path = Path::new(steam_dir).join(format!("config\\avatarcache\\{}.png", steam_id64));
if !t_path.exists() {
return None;
}
if let Ok(img) = fs::read(t_path) {
Some(STANDARD.encode(img))
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_users() {
let users = get_users("D:\\Programs\\Steam").unwrap();
println!("{:?}", users);
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,35 @@
"video.cfg"
{
"Version" "15"
"VendorID" "4318"
"DeviceID" "9861"
"setting.cpu_level" "3"
"setting.gpu_mem_level" "3"
"setting.gpu_level" "3"
"setting.knowndevice" "0"
"setting.defaultres" "2880"
"setting.defaultresheight" "2160"
"setting.refreshrate_numerator" "0"
"setting.refreshrate_denominator" "0"
"setting.fullscreen" "1"
"setting.coop_fullscreen" "0"
"setting.nowindowborder" "1"
"setting.mat_vsync" "0"
"setting.fullscreen_min_on_focus_loss" "1"
"setting.high_dpi" "0"
"AutoConfig" "2"
"setting.shaderquality" "0"
"setting.r_texturefilteringquality" "3"
"setting.msaa_samples" "2"
"setting.r_csgo_cmaa_enable" "0"
"setting.videocfg_shadow_quality" "0"
"setting.videocfg_dynamic_shadows" "1"
"setting.videocfg_texture_detail" "1"
"setting.videocfg_particle_detail" "0"
"setting.videocfg_ao_detail" "0"
"setting.videocfg_hdr_detail" "3"
"setting.videocfg_fsr_detail" "0"
"setting.monitor_index" "0"
"setting.r_low_latency" "1"
"setting.aspectratiomode" "1"
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,25 @@
"users"
{
"76561198315078806"
{
"AccountName" "_jerry_dota2"
"PersonaName" "Rop紫已黑化"
"RememberPassword" "1"
"WantsOfflineMode" "0"
"SkipOfflineModeWarning" "0"
"AllowAutoLogin" "1"
"MostRecent" "1"
"Timestamp" "1742706884"
}
"76561198107125441"
{
"AccountName" "_im_ai_"
"PersonaName" "Buongiorno"
"RememberPassword" "1"
"WantsOfflineMode" "0"
"SkipOfflineModeWarning" "0"
"AllowAutoLogin" "1"
"MostRecent" "0"
"Timestamp" "1739093763"
}
}

View File

@@ -42,9 +42,17 @@
},
"productName": "CS工具箱",
"mainBinaryName": "cstb",
"version": "0.0.1",
"version": "0.0.4",
"identifier": "upup.cool",
"plugins": {},
"plugins": {
"deep-link": {
"desktop": {
"schemes": [
"cstb"
]
}
}
},
"app": {
"macOSPrivateApi": true,
"windows": [
@@ -69,4 +77,4 @@
"csp": null
}
}
}
}

View File

@@ -70,20 +70,20 @@ interface CfgxCardProps {
function CfgxCard(props: CfgxCardProps) {
return (
<li className="flex flex-col gap-2 p-4 rounded-md bg-zinc-50 ">
<li className="flex flex-col gap-2 p-4 rounded-md bg-zinc-50 dark:bg-zinc-900">
<span className="flex items-center gap-3">
<h3 className="text-xl font-semibold">{props.title}</h3>
<Chip size="sm" className="bg-zinc-200">
<Chip size="sm" className="bg-zinc-200 dark:bg-zinc-800">
{props.version}
</Chip>
<Chip size="sm" className="bg-zinc-200">
<Chip size="sm" className="bg-zinc-200 dark:bg-zinc-800">
{props.author}
</Chip>
</span>
<p className="text-zinc-600">{props.description}</p>
<p className="text-zinc-600 dark:text-zinc-300">{props.description}</p>
<Code className="p-3 whitespace-pre-line">{props.content}</Code>
{props.alias_list && (
<p className="text-zinc-600">
<p className="text-zinc-600 dark:text-zinc-300">
{props.alias_list.id} {props.alias_list.info} {props.alias_list.value}
</p>
)}

View File

@@ -11,7 +11,7 @@ export default function PreferenceLayout({
const pathname = usePathname()
return (
<Card className="h-full max-w-full overflow-y-scroll">
<Card className="h-full max-w-full">
<CardHeader>
<CardIcon
type="menu"

View File

@@ -9,6 +9,7 @@ import {
import { ToolButton } from "@/components/window/ToolButton"
import { Chip } from "@heroui/react"
import { Refresh, SettingConfig } from "@icon-park/react"
// import { version } from "@tauri-apps/plugin-os"
import { useEffect, useState } from "react"
import { type AllSystemInfo, allSysInfo } from "tauri-plugin-system-info-api"
export default function Page() {
@@ -43,6 +44,7 @@ function HardwareInfo() {
// const [cpuData, setCpuData] = useState("")
// const [batteryData, setBatteryData] = useState("")
useEffect(() => {
const fetchData = async () => {
const sys = await allSysInfo()

View File

@@ -7,7 +7,6 @@ import LaunchOption from "@/components/cstb/LaunchOption"
import Notice from "@/components/cstb/Notice"
import PowerPlan from "@/components/cstb/PowerPlan"
import SmartTransfer from "@/components/cstb/SmartTranser"
const Home = () => {
return (
<section className="flex flex-col h-full gap-4">

View File

@@ -1,18 +1,22 @@
"use client"
import { appStore } from "@/store/app"
import { useSnapshot } from "valtio"
import { useAppStore } from "@/store/app"
import { Switch } from "@heroui/react"
export default function Page() {
void appStore.start()
const app = useSnapshot(appStore.state)
const app = useAppStore()
return (
<div className="flex flex-col items-start gap-3 pt-2 pb-1">
<p>{app.version}</p>
<p>{app.hasUpdate ? "有" : "无"}</p>
<p>{app.inited ? "是" : "否"}</p>
<p>{app.notice}</p>
<p>使{app.useMirror ? "是" : "否"}</p>
<p>{app.state.version}</p>
<p>{app.state.hasUpdate ? "有" : "无"}</p>
<p>使{app.state.useMirror ? "是" : "否"}</p>
<Switch
isSelected={app.state.autoStart}
size="sm"
onChange={(e) => app.setAutoStart(e.target.checked)}
>
{app.state.autoStart ? "开" : "关"}
</Switch>
</div>
)
}

View File

@@ -25,26 +25,26 @@ export default function PreferenceLayout({
const pathname = usePathname()
return (
<Card className="max-w-full overflow-y-scroll">
<Card className="max-w-full">
<CardHeader>
<CardIcon
type="menu"
onClick={() => router.push("/preference/general")}
className={cn(pathname === "/preference/general" && "bg-black/5")}
className={cn(pathname === "/preference/general" && "bg-black/5 dark:bg-white/5")}
>
<SettingConfig />
</CardIcon>
<CardIcon
type="menu"
onClick={() => router.push("/preference/path")}
className={cn(pathname === "/preference/path" && "bg-black/5")}
className={cn(pathname === "/preference/path" && "bg-black/5 dark:bg-white/5")}
>
<AssemblyLine />
</CardIcon>
<CardIcon
type="menu"
onClick={() => router.push("/preference/replay")}
className={cn(pathname === "/preference/replay" && "bg-black/5")}
className={cn(pathname === "/preference/replay" && "bg-black/5 dark:bg-white/5")}
>
<Videocamera />
</CardIcon>

View File

@@ -1,18 +1,16 @@
"use client"
import { currentUser, steamStore } from "@/store/steam"
import { useSnapshot } from "valtio"
import { useSteamStore } from "@/store/steam"
export default function Page() {
void steamStore.start()
const steam = useSnapshot(steamStore.state)
const steam = useSteamStore()
return (
<div className="flex flex-col items-start gap-3 pt-2 pb-1">
<p>Steam路径{steam.dir}</p>
<p>{steam.csDir}</p>
<p>Steam路径有效{steam.isDirValid ? "是" : "否"}</p>
<p>{steam.isCsDirValid ? "是" : "否"}</p>
<p>Steam账号{currentUser().accountName}</p>
<p>Steam路径{steam.state.steamDir}</p>
<p>{steam.state.cs2Dir}</p>
<p>Steam路径有效{steam.state.steamDirValid ? "是" : "否"}</p>
<p>{steam.state.cs2DirValid ? "是" : "否"}</p>
<p>Steam账号{steam.currentUser()?.account_name || " "}</p>
</div>
)
}

View File

@@ -1,4 +1,11 @@
"use client"
import VideoSetting from "@/components/cstb/VideoSetting"
export default function Page() {
return <div>Tool</div>
return (
<section className="flex flex-col h-full gap-4">
<VideoSetting />
</section>
)
}

View File

@@ -0,0 +1,48 @@
"use client"
import SteamUsers from "@/components/cstb/SteamUsers"
// import { usePathname, useRouter } from "next/navigation"
// import { platform } from "@tauri-apps/plugin-os"
export default function PreferenceLayout({
children,
}: { children: React.ReactNode }) {
// const router = useRouter()
// const pathname = usePathname()
return (
<div className="flex w-full gap-3">
{/* <Card className="flex-grow max-w-ful">
<CardHeader>
<CardIcon
type="menu"
onClick={() => router.push("/preference/general")}
className={cn(pathname === "/preference/general" && "bg-black/5")}
>
<SettingConfig /> 通用
</CardIcon>
<CardIcon
type="menu"
onClick={() => router.push("/preference/path")}
className={cn(pathname === "/preference/path" && "bg-black/5")}
>
<AssemblyLine /> 路径
</CardIcon>
<CardTool>
<ToolButton>
<UploadOne />
云同步
</ToolButton>
<ToolButton>
<HardDisk />
保存
</ToolButton>
</CardTool>
</CardHeader>
<CardBody>{children}</CardBody>
</Card> */}
{/* Steam用户区域 */}
<SteamUsers />
</div>
)
}

View File

@@ -1,12 +1,33 @@
// "use client"
"use client"
import { useSteamStore } from "@/store/steam"
import { useEffect } from "react"
import "./globals.css"
import Providers from "./providers"
import { init } from "@/store"
import { useDebounce } from "ahooks"
export default function RootLayout({ children }: { children: React.ReactNode }) {
useEffect(() => {
void init()
})
// 检测steam路径和游戏路径是否有效
const steam = useSteamStore()
const debounceSteamDir = useDebounce(steam.state.steamDir, {wait: 500, leading: true, trailing: true, maxWait: 2500})
const debounceCs2Dir = useDebounce(steam.state.cs2Dir, {wait: 500, leading: true, trailing: true, maxWait: 2500})
const debounceSteamDirValid = useDebounce(steam.state.steamDirValid, {wait: 500, leading: true, trailing: true, maxWait: 2500})
useEffect(() => {
void steam.checkSteamDirValid()
}, [debounceSteamDir])
useEffect(() => {
void steam.checkCs2DirValid()
}, [debounceCs2Dir])
useEffect(() => {
if (debounceSteamDirValid) {
void steam.getUsers()
}
}, [debounceSteamDirValid])
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>

View File

@@ -1,47 +1,26 @@
"use client"
import Nav from "@/components/window/Nav"
import { Button } from "@heroui/react"
// import { open } from "@tauri-apps/plugin-dialog"
import { useRouter } from "next/navigation"
import React from "react"
import { Prepare } from "@/components/cstb/Prepare"
import { motion, AnimatePresence } from "framer-motion"
const Home = () => {
const router = useRouter()
// const [file, setFile] = React.useState<string | null>("")
// const openFile = async () => {
// const filePath = await open({
// multiple: true,
// directory: false,
// })
// setFile(filePath?.join("\n") || " ")
// }
return (
<>
<Nav />
<main
className="flex flex-col items-center justify-center w-full h-screen gap-6"
data-tauri-drag-region
>
<h1 className="text-4xl font-bold tracking-wide">CS </h1>
<Button
onPress={() => router.push("/home")}
variant="solid"
color="primary"
size="sm"
>
</Button>
{/* <button
type="button"
onClick={openFile}
className="px-4 py-1 text-white bg-blue-500 rounded"
>
选择文件
</button>
<p className="text-center bg-zinc-50">{file}</p> */}
<main className="flex flex-col justify-center w-full h-screen overflow-hidden" data-tauri-drag-region>
<AnimatePresence>
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -30 }}
transition={{ duration: 0.5 }}
className="flex flex-col items-center justify-center gap-6 "
>
<h1 className="text-4xl font-bold tracking-wide">CS </h1>
<Prepare />
</motion.div>
</AnimatePresence>
</main>
</>
)

View File

@@ -1,18 +1,16 @@
"use client"
import { currentUser, setCsDir, setDir, steamStore } from "@/store/steam"
import { useSteamStore } from "@/store/steam"
import { useEffect, useState } from "react"
import { useSnapshot } from "valtio"
export default function Page() {
void steamStore.start()
const steam = useSnapshot(steamStore.state)
const [steamDir, setSteamDir] = useState(steam.dir)
const [cs2Dir, setCs2Dir] = useState(steam.csDir)
const steam = useSteamStore()
const [steamDir, setSteamDir] = useState(steam.state.steamDir)
const [cs2Dir, setCs2Dir] = useState(steam.state.cs2Dir)
useEffect(() => {
setSteamDir(steam.dir)
setCs2Dir(steam.csDir)
}, [steam.dir, steam.csDir])
setSteamDir(steam.state.steamDir)
setCs2Dir(steam.state.cs2Dir)
}, [steam.state.steamDir, steam.state.cs2Dir])
return (
<div
@@ -29,7 +27,7 @@ export default function Page() {
value={steamDir}
onChange={(e) => {
setSteamDir(e.target.value)
setDir(e.target.value)
steam.setDir(e.target.value)
}}
/>
<p>CS2所在文件夹</p>
@@ -38,10 +36,10 @@ export default function Page() {
value={cs2Dir}
onChange={(e) => {
setCs2Dir(e.target.value)
setCsDir(e.target.value)
steam.setCsDir(e.target.value)
}}
/>
<p>64SteamID{currentUser().steamID64}</p>
<p>64SteamID{steam.currentUser()?.steam_id64}</p>
</div>
</div>
)

View File

@@ -13,13 +13,10 @@ export default function Providers({ children }: { children: React.ReactNode }) {
return (
<HeroUIProvider
className={cn(
"h-full bg-zinc-100/90 dark:bg-zinc-900",
os === "macos" && "rounded-lg",
)}
className={cn("h-full bg-zinc-200/90 dark:bg-zinc-900", os === "macos" && "rounded-lg")}
>
<NextThemesProvider attribute="class" defaultTheme="light">
<ToastProvider toastOffset={10} placement="top-center" />
<ToastProvider toastOffset={10} placement="top-center" toastProps={{ timeout: 3000 }} />
{children}
</NextThemesProvider>
</HeroUIProvider>

View File

@@ -1,6 +1,9 @@
import { addToast } from "@heroui/react"
import { FolderFocusOne } from "@icon-park/react"
import { Card, CardBody, CardHeader, CardIcon } from "../window/Card"
import { invoke } from "@tauri-apps/api/core"
import { useSteamStore } from "@/store/steam"
import path from "path"
interface RoundedButtonProps {
children?: React.ReactNode
@@ -13,7 +16,7 @@ const RoundedButton = ({
return (
<button
type="button"
className="flex items-center justify-center px-3 py-1 transition rounded-full select-none min-w-fit active:scale-95 hover:bg-black/10 text-zinc-700 dark:text-zinc-100 bg-black/5"
className="flex items-center justify-center px-3 py-1 transition rounded-full select-none min-w-fit active:scale-95 hover:bg-black/10 text-zinc-700 dark:text-zinc-100 bg-black/5 dark:bg-white/5"
{...props}
>
{children}
@@ -22,6 +25,8 @@ const RoundedButton = ({
}
const CommonDir = () => {
const steam = useSteamStore()
return (
<Card>
<CardHeader>
@@ -32,49 +37,81 @@ const CommonDir = () => {
<CardBody>
<div className="flex gap-1.5">
<RoundedButton
onClick={() => {
alert("hello")
onClick={async () => {
await invoke("open_path", {
path: steam.state.steamDir,
})
addToast({ title: "Steam安装目录" })
}}
>
Steam安装位置
Steam安装目录
</RoundedButton>
<RoundedButton
onClick={() => {
onClick={async () => {
await invoke("open_path", {
path: steam.cs2BaseDir(),
})
addToast({ title: "CS2游戏目录" })
}}
>
CS2游戏目录
</RoundedButton>
<RoundedButton
onClick={() => {
addToast({ title: "地图文件" })
onClick={async () => {
await invoke("open_path", {
path: path.resolve(steam.cs2BaseDir(), "game", "csgo", "maps"),
})
addToast({ title: "地图目录" })
}}
>
</RoundedButton>
<RoundedButton
onClick={() => {
addToast({ title: "游戏CFG目录" })
onClick={async () => {
await invoke("open_path", {
path: path.resolve(steam.cs2BaseDir(), "game", "csgo", "cfg"),
})
addToast({ title: "游戏CFG" })
}}
>
CFG目录
CFG
</RoundedButton>
<RoundedButton
onClick={() => {
addToast({ title: "个人CFG目录" })
onClick={async () => {
const user = steam.currentUser()
if (!user) {
addToast({ title: "请先选择用户", color: "danger" })
return
}
await invoke("open_path", {
path: path.resolve(steam.state.steamDir, "userdata", user.steam_id32.toString(), "730", "local", "cfg"),
})
addToast({ title: "个人CFG" })
}}
>
CFG目录
CFG
</RoundedButton>
<RoundedButton
onClick={() => {
onClick={async () => {
await invoke("open_path", {
path: path.resolve(steam.cs2BaseDir(), "game", "csgo", "replays"),
})
addToast({ title: "官匹录像" })
}}
>
</RoundedButton>
<RoundedButton
onClick={async () => {
await invoke("open_path", { path: `%appdata%/Wmpvp/demo` })
addToast({ title: "完美平台录像" })
}}
>
</RoundedButton>
<RoundedButton
onClick={() => {
onClick={async () => {
await invoke("open_path", { path: `%appdata%/5E对战平台/demo` })
addToast({ title: "5E平台录像" })
}}
>

View File

@@ -1,19 +1,16 @@
import { steamStore } from "@/store/steam"
import { toolStore } from "@/store/tool"
import { useSteamStore } from "@/store/steam"
import { useToolStore } from "@/store/tool"
import { TakeOff } from "@icon-park/react"
import { invoke } from "@tauri-apps/api/core"
import { useSnapshot } from "valtio"
import { Card, CardBody, CardHeader, CardIcon } from "../window/Card"
// import { addToast } from "@heroui/react"
import { addToast, Button } from "@heroui/react"
const FastLaunch = () => {
void toolStore.start()
void steamStore.start()
const { launchOptions, launchIndex } = useSnapshot(toolStore.state)
const { dir } = useSnapshot(steamStore.state)
const steam = useSteamStore()
const tool = useToolStore()
return (
<Card>
<Card className="max-w-fit">
<CardHeader>
<CardIcon>
<TakeOff />
@@ -21,32 +18,34 @@ const FastLaunch = () => {
</CardHeader>
<CardBody>
<div className="flex gap-2">
<button
type="button"
onClick={() =>
invoke("launch_game", {
steamPath: `${dir}/steam.exe`,
launchOption: launchOptions[launchIndex] || "",
<Button
size="md"
onPress={() => {
void invoke("launch_game", {
steamPath: `${steam.state.steamDir}/steam.exe`,
launchOption: tool.state.launchOptions[tool.state.launchIndex] || "",
server: "perfectworld",
})
}
addToast({ title: "启动国服成功" })
}}
className="px-5 font-medium py-1.5 transition bg-red-200 dark:bg-red-900/60 rounded-full select-none"
>
</button>
<button
type="button"
onClick={() =>
invoke("launch_game", {
steamPath: `${dir}/steam.exe`,
launchOption: launchOptions[launchIndex] || "",
</Button>
<Button
size="md"
onPress={() => {
void invoke("launch_game", {
steamPath: `${steam.state.steamDir}/steam.exe`,
launchOption: tool.state.launchOptions[tool.state.launchIndex] || "",
server: "worldwide",
})
}
addToast({ title: "启动国际服成功" })
}}
className="px-5 font-medium py-1.5 transition bg-orange-200 dark:bg-orange-900/60 rounded-full select-none"
>
</button>
</Button>
</div>
</CardBody>
</Card>

View File

@@ -1,10 +1,11 @@
import { Power } from "@icon-park/react"
import { invoke } from "@tauri-apps/api/core"
import { Card, CardBody, CardHeader, CardIcon } from "../window/Card"
import { addToast, Button } from "@heroui/react"
const ForceQuit = () => {
return (
<Card>
<Card className="max-w-fit">
<CardHeader>
<CardIcon>
<Power /> 退
@@ -12,20 +13,26 @@ const ForceQuit = () => {
</CardHeader>
<CardBody>
<div className="flex gap-2">
<button
type="button"
onClick={() => invoke("kill_steam")}
<Button
size="md"
onPress={async () => {
await invoke("kill_steam")
addToast({ title: "已关闭Steam" })
}}
className="px-5 font-medium py-1.5 transition bg-blue-200 dark:bg-blue-900/60 rounded-full select-none"
>
Steam
</button>
<button
type="button"
onClick={() => invoke("kill_game")}
</Button>
<Button
size="md"
onPress={async () => {
await invoke("kill_game")
addToast({ title: "已关闭CS2" })
}}
className="px-5 font-medium py-1.5 transition bg-orange-200 dark:bg-orange-900/60 rounded-full select-none"
>
CS2
</button>
</Button>
</div>
</CardBody>
</Card>

View File

@@ -1,23 +1,17 @@
import {
addLaunchOption,
setLaunchIndex,
setLaunchOption,
toolStore,
} from "@/store/tool"
import { useToolStore } from "@/store/tool"
import { Plus, SettingConfig, Switch } from "@icon-park/react"
import { useEffect, useState } from "react"
import { useSnapshot } from "valtio"
import { Card, CardBody, CardHeader, CardIcon, CardTool } from "../window/Card"
import { ToolButton } from "../window/ToolButton"
import { input, Textarea } from "@heroui/react"
const LaunchOption = () => {
void toolStore.start()
const { launchOptions, launchIndex } = useSnapshot(toolStore.state)
const [launchOpt, setLaunchOpt] = useState(launchOptions[launchIndex] || "")
const tool = useToolStore()
const [launchOpt, setLaunchOpt] = useState(tool.state.launchOptions[tool.state.launchIndex] || "")
useEffect(() => {
setLaunchOpt(launchOptions[launchIndex] || "")
}, [launchIndex, launchOptions])
setLaunchOpt(tool.state.launchOptions[tool.state.launchIndex] || "")
}, [tool.state.launchIndex, tool.state.launchOptions])
return (
<Card>
@@ -26,12 +20,12 @@ const LaunchOption = () => {
<SettingConfig />
</CardIcon>
<CardTool>
{launchOptions.map((option, index) => (
<ToolButton key={option} onClick={() => setLaunchIndex(index)}>
{index + 1}
{tool.state.launchOptions.map((option, index) => (
<ToolButton key={index} onClick={() => tool.setLaunchIndex(index)}>
{option.name || index + 1}
</ToolButton>
))}
<ToolButton onClick={() => addLaunchOption("")}>
<ToolButton onClick={() => tool.addLaunchOption({ option: "", name: "" })}>
<Plus />
</ToolButton>
@@ -44,13 +38,16 @@ const LaunchOption = () => {
<CardBody>
<textarea
placeholder="请输入启动选项"
value={launchOpt}
value={launchOpt.option}
onChange={(e) => {
if (launchIndex < 0 || launchIndex > 10) return
setLaunchOpt(e.target.value)
setLaunchOption(e.target.value, launchIndex)
if (tool.state.launchIndex < 0 || tool.state.launchIndex > 10) return
setLaunchOpt({ option: e.target.value, name: launchOpt.name })
tool.setLaunchOption(
{ option: e.target.value, name: launchOpt.name },
tool.state.launchIndex
)
}}
className="w-full font-mono text-base bg-transparent outline-none resize-none min-h-20"
className="w-full font-mono text-base bg-transparent outline-none resize-none min-h-20 max-h-32"
/>
</CardBody>
</Card>

View File

@@ -6,12 +6,11 @@ import {
CardIcon,
CardTool,
} from "@/components/window/Card"
import { appStore } from "@/store/app"
import { useAppStore } from "@/store/app"
import { createClient } from "@/utils/supabase/client"
import { Skeleton } from "@heroui/react"
import { Refresh, VolumeNotice } from "@icon-park/react"
import useSWR, { useSWRConfig } from "swr"
import { useSnapshot } from "valtio"
import { ToolButton } from "../window/ToolButton"
const Notice = () => {
@@ -38,8 +37,7 @@ const Notice = () => {
}
const NoticeBody = () => {
void appStore.start()
const app = useSnapshot(appStore.state)
const app = useAppStore()
const noticeFetcher = async () => {
const supabase = createClient()
@@ -68,7 +66,7 @@ const NoticeBody = () => {
return (
<>
{notice?.content ||
app.notice ||
app.state.notice ||
"不会真的有人要更新CSGO工具箱吧不会吧不会吧 xswl"}
</>
)

View File

@@ -2,8 +2,59 @@ import { BatteryCharge, Refresh } from "@icon-park/react"
import { invoke } from "@tauri-apps/api/core"
import { Card, CardBody, CardHeader, CardIcon, CardTool } from "../window/Card"
import { ToolButton } from "../window/ToolButton"
import { Tabs, Tab, addToast } from "@heroui/react"
import { Key } from "@react-types/shared"
import { useToolStore } from "@/store/tool"
import { useEffect } from "react"
const PowerPlans = [
{
id: "0",
title: "其他",
},
{
id: "1",
title: "节能",
},
{
id: "2",
title: "平衡",
},
{
id: "3",
title: "高性能",
},
{
id: "4",
title: "卓越性能",
},
]
const PowerPlan = () => {
const tool = useToolStore()
const setPowerPlan = async (key: Key) => {
await tool.store.start()
const plan = Number(key)
await invoke("set_powerplan", { plan: plan })
const current = await invoke<number>("get_powerplan")
tool.setPowerPlan(current)
addToast({ title: `电源计划已切换 → ${ PowerPlans[current].title}` })
}
const getPowerPlan = async (toast: boolean) => {
await tool.store.start()
const current = await invoke<number>("get_powerplan")
tool.setPowerPlan(current)
if (toast) addToast({ title: `电源计划已切换 → ${ PowerPlans[current].title}` })
}
useEffect(() => {
void getPowerPlan(false)
}, [])
return (
<Card>
<CardHeader>
@@ -11,36 +62,26 @@ const PowerPlan = () => {
<BatteryCharge />
</CardIcon>
<CardTool>
<ToolButton>
<ToolButton onClick={() => getPowerPlan(true)}>
<Refresh />
</ToolButton>
</CardTool>
</CardHeader>
<CardBody>
<div className="flex p-0.5 bg-black/5 gap-2 rounded-full">
<button
type="button"
onClick={() => invoke("set_power_plan", { index: 1 })}
className="flex-1 px-2 py-1 transition rounded-full select-none bg-black/5"
>
</button>
<button
type="button"
onClick={() => invoke("set_power_plan", { index: 2 })}
className="flex-1 px-2 py-1 transition rounded-full select-none"
>
</button>
<button
type="button"
onClick={() => invoke("set_power_plan", { index: 3 })}
className="flex-1 px-2 py-1 transition rounded-full select-none"
>
</button>
</div>
<Tabs
selectedKey={String(tool.state.powerPlan)}
onSelectionChange={setPowerPlan}
aria-label="Power Plan Tabs"
size="md"
radius="full"
fullWidth
defaultSelectedKey="0"
>
{PowerPlans.slice(1).map((item) => (
<Tab key={item.id} title={item.title}></Tab>
))}
</Tabs>
</CardBody>
</Card>
)

View File

@@ -0,0 +1,233 @@
import { addToast, Button, Input, Spinner } from "@heroui/react"
import { useRouter } from "next/navigation"
import { useEffect, useState } from "react"
import { useSteamStore } from "@/store/steam"
import { open } from "@tauri-apps/plugin-dialog"
import { invoke } from "@tauri-apps/api/core"
import { onOpenUrl } from "@tauri-apps/plugin-deep-link"
import { useDebounce } from "ahooks"
import { useAppStore } from "@/store/app"
/**
* 检查指定路径的有效性
* @param path - 需要检查的路径
* @param suffix - (可选) 路径需要匹配的后缀条件
* @returns Promise<boolean> 路径是否有效且满足后缀条件
*/
async function check_path(path: string, suffix?: string) {
if (suffix && !path.toLowerCase().startsWith(suffix)) return false
const exist = await invoke<boolean>("check_path", { path: path })
return exist
}
function trim_end_string(str: string, suffix: string): string {
if (str.endsWith(suffix)) {
return str.slice(0, -suffix.length)
}
return str
}
export function Prepare() {
const steam = useSteamStore()
const app = useAppStore()
const router = useRouter()
const [loading, setLoading] = useState(true)
const [inited, setInited] = useState(false)
const [, setSteamDir] = useState(steam.state.steamDir)
const [, setCs2Dir] = useState(steam.state.cs2Dir)
// Init
useEffect(() => {
const init = async () => {
await steam.store.start()
await app.store.start()
if (!app.state.inited) await autoGetPaths(false)
setInited(true)
}
void init()
}, [])
// valid变动后调整State
const [checkCount, setCheckCount] = useState(0)
useEffect(() => {
setCheckCount((prev) => (prev >= 10 ? 10 : prev + 1))
// console.log(checkCount, "触发", steam.state.steamDir, steam.state.cs2Dir, app.state.inited)
if (checkCount <= 2) {
if (steam.state.steamDir && steam.state.cs2Dir && app.state.inited) {
setTimeout(() => {
router.push("/home")
}, 300)
} else {
setTimeout(() => {
setLoading(false)
}, 1000)
}
} else {
setTimeout(() => {
setLoading(false)
}, 1200)
}
}, [inited])
const handleSelectSteamDir = async () => {
const selected = await open({
title: "选择 Steam.exe 文件",
filters: [{ name: "steam", extensions: ["exe"] }],
})
if (selected) {
const dir = selected.replace(/\\[^\\]+$/, "")
const pathExist = await invoke("check_path", { path: dir })
if (!pathExist) {
addToast({ title: "路径不存在", color: "warning" })
return
}
setSteamDir(dir)
steam.setDir(dir)
}
}
const autoGetPaths = async (toast?: boolean) => {
try {
const steam_path = await invoke<string>("get_steam_path")
if (steam_path) steam.setDir(steam_path)
} catch (e) {
addToast({ title: "自动获取Steam路径失败", color: "danger" })
}
try {
const cs2_path = await invoke<string>("get_cs_path", {
name: "cs2",
steamDir: steam.state.steamDir,
})
if (cs2_path) {
steam.setCsDir(cs2_path)
if (toast) addToast({ title: "自动获取路径成功", color: "success" })
}
} catch (e) {
addToast({ title: "自动获取CS2路径失败", color: "danger" })
console.log(e)
}
}
const handleSelectCs2Dir = async () => {
const selected = await open({
title: "选择 CS2.exe 文件",
filters: [{ name: "cs2", extensions: ["exe"] }],
})
if (selected) {
const dir = selected.replace(/\\[^\\]+$/, "")
const pathExist = await invoke("check_path", { path: dir })
if (!pathExist) {
addToast({ title: "路径不存在", color: "warning" })
return
}
// setCs2Dir(dir)
steam.setCsDir(dir)
}
}
if (loading) {
return (
<div className="flex items-center justify-center gap-4">
<Spinner size="md" variant="simple" />
<p>...</p>
</div>
)
}
return (
<div className="flex flex-col w-full max-w-3xl gap-2 p-5">
<p className="text-center"></p>
<br />
<h3 className="font-semibold">Steam所在文件夹</h3>
<div className="flex gap-2">
<Input
variant="bordered"
size="sm"
value={steam.state.steamDir}
onValueChange={(value) => {
setSteamDir(value)
steam.setDir(value)
}}
description="steam.exe所在文件夹"
errorMessage={"路径无效"}
isInvalid={!steam.state.steamDirValid}
/>
<Button
onPress={handleSelectSteamDir}
variant="solid"
color="primary"
size="sm"
isLoading={steam.state.steamDirChecking}
>
</Button>
</div>
<h3 className="font-semibold">CS2所在文件夹</h3>
<div className="flex gap-2">
<Input
variant="bordered"
size="sm"
value={steam.state.cs2Dir}
onValueChange={(value) => {
setCs2Dir(value)
steam.setCsDir(value)
}}
description="cs2.exe所在文件夹"
errorMessage={"路径无效建议启动游戏后点击自动获取可以检测运行中的cs2"}
isInvalid={!steam.state.cs2DirValid}
/>
<Button
onPress={handleSelectCs2Dir}
variant="solid"
color="primary"
size="sm"
isLoading={steam.state.cs2DirChecking}
>
</Button>
</div>
<TestDeepLink />
<section className="flex justify-center w-full gap-3 mt-6">
<Button
onPress={() => void autoGetPaths(true)}
variant="ghost"
color="default"
size="md"
className="w-24"
>
</Button>
<Button
onPress={() => {
app.setInited(true)
router.push("/home")
}}
variant="solid"
color="primary"
size="md"
className="w-24"
isLoading={steam.state.steamDirChecking || steam.state.cs2DirChecking}
isDisabled={!steam.state.steamDirValid || !steam.state.cs2DirValid}
>
</Button>
</section>
</div>
)
}
function TestDeepLink() {
const [links, setLinks] = useState<string[]>([])
const router = useRouter()
useEffect(() => {
void onOpenUrl((urls) => {
console.log("deep link:", urls)
setLinks(urls)
})
}, [router])
return <>{links.length > 0 && <p className="text-white">{links}</p>}</>
}

View File

@@ -0,0 +1,83 @@
import { Refresh, User } from "@icon-park/react"
import { Card, CardBody, CardHeader, CardIcon, CardTool } from "../window/Card"
import { addToast, Button, Chip } from "@heroui/react"
import { useSteamStore } from "@/store/steam"
import { ToolButton } from "../window/ToolButton"
import { useAutoAnimate } from "@formkit/auto-animate/react"
const SteamUsers = ({ className }: { className?: string }) => {
const steam = useSteamStore()
const [parent /* , enableAnimations */] = useAutoAnimate(/* optional config */)
const getUsers = async (toast?: boolean) => {
if (!steam.state.steamDirValid) {
if (toast) addToast({ title: "Steam路径不可用", color: "warning" })
return
}
await steam.getUsers()
if (toast) addToast({ title: `已获取Steam用户` })
}
return (
<Card /* className={cn("max-w-96", className)} */>
<CardHeader>
<CardIcon>
<User /> Steam用户
</CardIcon>
<CardTool>
<ToolButton onClick={() => getUsers(true)}>
<Refresh />
</ToolButton>
</CardTool>
</CardHeader>
<CardBody>
<ul className="flex flex-col gap-3 mt-1" ref={parent}>
{steam.state.users.map((user, id) => (
<li
key={user.account_name}
className="flex gap-2 transition rounded-lg bg-zinc-50 dark:bg-zinc-900"
>
<img
src={user.avatar ? `data:image/png;base64,${user.avatar}` : "/logo_square.png"}
alt="avatar"
className="w-20 h-20 rounded-l-lg"
draggable="false"
/>
<div className="flex flex-col flex-grow justify-center gap-2 p-0.5">
<h3 className="text-xl font-semibold">{user.persona_name}</h3>
<div className="flex gap-2">
<Chip size="sm" className="bg-zinc-200 dark:bg-zinc-800">
{user.account_name}
</Chip>
<Chip size="sm" className="bg-zinc-200 dark:bg-zinc-800">
{user.steam_id32}
</Chip>
<Chip size="sm" className="bg-zinc-200 dark:bg-zinc-800">
{user.steam_id64.toString()}
</Chip>
{user.recent > 0 && (
<Chip size="sm" color="primary">
</Chip>
)}
</div>
</div>
<div className="flex items-end gap-2 p-2">
<Button size="sm" onPress={() => steam.switchLoginUser(id)}>
</Button>
<Button size="sm" onPress={() => steam.selectUser(id)}>
</Button>
</div>
</li>
))}
</ul>
</CardBody>
</Card>
)
}
export default SteamUsers

View File

@@ -0,0 +1,172 @@
import { CloseSmall, Down, Edit, Plus, SettingConfig, Up } from "@icon-park/react"
import { useState } from "react"
import { Card, CardBody, CardHeader, CardIcon, CardTool } from "../window/Card"
import { ToolButton } from "../window/ToolButton"
import { addToast, NumberInput, Tab, Tabs } from "@heroui/react"
import { motion } from "framer-motion"
import { useToolStore } from "@/store/tool"
const VideoSetting = () => {
const [hide, setHide] = useState(false)
const [edit, setEdit] = useState(false)
const tool = useToolStore()
// const [launchOpt, setLaunchOpt] = useState(tool.state.VideoSettings[tool.state.launchIndex] || "")
// useEffect(() => {
// setLaunchOpt(tool.state.VideoSettings[tool.state.launchIndex] || "")
// }, [tool.state.launchIndex, tool.state.VideoSettings])
// 设置对应关系
// TODO Value通过实际数值映射
const videoSettings = [
{ type: "", title: "全屏", value: "全屏", options: ["窗口", "全屏"] },
{ type: "", title: "垂直同步", value: "关闭", options: ["关闭", "开启"] },
{ type: "", title: "增强角色对比度", value: "禁用", options: ["禁用", "启用"] },
{ type: "", title: "CMAA2抗锯齿", value: "关闭", options: ["关闭", "开启"] },
{
type: "",
title: "多重采样抗锯齿",
value: "2X MSAA",
options: ["无", "CMAA2", "2X MSAA", "4X MSAA", "8X MSAA"],
},
{ type: "", title: "全局阴影效果", value: "低", options: ["低", "中", "高", "非常高"] },
{ type: "", title: "动态阴影", value: "全部", options: ["仅限日光", "全部"] },
{ type: "", title: "模型/贴图细节", value: "中", options: ["低", "中", "高"] },
{
type: "",
title: "贴图过滤模式",
value: "异向 4X",
options: ["双线性", "三线性", "异向 2X", "异向 4X", "异向 8X", "异向 16X"],
},
{ type: "", title: "光影细节", value: "低", options: ["低", "高"] },
{ type: "", title: "粒子细节", value: "低", options: ["低", "中", "高", "非常高"] },
{ type: "", title: "环境光遮蔽", value: "已禁用", options: ["已禁用", "中", "高"] },
{ type: "", title: "高动态范围", value: "性能", options: ["性能", "品质"] },
{
type: "",
title: "Fidelity FX 超级分辨率",
value: "已禁用",
options: ["性能", "均衡", "品质", "超高品质", "已禁用"],
},
]
return (
<Card>
<CardHeader>
<CardIcon>
<SettingConfig />
</CardIcon>
<CardTool>
{/* {tool.state.VideoSettings.map((option, index) => (
<ToolButton key={index} onClick={() => tool.setLaunchIndex(index)}>
{index + 1}
</ToolButton>
))} */}
{edit && (
<>
<ToolButton></ToolButton>
<ToolButton></ToolButton>
<ToolButton></ToolButton>
<ToolButton></ToolButton>
<ToolButton></ToolButton>
<ToolButton
onClick={() => {
addToast({ title: "测试中 功能完成后可应用设置到游戏" })
}}
>
<Plus />
</ToolButton>
</>
)}
<ToolButton onClick={() => setEdit(!edit)}>
{edit ? (
<>
<CloseSmall />
</>
) : (
<>
<Edit />
</>
)}
</ToolButton>
<ToolButton onClick={() => setHide(!hide)}>
{hide ? (
<>
<Up />
</>
) : (
<>
<Down />
</>
)}
</ToolButton>
</CardTool>
</CardHeader>
{!hide && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
>
<CardBody>
<ul className="flex flex-wrap gap-3 mt-1">
<li className="flex flex-col gap-1.5">
<span className="ml-2"></span>
<span className="flex gap-1.5">
<NumberInput
value={tool.state.videoSetting.width}
onValueChange={(value) => {
tool.setVideoSetting({
...tool.state.videoSetting,
width: value,
})
}}
radius="full"
className="max-w-28"
classNames={{ inputWrapper: "h-10" }}
/>
<NumberInput
value={tool.state.videoSetting.height}
onValueChange={(value) => {
tool.setVideoSetting({
...tool.state.videoSetting,
height: value,
})
}}
radius="full"
className="max-w-28"
classNames={{ inputWrapper: "h-10" }}
/>
</span>
</li>
{videoSettings.map((vid, index) => (
<li className="flex flex-col gap-1.5" key={index}>
<span className="ml-2">{vid.title}</span>
<Tabs
selectedKey={vid.value}
size="md"
radius="full"
className="min-w-36"
fullWidth
>
{vid.options.map((opt, index) => (
<Tab key={opt} title={opt} />
))}
</Tabs>
</li>
))}
</ul>
</CardBody>
</motion.div>
)}
</Card>
)
}
export default VideoSetting

View File

@@ -8,11 +8,14 @@ interface CardProps {
onClick?: () => void
}
const Card = ({ children }: CardProps) => {
const Card = ({ children, className, ...props }: CardProps) => {
return (
<div
className="dark:bg-white/[3%] dark:border-white/[6%] px-3 py-3 flex flex-col gap-2.5 border w-full rounded-lg bg-white/40 border-black/[6%]"
className={cn(
"dark:bg-white/[3%] dark:border-white/[6%] px-3 py-3 flex flex-col gap-2.5 border w-full rounded-lg bg-white/40 border-black/[6%]"
, className)}
data-tauri-drag-region
{...props}
>
{children}
</div>
@@ -33,7 +36,7 @@ const CardIcon = ({ children, type, className, ...rest }: CardProps) => {
className={cn(
"flex gap-1.5 items-center font-semibold",
type === "menu" &&
"transition cursor-pointer hover:bg-black/5 px-2 py-1 rounded-md active:scale-95",
"transition cursor-pointer hover:bg-black/5 dark:hover:bg-white/5 px-2 py-1 rounded-md active:scale-95",
className,
)}
{...rest}

View File

@@ -1,11 +1,17 @@
import { useSteamStore } from "@/store/steam"
import { useRouter } from "next/navigation"
const Header = () => {
const steam = useSteamStore()
const router = useRouter()
return (
<div className="pt-6 select-none pb-9" data-tauri-drag-region>
<h1 className="text-xl font-medium tracking-wide w-fit">
Faze.Rop紫本人
<h1 className="mb-0.5 text-xl font-semibold tracking-wide w-fit active:scale-95 transition cursor-pointer" onClick={() => router.push('/users')}>
{steam.currentUser()?.persona_name || 'CS工具箱'}
</h1>
<p className="text-sm font-light tracking-wide text-zinc-400 w-fit">
使CS工具箱 114
<p className="text-sm font-light tracking-wide transition cursor-pointer text-zinc-400 w-fit active:scale-95" onClick={() => router.push('/users')}>
{steam.currentUser()?.account_name || '本周使用CS工具箱 114 小时'}
</p>
</div>
)

View File

@@ -1,22 +1,16 @@
"use client"
import { setTheme as setTauriTheme } from "@/hooks/tauri/theme"
import { resetAppStore } from "@/store/app"
import { resetToolStore } from "@/store/tool"
import { addToast } from "@heroui/react"
import {
Close,
Minus,
Moon,
Refresh,
RocketOne,
Square,
SunOne,
} from "@icon-park/react"
import { useAppStore } from "@/store/app"
import { useToolStore } from "@/store/tool"
import { addToast, Button, useDisclosure } from "@heroui/react"
import { Close, Minus, Moon, Refresh, RocketOne, Square, SunOne } from "@icon-park/react"
import { type Theme, getCurrentWindow } from "@tauri-apps/api/window"
import { /* relaunch, */ exit } from "@tauri-apps/plugin-process"
import { useTheme } from "next-themes"
import { usePathname, useRouter } from "next/navigation"
import { saveAllNow } from "tauri-plugin-valtio"
import { saveAllNow } from "@tauri-store/valtio"
import { useSteamStore } from "@/store/steam"
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from "@heroui/react"
const Nav = () => {
const { theme, setTheme } = useTheme()
@@ -48,66 +42,52 @@ const Nav = () => {
const router = useRouter()
const pathname = usePathname()
const app = useAppStore()
return (
<nav
className="absolute top-0 right-0 flex flex-row h-16 gap-0.5 p-4"
data-tauri-drag-region
>
<button
type="button"
className="px-2 py-0 transition duration-150 rounded hover:bg-zinc-200/80 active:scale-95"
onClick={() => {
resetAppStore()
resetToolStore()
addToast({
title: "重置成功",
color: "success",
// description: "已重置所有设置",
})
}}
>
<Refresh size={16} />
</button>
<nav className="absolute top-0 right-0 flex flex-row h-16 gap-0.5 p-4" data-tauri-drag-region>
{pathname !== "/" && (
<button
type="button"
className="px-2 py-0 transition duration-150 rounded hover:bg-zinc-200/80 dark:hover:bg-zinc-100/10 active:scale-95"
onClick={() => {
app.setInited(false)
if(pathname !== "/") router.push("/")
}}
>
<RocketOne size={16} />
</button>
)}
<button
type="button"
className="px-2 py-0 transition duration-150 rounded hover:bg-zinc-200/80 active:scale-95"
onClick={() =>
pathname !== "/prepare" ? router.push("/prepare") : router.back()
}
>
<RocketOne size={16} />
</button>
<button
type="button"
className="px-2 py-0 transition duration-150 rounded hover:bg-zinc-200/80 active:scale-95"
onClick={() =>
theme === "light" ? setAppTheme("dark") : setAppTheme("light")
}
className="px-2 py-0 transition duration-150 rounded hover:bg-zinc-200/80 dark:hover:bg-zinc-100/10 active:scale-95"
onClick={() => (theme === "light" ? setAppTheme("dark") : setAppTheme("light"))}
>
{theme === "light" ? <SunOne size={16} /> : <Moon size={16} />}
</button>
<ResetModal />
{/* { platform() === "windows" && ( */}
<>
<button
type="button"
className="px-2 py-0 transition duration-150 rounded hover:bg-zinc-200/80 active:scale-95"
className="px-2 py-0 transition duration-150 rounded hover:bg-zinc-200/80 dark:hover:bg-zinc-100/10 active:scale-95"
onClick={minimize}
>
<Minus size={16} />
</button>
<button
type="button"
className="px-2 py-0 transition duration-150 rounded hover:bg-zinc-200/80 active:scale-95"
className="px-2 py-0 transition duration-150 rounded hover:bg-zinc-200/80 dark:hover:bg-zinc-100/10 active:scale-95"
onClick={toggleMaximize}
>
<Square size={16} />
</button>
<button
type="button"
className="px-2 py-0 transition duration-150 rounded hover:bg-zinc-200/80 active:scale-95"
className="px-2 py-0 transition duration-150 rounded hover:bg-zinc-200/80 dark:hover:bg-zinc-100/10 active:scale-95"
onClick={close}
>
<Close size={16} />
@@ -118,4 +98,62 @@ const Nav = () => {
)
}
function ResetModal() {
const app = useAppStore()
const tool = useToolStore()
const steam = useSteamStore()
const router = useRouter()
const { isOpen, onOpen, onOpenChange } = useDisclosure()
function reset() {
app.resetAppStore()
tool.resetToolStore()
steam.resetSteamStore()
addToast({
title: "重置成功",
color: "success",
// description: "已重置所有设置",
})
router.push("/")
}
return (
<>
<button
type="button"
className="px-2 py-0 transition duration-150 rounded hover:bg-zinc-200/80 dark:hover:bg-zinc-100/10 active:scale-95"
onClick={onOpen}
>
<Refresh size={16} />
</button>
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent>
{(onClose) => (
<>
<ModalHeader className="flex flex-col gap-1"></ModalHeader>
<ModalBody>
<p>CS工具箱的偏好设置为默认设置</p>
</ModalBody>
<ModalFooter>
<Button color="danger" variant="light" onPress={onClose}>
</Button>
<Button
color="primary"
onPress={() => {
reset()
onClose()
}}
>
</Button>
</ModalFooter>
</>
)}
</ModalContent>
</Modal>
</>
)
}
export default Nav

View File

@@ -1,19 +1,13 @@
"use client"
import { Button, cn } from "@heroui/react"
import {
Home,
MonitorOne,
Movie,
Setting,
Terminal,
Toolkit,
} from "@icon-park/react"
import { cn, user } from "@heroui/react"
import { Home, MonitorOne, Movie, Setting, Terminal, Toolkit } from "@icon-park/react"
import { usePathname, useRouter } from "next/navigation"
import type { ReactNode } from "react"
import { getVersion } from "@tauri-apps/api/app"
// import { platform } from "@tauri-apps/plugin-os"
import { appStore, setVersion } from "@/store/app"
import { useSnapshot } from "valtio"
import { useAppStore } from "@/store/app"
import { useSteamStore } from "@/store/steam"
interface SideButtonProps {
route: string
@@ -36,8 +30,8 @@ const SideButton = ({
onClick={() => router.push(route || "/")}
className={cn(
className,
"p-2.5 hover:bg-black/5 rounded-lg transition relative",
path.startsWith(route) && "bg-black/5",
"p-2.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg transition relative active:scale-90",
path.startsWith(route) && "bg-black/5 dark:bg-white/5"
)}
{...rest}
>
@@ -45,7 +39,7 @@ const SideButton = ({
<div
className={cn(
path.startsWith(route) && "opacity-100",
"transition-opacity duration-300 opacity-0 h-3.5 w-0.5 absolute left-0.5 bg-purple-500 rounded-full top-1/2 -translate-y-1/2",
"transition-opacity duration-300 opacity-0 h-3.5 w-0.5 absolute left-0.5 bg-purple-500 rounded-full top-1/2 -translate-y-1/2"
)}
/>
</button>
@@ -53,31 +47,39 @@ const SideButton = ({
}
const Avatar = () => {
const steam = useSteamStore()
const router = useRouter()
const path = usePathname()
return (
<button
onClick={() => router.push("/users")}
<img
src={
steam.currentUser()?.avatar
? `data:image/png;base64,${steam.currentUser()?.avatar || ''}`
: "/logo_square.png"
}
alt="avatar"
draggable={false}
className={cn(
"w-12 h-12 bg-gray-700 rounded-full shadow-2xl cursor-pointer transition active:scale-95 shadow-purple-800",
path.startsWith("/users") && "shadow-sm",
"w-12 h-12 bg-gray-700 rounded-full shadow-lg cursor-pointer transition active:scale-95 shadow-purple-900/50",
path.startsWith("/users") && "shadow-sm"
)}
type="button"
>
<img src="favicon.ico" alt="avatar" draggable={false} />
</button>
onClick={() => router.push("/users")}
/>
)
}
const SideBar = () => {
void appStore.start()
const { version } = useSnapshot(appStore.state)
const app = useAppStore()
void getVersion().then((Value) => {
app.setVersion(Value)
})
return (
<div
className={cn(
"absolute left-0 flex flex-col h-full select-none w-20 pt-7 pb-5",
"absolute left-0 flex flex-col h-full select-none w-20 pt-7 pb-5"
// platform() === "windows" ? "w-20" : "w-[4.25rem]"
)}
data-tauri-drag-region
@@ -110,19 +112,9 @@ const SideBar = () => {
</SideButton>
</section>
<div
className="mx-auto text-sm text-center text-zinc-500"
data-tauri-drag-region
>
<div className="mx-auto text-sm text-center text-zinc-500" data-tauri-drag-region>
<p></p>
<Button
variant="light"
size="sm"
className="mt-0.5 text-zinc-600"
onPress={() => setVersion("x.y.z")}
>
{version}
</Button>
<p className="py-1 text-sm text-zinc-600">{app.state.version}</p>
</div>
</div>
)

View File

@@ -1,14 +1,13 @@
import type { ReactNode } from "react"
interface ToolButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
interface ToolButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
children?: ReactNode
}
export const ToolButton = ({ children, ...rest }: ToolButtonProps) => {
return (
<button
type="button"
className="flex gap-0.5 active:scale-95 items-center min-w-7 justify-center px-2 py-1.5 bg-black/5 transition hover:bg-black/10 rounded-md text-sm leading-none"
className="flex gap-0.5 active:scale-95 items-center min-w-7 justify-center px-2 py-1.5 bg-black/5 transition hover:bg-black/10 dark:bg-white/5 dark:hover:bg-white/10 rounded-md text-sm leading-none"
{...rest}
>
{children}

View File

@@ -1,12 +1,7 @@
import { store } from "tauri-plugin-valtio"
import { DEFAULT_STORE_CONFIG } from "."
// Usage:
// import {appStore} from "@/store/app"
// import { useSnapshot } from "valtio"
// const app = useSnapshot(appStore.state)
// { app.version }
// () => appStore.setVersion("0.0.1")
import { store } from "@tauri-store/valtio"
import { useSnapshot } from "valtio"
import { DEFAULT_STORE_CONFIG } from "./config"
import { enable, isEnabled, disable } from "@tauri-apps/plugin-autostart"
const defaultValue = {
version: "0.0.1",
@@ -14,30 +9,58 @@ const defaultValue = {
inited: false,
notice: "",
useMirror: true,
autoStart: false,
}
export const appStore = store("app", { ...defaultValue }, DEFAULT_STORE_CONFIG)
export const setVersion = (version: string) => {
export const useAppStore = () => {
void appStore.start
const state = useSnapshot(appStore.state)
return {
state,
store: appStore,
setVersion,
setHasUpdate,
setInited,
setNotice,
setUseMirror,
setAutoStart,
resetAppStore,
}
}
const setVersion = (version: string) => {
appStore.state.version = version
}
export const setHasUpdate = (hasUpdate: boolean) => {
const setHasUpdate = (hasUpdate: boolean) => {
appStore.state.hasUpdate = hasUpdate
}
export const setInited = (inited: boolean) => {
const setInited = (inited: boolean) => {
appStore.state.inited = inited
}
export const setNotice = (notice: string) => {
const setNotice = (notice: string) => {
appStore.state.notice = notice
}
export const setUseMirror = (useMirror: boolean) => {
const setUseMirror = (useMirror: boolean) => {
appStore.state.useMirror = useMirror
}
export const resetAppStore = () => {
const setAutoStart = (autoStart: boolean) => {
if (autoStart) {
void enable()
} else {
void disable()
}
appStore.state.autoStart = autoStart
}
const resetAppStore = () => {
setVersion(defaultValue.version)
setHasUpdate(defaultValue.hasUpdate)
setInited(defaultValue.inited)
setNotice(defaultValue.notice)
setUseMirror(defaultValue.useMirror)
}
setAutoStart(defaultValue.autoStart)
}

6
src/store/config.ts Normal file
View File

@@ -0,0 +1,6 @@
export const DEFAULT_STORE_CONFIG = {
saveOnChange: true,
saveOnExit: true,
saveStrategy: "debounce" as const,
saveInterval: 2000,
}

View File

@@ -1,20 +1,14 @@
// @ts-ignore
// import path from "path"
// import { appConfigDir } from "@tauri-apps/api/path"
// import { setValtioPath } from "tauri-plugin-valtio"
import { appConfigDir } from "@tauri-apps/api/path"
import { setStoreCollectionPath } from "@tauri-store/valtio"
import { appStore } from "./app"
import { steamStore } from "./steam"
import { toolStore } from "./tool"
import path from "path"
// async function init() {
// const configDir = await appConfigDir()
// const distDir = path.join(configDir, "cstb")
// await setValtioPath(distDir)
// // console.log('init valtio', distDir);
// }
// void init()
export const DEFAULT_STORE_CONFIG = {
saveOnChange: true,
saveOnExit: true,
saveStrategy: "debounce" as const,
saveInterval: 2000,
export async function init() {
await appStore.start()
await toolStore.start()
await steamStore.start()
const appConfigDirPath = await appConfigDir()
await setStoreCollectionPath(path.resolve(appConfigDirPath, "cstb"))
}

View File

@@ -1,53 +1,130 @@
import type { SteamUser } from "@/types/steam"
import { store } from "tauri-plugin-valtio"
import { DEFAULT_STORE_CONFIG } from "."
import { store } from "@tauri-store/valtio"
import { DEFAULT_STORE_CONFIG } from "./config"
import { useSnapshot } from "valtio"
import { invoke } from "@tauri-apps/api/core"
import path from "path"
const defaultValue = {
dir: "C:\\Program Files (x86)\\Steam",
csDir: "",
users: [
{
steamID64: "76561198052315353",
steamID32: "STEAM_0:0:46157676",
accountName: "wrr",
personaName: "wrr",
recent: 0,
avatar: "",
},
] as SteamUser[],
isDirValid: false,
isCsDirValid: false,
steamDir: "C:\\Program Files (x86)\\Steam",
cs2Dir: "",
steamDirValid: false,
cs2DirValid: false,
steamDirChecking: false,
cs2DirChecking: false,
users: [] as SteamUser[],
}
export const steamStore = store(
"steam",
{ ...defaultValue },
DEFAULT_STORE_CONFIG,
)
export const setDir = (dir: string) => {
steamStore.state.dir = dir
export const useSteamStore = () => {
void steamStore.start
const state = useSnapshot(steamStore.state)
return {
state,
store: steamStore,
cs2BaseDir,
setDir,
setCsDir,
setUsers,
setSteamDirValid,
setCs2DirValid,
checkSteamDirValid,
checkCs2DirValid,
currentUser,
getUsers,
selectUser,
switchLoginUser,
resetSteamStore,
}
}
export const setCsDir = (dir: string) => {
steamStore.state.csDir = dir
const cs2BaseDir = () => {
return path.normalize(`${steamStore.state.cs2Dir.replaceAll("\\", "/")}/../../..`)
}
export const setUsers = (users: SteamUser[]) => {
const setDir = (dir: string) => {
steamStore.state.steamDir = dir
}
const setCsDir = (dir: string) => {
steamStore.state.cs2Dir = dir
}
const setUsers = (users: SteamUser[]) => {
steamStore.state.users = users
}
export const setIsDirValid = (valid: boolean) => {
steamStore.state.isDirValid = valid
const setSteamDirValid = (valid: boolean) => {
steamStore.state.steamDirValid = valid
}
export const setIsCsDirValid = (valid: boolean) => {
steamStore.state.isCsDirValid = valid
const setCs2DirValid = (valid: boolean) => {
steamStore.state.cs2DirValid = valid
}
export const currentUser = () => {
return steamStore.state.users[0] || defaultValue.users[0]
const setSteamDirChecking = (checking: boolean) => {
steamStore.state.steamDirChecking = checking
}
export const resetSteamStore = () => {
setDir(defaultValue.dir)
setCsDir(defaultValue.csDir)
const setCs2DirChecking = (checking: boolean) => {
steamStore.state.cs2DirChecking = checking
}
const checkSteamDirValid = async () => {
setSteamDirChecking(true)
const pathExist = await invoke<boolean>("check_path", { path: steamStore.state.steamDir })
setSteamDirValid(pathExist)
setTimeout(() => {
setSteamDirChecking(false)
}, 500)
}
const checkCs2DirValid = async () => {
setCs2DirChecking(true)
const pathExist = await invoke<boolean>("check_path", { path: steamStore.state.cs2Dir })
setCs2DirValid(pathExist)
setTimeout(() => {
setCs2DirChecking(false)
}, 500)
}
const currentUser = () => {
return steamStore.state.users.at(0) || undefined
}
const getUsers = async () => {
const users = await invoke<SteamUser[]>("get_steam_users", { steamDir: steamStore.state.steamDir })
console.log(users)
setUsers(users)
}
const selectUser = (index: number) => {
const user = steamStore.state.users.at(index)
console.log(index, user)
if (user) {
setUsers([
user,
...steamStore.state.users.slice(0, index),
...steamStore.state.users.slice(index + 1),
])
}
}
const switchLoginUser = async (index: number) => {
const user = steamStore.state.users.at(index)
if (user) {
await invoke<SteamUser[]>("set_auto_login_user", { user: user.account_name })
}
}
const resetSteamStore = () => {
setDir(defaultValue.steamDir)
setCsDir(defaultValue.cs2Dir)
setUsers(defaultValue.users)
setIsDirValid(defaultValue.isDirValid)
setIsCsDirValid(defaultValue.isCsDirValid)
}
setSteamDirValid(defaultValue.steamDirValid)
setCs2DirValid(defaultValue.cs2DirValid)
setSteamDirChecking(defaultValue.steamDirChecking)
setCs2DirChecking(defaultValue.cs2DirChecking)
}

View File

@@ -1,14 +1,43 @@
import { store } from "tauri-plugin-valtio"
import { DEFAULT_STORE_CONFIG } from "."
import { store } from "@tauri-store/valtio"
import { useSnapshot } from "valtio"
import { DEFAULT_STORE_CONFIG } from "./config"
interface LaunchOption {
option: string
name: string
}
export interface VideoSetting {
width: number; // 分辨率宽度
height: number; // 分辨率高度
fullscreen: string; // 全屏
vsync: string; // 垂直同步
enhanceCharacterContrast: string; // 增强角色对比度
cmaa2AntiAliasing: string; // CMAA2抗锯齿
msaaAntiAliasing: string; // 多重采样抗锯齿
globalShadowQuality: string; // 全局阴影效果
dynamicShadows: string; // 动态阴影
modelTextureDetail: string; // 模型/贴图细节
textureFilteringMode: string; // 贴图过滤模式
lightShadowDetail: string; // 光影细节
particleDetail: string; // 粒子细节
ambientOcclusion: string; // 环境光遮蔽
hdr: string; // 高动态范围
fidelityFxSuperResolution: string; // Fidelity FX 超级分辨率
}
const defaultValue = {
launchOptions: [
"-novid -high -freq 144 -fullscreen",
"-novid -high -w 1920 -h 1080 -freq 144 -sw -noborder",
"-novid -high -freq 144 -fullscreen -allow_third_party_software",
] as string[],
{ option: "-novid -high -freq 144 -fullscreen", name: "" },
{ option: "-novid -high -w 1920 -h 1080 -freq 144 -sw -noborder", name: "" },
{ option: "-novid -high -freq 144 -fullscreen -allow_third_party_software", name: "" },
] as LaunchOption[],
launchIndex: 0,
powerPlan: 0,
videoSetting: {
width: 1920,
height: 1080
} as VideoSetting,
}
export const toolStore = store(
@@ -17,7 +46,24 @@ export const toolStore = store(
DEFAULT_STORE_CONFIG,
)
export const setLaunchOption = (option: string, index: number) => {
export const useToolStore = () => {
void toolStore.start
const state = useSnapshot(toolStore.state)
return {
state,
store: toolStore,
setLaunchOption,
setLaunchOptions,
setLaunchIndex,
setPowerPlan,
setVideoSetting,
addLaunchOption,
resetToolStore,
}
}
const setLaunchOption = (option: LaunchOption, index: number) => {
toolStore.state.launchOptions = [
...toolStore.state.launchOptions.slice(0, index),
option,
@@ -25,19 +71,23 @@ export const setLaunchOption = (option: string, index: number) => {
]
}
export const setLaunchOptions = (options: string[]) => {
const setLaunchOptions = (options: LaunchOption[]) => {
toolStore.state.launchOptions = options
}
export const setLaunchIndex = (index: number) => {
const setLaunchIndex = (index: number) => {
toolStore.state.launchIndex = index
}
export const setPowerPlan = (plan: number) => {
const setPowerPlan = (plan: number) => {
toolStore.state.powerPlan = plan
}
export const addLaunchOption = (option: string) => {
const setVideoSetting = (setting: VideoSetting) => {
toolStore.state.videoSetting = setting
}
const addLaunchOption = (option: LaunchOption) => {
// 限制最高10个
if (toolStore.state.launchOptions.length >= 10) {
return
@@ -45,8 +95,9 @@ export const addLaunchOption = (option: string) => {
toolStore.state.launchOptions = [...toolStore.state.launchOptions, option]
}
export const resetToolStore = () => {
const resetToolStore = () => {
setLaunchOptions(defaultValue.launchOptions)
setLaunchIndex(defaultValue.launchIndex)
setPowerPlan(defaultValue.powerPlan)
setVideoSetting(defaultValue.videoSetting)
}

View File

@@ -1,10 +1,10 @@
import type { AdvancedListItem } from "@/types/common"
export interface SteamUser extends AdvancedListItem {
steamID64: string
steamID32: string
accountName: string
personaName: string
steam_id64: bigint
steam_id32: number
account_name: string
persona_name: string
recent: number
avatar: string
}