Compare commits
5 Commits
v0.0.4
...
v0.0.5-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66d62b970a | ||
|
|
0a78a9d056 | ||
|
|
0b65cdb129 | ||
|
|
e2d8f3effd | ||
|
|
7a99672317 |
@@ -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"
|
||||
}
|
||||
}
|
||||
217
README.md
217
README.md
@@ -1,216 +1,3 @@
|
||||
# Tauri + Next.js Template
|
||||
# CS工具箱
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||
@@ -35,7 +35,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 +43,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"
|
||||
},
|
||||
|
||||
4
src-tauri/Cargo.lock
generated
4
src-tauri/Cargo.lock
generated
@@ -5326,9 +5326,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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
],
|
||||
"definitions": {
|
||||
"Capability": {
|
||||
"description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows fine grained access to the Tauri core, application, or plugin commands. If a window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```",
|
||||
"description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"identifier",
|
||||
@@ -70,14 +70,14 @@
|
||||
"type": "boolean"
|
||||
},
|
||||
"windows": {
|
||||
"description": "List of windows that are affected by this capability. Can be a glob pattern.\n\nOn multiwebview windows, prefer [`Self::webviews`] for a fine grained access control.\n\n## Example\n\n`[\"main\"]`",
|
||||
"description": "List of windows that are affected by this capability. Can be a glob pattern.\n\nIf a window label matches any of the patterns in this list, the capability will be enabled on all the webviews of that window, regardless of the value of [`Self::webviews`].\n\nOn multiwebview windows, prefer specifying [`Self::webviews`] and omitting [`Self::windows`] for a fine grained access control.\n\n## Example\n\n`[\"main\"]`",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"webviews": {
|
||||
"description": "List of webviews that are affected by this capability. Can be a glob pattern.\n\nThis is only required when using on multiwebview contexts, by default all child webviews of a window that matches [`Self::windows`] are linked.\n\n## Example\n\n`[\"sub-webview-one\", \"sub-webview-two\"]`",
|
||||
"description": "List of webviews that are affected by this capability. Can be a glob pattern.\n\nThe capability will be enabled on all the webviews whose label matches any of the patterns in this list, regardless of whether the webview's window label matches a pattern in [`Self::windows`].\n\n## Example\n\n`[\"sub-webview-one\", \"sub-webview-two\"]`",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
@@ -2004,6 +2004,41 @@
|
||||
"Identifier": {
|
||||
"description": "Permission identifier",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "This permission set configures if your\napplication can enable or disable auto\nstarting the application on boot.\n\n#### Granted Permissions\n\nIt allows all to check, enable and\ndisable the automatic start on boot.\n\n",
|
||||
"type": "string",
|
||||
"const": "autostart:default"
|
||||
},
|
||||
{
|
||||
"description": "Enables the disable command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "autostart:allow-disable"
|
||||
},
|
||||
{
|
||||
"description": "Enables the enable command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "autostart:allow-enable"
|
||||
},
|
||||
{
|
||||
"description": "Enables the is_enabled command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "autostart:allow-is-enabled"
|
||||
},
|
||||
{
|
||||
"description": "Denies the disable command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "autostart:deny-disable"
|
||||
},
|
||||
{
|
||||
"description": "Denies the enable command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "autostart:deny-enable"
|
||||
},
|
||||
{
|
||||
"description": "Denies the is_enabled command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "autostart:deny-is-enabled"
|
||||
},
|
||||
{
|
||||
"description": "No features are enabled by default, as we believe\nthe clipboard can be inherently dangerous and it is \napplication specific if read and/or write access is needed.\n\nClipboard interaction needs to be explicitly enabled.\n",
|
||||
"type": "string",
|
||||
@@ -2094,11 +2129,26 @@
|
||||
"type": "string",
|
||||
"const": "core:app:allow-default-window-icon"
|
||||
},
|
||||
{
|
||||
"description": "Enables the fetch_data_store_identifiers command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:allow-fetch-data-store-identifiers"
|
||||
},
|
||||
{
|
||||
"description": "Enables the identifier command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:allow-identifier"
|
||||
},
|
||||
{
|
||||
"description": "Enables the name command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:allow-name"
|
||||
},
|
||||
{
|
||||
"description": "Enables the remove_data_store command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:allow-remove-data-store"
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_app_theme command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -2129,11 +2179,26 @@
|
||||
"type": "string",
|
||||
"const": "core:app:deny-default-window-icon"
|
||||
},
|
||||
{
|
||||
"description": "Denies the fetch_data_store_identifiers command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:deny-fetch-data-store-identifiers"
|
||||
},
|
||||
{
|
||||
"description": "Denies the identifier command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:deny-identifier"
|
||||
},
|
||||
{
|
||||
"description": "Denies the name command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:deny-name"
|
||||
},
|
||||
{
|
||||
"description": "Denies the remove_data_store command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:deny-remove-data-store"
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_app_theme command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -2929,6 +2994,11 @@
|
||||
"type": "string",
|
||||
"const": "core:window:allow-internal-toggle-maximize"
|
||||
},
|
||||
{
|
||||
"description": "Enables the is_always_on_top command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:window:allow-is-always-on-top"
|
||||
},
|
||||
{
|
||||
"description": "Enables the is_closable command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -3294,6 +3364,11 @@
|
||||
"type": "string",
|
||||
"const": "core:window:deny-internal-toggle-maximize"
|
||||
},
|
||||
{
|
||||
"description": "Denies the is_always_on_top command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:window:deny-is-always-on-top"
|
||||
},
|
||||
{
|
||||
"description": "Denies the is_closable command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -3599,6 +3674,51 @@
|
||||
"type": "string",
|
||||
"const": "core:window:deny-unminimize"
|
||||
},
|
||||
{
|
||||
"description": "Allows reading the opened deep link via the get_current command",
|
||||
"type": "string",
|
||||
"const": "deep-link:default"
|
||||
},
|
||||
{
|
||||
"description": "Enables the get_current command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deep-link:allow-get-current"
|
||||
},
|
||||
{
|
||||
"description": "Enables the is_registered command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deep-link:allow-is-registered"
|
||||
},
|
||||
{
|
||||
"description": "Enables the register command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deep-link:allow-register"
|
||||
},
|
||||
{
|
||||
"description": "Enables the unregister command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deep-link:allow-unregister"
|
||||
},
|
||||
{
|
||||
"description": "Denies the get_current command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deep-link:deny-get-current"
|
||||
},
|
||||
{
|
||||
"description": "Denies the is_registered command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deep-link:deny-is-registered"
|
||||
},
|
||||
{
|
||||
"description": "Denies the register command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deep-link:deny-register"
|
||||
},
|
||||
{
|
||||
"description": "Denies the unregister command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deep-link:deny-unregister"
|
||||
},
|
||||
{
|
||||
"description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n",
|
||||
"type": "string",
|
||||
@@ -5969,6 +6089,11 @@
|
||||
"type": "string",
|
||||
"const": "valtio:allow-get-save-strategy"
|
||||
},
|
||||
{
|
||||
"description": "Enables the get_store_collection_path command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "valtio:allow-get-store-collection-path"
|
||||
},
|
||||
{
|
||||
"description": "Enables the get_store_ids command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -5984,11 +6109,6 @@
|
||||
"type": "string",
|
||||
"const": "valtio:allow-get-store-state"
|
||||
},
|
||||
{
|
||||
"description": "Enables the get_valtio_path command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "valtio:allow-get-valtio-path"
|
||||
},
|
||||
{
|
||||
"description": "Enables the load command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -6039,16 +6159,16 @@
|
||||
"type": "string",
|
||||
"const": "valtio:allow-set-save-strategy"
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_store_collection_path command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "valtio:allow-set-store-collection-path"
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_store_options command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "valtio:allow-set-store-options"
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_valtio_path command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "valtio:allow-set-valtio-path"
|
||||
},
|
||||
{
|
||||
"description": "Enables the unload command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -6069,6 +6189,11 @@
|
||||
"type": "string",
|
||||
"const": "valtio:deny-get-save-strategy"
|
||||
},
|
||||
{
|
||||
"description": "Denies the get_store_collection_path command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "valtio:deny-get-store-collection-path"
|
||||
},
|
||||
{
|
||||
"description": "Denies the get_store_ids command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -6084,11 +6209,6 @@
|
||||
"type": "string",
|
||||
"const": "valtio:deny-get-store-state"
|
||||
},
|
||||
{
|
||||
"description": "Denies the get_valtio_path command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "valtio:deny-get-valtio-path"
|
||||
},
|
||||
{
|
||||
"description": "Denies the load command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -6139,16 +6259,16 @@
|
||||
"type": "string",
|
||||
"const": "valtio:deny-set-save-strategy"
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_store_collection_path command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "valtio:deny-set-store-collection-path"
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_store_options command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "valtio:deny-set-store-options"
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_valtio_path command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "valtio:deny-set-valtio-path"
|
||||
},
|
||||
{
|
||||
"description": "Denies the unload command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
|
||||
@@ -54,6 +54,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.into();
|
||||
|
||||
Ok(powerplan)
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
},
|
||||
"productName": "CS工具箱",
|
||||
"mainBinaryName": "cstb",
|
||||
"version": "0.0.4",
|
||||
"version": "0.0.5-beta.1",
|
||||
"identifier": "upup.cool",
|
||||
"plugins": {
|
||||
"deep-link": {
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: "启动国际服成功" })
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { CloseSmall, Down, Edit, Plus, SettingConfig, Up } from "@icon-park/reac
|
||||
import { useState } from "react"
|
||||
import { Card, CardBody, CardHeader, CardIcon, CardTool } from "../window/Card"
|
||||
import { ToolButton } from "../window/ToolButton"
|
||||
import { addToast, NumberInput, Tab, Tabs } from "@heroui/react"
|
||||
import { addToast, NumberInput, Tab, Tabs, Tooltip } from "@heroui/react"
|
||||
import { motion } from "framer-motion"
|
||||
import { useToolStore } from "@/store/tool"
|
||||
|
||||
@@ -21,13 +21,14 @@ const VideoSetting = () => {
|
||||
const videoSettings = [
|
||||
{ type: "", title: "全屏", value: "全屏", options: ["窗口", "全屏"] },
|
||||
{ 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"],
|
||||
options: ["无", "2X MSAA", "4X MSAA", "8X MSAA"],
|
||||
},
|
||||
{ type: "", title: "全局阴影效果", value: "低", options: ["低", "中", "高", "非常高"] },
|
||||
{ type: "", title: "动态阴影", value: "全部", options: ["仅限日光", "全部"] },
|
||||
@@ -51,121 +52,127 @@ const VideoSetting = () => {
|
||||
]
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardIcon>
|
||||
<SettingConfig /> 视频设置
|
||||
</CardIcon>
|
||||
<CardTool>
|
||||
{/* {tool.state.VideoSettings.map((option, index) => (
|
||||
<Tooltip content="功能测试中,尚未实装" showArrow={true} delay={300}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardIcon>
|
||||
<SettingConfig /> 视频设置
|
||||
</CardIcon>
|
||||
<CardTool>
|
||||
{/* {tool.state.VideoSettings.map((option, index) => (
|
||||
<ToolButton key={index} onClick={() => tool.setLaunchIndex(index)}>
|
||||
{index + 1}
|
||||
</ToolButton>
|
||||
))} */}
|
||||
{edit && (
|
||||
<>
|
||||
<ToolButton>低</ToolButton>
|
||||
<ToolButton>中</ToolButton>
|
||||
<ToolButton>高</ToolButton>
|
||||
<ToolButton>非常高</ToolButton>
|
||||
<ToolButton>推荐</ToolButton>
|
||||
<ToolButton
|
||||
onClick={() => {
|
||||
addToast({ title: "测试中 功能完成后可应用设置到游戏" })
|
||||
}}
|
||||
>
|
||||
<Plus />
|
||||
应用
|
||||
</ToolButton>
|
||||
</>
|
||||
)}
|
||||
<ToolButton onClick={() => setEdit(!edit)}>
|
||||
{edit ? (
|
||||
{edit && (
|
||||
<>
|
||||
<CloseSmall />
|
||||
取消编辑
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Edit />
|
||||
编辑
|
||||
<ToolButton>低</ToolButton>
|
||||
<ToolButton>中</ToolButton>
|
||||
<ToolButton>高</ToolButton>
|
||||
<ToolButton>非常高</ToolButton>
|
||||
<ToolButton>推荐</ToolButton>
|
||||
<ToolButton
|
||||
onClick={() => {
|
||||
addToast({ title: "测试中 功能完成后可应用设置到游戏" })
|
||||
}}
|
||||
>
|
||||
<Plus />
|
||||
应用
|
||||
</ToolButton>
|
||||
</>
|
||||
)}
|
||||
</ToolButton>
|
||||
<ToolButton onClick={() => setHide(!hide)}>
|
||||
{hide ? (
|
||||
<>
|
||||
<Up />
|
||||
显示
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Down />
|
||||
隐藏
|
||||
</>
|
||||
)}
|
||||
</ToolButton>
|
||||
</CardTool>
|
||||
</CardHeader>
|
||||
{!hide && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<CardBody>
|
||||
<ul className="flex flex-wrap gap-3 mt-1">
|
||||
<li className="flex flex-col gap-1.5">
|
||||
<span className="ml-2">分辨率</span>
|
||||
<span className="flex gap-1.5">
|
||||
<NumberInput
|
||||
value={tool.state.videoSetting.width}
|
||||
onValueChange={(value) => {
|
||||
tool.setVideoSetting({
|
||||
...tool.state.videoSetting,
|
||||
width: value,
|
||||
})
|
||||
}}
|
||||
radius="full"
|
||||
className="max-w-28"
|
||||
classNames={{ inputWrapper: "h-10" }}
|
||||
/>
|
||||
<NumberInput
|
||||
value={tool.state.videoSetting.height}
|
||||
onValueChange={(value) => {
|
||||
tool.setVideoSetting({
|
||||
...tool.state.videoSetting,
|
||||
height: value,
|
||||
})
|
||||
}}
|
||||
radius="full"
|
||||
className="max-w-28"
|
||||
classNames={{ inputWrapper: "h-10" }}
|
||||
/>
|
||||
</span>
|
||||
</li>
|
||||
{videoSettings.map((vid, index) => (
|
||||
<li className="flex flex-col gap-1.5" key={index}>
|
||||
<span className="ml-2">{vid.title}</span>
|
||||
<Tabs
|
||||
selectedKey={vid.value}
|
||||
size="md"
|
||||
radius="full"
|
||||
className="min-w-36"
|
||||
fullWidth
|
||||
>
|
||||
{vid.options.map((opt, index) => (
|
||||
<Tab key={opt} title={opt} />
|
||||
))}
|
||||
</Tabs>
|
||||
<ToolButton onClick={() => setEdit(!edit)}>
|
||||
{edit ? (
|
||||
<>
|
||||
<CloseSmall />
|
||||
取消编辑
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Edit />
|
||||
编辑
|
||||
</>
|
||||
)}
|
||||
</ToolButton>
|
||||
<ToolButton onClick={() => setHide(!hide)}>
|
||||
{hide ? (
|
||||
<>
|
||||
<Up />
|
||||
显示
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Down />
|
||||
隐藏
|
||||
</>
|
||||
)}
|
||||
</ToolButton>
|
||||
</CardTool>
|
||||
</CardHeader>
|
||||
{!hide && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<CardBody>
|
||||
<ul className="flex flex-wrap gap-3 mt-1">
|
||||
<li className="flex flex-col gap-1.5">
|
||||
<span className="ml-2">分辨率</span>
|
||||
<span className="flex gap-3">
|
||||
<NumberInput
|
||||
aria-label="width"
|
||||
value={tool.state.videoSetting.width}
|
||||
onValueChange={(value) => {
|
||||
tool.setVideoSetting({
|
||||
...tool.state.videoSetting,
|
||||
width: value,
|
||||
})
|
||||
}}
|
||||
radius="full"
|
||||
step={10}
|
||||
className="max-w-28"
|
||||
classNames={{ inputWrapper: "h-10" }}
|
||||
/>
|
||||
<NumberInput
|
||||
aria-label="height"
|
||||
value={tool.state.videoSetting.height}
|
||||
onValueChange={(value) => {
|
||||
tool.setVideoSetting({
|
||||
...tool.state.videoSetting,
|
||||
height: value,
|
||||
})
|
||||
}}
|
||||
radius="full"
|
||||
step={10}
|
||||
className="max-w-28"
|
||||
classNames={{ inputWrapper: "h-10" }}
|
||||
/>
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</CardBody>
|
||||
</motion.div>
|
||||
)}
|
||||
</Card>
|
||||
{videoSettings.map((vid, index) => (
|
||||
<li className="flex flex-col gap-1.5" key={index}>
|
||||
<span className="ml-2">{vid.title}</span>
|
||||
<Tabs
|
||||
selectedKey={vid.value}
|
||||
size="md"
|
||||
radius="full"
|
||||
className="min-w-36"
|
||||
fullWidth
|
||||
>
|
||||
{vid.options.map((opt, _) => (
|
||||
<Tab key={opt} title={opt} />
|
||||
))}
|
||||
</Tabs>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</CardBody>
|
||||
</motion.div>
|
||||
)}
|
||||
</Card>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
28
src/components/markdown/index.tsx
Normal file
28
src/components/markdown/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -2,8 +2,18 @@
|
||||
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,
|
||||
SurprisedFaceWithOpenBigMouth,
|
||||
} 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"
|
||||
@@ -46,26 +56,39 @@ const Nav = () => {
|
||||
|
||||
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="/preference"
|
||||
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 +96,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 +142,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) => (
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user