23 Commits

Author SHA1 Message Date
purp1e
b6dbbf94ec Merge pull request 'dev-video' (#3) from dev-video into master
Reviewed-on: #3
2025-03-27 18:30:18 +08:00
Purp1e
bbcaf8e0d1 [fix] missing light(shader) setting + tray emit triggers multiple times 2025-03-27 18:19:53 +08:00
Purp1e
faec03afb1 [fix] some vconfig not applied -> stable release 2025-03-27 18:02:51 +08:00
Purp1e
7ec20984c4 v0.0.5 2025-03-27 17:40:02 +08:00
Purp1e
dabbab9f3e [fix] lint errors + new version 2025-03-27 17:39:22 +08:00
Purp1e
0e7e6dd3ba [feat] enable tab switch in edit mode 2025-03-27 17:36:04 +08:00
Purp1e
a10cf8eddf [feat] template works but cannot edit value by clicking 2025-03-27 16:11:42 +08:00
purp1e
63172f12bc [feat] set video and template 2025-03-27 15:48:09 +08:00
purp1e
114def0b96 [feat] set video config 2025-03-27 15:29:12 +08:00
Purp1e
93cda8dc85 [feat] read video config is ok 2025-03-27 13:32:30 +08:00
purp1e
afa7355f4d testing video parse 2025-03-27 11:30:03 +08:00
Purp1e
e29b48b98c [fix] building error due to eslint 2025-03-27 00:59:03 +08:00
Purp1e
21cdc7c32d [feat] tray powerplan items and seperators 2025-03-27 00:58:18 +08:00
Purp1e
27439593b4 [fix] building error 2025-03-26 03:50:47 +08:00
Purp1e
ed040aadf5 remove unused type inference 2025-03-26 03:49:59 +08:00
Purp1e
8f885a5412 [feat] tray items with kill and launch game
todo: launch option selection + seperator
2025-03-26 03:49:03 +08:00
Purp1e
ee03bf0160 [feat] start hidden + hidden on close + basic tray function 2025-03-26 03:00:18 +08:00
Purp1e
e0a84a0570 [fix] feedback url 2025-03-25 02:51:59 +08:00
Purp1e
66d62b970a tag 0.0.5-beta.1 2025-03-25 01:07:42 +08:00
Purp1e
0a78a9d056 [fix] fast launch not working 2025-03-25 00:55:25 +08:00
Purp1e
0b65cdb129 [fix] lint errors by disable related checking 2025-03-25 00:46:38 +08:00
Purp1e
e2d8f3effd [feat] fix notice + add tooltips 2025-03-25 00:40:32 +08:00
purp1e
7a99672317 [dep] update dep 2025-03-24 09:55:21 +08:00
41 changed files with 1747 additions and 560 deletions

View File

@@ -51,6 +51,9 @@
"@typescript-eslint/no-unsafe-member-access": "warn",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-unused-vars": "warn",
"jsx-a11y/click-events-have-key-events": "off"
"jsx-a11y/click-events-have-key-events": "off",
"@typescript-eslint/no-unsafe-assignment": "warn",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-base-to-string": "warn"
}
}

6
.gitignore vendored
View File

@@ -38,4 +38,8 @@ yarn-error.log*
*.tsbuildinfo
.env
.env.*
.env.*
temp/
src-tauri/temp/
log/

217
README.md
View File

