Update On Fri Jun 6 20:37:27 CEST 2025

This commit is contained in:
github-action[bot]
2025-06-06 20:37:28 +02:00
parent f076575346
commit ea0afe0bab
269 changed files with 12409 additions and 7238 deletions
+1
View File
@@ -1020,3 +1020,4 @@ Update On Sun Jun 1 20:35:02 CEST 2025
Update On Mon Jun 2 20:41:58 CEST 2025 Update On Mon Jun 2 20:41:58 CEST 2025
Update On Tue Jun 3 20:38:26 CEST 2025 Update On Tue Jun 3 20:38:26 CEST 2025
Update On Wed Jun 4 20:38:34 CEST 2025 Update On Wed Jun 4 20:38:34 CEST 2025
Update On Fri Jun 6 20:37:19 CEST 2025
+12 -3
View File
@@ -27,11 +27,16 @@ type tValue[T any] struct {
} }
func (t *TypedValue[T]) Load() T { func (t *TypedValue[T]) Load() T {
value, _ := t.LoadOk()
return value
}
func (t *TypedValue[T]) LoadOk() (_ T, ok bool) {
value := t.value.Load() value := t.value.Load()
if value == nil { if value == nil {
return DefaultValue[T]() return DefaultValue[T](), false
} }
return value.(tValue[T]).value return value.(tValue[T]).value, true
} }
func (t *TypedValue[T]) Store(value T) { func (t *TypedValue[T]) Store(value T) {
@@ -47,7 +52,11 @@ func (t *TypedValue[T]) Swap(new T) T {
} }
func (t *TypedValue[T]) CompareAndSwap(old, new T) bool { func (t *TypedValue[T]) CompareAndSwap(old, new T) bool {
return t.value.CompareAndSwap(tValue[T]{old}, tValue[T]{new}) return t.value.CompareAndSwap(tValue[T]{old}, tValue[T]{new}) ||
// In the edge-case where [atomic.Value.Store] is uninitialized
// and trying to compare with the zero value of T,
// then compare-and-swap with the nil any value.
(any(old) == any(DefaultValue[T]()) && t.value.CompareAndSwap(any(nil), tValue[T]{new}))
} }
func (t *TypedValue[T]) MarshalJSON() ([]byte, error) { func (t *TypedValue[T]) MarshalJSON() ([]byte, error) {
+77
View File
@@ -0,0 +1,77 @@
package atomic
import (
"io"
"os"
"testing"
)
func TestTypedValue(t *testing.T) {
{
// Always wrapping should not allocate for simple values
// because tValue[T] has the same memory layout as T.
var v TypedValue[bool]
bools := []bool{true, false}
if n := int(testing.AllocsPerRun(1000, func() {
for _, b := range bools {
v.Store(b)
}
})); n != 0 {
t.Errorf("AllocsPerRun = %d, want 0", n)
}
}
{
var v TypedValue[int]
got, gotOk := v.LoadOk()
if got != 0 || gotOk {
t.Fatalf("LoadOk = (%v, %v), want (0, false)", got, gotOk)
}
v.Store(1)
got, gotOk = v.LoadOk()
if got != 1 || !gotOk {
t.Fatalf("LoadOk = (%v, %v), want (1, true)", got, gotOk)
}
}
{
var v TypedValue[error]
got, gotOk := v.LoadOk()
if got != nil || gotOk {
t.Fatalf("LoadOk = (%v, %v), want (nil, false)", got, gotOk)
}
v.Store(io.EOF)
got, gotOk = v.LoadOk()
if got != io.EOF || !gotOk {
t.Fatalf("LoadOk = (%v, %v), want (EOF, true)", got, gotOk)
}
err := &os.PathError{}
v.Store(err)
got, gotOk = v.LoadOk()
if got != err || !gotOk {
t.Fatalf("LoadOk = (%v, %v), want (%v, true)", got, gotOk, err)
}
v.Store(nil)
got, gotOk = v.LoadOk()
if got != nil || !gotOk {
t.Fatalf("LoadOk = (%v, %v), want (nil, true)", got, gotOk)
}
}
{
c1, c2, c3 := make(chan struct{}), make(chan struct{}), make(chan struct{})
var v TypedValue[chan struct{}]
if v.CompareAndSwap(c1, c2) != false {
t.Fatalf("CompareAndSwap = true, want false")
}
if v.CompareAndSwap(nil, c1) != true {
t.Fatalf("CompareAndSwap = false, want true")
}
if v.CompareAndSwap(c2, c3) != false {
t.Fatalf("CompareAndSwap = true, want false")
}
if v.CompareAndSwap(c1, c2) != true {
t.Fatalf("CompareAndSwap = false, want true")
}
}
}
+12 -5
View File
@@ -46,17 +46,24 @@ func RelayDnsConn(ctx context.Context, conn net.Conn, readTimeout time.Duration)
ctx, cancel := context.WithTimeout(ctx, DefaultDnsRelayTimeout) ctx, cancel := context.WithTimeout(ctx, DefaultDnsRelayTimeout)
defer cancel() defer cancel()
inData := buff[:n] inData := buff[:n]
msg, err := relayDnsPacket(ctx, inData, buff, 0) outBuff := buff[2:]
msg, err := relayDnsPacket(ctx, inData, outBuff, 0)
if err != nil { if err != nil {
return err return err
} }
err = binary.Write(conn, binary.BigEndian, uint16(len(msg))) if &msg[0] == &outBuff[0] { // msg is still in the buff
if err != nil { binary.BigEndian.PutUint16(buff[:2], uint16(len(msg)))
return err outBuff = buff[:2+len(msg)]
} else { // buff not big enough (WTF???)
newBuff := pool.Get(len(msg) + 2)
defer pool.Put(newBuff)
binary.BigEndian.PutUint16(newBuff[:2], uint16(len(msg)))
copy(newBuff[2:], msg)
outBuff = newBuff
} }
_, err = conn.Write(msg) _, err = conn.Write(outBuff)
if err != nil { if err != nil {
return err return err
} }
+13 -17
View File
@@ -1168,10 +1168,21 @@ func parseNameServer(servers []string, respectRules bool, preferH3 bool) ([]dns.
return nil, fmt.Errorf("DNS NameServer[%d] format error: %s", idx, err.Error()) return nil, fmt.Errorf("DNS NameServer[%d] format error: %s", idx, err.Error())
} }
proxyName := u.Fragment var proxyName string
params := map[string]string{}
for _, s := range strings.Split(u.Fragment, "&") {
arr := strings.SplitN(s, "=", 2)
switch len(arr) {
case 0:
continue
case 1:
proxyName = arr[0]
case 2:
params[arr[0]] = arr[1]
}
}
var addr, dnsNetType string var addr, dnsNetType string
params := map[string]string{}
switch u.Scheme { switch u.Scheme {
case "udp": case "udp":
addr, err = hostWithDefaultPort(u.Host, "53") addr, err = hostWithDefaultPort(u.Host, "53")
@@ -1189,23 +1200,8 @@ func parseNameServer(servers []string, respectRules bool, preferH3 bool) ([]dns.
addr, err = hostWithDefaultPort(u.Host, "80") addr, err = hostWithDefaultPort(u.Host, "80")
} }
if err == nil { if err == nil {
proxyName = ""
clearURL := url.URL{Scheme: u.Scheme, Host: addr, Path: u.Path, User: u.User} clearURL := url.URL{Scheme: u.Scheme, Host: addr, Path: u.Path, User: u.User}
addr = clearURL.String() addr = clearURL.String()
if len(u.Fragment) != 0 {
for _, s := range strings.Split(u.Fragment, "&") {
arr := strings.Split(s, "=")
if len(arr) == 0 {
continue
} else if len(arr) == 1 {
proxyName = arr[0]
} else if len(arr) == 2 {
params[arr[0]] = arr[1]
} else {
params[arr[0]] = strings.Join(arr[1:], "=")
}
}
}
} }
case "quic": case "quic":
addr, err = hostWithDefaultPort(u.Host, "853") addr, err = hostWithDefaultPort(u.Host, "853")
+23
View File
@@ -6,8 +6,10 @@ import (
"fmt" "fmt"
"net" "net"
"strings" "strings"
"time"
"github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/ca"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/log"
D "github.com/miekg/dns" D "github.com/miekg/dns"
@@ -105,3 +107,24 @@ func (c *client) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, error)
} }
func (c *client) ResetConnection() {} func (c *client) ResetConnection() {}
func newClient(addr string, resolver *Resolver, netType string, params map[string]string, proxyAdapter C.ProxyAdapter, proxyName string) *client {
host, port, _ := net.SplitHostPort(addr)
c := &client{
Client: &D.Client{
Net: netType,
TLSConfig: &tls.Config{
ServerName: host,
},
UDPSize: 4096,
Timeout: 5 * time.Second,
},
port: port,
host: host,
dialer: newDNSDialer(resolver, proxyAdapter, proxyName),
}
if params["skip-cert-verify"] == "true" {
c.TLSConfig.InsecureSkipVerify = true
}
return c
}
+2 -31
View File
@@ -9,7 +9,6 @@ import (
"io" "io"
"net" "net"
"net/http" "net/http"
"net/netip"
"net/url" "net/url"
"runtime" "runtime"
"strconv" "strconv"
@@ -71,8 +70,6 @@ type dnsOverHTTPS struct {
dialer *dnsDialer dialer *dnsDialer
addr string addr string
skipCertVerify bool skipCertVerify bool
ecsPrefix netip.Prefix
ecsOverride bool
} }
// type check // type check
@@ -105,28 +102,6 @@ func newDoHClient(urlString string, r *Resolver, preferH3 bool, params map[strin
doh.skipCertVerify = true doh.skipCertVerify = true
} }
if ecs := params["ecs"]; ecs != "" {
prefix, err := netip.ParsePrefix(ecs)
if err != nil {
addr, err := netip.ParseAddr(ecs)
if err != nil {
log.Warnln("DOH [%s] config with invalid ecs: %s", doh.addr, ecs)
} else {
doh.ecsPrefix = netip.PrefixFrom(addr, addr.BitLen())
}
} else {
doh.ecsPrefix = prefix
}
}
if doh.ecsPrefix.IsValid() {
log.Debugln("DOH [%s] config with ecs: %s", doh.addr, doh.ecsPrefix)
}
if params["ecs-override"] == "true" {
doh.ecsOverride = true
}
runtime.SetFinalizer(doh, (*dnsOverHTTPS).Close) runtime.SetFinalizer(doh, (*dnsOverHTTPS).Close)
return doh return doh
@@ -154,10 +129,6 @@ func (doh *dnsOverHTTPS) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.
} }
}() }()
if doh.ecsPrefix.IsValid() {
setEdns0Subnet(m, doh.ecsPrefix, doh.ecsOverride)
}
// Check if there was already an active client before sending the request. // Check if there was already an active client before sending the request.
// We'll only attempt to re-connect if there was one. // We'll only attempt to re-connect if there was one.
client, isCached, err := doh.getClient(ctx) client, isCached, err := doh.getClient(ctx)
@@ -552,8 +523,8 @@ func (doh *dnsOverHTTPS) createTransportH3(
Dial: func( Dial: func(
ctx context.Context, ctx context.Context,
// Ignore the address and always connect to the one that we got // Ignore the address and always connect to the one that we got
// from the bootstrapper. // from the bootstrapper.
_ string, _ string,
tlsCfg *tlsC.Config, tlsCfg *tlsC.Config,
cfg *quic.Config, cfg *quic.Config,
+10 -5
View File
@@ -61,15 +61,16 @@ type dnsOverQUIC struct {
bytesPool *sync.Pool bytesPool *sync.Pool
bytesPoolGuard sync.Mutex bytesPoolGuard sync.Mutex
addr string addr string
dialer *dnsDialer dialer *dnsDialer
skipCertVerify bool
} }
// type check // type check
var _ dnsClient = (*dnsOverQUIC)(nil) var _ dnsClient = (*dnsOverQUIC)(nil)
// newDoQ returns the DNS-over-QUIC Upstream. // newDoQ returns the DNS-over-QUIC Upstream.
func newDoQ(resolver *Resolver, addr string, proxyAdapter C.ProxyAdapter, proxyName string) (dnsClient, error) { func newDoQ(addr string, resolver *Resolver, params map[string]string, proxyAdapter C.ProxyAdapter, proxyName string) *dnsOverQUIC {
doq := &dnsOverQUIC{ doq := &dnsOverQUIC{
addr: addr, addr: addr,
dialer: newDNSDialer(resolver, proxyAdapter, proxyName), dialer: newDNSDialer(resolver, proxyAdapter, proxyName),
@@ -79,8 +80,12 @@ func newDoQ(resolver *Resolver, addr string, proxyAdapter C.ProxyAdapter, proxyN
}, },
} }
if params["skip-cert-verify"] == "true" {
doq.skipCertVerify = true
}
runtime.SetFinalizer(doq, (*dnsOverQUIC).Close) runtime.SetFinalizer(doq, (*dnsOverQUIC).Close)
return doq, nil return doq
} }
// Address implements the Upstream interface for *dnsOverQUIC. // Address implements the Upstream interface for *dnsOverQUIC.
@@ -329,7 +334,7 @@ func (doq *dnsOverQUIC) openConnection(ctx context.Context) (conn quic.Connectio
tlsConfig := ca.GetGlobalTLSConfig( tlsConfig := ca.GetGlobalTLSConfig(
&tls.Config{ &tls.Config{
ServerName: host, ServerName: host,
InsecureSkipVerify: false, InsecureSkipVerify: doq.skipCertVerify,
NextProtos: []string{ NextProtos: []string{
NextProtoDQ, NextProtoDQ,
}, },
+77 -30
View File
@@ -2,10 +2,8 @@ package dns
import ( import (
"context" "context"
"crypto/tls"
"errors" "errors"
"fmt" "fmt"
"net"
"net/netip" "net/netip"
"strings" "strings"
"time" "time"
@@ -92,46 +90,95 @@ func isIPRequest(q D.Question) bool {
func transform(servers []NameServer, resolver *Resolver) []dnsClient { func transform(servers []NameServer, resolver *Resolver) []dnsClient {
ret := make([]dnsClient, 0, len(servers)) ret := make([]dnsClient, 0, len(servers))
for _, s := range servers { for _, s := range servers {
var c dnsClient
switch s.Net { switch s.Net {
case "https": case "https":
ret = append(ret, newDoHClient(s.Addr, resolver, s.PreferH3, s.Params, s.ProxyAdapter, s.ProxyName)) c = newDoHClient(s.Addr, resolver, s.PreferH3, s.Params, s.ProxyAdapter, s.ProxyName)
continue
case "dhcp": case "dhcp":
ret = append(ret, newDHCPClient(s.Addr)) c = newDHCPClient(s.Addr)
continue
case "system": case "system":
ret = append(ret, newSystemClient()) c = newSystemClient()
continue
case "rcode": case "rcode":
ret = append(ret, newRCodeClient(s.Addr)) c = newRCodeClient(s.Addr)
continue
case "quic": case "quic":
if doq, err := newDoQ(resolver, s.Addr, s.ProxyAdapter, s.ProxyName); err == nil { c = newDoQ(s.Addr, resolver, s.Params, s.ProxyAdapter, s.ProxyName)
ret = append(ret, doq) default:
} else { c = newClient(s.Addr, resolver, s.Net, s.Params, s.ProxyAdapter, s.ProxyName)
log.Fatalln("DoQ format error: %v", err)
}
continue
} }
host, port, _ := net.SplitHostPort(s.Addr) c = warpClientWithEdns0Subnet(c, s.Params)
ret = append(ret, &client{
Client: &D.Client{ if s.Params["disable-ipv4"] == "true" {
Net: s.Net, c = warpClientWithDisableType(c, D.TypeA)
TLSConfig: &tls.Config{ }
ServerName: host,
}, if s.Params["disable-ipv6"] == "true" {
UDPSize: 4096, c = warpClientWithDisableType(c, D.TypeAAAA)
Timeout: 5 * time.Second, }
},
port: port, ret = append(ret, c)
host: host,
dialer: newDNSDialer(resolver, s.ProxyAdapter, s.ProxyName),
})
} }
return ret return ret
} }
type clientWithDisableType struct {
dnsClient
qType uint16
}
func (c clientWithDisableType) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) {
if len(m.Question) > 0 {
q := m.Question[0]
if q.Qtype == c.qType {
return handleMsgWithEmptyAnswer(m), nil
}
}
return c.dnsClient.ExchangeContext(ctx, m)
}
func warpClientWithDisableType(c dnsClient, qType uint16) dnsClient {
return clientWithDisableType{c, qType}
}
type clientWithEdns0Subnet struct {
dnsClient
ecsPrefix netip.Prefix
ecsOverride bool
}
func (c clientWithEdns0Subnet) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, error) {
m = m.Copy()
setEdns0Subnet(m, c.ecsPrefix, c.ecsOverride)
return c.dnsClient.ExchangeContext(ctx, m)
}
func warpClientWithEdns0Subnet(c dnsClient, params map[string]string) dnsClient {
var ecsPrefix netip.Prefix
var ecsOverride bool
if ecs := params["ecs"]; ecs != "" {
prefix, err := netip.ParsePrefix(ecs)
if err != nil {
addr, err := netip.ParseAddr(ecs)
if err != nil {
log.Warnln("DNS [%s] config with invalid ecs: %s", c.Address(), ecs)
} else {
ecsPrefix = netip.PrefixFrom(addr, addr.BitLen())
}
} else {
ecsPrefix = prefix
}
}
if ecsPrefix.IsValid() {
log.Debugln("DNS [%s] config with ecs: %s", c.Address(), ecsPrefix)
if params["ecs-override"] == "true" {
ecsOverride = true
}
return clientWithEdns0Subnet{c, ecsPrefix, ecsOverride}
}
return c
}
func handleMsgWithEmptyAnswer(r *D.Msg) *D.Msg { func handleMsgWithEmptyAnswer(r *D.Msg) *D.Msg {
msg := &D.Msg{} msg := &D.Msg{}
msg.Answer = []D.RR{} msg.Answer = []D.RR{}
+3 -3
View File
@@ -25,11 +25,11 @@ require (
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759
github.com/metacubex/quic-go v0.52.1-0.20250522021943-aef454b9e639 github.com/metacubex/quic-go v0.52.1-0.20250522021943-aef454b9e639
github.com/metacubex/randv2 v0.2.0 github.com/metacubex/randv2 v0.2.0
github.com/metacubex/sing v0.5.3 github.com/metacubex/sing v0.5.4-0.20250605054047-54dc6097da29
github.com/metacubex/sing-mux v0.3.2 github.com/metacubex/sing-mux v0.3.2
github.com/metacubex/sing-quic v0.0.0-20250523120938-f1a248e5ec7f github.com/metacubex/sing-quic v0.0.0-20250523120938-f1a248e5ec7f
github.com/metacubex/sing-shadowsocks v0.2.10 github.com/metacubex/sing-shadowsocks v0.2.11-0.20250531133822-e545de386d4c
github.com/metacubex/sing-shadowsocks2 v0.2.4 github.com/metacubex/sing-shadowsocks2 v0.2.5-0.20250531133559-f4d53bd59335
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2
github.com/metacubex/sing-tun v0.4.6-0.20250524142129-9d110c0af70c github.com/metacubex/sing-tun v0.4.6-0.20250524142129-9d110c0af70c
github.com/metacubex/sing-vmess v0.2.2 github.com/metacubex/sing-vmess v0.2.2
+6 -6
View File
@@ -116,16 +116,16 @@ github.com/metacubex/quic-go v0.52.1-0.20250522021943-aef454b9e639/go.mod h1:Kc6
github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs= github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs=
github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY= github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY=
github.com/metacubex/sing v0.5.2/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w= github.com/metacubex/sing v0.5.2/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=
github.com/metacubex/sing v0.5.3 h1:QWdN16WFKMk06x4nzkc8SvZ7y2x+TLQrpkPoHs+WSVM= github.com/metacubex/sing v0.5.4-0.20250605054047-54dc6097da29 h1:SD9q025FNTaepuFXFOKDhnGLVu6PQYChBvw2ZYPXeLo=
github.com/metacubex/sing v0.5.3/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w= github.com/metacubex/sing v0.5.4-0.20250605054047-54dc6097da29/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=
github.com/metacubex/sing-mux v0.3.2 h1:nJv52pyRivHcaZJKk2JgxpaVvj1GAXG81scSa9N7ncw= github.com/metacubex/sing-mux v0.3.2 h1:nJv52pyRivHcaZJKk2JgxpaVvj1GAXG81scSa9N7ncw=
github.com/metacubex/sing-mux v0.3.2/go.mod h1:3rt1soewn0O6j89GCLmwAQFsq257u0jf2zQSPhTL3Bw= github.com/metacubex/sing-mux v0.3.2/go.mod h1:3rt1soewn0O6j89GCLmwAQFsq257u0jf2zQSPhTL3Bw=
github.com/metacubex/sing-quic v0.0.0-20250523120938-f1a248e5ec7f h1:mP3vIm+9hRFI0C0Vl3pE0NESF/L85FDbuB0tGgUii6I= github.com/metacubex/sing-quic v0.0.0-20250523120938-f1a248e5ec7f h1:mP3vIm+9hRFI0C0Vl3pE0NESF/L85FDbuB0tGgUii6I=
github.com/metacubex/sing-quic v0.0.0-20250523120938-f1a248e5ec7f/go.mod h1:JPTpf7fpnojsSuwRJExhSZSy63pVbp3VM39+zj+sAJM= github.com/metacubex/sing-quic v0.0.0-20250523120938-f1a248e5ec7f/go.mod h1:JPTpf7fpnojsSuwRJExhSZSy63pVbp3VM39+zj+sAJM=
github.com/metacubex/sing-shadowsocks v0.2.10 h1:Pr7LDbjMANIQHl07zWgl1vDuhpsfDQUpZ8cX6DPabfg= github.com/metacubex/sing-shadowsocks v0.2.11-0.20250531133822-e545de386d4c h1:ZfgQx24XIN807046dp9CVBjAh0t9NJIzXG3X5jk+PEM=
github.com/metacubex/sing-shadowsocks v0.2.10/go.mod h1:MtRM0ZZjR0kaDOzy9zWSt6/4/UlrnsNBq+1FNAF4vBk= github.com/metacubex/sing-shadowsocks v0.2.11-0.20250531133822-e545de386d4c/go.mod h1:MtRM0ZZjR0kaDOzy9zWSt6/4/UlrnsNBq+1FNAF4vBk=
github.com/metacubex/sing-shadowsocks2 v0.2.4 h1:Ec0x3hHR7xkld5Z09IGh16wtUUpBb2HgqZ9DExd8Q7s= github.com/metacubex/sing-shadowsocks2 v0.2.5-0.20250531133559-f4d53bd59335 h1:nSSdMV+I7Tjcb7s6FzNDMj7jH/ZdryY1DHPlgtFjW98=
github.com/metacubex/sing-shadowsocks2 v0.2.4/go.mod h1:WP8+S0kqtnSbX1vlIpo5i8Irm/ijZITEPBcZ26B5unY= github.com/metacubex/sing-shadowsocks2 v0.2.5-0.20250531133559-f4d53bd59335/go.mod h1:WP8+S0kqtnSbX1vlIpo5i8Irm/ijZITEPBcZ26B5unY=
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 h1:gXU+MYPm7Wme3/OAY2FFzVq9d9GxPHOqu5AQfg/ddhI= github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 h1:gXU+MYPm7Wme3/OAY2FFzVq9d9GxPHOqu5AQfg/ddhI=
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2/go.mod h1:mbfboaXauKJNIHJYxQRa+NJs4JU9NZfkA+I33dS2+9E= github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2/go.mod h1:mbfboaXauKJNIHJYxQRa+NJs4JU9NZfkA+I33dS2+9E=
github.com/metacubex/sing-tun v0.4.6-0.20250524142129-9d110c0af70c h1:Y6jk7AH5BEg9Dsvczrf/KokYsvxeKSZZlCLHg+hC4ro= github.com/metacubex/sing-tun v0.4.6-0.20250524142129-9d110c0af70c h1:Y6jk7AH5BEg9Dsvczrf/KokYsvxeKSZZlCLHg+hC4ro=
+33 -34
View File
@@ -1688,7 +1688,7 @@ version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e"
dependencies = [ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@@ -4106,7 +4106,7 @@ dependencies = [
"js-sys", "js-sys",
"log", "log",
"wasm-bindgen", "wasm-bindgen",
"windows-core 0.58.0", "windows-core 0.61.2",
] ]
[[package]] [[package]]
@@ -4986,7 +4986,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a793df0d7afeac54f95b471d3af7f0d4fb975699f972341a4b76988d49cdf0c" checksum = "6a793df0d7afeac54f95b471d3af7f0d4fb975699f972341a4b76988d49cdf0c"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"windows-targets 0.48.5", "windows-targets 0.53.0",
] ]
[[package]] [[package]]
@@ -6501,9 +6501,9 @@ checksum = "26995317201fa17f3656c36716aed4a7c81743a9634ac4c99c0eeda495db0cec"
[[package]] [[package]]
name = "oxc-miette" name = "oxc-miette"
version = "2.2.1" version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8c278d00ecc50ee84aba4768a7ab74eb325dff4dca8c0581495b850d53480ba" checksum = "98b2c44324a4372caf6e3128a22744263c973e809fc598db3749ef3ff5e9fed4"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"owo-colors", "owo-colors",
@@ -6515,9 +6515,9 @@ dependencies = [
[[package]] [[package]]
name = "oxc-miette-derive" name = "oxc-miette-derive"
version = "2.1.2" version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c0c893f53900e3fe01eca3d6d3b54085573c3e48fe25af9d57dd94ef600dcd3" checksum = "3bd3da01a295024fa79e3b4aba14b590d91256a274ff29cc5ee8f55183b2df24"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -6526,23 +6526,22 @@ dependencies = [
[[package]] [[package]]
name = "oxc_allocator" name = "oxc_allocator"
version = "0.71.0" version = "0.72.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c8248980c6d9db21f8ad42e0c85c172ef4dd20335522fc81e4ac72b6b70f806" checksum = "94677be5d1874da150784b9d551bae0dfad7e5a6121a2c6acccda335c05504ea"
dependencies = [ dependencies = [
"allocator-api2", "allocator-api2",
"bumpalo", "bumpalo",
"hashbrown 0.15.3", "hashbrown 0.15.3",
"oxc_data_structures", "oxc_data_structures",
"rustc-hash 2.1.1", "rustc-hash 2.1.1",
"simdutf8",
] ]
[[package]] [[package]]
name = "oxc_ast" name = "oxc_ast"
version = "0.71.0" version = "0.72.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a05110cb2af185324857a9a5d1a1986196e2cf3c5127cd90a7694c6b326e97c9" checksum = "26f7b73c9a8e265169d98cd2395cbab52fcb849d3297b1ec2511a1d908775c73"
dependencies = [ dependencies = [
"bitflags 2.9.1", "bitflags 2.9.1",
"cow-utils", "cow-utils",
@@ -6557,9 +6556,9 @@ dependencies = [
[[package]] [[package]]
name = "oxc_ast_macros" name = "oxc_ast_macros"
version = "0.71.0" version = "0.72.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24598056bb57788599997bbdb6ebf21a24b3331805aa9190c5b1204c973e636e" checksum = "3f411489fef9ace92fdea3105490f9aab07a78c9adff25b62345b1a1bd49ee0c"
dependencies = [ dependencies = [
"phf 0.11.3", "phf 0.11.3",
"proc-macro2", "proc-macro2",
@@ -6569,9 +6568,9 @@ dependencies = [
[[package]] [[package]]
name = "oxc_ast_visit" name = "oxc_ast_visit"
version = "0.71.0" version = "0.72.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a263a5d0fcb1fd60696b54cc5fd8fba020266522effdb48ae3c74744602e0116" checksum = "cf97a9b275654d7fc921c0c472ceab1ae167b670c9a761e5c2aec66b9076157b"
dependencies = [ dependencies = [
"oxc_allocator", "oxc_allocator",
"oxc_ast", "oxc_ast",
@@ -6581,18 +6580,18 @@ dependencies = [
[[package]] [[package]]
name = "oxc_data_structures" name = "oxc_data_structures"
version = "0.71.0" version = "0.72.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112fcb78e9c0f3dda6beb1d93865f319d1c0165b0bf067fafda7f6529118328d" checksum = "09ebee1a21c5d47b6e845357274904ec9568b001d3ff6ab04d4cc4754fd148c2"
dependencies = [ dependencies = [
"rustversion", "rustversion",
] ]
[[package]] [[package]]
name = "oxc_diagnostics" name = "oxc_diagnostics"
version = "0.71.0" version = "0.72.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7b801ff7bbda76e11e0d5a43c06a80b3c105219e564c05e0b2fdb56d87b832a" checksum = "7fa03ff098ecf135235ed3c88e013a22e9b58caf1108d32eca5261daf2d88330"
dependencies = [ dependencies = [
"cow-utils", "cow-utils",
"oxc-miette", "oxc-miette",
@@ -6600,9 +6599,9 @@ dependencies = [
[[package]] [[package]]
name = "oxc_ecmascript" name = "oxc_ecmascript"
version = "0.71.0" version = "0.72.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea29dc44ee5ed0b63adb6d377dd07bf5870b6ae76d986965a92af1fac101dfd4" checksum = "e1b5dba7d2918b9a59f82ce974b85cc6de1e577d06f49d1e608505e9da6a0b02"
dependencies = [ dependencies = [
"cow-utils", "cow-utils",
"num-bigint", "num-bigint",
@@ -6614,9 +6613,9 @@ dependencies = [
[[package]] [[package]]
name = "oxc_estree" name = "oxc_estree"
version = "0.71.0" version = "0.72.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19ffa7908363d884399956c8971e924f1abba00ebe9995853bc70f536c0ccaaf" checksum = "7a870fcc8a8a8eeb3471cb51df3628f04e9f53521608e735a34bd9b6f96ce78c"
[[package]] [[package]]
name = "oxc_index" name = "oxc_index"
@@ -6626,9 +6625,9 @@ checksum = "2fa07b0cfa997730afed43705766ef27792873fdf5215b1391949fec678d2392"
[[package]] [[package]]
name = "oxc_parser" name = "oxc_parser"
version = "0.71.0" version = "0.72.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0d4e22dc3630700e32320bbbc9f04396f109e8be2bb18791a00950485b049a5" checksum = "ca8194f22ac433d9f2575f3f5a35fdc2ec403877a2c98b8bbb7047acc73e07e6"
dependencies = [ dependencies = [
"bitflags 2.9.1", "bitflags 2.9.1",
"cow-utils", "cow-utils",
@@ -6649,9 +6648,9 @@ dependencies = [
[[package]] [[package]]
name = "oxc_regular_expression" name = "oxc_regular_expression"
version = "0.71.0" version = "0.72.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4691c62894d99f689da38419894e6e38c33862ac414206a65f0539d80c3ef252" checksum = "bac34c62476a83f00f36fb08991730db51a722082125067f9562257fd97cbb88"
dependencies = [ dependencies = [
"bitflags 2.9.1", "bitflags 2.9.1",
"oxc_allocator", "oxc_allocator",
@@ -6665,9 +6664,9 @@ dependencies = [
[[package]] [[package]]
name = "oxc_span" name = "oxc_span"
version = "0.71.0" version = "0.72.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3d915cbabe501873b16236ae0e8ddfd5c001fd764b1dd073128858b5a0641a4" checksum = "886510fc6db2c5a7a905feeb966e613527bdbe2e544057923957ba79e3e93142"
dependencies = [ dependencies = [
"compact_str", "compact_str",
"oxc-miette", "oxc-miette",
@@ -6678,9 +6677,9 @@ dependencies = [
[[package]] [[package]]
name = "oxc_syntax" name = "oxc_syntax"
version = "0.71.0" version = "0.72.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b6318faa445653106b56da5497a080e4fcf9231d5121035639d8c003f992378" checksum = "3dddefc02737686c68da8597a88ba5514e7134aba006c61f72fb1752451f95cc"
dependencies = [ dependencies = [
"bitflags 2.9.1", "bitflags 2.9.1",
"cow-utils", "cow-utils",
@@ -8984,7 +8983,7 @@ dependencies = [
"ntapi", "ntapi",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-io-kit", "objc2-io-kit",
"windows 0.59.0", "windows 0.61.1",
] ]
[[package]] [[package]]
@@ -11223,7 +11222,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
+6 -6
View File
@@ -172,12 +172,12 @@ display-info = "0.5.0" # should be removed after upgrading to tauri v2
# OXC (The Oxidation Compiler) # OXC (The Oxidation Compiler)
# We use it to parse and transpile the old script profile to esm based script profile # We use it to parse and transpile the old script profile to esm based script profile
oxc_parser = "0.71" oxc_parser = "0.72"
oxc_allocator = "0.71" oxc_allocator = "0.72"
oxc_span = "0.71" oxc_span = "0.72"
oxc_ast = "0.71" oxc_ast = "0.72"
oxc_syntax = "0.71" oxc_syntax = "0.72"
oxc_ast_visit = "0.71" oxc_ast_visit = "0.72"
# Lua Integration # Lua Integration
mlua = { version = "0.10", features = [ mlua = { version = "0.10", features = [
@@ -11,7 +11,7 @@
"build": "tsc" "build": "tsc"
}, },
"dependencies": { "dependencies": {
"@tanstack/react-query": "5.79.0", "@tanstack/react-query": "5.80.5",
"@tauri-apps/api": "2.5.0", "@tauri-apps/api": "2.5.0",
"ahooks": "3.8.5", "ahooks": "3.8.5",
"dayjs": "1.11.13", "dayjs": "1.11.13",
@@ -55,9 +55,9 @@
"@csstools/normalize.css": "12.1.1", "@csstools/normalize.css": "12.1.1",
"@emotion/babel-plugin": "11.13.5", "@emotion/babel-plugin": "11.13.5",
"@emotion/react": "11.14.0", "@emotion/react": "11.14.0",
"@iconify/json": "2.2.345", "@iconify/json": "2.2.346",
"@monaco-editor/react": "4.7.0", "@monaco-editor/react": "4.7.0",
"@tanstack/react-query": "5.79.0", "@tanstack/react-query": "5.80.5",
"@tanstack/react-router": "1.120.15", "@tanstack/react-router": "1.120.15",
"@tanstack/react-router-devtools": "1.120.15", "@tanstack/react-router-devtools": "1.120.15",
"@tanstack/router-plugin": "1.120.15", "@tanstack/router-plugin": "1.120.15",
@@ -92,6 +92,6 @@
"vite-plugin-sass-dts": "1.3.31", "vite-plugin-sass-dts": "1.3.31",
"vite-plugin-svgr": "4.3.0", "vite-plugin-svgr": "4.3.0",
"vite-tsconfig-paths": "5.1.4", "vite-tsconfig-paths": "5.1.4",
"zod": "3.24.4" "zod": "3.25.51"
} }
} }
+2 -2
View File
@@ -2,7 +2,7 @@
"manifest_version": 1, "manifest_version": 1,
"latest": { "latest": {
"mihomo": "v1.19.10", "mihomo": "v1.19.10",
"mihomo_alpha": "alpha-71a8705", "mihomo_alpha": "alpha-40587b6",
"clash_rs": "v0.7.8", "clash_rs": "v0.7.8",
"clash_premium": "2023-09-05-gdcc8d87", "clash_premium": "2023-09-05-gdcc8d87",
"clash_rs_alpha": "0.7.8-alpha+sha.9e09f8c" "clash_rs_alpha": "0.7.8-alpha+sha.9e09f8c"
@@ -69,5 +69,5 @@
"linux-armv7hf": "clash-armv7-unknown-linux-gnueabihf" "linux-armv7hf": "clash-armv7-unknown-linux-gnueabihf"
} }
}, },
"updated_at": "2025-06-03T22:22:46.410Z" "updated_at": "2025-06-05T22:21:11.895Z"
} }
+1 -1
View File
@@ -84,7 +84,7 @@
"eslint-plugin-react-compiler": "19.1.0-rc.2", "eslint-plugin-react-compiler": "19.1.0-rc.2",
"eslint-plugin-react-hooks": "5.2.0", "eslint-plugin-react-hooks": "5.2.0",
"globals": "16.2.0", "globals": "16.2.0",
"knip": "5.59.1", "knip": "5.60.0",
"lint-staged": "16.1.0", "lint-staged": "16.1.0",
"neostandard": "0.12.1", "neostandard": "0.12.1",
"npm-run-all2": "8.0.4", "npm-run-all2": "8.0.4",
+112 -106
View File
@@ -101,8 +101,8 @@ importers:
specifier: 16.2.0 specifier: 16.2.0
version: 16.2.0 version: 16.2.0
knip: knip:
specifier: 5.59.1 specifier: 5.60.0
version: 5.59.1(@types/node@22.15.29)(typescript@5.8.3) version: 5.60.0(@types/node@22.15.29)(typescript@5.8.3)
lint-staged: lint-staged:
specifier: 16.1.0 specifier: 16.1.0
version: 16.1.0 version: 16.1.0
@@ -173,8 +173,8 @@ importers:
frontend/interface: frontend/interface:
dependencies: dependencies:
'@tanstack/react-query': '@tanstack/react-query':
specifier: 5.79.0 specifier: 5.80.5
version: 5.79.0(react@19.1.0) version: 5.80.5(react@19.1.0)
'@tauri-apps/api': '@tauri-apps/api':
specifier: 2.5.0 specifier: 2.5.0
version: 2.5.0 version: 2.5.0
@@ -247,7 +247,7 @@ importers:
version: 4.1.8 version: 4.1.8
'@tanstack/router-zod-adapter': '@tanstack/router-zod-adapter':
specifier: 1.81.5 specifier: 1.81.5
version: 1.81.5(@tanstack/react-router@1.120.15(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(zod@3.24.4) version: 1.81.5(@tanstack/react-router@1.120.15(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(zod@3.25.51)
'@tauri-apps/api': '@tauri-apps/api':
specifier: 2.5.0 specifier: 2.5.0
version: 2.5.0 version: 2.5.0
@@ -337,14 +337,14 @@ importers:
specifier: 11.14.0 specifier: 11.14.0
version: 11.14.0(@types/react@19.1.6)(react@19.1.0) version: 11.14.0(@types/react@19.1.6)(react@19.1.0)
'@iconify/json': '@iconify/json':
specifier: 2.2.345 specifier: 2.2.346
version: 2.2.345 version: 2.2.346
'@monaco-editor/react': '@monaco-editor/react':
specifier: 4.7.0 specifier: 4.7.0
version: 4.7.0(monaco-editor@0.52.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) version: 4.7.0(monaco-editor@0.52.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@tanstack/react-query': '@tanstack/react-query':
specifier: 5.79.0 specifier: 5.80.5
version: 5.79.0(react@19.1.0) version: 5.80.5(react@19.1.0)
'@tanstack/react-router': '@tanstack/react-router':
specifier: 1.120.15 specifier: 1.120.15
version: 1.120.15(react-dom@19.1.0(react@19.1.0))(react@19.1.0) version: 1.120.15(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -448,8 +448,8 @@ importers:
specifier: 5.1.4 specifier: 5.1.4
version: 5.1.4(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.29)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.8.0)) version: 5.1.4(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.29)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.8.0))
zod: zod:
specifier: 3.24.4 specifier: 3.25.51
version: 3.24.4 version: 3.25.51
frontend/ui: frontend/ui:
dependencies: dependencies:
@@ -566,8 +566,8 @@ importers:
specifier: 7.7.2 specifier: 7.7.2
version: 7.7.2 version: 7.7.2
zod: zod:
specifier: 3.24.4 specifier: 3.25.51
version: 3.24.4 version: 3.25.51
devDependencies: devDependencies:
'@octokit/types': '@octokit/types':
specifier: 14.1.0 specifier: 14.1.0
@@ -1648,8 +1648,8 @@ packages:
'@vue/compiler-sfc': '@vue/compiler-sfc':
optional: true optional: true
'@iconify/json@2.2.345': '@iconify/json@2.2.346':
resolution: {integrity: sha512-cWcTkpSw42OcltXXlLRMp4bnoFEMvEXEIZDPazqqpT7nr4dPN/ztEqOk6T3z0fXrN2E3OEgW0GnHlQqZz4qDgw==} resolution: {integrity: sha512-QcJNRnHf9UMuGdtbIISsGbUf/AArTpBr4ItaoBYryRjPiq7DHH7kcvbMdHpYcGvAMa6vidaL7g31iTLhOBgnyA==}
'@iconify/types@2.0.0': '@iconify/types@2.0.0':
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
@@ -2066,68 +2066,68 @@ packages:
resolution: {integrity: sha512-5Kva+/Gi7c+39d0/0MM/v/5RCZuwqm75fUD+t7Es3Iz/adui54GnjfNmJpkkPkXGC+5IWnEvgqwY6gstK/JlUQ==} resolution: {integrity: sha512-5Kva+/Gi7c+39d0/0MM/v/5RCZuwqm75fUD+t7Es3Iz/adui54GnjfNmJpkkPkXGC+5IWnEvgqwY6gstK/JlUQ==}
engines: {node: '>= 18'} engines: {node: '>= 18'}
'@oxc-resolver/binding-darwin-arm64@9.0.2': '@oxc-resolver/binding-darwin-arm64@11.1.0':
resolution: {integrity: sha512-MVyRgP2gzJJtAowjG/cHN3VQXwNLWnY+FpOEsyvDepJki1SdAX/8XDijM1yN6ESD1kr9uhBKjGelC6h3qtT+rA==} resolution: {integrity: sha512-n9y3Lb1+BwsOtm3BmXSUPu3iDtTq7Sf0gX4e+izFTfNrj+u6uTKqbmlq8ggV8CRdg1zGUaCvKNvg/9q3C/19gg==}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@oxc-resolver/binding-darwin-x64@9.0.2': '@oxc-resolver/binding-darwin-x64@11.1.0':
resolution: {integrity: sha512-7kV0EOFEZ3sk5Hjy4+bfA6XOQpCwbDiDkkHN4BHHyrBHsXxUR05EcEJPPL1WjItefg+9+8hrBmoK0xRoDs41+A==} resolution: {integrity: sha512-2aJTPN9/lTmq0xw1YYsy5GDPkTyp92EoYRtw9nVgGErwMvA87duuLnIdoztYk66LGa3g5y4RgOaEapZbK7132A==}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@oxc-resolver/binding-freebsd-x64@9.0.2': '@oxc-resolver/binding-freebsd-x64@11.1.0':
resolution: {integrity: sha512-6OvkEtRXrt8sJ4aVfxHRikjain9nV1clIsWtJ1J3J8NG1ZhjyJFgT00SCvqxbK+pzeWJq6XzHyTCN78ML+lY2w==} resolution: {integrity: sha512-GoPEd9GvEyuS1YyqvAhAlccZeBEyHFkrHPEhS/+UTPcrzDzZ16ckJSmZtwOPhci5FWHK/th4L6NPiOnDLGFrqQ==}
cpu: [x64] cpu: [x64]
os: [freebsd] os: [freebsd]
'@oxc-resolver/binding-linux-arm-gnueabihf@9.0.2': '@oxc-resolver/binding-linux-arm-gnueabihf@11.1.0':
resolution: {integrity: sha512-aYpNL6o5IRAUIdoweW21TyLt54Hy/ZS9tvzNzF6ya1ckOQ8DLaGVPjGpmzxdNja9j/bbV6aIzBH7lNcBtiOTkQ==} resolution: {integrity: sha512-mQdQDTbw2/RcJKvMi8RAmDECuEC4waM5jeUBn8Cz1pLVddH8MfYJgKbZJUATBNNaHjw/u+Sq9Q1tcJbm8dhpYQ==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
'@oxc-resolver/binding-linux-arm64-gnu@9.0.2': '@oxc-resolver/binding-linux-arm64-gnu@11.1.0':
resolution: {integrity: sha512-RGFW4vCfKMFEIzb9VCY0oWyyY9tR1/o+wDdNePhiUXZU4SVniRPQaZ1SJ0sUFI1k25pXZmzQmIP6cBmazi/Dew==} resolution: {integrity: sha512-HDFQiPl7cX2DVXFlulWOinjqXa5Rj4ydFY9xJCwWAHGx2LmqwLDD8MI0UrHVUaHhLLWn54vjGtwsJK94dtkCwg==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@oxc-resolver/binding-linux-arm64-musl@9.0.2': '@oxc-resolver/binding-linux-arm64-musl@11.1.0':
resolution: {integrity: sha512-lxx/PibBfzqYvut2Y8N2D0Ritg9H8pKO+7NUSJb9YjR/bfk2KRmP8iaUz3zB0JhPtf/W3REs65oKpWxgflGToA==} resolution: {integrity: sha512-0TFcZSVUQPV1r6sFUf7U2fz0mFCaqh5qMlb2zCioZj0C+xUJghC8bz88/qQUc5SA5K4gqg0WEOXzdqz/mXCLLA==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@oxc-resolver/binding-linux-riscv64-gnu@9.0.2': '@oxc-resolver/binding-linux-riscv64-gnu@11.1.0':
resolution: {integrity: sha512-yD28ptS/OuNhwkpXRPNf+/FvrO7lwURLsEbRVcL1kIE0GxNJNMtKgIE4xQvtKDzkhk6ZRpLho5VSrkkF+3ARTQ==} resolution: {integrity: sha512-crG0iy5U9ac99Xkt9trWo5YvtCoSpPUrNZMeUVDkIy1qy1znfv66CveOgCm0G5TwooIIWLJrtFUqi0AkazS3fw==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
'@oxc-resolver/binding-linux-s390x-gnu@9.0.2': '@oxc-resolver/binding-linux-s390x-gnu@11.1.0':
resolution: {integrity: sha512-WBwEJdspoga2w+aly6JVZeHnxuPVuztw3fPfWrei2P6rNM5hcKxBGWKKT6zO1fPMCB4sdDkFohGKkMHVV1eryQ==} resolution: {integrity: sha512-aPemnsn/FXADFu7/VnSprO8uVb9UhNVdBdrIlAREh3s7LoW1QksKyP8/DlFe0o2E79MRQ3XF1ONOgW5zLcUmzA==}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
'@oxc-resolver/binding-linux-x64-gnu@9.0.2': '@oxc-resolver/binding-linux-x64-gnu@11.1.0':
resolution: {integrity: sha512-a2z3/cbOOTUq0UTBG8f3EO/usFcdwwXnCejfXv42HmV/G8GjrT4fp5+5mVDoMByH3Ce3iVPxj1LmS6OvItKMYQ==} resolution: {integrity: sha512-eMQ0Iue4Bs0jabCIHiEJbZMPoczdx1oBGOiNS/ykCE76Oos/Hb5uD1FB+Vw4agP2cAxzcp8zHO7MpEW450yswg==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@oxc-resolver/binding-linux-x64-musl@9.0.2': '@oxc-resolver/binding-linux-x64-musl@11.1.0':
resolution: {integrity: sha512-bHZF+WShYQWpuswB9fyxcgMIWVk4sZQT0wnwpnZgQuvGTZLkYJ1JTCXJMtaX5mIFHf69ngvawnwPIUA4Feil0g==} resolution: {integrity: sha512-5IjxRv0vWiGb102QmwF+ljutUWA1+BZbdW+58lFOVzVVo29L+m5PrEtijY5kK0FMTDvwb/xFXpGq3/vQx+bpSg==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@oxc-resolver/binding-wasm32-wasi@9.0.2': '@oxc-resolver/binding-wasm32-wasi@11.1.0':
resolution: {integrity: sha512-I5cSgCCh5nFozGSHz+PjIOfrqW99eUszlxKLgoNNzQ1xQ2ou9ZJGzcZ94BHsM9SpyYHLtgHljmOZxCT9bgxYNA==} resolution: {integrity: sha512-+yz7LYHKW1GK+fJoHh9JibgIWDeBHf5wiu1tgDD92y5eLFEBxP+CjJ2caTZnVRREH74l03twOfcTR9EaLsEidQ==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
cpu: [wasm32] cpu: [wasm32]
'@oxc-resolver/binding-win32-arm64-msvc@9.0.2': '@oxc-resolver/binding-win32-arm64-msvc@11.1.0':
resolution: {integrity: sha512-5IhoOpPr38YWDWRCA5kP30xlUxbIJyLAEsAK7EMyUgqygBHEYLkElaKGgS0X5jRXUQ6l5yNxuW73caogb2FYaw==} resolution: {integrity: sha512-aTF/1TIq9v86Qy3++YFhKJVKXYSTO54yRRWIXwzpgGvZu41acjN/UsNOG7C2QFy/xdkitrZf1awYgawSqNox3g==}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
'@oxc-resolver/binding-win32-x64-msvc@9.0.2': '@oxc-resolver/binding-win32-x64-msvc@11.1.0':
resolution: {integrity: sha512-Qc40GDkaad9rZksSQr2l/V9UubigIHsW69g94Gswc2sKYB3XfJXfIfyV8WTJ67u6ZMXsZ7BH1msSC6Aen75mCg==} resolution: {integrity: sha512-CxalsPMU4oSoZviLMaw01RhLglyN7jrUUhTDRv4pYGcsRxxt5S7e/wO9P/lm5BYgAAq4TtP5MkGuGuMrm//a0g==}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
@@ -2754,11 +2754,11 @@ packages:
resolution: {integrity: sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==} resolution: {integrity: sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==}
engines: {node: '>=12'} engines: {node: '>=12'}
'@tanstack/query-core@5.79.0': '@tanstack/query-core@5.80.5':
resolution: {integrity: sha512-s+epTqqLM0/TbJzMAK7OEhZIzh63P9sWz5HEFc5XHL4FvKQXQkcjI8F3nee+H/xVVn7mrP610nVXwOytTSYd0w==} resolution: {integrity: sha512-kFWXdQOUcjL/Ugk3GrI9eMuG3DsKBGaLIgyOLekR2UOrRrJgkLgPUNzZ10i8FCkfi4SgLABhOtQhx1HjoB9EZQ==}
'@tanstack/react-query@5.79.0': '@tanstack/react-query@5.80.5':
resolution: {integrity: sha512-DjC4JIYZnYzxaTzbg3osOU63VNLP67dOrWet2cZvXgmgwAXNxfS52AMq86M5++ILuzW+BqTUEVMTjhrZ7/XBuA==} resolution: {integrity: sha512-C0d+pvIahk6fJK5bXxyf36r9Ft6R9O0mwl781CjBrYGRJc76XRJcKhkVpxIo68cjMy3i47gd4O1EGooAke/OCQ==}
peerDependencies: peerDependencies:
react: ^18 || ^19 react: ^18 || ^19
@@ -4754,8 +4754,8 @@ packages:
fastq@1.17.1: fastq@1.17.1:
resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==}
fd-package-json@1.2.0: fd-package-json@2.0.0:
resolution: {integrity: sha512-45LSPmWf+gC5tdCQMNH4s9Sr00bIkiD9aN7dc5hqkrEw1geRYyDQS1v1oMHAW3ysfxfndqGsrDREHHjNNbKUfA==} resolution: {integrity: sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==}
fd-slicer@1.1.0: fd-slicer@1.1.0:
resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==}
@@ -4823,8 +4823,8 @@ packages:
resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==}
engines: {node: '>=14'} engines: {node: '>=14'}
formatly@0.2.3: formatly@0.2.4:
resolution: {integrity: sha512-WH01vbXEjh9L3bqn5V620xUAWs32CmK4IzWRRY6ep5zpa/mrisL4d9+pRVuETORVDTQw8OycSO1WC68PL51RaA==} resolution: {integrity: sha512-lIN7GpcvX/l/i24r/L9bnJ0I8Qn01qijWpQpDDvTLL29nKqSaJJu4h20+7VJ6m2CAhQ2/En/GbxDiHCzq/0MyA==}
engines: {node: '>=18.3.0'} engines: {node: '>=18.3.0'}
hasBin: true hasBin: true
@@ -5658,8 +5658,8 @@ packages:
resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
knip@5.59.1: knip@5.60.0:
resolution: {integrity: sha512-pOMBw6sLQhi/RfnpI6TwBY6NrAtKXDO5wkmMm+pCsSK5eWbVfDnDtPXbLDGNCoZPXiuAojb27y4XOpp4JPNxlA==} resolution: {integrity: sha512-r6oIbaV0Ztz/7DKe1voxg2O5IRhLi9Q0GjhplfRqUZ1gvTChew6ywmLzehuaXIHVKkPs8LF5UKOxFlc93RKzow==}
engines: {node: '>=18.18.0'} engines: {node: '>=18.18.0'}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@@ -6300,8 +6300,8 @@ packages:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
oxc-resolver@9.0.2: oxc-resolver@11.1.0:
resolution: {integrity: sha512-w838ygc1p7rF+7+h5vR9A+Y9Fc4imy6C3xPthCMkdFUgFvUWkmABeNB8RBDQ6+afk44Q60/UMMQ+gfDUW99fBA==} resolution: {integrity: sha512-/W/9O6m7lkDJMIXtXvNKXE6THIoNWwstsKpR/R8+yI9e7vC9wu92MDqLBxkgckZ2fTFmKEjozTxVibHBaRUgCA==}
p-cancelable@2.1.1: p-cancelable@2.1.1:
resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==}
@@ -7248,8 +7248,8 @@ packages:
resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
smol-toml@1.3.1: smol-toml@1.3.4:
resolution: {integrity: sha512-tEYNll18pPKHroYSmLLrksq233j021G0giwW7P3D24jC54pQ5W5BXMsQ/Mvw1OJCmEYDgY+lrzT+3nNUtoNfXQ==} resolution: {integrity: sha512-UOPtVuYkzYGee0Bd2Szz8d2G3RfMfJ2t3qVdZUAozZyAk+a0Sxa+QKix0YCwjL/A1RR0ar44nCxaoN9FxdJGwA==}
engines: {node: '>= 18'} engines: {node: '>= 18'}
snake-case@3.0.4: snake-case@3.0.4:
@@ -7403,8 +7403,8 @@ packages:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'} engines: {node: '>=8'}
strip-json-comments@5.0.1: strip-json-comments@5.0.2:
resolution: {integrity: sha512-0fk9zBqO67Nq5M/m45qHCJxylV/DhBlIOVExqgOMiCCrzrhU6tCibRXNqE3jwJLftzE9SNuZtYbpzcO+i9FiKw==} resolution: {integrity: sha512-4X2FR3UwhNUE9G49aIsJW5hRRR3GXGTBTZRMfv568O60ojM8HcWjV/VxAxCDW3SUND33O6ZY66ZuRcdkj73q2g==}
engines: {node: '>=14.16'} engines: {node: '>=14.16'}
strip-literal@3.0.0: strip-literal@3.0.0:
@@ -8036,8 +8036,9 @@ packages:
vscode-uri@3.0.8: vscode-uri@3.0.8:
resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==}
walk-up-path@3.0.1: walk-up-path@4.0.0:
resolution: {integrity: sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA==} resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==}
engines: {node: 20 || >=22}
web-streams-polyfill@3.3.3: web-streams-polyfill@3.3.3:
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
@@ -8200,6 +8201,9 @@ packages:
zod@3.24.4: zod@3.24.4:
resolution: {integrity: sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==} resolution: {integrity: sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==}
zod@3.25.51:
resolution: {integrity: sha512-TQSnBldh+XSGL+opiSIq0575wvDPqu09AqWe1F7JhUMKY+M91/aGlK4MhpVNO7MgYfHcVCB1ffwAUTJzllKJqg==}
zwitch@2.0.4: zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
@@ -9421,7 +9425,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@iconify/json@2.2.345': '@iconify/json@2.2.346':
dependencies: dependencies:
'@iconify/types': 2.0.0 '@iconify/types': 2.0.0
pathe: 1.1.2 pathe: 1.1.2
@@ -9906,45 +9910,45 @@ snapshots:
'@octokit/request-error': 6.1.8 '@octokit/request-error': 6.1.8
'@octokit/webhooks-methods': 5.1.1 '@octokit/webhooks-methods': 5.1.1
'@oxc-resolver/binding-darwin-arm64@9.0.2': '@oxc-resolver/binding-darwin-arm64@11.1.0':
optional: true optional: true
'@oxc-resolver/binding-darwin-x64@9.0.2': '@oxc-resolver/binding-darwin-x64@11.1.0':
optional: true optional: true
'@oxc-resolver/binding-freebsd-x64@9.0.2': '@oxc-resolver/binding-freebsd-x64@11.1.0':
optional: true optional: true
'@oxc-resolver/binding-linux-arm-gnueabihf@9.0.2': '@oxc-resolver/binding-linux-arm-gnueabihf@11.1.0':
optional: true optional: true
'@oxc-resolver/binding-linux-arm64-gnu@9.0.2': '@oxc-resolver/binding-linux-arm64-gnu@11.1.0':
optional: true optional: true
'@oxc-resolver/binding-linux-arm64-musl@9.0.2': '@oxc-resolver/binding-linux-arm64-musl@11.1.0':
optional: true optional: true
'@oxc-resolver/binding-linux-riscv64-gnu@9.0.2': '@oxc-resolver/binding-linux-riscv64-gnu@11.1.0':
optional: true optional: true
'@oxc-resolver/binding-linux-s390x-gnu@9.0.2': '@oxc-resolver/binding-linux-s390x-gnu@11.1.0':
optional: true optional: true
'@oxc-resolver/binding-linux-x64-gnu@9.0.2': '@oxc-resolver/binding-linux-x64-gnu@11.1.0':
optional: true optional: true
'@oxc-resolver/binding-linux-x64-musl@9.0.2': '@oxc-resolver/binding-linux-x64-musl@11.1.0':
optional: true optional: true
'@oxc-resolver/binding-wasm32-wasi@9.0.2': '@oxc-resolver/binding-wasm32-wasi@11.1.0':
dependencies: dependencies:
'@napi-rs/wasm-runtime': 0.2.10 '@napi-rs/wasm-runtime': 0.2.10
optional: true optional: true
'@oxc-resolver/binding-win32-arm64-msvc@9.0.2': '@oxc-resolver/binding-win32-arm64-msvc@11.1.0':
optional: true optional: true
'@oxc-resolver/binding-win32-x64-msvc@9.0.2': '@oxc-resolver/binding-win32-x64-msvc@11.1.0':
optional: true optional: true
'@parcel/watcher-android-arm64@2.4.1': '@parcel/watcher-android-arm64@2.4.1':
@@ -10470,11 +10474,11 @@ snapshots:
dependencies: dependencies:
remove-accents: 0.5.0 remove-accents: 0.5.0
'@tanstack/query-core@5.79.0': {} '@tanstack/query-core@5.80.5': {}
'@tanstack/react-query@5.79.0(react@19.1.0)': '@tanstack/react-query@5.80.5(react@19.1.0)':
dependencies: dependencies:
'@tanstack/query-core': 5.79.0 '@tanstack/query-core': 5.80.5
react: 19.1.0 react: 19.1.0
'@tanstack/react-router-devtools@1.120.15(@tanstack/react-router@1.120.15(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@tanstack/router-core@1.120.15)(csstype@3.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(tiny-invariant@1.3.3)': '@tanstack/react-router-devtools@1.120.15(@tanstack/react-router@1.120.15(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@tanstack/router-core@1.120.15)(csstype@3.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(tiny-invariant@1.3.3)':
@@ -10540,7 +10544,7 @@ snapshots:
'@tanstack/virtual-file-routes': 1.115.0 '@tanstack/virtual-file-routes': 1.115.0
prettier: 3.5.3 prettier: 3.5.3
tsx: 4.19.4 tsx: 4.19.4
zod: 3.24.4 zod: 3.25.51
optionalDependencies: optionalDependencies:
'@tanstack/react-router': 1.120.15(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@tanstack/react-router': 1.120.15(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -10562,7 +10566,7 @@ snapshots:
babel-dead-code-elimination: 1.0.10 babel-dead-code-elimination: 1.0.10
chokidar: 3.6.0 chokidar: 3.6.0
unplugin: 2.3.5 unplugin: 2.3.5
zod: 3.24.4 zod: 3.25.51
optionalDependencies: optionalDependencies:
'@tanstack/react-router': 1.120.15(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@tanstack/react-router': 1.120.15(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
vite: 6.3.5(@types/node@22.15.29)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.8.0) vite: 6.3.5(@types/node@22.15.29)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.8.0)
@@ -10576,10 +10580,10 @@ snapshots:
ansis: 3.12.0 ansis: 3.12.0
diff: 7.0.0 diff: 7.0.0
'@tanstack/router-zod-adapter@1.81.5(@tanstack/react-router@1.120.15(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(zod@3.24.4)': '@tanstack/router-zod-adapter@1.81.5(@tanstack/react-router@1.120.15(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(zod@3.25.51)':
dependencies: dependencies:
'@tanstack/react-router': 1.120.15(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@tanstack/react-router': 1.120.15(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
zod: 3.24.4 zod: 3.25.51
'@tanstack/store@0.7.0': {} '@tanstack/store@0.7.0': {}
@@ -12863,9 +12867,9 @@ snapshots:
dependencies: dependencies:
reusify: 1.0.4 reusify: 1.0.4
fd-package-json@1.2.0: fd-package-json@2.0.0:
dependencies: dependencies:
walk-up-path: 3.0.1 walk-up-path: 4.0.0
fd-slicer@1.1.0: fd-slicer@1.1.0:
dependencies: dependencies:
@@ -12935,9 +12939,9 @@ snapshots:
cross-spawn: 7.0.6 cross-spawn: 7.0.6
signal-exit: 4.1.0 signal-exit: 4.1.0
formatly@0.2.3: formatly@0.2.4:
dependencies: dependencies:
fd-package-json: 1.2.0 fd-package-json: 2.0.0
formdata-polyfill@4.0.10: formdata-polyfill@4.0.10:
dependencies: dependencies:
@@ -13782,20 +13786,20 @@ snapshots:
kind-of@6.0.3: {} kind-of@6.0.3: {}
knip@5.59.1(@types/node@22.15.29)(typescript@5.8.3): knip@5.60.0(@types/node@22.15.29)(typescript@5.8.3):
dependencies: dependencies:
'@nodelib/fs.walk': 1.2.8 '@nodelib/fs.walk': 1.2.8
'@types/node': 22.15.29 '@types/node': 22.15.29
fast-glob: 3.3.3 fast-glob: 3.3.3
formatly: 0.2.3 formatly: 0.2.4
jiti: 2.4.2 jiti: 2.4.2
js-yaml: 4.1.0 js-yaml: 4.1.0
minimist: 1.2.8 minimist: 1.2.8
oxc-resolver: 9.0.2 oxc-resolver: 11.1.0
picocolors: 1.1.1 picocolors: 1.1.1
picomatch: 4.0.2 picomatch: 4.0.2
smol-toml: 1.3.1 smol-toml: 1.3.4
strip-json-comments: 5.0.1 strip-json-comments: 5.0.2
typescript: 5.8.3 typescript: 5.8.3
zod: 3.24.4 zod: 3.24.4
zod-validation-error: 3.3.1(zod@3.24.4) zod-validation-error: 3.3.1(zod@3.24.4)
@@ -14581,21 +14585,21 @@ snapshots:
type-check: 0.4.0 type-check: 0.4.0
word-wrap: 1.2.5 word-wrap: 1.2.5
oxc-resolver@9.0.2: oxc-resolver@11.1.0:
optionalDependencies: optionalDependencies:
'@oxc-resolver/binding-darwin-arm64': 9.0.2 '@oxc-resolver/binding-darwin-arm64': 11.1.0
'@oxc-resolver/binding-darwin-x64': 9.0.2 '@oxc-resolver/binding-darwin-x64': 11.1.0
'@oxc-resolver/binding-freebsd-x64': 9.0.2 '@oxc-resolver/binding-freebsd-x64': 11.1.0
'@oxc-resolver/binding-linux-arm-gnueabihf': 9.0.2 '@oxc-resolver/binding-linux-arm-gnueabihf': 11.1.0
'@oxc-resolver/binding-linux-arm64-gnu': 9.0.2 '@oxc-resolver/binding-linux-arm64-gnu': 11.1.0
'@oxc-resolver/binding-linux-arm64-musl': 9.0.2 '@oxc-resolver/binding-linux-arm64-musl': 11.1.0
'@oxc-resolver/binding-linux-riscv64-gnu': 9.0.2 '@oxc-resolver/binding-linux-riscv64-gnu': 11.1.0
'@oxc-resolver/binding-linux-s390x-gnu': 9.0.2 '@oxc-resolver/binding-linux-s390x-gnu': 11.1.0
'@oxc-resolver/binding-linux-x64-gnu': 9.0.2 '@oxc-resolver/binding-linux-x64-gnu': 11.1.0
'@oxc-resolver/binding-linux-x64-musl': 9.0.2 '@oxc-resolver/binding-linux-x64-musl': 11.1.0
'@oxc-resolver/binding-wasm32-wasi': 9.0.2 '@oxc-resolver/binding-wasm32-wasi': 11.1.0
'@oxc-resolver/binding-win32-arm64-msvc': 9.0.2 '@oxc-resolver/binding-win32-arm64-msvc': 11.1.0
'@oxc-resolver/binding-win32-x64-msvc': 9.0.2 '@oxc-resolver/binding-win32-x64-msvc': 11.1.0
p-cancelable@2.1.1: {} p-cancelable@2.1.1: {}
@@ -15500,7 +15504,7 @@ snapshots:
smart-buffer@4.2.0: {} smart-buffer@4.2.0: {}
smol-toml@1.3.1: {} smol-toml@1.3.4: {}
snake-case@3.0.4: snake-case@3.0.4:
dependencies: dependencies:
@@ -15675,7 +15679,7 @@ snapshots:
strip-json-comments@3.1.1: {} strip-json-comments@3.1.1: {}
strip-json-comments@5.0.1: {} strip-json-comments@5.0.2: {}
strip-literal@3.0.0: strip-literal@3.0.0:
dependencies: dependencies:
@@ -16409,7 +16413,7 @@ snapshots:
vscode-uri@3.0.8: {} vscode-uri@3.0.8: {}
walk-up-path@3.0.1: {} walk-up-path@4.0.0: {}
web-streams-polyfill@3.3.3: {} web-streams-polyfill@3.3.3: {}
@@ -16589,4 +16593,6 @@ snapshots:
zod@3.24.4: {} zod@3.24.4: {}
zod@3.25.51: {}
zwitch@2.0.4: {} zwitch@2.0.4: {}
+1 -1
View File
@@ -10,7 +10,7 @@
"filesize": "10.1.6", "filesize": "10.1.6",
"p-retry": "6.2.1", "p-retry": "6.2.1",
"semver": "7.7.2", "semver": "7.7.2",
"zod": "3.24.4" "zod": "3.25.51"
}, },
"devDependencies": { "devDependencies": {
"@octokit/types": "14.1.0", "@octokit/types": "14.1.0",
+58
View File
@@ -0,0 +1,58 @@
name: I18N / 多语言相关
title: "[I18N] "
description: 用于多语言翻译、国际化相关问题或建议 / For issues or suggestions related to translations and internationalization
labels: ["I18n"]
type: "Task"
body:
- type: markdown
attributes:
value: |
## I18N 相关问题/建议
请用此模板提交翻译错误、缺失、建议或新增语言请求。
Please use this template for translation errors, missing translations, suggestions, or new language requests.
- type: textarea
id: description
attributes:
label: 问题描述 / Description
description: 详细描述你的 I18N 问题或建议 / Please describe your I18N issue or suggestion in detail
validations:
required: true
- type: input
id: language
attributes:
label: 相关语言 / Language
description: 例如 zh, en, jp, ru, ... / e.g. zh, en, jp, ru, ...
validations:
required: true
- type: textarea
id: suggestion
attributes:
label: 建议或修正内容 / Suggestion or Correction
description: 如果是翻译修正或建议,请填写建议的内容 / If this is a translation correction or suggestion, please provide the suggested content
validations:
required: false
- type: checkboxes
id: i18n-type
attributes:
label: 问题类型 / Issue Type
description: 请选择适用类型(可多选) / Please select the applicable type(s)
options:
- label: 翻译错误 / Translation error
- label: 翻译缺失 / Missing translation
- label: 建议优化 / Suggestion
- label: 新增语言 / New language
validations:
required: true
- type: input
id: verge-version
attributes:
label: 软件版本 / Verge Version
description: 请提供你使用的 Verge 具体版本 / Please provide the specific version of Verge you are using
validations:
required: true
+44 -4
View File
@@ -4,10 +4,21 @@ on:
# 因为 alpha 不再负责频繁构建,且需要相对于 autobuild 更稳定使用环境 # 因为 alpha 不再负责频繁构建,且需要相对于 autobuild 更稳定使用环境
# 所以不再使用 workflow_dispatch 触发 # 所以不再使用 workflow_dispatch 触发
# 应当通过 git tag 来触发构建 # 应当通过 git tag 来触发构建
# workflow_dispatch: # TODO 手动控制版本号
push: workflow_dispatch:
tags: # inputs:
- "v*.*.*-alpha*" # tag_name:
# description: "Alpha tag name (e.g. v1.2.3-alpha.1)"
# required: true
# type: string
# push:
# # 应当限制在 dev 分支上触发发布。
# branches:
# - dev
# # 应当限制 v*.*.*-alpha* 的 tag 来触发发布。
# tags:
# - "v*.*.*-alpha*"
permissions: write-all permissions: write-all
env: env:
TAG_NAME: alpha TAG_NAME: alpha
@@ -18,8 +29,37 @@ concurrency:
group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}" group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}"
jobs: jobs:
check_alpha_tag:
name: Check Alpha Tag package.json Version Consistency
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Check tag and package.json version
id: check_tag
run: |
TAG_REF="${GITHUB_REF##*/}"
echo "Current tag: $TAG_REF"
if [[ ! "$TAG_REF" =~ -alpha ]]; then
echo "Current tag is not an alpha tag."
exit 1
fi
PKG_VERSION=$(jq -r .version package.json)
echo "package.json version: $PKG_VERSION"
if [[ "$PKG_VERSION" != *alpha* ]]; then
echo "package.json version is not an alpha version."
exit 1
fi
if [[ "$TAG_REF" != "v$PKG_VERSION" ]]; then
echo "Tag ($TAG_REF) does not match package.json version (v$PKG_VERSION)."
exit 1
fi
echo "Alpha tag and package.json version are consistent."
delete_old_assets: delete_old_assets:
name: Delete Old Alpha Release Assets and Tags name: Delete Old Alpha Release Assets and Tags
needs: check_alpha_tag
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Delete Old Alpha Tags Except Latest - name: Delete Old Alpha Tags Except Latest
+65
View File
@@ -0,0 +1,65 @@
name: Cross Platform Cargo Check
on:
workflow_dispatch:
# pull_request:
# push:
# branches: [main, dev]
permissions:
contents: read
jobs:
cargo-check:
# Treat all Rust compiler warnings as errors
env:
RUSTFLAGS: "-D warnings"
strategy:
fail-fast: false
matrix:
include:
- os: macos-latest
target: aarch64-apple-darwin
- os: windows-latest
target: x86_64-pc-windows-msvc
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Add Rust Target
run: rustup target add ${{ matrix.target }}
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "20"
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Pnpm install and check
run: |
pnpm i
pnpm check ${{ matrix.target }}
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
cache-all-crates: true
cache-on-failure: true
- name: Cargo Check (deny warnings)
working-directory: src-tauri
run: |
cargo check --target ${{ matrix.target }} --workspace --all-features
+51
View File
@@ -0,0 +1,51 @@
# Copyright 2019-2024 Tauri Programme within The Commons Conservancy
# SPDX-License-Identifier: Apache-2.0
# SPDX-License-Identifier: MIT
name: Check Formatting
on:
pull_request:
jobs:
rustfmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: install Rust stable and rustfmt
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- name: run cargo fmt
run: cargo fmt --manifest-path ./src-tauri/Cargo.toml --all -- --check
prettier:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm i -g --force corepack
- uses: actions/setup-node@v4
with:
node-version: "lts/*"
cache: "pnpm"
- run: pnpm i --frozen-lockfile
- run: pnpm format:check
# taplo:
# name: taplo (.toml files)
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v4
# - name: install Rust stable
# uses: dtolnay/rust-toolchain@stable
# - name: install taplo-cli
# uses: taiki-e/install-action@v2
# with:
# tool: taplo-cli
# - run: taplo fmt --check --diff
+24 -3
View File
@@ -5,9 +5,10 @@ on:
# ! 不再使用 workflow_dispatch 触发。 # ! 不再使用 workflow_dispatch 触发。
# workflow_dispatch: # workflow_dispatch:
push: push:
# ? 应当限制在 main 分支上触发发布。 # 应当限制在 main 分支上触发发布。
# branches: branches:
# - main - main
# 应当限制 v*.*.* 的 tag 触发发布。
tags: tags:
- "v*.*.*" - "v*.*.*"
permissions: write-all permissions: write-all
@@ -20,8 +21,28 @@ concurrency:
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs: jobs:
check_tag_version:
name: Check Release Tag and package.json Version Consistency
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Check tag and package.json version
run: |
TAG_REF="${GITHUB_REF##*/}"
echo "Current tag: $TAG_REF"
PKG_VERSION=$(jq -r .version package.json)
echo "package.json version: $PKG_VERSION"
if [[ "$TAG_REF" != "v$PKG_VERSION" ]]; then
echo "Tag ($TAG_REF) does not match package.json version (v$PKG_VERSION)."
exit 1
fi
echo "Tag and package.json version are consistent."
release: release:
name: Release Build name: Release Build
needs: check_tag_version
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
+16 -8
View File
@@ -2,15 +2,23 @@
#pnpm pretty-quick --staged #pnpm pretty-quick --staged
# 运行 clippy fmt if git diff --cached --name-only | grep -q '^src/'; then
cd src-tauri pnpm format:check
cargo fmt if [ $? -ne 0 ]; then
echo "Code format check failed in src/. Please fix formatting issues."
if [ $? -ne 0 ]; then exit 1
echo "rustfmt failed to format the code. Please fix the issues and try again." fi
exit 1 fi
if git diff --cached --name-only | grep -q '^src-tauri/'; then
cd src-tauri
cargo fmt
if [ $? -ne 0 ]; then
echo "rustfmt failed to format the code. Please fix the issues and try again."
exit 1
fi
cd ..
fi fi
cd ..
git add . git add .
+7
View File
@@ -0,0 +1,7 @@
# README.md
# UPDATELOG.md
# CONTRIBUTING.md
pnpm-lock.yaml
src-tauri/target/
+6
View File
@@ -0,0 +1,6 @@
{
"singleQuote": true,
"semi": false,
"trailingComma": "none",
"experimentalOperatorPosition": "start"
}
+16 -8
View File
@@ -33,12 +33,15 @@ npm install pnpm -g
``` ```
### Install Dependencies ### Install Dependencies
Install node packages Install node packages
```shell ```shell
pnpm install pnpm install
``` ```
Install apt packages ONLY for Ubuntu Install apt packages ONLY for Ubuntu
```shell ```shell
apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
``` ```
@@ -105,20 +108,25 @@ pnpm portable
If you changed the rust code, it's recommanded to execute code style formatting and quailty checks. If you changed the rust code, it's recommanded to execute code style formatting and quailty checks.
1. Code style formatting 1. Code quailty checks
```bash ```bash
# For rust backend
$ clash-verge-rev: pnpm clippy
# For frontend (not yet).
```
2. Code style formatting
```bash
# For rust backend
$ clash-verge-rev: cd src-tauri $ clash-verge-rev: cd src-tauri
$ clash-verge-rev/src-tauri: cargo fmt $ clash-verge-rev/src-tauri: cargo fmt
# For frontend
$ clash-verge-rev: pnpm format:check
$ clash-verge-rev: pnpm format
``` ```
2. Code quailty checks
```bash
$ clash-verge-rev: pnpm clippy
```
Once you have made your changes: Once you have made your changes:
1. Fork the repository. 1. Fork the repository.
+7 -6
View File
@@ -23,11 +23,11 @@ Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
#### 我应当怎样选择发行版 #### 我应当怎样选择发行版
| 版本 | 特征 | 链接 | | 版本 | 特征 | 链接 |
|:-----|:-----|:-----| | :-------- | :--------------------------------------- | :------------------------------------------------------------------------------------- |
|Stable|正式版,高可靠性,适合日常使用。|[Release](https://github.com/clash-verge-rev/clash-verge-rev/releases) | | Stable | 正式版,高可靠性,适合日常使用。 | [Release](https://github.com/clash-verge-rev/clash-verge-rev/releases) |
|Alpha|早期测试版,功能未完善,可能存在缺陷。|[Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha)| | Alpha | 早期测试版,功能未完善,可能存在缺陷。 | [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) |
|AutoBuild|滚动更新版,持续集成更新,适合开发测试。|[AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild)| | AutoBuild | 滚动更新版,持续集成更新,适合开发测试。 | [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) |
#### 安装说明和常见问题,请到 [文档页](https://clash-verge-rev.github.io/) 查看 #### 安装说明和常见问题,请到 [文档页](https://clash-verge-rev.github.io/) 查看
@@ -49,11 +49,12 @@ Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
- 解锁流媒体及 ChatGPT - 解锁流媒体及 ChatGPT
- 官网:[https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6) - 官网:[https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
#### 本项目的构建与发布环境由 [YXVM](https://yxvm.com/aff.php?aff=827) 独立服务器全力支持, #### 本项目的构建与发布环境由 [YXVM](https://yxvm.com/aff.php?aff=827) 独立服务器全力支持,
感谢提供 独享资源、高性能、高速网络 的强大后端环境。如果你觉得下载够快、使用够爽,那是因为我们用了好服务器! 感谢提供 独享资源、高性能、高速网络 的强大后端环境。如果你觉得下载够快、使用够爽,那是因为我们用了好服务器!
🧩 YXVM 独立服务器优势: 🧩 YXVM 独立服务器优势:
- 🌎 优质网络,回程优化,下载快到飞起 - 🌎 优质网络,回程优化,下载快到飞起
- 🔧 物理机独享资源,非VPS可比,性能拉满 - 🔧 物理机独享资源,非VPS可比,性能拉满
- 🧠 适合跑代理、搭建 WEB 站 CDN 站 、搞 CI/CD 或任何高负载应用 - 🧠 适合跑代理、搭建 WEB 站 CDN 站 、搞 CI/CD 或任何高负载应用
+152 -122
View File
@@ -3,141 +3,153 @@
尽管外部控制密钥已自动补全默认值且不允许为空。仍然推荐自行修改外部控制密钥。 尽管外部控制密钥已自动补全默认值且不允许为空。仍然推荐自行修改外部控制密钥。
#### ⚠️ 已知问题 #### ⚠️ 已知问题
- 仅在Ubuntu 22.04/24.04Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优
- MacOS 下 墙贴主要为浅色,Tray 图标深色时图标闪烁;彩色 Tray 速率颜色淡 - 仅在Ubuntu 22.04/24.04Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优
- 窗口状态管理器已确定上游存在缺陷,暂时移除。当前不再内置窗口大小和位置记忆。 - MacOS 下 墙贴主要为浅色,Tray 图标深色时图标闪烁;彩色 Tray 速率颜色淡
- MacOS 下卸载服务后需手动重启软件才能与内核通信 - 窗口状态管理器已确定上游存在缺陷,暂时移除。当前不再内置窗口大小和位置记忆
- MacOS 下卸载服务后需手动重启软件才能与内核通信。
### 2.3.0 相对于 2.2.3 ### 2.3.0 相对于 2.2.3
#### 🐞 修复问题 #### 🐞 修复问题
- 首页"代理模式"快速切换导致的卡死问题
- 解锁测试报错信息 - 首页"代理模式"快速切换导致的卡死问题
- Macos 快捷键关闭窗口无法启用自动轻量模式 - 解锁测试报错信息
- 静默启动异常窗口创建和关闭流程 - Macos 快捷键关闭窗口无法启用自动轻量模式
- Windows 错误的全局快捷键 `Ctrl+Q` 注册 - 静默启动异常窗口创建和关闭流程
- Vless URL 解码时网络类型错误 - Windows 错误的全局快捷键 `Ctrl+Q` 注册
- 切换自定义代理地址导致系统代理状态异常 - Vless URL 解码时网络类型错误
- Macos TUN 默认无效网卡名称 - 切换自定义代理地址导致系统代理状态异常
- 托盘更改订阅后 UI 不同步的问题 - Macos TUN 默认无效网卡名称
- 修复提权漏洞,改用带认证的 IPC 通信 - 托盘更改订阅后 UI 不同步的问题
- 编辑器中连字符问题 - 修复提权漏洞,改用带认证的 IPC 通信
- 安装服务模式后无法立即开启 TUN 模式 - 编辑器中连字符问题
- 同步更新多语言翻译 - 安装服务模式后无法立即开启 TUN 模式
- 修复 .window-state.json 无法删除的问题 - 同步更新多语言翻译
- 无法修改配置更新 HTTP 请求超时 - 修复 .window-state.json 无法删除的问题
- 修复 getDelayFix 钩子问题 - 无法修改配置更新 HTTP 请求超时
- 使用外部扩展脚本覆写代理组时首页无法显示代理组 - 修复 getDelayFix 钩子问题
- 导出诊断 Verge 版本与设置页面不同步 - 使用外部扩展脚本覆写代理组时首页无法显示代理组
- 切换语言时可能造成设置页面无法加载 - 导出诊断 Verge 版本与设置页面不同步
- 切换语言时可能造成设置页面无法加载
#### ✨ 新增功能 #### ✨ 新增功能
- Mihomo(Meta)内核升级至 1.19.10
- 允许代理主机地址设置为非 127.0.0.1 对 WSL 代理友好 - Mihomo(Meta)内核升级至 1.19.10
- 关闭系统代理时关闭已建立的网络连接 - 允许代理主机地址设置为非 127.0.0.1 对 WSL 代理友好
- 托盘显示当前轻量模式状态 - 关闭系统代理时关闭已建立的网络连接
- Webdav 请求加入 UA - 托盘显示当前轻量模式状态
- Webdav 支持目录重定向 - Webdav 请求加入 UA
- Webdav 备份目录检查和文件上传重试机制 - Webdav 支持目录重定向
- 系统代理守卫可检查意外设置变更并恢复 - Webdav 备份目录检查和文件上传重试机制
- 定时自动订阅更新也能自动回退使用代理 - 系统代理守卫可检查意外设置变更并恢复
- 订阅请求超时机制,防止订阅更新的时候卡死 - 定时自动订阅更新也能自动回退使用代理
- 订阅卡片点击时间可切换下次自动更新时间,自动更新触发后页面有明确的成功与否提示 - 订阅请求超时机制,防止订阅更新的时候卡死
- 添加网络管理器以优化网络请求处理,防止资源竞争导致的启动时 UI 卡死 - 订阅卡片点击时间可切换下次自动更新时间,自动更新触发后页面有明确的成功与否提示
- 更新依赖,替换弃用元素 - 添加网络管理器以优化网络请求处理,防止资源竞争导致的启动时 UI 卡死
- 首页当前节点增加排序功能 - 更新依赖,替换弃用元素
- DNS 覆写下增加 Hosts 设置功能 - 首页当前节点增加排序功能
- 修复服务模式安装后无法立即开启 TUN 模式的问题 - DNS 覆写下增加 Hosts 设置功能
- 支持手动卸载服务模式,回退到 Sidecar 模式 - 修复服务模式安装后无法立即开启 TUN 模式的问题
- 添加了土耳其语,日本语,德语,西班牙语,繁体中文的支持 - 支持手动卸载服务模式,回退到 Sidecar 模式
- 卸载服务的按钮 - 添加了土耳其语,日本语,德语,西班牙语,繁体中文的支持
- 添加了Zashboard的一键跳转URL - 卸载服务的按钮
- 使用操作系统默认的窗口管理器 - 添加了Zashboard的一键跳转URL
- 切换、升级、重启内核的状态管理 - 使用操作系统默认的窗口管理
- 更精细化控制自动日志清理,新增1天选项 - 切换、升级、重启内核的状态管理
- Winodws 快捷键名称改为 `Clash Verge` - 更精细化控制自动日志清理,新增1天选项
- 配置加载阶段自动补全 external-controller secret 字段。 - Winodws 快捷键名称改为 `Clash Verge`
- 配置加载阶段自动补全 external-controller secret 字段。
#### 🚀 优化改进 #### 🚀 优化改进
- 系统代理 Bypass 设置
- Windows 下使用 Startup 文件夹的方式实现开机自启,解决管理员模式下开机自启的各种问题 - 系统代理 Bypass 设置
- 切换到规则页面时自动刷新规则数据 - Windows 下使用 Startup 文件夹的方式实现开机自启,解决管理员模式下开机自启的各种问题
- 重构更新失败回退机制,使用后端完成更新失败后回退到使用 Clash 代理再次尝试更新 - 切换到规则页面时自动刷新规则数据
- 编辑非激活订阅的时候不在触发当前订阅配置重载 - 重构更新失败回退机制,使用后端完成更新失败后回退到使用 Clash 代理再次尝试更新
- 改进核心功能防止主进程阻塞、改进MihomoManager实现,以及优化窗口创建流程 - 编辑非激活订阅的时候不在触发当前订阅配置重载
- 优化系统代理设置更新逻辑 - 改进核心功能防止主进程阻塞、改进MihomoManager实现,以及优化窗口创建流程
- 重构前端通知系统分离通知线程防止前端卡死 - 优化系统代理设置更新逻辑
- 优化网络请求和错误处理 - 重构前端通知系统分离通知线程防止前端卡死
- 重构通知系统 - 优化网络请求和错误处理
- 使用异步方法重构 UI 启动逻辑,解决启动软件过程中的各种卡死问题 - 重构通知系统
- MacOS 下默认关闭托盘速率显示 - 使用异步方法重构 UI 启动逻辑,解决启动软件过程中的各种卡死问题
- 优化服务操作流程,提升系统服务相关操作的稳定性和用户体验 - MacOS 下默认关闭托盘速率显示
- 优化了其他语言的翻译问题 - 优化服务操作流程,提升系统服务相关操作的稳定性和用户体验
- Mihomo 内核默认日志等级为 warn - 优化了其他语言的翻译问题
- Clash Verge Rev 应用默认日志等级为 warn - Mihomo 内核默认日志等级为 warn
- 重构了原来的 IP 信息请求重试机制,采用轮询检测,解决了 Network Error 和超时问题 - Clash Verge Rev 应用默认日志等级为 warn
- 对轮询检测机制进行了优化,引入洗牌算法来增强随机性 - 重构了原来的 IP 信息请求重试机制,采用轮询检测,解决了 Network Error 和超时问题
-获取系统信息的流程进行了优化,并添加了去重检测机制,确保剔除重复的信息 -轮询检测机制进行了优化,引入洗牌算法来增强随机性
- 优化窗口状态初始化逻辑和添加缺失的权限设置 - 对获取系统信息的流程进行了优化,并添加了去重检测机制,确保剔除重复的信息
- 异步化配置:优化端口查找和配置保存逻辑 - 优化窗口状态初始化逻辑和添加缺失的权限设置
- 重构事件通知机制到独立线程,避免前端卡死 - 异步化配置:优化端口查找和配置保存逻辑
- 优化端口设置,每个端口可随机设置端口号 - 重构事件通知机制到独立线程,避免前端卡死
- 优化了保存机制,使用平滑函数,防止客户端卡死 - 优化端口设置,每个端口可随机设置端口号
- 优化端口设置退出和保存机制 - 优化了保存机制,使用平滑函数,防止客户端卡死
- 强制为 Mihomo 配置补全并覆盖 external-controller-cors 字段,默认不允许跨域和仅本地请求,提升 cors 安全性,升级配置时自动覆盖 - 优化端口设置退出和保存机制
- 修改 端口检测范围 (1111-65536 - 强制为 Mihomo 配置补全并覆盖 external-controller-cors 字段,默认不允许跨域和仅本地请求,提升 cors 安全性,升级配置时自动覆盖
- 配置文件缺失 secret 字段时自动填充默认值 set-your-secret - 修改 端口检测范围 (1111-65536
- 优化异步处理,防止部分组件 UI 阻塞 - 配置文件缺失 secret 字段时自动填充默认值 set-your-secret
- 关闭 DNS 启用 - 优化异步处理,防止部分组件 UI 阻塞
- 延迟测试链接更换为 Https 协议 https://cp.cloudflare.com/generate_204 - 关闭 DNS 启用
- 延迟测试链接更换为 Https 协议 https://cp.cloudflare.com/generate_204
#### 🗑️ 移除内容 #### 🗑️ 移除内容
- 窗口状态管理器
- Webdav 跨平台备份恢复限制 - 窗口状态管理器
- Webdav 跨平台备份恢复限制
## v2.2.3 ## v2.2.3
#### 已知问题 #### 已知问题
- 仅在Ubuntu 22.04/24.04Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优
- MacOS 自定义图标与速率显示推荐图标尺寸为 256x256。其他尺寸(可能)会导致不正常图标和速率间隙 - 仅在Ubuntu 22.04/24.04Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优
- MacOS 下 墙贴主要为浅色,Tray 图标深色时图标闪烁;彩色 Tray 速率颜色淡 - MacOS 自定义图标与速率显示推荐图标尺寸为 256x256。其他尺寸(可能)会导致不正常图标和速率间隙
- Linux 下 Clash Verge Rev 内存占用显著高于 Windows / MacOS - MacOS 下 墙贴主要为浅色,Tray 图标深色时图标闪烁;彩色 Tray 速率颜色淡
- Linux 下 Clash Verge Rev 内存占用显著高于 Windows / MacOS
### 2.2.3 相对于 2.2.2 ### 2.2.3 相对于 2.2.2
#### 修复了: #### 修复了:
- 首页“当前代理”因为重复刷新导致的CPU占用过高的问题
- “开机自启”和“DNS覆写”开关跳动问题 - 首页“当前代理”因为重复刷新导致的CPU占用过高的问题
- 自定义托盘图标未能应用更改 - “开机自启”和“DNS覆写”开关跳动问题
- MacOS 自定义托盘图标显示速率时图标和文本间隙过大 - 自定义托盘图标未能应用更改
- MacOS 托盘速率显示不全 - MacOS 自定义托盘图标显示速率时图标和文本间隙过大
- Linux 在系统服务模式下无法拉起 Mihomo 内核 - MacOS 托盘速率显示不全
- 使用异步操作,避免获取系统信息和切换代理模式可能带来的崩溃 - Linux 在系统服务模式下无法拉起 Mihomo 内核
- 相同节点名称可能导致的页面渲染出错 - 使用异步操作,避免获取系统信息和切换代理模式可能带来的崩溃
- URL Schemes被截断的问题 - 相同节点名称可能导致的页面渲染出错
- 首页流量统计卡更好的时间戳范围 - URL Schemes被截断的问题
- 静默启动无法触发自动轻量化计时器 - 首页流量统计卡更好的时间戳范围
- 静默启动无法触发自动轻量化计时器
#### 新增了: #### 新增了:
- Mihomo(Meta)内核升级至 1.19.4
- Clash Verge Rev 从现在开始不再强依赖系统服务和管理权限 - Mihomo(Meta)内核升级至 1.19.4
- 支持根据用户偏好选择Sidecar(用户空间)模式或安装服务 - Clash Verge Rev 从现在开始不再强依赖系统服务和管理权限
- 增加载入初始配置文件的错误提示,防止切换到错误的订阅配置 - 支持根据用户偏好选择Sidecar(用户空间)模式或安装服务
- 检测是否以管理员模式运行软件,如果是提示无法使用开机自启 - 增加载入初始配置文件的错误提示,防止切换到错误的订阅配置
- 代理组显示节点数量 - 检测是否以管理员模式运行软件,如果是提示无法使用开机自启
- 统一运行模式检测,支持管理员模式下开启TUN模式 - 代理组显示节点数量
- 托盘切换代理模式会根据设置自动断开之前连接 - 统一运行模式检测,支持管理员模式下开启TUN模式
- 如订阅获取失败回退使用Clash内核代理再次尝试 - 托盘切换代理模式会根据设置自动断开之前连接
- 如订阅获取失败回退使用Clash内核代理再次尝试
#### 移除了: #### 移除了:
- 实时保存窗口位置和大小。这个功能可能会导致窗口异常大小和位置,还需观察。
- 实时保存窗口位置和大小。这个功能可能会导致窗口异常大小和位置,还需观察。
#### 优化了: #### 优化了:
- 重构了后端内核管理逻辑,更轻量化和有效的管理内核,提高了性能和稳定性
- 前端统一刷新应用数据,优化数据获取和刷新逻辑 - 重构了后端内核管理逻辑,更轻量化和有效的管理内核,提高了性能和稳定性
- 优化首页流量图表代码,调整图表文字边距 - 前端统一刷新应用数据,优化数据获取和刷新逻辑
- MacOS 托盘速率更好的显示样式和更新逻辑 - 优化首页流量图表代码,调整图表文字边距
- 首页仅在有流量图表时显示流量图表区域 - MacOS 托盘速率更好的显示样式和更新逻辑
- 更新DNS默认覆写配置 - 首页仅在有流量图表时显示流量图表区域
- 移除测试目录,简化资源初始化逻辑 - 更新DNS默认覆写配置
- 移除测试目录,简化资源初始化逻辑
## v2.2.2 ## v2.2.2
@@ -148,23 +160,29 @@
代号释义: 本次发布在功能上的大幅扩展。新首页设计为用户带来全新交互体验,DNS 覆写功能增强网络控制能力,解锁测试页面助力内容访问自由度提升,轻量模式提供灵活使用选择。此外,macOS 应用菜单集成、sidecar 模式、诊断信息导出等新特性进一步丰富了软件的适用场景。这些新增功能显著拓宽了 Clash Verge 的功能边界,为用户提供了更强大的工具和可能性。 代号释义: 本次发布在功能上的大幅扩展。新首页设计为用户带来全新交互体验,DNS 覆写功能增强网络控制能力,解锁测试页面助力内容访问自由度提升,轻量模式提供灵活使用选择。此外,macOS 应用菜单集成、sidecar 模式、诊断信息导出等新特性进一步丰富了软件的适用场景。这些新增功能显著拓宽了 Clash Verge 的功能边界,为用户提供了更强大的工具和可能性。
#### 已知问题 #### 已知问题
- 仅在Ubuntu 22.04/24.04Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优
- 仅在Ubuntu 22.04/24.04Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优
### 2.2.2 相对于 2.2.1(已下架不再提供) ### 2.2.2 相对于 2.2.1(已下架不再提供)
#### 修复了: #### 修复了:
- 弹黑框的问题(原因是服务崩溃触发重装机制)
- MacOS进入轻量模式以后隐藏Dock图标 - 弹黑框的问题(原因是服务崩溃触发重装机制)
- 增加轻量模式缺失的tray翻译 - MacOS进入轻量模式以后隐藏Dock图标
- Linux下的窗口边框被削掉的问题 - 增加轻量模式缺失的tray翻译
- Linux下的窗口边框被削掉的问题
#### 新增了: #### 新增了:
- 加强服务检测和重装逻辑
- 增强内核与服务保活机制 - 加强服务检测和重装逻辑
-加服务模式下的僵尸进程清理机制 -强内核与服务保活机制
- 新增当服务模式多次尝试失败后自动回退至用户空间模式 - 增加服务模式下的僵尸进程清理机制
- 新增当服务模式多次尝试失败后自动回退至用户空间模式
### 2.2.1 相对于 2.2.0(已下架不再提供) ### 2.2.1 相对于 2.2.0(已下架不再提供)
#### 修复了: #### 修复了:
1. **首页** 1. **首页**
- 修复 Direct 模式首页无法渲染 - 修复 Direct 模式首页无法渲染
- 修复 首页启用轻量模式导致 ClashVergeRev 从托盘退出 - 修复 首页启用轻量模式导致 ClashVergeRev 从托盘退出
@@ -181,6 +199,7 @@
- 修复 MacOS 轻量模式下 Dock 栏图标无法隐藏。 - 修复 MacOS 轻量模式下 Dock 栏图标无法隐藏。
#### 新增了: #### 新增了:
1. **首页** 1. **首页**
- 首页文本过长自动截断 - 首页文本过长自动截断
2. **轻量模式** 2. **轻量模式**
@@ -197,7 +216,9 @@
## 2.2.0(已下架不再提供) ## 2.2.0(已下架不再提供)
#### 新增功能 #### 新增功能
1. **首页** 1. **首页**
- 新增首页功能,默认启动页面改为首页。 - 新增首页功能,默认启动页面改为首页。
- 首页流量图卡片显示上传/下载名称。 - 首页流量图卡片显示上传/下载名称。
- 首页支持轻量模式切换。 - 首页支持轻量模式切换。
@@ -205,17 +226,21 @@
- 限制首页配置文件卡片URL长度。 - 限制首页配置文件卡片URL长度。
2. **DNS 设置与覆写** 2. **DNS 设置与覆写**
- 新增 DNS 覆写功能。 - 新增 DNS 覆写功能。
- 默认启用 DNS 覆写。 - 默认启用 DNS 覆写。
3. **解锁测试** 3. **解锁测试**
- 新增解锁测试页面。 - 新增解锁测试页面。
4. **轻量模式** 4. **轻量模式**
- 新增轻量模式及设置。 - 新增轻量模式及设置。
- 添加自动轻量模式定时器。 - 添加自动轻量模式定时器。
5. **系统支持** 5. **系统支持**
- Mihomo(meta)内核升级 1.19.3 - Mihomo(meta)内核升级 1.19.3
- macOS 支持 CMD+W 关闭窗口。 - macOS 支持 CMD+W 关闭窗口。
- 新增 macOS 应用菜单。 - 新增 macOS 应用菜单。
@@ -228,7 +253,9 @@
- 新增代理命令。 - 新增代理命令。
#### 修复 #### 修复
1. **系统** 1. **系统**
- 修复 Windows 热键崩溃。 - 修复 Windows 热键崩溃。
- 修复 macOS 无框标题。 - 修复 macOS 无框标题。
- 修复 macOS 静默启动崩溃。 - 修复 macOS 静默启动崩溃。
@@ -241,7 +268,9 @@
- 修复构建失败问题。 - 修复构建失败问题。
#### 优化 #### 优化
1. **性能** 1. **性能**
- 重构后端,巨幅性能优化。 - 重构后端,巨幅性能优化。
- 优化首页组件性能。 - 优化首页组件性能。
- 优化流量图表资源使用。 - 优化流量图表资源使用。
@@ -254,6 +283,7 @@
- 优化修改verge配置性能。 - 优化修改verge配置性能。
2. **重构** 2. **重构**
- 重构后端,巨幅性能优化。 - 重构后端,巨幅性能优化。
- 优化定时器管理。 - 优化定时器管理。
- 重构 MihomoManager 处理流量。 - 重构 MihomoManager 处理流量。
+35 -33
View File
@@ -1,6 +1,6 @@
{ {
"name": "clash-verge", "name": "clash-verge",
"version": "2.3.0-alpha", "version": "2.3.0",
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"scripts": { "scripts": {
"dev": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev", "dev": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev",
@@ -21,7 +21,9 @@
"publish-version": "node scripts/publish-version.mjs", "publish-version": "node scripts/publish-version.mjs",
"prepare": "husky", "prepare": "husky",
"fmt": "cargo fmt --manifest-path ./src-tauri/Cargo.toml", "fmt": "cargo fmt --manifest-path ./src-tauri/Cargo.toml",
"clippy": "cargo clippy --manifest-path ./src-tauri/Cargo.toml" "clippy": "cargo clippy --manifest-path ./src-tauri/Cargo.toml",
"format": "prettier --write .",
"format:check": "prettier --check ."
}, },
"dependencies": { "dependencies": {
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",
@@ -30,15 +32,15 @@
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0", "@emotion/styled": "^11.14.0",
"@juggle/resize-observer": "^3.4.0", "@juggle/resize-observer": "^3.4.0",
"@mui/icons-material": "^7.0.2", "@mui/icons-material": "^7.1.1",
"@mui/lab": "7.0.0-beta.12", "@mui/lab": "7.0.0-beta.13",
"@mui/material": "^7.0.2", "@mui/material": "^7.1.1",
"@mui/x-data-grid": "^8.2.0", "@mui/x-data-grid": "^8.5.0",
"@tauri-apps/api": "2.5.0", "@tauri-apps/api": "2.5.0",
"@tauri-apps/plugin-clipboard-manager": "^2.2.2", "@tauri-apps/plugin-clipboard-manager": "^2.2.2",
"@tauri-apps/plugin-dialog": "^2.2.1", "@tauri-apps/plugin-dialog": "^2.2.2",
"@tauri-apps/plugin-fs": "^2.2.1", "@tauri-apps/plugin-fs": "^2.3.0",
"@tauri-apps/plugin-global-shortcut": "^2.2.0", "@tauri-apps/plugin-global-shortcut": "^2.2.1",
"@tauri-apps/plugin-notification": "^2.2.2", "@tauri-apps/plugin-notification": "^2.2.2",
"@tauri-apps/plugin-process": "^2.2.1", "@tauri-apps/plugin-process": "^2.2.1",
"@tauri-apps/plugin-shell": "2.2.1", "@tauri-apps/plugin-shell": "2.2.1",
@@ -46,61 +48,61 @@
"@tauri-apps/plugin-window-state": "^2.2.2", "@tauri-apps/plugin-window-state": "^2.2.2",
"@types/d3-shape": "^3.1.7", "@types/d3-shape": "^3.1.7",
"@types/json-schema": "^7.0.15", "@types/json-schema": "^7.0.15",
"ahooks": "^3.8.4", "ahooks": "^3.8.5",
"axios": "^1.8.3", "axios": "^1.9.0",
"chart.js": "^4.4.9", "chart.js": "^4.4.9",
"cli-color": "^2.0.4", "cli-color": "^2.0.4",
"d3-shape": "^3.2.0", "d3-shape": "^3.2.0",
"dayjs": "1.11.13", "dayjs": "1.11.13",
"foxact": "^0.2.44", "foxact": "^0.2.45",
"glob": "^11.0.1", "glob": "^11.0.2",
"i18next": "^25.0.2", "i18next": "^25.2.1",
"js-base64": "^3.7.7", "js-base64": "^3.7.7",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"monaco-editor": "^0.52.2", "monaco-editor": "^0.52.2",
"monaco-yaml": "^5.3.1", "monaco-yaml": "^5.4.0",
"nanoid": "^5.1.5", "nanoid": "^5.1.5",
"peggy": "^5.0.0", "peggy": "^5.0.3",
"react": "19.1.0", "react": "19.1.0",
"react-chartjs-2": "^5.3.0", "react-chartjs-2": "^5.3.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"react-error-boundary": "6.0.0", "react-error-boundary": "6.0.0",
"react-hook-form": "^7.54.2", "react-hook-form": "^7.57.0",
"react-i18next": "15.5.1", "react-i18next": "15.5.2",
"react-markdown": "10.1.0", "react-markdown": "10.1.0",
"react-monaco-editor": "0.58.0", "react-monaco-editor": "0.58.0",
"react-router-dom": "7.6.0", "react-router-dom": "7.6.2",
"react-virtuoso": "^4.12.7", "react-virtuoso": "^4.12.8",
"sockette": "^2.0.6", "sockette": "^2.0.6",
"swr": "^2.3.3", "swr": "^2.3.3",
"tar": "^7.4.3", "tar": "^7.4.3",
"types-pac": "^1.0.3", "types-pac": "^1.0.3",
"zustand": "^5.0.3" "zustand": "^5.0.5"
}, },
"devDependencies": { "devDependencies": {
"@actions/github": "^6.0.0", "@actions/github": "^6.0.1",
"@tauri-apps/cli": "2.2.7", "@tauri-apps/cli": "2.5.0",
"@types/js-cookie": "^3.0.6", "@types/js-cookie": "^3.0.6",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/react": "19.1.4", "@types/react": "19.1.6",
"@types/react-dom": "19.1.5", "@types/react-dom": "19.1.6",
"@vitejs/plugin-legacy": "^6.0.2", "@vitejs/plugin-legacy": "^6.1.1",
"@vitejs/plugin-react": "4.4.1", "@vitejs/plugin-react": "4.5.1",
"adm-zip": "^0.5.16", "adm-zip": "^0.5.16",
"commander": "^14.0.0", "commander": "^14.0.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"https-proxy-agent": "^7.0.6", "https-proxy-agent": "^7.0.6",
"husky": "^9.1.7", "husky": "^9.1.7",
"meta-json-schema": "^1.19.3", "meta-json-schema": "^1.19.10",
"node-fetch": "^3.3.2", "node-fetch": "^3.3.2",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"pretty-quick": "^4.1.1", "pretty-quick": "^4.2.2",
"sass": "^1.86.0", "sass": "^1.89.1",
"terser": "^5.39.0", "terser": "^5.40.0",
"typescript": "^5.8.2", "typescript": "^5.8.3",
"vite": "^6.2.2", "vite": "^6.3.5",
"vite-plugin-monaco-editor": "^1.1.0", "vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-svgr": "^4.3.0" "vite-plugin-svgr": "^4.3.0"
}, },
+287 -314
View File
File diff suppressed because it is too large Load Diff
+42
View File
@@ -0,0 +1,42 @@
{
"extends": ["config:recommended"],
"baseBranches": ["dev"],
"enabledManagers": ["cargo", "npm"],
"labels": ["dependencies"],
"ignorePaths": [
"**/node_modules/**",
"**/bower_components/**",
"**/vendor/**",
"**/__tests__/**",
"**/test/**",
"**/tests/**",
"**/__fixtures__/**",
"**/crate/**",
"shared/**"
],
"rangeStrategy": "bump",
"packageRules": [
{
"semanticCommitType": "chore",
"matchPackageNames": ["*"]
},
{
"description": "Disable node/pnpm version updates",
"matchPackageNames": ["node", "pnpm"],
"matchDepTypes": ["engines", "packageManager"],
"enabled": false
},
{
"description": "Group all cargo dependencies into a single PR",
"matchManagers": ["cargo"],
"groupName": "cargo dependencies"
},
{
"description": "Group all npm dependencies into a single PR",
"matchManagers": ["npm"],
"groupName": "npm dependencies"
}
],
"postUpdateOptions": ["pnpmDedupe"],
"ignoreDeps": ["serde_yaml"]
}
+30 -26
View File
@@ -1,21 +1,21 @@
import fs from 'fs'; import fs from "fs";
import path from 'path'; import path from "path";
import { fileURLToPath } from 'url'; import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
const LOCALES_DIR = path.resolve(__dirname, '../src/locales'); const LOCALES_DIR = path.resolve(__dirname, "../src/locales");
const SRC_DIRS = [ const SRC_DIRS = [
path.resolve(__dirname, '../src'), path.resolve(__dirname, "../src"),
path.resolve(__dirname, '../src-tauri') path.resolve(__dirname, "../src-tauri"),
]; ];
const exts = ['.js', '.ts', '.tsx', '.jsx', '.vue', '.rs']; const exts = [".js", ".ts", ".tsx", ".jsx", ".vue", ".rs"];
// 递归获取所有文件 // 递归获取所有文件
function getAllFiles(dir, exts) { function getAllFiles(dir, exts) {
let files = []; let files = [];
fs.readdirSync(dir).forEach(file => { fs.readdirSync(dir).forEach((file) => {
const full = path.join(dir, file); const full = path.join(dir, file);
if (fs.statSync(full).isDirectory()) { if (fs.statSync(full).isDirectory()) {
files = files.concat(getAllFiles(full, exts)); files = files.concat(getAllFiles(full, exts));
@@ -28,21 +28,21 @@ function getAllFiles(dir, exts) {
// 读取所有源码内容为一个大字符串 // 读取所有源码内容为一个大字符串
function getAllSourceContent() { function getAllSourceContent() {
const files = SRC_DIRS.flatMap(dir => getAllFiles(dir, exts)); const files = SRC_DIRS.flatMap((dir) => getAllFiles(dir, exts));
return files.map(f => fs.readFileSync(f, 'utf8')).join('\n'); return files.map((f) => fs.readFileSync(f, "utf8")).join("\n");
} }
// 白名单 key,不检查这些 key 是否被使用 // 白名单 key,不检查这些 key 是否被使用
const WHITELIST_KEYS = [ const WHITELIST_KEYS = [
'theme.light', "theme.light",
'theme.dark', "theme.dark",
'theme.system', "theme.system",
"Already Using Latest Core Version" "Already Using Latest Core Version",
]; ];
// 主流程 // 主流程
function processI18nFile(i18nPath, lang, allSource) { function processI18nFile(i18nPath, lang, allSource) {
const i18n = JSON.parse(fs.readFileSync(i18nPath, 'utf8')); const i18n = JSON.parse(fs.readFileSync(i18nPath, "utf8"));
const keys = Object.keys(i18n); const keys = Object.keys(i18n);
const used = {}; const used = {};
@@ -50,7 +50,7 @@ function processI18nFile(i18nPath, lang, allSource) {
let checked = 0; let checked = 0;
const total = keys.length; const total = keys.length;
keys.forEach(key => { keys.forEach((key) => {
if (WHITELIST_KEYS.includes(key)) { if (WHITELIST_KEYS.includes(key)) {
used[key] = i18n[key]; used[key] = i18n[key];
} else { } else {
@@ -65,8 +65,10 @@ function processI18nFile(i18nPath, lang, allSource) {
checked++; checked++;
if (checked % 20 === 0 || checked === total) { if (checked % 20 === 0 || checked === total) {
const percent = ((checked / total) * 100).toFixed(1); const percent = ((checked / total) * 100).toFixed(1);
process.stdout.write(`\r[${lang}] Progress: ${checked}/${total} (${percent}%)`); process.stdout.write(
if (checked === total) process.stdout.write('\n'); `\r[${lang}] Progress: ${checked}/${total} (${percent}%)`,
);
if (checked === total) process.stdout.write("\n");
} }
}); });
@@ -74,23 +76,25 @@ function processI18nFile(i18nPath, lang, allSource) {
console.log(`\n[${lang}] Unused keys:`, unused); console.log(`\n[${lang}] Unused keys:`, unused);
// 备份原文件 // 备份原文件
const oldPath = i18nPath + '.old'; const oldPath = i18nPath + ".old";
fs.renameSync(i18nPath, oldPath); fs.renameSync(i18nPath, oldPath);
// 写入精简后的 i18n 文件(保留原文件名) // 写入精简后的 i18n 文件(保留原文件名)
fs.writeFileSync(i18nPath, JSON.stringify(used, null, 2), 'utf8'); fs.writeFileSync(i18nPath, JSON.stringify(used, null, 2), "utf8");
console.log(`[${lang}] Cleaned i18n file written to src/locales/${path.basename(i18nPath)}`); console.log(
`[${lang}] Cleaned i18n file written to src/locales/${path.basename(i18nPath)}`,
);
console.log(`[${lang}] Original file backed up as ${path.basename(oldPath)}`); console.log(`[${lang}] Original file backed up as ${path.basename(oldPath)}`);
} }
function main() { function main() {
// 支持 zhtw.json、zh-tw.json、zh_CN.json 等 // 支持 zhtw.json、zh-tw.json、zh_CN.json 等
const files = fs.readdirSync(LOCALES_DIR).filter(f => const files = fs
/^[a-z0-9\-_]+\.json$/i.test(f) && !f.endsWith('.old') .readdirSync(LOCALES_DIR)
); .filter((f) => /^[a-z0-9\-_]+\.json$/i.test(f) && !f.endsWith(".old"));
const allSource = getAllSourceContent(); const allSource = getAllSourceContent();
files.forEach(file => { files.forEach((file) => {
const lang = path.basename(file, '.json'); const lang = path.basename(file, ".json");
processI18nFile(path.join(LOCALES_DIR, file), lang, allSource); processI18nFile(path.join(LOCALES_DIR, file), lang, allSource);
}); });
} }
+4 -2
View File
@@ -29,7 +29,7 @@ const runRelease = () =>
// 2. 判断是否需要打 tag // 2. 判断是否需要打 tag
function isSemver(version) { function isSemver(version) {
return /^v?\d+\.\d+\.\d+(-alpha)?$/.test(version); return /^v?\d+\.\d+\.\d+(-[0-9A-Za-z-.]+)?$/.test(version);
} }
async function run() { async function run() {
@@ -38,7 +38,9 @@ async function run() {
let tag = null; let tag = null;
if (versionArg === "alpha") { if (versionArg === "alpha") {
// 读取 package.json 里的主版本 // 读取 package.json 里的主版本
const pkg = await import(path.join(rootDir, "package.json"), { assert: { type: "json" } }); const pkg = await import(path.join(rootDir, "package.json"), {
assert: { type: "json" },
});
tag = `v${pkg.default.version}-alpha`; tag = `v${pkg.default.version}-alpha`;
} else if (isSemver(versionArg)) { } else if (isSemver(versionArg)) {
// 1.2.3 或 v1.2.3 // 1.2.3 或 v1.2.3
+39 -15
View File
@@ -1,5 +1,3 @@
/** /**
* CLI tool to update version numbers in package.json, src-tauri/Cargo.toml, and src-tauri/tauri.conf.json. * CLI tool to update version numbers in package.json, src-tauri/Cargo.toml, and src-tauri/tauri.conf.json.
* *
@@ -51,7 +49,9 @@ function generateShortTimestamp() {
* @returns {boolean} * @returns {boolean}
*/ */
function isValidVersion(version) { function isValidVersion(version) {
return /^v?\d+\.\d+\.\d+(-(alpha|beta|rc)(\.\d+)?)?(\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*)?$/i.test(version); return /^v?\d+\.\d+\.\d+(-(alpha|beta|rc)(\.\d+)?)?(\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*)?$/i.test(
version,
);
} }
/** /**
@@ -69,8 +69,8 @@ function normalizeVersion(version) {
* @returns {string} * @returns {string}
*/ */
function getBaseVersion(version) { function getBaseVersion(version) {
let base = version.replace(/-(alpha|beta|rc)(\.\d+)?/i, ''); let base = version.replace(/-(alpha|beta|rc)(\.\d+)?/i, "");
base = base.replace(/\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*/g, ''); base = base.replace(/\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*/g, "");
return base; return base;
} }
@@ -85,10 +85,21 @@ async function updatePackageVersion(newVersion) {
const data = await fs.readFile(packageJsonPath, "utf8"); const data = await fs.readFile(packageJsonPath, "utf8");
const packageJson = JSON.parse(data); const packageJson = JSON.parse(data);
console.log("[INFO]: Current package.json version is: ", packageJson.version); console.log(
packageJson.version = newVersion.startsWith("v") ? newVersion.slice(1) : newVersion; "[INFO]: Current package.json version is: ",
await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2), "utf8"); packageJson.version,
console.log(`[INFO]: package.json version updated to: ${packageJson.version}`); );
packageJson.version = newVersion.startsWith("v")
? newVersion.slice(1)
: newVersion;
await fs.writeFile(
packageJsonPath,
JSON.stringify(packageJson, null, 2),
"utf8",
);
console.log(
`[INFO]: package.json version updated to: ${packageJson.version}`,
);
} catch (error) { } catch (error) {
console.error("Error updating package.json version:", error); console.error("Error updating package.json version:", error);
throw error; throw error;
@@ -105,12 +116,17 @@ async function updateCargoVersion(newVersion) {
try { try {
const data = await fs.readFile(cargoTomlPath, "utf8"); const data = await fs.readFile(cargoTomlPath, "utf8");
const lines = data.split("\n"); const lines = data.split("\n");
const versionWithoutV = newVersion.startsWith("v") ? newVersion.slice(1) : newVersion; const versionWithoutV = newVersion.startsWith("v")
? newVersion.slice(1)
: newVersion;
const baseVersion = getBaseVersion(versionWithoutV); const baseVersion = getBaseVersion(versionWithoutV);
const updatedLines = lines.map((line) => { const updatedLines = lines.map((line) => {
if (line.trim().startsWith("version =")) { if (line.trim().startsWith("version =")) {
return line.replace(/version\s*=\s*"[^"]+"/, `version = "${baseVersion}"`); return line.replace(
/version\s*=\s*"[^"]+"/,
`version = "${baseVersion}"`,
);
} }
return line; return line;
}); });
@@ -133,12 +149,21 @@ async function updateTauriConfigVersion(newVersion) {
try { try {
const data = await fs.readFile(tauriConfigPath, "utf8"); const data = await fs.readFile(tauriConfigPath, "utf8");
const tauriConfig = JSON.parse(data); const tauriConfig = JSON.parse(data);
const versionWithoutV = newVersion.startsWith("v") ? newVersion.slice(1) : newVersion; const versionWithoutV = newVersion.startsWith("v")
? newVersion.slice(1)
: newVersion;
const baseVersion = getBaseVersion(versionWithoutV); const baseVersion = getBaseVersion(versionWithoutV);
console.log("[INFO]: Current tauri.conf.json version is: ", tauriConfig.version); console.log(
"[INFO]: Current tauri.conf.json version is: ",
tauriConfig.version,
);
tauriConfig.version = baseVersion; tauriConfig.version = baseVersion;
await fs.writeFile(tauriConfigPath, JSON.stringify(tauriConfig, null, 2), "utf8"); await fs.writeFile(
tauriConfigPath,
JSON.stringify(tauriConfig, null, 2),
"utf8",
);
console.log(`[INFO]: tauri.conf.json version updated to: ${baseVersion}`); console.log(`[INFO]: tauri.conf.json version updated to: ${baseVersion}`);
} catch (error) { } catch (error) {
console.error("Error updating tauri.conf.json version:", error); console.error("Error updating tauri.conf.json version:", error);
@@ -210,4 +235,3 @@ program
.argument("<version>", "version tag or full version") .argument("<version>", "version tag or full version")
.action(main) .action(main)
.parse(process.argv); .parse(process.argv);
+154 -197
View File
@@ -1057,7 +1057,7 @@ dependencies = [
"dunce", "dunce",
"futures", "futures",
"gethostname 1.0.2", "gethostname 1.0.2",
"getrandom 0.3.2", "getrandom 0.3.3",
"hex", "hex",
"hmac", "hmac",
"image", "image",
@@ -1106,7 +1106,7 @@ dependencies = [
"warp", "warp",
"winapi", "winapi",
"winreg 0.55.0", "winreg 0.55.0",
"zip", "zip 4.0.0",
] ]
[[package]] [[package]]
@@ -1337,21 +1337,6 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "crc"
version = "3.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636"
dependencies = [
"crc-catalog",
]
[[package]]
name = "crc-catalog"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
[[package]] [[package]]
name = "crc32fast" name = "crc32fast"
version = "1.4.2" version = "1.4.2"
@@ -1666,7 +1651,7 @@ dependencies = [
"tonic-health", "tonic-health",
"tonic-web", "tonic-web",
"tower 0.4.13", "tower 0.4.13",
"tower-http", "tower-http 0.4.4",
"tower-layer", "tower-layer",
"tracing", "tracing",
"tracing-core", "tracing-core",
@@ -2100,11 +2085,12 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.1.0" version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc" checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece"
dependencies = [ dependencies = [
"crc32fast", "crc32fast",
"libz-rs-sys",
"miniz_oxide", "miniz_oxide",
] ]
@@ -2462,9 +2448,9 @@ dependencies = [
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.3.2" version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys", "js-sys",
@@ -2587,9 +2573,9 @@ checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
[[package]] [[package]]
name = "global-hotkey" name = "global-hotkey"
version = "0.6.4" version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fbb3a4e56c901ee66c190fdb3fa08344e6d09593cc6c61f8eb9add7144b271" checksum = "b9247516746aa8e53411a0db9b62b0e24efbcf6a76e0ba73e5a91b512ddabed7"
dependencies = [ dependencies = [
"crossbeam-channel", "crossbeam-channel",
"keyboard-types", "keyboard-types",
@@ -2599,7 +2585,8 @@ dependencies = [
"serde", "serde",
"thiserror 2.0.12", "thiserror 2.0.12",
"windows-sys 0.59.0", "windows-sys 0.59.0",
"x11-dl", "x11rb",
"xkeysym",
] ]
[[package]] [[package]]
@@ -2939,7 +2926,7 @@ dependencies = [
"httpdate", "httpdate",
"itoa 1.0.15", "itoa 1.0.15",
"pin-project-lite", "pin-project-lite",
"socket2 0.5.8", "socket2 0.4.10",
"tokio", "tokio",
"tower-service", "tower-service",
"tracing", "tracing",
@@ -2981,7 +2968,7 @@ dependencies = [
"tokio", "tokio",
"tokio-rustls", "tokio-rustls",
"tower-service", "tower-service",
"webpki-roots", "webpki-roots 0.26.8",
] ]
[[package]] [[package]]
@@ -3014,21 +3001,28 @@ dependencies = [
[[package]] [[package]]
name = "hyper-util" name = "hyper-util"
version = "0.1.10" version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb"
dependencies = [ dependencies = [
"base64 0.22.1",
"bytes", "bytes",
"futures-channel", "futures-channel",
"futures-core",
"futures-util", "futures-util",
"http 1.3.1", "http 1.3.1",
"http-body 1.0.1", "http-body 1.0.1",
"hyper 1.6.0", "hyper 1.6.0",
"ipnet",
"libc",
"percent-encoding",
"pin-project-lite", "pin-project-lite",
"socket2 0.5.8", "socket2 0.5.10",
"system-configuration",
"tokio", "tokio",
"tower-service", "tower-service",
"tracing", "tracing",
"windows-registry",
] ]
[[package]] [[package]]
@@ -3380,6 +3374,16 @@ dependencies = [
"regex", "regex",
] ]
[[package]]
name = "iri-string"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2"
dependencies = [
"memchr",
"serde",
]
[[package]] [[package]]
name = "is-docker" name = "is-docker"
version = "0.2.0" version = "0.2.0"
@@ -3607,6 +3611,26 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "liblzma"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66352d7a8ac12d4877b6e6ea5a9b7650ee094257dc40889955bea5bc5b08c1d0"
dependencies = [
"liblzma-sys",
]
[[package]]
name = "liblzma-sys"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5839bad90c3cc2e0b8c4ed8296b80e86040240f81d46b9c0e9bc8dd51ddd3af1"
dependencies = [
"cc",
"libc",
"pkg-config",
]
[[package]] [[package]]
name = "libm" name = "libm"
version = "0.2.11" version = "0.2.11"
@@ -3624,6 +3648,15 @@ dependencies = [
"redox_syscall", "redox_syscall",
] ]
[[package]]
name = "libz-rs-sys"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6489ca9bd760fe9642d7644e827b0c9add07df89857b0416ee15c1cc1a3b8c5a"
dependencies = [
"zlib-rs",
]
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.3.8" version = "0.3.8"
@@ -3668,9 +3701,9 @@ dependencies = [
[[package]] [[package]]
name = "lock_api" name = "lock_api"
version = "0.4.12" version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"scopeguard", "scopeguard",
@@ -3743,27 +3776,6 @@ dependencies = [
"hashbrown 0.15.2", "hashbrown 0.15.2",
] ]
[[package]]
name = "lzma-rs"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e"
dependencies = [
"byteorder",
"crc",
]
[[package]]
name = "lzma-sys"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27"
dependencies = [
"cc",
"libc",
"pkg-config",
]
[[package]] [[package]]
name = "mac" name = "mac"
version = "0.1.1" version = "0.1.1"
@@ -4777,9 +4789,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.12.3" version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13"
dependencies = [ dependencies = [
"lock_api", "lock_api",
"parking_lot_core", "parking_lot_core",
@@ -4787,9 +4799,9 @@ dependencies = [
[[package]] [[package]]
name = "parking_lot_core" name = "parking_lot_core"
version = "0.9.10" version = "0.9.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
@@ -5381,7 +5393,7 @@ dependencies = [
"quinn-udp", "quinn-udp",
"rustc-hash", "rustc-hash",
"rustls", "rustls",
"socket2 0.5.8", "socket2 0.5.10",
"thiserror 2.0.12", "thiserror 2.0.12",
"tokio", "tokio",
"tracing", "tracing",
@@ -5395,7 +5407,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b820744eb4dc9b57a3398183639c511b5a26d2ed702cedd3febaa1393caa22cc" checksum = "b820744eb4dc9b57a3398183639c511b5a26d2ed702cedd3febaa1393caa22cc"
dependencies = [ dependencies = [
"bytes", "bytes",
"getrandom 0.3.2", "getrandom 0.3.3",
"rand 0.9.0", "rand 0.9.0",
"ring", "ring",
"rustc-hash", "rustc-hash",
@@ -5417,7 +5429,7 @@ dependencies = [
"cfg_aliases 0.2.1", "cfg_aliases 0.2.1",
"libc", "libc",
"once_cell", "once_cell",
"socket2 0.5.8", "socket2 0.5.10",
"tracing", "tracing",
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
@@ -5527,7 +5539,7 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
dependencies = [ dependencies = [
"getrandom 0.3.2", "getrandom 0.3.3",
] ]
[[package]] [[package]]
@@ -5727,9 +5739,9 @@ dependencies = [
[[package]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.12.15" version = "0.12.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" checksum = "a2f8e5513d63f2e5b386eb5106dc67eaf3f84e95258e210489136b8b92ad6119"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"bytes", "bytes",
@@ -5756,26 +5768,24 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
"quinn", "quinn",
"rustls", "rustls",
"rustls-pemfile",
"rustls-pki-types", "rustls-pki-types",
"serde", "serde",
"serde_json", "serde_json",
"serde_urlencoded", "serde_urlencoded",
"sync_wrapper 1.0.2", "sync_wrapper 1.0.2",
"system-configuration",
"tokio", "tokio",
"tokio-native-tls", "tokio-native-tls",
"tokio-rustls", "tokio-rustls",
"tokio-util", "tokio-util",
"tower 0.5.2", "tower 0.5.2",
"tower-http 0.6.6",
"tower-service", "tower-service",
"url", "url",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"wasm-streams", "wasm-streams",
"web-sys", "web-sys",
"webpki-roots", "webpki-roots 1.0.0",
"windows-registry 0.4.0",
] ]
[[package]] [[package]]
@@ -5967,15 +5977,6 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "rustls-pemfile"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
dependencies = [
"rustls-pki-types",
]
[[package]] [[package]]
name = "rustls-pki-types" name = "rustls-pki-types"
version = "1.11.0" version = "1.11.0"
@@ -6521,9 +6522,9 @@ dependencies = [
[[package]] [[package]]
name = "socket2" name = "socket2"
version = "0.5.8" version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.52.0", "windows-sys 0.52.0",
@@ -6708,9 +6709,9 @@ dependencies = [
[[package]] [[package]]
name = "sysinfo" name = "sysinfo"
version = "0.35.0" version = "0.35.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b897c8ea620e181c7955369a31be5f48d9a9121cb59fd33ecef9ff2a34323422" checksum = "3c3ffa3e4ff2b324a57f7aeb3c349656c7b127c3c189520251a648102a92496e"
dependencies = [ dependencies = [
"libc", "libc",
"memchr", "memchr",
@@ -7006,9 +7007,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-deep-link" name = "tauri-plugin-deep-link"
version = "2.2.1" version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dba4412f30eaff6f5d210e20383c2d6835593977402092e95b72497a4f8632fa" checksum = "e4976ac728ebc0487515aa956cfdf200abcc52b784e441493fc544bc6ce369c8"
dependencies = [ dependencies = [
"dunce", "dunce",
"rust-ini", "rust-ini",
@@ -7020,7 +7021,7 @@ dependencies = [
"thiserror 2.0.12", "thiserror 2.0.12",
"tracing", "tracing",
"url", "url",
"windows-registry 0.5.1", "windows-registry",
"windows-result 0.3.2", "windows-result 0.3.2",
] ]
@@ -7053,9 +7054,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-dialog" name = "tauri-plugin-dialog"
version = "2.2.1" version = "2.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcaf6e5d6062423a0f711a23c2a573ccba222b6a16a9322d8499928f27e41376" checksum = "a33318fe222fc2a612961de8b0419e2982767f213f54a4d3a21b0d7b85c41df8"
dependencies = [ dependencies = [
"log", "log",
"raw-window-handle", "raw-window-handle",
@@ -7071,9 +7072,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-fs" name = "tauri-plugin-fs"
version = "2.2.1" version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88371e340ad2f07409a3b68294abe73f20bc9c1bc1b631a31dc37a3d0161f682" checksum = "33ead0daec5d305adcefe05af9d970fc437bcc7996052d564e7393eb291252da"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"dunce", "dunce",
@@ -7089,14 +7090,13 @@ dependencies = [
"thiserror 2.0.12", "thiserror 2.0.12",
"toml", "toml",
"url", "url",
"uuid",
] ]
[[package]] [[package]]
name = "tauri-plugin-global-shortcut" name = "tauri-plugin-global-shortcut"
version = "2.2.0" version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00f646a09511e8d283267dcdaa08c2ef27c4116bf271d9114849d9ca215606c3" checksum = "31919f3c07bcb585afef217c0c33cde80da9ebccf5b8e2c90e0e0a535b14ab47"
dependencies = [ dependencies = [
"global-hotkey", "global-hotkey",
"log", "log",
@@ -7167,7 +7167,7 @@ dependencies = [
"tokio", "tokio",
"url", "url",
"windows-sys 0.59.0", "windows-sys 0.59.0",
"zip", "zip 2.6.1",
] ]
[[package]] [[package]]
@@ -7285,12 +7285,12 @@ dependencies = [
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.19.1" version = "3.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1"
dependencies = [ dependencies = [
"fastrand 2.3.0", "fastrand 2.3.0",
"getrandom 0.3.2", "getrandom 0.3.3",
"once_cell", "once_cell",
"rustix 1.0.3", "rustix 1.0.3",
"windows-sys 0.59.0", "windows-sys 0.59.0",
@@ -7514,9 +7514,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.44.2" version = "1.45.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"bytes", "bytes",
@@ -7525,7 +7525,7 @@ dependencies = [
"parking_lot", "parking_lot",
"pin-project-lite", "pin-project-lite",
"signal-hook-registry", "signal-hook-registry",
"socket2 0.5.8", "socket2 0.5.10",
"tokio-macros", "tokio-macros",
"tracing", "tracing",
"windows-sys 0.52.0", "windows-sys 0.52.0",
@@ -7730,7 +7730,7 @@ dependencies = [
"pin-project", "pin-project",
"tokio-stream", "tokio-stream",
"tonic", "tonic",
"tower-http", "tower-http 0.4.4",
"tower-layer", "tower-layer",
"tower-service", "tower-service",
"tracing", "tracing",
@@ -7789,6 +7789,24 @@ dependencies = [
"tower-service", "tower-service",
] ]
[[package]]
name = "tower-http"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
dependencies = [
"bitflags 2.9.0",
"bytes",
"futures-util",
"http 1.3.1",
"http-body 1.0.1",
"iri-string",
"pin-project-lite",
"tower 0.5.2",
"tower-layer",
"tower-service",
]
[[package]] [[package]]
name = "tower-layer" name = "tower-layer"
version = "0.3.3" version = "0.3.3"
@@ -8144,7 +8162,7 @@ version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
dependencies = [ dependencies = [
"getrandom 0.3.2", "getrandom 0.3.3",
"serde", "serde",
] ]
@@ -8514,6 +8532,15 @@ dependencies = [
"rustls-pki-types", "rustls-pki-types",
] ]
[[package]]
name = "webpki-roots"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb"
dependencies = [
"rustls-pki-types",
]
[[package]] [[package]]
name = "webview2-com" name = "webview2-com"
version = "0.37.0" version = "0.37.0"
@@ -8761,17 +8788,6 @@ dependencies = [
"windows-link", "windows-link",
] ]
[[package]]
name = "windows-registry"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3"
dependencies = [
"windows-result 0.3.2",
"windows-strings 0.3.1",
"windows-targets 0.53.0",
]
[[package]] [[package]]
name = "windows-registry" name = "windows-registry"
version = "0.5.1" version = "0.5.1"
@@ -8811,15 +8827,6 @@ dependencies = [
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
[[package]]
name = "windows-strings"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
dependencies = [
"windows-link",
]
[[package]] [[package]]
name = "windows-strings" name = "windows-strings"
version = "0.4.0" version = "0.4.0"
@@ -8904,29 +8911,13 @@ dependencies = [
"windows_aarch64_gnullvm 0.52.6", "windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6", "windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6", "windows_i686_gnu 0.52.6",
"windows_i686_gnullvm 0.52.6", "windows_i686_gnullvm",
"windows_i686_msvc 0.52.6", "windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6", "windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6", "windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6", "windows_x86_64_msvc 0.52.6",
] ]
[[package]]
name = "windows-targets"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b"
dependencies = [
"windows_aarch64_gnullvm 0.53.0",
"windows_aarch64_msvc 0.53.0",
"windows_i686_gnu 0.53.0",
"windows_i686_gnullvm 0.53.0",
"windows_i686_msvc 0.53.0",
"windows_x86_64_gnu 0.53.0",
"windows_x86_64_gnullvm 0.53.0",
"windows_x86_64_msvc 0.53.0",
]
[[package]] [[package]]
name = "windows-version" name = "windows-version"
version = "0.1.4" version = "0.1.4"
@@ -8954,12 +8945,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
[[package]] [[package]]
name = "windows_aarch64_msvc" name = "windows_aarch64_msvc"
version = "0.42.2" version = "0.42.2"
@@ -8978,12 +8963,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
[[package]] [[package]]
name = "windows_i686_gnu" name = "windows_i686_gnu"
version = "0.42.2" version = "0.42.2"
@@ -9002,24 +8981,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
[[package]] [[package]]
name = "windows_i686_gnullvm" name = "windows_i686_gnullvm"
version = "0.52.6" version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
[[package]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
version = "0.42.2" version = "0.42.2"
@@ -9038,12 +9005,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_i686_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
[[package]] [[package]]
name = "windows_x86_64_gnu" name = "windows_x86_64_gnu"
version = "0.42.2" version = "0.42.2"
@@ -9062,12 +9023,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
[[package]] [[package]]
name = "windows_x86_64_gnullvm" name = "windows_x86_64_gnullvm"
version = "0.42.2" version = "0.42.2"
@@ -9086,12 +9041,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
[[package]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
version = "0.42.2" version = "0.42.2"
@@ -9110,12 +9059,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.5.40" version = "0.5.40"
@@ -9313,21 +9256,18 @@ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
[[package]]
name = "xkeysym"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
[[package]] [[package]]
name = "xml-rs" name = "xml-rs"
version = "0.8.25" version = "0.8.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5b940ebc25896e71dd073bad2dbaa2abfe97b0a391415e22ad1326d9c54e3c4" checksum = "c5b940ebc25896e71dd073bad2dbaa2abfe97b0a391415e22ad1326d9c54e3c4"
[[package]]
name = "xz2"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2"
dependencies = [
"lzma-sys",
]
[[package]] [[package]]
name = "yoke" name = "yoke"
version = "0.7.5" version = "0.7.5"
@@ -9517,29 +9457,46 @@ name = "zip"
version = "2.6.1" version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dcb24d0152526ae49b9b96c1dcf71850ca1e0b882e4e28ed898a93c41334744" checksum = "1dcb24d0152526ae49b9b96c1dcf71850ca1e0b882e4e28ed898a93c41334744"
dependencies = [
"arbitrary",
"crc32fast",
"crossbeam-utils",
"indexmap 2.8.0",
"memchr",
]
[[package]]
name = "zip"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "153a6fff49d264c4babdcfa6b4d534747f520e56e8f0f384f3b808c4b64cc1fd"
dependencies = [ dependencies = [
"aes", "aes",
"arbitrary", "arbitrary",
"bzip2", "bzip2",
"constant_time_eq", "constant_time_eq",
"crc32fast", "crc32fast",
"crossbeam-utils",
"deflate64", "deflate64",
"flate2", "flate2",
"getrandom 0.3.2", "getrandom 0.3.3",
"hmac", "hmac",
"indexmap 2.8.0", "indexmap 2.8.0",
"lzma-rs", "liblzma",
"memchr", "memchr",
"pbkdf2", "pbkdf2",
"sha1", "sha1",
"time", "time",
"xz2",
"zeroize", "zeroize",
"zopfli", "zopfli",
"zstd", "zstd",
] ]
[[package]]
name = "zlib-rs"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "868b928d7949e09af2f6086dfc1e01936064cc7a819253bce650d4e2a2d63ba8"
[[package]] [[package]]
name = "zopfli" name = "zopfli"
version = "0.8.1" version = "0.8.1"
+18 -18
View File
@@ -16,33 +16,33 @@ identifier = "io.github.clash-verge-rev.clash-verge-rev"
tauri-build = { version = "2.2.0", features = [] } tauri-build = { version = "2.2.0", features = [] }
[dependencies] [dependencies]
warp = "0.3" warp = "0.3.7"
anyhow = "1.0.98" anyhow = "1.0.98"
dirs = "6.0" dirs = "6.0"
open = "5.3" open = "5.3.2"
log = "0.4" log = "0.4.27"
dunce = "1.0" dunce = "1.0.5"
log4rs = "1" log4rs = "1.3.0"
nanoid = "0.4" nanoid = "0.4"
chrono = "0.4.41" chrono = "0.4.41"
sysinfo = "0.35.0" sysinfo = "0.35.2"
boa_engine = "0.20.0" boa_engine = "0.20.0"
serde_json = "1.0.140" serde_json = "1.0.140"
serde_yaml = "0.9.34" serde_yaml = "0.9.34-deprecated"
once_cell = "1.21.3" once_cell = "1.21.3"
lazy_static = "1.5.0" lazy_static = "1.5.0"
port_scanner = "0.1.5" port_scanner = "0.1.5"
delay_timer = "0.11.6" delay_timer = "0.11.6"
parking_lot = "0.12.3" parking_lot = "0.12.4"
percent-encoding = "2.3.1" percent-encoding = "2.3.1"
tokio = { version = "1.44.2", features = [ tokio = { version = "1.45.1", features = [
"rt-multi-thread", "rt-multi-thread",
"macros", "macros",
"time", "time",
"sync", "sync",
] } ] }
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
reqwest = { version = "0.12.15", features = ["json", "rustls-tls", "cookies"] } reqwest = { version = "0.12.19", features = ["json", "rustls-tls", "cookies"] }
regex = "1.11.1" regex = "1.11.1"
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" } sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" }
image = "0.25.6" image = "0.25.6"
@@ -56,18 +56,18 @@ tauri = { version = "2.5.1", features = [
] } ] }
network-interface = { version = "2.0.1", features = ["serde"] } network-interface = { version = "2.0.1", features = ["serde"] }
tauri-plugin-shell = "2.2.1" tauri-plugin-shell = "2.2.1"
tauri-plugin-dialog = "2.2.1" tauri-plugin-dialog = "2.2.2"
tauri-plugin-fs = "2.2.1" tauri-plugin-fs = "2.3.0"
tauri-plugin-process = "2.2.1" tauri-plugin-process = "2.2.1"
tauri-plugin-clipboard-manager = "2.2.2" tauri-plugin-clipboard-manager = "2.2.2"
tauri-plugin-deep-link = "2.2.1" tauri-plugin-deep-link = "2.3.0"
tauri-plugin-devtools = "2.0.0" tauri-plugin-devtools = "2.0.0"
tauri-plugin-window-state = "2.2.2" tauri-plugin-window-state = "2.2.2"
zip = "2.6.1" zip = "4.0.0"
reqwest_dav = "0.2.1" reqwest_dav = "0.2.1"
aes-gcm = { version = "0.10.3", features = ["std"] } aes-gcm = { version = "0.10.3", features = ["std"] }
base64 = "0.22.1" base64 = "0.22.1"
getrandom = "0.3.2" getrandom = "0.3.3"
tokio-tungstenite = "0.26.2" tokio-tungstenite = "0.26.2"
futures = "0.3.31" futures = "0.3.31"
sys-locale = "0.3.2" sys-locale = "0.3.2"
@@ -85,7 +85,7 @@ hex = "0.4.3"
runas = "=1.2.0" runas = "=1.2.0"
deelevate = "0.2.0" deelevate = "0.2.0"
winreg = "0.55.0" winreg = "0.55.0"
winapi = { version = "0.3", features = [ winapi = { version = "0.3.9", features = [
"winbase", "winbase",
"fileapi", "fileapi",
"winnt", "winnt",
@@ -100,7 +100,7 @@ users = "0.11.0"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-autostart = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } tauri-plugin-autostart = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-global-shortcut = "2.2.0" tauri-plugin-global-shortcut = "2.2.1"
tauri-plugin-updater = "2.7.1" tauri-plugin-updater = "2.7.1"
[features] [features]
@@ -136,7 +136,7 @@ name = "app_lib"
crate-type = ["staticlib", "cdylib", "rlib"] crate-type = ["staticlib", "cdylib", "rlib"]
[dev-dependencies] [dev-dependencies]
tempfile = "3.19.1" tempfile = "3.20.0"
[workspace] [workspace]
members = ["src_crates/crate_mihomo_api"] members = ["src_crates/crate_mihomo_api"]
@@ -1,4 +1,7 @@
{ {
"identifier": "desktop-windows-capability",
"description": "permissions for desktop windows applications",
"windows": ["main"],
"permissions": [ "permissions": [
"core:webview:allow-create-webview", "core:webview:allow-create-webview",
"core:webview:allow-create-webview-window" "core:webview:allow-create-webview-window"
-1
View File
@@ -11,4 +11,3 @@ merge_derives = true
use_try_shorthand = false use_try_shorthand = false
use_field_init_shorthand = false use_field_init_shorthand = false
force_explicit_abi = true force_explicit_abi = true
imports_granularity = "Crate"
+2 -2
View File
@@ -22,7 +22,7 @@ pub async fn get_profiles() -> CmdResult<IProfiles> {
.await; .await;
match profiles_result { match profiles_result {
Ok(Ok(profiles)) => Ok(profiles), Ok(Ok(profiles)) => Ok(*profiles),
Ok(Err(join_err)) => { Ok(Err(join_err)) => {
logging!(error, Type::Cmd, true, "获取配置列表任务失败: {}", join_err); logging!(error, Type::Cmd, true, "获取配置列表任务失败: {}", join_err);
Ok(IProfiles { Ok(IProfiles {
@@ -41,7 +41,7 @@ pub async fn get_profiles() -> CmdResult<IProfiles> {
match tokio::task::spawn_blocking(move || Config::profiles().latest().clone()).await { match tokio::task::spawn_blocking(move || Config::profiles().latest().clone()).await {
Ok(profiles) => { Ok(profiles) => {
logging!(info, Type::Cmd, true, "使用latest()成功获取配置"); logging!(info, Type::Cmd, true, "使用latest()成功获取配置");
Ok(profiles) Ok(*profiles)
} }
Err(_) => { Err(_) => {
logging!(error, Type::Cmd, true, "fallback获取配置也失败,返回空配置"); logging!(error, Type::Cmd, true, "fallback获取配置也失败,返回空配置");
+55 -49
View File
@@ -1,71 +1,77 @@
use super::CmdResult; use super::CmdResult;
use crate::module::mihomo::MihomoManager; use crate::{core::handle, module::mihomo::MihomoManager, state::proxy::CmdProxyState};
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use std::{ use std::{
sync::atomic::{AtomicBool, Ordering}, sync::Mutex,
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use tauri::Manager;
static LAST_REFRESH_TIME: Lazy<Mutex<Option<Instant>>> = Lazy::new(|| Mutex::new(None)); const PROVIDERS_REFRESH_INTERVAL: Duration = Duration::from_secs(3);
static IS_REFRESHING: AtomicBool = AtomicBool::new(false); const PROXIES_REFRESH_INTERVAL: Duration = Duration::from_secs(1);
const REFRESH_INTERVAL: Duration = Duration::from_secs(5);
#[tauri::command] #[tauri::command]
pub async fn get_proxies() -> CmdResult<serde_json::Value> { pub async fn get_proxies() -> CmdResult<serde_json::Value> {
let manager = MihomoManager::global(); let manager = MihomoManager::global();
manager let app_handle = handle::Handle::global().app_handle().unwrap();
.refresh_proxies() let cmd_proxy_state = app_handle.state::<Mutex<CmdProxyState>>();
.await
.map(|_| manager.get_proxies()) let should_refresh = {
.or_else(|_| Ok(manager.get_proxies())) let mut state = cmd_proxy_state.lock().unwrap();
let now = Instant::now();
if now.duration_since(state.last_refresh_time) > PROXIES_REFRESH_INTERVAL {
state.need_refresh = true;
state.last_refresh_time = now;
}
state.need_refresh
};
if should_refresh {
let proxies = manager.get_refresh_proxies().await?;
{
let mut state = cmd_proxy_state.lock().unwrap();
state.proxies = Box::new(proxies);
state.need_refresh = false;
}
log::debug!(target: "app", "proxies刷新成功");
}
let proxies = {
let state = cmd_proxy_state.lock().unwrap();
state.proxies.clone()
};
Ok(*proxies)
} }
#[tauri::command] #[tauri::command]
pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> { pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
let manager = MihomoManager::global(); let app_handle = handle::Handle::global().app_handle().unwrap();
let cached_data = manager.get_providers_proxies(); let cmd_proxy_state = app_handle.state::<Mutex<CmdProxyState>>();
let safe_data = if cached_data.is_null() {
serde_json::json!({
"providers": {}
})
} else {
cached_data
};
// 检查是否需要刷新
let should_refresh = { let should_refresh = {
let last_refresh = LAST_REFRESH_TIME.lock(); let mut state = cmd_proxy_state.lock().unwrap();
match *last_refresh { let now = Instant::now();
Some(last_time) => last_time.elapsed() > REFRESH_INTERVAL, if now.duration_since(state.last_refresh_time) > PROVIDERS_REFRESH_INTERVAL {
None => true, state.need_refresh = true;
state.last_refresh_time = now;
} }
state.need_refresh
}; };
if should_refresh && !IS_REFRESHING.load(Ordering::Acquire) { if should_refresh {
IS_REFRESHING.store(true, Ordering::Release); let manager = MihomoManager::global();
let providers = manager.get_providers_proxies().await?;
crate::process::AsyncHandler::spawn(|| async move { {
let manager = MihomoManager::global(); let mut state = cmd_proxy_state.lock().unwrap();
match manager.refresh_providers_proxies().await { state.providers_proxies = Box::new(providers);
Ok(_) => { state.need_refresh = false;
log::debug!(target: "app", "providers_proxies静默后台刷新成功"); }
} log::debug!(target: "app", "providers_proxies刷新成功");
Err(e) => {
log::warn!(target: "app", "providers_proxies后台刷新失败: {}", e);
}
}
{
let mut last_refresh = LAST_REFRESH_TIME.lock();
*last_refresh = Some(Instant::now());
}
IS_REFRESHING.store(false, Ordering::Release);
});
} }
Ok(safe_data) let providers_proxies = {
let state = cmd_proxy_state.lock().unwrap();
state.providers_proxies.clone()
};
Ok(*providers_proxies)
} }
+1 -1
View File
@@ -6,7 +6,7 @@ use crate::{config::*, feat, wrap_err};
pub fn get_verge_config() -> CmdResult<IVergeResponse> { pub fn get_verge_config() -> CmdResult<IVergeResponse> {
let verge = Config::verge(); let verge = Config::verge();
let verge_data = verge.data().clone(); let verge_data = verge.data().clone();
Ok(IVergeResponse::from(verge_data)) Ok(IVergeResponse::from(*verge_data))
} }
/// 修改Verge配置 /// 修改Verge配置
+53 -14
View File
@@ -15,10 +15,10 @@ pub const RUNTIME_CONFIG: &str = "clash-verge.yaml";
pub const CHECK_CONFIG: &str = "clash-verge-check.yaml"; pub const CHECK_CONFIG: &str = "clash-verge-check.yaml";
pub struct Config { pub struct Config {
clash_config: Draft<IClashTemp>, clash_config: Draft<Box<IClashTemp>>,
verge_config: Draft<IVerge>, verge_config: Draft<Box<IVerge>>,
profiles_config: Draft<IProfiles>, profiles_config: Draft<Box<IProfiles>>,
runtime_config: Draft<IRuntime>, runtime_config: Draft<Box<IRuntime>>,
} }
impl Config { impl Config {
@@ -26,26 +26,26 @@ impl Config {
static CONFIG: OnceCell<Config> = OnceCell::new(); static CONFIG: OnceCell<Config> = OnceCell::new();
CONFIG.get_or_init(|| Config { CONFIG.get_or_init(|| Config {
clash_config: Draft::from(IClashTemp::new()), clash_config: Draft::from(Box::new(IClashTemp::new())),
verge_config: Draft::from(IVerge::new()), verge_config: Draft::from(Box::new(IVerge::new())),
profiles_config: Draft::from(IProfiles::new()), profiles_config: Draft::from(Box::new(IProfiles::new())),
runtime_config: Draft::from(IRuntime::new()), runtime_config: Draft::from(Box::new(IRuntime::new())),
}) })
} }
pub fn clash() -> Draft<IClashTemp> { pub fn clash() -> Draft<Box<IClashTemp>> {
Self::global().clash_config.clone() Self::global().clash_config.clone()
} }
pub fn verge() -> Draft<IVerge> { pub fn verge() -> Draft<Box<IVerge>> {
Self::global().verge_config.clone() Self::global().verge_config.clone()
} }
pub fn profiles() -> Draft<IProfiles> { pub fn profiles() -> Draft<Box<IProfiles>> {
Self::global().profiles_config.clone() Self::global().profiles_config.clone()
} }
pub fn runtime() -> Draft<IRuntime> { pub fn runtime() -> Draft<Box<IRuntime>> {
Self::global().runtime_config.clone() Self::global().runtime_config.clone()
} }
@@ -149,11 +149,11 @@ impl Config {
pub async fn generate() -> Result<()> { pub async fn generate() -> Result<()> {
let (config, exists_keys, logs) = enhance::enhance().await; let (config, exists_keys, logs) = enhance::enhance().await;
*Config::runtime().draft() = IRuntime { *Config::runtime().draft() = Box::new(IRuntime {
config: Some(config), config: Some(config),
exists_keys, exists_keys,
chain_logs: logs, chain_logs: logs,
}; });
Ok(()) Ok(())
} }
@@ -164,3 +164,42 @@ pub enum ConfigType {
Run, Run,
Check, Check,
} }
#[cfg(test)]
mod tests {
use super::*;
use std::mem;
#[test]
fn test_prfitem_from_merge_size() {
let merge_item = PrfItem::from_merge(Some("Merge".to_string())).unwrap();
dbg!(&merge_item);
let prfitem_size = mem::size_of_val(&merge_item);
dbg!(prfitem_size);
// Boxed version
let boxed_merge_item = Box::new(merge_item);
let box_prfitem_size = mem::size_of_val(&boxed_merge_item);
dbg!(box_prfitem_size);
// The size of Box<T> is always pointer-sized (usually 8 bytes on 64-bit)
// assert_eq!(box_prfitem_size, mem::size_of::<Box<PrfItem>>());
assert!(box_prfitem_size < prfitem_size);
}
#[test]
fn test_draft_size_non_boxed() {
let draft = Draft::from(IRuntime::new());
let iruntime_size = std::mem::size_of_val(&draft);
dbg!(iruntime_size);
assert_eq!(iruntime_size, std::mem::size_of::<Draft<IRuntime>>());
}
#[test]
fn test_draft_size_boxed() {
let draft = Draft::from(Box::new(IRuntime::new()));
let box_iruntime_size = std::mem::size_of_val(&draft);
dbg!(box_iruntime_size);
assert_eq!(
box_iruntime_size,
std::mem::size_of::<Draft<Box<IRuntime>>>()
);
}
}
+28 -20
View File
@@ -9,13 +9,21 @@ pub struct Draft<T: Clone + ToOwned> {
macro_rules! draft_define { macro_rules! draft_define {
($id: ident) => { ($id: ident) => {
impl Draft<$id> { impl From<$id> for Draft<$id> {
fn from(data: $id) -> Self {
Draft {
inner: Arc::new(Mutex::new((data, None))),
}
}
}
impl Draft<Box<$id>> {
#[allow(unused)] #[allow(unused)]
pub fn data(&self) -> MappedMutexGuard<$id> { pub fn data(&self) -> MappedMutexGuard<Box<$id>> {
MutexGuard::map(self.inner.lock(), |guard| &mut guard.0) MutexGuard::map(self.inner.lock(), |guard| &mut guard.0)
} }
pub fn latest(&self) -> MappedMutexGuard<$id> { pub fn latest(&self) -> MappedMutexGuard<Box<$id>> {
MutexGuard::map(self.inner.lock(), |inner| { MutexGuard::map(self.inner.lock(), |inner| {
if inner.1.is_none() { if inner.1.is_none() {
&mut inner.0 &mut inner.0
@@ -25,7 +33,7 @@ macro_rules! draft_define {
}) })
} }
pub fn draft(&self) -> MappedMutexGuard<$id> { pub fn draft(&self) -> MappedMutexGuard<Box<$id>> {
MutexGuard::map(self.inner.lock(), |inner| { MutexGuard::map(self.inner.lock(), |inner| {
if inner.1.is_none() { if inner.1.is_none() {
inner.1 = Some(inner.0.clone()); inner.1 = Some(inner.0.clone());
@@ -35,7 +43,7 @@ macro_rules! draft_define {
}) })
} }
pub fn apply(&self) -> Option<$id> { pub fn apply(&self) -> Option<Box<$id>> {
let mut inner = self.inner.lock(); let mut inner = self.inner.lock();
match inner.1.take() { match inner.1.take() {
@@ -48,14 +56,14 @@ macro_rules! draft_define {
} }
} }
pub fn discard(&self) -> Option<$id> { pub fn discard(&self) -> Option<Box<$id>> {
let mut inner = self.inner.lock(); let mut inner = self.inner.lock();
inner.1.take() inner.1.take()
} }
} }
impl From<$id> for Draft<$id> { impl From<Box<$id>> for Draft<Box<$id>> {
fn from(data: $id) -> Self { fn from(data: Box<$id>) -> Self {
Draft { Draft {
inner: Arc::new(Mutex::new((data, None))), inner: Arc::new(Mutex::new((data, None))),
} }
@@ -71,12 +79,12 @@ draft_define!(IRuntime);
draft_define!(IVerge); draft_define!(IVerge);
#[test] #[test]
fn test_draft() { fn test_draft_box() {
let verge = IVerge { let verge = Box::new(IVerge {
enable_auto_launch: Some(true), enable_auto_launch: Some(true),
enable_tun_mode: Some(false), enable_tun_mode: Some(false),
..IVerge::default() ..IVerge::default()
}; });
let draft = Draft::from(verge); let draft = Draft::from(verge);
@@ -86,10 +94,11 @@ fn test_draft() {
assert_eq!(draft.draft().enable_auto_launch, Some(true)); assert_eq!(draft.draft().enable_auto_launch, Some(true));
assert_eq!(draft.draft().enable_tun_mode, Some(false)); assert_eq!(draft.draft().enable_tun_mode, Some(false));
let mut d = draft.draft(); {
d.enable_auto_launch = Some(false); let mut d = draft.draft();
d.enable_tun_mode = Some(true); d.enable_auto_launch = Some(false);
drop(d); d.enable_tun_mode = Some(true);
}
assert_eq!(draft.data().enable_auto_launch, Some(true)); assert_eq!(draft.data().enable_auto_launch, Some(true));
assert_eq!(draft.data().enable_tun_mode, Some(false)); assert_eq!(draft.data().enable_tun_mode, Some(false));
@@ -109,18 +118,17 @@ fn test_draft() {
assert_eq!(draft.draft().enable_auto_launch, Some(false)); assert_eq!(draft.draft().enable_auto_launch, Some(false));
assert_eq!(draft.draft().enable_tun_mode, Some(true)); assert_eq!(draft.draft().enable_tun_mode, Some(true));
let mut d = draft.draft(); {
d.enable_auto_launch = Some(true); let mut d = draft.draft();
drop(d); d.enable_auto_launch = Some(true);
}
assert_eq!(draft.data().enable_auto_launch, Some(false)); assert_eq!(draft.data().enable_auto_launch, Some(false));
assert_eq!(draft.draft().enable_auto_launch, Some(true)); assert_eq!(draft.draft().enable_auto_launch, Some(true));
assert!(draft.discard().is_some()); assert!(draft.discard().is_some());
assert_eq!(draft.data().enable_auto_launch, Some(false)); assert_eq!(draft.data().enable_auto_launch, Some(false));
assert!(draft.discard().is_none()); assert!(draft.discard().is_none());
assert_eq!(draft.draft().enable_auto_launch, Some(false)); assert_eq!(draft.draft().enable_auto_launch, Some(false));
+2 -2
View File
@@ -140,11 +140,11 @@ impl CoreManager {
/// 使用默认配置 /// 使用默认配置
pub async fn use_default_config(&self, msg_type: &str, msg_content: &str) -> Result<()> { pub async fn use_default_config(&self, msg_type: &str, msg_content: &str) -> Result<()> {
let runtime_path = dirs::app_home_dir()?.join(RUNTIME_CONFIG); let runtime_path = dirs::app_home_dir()?.join(RUNTIME_CONFIG);
*Config::runtime().draft() = IRuntime { *Config::runtime().draft() = Box::new(IRuntime {
config: Some(Config::clash().latest().0.clone()), config: Some(Config::clash().latest().0.clone()),
exists_keys: vec![], exists_keys: vec![],
chain_logs: Default::default(), chain_logs: Default::default(),
}; });
help::save_yaml( help::save_yaml(
&runtime_path, &runtime_path,
&Config::clash().latest().0, &Config::clash().latest().0,
@@ -104,6 +104,10 @@ fn test_script() {
let (config, results) = use_script(script.into(), config, "".to_string()).unwrap(); let (config, results) = use_script(script.into(), config, "".to_string()).unwrap();
let _ = serde_yaml::to_string(&config).unwrap(); let _ = serde_yaml::to_string(&config).unwrap();
let yaml_config_size = std::mem::size_of_val(&config);
dbg!(yaml_config_size);
let box_yaml_config_size = std::mem::size_of_val(&Box::new(config));
dbg!(box_yaml_config_size);
dbg!(results); dbg!(results);
assert!(box_yaml_config_size < yaml_config_size);
} }
@@ -1 +0,0 @@
pub mod service;
@@ -1 +0,0 @@
+3 -2
View File
@@ -2,10 +2,10 @@ mod cmd;
mod config; mod config;
mod core; mod core;
mod enhance; mod enhance;
mod error;
mod feat; mod feat;
mod module; mod module;
mod process; mod process;
mod state;
mod utils; mod utils;
use crate::{ use crate::{
core::hotkey, core::hotkey,
@@ -15,7 +15,6 @@ use crate::{
use config::Config; use config::Config;
use std::sync::{Mutex, Once}; use std::sync::{Mutex, Once};
use tauri::AppHandle; use tauri::AppHandle;
#[cfg(target_os = "macos")]
use tauri::Manager; use tauri::Manager;
use tauri_plugin_autostart::MacosLauncher; use tauri_plugin_autostart::MacosLauncher;
use tauri_plugin_deep_link::DeepLinkExt; use tauri_plugin_deep_link::DeepLinkExt;
@@ -205,6 +204,8 @@ pub fn run() {
logging!(error, Type::Setup, true, "初始化资源失败: {}", e); logging!(error, Type::Setup, true, "初始化资源失败: {}", e);
} }
app.manage(Mutex::new(state::proxy::CmdProxyState::default()));
logging!(info, Type::Setup, true, "初始化完成,继续执行"); logging!(info, Type::Setup, true, "初始化完成,继续执行");
Ok(()) Ok(())
}) })
@@ -0,0 +1,4 @@
// Tauri Manager 会进行 Arc 管理,无需额外 Arc
// https://tauri.app/develop/state-management/#do-you-need-arc
pub mod proxy;
@@ -0,0 +1,19 @@
use serde_json::Value;
pub struct CmdProxyState {
pub last_refresh_time: std::time::Instant,
pub need_refresh: bool,
pub proxies: Box<Value>,
pub providers_proxies: Box<Value>,
}
impl Default for CmdProxyState {
fn default() -> Self {
Self {
last_refresh_time: std::time::Instant::now(),
need_refresh: true,
proxies: Box::new(Value::Null),
providers_proxies: Box::new(Value::Null),
}
}
}
+2 -2
View File
@@ -204,8 +204,8 @@ pub fn format_bytes_speed(speed: u64) -> String {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
#[test] #[test]
fn test_format_bytes_speed() { fn test_format_bytes_speed() {
assert_eq!(format_bytes_speed(0), "0B/s"); assert_eq!(format_bytes_speed(0), "0.0B/s");
assert_eq!(format_bytes_speed(1023), "1023B/s"); assert_eq!(format_bytes_speed(1023), "1.0KB/s");
assert_eq!(format_bytes_speed(1024), "1.0KB/s"); assert_eq!(format_bytes_speed(1024), "1.0KB/s");
assert_eq!(format_bytes_speed(1024 * 1024), "1.0MB/s"); assert_eq!(format_bytes_speed(1024 * 1024), "1.0MB/s");
assert_eq!(format_bytes_speed(1024 * 1024 * 1024), "1.0GB/s"); assert_eq!(format_bytes_speed(1024 * 1024 * 1024), "1.0GB/s");
@@ -2,13 +2,10 @@
name = "mihomo_api" name = "mihomo_api"
edition = "2024" edition = "2024"
[features]
debug = []
[dependencies] [dependencies]
reqwest = { version = "0.12.15", features = ["json"] } reqwest = { version = "0.12.19", features = ["json"] }
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140" serde_json = "1.0.140"
tokio = { version = "1.44.1", features = ["rt", "macros", "time"] } tokio = { version = "1.45.1", features = ["rt", "macros", "time"] }
[dev-dependencies] [dev-dependencies]
@@ -1,11 +1,8 @@
use reqwest::{Method, header::HeaderMap}; use reqwest::{Method, header::HeaderMap};
use serde_json::json; use serde_json::{Value, json};
use std::{ use std::time::Duration;
sync::{Arc, Mutex},
time::Duration,
};
pub mod model; pub mod model;
pub use model::{MihomoData, MihomoManager}; pub use model::MihomoManager;
impl MihomoManager { impl MihomoManager {
pub fn new(mihomo_server: String, headers: HeaderMap) -> Self { pub fn new(mihomo_server: String, headers: HeaderMap) -> Self {
@@ -20,38 +17,10 @@ impl MihomoManager {
Self { Self {
mihomo_server, mihomo_server,
data: Arc::new(Mutex::new(MihomoData {
proxies: serde_json::Value::Null,
providers_proxies: serde_json::Value::Null,
})),
client, client,
} }
} }
fn update_proxies(&self, proxies: serde_json::Value) {
let mut data = self.data.lock().expect("Mutex poisoned");
data.proxies = proxies;
}
fn update_providers_proxies(&self, providers_proxies: serde_json::Value) {
let mut data = self.data.lock().expect("Mutex poisoned");
data.providers_proxies = providers_proxies;
}
pub fn get_mihomo_server(&self) -> String {
self.mihomo_server.clone()
}
pub fn get_proxies(&self) -> serde_json::Value {
let data = self.data.lock().expect("Mutex poisoned");
data.proxies.clone()
}
pub fn get_providers_proxies(&self) -> serde_json::Value {
let data = self.data.lock().expect("Mutex poisoned");
data.providers_proxies.clone()
}
async fn send_request( async fn send_request(
&self, &self,
method: Method, method: Method,
@@ -87,18 +56,16 @@ impl MihomoManager {
Ok(response) Ok(response)
} }
pub async fn refresh_proxies(&self) -> Result<&Self, String> { pub async fn get_refresh_proxies(&self) -> Result<Value, String> {
let url = format!("{}/proxies", self.mihomo_server); let url = format!("{}/proxies", self.mihomo_server);
let proxies = self.send_request(Method::GET, url, None).await?; let proxies = self.send_request(Method::GET, url, None).await?;
self.update_proxies(proxies); Ok(proxies)
Ok(self)
} }
pub async fn refresh_providers_proxies(&self) -> Result<&Self, String> { pub async fn get_providers_proxies(&self) -> Result<Value, String> {
let url = format!("{}/providers/proxies", self.mihomo_server); let url = format!("{}/providers/proxies", self.mihomo_server);
let providers_proxies = self.send_request(Method::GET, url, None).await?; let providers_proxies = self.send_request(Method::GET, url, None).await?;
self.update_providers_proxies(providers_proxies); Ok(providers_proxies)
Ok(self)
} }
pub async fn close_all_connections(&self) -> Result<(), String> { pub async fn close_all_connections(&self) -> Result<(), String> {
@@ -1,27 +1,5 @@
use std::sync::{Arc, Mutex};
pub struct MihomoData {
pub(crate) proxies: serde_json::Value,
pub(crate) providers_proxies: serde_json::Value,
}
#[derive(Clone)] #[derive(Clone)]
pub struct MihomoManager { pub struct MihomoManager {
pub(crate) mihomo_server: String, pub(crate) mihomo_server: String,
pub(crate) data: Arc<Mutex<MihomoData>>,
pub(crate) client: reqwest::Client, pub(crate) client: reqwest::Client,
} }
#[cfg(feature = "debug")]
impl Drop for MihomoData {
fn drop(&mut self) {
println!("Dropping MihomoData");
}
}
#[cfg(feature = "debug")]
impl Drop for MihomoManager {
fn drop(&mut self) {
println!("Dropping MihomoManager");
}
}
@@ -1,29 +1,7 @@
use mihomo_api;
use reqwest::header::HeaderMap; use reqwest::header::HeaderMap;
#[test] #[test]
fn test_mihomo_manager_init() { fn test_mihomo_manager_init() {
let manager = mihomo_api::MihomoManager::new("url".into(), HeaderMap::new()); let _ = mihomo_api::MihomoManager::new("url".into(), HeaderMap::new());
assert_eq!(manager.get_proxies(), serde_json::Value::Null); assert_eq!(true, true);
assert_eq!(manager.get_providers_proxies(), serde_json::Value::Null);
}
#[tokio::test]
async fn test_refresh_proxies() {
let manager = mihomo_api::MihomoManager::new("http://127.0.0.1:9097".into(), HeaderMap::new());
let manager = manager.refresh_proxies().await.unwrap();
let proxies = manager.get_proxies();
let providers = manager.get_providers_proxies();
assert_ne!(proxies, serde_json::Value::Null);
assert_eq!(providers, serde_json::Value::Null);
}
#[tokio::test]
async fn test_refresh_providers_proxies() {
let manager = mihomo_api::MihomoManager::new("http://127.0.0.1:9097".into(), HeaderMap::new());
let manager = manager.refresh_providers_proxies().await.unwrap();
let proxies = manager.get_proxies();
let providers = manager.get_providers_proxies();
assert_eq!(proxies, serde_json::Value::Null);
assert_ne!(providers, serde_json::Value::Null);
} }
+5 -21
View File
@@ -11,15 +11,9 @@
"icons/icon.icns", "icons/icon.icns",
"icons/icon.ico" "icons/icon.ico"
], ],
"resources": [ "resources": ["resources", "resources/locales/*"],
"resources",
"resources/locales/*"
],
"publisher": "Clash Verge Rev", "publisher": "Clash Verge Rev",
"externalBin": [ "externalBin": ["sidecar/verge-mihomo", "sidecar/verge-mihomo-alpha"],
"sidecar/verge-mihomo",
"sidecar/verge-mihomo-alpha"
],
"copyright": "GNU General Public License v3.0", "copyright": "GNU General Public License v3.0",
"category": "DeveloperTool", "category": "DeveloperTool",
"shortDescription": "Clash Verge Rev", "shortDescription": "Clash Verge Rev",
@@ -50,25 +44,15 @@
}, },
"deep-link": { "deep-link": {
"desktop": { "desktop": {
"schemes": [ "schemes": ["clash", "clash-verge"]
"clash",
"clash-verge"
]
} }
} }
}, },
"app": { "app": {
"security": { "security": {
"capabilities": [ "capabilities": ["desktop-capability", "migrated"],
"desktop-capability",
"migrated"
],
"assetProtocol": { "assetProtocol": {
"scope": [ "scope": ["$APPDATA/**", "$RESOURCE/../**", "**"],
"$APPDATA/**",
"$RESOURCE/../**",
"**"
],
"enable": true "enable": true
}, },
"csp": null "csp": null
@@ -23,7 +23,11 @@
"app": { "app": {
"windows": [], "windows": [],
"security": { "security": {
"capabilities": ["desktop-capability", "desktop-windows", "migrated"] "capabilities": [
"desktop-capability",
"desktop-windows-capability",
"migrated"
]
} }
} }
} }
+3 -3
View File
@@ -4,9 +4,9 @@
body { body {
margin: 0; margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", font-family:
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu",
sans-serif; "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
user-select: none; user-select: none;
@@ -1,7 +1,11 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from "react";
import { Snackbar, Alert, IconButton, Box } from '@mui/material'; import { Snackbar, Alert, IconButton, Box } from "@mui/material";
import { CloseRounded } from '@mui/icons-material'; import { CloseRounded } from "@mui/icons-material";
import { subscribeNotices, hideNotice, NoticeItem } from '@/services/noticeService'; import {
subscribeNotices,
hideNotice,
NoticeItem,
} from "@/services/noticeService";
export const NoticeManager: React.FC = () => { export const NoticeManager: React.FC = () => {
const [currentNotices, setCurrentNotices] = useState<NoticeItem[]>([]); const [currentNotices, setCurrentNotices] = useState<NoticeItem[]>([]);
@@ -23,47 +27,47 @@ export const NoticeManager: React.FC = () => {
return ( return (
<Box <Box
sx={{ sx={{
position: 'fixed', position: "fixed",
top: '20px', top: "20px",
right: '20px', right: "20px",
zIndex: 1500, zIndex: 1500,
display: 'flex', display: "flex",
flexDirection: 'column', flexDirection: "column",
gap: '10px', gap: "10px",
maxWidth: '360px', maxWidth: "360px",
}} }}
> >
{currentNotices.map((notice) => ( {currentNotices.map((notice) => (
<Snackbar <Snackbar
key={notice.id} key={notice.id}
open={true} open={true}
anchorOrigin={{ vertical: 'top', horizontal: 'right' }} anchorOrigin={{ vertical: "top", horizontal: "right" }}
sx={{ sx={{
position: 'relative', position: "relative",
transform: 'none', transform: "none",
top: 'auto', top: "auto",
right: 'auto', right: "auto",
bottom: 'auto', bottom: "auto",
left: 'auto', left: "auto",
width: '100%', width: "100%",
}} }}
> >
<Alert <Alert
severity={notice.type} severity={notice.type}
variant="filled" variant="filled"
sx={{ width: '100%' }} sx={{ width: "100%" }}
action={ action={
<IconButton <IconButton
size="small" size="small"
color="inherit" color="inherit"
onClick={() => handleClose(notice.id)} onClick={() => handleClose(notice.id)}
> >
<CloseRounded fontSize="inherit" /> <CloseRounded fontSize="inherit" />
</IconButton> </IconButton>
} }
> >
{notice.message} {notice.message}
</Alert> </Alert>
</Snackbar> </Snackbar>
))} ))}
</Box> </Box>
@@ -157,7 +157,7 @@ export const BaseSearchBox = (props: SearchProps) => {
</Tooltip> </Tooltip>
</Box> </Box>
), ),
} },
}} }}
/> />
</Tooltip> </Tooltip>
@@ -107,7 +107,14 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
{information.map((each) => ( {information.map((each) => (
<div key={each.label}> <div key={each.label}>
<b>{each.label}</b> <b>{each.label}</b>
<span style={{ wordBreak: "break-all", color: theme.palette.text.primary }}>: {each.value}</span> <span
style={{
wordBreak: "break-all",
color: theme.palette.text.primary,
}}
>
: {each.value}
</span>
</div> </div>
))} ))}
@@ -24,11 +24,14 @@ export const ClashModeCard = () => {
const currentMode = clashConfig?.mode?.toLowerCase(); const currentMode = clashConfig?.mode?.toLowerCase();
// 模式图标映射 // 模式图标映射
const modeIcons = useMemo(() => ({ const modeIcons = useMemo(
rule: <MultipleStopRounded fontSize="small" />, () => ({
global: <LanguageRounded fontSize="small" />, rule: <MultipleStopRounded fontSize="small" />,
direct: <DirectionsRounded fontSize="small" /> global: <LanguageRounded fontSize="small" />,
}), []); direct: <DirectionsRounded fontSize="small" />,
}),
[],
);
// 切换模式的处理函数 // 切换模式的处理函数
const onChangeMode = useLockFn(async (mode: string) => { const onChangeMode = useLockFn(async (mode: string) => {
@@ -68,18 +71,19 @@ export const ClashModeCard = () => {
"&:active": { "&:active": {
transform: "translateY(1px)", transform: "translateY(1px)",
}, },
"&::after": mode === currentMode "&::after":
? { mode === currentMode
content: '""', ? {
position: "absolute", content: '""',
bottom: -16, position: "absolute",
left: "50%", bottom: -16,
width: 2, left: "50%",
height: 16, width: 2,
bgcolor: "primary.main", height: 16,
transform: "translateX(-50%)", bgcolor: "primary.main",
} transform: "translateX(-50%)",
: {}, }
: {},
}); });
// 描述样式 // 描述样式
@@ -143,12 +147,10 @@ export const ClashModeCard = () => {
overflow: "visible", overflow: "visible",
}} }}
> >
<Typography <Typography variant="caption" component="div" sx={descriptionStyles}>
variant="caption" {t(
component="div" `${currentMode?.charAt(0).toUpperCase()}${currentMode?.slice(1)} Mode Description`,
sx={descriptionStyles} )}
>
{t(`${currentMode?.charAt(0).toUpperCase()}${currentMode?.slice(1)} Mode Description`)}
</Typography> </Typography>
</Box> </Box>
</Box> </Box>
@@ -105,7 +105,7 @@ export const CurrentProxyCard = () => {
// 添加排序类型状态 // 添加排序类型状态
const [sortType, setSortType] = useState<ProxySortType>(() => { const [sortType, setSortType] = useState<ProxySortType>(() => {
const savedSortType = localStorage.getItem(STORAGE_KEY_SORT_TYPE); const savedSortType = localStorage.getItem(STORAGE_KEY_SORT_TYPE);
return savedSortType ? Number(savedSortType) as ProxySortType : 0; return savedSortType ? (Number(savedSortType) as ProxySortType) : 0;
}); });
// 定义状态类型 // 定义状态类型
@@ -156,7 +156,8 @@ export const CurrentProxyCard = () => {
primaryKeywords.some((keyword) => primaryKeywords.some((keyword) =>
group.name.toLowerCase().includes(keyword.toLowerCase()), group.name.toLowerCase().includes(keyword.toLowerCase()),
), ),
) || proxies.groups.filter((g: { name: string }) => g.name !== "GLOBAL")[0]; ) ||
proxies.groups.filter((g: { name: string }) => g.name !== "GLOBAL")[0];
return primaryGroup?.name || ""; return primaryGroup?.name || "";
}; };
@@ -200,11 +201,13 @@ export const CurrentProxyCard = () => {
// 只保留 Selector 类型的组用于选择 // 只保留 Selector 类型的组用于选择
const filteredGroups = proxies.groups const filteredGroups = proxies.groups
.filter((g: { name: string; type?: string }) => g.type === "Selector") .filter((g: { name: string; type?: string }) => g.type === "Selector")
.map((g: { name: string; now: string; all: Array<{ name: string }> }) => ({ .map(
name: g.name, (g: { name: string; now: string; all: Array<{ name: string }> }) => ({
now: g.now || "", name: g.name,
all: g.all.map((p: { name: string }) => p.name), now: g.now || "",
})); all: g.all.map((p: { name: string }) => p.name),
}),
);
let newProxy = ""; let newProxy = "";
let newDisplayProxy = null; let newDisplayProxy = null;
@@ -230,12 +233,12 @@ export const CurrentProxyCard = () => {
if (selectorGroup) { if (selectorGroup) {
newGroup = selectorGroup.name; newGroup = selectorGroup.name;
newProxy = selectorGroup.now || selectorGroup.all[0] || ""; newProxy = selectorGroup.now || selectorGroup.all[0] || "";
newDisplayProxy = proxies.records?.[newProxy] || null; newDisplayProxy = proxies.records?.[newProxy] || null;
if (!isGlobalMode && !isDirectMode) { if (!isGlobalMode && !isDirectMode) {
localStorage.setItem(STORAGE_KEY_GROUP, newGroup); localStorage.setItem(STORAGE_KEY_GROUP, newGroup);
if (newProxy) { if (newProxy) {
localStorage.setItem(STORAGE_KEY_PROXY, newProxy); localStorage.setItem(STORAGE_KEY_PROXY, newProxy);
} }
} }
} }
@@ -280,7 +283,9 @@ export const CurrentProxyCard = () => {
localStorage.setItem(STORAGE_KEY_GROUP, newGroup); localStorage.setItem(STORAGE_KEY_GROUP, newGroup);
setState((prev) => { setState((prev) => {
const group = prev.proxyData.groups.find((g: { name: string }) => g.name === newGroup); const group = prev.proxyData.groups.find(
(g: { name: string }) => g.name === newGroup,
);
if (group) { if (group) {
return { return {
...prev, ...prev,
@@ -368,14 +373,16 @@ export const CurrentProxyCard = () => {
}, [state.displayProxy]); }, [state.displayProxy]);
// 获取当前节点的延迟(增加非空校验) // 获取当前节点的延迟(增加非空校验)
const currentDelay = currentProxy && state.selection.group const currentDelay =
? delayManager.getDelayFix(currentProxy, state.selection.group) currentProxy && state.selection.group
: -1; ? delayManager.getDelayFix(currentProxy, state.selection.group)
: -1;
// 信号图标(增加非空校验) // 信号图标(增加非空校验)
const signalInfo = currentProxy && state.selection.group const signalInfo =
? getSignalIcon(currentDelay) currentProxy && state.selection.group
: { icon: <SignalNone />, text: "未初始化", color: "text.secondary" }; ? getSignalIcon(currentDelay)
: { icon: <SignalNone />, text: "未初始化", color: "text.secondary" };
// 自定义渲染选择框中的值 // 自定义渲染选择框中的值
const renderProxyValue = useCallback( const renderProxyValue = useCallback(
@@ -384,7 +391,7 @@ export const CurrentProxyCard = () => {
const delayValue = delayManager.getDelayFix( const delayValue = delayManager.getDelayFix(
state.proxyData.records[selected], state.proxyData.records[selected],
state.selection.group state.selection.group,
); );
return ( return (
@@ -441,7 +448,7 @@ export const CurrentProxyCard = () => {
return list; return list;
}, },
[sortType, state.proxyData.records, state.selection.group] [sortType, state.proxyData.records, state.selection.group],
); );
// 计算要显示的代理选项(增加非空校验) // 计算要显示的代理选项(增加非空校验)
@@ -452,11 +459,11 @@ export const CurrentProxyCard = () => {
if (isGlobalMode && proxies?.global) { if (isGlobalMode && proxies?.global) {
const options = proxies.global.all const options = proxies.global.all
.filter((p: any) => { .filter((p: any) => {
const name = typeof p === 'string' ? p : p.name; const name = typeof p === "string" ? p : p.name;
return name !== "DIRECT" && name !== "REJECT"; return name !== "DIRECT" && name !== "REJECT";
}) })
.map((p: any) => ({ .map((p: any) => ({
name: typeof p === 'string' ? p : p.name name: typeof p === "string" ? p : p.name,
})); }));
return sortProxies(options); return sortProxies(options);
@@ -464,7 +471,7 @@ export const CurrentProxyCard = () => {
// 规则模式 // 规则模式
const group = state.selection.group const group = state.selection.group
? state.proxyData.groups.find(g => g.name === state.selection.group) ? state.proxyData.groups.find((g) => g.name === state.selection.group)
: null; : null;
if (group) { if (group) {
@@ -473,7 +480,14 @@ export const CurrentProxyCard = () => {
} }
return []; return [];
}, [isDirectMode, isGlobalMode, proxies, state.proxyData, state.selection.group, sortProxies]); }, [
isDirectMode,
isGlobalMode,
proxies,
state.proxyData,
state.selection.group,
sortProxies,
]);
// 获取排序图标 // 获取排序图标
const getSortIcon = () => { const getSortIcon = () => {
@@ -660,12 +674,14 @@ export const CurrentProxyCard = () => {
{isDirectMode {isDirectMode
? null ? null
: proxyOptions.map((proxy, index) => { : proxyOptions.map((proxy, index) => {
const delayValue = state.proxyData.records[proxy.name] && state.selection.group const delayValue =
? delayManager.getDelayFix( state.proxyData.records[proxy.name] &&
state.proxyData.records[proxy.name], state.selection.group
state.selection.group, ? delayManager.getDelayFix(
) state.proxyData.records[proxy.name],
: -1; state.selection.group,
)
: -1;
return ( return (
<MenuItem <MenuItem
key={`${proxy.name}-${index}`} key={`${proxy.name}-${index}`}
@@ -38,7 +38,7 @@ export const EnhancedCard = ({
overflow: "hidden", overflow: "hidden",
textOverflow: "ellipsis", textOverflow: "ellipsis",
whiteSpace: "nowrap", whiteSpace: "nowrap",
display: "block" display: "block",
}; };
return ( return (
@@ -62,13 +62,15 @@ export const EnhancedCard = ({
borderColor: "divider", borderColor: "divider",
}} }}
> >
<Box sx={{ <Box
display: "flex", sx={{
alignItems: "center", display: "flex",
minWidth: 0, alignItems: "center",
flex: 1, minWidth: 0,
overflow: "hidden" flex: 1,
}}> overflow: "hidden",
}}
>
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
@@ -97,9 +99,7 @@ export const EnhancedCard = ({
{title} {title}
</Typography> </Typography>
) : ( ) : (
<Box sx={titleTruncateStyle}> <Box sx={titleTruncateStyle}>{title}</Box>
{title}
</Box>
)} )}
</Box> </Box>
</Box> </Box>
@@ -30,7 +30,7 @@ ChartJS.register(
PointElement, PointElement,
LineElement, LineElement,
Tooltip, Tooltip,
Filler Filler,
); );
// 流量数据项接口 // 流量数据项接口
@@ -54,8 +54,8 @@ type DataPoint = ITrafficItem & { name: string; timestamp: number };
/** /**
* *
*/ */
export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>( export const EnhancedTrafficGraph = memo(
(props, ref) => { forwardRef<EnhancedTrafficGraphRef>((props, ref) => {
const theme = useTheme(); const theme = useTheme();
const { t } = useTranslation(); const { t } = useTranslation();
@@ -70,13 +70,13 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
// 根据时间范围计算保留的数据点数量 // 根据时间范围计算保留的数据点数量
const getMaxPointsByTimeRange = useCallback( const getMaxPointsByTimeRange = useCallback(
(minutes: TimeRange): number => minutes * 60, (minutes: TimeRange): number => minutes * 60,
[] [],
); );
// 最大数据点数量 // 最大数据点数量
const MAX_BUFFER_SIZE = useMemo( const MAX_BUFFER_SIZE = useMemo(
() => getMaxPointsByTimeRange(10), () => getMaxPointsByTimeRange(10),
[getMaxPointsByTimeRange] [getMaxPointsByTimeRange],
); );
// 颜色配置 // 颜色配置
@@ -89,23 +89,28 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
text: theme.palette.text.primary, text: theme.palette.text.primary,
tooltipBorder: theme.palette.divider, tooltipBorder: theme.palette.divider,
}), }),
[theme] [theme],
); );
// 切换时间范围 // 切换时间范围
const handleTimeRangeClick = useCallback((event: React.MouseEvent<SVGTextElement>) => { const handleTimeRangeClick = useCallback(
event.stopPropagation(); (event: React.MouseEvent<SVGTextElement>) => {
setTimeRange((prevRange) => { event.stopPropagation();
return prevRange === 1 ? 5 : prevRange === 5 ? 10 : 1; setTimeRange((prevRange) => {
}); return prevRange === 1 ? 5 : prevRange === 5 ? 10 : 1;
}, []); });
},
[],
);
// 点击图表主体或图例时切换样式 // 点击图表主体或图例时切换样式
const handleToggleStyleClick = useCallback((event: React.MouseEvent<SVGTextElement | HTMLDivElement>) => { const handleToggleStyleClick = useCallback(
event.stopPropagation(); (event: React.MouseEvent<SVGTextElement | HTMLDivElement>) => {
setChartStyle((prev) => (prev === "line" ? "area" : "line")); event.stopPropagation();
}, []); setChartStyle((prev) => (prev === "line" ? "area" : "line"));
},
[],
);
// 初始化数据缓冲区 // 初始化数据缓冲区
useEffect(() => { useEffect(() => {
@@ -121,7 +126,9 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
let nameValue: string; let nameValue: string;
try { try {
if (isNaN(date.getTime())) { if (isNaN(date.getTime())) {
console.warn(`Initial data generation: Invalid date for timestamp ${pointTime}`); console.warn(
`Initial data generation: Invalid date for timestamp ${pointTime}`,
);
nameValue = "??:??:??"; nameValue = "??:??:??";
} else { } else {
nameValue = date.toLocaleTimeString("en-US", { nameValue = date.toLocaleTimeString("en-US", {
@@ -132,7 +139,14 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
}); });
} }
} catch (e) { } catch (e) {
console.error("Error in toLocaleTimeString during initial data gen:", e, "Date:", date, "Timestamp:", pointTime); console.error(
"Error in toLocaleTimeString during initial data gen:",
e,
"Date:",
date,
"Timestamp:",
pointTime,
);
nameValue = "Err:Time"; nameValue = "Err:Time";
} }
@@ -142,7 +156,7 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
timestamp: pointTime, timestamp: pointTime,
name: nameValue, name: nameValue,
}; };
} },
); );
dataBufferRef.current = initialBuffer; dataBufferRef.current = initialBuffer;
@@ -152,45 +166,56 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
setDisplayData(initialBuffer.slice(-pointsToShow)); setDisplayData(initialBuffer.slice(-pointsToShow));
}, [MAX_BUFFER_SIZE, getMaxPointsByTimeRange]); }, [MAX_BUFFER_SIZE, getMaxPointsByTimeRange]);
// 添加数据点方法 // 添加数据点方法
const appendData = useCallback((data: ITrafficItem) => { const appendData = useCallback(
const safeData = { (data: ITrafficItem) => {
up: typeof data.up === "number" && !isNaN(data.up) ? data.up : 0, const safeData = {
down: typeof data.down === "number" && !isNaN(data.down) ? data.down : 0, up: typeof data.up === "number" && !isNaN(data.up) ? data.up : 0,
}; down:
typeof data.down === "number" && !isNaN(data.down) ? data.down : 0,
};
const timestamp = data.timestamp || Date.now(); const timestamp = data.timestamp || Date.now();
const date = new Date(timestamp); const date = new Date(timestamp);
let nameValue: string; let nameValue: string;
try { try {
if (isNaN(date.getTime())) { if (isNaN(date.getTime())) {
console.warn(`appendData: Invalid date for timestamp ${timestamp}`); console.warn(`appendData: Invalid date for timestamp ${timestamp}`);
nameValue = "??:??:??"; nameValue = "??:??:??";
} else { } else {
nameValue = date.toLocaleTimeString("en-US", { nameValue = date.toLocaleTimeString("en-US", {
hour12: false, hour12: false,
hour: "2-digit", hour: "2-digit",
minute: "2-digit", minute: "2-digit",
second: "2-digit", second: "2-digit",
}); });
}
} catch (e) {
console.error(
"Error in toLocaleTimeString in appendData:",
e,
"Date:",
date,
"Timestamp:",
timestamp,
);
nameValue = "Err:Time";
} }
} catch (e) { // 带时间标签的新数据点
console.error("Error in toLocaleTimeString in appendData:", e, "Date:", date, "Timestamp:", timestamp); const newPoint: DataPoint = {
nameValue = "Err:Time"; ...safeData,
} name: nameValue,
// 带时间标签的新数据点 timestamp: timestamp,
const newPoint: DataPoint = { };
...safeData,
name: nameValue,
timestamp: timestamp,
};
const newBuffer = [...dataBufferRef.current.slice(1), newPoint]; const newBuffer = [...dataBufferRef.current.slice(1), newPoint];
dataBufferRef.current = newBuffer; dataBufferRef.current = newBuffer;
const pointsToShow = getMaxPointsByTimeRange(timeRange); const pointsToShow = getMaxPointsByTimeRange(timeRange);
setDisplayData(newBuffer.slice(-pointsToShow)); setDisplayData(newBuffer.slice(-pointsToShow));
}, [timeRange, getMaxPointsByTimeRange]); },
[timeRange, getMaxPointsByTimeRange],
);
// 监听时间范围变化 // 监听时间范围变化
useEffect(() => { useEffect(() => {
@@ -202,7 +227,7 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
// 切换图表样式 // 切换图表样式
const toggleStyle = useCallback(() => { const toggleStyle = useCallback(() => {
setChartStyle((prev) => prev === "line" ? "area" : "line"); setChartStyle((prev) => (prev === "line" ? "area" : "line"));
}, []); }, []);
// 暴露方法给父组件 // 暴露方法给父组件
@@ -212,30 +237,31 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
appendData, appendData,
toggleStyle, toggleStyle,
}), }),
[appendData, toggleStyle] [appendData, toggleStyle],
); );
const formatYAxis = useCallback((value: number | string): string => { const formatYAxis = useCallback((value: number | string): string => {
if (typeof value !== 'number') return String(value); if (typeof value !== "number") return String(value);
const [num, unit] = parseTraffic(value); const [num, unit] = parseTraffic(value);
return `${num}${unit}`; return `${num}${unit}`;
}, []); }, []);
const formatXLabel = useCallback((tickValue: string | number, index: number, ticks: any[]) => { const formatXLabel = useCallback(
const dataPoint = displayData[index as number]; (tickValue: string | number, index: number, ticks: any[]) => {
if (dataPoint && dataPoint.name) { const dataPoint = displayData[index as number];
const parts = dataPoint.name.split(":"); if (dataPoint && dataPoint.name) {
return `${parts[0]}:${parts[1]}`; const parts = dataPoint.name.split(":");
} return `${parts[0]}:${parts[1]}`;
if(typeof tickValue === 'string') { }
const parts = tickValue.split(":"); if (typeof tickValue === "string") {
if (parts.length >= 2) return `${parts[0]}:${parts[1]}`; const parts = tickValue.split(":");
return tickValue; if (parts.length >= 2) return `${parts[0]}:${parts[1]}`;
} return tickValue;
return ''; }
}, [displayData]); return "";
},
[displayData],
);
// 获取当前时间范围文本 // 获取当前时间范围文本
const getTimeRangeText = useCallback(() => { const getTimeRangeText = useCallback(() => {
@@ -243,13 +269,13 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
}, [timeRange, t]); }, [timeRange, t]);
const chartData = useMemo(() => { const chartData = useMemo(() => {
const labels = displayData.map(d => d.name); const labels = displayData.map((d) => d.name);
return { return {
labels, labels,
datasets: [ datasets: [
{ {
label: t("Upload"), label: t("Upload"),
data: displayData.map(d => d.up), data: displayData.map((d) => d.up),
borderColor: colors.up, borderColor: colors.up,
backgroundColor: chartStyle === "area" ? colors.up : colors.up, backgroundColor: chartStyle === "area" ? colors.up : colors.up,
fill: chartStyle === "area", fill: chartStyle === "area",
@@ -260,7 +286,7 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
}, },
{ {
label: t("Download"), label: t("Download"),
data: displayData.map(d => d.down), data: displayData.map((d) => d.down),
borderColor: colors.down, borderColor: colors.down,
backgroundColor: chartStyle === "area" ? colors.down : colors.down, backgroundColor: chartStyle === "area" ? colors.down : colors.down,
fill: chartStyle === "area", fill: chartStyle === "area",
@@ -268,113 +294,130 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
pointRadius: 0, pointRadius: 0,
pointHoverRadius: 4, pointHoverRadius: 4,
borderWidth: 2, borderWidth: 2,
} },
] ],
}; };
}, [displayData, colors.up, colors.down, t, chartStyle]); }, [displayData, colors.up, colors.down, t, chartStyle]);
const chartOptions = useMemo(() => ({ const chartOptions = useMemo(
responsive: true, () => ({
maintainAspectRatio: false, responsive: true,
animation: false as false, maintainAspectRatio: false,
scales: { animation: false as false,
x: { scales: {
display: true, x: {
type: 'category' as const,
labels: displayData.map(d => d.name),
ticks: {
display: true, display: true,
color: colors.text, type: "category" as const,
font: { size: 10 }, labels: displayData.map((d) => d.name),
callback: function(this: Scale, tickValue: string | number, index: number, ticks: Tick[]): string | undefined { ticks: {
let labelToFormat: string | undefined = undefined; display: true,
color: colors.text,
font: { size: 10 },
callback: function (
this: Scale,
tickValue: string | number,
index: number,
ticks: Tick[],
): string | undefined {
let labelToFormat: string | undefined = undefined;
const currentDisplayTick = ticks[index]; const currentDisplayTick = ticks[index];
if (currentDisplayTick && typeof currentDisplayTick.label === 'string') { if (
labelToFormat = currentDisplayTick.label; currentDisplayTick &&
} else { typeof currentDisplayTick.label === "string"
const sourceLabels = displayData.map(d => d.name); ) {
if (typeof tickValue === 'number' && tickValue >= 0 && tickValue < sourceLabels.length) { labelToFormat = currentDisplayTick.label;
labelToFormat = sourceLabels[tickValue]; } else {
} else if (typeof tickValue === 'string') { const sourceLabels = displayData.map((d) => d.name);
labelToFormat = tickValue; if (
typeof tickValue === "number" &&
tickValue >= 0 &&
tickValue < sourceLabels.length
) {
labelToFormat = sourceLabels[tickValue];
} else if (typeof tickValue === "string") {
labelToFormat = tickValue;
}
} }
}
if (typeof labelToFormat !== 'string') { if (typeof labelToFormat !== "string") {
return undefined; return undefined;
} }
const parts: string[] = labelToFormat.split(':'); const parts: string[] = labelToFormat.split(":");
return parts.length >= 2 ? `${parts[0]}:${parts[1]}` : labelToFormat; return parts.length >= 2
? `${parts[0]}:${parts[1]}`
: labelToFormat;
},
autoSkip: true,
maxTicksLimit: Math.max(
5,
Math.floor(displayData.length / (timeRange * 2)),
),
minRotation: 0,
maxRotation: 0,
},
grid: {
display: true,
drawOnChartArea: false,
drawTicks: true,
tickLength: 2,
color: colors.text,
}, },
autoSkip: true,
maxTicksLimit: Math.max(5, Math.floor(displayData.length / (timeRange * 2))),
minRotation: 0,
maxRotation: 0,
}, },
grid: { y: {
display: true, beginAtZero: true,
drawOnChartArea: false, ticks: {
drawTicks: true, color: colors.text,
tickLength: 2, font: { size: 10 },
color: colors.text, callback: formatYAxis,
},
grid: {
display: true,
drawTicks: true,
tickLength: 3,
color: colors.grid,
},
}, },
}, },
y: { plugins: {
beginAtZero: true, tooltip: {
ticks: { enabled: true,
color: colors.text, mode: "index" as const,
font: { size: 10 }, intersect: false,
callback: formatYAxis, backgroundColor: colors.tooltipBg,
}, titleColor: colors.text,
grid: { bodyColor: colors.text,
display: true, borderColor: colors.tooltipBorder,
drawTicks: true, borderWidth: 1,
tickLength: 3, cornerRadius: 4,
color: colors.grid, padding: 8,
callbacks: {
}, title: (tooltipItems: any[]) => {
} return `${t("Time")}: ${tooltipItems[0].label}`;
}, },
plugins: { label: (context: any): string => {
tooltip: { const label = context.dataset.label || "";
enabled: true, const value = context.parsed.y;
mode: 'index' as const, const [num, unit] = parseTraffic(value);
intersect: false, return `${label}: ${num} ${unit}/s`;
backgroundColor: colors.tooltipBg, },
titleColor: colors.text,
bodyColor: colors.text,
borderColor: colors.tooltipBorder,
borderWidth: 1,
cornerRadius: 4,
padding: 8,
callbacks: {
title: (tooltipItems: any[]) => {
return `${t("Time")}: ${tooltipItems[0].label}`;
}, },
label: (context: any): string => { },
const label = context.dataset.label || ''; legend: {
const value = context.parsed.y; display: false,
const [num, unit] = parseTraffic(value); },
return `${label}: ${num} ${unit}/s`;
}
}
}, },
legend: { layout: {
display: false padding: {
} top: 16,
}, right: 7,
layout: { left: 3,
padding: { },
top: 16, },
right: 7, }),
left: 3, [colors, t, formatYAxis, timeRange, displayData],
} );
}
}), [colors, t, formatYAxis, timeRange, displayData]);
return ( return (
<Box <Box
@@ -393,7 +436,16 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
<ChartJsLine data={chartData} options={chartOptions} /> <ChartJsLine data={chartData} options={chartOptions} />
)} )}
<svg width="100%" height="100%" style={{ position: 'absolute', top: 0, left: 0, pointerEvents: 'none' }}> <svg
width="100%"
height="100%"
style={{
position: "absolute",
top: 0,
left: 0,
pointerEvents: "none",
}}
>
<text <text
x="3.5%" x="3.5%"
y="10%" y="10%"
@@ -402,7 +454,7 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
fontSize={11} fontSize={11}
fontWeight="bold" fontWeight="bold"
onClick={handleTimeRangeClick} onClick={handleTimeRangeClick}
style={{ cursor: "pointer", pointerEvents: 'all' }} style={{ cursor: "pointer", pointerEvents: "all" }}
> >
{getTimeRangeText()} {getTimeRangeText()}
</text> </text>
@@ -415,7 +467,7 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
fontSize={12} fontSize={12}
fontWeight="bold" fontWeight="bold"
onClick={handleToggleStyleClick} onClick={handleToggleStyleClick}
style={{ cursor: "pointer", pointerEvents: 'all' }} style={{ cursor: "pointer", pointerEvents: "all" }}
> >
{t("Upload")} {t("Upload")}
</text> </text>
@@ -428,7 +480,7 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
fontSize={12} fontSize={12}
fontWeight="bold" fontWeight="bold"
onClick={handleToggleStyleClick} onClick={handleToggleStyleClick}
style={{ cursor: "pointer", pointerEvents: 'all' }} style={{ cursor: "pointer", pointerEvents: "all" }}
> >
{t("Download")} {t("Download")}
</text> </text>
@@ -436,7 +488,7 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
</div> </div>
</Box> </Box>
); );
}, }),
)); );
EnhancedTrafficGraph.displayName = "EnhancedTrafficGraph"; EnhancedTrafficGraph.displayName = "EnhancedTrafficGraph";
@@ -66,85 +66,90 @@ const CONNECTIONS_UPDATE_INTERVAL = 5000; // 5秒更新一次连接数据
const THROTTLE_TRAFFIC_UPDATE = 500; // 500ms节流流量数据更新 const THROTTLE_TRAFFIC_UPDATE = 500; // 500ms节流流量数据更新
// 统计卡片组件 - 使用memo优化 // 统计卡片组件 - 使用memo优化
const CompactStatCard = memo(({ const CompactStatCard = memo(
icon, ({ icon, title, value, unit, color, onClick }: StatCardProps) => {
title, const theme = useTheme();
value,
unit,
color,
onClick,
}: StatCardProps) => {
const theme = useTheme();
// 获取调色板颜色 - 使用useMemo避免重复计算 // 获取调色板颜色 - 使用useMemo避免重复计算
const colorValue = useMemo(() => { const colorValue = useMemo(() => {
const palette = theme.palette; const palette = theme.palette;
if ( if (
color in palette && color in palette &&
palette[color as keyof typeof palette] && palette[color as keyof typeof palette] &&
"main" in (palette[color as keyof typeof palette] as PaletteColor) "main" in (palette[color as keyof typeof palette] as PaletteColor)
) { ) {
return (palette[color as keyof typeof palette] as PaletteColor).main; return (palette[color as keyof typeof palette] as PaletteColor).main;
} }
return palette.primary.main; return palette.primary.main;
}, [theme.palette, color]); }, [theme.palette, color]);
return ( return (
<Paper <Paper
elevation={0} elevation={0}
sx={{
display: "flex",
alignItems: "center",
borderRadius: 2,
bgcolor: alpha(colorValue, 0.05),
border: `1px solid ${alpha(colorValue, 0.15)}`,
padding: "8px",
transition: "all 0.2s ease-in-out",
cursor: onClick ? "pointer" : "default",
"&:hover": onClick ? {
bgcolor: alpha(colorValue, 0.1),
border: `1px solid ${alpha(colorValue, 0.3)}`,
boxShadow: `0 4px 8px rgba(0,0,0,0.05)`,
} : {},
}}
onClick={onClick}
>
{/* 图标容器 */}
<Grid
component="div"
sx={{ sx={{
mr: 1,
ml: "2px",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "center", borderRadius: 2,
width: 32, bgcolor: alpha(colorValue, 0.05),
height: 32, border: `1px solid ${alpha(colorValue, 0.15)}`,
borderRadius: "50%", padding: "8px",
bgcolor: alpha(colorValue, 0.1), transition: "all 0.2s ease-in-out",
color: colorValue, cursor: onClick ? "pointer" : "default",
"&:hover": onClick
? {
bgcolor: alpha(colorValue, 0.1),
border: `1px solid ${alpha(colorValue, 0.3)}`,
boxShadow: `0 4px 8px rgba(0,0,0,0.05)`,
}
: {},
}} }}
onClick={onClick}
> >
{icon} {/* 图标容器 */}
</Grid> <Grid
component="div"
{/* 文本内容 */} sx={{
<Grid component="div" sx={{ flexGrow: 1, minWidth: 0 }}> mr: 1,
<Typography variant="caption" color="text.secondary" noWrap> ml: "2px",
{title} display: "flex",
</Typography> alignItems: "center",
<Grid component="div" sx={{ display: "flex", alignItems: "baseline" }}> justifyContent: "center",
<Typography variant="body1" fontWeight="bold" noWrap sx={{ mr: 0.5 }}> width: 32,
{value} height: 32,
</Typography> borderRadius: "50%",
<Typography variant="caption" color="text.secondary"> bgcolor: alpha(colorValue, 0.1),
{unit} color: colorValue,
</Typography> }}
>
{icon}
</Grid> </Grid>
</Grid>
</Paper> {/* 文本内容 */}
); <Grid component="div" sx={{ flexGrow: 1, minWidth: 0 }}>
}); <Typography variant="caption" color="text.secondary" noWrap>
{title}
</Typography>
<Grid
component="div"
sx={{ display: "flex", alignItems: "baseline" }}
>
<Typography
variant="body1"
fontWeight="bold"
noWrap
sx={{ mr: 0.5 }}
>
{value}
</Typography>
<Typography variant="caption" color="text.secondary">
{unit}
</Typography>
</Grid>
</Grid>
</Paper>
);
},
);
// 添加显示名称 // 添加显示名称
CompactStatCard.displayName = "CompactStatCard"; CompactStatCard.displayName = "CompactStatCard";
@@ -205,25 +210,25 @@ export const EnhancedTrafficStats = () => {
down: data.down, down: data.down,
timestamp: now, timestamp: now,
}); });
} catch { } } catch {}
return; return;
} }
lastUpdateRef.current.traffic = now; lastUpdateRef.current.traffic = now;
const safeUp = isNaN(data.up) ? 0 : data.up; const safeUp = isNaN(data.up) ? 0 : data.up;
const safeDown = isNaN(data.down) ? 0 : data.down; const safeDown = isNaN(data.down) ? 0 : data.down;
try { try {
setStats(prev => ({ setStats((prev) => ({
...prev, ...prev,
traffic: { up: safeUp, down: safeDown } traffic: { up: safeUp, down: safeDown },
})); }));
} catch { } } catch {}
try { try {
trafficRef.current?.appendData({ trafficRef.current?.appendData({
up: safeUp, up: safeUp,
down: safeDown, down: safeDown,
timestamp: now, timestamp: now,
}); });
} catch { } } catch {}
} }
} catch (err) { } catch (err) {
console.error("[Traffic] 解析数据错误:", err, event.data); console.error("[Traffic] 解析数据错误:", err, event.data);
@@ -235,12 +240,12 @@ export const EnhancedTrafficStats = () => {
try { try {
const data = JSON.parse(event.data) as MemoryUsage; const data = JSON.parse(event.data) as MemoryUsage;
if (data && typeof data.inuse === "number") { if (data && typeof data.inuse === "number") {
setStats(prev => ({ setStats((prev) => ({
...prev, ...prev,
memory: { memory: {
inuse: isNaN(data.inuse) ? 0 : data.inuse, inuse: isNaN(data.inuse) ? 0 : data.inuse,
oslimit: data.oslimit, oslimit: data.oslimit,
} },
})); }));
} }
} catch (err) { } catch (err) {
@@ -257,7 +262,7 @@ export const EnhancedTrafficStats = () => {
// 清理现有连接的函数 // 清理现有连接的函数
const cleanupSockets = () => { const cleanupSockets = () => {
Object.values(socketRefs.current).forEach(socket => { Object.values(socketRefs.current).forEach((socket) => {
if (socket) { if (socket) {
socket.close(); socket.close();
} }
@@ -269,40 +274,78 @@ export const EnhancedTrafficStats = () => {
cleanupSockets(); cleanupSockets();
// 创建新连接 // 创建新连接
console.log(`[Traffic][${EnhancedTrafficStats.name}] 正在连接: ${server}/traffic`); console.log(
socketRefs.current.traffic = createAuthSockette(`${server}/traffic`, secret, { `[Traffic][${EnhancedTrafficStats.name}] 正在连接: ${server}/traffic`,
onmessage: handleTrafficUpdate, );
onopen: (event) => { socketRefs.current.traffic = createAuthSockette(
console.log(`[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接已建立`, event); `${server}/traffic`,
secret,
{
onmessage: handleTrafficUpdate,
onopen: (event) => {
console.log(
`[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接已建立`,
event,
);
},
onerror: (event) => {
console.error(
`[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接错误或达到最大重试次数`,
event,
);
setStats((prev) => ({ ...prev, traffic: { up: 0, down: 0 } }));
},
onclose: (event) => {
console.log(
`[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接关闭`,
event.code,
event.reason,
);
if (event.code !== 1000 && event.code !== 1001) {
console.warn(
`[Traffic][${EnhancedTrafficStats.name}] 连接非正常关闭,重置状态`,
);
setStats((prev) => ({ ...prev, traffic: { up: 0, down: 0 } }));
}
},
}, },
onerror: (event) => { );
console.error(`[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接错误或达到最大重试次数`, event);
setStats(prev => ({ ...prev, traffic: { up: 0, down: 0 } }));
},
onclose: (event) => {
console.log(`[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接关闭`, event.code, event.reason);
if (event.code !== 1000 && event.code !== 1001) {
console.warn(`[Traffic][${EnhancedTrafficStats.name}] 连接非正常关闭,重置状态`);
setStats(prev => ({ ...prev, traffic: { up: 0, down: 0 } }));
}
},
});
console.log(`[Memory][${EnhancedTrafficStats.name}] 正在连接: ${server}/memory`); console.log(
`[Memory][${EnhancedTrafficStats.name}] 正在连接: ${server}/memory`,
);
socketRefs.current.memory = createAuthSockette(`${server}/memory`, secret, { socketRefs.current.memory = createAuthSockette(`${server}/memory`, secret, {
onmessage: handleMemoryUpdate, onmessage: handleMemoryUpdate,
onopen: (event) => { onopen: (event) => {
console.log(`[Memory][${EnhancedTrafficStats.name}] WebSocket 连接已建立`, event); console.log(
`[Memory][${EnhancedTrafficStats.name}] WebSocket 连接已建立`,
event,
);
}, },
onerror: (event) => { onerror: (event) => {
console.error(`[Memory][${EnhancedTrafficStats.name}] WebSocket 连接错误或达到最大重试次数`, event); console.error(
setStats(prev => ({ ...prev, memory: { inuse: 0, oslimit: undefined } })); `[Memory][${EnhancedTrafficStats.name}] WebSocket 连接错误或达到最大重试次数`,
event,
);
setStats((prev) => ({
...prev,
memory: { inuse: 0, oslimit: undefined },
}));
}, },
onclose: (event) => { onclose: (event) => {
console.log(`[Memory][${EnhancedTrafficStats.name}] WebSocket 连接关闭`, event.code, event.reason); console.log(
`[Memory][${EnhancedTrafficStats.name}] WebSocket 连接关闭`,
event.code,
event.reason,
);
if (event.code !== 1000 && event.code !== 1001) { if (event.code !== 1000 && event.code !== 1001) {
console.warn(`[Memory][${EnhancedTrafficStats.name}] 连接非正常关闭,重置状态`); console.warn(
setStats(prev => ({ ...prev, memory: { inuse: 0, oslimit: undefined } })); `[Memory][${EnhancedTrafficStats.name}] 连接非正常关闭,重置状态`,
);
setStats((prev) => ({
...prev,
memory: { inuse: 0, oslimit: undefined },
}));
} }
}, },
}); });
@@ -314,11 +357,11 @@ export const EnhancedTrafficStats = () => {
useEffect(() => { useEffect(() => {
return () => { return () => {
try { try {
Object.values(socketRefs.current).forEach(socket => { Object.values(socketRefs.current).forEach((socket) => {
if (socket) socket.close(); if (socket) socket.close();
}); });
socketRefs.current = { traffic: null, memory: null }; socketRefs.current = { traffic: null, memory: null };
} catch { } } catch {}
}; };
}, []); }, []);
@@ -339,13 +382,25 @@ export const EnhancedTrafficStats = () => {
const [up, upUnit] = parseTraffic(stats.traffic.up); const [up, upUnit] = parseTraffic(stats.traffic.up);
const [down, downUnit] = parseTraffic(stats.traffic.down); const [down, downUnit] = parseTraffic(stats.traffic.down);
const [inuse, inuseUnit] = parseTraffic(stats.memory.inuse); const [inuse, inuseUnit] = parseTraffic(stats.memory.inuse);
const [uploadTotal, uploadTotalUnit] = parseTraffic(connections.uploadTotal); const [uploadTotal, uploadTotalUnit] = parseTraffic(
const [downloadTotal, downloadTotalUnit] = parseTraffic(connections.downloadTotal); connections.uploadTotal,
);
const [downloadTotal, downloadTotalUnit] = parseTraffic(
connections.downloadTotal,
);
return { return {
up, upUnit, down, downUnit, inuse, inuseUnit, up,
uploadTotal, uploadTotalUnit, downloadTotal, downloadTotalUnit, upUnit,
connectionsCount: connections.count down,
downUnit,
inuse,
inuseUnit,
uploadTotal,
uploadTotalUnit,
downloadTotal,
downloadTotalUnit,
connectionsCount: connections.count,
}; };
}, [stats, connections]); }, [stats, connections]);
@@ -392,51 +447,54 @@ export const EnhancedTrafficStats = () => {
}, [trafficGraph, pageVisible, theme.palette.divider, isDebug]); }, [trafficGraph, pageVisible, theme.palette.divider, isDebug]);
// 使用useMemo计算统计卡片配置 // 使用useMemo计算统计卡片配置
const statCards = useMemo(() => [ const statCards = useMemo(
{ () => [
icon: <ArrowUpwardRounded fontSize="small" />, {
title: t("Upload Speed"), icon: <ArrowUpwardRounded fontSize="small" />,
value: parsedData.up, title: t("Upload Speed"),
unit: `${parsedData.upUnit}/s`, value: parsedData.up,
color: "secondary" as const, unit: `${parsedData.upUnit}/s`,
}, color: "secondary" as const,
{ },
icon: <ArrowDownwardRounded fontSize="small" />, {
title: t("Download Speed"), icon: <ArrowDownwardRounded fontSize="small" />,
value: parsedData.down, title: t("Download Speed"),
unit: `${parsedData.downUnit}/s`, value: parsedData.down,
color: "primary" as const, unit: `${parsedData.downUnit}/s`,
}, color: "primary" as const,
{ },
icon: <LinkRounded fontSize="small" />, {
title: t("Active Connections"), icon: <LinkRounded fontSize="small" />,
value: parsedData.connectionsCount, title: t("Active Connections"),
unit: "", value: parsedData.connectionsCount,
color: "success" as const, unit: "",
}, color: "success" as const,
{ },
icon: <CloudUploadRounded fontSize="small" />, {
title: t("Uploaded"), icon: <CloudUploadRounded fontSize="small" />,
value: parsedData.uploadTotal, title: t("Uploaded"),
unit: parsedData.uploadTotalUnit, value: parsedData.uploadTotal,
color: "secondary" as const, unit: parsedData.uploadTotalUnit,
}, color: "secondary" as const,
{ },
icon: <CloudDownloadRounded fontSize="small" />, {
title: t("Downloaded"), icon: <CloudDownloadRounded fontSize="small" />,
value: parsedData.downloadTotal, title: t("Downloaded"),
unit: parsedData.downloadTotalUnit, value: parsedData.downloadTotal,
color: "primary" as const, unit: parsedData.downloadTotalUnit,
}, color: "primary" as const,
{ },
icon: <MemoryRounded fontSize="small" />, {
title: t("Memory Usage"), icon: <MemoryRounded fontSize="small" />,
value: parsedData.inuse, title: t("Memory Usage"),
unit: parsedData.inuseUnit, value: parsedData.inuse,
color: "error" as const, unit: parsedData.inuseUnit,
onClick: isDebug ? handleGarbageCollection : undefined, color: "error" as const,
}, onClick: isDebug ? handleGarbageCollection : undefined,
], [t, parsedData, isDebug, handleGarbageCollection]); },
],
[t, parsedData, isDebug, handleGarbageCollection],
);
return ( return (
<Grid container spacing={1} columns={{ xs: 8, sm: 8, md: 12 }}> <Grid container spacing={1} columns={{ xs: 8, sm: 8, md: 12 }}>
@@ -78,11 +78,15 @@ const truncateStyle = {
maxWidth: "calc(100% - 28px)", maxWidth: "calc(100% - 28px)",
overflow: "hidden", overflow: "hidden",
textOverflow: "ellipsis", textOverflow: "ellipsis",
whiteSpace: "nowrap" whiteSpace: "nowrap",
}; };
// 提取独立组件减少主组件复杂度 // 提取独立组件减少主组件复杂度
const ProfileDetails = ({ current, onUpdateProfile, updating }: { const ProfileDetails = ({
current,
onUpdateProfile,
updating,
}: {
current: ProfileItem; current: ProfileItem;
onUpdateProfile: () => void; onUpdateProfile: () => void;
updating: boolean; updating: boolean;
@@ -99,7 +103,7 @@ const ProfileDetails = ({ current, onUpdateProfile, updating }: {
if (!current.extra || !current.extra.total) return 1; if (!current.extra || !current.extra.total) return 1;
return Math.min( return Math.min(
Math.round((usedTraffic * 100) / (current.extra.total + 0.01)) + 1, Math.round((usedTraffic * 100) / (current.extra.total + 0.01)) + 1,
100 100,
); );
}, [current.extra, usedTraffic]); }, [current.extra, usedTraffic]);
@@ -109,7 +113,12 @@ const ProfileDetails = ({ current, onUpdateProfile, updating }: {
{current.url && ( {current.url && (
<Stack direction="row" alignItems="center" spacing={1}> <Stack direction="row" alignItems="center" spacing={1}>
<DnsOutlined fontSize="small" color="action" /> <DnsOutlined fontSize="small" color="action" />
<Typography variant="body2" color="text.secondary" noWrap sx={{ display: "flex", alignItems: "center" }}> <Typography
variant="body2"
color="text.secondary"
noWrap
sx={{ display: "flex", alignItems: "center" }}
>
<span style={{ flexShrink: 0 }}>{t("From")}: </span> <span style={{ flexShrink: 0 }}>{t("From")}: </span>
{current.home ? ( {current.home ? (
<Link <Link
@@ -121,7 +130,7 @@ const ProfileDetails = ({ current, onUpdateProfile, updating }: {
alignItems: "center", alignItems: "center",
minWidth: 0, minWidth: 0,
maxWidth: "calc(100% - 40px)", maxWidth: "calc(100% - 40px)",
ml: 0.5 ml: 0.5,
}} }}
title={parseUrl(current.url)} title={parseUrl(current.url)}
> >
@@ -132,14 +141,19 @@ const ProfileDetails = ({ current, onUpdateProfile, updating }: {
textOverflow: "ellipsis", textOverflow: "ellipsis",
whiteSpace: "nowrap", whiteSpace: "nowrap",
minWidth: 0, minWidth: 0,
flex: 1 flex: 1,
}} }}
> >
{parseUrl(current.url)} {parseUrl(current.url)}
</Typography> </Typography>
<LaunchOutlined <LaunchOutlined
fontSize="inherit" fontSize="inherit"
sx={{ ml: 0.5, fontSize: "0.8rem", opacity: 0.7, flexShrink: 0 }} sx={{
ml: 0.5,
fontSize: "0.8rem",
opacity: 0.7,
flexShrink: 0,
}}
/> />
</Link> </Link>
) : ( ) : (
@@ -152,7 +166,7 @@ const ProfileDetails = ({ current, onUpdateProfile, updating }: {
whiteSpace: "nowrap", whiteSpace: "nowrap",
minWidth: 0, minWidth: 0,
flex: 1, flex: 1,
ml: 0.5 ml: 0.5,
}} }}
title={parseUrl(current.url)} title={parseUrl(current.url)}
> >
@@ -195,7 +209,8 @@ const ProfileDetails = ({ current, onUpdateProfile, updating }: {
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
{t("Used / Total")}:{" "} {t("Used / Total")}:{" "}
<Box component="span" fontWeight="medium"> <Box component="span" fontWeight="medium">
{parseTraffic(usedTraffic)} / {parseTraffic(current.extra.total)} {parseTraffic(usedTraffic)} /{" "}
{parseTraffic(current.extra.total)}
</Box> </Box>
</Typography> </Typography>
</Stack> </Stack>
@@ -268,7 +283,10 @@ const EmptyProfile = ({ onClick }: { onClick: () => void }) => {
); );
}; };
export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardProps) => { export const HomeProfileCard = ({
current,
onProfileUpdated,
}: HomeProfileCardProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const { refreshAll } = useAppData(); const { refreshAll } = useAppData();
@@ -282,13 +300,13 @@ export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardPr
setUpdating(true); setUpdating(true);
try { try {
await updateProfile(current.uid, current.option); await updateProfile(current.uid, current.option);
showNotice('success', t("Update subscription successfully"), 1000); showNotice("success", t("Update subscription successfully"), 1000);
onProfileUpdated?.(); onProfileUpdated?.();
// 刷新首页数据 // 刷新首页数据
refreshAll(); refreshAll();
} catch (err: any) { } catch (err: any) {
showNotice('error', err.message || err.toString(), 3000); showNotice("error", err.message || err.toString(), 3000);
} finally { } finally {
setUpdating(false); setUpdating(false);
} }
@@ -323,8 +341,8 @@ export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardPr
overflow: "hidden", overflow: "hidden",
textOverflow: "ellipsis", textOverflow: "ellipsis",
whiteSpace: "nowrap", whiteSpace: "nowrap",
flex: 1 flex: 1,
} },
}} }}
title={current.name} title={current.name}
> >
@@ -335,7 +353,7 @@ export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardPr
ml: 0.5, ml: 0.5,
fontSize: "0.8rem", fontSize: "0.8rem",
opacity: 0.7, opacity: 0.7,
flexShrink: 0 flexShrink: 0,
}} }}
/> />
</Link> </Link>
@@ -112,7 +112,7 @@ export const IpInfoCard = () => {
}, [fetchIpInfo]); }, [fetchIpInfo]);
const toggleShowIp = useCallback(() => { const toggleShowIp = useCallback(() => {
setShowIp(prev => !prev); setShowIp((prev) => !prev);
}, []); }, []);
// 渲染加载状态 // 渲染加载状态
@@ -282,9 +282,7 @@ export const IpInfoCard = () => {
<InfoItem label={t("ORG")} value={ipInfo?.asn_organization} /> <InfoItem label={t("ORG")} value={ipInfo?.asn_organization} />
<InfoItem <InfoItem
label={t("Location")} label={t("Location")}
value={[ipInfo?.city, ipInfo?.region] value={[ipInfo?.city, ipInfo?.region].filter(Boolean).join(", ")}
.filter(Boolean)
.join(", ")}
/> />
<InfoItem label={t("Timezone")} value={ipInfo?.timezone} /> <InfoItem label={t("Timezone")} value={ipInfo?.timezone} />
</Box> </Box>
@@ -1,19 +1,24 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Typography, Stack, Divider, Chip, IconButton, Tooltip } from "@mui/material"; import {
Typography,
Stack,
Divider,
Chip,
IconButton,
Tooltip,
} from "@mui/material";
import { import {
InfoOutlined, InfoOutlined,
SettingsOutlined, SettingsOutlined,
WarningOutlined, WarningOutlined,
AdminPanelSettingsOutlined, AdminPanelSettingsOutlined,
DnsOutlined, DnsOutlined,
ExtensionOutlined ExtensionOutlined,
} from "@mui/icons-material"; } from "@mui/icons-material";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
import { EnhancedCard } from "./enhanced-card"; import { EnhancedCard } from "./enhanced-card";
import useSWR from "swr"; import useSWR from "swr";
import { import { getSystemInfo } from "@/services/cmds";
getSystemInfo,
} from "@/services/cmds";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { version as appVersion } from "@root/package.json"; import { version as appVersion } from "@root/package.json";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
@@ -30,32 +35,35 @@ export const SystemInfoCard = () => {
const { isAdminMode, isSidecarMode, mutateRunningMode } = useSystemState(); const { isAdminMode, isSidecarMode, mutateRunningMode } = useSystemState();
const { installServiceAndRestartCore } = useServiceInstaller(); const { installServiceAndRestartCore } = useServiceInstaller();
// 系统信息状态 // 系统信息状态
const [systemState, setSystemState] = useState({ const [systemState, setSystemState] = useState({
osInfo: "", osInfo: "",
lastCheckUpdate: "-", lastCheckUpdate: "-",
}); });
// 初始化系统信息 // 初始化系统信息
useEffect(() => { useEffect(() => {
getSystemInfo() getSystemInfo()
.then((info) => { .then((info) => {
const lines = info.split("\n"); const lines = info.split("\n");
if (lines.length > 0) { if (lines.length > 0) {
const sysName = lines[0].split(": ")[1] || ""; const sysName = lines[0].split(": ")[1] || "";
let sysVersion = lines[1].split(": ")[1] || ""; let sysVersion = lines[1].split(": ")[1] || "";
if (sysName && sysVersion.toLowerCase().startsWith(sysName.toLowerCase())) { if (
sysVersion = sysVersion.substring(sysName.length).trim(); sysName &&
sysVersion.toLowerCase().startsWith(sysName.toLowerCase())
) {
sysVersion = sysVersion.substring(sysName.length).trim();
}
setSystemState((prev) => ({
...prev,
osInfo: `${sysName} ${sysVersion}`,
}));
} }
})
setSystemState((prev) => ({ .catch(console.error);
...prev,
osInfo: `${sysName} ${sysVersion}`,
}));
}
})
.catch(console.error);
// 获取最后检查更新时间 // 获取最后检查更新时间
const lastCheck = localStorage.getItem("last_check_update"); const lastCheck = localStorage.getItem("last_check_update");
@@ -122,7 +130,6 @@ useEffect(() => {
} }
}, [verge, patchVerge]); }, [verge, patchVerge]);
// 点击运行模式处理,Sidecar或纯管理员模式允许安装服务 // 点击运行模式处理,Sidecar或纯管理员模式允许安装服务
const handleRunningModeClick = useCallback(() => { const handleRunningModeClick = useCallback(() => {
if (isSidecarMode || (isAdminMode && isSidecarMode)) { if (isSidecarMode || (isAdminMode && isSidecarMode)) {
@@ -135,13 +142,13 @@ useEffect(() => {
try { try {
const info = await checkUpdate(); const info = await checkUpdate();
if (!info?.available) { if (!info?.available) {
showNotice('success', t("Currently on the Latest Version")); showNotice("success", t("Currently on the Latest Version"));
} else { } else {
showNotice('info', t("Update Available"), 2000); showNotice("info", t("Update Available"), 2000);
goToSettings(); goToSettings();
} }
} catch (err: any) { } catch (err: any) {
showNotice('error', err.message || err.toString()); showNotice("error", err.message || err.toString());
} }
}); });
@@ -155,13 +162,15 @@ useEffect(() => {
const runningModeStyle = useMemo( const runningModeStyle = useMemo(
() => ({ () => ({
// Sidecar或纯管理员模式允许安装服务 // Sidecar或纯管理员模式允许安装服务
cursor: (isSidecarMode || (isAdminMode && isSidecarMode)) ? "pointer" : "default", cursor:
textDecoration: (isSidecarMode || (isAdminMode && isSidecarMode)) ? "underline" : "none", isSidecarMode || (isAdminMode && isSidecarMode) ? "pointer" : "default",
textDecoration:
isSidecarMode || (isAdminMode && isSidecarMode) ? "underline" : "none",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
gap: 0.5, gap: 0.5,
"&:hover": { "&:hover": {
opacity: (isSidecarMode || (isAdminMode && isSidecarMode)) ? 0.7 : 1, opacity: isSidecarMode || (isAdminMode && isSidecarMode) ? 0.7 : 1,
}, },
}), }),
[isSidecarMode, isAdminMode], [isSidecarMode, isAdminMode],
@@ -247,13 +256,19 @@ useEffect(() => {
</Typography> </Typography>
</Stack> </Stack>
<Divider /> <Divider />
<Stack direction="row" justifyContent="space-between" alignItems="center"> <Stack
direction="row"
justifyContent="space-between"
alignItems="center"
>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
{t("Auto Launch")} {t("Auto Launch")}
</Typography> </Typography>
<Stack direction="row" spacing={1} alignItems="center"> <Stack direction="row" spacing={1} alignItems="center">
{isAdminMode && ( {isAdminMode && (
<Tooltip title={t("Administrator mode may not support auto launch")}> <Tooltip
title={t("Administrator mode may not support auto launch")}
>
<WarningOutlined sx={{ color: "warning.main", fontSize: 20 }} /> <WarningOutlined sx={{ color: "warning.main", fontSize: 20 }} />
</Tooltip> </Tooltip>
)} )}
@@ -268,7 +283,11 @@ useEffect(() => {
</Stack> </Stack>
</Stack> </Stack>
<Divider /> <Divider />
<Stack direction="row" justifyContent="space-between" alignItems="center"> <Stack
direction="row"
justifyContent="space-between"
alignItems="center"
>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
{t("Running Mode")} {t("Running Mode")}
</Typography> </Typography>
@@ -87,12 +87,12 @@ export const TestCard = () => {
} }
const newList = testList.map((x) => const newList = testList.map((x) =>
x.uid === uid ? { ...x, ...patch } : x x.uid === uid ? { ...x, ...patch } : x,
); );
mutateVerge({ ...verge, test_list: newList }, false); mutateVerge({ ...verge, test_list: newList }, false);
}, },
[testList, verge, mutateVerge] [testList, verge, mutateVerge],
); );
const onDeleteTestListItem = useCallback( const onDeleteTestListItem = useCallback(
@@ -101,7 +101,7 @@ export const TestCard = () => {
patchVerge({ test_list: newList }); patchVerge({ test_list: newList });
mutateVerge({ ...verge, test_list: newList }, false); mutateVerge({ ...verge, test_list: newList }, false);
}, },
[testList, verge, patchVerge, mutateVerge] [testList, verge, patchVerge, mutateVerge],
); );
const onDragEnd = useCallback( const onDragEnd = useCallback(
@@ -122,7 +122,7 @@ export const TestCard = () => {
const patchFn = () => { const patchFn = () => {
try { try {
patchVerge({ test_list: newList }); patchVerge({ test_list: newList });
} catch { } } catch {}
}; };
if (window.requestIdleCallback) { if (window.requestIdleCallback) {
window.requestIdleCallback(patchFn); window.requestIdleCallback(patchFn);
@@ -131,7 +131,7 @@ export const TestCard = () => {
} }
} }
}, },
[testList, verge, mutateVerge, patchVerge] [testList, verge, mutateVerge, patchVerge],
); );
// 仅在verge首次加载时初始化测试列表 // 仅在verge首次加载时初始化测试列表
@@ -142,22 +142,25 @@ export const TestCard = () => {
}, [verge, patchVerge]); }, [verge, patchVerge]);
// 使用useMemo优化UI内容,减少渲染计算 // 使用useMemo优化UI内容,减少渲染计算
const renderTestItems = useMemo(() => ( const renderTestItems = useMemo(
<Grid container spacing={1} columns={12}> () => (
<SortableContext items={testList.map((x) => x.uid)}> <Grid container spacing={1} columns={12}>
{testList.map((item) => ( <SortableContext items={testList.map((x) => x.uid)}>
<Grid key={item.uid} size={3}> {testList.map((item) => (
<TestItem <Grid key={item.uid} size={3}>
id={item.uid} <TestItem
itemData={item} id={item.uid}
onEdit={() => viewerRef.current?.edit(item)} itemData={item}
onDelete={onDeleteTestListItem} onEdit={() => viewerRef.current?.edit(item)}
/> onDelete={onDeleteTestListItem}
</Grid> />
))} </Grid>
</SortableContext> ))}
</Grid> </SortableContext>
), [testList, onDeleteTestListItem]); </Grid>
),
[testList, onDeleteTestListItem],
);
const handleTestAll = useCallback(() => { const handleTestAll = useCallback(() => {
emit("verge://test-all"); emit("verge://test-all");
@@ -24,7 +24,7 @@ export const UpdateButton = (props: Props) => {
errorRetryCount: 2, errorRetryCount: 2,
revalidateIfStale: false, revalidateIfStale: false,
focusThrottleInterval: 36e5, // 1 hour focusThrottleInterval: 36e5, // 1 hour
} },
); );
if (!updateInfo?.available) return null; if (!updateInfo?.available) return null;
@@ -1,6 +1,9 @@
import { useEffect, useMemo } from "react"; import { useEffect, useMemo } from "react";
import { alpha, createTheme, Shadows, Theme as MuiTheme } from "@mui/material"; import { alpha, createTheme, Shadows, Theme as MuiTheme } from "@mui/material";
import { getCurrentWebviewWindow, WebviewWindow } from "@tauri-apps/api/webviewWindow"; import {
getCurrentWebviewWindow,
WebviewWindow,
} from "@tauri-apps/api/webviewWindow";
import { useSetThemeMode, useThemeMode } from "@/services/states"; import { useSetThemeMode, useThemeMode } from "@/services/states";
import { defaultTheme, defaultDarkTheme } from "@/pages/_theme"; import { defaultTheme, defaultDarkTheme } from "@/pages/_theme";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
@@ -51,13 +54,16 @@ export const useCustomTheme = () => {
const timerId = setTimeout(() => { const timerId = setTimeout(() => {
if (!isMounted) return; if (!isMounted) return;
appWindow.theme().then((systemTheme) => { appWindow
if (isMounted && systemTheme) { .theme()
setMode(systemTheme); .then((systemTheme) => {
} if (isMounted && systemTheme) {
}).catch(err => { setMode(systemTheme);
console.error("Failed to get initial system theme:", err); }
}); })
.catch((err) => {
console.error("Failed to get initial system theme:", err);
});
}, 0); }, 0);
const unlistenPromise = appWindow.onThemeChanged(({ payload }) => { const unlistenPromise = appWindow.onThemeChanged(({ payload }) => {
@@ -69,13 +75,15 @@ export const useCustomTheme = () => {
return () => { return () => {
isMounted = false; isMounted = false;
clearTimeout(timerId); clearTimeout(timerId);
unlistenPromise.then((unlistenFn) => { unlistenPromise
if (typeof unlistenFn === 'function') { .then((unlistenFn) => {
unlistenFn(); if (typeof unlistenFn === "function") {
} unlistenFn();
}).catch(err => { }
console.error("Failed to unlisten from theme changes:", err); })
}); .catch((err) => {
console.error("Failed to unlisten from theme changes:", err);
});
}; };
}, [theme_mode, appWindow, setMode]); }, [theme_mode, appWindow, setMode]);
@@ -86,7 +94,10 @@ export const useCustomTheme = () => {
if (theme_mode === "system") { if (theme_mode === "system") {
appWindow.setTheme(null).catch((err) => { appWindow.setTheme(null).catch((err) => {
console.error("Failed to set window theme to follow system (setTheme(null)):", err); console.error(
"Failed to set window theme to follow system (setTheme(null)):",
err,
);
}); });
} else if (mode) { } else if (mode) {
appWindow.setTheme(mode as TauriOsTheme).catch((err) => { appWindow.setTheme(mode as TauriOsTheme).catch((err) => {
@@ -153,21 +164,24 @@ export const useCustomTheme = () => {
const rootEle = document.documentElement; const rootEle = document.documentElement;
if (rootEle) { if (rootEle) {
const backgroundColor = mode === "light" ? "#ECECEC" : "#2e303d"; const backgroundColor = mode === "light" ? "#ECECEC" : "#2e303d";
const selectColor = mode === "light" ? "#f5f5f5" : "#d5d5d5"; const selectColor = mode === "light" ? "#f5f5f5" : "#d5d5d5";
const scrollColor = mode === "light" ? "#90939980" : "#3E3E3Eee"; const scrollColor = mode === "light" ? "#90939980" : "#3E3E3Eee";
const dividerColor = const dividerColor =
mode === "light" ? "rgba(0, 0, 0, 0.06)" : "rgba(255, 255, 255, 0.06)"; mode === "light" ? "rgba(0, 0, 0, 0.06)" : "rgba(255, 255, 255, 0.06)";
rootEle.style.setProperty("--divider-color", dividerColor); rootEle.style.setProperty("--divider-color", dividerColor);
rootEle.style.setProperty("--background-color", backgroundColor); rootEle.style.setProperty("--background-color", backgroundColor);
rootEle.style.setProperty("--selection-color", selectColor); rootEle.style.setProperty("--selection-color", selectColor);
rootEle.style.setProperty("--scroller-color", scrollColor); rootEle.style.setProperty("--scroller-color", scrollColor);
rootEle.style.setProperty("--primary-main", muiTheme.palette.primary.main); rootEle.style.setProperty(
rootEle.style.setProperty( "--primary-main",
muiTheme.palette.primary.main,
);
rootEle.style.setProperty(
"--background-color-alpha", "--background-color-alpha",
alpha(muiTheme.palette.primary.main, 0.1), alpha(muiTheme.palette.primary.main, 0.1),
); );
} }
// inject css // inject css
let styleElement = document.querySelector("style#verge-theme"); let styleElement = document.querySelector("style#verge-theme");
@@ -127,7 +127,7 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
currData.current = value; currData.current = value;
onChange?.(prevData.current, currData.current); onChange?.(prevData.current, currData.current);
} catch (err: any) { } catch (err: any) {
showNotice('error', err.message || err.toString()); showNotice("error", err.message || err.toString());
} }
}); });
@@ -136,7 +136,7 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
!readOnly && onSave?.(prevData.current, currData.current); !readOnly && onSave?.(prevData.current, currData.current);
onClose(); onClose();
} catch (err: any) { } catch (err: any) {
showNotice('error', err.message || err.toString()); showNotice("error", err.message || err.toString());
} }
}); });
@@ -144,7 +144,7 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
try { try {
onClose(); onClose();
} catch (err: any) { } catch (err: any) {
showNotice('error', err.message || err.toString()); showNotice("error", err.message || err.toString());
} }
}); });
@@ -70,8 +70,8 @@ export const GroupItem = (props: Props) => {
? alpha(palette.background.paper, 0.3) ? alpha(palette.background.paper, 0.3)
: alpha(palette.grey[400], 0.3) : alpha(palette.grey[400], 0.3)
: type === "delete" : type === "delete"
? alpha(palette.error.main, 0.3) ? alpha(palette.error.main, 0.3)
: alpha(palette.success.main, 0.3), : alpha(palette.success.main, 0.3),
height: "100%", height: "100%",
margin: "8px 0", margin: "8px 0",
borderRadius: "8px", borderRadius: "8px",
@@ -90,27 +90,27 @@ export const GroupsEditorViewer = (props: Props) => {
const filteredPrependSeq = useMemo( const filteredPrependSeq = useMemo(
() => prependSeq.filter((group) => match(group.name)), () => prependSeq.filter((group) => match(group.name)),
[prependSeq, match] [prependSeq, match],
); );
const filteredGroupList = useMemo( const filteredGroupList = useMemo(
() => groupList.filter((group) => match(group.name)), () => groupList.filter((group) => match(group.name)),
[groupList, match] [groupList, match],
); );
const filteredAppendSeq = useMemo( const filteredAppendSeq = useMemo(
() => appendSeq.filter((group) => match(group.name)), () => appendSeq.filter((group) => match(group.name)),
[appendSeq, match] [appendSeq, match],
); );
const sensors = useSensors( const sensors = useSensors(
useSensor(PointerSensor), useSensor(PointerSensor),
useSensor(KeyboardSensor, { useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates, coordinateGetter: sortableKeyboardCoordinates,
}) }),
); );
const reorder = ( const reorder = (
list: IProxyGroupConfig[], list: IProxyGroupConfig[],
startIndex: number, startIndex: number,
endIndex: number endIndex: number,
) => { ) => {
const result = Array.from(list); const result = Array.from(list);
const [removed] = result.splice(startIndex, 1); const [removed] = result.splice(startIndex, 1);
@@ -188,8 +188,8 @@ export const GroupsEditorViewer = (props: Props) => {
setCurrData( setCurrData(
yaml.dump( yaml.dump(
{ prepend: prependSeq, append: appendSeq, delete: deleteSeq }, { prepend: prependSeq, append: appendSeq, delete: deleteSeq },
{ forceQuotes: true } { forceQuotes: true },
) ),
); );
} catch (e) { } catch (e) {
// 防止异常导致UI卡死 // 防止异常导致UI卡死
@@ -226,7 +226,7 @@ export const GroupsEditorViewer = (props: Props) => {
return !moreDeleteProxies.includes(proxy); return !moreDeleteProxies.includes(proxy);
} }
}), }),
moreAppendProxies moreAppendProxies,
); );
setProxyPolicyList( setProxyPolicyList(
@@ -236,8 +236,8 @@ export const GroupsEditorViewer = (props: Props) => {
.map((group: IProxyGroupConfig) => group.name) .map((group: IProxyGroupConfig) => group.name)
.filter((name) => !deleteSeq.includes(name)) || [], .filter((name) => !deleteSeq.includes(name)) || [],
appendSeq.map((group: IProxyGroupConfig) => group.name), appendSeq.map((group: IProxyGroupConfig) => group.name),
proxies.map((proxy: any) => proxy.name) proxies.map((proxy: any) => proxy.name),
) ),
); );
}; };
const fetchProfile = async () => { const fetchProfile = async () => {
@@ -266,7 +266,7 @@ export const GroupsEditorViewer = (props: Props) => {
{}, {},
originProvider, originProvider,
moreProvider, moreProvider,
globalProvider globalProvider,
); );
setProxyProviderList(Object.keys(provider)); setProxyProviderList(Object.keys(provider));
@@ -297,11 +297,11 @@ export const GroupsEditorViewer = (props: Props) => {
const handleSave = useLockFn(async () => { const handleSave = useLockFn(async () => {
try { try {
await saveProfileFile(property, currData); await saveProfileFile(property, currData);
showNotice('success', t("Saved Successfully")); showNotice("success", t("Saved Successfully"));
onSave?.(prevData, currData); onSave?.(prevData, currData);
onClose(); onClose();
} catch (err: any) { } catch (err: any) {
showNotice('error', err.toString()); showNotice("error", err.toString());
} }
}); });
@@ -502,7 +502,7 @@ export const GroupsEditorViewer = (props: Props) => {
{t("seconds")} {t("seconds")}
</InputAdornment> </InputAdornment>
), ),
} },
}} }}
/> />
</Item> </Item>
@@ -530,7 +530,7 @@ export const GroupsEditorViewer = (props: Props) => {
{t("millis")} {t("millis")}
</InputAdornment> </InputAdornment>
), ),
} },
}} }}
/> />
</Item> </Item>
@@ -742,7 +742,7 @@ export const GroupsEditorViewer = (props: Props) => {
} }
setPrependSeq([formIns.getValues(), ...prependSeq]); setPrependSeq([formIns.getValues(), ...prependSeq]);
} catch (err: any) { } catch (err: any) {
showNotice('error', err.message || err.toString()); showNotice("error", err.message || err.toString());
} }
}} }}
> >
@@ -764,7 +764,7 @@ export const GroupsEditorViewer = (props: Props) => {
} }
setAppendSeq([...appendSeq, formIns.getValues()]); setAppendSeq([...appendSeq, formIns.getValues()]);
} catch (err: any) { } catch (err: any) {
showNotice('error', err.message || err.toString()); showNotice("error", err.message || err.toString());
} }
}} }}
> >
@@ -811,8 +811,8 @@ export const GroupsEditorViewer = (props: Props) => {
onDelete={() => { onDelete={() => {
setPrependSeq( setPrependSeq(
prependSeq.filter( prependSeq.filter(
(v) => v.name !== item.name (v) => v.name !== item.name,
) ),
); );
}} }}
/> />
@@ -838,8 +838,8 @@ export const GroupsEditorViewer = (props: Props) => {
) { ) {
setDeleteSeq( setDeleteSeq(
deleteSeq.filter( deleteSeq.filter(
(v) => v !== filteredGroupList[newIndex].name (v) => v !== filteredGroupList[newIndex].name,
) ),
); );
} else { } else {
setDeleteSeq((prev) => [ setDeleteSeq((prev) => [
@@ -871,8 +871,8 @@ export const GroupsEditorViewer = (props: Props) => {
onDelete={() => { onDelete={() => {
setAppendSeq( setAppendSeq(
appendSeq.filter( appendSeq.filter(
(v) => v.name !== item.name (v) => v.name !== item.name,
) ),
); );
}} }}
/> />
@@ -906,8 +906,9 @@ export const GroupsEditorViewer = (props: Props) => {
padding: { padding: {
top: 33, // 顶部padding防止遮挡snippets 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" : "" 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, // 连字符 fontLigatures: false, // 连字符
smoothScrolling: true, // 平滑滚动 smoothScrolling: true, // 平滑滚动
}} }}
@@ -1,57 +1,58 @@
import { alpha, Box, styled } from "@mui/material"; import { alpha, Box, styled } from "@mui/material";
export const ProfileBox = styled(Box)( export const ProfileBox = styled(Box)(({
({ theme, "aria-selected": selected }) => { theme,
const { mode, primary, text } = theme.palette; "aria-selected": selected,
const key = `${mode}-${!!selected}`; }) => {
const { mode, primary, text } = theme.palette;
const key = `${mode}-${!!selected}`;
const backgroundColor = mode === "light" ? "#ffffff" : "#282A36"; const backgroundColor = mode === "light" ? "#ffffff" : "#282A36";
const color = { const color = {
"light-true": text.secondary, "light-true": text.secondary,
"light-false": text.secondary, "light-false": text.secondary,
"dark-true": alpha(text.secondary, 0.65), "dark-true": alpha(text.secondary, 0.65),
"dark-false": alpha(text.secondary, 0.65), "dark-false": alpha(text.secondary, 0.65),
}[key]!; }[key]!;
const h2color = { const h2color = {
"light-true": primary.main, "light-true": primary.main,
"light-false": text.primary, "light-false": text.primary,
"dark-true": primary.main, "dark-true": primary.main,
"dark-false": text.primary, "dark-false": text.primary,
}[key]!; }[key]!;
const borderSelect = { const borderSelect = {
"light-true": { "light-true": {
borderLeft: `3px solid ${primary.main}`, borderLeft: `3px solid ${primary.main}`,
width: `calc(100% + 3px)`, width: `calc(100% + 3px)`,
marginLeft: `-3px`, marginLeft: `-3px`,
}, },
"light-false": { "light-false": {
width: "100%", width: "100%",
}, },
"dark-true": { "dark-true": {
borderLeft: `3px solid ${primary.main}`, borderLeft: `3px solid ${primary.main}`,
width: `calc(100% + 3px)`, width: `calc(100% + 3px)`,
marginLeft: `-3px`, marginLeft: `-3px`,
}, },
"dark-false": { "dark-false": {
width: "100%", width: "100%",
}, },
}[key]; }[key];
return { return {
position: "relative", position: "relative",
display: "block", display: "block",
cursor: "pointer", cursor: "pointer",
textAlign: "left", textAlign: "left",
padding: "8px 16px", padding: "8px 16px",
boxSizing: "border-box", boxSizing: "border-box",
backgroundColor, backgroundColor,
...borderSelect, ...borderSelect,
borderRadius: "8px", borderRadius: "8px",
color, color,
"& h2": { color: h2color }, "& h2": { color: h2color },
}; };
} });
);
@@ -75,7 +75,10 @@ export const ProfileItem = (props: Props) => {
// 获取下次更新时间的函数 // 获取下次更新时间的函数
const fetchNextUpdateTime = useLockFn(async (forceRefresh = false) => { const fetchNextUpdateTime = useLockFn(async (forceRefresh = false) => {
if (itemData.option?.update_interval && itemData.option.update_interval > 0) { if (
itemData.option?.update_interval &&
itemData.option.update_interval > 0
) {
try { try {
console.log(`尝试获取配置 ${itemData.uid} 的下次更新时间`); console.log(`尝试获取配置 ${itemData.uid} 的下次更新时间`);
@@ -97,7 +100,7 @@ export const ProfileItem = (props: Props) => {
setNextUpdateTime(t("Last Update failed")); setNextUpdateTime(t("Last Update failed"));
} else { } else {
// 否则显示剩余时间 // 否则显示剩余时间
const diffMinutes = nextUpdateDate.diff(now, 'minute'); const diffMinutes = nextUpdateDate.diff(now, "minute");
if (diffMinutes < 60) { if (diffMinutes < 60) {
if (diffMinutes <= 0) { if (diffMinutes <= 0) {
@@ -159,11 +162,17 @@ export const ProfileItem = (props: Props) => {
}; };
// 只注册定时器更新事件监听 // 只注册定时器更新事件监听
window.addEventListener('verge://timer-updated', handleTimerUpdate as EventListener); window.addEventListener(
"verge://timer-updated",
handleTimerUpdate as EventListener,
);
return () => { return () => {
// 清理事件监听 // 清理事件监听
window.removeEventListener('verge://timer-updated', handleTimerUpdate as EventListener); window.removeEventListener(
"verge://timer-updated",
handleTimerUpdate as EventListener,
);
}; };
}, [showNextUpdate, itemData.uid]); }, [showNextUpdate, itemData.uid]);
@@ -271,7 +280,7 @@ export const ProfileItem = (props: Props) => {
try { try {
await viewProfile(itemData.uid); await viewProfile(itemData.uid);
} catch (err: any) { } catch (err: any) {
showNotice('error', err?.message || err.toString()); showNotice("error", err?.message || err.toString());
} }
}); });
@@ -302,7 +311,7 @@ export const ProfileItem = (props: Props) => {
await updateProfile(itemData.uid, option); await updateProfile(itemData.uid, option);
// 更新成功,刷新列表 // 更新成功,刷新列表
showNotice('success', t("Update subscription successfully")); showNotice("success", t("Update subscription successfully"));
mutate("getProfiles"); mutate("getProfiles");
} catch (err: any) { } catch (err: any) {
// 更新完全失败(包括后端的回退尝试) // 更新完全失败(包括后端的回退尝试)
@@ -421,13 +430,25 @@ export const ProfileItem = (props: Props) => {
}; };
// 注册事件监听 // 注册事件监听
window.addEventListener('profile-update-started', handleUpdateStarted as EventListener); window.addEventListener(
window.addEventListener('profile-update-completed', handleUpdateCompleted as EventListener); "profile-update-started",
handleUpdateStarted as EventListener,
);
window.addEventListener(
"profile-update-completed",
handleUpdateCompleted as EventListener,
);
return () => { return () => {
// 清理事件监听 // 清理事件监听
window.removeEventListener('profile-update-started', handleUpdateStarted as EventListener); window.removeEventListener(
window.removeEventListener('profile-update-completed', handleUpdateCompleted as EventListener); "profile-update-started",
handleUpdateStarted as EventListener,
);
window.removeEventListener(
"profile-update-completed",
handleUpdateCompleted as EventListener,
);
}; };
}, [itemData.uid, showNextUpdate]); }, [itemData.uid, showNextUpdate]);
@@ -541,13 +562,23 @@ export const ProfileItem = (props: Props) => {
) )
)} )}
{hasUrl && ( {hasUrl && (
<Box sx={{ display: "flex", justifyContent: "flex-end", ml: "auto" }}> <Box
sx={{
display: "flex",
justifyContent: "flex-end",
ml: "auto",
}}
>
<Typography <Typography
noWrap noWrap
component="span" component="span"
fontSize={14} fontSize={14}
textAlign="right" textAlign="right"
title={showNextUpdate ? t("Click to show last update time") : `${t("Update Time")}: ${parseExpire(updated)}\n${t("Click to show next update")}`} title={
showNextUpdate
? t("Click to show last update time")
: `${t("Update Time")}: ${parseExpire(updated)}\n${t("Click to show next update")}`
}
sx={{ sx={{
cursor: "pointer", cursor: "pointer",
display: "inline-block", display: "inline-block",
@@ -556,13 +587,15 @@ export const ProfileItem = (props: Props) => {
"&:hover": { "&:hover": {
borderBottomColor: "primary.main", borderBottomColor: "primary.main",
color: "primary.main", color: "primary.main",
} },
}} }}
onClick={toggleUpdateTimeDisplay} onClick={toggleUpdateTimeDisplay}
> >
{showNextUpdate {showNextUpdate
? nextUpdateTime ? nextUpdateTime
: (updated > 0 ? dayjs(updated * 1000).fromNow() : "")} : updated > 0
? dayjs(updated * 1000).fromNow()
: ""}
</Typography> </Typography>
</Box> </Box>
)} )}
@@ -43,7 +43,7 @@ export const ProfileMore = (props: Props) => {
try { try {
await viewProfile(id); await viewProfile(id);
} catch (err: any) { } catch (err: any) {
showNotice('error', err?.message || err.toString()); showNotice("error", err?.message || err.toString());
} }
}); });
@@ -201,7 +201,7 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
setOpen(false); setOpen(false);
fileDataRef.current = null; fileDataRef.current = null;
setTimeout(() => formIns.reset(), 500); setTimeout(() => formIns.reset(), 500);
} catch { } } catch {}
}; };
const text = { const text = {
@@ -66,27 +66,27 @@ export const ProxiesEditorViewer = (props: Props) => {
const filteredPrependSeq = useMemo( const filteredPrependSeq = useMemo(
() => prependSeq.filter((proxy) => match(proxy.name)), () => prependSeq.filter((proxy) => match(proxy.name)),
[prependSeq, match] [prependSeq, match],
); );
const filteredProxyList = useMemo( const filteredProxyList = useMemo(
() => proxyList.filter((proxy) => match(proxy.name)), () => proxyList.filter((proxy) => match(proxy.name)),
[proxyList, match] [proxyList, match],
); );
const filteredAppendSeq = useMemo( const filteredAppendSeq = useMemo(
() => appendSeq.filter((proxy) => match(proxy.name)), () => appendSeq.filter((proxy) => match(proxy.name)),
[appendSeq, match] [appendSeq, match],
); );
const sensors = useSensors( const sensors = useSensors(
useSensor(PointerSensor), useSensor(PointerSensor),
useSensor(KeyboardSensor, { useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates, coordinateGetter: sortableKeyboardCoordinates,
}) }),
); );
const reorder = ( const reorder = (
list: IProxyConfig[], list: IProxyConfig[],
startIndex: number, startIndex: number,
endIndex: number endIndex: number,
) => { ) => {
const result = Array.from(list); const result = Array.from(list);
const [removed] = result.splice(startIndex, 1); const [removed] = result.splice(startIndex, 1);
@@ -208,8 +208,8 @@ export const ProxiesEditorViewer = (props: Props) => {
setCurrData( setCurrData(
yaml.dump( yaml.dump(
{ prepend: prependSeq, append: appendSeq, delete: deleteSeq }, { prepend: prependSeq, append: appendSeq, delete: deleteSeq },
{ forceQuotes: true } { forceQuotes: true },
) ),
); );
} catch (e) { } catch (e) {
// 防止异常导致UI卡死 // 防止异常导致UI卡死
@@ -232,11 +232,11 @@ export const ProxiesEditorViewer = (props: Props) => {
const handleSave = useLockFn(async () => { const handleSave = useLockFn(async () => {
try { try {
await saveProfileFile(property, currData); await saveProfileFile(property, currData);
showNotice('success', t("Saved Successfully")); showNotice("success", t("Saved Successfully"));
onSave?.(prevData, currData); onSave?.(prevData, currData);
onClose(); onClose();
} catch (err: any) { } catch (err: any) {
showNotice('error', err.toString()); showNotice("error", err.toString());
} }
}); });
@@ -358,8 +358,8 @@ export const ProxiesEditorViewer = (props: Props) => {
onDelete={() => { onDelete={() => {
setPrependSeq( setPrependSeq(
prependSeq.filter( prependSeq.filter(
(v) => v.name !== item.name (v) => v.name !== item.name,
) ),
); );
}} }}
/> />
@@ -385,8 +385,8 @@ export const ProxiesEditorViewer = (props: Props) => {
) { ) {
setDeleteSeq( setDeleteSeq(
deleteSeq.filter( deleteSeq.filter(
(v) => v !== filteredProxyList[newIndex].name (v) => v !== filteredProxyList[newIndex].name,
) ),
); );
} else { } else {
setDeleteSeq((prev) => [ setDeleteSeq((prev) => [
@@ -418,8 +418,8 @@ export const ProxiesEditorViewer = (props: Props) => {
onDelete={() => { onDelete={() => {
setAppendSeq( setAppendSeq(
appendSeq.filter( appendSeq.filter(
(v) => v.name !== item.name (v) => v.name !== item.name,
) ),
); );
}} }}
/> />
@@ -453,8 +453,9 @@ export const ProxiesEditorViewer = (props: Props) => {
padding: { padding: {
top: 33, // 顶部padding防止遮挡snippets 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" : "" 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, // 连字符 fontLigatures: false, // 连字符
smoothScrolling: true, // 平滑滚动 smoothScrolling: true, // 平滑滚动
}} }}
@@ -49,8 +49,8 @@ export const ProxyItem = (props: Props) => {
? alpha(palette.background.paper, 0.3) ? alpha(palette.background.paper, 0.3)
: alpha(palette.grey[400], 0.3) : alpha(palette.grey[400], 0.3)
: type === "delete" : type === "delete"
? alpha(palette.error.main, 0.3) ? alpha(palette.error.main, 0.3)
: alpha(palette.success.main, 0.3), : alpha(palette.success.main, 0.3),
height: "100%", height: "100%",
margin: "8px 0", margin: "8px 0",
borderRadius: "8px", borderRadius: "8px",
@@ -52,8 +52,8 @@ export const RuleItem = (props: Props) => {
? alpha(palette.background.paper, 0.3) ? alpha(palette.background.paper, 0.3)
: alpha(palette.grey[400], 0.3) : alpha(palette.grey[400], 0.3)
: type === "delete" : type === "delete"
? alpha(palette.error.main, 0.3) ? alpha(palette.error.main, 0.3)
: alpha(palette.success.main, 0.3), : alpha(palette.success.main, 0.3),
height: "100%", height: "100%",
margin: "8px 0", margin: "8px 0",
borderRadius: "8px", borderRadius: "8px",
@@ -55,17 +55,17 @@ interface Props {
const portValidator = (value: string): boolean => { const portValidator = (value: string): boolean => {
return new RegExp( return new RegExp(
"^(?:[1-9]\\d{0,3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5])$" "^(?:[1-9]\\d{0,3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5])$",
).test(value); ).test(value);
}; };
const ipv4CIDRValidator = (value: string): boolean => { const ipv4CIDRValidator = (value: string): boolean => {
return new RegExp( return new RegExp(
"^(?:(?:[1-9]?[0-9]|1[0-9][0-9]|2(?:[0-4][0-9]|5[0-5]))\\.){3}(?:[1-9]?[0-9]|1[0-9][0-9]|2(?:[0-4][0-9]|5[0-5]))(?:\\/(?:[12]?[0-9]|3[0-2]))$" "^(?:(?:[1-9]?[0-9]|1[0-9][0-9]|2(?:[0-4][0-9]|5[0-5]))\\.){3}(?:[1-9]?[0-9]|1[0-9][0-9]|2(?:[0-4][0-9]|5[0-5]))(?:\\/(?:[12]?[0-9]|3[0-2]))$",
).test(value); ).test(value);
}; };
const ipv6CIDRValidator = (value: string): boolean => { const ipv6CIDRValidator = (value: string): boolean => {
return new RegExp( return new RegExp(
"^([0-9a-fA-F]{1,4}(?::[0-9a-fA-F]{1,4}){7}|::|:(?::[0-9a-fA-F]{1,4}){1,6}|[0-9a-fA-F]{1,4}:(?::[0-9a-fA-F]{1,4}){1,5}|(?:[0-9a-fA-F]{1,4}:){2}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){3}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){4}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){5}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,6}:)\\/(?:12[0-8]|1[01][0-9]|[1-9]?[0-9])$" "^([0-9a-fA-F]{1,4}(?::[0-9a-fA-F]{1,4}){7}|::|:(?::[0-9a-fA-F]{1,4}){1,6}|[0-9a-fA-F]{1,4}:(?::[0-9a-fA-F]{1,4}){1,5}|(?:[0-9a-fA-F]{1,4}:){2}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){3}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){4}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){5}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,6}:)\\/(?:12[0-8]|1[01][0-9]|[1-9]?[0-9])$",
).test(value); ).test(value);
}; };
@@ -76,161 +76,161 @@ const rules: {
noResolve?: boolean; noResolve?: boolean;
validator?: (value: string) => boolean; validator?: (value: string) => boolean;
}[] = [ }[] = [
{ {
name: "DOMAIN", name: "DOMAIN",
example: "example.com", example: "example.com",
}, },
{ {
name: "DOMAIN-SUFFIX", name: "DOMAIN-SUFFIX",
example: "example.com", example: "example.com",
}, },
{ {
name: "DOMAIN-KEYWORD", name: "DOMAIN-KEYWORD",
example: "example", example: "example",
}, },
{ {
name: "DOMAIN-REGEX", name: "DOMAIN-REGEX",
example: "example.*", example: "example.*",
}, },
{ {
name: "GEOSITE", name: "GEOSITE",
example: "youtube", example: "youtube",
}, },
{ {
name: "GEOIP", name: "GEOIP",
example: "CN", example: "CN",
noResolve: true, noResolve: true,
}, },
{ {
name: "SRC-GEOIP", name: "SRC-GEOIP",
example: "CN", example: "CN",
}, },
{ {
name: "IP-ASN", name: "IP-ASN",
example: "13335", example: "13335",
noResolve: true, noResolve: true,
validator: (value) => (+value ? true : false), validator: (value) => (+value ? true : false),
}, },
{ {
name: "SRC-IP-ASN", name: "SRC-IP-ASN",
example: "9808", example: "9808",
validator: (value) => (+value ? true : false), validator: (value) => (+value ? true : false),
}, },
{ {
name: "IP-CIDR", name: "IP-CIDR",
example: "127.0.0.0/8", example: "127.0.0.0/8",
noResolve: true, noResolve: true,
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value), validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
}, },
{ {
name: "IP-CIDR6", name: "IP-CIDR6",
example: "2620:0:2d0:200::7/32", example: "2620:0:2d0:200::7/32",
noResolve: true, noResolve: true,
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value), validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
}, },
{ {
name: "SRC-IP-CIDR", name: "SRC-IP-CIDR",
example: "192.168.1.201/32", example: "192.168.1.201/32",
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value), validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
}, },
{ {
name: "IP-SUFFIX", name: "IP-SUFFIX",
example: "8.8.8.8/24", example: "8.8.8.8/24",
noResolve: true, noResolve: true,
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value), validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
}, },
{ {
name: "SRC-IP-SUFFIX", name: "SRC-IP-SUFFIX",
example: "192.168.1.201/8", example: "192.168.1.201/8",
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value), validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
}, },
{ {
name: "SRC-PORT", name: "SRC-PORT",
example: "7777", example: "7777",
validator: (value) => portValidator(value), validator: (value) => portValidator(value),
}, },
{ {
name: "DST-PORT", name: "DST-PORT",
example: "80", example: "80",
validator: (value) => portValidator(value), validator: (value) => portValidator(value),
}, },
{ {
name: "IN-PORT", name: "IN-PORT",
example: "7890", example: "7890",
validator: (value) => portValidator(value), validator: (value) => portValidator(value),
}, },
{ {
name: "DSCP", name: "DSCP",
example: "4", example: "4",
}, },
{ {
name: "PROCESS-NAME", name: "PROCESS-NAME",
example: getSystem() === "windows" ? "chrome.exe" : "curl", example: getSystem() === "windows" ? "chrome.exe" : "curl",
}, },
{ {
name: "PROCESS-PATH", name: "PROCESS-PATH",
example: example:
getSystem() === "windows" getSystem() === "windows"
? "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" ? "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"
: "/usr/bin/wget", : "/usr/bin/wget",
}, },
{ {
name: "PROCESS-NAME-REGEX", name: "PROCESS-NAME-REGEX",
example: ".*telegram.*", example: ".*telegram.*",
}, },
{ {
name: "PROCESS-PATH-REGEX", name: "PROCESS-PATH-REGEX",
example: example:
getSystem() === "windows" ? "(?i).*Application\\chrome.*" : ".*bin/wget", getSystem() === "windows" ? "(?i).*Application\\chrome.*" : ".*bin/wget",
}, },
{ {
name: "NETWORK", name: "NETWORK",
example: "udp", example: "udp",
validator: (value) => ["tcp", "udp"].includes(value), validator: (value) => ["tcp", "udp"].includes(value),
}, },
{ {
name: "UID", name: "UID",
example: "1001", example: "1001",
validator: (value) => (+value ? true : false), validator: (value) => (+value ? true : false),
}, },
{ {
name: "IN-TYPE", name: "IN-TYPE",
example: "SOCKS/HTTP", example: "SOCKS/HTTP",
}, },
{ {
name: "IN-USER", name: "IN-USER",
example: "mihomo", example: "mihomo",
}, },
{ {
name: "IN-NAME", name: "IN-NAME",
example: "ss", example: "ss",
}, },
{ {
name: "SUB-RULE", name: "SUB-RULE",
example: "(NETWORK,tcp)", example: "(NETWORK,tcp)",
}, },
{ {
name: "RULE-SET", name: "RULE-SET",
example: "providername", example: "providername",
noResolve: true, noResolve: true,
}, },
{ {
name: "AND", name: "AND",
example: "((DOMAIN,baidu.com),(NETWORK,UDP))", example: "((DOMAIN,baidu.com),(NETWORK,UDP))",
}, },
{ {
name: "OR", name: "OR",
example: "((NETWORK,UDP),(DOMAIN,baidu.com))", example: "((NETWORK,UDP),(DOMAIN,baidu.com))",
}, },
{ {
name: "NOT", name: "NOT",
example: "((DOMAIN,baidu.com))", example: "((DOMAIN,baidu.com))",
}, },
{ {
name: "MATCH", name: "MATCH",
required: false, required: false,
}, },
]; ];
const builtinProxyPolicies = ["DIRECT", "REJECT", "REJECT-DROP", "PASS"]; const builtinProxyPolicies = ["DIRECT", "REJECT", "REJECT-DROP", "PASS"];
@@ -260,22 +260,22 @@ export const RulesEditorViewer = (props: Props) => {
const filteredPrependSeq = useMemo( const filteredPrependSeq = useMemo(
() => prependSeq.filter((rule) => match(rule)), () => prependSeq.filter((rule) => match(rule)),
[prependSeq, match] [prependSeq, match],
); );
const filteredRuleList = useMemo( const filteredRuleList = useMemo(
() => ruleList.filter((rule) => match(rule)), () => ruleList.filter((rule) => match(rule)),
[ruleList, match] [ruleList, match],
); );
const filteredAppendSeq = useMemo( const filteredAppendSeq = useMemo(
() => appendSeq.filter((rule) => match(rule)), () => appendSeq.filter((rule) => match(rule)),
[appendSeq, match] [appendSeq, match],
); );
const sensors = useSensors( const sensors = useSensors(
useSensor(PointerSensor), useSensor(PointerSensor),
useSensor(KeyboardSensor, { useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates, coordinateGetter: sortableKeyboardCoordinates,
}) }),
); );
const reorder = (list: string[], startIndex: number, endIndex: number) => { const reorder = (list: string[], startIndex: number, endIndex: number) => {
const result = Array.from(list); const result = Array.from(list);
@@ -333,11 +333,11 @@ export const RulesEditorViewer = (props: Props) => {
setCurrData( setCurrData(
yaml.dump( yaml.dump(
{ prepend: prependSeq, append: appendSeq, delete: deleteSeq }, { prepend: prependSeq, append: appendSeq, delete: deleteSeq },
{ forceQuotes: true } { forceQuotes: true },
) ),
); );
} catch (e: any) { } catch (e: any) {
showNotice('error', e?.message || e?.toString() || 'YAML dump error'); showNotice("error", e?.message || e?.toString() || "YAML dump error");
} }
}; };
if (window.requestIdleCallback) { if (window.requestIdleCallback) {
@@ -371,7 +371,7 @@ export const RulesEditorViewer = (props: Props) => {
return !moreDeleteGroups.includes(group); return !moreDeleteGroups.includes(group);
} }
}), }),
moreAppendGroups moreAppendGroups,
); );
let originRuleSetObj = yaml.load(data) as { "rule-providers": {} } | null; let originRuleSetObj = yaml.load(data) as { "rule-providers": {} } | null;
@@ -396,7 +396,7 @@ export const RulesEditorViewer = (props: Props) => {
let globalSubRule = globalSubRuleObj?.["sub-rules"] || {}; let globalSubRule = globalSubRuleObj?.["sub-rules"] || {};
let subRule = Object.assign({}, originSubRule, moreSubRule, globalSubRule); let subRule = Object.assign({}, originSubRule, moreSubRule, globalSubRule);
setProxyPolicyList( setProxyPolicyList(
builtinProxyPolicies.concat(groups.map((group: any) => group.name)) builtinProxyPolicies.concat(groups.map((group: any) => group.name)),
); );
setRuleSetList(Object.keys(ruleSet)); setRuleSetList(Object.keys(ruleSet));
setSubRuleList(Object.keys(subRule)); setSubRuleList(Object.keys(subRule));
@@ -417,19 +417,20 @@ export const RulesEditorViewer = (props: Props) => {
throw new Error(t("Invalid Rule")); throw new Error(t("Invalid Rule"));
} }
const condition = ruleType.required ?? true ? ruleContent : ""; const condition = (ruleType.required ?? true) ? ruleContent : "";
return `${ruleType.name}${condition ? "," + condition : ""},${proxyPolicy}${ruleType.noResolve && noResolve ? ",no-resolve" : "" return `${ruleType.name}${condition ? "," + condition : ""},${proxyPolicy}${
}`; ruleType.noResolve && noResolve ? ",no-resolve" : ""
}`;
}; };
const handleSave = useLockFn(async () => { const handleSave = useLockFn(async () => {
try { try {
await saveProfileFile(property, currData); await saveProfileFile(property, currData);
showNotice('success', t("Saved Successfully")); showNotice("success", t("Saved Successfully"));
onSave?.(prevData, currData); onSave?.(prevData, currData);
onClose(); onClose();
} catch (err: any) { } catch (err: any) {
showNotice('error', err.toString()); showNotice("error", err.toString());
} }
}); });
@@ -557,7 +558,7 @@ export const RulesEditorViewer = (props: Props) => {
if (prependSeq.includes(raw)) return; if (prependSeq.includes(raw)) return;
setPrependSeq([raw, ...prependSeq]); setPrependSeq([raw, ...prependSeq]);
} catch (err: any) { } catch (err: any) {
showNotice('error', err.message || err.toString()); showNotice("error", err.message || err.toString());
} }
}} }}
> >
@@ -575,7 +576,7 @@ export const RulesEditorViewer = (props: Props) => {
if (appendSeq.includes(raw)) return; if (appendSeq.includes(raw)) return;
setAppendSeq([...appendSeq, raw]); setAppendSeq([...appendSeq, raw]);
} catch (err: any) { } catch (err: any) {
showNotice('error', err.message || err.toString()); showNotice("error", err.message || err.toString());
} }
}} }}
> >
@@ -621,7 +622,7 @@ export const RulesEditorViewer = (props: Props) => {
ruleRaw={item} ruleRaw={item}
onDelete={() => { onDelete={() => {
setPrependSeq( setPrependSeq(
prependSeq.filter((v) => v !== item) prependSeq.filter((v) => v !== item),
); );
}} }}
/> />
@@ -645,8 +646,8 @@ export const RulesEditorViewer = (props: Props) => {
if (deleteSeq.includes(filteredRuleList[newIndex])) { if (deleteSeq.includes(filteredRuleList[newIndex])) {
setDeleteSeq( setDeleteSeq(
deleteSeq.filter( deleteSeq.filter(
(v) => v !== filteredRuleList[newIndex] (v) => v !== filteredRuleList[newIndex],
) ),
); );
} else { } else {
setDeleteSeq((prev) => [ setDeleteSeq((prev) => [
@@ -677,7 +678,7 @@ export const RulesEditorViewer = (props: Props) => {
ruleRaw={item} ruleRaw={item}
onDelete={() => { onDelete={() => {
setAppendSeq( setAppendSeq(
appendSeq.filter((v) => v !== item) appendSeq.filter((v) => v !== item),
); );
}} }}
/> />
@@ -711,8 +712,9 @@ export const RulesEditorViewer = (props: Props) => {
padding: { padding: {
top: 33, // 顶部padding防止遮挡snippets 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" : "" 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, // 连字符 fontLigatures: false, // 连字符
smoothScrolling: true, // 平滑滚动 smoothScrolling: true, // 平滑滚动
}} }}
@@ -15,7 +15,7 @@ import {
LinearProgress, LinearProgress,
alpha, alpha,
styled, styled,
useTheme useTheme,
} from "@mui/material"; } from "@mui/material";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
@@ -73,7 +73,7 @@ export const ProviderButton = () => {
const updateProvider = useLockFn(async (name: string) => { const updateProvider = useLockFn(async (name: string) => {
try { try {
// 设置更新状态 // 设置更新状态
setUpdating(prev => ({ ...prev, [name]: true })); setUpdating((prev) => ({ ...prev, [name]: true }));
await proxyProviderUpdate(name); await proxyProviderUpdate(name);
@@ -81,12 +81,15 @@ export const ProviderButton = () => {
await refreshProxy(); await refreshProxy();
await refreshProxyProviders(); await refreshProxyProviders();
showNotice('success', `${name} 更新成功`); showNotice("success", `${name} 更新成功`);
} catch (err: any) { } catch (err: any) {
showNotice('error', `${name} 更新失败: ${err?.message || err.toString()}`); showNotice(
"error",
`${name} 更新失败: ${err?.message || err.toString()}`,
);
} finally { } finally {
// 清除更新状态 // 清除更新状态
setUpdating(prev => ({ ...prev, [name]: false })); setUpdating((prev) => ({ ...prev, [name]: false }));
} }
}); });
@@ -96,15 +99,18 @@ export const ProviderButton = () => {
// 获取所有provider的名称 // 获取所有provider的名称
const allProviders = Object.keys(proxyProviders || {}); const allProviders = Object.keys(proxyProviders || {});
if (allProviders.length === 0) { if (allProviders.length === 0) {
showNotice('info', "没有可更新的代理提供者"); showNotice("info", "没有可更新的代理提供者");
return; return;
} }
// 设置所有provider为更新中状态 // 设置所有provider为更新中状态
const newUpdating = allProviders.reduce((acc, key) => { const newUpdating = allProviders.reduce(
acc[key] = true; (acc, key) => {
return acc; acc[key] = true;
}, {} as Record<string, boolean>); return acc;
},
{} as Record<string, boolean>,
);
setUpdating(newUpdating); setUpdating(newUpdating);
// 改为串行逐个更新所有provider // 改为串行逐个更新所有provider
@@ -112,7 +118,7 @@ export const ProviderButton = () => {
try { try {
await proxyProviderUpdate(name); await proxyProviderUpdate(name);
// 每个更新完成后更新状态 // 每个更新完成后更新状态
setUpdating(prev => ({ ...prev, [name]: false })); setUpdating((prev) => ({ ...prev, [name]: false }));
} catch (err) { } catch (err) {
console.error(`更新 ${name} 失败`, err); console.error(`更新 ${name} 失败`, err);
// 继续执行下一个,不中断整体流程 // 继续执行下一个,不中断整体流程
@@ -123,9 +129,9 @@ export const ProviderButton = () => {
await refreshProxy(); await refreshProxy();
await refreshProxyProviders(); await refreshProxyProviders();
showNotice('success', "全部代理提供者更新成功"); showNotice("success", "全部代理提供者更新成功");
} catch (err: any) { } catch (err: any) {
showNotice('error', `更新失败: ${err?.message || err.toString()}`); showNotice("error", `更新失败: ${err?.message || err.toString()}`);
} finally { } finally {
// 清除所有更新状态 // 清除所有更新状态
setUpdating({}); setUpdating({});
@@ -150,14 +156,13 @@ export const ProviderButton = () => {
{t("Proxy Provider")} {t("Proxy Provider")}
</Button> </Button>
<Dialog <Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
open={open}
onClose={handleClose}
maxWidth="sm"
fullWidth
>
<DialogTitle> <DialogTitle>
<Box display="flex" justifyContent="space-between" alignItems="center"> <Box
display="flex"
justifyContent="space-between"
alignItems="center"
>
<Typography variant="h6">{t("Proxy Provider")}</Typography> <Typography variant="h6">{t("Proxy Provider")}</Typography>
<Box> <Box>
<Button <Button
@@ -187,9 +192,13 @@ export const ProviderButton = () => {
const expire = sub?.Expire || 0; const expire = sub?.Expire || 0;
// 流量使用进度 // 流量使用进度
const progress = total > 0 const progress =
? Math.min(Math.round(((download + upload) * 100) / total) + 1, 100) total > 0
: 0; ? Math.min(
Math.round(((download + upload) * 100) / total) + 1,
100,
)
: 0;
return ( return (
<ListItem <ListItem
@@ -200,31 +209,34 @@ export const ProviderButton = () => {
mb: "8px", mb: "8px",
borderRadius: 2, borderRadius: 2,
overflow: "hidden", overflow: "hidden",
transition: "all 0.2s" transition: "all 0.2s",
}, },
({ palette: { mode, primary } }) => { ({ palette: { mode, primary } }) => {
const bgcolor = mode === "light" ? "#ffffff" : "#24252f"; const bgcolor = mode === "light" ? "#ffffff" : "#24252f";
const hoverColor = mode === "light" const hoverColor =
? alpha(primary.main, 0.1) mode === "light"
: alpha(primary.main, 0.2); ? alpha(primary.main, 0.1)
: alpha(primary.main, 0.2);
return { return {
backgroundColor: bgcolor, backgroundColor: bgcolor,
"&:hover": { "&:hover": {
backgroundColor: hoverColor, backgroundColor: hoverColor,
} },
}; };
} },
]} ]}
> >
<ListItemText <ListItemText
sx={{ px: 2, py: 1 }} sx={{ px: 2, py: 1 }}
primary={ primary={
<Box sx={{ <Box
display: "flex", sx={{
justifyContent: "space-between", display: "flex",
alignItems: "center", justifyContent: "space-between",
}}> alignItems: "center",
}}
>
<Typography <Typography
variant="subtitle1" variant="subtitle1"
component="div" component="div"
@@ -241,8 +253,13 @@ export const ProviderButton = () => {
</TypeBox> </TypeBox>
</Typography> </Typography>
<Typography variant="body2" color="text.secondary" noWrap> <Typography
<small>{t("Update At")}: </small>{time.fromNow()} variant="body2"
color="text.secondary"
noWrap
>
<small>{t("Update At")}: </small>
{time.fromNow()}
</Typography> </Typography>
</Box> </Box>
} }
@@ -251,14 +268,17 @@ export const ProviderButton = () => {
{/* 订阅信息 */} {/* 订阅信息 */}
{hasSubInfo && ( {hasSubInfo && (
<> <>
<Box sx={{ <Box
mb: 1, sx={{
display: "flex", mb: 1,
alignItems: "center", display: "flex",
justifyContent: "space-between", alignItems: "center",
}}> justifyContent: "space-between",
}}
>
<span title={t("Used / Total") as string}> <span title={t("Used / Total") as string}>
{parseTraffic(upload + download)} / {parseTraffic(total)} {parseTraffic(upload + download)} /{" "}
{parseTraffic(total)}
</span> </span>
<span title={t("Expire Time") as string}> <span title={t("Expire Time") as string}>
{parseExpire(expire)} {parseExpire(expire)}
@@ -281,12 +301,14 @@ export const ProviderButton = () => {
} }
/> />
<Divider orientation="vertical" flexItem /> <Divider orientation="vertical" flexItem />
<Box sx={{ <Box
width: 40, sx={{
display: "flex", width: 40,
justifyContent: "center", display: "flex",
alignItems: "center" justifyContent: "center",
}}> alignItems: "center",
}}
>
<IconButton <IconButton
size="small" size="small"
color="primary" color="primary"
@@ -295,11 +317,13 @@ export const ProviderButton = () => {
}} }}
disabled={isUpdating} disabled={isUpdating}
sx={{ sx={{
animation: isUpdating ? "spin 1s linear infinite" : "none", animation: isUpdating
? "spin 1s linear infinite"
: "none",
"@keyframes spin": { "@keyframes spin": {
"0%": { transform: "rotate(0deg)" }, "0%": { transform: "rotate(0deg)" },
"100%": { transform: "rotate(360deg)" } "100%": { transform: "rotate(360deg)" },
} },
}} }}
title={t("Update Provider") as string} title={t("Update Provider") as string}
> >
@@ -31,7 +31,10 @@ interface RenderProps {
onLocation: (group: IRenderItem["group"]) => void; onLocation: (group: IRenderItem["group"]) => void;
onCheckAll: (groupName: string) => void; onCheckAll: (groupName: string) => void;
onHeadState: (groupName: string, patch: Partial<HeadState>) => void; onHeadState: (groupName: string, patch: Partial<HeadState>) => void;
onChangeProxy: (group: IRenderItem["group"], proxy: IRenderItem["proxy"] & { name: string }) => void; onChangeProxy: (
group: IRenderItem["group"],
proxy: IRenderItem["proxy"] & { name: string },
) => void;
} }
export const ProxyRender = (props: RenderProps) => { export const ProxyRender = (props: RenderProps) => {
@@ -134,7 +137,8 @@ export const ProxyRender = (props: RenderProps) => {
label={`${group.all.length}`} label={`${group.all.length}`}
sx={{ sx={{
mr: 1, mr: 1,
backgroundColor: (theme) => alpha(theme.palette.primary.main, 0.1), backgroundColor: (theme) =>
alpha(theme.palette.primary.main, 0.1),
color: (theme) => theme.palette.primary.main, color: (theme) => theme.palette.primary.main,
}} }}
/> />
@@ -8,7 +8,7 @@ export default function useFilterSort(
proxies: IProxyItem[], proxies: IProxyItem[],
groupName: string, groupName: string,
filterText: string, filterText: string,
sortType: ProxySortType sortType: ProxySortType,
) { ) {
const [refresh, setRefresh] = useState({}); const [refresh, setRefresh] = useState({});
@@ -40,7 +40,7 @@ export function filterSort(
proxies: IProxyItem[], proxies: IProxyItem[],
groupName: string, groupName: string,
filterText: string, filterText: string,
sortType: ProxySortType sortType: ProxySortType,
) { ) {
const fp = filterProxies(proxies, groupName, filterText); const fp = filterProxies(proxies, groupName, filterText);
const sp = sortProxies(fp, groupName, sortType); const sp = sortProxies(fp, groupName, sortType);
@@ -60,7 +60,7 @@ const regex2 = /type=(.*)/i;
function filterProxies( function filterProxies(
proxies: IProxyItem[], proxies: IProxyItem[],
groupName: string, groupName: string,
filterText: string filterText: string,
) { ) {
if (!filterText) return proxies; if (!filterText) return proxies;
@@ -100,7 +100,7 @@ function filterProxies(
function sortProxies( function sortProxies(
proxies: IProxyItem[], proxies: IProxyItem[],
groupName: string, groupName: string,
sortType: ProxySortType sortType: ProxySortType,
) { ) {
if (!proxies) return []; if (!proxies) return [];
if (sortType === 0) return proxies; if (sortType === 0) return proxies;
@@ -37,7 +37,7 @@ export function useHeadStateNew() {
try { try {
const data = JSON.parse( const data = JSON.parse(
localStorage.getItem(HEAD_STATE_KEY)! localStorage.getItem(HEAD_STATE_KEY)!,
) as HeadStateStorage; ) as HeadStateStorage;
const value = data[current] || {}; const value = data[current] || {};
@@ -74,7 +74,7 @@ export function useHeadStateNew() {
return ret; return ret;
}); });
}, },
[current] [current],
); );
return [state, setHeadState] as const; return [state, setHeadState] as const;
@@ -14,7 +14,7 @@ import {
Divider, Divider,
alpha, alpha,
styled, styled,
useTheme useTheme,
} from "@mui/material"; } from "@mui/material";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
@@ -59,7 +59,7 @@ export const ProviderButton = () => {
const updateProvider = useLockFn(async (name: string) => { const updateProvider = useLockFn(async (name: string) => {
try { try {
// 设置更新状态 // 设置更新状态
setUpdating(prev => ({ ...prev, [name]: true })); setUpdating((prev) => ({ ...prev, [name]: true }));
await ruleProviderUpdate(name); await ruleProviderUpdate(name);
@@ -67,12 +67,15 @@ export const ProviderButton = () => {
await refreshRules(); await refreshRules();
await refreshRuleProviders(); await refreshRuleProviders();
showNotice('success', `${name} 更新成功`); showNotice("success", `${name} 更新成功`);
} catch (err: any) { } catch (err: any) {
showNotice('error', `${name} 更新失败: ${err?.message || err.toString()}`); showNotice(
"error",
`${name} 更新失败: ${err?.message || err.toString()}`,
);
} finally { } finally {
// 清除更新状态 // 清除更新状态
setUpdating(prev => ({ ...prev, [name]: false })); setUpdating((prev) => ({ ...prev, [name]: false }));
} }
}); });
@@ -82,15 +85,18 @@ export const ProviderButton = () => {
// 获取所有provider的名称 // 获取所有provider的名称
const allProviders = Object.keys(ruleProviders || {}); const allProviders = Object.keys(ruleProviders || {});
if (allProviders.length === 0) { if (allProviders.length === 0) {
showNotice('info', "没有可更新的规则提供者"); showNotice("info", "没有可更新的规则提供者");
return; return;
} }
// 设置所有provider为更新中状态 // 设置所有provider为更新中状态
const newUpdating = allProviders.reduce((acc, key) => { const newUpdating = allProviders.reduce(
acc[key] = true; (acc, key) => {
return acc; acc[key] = true;
}, {} as Record<string, boolean>); return acc;
},
{} as Record<string, boolean>,
);
setUpdating(newUpdating); setUpdating(newUpdating);
// 改为串行逐个更新所有provider // 改为串行逐个更新所有provider
@@ -98,7 +104,7 @@ export const ProviderButton = () => {
try { try {
await ruleProviderUpdate(name); await ruleProviderUpdate(name);
// 每个更新完成后更新状态 // 每个更新完成后更新状态
setUpdating(prev => ({ ...prev, [name]: false })); setUpdating((prev) => ({ ...prev, [name]: false }));
} catch (err) { } catch (err) {
console.error(`更新 ${name} 失败`, err); console.error(`更新 ${name} 失败`, err);
// 继续执行下一个,不中断整体流程 // 继续执行下一个,不中断整体流程
@@ -109,9 +115,9 @@ export const ProviderButton = () => {
await refreshRules(); await refreshRules();
await refreshRuleProviders(); await refreshRuleProviders();
showNotice('success', "全部规则提供者更新成功"); showNotice("success", "全部规则提供者更新成功");
} catch (err: any) { } catch (err: any) {
showNotice('error', `更新失败: ${err?.message || err.toString()}`); showNotice("error", `更新失败: ${err?.message || err.toString()}`);
} finally { } finally {
// 清除所有更新状态 // 清除所有更新状态
setUpdating({}); setUpdating({});
@@ -135,14 +141,13 @@ export const ProviderButton = () => {
{t("Rule Provider")} {t("Rule Provider")}
</Button> </Button>
<Dialog <Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
open={open}
onClose={handleClose}
maxWidth="sm"
fullWidth
>
<DialogTitle> <DialogTitle>
<Box display="flex" justifyContent="space-between" alignItems="center"> <Box
display="flex"
justifyContent="space-between"
alignItems="center"
>
<Typography variant="h6">{t("Rule Providers")}</Typography> <Typography variant="h6">{t("Rule Providers")}</Typography>
<Button <Button
variant="contained" variant="contained"
@@ -170,32 +175,35 @@ export const ProviderButton = () => {
mb: "8px", mb: "8px",
borderRadius: 2, borderRadius: 2,
overflow: "hidden", overflow: "hidden",
transition: "all 0.2s" transition: "all 0.2s",
}, },
({ palette: { mode, primary } }) => { ({ palette: { mode, primary } }) => {
const bgcolor = mode === "light" ? "#ffffff" : "#24252f"; const bgcolor = mode === "light" ? "#ffffff" : "#24252f";
const hoverColor = mode === "light" const hoverColor =
? alpha(primary.main, 0.1) mode === "light"
: alpha(primary.main, 0.2); ? alpha(primary.main, 0.1)
: alpha(primary.main, 0.2);
return { return {
backgroundColor: bgcolor, backgroundColor: bgcolor,
"&:hover": { "&:hover": {
backgroundColor: hoverColor, backgroundColor: hoverColor,
borderColor: alpha(primary.main, 0.3) borderColor: alpha(primary.main, 0.3),
} },
}; };
} },
]} ]}
> >
<ListItemText <ListItemText
sx={{ px: 2, py: 1 }} sx={{ px: 2, py: 1 }}
primary={ primary={
<Box sx={{ <Box
display: "flex", sx={{
justifyContent: "space-between", display: "flex",
alignItems: "center", justifyContent: "space-between",
}}> alignItems: "center",
}}
>
<Typography <Typography
variant="subtitle1" variant="subtitle1"
component="div" component="div"
@@ -209,8 +217,13 @@ export const ProviderButton = () => {
</TypeBox> </TypeBox>
</Typography> </Typography>
<Typography variant="body2" color="text.secondary" noWrap> <Typography
<small>{t("Update At")}: </small>{time.fromNow()} variant="body2"
color="text.secondary"
noWrap
>
<small>{t("Update At")}: </small>
{time.fromNow()}
</Typography> </Typography>
</Box> </Box>
} }
@@ -219,30 +232,32 @@ export const ProviderButton = () => {
<TypeBox component="span"> <TypeBox component="span">
{provider.vehicleType} {provider.vehicleType}
</TypeBox> </TypeBox>
<TypeBox component="span"> <TypeBox component="span">{provider.behavior}</TypeBox>
{provider.behavior}
</TypeBox>
</Box> </Box>
} }
/> />
<Divider orientation="vertical" flexItem /> <Divider orientation="vertical" flexItem />
<Box sx={{ <Box
width: 40, sx={{
display: "flex", width: 40,
justifyContent: "center", display: "flex",
alignItems: "center" justifyContent: "center",
}}> alignItems: "center",
}}
>
<IconButton <IconButton
size="small" size="small"
color="primary" color="primary"
onClick={() => updateProvider(key)} onClick={() => updateProvider(key)}
disabled={isUpdating} disabled={isUpdating}
sx={{ sx={{
animation: isUpdating ? "spin 1s linear infinite" : "none", animation: isUpdating
? "spin 1s linear infinite"
: "none",
"@keyframes spin": { "@keyframes spin": {
"0%": { transform: "rotate(0deg)" }, "0%": { transform: "rotate(0deg)" },
"100%": { transform: "rotate(360deg)" } "100%": { transform: "rotate(360deg)" },
} },
}} }}
title={t("Update Provider") as string} title={t("Update Provider") as string}
> >
@@ -82,21 +82,21 @@ export const BackupConfigViewer = memo(
if (!url) { if (!url) {
urlRef.current?.focus(); urlRef.current?.focus();
showNotice('error', t("WebDAV URL Required")); showNotice("error", t("WebDAV URL Required"));
throw new Error(t("WebDAV URL Required")); throw new Error(t("WebDAV URL Required"));
} else if (!isValidUrl(url)) { } else if (!isValidUrl(url)) {
urlRef.current?.focus(); urlRef.current?.focus();
showNotice('error', t("Invalid WebDAV URL")); showNotice("error", t("Invalid WebDAV URL"));
throw new Error(t("Invalid WebDAV URL")); throw new Error(t("Invalid WebDAV URL"));
} }
if (!username) { if (!username) {
usernameRef.current?.focus(); usernameRef.current?.focus();
showNotice('error', t("WebDAV URL Required")); showNotice("error", t("WebDAV URL Required"));
throw new Error(t("Username Required")); throw new Error(t("Username Required"));
} }
if (!password) { if (!password) {
passwordRef.current?.focus(); passwordRef.current?.focus();
showNotice('error', t("WebDAV URL Required")); showNotice("error", t("WebDAV URL Required"));
throw new Error(t("Password Required")); throw new Error(t("Password Required"));
} }
}; };
@@ -110,11 +110,11 @@ export const BackupConfigViewer = memo(
data.username.trim(), data.username.trim(),
data.password, data.password,
).then(() => { ).then(() => {
showNotice('success', t("WebDAV Config Saved")); showNotice("success", t("WebDAV Config Saved"));
onSaveSuccess(); onSaveSuccess();
}); });
} catch (error) { } catch (error) {
showNotice('error', t("WebDAV Config Save Failed", { error }), 3000); showNotice("error", t("WebDAV Config Save Failed", { error }), 3000);
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -125,11 +125,11 @@ export const BackupConfigViewer = memo(
try { try {
setLoading(true); setLoading(true);
await createWebdavBackup().then(async () => { await createWebdavBackup().then(async () => {
showNotice('success', t("Backup Created")); showNotice("success", t("Backup Created"));
await onBackupSuccess(); await onBackupSuccess();
}); });
} catch (error) { } catch (error) {
showNotice('error', t("Backup Failed", { error })); showNotice("error", t("Backup Failed", { error }));
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -61,7 +61,7 @@ export const BackupTableViewer = memo(
const handleRestore = useLockFn(async (filename: string) => { const handleRestore = useLockFn(async (filename: string) => {
await restoreWebDavBackup(filename).then(() => { await restoreWebDavBackup(filename).then(() => {
showNotice('success', t("Restore Success, App will restart in 1s")); showNotice("success", t("Restore Success, App will restart in 1s"));
}); });
await restartApp(); await restartApp();
}); });
@@ -52,7 +52,7 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
const errorMsg = await changeClashCore(core); const errorMsg = await changeClashCore(core);
if (errorMsg) { if (errorMsg) {
showNotice('error', errorMsg); showNotice("error", errorMsg);
setChangingCore(null); setChangingCore(null);
return; return;
} }
@@ -65,7 +65,7 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
}, 500); }, 500);
} catch (err: any) { } catch (err: any) {
setChangingCore(null); setChangingCore(null);
showNotice('error', err.message || err.toString()); showNotice("error", err.message || err.toString());
} }
}); });
@@ -73,11 +73,11 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
try { try {
setRestarting(true); setRestarting(true);
await restartCore(); await restartCore();
showNotice('success', t(`Clash Core Restarted`)); showNotice("success", t(`Clash Core Restarted`));
setRestarting(false); setRestarting(false);
} catch (err: any) { } catch (err: any) {
setRestarting(false); setRestarting(false);
showNotice('error', err.message || err.toString()); showNotice("error", err.message || err.toString());
} }
}); });
@@ -86,14 +86,14 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
setUpgrading(true); setUpgrading(true);
await upgradeCore(); await upgradeCore();
setUpgrading(false); setUpgrading(false);
showNotice('success', t(`Core Version Updated`)); showNotice("success", t(`Core Version Updated`));
} catch (err: any) { } catch (err: any) {
setUpgrading(false); setUpgrading(false);
const errMsg = err.response?.data?.message || err.toString(); const errMsg = err.response?.data?.message || err.toString();
const showMsg = errMsg.includes("already using latest version") const showMsg = errMsg.includes("already using latest version")
? "Already Using Latest Core Version" ? "Already Using Latest Core Version"
: errMsg; : errMsg;
showNotice('error', t(showMsg)); showNotice("error", t(showMsg));
} }
}); });
@@ -11,7 +11,7 @@ import {
ListItem, ListItem,
ListItemText, ListItemText,
Stack, Stack,
TextField TextField,
} from "@mui/material"; } from "@mui/material";
import { useLockFn, useRequest } from "ahooks"; import { useLockFn, useRequest } from "ahooks";
import { forwardRef, useImperativeHandle, useState } from "react"; import { forwardRef, useImperativeHandle, useState } from "react";
@@ -26,127 +26,136 @@ interface ClashPortViewerRef {
close: () => void; close: () => void;
} }
const generateRandomPort = () => Math.floor(Math.random() * (65535 - 1025 + 1)) + 1025; const generateRandomPort = () =>
Math.floor(Math.random() * (65535 - 1025 + 1)) + 1025;
export const ClashPortViewer = forwardRef<ClashPortViewerRef, ClashPortViewerProps>( export const ClashPortViewer = forwardRef<
(props, ref) => { ClashPortViewerRef,
const { t } = useTranslation(); ClashPortViewerProps
const { clashInfo, patchInfo } = useClashInfo(); >((props, ref) => {
const { verge, patchVerge } = useVerge(); const { t } = useTranslation();
const [open, setOpen] = useState(false); const { clashInfo, patchInfo } = useClashInfo();
const { verge, patchVerge } = useVerge();
const [open, setOpen] = useState(false);
// Mixed Port // Mixed Port
const [mixedPort, setMixedPort] = useState( const [mixedPort, setMixedPort] = useState(
verge?.verge_mixed_port ?? clashInfo?.mixed_port ?? 7897 verge?.verge_mixed_port ?? clashInfo?.mixed_port ?? 7897,
); );
// 其他端口状态 // 其他端口状态
const [socksPort, setSocksPort] = useState(verge?.verge_socks_port ?? 7898); const [socksPort, setSocksPort] = useState(verge?.verge_socks_port ?? 7898);
const [socksEnabled, setSocksEnabled] = useState(verge?.verge_socks_enabled ?? false); const [socksEnabled, setSocksEnabled] = useState(
const [httpPort, setHttpPort] = useState(verge?.verge_port ?? 7899); verge?.verge_socks_enabled ?? false,
const [httpEnabled, setHttpEnabled] = useState(verge?.verge_http_enabled ?? false); );
const [redirPort, setRedirPort] = useState(verge?.verge_redir_port ?? 7895); const [httpPort, setHttpPort] = useState(verge?.verge_port ?? 7899);
const [redirEnabled, setRedirEnabled] = useState(verge?.verge_redir_enabled ?? false); const [httpEnabled, setHttpEnabled] = useState(
const [tproxyPort, setTproxyPort] = useState(verge?.verge_tproxy_port ?? 7896); verge?.verge_http_enabled ?? false,
const [tproxyEnabled, setTproxyEnabled] = useState(verge?.verge_tproxy_enabled ?? false); );
const [redirPort, setRedirPort] = useState(verge?.verge_redir_port ?? 7895);
const [redirEnabled, setRedirEnabled] = useState(
verge?.verge_redir_enabled ?? false,
);
const [tproxyPort, setTproxyPort] = useState(
verge?.verge_tproxy_port ?? 7896,
);
const [tproxyEnabled, setTproxyEnabled] = useState(
verge?.verge_tproxy_enabled ?? false,
);
// 添加保存请求,防止GUI卡死 // 添加保存请求,防止GUI卡死
const { loading, run: saveSettings } = useRequest( const { loading, run: saveSettings } = useRequest(
async (params: { async (params: { clashConfig: any; vergeConfig: any }) => {
clashConfig: any; const { clashConfig, vergeConfig } = params;
vergeConfig: any; await Promise.all([patchInfo(clashConfig), patchVerge(vergeConfig)]);
}) => { },
const { clashConfig, vergeConfig } = params; {
await Promise.all([ manual: true,
patchInfo(clashConfig), onSuccess: () => {
patchVerge(vergeConfig) setOpen(false);
]); showNotice("success", t("Port settings saved")); // 调用提示函数
}, },
{ onError: () => {
manual: true, showNotice("error", t("Failed to save settings")); // 调用提示函数
onSuccess: () => {
setOpen(false);
showNotice("success", t("Port settings saved")); // 调用提示函数
},
onError: () => {
showNotice("error", t("Failed to save settings")); // 调用提示函数
}
}
);
useImperativeHandle(ref, () => ({
open: () => {
setMixedPort(verge?.verge_mixed_port ?? clashInfo?.mixed_port ?? 7897);
setSocksPort(verge?.verge_socks_port ?? 7898);
setSocksEnabled(verge?.verge_socks_enabled ?? false);
setHttpPort(verge?.verge_port ?? 7899);
setHttpEnabled(verge?.verge_http_enabled ?? false);
setRedirPort(verge?.verge_redir_port ?? 7895);
setRedirEnabled(verge?.verge_redir_enabled ?? false);
setTproxyPort(verge?.verge_tproxy_port ?? 7896);
setTproxyEnabled(verge?.verge_tproxy_enabled ?? false);
setOpen( true);
}, },
close: () => setOpen(false), },
})); );
const onSave = useLockFn(async () => { useImperativeHandle(ref, () => ({
// 端口冲突检测 open: () => {
const portList = [ setMixedPort(verge?.verge_mixed_port ?? clashInfo?.mixed_port ?? 7897);
mixedPort, setSocksPort(verge?.verge_socks_port ?? 7898);
socksEnabled ? socksPort : -1, setSocksEnabled(verge?.verge_socks_enabled ?? false);
httpEnabled ? httpPort : -1, setHttpPort(verge?.verge_port ?? 7899);
redirEnabled ? redirPort : -1, setHttpEnabled(verge?.verge_http_enabled ?? false);
tproxyEnabled ? tproxyPort : -1 setRedirPort(verge?.verge_redir_port ?? 7895);
].filter(p => p !== -1); setRedirEnabled(verge?.verge_redir_enabled ?? false);
setTproxyPort(verge?.verge_tproxy_port ?? 7896);
setTproxyEnabled(verge?.verge_tproxy_enabled ?? false);
setOpen(true);
},
close: () => setOpen(false),
}));
if (new Set(portList).size !== portList.length) { const onSave = useLockFn(async () => {
return; // 端口冲突检测
} const portList = [
mixedPort,
socksEnabled ? socksPort : -1,
httpEnabled ? httpPort : -1,
redirEnabled ? redirPort : -1,
tproxyEnabled ? tproxyPort : -1,
].filter((p) => p !== -1);
// 验证端口范围 if (new Set(portList).size !== portList.length) {
const isValidPort = (port: number) => port >= 1 && port <= 65535; return;
const allPortsValid = [ }
mixedPort,
socksEnabled ? socksPort : 0,
httpEnabled ? httpPort : 0,
redirEnabled ? redirPort : 0,
tproxyEnabled ? tproxyPort : 0
].every(port => port === 0 || isValidPort(port));
if (!allPortsValid) { // 验证端口范围
return; const isValidPort = (port: number) => port >= 1 && port <= 65535;
} const allPortsValid = [
mixedPort,
socksEnabled ? socksPort : 0,
httpEnabled ? httpPort : 0,
redirEnabled ? redirPort : 0,
tproxyEnabled ? tproxyPort : 0,
].every((port) => port === 0 || isValidPort(port));
// 准备配置数据 if (!allPortsValid) {
const clashConfig = { return;
"mixed-port": mixedPort, }
"socks-port": socksPort,
port: httpPort,
"redir-port": redirPort,
"tproxy-port": tproxyPort
};
const vergeConfig = { // 准备配置数据
verge_mixed_port: mixedPort, const clashConfig = {
verge_socks_port: socksPort, "mixed-port": mixedPort,
verge_socks_enabled: socksEnabled, "socks-port": socksPort,
verge_port: httpPort, port: httpPort,
verge_http_enabled: httpEnabled, "redir-port": redirPort,
verge_redir_port: redirPort, "tproxy-port": tproxyPort,
verge_redir_enabled: redirEnabled, };
verge_tproxy_port: tproxyPort,
verge_tproxy_enabled: tproxyEnabled
};
// 提交保存请求 const vergeConfig = {
await saveSettings({ clashConfig, vergeConfig }); verge_mixed_port: mixedPort,
}); verge_socks_port: socksPort,
verge_socks_enabled: socksEnabled,
verge_port: httpPort,
verge_http_enabled: httpEnabled,
verge_redir_port: redirPort,
verge_redir_enabled: redirEnabled,
verge_tproxy_port: tproxyPort,
verge_tproxy_enabled: tproxyEnabled,
};
// 优化的数字输入处理 // 提交保存请求
const handleNumericChange = (setter: (value: number) => void) => (e: React.ChangeEvent<HTMLInputElement>) => { await saveSettings({ clashConfig, vergeConfig });
const value = e.target.value.replace(/\D+/, ''); });
if (value === '') {
// 优化的数字输入处理
const handleNumericChange =
(setter: (value: number) => void) =>
(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value.replace(/\D+/, "");
if (value === "") {
setter(0); setter(0);
return; return;
} }
@@ -157,190 +166,201 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef, ClashPortViewerPro
} }
}; };
return ( return (
<BaseDialog <BaseDialog
open={open} open={open}
title={t("Port Configuration")} title={t("Port Configuration")}
contentSx={{ contentSx={{
width: 400 width: 400,
}} }}
okBtn={ okBtn={
loading ? ( loading ? (
<Stack direction="row" alignItems="center" spacing={1}> <Stack direction="row" alignItems="center" spacing={1}>
<CircularProgress size={20} /> <CircularProgress size={20} />
{t("Saving...")} {t("Saving...")}
</Stack> </Stack>
) : t("Save") ) : (
} t("Save")
cancelBtn={t("Cancel")} )
onClose={() => setOpen(false)} }
onCancel={() => setOpen(false)} cancelBtn={t("Cancel")}
onOk={onSave} onClose={() => setOpen(false)}
> onCancel={() => setOpen(false)}
<List sx={{ width: "100%" }}> onOk={onSave}
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}> >
<ListItemText <List sx={{ width: "100%" }}>
primary={t("Mixed Port")} <ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
primaryTypographyProps={{ fontSize: 12 }} <ListItemText
primary={t("Mixed Port")}
primaryTypographyProps={{ fontSize: 12 }}
/>
<div style={{ display: "flex", alignItems: "center" }}>
<TextField
size="small"
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
value={mixedPort}
onChange={(e) =>
setMixedPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))
}
inputProps={{ style: { fontSize: 12 } }}
/> />
<div style={{ display: "flex", alignItems: "center" }}> <IconButton
<TextField size="small"
size="small" onClick={() => setMixedPort(generateRandomPort())}
sx={{ width: 80, mr: 0.5, fontSize: 12 }} title={t("Random Port")}
value={mixedPort} sx={{ mr: 0.5 }}
onChange={(e) => setMixedPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))} >
inputProps={{ style: { fontSize: 12 } }} <Shuffle fontSize="small" />
/> </IconButton>
<IconButton <Switch
size="small" size="small"
onClick={() => setMixedPort(generateRandomPort())} checked={true}
title={t("Random Port")} disabled={true}
sx={{ mr: 0.5 }} sx={{ ml: 0.5, opacity: 0.7 }}
> />
<Shuffle fontSize="small" /> </div>
</IconButton> </ListItem>
<Switch
size="small"
checked={true}
disabled={true}
sx={{ ml: 0.5, opacity: 0.7 }}
/>
</div>
</ListItem>
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
<ListItemText
primary={t("Socks Port")}
primaryTypographyProps={{ fontSize: 12 }}
/>
<div style={{ display: "flex", alignItems: "center" }}>
<TextField
size="small"
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
value={socksPort}
onChange={(e) =>
setSocksPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))
}
disabled={!socksEnabled}
inputProps={{ style: { fontSize: 12 } }}
/>
<IconButton
size="small"
onClick={() => setSocksPort(generateRandomPort())}
title={t("Random Port")}
disabled={!socksEnabled}
sx={{ mr: 0.5 }}
>
<Shuffle fontSize="small" />
</IconButton>
<Switch
size="small"
checked={socksEnabled}
onChange={(_, c) => setSocksEnabled(c)}
sx={{ ml: 0.5 }}
/>
</div>
</ListItem>
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
<ListItemText
primary={t("HTTP Port")}
primaryTypographyProps={{ fontSize: 12 }}
/>
<div style={{ display: "flex", alignItems: "center" }}>
<TextField
size="small"
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
value={httpPort}
onChange={(e) =>
setHttpPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))
}
disabled={!httpEnabled}
inputProps={{ style: { fontSize: 12 } }}
/>
<IconButton
size="small"
onClick={() => setHttpPort(generateRandomPort())}
title={t("Random Port")}
disabled={!httpEnabled}
sx={{ mr: 0.5 }}
>
<Shuffle fontSize="small" />
</IconButton>
<Switch
size="small"
checked={httpEnabled}
onChange={(_, c) => setHttpEnabled(c)}
sx={{ ml: 0.5 }}
/>
</div>
</ListItem>
{OS !== "windows" && (
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}> <ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
<ListItemText <ListItemText
primary={t("Socks Port")} primary={t("Redir Port")}
primaryTypographyProps={{ fontSize: 12 }} primaryTypographyProps={{ fontSize: 12 }}
/> />
<div style={{ display: "flex", alignItems: "center" }}> <div style={{ display: "flex", alignItems: "center" }}>
<TextField <TextField
size="small" size="small"
sx={{ width: 80, mr: 0.5, fontSize: 12 }} sx={{ width: 80, mr: 0.5, fontSize: 12 }}
value={socksPort} value={redirPort}
onChange={(e) => setSocksPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))} onChange={(e) =>
disabled={!socksEnabled} setRedirPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))
}
disabled={!redirEnabled}
inputProps={{ style: { fontSize: 12 } }} inputProps={{ style: { fontSize: 12 } }}
/> />
<IconButton <IconButton
size="small" size="small"
onClick={() => setSocksPort(generateRandomPort())} onClick={() => setRedirPort(generateRandomPort())}
title={t("Random Port")} title={t("Random Port")}
disabled={!socksEnabled} disabled={!redirEnabled}
sx={{ mr: 0.5 }} sx={{ mr: 0.5 }}
> >
<Shuffle fontSize="small" /> <Shuffle fontSize="small" />
</IconButton> </IconButton>
<Switch <Switch
size="small" size="small"
checked={socksEnabled} checked={redirEnabled}
onChange={(_, c) => setSocksEnabled(c)} onChange={(_, c) => setRedirEnabled(c)}
sx={{ ml: 0.5 }} sx={{ ml: 0.5 }}
/> />
</div> </div>
</ListItem> </ListItem>
)}
{OS === "linux" && (
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}> <ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
<ListItemText <ListItemText
primary={t("HTTP Port")} primary={t("Tproxy Port")}
primaryTypographyProps={{ fontSize: 12 }} primaryTypographyProps={{ fontSize: 12 }}
/> />
<div style={{ display: "flex", alignItems: "center" }}> <div style={{ display: "flex", alignItems: "center" }}>
<TextField <TextField
size="small" size="small"
sx={{ width: 80, mr: 0.5, fontSize: 12 }} sx={{ width: 80, mr: 0.5, fontSize: 12 }}
value={httpPort} value={tproxyPort}
onChange={(e) => setHttpPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))} onChange={(e) =>
disabled={!httpEnabled} setTproxyPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))
}
disabled={!tproxyEnabled}
inputProps={{ style: { fontSize: 12 } }} inputProps={{ style: { fontSize: 12 } }}
/> />
<IconButton <IconButton
size="small" size="small"
onClick={() => setHttpPort(generateRandomPort())} onClick={() => setTproxyPort(generateRandomPort())}
title={t("Random Port")} title={t("Random Port")}
disabled={!httpEnabled} disabled={!tproxyEnabled}
sx={{ mr: 0.5 }} sx={{ mr: 0.5 }}
> >
<Shuffle fontSize="small" /> <Shuffle fontSize="small" />
</IconButton> </IconButton>
<Switch <Switch
size="small" size="small"
checked={httpEnabled} checked={tproxyEnabled}
onChange={(_, c) => setHttpEnabled(c)} onChange={(_, c) => setTproxyEnabled(c)}
sx={{ ml: 0.5 }} sx={{ ml: 0.5 }}
/> />
</div> </div>
</ListItem> </ListItem>
)}
{OS !== "windows" && ( </List>
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}> </BaseDialog>
<ListItemText );
primary={t("Redir Port")} });
primaryTypographyProps={{ fontSize: 12 }}
/>
<div style={{ display: "flex", alignItems: "center" }}>
<TextField
size="small"
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
value={redirPort}
onChange={(e) => setRedirPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))}
disabled={!redirEnabled}
inputProps={{ style: { fontSize: 12 } }}
/>
<IconButton
size="small"
onClick={() => setRedirPort(generateRandomPort())}
title={t("Random Port")}
disabled={!redirEnabled}
sx={{ mr: 0.5 }}
>
<Shuffle fontSize="small" />
</IconButton>
<Switch
size="small"
checked={redirEnabled}
onChange={(_, c) => setRedirEnabled(c)}
sx={{ ml: 0.5 }}
/>
</div>
</ListItem>
)}
{OS === "linux" && (
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
<ListItemText
primary={t("Tproxy Port")}
primaryTypographyProps={{ fontSize: 12 }}
/>
<div style={{ display: "flex", alignItems: "center" }}>
<TextField
size="small"
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
value={tproxyPort}
onChange={(e) => setTproxyPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))}
disabled={!tproxyEnabled}
inputProps={{ style: { fontSize: 12 } }}
/>
<IconButton
size="small"
onClick={() => setTproxyPort(generateRandomPort())}
title={t("Random Port")}
disabled={!tproxyEnabled}
sx={{ mr: 0.5 }}
>
<Shuffle fontSize="small" />
</IconButton>
<Switch
size="small"
checked={tproxyEnabled}
onChange={(_, c) => setTproxyEnabled(c)}
sx={{ ml: 0.5 }}
/>
</div>
</ListItem>
)}
</List>
</BaseDialog>
);
}
);
@@ -13,7 +13,7 @@ import {
ListItemText, ListItemText,
Snackbar, Snackbar,
TextField, TextField,
Tooltip Tooltip,
} from "@mui/material"; } from "@mui/material";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
import { forwardRef, useEffect, useImperativeHandle, useState } from "react"; import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
@@ -42,58 +42,72 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
// 保存配置 // 保存配置
const onSave = useLockFn(async () => { const onSave = useLockFn(async () => {
if (!controller.trim()) { if (!controller.trim()) {
showNotice('error', t("Controller address cannot be empty"), 3000); showNotice("error", t("Controller address cannot be empty"), 3000);
return; return;
} }
if (!secret.trim()) { if (!secret.trim()) {
showNotice('error', t("Secret cannot be empty"), 3000); showNotice("error", t("Secret cannot be empty"), 3000);
return; return;
} }
try { try {
setIsSaving(true); setIsSaving(true);
await patchInfo({ "external-controller": controller, secret }); await patchInfo({ "external-controller": controller, secret });
showNotice('success', t("Configuration saved successfully"), 2000); showNotice("success", t("Configuration saved successfully"), 2000);
setOpen(false); setOpen(false);
} catch (err: any) { } catch (err: any) {
showNotice('error', err.message || t("Failed to save configuration"), 4000); showNotice(
"error",
err.message || t("Failed to save configuration"),
4000,
);
} finally { } finally {
setIsSaving(false); setIsSaving(false);
} }
}); });
// 复制到剪贴板 // 复制到剪贴板
const handleCopyToClipboard = useLockFn(async (text: string, type: string) => { const handleCopyToClipboard = useLockFn(
try { async (text: string, type: string) => {
await navigator.clipboard.writeText(text); try {
setCopySuccess(type); await navigator.clipboard.writeText(text);
setTimeout(() => setCopySuccess(null), 2000); setCopySuccess(type);
} catch (err) { setTimeout(() => setCopySuccess(null), 2000);
showNotice('error', t("Failed to copy"), 2000); } catch (err) {
} showNotice("error", t("Failed to copy"), 2000);
}); }
},
);
return ( return (
<BaseDialog <BaseDialog
open={open} open={open}
title={t("External Controller")} title={t("External Controller")}
contentSx={{ width: 400 }} contentSx={{ width: 400 }}
okBtn={isSaving ? ( okBtn={
<Box display="flex" alignItems="center" gap={1}> isSaving ? (
<CircularProgress size={16} color="inherit" /> <Box display="flex" alignItems="center" gap={1}>
{t("Saving...")} <CircularProgress size={16} color="inherit" />
</Box> {t("Saving...")}
) : ( </Box>
t("Save") ) : (
)} t("Save")
)
}
cancelBtn={t("Cancel")} cancelBtn={t("Cancel")}
onClose={() => setOpen(false)} onClose={() => setOpen(false)}
onCancel={() => setOpen(false)} onCancel={() => setOpen(false)}
onOk={onSave} onOk={onSave}
> >
<List> <List>
<ListItem sx={{ padding: "5px 2px", display: "flex", justifyContent: "space-between" }}> <ListItem
sx={{
padding: "5px 2px",
display: "flex",
justifyContent: "space-between",
}}
>
<ListItemText primary={t("External Controller")} /> <ListItemText primary={t("External Controller")} />
<Box display="flex" alignItems="center" gap={1}> <Box display="flex" alignItems="center" gap={1}>
<TextField <TextField
@@ -101,11 +115,11 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
sx={{ sx={{
width: 175, width: 175,
opacity: 1, opacity: 1,
pointerEvents: 'auto' pointerEvents: "auto",
}} }}
value={controller} value={controller}
placeholder="Required" placeholder="Required"
onChange={e => setController(e.target.value)} onChange={(e) => setController(e.target.value)}
disabled={isSaving} disabled={isSaving}
/> />
<Tooltip title={t("Copy to clipboard")}> <Tooltip title={t("Copy to clipboard")}>
@@ -121,7 +135,13 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
</Box> </Box>
</ListItem> </ListItem>
<ListItem sx={{ padding: "5px 2px", display: "flex", justifyContent: "space-between" }}> <ListItem
sx={{
padding: "5px 2px",
display: "flex",
justifyContent: "space-between",
}}
>
<ListItemText primary={t("Core Secret")} /> <ListItemText primary={t("Core Secret")} />
<Box display="flex" alignItems="center" gap={1}> <Box display="flex" alignItems="center" gap={1}>
<TextField <TextField
@@ -129,11 +149,11 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
sx={{ sx={{
width: 175, width: 175,
opacity: 1, opacity: 1,
pointerEvents: 'auto' pointerEvents: "auto",
}} }}
value={secret} value={secret}
placeholder={t("Recommended")} placeholder={t("Recommended")}
onChange={e => setSecret(e.target.value)} onChange={(e) => setSecret(e.target.value)}
disabled={isSaving} disabled={isSaving}
/> />
<Tooltip title={t("Copy to clipboard")}> <Tooltip title={t("Copy to clipboard")}>
@@ -153,13 +173,12 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
<Snackbar <Snackbar
open={copySuccess !== null} open={copySuccess !== null}
autoHideDuration={2000} autoHideDuration={2000}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }} anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
> >
<Alert severity="success"> <Alert severity="success">
{copySuccess === "controller" {copySuccess === "controller"
? t("Controller address copied to clipboard") ? t("Controller address copied to clipboard")
: t("Secret copied to clipboard") : t("Secret copied to clipboard")}
}
</Alert> </Alert>
</Snackbar> </Snackbar>
</BaseDialog> </BaseDialog>
@@ -59,7 +59,13 @@ const DEFAULT_DNS_CONFIG = {
"*.msftncsi.com", "*.msftncsi.com",
"www.msftconnecttest.com", "www.msftconnecttest.com",
], ],
"default-nameserver": ["system", "223.6.6.6", "8.8.8.8", "2400:3200::1", "2001:4860:4860::8888"], "default-nameserver": [
"system",
"223.6.6.6",
"8.8.8.8",
"2400:3200::1",
"2001:4860:4860::8888",
],
nameserver: [ nameserver: [
"8.8.8.8", "8.8.8.8",
"https://doh.pub/dns-query", "https://doh.pub/dns-query",
@@ -70,7 +76,7 @@ const DEFAULT_DNS_CONFIG = {
"proxy-server-nameserver": [ "proxy-server-nameserver": [
"https://doh.pub/dns-query", "https://doh.pub/dns-query",
"https://dns.alidns.com/dns-query", "https://dns.alidns.com/dns-query",
"tls://223.5.5.5" "tls://223.5.5.5",
], ],
"direct-nameserver": [], "direct-nameserver": [],
"direct-nameserver-follow-policy": false, "direct-nameserver-follow-policy": false,
@@ -219,8 +225,7 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
dnsConfig["respect-rules"] ?? DEFAULT_DNS_CONFIG["respect-rules"], dnsConfig["respect-rules"] ?? DEFAULT_DNS_CONFIG["respect-rules"],
useHosts: dnsConfig["use-hosts"] ?? DEFAULT_DNS_CONFIG["use-hosts"], useHosts: dnsConfig["use-hosts"] ?? DEFAULT_DNS_CONFIG["use-hosts"],
useSystemHosts: useSystemHosts:
dnsConfig["use-system-hosts"] ?? dnsConfig["use-system-hosts"] ?? DEFAULT_DNS_CONFIG["use-system-hosts"],
DEFAULT_DNS_CONFIG["use-system-hosts"],
ipv6: dnsConfig.ipv6 ?? DEFAULT_DNS_CONFIG.ipv6, ipv6: dnsConfig.ipv6 ?? DEFAULT_DNS_CONFIG.ipv6,
fakeIpFilter: fakeIpFilter:
dnsConfig["fake-ip-filter"]?.join(", ") ?? dnsConfig["fake-ip-filter"]?.join(", ") ??
@@ -229,7 +234,8 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
dnsConfig.nameserver?.join(", ") ?? dnsConfig.nameserver?.join(", ") ??
DEFAULT_DNS_CONFIG.nameserver.join(", "), DEFAULT_DNS_CONFIG.nameserver.join(", "),
fallback: fallback:
dnsConfig.fallback?.join(", ") ?? DEFAULT_DNS_CONFIG.fallback.join(", "), dnsConfig.fallback?.join(", ") ??
DEFAULT_DNS_CONFIG.fallback.join(", "),
defaultNameserver: defaultNameserver:
dnsConfig["default-nameserver"]?.join(", ") ?? dnsConfig["default-nameserver"]?.join(", ") ??
DEFAULT_DNS_CONFIG["default-nameserver"].join(", "), DEFAULT_DNS_CONFIG["default-nameserver"].join(", "),
@@ -299,7 +305,6 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
// 从表单值更新YAML内容 // 从表单值更新YAML内容
const updateYamlFromValues = () => { const updateYamlFromValues = () => {
const config: Record<string, any> = {}; const config: Record<string, any> = {};
const dnsConfig = generateDnsConfig(); const dnsConfig = generateDnsConfig();
@@ -323,7 +328,7 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
updateValuesFromConfig(parsedYaml); updateValuesFromConfig(parsedYaml);
} catch (err: any) { } catch (err: any) {
showNotice('error', t("Invalid YAML format")); showNotice("error", t("Invalid YAML format"));
} }
}; };
@@ -523,28 +528,39 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
await invoke("save_dns_config", { dnsConfig: config }); await invoke("save_dns_config", { dnsConfig: config });
// 验证配置 // 验证配置
const [isValid, errorMsg] = await invoke<[boolean, string]>("validate_dns_config", {}); const [isValid, errorMsg] = await invoke<[boolean, string]>(
"validate_dns_config",
{},
);
if (!isValid) { if (!isValid) {
let cleanErrorMsg = errorMsg; let cleanErrorMsg = errorMsg;
// 提取关键错误信息 // 提取关键错误信息
if (errorMsg.includes("level=error")) { if (errorMsg.includes("level=error")) {
const errorLines = errorMsg.split('\n').filter(line => const errorLines = errorMsg
line.includes("level=error") || .split("\n")
line.includes("level=fatal") || .filter(
line.includes("failed") (line) =>
); line.includes("level=error") ||
line.includes("level=fatal") ||
line.includes("failed"),
);
if (errorLines.length > 0) { if (errorLines.length > 0) {
cleanErrorMsg = errorLines.map(line => { cleanErrorMsg = errorLines
const msgMatch = line.match(/msg="([^"]+)"/); .map((line) => {
return msgMatch ? msgMatch[1] : line; const msgMatch = line.match(/msg="([^"]+)"/);
}).join(", "); return msgMatch ? msgMatch[1] : line;
})
.join(", ");
} }
} }
showNotice('error', t("DNS configuration error") + ": " + cleanErrorMsg); showNotice(
"error",
t("DNS configuration error") + ": " + cleanErrorMsg,
);
return; return;
} }
@@ -555,9 +571,9 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
} }
setOpen(false); setOpen(false);
showNotice('success', t("DNS settings saved")); showNotice("success", t("DNS settings saved"));
} catch (err: any) { } catch (err: any) {
showNotice('error', err.message || err.toString()); showNotice("error", err.message || err.toString());
} }
}); });
@@ -80,7 +80,7 @@ export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
}); });
setOpen(false); setOpen(false);
} catch (err: any) { } catch (err: any) {
showNotice('error', err.toString()); showNotice("error", err.toString());
} }
}); });
@@ -88,7 +88,7 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
const onSwitchFormat = (_e: any, value: boolean) => value; const onSwitchFormat = (_e: any, value: boolean) => value;
const onError = (err: any) => { const onError = (err: any) => {
showNotice('error', err.message || err.toString()); showNotice("error", err.message || err.toString());
}; };
const onChangeData = (patch: Partial<IVergeConfig>) => { const onChangeData = (patch: Partial<IVergeConfig>) => {
mutateVerge({ ...verge, ...patch }, false); mutateVerge({ ...verge, ...patch }, false);
@@ -44,7 +44,7 @@ export const LiteModeViewer = forwardRef<DialogRef>((props, ref) => {
}); });
setOpen(false); setOpen(false);
} catch (err: any) { } catch (err: any) {
showNotice('error', err.message || err.toString()); showNotice("error", err.message || err.toString());
} }
}); });
@@ -62,7 +62,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
}); });
setOpen(false); setOpen(false);
} catch (err: any) { } catch (err: any) {
showNotice('error', err.toString()); showNotice("error", err.toString());
} }
}); });
@@ -246,7 +246,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
endAdornment: ( endAdornment: (
<InputAdornment position="end">{t("millis")}</InputAdornment> <InputAdornment position="end">{t("millis")}</InputAdornment>
), ),
} },
}} }}
/> />
</ListItem> </ListItem>

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