diff --git a/.github/update.log b/.github/update.log index ef359c0864..b1b05cf8ae 100644 --- a/.github/update.log +++ b/.github/update.log @@ -1200,3 +1200,4 @@ Update On Sat Nov 29 19:37:01 CET 2025 Update On Sun Nov 30 19:38:16 CET 2025 Update On Mon Dec 1 19:44:38 CET 2025 Update On Tue Dec 2 19:43:40 CET 2025 +Update On Wed Dec 3 19:42:45 CET 2025 diff --git a/clash-meta/adapter/outbound/sudoku.go b/clash-meta/adapter/outbound/sudoku.go index 48d0425876..0e6c3bba68 100644 --- a/clash-meta/adapter/outbound/sudoku.go +++ b/clash-meta/adapter/outbound/sudoku.go @@ -5,7 +5,6 @@ import ( "crypto/sha256" "encoding/binary" "fmt" - "io" "net" "strconv" "strings" @@ -14,17 +13,18 @@ import ( "github.com/saba-futai/sudoku/apis" "github.com/saba-futai/sudoku/pkg/crypto" "github.com/saba-futai/sudoku/pkg/obfs/httpmask" - "github.com/saba-futai/sudoku/pkg/obfs/sudoku" + sudokuobfs "github.com/saba-futai/sudoku/pkg/obfs/sudoku" N "github.com/metacubex/mihomo/common/net" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" + "github.com/metacubex/mihomo/transport/sudoku" ) type Sudoku struct { *Base option *SudokuOption - table *sudoku.Table + table *sudokuobfs.Table baseConf apis.ProtocolConfig } @@ -72,12 +72,45 @@ func (s *Sudoku) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Con // ListenPacketContext implements C.ProxyAdapter func (s *Sudoku) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) { - return nil, C.ErrNotSupport + if err := s.ResolveUDP(ctx, metadata); err != nil { + return nil, err + } + + cfg, err := s.buildConfig(metadata) + if err != nil { + return nil, err + } + + c, err := s.dialer.DialContext(ctx, "tcp", s.addr) + if err != nil { + return nil, fmt.Errorf("%s connect error: %w", s.addr, err) + } + + defer func() { + safeConnClose(c, err) + }() + + if ctx.Done() != nil { + done := N.SetupContextForConn(ctx, c) + defer done(&err) + } + + c, err = s.handshakeConn(c, cfg) + if err != nil { + return nil, err + } + + if err = sudoku.WritePreface(c); err != nil { + _ = c.Close() + return nil, fmt.Errorf("send uot preface failed: %w", err) + } + + return newPacketConn(N.NewThreadSafePacketConn(sudoku.NewUoTPacketConn(c)), s), nil } // SupportUOT implements C.ProxyAdapter func (s *Sudoku) SupportUOT() bool { - return false // Sudoku protocol only supports TCP + return true } // ProxyInfo implements C.ProxyAdapter @@ -101,14 +134,14 @@ func (s *Sudoku) buildConfig(metadata *C.Metadata) (*apis.ProtocolConfig, error) return &cfg, nil } -func (s *Sudoku) streamConn(rawConn net.Conn, cfg *apis.ProtocolConfig) (_ net.Conn, err error) { +func (s *Sudoku) handshakeConn(rawConn net.Conn, cfg *apis.ProtocolConfig) (_ net.Conn, err error) { if !cfg.DisableHTTPMask { if err = httpmask.WriteRandomRequestHeader(rawConn, cfg.ServerAddress); err != nil { return nil, fmt.Errorf("write http mask failed: %w", err) } } - obfsConn := sudoku.NewConn(rawConn, cfg.Table, cfg.PaddingMin, cfg.PaddingMax, false) + obfsConn := sudokuobfs.NewConn(rawConn, cfg.Table, cfg.PaddingMin, cfg.PaddingMax, false) cConn, err := crypto.NewAEADConn(obfsConn, cfg.Key, cfg.AEADMethod) if err != nil { return nil, fmt.Errorf("setup crypto failed: %w", err) @@ -120,7 +153,21 @@ func (s *Sudoku) streamConn(rawConn net.Conn, cfg *apis.ProtocolConfig) (_ net.C return nil, fmt.Errorf("send handshake failed: %w", err) } - if err = writeTargetAddress(cConn, cfg.TargetAddress); err != nil { + return cConn, nil +} + +func (s *Sudoku) streamConn(rawConn net.Conn, cfg *apis.ProtocolConfig) (_ net.Conn, err error) { + cConn, err := s.handshakeConn(rawConn, cfg) + if err != nil { + return nil, err + } + + addrBuf, err := sudoku.EncodeAddress(cfg.TargetAddress) + if err != nil { + return nil, fmt.Errorf("encode target address failed: %w", err) + } + + if _, err = cConn.Write(addrBuf); err != nil { cConn.Close() return nil, fmt.Errorf("send target address failed: %w", err) } @@ -153,7 +200,7 @@ func NewSudoku(option SudokuOption) (*Sudoku, error) { } start := time.Now() - table := sudoku.NewTable(seed, tableType) + table := sudokuobfs.NewTable(seed, tableType) log.Infoln("[Sudoku] Tables initialized (%s) in %v", tableType, time.Since(start)) defaultConf := apis.DefaultConfig() @@ -191,7 +238,7 @@ func NewSudoku(option SudokuOption) (*Sudoku, error) { name: option.Name, addr: baseConf.ServerAddress, tp: C.Sudoku, - udp: false, + udp: true, tfo: option.TFO, mpTcp: option.MPTCP, iface: option.Interface, @@ -213,40 +260,3 @@ func buildSudokuHandshakePayload(key string) [16]byte { copy(payload[8:], hash[:8]) return payload } - -func writeTargetAddress(w io.Writer, rawAddr string) error { - host, portStr, err := net.SplitHostPort(rawAddr) - if err != nil { - return err - } - - portInt, err := net.LookupPort("tcp", portStr) - if err != nil { - return err - } - - var buf []byte - if ip := net.ParseIP(host); ip != nil { - if ip4 := ip.To4(); ip4 != nil { - buf = append(buf, 0x01) // IPv4 - buf = append(buf, ip4...) - } else { - buf = append(buf, 0x04) // IPv6 - buf = append(buf, ip...) - } - } else { - if len(host) > 255 { - return fmt.Errorf("domain too long") - } - buf = append(buf, 0x03) // domain - buf = append(buf, byte(len(host))) - buf = append(buf, host...) - } - - var portBytes [2]byte - binary.BigEndian.PutUint16(portBytes[:], uint16(portInt)) - buf = append(buf, portBytes[:]...) - - _, err = w.Write(buf) - return err -} diff --git a/clash-meta/adapter/outboundgroup/fallback.go b/clash-meta/adapter/outboundgroup/fallback.go index 3772107bc1..0174a7b944 100644 --- a/clash-meta/adapter/outboundgroup/fallback.go +++ b/clash-meta/adapter/outboundgroup/fallback.go @@ -150,6 +150,14 @@ func (f *Fallback) ForceSet(name string) { f.selected = name } +func (f *Fallback) Providers() []P.ProxyProvider { + return f.providers +} + +func (f *Fallback) Proxies() []C.Proxy { + return f.GetProxies(false) +} + func NewFallback(option *GroupCommonOption, providers []P.ProxyProvider) *Fallback { return &Fallback{ GroupBase: NewGroupBase(GroupBaseOption{ diff --git a/clash-meta/adapter/outboundgroup/loadbalance.go b/clash-meta/adapter/outboundgroup/loadbalance.go index dff9b5ed9d..19ee38c7de 100644 --- a/clash-meta/adapter/outboundgroup/loadbalance.go +++ b/clash-meta/adapter/outboundgroup/loadbalance.go @@ -239,6 +239,18 @@ func (lb *LoadBalance) MarshalJSON() ([]byte, error) { }) } +func (lb *LoadBalance) Providers() []P.ProxyProvider { + return lb.providers +} + +func (lb *LoadBalance) Proxies() []C.Proxy { + return lb.GetProxies(false) +} + +func (lb *LoadBalance) Now() string { + return "" +} + func NewLoadBalance(option *GroupCommonOption, providers []P.ProxyProvider, strategy string) (lb *LoadBalance, err error) { var strategyFn strategyFn switch strategy { diff --git a/clash-meta/adapter/outboundgroup/patch_android.go b/clash-meta/adapter/outboundgroup/patch_android.go deleted file mode 100644 index f0c254c2b8..0000000000 --- a/clash-meta/adapter/outboundgroup/patch_android.go +++ /dev/null @@ -1,52 +0,0 @@ -//go:build android && cmfa - -package outboundgroup - -import ( - C "github.com/metacubex/mihomo/constant" - P "github.com/metacubex/mihomo/constant/provider" -) - -type ProxyGroup interface { - C.ProxyAdapter - - Providers() []P.ProxyProvider - Proxies() []C.Proxy - Now() string -} - -func (f *Fallback) Providers() []P.ProxyProvider { - return f.providers -} - -func (lb *LoadBalance) Providers() []P.ProxyProvider { - return lb.providers -} - -func (f *Fallback) Proxies() []C.Proxy { - return f.GetProxies(false) -} - -func (lb *LoadBalance) Proxies() []C.Proxy { - return lb.GetProxies(false) -} - -func (lb *LoadBalance) Now() string { - return "" -} - -func (s *Selector) Providers() []P.ProxyProvider { - return s.providers -} - -func (s *Selector) Proxies() []C.Proxy { - return s.GetProxies(false) -} - -func (u *URLTest) Providers() []P.ProxyProvider { - return u.providers -} - -func (u *URLTest) Proxies() []C.Proxy { - return u.GetProxies(false) -} diff --git a/clash-meta/adapter/outboundgroup/selector.go b/clash-meta/adapter/outboundgroup/selector.go index f8975df744..7bc138fdc7 100644 --- a/clash-meta/adapter/outboundgroup/selector.go +++ b/clash-meta/adapter/outboundgroup/selector.go @@ -108,6 +108,14 @@ func (s *Selector) selectedProxy(touch bool) C.Proxy { return proxies[0] } +func (s *Selector) Providers() []P.ProxyProvider { + return s.providers +} + +func (s *Selector) Proxies() []C.Proxy { + return s.GetProxies(false) +} + func NewSelector(option *GroupCommonOption, providers []P.ProxyProvider) *Selector { return &Selector{ GroupBase: NewGroupBase(GroupBaseOption{ diff --git a/clash-meta/adapter/outboundgroup/urltest.go b/clash-meta/adapter/outboundgroup/urltest.go index 2adb3b814e..49ea12aa0b 100644 --- a/clash-meta/adapter/outboundgroup/urltest.go +++ b/clash-meta/adapter/outboundgroup/urltest.go @@ -185,6 +185,14 @@ func (u *URLTest) MarshalJSON() ([]byte, error) { }) } +func (u *URLTest) Providers() []P.ProxyProvider { + return u.providers +} + +func (u *URLTest) Proxies() []C.Proxy { + return u.GetProxies(false) +} + func (u *URLTest) URLTest(ctx context.Context, url string, expectedStatus utils.IntRanges[uint16]) (map[string]uint16, error) { return u.GroupBase.URLTest(ctx, u.testUrl, expectedStatus) } diff --git a/clash-meta/adapter/outboundgroup/util.go b/clash-meta/adapter/outboundgroup/util.go index 66b2510c19..d35ea66f15 100644 --- a/clash-meta/adapter/outboundgroup/util.go +++ b/clash-meta/adapter/outboundgroup/util.go @@ -1,5 +1,29 @@ package outboundgroup +import ( + "context" + + "github.com/metacubex/mihomo/common/utils" + C "github.com/metacubex/mihomo/constant" + P "github.com/metacubex/mihomo/constant/provider" +) + +type ProxyGroup interface { + C.ProxyAdapter + + Providers() []P.ProxyProvider + Proxies() []C.Proxy + Now() string + Touch() + + URLTest(ctx context.Context, url string, expectedStatus utils.IntRanges[uint16]) (mp map[string]uint16, err error) +} + +var _ ProxyGroup = (*Fallback)(nil) +var _ ProxyGroup = (*LoadBalance)(nil) +var _ ProxyGroup = (*URLTest)(nil) +var _ ProxyGroup = (*Selector)(nil) + type SelectAble interface { Set(string) error ForceSet(name string) diff --git a/clash-meta/constant/adapters.go b/clash-meta/constant/adapters.go index c1ac3723e9..0b9098fd69 100644 --- a/clash-meta/constant/adapters.go +++ b/clash-meta/constant/adapters.go @@ -139,11 +139,6 @@ type ProxyAdapter interface { Close() error } -type Group interface { - URLTest(ctx context.Context, url string, expectedStatus utils.IntRanges[uint16]) (mp map[string]uint16, err error) - Touch() -} - type DelayHistory struct { Time time.Time `json:"time"` Delay uint16 `json:"delay"` diff --git a/clash-meta/go.mod b/clash-meta/go.mod index 35dfcbe2b1..500aad8349 100644 --- a/clash-meta/go.mod +++ b/clash-meta/go.mod @@ -23,7 +23,7 @@ require ( github.com/metacubex/fswatch v0.1.1 github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 github.com/metacubex/kcp-go v0.0.0-20251111012849-7455698490e9 - github.com/metacubex/quic-go v0.55.1-0.20251024060151-bd465f127128 + github.com/metacubex/quic-go v0.55.1-0.20251203073212-6940cac967c2 github.com/metacubex/randv2 v0.2.0 github.com/metacubex/restls-client-go v0.1.7 github.com/metacubex/sing v0.5.6 @@ -43,7 +43,7 @@ require ( github.com/mroth/weightedrand/v2 v2.1.0 github.com/openacid/low v0.1.21 github.com/oschwald/maxminddb-golang v1.12.0 // lastest version compatible with golang1.20 - github.com/saba-futai/sudoku v0.0.1-g + github.com/saba-futai/sudoku v0.0.1-i github.com/sagernet/cors v1.2.1 github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a github.com/samber/lo v1.52.0 diff --git a/clash-meta/go.sum b/clash-meta/go.sum index 45210afb98..37cf8ad76b 100644 --- a/clash-meta/go.sum +++ b/clash-meta/go.sum @@ -112,8 +112,8 @@ github.com/metacubex/kcp-go v0.0.0-20251111012849-7455698490e9 h1:7m3tRPrLpKOLOv github.com/metacubex/kcp-go v0.0.0-20251111012849-7455698490e9/go.mod h1:HIJZW4QMhbBqXuqC1ly6Hn0TEYT2SzRw58ns1yGhXTs= github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793 h1:1Qpuy+sU3DmyX9HwI+CrBT/oLNJngvBorR2RbajJcqo= github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793/go.mod h1:RjRNb4G52yAgfR+Oe/kp9G4PJJ97Fnj89eY1BFO3YyA= -github.com/metacubex/quic-go v0.55.1-0.20251024060151-bd465f127128 h1:I1uvJl206/HbkzEAZpLgGkZgUveOZb+P+6oTUj7dN+o= -github.com/metacubex/quic-go v0.55.1-0.20251024060151-bd465f127128/go.mod h1:1lktQFtCD17FZliVypbrDHwbsFSsmz2xz2TRXydvB5c= +github.com/metacubex/quic-go v0.55.1-0.20251203073212-6940cac967c2 h1:21KrRBqF5en0yXwwb5Vpptbeiiu3p7gD0G+RqNYvsvw= +github.com/metacubex/quic-go v0.55.1-0.20251203073212-6940cac967c2/go.mod h1:1lktQFtCD17FZliVypbrDHwbsFSsmz2xz2TRXydvB5c= github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs= github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY= github.com/metacubex/restls-client-go v0.1.7 h1:eCwiXCTQb5WJu9IlgYvDBA1OgrINv58dEe7hcN5H15k= @@ -171,8 +171,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= -github.com/saba-futai/sudoku v0.0.1-g h1:4q6OuAA6COaRW+CgoQtdim5AUPzzm0uOkvbYpJnOaBE= -github.com/saba-futai/sudoku v0.0.1-g/go.mod h1:2ZRzRwz93cS2K/o2yOG4CPJEltcvk5y6vbvUmjftGU0= +github.com/saba-futai/sudoku v0.0.1-i h1:t6H875LSceXaEEwho84GU9OoLa4ieoBo3v+dxpFf4wc= +github.com/saba-futai/sudoku v0.0.1-i/go.mod h1:FNtEAA44TSMvHI94o1kri/itbjvSMm1qCrbd0e6MTZY= github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ= github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI= github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis= diff --git a/clash-meta/hub/route/groups.go b/clash-meta/hub/route/groups.go index 873a94df51..05e8fe493e 100644 --- a/clash-meta/hub/route/groups.go +++ b/clash-meta/hub/route/groups.go @@ -31,7 +31,7 @@ func groupRouter() http.Handler { func getGroups(w http.ResponseWriter, r *http.Request) { var gs []C.Proxy for _, p := range tunnel.Proxies() { - if _, ok := p.Adapter().(C.Group); ok { + if _, ok := p.Adapter().(outboundgroup.ProxyGroup); ok { gs = append(gs, p) } } @@ -42,7 +42,7 @@ func getGroups(w http.ResponseWriter, r *http.Request) { func getGroup(w http.ResponseWriter, r *http.Request) { proxy := r.Context().Value(CtxKeyProxy).(C.Proxy) - if _, ok := proxy.Adapter().(C.Group); ok { + if _, ok := proxy.Adapter().(outboundgroup.ProxyGroup); ok { render.JSON(w, r, proxy) return } @@ -52,7 +52,7 @@ func getGroup(w http.ResponseWriter, r *http.Request) { func getGroupDelay(w http.ResponseWriter, r *http.Request) { proxy := r.Context().Value(CtxKeyProxy).(C.Proxy) - group, ok := proxy.Adapter().(C.Group) + group, ok := proxy.Adapter().(outboundgroup.ProxyGroup) if !ok { render.Status(r, http.StatusNotFound) render.JSON(w, r, ErrNotFound) diff --git a/clash-meta/hub/route/server.go b/clash-meta/hub/route/server.go index f2a52d4208..fe8275304e 100644 --- a/clash-meta/hub/route/server.go +++ b/clash-meta/hub/route/server.go @@ -47,8 +47,10 @@ func SetEmbedMode(embed bool) { } type Traffic struct { - Up int64 `json:"up"` - Down int64 `json:"down"` + Up int64 `json:"up"` + Down int64 `json:"down"` + UpTotal int64 `json:"upTotal"` + DownTotal int64 `json:"downTotal"` } type Memory struct { @@ -380,9 +382,12 @@ func traffic(w http.ResponseWriter, r *http.Request) { for range tick.C { buf.Reset() up, down := t.Now() + upTotal, downTotal := t.Total() if err := json.NewEncoder(buf).Encode(Traffic{ - Up: up, - Down: down, + Up: up, + Down: down, + UpTotal: upTotal, + DownTotal: downTotal, }); err != nil { break } diff --git a/clash-meta/listener/sudoku/server.go b/clash-meta/listener/sudoku/server.go index ce2b09afab..f3260eb341 100644 --- a/clash-meta/listener/sudoku/server.go +++ b/clash-meta/listener/sudoku/server.go @@ -1,6 +1,8 @@ package sudoku import ( + "errors" + "io" "net" "strings" @@ -10,7 +12,9 @@ import ( "github.com/metacubex/mihomo/adapter/inbound" C "github.com/metacubex/mihomo/constant" LC "github.com/metacubex/mihomo/listener/config" + "github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/transport/socks5" + "github.com/metacubex/mihomo/transport/sudoku" ) type Listener struct { @@ -43,19 +47,74 @@ func (l *Listener) Close() error { } func (l *Listener) handleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { - tunnelConn, target, err := apis.ServerHandshake(conn, &l.protoConf) + session, err := sudoku.ServerHandshake(conn, &l.protoConf) if err != nil { _ = conn.Close() return } - targetAddr := socks5.ParseAddr(target) - if targetAddr == nil { - _ = tunnelConn.Close() - return + switch session.Type { + case sudoku.SessionTypeUoT: + l.handleUoTSession(session.Conn, tunnel, additions...) + default: + targetAddr := socks5.ParseAddr(session.Target) + if targetAddr == nil { + _ = session.Conn.Close() + return + } + tunnel.HandleTCPConn(inbound.NewSocket(targetAddr, session.Conn, C.SUDOKU, additions...)) } +} - tunnel.HandleTCPConn(inbound.NewSocket(targetAddr, tunnelConn, C.SUDOKU, additions...)) +func (l *Listener) handleUoTSession(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { + writer := sudoku.NewUoTPacketConn(conn) + remoteAddr := conn.RemoteAddr() + + for { + addrStr, payload, err := sudoku.ReadDatagram(conn) + if err != nil { + if !errors.Is(err, io.EOF) { + log.Debugln("[Sudoku][UoT] session closed: %v", err) + } + _ = conn.Close() + return + } + + target := socks5.ParseAddr(addrStr) + if target == nil { + log.Debugln("[Sudoku][UoT] drop invalid target: %s", addrStr) + continue + } + + packet := &uotPacket{ + payload: payload, + writer: writer, + rAddr: remoteAddr, + } + tunnel.HandleUDPPacket(inbound.NewPacket(target, packet, C.SUDOKU, additions...)) + } +} + +type uotPacket struct { + payload []byte + writer *sudoku.UoTPacketConn + rAddr net.Addr +} + +func (p *uotPacket) Data() []byte { + return p.payload +} + +func (p *uotPacket) WriteBack(b []byte, addr net.Addr) (int, error) { + return p.writer.WriteTo(b, addr) +} + +func (p *uotPacket) Drop() { + p.payload = nil +} + +func (p *uotPacket) LocalAddr() net.Addr { + return p.rAddr } func New(config LC.SudokuServer, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) { diff --git a/clash-meta/transport/sudoku/address.go b/clash-meta/transport/sudoku/address.go new file mode 100644 index 0000000000..48b322967f --- /dev/null +++ b/clash-meta/transport/sudoku/address.go @@ -0,0 +1,91 @@ +package sudoku + +import ( + "encoding/binary" + "fmt" + "io" + "net" + "strconv" +) + +func EncodeAddress(rawAddr string) ([]byte, error) { + host, portStr, err := net.SplitHostPort(rawAddr) + if err != nil { + return nil, err + } + + portInt, err := strconv.ParseUint(portStr, 10, 16) + if err != nil { + return nil, err + } + + var buf []byte + if ip := net.ParseIP(host); ip != nil { + if ip4 := ip.To4(); ip4 != nil { + buf = append(buf, 0x01) // IPv4 + buf = append(buf, ip4...) + } else { + buf = append(buf, 0x04) // IPv6 + buf = append(buf, ip...) + } + } else { + if len(host) > 255 { + return nil, fmt.Errorf("domain too long") + } + buf = append(buf, 0x03) // domain + buf = append(buf, byte(len(host))) + buf = append(buf, host...) + } + + var portBytes [2]byte + binary.BigEndian.PutUint16(portBytes[:], uint16(portInt)) + buf = append(buf, portBytes[:]...) + return buf, nil +} + +func DecodeAddress(r io.Reader) (string, error) { + var atyp [1]byte + if _, err := io.ReadFull(r, atyp[:]); err != nil { + return "", err + } + + switch atyp[0] { + case 0x01: // IPv4 + var ipBuf [net.IPv4len]byte + if _, err := io.ReadFull(r, ipBuf[:]); err != nil { + return "", err + } + var portBuf [2]byte + if _, err := io.ReadFull(r, portBuf[:]); err != nil { + return "", err + } + return net.JoinHostPort(net.IP(ipBuf[:]).String(), fmt.Sprint(binary.BigEndian.Uint16(portBuf[:]))), nil + case 0x04: // IPv6 + var ipBuf [net.IPv6len]byte + if _, err := io.ReadFull(r, ipBuf[:]); err != nil { + return "", err + } + var portBuf [2]byte + if _, err := io.ReadFull(r, portBuf[:]); err != nil { + return "", err + } + return net.JoinHostPort(net.IP(ipBuf[:]).String(), fmt.Sprint(binary.BigEndian.Uint16(portBuf[:]))), nil + case 0x03: // domain + var lengthBuf [1]byte + if _, err := io.ReadFull(r, lengthBuf[:]); err != nil { + return "", err + } + l := int(lengthBuf[0]) + hostBuf := make([]byte, l) + if _, err := io.ReadFull(r, hostBuf); err != nil { + return "", err + } + var portBuf [2]byte + if _, err := io.ReadFull(r, portBuf[:]); err != nil { + return "", err + } + return net.JoinHostPort(string(hostBuf), fmt.Sprint(binary.BigEndian.Uint16(portBuf[:]))), nil + default: + return "", fmt.Errorf("unknown address type: %d", atyp[0]) + } +} diff --git a/clash-meta/transport/sudoku/handshake.go b/clash-meta/transport/sudoku/handshake.go new file mode 100644 index 0000000000..1f822707ce --- /dev/null +++ b/clash-meta/transport/sudoku/handshake.go @@ -0,0 +1,147 @@ +package sudoku + +import ( + "bufio" + "encoding/binary" + "fmt" + "io" + "net" + "time" + + "github.com/saba-futai/sudoku/apis" + "github.com/saba-futai/sudoku/pkg/crypto" + "github.com/saba-futai/sudoku/pkg/obfs/httpmask" + "github.com/saba-futai/sudoku/pkg/obfs/sudoku" + + "github.com/metacubex/mihomo/log" +) + +type SessionType int + +const ( + SessionTypeTCP SessionType = iota + SessionTypeUoT +) + +type ServerSession struct { + Conn net.Conn + Type SessionType + Target string +} + +type bufferedConn struct { + net.Conn + r *bufio.Reader +} + +func (bc *bufferedConn) Read(p []byte) (int, error) { + return bc.r.Read(p) +} + +type preBufferedConn struct { + net.Conn + buf []byte +} + +func (p *preBufferedConn) Read(b []byte) (int, error) { + if len(p.buf) > 0 { + n := copy(b, p.buf) + p.buf = p.buf[n:] + return n, nil + } + if p.Conn == nil { + return 0, io.EOF + } + return p.Conn.Read(b) +} + +func absInt64(v int64) int64 { + if v < 0 { + return -v + } + return v +} + +// ServerHandshake performs Sudoku server-side handshake and detects UoT preface. +func ServerHandshake(rawConn net.Conn, cfg *apis.ProtocolConfig) (*ServerSession, error) { + if cfg == nil { + return nil, fmt.Errorf("config is required") + } + if err := cfg.Validate(); err != nil { + return nil, fmt.Errorf("invalid config: %w", err) + } + + handshakeTimeout := time.Duration(cfg.HandshakeTimeoutSeconds) * time.Second + if handshakeTimeout <= 0 { + handshakeTimeout = 5 * time.Second + } + + bufReader := bufio.NewReader(rawConn) + if !cfg.DisableHTTPMask { + if peek, _ := bufReader.Peek(4); len(peek) == 4 && string(peek) == "POST" { + if _, err := httpmask.ConsumeHeader(bufReader); err != nil { + return nil, fmt.Errorf("invalid http header: %w", err) + } + } + } + + rawConn.SetReadDeadline(time.Now().Add(handshakeTimeout)) + bConn := &bufferedConn{ + Conn: rawConn, + r: bufReader, + } + sConn := sudoku.NewConn(bConn, cfg.Table, cfg.PaddingMin, cfg.PaddingMax, true) + cConn, err := crypto.NewAEADConn(sConn, cfg.Key, cfg.AEADMethod) + if err != nil { + return nil, fmt.Errorf("crypto setup failed: %w", err) + } + + var handshakeBuf [16]byte + if _, err := io.ReadFull(cConn, handshakeBuf[:]); err != nil { + cConn.Close() + return nil, fmt.Errorf("read handshake failed: %w", err) + } + + ts := int64(binary.BigEndian.Uint64(handshakeBuf[:8])) + if absInt64(time.Now().Unix()-ts) > 60 { + cConn.Close() + return nil, fmt.Errorf("timestamp skew detected") + } + + sConn.StopRecording() + + firstByte := make([]byte, 1) + if _, err := io.ReadFull(cConn, firstByte); err != nil { + cConn.Close() + return nil, fmt.Errorf("read first byte failed: %w", err) + } + + if firstByte[0] == UoTMagicByte { + version := make([]byte, 1) + if _, err := io.ReadFull(cConn, version); err != nil { + cConn.Close() + return nil, fmt.Errorf("read uot version failed: %w", err) + } + if version[0] != uotVersion { + cConn.Close() + return nil, fmt.Errorf("unsupported uot version: %d", version[0]) + } + rawConn.SetReadDeadline(time.Time{}) + return &ServerSession{Conn: cConn, Type: SessionTypeUoT}, nil + } + + prefixed := &preBufferedConn{Conn: cConn, buf: firstByte} + target, err := DecodeAddress(prefixed) + if err != nil { + cConn.Close() + return nil, fmt.Errorf("read target address failed: %w", err) + } + + rawConn.SetReadDeadline(time.Time{}) + log.Debugln("[Sudoku] incoming TCP session target: %s", target) + return &ServerSession{ + Conn: prefixed, + Type: SessionTypeTCP, + Target: target, + }, nil +} diff --git a/clash-meta/transport/sudoku/uot.go b/clash-meta/transport/sudoku/uot.go new file mode 100644 index 0000000000..be3fe90033 --- /dev/null +++ b/clash-meta/transport/sudoku/uot.go @@ -0,0 +1,158 @@ +package sudoku + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "io" + "net" + "net/netip" + "strconv" + "sync" + "time" + + "github.com/metacubex/mihomo/log" +) + +const ( + UoTMagicByte byte = 0xEE + uotVersion = 0x01 + maxUoTPayload = 64 * 1024 +) + +// WritePreface writes the UDP-over-TCP marker and version. +func WritePreface(w io.Writer) error { + _, err := w.Write([]byte{UoTMagicByte, uotVersion}) + return err +} + +// WriteDatagram sends a single UDP datagram frame over a reliable stream. +func WriteDatagram(w io.Writer, addr string, payload []byte) error { + addrBuf, err := EncodeAddress(addr) + if err != nil { + return fmt.Errorf("encode address: %w", err) + } + + if addrLen := len(addrBuf); addrLen == 0 || addrLen > maxUoTPayload { + return fmt.Errorf("address too long: %d", len(addrBuf)) + } + if payloadLen := len(payload); payloadLen > maxUoTPayload { + return fmt.Errorf("payload too large: %d", payloadLen) + } + + var header [4]byte + binary.BigEndian.PutUint16(header[:2], uint16(len(addrBuf))) + binary.BigEndian.PutUint16(header[2:], uint16(len(payload))) + + if _, err := w.Write(header[:]); err != nil { + return err + } + if _, err := w.Write(addrBuf); err != nil { + return err + } + _, err = w.Write(payload) + return err +} + +// ReadDatagram parses a single UDP datagram frame from the reliable stream. +func ReadDatagram(r io.Reader) (string, []byte, error) { + var header [4]byte + if _, err := io.ReadFull(r, header[:]); err != nil { + return "", nil, err + } + + addrLen := int(binary.BigEndian.Uint16(header[:2])) + payloadLen := int(binary.BigEndian.Uint16(header[2:])) + + if addrLen <= 0 || addrLen > maxUoTPayload { + return "", nil, fmt.Errorf("invalid address length: %d", addrLen) + } + if payloadLen < 0 || payloadLen > maxUoTPayload { + return "", nil, fmt.Errorf("invalid payload length: %d", payloadLen) + } + + addrBuf := make([]byte, addrLen) + if _, err := io.ReadFull(r, addrBuf); err != nil { + return "", nil, err + } + + addr, err := DecodeAddress(bytes.NewReader(addrBuf)) + if err != nil { + return "", nil, fmt.Errorf("decode address: %w", err) + } + + payload := make([]byte, payloadLen) + if _, err := io.ReadFull(r, payload); err != nil { + return "", nil, err + } + + return addr, payload, nil +} + +// UoTPacketConn adapts a net.Conn with the Sudoku UoT framing to net.PacketConn. +type UoTPacketConn struct { + conn net.Conn + writeMu sync.Mutex +} + +func NewUoTPacketConn(conn net.Conn) *UoTPacketConn { + return &UoTPacketConn{conn: conn} +} + +func (c *UoTPacketConn) ReadFrom(p []byte) (int, net.Addr, error) { + for { + addrStr, payload, err := ReadDatagram(c.conn) + if err != nil { + return 0, nil, err + } + + if len(payload) > len(p) { + return 0, nil, io.ErrShortBuffer + } + + host, port, _ := net.SplitHostPort(addrStr) + portInt, _ := strconv.ParseUint(port, 10, 16) + ip, err := netip.ParseAddr(host) + if err != nil { // disallow domain addr at here, just ignore + log.Debugln("[Sudoku][UoT] discard datagram with invalid address %s: %v", addrStr, err) + continue + } + udpAddr := net.UDPAddrFromAddrPort(netip.AddrPortFrom(ip.Unmap(), uint16(portInt))) + + copy(p, payload) + return len(payload), udpAddr, nil + } +} + +func (c *UoTPacketConn) WriteTo(p []byte, addr net.Addr) (int, error) { + if addr == nil { + return 0, errors.New("address is nil") + } + c.writeMu.Lock() + defer c.writeMu.Unlock() + if err := WriteDatagram(c.conn, addr.String(), p); err != nil { + return 0, err + } + return len(p), nil +} + +func (c *UoTPacketConn) Close() error { + return c.conn.Close() +} + +func (c *UoTPacketConn) LocalAddr() net.Addr { + return c.conn.LocalAddr() +} + +func (c *UoTPacketConn) SetDeadline(t time.Time) error { + return c.conn.SetDeadline(t) +} + +func (c *UoTPacketConn) SetReadDeadline(t time.Time) error { + return c.conn.SetReadDeadline(t) +} + +func (c *UoTPacketConn) SetWriteDeadline(t time.Time) error { + return c.conn.SetWriteDeadline(t) +} diff --git a/clash-meta/tunnel/statistic/manager.go b/clash-meta/tunnel/statistic/manager.go index 9db4601e14..c69746dbcb 100644 --- a/clash-meta/tunnel/statistic/manager.go +++ b/clash-meta/tunnel/statistic/manager.go @@ -72,6 +72,10 @@ func (m *Manager) Now() (up int64, down int64) { return m.uploadBlip.Load(), m.downloadBlip.Load() } +func (m *Manager) Total() (up, down int64) { + return m.uploadTotal.Load(), m.downloadTotal.Load() +} + func (m *Manager) Memory() uint64 { m.updateMemory() return m.memory diff --git a/clash-meta/tunnel/statistic/patch_android.go b/clash-meta/tunnel/statistic/patch_android.go deleted file mode 100644 index f1eee346eb..0000000000 --- a/clash-meta/tunnel/statistic/patch_android.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build android && cmfa - -package statistic - -func (m *Manager) Total() (up, down int64) { - return m.uploadTotal.Load(), m.downloadTotal.Load() -} diff --git a/clash-nyanpasu/manifest/version.json b/clash-nyanpasu/manifest/version.json index 16462edb2f..6a5b776520 100644 --- a/clash-nyanpasu/manifest/version.json +++ b/clash-nyanpasu/manifest/version.json @@ -2,7 +2,7 @@ "manifest_version": 1, "latest": { "mihomo": "v1.19.17", - "mihomo_alpha": "alpha-9a5e506", + "mihomo_alpha": "alpha-fdb7cb1", "clash_rs": "v0.9.2", "clash_premium": "2023-09-05-gdcc8d87", "clash_rs_alpha": "0.9.2-alpha+sha.e1f8fbb" @@ -69,5 +69,5 @@ "linux-armv7hf": "clash-armv7-unknown-linux-gnueabihf" } }, - "updated_at": "2025-12-01T22:21:34.119Z" + "updated_at": "2025-12-02T22:21:02.066Z" } diff --git a/clash-verge-rev/.github/workflows/alpha.yml b/clash-verge-rev/.github/workflows/alpha.yml index a7221a3673..cec7440ae7 100644 --- a/clash-verge-rev/.github/workflows/alpha.yml +++ b/clash-verge-rev/.github/workflows/alpha.yml @@ -35,7 +35,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Check tag and package.json version id: check_tag @@ -64,7 +64,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Delete Old Alpha Tags Except Latest - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | @@ -180,7 +180,7 @@ jobs: needs: delete_old_assets steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Fetch UPDATE logs id: fetch_update_logs @@ -275,7 +275,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Checkout Repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install Rust Stable uses: dtolnay/rust-toolchain@stable @@ -305,9 +305,9 @@ jobs: echo "PKG_CONFIG_PATH=$(brew --prefix openssl@3)/lib/pkgconfig" >> $GITHUB_ENV - name: Install Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: - node-version: "22" + node-version: "24.11.1" - uses: pnpm/action-setup@v4 name: Install pnpm @@ -360,7 +360,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Checkout Repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install Rust Stable uses: dtolnay/rust-toolchain@stable @@ -375,9 +375,9 @@ jobs: save-if: false - name: Install Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: - node-version: "22" + node-version: "24.11.1" - name: Install pnpm uses: pnpm/action-setup@v4 @@ -491,7 +491,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Checkout Repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Add Rust Target run: rustup target add ${{ matrix.target }} @@ -503,9 +503,9 @@ jobs: save-if: false - name: Install Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: - node-version: "22" + node-version: "24.11.1" - uses: pnpm/action-setup@v4 name: Install pnpm diff --git a/clash-verge-rev/.github/workflows/autobuild-check-test.yml b/clash-verge-rev/.github/workflows/autobuild-check-test.yml index ea827db229..4eb6da0a2c 100644 --- a/clash-verge-rev/.github/workflows/autobuild-check-test.yml +++ b/clash-verge-rev/.github/workflows/autobuild-check-test.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 2 diff --git a/clash-verge-rev/.github/workflows/autobuild.yml b/clash-verge-rev/.github/workflows/autobuild.yml index 0f978e8a3d..fd43e28b1b 100644 --- a/clash-verge-rev/.github/workflows/autobuild.yml +++ b/clash-verge-rev/.github/workflows/autobuild.yml @@ -31,22 +31,22 @@ jobs: if: ${{ needs.check_commit.outputs.should_run == 'true' }} steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Fetch UPDATE logs id: fetch_update_logs run: bash ./scripts/extract_update_logs.sh shell: bash - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v4.2.0 name: Install pnpm with: run_install: false - name: Install Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: - node-version: "22" + node-version: "24.11.1" - name: Install dependencies run: pnpm install --frozen-lockfile @@ -77,20 +77,20 @@ jobs: ### Windows (不再支持Win7) #### 正常版本(推荐) - - [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup_windows.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup_windows.exe) + - [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup.exe) #### 内置Webview2版(体积较大,仅在企业版系统或无法安装webview2时使用) - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_fixed_webview2-setup.exe) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64_fixed_webview2-setup.exe) ### macOS - - [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64_darwin.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_darwin.dmg) + - [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64.dmg) ### Linux #### DEB包(Debian系) 使用 apt ./路径 安装 - - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64_linux.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb) + - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb) #### RPM包(Redhat系) 使用 dnf ./路径 安装 - - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.x86_64_linux.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.armhfp.rpm) + - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.x86_64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.armhfp.rpm) ### FAQ - [常见问题](https://clash-verge-rev.github.io/faq/windows.html) @@ -109,7 +109,7 @@ jobs: body_path: release.txt prerelease: true token: ${{ secrets.GITHUB_TOKEN }} - generate_release_notes: true + generate_release_notes: false clean_old_assets: name: Clean Old Release Assets @@ -142,7 +142,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Checkout Repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install Rust Stable uses: dtolnay/rust-toolchain@master @@ -156,11 +156,13 @@ jobs: - name: Rust Cache uses: Swatinem/rust-cache@v2 with: - workspaces: src-tauri - cache-all-crates: true save-if: ${{ github.ref == 'refs/heads/dev' }} - shared-key: autobuild-${{ runner.os }}-${{ matrix.target }} - key: ${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('src-tauri/Cargo.lock') }} + prefix-key: "v1-rust" + key: "rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}" + workspaces: | + . -> target + cache-all-crates: true + cache-workspace-crates: true - name: Install dependencies (ubuntu only) if: matrix.os == 'ubuntu-22.04' @@ -177,24 +179,24 @@ jobs: echo "OPENSSL_LIB_DIR=$(brew --prefix openssl@3)/lib" >> $GITHUB_ENV echo "PKG_CONFIG_PATH=$(brew --prefix openssl@3)/lib/pkgconfig" >> $GITHUB_ENV - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v4.2.0 name: Install pnpm with: run_install: false - name: Install Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: - node-version: "22" + node-version: "24.11.1" cache: "pnpm" - - name: Cache pnpm store + - name: Pnpm Cache uses: actions/cache@v4 with: path: ~/.pnpm-store - key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }} + key: "pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}" restore-keys: | - ${{ runner.os }}-pnpm- + pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }} - name: Pnpm install and check run: | @@ -218,12 +220,12 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} - APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} - APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} - APPLE_ID: ${{ secrets.APPLE_ID }} - APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + # APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + # APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + # APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} + # APPLE_ID: ${{ secrets.APPLE_ID }} + # APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} + # APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} with: tagName: ${{ env.TAG_NAME }} releaseName: "Clash Verge Rev ${{ env.TAG_CHANNEL }}" @@ -251,7 +253,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Checkout Repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install Rust Stable uses: dtolnay/rust-toolchain@master @@ -265,30 +267,32 @@ jobs: - name: Rust Cache uses: Swatinem/rust-cache@v2 with: - workspaces: src-tauri - cache-all-crates: true save-if: ${{ github.ref == 'refs/heads/dev' }} - shared-key: autobuild-${{ runner.os }}-${{ matrix.target }} - key: ${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('src-tauri/Cargo.lock') }} + prefix-key: "v1-rust" + key: "rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}" + workspaces: | + . -> target + cache-all-crates: true + cache-workspace-crates: true - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v4.2.0 with: run_install: false - name: Install Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: - node-version: "22" + node-version: "24.11.1" cache: "pnpm" - - name: Cache pnpm store + - name: Pnpm Cache uses: actions/cache@v4 with: path: ~/.pnpm-store - key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }} + key: "pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}" restore-keys: | - ${{ runner.os }}-pnpm- + pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }} - name: Pnpm install and check run: | @@ -385,8 +389,8 @@ jobs: prerelease: true token: ${{ secrets.GITHUB_TOKEN }} files: | - src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb - src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm + target/${{ matrix.target }}/release/bundle/deb/*.deb + target/${{ matrix.target }}/release/bundle/rpm/*.rpm autobuild-x86-arm-windows_webview2: name: Autobuild x86 and ARM Windows with WebView2 @@ -405,7 +409,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Checkout Repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Add Rust Target run: rustup target add ${{ matrix.target }} @@ -413,30 +417,32 @@ jobs: - name: Rust Cache uses: Swatinem/rust-cache@v2 with: - workspaces: src-tauri - cache-all-crates: true save-if: ${{ github.ref == 'refs/heads/dev' }} - shared-key: autobuild-${{ runner.os }}-${{ matrix.target }} - key: ${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('src-tauri/Cargo.lock') }} + prefix-key: "v1-rust" + key: "rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}" + workspaces: | + . -> target + cache-all-crates: true + cache-workspace-crates: true - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v4.2.0 with: run_install: false - name: Install Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: - node-version: "22" + node-version: "24.11.1" cache: "pnpm" - - name: Cache pnpm store + - name: Pnpm Cache uses: actions/cache@v4 with: path: ~/.pnpm-store - key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }} + key: "pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}" restore-keys: | - ${{ runner.os }}-pnpm- + pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }} - name: Pnpm install and check run: | @@ -475,19 +481,19 @@ jobs: - name: Rename run: | - $files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe" + $files = Get-ChildItem ".\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe" foreach ($file in $files) { $newName = $file.Name -replace "-setup\.exe$", "_fixed_webview2-setup.exe" Rename-Item $file.FullName $newName } - $files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*.nsis.zip" + $files = Get-ChildItem ".\target\${{ matrix.target }}\release\bundle\nsis\*.nsis.zip" foreach ($file in $files) { $newName = $file.Name -replace "-setup\.nsis\.zip$", "_fixed_webview2-setup.nsis.zip" Rename-Item $file.FullName $newName } - $files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe.sig" + $files = Get-ChildItem ".\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe.sig" foreach ($file in $files) { $newName = $file.Name -replace "-setup\.exe\.sig$", "_fixed_webview2-setup.exe.sig" Rename-Item $file.FullName $newName @@ -500,7 +506,7 @@ jobs: name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}" prerelease: true token: ${{ secrets.GITHUB_TOKEN }} - files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup* + files: target/${{ matrix.target }}/release/bundle/nsis/*setup* - name: Portable Bundle run: pnpm portable-fixed-webview2 ${{ matrix.target }} --${{ env.TAG_NAME }} @@ -519,7 +525,7 @@ jobs: ] steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Fetch UPDATE logs id: fetch_update_logs @@ -527,11 +533,11 @@ jobs: shell: bash - name: Install Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: - node-version: "22" + node-version: "24.11.1" - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v4.2.0 name: Install pnpm with: run_install: false @@ -566,20 +572,20 @@ jobs: ### Windows (不再支持Win7) #### 正常版本(推荐) - - [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup_windows.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup_windows.exe) + - [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup.exe) #### 内置Webview2版(体积较大,仅在企业版系统或无法安装webview2时使用) - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_fixed_webview2-setup.exe) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64_fixed_webview2-setup.exe) ### macOS - - [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64_darwin.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_darwin.dmg) + - [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64.dmg) ### Linux #### DEB包(Debian系) 使用 apt ./路径 安装 - - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64_linux.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb) + - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb) #### RPM包(Redhat系) 使用 dnf ./路径 安装 - - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.x86_64_linux.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.armhfp.rpm) + - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.x86_64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.armhfp.rpm) ### FAQ - [常见问题](https://clash-verge-rev.github.io/faq/windows.html) diff --git a/clash-verge-rev/.github/workflows/check-commit-needs-build.yml b/clash-verge-rev/.github/workflows/check-commit-needs-build.yml index e625319b61..f78c0aab2c 100644 --- a/clash-verge-rev/.github/workflows/check-commit-needs-build.yml +++ b/clash-verge-rev/.github/workflows/check-commit-needs-build.yml @@ -53,7 +53,7 @@ jobs: autobuild_version: ${{ steps.check.outputs.autobuild_version }} steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 50 diff --git a/clash-verge-rev/.github/workflows/clean-old-assets.yml b/clash-verge-rev/.github/workflows/clean-old-assets.yml index 4c9ad9f029..cb21eb213c 100644 --- a/clash-verge-rev/.github/workflows/clean-old-assets.yml +++ b/clash-verge-rev/.github/workflows/clean-old-assets.yml @@ -42,7 +42,7 @@ jobs: autobuild_version: ${{ steps.check.outputs.autobuild_version }} steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 50 @@ -56,7 +56,7 @@ jobs: echo "🔍 Finding last commit with Tauri-related changes..." # Define patterns for Tauri-related files - TAURI_PATTERNS="src/ src-tauri/src src-tauri/Cargo.toml src-tauri/Cargo.lock src-tauri/tauri.*.conf.json src-tauri/build.rs src-tauri/capabilities" + TAURI_PATTERNS="src/ src-tauri/src src-tauri/Cargo.toml Cargo.lock src-tauri/tauri.*.conf.json src-tauri/build.rs src-tauri/capabilities" # Get the last commit that changed any of these patterns (excluding build artifacts) LAST_TAURI_COMMIT="" @@ -105,7 +105,7 @@ jobs: needs: check_current_version steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Clean old assets from release env: diff --git a/clash-verge-rev/.github/workflows/cross_check.yaml b/clash-verge-rev/.github/workflows/cross_check.yaml index 03a65bf147..06eeb91551 100644 --- a/clash-verge-rev/.github/workflows/cross_check.yaml +++ b/clash-verge-rev/.github/workflows/cross_check.yaml @@ -30,7 +30,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Checkout Repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install Rust Stable uses: dtolnay/rust-toolchain@stable @@ -41,9 +41,9 @@ jobs: run: rustup target add ${{ matrix.target }} - name: Install Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: - node-version: "20" + node-version: "24.11.1" - uses: pnpm/action-setup@v4 name: Install pnpm diff --git a/clash-verge-rev/.github/workflows/dev.yml b/clash-verge-rev/.github/workflows/dev.yml index e5ae26c327..74c7dd01f6 100644 --- a/clash-verge-rev/.github/workflows/dev.yml +++ b/clash-verge-rev/.github/workflows/dev.yml @@ -70,20 +70,22 @@ jobs: - name: Checkout Repository if: github.event.inputs[matrix.input] == 'true' - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install Rust Stable if: github.event.inputs[matrix.input] == 'true' uses: dtolnay/rust-toolchain@1.91.0 - name: Rust Cache - if: github.event.inputs[matrix.input] == 'true' uses: Swatinem/rust-cache@v2 with: - workspaces: src-tauri - save-if: false + save-if: ${{ github.ref == 'refs/heads/dev' }} + prefix-key: "v1-rust" + key: "rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}" + workspaces: | + . -> target cache-all-crates: true - shared-key: autobuild-shared + cache-workspace-crates: true - name: Install dependencies (ubuntu only) if: matrix.os == 'ubuntu-22.04' && github.event.inputs[matrix.input] == 'true' @@ -99,11 +101,20 @@ jobs: - name: Install Node if: github.event.inputs[matrix.input] == 'true' - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: - node-version: "20" + node-version: "24.11.1" cache: "pnpm" + - name: Pnpm Cache + uses: actions/cache@v4 + with: + path: ~/.pnpm-store + key: "pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}" + restore-keys: | + pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }} + lookup-only: true + - name: Pnpm install and check if: github.event.inputs[matrix.input] == 'true' run: | @@ -130,36 +141,36 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} - APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} - APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} - APPLE_ID: ${{ secrets.APPLE_ID }} - APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + # APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + # APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + # APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} + # APPLE_ID: ${{ secrets.APPLE_ID }} + # APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} + # APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} with: tauriScript: pnpm args: --target ${{ matrix.target }} -b ${{ matrix.bundle }} - name: Upload Artifacts (macOS) if: matrix.os == 'macos-latest' && github.event.inputs[matrix.input] == 'true' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: ${{ matrix.target }} - path: src-tauri/target/${{ matrix.target }}/release/bundle/dmg/*.dmg + path: target/${{ matrix.target }}/release/bundle/dmg/*.dmg if-no-files-found: error - name: Upload Artifacts (Windows) if: matrix.os == 'windows-latest' && github.event.inputs[matrix.input] == 'true' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: ${{ matrix.target }} - path: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*.exe + path: target/${{ matrix.target }}/release/bundle/nsis/*.exe if-no-files-found: error - name: Upload Artifacts (Linux) if: matrix.os == 'ubuntu-22.04' && github.event.inputs[matrix.input] == 'true' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: ${{ matrix.target }} - path: src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb + path: target/${{ matrix.target }}/release/bundle/deb/*.deb if-no-files-found: error diff --git a/clash-verge-rev/.github/workflows/frontend-check.yml b/clash-verge-rev/.github/workflows/frontend-check.yml new file mode 100644 index 0000000000..4bb8455bdf --- /dev/null +++ b/clash-verge-rev/.github/workflows/frontend-check.yml @@ -0,0 +1,75 @@ +name: Frontend Check + +on: + pull_request: + workflow_dispatch: + +env: + HUSKY: 0 + +jobs: + frontend: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Check frontend changes + id: check_frontend + uses: dorny/paths-filter@v3 + with: + filters: | + frontend: + - 'src/**' + - '**/*.js' + - '**/*.ts' + - '**/*.tsx' + - '**/*.css' + - '**/*.scss' + - '**/*.json' + - '**/*.md' + - 'package.json' + - 'pnpm-lock.yaml' + - 'pnpm-workspace.yaml' + - 'eslint.config.ts' + - 'tsconfig.json' + - 'vite.config.*' + + - name: Skip if no frontend changes + if: steps.check_frontend.outputs.frontend != 'true' + run: echo "No frontend changes, skipping frontend checks." + + - name: Install pnpm + if: steps.check_frontend.outputs.frontend == 'true' + uses: pnpm/action-setup@v4 + with: + run_install: false + + - uses: actions/setup-node@v6 + if: steps.check_frontend.outputs.frontend == 'true' + with: + node-version: "24.11.1" + cache: "pnpm" + + - name: Restore pnpm cache + if: steps.check_frontend.outputs.frontend == 'true' + uses: actions/cache@v4 + with: + path: ~/.pnpm-store + key: "pnpm-shared-stable-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}" + restore-keys: | + pnpm-shared-stable-${{ runner.os }}- + + - run: pnpm install --frozen-lockfile + if: steps.check_frontend.outputs.frontend == 'true' + + - name: Run Prettier check + if: steps.check_frontend.outputs.frontend == 'true' + run: pnpm format:check + + - name: Run ESLint + if: steps.check_frontend.outputs.frontend == 'true' + run: pnpm lint + + - name: Run TypeScript typecheck + if: steps.check_frontend.outputs.frontend == 'true' + run: pnpm typecheck diff --git a/clash-verge-rev/.github/workflows/lint-clippy.yml b/clash-verge-rev/.github/workflows/lint-clippy.yml index d7f3416ba7..ee74f9b0dd 100644 --- a/clash-verge-rev/.github/workflows/lint-clippy.yml +++ b/clash-verge-rev/.github/workflows/lint-clippy.yml @@ -44,7 +44,7 @@ jobs: echo "Manual trigger detected: skipping changes check and running clippy." - name: Checkout Repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install Rust Stable uses: dtolnay/rust-toolchain@master @@ -58,11 +58,13 @@ jobs: - name: Rust Cache uses: Swatinem/rust-cache@v2 with: - workspaces: src-tauri + save-if: ${{ github.ref == 'refs/heads/dev' }} + prefix-key: "v1-rust" + key: "rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}" + workspaces: | + . -> target cache-all-crates: true - save-if: false - shared-key: autobuild-${{ runner.os }}-${{ matrix.target }} - key: ${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('src-tauri/Cargo.lock') }} + cache-workspace-crates: true - name: Install dependencies (ubuntu only) if: matrix.os == 'ubuntu-22.04' diff --git a/clash-verge-rev/.github/workflows/release.yml b/clash-verge-rev/.github/workflows/release.yml index 153a5f8c95..f3f2b271fc 100644 --- a/clash-verge-rev/.github/workflows/release.yml +++ b/clash-verge-rev/.github/workflows/release.yml @@ -24,7 +24,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 @@ -69,7 +69,7 @@ jobs: needs: [release, release-for-linux-arm, release-for-fixed-webview2] steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Fetch UPDATE logs id: fetch_update_logs @@ -156,7 +156,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Checkout Repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install Rust Stable uses: dtolnay/rust-toolchain@stable @@ -186,9 +186,9 @@ jobs: echo "PKG_CONFIG_PATH=$(brew --prefix openssl@3)/lib/pkgconfig" >> $GITHUB_ENV - name: Install Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: - node-version: "22" + node-version: "24.11.1" - uses: pnpm/action-setup@v4 name: Install pnpm @@ -202,7 +202,7 @@ jobs: - name: Tauri build # 上游 5.24 修改了 latest.json 的生成逻辑,且依赖 tauri-plugin-update 2.10.0 暂未发布,故锁定在 0.5.23 版本 - uses: tauri-apps/tauri-action@v0.5.23 + uses: tauri-apps/tauri-action@v0.6.0 env: NODE_OPTIONS: "--max_old_space_size=4096" GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -240,7 +240,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Checkout Repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install Rust Stable uses: dtolnay/rust-toolchain@stable @@ -255,9 +255,9 @@ jobs: save-if: false - name: Install Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: - node-version: "22" + node-version: "24.11.1" - name: Install pnpm uses: pnpm/action-setup@v4 @@ -364,7 +364,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Checkout Repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Add Rust Target run: rustup target add ${{ matrix.target }} @@ -376,9 +376,9 @@ jobs: save-if: false - name: Install Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: - node-version: "22" + node-version: "24.11.1" - uses: pnpm/action-setup@v4 name: Install pnpm @@ -400,7 +400,7 @@ jobs: - name: Tauri build id: build # 上游 5.24 修改了 latest.json 的生成逻辑,且依赖 tauri-plugin-update 2.10.0 暂未发布,故锁定在 0.5.23 版本 - uses: tauri-apps/tauri-action@v0.5.23 + uses: tauri-apps/tauri-action@v0.6.0 env: NODE_OPTIONS: "--max_old_space_size=4096" GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -412,19 +412,19 @@ jobs: - name: Rename run: | - $files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe" + $files = Get-ChildItem ".\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe" foreach ($file in $files) { $newName = $file.Name -replace "-setup\.exe$", "_fixed_webview2-setup.exe" Rename-Item $file.FullName $newName } - $files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*.nsis.zip" + $files = Get-ChildItem ".\target\${{ matrix.target }}\release\bundle\nsis\*.nsis.zip" foreach ($file in $files) { $newName = $file.Name -replace "-setup\.nsis\.zip$", "_fixed_webview2-setup.nsis.zip" Rename-Item $file.FullName $newName } - $files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe.sig" + $files = Get-ChildItem ".\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe.sig" foreach ($file in $files) { $newName = $file.Name -replace "-setup\.exe\.sig$", "_fixed_webview2-setup.exe.sig" Rename-Item $file.FullName $newName @@ -452,12 +452,12 @@ jobs: needs: [update_tag] steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: - node-version: "22" + node-version: "24.11.1" - uses: pnpm/action-setup@v4 name: Install pnpm @@ -478,12 +478,12 @@ jobs: needs: [update_tag] steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: - node-version: "22" + node-version: "24.11.1" - uses: pnpm/action-setup@v4 name: Install pnpm @@ -505,7 +505,7 @@ jobs: needs: [update_tag, release-update] steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - name: Get Version @@ -535,7 +535,7 @@ jobs: ] steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Fetch UPDATE logs id: fetch_update_logs @@ -543,9 +543,9 @@ jobs: shell: bash - name: Install Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: - node-version: "22" + node-version: "24.11.1" - uses: pnpm/action-setup@v4 name: Install pnpm diff --git a/clash-verge-rev/.github/workflows/fmt.yml b/clash-verge-rev/.github/workflows/rustfmt.yml similarity index 56% rename from clash-verge-rev/.github/workflows/fmt.yml rename to clash-verge-rev/.github/workflows/rustfmt.yml index 58efc98b1c..c0f5c33c95 100644 --- a/clash-verge-rev/.github/workflows/fmt.yml +++ b/clash-verge-rev/.github/workflows/rustfmt.yml @@ -14,7 +14,7 @@ jobs: rustfmt: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Check Rust changes id: check_rust @@ -39,42 +39,6 @@ jobs: if: steps.check_rust.outputs.rust == 'true' run: cargo fmt --manifest-path ./src-tauri/Cargo.toml --all -- --check - prettier: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Check Web changes - id: check_web - uses: dorny/paths-filter@v3 - with: - filters: | - web: - - 'src/**' - - '**/*.js' - - '**/*.ts' - - '**/*.tsx' - - '**/*.css' - - '**/*.scss' - - '**/*.json' - - '**/*.md' - - '**/*.json' - - - name: Skip if no Web changes - if: steps.check_web.outputs.web != 'true' - run: echo "No web changes, skipping prettier." - - - uses: actions/setup-node@v4 - if: steps.check_web.outputs.web == 'true' - with: - node-version: "lts/*" - - run: corepack enable - if: steps.check_web.outputs.web == 'true' - - run: pnpm install --frozen-lockfile - if: steps.check_web.outputs.web == 'true' - - run: pnpm format:check - if: steps.check_web.outputs.web == 'true' - # taplo: # name: taplo (.toml files) # runs-on: ubuntu-latest diff --git a/clash-verge-rev/.github/workflows/updater.yml b/clash-verge-rev/.github/workflows/updater.yml index d482bc608b..1e182197cb 100644 --- a/clash-verge-rev/.github/workflows/updater.yml +++ b/clash-verge-rev/.github/workflows/updater.yml @@ -10,12 +10,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: - node-version: "22" + node-version: "24.11.1" - uses: pnpm/action-setup@v4 name: Install pnpm @@ -34,12 +34,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: - node-version: "22" + node-version: "24.11.1" - uses: pnpm/action-setup@v4 name: Install pnpm diff --git a/clash-verge-rev/.husky/pre-push b/clash-verge-rev/.husky/pre-push index 43ffed41fe..4f2a6a6b70 100644 --- a/clash-verge-rev/.husky/pre-push +++ b/clash-verge-rev/.husky/pre-push @@ -40,4 +40,3 @@ else fi echo "[pre-push] All checks passed." -exit 0 diff --git a/clash-verge-rev/CONTRIBUTING.md b/clash-verge-rev/CONTRIBUTING.md index e23cd352fd..833289ad41 100644 --- a/clash-verge-rev/CONTRIBUTING.md +++ b/clash-verge-rev/CONTRIBUTING.md @@ -1,151 +1,132 @@ # CONTRIBUTING -Thank you for your interest in contributing to Clash Verge Rev! This document provides guidelines and instructions to help you set up your development environment and start contributing. +Thank you for your interest in contributing to **Clash Verge Rev**! This guide provides instructions to help you set up your development environment and start contributing effectively. ## Internationalization (i18n) -We welcome translations and improvements to existing locales. Please follow the detailed guidelines in [CONTRIBUTING_i18n.md](docs/CONTRIBUTING_i18n.md) for instructions on extracting strings, file naming conventions, testing translations, and submitting translation PRs. +We welcome translations and improvements to existing locales. For details on contributing translations, please see [CONTRIBUTING_i18n.md](docs/CONTRIBUTING_i18n.md). ## Development Setup -Before you start contributing to the project, you need to set up your development environment. Here are the steps you need to follow: +Before contributing, you need to set up your development environment. Follow the steps below carefully. ### Prerequisites -1. **Install Rust and Node.js**: Our project requires both Rust and Node.js. Please follow the instructions provided [here](https://tauri.app/start/prerequisites/) to install them on your system. +1. **Install Rust and Node.js** + Our project requires both Rust and Node.js. Follow the official installation instructions [here](https://tauri.app/start/prerequisites/). -### Setup for Windows Users +### Windows Users -> [!NOTE] -> **If you are using a Windows ARM device, you additionally need to install [LLVM](https://github.com/llvm/llvm-project/releases) (including clang) and set the environment variable.** -> -> Because the `ring` crate is compiled based on `clang` under Windows ARM. +> [!NOTE] +> **Windows ARM users must also install [LLVM](https://github.com/llvm/llvm-project/releases) (including clang) and set the corresponding environment variables.** +> The `ring` crate depends on `clang` when building on Windows ARM. -If you're a Windows user, you may need to perform some additional steps: +Additional steps for Windows: -- Make sure to add Rust and Node.js to your system's PATH. This is usually done during the installation process, but you can verify and manually add them if necessary. -- The gnu `patch` tool should be installed +- Ensure Rust and Node.js are added to your system `PATH`. -When you setup `Rust` environment, Only use toolchain with `Windows MSVC` , to change settings follow command: +- Install the GNU `patch` tool. -```shell +- Use the MSVC toolchain for Rust: + +```bash rustup target add x86_64-pc-windows-msvc rustup set default-host x86_64-pc-windows-msvc ``` -### Install Node.js Package +### Install Node.js Package Manager -After installing Rust and Node.js, install the necessary Node.js and Node Package Manager: +Enable `corepack`: -```shell -npm install pnpm -g +```bash +corepack enable ``` -### Install Dependencies +### Install Project Dependencies -Install node packages +Node.js dependencies: -```shell +```bash pnpm install ``` -Install apt packages ONLY for Ubuntu +Ubuntu-only system packages: -```shell -apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf +```bash +sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf ``` -### Download the Mihomo Core Binary +### Download the Mihomo Core Binary (Automatic) -You have two options for downloading the clash binary: - -- Automatically download it via the provided script: - - ```shell - pnpm run prebuild - # Use '--force' or '-f' to update both the Mihomo core version - # and the Clash Verge Rev service version to the latest available. - pnpm run prebuild --force - ``` - -- Manually download it from the [Mihomo release](https://github.com/MetaCubeX/mihomo/releases). After downloading, rename the binary according to the [Tauri configuration](https://tauri.app/v1/api/config#bundleconfig.externalbin). +```bash +pnpm run prebuild +pnpm run prebuild --force # Re-download and overwrite Mihomo core and service binaries +``` ### Run the Development Server -To run the development server, use the following command: - -```shell -pnpm dev -# If an app instance already exists, use a different command -pnpm dev:diff -# To using tauri built-in dev tool -pnpm dev:tauri +```bash +pnpm dev # Standard +pnpm dev:diff # If an app instance already exists +pnpm dev:tauri # Run Tauri development mode ``` ### Build the Project -To build this project: +Standard build: -```shell +```bash pnpm build ``` -For a faster build, use the following command +Fast build for testing: -```shell +```bash pnpm build:fast ``` -This uses Rust's fast-release profile which significantly reduces compilation time by disabling optimization and LTO. The resulting binary will be larger and less performant than the standard build, but it's useful for testing changes quickly. +### Clean Build -The `Artifacts` will display in the `log` in the Terminal. - -### Build clean - -To clean rust build: - -```shell +```bash pnpm clean ``` ### Portable Version (Windows Only) -To package portable version after the build: - -```shell +```bash pnpm portable ``` ## Contributing Your Changes -#### Before commit your changes +### Before Committing -If you changed the rust code, it's recommanded to execute code style formatting and quailty checks. - -1. Code quailty checks +**Code quality checks:** ```bash -# For rust backend -$ clash-verge-rev: pnpm clippy -# For frontend (not yet). +# Rust backend +cargo clippy-all +# Frontend +pnpm lint ``` -2. Code style formatting +**Code formatting:** ```bash -# For rust backend -$ clash-verge-rev: cd src-tauri -$ clash-verge-rev/src-tauri: cargo fmt -# For frontend -$ clash-verge-rev: pnpm format:check -$ clash-verge-rev: pnpm format +# Rust backend +cargo fmt +# Frontend +pnpm format ``` -Once you have made your changes: +### Submitting Your Changes 1. Fork the repository. -2. Create a new branch for your feature or bug fix. -3. Commit your changes with clear and concise commit messages. -4. Push your branch to your fork and submit a pull request to our repository. -We appreciate your contributions and look forward to your active participation in our project! +2. Create a new branch for your feature or bug fix. + +3. Commit your changes with clear messages. + +4. Push your branch and submit a pull request. + +We appreciate your contributions and look forward to your participation! diff --git a/clash-verge-rev/src-tauri/Cargo.lock b/clash-verge-rev/Cargo.lock similarity index 94% rename from clash-verge-rev/src-tauri/Cargo.lock rename to clash-verge-rev/Cargo.lock index f763b73022..cc3352a4dc 100644 --- a/clash-verge-rev/src-tauri/Cargo.lock +++ b/clash-verge-rev/Cargo.lock @@ -170,6 +170,24 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "ashpd" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3d60bee1a1d38c2077030f4788e1b4e31058d2e79a8cfc8f2b440bd44db290" +dependencies = [ + "async-fs 2.2.0", + "async-net 2.0.0", + "enumflags2", + "futures-channel", + "futures-util", + "rand 0.8.5", + "serde", + "serde_repr", + "url", + "zbus", +] + [[package]] name = "ashpd" version = "0.11.0" @@ -252,6 +270,32 @@ dependencies = [ "futures-lite 1.13.0", ] +[[package]] +name = "async-fs" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" +dependencies = [ + "async-lock 3.4.1", + "blocking", + "futures-lite 2.6.1", +] + +[[package]] +name = "async-global-executor" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" +dependencies = [ + "async-channel 2.5.0", + "async-executor", + "async-io 2.6.0", + "async-lock 3.4.1", + "blocking", + "futures-lite 2.6.1", + "once_cell", +] + [[package]] name = "async-io" version = "1.13.0" @@ -321,6 +365,17 @@ dependencies = [ "futures-lite 1.13.0", ] +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io 2.6.0", + "blocking", + "futures-lite 2.6.1", +] + [[package]] name = "async-process" version = "1.8.1" @@ -364,7 +419,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -385,6 +440,32 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "async-std" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b" +dependencies = [ + "async-channel 1.9.0", + "async-global-executor", + "async-io 2.6.0", + "async-lock 3.4.1", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite 2.6.1", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -404,7 +485,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -421,7 +502,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -500,9 +581,9 @@ dependencies = [ [[package]] name = "axum" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871" +checksum = "5b098575ebe77cb6d14fc7f32749631a6e44edbef6b796f89b020e99ba20d425" dependencies = [ "axum-core 0.5.5", "bytes", @@ -676,7 +757,7 @@ dependencies = [ "boa_interner", "boa_macros", "boa_string", - "indexmap 2.12.0", + "indexmap 2.12.1", "num-bigint", "rustc-hash", ] @@ -706,9 +787,9 @@ dependencies = [ "futures-channel", "futures-concurrency", "futures-lite 2.6.1", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "icu_normalizer", - "indexmap 2.12.0", + "indexmap 2.12.1", "intrusive-collections", "itertools 0.14.0", "num-bigint", @@ -741,7 +822,7 @@ checksum = "f1179f690cbfcbe5364cceee5f1cb577265bb6f07b0be6f210aabe270adcf9da" dependencies = [ "boa_macros", "boa_string", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "thin-vec", ] @@ -753,8 +834,8 @@ checksum = "9626505d33dc63d349662437297df1d3afd9d5fc4a2b3ad34e5e1ce879a78848" dependencies = [ "boa_gc", "boa_macros", - "hashbrown 0.16.0", - "indexmap 2.12.0", + "hashbrown 0.16.1", + "indexmap 2.12.1", "once_cell", "phf 0.13.1", "rustc-hash", @@ -771,7 +852,7 @@ dependencies = [ "cow-utils", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", "synstructure", ] @@ -861,7 +942,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -878,9 +959,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" dependencies = [ "serde", ] @@ -978,9 +1059,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.43" +version = "1.2.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "739eb0f94557554b3ca9a86d2d37bebd49c5e6d0c1d2bda35ba5bdac830befc2" +checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07" dependencies = [ "find-msvc-tools", "jobserver", @@ -1080,18 +1161,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.51" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.51" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" dependencies = [ "anstyle", "clap_lex", @@ -1115,11 +1196,16 @@ dependencies = [ "base64 0.22.1", "boa_engine", "chrono", + "clash-verge-draft", + "clash-verge-logging", + "clash-verge-signal", + "clash-verge-types", "clash_verge_logger", "clash_verge_service_ipc", "compact_str", "console-subscriber", "criterion", + "dark-light", "deelevate", "delay_timer", "dunce", @@ -1127,7 +1213,6 @@ dependencies = [ "futures", "gethostname", "getrandom 0.3.4", - "libc", "log", "nanoid", "network-interface", @@ -1141,18 +1226,18 @@ dependencies = [ "reqwest_dav", "runas", "rust-i18n", + "rust_iso3166", "scopeguard", "serde", "serde_json", "serde_yaml_ng", - "signal-hook 0.3.18", "smartstring", "sys-locale", - "sysinfo", "sysproxy", "tauri", "tauri-build", "tauri-plugin-autostart", + "tauri-plugin-clash-verge-sysinfo", "tauri-plugin-clipboard-manager", "tauri-plugin-deep-link", "tauri-plugin-devtools", @@ -1170,15 +1255,55 @@ dependencies = [ "tokio-stream", "warp", "winapi", - "windows-sys 0.61.2", "winreg 0.55.0", "zip 6.0.0", ] +[[package]] +name = "clash-verge-draft" +version = "0.1.0" +dependencies = [ + "anyhow", + "criterion", + "parking_lot", + "tokio", +] + +[[package]] +name = "clash-verge-logging" +version = "0.1.0" +dependencies = [ + "compact_str", + "flexi_logger", + "log", + "tokio", +] + +[[package]] +name = "clash-verge-signal" +version = "0.1.0" +dependencies = [ + "clash-verge-logging", + "log", + "signal-hook 0.3.18", + "tauri", + "tokio", + "windows-sys 0.61.2", +] + +[[package]] +name = "clash-verge-types" +version = "0.1.0" +dependencies = [ + "serde", + "serde_yaml_ng", + "smartstring", +] + [[package]] name = "clash_verge_logger" -version = "0.2.0" -source = "git+https://github.com/clash-verge-rev/clash-verge-logger#9bb189b5b5c4c2eee35168ff4997e8fb10901c81" +version = "0.2.1" +source = "git+https://github.com/clash-verge-rev/clash-verge-logger#955f1b709890640ff01fd30009df0f35816bbca6" dependencies = [ "arraydeque", "compact_str", @@ -1191,7 +1316,7 @@ dependencies = [ [[package]] name = "clash_verge_service_ipc" version = "2.0.21" -source = "git+https://github.com/clash-verge-rev/clash-verge-service-ipc#1e34c648e48f8580208ff777686092e0a94b8025" +source = "git+https://github.com/clash-verge-rev/clash-verge-service-ipc#da00a684c2b9723d647ed4992714eb669fcbd8a2" dependencies = [ "anyhow", "compact_str", @@ -1287,7 +1412,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f76990911f2267d837d9d0ad060aa63aaad170af40904b29461734c339030d4d" dependencies = [ "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -1586,9 +1711,9 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "rand_core 0.6.4", @@ -1619,7 +1744,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.108", + "syn 2.0.111", +] + +[[package]] +name = "csv" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde_core", +] + +[[package]] +name = "csv-core" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" +dependencies = [ + "memchr", ] [[package]] @@ -1629,7 +1775,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -1641,6 +1787,20 @@ dependencies = [ "cipher", ] +[[package]] +name = "dark-light" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18e1a09f280e29a8b00bc7e81eca5ac87dca0575639c9422a5fa25a07bb884b8" +dependencies = [ + "ashpd 0.10.3", + "async-std", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "web-sys", + "winreg 0.52.0", +] + [[package]] name = "darling" version = "0.21.3" @@ -1662,7 +1822,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -1673,7 +1833,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -1778,7 +1938,7 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -1791,7 +1951,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version 0.4.1", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -1893,6 +2053,16 @@ dependencies = [ "dirs-sys 0.5.0", ] +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + [[package]] name = "dirs-sys" version = "0.3.7" @@ -1916,6 +2086,17 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users 0.4.6", + "winapi", +] + [[package]] name = "dispatch" version = "0.2.0" @@ -1942,7 +2123,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -1974,7 +2155,7 @@ checksum = "788160fb30de9cdd857af31c6a2675904b16ece8fc2737b2c7127ba368c9d0f4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -2060,7 +2241,7 @@ checksum = "1ec431cd708430d5029356535259c5d645d60edd3d39c54e5eea9782d46caa7d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -2089,6 +2270,12 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -2122,7 +2309,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -2142,7 +2329,7 @@ checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -2153,9 +2340,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "erased-serde" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "259d404d09818dec19332e31d94558aeb442fea04c817006456c24b5460bbd4b" +checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" dependencies = [ "serde", "serde_core", @@ -2254,7 +2441,7 @@ checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -2301,15 +2488,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" - -[[package]] -name = "fixedbitset" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" [[package]] name = "fixedbitset" @@ -2396,7 +2577,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -2474,7 +2655,7 @@ version = "7.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eb68017df91f2e477ed4bea586c59eaecaa47ed885a770d0444e21e62572cd2" dependencies = [ - "fixedbitset 0.5.7", + "fixedbitset", "futures-buffered", "futures-core", "futures-lite 2.6.1", @@ -2542,7 +2723,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -2699,9 +2880,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.9" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -2831,7 +3012,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -2892,6 +3073,18 @@ dependencies = [ "walkdir", ] +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "gobject-sys" version = "0.18.0" @@ -2952,7 +3145,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -2967,7 +3160,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.12.0", + "indexmap 2.12.1", "slab", "tokio", "tokio-util", @@ -2986,7 +3179,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.3.1", - "indexmap 2.12.0", + "indexmap 2.12.1", "slab", "tokio", "tokio-util", @@ -3043,9 +3236,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", @@ -3252,7 +3445,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.4.10", + "socket2 0.5.10", "tokio", "tower-service", "tracing", @@ -3261,9 +3454,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ "atomic-waker", "bytes", @@ -3289,7 +3482,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ "http 1.3.1", - "hyper 1.7.0", + "hyper 1.8.1", "hyper-util", "rustls", "rustls-pki-types", @@ -3317,7 +3510,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" dependencies = [ - "hyper 1.7.0", + "hyper 1.8.1", "hyper-util", "pin-project-lite", "tokio", @@ -3332,7 +3525,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper 1.7.0", + "hyper 1.8.1", "hyper-util", "native-tls", "tokio", @@ -3342,9 +3535,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" dependencies = [ "base64 0.22.1", "bytes", @@ -3353,7 +3546,7 @@ dependencies = [ "futures-util", "http 1.3.1", "http-body 1.0.1", - "hyper 1.7.0", + "hyper 1.8.1", "ipnet", "libc", "percent-encoding", @@ -3363,7 +3556,7 @@ dependencies = [ "tokio", "tower-service", "tracing", - "windows-registry", + "windows-registry 0.6.1", ] [[package]] @@ -3534,9 +3727,9 @@ dependencies = [ [[package]] name = "image" -version = "0.25.8" +version = "0.25.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "529feb3e6769d234375c4cf1ee2ce713682b8e76538cb13f9fc23e1400a591e7" +checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" dependencies = [ "bytemuck", "byteorder-lite", @@ -3559,12 +3752,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "serde", "serde_core", ] @@ -3666,9 +3859,9 @@ dependencies = [ [[package]] name = "iri-string" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" dependencies = [ "memchr", "serde", @@ -3683,6 +3876,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi 0.5.2", + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "is-wsl" version = "0.4.0" @@ -3869,10 +4073,19 @@ checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" dependencies = [ "cssparser", "html5ever", - "indexmap 2.12.0", + "indexmap 2.12.1", "selectors", ] +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -3911,9 +4124,9 @@ checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" [[package]] name = "libc" -version = "0.2.177" +version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "libloading" @@ -4008,9 +4221,12 @@ dependencies = [ [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +dependencies = [ + "value-bag", +] [[package]] name = "loom" @@ -4099,7 +4315,7 @@ checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -4266,7 +4482,7 @@ checksum = "4568f25ccbd45ab5d5603dc34318c1ec56b117531781260002151b8530a9f931" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -4421,6 +4637,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + [[package]] name = "normpath" version = "1.5.0" @@ -4527,7 +4752,7 @@ dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -4857,9 +5082,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "open" -version = "5.3.2" +version = "5.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95" +checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc" dependencies = [ "dunce", "is-wsl", @@ -4869,9 +5094,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.74" +version = "0.10.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24ad14dd45412269e1a30f52ad8f0664f0f4f4a89ee8fe28c3b3527021ebb654" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ "bitflags 2.10.0", "cfg-if", @@ -4890,7 +5115,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -4901,9 +5126,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.110" +version = "0.9.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" dependencies = [ "cc", "libc", @@ -5073,9 +5298,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.3" +version = "2.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989e7521a040efde50c3ab6bbadafbe15ab6dc042686926be59ac35d74607df4" +checksum = "cbcfd20a6d4eeba40179f05735784ad32bdaef05ce8e8af05f180d45bb3e7e22" dependencies = [ "memchr", "ucd-trie", @@ -5083,9 +5308,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.3" +version = "2.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "187da9a3030dbafabbbfb20cb323b976dc7b7ce91fcd84f2f74d6e31d378e2de" +checksum = "51f72981ade67b1ca6adc26ec221be9f463f2b5839c7508998daa17c23d94d7f" dependencies = [ "pest", "pest_generator", @@ -5093,22 +5318,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.3" +version = "2.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49b401d98f5757ebe97a26085998d6c0eecec4995cad6ab7fc30ffdf4b052843" +checksum = "dee9efd8cdb50d719a80088b76f81aec7c41ed6d522ee750178f83883d271625" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] name = "pest_meta" -version = "2.8.3" +version = "2.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f27a2cfee9f9039c4d86faa5af122a0ac3851441a34865b8a043b46be0065a" +checksum = "bf1d70880e76bdc13ba52eafa6239ce793d85c8e43896507e43dd8984ff05b82" dependencies = [ "pest", "sha2 0.10.9", @@ -5116,12 +5341,13 @@ dependencies = [ [[package]] name = "petgraph" -version = "0.6.5" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" dependencies = [ - "fixedbitset 0.4.2", - "indexmap 2.12.0", + "fixedbitset", + "hashbrown 0.15.5", + "indexmap 2.12.1", ] [[package]] @@ -5248,7 +5474,7 @@ dependencies = [ "phf_shared 0.11.3", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -5261,7 +5487,7 @@ dependencies = [ "phf_shared 0.13.1", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -5317,7 +5543,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -5356,8 +5582,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" dependencies = [ "base64 0.22.1", - "indexmap 2.12.0", - "quick-xml 0.38.3", + "indexmap 2.12.1", + "quick-xml 0.38.4", "serde", "time", ] @@ -5496,9 +5722,9 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppmd-rust" -version = "1.2.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c834641d8ad1b348c9ee86dec3b9840d805acd5f24daa5f90c788951a52ff59b" +checksum = "d558c559f0450f16f2a27a1f017ef38468c1090c9ce63c8e51366232d53717b4" [[package]] name = "ppv-lite86" @@ -5515,6 +5741,20 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +[[package]] +name = "prettytable-rs" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eea25e07510aa6ab6547308ebe3c036016d162b8da920dbb079e3ba8acf3d95a" +dependencies = [ + "csv", + "encode_unicode", + "is-terminal", + "lazy_static", + "term", + "unicode-width", +] + [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -5531,7 +5771,7 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" dependencies = [ - "toml_edit 0.20.2", + "toml_edit 0.20.7", ] [[package]] @@ -5612,7 +5852,7 @@ dependencies = [ "itertools 0.12.1", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -5625,7 +5865,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -5669,7 +5909,7 @@ checksum = "7347867d0a7e1208d93b46767be83e2b8f978c3dad35f775ac8d8847551d6fe1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -5708,9 +5948,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.38.3" +version = "0.38.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" dependencies = [ "memchr", ] @@ -5772,9 +6012,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.41" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] @@ -5984,7 +6224,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -6022,7 +6262,7 @@ version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2057b2325e68a893284d1538021ab90279adac1139957ca2a74426c6f118fb48" dependencies = [ - "hashbrown 0.16.0", + "hashbrown 0.16.1", "memchr", ] @@ -6049,7 +6289,7 @@ dependencies = [ "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper 1.7.0", + "hyper 1.8.1", "hyper-rustls", "hyper-tls", "hyper-util", @@ -6107,7 +6347,7 @@ version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" dependencies = [ - "ashpd", + "ashpd 0.11.0", "block2 0.6.2", "dispatch2", "glib-sys", @@ -6172,7 +6412,7 @@ checksum = "bd83f5f173ff41e00337d97f6572e416d022ef8a19f371817259ae960324c482" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -6221,7 +6461,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -6257,6 +6497,18 @@ dependencies = [ "ordered-multimap", ] +[[package]] +name = "rust_iso3166" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df0d3a0089ee08071ea1baaba83f2265c97f7646c53c3f92b205eb2cdaab72b1" +dependencies = [ + "js-sys", + "phf 0.11.3", + "prettytable-rs", + "wasm-bindgen", +] + [[package]] name = "rustc-hash" version = "2.1.1" @@ -6323,9 +6575,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.34" +version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ "once_cell", "ring", @@ -6421,9 +6673,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" dependencies = [ "dyn-clone", "ref-cast", @@ -6440,7 +6692,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -6590,7 +6842,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -6601,7 +6853,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -6625,7 +6877,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -6660,17 +6912,17 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.15.1" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa66c845eee442168b2c8134fec70ac50dc20e760769c8ba0ad1319ca1959b04" +checksum = "10574371d41b0d9b2cff89418eda27da52bcaff2cc8741db26382a77c29131f1" dependencies = [ "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.12.0", + "indexmap 2.12.1", "schemars 0.9.0", - "schemars 1.0.4", + "schemars 1.1.0", "serde_core", "serde_json", "serde_with_macros", @@ -6679,14 +6931,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.15.1" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91a903660542fced4e99881aa481bdbaec1634568ee02e0b8bd57c64cb38955" +checksum = "08a72d8216842fdd57820dc78d840bef99248e35fb2554ff923319e60f2d686b" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -6695,7 +6947,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.12.1", "itoa", "ryu", "serde", @@ -6708,7 +6960,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b4db627b98b36d4203a7b458cf3573730f2bb591b28871d916dfa9efabfd41f" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.12.1", "itoa", "ryu", "serde", @@ -6734,7 +6986,7 @@ checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -6851,9 +7103,9 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.6" +version = "1.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" dependencies = [ "libc", ] @@ -6917,10 +7169,10 @@ checksum = "13f2b548cd8447f8de0fdf1c592929f70f4fc7039a05e47404b0d096ec6987a1" dependencies = [ "async-channel 1.9.0", "async-executor", - "async-fs", + "async-fs 1.6.0", "async-io 1.13.0", "async-lock 2.8.0", - "async-net", + "async-net 1.8.0", "async-process 1.8.1", "blocking", "futures-lite 1.13.0", @@ -6936,6 +7188,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "socket2" version = "0.6.1" @@ -7058,7 +7320,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -7091,9 +7353,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.108" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", @@ -7123,7 +7385,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -7151,13 +7413,14 @@ dependencies = [ [[package]] name = "sysproxy" -version = "0.3.1" -source = "git+https://github.com/clash-verge-rev/sysproxy-rs#ea6e5b5bcef32025e1df914d663eea8558afacb2" +version = "0.4.0" +source = "git+https://github.com/clash-verge-rev/sysproxy-rs#0f844dd2639b0ac74da4548b1325335844947420" dependencies = [ "interfaces", "iptools", "log", "thiserror 2.0.17", + "tokio", "url", "windows 0.62.2", "winreg 0.55.0", @@ -7252,7 +7515,7 @@ checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -7280,9 +7543,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.9.3" +version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e492485dd390b35f7497401f67694f46161a2a00ffd800938d5dd3c898fb9d8" +checksum = "15524fc7959bfcaa051ba6d0b3fb1ef18e978de2176c7c6acb977f7fd14d35c7" dependencies = [ "anyhow", "bytes", @@ -7334,9 +7597,9 @@ dependencies = [ [[package]] name = "tauri-build" -version = "2.5.2" +version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87d6f8cafe6a75514ce5333f115b7b1866e8e68d9672bf4ca89fc0f35697ea9d" +checksum = "17fcb8819fd16463512a12f531d44826ce566f486d7ccd211c9c8cebdaec4e08" dependencies = [ "anyhow", "cargo_toml", @@ -7356,9 +7619,9 @@ dependencies = [ [[package]] name = "tauri-codegen" -version = "2.5.1" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7ef707148f0755110ca54377560ab891d722de4d53297595380a748026f139f" +checksum = "9fa9844cefcf99554a16e0a278156ae73b0d8680bbc0e2ad1e4287aadd8489cf" dependencies = [ "base64 0.22.1", "brotli", @@ -7372,7 +7635,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.10.9", - "syn 2.0.108", + "syn 2.0.111", "tauri-utils", "thiserror 2.0.17", "time", @@ -7383,14 +7646,14 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.5.1" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71664fd715ee6e382c05345ad258d6d1d50f90cf1b58c0aa726638b33c2a075d" +checksum = "3764a12f886d8245e66b7ee9b43ccc47883399be2019a61d80cf0f4117446fde" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", "tauri-codegen", "tauri-utils", ] @@ -7426,6 +7689,18 @@ dependencies = [ "thiserror 2.0.17", ] +[[package]] +name = "tauri-plugin-clash-verge-sysinfo" +version = "0.1.0" +dependencies = [ + "deelevate", + "libc", + "parking_lot", + "sysinfo", + "tauri", + "tauri-plugin-clipboard-manager", +] + [[package]] name = "tauri-plugin-clipboard-manager" version = "2.3.2" @@ -7458,7 +7733,7 @@ dependencies = [ "thiserror 2.0.17", "tracing", "url", - "windows-registry", + "windows-registry 0.5.3", "windows-result 0.3.4", ] @@ -7571,7 +7846,7 @@ dependencies = [ [[package]] name = "tauri-plugin-mihomo" version = "0.1.1" -source = "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo#d0f00b33cea294cc693e177441fc897426ecbc39" +source = "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo#24586eb0721314f88e65460b4ac01933b3376d3c" dependencies = [ "base64 0.22.1", "futures-util", @@ -7692,9 +7967,9 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "2.9.1" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9368f09358496f2229313fccb37682ad116b7f46fa76981efe116994a0628926" +checksum = "87f766fe9f3d1efc4b59b17e7a891ad5ed195fa8d23582abb02e6c9a01137892" dependencies = [ "cookie", "dpi", @@ -7717,9 +7992,9 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "2.9.1" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "929f5df216f5c02a9e894554401bcdab6eec3e39ec6a4a7731c7067fc8688a93" +checksum = "7950f3bde6bcca6655bc5e76d3d6ec587ceb81032851ab4ddbe1f508bdea2729" dependencies = [ "gtk", "http 1.3.1", @@ -7745,9 +8020,9 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6b8bbe426abdbf52d050e52ed693130dbd68375b9ad82a3fb17efb4c8d85673" +checksum = "76a423c51176eb3616ee9b516a9fa67fed5f0e78baaba680e44eb5dd2cc37490" dependencies = [ "anyhow", "brotli", @@ -7783,10 +8058,11 @@ dependencies = [ [[package]] name = "tauri-winres" -version = "0.3.3" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd21509dd1fa9bd355dc29894a6ff10635880732396aa38c0066c1e6c1ab8074" +checksum = "1087b111fe2b005e42dbdc1990fc18593234238d47453b0c99b7de1c9ab2c1e0" dependencies = [ + "dunce", "embed-resource", "toml 0.9.8", ] @@ -7827,6 +8103,17 @@ dependencies = [ "utf-8", ] +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -7922,7 +8209,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -7933,7 +8220,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -8074,7 +8361,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -8122,9 +8409,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.16" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" dependencies = [ "bytes", "futures-core", @@ -8151,7 +8438,7 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.12.1", "serde_core", "serde_spanned 1.0.3", "toml_datetime 0.7.3", @@ -8184,18 +8471,18 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.12.1", "toml_datetime 0.6.11", "winnow 0.5.40", ] [[package]] name = "toml_edit" -version = "0.20.2" +version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.12.1", "toml_datetime 0.6.11", "winnow 0.5.40", ] @@ -8206,7 +8493,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.12.1", "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.11", @@ -8220,7 +8507,7 @@ version = "0.23.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.12.1", "toml_datetime 0.7.3", "toml_parser", "winnow 0.7.13", @@ -8281,14 +8568,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb7613188ce9f7df5bfe185db26c5814347d110db17920415cf2fbcad85e7203" dependencies = [ "async-trait", - "axum 0.8.6", + "axum 0.8.7", "base64 0.22.1", "bytes", "h2 0.4.12", "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper 1.7.0", + "hyper 1.8.1", "hyper-timeout 0.5.2", "hyper-util", "percent-encoding", @@ -8375,7 +8662,7 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", - "indexmap 2.12.0", + "indexmap 2.12.1", "pin-project-lite", "slab", "sync_wrapper 1.0.2", @@ -8454,7 +8741,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -8520,13 +8807,12 @@ dependencies = [ [[package]] name = "tree_magic_mini" -version = "3.2.0" +version = "3.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f943391d896cdfe8eec03a04d7110332d445be7df856db382dd96a730667562c" +checksum = "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6" dependencies = [ "memchr", - "nom 7.1.3", - "once_cell", + "nom 8.0.0", "petgraph", ] @@ -8565,7 +8851,7 @@ checksum = "ee6ff59666c9cbaec3533964505d39154dc4e0a56151fdea30a09ed0301f62e2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", "termcolor", ] @@ -8674,6 +8960,12 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "universal-hash" version = "0.5.1" @@ -8768,6 +9060,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "value-bag" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" + [[package]] name = "vcpkg" version = "0.2.15" @@ -8776,9 +9074,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version-compare" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" [[package]] name = "version_check" @@ -8852,7 +9150,7 @@ dependencies = [ "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper 1.7.0", + "hyper 1.8.1", "hyper-util", "log", "mime", @@ -8935,7 +9233,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", "wasm-bindgen-shared", ] @@ -9100,9 +9398,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b130c0d2d49f8b6889abc456e795e82525204f27c42cf767cf0d7734e089b8" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" dependencies = [ "rustls-pki-types", ] @@ -9129,7 +9427,7 @@ checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -9145,9 +9443,9 @@ dependencies = [ [[package]] name = "weezl" -version = "0.1.10" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" [[package]] name = "which" @@ -9312,7 +9610,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -9323,7 +9621,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -9369,6 +9667,17 @@ dependencies = [ "windows-strings 0.4.2", ] +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + [[package]] name = "windows-result" version = "0.3.4" @@ -9756,6 +10065,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "winreg" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "winreg" version = "0.55.0" @@ -9940,7 +10259,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", "synstructure", ] @@ -9988,7 +10307,7 @@ dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", "zbus_names", "zvariant", "zvariant_utils", @@ -10008,22 +10327,22 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "43fa6694ed34d6e57407afbccdeecfa268c470a7d2a5b0cf49ce9fcc345afb90" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "c640b22cd9817fae95be82f0d2f90b11f7605f6c319d16705c459b27ac2cbc26" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -10043,7 +10362,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", "synstructure", ] @@ -10064,7 +10383,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -10098,7 +10417,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -10109,7 +10428,7 @@ checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" dependencies = [ "arbitrary", "crc32fast", - "indexmap 2.12.0", + "indexmap 2.12.1", "memchr", ] @@ -10128,7 +10447,7 @@ dependencies = [ "flate2", "getrandom 0.3.4", "hmac", - "indexmap 2.12.0", + "indexmap 2.12.1", "lzma-rust2", "memchr", "pbkdf2", @@ -10225,7 +10544,7 @@ dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", "zvariant_utils", ] @@ -10238,6 +10557,6 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.108", + "syn 2.0.111", "winnow 0.7.13", ] diff --git a/clash-verge-rev/Cargo.toml b/clash-verge-rev/Cargo.toml new file mode 100644 index 0000000000..dd4607e89b --- /dev/null +++ b/clash-verge-rev/Cargo.toml @@ -0,0 +1,134 @@ +[workspace] +members = [ + "src-tauri", + "crates/clash-verge-draft", + "crates/clash-verge-logging", + "crates/clash-verge-signal", + "crates/tauri-plugin-clash-verge-sysinfo", + "crates/clash-verge-types", +] +resolver = "2" + +[workspace.package] +edition = "2024" +rust-version = "1.91" + +[profile.release] +panic = "abort" +codegen-units = 1 +lto = "thin" +opt-level = 3 +debug = false +strip = true +overflow-checks = false +rpath = false + +[profile.dev] +incremental = true +codegen-units = 64 +opt-level = 0 +debug = true +strip = "none" +overflow-checks = true +lto = false +rpath = false + +[profile.fast-release] +inherits = "release" +codegen-units = 64 +incremental = true +lto = false +opt-level = 0 +debug = true +strip = false + +[workspace.dependencies] +clash-verge-draft = { path = "crates/clash-verge-draft" } +clash-verge-logging = { path = "crates/clash-verge-logging" } +clash-verge-signal = { path = "crates/clash-verge-signal" } +clash-verge-types = { path = "crates/clash-verge-types" } +tauri-plugin-clash-verge-sysinfo = { path = "crates/tauri-plugin-clash-verge-sysinfo" } + +tauri = { version = "2.9.4" } +tauri-plugin-clipboard-manager = "2.3.2" +parking_lot = { version = "0.12.5", features = ["hardware-lock-elision"] } +anyhow = "1.0.100" +criterion = { version = "0.7.0", features = ["async_tokio"] } +tokio = { version = "1.48.0", features = [ + "rt-multi-thread", + "macros", + "time", + "sync", +] } +flexi_logger = "0.31.7" +log = "0.4.29" + +smartstring = { version = "1.0.1" } +compact_str = { version = "0.9.0", features = ["serde"] } + +serde = { version = "1.0.228" } +serde_json = { version = "1.0.145" } +serde_yaml_ng = { version = "0.10.0" } + +# *** For Windows platform only *** +deelevate = "0.2.0" +# ********************************* + +[workspace.lints.clippy] +correctness = { level = "deny", priority = -1 } +suspicious = { level = "deny", priority = -1 } +unwrap_used = "warn" +expect_used = "warn" +panic = "deny" +unimplemented = "deny" +todo = "warn" +dbg_macro = "warn" +clone_on_ref_ptr = "warn" +rc_clone_in_vec_init = "warn" +large_stack_arrays = "warn" +large_const_arrays = "warn" +async_yields_async = "deny" +mutex_atomic = "deny" +mutex_integer = "deny" +rc_mutex = "deny" +unused_async = "deny" +await_holding_lock = "deny" +large_futures = "deny" +future_not_send = "deny" +redundant_else = "deny" +needless_continue = "deny" +needless_raw_string_hashes = "deny" +or_fun_call = "deny" +cognitive_complexity = "deny" +useless_let_if_seq = "deny" +use_self = "deny" +tuple_array_conversions = "deny" +trait_duplication_in_bounds = "deny" +suspicious_operation_groupings = "deny" +string_lit_as_bytes = "deny" +significant_drop_tightening = "deny" +significant_drop_in_scrutinee = "deny" +redundant_clone = "deny" +# option_if_let_else = "deny" // 过于激进,暂时不开启 +needless_pass_by_ref_mut = "deny" +needless_collect = "deny" +missing_const_for_fn = "deny" +iter_with_drain = "deny" +iter_on_single_items = "deny" +iter_on_empty_collections = "deny" +# fallible_impl_from = "deny" // 过于激进,暂时不开启 +equatable_if_let = "deny" +collection_is_never_read = "deny" +branches_sharing_code = "deny" +pathbuf_init_then_push = "deny" +option_as_ref_cloned = "deny" +large_types_passed_by_value = "deny" +# implicit_clone = "deny" // 可能会造成额外开销,暂时不开启 +expl_impl_clone_on_copy = "deny" +copy_iterator = "deny" +cloned_instead_of_copied = "deny" +# self_only_used_in_recursion = "deny" // Since 1.92.0 +unnecessary_self_imports = "deny" +unused_trait_names = "deny" +wildcard_imports = "deny" +unnecessary_wraps = "deny" diff --git a/clash-verge-rev/Changelog.md b/clash-verge-rev/Changelog.md index 264ac63038..f786ab6ef2 100644 --- a/clash-verge-rev/Changelog.md +++ b/clash-verge-rev/Changelog.md @@ -1,29 +1,63 @@ ## v2.4.4 +- **Mihomo(Meta) 内核升级至 v1.19.17** + +> [!WARNING] +> Apple 公证服务故障,临时暂停 macOS 签名 +> macOS 跳过签名,终端执行 `sudo xattr -rd com.apple.quarantine /Applications/Clash\ Verge.app/` + ### 🐞 修复问题 - Linux 无法切换 TUN 堆栈 - macOS service 启动项显示名称(试验性修改) +- macOS 非预期 Tproxy 端口设置 +- 流量图缩放异常 +- PAC 自动代理脚本内容无法动态调整 +- 兼容从旧版服务模式升级 +- Monaco 编辑器的行数上限 +- 已删除节点在手动分组中导致配置无法加载 +- 仪表盘与托盘状态不同步 +- 修复重启或退出应用,关闭系统时无法记忆用户行为 +- 彻底修复 macOS 连接页面显示异常 +- windows 端监听关机信号失败 +- 修复代理按钮和高亮状态不同步 +- 修复侧边栏可能的未能正确跳转 +- 修复解锁测试部分地区图标编码不正确 +- 修复 IP 检测切页后强制刷新,改为仅在必要时更新 +- 修复在搜索框输入不完整正则直接崩溃 +- 修复创建窗口时在非简体中文环境或深色主题下的短暂闪烁 +- 修复更新时加载进度条异常
✨ 新增功能 -- **Mihomo(Meta) 内核升级至 v1.19.16** - 支持连接页面各个项目的排序 - 实现可选的自动备份 - 连接页面支持查看已关闭的连接(最近最多 500 个已关闭连接) +- 日志页面支持按时间倒序 +- 增加「重新激活订阅」的全局快捷键
🚀 优化改进 +- 网络请求改为使用 rustls,提升 TLS 兼容性 +- rustls 避免因服务器证书链配置问题或较新 TLS 要求导致订阅无法导入 - 替换前端信息编辑组件,提供更好性能 - 优化后端内存和性能表现 - 防止退出时可能的禁用 TUN 失败 - i18n 支持 - 优化备份设置布局 - 优化流量图性能表现,实现动态 FPS 和窗口失焦自动暂停 +- 性能优化系统状态获取 +- 优化托盘菜单当前订阅检测逻辑 +- 优化连接页面表格渲染 +- 优化链式代理 UI 反馈 +- 优化重启应用的资源清理逻辑 +- 优化前端数据刷新 +- 优化流量采样和数据处理 +- 优化应用重启/退出时的资源清理性能, 大幅缩短执行时间
diff --git a/clash-verge-rev/crates/clash-verge-draft/Cargo.toml b/clash-verge-rev/crates/clash-verge-draft/Cargo.toml new file mode 100644 index 0000000000..798b9b8a87 --- /dev/null +++ b/clash-verge-rev/crates/clash-verge-draft/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "clash-verge-draft" +version = "0.1.0" +edition = "2024" + +[[bench]] +name = "draft_bench" +path = "bench/benche_me.rs" +harness = false + +[dependencies] +parking_lot = { workspace = true } +anyhow = { workspace = true } + +[dev-dependencies] +criterion = { workspace = true } +tokio = { workspace = true } diff --git a/clash-verge-rev/src-tauri/benches/draft_benchmark.rs b/clash-verge-rev/crates/clash-verge-draft/bench/benche_me.rs similarity index 93% rename from clash-verge-rev/src-tauri/benches/draft_benchmark.rs rename to clash-verge-rev/crates/clash-verge-draft/bench/benche_me.rs index c8a07c536e..792618ad96 100644 --- a/clash-verge-rev/src-tauri/benches/draft_benchmark.rs +++ b/clash-verge-rev/crates/clash-verge-draft/bench/benche_me.rs @@ -3,17 +3,20 @@ use std::hint::black_box; use std::process; use tokio::runtime::Runtime; -use app_lib::config::IVerge; -use app_lib::utils::Draft as DraftNew; +use clash_verge_draft::Draft; -/// 创建测试数据 -fn make_draft() -> DraftNew { +#[derive(Default, Clone, Debug)] +struct IVerge { + enable_auto_launch: Option, + enable_tun_mode: Option, +} + +fn make_draft() -> Draft { let verge = IVerge { enable_auto_launch: Some(true), enable_tun_mode: Some(false), - ..Default::default() }; - DraftNew::new(verge) + Draft::new(verge) } pub fn bench_draft(c: &mut Criterion) { diff --git a/clash-verge-rev/crates/clash-verge-draft/src/lib.rs b/clash-verge-rev/crates/clash-verge-draft/src/lib.rs new file mode 100644 index 0000000000..5d548d091f --- /dev/null +++ b/clash-verge-rev/crates/clash-verge-draft/src/lib.rs @@ -0,0 +1,102 @@ +use parking_lot::RwLock; +use std::sync::Arc; + +pub type SharedBox = Arc>; +type DraftInner = (SharedBox, Option>); + +/// Draft 管理:committed 与 optional draft 都以 Arc> 存储, +// (committed_snapshot, optional_draft_snapshot) +#[derive(Debug, Clone)] +pub struct Draft { + inner: Arc>>, +} + +impl Draft { + #[inline] + pub fn new(data: T) -> Self { + Self { + inner: Arc::new(RwLock::new((Arc::new(Box::new(data)), None))), + } + } + /// 以 Arc> 的形式获取当前“已提交(正式)”数据的快照(零拷贝,仅 clone Arc) + #[inline] + pub fn data_arc(&self) -> SharedBox { + let guard = self.inner.read(); + Arc::clone(&guard.0) + } + + /// 获取当前(草稿若存在则返回草稿,否则返回已提交)的快照 + /// 这也是零拷贝:只 clone Arc,不 clone T + #[inline] + pub fn latest_arc(&self) -> SharedBox { + let guard = self.inner.read(); + guard.1.clone().unwrap_or_else(|| Arc::clone(&guard.0)) + } + + /// 通过闭包以可变方式编辑草稿(在闭包中我们给出 &mut T) + /// - 延迟拷贝:如果只有这一个 Arc 引用,则直接修改,不会克隆 T; + /// - 若草稿被其他读者共享,Arc::make_mut 会做一次 T.clone(最小必要拷贝)。 + #[inline] + pub fn edit_draft(&self, f: F) -> R + where + F: FnOnce(&mut T) -> R, + { + // 先获得写锁以创建或取出草稿 Arc 的可变引用位置 + let mut guard = self.inner.write(); + let mut draft_arc = if guard.1.is_none() { + Arc::clone(&guard.0) + } else { + #[allow(clippy::unwrap_used)] + guard.1.take().unwrap() + }; + drop(guard); + // Arc::make_mut: 如果只有一个引用则返回可变引用;否则会克隆底层 Box(要求 T: Clone) + let boxed = Arc::make_mut(&mut draft_arc); // &mut Box + // 对 Box 解引用得到 &mut T + let result = f(&mut **boxed); + // 恢复修改后的草稿 Arc + self.inner.write().1 = Some(draft_arc); + result + } + + /// 将草稿提交到已提交位置(替换),并清除草稿 + #[inline] + pub fn apply(&self) { + let mut guard = self.inner.write(); + if let Some(d) = guard.1.take() { + guard.0 = d; + } + } + + /// 丢弃草稿(如果存在) + #[inline] + pub fn discard(&self) { + let mut guard = self.inner.write(); + guard.1 = None; + } + + /// 异步地以拥有 Box 的方式修改已提交数据:将克隆一次已提交数据到本地, + /// 异步闭包返回新的 Box(替换已提交数据)和业务返回值 R。 + #[inline] + pub async fn with_data_modify(&self, f: F) -> Result + where + T: Send + Sync + 'static, + F: FnOnce(Box) -> Fut + Send, + Fut: std::future::Future, R), anyhow::Error>> + Send, + { + // 读取已提交快照(cheap Arc clone, 然后得到 Box 所有权 via clone) + // 注意:为了让闭包接收 Box 所有权,我们需要 clone 底层 T(不可避免) + let local: Box = { + let guard = self.inner.read(); + // 将 Arc> 的 Box clone 出来(会调用 T: Clone) + (*guard.0).clone() + }; + + let (new_local, res) = f(local).await?; + + // 将新的 Box 放到已提交位置(包进 Arc) + self.inner.write().0 = Arc::new(new_local); + + Ok(res) + } +} diff --git a/clash-verge-rev/src-tauri/src/utils/draft.rs b/clash-verge-rev/crates/clash-verge-draft/tests/test_me.rs similarity index 71% rename from clash-verge-rev/src-tauri/src/utils/draft.rs rename to clash-verge-rev/crates/clash-verge-draft/tests/test_me.rs index 07782f4bd2..9e214d1639 100644 --- a/clash-verge-rev/src-tauri/src/utils/draft.rs +++ b/clash-verge-rev/crates/clash-verge-draft/tests/test_me.rs @@ -1,110 +1,7 @@ -use parking_lot::RwLock; -use std::sync::Arc; - -pub type SharedBox = Arc>; -type DraftInner = (SharedBox, Option>); - -/// Draft 管理:committed 与 optional draft 都以 Arc> 存储, -// (committed_snapshot, optional_draft_snapshot) -#[derive(Debug, Clone)] -pub struct Draft { - inner: Arc>>, -} - -impl Draft { - #[inline] - pub fn new(data: T) -> Self { - Self { - inner: Arc::new(RwLock::new((Arc::new(Box::new(data)), None))), - } - } - /// 以 Arc> 的形式获取当前“已提交(正式)”数据的快照(零拷贝,仅 clone Arc) - #[inline] - pub fn data_arc(&self) -> SharedBox { - let guard = self.inner.read(); - Arc::clone(&guard.0) - } - - /// 获取当前(草稿若存在则返回草稿,否则返回已提交)的快照 - /// 这也是零拷贝:只 clone Arc,不 clone T - #[inline] - pub fn latest_arc(&self) -> SharedBox { - let guard = self.inner.read(); - guard.1.clone().unwrap_or_else(|| Arc::clone(&guard.0)) - } - - /// 通过闭包以可变方式编辑草稿(在闭包中我们给出 &mut T) - /// - 延迟拷贝:如果只有这一个 Arc 引用,则直接修改,不会克隆 T; - /// - 若草稿被其他读者共享,Arc::make_mut 会做一次 T.clone(最小必要拷贝)。 - #[inline] - pub fn edit_draft(&self, f: F) -> R - where - F: FnOnce(&mut T) -> R, - { - // 先获得写锁以创建或取出草稿 Arc 的可变引用位置 - let mut guard = self.inner.write(); - let mut draft_arc = if guard.1.is_none() { - Arc::clone(&guard.0) - } else { - #[allow(clippy::unwrap_used)] - guard.1.take().unwrap() - }; - drop(guard); - // Arc::make_mut: 如果只有一个引用则返回可变引用;否则会克隆底层 Box(要求 T: Clone) - let boxed = Arc::make_mut(&mut draft_arc); // &mut Box - // 对 Box 解引用得到 &mut T - let result = f(&mut **boxed); - // 恢复修改后的草稿 Arc - self.inner.write().1 = Some(draft_arc); - result - } - - /// 将草稿提交到已提交位置(替换),并清除草稿 - #[inline] - pub fn apply(&self) { - let mut guard = self.inner.write(); - if let Some(d) = guard.1.take() { - guard.0 = d; - } - } - - /// 丢弃草稿(如果存在) - #[inline] - pub fn discard(&self) { - let mut guard = self.inner.write(); - guard.1 = None; - } - - /// 异步地以拥有 Box 的方式修改已提交数据:将克隆一次已提交数据到本地, - /// 异步闭包返回新的 Box(替换已提交数据)和业务返回值 R。 - #[inline] - pub async fn with_data_modify(&self, f: F) -> Result - where - T: Send + Sync + 'static, - F: FnOnce(Box) -> Fut + Send, - Fut: std::future::Future, R), anyhow::Error>> + Send, - { - // 读取已提交快照(cheap Arc clone, 然后得到 Box 所有权 via clone) - // 注意:为了让闭包接收 Box 所有权,我们需要 clone 底层 T(不可避免) - let local: Box = { - let guard = self.inner.read(); - // 将 Arc> 的 Box clone 出来(会调用 T: Clone) - (*guard.0).clone() - }; - - let (new_local, res) = f(local).await?; - - // 将新的 Box 放到已提交位置(包进 Arc) - self.inner.write().0 = Arc::new(new_local); - - Ok(res) - } -} - #[cfg(test)] mod tests { - use super::*; use anyhow::anyhow; + use clash_verge_draft::Draft; use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker}; diff --git a/clash-verge-rev/crates/clash-verge-logging/Cargo.toml b/clash-verge-rev/crates/clash-verge-logging/Cargo.toml new file mode 100644 index 0000000000..d0f43ddfd4 --- /dev/null +++ b/clash-verge-rev/crates/clash-verge-logging/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "clash-verge-logging" +version = "0.1.0" +edition = "2024" + +[dependencies] +log = { workspace = true } +tokio = { workspace = true } +compact_str = { workspace = true } +flexi_logger = { workspace = true } + +[features] +default = [] +tauri-dev = [] \ No newline at end of file diff --git a/clash-verge-rev/src-tauri/src/utils/logging.rs b/clash-verge-rev/crates/clash-verge-logging/src/lib.rs similarity index 81% rename from clash-verge-rev/src-tauri/src/utils/logging.rs rename to clash-verge-rev/crates/clash-verge-logging/src/lib.rs index 7b5de419ba..bb8672f538 100644 --- a/clash-verge-rev/src-tauri/src/utils/logging.rs +++ b/clash-verge-rev/crates/clash-verge-logging/src/lib.rs @@ -34,6 +34,7 @@ pub enum Type { } impl fmt::Display for Type { + #[inline] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Cmd => write!(f, "[Cmd]"), @@ -58,44 +59,6 @@ impl fmt::Display for Type { } } -#[macro_export] -macro_rules! error { - ($result: expr) => { - log::error!(target: "app", "{}", $result); - }; -} - -#[macro_export] -macro_rules! log_err { - ($result: expr) => { - if let Err(err) = $result { - log::error!(target: "app", "{err}"); - } - }; - - ($result: expr, $err_str: expr) => { - if let Err(_) = $result { - log::error!(target: "app", "{}", $err_str); - } - }; -} - -/// wrap the anyhow error -/// transform the error to String -#[macro_export] -macro_rules! wrap_err { - // Case 1: Future> - ($stat:expr, async) => {{ - match $stat.await { - Ok(a) => Ok::<_, ::anyhow::Error>(a), - Err(err) => { - log::error!(target: "app", "{}", err); - Err(::anyhow::Error::msg(err.to_string())) - } - } - }}; -} - #[macro_export] macro_rules! logging { // 不带 print 参数的版本(默认不打印) @@ -119,6 +82,7 @@ macro_rules! logging_error { }; } +#[inline] pub fn write_sidecar_log( writer: MutexGuard<'_, FileLogWriter>, now: &mut DeferredNow, @@ -158,6 +122,7 @@ impl<'a> NoModuleFilter<'a> { #[cfg(not(feature = "tauri-dev"))] impl<'a> LogLineFilter for NoModuleFilter<'a> { + #[inline] fn write( &self, now: &mut DeferredNow, diff --git a/clash-verge-rev/crates/clash-verge-signal/Cargo.toml b/clash-verge-rev/crates/clash-verge-signal/Cargo.toml new file mode 100644 index 0000000000..2781509f29 --- /dev/null +++ b/clash-verge-rev/crates/clash-verge-signal/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "clash-verge-signal" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true + +[dependencies] +clash-verge-logging = { workspace = true } +log = { workspace = true } +tokio = { workspace = true } + +[target.'cfg(unix)'.dependencies] +signal-hook = "0.3.18" + +[target.'cfg(windows)'.dependencies] +tauri = { workspace = true } +windows-sys = { version = "0.61.2", features = [ + "Win32_Foundation", + "Win32_Graphics_Gdi", + "Win32_System_SystemServices", + "Win32_UI_WindowsAndMessaging", +] } + + +[lints] +workspace = true diff --git a/clash-verge-rev/crates/clash-verge-signal/src/lib.rs b/clash-verge-rev/crates/clash-verge-signal/src/lib.rs new file mode 100644 index 0000000000..a7b7671bc9 --- /dev/null +++ b/clash-verge-rev/crates/clash-verge-signal/src/lib.rs @@ -0,0 +1,35 @@ +use std::sync::OnceLock; + +use clash_verge_logging::{Type, logging}; + +#[cfg(unix)] +mod unix; +#[cfg(windows)] +mod windows; + +pub(crate) static RUNTIME: OnceLock> = OnceLock::new(); + +pub fn register(#[cfg(windows)] app_handle: &tauri::AppHandle, f: F) +where + F: Fn() -> Fut + Send + Sync + 'static, + Fut: Future + Send + 'static, +{ + RUNTIME.get_or_init(|| match tokio::runtime::Runtime::new() { + Ok(rt) => Some(rt), + Err(e) => { + logging!( + info, + Type::System, + "register shutdown signal failed, create tokio runtime error: {}", + e + ); + None + } + }); + + #[cfg(unix)] + unix::register(f); + + #[cfg(windows)] + windows::register(app_handle, f); +} diff --git a/clash-verge-rev/crates/clash-verge-signal/src/unix.rs b/clash-verge-rev/crates/clash-verge-signal/src/unix.rs new file mode 100644 index 0000000000..9729c32b46 --- /dev/null +++ b/clash-verge-rev/crates/clash-verge-signal/src/unix.rs @@ -0,0 +1,54 @@ +use signal_hook::{ + consts::{SIGHUP, SIGINT, SIGTERM}, + iterator::Signals, + low_level, +}; + +use clash_verge_logging::{Type, logging, logging_error}; + +use crate::RUNTIME; + +pub fn register(f: F) +where + F: Fn() -> Fut + Send + Sync + 'static, + Fut: Future + Send + 'static, +{ + if let Some(Some(rt)) = RUNTIME.get() { + rt.spawn(async move { + let signals = [SIGTERM, SIGINT, SIGHUP]; + + let mut sigs = match Signals::new(signals) { + Ok(s) => s, + Err(e) => { + logging!(error, Type::System, "注册信号处理器失败: {}", e); + return; + } + }; + + for signal in &mut sigs { + let signal_to_str = |signal| match signal { + SIGTERM => "SIGTERM", + SIGINT => "SIGINT", + SIGHUP => "SIGHUP", + _ => "UNKNOWN", + }; + + logging!(info, Type::System, "捕获到信号 {}", signal_to_str(signal)); + + f().await; + + logging_error!( + Type::System, + "信号 {:?} 默认处理失败", + low_level::emulate_default_handler(signal) + ); + } + }); + } else { + logging!( + error, + Type::System, + "register shutdown signal failed, RUNTIME is not available" + ); + } +} diff --git a/clash-verge-rev/src-tauri/src/module/signal/windows.rs b/clash-verge-rev/crates/clash-verge-signal/src/windows.rs similarity index 71% rename from clash-verge-rev/src-tauri/src/module/signal/windows.rs rename to clash-verge-rev/crates/clash-verge-signal/src/windows.rs index f10b415cc2..860208a28f 100644 --- a/clash-verge-rev/src-tauri/src/module/signal/windows.rs +++ b/clash-verge-rev/crates/clash-verge-signal/src/windows.rs @@ -1,4 +1,6 @@ -use tauri::Manager as _; +use std::{future::Future, pin::Pin, sync::OnceLock}; + +use tauri::{AppHandle, Manager as _}; use windows_sys::Win32::{ Foundation::{HWND, LPARAM, LRESULT, WPARAM}, UI::WindowsAndMessaging::{ @@ -8,12 +10,19 @@ use windows_sys::Win32::{ }, }; -use crate::{core::handle, feat, logging, utils::logging::Type}; +use clash_verge_logging::{Type, logging}; + +use crate::RUNTIME; // code refer to: // global-hotkey (https://github.com/tauri-apps/global-hotkey) // Global Shortcut (https://github.com/tauri-apps/plugins-workspace/tree/v2/plugins/global-shortcut) +type ShutdownHandler = + Box Pin + Send>> + Send + Sync>; + +static SHUTDOWN_HANDLER: OnceLock = OnceLock::new(); + struct ShutdownState { hwnd: HWND, } @@ -48,11 +57,27 @@ unsafe extern "system" fn shutdown_proc( ); } WM_ENDSESSION => { - tauri::async_runtime::block_on(async move { - logging!(info, Type::System, "Session ended, system shutting down."); - feat::clean_async().await; - logging!(info, Type::System, "resolved reset finished"); - }); + if let Some(handler) = SHUTDOWN_HANDLER.get() { + if let Some(Some(rt)) = RUNTIME.get() { + rt.block_on(async { + logging!(info, Type::System, "Session ended, system shutting down."); + handler().await; + logging!(info, Type::System, "resolved reset finished"); + }); + } else { + logging!( + error, + Type::System, + "handle shutdown signal failed, RUNTIME is not available" + ); + } + } else { + logging!( + error, + Type::System, + "WM_ENDSESSION received but no shutdown handler is registered" + ); + } } _ => {} }; @@ -80,8 +105,18 @@ fn get_instance_handle() -> windows_sys::Win32::Foundation::HMODULE { unsafe { &__ImageBase as *const _ as _ } } -pub fn register() { - let app_handle = handle::Handle::app_handle(); +pub fn register(app_handle: &AppHandle, f: F) +where + F: Fn() -> Fut + Send + Sync + 'static, + Fut: Future + Send + 'static, +{ + let _ = SHUTDOWN_HANDLER.set(Box::new(move || { + let fut = (f)(); + Box::pin(async move { + fut.await; + }) as Pin + Send>> + })); + let class_name = encode_wide("global_shutdown_app"); unsafe { let hinstance = get_instance_handle(); diff --git a/clash-verge-rev/crates/clash-verge-types/Cargo.toml b/clash-verge-rev/crates/clash-verge-types/Cargo.toml new file mode 100644 index 0000000000..256a2e7869 --- /dev/null +++ b/clash-verge-rev/crates/clash-verge-types/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "clash-verge-types" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true + +[dependencies] +serde = { workspace = true } +serde_yaml_ng = { workspace = true } +smartstring = { workspace = true } + +[lints] +workspace = true diff --git a/clash-verge-rev/crates/clash-verge-types/src/lib.rs b/clash-verge-rev/crates/clash-verge-types/src/lib.rs new file mode 100644 index 0000000000..ea62c4145d --- /dev/null +++ b/clash-verge-rev/crates/clash-verge-types/src/lib.rs @@ -0,0 +1 @@ +pub mod runtime; diff --git a/clash-verge-rev/crates/clash-verge-types/src/runtime.rs b/clash-verge-rev/crates/clash-verge-types/src/runtime.rs new file mode 100644 index 0000000000..fd0bdf51aa --- /dev/null +++ b/clash-verge-rev/crates/clash-verge-types/src/runtime.rs @@ -0,0 +1,152 @@ +use serde_yaml_ng::{Mapping, Value}; +use smartstring::alias::String; +use std::collections::{HashMap, HashSet}; + +const PATCH_CONFIG_INNER: [&str; 4] = ["allow-lan", "ipv6", "log-level", "unified-delay"]; + +#[derive(Default, Clone)] +pub struct IRuntime { + pub config: Option, + // 记录在订阅中(包括merge和script生成的)出现过的keys + // 这些keys不一定都生效 + pub exists_keys: HashSet, + // TODO 或许可以用 FixMap 来存储以提升效率 + pub chain_logs: HashMap>, +} + +impl IRuntime { + #[inline] + pub fn new() -> Self { + Self::default() + } + + // 这里只更改 allow-lan | ipv6 | log-level | tun + #[inline] + pub fn patch_config(&mut self, patch: &Mapping) { + let config = if let Some(config) = self.config.as_mut() { + config + } else { + return; + }; + + for key in PATCH_CONFIG_INNER.iter() { + if let Some(value) = patch.get(key) { + config.insert((*key).into(), value.clone()); + } + } + + let patch_tun = patch.get("tun"); + if let Some(patch_tun_value) = patch_tun { + let mut tun = config + .get("tun") + .and_then(|val| val.as_mapping()) + .cloned() + .unwrap_or_else(Mapping::new); + + if let Some(patch_tun_mapping) = patch_tun_value.as_mapping() { + for key in use_keys(patch_tun_mapping) { + if let Some(value) = patch_tun_mapping.get(key.as_str()) { + tun.insert(Value::from(key.as_str()), value.clone()); + } + } + } + + config.insert("tun".into(), Value::from(tun)); + } + } + + /// 更新链式代理配置 + /// + /// 该函数更新 `proxies` 和 `proxy-groups` 配置,并处理链式代理的修改或(传入 None )删除。 + /// + /// 配置示例: + /// + /// ```json + /// { + /// "proxies": [ + /// { + /// "name": "入口节点", + /// "type": "xxx", + /// "server": "xxx", + /// "port": "xxx", + /// "ports": "xxx", + /// "password": "xxx", + /// "skip-cert-verify": "xxx" + /// }, + /// { + /// "name": "hop_node_1_xxxx", + /// "type": "xxx", + /// "server": "xxx", + /// "port": "xxx", + /// "ports": "xxx", + /// "password": "xxx", + /// "skip-cert-verify": "xxx", + /// "dialer-proxy": "入口节点" + /// }, + /// { + /// "name": "出口节点", + /// "type": "xxx", + /// "server": "xxx", + /// "port": "xxx", + /// "ports": "xxx", + /// "password": "xxx", + /// "skip-cert-verify": "xxx", + /// "dialer-proxy": "hop_node_1_xxxx" + /// } + /// ], + /// "proxy-groups": [ + /// { + /// "name": "proxy_chain", + /// "type": "select", + /// "proxies": ["出口节点"] + /// } + /// ] + /// } + /// ``` + #[inline] + pub fn update_proxy_chain_config(&mut self, proxy_chain_config: Option) { + let config = if let Some(config) = self.config.as_mut() { + config + } else { + return; + }; + + if let Some(Value::Sequence(proxies)) = config.get_mut("proxies") { + proxies.iter_mut().for_each(|proxy| { + if let Some(proxy) = proxy.as_mapping_mut() + && proxy.get("dialer-proxy").is_some() + { + proxy.remove("dialer-proxy"); + } + }); + } + + if let Some(Value::Sequence(dialer_proxies)) = proxy_chain_config + && let Some(Value::Sequence(proxies)) = config.get_mut("proxies") + { + for (i, dialer_proxy) in dialer_proxies.iter().enumerate() { + if let Some(Value::Mapping(proxy)) = proxies + .iter_mut() + .find(|proxy| proxy.get("name") == Some(dialer_proxy)) + && i != 0 + && let Some(dialer_proxy) = dialer_proxies.get(i - 1) + { + proxy.insert("dialer-proxy".into(), dialer_proxy.to_owned()); + } + } + } + } +} + +// TODO 完整迁移 enhance 行为后移除 +#[inline] +fn use_keys<'a>(config: &'a Mapping) -> impl Iterator + 'a { + config + .iter() + .filter_map(|(key, _)| key.as_str()) + .map(|s: &str| { + let mut s: String = s.into(); + s.make_ascii_lowercase(); + s + }) +} diff --git a/clash-verge-rev/crates/tauri-plugin-clash-verge-sysinfo/Cargo.toml b/clash-verge-rev/crates/tauri-plugin-clash-verge-sysinfo/Cargo.toml new file mode 100644 index 0000000000..2ed474147b --- /dev/null +++ b/clash-verge-rev/crates/tauri-plugin-clash-verge-sysinfo/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "tauri-plugin-clash-verge-sysinfo" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true + +[dependencies] +tauri = { workspace = true } +tauri-plugin-clipboard-manager = { workspace = true } +parking_lot = { workspace = true } +sysinfo = { version = "0.37.2", features = ["network", "system"] } + +[target.'cfg(not(windows))'.dependencies] +libc = "0.2.178" + +[target.'cfg(windows)'.dependencies] +deelevate = { workspace = true } + +[lints] +workspace = true diff --git a/clash-verge-rev/crates/tauri-plugin-clash-verge-sysinfo/src/commands.rs b/clash-verge-rev/crates/tauri-plugin-clash-verge-sysinfo/src/commands.rs new file mode 100644 index 0000000000..47e24a30c2 --- /dev/null +++ b/clash-verge-rev/crates/tauri-plugin-clash-verge-sysinfo/src/commands.rs @@ -0,0 +1,39 @@ +use parking_lot::RwLock; +use tauri::{AppHandle, Runtime, State, command}; +use tauri_plugin_clipboard_manager::{ClipboardExt as _, Error}; + +use crate::Platform; + +// TODO 迁移,让新的结构体允许通过 tauri command 正确使用 structure.field 方式获取信息 +#[command] +pub fn get_system_info(state: State<'_, RwLock>) -> Result { + Ok(state.inner().read().to_string()) +} + +/// 获取应用的运行时间(毫秒) +#[command] +pub fn get_app_uptime(state: State<'_, RwLock>) -> Result { + Ok(state + .inner() + .read() + .appinfo + .app_startup_time + .elapsed() + .as_millis()) +} + +/// 检查应用是否以管理员身份运行 +#[command] +pub fn app_is_admin(state: State<'_, RwLock>) -> Result { + Ok(state.inner().read().appinfo.app_is_admin) +} + +#[command] +pub fn export_diagnostic_info( + app_handle: AppHandle, + state: State<'_, RwLock>, +) -> Result<(), Error> { + let info = state.inner().read().to_string(); + let clipboard = app_handle.clipboard(); + clipboard.write_text(info) +} diff --git a/clash-verge-rev/crates/tauri-plugin-clash-verge-sysinfo/src/lib.rs b/clash-verge-rev/crates/tauri-plugin-clash-verge-sysinfo/src/lib.rs new file mode 100644 index 0000000000..fb1dd97a03 --- /dev/null +++ b/clash-verge-rev/crates/tauri-plugin-clash-verge-sysinfo/src/lib.rs @@ -0,0 +1,165 @@ +use std::{ + fmt::{Debug, Display}, + time::Instant, +}; + +pub mod commands; + +#[cfg(windows)] +use deelevate::{PrivilegeLevel, Token}; +use parking_lot::RwLock; +use sysinfo::{Networks, System}; +use tauri::{ + Manager as _, Runtime, + plugin::{Builder, TauriPlugin}, +}; + +pub struct SysInfo { + system_name: String, + system_version: String, + system_kernel_version: String, + system_arch: String, +} + +impl Default for SysInfo { + #[inline] + fn default() -> Self { + let system_name = System::name().unwrap_or_else(|| "Null".into()); + let system_version = System::long_os_version().unwrap_or_else(|| "Null".into()); + let system_kernel_version = System::kernel_version().unwrap_or_else(|| "Null".into()); + let system_arch = System::cpu_arch(); + Self { + system_name, + system_version, + system_kernel_version, + system_arch, + } + } +} + +pub struct AppInfo { + app_version: String, + app_core_mode: String, + pub app_startup_time: Instant, + pub app_is_admin: bool, +} + +impl Default for AppInfo { + #[inline] + fn default() -> Self { + let app_version = "0.0.0".into(); + let app_core_mode = "NotRunning".into(); + let app_is_admin = false; + let app_startup_time = Instant::now(); + Self { + app_version, + app_core_mode, + app_startup_time, + app_is_admin, + } + } +} + +#[derive(Default)] +pub struct Platform { + pub sysinfo: SysInfo, + pub appinfo: AppInfo, +} + +impl Debug for Platform { + #[inline] + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Platform") + .field("system_name", &self.sysinfo.system_name) + .field("system_version", &self.sysinfo.system_version) + .field("system_kernel_version", &self.sysinfo.system_kernel_version) + .field("system_arch", &self.sysinfo.system_arch) + .field("app_version", &self.appinfo.app_version) + .field("app_core_mode", &self.appinfo.app_core_mode) + .field("app_is_admin", &self.appinfo.app_is_admin) + .finish() + } +} + +impl Display for Platform { + #[inline] + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "System Name: {}\nSystem Version: {}\nSystem kernel Version: {}\nSystem Arch: {}\nVerge Version: {}\nRunning Mode: {}\nIs Admin: {}", + self.sysinfo.system_name, + self.sysinfo.system_version, + self.sysinfo.system_kernel_version, + self.sysinfo.system_arch, + self.appinfo.app_version, + self.appinfo.app_core_mode, + self.appinfo.app_is_admin + ) + } +} + +impl Platform { + #[inline] + fn new() -> Self { + Self::default() + } +} + +#[inline] +fn is_binary_admin() -> bool { + #[cfg(not(windows))] + unsafe { + libc::geteuid() == 0 + } + #[cfg(windows)] + Token::with_current_process() + .and_then(|token| token.privilege_level()) + .map(|level| level != PrivilegeLevel::NotPrivileged) + .unwrap_or(false) +} + +#[inline] +pub fn list_network_interfaces() -> Vec { + let mut networks = Networks::new(); + networks.refresh(false); + networks.keys().map(|name| name.to_owned()).collect() +} + +#[inline] +pub fn set_app_core_mode(app: &tauri::AppHandle, mode: impl Into) { + let platform_spec = app.state::>(); + let mut spec = platform_spec.write(); + spec.appinfo.app_core_mode = mode.into(); +} + +#[inline] +pub fn is_current_app_handle_admin(app: &tauri::AppHandle) -> bool { + let platform_spec = app.state::>(); + let spec = platform_spec.read(); + spec.appinfo.app_is_admin +} + +#[inline] +pub fn init() -> TauriPlugin { + Builder::::new("clash_verge_sysinfo") + // TODO 现在 crate 还不是真正的 tauri 插件,必须由主 lib 自行注册 + // TODO 从 clash-verge 中迁移获取系统信息的 commnand 并实现优雅 structure.field 访问 + // .invoke_handler(tauri::generate_handler![ + // commands::get_system_info, + // commands::get_app_uptime, + // commands::app_is_admin, + // commands::export_diagnostic_info, + // ]) + .setup(move |app, _api| { + let app_version = app.package_info().version.to_string(); + let is_admin = is_binary_admin(); + + let mut platform_spec = Platform::new(); + platform_spec.appinfo.app_version = app_version; + platform_spec.appinfo.app_is_admin = is_admin; + + app.manage(RwLock::new(platform_spec)); + Ok(()) + }) + .build() +} diff --git a/clash-verge-rev/package.json b/clash-verge-rev/package.json index 019bd4a012..fcc08e072f 100644 --- a/clash-verge-rev/package.json +++ b/clash-verge-rev/package.json @@ -24,16 +24,13 @@ "release:autobuild": "pnpm release-version autobuild", "release:deploytest": "pnpm release-version deploytest", "publish-version": "node scripts/publish-version.mjs", - "fmt": "cargo fmt --manifest-path ./src-tauri/Cargo.toml", - "clippy": "cargo clippy --all-features --all-targets --manifest-path ./src-tauri/Cargo.toml", "lint": "eslint -c eslint.config.ts --max-warnings=0 --cache --cache-location .eslintcache src", "lint:fix": "eslint -c eslint.config.ts --max-warnings=0 --cache --cache-location .eslintcache --fix src", "format": "prettier --write .", "format:check": "prettier --check .", "format:i18n": "node scripts/cleanup-unused-i18n.mjs --align --apply", "i18n:types": "node scripts/generate-i18n-keys.mjs", - "typecheck": "tsc --noEmit", - "test": "vitest run" + "typecheck": "tsc --noEmit" }, "dependencies": { "@dnd-kit/core": "^6.3.1", @@ -46,8 +43,9 @@ "@mui/icons-material": "^7.3.5", "@mui/lab": "7.0.0-beta.17", "@mui/material": "^7.3.5", - "@mui/x-data-grid": "^8.18.0", - "@tauri-apps/api": "2.9.0", + "@tanstack/react-table": "^8.21.3", + "@tanstack/react-virtual": "^3.13.12", + "@tauri-apps/api": "2.9.1", "@tauri-apps/plugin-clipboard-manager": "^2.3.2", "@tauri-apps/plugin-dialog": "^2.4.2", "@tauri-apps/plugin-fs": "^2.4.4", @@ -55,39 +53,37 @@ "@tauri-apps/plugin-process": "^2.3.1", "@tauri-apps/plugin-shell": "2.3.3", "@tauri-apps/plugin-updater": "2.9.0", - "@types/json-schema": "^7.0.15", "ahooks": "^3.9.6", "axios": "^1.13.2", "dayjs": "1.11.19", "foxact": "^0.2.49", - "i18next": "^25.6.2", + "i18next": "^25.7.1", "js-yaml": "^4.1.1", - "json-schema": "^0.4.0", "lodash-es": "^4.17.21", - "monaco-editor": "^0.54.0", + "monaco-editor": "^0.55.1", "monaco-yaml": "^5.4.0", "nanoid": "^5.1.6", "react": "19.2.0", "react-dom": "19.2.0", "react-error-boundary": "6.0.0", - "react-hook-form": "^7.66.0", - "react-i18next": "16.3.3", + "react-hook-form": "^7.67.0", + "react-i18next": "16.3.5", "react-markdown": "10.1.0", - "react-router": "^7.9.6", - "react-virtuoso": "^4.14.1", - "swr": "^2.3.6", + "react-router": "^7.10.0", + "react-virtuoso": "^4.16.1", + "swr": "^2.3.7", "tauri-plugin-mihomo-api": "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo#main", "types-pac": "^1.0.3" }, "devDependencies": { "@actions/github": "^6.0.1", - "@eslint-react/eslint-plugin": "^2.3.5", + "@eslint-react/eslint-plugin": "^2.3.11", "@eslint/js": "^9.39.1", - "@tauri-apps/cli": "2.9.4", + "@tauri-apps/cli": "2.9.5", "@types/js-yaml": "^4.0.9", "@types/lodash-es": "^4.17.12", "@types/node": "^24.10.1", - "@types/react": "19.2.4", + "@types/react": "19.2.7", "@types/react-dom": "19.2.3", "@vitejs/plugin-legacy": "^7.2.1", "@vitejs/plugin-react-swc": "^4.2.2", @@ -103,36 +99,31 @@ "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "eslint-plugin-unused-imports": "^4.3.0", - "glob": "^11.0.3", + "glob": "^13.0.0", "globals": "^16.5.0", "https-proxy-agent": "^7.0.6", "husky": "^9.1.7", "jiti": "^2.6.1", - "lint-staged": "^16.2.6", - "meta-json-schema": "^1.19.16", + "lint-staged": "^16.2.7", "node-fetch": "^3.3.2", - "prettier": "^3.6.2", - "sass": "^1.94.0", + "prettier": "^3.7.3", + "sass": "^1.94.2", "tar": "^7.5.2", "terser": "^5.44.1", "typescript": "^5.9.3", - "typescript-eslint": "^8.46.4", - "vite": "^7.2.2", - "vite-plugin-monaco-editor-esm": "^2.0.2", - "vite-plugin-svgr": "^4.5.0", - "vitest": "^4.0.9" + "typescript-eslint": "^8.48.1", + "vite": "^7.2.6", + "vite-plugin-svgr": "^4.5.0" }, "lint-staged": { "*.{ts,tsx,js,jsx}": [ "eslint --fix --max-warnings=0", - "prettier --write", - "git add" + "prettier --write" ], "*.{css,scss,json,md}": [ - "prettier --write", - "git add" + "prettier --write" ] }, "type": "module", - "packageManager": "pnpm@9.13.2" + "packageManager": "pnpm@10.22.0" } diff --git a/clash-verge-rev/pnpm-lock.yaml b/clash-verge-rev/pnpm-lock.yaml index b2b6aeb295..9dde93ccd8 100644 --- a/clash-verge-rev/pnpm-lock.yaml +++ b/clash-verge-rev/pnpm-lock.yaml @@ -19,31 +19,34 @@ importers: version: 3.2.2(react@19.2.0) '@emotion/react': specifier: ^11.14.0 - version: 11.14.0(@types/react@19.2.4)(react@19.2.0) + version: 11.14.0(@types/react@19.2.7)(react@19.2.0) '@emotion/styled': specifier: ^11.14.1 - version: 11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0) + version: 11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0) '@juggle/resize-observer': specifier: ^3.4.0 version: 3.4.0 '@monaco-editor/react': specifier: ^4.7.0 - version: 4.7.0(monaco-editor@0.54.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 4.7.0(monaco-editor@0.55.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@mui/icons-material': specifier: ^7.3.5 - version: 7.3.5(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.4)(react@19.2.0) + version: 7.3.5(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.7)(react@19.2.0) '@mui/lab': specifier: 7.0.0-beta.17 - version: 7.0.0-beta.17(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 7.0.0-beta.17(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0))(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@mui/material': specifier: ^7.3.5 - version: 7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@mui/x-data-grid': - specifier: ^8.18.0 - version: 8.18.0(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@mui/system@7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 7.3.5(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@tanstack/react-table': + specifier: ^8.21.3 + version: 8.21.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@tanstack/react-virtual': + specifier: ^3.13.12 + version: 3.13.12(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@tauri-apps/api': - specifier: 2.9.0 - version: 2.9.0 + specifier: 2.9.1 + version: 2.9.1 '@tauri-apps/plugin-clipboard-manager': specifier: ^2.3.2 version: 2.3.2 @@ -65,9 +68,6 @@ importers: '@tauri-apps/plugin-updater': specifier: 2.9.0 version: 2.9.0 - '@types/json-schema': - specifier: ^7.0.15 - version: 7.0.15 ahooks: specifier: ^3.9.6 version: 3.9.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -81,23 +81,20 @@ importers: specifier: ^0.2.49 version: 0.2.49(react@19.2.0) i18next: - specifier: ^25.6.2 - version: 25.6.2(typescript@5.9.3) + specifier: ^25.7.1 + version: 25.7.1(typescript@5.9.3) js-yaml: specifier: ^4.1.1 version: 4.1.1 - json-schema: - specifier: ^0.4.0 - version: 0.4.0 lodash-es: specifier: ^4.17.21 version: 4.17.21 monaco-editor: - specifier: ^0.54.0 - version: 0.54.0 + specifier: ^0.55.1 + version: 0.55.1 monaco-yaml: specifier: ^5.4.0 - version: 5.4.0(monaco-editor@0.54.0) + version: 5.4.0(monaco-editor@0.55.1) nanoid: specifier: ^5.1.6 version: 5.1.6 @@ -111,26 +108,26 @@ importers: specifier: 6.0.0 version: 6.0.0(react@19.2.0) react-hook-form: - specifier: ^7.66.0 - version: 7.66.0(react@19.2.0) + specifier: ^7.67.0 + version: 7.67.0(react@19.2.0) react-i18next: - specifier: 16.3.3 - version: 16.3.3(i18next@25.6.2(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3) + specifier: 16.3.5 + version: 16.3.5(i18next@25.7.1(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3) react-markdown: specifier: 10.1.0 - version: 10.1.0(@types/react@19.2.4)(react@19.2.0) + version: 10.1.0(@types/react@19.2.7)(react@19.2.0) react-router: - specifier: ^7.9.6 - version: 7.9.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + specifier: ^7.10.0 + version: 7.10.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react-virtuoso: - specifier: ^4.14.1 - version: 4.14.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + specifier: ^4.16.1 + version: 4.16.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) swr: - specifier: ^2.3.6 - version: 2.3.6(react@19.2.0) + specifier: ^2.3.7 + version: 2.3.7(react@19.2.0) tauri-plugin-mihomo-api: specifier: git+https://github.com/clash-verge-rev/tauri-plugin-mihomo#main - version: https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/1e2ada19e20c5504a2a8cef367e87c667806dd65 + version: https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/85fc6b364227e69f5b1cfeb8ca82642c153b5210 types-pac: specifier: ^1.0.3 version: 1.0.3 @@ -139,14 +136,14 @@ importers: specifier: ^6.0.1 version: 6.0.1 '@eslint-react/eslint-plugin': - specifier: ^2.3.5 - version: 2.3.5(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + specifier: ^2.3.11 + version: 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) '@eslint/js': specifier: ^9.39.1 version: 9.39.1 '@tauri-apps/cli': - specifier: 2.9.4 - version: 2.9.4 + specifier: 2.9.5 + version: 2.9.5 '@types/js-yaml': specifier: ^4.0.9 version: 4.0.9 @@ -157,17 +154,17 @@ importers: specifier: ^24.10.1 version: 24.10.1 '@types/react': - specifier: 19.2.4 - version: 19.2.4 + specifier: 19.2.7 + version: 19.2.7 '@types/react-dom': specifier: 19.2.3 - version: 19.2.3(@types/react@19.2.4) + version: 19.2.3(@types/react@19.2.7) '@vitejs/plugin-legacy': specifier: ^7.2.1 - version: 7.2.1(terser@5.44.1)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)) + version: 7.2.1(terser@5.44.1)(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.1)) '@vitejs/plugin-react-swc': specifier: ^4.2.2 - version: 4.2.2(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)) + version: 4.2.2(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.1)) adm-zip: specifier: ^0.5.16 version: 0.5.16 @@ -188,13 +185,13 @@ importers: version: 10.1.8(eslint@9.39.1(jiti@2.6.1)) eslint-import-resolver-typescript: specifier: ^4.4.4 - version: 4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) + version: 4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-import-x: specifier: ^4.16.1 - version: 4.16.1(@typescript-eslint/utils@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)) + version: 4.16.1(@typescript-eslint/utils@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-prettier: specifier: ^5.5.4 - version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.6.2) + version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.7.3) eslint-plugin-react-hooks: specifier: ^7.0.1 version: 7.0.1(eslint@9.39.1(jiti@2.6.1)) @@ -203,10 +200,10 @@ importers: version: 0.4.24(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-unused-imports: specifier: ^4.3.0 - version: 4.3.0(@typescript-eslint/eslint-plugin@8.46.4(@typescript-eslint/parser@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)) + version: 4.3.0(@typescript-eslint/eslint-plugin@8.48.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)) glob: - specifier: ^11.0.3 - version: 11.0.3 + specifier: ^13.0.0 + version: 13.0.0 globals: specifier: ^16.5.0 version: 16.5.0 @@ -220,20 +217,17 @@ importers: specifier: ^2.6.1 version: 2.6.1 lint-staged: - specifier: ^16.2.6 - version: 16.2.6 - meta-json-schema: - specifier: ^1.19.16 - version: 1.19.16 + specifier: ^16.2.7 + version: 16.2.7 node-fetch: specifier: ^3.3.2 version: 3.3.2 prettier: - specifier: ^3.6.2 - version: 3.6.2 + specifier: ^3.7.3 + version: 3.7.3 sass: - specifier: ^1.94.0 - version: 1.94.0 + specifier: ^1.94.2 + version: 1.94.2 tar: specifier: ^7.5.2 version: 7.5.2 @@ -244,20 +238,14 @@ importers: specifier: ^5.9.3 version: 5.9.3 typescript-eslint: - specifier: ^8.46.4 - version: 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + specifier: ^8.48.1 + version: 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) vite: - specifier: ^7.2.2 - version: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1) - vite-plugin-monaco-editor-esm: - specifier: ^2.0.2 - version: 2.0.2(monaco-editor@0.54.0) + specifier: ^7.2.6 + version: 7.2.6(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.1) vite-plugin-svgr: specifier: ^4.5.0 - version: 4.5.0(rollup@4.46.2)(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)) - vitest: - specifier: ^4.0.9 - version: 4.0.9(@types/debug@4.1.12)(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1) + version: 4.5.0(rollup@4.46.2)(typescript@5.9.3)(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.1)) packages: @@ -1010,32 +998,44 @@ packages: resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint-react/ast@2.3.5': - resolution: {integrity: sha512-gTnLEdQ82Kcy2Yn8fLe6ks/yQx1kI3OYuWgYNb4D1XSAOYvL1Cj+UIx2/+ew9vMBLMO3NJr90EMPUr0yVOhC7w==} - engines: {node: '>=20.19.0'} - - '@eslint-react/core@2.3.5': - resolution: {integrity: sha512-6+/3bMmkxIk4vlMwfxw4lU6y7/Z1cjGURPsooAULitbBS4+s0M0N1UjWaPpDwT4FR0SVVqjOp1yUcI66uQvQKg==} - engines: {node: '>=20.19.0'} - - '@eslint-react/eff@2.3.5': - resolution: {integrity: sha512-F2bj6v7Q1hgLn+N28pkJyYvBiTaUFh0qOEz3IXUupkqqnu9zGxmh3P7c0l//8AlR2CvRTCmSVBBhem4BhoSczw==} - engines: {node: '>=20.19.0'} - - '@eslint-react/eslint-plugin@2.3.5': - resolution: {integrity: sha512-5VTcKcbyDNGrpXj3y5wfYKogA8g1aVPcyupSL9/URyxLhnv14tfSNAJ64qTh0NBunETU69n7T81e4ZYJS2ctGw==} + '@eslint-react/ast@2.3.11': + resolution: {integrity: sha512-6DNGjdAf7npXrcJdZk8yzO6dCn5c1jwPCvH98GWBzVOANtlgtF/jaSD7KR+u8gdCfZEs3hwfI7SyWEwOIBWvFA==} engines: {node: '>=20.19.0'} peerDependencies: - eslint: ^9.39.1 - typescript: ^5.9.3 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' - '@eslint-react/shared@2.3.5': - resolution: {integrity: sha512-k65W/X2MeiDX21HPwtcPaFHciYVRYrzE+EZ2ok2BVQWcl24GQUEckAfdMzKQ6cS19OgjQm9k0juHjpUcyHj29g==} + '@eslint-react/core@2.3.11': + resolution: {integrity: sha512-d817I6VmUmH+p+7orrHVfTiFHLcqvwbMQVaMYRN3tAXJukJRqj6vqktfFYdrRm/9MBAxyRy90xuHdHRHwJ1+tg==} + engines: {node: '>=20.19.0'} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@eslint-react/eff@2.3.11': + resolution: {integrity: sha512-nMyqC4aMdPCU2BcHYhPNG43hxRE6ixkEmq7CLFihvqN7R1P94EzS9osYJVYvEvuaae+23D3MonAuTOa4ktVTYA==} engines: {node: '>=20.19.0'} - '@eslint-react/var@2.3.5': - resolution: {integrity: sha512-BDq9o4kUu4h0Lvv29AY+N9LFh69tgICRNDmr5GnRmRFaYZ6/fq+UbO18K47ccb2tj2TI8V6VJFpkPx1fK7lYeQ==} + '@eslint-react/eslint-plugin@2.3.11': + resolution: {integrity: sha512-9zn4+ltrRp0kevI13y2q8ITvnZ4et+CjAyUNJDYsI/eIwWiBEIl6nkmaV67WjtvAlKUbTBQf5uRI/S5M3qNJtg==} engines: {node: '>=20.19.0'} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@eslint-react/shared@2.3.11': + resolution: {integrity: sha512-yvxKELAb0W4N8OeUTSEyY1+Po4kuFwepv0gZeAOX5B2w79nmylEdHGXcIabqr6LEELxRSYGXIHSZ8WbUUJWTnw==} + engines: {node: '>=20.19.0'} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@eslint-react/var@2.3.11': + resolution: {integrity: sha512-vu1Fyruq+qTY2K/tOPPBacr2f/2Eqm0lJBLHMUijGNWZ43YFq63jDSHwAyPUjnadgatVrhVbup2KPfaDxFfYiQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' '@eslint/config-array@0.21.1': resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} @@ -1097,10 +1097,6 @@ packages: resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} engines: {node: 20 || >=22} - '@isaacs/cliui@8.0.2': - resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} - engines: {node: '>=12'} - '@isaacs/fs-minipass@4.0.1': resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} engines: {node: '>=18.0.0'} @@ -1249,50 +1245,9 @@ packages: '@types/react': optional: true - '@mui/x-data-grid@8.18.0': - resolution: {integrity: sha512-g8y5EI3TNqrimHpH/Hv6u6i04cbvsqh39Tg4bZEhGq+SDxWp42iABlUvB7p+gtXfyd+IbmpfzUQ1hOCsHlTMZw==} - engines: {node: '>=14.0.0'} - peerDependencies: - '@emotion/react': ^11.9.0 - '@emotion/styled': ^11.8.1 - '@mui/material': ^5.15.14 || ^6.0.0 || ^7.0.0 - '@mui/system': ^5.15.14 || ^6.0.0 || ^7.0.0 - react: ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - '@emotion/react': - optional: true - '@emotion/styled': - optional: true - - '@mui/x-internals@8.18.0': - resolution: {integrity: sha512-iM2SJALLo4kNqxTel8lfjIymYV9MgTa6021/rAlfdh/vwPMglaKyXQHrxkkWs2Eu/JFKkCKr5Fd34Gsdp63wIg==} - engines: {node: '>=14.0.0'} - peerDependencies: - react: ^17.0.0 || ^18.0.0 || ^19.0.0 - - '@mui/x-virtualizer@0.2.8': - resolution: {integrity: sha512-hCkhTg3BLLbf0SIw9Cx/NHTCUmbna+P5F2V+Bcv/9XiYhfzzmhYnm68+V6vOOhKVbV3j8JKsUEqcTC9K2Jpu8A==} - engines: {node: '>=14.0.0'} - peerDependencies: - react: ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 - '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} - '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - - '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - - '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - '@octokit/auth-token@4.0.0': resolution: {integrity: sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==} engines: {node: '>= 18'} @@ -1545,9 +1500,6 @@ packages: '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} - '@standard-schema/spec@1.0.0': - resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} - '@svgr/babel-plugin-add-jsx-attribute@8.0.0': resolution: {integrity: sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==} engines: {node: '>=14'} @@ -1691,77 +1643,97 @@ packages: '@swc/types@0.1.25': resolution: {integrity: sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==} - '@tauri-apps/api@2.9.0': - resolution: {integrity: sha512-qD5tMjh7utwBk9/5PrTA/aGr3i5QaJ/Mlt7p8NilQ45WgbifUNPyKWsA63iQ8YfQq6R8ajMapU+/Q8nMcPRLNw==} + '@tanstack/react-table@8.21.3': + resolution: {integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==} + engines: {node: '>=12'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' - '@tauri-apps/cli-darwin-arm64@2.9.4': - resolution: {integrity: sha512-9rHkMVtbMhe0AliVbrGpzMahOBg3rwV46JYRELxR9SN6iu1dvPOaMaiC4cP6M/aD1424ziXnnMdYU06RAH8oIw==} + '@tanstack/react-virtual@3.13.12': + resolution: {integrity: sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tanstack/table-core@8.21.3': + resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} + engines: {node: '>=12'} + + '@tanstack/virtual-core@3.13.12': + resolution: {integrity: sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==} + + '@tauri-apps/api@2.9.1': + resolution: {integrity: sha512-IGlhP6EivjXHepbBic618GOmiWe4URJiIeZFlB7x3czM0yDHHYviH1Xvoiv4FefdkQtn6v7TuwWCRfOGdnVUGw==} + + '@tauri-apps/cli-darwin-arm64@2.9.5': + resolution: {integrity: sha512-P5XDyCwq3VbWGAplyfP/bgmuUITVDcypxgZUyX45SM7HbU1Nrkk0cNK1HCOkuNBAVVbWen2GUNWah/AiupHHXg==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@tauri-apps/cli-darwin-x64@2.9.4': - resolution: {integrity: sha512-VT9ymNuT06f5TLjCZW2hfSxbVtZDhORk7CDUDYiq5TiSYQdxkl8MVBy0CCFFcOk4QAkUmqmVUA9r3YZ/N/vPRQ==} + '@tauri-apps/cli-darwin-x64@2.9.5': + resolution: {integrity: sha512-JC9UfQ2ZKavx60dnNxsWztRF3oUH3dgPwN1WJ3/5RUy2aNwD/vXqvJAfNFZ4GWeQpoQ+PqJxduev0U4OMQonnA==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@tauri-apps/cli-linux-arm-gnueabihf@2.9.4': - resolution: {integrity: sha512-tTWkEPig+2z3Rk0zqZYfjUYcgD+aSm72wdrIhdYobxbQZOBw0zfn50YtWv+av7bm0SHvv75f0l7JuwgZM1HFow==} + '@tauri-apps/cli-linux-arm-gnueabihf@2.9.5': + resolution: {integrity: sha512-iCQm2Uvx8AheghfG/QUv1y8Ga9yquJt6xJwH1uF0x5KfmJmwBi8pHBvB924dDi59PS84qTdIBeJejQT00QX3Iw==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@tauri-apps/cli-linux-arm64-gnu@2.9.4': - resolution: {integrity: sha512-ql6vJ611qoqRYHxkKPnb2vHa27U+YRKRmIpLMMBeZnfFtZ938eao7402AQCH1mO2+/8ioUhbpy9R/ZcLTXVmkg==} + '@tauri-apps/cli-linux-arm64-gnu@2.9.5': + resolution: {integrity: sha512-b6AW8Gr5nQOQIYH0TsUev7rEThGHIvsx192eElOmOz/dh33J4pninHK32laMj2hzHMJ27qmDq5vANL+wrFo9sg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tauri-apps/cli-linux-arm64-musl@2.9.4': - resolution: {integrity: sha512-vg7yNn7ICTi6hRrcA/6ff2UpZQP7un3xe3SEld5QM0prgridbKAiXGaCKr3BnUBx/rGXegQlD/wiLcWdiiraSw==} + '@tauri-apps/cli-linux-arm64-musl@2.9.5': + resolution: {integrity: sha512-/gRBMnphS9E8riZ0LIbBhZ9Oy16A2rx/g3DGR0DcDBvUtkLfbL0lMu4s+sY85nkn9An15+cZ1ZK6d7AIqWahLA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tauri-apps/cli-linux-riscv64-gnu@2.9.4': - resolution: {integrity: sha512-l8L+3VxNk6yv5T/Z/gv5ysngmIpsai40B9p6NQQyqYqxImqYX37pqREoEBl1YwG7szGnDibpWhidPrWKR59OJA==} + '@tauri-apps/cli-linux-riscv64-gnu@2.9.5': + resolution: {integrity: sha512-NOzjPF9YIBodjdkFcJmqINT0k3YDoR5ANM/jg6Z6s3Zmk8ScN6inI60jTxcfgfWyITiKsPy7GJyYou3Cm2XNzw==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] - '@tauri-apps/cli-linux-x64-gnu@2.9.4': - resolution: {integrity: sha512-PepPhCXc/xVvE3foykNho46OmCyx47E/aG676vKTVp+mqin5d+IBqDL6wDKiGNT5OTTxKEyNlCQ81Xs2BQhhqA==} + '@tauri-apps/cli-linux-x64-gnu@2.9.5': + resolution: {integrity: sha512-SfGbwgvTphM5y+J91NyU/psleMUlyyPkZyDCFg8WU1HX8DpKUT3Vwhb/W1xpUBGb56tJgGCO46FCVkr8w4Areg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tauri-apps/cli-linux-x64-musl@2.9.4': - resolution: {integrity: sha512-zcd1QVffh5tZs1u1SCKUV/V7RRynebgYUNWHuV0FsIF1MjnULUChEXhAhug7usCDq4GZReMJOoXa6rukEozWIw==} + '@tauri-apps/cli-linux-x64-musl@2.9.5': + resolution: {integrity: sha512-ZfeoiASAOGDzyvN+TDAg8A1pCeS082h4uc0vZKvtWUN+9QBIMfz0yJwltAv+SN/afap6NS6DVkbPV3UVuI9V5A==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tauri-apps/cli-win32-arm64-msvc@2.9.4': - resolution: {integrity: sha512-/7ZhnP6PY04bEob23q8MH/EoDISdmR1wuNm0k9d5HV7TDMd2GGCDa8dPXA4vJuglJKXIfXqxFmZ4L+J+MO42+w==} + '@tauri-apps/cli-win32-arm64-msvc@2.9.5': + resolution: {integrity: sha512-ulg7irow+ekjaK4inFHVq7m1KQebDSYNb17DFKV+h+x7qnLZymz2gHK7df2u4YyEjqvzwRd3AJpU3HNxRurSFQ==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@tauri-apps/cli-win32-ia32-msvc@2.9.4': - resolution: {integrity: sha512-1LmAfaC4Cq+3O1Ir1ksdhczhdtFSTIV51tbAGtbV/mr348O+M52A/xwCCXQank0OcdBxy5BctqkMtuZnQvA8uQ==} + '@tauri-apps/cli-win32-ia32-msvc@2.9.5': + resolution: {integrity: sha512-6lF0k/Qduhn1Z3IOXlp2ts8jNOMIX4cK4Fbk3axGeX7LMcVVbOSEAFwbTqS8BKZDFac0WRS8N1C96+Ms5LOS1Q==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] - '@tauri-apps/cli-win32-x64-msvc@2.9.4': - resolution: {integrity: sha512-EdYd4c9wGvtPB95kqtEyY+bUR+k4kRw3IA30mAQ1jPH6z57AftT8q84qwv0RDp6kkEqOBKxeInKfqi4BESYuqg==} + '@tauri-apps/cli-win32-x64-msvc@2.9.5': + resolution: {integrity: sha512-Vg50U74x1A4b2iBVtDcAVPbI1XVuzSmwlduuBM1VewxtRaVj5GDzWnYtBcnuIk+VGzNApRDfDhraAXGaW2a/Gw==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@tauri-apps/cli@2.9.4': - resolution: {integrity: sha512-pvylWC9QckrOS9ATWXIXcgu7g2hKK5xTL5ZQyZU/U0n9l88SEFGcWgLQNa8WZmd+wWIOWhkxOFcOl3i6ubDNNw==} + '@tauri-apps/cli@2.9.5': + resolution: {integrity: sha512-z88tX6O6kwTgMjYozhNGbehzQyBazgXejyH784CwSfBOWm06xFcogd0PY/jhcPsqzJF9kLRIkmlQy+cqdrioOQ==} engines: {node: '>= 10'} hasBin: true @@ -1789,15 +1761,9 @@ packages: '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} - '@types/chai@5.2.2': - resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} - '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} - '@types/deep-eql@4.0.2': - resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} - '@types/estree-jsx@1.0.5': resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} @@ -1850,8 +1816,11 @@ packages: peerDependencies: '@types/react': '*' - '@types/react@19.2.4': - resolution: {integrity: sha512-tBFxBp9Nfyy5rsmefN+WXc1JeW/j2BpBHFdLZbEVfs9wn3E3NRFxwV0pJg8M1qQAexFpvz73hJXFofV0ZAu92A==} + '@types/react@19.2.7': + resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -1859,63 +1828,63 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} - '@typescript-eslint/eslint-plugin@8.46.4': - resolution: {integrity: sha512-R48VhmTJqplNyDxCyqqVkFSZIx1qX6PzwqgcXn1olLrzxcSBDlOsbtcnQuQhNtnNiJ4Xe5gREI1foajYaYU2Vg==} + '@typescript-eslint/eslint-plugin@8.48.1': + resolution: {integrity: sha512-X63hI1bxl5ohelzr0LY5coufyl0LJNthld+abwxpCoo6Gq+hSqhKwci7MUWkXo67mzgUK6YFByhmaHmUcuBJmA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.46.4 + '@typescript-eslint/parser': ^8.48.1 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.46.4': - resolution: {integrity: sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==} + '@typescript-eslint/parser@8.48.1': + resolution: {integrity: sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.46.4': - resolution: {integrity: sha512-nPiRSKuvtTN+no/2N1kt2tUh/HoFzeEgOm9fQ6XQk4/ApGqjx0zFIIaLJ6wooR1HIoozvj2j6vTi/1fgAz7UYQ==} + '@typescript-eslint/project-service@8.48.1': + resolution: {integrity: sha512-HQWSicah4s9z2/HifRPQ6b6R7G+SBx64JlFQpgSSHWPKdvCZX57XCbszg/bapbRsOEv42q5tayTYcEFpACcX1w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@8.46.4': - resolution: {integrity: sha512-tMDbLGXb1wC+McN1M6QeDx7P7c0UWO5z9CXqp7J8E+xGcJuUuevWKxuG8j41FoweS3+L41SkyKKkia16jpX7CA==} + '@typescript-eslint/scope-manager@8.48.1': + resolution: {integrity: sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.46.4': - resolution: {integrity: sha512-+/XqaZPIAk6Cjg7NWgSGe27X4zMGqrFqZ8atJsX3CWxH/jACqWnrWI68h7nHQld0y+k9eTTjb9r+KU4twLoo9A==} + '@typescript-eslint/tsconfig-utils@8.48.1': + resolution: {integrity: sha512-k0Jhs4CpEffIBm6wPaCXBAD7jxBtrHjrSgtfCjUvPp9AZ78lXKdTR8fxyZO5y4vWNlOvYXRtngSZNSn+H53Jkw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.46.4': - resolution: {integrity: sha512-V4QC8h3fdT5Wro6vANk6eojqfbv5bpwHuMsBcJUJkqs2z5XnYhJzyz9Y02eUmF9u3PgXEUiOt4w4KHR3P+z0PQ==} + '@typescript-eslint/type-utils@8.48.1': + resolution: {integrity: sha512-1jEop81a3LrJQLTf/1VfPQdhIY4PlGDBc/i67EVWObrtvcziysbLN3oReexHOM6N3jyXgCrkBsZpqwH0hiDOQg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.46.4': - resolution: {integrity: sha512-USjyxm3gQEePdUwJBFjjGNG18xY9A2grDVGuk7/9AkjIF1L+ZrVnwR5VAU5JXtUnBL/Nwt3H31KlRDaksnM7/w==} + '@typescript-eslint/types@8.48.1': + resolution: {integrity: sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.46.4': - resolution: {integrity: sha512-7oV2qEOr1d4NWNmpXLR35LvCfOkTNymY9oyW+lUHkmCno7aOmIf/hMaydnJBUTBMRCOGZh8YjkFOc8dadEoNGA==} + '@typescript-eslint/typescript-estree@8.48.1': + resolution: {integrity: sha512-/9wQ4PqaefTK6POVTjJaYS0bynCgzh6ClJHGSBj06XEHjkfylzB+A3qvyaXnErEZSaxhIo4YdyBgq6j4RysxDg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.46.4': - resolution: {integrity: sha512-AbSv11fklGXV6T28dp2Me04Uw90R2iJ30g2bgLz529Koehrmkbs1r7paFqr1vPCZi7hHwYxYtxfyQMRC8QaVSg==} + '@typescript-eslint/utils@8.48.1': + resolution: {integrity: sha512-fAnhLrDjiVfey5wwFRwrweyRlCmdz5ZxXz2G/4cLn0YDLjTapmN4gcCsTBR1N2rWnZSDeWpYtgLDsJt+FpmcwA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@8.46.4': - resolution: {integrity: sha512-/++5CYLQqsO9HFGLI7APrxBJYo+5OCMpViuhV8q5/Qa3o5mMrF//eQHks+PXcsAVaLdn817fMuS7zqoXNNZGaw==} + '@typescript-eslint/visitor-keys@8.48.1': + resolution: {integrity: sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@ungap/structured-clone@1.3.0': @@ -2029,35 +1998,6 @@ packages: peerDependencies: vite: ^4 || ^5 || ^6 || ^7 - '@vitest/expect@4.0.9': - resolution: {integrity: sha512-C2vyXf5/Jfj1vl4DQYxjib3jzyuswMi/KHHVN2z+H4v16hdJ7jMZ0OGe3uOVIt6LyJsAofDdaJNIFEpQcrSTFw==} - - '@vitest/mocker@4.0.9': - resolution: {integrity: sha512-PUyaowQFHW+9FKb4dsvvBM4o025rWMlEDXdWRxIOilGaHREYTi5Q2Rt9VCgXgPy/hHZu1LeuXtrA/GdzOatP2g==} - peerDependencies: - msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0-0 - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true - - '@vitest/pretty-format@4.0.9': - resolution: {integrity: sha512-Hor0IBTwEi/uZqB7pvGepyElaM8J75pYjrrqbC8ZYMB9/4n5QA63KC15xhT+sqHpdGWfdnPo96E8lQUxs2YzSQ==} - - '@vitest/runner@4.0.9': - resolution: {integrity: sha512-aF77tsXdEvIJRkj9uJZnHtovsVIx22Ambft9HudC+XuG/on1NY/bf5dlDti1N35eJT+QZLb4RF/5dTIG18s98w==} - - '@vitest/snapshot@4.0.9': - resolution: {integrity: sha512-r1qR4oYstPbnOjg0Vgd3E8ADJbi4ditCzqr+Z9foUrRhIy778BleNyZMeAJ2EjV+r4ASAaDsdciC9ryMy8xMMg==} - - '@vitest/spy@4.0.9': - resolution: {integrity: sha512-J9Ttsq0hDXmxmT8CUOWUr1cqqAj2FJRGTdyEjSR+NjoOGKEqkEWj+09yC0HhI8t1W6t4Ctqawl1onHgipJve1A==} - - '@vitest/utils@4.0.9': - resolution: {integrity: sha512-cEol6ygTzY4rUPvNZM19sDf7zGa35IYTm9wfzkHoT/f5jX10IOY7QleWSOh5T0e3I3WVozwK5Asom79qW8DiuQ==} - acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -2089,10 +2029,6 @@ packages: resolution: {integrity: sha512-Zhl0ErHcSRUaVfGUeUdDuLgpkEo8KIFjB4Y9uAc46ScOpdDiU1Dbyplh7qWJeJ/ZHpbyMSM26+X3BySgnIz40Q==} engines: {node: '>=18'} - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - ansi-regex@6.1.0: resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} engines: {node: '>=12'} @@ -2228,10 +2164,6 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} - chai@6.2.0: - resolution: {integrity: sha512-aUTnJc/JipRzJrNADXVvpVqi6CO0dn3nx4EVPxijri+fj3LUUDyZQOgVeW54Ob3Y1Xh9Iz8f+CgaCl8v0mn9bA==} - engines: {node: '>=18'} - chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -2347,8 +2279,8 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} - csstype@3.1.3: - resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} d@1.0.2: resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} @@ -2430,8 +2362,8 @@ packages: dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} - dompurify@3.1.7: - resolution: {integrity: sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==} + dompurify@3.2.7: + resolution: {integrity: sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==} dot-case@3.0.4: resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} @@ -2440,21 +2372,12 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} - eastasianwidth@0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - electron-to-chromium@1.5.173: resolution: {integrity: sha512-2bFhXP2zqSfQHugjqJIDFVwa+qIxyNApenmXTp9EjaKtdPrES5Qcn9/aSFy/NaP2E+fWG/zxKu/LBvY36p5VNQ==} emoji-regex@10.5.0: resolution: {integrity: sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==} - emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - - emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -2478,9 +2401,6 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} - es-module-lexer@1.7.0: - resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} - es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -2613,19 +2533,19 @@ packages: eslint-config-prettier: optional: true - eslint-plugin-react-dom@2.3.5: - resolution: {integrity: sha512-SsIF5HbsXLJcbEoFbzgabqA7DOnfGd0BhD7QzZd5tqgz4gL2j2mUGCBbQjQIE0BMbKtOihbhuceQfQ/QxoJJIg==} + eslint-plugin-react-dom@2.3.11: + resolution: {integrity: sha512-++1BN87Nn7smgD1iZiQXRUE3ypcMXo7FeD+hTcL4d3aa32ur6vF30xkPmSfBs+mPCuGygNnRN8uvSWtkkWCa5w==} engines: {node: '>=20.19.0'} peerDependencies: - eslint: ^9.39.1 - typescript: ^5.9.3 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' - eslint-plugin-react-hooks-extra@2.3.5: - resolution: {integrity: sha512-IxPs6O/XCpm8FAv38TyJKcHkeS/qNb97PdbH1OqHbf4BAT/QTInWweNEpePiyydQ0YuLvHqTo1dreY8Jj6Re3A==} + eslint-plugin-react-hooks-extra@2.3.11: + resolution: {integrity: sha512-bBxCWIrt0cKKCIa8wj36EYnt1Hw3Z93yCPph3FEmzQTClDTJkCoQQKhTTuofMHvAVRSeYG0TJnCtNW34XwQs0Q==} engines: {node: '>=20.0.0'} peerDependencies: - eslint: ^9.39.1 - typescript: ^5.9.3 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' eslint-plugin-react-hooks@7.0.1: resolution: {integrity: sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==} @@ -2633,31 +2553,31 @@ packages: peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 - eslint-plugin-react-naming-convention@2.3.5: - resolution: {integrity: sha512-sjKvdJq90HWNYRBtwia7C/N8NXdg+k8O7ikQqf6QsOuTUHGLgFWGtxx1AktfizlSusCdb96w5LJ4MSi+KsuVZg==} + eslint-plugin-react-naming-convention@2.3.11: + resolution: {integrity: sha512-fN/EjnmMEbv6EADSyNZcs3KjPdWEI5A6i1rQqDk93KQhITXWNMbXHy/bn2a4Q3ZIfgWtJbwVE7RhcSOqJGFwVA==} engines: {node: '>=20.19.0'} peerDependencies: - eslint: ^9.39.1 - typescript: ^5.9.3 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' eslint-plugin-react-refresh@0.4.24: resolution: {integrity: sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==} peerDependencies: eslint: '>=8.40' - eslint-plugin-react-web-api@2.3.5: - resolution: {integrity: sha512-wY/hNWQxshTZ2niuu8QcARQuDg5w+cEA2OYtnrnPDjhy0qxikAaYA4NUx7HTAXoMC1Kxl78+NbQBBXnlwoMAZA==} + eslint-plugin-react-web-api@2.3.11: + resolution: {integrity: sha512-YU1WMWOpJNKVi23Mn+4C508u1ueo+eggAZToAAGIe31BCRiJpAw3YpOJEIPmkUMYhqNU46zB5KhhpAKwb8oj1w==} engines: {node: '>=20.19.0'} peerDependencies: - eslint: ^9.39.1 - typescript: ^5.9.3 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' - eslint-plugin-react-x@2.3.5: - resolution: {integrity: sha512-Yj+6e2ds6Gg3KRPgNdifincu3cuxDYPcboCXc5EGHC//6JZXRgtqQ3N5uP9RVHnCHmKF2EiZ76XyPDnp4hMgEg==} + eslint-plugin-react-x@2.3.11: + resolution: {integrity: sha512-MJ3kFsrP1CVICXhzlBPVLnO/2f/FXZMIwRMsCDMwm+liIzZ+WZqvdOBJbb/F7UBKVD2LIvrr51wWJIVJpBFbtg==} engines: {node: '>=20.19.0'} peerDependencies: - eslint: ^9.39.1 - typescript: ^5.9.3 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' eslint-plugin-unused-imports@4.3.0: resolution: {integrity: sha512-ZFBmXMGBYfHttdRtOG9nFFpmUvMtbHSjsKrS20vdWdbfiVYsO3yA2SGYy9i9XmZJDfMGBflZGBCm70SEnFQtOA==} @@ -2716,9 +2636,6 @@ packages: estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} - estree-walker@3.0.3: - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -2729,10 +2646,6 @@ packages: eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} - expect-type@1.2.2: - resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} - engines: {node: '>=12.0.0'} - ext@1.7.0: resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} @@ -2745,19 +2658,12 @@ packages: fast-diff@1.3.0: resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} - fast-glob@3.3.3: - resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} - engines: {node: '>=8.6.0'} - fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - fastq@1.19.1: - resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} - fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -2806,10 +2712,6 @@ packages: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} - foreground-child@3.3.1: - resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} - engines: {node: '>=14'} - form-data@4.0.4: resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} engines: {node: '>= 6'} @@ -2864,18 +2766,13 @@ packages: get-tsconfig@4.10.1: resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} - glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} - glob@11.0.3: - resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==} + glob@13.0.0: + resolution: {integrity: sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==} engines: {node: 20 || >=22} - hasBin: true globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} @@ -2953,8 +2850,8 @@ packages: engines: {node: '>=18'} hasBin: true - i18next@25.6.2: - resolution: {integrity: sha512-0GawNyVUw0yvJoOEBq1VHMAsqdM23XrHkMtl2gKEjviJQSLVXsrPqsoYAxBEugW5AB96I2pZkwRxyl8WZVoWdw==} + i18next@25.7.1: + resolution: {integrity: sha512-XbTnkh1yCZWSAZGnA9xcQfHcYNgZs2cNxm+c6v1Ma9UAUGCeJPplRe1ILia6xnDvXBjk0uXU+Z8FYWhA19SKFw==} peerDependencies: typescript: ^5 peerDependenciesMeta: @@ -3046,10 +2943,6 @@ packages: resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} engines: {node: '>= 0.4'} - is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - is-fullwidth-code-point@5.1.0: resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} engines: {node: '>=18'} @@ -3136,10 +3029,6 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - jackspeak@4.1.1: - resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==} - engines: {node: 20 || >=22} - jiti@2.6.1: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true @@ -3174,9 +3063,6 @@ packages: json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - json-schema@0.4.0: - resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} - json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -3202,8 +3088,8 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - lint-staged@16.2.6: - resolution: {integrity: sha512-s1gphtDbV4bmW1eylXpVMk2u7is7YsrLl8hzrtvC70h4ByhcMLZFY01Fx05ZUDNuv1H8HO4E+e2zgejV1jVwNw==} + lint-staged@16.2.7: + resolution: {integrity: sha512-lDIj4RnYmK7/kXMya+qJsmkRFkGolciXjrsZ6PC25GdTfWOAWetR0ZbsNXRAj1EHHImRSalc+whZFg56F5DVow==} engines: {node: '>=20.17'} hasBin: true @@ -3295,14 +3181,6 @@ packages: resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==} engines: {node: '>=18'} - merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - - meta-json-schema@1.19.16: - resolution: {integrity: sha512-Py3XR3VRXs3tAMg3sy7fmex8IU4p4FTxVbF86WTtssWpFcSNbBUjk0QjpdhGrh+9qPMSwCJY1drXnvgDq9XQ7Q==} - engines: {node: '>=18', pnpm: '>=9'} - micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} @@ -3382,8 +3260,8 @@ packages: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} - minimatch@10.0.3: - resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==} + minimatch@10.1.1: + resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} engines: {node: 20 || >=22} minimatch@3.1.2: @@ -3404,8 +3282,8 @@ packages: resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} engines: {node: '>= 18'} - monaco-editor@0.54.0: - resolution: {integrity: sha512-hx45SEUoLatgWxHKCmlLJH81xBo0uXP4sRkESUpmDQevfi+e7K1VuiSprK6UpQ8u4zOcKNiH0pMvHvlMWA/4cw==} + monaco-editor@0.55.1: + resolution: {integrity: sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==} monaco-languageserver-types@0.4.0: resolution: {integrity: sha512-QQ3BZiU5LYkJElGncSNb5AKoJ/LCs6YBMCJMAz9EA7v+JaOdn3kx2cXpPTcZfKA5AEsR0vc97sAw+5mdNhVBmw==} @@ -3523,9 +3401,6 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} - package-json-from-dist@1.0.1: - resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} - parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -3559,9 +3434,6 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} - pathe@2.0.3: - resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -3594,8 +3466,8 @@ packages: resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} engines: {node: '>=6.0.0'} - prettier@3.6.2: - resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + prettier@3.7.3: + resolution: {integrity: sha512-QgODejq9K3OzoBbuyobZlUhznP5SKwPqp+6Q6xw6o8gnhr4O85L2U915iM2IDcfF2NPXVaM9zlo9tdwipnYwzg==} engines: {node: '>=14'} hasBin: true @@ -3612,9 +3484,6 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - react-dom@19.2.0: resolution: {integrity: sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==} peerDependencies: @@ -3628,14 +3497,14 @@ packages: react-fast-compare@3.2.2: resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} - react-hook-form@7.66.0: - resolution: {integrity: sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw==} + react-hook-form@7.67.0: + resolution: {integrity: sha512-E55EOwKJHHIT/I6J9DmQbCWToAYSw9nN5R57MZw9rMtjh+YQreMDxRLfdjfxQbiJ3/qbg3Z02wGzBX4M+5fMtQ==} engines: {node: '>=18.0.0'} peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 - react-i18next@16.3.3: - resolution: {integrity: sha512-IaY2W+ueVd/fe7H6Wj2S4bTuLNChnajFUlZFfCTrTHWzGcOrUHlVzW55oXRSl+J51U8Onn6EvIhQ+Bar9FUcjw==} + react-i18next@16.3.5: + resolution: {integrity: sha512-F7Kglc+T0aE6W2rO5eCAFBEuWRpNb5IFmXOYEgztjZEuiuSLTe/xBIEG6Q3S0fbl8GXMNo+Q7gF8bpokFNWJww==} peerDependencies: i18next: '>= 25.6.2' react: '>= 16.8.0' @@ -3662,8 +3531,8 @@ packages: '@types/react': '>=18' react: '>=18' - react-router@7.9.6: - resolution: {integrity: sha512-Y1tUp8clYRXpfPITyuifmSoE2vncSME18uVLgaqyxh9H35JWpIfzHo+9y3Fzh5odk/jxPW29IgLgzcdwxGqyNA==} + react-router@7.10.0: + resolution: {integrity: sha512-FVyCOH4IZ0eDDRycODfUqoN8ZSR2LbTvtx6RPsBgzvJ8xAXlMZNCrOFpu+jb8QbtZnpAd/cEki2pwE848pNGxw==} engines: {node: '>=20.0.0'} peerDependencies: react: '>=18' @@ -3678,8 +3547,8 @@ packages: react: '>=16.6.0' react-dom: '>=16.6.0' - react-virtuoso@4.14.1: - resolution: {integrity: sha512-NRUF1ak8lY+Tvc6WN9cce59gU+lilzVtOozP+pm9J7iHshLGGjsiAB4rB2qlBPHjFbcXOQpT+7womNHGDUql8w==} + react-virtuoso@4.16.1: + resolution: {integrity: sha512-V9ZDw7TFspJb02gNWqHyVZvaMaCFaoL30F/tOVepCI12kdLjA2oxFfWvNC66AVJdOH5cwiq8317p2Q9OpG+TDw==} peerDependencies: react: '>=16 || >=17 || >= 18 || >= 19' react-dom: '>=16 || >=17 || >= 18 || >=19' @@ -3727,9 +3596,6 @@ packages: remark-rehype@11.1.2: resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} - reselect@5.1.1: - resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} - resize-observer-polyfill@1.5.1: resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} @@ -3749,10 +3615,6 @@ packages: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} - reusify@1.1.0: - resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} @@ -3761,9 +3623,6 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - safe-array-concat@1.1.3: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} @@ -3776,8 +3635,8 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} - sass@1.94.0: - resolution: {integrity: sha512-Dqh7SiYcaFtdv5Wvku6QgS5IGPm281L+ZtVD1U2FJa7Q0EFRlq8Z3sjYtz6gYObsYThUOz9ArwFqPZx+1azILQ==} + sass@1.94.2: + resolution: {integrity: sha512-N+7WK20/wOr7CzA2snJcUSSNTCzeCGUTFY3OgeQP3mZ1aj9NMQ0mSTXwlrnd89j33zzQJGqIN52GIOmYrfq46A==} engines: {node: '>=14.0.0'} hasBin: true @@ -3839,9 +3698,6 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} - siginfo@2.0.0: - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -3875,15 +3731,9 @@ packages: resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==} engines: {node: '>=12.0.0'} - stackback@0.0.2: - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - state-local@1.0.7: resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} - std-env@3.10.0: - resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} - stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} @@ -3892,16 +3742,8 @@ packages: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} - string-ts@2.2.1: - resolution: {integrity: sha512-Q2u0gko67PLLhbte5HmPfdOjNvUKbKQM+mCNQae6jE91DmoFHY6HH9GcdqCeNx87DZ2KKjiFxmA0R/42OneGWw==} - - string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - - string-width@5.1.2: - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} - engines: {node: '>=12'} + string-ts@2.3.1: + resolution: {integrity: sha512-xSJq+BS52SaFFAVxuStmx6n5aYZU571uYUnUrPXkPFCfdHyZMMlbP2v2Wx5sNBnAVzq/2+0+mcBLBa3Xa5ubYw==} string-width@7.2.0: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} @@ -3926,10 +3768,6 @@ packages: stringify-entities@4.0.4: resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - strip-ansi@7.1.0: resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} engines: {node: '>=12'} @@ -3962,8 +3800,8 @@ packages: svg-parser@2.0.4: resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==} - swr@2.3.6: - resolution: {integrity: sha512-wfHRmHWk/isGNMwlLGlZX5Gzz/uTgo0o2IRuTMcf4CPuPFJZlq0rDaKUx+ozB5nBOReNV1kiOyzMfj+MBMikLw==} + swr@2.3.7: + resolution: {integrity: sha512-ZEquQ82QvalqTxhBVv/DlAg2mbmUjF4UgpPg9wwk4ufb9rQnZXh1iKyyKBqV6bQGu1Ie7L1QwSYO07qFIa1p+g==} peerDependencies: react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -3978,8 +3816,8 @@ packages: resolution: {integrity: sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==} engines: {node: '>=18'} - tauri-plugin-mihomo-api@https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/1e2ada19e20c5504a2a8cef367e87c667806dd65: - resolution: {tarball: https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/1e2ada19e20c5504a2a8cef367e87c667806dd65} + tauri-plugin-mihomo-api@https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/85fc6b364227e69f5b1cfeb8ca82642c153b5210: + resolution: {tarball: https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/85fc6b364227e69f5b1cfeb8ca82642c153b5210} version: 0.1.0 terser@5.44.1: @@ -3991,20 +3829,10 @@ packages: resolution: {integrity: sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==} engines: {node: '>=0.12'} - tinybench@2.9.0: - resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - - tinyexec@0.3.2: - resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} - tinyrainbow@3.0.3: - resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} - engines: {node: '>=14.0.0'} - to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -4065,8 +3893,8 @@ packages: types-pac@1.0.3: resolution: {integrity: sha512-MF2UAZGvGMOM+vHi9Zj/LvQqdNN1m1xSB+PjAW9B/GvFqaB4GwR18YaIbGIGDRTW/J8iqFXQHLZd5eJVtho46w==} - typescript-eslint@8.46.4: - resolution: {integrity: sha512-KALyxkpYV5Ix7UhvjTwJXZv76VWsHG+NjNlt/z+a17SOQSiOcBdUXdbJdyXi7RPxrBFECtFOiPwUJQusJuCqrg==} + typescript-eslint@8.48.1: + resolution: {integrity: sha512-FbOKN1fqNoXp1hIl5KYpObVrp0mCn+CLgn479nmu2IsRMrx2vyv74MmsBLVlhg8qVwNFGbXSp8fh1zp8pEoC2A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -4148,18 +3976,13 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - vite-plugin-monaco-editor-esm@2.0.2: - resolution: {integrity: sha512-XVkOpL/r0rw1NpbO30vUwG4S0THkC9KB1vjjV8olGd49h4/EQsKl3DrxB6KRDwyZNC9mKiiZgk2L6njUYj3oKQ==} - peerDependencies: - monaco-editor: '>=0.33.0' - vite-plugin-svgr@4.5.0: resolution: {integrity: sha512-W+uoSpmVkSmNOGPSsDCWVW/DDAyv+9fap9AZXBvWiQqrboJ08j2vh0tFxTD/LjwqwAd3yYSVJgm54S/1GhbdnA==} peerDependencies: vite: '>=2.6.0' - vite@7.2.2: - resolution: {integrity: sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==} + vite@7.2.6: + resolution: {integrity: sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -4198,40 +4021,6 @@ packages: yaml: optional: true - vitest@4.0.9: - resolution: {integrity: sha512-E0Ja2AX4th+CG33yAFRC+d1wFx2pzU5r6HtG6LiPSE04flaE0qB6YyjSw9ZcpJAtVPfsvZGtJlKWZpuW7EHRxg==} - engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@types/debug': ^4.1.12 - '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.0.9 - '@vitest/browser-preview': 4.0.9 - '@vitest/browser-webdriverio': 4.0.9 - '@vitest/ui': 4.0.9 - happy-dom: '*' - jsdom: '*' - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@types/debug': - optional: true - '@types/node': - optional: true - '@vitest/browser-playwright': - optional: true - '@vitest/browser-preview': - optional: true - '@vitest/browser-webdriverio': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: - optional: true - void-elements@3.1.0: resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} engines: {node: '>=0.10.0'} @@ -4277,23 +4066,10 @@ packages: engines: {node: '>= 8'} hasBin: true - why-is-node-running@2.3.0: - resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} - engines: {node: '>=8'} - hasBin: true - word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} - wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} - - wrap-ansi@8.1.0: - resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} - engines: {node: '>=12'} - wrap-ansi@9.0.2: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} @@ -4327,8 +4103,8 @@ packages: peerDependencies: zod: ^3.25.0 || ^4.0.0 - zod@4.1.12: - resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==} + zod@4.1.13: + resolution: {integrity: sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==} zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -5077,7 +4853,7 @@ snapshots: '@emotion/memoize@0.9.0': {} - '@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0)': + '@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 '@emotion/babel-plugin': 11.13.5 @@ -5089,7 +4865,7 @@ snapshots: hoist-non-react-statics: 3.3.2 react: 19.2.0 optionalDependencies: - '@types/react': 19.2.4 + '@types/react': 19.2.7 transitivePeerDependencies: - supports-color @@ -5099,22 +4875,22 @@ snapshots: '@emotion/memoize': 0.9.0 '@emotion/unitless': 0.10.0 '@emotion/utils': 1.4.2 - csstype: 3.1.3 + csstype: 3.2.3 '@emotion/sheet@1.4.0': {} - '@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0)': + '@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 '@emotion/babel-plugin': 11.13.5 '@emotion/is-prop-valid': 1.3.1 - '@emotion/react': 11.14.0(@types/react@19.2.4)(react@19.2.0) + '@emotion/react': 11.14.0(@types/react@19.2.7)(react@19.2.0) '@emotion/serialize': 1.3.3 '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.0) '@emotion/utils': 1.4.2 react: 19.2.0 optionalDependencies: - '@types/react': 19.2.4 + '@types/react': 19.2.7 transitivePeerDependencies: - supports-color @@ -5212,78 +4988,78 @@ snapshots: '@eslint-community/regexpp@4.12.1': {} - '@eslint-react/ast@2.3.5(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@eslint-react/ast@2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-react/eff': 2.3.5 - '@typescript-eslint/types': 8.46.4 - '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - string-ts: 2.2.1 - transitivePeerDependencies: - - eslint - - supports-color - - typescript - - '@eslint-react/core@2.3.5(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': - dependencies: - '@eslint-react/ast': 2.3.5(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/eff': 2.3.5 - '@eslint-react/shared': 2.3.5(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/var': 2.3.5(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.46.4 - '@typescript-eslint/types': 8.46.4 - '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - birecord: 0.1.1 - ts-pattern: 5.9.0 - transitivePeerDependencies: - - eslint - - supports-color - - typescript - - '@eslint-react/eff@2.3.5': {} - - '@eslint-react/eslint-plugin@2.3.5(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': - dependencies: - '@eslint-react/eff': 2.3.5 - '@eslint-react/shared': 2.3.5(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.46.4 - '@typescript-eslint/type-utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/types': 8.46.4 - '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.3.11 + '@typescript-eslint/types': 8.48.1 + '@typescript-eslint/typescript-estree': 8.48.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) - eslint-plugin-react-dom: 2.3.5(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-react-hooks-extra: 2.3.5(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-react-naming-convention: 2.3.5(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-react-web-api: 2.3.5(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-react-x: 2.3.5(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + string-ts: 2.3.1 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@eslint-react/core@2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-react/ast': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.3.11 + '@eslint-react/shared': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.48.1 + '@typescript-eslint/types': 8.48.1 + '@typescript-eslint/utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + birecord: 0.1.1 + eslint: 9.39.1(jiti@2.6.1) + ts-pattern: 5.9.0 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@eslint-react/eff@2.3.11': {} + + '@eslint-react/eslint-plugin@2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-react/eff': 2.3.11 + '@eslint-react/shared': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.48.1 + '@typescript-eslint/type-utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.48.1 + '@typescript-eslint/utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.1(jiti@2.6.1) + eslint-plugin-react-dom: 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-react-hooks-extra: 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-react-naming-convention: 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-react-web-api: 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-react-x: 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) ts-api-utils: 2.1.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@eslint-react/shared@2.3.5(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@eslint-react/shared@2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-react/eff': 2.3.5 - '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.3.11 + '@typescript-eslint/utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.1(jiti@2.6.1) ts-pattern: 5.9.0 - zod: 4.1.12 + typescript: 5.9.3 + zod: 4.1.13 transitivePeerDependencies: - - eslint - supports-color - - typescript - '@eslint-react/var@2.3.5(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@eslint-react/var@2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-react/ast': 2.3.5(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/eff': 2.3.5 - '@typescript-eslint/scope-manager': 8.46.4 - '@typescript-eslint/types': 8.46.4 - '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/ast': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.3.11 + '@typescript-eslint/scope-manager': 8.48.1 + '@typescript-eslint/types': 8.48.1 + '@typescript-eslint/utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.1(jiti@2.6.1) ts-pattern: 5.9.0 + typescript: 5.9.3 transitivePeerDependencies: - - eslint - supports-color - - typescript '@eslint/config-array@0.21.1': dependencies: @@ -5345,15 +5121,6 @@ snapshots: dependencies: '@isaacs/balanced-match': 4.0.1 - '@isaacs/cliui@8.0.2': - dependencies: - string-width: 5.1.2 - string-width-cjs: string-width@4.2.3 - strip-ansi: 7.1.0 - strip-ansi-cjs: strip-ansi@6.0.1 - wrap-ansi: 8.1.0 - wrap-ansi-cjs: wrap-ansi@7.0.0 - '@isaacs/fs-minipass@4.0.1': dependencies: minipass: 7.1.2 @@ -5388,154 +5155,115 @@ snapshots: dependencies: state-local: 1.0.7 - '@monaco-editor/react@4.7.0(monaco-editor@0.54.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@monaco-editor/react@4.7.0(monaco-editor@0.55.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@monaco-editor/loader': 1.6.1 - monaco-editor: 0.54.0 + monaco-editor: 0.55.1 react: 19.2.0 react-dom: 19.2.0(react@19.2.0) '@mui/core-downloads-tracker@7.3.5': {} - '@mui/icons-material@7.3.5(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.4)(react@19.2.0)': + '@mui/icons-material@7.3.5(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.7)(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 - '@mui/material': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@mui/material': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react: 19.2.0 optionalDependencies: - '@types/react': 19.2.4 + '@types/react': 19.2.7 - '@mui/lab@7.0.0-beta.17(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@mui/lab@7.0.0-beta.17(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0))(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 - '@mui/material': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@mui/system': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0) - '@mui/types': 7.4.8(@types/react@19.2.4) - '@mui/utils': 7.3.5(@types/react@19.2.4)(react@19.2.0) + '@mui/material': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@mui/system': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0) + '@mui/types': 7.4.8(@types/react@19.2.7) + '@mui/utils': 7.3.5(@types/react@19.2.7)(react@19.2.0) clsx: 2.1.1 prop-types: 15.8.1 react: 19.2.0 react-dom: 19.2.0(react@19.2.0) optionalDependencies: - '@emotion/react': 11.14.0(@types/react@19.2.4)(react@19.2.0) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0) - '@types/react': 19.2.4 + '@emotion/react': 11.14.0(@types/react@19.2.7)(react@19.2.0) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0) + '@types/react': 19.2.7 - '@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 '@mui/core-downloads-tracker': 7.3.5 - '@mui/system': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0) - '@mui/types': 7.4.8(@types/react@19.2.4) - '@mui/utils': 7.3.5(@types/react@19.2.4)(react@19.2.0) + '@mui/system': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0) + '@mui/types': 7.4.8(@types/react@19.2.7) + '@mui/utils': 7.3.5(@types/react@19.2.7)(react@19.2.0) '@popperjs/core': 2.11.8 - '@types/react-transition-group': 4.4.12(@types/react@19.2.4) + '@types/react-transition-group': 4.4.12(@types/react@19.2.7) clsx: 2.1.1 - csstype: 3.1.3 + csstype: 3.2.3 prop-types: 15.8.1 react: 19.2.0 react-dom: 19.2.0(react@19.2.0) react-is: 19.2.0 react-transition-group: 4.4.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0) optionalDependencies: - '@emotion/react': 11.14.0(@types/react@19.2.4)(react@19.2.0) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0) - '@types/react': 19.2.4 + '@emotion/react': 11.14.0(@types/react@19.2.7)(react@19.2.0) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0) + '@types/react': 19.2.7 - '@mui/private-theming@7.3.5(@types/react@19.2.4)(react@19.2.0)': + '@mui/private-theming@7.3.5(@types/react@19.2.7)(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 - '@mui/utils': 7.3.5(@types/react@19.2.4)(react@19.2.0) + '@mui/utils': 7.3.5(@types/react@19.2.7)(react@19.2.0) prop-types: 15.8.1 react: 19.2.0 optionalDependencies: - '@types/react': 19.2.4 + '@types/react': 19.2.7 - '@mui/styled-engine@7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(react@19.2.0)': + '@mui/styled-engine@7.3.5(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0))(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 '@emotion/cache': 11.14.0 '@emotion/serialize': 1.3.3 '@emotion/sheet': 1.4.0 - csstype: 3.1.3 + csstype: 3.2.3 prop-types: 15.8.1 react: 19.2.0 optionalDependencies: - '@emotion/react': 11.14.0(@types/react@19.2.4)(react@19.2.0) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0) + '@emotion/react': 11.14.0(@types/react@19.2.7)(react@19.2.0) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0) - '@mui/system@7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0)': + '@mui/system@7.3.5(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 - '@mui/private-theming': 7.3.5(@types/react@19.2.4)(react@19.2.0) - '@mui/styled-engine': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(react@19.2.0) - '@mui/types': 7.4.8(@types/react@19.2.4) - '@mui/utils': 7.3.5(@types/react@19.2.4)(react@19.2.0) + '@mui/private-theming': 7.3.5(@types/react@19.2.7)(react@19.2.0) + '@mui/styled-engine': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0))(react@19.2.0) + '@mui/types': 7.4.8(@types/react@19.2.7) + '@mui/utils': 7.3.5(@types/react@19.2.7)(react@19.2.0) clsx: 2.1.1 - csstype: 3.1.3 + csstype: 3.2.3 prop-types: 15.8.1 react: 19.2.0 optionalDependencies: - '@emotion/react': 11.14.0(@types/react@19.2.4)(react@19.2.0) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0) - '@types/react': 19.2.4 + '@emotion/react': 11.14.0(@types/react@19.2.7)(react@19.2.0) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0) + '@types/react': 19.2.7 - '@mui/types@7.4.8(@types/react@19.2.4)': + '@mui/types@7.4.8(@types/react@19.2.7)': dependencies: '@babel/runtime': 7.28.4 optionalDependencies: - '@types/react': 19.2.4 + '@types/react': 19.2.7 - '@mui/utils@7.3.5(@types/react@19.2.4)(react@19.2.0)': + '@mui/utils@7.3.5(@types/react@19.2.7)(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 - '@mui/types': 7.4.8(@types/react@19.2.4) + '@mui/types': 7.4.8(@types/react@19.2.7) '@types/prop-types': 15.7.15 clsx: 2.1.1 prop-types: 15.8.1 react: 19.2.0 react-is: 19.2.0 optionalDependencies: - '@types/react': 19.2.4 - - '@mui/x-data-grid@8.18.0(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@mui/system@7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@babel/runtime': 7.28.4 - '@mui/material': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@mui/system': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0) - '@mui/utils': 7.3.5(@types/react@19.2.4)(react@19.2.0) - '@mui/x-internals': 8.18.0(@types/react@19.2.4)(react@19.2.0) - '@mui/x-virtualizer': 0.2.8(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - clsx: 2.1.1 - prop-types: 15.8.1 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - use-sync-external-store: 1.6.0(react@19.2.0) - optionalDependencies: - '@emotion/react': 11.14.0(@types/react@19.2.4)(react@19.2.0) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0) - transitivePeerDependencies: - - '@types/react' - - '@mui/x-internals@8.18.0(@types/react@19.2.4)(react@19.2.0)': - dependencies: - '@babel/runtime': 7.28.4 - '@mui/utils': 7.3.5(@types/react@19.2.4)(react@19.2.0) - react: 19.2.0 - reselect: 5.1.1 - use-sync-external-store: 1.6.0(react@19.2.0) - transitivePeerDependencies: - - '@types/react' - - '@mui/x-virtualizer@0.2.8(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@babel/runtime': 7.28.4 - '@mui/utils': 7.3.5(@types/react@19.2.4)(react@19.2.0) - '@mui/x-internals': 8.18.0(@types/react@19.2.4)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - transitivePeerDependencies: - - '@types/react' + '@types/react': 19.2.7 '@napi-rs/wasm-runtime@0.2.12': dependencies: @@ -5544,18 +5272,6 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true - '@nodelib/fs.scandir@2.1.5': - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - - '@nodelib/fs.stat@2.0.5': {} - - '@nodelib/fs.walk@1.2.8': - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.19.1 - '@octokit/auth-token@4.0.0': {} '@octokit/core@5.2.1': @@ -5752,8 +5468,6 @@ snapshots: '@rtsao/scc@1.1.0': optional: true - '@standard-schema/spec@1.0.0': {} - '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.28.4)': dependencies: '@babel/core': 7.28.4 @@ -5876,98 +5590,108 @@ snapshots: dependencies: '@swc/counter': 0.1.3 - '@tauri-apps/api@2.9.0': {} + '@tanstack/react-table@8.21.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@tanstack/table-core': 8.21.3 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) - '@tauri-apps/cli-darwin-arm64@2.9.4': + '@tanstack/react-virtual@3.13.12(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@tanstack/virtual-core': 3.13.12 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + + '@tanstack/table-core@8.21.3': {} + + '@tanstack/virtual-core@3.13.12': {} + + '@tauri-apps/api@2.9.1': {} + + '@tauri-apps/cli-darwin-arm64@2.9.5': optional: true - '@tauri-apps/cli-darwin-x64@2.9.4': + '@tauri-apps/cli-darwin-x64@2.9.5': optional: true - '@tauri-apps/cli-linux-arm-gnueabihf@2.9.4': + '@tauri-apps/cli-linux-arm-gnueabihf@2.9.5': optional: true - '@tauri-apps/cli-linux-arm64-gnu@2.9.4': + '@tauri-apps/cli-linux-arm64-gnu@2.9.5': optional: true - '@tauri-apps/cli-linux-arm64-musl@2.9.4': + '@tauri-apps/cli-linux-arm64-musl@2.9.5': optional: true - '@tauri-apps/cli-linux-riscv64-gnu@2.9.4': + '@tauri-apps/cli-linux-riscv64-gnu@2.9.5': optional: true - '@tauri-apps/cli-linux-x64-gnu@2.9.4': + '@tauri-apps/cli-linux-x64-gnu@2.9.5': optional: true - '@tauri-apps/cli-linux-x64-musl@2.9.4': + '@tauri-apps/cli-linux-x64-musl@2.9.5': optional: true - '@tauri-apps/cli-win32-arm64-msvc@2.9.4': + '@tauri-apps/cli-win32-arm64-msvc@2.9.5': optional: true - '@tauri-apps/cli-win32-ia32-msvc@2.9.4': + '@tauri-apps/cli-win32-ia32-msvc@2.9.5': optional: true - '@tauri-apps/cli-win32-x64-msvc@2.9.4': + '@tauri-apps/cli-win32-x64-msvc@2.9.5': optional: true - '@tauri-apps/cli@2.9.4': + '@tauri-apps/cli@2.9.5': optionalDependencies: - '@tauri-apps/cli-darwin-arm64': 2.9.4 - '@tauri-apps/cli-darwin-x64': 2.9.4 - '@tauri-apps/cli-linux-arm-gnueabihf': 2.9.4 - '@tauri-apps/cli-linux-arm64-gnu': 2.9.4 - '@tauri-apps/cli-linux-arm64-musl': 2.9.4 - '@tauri-apps/cli-linux-riscv64-gnu': 2.9.4 - '@tauri-apps/cli-linux-x64-gnu': 2.9.4 - '@tauri-apps/cli-linux-x64-musl': 2.9.4 - '@tauri-apps/cli-win32-arm64-msvc': 2.9.4 - '@tauri-apps/cli-win32-ia32-msvc': 2.9.4 - '@tauri-apps/cli-win32-x64-msvc': 2.9.4 + '@tauri-apps/cli-darwin-arm64': 2.9.5 + '@tauri-apps/cli-darwin-x64': 2.9.5 + '@tauri-apps/cli-linux-arm-gnueabihf': 2.9.5 + '@tauri-apps/cli-linux-arm64-gnu': 2.9.5 + '@tauri-apps/cli-linux-arm64-musl': 2.9.5 + '@tauri-apps/cli-linux-riscv64-gnu': 2.9.5 + '@tauri-apps/cli-linux-x64-gnu': 2.9.5 + '@tauri-apps/cli-linux-x64-musl': 2.9.5 + '@tauri-apps/cli-win32-arm64-msvc': 2.9.5 + '@tauri-apps/cli-win32-ia32-msvc': 2.9.5 + '@tauri-apps/cli-win32-x64-msvc': 2.9.5 '@tauri-apps/plugin-clipboard-manager@2.3.2': dependencies: - '@tauri-apps/api': 2.9.0 + '@tauri-apps/api': 2.9.1 '@tauri-apps/plugin-dialog@2.4.2': dependencies: - '@tauri-apps/api': 2.9.0 + '@tauri-apps/api': 2.9.1 '@tauri-apps/plugin-fs@2.4.4': dependencies: - '@tauri-apps/api': 2.9.0 + '@tauri-apps/api': 2.9.1 '@tauri-apps/plugin-http@2.5.4': dependencies: - '@tauri-apps/api': 2.9.0 + '@tauri-apps/api': 2.9.1 '@tauri-apps/plugin-process@2.3.1': dependencies: - '@tauri-apps/api': 2.9.0 + '@tauri-apps/api': 2.9.1 '@tauri-apps/plugin-shell@2.3.3': dependencies: - '@tauri-apps/api': 2.9.0 + '@tauri-apps/api': 2.9.1 '@tauri-apps/plugin-updater@2.9.0': dependencies: - '@tauri-apps/api': 2.9.0 + '@tauri-apps/api': 2.9.1 '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 optional: true - '@types/chai@5.2.2': - dependencies: - '@types/deep-eql': 4.0.2 - '@types/debug@4.1.12': dependencies: '@types/ms': 2.1.0 - '@types/deep-eql@4.0.2': {} - '@types/estree-jsx@1.0.5': dependencies: '@types/estree': 1.0.8 @@ -6007,30 +5731,33 @@ snapshots: '@types/prop-types@15.7.15': {} - '@types/react-dom@19.2.3(@types/react@19.2.4)': + '@types/react-dom@19.2.3(@types/react@19.2.7)': dependencies: - '@types/react': 19.2.4 + '@types/react': 19.2.7 - '@types/react-transition-group@4.4.12(@types/react@19.2.4)': + '@types/react-transition-group@4.4.12(@types/react@19.2.7)': dependencies: - '@types/react': 19.2.4 + '@types/react': 19.2.7 - '@types/react@19.2.4': + '@types/react@19.2.7': dependencies: - csstype: 3.1.3 + csstype: 3.2.3 + + '@types/trusted-types@2.0.7': + optional: true '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} - '@typescript-eslint/eslint-plugin@8.46.4(@typescript-eslint/parser@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.48.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.46.4 - '@typescript-eslint/type-utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.46.4 + '@typescript-eslint/parser': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.48.1 + '@typescript-eslint/type-utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.48.1 eslint: 9.39.1(jiti@2.6.1) graphemer: 1.4.0 ignore: 7.0.5 @@ -6040,41 +5767,41 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.46.4 - '@typescript-eslint/types': 8.46.4 - '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.46.4 + '@typescript-eslint/scope-manager': 8.48.1 + '@typescript-eslint/types': 8.48.1 + '@typescript-eslint/typescript-estree': 8.48.1(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.48.1 debug: 4.4.3 eslint: 9.39.1(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.46.4(typescript@5.9.3)': + '@typescript-eslint/project-service@8.48.1(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.46.4(typescript@5.9.3) - '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/tsconfig-utils': 8.48.1(typescript@5.9.3) + '@typescript-eslint/types': 8.48.1 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.46.4': + '@typescript-eslint/scope-manager@8.48.1': dependencies: - '@typescript-eslint/types': 8.46.4 - '@typescript-eslint/visitor-keys': 8.46.4 + '@typescript-eslint/types': 8.48.1 + '@typescript-eslint/visitor-keys': 8.48.1 - '@typescript-eslint/tsconfig-utils@8.46.4(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.48.1(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.46.4 - '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.48.1 + '@typescript-eslint/typescript-estree': 8.48.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) debug: 4.4.3 eslint: 9.39.1(jiti@2.6.1) ts-api-utils: 2.1.0(typescript@5.9.3) @@ -6082,38 +5809,37 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.46.4': {} + '@typescript-eslint/types@8.48.1': {} - '@typescript-eslint/typescript-estree@8.46.4(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.48.1(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.46.4(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.46.4(typescript@5.9.3) - '@typescript-eslint/types': 8.46.4 - '@typescript-eslint/visitor-keys': 8.46.4 + '@typescript-eslint/project-service': 8.48.1(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.48.1(typescript@5.9.3) + '@typescript-eslint/types': 8.48.1 + '@typescript-eslint/visitor-keys': 8.48.1 debug: 4.4.3 - fast-glob: 3.3.3 - is-glob: 4.0.3 minimatch: 9.0.5 semver: 7.7.2 + tinyglobby: 0.2.15 ts-api-utils: 2.1.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/utils@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.8.0(eslint@9.39.1(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.46.4 - '@typescript-eslint/types': 8.46.4 - '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.48.1 + '@typescript-eslint/types': 8.48.1 + '@typescript-eslint/typescript-estree': 8.48.1(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.46.4': + '@typescript-eslint/visitor-keys@8.48.1': dependencies: - '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/types': 8.48.1 eslint-visitor-keys: 4.2.1 '@ungap/structured-clone@1.3.0': {} @@ -6177,7 +5903,7 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@vitejs/plugin-legacy@7.2.1(terser@5.44.1)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))': + '@vitejs/plugin-legacy@7.2.1(terser@5.44.1)(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.1))': dependencies: '@babel/core': 7.28.4 '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.28.4) @@ -6192,57 +5918,18 @@ snapshots: regenerator-runtime: 0.14.1 systemjs: 6.15.1 terser: 5.44.1 - vite: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1) + vite: 7.2.6(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.1) transitivePeerDependencies: - supports-color - '@vitejs/plugin-react-swc@4.2.2(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))': + '@vitejs/plugin-react-swc@4.2.2(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.1))': dependencies: '@rolldown/pluginutils': 1.0.0-beta.47 '@swc/core': 1.14.0 - vite: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1) + vite: 7.2.6(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.1) transitivePeerDependencies: - '@swc/helpers' - '@vitest/expect@4.0.9': - dependencies: - '@standard-schema/spec': 1.0.0 - '@types/chai': 5.2.2 - '@vitest/spy': 4.0.9 - '@vitest/utils': 4.0.9 - chai: 6.2.0 - tinyrainbow: 3.0.3 - - '@vitest/mocker@4.0.9(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))': - dependencies: - '@vitest/spy': 4.0.9 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1) - - '@vitest/pretty-format@4.0.9': - dependencies: - tinyrainbow: 3.0.3 - - '@vitest/runner@4.0.9': - dependencies: - '@vitest/utils': 4.0.9 - pathe: 2.0.3 - - '@vitest/snapshot@4.0.9': - dependencies: - '@vitest/pretty-format': 4.0.9 - magic-string: 0.30.21 - pathe: 2.0.3 - - '@vitest/spy@4.0.9': {} - - '@vitest/utils@4.0.9': - dependencies: - '@vitest/pretty-format': 4.0.9 - tinyrainbow: 3.0.3 - acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -6279,8 +5966,6 @@ snapshots: dependencies: environment: 1.1.0 - ansi-regex@5.0.1: {} - ansi-regex@6.1.0: {} ansi-styles@4.3.0: @@ -6457,8 +6142,6 @@ snapshots: ccount@2.0.1: {} - chai@6.2.0: {} - chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -6563,7 +6246,7 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - csstype@3.1.3: {} + csstype@3.2.3: {} d@1.0.2: dependencies: @@ -6645,9 +6328,11 @@ snapshots: dom-helpers@5.2.1: dependencies: '@babel/runtime': 7.28.4 - csstype: 3.1.3 + csstype: 3.2.3 - dompurify@3.1.7: {} + dompurify@3.2.7: + optionalDependencies: + '@types/trusted-types': 2.0.7 dot-case@3.0.4: dependencies: @@ -6660,16 +6345,10 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 - eastasianwidth@0.2.0: {} - electron-to-chromium@1.5.173: {} emoji-regex@10.5.0: {} - emoji-regex@8.0.0: {} - - emoji-regex@9.2.2: {} - entities@4.5.0: {} environment@1.1.0: {} @@ -6740,8 +6419,6 @@ snapshots: es-errors@1.3.0: {} - es-module-lexer@1.7.0: {} - es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -6842,7 +6519,7 @@ snapshots: - supports-color optional: true - eslint-import-resolver-typescript@4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)): + eslint-import-resolver-typescript@4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)): dependencies: debug: 4.4.3 eslint: 9.39.1(jiti@2.6.1) @@ -6853,42 +6530,42 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1(jiti@2.6.1)) - eslint-plugin-import-x: 4.16.1(@typescript-eslint/utils@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-import-x: 4.16.1(@typescript-eslint/utils@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) transitivePeerDependencies: - supports-color optional: true - eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)): + eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)): dependencies: - '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/types': 8.48.1 comment-parser: 1.4.1 debug: 4.4.3 eslint: 9.39.1(jiti@2.6.1) eslint-import-context: 0.1.9(unrs-resolver@1.11.1) is-glob: 4.0.3 - minimatch: 10.0.3 + minimatch: 10.1.1 semver: 7.7.2 stable-hash-x: 0.2.0 unrs-resolver: 1.11.1 optionalDependencies: - '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -6899,7 +6576,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -6911,53 +6588,53 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color optional: true - eslint-plugin-prettier@5.5.4(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.6.2): + eslint-plugin-prettier@5.5.4(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.7.3): dependencies: eslint: 9.39.1(jiti@2.6.1) - prettier: 3.6.2 + prettier: 3.7.3 prettier-linter-helpers: 1.0.0 synckit: 0.11.11 optionalDependencies: eslint-config-prettier: 10.1.8(eslint@9.39.1(jiti@2.6.1)) - eslint-plugin-react-dom@2.3.5(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-react-dom@2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@eslint-react/ast': 2.3.5(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/core': 2.3.5(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/eff': 2.3.5 - '@eslint-react/shared': 2.3.5(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/var': 2.3.5(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.46.4 - '@typescript-eslint/types': 8.46.4 - '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/ast': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/core': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.3.11 + '@eslint-react/shared': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.48.1 + '@typescript-eslint/types': 8.48.1 + '@typescript-eslint/utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) compare-versions: 6.1.1 eslint: 9.39.1(jiti@2.6.1) - string-ts: 2.2.1 + string-ts: 2.3.1 ts-pattern: 5.9.0 typescript: 5.9.3 transitivePeerDependencies: - supports-color - eslint-plugin-react-hooks-extra@2.3.5(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-react-hooks-extra@2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@eslint-react/ast': 2.3.5(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/core': 2.3.5(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/eff': 2.3.5 - '@eslint-react/shared': 2.3.5(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/var': 2.3.5(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.46.4 - '@typescript-eslint/type-utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/types': 8.46.4 - '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/ast': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/core': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.3.11 + '@eslint-react/shared': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.48.1 + '@typescript-eslint/type-utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.48.1 + '@typescript-eslint/utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) - string-ts: 2.2.1 + string-ts: 2.3.1 ts-pattern: 5.9.0 typescript: 5.9.3 transitivePeerDependencies: @@ -6969,24 +6646,24 @@ snapshots: '@babel/parser': 7.28.4 eslint: 9.39.1(jiti@2.6.1) hermes-parser: 0.25.1 - zod: 4.1.12 - zod-validation-error: 4.0.2(zod@4.1.12) + zod: 4.1.13 + zod-validation-error: 4.0.2(zod@4.1.13) transitivePeerDependencies: - supports-color - eslint-plugin-react-naming-convention@2.3.5(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-react-naming-convention@2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@eslint-react/ast': 2.3.5(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/core': 2.3.5(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/eff': 2.3.5 - '@eslint-react/shared': 2.3.5(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/var': 2.3.5(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.46.4 - '@typescript-eslint/type-utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/types': 8.46.4 - '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/ast': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/core': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.3.11 + '@eslint-react/shared': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.48.1 + '@typescript-eslint/type-utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.48.1 + '@typescript-eslint/utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) - string-ts: 2.2.1 + string-ts: 2.3.1 ts-pattern: 5.9.0 typescript: 5.9.3 transitivePeerDependencies: @@ -6996,49 +6673,49 @@ snapshots: dependencies: eslint: 9.39.1(jiti@2.6.1) - eslint-plugin-react-web-api@2.3.5(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-react-web-api@2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@eslint-react/ast': 2.3.5(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/core': 2.3.5(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/eff': 2.3.5 - '@eslint-react/shared': 2.3.5(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/var': 2.3.5(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.46.4 - '@typescript-eslint/types': 8.46.4 - '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/ast': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/core': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.3.11 + '@eslint-react/shared': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.48.1 + '@typescript-eslint/types': 8.48.1 + '@typescript-eslint/utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) - string-ts: 2.2.1 + string-ts: 2.3.1 ts-pattern: 5.9.0 typescript: 5.9.3 transitivePeerDependencies: - supports-color - eslint-plugin-react-x@2.3.5(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-react-x@2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@eslint-react/ast': 2.3.5(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/core': 2.3.5(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/eff': 2.3.5 - '@eslint-react/shared': 2.3.5(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/var': 2.3.5(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.46.4 - '@typescript-eslint/type-utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/types': 8.46.4 - '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/ast': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/core': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.3.11 + '@eslint-react/shared': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 2.3.11(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.48.1 + '@typescript-eslint/type-utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.48.1 + '@typescript-eslint/utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) compare-versions: 6.1.1 eslint: 9.39.1(jiti@2.6.1) is-immutable-type: 5.0.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - string-ts: 2.2.1 + string-ts: 2.3.1 ts-api-utils: 2.1.0(typescript@5.9.3) ts-pattern: 5.9.0 typescript: 5.9.3 transitivePeerDependencies: - supports-color - eslint-plugin-unused-imports@4.3.0(@typescript-eslint/eslint-plugin@8.46.4(@typescript-eslint/parser@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)): + eslint-plugin-unused-imports@4.3.0(@typescript-eslint/eslint-plugin@8.48.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.46.4(@typescript-eslint/parser@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.48.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint-scope@8.4.0: dependencies: @@ -7117,10 +6794,6 @@ snapshots: estree-walker@2.0.2: {} - estree-walker@3.0.3: - dependencies: - '@types/estree': 1.0.8 - esutils@2.0.3: {} event-emitter@0.3.5: @@ -7130,8 +6803,6 @@ snapshots: eventemitter3@5.0.1: {} - expect-type@1.2.2: {} - ext@1.7.0: dependencies: type: 2.7.3 @@ -7142,22 +6813,10 @@ snapshots: fast-diff@1.3.0: {} - fast-glob@3.3.3: - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.8 - fast-json-stable-stringify@2.1.0: {} fast-levenshtein@2.0.6: {} - fastq@1.19.1: - dependencies: - reusify: 1.1.0 - fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 @@ -7196,11 +6855,6 @@ snapshots: is-callable: 1.2.7 optional: true - foreground-child@3.3.1: - dependencies: - cross-spawn: 7.0.6 - signal-exit: 4.1.0 - form-data@4.0.4: dependencies: asynckit: 0.4.0 @@ -7271,21 +6925,14 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 - glob-parent@5.1.2: - dependencies: - is-glob: 4.0.3 - glob-parent@6.0.2: dependencies: is-glob: 4.0.3 - glob@11.0.3: + glob@13.0.0: dependencies: - foreground-child: 3.3.1 - jackspeak: 4.1.1 - minimatch: 10.0.3 + minimatch: 10.1.1 minipass: 7.1.2 - package-json-from-dist: 1.0.1 path-scurry: 2.0.0 globals@14.0.0: {} @@ -7376,7 +7023,7 @@ snapshots: husky@9.1.7: {} - i18next@25.6.2(typescript@5.9.3): + i18next@25.7.1(typescript@5.9.3): dependencies: '@babel/runtime': 7.28.4 optionalDependencies: @@ -7475,8 +7122,6 @@ snapshots: call-bound: 1.0.4 optional: true - is-fullwidth-code-point@3.0.0: {} - is-fullwidth-code-point@5.1.0: dependencies: get-east-asian-width: 1.4.0 @@ -7497,7 +7142,7 @@ snapshots: is-immutable-type@5.0.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/type-utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) ts-api-utils: 2.1.0(typescript@5.9.3) ts-declaration-location: 1.0.7(typescript@5.9.3) @@ -7576,10 +7221,6 @@ snapshots: isexe@2.0.0: {} - jackspeak@4.1.1: - dependencies: - '@isaacs/cliui': 8.0.2 - jiti@2.6.1: {} js-cookie@3.0.5: {} @@ -7600,8 +7241,6 @@ snapshots: json-schema-traverse@0.4.1: {} - json-schema@0.4.0: {} - json-stable-stringify-without-jsonify@1.0.1: {} json5@1.0.2: @@ -7624,7 +7263,7 @@ snapshots: lines-and-columns@1.2.4: {} - lint-staged@16.2.6: + lint-staged@16.2.7: dependencies: commander: 14.0.2 listr2: 9.0.5 @@ -7793,10 +7432,6 @@ snapshots: meow@13.2.0: {} - merge2@1.4.1: {} - - meta-json-schema@1.19.16: {} - micromark-core-commonmark@2.0.3: dependencies: decode-named-character-reference: 1.1.0 @@ -7943,7 +7578,7 @@ snapshots: mimic-function@5.0.1: {} - minimatch@10.0.3: + minimatch@10.1.1: dependencies: '@isaacs/brace-expansion': 5.0.0 @@ -7964,9 +7599,9 @@ snapshots: dependencies: minipass: 7.1.2 - monaco-editor@0.54.0: + monaco-editor@0.55.1: dependencies: - dompurify: 3.1.7 + dompurify: 3.2.7 marked: 14.0.0 monaco-languageserver-types@0.4.0: @@ -7981,20 +7616,20 @@ snapshots: monaco-types@0.1.0: {} - monaco-worker-manager@2.0.1(monaco-editor@0.54.0): + monaco-worker-manager@2.0.1(monaco-editor@0.55.1): dependencies: - monaco-editor: 0.54.0 + monaco-editor: 0.55.1 - monaco-yaml@5.4.0(monaco-editor@0.54.0): + monaco-yaml@5.4.0(monaco-editor@0.55.1): dependencies: jsonc-parser: 3.3.1 - monaco-editor: 0.54.0 + monaco-editor: 0.55.1 monaco-languageserver-types: 0.4.0 monaco-marker-data-provider: 1.2.4 monaco-types: 0.1.0 - monaco-worker-manager: 2.0.1(monaco-editor@0.54.0) + monaco-worker-manager: 2.0.1(monaco-editor@0.55.1) path-browserify: 1.0.1 - prettier: 3.6.2 + prettier: 3.7.3 vscode-languageserver-textdocument: 1.0.12 vscode-languageserver-types: 3.17.5 vscode-uri: 3.1.0 @@ -8105,8 +7740,6 @@ snapshots: dependencies: p-limit: 3.1.0 - package-json-from-dist@1.0.1: {} - parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -8143,8 +7776,6 @@ snapshots: path-type@4.0.0: {} - pathe@2.0.3: {} - picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -8168,7 +7799,7 @@ snapshots: dependencies: fast-diff: 1.3.0 - prettier@3.6.2: {} + prettier@3.7.3: {} prop-types@15.8.1: dependencies: @@ -8182,8 +7813,6 @@ snapshots: punycode@2.3.1: {} - queue-microtask@1.2.3: {} - react-dom@19.2.0(react@19.2.0): dependencies: react: 19.2.0 @@ -8196,15 +7825,15 @@ snapshots: react-fast-compare@3.2.2: {} - react-hook-form@7.66.0(react@19.2.0): + react-hook-form@7.67.0(react@19.2.0): dependencies: react: 19.2.0 - react-i18next@16.3.3(i18next@25.6.2(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3): + react-i18next@16.3.5(i18next@25.7.1(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3): dependencies: '@babel/runtime': 7.28.4 html-parse-stringify: 3.0.1 - i18next: 25.6.2(typescript@5.9.3) + i18next: 25.7.1(typescript@5.9.3) react: 19.2.0 use-sync-external-store: 1.6.0(react@19.2.0) optionalDependencies: @@ -8215,11 +7844,11 @@ snapshots: react-is@19.2.0: {} - react-markdown@10.1.0(@types/react@19.2.4)(react@19.2.0): + react-markdown@10.1.0(@types/react@19.2.7)(react@19.2.0): dependencies: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 - '@types/react': 19.2.4 + '@types/react': 19.2.7 devlop: 1.1.0 hast-util-to-jsx-runtime: 2.3.6 html-url-attributes: 3.0.1 @@ -8233,7 +7862,7 @@ snapshots: transitivePeerDependencies: - supports-color - react-router@7.9.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + react-router@7.10.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: cookie: 1.0.2 react: 19.2.0 @@ -8250,7 +7879,7 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) - react-virtuoso@4.14.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + react-virtuoso@4.16.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) @@ -8321,8 +7950,6 @@ snapshots: unified: 11.0.5 vfile: 6.0.3 - reselect@5.1.1: {} - resize-observer-polyfill@1.5.1: {} resolve-from@4.0.0: {} @@ -8340,8 +7967,6 @@ snapshots: onetime: 7.0.0 signal-exit: 4.1.0 - reusify@1.1.0: {} - rfdc@1.4.1: {} rollup@4.46.2: @@ -8370,10 +7995,6 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.46.2 fsevents: 2.3.3 - run-parallel@1.2.0: - dependencies: - queue-microtask: 1.2.3 - safe-array-concat@1.1.3: dependencies: call-bind: 1.0.8 @@ -8396,7 +8017,7 @@ snapshots: is-regex: 1.2.1 optional: true - sass@1.94.0: + sass@1.94.2: dependencies: chokidar: 4.0.3 immutable: 5.1.2 @@ -8479,8 +8100,6 @@ snapshots: side-channel-weakmap: 1.0.2 optional: true - siginfo@2.0.0: {} - signal-exit@4.1.0: {} slice-ansi@7.1.2: @@ -8508,12 +8127,8 @@ snapshots: stable-hash-x@0.2.0: {} - stackback@0.0.2: {} - state-local@1.0.7: {} - std-env@3.10.0: {} - stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0 @@ -8522,19 +8137,7 @@ snapshots: string-argv@0.3.2: {} - string-ts@2.2.1: {} - - string-width@4.2.3: - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - - string-width@5.1.2: - dependencies: - eastasianwidth: 0.2.0 - emoji-regex: 9.2.2 - strip-ansi: 7.1.0 + string-ts@2.3.1: {} string-width@7.2.0: dependencies: @@ -8578,10 +8181,6 @@ snapshots: character-entities-html4: 2.1.0 character-entities-legacy: 3.0.0 - strip-ansi@6.0.1: - dependencies: - ansi-regex: 5.0.1 - strip-ansi@7.1.0: dependencies: ansi-regex: 6.1.0 @@ -8609,7 +8208,7 @@ snapshots: svg-parser@2.0.4: {} - swr@2.3.6(react@19.2.0): + swr@2.3.7(react@19.2.0): dependencies: dequal: 2.0.3 react: 19.2.0 @@ -8629,9 +8228,9 @@ snapshots: minizlib: 3.1.0 yallist: 5.0.0 - tauri-plugin-mihomo-api@https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/1e2ada19e20c5504a2a8cef367e87c667806dd65: + tauri-plugin-mihomo-api@https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/85fc6b364227e69f5b1cfeb8ca82642c153b5210: dependencies: - '@tauri-apps/api': 2.9.0 + '@tauri-apps/api': 2.9.1 terser@5.44.1: dependencies: @@ -8645,17 +8244,11 @@ snapshots: es5-ext: 0.10.64 next-tick: 1.1.0 - tinybench@2.9.0: {} - - tinyexec@0.3.2: {} - tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 - tinyrainbow@3.0.3: {} - to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -8732,12 +8325,12 @@ snapshots: types-pac@1.0.3: {} - typescript-eslint@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): + typescript-eslint@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.46.4(@typescript-eslint/parser@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.48.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.48.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: @@ -8853,22 +8446,18 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - vite-plugin-monaco-editor-esm@2.0.2(monaco-editor@0.54.0): - dependencies: - monaco-editor: 0.54.0 - - vite-plugin-svgr@4.5.0(rollup@4.46.2)(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)): + vite-plugin-svgr@4.5.0(rollup@4.46.2)(typescript@5.9.3)(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.1)): dependencies: '@rollup/pluginutils': 5.2.0(rollup@4.46.2) '@svgr/core': 8.1.0(typescript@5.9.3) '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3)) - vite: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1) + vite: 7.2.6(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.1) transitivePeerDependencies: - rollup - supports-color - typescript - vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1): + vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.1): dependencies: esbuild: 0.25.4 fdir: 6.5.0(picomatch@4.0.3) @@ -8880,49 +8469,10 @@ snapshots: '@types/node': 24.10.1 fsevents: 2.3.3 jiti: 2.6.1 - sass: 1.94.0 + sass: 1.94.2 terser: 5.44.1 yaml: 2.8.1 - vitest@4.0.9(@types/debug@4.1.12)(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1): - dependencies: - '@vitest/expect': 4.0.9 - '@vitest/mocker': 4.0.9(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)) - '@vitest/pretty-format': 4.0.9 - '@vitest/runner': 4.0.9 - '@vitest/snapshot': 4.0.9 - '@vitest/spy': 4.0.9 - '@vitest/utils': 4.0.9 - debug: 4.4.3 - es-module-lexer: 1.7.0 - expect-type: 1.2.2 - magic-string: 0.30.21 - pathe: 2.0.3 - picomatch: 4.0.3 - std-env: 3.10.0 - tinybench: 2.9.0 - tinyexec: 0.3.2 - tinyglobby: 0.2.15 - tinyrainbow: 3.0.3 - vite: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1) - why-is-node-running: 2.3.0 - optionalDependencies: - '@types/debug': 4.1.12 - '@types/node': 24.10.1 - transitivePeerDependencies: - - jiti - - less - - lightningcss - - msw - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - void-elements@3.1.0: {} vscode-jsonrpc@8.2.0: {} @@ -8989,25 +8539,8 @@ snapshots: dependencies: isexe: 2.0.0 - why-is-node-running@2.3.0: - dependencies: - siginfo: 2.0.0 - stackback: 0.0.2 - word-wrap@1.2.5: {} - wrap-ansi@7.0.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - - wrap-ansi@8.1.0: - dependencies: - ansi-styles: 6.2.1 - string-width: 5.1.2 - strip-ansi: 7.1.0 - wrap-ansi@9.0.2: dependencies: ansi-styles: 6.2.1 @@ -9026,10 +8559,10 @@ snapshots: yocto-queue@0.1.0: {} - zod-validation-error@4.0.2(zod@4.1.12): + zod-validation-error@4.0.2(zod@4.1.13): dependencies: - zod: 4.1.12 + zod: 4.1.13 - zod@4.1.12: {} + zod@4.1.13: {} zwitch@2.0.4: {} diff --git a/clash-verge-rev/pnpm-workspace.yaml b/clash-verge-rev/pnpm-workspace.yaml new file mode 100644 index 0000000000..97a7be15ce --- /dev/null +++ b/clash-verge-rev/pnpm-workspace.yaml @@ -0,0 +1,7 @@ +onlyBuiltDependencies: + - "@parcel/watcher" + - "@swc/core" + - core-js + - es5-ext + - esbuild + - unrs-resolver diff --git a/clash-verge-rev/renovate.json b/clash-verge-rev/renovate.json index 3aa47244e4..f3d1b10763 100644 --- a/clash-verge-rev/renovate.json +++ b/clash-verge-rev/renovate.json @@ -1,7 +1,7 @@ { "extends": ["config:recommended", ":disableDependencyDashboard"], "baseBranches": ["dev"], - "enabledManagers": ["cargo", "npm"], + "enabledManagers": ["cargo", "npm", "github-actions"], "labels": ["dependencies"], "ignorePaths": [ "**/node_modules/**", diff --git a/clash-verge-rev/src-tauri/Cargo.toml b/clash-verge-rev/src-tauri/Cargo.toml index 158efb45ae..ac1810f2a8 100755 --- a/clash-verge-rev/src-tauri/Cargo.toml +++ b/clash-verge-rev/src-tauri/Cargo.toml @@ -6,56 +6,73 @@ authors = ["zzzgydi", "Tunglies", "wonfen", "MystiPanda"] license = "GPL-3.0-only" repository = "https://github.com/clash-verge-rev/clash-verge-rev.git" default-run = "clash-verge" -edition = "2024" build = "build.rs" -rust-version = "1.91" +edition = { workspace = true } +rust-version = { workspace = true } + +[lib] +name = "app_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[features] +default = ["custom-protocol"] +custom-protocol = ["tauri/custom-protocol"] +verge-dev = ["clash_verge_logger/color"] +tauri-dev = ["clash-verge-logging/tauri-dev"] +tokio-trace = ["console-subscriber"] +clippy = ["tauri/test"] +tracing = [] [package.metadata.bundle] identifier = "io.github.clash-verge-rev.clash-verge-rev" [build-dependencies] -tauri-build = { version = "2.5.2", features = [] } +tauri-build = { version = "2.5.3", features = [] } [dependencies] -warp = { version = "0.4.2", features = ["server"] } -anyhow = "1.0.100" -open = "5.3.2" -log = "0.4.28" -dunce = "1.0.5" -nanoid = "0.4" -chrono = "0.4.42" -sysinfo = { version = "0.37.2", features = ["network", "system"] } -boa_engine = "0.21.0" -serde_json = "1.0.145" -serde_yaml_ng = "0.10.0" -once_cell = { version = "1.21.3", features = ["parking_lot"] } -port_scanner = "0.1.5" -delay_timer = "0.11.6" -parking_lot = { version = "0.12.5", features = ["hardware-lock-elision"] } -percent-encoding = "2.3.2" -tokio = { version = "1.48.0", features = [ - "rt-multi-thread", - "macros", - "time", - "sync", -] } -serde = { version = "1.0.228", features = ["derive"] } -reqwest = { version = "0.12.24", features = ["json", "cookies"] } -regex = "1.12.2" -sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" } -tauri = { version = "2.9.3", features = [ +clash-verge-draft = { workspace = true } +clash-verge-logging = { workspace = true } +clash-verge-signal = { workspace = true } +clash-verge-types = { workspace = true } +tauri-plugin-clash-verge-sysinfo = { workspace = true } +tauri-plugin-clipboard-manager = { workspace = true } +tauri = { workspace = true, features = [ "protocol-asset", "devtools", "tray-icon", "image-ico", "image-png", ] } +parking_lot = { workspace = true } +anyhow = { workspace = true } +tokio = { workspace = true } +compact_str = { workspace = true } +flexi_logger = { workspace = true } +log = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +serde_yaml_ng = { workspace = true } +smartstring = { workspace = true, features = ["serde"] } +warp = { version = "0.4.2", features = ["server"] } +open = "5.3.3" +dunce = "1.0.5" +nanoid = "0.4" +chrono = "0.4.42" +boa_engine = "0.21.0" +once_cell = { version = "1.21.3", features = ["parking_lot"] } +port_scanner = "0.1.5" +delay_timer = "0.11.6" +percent-encoding = "2.3.2" +reqwest = { version = "0.12.24", features = ["json", "cookies", "rustls-tls"] } +regex = "1.12.2" +sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs", features = [ + "guard", +] } network-interface = { version = "2.0.3", features = ["serde"] } tauri-plugin-shell = "2.3.3" tauri-plugin-dialog = "2.4.2" tauri-plugin-fs = "2.4.4" tauri-plugin-process = "2.3.1" -tauri-plugin-clipboard-manager = "2.3.2" tauri-plugin-deep-link = "2.4.5" tauri-plugin-window-state = "2.4.1" zip = "6.0.0" @@ -65,30 +82,28 @@ base64 = "0.22.1" getrandom = "0.3.4" futures = "0.3.31" sys-locale = "0.3.2" -libc = "0.2.177" gethostname = "1.1.0" scopeguard = "1.2.0" tauri-plugin-notification = "2.3.3" tokio-stream = "0.1.17" backoff = { version = "0.4.0", features = ["tokio"] } -compact_str = { version = "0.9.0", features = ["serde"] } tauri-plugin-http = "2.5.4" -flexi_logger = "0.31.7" console-subscriber = { version = "0.5.0", optional = true } tauri-plugin-devtools = { version = "2.0.1" } tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo" } clash_verge_logger = { git = "https://github.com/clash-verge-rev/clash-verge-logger" } async-trait = "0.1.89" -smartstring = { version = "1.0.1", features = ["serde"] } clash_verge_service_ipc = { version = "2.0.21", features = [ "client", ], git = "https://github.com/clash-verge-rev/clash-verge-service-ipc" } arc-swap = "1.7.1" rust-i18n = "3.1.5" +rust_iso3166 = "0.1.14" +dark-light = "2.0.0" [target.'cfg(windows)'.dependencies] +deelevate = { workspace = true } runas = "=1.2.0" -deelevate = "0.2.0" winreg = "0.55.0" winapi = { version = "0.3.9", features = [ "winbase", @@ -103,154 +118,14 @@ winapi = { version = "0.3.9", features = [ "winhttp", "winreg", ] } -windows-sys = { version = "0.61.2", features = [ - "Win32_Foundation", - "Win32_Graphics_Gdi", - "Win32_System_SystemServices", - "Win32_UI_WindowsAndMessaging", -] } - -[target.'cfg(unix)'.dependencies] -signal-hook = "0.3.18" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-autostart = "2.5.1" tauri-plugin-global-shortcut = "2.3.1" tauri-plugin-updater = "2.9.0" -[features] -default = ["custom-protocol"] -custom-protocol = ["tauri/custom-protocol"] -verge-dev = ["clash_verge_logger/color"] -tauri-dev = [] -tokio-trace = ["console-subscriber"] -clippy = ["tauri/test"] -tracing = [] - -[[bench]] -name = "draft_benchmark" -path = "benches/draft_benchmark.rs" -harness = false - -[profile.release] -panic = "abort" -codegen-units = 1 -lto = "thin" -opt-level = 3 -debug = false -strip = true -overflow-checks = false -rpath = false - -[profile.dev] -incremental = true -codegen-units = 64 -opt-level = 0 -debug = true -strip = "none" -overflow-checks = true -lto = false -rpath = false - -[profile.fast-release] -inherits = "release" -codegen-units = 64 -incremental = true -lto = false -opt-level = 0 -debug = true -strip = false - -[lib] -name = "app_lib" -crate-type = ["staticlib", "cdylib", "rlib"] - [dev-dependencies] -criterion = { version = "0.7.0", features = ["async_tokio"] } +criterion = { workspace = true } -[lints.clippy] -# Core categories - most important for code safety and correctness -correctness = { level = "deny", priority = -1 } -suspicious = { level = "deny", priority = -1 } - -# Critical safety lints - warn for now due to extensive existing usage -unwrap_used = "warn" -expect_used = "warn" -panic = "deny" -unimplemented = "deny" - -# Development quality lints -todo = "warn" -dbg_macro = "warn" - -# 我们期望所有输出方式通过 logging 模块进行统一管理 -# print_stdout = "deny" -# print_stderr = "deny" - -# Performance lints for proxy application -clone_on_ref_ptr = "warn" -rc_clone_in_vec_init = "warn" -large_stack_arrays = "warn" -large_const_arrays = "warn" - -# Security lints -#integer_division = "warn" -#lossy_float_literal = "warn" -#default_numeric_fallback = "warn" - -# Mutex and async lints - strict control -async_yields_async = "deny" # Prevents missing await in async blocks -mutex_atomic = "deny" # Use atomics instead of Mutex -mutex_integer = "deny" # Use AtomicInt instead of Mutex -rc_mutex = "deny" # Single-threaded Rc with Mutex is wrong -unused_async = "deny" # Too many false positives in Tauri/framework code -await_holding_lock = "deny" -large_futures = "deny" -future_not_send = "deny" - -# Common style improvements -redundant_else = "deny" # Too many in existing code -needless_continue = "deny" # Too many in existing code -needless_raw_string_hashes = "deny" # Too many in existing code - -# Disable noisy categories for existing codebase but keep them available -#style = { level = "allow", priority = -1 } -#complexity = { level = "allow", priority = -1 } -#perf = { level = "allow", priority = -1 } -#pedantic = { level = "allow", priority = -1 } -#nursery = { level = "allow", priority = -1 } -#restriction = { level = "allow", priority = -1 } - -or_fun_call = "deny" -cognitive_complexity = "deny" -useless_let_if_seq = "deny" -use_self = "deny" -tuple_array_conversions = "deny" -trait_duplication_in_bounds = "deny" -suspicious_operation_groupings = "deny" -string_lit_as_bytes = "deny" -significant_drop_tightening = "deny" -significant_drop_in_scrutinee = "deny" -redundant_clone = "deny" -# option_if_let_else = "deny" // 过于激进,暂时不开启 -needless_pass_by_ref_mut = "deny" -needless_collect = "deny" -missing_const_for_fn = "deny" -iter_with_drain = "deny" -iter_on_single_items = "deny" -iter_on_empty_collections = "deny" -# fallible_impl_from = "deny" // 过于激进,暂时不开启 -equatable_if_let = "deny" -collection_is_never_read = "deny" -branches_sharing_code = "deny" -pathbuf_init_then_push = "deny" -option_as_ref_cloned = "deny" -large_types_passed_by_value = "deny" -# implicit_clone = "deny" // 可能会造成额外开销,暂时不开启 -expl_impl_clone_on_copy = "deny" -copy_iterator = "deny" -cloned_instead_of_copied = "deny" -# self_only_used_in_recursion = "deny" // Since 1.92.0 -unnecessary_self_imports = "deny" -unused_trait_names = "deny" -wildcard_imports = "deny" +[lints] +workspace = true diff --git a/clash-verge-rev/src-tauri/locales/ar.yml b/clash-verge-rev/src-tauri/locales/ar.yml index a054627f71..bd2ef91fd1 100644 --- a/clash-verge-rev/src-tauri/locales/ar.yml +++ b/clash-verge-rev/src-tauri/locales/ar.yml @@ -15,6 +15,9 @@ notifications: lightweightModeEntered: title: Lightweight Mode body: Entered lightweight mode. + profilesReactivated: + title: Profiles + body: Profile Reactivated. appQuit: title: About to Exit body: Clash Verge is about to exit. diff --git a/clash-verge-rev/src-tauri/locales/de.yml b/clash-verge-rev/src-tauri/locales/de.yml index 2cda8afe80..4bef5d8bd7 100644 --- a/clash-verge-rev/src-tauri/locales/de.yml +++ b/clash-verge-rev/src-tauri/locales/de.yml @@ -15,6 +15,9 @@ notifications: lightweightModeEntered: title: Lightweight Mode body: Entered lightweight mode. + profilesReactivated: + title: Profiles + body: Profile Reactivated. appQuit: title: About to Exit body: Clash Verge is about to exit. diff --git a/clash-verge-rev/src-tauri/locales/en.yml b/clash-verge-rev/src-tauri/locales/en.yml index a054627f71..bd2ef91fd1 100644 --- a/clash-verge-rev/src-tauri/locales/en.yml +++ b/clash-verge-rev/src-tauri/locales/en.yml @@ -15,6 +15,9 @@ notifications: lightweightModeEntered: title: Lightweight Mode body: Entered lightweight mode. + profilesReactivated: + title: Profiles + body: Profile Reactivated. appQuit: title: About to Exit body: Clash Verge is about to exit. diff --git a/clash-verge-rev/src-tauri/locales/es.yml b/clash-verge-rev/src-tauri/locales/es.yml index 974899b15f..6616029e50 100644 --- a/clash-verge-rev/src-tauri/locales/es.yml +++ b/clash-verge-rev/src-tauri/locales/es.yml @@ -15,6 +15,9 @@ notifications: lightweightModeEntered: title: Lightweight Mode body: Entered lightweight mode. + profilesReactivated: + title: Profiles + body: Profile Reactivated. appQuit: title: About to Exit body: Clash Verge is about to exit. diff --git a/clash-verge-rev/src-tauri/locales/fa.yml b/clash-verge-rev/src-tauri/locales/fa.yml index a054627f71..bd2ef91fd1 100644 --- a/clash-verge-rev/src-tauri/locales/fa.yml +++ b/clash-verge-rev/src-tauri/locales/fa.yml @@ -15,6 +15,9 @@ notifications: lightweightModeEntered: title: Lightweight Mode body: Entered lightweight mode. + profilesReactivated: + title: Profiles + body: Profile Reactivated. appQuit: title: About to Exit body: Clash Verge is about to exit. diff --git a/clash-verge-rev/src-tauri/locales/id.yml b/clash-verge-rev/src-tauri/locales/id.yml index 587534e9c9..1a095afc94 100644 --- a/clash-verge-rev/src-tauri/locales/id.yml +++ b/clash-verge-rev/src-tauri/locales/id.yml @@ -15,6 +15,9 @@ notifications: lightweightModeEntered: title: Lightweight Mode body: Entered lightweight mode. + profilesReactivated: + title: Profiles + body: Profile Reactivated. appQuit: title: About to Exit body: Clash Verge is about to exit. diff --git a/clash-verge-rev/src-tauri/locales/jp.yml b/clash-verge-rev/src-tauri/locales/jp.yml index 4ed031a4a0..c8bb594121 100644 --- a/clash-verge-rev/src-tauri/locales/jp.yml +++ b/clash-verge-rev/src-tauri/locales/jp.yml @@ -15,6 +15,9 @@ notifications: lightweightModeEntered: title: Lightweight Mode body: Entered lightweight mode. + profilesReactivated: + title: Profiles + body: Profile Reactivated. appQuit: title: About to Exit body: Clash Verge is about to exit. diff --git a/clash-verge-rev/src-tauri/locales/ko.yml b/clash-verge-rev/src-tauri/locales/ko.yml index 51dccf885d..ce4a451175 100644 --- a/clash-verge-rev/src-tauri/locales/ko.yml +++ b/clash-verge-rev/src-tauri/locales/ko.yml @@ -15,6 +15,9 @@ notifications: lightweightModeEntered: title: 경량 모드 body: 경량 모드에 진입했습니다. + profilesReactivated: + title: Profiles + body: Profile Reactivated. appQuit: title: 곧 종료 body: Clash Verge가 곧 종료됩니다. diff --git a/clash-verge-rev/src-tauri/locales/ru.yml b/clash-verge-rev/src-tauri/locales/ru.yml index cff8fe27c1..24cf7689ba 100644 --- a/clash-verge-rev/src-tauri/locales/ru.yml +++ b/clash-verge-rev/src-tauri/locales/ru.yml @@ -15,6 +15,9 @@ notifications: lightweightModeEntered: title: Lightweight Mode body: Entered lightweight mode. + profilesReactivated: + title: Profiles + body: Profile Reactivated. appQuit: title: About to Exit body: Clash Verge is about to exit. diff --git a/clash-verge-rev/src-tauri/locales/tr.yml b/clash-verge-rev/src-tauri/locales/tr.yml index f6ffb6895a..3b1df84080 100644 --- a/clash-verge-rev/src-tauri/locales/tr.yml +++ b/clash-verge-rev/src-tauri/locales/tr.yml @@ -15,6 +15,9 @@ notifications: lightweightModeEntered: title: Lightweight Mode body: Entered lightweight mode. + profilesReactivated: + title: Profiles + body: Profile Reactivated. appQuit: title: About to Exit body: Clash Verge is about to exit. diff --git a/clash-verge-rev/src-tauri/locales/tt.yml b/clash-verge-rev/src-tauri/locales/tt.yml index a054627f71..bd2ef91fd1 100644 --- a/clash-verge-rev/src-tauri/locales/tt.yml +++ b/clash-verge-rev/src-tauri/locales/tt.yml @@ -15,6 +15,9 @@ notifications: lightweightModeEntered: title: Lightweight Mode body: Entered lightweight mode. + profilesReactivated: + title: Profiles + body: Profile Reactivated. appQuit: title: About to Exit body: Clash Verge is about to exit. diff --git a/clash-verge-rev/src-tauri/locales/zh.yml b/clash-verge-rev/src-tauri/locales/zh.yml index 6b137faeb3..dd1fa4b537 100644 --- a/clash-verge-rev/src-tauri/locales/zh.yml +++ b/clash-verge-rev/src-tauri/locales/zh.yml @@ -15,6 +15,9 @@ notifications: lightweightModeEntered: title: 轻量模式 body: 已进入轻量模式。 + profilesReactivated: + title: 订阅 + body: 订阅已激活。 appQuit: title: 即将退出 body: Clash Verge 即将退出。 diff --git a/clash-verge-rev/src-tauri/locales/zhtw.yml b/clash-verge-rev/src-tauri/locales/zhtw.yml index 9e07ca7143..aaeec6c91f 100644 --- a/clash-verge-rev/src-tauri/locales/zhtw.yml +++ b/clash-verge-rev/src-tauri/locales/zhtw.yml @@ -15,6 +15,9 @@ notifications: lightweightModeEntered: title: 輕量模式 body: 已進入輕量模式。 + profilesReactivated: + title: 訂閱 + body: 訂閱已啟用。 appQuit: title: 即將退出 body: Clash Verge 即將退出。 diff --git a/clash-verge-rev/src-tauri/src/cmd/app.rs b/clash-verge-rev/src-tauri/src/cmd/app.rs index 4a806acb70..1e0f294227 100644 --- a/clash-verge-rev/src-tauri/src/cmd/app.rs +++ b/clash-verge-rev/src-tauri/src/cmd/app.rs @@ -3,12 +3,10 @@ use crate::core::sysopt::Sysopt; use crate::utils::resolve::ui::{self, UiReadyStage}; use crate::{ cmd::StringifyErr as _, - feat, logging, - utils::{ - dirs::{self, PathBufExec as _}, - logging::Type, - }, + feat, + utils::dirs::{self, PathBufExec as _}, }; +use clash_verge_logging::{Type, logging}; use smartstring::alias::String; use std::path::Path; use tauri::{AppHandle, Manager as _}; @@ -84,8 +82,8 @@ pub async fn restart_app() -> CmdResult<()> { /// 获取便携版标识 #[tauri::command] -pub fn get_portable_flag() -> CmdResult { - Ok(*dirs::PORTABLE_FLAG.get().unwrap_or(&false)) +pub fn get_portable_flag() -> bool { + *dirs::PORTABLE_FLAG.get().unwrap_or(&false) } /// 获取应用目录 @@ -241,16 +239,14 @@ pub async fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult CmdResult<()> { +pub fn notify_ui_ready() { logging!(info, Type::Cmd, "前端UI已准备就绪"); ui::mark_ui_ready(); - Ok(()) } /// UI加载阶段 #[tauri::command] -pub fn update_ui_stage(stage: UiReadyStage) -> CmdResult<()> { +pub fn update_ui_stage(stage: UiReadyStage) { logging!(info, Type::Cmd, "UI加载阶段更新: {:?}", &stage); ui::update_ui_ready_stage(stage); - Ok(()) } diff --git a/clash-verge-rev/src-tauri/src/cmd/clash.rs b/clash-verge-rev/src-tauri/src/cmd/clash.rs index ab37a4b3d7..830eb2c721 100644 --- a/clash-verge-rev/src-tauri/src/cmd/clash.rs +++ b/clash-verge-rev/src-tauri/src/cmd/clash.rs @@ -1,4 +1,5 @@ use super::CmdResult; +use crate::feat; use crate::utils::dirs; use crate::{ cmd::StringifyErr as _, @@ -6,7 +7,7 @@ use crate::{ constants, core::{CoreManager, handle, validate::CoreConfigValidator}, }; -use crate::{feat, logging, utils::logging::Type}; +use clash_verge_logging::{Type, logging, logging_error}; use compact_str::CompactString; use serde_yaml_ng::Mapping; use smartstring::alias::String; @@ -28,7 +29,7 @@ pub async fn get_clash_info() -> CmdResult { /// 修改Clash配置 #[tauri::command] pub async fn patch_clash_config(payload: Mapping) -> CmdResult { - feat::patch_clash(payload).await.stringify_err() + feat::patch_clash(&payload).await.stringify_err() } /// 修改Clash模式 @@ -45,6 +46,11 @@ pub async fn change_clash_core(clash_core: String) -> CmdResult> match CoreManager::global().change_core(&clash_core).await { Ok(_) => { + logging_error!( + Type::Core, + Config::profiles().await.latest_arc().save_file().await + ); + // 切换内核后重启内核 match CoreManager::global().restart_core().await { Ok(_) => { @@ -88,6 +94,10 @@ pub async fn start_core() -> CmdResult { /// 关闭核心 #[tauri::command] pub async fn stop_core() -> CmdResult { + logging_error!( + Type::Core, + Config::profiles().await.latest_arc().save_file().await + ); let result = CoreManager::global().stop_core().await.stringify_err(); if result.is_ok() { handle::Handle::refresh_clash(); @@ -98,6 +108,10 @@ pub async fn stop_core() -> CmdResult { /// 重启核心 #[tauri::command] pub async fn restart_core() -> CmdResult { + logging_error!( + Type::Core, + Config::profiles().await.latest_arc().save_file().await + ); let result = CoreManager::global().restart_core().await.stringify_err(); if result.is_ok() { handle::Handle::refresh_clash(); @@ -170,7 +184,7 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult { // 应用DNS配置到运行时配置 Config::runtime().await.edit_draft(|d| { - d.patch_config(patch); + d.patch_config(&patch); }); // 重新生成配置 diff --git a/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/bahamut.rs b/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/bahamut.rs index 9600117452..7ac98dc6c7 100644 --- a/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/bahamut.rs +++ b/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/bahamut.rs @@ -1,10 +1,9 @@ use std::sync::Arc; +use clash_verge_logging::{Type, logging}; use regex::Regex; use reqwest::{Client, cookie::Jar}; -use crate::{logging, utils::logging::Type}; - use super::UnlockItem; use super::utils::{country_code_to_emoji, get_local_date_string}; @@ -12,6 +11,7 @@ pub(super) async fn check_bahamut_anime(client: &Client) -> UnlockItem { let cookie_store = Arc::new(Jar::default()); let client_with_cookies = match Client::builder() + .use_rustls_tls() .user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36") .cookie_provider(Arc::clone(&cookie_store)) .build() { diff --git a/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/disney_plus.rs b/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/disney_plus.rs index dc18978eb1..084f44e90b 100644 --- a/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/disney_plus.rs +++ b/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/disney_plus.rs @@ -1,7 +1,7 @@ use regex::Regex; use reqwest::Client; -use crate::{logging, utils::logging::Type}; +use clash_verge_logging::{Type, logging}; use super::UnlockItem; use super::utils::{country_code_to_emoji, get_local_date_string}; diff --git a/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/gemini.rs b/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/gemini.rs index 4701bb6ef0..e9412a1fb6 100644 --- a/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/gemini.rs +++ b/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/gemini.rs @@ -1,7 +1,7 @@ use regex::Regex; use reqwest::Client; -use crate::{logging, utils::logging::Type}; +use clash_verge_logging::{Type, logging}; use super::UnlockItem; use super::utils::{country_code_to_emoji, get_local_date_string}; diff --git a/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/mod.rs b/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/mod.rs index c01df29261..015078b0df 100644 --- a/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/mod.rs +++ b/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/mod.rs @@ -4,7 +4,7 @@ use reqwest::Client; use tauri::command; use tokio::{sync::Mutex, task::JoinSet}; -use crate::{logging, utils::logging::Type}; +use clash_verge_logging::{Type, logging}; mod bahamut; mod bilibili; @@ -42,6 +42,7 @@ pub async fn get_unlock_items() -> Result, String> { #[command] pub async fn check_media_unlock() -> Result, String> { let client = match Client::builder() + .use_rustls_tls() .user_agent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36") .timeout(std::time::Duration::from_secs(30)) .danger_accept_invalid_certs(true) diff --git a/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/netflix.rs b/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/netflix.rs index bc1bc30b29..63def9f5d8 100644 --- a/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/netflix.rs +++ b/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/netflix.rs @@ -1,7 +1,7 @@ use reqwest::Client; use serde_json::Value; -use crate::{logging, utils::logging::Type}; +use clash_verge_logging::{Type, logging}; use super::UnlockItem; use super::utils::{country_code_to_emoji, get_local_date_string}; diff --git a/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/prime_video.rs b/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/prime_video.rs index fb76723d1a..978c7d7c70 100644 --- a/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/prime_video.rs +++ b/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/prime_video.rs @@ -1,7 +1,7 @@ use regex::Regex; use reqwest::Client; -use crate::{logging, utils::logging::Type}; +use clash_verge_logging::{Type, logging}; use super::UnlockItem; use super::utils::{country_code_to_emoji, get_local_date_string}; diff --git a/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/types.rs b/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/types.rs index dd93d37816..fcb3ea23a5 100644 --- a/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/types.rs +++ b/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/types.rs @@ -26,7 +26,7 @@ const DEFAULT_UNLOCK_ITEM_NAMES: [&str; 13] = [ "ChatGPT Web", "Claude", "Gemini", - "Youtube Premium", + "YouTube Premium", "Bahamut Anime", "Netflix", "Disney+", diff --git a/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/utils.rs b/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/utils.rs index c1852b4d8d..0308b7dc03 100644 --- a/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/utils.rs +++ b/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/utils.rs @@ -1,4 +1,5 @@ use chrono::Local; +use rust_iso3166; pub fn get_local_date_string() -> String { let now = Local::now(); @@ -6,16 +7,70 @@ pub fn get_local_date_string() -> String { } pub fn country_code_to_emoji(country_code: &str) -> String { - let country_code = country_code.to_uppercase(); - if country_code.len() < 2 { - return String::new(); - } + let uc = country_code.to_ascii_uppercase(); - let bytes = country_code.as_bytes(); + // 长度校验:仅允许 2 或 3 + match uc.len() { + 2 => { + // 校验是否是合法 alpha2 + if rust_iso3166::from_alpha2(&uc).is_none() { + return String::new(); + } + alpha2_to_emoji(&uc) + } + 3 => { + // 转换并校验 alpha3 + match rust_iso3166::from_alpha3(&uc) { + Some(c) => { + let alpha2 = c.alpha2.to_ascii_uppercase(); + alpha2_to_emoji(&alpha2) + } + None => String::new(), + } + } + _ => String::new(), + } +} + +fn alpha2_to_emoji(alpha2: &str) -> String { + let bytes = alpha2.as_bytes(); let c1 = 0x1F1E6 + (bytes[0] as u32) - ('A' as u32); let c2 = 0x1F1E6 + (bytes[1] as u32) - ('A' as u32); - char::from_u32(c1) - .and_then(|c1| char::from_u32(c2).map(|c2| format!("{c1}{c2}"))) + .and_then(|x| char::from_u32(c2).map(|y| format!("{x}{y}"))) .unwrap_or_default() } + +#[cfg(test)] +mod tests { + use super::country_code_to_emoji; + + #[test] + fn country_code_to_emoji_iso2() { + assert_eq!(country_code_to_emoji("CN"), "🇨🇳"); + assert_eq!(country_code_to_emoji("us"), "🇺🇸"); + } + + #[test] + fn country_code_to_emoji_iso3() { + assert_eq!(country_code_to_emoji("CHN"), "🇨🇳"); + assert_eq!(country_code_to_emoji("USA"), "🇺🇸"); + } + + #[test] + fn country_code_to_emoji_invalid() { + assert_eq!(country_code_to_emoji("XXX"), ""); + assert_eq!(country_code_to_emoji("ZZ"), ""); + } + + #[test] + fn country_code_to_emoji_short() { + assert_eq!(country_code_to_emoji("C"), ""); + assert_eq!(country_code_to_emoji(""), ""); + } + + #[test] + fn country_code_to_emoji_long() { + assert_eq!(country_code_to_emoji("CNAAA"), ""); + } +} diff --git a/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/youtube.rs b/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/youtube.rs index d7caf6f45f..116335d65b 100644 --- a/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/youtube.rs +++ b/clash-verge-rev/src-tauri/src/cmd/media_unlock_checker/youtube.rs @@ -1,7 +1,7 @@ use regex::Regex; use reqwest::Client; -use crate::{logging, utils::logging::Type}; +use clash_verge_logging::{Type, logging}; use super::UnlockItem; use super::utils::{country_code_to_emoji, get_local_date_string}; @@ -42,14 +42,14 @@ pub(super) async fn check_youtube_premium(client: &Client) -> UnlockItem { } UnlockItem { - name: "Youtube Premium".to_string(), + name: "YouTube Premium".to_string(), status: status.to_string(), region, check_time: Some(get_local_date_string()), } } else { UnlockItem { - name: "Youtube Premium".to_string(), + name: "YouTube Premium".to_string(), status: "Failed".to_string(), region: None, check_time: Some(get_local_date_string()), @@ -57,7 +57,7 @@ pub(super) async fn check_youtube_premium(client: &Client) -> UnlockItem { } } Err(_) => UnlockItem { - name: "Youtube Premium".to_string(), + name: "YouTube Premium".to_string(), status: "Failed".to_string(), region: None, check_time: Some(get_local_date_string()), diff --git a/clash-verge-rev/src-tauri/src/cmd/network.rs b/clash-verge-rev/src-tauri/src/cmd/network.rs index be2fccff8f..b3d85b8ec5 100644 --- a/clash-verge-rev/src-tauri/src/cmd/network.rs +++ b/clash-verge-rev/src-tauri/src/cmd/network.rs @@ -1,33 +1,37 @@ use super::CmdResult; use crate::cmd::StringifyErr as _; -use crate::core::{EventDrivenProxyManager, async_proxy_query::AsyncProxyQuery}; -use crate::process::AsyncHandler; -use crate::{logging, utils::logging::Type}; +use clash_verge_logging::{Type, logging}; +use gethostname::gethostname; use network_interface::NetworkInterface; use serde_yaml_ng::Mapping; +use sysproxy::{Autoproxy, Sysproxy}; +use tauri_plugin_clash_verge_sysinfo; /// get the system proxy #[tauri::command] pub async fn get_sys_proxy() -> CmdResult { logging!(debug, Type::Network, "异步获取系统代理配置"); - let current = AsyncProxyQuery::get_system_proxy().await; + let sys_proxy = Sysproxy::get_system_proxy().stringify_err()?; + let Sysproxy { + ref host, + ref bypass, + ref port, + ref enable, + } = sys_proxy; let mut map = Mapping::new(); - map.insert("enable".into(), current.enable.into()); - map.insert( - "server".into(), - format!("{}:{}", current.host, current.port).into(), - ); - map.insert("bypass".into(), current.bypass.into()); + map.insert("enable".into(), (*enable).into()); + map.insert("server".into(), format!("{}:{}", host, port).into()); + map.insert("bypass".into(), bypass.as_str().into()); logging!( debug, Type::Network, "返回系统代理配置: enable={}, {}:{}", - current.enable, - current.host, - current.port + sys_proxy.enable, + sys_proxy.host, + sys_proxy.port ); Ok(map) } @@ -35,37 +39,31 @@ pub async fn get_sys_proxy() -> CmdResult { /// 获取自动代理配置 #[tauri::command] pub async fn get_auto_proxy() -> CmdResult { - logging!(debug, Type::Network, "开始获取自动代理配置(事件驱动)"); - - let proxy_manager = EventDrivenProxyManager::global(); - - let current = proxy_manager.get_auto_proxy_cached().await; - // 异步请求更新,立即返回缓存数据 - AsyncHandler::spawn(move || async move { - let _ = proxy_manager.get_auto_proxy_async().await; - }); + let auto_proxy = Autoproxy::get_auto_proxy().stringify_err()?; + let Autoproxy { + ref enable, + ref url, + } = auto_proxy; let mut map = Mapping::new(); - map.insert("enable".into(), current.enable.into()); - map.insert("url".into(), current.url.clone().into()); + map.insert("enable".into(), (*enable).into()); + map.insert("url".into(), url.as_str().into()); logging!( debug, Type::Network, "返回自动代理配置(缓存): enable={}, url={}", - current.enable, - current.url + auto_proxy.enable, + auto_proxy.url ); Ok(map) } /// 获取系统主机名 #[tauri::command] -pub fn get_system_hostname() -> CmdResult { - use gethostname::gethostname; - +pub fn get_system_hostname() -> String { // 获取系统主机名,处理可能的非UTF-8字符 - let hostname = match gethostname().into_string() { + match gethostname().into_string() { Ok(name) => name, Err(os_string) => { // 对于包含非UTF-8的主机名,使用调试格式化 @@ -73,21 +71,13 @@ pub fn get_system_hostname() -> CmdResult { // 去掉可能存在的引号 fallback.trim_matches('"').to_string() } - }; - - Ok(hostname) + } } /// 获取网络接口列表 #[tauri::command] pub fn get_network_interfaces() -> Vec { - use sysinfo::Networks; - let mut result = Vec::new(); - let networks = Networks::new_with_refreshed_list(); - for (interface_name, _) in &networks { - result.push(interface_name.clone()); - } - result + tauri_plugin_clash_verge_sysinfo::list_network_interfaces() } /// 获取网络接口详细信息 diff --git a/clash-verge-rev/src-tauri/src/cmd/profile.rs b/clash-verge-rev/src-tauri/src/cmd/profile.rs index edb94795a9..8610400668 100644 --- a/clash-verge-rev/src-tauri/src/cmd/profile.rs +++ b/clash-verge-rev/src-tauri/src/cmd/profile.rs @@ -10,12 +10,14 @@ use crate::{ profiles_append_item_safe, }, core::{CoreManager, handle, timer::Timer, tray::Tray}, - feat, logging, + feat, module::auto_backup::{AutoBackupManager, AutoBackupTrigger}, process::AsyncHandler, ret_err, - utils::{dirs, help, logging::Type}, + utils::{dirs, help}, }; +use clash_verge_draft::SharedBox; +use clash_verge_logging::{Type, logging}; use scopeguard::defer; use smartstring::alias::String; use std::sync::atomic::{AtomicBool, Ordering}; @@ -24,10 +26,10 @@ use std::time::Duration; static CURRENT_SWITCHING_PROFILE: AtomicBool = AtomicBool::new(false); #[tauri::command] -pub async fn get_profiles() -> CmdResult { +pub async fn get_profiles() -> CmdResult> { logging!(debug, Type::Cmd, "获取配置文件列表"); let draft = Config::profiles().await; - let data = (**draft.data_arc()).clone(); + let data = draft.data_arc(); Ok(data) } @@ -35,14 +37,29 @@ pub async fn get_profiles() -> CmdResult { #[tauri::command] pub async fn enhance_profiles() -> CmdResult { match feat::enhance_profiles().await { - Ok(_) => {} + Ok((true, _)) => { + handle::Handle::refresh_clash(); + Ok(()) + } + Ok((false, msg)) => { + let message: String = if msg.is_empty() { + "Failed to reactivate profiles".into() + } else { + msg + }; + logging!( + warn, + Type::Cmd, + "Reactivate profiles command failed validation: {}", + message.as_str() + ); + Err(message) + } Err(e) => { logging!(error, Type::Cmd, "{}", e); - return Err(e.to_string().into()); + Err(e.to_string().into()) } } - handle::Handle::refresh_clash(); - Ok(()) } /// 导入配置文件 @@ -76,7 +93,7 @@ pub async fn import_profile(url: std::string::String, option: Option) return Err(format!("导入订阅失败: {}", e).into()); } } - // 立即发送配置变更通知 + if let Some(uid) = &item.uid { logging!(info, Type::Cmd, "[导入订阅] 发送配置变更通知: {}", uid); handle::Handle::notify_profile_changed(uid.clone()); @@ -87,6 +104,7 @@ pub async fn import_profile(url: std::string::String, option: Option) if let Some(uid) = uid_clone { // 延迟发送,确保文件已完全写入 tokio::time::sleep(Duration::from_millis(100)).await; + logging!(info, Type::Cmd, "[导入订阅] 发送配置变更通知: {}", uid); handle::Handle::notify_profile_changed(uid); } @@ -119,9 +137,9 @@ pub async fn create_profile(item: PrfItem, file_data: Option) -> CmdResu match profiles_append_item_with_filedata_safe(&item, file_data).await { Ok(_) => { // 发送配置变更通知 - if let Some(uid) = &item.uid { + if let Some(uid) = item.uid.clone() { logging!(info, Type::Cmd, "[创建订阅] 发送配置变更通知: {}", uid); - handle::Handle::notify_profile_changed(uid.clone()); + handle::Handle::notify_profile_changed(uid); } Config::profiles().await.apply(); AutoBackupManager::trigger_backup(AutoBackupTrigger::ProfileChange); @@ -477,7 +495,7 @@ pub async fn view_profile(index: String) -> CmdResult { .get_item(&index) .stringify_err()? .file - .clone() + .as_ref() .ok_or("the file field is null")?; let path = dirs::app_profiles_dir() @@ -497,7 +515,11 @@ pub async fn read_profile_file(index: String) -> CmdResult { let profiles = Config::profiles().await; let profiles_ref = profiles.latest_arc(); PrfItem { - file: profiles_ref.get_item(&index).stringify_err()?.file.clone(), + file: profiles_ref + .get_item(&index) + .stringify_err()? + .file + .to_owned(), ..Default::default() } }; diff --git a/clash-verge-rev/src-tauri/src/cmd/proxy.rs b/clash-verge-rev/src-tauri/src/cmd/proxy.rs index 6f94f729cd..0d74667e50 100644 --- a/clash-verge-rev/src-tauri/src/cmd/proxy.rs +++ b/clash-verge-rev/src-tauri/src/cmd/proxy.rs @@ -1,5 +1,5 @@ use super::CmdResult; -use crate::{logging, utils::logging::Type}; +use clash_verge_logging::{Type, logging}; // TODO: 前端通过 emit 发送更新事件, tray 监听更新事件 /// 同步托盘和GUI的代理选择状态 diff --git a/clash-verge-rev/src-tauri/src/cmd/runtime.rs b/clash-verge-rev/src-tauri/src/cmd/runtime.rs index d75e470d0d..24f0cbec5c 100644 --- a/clash-verge-rev/src-tauri/src/cmd/runtime.rs +++ b/clash-verge-rev/src-tauri/src/cmd/runtime.rs @@ -1,14 +1,10 @@ use super::CmdResult; -use crate::{ - cmd::StringifyErr as _, - config::{Config, ConfigType}, - core::CoreManager, - log_err, -}; +use crate::{cmd::StringifyErr as _, config::Config, core::CoreManager}; use anyhow::{Context as _, anyhow}; +use clash_verge_logging::{Type, logging_error}; use serde_yaml_ng::Mapping; use smartstring::alias::String; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; /// 获取运行时配置 #[tauri::command] @@ -35,7 +31,7 @@ pub async fn get_runtime_yaml() -> CmdResult { /// 获取运行时存在的键 #[tauri::command] -pub async fn get_runtime_exists() -> CmdResult> { +pub async fn get_runtime_exists() -> CmdResult> { Ok(Config::runtime().await.latest_arc().exists_keys.clone()) } @@ -104,14 +100,9 @@ pub async fn update_proxy_chain_config_in_runtime( { let runtime = Config::runtime().await; runtime.edit_draft(|d| d.update_proxy_chain_config(proxy_chain_config)); - runtime.apply(); + // 我们需要在 CoreManager 中验证并应用配置,这里不应该直接调用 runtime.apply() } - - // 生成新的运行配置文件并通知 Clash 核心重新加载 - let run_path = Config::generate_file(ConfigType::Run) - .await - .stringify_err()?; - log_err!(CoreManager::global().put_configs_force(run_path).await); + logging_error!(Type::Core, CoreManager::global().update_config().await); Ok(()) } diff --git a/clash-verge-rev/src-tauri/src/cmd/save_profile.rs b/clash-verge-rev/src-tauri/src/cmd/save_profile.rs index ed271504bd..9ccbce15aa 100644 --- a/clash-verge-rev/src-tauri/src/cmd/save_profile.rs +++ b/clash-verge-rev/src-tauri/src/cmd/save_profile.rs @@ -3,10 +3,10 @@ use crate::{ cmd::StringifyErr as _, config::{Config, PrfItem}, core::{CoreManager, handle, validate::CoreConfigValidator}, - logging, module::auto_backup::{AutoBackupManager, AutoBackupTrigger}, - utils::{dirs, logging::Type}, + utils::dirs, }; +use clash_verge_logging::{Type, logging}; use smartstring::alias::String; use tokio::fs; diff --git a/clash-verge-rev/src-tauri/src/cmd/system.rs b/clash-verge-rev/src-tauri/src/cmd/system.rs index 27a4f5f6c4..6eaeb3f15b 100644 --- a/clash-verge-rev/src-tauri/src/cmd/system.rs +++ b/clash-verge-rev/src-tauri/src/cmd/system.rs @@ -1,64 +1,9 @@ use std::sync::Arc; -use super::CmdResult; -use crate::{ - core::{CoreManager, handle, manager::RunningMode}, - logging, - module::sysinfo::PlatformSpecification, - utils::logging::Type, -}; -#[cfg(target_os = "windows")] -use deelevate::{PrivilegeLevel, Token}; -use once_cell::sync::Lazy; -use tauri_plugin_clipboard_manager::ClipboardExt as _; -use tokio::time::Instant; - -// 存储应用启动时间的全局变量 -static APP_START_TIME: Lazy = Lazy::new(Instant::now); -#[cfg(not(target_os = "windows"))] -static APPS_RUN_AS_ADMIN: Lazy = Lazy::new(|| unsafe { libc::geteuid() } == 0); -#[cfg(target_os = "windows")] -static APPS_RUN_AS_ADMIN: Lazy = Lazy::new(|| { - Token::with_current_process() - .and_then(|token| token.privilege_level()) - .map(|level| level != PrivilegeLevel::NotPrivileged) - .unwrap_or(false) -}); - -#[tauri::command] -pub async fn export_diagnostic_info() -> CmdResult<()> { - let sysinfo = PlatformSpecification::new_sync(); - let info = format!("{sysinfo:?}"); - - let app_handle = handle::Handle::app_handle(); - let cliboard = app_handle.clipboard(); - if cliboard.write_text(info).is_err() { - logging!(error, Type::System, "Failed to write to clipboard"); - } - Ok(()) -} - -#[tauri::command] -pub async fn get_system_info() -> CmdResult { - let sysinfo = PlatformSpecification::new_sync(); - let info = format!("{sysinfo:?}"); - Ok(info) -} +use crate::core::{CoreManager, manager::RunningMode}; /// 获取当前内核运行模式 #[tauri::command] pub async fn get_running_mode() -> Result, String> { Ok(CoreManager::global().get_running_mode()) } - -/// 获取应用的运行时间(毫秒) -#[tauri::command] -pub fn get_app_uptime() -> CmdResult { - Ok(APP_START_TIME.elapsed().as_millis()) -} - -/// 检查应用是否以管理员身份运行 -#[tauri::command] -pub fn is_admin() -> CmdResult { - Ok(*APPS_RUN_AS_ADMIN) -} diff --git a/clash-verge-rev/src-tauri/src/cmd/uwp.rs b/clash-verge-rev/src-tauri/src/cmd/uwp.rs index 779029583a..d87254880e 100644 --- a/clash-verge-rev/src-tauri/src/cmd/uwp.rs +++ b/clash-verge-rev/src-tauri/src/cmd/uwp.rs @@ -17,6 +17,7 @@ mod platform { mod platform { use super::CmdResult; + #[allow(clippy::unnecessary_wraps)] pub const fn invoke_uwp_tool() -> CmdResult { Ok(()) } diff --git a/clash-verge-rev/src-tauri/src/cmd/validate.rs b/clash-verge-rev/src-tauri/src/cmd/validate.rs index a0134f3403..ee80675be8 100644 --- a/clash-verge-rev/src-tauri/src/cmd/validate.rs +++ b/clash-verge-rev/src-tauri/src/cmd/validate.rs @@ -1,9 +1,6 @@ use super::CmdResult; -use crate::{ - core::{handle, validate::CoreConfigValidator}, - logging, - utils::logging::Type, -}; +use crate::core::{handle, validate::CoreConfigValidator}; +use clash_verge_logging::{Type, logging}; use smartstring::alias::String; /// 发送脚本验证通知消息 diff --git a/clash-verge-rev/src-tauri/src/cmd/verge.rs b/clash-verge-rev/src-tauri/src/cmd/verge.rs index 943eb9db65..a20ad6d8a6 100644 --- a/clash-verge-rev/src-tauri/src/cmd/verge.rs +++ b/clash-verge-rev/src-tauri/src/cmd/verge.rs @@ -1,5 +1,6 @@ use super::CmdResult; -use crate::{cmd::StringifyErr as _, config::IVerge, feat, utils::draft::SharedBox}; +use crate::{cmd::StringifyErr as _, config::IVerge, feat}; +use clash_verge_draft::SharedBox; /// 获取Verge配置 #[tauri::command] diff --git a/clash-verge-rev/src-tauri/src/config/clash.rs b/clash-verge-rev/src-tauri/src/config/clash.rs index 095a0b8125..f036c9ce61 100644 --- a/clash-verge-rev/src-tauri/src/config/clash.rs +++ b/clash-verge-rev/src-tauri/src/config/clash.rs @@ -2,8 +2,8 @@ use crate::config::Config; use crate::constants::{network, tun as tun_const}; use crate::utils::dirs::{ipc_path, path_to_str}; use crate::utils::{dirs, help}; -use crate::{logging, utils::logging::Type}; use anyhow::Result; +use clash_verge_logging::{Type, logging}; use serde::{Deserialize, Serialize}; use serde_yaml_ng::{Mapping, Value}; use std::{ @@ -16,7 +16,6 @@ pub struct IClashTemp(pub Mapping); impl IClashTemp { pub async fn new() -> Self { - let template = Self::template(); let clash_path_result = dirs::clash_path(); let map_result = if let Ok(path) = clash_path_result { help::read_mapping(&path).await @@ -26,24 +25,26 @@ impl IClashTemp { match map_result { Ok(mut map) => { - template.0.keys().for_each(|key| { - if !map.contains_key(key) - && let Some(value) = template.0.get(key) - { - map.insert(key.clone(), value.clone()); + let template_map = Self::template().0; + for (key, value) in template_map.into_iter() { + if !map.contains_key(&key) { + map.insert(key, value); } - }); + } + // 确保 secret 字段存在且不为空 - if let Some(Value::String(s)) = map.get_mut("secret") + if let Some(val) = map.get_mut("secret") + && let Value::String(s) = val && s.is_empty() { *s = "set-your-secret".into(); } + Self(Self::guard(map)) } Err(err) => { logging!(error, Type::Config, "{err}"); - template + Self::template() } } } @@ -144,9 +145,9 @@ impl IClashTemp { config } - pub fn patch_config(&mut self, patch: Mapping) { - for (key, value) in patch.into_iter() { - self.0.insert(key, value); + pub fn patch_config(&mut self, patch: &Mapping) { + for (key, value) in patch.iter() { + self.0.insert(key.to_owned(), value.to_owned()); } } diff --git a/clash-verge-rev/src-tauri/src/config/config.rs b/clash-verge-rev/src-tauri/src/config/config.rs index ff49022626..4c391de62d 100644 --- a/clash-verge-rev/src-tauri/src/config/config.rs +++ b/clash-verge-rev/src-tauri/src/config/config.rs @@ -1,16 +1,25 @@ -use super::{IClashTemp, IProfiles, IRuntime, IVerge}; +use super::{IClashTemp, IProfiles, IVerge}; use crate::{ - cmd, config::{PrfItem, profiles_append_item_safe}, constants::{files, timing}, - core::{CoreManager, handle, service, tray, validate::CoreConfigValidator}, - enhance, logging, logging_error, - utils::{Draft, dirs, help, logging::Type}, + core::{ + CoreManager, + handle::{self, Handle}, + service, tray, + validate::CoreConfigValidator, + }, + enhance, + process::AsyncHandler, + utils::{dirs, help}, }; use anyhow::{Result, anyhow}; use backoff::{Error as BackoffError, ExponentialBackoff}; +use clash_verge_draft::Draft; +use clash_verge_logging::{Type, logging, logging_error}; +use clash_verge_types::runtime::IRuntime; use smartstring::alias::String; use std::path::PathBuf; +use tauri_plugin_clash_verge_sysinfo::is_current_app_handle_admin; use tokio::sync::OnceCell; use tokio::time::sleep; @@ -57,9 +66,10 @@ impl Config { Self::ensure_default_profile_items().await?; // init Tun mode - if !cmd::system::is_admin().unwrap_or_default() - && service::is_service_available().await.is_err() - { + let handle = Handle::app_handle(); + let is_admin = is_current_app_handle_admin(handle); + let is_service_available = service::is_service_available().await.is_ok(); + if !is_admin && !is_service_available { let verge = Self::verge().await; verge.edit_draft(|d| { d.enable_tun_mode = Some(false); @@ -200,6 +210,32 @@ impl Config { logging!(error, Type::Setup, "Config init verification failed: {}", e); } } + + // 升级草稿为正式数据,并写入文件。避免用户行为丢失。 + // 仅在应用退出、重启、关机监听事件启用 + pub async fn apply_all_and_save_file() { + logging!(info, Type::Config, "save all draft data"); + let save_clash_task = AsyncHandler::spawn(|| async { + let clash = Self::clash().await; + clash.apply(); + logging_error!(Type::Config, clash.data_arc().save_config().await); + }); + + let save_verge_task = AsyncHandler::spawn(|| async { + let verge = Self::verge().await; + verge.apply(); + logging_error!(Type::Config, verge.data_arc().save_file().await); + }); + + let save_profiles_task = AsyncHandler::spawn(|| async { + let profiles = Self::profiles().await; + profiles.apply(); + logging_error!(Type::Config, profiles.data_arc().save_file().await); + }); + + let _ = tokio::join!(save_clash_task, save_verge_task, save_profiles_task); + logging!(info, Type::Config, "save all draft data finished"); + } } #[derive(Debug)] diff --git a/clash-verge-rev/src-tauri/src/config/mod.rs b/clash-verge-rev/src-tauri/src/config/mod.rs index 342c11f47d..cf935f2aed 100644 --- a/clash-verge-rev/src-tauri/src/config/mod.rs +++ b/clash-verge-rev/src-tauri/src/config/mod.rs @@ -4,10 +4,9 @@ mod config; mod encrypt; mod prfitem; pub mod profiles; -mod runtime; mod verge; -pub use self::{clash::*, config::*, encrypt::*, prfitem::*, profiles::*, runtime::*, verge::*}; +pub use self::{clash::*, config::*, encrypt::*, prfitem::*, profiles::*, verge::*}; pub const DEFAULT_PAC: &str = r#"function FindProxyForURL(url, host) { return "PROXY 127.0.0.1:%mixed-port%; SOCKS5 127.0.0.1:%mixed-port%; DIRECT;"; diff --git a/clash-verge-rev/src-tauri/src/config/prfitem.rs b/clash-verge-rev/src-tauri/src/config/prfitem.rs index bdbac01837..3e21dd583c 100644 --- a/clash-verge-rev/src-tauri/src/config/prfitem.rs +++ b/clash-verge-rev/src-tauri/src/config/prfitem.rs @@ -555,32 +555,33 @@ impl PrfItem { impl PrfItem { /// 获取current指向的订阅的merge - pub fn current_merge(&self) -> Option { - self.option.as_ref().and_then(|o| o.merge.clone()) + pub fn current_merge(&self) -> Option<&String> { + self.option.as_ref().and_then(|o| o.merge.as_ref()) } /// 获取current指向的订阅的script - pub fn current_script(&self) -> Option { - self.option.as_ref().and_then(|o| o.script.clone()) + pub fn current_script(&self) -> Option<&String> { + self.option.as_ref().and_then(|o| o.script.as_ref()) } /// 获取current指向的订阅的rules - pub fn current_rules(&self) -> Option { - self.option.as_ref().and_then(|o| o.rules.clone()) + pub fn current_rules(&self) -> Option<&String> { + self.option.as_ref().and_then(|o| o.rules.as_ref()) } /// 获取current指向的订阅的proxies - pub fn current_proxies(&self) -> Option { - self.option.as_ref().and_then(|o| o.proxies.clone()) + pub fn current_proxies(&self) -> Option<&String> { + self.option.as_ref().and_then(|o| o.proxies.as_ref()) } /// 获取current指向的订阅的groups - pub fn current_groups(&self) -> Option { - self.option.as_ref().and_then(|o| o.groups.clone()) + pub fn current_groups(&self) -> Option<&String> { + self.option.as_ref().and_then(|o| o.groups.as_ref()) } } // 向前兼容,默认为订阅启用自动更新 +#[allow(clippy::unnecessary_wraps)] const fn default_allow_auto_update() -> Option { Some(true) } diff --git a/clash-verge-rev/src-tauri/src/config/profiles.rs b/clash-verge-rev/src-tauri/src/config/profiles.rs index 0f8a836716..b70a209688 100644 --- a/clash-verge-rev/src-tauri/src/config/profiles.rs +++ b/clash-verge-rev/src-tauri/src/config/profiles.rs @@ -3,8 +3,8 @@ use crate::utils::{ dirs::{self, PathBufExec as _}, help, }; -use crate::{logging, utils::logging::Type}; use anyhow::{Context as _, Result, bail}; +use clash_verge_logging::{Type, logging}; use serde::{Deserialize, Serialize}; use serde_yaml_ng::Mapping; use smartstring::alias::String; @@ -21,6 +21,12 @@ pub struct IProfiles { pub items: Option>, } +pub struct IProfilePreview<'a> { + pub uid: &'a String, + pub name: &'a String, + pub is_current: bool, +} + /// 清理结果 #[derive(Debug, Clone)] pub struct CleanupResult { @@ -367,14 +373,20 @@ impl IProfiles { self.current.as_ref() == Some(index) } - /// 获取所有的profiles(uid,名称) - pub fn all_profile_uid_and_name(&self) -> Option> { + /// 获取所有的profiles(uid,名称, 是否为 current) + pub fn profiles_preview(&self) -> Option>> { self.items.as_ref().map(|items| { items .iter() .filter_map(|e| { if let (Some(uid), Some(name)) = (e.uid.as_ref(), e.name.as_ref()) { - Some((uid, name)) + let is_current = self.is_current_profile_index(uid); + let preview = IProfilePreview { + uid, + name, + is_current, + }; + Some(preview) } else { None } diff --git a/clash-verge-rev/src-tauri/src/config/runtime.rs b/clash-verge-rev/src-tauri/src/config/runtime.rs deleted file mode 100644 index e394d5d680..0000000000 --- a/clash-verge-rev/src-tauri/src/config/runtime.rs +++ /dev/null @@ -1,123 +0,0 @@ -use crate::enhance::field::use_keys; -use serde::{Deserialize, Serialize}; -use serde_yaml_ng::{Mapping, Value}; -use smartstring::alias::String; -use std::collections::HashMap; - -#[derive(Default, Debug, Clone, Deserialize, Serialize)] -pub struct IRuntime { - pub config: Option, - // 记录在订阅中(包括merge和script生成的)出现过的keys - // 这些keys不一定都生效 - pub exists_keys: Vec, - pub chain_logs: HashMap>, -} - -impl IRuntime { - pub fn new() -> Self { - Self::default() - } - - // 这里只更改 allow-lan | ipv6 | log-level | tun - pub fn patch_config(&mut self, patch: Mapping) { - if let Some(config) = self.config.as_mut() { - ["allow-lan", "ipv6", "log-level", "unified-delay"] - .into_iter() - .for_each(|key| { - if let Some(value) = patch.get(key).to_owned() { - config.insert(key.into(), value.clone()); - } - }); - - let patch_tun = patch.get("tun"); - if patch_tun.is_some() { - let tun = config.get("tun"); - let mut tun: Mapping = tun.map_or_else(Mapping::new, |val| { - val.as_mapping().cloned().unwrap_or_else(Mapping::new) - }); - let patch_tun = patch_tun.map_or_else(Mapping::new, |val| { - val.as_mapping().cloned().unwrap_or_else(Mapping::new) - }); - use_keys(&patch_tun).into_iter().for_each(|key| { - if let Some(value) = patch_tun.get(key.as_str()) { - tun.insert(Value::from(key.as_str()), value.clone()); - } - }); - - config.insert("tun".into(), Value::from(tun)); - } - } - } - - //跟新链式代理配置文件 - /// { - /// "proxies":[ - /// { - /// name : 入口节点, - /// type: xxx - /// server: xxx - /// port: xxx - /// ports: xxx - /// password: xxx - /// skip-cert-verify: xxx, - /// }, - /// { - /// name : hop_node_1_xxxx, - /// type: xxx - /// server: xxx - /// port: xxx - /// ports: xxx - /// password: xxx - /// skip-cert-verify: xxx, - /// dialer-proxy : "入口节点" - /// }, - /// { - /// name : 出口节点, - /// type: xxx - /// server: xxx - /// port: xxx - /// ports: xxx - /// password: xxx - /// skip-cert-verify: xxx, - /// dialer-proxy : "hop_node_1_xxxx" - /// } - /// ], - /// "proxy-groups" : [ - /// { - /// name : "proxy_chain", - /// type: "select", - /// proxies ["出口节点"] - /// } - /// ] - /// } - /// - /// 传入none 为删除 - pub fn update_proxy_chain_config(&mut self, proxy_chain_config: Option) { - if let Some(config) = self.config.as_mut() { - if let Some(Value::Sequence(proxies)) = config.get_mut("proxies") { - proxies.iter_mut().for_each(|proxy| { - if let Some(proxy) = proxy.as_mapping_mut() - && proxy.get("dialer-proxy").is_some() - { - proxy.remove("dialer-proxy"); - } - }); - } - - if let Some(Value::Sequence(dialer_proxies)) = proxy_chain_config - && let Some(Value::Sequence(proxies)) = config.get_mut("proxies") - { - for (i, dialer_proxy) in dialer_proxies.iter().enumerate() { - if let Some(Value::Mapping(proxy)) = proxies - .iter_mut() - .find(|proxy| proxy.get("name") == Some(dialer_proxy)) - && i != 0 - && let Some(dialer_proxy) = dialer_proxies.get(i - 1) - { - proxy.insert("dialer-proxy".into(), dialer_proxy.to_owned()); - } - } - } - } - } -} diff --git a/clash-verge-rev/src-tauri/src/config/verge.rs b/clash-verge-rev/src-tauri/src/config/verge.rs index 70ad8964de..f47e4b02b7 100644 --- a/clash-verge-rev/src-tauri/src/config/verge.rs +++ b/clash-verge-rev/src-tauri/src/config/verge.rs @@ -1,10 +1,10 @@ use crate::config::Config; use crate::{ config::{DEFAULT_PAC, deserialize_encrypted, serialize_encrypted}, - logging, - utils::{dirs, help, i18n, logging::Type}, + utils::{dirs, help, i18n}, }; use anyhow::Result; +use clash_verge_logging::{Type, logging}; use log::LevelFilter; use serde::{Deserialize, Serialize}; use smartstring::alias::String; diff --git a/clash-verge-rev/src-tauri/src/constants.rs b/clash-verge-rev/src-tauri/src/constants.rs index a07da7c156..93ef8e8728 100644 --- a/clash-verge-rev/src-tauri/src/constants.rs +++ b/clash-verge-rev/src-tauri/src/constants.rs @@ -1,7 +1,6 @@ use std::time::Duration; pub mod network { - pub const DEFAULT_PROXY_HOST: &str = "127.0.0.1"; pub const DEFAULT_EXTERNAL_CONTROLLER: &str = "127.0.0.1:9097"; pub mod ports { @@ -20,23 +19,10 @@ pub mod network { } } -pub mod bypass { - #[cfg(target_os = "windows")] - pub const DEFAULT: &str = "localhost;127.*;192.168.*;10.*;172.16.*;172.17.*;172.18.*;172.19.*;172.20.*;172.21.*;172.22.*;172.23.*;172.24.*;172.25.*;172.26.*;172.27.*;172.28.*;172.29.*;172.30.*;172.31.*;"; - - #[cfg(target_os = "linux")] - pub const DEFAULT: &str = - "localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,::1"; - - #[cfg(target_os = "macos")] - pub const DEFAULT: &str = "127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,localhost,*.local,*.crashlytics.com,"; -} - pub mod timing { use super::Duration; - pub const CONFIG_UPDATE_DEBOUNCE: Duration = Duration::from_millis(500); - pub const CONFIG_RELOAD_DELAY: Duration = Duration::from_millis(300); + pub const CONFIG_UPDATE_DEBOUNCE: Duration = Duration::from_millis(300); pub const EVENT_EMIT_DELAY: Duration = Duration::from_millis(20); pub const STARTUP_ERROR_DELAY: Duration = Duration::from_secs(2); pub const ERROR_BATCH_DELAY: Duration = Duration::from_millis(300); @@ -58,15 +44,6 @@ pub mod files { pub const WINDOW_STATE: &str = "window_state.json"; } -pub mod error_patterns { - pub const CONNECTION_ERRORS: &[&str] = &[ - "Failed to create connection", - "The system cannot find the file specified", - "operation timed out", - "connection refused", - ]; -} - pub mod tun { pub const DEFAULT_STACK: &str = "gvisor"; diff --git a/clash-verge-rev/src-tauri/src/core/async_proxy_query.rs b/clash-verge-rev/src-tauri/src/core/async_proxy_query.rs deleted file mode 100644 index f0e579f6f7..0000000000 --- a/clash-verge-rev/src-tauri/src/core/async_proxy_query.rs +++ /dev/null @@ -1,565 +0,0 @@ -#[cfg(target_os = "windows")] -use crate::process::AsyncHandler; -use crate::{logging, utils::logging::Type}; -use anyhow::Result; -use serde::{Deserialize, Serialize}; -use tokio::time::{Duration, timeout}; - -#[cfg(target_os = "linux")] -use anyhow::anyhow; -#[cfg(not(target_os = "windows"))] -use tokio::process::Command; - -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct AsyncAutoproxy { - pub enable: bool, - pub url: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AsyncSysproxy { - pub enable: bool, - pub host: String, - pub port: u16, - pub bypass: String, -} - -impl Default for AsyncSysproxy { - fn default() -> Self { - Self { - enable: false, - host: "127.0.0.1".into(), - port: 7897, - bypass: String::new(), - } - } -} - -pub struct AsyncProxyQuery; - -impl AsyncProxyQuery { - /// 异步获取自动代理配置(PAC) - pub async fn get_auto_proxy() -> AsyncAutoproxy { - match timeout(Duration::from_secs(3), Self::get_auto_proxy_impl()).await { - Ok(Ok(proxy)) => { - logging!( - debug, - Type::Network, - "异步获取自动代理成功: enable={}, url={}", - proxy.enable, - proxy.url - ); - proxy - } - Ok(Err(e)) => { - logging!(warn, Type::Network, "Warning: 异步获取自动代理失败: {e}"); - AsyncAutoproxy::default() - } - Err(_) => { - logging!(warn, Type::Network, "Warning: 异步获取自动代理超时"); - AsyncAutoproxy::default() - } - } - } - - /// 异步获取系统代理配置 - pub async fn get_system_proxy() -> AsyncSysproxy { - match timeout(Duration::from_secs(3), Self::get_system_proxy_impl()).await { - Ok(Ok(proxy)) => { - logging!( - debug, - Type::Network, - "异步获取系统代理成功: enable={}, {}:{}", - proxy.enable, - proxy.host, - proxy.port - ); - proxy - } - Ok(Err(e)) => { - logging!(warn, Type::Network, "Warning: 异步获取系统代理失败: {e}"); - AsyncSysproxy::default() - } - Err(_) => { - logging!(warn, Type::Network, "Warning: 异步获取系统代理超时"); - AsyncSysproxy::default() - } - } - } - - #[cfg(target_os = "windows")] - async fn get_auto_proxy_impl() -> Result { - // Windows: 从注册表读取PAC配置 - AsyncHandler::spawn_blocking(move || -> Result { - Self::get_pac_config_from_registry() - }) - .await? - } - - #[cfg(target_os = "windows")] - fn get_pac_config_from_registry() -> Result { - use std::ptr; - use winapi::shared::minwindef::{DWORD, HKEY}; - use winapi::um::winnt::{KEY_READ, REG_DWORD, REG_SZ}; - use winapi::um::winreg::{HKEY_CURRENT_USER, RegCloseKey, RegOpenKeyExW, RegQueryValueExW}; - - unsafe { - let key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\0" - .encode_utf16() - .collect::>(); - - let mut hkey: HKEY = ptr::null_mut(); - let result = - RegOpenKeyExW(HKEY_CURRENT_USER, key_path.as_ptr(), 0, KEY_READ, &mut hkey); - - if result != 0 { - logging!(debug, Type::Network, "无法打开注册表项"); - return Ok(AsyncAutoproxy::default()); - } - - // 1. 检查自动配置是否启用 (AutoConfigURL 存在且不为空即表示启用) - let auto_config_url_name = "AutoConfigURL\0".encode_utf16().collect::>(); - let mut url_buffer = vec![0u16; 1024]; - let mut url_buffer_size: DWORD = (url_buffer.len() * 2) as DWORD; - let mut url_value_type: DWORD = 0; - - let url_query_result = RegQueryValueExW( - hkey, - auto_config_url_name.as_ptr(), - ptr::null_mut(), - &mut url_value_type, - url_buffer.as_mut_ptr() as *mut u8, - &mut url_buffer_size, - ); - - let mut pac_url = String::new(); - if url_query_result == 0 && url_value_type == REG_SZ && url_buffer_size > 0 { - let end_pos = url_buffer - .iter() - .position(|&x| x == 0) - .unwrap_or(url_buffer.len()); - pac_url = String::from_utf16_lossy(&url_buffer[..end_pos]); - logging!(debug, Type::Network, "从注册表读取到PAC URL: {pac_url}"); - } - - // 2. 检查自动检测设置是否启用 - let auto_detect_name = "AutoDetect\0".encode_utf16().collect::>(); - let mut auto_detect: DWORD = 0; - let mut detect_buffer_size: DWORD = 4; - let mut detect_value_type: DWORD = 0; - - let detect_query_result = RegQueryValueExW( - hkey, - auto_detect_name.as_ptr(), - ptr::null_mut(), - &mut detect_value_type, - &mut auto_detect as *mut DWORD as *mut u8, - &mut detect_buffer_size, - ); - - RegCloseKey(hkey); - - // PAC 启用的条件:AutoConfigURL 不为空,或 AutoDetect 被启用 - let pac_enabled = !pac_url.is_empty() - || (detect_query_result == 0 && detect_value_type == REG_DWORD && auto_detect != 0); - - if pac_enabled { - logging!( - debug, - Type::Network, - "PAC配置启用: URL={pac_url}, AutoDetect={auto_detect}" - ); - - if pac_url.is_empty() && auto_detect != 0 { - pac_url = "auto-detect".into(); - } - - Ok(AsyncAutoproxy { - enable: true, - url: pac_url, - }) - } else { - logging!(debug, Type::Network, "PAC配置未启用"); - Ok(AsyncAutoproxy::default()) - } - } - } - - #[cfg(target_os = "macos")] - async fn get_auto_proxy_impl() -> Result { - // macOS: 使用 scutil --proxy 命令 - let output = Command::new("scutil").args(["--proxy"]).output().await?; - - if !output.status.success() { - return Ok(AsyncAutoproxy::default()); - } - - let stdout = String::from_utf8_lossy(&output.stdout); - crate::logging!( - debug, - crate::utils::logging::Type::Network, - "scutil output: {stdout}" - ); - - let mut pac_enabled = false; - let mut pac_url = String::new(); - - // 解析 scutil 输出 - for line in stdout.lines() { - let line = line.trim(); - if line.contains("ProxyAutoConfigEnable") && line.contains("1") { - pac_enabled = true; - } else if line.contains("ProxyAutoConfigURLString") { - // 正确解析包含冒号的URL - // 格式: "ProxyAutoConfigURLString : http://127.0.0.1:11233/commands/pac" - if let Some(colon_pos) = line.find(" : ") { - pac_url = line[colon_pos + 3..].trim().into(); - } - } - } - - crate::logging!( - debug, - crate::utils::logging::Type::Network, - "解析结果: pac_enabled={pac_enabled}, pac_url={pac_url}" - ); - - Ok(AsyncAutoproxy { - enable: pac_enabled && !pac_url.is_empty(), - url: pac_url, - }) - } - - #[cfg(target_os = "linux")] - async fn get_auto_proxy_impl() -> Result { - // Linux: 检查环境变量和GNOME设置 - - // 首先检查环境变量 - if let Ok(auto_proxy) = std::env::var("auto_proxy") - && !auto_proxy.is_empty() - { - return Ok(AsyncAutoproxy { - enable: true, - url: auto_proxy, - }); - } - - // 尝试使用 gsettings 获取 GNOME 代理设置 - let output = Command::new("gsettings") - .args(["get", "org.gnome.system.proxy", "mode"]) - .output() - .await; - - if let Ok(output) = output - && output.status.success() - { - let mode: String = String::from_utf8_lossy(&output.stdout).trim().into(); - if mode.contains("auto") { - // 获取 PAC URL - let pac_output = Command::new("gsettings") - .args(["get", "org.gnome.system.proxy", "autoconfig-url"]) - .output() - .await; - - if let Ok(pac_output) = pac_output - && pac_output.status.success() - { - let pac_url: String = String::from_utf8_lossy(&pac_output.stdout) - .trim() - .trim_matches('\'') - .trim_matches('"') - .into(); - - if !pac_url.is_empty() { - return Ok(AsyncAutoproxy { - enable: true, - url: pac_url, - }); - } - } - } - } - - Ok(AsyncAutoproxy::default()) - } - - #[cfg(target_os = "windows")] - async fn get_system_proxy_impl() -> Result { - // Windows: 使用注册表直接读取代理设置 - AsyncHandler::spawn_blocking(move || -> Result { - Self::get_system_proxy_from_registry() - }) - .await? - } - - #[cfg(target_os = "windows")] - fn get_system_proxy_from_registry() -> Result { - use std::ptr; - use winapi::shared::minwindef::{DWORD, HKEY}; - use winapi::um::winnt::{KEY_READ, REG_DWORD, REG_SZ}; - use winapi::um::winreg::{HKEY_CURRENT_USER, RegCloseKey, RegOpenKeyExW, RegQueryValueExW}; - - unsafe { - let key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\0" - .encode_utf16() - .collect::>(); - - let mut hkey: HKEY = ptr::null_mut(); - let result = - RegOpenKeyExW(HKEY_CURRENT_USER, key_path.as_ptr(), 0, KEY_READ, &mut hkey); - - if result != 0 { - return Ok(AsyncSysproxy::default()); - } - - // 检查代理是否启用 - let proxy_enable_name = "ProxyEnable\0".encode_utf16().collect::>(); - let mut proxy_enable: DWORD = 0; - let mut buffer_size: DWORD = 4; - let mut value_type: DWORD = 0; - - let enable_result = RegQueryValueExW( - hkey, - proxy_enable_name.as_ptr(), - ptr::null_mut(), - &mut value_type, - &mut proxy_enable as *mut DWORD as *mut u8, - &mut buffer_size, - ); - - if enable_result != 0 || value_type != REG_DWORD || proxy_enable == 0 { - RegCloseKey(hkey); - return Ok(AsyncSysproxy::default()); - } - - // 读取代理服务器设置 - let proxy_server_name = "ProxyServer\0".encode_utf16().collect::>(); - let mut buffer = vec![0u16; 1024]; - let mut buffer_size: DWORD = (buffer.len() * 2) as DWORD; - let mut value_type: DWORD = 0; - - let server_result = RegQueryValueExW( - hkey, - proxy_server_name.as_ptr(), - ptr::null_mut(), - &mut value_type, - buffer.as_mut_ptr() as *mut u8, - &mut buffer_size, - ); - - let proxy_server = if server_result == 0 && value_type == REG_SZ && buffer_size > 0 { - let end_pos = buffer.iter().position(|&x| x == 0).unwrap_or(buffer.len()); - String::from_utf16_lossy(&buffer[..end_pos]) - } else { - String::new() - }; - - // 读取代理绕过列表 - let proxy_override_name = "ProxyOverride\0".encode_utf16().collect::>(); - let mut bypass_buffer = vec![0u16; 1024]; - let mut bypass_buffer_size: DWORD = (bypass_buffer.len() * 2) as DWORD; - let mut bypass_value_type: DWORD = 0; - - let override_result = RegQueryValueExW( - hkey, - proxy_override_name.as_ptr(), - ptr::null_mut(), - &mut bypass_value_type, - bypass_buffer.as_mut_ptr() as *mut u8, - &mut bypass_buffer_size, - ); - - let bypass_list = - if override_result == 0 && bypass_value_type == REG_SZ && bypass_buffer_size > 0 { - let end_pos = bypass_buffer - .iter() - .position(|&x| x == 0) - .unwrap_or(bypass_buffer.len()); - String::from_utf16_lossy(&bypass_buffer[..end_pos]) - } else { - String::new() - }; - - RegCloseKey(hkey); - - if !proxy_server.is_empty() { - // 解析服务器地址和端口 - let (host, port) = if let Some(colon_pos) = proxy_server.rfind(':') { - let host = proxy_server[..colon_pos].into(); - let port = proxy_server[colon_pos + 1..].parse::().unwrap_or(8080); - (host, port) - } else { - (proxy_server, 8080) - }; - - logging!( - debug, - Type::Network, - "从注册表读取到代理设置: {host}:{port}, bypass: {bypass_list}" - ); - - Ok(AsyncSysproxy { - enable: true, - host, - port, - bypass: bypass_list, - }) - } else { - Ok(AsyncSysproxy::default()) - } - } - } - - #[cfg(target_os = "macos")] - async fn get_system_proxy_impl() -> Result { - let output = Command::new("scutil").args(["--proxy"]).output().await?; - - if !output.status.success() { - return Ok(AsyncSysproxy::default()); - } - - let stdout = String::from_utf8_lossy(&output.stdout); - logging!(debug, Type::Network, "scutil proxy output: {stdout}"); - - let mut http_enabled = false; - let mut http_host = String::new(); - let mut http_port = 8080u16; - let mut exceptions: Vec = Vec::new(); - - for line in stdout.lines() { - let line = line.trim(); - if line.contains("HTTPEnable") && line.contains("1") { - http_enabled = true; - } else if line.contains("HTTPProxy") && !line.contains("Port") { - if let Some(host_part) = line.split(':').nth(1) { - http_host = host_part.trim().into(); - } - } else if line.contains("HTTPPort") { - if let Some(port_part) = line.split(':').nth(1) - && let Ok(port) = port_part.trim().parse::() - { - http_port = port; - } - } else if line.contains("ExceptionsList") { - // 解析异常列表 - if let Some(list_part) = line.split(':').nth(1) { - let list = list_part.trim(); - if !list.is_empty() { - exceptions.push(list.into()); - } - } - } - } - - Ok(AsyncSysproxy { - enable: http_enabled && !http_host.is_empty(), - host: http_host, - port: http_port, - bypass: exceptions.join(","), - }) - } - - #[cfg(target_os = "linux")] - async fn get_system_proxy_impl() -> Result { - // Linux: 检查环境变量和桌面环境设置 - - // 首先检查环境变量 - if let Ok(http_proxy) = std::env::var("http_proxy") - && let Ok(proxy_info) = Self::parse_proxy_url(&http_proxy) - { - return Ok(proxy_info); - } - - if let Ok(https_proxy) = std::env::var("https_proxy") - && let Ok(proxy_info) = Self::parse_proxy_url(&https_proxy) - { - return Ok(proxy_info); - } - - // 尝试使用 gsettings 获取 GNOME 代理设置 - let mode_output = Command::new("gsettings") - .args(["get", "org.gnome.system.proxy", "mode"]) - .output() - .await; - - if let Ok(mode_output) = mode_output - && mode_output.status.success() - { - let mode: String = String::from_utf8_lossy(&mode_output.stdout).trim().into(); - if mode.contains("manual") { - // 获取HTTP代理设置 - let host_result = Command::new("gsettings") - .args(["get", "org.gnome.system.proxy.http", "host"]) - .output() - .await; - - let port_result = Command::new("gsettings") - .args(["get", "org.gnome.system.proxy.http", "port"]) - .output() - .await; - - if let (Ok(host_output), Ok(port_output)) = (host_result, port_result) - && host_output.status.success() - && port_output.status.success() - { - let host: String = String::from_utf8_lossy(&host_output.stdout) - .trim() - .trim_matches('\'') - .trim_matches('"') - .into(); - - let port = String::from_utf8_lossy(&port_output.stdout) - .trim() - .parse::() - .unwrap_or(8080); - - if !host.is_empty() { - return Ok(AsyncSysproxy { - enable: true, - host, - port, - bypass: String::new(), - }); - } - } - } - } - - Ok(AsyncSysproxy::default()) - } - - #[cfg(target_os = "linux")] - fn parse_proxy_url(proxy_url: &str) -> Result { - // 解析形如 "http://proxy.example.com:8080" 的URL - let url = proxy_url.trim(); - - // 移除协议前缀 - let url = if let Some(stripped) = url.strip_prefix("http://") { - stripped - } else if let Some(stripped) = url.strip_prefix("https://") { - stripped - } else { - url - }; - - // 解析主机和端口 - let (host, port) = if let Some(colon_pos) = url.rfind(':') { - let host: String = url[..colon_pos].into(); - let port = url[colon_pos + 1..].parse::().unwrap_or(8080); - (host, port) - } else { - (url.into(), 8080) - }; - - if host.is_empty() { - return Err(anyhow!("无效的代理URL")); - } - - Ok(AsyncSysproxy { - enable: true, - host, - port, - bypass: std::env::var("no_proxy").unwrap_or_default(), - }) - } -} diff --git a/clash-verge-rev/src-tauri/src/core/backup.rs b/clash-verge-rev/src-tauri/src/core/backup.rs index ad3168546e..08476d5ccc 100644 --- a/clash-verge-rev/src-tauri/src/core/backup.rs +++ b/clash-verge-rev/src-tauri/src/core/backup.rs @@ -1,12 +1,8 @@ use crate::constants::files::DNS_CONFIG; -use crate::{ - config::Config, - logging, - process::AsyncHandler, - utils::{dirs, logging::Type}, -}; +use crate::{config::Config, process::AsyncHandler, utils::dirs}; use anyhow::Error; use arc_swap::{ArcSwap, ArcSwapOption}; +use clash_verge_logging::{Type, logging}; use once_cell::sync::OnceCell; use reqwest_dav::list_cmd::{ListEntity, ListFile}; use smartstring::alias::String; @@ -118,6 +114,7 @@ impl WebDavClient { let client = reqwest_dav::ClientBuilder::new() .set_agent( reqwest::Client::builder() + .use_rustls_tls() .danger_accept_invalid_certs(true) .timeout(Duration::from_secs(op.timeout())) .user_agent(format!("clash-verge/{APP_VERSION} ({OS} WebDAV-Client)")) diff --git a/clash-verge-rev/src-tauri/src/core/event_driven_proxy.rs b/clash-verge-rev/src-tauri/src/core/event_driven_proxy.rs deleted file mode 100644 index e03e9eecab..0000000000 --- a/clash-verge-rev/src-tauri/src/core/event_driven_proxy.rs +++ /dev/null @@ -1,548 +0,0 @@ -use std::sync::Arc; -use tokio::sync::RwLock; -use tokio::sync::{mpsc, oneshot}; -use tokio::time::{Duration, sleep, timeout}; -use tokio_stream::{StreamExt as _, wrappers::UnboundedReceiverStream}; - -use crate::config::{Config, IVerge}; -use crate::core::{async_proxy_query::AsyncProxyQuery, handle}; -use crate::process::AsyncHandler; -use crate::{logging, utils::logging::Type}; -use once_cell::sync::Lazy; -use smartstring::alias::String; -use sysproxy::{Autoproxy, Sysproxy}; - -#[derive(Debug, Clone)] -pub enum ProxyEvent { - /// 配置变更事件 - ConfigChanged, - /// 应用启动事件 - AppStarted, - /// 应用关闭事件 - AppStopping, -} - -#[derive(Debug, Clone)] -pub struct ProxyState { - pub sys_enabled: bool, - pub pac_enabled: bool, - pub auto_proxy: Autoproxy, - pub sys_proxy: Sysproxy, - pub last_updated: std::time::Instant, - pub is_healthy: bool, -} - -impl Default for ProxyState { - fn default() -> Self { - Self { - sys_enabled: false, - pac_enabled: false, - auto_proxy: Autoproxy { - enable: false, - url: "".into(), - }, - sys_proxy: Sysproxy { - enable: false, - host: "127.0.0.1".into(), - port: 7897, - bypass: "".into(), - }, - last_updated: std::time::Instant::now(), - is_healthy: true, - } - } -} - -pub struct EventDrivenProxyManager { - state: Arc>, - event_sender: mpsc::UnboundedSender, - query_sender: mpsc::UnboundedSender, -} - -#[derive(Debug)] -pub struct QueryRequest { - response_tx: oneshot::Sender, -} - -// 配置结构体移到外部 -struct ProxyConfig { - sys_enabled: bool, - pac_enabled: bool, - guard_enabled: bool, - guard_duration: u64, -} - -static PROXY_MANAGER: Lazy = Lazy::new(EventDrivenProxyManager::new); - -impl EventDrivenProxyManager { - pub fn global() -> &'static Self { - &PROXY_MANAGER - } - - fn new() -> Self { - let state = Arc::new(RwLock::new(ProxyState::default())); - let (event_tx, event_rx) = mpsc::unbounded_channel(); - let (query_tx, query_rx) = mpsc::unbounded_channel(); - - let state_clone = Arc::clone(&state); - AsyncHandler::spawn(move || Self::start_event_loop(state_clone, event_rx, query_rx)); - - Self { - state, - event_sender: event_tx, - query_sender: query_tx, - } - } - - /// 获取自动代理配置(缓存) - pub async fn get_auto_proxy_cached(&self) -> Autoproxy { - self.state.read().await.auto_proxy.clone() - } - - /// 异步获取最新的自动代理配置 - pub async fn get_auto_proxy_async(&self) -> Autoproxy { - let (tx, rx) = oneshot::channel(); - let query = QueryRequest { response_tx: tx }; - - if self.query_sender.send(query).is_err() { - logging!(error, Type::Network, "发送查询请求失败,返回缓存数据"); - return self.get_auto_proxy_cached().await; - } - - match timeout(Duration::from_secs(5), rx).await { - Ok(Ok(result)) => result, - _ => { - logging!(warn, Type::Network, "Warning: 查询超时,返回缓存数据"); - self.get_auto_proxy_cached().await - } - } - } - - /// 通知配置变更 - pub fn notify_config_changed(&self) { - self.send_event(ProxyEvent::ConfigChanged); - } - - /// 通知应用启动 - pub fn notify_app_started(&self) { - self.send_event(ProxyEvent::AppStarted); - } - - /// 通知应用即将关闭 - pub fn notify_app_stopping(&self) { - self.send_event(ProxyEvent::AppStopping); - } - - fn send_event(&self, event: ProxyEvent) { - if let Err(e) = self.event_sender.send(event) { - logging!(error, Type::Network, "发送代理事件失败: {e}"); - } - } - - pub async fn start_event_loop( - state: Arc>, - event_rx: mpsc::UnboundedReceiver, - query_rx: mpsc::UnboundedReceiver, - ) { - logging!(info, Type::Network, "事件驱动代理管理器启动"); - - // 将 mpsc 接收器包装成 Stream,避免每次循环创建 future - let mut event_stream = UnboundedReceiverStream::new(event_rx); - let mut query_stream = UnboundedReceiverStream::new(query_rx); - - // 初始化定时器,用于周期性检查代理设置 - let config = Self::get_proxy_config().await; - let mut guard_interval = tokio::time::interval(Duration::from_secs(config.guard_duration)); - // 防止首次立即触发 - guard_interval.tick().await; - - loop { - tokio::select! { - Some(event) = event_stream.next() => { - logging!(debug, Type::Network, "处理代理事件: {event:?}"); - let event_clone = event.clone(); // 保存一份副本用于后续检查 - Self::handle_event(&state, event).await; - - // 检查是否是配置变更事件,如果是,则可能需要更新定时器 - if matches!(event_clone, ProxyEvent::ConfigChanged | ProxyEvent::AppStarted) { - let new_config = Self::get_proxy_config().await; - // 重新设置定时器间隔 - guard_interval = tokio::time::interval(Duration::from_secs(new_config.guard_duration)); - // 防止首次立即触发 - guard_interval.tick().await; - } - } - Some(query) = query_stream.next() => { - let result = Self::handle_query(&state).await; - let _ = query.response_tx.send(result); - } - _ = guard_interval.tick() => { - // 定时检查代理设置 - let config = Self::get_proxy_config().await; - if config.guard_enabled && config.sys_enabled { - logging!(debug, Type::Network, "定时检查代理设置"); - Self::check_and_restore_proxy(&state).await; - } - } - else => { - // 两个通道都关闭时退出 - logging!(info, Type::Network, "事件或查询通道关闭,代理管理器停止"); - break; - } - } - } - } - - async fn handle_event(state: &Arc>, event: ProxyEvent) { - match event { - ProxyEvent::ConfigChanged => { - Self::update_proxy_config(state).await; - } - ProxyEvent::AppStarted => { - Self::initialize_proxy_state(state).await; - } - ProxyEvent::AppStopping => { - logging!(info, Type::Network, "清理代理状态"); - Self::update_state_timestamp(state, |s| { - s.sys_enabled = false; - s.pac_enabled = false; - s.is_healthy = false; - }) - .await; - } - } - } - - async fn handle_query(state: &Arc>) -> Autoproxy { - let auto_proxy = Self::get_auto_proxy_with_timeout().await; - - Self::update_state_timestamp(state, |s| { - s.auto_proxy = auto_proxy.clone(); - }) - .await; - - auto_proxy - } - - async fn initialize_proxy_state(state: &Arc>) { - logging!(info, Type::Network, "初始化代理状态"); - - let config = Self::get_proxy_config().await; - let auto_proxy = Self::get_auto_proxy_with_timeout().await; - let sys_proxy = Self::get_sys_proxy_with_timeout().await; - - Self::update_state_timestamp(state, |s| { - s.sys_enabled = config.sys_enabled; - s.pac_enabled = config.pac_enabled; - s.auto_proxy = auto_proxy; - s.sys_proxy = sys_proxy; - s.is_healthy = true; - }) - .await; - - logging!( - info, - Type::Network, - "代理状态初始化完成: sys={}, pac={}", - config.sys_enabled, - config.pac_enabled - ); - } - - async fn update_proxy_config(state: &Arc>) { - logging!(debug, Type::Network, "更新代理配置"); - - let config = Self::get_proxy_config().await; - - Self::update_state_timestamp(state, |s| { - s.sys_enabled = config.sys_enabled; - s.pac_enabled = config.pac_enabled; - }) - .await; - - if config.guard_enabled && config.sys_enabled { - Self::check_and_restore_proxy(state).await; - } - } - - async fn check_and_restore_proxy(state: &Arc>) { - if handle::Handle::global().is_exiting() { - logging!(debug, Type::Network, "应用正在退出,跳过系统代理守卫检查"); - return; - } - let (sys_enabled, pac_enabled) = { - let s = state.read().await; - (s.sys_enabled, s.pac_enabled) - }; - - if !sys_enabled { - return; - } - - logging!(debug, Type::Network, "检查代理状态"); - - if pac_enabled { - Self::check_and_restore_pac_proxy(state).await; - } else { - Self::check_and_restore_sys_proxy(state).await; - } - } - - async fn check_and_restore_pac_proxy(state: &Arc>) { - if handle::Handle::global().is_exiting() { - logging!(debug, Type::Network, "应用正在退出,跳过PAC代理恢复检查"); - return; - } - - let current = Self::get_auto_proxy_with_timeout().await; - let expected = Self::get_expected_pac_config().await; - - Self::update_state_timestamp(state, |s| { - s.auto_proxy = current.clone(); - }) - .await; - - if !current.enable || current.url != expected.url { - logging!(info, Type::Network, "PAC代理设置异常,正在恢复..."); - if let Err(e) = Self::restore_pac_proxy(&expected.url).await { - logging!(error, Type::Network, "恢复PAC代理失败: {}", e); - } - - sleep(Duration::from_millis(500)).await; - let restored = Self::get_auto_proxy_with_timeout().await; - - Self::update_state_timestamp(state, |s| { - s.is_healthy = restored.enable && restored.url == expected.url; - s.auto_proxy = restored; - }) - .await; - } - } - - async fn check_and_restore_sys_proxy(state: &Arc>) { - if handle::Handle::global().is_exiting() { - logging!(debug, Type::Network, "应用正在退出,跳过系统代理恢复检查"); - return; - } - - let current = Self::get_sys_proxy_with_timeout().await; - let expected = Self::get_expected_sys_proxy().await; - - Self::update_state_timestamp(state, |s| { - s.sys_proxy = current.clone(); - }) - .await; - - if !current.enable || current.host != expected.host || current.port != expected.port { - logging!(info, Type::Network, "系统代理设置异常,正在恢复..."); - if let Err(e) = Self::restore_sys_proxy(&expected).await { - logging!(error, Type::Network, "恢复系统代理失败: {}", e); - } - - sleep(Duration::from_millis(500)).await; - let restored = Self::get_sys_proxy_with_timeout().await; - - Self::update_state_timestamp(state, |s| { - s.is_healthy = restored.enable - && restored.host == expected.host - && restored.port == expected.port; - s.sys_proxy = restored; - }) - .await; - } - } - - async fn get_auto_proxy_with_timeout() -> Autoproxy { - let async_proxy = AsyncProxyQuery::get_auto_proxy().await; - - // 转换为兼容的结构 - Autoproxy { - enable: async_proxy.enable, - url: async_proxy.url, - } - } - - async fn get_sys_proxy_with_timeout() -> Sysproxy { - let async_proxy = AsyncProxyQuery::get_system_proxy().await; - - // 转换为兼容的结构 - Sysproxy { - enable: async_proxy.enable, - host: async_proxy.host, - port: async_proxy.port, - bypass: async_proxy.bypass, - } - } - - // 统一的状态更新方法 - async fn update_state_timestamp(state: &Arc>, update_fn: F) - where - F: FnOnce(&mut ProxyState), - { - let mut state_guard = state.write().await; - update_fn(&mut state_guard); - state_guard.last_updated = std::time::Instant::now(); - } - - async fn get_proxy_config() -> ProxyConfig { - let (sys_enabled, pac_enabled, guard_enabled, guard_duration) = { - let verge_config = Config::verge().await; - let verge = verge_config.latest_arc(); - ( - verge.enable_system_proxy.unwrap_or(false), - verge.proxy_auto_config.unwrap_or(false), - verge.enable_proxy_guard.unwrap_or(false), - verge.proxy_guard_duration.unwrap_or(30), // 默认30秒 - ) - }; - ProxyConfig { - sys_enabled, - pac_enabled, - guard_enabled, - guard_duration, - } - } - - async fn get_expected_pac_config() -> Autoproxy { - let proxy_host = { - let verge_config = Config::verge().await; - let verge = verge_config.latest_arc(); - verge - .proxy_host - .clone() - .unwrap_or_else(|| "127.0.0.1".into()) - }; - let pac_port = IVerge::get_singleton_port(); - Autoproxy { - enable: true, - url: format!("http://{proxy_host}:{pac_port}/commands/pac"), - } - } - - async fn get_expected_sys_proxy() -> Sysproxy { - use crate::constants::network; - - let (verge_mixed_port, proxy_host) = { - let verge_config = Config::verge().await; - let verge_ref = verge_config.latest_arc(); - (verge_ref.verge_mixed_port, verge_ref.proxy_host.clone()) - }; - - let default_port = { - let clash_config = Config::clash().await; - clash_config.latest_arc().get_mixed_port() - }; - - let port = verge_mixed_port.unwrap_or(default_port); - let host = proxy_host - .unwrap_or_else(|| network::DEFAULT_PROXY_HOST.into()) - .into(); - - Sysproxy { - enable: true, - host, - port, - bypass: Self::get_bypass_config().await.into(), - } - } - - async fn get_bypass_config() -> String { - use crate::constants::bypass; - - let verge_config = Config::verge().await; - let verge = verge_config.latest_arc(); - let use_default = verge.use_default_bypass.unwrap_or(true); - let custom = verge.system_proxy_bypass.as_deref().unwrap_or(""); - - match (use_default, custom.is_empty()) { - (_, true) => bypass::DEFAULT.into(), - (true, false) => format!("{},{}", bypass::DEFAULT, custom).into(), - (false, false) => custom.into(), - } - } - - #[cfg(target_os = "windows")] - async fn restore_pac_proxy(expected_url: &str) -> Result<(), anyhow::Error> { - if handle::Handle::global().is_exiting() { - logging!(debug, Type::Network, "应用正在退出,跳过PAC代理恢复"); - return Ok(()); - } - Self::execute_sysproxy_command(&["pac", expected_url]).await - } - - #[allow(clippy::unused_async)] - #[cfg(not(target_os = "windows"))] - async fn restore_pac_proxy(expected_url: &str) -> Result<(), anyhow::Error> { - { - let new_autoproxy = Autoproxy { - enable: true, - url: expected_url.to_string(), - }; - // logging_error!(Type::System, true, new_autoproxy.set_auto_proxy()); - new_autoproxy - .set_auto_proxy() - .map_err(|e| anyhow::anyhow!("Failed to set auto proxy: {}", e)) - } - } - - #[cfg(target_os = "windows")] - async fn restore_sys_proxy(expected: &Sysproxy) -> Result<(), anyhow::Error> { - if handle::Handle::global().is_exiting() { - logging!(debug, Type::Network, "应用正在退出,跳过系统代理恢复"); - return Ok(()); - } - let address = format!("{}:{}", expected.host, expected.port); - Self::execute_sysproxy_command(&["global", &address, &expected.bypass]).await - } - - #[allow(clippy::unused_async)] - #[cfg(not(target_os = "windows"))] - async fn restore_sys_proxy(expected: &Sysproxy) -> Result<(), anyhow::Error> { - { - // logging_error!(Type::System, true, expected.set_system_proxy()); - expected - .set_system_proxy() - .map_err(|e| anyhow::anyhow!("Failed to set system proxy: {}", e)) - } - } - - #[cfg(target_os = "windows")] - async fn execute_sysproxy_command(args: &[&str]) -> Result<(), anyhow::Error> { - if handle::Handle::global().is_exiting() { - logging!( - debug, - Type::Network, - "应用正在退出,取消调用 sysproxy.exe,参数: {:?}", - args - ); - return Ok(()); - } - - use crate::utils::dirs; - #[allow(unused_imports)] // creation_flags必须 - use std::os::windows::process::CommandExt as _; - use tokio::process::Command; - - let binary_path = match dirs::service_path() { - Ok(path) => path, - Err(e) => { - logging!(error, Type::Network, "获取服务路径失败: {e}"); - return Err(e); - } - }; - - let sysproxy_exe = binary_path.with_file_name("sysproxy.exe"); - if !sysproxy_exe.exists() { - logging!(error, Type::Network, "sysproxy.exe 不存在"); - } - anyhow::ensure!(sysproxy_exe.exists(), "sysproxy.exe does not exist"); - - let _output = Command::new(sysproxy_exe) - .args(args) - .creation_flags(0x08000000) // CREATE_NO_WINDOW - 隐藏窗口 - .output() - .await?; - - Ok(()) - } -} diff --git a/clash-verge-rev/src-tauri/src/core/hotkey.rs b/clash-verge-rev/src-tauri/src/core/hotkey.rs index 44db1f0196..fb72551f75 100755 --- a/clash-verge-rev/src-tauri/src/core/hotkey.rs +++ b/clash-verge-rev/src-tauri/src/core/hotkey.rs @@ -1,11 +1,10 @@ use crate::process::AsyncHandler; +use crate::singleton; use crate::utils::notification::{NotificationEvent, notify_event}; -use crate::{ - config::Config, core::handle, feat, logging, module::lightweight::entry_lightweight_mode, - singleton_with_logging, utils::logging::Type, -}; +use crate::{config::Config, core::handle, feat, module::lightweight::entry_lightweight_mode}; use anyhow::{Result, bail}; use arc_swap::ArcSwap; +use clash_verge_logging::{Type, logging}; use smartstring::alias::String; use std::{collections::HashMap, fmt, str::FromStr, sync::Arc}; use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt as _, ShortcutState}; @@ -20,6 +19,7 @@ pub enum HotkeyFunction { ToggleSystemProxy, ToggleTunMode, EntryLightweightMode, + ReactivateProfiles, Quit, #[cfg(target_os = "macos")] Hide, @@ -35,6 +35,7 @@ impl fmt::Display for HotkeyFunction { Self::ToggleSystemProxy => "toggle_system_proxy", Self::ToggleTunMode => "toggle_tun_mode", Self::EntryLightweightMode => "entry_lightweight_mode", + Self::ReactivateProfiles => "reactivate_profiles", Self::Quit => "quit", #[cfg(target_os = "macos")] Self::Hide => "hide", @@ -55,6 +56,7 @@ impl FromStr for HotkeyFunction { "toggle_system_proxy" => Ok(Self::ToggleSystemProxy), "toggle_tun_mode" => Ok(Self::ToggleTunMode), "entry_lightweight_mode" => Ok(Self::EntryLightweightMode), + "reactivate_profiles" => Ok(Self::ReactivateProfiles), "quit" => Ok(Self::Quit), #[cfg(target_os = "macos")] "hide" => Ok(Self::Hide), @@ -148,6 +150,40 @@ impl Hotkey { notify_event(NotificationEvent::LightweightModeEntered).await; }); } + HotkeyFunction::ReactivateProfiles => { + AsyncHandler::spawn(async move || match feat::enhance_profiles().await { + Ok((true, _)) => { + handle::Handle::refresh_clash(); + notify_event(NotificationEvent::ProfilesReactivated).await; + } + Ok((false, msg)) => { + let message = if msg.is_empty() { + "Failed to reactivate profiles.".to_string() + } else { + msg.to_string() + }; + logging!( + warn, + Type::Hotkey, + "Hotkey profile reactivation failed validation: {}", + message.as_str() + ); + handle::Handle::notice_message("reactivate_profiles::error", message); + } + Err(err) => { + logging!( + error, + Type::Hotkey, + "Failed to reactivate subscriptions via hotkey: {}", + err + ); + handle::Handle::notice_message( + "reactivate_profiles::error", + err.to_string(), + ); + } + }); + } HotkeyFunction::Quit => { AsyncHandler::spawn(async move || { notify_event(NotificationEvent::AppQuit).await; @@ -258,8 +294,7 @@ impl Hotkey { } } -// Use unified singleton macro -singleton_with_logging!(Hotkey, INSTANCE, "Hotkey"); +singleton!(Hotkey, INSTANCE); impl Hotkey { pub async fn init(&self, skip: bool) -> Result<()> { diff --git a/clash-verge-rev/src-tauri/src/core/manager/config.rs b/clash-verge-rev/src-tauri/src/core/manager/config.rs index 64283a590e..b0a4992877 100644 --- a/clash-verge-rev/src-tauri/src/core/manager/config.rs +++ b/clash-verge-rev/src-tauri/src/core/manager/config.rs @@ -1,16 +1,16 @@ use super::CoreManager; use crate::{ - config::{Config, ConfigType, IRuntime}, + config::{Config, ConfigType}, constants::timing, core::{handle, validate::CoreConfigValidator}, - logging, - utils::{dirs, help, logging::Type}, + utils::{dirs, help}, }; use anyhow::{Result, anyhow}; +use clash_verge_logging::{Type, logging}; +use clash_verge_types::runtime::IRuntime; use smartstring::alias::String; -use std::{path::PathBuf, time::Instant}; +use std::{collections::HashSet, path::PathBuf, time::Instant}; use tauri_plugin_mihomo::Error as MihomoError; -use tokio::time::sleep; impl CoreManager { pub async fn use_default_config(&self, error_key: &str, error_msg: &str) -> Result<()> { @@ -22,7 +22,7 @@ impl CoreManager { Config::runtime().await.edit_draft(|d| { *d = IRuntime { config: Some(clash_config.to_owned()), - exists_keys: vec![], + exists_keys: HashSet::new(), chain_logs: Default::default(), } }); @@ -37,25 +37,25 @@ impl CoreManager { return Ok((true, String::new())); } - if !self.should_update_config()? { + if !self.should_update_config() { return Ok((true, String::new())); } self.perform_config_update().await } - fn should_update_config(&self) -> Result { + fn should_update_config(&self) -> bool { let now = Instant::now(); let last = self.get_last_update(); if let Some(last_time) = last && now.duration_since(*last_time) < timing::CONFIG_UPDATE_DEBOUNCE { - return Ok(false); + return false; } self.set_last_update(now); - Ok(true) + true } async fn perform_config_update(&self) -> Result<(bool, String)> { @@ -78,22 +78,14 @@ impl CoreManager { } } - pub async fn put_configs_force(&self, path: PathBuf) -> Result<()> { - self.apply_config(path).await - } - - pub(super) async fn apply_config(&self, path: PathBuf) -> Result<()> { - let path_str = dirs::path_to_str(&path)?; - - match self.reload_config(path_str).await { + async fn apply_config(&self, path: PathBuf) -> Result<()> { + let path = dirs::path_to_str(&path)?; + match self.reload_config(path).await { Ok(_) => { Config::runtime().await.apply(); logging!(info, Type::Core, "Configuration applied"); Ok(()) } - Err(err) if Self::should_restart_on_error(&err) => { - self.retry_with_restart(path_str).await - } Err(err) => { Config::runtime().await.discard(); Err(anyhow!("Failed to apply config: {}", err)) @@ -101,54 +93,10 @@ impl CoreManager { } } - async fn retry_with_restart(&self, config_path: &str) -> Result<()> { - if handle::Handle::global().is_exiting() { - return Err(anyhow!("Application exiting")); - } - - logging!(warn, Type::Core, "Restarting core for config reload"); - self.restart_core().await?; - sleep(timing::CONFIG_RELOAD_DELAY).await; - - self.reload_config(config_path).await?; - Config::runtime().await.apply(); - logging!(info, Type::Core, "Configuration applied after restart"); - Ok(()) - } - async fn reload_config(&self, path: &str) -> Result<(), MihomoError> { handle::Handle::mihomo() .await .reload_config(true, path) .await } - - fn should_restart_on_error(err: &MihomoError) -> bool { - match err { - MihomoError::ConnectionFailed | MihomoError::ConnectionLost => true, - MihomoError::Io(io_err) => Self::is_connection_io_error(io_err.kind()), - MihomoError::Reqwest(req_err) => { - req_err.is_connect() - || req_err.is_timeout() - || Self::contains_error_pattern(&req_err.to_string()) - } - MihomoError::FailedResponse(msg) => Self::contains_error_pattern(msg), - _ => false, - } - } - - const fn is_connection_io_error(kind: std::io::ErrorKind) -> bool { - matches!( - kind, - std::io::ErrorKind::ConnectionAborted - | std::io::ErrorKind::ConnectionRefused - | std::io::ErrorKind::ConnectionReset - | std::io::ErrorKind::NotFound - ) - } - - fn contains_error_pattern(text: &str) -> bool { - use crate::constants::error_patterns::CONNECTION_ERRORS; - CONNECTION_ERRORS.iter().any(|p| text.contains(p)) - } } diff --git a/clash-verge-rev/src-tauri/src/core/manager/lifecycle.rs b/clash-verge-rev/src-tauri/src/core/manager/lifecycle.rs index 75574068c2..89926415cc 100644 --- a/clash-verge-rev/src-tauri/src/core/manager/lifecycle.rs +++ b/clash-verge-rev/src-tauri/src/core/manager/lifecycle.rs @@ -1,19 +1,23 @@ use super::{CoreManager, RunningMode}; -use crate::config::{Config, ConfigType, IVerge}; -use crate::{ - core::{ - logger::CLASH_LOGGER, - service::{SERVICE_MANAGER, ServiceStatus}, - }, - logging, - utils::logging::Type, +use crate::cmd::StringifyErr as _; +use crate::config::{Config, IVerge}; +use crate::core::handle::Handle; +use crate::core::{ + logger::CLASH_LOGGER, + service::{SERVICE_MANAGER, ServiceStatus}, }; use anyhow::Result; +use clash_verge_logging::{Type, logging}; +use scopeguard::defer; use smartstring::alias::String; +use tauri_plugin_clash_verge_sysinfo; impl CoreManager { pub async fn start_core(&self) -> Result<()> { self.prepare_startup().await?; + defer! { + self.after_core_process(); + } match *self.get_running_mode() { RunningMode::Service => self.start_core_by_service().await, @@ -23,10 +27,16 @@ impl CoreManager { pub async fn stop_core(&self) -> Result<()> { CLASH_LOGGER.clear_logs().await; + defer! { + self.after_core_process(); + } match *self.get_running_mode() { RunningMode::Service => self.stop_core_by_service().await, - RunningMode::Sidecar => self.stop_core_by_sidecar(), + RunningMode::Sidecar => { + self.stop_core_by_sidecar(); + Ok(()) + } RunningMode::NotRunning => Ok(()), } } @@ -55,13 +65,8 @@ impl CoreManager { let verge_data = Config::verge().await.latest_arc(); verge_data.save_file().await.map_err(|e| e.to_string())?; - let run_path = Config::generate_file(ConfigType::Run) - .await - .map_err(|e| e.to_string())?; - - self.apply_config(run_path) - .await - .map_err(|e| e.to_string().into()) + self.update_config().await.stringify_err()?; + Ok(()) } async fn prepare_startup(&self) -> Result<()> { @@ -78,6 +83,14 @@ impl CoreManager { Ok(()) } + fn after_core_process(&self) { + let app_handle = Handle::app_handle(); + tauri_plugin_clash_verge_sysinfo::set_app_core_mode( + app_handle, + self.get_running_mode().to_string(), + ); + } + #[cfg(target_os = "windows")] async fn wait_for_service_if_needed(&self) { use crate::{config::Config, constants::timing}; diff --git a/clash-verge-rev/src-tauri/src/core/manager/mod.rs b/clash-verge-rev/src-tauri/src/core/manager/mod.rs index 8f41af64fc..ed0f1e53d2 100644 --- a/clash-verge-rev/src-tauri/src/core/manager/mod.rs +++ b/clash-verge-rev/src-tauri/src/core/manager/mod.rs @@ -5,9 +5,9 @@ mod state; use anyhow::Result; use arc_swap::{ArcSwap, ArcSwapOption}; use std::{fmt, sync::Arc, time::Instant}; +use tauri_plugin_shell::process::CommandChild; -use crate::process::CommandChildGuard; -use crate::singleton_lazy; +use crate::singleton; #[derive(Debug, serde::Serialize, PartialEq, Eq)] pub enum RunningMode { @@ -35,7 +35,7 @@ pub struct CoreManager { #[derive(Debug)] struct State { running_mode: ArcSwap, - child_sidecar: ArcSwapOption, + child_sidecar: ArcSwapOption, } impl Default for State { @@ -57,11 +57,15 @@ impl Default for CoreManager { } impl CoreManager { + fn new() -> Self { + Self::default() + } + pub fn get_running_mode(&self) -> Arc { Arc::clone(&self.state.load().running_mode.load()) } - pub fn take_child_sidecar(&self) -> Option { + pub fn take_child_sidecar(&self) -> Option { self.state .load() .child_sidecar @@ -78,7 +82,7 @@ impl CoreManager { state.running_mode.store(Arc::new(mode)); } - pub fn set_running_child_sidecar(&self, child: CommandChildGuard) { + pub fn set_running_child_sidecar(&self, child: CommandChild) { let state = self.state.load(); state.child_sidecar.store(Some(Arc::new(child))); } @@ -93,4 +97,4 @@ impl CoreManager { } } -singleton_lazy!(CoreManager, CORE_MANAGER, CoreManager::default); +singleton!(CoreManager, CORE_MANAGER); diff --git a/clash-verge-rev/src-tauri/src/core/manager/state.rs b/clash-verge-rev/src-tauri/src/core/manager/state.rs index 520c1f2a1a..897383c254 100644 --- a/clash-verge-rev/src-tauri/src/core/manager/state.rs +++ b/clash-verge-rev/src-tauri/src/core/manager/state.rs @@ -4,14 +4,10 @@ use crate::{ config::Config, core::{handle, logger::CLASH_LOGGER, service}, logging, - process::CommandChildGuard, - utils::{ - dirs, - init::sidecar_writer, - logging::{SharedWriter, Type, write_sidecar_log}, - }, + utils::{dirs, init::sidecar_writer}, }; use anyhow::Result; +use clash_verge_logging::{SharedWriter, Type, write_sidecar_log}; use compact_str::CompactString; use flexi_logger::DeferredNow; use log::Level; @@ -49,7 +45,7 @@ impl CoreManager { let pid = child.pid(); logging!(trace, Type::Core, "Sidecar started with PID: {}", pid); - self.set_running_child_sidecar(CommandChildGuard::new(child)); + self.set_running_child_sidecar(child); self.set_running_mode(RunningMode::Sidecar); let shared_writer: SharedWriter = @@ -96,17 +92,22 @@ impl CoreManager { Ok(()) } - pub(super) fn stop_core_by_sidecar(&self) -> Result<()> { + pub(super) fn stop_core_by_sidecar(&self) { logging!(info, Type::Core, "Stopping sidecar"); defer! { self.set_running_mode(RunningMode::NotRunning); } if let Some(child) = self.take_child_sidecar() { let pid = child.pid(); - drop(child); - logging!(trace, Type::Core, "Sidecar stopped (PID: {:?})", pid); + let result = child.kill(); + logging!( + trace, + Type::Core, + "Sidecar stopped (PID: {:?}, Result: {:?})", + pid, + result + ); } - Ok(()) } pub(super) async fn start_core_by_service(&self) -> Result<()> { diff --git a/clash-verge-rev/src-tauri/src/core/mod.rs b/clash-verge-rev/src-tauri/src/core/mod.rs index 6bdb2418b0..9dc5787969 100644 --- a/clash-verge-rev/src-tauri/src/core/mod.rs +++ b/clash-verge-rev/src-tauri/src/core/mod.rs @@ -1,6 +1,4 @@ -pub mod async_proxy_query; pub mod backup; -pub mod event_driven_proxy; pub mod handle; pub mod hotkey; pub mod logger; @@ -13,4 +11,4 @@ pub mod tray; pub mod validate; pub mod win_uwp; -pub use self::{event_driven_proxy::EventDrivenProxyManager, manager::CoreManager, timer::Timer}; +pub use self::{manager::CoreManager, timer::Timer}; diff --git a/clash-verge-rev/src-tauri/src/core/notification.rs b/clash-verge-rev/src-tauri/src/core/notification.rs index fba5ecaa1f..472854a4c4 100644 --- a/clash-verge-rev/src-tauri/src/core/notification.rs +++ b/clash-verge-rev/src-tauri/src/core/notification.rs @@ -1,9 +1,6 @@ use super::handle::Handle; -use crate::{ - constants::{retry, timing}, - logging, - utils::logging::Type, -}; +use crate::constants::{retry, timing}; +use clash_verge_logging::{Type, logging}; use parking_lot::RwLock; use smartstring::alias::String; use std::{ @@ -16,6 +13,7 @@ use std::{ }; use tauri::{Emitter as _, WebviewWindow}; +// TODO 重构或优化,避免 Clone 过多 #[derive(Debug, Clone)] pub enum FrontendEvent { RefreshClash, @@ -105,8 +103,6 @@ impl NotificationSystem { } } - // Clippy 似乎对 parking lot 的 RwLock 有误报,这里禁用相关警告 - #[allow(clippy::significant_drop_tightening)] fn process_event(handle: &super::handle::Handle, event: FrontendEvent) { let binding = handle.notification_system.read(); let system = match binding.as_ref() { @@ -120,6 +116,7 @@ impl NotificationSystem { if let Some(window) = super::handle::Handle::get_window() { system.emit_to_window(&window, event); + drop(binding); thread::sleep(timing::EVENT_EMIT_DELAY); } } diff --git a/clash-verge-rev/src-tauri/src/core/service.rs b/clash-verge-rev/src-tauri/src/core/service.rs index 892294d95d..34c2633264 100644 --- a/clash-verge-rev/src-tauri/src/core/service.rs +++ b/clash-verge-rev/src-tauri/src/core/service.rs @@ -1,10 +1,10 @@ use crate::{ config::Config, core::tray, - logging, logging_error, - utils::{dirs, init::service_writer_config, logging::Type}, + utils::{dirs, init::service_writer_config}, }; use anyhow::{Context as _, Result, bail}; +use clash_verge_logging::{Type, logging, logging_error}; use clash_verge_service_ipc::CoreConfig; use compact_str::CompactString; use once_cell::sync::Lazy; @@ -136,11 +136,28 @@ async fn uninstall_service() -> Result<()> { let status = if linux_running_as_root() { StdCommand::new(&uninstall_path).status()? } else { - StdCommand::new(elevator) + let result = StdCommand::new(&elevator) .arg("sh") .arg("-c") - .arg(uninstall_shell) - .status()? + .arg(&uninstall_shell) + .status()?; + + // 如果 pkexec 执行失败,回退到 sudo + if !result.success() && elevator.contains("pkexec") { + logging!( + warn, + Type::Service, + "pkexec failed with code {}, falling back to sudo", + result.code().unwrap_or(-1) + ); + StdCommand::new("sudo") + .arg("sh") + .arg("-c") + .arg(&uninstall_shell) + .status()? + } else { + result + } }; logging!( info, @@ -177,11 +194,28 @@ async fn install_service() -> Result<()> { let status = if linux_running_as_root() { StdCommand::new(&install_path).status()? } else { - StdCommand::new(elevator) + let result = StdCommand::new(&elevator) .arg("sh") .arg("-c") - .arg(install_shell) - .status()? + .arg(&install_shell) + .status()?; + + // 如果 pkexec 执行失败,回退到 sudo + if !result.success() && elevator.contains("pkexec") { + logging!( + warn, + Type::Service, + "pkexec failed with code {}, falling back to sudo", + result.code().unwrap_or(-1) + ); + StdCommand::new("sudo") + .arg("sh") + .arg("-c") + .arg(&install_shell) + .status()? + } else { + result + } }; logging!( info, @@ -220,9 +254,10 @@ async fn reinstall_service() -> Result<()> { #[cfg(target_os = "linux")] fn linux_running_as_root() -> bool { - const ROOT_UID: u32 = 0; - - unsafe { libc::geteuid() == ROOT_UID } + use crate::core::handle; + use tauri_plugin_clash_verge_sysinfo::is_current_app_handle_admin; + let app_handle = handle::Handle::app_handle(); + is_current_app_handle_admin(app_handle) } #[cfg(target_os = "macos")] @@ -456,12 +491,12 @@ impl ServiceManager { Self(ServiceStatus::Unavailable("Need Checks".into())) } - pub const fn config() -> Option { - Some(clash_verge_service_ipc::IpcConfig { + pub const fn config() -> clash_verge_service_ipc::IpcConfig { + clash_verge_service_ipc::IpcConfig { default_timeout: Duration::from_millis(30), retry_delay: Duration::from_millis(250), max_retries: 6, - }) + } } pub async fn init(&mut self) -> Result<()> { @@ -521,6 +556,13 @@ impl ServiceManager { ServiceStatus::InstallRequired => { logging!(info, Type::Service, "需要安装服务,执行安装流程"); install_service().await?; + // compatible with older service version, force reinstall if service is unavailable + // wait for service server is running + tokio::time::sleep(Duration::from_millis(500)).await; + if is_service_available().await.is_err() { + logging!(info, Type::Service, "服务需要强制重装,执行强制重装流程"); + force_reinstall_service().await?; + } self.0 = ServiceStatus::Ready; } ServiceStatus::UninstallRequired => { diff --git a/clash-verge-rev/src-tauri/src/core/sysopt.rs b/clash-verge-rev/src-tauri/src/core/sysopt.rs index bd5383d973..39918369bf 100644 --- a/clash-verge-rev/src-tauri/src/core/sysopt.rs +++ b/clash-verge-rev/src-tauri/src/core/sysopt.rs @@ -2,31 +2,60 @@ use crate::utils::autostart as startup_shortcut; use crate::{ config::{Config, IVerge}, - core::{EventDrivenProxyManager, handle::Handle}, - logging, logging_error, singleton_lazy, - utils::logging::Type, + core::handle::Handle, + singleton, }; use anyhow::Result; +use clash_verge_logging::{Type, logging, logging_error}; +#[cfg(not(target_os = "windows"))] +use parking_lot::Mutex; +use parking_lot::RwLock; use scopeguard::defer; use smartstring::alias::String; -use std::sync::atomic::{AtomicBool, Ordering}; -#[cfg(not(target_os = "windows"))] -use sysproxy::{Autoproxy, Sysproxy}; +use std::{ + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, + time::Duration, +}; +use sysproxy::{Autoproxy, GuardMonitor, GuardType, Sysproxy}; use tauri_plugin_autostart::ManagerExt as _; pub struct Sysopt { - initialed: AtomicBool, update_sysproxy: AtomicBool, reset_sysproxy: AtomicBool, + #[cfg(not(target_os = "windows"))] + sysproxy: Arc>, + #[cfg(not(target_os = "windows"))] + autoproxy: Arc>, + guard: Arc>, +} + +impl Default for Sysopt { + fn default() -> Self { + Self { + update_sysproxy: AtomicBool::new(false), + reset_sysproxy: AtomicBool::new(false), + #[cfg(not(target_os = "windows"))] + sysproxy: Arc::new(Mutex::new(Sysproxy::default())), + #[cfg(not(target_os = "windows"))] + autoproxy: Arc::new(Mutex::new(Autoproxy::default())), + guard: Arc::new(RwLock::new(GuardMonitor::new( + GuardType::None, + Duration::from_secs(30), + ))), + } + } } #[cfg(target_os = "windows")] static DEFAULT_BYPASS: &str = "localhost;127.*;192.168.*;10.*;172.16.*;172.17.*;172.18.*;172.19.*;172.20.*;172.21.*;172.22.*;172.23.*;172.24.*;172.25.*;172.26.*;172.27.*;172.28.*;172.29.*;172.30.*;172.31.*;"; #[cfg(target_os = "linux")] -static DEFAULT_BYPASS: &str = - "localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,::1"; +static DEFAULT_BYPASS: &str = "localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,::1"; #[cfg(target_os = "macos")] -static DEFAULT_BYPASS: &str = "127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,localhost,*.local,*.crashlytics.com,"; +static DEFAULT_BYPASS: &str = + "127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,localhost,*.local,*.crashlytics.com,"; async fn get_bypass() -> String { let use_default = Config::verge() @@ -82,49 +111,70 @@ async fn execute_sysproxy_command(args: Vec) -> Result<()> Ok(()) } -impl Default for Sysopt { - fn default() -> Self { - Self { - initialed: AtomicBool::new(false), - update_sysproxy: AtomicBool::new(false), - reset_sysproxy: AtomicBool::new(false), - } - } -} - -// Use simplified singleton_lazy macro -singleton_lazy!(Sysopt, SYSOPT, Sysopt::default); +singleton!(Sysopt, SYSOPT); impl Sysopt { - pub fn is_initialed(&self) -> bool { - self.initialed.load(Ordering::SeqCst) + fn new() -> Self { + Self::default() } - pub fn init_guard_sysproxy(&self) -> Result<()> { - // 使用事件驱动代理管理器 - let proxy_manager = EventDrivenProxyManager::global(); - proxy_manager.notify_app_started(); + fn access_guard(&self) -> Arc> { + Arc::clone(&self.guard) + } - logging!(info, Type::Core, "已启用事件驱动代理守卫"); - Ok(()) + pub async fn refresh_guard(&self) { + logging!(info, Type::Core, "Refreshing system proxy guard..."); + let verge = Config::verge().await.latest_arc(); + if !verge.enable_system_proxy.unwrap_or(false) { + logging!(info, Type::Core, "System proxy is disabled."); + self.access_guard().write().stop(); + return; + } + if !verge.enable_proxy_guard.unwrap_or(false) { + logging!(info, Type::Core, "System proxy guard is disabled."); + return; + } + logging!( + info, + Type::Core, + "Updating system proxy with duration: {} seconds", + verge.proxy_guard_duration.unwrap_or(30) + ); + { + let guard = self.access_guard(); + guard.write().set_interval(Duration::from_secs( + verge.proxy_guard_duration.unwrap_or(30), + )); + } + logging!(info, Type::Core, "Starting system proxy guard..."); + { + let guard = self.access_guard(); + guard.write().start(); + } } /// init the sysproxy pub async fn update_sysproxy(&self) -> Result<()> { - self.initialed.store(true, Ordering::SeqCst); + if self.update_sysproxy.load(Ordering::Acquire) { + logging!(info, Type::Core, "Sysproxy update is already in progress."); + return Ok(()); + } if self .update_sysproxy - .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) + .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) .is_err() { + logging!(info, Type::Core, "Sysproxy update is already in progress."); return Ok(()); } defer! { - self.update_sysproxy.store(false, Ordering::SeqCst); + logging!(info, Type::Core, "Sysproxy update completed."); + self.update_sysproxy.store(false, Ordering::Release); } + let verge = Config::verge().await.latest_arc(); let port = { - let verge_port = Config::verge().await.latest_arc().verge_mixed_port; + let verge_port = verge.verge_mixed_port; match verge_port { Some(port) => port, None => Config::clash().await.latest_arc().get_mixed_port(), @@ -133,8 +183,6 @@ impl Sysopt { let pac_port = IVerge::get_singleton_port(); let (sys_enable, pac_enable, proxy_host) = { - let verge = Config::verge().await; - let verge = verge.latest_arc(); ( verge.enable_system_proxy.unwrap_or(false), verge.proxy_auto_config.unwrap_or(false), @@ -147,22 +195,25 @@ impl Sysopt { #[cfg(not(target_os = "windows"))] { - let mut sys = Sysproxy { - enable: false, - host: proxy_host.clone().into(), - port, - bypass: get_bypass().await.into(), - }; - let mut auto = Autoproxy { - enable: false, - url: format!("http://{proxy_host}:{pac_port}/commands/pac"), - }; + // 先 await, 避免持有锁导致的 Send 问题 + let bypass = get_bypass().await; + + let mut sys = self.sysproxy.lock(); + sys.enable = false; + sys.host = proxy_host.clone().into(); + sys.port = port; + sys.bypass = bypass.into(); + + let mut auto = self.autoproxy.lock(); + auto.enable = false; + auto.url = format!("http://{proxy_host}:{pac_port}/commands/pac"); if !sys_enable { sys.set_system_proxy()?; auto.set_auto_proxy()?; - let proxy_manager = EventDrivenProxyManager::global(); - proxy_manager.notify_config_changed(); + self.access_guard() + .write() + .set_guard_type(GuardType::Sysproxy(sys.clone())); return Ok(()); } @@ -171,8 +222,9 @@ impl Sysopt { auto.enable = true; sys.set_system_proxy()?; auto.set_auto_proxy()?; - let proxy_manager = EventDrivenProxyManager::global(); - proxy_manager.notify_config_changed(); + self.access_guard() + .write() + .set_guard_type(GuardType::Autoproxy(auto.clone())); return Ok(()); } @@ -181,33 +233,49 @@ impl Sysopt { sys.enable = true; auto.set_auto_proxy()?; sys.set_system_proxy()?; - let proxy_manager = EventDrivenProxyManager::global(); - proxy_manager.notify_config_changed(); + drop(auto); + self.access_guard() + .write() + .set_guard_type(GuardType::Sysproxy(sys.clone())); + drop(sys); return Ok(()); } } + #[cfg(target_os = "windows")] { if !sys_enable { - let result = self.reset_sysproxy().await; - let proxy_manager = EventDrivenProxyManager::global(); - proxy_manager.notify_config_changed(); - return result; + self.access_guard().write().set_guard_type(GuardType::None); + return self.reset_sysproxy().await; } - let args: Vec = if pac_enable { + let (args, guard_type): (Vec, GuardType) = if pac_enable { let address = format!("http://{proxy_host}:{pac_port}/commands/pac"); - vec!["pac".into(), address] + ( + vec!["pac".into(), address.clone()], + GuardType::Autoproxy(Autoproxy { + enable: true, + url: address, + }), + ) } else { let address = format!("{proxy_host}:{port}"); let bypass = get_bypass().await; - vec!["global".into(), address, bypass.into()] + let bypass_for_guard = bypass.as_str().to_owned(); + ( + vec!["global".into(), address.clone(), bypass.into()], + GuardType::Sysproxy(Sysproxy { + enable: true, + host: proxy_host.clone().into(), + port, + bypass: bypass_for_guard, + }), + ) }; execute_sysproxy_command(args).await?; + self.access_guard().write().set_guard_type(guard_type); } - let proxy_manager = EventDrivenProxyManager::global(); - proxy_manager.notify_config_changed(); Ok(()) } @@ -228,32 +296,13 @@ impl Sysopt { //直接关闭所有代理 #[cfg(not(target_os = "windows"))] { - let mut sysproxy: Sysproxy = match Sysproxy::get_system_proxy() { - Ok(sp) => sp, - Err(e) => { - logging!( - warn, - Type::Core, - "Warning: 重置代理时获取系统代理配置失败: {e}, 使用默认配置" - ); - Sysproxy::default() - } - }; - let mut autoproxy = match Autoproxy::get_auto_proxy() { - Ok(ap) => ap, - Err(e) => { - logging!( - warn, - Type::Core, - "Warning: 重置代理时获取自动代理配置失败: {e}, 使用默认配置" - ); - Autoproxy::default() - } - }; + let mut sysproxy = self.sysproxy.lock(); sysproxy.enable = false; + sysproxy.set_system_proxy()?; + drop(sysproxy); + let mut autoproxy = self.autoproxy.lock(); autoproxy.enable = false; autoproxy.set_auto_proxy()?; - sysproxy.set_system_proxy()?; } #[cfg(target_os = "windows")] diff --git a/clash-verge-rev/src-tauri/src/core/timer.rs b/clash-verge-rev/src-tauri/src/core/timer.rs index 20c0bb6c05..0812dddc5a 100644 --- a/clash-verge-rev/src-tauri/src/core/timer.rs +++ b/clash-verge-rev/src-tauri/src/core/timer.rs @@ -1,8 +1,6 @@ -use crate::{ - config::Config, core::sysopt::Sysopt, feat, logging, logging_error, singleton, - utils::logging::Type, -}; +use crate::{config::Config, feat, singleton, utils::resolve::is_resolve_done}; use anyhow::{Context as _, Result}; +use clash_verge_logging::{Type, logging, logging_error}; use delay_timer::prelude::{DelayTimer, DelayTimerBuilder, TaskBuilder}; use parking_lot::RwLock; use smartstring::alias::String; @@ -250,12 +248,10 @@ impl Timer { } // Locks are dropped here // Now perform async operations without holding locks + let delay_timer = self.delay_timer.write(); for (uid, tid, interval) in operations_to_add { - // Re-acquire locks for individual operations - let delay_timer = self.delay_timer.write(); if let Err(e) = self.add_task(&delay_timer, uid.clone(), tid, interval) { logging_error!(Type::Timer, "Failed to add task for uid {}: {}", uid, e); - // Rollback on failure - remove from timer_map self.timer_map.write().remove(&uid); } else { @@ -392,7 +388,7 @@ impl Timer { .spawn_async_routine(move || { let uid = uid.clone(); Box::pin(async move { - Self::wait_until_sysopt(Duration::from_millis(1000)).await; + Self::wait_until_resolve_done(Duration::from_millis(5000)).await; Self::async_task(&uid).await; }) as Pin + Send>> }) @@ -523,11 +519,11 @@ impl Timer { Self::emit_update_event(uid, false); } - async fn wait_until_sysopt(max_wait: Duration) { + async fn wait_until_resolve_done(max_wait: Duration) { let _ = timeout(max_wait, async { - while !Sysopt::global().is_initialed() { - logging!(warn, Type::Timer, "Waiting for Sysopt to be initialized..."); - sleep(Duration::from_millis(30)).await; + while !is_resolve_done() { + logging!(debug, Type::Timer, "Waiting for resolve to be done..."); + sleep(Duration::from_millis(200)).await; } }) .await; diff --git a/clash-verge-rev/src-tauri/src/core/tray/mod.rs b/clash-verge-rev/src-tauri/src/core/tray/mod.rs index ce29aecc02..832f10d2d7 100644 --- a/clash-verge-rev/src-tauri/src/core/tray/mod.rs +++ b/clash-verge-rev/src-tauri/src/core/tray/mod.rs @@ -1,26 +1,26 @@ use once_cell::sync::OnceCell; use tauri::tray::TrayIconBuilder; +use tauri_plugin_clash_verge_sysinfo::is_current_app_handle_admin; use tauri_plugin_mihomo::models::Proxies; use tokio::fs; #[cfg(target_os = "macos")] pub mod speed_rate; -use crate::config::{IVerge, PrfSelected}; +use crate::config::{IProfilePreview, IVerge, PrfSelected}; use crate::core::service; use crate::module::lightweight; use crate::process::AsyncHandler; +use crate::singleton; use crate::utils::window_manager::WindowManager; use crate::{ Type, cmd, config::Config, feat, logging, module::lightweight::is_in_lightweight_mode, - singleton_lazy, utils::{dirs::find_target_icons, i18n}, }; use super::handle; use anyhow::Result; -use futures::future::join_all; use parking_lot::Mutex; use smartstring::alias::String; use std::collections::HashMap; @@ -200,10 +200,13 @@ impl Default for Tray { } } -// Use simplified singleton_lazy macro -singleton_lazy!(Tray, TRAY, Tray::default); +singleton!(Tray, TRAY); impl Tray { + fn new() -> Self { + Self::default() + } + pub async fn init(&self) -> Result<()> { if handle::Handle::global().is_exiting() { logging!(debug, Type::Tray, "应用正在退出,跳过托盘初始化"); @@ -301,7 +304,7 @@ impl Tray { let verge = Config::verge().await.latest_arc(); let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false); let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false); - let tun_mode_available = cmd::system::is_admin().unwrap_or_default() + let tun_mode_available = is_current_app_handle_admin(app_handle) || service::is_service_available().await.is_ok(); let mode = { Config::clash() @@ -315,7 +318,7 @@ impl Tray { }; let profiles_config = Config::profiles().await; let profiles_arc = profiles_config.latest_arc(); - let profile_uid_and_name = profiles_arc.all_profile_uid_and_name().unwrap_or_default(); + let profiles_preview = profiles_arc.profiles_preview().unwrap_or_default(); let is_lightweight_mode = is_in_lightweight_mode(); match app_handle.tray_by_id("main") { @@ -327,7 +330,7 @@ impl Tray { *system_proxy, *tun_mode, tun_mode_available, - profile_uid_and_name, + profiles_preview, is_lightweight_mode, ) .await?, @@ -607,31 +610,24 @@ fn create_hotkeys(hotkeys: &Option>) -> HashMap { .unwrap_or_default() } -async fn create_profile_menu_item( +fn create_profile_menu_item( app_handle: &AppHandle, - profile_uid_and_name: Vec<(&String, &String)>, + profiles_preview: Vec>, ) -> Result>> { - let futures = profile_uid_and_name - .iter() - .map(|(profile_uid, profile_name)| { - let app_handle = app_handle.clone(); - async move { - let is_current_profile = Config::profiles() - .await - .latest_arc() - .is_current_profile_index(profile_uid); - CheckMenuItem::with_id( - &app_handle, - format!("profiles_{profile_uid}"), - profile_name.as_str(), - true, - is_current_profile, - None::<&str>, - ) - } - }); - let results = join_all(futures).await; - Ok(results.into_iter().collect::, _>>()?) + profiles_preview + .into_iter() + .map(|profile| { + CheckMenuItem::with_id( + app_handle, + format!("profiles_{}", profile.uid), + profile.name, + true, + profile.is_current, + None::<&str>, + ) + .map_err(|e| e.into()) + }) + .collect() } fn create_subcreate_proxy_menu_item( @@ -640,7 +636,7 @@ fn create_subcreate_proxy_menu_item( current_profile_selected: &[PrfSelected], proxy_group_order_map: Option>, proxy_nodes_data: Result, -) -> Result>> { +) -> Vec> { let proxy_submenus: Vec> = { let mut submenus: Vec<(String, usize, Submenu)> = Vec::new(); @@ -767,7 +763,7 @@ fn create_subcreate_proxy_menu_item( .map(|(_, _, submenu)| submenu) .collect() }; - Ok(proxy_submenus) + proxy_submenus } fn create_proxy_menu_item( @@ -813,7 +809,7 @@ async fn create_tray_menu( system_proxy_enabled: bool, tun_mode_enabled: bool, tun_mode_available: bool, - profile_uid_and_name: Vec<(&String, &String)>, + profiles_preview: Vec>, is_lightweight_mode: bool, ) -> Result> { let current_proxy_mode = mode.unwrap_or(""); @@ -877,7 +873,7 @@ async fn create_tray_menu( let hotkeys = create_hotkeys(&verge_settings.hotkeys); let profile_menu_items: Vec> = - create_profile_menu_item(app_handle, profile_uid_and_name).await?; + create_profile_menu_item(app_handle, profiles_preview)?; // Pre-fetch all localized strings let texts = MenuTexts::new(); @@ -955,7 +951,7 @@ async fn create_tray_menu( ¤t_profile_selected, proxy_group_order_map, proxy_nodes_data.map_err(anyhow::Error::from), - )?; + ); let (proxies_menu, inline_proxy_items) = create_proxy_menu_item( app_handle, diff --git a/clash-verge-rev/src-tauri/src/core/validate.rs b/clash-verge-rev/src-tauri/src/core/validate.rs index 330c96f3c7..153daa118b 100644 --- a/clash-verge-rev/src-tauri/src/core/validate.rs +++ b/clash-verge-rev/src-tauri/src/core/validate.rs @@ -7,9 +7,9 @@ use tokio::fs; use crate::config::{Config, ConfigType}; use crate::core::handle; -use crate::singleton_lazy; +use crate::singleton; use crate::utils::dirs; -use crate::{logging, utils::logging::Type}; +use clash_verge_logging::{Type, logging}; pub struct CoreConfigValidator { is_processing: AtomicBool, @@ -361,8 +361,4 @@ fn contains_any_keyword<'a>(buf: &'a [u8], keywords: &'a [&str]) -> bool { false } -singleton_lazy!( - CoreConfigValidator, - CORECONFIGVALIDATOR, - CoreConfigValidator::new -); +singleton!(CoreConfigValidator, CORECONFIGVALIDATOR); diff --git a/clash-verge-rev/src-tauri/src/enhance/field.rs b/clash-verge-rev/src-tauri/src/enhance/field.rs index b75f682fe6..72f9e916bd 100644 --- a/clash-verge-rev/src-tauri/src/enhance/field.rs +++ b/clash-verge-rev/src-tauri/src/enhance/field.rs @@ -25,14 +25,14 @@ pub const DEFAULT_FIELDS: [&str; 5] = [ "rules", ]; -pub fn use_lowercase(config: Mapping) -> Mapping { +pub fn use_lowercase(config: &Mapping) -> Mapping { let mut ret = Mapping::new(); for (key, value) in config.into_iter() { if let Some(key_str) = key.as_str() { let mut key_str = String::from(key_str); key_str.make_ascii_lowercase(); - ret.insert(Value::from(key_str.as_str()), value); + ret.insert(Value::from(key_str.as_str()), value.clone()); } } ret diff --git a/clash-verge-rev/src-tauri/src/enhance/merge.rs b/clash-verge-rev/src-tauri/src/enhance/merge.rs index b93829e412..7579b585d8 100644 --- a/clash-verge-rev/src-tauri/src/enhance/merge.rs +++ b/clash-verge-rev/src-tauri/src/enhance/merge.rs @@ -1,4 +1,4 @@ -use crate::{logging, utils::logging::Type}; +use clash_verge_logging::{Type, logging}; use super::use_lowercase; use serde_yaml_ng::{self, Mapping, Value}; @@ -14,7 +14,7 @@ fn deep_merge(a: &mut Value, b: &Value) { } } -pub fn use_merge(merge: Mapping, config: Mapping) -> Mapping { +pub fn use_merge(merge: &Mapping, config: Mapping) -> Mapping { let mut config = Value::from(config); let merge = use_lowercase(merge); @@ -61,7 +61,7 @@ fn test_merge() -> anyhow::Result<()> { let merge = serde_yaml_ng::from_str::(merge)?; let config = serde_yaml_ng::from_str::(config)?; - let _ = serde_yaml_ng::to_string(&use_merge(merge, config))?; + let _ = serde_yaml_ng::to_string(&use_merge(&merge, config))?; Ok(()) } diff --git a/clash-verge-rev/src-tauri/src/enhance/mod.rs b/clash-verge-rev/src-tauri/src/enhance/mod.rs index 6b74b74a08..f8e8ba05cf 100644 --- a/clash-verge-rev/src-tauri/src/enhance/mod.rs +++ b/clash-verge-rev/src-tauri/src/enhance/mod.rs @@ -13,12 +13,13 @@ use self::{ seq::{SeqMap, use_seq}, tun::use_tun, }; -use crate::constants; use crate::utils::dirs; use crate::{config::Config, utils::tmpl}; -use crate::{logging, utils::logging::Type}; -use serde_yaml_ng::Mapping; +use crate::{config::IVerge, constants}; +use clash_verge_logging::{Type, logging}; +use serde_yaml_ng::{Mapping, Value}; use smartstring::alias::String; +use std::borrow::Cow; use std::collections::{HashMap, HashSet}; use tokio::fs; @@ -89,34 +90,41 @@ impl Default for ProfileItems { } async fn get_config_values() -> ConfigValues { - let clash_config = { Config::clash().await.latest_arc().0.clone() }; + let clash = Config::clash().await; + let clash_arc = clash.latest_arc(); + let clash_config = clash_arc.0.clone(); + drop(clash_arc); + drop(clash); - let (clash_core, enable_tun, enable_builtin, socks_enabled, http_enabled, enable_dns_settings) = { - let verge = Config::verge().await; - let verge = verge.latest_arc(); - ( - Some(verge.get_valid_clash_core()), - verge.enable_tun_mode.unwrap_or(false), - verge.enable_builtin_enhanced.unwrap_or(true), - verge.verge_socks_enabled.unwrap_or(false), - verge.verge_http_enabled.unwrap_or(false), - verge.enable_dns_settings.unwrap_or(false), - ) - }; + let verge = Config::verge().await; + + let verge_arc = verge.latest_arc(); + let IVerge { + ref enable_tun_mode, + ref enable_builtin_enhanced, + ref verge_socks_enabled, + ref verge_http_enabled, + ref enable_dns_settings, + .. + } = **verge_arc; + + let (clash_core, enable_tun, enable_builtin, socks_enabled, http_enabled, enable_dns_settings) = ( + Some(verge_arc.get_valid_clash_core()), + enable_tun_mode.unwrap_or(false), + enable_builtin_enhanced.unwrap_or(true), + verge_socks_enabled.unwrap_or(false), + verge_http_enabled.unwrap_or(false), + enable_dns_settings.unwrap_or(false), + ); #[cfg(not(target_os = "windows"))] - let redir_enabled = { - let verge = Config::verge().await; - let verge = verge.latest_arc(); - verge.verge_redir_enabled.unwrap_or(false) - }; + let redir_enabled = verge_arc.verge_redir_enabled.unwrap_or(false); #[cfg(target_os = "linux")] - let tproxy_enabled = { - let verge = Config::verge().await; - let verge = verge.latest_arc(); - verge.verge_tproxy_enabled.unwrap_or(false) - }; + let tproxy_enabled = verge_arc.verge_tproxy_enabled.unwrap_or(false); + + drop(verge_arc); + drop(verge); ConfigValues { clash_config, @@ -135,66 +143,62 @@ async fn get_config_values() -> ConfigValues { #[allow(clippy::cognitive_complexity)] async fn collect_profile_items() -> ProfileItems { - // 从profiles里拿东西 - 先收集需要的数据,然后释放锁 - let (current, merge_uid, script_uid, rules_uid, proxies_uid, groups_uid, name) = { - let current = { - let profiles = Config::profiles().await; - let profiles_clone = profiles.latest_arc(); - profiles_clone.current_mapping().await.unwrap_or_default() - }; + let profiles = Config::profiles().await; + let profiles_arc = profiles.latest_arc(); + drop(profiles); - let profiles = Config::profiles().await; - let profiles_ref = profiles.latest_arc(); - let current_profile_uid = match profiles_ref.get_current() { - Some(uid) => uid.clone(), - None => return ProfileItems::default(), - }; + let current = profiles_arc.current_mapping().await.unwrap_or_default(); - let current_item = match profiles_ref.get_item_arc(¤t_profile_uid) { - Some(item) => item, - None => return ProfileItems::default(), - }; - - let merge_uid = current_item - .current_merge() - .unwrap_or_else(|| "Merge".into()); - let script_uid = current_item - .current_script() - .unwrap_or_else(|| "Script".into()); - let rules_uid = current_item - .current_rules() - .unwrap_or_else(|| "Rules".into()); - let proxies_uid = current_item - .current_proxies() - .unwrap_or_else(|| "Proxies".into()); - let groups_uid = current_item - .current_groups() - .unwrap_or_else(|| "Groups".into()); - - let name = profiles_ref - .get_item(¤t_profile_uid) - .ok() - .and_then(|item| item.name.clone()) - .unwrap_or_default(); - - ( - current, - merge_uid, - script_uid, - rules_uid, - proxies_uid, - groups_uid, - name, - ) + let current_profile_uid = match profiles_arc.get_current() { + Some(uid) => uid, + None => { + drop(profiles_arc); + return ProfileItems::default(); + } }; - // 现在获取具体的items,此时profiles锁已经释放 + let current_item = match profiles_arc.get_item(current_profile_uid) { + Ok(item) => item, + Err(_) => { + drop(profiles_arc); + return ProfileItems::default(); + } + }; + + let merge_uid: Cow<'_, str> = if let Some(s) = current_item.current_merge() { + Cow::Borrowed(s) + } else { + Cow::Owned("Merge".into()) + }; + let script_uid: Cow<'_, str> = if let Some(s) = current_item.current_script() { + Cow::Borrowed(s) + } else { + Cow::Owned("Script".into()) + }; + let rules_uid: Cow<'_, str> = if let Some(s) = current_item.current_rules() { + Cow::Borrowed(s) + } else { + Cow::Owned("Rules".into()) + }; + let proxies_uid: Cow<'_, str> = if let Some(s) = current_item.current_proxies() { + Cow::Borrowed(s) + } else { + Cow::Owned("Proxies".into()) + }; + let groups_uid: Cow<'_, str> = if let Some(s) = current_item.current_groups() { + Cow::Borrowed(s) + } else { + Cow::Owned("Groups".into()) + }; + + let name = profiles_arc + .get_item(current_profile_uid) + .ok() + .and_then(|item| item.name.clone()) + .unwrap_or_default(); + let merge_item = { - let item = { - let profiles = Config::profiles().await; - let profiles = profiles.latest_arc(); - profiles.get_item(&merge_uid).ok().cloned() - }; + let item = profiles_arc.get_item(&merge_uid).ok().cloned(); if let Some(item) = item { >::from_async(&item).await } else { @@ -207,11 +211,7 @@ async fn collect_profile_items() -> ProfileItems { }); let script_item = { - let item = { - let profiles = Config::profiles().await; - let profiles = profiles.latest_arc(); - profiles.get_item(&script_uid).ok().cloned() - }; + let item = profiles_arc.get_item(&script_uid).ok().cloned(); if let Some(item) = item { >::from_async(&item).await } else { @@ -224,11 +224,7 @@ async fn collect_profile_items() -> ProfileItems { }); let rules_item = { - let item = { - let profiles = Config::profiles().await; - let profiles = profiles.latest_arc(); - profiles.get_item(&rules_uid).ok().cloned() - }; + let item = profiles_arc.get_item(&rules_uid).ok().cloned(); if let Some(item) = item { >::from_async(&item).await } else { @@ -241,11 +237,7 @@ async fn collect_profile_items() -> ProfileItems { }); let proxies_item = { - let item = { - let profiles = Config::profiles().await; - let profiles = profiles.latest_arc(); - profiles.get_item(&proxies_uid).ok().cloned() - }; + let item = profiles_arc.get_item(&proxies_uid).ok().cloned(); if let Some(item) = item { >::from_async(&item).await } else { @@ -258,11 +250,7 @@ async fn collect_profile_items() -> ProfileItems { }); let groups_item = { - let item = { - let profiles = Config::profiles().await; - let profiles = profiles.latest_arc(); - profiles.get_item(&groups_uid).ok().cloned() - }; + let item = profiles_arc.get_item(&groups_uid).ok().cloned(); if let Some(item) = item { >::from_async(&item).await } else { @@ -275,11 +263,7 @@ async fn collect_profile_items() -> ProfileItems { }); let global_merge = { - let item = { - let profiles = Config::profiles().await; - let profiles = profiles.latest_arc(); - profiles.get_item("Merge").ok().cloned() - }; + let item = profiles_arc.get_item("Merge").ok().cloned(); if let Some(item) = item { >::from_async(&item).await } else { @@ -292,11 +276,7 @@ async fn collect_profile_items() -> ProfileItems { }); let global_script = { - let item = { - let profiles = Config::profiles().await; - let profiles = profiles.latest_arc(); - profiles.get_item("Script").ok().cloned() - }; + let item = profiles_arc.get_item("Script").ok().cloned(); if let Some(item) = item { >::from_async(&item).await } else { @@ -308,6 +288,8 @@ async fn collect_profile_items() -> ProfileItems { data: ChainType::Script(tmpl::ITEM_SCRIPT.into()), }); + drop(profiles_arc); + ProfileItems { config: current, merge_item, @@ -325,19 +307,19 @@ fn process_global_items( mut config: Mapping, global_merge: ChainItem, global_script: ChainItem, - profile_name: String, + profile_name: &String, ) -> (Mapping, Vec, HashMap) { let mut result_map = HashMap::new(); let mut exists_keys = use_keys(&config); if let ChainType::Merge(merge) = global_merge.data { exists_keys.extend(use_keys(&merge)); - config = use_merge(merge, config.to_owned()); + config = use_merge(&merge, config.to_owned()); } if let ChainType::Script(script) = global_script.data { let mut logs = vec![]; - match use_script(script, config.to_owned(), profile_name) { + match use_script(script, &config, profile_name) { Ok((res_config, res_logs)) => { exists_keys.extend(use_keys(&res_config)); config = res_config; @@ -361,7 +343,7 @@ fn process_profile_items( groups_item: ChainItem, merge_item: ChainItem, script_item: ChainItem, - profile_name: String, + profile_name: &String, ) -> (Mapping, Vec, HashMap) { if let ChainType::Rules(rules) = rules_item.data { config = use_seq(rules, config.to_owned(), "rules"); @@ -377,12 +359,12 @@ fn process_profile_items( if let ChainType::Merge(merge) = merge_item.data { exists_keys.extend(use_keys(&merge)); - config = use_merge(merge, config.to_owned()); + config = use_merge(&merge, config.to_owned()); } if let ChainType::Script(script) = script_item.data { let mut logs = vec![]; - match use_script(script, config.to_owned(), profile_name) { + match use_script(script, &config, profile_name) { Ok((res_config, res_logs)) => { exists_keys.extend(use_keys(&res_config)); config = res_config; @@ -425,7 +407,7 @@ async fn merge_default_config( } #[cfg(target_os = "windows")] { - if key.as_str() == Some("redir-port") || key.as_str() == Some("tproxy-port") { + if key.as_str() == Some("redir-port") { continue; } } @@ -443,6 +425,13 @@ async fn merge_default_config( continue; } } + #[cfg(not(target_os = "linux"))] + { + if key.as_str() == Some("tproxy-port") { + config.remove("tproxy-port"); + continue; + } + } // 处理 external-controller 键的开关逻辑 if key.as_str() == Some("external-controller") { let enable_external_controller = Config::verge() @@ -479,7 +468,7 @@ fn apply_builtin_scripts( .for_each(|item| { logging!(debug, Type::Core, "run builtin script {}", item.uid); if let ChainType::Script(script) = item.data { - match use_script(script, config.to_owned(), "".into()) { + match use_script(script, &config, &String::from("")) { Ok((res_config, _)) => { config = res_config; } @@ -494,6 +483,88 @@ fn apply_builtin_scripts( config } +fn cleanup_proxy_groups(mut config: Mapping) -> Mapping { + const BUILTIN_POLICIES: &[&str] = &["DIRECT", "REJECT", "REJECT-DROP", "PASS"]; + + let proxy_names = config + .get("proxies") + .and_then(|v| v.as_sequence()) + .map(|seq| { + seq.iter() + .filter_map(|item| match item { + Value::Mapping(map) => map + .get("name") + .and_then(Value::as_str) + .map(|name| name.to_owned().into()), + Value::String(name) => Some(name.to_owned().into()), + _ => None, + }) + .collect::>() + }) + .unwrap_or_default(); + + let group_names = config + .get("proxy-groups") + .and_then(|v| v.as_sequence()) + .map(|seq| { + seq.iter() + .filter_map(|item| { + item.as_mapping() + .and_then(|map| map.get("name")) + .and_then(Value::as_str) + .map(std::convert::Into::into) + }) + .collect::>() + }) + .unwrap_or_default(); + + let provider_names = config + .get("proxy-providers") + .and_then(Value::as_mapping) + .map(|map| { + map.keys() + .filter_map(Value::as_str) + .map(std::convert::Into::into) + .collect::>() + }) + .unwrap_or_default(); + + let mut allowed_names = proxy_names; + allowed_names.extend(group_names); + allowed_names.extend(provider_names.iter().cloned()); + allowed_names.extend(BUILTIN_POLICIES.iter().map(|p| (*p).into())); + + if let Some(Value::Sequence(groups)) = config.get_mut("proxy-groups") { + for group in groups { + if let Some(group_map) = group.as_mapping_mut() { + let mut has_valid_provider = false; + + if let Some(Value::Sequence(uses)) = group_map.get_mut("use") { + uses.retain(|provider| match provider { + Value::String(name) => { + let exists = provider_names.contains(name.as_str()); + has_valid_provider = has_valid_provider || exists; + exists + } + _ => false, + }); + } + + if let Some(Value::Sequence(proxies)) = group_map.get_mut("proxies") { + proxies.retain(|proxy| match proxy { + Value::String(name) => { + allowed_names.contains(name.as_str()) || has_valid_provider + } + _ => true, + }); + } + } + } + } + + config +} + async fn apply_dns_settings(mut config: Mapping, enable_dns_settings: bool) -> Mapping { if enable_dns_settings && let Ok(app_dir) = dirs::app_home_dir() { let dns_path = app_dir.join(constants::files::DNS_CONFIG); @@ -526,7 +597,7 @@ async fn apply_dns_settings(mut config: Mapping, enable_dns_settings: bool) -> M /// Enhance mode /// 返回最终订阅、该订阅包含的键、和script执行的结果 -pub async fn enhance() -> (Mapping, Vec, HashMap) { +pub async fn enhance() -> (Mapping, HashSet, HashMap) { // gather config values let cfg_vals = get_config_values().await; let ConfigValues { @@ -557,7 +628,7 @@ pub async fn enhance() -> (Mapping, Vec, HashMap) { // process globals let (config, exists_keys, result_map) = - process_global_items(config, global_merge, global_script, profile_name.clone()); + process_global_items(config, global_merge, global_script, &profile_name); // process profile-specific items let (config, exists_keys, result_map) = process_profile_items( @@ -569,7 +640,7 @@ pub async fn enhance() -> (Mapping, Vec, HashMap) { groups_item, merge_item, script_item, - profile_name, + &profile_name, ); // merge default clash config @@ -588,15 +659,189 @@ pub async fn enhance() -> (Mapping, Vec, HashMap) { // builtin scripts let mut config = apply_builtin_scripts(config, clash_core, enable_builtin); + config = cleanup_proxy_groups(config); + config = use_tun(config, enable_tun); config = use_sort(config); // dns settings config = apply_dns_settings(config, enable_dns_settings).await; - let mut exists_set = HashSet::new(); - exists_set.extend(exists_keys); - let exists_keys: Vec = exists_set.into_iter().collect(); + let mut exists_keys_set = HashSet::new(); + exists_keys_set.extend(exists_keys); - (config, exists_keys, result_map) + (config, exists_keys_set, result_map) +} + +#[allow(clippy::expect_used)] +#[cfg(test)] +mod tests { + use super::cleanup_proxy_groups; + + #[test] + fn remove_missing_proxies_from_groups() { + let config_str = r#" +proxies: + - name: "alive-node" + type: ss +proxy-groups: + - name: "manual" + type: select + proxies: + - "alive-node" + - "missing-node" + - "DIRECT" + - name: "nested" + type: select + proxies: + - "manual" + - "ghost" +"#; + + let mut config: serde_yaml_ng::Mapping = + serde_yaml_ng::from_str(config_str).expect("Failed to parse test yaml"); + config = cleanup_proxy_groups(config); + + let groups = config + .get("proxy-groups") + .and_then(|v| v.as_sequence()) + .cloned() + .expect("proxy-groups should be a sequence"); + + let manual_group = groups + .iter() + .find(|group| { + group.get("name").and_then(serde_yaml_ng::Value::as_str) == Some("manual") + }) + .and_then(|group| group.as_mapping()) + .expect("manual group should exist"); + + let manual_proxies = manual_group + .get("proxies") + .and_then(|v| v.as_sequence()) + .expect("manual proxies should be a sequence"); + + assert_eq!(manual_proxies.len(), 2); + assert!( + manual_proxies + .iter() + .any(|p| p.as_str() == Some("alive-node")) + ); + assert!(manual_proxies.iter().any(|p| p.as_str() == Some("DIRECT"))); + + let nested_group = groups + .iter() + .find(|group| { + group.get("name").and_then(serde_yaml_ng::Value::as_str) == Some("nested") + }) + .and_then(|group| group.as_mapping()) + .expect("nested group should exist"); + + let nested_proxies = nested_group + .get("proxies") + .and_then(|v| v.as_sequence()) + .expect("nested proxies should be a sequence"); + + assert_eq!(nested_proxies.len(), 1); + assert_eq!(nested_proxies[0].as_str(), Some("manual")); + } + + #[test] + fn keep_provider_backed_groups_intact() { + let config_str = r#" +proxy-providers: + providerA: + type: http + url: https://example.com + path: ./providerA.yaml +proxies: [] +proxy-groups: + - name: "manual" + type: select + use: + - "providerA" + - "ghostProvider" + proxies: + - "dynamic-node" + - "DIRECT" +"#; + + let mut config: serde_yaml_ng::Mapping = + serde_yaml_ng::from_str(config_str).expect("Failed to parse test yaml"); + config = cleanup_proxy_groups(config); + + let groups = config + .get("proxy-groups") + .and_then(|v| v.as_sequence()) + .cloned() + .expect("proxy-groups should be a sequence"); + + let manual_group = groups + .iter() + .find(|group| { + group.get("name").and_then(serde_yaml_ng::Value::as_str) == Some("manual") + }) + .and_then(|group| group.as_mapping()) + .expect("manual group should exist"); + + let uses = manual_group + .get("use") + .and_then(|v| v.as_sequence()) + .expect("use should be a sequence"); + assert_eq!(uses.len(), 1); + assert_eq!(uses[0].as_str(), Some("providerA")); + + let proxies = manual_group + .get("proxies") + .and_then(|v| v.as_sequence()) + .expect("proxies should be a sequence"); + assert_eq!(proxies.len(), 2); + assert!(proxies.iter().any(|p| p.as_str() == Some("dynamic-node"))); + assert!(proxies.iter().any(|p| p.as_str() == Some("DIRECT"))); + } + + #[test] + fn prune_invalid_provider_and_proxies_without_provider() { + let config_str = r#" +proxy-groups: + - name: "manual" + type: select + use: + - "ghost-provider" + proxies: + - "ghost-node" + - "DIRECT" +"#; + + let mut config: serde_yaml_ng::Mapping = + serde_yaml_ng::from_str(config_str).expect("Failed to parse test yaml"); + config = cleanup_proxy_groups(config); + + let groups = config + .get("proxy-groups") + .and_then(|v| v.as_sequence()) + .cloned() + .expect("proxy-groups should be a sequence"); + + let manual_group = groups + .iter() + .find(|group| { + group.get("name").and_then(serde_yaml_ng::Value::as_str) == Some("manual") + }) + .and_then(|group| group.as_mapping()) + .expect("manual group should exist"); + + let uses = manual_group + .get("use") + .and_then(|v| v.as_sequence()) + .expect("use should be a sequence"); + assert_eq!(uses.len(), 0); + + let proxies = manual_group + .get("proxies") + .and_then(|v| v.as_sequence()) + .expect("proxies should be a sequence"); + assert_eq!(proxies.len(), 1); + assert_eq!(proxies[0].as_str(), Some("DIRECT")); + } } diff --git a/clash-verge-rev/src-tauri/src/enhance/script.rs b/clash-verge-rev/src-tauri/src/enhance/script.rs index 799c19dc37..d1c19184ba 100644 --- a/clash-verge-rev/src-tauri/src/enhance/script.rs +++ b/clash-verge-rev/src-tauri/src/enhance/script.rs @@ -1,56 +1,77 @@ use super::use_lowercase; use anyhow::{Error, Result}; +use boa_engine::{Context, JsString, JsValue, Source, native_function::NativeFunction}; +use clash_verge_logging::{Type, logging_error}; +use parking_lot::Mutex; use serde_yaml_ng::Mapping; use smartstring::alias::String; +use std::sync::Arc; +const MAX_OUTPUTS: usize = 1000; +const MAX_OUTPUT_SIZE: usize = 1024 * 1024; // 1MB +const MAX_JSON_SIZE: usize = 10 * 1024 * 1024; // 10MB + +// TODO 使用引用改进上下相关处理,避免不必要 Clone pub fn use_script( script: String, - config: Mapping, - name: String, + config: &Mapping, + name: &String, ) -> Result<(Mapping, Vec<(String, String)>)> { - use boa_engine::{Context, JsString, JsValue, Source, native_function::NativeFunction}; - use std::{cell::RefCell, rc::Rc}; let mut context = Context::default(); - let outputs = Rc::new(RefCell::new(vec![])); + let outputs = Arc::new(Mutex::new(vec![])); + let total_size = Arc::new(Mutex::new(0usize)); - let copy_outputs = Rc::clone(&outputs); - unsafe { - let _ = context.register_global_builtin_callable( - "__verge_log__".into(), - 2, - NativeFunction::from_closure( - move |_: &JsValue, args: &[JsValue], context: &mut Context| { - let level = args.first().ok_or_else(|| { - boa_engine::JsError::from_opaque( - JsString::from("Missing level argument").into(), - ) - })?; - let level = level.to_string(context)?; - let level = level.to_std_string().map_err(|_| { - boa_engine::JsError::from_opaque( - JsString::from("Failed to convert level to string").into(), - ) - })?; + let outputs_clone = Arc::clone(&outputs); + let total_size_clone = Arc::clone(&total_size); + + let _ = context.register_global_builtin_callable("__verge_log__".into(), 2, unsafe { + NativeFunction::from_closure( + move |_: &JsValue, args: &[JsValue], context: &mut Context| { + let level = args.first().ok_or_else(|| { + boa_engine::JsError::from_opaque( + JsString::from("Missing level argument").into(), + ) + })?; + let level = level.to_string(context)?; + let level = level.to_std_string().map_err(|_| { + boa_engine::JsError::from_opaque( + JsString::from("Failed to convert level to string").into(), + ) + })?; + + let data = args.get(1).ok_or_else(|| { + boa_engine::JsError::from_opaque(JsString::from("Missing data argument").into()) + })?; + let data = data.to_string(context)?; + let data = data.to_std_string().map_err(|_| { + boa_engine::JsError::from_opaque( + JsString::from("Failed to convert data to string").into(), + ) + })?; + + // 检查输出限制 + if outputs_clone.lock().len() >= MAX_OUTPUTS { + return Err(boa_engine::JsError::from_opaque( + JsString::from("Maximum number of log outputs exceeded").into(), + )); + } + + let mut size = total_size_clone.lock(); + let new_size = *size + level.len() + data.len(); + if new_size > MAX_OUTPUT_SIZE { + return Err(boa_engine::JsError::from_opaque( + JsString::from("Maximum output size exceeded").into(), + )); + } + *size = new_size; + drop(size); + outputs_clone.lock().push((level.into(), data.into())); + Ok(JsValue::undefined()) + }, + ) + }); - let data = args.get(1).ok_or_else(|| { - boa_engine::JsError::from_opaque( - JsString::from("Missing data argument").into(), - ) - })?; - let data = data.to_string(context)?; - let data = data.to_std_string().map_err(|_| { - boa_engine::JsError::from_opaque( - JsString::from("Failed to convert data to string").into(), - ) - })?; - let mut out = copy_outputs.borrow_mut(); - out.push((level.into(), data.into())); - Ok(JsValue::undefined()) - }, - ), - ); - } let _ = context.eval(Source::from_bytes( r#"var console = Object.freeze({ log(data){__verge_log__("log",JSON.stringify(data, null, 2))}, @@ -64,9 +85,15 @@ pub fn use_script( let config = use_lowercase(config); let config_str = serde_json::to_string(&config)?; + if config_str.len() > MAX_JSON_SIZE { + anyhow::bail!("Configuration size exceeds maximum allowed size"); + } // 仅处理 name 参数中的特殊字符 - let safe_name = escape_js_string_for_single_quote(&name); + let safe_name = escape_js_string_for_single_quote(name); + if safe_name.len() > 1024 { + anyhow::bail!("Name parameter too long"); + } let code = format!( r"try{{ @@ -88,15 +115,25 @@ pub fn use_script( .to_std_string() .map_err(|_| anyhow::anyhow!("Failed to convert JS string to std string"))?; - // 直接解析JSON结果,不做其他解析 + if result.len() > MAX_JSON_SIZE { + anyhow::bail!("Script result exceeds maximum allowed size"); + } + let res: Result = parse_json_safely(&result); - let mut out = outputs.borrow_mut(); match res { - Ok(config) => Ok((use_lowercase(config), out.to_vec())), + Ok(config) => Ok((use_lowercase(&config), outputs.lock().to_vec())), Err(err) => { - out.push(("exception".into(), err.to_string().into())); - Ok((config, out.to_vec())) + outputs + .lock() + .push(("exception".into(), "Script execution failed".into())); + logging_error!( + Type::Config, + "Script execution error: {}. Script name: {}", + err, + name + ); + Ok((config, outputs.lock().to_vec())) } } } else { @@ -105,14 +142,22 @@ pub fn use_script( } fn parse_json_safely(json_str: &str) -> Result { - let json_str = strip_outer_quotes(json_str); + if json_str.len() > MAX_JSON_SIZE { + anyhow::bail!("JSON string too large"); + } + let json_str = strip_outer_quotes(json_str); Ok(serde_json::from_str::(json_str)?) } -// 移除字符串外层的引号 +// 安全地移除外层引号 fn strip_outer_quotes(s: &str) -> &str { let s = s.trim(); + + if s.len() < 2 { + return s; + } + if (s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\'')) { &s[1..s.len() - 1] } else { @@ -120,9 +165,18 @@ fn strip_outer_quotes(s: &str) -> &str { } } -// 转义单引号和反斜杠,用于单引号包裹的JavaScript字符串 +// 安全地转义字符串 fn escape_js_string_for_single_quote(s: &str) -> String { - s.replace('\\', "\\\\").replace('\'', "\\'").into() + // 限制处理的字符串长度 + if s.len() > 10240 { + return s[..10240].replace('\\', "\\\\").replace('\'', "\\'").into(); + } + + s.replace('\\', "\\\\") + .replace('\'', "\\'") + .replace('\n', "\\n") // 添加换行符转义 + .replace('\r', "\\r") // 添加回车转义 + .into() } #[test] @@ -150,8 +204,8 @@ fn test_script() { enable: false "; - let config = serde_yaml_ng::from_str(config).expect("Failed to parse test config YAML"); - let (config, results) = use_script(script.into(), config, "".into()) + let config = &serde_yaml_ng::from_str(config).expect("Failed to parse test config YAML"); + let (config, results) = use_script(script.into(), config, &String::from("")) .expect("Script execution should succeed in test"); let _ = serde_yaml_ng::to_string(&config).expect("Failed to serialize config to YAML"); @@ -182,3 +236,32 @@ fn test_escape_unescape() { assert!(parsed_quoted.contains_key("key")); assert!(parsed_quoted.contains_key("nested")); } + +#[test] +fn test_strip_outer_quotes_edge_cases() { + assert_eq!(strip_outer_quotes(""), ""); + assert_eq!(strip_outer_quotes("'"), "'"); + assert_eq!(strip_outer_quotes("\""), "\""); + assert_eq!(strip_outer_quotes("''"), ""); + assert_eq!(strip_outer_quotes("\"\""), ""); + assert_eq!(strip_outer_quotes("'a'"), "a"); +} + +#[test] +fn test_memory_limits() { + // 测试输出限制 + let script = r#" + function main(config) { + for(let i = 0; i < 2000; i++) { + console.log("test"); + } + return config; + } + "#; + + #[allow(clippy::expect_used)] + let config = &serde_yaml_ng::from_str("test: value").expect("Failed to parse test YAML"); + let result = use_script(script.into(), config, &String::from("")); + // 应该失败或被限制 + assert!(result.is_ok()); // 会被限制但不会 panic +} diff --git a/clash-verge-rev/src-tauri/src/enhance/seq.rs b/clash-verge-rev/src-tauri/src/enhance/seq.rs index 97d143303e..c978fc76b2 100644 --- a/clash-verge-rev/src-tauri/src/enhance/seq.rs +++ b/clash-verge-rev/src-tauri/src/enhance/seq.rs @@ -50,7 +50,6 @@ pub fn use_seq(seq: SeqMap, mut config: Mapping, field: &str) -> Mapping { let mut new_groups = Sequence::new(); for group in groups { if let Value::Mapping(group_map) = group { - let mut new_group = group_map.clone(); if let Some(Value::Sequence(proxies)) = group_map.get("proxies") { let filtered_proxies: Sequence = proxies .iter() @@ -63,14 +62,14 @@ pub fn use_seq(seq: SeqMap, mut config: Mapping, field: &str) -> Mapping { }) .cloned() .collect(); - new_group.insert( + group_map.insert( Value::String("proxies".into()), Value::Sequence(filtered_proxies), ); } - new_groups.push(Value::Mapping(new_group)); + new_groups.push(Value::Mapping(group_map.to_owned())); } else { - new_groups.push(group.clone()); + new_groups.push(group.to_owned()); } } config.insert( diff --git a/clash-verge-rev/src-tauri/src/feat/backup.rs b/clash-verge-rev/src-tauri/src/feat/backup.rs index 055b1d6804..a155bafc6d 100644 --- a/clash-verge-rev/src-tauri/src/feat/backup.rs +++ b/clash-verge-rev/src-tauri/src/feat/backup.rs @@ -1,15 +1,15 @@ use crate::{ config::{Config, IVerge}, core::backup, - logging, logging_error, process::AsyncHandler, utils::{ - dirs::{PathBufExec as _, app_home_dir, local_backup_dir}, - logging::Type, + dirs::{PathBufExec as _, app_home_dir, local_backup_dir, verge_path}, + help, }, }; use anyhow::{Result, anyhow}; use chrono::Utc; +use clash_verge_logging::{Type, logging}; use reqwest_dav::list_cmd::ListFile; use serde::Serialize; use smartstring::alias::String; @@ -24,6 +24,38 @@ pub struct LocalBackupFile { pub content_length: u64, } +/// Load restored verge.yaml from disk, merge back WebDAV creds, save, and sync memory. +async fn finalize_restored_verge_config( + webdav_url: Option, + webdav_username: Option, + webdav_password: Option, +) -> Result<()> { + // Do NOT silently fallback to defaults; a broken/missing verge.yaml means restore failed. + // Propagate the error so the UI/user can react accordingly. + let mut restored = help::read_yaml::(&verge_path()?).await?; + restored.webdav_url = webdav_url; + restored.webdav_username = webdav_username; + restored.webdav_password = webdav_password; + restored.save_file().await?; + + let verge_draft = Config::verge().await; + verge_draft.edit_draft(|d| { + *d = restored.clone(); + }); + verge_draft.apply(); + + // Ensure side-effects (flags, tray, sysproxy, hotkeys, auto-backup refresh, etc.) run. + // Use not_save_file = true to avoid extra I/O (we already persisted the restored file). + if let Err(err) = super::patch_verge(&restored, true).await { + logging!( + error, + Type::Backup, + "Failed to apply restored verge config: {err:#?}" + ); + } + Ok(()) +} + /// Create a backup and upload to WebDAV pub async fn create_backup_and_upload_webdav() -> Result<()> { let (file_name, temp_file_path) = backup::create_backup().await.map_err(|err| { @@ -103,22 +135,10 @@ pub async fn restore_webdav_backup(filename: String) -> Result<()> { let file = AsyncHandler::spawn_blocking(move || std::fs::File::open(&value)).await??; let mut zip = zip::ZipArchive::new(file)?; zip.extract(app_home_dir()?)?; - logging_error!( - Type::Backup, - super::patch_verge( - &IVerge { - webdav_url, - webdav_username, - webdav_password, - ..IVerge::default() - }, - false - ) - .await - ); - // 最后删除临时文件 - backup_storage_path.remove_if_exists().await?; - Ok(()) + let res = finalize_restored_verge_config(webdav_url, webdav_username, webdav_password).await; + // Finally remove the temp file (attempt cleanup even if finalize fails) + let _ = backup_storage_path.remove_if_exists().await; + res } /// Create a backup and save to local storage @@ -264,19 +284,7 @@ pub async fn restore_local_backup(filename: String) -> Result<()> { let file = AsyncHandler::spawn_blocking(move || std::fs::File::open(&target_path)).await??; let mut zip = zip::ZipArchive::new(file)?; zip.extract(app_home_dir()?)?; - logging_error!( - Type::Backup, - super::patch_verge( - &IVerge { - webdav_url, - webdav_username, - webdav_password, - ..IVerge::default() - }, - false - ) - .await - ); + finalize_restored_verge_config(webdav_url, webdav_username, webdav_password).await?; Ok(()) } diff --git a/clash-verge-rev/src-tauri/src/feat/clash.rs b/clash-verge-rev/src-tauri/src/feat/clash.rs index 43e76157a7..5142791ac2 100644 --- a/clash-verge-rev/src-tauri/src/feat/clash.rs +++ b/clash-verge-rev/src-tauri/src/feat/clash.rs @@ -1,10 +1,11 @@ use crate::{ config::Config, core::{CoreManager, handle, tray}, - logging, logging_error, + feat::clean_async, process::AsyncHandler, - utils::{self, logging::Type, resolve}, + utils::{self, resolve::reset_resolve_done}, }; +use clash_verge_logging::{Type, logging, logging_error}; use serde_yaml_ng::{Mapping, Value}; use smartstring::alias::String; @@ -24,16 +25,24 @@ pub async fn restart_clash_core() { /// Restart the application pub async fn restart_app() { - utils::server::shutdown_embedded_server(); - if let Err(err) = resolve::resolve_reset_async().await { - handle::Handle::notice_message( - "restart_app::error", - format!("Failed to cleanup resources: {err}"), - ); - logging!(error, Type::Core, "Restart failed during cleanup: {err}"); - return; - } + logging!(debug, Type::System, "启动重启应用流程"); + // 设置退出标志 + handle::Handle::global().set_is_exiting(); + utils::server::shutdown_embedded_server(); + Config::apply_all_and_save_file().await; + + logging!(info, Type::System, "开始异步清理资源"); + let cleanup_result = clean_async().await; + + logging!( + info, + Type::System, + "资源清理完成,退出代码: {}", + if cleanup_result { 0 } else { 1 } + ); + + reset_resolve_done(); let app_handle = handle::Handle::app_handle(); app_handle.restart(); } @@ -75,7 +84,7 @@ pub async fn change_clash_mode(mode: String) { // 更新订阅 Config::clash() .await - .edit_draft(|d| d.patch_config(mapping)); + .edit_draft(|d| d.patch_config(&mapping)); // 分离数据获取和异步调用 let clash_data = Config::clash().await.data_arc(); diff --git a/clash-verge-rev/src-tauri/src/feat/config.rs b/clash-verge-rev/src-tauri/src/feat/config.rs index cfa9aef00f..07ca1a2e95 100644 --- a/clash-verge-rev/src-tauri/src/feat/config.rs +++ b/clash-verge-rev/src-tauri/src/feat/config.rs @@ -1,18 +1,16 @@ use crate::{ config::{Config, IVerge}, core::{CoreManager, handle, hotkey, sysopt, tray}, - logging_error, module::{auto_backup::AutoBackupManager, lightweight}, - utils::{draft::SharedBox, logging::Type}, }; use anyhow::Result; +use clash_verge_draft::SharedBox; +use clash_verge_logging::{Type, logging, logging_error}; use serde_yaml_ng::Mapping; /// Patch Clash configuration -pub async fn patch_clash(patch: Mapping) -> Result<()> { - Config::clash() - .await - .edit_draft(|d| d.patch_config(patch.clone())); +pub async fn patch_clash(patch: &Mapping) -> Result<()> { + Config::clash().await.edit_draft(|d| d.patch_config(patch)); let res = { // 激活订阅 @@ -107,6 +105,8 @@ fn determine_update_flags(patch: &IVerge) -> i32 { let enable_auto_light_weight = patch.enable_auto_light_weight_mode; let enable_external_controller = patch.enable_external_controller; let tray_inline_proxy_groups = patch.tray_inline_proxy_groups; + let enable_proxy_guard = patch.enable_proxy_guard; + let proxy_guard_duration = patch.proxy_guard_duration; if tun_mode.is_some() { update_flags |= UpdateFlags::ClashConfig as i32; @@ -144,7 +144,12 @@ fn determine_update_flags(patch: &IVerge) -> i32 { update_flags |= UpdateFlags::SystrayIcon as i32; } - if proxy_bypass.is_some() || pac_content.is_some() || pac.is_some() { + if proxy_bypass.is_some() + || pac_content.is_some() + || pac.is_some() + || enable_proxy_guard.is_some() + || proxy_guard_duration.is_some() + { update_flags |= UpdateFlags::SysProxy as i32; } @@ -207,6 +212,7 @@ async fn process_terminated_flags(update_flags: i32, patch: &IVerge) -> Result<( } if (update_flags & (UpdateFlags::SysProxy as i32)) != 0 { sysopt::Sysopt::global().update_sysproxy().await?; + sysopt::Sysopt::global().refresh_guard().await; } if (update_flags & (UpdateFlags::Hotkey as i32)) != 0 && let Some(hotkeys) = &patch.hotkeys @@ -258,6 +264,7 @@ pub async fn patch_verge(patch: &IVerge, not_save_file: bool) -> Result<()> { if !not_save_file { // 分离数据获取和异步调用 let verge_data = Config::verge().await.data_arc(); + logging!(info, Type::Setup, "Saving Verge configuration to file..."); verge_data.save_file().await?; } Ok(()) diff --git a/clash-verge-rev/src-tauri/src/feat/profile.rs b/clash-verge-rev/src-tauri/src/feat/profile.rs index 83446aeb9d..932cafdd7c 100644 --- a/clash-verge-rev/src-tauri/src/feat/profile.rs +++ b/clash-verge-rev/src-tauri/src/feat/profile.rs @@ -2,10 +2,9 @@ use crate::{ cmd, config::{Config, PrfItem, PrfOption, profiles::profiles_draft_update_item_safe}, core::{CoreManager, handle, tray}, - logging, logging_error, - utils::logging::Type, }; use anyhow::{Result, bail}; +use clash_verge_logging::{Type, logging, logging_error}; use smartstring::alias::String; use tauri::Emitter as _; @@ -119,7 +118,7 @@ async fn should_update_profile( "[订阅更新] {} 是远程订阅,URL: {}", uid, item.url - .clone() + .as_ref() .ok_or_else(|| anyhow::anyhow!("Profile URL is None"))? ); Ok(Some(( @@ -260,9 +259,6 @@ pub async fn update_profile( } /// 增强配置 -pub async fn enhance_profiles() -> Result<()> { - crate::core::CoreManager::global() - .update_config() - .await - .map(|_| ()) +pub async fn enhance_profiles() -> Result<(bool, String)> { + crate::core::CoreManager::global().update_config().await } diff --git a/clash-verge-rev/src-tauri/src/feat/proxy.rs b/clash-verge-rev/src-tauri/src/feat/proxy.rs index 69d4e03395..3f2cadc05e 100644 --- a/clash-verge-rev/src-tauri/src/feat/proxy.rs +++ b/clash-verge-rev/src-tauri/src/feat/proxy.rs @@ -1,9 +1,8 @@ use crate::{ config::{Config, IVerge}, core::handle, - logging, - utils::logging::Type, }; +use clash_verge_logging::{Type, logging}; use std::env; use tauri_plugin_clipboard_manager::ClipboardExt as _; @@ -61,43 +60,32 @@ pub async fn toggle_tun_mode(not_save_file: Option) { /// Copy proxy environment variables to clipboard pub async fn copy_clash_env() { - // 从环境变量获取IP地址,如果没有则从配置中获取 proxy_host,默认为 127.0.0.1 - let clash_verge_rev_ip = match env::var("CLASH_VERGE_REV_IP") { - Ok(ip) => ip.into(), - Err(_) => Config::verge() - .await - .latest_arc() - .proxy_host - .clone() - .unwrap_or_else(|| "127.0.0.1".into()), - }; + let env_ip = env::var("CLASH_VERGE_REV_IP").ok(); + let verge_cfg = Config::verge().await.latest_arc(); + let ip = env_ip + .as_deref() + .unwrap_or_else(|| verge_cfg.proxy_host.as_deref().unwrap_or("127.0.0.1")); let app_handle = handle::Handle::app_handle(); - let port = { - Config::verge() - .await - .latest_arc() - .verge_mixed_port - .unwrap_or(7897) - }; - let http_proxy = format!("http://{clash_verge_rev_ip}:{port}"); - let socks5_proxy = format!("socks5://{clash_verge_rev_ip}:{port}"); + let port = verge_cfg.verge_mixed_port.unwrap_or(7897); + let http_proxy = format!("http://{ip}:{port}"); + let socks5_proxy = format!("socks5://{ip}:{port}"); - let cliboard = app_handle.clipboard(); - let env_type = { Config::verge().await.latest_arc().env_type.clone() }; - let env_type = match env_type { - Some(env_type) => env_type, - None => { - #[cfg(not(target_os = "windows"))] - let default = "bash"; - #[cfg(target_os = "windows")] - let default = "powershell"; + let clipboard = app_handle.clipboard(); - default.into() + let default_env = { + #[cfg(not(target_os = "windows"))] + { + "bash" + } + #[cfg(target_os = "windows")] + { + "powershell" } }; + let env_type = verge_cfg.env_type.as_deref().unwrap_or(default_env); - let export_text = match env_type.as_str() { + let export_text = match env_type { "bash" => format!( "export https_proxy={http_proxy} http_proxy={http_proxy} all_proxy={socks5_proxy}" ), @@ -119,7 +107,7 @@ pub async fn copy_clash_env() { } }; - if cliboard.write_text(export_text).is_err() { + if clipboard.write_text(&export_text).is_err() { logging!(error, Type::ProxyMode, "Failed to write to clipboard"); } } diff --git a/clash-verge-rev/src-tauri/src/feat/window.rs b/clash-verge-rev/src-tauri/src/feat/window.rs index 876ba44322..fde7df86e3 100644 --- a/clash-verge-rev/src-tauri/src/feat/window.rs +++ b/clash-verge-rev/src-tauri/src/feat/window.rs @@ -1,9 +1,10 @@ use crate::config::Config; -use crate::core::event_driven_proxy::EventDrivenProxyManager; use crate::core::{CoreManager, handle, sysopt}; +use crate::module::lightweight; use crate::utils; use crate::utils::window_manager::WindowManager; -use crate::{logging, module::lightweight, utils::logging::Type}; +use clash_verge_logging::{Type, logging}; +use tokio::time::{Duration, timeout}; /// Public API: open or close the dashboard pub async fn open_or_close_dashboard() { @@ -19,12 +20,11 @@ async fn open_or_close_dashboard_internal() { pub async fn quit() { logging!(debug, Type::System, "启动退出流程"); - utils::server::shutdown_embedded_server(); - - // 获取应用句柄并设置退出标志 - let app_handle = handle::Handle::app_handle(); + // 设置退出标志 handle::Handle::global().set_is_exiting(); - EventDrivenProxyManager::global().notify_app_stopping(); + + utils::server::shutdown_embedded_server(); + Config::apply_all_and_save_file().await; logging!(info, Type::System, "开始异步清理资源"); let cleanup_result = clean_async().await; @@ -35,58 +35,16 @@ pub async fn quit() { "资源清理完成,退出代码: {}", if cleanup_result { 0 } else { 1 } ); + + let app_handle = handle::Handle::app_handle(); app_handle.exit(if cleanup_result { 0 } else { 1 }); } pub async fn clean_async() -> bool { - use tokio::time::{Duration, timeout}; - logging!(info, Type::System, "开始执行异步清理操作..."); - // 1. 处理TUN模式 - let tun_task = async { - let tun_enabled = Config::verge() - .await - .data_arc() - .enable_tun_mode - .unwrap_or(false); - - if !tun_enabled { - return true; - } - - let disable_tun = serde_json::json!({ "tun": { "enable": false } }); - - match timeout( - Duration::from_millis(1000), - handle::Handle::mihomo() - .await - .patch_base_config(&disable_tun), - ) - .await - { - Ok(Ok(_)) => { - logging!(info, Type::Window, "TUN模式已禁用"); - true - } - Ok(Err(e)) => { - logging!(warn, Type::Window, "Warning: 禁用TUN模式失败: {e}"); - // 超时不阻塞退出 - true - } - Err(_) => { - logging!( - warn, - Type::Window, - "Warning: 禁用TUN模式超时(可能系统正在关机),继续退出流程" - ); - true - } - } - }; - - // 2. 系统代理重置 - let proxy_task = async { + // 重置系统代理 + let proxy_task = tokio::task::spawn(async { #[cfg(target_os = "windows")] { use sysproxy::{Autoproxy, Sysproxy}; @@ -202,15 +160,50 @@ pub async fn clean_async() -> bool { } } } - }; + }); + + // 关闭 Tun 模式 + 停止核心服务 + let core_task = tokio::task::spawn(async { + logging!(info, Type::System, "disable tun"); + let tun_enabled = Config::verge() + .await + .data_arc() + .enable_tun_mode + .unwrap_or(false); + if tun_enabled { + let disable_tun = serde_json::json!({ "tun": { "enable": false } }); + + logging!(info, Type::System, "send disable tun request to mihomo"); + match timeout( + Duration::from_millis(1000), + handle::Handle::mihomo() + .await + .patch_base_config(&disable_tun), + ) + .await + { + Ok(Ok(_)) => { + logging!(info, Type::Window, "TUN模式已禁用"); + } + Ok(Err(e)) => { + logging!(warn, Type::Window, "Warning: 禁用TUN模式失败: {e}"); + } + Err(_) => { + logging!( + warn, + Type::Window, + "Warning: 禁用TUN模式超时(可能系统正在关机),继续退出流程" + ); + } + } + } - // 3. 核心服务停止 - let core_task = async { #[cfg(target_os = "windows")] let stop_timeout = Duration::from_secs(2); #[cfg(not(target_os = "windows"))] let stop_timeout = Duration::from_secs(3); + logging!(info, Type::System, "stop core"); match timeout(stop_timeout, CoreManager::global().stop_core()).await { Ok(_) => { logging!(info, Type::Window, "core已停止"); @@ -225,11 +218,11 @@ pub async fn clean_async() -> bool { true } } - }; + }); - // 4. DNS恢复(仅macOS) - #[cfg(target_os = "macos")] - let dns_task = async { + // DNS恢复(仅macOS) + let dns_task = tokio::task::spawn(async { + #[cfg(target_os = "macos")] match timeout( Duration::from_millis(1000), crate::utils::resolve::dns::restore_public_dns(), @@ -245,22 +238,23 @@ pub async fn clean_async() -> bool { false } } - }; + #[cfg(not(target_os = "macos"))] + true + }); - #[cfg(not(target_os = "macos"))] - let dns_task = async { true }; - - let tun_success = tun_task.await; // 并行执行清理任务 - let (proxy_success, core_success, dns_success) = tokio::join!(proxy_task, core_task, dns_task); + let (proxy_result, core_result, dns_result) = tokio::join!(proxy_task, core_task, dns_task); - let all_success = tun_success && proxy_success && core_success && dns_success; + let proxy_success = proxy_result.unwrap_or_default(); + let core_success = core_result.unwrap_or_default(); + let dns_success = dns_result.unwrap_or_default(); + + let all_success = proxy_success && core_success && dns_success; logging!( info, Type::System, - "异步关闭操作完成 - TUN: {}, 代理: {}, 核心: {}, DNS: {}, 总体: {}", - tun_success, + "异步关闭操作完成 - 代理: {}, 核心: {}, DNS: {}, 总体: {}", proxy_success, core_success, dns_success, @@ -270,41 +264,6 @@ pub async fn clean_async() -> bool { all_success } -pub fn clean() -> bool { - use crate::process::AsyncHandler; - - let (tx, rx) = std::sync::mpsc::channel(); - - AsyncHandler::spawn(move || async move { - logging!(info, Type::System, "开始执行关闭操作..."); - - // 使用已有的异步清理函数 - let cleanup_result = clean_async().await; - - let _ = tx.send(cleanup_result); - }); - - #[cfg(target_os = "windows")] - let total_timeout = std::time::Duration::from_secs(5); - #[cfg(not(target_os = "windows"))] - let total_timeout = std::time::Duration::from_secs(8); - - match rx.recv_timeout(total_timeout) { - Ok(result) => { - logging!(info, Type::System, "关闭操作完成,结果: {}", result); - result - } - Err(_) => { - logging!( - warn, - Type::System, - "清理操作超时(可能正在关机),返回成功避免阻塞" - ); - true - } - } -} - #[cfg(target_os = "macos")] pub async fn hide() { use crate::module::lightweight::add_light_weight_timer; diff --git a/clash-verge-rev/src-tauri/src/lib.rs b/clash-verge-rev/src-tauri/src/lib.rs index c3b374ca03..08e34c4a9e 100644 --- a/clash-verge-rev/src-tauri/src/lib.rs +++ b/clash-verge-rev/src-tauri/src/lib.rs @@ -10,22 +10,25 @@ mod feat; mod module; mod process; pub mod utils; -use crate::constants::files; #[cfg(target_os = "linux")] use crate::utils::linux; +use crate::utils::resolve::init_signal; +use crate::{constants::files, utils::resolve::prioritize_initialization}; use crate::{ - core::{EventDrivenProxyManager, handle}, + core::handle, process::AsyncHandler, utils::{resolve, server}, }; use anyhow::Result; +use clash_verge_logging::{Type, logging}; use once_cell::sync::OnceCell; use rust_i18n::i18n; +use std::time::Duration; use tauri::{AppHandle, Manager as _}; #[cfg(target_os = "macos")] use tauri_plugin_autostart::MacosLauncher; use tauri_plugin_deep_link::DeepLinkExt as _; -use utils::logging::Type; +use tauri_plugin_mihomo::RejectPolicy; i18n!("locales", fallback = "zh"); @@ -47,6 +50,7 @@ mod app_init { pub fn setup_plugins(builder: tauri::Builder) -> tauri::Builder { #[allow(unused_mut)] let mut builder = builder + .plugin(tauri_plugin_clash_verge_sysinfo::init()) .plugin(tauri_plugin_notification::init()) .plugin(tauri_plugin_updater::Builder::new().build()) .plugin(tauri_plugin_clipboard_manager::init()) @@ -63,10 +67,11 @@ mod app_init { .socket_path(crate::config::IClashTemp::guard_external_controller_ipc()) .pool_config( tauri_plugin_mihomo::IpcPoolConfigBuilder::new() - .min_connections(0) - .max_connections(20) - .idle_timeout(std::time::Duration::from_millis(500)) - .health_check_interval(std::time::Duration::from_secs(10)) + .min_connections(1) + .max_connections(32) + .idle_timeout(std::time::Duration::from_secs(60)) + .health_check_interval(std::time::Duration::from_secs(60)) + .reject_policy(RejectPolicy::Timeout(Duration::from_secs(3))) .build(), ) .build(), @@ -82,11 +87,11 @@ mod app_init { } /// Setup deep link handling - pub fn setup_deep_links(app: &tauri::App) -> Result<(), Box> { + pub fn setup_deep_links(app: &tauri::App) { #[cfg(any(target_os = "linux", all(debug_assertions, windows)))] { logging!(info, Type::Setup, "注册深层链接..."); - app.deep_link().register_all()?; + let _ = app.deep_link().register_all(); } app.deep_link().on_open_url(|event| { @@ -99,8 +104,6 @@ mod app_init { } }); }); - - Ok(()) } /// Setup autostart plugin @@ -134,6 +137,10 @@ mod app_init { pub fn generate_handlers() -> impl Fn(tauri::ipc::Invoke) -> bool + Send + Sync + 'static { tauri::generate_handler![ + tauri_plugin_clash_verge_sysinfo::commands::get_system_info, + tauri_plugin_clash_verge_sysinfo::commands::get_app_uptime, + tauri_plugin_clash_verge_sysinfo::commands::app_is_admin, + tauri_plugin_clash_verge_sysinfo::commands::export_diagnostic_info, cmd::get_sys_proxy, cmd::get_auto_proxy, cmd::open_app_dir, @@ -152,9 +159,7 @@ mod app_init { cmd::notify_ui_ready, cmd::update_ui_stage, cmd::get_running_mode, - cmd::get_app_uptime, cmd::get_auto_launch_status, - cmd::is_admin, cmd::entry_lightweight_mode, cmd::exit_lightweight_mode, cmd::install_service, @@ -215,8 +220,6 @@ mod app_init { cmd::list_webdav_backup, cmd::delete_webdav_backup, cmd::restore_webdav_backup, - cmd::export_diagnostic_info, - cmd::get_system_info, cmd::get_unlock_items, cmd::check_media_unlock, ] @@ -235,20 +238,19 @@ pub fn run() { let builder = app_init::setup_plugins(tauri::Builder::default()) .setup(|app| { - logging!(info, Type::Setup, "开始应用初始化..."); - #[allow(clippy::expect_used)] APP_HANDLE .set(app.app_handle().clone()) .expect("failed to set global app handle"); + let _handle = AsyncHandler::block_on(async { prioritize_initialization().await }); + + logging!(info, Type::Setup, "开始应用初始化..."); if let Err(e) = app_init::setup_autostart(app) { logging!(error, Type::Setup, "Failed to setup autostart: {}", e); } - if let Err(e) = app_init::setup_deep_links(app) { - logging!(error, Type::Setup, "Failed to setup deep links: {}", e); - } + app_init::setup_deep_links(app); if let Err(e) = app_init::setup_window_state(app) { logging!(error, Type::Setup, "Failed to setup window state: {}", e); @@ -257,6 +259,8 @@ pub fn run() { resolve::resolve_setup_handle(); resolve::resolve_setup_async(); resolve::resolve_setup_sync(); + init_signal(); + resolve::resolve_done(); logging!(info, Type::Setup, "初始化已启动"); Ok(()) @@ -271,10 +275,9 @@ pub fn run() { use crate::{ config::Config, core::{self, handle, hotkey}, - logging, process::AsyncHandler, - utils::logging::Type, }; + use clash_verge_logging::{Type, logging}; use tauri::AppHandle; #[cfg(target_os = "macos")] use tauri::Manager as _; @@ -424,6 +427,12 @@ pub fn run() { event_handlers::handle_reopen(has_visible_windows).await; }); } + #[cfg(target_os = "macos")] + tauri::RunEvent::Exit => AsyncHandler::block_on(async { + if !handle::Handle::global().is_exiting() { + feat::quit().await; + } + }), tauri::RunEvent::ExitRequested { api, code, .. } => { AsyncHandler::block_on(async { let _ = handle::Handle::mihomo() @@ -440,14 +449,6 @@ pub fn run() { api.prevent_exit(); } } - tauri::RunEvent::Exit => { - let handle = core::handle::Handle::global(); - if !handle.is_exiting() { - handle.set_is_exiting(); - EventDrivenProxyManager::global().notify_app_stopping(); - feat::clean(); - } - } tauri::RunEvent::WindowEvent { label, event, .. } if label == "main" => match event { tauri::WindowEvent::CloseRequested { .. } => { event_handlers::handle_window_close(&event); diff --git a/clash-verge-rev/src-tauri/src/module/auto_backup.rs b/clash-verge-rev/src-tauri/src/module/auto_backup.rs index 2cb9c89bbd..6c6b2dbc74 100644 --- a/clash-verge-rev/src-tauri/src/module/auto_backup.rs +++ b/clash-verge-rev/src-tauri/src/module/auto_backup.rs @@ -1,12 +1,12 @@ use crate::{ config::{Config, IVerge}, feat::create_local_backup_with_namer, - logging, process::AsyncHandler, - utils::{dirs::local_backup_dir, logging::Type}, + utils::dirs::local_backup_dir, }; use anyhow::Result; use chrono::Local; +use clash_verge_logging::{Type, logging}; use once_cell::sync::OnceCell; use parking_lot::RwLock; use std::{ diff --git a/clash-verge-rev/src-tauri/src/module/lightweight.rs b/clash-verge-rev/src-tauri/src/module/lightweight.rs index 25978b1631..e91837426c 100644 --- a/clash-verge-rev/src-tauri/src/module/lightweight.rs +++ b/clash-verge-rev/src-tauri/src/module/lightweight.rs @@ -1,13 +1,10 @@ use crate::{ config::Config, core::{handle, timer::Timer, tray::Tray}, - log_err, logging, process::AsyncHandler, - utils::logging::Type, }; -#[cfg(target_os = "macos")] -use crate::logging_error; +use clash_verge_logging::{Type, logging, logging_error}; use crate::utils::window_manager::WindowManager; use anyhow::{Context as _, Result}; @@ -184,7 +181,7 @@ fn cancel_window_close_listener() { fn setup_webview_focus_listener() { if let Some(window) = handle::Handle::get_window() { let handler_id = window.listen("tauri://focus", move |_event| { - log_err!(cancel_light_weight_timer()); + logging_error!(Type::Lightweight, cancel_light_weight_timer()); logging!( debug, Type::Lightweight, diff --git a/clash-verge-rev/src-tauri/src/module/mod.rs b/clash-verge-rev/src-tauri/src/module/mod.rs index 87055c6caa..97494e8f3d 100644 --- a/clash-verge-rev/src-tauri/src/module/mod.rs +++ b/clash-verge-rev/src-tauri/src/module/mod.rs @@ -1,4 +1,2 @@ pub mod auto_backup; pub mod lightweight; -pub mod signal; -pub mod sysinfo; diff --git a/clash-verge-rev/src-tauri/src/module/signal/mod.rs b/clash-verge-rev/src-tauri/src/module/signal/mod.rs deleted file mode 100644 index 10f3d69cf5..0000000000 --- a/clash-verge-rev/src-tauri/src/module/signal/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -#[cfg(unix)] -mod unix; -#[cfg(windows)] -mod windows; - -pub fn register() { - #[cfg(windows)] - windows::register(); - - #[cfg(unix)] - unix::register(); -} diff --git a/clash-verge-rev/src-tauri/src/module/signal/unix.rs b/clash-verge-rev/src-tauri/src/module/signal/unix.rs deleted file mode 100644 index 866dce4e24..0000000000 --- a/clash-verge-rev/src-tauri/src/module/signal/unix.rs +++ /dev/null @@ -1,36 +0,0 @@ -use signal_hook::{ - consts::{SIGHUP, SIGINT, SIGTERM}, - iterator::Signals, - low_level, -}; - -use crate::{feat, logging, logging_error, utils::logging::Type}; - -pub fn register() { - tauri::async_runtime::spawn(async { - let signals = [SIGTERM, SIGINT, SIGHUP]; - match Signals::new(signals) { - Ok(mut sigs) => { - for signal in &mut sigs { - let signal_to_str = |signal: i32| match signal { - SIGTERM => "SIGTERM", - SIGINT => "SIGINT", - SIGHUP => "SIGHUP", - _ => "UNKNOWN", - }; - logging!(info, Type::System, "捕获到信号 {}", signal_to_str(signal)); - feat::clean_async().await; - // After printing it, do whatever the signal was supposed to do in the first place - logging_error!( - Type::System, - "信号 {:?} 默认处理失败", - low_level::emulate_default_handler(signal) - ); - } - } - Err(e) => { - logging!(error, Type::System, "注册信号处理器失败: {}", e); - } - } - }); -} diff --git a/clash-verge-rev/src-tauri/src/module/sysinfo.rs b/clash-verge-rev/src-tauri/src/module/sysinfo.rs deleted file mode 100644 index 74ebe1f3ea..0000000000 --- a/clash-verge-rev/src-tauri/src/module/sysinfo.rs +++ /dev/null @@ -1,69 +0,0 @@ -use crate::{ - cmd::system, - core::{CoreManager, handle}, -}; -use std::fmt::{self, Debug, Formatter}; -use sysinfo::System; - -pub struct PlatformSpecification { - system_name: String, - system_version: String, - system_kernel_version: String, - system_arch: String, - verge_version: String, - running_mode: String, - is_admin: bool, -} - -impl Debug for PlatformSpecification { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!( - f, - "System Name: {}\nSystem Version: {}\nSystem kernel Version: {}\nSystem Arch: {}\nVerge Version: {}\nRunning Mode: {}\nIs Admin: {}", - self.system_name, - self.system_version, - self.system_kernel_version, - self.system_arch, - self.verge_version, - self.running_mode, - self.is_admin - ) - } -} - -impl PlatformSpecification { - pub fn new() -> Self { - let system_name = System::name().unwrap_or_else(|| "Null".into()); - let system_version = System::long_os_version().unwrap_or_else(|| "Null".into()); - let system_kernel_version = System::kernel_version().unwrap_or_else(|| "Null".into()); - let system_arch = System::cpu_arch(); - - let handler = handle::Handle::app_handle(); - let verge_version = handler.package_info().version.to_string(); - - // 使用默认值避免在同步上下文中执行异步操作 - let running_mode = "NotRunning".to_string(); - - let is_admin = system::is_admin().unwrap_or_default(); - - Self { - system_name, - system_version, - system_kernel_version, - system_arch, - verge_version, - running_mode, - is_admin, - } - } - - // 异步方法来获取完整的系统信息 - pub fn new_sync() -> Self { - let mut info = Self::new(); - - let running_mode = CoreManager::global().get_running_mode(); - info.running_mode = running_mode.to_string(); - - info - } -} diff --git a/clash-verge-rev/src-tauri/src/process/guard.rs b/clash-verge-rev/src-tauri/src/process/guard.rs deleted file mode 100644 index d1a41ab505..0000000000 --- a/clash-verge-rev/src-tauri/src/process/guard.rs +++ /dev/null @@ -1,41 +0,0 @@ -use anyhow::Result; -use tauri_plugin_shell::process::CommandChild; - -use crate::{logging, utils::logging::Type}; - -#[derive(Debug)] -pub struct CommandChildGuard(Option); - -impl Drop for CommandChildGuard { - #[inline] - fn drop(&mut self) { - if let Err(err) = self.kill() { - logging!( - error, - Type::Service, - "Failed to kill child process: {}", - err - ); - } - } -} - -impl CommandChildGuard { - #[inline] - pub const fn new(child: CommandChild) -> Self { - Self(Some(child)) - } - - #[inline] - pub fn kill(&mut self) -> Result<()> { - if let Some(child) = self.0.take() { - let _ = child.kill(); - } - Ok(()) - } - - #[inline] - pub fn pid(&self) -> Option { - self.0.as_ref().map(|c| c.pid()) - } -} diff --git a/clash-verge-rev/src-tauri/src/process/mod.rs b/clash-verge-rev/src-tauri/src/process/mod.rs index 77f99e78ed..ac83ecc0d1 100644 --- a/clash-verge-rev/src-tauri/src/process/mod.rs +++ b/clash-verge-rev/src-tauri/src/process/mod.rs @@ -1,4 +1,2 @@ mod async_handler; pub use async_handler::AsyncHandler; -mod guard; -pub use guard::CommandChildGuard; diff --git a/clash-verge-rev/src-tauri/src/utils/autostart.rs b/clash-verge-rev/src-tauri/src/utils/autostart.rs index d898a6d09d..3de6d988ea 100644 --- a/clash-verge-rev/src-tauri/src/utils/autostart.rs +++ b/clash-verge-rev/src-tauri/src/utils/autostart.rs @@ -1,7 +1,7 @@ #[cfg(target_os = "windows")] -use crate::{logging, utils::logging::Type}; -#[cfg(target_os = "windows")] use anyhow::{Result, anyhow}; +#[cfg(target_os = "windows")] +use clash_verge_logging::{Type, logging}; #[cfg(target_os = "windows")] use std::{os::windows::process::CommandExt as _, path::Path, path::PathBuf}; diff --git a/clash-verge-rev/src-tauri/src/utils/dirs.rs b/clash-verge-rev/src-tauri/src/utils/dirs.rs index f00e33f336..3bda8e60c6 100644 --- a/clash-verge-rev/src-tauri/src/utils/dirs.rs +++ b/clash-verge-rev/src-tauri/src/utils/dirs.rs @@ -1,10 +1,7 @@ -use crate::{ - core::{CoreManager, handle, manager::RunningMode}, - logging, - utils::logging::Type, -}; +use crate::core::{CoreManager, handle, manager::RunningMode}; use anyhow::Result; use async_trait::async_trait; +use clash_verge_logging::{Type, logging}; use once_cell::sync::OnceCell; #[cfg(unix)] use std::iter; diff --git a/clash-verge-rev/src-tauri/src/utils/help.rs b/clash-verge-rev/src-tauri/src/utils/help.rs index a33b365a44..1206ff57a1 100644 --- a/clash-verge-rev/src-tauri/src/utils/help.rs +++ b/clash-verge-rev/src-tauri/src/utils/help.rs @@ -1,5 +1,6 @@ -use crate::{config::with_encryption, enhance::seq::SeqMap, logging, utils::logging::Type}; +use crate::{config::with_encryption, enhance::seq::SeqMap}; use anyhow::{Context as _, Result, anyhow, bail}; +use clash_verge_logging::{Type, logging}; use nanoid::nanoid; use serde::{Serialize, de::DeserializeOwned}; use serde_yaml_ng::Mapping; @@ -75,7 +76,9 @@ pub async fn save_yaml( let path_str = path.as_os_str().to_string_lossy().to_string(); tokio::fs::write(path, yaml_str.as_bytes()) .await - .with_context(|| format!("failed to save file \"{path_str}\"")) + .with_context(|| format!("failed to save file \"{path_str}\""))?; + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + Ok(()) } const ALPHABET: [char; 62] = [ diff --git a/clash-verge-rev/src-tauri/src/utils/i18n.rs b/clash-verge-rev/src-tauri/src/utils/i18n.rs index 0579082837..1bd99319ec 100644 --- a/clash-verge-rev/src-tauri/src/utils/i18n.rs +++ b/clash-verge-rev/src-tauri/src/utils/i18n.rs @@ -82,9 +82,9 @@ pub async fn current_language() -> String { .await .latest_arc() .language - .clone() + .as_ref() .filter(|lang| !lang.is_empty()) - .and_then(|lang| resolve_supported_language(&lang)) + .and_then(|lang| resolve_supported_language(lang)) .unwrap_or_else(system_language) } diff --git a/clash-verge-rev/src-tauri/src/utils/init.rs b/clash-verge-rev/src-tauri/src/utils/init.rs index 66503ff631..eff1a9a58d 100644 --- a/clash-verge-rev/src-tauri/src/utils/init.rs +++ b/clash-verge-rev/src-tauri/src/utils/init.rs @@ -1,6 +1,4 @@ // #[cfg(not(feature = "tracing"))] -#[cfg(not(feature = "tauri-dev"))] -use crate::utils::logging::NoModuleFilter; use crate::{ config::{Config, IClashTemp, IProfiles, IVerge}, constants, @@ -10,16 +8,18 @@ use crate::{ utils::{ dirs::{self, PathBufExec as _, service_log_dir, sidecar_log_dir}, help, - logging::Type, }, }; use anyhow::Result; use chrono::{Local, TimeZone as _}; +#[cfg(not(feature = "tauri-dev"))] +use clash_verge_logging::NoModuleFilter; +use clash_verge_logging::Type; use clash_verge_service_ipc::WriterConfig; use flexi_logger::writers::FileLogWriter; use flexi_logger::{Cleanup, Criterion, FileSpec}; #[cfg(not(feature = "tauri-dev"))] -use flexi_logger::{Duplicate, LogSpecBuilder, Logger}; +use flexi_logger::{Duplicate, LogSpecBuilder, Logger, LoggerHandle}; use std::{path::PathBuf, str::FromStr as _}; use tauri_plugin_shell::ShellExt as _; use tokio::fs; @@ -27,7 +27,7 @@ use tokio::fs::DirEntry; /// initialize this instance's log file #[cfg(not(feature = "tauri-dev"))] -pub async fn init_logger() -> Result<()> { +pub async fn init_logger() -> Result { // TODO 提供 runtime 级别实时修改 let (log_level, log_max_size, log_max_count) = { let verge_guard = Config::verge().await; @@ -47,11 +47,9 @@ pub async fn init_logger() -> Result<()> { .unwrap_or(log_level); spec.default(level); #[cfg(feature = "tracing")] - spec.module("tauri", log::LevelFilter::Debug); - #[cfg(feature = "tracing")] - spec.module("wry", log::LevelFilter::Off); - #[cfg(feature = "tracing")] - spec.module("tauri_plugin_mihomo", log::LevelFilter::Off); + spec.module("tauri", log::LevelFilter::Debug) + .module("wry", log::LevelFilter::Off) + .module("tauri_plugin_mihomo", log::LevelFilter::Off); let spec = spec.build(); let logger = Logger::with(spec) @@ -68,22 +66,29 @@ pub async fn init_logger() -> Result<()> { Cleanup::KeepLogFiles(log_max_count), ); #[cfg(not(feature = "tracing"))] - let logger = logger.filter(Box::new(NoModuleFilter(&["wry", "tauri"]))); + let logger = logger.filter(Box::new(NoModuleFilter(&[ + "wry", + "tauri", + "tokio_tungstenite", + "tungstenite", + ]))); #[cfg(feature = "tracing")] let logger = logger.filter(Box::new(NoModuleFilter(&[ "wry", "tauri_plugin_mihomo", + "tokio_tungstenite", + "tungstenite", "kode_bridge", ]))); - let _handle = logger.start()?; + let handle = logger.start()?; // TODO 全局 logger handle 控制 // GlobalLoggerProxy::global().set_inner(handle); // TODO 提供前端设置等级,热更新等级 // logger.parse_new_spec(spec) - Ok(()) + Ok(handle) } pub async fn sidecar_writer() -> Result { diff --git a/clash-verge-rev/src-tauri/src/utils/linux.rs b/clash-verge-rev/src-tauri/src/utils/linux.rs index e3ced3871c..de577cabc2 100644 --- a/clash-verge-rev/src-tauri/src/utils/linux.rs +++ b/clash-verge-rev/src-tauri/src/utils/linux.rs @@ -1,6 +1,5 @@ -use crate::logging; -use crate::utils::logging::Type; use anyhow::Result; +use clash_verge_logging::{Type, logging}; use std::collections::{HashMap, HashSet}; use std::env; use std::fs; diff --git a/clash-verge-rev/src-tauri/src/utils/mod.rs b/clash-verge-rev/src-tauri/src/utils/mod.rs index 7c3e1abac1..3eb1d30ae4 100644 --- a/clash-verge-rev/src-tauri/src/utils/mod.rs +++ b/clash-verge-rev/src-tauri/src/utils/mod.rs @@ -1,13 +1,11 @@ pub mod autostart; pub mod dirs; -pub mod draft; pub mod format; pub mod help; pub mod i18n; pub mod init; #[cfg(target_os = "linux")] pub mod linux; -pub mod logging; pub mod network; pub mod notification; pub mod resolve; @@ -15,5 +13,3 @@ pub mod server; pub mod singleton; pub mod tmpl; pub mod window_manager; - -pub use draft::Draft; diff --git a/clash-verge-rev/src-tauri/src/utils/network.rs b/clash-verge-rev/src-tauri/src/utils/network.rs index d48bb2a6f9..c1dbc85763 100644 --- a/clash-verge-rev/src-tauri/src/utils/network.rs +++ b/clash-verge-rev/src-tauri/src/utils/network.rs @@ -110,6 +110,7 @@ impl NetworkManager { timeout_secs: Option, ) -> Result { let mut builder = Client::builder() + .use_rustls_tls() .redirect(reqwest::redirect::Policy::limited(10)) .tcp_keepalive(Duration::from_secs(60)) .pool_max_idle_per_host(0) diff --git a/clash-verge-rev/src-tauri/src/utils/notification.rs b/clash-verge-rev/src-tauri/src/utils/notification.rs index d7ff371a5d..1308be6b6b 100644 --- a/clash-verge-rev/src-tauri/src/utils/notification.rs +++ b/clash-verge-rev/src-tauri/src/utils/notification.rs @@ -9,6 +9,7 @@ pub enum NotificationEvent<'a> { SystemProxyToggled, TunModeToggled, LightweightModeEntered, + ProfilesReactivated, AppQuit, #[cfg(target_os = "macos")] AppHidden, @@ -54,6 +55,11 @@ pub async fn notify_event<'a>(event: NotificationEvent<'a>) { let body = rust_i18n::t!("notifications.lightweightModeEntered.body").to_string(); notify(&title, &body); } + NotificationEvent::ProfilesReactivated => { + let title = rust_i18n::t!("notifications.profilesReactivated.title").to_string(); + let body = rust_i18n::t!("notifications.profilesReactivated.body").to_string(); + notify(&title, &body); + } NotificationEvent::AppQuit => { let title = rust_i18n::t!("notifications.appQuit.title").to_string(); let body = rust_i18n::t!("notifications.appQuit.body").to_string(); diff --git a/clash-verge-rev/src-tauri/src/utils/resolve/dns.rs b/clash-verge-rev/src-tauri/src/utils/resolve/dns.rs index 0896bccaf3..12dd86bd8c 100644 --- a/clash-verge-rev/src-tauri/src/utils/resolve/dns.rs +++ b/clash-verge-rev/src-tauri/src/utils/resolve/dns.rs @@ -1,8 +1,7 @@ -#[cfg(target_os = "macos")] -use crate::{logging, utils::logging::Type}; +use clash_verge_logging::{Type, logging}; + pub async fn set_public_dns(dns_server: String) { - use crate::utils::logging::Type; - use crate::{core::handle, logging, utils::dirs}; + use crate::{core::handle, utils::dirs}; use tauri_plugin_shell::ShellExt as _; let app_handle = handle::Handle::app_handle(); diff --git a/clash-verge-rev/src-tauri/src/utils/resolve/mod.rs b/clash-verge-rev/src-tauri/src/utils/resolve/mod.rs index b855b35c45..639072e258 100644 --- a/clash-verge-rev/src-tauri/src/utils/resolve/mod.rs +++ b/clash-verge-rev/src-tauri/src/utils/resolve/mod.rs @@ -1,4 +1,7 @@ +use std::sync::atomic::{AtomicBool, Ordering}; + use anyhow::Result; +use flexi_logger::LoggerHandle; use crate::{ config::Config, @@ -9,11 +12,13 @@ use crate::{ sysopt, tray::Tray, }, - logging, logging_error, - module::{auto_backup::AutoBackupManager, lightweight::auto_lightweight_boot, signal}, + feat, + module::{auto_backup::AutoBackupManager, lightweight::auto_lightweight_boot}, process::AsyncHandler, - utils::{init, logging::Type, server, window_manager::WindowManager}, + utils::{init, server, window_manager::WindowManager}, }; +use clash_verge_logging::{Type, logging, logging_error}; +use clash_verge_signal; pub mod dns; pub mod scheme; @@ -21,6 +26,23 @@ pub mod ui; pub mod window; pub mod window_script; +static RESOLVE_DONE: AtomicBool = AtomicBool::new(false); + +pub async fn prioritize_initialization() -> Option { + init_work_config().await; + init_resources().await; + + #[cfg(not(feature = "tauri-dev"))] + { + logging!(info, Type::Setup, "Initializing logger"); + init::init_logger().await.ok() + } + #[cfg(feature = "tauri-dev")] + { + None + } +} + pub fn resolve_setup_handle() { init_handle(); } @@ -29,14 +51,11 @@ pub fn resolve_setup_sync() { AsyncHandler::spawn(|| async { AsyncHandler::spawn_blocking(init_scheme); AsyncHandler::spawn_blocking(init_embed_server); - AsyncHandler::spawn_blocking(init_signal); }); } pub fn resolve_setup_async() { AsyncHandler::spawn(|| async { - #[cfg(not(feature = "tauri-dev"))] - resolve_setup_logger().await; logging!( info, Type::ClashVergeRev, @@ -54,7 +73,7 @@ pub fn resolve_setup_async() { init_service_manager().await; init_core_manager().await; init_system_proxy().await; - AsyncHandler::spawn_blocking(init_system_proxy_guard); + init_system_proxy_guard().await; }); let tray_init = async { @@ -94,11 +113,6 @@ pub(super) fn init_scheme() { logging_error!(Type::Setup, init::init_scheme()); } -#[cfg(not(feature = "tauri-dev"))] -pub(super) async fn resolve_setup_logger() { - logging_error!(Type::Setup, init::init_logger().await); -} - pub async fn resolve_scheme(param: &str) -> Result<()> { logging_error!(Type::Setup, scheme::resolve_scheme(param).await); Ok(()) @@ -132,9 +146,13 @@ pub(super) async fn init_auto_backup() { logging_error!(Type::Setup, AutoBackupManager::global().init().await); } -pub(super) fn init_signal() { +pub fn init_signal() { logging!(info, Type::Setup, "Initializing signal handlers..."); - signal::register(); + clash_verge_signal::register( + #[cfg(windows)] + handle::Handle::app_handle(), + feat::quit, + ); } pub async fn init_work_config() { @@ -153,7 +171,7 @@ pub(super) async fn init_verge_config() { } pub(super) async fn init_service_manager() { - clash_verge_service_ipc::set_config(ServiceManager::config()).await; + clash_verge_service_ipc::set_config(Some(ServiceManager::config())).await; if !is_service_ipc_path_exists() { return; } @@ -173,8 +191,8 @@ pub(super) async fn init_system_proxy() { ); } -pub(super) fn init_system_proxy_guard() { - logging_error!(Type::Setup, sysopt::Sysopt::global().init_guard_sysproxy()); +pub(super) async fn init_system_proxy_guard() { + sysopt::Sysopt::global().refresh_guard().await; } pub(super) async fn refresh_tray_menu() { @@ -194,3 +212,15 @@ pub(super) async fn init_window() { } WindowManager::create_window(!is_silent_start).await; } + +pub fn resolve_done() { + RESOLVE_DONE.store(true, Ordering::Release); +} + +pub fn is_resolve_done() -> bool { + RESOLVE_DONE.load(Ordering::Acquire) +} + +pub fn reset_resolve_done() { + RESOLVE_DONE.store(false, Ordering::Release); +} diff --git a/clash-verge-rev/src-tauri/src/utils/resolve/scheme.rs b/clash-verge-rev/src-tauri/src/utils/resolve/scheme.rs index e36125c225..6fa02230e4 100644 --- a/clash-verge-rev/src-tauri/src/utils/resolve/scheme.rs +++ b/clash-verge-rev/src-tauri/src/utils/resolve/scheme.rs @@ -8,9 +8,8 @@ use tauri::Url; use crate::{ config::{Config, PrfItem, profiles}, core::handle, - logging, - utils::logging::Type, }; +use clash_verge_logging::{Type, logging}; pub(super) async fn resolve_scheme(param: &str) -> Result<()> { logging!(info, Type::Config, "received deep link: {param}"); diff --git a/clash-verge-rev/src-tauri/src/utils/resolve/ui.rs b/clash-verge-rev/src-tauri/src/utils/resolve/ui.rs index d93fe9e6f9..6c174f332e 100644 --- a/clash-verge-rev/src-tauri/src/utils/resolve/ui.rs +++ b/clash-verge-rev/src-tauri/src/utils/resolve/ui.rs @@ -6,7 +6,7 @@ use std::sync::{ }; use tokio::sync::Notify; -use crate::{logging, utils::logging::Type}; +use clash_verge_logging::{Type, logging}; // 获取 UI 是否准备就绪的全局状态 static UI_READY: AtomicBool = AtomicBool::new(false); diff --git a/clash-verge-rev/src-tauri/src/utils/resolve/window.rs b/clash-verge-rev/src-tauri/src/utils/resolve/window.rs index cf8cecd53d..df0df0412c 100644 --- a/clash-verge-rev/src-tauri/src/utils/resolve/window.rs +++ b/clash-verge-rev/src-tauri/src/utils/resolve/window.rs @@ -1,14 +1,18 @@ -use tauri::WebviewWindow; +use dark_light::{Mode as SystemTheme, detect as detect_system_theme}; +use tauri::utils::config::Color; +use tauri::{Theme, WebviewWindow}; use crate::{ config::Config, core::handle, - logging_error, - utils::{ - logging::Type, - resolve::window_script::{INITIAL_LOADING_OVERLAY, WINDOW_INITIAL_SCRIPT}, - }, + utils::resolve::window_script::{INITIAL_LOADING_OVERLAY, build_window_initial_script}, }; +use clash_verge_logging::{Type, logging_error}; + +const DARK_BACKGROUND_COLOR: Color = Color(46, 48, 61, 255); // #2E303D +const LIGHT_BACKGROUND_COLOR: Color = Color(245, 245, 245, 255); // #F5F5F5 +const DARK_BACKGROUND_HEX: &str = "#2E303D"; +const LIGHT_BACKGROUND_HEX: &str = "#F5F5F5"; // 定义默认窗口尺寸常量 const DEFAULT_WIDTH: f64 = 940.0; @@ -24,8 +28,37 @@ pub async fn build_new_window() -> Result { let config = Config::verge().await; let latest = config.latest_arc(); let start_page = latest.start_page.as_deref().unwrap_or("/"); + let initial_theme_mode = match latest.theme_mode.as_deref() { + Some("dark") => "dark", + Some("light") => "light", + _ => "system", + }; - match tauri::WebviewWindowBuilder::new( + let resolved_theme = match initial_theme_mode { + "dark" => Some(Theme::Dark), + "light" => Some(Theme::Light), + _ => None, + }; + + let prefers_dark_background = match resolved_theme { + Some(Theme::Dark) => true, + Some(Theme::Light) => false, + _ => !matches!(detect_system_theme().ok(), Some(SystemTheme::Light)), + }; + + let background_color = if prefers_dark_background { + DARK_BACKGROUND_COLOR + } else { + LIGHT_BACKGROUND_COLOR + }; + + let initial_script = build_window_initial_script( + initial_theme_mode, + DARK_BACKGROUND_HEX, + LIGHT_BACKGROUND_HEX, + ); + + let mut builder = tauri::WebviewWindowBuilder::new( app_handle, "main", /* the unique window label */ tauri::WebviewUrl::App(start_page.into()), @@ -37,11 +70,21 @@ pub async fn build_new_window() -> Result { .fullscreen(false) .inner_size(DEFAULT_WIDTH, DEFAULT_HEIGHT) .min_inner_size(MINIMAL_WIDTH, MINIMAL_HEIGHT) - .visible(true) // 立即显示窗口,避免用户等待 - .initialization_script(WINDOW_INITIAL_SCRIPT) - .build() - { + .visible(false) // 等待主题色准备好后再展示,避免启动色差 + .initialization_script(&initial_script); + + if let Some(theme) = resolved_theme { + builder = builder.theme(Some(theme)); + } + + builder = builder.background_color(background_color); + + match builder.build() { Ok(window) => { + logging_error!( + Type::Window, + window.set_background_color(Some(background_color)) + ); logging_error!(Type::Window, window.eval(INITIAL_LOADING_OVERLAY)); Ok(window) } diff --git a/clash-verge-rev/src-tauri/src/utils/resolve/window_script.rs b/clash-verge-rev/src-tauri/src/utils/resolve/window_script.rs index 632b0bf864..ff7062cd16 100644 --- a/clash-verge-rev/src-tauri/src/utils/resolve/window_script.rs +++ b/clash-verge-rev/src-tauri/src/utils/resolve/window_script.rs @@ -1,66 +1,100 @@ -pub const WINDOW_INITIAL_SCRIPT: &str = r#" +pub fn build_window_initial_script( + initial_theme_mode: &str, + dark_background: &str, + light_background: &str, +) -> String { + let theme_mode = match initial_theme_mode { + "dark" => "dark", + "light" => "light", + _ => "system", + }; + format!( + r#" + window.__VERGE_INITIAL_THEME_MODE = "{theme_mode}"; + window.__VERGE_INITIAL_THEME_COLORS = {{ + darkBg: "{dark_background}", + lightBg: "{light_background}", + }}; +{script} +"#, + theme_mode = theme_mode, + dark_background = dark_background, + light_background = light_background, + script = WINDOW_INITIAL_SCRIPT, + ) +} + +pub const WINDOW_INITIAL_SCRIPT: &str = r##" console.log('[Tauri] 窗口初始化脚本开始执行'); - function createLoadingOverlay() { - - if (document.getElementById('initial-loading-overlay')) { - console.log('[Tauri] 加载指示器已存在'); - return; + const initialColors = (() => { + try { + const colors = window.__VERGE_INITIAL_THEME_COLORS; + if (colors && typeof colors === "object") { + const { darkBg, lightBg } = colors; + if (typeof darkBg === "string" && typeof lightBg === "string") { + return { darkBg, lightBg }; + } + } + } catch (error) { + console.warn("[Tauri] 读取初始主题颜色失败:", error); } + return { darkBg: "#2E303D", lightBg: "#F5F5F5" }; + })(); - console.log('[Tauri] 创建加载指示器'); - const loadingDiv = document.createElement('div'); - loadingDiv.id = 'initial-loading-overlay'; - loadingDiv.innerHTML = ` -
-
-
-
-
Loading Clash Verge...
-
- - `; + const prefersDark = (() => { + try { + return !!window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)")?.matches; + } catch (error) { + console.warn("[Tauri] 读取系统主题失败:", error); + return false; + } + })(); - if (document.body) { - document.body.appendChild(loadingDiv); + const initialThemeMode = typeof window.__VERGE_INITIAL_THEME_MODE === "string" + ? window.__VERGE_INITIAL_THEME_MODE + : "system"; + + let initialTheme = prefersDark ? "dark" : "light"; + if (initialThemeMode === "dark") { + initialTheme = "dark"; + } else if (initialThemeMode === "light") { + initialTheme = "light"; + } + + const applyInitialTheme = (theme) => { + const isDark = theme === "dark"; + const root = document.documentElement; + const bgColor = isDark ? initialColors.darkBg : initialColors.lightBg; + const textColor = isDark ? "#ffffff" : "#333"; + if (root) { + root.dataset.theme = theme; + root.style.setProperty("--bg-color", bgColor); + root.style.setProperty("--text-color", textColor); + root.style.colorScheme = isDark ? "dark" : "light"; + root.style.color = textColor; + } + const paintBody = () => { + if (!document.body) return; + document.body.style.color = textColor; + }; + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", paintBody, { once: true }); } else { - document.addEventListener('DOMContentLoaded', () => { - if (document.body && !document.getElementById('initial-loading-overlay')) { - document.body.appendChild(loadingDiv); - } - }); + paintBody(); } - } + try { + localStorage.setItem("verge-theme-mode-cache", theme); + } catch (error) { + console.warn("[Tauri] 缓存主题模式失败:", error); + } + return isDark; + }; - createLoadingOverlay(); - - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', createLoadingOverlay); - } else { - createLoadingOverlay(); - } + applyInitialTheme(initialTheme); console.log('[Tauri] 窗口初始化脚本执行完成'); -"#; +"##; pub const INITIAL_LOADING_OVERLAY: &str = r" const overlay = document.getElementById('initial-loading-overlay'); diff --git a/clash-verge-rev/src-tauri/src/utils/server.rs b/clash-verge-rev/src-tauri/src/utils/server.rs index 4defbd7598..a8b96ff1a7 100644 --- a/clash-verge-rev/src-tauri/src/utils/server.rs +++ b/clash-verge-rev/src-tauri/src/utils/server.rs @@ -1,12 +1,12 @@ use super::resolve; use crate::{ config::{Config, DEFAULT_PAC, IVerge}, - logging, logging_error, module::lightweight, process::AsyncHandler, - utils::{logging::Type, window_manager::WindowManager}, + utils::window_manager::WindowManager, }; use anyhow::{Result, bail}; +use clash_verge_logging::{Type, logging, logging_error}; use once_cell::sync::OnceCell; use parking_lot::Mutex; use port_scanner::local_port_available; @@ -73,20 +73,20 @@ pub fn embed_server() { .expect("failed to set shutdown signal for embedded server"); let port = IVerge::get_singleton_port(); - AsyncHandler::spawn(move || async move { - let visible = warp::path!("commands" / "visible").and_then(|| async { - logging!(info, Type::Window, "检测到从单例模式恢复应用窗口"); - if !lightweight::exit_lightweight_mode().await { - WindowManager::show_main_window().await; - } else { - logging!(error, Type::Window, "轻量模式退出失败,无法恢复应用窗口"); - }; - Ok::<_, warp::Rejection>(warp::reply::with_status::( - "ok".to_string(), - warp::http::StatusCode::OK, - )) - }); + let visible = warp::path!("commands" / "visible").and_then(|| async { + logging!(info, Type::Window, "检测到从单例模式恢复应用窗口"); + if !lightweight::exit_lightweight_mode().await { + WindowManager::show_main_window().await; + } else { + logging!(error, Type::Window, "轻量模式退出失败,无法恢复应用窗口"); + }; + Ok::<_, warp::Rejection>(warp::reply::with_status::( + "ok".to_string(), + warp::http::StatusCode::OK, + )) + }); + let pac = warp::path!("commands" / "pac").and_then(|| async move { let verge_config = Config::verge().await; let clash_config = Config::clash().await; @@ -100,29 +100,31 @@ pub fn embed_server() { .data_arc() .verge_mixed_port .unwrap_or_else(|| clash_config.data_arc().get_mixed_port()); - - let pac = warp::path!("commands" / "pac").map(move || { - let processed_content = pac_content.replace("%mixed-port%", &format!("{pac_port}")); + let processed_content = pac_content.replace("%mixed-port%", &format!("{pac_port}")); + Ok::<_, warp::Rejection>( warp::http::Response::builder() .header("Content-Type", "application/x-ns-proxy-autoconfig") .body(processed_content) - .unwrap_or_default() + .unwrap_or_default(), + ) + }); + + // Use map instead of and_then to avoid Send issues + let scheme = warp::path!("commands" / "scheme") + .and(warp::query::()) + .and_then(|query: QueryParam| async move { + AsyncHandler::spawn(|| async move { + logging_error!(Type::Setup, resolve::resolve_scheme(&query.param).await); + }); + Ok::<_, warp::Rejection>(warp::reply::with_status::( + "ok".to_string(), + warp::http::StatusCode::OK, + )) }); - // Use map instead of and_then to avoid Send issues - let scheme = warp::path!("commands" / "scheme") - .and(warp::query::()) - .and_then(|query: QueryParam| async move { - AsyncHandler::spawn(|| async move { - logging_error!(Type::Setup, resolve::resolve_scheme(&query.param).await); - }); - Ok::<_, warp::Rejection>(warp::reply::with_status::( - "ok".to_string(), - warp::http::StatusCode::OK, - )) - }); + let commands = visible.or(scheme).or(pac); - let commands = visible.or(scheme).or(pac); + AsyncHandler::spawn(move || async move { warp::serve(commands) .bind(([127, 0, 0, 1], port)) .await diff --git a/clash-verge-rev/src-tauri/src/utils/singleton.rs b/clash-verge-rev/src-tauri/src/utils/singleton.rs index 1c79a8ba5a..44127e2cc3 100644 --- a/clash-verge-rev/src-tauri/src/utils/singleton.rs +++ b/clash-verge-rev/src-tauri/src/utils/singleton.rs @@ -37,65 +37,6 @@ macro_rules! singleton { }; } -/// Macro for singleton pattern with logging -#[macro_export] -macro_rules! singleton_with_logging { - ($struct_name:ty, $instance_name:ident, $struct_name_str:literal) => { - static $instance_name: std::sync::OnceLock<$struct_name> = std::sync::OnceLock::new(); - - impl $struct_name { - pub fn global() -> &'static $struct_name { - $instance_name.get_or_init(|| { - let instance = Self::new(); - $crate::logging!( - info, - $crate::utils::logging::Type::Setup, - concat!($struct_name_str, " initialized") - ); - instance - }) - } - } - }; -} - -/// Macro for singleton pattern with lazy initialization using a closure -/// This replaces patterns like lazy_static! or complex OnceLock initialization -#[macro_export] -macro_rules! singleton_lazy { - ($struct_name:ty, $instance_name:ident, $init_closure:expr) => { - static $instance_name: std::sync::OnceLock<$struct_name> = std::sync::OnceLock::new(); - - impl $struct_name { - pub fn global() -> &'static $struct_name { - $instance_name.get_or_init($init_closure) - } - } - }; -} - -/// Macro for singleton pattern with lazy initialization and logging -#[macro_export] -macro_rules! singleton_lazy_with_logging { - ($struct_name:ty, $instance_name:ident, $struct_name_str:literal, $init_closure:expr) => { - static $instance_name: std::sync::OnceLock<$struct_name> = std::sync::OnceLock::new(); - - impl $struct_name { - pub fn global() -> &'static $struct_name { - $instance_name.get_or_init(|| { - let instance = $init_closure(); - $crate::logging!( - info, - $crate::utils::logging::Type::Setup, - concat!($struct_name_str, " initialized") - ); - instance - }) - } - } - }; -} - #[cfg(test)] mod tests { struct TestStruct { diff --git a/clash-verge-rev/src-tauri/src/utils/window_manager.rs b/clash-verge-rev/src-tauri/src/utils/window_manager.rs index c6a5356965..57cc11402f 100644 --- a/clash-verge-rev/src-tauri/src/utils/window_manager.rs +++ b/clash-verge-rev/src-tauri/src/utils/window_manager.rs @@ -1,8 +1,5 @@ -use crate::{ - core::handle, - logging, - utils::{logging::Type, resolve::window::build_new_window}, -}; +use crate::{core::handle, utils::resolve::window::build_new_window}; +use clash_verge_logging::{Type, logging}; use std::future::Future; use std::pin::Pin; use tauri::{Manager as _, WebviewWindow, Wry}; @@ -342,17 +339,19 @@ impl WindowManager { return false; } - match build_new_window().await { - Ok(_) => { + let window = match build_new_window().await { + Ok(window) => { logging!(info, Type::Window, "新窗口创建成功"); + window } Err(e) => { logging!(error, Type::Window, "新窗口创建失败: {}", e); return false; } - } + }; - if WindowOperationResult::Failed == Self::show_main_window().await { + // 直接激活刚创建的窗口,避免因防抖导致首次显示被跳过 + if WindowOperationResult::Failed == Self::activate_window(&window) { return false; } diff --git a/clash-verge-rev/src-tauri/tauri.conf.json b/clash-verge-rev/src-tauri/tauri.conf.json index daf6d3801d..4a50c25943 100755 --- a/clash-verge-rev/src-tauri/tauri.conf.json +++ b/clash-verge-rev/src-tauri/tauri.conf.json @@ -23,7 +23,8 @@ "beforeBuildCommand": "pnpm run web:build", "frontendDist": "../dist", "beforeDevCommand": "pnpm run web:dev", - "devUrl": "http://localhost:3000/" + "devUrl": "http://localhost:3000/", + "removeUnusedCommands": true }, "productName": "Clash Verge", "identifier": "io.github.clash-verge-rev.clash-verge-rev", diff --git a/clash-verge-rev/src/components/base/base-loading-overlay.tsx b/clash-verge-rev/src/components/base/base-loading-overlay.tsx index 98727dcb1b..a115da25c0 100644 --- a/clash-verge-rev/src/components/base/base-loading-overlay.tsx +++ b/clash-verge-rev/src/components/base/base-loading-overlay.tsx @@ -21,7 +21,11 @@ export const BaseLoadingOverlay: React.FC = ({ display: "flex", justifyContent: "center", alignItems: "center", - backgroundColor: "rgba(255, 255, 255, 0.7)", + // Respect current theme; avoid bright flash in dark mode + backgroundColor: (theme) => + theme.palette.mode === "dark" + ? "rgba(0, 0, 0, 0.5)" + : "rgba(255, 255, 255, 0.7)", zIndex: 1000, }} > diff --git a/clash-verge-rev/src/components/base/base-search-box.tsx b/clash-verge-rev/src/components/base/base-search-box.tsx index beb961e0c1..47cd570114 100644 --- a/clash-verge-rev/src/components/base/base-search-box.tsx +++ b/clash-verge-rev/src/components/base/base-search-box.tsx @@ -50,6 +50,20 @@ export const BaseSearchBox = ({ onSearch, }: SearchProps) => { const { t } = useTranslation(); + + const escapeRegex = useCallback((value: string) => { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + }, []); + + const buildRegex = useCallback((pattern: string, flags = "") => { + try { + return new RegExp(pattern, flags); + } catch (e) { + console.warn("[BaseSearchBox] buildRegex error:", e); + return null; + } + }, []); + const inputRef = useRef(null); const onSearchRef = useRef(onSearch); const [matchCase, setMatchCase] = useState(defaultMatchCase); @@ -69,47 +83,63 @@ export const BaseSearchBox = ({ }; // Helper that verifies whether a pattern is a valid regular expression - const validateRegex = useCallback((pattern: string) => { - if (!pattern) return true; - try { - new RegExp(pattern); - return true; - } catch (e) { - console.warn("[BaseSearchBox] validateRegex error:", e); - return false; - } - }, []); + const validateRegex = useCallback( + (pattern: string, flags = "") => { + if (!pattern) return true; + return !!buildRegex(pattern, flags); + }, + [buildRegex], + ); const createMatcher = useMemo(() => { return (searchText: string) => { - if (useRegularExpression && searchText) { - const isValid = validateRegex(searchText); - if (!isValid) { - // Invalid regex should result in no match - return () => false; - } + if (!searchText) { + return () => true; + } + + const flags = matchCase ? "" : "i"; + + if (useRegularExpression) { + const regex = buildRegex(searchText, flags); + if (!regex) return () => false; + + return (content: string) => { + try { + return regex.test(content); + } catch (e) { + console.warn("[BaseSearchBox] regex match error:", e); + return false; + } + }; + } + + if (matchWholeWord) { + const regex = buildRegex(`\\b${escapeRegex(searchText)}\\b`, flags); + if (!regex) return () => false; + + return (content: string) => { + try { + return regex.test(content); + } catch (e) { + console.warn("[BaseSearchBox] whole word match error:", e); + return false; + } + }; } return (content: string) => { - if (!searchText) { - return true; - } - - const item = !matchCase ? content.toLowerCase() : content; - const searchItem = !matchCase ? searchText.toLowerCase() : searchText; - - if (useRegularExpression) { - return new RegExp(searchItem).test(item); - } - - if (matchWholeWord) { - return new RegExp(`\\b${searchItem}\\b`).test(item); - } - - return item.includes(searchItem); + const item = matchCase ? content : content.toLowerCase(); + const target = matchCase ? searchText : searchText.toLowerCase(); + return item.includes(target); }; }; - }, [matchCase, matchWholeWord, useRegularExpression, validateRegex]); + }, [ + buildRegex, + escapeRegex, + matchCase, + matchWholeWord, + useRegularExpression, + ]); useEffect(() => { onSearchRef.current = onSearch; @@ -130,10 +160,11 @@ export const BaseSearchBox = ({ const onChange = (e: ChangeEvent) => { const value = e.target?.value ?? ""; setErrorMessage(""); + const flags = matchCase ? "" : "i"; // Validate regex input eagerly if (useRegularExpression && value) { - const isValid = validateRegex(value); + const isValid = validateRegex(value, flags); if (!isValid) { setErrorMessage(t("shared.validation.invalidRegex")); } @@ -155,7 +186,8 @@ export const BaseSearchBox = ({ setErrorMessage(""); } else { const value = inputRef.current?.value ?? ""; - if (value && !validateRegex(value)) { + const flags = matchCase ? "" : "i"; + if (value && !validateRegex(value, flags)) { setErrorMessage(t("shared.validation.invalidRegex")); } } diff --git a/clash-verge-rev/src/components/common/traffic-error-boundary.tsx b/clash-verge-rev/src/components/common/traffic-error-boundary.tsx index 763dcbbea0..c43d3d5798 100644 --- a/clash-verge-rev/src/components/common/traffic-error-boundary.tsx +++ b/clash-verge-rev/src/components/common/traffic-error-boundary.tsx @@ -71,8 +71,7 @@ export class TrafficErrorBoundary extends Component { url: window.location.href, }; - console.log("[TrafficErrorBoundary] 错误报告:", errorReport); - + console.error("[TrafficErrorBoundary] 错误报告:", errorReport); // TODO: 发送到错误监控服务 // sendErrorReport(errorReport); }; diff --git a/clash-verge-rev/src/components/connection/connection-table.tsx b/clash-verge-rev/src/components/connection/connection-table.tsx index c43e6e0e53..fd8eb46528 100644 --- a/clash-verge-rev/src/components/connection/connection-table.tsx +++ b/clash-verge-rev/src/components/connection/connection-table.tsx @@ -1,26 +1,27 @@ -import { Box } from "@mui/material"; +import { ViewColumnRounded } from "@mui/icons-material"; +import { Box, IconButton, Tooltip } from "@mui/material"; import { - DataGrid, - GridColDef, - GridColumnOrderChangeParams, - GridColumnResizeParams, - GridColumnVisibilityModel, - useGridApiRef, - GridColumnMenuItemProps, - GridColumnMenuHideItem, - useGridRootProps, -} from "@mui/x-data-grid"; + ColumnDef, + ColumnSizingState, + SortingState, + VisibilityState, + flexRender, + getCoreRowModel, + getSortedRowModel, + Updater, + useReactTable, +} from "@tanstack/react-table"; +import { useVirtualizer } from "@tanstack/react-virtual"; import dayjs from "dayjs"; import { useLocalStorage } from "foxact/use-local-storage"; import { useCallback, useEffect, - useLayoutEffect, useMemo, - createContext, - use, + useRef, + useState, + type ReactNode, } from "react"; -import type { MouseEvent } from "react"; import { useTranslation } from "react-i18next"; import parseTraffic from "@/utils/parse-traffic"; @@ -28,7 +29,7 @@ import { truncateStr } from "@/utils/truncate-str"; import { ConnectionColumnManager } from "./connection-column-manager"; -const ColumnManagerContext = createContext<() => void>(() => {}); +const ROW_HEIGHT = 40; /** * Reconcile stored column order with base columns to handle added/removed fields @@ -59,131 +60,6 @@ export const ConnectionTable = (props: Props) => { onCloseColumnManager, } = props; const { t } = useTranslation(); - const apiRef = useGridApiRef(); - useLayoutEffect(() => { - const PATCH_FLAG_KEY = "__clashPatchedPublishEvent" as const; - const ORIGINAL_KEY = "__clashOriginalPublishEvent" as const; - let isUnmounted = false; - let retryHandle: ReturnType | null = null; - let cleanupOriginal: (() => void) | null = null; - - const scheduleRetry = () => { - if (isUnmounted || retryHandle !== null) return; - retryHandle = setTimeout(() => { - retryHandle = null; - ensurePatched(); - }, 16); - }; - - // Safari occasionally emits grid events without an event object, - // and MUI expects `defaultMuiPrevented` to exist. Normalize here to avoid crashes. - const createFallbackEvent = () => { - const fallback = { - defaultMuiPrevented: false, - preventDefault() { - fallback.defaultMuiPrevented = true; - }, - }; - return fallback; - }; - - const ensureMuiEvent = ( - value: unknown, - ): { - defaultMuiPrevented: boolean; - preventDefault: () => void; - [key: string]: unknown; - } => { - if (!value || typeof value !== "object" || Array.isArray(value)) { - return createFallbackEvent(); - } - - const eventObject = value as { - defaultMuiPrevented?: unknown; - preventDefault?: () => void; - [key: string]: unknown; - }; - - if (typeof eventObject.defaultMuiPrevented !== "boolean") { - eventObject.defaultMuiPrevented = false; - } - - if (typeof eventObject.preventDefault !== "function") { - eventObject.preventDefault = () => { - eventObject.defaultMuiPrevented = true; - }; - } - - return eventObject as { - defaultMuiPrevented: boolean; - preventDefault: () => void; - [key: string]: unknown; - }; - }; - - const ensurePatched = () => { - if (isUnmounted) return; - const api = apiRef.current; - - if (!api?.publishEvent) { - scheduleRetry(); - return; - } - - const metadataApi = api as unknown as typeof api & - Record; - if (metadataApi[PATCH_FLAG_KEY] === true) return; - - const originalPublishEvent = api.publishEvent; - - // Use Proxy to create a more resilient wrapper that always normalizes events - const patchedPublishEvent = new Proxy(originalPublishEvent, { - apply(target, thisArg, rawArgs: unknown[]) { - rawArgs[2] = ensureMuiEvent(rawArgs[2]); - - return Reflect.apply( - target as (...args: unknown[]) => unknown, - thisArg, - rawArgs, - ); - }, - }) as typeof originalPublishEvent; - - api.publishEvent = patchedPublishEvent; - metadataApi[PATCH_FLAG_KEY] = true; - metadataApi[ORIGINAL_KEY] = originalPublishEvent; - - cleanupOriginal = () => { - const storedOriginal = metadataApi[ORIGINAL_KEY] as - | typeof originalPublishEvent - | undefined; - - api.publishEvent = ( - typeof storedOriginal === "function" - ? storedOriginal - : originalPublishEvent - ) as typeof originalPublishEvent; - - delete metadataApi[PATCH_FLAG_KEY]; - delete metadataApi[ORIGINAL_KEY]; - }; - }; - - ensurePatched(); - - return () => { - isUnmounted = true; - if (retryHandle !== null) { - clearTimeout(retryHandle); - retryHandle = null; - } - if (cleanupOriginal) { - cleanupOriginal(); - cleanupOriginal = null; - } - }; - }, [apiRef]); - const [columnWidths, setColumnWidths] = useLocalStorage< Record >( @@ -229,95 +105,132 @@ export const ConnectionTable = (props: Props) => { }, ); - const baseColumns = useMemo(() => { + const createConnectionRow = (each: IConnectionsItem) => { + const { metadata, rulePayload } = each; + const chains = [...each.chains].reverse().join(" / "); + const rule = rulePayload ? `${each.rule}(${rulePayload})` : each.rule; + const Destination = metadata.destinationIP + ? `${metadata.destinationIP}:${metadata.destinationPort}` + : `${metadata.remoteDestination}:${metadata.destinationPort}`; + return { + id: each.id, + host: metadata.host + ? `${metadata.host}:${metadata.destinationPort}` + : `${metadata.remoteDestination}:${metadata.destinationPort}`, + download: each.download, + upload: each.upload, + dlSpeed: each.curDownload, + ulSpeed: each.curUpload, + chains, + rule, + process: truncateStr(metadata.process || metadata.processPath), + time: each.start, + source: `${metadata.sourceIP}:${metadata.sourcePort}`, + remoteDestination: Destination, + type: `${metadata.type}(${metadata.network})`, + connectionData: each, + }; + }; + + type ConnectionRow = ReturnType; + + type ColumnField = Exclude; + + interface BaseColumn { + field: ColumnField; + headerName: string; + width?: number; + minWidth?: number; + align?: "left" | "right"; + cell?: (row: ConnectionRow) => ReactNode; + } + + const baseColumns = useMemo(() => { return [ { field: "host", headerName: t("connections.components.fields.host"), - width: columnWidths["host"] || 220, - minWidth: 180, + width: 180, + minWidth: 140, }, { field: "download", headerName: t("shared.labels.downloaded"), - width: columnWidths["download"] || 88, + width: 76, + minWidth: 60, align: "right", - headerAlign: "right", - valueFormatter: (value: number) => parseTraffic(value).join(" "), + cell: (row) => parseTraffic(row.download).join(" "), }, { field: "upload", headerName: t("shared.labels.uploaded"), - width: columnWidths["upload"] || 88, + width: 76, + minWidth: 60, align: "right", - headerAlign: "right", - valueFormatter: (value: number) => parseTraffic(value).join(" "), + cell: (row) => parseTraffic(row.upload).join(" "), }, { field: "dlSpeed", headerName: t("connections.components.fields.dlSpeed"), - width: columnWidths["dlSpeed"] || 88, + width: 76, + minWidth: 60, align: "right", - headerAlign: "right", - valueFormatter: (value: number) => parseTraffic(value).join(" ") + "/s", + cell: (row) => `${parseTraffic(row.dlSpeed).join(" ")}/s`, }, { field: "ulSpeed", headerName: t("connections.components.fields.ulSpeed"), - width: columnWidths["ulSpeed"] || 88, + width: 76, + minWidth: 60, align: "right", - headerAlign: "right", - valueFormatter: (value: number) => parseTraffic(value).join(" ") + "/s", + cell: (row) => `${parseTraffic(row.ulSpeed).join(" ")}/s`, }, { field: "chains", headerName: t("connections.components.fields.chains"), - width: columnWidths["chains"] || 340, - minWidth: 180, + width: 280, + minWidth: 160, }, { field: "rule", headerName: t("connections.components.fields.rule"), - width: columnWidths["rule"] || 280, - minWidth: 180, + width: 220, + minWidth: 160, }, { field: "process", headerName: t("connections.components.fields.process"), - width: columnWidths["process"] || 220, - minWidth: 180, + width: 180, + minWidth: 140, }, { field: "time", headerName: t("connections.components.fields.time"), - width: columnWidths["time"] || 120, - minWidth: 100, + width: 100, + minWidth: 80, align: "right", - headerAlign: "right", - sortComparator: (v1: string, v2: string) => - new Date(v2).getTime() - new Date(v1).getTime(), - valueFormatter: (value: number) => dayjs(value).fromNow(), + cell: (row) => dayjs(row.time).fromNow(), }, { field: "source", headerName: t("connections.components.fields.source"), - width: columnWidths["source"] || 200, - minWidth: 130, + width: 160, + minWidth: 120, }, { field: "remoteDestination", headerName: t("connections.components.fields.destination"), - width: columnWidths["remoteDestination"] || 200, - minWidth: 130, + width: 160, + minWidth: 120, }, { field: "type", headerName: t("connections.components.fields.type"), - width: columnWidths["type"] || 160, - minWidth: 100, + width: 120, + minWidth: 80, }, ]; - }, [columnWidths, t]); + }, [t]); useEffect(() => { setColumnOrder((prevValue) => { @@ -334,7 +247,7 @@ export const ConnectionTable = (props: Props) => { }); }, [baseColumns, setColumnOrder]); - const columns = useMemo(() => { + const columns = useMemo(() => { const order = Array.isArray(columnOrder) ? columnOrder : []; const orderMap = new Map(order.map((field, index) => [field, index])); @@ -362,42 +275,6 @@ export const ConnectionTable = (props: Props) => { }, 0); }, [columns, columnVisibilityModel]); - const handleColumnResize = (params: GridColumnResizeParams) => { - const { colDef, width } = params; - setColumnWidths((prev) => ({ - ...prev, - [colDef.field]: width, - })); - }; - - const handleColumnVisibilityChange = useCallback( - (model: GridColumnVisibilityModel) => { - const hiddenFields = new Set(); - Object.entries(model).forEach(([field, value]) => { - if (value === false) { - hiddenFields.add(field); - } - }); - - const nextVisibleCount = columns.reduce((count, column) => { - return hiddenFields.has(column.field) ? count : count + 1; - }, 0); - - if (nextVisibleCount === 0) { - return; - } - - setColumnVisibilityModel(() => { - const sanitized: Partial> = {}; - hiddenFields.forEach((field) => { - sanitized[field] = false; - }); - return sanitized; - }); - }, - [columns, setColumnVisibilityModel], - ); - const handleToggleColumn = useCallback( (field: string, visible: boolean) => { if (!visible && visibleColumnsCount <= 1) { @@ -417,30 +294,6 @@ export const ConnectionTable = (props: Props) => { [setColumnVisibilityModel, visibleColumnsCount], ); - const handleColumnOrderChange = useCallback( - (params: GridColumnOrderChangeParams) => { - setColumnOrder((prevValue) => { - const baseFields = baseColumns.map((col) => col.field); - const currentOrder = Array.isArray(prevValue) - ? [...prevValue] - : [...baseFields]; - const field = params.column.field; - const currentIndex = currentOrder.indexOf(field); - if (currentIndex === -1) return currentOrder; - - currentOrder.splice(currentIndex, 1); - const targetIndex = Math.min( - Math.max(params.targetIndex, 0), - currentOrder.length, - ); - currentOrder.splice(targetIndex, 0, field); - - return currentOrder; - }); - }, - [baseColumns, setColumnOrder], - ); - const handleManagerOrderChange = useCallback( (order: string[]) => { setColumnOrder(() => { @@ -456,16 +309,54 @@ export const ConnectionTable = (props: Props) => { setColumnOrder(baseColumns.map((col) => col.field)); }, [baseColumns, setColumnOrder, setColumnVisibilityModel]); - const gridVisibilityModel = useMemo(() => { - const result: GridColumnVisibilityModel = {}; - if (!columnVisibilityModel) return result; - Object.entries(columnVisibilityModel).forEach(([field, value]) => { - if (typeof value === "boolean") { - result[field] = value; - } + const handleColumnVisibilityChange = useCallback( + (update: Updater) => { + setColumnVisibilityModel((prev) => { + const current = prev ?? {}; + const baseState: VisibilityState = {}; + columns.forEach((column) => { + baseState[column.field] = (current[column.field] ?? true) !== false; + }); + + const mergedState = + typeof update === "function" + ? update(baseState) + : { ...baseState, ...update }; + + const hiddenFields = columns + .filter((column) => mergedState[column.field] === false) + .map((column) => column.field); + + if (columns.length - hiddenFields.length === 0) { + return current; + } + + const sanitized: Partial> = {}; + hiddenFields.forEach((field) => { + sanitized[field] = false; + }); + return sanitized; + }); + }, + [columns, setColumnVisibilityModel], + ); + + const columnVisibilityState = useMemo(() => { + const result: VisibilityState = {}; + if (!columnVisibilityModel) { + columns.forEach((column) => { + result[column.field] = true; + }); + return result; + } + + columns.forEach((column) => { + result[column.field] = + (columnVisibilityModel?.[column.field] ?? true) !== false; }); + return result; - }, [columnVisibilityModel]); + }, [columnVisibilityModel, columns]); const columnOptions = useMemo(() => { return columns.map((column) => ({ @@ -475,72 +366,295 @@ export const ConnectionTable = (props: Props) => { })); }, [columns, columnVisibilityModel]); - const connRows = useMemo(() => { - return connections.map((each) => { - const { metadata, rulePayload } = each; - const chains = [...each.chains].reverse().join(" / "); - const rule = rulePayload ? `${each.rule}(${rulePayload})` : each.rule; - const Destination = metadata.destinationIP - ? `${metadata.destinationIP}:${metadata.destinationPort}` - : `${metadata.remoteDestination}:${metadata.destinationPort}`; - return { - id: each.id, - host: metadata.host - ? `${metadata.host}:${metadata.destinationPort}` - : `${metadata.remoteDestination}:${metadata.destinationPort}`, - download: each.download, - upload: each.upload, - dlSpeed: each.curDownload, - ulSpeed: each.curUpload, - chains, - rule, - process: truncateStr(metadata.process || metadata.processPath), - time: each.start, - source: `${metadata.sourceIP}:${metadata.sourcePort}`, - remoteDestination: Destination, - type: `${metadata.type}(${metadata.network})`, - connectionData: each, - }; - }); - }, [connections]); + const connRows = useMemo( + () => connections.map((each) => createConnectionRow(each)), + [connections], + ); + + const columnDefs = useMemo[]>(() => { + return columns.map((column) => ({ + id: column.field, + accessorKey: column.field, + header: column.headerName, + size: column.width, + minSize: column.minWidth ?? 80, + enableResizing: true, + meta: { + align: column.align ?? "left", + field: column.field, + }, + cell: column.cell + ? ({ row }) => column.cell?.(row.original) + : (info) => info.getValue(), + })); + }, [columns]); + + const [sorting, setSorting] = useState([]); + + const handleColumnSizingChange = useCallback( + (updater: Updater) => { + setColumnWidths((prev) => { + const prevState = prev ?? {}; + const nextState = + typeof updater === "function" ? updater(prevState) : updater; + const sanitized: Record = {}; + Object.entries(nextState).forEach(([key, size]) => { + if (typeof size === "number" && Number.isFinite(size)) { + sanitized[key] = size; + } + }); + return sanitized; + }); + }, + [setColumnWidths], + ); + + const table = useReactTable({ + data: connRows, + columns: columnDefs, + state: { + columnVisibility: columnVisibilityState, + columnSizing: columnWidths, + sorting, + }, + columnResizeMode: "onChange", + enableSortingRemoval: true, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + onSortingChange: setSorting, + onColumnSizingChange: handleColumnSizingChange, + onColumnVisibilityChange: handleColumnVisibilityChange, + }); + + const rows = table.getRowModel().rows; + const tableContainerRef = useRef(null); + const rowVirtualizer = useVirtualizer({ + count: rows.length, + getScrollElement: () => tableContainerRef.current, + estimateSize: () => ROW_HEIGHT, + overscan: 8, + }); + + const virtualRows = rowVirtualizer.getVirtualItems(); + const totalSize = rowVirtualizer.getTotalSize(); + const tableWidth = table.getTotalSize(); return ( - + <> theme.typography.fontFamily, }} > - onShowDetail(e.row.connectionData)} - density="compact" + + + theme.palette.mode === "dark" + ? theme.palette.background.default + : theme.palette.background.paper, + "&:hover": { + backgroundColor: (theme) => theme.palette.action.hover, + }, + }} + > + + + + + > + + + {table.getHeaderGroups().map((headerGroup) => ( + + `1px solid ${theme.palette.divider}`, + backgroundColor: (theme) => theme.palette.background.paper, + }} + > + {headerGroup.headers.map((header) => { + if (header.isPlaceholder) { + return null; + } + const meta = header.column.columnDef.meta as { + align?: "left" | "right"; + field: string; + }; + return ( + + theme.palette.action.hover, + }, + }} + > + + {flexRender( + header.column.columnDef.header, + header.getContext(), + )} + {{ + asc: "▲", + desc: "▼", + }[header.column.getIsSorted() as string] ?? null} + + {header.column.getCanResize() && ( + + theme.palette.action.active, + }, + }} + /> + )} + + ); + })} + + ))} + + + {virtualRows.map((virtualRow) => { + const row = rows[virtualRow.index]; + if (!row) return null; + + return ( + onShowDetail(row.original.connectionData)} + sx={{ + display: "flex", + position: "absolute", + left: 0, + right: 0, + height: virtualRow.size, + transform: `translateY(${virtualRow.start}px)`, + borderBottom: (theme) => + `1px solid ${theme.palette.divider}`, + cursor: "pointer", + "&:hover": { + backgroundColor: (theme) => theme.palette.action.hover, + }, + }} + > + {row.getVisibleCells().map((cell) => { + const meta = cell.column.columnDef.meta as { + align?: "left" | "right"; + }; + return ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ); + })} + + ); + })} + + + { onOrderChange={handleManagerOrderChange} onReset={handleResetColumns} /> - - ); -}; - -type ConnectionColumnMenuManageItemProps = GridColumnMenuItemProps & { - onOpenColumnManager: () => void; -}; - -const ConnectionColumnMenuManageItem = ( - props: ConnectionColumnMenuManageItemProps, -) => { - const { onClick, onOpenColumnManager } = props; - const rootProps = useGridRootProps(); - const { t } = useTranslation(); - const handleClick = useCallback( - (event: MouseEvent) => { - onClick(event); - onOpenColumnManager(); - }, - [onClick, onOpenColumnManager], - ); - - if (rootProps.disableColumnSelector) { - return null; - } - - const MenuItem = rootProps.slots.baseMenuItem; - const Icon = rootProps.slots.columnMenuManageColumnsIcon; - - return ( - }> - {t("connections.components.columnManager.title")} - - ); -}; - -const ConnectionColumnMenuColumnsItem = (props: GridColumnMenuItemProps) => { - const onOpenColumnManager = use(ColumnManagerContext); - - return ( - <> - - ); }; diff --git a/clash-verge-rev/src/components/home/clash-info-card.tsx b/clash-verge-rev/src/components/home/clash-info-card.tsx index ed62732f52..bb89c7d31e 100644 --- a/clash-verge-rev/src/components/home/clash-info-card.tsx +++ b/clash-verge-rev/src/components/home/clash-info-card.tsx @@ -3,8 +3,14 @@ import { Divider, Stack, Typography } from "@mui/material"; import { useMemo } from "react"; import { useTranslation } from "react-i18next"; +import { + useAppUptime, + useClashConfig, + useRulesData, + useSystemProxyAddress, + useSystemProxyData, +} from "@/hooks/app-data"; import { useClash } from "@/hooks/use-clash"; -import { useAppData } from "@/providers/app-data-context"; import { EnhancedCard } from "./enhanced-card"; @@ -19,7 +25,14 @@ const formatUptime = (uptimeMs: number) => { export const ClashInfoCard = () => { const { t } = useTranslation(); const { version: clashVersion } = useClash(); - const { clashConfig, rules, uptime, systemProxyAddress } = useAppData(); + const { clashConfig } = useClashConfig(); + const { sysproxy } = useSystemProxyData(); + const { rules } = useRulesData(); + const { uptime } = useAppUptime(); + const systemProxyAddress = useSystemProxyAddress({ + clashConfig, + sysproxy, + }); // 使用useMemo缓存格式化后的uptime,避免频繁计算 const formattedUptime = useMemo(() => formatUptime(uptime), [uptime]); diff --git a/clash-verge-rev/src/components/home/clash-mode-card.tsx b/clash-verge-rev/src/components/home/clash-mode-card.tsx index ea48128ca9..2624ce2dd2 100644 --- a/clash-verge-rev/src/components/home/clash-mode-card.tsx +++ b/clash-verge-rev/src/components/home/clash-mode-card.tsx @@ -9,8 +9,8 @@ import { useMemo } from "react"; import { useTranslation } from "react-i18next"; import { closeAllConnections } from "tauri-plugin-mihomo-api"; +import { useClashConfig } from "@/hooks/app-data"; import { useVerge } from "@/hooks/use-verge"; -import { useAppData } from "@/providers/app-data-context"; import { patchClashMode } from "@/services/cmds"; import type { TranslationKey } from "@/types/generated/i18n-keys"; @@ -41,7 +41,7 @@ const MODE_META: Record< export const ClashModeCard = () => { const { t } = useTranslation(); const { verge } = useVerge(); - const { clashConfig, refreshClashConfig } = useAppData(); + const { clashConfig, refreshClashConfig } = useClashConfig(); // 支持的模式列表 const modeList = CLASH_MODES; diff --git a/clash-verge-rev/src/components/home/current-proxy-card.tsx b/clash-verge-rev/src/components/home/current-proxy-card.tsx index c29db668b5..2aabba29b8 100644 --- a/clash-verge-rev/src/components/home/current-proxy-card.tsx +++ b/clash-verge-rev/src/components/home/current-proxy-card.tsx @@ -34,11 +34,12 @@ import { useNavigate } from "react-router"; import { delayGroup, healthcheckProxyProvider } from "tauri-plugin-mihomo-api"; import { EnhancedCard } from "@/components/home/enhanced-card"; +import { useClashConfig, useProxiesData, useRulesData } from "@/hooks/app-data"; import { useProfiles } from "@/hooks/use-profiles"; import { useProxySelection } from "@/hooks/use-proxy-selection"; import { useVerge } from "@/hooks/use-verge"; -import { useAppData } from "@/providers/app-data-context"; import delayManager from "@/services/delay"; +import { debugLog } from "@/utils/debug"; // 本地存储的键名 const STORAGE_KEY_GROUP = "clash-verge-selected-proxy-group"; @@ -100,7 +101,9 @@ export const CurrentProxyCard = () => { const { t } = useTranslation(); const navigate = useNavigate(); const theme = useTheme(); - const { proxies, clashConfig, refreshProxy, rules } = useAppData(); + const { proxies, refreshProxy } = useProxiesData(); + const { clashConfig } = useClashConfig(); + const { rules } = useRulesData(); const { verge } = useVerge(); const { current: currentProfile } = useProfiles(); const autoDelayEnabled = verge?.enable_auto_delay_detection ?? false; @@ -537,7 +540,7 @@ export const CurrentProxyCard = () => { const proxyRecord = latestProxyRecordRef.current; if (!proxyRecord) { - console.log( + debugLog( `[CurrentProxyCard] 自动延迟检测跳过,组: ${groupName}, 节点: ${proxyName} 未找到`, ); return; @@ -548,7 +551,7 @@ export const CurrentProxyCard = () => { const timeout = latestTimeoutRef.current || 10000; try { - console.log( + debugLog( `[CurrentProxyCard] 自动检测当前节点延迟,组: ${groupName}, 节点: ${proxyName}`, ); if (proxyRecord.provider) { @@ -645,7 +648,7 @@ export const CurrentProxyCard = () => { const groupName = state.selection.group; if (!groupName || isDirectMode) return; - console.log(`[CurrentProxyCard] 开始测试所有延迟,组: ${groupName}`); + debugLog(`[CurrentProxyCard] 开始测试所有延迟,组: ${groupName}`); const timeout = verge?.default_latency_timeout || 10000; @@ -685,13 +688,13 @@ export const CurrentProxyCard = () => { } } - console.log( + debugLog( `[CurrentProxyCard] 找到代理数量: ${proxyNames.length}, 提供者数量: ${providers.size}`, ); // 测试提供者的节点 if (providers.size > 0) { - console.log(`[CurrentProxyCard] 开始测试提供者节点`); + debugLog(`[CurrentProxyCard] 开始测试提供者节点`); await Promise.allSettled( [...providers].map((p) => healthcheckProxyProvider(p)), ); @@ -700,14 +703,14 @@ export const CurrentProxyCard = () => { // 测试非提供者的节点 if (proxyNames.length > 0) { const url = delayManager.getUrl(groupName); - console.log(`[CurrentProxyCard] 测试URL: ${url}, 超时: ${timeout}ms`); + debugLog(`[CurrentProxyCard] 测试URL: ${url}, 超时: ${timeout}ms`); try { await Promise.race([ delayManager.checkListDelay(proxyNames, groupName, timeout), delayGroup(groupName, url, timeout), ]); - console.log(`[CurrentProxyCard] 延迟测试完成,组: ${groupName}`); + debugLog(`[CurrentProxyCard] 延迟测试完成,组: ${groupName}`); } catch (error) { console.error( `[CurrentProxyCard] 延迟测试出错,组: ${groupName}`, diff --git a/clash-verge-rev/src/components/home/enhanced-canvas-traffic-graph.tsx b/clash-verge-rev/src/components/home/enhanced-canvas-traffic-graph.tsx index 0cb5d43cf0..9390b84571 100644 --- a/clash-verge-rev/src/components/home/enhanced-canvas-traffic-graph.tsx +++ b/clash-verge-rev/src/components/home/enhanced-canvas-traffic-graph.tsx @@ -16,6 +16,7 @@ import { useTrafficGraphDataEnhanced, type ITrafficDataPoint, } from "@/hooks/use-traffic-monitor"; +import { debugLog } from "@/utils/debug"; import parseTraffic from "@/utils/parse-traffic"; // 流量数据项接口 @@ -109,7 +110,7 @@ export const EnhancedCanvasTrafficGraph = memo( const { t } = useTranslation(); // 使用增强版全局流量数据管理 - const { dataPoints, getDataForTimeRange, samplerStats } = + const { dataPoints, requestRange, samplerStats } = useTrafficGraphDataEnhanced(); // 基础状态 @@ -182,8 +183,7 @@ export const EnhancedCanvasTrafficGraph = memo( // 监听数据变化 useEffect(() => { - const timeRangeData = getDataForTimeRange(timeRange); - updateDisplayData(timeRangeData); + updateDisplayData(dataPoints); return () => { if (debounceTimeoutRef.current !== null) { @@ -191,7 +191,11 @@ export const EnhancedCanvasTrafficGraph = memo( debounceTimeoutRef.current = null; } }; - }, [dataPoints, timeRange, getDataForTimeRange, updateDisplayData]); + }, [dataPoints, updateDisplayData]); + + useEffect(() => { + requestRange(timeRange); + }, [requestRange, timeRange]); useEffect(() => { if (displayData.length === 0) { @@ -795,32 +799,43 @@ export const EnhancedCanvasTrafficGraph = memo( const ctx = canvas.getContext("2d"); if (!ctx) return; - // Canvas尺寸设置 + // Compute CSS size and pixel buffer size. + // Note: WebView2 on Windows may return fractional CSS sizes after maximize. + // We round pixel buffer to integers to avoid 1px gaps/cropping artifacts. const rect = canvas.getBoundingClientRect(); const dpr = window.devicePixelRatio || 1; - const width = rect.width; - const height = rect.height; + const cssWidth = rect.width; + const cssHeight = rect.height; + const pixelWidth = Math.max(1, Math.floor(cssWidth * dpr)); + const pixelHeight = Math.max(1, Math.floor(cssHeight * dpr)); - // 只在尺寸变化时重新设置Canvas - if (canvas.width !== width * dpr || canvas.height !== height * dpr) { - canvas.width = width * dpr; - canvas.height = height * dpr; - ctx.scale(dpr, dpr); - canvas.style.width = width + "px"; - canvas.style.height = height + "px"; + // Keep CSS-driven sizing so the canvas stretches with its container (e.g., on maximize). + if (canvas.style.width !== "100%") { + canvas.style.width = "100%"; + } + if (canvas.style.height !== "100%") { + canvas.style.height = "100%"; } - // 清空画布 - ctx.clearRect(0, 0, width, height); + if (canvas.width !== pixelWidth || canvas.height !== pixelHeight) { + canvas.width = pixelWidth; + canvas.height = pixelHeight; + // Reset transform before scaling to avoid cumulative scaling offsets. + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.scale(dpr, dpr); // map CSS units to device pixels + } + + // Clear using CSS dimensions; context is already scaled by DPR. + ctx.clearRect(0, 0, cssWidth, cssHeight); // 绘制Y轴刻度线(背景层) - drawYAxis(ctx, width, height, displayData); + drawYAxis(ctx, cssWidth, cssHeight, displayData); // 绘制网格 - drawGrid(ctx, width, height); + drawGrid(ctx, cssWidth, cssHeight); // 绘制时间轴 - drawTimeAxis(ctx, width, height, displayData); + drawTimeAxis(ctx, cssWidth, cssHeight, displayData); // 提取流量数据 const upValues = displayData.map((d) => d.up); @@ -830,8 +845,8 @@ export const EnhancedCanvasTrafficGraph = memo( drawTrafficLine( ctx, downValues, - width, - height, + cssWidth, + cssHeight, colors.down, true, displayData, @@ -841,8 +856,8 @@ export const EnhancedCanvasTrafficGraph = memo( drawTrafficLine( ctx, upValues, - width, - height, + cssWidth, + cssHeight, colors.up, true, displayData, @@ -851,7 +866,7 @@ export const EnhancedCanvasTrafficGraph = memo( // 绘制悬浮高亮线 if (tooltipData.visible && tooltipData.dataIndex >= 0) { const padding = GRAPH_CONFIG.padding; - const effectiveWidth = width - padding.left - padding.right; + const effectiveWidth = cssWidth - padding.left - padding.right; const dataX = padding.left + (tooltipData.dataIndex / (displayData.length - 1)) * effectiveWidth; @@ -865,13 +880,13 @@ export const EnhancedCanvasTrafficGraph = memo( // 绘制垂直指示线 ctx.beginPath(); ctx.moveTo(dataX, padding.top); - ctx.lineTo(dataX, height - padding.bottom); + ctx.lineTo(dataX, cssHeight - padding.bottom); ctx.stroke(); // 绘制水平指示线(高亮Y轴位置) ctx.beginPath(); ctx.moveTo(padding.left, tooltipData.highlightY); - ctx.lineTo(width - padding.right, tooltipData.highlightY); + ctx.lineTo(cssWidth - padding.right, tooltipData.highlightY); ctx.stroke(); ctx.restore(); @@ -1033,7 +1048,7 @@ export const EnhancedCanvasTrafficGraph = memo( // 兼容性方法 const appendData = useCallback((data: ITrafficItem) => { - console.log( + debugLog( "[EnhancedCanvasTrafficGraphV2] appendData called (using global data):", data, ); diff --git a/clash-verge-rev/src/components/home/home-profile-card.tsx b/clash-verge-rev/src/components/home/home-profile-card.tsx index 86d0e3ef02..8e40287f41 100644 --- a/clash-verge-rev/src/components/home/home-profile-card.tsx +++ b/clash-verge-rev/src/components/home/home-profile-card.tsx @@ -24,7 +24,7 @@ import { useCallback, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router"; -import { useAppData } from "@/providers/app-data-context"; +import { useRefreshAll } from "@/hooks/app-data"; import { openWebUrl, updateProfile } from "@/services/cmds"; import { showNotice } from "@/services/noticeService"; import parseTraffic from "@/utils/parse-traffic"; @@ -281,7 +281,7 @@ export const HomeProfileCard = ({ }: HomeProfileCardProps) => { const { t } = useTranslation(); const navigate = useNavigate(); - const { refreshAll } = useAppData(); + const refreshAll = useRefreshAll(); // 更新当前订阅 const [updating, setUpdating] = useState(false); @@ -363,7 +363,7 @@ export const HomeProfileCard = ({ endIcon={} sx={{ borderRadius: 1.5 }} > - {t("layout.components.navigation.tabs.proxies")} + {t("layout.components.navigation.tabs.profiles")} ); }, [current, goToProfiles, t]); diff --git a/clash-verge-rev/src/components/home/ip-info-card.tsx b/clash-verge-rev/src/components/home/ip-info-card.tsx index 18360e5502..70be583797 100644 --- a/clash-verge-rev/src/components/home/ip-info-card.tsx +++ b/clash-verge-rev/src/components/home/ip-info-card.tsx @@ -5,7 +5,7 @@ import { VisibilityOutlined, } from "@mui/icons-material"; import { Box, Button, IconButton, Skeleton, Typography } from "@mui/material"; -import { memo, useCallback, useEffect, useState } from "react"; +import { memo, useCallback, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { getIpInfo } from "@/services/api"; @@ -14,6 +14,7 @@ import { EnhancedCard } from "./enhanced-card"; // 定义刷新时间(秒) const IP_REFRESH_SECONDS = 300; +const IP_INFO_CACHE_KEY = "cv_ip_info_cache"; // 提取InfoItem子组件并使用memo优化 const InfoItem = memo(({ label, value }: { label: string; value: string }) => ( @@ -59,47 +60,100 @@ export const IpInfoCard = () => { const [error, setError] = useState(""); const [showIp, setShowIp] = useState(false); const [countdown, setCountdown] = useState(IP_REFRESH_SECONDS); + const lastFetchRef = useRef(null); - // 获取IP信息 - const fetchIpInfo = useCallback(async () => { - try { - setLoading(true); + const fetchIpInfo = useCallback( + async (force = false) => { setError(""); - const data = await getIpInfo(); - setIpInfo(data); - setCountdown(IP_REFRESH_SECONDS); - } catch (err) { - setError( - err instanceof Error - ? err.message - : t("home.components.ipInfo.errors.load"), - ); - } finally { - setLoading(false); - } - }, [t]); - // 组件加载时获取IP信息 + try { + if (!force && typeof window !== "undefined" && window.sessionStorage) { + const raw = window.sessionStorage.getItem(IP_INFO_CACHE_KEY); + if (raw) { + const parsed = JSON.parse(raw); + const now = Date.now(); + if ( + parsed?.ts && + parsed?.data && + now - parsed.ts < IP_REFRESH_SECONDS * 1000 + ) { + setIpInfo(parsed.data); + lastFetchRef.current = parsed.ts; + const elapsed = Math.floor((now - parsed.ts) / 1000); + setCountdown(Math.max(IP_REFRESH_SECONDS - elapsed, 0)); + setLoading(false); + return; + } + } + } + } catch (e) { + console.warn("Failed to read IP info from sessionStorage:", e); + } + + try { + setLoading(true); + const data = await getIpInfo(); + setIpInfo(data); + const ts = Date.now(); + lastFetchRef.current = ts; + try { + if (typeof window !== "undefined" && window.sessionStorage) { + window.sessionStorage.setItem( + IP_INFO_CACHE_KEY, + JSON.stringify({ data, ts }), + ); + } + } catch (e) { + console.warn("Failed to write IP info to sessionStorage:", e); + } + setCountdown(IP_REFRESH_SECONDS); + } catch (err) { + setError( + err instanceof Error + ? err.message + : t("home.components.ipInfo.errors.load"), + ); + } finally { + setLoading(false); + } + }, + [t], + ); + + // 组件加载时获取IP信息并启动基于上次请求时间的倒计时 useEffect(() => { fetchIpInfo(); - // 倒计时实现优化,减少不必要的重渲染 let timer: number | null = null; - let currentCount = IP_REFRESH_SECONDS; - // 只在必要时更新状态,减少重渲染次数 const startCountdown = () => { timer = window.setInterval(() => { - currentCount -= 1; + const now = Date.now(); + let ts = lastFetchRef.current; + try { + if (!ts && typeof window !== "undefined" && window.sessionStorage) { + const raw = window.sessionStorage.getItem(IP_INFO_CACHE_KEY); + if (raw) { + const parsed = JSON.parse(raw); + ts = parsed?.ts || null; + } + } + } catch (e) { + console.warn("Failed to read IP info from sessionStorage:", e); + ts = ts || null; + } - if (currentCount <= 0) { + const elapsed = ts ? Math.floor((now - ts) / 1000) : 0; + let remaining = IP_REFRESH_SECONDS - elapsed; + + if (remaining <= 0) { fetchIpInfo(); - currentCount = IP_REFRESH_SECONDS; + remaining = IP_REFRESH_SECONDS; } // 每5秒或倒计时结束时才更新UI - if (currentCount % 5 === 0 || currentCount <= 0) { - setCountdown(currentCount); + if (remaining % 5 === 0 || remaining <= 0) { + setCountdown(remaining); } }, 1000); }; @@ -122,7 +176,11 @@ export const IpInfoCard = () => { icon={} iconColor="info" action={ - + fetchIpInfo(true)} + disabled={true} + > } @@ -145,7 +203,7 @@ export const IpInfoCard = () => { icon={} iconColor="info" action={ - + fetchIpInfo(true)}> } @@ -163,7 +221,7 @@ export const IpInfoCard = () => { {error} - @@ -178,7 +236,7 @@ export const IpInfoCard = () => { icon={} iconColor="info" action={ - + fetchIpInfo(true)}> } diff --git a/clash-verge-rev/src/components/home/test-card.tsx b/clash-verge-rev/src/components/home/test-card.tsx index 903f3c7ce0..ffaa69c916 100644 --- a/clash-verge-rev/src/components/home/test-card.tsx +++ b/clash-verge-rev/src/components/home/test-card.tsx @@ -62,7 +62,7 @@ const DEFAULT_TEST_LIST = [ }, { uid: nanoid(), - name: "Youtube", + name: "YouTube", url: "https://www.youtube.com", icon: youtube, }, diff --git a/clash-verge-rev/src/components/layout/layout-item.tsx b/clash-verge-rev/src/components/layout/layout-item.tsx index e1c233ba04..fdc2e1539c 100644 --- a/clash-verge-rev/src/components/layout/layout-item.tsx +++ b/clash-verge-rev/src/components/layout/layout-item.tsx @@ -6,11 +6,11 @@ import { alpha, ListItem, ListItemButton, - ListItemText, ListItemIcon, + ListItemText, } from "@mui/material"; import type { CSSProperties, ReactNode } from "react"; -import { useMatch, useResolvedPath, useNavigate } from "react-router"; +import { useMatch, useNavigate, useResolvedPath } from "react-router"; import { useVerge } from "@/hooks/use-verge"; @@ -58,7 +58,7 @@ export const LayoutItem = (props: Props) => { paddingLeft: 1, paddingRight: 1, marginRight: 1.25, - cursor: sortable && !sortable.disabled ? "grab" : "pointer", + cursor: "pointer", "& .MuiListItemText-primary": { color: "text.primary", fontWeight: "700", @@ -70,7 +70,6 @@ export const LayoutItem = (props: Props) => { ? alpha(primary.main, 0.15) : alpha(primary.main, 0.35); const color = mode === "light" ? "#1f1f1f" : "#ffffff"; - return { "&.Mui-selected": { bgcolor }, "&.Mui-selected:hover": { bgcolor }, @@ -79,15 +78,29 @@ export const LayoutItem = (props: Props) => { }, ]} onClick={() => navigate(to)} - {...(attributes ?? {})} - {...(listeners ?? {})} > {(menu_icon === "monochrome" || !menu_icon) && ( - + {icon[0]} )} - {menu_icon === "colorful" && {icon[1]}} + {menu_icon === "colorful" && ( + + {icon[1]} + + )} = { - zh: { ...zhXDataGrid }, - fa: { ...faXDataGrid }, - ru: { ...ruXDataGrid }, - ar: { ...arXDataGrid }, - en: { ...enXDataGrid }, +const CSS_INJECTION_SCOPE_ROOT = "[data-css-injection-root]"; +const CSS_INJECTION_SCOPE_LIMIT = + ':is(.monaco-editor .view-lines, .monaco-editor .view-line, .monaco-editor .margin, .monaco-editor .margin-view-overlays, .monaco-editor .view-overlays, .monaco-editor [class^="mtk"], .monaco-editor [class*=" mtk"])'; +const TOP_LEVEL_AT_RULES = [ + "@charset", + "@import", + "@namespace", + "@font-face", + "@keyframes", + "@counter-style", + "@page", + "@property", + "@font-feature-values", + "@color-profile", +]; +let cssScopeSupport: boolean | null = null; + +const canUseCssScope = () => { + if (cssScopeSupport !== null) { + return cssScopeSupport; + } + if (typeof document === "undefined") { + return false; + } + try { + const testStyle = document.createElement("style"); + testStyle.textContent = "@scope (:root) { }"; + document.head.appendChild(testStyle); + cssScopeSupport = !!testStyle.sheet?.cssRules?.length; + document.head.removeChild(testStyle); + } catch { + cssScopeSupport = false; + } + return cssScopeSupport; }; -const getLanguagePackMap = (key: string) => - languagePackMap[key] || languagePackMap.en; +const wrapCssInjectionWithScope = (css?: string) => { + if (!css?.trim()) { + return ""; + } + const lowerCss = css.toLowerCase(); + const hasTopLevelOnlyRule = TOP_LEVEL_AT_RULES.some((rule) => + lowerCss.includes(rule), + ); + if (hasTopLevelOnlyRule) { + return null; + } + const scopeRoot = CSS_INJECTION_SCOPE_ROOT; + const scopeLimit = CSS_INJECTION_SCOPE_LIMIT; + const scopedBlock = `@scope (${scopeRoot}) to (${scopeLimit}) { +${css} +}`; + return scopedBlock; +}; /** * custom theme @@ -35,7 +71,6 @@ const getLanguagePackMap = (key: string) => export const useCustomTheme = () => { const appWindow: WebviewWindow = useMemo(() => getCurrentWebviewWindow(), []); const { verge } = useVerge(); - const { i18n } = useTranslation(); const { theme_mode, theme_setting } = verge ?? {}; const mode = useThemeMode(); const setMode = useSetThemeMode(); @@ -134,8 +169,27 @@ export const useCustomTheme = () => { }; const legacyQuery = mediaQuery as MediaQueryListLegacy; - legacyQuery.addListener?.(handleChange); - return () => legacyQuery.removeListener?.(handleChange); + const legacyAddListener = ( + legacyQuery as { + addListener?: ( + listener: (this: MediaQueryList, event: MediaQueryListEvent) => void, + ) => void; + } + ).addListener; + legacyAddListener?.call(legacyQuery, handleChange); + return () => { + const legacyRemoveListener = ( + legacyQuery as { + removeListener?: ( + listener: ( + this: MediaQueryList, + event: MediaQueryListEvent, + ) => void, + ) => void; + } + ).removeListener; + legacyRemoveListener?.call(legacyQuery, handleChange); + }; }, [theme_mode, setMode]); useEffect(() => { @@ -163,37 +217,34 @@ export const useCustomTheme = () => { let muiTheme: MuiTheme; try { - muiTheme = createTheme( - { - breakpoints: { - values: { xs: 0, sm: 650, md: 900, lg: 1200, xl: 1536 }, + muiTheme = createTheme({ + breakpoints: { + values: { xs: 0, sm: 650, md: 900, lg: 1200, xl: 1536 }, + }, + palette: { + mode, + primary: { main: setting.primary_color || dt.primary_color }, + secondary: { main: setting.secondary_color || dt.secondary_color }, + info: { main: setting.info_color || dt.info_color }, + error: { main: setting.error_color || dt.error_color }, + warning: { main: setting.warning_color || dt.warning_color }, + success: { main: setting.success_color || dt.success_color }, + text: { + primary: setting.primary_text || dt.primary_text, + secondary: setting.secondary_text || dt.secondary_text, }, - palette: { - mode, - primary: { main: setting.primary_color || dt.primary_color }, - secondary: { main: setting.secondary_color || dt.secondary_color }, - info: { main: setting.info_color || dt.info_color }, - error: { main: setting.error_color || dt.error_color }, - warning: { main: setting.warning_color || dt.warning_color }, - success: { main: setting.success_color || dt.success_color }, - text: { - primary: setting.primary_text || dt.primary_text, - secondary: setting.secondary_text || dt.secondary_text, - }, - background: { - paper: dt.background_color, - default: dt.background_color, - }, - }, - shadows: Array(25).fill("none") as Shadows, - typography: { - fontFamily: setting.font_family - ? `${setting.font_family}, ${dt.font_family}` - : dt.font_family, + background: { + paper: dt.background_color, + default: dt.background_color, }, }, - getLanguagePackMap(i18n.language), - ); + shadows: Array(25).fill("none") as Shadows, + typography: { + fontFamily: setting.font_family + ? `${setting.font_family}, ${dt.font_family}` + : dt.font_family, + }, + }); } catch (e) { console.error("Error creating MUI theme, falling back to defaults:", e); muiTheme = createTheme({ @@ -264,6 +315,7 @@ export const useCustomTheme = () => { ? String(setting.background_opacity) : "1", ); + rootEle.setAttribute("data-css-injection-root", "true"); } let styleElement = document.querySelector("style#verge-theme"); @@ -274,6 +326,11 @@ export const useCustomTheme = () => { } if (styleElement) { + let scopedCss: string | null = null; + if (canUseCssScope() && setting.css_injection) { + scopedCss = wrapCssInjectionWithScope(setting.css_injection); + } + const effectiveInjectedCss = scopedCss ?? setting.css_injection ?? ""; const globalStyles = ` /* 修复滚动条样式 */ ::-webkit-scrollbar { @@ -323,7 +380,7 @@ export const useCustomTheme = () => { } `; - styleElement.innerHTML = (setting.css_injection || "") + globalStyles; + styleElement.innerHTML = effectiveInjectedCss + globalStyles; } const { palette } = muiTheme; @@ -339,13 +396,7 @@ export const useCustomTheme = () => { }, 0); return muiTheme; - }, [ - mode, - theme_setting, - i18n.language, - userBackgroundImage, - hasUserBackground, - ]); + }, [mode, theme_setting, userBackgroundImage, hasUserBackground]); return { theme }; }; diff --git a/clash-verge-rev/src/components/profile/editor-viewer.tsx b/clash-verge-rev/src/components/profile/editor-viewer.tsx index 64f2e16756..6ab2e0ae8f 100644 --- a/clash-verge-rev/src/components/profile/editor-viewer.tsx +++ b/clash-verge-rev/src/components/profile/editor-viewer.tsx @@ -1,6 +1,7 @@ import MonacoEditor from "@monaco-editor/react"; import { CloseFullscreenRounded, + ContentPasteRounded, FormatPaintRounded, OpenInFullRounded, } from "@mui/icons-material"; @@ -15,16 +16,14 @@ import { } from "@mui/material"; import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; import { useLockFn } from "ahooks"; -import { type JSONSchema7 } from "json-schema"; -import mergeSchema from "meta-json-schema/schemas/clash-verge-merge-json-schema.json"; -import metaSchema from "meta-json-schema/schemas/meta-json-schema.json"; import * as monaco from "monaco-editor"; import { configureMonacoYaml } from "monaco-yaml"; import { nanoid } from "nanoid"; -import { ReactNode, useEffect, useMemo, useRef, useState } from "react"; +import { ReactNode, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import pac from "types-pac/pac.d.ts?raw"; +import { BaseLoadingOverlay } from "@/components/base"; import { showNotice } from "@/services/noticeService"; import { useThemeMode } from "@/services/states"; import debounce from "@/utils/debounce"; @@ -32,22 +31,19 @@ import getSystem from "@/utils/get-system"; const appWindow = getCurrentWebviewWindow(); type Language = "yaml" | "javascript" | "css"; -type Schema = LanguageSchemaMap[T]; -interface LanguageSchemaMap { - yaml: "clash" | "merge"; - javascript: never; - css: never; -} interface Props { open: boolean; title?: string | ReactNode; - initialData: Promise; + // Initial content loader: prefer passing a stable function. A plain Promise is supported, + // but it won't trigger background refreshes and should be paired with a stable `dataKey`. + initialData: Promise | (() => Promise); + // Logical document id; reloads when this or language changes. + dataKey?: string | number; readOnly?: boolean; language: T; - schema?: Schema; onChange?: (prev?: string, curr?: string) => void; - onSave?: (prev?: string, curr?: string) => void; + onSave?: (prev?: string, curr?: string) => void | Promise; onClose: () => void; } @@ -55,27 +51,13 @@ let initialized = false; const monacoInitialization = () => { if (initialized) return; - // configure yaml worker + // YAML worker setup configureMonacoYaml(monaco, { validate: true, enableSchemaRequest: true, - schemas: [ - { - uri: "http://example.com/meta-json-schema.json", - fileMatch: ["**/*.clash.yaml"], - // @ts-expect-error -- meta schema JSON import does not satisfy JSONSchema7 at compile time - schema: metaSchema as JSONSchema7, - }, - { - uri: "http://example.com/clash-verge-merge-json-schema.json", - fileMatch: ["**/*.merge.yaml"], - // @ts-expect-error -- merge schema JSON import does not satisfy JSONSchema7 at compile time - schema: mergeSchema as JSONSchema7, - }, - ], }); - // configure PAC definition - monaco.languages.typescript.javascriptDefaults.addExtraLib(pac, "pac.d.ts"); + // PAC type definitions for JS suggestions + monaco.typescript.javascriptDefaults.addExtraLib(pac, "pac.d.ts"); initialized = true; }; @@ -89,48 +71,230 @@ export const EditorViewer = (props: Props) => { open = false, title, initialData, + dataKey, readOnly = false, language = "yaml", - schema, onChange, onSave, onClose, } = props; const resolvedTitle = title ?? t("profiles.components.menu.editFile"); - const resolvedInitialData = useMemo( - () => initialData ?? Promise.resolve(""), - [initialData], - ); const editorRef = useRef(undefined); const prevData = useRef(""); const currData = useRef(""); + // Hold the latest loader without making effects depend on its identity + const initialDataRef = useRef["initialData"]>(initialData); + // Track mount/open state to prevent setState after unmount/close + const isMountedRef = useRef(true); + const openRef = useRef(open); + useEffect(() => { + openRef.current = open; + }, [open]); + useEffect(() => { + isMountedRef.current = true; + return () => { + isMountedRef.current = false; + }; + }, []); + const [initialText, setInitialText] = useState(null); + const [modelPath, setModelPath] = useState(""); + const modelChangeDisposableRef = useRef(null); + // Unique per-component instance id to avoid shared Monaco models across dialogs + const instanceIdRef = useRef(nanoid()); + // Disable actions while loading or before modelPath is ready + const isLoading = initialText === null || !modelPath; + // Track if background refresh failed; offer a retry action in UI + const [refreshFailed, setRefreshFailed] = useState(null); + // Skip the first background refresh triggered by [open, modelPath, dataKey] + // to avoid double-invoking the loader right after the initial load. + const skipNextRefreshRef = useRef(false); + // Monotonic token to cancel stale background refreshes + const reloadTokenRef = useRef(0); + // Track whether the editor has a usable baseline (either loaded or fallback). + // This avoids saving before the model/path are ready, while still allowing recovery + // when the initial load fails but an empty buffer is presented. + const [hasLoadedOnce, setHasLoadedOnce] = useState(false); + // Editor should only be read-only when explicitly requested by prop. + // A refresh/load failure must not lock the editor to allow manual recovery. + const effectiveReadOnly = readOnly; + // Keep ref in sync with prop without triggering loads + useEffect(() => { + initialDataRef.current = initialData; + }, [initialData]); + // Background refresh: when the dialog/model is ready and the underlying resource key changes, + // try to refresh content (only if user hasn't typed). Do NOT depend on `initialData` function + // identity because callers often pass inline lambdas that change every render. + useEffect(() => { + if (!open) return; + // Only attempt after initial model is ready to avoid racing the initial load + if (!modelPath) return; + // Avoid immediate double-load on open: the initial load has just completed. + if (skipNextRefreshRef.current) { + skipNextRefreshRef.current = false; + return; + } + // Only meaningful when a callable loader is provided (plain Promise cannot be "recalled") + if (typeof initialDataRef.current === "function") { + void reloadLatest(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [open, modelPath, dataKey]); + // Helper to (soft) reload latest source and apply only if the user hasn't typed yet + const reloadLatest = useLockFn(async () => { + // Snapshot the model/doc identity and bump a token so older calls can't win + const myToken = ++reloadTokenRef.current; + const expectedModelPath = modelPath; + const expectedKey = dataKey; + if (isMountedRef.current && openRef.current) { + // Clear previous error (UI hint) at the start of a new attempt + setRefreshFailed(null); + } + try { + const src = initialDataRef.current; + const promise = + typeof src === "function" + ? (src as () => Promise)() + : (src ?? Promise.resolve("")); + const next = await promise; + // Abort if component/dialog state changed meanwhile: + // - unmounted or closed + // - document switched (modelPath/dataKey no longer match) + // - a newer reload was started + if ( + !isMountedRef.current || + !openRef.current || + expectedModelPath !== modelPath || + expectedKey !== dataKey || + myToken !== reloadTokenRef.current + ) { + return; + } + // Only update when untouched and value changed + const userUntouched = currData.current === prevData.current; + if (userUntouched && next !== prevData.current) { + prevData.current = next; + currData.current = next; + editorRef.current?.setValue(next); + } + // Ensure any previous error state is cleared after a successful refresh + if (isMountedRef.current && openRef.current) { + setRefreshFailed(null); + } + // If we previously failed to load, a successful refresh establishes a valid baseline + if (isMountedRef.current && openRef.current) { + setHasLoadedOnce(true); + } + } catch (err) { + // Only report if still mounted/open and this call is the latest + if ( + isMountedRef.current && + openRef.current && + myToken === reloadTokenRef.current + ) { + setRefreshFailed(err ?? true); + showNotice.error( + "shared.feedback.notifications.common.refreshFailed", + err, + ); + } + } + }); const beforeMount = () => { - monacoInitialization(); // initialize monaco + monacoInitialization(); }; + // Prepare initial content and a stable model path for monaco-react + /* eslint-disable @eslint-react/hooks-extra/no-direct-set-state-in-use-effect */ + useEffect(() => { + if (!open) return; + let cancelled = false; + // Clear state up-front to avoid showing stale content while loading + setInitialText(null); + setModelPath(""); + // Clear any stale refresh error when starting a new load + setRefreshFailed(null); + // Reset initial-load success flag on open/start + setHasLoadedOnce(false); + // We will perform an explicit initial load below; skip the first background refresh. + skipNextRefreshRef.current = true; + prevData.current = undefined; + currData.current = undefined; + + (async () => { + try { + const dataSource = initialDataRef.current; + const dataPromise = + typeof dataSource === "function" + ? (dataSource as () => Promise)() + : (dataSource ?? Promise.resolve("")); + const data = await dataPromise; + if (cancelled) return; + prevData.current = data; + currData.current = data; + + setInitialText(data); + // Build a stable model path and avoid "undefined" in the name + const pathParts = [String(dataKey ?? nanoid()), instanceIdRef.current]; + pathParts.push(language); + + setModelPath(pathParts.join(".")); + // Successful initial load should clear any previous refresh error flag + setRefreshFailed(null); + // Mark that we have a valid baseline content + setHasLoadedOnce(true); + } catch (err) { + if (cancelled) return; + // Notify the error and still show an empty editor so the user isn't stuck + showNotice.error(err); + + // Align refs with fallback text after a load failure + prevData.current = ""; + currData.current = ""; + + setInitialText(""); + const pathParts = [String(dataKey ?? nanoid()), instanceIdRef.current]; + pathParts.push(language); + + setModelPath(pathParts.join(".")); + // Mark refresh failure so users can retry + setRefreshFailed(err ?? true); + // Initial load failed; keep `hasLoadedOnce` false to prevent accidental save + // of an empty buffer. It will be enabled on successful refresh or first edit. + setHasLoadedOnce(false); + } + })(); + + return () => { + cancelled = true; + }; + }, [open, dataKey, language]); + /* eslint-enable @eslint-react/hooks-extra/no-direct-set-state-in-use-effect */ + const onMount = async (editor: monaco.editor.IStandaloneCodeEditor) => { editorRef.current = editor; - - // retrieve initial data - await resolvedInitialData.then((data) => { - prevData.current = data; - currData.current = data; - - // create and set model - const uri = monaco.Uri.parse(`${nanoid()}.${schema}.${language}`); - const model = monaco.editor.createModel(data, language, uri); - editorRef.current?.setModel(model); + // Dispose previous model when switching (monaco-react creates a fresh model when `path` changes) + modelChangeDisposableRef.current?.dispose(); + modelChangeDisposableRef.current = editor.onDidChangeModel((e) => { + if (e.oldModelUrl) { + const oldModel = monaco.editor.getModel(e.oldModelUrl); + oldModel?.dispose(); + } }); + // No refresh on mount; doing so would double-load. + // Background refreshes are handled by the [open, modelPath, dataKey] effect. }; - const handleChange = useLockFn(async (_value?: string) => { + const handleChange = useLockFn(async (value?: string) => { try { - const value = editorRef.current?.getValue(); - currData.current = value; + currData.current = value ?? editorRef.current?.getValue(); onChange?.(prevData.current, currData.current); + // If the initial load failed, allow saving after the user makes an edit. + if (!hasLoadedOnce) { + setHasLoadedOnce(true); + } } catch (err) { showNotice.error(err); } @@ -138,9 +302,18 @@ export const EditorViewer = (props: Props) => { const handleSave = useLockFn(async () => { try { - if (!readOnly) { - currData.current = editorRef.current?.getValue(); - onSave?.(prevData.current, currData.current); + // Disallow saving if initial content never loaded successfully to avoid accidental overwrite + if (!readOnly && hasLoadedOnce) { + // Guard: if the editor/model hasn't mounted, bail out + if (!editorRef.current) { + return; + } + currData.current = editorRef.current.getValue(); + if (onSave) { + await onSave(prevData.current, currData.current); + // If save succeeds, align prev with current + prevData.current = currData.current; + } } onClose(); } catch (err) { @@ -148,6 +321,33 @@ export const EditorViewer = (props: Props) => { } }); + // Explicit paste action: works even when Monaco's context-menu paste cannot read clipboard. + const handlePaste = useLockFn(async () => { + try { + if (!editorRef.current || effectiveReadOnly) return; + const text = await navigator.clipboard.readText(); + if (!text) return; + const editor = editorRef.current; + const model = editor.getModel(); + const selections = editor.getSelections(); + if (!model || !selections || selections.length === 0) return; + // Group edits to allow single undo step + editor.pushUndoStop(); + editor.executeEdits( + "explicit-paste", + selections.map((sel) => ({ + range: sel, + text, + forceMoveMarkers: true, + })), + ); + editor.pushUndoStop(); + editor.focus(); + } catch (err) { + showNotice.error(err); + } + }); + const handleClose = useLockFn(async () => { try { onClose(); @@ -156,30 +356,30 @@ export const EditorViewer = (props: Props) => { } }); - const editorResize = useMemo( - () => - debounce(() => { - editorRef.current?.layout(); - setTimeout(() => editorRef.current?.layout(), 500); - }, 100), - [], - ); - useEffect(() => { const onResized = debounce(() => { - editorResize(); - appWindow.isMaximized().then((maximized) => { - setIsMaximized(() => maximized); - }); + appWindow + .isMaximized() + .then((maximized) => setIsMaximized(() => maximized)); + // Ensure Monaco recalculates layout after window resize/maximize/restore. + // automaticLayout is not always sufficient when the parent dialog resizes. + try { + editorRef.current?.layout(); + } catch {} }, 100); const unlistenResized = appWindow.onResized(onResized); return () => { unlistenResized.then((fn) => fn()); + // Clean up editor and model to avoid leaks + const model = editorRef.current?.getModel(); editorRef.current?.dispose(); + model?.dispose(); + modelChangeDisposableRef.current?.dispose(); + modelChangeDisposableRef.current = null; editorRef.current = undefined; }; - }, [editorResize]); + }, []); return ( @@ -188,52 +388,115 @@ export const EditorViewer = (props: Props) => { - = 1500, // 超过一定宽度显示minimap滚动条 - }, - mouseWheelZoom: true, // 按住Ctrl滚轮调节缩放比例 - readOnly: readOnly, // 只读模式 - readOnlyMessage: { - value: t("profiles.modals.editor.messages.readOnly"), - }, // 只读模式尝试编辑时的提示信息 - renderValidationDecorations: "on", // 只读模式下显示校验信息 - quickSuggestions: { - strings: true, // 字符串类型的建议 - comments: true, // 注释类型的建议 - other: true, // 其他类型的建议 - }, - padding: { - top: 33, // 顶部padding防止遮挡snippets - }, - fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${ - getSystem() === "windows" ? ", twemoji mozilla" : "" - }`, - fontLigatures: false, // 连字符 - smoothScrolling: true, // 平滑滚动 - }} - beforeMount={beforeMount} - onMount={onMount} - onChange={handleChange} - /> +
+ {/* Show overlay while loading or until modelPath is ready */} + + {/* Background refresh failure helper */} + {!!refreshFailed && ( +
+ + {t("shared.feedback.notifications.common.refreshFailed")} + + +
+ )} + {initialText !== null && modelPath && ( + = 1500, + }, + mouseWheelZoom: true, + readOnly: effectiveReadOnly, + readOnlyMessage: { + value: t("profiles.modals.editor.messages.readOnly"), + }, + renderValidationDecorations: "on", + quickSuggestions: { + strings: true, + comments: true, + other: true, + }, + padding: { + top: 33, // Top padding to prevent snippet overlap + }, + fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${ + getSystem() === "windows" ? ", twemoji mozilla" : "" + }`, + fontLigatures: false, + smoothScrolling: true, + }} + beforeMount={beforeMount} + onMount={onMount} + onChange={handleChange} + /> + )} +
+ handlePaste()} + > + + editorRef.current ?.getAction("editor.action.formatDocument") @@ -248,7 +511,21 @@ export const EditorViewer = (props: Props) => { title={t( isMaximized ? "shared.window.minimize" : "shared.window.maximize", )} - onClick={() => appWindow.toggleMaximize().then(editorResize)} + onClick={() => + appWindow + .toggleMaximize() + .then(() => + appWindow + .isMaximized() + .then((maximized) => setIsMaximized(maximized)), + ) + .finally(() => { + // Nudge a layout in case the resize event batching lags behind + try { + editorRef.current?.layout(); + } catch {} + }) + } > {isMaximized ? : } @@ -260,7 +537,11 @@ export const EditorViewer = (props: Props) => { {t(readOnly ? "shared.actions.close" : "shared.actions.cancel")} {!readOnly && ( - )} diff --git a/clash-verge-rev/src/components/profile/profile-item.tsx b/clash-verge-rev/src/components/profile/profile-item.tsx index 7f928336d3..800c10238f 100644 --- a/clash-verge-rev/src/components/profile/profile-item.tsx +++ b/clash-verge-rev/src/components/profile/profile-item.tsx @@ -37,6 +37,7 @@ import { import { showNotice } from "@/services/noticeService"; import { useLoadingCache, useSetLoadingCache } from "@/services/states"; import type { TranslationKey } from "@/types/generated/i18n-keys"; +import { debugLog } from "@/utils/debug"; import parseTraffic from "@/utils/parse-traffic"; import { ProfileBox } from "./profile-box"; @@ -104,16 +105,16 @@ export const ProfileItem = (props: Props) => { itemData.option.update_interval > 0 ) { try { - console.log(`尝试获取配置 ${itemData.uid} 的下次更新时间`); + debugLog(`尝试获取配置 ${itemData.uid} 的下次更新时间`); // 如果需要强制刷新,先触发Timer.refresh() if (forceRefresh) { // 这里可以通过一个新的API来触发刷新,但目前我们依赖patch_profile中的刷新 - console.log(`强制刷新定时器任务`); + debugLog(`强制刷新定时器任务`); } const nextUpdate = await getNextUpdateTime(itemData.uid); - console.log(`获取到下次更新时间结果:`, nextUpdate); + debugLog(`获取到下次更新时间结果:`, nextUpdate); if (nextUpdate) { const nextUpdateDate = dayjs(nextUpdate * 1000); @@ -147,7 +148,7 @@ export const ProfileItem = (props: Props) => { } } } else { - console.log(`返回的下次更新时间为空`); + debugLog(`返回的下次更新时间为空`); setNextUpdateTime( t("profiles.components.profileItem.status.noSchedule"), ); @@ -157,7 +158,7 @@ export const ProfileItem = (props: Props) => { setNextUpdateTime(t("profiles.components.profileItem.status.unknown")); } } else { - console.log(`该配置未设置更新间隔或间隔为0`); + debugLog(`该配置未设置更新间隔或间隔为0`); setNextUpdateTime( t("profiles.components.profileItem.status.autoUpdateDisabled"), ); @@ -197,7 +198,7 @@ export const ProfileItem = (props: Props) => { // 只有当更新的是当前配置时才刷新显示 if (updatedUid === itemData.uid && showNextUpdate) { - console.log(`收到定时器更新事件: uid=${updatedUid}`); + debugLog(`收到定时器更新事件: uid=${updatedUid}`); if (refreshTimeout !== undefined) { clearTimeout(refreshTimeout); } @@ -830,9 +831,9 @@ export const ProfileItem = (props: Props) => { {fileOpen && ( readProfileFile(uid)} + dataKey={uid} language="yaml" - schema="clash" onSave={async (prev, curr) => { await saveProfileFile(uid, curr ?? ""); onSave?.(prev, curr); @@ -876,9 +877,9 @@ export const ProfileItem = (props: Props) => { {mergeOpen && ( readProfileFile(option?.merge ?? "")} + dataKey={`merge:${option?.merge ?? ""}`} language="yaml" - schema="clash" onSave={async (prev, curr) => { await saveProfileFile(option?.merge ?? "", curr ?? ""); onSave?.(prev, curr); @@ -889,7 +890,8 @@ export const ProfileItem = (props: Props) => { {scriptOpen && ( readProfileFile(option?.script ?? "")} + dataKey={`script:${option?.script ?? ""}`} language="javascript" onSave={async (prev, curr) => { await saveProfileFile(option?.script ?? "", curr ?? ""); diff --git a/clash-verge-rev/src/components/profile/profile-more.tsx b/clash-verge-rev/src/components/profile/profile-more.tsx index bcc7acd34a..10d9017ab8 100644 --- a/clash-verge-rev/src/components/profile/profile-more.tsx +++ b/clash-verge-rev/src/components/profile/profile-more.tsx @@ -181,9 +181,9 @@ export const ProfileMore = (props: Props) => { readProfileFile(id)} + dataKey={id} language={id === "Merge" ? "yaml" : "javascript"} - schema={id === "Merge" ? "clash" : undefined} onSave={async (prev, curr) => { await saveProfileFile(id, curr ?? ""); onSave?.(prev, curr); diff --git a/clash-verge-rev/src/components/proxy/provider-button.tsx b/clash-verge-rev/src/components/proxy/provider-button.tsx index 4355163860..68217775a2 100644 --- a/clash-verge-rev/src/components/proxy/provider-button.tsx +++ b/clash-verge-rev/src/components/proxy/provider-button.tsx @@ -22,7 +22,7 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { updateProxyProvider } from "tauri-plugin-mihomo-api"; -import { useAppData } from "@/providers/app-data-context"; +import { useProxiesData, useProxyProvidersData } from "@/hooks/app-data"; import { showNotice } from "@/services/noticeService"; import parseTraffic from "@/utils/parse-traffic"; @@ -48,7 +48,8 @@ const parseExpire = (expire?: number) => { export const ProviderButton = () => { const { t } = useTranslation(); const [open, setOpen] = useState(false); - const { proxyProviders, refreshProxy, refreshProxyProviders } = useAppData(); + const { proxyProviders, refreshProxyProviders } = useProxyProvidersData(); + const { refreshProxy } = useProxiesData(); const [updating, setUpdating] = useState>({}); // 检查是否有提供者 @@ -175,8 +176,8 @@ export const ProviderButton = () => { {Object.entries(proxyProviders || {}) .sort() - .map(([key, item]) => { - const provider = item; + .map(([key, provider]) => { + if (!provider) return null; const time = dayjs(provider.updatedAt); const isUpdating = updating[key]; diff --git a/clash-verge-rev/src/components/proxy/proxy-chain.tsx b/clash-verge-rev/src/components/proxy/proxy-chain.tsx index 99bf104355..b10676ce15 100644 --- a/clash-verge-rev/src/components/proxy/proxy-chain.tsx +++ b/clash-verge-rev/src/components/proxy/proxy-chain.tsx @@ -39,8 +39,9 @@ import { selectNodeForGroup, } from "tauri-plugin-mihomo-api"; -import { useAppData } from "@/providers/app-data-context"; +import { useProxiesData } from "@/hooks/app-data"; import { calcuProxies, updateProxyChainConfigInRuntime } from "@/services/cmds"; +import { debugLog } from "@/utils/debug"; interface ProxyChainItem { id: string; @@ -199,7 +200,7 @@ export const ProxyChain = ({ }: ProxyChainProps) => { const theme = useTheme(); const { t } = useTranslation(); - const { proxies } = useAppData(); + const { proxies } = useProxiesData(); const [isConnecting, setIsConnecting] = useState(false); const markUnsavedChanges = useCallback(() => { onMarkUnsavedChanges?.(); @@ -319,13 +320,13 @@ export const ProxyChain = ({ try { // 第一步:保存链式代理配置 const chainProxies = proxyChain.map((node) => node.name); - console.log("Saving chain config:", chainProxies); + debugLog("Saving chain config:", chainProxies); await updateProxyChainConfigInRuntime(chainProxies); - console.log("Chain configuration saved successfully"); + debugLog("Chain configuration saved successfully"); // 第二步:连接到代理链的最后一个节点 const lastNode = proxyChain[proxyChain.length - 1]; - console.log(`Connecting to proxy chain, last node: ${lastNode.name}`); + debugLog(`Connecting to proxy chain, last node: ${lastNode.name}`); // 根据模式确定使用的代理组名称 if (mode !== "global" && !selectedGroup) { @@ -340,7 +341,7 @@ export const ProxyChain = ({ // 刷新代理信息以更新连接状态 mutateProxies(); - console.log("Successfully connected to proxy chain"); + debugLog("Successfully connected to proxy chain"); } catch (error) { console.error("Failed to connect to proxy chain:", error); alert(t("proxies.page.chain.connectFailed") || "连接链式代理失败"); diff --git a/clash-verge-rev/src/components/proxy/proxy-groups.tsx b/clash-verge-rev/src/components/proxy/proxy-groups.tsx index 84e771c401..ab4814262d 100644 --- a/clash-verge-rev/src/components/proxy/proxy-groups.tsx +++ b/clash-verge-rev/src/components/proxy/proxy-groups.tsx @@ -15,11 +15,12 @@ import { useTranslation } from "react-i18next"; import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"; import { delayGroup, healthcheckProxyProvider } from "tauri-plugin-mihomo-api"; +import { useProxiesData } from "@/hooks/app-data"; import { useProxySelection } from "@/hooks/use-proxy-selection"; import { useVerge } from "@/hooks/use-verge"; -import { useAppData } from "@/providers/app-data-context"; import { updateProxyChainConfigInRuntime } from "@/services/cmds"; import delayManager from "@/services/delay"; +import { debugLog } from "@/utils/debug"; import { BaseEmpty } from "../base"; import { ScrollTopButton } from "../layout/scroll-top-button"; @@ -61,7 +62,7 @@ export const ProxyGroups = (props: Props) => { }>({ open: false, message: "" }); const { verge } = useVerge(); - const { proxies: proxiesData } = useAppData(); + const { proxies: proxiesData } = useProxiesData(); const groups = proxiesData?.groups; const availableGroups = useMemo(() => groups ?? [], [groups]); @@ -274,7 +275,7 @@ export const ProxyGroups = (props: Props) => { // 测全部延迟 const handleCheckAll = useLockFn(async (groupName: string) => { - console.log(`[ProxyGroups] 开始测试所有延迟,组: ${groupName}`); + debugLog(`[ProxyGroups] 开始测试所有延迟,组: ${groupName}`); const proxies = renderList .filter( @@ -283,37 +284,37 @@ export const ProxyGroups = (props: Props) => { .flatMap((e) => e.proxyCol || e.proxy!) .filter(Boolean); - console.log(`[ProxyGroups] 找到代理数量: ${proxies.length}`); + debugLog(`[ProxyGroups] 找到代理数量: ${proxies.length}`); const providers = new Set(proxies.map((p) => p!.provider!).filter(Boolean)); if (providers.size) { - console.log(`[ProxyGroups] 发现提供者,数量: ${providers.size}`); + debugLog(`[ProxyGroups] 发现提供者,数量: ${providers.size}`); Promise.allSettled( [...providers].map((p) => healthcheckProxyProvider(p)), ).then(() => { - console.log(`[ProxyGroups] 提供者健康检查完成`); + debugLog(`[ProxyGroups] 提供者健康检查完成`); onProxies(); }); } const names = proxies.filter((p) => !p!.provider).map((p) => p!.name); - console.log(`[ProxyGroups] 过滤后需要测试的代理数量: ${names.length}`); + debugLog(`[ProxyGroups] 过滤后需要测试的代理数量: ${names.length}`); const url = delayManager.getUrl(groupName); - console.log(`[ProxyGroups] 测试URL: ${url}, 超时: ${timeout}ms`); + debugLog(`[ProxyGroups] 测试URL: ${url}, 超时: ${timeout}ms`); try { await Promise.race([ delayManager.checkListDelay(names, groupName, timeout), delayGroup(groupName, url, timeout).then((result) => { - console.log( + debugLog( `[ProxyGroups] getGroupProxyDelays返回结果数量:`, Object.keys(result || {}).length, ); }), // 查询group delays 将清除fixed(不关注调用结果) ]); - console.log(`[ProxyGroups] 延迟测试完成,组: ${groupName}`); + debugLog(`[ProxyGroups] 延迟测试完成,组: ${groupName}`); } catch (error) { console.error(`[ProxyGroups] 延迟测试出错,组: ${groupName}`, error); } finally { diff --git a/clash-verge-rev/src/components/proxy/proxy-head.tsx b/clash-verge-rev/src/components/proxy/proxy-head.tsx index 56a034bd09..6dea0d7a00 100644 --- a/clash-verge-rev/src/components/proxy/proxy-head.tsx +++ b/clash-verge-rev/src/components/proxy/proxy-head.tsx @@ -17,6 +17,7 @@ import { useTranslation } from "react-i18next"; import { useVerge } from "@/hooks/use-verge"; import delayManager from "@/services/delay"; +import { debugLog } from "@/utils/debug"; import type { ProxySortType } from "./use-filter-sort"; import type { HeadState } from "./use-head-state"; @@ -78,10 +79,10 @@ export const ProxyHead = ({ color="inherit" title={t("proxies.page.tooltips.delayCheck")} onClick={() => { - console.log(`[ProxyHead] 点击延迟测试按钮,组: ${groupName}`); + debugLog(`[ProxyHead] 点击延迟测试按钮,组: ${groupName}`); // Remind the user that it is custom test url if (testUrl?.trim() && textState !== "filter") { - console.log(`[ProxyHead] 使用自定义测试URL: ${testUrl}`); + debugLog(`[ProxyHead] 使用自定义测试URL: ${testUrl}`); onHeadState({ textState: "url" }); } onCheckDelay(); diff --git a/clash-verge-rev/src/components/proxy/use-render-list.ts b/clash-verge-rev/src/components/proxy/use-render-list.ts index 7a5949ae3b..9e25c58ec2 100644 --- a/clash-verge-rev/src/components/proxy/use-render-list.ts +++ b/clash-verge-rev/src/components/proxy/use-render-list.ts @@ -1,10 +1,11 @@ import { useEffect, useMemo } from "react"; import useSWR from "swr"; +import { useProxiesData } from "@/hooks/app-data"; import { useVerge } from "@/hooks/use-verge"; -import { useAppData } from "@/providers/app-data-context"; import { getRuntimeConfig } from "@/services/cmds"; import delayManager from "@/services/delay"; +import { debugLog } from "@/utils/debug"; import { filterSort } from "./use-filter-sort"; import { @@ -33,24 +34,8 @@ interface IProxyItem { } // 代理组类型 -type ProxyGroup = { - name: string; - type: string; - udp: boolean; - xudp: boolean; - tfo: boolean; - mptcp: boolean; - smux: boolean; - history: { - time: string; - delay: number; - }[]; - now: string; - all: IProxyItem[]; - hidden?: boolean; - icon?: string; - testUrl?: string; - provider?: string; +type ProxyGroup = IProxyGroupItem & { + now?: string; }; export interface IRenderItem { @@ -99,7 +84,7 @@ export const useRenderList = ( selectedGroup?: string | null, ) => { // 使用全局数据提供者 - const { proxies: proxiesData, refreshProxy } = useAppData(); + const { proxies: proxiesData, refreshProxy } = useProxiesData(); const { verge } = useVerge(); const { width } = useWindowWidth(); const [headStates, setHeadState] = useHeadStateNew(); @@ -146,7 +131,7 @@ export const useRenderList = ( // 设置组监听器,当有延迟更新时自动刷新 const groupListener = () => { - console.log("[ChainMode] 延迟更新,刷新UI"); + debugLog("[ChainMode] 延迟更新,刷新UI"); refreshProxy(); }; @@ -157,7 +142,7 @@ export const useRenderList = ( const timeout = verge?.default_latency_timeout || 10000; const proxyNames = allProxies.map((proxy) => proxy.name); - console.log(`[ChainMode] 开始计算 ${proxyNames.length} 个节点的延迟`); + debugLog(`[ChainMode] 开始计算 ${proxyNames.length} 个节点的延迟`); // 使用 delayManager 计算延迟,每个节点计算完成后会自动触发监听器刷新界面 delayManager.checkListDelay(proxyNames, "chain-mode", timeout); diff --git a/clash-verge-rev/src/components/rule/provider-button.tsx b/clash-verge-rev/src/components/rule/provider-button.tsx index 367fa3d6ff..875be01b30 100644 --- a/clash-verge-rev/src/components/rule/provider-button.tsx +++ b/clash-verge-rev/src/components/rule/provider-button.tsx @@ -21,7 +21,7 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { updateRuleProvider } from "tauri-plugin-mihomo-api"; -import { useAppData } from "@/providers/app-data-context"; +import type { useRuleProvidersData, useRulesData } from "@/hooks/app-data"; import { showNotice } from "@/services/noticeService"; // 辅助组件 - 类型框 @@ -37,10 +37,22 @@ const TypeBox = styled(Box)<{ component?: React.ElementType }>(({ theme }) => ({ lineHeight: 1.25, })); -export const ProviderButton = () => { +type RuleProvidersHook = ReturnType; +type RulesHook = ReturnType; + +interface ProviderButtonProps { + ruleProviders: RuleProvidersHook["ruleProviders"]; + refreshRuleProviders: RuleProvidersHook["refreshRuleProviders"]; + refreshRules: RulesHook["refreshRules"]; +} + +export const ProviderButton = ({ + ruleProviders, + refreshRuleProviders, + refreshRules, +}: ProviderButtonProps) => { const { t } = useTranslation(); const [open, setOpen] = useState(false); - const { ruleProviders, refreshRules, refreshRuleProviders } = useAppData(); const [updating, setUpdating] = useState>({}); // 检查是否有提供者 @@ -163,8 +175,8 @@ export const ProviderButton = () => { {Object.entries(ruleProviders || {}) .sort() - .map(([key, item]) => { - const provider = item; + .map(([key, provider]) => { + if (!provider) return null; const time = dayjs(provider.updatedAt); const isUpdating = updating[key]; diff --git a/clash-verge-rev/src/components/setting/mods/backup-config-viewer.tsx b/clash-verge-rev/src/components/setting/mods/backup-config-viewer.tsx index 21ce89c143..5b5e98231d 100644 --- a/clash-verge-rev/src/components/setting/mods/backup-config-viewer.tsx +++ b/clash-verge-rev/src/components/setting/mods/backup-config-viewer.tsx @@ -153,6 +153,7 @@ export const BackupConfigViewer = memo( autoCapitalize="off" spellCheck="false" inputRef={urlRef} + sx={{ mt: 1 }} /> diff --git a/clash-verge-rev/src/components/setting/mods/backup-history-viewer.tsx b/clash-verge-rev/src/components/setting/mods/backup-history-viewer.tsx index 4a63fc7434..9b24ad25e6 100644 --- a/clash-verge-rev/src/components/setting/mods/backup-history-viewer.tsx +++ b/clash-verge-rev/src/components/setting/mods/backup-history-viewer.tsx @@ -265,40 +265,57 @@ export const BackupHistoryViewer = ({ ) : ( pagedRows.map((row) => ( - - {isLocal && ( - handleExport(row.filename)} - > - - - )} - handleDelete(row.filename)} - > - - - handleRestore(row.filename)} - > - - - - } - > + + {row.filename} + + } + secondary={ + + + {`${row.platform} · ${row.backup_time.format("YYYY-MM-DD HH:mm")}`} + + + {isLocal && ( + handleExport(row.filename)} + > + + + )} + handleDelete(row.filename)} + > + + + handleRestore(row.filename)} + > + + + + + } /> )) diff --git a/clash-verge-rev/src/components/setting/mods/backup-viewer.tsx b/clash-verge-rev/src/components/setting/mods/backup-viewer.tsx index b9b62b76ce..185c45d6e6 100644 --- a/clash-verge-rev/src/components/setting/mods/backup-viewer.tsx +++ b/clash-verge-rev/src/components/setting/mods/backup-viewer.tsx @@ -9,7 +9,7 @@ import { } from "@mui/material"; import { useLockFn } from "ahooks"; import type { ReactNode, Ref } from "react"; -import { useImperativeHandle, useState } from "react"; +import { useCallback, useImperativeHandle, useState } from "react"; import { useTranslation } from "react-i18next"; import { BaseDialog, DialogRef } from "@/components/base"; @@ -67,6 +67,13 @@ export function BackupViewer({ ref }: { ref?: Ref }) { } }); + const setWebdavBusy = useCallback( + (loading: boolean) => { + setBusyAction(loading ? "webdav" : null); + }, + [setBusyAction], + ); + return ( }) { open={webdavDialogOpen} onClose={() => setWebdavDialogOpen(false)} onBackupSuccess={() => openHistory("webdav")} - setBusy={(loading) => setBusyAction(loading ? "webdav" : null)} + setBusy={setWebdavBusy} /> ); diff --git a/clash-verge-rev/src/components/setting/mods/config-viewer.tsx b/clash-verge-rev/src/components/setting/mods/config-viewer.tsx index 564a56dcc5..f71d809e73 100644 --- a/clash-verge-rev/src/components/setting/mods/config-viewer.tsx +++ b/clash-verge-rev/src/components/setting/mods/config-viewer.tsx @@ -31,10 +31,10 @@ export const ConfigViewer = forwardRef((_, ref) => { } - initialData={Promise.resolve(runtimeConfig)} + initialData={() => Promise.resolve(runtimeConfig)} + dataKey="runtime-config" readOnly language="yaml" - schema="clash" onClose={() => setOpen(false)} /> ); diff --git a/clash-verge-rev/src/components/setting/mods/dns-viewer.tsx b/clash-verge-rev/src/components/setting/mods/dns-viewer.tsx index 0b0f520d2a..b1271226bd 100644 --- a/clash-verge-rev/src/components/setting/mods/dns-viewer.tsx +++ b/clash-verge-rev/src/components/setting/mods/dns-viewer.tsx @@ -31,6 +31,7 @@ import { BaseDialog, DialogRef, Switch } from "@/components/base"; import { useClash } from "@/hooks/use-clash"; import { showNotice } from "@/services/noticeService"; import { useThemeMode } from "@/services/states"; +import { debugLog } from "@/utils/debug"; import getSystem from "@/utils/get-system"; const Item = styled(ListItem)(() => ({ @@ -580,7 +581,7 @@ export function DnsViewer({ ref }: { ref?: Ref }) { }, 300); } } catch (err) { - console.log("YAML解析错误,忽略自动更新", err); + debugLog("YAML解析错误,忽略自动更新", err); } }; diff --git a/clash-verge-rev/src/components/setting/mods/guard-state.tsx b/clash-verge-rev/src/components/setting/mods/guard-state.tsx index 4889749da4..ffd6a0a0c0 100644 --- a/clash-verge-rev/src/components/setting/mods/guard-state.tsx +++ b/clash-verge-rev/src/components/setting/mods/guard-state.tsx @@ -60,6 +60,7 @@ export function GuardState(props: Props) { if (waitTime <= 0) { await onGuard(newValue, value!); + lockRef.current = false; } else { // debounce guard clearTimeout(timeRef.current); @@ -71,6 +72,8 @@ export function GuardState(props: Props) { // 状态回退 onChange(saveRef.current!); onCatch(err); + } finally { + lockRef.current = false; } }, waitTime); } @@ -78,8 +81,8 @@ export function GuardState(props: Props) { // 状态回退 onChange(saveRef.current!); onCatch(err); + lockRef.current = false; } - lockRef.current = false; }; const { children: nestedChildren, ...restProps } = childProps; diff --git a/clash-verge-rev/src/components/setting/mods/hotkey-viewer.tsx b/clash-verge-rev/src/components/setting/mods/hotkey-viewer.tsx index 574abea4b5..8052bba1c7 100644 --- a/clash-verge-rev/src/components/setting/mods/hotkey-viewer.tsx +++ b/clash-verge-rev/src/components/setting/mods/hotkey-viewer.tsx @@ -24,6 +24,7 @@ const HOTKEY_FUNC = [ "toggle_system_proxy", "toggle_tun_mode", "entry_lightweight_mode", + "reactivate_profiles", ] as const; const HOTKEY_FUNC_LABELS: Record<(typeof HOTKEY_FUNC)[number], string> = { @@ -36,6 +37,7 @@ const HOTKEY_FUNC_LABELS: Record<(typeof HOTKEY_FUNC)[number], string> = { toggle_tun_mode: "settings.modals.hotkey.functions.toggleTunMode", entry_lightweight_mode: "settings.modals.hotkey.functions.entryLightweightMode", + reactivate_profiles: "settings.modals.hotkey.functions.reactivateProfiles", }; export const HotkeyViewer = forwardRef((props, ref) => { diff --git a/clash-verge-rev/src/components/setting/mods/sysproxy-viewer.tsx b/clash-verge-rev/src/components/setting/mods/sysproxy-viewer.tsx index 3d2b0f37ad..4b7a4bd020 100644 --- a/clash-verge-rev/src/components/setting/mods/sysproxy-viewer.tsx +++ b/clash-verge-rev/src/components/setting/mods/sysproxy-viewer.tsx @@ -20,15 +20,18 @@ import { useState, } from "react"; import { useTranslation } from "react-i18next"; -import useSWR, { mutate } from "swr"; -import { getBaseConfig } from "tauri-plugin-mihomo-api"; +import { mutate } from "swr"; import { BaseDialog, DialogRef, Switch } from "@/components/base"; import { BaseFieldset } from "@/components/base/base-fieldset"; import { TooltipIcon } from "@/components/base/base-tooltip-icon"; import { EditorViewer } from "@/components/profile/editor-viewer"; +import { + useClashConfig, + useSystemProxyAddress, + useSystemProxyData, +} from "@/hooks/app-data"; import { useVerge } from "@/hooks/use-verge"; -import { useAppData } from "@/providers/app-data-context"; import { getAutotemProxy, getNetworkInterfacesInfo, @@ -37,6 +40,7 @@ import { patchVergeConfig, } from "@/services/cmds"; import { showNotice } from "@/services/noticeService"; +import { debugLog } from "@/utils/debug"; import getSystem from "@/utils/get-system"; const sleep = (ms: number) => @@ -92,9 +96,6 @@ export const SysproxyViewer = forwardRef((props, ref) => { const { verge, patchVerge, mutateVerge } = useVerge(); const [hostOptions, setHostOptions] = useState([]); - type SysProxy = Awaited>; - const [sysproxy, setSysproxy] = useState(); - type AutoProxy = Awaited>; const [autoproxy, setAutoproxy] = useState(); @@ -124,17 +125,13 @@ export const SysproxyViewer = forwardRef((props, ref) => { return "localhost;127.*;192.168.*;10.*;172.16.*;172.17.*;172.18.*;172.19.*;172.20.*;172.21.*;172.22.*;172.23.*;172.24.*;172.25.*;172.26.*;172.27.*;172.28.*;172.29.*;172.30.*;172.31.*;"; } if (getSystem() === "linux") { - return "localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,::1"; + return "localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,::1"; } - return "127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,localhost,*.local,*.crashlytics.com,"; + return "127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,localhost,*.local,*.crashlytics.com,"; }; - const { data: clashConfig } = useSWR("getClashConfig", getBaseConfig, { - revalidateOnFocus: false, - revalidateIfStale: true, - dedupingInterval: 1000, - errorRetryInterval: 5000, - }); + const { clashConfig } = useClashConfig(); + const { sysproxy, refreshSysproxy } = useSystemProxyData(); const prevMixedPortRef = useRef(clashConfig?.mixedPort); @@ -168,7 +165,10 @@ export const SysproxyViewer = forwardRef((props, ref) => { updateProxy(); }, [clashConfig?.mixedPort, value.pac]); - const { systemProxyAddress } = useAppData(); + const systemProxyAddress = useSystemProxyAddress({ + clashConfig, + sysproxy, + }); // 为当前状态计算系统代理地址 const getSystemProxyAddress = useMemo(() => { @@ -209,7 +209,7 @@ export const SysproxyViewer = forwardRef((props, ref) => { pac_content: pac_file_content ?? DEFAULT_PAC, proxy_host: proxy_host ?? "127.0.0.1", }); - getSystemProxy().then((p) => setSysproxy(p)); + void refreshSysproxy(); getAutotemProxy().then((p) => setAutoproxy(p)); fetchNetworkInterfaces(); }, @@ -239,7 +239,7 @@ export const SysproxyViewer = forwardRef((props, ref) => { let hostname = ""; try { hostname = await getSystemHostname(); - console.log("获取到主机名:", hostname); + debugLog("获取到主机名:", hostname); } catch (err) { console.error("获取主机名失败:", err); } @@ -253,12 +253,12 @@ export const SysproxyViewer = forwardRef((props, ref) => { if (hostname !== "localhost" && hostname !== "127.0.0.1") { hostname = hostname + ".local"; options.push(hostname); - console.log("主机名已添加到选项中:", hostname); + debugLog("主机名已添加到选项中:", hostname); } else { - console.log("主机名与已有选项重复:", hostname); + debugLog("主机名与已有选项重复:", hostname); } } else { - console.log("主机名为空"); + debugLog("主机名为空"); } // 添加IP地址 @@ -266,7 +266,7 @@ export const SysproxyViewer = forwardRef((props, ref) => { // 去重 const uniqueOptions = Array.from(new Set(options)); - console.log("最终选项列表:", uniqueOptions); + debugLog("最终选项列表:", uniqueOptions); setHostOptions(uniqueOptions); } catch (error) { console.error("获取网络接口失败:", error); @@ -617,7 +617,8 @@ export const SysproxyViewer = forwardRef((props, ref) => { Promise.resolve(value.pac_content ?? "")} + dataKey="sysproxy-pac" language="javascript" onSave={(_prev, curr) => { let pac = DEFAULT_PAC; diff --git a/clash-verge-rev/src/components/setting/mods/theme-viewer.tsx b/clash-verge-rev/src/components/setting/mods/theme-viewer.tsx index b9f52a8007..0f07f4ccc7 100644 --- a/clash-verge-rev/src/components/setting/mods/theme-viewer.tsx +++ b/clash-verge-rev/src/components/setting/mods/theme-viewer.tsx @@ -9,7 +9,14 @@ import { useTheme, } from "@mui/material"; import { useLockFn } from "ahooks"; -import { useImperativeHandle, useMemo, useState } from "react"; +import { + useEffect, + useImperativeHandle, + useMemo, + useRef, + useState, + useCallback, +} from "react"; import { useTranslation } from "react-i18next"; import { BaseDialog, DialogRef } from "@/components/base"; @@ -27,6 +34,11 @@ export function ThemeViewer(props: { ref?: React.Ref }) { const { verge, patchVerge } = useVerge(); const { theme_setting } = verge ?? {}; const [theme, setTheme] = useState(theme_setting || {}); + // Latest theme ref to avoid stale closures when saving CSS + const themeRef = useRef(theme); + useEffect(() => { + themeRef.current = theme; + }, [theme]); useImperativeHandle(ref, () => ({ open: () => { @@ -55,7 +67,6 @@ export function ThemeViewer(props: { ref?: React.Ref }) { } }); - // default theme const { palette } = useTheme(); const dt = palette.mode === "light" ? defaultTheme : defaultDarkTheme; @@ -100,6 +111,13 @@ export function ThemeViewer(props: { ref?: React.Ref }) { [], ); + // Stable loader that returns a fresh Promise each call so EditorViewer + // can retry/refresh and always read the latest staged CSS from state. + const loadCss = useCallback( + () => Promise.resolve(themeRef.current?.css_injection ?? ""), + [], + ); + const renderItem = (labelKey: string, key: ThemeKey) => { const label = t(labelKey); return ( @@ -159,11 +177,15 @@ export function ThemeViewer(props: { ref?: React.Ref }) { { - theme.css_injection = curr; - handleChange("css_injection"); + onSave={async (_prev, curr) => { + // Only stage the CSS change locally. Persistence happens + // when the outer Theme dialog's Save button is pressed. + const prevTheme = themeRef.current || {}; + const nextCss = curr ?? ""; + setTheme({ ...prevTheme, css_injection: nextCss }); }} onClose={() => { setEditorOpen(false); diff --git a/clash-verge-rev/src/components/setting/mods/tun-viewer.tsx b/clash-verge-rev/src/components/setting/mods/tun-viewer.tsx index 98aab374dc..02bc984b17 100644 --- a/clash-verge-rev/src/components/setting/mods/tun-viewer.tsx +++ b/clash-verge-rev/src/components/setting/mods/tun-viewer.tsx @@ -13,6 +13,7 @@ import { useImperativeHandle, useState } from "react"; import { useTranslation } from "react-i18next"; import { BaseDialog, DialogRef, Switch } from "@/components/base"; +import { TooltipIcon } from "@/components/base/base-tooltip-icon"; import { useClash } from "@/hooks/use-clash"; import { enhanceProfiles } from "@/services/cmds"; import { showNotice } from "@/services/noticeService"; @@ -32,6 +33,7 @@ export function TunViewer({ ref }: { ref?: Ref }) { stack: "mixed", device: OS === "macos" ? "utun1024" : "Mihomo", autoRoute: true, + autoRedirect: OS === "linux", autoDetectInterface: true, dnsHijack: ["any:53"], strictRoute: false, @@ -41,10 +43,15 @@ export function TunViewer({ ref }: { ref?: Ref }) { useImperativeHandle(ref, () => ({ open: () => { setOpen(true); + const nextAutoRoute = clash?.tun["auto-route"] ?? true; + const rawAutoRedirect = clash?.tun["auto-redirect"] ?? true; + const computedAutoRedirect = + OS === "linux" ? (nextAutoRoute ? rawAutoRedirect : false) : false; setValues({ stack: clash?.tun.stack ?? "gvisor", device: clash?.tun.device ?? (OS === "macos" ? "utun1024" : "Mihomo"), - autoRoute: clash?.tun["auto-route"] ?? true, + autoRoute: nextAutoRoute, + autoRedirect: computedAutoRedirect, autoDetectInterface: clash?.tun["auto-detect-interface"] ?? true, dnsHijack: clash?.tun["dns-hijack"] ?? ["any:53"], strictRoute: clash?.tun["strict-route"] ?? false, @@ -56,7 +63,7 @@ export function TunViewer({ ref }: { ref?: Ref }) { const onSave = useLockFn(async () => { try { - const tun = { + const tun: IConfigData["tun"] = { stack: values.stack, device: values.device === "" @@ -65,6 +72,11 @@ export function TunViewer({ ref }: { ref?: Ref }) { : "Mihomo" : values.device, "auto-route": values.autoRoute, + ...(OS === "linux" + ? { + "auto-redirect": values.autoRedirect, + } + : {}), "auto-detect-interface": values.autoDetectInterface, "dns-hijack": values.dnsHijack[0] === "" ? [] : values.dnsHijack, "strict-route": values.strictRoute, @@ -100,10 +112,15 @@ export function TunViewer({ ref }: { ref?: Ref }) { variant="outlined" size="small" onClick={async () => { - const tun = { + const tun: IConfigData["tun"] = { stack: "gvisor", device: OS === "macos" ? "utun1024" : "Mihomo", "auto-route": true, + ...(OS === "linux" + ? { + "auto-redirect": true, + } + : {}), "auto-detect-interface": true, "dns-hijack": ["any:53"], "strict-route": false, @@ -113,6 +130,7 @@ export function TunViewer({ ref }: { ref?: Ref }) { stack: "gvisor", device: OS === "macos" ? "utun1024" : "Mihomo", autoRoute: true, + autoRedirect: OS === "linux" ? true : false, autoDetectInterface: true, dnsHijack: ["any:53"], strictRoute: false, @@ -175,10 +193,41 @@ export function TunViewer({ ref }: { ref?: Ref }) { setValues((v) => ({ ...v, autoRoute: c }))} + onChange={(_, c) => + setValues((v) => ({ + ...v, + autoRoute: c, + autoRedirect: c ? v.autoRedirect : false, + })) + } /> + {OS === "linux" && ( + + + + + setValues((v) => ({ + ...v, + autoRedirect: v.autoRoute ? c : v.autoRedirect, + })) + } + disabled={!values.autoRoute} + sx={{ marginLeft: "auto" }} + /> + + )} + }) { const { t } = useTranslation(); const [open, setOpen] = useState(false); - const [currentProgressListener, setCurrentProgressListener] = - useState(null); - const updateState = useUpdateState(); const setUpdateState = useSetUpdateState(); - const { addListener } = useListen(); const { data: updateInfo } = useSWR("checkUpdate", checkUpdate, { errorRetryCount: 2, @@ -34,8 +29,14 @@ export function UpdateViewer({ ref }: { ref?: Ref }) { }); const [downloaded, setDownloaded] = useState(0); - const [buffer, setBuffer] = useState(0); const [total, setTotal] = useState(0); + const downloadedRef = useRef(0); + const totalRef = useRef(0); + + const progress = useMemo(() => { + if (total <= 0) return 0; + return Math.min((downloaded / total) * 100, 100); + }, [downloaded, total]); useImperativeHandle(ref, () => ({ open: () => setOpen(true), @@ -68,46 +69,49 @@ export function UpdateViewer({ ref }: { ref?: Ref }) { } if (updateState) return; setUpdateState(true); + setDownloaded(0); + setTotal(0); + downloadedRef.current = 0; + totalRef.current = 0; - if (currentProgressListener) { - currentProgressListener(); - } + const onDownloadEvent = (event: DownloadEvent) => { + if (event.event === "Started") { + const contentLength = event.data.contentLength ?? 0; + totalRef.current = contentLength; + setTotal(contentLength); + setDownloaded(0); + downloadedRef.current = 0; + return; + } - const progressListener = await addListener( - "tauri://update-download-progress", - (e: Event) => { - setTotal(e.payload.contentLength); - setBuffer(e.payload.chunkLength); - setDownloaded((a) => { - return a + e.payload.chunkLength; + if (event.event === "Progress") { + setDownloaded((prev) => { + const next = prev + event.data.chunkLength; + downloadedRef.current = next; + return next; }); - }, - ); - setCurrentProgressListener(() => progressListener); + } + + if (event.event === "Finished" && totalRef.current === 0) { + totalRef.current = downloadedRef.current; + setTotal(downloadedRef.current); + } + }; try { - await updateInfo.downloadAndInstall(); + await updateInfo.downloadAndInstall(onDownloadEvent); await relaunch(); } catch (err: any) { showNotice.error(err); } finally { setUpdateState(false); - if (progressListener) { - progressListener(); - } - setCurrentProgressListener(null); + setDownloaded(0); + setTotal(0); + downloadedRef.current = 0; + totalRef.current = 0; } }); - useEffect(() => { - return () => { - if (currentProgressListener) { - console.log("UpdateViewer unmounting, cleaning up progress listener."); - currentProgressListener(); - } - }; - }, [currentProgressListener]); - return ( }) { {updateState && ( 0 ? "determinate" : "indeterminate"} + value={progress} sx={{ marginTop: "5px" }} /> )} diff --git a/clash-verge-rev/src/components/shared/ProxyControlSwitches.tsx b/clash-verge-rev/src/components/shared/ProxyControlSwitches.tsx index c26eeb41a8..79466dfb6c 100644 --- a/clash-verge-rev/src/components/shared/ProxyControlSwitches.tsx +++ b/clash-verge-rev/src/components/shared/ProxyControlSwitches.tsx @@ -123,7 +123,7 @@ const ProxyControlSwitches = ({ const sysproxyRef = useRef(null); const tunRef = useRef(null); - const { enable_tun_mode, enable_system_proxy } = verge ?? {}; + const { enable_tun_mode } = verge ?? {}; const showErrorNotice = useCallback( (msg: string) => showNotice.error(msg), @@ -175,7 +175,7 @@ const ProxyControlSwitches = ({ onInfoClick={() => sysproxyRef.current?.open()} onToggle={(value) => toggleSystemProxy(value)} onError={onError} - highlight={enable_system_proxy} + highlight={systemProxyActualState} /> )} diff --git a/clash-verge-rev/src/components/test/test-item.tsx b/clash-verge-rev/src/components/test/test-item.tsx index 738c41c026..63c181ced9 100644 --- a/clash-verge-rev/src/components/test/test-item.tsx +++ b/clash-verge-rev/src/components/test/test-item.tsx @@ -13,6 +13,7 @@ import { useListen } from "@/hooks/use-listen"; import { cmdTestDelay, downloadIconCache } from "@/services/cmds"; import delayManager from "@/services/delay"; import { showNotice } from "@/services/noticeService"; +import { debugLog } from "@/utils/debug"; import { TestBox } from "./test-box"; @@ -107,7 +108,7 @@ export const TestItem = ({ return () => { if (unlistenFn) { - console.log( + debugLog( `TestItem for ${id} unmounting or url changed, cleaning up test-all listener.`, ); unlistenFn(); diff --git a/clash-verge-rev/src/hooks/app-data.ts b/clash-verge-rev/src/hooks/app-data.ts new file mode 100644 index 0000000000..cfd1130581 --- /dev/null +++ b/clash-verge-rev/src/hooks/app-data.ts @@ -0,0 +1,206 @@ +import { useCallback, useMemo } from "react"; +import useSWR, { useSWRConfig } from "swr"; +import { + getBaseConfig, + getRuleProviders, + getRules, +} from "tauri-plugin-mihomo-api"; + +import { + calcuProxies, + calcuProxyProviders, + getAppUptime, + getSystemProxy, +} from "@/services/cmds"; +import { SWR_DEFAULTS, SWR_REALTIME, SWR_SLOW_POLL } from "@/services/config"; + +import { useSharedSWRPoller } from "./use-shared-swr-poller"; +import { useVerge } from "./use-verge"; + +export const useProxiesData = () => { + const { mutate: globalMutate } = useSWRConfig(); + const { data, error, isLoading } = useSWR("getProxies", calcuProxies, { + ...SWR_REALTIME, + refreshInterval: 0, + onError: (err) => console.warn("[AppData] Proxy fetch failed:", err), + }); + + const refreshProxy = useCallback( + () => globalMutate("getProxies"), + [globalMutate], + ); + const pollerRefresh = useCallback(() => { + void globalMutate("getProxies"); + }, [globalMutate]); + + useSharedSWRPoller("getProxies", SWR_REALTIME.refreshInterval, pollerRefresh); + + return { + proxies: data, + refreshProxy, + isLoading, + error, + }; +}; + +export const useClashConfig = () => { + const { mutate: globalMutate } = useSWRConfig(); + const { data, error, isLoading } = useSWR("getClashConfig", getBaseConfig, { + ...SWR_SLOW_POLL, + refreshInterval: 0, + }); + + const refreshClashConfig = useCallback( + () => globalMutate("getClashConfig"), + [globalMutate], + ); + const pollerRefresh = useCallback(() => { + void globalMutate("getClashConfig"); + }, [globalMutate]); + + useSharedSWRPoller( + "getClashConfig", + SWR_SLOW_POLL.refreshInterval, + pollerRefresh, + ); + + return { + clashConfig: data, + refreshClashConfig, + isLoading, + error, + }; +}; + +export const useProxyProvidersData = () => { + const { data, error, isLoading, mutate } = useSWR( + "getProxyProviders", + calcuProxyProviders, + SWR_DEFAULTS, + ); + + const refreshProxyProviders = useCallback(() => mutate(), [mutate]); + + return { + proxyProviders: data || {}, + refreshProxyProviders, + isLoading, + error, + }; +}; + +export const useRuleProvidersData = () => { + const { data, error, isLoading, mutate } = useSWR( + "getRuleProviders", + getRuleProviders, + SWR_DEFAULTS, + ); + + const refreshRuleProviders = useCallback(() => mutate(), [mutate]); + + return { + ruleProviders: data?.providers || {}, + refreshRuleProviders, + isLoading, + error, + }; +}; + +export const useRulesData = () => { + const { data, error, isLoading, mutate } = useSWR( + "getRules", + getRules, + SWR_DEFAULTS, + ); + + const refreshRules = useCallback(() => mutate(), [mutate]); + + return { + rules: data?.rules || [], + refreshRules, + isLoading, + error, + }; +}; + +export const useSystemProxyData = () => { + const { data, error, isLoading, mutate } = useSWR( + "getSystemProxy", + getSystemProxy, + SWR_DEFAULTS, + ); + + const refreshSysproxy = useCallback(() => mutate(), [mutate]); + + return { + sysproxy: data, + refreshSysproxy, + isLoading, + error, + }; +}; + +type ClashConfig = Awaited>; +type SystemProxy = Awaited>; + +interface SystemProxyAddressParams { + clashConfig?: ClashConfig | null; + sysproxy?: SystemProxy | null; +} + +export const useSystemProxyAddress = ({ + clashConfig, + sysproxy, +}: SystemProxyAddressParams) => { + const { verge } = useVerge(); + + return useMemo(() => { + if (!verge || !clashConfig) return "-"; + + const isPacMode = verge.proxy_auto_config ?? false; + + if (isPacMode) { + const proxyHost = verge.proxy_host || "127.0.0.1"; + const proxyPort = verge.verge_mixed_port || clashConfig.mixedPort || 7897; + return [proxyHost, proxyPort].join(":"); + } + + const systemServer = sysproxy?.server; + if (systemServer && systemServer !== "-" && !systemServer.startsWith(":")) { + return systemServer; + } + + const proxyHost = verge.proxy_host || "127.0.0.1"; + const proxyPort = verge.verge_mixed_port || clashConfig.mixedPort || 7897; + return [proxyHost, proxyPort].join(":"); + }, [clashConfig, sysproxy, verge]); +}; + +export const useAppUptime = () => { + const { data, error, isLoading } = useSWR("appUptime", getAppUptime, { + ...SWR_DEFAULTS, + refreshInterval: 3000, + errorRetryCount: 1, + }); + + return { + uptime: data || 0, + error, + isLoading, + }; +}; + +export const useRefreshAll = () => { + const { mutate } = useSWRConfig(); + + return useCallback(async () => { + await Promise.all([ + mutate("getProxies"), + mutate("getClashConfig"), + mutate("getRules"), + mutate("getSystemProxy"), + mutate("getProxyProviders"), + mutate("getRuleProviders"), + ]); + }, [mutate]); +}; diff --git a/clash-verge-rev/src/hooks/use-current-proxy.ts b/clash-verge-rev/src/hooks/use-current-proxy.ts index 7d35232692..e1d1ab1bab 100644 --- a/clash-verge-rev/src/hooks/use-current-proxy.ts +++ b/clash-verge-rev/src/hooks/use-current-proxy.ts @@ -1,17 +1,11 @@ import { useMemo } from "react"; -import { useAppData } from "@/providers/app-data-context"; - -// 定义代理组类型 -interface ProxyGroup { - name: string; - now: string; -} +import { useClashConfig, useProxiesData } from "@/hooks/app-data"; // 获取当前代理节点信息的自定义Hook export const useCurrentProxy = () => { - // 从AppDataProvider获取数据 - const { proxies, clashConfig, refreshProxy } = useAppData(); + const { proxies, refreshProxy } = useProxiesData(); + const { clashConfig } = useClashConfig(); // 获取当前模式 const currentMode = clashConfig?.mode?.toLowerCase() || "rule"; @@ -20,11 +14,15 @@ export const useCurrentProxy = () => { const currentProxyInfo = useMemo(() => { if (!proxies) return { currentProxy: null, primaryGroupName: null }; - const { global, groups, records } = proxies; + const globalGroup = proxies.global as IProxyGroupItem | undefined; + const groups: IProxyGroupItem[] = Array.isArray(proxies.groups) + ? (proxies.groups as IProxyGroupItem[]) + : []; + const records = (proxies.records || {}) as Record; // 默认信息 let primaryGroupName = "GLOBAL"; - let currentName = global?.now; + let currentName = globalGroup?.now; // 在规则模式下,寻找主要代理组(通常是第一个或者名字包含特定关键词的组) if (currentMode === "rule" && groups.length > 0) { @@ -37,11 +35,11 @@ export const useCurrentProxy = () => { "自动选择", ]; const primaryGroup = - groups.find((group: ProxyGroup) => + groups.find((group) => primaryKeywords.some((keyword) => group.name.toLowerCase().includes(keyword.toLowerCase()), ), - ) || groups.filter((g: ProxyGroup) => g.name !== "GLOBAL")[0]; + ) || groups.filter((g) => g.name !== "GLOBAL")[0]; if (primaryGroup) { primaryGroupName = primaryGroup.name; diff --git a/clash-verge-rev/src/hooks/use-i18n.ts b/clash-verge-rev/src/hooks/use-i18n.ts index c82b05ffaa..ca04ab1707 100644 --- a/clash-verge-rev/src/hooks/use-i18n.ts +++ b/clash-verge-rev/src/hooks/use-i18n.ts @@ -1,7 +1,11 @@ import { useState, useCallback } from "react"; import { useTranslation } from "react-i18next"; -import { changeLanguage, supportedLanguages } from "@/services/i18n"; +import { + changeLanguage, + resolveLanguage, + supportedLanguages, +} from "@/services/i18n"; import { useVerge } from "./use-verge"; @@ -12,21 +16,23 @@ export const useI18n = () => { const switchLanguage = useCallback( async (language: string) => { - if (!supportedLanguages.includes(language)) { + const targetLanguage = resolveLanguage(language); + + if (!supportedLanguages.includes(targetLanguage)) { console.warn(`Unsupported language: ${language}`); return; } - if (i18n.language === language) { + if (i18n.language === targetLanguage) { return; } setIsLoading(true); try { - await changeLanguage(language); + await changeLanguage(targetLanguage); if (patchVerge) { - await patchVerge({ language }); + await patchVerge({ language: targetLanguage }); } } catch (error) { console.error("Failed to change language:", error); diff --git a/clash-verge-rev/src/hooks/use-profiles.ts b/clash-verge-rev/src/hooks/use-profiles.ts index c517c8352d..76db32faac 100644 --- a/clash-verge-rev/src/hooks/use-profiles.ts +++ b/clash-verge-rev/src/hooks/use-profiles.ts @@ -7,6 +7,7 @@ import { patchProfilesConfig, } from "@/services/cmds"; import { calcuProxies } from "@/services/cmds"; +import { debugLog } from "@/utils/debug"; export const useProfiles = () => { const { @@ -25,7 +26,7 @@ export const useProfiles = () => { console.error("[useProfiles] SWR错误:", error); }, onSuccess: (data) => { - console.log( + debugLog( "[useProfiles] 配置数据更新成功,配置数量:", data?.items?.length || 0, ); @@ -71,7 +72,7 @@ export const useProfiles = () => { // 根据selected的节点选择 const activateSelected = async () => { try { - console.log("[ActivateSelected] 开始处理代理选择"); + debugLog("[ActivateSelected] 开始处理代理选择"); const [proxiesData, profileData] = await Promise.all([ calcuProxies(), @@ -79,7 +80,7 @@ export const useProfiles = () => { ]); if (!profileData || !proxiesData) { - console.log("[ActivateSelected] 代理或配置数据不可用,跳过处理"); + debugLog("[ActivateSelected] 代理或配置数据不可用,跳过处理"); return; } @@ -88,18 +89,18 @@ export const useProfiles = () => { ); if (!current) { - console.log("[ActivateSelected] 未找到当前profile配置"); + debugLog("[ActivateSelected] 未找到当前profile配置"); return; } // 检查是否有saved的代理选择 const { selected = [] } = current; if (selected.length === 0) { - console.log("[ActivateSelected] 当前profile无保存的代理选择,跳过"); + debugLog("[ActivateSelected] 当前profile无保存的代理选择,跳过"); return; } - console.log( + debugLog( `[ActivateSelected] 当前profile有 ${selected.length} 个代理选择配置`, ); @@ -160,7 +161,7 @@ export const useProfiles = () => { } if (savedProxy !== now) { - console.log( + debugLog( `[ActivateSelected] 需要切换代理组 ${name}: ${now} -> ${savedProxy}`, ); hasChange = true; @@ -171,15 +172,15 @@ export const useProfiles = () => { }); if (!hasChange) { - console.log("[ActivateSelected] 所有代理选择已经是目标状态,无需更新"); + debugLog("[ActivateSelected] 所有代理选择已经是目标状态,无需更新"); return; } - console.log(`[ActivateSelected] 完成代理切换,保存新的选择配置`); + debugLog(`[ActivateSelected] 完成代理切换,保存新的选择配置`); try { await patchProfile(profileData.current!, { selected: newSelected }); - console.log("[ActivateSelected] 代理选择配置保存成功"); + debugLog("[ActivateSelected] 代理选择配置保存成功"); setTimeout(() => { mutate("getProxies", calcuProxies()); diff --git a/clash-verge-rev/src/hooks/use-proxy-selection.ts b/clash-verge-rev/src/hooks/use-proxy-selection.ts index 8d2a2df421..4cfb6a9606 100644 --- a/clash-verge-rev/src/hooks/use-proxy-selection.ts +++ b/clash-verge-rev/src/hooks/use-proxy-selection.ts @@ -9,6 +9,7 @@ import { import { useProfiles } from "@/hooks/use-profiles"; import { useVerge } from "@/hooks/use-verge"; import { syncTrayProxySelection } from "@/services/cmds"; +import { debugLog } from "@/utils/debug"; // 缓存连接清理 const cleanupConnections = async (previousProxy: string) => { @@ -20,7 +21,7 @@ const cleanupConnections = async (previousProxy: string) => { if (cleanupPromises.length > 0) { await Promise.allSettled(cleanupPromises); - console.log(`[ProxySelection] 清理了 ${cleanupPromises.length} 个连接`); + debugLog(`[ProxySelection] 清理了 ${cleanupPromises.length} 个连接`); } } catch (error) { console.warn("[ProxySelection] 连接清理失败:", error); @@ -57,7 +58,7 @@ export const useProxySelection = (options: ProxySelectionOptions = {}) => { previousProxy?: string, skipConfigSave: boolean = false, ) => { - console.log(`[ProxySelection] 代理切换: ${groupName} -> ${proxyName}`); + debugLog(`[ProxySelection] 代理切换: ${groupName} -> ${proxyName}`); try { if (current && !skipConfigSave) { @@ -77,7 +78,7 @@ export const useProxySelection = (options: ProxySelectionOptions = {}) => { await selectNodeForGroup(groupName, proxyName); await syncTrayProxySelection(); - console.log( + debugLog( `[ProxySelection] 代理和状态同步完成: ${groupName} -> ${proxyName}`, ); @@ -100,7 +101,7 @@ export const useProxySelection = (options: ProxySelectionOptions = {}) => { await selectNodeForGroup(groupName, proxyName); await syncTrayProxySelection(); onSuccess?.(); - console.log( + debugLog( `[ProxySelection] 代理切换回退成功: ${groupName} -> ${proxyName}`, ); } catch (fallbackError) { diff --git a/clash-verge-rev/src/hooks/use-shared-swr-poller.ts b/clash-verge-rev/src/hooks/use-shared-swr-poller.ts new file mode 100644 index 0000000000..d5399ff371 --- /dev/null +++ b/clash-verge-rev/src/hooks/use-shared-swr-poller.ts @@ -0,0 +1,121 @@ +import { useEffect } from "react"; +import type { Key } from "swr"; + +type SharedPollerEntry = { + subscribers: number; + timer: number | null; + interval: number; + callback: (() => void) | null; + refreshWhenHidden: boolean; + refreshWhenOffline: boolean; +}; + +const sharedPollers = new Map(); + +const isDocumentHidden = () => { + if (typeof document === "undefined") return false; + return document.visibilityState === "hidden"; +}; + +const isOffline = () => { + if (typeof navigator === "undefined") return false; + return navigator.onLine === false; +}; + +const ensureTimer = (key: string, entry: SharedPollerEntry) => { + if (typeof window === "undefined") return; + + if (entry.timer !== null) { + clearInterval(entry.timer); + } + + entry.timer = window.setInterval(() => { + if (!entry.refreshWhenHidden && isDocumentHidden()) return; + if (!entry.refreshWhenOffline && isOffline()) return; + entry.callback?.(); + }, entry.interval); +}; + +const registerSharedPoller = ( + key: string, + interval: number, + callback: () => void, + options: { refreshWhenHidden: boolean; refreshWhenOffline: boolean }, +) => { + let entry = sharedPollers.get(key); + + if (!entry) { + entry = { + subscribers: 0, + timer: null, + interval, + callback, + refreshWhenHidden: options.refreshWhenHidden, + refreshWhenOffline: options.refreshWhenOffline, + }; + sharedPollers.set(key, entry); + } + + entry.subscribers += 1; + entry.callback = callback; + entry.interval = Math.min(entry.interval, interval); + entry.refreshWhenHidden = + entry.refreshWhenHidden || options.refreshWhenHidden; + entry.refreshWhenOffline = + entry.refreshWhenOffline || options.refreshWhenOffline; + + ensureTimer(key, entry); + + return () => { + const current = sharedPollers.get(key); + if (!current) return; + + current.subscribers -= 1; + if (current.subscribers <= 0) { + if (current.timer !== null) { + clearInterval(current.timer); + } + sharedPollers.delete(key); + } + }; +}; + +const normalizeKey = (key: Key): string | null => { + if (typeof key === "string") return key; + if (typeof key === "number" || typeof key === "boolean") return String(key); + if (Array.isArray(key)) { + try { + return JSON.stringify(key); + } catch { + return null; + } + } + return null; +}; + +export interface SharedSWRPollerOptions { + refreshWhenHidden?: boolean; + refreshWhenOffline?: boolean; +} + +export const useSharedSWRPoller = ( + key: Key, + interval?: number, + callback?: () => void, + options?: SharedSWRPollerOptions, +) => { + const refreshWhenHidden = options?.refreshWhenHidden ?? false; + const refreshWhenOffline = options?.refreshWhenOffline ?? false; + + useEffect(() => { + if (!key || !interval || interval <= 0 || !callback) return; + + const serializedKey = normalizeKey(key); + if (!serializedKey) return; + + return registerSharedPoller(serializedKey, interval, callback, { + refreshWhenHidden, + refreshWhenOffline, + }); + }, [key, interval, callback, refreshWhenHidden, refreshWhenOffline]); +}; diff --git a/clash-verge-rev/src/hooks/use-system-proxy-state.ts b/clash-verge-rev/src/hooks/use-system-proxy-state.ts index 29c028e8b3..ef25274b19 100644 --- a/clash-verge-rev/src/hooks/use-system-proxy-state.ts +++ b/clash-verge-rev/src/hooks/use-system-proxy-state.ts @@ -1,14 +1,15 @@ +import { useLockFn } from "ahooks"; import useSWR, { mutate } from "swr"; import { closeAllConnections } from "tauri-plugin-mihomo-api"; +import { useSystemProxyData } from "@/hooks/app-data"; import { useVerge } from "@/hooks/use-verge"; -import { useAppData } from "@/providers/app-data-context"; import { getAutotemProxy } from "@/services/cmds"; // 系统代理状态检测统一逻辑 export const useSystemProxyState = () => { const { verge, mutateVerge, patchVerge } = useVerge(); - const { sysproxy } = useAppData(); + const { sysproxy } = useSystemProxyData(); const { data: autoproxy } = useSWR("getAutotemProxy", getAutotemProxy, { revalidateOnFocus: true, revalidateOnReconnect: true, @@ -41,31 +42,30 @@ export const useSystemProxyState = () => { } }; - const updateProxyStatus = async () => { - await new Promise((resolve) => setTimeout(resolve, 100)); + const updateProxyStatus = async (isEnabling: boolean) => { + // 关闭时更快响应,开启时等待系统确认 + const delay = isEnabling ? 20 : 10; + await new Promise((resolve) => setTimeout(resolve, delay)); await mutate("getSystemProxy"); await mutate("getAutotemProxy"); }; - const toggleSystemProxy = (enabled: boolean) => { + const toggleSystemProxy = useLockFn(async (enabled: boolean) => { mutateVerge({ ...verge, enable_system_proxy: enabled }, false); - setTimeout(async () => { - try { - if (!enabled && verge?.auto_close_connection) { - closeAllConnections(); - } - await patchVerge({ enable_system_proxy: enabled }); - - updateProxyStatus(); - } catch (error) { - console.warn("[useSystemProxyState] toggleSystemProxy failed:", error); - mutateVerge({ ...verge, enable_system_proxy: !enabled }, false); + try { + if (!enabled && verge?.auto_close_connection) { + await closeAllConnections(); } - }, 0); - - return Promise.resolve(); - }; + await patchVerge({ enable_system_proxy: enabled }); + await updateProxyStatus(enabled); + } catch (error) { + console.warn("[useSystemProxyState] toggleSystemProxy failed:", error); + mutateVerge({ ...verge, enable_system_proxy: !enabled }, false); + await updateProxyStatus(!enabled); + throw error; + } + }); return { actualState: getSystemProxyActualState(), diff --git a/clash-verge-rev/src/hooks/use-traffic-data.ts b/clash-verge-rev/src/hooks/use-traffic-data.ts index 1e386da790..eb8ca41110 100644 --- a/clash-verge-rev/src/hooks/use-traffic-data.ts +++ b/clash-verge-rev/src/hooks/use-traffic-data.ts @@ -15,7 +15,7 @@ export const useTrafficData = () => { const trafficRef = useRef(null); const { graphData: { appendData }, - } = useTrafficMonitorEnhanced(); + } = useTrafficMonitorEnhanced({ subscribe: false }); const ws = useRef(null); const wsFirstConnection = useRef(true); const timeoutRef = useRef>(null); diff --git a/clash-verge-rev/src/hooks/use-traffic-monitor.ts b/clash-verge-rev/src/hooks/use-traffic-monitor.ts index de9ae52a23..0f1291be61 100644 --- a/clash-verge-rev/src/hooks/use-traffic-monitor.ts +++ b/clash-verge-rev/src/hooks/use-traffic-monitor.ts @@ -1,59 +1,51 @@ -import { useEffect, useRef, useCallback, useReducer } from "react"; +import { + useCallback, + useEffect, + useMemo, + useReducer, + useRef, + useState, +} from "react"; import { Traffic } from "tauri-plugin-mihomo-api"; -// 增强的流量数据点接口 -export interface ITrafficDataPoint { - up: number; - down: number; - timestamp: number; - name: string; -} +import type { + ISamplerStats, + ITrafficDataPoint, + ITrafficWorkerSnapshotMessage, + TrafficWorkerRequestMessage, + TrafficWorkerResponseMessage, +} from "@/types/traffic-monitor"; +import { debugLog } from "@/utils/debug"; +import { TrafficDataSampler, formatTrafficName } from "@/utils/traffic-sampler"; -// 压缩的数据点(用于长期存储) -interface ICompressedDataPoint { - up: number; - down: number; - timestamp: number; - samples: number; // 压缩了多少个原始数据点 -} - -// 数据采样器配置 -interface ISamplingConfig { - // 原始数据保持时间(分钟) - rawDataMinutes: number; - // 压缩数据保持时间(分钟) - compressedDataMinutes: number; - // 压缩比例(多少个原始点压缩成1个) - compressionRatio: number; -} +export type { ITrafficDataPoint } from "@/types/traffic-monitor"; // 引用计数管理器 class ReferenceCounter { private count = 0; - private callbacks: (() => void)[] = []; + private callbacks = new Set<() => void>(); + + private notify() { + this.callbacks.forEach((cb) => cb()); + } increment(): () => void { this.count++; - console.log(`[ReferenceCounter] 引用计数增加: ${this.count}`); + debugLog(`[ReferenceCounter] 引用计数增加: ${this.count}`); - if (this.count === 1) { - // 从0到1,开始数据收集 - this.callbacks.forEach((cb) => cb()); - } + this.notify(); return () => { this.count--; - console.log(`[ReferenceCounter] 引用计数减少: ${this.count}`); + debugLog(`[ReferenceCounter] 引用计数减少: ${this.count}`); - if (this.count === 0) { - // 从1到0,停止数据收集 - this.callbacks.forEach((cb) => cb()); - } + this.notify(); }; } onCountChange(callback: () => void) { - this.callbacks.push(callback); + this.callbacks.add(callback); + return () => this.callbacks.delete(callback); } getCount(): number { @@ -61,225 +53,405 @@ class ReferenceCounter { } } -// 智能数据采样器 -class TrafficDataSampler { - private rawBuffer: ITrafficDataPoint[] = []; - private compressedBuffer: ICompressedDataPoint[] = []; - private config: ISamplingConfig; - private compressionQueue: ITrafficDataPoint[] = []; +const WORKER_CONFIG = { + rawDataMinutes: 10, + compressedDataMinutes: 60, + compressionRatio: 5, + snapshotIntervalMs: 250, + defaultRangeMinutes: 10, +}; - constructor(config: ISamplingConfig) { - this.config = config; - } +class InlineTrafficMonitor { + private config = { ...WORKER_CONFIG }; + private sampler = new TrafficDataSampler(this.config); + private throttleTimer: ReturnType | null = null; + private currentRange = this.config.defaultRangeMinutes; + private lastTimestamp: number | undefined; - addDataPoint(point: ITrafficDataPoint): void { - // 添加到原始缓冲区 - this.rawBuffer.push(point); + constructor( + private emit: (snapshot: ITrafficWorkerSnapshotMessage) => void, + ) {} - // 清理过期的原始数据 - const rawCutoff = Date.now() - this.config.rawDataMinutes * 60 * 1000; - this.rawBuffer = this.rawBuffer.filter((p) => p.timestamp > rawCutoff); - - // 添加到压缩队列 - this.compressionQueue.push(point); - - // 当压缩队列达到压缩比例时,执行压缩 - if (this.compressionQueue.length >= this.config.compressionRatio) { - this.compressData(); - } - - // 清理过期的压缩数据 - const compressedCutoff = - Date.now() - this.config.compressedDataMinutes * 60 * 1000; - this.compressedBuffer = this.compressedBuffer.filter( - (p) => p.timestamp > compressedCutoff, - ); - } - - private compressData(): void { - if (this.compressionQueue.length === 0) return; - - // 计算平均值进行压缩 - const totalUp = this.compressionQueue.reduce((sum, p) => sum + p.up, 0); - const totalDown = this.compressionQueue.reduce((sum, p) => sum + p.down, 0); - const avgTimestamp = - this.compressionQueue.reduce((sum, p) => sum + p.timestamp, 0) / - this.compressionQueue.length; - - const compressedPoint: ICompressedDataPoint = { - up: totalUp / this.compressionQueue.length, - down: totalDown / this.compressionQueue.length, - timestamp: avgTimestamp, - samples: this.compressionQueue.length, - }; - - this.compressedBuffer.push(compressedPoint); - this.compressionQueue = []; - - console.log(`[DataSampler] 压缩了 ${compressedPoint.samples} 个数据点`); - } - - getDataForTimeRange(minutes: number): ITrafficDataPoint[] { - const cutoff = Date.now() - minutes * 60 * 1000; - - // 如果请求的时间范围在原始数据范围内,直接返回原始数据 - if (minutes <= this.config.rawDataMinutes) { - return this.rawBuffer.filter((p) => p.timestamp > cutoff); - } - - // 否则组合原始数据和压缩数据 - const rawData = this.rawBuffer.filter((p) => p.timestamp > cutoff); - const compressedData = this.compressedBuffer - .filter( - (p) => - p.timestamp > cutoff && - p.timestamp <= Date.now() - this.config.rawDataMinutes * 60 * 1000, - ) - .map((p) => ({ - up: p.up, - down: p.down, - timestamp: p.timestamp, - name: new Date(p.timestamp).toLocaleTimeString("en-US", { - hour12: false, - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - }), - })); - - return [...compressedData, ...rawData].sort( - (a, b) => a.timestamp - b.timestamp, - ); - } - - getStats() { - return { - rawBufferSize: this.rawBuffer.length, - compressedBufferSize: this.compressedBuffer.length, - compressionQueueSize: this.compressionQueue.length, - totalMemoryPoints: this.rawBuffer.length + this.compressedBuffer.length, - }; - } - - clear(): void { - this.rawBuffer = []; - this.compressedBuffer = []; - this.compressionQueue = []; - } -} - -// 全局单例 -const refCounter = new ReferenceCounter(); -let globalSampler: TrafficDataSampler | null = null; - -/** - * 增强的流量监控Hook - 支持数据压缩、采样和引用计数 - */ -export const useTrafficMonitorEnhanced = () => { - // 初始化采样器 - if (!globalSampler) { - globalSampler = new TrafficDataSampler({ - rawDataMinutes: 10, // 原始数据保持10分钟 - compressedDataMinutes: 60, // 压缩数据保持1小时 - compressionRatio: 5, // 每5个原始点压缩成1个 + start(rangeMinutes?: number) { + this.currentRange = rangeMinutes ?? this.currentRange; + this.handle({ + type: "init", + config: { + ...this.config, + defaultRangeMinutes: this.currentRange, + }, }); } - const [, forceRender] = useReducer((version: number) => version + 1, 0); - const cleanupRef = useRef<(() => void) | null>(null); + stop() { + if (this.throttleTimer !== null) { + clearTimeout(this.throttleTimer); + this.throttleTimer = null; + } + this.sampler.clear(); + this.lastTimestamp = undefined; + } - const bumpRenderVersion = useCallback(() => { - forceRender(); - }, []); + handle(message: TrafficWorkerRequestMessage) { + switch (message.type) { + case "init": { + this.config = { ...message.config }; + this.sampler = new TrafficDataSampler(this.config); + this.currentRange = message.config.defaultRangeMinutes; + this.emitSnapshot("init"); + break; + } + case "append": { + const timestamp = message.payload.timestamp ?? Date.now(); + const dataPoint: ITrafficDataPoint = { + up: message.payload.up || 0, + down: message.payload.down || 0, + timestamp, + name: formatTrafficName(timestamp), + }; - // 注册引用计数 + this.lastTimestamp = timestamp; + this.sampler.addDataPoint(dataPoint); + this.scheduleSnapshot("append-throttle"); + break; + } + case "clear": { + this.sampler.clear(); + this.lastTimestamp = undefined; + this.emitSnapshot("clear"); + break; + } + case "setRange": { + if (this.currentRange !== message.minutes) { + this.currentRange = message.minutes; + this.emitSnapshot("range-change"); + } + break; + } + case "requestSnapshot": { + this.emitSnapshot("request"); + break; + } + default: + break; + } + } + + private emitSnapshot(reason: ITrafficWorkerSnapshotMessage["reason"]) { + const dataPoints = this.sampler.getDataForTimeRange(this.currentRange); + const availableDataPoints = this.sampler.getDataForTimeRange( + this.config.compressedDataMinutes, + ); + + this.emit({ + type: "snapshot", + dataPoints, + availableDataPoints, + samplerStats: this.sampler.getStats(), + rangeMinutes: this.currentRange, + lastTimestamp: this.lastTimestamp, + reason, + }); + } + + private scheduleSnapshot(reason: ITrafficWorkerSnapshotMessage["reason"]) { + if (this.throttleTimer !== null) return; + this.throttleTimer = setTimeout(() => { + this.throttleTimer = null; + this.emitSnapshot(reason); + }, this.config.snapshotIntervalMs); + } +} + +class TrafficWorkerClient { + private worker: Worker | null = null; + private inlineMonitor: InlineTrafficMonitor | null = null; + private mode: "worker" | "inline" | null = null; + private listeners = new Set< + (snapshot: ITrafficWorkerSnapshotMessage) => void + >(); + private pendingMessages: TrafficWorkerRequestMessage[] = []; + private ready = false; + private currentRange = WORKER_CONFIG.defaultRangeMinutes; + + start(rangeMinutes?: number) { + if (typeof window === "undefined") { + debugLog("[TrafficWorkerClient] Window not available, skip start"); + return; + } + + this.currentRange = rangeMinutes ?? this.currentRange; + + if (this.ready) return; + + const initMessage: TrafficWorkerRequestMessage = { + type: "init", + config: { + rawDataMinutes: WORKER_CONFIG.rawDataMinutes, + compressedDataMinutes: WORKER_CONFIG.compressedDataMinutes, + compressionRatio: WORKER_CONFIG.compressionRatio, + snapshotIntervalMs: WORKER_CONFIG.snapshotIntervalMs, + defaultRangeMinutes: this.currentRange, + }, + }; + + if (typeof Worker !== "undefined") { + try { + this.worker = new Worker( + new URL("../services/traffic-monitor-worker.ts", import.meta.url), + { type: "module" }, + ); + this.mode = "worker"; + + this.worker.onmessage = ( + event: MessageEvent, + ) => { + const message = event.data; + if (message.type === "snapshot") { + this.listeners.forEach((listener) => listener(message)); + } + }; + + this.worker.onerror = (error) => { + debugLog(`[TrafficWorkerClient] Worker error: ${String(error)}`); + }; + + this.ready = true; + this.post(initMessage); + this.flushQueue(); + return; + } catch (error) { + debugLog( + `[TrafficWorkerClient] Worker initialization failed, falling back to inline sampler: ${String(error)}`, + ); + this.worker = null; + this.mode = null; + } + } else { + debugLog( + "[TrafficWorkerClient] Worker not supported, using inline sampler", + ); + } + + this.startInline(initMessage); + } + + private startInline(initMessage: TrafficWorkerRequestMessage) { + this.inlineMonitor = new InlineTrafficMonitor((snapshot) => + this.listeners.forEach((listener) => listener(snapshot)), + ); + this.mode = "inline"; + this.ready = true; + this.post(initMessage); + this.flushQueue(); + } + + stop() { + if (this.worker) { + this.worker.terminate(); + } + if (this.inlineMonitor) { + this.inlineMonitor.stop(); + } + this.worker = null; + this.inlineMonitor = null; + this.mode = null; + this.ready = false; + this.pendingMessages = []; + } + + onSnapshot(listener: (snapshot: ITrafficWorkerSnapshotMessage) => void) { + this.listeners.add(listener); + return () => { + this.listeners.delete(listener); + }; + } + + private post(message: TrafficWorkerRequestMessage) { + if (!this.ready) { + this.pendingMessages.push(message); + return; + } + + if (this.mode === "worker" && this.worker) { + this.worker.postMessage(message); + return; + } + + if (this.mode === "inline" && this.inlineMonitor) { + this.inlineMonitor.handle(message); + return; + } + + this.pendingMessages.push(message); + } + + private flushQueue() { + if (!this.ready || this.pendingMessages.length === 0) { + return; + } + const queued = [...this.pendingMessages]; + this.pendingMessages = []; + queued.forEach((message) => { + this.post(message); + }); + } + + private ensureStarted() { + if (!this.ready) { + this.start(this.currentRange); + } + } + + appendData(traffic: Traffic) { + this.ensureStarted(); + this.post({ + type: "append", + payload: { + up: traffic?.up ?? 0, + down: traffic?.down ?? 0, + timestamp: Date.now(), + }, + }); + } + + clearData() { + this.ensureStarted(); + this.post({ type: "clear" }); + } + + setRange(minutes: number) { + this.currentRange = minutes; + this.post({ type: "setRange", minutes }); + } + + requestSnapshot() { + this.ensureStarted(); + this.post({ type: "requestSnapshot" }); + } +} + +const refCounter = new ReferenceCounter(); +let workerClient: TrafficWorkerClient | null = null; +const getWorkerClient = () => { + if (!workerClient) { + workerClient = new TrafficWorkerClient(); + } + return workerClient; +}; + +const EMPTY_STATS: ISamplerStats = { + rawBufferSize: 0, + compressedBufferSize: 0, + compressionQueueSize: 0, + totalMemoryPoints: 0, +}; + +/** + * 增强的流量监控Hook - Web Worker驱动的数据采样与压缩 + */ +export const useTrafficMonitorEnhanced = (options?: { + subscribe?: boolean; +}) => { + const subscribeToSnapshots = options?.subscribe ?? true; + const [latestSnapshot, setLatestSnapshot] = useState<{ + availableDataPoints: ITrafficDataPoint[]; + samplerStats: ISamplerStats; + lastTimestamp?: number; + }>({ + availableDataPoints: [], + samplerStats: EMPTY_STATS, + lastTimestamp: undefined, + }); + const [rangeMinutes, setRangeMinutes] = useState( + WORKER_CONFIG.defaultRangeMinutes, + ); + const [now, setNow] = useState(() => Date.now()); + const [, forceRefCountRender] = useReducer((value) => value + 1, 0); + + const clientRef = useRef(getWorkerClient()); + const currentRangeRef = useRef(WORKER_CONFIG.defaultRangeMinutes); + + // 注册引用计数与Worker生命周期 useEffect(() => { - console.log("[TrafficMonitorEnhanced] 组件挂载,注册引用计数"); + const client = getWorkerClient(); + clientRef.current = client; + + const stopWatchRefCount = refCounter.onCountChange(() => + forceRefCountRender(), + ); const cleanup = refCounter.increment(); - cleanupRef.current = cleanup; + client.start(currentRangeRef.current); + + let unsubscribe: (() => void) | undefined; + if (subscribeToSnapshots) { + unsubscribe = client.onSnapshot((message) => { + setLatestSnapshot({ + availableDataPoints: + message.availableDataPoints ?? message.dataPoints, + samplerStats: message.samplerStats, + lastTimestamp: message.lastTimestamp, + }); + }); + + client.requestSnapshot(); + } return () => { - console.log("[TrafficMonitorEnhanced] 组件卸载,清理引用计数"); + unsubscribe?.(); + stopWatchRefCount(); cleanup(); - cleanupRef.current = null; - }; - }, []); - - // 设置引用计数变化回调 - useEffect(() => { - const handleCountChange = () => { - console.log( - `[TrafficMonitorEnhanced] 引用计数变化: ${refCounter.getCount()}`, - ); if (refCounter.getCount() === 0) { - console.log("[TrafficMonitorEnhanced] 所有组件已卸载,暂停数据收集"); - } else { - console.log("[TrafficMonitorEnhanced] 开始数据收集"); + client.stop(); } }; + }, [subscribeToSnapshots]); - refCounter.onCountChange(handleCountChange); - }, []); + // Periodically refresh "now" so idle streams age out of the selected window when subscribed + useEffect(() => { + if (!subscribeToSnapshots) return; - // 获取指定时间范围的数据 - const getDataForTimeRange = useCallback( - (minutes: number): ITrafficDataPoint[] => { - if (!globalSampler) return []; - return globalSampler.getDataForTimeRange(minutes); - }, - [], - ); + const timer = window.setInterval(() => { + setNow(Date.now()); + }, 1000); + + return () => window.clearInterval(timer); + }, [subscribeToSnapshots]); // 添加流量数据 const appendData = useCallback((traffic: Traffic) => { - if (globalSampler) { - // 添加到采样器 - const timestamp = Date.now(); - const dataPoint: ITrafficDataPoint = { - up: traffic?.up || 0, - down: traffic?.down || 0, - timestamp, - name: new Date(timestamp).toLocaleTimeString("en-US", { - hour12: false, - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - }), - }; - globalSampler.addDataPoint(dataPoint); - } + clientRef.current?.appendData(traffic); + }, []); + + // 请求不同时间范围的数据 + const requestRange = useCallback((minutes: number) => { + currentRangeRef.current = minutes; + setRangeMinutes(minutes); + clientRef.current?.setRange(minutes); }, []); // 清空数据 const clearData = useCallback(() => { - if (globalSampler) { - globalSampler.clear(); - bumpRenderVersion(); - } - }, [bumpRenderVersion]); - - // 获取采样器统计信息 - const getSamplerStats = useCallback(() => { - return ( - globalSampler?.getStats() || { - rawBufferSize: 0, - compressedBufferSize: 0, - compressionQueueSize: 0, - totalMemoryPoints: 0, - } - ); + clientRef.current?.clearData(); }, []); + const filteredDataPoints = useMemo(() => { + const sourceData = latestSnapshot.availableDataPoints; + if (sourceData.length === 0) return []; + + const cutoff = now - rangeMinutes * 60 * 1000; + return sourceData.filter((point) => point.timestamp > cutoff); + }, [latestSnapshot.availableDataPoints, rangeMinutes, now]); + return { - // 图表数据管理 graphData: { - dataPoints: globalSampler?.getDataForTimeRange(60) || [], // 默认获取1小时数据 - getDataForTimeRange, + dataPoints: filteredDataPoints, + currentRangeMinutes: rangeMinutes, + requestRange, appendData, clearData, }, - // 性能统计 - samplerStats: getSamplerStats(), + samplerStats: latestSnapshot.samplerStats, referenceCount: refCounter.getCount(), }; }; diff --git a/clash-verge-rev/src/hooks/use-verge.ts b/clash-verge-rev/src/hooks/use-verge.ts index f6415ff80f..8750eaaece 100644 --- a/clash-verge-rev/src/hooks/use-verge.ts +++ b/clash-verge-rev/src/hooks/use-verge.ts @@ -1,14 +1,21 @@ import useSWR from "swr"; import { getVergeConfig, patchVergeConfig } from "@/services/cmds"; +import { getPreloadConfig, setPreloadConfig } from "@/services/preload"; export const useVerge = () => { + const initialVergeConfig = getPreloadConfig(); const { data: verge, mutate: mutateVerge } = useSWR( "getVergeConfig", async () => { const config = await getVergeConfig(); + setPreloadConfig(config); return config; }, + { + fallbackData: initialVergeConfig ?? undefined, + revalidateOnMount: !initialVergeConfig, + }, ); const patchVerge = async (value: Partial) => { diff --git a/clash-verge-rev/src/index.html b/clash-verge-rev/src/index.html index ac7da5cc2f..a148cfdc8e 100644 --- a/clash-verge-rev/src/index.html +++ b/clash-verge-rev/src/index.html @@ -9,8 +9,92 @@ /> Clash Verge + +
+
+
+ Loading Clash Verge... +
+
diff --git a/clash-verge-rev/src/locales/ar/logs.json b/clash-verge-rev/src/locales/ar/logs.json index 06f3c65e04..f603527c3d 100644 --- a/clash-verge-rev/src/locales/ar/logs.json +++ b/clash-verge-rev/src/locales/ar/logs.json @@ -1,5 +1,9 @@ { "page": { "title": "السجلات" + }, + "actions": { + "showDescending": "Newest first", + "showAscending": "Oldest first" } } diff --git a/clash-verge-rev/src/locales/ar/settings.json b/clash-verge-rev/src/locales/ar/settings.json index 6a9ffcac3c..dab0b65c2a 100644 --- a/clash-verge-rev/src/locales/ar/settings.json +++ b/clash-verge-rev/src/locales/ar/settings.json @@ -427,10 +427,12 @@ "strictRoute": "توجيه صارم", "autoDetectInterface": "الكشف التلقائي عن الواجهة", "dnsHijack": "اختطاف DNS", - "mtu": "وحدة الإرسال القصوى" + "mtu": "وحدة الإرسال القصوى", + "autoRedirect": "Auto Redirect" }, "tooltips": { - "dnsHijack": "Please use , to separate multiple DNS servers" + "dnsHijack": "Please use , to separate multiple DNS servers", + "autoRedirect": "Automatically configures nftables/iptables TCP redirects" }, "messages": { "applied": "تم تطبيق الإعدادات" @@ -553,7 +555,8 @@ "toggleSystemProxy": "تفعيل/تعطيل وكيل النظام", "toggleTunMode": "تفعيل/تعطيل وضع TUN", "entryLightweightMode": "Entry Lightweight Mode", - "direct": "الوضع المباشر" + "direct": "الوضع المباشر", + "reactivateProfiles": "إعادة تنشيط الملفات الشخصية" } }, "password": { diff --git a/clash-verge-rev/src/locales/ar/shared.json b/clash-verge-rev/src/locales/ar/shared.json index 9c4d4489b1..5ef8d4f1f4 100644 --- a/clash-verge-rev/src/locales/ar/shared.json +++ b/clash-verge-rev/src/locales/ar/shared.json @@ -89,7 +89,8 @@ "common": { "copySuccess": "تم النسخ بنجاح", "saveSuccess": "Configuration saved successfully", - "saveFailed": "Failed to save configuration" + "saveFailed": "Failed to save configuration", + "refreshFailed": "فشل التحديث" } }, "validation": { diff --git a/clash-verge-rev/src/locales/de/logs.json b/clash-verge-rev/src/locales/de/logs.json index aec2ccf029..5c30be040c 100644 --- a/clash-verge-rev/src/locales/de/logs.json +++ b/clash-verge-rev/src/locales/de/logs.json @@ -1,5 +1,9 @@ { "page": { "title": "Protokolle" + }, + "actions": { + "showDescending": "Newest first", + "showAscending": "Oldest first" } } diff --git a/clash-verge-rev/src/locales/de/settings.json b/clash-verge-rev/src/locales/de/settings.json index 1cfa4c01f5..0409abf00c 100644 --- a/clash-verge-rev/src/locales/de/settings.json +++ b/clash-verge-rev/src/locales/de/settings.json @@ -427,10 +427,12 @@ "strictRoute": "Strenges Routing", "autoDetectInterface": "Netzwerkschnittstelle automatisch auswählen", "dnsHijack": "DNS-Hijacking", - "mtu": "Maximale Übertragungseinheit" + "mtu": "Maximale Übertragungseinheit", + "autoRedirect": "Auto Redirect" }, "tooltips": { - "dnsHijack": "Please use , to separate multiple DNS servers" + "dnsHijack": "Please use , to separate multiple DNS servers", + "autoRedirect": "Automatically configures nftables/iptables TCP redirects" }, "messages": { "applied": "Einstellungen angewendet" @@ -553,7 +555,8 @@ "toggleSystemProxy": "Systemproxy ein/ausschalten", "toggleTunMode": "TUN-Modus ein/ausschalten", "entryLightweightMode": "Leichtgewichtigen Modus betreten", - "direct": "Direktverbindungs-Modus" + "direct": "Direktverbindungs-Modus", + "reactivateProfiles": "Abonnement erneut aktivieren" } }, "password": { diff --git a/clash-verge-rev/src/locales/de/shared.json b/clash-verge-rev/src/locales/de/shared.json index eb2e34d7dd..69faa3a6e2 100644 --- a/clash-verge-rev/src/locales/de/shared.json +++ b/clash-verge-rev/src/locales/de/shared.json @@ -89,7 +89,8 @@ "common": { "copySuccess": "Kopieren erfolgreich", "saveSuccess": "Zufalls-Konfiguration erfolgreich gespeichert", - "saveFailed": "Failed to save configuration" + "saveFailed": "Failed to save configuration", + "refreshFailed": "Aktualisierung fehlgeschlagen" } }, "validation": { diff --git a/clash-verge-rev/src/locales/en/logs.json b/clash-verge-rev/src/locales/en/logs.json index 85afa00bb7..9fca86496f 100644 --- a/clash-verge-rev/src/locales/en/logs.json +++ b/clash-verge-rev/src/locales/en/logs.json @@ -1,5 +1,9 @@ { "page": { "title": "Logs" + }, + "actions": { + "showDescending": "Newest first", + "showAscending": "Oldest first" } } diff --git a/clash-verge-rev/src/locales/en/settings.json b/clash-verge-rev/src/locales/en/settings.json index 08cce1d218..24e821b1c0 100644 --- a/clash-verge-rev/src/locales/en/settings.json +++ b/clash-verge-rev/src/locales/en/settings.json @@ -427,10 +427,12 @@ "strictRoute": "Strict Route", "autoDetectInterface": "Auto Detect Interface", "dnsHijack": "DNS Hijack", - "mtu": "Max Transmission Unit" + "mtu": "Max Transmission Unit", + "autoRedirect": "Auto Redirect" }, "tooltips": { - "dnsHijack": "Please use , to separate multiple DNS servers" + "dnsHijack": "Please use , to separate multiple DNS servers", + "autoRedirect": "Automatically configures nftables/iptables TCP redirects" }, "messages": { "applied": "Settings Applied" @@ -553,7 +555,8 @@ "toggleSystemProxy": "Enable/Disable System Proxy", "toggleTunMode": "Enable/Disable Tun Mode", "entryLightweightMode": "Entry Lightweight Mode", - "direct": "Direct Mode" + "direct": "Direct Mode", + "reactivateProfiles": "Reactivate Profiles" } }, "password": { diff --git a/clash-verge-rev/src/locales/en/shared.json b/clash-verge-rev/src/locales/en/shared.json index 1e2f613179..e27052384e 100644 --- a/clash-verge-rev/src/locales/en/shared.json +++ b/clash-verge-rev/src/locales/en/shared.json @@ -89,7 +89,8 @@ "common": { "copySuccess": "Copy Success", "saveSuccess": "Configuration saved successfully", - "saveFailed": "Failed to save configuration" + "saveFailed": "Failed to save configuration", + "refreshFailed": "Refresh failed; showing last loaded value" } }, "validation": { diff --git a/clash-verge-rev/src/locales/es/logs.json b/clash-verge-rev/src/locales/es/logs.json index fe479175f7..6eb288dd89 100644 --- a/clash-verge-rev/src/locales/es/logs.json +++ b/clash-verge-rev/src/locales/es/logs.json @@ -1,5 +1,9 @@ { "page": { "title": "Registros" + }, + "actions": { + "showDescending": "Newest first", + "showAscending": "Oldest first" } } diff --git a/clash-verge-rev/src/locales/es/settings.json b/clash-verge-rev/src/locales/es/settings.json index a687ed5885..39692beeaa 100644 --- a/clash-verge-rev/src/locales/es/settings.json +++ b/clash-verge-rev/src/locales/es/settings.json @@ -427,10 +427,12 @@ "strictRoute": "Enrutamiento estricto", "autoDetectInterface": "Detectar automáticamente la interfaz de salida del tráfico", "dnsHijack": "Secuestro de DNS", - "mtu": "Unidad máxima de transmisión" + "mtu": "Unidad máxima de transmisión", + "autoRedirect": "Auto Redirect" }, "tooltips": { - "dnsHijack": "Please use , to separate multiple DNS servers" + "dnsHijack": "Please use , to separate multiple DNS servers", + "autoRedirect": "Automatically configures nftables/iptables TCP redirects" }, "messages": { "applied": "Ajustes aplicados" @@ -553,7 +555,8 @@ "toggleSystemProxy": "Activar/desactivar el proxy del sistema", "toggleTunMode": "Activar/desactivar el modo TUN", "entryLightweightMode": "Entrar en modo ligero", - "direct": "Modo de conexión directa" + "direct": "Modo de conexión directa", + "reactivateProfiles": "Reactivar suscripciones" } }, "password": { diff --git a/clash-verge-rev/src/locales/es/shared.json b/clash-verge-rev/src/locales/es/shared.json index 7ed7494c6c..7babab4ef2 100644 --- a/clash-verge-rev/src/locales/es/shared.json +++ b/clash-verge-rev/src/locales/es/shared.json @@ -89,7 +89,8 @@ "common": { "copySuccess": "Copia exitosa", "saveSuccess": "Configuración aleatoria guardada correctamente", - "saveFailed": "Failed to save configuration" + "saveFailed": "Failed to save configuration", + "refreshFailed": "Error al actualizar" } }, "validation": { diff --git a/clash-verge-rev/src/locales/fa/logs.json b/clash-verge-rev/src/locales/fa/logs.json index 18d2842d16..36be0f7c22 100644 --- a/clash-verge-rev/src/locales/fa/logs.json +++ b/clash-verge-rev/src/locales/fa/logs.json @@ -1,5 +1,9 @@ { "page": { "title": "لاگ‌ها" + }, + "actions": { + "showDescending": "Newest first", + "showAscending": "Oldest first" } } diff --git a/clash-verge-rev/src/locales/fa/settings.json b/clash-verge-rev/src/locales/fa/settings.json index b99ec661ed..ee9fd2467e 100644 --- a/clash-verge-rev/src/locales/fa/settings.json +++ b/clash-verge-rev/src/locales/fa/settings.json @@ -427,10 +427,12 @@ "strictRoute": "مسیر دقیق", "autoDetectInterface": "تشخیص خودکار رابط", "dnsHijack": "ربایش DNS", - "mtu": "واحد حداکثر انتقال" + "mtu": "واحد حداکثر انتقال", + "autoRedirect": "Auto Redirect" }, "tooltips": { - "dnsHijack": "Please use , to separate multiple DNS servers" + "dnsHijack": "Please use , to separate multiple DNS servers", + "autoRedirect": "Automatically configures nftables/iptables TCP redirects" }, "messages": { "applied": "تنظیمات اعمال شد" @@ -553,7 +555,8 @@ "toggleSystemProxy": "فعال/غیرفعال کردن پراکسی سیستم", "toggleTunMode": "فعال/غیرفعال کردن حالت Tun", "entryLightweightMode": "Entry Lightweight Mode", - "direct": "حالت مستقیم" + "direct": "حالت مستقیم", + "reactivateProfiles": "فعال‌سازی مجدد پروفایل‌ها" } }, "password": { diff --git a/clash-verge-rev/src/locales/fa/shared.json b/clash-verge-rev/src/locales/fa/shared.json index 6ab4e72d05..4584050753 100644 --- a/clash-verge-rev/src/locales/fa/shared.json +++ b/clash-verge-rev/src/locales/fa/shared.json @@ -89,7 +89,8 @@ "common": { "copySuccess": "کپی با موفقیت انجام شد", "saveSuccess": "Configuration saved successfully", - "saveFailed": "Failed to save configuration" + "saveFailed": "Failed to save configuration", + "refreshFailed": "به‌روزرسانی ناموفق بود" } }, "validation": { diff --git a/clash-verge-rev/src/locales/id/logs.json b/clash-verge-rev/src/locales/id/logs.json index b41b2a76b7..2896570b0b 100644 --- a/clash-verge-rev/src/locales/id/logs.json +++ b/clash-verge-rev/src/locales/id/logs.json @@ -1,5 +1,9 @@ { "page": { "title": "Log" + }, + "actions": { + "showDescending": "Newest first", + "showAscending": "Oldest first" } } diff --git a/clash-verge-rev/src/locales/id/settings.json b/clash-verge-rev/src/locales/id/settings.json index abdf1bea57..c32b3f8bac 100644 --- a/clash-verge-rev/src/locales/id/settings.json +++ b/clash-verge-rev/src/locales/id/settings.json @@ -427,10 +427,12 @@ "strictRoute": "Rute Ketat", "autoDetectInterface": "Deteksi Antarmuka Otomatis", "dnsHijack": "Pembajakan DNS", - "mtu": "Unit Transmisi Maksimum" + "mtu": "Unit Transmisi Maksimum", + "autoRedirect": "Auto Redirect" }, "tooltips": { - "dnsHijack": "Please use , to separate multiple DNS servers" + "dnsHijack": "Please use , to separate multiple DNS servers", + "autoRedirect": "Automatically configures nftables/iptables TCP redirects" }, "messages": { "applied": "Pengaturan Diterapkan" @@ -553,7 +555,8 @@ "toggleSystemProxy": "Aktifkan/Nonaktifkan Proksi Sistem", "toggleTunMode": "Aktifkan/Nonaktifkan Mode Tun", "entryLightweightMode": "Entry Lightweight Mode", - "direct": "Mode Langsung" + "direct": "Mode Langsung", + "reactivateProfiles": "Reaktivasi Profil" } }, "password": { diff --git a/clash-verge-rev/src/locales/id/shared.json b/clash-verge-rev/src/locales/id/shared.json index 057e9f74be..0effab311c 100644 --- a/clash-verge-rev/src/locales/id/shared.json +++ b/clash-verge-rev/src/locales/id/shared.json @@ -89,7 +89,8 @@ "common": { "copySuccess": "Salin Berhasil", "saveSuccess": "Configuration saved successfully", - "saveFailed": "Failed to save configuration" + "saveFailed": "Failed to save configuration", + "refreshFailed": "Penyegaran gagal" } }, "validation": { diff --git a/clash-verge-rev/src/locales/jp/logs.json b/clash-verge-rev/src/locales/jp/logs.json index 3ae97f236b..cdbfb0ff78 100644 --- a/clash-verge-rev/src/locales/jp/logs.json +++ b/clash-verge-rev/src/locales/jp/logs.json @@ -1,5 +1,9 @@ { "page": { "title": "ログ" + }, + "actions": { + "showDescending": "Newest first", + "showAscending": "Oldest first" } } diff --git a/clash-verge-rev/src/locales/jp/settings.json b/clash-verge-rev/src/locales/jp/settings.json index f02ec9b551..b0cb62d440 100644 --- a/clash-verge-rev/src/locales/jp/settings.json +++ b/clash-verge-rev/src/locales/jp/settings.json @@ -427,10 +427,12 @@ "strictRoute": "厳格なルート", "autoDetectInterface": "トラフィックの出口インターフェースを自動選択", "dnsHijack": "DNSハイジャック", - "mtu": "最大転送単位" + "mtu": "最大転送単位", + "autoRedirect": "Auto Redirect" }, "tooltips": { - "dnsHijack": "Please use , to separate multiple DNS servers" + "dnsHijack": "Please use , to separate multiple DNS servers", + "autoRedirect": "Automatically configures nftables/iptables TCP redirects" }, "messages": { "applied": "設定が適用されました。" @@ -553,7 +555,8 @@ "toggleSystemProxy": "システムプロキシを開く/閉じる", "toggleTunMode": "TUNモードを開く/閉じる", "entryLightweightMode": "軽量モードに入る", - "direct": "直接接続モード" + "direct": "直接接続モード", + "reactivateProfiles": "プロファイルを再アクティブ化" } }, "password": { diff --git a/clash-verge-rev/src/locales/jp/shared.json b/clash-verge-rev/src/locales/jp/shared.json index 292e7c1b27..20a88547cf 100644 --- a/clash-verge-rev/src/locales/jp/shared.json +++ b/clash-verge-rev/src/locales/jp/shared.json @@ -89,7 +89,8 @@ "common": { "copySuccess": "コピー成功", "saveSuccess": "ランダム設定を保存完了", - "saveFailed": "Failed to save configuration" + "saveFailed": "Failed to save configuration", + "refreshFailed": "更新に失敗しました" } }, "validation": { diff --git a/clash-verge-rev/src/locales/ko/logs.json b/clash-verge-rev/src/locales/ko/logs.json index a5e30a1da8..542d40fc33 100644 --- a/clash-verge-rev/src/locales/ko/logs.json +++ b/clash-verge-rev/src/locales/ko/logs.json @@ -1,5 +1,9 @@ { "page": { "title": "로그" + }, + "actions": { + "showDescending": "Newest first", + "showAscending": "Oldest first" } } diff --git a/clash-verge-rev/src/locales/ko/settings.json b/clash-verge-rev/src/locales/ko/settings.json index faa61a57c2..7c00960309 100644 --- a/clash-verge-rev/src/locales/ko/settings.json +++ b/clash-verge-rev/src/locales/ko/settings.json @@ -427,10 +427,12 @@ "strictRoute": "엄격 라우팅", "autoDetectInterface": "인터페이스 자동 감지", "dnsHijack": "DNS 하이재킹", - "mtu": "MTU" + "mtu": "MTU", + "autoRedirect": "Auto Redirect" }, "tooltips": { - "dnsHijack": "여러 DNS 서버는 쉼표(,)로 구분하세요" + "dnsHijack": "여러 DNS 서버는 쉼표(,)로 구분하세요", + "autoRedirect": "Automatically configures nftables/iptables TCP redirects" }, "messages": { "applied": "설정이 적용되었습니다" @@ -553,7 +555,8 @@ "toggleSystemProxy": "시스템 프록시 켜기/끄기", "toggleTunMode": "TUN 모드 켜기/끄기", "entryLightweightMode": "경량 모드 진입", - "direct": "직접 모드" + "direct": "직접 모드", + "reactivateProfiles": "프로필 재활성화" } }, "password": { diff --git a/clash-verge-rev/src/locales/ko/shared.json b/clash-verge-rev/src/locales/ko/shared.json index 4309e5f9c2..13877c0e17 100644 --- a/clash-verge-rev/src/locales/ko/shared.json +++ b/clash-verge-rev/src/locales/ko/shared.json @@ -89,7 +89,8 @@ "common": { "copySuccess": "복사 성공", "saveSuccess": "설정이 저장되었습니다", - "saveFailed": "설정 저장 실패" + "saveFailed": "설정 저장 실패", + "refreshFailed": "새로고침 실패" } }, "validation": { diff --git a/clash-verge-rev/src/locales/ru/logs.json b/clash-verge-rev/src/locales/ru/logs.json index 523666c1c7..1465500f5c 100644 --- a/clash-verge-rev/src/locales/ru/logs.json +++ b/clash-verge-rev/src/locales/ru/logs.json @@ -1,5 +1,9 @@ { "page": { "title": "Логи" + }, + "actions": { + "showDescending": "Newest first", + "showAscending": "Oldest first" } } diff --git a/clash-verge-rev/src/locales/ru/settings.json b/clash-verge-rev/src/locales/ru/settings.json index 2a73cbf61a..f2da50b192 100644 --- a/clash-verge-rev/src/locales/ru/settings.json +++ b/clash-verge-rev/src/locales/ru/settings.json @@ -427,10 +427,12 @@ "strictRoute": "Строгая маршрутизация", "autoDetectInterface": "Автоопределение интерфейса", "dnsHijack": "DNS-перехват", - "mtu": "MTU" + "mtu": "MTU", + "autoRedirect": "Auto Redirect" }, "tooltips": { - "dnsHijack": "Please use , to separate multiple DNS servers" + "dnsHijack": "Please use , to separate multiple DNS servers", + "autoRedirect": "Automatically configures nftables/iptables TCP redirects" }, "messages": { "applied": "Настройки применены" @@ -553,7 +555,8 @@ "toggleSystemProxy": "Включить/Отключить системный прокси", "toggleTunMode": "Включить/Отключить режим TUN", "entryLightweightMode": "Вход в LightWeight Mode", - "direct": "Прямой режим" + "direct": "Прямой режим", + "reactivateProfiles": "Перезапустить профиль" } }, "password": { diff --git a/clash-verge-rev/src/locales/ru/shared.json b/clash-verge-rev/src/locales/ru/shared.json index be79ca5896..573de6d2f0 100644 --- a/clash-verge-rev/src/locales/ru/shared.json +++ b/clash-verge-rev/src/locales/ru/shared.json @@ -89,7 +89,8 @@ "common": { "copySuccess": "Скопировано", "saveSuccess": "Configuration saved successfully", - "saveFailed": "Failed to save configuration" + "saveFailed": "Failed to save configuration", + "refreshFailed": "Не удалось обновить" } }, "validation": { diff --git a/clash-verge-rev/src/locales/tr/logs.json b/clash-verge-rev/src/locales/tr/logs.json index 4fa81c059c..ed373cc241 100644 --- a/clash-verge-rev/src/locales/tr/logs.json +++ b/clash-verge-rev/src/locales/tr/logs.json @@ -1,5 +1,9 @@ { "page": { "title": "Günlükler" + }, + "actions": { + "showDescending": "Newest first", + "showAscending": "Oldest first" } } diff --git a/clash-verge-rev/src/locales/tr/settings.json b/clash-verge-rev/src/locales/tr/settings.json index 2dee692536..db77989881 100644 --- a/clash-verge-rev/src/locales/tr/settings.json +++ b/clash-verge-rev/src/locales/tr/settings.json @@ -427,10 +427,12 @@ "strictRoute": "Katı Yönlendirme", "autoDetectInterface": "Arayüzü Otomatik Algıla", "dnsHijack": "DNS Ele Geçirme", - "mtu": "Maksimum İletim Birimi" + "mtu": "Maksimum İletim Birimi", + "autoRedirect": "Auto Redirect" }, "tooltips": { - "dnsHijack": "Please use , to separate multiple DNS servers" + "dnsHijack": "Please use , to separate multiple DNS servers", + "autoRedirect": "Automatically configures nftables/iptables TCP redirects" }, "messages": { "applied": "Ayarlar Uygulandı" @@ -553,7 +555,8 @@ "toggleSystemProxy": "Sistem Vekil'ini Etkinleştir/Devre Dışı Bırak", "toggleTunMode": "Tun Modunu Etkinleştir/Devre Dışı Bırak", "entryLightweightMode": "Hafif Moda Gir", - "direct": "Doğrudan Mod" + "direct": "Doğrudan Mod", + "reactivateProfiles": "Profilleri Yeniden Etkinleştir" } }, "password": { diff --git a/clash-verge-rev/src/locales/tr/shared.json b/clash-verge-rev/src/locales/tr/shared.json index e094bdbc23..eeac37355b 100644 --- a/clash-verge-rev/src/locales/tr/shared.json +++ b/clash-verge-rev/src/locales/tr/shared.json @@ -89,7 +89,8 @@ "common": { "copySuccess": "Kopyalama Başarılı", "saveSuccess": "Configuration saved successfully", - "saveFailed": "Failed to save configuration" + "saveFailed": "Failed to save configuration", + "refreshFailed": "Yenileme başarısız" } }, "validation": { diff --git a/clash-verge-rev/src/locales/tt/logs.json b/clash-verge-rev/src/locales/tt/logs.json index 5e4931c772..93e8394bb8 100644 --- a/clash-verge-rev/src/locales/tt/logs.json +++ b/clash-verge-rev/src/locales/tt/logs.json @@ -1,5 +1,9 @@ { "page": { "title": "Логлар" + }, + "actions": { + "showDescending": "Newest first", + "showAscending": "Oldest first" } } diff --git a/clash-verge-rev/src/locales/tt/settings.json b/clash-verge-rev/src/locales/tt/settings.json index 2317799985..fee717492d 100644 --- a/clash-verge-rev/src/locales/tt/settings.json +++ b/clash-verge-rev/src/locales/tt/settings.json @@ -427,10 +427,12 @@ "strictRoute": "Катгый маршрутлау", "autoDetectInterface": "Интерфейсны автоматик ачыклау", "dnsHijack": "DNS'ны үзгәртеп тоту (hijack)", - "mtu": "MTU (макс. тапшыру берәмлеге)" + "mtu": "MTU (макс. тапшыру берәмлеге)", + "autoRedirect": "Auto Redirect" }, "tooltips": { - "dnsHijack": "Please use , to separate multiple DNS servers" + "dnsHijack": "Please use , to separate multiple DNS servers", + "autoRedirect": "Automatically configures nftables/iptables TCP redirects" }, "messages": { "applied": "Көйләүләр кулланылды" @@ -553,7 +555,8 @@ "toggleSystemProxy": "Системалы проксины кабызу/сүндерү", "toggleTunMode": "Tun режимын кабызу/сүндерү", "entryLightweightMode": "Entry Lightweight Mode", - "direct": "Туры режим" + "direct": "Туры режим", + "reactivateProfiles": "Профильләрне янәдән активлаштыру" } }, "password": { diff --git a/clash-verge-rev/src/locales/tt/shared.json b/clash-verge-rev/src/locales/tt/shared.json index 9405b43ac4..eb6316b33e 100644 --- a/clash-verge-rev/src/locales/tt/shared.json +++ b/clash-verge-rev/src/locales/tt/shared.json @@ -89,7 +89,8 @@ "common": { "copySuccess": "Күчерелде", "saveSuccess": "Configuration saved successfully", - "saveFailed": "Failed to save configuration" + "saveFailed": "Failed to save configuration", + "refreshFailed": "Refresh failed" } }, "validation": { diff --git a/clash-verge-rev/src/locales/zh/logs.json b/clash-verge-rev/src/locales/zh/logs.json index 1e38a12ce8..d0fea4e4fb 100644 --- a/clash-verge-rev/src/locales/zh/logs.json +++ b/clash-verge-rev/src/locales/zh/logs.json @@ -1,5 +1,9 @@ { "page": { "title": "日志" + }, + "actions": { + "showDescending": "按时间倒序", + "showAscending": "按时间正序" } } diff --git a/clash-verge-rev/src/locales/zh/rules.json b/clash-verge-rev/src/locales/zh/rules.json index 15596df303..d660f141c8 100644 --- a/clash-verge-rev/src/locales/zh/rules.json +++ b/clash-verge-rev/src/locales/zh/rules.json @@ -16,8 +16,8 @@ "updateSuccess": "{{name}} 更新成功", "updateFailed": "{{name}} 更新失败: {{message}}", "genericError": "更新失败: {{message}}", - "none": "没有可更新的 provider", - "allUpdated": "所有 provider 均已更新" + "none": "没有可更新的规则集合", + "allUpdated": "所有规则集合均已更新" } } }, diff --git a/clash-verge-rev/src/locales/zh/settings.json b/clash-verge-rev/src/locales/zh/settings.json index 3677fe1cf3..8ab74085c3 100644 --- a/clash-verge-rev/src/locales/zh/settings.json +++ b/clash-verge-rev/src/locales/zh/settings.json @@ -427,10 +427,12 @@ "strictRoute": "严格路由", "autoDetectInterface": "自动选择流量出口接口", "dnsHijack": "DNS 劫持", - "mtu": "最大传输单元" + "mtu": "最大传输单元", + "autoRedirect": "自动重定向" }, "tooltips": { - "dnsHijack": "多个 DNS 服务器请使用 , 分隔" + "dnsHijack": "多个 DNS 服务器请使用 , 分隔", + "autoRedirect": "自动配置 nftables/iptables 的 TCP 重定向" }, "messages": { "applied": "设置已应用" @@ -553,7 +555,8 @@ "toggleSystemProxy": "打开/关闭系统代理", "toggleTunMode": "打开/关闭 TUN 模式", "entryLightweightMode": "进入轻量模式", - "direct": "直连模式" + "direct": "直连模式", + "reactivateProfiles": "重新激活订阅" } }, "password": { diff --git a/clash-verge-rev/src/locales/zh/shared.json b/clash-verge-rev/src/locales/zh/shared.json index 7db791f853..047ce8186f 100644 --- a/clash-verge-rev/src/locales/zh/shared.json +++ b/clash-verge-rev/src/locales/zh/shared.json @@ -89,7 +89,8 @@ "common": { "copySuccess": "复制成功", "saveSuccess": "配置保存成功", - "saveFailed": "配置保存失败" + "saveFailed": "配置保存失败", + "refreshFailed": "刷新失败" } }, "validation": { diff --git a/clash-verge-rev/src/locales/zhtw/logs.json b/clash-verge-rev/src/locales/zhtw/logs.json index 183916d3c2..53305bf4a9 100644 --- a/clash-verge-rev/src/locales/zhtw/logs.json +++ b/clash-verge-rev/src/locales/zhtw/logs.json @@ -1,5 +1,9 @@ { "page": { "title": "日誌" + }, + "actions": { + "showDescending": "按時間倒序", + "showAscending": "按時間正序" } } diff --git a/clash-verge-rev/src/locales/zhtw/rules.json b/clash-verge-rev/src/locales/zhtw/rules.json index c65bee98e7..73ca39db77 100644 --- a/clash-verge-rev/src/locales/zhtw/rules.json +++ b/clash-verge-rev/src/locales/zhtw/rules.json @@ -16,8 +16,8 @@ "updateSuccess": "已成功更新 {{name}}", "updateFailed": "更新 {{name}} 失敗:{{message}}", "genericError": "更新失敗:{{message}}", - "none": "沒有可供更新的提供者", - "allUpdated": "所有提供者都已成功更新" + "none": "沒有可更新的規則集合", + "allUpdated": "所有規則集合均已更新" } } }, diff --git a/clash-verge-rev/src/locales/zhtw/settings.json b/clash-verge-rev/src/locales/zhtw/settings.json index d78440f295..a2b716f40a 100644 --- a/clash-verge-rev/src/locales/zhtw/settings.json +++ b/clash-verge-rev/src/locales/zhtw/settings.json @@ -427,10 +427,12 @@ "strictRoute": "嚴格路由", "autoDetectInterface": "自動偵測流量輸出介面", "dnsHijack": "DNS 綁架", - "mtu": "最大傳輸單位" + "mtu": "最大傳輸單位", + "autoRedirect": "自動重導" }, "tooltips": { - "dnsHijack": "請用半形逗號來區隔多個 DNS 伺服器" + "dnsHijack": "請用半形逗號來區隔多個 DNS 伺服器", + "autoRedirect": "自動配置 nftables/iptables 的 TCP 重導" }, "messages": { "applied": "設定已套用" @@ -553,7 +555,8 @@ "toggleSystemProxy": "開啟/關閉系統代理", "toggleTunMode": "開啟/關閉 虛擬網路介面卡模式", "entryLightweightMode": "進入輕量模式", - "direct": "直連模式" + "direct": "直連模式", + "reactivateProfiles": "重新啟用訂閱" } }, "password": { diff --git a/clash-verge-rev/src/locales/zhtw/shared.json b/clash-verge-rev/src/locales/zhtw/shared.json index c7d5fe72c8..0895910553 100644 --- a/clash-verge-rev/src/locales/zhtw/shared.json +++ b/clash-verge-rev/src/locales/zhtw/shared.json @@ -89,7 +89,8 @@ "common": { "copySuccess": "複製成功", "saveSuccess": "設定儲存完成", - "saveFailed": "設定儲存失敗" + "saveFailed": "設定儲存失敗", + "refreshFailed": "重整失敗" } }, "validation": { diff --git a/clash-verge-rev/src/main.tsx b/clash-verge-rev/src/main.tsx index 62e3c4e278..c2b9d42ca7 100644 --- a/clash-verge-rev/src/main.tsx +++ b/clash-verge-rev/src/main.tsx @@ -1,6 +1,7 @@ /// /// import "./assets/styles/index.scss"; +import "./utils/monaco"; import { ResizeObserver } from "@juggle/resize-observer"; import { ComposeContextProvider } from "foxact/compose-context-provider"; @@ -13,12 +14,18 @@ import { BaseErrorBoundary } from "./components/base"; import { router } from "./pages/_routers"; import { AppDataProvider } from "./providers/app-data-provider"; import { WindowProvider } from "./providers/window"; -import { initializeLanguage } from "./services/i18n"; +import { FALLBACK_LANGUAGE, initializeLanguage } from "./services/i18n"; +import { + preloadAppData, + resolveThemeMode, + getPreloadConfig, +} from "./services/preload"; import { LoadingCacheProvider, ThemeModeProvider, UpdateStateProvider, } from "./services/states"; +import { disableWebViewShortcuts } from "./utils/disable-webview-shortcuts"; if (!window.ResizeObserver) { window.ResizeObserver = ResizeObserver; @@ -33,23 +40,11 @@ if (!container) { ); } -document.addEventListener("keydown", (event) => { - // Disable WebView keyboard shortcuts - const disabledShortcuts = - ["F5", "F7"].includes(event.key) || - (event.altKey && ["ArrowLeft", "ArrowRight"].includes(event.key)) || - ((event.ctrlKey || event.metaKey) && - ["F", "G", "H", "J", "P", "Q", "R", "U"].includes( - event.key.toUpperCase(), - )); - if (disabledShortcuts) { - event.preventDefault(); - } -}); +disableWebViewShortcuts(); -const initializeApp = () => { +const initializeApp = (initialThemeMode: "light" | "dark") => { const contexts = [ - , + , , , ]; @@ -70,20 +65,39 @@ const initializeApp = () => { ); }; -initializeLanguage("zh").catch(console.error); -initializeApp(); +const bootstrap = async () => { + const { initialThemeMode } = await preloadAppData(); + initializeApp(initialThemeMode); +}; -// 错误处理 +bootstrap().catch((error) => { + console.error( + "[main.tsx] App bootstrap failed, falling back to default language:", + error, + ); + initializeLanguage(FALLBACK_LANGUAGE) + .catch((fallbackError) => { + console.error( + "[main.tsx] Fallback language initialization failed:", + fallbackError, + ); + }) + .finally(() => { + initializeApp(resolveThemeMode(getPreloadConfig())); + }); +}); + +// Error handling window.addEventListener("error", (event) => { - console.error("[main.tsx] 全局错误:", event.error); + console.error("[main.tsx] Global error:", event.error); }); window.addEventListener("unhandledrejection", (event) => { - console.error("[main.tsx] 未处理的Promise拒绝:", event.reason); + console.error("[main.tsx] Unhandled promise rejection:", event.reason); }); -// 页面关闭/刷新事件 +// Page close/refresh events window.addEventListener("beforeunload", () => { - // 同步清理所有 WebSocket 实例, 防止内存泄漏 + // Clean up all WebSocket instances to prevent memory leaks MihomoWebSocket.cleanupAll(); }); diff --git a/clash-verge-rev/src/pages/_layout/notificationHandlers.ts b/clash-verge-rev/src/pages/_layout/notificationHandlers.ts index 58631b8f3b..29cda855b2 100644 --- a/clash-verge-rev/src/pages/_layout/notificationHandlers.ts +++ b/clash-verge-rev/src/pages/_layout/notificationHandlers.ts @@ -34,6 +34,7 @@ export const handleNoticeMessage = ( "settings.feedback.notifications.updater.withClashProxyFailed", msg, ), + "reactivate_profiles::error": () => showNotice.error(msg), update_failed: () => showNotice.error(msg), "config_validate::boot_error": () => showNotice.error("shared.feedback.validation.config.bootFailed", msg), diff --git a/clash-verge-rev/src/pages/logs.tsx b/clash-verge-rev/src/pages/logs.tsx index 73e4d7fb16..c3dbb9e31d 100644 --- a/clash-verge-rev/src/pages/logs.tsx +++ b/clash-verge-rev/src/pages/logs.tsx @@ -1,6 +1,7 @@ import { PlayCircleOutlineRounded, PauseCircleOutlineRounded, + SwapVertRounded, } from "@mui/icons-material"; import { Box, Button, IconButton, MenuItem } from "@mui/material"; import { useMemo, useState } from "react"; @@ -20,6 +21,8 @@ const LogPage = () => { const [clashLog, setClashLog] = useClashLog(); const enableLog = clashLog.enable; const logState = clashLog.logFilter; + const logOrder = clashLog.logOrder ?? "asc"; + const isDescending = logOrder === "desc"; const [match, setMatch] = useState(() => (_: string) => true); const [searchState, setSearchState] = useState(); @@ -49,6 +52,11 @@ const LogPage = () => { }); }, [logData, logState, match]); + const filteredLogs = useMemo( + () => (isDescending ? [...filterLogs].reverse() : filterLogs), + [filterLogs, isDescending], + ); + const handleLogLevelChange = (newLevel: string) => { setClashLog((pre: any) => ({ ...pre, logFilter: newLevel })); }; @@ -57,6 +65,13 @@ const LogPage = () => { setClashLog((pre: any) => ({ ...pre, enable: !enableLog })); }; + const handleToggleOrder = () => { + setClashLog((pre: any) => ({ + ...pre, + logOrder: pre.logOrder === "desc" ? "asc" : "desc", + })); + }; + return ( { )}
+ + +