Update On Fri Dec 19 19:41:39 CET 2025

This commit is contained in:
github-action[bot]
2025-12-19 19:41:40 +01:00
parent f649634e7c
commit 095f4a24d3
406 changed files with 14464 additions and 10964 deletions
+1
View File
@@ -1216,3 +1216,4 @@ Update On Mon Dec 15 19:43:13 CET 2025
Update On Tue Dec 16 19:42:39 CET 2025
Update On Wed Dec 17 19:43:54 CET 2025
Update On Thu Dec 18 19:42:36 CET 2025
Update On Fri Dec 19 19:41:31 CET 2025
+4 -4
View File
@@ -10,7 +10,6 @@ import (
"sync"
"github.com/metacubex/mihomo/common/once"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/ntp"
"github.com/metacubex/tls"
@@ -107,12 +106,13 @@ func GetTLSConfig(opt Option) (tlsConfig *tls.Config, err error) {
}
if len(opt.Certificate) > 0 || len(opt.PrivateKey) > 0 {
var cert tls.Certificate
cert, err = LoadTLSKeyPair(opt.Certificate, opt.PrivateKey, C.Path)
certLoader, err := NewTLSKeyPairLoader(opt.Certificate, opt.PrivateKey)
if err != nil {
return nil, err
}
tlsConfig.Certificates = []tls.Certificate{cert}
tlsConfig.GetClientCertificate = func(*tls.CertificateRequestInfo) (*tls.Certificate, error) {
return certLoader()
}
}
return tlsConfig, nil
}
+46 -30
View File
@@ -12,67 +12,80 @@ import (
"fmt"
"math/big"
"os"
"runtime"
"sync"
"time"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/fswatch"
"github.com/metacubex/tls"
)
type Path interface {
Resolve(path string) string
IsSafePath(path string) bool
ErrNotSafePath(path string) error
}
// LoadTLSKeyPair loads a TLS key pair from the provided certificate and private key data or file paths, supporting fallback resolution.
// Returns a tls.Certificate and an error, where the error indicates issues during parsing or file loading.
// NewTLSKeyPairLoader creates a loader function for TLS key pairs from the provided certificate and private key data or file paths.
// If both certificate and privateKey are empty, generates a random TLS RSA key pair.
// Accepts a Path interface for resolving file paths when necessary.
func LoadTLSKeyPair(certificate, privateKey string, path Path) (tls.Certificate, error) {
func NewTLSKeyPairLoader(certificate, privateKey string) (func() (*tls.Certificate, error), error) {
if certificate == "" && privateKey == "" {
var err error
certificate, privateKey, _, err = NewRandomTLSKeyPair(KeyPairTypeRSA)
if err != nil {
return tls.Certificate{}, err
return nil, err
}
}
cert, painTextErr := tls.X509KeyPair([]byte(certificate), []byte(privateKey))
if painTextErr == nil {
return cert, nil
}
if path == nil {
return tls.Certificate{}, painTextErr
return func() (*tls.Certificate, error) {
return &cert, nil
}, nil
}
certificate = path.Resolve(certificate)
privateKey = path.Resolve(privateKey)
certificate = C.Path.Resolve(certificate)
privateKey = C.Path.Resolve(privateKey)
var loadErr error
if !path.IsSafePath(certificate) {
loadErr = path.ErrNotSafePath(certificate)
} else if !path.IsSafePath(privateKey) {
loadErr = path.ErrNotSafePath(privateKey)
if !C.Path.IsSafePath(certificate) {
loadErr = C.Path.ErrNotSafePath(certificate)
} else if !C.Path.IsSafePath(privateKey) {
loadErr = C.Path.ErrNotSafePath(privateKey)
} else {
cert, loadErr = tls.LoadX509KeyPair(certificate, privateKey)
}
if loadErr != nil {
return tls.Certificate{}, fmt.Errorf("parse certificate failed, maybe format error:%s, or path error: %s", painTextErr.Error(), loadErr.Error())
return nil, fmt.Errorf("parse certificate failed, maybe format error:%s, or path error: %s", painTextErr.Error(), loadErr.Error())
}
return cert, nil
gcFlag := new(os.File)
updateMutex := sync.RWMutex{}
if watcher, err := fswatch.NewWatcher(fswatch.Options{Path: []string{certificate, privateKey}, Callback: func(path string) {
updateMutex.Lock()
defer updateMutex.Unlock()
if newCert, err := tls.LoadX509KeyPair(certificate, privateKey); err == nil {
cert = newCert
}
}}); err == nil {
if err = watcher.Start(); err == nil {
runtime.SetFinalizer(gcFlag, func(f *os.File) {
_ = watcher.Close()
})
}
}
return func() (*tls.Certificate, error) {
defer runtime.KeepAlive(gcFlag)
updateMutex.RLock()
defer updateMutex.RUnlock()
return &cert, nil
}, nil
}
func LoadCertificates(certificate string, path Path) (*x509.CertPool, error) {
func LoadCertificates(certificate string) (*x509.CertPool, error) {
pool := x509.NewCertPool()
if pool.AppendCertsFromPEM([]byte(certificate)) {
return pool, nil
}
painTextErr := fmt.Errorf("invalid certificate: %s", certificate)
if path == nil {
return nil, painTextErr
}
certificate = path.Resolve(certificate)
certificate = C.Path.Resolve(certificate)
var loadErr error
if !path.IsSafePath(certificate) {
loadErr = path.ErrNotSafePath(certificate)
if !C.Path.IsSafePath(certificate) {
loadErr = C.Path.ErrNotSafePath(certificate)
} else {
certPEMBlock, err := os.ReadFile(certificate)
if pool.AppendCertsFromPEM(certPEMBlock) {
@@ -83,6 +96,9 @@ func LoadCertificates(certificate string, path Path) (*x509.CertPool, error) {
if loadErr != nil {
return nil, fmt.Errorf("parse certificate failed, maybe format error:%s, or path error: %s", painTextErr.Error(), loadErr.Error())
}
//TODO: support dynamic update pool too
// blocked by: https://github.com/golang/go/issues/64796
// maybe we can direct add `GetRootCAs` and `GetClientCAs` to ourselves tls fork
return pool, nil
}
+40 -12
View File
@@ -8,9 +8,12 @@ import (
"errors"
"fmt"
"os"
"runtime"
"sync"
"github.com/metacubex/mihomo/component/ca"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/fswatch"
"github.com/metacubex/tls"
"golang.org/x/crypto/cryptobyte"
)
@@ -104,40 +107,65 @@ func UnmarshalECHKeys(raw []byte) ([]tls.EncryptedClientHelloKey, error) {
return keys, nil
}
func LoadECHKey(key string, tlsConfig *tls.Config, path ca.Path) error {
func LoadECHKey(key string, tlsConfig *tls.Config) error {
if key == "" {
return nil
}
painTextErr := loadECHKey([]byte(key), tlsConfig)
echKeys, painTextErr := loadECHKey([]byte(key))
if painTextErr == nil {
tlsConfig.GetEncryptedClientHelloKeys = func(info *tls.ClientHelloInfo) ([]tls.EncryptedClientHelloKey, error) {
return echKeys, nil
}
return nil
}
key = path.Resolve(key)
key = C.Path.Resolve(key)
var loadErr error
if !path.IsSafePath(key) {
loadErr = path.ErrNotSafePath(key)
if !C.Path.IsSafePath(key) {
loadErr = C.Path.ErrNotSafePath(key)
} else {
var echKey []byte
echKey, loadErr = os.ReadFile(key)
if loadErr == nil {
loadErr = loadECHKey(echKey, tlsConfig)
echKeys, loadErr = loadECHKey(echKey)
}
}
if loadErr != nil {
return fmt.Errorf("parse ECH keys failed, maybe format error:%s, or path error: %s", painTextErr.Error(), loadErr.Error())
}
gcFlag := new(os.File)
updateMutex := sync.RWMutex{}
if watcher, err := fswatch.NewWatcher(fswatch.Options{Path: []string{key}, Callback: func(path string) {
updateMutex.Lock()
defer updateMutex.Unlock()
if echKey, err := os.ReadFile(key); err == nil {
if newEchKeys, err := loadECHKey(echKey); err == nil {
echKeys = newEchKeys
}
}
}}); err == nil {
if err = watcher.Start(); err == nil {
runtime.SetFinalizer(gcFlag, func(f *os.File) {
_ = watcher.Close()
})
}
}
tlsConfig.GetEncryptedClientHelloKeys = func(info *tls.ClientHelloInfo) ([]tls.EncryptedClientHelloKey, error) {
defer runtime.KeepAlive(gcFlag)
updateMutex.RLock()
defer updateMutex.RUnlock()
return echKeys, nil
}
return nil
}
func loadECHKey(echKey []byte, tlsConfig *tls.Config) error {
func loadECHKey(echKey []byte) ([]tls.EncryptedClientHelloKey, error) {
block, rest := pem.Decode(echKey)
if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 {
return errors.New("invalid ECH keys pem")
return nil, errors.New("invalid ECH keys pem")
}
echKeys, err := UnmarshalECHKeys(block.Bytes)
if err != nil {
return fmt.Errorf("parse ECH keys: %w", err)
return nil, fmt.Errorf("parse ECH keys: %w", err)
}
tlsConfig.EncryptedClientHelloKeys = echKeys
return nil
return echKeys, err
}
+53 -1
View File
@@ -1,7 +1,10 @@
package tls
import (
"context"
"net"
"reflect"
"unsafe"
"github.com/metacubex/mihomo/common/once"
"github.com/metacubex/mihomo/common/utils"
@@ -126,8 +129,11 @@ type EncryptedClientHelloKey = utls.EncryptedClientHelloKey
type Config = utls.Config
var tlsCertificateRequestInfoCtxOffset = utils.MustOK(reflect.TypeOf((*tls.CertificateRequestInfo)(nil)).Elem().FieldByName("ctx")).Offset
var tlsClientHelloInfoCtxOffset = utils.MustOK(reflect.TypeOf((*tls.ClientHelloInfo)(nil)).Elem().FieldByName("ctx")).Offset
func UConfig(config *tls.Config) *utls.Config {
return &utls.Config{
cfg := &utls.Config{
Rand: config.Rand,
Time: config.Time,
Certificates: utils.Map(config.Certificates, UCertificate),
@@ -147,6 +153,52 @@ func UConfig(config *tls.Config) *utls.Config {
SessionTicketsDisabled: config.SessionTicketsDisabled,
Renegotiation: utls.RenegotiationSupport(config.Renegotiation),
}
if config.GetClientCertificate != nil {
cfg.GetClientCertificate = func(info *utls.CertificateRequestInfo) (*utls.Certificate, error) {
tlsInfo := &tls.CertificateRequestInfo{
AcceptableCAs: info.AcceptableCAs,
SignatureSchemes: utils.Map(info.SignatureSchemes, func(it utls.SignatureScheme) tls.SignatureScheme {
return tls.SignatureScheme(it)
}),
Version: info.Version,
}
*(*context.Context)(unsafe.Add(unsafe.Pointer(tlsInfo), tlsCertificateRequestInfoCtxOffset)) = info.Context() // for tlsInfo.ctx
cert, err := config.GetClientCertificate(tlsInfo)
if err != nil {
return nil, err
}
uCert := UCertificate(*cert)
return &uCert, err
}
}
if config.GetCertificate != nil {
cfg.GetCertificate = func(info *utls.ClientHelloInfo) (*utls.Certificate, error) {
tlsInfo := &tls.ClientHelloInfo{
CipherSuites: info.CipherSuites,
ServerName: info.ServerName,
SupportedCurves: utils.Map(info.SupportedCurves, func(it utls.CurveID) tls.CurveID {
return tls.CurveID(it)
}),
SupportedPoints: info.SupportedPoints,
SignatureSchemes: utils.Map(info.SignatureSchemes, func(it utls.SignatureScheme) tls.SignatureScheme {
return tls.SignatureScheme(it)
}),
SupportedProtos: info.SupportedProtos,
SupportedVersions: info.SupportedVersions,
Extensions: info.Extensions,
Conn: info.Conn,
//HelloRetryRequest: info.HelloRetryRequest,
}
*(*context.Context)(unsafe.Add(unsafe.Pointer(tlsInfo), tlsClientHelloInfoCtxOffset)) = info.Context() // for tlsInfo.ctx
cert, err := config.GetCertificate(tlsInfo)
if err != nil {
return nil, err
}
uCert := UCertificate(*cert)
return &uCert, err
}
}
return cfg
}
// BuildWebsocketHandshakeState it will only send http/1.1 in its ALPN.
+9 -9
View File
@@ -1292,7 +1292,7 @@ func parseNameServerPolicy(nsPolicy *orderedmap.OrderedMap[string, any], rulePro
}
kLower := strings.ToLower(k)
if strings.Contains(kLower, ",") {
if strings.Contains(kLower, "geosite:") {
if strings.HasPrefix(kLower, "geosite:") {
subkeys := strings.Split(k, ":")
subkeys = subkeys[1:]
subkeys = strings.Split(subkeys[0], ",")
@@ -1300,7 +1300,7 @@ func parseNameServerPolicy(nsPolicy *orderedmap.OrderedMap[string, any], rulePro
newKey := "geosite:" + subkey
policy = append(policy, dns.Policy{Domain: newKey, NameServers: nameservers})
}
} else if strings.Contains(kLower, "rule-set:") {
} else if strings.HasPrefix(kLower, "rule-set:") {
subkeys := strings.Split(k, ":")
subkeys = subkeys[1:]
subkeys = strings.Split(subkeys[0], ",")
@@ -1315,9 +1315,9 @@ func parseNameServerPolicy(nsPolicy *orderedmap.OrderedMap[string, any], rulePro
}
}
} else {
if strings.Contains(kLower, "geosite:") {
if strings.HasPrefix(kLower, "geosite:") {
policy = append(policy, dns.Policy{Domain: "geosite:" + k[8:], NameServers: nameservers})
} else if strings.Contains(kLower, "rule-set:") {
} else if strings.HasPrefix(kLower, "rule-set:") {
policy = append(policy, dns.Policy{Domain: "rule-set:" + k[9:], NameServers: nameservers})
} else {
policy = append(policy, dns.Policy{Domain: k, NameServers: nameservers})
@@ -1712,7 +1712,7 @@ func parseSniffer(snifferRaw RawSniffer, ruleProviders map[string]P.RuleProvider
}
snifferConfig.SkipSrcAddress = skipSrcAddress
skipDstAddress, err := parseIPCIDR(snifferRaw.SkipDstAddress, nil, "sniffer.skip-src-address", ruleProviders)
skipDstAddress, err := parseIPCIDR(snifferRaw.SkipDstAddress, nil, "sniffer.skip-dst-address", ruleProviders)
if err != nil {
return nil, fmt.Errorf("error in skip-dst-address, error:%w", err)
}
@@ -1731,7 +1731,7 @@ func parseIPCIDR(addresses []string, cidrSet *cidr.IpCidrSet, adapterName string
var matcher C.IpMatcher
for _, ipcidr := range addresses {
ipcidrLower := strings.ToLower(ipcidr)
if strings.Contains(ipcidrLower, "geoip:") {
if strings.HasPrefix(ipcidrLower, "geoip:") {
subkeys := strings.Split(ipcidr, ":")
subkeys = subkeys[1:]
subkeys = strings.Split(subkeys[0], ",")
@@ -1742,7 +1742,7 @@ func parseIPCIDR(addresses []string, cidrSet *cidr.IpCidrSet, adapterName string
}
matchers = append(matchers, matcher)
}
} else if strings.Contains(ipcidrLower, "rule-set:") {
} else if strings.HasPrefix(ipcidrLower, "rule-set:") {
subkeys := strings.Split(ipcidr, ":")
subkeys = subkeys[1:]
subkeys = strings.Split(subkeys[0], ",")
@@ -1778,7 +1778,7 @@ func parseDomain(domains []string, domainTrie *trie.DomainTrie[struct{}], adapte
var matcher C.DomainMatcher
for _, domain := range domains {
domainLower := strings.ToLower(domain)
if strings.Contains(domainLower, "geosite:") {
if strings.HasPrefix(domainLower, "geosite:") {
subkeys := strings.Split(domain, ":")
subkeys = subkeys[1:]
subkeys = strings.Split(subkeys[0], ",")
@@ -1789,7 +1789,7 @@ func parseDomain(domains []string, domainTrie *trie.DomainTrie[struct{}], adapte
}
matchers = append(matchers, matcher)
}
} else if strings.Contains(domainLower, "rule-set:") {
} else if strings.HasPrefix(domainLower, "rule-set:") {
subkeys := strings.Split(domain, ":")
subkeys = subkeys[1:]
subkeys = strings.Split(subkeys[0], ",")
+6 -4
View File
@@ -191,7 +191,7 @@ func startTLS(cfg *Config) {
// handle tlsAddr
if len(cfg.TLSAddr) > 0 {
cert, err := ca.LoadTLSKeyPair(cfg.Certificate, cfg.PrivateKey, C.Path)
certLoader, err := ca.NewTLSKeyPairLoader(cfg.Certificate, cfg.PrivateKey)
if err != nil {
log.Errorln("External controller tls listen error: %s", err)
return
@@ -206,7 +206,9 @@ func startTLS(cfg *Config) {
log.Infoln("RESTful API tls listening at: %s", l.Addr().String())
tlsConfig := &tls.Config{Time: ntp.Now}
tlsConfig.NextProtos = []string{"h2", "http/1.1"}
tlsConfig.Certificates = []tls.Certificate{cert}
tlsConfig.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
return certLoader()
}
tlsConfig.ClientAuth = ca.ClientAuthTypeFromString(cfg.ClientAuthType)
if len(cfg.ClientAuthCert) > 0 {
if tlsConfig.ClientAuth == tls.NoClientCert {
@@ -214,7 +216,7 @@ func startTLS(cfg *Config) {
}
}
if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert {
pool, err := ca.LoadCertificates(cfg.ClientAuthCert, C.Path)
pool, err := ca.LoadCertificates(cfg.ClientAuthCert)
if err != nil {
log.Errorln("External controller tls listen error: %s", err)
return
@@ -223,7 +225,7 @@ func startTLS(cfg *Config) {
}
if cfg.EchKey != "" {
err = ech.LoadECHKey(cfg.EchKey, tlsConfig, C.Path)
err = ech.LoadECHKey(cfg.EchKey, tlsConfig)
if err != nil {
log.Errorln("External controller tls serve error: %s", err)
return
+7 -5
View File
@@ -45,14 +45,16 @@ func New(config LC.AnyTLSServer, tunnel C.Tunnel, additions ...inbound.Addition)
tlsConfig := &tls.Config{Time: ntp.Now}
if config.Certificate != "" && config.PrivateKey != "" {
cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path)
certLoader, err := ca.NewTLSKeyPairLoader(config.Certificate, config.PrivateKey)
if err != nil {
return nil, err
}
tlsConfig.Certificates = []tls.Certificate{cert}
tlsConfig.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
return certLoader()
}
if config.EchKey != "" {
err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path)
err = ech.LoadECHKey(config.EchKey, tlsConfig)
if err != nil {
return nil, err
}
@@ -65,7 +67,7 @@ func New(config LC.AnyTLSServer, tunnel C.Tunnel, additions ...inbound.Addition)
}
}
if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert {
pool, err := ca.LoadCertificates(config.ClientAuthCert, C.Path)
pool, err := ca.LoadCertificates(config.ClientAuthCert)
if err != nil {
return nil, err
}
@@ -108,7 +110,7 @@ func New(config LC.AnyTLSServer, tunnel C.Tunnel, additions ...inbound.Addition)
if err != nil {
return nil, err
}
if len(tlsConfig.Certificates) > 0 {
if tlsConfig.GetCertificate != nil {
l = tls.NewListener(l, tlsConfig)
} else {
return nil, errors.New("disallow using AnyTLS without certificates config")
+8 -6
View File
@@ -71,14 +71,16 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A
var realityBuilder *reality.Builder
if config.Certificate != "" && config.PrivateKey != "" {
cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path)
certLoader, err := ca.NewTLSKeyPairLoader(config.Certificate, config.PrivateKey)
if err != nil {
return nil, err
}
tlsConfig.Certificates = []tls.Certificate{cert}
tlsConfig.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
return certLoader()
}
if config.EchKey != "" {
err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path)
err = ech.LoadECHKey(config.EchKey, tlsConfig)
if err != nil {
return nil, err
}
@@ -91,14 +93,14 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A
}
}
if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert {
pool, err := ca.LoadCertificates(config.ClientAuthCert, C.Path)
pool, err := ca.LoadCertificates(config.ClientAuthCert)
if err != nil {
return nil, err
}
tlsConfig.ClientCAs = pool
}
if config.RealityConfig.PrivateKey != "" {
if tlsConfig.Certificates != nil {
if tlsConfig.GetCertificate != nil {
return nil, errors.New("certificate is unavailable in reality")
}
if tlsConfig.ClientAuth != tls.NoClientCert {
@@ -112,7 +114,7 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A
if realityBuilder != nil {
l = realityBuilder.NewListener(l)
} else if len(tlsConfig.Certificates) > 0 {
} else if tlsConfig.GetCertificate != nil {
l = tls.NewListener(l, tlsConfig)
}
+8 -6
View File
@@ -67,14 +67,16 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A
var realityBuilder *reality.Builder
if config.Certificate != "" && config.PrivateKey != "" {
cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path)
certLoader, err := ca.NewTLSKeyPairLoader(config.Certificate, config.PrivateKey)
if err != nil {
return nil, err
}
tlsConfig.Certificates = []tls.Certificate{cert}
tlsConfig.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
return certLoader()
}
if config.EchKey != "" {
err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path)
err = ech.LoadECHKey(config.EchKey, tlsConfig)
if err != nil {
return nil, err
}
@@ -87,14 +89,14 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A
}
}
if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert {
pool, err := ca.LoadCertificates(config.ClientAuthCert, C.Path)
pool, err := ca.LoadCertificates(config.ClientAuthCert)
if err != nil {
return nil, err
}
tlsConfig.ClientCAs = pool
}
if config.RealityConfig.PrivateKey != "" {
if tlsConfig.Certificates != nil {
if tlsConfig.GetCertificate != nil {
return nil, errors.New("certificate is unavailable in reality")
}
if tlsConfig.ClientAuth != tls.NoClientCert {
@@ -108,7 +110,7 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A
if realityBuilder != nil {
l = realityBuilder.NewListener(l)
} else if len(tlsConfig.Certificates) > 0 {
} else if tlsConfig.GetCertificate != nil {
l = tls.NewListener(l, tlsConfig)
}
+9 -7
View File
@@ -56,15 +56,17 @@ func New(config LC.Hysteria2Server, tunnel C.Tunnel, additions ...inbound.Additi
sl = &Listener{false, config, nil, nil}
cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path)
if err != nil {
return nil, err
}
tlsConfig := &tls.Config{
Time: ntp.Now,
MinVersion: tls.VersionTLS13,
}
tlsConfig.Certificates = []tls.Certificate{cert}
certLoader, err := ca.NewTLSKeyPairLoader(config.Certificate, config.PrivateKey)
if err != nil {
return nil, err
}
tlsConfig.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
return certLoader()
}
tlsConfig.ClientAuth = ca.ClientAuthTypeFromString(config.ClientAuthType)
if len(config.ClientAuthCert) > 0 {
if tlsConfig.ClientAuth == tls.NoClientCert {
@@ -72,7 +74,7 @@ func New(config LC.Hysteria2Server, tunnel C.Tunnel, additions ...inbound.Additi
}
}
if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert {
pool, err := ca.LoadCertificates(config.ClientAuthCert, C.Path)
pool, err := ca.LoadCertificates(config.ClientAuthCert)
if err != nil {
return nil, err
}
@@ -80,7 +82,7 @@ func New(config LC.Hysteria2Server, tunnel C.Tunnel, additions ...inbound.Additi
}
if config.EchKey != "" {
err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path)
err = ech.LoadECHKey(config.EchKey, tlsConfig)
if err != nil {
return nil, err
}
+8 -6
View File
@@ -81,14 +81,16 @@ func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition)
var httpServer http.Server
if config.Certificate != "" && config.PrivateKey != "" {
cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path)
certLoader, err := ca.NewTLSKeyPairLoader(config.Certificate, config.PrivateKey)
if err != nil {
return nil, err
}
tlsConfig.Certificates = []tls.Certificate{cert}
tlsConfig.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
return certLoader()
}
if config.EchKey != "" {
err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path)
err = ech.LoadECHKey(config.EchKey, tlsConfig)
if err != nil {
return nil, err
}
@@ -101,14 +103,14 @@ func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition)
}
}
if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert {
pool, err := ca.LoadCertificates(config.ClientAuthCert, C.Path)
pool, err := ca.LoadCertificates(config.ClientAuthCert)
if err != nil {
return nil, err
}
tlsConfig.ClientCAs = pool
}
if config.RealityConfig.PrivateKey != "" {
if tlsConfig.Certificates != nil {
if tlsConfig.GetCertificate != nil {
return nil, errors.New("certificate is unavailable in reality")
}
if tlsConfig.ClientAuth != tls.NoClientCert {
@@ -153,7 +155,7 @@ func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition)
}
if realityBuilder != nil {
l = realityBuilder.NewListener(l)
} else if len(tlsConfig.Certificates) > 0 {
} else if tlsConfig.GetCertificate != nil {
l = tls.NewListener(l, tlsConfig)
} else if sl.decryption == nil {
return nil, errors.New("disallow using Vless without any certificates/reality/decryption config")
+8 -6
View File
@@ -81,14 +81,16 @@ func New(config LC.VmessServer, tunnel C.Tunnel, additions ...inbound.Addition)
var httpServer http.Server
if config.Certificate != "" && config.PrivateKey != "" {
cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path)
certLoader, err := ca.NewTLSKeyPairLoader(config.Certificate, config.PrivateKey)
if err != nil {
return nil, err
}
tlsConfig.Certificates = []tls.Certificate{cert}
tlsConfig.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
return certLoader()
}
if config.EchKey != "" {
err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path)
err = ech.LoadECHKey(config.EchKey, tlsConfig)
if err != nil {
return nil, err
}
@@ -101,14 +103,14 @@ func New(config LC.VmessServer, tunnel C.Tunnel, additions ...inbound.Addition)
}
}
if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert {
pool, err := ca.LoadCertificates(config.ClientAuthCert, C.Path)
pool, err := ca.LoadCertificates(config.ClientAuthCert)
if err != nil {
return nil, err
}
tlsConfig.ClientCAs = pool
}
if config.RealityConfig.PrivateKey != "" {
if tlsConfig.Certificates != nil {
if tlsConfig.GetCertificate != nil {
return nil, errors.New("certificate is unavailable in reality")
}
if tlsConfig.ClientAuth != tls.NoClientCert {
@@ -153,7 +155,7 @@ func New(config LC.VmessServer, tunnel C.Tunnel, additions ...inbound.Addition)
}
if realityBuilder != nil {
l = realityBuilder.NewListener(l)
} else if len(tlsConfig.Certificates) > 0 {
} else if tlsConfig.GetCertificate != nil {
l = tls.NewListener(l, tlsConfig)
}
sl.listeners = append(sl.listeners, l)
+8 -6
View File
@@ -66,14 +66,16 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A
var realityBuilder *reality.Builder
if config.Certificate != "" && config.PrivateKey != "" {
cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path)
certLoader, err := ca.NewTLSKeyPairLoader(config.Certificate, config.PrivateKey)
if err != nil {
return nil, err
}
tlsConfig.Certificates = []tls.Certificate{cert}
tlsConfig.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
return certLoader()
}
if config.EchKey != "" {
err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path)
err = ech.LoadECHKey(config.EchKey, tlsConfig)
if err != nil {
return nil, err
}
@@ -86,14 +88,14 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A
}
}
if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert {
pool, err := ca.LoadCertificates(config.ClientAuthCert, C.Path)
pool, err := ca.LoadCertificates(config.ClientAuthCert)
if err != nil {
return nil, err
}
tlsConfig.ClientCAs = pool
}
if config.RealityConfig.PrivateKey != "" {
if tlsConfig.Certificates != nil {
if tlsConfig.GetCertificate != nil {
return nil, errors.New("certificate is unavailable in reality")
}
if tlsConfig.ClientAuth != tls.NoClientCert {
@@ -107,7 +109,7 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A
if realityBuilder != nil {
l = realityBuilder.NewListener(l)
} else if len(tlsConfig.Certificates) > 0 {
} else if tlsConfig.GetCertificate != nil {
l = tls.NewListener(l, tlsConfig)
}
+8 -6
View File
@@ -76,14 +76,16 @@ func New(config LC.TrojanServer, tunnel C.Tunnel, additions ...inbound.Addition)
var httpServer http.Server
if config.Certificate != "" && config.PrivateKey != "" {
cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path)
certLoader, err := ca.NewTLSKeyPairLoader(config.Certificate, config.PrivateKey)
if err != nil {
return nil, err
}
tlsConfig.Certificates = []tls.Certificate{cert}
tlsConfig.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
return certLoader()
}
if config.EchKey != "" {
err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path)
err = ech.LoadECHKey(config.EchKey, tlsConfig)
if err != nil {
return nil, err
}
@@ -96,14 +98,14 @@ func New(config LC.TrojanServer, tunnel C.Tunnel, additions ...inbound.Addition)
}
}
if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert {
pool, err := ca.LoadCertificates(config.ClientAuthCert, C.Path)
pool, err := ca.LoadCertificates(config.ClientAuthCert)
if err != nil {
return nil, err
}
tlsConfig.ClientCAs = pool
}
if config.RealityConfig.PrivateKey != "" {
if tlsConfig.Certificates != nil {
if tlsConfig.GetCertificate != nil {
return nil, errors.New("certificate is unavailable in reality")
}
if tlsConfig.ClientAuth != tls.NoClientCert {
@@ -148,7 +150,7 @@ func New(config LC.TrojanServer, tunnel C.Tunnel, additions ...inbound.Addition)
}
if realityBuilder != nil {
l = realityBuilder.NewListener(l)
} else if len(tlsConfig.Certificates) > 0 {
} else if tlsConfig.GetCertificate != nil {
l = tls.NewListener(l, tlsConfig)
} else if !config.TrojanSSOption.Enabled {
return nil, errors.New("disallow using Trojan without both certificates/reality/ss config")
+9 -7
View File
@@ -49,15 +49,17 @@ func New(config LC.TuicServer, tunnel C.Tunnel, additions ...inbound.Addition) (
return nil, err
}
cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path)
if err != nil {
return nil, err
}
tlsConfig := &tls.Config{
Time: ntp.Now,
MinVersion: tls.VersionTLS13,
}
tlsConfig.Certificates = []tls.Certificate{cert}
certLoader, err := ca.NewTLSKeyPairLoader(config.Certificate, config.PrivateKey)
if err != nil {
return nil, err
}
tlsConfig.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
return certLoader()
}
tlsConfig.ClientAuth = ca.ClientAuthTypeFromString(config.ClientAuthType)
if len(config.ClientAuthCert) > 0 {
if tlsConfig.ClientAuth == tls.NoClientCert {
@@ -65,7 +67,7 @@ func New(config LC.TuicServer, tunnel C.Tunnel, additions ...inbound.Addition) (
}
}
if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert {
pool, err := ca.LoadCertificates(config.ClientAuthCert, C.Path)
pool, err := ca.LoadCertificates(config.ClientAuthCert)
if err != nil {
return nil, err
}
@@ -73,7 +75,7 @@ func New(config LC.TuicServer, tunnel C.Tunnel, additions ...inbound.Addition) (
}
if config.EchKey != "" {
err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path)
err = ech.LoadECHKey(config.EchKey, tlsConfig)
if err != nil {
return nil, err
}
+2 -2
View File
@@ -27,8 +27,8 @@ jobs:
- uses: actions/checkout@v5
- name: Rust
run: |
rustup toolchain install stable --profile minimal --no-self-update
rustup default stable
rustup toolchain install nightly --profile minimal --no-self-update
rustup default nightly
rustup component add clippy rustfmt
rustc --version
cargo --version
+3 -3
View File
@@ -49,10 +49,10 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v5
- name: Install Rust stable
- name: Install Rust nightly
run: |
rustup install stable --profile minimal --no-self-update
rustup default stable
rustup install nightly --profile minimal --no-self-update
rustup default nightly
- name: Setup Cargo binstall
if: ${{ inputs.arch != 'x86_64' }}
+3 -3
View File
@@ -51,10 +51,10 @@ jobs:
with:
xcode-version: 16
- name: install Rust stable
- name: install Rust nightly
run: |
rustup install stable --profile minimal --no-self-update
rustup default stable
rustup install nightly --profile minimal --no-self-update
rustup default nightly
- name: Install Rust intel target
if: ${{ inputs.aarch64 == false }}
@@ -75,10 +75,10 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v5
- name: Install Rust stable
- name: Install Rust nightly
run: |
rustup install stable --profile minimal --no-self-update
rustup default stable
rustup install nightly --profile minimal --no-self-update
rustup default nightly
- name: Setup Rust target
if: ${{ inputs.arch != 'x86_64' }}
+4 -4
View File
@@ -13,15 +13,15 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v5
- name: install Rust stable
- name: install Rust nightly
run: |
rustup install stable --profile minimal --no-self-update
rustup default stable
rustup install nightly --profile minimal --no-self-update
rustup default nightly
- uses: Swatinem/rust-cache@v2
with:
workspaces: './backend/'
prefix-key: 'rust-stable'
prefix-key: 'rust-nightly'
key: 'macos-13'
shared-key: 'release'
- uses: maxim-lobanov/setup-xcode@v1
+15 -1
View File
@@ -1,5 +1,19 @@
export default {
'*.{js,cjs,.mjs,jsx}': ['prettier --write', 'eslint --cache --fix'],
'*.{js,cjs,.mjs,jsx}': (filenames) => {
const configFiles = [
'eslint.config.js',
'.lintstagedrc.js',
'commitlint.config.js',
]
const filtered = filenames.filter(
(file) => !configFiles.some((config) => file.endsWith(config)),
)
if (filtered.length === 0) return []
return [
`prettier --write ${filtered.join(' ')}`,
`eslint --cache --fix ${filtered.join(' ')}`,
]
},
'scripts/**/*.{ts,tsx}': [
'prettier --write',
'eslint --cache --fix',
+1
View File
@@ -8,4 +8,5 @@ pnpm-lock.yaml
*.wxs
frontend/nyanpasu/src/route-tree.gen.ts
frontend/nyanpasu/auto-imports.d.ts
frontend/nyanpasu/src/paraglide/
backend/tauri/gen/schemas/
+2 -2
View File
@@ -4457,7 +4457,7 @@ checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408"
[[package]]
name = "include-compress-bytes"
version = "0.1.0"
source = "git+https://github.com/libnyanpasu/include-compress-bytes?rev=250a12c#250a12c13a8fed1cd2c0476e01c3dd89555840d2"
source = "git+https://github.com/libnyanpasu/include-compress-bytes?rev=4e4f25b#4e4f25b794ed0b9a323a893bcdaf22f5024ab58a"
dependencies = [
"brotli",
"cargo-emit 0.2.1",
@@ -4470,7 +4470,7 @@ dependencies = [
[[package]]
name = "include_url_macro"
version = "0.1.0"
source = "git+https://github.com/libnyanpasu/include_url_macro?rev=b0b88e5#b0b88e5dd0685e607c6d965420fc2a035b4a0efe"
source = "git+https://github.com/libnyanpasu/include_url_macro?rev=fbe47bd#fbe47bd71047892a218e3166a2a1bac474488218"
dependencies = [
"brotli",
"bytes",
+2 -2
View File
@@ -24,8 +24,8 @@ tracing = "0.1"
url = "2"
log = "0.4"
anyhow = "1.0"
include_url_macro = { git = "https://github.com/libnyanpasu/include_url_macro", rev = "b0b88e5" }
include-compress-bytes = { git = "https://github.com/libnyanpasu/include-compress-bytes", rev = "250a12c" }
include_url_macro = { git = "https://github.com/libnyanpasu/include_url_macro", rev = "fbe47bd" }
include-compress-bytes = { git = "https://github.com/libnyanpasu/include-compress-bytes", rev = "4e4f25b" }
phf = { version = "0.13.1", features = ["macros"] }
# for cacheing
+27 -2
View File
@@ -31,6 +31,10 @@ const ignores = [
'dist/',
'backend/',
'backend/**/target',
'scripts/deno/**',
'eslint.config.js',
'.lintstagedrc.js',
'commitlint.config.js',
]
export default tseslint.config(
@@ -80,7 +84,12 @@ export default tseslint.config(
{
files: ['**/*.{ts,tsx,mtsx}'],
extends: [...tseslint.configs.recommended],
ignores: [...ignores, '**/vite.config.ts', '**/tailwind.config.ts'],
ignores: [
...ignores,
'frontend/nyanpasu/vite.config.ts',
'frontend/nyanpasu/tailwind.config.ts',
'frontend/ui/vite.config.ts',
],
rules: {
'@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/no-explicit-any': 'warn',
@@ -93,7 +102,10 @@ export default tseslint.config(
},
},
{
files: ['**/vite.config.ts', '**/tailwind.config.ts'],
files: [
'frontend/nyanpasu/vite.config.ts',
'frontend/nyanpasu/tailwind.config.ts',
],
extends: [...tseslint.configs.recommended],
rules: {
'@typescript-eslint/no-unused-vars': 'warn',
@@ -105,6 +117,19 @@ export default tseslint.config(
},
},
},
{
files: ['frontend/ui/vite.config.ts'],
extends: [...tseslint.configs.recommended],
rules: {
'@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/no-explicit-any': 'warn',
},
languageOptions: {
parserOptions: {
project: './frontend/ui/tsconfig.json',
},
},
},
{
files: ['**/*.{jsx,mjsx,tsx,mtsx}'],
languageOptions: {
@@ -17,5 +17,18 @@
"settings_system_proxy_proxy_bypass_label": "System Proxy Bypass",
"settings_system_proxy_current_system_proxy_label": "Current System Proxy",
"settings_user_interface_title": "User Interface",
"unit_seconds": "s"
"settings_user_interface_language_label": "Language",
"settings_user_interface_theme_mode_label": "Theme Mode",
"settings_user_interface_theme_mode_light": "Light",
"settings_user_interface_theme_mode_dark": "Dark",
"settings_user_interface_theme_mode_system": "System",
"settings_user_interface_theme_color_label": "Theme Color",
"settings_clash_settings_title": "Clash Settings",
"settings_clash_settings_allow_lan_label": "Allow LAN",
"settings_clash_settings_ipv6_label": "Enable IPv6",
"settings_clash_settings_tun_stack_label": "TUN Stack",
"settings_clash_settings_log_level_label": "Log Level",
"unit_seconds": "s",
"common_submit": "Submit",
"common_cancel": "Cancel"
}
@@ -17,5 +17,18 @@
"settings_system_proxy_proxy_bypass_label": "Обход прокси",
"settings_system_proxy_current_system_proxy_label": "Текущий системный прокси",
"settings_user_interface_title": "Интерфейс пользователя",
"unit_seconds": "секунды"
"settings_user_interface_language_label": "Язык",
"settings_user_interface_theme_mode_label": "Режим темы",
"settings_user_interface_theme_mode_light": "Светлый",
"settings_user_interface_theme_mode_dark": "Темный",
"settings_user_interface_theme_mode_system": "Системный",
"settings_user_interface_theme_color_label": "Цвет темы",
"settings_clash_settings_title": "Настройки Clash",
"settings_clash_settings_allow_lan_label": "Разрешить LAN",
"settings_clash_settings_ipv6_label": "Включить IPv6",
"settings_clash_settings_tun_stack_label": "TUN Stack",
"settings_clash_settings_log_level_label": "Уровень журнала",
"unit_seconds": "секунды",
"common_submit": "Отправить",
"common_cancel": "Отменить"
}
@@ -17,5 +17,18 @@
"settings_system_proxy_proxy_bypass_label": "系统代理绕过",
"settings_system_proxy_current_system_proxy_label": "当前系统代理",
"settings_user_interface_title": "用户界面",
"unit_seconds": "秒"
"settings_user_interface_language_label": "语言",
"settings_user_interface_theme_mode_label": "主题模式",
"settings_user_interface_theme_mode_light": "浅色",
"settings_user_interface_theme_mode_dark": "深色",
"settings_user_interface_theme_mode_system": "跟随系统",
"settings_user_interface_theme_color_label": "主题颜色",
"settings_clash_settings_title": "Clash 设置",
"settings_clash_settings_allow_lan_label": "允许局域网连接",
"settings_clash_settings_ipv6_label": "启用 IPv6",
"settings_clash_settings_tun_stack_label": "TUN 堆栈",
"settings_clash_settings_log_level_label": "日志级别",
"unit_seconds": "秒",
"common_submit": "提交",
"common_cancel": "取消"
}
@@ -17,5 +17,18 @@
"settings_system_proxy_proxy_bypass_label": "系統代理繞過",
"settings_system_proxy_current_system_proxy_label": "當前系統代理",
"settings_user_interface_title": "使用者介面",
"unit_seconds": "秒"
"settings_user_interface_language_label": "語言",
"settings_user_interface_theme_mode_label": "主題模式",
"settings_user_interface_theme_mode_light": "淺色",
"settings_user_interface_theme_mode_dark": "深色",
"settings_user_interface_theme_mode_system": "跟隨系統",
"settings_user_interface_theme_color_label": "主題顏色",
"settings_clash_settings_title": "Clash 設置",
"settings_clash_settings_allow_lan_label": "允許區域網路連線",
"settings_clash_settings_ipv6_label": "啟用 IPv6",
"settings_clash_settings_tun_stack_label": "TUN 堆棧",
"settings_clash_settings_log_level_label": "日誌級別",
"unit_seconds": "秒",
"common_submit": "提交",
"common_cancel": "取消"
}
@@ -23,14 +23,18 @@
"@mui/x-date-pickers": "8.17.0",
"@nyanpasu/interface": "workspace:^",
"@nyanpasu/ui": "workspace:^",
"@radix-ui/react-dropdown-menu": "2.1.16",
"@radix-ui/react-scroll-area": "1.2.10",
"@radix-ui/react-select": "2.2.6",
"@radix-ui/react-slot": "1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-switch": "1.2.6",
"@radix-ui/react-use-controllable-state": "1.2.2",
"@tailwindcss/postcss": "4.1.17",
"@tanstack/router-zod-adapter": "1.81.5",
"@tauri-apps/api": "2.8.0",
"@types/json-schema": "7.0.15",
"@uidotdev/usehooks": "2.4.1",
"@uiw/react-color": "2.9.2",
"ahooks": "3.9.6",
"allotment": "1.20.4",
"class-variance-authority": "0.7.1",
@@ -93,7 +97,7 @@
"shiki": "2.5.0",
"unplugin-auto-import": "20.3.0",
"unplugin-icons": "22.5.0",
"validator": "13.15.23",
"validator": "13.15.26",
"vite": "7.2.4",
"vite-plugin-html": "3.2.2",
"vite-plugin-sass-dts": "1.3.34",
@@ -0,0 +1,44 @@
import { createContext, PropsWithChildren, useContext, useEffect } from 'react'
import { useLockFn } from '@/hooks/use-lock-fn'
import { getLocale, Locale, setLocale } from '@/paraglide/runtime'
import { useSetting } from '@nyanpasu/interface'
const LanguageContext = createContext<{
language?: Locale
setLanguage: (value: Locale) => Promise<void>
} | null>(null)
export const useLanguage = () => {
const context = useContext(LanguageContext)
if (!context) {
throw new Error('useLanguage must be used within a LanguageProvider')
}
return context
}
export const LanguageProvider = ({ children }: PropsWithChildren) => {
const language = useSetting('language')
const setLanguage = useLockFn(async (value: Locale) => {
await language.upsert(value)
})
useEffect(() => {
if (language.value && language.value !== getLocale()) {
setLocale(language.value as Locale)
}
}, [language.value])
return (
<LanguageContext.Provider
value={{
language: language.value as Locale,
setLanguage,
}}
>
{children}
</LanguageContext.Provider>
)
}
@@ -14,10 +14,19 @@ import {
themeFromSourceColor,
} from '@material/material-color-utilities'
import { useSetting } from '@nyanpasu/interface'
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import { useLocalStorage } from '@uidotdev/usehooks'
const appWindow = getCurrentWebviewWindow()
export const DEFAULT_COLOR = '#1867C0'
export enum ThemeMode {
LIGHT = 'light',
DARK = 'dark',
SYSTEM = 'system',
}
const CUSTOM_THEME_KEY = 'custom-theme' as const
const THEME_PALETTE_KEY = 'theme-palette-v1' as const
@@ -52,11 +61,29 @@ const generateThemeCssVars = ({ schemes }: Theme) => {
return lightCssVars + darkCssVars
}
const changeHtmlThemeMode = (mode: Omit<ThemeMode, 'system'>) => {
const root = document.documentElement
if (mode === ThemeMode.DARK) {
root.classList.add(ThemeMode.DARK)
} else {
root.classList.remove(ThemeMode.DARK)
}
if (mode === ThemeMode.LIGHT) {
root.classList.add(ThemeMode.LIGHT)
} else {
root.classList.remove(ThemeMode.LIGHT)
}
}
const ThemeContext = createContext<{
themePalette: Theme
themeCssVars: string
themeColor: string
setTheme: (color: string) => void
setThemeColor: (color: string) => Promise<void>
themeMode: ThemeMode
setThemeMode: (mode: ThemeMode) => Promise<void>
} | null>(null)
export function useExperimentalThemeContext() {
@@ -72,13 +99,15 @@ export function useExperimentalThemeContext() {
}
export function ExperimentalThemeProvider({ children }: PropsWithChildren) {
const { value: themeColor } = useSetting('theme_color')
const themeMode = useSetting('theme_mode')
const themeColor = useSetting('theme_color')
const [cachedThemePalette, setCachedThemePalette] = useLocalStorage<Theme>(
THEME_PALETTE_KEY,
themeFromSourceColor(
// use default color if theme color is not set
argbFromHex(themeColor || DEFAULT_COLOR),
argbFromHex(themeColor.value || DEFAULT_COLOR),
),
)
@@ -93,10 +122,12 @@ export function ExperimentalThemeProvider({ children }: PropsWithChildren) {
insertStyle(CUSTOM_THEME_KEY, cachedThemeCssVars)
}, [cachedThemeCssVars])
const setTheme = useCallback(
(color: string) => {
if (color === themeColor) {
const setThemeColor = useCallback(
async (color: string) => {
if (color === themeColor.value) {
return
} else {
await themeColor.upsert(color)
}
const materialColor = themeFromSourceColor(
@@ -121,13 +152,42 @@ export function ExperimentalThemeProvider({ children }: PropsWithChildren) {
],
)
// listen to theme changed event and change html theme mode
useEffect(() => {
const unlisten = appWindow.onThemeChanged((e) => {
if (themeMode.value === ThemeMode.SYSTEM) {
changeHtmlThemeMode(e.payload)
}
})
return () => {
unlisten.then((fn) => fn())
}
}, [themeMode.value])
const setThemeMode = useCallback(
async (mode: ThemeMode) => {
// if theme mode is not system, change html theme mode
if (mode !== ThemeMode.SYSTEM) {
changeHtmlThemeMode(mode)
}
if (mode !== themeMode.value) {
await themeMode.upsert(mode)
}
},
[themeMode],
)
return (
<ThemeContext.Provider
value={{
themePalette: cachedThemePalette,
themeCssVars: cachedThemeCssVars,
themeColor: themeColor || DEFAULT_COLOR,
setTheme,
themeColor: themeColor.value || DEFAULT_COLOR,
setThemeColor,
themeMode: themeMode.value as ThemeMode,
setThemeMode,
}}
>
{children}
@@ -4,11 +4,13 @@ import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { isHexColor } from 'validator'
import { atomIsDrawerOnlyIcon } from '@/store'
import { setEnabledExperimentalRouter } from '@/utils/experimental'
import { languageOptions } from '@/utils/language'
import Done from '@mui/icons-material/Done'
import { Button, List, ListItem, ListItemText } from '@mui/material'
import { useSetting } from '@nyanpasu/interface'
import { BaseCard, Expand, MenuItem, SwitchItem } from '@nyanpasu/ui'
import { useNavigate } from '@tanstack/react-router'
import { DEFAULT_COLOR } from '../layout/use-custom-theme'
const commonSx = {
@@ -106,6 +108,25 @@ const ThemeColor = () => {
)
}
const ExperimentalSwitch = () => {
const navigate = useNavigate()
const handleClick = () => {
setEnabledExperimentalRouter(true)
navigate({ to: '/experimental/dashboard' })
}
return (
<ListItem sx={{ pl: 0, pr: 0 }}>
<ListItemText primary="Switch to Experimental UI" />
<Button variant="contained" onClick={handleClick}>
Continue
</Button>
</ListItem>
)
}
export const SettingNyanpasuUI = () => {
const { t } = useTranslation()
@@ -125,6 +146,8 @@ export const SettingNyanpasuUI = () => {
checked={onlyIcon}
onChange={() => setOnlyIcon(!onlyIcon)}
/>
<ExperimentalSwitch />
</List>
</BaseCard>
)
@@ -21,7 +21,7 @@ const ProxyButton = ({
className={cn(
'group h-16 rounded-3xl font-bold',
'flex items-center justify-between gap-2',
'data-[active=false]:bg-black',
'data-[active=false]:bg-white dark:data-[active=false]:bg-black',
className,
)}
data-active={String(Boolean(isActive))}
@@ -0,0 +1,259 @@
import Check from '~icons/material-symbols/check-rounded'
import RadioChecked from '~icons/material-symbols/radio-button-checked'
import Radio from '~icons/material-symbols/radio-button-unchecked'
import { AnimatePresence, motion } from 'framer-motion'
import { ComponentProps, createContext, useContext } from 'react'
import { cn } from '@nyanpasu/ui'
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
import { useControllableState } from '@radix-ui/react-use-controllable-state'
const DropdownMenuContext = createContext<{
open: boolean
} | null>(null)
const useDropdownMenuContext = () => {
const context = useContext(DropdownMenuContext)
if (context === null) {
throw new Error(
'DropdownMenu compound components cannot be rendered outside the DropdownMenu component',
)
}
return context
}
export const DropdownMenu = ({
open: inputOpen,
defaultOpen,
onOpenChange,
...props
}: ComponentProps<typeof DropdownMenuPrimitive.Root>) => {
const [open, setOpen] = useControllableState({
prop: inputOpen,
defaultProp: defaultOpen ?? false,
onChange: onOpenChange,
})
return (
<DropdownMenuContext.Provider value={{ open }}>
<DropdownMenuPrimitive.Root
{...props}
open={open}
onOpenChange={setOpen}
/>
</DropdownMenuContext.Provider>
)
}
export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
export const DropdownMenuGroup = DropdownMenuPrimitive.Group
export const DropdownMenuPortal = DropdownMenuPrimitive.Portal
export const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroupContext = createContext<{
value: string | null
}>({ value: null })
const useDropdownMenuRadioGroupContext = () => {
const context = useContext(DropdownMenuRadioGroupContext)
if (context === undefined) {
throw new Error(
'DropdownMenuRadioGroup compound components cannot be rendered outside the DropdownMenuRadioGroup component',
)
}
return context
}
export const DropdownMenuRadioGroup = ({
value: inputValue,
defaultValue,
onValueChange,
...props
}: ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) => {
const [value, setValue] = useControllableState({
prop: inputValue,
defaultProp: String(defaultValue),
onChange: onValueChange,
})
return (
<DropdownMenuRadioGroupContext.Provider value={{ value }}>
<DropdownMenuPrimitive.RadioGroup
{...props}
value={value}
onValueChange={setValue}
/>
</DropdownMenuRadioGroupContext.Provider>
)
}
export const DropdownMenuSubTrigger = DropdownMenuPrimitive.SubTrigger
export const DropdownMenuSubContent = DropdownMenuPrimitive.SubContent
export const DropdownMenuContent = ({
children,
className,
...props
}: ComponentProps<typeof DropdownMenuPrimitive.Content>) => {
const { open } = useDropdownMenuContext()
return (
<AnimatePresence initial={false}>
{open && (
<DropdownMenuPrimitive.Portal forceMount>
<DropdownMenuPrimitive.Content {...props} asChild>
<motion.div
className={cn(
'shadow-container relative z-50 w-full overflow-auto rounded',
'dark:text-on-surface',
'bg-inverse-on-surface dark:bg-surface',
className,
)}
style={{
maxHeight: 'var(--radix-popper-available-height)',
}}
initial={{
opacity: 0,
scaleY: 0.9,
transformOrigin: 'top',
}}
animate={{
opacity: 1,
scaleY: 1,
transformOrigin: 'top',
}}
exit={{
opacity: 0,
scaleY: 0.9,
transformOrigin: 'top',
}}
transition={{
type: 'spring',
bounce: 0,
duration: 0.35,
}}
>
{children}
</motion.div>
</DropdownMenuPrimitive.Content>
</DropdownMenuPrimitive.Portal>
)}
</AnimatePresence>
)
}
export const DropdownMenuItem = ({
className,
...props
}: ComponentProps<typeof DropdownMenuPrimitive.Item>) => {
return (
<DropdownMenuPrimitive.Item
className={cn(
'flex h-12 cursor-default items-center justify-between gap-2 p-4 outline-hidden',
'cursor-pointer',
'hover:bg-surface-variant',
'dark:hover:bg-surface-variant',
className,
)}
{...props}
/>
)
}
export const DropdownMenuCheckboxItem = ({
children,
className,
...props
}: ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) => {
return (
<DropdownMenuPrimitive.CheckboxItem
className={cn(
'flex h-12 cursor-default items-center justify-between gap-2 p-4 outline-hidden',
'cursor-pointer',
'hover:bg-surface-variant',
'dark:hover:bg-surface-variant',
'data-[state=checked]:bg-primary-container dark:data-[state=checked]:bg-on-primary',
className,
)}
{...props}
>
{children}
<DropdownMenuPrimitive.ItemIndicator>
<Check className="text-primary" />
</DropdownMenuPrimitive.ItemIndicator>
</DropdownMenuPrimitive.CheckboxItem>
)
}
export const DropdownMenuRadioItem = ({
value,
children,
className,
...props
}: ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) => {
const context = useDropdownMenuRadioGroupContext()
const selected = context.value === value
return (
<DropdownMenuPrimitive.RadioItem
className={cn(
'flex h-12 cursor-default items-center justify-between gap-2 p-4 outline-hidden',
'cursor-pointer',
'hover:bg-surface-variant',
'dark:hover:bg-surface-variant',
'data-[state=checked]:bg-primary-container dark:data-[state=checked]:bg-on-primary',
className,
)}
value={value}
{...props}
>
<DropdownMenuPrimitive.ItemIndicator>
<RadioChecked className="text-primary" />
</DropdownMenuPrimitive.ItemIndicator>
{!selected && (
<span>
<Radio className="text-outline-variant dark:text-outline" />
</span>
)}
<div className="flex-1">{children}</div>
</DropdownMenuPrimitive.RadioItem>
)
}
export const DropdownMenuLabel = ({
className,
...props
}: ComponentProps<typeof DropdownMenuPrimitive.Label>) => {
return (
<DropdownMenuPrimitive.Label
className={cn(
'flex h-12 cursor-default items-center justify-between gap-2 p-4 outline-hidden',
className,
)}
{...props}
/>
)
}
export const DropdownMenuSeparator = ({
className,
...props
}: ComponentProps<typeof DropdownMenuPrimitive.Separator>) => {
return (
<DropdownMenuPrimitive.Separator
className={cn('bg-outline-variant/50 h-px', className)}
{...props}
/>
)
}
@@ -0,0 +1,505 @@
import ArrowDropDown from '~icons/material-symbols/arrow-drop-down-rounded'
import Check from '~icons/material-symbols/check-rounded'
import { cva, type VariantProps } from 'class-variance-authority'
import { AnimatePresence, motion } from 'framer-motion'
import {
ComponentProps,
createContext,
useCallback,
useContext,
useEffect,
useState,
} from 'react'
import { chains } from '@/utils/chain'
import { cn } from '@nyanpasu/ui'
import * as SelectPrimitive from '@radix-ui/react-select'
import { useControllableState } from '@radix-ui/react-use-controllable-state'
export const selectTriggerVariants = cva(
[
'group relative box-border inline-flex w-full flex-auto items-baseline',
'cursor-pointer',
'px-4 py-4 outline-hidden',
// TODO: size variants, fix this
'flex items-center justify-between h-14',
'dark:text-on-surface',
],
{
variants: {
variant: {
filled: 'rounded-t bg-surface-variant dark:bg-on-surface-variant',
// outlined use selectValuePlaceholderFieldsetVariants
outlined: '',
},
},
defaultVariants: {
variant: 'filled',
},
},
)
export type SelectTriggerVariants = VariantProps<typeof selectTriggerVariants>
export const selectLineVariants = cva('', {
variants: {
variant: {
filled: [
'absolute inset-x-0 bottom-0 w-full border-b border-on-primary-container',
'transition-all duration-200',
// pseudo elements be overlay parent element, will not affect the box size
'after:absolute after:inset-x-0 after:bottom-0 after:z-10',
"after:scale-x-0 after:border-b-2 after:opacity-0 after:content-['']",
'after:transition-all after:duration-200',
'after:border-primary dark:after:border-on-primary-container',
// sync parent group state, state from radix-ui
'group-data-[state=open]:border-b-0',
'group-data-[state=open]:after:scale-x-100',
'group-data-[state=open]:after:opacity-100',
'peer-focus:border-b-0',
'peer-focus:after:scale-x-100',
'peer-focus:after:opacity-100',
],
// hidden line for outlined variant
outlined: 'hidden',
},
},
defaultVariants: {
variant: 'filled',
},
})
export type SelectLineVariants = VariantProps<typeof selectLineVariants>
export const selectValueVariants = cva('pointer-events-none', {
variants: {
variant: {
filled: '',
outlined: '',
},
haveValue: {
true: '',
false: '',
},
},
compoundVariants: [
{
variant: 'filled',
haveValue: true,
className: 'mt-3',
},
],
defaultVariants: {
variant: 'filled',
haveValue: false,
},
})
export type SelectValueVariants = VariantProps<typeof selectValueVariants>
export const selectValuePlaceholderVariants = cva(
[
'absolute',
'left-4 top-4',
'pointer-events-none',
'text-base select-none',
// TODO: only transition position, not text color
'transition-all duration-200',
],
{
variants: {
variant: {
filled: [
'group-data-[state=open]:top-2 group-data-[state=open]:dark:text-surface',
'group-data-[state=open]:text-xs group-data-[state=open]:text-primary',
],
outlined: [
'group-data-[state=open]:-top-2',
'group-data-[state=open]:text-sm',
'group-data-[state=open]:text-primary',
'dark:group-data-[state=open]:text-inverse-primary',
'dark:group-data-[state=closed]:text-on-primary-container',
],
},
focus: {
true: '',
false: '',
},
},
compoundVariants: [
{
variant: 'filled',
focus: true,
className: 'top-2 text-xs',
},
{
variant: 'outlined',
focus: true,
className: '-top-2 text-sm',
},
],
defaultVariants: {
variant: 'filled',
focus: false,
},
},
)
export type SelectValuePlaceholderVariants = VariantProps<
typeof selectValuePlaceholderVariants
>
export const selectValuePlaceholderFieldsetVariants = cva(
'pointer-events-none',
{
variants: {
variant: {
// only for outlined variant
filled: 'hidden',
outlined: [
'absolute inset-0 text-left',
'rounded transition-all duration-200',
// may open border width will be 1.5, idk
'group-data-[state=closed]:border',
'group-data-[state=open]:border-2',
'peer-not-focus:border',
'peer-focus:border-2',
// different material web border color, i think this looks better
'group-data-[state=closed]:border-outline-variant',
'group-data-[state=open]:border-primary',
'peer-not-focus:border-primary-container',
'peer-focus:border-primary',
// dark must be prefixed
'dark:group-data-[state=closed]:border-outline-variant',
'dark:group-data-[state=open]:border-primary-container',
'dark:peer-not-focus:border-outline-variant',
'dark:peer-focus:border-primary-container',
],
},
},
defaultVariants: {
variant: 'filled',
},
},
)
export type SelectValuePlaceholderFieldsetVariants = VariantProps<
typeof selectValuePlaceholderFieldsetVariants
>
export const selectValuePlaceholderLegendVariants = cva('', {
variants: {
variant: {
// only for outlined variant
filled: 'hidden',
outlined: 'invisible ml-2 px-2 text-sm h-0',
},
haveValue: {
true: '',
false: '',
},
},
compoundVariants: [
{
variant: 'outlined',
haveValue: false,
className: 'group-data-[state=closed]:hidden group-not-focus:hidden',
},
],
defaultVariants: {
variant: 'filled',
haveValue: false,
},
})
export type SelectValuePlaceholderLegendVariants = VariantProps<
typeof selectValuePlaceholderLegendVariants
>
export const selectContentVariants = cva(
[
'relative w-full overflow-auto rounded shadow-container z-50',
'bg-inverse-on-surface dark:bg-surface',
'dark:text-on-surface',
],
{
variants: {
variant: {
filled: 'rounded-t-none',
outlined: '',
},
},
defaultVariants: {
variant: 'filled',
},
},
)
export type SelectContentVariants = VariantProps<typeof selectContentVariants>
type SelectContextType = {
haveValue?: boolean
open?: boolean
} & SelectTriggerVariants
const SelectContext = createContext<SelectContextType | null>(null)
const useSelectContext = () => {
const context = useContext(SelectContext)
if (!context) {
throw new Error('useSelectContext must be used within a SelectProvider')
}
return context
}
export const SelectLine = ({ className, ...props }: ComponentProps<'div'>) => {
const { variant } = useSelectContext()
return (
<div
className={cn(
selectLineVariants({
variant,
}),
className,
)}
{...props}
/>
)
}
export const Select = ({
onValueChange,
variant,
open: inputOpen,
defaultOpen,
onOpenChange,
...props
}: React.ComponentProps<typeof SelectPrimitive.Root> &
SelectTriggerVariants) => {
const [open, setOpen] = useControllableState({
prop: inputOpen,
defaultProp: defaultOpen ?? false,
onChange: onOpenChange,
})
const [haveValue, setHaveValue] = useState(
Boolean(props.value || props.defaultValue),
)
const handleOnChange = useCallback((value?: string) => {
setHaveValue(Boolean(value))
}, [])
useEffect(() => {
setHaveValue(Boolean(props.value || props.defaultValue))
}, [props.value, props.defaultValue])
return (
<SelectContext.Provider
value={{
open,
haveValue,
variant,
}}
>
<SelectPrimitive.Root
open={open}
onOpenChange={setOpen}
onValueChange={chains(handleOnChange, onValueChange)}
{...props}
/>
</SelectContext.Provider>
)
}
export type SelectProps = ComponentProps<typeof Select>
export const SelectValue = ({
className,
placeholder,
...props
}: ComponentProps<typeof SelectPrimitive.Value>) => {
const { haveValue, open, variant } = useSelectContext()
return (
<>
<div
className={cn(
selectValueVariants({
variant,
haveValue,
}),
className,
)}
>
<SelectPrimitive.Value {...props} />
</div>
<fieldset
className={cn(
selectValuePlaceholderFieldsetVariants({
variant,
}),
)}
>
<legend
className={cn(
selectValuePlaceholderLegendVariants({
variant,
haveValue: haveValue || open,
}),
)}
>
{placeholder}
</legend>
</fieldset>
<div
className={cn(
selectValuePlaceholderVariants({
variant,
focus: haveValue || open,
}),
)}
>
{placeholder}
</div>
</>
)
}
export const SelectGroup = (
props: ComponentProps<typeof SelectPrimitive.Group>,
) => {
return <SelectPrimitive.Group {...props} />
}
export const SelectLabel = ({
className,
...props
}: ComponentProps<typeof SelectPrimitive.Label>) => {
return (
<SelectPrimitive.Label
className={cn(
'flex h-12 cursor-default items-center justify-between gap-2 p-4 outline-hidden',
'text-primary dark:text-inverse-primary',
className,
)}
{...props}
/>
)
}
export const SelectTrigger = ({
className,
children,
...props
}: ComponentProps<typeof SelectPrimitive.Trigger>) => {
const { variant } = useSelectContext()
return (
<SelectPrimitive.Trigger
className={cn(
selectTriggerVariants({
variant,
}),
className,
)}
{...props}
>
{children}
<SelectLine />
<SelectIcon />
</SelectPrimitive.Trigger>
)
}
export const SelectIcon = ({
asChild,
children,
className,
...props
}: ComponentProps<typeof SelectPrimitive.Icon>) => {
return (
<SelectPrimitive.Icon
className={cn('absolute right-4', className)}
asChild
{...props}
>
{asChild ? children : <ArrowDropDown />}
</SelectPrimitive.Icon>
)
}
export const SelectContent = ({
className,
children,
...props
}: ComponentProps<typeof SelectPrimitive.Content>) => {
const { open, variant } = useSelectContext()
return (
<AnimatePresence initial={false}>
{open && (
<SelectPrimitive.Portal>
<SelectPrimitive.Content {...props} position="popper" asChild>
<motion.div
className={cn(
selectContentVariants({
variant,
}),
className,
)}
style={{
width: 'var(--radix-popper-anchor-width)',
maxHeight: 'var(--radix-popper-available-height)',
}}
initial={{ opacity: 0, scaleY: 0.9, transformOrigin: 'top' }}
animate={{ opacity: 1, scaleY: 1, transformOrigin: 'top' }}
exit={{ opacity: 0, scaleY: 0.9, transformOrigin: 'top' }}
transition={{
type: 'spring',
bounce: 0,
duration: 0.35,
}}
>
<SelectPrimitive.Viewport>{children}</SelectPrimitive.Viewport>
</motion.div>
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
)}
</AnimatePresence>
)
}
export const SelectItem = ({
className,
children,
...props
}: ComponentProps<typeof SelectPrimitive.Item>) => {
return (
<SelectPrimitive.Item
className={cn(
'flex h-12 cursor-default items-center justify-between gap-2 p-4 outline-hidden',
'cursor-pointer',
'hover:bg-surface-variant data-[state=checked]:bg-primary-container',
'dark:hover:bg-surface-variant dark:data-[state=checked]:bg-primary-container',
className,
)}
{...props}
>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
<SelectPrimitive.ItemIndicator>
<Check className="text-primary" />
</SelectPrimitive.ItemIndicator>
</SelectPrimitive.Item>
)
}
@@ -0,0 +1,43 @@
import { useMemo } from 'react'
import { Switch } from '@/components/ui/switch'
import { useLockFn } from '@/hooks/use-lock-fn'
import { m } from '@/paraglide/messages'
import { message } from '@/utils/notification'
import { useClashConfig } from '@nyanpasu/interface'
import { SettingsCard, SettingsCardContent } from '../../_modules/settings-card'
export default function AllowLanSwitch() {
const { query, upsert } = useClashConfig()
const value = useMemo(() => query.data?.['allow-lan'], [query.data])
const handleAllowLan = useLockFn(async (input: boolean) => {
try {
await upsert.mutateAsync({
'allow-lan': input,
})
} catch (error) {
message(`Activation Allow LAN failed!`, {
title: 'Error',
kind: 'error',
})
}
})
return (
<SettingsCard data-slot="allow-lan-card">
<SettingsCardContent
className="flex items-center justify-between px-3"
data-slot="allow-lan-card-content"
>
<div>{m.settings_clash_settings_allow_lan_label()}</div>
<Switch
checked={Boolean(value)}
onCheckedChange={handleAllowLan}
loading={upsert.isPending}
/>
</SettingsCardContent>
</SettingsCard>
)
}
@@ -0,0 +1,43 @@
import { useMemo } from 'react'
import { Switch } from '@/components/ui/switch'
import { useLockFn } from '@/hooks/use-lock-fn'
import { m } from '@/paraglide/messages'
import { message } from '@/utils/notification'
import { useClashConfig } from '@nyanpasu/interface'
import { SettingsCard, SettingsCardContent } from '../../_modules/settings-card'
export default function IPv6Switch() {
const { query, upsert } = useClashConfig()
const value = useMemo(() => query.data?.['ipv6'], [query.data])
const handleIPv6 = useLockFn(async (input: boolean) => {
try {
await upsert.mutateAsync({
ipv6: input,
})
} catch (error) {
message(`Activation IPv6 failed!`, {
title: 'Error',
kind: 'error',
})
}
})
return (
<SettingsCard data-slot="ipv6-card">
<SettingsCardContent
className="flex items-center justify-between px-3"
data-slot="ipv6-card-content"
>
<div>{m.settings_clash_settings_ipv6_label()}</div>
<Switch
checked={Boolean(value)}
onCheckedChange={handleIPv6}
loading={upsert.isPending}
/>
</SettingsCardContent>
</SettingsCard>
)
}
@@ -0,0 +1,68 @@
import { useCallback, useMemo } from 'react'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { m } from '@/paraglide/messages'
import { useClashConfig } from '@nyanpasu/interface'
import { SettingsCard, SettingsCardContent } from '../../_modules/settings-card'
const LOG_LEVEL_OPTIONS = {
debug: 'Debug',
info: 'Info',
warning: 'Warn',
error: 'Error',
silent: 'Silent',
} as const
export default function LogLevelSelector() {
const { query, upsert } = useClashConfig()
const value = useMemo(
() => query.data?.['log-level'] as keyof typeof LOG_LEVEL_OPTIONS,
[query.data],
)
const handleLogLevelChange = useCallback(
async (value: string) => {
await upsert.mutateAsync({
'log-level': value as string,
})
},
[upsert],
)
return (
<SettingsCard data-slot="log-level-selector-card">
<SettingsCardContent
className="px-2"
data-slot="log-level-selector-card-content"
>
<Select
variant="outlined"
value={value}
onValueChange={handleLogLevelChange}
>
<SelectTrigger>
<SelectValue
placeholder={m.settings_clash_settings_log_level_label()}
>
{value ? LOG_LEVEL_OPTIONS[value] : null}
</SelectValue>
</SelectTrigger>
<SelectContent>
{Object.entries(LOG_LEVEL_OPTIONS).map(([key, value]) => (
<SelectItem key={key} value={key}>
{value}
</SelectItem>
))}
</SelectContent>
</Select>
</SettingsCardContent>
</SettingsCard>
)
}
@@ -0,0 +1,101 @@
import { useCallback, useMemo } from 'react'
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { useCoreType } from '@/hooks/use-store'
import { m } from '@/paraglide/messages'
import { formatError } from '@/utils'
import { message } from '@/utils/notification'
import { TunStack, useRuntimeProfile, useSetting } from '@nyanpasu/interface'
import { SettingsCard, SettingsCardContent } from '../../_modules/settings-card'
export default function TunStackSelector() {
const [coreType] = useCoreType()
const tunStack = useSetting('tun_stack')
const enableTunMode = useSetting('enable_tun_mode')
const runtimeProfile = useRuntimeProfile()
const tunStackOptions = useMemo(() => {
const options: {
[key: string]: string
} = {
system: 'System',
gvisor: 'gVisor',
mixed: 'Mixed',
}
// clash not support mixed
if (coreType === 'clash') {
delete options.mixed
}
return options
}, [coreType])
const currentTunStack = useMemo(() => {
const stack = tunStack.value || 'gvisor'
return stack in tunStackOptions ? stack : 'gvisor'
}, [tunStackOptions, tunStack.value])
const handleTunStackChange = useCallback(
async (value: string) => {
try {
await tunStack.upsert(value as TunStack)
if (enableTunMode.value) {
// just to reload clash config
await enableTunMode.upsert(true)
}
// need manual mutate to refetch runtime profile
await runtimeProfile.refetch()
} catch (error) {
message(`Change Tun Stack failed ! \n Error: ${formatError(error)}`, {
title: 'Error',
kind: 'error',
})
}
},
[tunStack, enableTunMode, runtimeProfile],
)
return (
<SettingsCard data-slot="tun-stack-selector-card">
<SettingsCardContent
className="px-2"
data-slot="tun-stack-selector-card-content"
>
<Select
variant="outlined"
value={currentTunStack}
onValueChange={handleTunStackChange}
>
<SelectTrigger>
<SelectValue
placeholder={m.settings_clash_settings_tun_stack_label()}
>
{tunStackOptions[currentTunStack]}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectGroup>
{Object.entries(tunStackOptions).map(([key, value]) => (
<SelectItem key={key} value={key}>
{value}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</SettingsCardContent>
</SettingsCard>
)
}
@@ -1,4 +1,13 @@
import { m } from '@/paraglide/messages'
import { createFileRoute } from '@tanstack/react-router'
import {
SettingsTitle,
SettingsTitlePlaceholder,
} from '../_modules/settings-title'
import AllowLanSwitch from './_modules/allow-lan-switch'
import IPv6Switch from './_modules/ipv6-switch'
import LogLevelSelector from './_modules/log-level-selector'
import TunStackSelector from './_modules/tun-stack-selector'
export const Route = createFileRoute(
'/(experimental)/experimental/settings/clash-settings',
@@ -8,6 +17,17 @@ export const Route = createFileRoute(
function RouteComponent() {
return (
<div>Hello "/(experimental)/experimental/settings/clash-settings"!</div>
<>
<SettingsTitlePlaceholder />
<SettingsTitle>{m.settings_system_proxy_title()}</SettingsTitle>
<AllowLanSwitch />
<IPv6Switch />
<TunStackSelector />
<LogLevelSelector />
</>
)
}
@@ -0,0 +1,27 @@
import { Button } from '@/components/ui/button'
import { setEnabledExperimentalRouter } from '@/utils/experimental'
import { useNavigate } from '@tanstack/react-router'
import { SettingsCard, SettingsCardContent } from '../../_modules/settings-card'
export default function ExperimentalSwitch() {
const navigate = useNavigate()
const handleClick = () => {
setEnabledExperimentalRouter(false)
navigate({ to: '/' })
}
return (
<SettingsCard data-slot="experimental-switch-card">
<SettingsCardContent
className="flex items-center justify-between px-3"
data-slot="experimental-switch-card-content"
>
<div>Switch to Legacy UI</div>
<Button variant="flat" onClick={handleClick}>
Im sure, continue!
</Button>
</SettingsCardContent>
</SettingsCard>
)
}
@@ -0,0 +1,50 @@
import { useLanguage } from '@/components/providers/language-provider'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { m } from '@/paraglide/messages'
import { Locale, locales } from '@/paraglide/runtime'
import { SettingsCard, SettingsCardContent } from '../../_modules/settings-card'
export default function LanguageSelector() {
const { language, setLanguage } = useLanguage()
const handleLanguageChange = (value: string) => {
setLanguage(value as Locale)
}
return (
<SettingsCard data-slot="language-selector-card">
<SettingsCardContent
className="flex items-center justify-between px-3"
data-slot="language-selector-card-content"
>
<div>{m.settings_user_interface_language_label()}</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="flat">{m.language()}</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuRadioGroup
value={language}
onValueChange={handleLanguageChange}
>
{locales.map((value) => (
<DropdownMenuRadioItem key={value} value={value}>
{m.language(value, { locale: value })}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
</SettingsCardContent>
</SettingsCard>
)
}
@@ -0,0 +1,68 @@
import Check from '~icons/material-symbols/check-rounded'
import { useCallback, useState } from 'react'
import { useExperimentalThemeContext } from '@/components/providers/theme-provider'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { m } from '@/paraglide/messages'
import { Wheel } from '@uiw/react-color'
import { SettingsCard, SettingsCardContent } from '../../_modules/settings-card'
export default function ThemeColorConfig() {
const { themeColor, setThemeColor } = useExperimentalThemeContext()
const [open, setOpen] = useState(false)
const [cachedThemeColor, setCachedThemeColor] = useState(themeColor)
const handleSubmit = useCallback(async () => {
setOpen(false)
await setThemeColor(cachedThemeColor)
}, [cachedThemeColor, setThemeColor])
return (
<SettingsCard data-slot="theme-color-config-card">
<SettingsCardContent
className="flex items-center justify-between px-3"
data-slot="theme-color-config-card-content"
>
<div>{m.settings_user_interface_theme_color_label()}</div>
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild>
<Button className="flex items-center gap-2 px-4" variant="flat">
<span
className="size-4 rounded"
style={{ backgroundColor: themeColor }}
/>
<span>{themeColor}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="flex flex-col gap-4 rounded-2xl p-4">
<Wheel
data-slot="theme-color-config-colorful"
color={cachedThemeColor}
onChange={(color) => {
setCachedThemeColor(color.hex)
}}
/>
<Button
className="flex items-center justify-center gap-2"
variant="flat"
onClick={handleSubmit}
>
<Check className="size-5" />
<span>{m.common_submit()}</span>
</Button>
</DropdownMenuContent>
</DropdownMenu>
</SettingsCardContent>
</SettingsCard>
)
}
@@ -0,0 +1,58 @@
import {
ThemeMode,
useExperimentalThemeContext,
} from '@/components/providers/theme-provider'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { m } from '@/paraglide/messages'
import { SettingsCard, SettingsCardContent } from '../../_modules/settings-card'
export default function ThemeModeSelector() {
const { themeMode, setThemeMode } = useExperimentalThemeContext()
const handleThemeModeChange = (value: string) => {
setThemeMode(value as ThemeMode)
}
const messages = {
[ThemeMode.LIGHT]: m.settings_user_interface_theme_mode_light(),
[ThemeMode.DARK]: m.settings_user_interface_theme_mode_dark(),
[ThemeMode.SYSTEM]: m.settings_user_interface_theme_mode_system(),
} satisfies Record<ThemeMode, string>
return (
<SettingsCard data-slot="theme-mode-selection-card">
<SettingsCardContent
className="flex items-center justify-between px-3"
data-slot="theme-mode-selection-card-content"
>
<div>{m.settings_user_interface_theme_mode_label()}</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="flat">{messages[themeMode]}</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuRadioGroup
value={themeMode}
onValueChange={handleThemeModeChange}
>
{Object.values(ThemeMode).map((value) => (
<DropdownMenuRadioItem key={value} value={value}>
{value}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
</SettingsCardContent>
</SettingsCard>
)
}
@@ -4,6 +4,10 @@ import {
SettingsTitle,
SettingsTitlePlaceholder,
} from '../_modules/settings-title'
import ExperimentalSwitch from './_modules/experimental-switch'
import LanguageSelector from './_modules/language-selector'
import ThemeColorConfig from './_modules/theme-color-config'
import ThemeModeSelector from './_modules/theme-mode-selector'
export const Route = createFileRoute(
'/(experimental)/experimental/settings/user-interface',
@@ -23,6 +27,14 @@ function RouteComponent() {
<>
<SettingsTitlePlaceholder />
<SettingsTitle>{m.settings_user_interface_title()}</SettingsTitle>
<LanguageSelector />
<ThemeModeSelector />
<ThemeColorConfig />
<ExperimentalSwitch />
</>
)
}
@@ -19,6 +19,7 @@ import customParseFormat from 'dayjs/plugin/customParseFormat'
import relativeTime from 'dayjs/plugin/relativeTime'
import { lazy } from 'react'
import { BlockTaskProvider } from '@/components/providers/block-task-provider'
import { LanguageProvider } from '@/components/providers/language-provider'
import { ExperimentalThemeProvider } from '@/components/providers/theme-provider'
import { NyanpasuProvider } from '@nyanpasu/interface'
import styles from './-__root.module.scss'
@@ -74,17 +75,19 @@ export default function App() {
return (
<NyanpasuProvider>
<BlockTaskProvider>
<ExperimentalThemeProvider>
<StyledEngineProvider injectFirst>
<ThemeModeProvider>
<CssBaseline />
<LanguageProvider>
<ExperimentalThemeProvider>
<StyledEngineProvider injectFirst>
<ThemeModeProvider>
<CssBaseline />
<Outlet />
</ThemeModeProvider>
</StyledEngineProvider>
</ExperimentalThemeProvider>
<Outlet />
</ThemeModeProvider>
</StyledEngineProvider>
</ExperimentalThemeProvider>
<TanStackRouterDevtools />
<TanStackRouterDevtools />
</LanguageProvider>
</BlockTaskProvider>
</NyanpasuProvider>
)
+2 -2
View File
@@ -2,7 +2,7 @@
"manifest_version": 1,
"latest": {
"mihomo": "v1.19.17",
"mihomo_alpha": "alpha-bc8f0dc",
"mihomo_alpha": "alpha-17966b5",
"clash_rs": "v0.9.3",
"clash_premium": "2023-09-05-gdcc8d87",
"clash_rs_alpha": "0.9.3-alpha+sha.a6538ac"
@@ -69,5 +69,5 @@
"linux-armv7hf": "clash-armv7-unknown-linux-gnueabihf"
}
},
"updated_at": "2025-12-17T22:21:31.955Z"
"updated_at": "2025-12-18T22:21:30.469Z"
}
+3 -2
View File
@@ -35,6 +35,7 @@
"fmt": "run-p fmt:*",
"fmt:backend": "cargo fmt --manifest-path ./backend/Cargo.toml --all",
"fmt:prettier": "prettier --write .",
"fmt:eslint": "eslint --cache --fix . eslint.config.js",
"updater": "tsx scripts/updater.ts",
"updater:nightly": "tsx scripts/updater-nightly.ts",
"send-notify": "tsx scripts/telegram-notify.ts",
@@ -71,7 +72,7 @@
"autoprefixer": "10.4.23",
"conventional-changelog-conventionalcommits": "9.1.0",
"cross-env": "10.1.0",
"dedent": "1.7.0",
"dedent": "1.7.1",
"eslint": "9.39.2",
"eslint-config-prettier": "10.1.8",
"eslint-import-resolver-alias": "1.1.2",
@@ -108,7 +109,7 @@
"typescript": "5.9.3",
"typescript-eslint": "8.50.0"
},
"packageManager": "pnpm@10.20.0",
"packageManager": "pnpm@10.26.1",
"engines": {
"node": "22.21.1"
},
+914 -17
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -19,7 +19,7 @@
"adm-zip": "0.5.16",
"colorize-template": "1.0.0",
"consola": "3.4.2",
"fs-extra": "11.3.2",
"fs-extra": "11.3.3",
"octokit": "5.0.5",
"picocolors": "1.1.1",
"tar": "7.5.2",
+3 -3
View File
@@ -11,11 +11,11 @@ body:
## 在提交问题之前,请确认以下事项:
1. 请 **确保** 您已经查阅了 [Clash Verge Rev 官方文档](https://clash-verge-rev.github.io/guide/term.html) 以及 [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
2. 请 **确保** [已有的问题](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue) 中没有人提交过相似issue,否则请在已有的issue下进行讨论
3. 请 **务必** 给issue填写一个简洁明了的标题,以便他人快速检索
2. 请 **确保** [已有的问题](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue) 中没有人提交过相似 issue,否则请在已有的 issue 下进行讨论
3. 请 **务必** 给 issue 填写一个简洁明了的标题,以便他人快速检索
4. 请 **务必** 查看 [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) 版本更新日志
5. 请 **务必** 尝试 [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) 版本,确定问题是否仍然存在
6. 请 **务必** 按照模板规范详细描述问题以及尝试更新 Alpha 版本,否则issue将会被直接关闭
6. 请 **务必** 按照模板规范详细描述问题以及尝试更新 AutoBuild 版本,否则 issue 将会被直接关闭
## Before submitting the issue, please make sure of the following checklist:
+2 -2
View File
@@ -10,10 +10,10 @@ body:
value: |
## 在提交问题之前,请确认以下事项:
1. 请 **确保** 您已经查阅了 [Clash Verge Rev 官方文档](https://clash-verge-rev.github.io/guide/term.html) 确认软件不存在类似的功能
2. 请 **确保** [已有的问题](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue) 中没有人提交过相似issue,否则请在已有的issue下进行讨论
2. 请 **确保** [已有的问题](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue) 中没有人提交过相似 issue,否则请在已有的 issue 下进行讨论
3. 请 **务必** 给issue填写一个简洁明了的标题,以便他人快速检索
4. 请 **务必** 先下载 [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) 版本测试,确保该功能还未实现
5. 请 **务必** 按照模板规范详细描述问题,否则issue将会被关闭
5. 请 **务必** 按照模板规范详细描述问题,否则 issue 将会被关闭
## Before submitting the issue, please make sure of the following checklist:
1. Please make sure you have read the [Clash Verge Rev official documentation](https://clash-verge-rev.github.io/guide/term.html) to confirm that the software does not have similar functions
2. Please make sure there is no similar issue in the [existing issues](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue), otherwise please discuss under the existing issue
+5 -5
View File
@@ -307,7 +307,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: "24.11.1"
node-version: "24.12.0"
- uses: pnpm/action-setup@v4
name: Install pnpm
@@ -377,7 +377,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: "24.11.1"
node-version: "24.12.0"
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -505,7 +505,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: "24.11.1"
node-version: "24.12.0"
- uses: pnpm/action-setup@v4
name: Install pnpm
@@ -522,8 +522,8 @@ jobs:
- name: Download WebView2 Runtime
run: |
invoke-webrequest -uri https://github.com/westinyang/WebView2RuntimeArchive/releases/download/109.0.1518.78/Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab -outfile Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab
Expand .\Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab -F:* ./src-tauri
invoke-webrequest -uri https://github.com/westinyang/WebView2RuntimeArchive/releases/download/133.0.3065.92/Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${{ matrix.arch }}.cab -outfile Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${{ matrix.arch }}.cab
Expand .\Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${{ matrix.arch }}.cab -F:* ./src-tauri
Remove-Item .\src-tauri\tauri.windows.conf.json
Rename-Item .\src-tauri\webview2.${{ matrix.arch }}.json tauri.windows.conf.json
+16 -16
View File
@@ -46,7 +46,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: "24.11.1"
node-version: "24.12.0"
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -187,11 +187,11 @@ jobs:
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: "24.11.1"
node-version: "24.12.0"
cache: "pnpm"
- name: Pnpm Cache
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: ~/.pnpm-store
key: "pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}"
@@ -220,12 +220,12 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
# APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
# APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
# APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
# APPLE_ID: ${{ secrets.APPLE_ID }}
# APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
# APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
with:
tagName: ${{ env.TAG_NAME }}
releaseName: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
@@ -283,11 +283,11 @@ jobs:
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: "24.11.1"
node-version: "24.12.0"
cache: "pnpm"
- name: Pnpm Cache
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: ~/.pnpm-store
key: "pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}"
@@ -433,11 +433,11 @@ jobs:
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: "24.11.1"
node-version: "24.12.0"
cache: "pnpm"
- name: Pnpm Cache
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: ~/.pnpm-store
key: "pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}"
@@ -454,8 +454,8 @@ jobs:
- name: Download WebView2 Runtime
run: |
invoke-webrequest -uri https://github.com/westinyang/WebView2RuntimeArchive/releases/download/109.0.1518.78/Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab -outfile Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab
Expand .\Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab -F:* ./src-tauri
invoke-webrequest -uri https://github.com/westinyang/WebView2RuntimeArchive/releases/download/133.0.3065.92/Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${{ matrix.arch }}.cab -outfile Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${{ matrix.arch }}.cab
Expand .\Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${{ matrix.arch }}.cab -F:* ./src-tauri
Remove-Item .\src-tauri\tauri.windows.conf.json
Rename-Item .\src-tauri\webview2.${{ matrix.arch }}.json tauri.windows.conf.json
@@ -535,7 +535,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: "24.11.1"
node-version: "24.12.0"
- uses: pnpm/action-setup@v4.2.0
name: Install pnpm
+1 -1
View File
@@ -43,7 +43,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: "24.11.1"
node-version: "24.12.0"
- uses: pnpm/action-setup@v4
name: Install pnpm
+11 -11
View File
@@ -103,11 +103,11 @@ jobs:
if: github.event.inputs[matrix.input] == 'true'
uses: actions/setup-node@v6
with:
node-version: "24.11.1"
node-version: "24.12.0"
cache: "pnpm"
- name: Pnpm Cache
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: ~/.pnpm-store
key: "pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}"
@@ -141,19 +141,19 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
# APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
# APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
# APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
# APPLE_ID: ${{ secrets.APPLE_ID }}
# APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
# APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
with:
tauriScript: pnpm
args: --target ${{ matrix.target }} -b ${{ matrix.bundle }}
- name: Upload Artifacts (macOS)
if: matrix.os == 'macos-latest' && github.event.inputs[matrix.input] == 'true'
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: ${{ matrix.target }}
path: target/${{ matrix.target }}/release/bundle/dmg/*.dmg
@@ -161,7 +161,7 @@ jobs:
- name: Upload Artifacts (Windows)
if: matrix.os == 'windows-latest' && github.event.inputs[matrix.input] == 'true'
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: ${{ matrix.target }}
path: target/${{ matrix.target }}/release/bundle/nsis/*.exe
@@ -169,7 +169,7 @@ jobs:
- name: Upload Artifacts (Linux)
if: matrix.os == 'ubuntu-22.04' && github.event.inputs[matrix.input] == 'true'
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: ${{ matrix.target }}
path: target/${{ matrix.target }}/release/bundle/deb/*.deb
+2 -2
View File
@@ -47,12 +47,12 @@ jobs:
- uses: actions/setup-node@v6
if: steps.check_frontend.outputs.frontend == 'true'
with:
node-version: "24.11.1"
node-version: "24.12.0"
cache: "pnpm"
- name: Restore pnpm cache
if: steps.check_frontend.outputs.frontend == 'true'
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: ~/.pnpm-store
key: "pnpm-shared-stable-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}"
+69 -20
View File
@@ -121,6 +121,7 @@ jobs:
### 稳定机场VPN推荐
- [狗狗加速](https://verge.dginv.click/#/register?code=oaxsAGo6)
Created at ${{ env.BUILDTIME }}.
EOF
@@ -159,7 +160,10 @@ jobs:
uses: actions/checkout@v6
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@master
with:
toolchain: "1.91.0"
targets: ${{ matrix.target }}
- name: Add Rust Target
run: rustup target add ${{ matrix.target }}
@@ -167,8 +171,13 @@ jobs:
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
save-if: false
save-if: ${{ github.ref == 'refs/heads/dev' }}
prefix-key: "v1-rust"
key: "rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}"
workspaces: |
. -> target
cache-all-crates: true
cache-workspace-crates: true
- name: Install dependencies (ubuntu only)
if: matrix.os == 'ubuntu-22.04'
@@ -188,7 +197,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: "24.11.1"
node-version: "24.12.0"
- uses: pnpm/action-setup@v4
name: Install pnpm
@@ -200,6 +209,13 @@ jobs:
pnpm i
pnpm run prebuild ${{ matrix.target }}
- name: Add Rust Target
run: |
# Ensure cross target is installed for the pinned toolchain; fallback without explicit toolchain if needed
rustup target add ${{ matrix.target }} --toolchain 1.91.0 || rustup target add ${{ matrix.target }}
rustup target list --installed
echo "Rust target ${{ matrix.target }} installed."
- name: Tauri build
# 上游 5.24 修改了 latest.json 的生成逻辑,且依赖 tauri-plugin-update 2.10.0 暂未发布,故锁定在 0.5.23 版本
uses: tauri-apps/tauri-action@v0.6.0
@@ -243,7 +259,10 @@ jobs:
uses: actions/checkout@v6
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@master
with:
toolchain: "1.91.0"
targets: ${{ matrix.target }}
- name: Add Rust Target
run: rustup target add ${{ matrix.target }}
@@ -251,13 +270,18 @@ jobs:
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
save-if: false
save-if: ${{ github.ref == 'refs/heads/dev' }}
prefix-key: "v1-rust"
key: "rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}"
workspaces: |
. -> target
cache-all-crates: true
cache-workspace-crates: true
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: "24.11.1"
node-version: "24.12.0"
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -313,6 +337,13 @@ jobs:
gcc-arm-linux-gnueabihf \
g++-arm-linux-gnueabihf
- name: Add Rust Target
run: |
# Ensure cross target is installed for the pinned toolchain; fallback without explicit toolchain if needed
rustup target add ${{ matrix.target }} --toolchain 1.91.0 || rustup target add ${{ matrix.target }}
rustup target list --installed
echo "Rust target ${{ matrix.target }} installed."
- name: Build for Linux
run: |
export PKG_CONFIG_ALLOW_CROSS=1
@@ -345,8 +376,8 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
prerelease: ${{ contains(github.ref_name, '-rc') }}
files: |
src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm
target/${{ matrix.target }}/release/bundle/deb/*.deb
target/${{ matrix.target }}/release/bundle/rpm/*.rpm
release-for-fixed-webview2:
name: Release Build for Fixed WebView2
@@ -366,19 +397,30 @@ jobs:
- name: Checkout Repository
uses: actions/checkout@v6
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@master
with:
toolchain: "1.91.0"
targets: ${{ matrix.target }}
- name: Add Rust Target
run: rustup target add ${{ matrix.target }}
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
save-if: false
save-if: ${{ github.ref == 'refs/heads/dev' }}
prefix-key: "v1-rust"
key: "rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}"
workspaces: |
. -> target
cache-all-crates: true
cache-workspace-crates: true
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: "24.11.1"
node-version: "24.12.0"
- uses: pnpm/action-setup@v4
name: Install pnpm
@@ -392,14 +434,20 @@ jobs:
- name: Download WebView2 Runtime
run: |
invoke-webrequest -uri https://github.com/westinyang/WebView2RuntimeArchive/releases/download/109.0.1518.78/Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab -outfile Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab
Expand .\Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab -F:* ./src-tauri
invoke-webrequest -uri https://github.com/westinyang/WebView2RuntimeArchive/releases/download/133.0.3065.92/Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${{ matrix.arch }}.cab -outfile Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${{ matrix.arch }}.cab
Expand .\Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${{ matrix.arch }}.cab -F:* ./src-tauri
Remove-Item .\src-tauri\tauri.windows.conf.json
Rename-Item .\src-tauri\webview2.${{ matrix.arch }}.json tauri.windows.conf.json
- name: Add Rust Target
run: |
# Ensure cross target is installed for the pinned toolchain; fallback without explicit toolchain if needed
rustup target add ${{ matrix.target }} --toolchain 1.91.0 || rustup target add ${{ matrix.target }}
rustup target list --installed
echo "Rust target ${{ matrix.target }} installed."
- name: Tauri build
id: build
# 上游 5.24 修改了 latest.json 的生成逻辑,且依赖 tauri-plugin-update 2.10.0 暂未发布,故锁定在 0.5.23 版本
uses: tauri-apps/tauri-action@v0.6.0
env:
NODE_OPTIONS: "--max_old_space_size=4096"
@@ -438,7 +486,7 @@ jobs:
body: "See release notes for detailed changelog."
token: ${{ secrets.GITHUB_TOKEN }}
prerelease: ${{ contains(github.ref_name, '-rc') }}
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
files: target/${{ matrix.target }}/release/bundle/nsis/*setup*
- name: Portable Bundle
run: pnpm portable-fixed-webview2 ${{ matrix.target }}
@@ -457,7 +505,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: "24.11.1"
node-version: "24.12.0"
- uses: pnpm/action-setup@v4
name: Install pnpm
@@ -483,7 +531,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: "24.11.1"
node-version: "24.12.0"
- uses: pnpm/action-setup@v4
name: Install pnpm
@@ -545,7 +593,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: "24.11.1"
node-version: "24.12.0"
- uses: pnpm/action-setup@v4
name: Install pnpm
@@ -599,6 +647,7 @@ jobs:
### 稳定机场VPN推荐
- [狗狗加速](https://verge.dginv.click/#/register?code=oaxsAGo6)
Created at ${{ env.BUILDTIME }}.
EOF
+2 -2
View File
@@ -15,7 +15,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: "24.11.1"
node-version: "24.12.0"
- uses: pnpm/action-setup@v4
name: Install pnpm
@@ -39,7 +39,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: "24.11.1"
node-version: "24.12.0"
- uses: pnpm/action-setup@v4
name: Install pnpm
+7 -13
View File
@@ -28,24 +28,18 @@ pnpm exec lint-staged
RUST_FILES="$(git diff --cached --name-only --diff-filter=ACMR | grep -E '^src-tauri/.*\.rs$' || true)"
if [ -n "$RUST_FILES" ]; then
echo "[pre-commit] Formatting Rust changes with cargo fmt..."
(
cd src-tauri
cargo fmt
)
cargo fmt
while IFS= read -r file; do
[ -n "$file" ] && git add "$file"
done <<< "$RUST_FILES"
echo "[pre-commit] Linting Rust changes with cargo clippy..."
(
cd src-tauri
cargo clippy-all
if ! command -v clash-verge-logging-check >/dev/null 2>&1; then
echo "[pre-commit] Installing clash-verge-logging-check..."
cargo install --git https://github.com/clash-verge-rev/clash-verge-logging-check.git
fi
clash-verge-logging-check
)
cargo clippy-all
if ! command -v clash-verge-logging-check >/dev/null 2>&1; then
echo "[pre-commit] Installing clash-verge-logging-check..."
cargo install --git https://github.com/clash-verge-rev/clash-verge-logging-check.git
fi
clash-verge-logging-check
fi
TS_FILES="$(git diff --cached --name-only --diff-filter=ACMR | grep -E '\.(ts|tsx)$' || true)"
+2 -8
View File
@@ -25,16 +25,10 @@ pnpm typecheck
if command -v cargo >/dev/null 2>&1; then
echo "[pre-push] Verifying Rust formatting..."
(
cd src-tauri
cargo fmt --check
)
cargo fmt --check
echo "[pre-push] Running cargo clippy..."
(
cd src-tauri
cargo clippy-all
)
cargo clippy-all
else
echo "[pre-push] ⚠️ cargo not found; skipping Rust checks."
fi
+5 -1
View File
@@ -119,13 +119,17 @@ cargo fmt
pnpm format
```
### Signing your commit
Signed commits are required to verify authorship and ensure your contributions can be merged. Reference signing-commits [here](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits).
### Submitting Your Changes
1. Fork the repository.
2. Create a new branch for your feature or bug fix.
3. Commit your changes with clear messages.
3. Commit your changes with clear messages and make sure it's signed.
4. Push your branch and submit a pull request.
+139 -168
View File
@@ -588,7 +588,7 @@ dependencies = [
"axum-core 0.5.5",
"bytes",
"futures-util",
"http 1.3.1",
"http 1.4.0",
"http-body 1.0.1",
"http-body-util",
"itoa",
@@ -629,7 +629,7 @@ checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22"
dependencies = [
"bytes",
"futures-core",
"http 1.3.1",
"http 1.4.0",
"http-body 1.0.1",
"http-body-util",
"mime",
@@ -1002,9 +1002,9 @@ dependencies = [
[[package]]
name = "camino"
version = "1.2.1"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609"
checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48"
dependencies = [
"serde_core",
]
@@ -1059,9 +1059,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.47"
version = "1.2.49"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07"
checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -1186,7 +1186,7 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
[[package]]
name = "clash-verge"
version = "2.4.4"
version = "2.4.4-rc.1"
dependencies = [
"aes-gcm",
"anyhow",
@@ -1285,10 +1285,7 @@ version = "0.1.0"
dependencies = [
"clash-verge-logging",
"log",
"signal-hook 0.3.18",
"tauri",
"tokio",
"windows-sys 0.61.2",
]
[[package]]
@@ -1302,8 +1299,8 @@ dependencies = [
[[package]]
name = "clash_verge_logger"
version = "0.2.1"
source = "git+https://github.com/clash-verge-rev/clash-verge-logger#955f1b709890640ff01fd30009df0f35816bbca6"
version = "0.2.2"
source = "git+https://github.com/clash-verge-rev/clash-verge-logger#e4768e3852c4868ed86e7210df82c1178467820d"
dependencies = [
"arraydeque",
"compact_str",
@@ -1315,8 +1312,8 @@ dependencies = [
[[package]]
name = "clash_verge_service_ipc"
version = "2.0.21"
source = "git+https://github.com/clash-verge-rev/clash-verge-service-ipc#da00a684c2b9723d647ed4992714eb669fcbd8a2"
version = "2.0.26"
source = "git+https://github.com/clash-verge-rev/clash-verge-service-ipc#37b9964a9bce767b5b95ea2be75613b23400c9f0"
dependencies = [
"anyhow",
"compact_str",
@@ -1602,9 +1599,9 @@ dependencies = [
[[package]]
name = "crc"
version = "3.3.0"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675"
checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d"
dependencies = [
"crc-catalog",
]
@@ -2137,9 +2134,9 @@ dependencies = [
[[package]]
name = "dlopen2"
version = "0.8.0"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b54f373ccf864bf587a89e880fb7610f8d73f3045f13580948ccbcaff26febff"
checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4"
dependencies = [
"dlopen2_derive",
"libc",
@@ -2149,9 +2146,9 @@ dependencies = [
[[package]]
name = "dlopen2_derive"
version = "0.4.1"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "788160fb30de9cdd857af31c6a2675904b16ece8fc2737b2c7127ba368c9d0f4"
checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f"
dependencies = [
"proc-macro2",
"quote",
@@ -2287,9 +2284,9 @@ dependencies = [
[[package]]
name = "endi"
version = "1.1.0"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf"
checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
[[package]]
name = "enumflags2"
@@ -3178,7 +3175,7 @@ dependencies = [
"fnv",
"futures-core",
"futures-sink",
"http 1.3.1",
"http 1.4.0",
"indexmap 2.12.1",
"slab",
"tokio",
@@ -3267,7 +3264,7 @@ dependencies = [
"base64 0.22.1",
"bytes",
"headers-core",
"http 1.3.1",
"http 1.4.0",
"httpdate",
"mime",
"sha1",
@@ -3279,7 +3276,7 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4"
dependencies = [
"http 1.3.1",
"http 1.4.0",
]
[[package]]
@@ -3355,12 +3352,11 @@ dependencies = [
[[package]]
name = "http"
version = "1.3.1"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
dependencies = [
"bytes",
"fnv",
"itoa",
]
@@ -3382,7 +3378,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
dependencies = [
"bytes",
"http 1.3.1",
"http 1.4.0",
]
[[package]]
@@ -3393,7 +3389,7 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
dependencies = [
"bytes",
"futures-core",
"http 1.3.1",
"http 1.4.0",
"http-body 1.0.1",
"pin-project-lite",
]
@@ -3463,7 +3459,7 @@ dependencies = [
"futures-channel",
"futures-core",
"h2 0.4.12",
"http 1.3.1",
"http 1.4.0",
"http-body 1.0.1",
"httparse",
"httpdate",
@@ -3481,7 +3477,7 @@ version = "0.27.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
dependencies = [
"http 1.3.1",
"http 1.4.0",
"hyper 1.8.1",
"hyper-util",
"rustls",
@@ -3535,16 +3531,16 @@ dependencies = [
[[package]]
name = "hyper-util"
version = "0.1.18"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56"
checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f"
dependencies = [
"base64 0.22.1",
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"http 1.3.1",
"http 1.4.0",
"http-body 1.0.1",
"hyper 1.8.1",
"ipnet",
@@ -3996,9 +3992,9 @@ dependencies = [
[[package]]
name = "js-sys"
version = "0.3.82"
version = "0.3.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65"
checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8"
dependencies = [
"once_cell",
"wasm-bindgen",
@@ -4039,13 +4035,13 @@ dependencies = [
[[package]]
name = "kode-bridge"
version = "0.3.4"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b158d8b9ab9f07c892a3f1b5d44a79e6c74e97541bf66488025796d429cf7aac"
checksum = "0ba2fd0531052f4438d171509ee7686b6b2207314366a5e2dde80586f6d6a09b"
dependencies = [
"bytes",
"futures",
"http 1.3.1",
"http 1.4.0",
"httparse",
"interprocess",
"libc",
@@ -4161,9 +4157,9 @@ dependencies = [
[[package]]
name = "libz-rs-sys"
version = "0.5.2"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "840db8cf39d9ec4dd794376f38acc40d0fc65eec2a8f484f7fd375b84602becd"
checksum = "15413ef615ad868d4d65dce091cb233b229419c7c0c4bcaa746c0901c49ff39c"
dependencies = [
"zlib-rs",
]
@@ -4274,9 +4270,9 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
[[package]]
name = "mac-notification-sys"
version = "0.6.8"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ee70bb2bba058d58e252d2944582d634fc884fc9c489a966d428dedcf653e97"
checksum = "65fd3f75411f4725061682ed91f131946e912859d0044d39c4ec0aac818d7621"
dependencies = [
"cc",
"objc2 0.6.3",
@@ -4425,9 +4421,9 @@ dependencies = [
[[package]]
name = "mio"
version = "1.1.0"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873"
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
dependencies = [
"libc",
"wasi 0.11.1+wasi-snapshot-preview1",
@@ -4436,9 +4432,9 @@ dependencies = [
[[package]]
name = "moxcms"
version = "0.7.9"
version = "0.7.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fbdd3d7436f8b5e892b8b7ea114271ff0fa00bc5acae845d53b07d498616ef6"
checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97"
dependencies = [
"num-traits",
"pxfm",
@@ -4817,7 +4813,7 @@ dependencies = [
"objc2-core-text",
"objc2-core-video",
"objc2-foundation 0.3.2",
"objc2-quartz-core 0.3.2",
"objc2-quartz-core",
]
[[package]]
@@ -4972,18 +4968,6 @@ dependencies = [
"objc2-core-foundation",
]
[[package]]
name = "objc2-metal"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
dependencies = [
"bitflags 2.10.0",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
]
[[package]]
name = "objc2-osa-kit"
version = "0.3.2"
@@ -4996,19 +4980,6 @@ dependencies = [
"objc2-foundation 0.3.2",
]
[[package]]
name = "objc2-quartz-core"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
dependencies = [
"bitflags 2.10.0",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
"objc2-metal",
]
[[package]]
name = "objc2-quartz-core"
version = "0.3.2"
@@ -5017,6 +4988,7 @@ checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f"
dependencies = [
"bitflags 2.10.0",
"objc2 0.6.3",
"objc2-core-foundation",
"objc2-foundation 0.3.2",
]
@@ -5780,7 +5752,7 @@ version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983"
dependencies = [
"toml_edit 0.23.7",
"toml_edit 0.23.9",
]
[[package]]
@@ -5924,9 +5896,9 @@ dependencies = [
[[package]]
name = "pxfm"
version = "0.1.25"
version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3cbdf373972bf78df4d3b518d07003938e2c7d1fb5891e55f9cb6df57009d84"
checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8"
dependencies = [
"num-traits",
]
@@ -6274,9 +6246,9 @@ checksum = "cadadef317c2f20755a64d7fdc48f9e7178ee6b0e1f7fce33fa60f1d68a276e6"
[[package]]
name = "reqwest"
version = "0.12.24"
version = "0.12.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f"
checksum = "b6eff9328d40131d43bd911d42d79eb6a47312002a4daefc9e37f17e74a7701a"
dependencies = [
"base64 0.22.1",
"bytes",
@@ -6286,7 +6258,7 @@ dependencies = [
"futures-core",
"futures-util",
"h2 0.4.12",
"http 1.3.1",
"http 1.4.0",
"http-body 1.0.1",
"http-body-util",
"hyper 1.8.1",
@@ -6311,7 +6283,7 @@ dependencies = [
"tokio-rustls",
"tokio-util",
"tower 0.5.2",
"tower-http 0.6.6",
"tower-http 0.6.8",
"tower-service",
"url",
"wasm-bindgen",
@@ -6330,7 +6302,7 @@ dependencies = [
"async-trait",
"chrono",
"digest_auth",
"http 1.3.1",
"http 1.4.0",
"httpdate",
"reqwest",
"serde",
@@ -6589,9 +6561,9 @@ dependencies = [
[[package]]
name = "rustls-pki-types"
version = "1.13.0"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a"
checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c"
dependencies = [
"web-time",
"zeroize",
@@ -6912,9 +6884,9 @@ dependencies = [
[[package]]
name = "serde_with"
version = "3.16.0"
version = "3.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10574371d41b0d9b2cff89418eda27da52bcaff2cc8741db26382a77c29131f1"
checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7"
dependencies = [
"base64 0.22.1",
"chrono",
@@ -6931,9 +6903,9 @@ dependencies = [
[[package]]
name = "serde_with_macros"
version = "3.16.0"
version = "3.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08a72d8216842fdd57820dc78d840bef99248e35fb2554ff923319e60f2d686b"
checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c"
dependencies = [
"darling",
"proc-macro2",
@@ -7112,9 +7084,9 @@ dependencies = [
[[package]]
name = "simd-adler32"
version = "0.3.7"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
[[package]]
name = "siphasher"
@@ -7210,24 +7182,24 @@ dependencies = [
[[package]]
name = "softbuffer"
version = "0.4.6"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08"
checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3"
dependencies = [
"bytemuck",
"cfg_aliases",
"core-graphics",
"foreign-types 0.5.0",
"js-sys",
"log",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
"objc2-quartz-core 0.2.2",
"ndk",
"objc2 0.6.3",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-foundation 0.3.2",
"objc2-quartz-core",
"raw-window-handle",
"redox_syscall",
"tracing",
"wasm-bindgen",
"web-sys",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -7413,8 +7385,8 @@ dependencies = [
[[package]]
name = "sysproxy"
version = "0.4.0"
source = "git+https://github.com/clash-verge-rev/sysproxy-rs#0f844dd2639b0ac74da4548b1325335844947420"
version = "0.4.2"
source = "git+https://github.com/clash-verge-rev/sysproxy-rs#51256d5921a01bbcf24fe5629296d2bfe493329d"
dependencies = [
"interfaces",
"iptools",
@@ -7543,9 +7515,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]]
name = "tauri"
version = "2.9.4"
version = "2.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15524fc7959bfcaa051ba6d0b3fb1ef18e978de2176c7c6acb977f7fd14d35c7"
checksum = "8a3868da5508446a7cd08956d523ac3edf0a8bc20bf7e4038f9a95c2800d2033"
dependencies = [
"anyhow",
"bytes",
@@ -7557,7 +7529,7 @@ dependencies = [
"glob",
"gtk",
"heck 0.5.0",
"http 1.3.1",
"http 1.4.0",
"http-range",
"image",
"jni",
@@ -7660,9 +7632,9 @@ dependencies = [
[[package]]
name = "tauri-plugin"
version = "2.5.1"
version = "2.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "076c78a474a7247c90cad0b6e87e593c4c620ed4efdb79cbe0214f0021f6c39d"
checksum = "0e1d0a4860b7ff570c891e1d2a586bf1ede205ff858fbc305e0b5ae5d14c1377"
dependencies = [
"anyhow",
"glob",
@@ -7828,7 +7800,7 @@ dependencies = [
"bytes",
"cookie_store",
"data-url",
"http 1.3.1",
"http 1.4.0",
"regex",
"reqwest",
"schemars 0.8.22",
@@ -7846,11 +7818,11 @@ dependencies = [
[[package]]
name = "tauri-plugin-mihomo"
version = "0.1.1"
source = "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo#24586eb0721314f88e65460b4ac01933b3376d3c"
source = "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo#153f16f7b3f979aa130a2d3d7c39a52a39987288"
dependencies = [
"base64 0.22.1",
"futures-util",
"http 1.3.1",
"http 1.4.0",
"httparse",
"log",
"pin-project",
@@ -7928,7 +7900,7 @@ dependencies = [
"dirs 6.0.0",
"flate2",
"futures-util",
"http 1.3.1",
"http 1.4.0",
"infer",
"log",
"minisign-verify",
@@ -7974,7 +7946,7 @@ dependencies = [
"cookie",
"dpi",
"gtk",
"http 1.3.1",
"http 1.4.0",
"jni",
"objc2 0.6.3",
"objc2-ui-kit",
@@ -7992,12 +7964,12 @@ dependencies = [
[[package]]
name = "tauri-runtime-wry"
version = "2.9.2"
version = "2.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7950f3bde6bcca6655bc5e76d3d6ec587ceb81032851ab4ddbe1f508bdea2729"
checksum = "187a3f26f681bdf028f796ccf57cf478c1ee422c50128e5a0a6ebeb3f5910065"
dependencies = [
"gtk",
"http 1.3.1",
"http 1.4.0",
"jni",
"log",
"objc2 0.6.3",
@@ -8031,7 +8003,7 @@ dependencies = [
"dunce",
"glob",
"html5ever",
"http 1.3.1",
"http 1.4.0",
"infer",
"json-patch",
"kuchikiki",
@@ -8444,7 +8416,7 @@ dependencies = [
"toml_datetime 0.7.3",
"toml_parser",
"toml_writer",
"winnow 0.7.13",
"winnow 0.7.14",
]
[[package]]
@@ -8498,19 +8470,19 @@ dependencies = [
"serde_spanned 0.6.9",
"toml_datetime 0.6.11",
"toml_write",
"winnow 0.7.13",
"winnow 0.7.14",
]
[[package]]
name = "toml_edit"
version = "0.23.7"
version = "0.23.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d"
checksum = "5d7cbc3b4b49633d57a0509303158ca50de80ae32c265093b24c414705807832"
dependencies = [
"indexmap 2.12.1",
"toml_datetime 0.7.3",
"toml_parser",
"winnow 0.7.13",
"winnow 0.7.14",
]
[[package]]
@@ -8519,7 +8491,7 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e"
dependencies = [
"winnow 0.7.13",
"winnow 0.7.14",
]
[[package]]
@@ -8572,7 +8544,7 @@ dependencies = [
"base64 0.22.1",
"bytes",
"h2 0.4.12",
"http 1.3.1",
"http 1.4.0",
"http-body 1.0.1",
"http-body-util",
"hyper 1.8.1",
@@ -8693,14 +8665,14 @@ dependencies = [
[[package]]
name = "tower-http"
version = "0.6.6"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
dependencies = [
"bitflags 2.10.0",
"bytes",
"futures-util",
"http 1.3.1",
"http 1.4.0",
"http-body 1.0.1",
"iri-string",
"pin-project-lite",
@@ -8723,9 +8695,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tracing"
version = "0.1.41"
version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647"
dependencies = [
"log",
"pin-project-lite",
@@ -8735,9 +8707,9 @@ dependencies = [
[[package]]
name = "tracing-attributes"
version = "0.1.30"
version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
@@ -8746,9 +8718,9 @@ dependencies = [
[[package]]
name = "tracing-core"
version = "0.1.34"
version = "0.1.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c"
dependencies = [
"once_cell",
"valuable",
@@ -8767,9 +8739,9 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
version = "0.3.20"
version = "0.3.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
dependencies = [
"matchers",
"nu-ansi-term",
@@ -8863,7 +8835,7 @@ checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442"
dependencies = [
"bytes",
"data-encoding",
"http 1.3.1",
"http 1.4.0",
"httparse",
"log",
"rand 0.9.2",
@@ -9044,13 +9016,13 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.18.1"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2"
checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a"
dependencies = [
"getrandom 0.3.4",
"js-sys",
"serde",
"serde_core",
"wasm-bindgen",
]
@@ -9147,7 +9119,7 @@ dependencies = [
"bytes",
"futures-util",
"headers",
"http 1.3.1",
"http 1.4.0",
"http-body 1.0.1",
"http-body-util",
"hyper 1.8.1",
@@ -9190,9 +9162,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen"
version = "0.2.105"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60"
checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd"
dependencies = [
"cfg-if",
"once_cell",
@@ -9203,9 +9175,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.55"
version = "0.4.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0"
checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c"
dependencies = [
"cfg-if",
"js-sys",
@@ -9216,9 +9188,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.105"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2"
checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -9226,9 +9198,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.105"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc"
checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40"
dependencies = [
"bumpalo",
"proc-macro2",
@@ -9239,9 +9211,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.105"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76"
checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4"
dependencies = [
"unicode-ident",
]
@@ -9334,9 +9306,9 @@ dependencies = [
[[package]]
name = "web-sys"
version = "0.3.82"
version = "0.3.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1"
checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -10049,9 +10021,9 @@ dependencies = [
[[package]]
name = "winnow"
version = "0.7.13"
version = "0.7.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf"
checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
dependencies = [
"memchr",
]
@@ -10093,15 +10065,14 @@ checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
[[package]]
name = "wl-clipboard-rs"
version = "0.9.2"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e5ff8d0e60065f549fafd9d6cb626203ea64a798186c80d8e7df4f8af56baeb"
checksum = "e9651471a32e87d96ef3a127715382b2d11cc7c8bb9822ded8a7cc94072eb0a3"
dependencies = [
"libc",
"log",
"os_pipe",
"rustix 0.38.44",
"tempfile",
"rustix 1.1.2",
"thiserror 2.0.17",
"tree_magic_mini",
"wayland-backend",
@@ -10138,7 +10109,7 @@ dependencies = [
"gdkx11",
"gtk",
"html5ever",
"http 1.3.1",
"http 1.4.0",
"javascriptcore-rs",
"jni",
"kuchikiki",
@@ -10292,7 +10263,7 @@ dependencies = [
"uds_windows",
"uuid",
"windows-sys 0.61.2",
"winnow 0.7.13",
"winnow 0.7.14",
"zbus_macros",
"zbus_names",
"zvariant",
@@ -10321,24 +10292,24 @@ checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97"
dependencies = [
"serde",
"static_assertions",
"winnow 0.7.13",
"winnow 0.7.14",
"zvariant",
]
[[package]]
name = "zerocopy"
version = "0.8.28"
version = "0.8.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43fa6694ed34d6e57407afbccdeecfa268c470a7d2a5b0cf49ce9fcc345afb90"
checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.28"
version = "0.8.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c640b22cd9817fae95be82f0d2f90b11f7605f6c319d16705c459b27ac2cbc26"
checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a"
dependencies = [
"proc-macro2",
"quote",
@@ -10461,9 +10432,9 @@ dependencies = [
[[package]]
name = "zlib-rs"
version = "0.5.2"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2"
checksum = "51f936044d677be1a1168fae1d03b583a285a5dd9d8cbf7b24c23aa1fc775235"
[[package]]
name = "zopfli"
@@ -10530,7 +10501,7 @@ dependencies = [
"enumflags2",
"serde",
"url",
"winnow 0.7.13",
"winnow 0.7.14",
"zvariant_derive",
"zvariant_utils",
]
@@ -10558,5 +10529,5 @@ dependencies = [
"quote",
"serde",
"syn 2.0.111",
"winnow 0.7.13",
"winnow 0.7.14",
]
+1 -4
View File
@@ -9,9 +9,6 @@ members = [
]
resolver = "2"
[workspace.package]
edition = "2024"
rust-version = "1.91"
[profile.release]
panic = "abort"
@@ -49,7 +46,7 @@ clash-verge-signal = { path = "crates/clash-verge-signal" }
clash-verge-types = { path = "crates/clash-verge-types" }
tauri-plugin-clash-verge-sysinfo = { path = "crates/tauri-plugin-clash-verge-sysinfo" }
tauri = { version = "2.9.4" }
tauri = { version = "2.9.5" }
tauri-plugin-clipboard-manager = "2.3.2"
parking_lot = { version = "0.12.5", features = ["hardware-lock-elision"] }
anyhow = "1.0.100"
+17 -6
View File
@@ -2,10 +2,6 @@
- **Mihomo(Meta) 内核升级至 v1.19.17**
> [!WARNING]
> Apple 公证服务故障,临时暂停 macOS 签名
> macOS 跳过签名,终端执行 `sudo xattr -rd com.apple.quarantine /Applications/Clash\ Verge.app/`
### 🐞 修复问题
- Linux 无法切换 TUN 堆栈
@@ -17,7 +13,6 @@
- Monaco 编辑器的行数上限
- 已删除节点在手动分组中导致配置无法加载
- 仪表盘与托盘状态不同步
- 修复重启或退出应用,关闭系统时无法记忆用户行为
- 彻底修复 macOS 连接页面显示异常
- windows 端监听关机信号失败
- 修复代理按钮和高亮状态不同步
@@ -27,6 +22,14 @@
- 修复在搜索框输入不完整正则直接崩溃
- 修复创建窗口时在非简体中文环境或深色主题下的短暂闪烁
- 修复更新时加载进度条异常
- 升级内核失败导致内核不可用问题
- 修复 macOS 在安装和卸载服务时提示与操作不匹配
- 修复菜单排序模式拖拽异常
- 修复托盘菜单代理组前的异常勾选状态
- 修复 Windows 下自定义标题栏按钮在最小化 / 关闭后 hover 状态残留
- 修复直接覆盖 `config.yaml` 使用时无法展开代理组
- 修复 macOS 下应用启动时系统托盘图标颜色闪烁
- 修复应用静默启动模式下非全局热键一直抢占其他应用按键问题
<details>
<summary><strong> ✨ 新增功能 </strong></summary>
@@ -36,6 +39,9 @@
- 连接页面支持查看已关闭的连接(最近最多 500 个已关闭连接)
- 日志页面支持按时间倒序
- 增加「重新激活订阅」的全局快捷键
- WebView2 Runtime 修复构建升级到 133.0.3065.92
- 侧边栏右键新增「恢复默认排序」
- Linux 下新增对 TUN 「自动重定向」(`auto-redirect` 字段)的配置支持,默认关闭
</details>
@@ -47,7 +53,7 @@
- 替换前端信息编辑组件,提供更好性能
- 优化后端内存和性能表现
- 防止退出时可能的禁用 TUN 失败
- i18n 支持
- 全新 i18n 支持方式
- 优化备份设置布局
- 优化流量图性能表现,实现动态 FPS 和窗口失焦自动暂停
- 性能优化系统状态获取
@@ -58,6 +64,11 @@
- 优化前端数据刷新
- 优化流量采样和数据处理
- 优化应用重启/退出时的资源清理性能, 大幅缩短执行时间
- 优化前端 WebSocket 连接机制
- 改进旧版 Service 需要重新安装检测流程
- 优化 macOS, Linux 和 Windows 系统信号处理
- 链式代理仅显示 Selector 类型规则组
- 优化 Windows 系统代理设置,不再依赖 `sysproxy.exe` 来设置代理
</details>
+18 -11
View File
@@ -16,7 +16,8 @@ A Clash Meta GUI based on <a href="https://github.com/tauri-apps/tauri">Tauri</a
<a href="./docs/README_es.md">Español</a> ·
<a href="./docs/README_ru.md">Русский</a> ·
<a href="./docs/README_ja.md">日本語</a> ·
<a href="./docs/README_ko.md">한국어</a>
<a href="./docs/README_ko.md">한국어</a> ·
<a href="./docs/README_fa.md">فارسی</a>
</p>
## Preview
@@ -47,17 +48,23 @@ Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
## Promotion
#### [狗狗加速 —— 技术流机场 Doggygo VPN](https://verge.dginv.click/#/register?code=oaxsAGo6)
### ✈️ [狗狗加速 —— 技术流机场 Doggygo VPN](https://verge.dginv.click/#/register?code=oaxsAGo6)
- 高性能海外机场,免费试用优惠套餐,解锁流媒体,全球首家支持 Hysteria 协议
- 使用 Clash Verge 专属邀请链接注册送 3 天,每天 1G 流量免费试用:[点此注册](https://verge.dginv.click/#/register?code=oaxsAGo6)
- Clash Verge 专属 8 折优惠码: verge20 (仅有 500 份)
- 优惠套餐每月仅需 15.8 元,160G 流量,年付 8 折
- 海外团队,无跑路风险,高达 50% 返佣
- 集群负载均衡设计,高速专线(兼容老客户端),极低延迟,无视晚高峰,4K 秒开
- 全球首家 Hysteria 协议机场,现已上线更快的 `Hysteria2` 协议(Clash Verge 客户端最佳搭配)
- 解锁流媒体及 ChatGPT
- 官网:[https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
🚀 高性能海外技术流机场,支持免费试用优惠套餐,全面解锁流媒体及 AI 服务,全球首家采用 **QUIC 协议**
🎁 使用 **Clash Verge 专属邀请链接** 注册即送 **3 天免费试用**,每日 **1GB 流量**:👉 [点此注册](https://verge.dginv.click/#/register?code=oaxsAGo6)
#### **核心优势:**
- 📱 自研 iOS 客户端(业内"唯一")技术经得起考验,极大**持续研发**投入
- 🧑‍💻 **12小时真人客服**(顺带解决 Clash Verge 使用问题)
- 💰 优惠套餐每月**仅需 21 元,160G 流量,年付 8 折**
- 🌍 海外团队,无跑路风险,高达 50% 返佣
- ⚙️ **集群负载均衡**设计,**负载监控和随时扩容**,高速专线(兼容老客户端),极低延迟,无视晚高峰,4K 秒开
- ⚡ 全球首家**Quic 协议机场**,现已上线更快的 Tuic 协议(Clash Verge 客户端最佳搭配)
- 🎬 解锁**流媒体及 主流 AI**
🌐 官网:👉 [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
#### 本项目的构建与发布环境由 [YXVM](https://yxvm.com/aff.php?aff=827) 独立服务器全力支持,
@@ -18,6 +18,7 @@ pub enum Type {
Config,
Setup,
System,
SystemSignal,
Service,
Hotkey,
Window,
@@ -42,6 +43,7 @@ impl fmt::Display for Type {
Self::Config => write!(f, "[Config]"),
Self::Setup => write!(f, "[Setup]"),
Self::System => write!(f, "[System]"),
Self::SystemSignal => write!(f, "[SysSignal]"),
Self::Service => write!(f, "[Service]"),
Self::Hotkey => write!(f, "[Hotkey]"),
Self::Window => write!(f, "[Window]"),
@@ -1,26 +1,13 @@
[package]
name = "clash-verge-signal"
version = "0.1.0"
edition.workspace = true
rust-version.workspace = true
edition = "2024"
rust-version = "1.91"
[dependencies]
clash-verge-logging = { workspace = true }
log = { workspace = true }
tokio = { workspace = true }
[target.'cfg(unix)'.dependencies]
signal-hook = "0.3.18"
[target.'cfg(windows)'.dependencies]
tauri = { workspace = true }
windows-sys = { version = "0.61.2", features = [
"Win32_Foundation",
"Win32_Graphics_Gdi",
"Win32_System_SystemServices",
"Win32_UI_WindowsAndMessaging",
] }
[lints]
workspace = true
@@ -9,7 +9,7 @@ mod windows;
pub(crate) static RUNTIME: OnceLock<Option<tokio::runtime::Runtime>> = OnceLock::new();
pub fn register<F, Fut>(#[cfg(windows)] app_handle: &tauri::AppHandle, f: F)
pub fn register<F, Fut>(f: F)
where
F: Fn() -> Fut + Send + Sync + 'static,
Fut: Future + Send + 'static,
@@ -19,7 +19,7 @@ where
Err(e) => {
logging!(
info,
Type::System,
Type::SystemSignal,
"register shutdown signal failed, create tokio runtime error: {}",
e
);
@@ -31,5 +31,5 @@ where
unix::register(f);
#[cfg(windows)]
windows::register(app_handle, f);
windows::register(f);
}
@@ -1,13 +1,12 @@
use signal_hook::{
consts::{SIGHUP, SIGINT, SIGTERM},
iterator::Signals,
low_level,
};
use std::sync::atomic::{AtomicBool, Ordering};
use clash_verge_logging::{Type, logging, logging_error};
use clash_verge_logging::{Type, logging};
use tokio::signal::unix::{SignalKind, signal};
use crate::RUNTIME;
static IS_CLEANING_UP: AtomicBool = AtomicBool::new(false);
pub fn register<F, Fut>(f: F)
where
F: Fn() -> Fut + Send + Sync + 'static,
@@ -15,39 +14,80 @@ where
{
if let Some(Some(rt)) = RUNTIME.get() {
rt.spawn(async move {
let signals = [SIGTERM, SIGINT, SIGHUP];
let mut sigs = match Signals::new(signals) {
let mut sigterm = match signal(SignalKind::terminate()) {
Ok(s) => s,
Err(e) => {
logging!(error, Type::System, "注册信号处理器失败: {}", e);
logging!(
error,
Type::SystemSignal,
"Failed to register SIGTERM: {}",
e
);
return;
}
};
let mut sigint = match signal(SignalKind::interrupt()) {
Ok(s) => s,
Err(e) => {
logging!(
error,
Type::SystemSignal,
"Failed to register SIGINT: {}",
e
);
return;
}
};
let mut sighup = match signal(SignalKind::hangup()) {
Ok(s) => s,
Err(e) => {
logging!(
error,
Type::SystemSignal,
"Failed to register SIGHUP: {}",
e
);
return;
}
};
for signal in &mut sigs {
let signal_to_str = |signal| match signal {
SIGTERM => "SIGTERM",
SIGINT => "SIGINT",
SIGHUP => "SIGHUP",
_ => "UNKNOWN",
};
loop {
let signal_name;
tokio::select! {
_ = sigterm.recv() => {
signal_name = "SIGTERM";
}
_ = sigint.recv() => {
signal_name = "SIGINT";
}
_ = sighup.recv() => {
signal_name = "SIGHUP";
}
else => {
break;
}
}
logging!(info, Type::System, "捕获到信号 {}", signal_to_str(signal));
if IS_CLEANING_UP.load(Ordering::SeqCst) {
logging!(
info,
Type::SystemSignal,
"Already shutting down, ignoring repeated signal: {}",
signal_name
);
continue;
}
IS_CLEANING_UP.store(true, Ordering::SeqCst);
logging!(info, Type::SystemSignal, "Caught signal {}", signal_name);
f().await;
logging_error!(
Type::System,
"信号 {:?} 默认处理失败",
low_level::emulate_default_handler(signal)
);
}
});
} else {
logging!(
error,
Type::System,
Type::SystemSignal,
"register shutdown signal failed, RUNTIME is not available"
);
}
@@ -1,161 +1,114 @@
use std::{future::Future, pin::Pin, sync::OnceLock};
use tauri::{AppHandle, Manager as _};
use windows_sys::Win32::{
Foundation::{HWND, LPARAM, LRESULT, WPARAM},
UI::WindowsAndMessaging::{
CW_USEDEFAULT, CreateWindowExW, DefWindowProcW, DestroyWindow, RegisterClassW,
WM_ENDSESSION, WM_QUERYENDSESSION, WNDCLASSW, WS_EX_LAYERED, WS_EX_NOACTIVATE,
WS_EX_TOOLWINDOW, WS_EX_TRANSPARENT, WS_OVERLAPPED,
},
};
use std::sync::atomic::{AtomicBool, Ordering};
use clash_verge_logging::{Type, logging};
use tokio::signal::windows;
use crate::RUNTIME;
// code refer to:
// global-hotkey (https://github.com/tauri-apps/global-hotkey)
// Global Shortcut (https://github.com/tauri-apps/plugins-workspace/tree/v2/plugins/global-shortcut)
static IS_CLEANING_UP: AtomicBool = AtomicBool::new(false);
type ShutdownHandler =
Box<dyn Fn() -> Pin<Box<dyn std::future::Future<Output = ()> + Send>> + Send + Sync>;
static SHUTDOWN_HANDLER: OnceLock<ShutdownHandler> = OnceLock::new();
struct ShutdownState {
hwnd: HWND,
}
unsafe impl Send for ShutdownState {}
unsafe impl Sync for ShutdownState {}
impl Drop for ShutdownState {
fn drop(&mut self) {
// this log not be printed, I don't know why.
logging!(info, Type::System, "正在销毁系统关闭监听窗口");
unsafe {
DestroyWindow(self.hwnd);
}
}
}
unsafe extern "system" fn shutdown_proc(
hwnd: HWND,
msg: u32,
wparam: WPARAM,
lparam: LPARAM,
) -> LRESULT {
// refer: https://learn.microsoft.com/zh-cn/windows/win32/shutdown/shutting-down#shutdown-notifications
// only perform reset operations in `WM_ENDSESSION`
match msg {
WM_QUERYENDSESSION => {
logging!(
info,
Type::System,
"System is shutting down or user is logging off."
);
}
WM_ENDSESSION => {
if let Some(handler) = SHUTDOWN_HANDLER.get() {
if let Some(Some(rt)) = RUNTIME.get() {
rt.block_on(async {
logging!(info, Type::System, "Session ended, system shutting down.");
handler().await;
logging!(info, Type::System, "resolved reset finished");
});
} else {
logging!(
error,
Type::System,
"handle shutdown signal failed, RUNTIME is not available"
);
}
} else {
logging!(
error,
Type::System,
"WM_ENDSESSION received but no shutdown handler is registered"
);
}
}
_ => {}
};
unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }
}
fn encode_wide<S: AsRef<std::ffi::OsStr>>(string: S) -> Vec<u16> {
std::os::windows::prelude::OsStrExt::encode_wide(string.as_ref())
.chain(std::iter::once(0))
.collect::<Vec<u16>>()
}
fn get_instance_handle() -> windows_sys::Win32::Foundation::HMODULE {
// Gets the instance handle by taking the address of the
// pseudo-variable created by the microsoft linker:
// https://devblogs.microsoft.com/oldnewthing/20041025-00/?p=37483
// This is preferred over GetModuleHandle(NULL) because it also works in DLLs:
// https://stackoverflow.com/questions/21718027/getmodulehandlenull-vs-hinstance
unsafe extern "C" {
static __ImageBase: windows_sys::Win32::System::SystemServices::IMAGE_DOS_HEADER;
}
unsafe { &__ImageBase as *const _ as _ }
}
pub fn register<F, Fut>(app_handle: &AppHandle, f: F)
pub fn register<F, Fut>(f: F)
where
F: Fn() -> Fut + Send + Sync + 'static,
Fut: Future + Send + 'static,
{
let _ = SHUTDOWN_HANDLER.set(Box::new(move || {
let fut = (f)();
Box::pin(async move {
fut.await;
}) as Pin<Box<dyn std::future::Future<Output = ()> + Send>>
}));
if let Some(Some(rt)) = RUNTIME.get() {
rt.spawn(async move {
let mut ctrl_c = match windows::ctrl_c() {
Ok(s) => s,
Err(e) => {
logging!(
error,
Type::SystemSignal,
"Failed to register Ctrl+C: {}",
e
);
return;
}
};
let class_name = encode_wide("global_shutdown_app");
unsafe {
let hinstance = get_instance_handle();
let mut ctrl_close = match windows::ctrl_close() {
Ok(s) => s,
Err(e) => {
logging!(
error,
Type::SystemSignal,
"Failed to register Ctrl+Close: {}",
e
);
return;
}
};
let wnd_class = WNDCLASSW {
lpfnWndProc: Some(shutdown_proc),
lpszClassName: class_name.as_ptr(),
hInstance: hinstance,
..std::mem::zeroed()
};
let mut ctrl_shutdown = match windows::ctrl_shutdown() {
Ok(s) => s,
Err(e) => {
logging!(
error,
Type::SystemSignal,
"Failed to register Ctrl+Shutdown: {}",
e
);
return;
}
};
RegisterClassW(&wnd_class);
let mut ctrl_logoff = match windows::ctrl_logoff() {
Ok(s) => s,
Err(e) => {
logging!(
error,
Type::SystemSignal,
"Failed to register Ctrl+Logoff: {}",
e
);
return;
}
};
let hwnd = CreateWindowExW(
WS_EX_NOACTIVATE | WS_EX_TRANSPARENT | WS_EX_LAYERED |
// WS_EX_TOOLWINDOW prevents this window from ever showing up in the taskbar, which
// we want to avoid. If you remove this style, this window won't show up in the
// taskbar *initially*, but it can show up at some later point. This can sometimes
// happen on its own after several hours have passed, although this has proven
// difficult to reproduce. Alternatively, it can be manually triggered by killing
// `explorer.exe` and then starting the process back up.
// It is unclear why the bug is triggered by waiting for several hours.
WS_EX_TOOLWINDOW,
class_name.as_ptr(),
std::ptr::null(),
WS_OVERLAPPED,
CW_USEDEFAULT,
0,
CW_USEDEFAULT,
0,
std::ptr::null_mut(),
std::ptr::null_mut(),
hinstance,
std::ptr::null_mut(),
loop {
let signal_name;
tokio::select! {
_ = ctrl_c.recv() => {
signal_name = "Ctrl+C";
}
_ = ctrl_close.recv() => {
signal_name = "Ctrl+Close";
}
_ = ctrl_shutdown.recv() => {
signal_name = "Ctrl+Shutdown";
}
_ = ctrl_logoff.recv() => {
signal_name = "Ctrl+Logoff";
}
}
if IS_CLEANING_UP.load(Ordering::SeqCst) {
logging!(
info,
Type::SystemSignal,
"Already shutting down, ignoring repeated signal: {}",
signal_name
);
continue;
}
IS_CLEANING_UP.store(true, Ordering::SeqCst);
logging!(
info,
Type::SystemSignal,
"Caught Windows signal: {}",
signal_name
);
f().await;
}
});
} else {
logging!(
error,
Type::SystemSignal,
"register shutdown signal failed, RUNTIME is not available"
);
if hwnd.is_null() {
logging!(error, Type::System, "failed to create shutdown window");
} else {
app_handle.manage(ShutdownState { hwnd });
}
}
}
@@ -1,8 +1,8 @@
[package]
name = "clash-verge-types"
version = "0.1.0"
edition.workspace = true
rust-version.workspace = true
edition = "2024"
rust-version = "1.91"
[dependencies]
serde = { workspace = true }
@@ -1,8 +1,8 @@
[package]
name = "tauri-plugin-clash-verge-sysinfo"
version = "0.1.0"
edition.workspace = true
rust-version.workspace = true
edition = "2024"
rust-version = "1.91"
[dependencies]
tauri = { workspace = true }
+18 -11
View File
@@ -16,7 +16,8 @@ A Clash Meta GUI built with <a href="https://github.com/tauri-apps/tauri">Tauri<
<a href="./README_es.md">Español</a> ·
<a href="./README_ru.md">Русский</a> ·
<a href="./README_ja.md">日本語</a> ·
<a href="./README_ko.md">한국어</a>
<a href="./README_ko.md">한국어</a> ·
<a href="./README_fa.md">فارسی</a>
</p>
## Preview
@@ -50,17 +51,23 @@ Join [@clash_verge_rev](https://t.me/clash_verge_re) for update announcements.
## Promotion
#### [Doggygo VPN — Performance-oriented global accelerator](https://verge.dginv.click/#/register?code=oaxsAGo6)
### ✈️ [Doggygo VPN — A Technical-Grade Proxy Service](https://verge.dginv.click/#/register?code=oaxsAGo6)
- High-performance overseas network service with free trials, discounted plans, streaming unlocks, and first-class Hysteria protocol support.
- Register through the exclusive Clash Verge link to get a 3-day trial with 1 GB of traffic per day: [Sign up](https://verge.dginv.click/#/register?code=oaxsAGo6)
- Exclusive 20% off coupon for Clash Verge users: `verge20` (limited to 500 uses)
- Discounted bundle from ¥15.8 per month for 160 GB, plus an additional 20% off for yearly billing
- Operated by an overseas team with reliable service and up to 50% revenue share
- Load-balanced clusters with high-speed dedicated routes (compatible with legacy clients), exceptionally low latency, smooth 4K playback
- First global provider to support the `Hysteria2` protocol—perfect fit for the Clash Verge client
- Supports streaming services and ChatGPT access
- Official site: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
🚀 A high-performance, overseas, technical-grade proxy service offering free trials and discounted plans, fully unlocking streaming platforms and AI services. The worlds first provider to adopt the **QUIC protocol**.
🎁 Register via the **Clash Verge exclusive invitation link** to receive **3 days of free trial**, with **1GB traffic per day**: 👉 [Register here](https://verge.dginv.click/#/register?code=oaxsAGo6)
#### **Core Advantages:**
- 📱 Self-developed iOS client (the industrys “only one”), with technology proven in production and **significant ongoing R&D investment**
- 🧑‍💻 **12-hour live customer support** (also assists with Clash Verge usage issues)
- 💰 Discounted plans at **only CNY 21 per month, 160GB traffic, 20% off with annual billing**
- 🌍 Overseas team, no risk of shutdown or exit scams, with up to **50% referral commission**
- ⚙️ **Cluster-based load balancing** architecture with **real-time load monitoring and elastic scaling**, high-speed dedicated lines (compatible with legacy clients), ultra-low latency, unaffected by peak hours, **4K streaming loads instantly**
- ⚡ The worlds first **QUIC-protocol-based proxy service**, now upgraded with the faster **Tuic protocol** (best paired with the Clash Verge client)
- 🎬 Unlocks **streaming platforms and mainstream AI services**
🌐 Official Website: 👉 [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
#### Build Infrastructure Sponsor — [YXVM Dedicated Servers](https://yxvm.com/aff.php?aff=827)
+2 -1
View File
@@ -16,7 +16,8 @@ Una interfaz gráfica para Clash Meta construida con <a href="https://github.com
<a href="./README_es.md">Español</a> ·
<a href="./README_ru.md">Русский</a> ·
<a href="./README_ja.md">日本語</a> ·
<a href="./README_ko.md">한국어</a>
<a href="./README_ko.md">한국어</a> ·
<a href="./README_fa.md">فارسی</a>
</p>
## Vista previa
+124
View File
@@ -0,0 +1,124 @@
<h1 align="center">
<img src="../src-tauri/icons/icon.png" alt="Clash" width="128" />
<br>
Continuation of <a href="https://github.com/zzzgydi/clash-verge">Clash Verge</a>
<br>
</h1>
<h3 align="center">
یک رابط کاربری گرافیکی Clash Meta که با <a href="https://github.com/tauri-apps/tauri">Tauri</a> ساخته شده است.
</h3>
<p align="center">
زبان‌ها:
<a href="../README.md">简体中文</a> ·
<a href="./README_en.md">English</a> ·
<a href="./README_es.md">Español</a> ·
<a href="./README_ru.md">Русский</a> ·
<a href="./README_ja.md">日本語</a> ·
<a href="./README_ko.md">한국어</a> ·
<a href="./README_fa.md">فارسی</a>
</p>
## پیش‌نمایش
| تاریک | روشن |
| ----------------------------------- | ------------------------------------- |
| ![Dark Preview](./preview_dark.png) | ![Light Preview](./preview_light.png) |
## نصب
برای دانلود فایل نصبی متناسب با پلتفرم خود، به [صفحه انتشار](https://github.com/clash-verge-rev/clash-verge-rev/releases) مراجعه کنید.<br> ما بسته‌هایی برای ویندوز (x64/x86)، لینوکس (x64/arm64) و macOS 10.15+ (اینتل/اپل) ارائه می‌دهیم.
#### انتخاب کانال انتشار
| Channel | توضیحات | Link |
| :---------- | :------------------------------------------------------------------------------------------------ | :------------------------------------------------------------------------------------- |
| Stable | ساخت رسمی با قابلیت اطمینان بالا، ایده‌آل برای استفاده روزانه. | [Release](https://github.com/clash-verge-rev/clash-verge-rev/releases) |
| Alpha (EOL) | نسخه‌های قدیمی (Legacy builds) برای اعتبارسنجی خط لوله انتشار (publish pipeline) استفاده می‌شوند. | [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) |
#### راهنماهای نصب و سوالات متداول
برای مراحل نصب، عیب‌یابی و سوالات متداول، [مستندات پروژه](https://clash-verge-rev.github.io/) را مطالعه کنید.
---
### کانال تلگرام
برای اطلاع از آخرین اخبار به [@clash_verge_rev](https://t.me/clash_verge_re) بپیوندید.
## تبلیغات
#### [Doggygo VPN — شتاب‌دهنده جهانی عملکردگرا](https://verge.dginv.click/#/register?code=oaxsAGo6)
- سرویس شبکه برون مرزی با عملکرد بالا به همراه دوره‌های آزمایشی رایگان، طرح‌های تخفیف‌دار، امکان باز کردن قفل استریم و پشتیبانی درجه یک از پروتکل هیستریا.
- از طریق لینک اختصاصی Clash Verge ثبت نام کنید تا یک دوره آزمایشی ۳ روزه با ۱ گیگابایت ترافیک در روز دریافت کنید: [ثبت نام](https://verge.dginv.click/#/register?code=oaxsAGo6)
- کوپن تخفیف ۲۰٪ ویژه کاربران Clash Verge: `verge20` (محدود به ۵۰۰ بار استفاده)
- بسته تخفیف‌دار از ۱۵.۸ ین در ماه برای ۱۶۰ گیگابایت، به علاوه ۲۰٪ تخفیف اضافی برای صورتحساب سالانه
- توسط یک تیم خارجی با خدمات قابل اعتماد و تا 50٪ سهم درآمد اداره می‌شود
- کلاسترهای متعادل بار با مسیرهای اختصاصی پرسرعت (سازگار با کلاینت‌های قدیمی)، تأخیر فوق‌العاده کم، پخش روان 4K
- اولین ارائه‌دهنده جهانی که از پروتکل «Hysteria2» پشتیبانی می‌کند - کاملاً مناسب برای کلاینت Clash Verge
- پشتیبانی از سرویس‌های استریم و دسترسی به ChatGPT
- وبسایت رسمی: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
#### حامی زیرساخت ساخت — [سرورهای اختصاصی YXVM](https://yxvm.com/aff.php?aff=827)
بیلدها و نسخه‌های ما روی سرورهای اختصاصی YXVM اجرا می‌شوند که منابع ممتاز، عملکرد قوی و شبکه پرسرعت را ارائه می‌دهند. اگر دانلودها سریع و استفاده از آن سریع به نظر می‌رسد، به لطف سخت‌افزار قوی است.
🧩 نکات برجسته سرورهای اختصاصی YXVM:
- 🌎 مسیرهای جهانی بهینه شده برای دانلودهای بسیار سریعتر
- 🔧 منابع فیزیکی به جای ظرفیت VPS مشترک برای حداکثر کارایی
- 🧠 عالی برای بارهای کاری پروکسی، میزبانی سرویس‌های وب/CDN، خطوط لوله CI/CD یا هرگونه کار با بار بالا
- 💡 آماده استفاده فوری با گزینه‌های متعدد مرکز داده، از جمله CN2 و IEPL
- 📦 پیکربندی مورد استفاده در این پروژه در حال فروش است - می‌توانید همان تنظیمات را تهیه کنید.
- 🎯 آیا محیط ساخت مشابهی می‌خواهید؟ [همین امروز یک سرور YXVM سفارش دهید](https://yxvm.com/aff.php?aff=827)
## ویژگی‌ها
- ساخته شده بر اساس Rust با کارایی بالا و فریم‌ورک Tauri 2
- با هسته جاسازی‌شده [Clash.Meta (mihomo)](https://github.com/MetaCubeX/mihomo) ارائه می‌شود و از تغییر به کانال «آلفا» پشتیبانی می‌کند.
- رابط کاربری تمیز و مرتب با کنترل‌های رنگ تم، آیکون‌های گروه/سینی پروکسی و `تزریق CSS`
- مدیریت پروفایل پیشرفته (ادغام و کمک‌کننده‌های اسکریپت) با نکات مربوط به سینتکس پیکربندی
- کنترل‌های پروکسی سیستم، حالت محافظت و پشتیبانی از `TUN` (آداپتور شبکه مجازی)
- ویرایشگرهای بصری برای گره‌ها و قوانین
- پشتیبان‌گیری و همگام‌سازی مبتنی بر WebDAV برای تنظیمات
### سوالات متداول
برای راهنمایی‌های مربوط به هر پلتفرم، به [صفحه سوالات متداول](https://clash-verge-rev.github.io/faq/windows.html) مراجعه کنید.
### اهدا
[پشتیبانی از توسعه Clash Verge Rev](https://github.com/sponsors/clash-verge-rev)
## توسعه
برای دستورالعمل‌های دقیق مشارکت، به [CONTRIBUTING.md](../CONTRIBUTING.md) مراجعه کنید.
پس از نصب تمام پیش‌نیازهای **Tauri**، پوسته توسعه را با دستور زیر اجرا کنید:
```shell
pnpm i
pnpm run prebuild
pnpm dev
```
## مشارکت‌ها
مشکلات و درخواست‌های pull مورد استقبال قرار می‌گیرند!
## تقدیر و تشکر
Clash Verge Rev بر اساس این پروژه‌ها ساخته شده یا از آنها الهام گرفته است:
- [zzzgydi/clash-verge](https://github.com/zzzgydi/clash-verge): یک رابط کاربری گرافیکی Clash مبتنی بر Tauri برای ویندوز، macOS و لینوکس..
- [tauri-apps/tauri](https://github.com/tauri-apps/tauri): ساخت برنامه‌های دسکتاپ کوچک‌تر، سریع‌تر و امن‌تر با رابط کاربری وب.
- [Dreamacro/clash](https://github.com/Dreamacro/clash): یک تونل مبتنی بر قانون که با زبان Go نوشته شده است.
- [MetaCubeX/mihomo](https://github.com/MetaCubeX/mihomo): یک تونل مبتنی بر قانون که با زبان Go نوشته شده است.
- [Fndroid/clash_for_windows_pkg](https://github.com/Fndroid/clash_for_windows_pkg): رابط کاربری گرافیکی Clash برای ویندوز و macOS.
- [vitejs/vite](https://github.com/vitejs/vite): ابزارهای فرانت‌اند نسل بعدی با DX فوق‌العاده سریع.
## مجوز
مجوز GPL-3.0. برای جزئیات بیشتر به [فایل مجوز](../LICENSE) مراجعه کنید.
+2 -1
View File
@@ -16,7 +16,8 @@
<a href="./README_es.md">Español</a> ·
<a href="./README_ru.md">Русский</a> ·
<a href="./README_ja.md">日本語</a> ·
<a href="./README_ko.md">한국어</a>
<a href="./README_ko.md">한국어</a> ·
<a href="./README_fa.md">فارسی</a>
</p>
## プレビュー
+2 -1
View File
@@ -16,7 +16,8 @@
<a href="./README_es.md">Español</a> ·
<a href="./README_ru.md">Русский</a> ·
<a href="./README_ja.md">日本語</a> ·
<a href="./README_ko.md">한국어</a>
<a href="./README_ko.md">한국어</a> ·
<a href="./README_fa.md">فارسی</a>
</p>
## 미리보기
+2 -1
View File
@@ -16,7 +16,8 @@ Clash Meta GUI базируется на <a href="https://github.com/tauri-apps/
<a href="./README_es.md">Español</a> ·
<a href="./README_ru.md">Русский</a> ·
<a href="./README_ja.md">日本語</a> ·
<a href="./README_ko.md">한국어</a>
<a href="./README_ko.md">한국어</a> ·
<a href="./README_fa.md">فارسی</a>
</p>
## Предпросмотр
+35 -25
View File
@@ -1,6 +1,6 @@
{
"name": "clash-verge",
"version": "2.4.4",
"version": "2.4.4-rc.1",
"license": "GPL-3.0-only",
"scripts": {
"prepare": "husky || true",
@@ -40,11 +40,11 @@
"@emotion/styled": "^11.14.1",
"@juggle/resize-observer": "^3.4.0",
"@monaco-editor/react": "^4.7.0",
"@mui/icons-material": "^7.3.5",
"@mui/icons-material": "^7.3.6",
"@mui/lab": "7.0.0-beta.17",
"@mui/material": "^7.3.5",
"@mui/material": "^7.3.6",
"@tanstack/react-table": "^8.21.3",
"@tanstack/react-virtual": "^3.13.12",
"@tanstack/react-virtual": "^3.13.13",
"@tauri-apps/api": "2.9.1",
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
"@tauri-apps/plugin-dialog": "^2.4.2",
@@ -57,32 +57,32 @@
"axios": "^1.13.2",
"dayjs": "1.11.19",
"foxact": "^0.2.49",
"i18next": "^25.7.1",
"i18next": "^25.7.3",
"js-yaml": "^4.1.1",
"lodash-es": "^4.17.21",
"lodash-es": "^4.17.22",
"monaco-editor": "^0.55.1",
"monaco-yaml": "^5.4.0",
"nanoid": "^5.1.6",
"react": "19.2.0",
"react-dom": "19.2.0",
"react": "19.2.3",
"react-dom": "19.2.3",
"react-error-boundary": "6.0.0",
"react-hook-form": "^7.67.0",
"react-i18next": "16.3.5",
"react-hook-form": "^7.68.0",
"react-i18next": "16.5.0",
"react-markdown": "10.1.0",
"react-router": "^7.10.0",
"react-virtuoso": "^4.16.1",
"swr": "^2.3.7",
"tauri-plugin-mihomo-api": "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo#main",
"react-router": "^7.11.0",
"react-virtuoso": "^4.17.0",
"swr": "^2.3.8",
"tauri-plugin-mihomo-api": "github:clash-verge-rev/tauri-plugin-mihomo#main",
"types-pac": "^1.0.3"
},
"devDependencies": {
"@actions/github": "^6.0.1",
"@eslint-react/eslint-plugin": "^2.3.11",
"@eslint/js": "^9.39.1",
"@tauri-apps/cli": "2.9.5",
"@eslint-react/eslint-plugin": "^2.3.13",
"@eslint/js": "^9.39.2",
"@tauri-apps/cli": "2.9.6",
"@types/js-yaml": "^4.0.9",
"@types/lodash-es": "^4.17.12",
"@types/node": "^24.10.1",
"@types/node": "^24.10.4",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"@vitejs/plugin-legacy": "^7.2.1",
@@ -91,13 +91,13 @@
"cli-color": "^2.0.4",
"commander": "^14.0.2",
"cross-env": "^10.1.0",
"eslint": "^9.39.1",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import-x": "^4.16.1",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"eslint-plugin-react-refresh": "^0.4.26",
"eslint-plugin-unused-imports": "^4.3.0",
"glob": "^13.0.0",
"globals": "^16.5.0",
@@ -106,13 +106,13 @@
"jiti": "^2.6.1",
"lint-staged": "^16.2.7",
"node-fetch": "^3.3.2",
"prettier": "^3.7.3",
"sass": "^1.94.2",
"prettier": "^3.7.4",
"sass": "^1.97.0",
"tar": "^7.5.2",
"terser": "^5.44.1",
"typescript": "^5.9.3",
"typescript-eslint": "^8.48.1",
"vite": "^7.2.6",
"typescript-eslint": "^8.50.0",
"vite": "^7.3.0",
"vite-plugin-svgr": "^4.5.0"
},
"lint-staged": {
@@ -125,5 +125,15 @@
]
},
"type": "module",
"packageManager": "pnpm@10.22.0"
"packageManager": "pnpm@10.26.1",
"pnpm": {
"onlyBuiltDependencies": [
"@parcel/watcher",
"@swc/core",
"core-js",
"es5-ext",
"esbuild",
"unrs-resolver"
]
}
}
+1454 -1430
View File
File diff suppressed because it is too large Load Diff
-7
View File
@@ -1,7 +0,0 @@
onlyBuiltDependencies:
- "@parcel/watcher"
- "@swc/core"
- core-js
- es5-ext
- esbuild
- unrs-resolver
-47
View File
@@ -1,47 +0,0 @@
{
"extends": ["config:recommended", ":disableDependencyDashboard"],
"baseBranches": ["dev"],
"enabledManagers": ["cargo", "npm", "github-actions"],
"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"
},
{
"description": "Group all GitHub Actions updates into a single PR",
"matchManagers": ["github-actions"],
"groupName": "github actions"
}
],
"postUpdateOptions": ["pnpmDedupe", "updateCargoLock"],
"ignoreDeps": ["criterion"]
}
+50
View File
@@ -0,0 +1,50 @@
{
extends: ["config:recommended", ":disableDependencyDashboard"],
baseBranches: ["dev"],
enabledManagers: ["cargo", "npm", "github-actions"],
labels: ["dependencies"],
ignorePaths: [
"**/node_modules/**",
"**/bower_components/**",
"**/vendor/**",
"**/__tests__/**",
"**/test/**",
"**/tests/**",
"**/__fixtures__/**",
"shared/**",
],
rangeStrategy: "replace",
packageRules: [
{
matchUpdateTypes: ["patch"],
automerge: true,
},
{
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",
},
{
description: "Group all GitHub Actions updates into a single PR",
matchManagers: ["github-actions"],
groupName: "github actions",
},
],
postUpdateOptions: ["pnpmDedupe"],
ignoreDeps: ["criterion"],
}
@@ -77,9 +77,9 @@ const IGNORED_KEY_PREFIXES = new Set([
const NOTICE_METHOD_NAMES = new Set(["success", "error", "info", "warning"]);
const NOTICE_SERVICE_IDENTIFIERS = new Set([
"@/services/noticeService",
"./noticeService",
"../services/noticeService",
"@/services/notice-service",
"./notice-service",
"../services/notice-service",
]);
const WHITELIST_KEYS = new Set([
@@ -3,7 +3,7 @@ import fsp from "fs/promises";
import { createRequire } from "module";
import path from "path";
import { getOctokit, context } from "@actions/github";
import { context, getOctokit } from "@actions/github";
import AdmZip from "adm-zip";
const target = process.argv.slice(2)[0];
@@ -50,9 +50,9 @@ async function resolvePortable() {
zip.addLocalFolder(
path.join(
releaseDir,
`Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${arch}`,
`Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${arch}`,
),
`Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${arch}`,
`Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${arch}`,
);
zip.addLocalFolder(configDir, ".config");
+1 -12
View File
@@ -576,7 +576,7 @@ const resolveServicePermission = async () => {
};
// =======================
// Other resource resolvers (service, mmdb, geosite, geoip, enableLoopback, sysproxy)
// Other resource resolvers (service, mmdb, geosite, geoip, enableLoopback)
// =======================
const SERVICE_URL = `https://github.com/clash-verge-rev/clash-verge-service-ipc/releases/download/${SIDECAR_HOST}`;
const resolveService = () => {
@@ -624,11 +624,6 @@ const resolveEnableLoopback = () =>
file: "enableLoopback.exe",
downloadURL: `https://github.com/Kuingsmile/uwp-tool/releases/download/latest/enableLoopback.exe`,
});
const resolveWinSysproxy = () =>
resolveResource({
file: "sysproxy.exe",
downloadURL: `https://github.com/clash-verge-rev/sysproxy/releases/download/${arch}/sysproxy.exe`,
});
const resolveSetDnsScript = () =>
resolveResource({
@@ -676,12 +671,6 @@ const tasks = [
retry: 5,
unixOnly: platform === "linux" || platform === "darwin",
},
{
name: "windows-sysproxy",
func: resolveWinSysproxy,
retry: 5,
winOnly: true,
},
{
name: "set_dns_script",
func: resolveSetDnsScript,
+5 -4
View File
@@ -1,14 +1,14 @@
[package]
name = "clash-verge"
version = "2.4.4"
version = "2.4.4-rc.1"
description = "clash verge"
authors = ["zzzgydi", "Tunglies", "wonfen", "MystiPanda"]
license = "GPL-3.0-only"
repository = "https://github.com/clash-verge-rev/clash-verge-rev.git"
default-run = "clash-verge"
build = "build.rs"
edition = { workspace = true }
rust-version = { workspace = true }
edition = "2024"
rust-version = "1.91"
[lib]
name = "app_lib"
@@ -22,6 +22,7 @@ tauri-dev = ["clash-verge-logging/tauri-dev"]
tokio-trace = ["console-subscriber"]
clippy = ["tauri/test"]
tracing = []
tracing-full = []
[package.metadata.bundle]
identifier = "io.github.clash-verge-rev.clash-verge-rev"
@@ -93,7 +94,7 @@ tauri-plugin-devtools = { version = "2.0.1" }
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo" }
clash_verge_logger = { git = "https://github.com/clash-verge-rev/clash-verge-logger" }
async-trait = "0.1.89"
clash_verge_service_ipc = { version = "2.0.21", features = [
clash_verge_service_ipc = { version = "2.0.26", features = [
"client",
], git = "https://github.com/clash-verge-rev/clash-verge-service-ipc" }
arc-swap = "1.7.1"
+3 -1
View File
@@ -25,7 +25,9 @@ notifications:
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminPrompt: Installing the service requires administrator privileges.
adminInstallPrompt: Installing the service requires administrator privileges.
adminUninstallPrompt: Uninstalling the service requires administrator privileges.
tray:
dashboard: Dashboard
ruleMode: Rule Mode
+2 -1
View File
@@ -25,7 +25,8 @@ notifications:
title: 应用已隐藏
body: Clash Verge 正在后台运行。
service:
adminPrompt: 安装服务需要管理员权限
adminInstallPrompt: 安装 Clash Verge 服务需要管理员权限
adminUninstallPrompt: 卸载 Clash Verge 服务需要管理员权限
tray:
dashboard: 仪表板
ruleMode: 规则模式
+2 -1
View File
@@ -25,7 +25,8 @@ notifications:
title: 應用已隱藏
body: Clash Verge 正在背景執行。
service:
adminPrompt: 安裝服務需要管理員權限
adminInstallPrompt: 安裝服務需要管理員權限
adminUninstallPrompt: 卸载服務需要管理員權限
tray:
dashboard: 儀表板
ruleMode: 規則模式
+1 -1
View File
@@ -1,4 +1,4 @@
max_width = 100
max_width = 120
hard_tabs = false
tab_spaces = 4
newline_style = "Auto"
+3 -11
View File
@@ -89,10 +89,7 @@ pub fn get_portable_flag() -> bool {
/// 获取应用目录
#[tauri::command]
pub fn get_app_dir() -> CmdResult<String> {
let app_home_dir = dirs::app_home_dir()
.stringify_err()?
.to_string_lossy()
.into();
let app_home_dir = dirs::app_home_dir().stringify_err()?.to_string_lossy().into();
Ok(app_home_dir)
}
@@ -105,10 +102,7 @@ pub fn get_auto_launch_status() -> CmdResult<bool> {
/// 下载图标缓存
#[tauri::command]
pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String> {
let icon_cache_dir = dirs::app_home_dir()
.stringify_err()?
.join("icons")
.join("cache");
let icon_cache_dir = dirs::app_home_dir().stringify_err()?.join("icons").join("cache");
let icon_path = icon_cache_dir.join(name.as_str());
if icon_path.exists() {
@@ -134,9 +128,7 @@ pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String>
let content = response.bytes().await.stringify_err()?;
let is_html = content.len() > 15
&& (content.starts_with(b"<!DOCTYPE html")
|| content.starts_with(b"<html")
|| content.starts_with(b"<?xml"));
&& (content.starts_with(b"<!DOCTYPE html") || content.starts_with(b"<html") || content.starts_with(b"<?xml"));
if is_image && !is_html {
{
+1 -3
View File
@@ -30,7 +30,5 @@ pub async fn restore_local_backup(filename: String) -> CmdResult<()> {
/// Export local backup to a user selected destination
#[tauri::command]
pub async fn export_local_backup(filename: String, destination: String) -> CmdResult<()> {
feat::export_local_backup(filename, destination)
.await
.stringify_err()
feat::export_local_backup(filename, destination).await.stringify_err()
}
+22 -58
View File
@@ -46,26 +46,18 @@ pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>>
match CoreManager::global().change_core(&clash_core).await {
Ok(_) => {
logging_error!(
Type::Core,
Config::profiles().await.latest_arc().save_file().await
);
logging_error!(Type::Core, Config::profiles().await.latest_arc().save_file().await);
// 切换内核后重启内核
match CoreManager::global().restart_core().await {
Ok(_) => {
logging!(
info,
Type::Core,
"core changed and restarted to {clash_core}"
);
logging!(info, Type::Core, "core changed and restarted to {clash_core}");
handle::Handle::notice_message("config_core::change_success", clash_core);
handle::Handle::refresh_clash();
Ok(None)
}
Err(err) => {
let error_msg: String =
format!("Core changed but failed to restart: {err}").into();
let error_msg: String = format!("Core changed but failed to restart: {err}").into();
handle::Handle::notice_message("config_core::change_error", error_msg.clone());
logging!(error, Type::Core, "{error_msg}");
Ok(Some(error_msg))
@@ -94,10 +86,7 @@ pub async fn start_core() -> CmdResult {
/// 关闭核心
#[tauri::command]
pub async fn stop_core() -> CmdResult {
logging_error!(
Type::Core,
Config::profiles().await.latest_arc().save_file().await
);
logging_error!(Type::Core, Config::profiles().await.latest_arc().save_file().await);
let result = CoreManager::global().stop_core().await.stringify_err();
if result.is_ok() {
handle::Handle::refresh_clash();
@@ -108,10 +97,7 @@ pub async fn stop_core() -> CmdResult {
/// 重启核心
#[tauri::command]
pub async fn restart_core() -> CmdResult {
logging_error!(
Type::Core,
Config::profiles().await.latest_arc().save_file().await
);
logging_error!(Type::Core, Config::profiles().await.latest_arc().save_file().await);
let result = CoreManager::global().restart_core().await.stringify_err();
if result.is_ok() {
handle::Handle::refresh_clash();
@@ -140,9 +126,7 @@ pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
use tokio::fs;
// 获取DNS配置文件路径
let dns_path = dirs::app_home_dir()
.stringify_err()?
.join(constants::files::DNS_CONFIG);
let dns_path = dirs::app_home_dir().stringify_err()?.join(constants::files::DNS_CONFIG);
// 保存DNS配置到文件
let yaml_str = serde_yaml_ng::to_string(&dns_config).stringify_err()?;
@@ -157,9 +141,7 @@ pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
pub async fn apply_dns_config(apply: bool) -> CmdResult {
if apply {
// 读取DNS配置文件
let dns_path = dirs::app_home_dir()
.stringify_err()?
.join(constants::files::DNS_CONFIG);
let dns_path = dirs::app_home_dir().stringify_err()?.join(constants::files::DNS_CONFIG);
if !dns_path.exists() {
logging!(warn, Type::Config, "DNS config file not found");
@@ -171,10 +153,9 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult {
})?;
// 解析DNS配置
let patch_config = serde_yaml_ng::from_str::<serde_yaml_ng::Mapping>(&dns_yaml)
.stringify_err_log(|e| {
logging!(error, Type::Config, "Failed to parse DNS config: {e}");
})?;
let patch_config = serde_yaml_ng::from_str::<serde_yaml_ng::Mapping>(&dns_yaml).stringify_err_log(|e| {
logging!(error, Type::Config, "Failed to parse DNS config: {e}");
})?;
logging!(info, Type::Config, "Applying DNS config from file");
@@ -194,35 +175,25 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult {
})?;
// 应用新配置
CoreManager::global()
.update_config()
.await
.stringify_err_log(|err| {
let err = format!("Failed to apply config with DNS: {err}");
logging!(error, Type::Config, "{err}");
})?;
CoreManager::global().update_config().await.stringify_err_log(|err| {
let err = format!("Failed to apply config with DNS: {err}");
logging!(error, Type::Config, "{err}");
})?;
logging!(info, Type::Config, "DNS config successfully applied");
} else {
// 当关闭DNS设置时,重新生成配置(不加载DNS配置文件)
logging!(
info,
Type::Config,
"DNS settings disabled, regenerating config"
);
logging!(info, Type::Config, "DNS settings disabled, regenerating config");
Config::generate().await.stringify_err_log(|err| {
let err = format!("Failed to regenerate config: {err}");
logging!(error, Type::Config, "{err}");
})?;
CoreManager::global()
.update_config()
.await
.stringify_err_log(|err| {
let err = format!("Failed to apply regenerated config: {err}");
logging!(error, Type::Config, "{err}");
})?;
CoreManager::global().update_config().await.stringify_err_log(|err| {
let err = format!("Failed to apply regenerated config: {err}");
logging!(error, Type::Config, "{err}");
})?;
logging!(info, Type::Config, "Config regenerated successfully");
}
@@ -236,9 +207,7 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult {
pub fn check_dns_config_exists() -> CmdResult<bool> {
use crate::utils::dirs;
let dns_path = dirs::app_home_dir()
.stringify_err()?
.join(constants::files::DNS_CONFIG);
let dns_path = dirs::app_home_dir().stringify_err()?.join(constants::files::DNS_CONFIG);
Ok(dns_path.exists())
}
@@ -249,9 +218,7 @@ pub async fn get_dns_config_content() -> CmdResult<String> {
use crate::utils::dirs;
use tokio::fs;
let dns_path = dirs::app_home_dir()
.stringify_err()?
.join(constants::files::DNS_CONFIG);
let dns_path = dirs::app_home_dir().stringify_err()?.join(constants::files::DNS_CONFIG);
if !fs::try_exists(&dns_path).await.stringify_err()? {
return Err("DNS config file not found".into());
@@ -279,9 +246,6 @@ pub async fn validate_dns_config() -> CmdResult<(bool, String)> {
#[tauri::command]
pub async fn get_clash_logs() -> CmdResult<Vec<CompactString>> {
let logs = CoreManager::global()
.get_clash_logs()
.await
.unwrap_or_default();
let logs = CoreManager::global().get_clash_logs().await.unwrap_or_default();
Ok(logs)
}
@@ -12,9 +12,12 @@ pub(super) async fn check_bahamut_anime(client: &Client) -> UnlockItem {
let client_with_cookies = match Client::builder()
.use_rustls_tls()
.user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36")
.user_agent(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
)
.cookie_provider(Arc::clone(&cookie_store))
.build() {
.build()
{
Ok(client) => client,
Err(e) => {
logging!(
@@ -59,8 +62,7 @@ pub(super) async fn check_bahamut_anime(client: &Client) -> UnlockItem {
};
}
let url =
format!("https://ani.gamer.com.tw/ajax/token.php?adID=89422&sn=37783&device={device_id}");
let url = format!("https://ani.gamer.com.tw/ajax/token.php?adID=89422&sn=37783&device={device_id}");
let token_result = match client_with_cookies.get(&url).send().await {
Ok(response) => match response.text().await {
@@ -85,21 +87,14 @@ pub(super) async fn check_bahamut_anime(client: &Client) -> UnlockItem {
};
}
let region = match client_with_cookies
.get("https://ani.gamer.com.tw/")
.send()
.await
{
let region = match client_with_cookies.get("https://ani.gamer.com.tw/").send().await {
Ok(response) => match response.text().await {
Ok(body) => match Regex::new(r#"data-geo="([^"]+)"#) {
Ok(region_re) => region_re
.captures(&body)
.and_then(|caps| caps.get(1))
.map(|m| {
let country_code = m.as_str();
let emoji = country_code_to_emoji(country_code);
format!("{emoji}{country_code}")
}),
Ok(region_re) => region_re.captures(&body).and_then(|caps| caps.get(1)).map(|m| {
let country_code = m.as_str();
let emoji = country_code_to_emoji(country_code);
format!("{emoji}{country_code}")
}),
Err(e) => {
logging!(
error,
@@ -9,8 +9,7 @@ use super::utils::{country_code_to_emoji, get_local_date_string};
#[allow(clippy::cognitive_complexity)]
pub(super) async fn check_disney_plus(client: &Client) -> UnlockItem {
let device_api_url = "https://disney.api.edge.bamgrid.com/devices";
let auth_header =
"Bearer ZGlzbmV5JmJyb3dzZXImMS4wLjA.Cu56AgSfBTDag5NiRA81oLHkDZfu5L3CKadnefEAY84";
let auth_header = "Bearer ZGlzbmV5JmJyb3dzZXImMS4wLjA.Cu56AgSfBTDag5NiRA81oLHkDZfu5L3CKadnefEAY84";
let device_req_body = serde_json::json!({
"deviceFamily": "browser",
@@ -39,12 +38,7 @@ pub(super) async fn check_disney_plus(client: &Client) -> UnlockItem {
let device_response = match device_result {
Ok(response) => response,
Err(e) => {
logging!(
error,
Type::Network,
"Failed to get Disney+ device response: {}",
e
);
logging!(error, Type::Network, "Failed to get Disney+ device response: {}", e);
return UnlockItem {
name: "Disney+".to_string(),
status: "Failed (Network Connection)".to_string(),
@@ -120,18 +114,12 @@ pub(super) async fn check_disney_plus(client: &Client) -> UnlockItem {
}
};
let token_body = [
(
"grant_type",
"urn:ietf:params:oauth:grant-type:token-exchange",
),
("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange"),
("latitude", "0"),
("longitude", "0"),
("platform", "browser"),
("subject_token", assertion_str.as_str()),
(
"subject_token_type",
"urn:bamtech:params:oauth:token-type:device",
),
("subject_token_type", "urn:bamtech:params:oauth:token-type:device"),
];
let token_result = client
@@ -154,12 +142,7 @@ pub(super) async fn check_disney_plus(client: &Client) -> UnlockItem {
let token_response = match token_result {
Ok(response) => response,
Err(e) => {
logging!(
error,
Type::Network,
"Failed to get Disney+ token response: {}",
e
);
logging!(error, Type::Network, "Failed to get Disney+ token response: {}", e);
return UnlockItem {
name: "Disney+".to_string(),
status: "Failed (Network Connection)".to_string(),
@@ -264,12 +247,7 @@ pub(super) async fn check_disney_plus(client: &Client) -> UnlockItem {
let graphql_response = match graphql_result {
Ok(response) => response,
Err(e) => {
logging!(
error,
Type::Network,
"Failed to get Disney+ GraphQL response: {}",
e
);
logging!(error, Type::Network, "Failed to get Disney+ GraphQL response: {}", e);
return UnlockItem {
name: "Disney+".to_string(),
status: "Failed (Network Connection)".to_string(),
@@ -18,12 +18,7 @@ pub(super) async fn check_gemini(client: &Client) -> UnlockItem {
let re = match Regex::new(r#",2,1,200,"([A-Z]{3})""#) {
Ok(re) => re,
Err(e) => {
logging!(
error,
Type::Network,
"Failed to compile Gemini regex: {}",
e
);
logging!(error, Type::Network, "Failed to compile Gemini regex: {}", e);
return UnlockItem {
name: "Gemini".to_string(),
status: "Failed".to_string(),

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