mirror of
https://github.com/bolucat/Archive.git
synced 2026-04-22 16:07:49 +08:00
Update On Fri Dec 19 19:41:39 CET 2025
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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], ",")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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/
|
||||
|
||||
Generated
+2
-2
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
+43
@@ -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>
|
||||
)
|
||||
}
|
||||
+43
@@ -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>
|
||||
)
|
||||
}
|
||||
+68
@@ -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>
|
||||
)
|
||||
}
|
||||
+101
@@ -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>
|
||||
)
|
||||
}
|
||||
+21
-1
@@ -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 />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
+27
@@ -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>
|
||||
)
|
||||
}
|
||||
+50
@@ -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>
|
||||
)
|
||||
}
|
||||
+68
@@ -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>
|
||||
)
|
||||
}
|
||||
+58
@@ -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>
|
||||
)
|
||||
}
|
||||
+12
@@ -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,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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
Generated
+914
-17
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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:
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
Generated
+139
-168
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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 }
|
||||
|
||||
@@ -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 world’s 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 industry’s “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 world’s 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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
## پیشنمایش
|
||||
|
||||
| تاریک | روشن |
|
||||
| ----------------------------------- | ------------------------------------- |
|
||||
|  |  |
|
||||
|
||||
## نصب
|
||||
|
||||
برای دانلود فایل نصبی متناسب با پلتفرم خود، به [صفحه انتشار](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) مراجعه کنید.
|
||||
@@ -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>
|
||||
|
||||
## プレビュー
|
||||
|
||||
@@ -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>
|
||||
|
||||
## 미리보기
|
||||
|
||||
@@ -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>
|
||||
## Предпросмотр
|
||||
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+1454
-1430
File diff suppressed because it is too large
Load Diff
@@ -1,7 +0,0 @@
|
||||
onlyBuiltDependencies:
|
||||
- "@parcel/watcher"
|
||||
- "@swc/core"
|
||||
- core-js
|
||||
- es5-ext
|
||||
- esbuild
|
||||
- unrs-resolver
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -25,7 +25,8 @@ notifications:
|
||||
title: 应用已隐藏
|
||||
body: Clash Verge 正在后台运行。
|
||||
service:
|
||||
adminPrompt: 安装服务需要管理员权限
|
||||
adminInstallPrompt: 安装 Clash Verge 服务需要管理员权限
|
||||
adminUninstallPrompt: 卸载 Clash Verge 服务需要管理员权限
|
||||
tray:
|
||||
dashboard: 仪表板
|
||||
ruleMode: 规则模式
|
||||
|
||||
@@ -25,7 +25,8 @@ notifications:
|
||||
title: 應用已隱藏
|
||||
body: Clash Verge 正在背景執行。
|
||||
service:
|
||||
adminPrompt: 安裝服務需要管理員權限
|
||||
adminInstallPrompt: 安裝服務需要管理員權限
|
||||
adminUninstallPrompt: 卸载服務需要管理員權限
|
||||
tray:
|
||||
dashboard: 儀表板
|
||||
ruleMode: 規則模式
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
max_width = 100
|
||||
max_width = 120
|
||||
hard_tabs = false
|
||||
tab_spaces = 4
|
||||
newline_style = "Auto"
|
||||
|
||||
@@ -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 {
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user