Update On Tue May 20 20:37:27 CEST 2025

This commit is contained in:
github-action[bot]
2025-05-20 20:37:27 +02:00
parent 12fb6c8088
commit cc01b81d8c
104 changed files with 2544 additions and 1865 deletions
+1
View File
@@ -1004,3 +1004,4 @@ Update On Fri May 16 20:36:33 CEST 2025
Update On Sat May 17 20:35:13 CEST 2025
Update On Sun May 18 20:35:03 CEST 2025
Update On Mon May 19 20:37:08 CEST 2025
Update On Tue May 20 20:37:19 CEST 2025
+2
View File
@@ -12,6 +12,8 @@ public static class Config
public static string HOST { get; set; } = "api.bilibili.com";
//BiliPlus EP Host
public static string EPHOST { get; set; } = "api.bilibili.com";
//Bili Tv Api Host
public static string TVHOST { get; set; } = "api.snm0516.aisee.tv";
//BiliPlus Area
public static string AREA { get; set; } = "";
+1 -1
View File
@@ -29,7 +29,7 @@ public static partial class Parser
if (appApi) return await AppHelper.DoReqAsync(aid, cid, epId, qn, bangumi, encoding, Config.TOKEN);
string prefix = tvApi ? bangumi ? "api.snm0516.aisee.tv/pgc/player/api/playurltv" : "api.snm0516.aisee.tv/x/tv/playurl"
string prefix = tvApi ? bangumi ? $"{Config.TVHOST}/pgc/player/api/playurltv" : $"{Config.TVHOST}/x/tv/playurl"
: bangumi ? $"{Config.HOST}/pgc/player/web/v2/playurl" : "api.bilibili.com/x/player/wbi/playurl";
prefix = $"https://{prefix}?";
+3
View File
@@ -77,6 +77,7 @@ internal static class CommandLineInvoker
private static readonly Option<string> MultiFilePattern = new(["--multi-file-pattern", "-M"], $"使用内置变量自定义多P存储文件名:\r\n\r\n默认为: {Program.MultiPageDefaultSavePath}\r\n");
private static readonly Option<string> Host = new(["--host"], "指定BiliPlus host(使用BiliPlus需要access_token, 不需要cookie, 解析服务器能够获取你账号的大部分权限!)");
private static readonly Option<string> EpHost = new(["--ep-host"], "指定BiliPlus EP host(用于代理api.bilibili.com/pgc/view/web/season, 大部分解析服务器不支持代理该接口)");
private static readonly Option<string> TvHost = new(["--tv-host"], "自定义tv端接口请求Host(用于代理api.snm0516.aisee.tv)");
private static readonly Option<string> Area = new(["--area"], "(hk|tw|th) 使用BiliPlus时必选, 指定BiliPlus area");
private static readonly Option<string> ConfigFile = new(["--config-file"], "读取指定的BBDown本地配置文件(默认为: BBDown.config)");//以下仅为兼容旧版本命令行, 不建议使用
private static readonly Option<string> Aria2cProxy = new(["--aria2c-proxy"], "调用aria2c进行下载时的代理地址配置") { IsHidden = true };
@@ -144,6 +145,7 @@ internal static class CommandLineInvoker
if (bindingContext.ParseResult.HasOption(DelayPerPage)) option.DelayPerPage = bindingContext.ParseResult.GetValueForOption(DelayPerPage)!;
if (bindingContext.ParseResult.HasOption(Host)) option.Host = bindingContext.ParseResult.GetValueForOption(Host)!;
if (bindingContext.ParseResult.HasOption(EpHost)) option.EpHost = bindingContext.ParseResult.GetValueForOption(EpHost)!;
if (bindingContext.ParseResult.HasOption(TvHost)) option.TvHost = bindingContext.ParseResult.GetValueForOption(TvHost)!;
if (bindingContext.ParseResult.HasOption(Area)) option.Area = bindingContext.ParseResult.GetValueForOption(Area)!;
if (bindingContext.ParseResult.HasOption(ConfigFile)) option.ConfigFile = bindingContext.ParseResult.GetValueForOption(ConfigFile)!;
if (bindingContext.ParseResult.HasOption(Aria2cProxy)) option.Aria2cProxy = bindingContext.ParseResult.GetValueForOption(Aria2cProxy)!;
@@ -208,6 +210,7 @@ internal static class CommandLineInvoker
DelayPerPage,
Host,
EpHost,
TvHost,
Area,
ConfigFile,
Aria2cProxy,
+1
View File
@@ -50,6 +50,7 @@ internal class MyOption
public string DelayPerPage { get; set; } = "0";
public string Host { get; set; } = "api.bilibili.com";
public string EpHost { get; set; } = "api.bilibili.com";
public string TvHost { get; set; } = "api.snm0516.aisee.tv";
public string Area { get; set; } = "";
public string? ConfigFile { get; set; }
//以下仅为兼容旧版本命令行,不建议使用
+1
View File
@@ -217,6 +217,7 @@ partial class Program
Config.DEBUG_LOG = myOption.Debug;
Config.HOST = myOption.Host;
Config.EPHOST = myOption.EpHost;
Config.TVHOST = myOption.TvHost;
Config.AREA = myOption.Area;
Config.COOKIE = myOption.Cookie;
Config.TOKEN = myOption.AccessToken.Replace("access_token=", "");
+1
View File
@@ -111,6 +111,7 @@ Options:
--delay-per-page <delay-per-page> 设置下载合集分P之间的下载间隔时间(单位: 秒, 默认无间隔)
--host <host> 指定BiliPlus host(使用BiliPlus需要access_token, 不需要cookie, 解析服务器能够获取你账号的大部分权限!)
--ep-host <ep-host> 指定BiliPlus EP host(用于代理api.bilibili.com/pgc/view/web/season, 大部分解析服务器不支持代理该接口)
--tv-host <tv-host> 自定义tv端接口请求Host(用于代理api.snm0516.aisee.tv)
--area <area> (hk|tw|th) 使用BiliPlus时必选, 指定BiliPlus area
--config-file <config-file> 读取指定的BBDown本地配置文件(默认为: BBDown.config)
--version Show version information
+36 -119
View File
@@ -118,7 +118,10 @@ func (u *UIUpdater) downloadUI() error {
tmpDir := C.Path.Resolve("downloadUI.tmp")
defer os.RemoveAll(tmpDir)
extractedFolder, err := extract(data, tmpDir)
os.RemoveAll(tmpDir) // cleanup tmp dir before extract
log.Debugln("extractedFolder: %s", tmpDir)
err = extract(data, tmpDir)
if err != nil {
return fmt.Errorf("can't extract compressed file: %w", err)
}
@@ -136,8 +139,8 @@ func (u *UIUpdater) downloadUI() error {
return fmt.Errorf("prepare UI path failed: %w", err)
}
log.Debugln("moveFolder from %s to %s", extractedFolder, u.externalUIPath)
err = moveDir(extractedFolder, u.externalUIPath) // move files from tmp to target
log.Debugln("moveFolder from %s to %s", tmpDir, u.externalUIPath)
err = moveDir(tmpDir, u.externalUIPath) // move files from tmp to target
if err != nil {
return fmt.Errorf("move UI folder failed: %w", err)
}
@@ -154,63 +157,19 @@ func (u *UIUpdater) prepareUIPath() error {
return nil
}
func unzip(data []byte, dest string) (string, error) {
func unzip(data []byte, dest string) error {
r, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
if err != nil {
return "", err
return err
}
// check whether or not only exists singleRoot dir
rootDir := ""
isSingleRoot := true
rootItemCount := 0
for _, f := range r.File {
parts := strings.Split(strings.Trim(f.Name, "/"), "/")
if len(parts) == 0 {
continue
}
if len(parts) == 1 {
isDir := strings.HasSuffix(f.Name, "/")
if !isDir {
isSingleRoot = false
break
}
if rootDir == "" {
rootDir = parts[0]
}
rootItemCount++
}
}
if rootItemCount != 1 {
isSingleRoot = false
}
// build the dir of extraction
var extractedFolder string
if isSingleRoot && rootDir != "" {
// if the singleRoot, use it directly
log.Debugln("Match the singleRoot")
extractedFolder = filepath.Join(dest, rootDir)
log.Debugln("extractedFolder: %s", extractedFolder)
} else {
log.Debugln("Match the multiRoot")
extractedFolder = dest
log.Debugln("extractedFolder: %s", extractedFolder)
}
for _, f := range r.File {
var fpath string
if isSingleRoot && rootDir != "" {
fpath = filepath.Join(dest, f.Name)
} else {
fpath = filepath.Join(extractedFolder, f.Name)
}
fpath := filepath.Join(dest, f.Name)
if !inDest(fpath, dest) {
return "", fmt.Errorf("invalid file path: %s", fpath)
return fmt.Errorf("invalid file path: %s", fpath)
}
info := f.FileInfo()
if info.IsDir() {
@@ -221,128 +180,77 @@ func unzip(data []byte, dest string) (string, error) {
continue // disallow symlink
}
if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
return "", err
return err
}
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
return "", err
return err
}
rc, err := f.Open()
if err != nil {
return "", err
return err
}
_, err = io.Copy(outFile, rc)
outFile.Close()
rc.Close()
if err != nil {
return "", err
return err
}
}
return extractedFolder, nil
return nil
}
func untgz(data []byte, dest string) (string, error) {
func untgz(data []byte, dest string) error {
gzr, err := gzip.NewReader(bytes.NewReader(data))
if err != nil {
return "", err
return err
}
defer gzr.Close()
tr := tar.NewReader(gzr)
rootDir := ""
isSingleRoot := true
rootItemCount := 0
for {
header, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return "", err
}
parts := strings.Split(cleanTarPath(header.Name), string(os.PathSeparator))
if len(parts) == 0 {
continue
}
if len(parts) == 1 {
isDir := header.Typeflag == tar.TypeDir
if !isDir {
isSingleRoot = false
break
}
if rootDir == "" {
rootDir = parts[0]
}
rootItemCount++
}
}
if rootItemCount != 1 {
isSingleRoot = false
}
_ = gzr.Reset(bytes.NewReader(data))
tr = tar.NewReader(gzr)
var extractedFolder string
if isSingleRoot && rootDir != "" {
log.Debugln("Match the singleRoot")
extractedFolder = filepath.Join(dest, rootDir)
log.Debugln("extractedFolder: %s", extractedFolder)
} else {
log.Debugln("Match the multiRoot")
extractedFolder = dest
log.Debugln("extractedFolder: %s", extractedFolder)
}
for {
header, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return "", err
return err
}
var fpath string
if isSingleRoot && rootDir != "" {
fpath = filepath.Join(dest, cleanTarPath(header.Name))
} else {
fpath = filepath.Join(extractedFolder, cleanTarPath(header.Name))
}
fpath := filepath.Join(dest, header.Name)
if !inDest(fpath, dest) {
return "", fmt.Errorf("invalid file path: %s", fpath)
return fmt.Errorf("invalid file path: %s", fpath)
}
switch header.Typeflag {
case tar.TypeDir:
if err = os.MkdirAll(fpath, os.FileMode(header.Mode)); err != nil {
return "", err
return err
}
case tar.TypeReg:
if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
return "", err
return err
}
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.FileMode(header.Mode))
if err != nil {
return "", err
return err
}
if _, err := io.Copy(outFile, tr); err != nil {
outFile.Close()
return "", err
return err
}
outFile.Close()
}
}
return extractedFolder, nil
return nil
}
func extract(data []byte, dest string) (string, error) {
func extract(data []byte, dest string) error {
fileType := detectFileType(data)
log.Debugln("compression Type: %s", fileType)
switch fileType {
@@ -351,7 +259,7 @@ func extract(data []byte, dest string) (string, error) {
case typeTarGzip:
return untgz(data, dest)
default:
return "", fmt.Errorf("unknown or unsupported file type")
return fmt.Errorf("unknown or unsupported file type")
}
}
@@ -393,6 +301,15 @@ func moveDir(src string, dst string) error {
return err
}
if len(dirEntryList) == 1 && dirEntryList[0].IsDir() {
src = filepath.Join(src, dirEntryList[0].Name())
log.Debugln("match the singleRoot: %s", src)
dirEntryList, err = os.ReadDir(src)
if err != nil {
return err
}
}
for _, dirEntry := range dirEntryList {
err = os.Rename(filepath.Join(src, dirEntry.Name()), filepath.Join(dst, dirEntry.Name()))
if err != nil {
+2
View File
@@ -645,6 +645,7 @@ proxies: # socks5
reality-opts:
public-key: xxx
short-id: xxx # optional
support-x25519mlkem768: false # 如果服务端支持可手动设置为true
client-fingerprint: chrome # cannot be empty
- name: "vless-reality-grpc"
@@ -664,6 +665,7 @@ proxies: # socks5
reality-opts:
public-key: CrrQSjAG_YkHLwvM2M-7XkKJilgL5upBKCp0od0tLhE
short-id: 10f897e26c4b9478
support-x25519mlkem768: false # 如果服务端支持可手动设置为true
- name: "vless-ws"
type: vless
+1 -1
View File
@@ -27,7 +27,7 @@ require (
github.com/metacubex/randv2 v0.2.0
github.com/metacubex/sing v0.5.3-0.20250504031621-1f99e54c15b7
github.com/metacubex/sing-mux v0.3.2
github.com/metacubex/sing-quic v0.0.0-20250517090120-462e75d27336
github.com/metacubex/sing-quic v0.0.0-20250520025433-6e556a6bef7a
github.com/metacubex/sing-shadowsocks v0.2.9
github.com/metacubex/sing-shadowsocks2 v0.2.3
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2
+2 -4
View File
@@ -120,10 +120,8 @@ github.com/metacubex/sing v0.5.3-0.20250504031621-1f99e54c15b7 h1:m4nSxvw46JEgxM
github.com/metacubex/sing v0.5.3-0.20250504031621-1f99e54c15b7/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=
github.com/metacubex/sing-mux v0.3.2 h1:nJv52pyRivHcaZJKk2JgxpaVvj1GAXG81scSa9N7ncw=
github.com/metacubex/sing-mux v0.3.2/go.mod h1:3rt1soewn0O6j89GCLmwAQFsq257u0jf2zQSPhTL3Bw=
github.com/metacubex/sing-quic v0.0.0-20250511034158-b46e0e3e81b2 h1:wfmYgtECbEYo1slMtyo+2kMqscYYDSjU/TVgS3018F4=
github.com/metacubex/sing-quic v0.0.0-20250511034158-b46e0e3e81b2/go.mod h1:P1kd57U6XXmXv9PbwWdznUGT0k9bKgFJXF0fEORbIlk=
github.com/metacubex/sing-quic v0.0.0-20250517090120-462e75d27336 h1:5BgpaFkTzkePwF1A8rmhCqgyOMG79BLsAhFR8W8SiRo=
github.com/metacubex/sing-quic v0.0.0-20250517090120-462e75d27336/go.mod h1:JPTpf7fpnojsSuwRJExhSZSy63pVbp3VM39+zj+sAJM=
github.com/metacubex/sing-quic v0.0.0-20250520025433-6e556a6bef7a h1:Ho73vGiB94LmtK5T+tKVwtCNEi/YiHmPjlqpHSAmAVs=
github.com/metacubex/sing-quic v0.0.0-20250520025433-6e556a6bef7a/go.mod h1:JPTpf7fpnojsSuwRJExhSZSy63pVbp3VM39+zj+sAJM=
github.com/metacubex/sing-shadowsocks v0.2.9 h1:2e++13WNN7EGjGtvrGLUzW1xrCdQbW2gIFpgw5GEw00=
github.com/metacubex/sing-shadowsocks v0.2.9/go.mod h1:CJSEGO4FWQAWe+ZiLZxCweGdjRR60A61SIoVjdjQeBA=
github.com/metacubex/sing-shadowsocks2 v0.2.3 h1:v3rNS/5Ywh0NIZ6VU/NmdERQIN5RePzyxCFeQsU4Cx0=
+37 -23
View File
@@ -18,7 +18,6 @@ import (
"sync"
"time"
"github.com/metacubex/mihomo/common/atomic"
"github.com/metacubex/mihomo/common/buf"
"github.com/metacubex/mihomo/common/pool"
"github.com/metacubex/mihomo/component/ech"
@@ -42,16 +41,19 @@ type DialFn = func(ctx context.Context, network, addr string) (net.Conn, error)
type Conn struct {
initFn func() (io.ReadCloser, netAddr, error)
writer io.Writer
writer io.Writer // writer must not nil
closer io.Closer
netAddr
reader io.ReadCloser
once sync.Once
closed atomic.Bool
err error
remain int
br *bufio.Reader
initOnce sync.Once
initErr error
reader io.ReadCloser
br *bufio.Reader
remain int
closeMutex sync.Mutex
closed bool
// deadlines
deadline *time.Timer
}
@@ -65,7 +67,7 @@ type Config struct {
func (g *Conn) initReader() {
reader, addr, err := g.initFn()
if err != nil {
g.err = err
g.initErr = err
if closer, ok := g.writer.(io.Closer); ok {
closer.Close()
}
@@ -73,17 +75,21 @@ func (g *Conn) initReader() {
}
g.netAddr = addr
if !g.closed.Load() {
g.reader = reader
g.br = bufio.NewReader(reader)
} else {
reader.Close()
g.closeMutex.Lock()
defer g.closeMutex.Unlock()
if g.closed { // if g.Close() be called between g.initFn(), direct close the initFn returned reader
_ = reader.Close()
g.initErr = net.ErrClosed
return
}
g.reader = reader
g.br = bufio.NewReader(reader)
}
func (g *Conn) Init() error {
g.once.Do(g.initReader)
return g.err
g.initOnce.Do(g.initReader)
return g.initErr
}
func (g *Conn) Read(b []byte) (n int, err error) {
@@ -100,8 +106,6 @@ func (g *Conn) Read(b []byte) (n int, err error) {
n, err = io.ReadFull(g.br, b[:size])
g.remain -= n
return
} else if g.reader == nil {
return 0, net.ErrClosed
}
// 0x00 grpclength(uint32) 0x0A uleb128 payload
@@ -147,8 +151,8 @@ func (g *Conn) Write(b []byte) (n int, err error) {
buf.Write(b)
_, err = g.writer.Write(buf.Bytes())
if err == io.ErrClosedPipe && g.err != nil {
err = g.err
if err == io.ErrClosedPipe && g.initErr != nil {
err = g.initErr
}
if flusher, ok := g.writer.(http.Flusher); ok {
@@ -170,8 +174,8 @@ func (g *Conn) WriteBuffer(buffer *buf.Buffer) error {
binary.PutUvarint(header[6:], uint64(dataLen))
_, err := g.writer.Write(buffer.Bytes())
if err == io.ErrClosedPipe && g.err != nil {
err = g.err
if err == io.ErrClosedPipe && g.initErr != nil {
err = g.initErr
}
if flusher, ok := g.writer.(http.Flusher); ok {
@@ -186,7 +190,17 @@ func (g *Conn) FrontHeadroom() int {
}
func (g *Conn) Close() error {
g.closed.Store(true)
g.initOnce.Do(func() { // if initReader not called, it should not be run anymore
g.initErr = net.ErrClosed
})
g.closeMutex.Lock()
defer g.closeMutex.Unlock()
if g.closed {
return nil
}
g.closed = true
var errorArr []error
if reader := g.reader; reader != nil {
+554 -466
View File
File diff suppressed because it is too large Load Diff
+7 -7
View File
@@ -110,7 +110,7 @@ semver = "1.0"
# Compression & Encoding
flate2 = "1.0"
zip = "2.0.0"
zip = "3.0.0"
zip-extensions = "0.8.0"
base64 = "0.22"
adler = "1.0.2"
@@ -172,12 +172,12 @@ display-info = "0.5.0" # should be removed after upgrading to tauri v2
# OXC (The Oxidation Compiler)
# We use it to parse and transpile the old script profile to esm based script profile
oxc_parser = "0.68"
oxc_allocator = "0.68"
oxc_span = "0.68"
oxc_ast = "0.68"
oxc_syntax = "0.68"
oxc_ast_visit = "0.68"
oxc_parser = "0.71"
oxc_allocator = "0.71"
oxc_span = "0.71"
oxc_ast = "0.71"
oxc_syntax = "0.71"
oxc_ast_visit = "0.71"
# Lua Integration
mlua = { version = "0.10", features = [
@@ -92,10 +92,16 @@ impl Instance {
.unwrap_or(&ClashCore::ClashPremium))
.into()
};
let data_dir = dirs::app_data_dir()?;
let binary = find_binary_path(&core_type)?;
let config_path = Config::generate_file(ConfigType::Run)?;
let pid_path = dirs::clash_pid_path()?;
let data_dir = camino::Utf8PathBuf::from_path_buf(dirs::app_data_dir()?)
.map_err(|e| anyhow::anyhow!("failed to convert data dir to utf8 path: {:?}", e))?;
let binary = camino::Utf8PathBuf::from_path_buf(find_binary_path(&core_type)?)
.map_err(|e| anyhow::anyhow!("failed to convert binary path to utf8 path: {:?}", e))?;
let config_path = camino::Utf8PathBuf::from_path_buf(Config::generate_file(
ConfigType::Run,
)?)
.map_err(|e| anyhow::anyhow!("failed to convert config path to utf8 path: {:?}", e))?;
let pid_path = camino::Utf8PathBuf::from_path_buf(dirs::clash_pid_path()?)
.map_err(|e| anyhow::anyhow!("failed to convert pid path to utf8 path: {:?}", e))?;
match run_type {
RunType::Normal => {
let instance = Arc::new(
@@ -114,7 +120,7 @@ impl Instance {
})
}
RunType::Service => Ok(Instance::Service {
config_path,
config_path: config_path.into(),
core_type,
}),
RunType::Elevated => {
@@ -1,12 +1,12 @@
import getSystem from '@/utils/get-system'
import { alpha, useTheme } from '@mui/material'
import { Box } from '@mui/material'
import Paper from '@mui/material/Paper'
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import 'allotment/dist/style.css'
import { useAtomValue } from 'jotai'
import { ReactNode, useEffect, useRef } from 'react'
import { atomIsDrawerOnlyIcon } from '@/store'
import { cn } from '@nyanpasu/ui'
import { alpha, cn } from '@nyanpasu/ui'
import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query'
import { TauriEvent, UnlistenFn } from '@tauri-apps/api/event'
import { LayoutControl } from '../layout/layout-control'
@@ -25,7 +25,6 @@ export const AppContainer = ({
children?: ReactNode
isDrawer?: boolean
}) => {
const { palette } = useTheme()
const { data: isMaximized } = useSuspenseQuery({
queryKey: ['isMaximized'],
queryFn: () => appWindow.isMaximized(),
@@ -78,9 +77,11 @@ export const AppContainer = ({
)}
{/* TODO: add a framer motion animation to toggle the maximized state */}
{OS === 'macos' && !isMaximized && (
<div
<Box
className="z-top fixed top-3 left-4 h-8 w-[4.5rem] rounded-full"
style={{ backgroundColor: alpha(palette.primary.main, 0.1) }}
sx={(theme) => ({
backgroundColor: alpha(theme.vars.palette.primary.main, 0.1),
})}
/>
)}
@@ -2,8 +2,8 @@ import { AnimatePresence, motion } from 'framer-motion'
import { useState } from 'react'
import getSystem from '@/utils/get-system'
import { MenuOpen } from '@mui/icons-material'
import { alpha, Backdrop, IconButton } from '@mui/material'
import { cn } from '@nyanpasu/ui'
import { Backdrop, IconButton } from '@mui/material'
import { alpha, cn } from '@nyanpasu/ui'
import AnimatedLogo from '../layout/animated-logo'
import DrawerContent from './drawer-content'
@@ -2,9 +2,9 @@ import { createElement } from 'react'
import { useTranslation } from 'react-i18next'
import { languageQuirks } from '@/utils/language'
import { SvgIconComponent } from '@mui/icons-material'
import { alpha, ListItemButton, ListItemIcon, useTheme } from '@mui/material'
import { Box, ListItemButton, ListItemIcon } from '@mui/material'
import { useSetting } from '@nyanpasu/interface'
import { cn } from '@nyanpasu/ui'
import { alpha, cn } from '@nyanpasu/ui'
import { useMatch, useNavigate } from '@tanstack/react-router'
export const RouteListItem = ({
@@ -19,8 +19,6 @@ export const RouteListItem = ({
onlyIcon?: boolean
}) => {
const { t } = useTranslation()
const { palette } = useTheme()
const match = useMatch({
strict: false,
shouldThrow: false,
@@ -37,24 +35,18 @@ export const RouteListItem = ({
onlyIcon ? '!mx-auto !size-16 !rounded-3xl' : '!rounded-full !pr-14',
)}
sx={[
match
? {
backgroundColor: alpha(palette.primary.main, 0.3),
}
: {
backgroundColor: alpha(palette.background.paper, 0.15),
},
match
? {
'&:hover': {
backgroundColor: alpha(palette.primary.main, 0.5),
},
}
: {
'&:hover': {
backgroundColor: null,
},
},
(theme) => ({
backgroundColor: match
? alpha(theme.vars.palette.primary.main, 0.3)
: alpha(theme.vars.palette.background.paper, 0.15),
}),
(theme) => ({
'&:hover': {
backgroundColor: match
? alpha(theme.vars.palette.primary.main, 0.5)
: null,
},
}),
]}
onClick={() => {
navigate({
@@ -64,22 +56,24 @@ export const RouteListItem = ({
>
<ListItemIcon>
{createElement(icon, {
sx: {
fill: match ? palette.primary.main : undefined,
},
sx: (theme) => ({
fill: match ? theme.vars.palette.primary.main : undefined,
}),
className: onlyIcon ? '!size-8' : undefined,
})}
</ListItemIcon>
{!onlyIcon && (
<div
<Box
className={cn(
'w-full pt-1 pb-1 text-nowrap',
language && languageQuirks[language].drawer.itemClassNames,
)}
style={{ color: match ? palette.primary.main : undefined }}
sx={(theme) => ({
color: match ? theme.vars.palette.primary.main : undefined,
})}
>
{t(`label_${name}`)}
</div>
</Box>
)}
</ListItemButton>
)
@@ -1,5 +1,6 @@
import { InboxRounded } from '@mui/icons-material'
import { alpha, Box, Typography } from '@mui/material'
import { Box, Typography } from '@mui/material'
import { alpha } from '@nyanpasu/ui'
interface Props {
text?: React.ReactNode
@@ -11,14 +12,14 @@ export const BaseEmpty = (props: Props) => {
return (
<Box
sx={({ palette }) => ({
sx={(theme) => ({
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
color: alpha(palette.text.secondary, 0.75),
color: alpha(theme.vars.palette.text.secondary, 0.75),
})}
>
<InboxRounded sx={{ fontSize: '4em' }} />
@@ -1,8 +1,9 @@
import { filesize } from 'filesize'
import { useEffect, useRef, useState } from 'react'
import { Download, Upload } from '@mui/icons-material'
import { darken, lighten, Paper } from '@mui/material'
import { Paper } from '@mui/material'
import { useClashConnections } from '@nyanpasu/interface'
import { darken, lighten } from '@nyanpasu/ui'
export default function ConnectionTotal() {
const {
@@ -65,12 +66,12 @@ export default function ConnectionTotal() {
sx={[
(theme) => ({
color: darken(
theme.palette.primary.main,
theme.vars.palette.primary.main,
downloadHighlight ? 0.9 : 0.3,
),
...theme.applyStyles('dark', {
color: lighten(
theme.palette.primary.main,
theme.vars.palette.primary.main,
downloadHighlight ? 0.2 : 0.9,
),
}),
@@ -94,12 +95,12 @@ export default function ConnectionTotal() {
sx={[
(theme) => ({
color: darken(
theme.palette.primary.main,
theme.vars.palette.primary.main,
uploadHighlight ? 0.9 : 0.3,
),
...theme.applyStyles('dark', {
color: lighten(
theme.palette.primary.main,
theme.vars.palette.primary.main,
downloadHighlight ? 0.2 : 0.9,
),
}),
@@ -1,21 +1,14 @@
import { useTranslation } from 'react-i18next'
import {
alpha,
FilledInputProps,
TextField,
TextFieldProps,
useTheme,
} from '@mui/material'
import { FilledInputProps, TextField, TextFieldProps } from '@mui/material'
import { alpha } from '@nyanpasu/ui'
export const HeaderSearch = (props: TextFieldProps) => {
const { t } = useTranslation()
const { palette } = useTheme()
const inputProps: Partial<FilledInputProps> = {
sx: {
sx: (theme) => ({
borderRadius: 7,
backgroundColor: alpha(palette.primary.main, 0.1),
backgroundColor: alpha(theme.vars.palette.primary.main, 0.1),
'&::before': {
display: 'none',
@@ -24,7 +17,7 @@ export const HeaderSearch = (props: TextFieldProps) => {
'&::after': {
display: 'none',
},
},
}),
}
return (
@@ -36,7 +29,9 @@ export const HeaderSearch = (props: TextFieldProps) => {
variant="filled"
className="!pb-0"
sx={{ input: { py: 1, fontSize: 14 } }}
InputProps={inputProps}
slotProps={{
input: inputProps,
}}
{...props}
/>
)
@@ -1,23 +1,23 @@
import { useAtomValue } from 'jotai'
import { useTranslation } from 'react-i18next'
import { useColorForDelay } from '@/hooks/theme'
import { useColorSxForDelay } from '@/hooks/theme'
import { atomIsDrawer } from '@/store'
import { Paper } from '@mui/material'
import { Box, Paper } from '@mui/material'
import Grid from '@mui/material/Grid'
import { useSetting } from '@nyanpasu/interface'
function LatencyTag({ name, value }: { name: string; value: number }) {
const { t } = useTranslation()
const color = useColorForDelay(value)
const sx = useColorSxForDelay(value)
return (
<div className="flex justify-between gap-1">
<div className="font-bold">{name}:</div>
<div className="truncate" style={{ color }}>
<Box className="truncate" sx={sx}>
{value ? `${value.toFixed(0)} ms` : t('Timeout')}
</div>
</Box>
</div>
)
}
@@ -6,20 +6,25 @@ import { useTranslation } from 'react-i18next'
import useSWR from 'swr'
import { atomIsDrawer } from '@/store'
import {
alpha,
Box,
CircularProgress,
Paper,
SxProps,
Theme,
Tooltip,
useTheme,
} from '@mui/material'
import Grid from '@mui/material/Grid'
import { getCoreStatus, useSystemService } from '@nyanpasu/interface'
import { alpha } from '@nyanpasu/ui'
type Status = {
label: string
sx: SxProps<Theme>
}
export const ServiceShortcuts = () => {
const { t } = useTranslation()
const { palette } = useTheme()
const isDrawer = useAtomValue(atomIsDrawer)
const {
@@ -32,19 +37,29 @@ export const ServiceShortcuts = () => {
revalidateOnFocus: false,
})
const status = useMemo(() => {
const status: Status = useMemo(() => {
switch (serviceStatus?.status) {
case 'running': {
return {
label: t('running'),
color: alpha(palette.success[palette.mode], 0.3),
sx: (theme) => ({
backgroundColor: alpha(theme.vars.palette.success.light, 0.3),
...theme.applyStyles('dark', {
backgroundColor: alpha(theme.vars.palette.success.dark, 0.3),
}),
}),
}
}
case 'stopped': {
return {
label: t('stopped'),
color: alpha(palette.error[palette.mode], 0.3),
sx: (theme) => ({
backgroundColor: alpha(theme.vars.palette.error.light, 0.3),
...theme.applyStyles('dark', {
backgroundColor: alpha(theme.vars.palette.error.dark, 0.3),
}),
}),
}
}
@@ -52,24 +67,18 @@ export const ServiceShortcuts = () => {
default: {
return {
label: t('not_installed'),
color:
palette.mode === 'light'
? palette.grey[100]
: palette.background.paper,
sx: (theme) => ({
backgroundColor: theme.vars.palette.grey[100],
...theme.applyStyles('dark', {
backgroundColor: theme.vars.palette.background.paper,
}),
}),
}
}
}
}, [
serviceStatus,
t,
palette.success,
palette.mode,
palette.error,
palette.grey,
palette.background.paper,
])
}, [serviceStatus, t])
const coreStatus = useMemo(() => {
const coreStatus: Status = useMemo(() => {
const status = coreStatusSWR.data || [{ Stopped: null }, 0, 'normal']
if (
isObject(status[0]) &&
@@ -81,16 +90,26 @@ export const ServiceShortcuts = () => {
!!Stopped && Stopped.trim()
? t('stopped_reason', { reason: Stopped })
: t('stopped'),
color: alpha(palette.success[palette.mode], 0.3),
sx: (theme) => ({
backgroundColor: alpha(theme.vars.palette.success.light, 0.3),
...theme.applyStyles('dark', {
backgroundColor: alpha(theme.vars.palette.success.dark, 0.3),
}),
}),
}
}
return {
label: t('service_shortcuts.core_started_by', {
by: t(status[2] === 'normal' ? 'UI' : 'service'),
}),
color: alpha(palette.success[palette.mode], 0.3),
sx: (theme) => ({
backgroundColor: alpha(theme.vars.palette.success.light, 0.3),
...theme.applyStyles('dark', {
backgroundColor: alpha(theme.vars.palette.success.dark, 0.3),
}),
}),
}
}, [coreStatusSWR.data, palette.mode, palette.success, t])
}, [coreStatusSWR.data, t])
return (
<Grid
@@ -109,17 +128,17 @@ export const ServiceShortcuts = () => {
</div>
<div className="flex w-full flex-col gap-2">
<div
<Box
className="flex w-full justify-center gap-[2px] rounded-2xl py-2"
style={{ backgroundColor: status.color }}
sx={status.sx}
>
<div>{t('service_shortcuts.service_status')}</div>
<div>{t(status.label)}</div>
</div>
</Box>
<div
<Box
className="flex w-full justify-center gap-[2px] rounded-2xl py-2"
style={{ backgroundColor: coreStatus.color }}
sx={coreStatus.sx}
>
<div>{t('service_shortcuts.core_status')}</div>
<Tooltip
@@ -132,7 +151,7 @@ export const ServiceShortcuts = () => {
>
<div>{coreStatus.label}</div>
</Tooltip>
</div>
</Box>
</div>
</>
) : (
@@ -8,9 +8,9 @@ import {
PushPin,
PushPinOutlined,
} from '@mui/icons-material'
import { alpha, Button, ButtonProps, useTheme } from '@mui/material'
import { Button, ButtonProps } from '@mui/material'
import { saveWindowSizeState, useSetting } from '@nyanpasu/interface'
import { cn } from '@nyanpasu/ui'
import { alpha, cn } from '@nyanpasu/ui'
import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query'
import { listen, TauriEvent, UnlistenFn } from '@tauri-apps/api/event'
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
@@ -19,15 +19,13 @@ import { platform as getPlatform } from '@tauri-apps/plugin-os'
const appWindow = getCurrentWebviewWindow()
const CtrlButton = (props: ButtonProps) => {
const { palette } = useTheme()
return (
<Button
className="!size-8 !min-w-0"
sx={{
backgroundColor: alpha(palette.primary.main, 0.1),
sx={(theme) => ({
backgroundColor: alpha(theme.vars.palette.primary.main, 0.1),
svg: { transform: 'scale(0.9)' },
}}
})}
{...props}
/>
)
@@ -1,5 +1,6 @@
import { useTranslation } from 'react-i18next'
import { alpha, FilledInputProps, TextField, useTheme } from '@mui/material'
import { FilledInputProps, TextField } from '@mui/material'
import { alpha } from '@nyanpasu/ui'
import { useLogContext } from './log-provider'
export const LogFilter = () => {
@@ -7,17 +8,15 @@ export const LogFilter = () => {
const { filterText, setFilterText } = useLogContext()
const { palette } = useTheme()
const inputProps: Partial<FilledInputProps> = {
sx: {
sx: (theme) => ({
borderRadius: 7,
backgroundColor: alpha(palette.primary.main, 0.1),
backgroundColor: alpha(theme.vars.palette.primary.main, 0.1),
fieldset: {
border: 'none',
},
},
}),
}
return (
@@ -30,7 +29,9 @@ export const LogFilter = () => {
onChange={(e) => setFilterText(e.target.value)}
className="!pb-0"
sx={{ input: { py: 1, fontSize: 14 } }}
InputProps={inputProps}
slotProps={{
input: inputProps,
}}
/>
)
}
@@ -1,11 +1,23 @@
import { useAsyncEffect } from 'ahooks'
import { CSSProperties, useState } from 'react'
import { formatAnsi } from '@/utils/shiki'
import { useTheme } from '@mui/material'
import { Box, SxProps, Theme, useColorScheme } from '@mui/material'
import { LogMessage } from '@nyanpasu/interface'
import { cn } from '@nyanpasu/ui'
import styles from './log-item.module.scss'
const colorMapping: { [key: string]: SxProps<Theme> } = {
error: (theme) => ({
color: theme.vars.palette.error.main,
}),
warning: (theme) => ({
color: theme.vars.palette.warning.main,
}),
info: (theme) => ({
color: theme.vars.palette.info.main,
}),
}
export const LogItem = ({
value,
className,
@@ -13,15 +25,9 @@ export const LogItem = ({
value: LogMessage
className?: string
}) => {
const { palette } = useTheme()
const [payload, setPayload] = useState(value.payload)
const colorMapping: { [key: string]: string } = {
error: palette.error.main,
warning: palette.warning.main,
info: palette.info.main,
}
const { mode } = useColorScheme()
useAsyncEffect(async () => {
setPayload(await formatAnsi(value.payload))
@@ -34,19 +40,18 @@ export const LogItem = ({
<div className="flex gap-2">
<span className="font-thin">{value.time}</span>
<span
<Box
component="span"
className="inline-block font-semibold uppercase"
style={{
color: colorMapping[value.type],
}}
sx={colorMapping[value.type]}
>
{value.type}
</span>
</Box>
</div>
<div className="pb-2 text-wrap">
<div
className={cn(styles.item, palette.mode === 'dark' && styles.dark)}
className={cn(styles.item, mode === 'dark' && styles.dark)}
style={
{
'--item-font': 'var(--font-mono)',
@@ -1,12 +1,11 @@
import { useState } from 'react'
import { alpha, Button, Menu, MenuItem, useTheme } from '@mui/material'
import { Button, Menu, MenuItem } from '@mui/material'
import { alpha } from '@nyanpasu/ui'
import { useLogContext } from './log-provider'
export const LogLevel = () => {
const { logLevel, setLogLevel } = useLogContext()
const { palette } = useTheme()
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
const handleClick = (value: string) => {
@@ -25,10 +24,10 @@ export const LogLevel = () => {
<>
<Button
size="small"
sx={{
sx={(theme) => ({
textTransform: 'none',
backgroundColor: alpha(palette.primary.main, 0.1),
}}
backgroundColor: alpha(theme.vars.palette.primary.main, 0.1),
})}
onClick={(e) => setAnchorEl(e.currentTarget)}
>
{mapping[logLevel]}
@@ -2,16 +2,9 @@ import { Reorder } from 'framer-motion'
import { memo, PointerEvent, useRef, useState, useTransition } from 'react'
import { useTranslation } from 'react-i18next'
import { Menu as MenuIcon } from '@mui/icons-material'
import {
alpha,
Button,
ListItemButton,
Menu,
MenuItem,
useTheme,
} from '@mui/material'
import { Button, ListItemButton, Menu, MenuItem } from '@mui/material'
import { ProfileQueryResultItem } from '@nyanpasu/interface'
import { cleanDeepClickEvent } from '@nyanpasu/ui'
import { alpha, cleanDeepClickEvent } from '@nyanpasu/ui'
const longPressDelay = 200
@@ -28,8 +21,6 @@ export const ChainItem = memo(function ChainItem({
}) {
const { t } = useTranslation()
const { palette } = useTheme()
const [isPending, startTransition] = useTransition()
const handleClick = () => {
@@ -89,24 +80,18 @@ export const ChainItem = memo(function ChainItem({
{
borderRadius: 4,
},
selected
? {
backgroundColor: alpha(palette.primary.main, 0.3),
}
: {
backgroundColor: alpha(palette.secondary.main, 0.1),
},
selected
? {
'&:hover': {
backgroundColor: alpha(palette.primary.main, 0.5),
},
}
: {
'&:hover': {
backgroundColor: null,
},
},
(theme) => ({
backgroundColor: selected
? alpha(theme.vars.palette.primary.main, 0.3)
: alpha(theme.vars.palette.secondary.main, 0.1),
}),
(theme) => ({
'&:hover': {
backgroundColor: selected
? alpha(theme.vars.palette.primary.main, 0.5)
: null,
},
}),
]}
onClick={handleClick}
disabled={isPending}
@@ -1,22 +1,21 @@
import { alpha, useTheme } from '@mui/material'
import { Box } from '@mui/material'
import { alpha } from '@nyanpasu/ui'
import { getLanguage, ProfileType } from '../utils'
export const LanguageChip = ({ type }: { type: ProfileType }) => {
const { palette } = useTheme()
const lang = getLanguage(type, true)
return (
lang && (
<div
<Box
className="my-auto rounded-full px-2 py-0.5 text-center text-sm font-bold"
style={{
backgroundColor: alpha(palette.primary.main, 0.1),
color: palette.primary.main,
}}
sx={(theme) => ({
backgroundColor: alpha(theme.vars.palette.primary.main, 0.1),
color: theme.vars.palette.primary.main,
})}
>
{lang}
</div>
</Box>
)
)
}
@@ -6,8 +6,9 @@ import { useTranslation } from 'react-i18next'
import { formatError } from '@/utils'
import { message } from '@/utils/notification'
import { Add } from '@mui/icons-material'
import { alpha, ListItemButton, useTheme } from '@mui/material'
import { ListItemButton } from '@mui/material'
import { ProfileQueryResultItem, useProfile } from '@nyanpasu/interface'
import { alpha } from '@nyanpasu/ui'
import { ClashProfile, filterProfiles } from '../utils'
import ChainItem from './chain-item'
import { atomChainsSelected, atomGlobalChainCurrent } from './store'
@@ -19,8 +20,6 @@ export interface SideChainProps {
export const SideChain = ({ onChainEdit }: SideChainProps) => {
const { t } = useTranslation()
const { palette } = useTheme()
const isGlobalChainCurrent = useAtomValue(atomGlobalChainCurrent)
const currentProfileUid = useAtomValue(atomChainsSelected)
@@ -103,10 +102,10 @@ export const SideChain = ({ onChainEdit }: SideChainProps) => {
<ListItemButton
className="!mt-2 !mb-2 flex justify-center gap-2"
sx={{
backgroundColor: alpha(palette.secondary.main, 0.1),
sx={(theme) => ({
backgroundColor: alpha(theme.vars.palette.secondary.main, 0.1),
borderRadius: 4,
}}
})}
onClick={() => onChainEdit()}
>
<Add color="primary" />
@@ -15,7 +15,6 @@ import {
Update,
} from '@mui/icons-material'
import {
alpha,
Badge,
Button,
Chip,
@@ -24,18 +23,16 @@ import {
MenuItem,
Paper,
Tooltip,
useTheme,
} from '@mui/material'
import {
Profile,
ProfileQueryResultItem,
RemoteProfile,
RemoteProfileOptions,
RemoteProfileOptionsBuilder,
useClashConnections,
useProfile,
} from '@nyanpasu/interface'
import { cleanDeepClickEvent, cn } from '@nyanpasu/ui'
import { alpha, cleanDeepClickEvent, cn } from '@nyanpasu/ui'
import { ProfileDialog } from './profile-dialog'
import { GlobalUpdatePendingContext } from './provider'
@@ -59,8 +56,6 @@ export const ProfileItem = memo(function ProfileItem({
}: ProfileItemProps) {
const { t } = useTranslation()
const { palette } = useTheme()
const { deleteConnections } = useClashConnections()
const { upsert } = useProfile()
@@ -227,13 +222,11 @@ export const ProfileItem = memo(function ProfileItem({
{
borderRadius: 6,
},
selected
? {
backgroundColor: alpha(palette.primary.main, 0.2),
}
: {
backgroundColor: null,
},
(theme) => ({
backgroundColor: selected
? alpha(theme.vars.palette.primary.main, 0.2)
: null,
}),
]}
>
<div
@@ -252,9 +245,9 @@ export const ProfileItem = memo(function ProfileItem({
{selected && (
<FiberManualRecord
className="top-0 mr-auto !size-3 animate-bounce"
sx={{
fill: palette.success.main,
}}
sx={(theme) => ({
fill: theme.vars.palette.success.main,
})}
/>
)}
@@ -2,22 +2,19 @@ import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { ClearRounded, ContentCopyRounded, Download } from '@mui/icons-material'
import {
alpha,
CircularProgress,
FilledInputProps,
IconButton,
TextField,
Tooltip,
useTheme,
} from '@mui/material'
import { useProfile } from '@nyanpasu/interface'
import { alpha } from '@nyanpasu/ui'
import { readText } from '@tauri-apps/plugin-clipboard-manager'
export const QuickImport = () => {
const { t } = useTranslation()
const { palette } = useTheme()
const [url, setUrl] = useState('')
const [loading, setLoading] = useState(false)
@@ -82,14 +79,14 @@ export const QuickImport = () => {
}
const inputProps: Partial<FilledInputProps> = {
sx: {
sx: (theme) => ({
borderRadius: 7,
backgroundColor: alpha(palette.primary.main, 0.1),
backgroundColor: alpha(theme.vars.palette.primary.main, 0.1),
fieldset: {
border: 'none',
},
},
}),
endAdornment: endAdornment(),
}
@@ -104,7 +101,9 @@ export const QuickImport = () => {
onChange={(e) => setUrl(e.target.value)}
onKeyDown={(e) => url !== '' && e.key === 'Enter' && handleImport()}
sx={{ input: { py: 1, px: 2 } }}
InputProps={inputProps}
slotProps={{
input: inputProps,
}}
/>
)
}
@@ -2,14 +2,8 @@ import { useDebounceFn, useLockFn } from 'ahooks'
import { memo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Bolt, Done } from '@mui/icons-material'
import {
alpha,
Button,
CircularProgress,
Tooltip,
useTheme,
} from '@mui/material'
import { cn } from '@nyanpasu/ui'
import { Button, CircularProgress, Tooltip } from '@mui/material'
import { alpha, cn } from '@nyanpasu/ui'
export const DelayButton = memo(function DelayButton({
onClick,
@@ -18,8 +12,6 @@ export const DelayButton = memo(function DelayButton({
}) {
const { t } = useTranslation()
const { palette } = useTheme()
const [loading, setLoading] = useState(false)
const [mounted, setMounted] = useState(false)
@@ -48,21 +40,21 @@ export const DelayButton = memo(function DelayButton({
<Tooltip title={t('Latency check')}>
<Button
className="!fixed right-8 bottom-8 z-10 size-16 !rounded-2xl backdrop-blur"
sx={{
sx={(theme) => ({
boxShadow: 8,
backgroundColor: alpha(
palette[isSuccess ? 'success' : 'primary'].main,
theme.vars.palette[isSuccess ? 'success' : 'primary'].main,
isSuccess ? 0.7 : 0.3,
),
'&:hover': {
backgroundColor: alpha(palette.primary.main, 0.45),
backgroundColor: alpha(theme.vars.palette.primary.main, 0.45),
},
'&.MuiButton-loading': {
backgroundColor: alpha(palette.primary.main, 0.15),
backgroundColor: alpha(theme.vars.palette.primary.main, 0.15),
},
}}
})}
onClick={handleClick}
>
<Bolt
@@ -1,5 +1,6 @@
import { memo, useState } from 'react'
import { useColorForDelay } from '@/hooks/theme'
import { useColorSxForDelay } from '@/hooks/theme'
import { mergeSxProps } from '@/utils/mui-theme'
import { Bolt } from '@mui/icons-material'
import { CircularProgress } from '@mui/material'
import { cn } from '@nyanpasu/ui'
@@ -29,10 +30,12 @@ export const DelayChip = memo(function DelayChip({
return (
<FeatureChip
className={cn(className, loading && '!visible')}
sx={{
ml: 'auto',
color: useColorForDelay(delay),
}}
sx={mergeSxProps(
{
ml: 'auto',
},
useColorSxForDelay(delay),
)}
label={
<>
<span
@@ -5,16 +5,14 @@ import { Virtualizer } from 'virtua'
import { proxyGroupAtom } from '@/store'
import { proxiesFilterAtom } from '@/store/proxies'
import {
alpha,
ListItem,
ListItemButton,
ListItemButtonProps,
ListItemIcon,
ListItemText,
useTheme,
} from '@mui/material'
import { getServerPort, useClashProxies } from '@nyanpasu/interface'
import { LazyImage } from '@nyanpasu/ui'
import { alpha, LazyImage } from '@nyanpasu/ui'
const IconRender = memo(function IconRender({ icon }: { icon: string }) {
const {
@@ -55,8 +53,6 @@ export const GroupList = ({
}: GroupListProps) => {
const { data } = useClashProxies()
const { palette } = useTheme()
const [proxyGroup, setProxyGroup] = useAtom(proxyGroupAtom)
const proxiesFilter = useAtomValue(proxiesFilterAtom)
const deferredProxiesFilter = useDeferredValue(proxiesFilter)
@@ -97,13 +93,11 @@ export const GroupList = ({
selected={selected}
onClick={() => handleSelect(index)}
sx={[
selected
? {
backgroundColor: `${alpha(palette.primary.main, 0.3)} !important`,
}
: {
backgroundColor: null,
},
(theme) => ({
backgroundColor: selected
? `${alpha(theme.vars.palette.primary.main, 0.3)} !important`
: null,
}),
]}
{...listItemButtonProps}
>
@@ -1,9 +1,8 @@
import { useLockFn } from 'ahooks'
import { CSSProperties, memo, useMemo } from 'react'
import { alpha, useTheme } from '@mui/material'
import Box from '@mui/material/Box'
import { ClashProxiesQueryProxyItem } from '@nyanpasu/interface'
import { cn } from '@nyanpasu/ui'
import { alpha, cn } from '@nyanpasu/ui'
import { PaperSwitchButton } from '../setting/modules/system-proxy'
import DelayChip from './delay-chip'
import FeatureChip from './feature-chip'
@@ -21,8 +20,6 @@ export const NodeCard = memo(function NodeCard({
disabled?: boolean
style?: CSSProperties
}) {
const { palette } = useTheme()
const delay = useMemo(() => filterDelay(node.history), [node.history])
const checked = node.name === now
@@ -44,13 +41,16 @@ export const NodeCard = memo(function NodeCard({
disabled={disabled}
style={style}
className={cn(styles.Card, delay === -1 && styles.NoDelay)}
sxPaper={{
sxPaper={(theme) => ({
backgroundColor: checked
? alpha(palette.primary.main, 0.3)
: palette.mode === 'dark'
? alpha(palette.grey[900], 0.3)
: palette.grey[100],
}}
? alpha(theme.vars.palette.primary.main, 0.3)
: theme.vars.palette.grey[100],
...theme.applyStyles('dark', {
backgroundColor: checked
? alpha(theme.vars.palette.primary.main, 0.3)
: theme.vars.palette.grey[900],
}),
})}
>
<Box width="100%" display="flex" gap={0.5}>
<FeatureChip label={node.type} />
@@ -1,20 +1,19 @@
import { useTranslation } from 'react-i18next'
import { Radar } from '@mui/icons-material'
import { alpha, Button, Tooltip, useTheme } from '@mui/material'
import { Button, Tooltip } from '@mui/material'
import { alpha } from '@nyanpasu/ui'
export const ScrollCurrentNode = ({ onClick }: { onClick?: () => void }) => {
const { t } = useTranslation()
const { palette } = useTheme()
return (
<Tooltip title={t('Locate')}>
<Button
size="small"
className="!size-8 !min-w-0"
sx={{
backgroundColor: alpha(palette.primary.main, 0.1),
}}
sx={(theme) => ({
backgroundColor: alpha(theme.vars.palette.primary.main, 0.1),
})}
onClick={onClick}
>
<Radar />
@@ -2,13 +2,12 @@ import { useAtom } from 'jotai'
import { memo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { proxyGroupSortAtom } from '@/store'
import { alpha, Button, Menu, MenuItem, useTheme } from '@mui/material'
import { Button, Menu, MenuItem } from '@mui/material'
import { alpha } from '@nyanpasu/ui'
export const SortSelector = memo(function SortSelector() {
const { t } = useTranslation()
const { palette } = useTheme()
const [proxyGroupSort, setProxyGroupSort] = useAtom(proxyGroupSortAtom)
type SortType = typeof proxyGroupSort
@@ -31,10 +30,10 @@ export const SortSelector = memo(function SortSelector() {
<Button
size="small"
className="!px-2"
sx={{
sx={(theme) => ({
textTransform: 'none',
backgroundColor: alpha(palette.primary.main, 0.1),
}}
backgroundColor: alpha(theme.vars.palette.primary.main, 0.1),
})}
onClick={(e) => setAnchorEl(e.currentTarget)}
>
{t(tmaps[proxyGroupSort])}
@@ -1,4 +1,4 @@
import { useTheme } from '@mui/material'
import { Box, SxProps, Theme } from '@mui/material'
import { ClashRule } from '@nyanpasu/interface'
interface Props {
@@ -6,26 +6,36 @@ interface Props {
value: ClashRule
}
const COLOR = [
(theme) => ({
color: theme.vars.palette.primary.main,
}),
(theme) => ({
color: theme.vars.palette.secondary.main,
}),
(theme) => ({
color: theme.vars.palette.info.main,
}),
(theme) => ({
color: theme.vars.palette.warning.main,
}),
(theme) => ({
color: theme.vars.palette.success.main,
}),
] satisfies SxProps<Theme>[]
const RuleItem = ({ index, value }: Props) => {
const { palette } = useTheme()
const COLOR = [
palette.primary.main,
palette.secondary.main,
palette.info.main,
palette.warning.main,
palette.success.main,
]
const parseColor = (text: string) => {
const parseColorSx: (text: string) => SxProps<Theme> = (text) => {
const TYPE = {
reject: ['REJECT', 'REJECT-DROP'],
direct: ['DIRECT'],
}
if (TYPE.reject.includes(text)) return palette.error.main
if (TYPE.reject.includes(text))
return (theme) => ({ color: theme.vars.palette.error.main })
if (TYPE.direct.includes(text)) return palette.text.primary
if (TYPE.direct.includes(text))
return (theme) => ({ color: theme.vars.palette.text.primary })
let sum = 0
@@ -38,24 +48,24 @@ const RuleItem = ({ index, value }: Props) => {
return (
<div className="flex p-2 pr-7 pl-7 select-text">
<div style={{ color: palette.text.secondary }} className="min-w-14">
<Box
sx={(theme) => ({ color: theme.vars.palette.text.secondary })}
className="min-w-14"
>
{index + 1}
</div>
</Box>
<div className="flex flex-col gap-1">
<div style={{ color: palette.text.primary }}>
<Box sx={(theme) => ({ color: theme.vars.palette.text.primary })}>
{value.payload || '-'}
</div>
</Box>
<div className="flex gap-8">
<div className="min-w-40 text-sm">{value.type}</div>
<div
className="text-s text-sm"
style={{ color: parseColor(value.proxy) }}
>
<Box className="text-s text-sm" sx={parseColorSx(value.proxy)}>
{value.proxy}
</div>
</Box>
</div>
</div>
</div>
@@ -10,10 +10,9 @@ import { message } from '@/utils/notification'
import parseTraffic from '@/utils/parse-traffic'
import FiberManualRecord from '@mui/icons-material/FiberManualRecord'
import Update from '@mui/icons-material/Update'
import { Button } from '@mui/material'
import { Box, Button } from '@mui/material'
import ListItem from '@mui/material/ListItem'
import ListItemButton from '@mui/material/ListItemButton'
import { alpha, useTheme } from '@mui/material/styles'
import Tooltip from '@mui/material/Tooltip'
import {
ClashCore,
@@ -22,7 +21,7 @@ import {
inspectUpdater,
useClashCores,
} from '@nyanpasu/interface'
import { cleanDeepClickEvent, cn } from '@nyanpasu/ui'
import { alpha, cleanDeepClickEvent, cn } from '@nyanpasu/ui'
export const getImage = (core: ClashCore) => {
switch (core) {
@@ -56,8 +55,6 @@ const CardProgress = ({
data?: InspectUpdater
show?: boolean
}) => {
const { palette } = useTheme()
const parsedState = () => {
if (data?.downloader?.state) {
return 'waiting'
@@ -69,14 +66,15 @@ const CardProgress = ({
}
return (
<motion.div
<Box
component={motion.div}
className={cn(
'absolute top-0 left-0 z-10 h-full w-full rounded-2xl backdrop-blur',
'flex flex-col items-center justify-center gap-2',
)}
style={{
backgroundColor: alpha(palette.primary.main, 0.3),
}}
sx={(theme) => ({
backgroundColor: alpha(theme.vars.palette.primary.main, 0.3),
})}
animate={show ? 'open' : 'closed'}
initial={{ opacity: 0 }}
variants={{
@@ -92,12 +90,12 @@ const CardProgress = ({
},
}}
>
<div
<Box
className="absolute left-0 h-full rounded-2xl transition-all"
style={{
backgroundColor: alpha(palette.primary.main, 0.3),
sx={(theme) => ({
backgroundColor: alpha(theme.vars.palette.primary.main, 0.3),
width: `${calcProgress(data) < 10 ? 10 : calcProgress(data)}%`,
}}
})}
/>
<div className="truncate capitalize">{parsedState()}</div>
@@ -106,7 +104,7 @@ const CardProgress = ({
{calcProgress(data).toFixed(0)}%{''}
<span>({parseTraffic(data?.downloader.speed || 0)}/s)</span>
</div>
</motion.div>
</Box>
)
}
@@ -138,8 +136,6 @@ export const ClashCoreItem = ({
}: ClashCoreItemProps) => {
const { t } = useTranslation()
const { palette } = useTheme()
const { query, updateCore } = useClashCores()
const haveNewVersion = data.latestVersion
@@ -204,14 +200,14 @@ export const ClashCoreItem = ({
<ListItem sx={{ pl: 0, pr: 0 }}>
<ListItemButton
className="!relative !p-0"
sx={{
sx={(theme) => ({
borderRadius: '16px',
backgroundColor: alpha(palette.background.paper, 0.3),
backgroundColor: alpha(theme.vars.palette.background.paper, 0.3),
'&.Mui-selected': {
backgroundColor: alpha(palette.primary.main, 0.3),
backgroundColor: alpha(theme.vars.palette.primary.main, 0.3),
},
}}
})}
selected={selected}
onClick={() => {
if (!downloadState) {
@@ -230,7 +226,10 @@ export const ClashCoreItem = ({
{haveNewVersion && (
<FiberManualRecord
sx={{ height: 10, fill: palette.success.main }}
sx={(theme) => ({
height: 10,
fill: theme.vars.palette.success.main,
})}
/>
)}
</div>
@@ -2,7 +2,6 @@ import { ChangeEvent, useState } from 'react'
import Marquee from 'react-fast-marquee'
import ArrowForwardIos from '@mui/icons-material/ArrowForwardIos'
import OpenInNewRounded from '@mui/icons-material/OpenInNewRounded'
import { alpha, useTheme } from '@mui/material'
import Box from '@mui/material/Box'
import ButtonBase, { ButtonBaseProps } from '@mui/material/ButtonBase'
import Grid from '@mui/material/Grid'
@@ -12,7 +11,7 @@ import { SwitchProps } from '@mui/material/Switch'
import Tooltip from '@mui/material/Tooltip'
import Typography from '@mui/material/Typography'
import { openThat } from '@nyanpasu/interface'
import { LoadingSwitch } from '@nyanpasu/ui'
import { alpha, LoadingSwitch } from '@nyanpasu/ui'
export interface LabelSwitchProps extends SwitchProps {
label: string
@@ -42,8 +41,6 @@ export const LabelSwitch = ({
onChange,
...props
}: LabelSwitchProps) => {
const { palette } = useTheme()
const [loading, setLoading] = useState(false)
const handleChange = async (
@@ -63,14 +60,14 @@ export const LabelSwitch = ({
return (
<Paper
sx={{
sx={(theme) => ({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: 2,
borderRadius: 6,
backgroundColor: alpha(palette.primary.light, 0.1),
}}
backgroundColor: alpha(theme.vars.palette.primary.light, 0.1),
})}
elevation={0}
>
<Box display="flex" alignItems="center" gap={1}>
@@ -114,8 +111,6 @@ export const ClashFieldItem = ({
fields,
...props
}: ClashFieldItemProps) => {
const { palette } = useTheme()
return (
<Grid
size={{
@@ -125,10 +120,10 @@ export const ClashFieldItem = ({
>
<Paper
elevation={0}
sx={{
sx={(theme) => ({
borderRadius: 6,
backgroundColor: alpha(palette.primary.light, 0.1),
}}
backgroundColor: alpha(theme.vars.palette.primary.light, 0.1),
})}
>
<ButtonBase
sx={{
@@ -7,9 +7,10 @@ import Box from '@mui/material/Box'
import Chip from '@mui/material/Chip'
import IconButton from '@mui/material/IconButton'
import Paper, { PaperProps } from '@mui/material/Paper'
import { alpha, styled } from '@mui/material/styles'
import { styled } from '@mui/material/styles'
import Typography from '@mui/material/Typography'
import { openThat } from '@nyanpasu/interface'
import { alpha } from '@nyanpasu/ui'
/**
* @example
@@ -1,9 +1,9 @@
import { parseHotkey } from '@/utils/parse-hotkey'
import { Dangerous, DeleteRounded } from '@mui/icons-material'
import { alpha, CircularProgress, IconButton, useTheme } from '@mui/material'
import { CircularProgress, IconButton, useTheme } from '@mui/material'
import type {} from '@mui/material/themeCssVarsAugmentation'
import { CSSProperties, useEffect, useRef, useState } from 'react'
import { cn, Kbd } from '@nyanpasu/ui'
import { alpha, cn, Kbd } from '@nyanpasu/ui'
import styles from './hotkey-input.module.scss'
export interface Props extends React.HTMLAttributes<HTMLInputElement> {
@@ -1,19 +1,20 @@
import { memo, ReactNode } from 'react'
import { mergeSxProps } from '@/utils/mui-theme'
import {
alpha,
ButtonBase,
ButtonBaseProps,
Paper,
SxProps,
Theme,
Typography,
useTheme,
} from '@mui/material'
import { alpha } from '@nyanpasu/ui'
export interface PaperButtonProps extends ButtonBaseProps {
label?: string
children?: ReactNode
sxPaper?: SxProps
sxButton?: SxProps
sxPaper?: SxProps<Theme>
sxButton?: SxProps<Theme>
}
export const PaperButton = memo(function PaperButton({
@@ -23,33 +24,35 @@ export const PaperButton = memo(function PaperButton({
sxButton,
...props
}: PaperButtonProps) {
const { palette } = useTheme()
return (
<Paper
elevation={0}
sx={{
borderRadius: 6,
backgroundColor: alpha(palette.primary.main, 0.1),
...sxPaper,
}}
sx={mergeSxProps(
(theme) => ({
borderRadius: 6,
backgroundColor: alpha(theme.vars.palette.primary.main, 0.1),
}),
sxPaper,
)}
>
<ButtonBase
sx={{
borderRadius: 6,
width: '100%',
textAlign: 'start',
padding: 2,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
sx={mergeSxProps(
{
borderRadius: 6,
width: '100%',
textAlign: 'start',
padding: 2,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
'&.Mui-disabled': {
pointerEvents: 'auto',
cursor: 'not-allowed',
'&.Mui-disabled': {
pointerEvents: 'auto',
cursor: 'not-allowed',
},
},
...sxButton,
}}
sxButton,
)}
{...props}
>
{label && (
@@ -1,7 +1,8 @@
import { useControllableValue } from 'ahooks'
import { merge } from 'lodash-es'
import { memo, ReactNode } from 'react'
import { alpha, CircularProgress, SxProps, useTheme } from '@mui/material'
import { mergeSxProps } from '@/utils/mui-theme'
import { CircularProgress, SxProps, Theme } from '@mui/material'
import { alpha } from '@nyanpasu/ui'
import { PaperButton, PaperButtonProps } from './nyanpasu-path'
export interface PaperSwitchButtonProps extends PaperButtonProps {
@@ -11,7 +12,7 @@ export interface PaperSwitchButtonProps extends PaperButtonProps {
disableLoading?: boolean
children?: ReactNode
onClick?: () => Promise<void> | void
sxPaper?: SxProps
sxPaper?: SxProps<Theme>
}
export const PaperSwitchButton = memo(function PaperSwitchButton({
@@ -24,8 +25,6 @@ export const PaperSwitchButton = memo(function PaperSwitchButton({
sxPaper,
...props
}: PaperSwitchButtonProps) {
const { palette } = useTheme()
const [pending, setPending] = useControllableValue<boolean>(
{ loading },
{
@@ -48,15 +47,17 @@ export const PaperSwitchButton = memo(function PaperSwitchButton({
return (
<PaperButton
label={label}
sxPaper={merge(
{
sxPaper={mergeSxProps(
(theme) => ({
backgroundColor: checked
? alpha(palette.primary.main, 0.1)
: palette.mode === 'dark'
? palette.common.black
: palette.grey[100],
cursor: pending ? 'progress' : 'none',
},
? alpha(theme.vars.palette.primary.main, 0.1)
: theme.vars.palette.grey[100],
...theme.applyStyles('dark', {
backgroundColor: checked
? alpha(theme.vars.palette.primary.main, 0.1)
: theme.vars.palette.common.black,
}),
}),
sxPaper,
)}
sxButton={{
@@ -7,18 +7,9 @@ import { checkUpdate, useUpdaterPlatformSupported } from '@/hooks/use-updater'
import { UpdaterInstanceAtom } from '@/store/updater'
import { formatError } from '@/utils'
import { message } from '@/utils/notification'
import {
alpha,
Box,
Button,
List,
ListItem,
Paper,
Typography,
useTheme,
} from '@mui/material'
import { Box, Button, List, ListItem, Paper, Typography } from '@mui/material'
import { useSetting } from '@nyanpasu/interface'
import { BaseCard } from '@nyanpasu/ui'
import { alpha, BaseCard } from '@nyanpasu/ui'
import { version } from '@root/package.json'
import { LabelSwitch } from './modules/clash-field'
@@ -39,8 +30,6 @@ const AutoCheckUpdate = () => {
export const SettingNyanpasuVersion = () => {
const { t } = useTranslation()
const { palette } = useTheme()
const [loading, setLoading] = useState(false)
const setUpdaterInstance = useSetAtom(UpdaterInstanceAtom)
@@ -78,13 +67,13 @@ export const SettingNyanpasuVersion = () => {
<ListItem sx={{ pl: 0, pr: 0 }}>
<Paper
elevation={0}
sx={{
sx={(theme) => ({
mt: 1,
padding: 2,
backgroundColor: alpha(palette.primary.main, 0.1),
backgroundColor: alpha(theme.vars.palette.primary.main, 0.1),
borderRadius: 6,
width: '100%',
}}
})}
>
<Box
display="flex"
@@ -1,25 +1,25 @@
import { useTheme } from '@mui/material'
import { SxProps, Theme } from '@mui/material'
export const useColorForDelay = (delay: number): string => {
const { palette } = useTheme()
const delayColorMapping: { [key: string]: SxProps<Theme> } = {
'-1': (theme) => ({ color: theme.vars.palette.text.primary }),
'0': (theme) => ({ color: theme.vars.palette.text.secondary }),
'1': (theme) => ({ color: theme.vars.palette.text.secondary }),
'500': (theme) => ({ color: theme.vars.palette.success.main }),
'2000': (theme) => ({ color: theme.vars.palette.warning.main }),
'10000': (theme) => ({ color: theme.vars.palette.error.main }),
}
const delayColorMapping: { [key: string]: string } = {
'-1': palette.text.primary,
'0': palette.text.secondary,
'1': palette.text.secondary,
'500': palette.success.main,
'2000': palette.warning.main,
'10000': palette.error.main,
}
let color: string = palette.text.secondary
export const useColorSxForDelay = (delay: number): SxProps<Theme> => {
let sx: SxProps<Theme> = (theme: Theme) => ({
color: theme.vars.palette.text.secondary,
})
for (const key in delayColorMapping) {
if (delay <= parseInt(key)) {
color = delayColorMapping[key]
sx = delayColorMapping[key]
break
}
}
return color
return sx
}
@@ -15,19 +15,13 @@ import SortSelector from '@/components/proxies/sort-selector'
import { proxyGroupAtom } from '@/store'
import { proxiesFilterAtom } from '@/store/proxies'
import { Check } from '@mui/icons-material'
import {
alpha,
TextField,
ToggleButton,
ToggleButtonGroup,
useTheme,
} from '@mui/material'
import { TextField, ToggleButton, ToggleButtonGroup } from '@mui/material'
import {
ProxyGroupItem,
useClashProxies,
useProxyMode,
} from '@nyanpasu/interface'
import { cn, SidePage } from '@nyanpasu/ui'
import { alpha, cn, SidePage } from '@nyanpasu/ui'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/proxies')({
@@ -35,7 +29,6 @@ export const Route = createFileRoute('/proxies')({
})
function SideBar() {
const { palette } = useTheme()
const [proxiesFilter, setProxiesFilter] = useAtom(proxiesFilterAtom)
const { t } = useTranslation()
@@ -52,14 +45,16 @@ function SideBar() {
onChange={(e) =>
setProxiesFilter(!e.target.value.trim().length ? null : e.target.value)
}
InputProps={{
sx: {
borderRadius: 7,
backgroundColor: alpha(palette.primary.main, 0.1),
slotProps={{
input: {
sx: (theme) => ({
borderRadius: 7,
backgroundColor: alpha(theme.vars.palette.primary.main, 0.1),
fieldset: {
border: 'none',
},
fieldset: {
border: 'none',
},
}),
},
}}
/>
@@ -3,9 +3,9 @@ import { useSetAtom } from 'jotai'
import { lazy, RefObject, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { atomRulePage } from '@/components/rules/modules/store'
import { alpha, FilledInputProps, TextField, useTheme } from '@mui/material'
import { FilledInputProps, TextField } from '@mui/material'
import { useClashRules } from '@nyanpasu/interface'
import { BasePage } from '@nyanpasu/ui'
import { alpha, BasePage } from '@nyanpasu/ui'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/rules')({
@@ -15,8 +15,6 @@ export const Route = createFileRoute('/rules')({
function RulesPage() {
const { t } = useTranslation()
const { palette } = useTheme()
const { data } = useClashRules()
const [filterText, setFilterText] = useState('')
@@ -37,14 +35,14 @@ function RulesPage() {
)
const inputProps: Partial<FilledInputProps> = {
sx: {
sx: (theme) => ({
borderRadius: 7,
backgroundColor: alpha(palette.primary.main, 0.1),
backgroundColor: alpha(theme.vars.palette.primary.main, 0.1),
fieldset: {
border: 'none',
},
},
}),
}
const Component = lazy(() => import('@/components/rules/rule-page'))
@@ -63,7 +61,9 @@ function RulesPage() {
onChange={(e) => setFilterText(e.target.value)}
className="!pb-0"
sx={{ input: { py: 1, fontSize: 14 } }}
InputProps={inputProps}
slotProps={{
input: inputProps,
}}
/>
}
viewportRef={viewportRef}
@@ -0,0 +1,15 @@
import { SxProps, Theme } from '@mui/material'
export const mergeSxProps = (...props: Array<SxProps<Theme> | undefined>) => {
return props.reduce((acc, curr) => {
if (!curr) {
return acc
}
if (Array.isArray(curr)) {
return [...acc, ...curr]
}
return [...acc, curr]
}, [] as SxProps<Theme>[])
}
@@ -1,7 +1,7 @@
import * as d3 from 'd3'
import { interpolatePath } from 'd3-interpolate-path'
import { CSSProperties, FC, useEffect, useRef } from 'react'
import { alpha, useTheme } from '@mui/material'
import { CSSProperties, FC, useEffect, useMemo, useRef } from 'react'
import { alpha, useColorScheme, useTheme } from '@mui/material'
interface SparklineProps {
data: number[]
@@ -10,7 +10,24 @@ interface SparklineProps {
}
export const Sparkline: FC<SparklineProps> = ({ data, className, style }) => {
const { palette } = useTheme()
const theme = useTheme()
const { mode } = useColorScheme()
const lineColor = useMemo(
() =>
mode === 'dark'
? alpha(theme.colorSchemes.light!.palette.primary.main, 0.7)
: alpha(theme.colorSchemes.dark!.palette.primary.main, 0.7),
[mode, theme],
)
const areaColor = useMemo(
() =>
mode === 'dark'
? alpha(theme.colorSchemes.light!.palette.primary.main, 0.1)
: alpha(theme.colorSchemes.dark!.palette.primary.main, 0.1),
[mode, theme],
)
const svgRef = useRef<SVGSVGElement | null>(null)
@@ -67,7 +84,7 @@ export const Sparkline: FC<SparklineProps> = ({ data, className, style }) => {
.append('path')
.datum(data)
.attr('class', 'area')
.attr('fill', alpha(palette.primary.main, 0.1))
.attr('fill', areaColor)
.attr('d', area)
svg
@@ -75,7 +92,7 @@ export const Sparkline: FC<SparklineProps> = ({ data, className, style }) => {
.datum(data)
.attr('class', 'line')
.attr('fill', 'none')
.attr('stroke', alpha(palette.primary.main, 0.7))
.attr('stroke', lineColor)
.attr('stroke-width', 2)
.attr('d', line)
@@ -106,7 +123,7 @@ export const Sparkline: FC<SparklineProps> = ({ data, className, style }) => {
}
updateChart()
}, [data, palette.primary.main])
}, [data, lineColor, areaColor])
return (
<svg
@@ -10,9 +10,9 @@ import {
} from 'react'
import { useTranslation } from 'react-i18next'
import { getSystem, useClickPosition } from '@/hooks'
import { cn } from '@/utils'
import { Button, Divider } from '@mui/material'
import { alpha, useTheme } from '@mui/material/styles'
import { alpha, cn } from '@/utils'
import { Box, Button, Divider } from '@mui/material'
import { useColorScheme } from '@mui/material/styles'
import * as Portal from '@radix-ui/react-portal'
const OS = getSystem()
@@ -48,7 +48,7 @@ export const BaseDialog = ({
}: BaseDialogProps) => {
const { t } = useTranslation()
const { palette } = useTheme()
const { mode } = useColorScheme()
const [mounted, setMounted] = useState(false)
@@ -76,6 +76,7 @@ export const BaseDialog = ({
y: getClickPosition()?.y ?? 0,
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open])
const handleClose = useLockFn(async () => {
@@ -125,17 +126,21 @@ export const BaseDialog = ({
{mounted && (
<Portal.Root className="fixed top-0 left-0 z-50 h-dvh w-full">
{!full && (
<motion.div
<Box
component={motion.div}
className={cn(
'fixed inset-0 z-50',
OS === 'linux' ? 'bg-black/50' : 'backdrop-blur-xl',
)}
style={{
backgroundColor:
OS === 'linux'
? undefined
: alpha(palette.primary[palette.mode], 0.1),
}}
sx={[
OS === 'linux' ||
((theme) => ({
backgroundColor: alpha(
theme.vars.palette.primary.main,
0.1,
),
})),
]}
animate={open ? 'open' : 'closed'}
initial={{ opacity: 0 }}
variants={{
@@ -146,17 +151,16 @@ export const BaseDialog = ({
/>
)}
<motion.div
<Box
component={motion.div}
className={cn(
'fixed top-[50%] left-[50%] z-50',
full ? 'h-dvh w-full' : 'min-w-96 rounded-3xl shadow',
palette.mode === 'dark'
? 'text-white shadow-zinc-900'
: 'text-black',
mode === 'dark' ? 'text-white shadow-zinc-900' : 'text-black',
)}
style={{
backgroundColor: palette.background.default,
}}
sx={(theme) => ({
backgroundColor: theme.vars.palette.background.default,
})}
animate={open ? 'open' : 'closed'}
initial={{
opacity: 0.3,
@@ -238,7 +242,7 @@ export const BaseDialog = ({
</Button>
)}
</div>
</motion.div>
</Box>
</Portal.Root>
)}
</AnimatePresence>
@@ -1,6 +1,6 @@
import { ReactNode } from 'react'
import { cn } from '@/utils'
import { alpha, Button, ButtonProps, useTheme } from '@mui/material'
import { alpha, cn } from '@/utils'
import { Button, ButtonProps } from '@mui/material'
export interface FloatingButtonProps extends ButtonProps {
children: ReactNode
@@ -12,23 +12,21 @@ export const FloatingButton = ({
className,
...props
}: FloatingButtonProps) => {
const { palette } = useTheme()
return (
<Button
className={cn(
`right-8 bottom-8 z-10 size-16 !rounded-2xl backdrop-blur`,
className,
)}
sx={{
sx={(theme) => ({
position: 'fixed',
boxShadow: 8,
backgroundColor: alpha(palette.primary.main, 0.3),
backgroundColor: alpha(theme.vars.palette.primary.main, 0.3),
'&:hover': {
backgroundColor: alpha(palette.primary.main, 0.45),
backgroundColor: alpha(theme.vars.palette.primary.main, 0.45),
},
}}
})}
{...props}
>
{children}
@@ -1,6 +1,5 @@
import { mode } from 'd3'
import { cn } from '@/utils'
import { useColorScheme, useTheme } from '@mui/material'
import { useColorScheme } from '@mui/material'
import styles from './index.module.scss'
export type Props = React.DetailedHTMLProps<
@@ -1,59 +1,63 @@
import { alpha, darken, Theme } from '@mui/material'
import { alpha } from '@/utils/color-mix'
import { darken, Theme } from '@mui/material'
import { Components } from '@mui/material/styles'
export const MuiButtonGroup: Components<Theme>['MuiButtonGroup'] = {
styleOverrides: {
grouped: ({ theme }) => ({
fontWeight: 700,
height: '2.5em',
padding: '0 1.25em',
border: `1px solid ${darken(theme.vars.palette.primary.main, 0.09)}`,
color: darken(theme.vars.palette.primary.main, 0.2),
grouped: ({ theme }) =>
theme.unstable_sx({
fontWeight: 700,
height: '2.5em',
padding: '0 1.25em',
border: `1px solid ${darken(theme.vars.palette.primary.main, 0.09)}`,
color: darken(theme.vars.palette.primary.main, 0.2),
'&.MuiButton-contained.MuiButton-colorPrimary': {
boxShadow: 'none',
border: `1px solid ${theme.vars.palette.primary.mainChannel}`,
backgroundColor: alpha(theme.vars.palette.primary.main, 0.2),
color: theme.vars.palette.primary.main,
'&::before': {
content: 'none',
'&.MuiButton-contained.MuiButton-colorPrimary': {
boxShadow: 'none',
border: `1px solid ${theme.vars.palette.primary.mainChannel}`,
backgroundColor: alpha(theme.vars.palette.primary.main, 0.2),
color: theme.vars.palette.primary.main,
'&::before': {
content: 'none',
},
'&:hover': {
backgroundColor: alpha(theme.vars.palette.primary.main, 0.3),
},
},
'&:hover': {
backgroundColor: alpha(theme.vars.palette.primary.main, 0.3),
}),
firstButton: ({ theme }) =>
theme.unstable_sx({
borderTopLeftRadius: 48,
borderBottomLeftRadius: 48,
'&.MuiButton-sizeSmall': {
paddingLeft: '1.5em',
},
},
}),
firstButton: {
borderTopLeftRadius: 48,
borderBottomLeftRadius: 48,
'&.MuiButton-sizeSmall': {
paddingLeft: '1.5em',
},
'&.MuiButton-sizeMedium': {
paddingLeft: '20px',
},
'&.MuiButton-sizeMedium': {
paddingLeft: '20px',
},
'&.MuiButton-sizeLarge': {
paddingLeft: '26px',
},
}),
lastButton: ({ theme }) =>
theme.unstable_sx({
borderTopRightRadius: 48,
borderBottomRightRadius: 48,
'&.MuiButton-sizeLarge': {
paddingLeft: '26px',
},
},
lastButton: {
borderTopRightRadius: 48,
borderBottomRightRadius: 48,
'&.MuiButton-sizeSmall': {
paddingRight: '1.5em',
},
'&.MuiButton-sizeSmall': {
paddingRight: '1.5em',
},
'&.MuiButton-sizeMedium': {
paddingRight: '20px',
},
'&.MuiButton-sizeMedium': {
paddingRight: '20px',
},
'&.MuiButton-sizeLarge': {
paddingRight: '26px',
},
},
'&.MuiButton-sizeLarge': {
paddingRight: '26px',
},
}),
},
} satisfies Components<Theme>['MuiButtonGroup']
@@ -0,0 +1,11 @@
export const alpha = (color: string, alpha: number) => {
return `color-mix(in srgb, ${color} ${(alpha * 100).toFixed(2)}%, transparent ${((1 - alpha) * 100).toFixed(2)}%)`
}
export const lighten = (color: string, alpha: number) => {
return `color-mix(in lch, ${color} ${((1 - alpha) * 100).toFixed(2)}%, white ${(alpha * 100).toFixed(2)}%)`
}
export const darken = (color: string, alpha: number) => {
return `color-mix(in lch, ${color} ${((1 - alpha) * 100).toFixed(2)}%, black ${(alpha * 100).toFixed(2)}%)`
}
@@ -1,3 +1,4 @@
export { cn } from './cn'
export * from './event'
export * from './ts-helper'
export * from './color-mix'
+2 -2
View File
@@ -2,7 +2,7 @@
"manifest_version": 1,
"latest": {
"mihomo": "v1.19.8",
"mihomo_alpha": "alpha-d036d98",
"mihomo_alpha": "alpha-9f7a2a3",
"clash_rs": "v0.7.8",
"clash_premium": "2023-09-05-gdcc8d87",
"clash_rs_alpha": "0.7.7-alpha+sha.72871ca"
@@ -69,5 +69,5 @@
"linux-armv7hf": "clash-armv7-unknown-linux-gnueabihf"
}
},
"updated_at": "2025-05-18T22:20:58.018Z"
"updated_at": "2025-05-19T22:21:05.925Z"
}
+2 -1
View File
@@ -85,7 +85,7 @@
"eslint-plugin-react-hooks": "5.2.0",
"globals": "16.1.0",
"knip": "5.55.1",
"lint-staged": "15.5.2",
"lint-staged": "16.0.0",
"neostandard": "0.12.1",
"npm-run-all2": "8.0.1",
"postcss": "8.5.3",
@@ -117,6 +117,7 @@
"vite-plugin-monaco-editor": "npm:vite-plugin-monaco-editor-new@1.1.3"
},
"onlyBuiltDependencies": [
"@tailwindcss/oxide",
"core-js",
"esbuild"
]
+58 -113
View File
@@ -103,8 +103,8 @@ importers:
specifier: 5.55.1
version: 5.55.1(@types/node@22.15.18)(typescript@5.8.3)
lint-staged:
specifier: 15.5.2
version: 15.5.2
specifier: 16.0.0
version: 16.0.0
neostandard:
specifier: 0.12.1
version: 0.12.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.32.1(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.26.0(jiti@2.4.2)))(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3)
@@ -349,7 +349,7 @@ importers:
version: 1.120.3(@tanstack/react-router@1.120.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@tanstack/router-core@1.120.3)(csstype@3.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(tiny-invariant@1.3.3)
'@tanstack/router-plugin':
specifier: 1.120.3
version: 1.120.3(@tanstack/react-router@1.120.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.7.0))
version: 1.120.3(@tanstack/react-router@1.120.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.8.0))
'@tauri-apps/plugin-clipboard-manager':
specifier: 2.2.2
version: 2.2.2
@@ -385,13 +385,13 @@ importers:
version: 13.15.0
'@vitejs/plugin-legacy':
specifier: 6.1.1
version: 6.1.1(terser@5.36.0)(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.7.0))
version: 6.1.1(terser@5.36.0)(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.8.0))
'@vitejs/plugin-react':
specifier: 4.4.1
version: 4.4.1(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.7.0))
version: 4.4.1(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.8.0))
'@vitejs/plugin-react-swc':
specifier: 3.9.0
version: 3.9.0(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.7.0))
version: 3.9.0(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.8.0))
change-case:
specifier: 5.4.4
version: 5.4.4
@@ -430,19 +430,19 @@ importers:
version: 13.15.0
vite:
specifier: 6.3.5
version: 6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.7.0)
version: 6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.8.0)
vite-plugin-html:
specifier: 3.2.2
version: 3.2.2(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.7.0))
version: 3.2.2(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.8.0))
vite-plugin-sass-dts:
specifier: 1.3.31
version: 1.3.31(postcss@8.5.3)(prettier@3.5.3)(sass-embedded@1.88.0)(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.7.0))
version: 1.3.31(postcss@8.5.3)(prettier@3.5.3)(sass-embedded@1.88.0)(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.8.0))
vite-plugin-svgr:
specifier: 4.3.0
version: 4.3.0(rollup@4.40.0)(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.7.0))
version: 4.3.0(rollup@4.40.0)(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.8.0))
vite-tsconfig-paths:
specifier: 5.1.4
version: 5.1.4(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.7.0))
version: 5.1.4(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.8.0))
zod:
specifier: 3.24.4
version: 3.24.4
@@ -478,7 +478,7 @@ importers:
version: 19.1.4
'@vitejs/plugin-react':
specifier: 4.4.1
version: 4.4.1(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.7.0))
version: 4.4.1(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.8.0))
ahooks:
specifier: 3.8.4
version: 3.8.4(react@19.1.0)
@@ -508,10 +508,10 @@ importers:
version: 4.1.6
vite:
specifier: 6.3.5
version: 6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.7.0)
version: 6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.8.0)
vite-tsconfig-paths:
specifier: 5.1.4
version: 5.1.4(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.7.0))
version: 5.1.4(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.8.0))
devDependencies:
'@emotion/react':
specifier: 11.14.0
@@ -536,7 +536,7 @@ importers:
version: 5.1.0(typescript@5.8.3)
vite-plugin-dts:
specifier: 4.5.4
version: 4.5.4(@types/node@22.15.18)(rollup@4.40.0)(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.7.0))
version: 4.5.4(@types/node@22.15.18)(rollup@4.40.0)(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.8.0))
scripts:
dependencies:
@@ -4733,10 +4733,6 @@ packages:
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
engines: {node: '>=10'}
execa@8.0.1:
resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==}
engines: {node: '>=16.17'}
express-rate-limit@7.5.0:
resolution: {integrity: sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==}
engines: {node: '>= 16'}
@@ -4981,10 +4977,6 @@ packages:
resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
engines: {node: '>=10'}
get-stream@8.0.1:
resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==}
engines: {node: '>=16'}
get-symbol-description@1.0.2:
resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==}
engines: {node: '>= 0.4'}
@@ -5206,10 +5198,6 @@ packages:
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
engines: {node: '>=10.17.0'}
human-signals@5.0.0:
resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==}
engines: {node: '>=16.17.0'}
husky@9.1.7:
resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==}
engines: {node: '>=18'}
@@ -5538,10 +5526,6 @@ packages:
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
engines: {node: '>=8'}
is-stream@3.0.0:
resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
is-string@1.0.7:
resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==}
engines: {node: '>= 0.4'}
@@ -5837,13 +5821,13 @@ packages:
lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
lint-staged@15.5.2:
resolution: {integrity: sha512-YUSOLq9VeRNAo/CTaVmhGDKG+LBtA8KF1X4K5+ykMSwWST1vDxJRB2kv2COgLb1fvpCo+A/y9A0G0znNVmdx4w==}
engines: {node: '>=18.12.0'}
lint-staged@16.0.0:
resolution: {integrity: sha512-sUCprePs6/rbx4vKC60Hez6X10HPkpDJaGcy3D1NdwR7g1RcNkWL8q9mJMreOqmHBTs+1sNFp+wOiX9fr+hoOQ==}
engines: {node: '>=20.18'}
hasBin: true
listr2@8.2.5:
resolution: {integrity: sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==}
listr2@8.3.3:
resolution: {integrity: sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==}
engines: {node: '>=18.0.0'}
local-pkg@1.0.0:
@@ -6130,10 +6114,6 @@ packages:
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
engines: {node: '>=6'}
mimic-fn@4.0.0:
resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==}
engines: {node: '>=12'}
mimic-function@5.0.1:
resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==}
engines: {node: '>=18'}
@@ -6239,6 +6219,10 @@ packages:
react: '*'
react-dom: '*'
nano-spawn@1.0.1:
resolution: {integrity: sha512-BfcvzBlUTxSDWfT+oH7vd6CbUV+rThLLHCIym/QO6GGLBsyVXleZs00fto2i2jzC/wPiBYk5jyOmpXWg4YopiA==}
engines: {node: '>=20.18'}
nanoid@3.3.8:
resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@@ -6332,10 +6316,6 @@ packages:
resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
engines: {node: '>=8'}
npm-run-path@5.3.0:
resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
nth-check@2.1.1:
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
@@ -6401,10 +6381,6 @@ packages:
resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
engines: {node: '>=6'}
onetime@6.0.0:
resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==}
engines: {node: '>=12'}
onetime@7.0.0:
resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
engines: {node: '>=18'}
@@ -6520,10 +6496,6 @@ packages:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
path-key@4.0.0:
resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==}
engines: {node: '>=12'}
path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
@@ -7562,10 +7534,6 @@ packages:
resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==}
engines: {node: '>=6'}
strip-final-newline@3.0.0:
resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==}
engines: {node: '>=12'}
strip-json-comments@2.0.1:
resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
engines: {node: '>=0.10.0'}
@@ -8349,6 +8317,11 @@ packages:
engines: {node: '>= 14'}
hasBin: true
yaml@2.8.0:
resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==}
engines: {node: '>= 14.6'}
hasBin: true
yargs-parser@21.1.1:
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
engines: {node: '>=12'}
@@ -10802,7 +10775,7 @@ snapshots:
optionalDependencies:
'@tanstack/react-router': 1.120.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@tanstack/router-plugin@1.120.3(@tanstack/react-router@1.120.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.7.0))':
'@tanstack/router-plugin@1.120.3(@tanstack/react-router@1.120.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.8.0))':
dependencies:
'@babel/core': 7.26.10
'@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.10)
@@ -10823,7 +10796,7 @@ snapshots:
zod: 3.24.4
optionalDependencies:
'@tanstack/react-router': 1.120.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
vite: 6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.7.0)
vite: 6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.8.0)
transitivePeerDependencies:
- supports-color
@@ -11334,7 +11307,7 @@ snapshots:
'@ungap/structured-clone@1.2.0': {}
'@vitejs/plugin-legacy@6.1.1(terser@5.36.0)(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.7.0))':
'@vitejs/plugin-legacy@6.1.1(terser@5.36.0)(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.8.0))':
dependencies:
'@babel/core': 7.26.10
'@babel/preset-env': 7.26.9(@babel/core@7.26.10)
@@ -11345,25 +11318,25 @@ snapshots:
regenerator-runtime: 0.14.1
systemjs: 6.15.1
terser: 5.36.0
vite: 6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.7.0)
vite: 6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.8.0)
transitivePeerDependencies:
- supports-color
'@vitejs/plugin-react-swc@3.9.0(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.7.0))':
'@vitejs/plugin-react-swc@3.9.0(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.8.0))':
dependencies:
'@swc/core': 1.11.21
vite: 6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.7.0)
vite: 6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.8.0)
transitivePeerDependencies:
- '@swc/helpers'
'@vitejs/plugin-react@4.4.1(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.7.0))':
'@vitejs/plugin-react@4.4.1(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.8.0))':
dependencies:
'@babel/core': 7.26.10
'@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.10)
'@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.10)
'@types/babel__core': 7.20.5
react-refresh: 0.17.0
vite: 6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.7.0)
vite: 6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.8.0)
transitivePeerDependencies:
- supports-color
@@ -13055,18 +13028,6 @@ snapshots:
signal-exit: 3.0.7
strip-final-newline: 2.0.0
execa@8.0.1:
dependencies:
cross-spawn: 7.0.6
get-stream: 8.0.1
human-signals: 5.0.0
is-stream: 3.0.0
merge-stream: 2.0.0
npm-run-path: 5.3.0
onetime: 6.0.0
signal-exit: 4.1.0
strip-final-newline: 3.0.0
express-rate-limit@7.5.0(express@5.1.0):
dependencies:
express: 5.1.0
@@ -13362,8 +13323,6 @@ snapshots:
get-stream@6.0.1: {}
get-stream@8.0.1: {}
get-symbol-description@1.0.2:
dependencies:
call-bind: 1.0.8
@@ -13657,8 +13616,6 @@ snapshots:
human-signals@2.1.0: {}
human-signals@5.0.0: {}
husky@9.1.7: {}
hyphenate-style-name@1.1.0: {}
@@ -13938,8 +13895,6 @@ snapshots:
is-stream@2.0.1: {}
is-stream@3.0.0: {}
is-string@1.0.7:
dependencies:
has-tostringtag: 1.0.2
@@ -14205,22 +14160,22 @@ snapshots:
lines-and-columns@1.2.4: {}
lint-staged@15.5.2:
lint-staged@16.0.0:
dependencies:
chalk: 5.4.1
commander: 13.1.0
debug: 4.4.0
execa: 8.0.1
lilconfig: 3.1.3
listr2: 8.2.5
listr2: 8.3.3
micromatch: 4.0.8
nano-spawn: 1.0.1
pidtree: 0.6.0
string-argv: 0.3.2
yaml: 2.7.0
yaml: 2.8.0
transitivePeerDependencies:
- supports-color
listr2@8.2.5:
listr2@8.3.3:
dependencies:
cli-truncate: 4.0.0
colorette: 2.0.20
@@ -14614,8 +14569,6 @@ snapshots:
mimic-fn@2.1.0: {}
mimic-fn@4.0.0: {}
mimic-function@5.0.1: {}
mimic-response@1.0.1: {}
@@ -14729,6 +14682,8 @@ snapshots:
stacktrace-js: 2.0.2
stylis: 4.3.2
nano-spawn@1.0.1: {}
nanoid@3.3.8: {}
nanoid@5.1.5: {}
@@ -14822,10 +14777,6 @@ snapshots:
dependencies:
path-key: 3.1.1
npm-run-path@5.3.0:
dependencies:
path-key: 4.0.0
nth-check@2.1.1:
dependencies:
boolbase: 1.0.0
@@ -14918,10 +14869,6 @@ snapshots:
dependencies:
mimic-fn: 2.1.0
onetime@6.0.0:
dependencies:
mimic-fn: 4.0.0
onetime@7.0.0:
dependencies:
mimic-function: 5.0.1
@@ -15044,8 +14991,6 @@ snapshots:
path-key@3.1.1: {}
path-key@4.0.0: {}
path-parse@1.0.7: {}
path-scurry@1.11.1:
@@ -16082,8 +16027,6 @@ snapshots:
strip-final-newline@2.0.0: {}
strip-final-newline@3.0.0: {}
strip-json-comments@2.0.1: {}
strip-json-comments@3.1.1: {}
@@ -16729,7 +16672,7 @@ snapshots:
- rollup
- supports-color
vite-plugin-dts@4.5.4(@types/node@22.15.18)(rollup@4.40.0)(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.7.0)):
vite-plugin-dts@4.5.4(@types/node@22.15.18)(rollup@4.40.0)(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.8.0)):
dependencies:
'@microsoft/api-extractor': 7.51.0(@types/node@22.15.18)
'@rollup/pluginutils': 5.1.4(rollup@4.40.0)
@@ -16742,13 +16685,13 @@ snapshots:
magic-string: 0.30.17
typescript: 5.8.3
optionalDependencies:
vite: 6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.7.0)
vite: 6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.8.0)
transitivePeerDependencies:
- '@types/node'
- rollup
- supports-color
vite-plugin-html@3.2.2(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.7.0)):
vite-plugin-html@3.2.2(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.8.0)):
dependencies:
'@rollup/pluginutils': 4.2.1
colorette: 2.0.20
@@ -16762,39 +16705,39 @@ snapshots:
html-minifier-terser: 6.1.0
node-html-parser: 5.4.2
pathe: 0.2.0
vite: 6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.7.0)
vite: 6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.8.0)
vite-plugin-sass-dts@1.3.31(postcss@8.5.3)(prettier@3.5.3)(sass-embedded@1.88.0)(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.7.0)):
vite-plugin-sass-dts@1.3.31(postcss@8.5.3)(prettier@3.5.3)(sass-embedded@1.88.0)(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.8.0)):
dependencies:
postcss: 8.5.3
postcss-js: 4.0.1(postcss@8.5.3)
prettier: 3.5.3
sass-embedded: 1.88.0
vite: 6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.7.0)
vite: 6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.8.0)
vite-plugin-svgr@4.3.0(rollup@4.40.0)(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.7.0)):
vite-plugin-svgr@4.3.0(rollup@4.40.0)(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.8.0)):
dependencies:
'@rollup/pluginutils': 5.1.3(rollup@4.40.0)
'@svgr/core': 8.1.0(typescript@5.8.3)
'@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.8.3))
vite: 6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.7.0)
vite: 6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.8.0)
transitivePeerDependencies:
- rollup
- supports-color
- typescript
vite-tsconfig-paths@5.1.4(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.7.0)):
vite-tsconfig-paths@5.1.4(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.8.0)):
dependencies:
debug: 4.3.7
globrex: 0.1.2
tsconfck: 3.0.3(typescript@5.8.3)
optionalDependencies:
vite: 6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.7.0)
vite: 6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.8.0)
transitivePeerDependencies:
- supports-color
- typescript
vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.7.0):
vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.8.0):
dependencies:
esbuild: 0.25.0
fdir: 6.4.4(picomatch@4.0.2)
@@ -16813,7 +16756,7 @@ snapshots:
stylus: 0.62.0
terser: 5.36.0
tsx: 4.19.4
yaml: 2.7.0
yaml: 2.8.0
void-elements@3.1.0: {}
@@ -16981,6 +16924,8 @@ snapshots:
yaml@2.7.0: {}
yaml@2.8.0: {}
yargs-parser@21.1.1: {}
yargs@17.7.2:
+4 -2
View File
@@ -3,14 +3,16 @@
#pnpm pretty-quick --staged
# 运行 clippy fmt
cargo fmt --manifest-path ./src-tauri/Cargo.toml
cd src-tauri
cargo fmt
if [ $? -ne 0 ]; then
echo "rustfmt failed to format the code. Please fix the issues and try again."
exit 1
fi
cd ..
#git add .
git add .
# 允许提交
exit 0
+18
View File
@@ -96,6 +96,24 @@ pnpm portable
## Contributing Your Changes
#### Before commit your changes
If you changed the rust code, it's recommanded to execute code style formatting and quailty checks.
1. Code style formatting
```bash
$ clash-verge-rev: cd src-tauri
$ clash-verge-rev/src-tauri: cargo fmt
```
2. Code quailty checks
```bash
$ clash-verge-rev: pnpm clippy
```
Once you have made your changes:
1. Fork the repository.
+6
View File
@@ -24,6 +24,7 @@
- 同步更新多语言翻译
- 修复 .window-state.json 无法删除的问题
- 无法修改配置更新 HTTP 请求超时
- 修复 getDelayFix 钩子问题
#### 新增了:
- Mihomo(Meta)内核升级至 1.19.8
@@ -49,6 +50,8 @@
- 添加了Zashboard的一键跳转URL
- 使用操作系统默认的窗口管理器
- 切换、升级、重启内核的状态管理
- 增加了一键随机API端口和密钥/单独刷新按钮
- 更精细化控制自动日志清理,新增1天选项。
#### 优化了:
- 系统代理 Bypass 设置
@@ -73,6 +76,9 @@
- 优化窗口状态初始化逻辑和添加缺失的权限设置
- 异步化配置:优化端口查找和配置保存逻辑
- 重构事件通知机制到独立线程,避免前端卡死
- 优化端口设置,每个端口可随机设置端口号
- 优化了随机端口和密钥机制,防止随机时卡死!
- 优化了保存机制,使用平滑函数,防止客户端卡死!
## v2.2.3
+1
View File
@@ -20,6 +20,7 @@
"release-version": "node scripts/release_version.mjs",
"release-alpha-version": "node scripts/release-alpha_version.mjs",
"prepare": "husky",
"fmt": "cargo fmt --manifest-path ./src-tauri/Cargo.toml",
"clippy": "cargo clippy --manifest-path ./src-tauri/Cargo.toml"
},
"dependencies": {
@@ -134,7 +134,7 @@ pub struct IVerge {
pub test_list: Option<Vec<IVergeTestItem>>,
/// 日志清理
/// 0: 不清理; 1: 7天; 2: 30天; 3: 90天
/// 0: 不清理; 1: 1天;2: 7天; 3: 30天; 4: 90天
pub auto_log_clean: Option<i32>,
/// 是否启用随机端口
+9 -12
View File
@@ -472,19 +472,16 @@ impl CoreManager {
tokio::spawn(async move {
while let Some(event) = rx.recv().await {
match event {
tauri_plugin_shell::process::CommandEvent::Stdout(line) => {
if let Err(e) = writeln!(log_file, "{}", String::from_utf8_lossy(&line)) {
logging!(
error,
Type::Core,
true,
"[Sidecar] Failed to write stdout to file: {}",
e
);
}
if let tauri_plugin_shell::process::CommandEvent::Stdout(line) = event {
if let Err(e) = writeln!(log_file, "{}", String::from_utf8_lossy(&line)) {
logging!(
error,
Type::Core,
true,
"[Sidecar] Failed to write stdout to file: {}",
e
);
}
_ => {}
}
}
});
+12 -4
View File
@@ -317,9 +317,13 @@ impl Handle {
let system_opt = handle.notification_system.read();
if let Some(system) = system_opt.as_ref() {
system.send_event(FrontendEvent::ProfileChanged { current_profile_id: profile_id });
system.send_event(FrontendEvent::ProfileChanged {
current_profile_id: profile_id,
});
} else {
log::warn!("Notification system not initialized when trying to send ProfileChanged event.");
log::warn!(
"Notification system not initialized when trying to send ProfileChanged event."
);
}
}
@@ -333,7 +337,9 @@ impl Handle {
if let Some(system) = system_opt.as_ref() {
system.send_event(FrontendEvent::TimerUpdated { profile_index });
} else {
log::warn!("Notification system not initialized when trying to send TimerUpdated event.");
log::warn!(
"Notification system not initialized when trying to send TimerUpdated event."
);
}
}
@@ -347,7 +353,9 @@ impl Handle {
if let Some(system) = system_opt.as_ref() {
system.send_event(FrontendEvent::StartupCompleted);
} else {
log::warn!("Notification system not initialized when trying to send StartupCompleted event.");
log::warn!(
"Notification system not initialized when trying to send StartupCompleted event."
);
}
}
@@ -58,7 +58,8 @@ impl MihomoManager {
url: String,
data: Option<serde_json::Value>,
) -> Result<serde_json::Value, String> {
let client_response = self.client
let client_response = self
.client
.request(method.clone(), &url)
.json(&data.unwrap_or(json!({})))
.send()
@@ -1,4 +1,4 @@
use std::sync::{Arc, Mutex};
use std::sync::{Arc, Mutex};
pub struct MihomoData {
pub(crate) proxies: serde_json::Value,
@@ -24,5 +24,4 @@ impl Drop for MihomoManager {
fn drop(&mut self) {
println!("Dropping MihomoManager");
}
}
}
@@ -26,4 +26,4 @@ async fn test_refresh_providers_proxies() {
let providers = manager.get_providers_proxies();
assert_eq!(proxies, serde_json::Value::Null);
assert_ne!(providers, serde_json::Value::Null);
}
}
@@ -101,13 +101,13 @@ export const CurrentProxyCard = () => {
const mode = clashConfig?.mode?.toLowerCase() || "rule";
const isGlobalMode = mode === "global";
const isDirectMode = mode === "direct";
// 添加排序类型状态
const [sortType, setSortType] = useState<ProxySortType>(() => {
const savedSortType = localStorage.getItem(STORAGE_KEY_SORT_TYPE);
return savedSortType ? Number(savedSortType) as ProxySortType : 0;
});
// 定义状态类型
type ProxyState = {
proxyData: {
@@ -128,7 +128,7 @@ export const CurrentProxyCard = () => {
groups: [],
records: {},
globalProxy: "",
directProxy: null,
directProxy: { name: "DIRECT" }, // 默认值避免 undefined
},
selection: {
group: "",
@@ -160,9 +160,9 @@ export const CurrentProxyCard = () => {
return primaryGroup?.name || "";
};
const primaryGroupName = getPrimaryGroupName();
// 根据模式确定初始组
if (isGlobalMode) {
setState((prev) => ({
@@ -204,7 +204,7 @@ export const CurrentProxyCard = () => {
now: g.now || "",
all: g.all.map((p: { name: string }) => p.name),
}));
let newProxy = "";
let newDisplayProxy = null;
let newGroup = prev.selection.group;
@@ -213,7 +213,7 @@ export const CurrentProxyCard = () => {
if (isDirectMode) {
newGroup = "DIRECT";
newProxy = "DIRECT";
newDisplayProxy = proxies.records?.DIRECT || null;
newDisplayProxy = proxies.records?.DIRECT || { name: "DIRECT" }; // 确保非空
} else if (isGlobalMode && proxies.global) {
newGroup = "GLOBAL";
newProxy = proxies.global.now || "";
@@ -248,7 +248,7 @@ export const CurrentProxyCard = () => {
groups: filteredGroups,
records: proxies.records || {},
globalProxy: proxies.global?.now || "",
directProxy: proxies.records?.DIRECT || null,
directProxy: proxies.records?.DIRECT || { name: "DIRECT" },
},
selection: {
group: newGroup,
@@ -364,12 +364,15 @@ export const CurrentProxyCard = () => {
return state.displayProxy;
}, [state.displayProxy]);
// 获取当前节点的延迟
const currentDelay = currentProxy
// 获取当前节点的延迟(增加非空校验)
const currentDelay = currentProxy && state.selection.group
? delayManager.getDelayFix(currentProxy, state.selection.group)
: -1;
const signalInfo = getSignalIcon(currentDelay);
// 信号图标(增加非空校验)
const signalInfo = currentProxy && state.selection.group
? getSignalIcon(currentDelay)
: { icon: <SignalNone />, text: "未初始化", color: "text.secondary" };
// 自定义渲染选择框中的值
const renderProxyValue = useCallback(
@@ -378,7 +381,7 @@ export const CurrentProxyCard = () => {
const delayValue = delayManager.getDelayFix(
state.proxyData.records[selected],
state.selection.group,
state.selection.group
);
return (
@@ -402,23 +405,27 @@ export const CurrentProxyCard = () => {
localStorage.setItem(STORAGE_KEY_SORT_TYPE, newSortType.toString());
}, [sortType]);
// 排序代理函数
// 排序代理函数(增加非空校验)
const sortProxies = useCallback(
(proxies: ProxyOption[]) => {
if (!proxies || sortType === 0) return proxies;
// 确保数据存在
if (!state.proxyData.records || !state.selection.group) return proxies;
const list = [...proxies];
if (sortType === 1) {
list.sort((a, b) => {
const ad = delayManager.getDelayFix(
state.proxyData.records[a.name],
state.selection.group
);
const bd = delayManager.getDelayFix(
state.proxyData.records[b.name],
state.selection.group
);
const recordA = state.proxyData.records[a.name];
const recordB = state.proxyData.records[b.name];
// 处理 record 不存在的情况
if (!recordA) return 1;
if (!recordB) return -1;
const ad = delayManager.getDelayFix(recordA, state.selection.group);
const bd = delayManager.getDelayFix(recordB, state.selection.group);
if (ad === -1 || ad === -2) return 1;
if (bd === -1 || bd === -2) return -1;
@@ -428,13 +435,13 @@ export const CurrentProxyCard = () => {
} else {
list.sort((a, b) => a.name.localeCompare(b.name));
}
return list;
},
[sortType, state?.proxyData?.records, state?.selection?.group]
[sortType, state.proxyData.records, state.selection.group]
);
// 计算要显示的代理选项
// 计算要显示的代理选项(增加非空校验)
const proxyOptions = useMemo(() => {
if (isDirectMode) {
return [{ name: "DIRECT" }];
@@ -445,21 +452,23 @@ export const CurrentProxyCard = () => {
const name = typeof p === 'string' ? p : p.name;
return name !== "DIRECT" && name !== "REJECT";
})
.map((p: any) => ({
name: typeof p === 'string' ? p : p.name
.map((p: any) => ({
name: typeof p === 'string' ? p : p.name
}));
return sortProxies(options);
}
// 规则模式
const group = state.proxyData.groups.find(
(g: { name: string }) => g.name === state.selection.group,
);
const group = state.selection.group
? state.proxyData.groups.find(g => g.name === state.selection.group)
: null;
if (group) {
const options = group.all.map((name) => ({ name }));
return sortProxies(options);
}
return [];
}, [isDirectMode, isGlobalMode, proxies, state.proxyData, state.selection.group, sortProxies]);
@@ -509,8 +518,8 @@ export const CurrentProxyCard = () => {
action={
<Box sx={{ display: "flex", alignItems: "center" }}>
<Tooltip title={getSortTooltip()}>
<IconButton
size="small"
<IconButton
size="small"
color="inherit"
onClick={handleSortTypeChange}
sx={{ mr: 1 }}
@@ -648,10 +657,12 @@ export const CurrentProxyCard = () => {
{isDirectMode
? null
: proxyOptions.map((proxy, index) => {
const delayValue = delayManager.getDelayFix(
state.proxyData.records[proxy.name],
state.selection.group,
);
const delayValue = state.proxyData.records[proxy.name] && state.selection.group
? delayManager.getDelayFix(
state.proxyData.records[proxy.name],
state.selection.group,
)
: -1;
return (
<MenuItem
key={`${proxy.name}-${index}`}
@@ -692,4 +703,4 @@ export const CurrentProxyCard = () => {
)}
</EnhancedCard>
);
};
};
@@ -1,286 +1,282 @@
import { BaseDialog, Switch } from "@/components/base";
import { useClashInfo } from "@/hooks/use-clash";
import { useVerge } from "@/hooks/use-verge";
import { showNotice } from "@/services/noticeService";
import getSystem from "@/utils/get-system";
import { Shuffle } from "@mui/icons-material";
import { IconButton, List, ListItem, ListItemText, TextField } from "@mui/material";
import { useLockFn } from "ahooks";
import { forwardRef, useImperativeHandle, useState } from "react";
import { useTranslation } from "react-i18next";
import { useLockFn } from "ahooks";
import { List, ListItem, ListItemText, TextField } from "@mui/material";
import { useClashInfo } from "@/hooks/use-clash";
import { BaseDialog, DialogRef, Switch } from "@/components/base";
import { useVerge } from "@/hooks/use-verge";
import getSystem from "@/utils/get-system";
import { showNotice } from "@/services/noticeService";
const OS = getSystem();
export const ClashPortViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation();
interface ClashPortViewerProps {}
const { clashInfo, patchInfo } = useClashInfo();
const { verge, patchVerge } = useVerge();
const [open, setOpen] = useState(false);
const [redirPort, setRedirPort] = useState(
verge?.verge_redir_port ?? clashInfo?.redir_port ?? 7895
);
const [redirEnabled, setRedirEnabled] = useState(
verge?.verge_redir_enabled ?? false
);
const [tproxyPort, setTproxyPort] = useState(
verge?.verge_tproxy_port ?? clashInfo?.tproxy_port ?? 7896
);
const [tproxyEnabled, setTproxyEnabled] = useState(
verge?.verge_tproxy_enabled ?? false
);
const [mixedPort, setMixedPort] = useState(
verge?.verge_mixed_port ?? clashInfo?.mixed_port ?? 7897
);
const [socksPort, setSocksPort] = useState(
verge?.verge_socks_port ?? clashInfo?.socks_port ?? 7898
);
const [socksEnabled, setSocksEnabled] = useState(
verge?.verge_socks_enabled ?? false
);
const [port, setPort] = useState(
verge?.verge_port ?? clashInfo?.port ?? 7899
);
const [httpEnabled, setHttpEnabled] = useState(
verge?.verge_http_enabled ?? false
);
interface ClashPortViewerRef {
open: () => void;
close: () => void;
}
useImperativeHandle(ref, () => ({
open: () => {
if (verge?.verge_redir_port) setRedirPort(verge?.verge_redir_port);
setRedirEnabled(verge?.verge_redir_enabled ?? false);
if (verge?.verge_tproxy_port) setTproxyPort(verge?.verge_tproxy_port);
setTproxyEnabled(verge?.verge_tproxy_enabled ?? false);
if (verge?.verge_mixed_port) setMixedPort(verge?.verge_mixed_port);
if (verge?.verge_socks_port) setSocksPort(verge?.verge_socks_port);
setSocksEnabled(verge?.verge_socks_enabled ?? false);
if (verge?.verge_port) setPort(verge?.verge_port);
setHttpEnabled(verge?.verge_http_enabled ?? false);
setOpen(true);
},
close: () => setOpen(false),
}));
const generateRandomPort = () => Math.floor(Math.random() * (65535 - 1025 + 1)) + 1025;
const onSave = useLockFn(async () => {
if (
redirPort === verge?.verge_redir_port &&
tproxyPort === verge?.verge_tproxy_port &&
mixedPort === verge?.verge_mixed_port &&
socksPort === verge?.verge_socks_port &&
port === verge?.verge_port &&
redirEnabled === verge?.verge_redir_enabled &&
tproxyEnabled === verge?.verge_tproxy_enabled &&
socksEnabled === verge?.verge_socks_enabled &&
httpEnabled === verge?.verge_http_enabled
) {
setOpen(false);
return;
}
export const ClashPortViewer = forwardRef<ClashPortViewerRef, ClashPortViewerProps>(
(props, ref) => {
const { t } = useTranslation();
const { clashInfo, patchInfo } = useClashInfo();
const { verge, patchVerge } = useVerge();
const [open, setOpen] = useState(false);
if (
OS === "linux" &&
new Set([redirPort, tproxyPort, mixedPort, socksPort, port]).size !== 5
) {
showNotice('error', t("Port Conflict"));
return;
}
if (
OS === "macos" &&
new Set([redirPort, mixedPort, socksPort, port]).size !== 4
) {
showNotice('error', t("Port Conflict"));
return;
}
if (OS === "windows" && new Set([mixedPort, socksPort, port]).size !== 3) {
showNotice('error', t("Port Conflict"));
return;
}
try {
if (OS === "windows") {
// Mixed Port
const [mixedPort, setMixedPort] = useState(
verge?.verge_mixed_port ?? clashInfo?.mixed_port ?? 7897
);
// 其他端口状态
const [socksPort, setSocksPort] = useState(verge?.verge_socks_port ?? 7898);
const [socksEnabled, setSocksEnabled] = useState(verge?.verge_socks_enabled ?? false);
const [httpPort, setHttpPort] = useState(verge?.verge_port ?? 7899);
const [httpEnabled, setHttpEnabled] = useState(verge?.verge_http_enabled ?? false);
const [redirPort, setRedirPort] = useState(verge?.verge_redir_port ?? 7895);
const [redirEnabled, setRedirEnabled] = useState(verge?.verge_redir_enabled ?? false);
const [tproxyPort, setTproxyPort] = useState(verge?.verge_tproxy_port ?? 7896);
const [tproxyEnabled, setTproxyEnabled] = useState(verge?.verge_tproxy_enabled ?? false);
useImperativeHandle(ref, () => ({
open: () => {
setMixedPort(verge?.verge_mixed_port ?? clashInfo?.mixed_port ?? 7897);
setSocksPort(verge?.verge_socks_port ?? 7898);
setSocksEnabled(verge?.verge_socks_enabled ?? false);
setHttpPort(verge?.verge_port ?? 7899);
setHttpEnabled(verge?.verge_http_enabled ?? false);
setRedirPort(verge?.verge_redir_port ?? 7895);
setRedirEnabled(verge?.verge_redir_enabled ?? false);
setTproxyPort(verge?.verge_tproxy_port ?? 7896);
setTproxyEnabled(verge?.verge_tproxy_enabled ?? false);
setOpen(true);
},
close: () => setOpen(false),
}));
const onSave = useLockFn(async () => {
// 端口冲突检测
const portList = [
mixedPort,
socksEnabled ? socksPort : -1,
httpEnabled ? httpPort : -1,
redirEnabled ? redirPort : -1,
tproxyEnabled ? tproxyPort : -1
].filter(p => p !== -1);
if (new Set(portList).size !== portList.length) {
showNotice("error", t("Port Conflict"));
return;
}
try {
// 更新Clash配置
await patchInfo({
"mixed-port": mixedPort,
"socks-port": socksPort,
port,
});
port: httpPort,
"redir-port": redirPort,
"tproxy-port": tproxyPort
} as any);
// 更新Verge配置
await patchVerge({
verge_mixed_port: mixedPort,
verge_socks_port: socksPort,
verge_socks_enabled: socksEnabled,
verge_port: port,
verge_port: httpPort,
verge_http_enabled: httpEnabled,
});
}
if (OS === "macos") {
await patchInfo({
"redir-port": redirPort,
"mixed-port": mixedPort,
"socks-port": socksPort,
port,
});
await patchVerge({
verge_redir_port: redirPort,
verge_redir_enabled: redirEnabled,
verge_mixed_port: mixedPort,
verge_socks_port: socksPort,
verge_socks_enabled: socksEnabled,
verge_port: port,
verge_http_enabled: httpEnabled,
});
}
if (OS === "linux") {
await patchInfo({
"redir-port": redirPort,
"tproxy-port": tproxyPort,
"mixed-port": mixedPort,
"socks-port": socksPort,
port,
});
await patchVerge({
verge_redir_port: redirPort,
verge_redir_enabled: redirEnabled,
verge_tproxy_port: tproxyPort,
verge_tproxy_enabled: tproxyEnabled,
verge_mixed_port: mixedPort,
verge_socks_port: socksPort,
verge_socks_enabled: socksEnabled,
verge_port: port,
verge_http_enabled: httpEnabled,
verge_tproxy_enabled: tproxyEnabled
});
}
setOpen(false);
showNotice('success', t("Clash Port Modified"));
} catch (err: any) {
showNotice('error', err.message || err.toString());
}
});
return (
<BaseDialog
open={open}
title={t("Port Config")}
contentSx={{ width: 300 }}
okBtn={t("Save")}
cancelBtn={t("Cancel")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
onOk={onSave}
>
<List>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Mixed Port")} />
<TextField
autoComplete="new-password"
size="small"
sx={{ width: 135 }}
value={mixedPort}
onChange={(e) =>
setMixedPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))
}
/>
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Socks Port")} />
<TextField
autoComplete="new-password"
size="small"
sx={{ width: 135 }}
value={socksPort}
onChange={(e) =>
setSocksPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))
}
slotProps={{
input: {
sx: { pr: 1 },
endAdornment: (
<Switch
checked={socksEnabled}
onChange={(_, c) => {
setSocksEnabled(c);
}}
/>
),
}
}}
/>
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Http Port")} />
<TextField
autoComplete="new-password"
size="small"
sx={{ width: 135 }}
value={port}
onChange={(e) =>
setPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))
}
slotProps={{
input: {
sx: { pr: 1 },
endAdornment: (
<Switch
checked={httpEnabled}
onChange={(_, c) => {
setHttpEnabled(c);
}}
/>
),
}
}}
/>
</ListItem>
{OS !== "windows" && (
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Redir Port")} />
<TextField
autoComplete="new-password"
size="small"
sx={{ width: 135 }}
value={redirPort}
onChange={(e) =>
setRedirPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))
}
slotProps={{
input: {
sx: { pr: 1 },
endAdornment: (
<Switch
checked={redirEnabled}
onChange={(_, c) => {
setRedirEnabled(c);
}}
/>
),
}
}}
setOpen(false);
showNotice("success", t("Port settings saved"));
} catch (err) {
showNotice("error", t("Failed to save settings"));
}
});
return (
<BaseDialog
open={open}
title={t("Port Configuration")}
contentSx={{ width: 360, padding: "8px" }}
okBtn={t("Save")}
cancelBtn={t("Cancel")}
onClose={() => setOpen(false)}
onOk={onSave}
>
<List sx={{ width: "100%" }}>
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
<ListItemText
primary={t("Mixed Port")}
primaryTypographyProps={{ fontSize: 12 }}
/>
<div style={{ display: "flex", alignItems: "center" }}>
<TextField
size="small"
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
value={mixedPort}
onChange={(e) => setMixedPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))}
inputProps={{ style: { fontSize: 12 } }}
/>
<IconButton
size="small"
onClick={() => setMixedPort(generateRandomPort())}
title={t("Random Port")}
sx={{ mr: 0.5 }}
>
<Shuffle fontSize="small" />
</IconButton>
<Switch
size="small"
checked={true}
disabled={true}
sx={{ ml: 0.5, opacity: 0.7 }}
/>
</div>
</ListItem>
)}
{OS === "linux" && (
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Tproxy Port")} />
<TextField
autoComplete="new-password"
size="small"
sx={{ width: 135 }}
value={tproxyPort}
onChange={(e) =>
setTproxyPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))
}
slotProps={{
input: {
sx: { pr: 1 },
endAdornment: (
<Switch
checked={tproxyEnabled}
onChange={(_, c) => {
setTproxyEnabled(c);
}}
/>
),
}
}}
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
<ListItemText
primary={t("Socks Port")}
primaryTypographyProps={{ fontSize: 12 }}
/>
<div style={{ display: "flex", alignItems: "center" }}>
<TextField
size="small"
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
value={socksPort}
onChange={(e) => setSocksPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))}
disabled={!socksEnabled}
inputProps={{ style: { fontSize: 12 } }}
/>
<IconButton
size="small"
onClick={() => setSocksPort(generateRandomPort())}
title={t("Random Port")}
disabled={!socksEnabled}
sx={{ mr: 0.5 }}
>
<Shuffle fontSize="small" />
</IconButton>
<Switch
size="small"
checked={socksEnabled}
onChange={(_, c) => setSocksEnabled(c)}
sx={{ ml: 0.5 }}
/>
</div>
</ListItem>
)}
</List>
</BaseDialog>
);
});
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
<ListItemText
primary={t("HTTP Port")}
primaryTypographyProps={{ fontSize: 12 }}
/>
<div style={{ display: "flex", alignItems: "center" }}>
<TextField
size="small"
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
value={httpPort}
onChange={(e) => setHttpPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))}
disabled={!httpEnabled}
inputProps={{ style: { fontSize: 12 } }}
/>
<IconButton
size="small"
onClick={() => setHttpPort(generateRandomPort())}
title={t("Random Port")}
disabled={!httpEnabled}
sx={{ mr: 0.5 }}
>
<Shuffle fontSize="small" />
</IconButton>
<Switch
size="small"
checked={httpEnabled}
onChange={(_, c) => setHttpEnabled(c)}
sx={{ ml: 0.5 }}
/>
</div>
</ListItem>
{OS !== "windows" && (
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
<ListItemText
primary={t("Redir Port")}
primaryTypographyProps={{ fontSize: 12 }}
/>
<div style={{ display: "flex", alignItems: "center" }}>
<TextField
size="small"
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
value={redirPort}
onChange={(e) => setRedirPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))}
disabled={!redirEnabled}
inputProps={{ style: { fontSize: 12 } }}
/>
<IconButton
size="small"
onClick={() => setRedirPort(generateRandomPort())}
title={t("Random Port")}
disabled={!redirEnabled}
sx={{ mr: 0.5 }}
>
<Shuffle fontSize="small" />
</IconButton>
<Switch
size="small"
checked={redirEnabled}
onChange={(_, c) => setRedirEnabled(c)}
sx={{ ml: 0.5 }}
/>
</div>
</ListItem>
)}
{OS === "linux" && (
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
<ListItemText
primary={t("Tproxy Port")}
primaryTypographyProps={{ fontSize: 12 }}
/>
<div style={{ display: "flex", alignItems: "center" }}>
<TextField
size="small"
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
value={tproxyPort}
onChange={(e) => setTproxyPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))}
disabled={!tproxyEnabled}
inputProps={{ style: { fontSize: 12 } }}
/>
<IconButton
size="small"
onClick={() => setTproxyPort(generateRandomPort())}
title={t("Random Port")}
disabled={!tproxyEnabled}
sx={{ mr: 0.5 }}
>
<Shuffle fontSize="small" />
</IconButton>
<Switch
size="small"
checked={tproxyEnabled}
onChange={(_, c) => setTproxyEnabled(c)}
sx={{ ml: 0.5 }}
/>
</div>
</ListItem>
)}
</List>
</BaseDialog>
);
}
);
@@ -1,36 +1,202 @@
import { forwardRef, useImperativeHandle, useState } from "react";
import { useLockFn } from "ahooks";
import { useTranslation } from "react-i18next";
import { List, ListItem, ListItemText, TextField } from "@mui/material";
import { useClashInfo } from "@/hooks/use-clash";
import { BaseDialog, DialogRef } from "@/components/base";
import { useClashInfo } from "@/hooks/use-clash";
import { showNotice } from "@/services/noticeService";
import {
ContentCopy,
RefreshRounded,
} from "@mui/icons-material";
import {
Alert,
Box,
CircularProgress,
FormControlLabel,
IconButton,
List,
ListItem,
ListItemText,
Snackbar,
Switch,
TextField,
Tooltip
} from "@mui/material";
import { useLocalStorageState, useLockFn } from "ahooks";
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
import { useTranslation } from "react-i18next";
// 随机端口和密码生成
const generateRandomPort = (): number => {
return Math.floor(Math.random() * (65535 - 1024 + 1)) + 1024;
};
const generateRandomPassword = (length: number = 32): string => {
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let password = "";
for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * charset.length);
password += charset.charAt(randomIndex);
}
return password;
};
export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
// 防止数值null
const [autoGenerateState, setAutoGenerate] = useLocalStorageState<boolean>(
'autoGenerateConfig',
{ defaultValue: false as boolean }
);
const autoGenerate = autoGenerateState!;
const [copySuccess, setCopySuccess] = useState<null | string>(null);
const [isSaving, setIsSaving] = useState(false);
const [isRestarting, setIsRestarting] = useState(false);
const { clashInfo, patchInfo } = useClashInfo();
const [controller, setController] = useState(clashInfo?.server || "");
const [secret, setSecret] = useState(clashInfo?.secret || "");
// 直接通过API重启内核
const restartCoreDirectly = useLockFn(async () => {
try {
const controllerUrl = controller || clashInfo?.server || 'http://localhost:9090';
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
if (secret) {
headers['Authorization'] = `Bearer ${secret}`;
}
const response = await fetch(`${controllerUrl}/restart`, {
method: 'POST',
headers,
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(errorText || 'Failed to restart core');
}
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
return await response.json();
} else {
const text = await response.text();
console.log('Non-JSON response:', text);
return { message: 'Restart request sent successfully' };
}
} catch (err: any) {
console.error('Error restarting core:', err);
throw err;
}
});
// 生成随机配置并重启内核
const generateAndRestart = useLockFn(async () => {
try {
setIsRestarting(true);
const port = generateRandomPort();
const password = generateRandomPassword();
const host = controller.split(':')[0] || '127.0.0.1';
const newController = `${host}:${port}`;
setController(newController);
setSecret(password);
// 更新配置
await patchInfo({ "external-controller": newController, secret: password });
// 直接重启内核
await restartCoreDirectly();
// 静默执行,不显示通知
} catch (err: any) {
showNotice('error', err.message || t("Failed to generate configuration or restart core"), 4000);
} finally {
setIsRestarting(false);
}
});
// 仅在对话框打开时生成配置
useImperativeHandle(ref, () => ({
open: () => {
open: async () => {
setOpen(true);
setController(clashInfo?.server || "");
setSecret(clashInfo?.secret || "");
if (autoGenerate) {
await generateAndRestart();
} else {
setController(clashInfo?.server || "");
setSecret(clashInfo?.secret || "");
}
},
close: () => setOpen(false),
}));
// 当自动生成开关状态变化时触发
useEffect(() => {
if (autoGenerate && open) {
generateAndRestart();
}
}, [autoGenerate, open]);
// 保存函数(优化)
const onSave = useLockFn(async () => {
if (!controller.trim()) {
showNotice('info', t("Controller address cannot be empty"), 3000);
return;
}
try {
setIsSaving(true);
await patchInfo({ "external-controller": controller, secret });
showNotice('success', t("External Controller Address Modified"), 1000);
showNotice('success', t("Configuration saved successfully"), 2000);
setOpen(false);
} catch (err: any) {
showNotice('error', err.message || err.toString(), 4000);
showNotice('error', err.message || t("Failed to save configuration"), 4000);
} finally {
setIsSaving(false);
}
});
// 生成随机端口
const handleGeneratePort = useLockFn(async () => {
if (!autoGenerate) {
const port = generateRandomPort();
const host = controller.split(':')[0] || '127.0.0.1';
setController(`${host}:${port}`);
showNotice('success', t("Random port generated"), 1000);
}
return Promise.resolve();
});
// 生成随机 Secret
const handleGenerateSecret = useLockFn(async () => {
if (!autoGenerate) {
const password = generateRandomPassword();
setSecret(password);
showNotice('success', t("Random secret generated"), 1000);
}
return Promise.resolve();
});
// 复制到剪贴板
const handleCopyToClipboard = useLockFn(async (text: string, type: string) => {
try {
await navigator.clipboard.writeText(text);
setCopySuccess(type);
setTimeout(() => setCopySuccess(null), 2000);
} catch (err) {
showNotice('error', t("Failed to copy"), 2000);
}
});
@@ -39,39 +205,141 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
open={open}
title={t("External Controller")}
contentSx={{ width: 400 }}
okBtn={t("Save")}
okBtn={
isSaving ? (
<Box display="flex" alignItems="center" gap={1}>
<CircularProgress size={16} color="inherit" />
{t("Saving...")}
</Box>
) : (
t("Save")
)
}
cancelBtn={t("Cancel")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
onOk={onSave}
>
<List>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("External Controller")} />
<TextField
autoComplete="new-password"
size="small"
sx={{ width: 175 }}
value={controller}
placeholder="Required"
onChange={(e) => setController(e.target.value)}
/>
<ListItem sx={{ padding: "5px 2px", display: "flex", justifyContent: "space-between" }}>
<Box display="flex" alignItems="center" gap={1}>
<ListItemText primary={t("External Controller")} />
<Tooltip title={t("Generate Random Port")}>
<IconButton
size="small"
onClick={handleGeneratePort}
color="primary"
disabled={autoGenerate || isSaving || isRestarting}
>
<RefreshRounded fontSize="small" />
</IconButton>
</Tooltip>
</Box>
<Box display="flex" alignItems="center" gap={1}>
<TextField
autoComplete="new-password"
size="small"
sx={{ width: 175, opacity: autoGenerate ? 0.7 : 1 }}
value={controller}
placeholder="Required"
onChange={(e) => setController(e.target.value)}
disabled={autoGenerate || isSaving || isRestarting}
/>
{autoGenerate && (
<Tooltip title={t("Copy to clipboard")}>
<IconButton
size="small"
onClick={() => handleCopyToClipboard(controller, "controller")}
color="primary"
disabled={isSaving || isRestarting}
>
<ContentCopy fontSize="small" />
</IconButton>
</Tooltip>
)}
</Box>
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Core Secret")} />
<TextField
autoComplete="new-password"
size="small"
sx={{ width: 175 }}
value={secret}
placeholder={t("Recommended")}
onChange={(e) =>
setSecret(e.target.value?.replace(/[^\x00-\x7F]/g, ""))
<ListItem sx={{ padding: "5px 2px", display: "flex", justifyContent: "space-between" }}>
<Box display="flex" alignItems="center" gap={1}>
<ListItemText primary={t("Core Secret")} />
<Tooltip title={t("Generate Random Secret")}>
<IconButton
size="small"
onClick={handleGenerateSecret}
color="primary"
disabled={autoGenerate || isSaving || isRestarting}
>
<RefreshRounded fontSize="small" />
</IconButton>
</Tooltip>
</Box>
<Box display="flex" alignItems="center" gap={1}>
<TextField
autoComplete="new-password"
size="small"
sx={{ width: 175, opacity: autoGenerate ? 0.7 : 1 }}
value={secret}
placeholder={t("Recommended")}
onChange={(e) =>
setSecret(e.target.value?.replace(/[^\x00-\x7F]/g, ""))
}
disabled={autoGenerate || isSaving || isRestarting}
/>
{autoGenerate && (
<Tooltip title={t("Copy to clipboard")}>
<IconButton
size="small"
onClick={() => handleCopyToClipboard(secret, "secret")}
color="primary"
disabled={isSaving || isRestarting}
>
<ContentCopy fontSize="small" />
</IconButton>
</Tooltip>
)}
</Box>
</ListItem>
<ListItem sx={{ padding: "5px 2px", display: "flex", justifyContent: "space-between" }}>
<ListItemText
primary={t("Auto Random Config")}
secondary={
autoGenerate
? t("Generate new config and restart core when entering settings")
: t("Manual configuration")
}
/>
<FormControlLabel
control={
<Switch
checked={autoGenerate}
onChange={() => setAutoGenerate(!autoGenerate)}
color="primary"
disabled={isSaving || isRestarting}
/>
}
label={autoGenerate ? t("On") : t("Off")}
labelPlacement="start"
/>
</ListItem>
</List>
<Snackbar
open={copySuccess !== null}
autoHideDuration={2000}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
>
<Alert
severity="success"
sx={{ width: '100%' }}
>
{copySuccess === "controller"
? t("Controller address copied to clipboard")
: t("Secret copied to clipboard")
}
</Alert>
</Snackbar>
</BaseDialog>
);
});
@@ -187,9 +187,10 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
>
{[
{ key: t("Never Clean"), value: 0 },
{ key: t("Retain _n Days", { n: 7 }), value: 1 },
{ key: t("Retain _n Days", { n: 30 }), value: 2 },
{ key: t("Retain _n Days", { n: 90 }), value: 3 },
{ key: t("Retain _n Days", { n: 1 }), value: 1 },
{ key: t("Retain _n Days", { n: 7 }), value: 2 },
{ key: t("Retain _n Days", { n: 30 }), value: 3 },
{ key: t("Retain _n Days", { n: 90 }), value: 4 },
].map((i) => (
<MenuItem key={i.value} value={i.value}>
{i.key}
@@ -67,13 +67,13 @@ export const WebUIViewer = forwardRef<DialogRef>((props, ref) => {
url = url.replaceAll("%port", port || "9097");
url = url.replaceAll(
"%secret",
encodeURIComponent(clashInfo.secret || "")
encodeURIComponent(clashInfo.secret || ""),
);
}
await openWebUrl(url);
} catch (e: any) {
showNotice('error', e.message || e.toString());
showNotice("error", e.message || e.toString());
}
});
@@ -1,30 +1,30 @@
import { useRef, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { TextField, Select, MenuItem, Typography } from "@mui/material";
import {
SettingsRounded,
ShuffleRounded,
LanRounded,
} from "@mui/icons-material";
import { DialogRef, Switch } from "@/components/base";
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
import { useClash } from "@/hooks/use-clash";
import { GuardState } from "./mods/guard-state";
import { WebUIViewer } from "./mods/web-ui-viewer";
import { ClashPortViewer } from "./mods/clash-port-viewer";
import { ControllerViewer } from "./mods/controller-viewer";
import { SettingList, SettingItem } from "./mods/setting-comp";
import { ClashCoreViewer } from "./mods/clash-core-viewer";
import { invoke_uwp_tool } from "@/services/cmds";
import getSystem from "@/utils/get-system";
import { useListen } from "@/hooks/use-listen";
import { useVerge } from "@/hooks/use-verge";
import { updateGeoData } from "@/services/api";
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
import { NetworkInterfaceViewer } from "./mods/network-interface-viewer";
import { DnsViewer } from "./mods/dns-viewer";
import { invoke_uwp_tool } from "@/services/cmds";
import { showNotice } from "@/services/noticeService";
import getSystem from "@/utils/get-system";
import {
LanRounded,
SettingsRounded
} from "@mui/icons-material";
import ErrorOutlineRounded from '@mui/icons-material/ErrorOutlineRounded';
import { MenuItem, Select, TextField, Typography } from "@mui/material";
import { invoke } from "@tauri-apps/api/core";
import { useLockFn } from "ahooks";
import { useListen } from "@/hooks/use-listen";
import { showNotice } from "@/services/noticeService";
import { useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { ClashCoreViewer } from "./mods/clash-core-viewer";
import { ClashPortViewer } from "./mods/clash-port-viewer";
import { ControllerViewer } from "./mods/controller-viewer";
import { DnsViewer } from "./mods/dns-viewer";
import { GuardState } from "./mods/guard-state";
import { NetworkInterfaceViewer } from "./mods/network-interface-viewer";
import { SettingItem, SettingList } from "./mods/setting-comp";
import { WebUIViewer } from "./mods/web-ui-viewer";
const isWIN = getSystem() === "windows";
@@ -50,16 +50,9 @@ const SettingClash = ({ onError }: Props) => {
// 独立跟踪DNS设置开关状态
const [dnsSettingsEnabled, setDnsSettingsEnabled] = useState(() => {
// 尝试从localStorage获取之前保存的状态
// 如果重装(或删除数据更新)前开关处于关闭状态,重装后会获取到错误的状态
// const savedState = localStorage.getItem("dns_settings_enabled");
// if (savedState !== null) {
// return savedState === "true";
// }
// 如果没有保存的状态,则从verge配置中获取
return verge?.enable_dns_settings ?? false;
});
const { addListener } = useListen();
const webRef = useRef<DialogRef>(null);
@@ -88,24 +81,18 @@ const SettingClash = ({ onError }: Props) => {
// 实现DNS设置开关处理函数
const handleDnsToggle = useLockFn(async (enable: boolean) => {
try {
// 立即更新UI状态
setDnsSettingsEnabled(enable);
// 保存到localStorage,用于记住用户的选择
localStorage.setItem("dns_settings_enabled", String(enable));
// 更新verge配置
await patchVerge({ enable_dns_settings: enable });
await invoke("apply_dns_config", { apply: enable });
setTimeout(() => {
mutateClash();
}, 500); // 延迟500ms确保后端完成处理
}, 500);
} catch (err: any) {
// 如果出错,恢复原始状态
setDnsSettingsEnabled(!enable);
localStorage.setItem("dns_settings_enabled", String(!enable));
showNotice('error', err.message || err.toString());
await patchVerge({ enable_dns_settings: !enable }).catch(() => {
// 忽略恢复状态时的错误
});
await patchVerge({ enable_dns_settings: !enable }).catch(() => {});
throw err;
}
});
@@ -219,22 +206,10 @@ const SettingClash = ({ onError }: Props) => {
<SettingItem
label={t("Port Config")}
extra={
<TooltipIcon
title={t("Random Port")}
color={enable_random_port ? "primary" : "inherit"}
icon={ShuffleRounded}
onClick={() => {
showNotice('success', t("Restart Application to Apply Modifications"), 1000);
onChangeVerge({ enable_random_port: !enable_random_port });
patchVerge({ enable_random_port: !enable_random_port });
}}
/>
}
>
<TextField
autoComplete="new-password"
disabled={enable_random_port}
disabled={false}
size="small"
value={verge_mixed_port ?? 7897}
sx={{ width: 100, input: { py: "7.5px", cursor: "pointer" } }}
@@ -245,10 +220,20 @@ const SettingClash = ({ onError }: Props) => {
/>
</SettingItem>
<SettingItem
onClick={() => ctrlRef.current?.open()}
label={t("External")}
/>
<SettingItem
onClick={() => ctrlRef.current?.open()}
label={
<>
{t("External")}
<TooltipIcon
title={t("Enable one-click random API port and key. Click to randomize the port and key")}
color={"error"}
icon={ErrorOutlineRounded}
sx={{ fontSize: "0.875em", ml: 0.5 }}
/>
</>
}
/>
<SettingItem onClick={() => webRef.current?.open()} label={t("Web UI")} />
+18 -5
View File
@@ -274,11 +274,11 @@
"Unified Delay Info": "When unified delay is turned on, two delay tests will be performed to eliminate the delay differences between different types of nodes caused by connection handshakes, etc",
"Log Level": "Log Level",
"Log Level Info": "This parameter is valid only for kernel log files in the log directory Service folder",
"Port Config": "Port Config",
"Port Configuration": "Port Configuration",
"Random Port": "Random Port",
"Mixed Port": "Mixed Port",
"Socks Port": "Socks Port",
"Http Port": "Http(s) Port",
"HTTP Port": "Http(s) Port",
"Redir Port": "Redir Port",
"Tproxy Port": "Tproxy Port",
"External": "External",
@@ -392,7 +392,7 @@
"Stopping Core...": "Stopping Core...",
"Restarting Core...": "Restarting Core...",
"Installing Service...": "Installing Service...",
"Uninstall Service":"Uninstall Service",
"Uninstall Service": "Uninstall Service",
"Service Installed Successfully": "Service Installed Successfully",
"Service is ready and core restarted": "Service is ready and core restarted",
"Core restarted. Service is now available.": "Core restarted. Service is now available.",
@@ -615,5 +615,18 @@
"Originals Only": "Originals Only",
"No (IP Banned By Disney+)": "No (IP Banned By Disney+)",
"Unsupported Country/Region": "Unsupported Country/Region",
"Failed (Network Connection)": "Failed (Network Connection)"
}
"Failed (Network Connection)": "Failed (Network Connection)",
"Auto Random Config": "Auto Random Config",
"Generate new config and restart core when entering settings": "Generate new config and restart core when entering settings",
"Manual configuration": "Manual configuration",
"Controller address copied to clipboard": "Controller address copied to clipboard",
"Secret copied to clipboard": "Secret copied to clipboard",
"Copy to clipboard": "Copy to clipboard",
"Generate Random Secret": "Generate Random Secret",
"Generate Random Port": "Generate Random Port",
"Port Config": "Port Config",
"Configuration saved successfully": "Configuration saved successfully",
"Last generated": "Last generated",
"External Controller Config": "External Controller Config",
"Enable one-click random API port and key. Click to randomize the port and key": "Enable one-click random API port and key. Click to randomize the port and key"
}
+19 -6
View File
@@ -274,13 +274,13 @@
"Unified Delay Info": "开启统一延迟时,会进行两次延迟测试,以消除连接握手等带来的不同类型节点的延迟差异",
"Log Level": "日志等级",
"Log Level Info": "仅对日志目录 Service 文件夹下的内核日志文件生效",
"Port Config": "端口设置",
"Port Configuration": "端口设置",
"Random Port": "随机端口",
"Mixed Port": "混合代理端口",
"Socks Port": "SOCKS 代理端口",
"Http Port": "HTTP(S) 代理端口",
"HTTP Port": "HTTP(S) 代理端口",
"Redir Port": "Redir 透明代理端口",
"TPROXY Port": "TPROXY 透明代理端口",
"Tproxy Port": "TPROXY 透明代理端口",
"External": "外部控制",
"External Controller": "外部控制器监听地址",
"Core Secret": "API 访问密钥",
@@ -392,7 +392,7 @@
"Stopping Core...": "内核停止中...",
"Restarting Core...": "内核重启中...",
"Installing Service...": "安装服务中...",
"Uninstall Service":"卸载服务",
"Uninstall Service": "卸载服务",
"Service Installed Successfully": "已成功安装服务",
"Service is ready and core restarted": "服务已就绪,内核已重启",
"Core restarted. Service is now available.": "内核已重启,服务已就绪",
@@ -615,5 +615,18 @@
"Originals Only": "仅限原创",
"No (IP Banned By Disney+)": "不支持(IP被Disney+禁止)",
"Unsupported Country/Region": "不支持的国家/地区",
"Failed (Network Connection)": "测试失败(网络连接问题)"
}
"Failed (Network Connection)": "测试失败(网络连接问题)",
"Auto Random Config": "启动时自动随机端口和密码",
"Generate new config and restart core when entering settings": "自动随机API端口和密码,重新进入设置即可",
"Manual configuration": "手动配置",
"Controller address copied to clipboard": "API端口已经复制到剪贴板",
"Secret copied to clipboard": "API密钥已经复制到剪贴板",
"Copy to clipboard": "点击我复制",
"Generate Random Secret": "随机API密钥",
"Generate Random Port": "随机API端口",
"Port Config": "端口设置",
"Configuration saved successfully": "随机配置,保存完成",
"Last generated": "记录生成",
"External Controller Config": "API配置",
"Enable one-click random API port and key. Click to randomize the port and key": "开启一键随机API端口和密钥,点进去就可以随机端口和密钥"
}
+20 -12
View File
@@ -2,7 +2,7 @@ import dayjs from "dayjs";
import i18next from "i18next";
import relativeTime from "dayjs/plugin/relativeTime";
import { SWRConfig, mutate } from "swr";
import { useEffect, useCallback } from "react";
import { useEffect, useCallback, useState, useRef } from "react";
import { useTranslation } from "react-i18next";
import { useLocation, useRoutes, useNavigate } from "react-router-dom";
import { List, Paper, ThemeProvider, SvgIcon } from "@mui/material";
@@ -150,6 +150,7 @@ const Layout = () => {
const location = useLocation();
const routersEles = useRoutes(routers);
const { addListener, setupCloseListener } = useListen();
const initRef = useRef(false);
const handleNotice = useCallback(
(payload: [string, string]) => {
@@ -215,53 +216,60 @@ const Layout = () => {
}, [handleNotice]);
useEffect(() => {
if (initRef.current) {
console.log("[Layout] 初始化代码已执行过,跳过");
return;
}
console.log("[Layout] 开始执行初始化代码");
initRef.current = true;
const notifyUiStage = async (stage: string) => {
try {
console.log(`UI加载阶段: ${stage}`);
console.log(`[Layout] UI加载阶段: ${stage}`);
await invoke("update_ui_stage", { stage });
} catch (err) {
console.error(`通知UI加载阶段(${stage})失败:`, err);
console.error(`[Layout] 通知UI加载阶段(${stage})失败:`, err);
}
};
const notifyUiCoreReady = async () => {
try {
console.log("核心组件已加载,通知后端");
console.log("[Layout] 核心组件已加载,通知后端");
await invoke("update_ui_stage", { stage: "DomReady" });
} catch (err) {
console.error("通知核心组件加载完成失败:", err);
console.error("[Layout] 通知核心组件加载完成失败:", err);
}
};
const notifyUiResourcesLoaded = async () => {
try {
console.log("所有资源已加载,通知后端");
console.log("[Layout] 所有资源已加载,通知后端");
await invoke("update_ui_stage", { stage: "ResourcesLoaded" });
} catch (err) {
console.error("通知资源加载完成失败:", err);
console.error("[Layout] 通知资源加载完成失败:", err);
}
};
const notifyUiReady = async () => {
try {
console.log("UI完全准备就绪,通知后端");
console.log("[Layout] UI完全准备就绪,通知后端");
await invoke("notify_ui_ready");
} catch (err) {
console.error("通知UI准备就绪失败:", err);
console.error("[Layout] 通知UI准备就绪失败:", err);
}
};
// 监听后端发送的启动完成事件
const listenStartupCompleted = async () => {
try {
console.log("开始监听启动完成事件");
console.log("[Layout] 开始监听启动完成事件");
const unlisten = await listen("verge://startup-completed", () => {
console.log("收到启动完成事件,开始通知UI就绪");
console.log("[Layout] 收到启动完成事件,开始通知UI就绪");
notifyUiReady();
});
return unlisten;
} catch (err) {
console.error("监听启动完成事件失败:", err);
console.error("[Layout] 监听启动完成事件失败:", err);
return () => {};
}
};
+1 -1
View File
@@ -785,7 +785,7 @@ interface IVergeConfig {
default_latency_test?: string;
default_latency_timeout?: number;
enable_builtin_enhanced?: boolean;
auto_log_clean?: 0 | 1 | 2 | 3;
auto_log_clean?: 0 | 1 | 2 | 3 | 4;
proxy_layout_column?: number;
test_list?: IVergeTestItem[];
webdav_url?: string;
+2 -2
View File
@@ -1,2 +1,2 @@
LINUX_VERSION-5.15 = .182
LINUX_KERNEL_HASH-5.15.182 = b6abfa53315a04e459070b927c58beb41f085433117d58756504d68b67f6a31e
LINUX_VERSION-5.15 = .183
LINUX_KERNEL_HASH-5.15.183 = d06f7f629a4d61a87ebd0db285ace9ebf4fce0226b10b2c0ec235e3550c58ee8
+2 -2
View File
@@ -1,2 +1,2 @@
LINUX_VERSION-6.1 = .138
LINUX_KERNEL_HASH-6.1.138 = e319a5bb9049ba9fb8cbc08cba4874716e8985bd10f7971f2573ea802c257911
LINUX_VERSION-6.1 = .139
LINUX_KERNEL_HASH-6.1.139 = f66affdfee8b6cf8a14cfa00cc7842f79af0e7a70a68604289074f3ecffc9f18
+2 -2
View File
@@ -1,2 +1,2 @@
LINUX_VERSION-6.6 = .90
LINUX_KERNEL_HASH-6.6.90 = ff856748671629c1fefef219099e0b4b81131c2d325e768cb0806e204157014e
LINUX_VERSION-6.6 = .91
LINUX_KERNEL_HASH-6.6.91 = d08d3d175407a52cd0b25fc95e149bbd2fd6922cd37816c8fcfad18f95e254f4
+2 -2
View File
@@ -8,7 +8,7 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=openssl
PKG_VERSION:=3.0.16
PKG_VERSION:=3.5.0
PKG_RELEASE:=1
PKG_USE_MIPS16:=0
PKG_BUILD_FLAGS:=gc-sections no-lto
@@ -19,7 +19,7 @@ PKG_BASE:=$(subst $(space),.,$(wordlist 1,2,$(subst .,$(space),$(PKG_VERSION))))
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://github.com/openssl/openssl/releases/download/$(PKG_NAME)-$(PKG_VERSION)/
PKG_HASH:=57e03c50feab5d31b152af2b764f10379aecd8ee92f16c985983ce4a99f7ef86
PKG_HASH:=344d0a79f1a9b08029b0744e2cc401a43f9c90acd1044d09a530b4885a8e9fc0
PKG_LICENSE:=Apache-2.0
PKG_LICENSE_FILES:=LICENSE
@@ -10,7 +10,7 @@ Signed-off-by: Eneas U de Queiroz <cote2004-github@yahoo.com>
--- a/Configure
+++ b/Configure
@@ -1677,7 +1677,9 @@ $config{CFLAGS} = [ map { $_ eq '--ossl-
@@ -1810,7 +1810,9 @@ $config{CFLAGS} = [ map { $_ eq '--ossl-
unless ($disabled{afalgeng}) {
$config{afalgeng}="";
@@ -18,6 +18,6 @@ Signed-off-by: Eneas U de Queiroz <cote2004-github@yahoo.com>
+ if ($target =~ m/openwrt$/) {
+ push @{$config{engdirs}}, "afalg";
+ } elsif (grep { $_ eq 'afalgeng' } @{$target{enable}}) {
my $minver = 4*10000 + 1*100 + 0;
if ($config{CROSS_COMPILE} eq "") {
my $verstr = `uname -r`;
push @{$config{engdirs}}, "afalg";
} else {
disable('not-linux', 'afalgeng');
@@ -10,7 +10,7 @@ Signed-off-by: Eneas U de Queiroz <cote2004-github@yahoo.com>
--- a/crypto/build.info
+++ b/crypto/build.info
@@ -109,7 +109,7 @@ DEFINE[../libcrypto]=$UPLINKDEF
@@ -115,7 +115,7 @@ DEFINE[../libcrypto]=$UPLINKDEF
DEPEND[info.o]=buildinf.h
DEPEND[cversion.o]=buildinf.h
@@ -16,7 +16,7 @@ Signed-off-by: Eneas U de Queiroz <cote2004-github@yahoo.com>
--- a/ssl/ssl_ciph.c
+++ b/ssl/ssl_ciph.c
@@ -1506,11 +1506,29 @@ STACK_OF(SSL_CIPHER) *ssl_create_cipher_
@@ -1488,11 +1488,29 @@ STACK_OF(SSL_CIPHER) *ssl_create_cipher_
ssl_cipher_apply_rule(0, SSL_kECDHE, 0, 0, 0, 0, 0, CIPHER_DEL, -1, &head,
&tail);
@@ -46,7 +46,7 @@ Signed-off-by: Eneas U de Queiroz <cote2004-github@yahoo.com>
/*
* ...and generally, our preferred cipher is AES.
@@ -1565,7 +1583,7 @@ STACK_OF(SSL_CIPHER) *ssl_create_cipher_
@@ -1547,7 +1565,7 @@ STACK_OF(SSL_CIPHER) *ssl_create_cipher_
* Within each group, ciphers remain sorted by strength and previous
* preference, i.e.,
* 1) ECDHE > DHE
@@ -55,7 +55,7 @@ Signed-off-by: Eneas U de Queiroz <cote2004-github@yahoo.com>
* 3) AES > rest
* 4) TLS 1.2 > legacy
*
@@ -2236,7 +2254,13 @@ const char *OSSL_default_cipher_list(voi
@@ -2246,7 +2264,13 @@ const char *OSSL_default_cipher_list(voi
*/
const char *OSSL_default_ciphersuites(void)
{
@@ -71,7 +71,7 @@ Signed-off-by: Eneas U de Queiroz <cote2004-github@yahoo.com>
}
--- a/include/openssl/ssl.h.in
+++ b/include/openssl/ssl.h.in
@@ -195,9 +195,15 @@ extern "C" {
@@ -199,9 +199,15 @@ extern "C" {
* DEPRECATED IN 3.0.0, in favor of OSSL_default_ciphersuites()
* Update both macro and function simultaneously
*/
@@ -21,7 +21,7 @@ Signed-off-by: Eneas U de Queiroz <cote2004-github@yahoo.com>
--- a/engines/e_devcrypto.c
+++ b/engines/e_devcrypto.c
@@ -905,7 +905,7 @@ static void prepare_digest_methods(void)
@@ -906,7 +906,7 @@ static void prepare_digest_methods(void)
for (i = 0, known_digest_nids_amount = 0; i < OSSL_NELEM(digest_data);
i++) {
@@ -30,7 +30,7 @@ Signed-off-by: Eneas U de Queiroz <cote2004-github@yahoo.com>
/*
* Check that the digest is usable
@@ -1119,7 +1119,7 @@ static const ENGINE_CMD_DEFN devcrypto_c
@@ -1120,7 +1120,7 @@ static const ENGINE_CMD_DEFN devcrypto_c
#ifdef IMPLEMENT_DIGEST
{DEVCRYPTO_CMD_DIGESTS,
"DIGESTS",
@@ -10,7 +10,7 @@ Signed-off-by: Eneas U de Queiroz <cote2004-github@yahoo.com>
--- a/engines/e_devcrypto.c
+++ b/engines/e_devcrypto.c
@@ -211,9 +211,8 @@ static int cipher_init(EVP_CIPHER_CTX *c
@@ -212,9 +212,8 @@ static int cipher_init(EVP_CIPHER_CTX *c
int ret;
/* cleanup a previous session */
+36 -119
View File
@@ -118,7 +118,10 @@ func (u *UIUpdater) downloadUI() error {
tmpDir := C.Path.Resolve("downloadUI.tmp")
defer os.RemoveAll(tmpDir)
extractedFolder, err := extract(data, tmpDir)
os.RemoveAll(tmpDir) // cleanup tmp dir before extract
log.Debugln("extractedFolder: %s", tmpDir)
err = extract(data, tmpDir)
if err != nil {
return fmt.Errorf("can't extract compressed file: %w", err)
}
@@ -136,8 +139,8 @@ func (u *UIUpdater) downloadUI() error {
return fmt.Errorf("prepare UI path failed: %w", err)
}
log.Debugln("moveFolder from %s to %s", extractedFolder, u.externalUIPath)
err = moveDir(extractedFolder, u.externalUIPath) // move files from tmp to target
log.Debugln("moveFolder from %s to %s", tmpDir, u.externalUIPath)
err = moveDir(tmpDir, u.externalUIPath) // move files from tmp to target
if err != nil {
return fmt.Errorf("move UI folder failed: %w", err)
}
@@ -154,63 +157,19 @@ func (u *UIUpdater) prepareUIPath() error {
return nil
}
func unzip(data []byte, dest string) (string, error) {
func unzip(data []byte, dest string) error {
r, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
if err != nil {
return "", err
return err
}
// check whether or not only exists singleRoot dir
rootDir := ""
isSingleRoot := true
rootItemCount := 0
for _, f := range r.File {
parts := strings.Split(strings.Trim(f.Name, "/"), "/")
if len(parts) == 0 {
continue
}
if len(parts) == 1 {
isDir := strings.HasSuffix(f.Name, "/")
if !isDir {
isSingleRoot = false
break
}
if rootDir == "" {
rootDir = parts[0]
}
rootItemCount++
}
}
if rootItemCount != 1 {
isSingleRoot = false
}
// build the dir of extraction
var extractedFolder string
if isSingleRoot && rootDir != "" {
// if the singleRoot, use it directly
log.Debugln("Match the singleRoot")
extractedFolder = filepath.Join(dest, rootDir)
log.Debugln("extractedFolder: %s", extractedFolder)
} else {
log.Debugln("Match the multiRoot")
extractedFolder = dest
log.Debugln("extractedFolder: %s", extractedFolder)
}
for _, f := range r.File {
var fpath string
if isSingleRoot && rootDir != "" {
fpath = filepath.Join(dest, f.Name)
} else {
fpath = filepath.Join(extractedFolder, f.Name)
}
fpath := filepath.Join(dest, f.Name)
if !inDest(fpath, dest) {
return "", fmt.Errorf("invalid file path: %s", fpath)
return fmt.Errorf("invalid file path: %s", fpath)
}
info := f.FileInfo()
if info.IsDir() {
@@ -221,128 +180,77 @@ func unzip(data []byte, dest string) (string, error) {
continue // disallow symlink
}
if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
return "", err
return err
}
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
return "", err
return err
}
rc, err := f.Open()
if err != nil {
return "", err
return err
}
_, err = io.Copy(outFile, rc)
outFile.Close()
rc.Close()
if err != nil {
return "", err
return err
}
}
return extractedFolder, nil
return nil
}
func untgz(data []byte, dest string) (string, error) {
func untgz(data []byte, dest string) error {
gzr, err := gzip.NewReader(bytes.NewReader(data))
if err != nil {
return "", err
return err
}
defer gzr.Close()
tr := tar.NewReader(gzr)
rootDir := ""
isSingleRoot := true
rootItemCount := 0
for {
header, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return "", err
}
parts := strings.Split(cleanTarPath(header.Name), string(os.PathSeparator))
if len(parts) == 0 {
continue
}
if len(parts) == 1 {
isDir := header.Typeflag == tar.TypeDir
if !isDir {
isSingleRoot = false
break
}
if rootDir == "" {
rootDir = parts[0]
}
rootItemCount++
}
}
if rootItemCount != 1 {
isSingleRoot = false
}
_ = gzr.Reset(bytes.NewReader(data))
tr = tar.NewReader(gzr)
var extractedFolder string
if isSingleRoot && rootDir != "" {
log.Debugln("Match the singleRoot")
extractedFolder = filepath.Join(dest, rootDir)
log.Debugln("extractedFolder: %s", extractedFolder)
} else {
log.Debugln("Match the multiRoot")
extractedFolder = dest
log.Debugln("extractedFolder: %s", extractedFolder)
}
for {
header, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return "", err
return err
}
var fpath string
if isSingleRoot && rootDir != "" {
fpath = filepath.Join(dest, cleanTarPath(header.Name))
} else {
fpath = filepath.Join(extractedFolder, cleanTarPath(header.Name))
}
fpath := filepath.Join(dest, header.Name)
if !inDest(fpath, dest) {
return "", fmt.Errorf("invalid file path: %s", fpath)
return fmt.Errorf("invalid file path: %s", fpath)
}
switch header.Typeflag {
case tar.TypeDir:
if err = os.MkdirAll(fpath, os.FileMode(header.Mode)); err != nil {
return "", err
return err
}
case tar.TypeReg:
if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
return "", err
return err
}
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.FileMode(header.Mode))
if err != nil {
return "", err
return err
}
if _, err := io.Copy(outFile, tr); err != nil {
outFile.Close()
return "", err
return err
}
outFile.Close()
}
}
return extractedFolder, nil
return nil
}
func extract(data []byte, dest string) (string, error) {
func extract(data []byte, dest string) error {
fileType := detectFileType(data)
log.Debugln("compression Type: %s", fileType)
switch fileType {
@@ -351,7 +259,7 @@ func extract(data []byte, dest string) (string, error) {
case typeTarGzip:
return untgz(data, dest)
default:
return "", fmt.Errorf("unknown or unsupported file type")
return fmt.Errorf("unknown or unsupported file type")
}
}
@@ -393,6 +301,15 @@ func moveDir(src string, dst string) error {
return err
}
if len(dirEntryList) == 1 && dirEntryList[0].IsDir() {
src = filepath.Join(src, dirEntryList[0].Name())
log.Debugln("match the singleRoot: %s", src)
dirEntryList, err = os.ReadDir(src)
if err != nil {
return err
}
}
for _, dirEntry := range dirEntryList {
err = os.Rename(filepath.Join(src, dirEntry.Name()), filepath.Join(dst, dirEntry.Name()))
if err != nil {
+2
View File
@@ -645,6 +645,7 @@ proxies: # socks5
reality-opts:
public-key: xxx
short-id: xxx # optional
support-x25519mlkem768: false # 如果服务端支持可手动设置为true
client-fingerprint: chrome # cannot be empty
- name: "vless-reality-grpc"
@@ -664,6 +665,7 @@ proxies: # socks5
reality-opts:
public-key: CrrQSjAG_YkHLwvM2M-7XkKJilgL5upBKCp0od0tLhE
short-id: 10f897e26c4b9478
support-x25519mlkem768: false # 如果服务端支持可手动设置为true
- name: "vless-ws"
type: vless
+1 -1
View File
@@ -27,7 +27,7 @@ require (
github.com/metacubex/randv2 v0.2.0
github.com/metacubex/sing v0.5.3-0.20250504031621-1f99e54c15b7
github.com/metacubex/sing-mux v0.3.2
github.com/metacubex/sing-quic v0.0.0-20250517090120-462e75d27336
github.com/metacubex/sing-quic v0.0.0-20250520025433-6e556a6bef7a
github.com/metacubex/sing-shadowsocks v0.2.9
github.com/metacubex/sing-shadowsocks2 v0.2.3
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2
+2 -4
View File
@@ -120,10 +120,8 @@ github.com/metacubex/sing v0.5.3-0.20250504031621-1f99e54c15b7 h1:m4nSxvw46JEgxM
github.com/metacubex/sing v0.5.3-0.20250504031621-1f99e54c15b7/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=
github.com/metacubex/sing-mux v0.3.2 h1:nJv52pyRivHcaZJKk2JgxpaVvj1GAXG81scSa9N7ncw=
github.com/metacubex/sing-mux v0.3.2/go.mod h1:3rt1soewn0O6j89GCLmwAQFsq257u0jf2zQSPhTL3Bw=
github.com/metacubex/sing-quic v0.0.0-20250511034158-b46e0e3e81b2 h1:wfmYgtECbEYo1slMtyo+2kMqscYYDSjU/TVgS3018F4=
github.com/metacubex/sing-quic v0.0.0-20250511034158-b46e0e3e81b2/go.mod h1:P1kd57U6XXmXv9PbwWdznUGT0k9bKgFJXF0fEORbIlk=
github.com/metacubex/sing-quic v0.0.0-20250517090120-462e75d27336 h1:5BgpaFkTzkePwF1A8rmhCqgyOMG79BLsAhFR8W8SiRo=
github.com/metacubex/sing-quic v0.0.0-20250517090120-462e75d27336/go.mod h1:JPTpf7fpnojsSuwRJExhSZSy63pVbp3VM39+zj+sAJM=
github.com/metacubex/sing-quic v0.0.0-20250520025433-6e556a6bef7a h1:Ho73vGiB94LmtK5T+tKVwtCNEi/YiHmPjlqpHSAmAVs=
github.com/metacubex/sing-quic v0.0.0-20250520025433-6e556a6bef7a/go.mod h1:JPTpf7fpnojsSuwRJExhSZSy63pVbp3VM39+zj+sAJM=
github.com/metacubex/sing-shadowsocks v0.2.9 h1:2e++13WNN7EGjGtvrGLUzW1xrCdQbW2gIFpgw5GEw00=
github.com/metacubex/sing-shadowsocks v0.2.9/go.mod h1:CJSEGO4FWQAWe+ZiLZxCweGdjRR60A61SIoVjdjQeBA=
github.com/metacubex/sing-shadowsocks2 v0.2.3 h1:v3rNS/5Ywh0NIZ6VU/NmdERQIN5RePzyxCFeQsU4Cx0=
+37 -23
View File
@@ -18,7 +18,6 @@ import (
"sync"
"time"
"github.com/metacubex/mihomo/common/atomic"
"github.com/metacubex/mihomo/common/buf"
"github.com/metacubex/mihomo/common/pool"
"github.com/metacubex/mihomo/component/ech"
@@ -42,16 +41,19 @@ type DialFn = func(ctx context.Context, network, addr string) (net.Conn, error)
type Conn struct {
initFn func() (io.ReadCloser, netAddr, error)
writer io.Writer
writer io.Writer // writer must not nil
closer io.Closer
netAddr
reader io.ReadCloser
once sync.Once
closed atomic.Bool
err error
remain int
br *bufio.Reader
initOnce sync.Once
initErr error
reader io.ReadCloser
br *bufio.Reader
remain int
closeMutex sync.Mutex
closed bool
// deadlines
deadline *time.Timer
}
@@ -65,7 +67,7 @@ type Config struct {
func (g *Conn) initReader() {
reader, addr, err := g.initFn()
if err != nil {
g.err = err
g.initErr = err
if closer, ok := g.writer.(io.Closer); ok {
closer.Close()
}
@@ -73,17 +75,21 @@ func (g *Conn) initReader() {
}
g.netAddr = addr
if !g.closed.Load() {
g.reader = reader
g.br = bufio.NewReader(reader)
} else {
reader.Close()
g.closeMutex.Lock()
defer g.closeMutex.Unlock()
if g.closed { // if g.Close() be called between g.initFn(), direct close the initFn returned reader
_ = reader.Close()
g.initErr = net.ErrClosed
return
}
g.reader = reader
g.br = bufio.NewReader(reader)
}
func (g *Conn) Init() error {
g.once.Do(g.initReader)
return g.err
g.initOnce.Do(g.initReader)
return g.initErr
}
func (g *Conn) Read(b []byte) (n int, err error) {
@@ -100,8 +106,6 @@ func (g *Conn) Read(b []byte) (n int, err error) {
n, err = io.ReadFull(g.br, b[:size])
g.remain -= n
return
} else if g.reader == nil {
return 0, net.ErrClosed
}
// 0x00 grpclength(uint32) 0x0A uleb128 payload
@@ -147,8 +151,8 @@ func (g *Conn) Write(b []byte) (n int, err error) {
buf.Write(b)
_, err = g.writer.Write(buf.Bytes())
if err == io.ErrClosedPipe && g.err != nil {
err = g.err
if err == io.ErrClosedPipe && g.initErr != nil {
err = g.initErr
}
if flusher, ok := g.writer.(http.Flusher); ok {
@@ -170,8 +174,8 @@ func (g *Conn) WriteBuffer(buffer *buf.Buffer) error {
binary.PutUvarint(header[6:], uint64(dataLen))
_, err := g.writer.Write(buffer.Bytes())
if err == io.ErrClosedPipe && g.err != nil {
err = g.err
if err == io.ErrClosedPipe && g.initErr != nil {
err = g.initErr
}
if flusher, ok := g.writer.(http.Flusher); ok {
@@ -186,7 +190,17 @@ func (g *Conn) FrontHeadroom() int {
}
func (g *Conn) Close() error {
g.closed.Store(true)
g.initOnce.Do(func() { // if initReader not called, it should not be run anymore
g.initErr = net.ErrClosed
})
g.closeMutex.Lock()
defer g.closeMutex.Unlock()
if g.closed {
return nil
}
g.closed = true
var errorArr []error
if reader := g.reader; reader != nil {
@@ -4,7 +4,10 @@
"read": {
"file": {
"/etc/init.d/ddns-go": [ "exec" ],
"/usr/libexec/ddns-go-call": [ "exec" ],
"/usr/libexec/ddns-go-call": [ "exec" ],
"/bin/pidof": [ "exec" ],
"/bin/ps": [ "exec" ],
"/bin/ash": [ "exec" ],
"/etc/ddns-go/ddns-go-config.yaml": [ "read" ],
"/var/log/ddns-go.log": [ "read" ]
},
+4
View File
@@ -2,6 +2,10 @@
icon: material/alert-decagram
---
#### 1.12.0-beta.16
* Fixes and improvements
#### 1.12.0-beta.15
* Add DERP service **1**
@@ -33,7 +33,9 @@ See [Listen Fields](/configuration/shared/listen/) for details.
==Required==
A mapping Object from HTTP endpoints to Shadowsocks inbound tags.
A mapping Object from HTTP endpoints to [Shadowsocks Inbound](/configuration/inbound/shadowsocks) tags.
Selected Shadowsocks inbounds must be configured with [managed](/configuration/inbound/shadowsocks#managed) enabled.
Example:
+1 -1
View File
@@ -27,7 +27,7 @@ require (
github.com/sagernet/gomobile v0.1.6
github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb
github.com/sagernet/quic-go v0.51.0-beta.5
github.com/sagernet/sing v0.6.10-0.20250505040842-ba62fee9470f
github.com/sagernet/sing v0.6.10-0.20250520114755-0e0545dd92c4
github.com/sagernet/sing-mux v0.3.2
github.com/sagernet/sing-quic v0.4.1-0.20250511050139-d459f561c9c3
github.com/sagernet/sing-shadowsocks v0.2.7
+2 -2
View File
@@ -168,8 +168,8 @@ github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/l
github.com/sagernet/quic-go v0.51.0-beta.5 h1:/mME3sJvQ8k/JKP0oC/9XoWrm0znO7hWXviB5yiipJY=
github.com/sagernet/quic-go v0.51.0-beta.5/go.mod h1:OV+V5kEBb8kJS7k29MzDu6oj9GyMc7HA07sE1tedxz4=
github.com/sagernet/sing v0.6.9/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing v0.6.10-0.20250505040842-ba62fee9470f h1:lttLhNtFuMItQcTD29QP6aBS8kR1UhG7zZ+pwzTYkFM=
github.com/sagernet/sing v0.6.10-0.20250505040842-ba62fee9470f/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing v0.6.10-0.20250520114755-0e0545dd92c4 h1:mP53fzZ9GIFaT1pZ2brV1BhbaDwN8kG8ZR0OYr11EBc=
github.com/sagernet/sing v0.6.10-0.20250520114755-0e0545dd92c4/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-mux v0.3.2 h1:meZVFiiStvHThb/trcpAkCrmtJOuItG5Dzl1RRP5/NE=
github.com/sagernet/sing-mux v0.3.2/go.mod h1:pht8iFY4c9Xltj7rhVd208npkNaeCxzyXCgulDPLUDA=
github.com/sagernet/sing-quic v0.4.1-0.20250511050139-d459f561c9c3 h1:1J+s1yyZ8+YAYaClI+az8YuFgV9NGXUUCZnriKmos6w=
+2 -4
View File
@@ -214,7 +214,6 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
if !loaded {
return nil, E.New("parse route_address_set: rule-set not found: ", routeAddressSet)
}
ruleSet.IncRef()
inbound.routeRuleSet = append(inbound.routeRuleSet, ruleSet)
}
for _, routeExcludeAddressSet := range options.RouteExcludeAddressSet {
@@ -222,7 +221,6 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
if !loaded {
return nil, E.New("parse route_exclude_address_set: rule-set not found: ", routeExcludeAddressSet)
}
ruleSet.IncRef()
inbound.routeExcludeRuleSet = append(inbound.routeExcludeRuleSet, ruleSet)
}
if options.AutoRedirect {
@@ -312,7 +310,7 @@ func (t *Inbound) Start(stage adapter.StartStage) error {
if len(ipSets) == 0 {
t.logger.Warn("route_address_set: no destination IP CIDR rules found in rule-set: ", routeRuleSet.Name())
}
routeRuleSet.DecRef()
routeRuleSet.IncRef()
t.routeAddressSet = append(t.routeAddressSet, ipSets...)
if t.autoRedirect != nil {
t.routeRuleSetCallback = append(t.routeRuleSetCallback, routeRuleSet.RegisterCallback(t.updateRouteAddressSet))
@@ -324,7 +322,7 @@ func (t *Inbound) Start(stage adapter.StartStage) error {
if len(ipSets) == 0 {
t.logger.Warn("route_address_set: no destination IP CIDR rules found in rule-set: ", routeExcludeRuleSet.Name())
}
routeExcludeRuleSet.DecRef()
routeExcludeRuleSet.IncRef()
t.routeExcludeAddressSet = append(t.routeExcludeAddressSet, ipSets...)
if t.autoRedirect != nil {
t.routeExcludeRuleSetCallback = append(t.routeExcludeRuleSetCallback, routeExcludeRuleSet.RegisterCallback(t.updateRouteAddressSet))

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