@@ -1,216 +1,3 @@
# Tauri + Next.js Template
# CS工具箱
![Tauri window screenshot](public/tauri-nextjs-template_screenshot.png)
This is a [Tauri](https://tauri.app/) project template using [Next.js](https://nextjs.org/),
bootstrapped by combining [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app)
and [`create tauri-app`](https://tauri.app/v1/guides/getting-started/setup).
This template uses [`pnpm`](https://pnpm.io/) as the Node.js dependency
manager.
## Template Features
- TypeScript frontend using Next.js React framework
- [TailwindCSS](https://tailwindcss.com/) as a utility-first atomic CSS framework
- The example page in this template app has been updated to use only TailwindCSS
- While not included by default, consider using
[React Aria components](https://react-spectrum.adobe.com/react-aria/index.html)
and/or [HeadlessUI components](https://headlessui.com/) for completely unstyled and
fully accessible UI components, which integrate nicely with TailwindCSS
- Opinionated formatting and linting already setup and enabled
- [ESLint](https://eslint.org/) for pure React + TypeScript linting, and
[Biome](https://biomejs.dev/) for a combination of fast formatting, linting, and
import sorting of JavaScript and TypeScript code
- [clippy](https://github.com/rust-lang/rust-clippy) and
[rustfmt](https://github.com/rust-lang/rustfmt) for Rust code
- GitHub Actions to check code formatting and linting for both TypeScript and Rust
## Getting Started
### Running development server and use Tauri window
After cloning for the first time, set up git pre-commit hooks:
```shell
pnpm prepare
```
To develop and run the frontend in a Tauri window:
```shell
pnpm dev
```
This will load the Next.js frontend directly in a Tauri webview window, in addition to
starting a development server on `localhost:3000`.
### Building for release
To export the Next.js frontend via SSG and build the Tauri application for release:
```shell
pnpm build
```
Please remember to change the bundle identifier in
`tauri.conf.json > tauri > bundle > identifier`, as the default value will yield an
error that prevents you from building the application for release.
### Source structure
Next.js frontend source files are located in `src/` and Tauri Rust application source
files are located in `src-tauri/`. Please consult the Next.js and Tauri documentation
respectively for questions pertaining to either technology.
## Caveats
### Static Site Generation / Pre-rendering
Next.js is a great React frontend framework which supports server-side rendering (SSR)
as well as static site generation (SSG or pre-rendering). For the purposes of creating a
Tauri frontend, only SSG can be used since SSR requires an active Node.js server.
Using Next.js and SSG helps to provide a quick and performant single-page-application
(SPA) frontend experience. More information regarding this can be found here:
https://nextjs.org/docs/basic-features/pages#pre-rendering
### `next/image`
The [`next/image` component](https://nextjs.org/docs/basic-features/image-optimization)
is an enhancement over the regular `<img>` HTML element with additional optimizations
built in. However, because we are not deploying the frontend onto Vercel directly, some
optimizations must be disabled to properly build and export the frontend via SSG.
As such, the
[`unoptimized` property](https://nextjs.org/docs/api-reference/next/image#unoptimized)
is set to true for the `next/image` component in the `next.config.js` configuration.
This will allow the image to be served as-is from source, without
changes to its quality, size, or format.
### error[E0554]: `#![feature]` may not be used on the stable release channel
If you are getting this issue when trying to run `pnpm tauri dev`, it may be that you
have a newer version of a Rust dependency that uses an unstable feature.
`pnpm tauri build` should still work for production builds, but to get the dev command
working, either downgrade the dependency or use Rust nightly via
`rustup override set nightly`.
### ReferenceError: navigator is not defined
If you are using Tauri's `invoke` function or any OS related Tauri function from within
JavaScript, you may encounter this error when importing the function in a global,
non-browser context. This is due to the nature of Next.js' dev server effectively
running a Node.js server for SSR and hot module replacement (HMR), and Node.js does not
have a notion of `window` or `navigator`.
#### Solution 1 - Dependency Injection (may not always work)
Make sure that you are calling these functions within the browser context, e.g. within a
React component inside a `useEffect` hook when the DOM actually exists by then. If you
are trying to use a Tauri function in a generalized utility source file, a workaround is
to use dependency injection for the function itself to delay the actual importing of the
real function (see example below for more info).
Example using Tauri's `invoke` function:
`src/lib/some_tauri_functions.ts` (problematic)
```typescript
// Generalized file containing all the invoke functions we need to fetch data from Rust
import { invoke } from "@tauri-apps/api/tauri"
const loadFoo = (): Promise<string> => {
return invoke<string>("invoke_handler_foo")
}
const loadBar = (): Promise<string> => {
return invoke<string>("invoke_handler_bar")
}
const loadBaz = (): Promise<string> => {
return invoke<string>("invoke_handler_baz")
}
// and so on ...
```
`src/lib/some_tauri_functions.ts` (fixed)
```typescript
// Generalized file containing all the invoke functions we need to fetch data from Rust
//
// We apply the idea of dependency injection to use a supplied invoke function as a
// function argument, rather than directly referencing the Tauri invoke function.
// Hence, don't import invoke globally in this file.
//
// import { invoke } from "@tauri-apps/api/tauri" <-- remove this!
//
import { InvokeArgs } from "@tauri-apps/api/tauri"
type InvokeFunction = <T>(cmd: string, args?: InvokeArgs | undefined) => Promise<T>
const loadFoo = (invoke: InvokeFunction): Promise<string> => {
return invoke<string>("invoke_handler_foo")
}
const loadBar = (invoke: InvokeFunction): Promise<string> => {
return invoke<string>("invoke_handler_bar")
}
const loadBaz = (invoke: InvokeFunction): Promise<string> => {
return invoke<string>("invoke_handler_baz")
}
// and so on ...
```
Then, when using `loadFoo`/`loadBar`/`loadBaz` within your React components, import the
invoke function from `@tauri-apps/api` and pass `invoke` into the loadXXX function as
the `InvokeFunction` argument. This should allow the actual Tauri API to be bundled
only within the context of a React component, so it should not be loaded by Next.js upon
initial startup until the browser has finished loading the page.
#### Solution 2: Wrap Tauri API behind dynamic `import()`
Since the Tauri API needs to read from the browser's `window` and `navigator` object,
this data does not exist in a Node.js and hence SSR environment. One can create an
exported function that wraps the Tauri API behind a dynamic runtime `import()` call.
Example: create a `src/lib/tauri.ts` to re-export `invoke`
```typescript
import type { InvokeArgs } from "@tauri-apps/api/tauri"
const isNode = (): boolean =>
Object.prototype.toString.call(typeof process !== "undefined" ? process : 0) ===
"[object process]"
export async function invoke<T>(
cmd: string,
args?: InvokeArgs | undefined,
): Promise<T> {
if (isNode()) {
// This shouldn't ever happen when React fully loads
return Promise.resolve(undefined as unknown as T)
}
const tauriAppsApi = await import("@tauri-apps/api")
const tauriInvoke = tauriAppsApi.invoke
return tauriInvoke(cmd, args)
}
```
Then, instead of importing `import { invoke } from "@tauri-apps/api/tauri"`, use invoke
from `import { invoke } from "@/lib/tauri"`.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and
API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
And to learn more about Tauri, take a look at the following resources:
- [Tauri Documentation - Guides](https://tauri.app/v1/guides/) - learn about the Tauri
toolkit.
> CS Toolbox

BIN
bun.lockb

Binary file not shown.

View File

@@ -24,6 +24,7 @@
"@supabase/ssr": "0.6.1",
"@tauri-apps/api": "2.4.0",
"@tauri-apps/plugin-autostart": "^2.2.0",
"@tauri-apps/plugin-cli": "~2",
"@tauri-apps/plugin-clipboard-manager": "2.2.2",
"@tauri-apps/plugin-deep-link": "~2.2.0",
"@tauri-apps/plugin-dialog": "~2.2.0",
@@ -35,7 +36,7 @@
"@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",
"@tauri-store/valtio": "2.1.1",
"@types/throttle-debounce": "^5.0.2",
"ahooks": "^3.8.4",
"framer-motion": "^12.5.0",
@@ -43,6 +44,8 @@
"next-themes": "^0.4.6",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-markdown": "^10.1.0",
"remark-gfm": "^4.0.1",
"swr": "^2.3.3",
"tauri-plugin-system-info-api": "^2.0.10"
},
@@ -53,11 +56,11 @@
"@testing-library/react": "^16.2.0",
"@testing-library/user-event": "^14.6.1",
"@types/jest": "^29.5.14",
"@types/node": "^22.13.11",
"@types/node": "^22.13.13",
"@types/react": "19.0.10",
"@types/react-dom": "19.0.4",
"@typescript-eslint/eslint-plugin": "^8.27.0",
"@typescript-eslint/parser": "^8.27.0",
"@typescript-eslint/eslint-plugin": "^8.28.0",
"@typescript-eslint/parser": "^8.28.0",
"autoprefixer": "^10.4.21",
"clsx": "^2.1.1",
"cross-env": "^7.0.3",

View File

@@ -1,3 +1,3 @@
# Generated by Cargo
# will have compiled files and executables
/target/
/target/

115
src-tauri/Cargo.lock generated
View File

@@ -16,6 +16,7 @@ dependencies = [
"tauri",
"tauri-build",
"tauri-plugin-autostart",
"tauri-plugin-cli",
"tauri-plugin-clipboard-manager",
"tauri-plugin-deep-link",
"tauri-plugin-dialog",
@@ -90,6 +91,56 @@ dependencies = [
"libc",
]
[[package]]
name = "anstream"
version = "0.6.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
[[package]]
name = "anstyle-parse"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
dependencies = [
"anstyle",
"once_cell",
"windows-sys 0.59.0",
]
[[package]]
name = "anyhow"
version = "1.0.97"
@@ -691,6 +742,33 @@ dependencies = [
"windows-link",
]
[[package]]
name = "clap"
version = "4.5.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6088f3ae8c3608d19260cd7445411865a485688711b78b5be70d78cd96136f83"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
version = "4.5.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22a7ef7f676155edfb82daa97f99441f3ebf4a58d5e32f295a56259f1b6facc8"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_lex"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
[[package]]
name = "clipboard-win"
version = "5.4.0"
@@ -730,6 +808,12 @@ dependencies = [
"objc",
]
[[package]]
name = "colorchoice"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]]
name = "combine"
version = "4.6.7"
@@ -2441,6 +2525,12 @@ dependencies = [
"once_cell",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "itertools"
version = "0.14.0"
@@ -5075,6 +5165,21 @@ dependencies = [
"thiserror 2.0.12",
]
[[package]]
name = "tauri-plugin-cli"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5458ae16eac81bdbe8d9da2a9f3e01e8cdedbc381cc1727c01127542c8a61c5"
dependencies = [
"clap",
"log",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"thiserror 2.0.12",
]
[[package]]
name = "tauri-plugin-clipboard-manager"
version = "2.2.2"
@@ -5326,9 +5431,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-valtio"
version = "2.1.0"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0b4f506590b0ce053703e225e780adc8dfae67cdb6e7a60141d4c25e23be0a0"
checksum = "9de45344d9278229b43786fc7301c2afd8a05d8232862290e746767a3eb60a2c"
dependencies = [
"serde",
"tauri",
@@ -6013,6 +6118,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.16.0"

View File

@@ -44,7 +44,7 @@ tauri-plugin-clipboard-manager = "2.2.2"
tauri-plugin-shell = "2.2.0"
tauri-plugin-http = "2.4.2"
tauri-plugin-notification = "2.2.2"
tauri-plugin-valtio = "2.1.0"
tauri-plugin-valtio = "2.1.1"
tauri-plugin-store = "2.2.0"
tauri-plugin-system-info = "2.0.9"
tauri-plugin-theme = "2.1.3"
@@ -64,5 +64,6 @@ default = [ "custom-protocol" ]
custom-protocol = [ "tauri/custom-protocol" ]
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-cli = "2"
tauri-plugin-global-shortcut = "2.2.0"
tauri-plugin-single-instance = "2.2.2"

View File

@@ -27,6 +27,7 @@
"deep-link:allow-get-current",
"autostart:default",
"autostart:allow-enable",
"autostart:allow-disable"
"autostart:allow-disable",
"cli:default"
]
}

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","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"]}}
{"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","cli:default"],"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

@@ -2039,6 +2039,21 @@
"type": "string",
"const": "autostart:deny-is-enabled"
},
{
"description": "Allows reading the CLI matches",
"type": "string",
"const": "cli:default"
},
{
"description": "Enables the cli_matches command without any pre-configured scope.",
"type": "string",
"const": "cli:allow-cli-matches"
},
{
"description": "Denies the cli_matches command without any pre-configured scope.",
"type": "string",
"const": "cli:deny-cli-matches"
},
{
"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",

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,56 @@
"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": "Allows reading the CLI matches",
"type": "string",
"const": "cli:default"
},
{
"description": "Enables the cli_matches command without any pre-configured scope.",
"type": "string",
"const": "cli:allow-cli-matches"
},
{
"description": "Denies the cli_matches command without any pre-configured scope.",
"type": "string",
"const": "cli:deny-cli-matches"
},
{
"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 +2144,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 +2194,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 +3009,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 +3379,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 +3689,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 +6104,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 +6124,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 +6174,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 +6204,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 +6224,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 +6274,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

@@ -2039,6 +2039,21 @@
"type": "string",
"const": "autostart:deny-is-enabled"
},
{
"description": "Allows reading the CLI matches",
"type": "string",
"const": "cli:default"
},
{
"description": "Enables the cli_matches command without any pre-configured scope.",
"type": "string",
"const": "cli:allow-cli-matches"
},
{
"description": "Denies the cli_matches command without any pre-configured scope.",
"type": "string",
"const": "cli:deny-cli-matches"
},
{
"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",

View File

@@ -1,6 +1,7 @@
use crate::steam;
use crate::tool::*;
use crate::vdf::preset;
use crate::vdf::preset::VideoConfig;
use crate::wrap_err;
use anyhow::Result;
@@ -54,6 +55,9 @@ pub fn get_powerplan() -> Result<i32, String> {
#[cfg(target_os = "windows")]
let powerplan = powerplan::get_powerplan()?;
#[cfg(not(target_os = "windows"))]
let powerplan = powerplan::PowerPlanMode::Other as i32;
Ok(powerplan)
}
@@ -67,7 +71,7 @@ pub fn set_powerplan(plan: i32) -> Result<(), String> {
#[tauri::command]
pub fn get_steam_users(steam_dir: &str) -> Result<Vec<preset::User>, String> {
wrap_err!(preset::get_users(steam_dir))
wrap_err!(preset::get_users(steam_dir))
}
#[tauri::command]
@@ -78,6 +82,31 @@ pub fn set_auto_login_user(user: &str) -> Result<String, String> {
Ok(format!("Set auto login user to {}", user))
}
#[tauri::command]
pub fn get_cs2_video_config(steam_dir: &str, steam_id32: u32) -> Result<VideoConfig, String> {
let p = format!(
"{}/userdata/{}/730/local/cfg/cs2_video.txt",
steam_dir, steam_id32
);
let video = preset::get_cs2_video(p.as_str()).map_err(|e| e.to_string())?;
Ok(video)
}
#[tauri::command]
pub fn set_cs2_video_config(
steam_dir: &str,
steam_id32: u32,
video_config: VideoConfig,
) -> Result<(), String> {
let p = format!(
"{}/userdata/{}/730/local/cfg/cs2_video.txt",
steam_dir, steam_id32
);
preset::set_cs2_video(p.as_str(), video_config).map_err(|e| e.to_string())?;
Ok(())
}
#[tauri::command]
pub fn check_path(path: &str) -> Result<bool, String> {
Ok(std::path::Path::new(&path).exists())

View File

@@ -4,12 +4,14 @@
)]
use tauri::Manager;
use tauri_plugin_deep_link::DeepLinkExt;
use tauri_plugin_autostart::MacosLauncher;
use tauri_plugin_cli::CliExt;
use tauri_plugin_deep_link::DeepLinkExt;
use tauri_plugin_store::StoreExt;
// Window Vibrancy
#[cfg(target_os = "windows")]
use window_vibrancy::apply_mica;
use window_vibrancy::apply_acrylic;
#[cfg(target_os = "macos")]
use window_vibrancy::{apply_vibrancy, NSVisualEffectMaterial};
@@ -50,7 +52,10 @@ fn main() {
.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_autostart::init(
MacosLauncher::LaunchAgent,
Some(vec!["hidden"]), /* 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())
@@ -60,9 +65,29 @@ fn main() {
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_system_info::init())
.plugin(tauri_plugin_cli::init())
// .plugin(tauri_plugin_store::Builder::default().build())
// .plugin(tauri_plugin_updater::Builder::new().build())
.setup(|app| {
// Get Window
let window = app.get_webview_window("main").unwrap();
let store = app.store("cstb.json")?;
// 获取boolean类型的hidden值Err时设置为False
let hidden: bool = store.get("hidden").and_then(|v| v.as_bool()).unwrap_or(false);
// Vibrant Window
#[cfg(target_os = "macos")]
apply_vibrancy(&window, NSVisualEffectMaterial::HudWindow, None, Some(10.0))
.expect("Unsupported platform! 'apply_vibrancy' is only supported on macOS");
#[cfg(target_os = "windows")]
apply_acrylic(&window, None)
.expect("Unsupported platform! 'apply_acrylic' is only supported on Windows");
// apply_blur(&window, Some((18, 18, 18, 0)))
// .expect("Unsupported platform! 'apply_blur' is only supported on Windows");
// Deep Link
#[cfg(desktop)]
app.deep_link().register("cstb")?;
@@ -74,20 +99,22 @@ fn main() {
tray::create_tray(handle)?;
}
// Get Window
let window = app.get_webview_window("main").unwrap();
// CLI
match app.cli().matches() {
// `matches` here is a Struct with { args, subcommand }.
// `args` is `HashMap<String, ArgData>` where `ArgData` is a struct with { value, occurrences }.
// `subcommand` is `Option<Box<SubcommandMatches>>` where `SubcommandMatches` is a struct with { name, matches }.
Ok(matches) => {
println!("{:?}", matches);
if matches.args.contains_key("hidden") && matches.args["hidden"].value == true && hidden {
window.hide().unwrap();
} else {
window.show().unwrap();
}
}
Err(_) => {}
}
// Vibrant Window
#[cfg(target_os = "macos")]
apply_vibrancy(&window, NSVisualEffectMaterial::HudWindow, None, Some(10.0))
.expect("Unsupported platform! 'apply_vibrancy' is only supported on macOS");
#[cfg(target_os = "windows")]
apply_mica(&window, Some(false))
.expect("Unsupported platform! 'apply_mica' is only supported on Windows");
// apply_blur(&window, Some((18, 18, 18, 0)))
// .expect("Unsupported platform! 'apply_blur' is only supported on Windows");
Ok(())
})
.invoke_handler(tauri::generate_handler![
@@ -100,8 +127,10 @@ fn main() {
cmds::open_path,
cmds::get_powerplan,
cmds::set_powerplan,
cmds::get_steam_users,
cmds::get_steam_users,
cmds::set_auto_login_user,
cmds::get_cs2_video_config,
cmds::set_cs2_video_config,
cmds::check_path,
on_button_clicked
])

View File

@@ -1,10 +1,12 @@
use std::os::windows::process::CommandExt;
use std::process::Command;
#[cfg(windows)]
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x08000000;
// const DETACHED_PROCESS: u32 = 0x00000008;
pub fn kill(name: &str) -> String {
#[cfg(windows)]
Command::new("taskkill")
.args(&["/IM", name, "/F"])
.creation_flags(CREATE_NO_WINDOW)
@@ -15,12 +17,17 @@ pub fn kill(name: &str) -> String {
}
pub fn run_steam() -> std::io::Result<std::process::Output> {
Command::new("cmd")
#[cfg(target_os = "windows")]
return Command::new("cmd")
.args(&["/C", "start", "steam://run"])
.creation_flags(CREATE_NO_WINDOW)
.output()
}
.output();
#[cfg(target_os = "macos")]
Command::new("open")
.args(&["-a", "Steam"])
.output()
}
pub fn get_exe_path(name: &str) -> Result<String, std::io::Error> {
// [原理]
@@ -31,11 +38,17 @@ pub fn get_exe_path(name: &str) -> Result<String, std::io::Error> {
// 进程路径
let command = format!("Get-Process {} | Select-Object path", name);
let args = command.split_whitespace().collect::<Vec<&str>>();
#[cfg(windows)]
let output = Command::new("powershell.exe")
.args(&args)
.creation_flags(CREATE_NO_WINDOW)
.output()?;
#[cfg(target_os = "macos")]
let output = Command::new("osascript")
.args(&["-e", &format!("tell application \"{}\" to get path to me", name)])
.output()?;
let out = String::from_utf8_lossy(&output.stdout).to_string();
if out.contains("Path") {
@@ -55,6 +68,7 @@ pub fn get_exe_path(name: &str) -> Result<String, std::io::Error> {
pub fn open_path(path: &str) -> Result<(), std::io::Error> {
// path中所有/ 转换为 \
let path = path.replace("/", "\\");
#[cfg(windows)]
Command::new("cmd.exe")
.args(["/c", "start", "", &path])
.creation_flags(CREATE_NO_WINDOW)

View File

@@ -42,4 +42,4 @@ macro_rules! wrap_err {
}
}
};
}
}

View File

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

View File

@@ -1,7 +1,8 @@
use std::collections::HashMap;
use std::process::Command;
use std::os::windows::process::CommandExt;
#[cfg(windows)]
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x08000000;
// const DETACHED_PROCESS: u32 = 0x00000008;
@@ -38,6 +39,7 @@ impl PowerPlan {
.get(&mode)
.ok_or("Invalid power plan number (expect from 1 to 4)")?;
#[cfg(target_os = "windows")]
let output = Command::new("powercfg")
.arg("/S")
.arg(guid)
@@ -45,6 +47,14 @@ impl PowerPlan {
.output()
.map_err(|e| format!("Failed to execute powercfg command: {}", e))?;
#[cfg(target_os = "macos")]
let output = Command::new("pmset")
.arg("-g")
.arg("plan")
.output()
.map_err(|e| format!("Failed to execute pmset command: {}", e))?;
if !output.status.success() {
return Err(format!(
"Powercfg command failed: {}",
@@ -56,12 +66,20 @@ impl PowerPlan {
}
pub fn get(&self) -> Result<i32, String> {
#[cfg(windows)]
let output = Command::new("powercfg")
.arg("/L")
.creation_flags(CREATE_NO_WINDOW)
.output()
.map_err(|e| format!("Failed to execute powercfg command: {}", e))?;
#[cfg(target_os = "macos")]
let output = Command::new("pmset")
.arg("-g")
.arg("plan")
.output()
.map_err(|e| format!("Failed to execute pmset 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))?;

View File

@@ -1,12 +1,125 @@
use tauri::{
menu::{Menu, MenuItem},
menu::{CheckMenuItem, Menu, MenuItem, PredefinedMenuItem},
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
Manager, Runtime,
Emitter, Listener, Manager, Runtime,
};
use crate::tool::powerplan::PowerPlanMode;
pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
let quit_i = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
let menu = Menu::with_items(app, &[&quit_i])?;
// 托盘菜单项目
let separator = &PredefinedMenuItem::separator(app).unwrap();
let show_i = &MenuItem::with_id(app, "show", "显示主界面", true, None::<&str>)?;
let quit_i = &MenuItem::with_id(app, "quit", "退出", true, None::<&str>)?;
let kill_game_i = &MenuItem::with_id(app, "kill_game", "关闭CS2", true, None::<&str>)?;
let kill_steam_i = &MenuItem::with_id(app, "kill_steam", "关闭Steam", true, None::<&str>)?;
let launch_ww_i = &MenuItem::with_id(app, "launch_ww", "启动国际服", true, None::<&str>)?;
let launch_pw_i = &MenuItem::with_id(app, "launch_pw", "启动国服", true, None::<&str>)?;
let power_plan_extreme = CheckMenuItem::with_id(
app,
"power_plan_extreme",
"卓越性能",
true,
false,
None::<&str>,
)?;
let power_plan_high =
CheckMenuItem::with_id(app, "power_plan_high", "高性能", true, false, None::<&str>)?;
let power_plan_balanced = CheckMenuItem::with_id(
app,
"power_plan_balanced",
"平衡",
true,
false,
None::<&str>,
)?;
let power_plan_powersave = CheckMenuItem::with_id(
app,
"power_plan_powersave",
"节能",
true,
false,
None::<&str>,
)?;
let current_launch_option = MenuItem::with_id(
app,
"current_launch_option",
"启动项档位",
true,
None::<&str>,
)?;
// 创建托盘菜单
let menu = Menu::with_items(
app,
&[
&power_plan_extreme,
&power_plan_high,
&power_plan_balanced,
&power_plan_powersave,
separator,
&current_launch_option,
launch_ww_i,
launch_pw_i,
separator,
kill_game_i,
kill_steam_i,
separator,
show_i,
quit_i,
],
)?;
let _ = app.listen("tray://get_powerplan", move |event| {
if let Ok(payload) = event.payload().parse::<i32>() {
match payload {
x if x == PowerPlanMode::Other as i32 => {
let _ = power_plan_powersave.set_checked(false);
let _ = power_plan_balanced.set_checked(false);
let _ = power_plan_high.set_checked(false);
let _ = power_plan_extreme.set_checked(false);
}
x if x == PowerPlanMode::PowerSaving as i32 => {
let _ = power_plan_powersave.set_checked(true);
let _ = power_plan_balanced.set_checked(false);
let _ = power_plan_high.set_checked(false);
let _ = power_plan_extreme.set_checked(false);
}
x if x == PowerPlanMode::Balanced as i32 => {
let _ = power_plan_powersave.set_checked(false);
let _ = power_plan_balanced.set_checked(true);
let _ = power_plan_high.set_checked(false);
let _ = power_plan_extreme.set_checked(false);
}
x if x == PowerPlanMode::HighPerformance as i32 => {
let _ = power_plan_powersave.set_checked(false);
let _ = power_plan_balanced.set_checked(false);
let _ = power_plan_high.set_checked(true);
let _ = power_plan_extreme.set_checked(false);
}
x if x == PowerPlanMode::Extreme as i32 => {
let _ = power_plan_powersave.set_checked(false);
let _ = power_plan_balanced.set_checked(false);
let _ = power_plan_high.set_checked(false);
let _ = power_plan_extreme.set_checked(true);
}
_ => {}
}
}
});
let _ = app.listen("tray://get_current_launch_option", move |event| {
let payload = event.payload();
if payload != "" {
let _ = current_launch_option.set_text("启动项档位 ".to_string() + payload);
}
});
let _ = TrayIconBuilder::with_id("tray")
.icon(app.default_window_icon().unwrap().clone())
@@ -16,7 +129,43 @@ pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
"quit" => {
app.exit(0);
}
// Add more events here
"show" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
}
"launch_ww" => {
let _ = app.emit("tray://launch_game", "worldwide");
}
"launch_pw" => {
let _ = app.emit("tray://launch_game", "perfectworld");
}
"kill_game" => {
let _ = app.emit("tray://kill_game", None::<()>);
}
"kill_steam" => {
let _ = app.emit("tray://kill_steam", None::<()>);
}
"power_plan_extreme" => {
let _ = app.emit("tray://set_powerplan", PowerPlanMode::Extreme as i32);
// let _ = power_plan_extreme.set_checked(true);
}
"power_plan_high" => {
let _ = app.emit(
"tray://set_powerplan",
PowerPlanMode::HighPerformance as i32,
);
// let _ = power_plan_high.set_checked(true);
}
"power_plan_balanced" => {
let _ = app.emit("tray://set_powerplan", PowerPlanMode::Balanced as i32);
// let _ = power_plan_balanced.set_checked(true);
}
"power_plan_powersave" => {
let _ = app.emit("tray://set_powerplan", PowerPlanMode::PowerSaving as i32);
// let _ = power_plan_powersave.set_checked(true);
}
_ => {}
})
.on_tray_icon_event(|tray, event| {
@@ -37,68 +186,3 @@ pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
Ok(())
}
// Tray Menu
// let quit = CustomMenuItem::new("quit".to_string(), "Quit");
// let hide = CustomMenuItem::new("hide".to_string(), "Hide");
// let tray_menu = SystemTrayMenu::new() // insert the menu items here
// .add_item(hide)
// .add_item(quit);
// .add_native_item(SystemTrayMenuItem::Separator)
// let toggle = MenuItemBuilder::with_id("toggle", "Toggle").build(app)?;
// let menu = MenuBuilder::new(app).items(&[&toggle]).build()?;
// Setup Tray
// let tray = tauri::tray::TrayIconBuilder::with_id("my-tray").build(app)?;
// let _ = TrayIconBuilder::new()
// .menu(&menu)
// .on_menu_event(move |_, event| {
// match event.id().as_ref() {
// "toggle" => {
// println!("toggle clicked");
// }
// _ => (),
// }
// // match event {
// // SystemTrayEvent::LeftClick { position: _, size: _, .. } => {
// // let window = app.get_window("main").unwrap();
// // window.show().unwrap();
// // window.set_focus().unwrap();
// // // thread::sleep(Duration::from_millis(100));
// // // window.set_always_on_top(false).unwrap();
// // println!("system tray received a left click");
// // }
// // SystemTrayEvent::RightClick { position: _, size: _, .. } => {
// // // let window = app.get_window("main").unwrap();
// // // window.hide().unwrap();
// // println!("system tray received a right click");
// // }
// // SystemTrayEvent::DoubleClick { position: _, size: _, .. } => {
// // println!("system tray received a double click");
// // }
// // SystemTrayEvent::MenuItemClick { id, .. } =>
// // match id.as_str() {
// // "quit" => {
// // std::process::exit(0);
// // }
// // "hide" => {
// // let window = app.get_window("main").unwrap();
// // window.hide().unwrap();
// // }
// // _ => {}
// // }
// // _ => {}
// // }
// })
// .on_tray_icon_event(|tray, event| {
// if event.click_type == ClickType::Left {
// let app = tray.app_handle();
// if let Some(webview_window) = app.get_webview_window("main") {
// let _ = webview_window.show();
// let _ = webview_window.set_focus();
// }
// }
// })
// .build(app)
// .unwrap();

View File

@@ -1,11 +1,12 @@
pub fn to_json(vdf_data: &str) -> String {
let linebreak = match std::env::consts::OS {
"macos" => "\r",
"macos" => "\n", //"\r",
"windows" => "\n",
"linux" => "\n",
_ => "\n",
};
// NOTE: 这样会跳过顶层{}
let startpoint = vdf_data.find('{').unwrap_or(0);
let vdf_data = &vdf_data[startpoint..];
@@ -31,49 +32,108 @@ pub fn to_json(vdf_data: &str) -> String {
json_data.push_str(&line);
}
// let json_str = json_data
json_data = json_data
.replace(",}", "}")
.trim_start_matches(": ")
.trim_end_matches(',')
.to_string();
// json_data = format!("{{{}}}", json_str);
json_data
return json_data;
}
pub fn to_vdf(json_data: &str) -> String {
let json_value: serde_json::Value = serde_json::from_str(json_data).unwrap();
let mut vdf_data = String::new();
build_vdf(&json_value, &mut vdf_data, 0);
vdf_data
}
fn build_vdf(json_value: &serde_json::Value, vdf_data: &mut String, indent_level: usize) {
match json_value {
serde_json::Value::Object(obj) => {
for (key, value) in obj {
vdf_data.push_str(&"\t".repeat(indent_level));
vdf_data.push_str(&format!("\"{}\"\n", key));
vdf_data.push_str(&"\t".repeat(indent_level));
vdf_data.push_str("{\n");
build_vdf(value, vdf_data, indent_level + 1);
vdf_data.push_str(&"\t".repeat(indent_level));
vdf_data.push_str("}\n");
}
}
serde_json::Value::String(s) => {
vdf_data.push_str(&"\t".repeat(indent_level));
vdf_data.push_str(&format!("\"{}\"\t\t\"{}\"\n", s, s));
}
_ => {
vdf_data.push_str(&"\t".repeat(indent_level));
vdf_data.push_str(&format!("\"{}\"\t\t\"{}\"\n", json_value, json_value));
}
}
}
mod tests {
use super::*;
static VDF_DATA: &str = r#""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"
}
}
"#;
static JSON_DATA: &str = r#"{
"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"
}
}
}"#;
#[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);
let json_data = to_json(VDF_DATA);
println!("{}", json_data);
// 解析json
let json_value: serde_json::Value = serde_json::from_str(&json_data).unwrap();
@@ -81,4 +141,12 @@ mod tests {
println!("{}", json_value)
// assert_eq!(to_json(vdf_data), expected_json);
}
#[test]
fn test_to_vdf() {
// let json_data = r#"{"key1": "value1","key2": "value2","subkey": {"key3": "value3"}}"#;
let vdf_data = to_vdf(JSON_DATA);
println!("{}", vdf_data);
}
}

View File

@@ -1,6 +1,7 @@
use anyhow::Result;
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use regex::Regex;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
@@ -44,6 +45,81 @@ pub struct LocalUser {
avatar_key: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct VideoConfig {
version: String,
vendor_id: String,
device_id: String,
cpu_level: String,
gpu_mem_level: String,
gpu_level: String,
knowndevice: String,
defaultres: String,
defaultresheight: String,
refreshrate_numerator: String,
refreshrate_denominator: String,
fullscreen: String,
coop_fullscreen: String,
nowindowborder: String,
mat_vsync: String,
fullscreen_min_on_focus_loss: String,
high_dpi: String,
auto_config: String,
shaderquality: String,
r_texturefilteringquality: String,
msaa_samples: String,
r_csgo_cmaa_enable: String,
videocfg_shadow_quality: String,
videocfg_dynamic_shadows: String,
videocfg_texture_detail: String,
videocfg_particle_detail: String,
videocfg_ao_detail: String,
videocfg_hdr_detail: String,
videocfg_fsr_detail: String,
monitor_index: String,
r_low_latency: String,
aspectratiomode: String,
}
impl Default for VideoConfig {
fn default() -> Self {
VideoConfig {
version: "15".to_string(),
vendor_id: "0".to_string(),
device_id: "0".to_string(),
cpu_level: "3".to_string(),
gpu_mem_level: "3".to_string(),
gpu_level: "3".to_string(),
knowndevice: "0".to_string(),
defaultres: "1920".to_string(),
defaultresheight: "1080".to_string(),
refreshrate_numerator: "144".to_string(),
refreshrate_denominator: "1".to_string(),
fullscreen: "1".to_string(),
coop_fullscreen: "0".to_string(),
nowindowborder: "1".to_string(),
mat_vsync: "0".to_string(),
fullscreen_min_on_focus_loss: "1".to_string(),
high_dpi: "0".to_string(),
auto_config: "2".to_string(),
shaderquality: "0".to_string(),
r_texturefilteringquality: "3".to_string(),
msaa_samples: "2".to_string(),
r_csgo_cmaa_enable: "0".to_string(),
videocfg_shadow_quality: "0".to_string(),
videocfg_dynamic_shadows: "1".to_string(),
videocfg_texture_detail: "1".to_string(),
videocfg_particle_detail: "0".to_string(),
videocfg_ao_detail: "0".to_string(),
videocfg_hdr_detail: "3".to_string(),
videocfg_fsr_detail: "0".to_string(),
monitor_index: "0".to_string(),
r_low_latency: "1".to_string(),
aspectratiomode: "0".to_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() {
@@ -151,6 +227,9 @@ pub fn parse_local_users(steam_dir: &str) -> Result<Vec<LocalUser>> {
let json_data = super::parse::to_json(&data);
let kv: HashMap<String, Value> = serde_json::from_str(&json_data)?;
// 剥离顶层 UserLocalConfigStore
// let kv = kv.get("UserLocalConfigStore").and_then(|v| v.as_object()).unwrap();
// 获取 friends 节点
let friends = kv.get("friends").and_then(|v| v.as_object());
if friends.is_none() {
@@ -270,6 +349,184 @@ fn read_avatar(steam_dir: &str, steam_id64: &str) -> Option<String> {
}
}
pub fn get_cs2_video(file_path: &str) -> Result<VideoConfig> {
// TODO: no kv
let data = fs::read_to_string(file_path)?;
let json_data = super::parse::to_json(&data);
let kv: HashMap<String, String> = serde_json::from_str(&json_data)?;
let video_config = VideoConfig {
version: kv.get("Version").unwrap_or(&"".to_string()).to_string(),
vendor_id: kv.get("VendorID").unwrap_or(&"".to_string()).to_string(),
device_id: kv.get("DeviceID").unwrap_or(&"".to_string()).to_string(),
cpu_level: kv
.get("setting.cpu_level")
.unwrap_or(&"".to_string())
.to_string(),
gpu_mem_level: kv
.get("setting.gpu_mem_level")
.unwrap_or(&"".to_string())
.to_string(),
gpu_level: kv
.get("setting.gpu_level")
.unwrap_or(&"".to_string())
.to_string(),
knowndevice: kv
.get("setting.knowndevice")
.unwrap_or(&"".to_string())
.to_string(),
defaultres: kv
.get("setting.defaultres")
.unwrap_or(&"".to_string())
.to_string(),
defaultresheight: kv
.get("setting.defaultresheight")
.unwrap_or(&"".to_string())
.to_string(),
refreshrate_numerator: kv
.get("setting.refreshrate_numerator")
.unwrap_or(&"".to_string())
.to_string(),
refreshrate_denominator: kv
.get("setting.refreshrate_denominator")
.unwrap_or(&"".to_string())
.to_string(),
fullscreen: kv
.get("setting.fullscreen")
.unwrap_or(&"".to_string())
.to_string(),
coop_fullscreen: kv
.get("setting.coop_fullscreen")
.unwrap_or(&"".to_string())
.to_string(),
nowindowborder: kv
.get("setting.nowindowborder")
.unwrap_or(&"".to_string())
.to_string(),
mat_vsync: kv
.get("setting.mat_vsync")
.unwrap_or(&"".to_string())
.to_string(),
fullscreen_min_on_focus_loss: kv
.get("setting.fullscreen_min_on_focus_loss")
.unwrap_or(&"".to_string())
.to_string(),
high_dpi: kv
.get("setting.high_dpi")
.unwrap_or(&"".to_string())
.to_string(),
auto_config: kv.get("AutoConfig").unwrap_or(&"".to_string()).to_string(),
shaderquality: kv
.get("setting.shaderquality")
.unwrap_or(&"".to_string())
.to_string(),
r_texturefilteringquality: kv
.get("setting.r_texturefilteringquality")
.unwrap_or(&"".to_string())
.to_string(),
msaa_samples: kv
.get("setting.msaa_samples")
.unwrap_or(&"".to_string())
.to_string(),
r_csgo_cmaa_enable: kv
.get("setting.r_csgo_cmaa_enable")
.unwrap_or(&"".to_string())
.to_string(),
videocfg_shadow_quality: kv
.get("setting.videocfg_shadow_quality")
.unwrap_or(&"".to_string())
.to_string(),
videocfg_dynamic_shadows: kv
.get("setting.videocfg_dynamic_shadows")
.unwrap_or(&"".to_string())
.to_string(),
videocfg_texture_detail: kv
.get("setting.videocfg_texture_detail")
.unwrap_or(&"".to_string())
.to_string(),
videocfg_particle_detail: kv
.get("setting.videocfg_particle_detail")
.unwrap_or(&"".to_string())
.to_string(),
videocfg_ao_detail: kv
.get("setting.videocfg_ao_detail")
.unwrap_or(&"".to_string())
.to_string(),
videocfg_hdr_detail: kv
.get("setting.videocfg_hdr_detail")
.unwrap_or(&"".to_string())
.to_string(),
videocfg_fsr_detail: kv
.get("setting.videocfg_fsr_detail")
.unwrap_or(&"".to_string())
.to_string(),
monitor_index: kv
.get("setting.monitor_index")
.unwrap_or(&"".to_string())
.to_string(),
r_low_latency: kv
.get("setting.r_low_latency")
.unwrap_or(&"".to_string())
.to_string(),
aspectratiomode: kv
.get("setting.aspectratiomode")
.unwrap_or(&"".to_string())
.to_string(),
};
Ok(video_config)
}
pub fn set_cs2_video(file_path: &str, data: VideoConfig) -> Result<()> {
// 读取文件内容
let file_content = fs::read_to_string(file_path)?;
// 定义正则表达式匹配模式
let re = Regex::new(r#""(setting\.\w+)"\s+"-?\d+""#).unwrap();
// 替换字段值
let updated_content = re.replace_all(&file_content, |caps: &regex::Captures| {
let key = &caps[1]; // 捕获的键名
let value = match key {
"Version" => &data.version,
"VendorID" => &data.vendor_id,
"DeviceID" => &data.device_id,
"setting.cpu_level" => &data.cpu_level,
"setting.gpu_mem_level" => &data.gpu_mem_level,
"setting.gpu_level" => &data.gpu_level,
"setting.knowndevice" => &data.knowndevice,
"setting.defaultres" => &data.defaultres,
"setting.defaultresheight" => &data.defaultresheight,
"setting.refreshrate_numerator" => &data.refreshrate_numerator,
"setting.refreshrate_denominator" => &data.refreshrate_denominator,
"setting.fullscreen" => &data.fullscreen,
"setting.coop_fullscreen" => &data.coop_fullscreen,
"setting.nowindowborder" => &data.nowindowborder,
"setting.mat_vsync" => &data.mat_vsync,
"setting.fullscreen_min_on_focus_loss" => &data.fullscreen_min_on_focus_loss,
"setting.high_dpi" => &data.high_dpi,
"AutoConfig" => &data.auto_config,
"setting.shaderquality" => &data.shaderquality,
"setting.r_texturefilteringquality" => &data.r_texturefilteringquality,
"setting.msaa_samples" => &data.msaa_samples,
"setting.r_csgo_cmaa_enable" => &data.r_csgo_cmaa_enable,
"setting.videocfg_shadow_quality" => &data.videocfg_shadow_quality,
"setting.videocfg_dynamic_shadows" => &data.videocfg_dynamic_shadows,
"setting.videocfg_texture_detail" => &data.videocfg_texture_detail,
"setting.videocfg_particle_detail" => &data.videocfg_particle_detail,
"setting.videocfg_ao_detail" => &data.videocfg_ao_detail,
"setting.videocfg_hdr_detail" => &data.videocfg_hdr_detail,
"setting.videocfg_fsr_detail" => &data.videocfg_fsr_detail,
"setting.monitor_index" => &data.monitor_index,
"setting.r_low_latency" => &data.r_low_latency,
"setting.aspectratiomode" => &data.aspectratiomode,
_ => "", // 默认情况
};
format!(r#""{}" "{}""#, key, value)
});
fs::write(file_path, updated_content.as_ref())?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
@@ -279,4 +536,25 @@ mod tests {
let users = get_users("D:\\Programs\\Steam").unwrap();
println!("{:?}", users);
}
#[test]
fn test_get_cs2_video() {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let file_path = format!("{}/src/vdf/tests/cs2_video.txt", manifest_dir);
let video_config = get_cs2_video(&file_path).unwrap();
println!("{:?}", video_config);
}
#[test]
fn test_set_cs2_video() {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let file_path = format!("{}/temp/cs2_video.txt", manifest_dir);
fs::copy(
format!("{}/src/vdf/tests/cs2_video.txt", manifest_dir),
file_path.clone(),
)
.unwrap();
let video_config = VideoConfig::default();
set_cs2_video(&file_path, video_config).unwrap();
}
}

View File

@@ -42,7 +42,7 @@
},
"productName": "CS工具箱",
"mainBinaryName": "cstb",
"version": "0.0.4",
"version": "0.0.5",
"identifier": "upup.cool",
"plugins": {
"deep-link": {
@@ -51,6 +51,15 @@
"cstb"
]
}
},
"cli": {
"description": "CS Toolbox CLI",
"args": [
{
"name": "hidden",
"description": "hidden on start"
}
]
}
},
"app": {
@@ -70,7 +79,8 @@
"transparent": true,
"theme": null,
"hiddenTitle": true,
"titleBarStyle": "Transparent"
"titleBarStyle": "Transparent",
"visible": false
}
],
"security": {

View File

@@ -7,12 +7,13 @@ 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">
<div className="flex flex-grow w-full gap-4">
<Notice />
<SmartTransfer />
<SmartTransfer />
</div>
<CommonDir />

View File

@@ -17,6 +17,21 @@ export default function Page() {
>
{app.state.autoStart ? "开" : "关"}
</Switch>
<Switch
isSelected={app.state.startHidden}
size="sm"
onChange={(e) => app.setStartHidden(e.target.checked)}
>
{app.state.startHidden ? "开" : "关"}
</Switch>
{/* hiddenOnClose */}
<Switch
isSelected={app.state.hiddenOnClose}
size="sm"
onChange={(e) => app.setHiddenOnClose(e.target.checked)}
>
{app.state.hiddenOnClose ? "开" : "关"}
</Switch>
</div>
)
}

View File

@@ -3,17 +3,11 @@ import {
Card,
CardBody,
CardHeader,
CardIcon,
CardTool,
CardIcon
} from "@/components/window/Card"
import { ToolButton } from "@/components/window/ToolButton"
import { cn } from "@heroui/react"
import {
AssemblyLine,
HardDisk,
SettingConfig,
UploadOne,
Videocamera,
AssemblyLine, SettingConfig, Videocamera
} from "@icon-park/react"
import { usePathname, useRouter } from "next/navigation"
// import { platform } from "@tauri-apps/plugin-os"
@@ -49,7 +43,8 @@ export default function PreferenceLayout({
<Videocamera />
</CardIcon>
<CardTool>
{/* TODO 完善云同步等功能 */}
{/* <CardTool>
<ToolButton>
<UploadOne />
云同步
@@ -58,7 +53,7 @@ export default function PreferenceLayout({
<HardDisk />
保存
</ToolButton>
</CardTool>
</CardTool> */}
</CardHeader>
<CardBody>{children}</CardBody>
</Card>

View File

@@ -1,21 +1,71 @@
"use client"
import { init } from "@/store"
import { useSteamStore } from "@/store/steam"
import { useToolStore } from "@/store/tool"
import { addToast } from "@heroui/react"
import { invoke } from "@tauri-apps/api/core"
import { listen } from "@tauri-apps/api/event"
import { useDebounce } from "ahooks"
import { useEffect } from "react"
import "./globals.css"
import Providers from "./providers"
import { init } from "@/store"
import { useDebounce } from "ahooks"
import { PowerPlans } from "@/components/cstb/PowerPlan"
export default function RootLayout({ children }: { children: React.ReactNode }) {
const steam = useSteamStore()
const tool = useToolStore()
useEffect(() => {
void init()
})
void listen<string>("tray://launch_game", async (event) => {
await invoke("launch_game", {
steamPath: `${steam.state.steamDir}/steam.exe`,
launchOption: tool.state.launchOptions[tool.state.launchIndex].option || "",
server: event.payload || "worldwide",
})
addToast({ title: "启动国服成功" })
})
void listen("tray://kill_steam", async () => {
await invoke("kill_steam")
addToast({ title: "已关闭Steam" })
})
void listen("tray://kill_game", async () => {
await invoke("kill_game")
addToast({ title: "已关闭CS2" })
})
void listen<number>("tray://set_powerplan", async (event) => {
if (typeof(event.payload) === "number" && event.payload <= 0 && event.payload > 4) return
await invoke("set_powerplan", { plan: event.payload })
const current = await invoke<number>("get_powerplan")
tool.setPowerPlan(current)
addToast({ title: `电源计划已切换 → ${PowerPlans[current].title}` })
})
}, [])
// 检测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})
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])

View File

@@ -23,7 +23,7 @@ const FastLaunch = () => {
onPress={() => {
void invoke("launch_game", {
steamPath: `${steam.state.steamDir}/steam.exe`,
launchOption: tool.state.launchOptions[tool.state.launchIndex] || "",
launchOption: tool.state.launchOptions[tool.state.launchIndex].option || "",
server: "perfectworld",
})
addToast({ title: "启动国服成功" })
@@ -37,7 +37,7 @@ const FastLaunch = () => {
onPress={() => {
void invoke("launch_game", {
steamPath: `${steam.state.steamDir}/steam.exe`,
launchOption: tool.state.launchOptions[tool.state.launchIndex] || "",
launchOption: tool.state.launchOptions[tool.state.launchIndex].option || "",
server: "worldwide",
})
addToast({ title: "启动国际服成功" })

View File

@@ -3,7 +3,7 @@ import { Plus, SettingConfig, Switch } from "@icon-park/react"
import { useEffect, useState } from "react"
import { Card, CardBody, CardHeader, CardIcon, CardTool } from "../window/Card"
import { ToolButton } from "../window/ToolButton"
import { input, Textarea } from "@heroui/react"
import { Tooltip } from "@heroui/react"
const LaunchOption = () => {
const tool = useToolStore()
@@ -29,10 +29,12 @@ const LaunchOption = () => {
<Plus />
</ToolButton>
<ToolButton>
<Switch />
</ToolButton>
<Tooltip content="功能测试中,尚未实装" showArrow={true} delay={300}>
<ToolButton>
<Switch />
</ToolButton>
</Tooltip>
</CardTool>
</CardHeader>
<CardBody>

View File

@@ -1,17 +1,12 @@
"use client"
import {
Card,
CardBody,
CardHeader,
CardIcon,
CardTool,
} from "@/components/window/Card"
import { Card, CardBody, CardHeader, CardIcon, CardTool } from "@/components/window/Card"
import { useAppStore } from "@/store/app"
import { createClient } from "@/utils/supabase/client"
import { Skeleton } from "@heroui/react"
import { Refresh, VolumeNotice } from "@icon-park/react"
import { Link, Skeleton } from "@heroui/react"
import { Refresh, VolumeNotice, WebPage } from "@icon-park/react"
import useSWR, { useSWRConfig } from "swr"
import { ToolButton } from "../window/ToolButton"
import { MarkdownRender } from "../markdown"
const Notice = () => {
const { mutate } = useSWRConfig()
@@ -23,6 +18,12 @@ const Notice = () => {
<VolumeNotice />
</CardIcon>
<CardTool>
<Link href="https://cstb.upup.cool" target="_blank" className="dark:text-white text-zinc-800" >
<ToolButton>
<WebPage />
</ToolButton>
</Link>
<ToolButton onClick={() => mutate("/api/notice")}>
<Refresh />
@@ -45,15 +46,13 @@ const NoticeBody = () => {
.from("Notice")
.select("created_at, content, url")
.order("created_at", { ascending: false })
.limit(1)
.single()
return data
}
const { data: notice /* , error */, isLoading } = useSWR(
"/api/notice",
noticeFetcher,
)
const { data: notice /* , error */, isLoading } = useSWR("/api/notice", noticeFetcher)
// if (error) return <>错误:{error}</>
if (isLoading)
@@ -64,11 +63,28 @@ const NoticeBody = () => {
)
return (
<>
{notice?.content ||
app.state.notice ||
"不会真的有人要更新CSGO工具箱吧不会吧不会吧 xswl"}
</>
<div className="flex flex-col h-full gap-2">
<div className="">
<MarkdownRender>
{notice?.content ||
app.state.notice ||
"不会真的有人要更新CSGO工具箱吧不会吧不会吧 xswl"}
</MarkdownRender>
</div>
{/* {notice?.url && (
<Button
variant="flat"
// color="default"
as={Link}
href={notice?.url}
target="_blank"
size="sm"
className="bg-transparent w-fit"
>
传送门
</Button>
)} */}
</div>
)
}

View File

@@ -7,7 +7,7 @@ import { Key } from "@react-types/shared"
import { useToolStore } from "@/store/tool"
import { useEffect } from "react"
const PowerPlans = [
export const PowerPlans = [
{
id: "0",
title: "其他",

View File

@@ -5,7 +5,6 @@ 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"
/**

View File

@@ -1,27 +1,30 @@
import { FolderConversion, FolderPlus } from "@icon-park/react"
import { Card, CardBody, CardHeader, CardIcon, CardTool } from "../window/Card"
import { ToolButton } from "../window/ToolButton"
import { Tooltip } from "@heroui/react"
const SmartTransfer = () => {
return (
<Card>
<CardHeader>
<CardIcon>
<FolderConversion />
</CardIcon>
<CardTool>
<ToolButton>
<FolderPlus /> {" "}
</ToolButton>
</CardTool>
</CardHeader>
<CardBody>
<div className="flex flex-col items-center justify-center w-full h-full p-5 text-lg font-medium transition rounded-lg cursor-pointer select-none bg-black/5 hover:bg-black/10">
<p></p>
<p> .dem .cfg</p>
</div>
</CardBody>
</Card>
<Tooltip content="功能测试中,尚未实装" showArrow={true} delay={300}>
<Card>
<CardHeader>
<CardIcon>
<FolderConversion />
</CardIcon>
<CardTool>
<ToolButton>
<FolderPlus /> {" "}
</ToolButton>
</CardTool>
</CardHeader>
<CardBody>
<div className="flex flex-col items-center justify-center w-full h-full p-5 text-lg font-medium transition rounded-lg cursor-pointer select-none bg-black/5 hover:bg-black/10">
<p></p>
<p> .dem .cfg</p>
</div>
</CardBody>
</Card>
</Tooltip>
)
}

View File

@@ -1,54 +1,257 @@
import { CloseSmall, Down, Edit, Plus, SettingConfig, Up } from "@icon-park/react"
import { useState } from "react"
import { useEffect, 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 { addToast, NumberInput, Tab, Tabs, Tooltip } from "@heroui/react"
import { motion } from "framer-motion"
import { useToolStore } from "@/store/tool"
import { useToolStore, VideoSetting as VideoConfig, VideoSettingTemplate } from "@/store/tool"
import { useSteamStore } from "@/store/steam"
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] || "")
const steam = useSteamStore()
const videoSettings = (video: VideoConfig) => {
return [
{
type: "fullscreen",
title: "全屏",
value: video.fullscreen === "1" ? "全屏" : "窗口",
options: ["窗口", "全屏"],
mapping: (value: string) => {
return (
{
: "0",
: "1",
}[value] || "0"
)
},
},
{
type: "mat_vsync",
title: "垂直同步",
value: video.mat_vsync === "1" ? "开启" : "关闭",
options: ["关闭", "开启"],
mapping: (value: string) => {
return (
{
: "0",
: "1",
}[value] || "0"
)
},
},
{
type: "r_low_latency",
title: "低延迟模式",
value: video.r_low_latency === "1" ? "开启" : "关闭",
options: ["关闭", "开启"],
mapping: (value: string) => {
return (
{
: "0",
: "1",
}[value] || "0"
)
},
},
// TODO: 改选项不在 cs2_video.txt 中
// {
// type: "r_player_visible_mode",
// title: "增强角色对比度",
// value: video.r_csgo_cmaa_enable === "1" ? "启用" : "禁用",
// options: ["禁用", "启用"],
// },
{
type: "r_csgo_cmaa_enable",
title: "CMAA2抗锯齿",
value: video.r_csgo_cmaa_enable === "1" ? "开启" : "关闭",
options: ["关闭", "开启"],
mapping: (value: string) => {
return (
{
: "0",
: "1",
}[value] || "0"
)
},
},
{
type: "msaa_samples",
title: "多重采样抗锯齿",
value:
{
0: "无",
2: "2X MSAA",
4: "4X MSAA",
8: "8X MSAA",
}[parseInt(video.msaa_samples, 10)] || "无",
options: ["无", "2X MSAA", "4X MSAA", "8X MSAA"],
mapping: (value: string) => {
return (
{
: "0",
"2X MSAA": "2",
"4X MSAA": "4",
"8X MSAA": "8",
}[value] || "0"
)
},
},
{
type: "videocfg_shadow_quality",
title: "全局阴影效果",
value: ["低", "中", "高", "非常高"][parseInt(video.videocfg_shadow_quality, 10)] || "低",
options: ["低", "中", "高", "非常高"],
mapping: (value: string) => {
return (
{
: "0",
: "1",
: "2",
: "3",
}[value] || "0"
)
},
},
{
type: "videocfg_dynamic_shadows",
title: "动态阴影",
value: ["仅限日光", "全部"][parseInt(video.videocfg_dynamic_shadows, 10)] || "仅限日光",
options: ["仅限日光", "全部"],
mapping: (value: string) => {
return (
{
: "0",
: "1",
}[value] || "0"
)
},
},
{
type: "videocfg_texture_detail",
title: "模型/贴图细节",
value: ["低", "中", "高"][parseInt(video.videocfg_texture_detail, 10)] || "低",
options: ["低", "中", "高"],
mapping: (value: string) => {
return (
{
: "0",
: "1",
: "2",
}[value] || "0"
)
},
},
{
type: "r_texturefilteringquality",
title: "贴图过滤模式",
value:
["双线性", "三线性", "异向 2X", "异向 4X", "异向 8X", "异向 16X"][
parseInt(video.r_texturefilteringquality, 10)
] || "双线性",
options: ["双线性", "三线性", "异向 2X", "异向 4X", "异向 8X", "异向 16X"],
mapping: (value: string) => {
return (
{
线: "0",
线: "1",
"异向 2X": "2",
"异向 4X": "3",
"异向 8X": "4",
"异向 16X": "5",
}[value] || "0"
)
},
},
{
type: "shaderquality",
title: "光影细节",
value: video.shaderquality === "1" ? "高" : "低",
options: ["低", "高"],
mapping: (value: string) => {
return (
{
: "0",
: "1",
}[value] || "0"
)
},
},
{
type: "videocfg_particle_detail",
title: "粒子细节",
value: ["低", "中", "高", "非常高"][parseInt(video.videocfg_particle_detail, 10)] || "低",
options: ["低", "中", "高", "非常高"],
mapping: (value: string) => {
return (
{
: "0",
: "1",
: "2",
: "3",
}[value] || "低"
)
},
},
{
type: "videocfg_ao_detail",
title: "环境光遮蔽",
value: ["已禁用", "中", "高"][parseInt(video.videocfg_ao_detail, 10)] || "已禁用",
options: ["已禁用", "中", "高"],
mapping: (value: string) => {
return (
{
: "0",
: "1",
: "2",
}[value] || "已禁用"
)
},
},
{
type: "videocfg_hdr_detail",
title: "高动态范围",
value: video.videocfg_hdr_detail === "-1" ? "品质" : "性能",
options: ["性能", "品质"],
mapping: (value: string) => {
return (
{
: "3",
: "-1",
}[value] || "3"
)
},
},
{
type: "videocfg_fsr_detail",
title: "Fidelity FX 超级分辨率",
value:
["已禁用", "超高品质", "品质", "均衡", "性能"][parseInt(video.videocfg_fsr_detail, 10)] ||
"性能",
options: ["性能", "均衡", "品质", "超高品质", "已禁用"],
mapping: (value: string) => {
return (
{
: "0",
: "1",
: "2",
: "3",
: "4",
}[value] || "已禁用"
)
},
},
]
}
// useEffect(() => {
// setLaunchOpt(tool.state.VideoSettings[tool.state.launchIndex] || "")
// }, [tool.state.launchIndex, tool.state.VideoSettings])
const [vconfig, setVconfig] = useState<VideoConfig>(tool.state.videoSetting)
// 设置对应关系
// 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: ["性能", "均衡", "品质", "超高品质", "已禁用"],
},
]
useEffect(() => {
if (steam.state.steamDirValid && steam.currentUser())
void tool.getVideoConfig(steam.state.steamDir, steam.currentUser()?.steam_id32 || 0)
}, [])
return (
<Card>
@@ -64,14 +267,40 @@ const VideoSetting = () => {
))} */}
{edit && (
<>
<ToolButton></ToolButton>
<ToolButton></ToolButton>
<ToolButton></ToolButton>
<ToolButton></ToolButton>
<ToolButton></ToolButton>
<ToolButton onClick={() => setVconfig({ ...vconfig, ...VideoSettingTemplate.low })}>
</ToolButton>
<ToolButton
onClick={() => {
addToast({ title: "测试中 功能完成后可应用设置到游戏" })
onClick={() => setVconfig({ ...vconfig, ...VideoSettingTemplate.middle })}
>
</ToolButton>
<ToolButton onClick={() => setVconfig({ ...vconfig, ...VideoSettingTemplate.high })}>
</ToolButton>
<ToolButton
onClick={() => setVconfig({ ...vconfig, ...VideoSettingTemplate.veryhigh })}
>
</ToolButton>
<ToolButton
onClick={() => setVconfig({ ...vconfig, ...VideoSettingTemplate.recommend })}
>
</ToolButton>
<ToolButton
onClick={async () => {
await tool.setVideoConfig(
steam.state.steamDir,
steam.currentUser()?.steam_id32 || 0,
vconfig
)
await tool.getVideoConfig(
steam.state.steamDir,
steam.currentUser()?.steam_id32 || 0
)
setEdit(false)
addToast({ title: "应用设置成功" })
}}
>
<Plus />
@@ -79,7 +308,17 @@ const VideoSetting = () => {
</ToolButton>
</>
)}
<ToolButton onClick={() => setEdit(!edit)}>
<ToolButton
onClick={async () => {
if (steam.state.steamDirValid && steam.currentUser())
await tool.getVideoConfig(
steam.state.steamDir,
steam.currentUser()?.steam_id32 || 0
)
setVconfig(tool.state.videoSetting)
setEdit(!edit)
}}
>
{edit ? (
<>
<CloseSmall />
@@ -92,6 +331,20 @@ const VideoSetting = () => {
</>
)}
</ToolButton>
<ToolButton
onClick={async () => {
if (steam.state.steamDirValid && steam.currentUser()) {
await tool.getVideoConfig(
steam.state.steamDir,
steam.currentUser()?.steam_id32 || 0
)
addToast({ title: "读取成功" })
} else addToast({ title: "请先选择用户", color: "danger" })
}}
>
</ToolButton>
<ToolButton onClick={() => setHide(!hide)}>
{hide ? (
<>
@@ -118,45 +371,73 @@ const VideoSetting = () => {
<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">
<span className="flex gap-3">
<NumberInput
value={tool.state.videoSetting.width}
aria-label="width"
value={parseInt(
edit ? vconfig.defaultres : tool.state.videoSetting.defaultres,
10
)}
onValueChange={(value) => {
tool.setVideoSetting({
...tool.state.videoSetting,
width: value,
})
const _ = edit
? setVconfig({
...vconfig,
defaultres: value.toString(),
})
: tool.setVideoSetting({
...tool.state.videoSetting,
defaultres: value.toString(),
})
}}
radius="full"
step={10}
className="max-w-28"
classNames={{ inputWrapper: "h-10" }}
/>
<NumberInput
value={tool.state.videoSetting.height}
aria-label="height"
value={parseInt(edit ? vconfig.defaultresheight : tool.state.videoSetting.defaultresheight, 10)}
onValueChange={(value) => {
tool.setVideoSetting({
...tool.state.videoSetting,
height: value,
})
const _ = edit
? setVconfig({
...vconfig,
defaultresheight: value.toString(),
})
: tool.setVideoSetting({
...tool.state.videoSetting,
defaultresheight: value.toString(),
})
}}
radius="full"
step={10}
className="max-w-28"
classNames={{ inputWrapper: "h-10" }}
/>
</span>
</li>
{videoSettings.map((vid, index) => (
{videoSettings(edit ? vconfig : tool.state.videoSetting).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
selectedKey={vid.value}
onSelectionChange={(key) => {
// console.log(vid.type, key)
// 修改 vconfig 名为 vid.type 的 value为 key
const _ =
edit && key
? setVconfig({
...vconfig,
[vid.type]: vid.mapping(key.toString()),
})
: null
}}
>
{vid.options.map((opt, index) => (
<Tab key={opt} title={opt} />
{vid.options.map((opt, _) => (
<Tab key={opt} title={opt} titleValue={opt} />
))}
</Tabs>
</li>

View File

@@ -0,0 +1,28 @@
// @ts-nocheck
import { Code, Link } from '@heroui/react'
import Markdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
export const components = {
a: ({ href, children }: { href: string; children: React.ReactNode }) => (
<Link href={href} target="_blank" rel="noopener noreferrer">
{children}
</Link>
),
// img: ({ src, alt }: { src: string; alt: string }) => <Image src={src} alt={alt} className="object-cover w-full h-full" />,
h1: ({ children }: { children: React.ReactNode }) => <h1 className="text-2xl font-bold mb-2.5">{children}</h1>,
h2: ({ children }: { children: React.ReactNode }) => <h2 className="text-xl font-semibold mb-2.5">{children}</h2>,
h3: ({ children }: { children: React.ReactNode }) => <h3 className="text-lg font-medium mb-2.5">{children}</h3>,
p: ({ children }: { children: React.ReactNode }) => <p className="mb-2.5 text-base">{children}</p>,
ul: ({ children }: { children: React.ReactNode }) => <ul className="list-disc pl-6 mb-2.5">{children}</ul>,
li: ({ children }: { children: React.ReactNode }) => <li className="mb-2">{children}</li>,
code: ({ children }: { children: React.ReactNode }) => <Code size="sm" >{children}</Code>,
}
export function MarkdownRender({ children }: { children: React.ReactNode }) {
return (
<Markdown remarkPlugins={[remarkGfm]} components={components as any}>
{children?.toString()}
</Markdown>
)
}

View File

@@ -2,8 +2,17 @@
import { setTheme as setTauriTheme } from "@/hooks/tauri/theme"
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 { addToast, Button, Link, Tooltip, useDisclosure } from "@heroui/react"
import {
Close,
Communication,
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"
@@ -11,6 +20,7 @@ import { usePathname, useRouter } from "next/navigation"
import { saveAllNow } from "@tauri-store/valtio"
import { useSteamStore } from "@/store/steam"
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from "@heroui/react"
import { window } from "@tauri-apps/api"
const Nav = () => {
const { theme, setTheme } = useTheme()
@@ -19,10 +29,12 @@ const Nav = () => {
await setTauriTheme(theme)
}
const app = useAppStore()
const close = async () => {
// (await window.hideOnClose) ? getCurrent().hide() : exit();
await saveAllNow()
await exit()
// await exit()
if (app.state.hiddenOnClose) await window.getCurrentWindow().hide()
else await exit()
}
const minimize = async () => {
@@ -42,30 +54,42 @@ 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>
{pathname !== "/" && (
<Tooltip content="启动页确认设置" showArrow={true} delay={300}>
{pathname !== "/" && (
<button
type="button"
className="px-2 py-0 transition duration-150 rounded hover:bg-black/10 dark:hover:bg-zinc-100/10 active:scale-95"
onClick={() => {
app.setInited(false)
if (pathname !== "/") router.push("/")
}}
>
<RocketOne size={16} />
</button>
)}
</Tooltip>
<Tooltip content="深色模式" showArrow={true} delay={300}>
<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("/")
}}
className="px-2 py-0 transition duration-150 rounded hover:bg-black/10 dark:hover:bg-zinc-100/10 active:scale-95"
onClick={() => (theme === "light" ? setAppTheme("dark") : setAppTheme("light"))}
>
<RocketOne size={16} />
{theme === "light" ? <SunOne size={16} /> : <Moon size={16} />}
</button>
)}
<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={() => (theme === "light" ? setAppTheme("dark") : setAppTheme("light"))}
>
{theme === "light" ? <SunOne size={16} /> : <Moon size={16} />}
</button>
</Tooltip>
<Tooltip content="反馈" showArrow={true} delay={300}>
<Link
href="https://docs.qq.com/form/page/DZU1ieW9SQkxWU1RF"
target="_blank"
className="px-2 py-0 text-black transition duration-150 rounded dark:text-white hover:bg-black/10 dark:hover:bg-zinc-100/10 active:scale-95"
>
<button type="button">
<Communication size={16} />
</button>
</Link>
</Tooltip>
<ResetModal />
@@ -73,21 +97,21 @@ const Nav = () => {
<>
<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"
className="px-2 py-0 transition duration-150 rounded hover:bg-black/10 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 dark:hover:bg-zinc-100/10 active:scale-95"
className="px-2 py-0 transition duration-150 rounded hover:bg-black/10 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 dark:hover:bg-zinc-100/10 active:scale-95"
className="px-2 py-0 transition duration-150 rounded hover:bg-black/10 dark:hover:bg-zinc-100/10 active:scale-95"
onClick={close}
>
<Close size={16} />
@@ -119,13 +143,15 @@ function ResetModal() {
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>
<Tooltip content="重置设置" showArrow={true} delay={300}>
<button
type="button"
className="px-2 py-0 transition duration-150 rounded hover:bg-black/10 dark:hover:bg-zinc-100/10 active:scale-95"
onClick={onOpen}
>
<Refresh size={16} />
</button>
</Tooltip>
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent>
{(onClose) => (

View File

@@ -55,7 +55,7 @@ const Avatar = () => {
<img
src={
steam.currentUser()?.avatar
? `data:image/png;base64,${steam.currentUser()?.avatar || ''}`
? `data:image/png;base64,${steam.currentUser()?.avatar || ""}`
: "/logo_square.png"
}
alt="avatar"
@@ -72,9 +72,10 @@ const Avatar = () => {
const SideBar = () => {
const app = useAppStore()
void getVersion().then((Value) => {
app.setVersion(Value)
})
if (typeof window !== "undefined")
void getVersion().then((Value) => {
app.setVersion(Value)
})
return (
<div

View File

@@ -1,7 +1,8 @@
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"
import { enable, disable } from "@tauri-apps/plugin-autostart"
import { LazyStore } from '@tauri-apps/plugin-store';
const defaultValue = {
version: "0.0.1",
@@ -10,6 +11,8 @@ const defaultValue = {
notice: "",
useMirror: true,
autoStart: false,
startHidden: false,
hiddenOnClose: false,
}
export const appStore = store("app", { ...defaultValue }, DEFAULT_STORE_CONFIG)
@@ -27,10 +30,15 @@ export const useAppStore = () => {
setNotice,
setUseMirror,
setAutoStart,
setStartHidden,
setHiddenOnClose,
resetAppStore,
}
}
const launchStore = new LazyStore('cstb.json', { autoSave: true });
if (typeof window !== 'undefined') void launchStore.save()
const setVersion = (version: string) => {
appStore.state.version = version
}
@@ -56,6 +64,17 @@ const setAutoStart = (autoStart: boolean) => {
appStore.state.autoStart = autoStart
}
// 同步到 launchStore 使 start hidden 生效
const setStartHidden = async (startHidden: boolean) => {
appStore.state.startHidden = startHidden;
await launchStore.set('hidden', startHidden);
await launchStore.save();
}
const setHiddenOnClose = (hiddenOnClose: boolean) => {
appStore.state.hiddenOnClose = hiddenOnClose;
}
const resetAppStore = () => {
setVersion(defaultValue.version)
setHasUpdate(defaultValue.hasUpdate)
@@ -63,4 +82,6 @@ const resetAppStore = () => {
setNotice(defaultValue.notice)
setUseMirror(defaultValue.useMirror)
setAutoStart(defaultValue.autoStart)
void setStartHidden(defaultValue.startHidden)
setHiddenOnClose(defaultValue.hiddenOnClose)
}

View File

@@ -96,13 +96,11 @@ const currentUser = () => {
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,

View File

@@ -1,6 +1,9 @@
import { store } from "@tauri-store/valtio"
import { useSnapshot } from "valtio"
import { DEFAULT_STORE_CONFIG } from "./config"
import { emit } from "@tauri-apps/api/event"
import { invoke } from "@tauri-apps/api/core"
import VideoSetting from "@/components/cstb/VideoSetting"
interface LaunchOption {
option: string
@@ -8,24 +11,109 @@ interface LaunchOption {
}
export interface VideoSetting {
width: number; // 分辨率宽度
height: number; // 分辨率高度
version: string; // 版本
vendor_id: string; // 供应商ID
device_id: string; // 设备ID
cpu_level: string; // CPU等级
gpu_mem_level: string; // GPU内存等级
gpu_level: string; // GPU等级
knowndevice: string; // 已知设备
defaultres: string; // 默认分辨率宽度
defaultresheight: string; // 默认分辨率高度
refreshrate_numerator: string; // 刷新率分子
refreshrate_denominator: string; // 刷新率分母
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 超级分辨率
coop_fullscreen: string; // 合作模式全屏
nowindowborder: string; // 无窗口边框
mat_vsync: string; // 垂直同步
fullscreen_min_on_focus_loss: string; // 失去焦点时最小化全屏
high_dpi: string; // 高DPI
auto_config: string; // 自动配置
shaderquality: string; // 光影质量
r_texturefilteringquality: string; // 纹理过滤质量
msaa_samples: string; // 多重采样抗锯齿样本数
r_csgo_cmaa_enable: string; // CMAA抗锯齿启用
videocfg_shadow_quality: string; // 阴影质量
videocfg_dynamic_shadows: string; // 动态阴影
videocfg_texture_detail: string; // 纹理细节
videocfg_particle_detail: string; // 粒子细节
videocfg_ao_detail: string; // 环境光遮蔽细节
videocfg_hdr_detail: string; // 高动态范围细节
videocfg_fsr_detail: string; // FSR细节
monitor_index: string; // 显示器索引
r_low_latency: string; // 低延迟
aspectratiomode: string; // 宽高比模式
}
// 视频设置预设模版
export const VideoSettingTemplate = {
veryhigh: {
shaderquality: "1",
r_texturefilteringquality: "3",
msaa_samples: "8",
r_csgo_cmaa_enable: "0",
videocfg_shadow_quality: "3",
videocfg_dynamic_shadows: "1",
videocfg_texture_detail: "2",
videocfg_particle_detail: "3",
videocfg_ao_detail: "3",
videocfg_hdr_detail: "-1",
videocfg_fsr_detail: "0",
},
high: {
shaderquality: "1",
r_texturefilteringquality: "3",
msaa_samples: "4",
r_csgo_cmaa_enable: "0",
videocfg_shadow_quality: "2",
videocfg_dynamic_shadows: "1",
videocfg_texture_detail: "2",
videocfg_particle_detail: "2",
videocfg_ao_detail: "2",
videocfg_hdr_detail: "-1",
videocfg_fsr_detail: "0",
},
middle: {
shaderquality: "0",
r_texturefilteringquality: "1",
msaa_samples: "2",
r_csgo_cmaa_enable: "0",
videocfg_shadow_quality: "1",
videocfg_dynamic_shadows: "1",
videocfg_texture_detail: "1",
videocfg_particle_detail: "1",
videocfg_ao_detail: "0",
videocfg_fsr_detail: "2",
},
low: {
shaderquality: "0",
r_texturefilteringquality: "0",
msaa_samples: "0",
r_csgo_cmaa_enable: "0",
videocfg_shadow_quality: "0",
videocfg_dynamic_shadows: "0",
videocfg_texture_detail: "0",
videocfg_particle_detail: "0",
videocfg_ao_detail: "0",
videocfg_hdr_detail: "3",
videocfg_fsr_detail: "3",
},
recommend: {
shaderquality: "0",
r_texturefilteringquality: "3",
msaa_samples: "2",
r_csgo_cmaa_enable: "0",
videocfg_shadow_quality: "0",
videocfg_dynamic_shadows: "1",
videocfg_texture_detail: "1",
videocfg_particle_detail: "0",
videocfg_ao_detail: "0",
videocfg_hdr_detail: "3",
videocfg_fsr_detail: "0",
},
}
const defaultValue = {
launchOptions: [
{ option: "-novid -high -freq 144 -fullscreen", name: "" },
@@ -35,8 +123,38 @@ const defaultValue = {
launchIndex: 0,
powerPlan: 0,
videoSetting: {
width: 1920,
height: 1080
version: "15",
vendor_id: "0",
device_id: "0",
cpu_level: "3",
gpu_mem_level: "3",
gpu_level: "3",
knowndevice: "0",
defaultres: "1920",
defaultresheight: "1080",
refreshrate_numerator: "144",
refreshrate_denominator: "1",
fullscreen: "1",
coop_fullscreen: "0",
nowindowborder: "1",
mat_vsync: "0",
fullscreen_min_on_focus_loss: "1",
high_dpi: "0",
auto_config: "2",
shaderquality: "0",
r_texturefilteringquality: "3",
msaa_samples: "2",
r_csgo_cmaa_enable: "0",
videocfg_shadow_quality: "0",
videocfg_dynamic_shadows: "1",
videocfg_texture_detail: "1",
videocfg_particle_detail: "0",
videocfg_ao_detail: "0",
videocfg_hdr_detail: "3",
videocfg_fsr_detail: "0",
monitor_index: "0",
r_low_latency: "1",
aspectratiomode: "0",
} as VideoSetting,
}
@@ -50,6 +168,13 @@ export const useToolStore = () => {
void toolStore.start
const state = useSnapshot(toolStore.state)
if (typeof window !== 'undefined') {
setTimeout(() => {
sendCurrentLaunchOptionToTray(state.launchIndex)
sendPowerPlanToTray(state.powerPlan)
}, 500)
}
return {
state,
store: toolStore,
@@ -58,6 +183,8 @@ export const useToolStore = () => {
setLaunchIndex,
setPowerPlan,
setVideoSetting,
getVideoConfig,
setVideoConfig,
addLaunchOption,
resetToolStore,
}
@@ -77,16 +204,35 @@ const setLaunchOptions = (options: LaunchOption[]) => {
const setLaunchIndex = (index: number) => {
toolStore.state.launchIndex = index
sendCurrentLaunchOptionToTray(index)
}
const sendCurrentLaunchOptionToTray = (index: number) => {
void emit("tray://get_current_launch_option", toolStore.state.launchOptions[index].name || index + 1)
}
const setPowerPlan = (plan: number) => {
toolStore.state.powerPlan = plan
sendPowerPlanToTray(plan)
}
const sendPowerPlanToTray = (plan: number) => {
void emit("tray://get_powerplan", plan)
}
const setVideoSetting = (setting: VideoSetting) => {
toolStore.state.videoSetting = setting
}
const getVideoConfig = async (steam_dir: string, steam_id32: number) => {
const video = await invoke<VideoSetting>("get_cs2_video_config", { steamDir: steam_dir, steamId32: steam_id32 })
// console.log(video)
setVideoSetting(video)
}
const setVideoConfig = async (steam_dir: string, steam_id32: number, video_config: VideoSetting) => {
console.log(video_config.videocfg_hdr_detail)
await invoke("set_cs2_video_config", { steamDir: steam_dir, steamId32: steam_id32, videoConfig: video_config })
}
const addLaunchOption = (option: LaunchOption) => {
// 限制最高10个
if (toolStore.state.launchOptions.length >= 10) {