Update On Fri Feb 6 20:02:30 CET 2026

This commit is contained in:
github-action[bot]
2026-02-06 20:02:31 +01:00
parent 5204ea00e5
commit 05f4e1b204
315 changed files with 10041 additions and 8379 deletions
+1
View File
@@ -1263,3 +1263,4 @@ Update On Sun Feb 1 19:47:34 CET 2026
Update On Tue Feb 3 20:07:51 CET 2026
Update On Wed Feb 4 20:03:26 CET 2026
Update On Thu Feb 5 20:02:24 CET 2026
Update On Fri Feb 6 20:02:22 CET 2026
+6 -6
View File
@@ -1,7 +1,7 @@
export default {
'*.{js,cjs,.mjs,jsx}': (filenames) => {
const configFiles = [
'eslint.config.js',
'.oxlintrc.json',
'.lintstagedrc.js',
'commitlint.config.js',
]
@@ -9,26 +9,26 @@ export default {
(file) => !configFiles.some((config) => file.endsWith(config)),
)
if (filtered.length === 0) return []
return ['prettier --write', 'eslint --cache --fix']
return ['prettier --write', 'oxlint --fix']
},
'scripts/**/*.{ts,tsx}': [
'prettier --write',
'node ./node_modules/eslint/bin/eslint.js --cache --fix',
'oxlint --fix',
() => 'tsc -p scripts/tsconfig.json --noEmit',
],
'frontend/interface/**/*.{ts,tsx}': [
'prettier --write',
'node ./node_modules/eslint/bin/eslint.js --cache --fix',
'oxlint --fix',
() => 'tsc -p frontend/interface/tsconfig.json --noEmit',
],
'frontend/ui/**/*.{ts,tsx}': [
'prettier --write',
'node ./node_modules/eslint/bin/eslint.js --cache --fix',
'oxlint --fix',
() => 'tsc -p frontend/ui/tsconfig.json --noEmit',
],
'frontend/nyanpasu/**/*.{ts,tsx}': [
'prettier --write',
'node ./node_modules/eslint/bin/eslint.js --cache --fix',
'oxlint --fix',
() => 'tsc -p frontend/nyanpasu/tsconfig.json --noEmit',
],
'backend/**/*.{rs,toml}': [
+651
View File
@@ -0,0 +1,651 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": [],
"categories": {
"correctness": "off"
},
"env": {
"builtin": true
},
"ignorePatterns": [
"**/node_modules",
"**/.DS_Store",
"**/dist",
"**/*.local",
"**/update.json",
"scripts/_env.sh",
"**/.eslintcache",
"**/.stylelintcache",
"**/tauri.nightly.conf.json",
"**/tauri.preview.conf.json",
"**/.idea",
"**/*.tsbuildinfo",
"manifest/site/updater/*",
"!manifest/site/updater/.gitkeep",
"backend/tauri/gen/",
"**/index.html",
"**/node_modules/",
"node_modules/",
"backend/",
"backend/**/target",
"scripts/deno/**",
".lintstagedrc.js",
"commitlint.config.js"
],
"overrides": [
{
"files": ["**/*.{jsx,mjsx,tsx,mtsx}"],
"rules": {
"react/display-name": "error",
"react/jsx-key": "error",
"react/jsx-no-comment-textnodes": "error",
"react/jsx-no-duplicate-props": "error",
"react/jsx-no-target-blank": "error",
// "react/jsx-no-undef": "error",
"react/no-children-prop": "error",
"react/no-danger-with-children": "error",
"react/no-direct-mutation-state": "error",
"react/no-find-dom-node": "error",
"react/no-is-mounted": "error",
"react/no-render-return-value": "error",
"react/no-string-refs": "error",
"react/no-unknown-property": "error",
"react/no-unsafe": "off",
"react/react-in-jsx-scope": "error"
},
"plugins": ["react"]
},
{
"files": ["**/*.{jsx,mjsx,tsx,mtsx}"],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
},
"plugins": ["react"]
},
{
"files": ["**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}"],
"rules": {
"no-var": "warn",
"accessor-pairs": [
"error",
{
"setWithoutGet": true,
"enforceForClassMembers": true
}
],
"array-callback-return": [
"error",
{
"allowImplicit": false,
"checkForEach": false
}
],
"constructor-super": "error",
"curly": ["error", "multi-line"],
"default-case-last": "error",
"eqeqeq": [
"error",
"always",
{
"null": "ignore"
}
],
"new-cap": [
"error",
{
"newIsCap": true,
"capIsNew": false,
"properties": true
}
],
"no-array-constructor": "error",
"no-async-promise-executor": "error",
"no-caller": "error",
"no-case-declarations": "error",
"no-class-assign": "error",
"no-compare-neg-zero": "error",
"no-cond-assign": "error",
"no-const-assign": "error",
"no-constant-condition": [
"error",
{
"checkLoops": false
}
],
"no-control-regex": "error",
"no-debugger": "error",
"no-delete-var": "error",
"no-dupe-class-members": "error",
"no-dupe-keys": "error",
"no-duplicate-case": "error",
"no-useless-backreference": "error",
"no-empty": [
"error",
{
"allowEmptyCatch": true
}
],
"no-empty-character-class": "error",
"no-empty-pattern": "error",
"no-eval": "error",
"no-ex-assign": "error",
"no-extend-native": "error",
"no-extra-bind": "error",
"no-extra-boolean-cast": "error",
"no-fallthrough": "error",
"no-func-assign": "error",
"no-global-assign": "error",
"no-import-assign": "error",
"no-invalid-regexp": "error",
"no-irregular-whitespace": "error",
"no-iterator": "error",
"no-labels": [
"error",
{
"allowLoop": false,
"allowSwitch": false
}
],
"no-lone-blocks": "error",
"no-loss-of-precision": "error",
"no-prototype-builtins": "error",
"no-useless-catch": "error",
"no-multi-str": "error",
"no-new": "error",
"no-new-func": "error",
"no-object-constructor": "error",
"no-new-native-nonconstructor": "error",
"no-new-wrappers": "error",
"no-obj-calls": "error",
"no-proto": "error",
"no-redeclare": [
"error",
{
"builtinGlobals": false
}
],
"no-regex-spaces": "error",
"no-return-assign": ["error", "except-parens"],
"no-self-assign": [
"error",
{
"props": true
}
],
"no-self-compare": "error",
"no-sequences": "error",
"no-shadow-restricted-names": "error",
"no-sparse-arrays": "error",
"no-template-curly-in-string": "error",
"no-this-before-super": "error",
"no-throw-literal": "error",
"no-unexpected-multiline": "error",
"no-unneeded-ternary": [
"error",
{
"defaultAssignment": false
}
],
"no-unsafe-finally": "error",
"no-unsafe-negation": "error",
"no-unused-expressions": [
"error",
{
"allowShortCircuit": true,
"allowTernary": true,
"allowTaggedTemplates": true
}
],
"no-unused-vars": [
"error",
{
"args": "none",
"caughtErrors": "none",
"ignoreRestSiblings": true,
"vars": "all"
}
],
"no-useless-call": "error",
"no-useless-computed-key": "error",
"no-useless-constructor": "error",
"no-useless-escape": "error",
"no-useless-rename": "error",
"no-useless-return": "error",
"no-void": "error",
"no-with": "error",
"prefer-const": [
"error",
{
"destructuring": "all"
}
],
"prefer-promise-reject-errors": "error",
"symbol-description": "error",
"unicode-bom": ["error", "never"],
"use-isnan": [
"error",
{
"enforceForSwitchCase": true,
"enforceForIndexOf": true
}
],
"valid-typeof": [
"error",
{
"requireStringLiterals": true
}
],
"yoda": ["error", "never"],
"import-x/first": "error",
"import-x/no-absolute-path": [
"error",
{
"esmodule": true,
"commonjs": true,
"amd": false
}
],
"import-x/no-duplicates": "error",
"import-x/no-named-default": "error",
"import-x/no-webpack-loader-syntax": "error",
"promise/param-names": "error",
"node/no-exports-assign": "error",
"node/no-new-require": "error"
},
"globals": {
"__dirname": "readonly",
"__filename": "readonly",
"AbortController": "readonly",
"AbortSignal": "readonly",
"atob": "readonly",
"Blob": "readonly",
"BroadcastChannel": "readonly",
"btoa": "readonly",
"Buffer": "readonly",
"ByteLengthQueuingStrategy": "readonly",
"clearImmediate": "readonly",
"clearInterval": "readonly",
"clearTimeout": "readonly",
"CloseEvent": "readonly",
"CompressionStream": "readonly",
"console": "readonly",
"CountQueuingStrategy": "readonly",
"crypto": "readonly",
"Crypto": "readonly",
"CryptoKey": "readonly",
"CustomEvent": "readonly",
"DecompressionStream": "readonly",
"DOMException": "readonly",
"Event": "readonly",
"EventTarget": "readonly",
"fetch": "readonly",
"File": "readonly",
"FormData": "readonly",
"Headers": "readonly",
"MessageChannel": "readonly",
"MessageEvent": "readonly",
"MessagePort": "readonly",
"navigator": "readonly",
"Navigator": "readonly",
"performance": "readonly",
"Performance": "readonly",
"PerformanceEntry": "readonly",
"PerformanceMark": "readonly",
"PerformanceMeasure": "readonly",
"PerformanceObserver": "readonly",
"PerformanceObserverEntryList": "readonly",
"PerformanceResourceTiming": "readonly",
"process": "readonly",
"queueMicrotask": "readonly",
"ReadableByteStreamController": "readonly",
"ReadableStream": "readonly",
"ReadableStreamBYOBReader": "readonly",
"ReadableStreamBYOBRequest": "readonly",
"ReadableStreamDefaultController": "readonly",
"ReadableStreamDefaultReader": "readonly",
"Request": "readonly",
"Response": "readonly",
"setImmediate": "readonly",
"setInterval": "readonly",
"setTimeout": "readonly",
"structuredClone": "readonly",
"SubtleCrypto": "readonly",
"TextDecoder": "readonly",
"TextDecoderStream": "readonly",
"TextEncoder": "readonly",
"TextEncoderStream": "readonly",
"TransformStream": "readonly",
"TransformStreamDefaultController": "readonly",
"URL": "readonly",
"URLSearchParams": "readonly",
"WebAssembly": "readonly",
"WebSocket": "readonly",
"WritableStream": "readonly",
"WritableStreamDefaultController": "readonly",
"WritableStreamDefaultWriter": "readonly",
"document": "readonly",
"window": "readonly"
},
"env": {
"commonjs": true,
"es2024": true
},
"plugins": ["import", "node", "promise"]
},
{
"files": ["**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}"],
"rules": {
"react/jsx-boolean-value": "error",
"react/jsx-fragments": ["error", "syntax"],
"react/jsx-handler-names": "error",
"react/jsx-key": [
"error",
{
"checkFragmentShorthand": true
}
],
"react/jsx-no-comment-textnodes": "error",
"react/jsx-no-duplicate-props": "error",
"react/jsx-no-target-blank": [
"error",
{
"enforceDynamicLinks": "always"
}
],
// "react/jsx-no-undef": [
// "error",
// {
// "allowGlobals": true
// }
// ],
"react/no-children-prop": "error",
"react/no-danger-with-children": "error",
"react/no-direct-mutation-state": "error",
"react/no-find-dom-node": "error",
"react/no-is-mounted": "error",
"react/no-string-refs": [
"error",
{
"noTemplateLiterals": true
}
],
"react/no-unescaped-entities": "off",
"react/no-render-return-value": "error",
"react/self-closing-comp": "error"
},
"plugins": ["react"]
},
{
"files": ["**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}"],
"rules": {
"constructor-super": "off",
"no-const-assign": "off",
"no-dupe-class-members": "error",
"no-dupe-keys": "off",
"no-func-assign": "off",
"no-import-assign": "off",
"no-new-native-nonconstructor": "off",
"no-obj-calls": "off",
"no-redeclare": [
"error",
{
"builtinGlobals": false
}
],
"no-this-before-super": "off",
"no-unsafe-negation": "off",
"no-array-constructor": "error",
"no-loss-of-precision": "error",
"no-unused-expressions": [
"error",
{
"allowShortCircuit": true,
"allowTernary": true,
"allowTaggedTemplates": true
}
],
"no-unused-vars": [
"error",
{
"args": "none",
"caughtErrors": "none",
"ignoreRestSiblings": true,
"vars": "all"
}
],
"no-useless-constructor": "error"
},
"plugins": ["typescript"]
},
{
"files": ["**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}"],
"rules": {
"curly": "off",
"no-unexpected-multiline": "off",
"unicorn/empty-brace-spaces": "off",
"unicorn/no-nested-ternary": "off",
"unicorn/number-literal-case": "off"
},
"plugins": ["unicorn"]
},
{
"files": ["**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}"],
"rules": {
"curly": "off",
"no-unexpected-multiline": "off",
"unicorn/empty-brace-spaces": "off",
"unicorn/no-nested-ternary": "off",
"unicorn/number-literal-case": "off",
"arrow-body-style": "off"
},
"plugins": ["unicorn"]
},
{
"files": ["**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}"],
"rules": {
"no-console": "off",
"no-debugger": "off",
"no-unused-vars": "warn",
"react/react-in-jsx-scope": "off"
},
"plugins": ["typescript", "react"]
},
{
"files": ["**/*.{ts,tsx,mtsx}"],
"rules": {
"constructor-super": "off",
"no-class-assign": "off",
"no-const-assign": "off",
"no-dupe-class-members": "off",
"no-dupe-keys": "off",
"no-func-assign": "off",
"no-import-assign": "off",
"no-new-native-nonconstructor": "off",
"no-obj-calls": "off",
"no-redeclare": "off",
"no-setter-return": "off",
"no-this-before-super": "off",
"no-unsafe-negation": "off",
"no-var": "error",
"no-with": "off",
"prefer-const": "error",
"prefer-rest-params": "error",
"prefer-spread": "error"
}
},
{
"files": ["**/*.{ts,tsx,mtsx}"],
"rules": {
"@typescript-eslint/ban-ts-comment": "error",
"no-array-constructor": "error",
"@typescript-eslint/no-duplicate-enum-values": "error",
"@typescript-eslint/no-empty-object-type": "error",
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-extra-non-null-assertion": "error",
"@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-namespace": "error",
"@typescript-eslint/no-non-null-asserted-optional-chain": "error",
"@typescript-eslint/no-require-imports": "error",
"@typescript-eslint/no-this-alias": "error",
"@typescript-eslint/no-unnecessary-type-constraint": "error",
"@typescript-eslint/no-unsafe-declaration-merging": "error",
"@typescript-eslint/no-unsafe-function-type": "error",
"no-unused-expressions": "error",
"no-unused-vars": "error",
"@typescript-eslint/no-wrapper-object-types": "error",
"@typescript-eslint/prefer-as-const": "error",
"@typescript-eslint/prefer-namespace-keyword": "error",
"@typescript-eslint/triple-slash-reference": "error"
},
"plugins": ["typescript"]
},
{
"files": ["**/*.{ts,tsx,mtsx}"],
"rules": {
"@typescript-eslint/no-explicit-any": "warn",
"no-unused-vars": "warn"
},
"plugins": ["typescript"]
},
{
"files": [
"frontend/nyanpasu/vite.config.ts",
"frontend/nyanpasu/tailwind.config.ts"
],
"rules": {
"constructor-super": "off",
"no-class-assign": "off",
"no-const-assign": "off",
"no-dupe-class-members": "off",
"no-dupe-keys": "off",
"no-func-assign": "off",
"no-import-assign": "off",
"no-new-native-nonconstructor": "off",
"no-obj-calls": "off",
"no-redeclare": "off",
"no-setter-return": "off",
"no-this-before-super": "off",
"no-unsafe-negation": "off",
"no-var": "error",
"no-with": "off",
"prefer-const": "error",
"prefer-rest-params": "error",
"prefer-spread": "error"
}
},
{
"files": [
"frontend/nyanpasu/vite.config.ts",
"frontend/nyanpasu/tailwind.config.ts"
],
"rules": {
"@typescript-eslint/ban-ts-comment": "error",
"no-array-constructor": "error",
"@typescript-eslint/no-duplicate-enum-values": "error",
"@typescript-eslint/no-empty-object-type": "error",
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-extra-non-null-assertion": "error",
"@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-namespace": "error",
"@typescript-eslint/no-non-null-asserted-optional-chain": "error",
"@typescript-eslint/no-require-imports": "error",
"@typescript-eslint/no-this-alias": "error",
"@typescript-eslint/no-unnecessary-type-constraint": "error",
"@typescript-eslint/no-unsafe-declaration-merging": "error",
"@typescript-eslint/no-unsafe-function-type": "error",
"no-unused-expressions": "error",
"no-unused-vars": "error",
"@typescript-eslint/no-wrapper-object-types": "error",
"@typescript-eslint/prefer-as-const": "error",
"@typescript-eslint/prefer-namespace-keyword": "error",
"@typescript-eslint/triple-slash-reference": "error"
},
"plugins": ["typescript"]
},
{
"files": [
"frontend/nyanpasu/vite.config.ts",
"frontend/nyanpasu/tailwind.config.ts"
],
"rules": {
"@typescript-eslint/no-explicit-any": "warn",
"no-unused-vars": "warn"
},
"plugins": ["typescript"]
},
{
"files": ["frontend/ui/vite.config.ts"],
"rules": {
"constructor-super": "off",
"no-class-assign": "off",
"no-const-assign": "off",
"no-dupe-class-members": "off",
"no-dupe-keys": "off",
"no-func-assign": "off",
"no-import-assign": "off",
"no-new-native-nonconstructor": "off",
"no-obj-calls": "off",
"no-redeclare": "off",
"no-setter-return": "off",
"no-this-before-super": "off",
"no-unsafe-negation": "off",
"no-var": "error",
"no-with": "off",
"prefer-const": "error",
"prefer-rest-params": "error",
"prefer-spread": "error"
}
},
{
"files": ["frontend/ui/vite.config.ts"],
"rules": {
"@typescript-eslint/ban-ts-comment": "error",
"no-array-constructor": "error",
"@typescript-eslint/no-duplicate-enum-values": "error",
"@typescript-eslint/no-empty-object-type": "error",
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-extra-non-null-assertion": "error",
"@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-namespace": "error",
"@typescript-eslint/no-non-null-asserted-optional-chain": "error",
"@typescript-eslint/no-require-imports": "error",
"@typescript-eslint/no-this-alias": "error",
"@typescript-eslint/no-unnecessary-type-constraint": "error",
"@typescript-eslint/no-unsafe-declaration-merging": "error",
"@typescript-eslint/no-unsafe-function-type": "error",
"no-unused-expressions": "error",
"no-unused-vars": "error",
"@typescript-eslint/no-wrapper-object-types": "error",
"@typescript-eslint/prefer-as-const": "error",
"@typescript-eslint/prefer-namespace-keyword": "error",
"@typescript-eslint/triple-slash-reference": "error"
},
"plugins": ["typescript"]
},
{
"files": ["frontend/ui/vite.config.ts"],
"rules": {
"@typescript-eslint/no-explicit-any": "warn",
"no-unused-vars": "warn"
},
"plugins": ["typescript"]
},
{
"files": ["**/*.{jsx,mjsx,tsx,mtsx}"],
"globals": {
"AudioWorkletGlobalScope": "readonly",
"AudioWorkletProcessor": "readonly",
"currentFrame": "readonly",
"currentTime": "readonly",
"registerProcessor": "readonly",
"sampleRate": "readonly",
"WorkletGlobalScope": "readonly"
},
"env": {
"browser": true,
"serviceworker": true
}
}
]
}
+2 -2
View File
@@ -4,13 +4,13 @@
"editorconfig.editorconfig",
"vadimcn.vscode-lldb",
"bungcip.better-toml",
"dbaeumer.vscode-eslint",
"denoland.vscode-deno",
"esbenp.prettier-vscode",
"yoavbls.pretty-ts-errors",
"rust-lang.rust-analyzer",
"syler.sass-indented",
"stylelint.vscode-stylelint",
"bradlc.vscode-tailwindcss"
"bradlc.vscode-tailwindcss",
"oxc.oxc-vscode"
]
}
+8 -8
View File
@@ -355,7 +355,7 @@ dependencies = [
"objc2-foundation 0.3.2",
"parking_lot",
"percent-encoding",
"windows-sys 0.52.0",
"windows-sys 0.60.2",
"wl-clipboard-rs",
"x11rb",
]
@@ -2859,7 +2859,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.60.2",
]
[[package]]
@@ -4456,9 +4456,9 @@ dependencies = [
[[package]]
name = "interprocess"
version = "2.2.3"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d941b405bd2322993887859a8ee6ac9134945a24ec5ec763a8a962fc64dfec2d"
checksum = "7b00d05442c2106c75b7410f820b152f61ec0edc7befcb9b381b673a20314753"
dependencies = [
"doctest-file",
"futures-core",
@@ -7223,7 +7223,7 @@ dependencies = [
"once_cell",
"socket2",
"tracing",
"windows-sys 0.52.0",
"windows-sys 0.60.2",
]
[[package]]
@@ -7786,7 +7786,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.4.15",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -7799,7 +7799,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.11.0",
"windows-sys 0.52.0",
"windows-sys 0.60.2",
]
[[package]]
@@ -9473,7 +9473,7 @@ dependencies = [
"getrandom 0.3.3",
"once_cell",
"rustix 1.1.3",
"windows-sys 0.52.0",
"windows-sys 0.60.2",
]
[[package]]
+1 -1
View File
@@ -307,7 +307,7 @@ pub fn run() -> std::io::Result<()> {
.map_err(io::Error::other)
})
.bigint(BigIntExportBehavior::Number)
.header("/* eslint-disable */\n// @ts-nocheck"),
.header("/* oxlint-disable */\n// @ts-nocheck"),
SPECTA_BINDINGS_PATH,
) {
Ok(_) => {
-143
View File
@@ -1,143 +0,0 @@
// @ts-check
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import eslintConfigPrettier from 'eslint-config-prettier'
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
import react from 'eslint-plugin-react'
import pluginReactCompiler from 'eslint-plugin-react-compiler'
import pluginReactHooks from 'eslint-plugin-react-hooks'
import globals from 'globals'
import neostandard from 'neostandard'
import tseslint from 'typescript-eslint'
import { includeIgnoreFile } from '@eslint/compat'
import { FlatCompat } from '@eslint/eslintrc'
// import ImportX from "eslint-plugin-import-x";
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const compat = new FlatCompat({
// import.meta.dirname is available after Node.js v20.11.0
baseDirectory: __dirname,
})
const gitignorePath = path.resolve(__dirname, '.gitignore')
const ignores = [
path.resolve(__dirname, 'index.html'),
'**/node_modules/',
'node_modules/',
'**/dist/',
'dist/',
'backend/',
'backend/**/target',
'scripts/deno/**',
'eslint.config.js',
'.lintstagedrc.js',
'commitlint.config.js',
]
export default tseslint.config(
includeIgnoreFile(gitignorePath),
{
ignores,
},
{
files: ['**/*.{jsx,mjsx,tsx,mtsx}'],
extends: [react.configs.flat.recommended],
plugins: {
'react-hooks': pluginReactHooks,
'react-compiler': pluginReactCompiler,
},
settings: {
react: {
version: 'detect',
},
},
rules: {
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
'react-compiler/react-compiler': 'warn',
},
},
{
files: ['**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}'],
extends: [
...neostandard({ ts: true, semi: true, noStyle: true }),
eslintConfigPrettier,
eslintPluginPrettierRecommended,
],
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': 'warn',
'react/react-in-jsx-scope': 'off',
'prettier/prettier': [
'error',
{
singleQuote: true,
},
],
},
},
{
files: ['**/*.{ts,tsx,mtsx}'],
extends: [...tseslint.configs.recommended],
ignores: [
...ignores,
'frontend/nyanpasu/vite.config.ts',
'frontend/nyanpasu/tailwind.config.ts',
'frontend/ui/vite.config.ts',
],
rules: {
'@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/no-explicit-any': 'warn',
},
languageOptions: {
parserOptions: {
project: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
{
files: [
'frontend/nyanpasu/vite.config.ts',
'frontend/nyanpasu/tailwind.config.ts',
],
extends: [...tseslint.configs.recommended],
rules: {
'@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/no-explicit-any': 'warn',
},
languageOptions: {
parserOptions: {
project: './frontend/nyanpasu/tsconfig.node.json',
},
},
},
{
files: ['frontend/ui/vite.config.ts'],
extends: [...tseslint.configs.recommended],
rules: {
'@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/no-explicit-any': 'warn',
},
languageOptions: {
parserOptions: {
project: './frontend/ui/tsconfig.json',
},
},
},
{
files: ['**/*.{jsx,mjsx,tsx,mtsx}'],
languageOptions: {
...react.configs.flat?.recommended.languageOptions,
globals: {
...globals.serviceworker,
...globals.browser,
},
},
},
)
@@ -22,6 +22,6 @@
},
"devDependencies": {
"@types/lodash-es": "4.17.12",
"@types/react": "19.2.11"
"@types/react": "19.2.13"
}
}
@@ -7,7 +7,7 @@ import {
import * as TAURI_API_EVENT from '@tauri-apps/api/event'
import { type WebviewWindow as __WebviewWindow__ } from '@tauri-apps/api/webviewWindow'
/* eslint-disable */
/* oxlint-disable */
// @ts-nocheck
// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually.
@@ -3,6 +3,16 @@
*/
export const NYANPASU_BACKEND_EVENT_NAME = 'nyanpasu://mutation'
/**
* Service prompt query key, used by useServicePrompt hook
*/
export const SERVICE_PROMPT_QUERY_KEY = 'service-prompt'
/**
* Core dir query key, used by useCoreDir hook
*/
export const CORE_DIR_QUERY_KEY = 'core-dir'
/**
* Server port query key, used by useServerPort hook
*/
@@ -22,6 +22,8 @@ export * from './use-runtime-profile'
export * from './use-settings'
export * from './use-system-proxy'
export * from './use-system-service'
export * from './use-service-prompt'
export * from './use-core-dir'
export { commands } from './bindings'
export type * from './bindings'
@@ -0,0 +1,17 @@
import { unwrapResult } from '@/utils'
import { useQuery } from '@tanstack/react-query'
import { commands } from './bindings'
import { CORE_DIR_QUERY_KEY } from './consts'
export const useCoreDir = () => {
const query = useQuery({
queryKey: [CORE_DIR_QUERY_KEY],
queryFn: async () => {
return unwrapResult(await commands.getCoreDir())
},
})
return {
...query,
}
}
@@ -48,7 +48,7 @@ export const useProxyMode = () => {
}
return modes
}, [clashConfig.query.data])
}, [clashConfig.query.data, clashCore.value])
const upsert = async (mode: string) => {
await deleteConnections()
@@ -0,0 +1,17 @@
import { unwrapResult } from '@/utils'
import { useQuery } from '@tanstack/react-query'
import { commands } from './bindings'
import { SERVICE_PROMPT_QUERY_KEY } from './consts'
export const useServicePrompt = () => {
const query = useQuery({
queryKey: [SERVICE_PROMPT_QUERY_KEY],
queryFn: async () => {
return unwrapResult(await commands.getServiceInstallPrompt())
},
})
return {
...query,
}
}
@@ -1,3 +1,4 @@
// oxlint-disable typescript/no-explicit-any
import { IPSBResponse } from '@/openapi'
import { invoke } from '@tauri-apps/api/core'
import type {
@@ -9,6 +9,15 @@
"navbar_label_rules": "Rules",
"navbar_label_settings": "Settings",
"navbar_label_providers": "Providers",
"header_help_action_title": "Help",
"header_help_action_wiki": "Official Wiki",
"header_help_action_issues": "Report Issue",
"header_help_action_about": "About",
"header_settings_action_title": "Settings",
"header_settings_action_language": "Language",
"header_settings_action_theme_mode": "Theme Mode",
"header_file_action_title": "File",
"header_file_action_import_local_profile": "Import Local Profile",
"settings_system_proxy_title": "System Settings",
"settings_system_proxy_system_proxy_label": "System Proxy",
"settings_system_proxy_tun_mode_label": "TUN Mode",
@@ -16,6 +25,19 @@
"settings_system_proxy_proxy_guard_interval_label": "System Proxy Guard Interval",
"settings_system_proxy_proxy_bypass_label": "System Proxy Bypass",
"settings_system_proxy_current_system_proxy_label": "Current System Proxy",
"settings_system_proxy_service_mode_label": "Service Mode",
"settings_system_proxy_service_mode_disabled_tooltip": "To enable service mode, make sure the Clash Nyanpasu service is installed and started",
"settings_system_proxy_system_service_ctrl_label": "System Service",
"settings_system_proxy_system_service_ctrl_detail": "Service Detail",
"settings_system_proxy_system_service_ctrl_install": "Install",
"settings_system_proxy_system_service_ctrl_uninstall": "Uninstall",
"settings_system_proxy_system_service_ctrl_failed_install": "Install failed",
"settings_system_proxy_system_service_ctrl_failed_uninstall": "Uninstall failed",
"settings_system_proxy_system_service_ctrl_prompt": "Service Prompt",
"settings_system_proxy_system_service_ctrl_manual_prompt": "Service Manual Tips",
"settings_system_proxy_system_service_ctrl_manual_operation_prompt": "Unable to control the service automatically. Please navigate to the core directory, run PowerShell as administrator on Windows or a terminal emulator on macOS/Linux, and execute the following commands:",
"settings_system_proxy_system_service_ctrl_start": "Start",
"settings_system_proxy_system_service_ctrl_stop": "Stop",
"settings_user_interface_title": "User Interface",
"settings_user_interface_language_label": "Language",
"settings_user_interface_theme_mode_label": "Theme Mode",
@@ -23,6 +45,7 @@
"settings_user_interface_theme_mode_dark": "Dark",
"settings_user_interface_theme_mode_system": "System",
"settings_user_interface_theme_color_label": "Theme Color",
"settings_user_interface_theme_color_custom": "Custom",
"settings_clash_settings_title": "Clash Settings",
"settings_clash_settings_allow_lan_label": "Allow LAN",
"settings_clash_settings_ipv6_label": "Enable IPv6",
@@ -32,11 +55,26 @@
"settings_clash_settings_random_port_label": "Random Port",
"settings_clash_settings_random_port_enabled": "Random port enabled, after restart to take effect.",
"settings_clash_settings_random_port_disabled": "Random port disabled, after restart to take effect.",
"settings_label_system": "System Settings",
"settings_label_system_description": "Proxy mode, proxy bypass, auto start, silent start and more.",
"settings_label_user_interface": "User Interface",
"settings_label_user_interface_description": "Language, theme mode, theme color and more.",
"settings_label_clash_settings": "Clash Settings",
"settings_label_clash_settings_description": "Clash configuration, log level, mixed port, random port and more.",
"settings_label_external_controll": "Clash External Control",
"settings_label_external_controll_description": "Web UI address, port strategy, API key and more.",
"settings_label_nyanpasu": "Nyanpasu Settings",
"settings_label_nyanpasu_description": "Nyanpasu specific settings",
"settings_label_debug": "Debug Tools",
"settings_label_debug_description": "Debug tools and more.",
"settings_label_about": "About",
"settings_label_about_description": "About Clash Nyanpasu",
"profile_subscription_title": "Subscription",
"profile_subscription_updated_at": "{updated} updated",
"profile_subscription_next_update_at": "Next update at {next} update",
"profile_subscription_expires_in": "{expires} expires",
"profile_subscription_update": "Update",
"profile_base_info_title": "Basic Info",
"profile_name_editor_title": "Edit Name",
"profile_name_label": "Profile Name",
"profile_update_option_edit": "Sub. Opts",
@@ -92,5 +130,7 @@
"common_apply": "Apply",
"common_reset": "Reset",
"common_save": "Save",
"common_validate": "Validate"
"common_validate": "Validate",
"common_close": "Close",
"common_copy": "Copy"
}
@@ -9,6 +9,15 @@
"navbar_label_rules": "Правила",
"navbar_label_settings": "Настройки",
"navbar_label_providers": "Поставщики",
"header_help_action_title": "Помощь",
"header_help_action_wiki": "Онлайн документация",
"header_help_action_issues": "Сообщить о проблеме",
"header_help_action_about": "О программе",
"header_settings_action_title": "Настройки",
"header_settings_action_language": "Язык",
"header_settings_action_theme_mode": "Режим темы",
"header_file_action_title": "Файл",
"header_file_action_import_local_profile": "Импорт локального профиля",
"settings_system_proxy_title": "Системный прокси",
"settings_system_proxy_system_proxy_label": "Системный прокси",
"settings_system_proxy_tun_mode_label": "TUN режим",
@@ -16,6 +25,19 @@
"settings_system_proxy_proxy_guard_interval_label": "Интервал охраны прокси",
"settings_system_proxy_proxy_bypass_label": "Обход прокси",
"settings_system_proxy_current_system_proxy_label": "Текущий системный прокси",
"settings_system_proxy_service_mode_label": "Режим службы",
"settings_system_proxy_service_mode_disabled_tooltip": "Чтобы включить режим службы, убедитесь, что служба Clash Nyanpasu установлена и запущена",
"settings_system_proxy_system_service_ctrl_label": "Системный сервис",
"settings_system_proxy_system_service_ctrl_detail": "Подробности сервиса",
"settings_system_proxy_system_service_ctrl_install": "Установить",
"settings_system_proxy_system_service_ctrl_uninstall": "Удалить",
"settings_system_proxy_system_service_ctrl_failed_install": "Установка не удалась",
"settings_system_proxy_system_service_ctrl_failed_uninstall": "Удаление не удалось",
"settings_system_proxy_system_service_ctrl_prompt": "Подсказка по сервису",
"settings_system_proxy_system_service_ctrl_manual_prompt": "Подсказка по сервису",
"settings_system_proxy_system_service_ctrl_manual_operation_prompt": "Не удалось автоматически управлять сервисом. Пожалуйста, перейдите в каталог ядра, запустите PowerShell как администратор на Windows или эмулятор терминала на macOS/Linux и выполните следующие команды:",
"settings_system_proxy_system_service_ctrl_start": "Запустить",
"settings_system_proxy_system_service_ctrl_stop": "Остановить",
"settings_user_interface_title": "Интерфейс пользователя",
"settings_user_interface_language_label": "Язык",
"settings_user_interface_theme_mode_label": "Режим темы",
@@ -23,6 +45,7 @@
"settings_user_interface_theme_mode_dark": "Темный",
"settings_user_interface_theme_mode_system": "Системный",
"settings_user_interface_theme_color_label": "Цвет темы",
"settings_user_interface_theme_color_custom": "Пользовательский",
"settings_clash_settings_title": "Настройки Clash",
"settings_clash_settings_allow_lan_label": "Разрешить LAN",
"settings_clash_settings_ipv6_label": "Включить IPv6",
@@ -32,11 +55,26 @@
"settings_clash_settings_random_port_label": "Случайный порт",
"settings_clash_settings_random_port_enabled": "Случайный порт включен, после перезапуска для вступления в силу.",
"settings_clash_settings_random_port_disabled": "Случайный порт отключен, после перезапуска для вступления в силу.",
"settings_label_system": "Системные настройки",
"settings_label_system_description": "Режим прокси, обход прокси, автозапуск, старт без отображения окна и т.д.",
"settings_label_user_interface": "Интерфейс пользователя",
"settings_label_user_interface_description": "Язык, режим темы, цвет темы и т.д.",
"settings_label_clash_settings": "Настройки Clash",
"settings_label_clash_settings_description": "Конфигурация Clash, уровень логирования, смешанный порт, случайный порт и т.д.",
"settings_label_external_controll": "Внешнее управление Clash",
"settings_label_external_controll_description": "Адрес Web UI, стратегия порта, API ключ и т.д.",
"settings_label_nyanpasu": "Настройки Nyanpasu",
"settings_label_nyanpasu_description": "Специфические настройки Nyanpasu",
"settings_label_debug": "Отладочные инструменты",
"settings_label_debug_description": "Отладочные инструменты и т.д.",
"settings_label_about": "О программе",
"settings_label_about_description": "О Clash Nyanpasu",
"profile_subscription_title": "Информация о подписке",
"profile_subscription_updated_at": "{updated} обновлено",
"profile_subscription_next_update_at": "Следующее обновление через {next} обновление",
"profile_subscription_expires_in": "{expires} истекает",
"profile_subscription_update": "Обновить",
"profile_base_info_title": "Основная информация",
"profile_name_editor_title": "Редактировать название",
"profile_name_label": "Название профиля",
"profile_update_option_edit": "Опции подписки",
@@ -92,5 +130,7 @@
"common_apply": "Применить",
"common_reset": "Сбросить",
"common_save": "Сохранить",
"common_validate": "Проверить"
"common_validate": "Проверить",
"common_close": "Закрыть",
"common_copy": "Копировать"
}
@@ -9,6 +9,15 @@
"navbar_label_rules": "规则",
"navbar_label_settings": "设置",
"navbar_label_providers": "资源",
"header_help_action_title": "帮助",
"header_help_action_wiki": "在线文档",
"header_help_action_issues": "报告问题",
"header_help_action_about": "关于",
"header_settings_action_title": "设置",
"header_settings_action_language": "语言",
"header_settings_action_theme_mode": "主题模式",
"header_file_action_title": "文件",
"header_file_action_import_local_profile": "导入本地配置",
"settings_system_proxy_title": "系统设置",
"settings_system_proxy_system_proxy_label": "系统代理",
"settings_system_proxy_tun_mode_label": "TUN 模式",
@@ -16,6 +25,19 @@
"settings_system_proxy_proxy_guard_interval_label": "系统代理守卫间隔",
"settings_system_proxy_proxy_bypass_label": "系统代理绕过",
"settings_system_proxy_current_system_proxy_label": "当前系统代理",
"settings_system_proxy_service_mode_label": "服务模式",
"settings_system_proxy_service_mode_disabled_tooltip": "要启用服务模式,请确保 Clash Nyanpasu 服务已安装并启动",
"settings_system_proxy_system_service_ctrl_label": "系统服务",
"settings_system_proxy_system_service_ctrl_detail": "服务详情",
"settings_system_proxy_system_service_ctrl_install": "安装",
"settings_system_proxy_system_service_ctrl_uninstall": "卸载",
"settings_system_proxy_system_service_ctrl_failed_install": "安装失败",
"settings_system_proxy_system_service_ctrl_failed_uninstall": "卸载失败",
"settings_system_proxy_system_service_ctrl_prompt": "服务提示",
"settings_system_proxy_system_service_ctrl_manual_prompt": "手动操作服务提示",
"settings_system_proxy_system_service_ctrl_manual_operation_prompt": "无法自动操作服务。请导航到内核所在目录,在 Windows 上以管理员身份打开 PowerShell 或在 macOS/Linux 上打开终端仿真器,然后执行以下命令:",
"settings_system_proxy_system_service_ctrl_start": "启动",
"settings_system_proxy_system_service_ctrl_stop": "停止",
"settings_user_interface_title": "用户界面",
"settings_user_interface_language_label": "语言",
"settings_user_interface_theme_mode_label": "主题模式",
@@ -23,6 +45,7 @@
"settings_user_interface_theme_mode_dark": "深色",
"settings_user_interface_theme_mode_system": "跟随系统",
"settings_user_interface_theme_color_label": "主题颜色",
"settings_user_interface_theme_color_custom": "自定义",
"settings_clash_settings_title": "Clash 设置",
"settings_clash_settings_allow_lan_label": "允许局域网连接",
"settings_clash_settings_ipv6_label": "启用 IPv6",
@@ -32,6 +55,20 @@
"settings_clash_settings_random_port_label": "随机端口",
"settings_clash_settings_random_port_enabled": "随机端口已启用,重启后生效。",
"settings_clash_settings_random_port_disabled": "随机端口已禁用,重启后生效。",
"settings_label_system": "系统设置",
"settings_label_system_description": "代理模式、代理绕过、开机自启、静默启动等设置",
"settings_label_user_interface": "用户界面",
"settings_label_user_interface_description": "语言、主题模式、主题颜色等设置",
"settings_label_clash_settings": "Clash 设置",
"settings_label_clash_settings_description": "Clash 配置、日志级别、混合端口、随机端口等设置",
"settings_label_external_controll": "Web UI 与外部控制",
"settings_label_external_controll_description": "Web UI 地址、端口策略、API 密钥等设置",
"settings_label_nyanpasu": "Nyanpasu 配置",
"settings_label_nyanpasu_description": "Nyanpasu 特性配置",
"settings_label_debug": "调试工具",
"settings_label_debug_description": "调试工具配置",
"settings_label_about": "关于",
"settings_label_about_description": "关于 Clash Nyanpasu",
"profile_subscription_title": "订阅信息",
"profile_subscription_updated_at": "{updated}更新",
"profile_subscription_next_update_at": "下次更新于 {next} 更新",
@@ -93,5 +130,7 @@
"common_apply": "应用",
"common_reset": "重置",
"common_save": "保存",
"common_validate": "验证"
"common_validate": "验证",
"common_close": "关闭",
"common_copy": "复制"
}
@@ -9,6 +9,15 @@
"navbar_label_rules": "規則",
"navbar_label_settings": "設置",
"navbar_label_providers": "資源",
"header_help_action_title": "幫助",
"header_help_action_wiki": "線上文檔",
"header_help_action_issues": "報告問題",
"header_help_action_about": "關於",
"header_settings_action_title": "設置",
"header_settings_action_language": "語言",
"header_settings_action_theme_mode": "主題模式",
"header_file_action_title": "文件",
"header_file_action_import_local_profile": "導入本地配置",
"settings_system_proxy_title": "系統設置",
"settings_system_proxy_system_proxy_label": "系統代理",
"settings_system_proxy_tun_mode_label": "TUN 模式",
@@ -16,6 +25,19 @@
"settings_system_proxy_proxy_guard_interval_label": "系統代理守衛間隔",
"settings_system_proxy_proxy_bypass_label": "系統代理繞過",
"settings_system_proxy_current_system_proxy_label": "當前系統代理",
"settings_system_proxy_service_mode_label": "服務模式",
"settings_system_proxy_service_mode_disabled_tooltip": "要啟用服務模式,請確保 Clash Nyanpasu 服務已安裝並啟動",
"settings_system_proxy_system_service_ctrl_label": "系統服務",
"settings_system_proxy_system_service_ctrl_detail": "服務詳情",
"settings_system_proxy_system_service_ctrl_install": "安裝",
"settings_system_proxy_system_service_ctrl_uninstall": "卸載",
"settings_system_proxy_system_service_ctrl_failed_install": "安裝失敗",
"settings_system_proxy_system_service_ctrl_failed_uninstall": "卸載失敗",
"settings_system_proxy_system_service_ctrl_prompt": "服務提示",
"settings_system_proxy_system_service_ctrl_manual_prompt": "手動操作服務提示",
"settings_system_proxy_system_service_ctrl_manual_operation_prompt": "無法自動操作服務。請開啟核心所在目錄,在 Windows 上以管理員身分開啟 PowerShell 或在 macOS/Linux 上開啟終端模擬器,然後執行以下指令:",
"settings_system_proxy_system_service_ctrl_start": "啟動",
"settings_system_proxy_system_service_ctrl_stop": "停止",
"settings_user_interface_title": "使用者介面",
"settings_user_interface_language_label": "語言",
"settings_user_interface_theme_mode_label": "主題模式",
@@ -23,6 +45,7 @@
"settings_user_interface_theme_mode_dark": "深色",
"settings_user_interface_theme_mode_system": "跟隨系統",
"settings_user_interface_theme_color_label": "主題顏色",
"settings_user_interface_theme_color_custom": "自定義",
"settings_clash_settings_title": "Clash 設置",
"settings_clash_settings_allow_lan_label": "允許區域網路連線",
"settings_clash_settings_ipv6_label": "啟用 IPv6",
@@ -32,11 +55,26 @@
"settings_clash_settings_random_port_label": "隨機端口",
"settings_clash_settings_random_port_enabled": "隨機端口已啟用,重啟後生效。",
"settings_clash_settings_random_port_disabled": "隨機端口已禁用,重啟後生效。",
"settings_label_system": "系統設置",
"settings_label_system_description": "代理模式、代理繞過、開機自啟、靜默啟動等設定",
"settings_label_user_interface": "使用者介面",
"settings_label_user_interface_description": "語言、主題模式、主題顏色等設定",
"settings_label_clash_settings": "Clash 設置",
"settings_label_clash_settings_description": "Clash 配置、日誌級別、混合端口、隨機端口等設定",
"settings_label_external_controll": "Web UI 與外部控制",
"settings_label_external_controll_description": "Web UI 位址、端口策略、API 密鑰等設定",
"settings_label_nyanpasu": "Nyanpasu 設置",
"settings_label_nyanpasu_description": "Nyanpasu 特性設定",
"settings_label_debug": "調試工具",
"settings_label_debug_description": "調試工具設定",
"settings_label_about": "關於",
"settings_label_about_description": "關於 Clash Nyanpasu",
"profile_subscription_title": "訂閱信息",
"profile_subscription_updated_at": "{updated}更新",
"profile_subscription_next_update_at": "下次更新於 {next} 更新",
"profile_subscription_expires_in": "{expires}到期",
"profile_subscription_update": "更新",
"profile_base_info_title": "基本資訊",
"profile_name_editor_title": "編輯名稱",
"profile_name_label": "配置名稱",
"profile_update_option_edit": "訂閱選項",
@@ -92,5 +130,7 @@
"common_apply": "應用",
"common_reset": "重置",
"common_save": "保存",
"common_validate": "驗證"
"common_validate": "驗證",
"common_close": "關閉",
"common_copy": "複製"
}
@@ -47,9 +47,9 @@
"country-code-emoji": "2.3.0",
"country-emoji": "1.5.6",
"dayjs": "1.11.19",
"framer-motion": "12.31.0",
"framer-motion": "12.33.0",
"i18next": "25.7.3",
"jotai": "2.16.1",
"jotai": "2.17.1",
"json-schema": "0.4.0",
"material-react-table": "3.2.1",
"monaco-editor": "0.54.0",
@@ -58,7 +58,7 @@
"react-dom": "19.2.4",
"react-error-boundary": "6.0.0",
"react-fast-marquee": "1.6.5",
"react-hook-form": "7.69.0",
"react-hook-form": "7.71.1",
"react-hook-form-mui": "8.2.0",
"react-i18next": "15.7.4",
"react-markdown": "10.1.0",
@@ -73,7 +73,7 @@
"@csstools/normalize.css": "12.1.1",
"@emotion/babel-plugin": "11.13.5",
"@emotion/react": "11.14.0",
"@iconify/json": "2.2.435",
"@iconify/json": "2.2.436",
"@monaco-editor/react": "4.7.0",
"@tanstack/react-query": "5.90.20",
"@tanstack/react-router": "1.158.1",
@@ -87,7 +87,7 @@
"@tauri-apps/plugin-process": "2.3.0",
"@tauri-apps/plugin-shell": "2.3.1",
"@tauri-apps/plugin-updater": "2.9.0",
"@types/react": "19.2.11",
"@types/react": "19.2.13",
"@types/react-dom": "19.2.3",
"@types/validator": "13.15.10",
"@vitejs/plugin-legacy": "7.2.1",
@@ -54,8 +54,8 @@ export const AppContainer = ({
square
elevation={0}
className={styles.layout}
onPointerDown={(e: any) => {
if (e.target?.dataset?.windrag) {
onPointerDown={(e) => {
if ((e.target as HTMLElement)?.dataset?.windrag) {
appWindow.startDragging()
}
}}
@@ -5,7 +5,7 @@ import { SvgIconComponent } from '@mui/icons-material'
import { Box, ListItemButton, ListItemIcon, Tooltip } from '@mui/material'
import { useSetting } from '@nyanpasu/interface'
import { alpha, cn } from '@nyanpasu/ui'
import { useLocation, useMatch, useNavigate } from '@tanstack/react-router'
import { useLocation, useNavigate } from '@tanstack/react-router'
export const RouteListItem = ({
name,
@@ -18,6 +18,8 @@ const NoticeInner = (props: InnerProps) => {
setVisible(false)
onClose()
}
// oxlint-disable-next-line typescript/no-explicit-any
const onAutoClose = (_e: any, reason: string) => {
if (reason !== 'clickaway') onBtnClose()
}
@@ -38,7 +38,7 @@ const MAX_WIDTH = 'calc(100% - 48px - 16px)'
export const IPASNPanel = ({ refreshCount }: { refreshCount: number }) => {
const { t } = useTranslation()
const { data, mutate, isValidating, isLoading } = useIPSB()
const { data, mutate, isValidating } = useIPSB()
const handleRefreshIP = () => {
mutate()
@@ -75,10 +75,13 @@ export const ProxyShortcuts = () => {
try {
await systemProxy.upsert(!systemProxy.value)
} catch (error) {
message(`Activation System Proxy failed!`, {
title: t('Error'),
kind: 'error',
})
message(
`Activation System Proxy failed!\n Error: ${formatError(error)}`,
{
title: t('Error'),
kind: 'error',
},
)
}
})
@@ -78,7 +78,7 @@ export const LayoutControl = ({ className }: { className?: string }) => {
<CtrlButton
onClick={() => {
appWindow.toggleMaximize().then((isMaximized) => {
appWindow.toggleMaximize().then(() => {
queryClient.invalidateQueries({ queryKey: ['isMaximized'] })
})
}}
@@ -1,3 +1,4 @@
// oxlint-disable no-unsafe-optional-chaining
import { useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { notification, NotificationType } from '@/utils/notification'
@@ -63,7 +63,7 @@ const ThemeInner = ({ children }: PropsWithChildren) => {
const setThemeMode = useSetAtom(themeModeAtom)
const { mode, setMode } = useColorScheme()
const { setMode } = useColorScheme()
useEffect(() => {
if (themeMode === 'system') {
@@ -23,8 +23,6 @@ import { formatError } from '@/utils'
import { message } from '@/utils/notification'
import { Divider, InputAdornment } from '@mui/material'
import {
LocalProfile,
ProfileBuilder,
ProfileQueryResultItem,
ProfileTemplate,
RemoteProfile,
@@ -254,10 +254,15 @@ export const ProfileItem = memo(function ProfileItem({
className="flex h-6 w-30 items-center"
nodes={[
!!item.updated && (
<TimeSpan ts={item.updated!} k="Subscription Updated At" />
<TimeSpan
key="updated"
ts={item.updated!}
k="Subscription Updated At"
/>
),
!!(item as RemoteProfile).extra?.expire && (
<TimeSpan
key="expire"
ts={(item as RemoteProfile).extra!.expire!}
k="Subscription Expires In"
/>
@@ -62,7 +62,7 @@ export const beforeEditorMount = (monaco: Monaco) => {
// Register link provider for all supported languages
const registerLinkProvider = (language: string) => {
monaco.languages.registerLinkProvider(language, {
provideLinks: (model, token) => {
provideLinks: (model) => {
const links = []
// More robust URL regex pattern
const urlRegex = /\b(?:https?:\/\/|www\.)[^\s<>"']*[^<>\s"',.!?]/gi
@@ -131,7 +131,7 @@ export default function ProfileMonacoViewer({
)
const handleEditorDidMount = useCallback(
(editor: editor.IStandaloneCodeEditor, monaco: Monaco) => {
(editor: editor.IStandaloneCodeEditor) => {
editorRef.current = editor
// Enable URL detection and handling
@@ -1,4 +1,3 @@
import { isEqual } from 'lodash-es'
import type { Profile, ProfileBuilder } from '@nyanpasu/interface'
/**
@@ -27,7 +27,7 @@ export const UpdateProviders = () => {
setLoading(true)
await Promise.all(
Object.entries(rulesProvider.data).map(([name, provider]) => {
Object.values(rulesProvider.data).map((provider) => {
return provider.mutate()
}),
)
@@ -164,7 +164,7 @@ export const NodeList = forwardRef(function NodeList(
)
return disableMotion ? (
<div className="relative overflow-hidden">
<div key={render.name} className="relative overflow-hidden">
<Card />
</div>
) : (
@@ -8,7 +8,7 @@ import { serviceManualPromptDialogAtom } from '@/store/service'
import { notification } from '@/utils/notification'
import { getShikiSingleton } from '@/utils/shiki'
import ContentPasteIcon from '@mui/icons-material/ContentPaste'
import { IconButton, Tooltip, useTheme } from '@mui/material'
import { IconButton, Tooltip } from '@mui/material'
import { useColorScheme } from '@mui/material/styles'
import { getCoreDir, getServiceInstallPrompt } from '@nyanpasu/interface'
import { BaseDialog, BaseDialogProps, cn } from '@nyanpasu/ui'
@@ -91,10 +91,8 @@ export const SettingClashField = () => {
const mergeFields = useMemo(
() => [
...[
...Object.keys(CLASH_FIELD.default),
...Object.keys(CLASH_FIELD.handle),
],
...Object.keys(CLASH_FIELD.default),
...Object.keys(CLASH_FIELD.handle),
...(query.data?.valid ?? []),
],
[query.data],
@@ -1,3 +1,4 @@
// oxlint-disable typescript/no-explicit-any
import { useTranslation } from 'react-i18next'
import { useSetting } from '@nyanpasu/interface'
import { SwitchItem } from '@nyanpasu/ui'
@@ -50,10 +50,13 @@ const SystemProxyButton = () => {
try {
await systemProxy.upsert(!systemProxy.value)
} catch (error) {
message(`Activation System Proxy failed!`, {
title: t('Error'),
kind: 'error',
})
message(
`Activation System Proxy failed!\n Error: ${formatError(error)}`,
{
title: t('Error'),
kind: 'error',
},
)
}
})
@@ -75,7 +78,7 @@ const ProxyGuardSwitch = () => {
try {
await proxyGuard.upsert(!proxyGuard.value)
} catch (error) {
message(`Activation Proxy Guard failed!`, {
message(`Activation Proxy Guard failed!\n Error: ${formatError(error)}`, {
title: t('Error'),
kind: 'error',
})
@@ -1,3 +1,4 @@
import ArrowRight from '~icons/material-symbols/arrow-right-rounded'
import Check from '~icons/material-symbols/check-rounded'
import RadioChecked from '~icons/material-symbols/radio-button-checked'
import Radio from '~icons/material-symbols/radio-button-unchecked'
@@ -7,6 +8,50 @@ import { cn } from '@nyanpasu/ui'
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
import { useControllableState } from '@radix-ui/react-use-controllable-state'
const MotionContent = ({
children,
className,
...props
}: ComponentProps<typeof motion.div>) => {
return (
<motion.div
className={cn(
'relative z-50 w-full overflow-auto rounded',
'dark:text-on-surface',
'bg-inverse-on-surface dark:bg-surface',
'shadow shadow-zinc-300 dark:shadow-zinc-900',
className,
)}
style={{
maxHeight: 'var(--radix-popper-available-height)',
}}
initial={{
opacity: 0,
scaleY: 0.9,
transformOrigin: 'top',
}}
animate={{
opacity: 1,
scaleY: 1,
transformOrigin: 'top',
}}
exit={{
opacity: 0,
scaleY: 0.9,
transformOrigin: 'top',
}}
transition={{
type: 'spring',
bounce: 0,
duration: 0.35,
}}
{...props}
>
{children}
</motion.div>
)
}
const DropdownMenuContext = createContext<{
open: boolean
} | null>(null)
@@ -52,7 +97,92 @@ export const DropdownMenuGroup = DropdownMenuPrimitive.Group
export const DropdownMenuPortal = DropdownMenuPrimitive.Portal
export const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuSubContext = createContext<{
open: boolean
} | null>(null)
const useDropdownMenuSubContext = () => {
const context = useContext(DropdownMenuSubContext)
if (context === null) {
throw new Error(
'DropdownMenuSub compound components cannot be rendered outside the DropdownMenuSub component',
)
}
return context
}
export const DropdownMenuSub = ({
open: inputOpen,
defaultOpen,
onOpenChange,
children,
...props
}: ComponentProps<typeof DropdownMenuPrimitive.Sub>) => {
const [open, setOpen] = useControllableState({
prop: inputOpen,
defaultProp: defaultOpen ?? false,
onChange: onOpenChange,
})
return (
<DropdownMenuSubContext.Provider
value={{
open,
}}
>
<DropdownMenuPrimitive.Sub {...props} open={open} onOpenChange={setOpen}>
{children}
</DropdownMenuPrimitive.Sub>
</DropdownMenuSubContext.Provider>
)
}
export function DropdownMenuSubTrigger({
children,
className,
...props
}: ComponentProps<typeof DropdownMenuPrimitive.SubTrigger>) {
return (
<DropdownMenuPrimitive.SubTrigger
className={cn(
'flex h-12 cursor-default items-center justify-between gap-2 p-4 pr-2 outline-hidden',
'cursor-pointer',
'hover:bg-surface-variant',
'dark:hover:bg-surface-variant',
'data-[state=open]:bg-surface-variant/30',
'dark:data-[state=open]:bg-surface-variant/30',
className,
)}
{...props}
>
{children}
<ArrowRight className="text-outline-variant dark:text-outline size-6" />
</DropdownMenuPrimitive.SubTrigger>
)
}
export function DropdownMenuSubContent({
children,
className,
...props
}: ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
const { open } = useDropdownMenuSubContext()
return (
<AnimatePresence initial={false}>
{open && (
<DropdownMenuPortal forceMount>
<DropdownMenuPrimitive.SubContent {...props} asChild>
<MotionContent className={className}>{children}</MotionContent>
</DropdownMenuPrimitive.SubContent>
</DropdownMenuPortal>
)}
</AnimatePresence>
)
}
const DropdownMenuRadioGroupContext = createContext<{
value: string | null
@@ -93,10 +223,6 @@ export const DropdownMenuRadioGroup = ({
)
}
export const DropdownMenuSubTrigger = DropdownMenuPrimitive.SubTrigger
export const DropdownMenuSubContent = DropdownMenuPrimitive.SubContent
export const DropdownMenuContent = ({
children,
className,
@@ -109,39 +235,7 @@ export const DropdownMenuContent = ({
{open && (
<DropdownMenuPrimitive.Portal forceMount>
<DropdownMenuPrimitive.Content {...props} asChild>
<motion.div
className={cn(
'shadow-container relative z-50 w-full overflow-auto rounded',
'dark:text-on-surface',
'bg-inverse-on-surface dark:bg-surface',
className,
)}
style={{
maxHeight: 'var(--radix-popper-available-height)',
}}
initial={{
opacity: 0,
scaleY: 0.9,
transformOrigin: 'top',
}}
animate={{
opacity: 1,
scaleY: 1,
transformOrigin: 'top',
}}
exit={{
opacity: 0,
scaleY: 0.9,
transformOrigin: 'top',
}}
transition={{
type: 'spring',
bounce: 0,
duration: 0.35,
}}
>
{children}
</motion.div>
<MotionContent className={className}>{children}</MotionContent>
</DropdownMenuPrimitive.Content>
</DropdownMenuPrimitive.Portal>
)}
@@ -4,7 +4,6 @@ import {
ComponentProps,
createContext,
DragEvent,
ReactNode,
RefObject,
useContext,
useEffect,
@@ -103,7 +102,6 @@ export interface FileDropZoneProps
onFileRead?: (content: string) => void
accept: string[]
disabled?: boolean
fileSelected?: (fileName: string) => ReactNode
}
export function FileDropZone({
@@ -114,7 +112,6 @@ export function FileDropZone({
className,
disabled = false,
variant,
fileSelected,
children,
...props
}: FileDropZoneProps) {
@@ -10,7 +10,6 @@ import {
useState,
} from 'react'
import { cn } from '@nyanpasu/ui'
import { Slot } from '@radix-ui/react-slot'
export const inputContainerVariants = cva(
[
@@ -402,6 +401,7 @@ export const InputLabel = ({
variant,
focus: haveValue,
}),
className,
)}
{...props}
/>
@@ -420,7 +420,6 @@ export type NumericInputProps = Omit<
label?: string
min?: number
max?: number
step?: number
decimalScale?: number
allowNegative?: boolean
} & InputContainerVariants
@@ -434,7 +433,6 @@ export const NumericInput = ({
defaultValue,
min,
max,
step = 1,
decimalScale,
allowNegative = true,
...props
@@ -1,9 +1,4 @@
import {
ComponentProps,
createContext,
PropsWithChildren,
useContext,
} from 'react'
import { ComponentProps, createContext, useContext } from 'react'
import useIsMobile from '@/hooks/use-is-moblie'
import { cn } from '@nyanpasu/ui'
import { AppContentScrollArea } from './scroll-area'
@@ -0,0 +1,212 @@
import { motion, useAnimationControls } from 'framer-motion'
import { useCallback, useEffect, useRef, useState } from 'react'
import { sleep } from '@/utils'
import { cn } from '@nyanpasu/ui'
export default function TextMarquee({
children,
className,
speed = 30,
gap = 32,
pauseDuration = 1,
// pauseOnHover = true,
}: {
children: React.ReactNode
className?: string
speed?: number
gap?: number
pauseDuration?: number
// pauseOnHover?: boolean
}) {
const containerRef = useRef<HTMLDivElement>(null)
const textRef = useRef<HTMLDivElement>(null)
const [shouldAnimate, setShouldAnimate] = useState(false)
const [textWidth, setTextWidth] = useState(0)
const controls = useAnimationControls()
const isHoveredRef = useRef(false)
// Check if text overflows container
const checkOverflow = useCallback(() => {
if (!containerRef.current || !textRef.current) {
return
}
const container = containerRef.current
const text = textRef.current
const containerW = container.offsetWidth
const textW = text.scrollWidth
setTextWidth(textW)
setShouldAnimate(textW > containerW)
}, [])
// Observe container size changes
useEffect(() => {
checkOverflow()
const resizeObserver = new ResizeObserver(() => {
checkOverflow()
})
if (containerRef.current) {
resizeObserver.observe(containerRef.current)
}
return () => {
resizeObserver.disconnect()
}
}, [checkOverflow, children])
// Animate when shouldAnimate changes
useEffect(() => {
if (!shouldAnimate) {
controls.set({ x: 0 })
return
}
const totalDistance = textWidth + gap
const animDuration = totalDistance / speed
const cancelledRef = { current: false }
const runAnimationLoop = async () => {
// Wait at start position
await sleep(pauseDuration * 1000)
if (cancelledRef.current) {
return
}
// Check if hovered, wait and retry
if (isHoveredRef.current) {
await sleep(100)
if (!cancelledRef.current) {
runAnimationLoop()
}
return
}
// Animate to end
await controls.start({
x: -totalDistance,
transition: {
duration: animDuration,
ease: 'linear',
},
})
if (cancelledRef.current) {
return
}
// Reset to start position instantly and loop
controls.set({ x: 0 })
if (!cancelledRef.current) {
runAnimationLoop()
}
}
runAnimationLoop()
return () => {
cancelledRef.current = true
controls.stop()
}
}, [shouldAnimate, textWidth, gap, speed, pauseDuration, controls])
// const handleMouseEnter = () => {
// if (!pauseOnHover) {
// return
// }
// isHoveredRef.current = true
// controls.stop()
// }
// const handleMouseLeave = () => {
// if (!pauseOnHover || !shouldAnimate) {
// return
// }
// isHoveredRef.current = false
// resumeAnimation()
// }
// const resumeAnimation = () => {
// const totalDistance = textWidth + gap
// // Resume animation
// const marqueeContent = containerRef.current?.querySelector<HTMLDivElement>(
// '[data-marquee-content]',
// )
// if (marqueeContent) {
// const transform = window.getComputedStyle(marqueeContent).transform
// const matrix = new DOMMatrix(transform)
// const currentPosition = matrix.m41
// const remainingDistance = -totalDistance - currentPosition
// const remainingDuration = Math.abs(remainingDistance) / speed
// controls.start({
// x: -totalDistance,
// transition: {
// duration: remainingDuration,
// ease: 'linear',
// },
// })
// }
// }
return (
<div
ref={containerRef}
className={cn('overflow-hidden', className)}
data-slot="text-marquee"
>
{shouldAnimate ? (
<motion.div
className="flex whitespace-nowrap"
animate={controls}
data-slot="text-marquee-content"
>
<span
ref={textRef}
data-slot="text-marquee-content-item"
data-index="0"
>
{children}
</span>
<span
style={{
paddingLeft: gap,
}}
data-slot="text-marquee-content-item"
data-index="1"
>
{children}
</span>
</motion.div>
) : (
<div
ref={textRef}
className="truncate"
data-slot="text-marquee-content"
>
{children}
</div>
)}
</div>
)
}
@@ -3,7 +3,7 @@ import { isAppImage } from '@nyanpasu/interface'
export const useIsAppImage = (config?: Partial<SWRConfiguration>) => {
return useSWR<boolean>('/api/is_appimage', isAppImage, {
...(config || {}),
...config,
revalidateOnFocus: false,
revalidateOnReconnect: false,
refreshInterval: 0,
@@ -0,0 +1,22 @@
import ClashRs from '@/assets/image/core/clash-rs.png'
import ClashMeta from '@/assets/image/core/clash.meta.png'
import Clash from '@/assets/image/core/clash.png'
import { useSetting } from '@nyanpasu/interface'
export default function useCurrentCoreIcon() {
const { value: currentCore } = useSetting('clash_core')
switch (currentCore) {
case 'clash':
return Clash
case 'clash-rs':
case 'clash-rs-alpha':
return ClashRs
case 'mihomo':
case 'mihomo-alpha':
return ClashMeta
// sync from backend
default:
return ClashMeta
}
}
@@ -57,7 +57,7 @@ export const beforeEditorMount = (monaco: Monaco) => {
// Register link provider for all supported languages
const registerLinkProvider = (language: string) => {
monaco.languages.registerLinkProvider(language, {
provideLinks: (model, token) => {
provideLinks: (model) => {
const links = []
// More robust URL regex pattern
const urlRegex = /\b(?:https?:\/\/|www\.)[^\s<>"']*[^<>\s"',.!?]/gi
@@ -7,7 +7,7 @@ import { useExperimentalThemeContext } from '@/components/providers/theme-provid
import { Button } from '@/components/ui/button'
import { useLockFn } from '@/hooks/use-lock-fn'
import { m } from '@/paraglide/messages'
import MonacoEditor, { Monaco } from '@monaco-editor/react'
import MonacoEditor from '@monaco-editor/react'
import { openThat, useProfileContent } from '@nyanpasu/interface'
import { cn } from '@nyanpasu/ui'
import { createFileRoute } from '@tanstack/react-router'
@@ -112,7 +112,7 @@ function RouteComponent() {
})
const handleEditorDidMount = useCallback(
(editor: editor.IStandaloneCodeEditor, monaco: Monaco) => {
(editor: editor.IStandaloneCodeEditor) => {
editorRef.current = editor
// Enable URL detection and handling
@@ -33,7 +33,7 @@ function Connections() {
const [mountTable, setMountTable] = useState(true)
const deferredMountTable = useDeferredValue(mountTable)
const { proceed } = useBlocker({
shouldBlockFn: (args) => {
shouldBlockFn: () => {
setMountTable(false)
return !mountTable
},
@@ -7,7 +7,7 @@ import TrayIconDialog from '@/components/setting/modules/tray-icon-dialog'
import { formatEnvInfos } from '@/utils'
import { Feedback, GitHub, Keyboard } from '@mui/icons-material'
import { IconButton } from '@mui/material'
import { collectEnvs, openThat } from '@nyanpasu/interface'
import { commands, openThat } from '@nyanpasu/interface'
import { BasePage } from '@nyanpasu/ui'
import { createFileRoute } from '@tanstack/react-router'
@@ -38,9 +38,14 @@ function SettingPage() {
const FeedbackIcon = () => {
const toFeedback = useLockFn(async () => {
const envs = await collectEnvs()
const envs = await commands.collectEnvs()
if (envs.status !== 'ok') {
return
}
const formattedEnv = encodeURIComponent(
formatEnvInfos(envs)
formatEnvInfos(envs.data)
.split('\n')
.map((v) => `> ${v}`)
.join('\n'),
@@ -0,0 +1,35 @@
import { PropsWithChildren } from 'react'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { m } from '@/paraglide/messages'
import { Link } from '@tanstack/react-router'
import { ProfileType } from '../main/profiles/_modules/consts'
import { Action } from '../main/profiles/$type/index'
export default function HeaderFileAction({ children }: PropsWithChildren) {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem asChild>
<Link
to="/main/profiles/$type"
params={{
type: ProfileType.Profile,
}}
search={{
action: Action.ImportLocalProfile,
}}
>
{m.header_file_action_import_local_profile()}
</Link>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
@@ -0,0 +1,80 @@
import { PropsWithChildren } from 'react'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { useLockFn } from '@/hooks/use-lock-fn'
import { m } from '@/paraglide/messages'
import { formatEnvInfos } from '@/utils'
import { commands } from '@nyanpasu/interface'
import { Link } from '@tanstack/react-router'
const WikiItem = () => {
const handleClick = useLockFn(async () => {
await commands.openThat('https://nyanpasu.elaina.moe')
})
return (
<DropdownMenuItem onClick={handleClick}>
{m.header_help_action_wiki()}
</DropdownMenuItem>
)
}
const IssuesItem = () => {
const handleClick = useLockFn(async () => {
const envs = await commands.collectEnvs()
if (envs.status !== 'ok') {
return
}
const formattedEnv = encodeURIComponent(
formatEnvInfos(envs.data)
.split('\n')
.map((v) => `> ${v}`)
.join('\n'),
)
const params = new URLSearchParams({
assignees: '',
labels: 'T%3A+Bug%2CS%3A+Untriaged',
projects: '',
template: 'bug_report.yaml',
})
return commands.openThat(
'https://github.com/libnyanpasu/clash-nyanpasu/issues/new?' +
params.toString() +
// envs can't be serialized
'&env_infos=' +
formattedEnv,
)
})
return (
<DropdownMenuItem onClick={handleClick}>
{m.header_help_action_issues()}
</DropdownMenuItem>
)
}
export default function HeaderHelpAction({ children }: PropsWithChildren) {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
<DropdownMenuContent>
<WikiItem />
<IssuesItem />
<DropdownMenuItem asChild>
<Link to="/main/settings/about">{m.header_help_action_about()}</Link>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
@@ -1,12 +1,17 @@
import { ComponentProps } from 'react'
import { Button, ButtonProps } from '@/components/ui/button'
import { m } from '@/paraglide/messages'
import { cn } from '@nyanpasu/ui'
import HeaderFileAction from './header-file-action'
import HeaderHelpAction from './header-help-action'
import HeaderSettingsAction from './header-settings-action'
const MenuButton = ({ className, ...props }: ButtonProps) => {
return (
<Button
className={cn(
'hover:bg-primary-container dark:hover:bg-on-primary h-8 min-w-0 px-3',
'data-[state=open]:bg-primary-container dark:data-[state=open]:bg-on-primary',
className,
)}
{...props}
@@ -25,10 +30,17 @@ export default function HeaderMenu({
{...props}
data-tauri-drag-region
>
<MenuButton>Files</MenuButton>
<MenuButton>Actions</MenuButton>
<MenuButton>Settings</MenuButton>
<MenuButton>Help</MenuButton>
<HeaderFileAction>
<MenuButton>{m.header_file_action_title()}</MenuButton>
</HeaderFileAction>
<HeaderSettingsAction>
<MenuButton>{m.header_settings_action_title()}</MenuButton>
</HeaderSettingsAction>
<HeaderHelpAction>
<MenuButton>{m.header_help_action_title()}</MenuButton>
</HeaderHelpAction>
</div>
)
}
@@ -0,0 +1,96 @@
import { PropsWithChildren } from 'react'
import { useLanguage } from '@/components/providers/language-provider'
import {
ThemeMode,
useExperimentalThemeContext,
} from '@/components/providers/theme-provider'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { m } from '@/paraglide/messages'
import { Locale, locales } from '@/paraglide/runtime'
const LanguageSelector = () => {
const { language, setLanguage } = useLanguage()
const handleLanguageChange = (value: string) => {
setLanguage(value as Locale)
}
return (
<DropdownMenuSub>
<DropdownMenuSubTrigger>
{m.header_settings_action_language()}
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuRadioGroup
value={language}
onValueChange={handleLanguageChange}
>
{Object.entries(locales).map(([key, value]) => (
<DropdownMenuRadioItem key={key} value={value}>
{m.language(key, { locale: value })}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuSubContent>
</DropdownMenuSub>
)
}
const ThemeModeSelector = () => {
const { themeMode, setThemeMode } = useExperimentalThemeContext()
const handleThemeModeChange = (value: string) => {
setThemeMode(value as ThemeMode)
}
const messages = {
[ThemeMode.LIGHT]: m.settings_user_interface_theme_mode_light(),
[ThemeMode.DARK]: m.settings_user_interface_theme_mode_dark(),
[ThemeMode.SYSTEM]: m.settings_user_interface_theme_mode_system(),
} satisfies Record<ThemeMode, string>
return (
<DropdownMenuSub>
<DropdownMenuSubTrigger>
{m.header_settings_action_theme_mode()}
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuRadioGroup
value={themeMode}
onValueChange={handleThemeModeChange}
>
{Object.entries(messages).map(([key, value]) => (
<DropdownMenuRadioItem key={key} value={key}>
{value}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuSubContent>
</DropdownMenuSub>
)
}
export default function HeaderSettingsAction({ children }: PropsWithChildren) {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
<DropdownMenuContent>
<LanguageSelector />
<ThemeModeSelector />
</DropdownMenuContent>
</DropdownMenu>
)
}
@@ -103,6 +103,7 @@ const ProxiesGroupButton = () => {
return (
<NavbarButton
to="/main/proxies/group/$name"
mobileTo="/main/proxies"
params={{ name: fristGroup } as never}
icon={<Public className="size-5" />}
label={m.navbar_label_proxies()}
@@ -111,8 +112,6 @@ const ProxiesGroupButton = () => {
}
export default function Navbar({ className, ...props }: ComponentProps<'div'>) {
const isMobile = useIsMobile()
return (
<div
className={cn(
@@ -159,7 +158,8 @@ export default function Navbar({ className, ...props }: ComponentProps<'div'>) {
/>
<NavbarButton
to={isMobile ? '/main/settings' : '/main/settings/system-proxy'}
to="/main/settings/system"
mobileTo="/main/settings"
icon={<SettingsRounded className="size-5" />}
label={m.navbar_label_settings()}
/>
@@ -13,7 +13,7 @@ import {
import { m } from '@/paraglide/messages'
import { cn } from '@nyanpasu/ui'
import { ProfileType } from '../../_modules/consts'
import { Route as IndexRoute } from '../index'
import { Action, Route as IndexRoute } from '../index'
import ChainProfileImport from './chain-profile-import'
import LocalProfileButton from './local-profile-button'
import RemoteProfileButton from './remote-profile-button'
@@ -52,8 +52,17 @@ const SelectButton = ({
const ProxyProfileImport = () => {
const { isScrolling } = useScrollArea()
const { action } = IndexRoute.useSearch()
const [open, setOpen] = useState(false)
useEffect(() => {
// for animation duration to open the modal
if (action === Action.ImportLocalProfile) {
setOpen(true)
}
}, [action])
const handleToggle = () => {
setOpen(!open)
}
@@ -2,7 +2,7 @@ import UploadFileRounded from '~icons/material-symbols/upload-file-rounded'
import dayjs from 'dayjs'
import { filesize } from 'filesize'
import { AnimatePresence } from 'framer-motion'
import { PropsWithChildren, useState } from 'react'
import { PropsWithChildren, useEffect, useState } from 'react'
import { Controller, useForm } from 'react-hook-form'
import z from 'zod'
import { useBlockTask } from '@/components/providers/block-task-provider'
@@ -29,7 +29,9 @@ import { formatError } from '@/utils'
import { message } from '@/utils/notification'
import { zodResolver } from '@hookform/resolvers/zod'
import { LocalProfileBuilder, useProfile } from '@nyanpasu/interface'
import { useLocation } from '@tanstack/react-router'
import AnimatedErrorItem from '../../_modules/error-item'
import { Action, Route as IndexRoute } from '../index'
const formSchema = z.object({
uid: z.string().nullable(),
@@ -56,10 +58,35 @@ const getDefaultValues = () => {
}
export default function LocalProfileButton({ children }: PropsWithChildren) {
const { action } = IndexRoute.useSearch()
const navigate = IndexRoute.useNavigate()
const { pathname } = useLocation()
const { create } = useProfile()
const [open, setOpen] = useState(false)
useEffect(() => {
if (action === Action.ImportLocalProfile) {
// if the current path is the index page, open the modal immediately
if (pathname === '/main/profiles/$type') {
setOpen(true)
return
}
// else, wait animation duration to open the modal
const timeout = setTimeout(() => {
setOpen(true)
}, 150)
return () => {
clearTimeout(timeout)
}
}
}, [action])
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: getDefaultValues(),
@@ -97,6 +124,12 @@ export default function LocalProfileButton({ children }: PropsWithChildren) {
setOpen(value)
navigate({
search: {
action: null,
},
})
if (value) {
form.reset(getDefaultValues())
}
@@ -1,10 +1,18 @@
import z from 'zod'
import { createFileRoute } from '@tanstack/react-router'
import ImportButton from './_modules/import-button'
import ProfilesHeader from './_modules/profiles-header'
import ProfilesList from './_modules/profiles-list'
export enum Action {
ImportLocalProfile,
}
export const Route = createFileRoute('/(main)/main/profiles/$type/')({
component: RouteComponent,
validateSearch: z.object({
action: z.enum(Action).optional().nullable(),
}),
})
function RouteComponent() {
@@ -1,142 +1,206 @@
import ComputerOutlineRounded from '~icons/material-symbols/computer-outline-rounded'
import DisplayExternalInput from '~icons/material-symbols/display-external-input-rounded'
import FrameBugOutlineRounded from '~icons/material-symbols/frame-bug-outline-rounded'
import ListsRounded from '~icons/material-symbols/lists-rounded'
import NetworkNode from '~icons/material-symbols/network-node'
import SettingsBoltRounded from '~icons/material-symbols/settings-b-roll-rounded'
import SettingsEthernet from '~icons/material-symbols/settings-ethernet-rounded'
import ShareWindows from '~icons/material-symbols/share-windows-rounded'
import SettingsRounded from '~icons/material-symbols/settings-rounded'
import ViewQuilt from '~icons/material-symbols/view-quilt-rounded'
import ClashMeta from '@/assets/image/core/clash.meta.png'
import { ComponentProps, ReactNode } from 'react'
import LogoSvg from '@/assets/image/logo.svg?react'
import { Button } from '@/components/ui/button'
import TextMarquee from '@/components/ui/text-marquee'
import useCurrentCoreIcon from '@/hooks/use-current-core-icon'
import { m } from '@/paraglide/messages'
import { cn } from '@nyanpasu/ui'
import { Link, useLocation } from '@tanstack/react-router'
const StyleClashMeta = () => {
return (
<img src={ClashMeta} alt="Clash Meta" className="size-8 grayscale-50" />
)
}
const StyleLogo = () => {
const NyanpasuLogo = () => {
return (
<LogoSvg className="[&_#element]:fill-primary [&_#bg]:fill-surface size-8" />
)
}
const ROUTES = [
{
label: 'System Proxy',
description: 'Configure the system proxy',
href: '/main/settings/system-proxy',
icon: SettingsEthernet,
},
{
label: 'User Interface',
description: 'Configure the user interface',
href: '/main/settings/user-interface',
icon: ViewQuilt,
},
{
label: 'Clash Settings',
description: 'Configure the clash settings',
href: '/main/settings/clash-settings',
icon: SettingsBoltRounded,
},
{
label: 'Clash External Controll',
description: 'Configure the clash external controll',
href: '/main/settings/clash-external-controll',
icon: DisplayExternalInput,
},
{
label: 'Web UI',
description: 'Configure the web ui',
href: '/main/settings/web-ui',
icon: ShareWindows,
},
{
label: 'Clash Core',
description: 'Configure the clash core',
href: '/main/settings/clash-core',
icon: StyleClashMeta,
},
{
label: 'Clash Filed',
description: 'Configure the clash filed',
href: '/main/settings/clash-filed',
icon: ListsRounded,
},
{
label: 'System Behavior',
description: 'Configure the system behavior',
href: '/main/settings/system-behavior',
icon: ComputerOutlineRounded,
},
{
label: 'System Service',
description: 'Configure the system service',
href: '/main/settings/system-service',
icon: NetworkNode,
},
{
label: 'Nyanpasu Config',
description: 'Configure the nyanpasu config',
href: '/main/settings/nyanpasu-config',
icon: StyleLogo,
},
{
label: 'Debug Utils',
description: 'Configure the debug utils',
href: '/main/settings/debug-utils',
icon: FrameBugOutlineRounded,
},
{
label: 'About',
description: 'About the nyanpasu',
href: '/main/settings/about',
icon: StyleLogo,
},
] as const
export default function SettingsNavigate() {
const location = useLocation()
const CurrentCoreIcon = ({
className,
...props
}: Omit<ComponentProps<'img'>, 'src'>) => {
const currentCoreIconUrl = useCurrentCoreIcon()
return (
<div className="flex flex-col gap-2 p-2">
{ROUTES.map((route) => (
<Button
key={route.href}
variant="fab"
data-active={String(location.pathname === route.href)}
asChild
>
<Link
className={cn(
'h-16',
'flex items-center gap-2',
'data-[active=true]:bg-surface-variant/80',
'data-[active=false]:bg-transparent',
'data-[active=false]:shadow-none',
'data-[active=false]:hover:shadow-none',
'data-[active=false]:hover:bg-surface-variant/30',
)}
to={route.href}
>
<div className="flex items-center gap-2.5">
<div className="size-8">
<route.icon className="size-8" />
</div>
<img
src={currentCoreIconUrl}
className={cn('size-full', className)}
{...props}
/>
)
}
<div className="flex flex-col gap-1">
<div className="text-sm font-medium">{route.label}</div>
<div className="text-xs text-zinc-500">{route.description}</div>
</div>
</div>
</Link>
</Button>
))}
const NavigateButton = ({
icon,
label,
description,
className,
...props
}: ComponentProps<typeof Link> & {
icon: ReactNode
label: string
description: string
}) => {
const location = useLocation()
const isActive = location.pathname === props.to
return (
<Button
variant="fab"
data-active={String(isActive)}
className={cn(
'h-16',
'flex items-center gap-2',
'data-[active=true]:bg-surface-variant/80',
'data-[active=false]:bg-transparent',
'data-[active=false]:shadow-none',
'data-[active=false]:hover:shadow-none',
'data-[active=false]:hover:bg-surface-variant/30',
className,
)}
asChild
>
<Link {...props}>
<div className="flex max-w-full items-center gap-3">
<div className="size-8">{icon}</div>
<div className="flex min-w-0 flex-1 flex-col gap-1">
<div className="text-sm font-medium">{label}</div>
<TextMarquee className="text-xs text-zinc-500">
{description}
</TextMarquee>
</div>
</div>
</Link>
</Button>
)
}
const SystemButton = () => {
return (
<NavigateButton
icon={<SettingsEthernet className="size-8" />}
label={m.settings_label_system()}
description={m.settings_label_system_description()}
to="/main/settings/system"
/>
)
}
const UserInterfaceButton = () => {
return (
<NavigateButton
icon={<ViewQuilt className="size-8" />}
label={m.settings_label_user_interface()}
description={m.settings_label_user_interface_description()}
to="/main/settings/user-interface"
/>
)
}
const ClashButton = () => {
return (
<NavigateButton
icon={<SettingsBoltRounded className="size-8" />}
label={m.settings_label_clash_settings()}
description={m.settings_label_clash_settings_description()}
to="/main/settings/clash"
/>
)
}
const ExternalControllButton = () => {
return (
<NavigateButton
icon={
<div className="relative size-8">
<CurrentCoreIcon className="size-7.5" />
<div
className={cn(
'absolute -right-1 -bottom-1 size-4 p-0.5',
'text-primary bg-surface-variant rounded-full shadow-sm',
)}
>
<DisplayExternalInput className="size-3" />
</div>
</div>
}
label={m.settings_label_external_controll()}
description={m.settings_label_external_controll_description()}
to="/main/settings/web-ui"
/>
)
}
const NyanpasuButton = () => {
return (
<NavigateButton
icon={
<div className="relative size-8">
<NyanpasuLogo />
<div
className={cn(
'absolute -right-1 -bottom-1 size-4 p-0.5',
'text-primary bg-surface-variant rounded-full shadow-sm',
)}
>
<SettingsRounded className="text-primary size-3" />
</div>
</div>
}
label={m.settings_label_nyanpasu()}
description={m.settings_label_nyanpasu_description()}
to="/main/settings/nyanpasu"
/>
)
}
const DebugButton = () => {
return (
<NavigateButton
icon={<FrameBugOutlineRounded className="size-8" />}
label={m.settings_label_debug()}
description={m.settings_label_debug_description()}
to="/main/settings/debug"
/>
)
}
const AboutButton = () => {
return (
<NavigateButton
icon={<NyanpasuLogo />}
label={m.settings_label_about()}
description={m.settings_label_about_description()}
to="/main/settings/about"
/>
)
}
export default function SettingsNavigate() {
return (
<div className="flex flex-col gap-2 p-2">
<SystemButton />
<UserInterfaceButton />
<ClashButton />
<ExternalControllButton />
<NyanpasuButton />
<DebugButton />
<AboutButton />
</div>
)
}
@@ -1,9 +0,0 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/(main)/main/settings/clash-core')({
component: RouteComponent,
})
function RouteComponent() {
return <div>Hello "/(main)/main/settings/clash-core"!</div>
}
@@ -1,11 +0,0 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute(
'/(main)/main/settings/clash-external-controll',
)({
component: RouteComponent,
})
function RouteComponent() {
return <div>Hello "/(main)/main/settings/clash-external-controll"!</div>
}
@@ -1,9 +0,0 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/(main)/main/settings/clash-filed')({
component: RouteComponent,
})
function RouteComponent() {
return <div>Hello "/(main)/main/settings/clash-filed"!</div>
}
@@ -2,6 +2,7 @@ import { useMemo } from 'react'
import { Switch } from '@/components/ui/switch'
import { useLockFn } from '@/hooks/use-lock-fn'
import { m } from '@/paraglide/messages'
import { formatError } from '@/utils'
import { message } from '@/utils/notification'
import { useClashConfig } from '@nyanpasu/interface'
import { SettingsCard, SettingsCardContent } from '../../_modules/settings-card'
@@ -17,7 +18,7 @@ export default function AllowLanSwitch() {
'allow-lan': input,
})
} catch (error) {
message(`Activation Allow LAN failed!`, {
message(`Activation Allow LAN failed!\n Error: ${formatError(error)}`, {
title: 'Error',
kind: 'error',
})
@@ -2,6 +2,7 @@ import { useMemo } from 'react'
import { Switch } from '@/components/ui/switch'
import { useLockFn } from '@/hooks/use-lock-fn'
import { m } from '@/paraglide/messages'
import { formatError } from '@/utils'
import { message } from '@/utils/notification'
import { useClashConfig } from '@nyanpasu/interface'
import { SettingsCard, SettingsCardContent } from '../../_modules/settings-card'
@@ -17,7 +18,7 @@ export default function IPv6Switch() {
ipv6: input,
})
} catch (error) {
message(`Activation IPv6 failed!`, {
message(`Activation IPv6 failed!\n Error: ${formatError(error)}`, {
title: 'Error',
kind: 'error',
})
@@ -12,7 +12,7 @@ import MixedPortConfig from './_modules/mixed-port-config'
import RandomPortSwitch from './_modules/random-port-switch'
import TunStackSelector from './_modules/tun-stack-selector'
export const Route = createFileRoute('/(main)/main/settings/clash-settings')({
export const Route = createFileRoute('/(main)/main/settings/clash')({
component: RouteComponent,
})
@@ -1,7 +1,7 @@
import { createFileRoute } from '@tanstack/react-router'
import WindowDebug from './_modules/window-debug'
export const Route = createFileRoute('/(main)/main/settings/debug-utils')({
export const Route = createFileRoute('/(main)/main/settings/debug')({
component: RouteComponent,
})
@@ -1,6 +1,6 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/(main)/main/settings/nyanpasu-config')({
export const Route = createFileRoute('/(main)/main/settings/nyanpasu')({
component: RouteComponent,
})
@@ -13,7 +13,7 @@ function RouteComponent() {
return (
<Sidebar data-slot="settings-container">
<SidebarContent
className="bg-surface-variant/10"
className="bg-surface-variant/10 [&>div>div]:block!"
data-slot="settings-sidebar-scroll-area"
>
<SettingsNavigate />
@@ -1,9 +0,0 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/(main)/main/settings/system-behavior')({
component: RouteComponent,
})
function RouteComponent() {
return <div>Hello "/(main)/main/settings/system-behavior"!</div>
}
@@ -1,9 +0,0 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/(main)/main/settings/system-service')({
component: RouteComponent,
})
function RouteComponent() {
return <div>Hello "/(main)/main/settings/system-service"!</div>
}
@@ -40,10 +40,10 @@ export default function ProxyBypassConfig() {
const handleSubmit = form.handleSubmit(async (data) => {
try {
await systemProxyBypass.upsert(data.systemProxyBypass ?? DEFAULT_BYPASS)
await systemProxyBypass.upsert(data.systemProxyBypass || DEFAULT_BYPASS)
form.reset({
systemProxyBypass: data.systemProxyBypass ?? DEFAULT_BYPASS,
systemProxyBypass: data.systemProxyBypass || DEFAULT_BYPASS,
})
} catch (error) {
message(formatError(error), {
@@ -1,6 +1,7 @@
import { Switch } from '@/components/ui/switch'
import { useLockFn } from '@/hooks/use-lock-fn'
import { m } from '@/paraglide/messages'
import { formatError } from '@/utils'
import { message } from '@/utils/notification'
import { useSetting } from '@nyanpasu/interface'
import { SettingsCard, SettingsCardContent } from '../../_modules/settings-card'
@@ -12,7 +13,7 @@ export default function ProxyGuardSwitch() {
try {
await proxyGuard.upsert(!proxyGuard.value)
} catch (error) {
message(`Activation Proxy Guard failed!`, {
message(`Activation Proxy Guard failed!\n Error: ${formatError(error)}`, {
title: 'Error',
kind: 'error',
})
@@ -0,0 +1,306 @@
import { startCase } from 'lodash-es'
import { useEffect, useMemo, useState } from 'react'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card'
import {
Modal,
ModalClose,
ModalContent,
ModalTitle,
ModalTrigger,
} from '@/components/ui/modal'
import { OS } from '@/consts'
import { useLockFn } from '@/hooks/use-lock-fn'
import { m } from '@/paraglide/messages'
import { formatError } from '@/utils'
import { message } from '@/utils/notification'
import { getShikiSingleton } from '@/utils/shiki'
import {
commands,
useCoreDir,
useServicePrompt,
useSystemService,
} from '@nyanpasu/interface'
import { cn } from '@nyanpasu/ui'
import { writeText } from '@tauri-apps/plugin-clipboard-manager'
import { SettingsCard, SettingsCardContent } from '../../_modules/settings-card'
const SystemServiceCtrlItem = ({
name,
value,
}: {
name: string
value?: string
}) => {
return (
<div className="flex w-full leading-8" data-slot="system-service-ctrl-item">
<div
className="w-32 capitalize"
data-slot="system-service-ctrl-item-name"
>
{name}:
</div>
<div
className="text-warp flex-1 break-all"
data-slot="system-service-ctrl-item-value"
>
{value ?? '-'}
</div>
</div>
)
}
const ServiceDetailButton = () => {
const { query } = useSystemService()
return (
<Modal>
<ModalTrigger asChild>
<Button data-slot="system-service-detail-button">
{m.settings_system_proxy_system_service_ctrl_detail()}
</Button>
</ModalTrigger>
<ModalContent>
<Card className="w-96">
<CardHeader>
<ModalTitle>
{m.settings_system_proxy_system_service_ctrl_detail()}
</ModalTitle>
</CardHeader>
<CardContent>
<pre className="overflow-auto font-mono select-text">
{JSON.stringify(query.data, null, 2)}
</pre>
</CardContent>
<CardFooter>
<ModalClose>{m.common_close()}</ModalClose>
</CardFooter>
</Card>
</ModalContent>
</Modal>
)
}
const ServiceInstallButton = () => {
const { upsert } = useSystemService()
const handleInstallClick = useLockFn(async () => {
try {
await upsert.mutateAsync('install')
await commands.restartSidecar()
} catch (e) {
const errorMessage = `${m.settings_system_proxy_system_service_ctrl_failed_install()}: ${formatError(e)}`
message(errorMessage, {
kind: 'error',
})
// // If the installation fails, prompt the user to manually install the service
// promptDialog.show(
// query.data?.status === 'not_installed' ? 'install' : 'uninstall',
// )
}
})
return (
<Button
variant="flat"
onClick={handleInstallClick}
loading={upsert.isPending}
>
{m.settings_system_proxy_system_service_ctrl_install()}
</Button>
)
}
const ServiceUninstallButton = () => {
const { upsert } = useSystemService()
const handleUninstallClick = useLockFn(async () => {
await upsert.mutateAsync('uninstall')
})
return (
<Button onClick={handleUninstallClick} loading={upsert.isPending}>
{m.settings_system_proxy_system_service_ctrl_uninstall()}
</Button>
)
}
// {
// operation: 'uninstall' | 'install' | 'start' | 'stop' | null
// }
const ServicePromptButton = () => {
const {
query: { data: systemService },
} = useSystemService()
const { data: serviceInstallPrompt } = useServicePrompt()
const { data: coreDir } = useCoreDir()
const [codes, setCodes] = useState<string | null>(null)
const userOperationCommands = useMemo(() => {
if (systemService?.status === 'not_installed' && serviceInstallPrompt) {
return `cd "${coreDir}"\n${serviceInstallPrompt}`
} else if (systemService?.status) {
const operation = systemService?.status === 'running' ? 'stop' : 'start'
return `cd "${coreDir}"\n${OS !== 'windows' ? 'sudo ' : ''}./nyanpasu-service ${operation}`
}
return ''
}, [systemService?.status, serviceInstallPrompt, coreDir])
useEffect(() => {
const handleGenerateCodes = async () => {
const shiki = await getShikiSingleton()
const code = shiki.codeToHtml(userOperationCommands, {
lang: 'shell',
themes: {
dark: 'nord',
light: 'min-light',
},
})
setCodes(code)
}
handleGenerateCodes()
}, [userOperationCommands])
const handleCopyToClipboard = useLockFn(async () => {
if (!userOperationCommands) {
return
}
await writeText(userOperationCommands)
})
return (
<Modal>
<ModalTrigger asChild>
<Button variant="flat">
{m.settings_system_proxy_system_service_ctrl_prompt()}
</Button>
</ModalTrigger>
<ModalContent>
<Card className="max-w-3xl min-w-96">
<CardHeader>
<ModalTitle>
{m.settings_system_proxy_system_service_ctrl_manual_prompt()}
</ModalTitle>
</CardHeader>
<CardContent>
<p className="leading-6">
{m.settings_system_proxy_system_service_ctrl_manual_operation_prompt()}
</p>
{codes && (
<div
className={cn(
'overflow-clip rounded select-text',
'[&>pre]:overflow-auto [&>pre]:p-2',
'[&>pre]:bg-surface-variant! dark:[&>pre]:bg-black!',
)}
dangerouslySetInnerHTML={{
__html: codes,
}}
/>
)}
</CardContent>
<CardFooter className="gap-2">
<Button variant="flat" onClick={handleCopyToClipboard}>
{m.common_copy()}
</Button>
<ModalClose>{m.common_close()}</ModalClose>
</CardFooter>
</Card>
</ModalContent>
</Modal>
)
}
const ServiceControlButtons = () => {
const { query, upsert } = useSystemService()
const handleToggleClick = useLockFn(async () => {
await upsert.mutateAsync(
query.data?.status === 'running' ? 'stop' : 'start',
)
})
return (
<Button
variant="flat"
onClick={handleToggleClick}
loading={upsert.isPending}
>
{query.data?.status === 'running'
? m.settings_system_proxy_system_service_ctrl_stop()
: m.settings_system_proxy_system_service_ctrl_start()}
</Button>
)
}
export default function SystemServiceCtrl() {
const { query } = useSystemService()
const isInstalled = query.data?.status !== 'not_installed'
return (
<SettingsCard data-slot="system-service-ctrl-card">
<SettingsCardContent
data-slot="system-service-ctrl-card-content"
className="flex flex-col gap-3 px-2"
>
<Card>
<CardHeader>
{m.settings_system_proxy_system_service_ctrl_label()}
</CardHeader>
<CardContent className="gap-1 select-text">
<SystemServiceCtrlItem
name="Service Name"
value={query.data?.name}
/>
<SystemServiceCtrlItem
name="Server Version"
value={query.data?.server?.version}
/>
<SystemServiceCtrlItem
name="Service Status"
value={startCase(query.data?.status)}
/>
</CardContent>
<CardFooter className="flex-wrap-reverse gap-2">
{isInstalled ? (
<>
<ServiceControlButtons />
<ServiceUninstallButton />
</>
) : (
<ServiceInstallButton />
)}
<ServiceDetailButton />
<div className="flex-1" />
<ServicePromptButton />
</CardFooter>
</Card>
</SettingsCardContent>
</SettingsCard>
)
}
@@ -0,0 +1,66 @@
import { Switch } from '@/components/ui/switch'
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip'
import { useLockFn } from '@/hooks/use-lock-fn'
import { m } from '@/paraglide/messages'
import { formatError } from '@/utils'
import { message } from '@/utils/notification'
import { useSetting, useSystemService } from '@nyanpasu/interface'
import { SettingsCard, SettingsCardContent } from '../../_modules/settings-card'
export default function SystemServiceSwitch() {
const serviceMode = useSetting('enable_service_mode')
const { query } = useSystemService()
const disabled = query.data?.status === 'not_installed'
const handleServiceMode = useLockFn(async () => {
try {
await serviceMode.upsert(!serviceMode.value)
} catch (error) {
message(
`Activation Service Mode failed!\n Error: ${formatError(error)}`,
{
title: 'Error',
kind: 'error',
},
)
}
})
return (
<SettingsCard data-slot="system-service-switch-card">
<SettingsCardContent
className="flex items-center justify-between px-3"
data-slot="system-service-switch-card-content"
>
<div>{m.settings_system_proxy_service_mode_label()}</div>
<Tooltip>
<TooltipTrigger asChild>
<div>
<Switch
checked={Boolean(serviceMode.value)}
onCheckedChange={handleServiceMode}
loading={serviceMode.isPending}
disabled={disabled}
/>
</div>
</TooltipTrigger>
{disabled && (
<TooltipContent>
<span>
{m.settings_system_proxy_service_mode_disabled_tooltip()}
</span>
</TooltipContent>
)}
</Tooltip>
</SettingsCardContent>
</SettingsCard>
)
}
@@ -2,6 +2,7 @@ import {
SystemProxyButton,
TunModeButton,
} from '@/components/settings/system-proxy'
import { Separator } from '@/components/ui/separator'
import { m } from '@/paraglide/messages'
import { createFileRoute } from '@tanstack/react-router'
import { SettingsCard, SettingsCardContent } from '../_modules/settings-card'
@@ -13,8 +14,10 @@ import CurrentSystemProxy from './_modules/current-system-proxy'
import ProxyBypassConfig from './_modules/proxy-bypass-config'
import ProxyGuardConfig from './_modules/proxy-guard-config'
import ProxyGuardSwitch from './_modules/proxy-guard-switch'
import SystemServiceCtrl from './_modules/system-service-ctrl'
import SystemServiceSwitch from './_modules/system-service-switch'
export const Route = createFileRoute('/(main)/main/settings/system-proxy')({
export const Route = createFileRoute('/(main)/main/settings/system')({
component: RouteComponent,
})
@@ -22,7 +25,7 @@ function RouteComponent() {
return (
<>
<SettingsTitlePlaceholder />
<SettingsTitle>{m.settings_system_proxy_title()}</SettingsTitle>
<SettingsTitle>{m.settings_label_system()}</SettingsTitle>
<SettingsCard>
<SettingsCardContent>
@@ -41,6 +44,12 @@ function RouteComponent() {
<ProxyBypassConfig />
<CurrentSystemProxy />
<Separator className="my-4" />
<SystemServiceSwitch />
<SystemServiceCtrl />
</>
)
}
@@ -1,12 +1,11 @@
import { useLanguage } from '@/components/providers/language-provider'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { m } from '@/paraglide/messages'
import { Locale, locales } from '@/paraglide/runtime'
import { SettingsCard, SettingsCardContent } from '../../_modules/settings-card'
@@ -21,29 +20,30 @@ export default function LanguageSelector() {
return (
<SettingsCard data-slot="language-selector-card">
<SettingsCardContent
className="flex items-center justify-between px-3"
className="flex items-center justify-between px-2"
data-slot="language-selector-card-content"
>
<div>{m.settings_user_interface_language_label()}</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="flat">{m.language()}</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuRadioGroup
value={language}
onValueChange={handleLanguageChange}
<Select
variant="outlined"
value={language}
onValueChange={handleLanguageChange}
>
<SelectTrigger>
<SelectValue
placeholder={m.settings_user_interface_language_label()}
>
{locales.map((value) => (
<DropdownMenuRadioItem key={value} value={value}>
{m.language(value, { locale: value })}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
{language ? m.language(language, { locale: language }) : null}
</SelectValue>
</SelectTrigger>
<SelectContent>
{Object.entries(locales).map(([key, value]) => (
<SelectItem key={key} value={value}>
{m.language(key, { locale: value })}
</SelectItem>
))}
</SelectContent>
</Select>
</SettingsCardContent>
</SettingsCard>
)
@@ -26,43 +26,47 @@ export default function SwitchLegacy() {
return (
<SettingsCard data-slot="switch-legacy-card">
<SettingsCardContent
className="flex items-center justify-between px-3"
className="flex items-center justify-between px-2"
data-slot="switch-legacy-card-content"
>
<div>Switch to Legacy UI</div>
<Card className="w-full space-y-4">
<CardHeader>Switch to Legacy UI</CardHeader>
<Modal>
<ModalTrigger asChild>
<Button variant="flat">Open</Button>
</ModalTrigger>
<CardFooter>
<Modal>
<ModalTrigger asChild>
<Button variant="stroked">Open</Button>
</ModalTrigger>
<ModalContent>
<Card className="w-96">
<CardHeader>
<ModalTitle>
Are you sure you want to switch to Legacy UI?
</ModalTitle>
</CardHeader>
<ModalContent>
<Card className="w-96">
<CardHeader>
<ModalTitle>
Are you sure you want to switch to Legacy UI?
</ModalTitle>
</CardHeader>
<CardContent>
<p>
Switching to Legacy UI will revert the UI to the original
design.
</p>
</CardContent>
<CardContent>
<p>
Switching to Legacy UI will revert the UI to the original
design.
</p>
</CardContent>
<CardFooter className="gap-2">
<Button variant="flat" onClick={handleClick}>
Continue
</Button>
<CardFooter className="gap-2">
<Button variant="flat" onClick={handleClick}>
Continue
</Button>
<ModalClose asChild>
<Button>Cancel</Button>
</ModalClose>
</CardFooter>
</Card>
</ModalContent>
</Modal>
<ModalClose asChild>
<Button>Cancel</Button>
</ModalClose>
</CardFooter>
</Card>
</ModalContent>
</Modal>
</CardFooter>
</Card>
</SettingsCardContent>
</SettingsCard>
)
@@ -1,7 +1,11 @@
import Check from '~icons/material-symbols/check-rounded'
import { useCallback, useState } from 'react'
import { useExperimentalThemeContext } from '@/components/providers/theme-provider'
import {
DEFAULT_COLOR,
useExperimentalThemeContext,
} from '@/components/providers/theme-provider'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card'
import {
DropdownMenu,
DropdownMenuContent,
@@ -11,6 +15,15 @@ import { m } from '@/paraglide/messages'
import { Wheel } from '@uiw/react-color'
import { SettingsCard, SettingsCardContent } from '../../_modules/settings-card'
const PERSETS = [
DEFAULT_COLOR,
'#9e1e67',
'#3d009e',
'#00089e',
'#066b9e',
'#9e5a00',
] as const
export default function ThemeColorConfig() {
const { themeColor, setThemeColor } = useExperimentalThemeContext()
@@ -26,42 +39,74 @@ export default function ThemeColorConfig() {
return (
<SettingsCard data-slot="theme-color-config-card">
<SettingsCardContent
className="flex items-center justify-between px-3"
className="flex items-center justify-between px-2"
data-slot="theme-color-config-card-content"
>
<div>{m.settings_user_interface_theme_color_label()}</div>
<Card className="w-full">
<CardHeader>
{m.settings_user_interface_theme_color_label()}
</CardHeader>
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild>
<Button className="flex items-center gap-2 px-4" variant="flat">
<span
className="size-4 rounded"
style={{ backgroundColor: themeColor }}
/>
<CardContent>
<div className="flex flex-wrap gap-2">
{PERSETS.map((color) => (
<Button
key={color}
className="flex items-center gap-2 px-4"
variant={themeColor === color ? 'flat' : 'stroked'}
onClick={() => setThemeColor(color)}
>
<span
className="outline-surface-variant size-4 rounded outline"
style={{ backgroundColor: color }}
/>
<span>{themeColor}</span>
</Button>
</DropdownMenuTrigger>
<span>{color.toLocaleUpperCase()}</span>
</Button>
))}
</div>
</CardContent>
<DropdownMenuContent className="flex flex-col gap-4 rounded-2xl p-4">
<Wheel
data-slot="theme-color-config-colorful"
color={cachedThemeColor}
onChange={(color) => {
setCachedThemeColor(color.hex)
}}
/>
<CardFooter>
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild>
<Button className="flex items-center gap-2 px-4" variant="flat">
<span
className="outline-surface-variant size-4 rounded outline"
style={{
backgroundColor: themeColor,
}}
/>
<Button
className="flex items-center justify-center gap-2"
variant="flat"
onClick={handleSubmit}
>
<Check className="size-5" />
<span>{m.common_submit()}</span>
</Button>
</DropdownMenuContent>
</DropdownMenu>
<span>
{PERSETS.includes(themeColor as (typeof PERSETS)[number])
? m.settings_user_interface_theme_color_custom()
: themeColor.toLocaleUpperCase()}
</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="flex flex-col gap-4 rounded-2xl p-4">
<Wheel
data-slot="theme-color-config-colorful"
color={cachedThemeColor}
onChange={(color) => {
setCachedThemeColor(color.hex)
}}
/>
<Button
className="flex items-center justify-center gap-2"
variant="flat"
onClick={handleSubmit}
>
<Check className="size-5" />
<span>{m.common_submit()}</span>
</Button>
</DropdownMenuContent>
</DropdownMenu>
</CardFooter>
</Card>
</SettingsCardContent>
</SettingsCard>
)
@@ -2,14 +2,13 @@ import {
ThemeMode,
useExperimentalThemeContext,
} from '@/components/providers/theme-provider'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { m } from '@/paraglide/messages'
import { SettingsCard, SettingsCardContent } from '../../_modules/settings-card'
@@ -29,29 +28,30 @@ export default function ThemeModeSelector() {
return (
<SettingsCard data-slot="theme-mode-selection-card">
<SettingsCardContent
className="flex items-center justify-between px-3"
className="flex items-center justify-between px-2"
data-slot="theme-mode-selection-card-content"
>
<div>{m.settings_user_interface_theme_mode_label()}</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="flat">{messages[themeMode]}</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuRadioGroup
value={themeMode}
onValueChange={handleThemeModeChange}
<Select
variant="outlined"
value={themeMode}
onValueChange={handleThemeModeChange}
>
<SelectTrigger>
<SelectValue
placeholder={m.settings_user_interface_theme_mode_label()}
>
{Object.values(ThemeMode).map((value) => (
<DropdownMenuRadioItem key={value} value={value}>
{value}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
{themeMode ? messages[themeMode] : null}
</SelectValue>
</SelectTrigger>
<SelectContent>
{Object.entries(messages).map(([key, value]) => (
<SelectItem key={key} value={key}>
{value}
</SelectItem>
))}
</SelectContent>
</Select>
</SettingsCardContent>
</SettingsCard>
)
@@ -37,15 +37,10 @@ import { Route as mainMainProxiesIndexRouteImport } from './pages/(main)/main/pr
import { Route as mainMainProfilesIndexRouteImport } from './pages/(main)/main/profiles/index'
import { Route as mainMainSettingsWebUiRouteRouteImport } from './pages/(main)/main/settings/web-ui/route'
import { Route as mainMainSettingsUserInterfaceRouteRouteImport } from './pages/(main)/main/settings/user-interface/route'
import { Route as mainMainSettingsSystemServiceRouteRouteImport } from './pages/(main)/main/settings/system-service/route'
import { Route as mainMainSettingsSystemProxyRouteRouteImport } from './pages/(main)/main/settings/system-proxy/route'
import { Route as mainMainSettingsSystemBehaviorRouteRouteImport } from './pages/(main)/main/settings/system-behavior/route'
import { Route as mainMainSettingsNyanpasuConfigRouteRouteImport } from './pages/(main)/main/settings/nyanpasu-config/route'
import { Route as mainMainSettingsDebugUtilsRouteRouteImport } from './pages/(main)/main/settings/debug-utils/route'
import { Route as mainMainSettingsClashSettingsRouteRouteImport } from './pages/(main)/main/settings/clash-settings/route'
import { Route as mainMainSettingsClashFiledRouteRouteImport } from './pages/(main)/main/settings/clash-filed/route'
import { Route as mainMainSettingsClashExternalControllRouteRouteImport } from './pages/(main)/main/settings/clash-external-controll/route'
import { Route as mainMainSettingsClashCoreRouteRouteImport } from './pages/(main)/main/settings/clash-core/route'
import { Route as mainMainSettingsSystemRouteRouteImport } from './pages/(main)/main/settings/system/route'
import { Route as mainMainSettingsNyanpasuRouteRouteImport } from './pages/(main)/main/settings/nyanpasu/route'
import { Route as mainMainSettingsDebugRouteRouteImport } from './pages/(main)/main/settings/debug/route'
import { Route as mainMainSettingsClashRouteRouteImport } from './pages/(main)/main/settings/clash/route'
import { Route as mainMainSettingsAboutRouteRouteImport } from './pages/(main)/main/settings/about/route'
import { Route as mainMainProfilesInspectRouteRouteImport } from './pages/(main)/main/profiles/inspect/route'
import { Route as mainMainProfilesTypeIndexRouteImport } from './pages/(main)/main/profiles/$type/index'
@@ -193,58 +188,28 @@ const mainMainSettingsUserInterfaceRouteRoute =
path: '/user-interface',
getParentRoute: () => mainMainSettingsRouteRoute,
} as any)
const mainMainSettingsSystemServiceRouteRoute =
mainMainSettingsSystemServiceRouteRouteImport.update({
id: '/system-service',
path: '/system-service',
const mainMainSettingsSystemRouteRoute =
mainMainSettingsSystemRouteRouteImport.update({
id: '/system',
path: '/system',
getParentRoute: () => mainMainSettingsRouteRoute,
} as any)
const mainMainSettingsSystemProxyRouteRoute =
mainMainSettingsSystemProxyRouteRouteImport.update({
id: '/system-proxy',
path: '/system-proxy',
const mainMainSettingsNyanpasuRouteRoute =
mainMainSettingsNyanpasuRouteRouteImport.update({
id: '/nyanpasu',
path: '/nyanpasu',
getParentRoute: () => mainMainSettingsRouteRoute,
} as any)
const mainMainSettingsSystemBehaviorRouteRoute =
mainMainSettingsSystemBehaviorRouteRouteImport.update({
id: '/system-behavior',
path: '/system-behavior',
const mainMainSettingsDebugRouteRoute =
mainMainSettingsDebugRouteRouteImport.update({
id: '/debug',
path: '/debug',
getParentRoute: () => mainMainSettingsRouteRoute,
} as any)
const mainMainSettingsNyanpasuConfigRouteRoute =
mainMainSettingsNyanpasuConfigRouteRouteImport.update({
id: '/nyanpasu-config',
path: '/nyanpasu-config',
getParentRoute: () => mainMainSettingsRouteRoute,
} as any)
const mainMainSettingsDebugUtilsRouteRoute =
mainMainSettingsDebugUtilsRouteRouteImport.update({
id: '/debug-utils',
path: '/debug-utils',
getParentRoute: () => mainMainSettingsRouteRoute,
} as any)
const mainMainSettingsClashSettingsRouteRoute =
mainMainSettingsClashSettingsRouteRouteImport.update({
id: '/clash-settings',
path: '/clash-settings',
getParentRoute: () => mainMainSettingsRouteRoute,
} as any)
const mainMainSettingsClashFiledRouteRoute =
mainMainSettingsClashFiledRouteRouteImport.update({
id: '/clash-filed',
path: '/clash-filed',
getParentRoute: () => mainMainSettingsRouteRoute,
} as any)
const mainMainSettingsClashExternalControllRouteRoute =
mainMainSettingsClashExternalControllRouteRouteImport.update({
id: '/clash-external-controll',
path: '/clash-external-controll',
getParentRoute: () => mainMainSettingsRouteRoute,
} as any)
const mainMainSettingsClashCoreRouteRoute =
mainMainSettingsClashCoreRouteRouteImport.update({
id: '/clash-core',
path: '/clash-core',
const mainMainSettingsClashRouteRoute =
mainMainSettingsClashRouteRouteImport.update({
id: '/clash',
path: '/clash',
getParentRoute: () => mainMainSettingsRouteRoute,
} as any)
const mainMainSettingsAboutRouteRoute =
@@ -301,15 +266,10 @@ export interface FileRoutesByFullPath {
'/main/': typeof mainMainIndexRoute
'/main/profiles/inspect': typeof mainMainProfilesInspectRouteRoute
'/main/settings/about': typeof mainMainSettingsAboutRouteRoute
'/main/settings/clash-core': typeof mainMainSettingsClashCoreRouteRoute
'/main/settings/clash-external-controll': typeof mainMainSettingsClashExternalControllRouteRoute
'/main/settings/clash-filed': typeof mainMainSettingsClashFiledRouteRoute
'/main/settings/clash-settings': typeof mainMainSettingsClashSettingsRouteRoute
'/main/settings/debug-utils': typeof mainMainSettingsDebugUtilsRouteRoute
'/main/settings/nyanpasu-config': typeof mainMainSettingsNyanpasuConfigRouteRoute
'/main/settings/system-behavior': typeof mainMainSettingsSystemBehaviorRouteRoute
'/main/settings/system-proxy': typeof mainMainSettingsSystemProxyRouteRoute
'/main/settings/system-service': typeof mainMainSettingsSystemServiceRouteRoute
'/main/settings/clash': typeof mainMainSettingsClashRouteRoute
'/main/settings/debug': typeof mainMainSettingsDebugRouteRoute
'/main/settings/nyanpasu': typeof mainMainSettingsNyanpasuRouteRoute
'/main/settings/system': typeof mainMainSettingsSystemRouteRoute
'/main/settings/user-interface': typeof mainMainSettingsUserInterfaceRouteRoute
'/main/settings/web-ui': typeof mainMainSettingsWebUiRouteRoute
'/main/profiles/': typeof mainMainProfilesIndexRoute
@@ -338,15 +298,10 @@ export interface FileRoutesByTo {
'/main': typeof mainMainIndexRoute
'/main/profiles/inspect': typeof mainMainProfilesInspectRouteRoute
'/main/settings/about': typeof mainMainSettingsAboutRouteRoute
'/main/settings/clash-core': typeof mainMainSettingsClashCoreRouteRoute
'/main/settings/clash-external-controll': typeof mainMainSettingsClashExternalControllRouteRoute
'/main/settings/clash-filed': typeof mainMainSettingsClashFiledRouteRoute
'/main/settings/clash-settings': typeof mainMainSettingsClashSettingsRouteRoute
'/main/settings/debug-utils': typeof mainMainSettingsDebugUtilsRouteRoute
'/main/settings/nyanpasu-config': typeof mainMainSettingsNyanpasuConfigRouteRoute
'/main/settings/system-behavior': typeof mainMainSettingsSystemBehaviorRouteRoute
'/main/settings/system-proxy': typeof mainMainSettingsSystemProxyRouteRoute
'/main/settings/system-service': typeof mainMainSettingsSystemServiceRouteRoute
'/main/settings/clash': typeof mainMainSettingsClashRouteRoute
'/main/settings/debug': typeof mainMainSettingsDebugRouteRoute
'/main/settings/nyanpasu': typeof mainMainSettingsNyanpasuRouteRoute
'/main/settings/system': typeof mainMainSettingsSystemRouteRoute
'/main/settings/user-interface': typeof mainMainSettingsUserInterfaceRouteRoute
'/main/settings/web-ui': typeof mainMainSettingsWebUiRouteRoute
'/main/profiles': typeof mainMainProfilesIndexRoute
@@ -383,15 +338,10 @@ export interface FileRoutesById {
'/(main)/main/': typeof mainMainIndexRoute
'/(main)/main/profiles/inspect': typeof mainMainProfilesInspectRouteRoute
'/(main)/main/settings/about': typeof mainMainSettingsAboutRouteRoute
'/(main)/main/settings/clash-core': typeof mainMainSettingsClashCoreRouteRoute
'/(main)/main/settings/clash-external-controll': typeof mainMainSettingsClashExternalControllRouteRoute
'/(main)/main/settings/clash-filed': typeof mainMainSettingsClashFiledRouteRoute
'/(main)/main/settings/clash-settings': typeof mainMainSettingsClashSettingsRouteRoute
'/(main)/main/settings/debug-utils': typeof mainMainSettingsDebugUtilsRouteRoute
'/(main)/main/settings/nyanpasu-config': typeof mainMainSettingsNyanpasuConfigRouteRoute
'/(main)/main/settings/system-behavior': typeof mainMainSettingsSystemBehaviorRouteRoute
'/(main)/main/settings/system-proxy': typeof mainMainSettingsSystemProxyRouteRoute
'/(main)/main/settings/system-service': typeof mainMainSettingsSystemServiceRouteRoute
'/(main)/main/settings/clash': typeof mainMainSettingsClashRouteRoute
'/(main)/main/settings/debug': typeof mainMainSettingsDebugRouteRoute
'/(main)/main/settings/nyanpasu': typeof mainMainSettingsNyanpasuRouteRoute
'/(main)/main/settings/system': typeof mainMainSettingsSystemRouteRoute
'/(main)/main/settings/user-interface': typeof mainMainSettingsUserInterfaceRouteRoute
'/(main)/main/settings/web-ui': typeof mainMainSettingsWebUiRouteRoute
'/(main)/main/profiles/': typeof mainMainProfilesIndexRoute
@@ -427,15 +377,10 @@ export interface FileRouteTypes {
| '/main/'
| '/main/profiles/inspect'
| '/main/settings/about'
| '/main/settings/clash-core'
| '/main/settings/clash-external-controll'
| '/main/settings/clash-filed'
| '/main/settings/clash-settings'
| '/main/settings/debug-utils'
| '/main/settings/nyanpasu-config'
| '/main/settings/system-behavior'
| '/main/settings/system-proxy'
| '/main/settings/system-service'
| '/main/settings/clash'
| '/main/settings/debug'
| '/main/settings/nyanpasu'
| '/main/settings/system'
| '/main/settings/user-interface'
| '/main/settings/web-ui'
| '/main/profiles/'
@@ -464,15 +409,10 @@ export interface FileRouteTypes {
| '/main'
| '/main/profiles/inspect'
| '/main/settings/about'
| '/main/settings/clash-core'
| '/main/settings/clash-external-controll'
| '/main/settings/clash-filed'
| '/main/settings/clash-settings'
| '/main/settings/debug-utils'
| '/main/settings/nyanpasu-config'
| '/main/settings/system-behavior'
| '/main/settings/system-proxy'
| '/main/settings/system-service'
| '/main/settings/clash'
| '/main/settings/debug'
| '/main/settings/nyanpasu'
| '/main/settings/system'
| '/main/settings/user-interface'
| '/main/settings/web-ui'
| '/main/profiles'
@@ -508,15 +448,10 @@ export interface FileRouteTypes {
| '/(main)/main/'
| '/(main)/main/profiles/inspect'
| '/(main)/main/settings/about'
| '/(main)/main/settings/clash-core'
| '/(main)/main/settings/clash-external-controll'
| '/(main)/main/settings/clash-filed'
| '/(main)/main/settings/clash-settings'
| '/(main)/main/settings/debug-utils'
| '/(main)/main/settings/nyanpasu-config'
| '/(main)/main/settings/system-behavior'
| '/(main)/main/settings/system-proxy'
| '/(main)/main/settings/system-service'
| '/(main)/main/settings/clash'
| '/(main)/main/settings/debug'
| '/(main)/main/settings/nyanpasu'
| '/(main)/main/settings/system'
| '/(main)/main/settings/user-interface'
| '/(main)/main/settings/web-ui'
| '/(main)/main/profiles/'
@@ -732,67 +667,32 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof mainMainSettingsUserInterfaceRouteRouteImport
parentRoute: typeof mainMainSettingsRouteRoute
}
'/(main)/main/settings/system-service': {
id: '/(main)/main/settings/system-service'
path: '/system-service'
fullPath: '/main/settings/system-service'
preLoaderRoute: typeof mainMainSettingsSystemServiceRouteRouteImport
'/(main)/main/settings/system': {
id: '/(main)/main/settings/system'
path: '/system'
fullPath: '/main/settings/system'
preLoaderRoute: typeof mainMainSettingsSystemRouteRouteImport
parentRoute: typeof mainMainSettingsRouteRoute
}
'/(main)/main/settings/system-proxy': {
id: '/(main)/main/settings/system-proxy'
path: '/system-proxy'
fullPath: '/main/settings/system-proxy'
preLoaderRoute: typeof mainMainSettingsSystemProxyRouteRouteImport
'/(main)/main/settings/nyanpasu': {
id: '/(main)/main/settings/nyanpasu'
path: '/nyanpasu'
fullPath: '/main/settings/nyanpasu'
preLoaderRoute: typeof mainMainSettingsNyanpasuRouteRouteImport
parentRoute: typeof mainMainSettingsRouteRoute
}
'/(main)/main/settings/system-behavior': {
id: '/(main)/main/settings/system-behavior'
path: '/system-behavior'
fullPath: '/main/settings/system-behavior'
preLoaderRoute: typeof mainMainSettingsSystemBehaviorRouteRouteImport
'/(main)/main/settings/debug': {
id: '/(main)/main/settings/debug'
path: '/debug'
fullPath: '/main/settings/debug'
preLoaderRoute: typeof mainMainSettingsDebugRouteRouteImport
parentRoute: typeof mainMainSettingsRouteRoute
}
'/(main)/main/settings/nyanpasu-config': {
id: '/(main)/main/settings/nyanpasu-config'
path: '/nyanpasu-config'
fullPath: '/main/settings/nyanpasu-config'
preLoaderRoute: typeof mainMainSettingsNyanpasuConfigRouteRouteImport
parentRoute: typeof mainMainSettingsRouteRoute
}
'/(main)/main/settings/debug-utils': {
id: '/(main)/main/settings/debug-utils'
path: '/debug-utils'
fullPath: '/main/settings/debug-utils'
preLoaderRoute: typeof mainMainSettingsDebugUtilsRouteRouteImport
parentRoute: typeof mainMainSettingsRouteRoute
}
'/(main)/main/settings/clash-settings': {
id: '/(main)/main/settings/clash-settings'
path: '/clash-settings'
fullPath: '/main/settings/clash-settings'
preLoaderRoute: typeof mainMainSettingsClashSettingsRouteRouteImport
parentRoute: typeof mainMainSettingsRouteRoute
}
'/(main)/main/settings/clash-filed': {
id: '/(main)/main/settings/clash-filed'
path: '/clash-filed'
fullPath: '/main/settings/clash-filed'
preLoaderRoute: typeof mainMainSettingsClashFiledRouteRouteImport
parentRoute: typeof mainMainSettingsRouteRoute
}
'/(main)/main/settings/clash-external-controll': {
id: '/(main)/main/settings/clash-external-controll'
path: '/clash-external-controll'
fullPath: '/main/settings/clash-external-controll'
preLoaderRoute: typeof mainMainSettingsClashExternalControllRouteRouteImport
parentRoute: typeof mainMainSettingsRouteRoute
}
'/(main)/main/settings/clash-core': {
id: '/(main)/main/settings/clash-core'
path: '/clash-core'
fullPath: '/main/settings/clash-core'
preLoaderRoute: typeof mainMainSettingsClashCoreRouteRouteImport
'/(main)/main/settings/clash': {
id: '/(main)/main/settings/clash'
path: '/clash'
fullPath: '/main/settings/clash'
preLoaderRoute: typeof mainMainSettingsClashRouteRouteImport
parentRoute: typeof mainMainSettingsRouteRoute
}
'/(main)/main/settings/about': {
@@ -906,15 +806,10 @@ const mainMainRulesRouteRouteWithChildren =
interface mainMainSettingsRouteRouteChildren {
mainMainSettingsAboutRouteRoute: typeof mainMainSettingsAboutRouteRoute
mainMainSettingsClashCoreRouteRoute: typeof mainMainSettingsClashCoreRouteRoute
mainMainSettingsClashExternalControllRouteRoute: typeof mainMainSettingsClashExternalControllRouteRoute
mainMainSettingsClashFiledRouteRoute: typeof mainMainSettingsClashFiledRouteRoute
mainMainSettingsClashSettingsRouteRoute: typeof mainMainSettingsClashSettingsRouteRoute
mainMainSettingsDebugUtilsRouteRoute: typeof mainMainSettingsDebugUtilsRouteRoute
mainMainSettingsNyanpasuConfigRouteRoute: typeof mainMainSettingsNyanpasuConfigRouteRoute
mainMainSettingsSystemBehaviorRouteRoute: typeof mainMainSettingsSystemBehaviorRouteRoute
mainMainSettingsSystemProxyRouteRoute: typeof mainMainSettingsSystemProxyRouteRoute
mainMainSettingsSystemServiceRouteRoute: typeof mainMainSettingsSystemServiceRouteRoute
mainMainSettingsClashRouteRoute: typeof mainMainSettingsClashRouteRoute
mainMainSettingsDebugRouteRoute: typeof mainMainSettingsDebugRouteRoute
mainMainSettingsNyanpasuRouteRoute: typeof mainMainSettingsNyanpasuRouteRoute
mainMainSettingsSystemRouteRoute: typeof mainMainSettingsSystemRouteRoute
mainMainSettingsUserInterfaceRouteRoute: typeof mainMainSettingsUserInterfaceRouteRoute
mainMainSettingsWebUiRouteRoute: typeof mainMainSettingsWebUiRouteRoute
mainMainSettingsIndexRoute: typeof mainMainSettingsIndexRoute
@@ -922,20 +817,10 @@ interface mainMainSettingsRouteRouteChildren {
const mainMainSettingsRouteRouteChildren: mainMainSettingsRouteRouteChildren = {
mainMainSettingsAboutRouteRoute: mainMainSettingsAboutRouteRoute,
mainMainSettingsClashCoreRouteRoute: mainMainSettingsClashCoreRouteRoute,
mainMainSettingsClashExternalControllRouteRoute:
mainMainSettingsClashExternalControllRouteRoute,
mainMainSettingsClashFiledRouteRoute: mainMainSettingsClashFiledRouteRoute,
mainMainSettingsClashSettingsRouteRoute:
mainMainSettingsClashSettingsRouteRoute,
mainMainSettingsDebugUtilsRouteRoute: mainMainSettingsDebugUtilsRouteRoute,
mainMainSettingsNyanpasuConfigRouteRoute:
mainMainSettingsNyanpasuConfigRouteRoute,
mainMainSettingsSystemBehaviorRouteRoute:
mainMainSettingsSystemBehaviorRouteRoute,
mainMainSettingsSystemProxyRouteRoute: mainMainSettingsSystemProxyRouteRoute,
mainMainSettingsSystemServiceRouteRoute:
mainMainSettingsSystemServiceRouteRoute,
mainMainSettingsClashRouteRoute: mainMainSettingsClashRouteRoute,
mainMainSettingsDebugRouteRoute: mainMainSettingsDebugRouteRoute,
mainMainSettingsNyanpasuRouteRoute: mainMainSettingsNyanpasuRouteRoute,
mainMainSettingsSystemRouteRoute: mainMainSettingsSystemRouteRoute,
mainMainSettingsUserInterfaceRouteRoute:
mainMainSettingsUserInterfaceRouteRoute,
mainMainSettingsWebUiRouteRoute: mainMainSettingsWebUiRouteRoute,
@@ -1,3 +1,4 @@
// oxlint-disable typescript/no-explicit-any
// Deep copy and change all keys to lowercase
type TData = Record<string, any>
@@ -1,12 +1,12 @@
// oxlint-disable typescript/no-explicit-any
import { includes, isArray, isObject, isString, some } from 'lodash-es'
import { EnvInfos } from '@nyanpasu/interface'
import { EnvInfo } from '@nyanpasu/interface'
/**
* classNames filter out falsy values and join the rest with a space
* @param classes - array of classes
* @returns string of classes
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function classNames(...classes: any[]) {
return classes.filter(Boolean).join(' ')
}
@@ -33,7 +33,7 @@ export function formatError(err: unknown): string {
return `Error: ${err instanceof Error ? err.message : String(err)}`
}
export function formatEnvInfos(envs: EnvInfos) {
export function formatEnvInfos(envs: EnvInfo) {
let result = '----------- System -----------\n'
result += `OS: ${envs.os}\n`
result += `Arch: ${envs.arch}\n`
@@ -50,9 +50,11 @@ export function formatEnvInfos(envs: EnvInfos) {
for (const k of Object.keys(envs.build_info) as string[]) {
const key = k
.split('_')
.map((v) => v.charAt(0).toUpperCase() + v.slice(1))
.map((v: string) => v.charAt(0).toUpperCase() + v.slice(1))
.join(' ')
result += `${key}: ${envs.build_info[k]}\n`
// Fix linter error: explicitly type k as keyof typeof envs.build_info
result += `${key}: ${envs.build_info[k as keyof typeof envs.build_info]}\n`
}
return result
}
@@ -156,5 +156,6 @@ export default defineConfig(({ command, mode }) => {
html: {},
} satisfies UserConfig
// fucking vite why embedded their own sass types definition????
// oxlint-disable-next-line typescript/no-explicit-any
return config as any as UserConfig
})
+2 -2
View File
@@ -19,11 +19,11 @@
"@radix-ui/react-scroll-area": "1.2.10",
"@tauri-apps/api": "2.8.0",
"@types/d3": "7.4.3",
"@types/react": "19.2.11",
"@types/react": "19.2.13",
"@vitejs/plugin-react": "5.1.3",
"ahooks": "3.9.6",
"d3": "7.9.0",
"framer-motion": "12.31.0",
"framer-motion": "12.33.0",
"react": "19.2.4",
"react-dom": "19.2.4",
"react-error-boundary": "6.0.0",
@@ -1,3 +1,4 @@
// oxlint-disable typescript/no-explicit-any
import * as d3 from 'd3'
import { interpolatePath } from 'd3-interpolate-path'
import { CSSProperties, FC, useEffect, useMemo, useRef } from 'react'
@@ -118,7 +118,7 @@ export const useContainerBreakpointValue = <T>(
): T => {
const currentBreakpoint = useContainerBreakpoint(containerRef)
const calculateValue = (): T => {
const memoizedValue = useMemo(() => {
const value = values[currentBreakpoint]
if (value !== undefined) {
@@ -136,12 +136,7 @@ export const useContainerBreakpointValue = <T>(
}
return defaultValue ?? (values[breakpointsOrder[0]] as T)
}
const memoizedValue = useMemo(
() => calculateValue(),
[currentBreakpoint, values, defaultValue],
)
}, [currentBreakpoint, values, defaultValue])
return memoizedValue
}
+2 -2
View File
@@ -2,7 +2,7 @@
"manifest_version": 1,
"latest": {
"mihomo": "v1.19.19",
"mihomo_alpha": "alpha-558b384",
"mihomo_alpha": "alpha-86257fc",
"clash_rs": "v0.9.4",
"clash_premium": "2023-09-05-gdcc8d87",
"clash_rs_alpha": "0.9.4-alpha+sha.5675004"
@@ -69,5 +69,5 @@
"linux-armv7hf": "clash-armv7-unknown-linux-gnueabihf"
}
},
"updated_at": "2026-02-05T00:18:37.438Z"
"updated_at": "2026-02-05T22:41:40.244Z"
}
+5 -21
View File
@@ -20,7 +20,7 @@
"web:visualize": "pnpm --filter=@nyanpasu/nyanpasu bundle:visualize",
"lint": "run-s lint:*",
"lint:prettier": "prettier --check .",
"lint:eslint": "eslint --cache .",
"lint:oxlint": "oxlint .",
"lint:styles": "stylelint --cache --allow-empty-input \"**/*.{css,scss}\"",
"lint:ts": "run-s lint:ts:*",
"lint:ts:scripts": "tsc --noEmit --project ./scripts/tsconfig.json",
@@ -35,7 +35,7 @@
"fmt": "run-p fmt:*",
"fmt:backend": "cargo fmt --manifest-path ./backend/Cargo.toml --all",
"fmt:prettier": "prettier --write .",
"fmt:eslint": "eslint --cache --fix . eslint.config.js",
"fmt:oxlint": "oxlint --fix .",
"updater": "tsx scripts/updater.ts",
"updater:nightly": "tsx scripts/updater-nightly.ts",
"send-notify": "tsx scripts/telegram-notify.ts",
@@ -60,35 +60,20 @@
"devDependencies": {
"@commitlint/cli": "20.4.1",
"@commitlint/config-conventional": "20.4.1",
"@eslint/compat": "1.4.1",
"@eslint/eslintrc": "3.3.3",
"@ianvs/prettier-plugin-sort-imports": "4.7.1",
"@tauri-apps/cli": "2.8.4",
"@types/fs-extra": "11.0.4",
"@types/lodash-es": "4.17.12",
"@types/node": "24.10.10",
"@typescript-eslint/eslint-plugin": "8.54.0",
"@typescript-eslint/parser": "8.54.0",
"@types/node": "24.10.11",
"autoprefixer": "10.4.24",
"conventional-changelog-conventionalcommits": "9.1.0",
"cross-env": "10.1.0",
"dedent": "1.7.1",
"eslint": "9.39.2",
"eslint-config-prettier": "10.1.8",
"eslint-import-resolver-alias": "1.1.2",
"eslint-plugin-html": "8.1.4",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-n": "17.23.2",
"eslint-plugin-prettier": "5.5.5",
"eslint-plugin-promise": "7.2.1",
"eslint-plugin-react": "7.37.5",
"eslint-plugin-react-compiler": "19.1.0-rc.2",
"eslint-plugin-react-hooks": "7.0.1",
"globals": "16.5.0",
"knip": "5.83.0",
"lint-staged": "16.2.7",
"neostandard": "0.12.2",
"npm-run-all2": "8.0.4",
"oxlint": "^1.43.0",
"postcss": "8.5.6",
"postcss-html": "1.8.1",
"postcss-import": "16.1.1",
@@ -106,8 +91,7 @@
"stylelint-scss": "6.14.0",
"tailwindcss": "4.1.18",
"tsx": "4.21.0",
"typescript": "5.9.3",
"typescript-eslint": "8.54.0"
"typescript": "5.9.3"
},
"packageManager": "pnpm@10.28.2",
"engines": {
+552 -3946
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -9,7 +9,7 @@
"figlet": "1.10.0",
"filesize": "11.0.13",
"p-retry": "7.1.1",
"semver": "7.7.3",
"semver": "7.7.4",
"zod": "4.2.1"
},
"devDependencies": {
@@ -1,16 +1,16 @@
--- a/include/linux/if_bridge.h
+++ b/include/linux/if_bridge.h
@@ -71,6 +71,9 @@ void brioctl_set(int (*hook)(struct net
@@ -68,6 +68,9 @@ struct net_bridge;
void brioctl_set(int (*hook)(struct net *net, unsigned int cmd,
void __user *uarg));
int br_ioctl_call(struct net *net, struct net_bridge *br, unsigned int cmd,
struct ifreq *ifr, void __user *uarg);
int br_ioctl_call(struct net *net, unsigned int cmd, void __user *uarg);
+/* extern void br_dev_update_stats(struct net_device *dev,
+ struct rtnl_link_stats64 *nlstats); */
+extern bool br_is_hairpin_enabled(struct net_device *dev);
extern void br_dev_update_stats(struct net_device *dev,
struct rtnl_link_stats64 *nlstats);
@@ -216,4 +219,42 @@ static inline clock_t br_get_ageing_time
#if IS_ENABLED(CONFIG_BRIDGE) && IS_ENABLED(CONFIG_BRIDGE_IGMP_SNOOPING)
int br_multicast_list_adjacent(struct net_device *dev,
@@ -210,4 +213,42 @@ static inline clock_t br_get_ageing_time(const struct net_device *br_dev)
}
#endif
@@ -81,7 +81,7 @@
return false;
--- a/include/linux/netdevice.h
+++ b/include/linux/netdevice.h
@@ -2907,6 +2907,10 @@ enum netdev_cmd {
@@ -2897,6 +2897,10 @@ enum netdev_cmd {
NETDEV_OFFLOAD_XSTATS_REPORT_USED,
NETDEV_OFFLOAD_XSTATS_REPORT_DELTA,
NETDEV_XDP_FEAT_CHANGE,
@@ -134,12 +134,6 @@
struct neigh_seq_state {
struct seq_net_private p;
struct neigh_table *tbl;
@@ -601,4 +613,5 @@ static inline void neigh_update_is_route
*notify = 1;
}
}
+
#endif
--- a/include/net/route.h
+++ b/include/net/route.h
@@ -246,6 +246,11 @@ struct rtable *rt_dst_alloc(struct net_d
@@ -156,7 +150,7 @@
void fib_del_ifaddr(struct in_ifaddr *, struct in_ifaddr *);
--- a/net/bridge/br_private.h
+++ b/net/bridge/br_private.h
@@ -2304,4 +2304,6 @@ void br_do_suppress_nd(struct sk_buff *s
@@ -2311,4 +2311,6 @@ void br_do_suppress_nd(struct sk_buff *skb, struct net_bridge *br,
u16 vid, struct net_bridge_port *p, struct nd_msg *msg);
struct nd_msg *br_is_nd_neigh_msg(struct sk_buff *skb, struct nd_msg *m);
bool br_is_neigh_suppress_enabled(const struct net_bridge_port *p, u16 vid);
@@ -307,7 +301,7 @@
spin_unlock_bh(&br->hash_lock);
}
}
@@ -928,6 +985,12 @@ void br_fdb_update(struct net_bridge *br
@@ -932,6 +989,12 @@ void br_fdb_update(struct net_bridge *br, struct net_bridge_port *source,
*/
if (unlikely(test_bit(BR_FDB_LOCKED, &fdb->flags)))
clear_bit(BR_FDB_LOCKED, &fdb->flags);
@@ -320,7 +314,7 @@
}
if (unlikely(test_bit(BR_FDB_ADDED_BY_USER, &flags))) {
@@ -955,6 +1018,64 @@ void br_fdb_update(struct net_bridge *br
@@ -959,6 +1022,64 @@ void br_fdb_update(struct net_bridge *br, struct net_bridge_port *source,
}
}
@@ -400,7 +394,7 @@
/*
* Determine initial path cost based on speed.
* using recommendations from 802.1d standard
@@ -697,6 +703,8 @@ int br_add_if(struct net_bridge *br, str
@@ -698,6 +704,8 @@ int br_add_if(struct net_bridge *br, struct net_device *dev,
kobject_uevent(&p->kobj, KOBJ_ADD);
@@ -409,7 +403,7 @@
return 0;
err6:
@@ -732,6 +740,8 @@ int br_del_if(struct net_bridge *br, str
@@ -733,6 +741,8 @@ int br_del_if(struct net_bridge *br, struct net_device *dev)
if (!p || p->br != br)
return -EINVAL;
@@ -418,7 +412,7 @@
/* Since more than one interface can be attached to a bridge,
* there still maybe an alternate path for netconsole to use;
* therefore there is no reason for a NETDEV_RELEASE event.
@@ -797,3 +807,97 @@ bool br_port_flag_is_set(const struct ne
@@ -776,3 +786,97 @@ bool br_port_flag_is_set(const struct net_device *dev, unsigned long flag)
return p->flags & flag;
}
EXPORT_SYMBOL_GPL(br_port_flag_is_set);
@@ -620,7 +614,7 @@
fib_release_info(fa_to_delete->fa_info);
alias_free_mem_rcu(fa_to_delete);
return 0;
@@ -2386,6 +2395,20 @@ void __init fib_trie_init(void)
@@ -2387,6 +2396,20 @@ void __init fib_trie_init(void)
0, SLAB_PANIC | SLAB_ACCOUNT, NULL);
}
@@ -643,7 +637,7 @@
struct fib_table *tb;
--- a/net/ipv6/ndisc.c
+++ b/net/ipv6/ndisc.c
@@ -670,6 +670,7 @@ void ndisc_send_ns(struct net_device *de
@@ -672,6 +672,7 @@ void ndisc_send_ns(struct net_device *dev, const struct in6_addr *solicit,
if (skb)
ndisc_send_skb(skb, daddr, saddr);
}
@@ -653,7 +647,7 @@
const struct in6_addr *daddr)
--- a/net/ipv6/route.c
+++ b/net/ipv6/route.c
@@ -196,6 +196,9 @@ static void rt6_uncached_list_flush_dev(
@@ -194,6 +194,9 @@ static void rt6_uncached_list_flush_dev(struct net_device *dev)
}
}
@@ -663,7 +657,7 @@
static inline const void *choose_neigh_daddr(const struct in6_addr *p,
struct sk_buff *skb,
const void *daddr)
@@ -3917,6 +3920,10 @@ int ip6_route_add(struct fib6_config *cf
@@ -3909,6 +3912,10 @@ int ip6_route_add(struct fib6_config *cfg, gfp_t gfp_flags,
return PTR_ERR(rt);
err = __ip6_ins_rt(rt, &cfg->fc_nlinfo, extack);
@@ -674,7 +668,7 @@
fib6_info_release(rt);
return err;
@@ -3938,6 +3945,9 @@ static int __ip6_del_rt(struct fib6_info
@@ -3930,6 +3937,9 @@ static int __ip6_del_rt(struct fib6_info *rt, struct nl_info *info)
err = fib6_del(rt, info);
spin_unlock_bh(&table->tb6_lock);
@@ -684,7 +678,7 @@
out:
fib6_info_release(rt);
return err;
@@ -6409,6 +6419,20 @@ static int ip6_route_dev_notify(struct n
@@ -6390,6 +6400,20 @@ static int ip6_route_dev_notify(struct notifier_block *this,
return NOTIFY_OK;
}
@@ -707,7 +701,7 @@
*/
--- a/net/core/dev.c
+++ b/net/core/dev.c
@@ -1768,6 +1768,7 @@ const char *netdev_cmd_to_name(enum netd
@@ -1750,6 +1750,7 @@ const char *netdev_cmd_to_name(enum netdev_cmd cmd)
N(PRE_CHANGEADDR) N(OFFLOAD_XSTATS_ENABLE) N(OFFLOAD_XSTATS_DISABLE)
N(OFFLOAD_XSTATS_REPORT_USED) N(OFFLOAD_XSTATS_REPORT_DELTA)
N(XDP_FEAT_CHANGE)
@@ -785,12 +779,12 @@
#endif /* _UAPI_LINUX_IN_H */
--- a/net/netfilter/nf_conntrack_ecache.c
+++ b/net/netfilter/nf_conntrack_ecache.c
@@ -365,7 +365,7 @@ int nf_conntrack_register_notifier(struc
@@ -266,7 +266,7 @@ void nf_conntrack_register_notifier(struct net *net,
mutex_lock(&nf_ct_ecache_mutex);
notify = rcu_dereference_protected(net->ct.nf_conntrack_event_cb,
lockdep_is_held(&nf_ct_ecache_mutex));
- WARN_ON_ONCE(notify);
+ /* WARN_ON_ONCE(notify); */
if (notify != NULL) {
ret = -EBUSY;
goto out_unlock;
rcu_assign_pointer(net->ct.nf_conntrack_event_cb, new);
mutex_unlock(&nf_ct_ecache_mutex);
}
+13 -1
View File
@@ -180,7 +180,7 @@ client-mac-arm64:
# Build windows clients.
.PHONY: client-windows
client-windows: client-windows-x86 client-windows-amd64
client-windows: client-windows-x86 client-windows-amd64 client-windows-arm64
# Build windows x86 client.
.PHONY: client-windows-x86
@@ -206,6 +206,18 @@ client-windows-amd64:
mv mieru_${VERSION}_windows_amd64.zip ../../
mv mieru_${VERSION}_windows_amd64.zip.sha256.txt ../../
# Build windows arm64 client.
.PHONY: client-windows-arm64
client-windows-arm64:
mkdir -p release/windows/arm64
env GOOS=windows GOARCH=arm64 CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o release/windows/arm64/mieru.exe cmd/mieru/mieru.go
cd release/windows/arm64
sha256sum mieru.exe > mieru_${VERSION}_windows_arm64.exe.sha256.txt
zip -r mieru_${VERSION}_windows_arm64.zip mieru.exe
sha256sum mieru_${VERSION}_windows_arm64.zip > mieru_${VERSION}_windows_arm64.zip.sha256.txt
mv mieru_${VERSION}_windows_arm64.zip ../../
mv mieru_${VERSION}_windows_arm64.zip.sha256.txt ../../
# Build linux servers.
.PHONY: server-linux
server-linux: server-linux-amd64 server-linux-arm64
+12 -13
View File
@@ -29,20 +29,19 @@ import (
"github.com/enfein/mieru/v3/pkg/stderror"
)
// ValidateClientConfigSingleProfile validates
// a single client config profile.
// ValidateClientConfigSingleProfile validates a single client config profile.
//
// It validates
// 1. profile name is not empty
// 2. user name is not empty
// 3. user has either a password or a hashed password
// 4. user has no quota
// 5. it has at least 1 server, and for each server
// 5.1. the server has either IP address or domain name
// 5.2. if set, server's IP address is parsable
// 5.3. the server has at least 1 port binding, and all port bindings are valid
// 6. if set, MTU is valid
// 7. if set, traffic pattern is valid
// It validates:
// - profile name is not empty
// - user name is not empty
// - user has either a password or a hashed password
// - user has no quota
// - it has at least 1 server, and for each server:
// 1. the server has either IP address or domain name
// 2. if set, server's IP address is parsable
// 3. the server has at least 1 port binding, and all port bindings are valid
// - if set, MTU is valid
// - if set, traffic pattern is valid
func ValidateClientConfigSingleProfile(profile *pb.ClientProfile) error {
name := profile.GetProfileName()
if name == "" {
+7 -8
View File
@@ -21,15 +21,14 @@ import (
pb "github.com/enfein/mieru/v3/pkg/appctl/appctlpb"
)
// ValidateServerConfigSingleUser validates
// a single server config user.
// ValidateServerConfigSingleUser validates a single server config user.
//
// It validates
// 1. user name is not empty
// 2. user has either a password or a hashed password
// 3. for each quota
// 3.1. number of days is valid
// 3.2. traffic volume in megabyte is valid
// It validates:
// - user name is not empty
// - user has either a password or a hashed password
// - for each quota:
// 1. number of days is valid
// 2. traffic volume in megabyte is valid
func ValidateServerConfigSingleUser(user *pb.User) error {
if user.GetName() == "" {
return fmt.Errorf("user name is not set")

Some files were not shown because too many files have changed in this diff Show More