mirror of
https://github.com/bolucat/Archive.git
synced 2026-04-23 00:17:16 +08:00
Update On Mon Feb 3 19:34:33 CET 2025
This commit is contained in:
@@ -902,3 +902,4 @@ Update On Thu Jan 30 19:32:29 CET 2025
|
||||
Update On Fri Jan 31 19:32:11 CET 2025
|
||||
Update On Sat Feb 1 19:32:16 CET 2025
|
||||
Update On Sun Feb 2 19:31:20 CET 2025
|
||||
Update On Mon Feb 3 19:34:24 CET 2025
|
||||
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
"github.com/metacubex/mihomo/component/resolver"
|
||||
tlsC "github.com/metacubex/mihomo/component/tls"
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
"github.com/metacubex/mihomo/log"
|
||||
"github.com/metacubex/mihomo/transport/gun"
|
||||
"github.com/metacubex/mihomo/transport/socks5"
|
||||
"github.com/metacubex/mihomo/transport/vless"
|
||||
@@ -513,8 +512,6 @@ func NewVless(option VlessOption) (*Vless, error) {
|
||||
if option.Flow != vless.XRV {
|
||||
return nil, fmt.Errorf("unsupported xtls flow type: %s", option.Flow)
|
||||
}
|
||||
|
||||
log.Warnln("To use %s, ensure your server is upgrade to Xray-core v1.8.0+", vless.XRV)
|
||||
addons = &vless.Addons{
|
||||
Flow: option.Flow,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package generater
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
|
||||
"github.com/gofrs/uuid/v5"
|
||||
)
|
||||
|
||||
func Main(args []string) {
|
||||
if len(args) < 1 {
|
||||
panic("Using: generate uuid/reality-keypair/wg-keypair")
|
||||
}
|
||||
switch args[0] {
|
||||
case "uuid":
|
||||
newUUID, err := uuid.NewV4()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println(newUUID.String())
|
||||
case "reality-keypair":
|
||||
privateKey, err := GeneratePrivateKey()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
publicKey := privateKey.PublicKey()
|
||||
fmt.Println("PrivateKey: " + base64.RawURLEncoding.EncodeToString(privateKey[:]))
|
||||
fmt.Println("PublicKey: " + base64.RawURLEncoding.EncodeToString(publicKey[:]))
|
||||
case "wg-keypair":
|
||||
privateKey, err := GeneratePrivateKey()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println("PrivateKey: " + privateKey.String())
|
||||
fmt.Println("PublicKey: " + privateKey.PublicKey().String())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// Copy from https://github.com/WireGuard/wgctrl-go/blob/a9ab2273dd1075ea74b88c76f8757f8b4003fcbf/wgtypes/types.go#L71-L155
|
||||
|
||||
package generater
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/crypto/curve25519"
|
||||
)
|
||||
|
||||
// KeyLen is the expected key length for a WireGuard key.
|
||||
const KeyLen = 32 // wgh.KeyLen
|
||||
|
||||
// A Key is a public, private, or pre-shared secret key. The Key constructor
|
||||
// functions in this package can be used to create Keys suitable for each of
|
||||
// these applications.
|
||||
type Key [KeyLen]byte
|
||||
|
||||
// GenerateKey generates a Key suitable for use as a pre-shared secret key from
|
||||
// a cryptographically safe source.
|
||||
//
|
||||
// The output Key should not be used as a private key; use GeneratePrivateKey
|
||||
// instead.
|
||||
func GenerateKey() (Key, error) {
|
||||
b := make([]byte, KeyLen)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return Key{}, fmt.Errorf("wgtypes: failed to read random bytes: %v", err)
|
||||
}
|
||||
|
||||
return NewKey(b)
|
||||
}
|
||||
|
||||
// GeneratePrivateKey generates a Key suitable for use as a private key from a
|
||||
// cryptographically safe source.
|
||||
func GeneratePrivateKey() (Key, error) {
|
||||
key, err := GenerateKey()
|
||||
if err != nil {
|
||||
return Key{}, err
|
||||
}
|
||||
|
||||
// Modify random bytes using algorithm described at:
|
||||
// https://cr.yp.to/ecdh.html.
|
||||
key[0] &= 248
|
||||
key[31] &= 127
|
||||
key[31] |= 64
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// NewKey creates a Key from an existing byte slice. The byte slice must be
|
||||
// exactly 32 bytes in length.
|
||||
func NewKey(b []byte) (Key, error) {
|
||||
if len(b) != KeyLen {
|
||||
return Key{}, fmt.Errorf("wgtypes: incorrect key size: %d", len(b))
|
||||
}
|
||||
|
||||
var k Key
|
||||
copy(k[:], b)
|
||||
|
||||
return k, nil
|
||||
}
|
||||
|
||||
// ParseKey parses a Key from a base64-encoded string, as produced by the
|
||||
// Key.String method.
|
||||
func ParseKey(s string) (Key, error) {
|
||||
b, err := base64.StdEncoding.DecodeString(s)
|
||||
if err != nil {
|
||||
return Key{}, fmt.Errorf("wgtypes: failed to parse base64-encoded key: %v", err)
|
||||
}
|
||||
|
||||
return NewKey(b)
|
||||
}
|
||||
|
||||
// PublicKey computes a public key from the private key k.
|
||||
//
|
||||
// PublicKey should only be called when k is a private key.
|
||||
func (k Key) PublicKey() Key {
|
||||
var (
|
||||
pub [KeyLen]byte
|
||||
priv = [KeyLen]byte(k)
|
||||
)
|
||||
|
||||
// ScalarBaseMult uses the correct base value per https://cr.yp.to/ecdh.html,
|
||||
// so no need to specify it.
|
||||
curve25519.ScalarBaseMult(&pub, &priv)
|
||||
|
||||
return Key(pub)
|
||||
}
|
||||
|
||||
// String returns the base64-encoded string representation of a Key.
|
||||
//
|
||||
// ParseKey can be used to produce a new Key from this string.
|
||||
func (k Key) String() string {
|
||||
return base64.StdEncoding.EncodeToString(k[:])
|
||||
}
|
||||
@@ -25,6 +25,7 @@ const (
|
||||
SOCKS5
|
||||
SHADOWSOCKS
|
||||
VMESS
|
||||
VLESS
|
||||
REDIR
|
||||
TPROXY
|
||||
TUNNEL
|
||||
@@ -69,6 +70,8 @@ func (t Type) String() string {
|
||||
return "ShadowSocks"
|
||||
case VMESS:
|
||||
return "Vmess"
|
||||
case VLESS:
|
||||
return "Vless"
|
||||
case REDIR:
|
||||
return "Redir"
|
||||
case TPROXY:
|
||||
@@ -103,6 +106,8 @@ func ParseType(t string) (*Type, error) {
|
||||
res = SHADOWSOCKS
|
||||
case "VMESS":
|
||||
res = VMESS
|
||||
case "VLESS":
|
||||
res = VLESS
|
||||
case "REDIR":
|
||||
res = REDIR
|
||||
case "TPROXY":
|
||||
|
||||
@@ -1176,6 +1176,30 @@ listeners:
|
||||
network: [tcp, udp]
|
||||
target: target.com
|
||||
|
||||
- name: vless-in-1
|
||||
type: vless
|
||||
port: 10817
|
||||
listen: 0.0.0.0
|
||||
# rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules
|
||||
# proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错)
|
||||
users:
|
||||
- username: 1
|
||||
uuid: 9d0cb9d0-964f-4ef6-897d-6c6b3ccf9e68
|
||||
flow: xtls-rprx-vision
|
||||
# ws-path: "/" # 如果不为空则开启 websocket 传输层
|
||||
# 下面两项如果填写则开启 tls(需要同时填写)
|
||||
# certificate: ./server.crt
|
||||
# private-key: ./server.key
|
||||
# 如果填写reality-config则开启reality(注意不可与certificate和private-key同时填写)
|
||||
reality-config:
|
||||
dest: test.com:443
|
||||
private-key: jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0 # 可由 mihomo generate reality-keypair 命令生成
|
||||
short-id:
|
||||
- 0123456789abcdef
|
||||
server-names:
|
||||
- test.com
|
||||
### 注意,对于vless listener, 至少需要填写 “certificate和private-key” 或 “reality-config” 的其中一项 ###
|
||||
|
||||
- name: tun-in-1
|
||||
type: tun
|
||||
# rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules
|
||||
|
||||
+2
-1
@@ -27,7 +27,7 @@ require (
|
||||
github.com/metacubex/sing-shadowsocks v0.2.8
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.2
|
||||
github.com/metacubex/sing-tun v0.4.5
|
||||
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9
|
||||
github.com/metacubex/sing-vmess v0.1.14-0.20250203033000-f61322b3dbe3
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589
|
||||
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422
|
||||
github.com/metacubex/utls v1.6.6
|
||||
@@ -40,6 +40,7 @@ require (
|
||||
github.com/sagernet/cors v1.2.1
|
||||
github.com/sagernet/fswatch v0.1.1
|
||||
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a
|
||||
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691
|
||||
github.com/sagernet/sing v0.5.1
|
||||
github.com/sagernet/sing-mux v0.2.1
|
||||
github.com/sagernet/sing-shadowtls v0.1.5
|
||||
|
||||
+4
-2
@@ -122,8 +122,8 @@ github.com/metacubex/sing-shadowsocks2 v0.2.2 h1:eaf42uVx4Lr21S6MDYs0ZdTvGA0GEhD
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.2/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q=
|
||||
github.com/metacubex/sing-tun v0.4.5 h1:kWSyQzuzHI40r50OFBczfWIDvMBMy1RIk+JsXeBPRB0=
|
||||
github.com/metacubex/sing-tun v0.4.5/go.mod h1:V0N4rr0dWPBEE20ESkTXdbtx2riQYcb6YtwC5w/9wl0=
|
||||
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 h1:OAXiCosqY8xKDp3pqTW3qbrCprZ1l6WkrXSFSCwyY4I=
|
||||
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9/go.mod h1:olVkD4FChQ5gKMHG4ZzuD7+fMkJY1G8vwOKpRehjrmY=
|
||||
github.com/metacubex/sing-vmess v0.1.14-0.20250203033000-f61322b3dbe3 h1:2kq6azIvsTjTnyw66xXDl5zMzIJqF7GTbvLpkroHssg=
|
||||
github.com/metacubex/sing-vmess v0.1.14-0.20250203033000-f61322b3dbe3/go.mod h1:nE7Mdzj/QUDwgRi/8BASPtsxtIFZTHA4Yst5GgwbGCQ=
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 h1:Z6bNy0HLTjx6BKIkV48sV/yia/GP8Bnyb5JQuGgSGzg=
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589/go.mod h1:4NclTLIZuk+QkHVCGrP87rHi/y8YjgPytxTgApJNMhc=
|
||||
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 h1:zGeQt3UyNydIVrMRB97AA5WsYEau/TyCnRtTf1yUmJY=
|
||||
@@ -170,6 +170,8 @@ github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZN
|
||||
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
|
||||
github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I=
|
||||
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
|
||||
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc=
|
||||
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU=
|
||||
github.com/sagernet/sing-mux v0.2.1 h1:N/3MHymfnFZRd29tE3TaXwPUVVgKvxhtOkiCMLp9HVo=
|
||||
github.com/sagernet/sing-mux v0.2.1/go.mod h1:dm3BWL6NvES9pbib7llpylrq7Gq+LjlzG+0RacdxcyE=
|
||||
github.com/sagernet/sing-shadowtls v0.1.5 h1:uXxmq/HXh8DIiBGLzpMjCbWnzIAFs+lIxiTOjdgG5qo=
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/metacubex/mihomo/listener/sing"
|
||||
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
type VlessUser struct {
|
||||
Username string
|
||||
UUID string
|
||||
Flow string
|
||||
}
|
||||
|
||||
type VlessServer struct {
|
||||
Enable bool
|
||||
Listen string
|
||||
Users []VlessUser
|
||||
WsPath string
|
||||
Certificate string
|
||||
PrivateKey string
|
||||
RealityConfig RealityConfig
|
||||
MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"`
|
||||
}
|
||||
|
||||
func (t VlessServer) String() string {
|
||||
b, _ := json.Marshal(t)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
type RealityConfig struct {
|
||||
Dest string
|
||||
PrivateKey string
|
||||
ShortID []string
|
||||
ServerNames []string
|
||||
MaxTimeDifference int
|
||||
Proxy string
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package inbound
|
||||
|
||||
import (
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
LC "github.com/metacubex/mihomo/listener/config"
|
||||
"github.com/metacubex/mihomo/listener/sing_vless"
|
||||
"github.com/metacubex/mihomo/log"
|
||||
)
|
||||
|
||||
type VlessOption struct {
|
||||
BaseOption
|
||||
Users []VlessUser `inbound:"users"`
|
||||
WsPath string `inbound:"ws-path,omitempty"`
|
||||
Certificate string `inbound:"certificate,omitempty"`
|
||||
PrivateKey string `inbound:"private-key,omitempty"`
|
||||
RealityConfig RealityConfig `inbound:"reality-config,omitempty"`
|
||||
MuxOption MuxOption `inbound:"mux-option,omitempty"`
|
||||
}
|
||||
|
||||
type VlessUser struct {
|
||||
Username string `inbound:"username,omitempty"`
|
||||
UUID string `inbound:"uuid"`
|
||||
Flow string `inbound:"flow,omitempty"`
|
||||
}
|
||||
|
||||
type RealityConfig struct {
|
||||
Dest string `inbound:"dest"`
|
||||
PrivateKey string `inbound:"private-key"`
|
||||
ShortID []string `inbound:"short-id"`
|
||||
ServerNames []string `inbound:"server-names"`
|
||||
MaxTimeDifference int `inbound:"max-time-difference,omitempty"`
|
||||
Proxy string `inbound:"proxy,omitempty"`
|
||||
}
|
||||
|
||||
func (c RealityConfig) Build() LC.RealityConfig {
|
||||
return LC.RealityConfig{
|
||||
Dest: c.Dest,
|
||||
PrivateKey: c.PrivateKey,
|
||||
ShortID: c.ShortID,
|
||||
ServerNames: c.ServerNames,
|
||||
MaxTimeDifference: c.MaxTimeDifference,
|
||||
Proxy: c.Proxy,
|
||||
}
|
||||
}
|
||||
|
||||
func (o VlessOption) Equal(config C.InboundConfig) bool {
|
||||
return optionToString(o) == optionToString(config)
|
||||
}
|
||||
|
||||
type Vless struct {
|
||||
*Base
|
||||
config *VlessOption
|
||||
l C.MultiAddrListener
|
||||
vs LC.VlessServer
|
||||
}
|
||||
|
||||
func NewVless(options *VlessOption) (*Vless, error) {
|
||||
base, err := NewBase(&options.BaseOption)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
users := make([]LC.VlessUser, len(options.Users))
|
||||
for i, v := range options.Users {
|
||||
users[i] = LC.VlessUser{
|
||||
Username: v.Username,
|
||||
UUID: v.UUID,
|
||||
Flow: v.Flow,
|
||||
}
|
||||
}
|
||||
return &Vless{
|
||||
Base: base,
|
||||
config: options,
|
||||
vs: LC.VlessServer{
|
||||
Enable: true,
|
||||
Listen: base.RawAddress(),
|
||||
Users: users,
|
||||
WsPath: options.WsPath,
|
||||
Certificate: options.Certificate,
|
||||
PrivateKey: options.PrivateKey,
|
||||
RealityConfig: options.RealityConfig.Build(),
|
||||
MuxOption: options.MuxOption.Build(),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Config implements constant.InboundListener
|
||||
func (v *Vless) Config() C.InboundConfig {
|
||||
return v.config
|
||||
}
|
||||
|
||||
// Address implements constant.InboundListener
|
||||
func (v *Vless) Address() string {
|
||||
if v.l != nil {
|
||||
for _, addr := range v.l.AddrList() {
|
||||
return addr.String()
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Listen implements constant.InboundListener
|
||||
func (v *Vless) Listen(tunnel C.Tunnel) error {
|
||||
var err error
|
||||
users := make([]LC.VlessUser, len(v.config.Users))
|
||||
for i, v := range v.config.Users {
|
||||
users[i] = LC.VlessUser{
|
||||
Username: v.Username,
|
||||
UUID: v.UUID,
|
||||
Flow: v.Flow,
|
||||
}
|
||||
}
|
||||
v.l, err = sing_vless.New(v.vs, tunnel, v.Additions()...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infoln("Vless[%s] proxy listening at: %s", v.Name(), v.Address())
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close implements constant.InboundListener
|
||||
func (v *Vless) Close() error {
|
||||
return v.l.Close()
|
||||
}
|
||||
|
||||
var _ C.InboundListener = (*Vless)(nil)
|
||||
@@ -86,6 +86,13 @@ func ParseListener(mapping map[string]any) (C.InboundListener, error) {
|
||||
return nil, err
|
||||
}
|
||||
listener, err = IN.NewVmess(vmessOption)
|
||||
case "vless":
|
||||
vlessOption := &IN.VlessOption{}
|
||||
err = decoder.Decode(mapping, vlessOption)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
listener, err = IN.NewVless(vlessOption)
|
||||
case "hysteria2":
|
||||
hysteria2Option := &IN.Hysteria2Option{}
|
||||
err = decoder.Decode(mapping, hysteria2Option)
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
package sing_vless
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/metacubex/mihomo/adapter/inbound"
|
||||
N "github.com/metacubex/mihomo/common/net"
|
||||
tlsC "github.com/metacubex/mihomo/component/tls"
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
LC "github.com/metacubex/mihomo/listener/config"
|
||||
"github.com/metacubex/mihomo/listener/inner"
|
||||
"github.com/metacubex/mihomo/listener/sing"
|
||||
"github.com/metacubex/mihomo/log"
|
||||
"github.com/metacubex/mihomo/ntp"
|
||||
mihomoVMess "github.com/metacubex/mihomo/transport/vmess"
|
||||
|
||||
"github.com/metacubex/sing-vmess/vless"
|
||||
utls "github.com/metacubex/utls"
|
||||
"github.com/sagernet/reality"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/metadata"
|
||||
)
|
||||
|
||||
func init() {
|
||||
vless.RegisterTLS(func(conn net.Conn) (loaded bool, netConn net.Conn, reflectType reflect.Type, reflectPointer unsafe.Pointer) {
|
||||
tlsConn, loaded := common.Cast[*reality.Conn](conn)
|
||||
if !loaded {
|
||||
return
|
||||
}
|
||||
return true, tlsConn.NetConn(), reflect.TypeOf(tlsConn).Elem(), unsafe.Pointer(tlsConn)
|
||||
})
|
||||
|
||||
vless.RegisterTLS(func(conn net.Conn) (loaded bool, netConn net.Conn, reflectType reflect.Type, reflectPointer unsafe.Pointer) {
|
||||
tlsConn, loaded := common.Cast[*utls.UConn](conn)
|
||||
if !loaded {
|
||||
return
|
||||
}
|
||||
return true, tlsConn.NetConn(), reflect.TypeOf(tlsConn.Conn).Elem(), unsafe.Pointer(tlsConn.Conn)
|
||||
})
|
||||
|
||||
vless.RegisterTLS(func(conn net.Conn) (loaded bool, netConn net.Conn, reflectType reflect.Type, reflectPointer unsafe.Pointer) {
|
||||
tlsConn, loaded := common.Cast[*tlsC.UConn](conn)
|
||||
if !loaded {
|
||||
return
|
||||
}
|
||||
return true, tlsConn.NetConn(), reflect.TypeOf(tlsConn.Conn).Elem(), unsafe.Pointer(tlsConn.Conn)
|
||||
})
|
||||
}
|
||||
|
||||
type Listener struct {
|
||||
closed bool
|
||||
config LC.VlessServer
|
||||
listeners []net.Listener
|
||||
service *vless.Service[string]
|
||||
}
|
||||
|
||||
func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition) (sl *Listener, err error) {
|
||||
if len(additions) == 0 {
|
||||
additions = []inbound.Addition{
|
||||
inbound.WithInName("DEFAULT-VLESS"),
|
||||
inbound.WithSpecialRules(""),
|
||||
}
|
||||
}
|
||||
h, err := sing.NewListenerHandler(sing.ListenerConfig{
|
||||
Tunnel: tunnel,
|
||||
Type: C.VLESS,
|
||||
Additions: additions,
|
||||
MuxOption: config.MuxOption,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
service := vless.NewService[string](log.SingLogger, h)
|
||||
service.UpdateUsers(
|
||||
common.Map(config.Users, func(it LC.VlessUser) string {
|
||||
return it.Username
|
||||
}),
|
||||
common.Map(config.Users, func(it LC.VlessUser) string {
|
||||
return it.UUID
|
||||
}),
|
||||
common.Map(config.Users, func(it LC.VlessUser) string {
|
||||
return it.Flow
|
||||
}))
|
||||
|
||||
sl = &Listener{false, config, nil, service}
|
||||
|
||||
tlsConfig := &tls.Config{}
|
||||
var realityConfig *reality.Config
|
||||
var httpMux *http.ServeMux
|
||||
|
||||
if config.Certificate != "" && config.PrivateKey != "" {
|
||||
cert, err := N.ParseCert(config.Certificate, config.PrivateKey, C.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsConfig.Certificates = []tls.Certificate{cert}
|
||||
}
|
||||
if config.WsPath != "" {
|
||||
httpMux = http.NewServeMux()
|
||||
httpMux.HandleFunc(config.WsPath, func(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := mihomoVMess.StreamUpgradedWebsocketConn(w, r)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
sl.HandleConn(conn, tunnel)
|
||||
})
|
||||
tlsConfig.NextProtos = append(tlsConfig.NextProtos, "http/1.1")
|
||||
}
|
||||
if config.RealityConfig.PrivateKey != "" {
|
||||
if tlsConfig.Certificates != nil {
|
||||
return nil, errors.New("certificate is unavailable in reality")
|
||||
}
|
||||
realityConfig = &reality.Config{}
|
||||
realityConfig.SessionTicketsDisabled = true
|
||||
realityConfig.Type = "tcp"
|
||||
realityConfig.Dest = config.RealityConfig.Dest
|
||||
realityConfig.Time = ntp.Now
|
||||
realityConfig.ServerNames = make(map[string]bool)
|
||||
for _, it := range config.RealityConfig.ServerNames {
|
||||
realityConfig.ServerNames[it] = true
|
||||
}
|
||||
privateKey, err := base64.RawURLEncoding.DecodeString(config.RealityConfig.PrivateKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decode private key: %w", err)
|
||||
}
|
||||
if len(privateKey) != 32 {
|
||||
return nil, errors.New("invalid private key")
|
||||
}
|
||||
realityConfig.PrivateKey = privateKey
|
||||
|
||||
realityConfig.MaxTimeDiff = time.Duration(config.RealityConfig.MaxTimeDifference) * time.Microsecond
|
||||
|
||||
realityConfig.ShortIds = make(map[[8]byte]bool)
|
||||
for i, shortIDString := range config.RealityConfig.ShortID {
|
||||
var shortID [8]byte
|
||||
decodedLen, err := hex.Decode(shortID[:], []byte(shortIDString))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decode short_id[%d] '%s': %w", i, shortIDString, err)
|
||||
}
|
||||
if decodedLen > 8 {
|
||||
return nil, fmt.Errorf("invalid short_id[%d]: %s", i, shortIDString)
|
||||
}
|
||||
realityConfig.ShortIds[shortID] = true
|
||||
}
|
||||
|
||||
realityConfig.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
return inner.HandleTcp(address, config.RealityConfig.Proxy)
|
||||
}
|
||||
}
|
||||
|
||||
for _, addr := range strings.Split(config.Listen, ",") {
|
||||
addr := addr
|
||||
|
||||
//TCP
|
||||
l, err := inbound.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if realityConfig != nil {
|
||||
l = reality.NewListener(l, realityConfig)
|
||||
// Due to low implementation quality, the reality server intercepted half close and caused memory leaks.
|
||||
// We fixed it by calling Close() directly.
|
||||
l = realityListenerWrapper{l}
|
||||
} else if len(tlsConfig.Certificates) > 0 {
|
||||
l = tls.NewListener(l, tlsConfig)
|
||||
} else {
|
||||
return nil, errors.New("disallow using Vless without both certificates/reality config")
|
||||
}
|
||||
sl.listeners = append(sl.listeners, l)
|
||||
|
||||
go func() {
|
||||
if httpMux != nil {
|
||||
_ = http.Serve(l, httpMux)
|
||||
return
|
||||
}
|
||||
for {
|
||||
c, err := l.Accept()
|
||||
if err != nil {
|
||||
if sl.closed {
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
go sl.HandleConn(c, tunnel)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return sl, nil
|
||||
}
|
||||
|
||||
func (l *Listener) Close() error {
|
||||
l.closed = true
|
||||
var retErr error
|
||||
for _, lis := range l.listeners {
|
||||
err := lis.Close()
|
||||
if err != nil {
|
||||
retErr = err
|
||||
}
|
||||
}
|
||||
return retErr
|
||||
}
|
||||
|
||||
func (l *Listener) Config() string {
|
||||
return l.config.String()
|
||||
}
|
||||
|
||||
func (l *Listener) AddrList() (addrList []net.Addr) {
|
||||
for _, lis := range l.listeners {
|
||||
addrList = append(addrList, lis.Addr())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (l *Listener) HandleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) {
|
||||
ctx := sing.WithAdditions(context.TODO(), additions...)
|
||||
err := l.service.NewConnection(ctx, conn, metadata.Metadata{
|
||||
Protocol: "vless",
|
||||
Source: metadata.ParseSocksaddr(conn.RemoteAddr().String()),
|
||||
})
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type realityConnWrapper struct {
|
||||
*reality.Conn
|
||||
}
|
||||
|
||||
func (c realityConnWrapper) Upstream() any {
|
||||
return c.Conn
|
||||
}
|
||||
|
||||
func (c realityConnWrapper) CloseWrite() error {
|
||||
return c.Close()
|
||||
}
|
||||
|
||||
type realityListenerWrapper struct {
|
||||
net.Listener
|
||||
}
|
||||
|
||||
func (l realityListenerWrapper) Accept() (net.Conn, error) {
|
||||
c, err := l.Listener.Accept()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return realityConnWrapper{c.(*reality.Conn)}, nil
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/metacubex/mihomo/component/generater"
|
||||
"github.com/metacubex/mihomo/component/geodata"
|
||||
"github.com/metacubex/mihomo/component/updater"
|
||||
"github.com/metacubex/mihomo/config"
|
||||
@@ -71,6 +72,11 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
if len(os.Args) > 1 && os.Args[1] == "generate" {
|
||||
generater.Main(os.Args[2:])
|
||||
return
|
||||
}
|
||||
|
||||
if version {
|
||||
fmt.Printf("Mihomo Meta %s %s %s with %s %s\n",
|
||||
C.Version, runtime.GOOS, runtime.GOARCH, runtime.Version(), C.BuildTime)
|
||||
|
||||
Generated
+4
-4
@@ -5957,9 +5957,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.69"
|
||||
version = "0.10.70"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f5e534d133a060a3c19daec1eb3e98ec6f4685978834f2dbadfe2ec215bab64e"
|
||||
checksum = "61cfb4e166a8bb8c9b55c500bc2308550148ece889be90f609377e58140f42c6"
|
||||
dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
"cfg-if",
|
||||
@@ -5998,9 +5998,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.104"
|
||||
version = "0.9.105"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741"
|
||||
checksum = "8b22d5b84be05a8d6947c7cb71f7c849aa0f112acd4bf51c2a7c1c988ac0a9dc"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
|
||||
@@ -10,7 +10,7 @@ edition = { workspace = true }
|
||||
build = "build.rs"
|
||||
|
||||
[lib]
|
||||
name = "app_lib"
|
||||
name = "clash_nyanpasu_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use super::IVerge;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::{Display, EnumString};
|
||||
use tracing_subscriber::filter;
|
||||
#[derive(Deserialize, Serialize, Debug, Clone, specta::Type)]
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone, specta::Type, EnumString, Display)]
|
||||
#[strum(serialize_all = "kebab-case")]
|
||||
pub enum LoggingLevel {
|
||||
#[serde(rename = "silent", alias = "off")]
|
||||
Silent,
|
||||
|
||||
@@ -149,10 +149,6 @@ pub struct IVerge {
|
||||
/// `light` or `dark` or `system`
|
||||
pub theme_mode: Option<String>,
|
||||
|
||||
/// enable blur mode
|
||||
/// maybe be able to set the alpha
|
||||
pub theme_blur: Option<bool>,
|
||||
|
||||
/// enable traffic graph default is true
|
||||
pub traffic_graph: Option<bool>,
|
||||
|
||||
@@ -189,7 +185,7 @@ pub struct IVerge {
|
||||
pub proxy_guard_interval: Option<u64>,
|
||||
|
||||
/// theme setting
|
||||
pub theme_setting: Option<IVergeTheme>,
|
||||
pub theme_color: Option<String>,
|
||||
|
||||
/// web ui list
|
||||
pub web_ui_list: Option<Vec<String>>,
|
||||
@@ -267,24 +263,6 @@ pub struct WindowState {
|
||||
pub fullscreen: bool,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, specta::Type)]
|
||||
pub struct IVergeTheme {
|
||||
pub primary_color: Option<String>,
|
||||
pub secondary_color: Option<String>,
|
||||
pub primary_text: Option<String>,
|
||||
pub secondary_text: Option<String>,
|
||||
|
||||
pub info_color: Option<String>,
|
||||
pub error_color: Option<String>,
|
||||
pub warning_color: Option<String>,
|
||||
pub success_color: Option<String>,
|
||||
|
||||
pub font_family: Option<String>,
|
||||
pub css_injection: Option<String>,
|
||||
|
||||
pub page_transition_duration: Option<f64>,
|
||||
}
|
||||
|
||||
impl IVerge {
|
||||
pub fn new() -> Self {
|
||||
match dirs::nyanpasu_config_path().and_then(|path| help::read_yaml::<IVerge>(&path)) {
|
||||
@@ -331,7 +309,6 @@ impl IVerge {
|
||||
},
|
||||
app_log_level: Some(logging::LoggingLevel::default()),
|
||||
theme_mode: Some("system".into()),
|
||||
theme_blur: Some(false),
|
||||
traffic_graph: Some(true),
|
||||
enable_memory_usage: Some(true),
|
||||
enable_auto_launch: Some(false),
|
||||
|
||||
@@ -13,6 +13,7 @@ pub static UNITS: Lazy<Vec<DynMigration>> = Lazy::new(|| {
|
||||
vec![
|
||||
MigrateProfilesNullValue.into(),
|
||||
MigrateLanguageOption.into(),
|
||||
MigrateThemeSetting.into(),
|
||||
]
|
||||
});
|
||||
|
||||
@@ -147,3 +148,63 @@ impl<'a> Migration<'a> for MigrateLanguageOption {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MigrateThemeSetting;
|
||||
impl<'a> Migration<'a> for MigrateThemeSetting {
|
||||
fn version(&self) -> &'a semver::Version {
|
||||
&VERSION
|
||||
}
|
||||
|
||||
fn name(&self) -> std::borrow::Cow<'a, str> {
|
||||
Cow::Borrowed("Migrate Theme Setting")
|
||||
}
|
||||
|
||||
fn migrate(&self) -> std::io::Result<()> {
|
||||
let config_path = crate::utils::dirs::nyanpasu_config_path().unwrap();
|
||||
if !config_path.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
let raw_config = std::fs::read_to_string(&config_path)?;
|
||||
let mut config: Mapping = serde_yaml::from_str(&raw_config)
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
|
||||
if let Some(theme) = config.get("theme_setting") {
|
||||
if !theme.is_null() {
|
||||
if let Some(theme_obj) = theme.as_mapping() {
|
||||
if let Some(color) = theme_obj.get("primary_color") {
|
||||
println!("color: {:?}", color);
|
||||
config.insert("theme_color".into(), color.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
config.remove("theme_setting");
|
||||
let new_config = serde_yaml::to_string(&config)
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
|
||||
std::fs::write(&config_path, new_config)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn discard(&self) -> std::io::Result<()> {
|
||||
let config_path = crate::utils::dirs::nyanpasu_config_path().unwrap();
|
||||
if !config_path.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
let raw_config = std::fs::read_to_string(&config_path)?;
|
||||
let mut config: Mapping = serde_yaml::from_str(&raw_config)
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
|
||||
if let Some(color) = config.get("theme_color") {
|
||||
let mut theme_obj = Mapping::new();
|
||||
theme_obj.insert("primary_color".into(), color.clone());
|
||||
config.insert(
|
||||
"theme_setting".into(),
|
||||
serde_yaml::Value::Mapping(theme_obj),
|
||||
);
|
||||
config.remove("theme_color");
|
||||
}
|
||||
let new_config = serde_yaml::to_string(&config)
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
|
||||
std::fs::write(&config_path, new_config)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,7 +280,7 @@ mod platform_impl {
|
||||
let mut items = Vec::new();
|
||||
if proxies.is_empty() {
|
||||
items.push(MenuItemKind::MenuItem(
|
||||
MenuItemBuilder::new("No Proxies")
|
||||
MenuItemBuilder::new(t!("tray.no_proxies"))
|
||||
.id("no_proxies")
|
||||
.enabled(false)
|
||||
.build(app_handle)?,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
app_lib::run().unwrap();
|
||||
clash_nyanpasu_lib::run().unwrap();
|
||||
}
|
||||
|
||||
@@ -62,9 +62,11 @@ pub fn init() -> Result<()> {
|
||||
let (filter, filter_handle) = reload::Layer::new(
|
||||
EnvFilter::builder()
|
||||
.with_default_directive(
|
||||
std::convert::Into::<filter::LevelFilter>::into(log_level).into(),
|
||||
std::convert::Into::<filter::LevelFilter>::into(LoggingLevel::Warn).into(),
|
||||
)
|
||||
.from_env_lossy(),
|
||||
.from_env_lossy()
|
||||
.add_directive(format!("nyanpasu={}", log_level).parse().unwrap())
|
||||
.add_directive(format!("clash_nyanpasu={}", log_level).parse().unwrap()),
|
||||
);
|
||||
|
||||
// register the logger
|
||||
@@ -92,9 +94,12 @@ pub fn init() -> Result<()> {
|
||||
.reload(
|
||||
EnvFilter::builder()
|
||||
.with_default_directive(
|
||||
std::convert::Into::<filter::LevelFilter>::into(level).into(),
|
||||
std::convert::Into::<filter::LevelFilter>::into(LoggingLevel::Warn)
|
||||
.into(),
|
||||
)
|
||||
.from_env_lossy(),
|
||||
.from_env_lossy()
|
||||
.add_directive(format!("nyanpasu={}", level).parse().unwrap())
|
||||
.add_directive(format!("clash_nyanpasu={}", level).parse().unwrap()),
|
||||
)
|
||||
.unwrap(); // panic if error
|
||||
}
|
||||
|
||||
@@ -12,14 +12,14 @@
|
||||
"type": "embedBootstrapper"
|
||||
},
|
||||
"wix": {
|
||||
"language": ["zh-CN", "en-US", "ru-RU"],
|
||||
"language": ["en-US", "ru-RU", "zh-CN", "zh-TW"],
|
||||
"template": "./templates/installer.wxs",
|
||||
"fragmentPaths": ["./templates/cleanup.wxs"]
|
||||
},
|
||||
"nsis": {
|
||||
"displayLanguageSelector": true,
|
||||
"installerIcon": "icons/icon.ico",
|
||||
"languages": ["SimpChinese", "English", "Russian"],
|
||||
"languages": ["English", "Russian", "SimpChinese", "TradChinese"],
|
||||
"template": "./templates/installer.nsi",
|
||||
"installMode": "both"
|
||||
}
|
||||
|
||||
@@ -14,11 +14,13 @@
|
||||
"@tanstack/react-query": "5.66.0",
|
||||
"@tauri-apps/api": "2.2.0",
|
||||
"ahooks": "3.8.4",
|
||||
"lodash-es": "4.17.21",
|
||||
"ofetch": "1.4.1",
|
||||
"react": "19.0.0",
|
||||
"swr": "2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash-es": "4.17.12",
|
||||
"@types/react": "19.0.8"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,5 @@ export * from './ipc'
|
||||
export * from './openapi'
|
||||
export * from './provider'
|
||||
export * from './service'
|
||||
export * from './template'
|
||||
export * from './utils'
|
||||
|
||||
@@ -869,11 +869,6 @@ export type IVerge = {
|
||||
* `light` or `dark` or `system`
|
||||
*/
|
||||
theme_mode: string | null
|
||||
/**
|
||||
* enable blur mode
|
||||
* maybe be able to set the alpha
|
||||
*/
|
||||
theme_blur: boolean | null
|
||||
/**
|
||||
* enable traffic graph default is true
|
||||
*/
|
||||
@@ -921,7 +916,7 @@ export type IVerge = {
|
||||
/**
|
||||
* theme setting
|
||||
*/
|
||||
theme_setting: IVergeTheme | null
|
||||
theme_color: string | null
|
||||
/**
|
||||
* web ui list
|
||||
*/
|
||||
@@ -1002,19 +997,6 @@ export type IVerge = {
|
||||
*/
|
||||
network_statistic_widget?: NetworkStatisticWidgetConfig | null
|
||||
}
|
||||
export type IVergeTheme = {
|
||||
primary_color: string | null
|
||||
secondary_color: string | null
|
||||
primary_text: string | null
|
||||
secondary_text: string | null
|
||||
info_color: string | null
|
||||
error_color: string | null
|
||||
warning_color: string | null
|
||||
success_color: string | null
|
||||
font_family: string | null
|
||||
css_injection: string | null
|
||||
page_transition_duration: number | null
|
||||
}
|
||||
export type JsonValue =
|
||||
| null
|
||||
| boolean
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
export * from './use-profile-content'
|
||||
export * from './use-profile'
|
||||
export * from './use-runtime-profile'
|
||||
export * from './use-settings'
|
||||
export * from './use-system-proxy'
|
||||
export * from './useNyanpasu'
|
||||
export * from './useClash'
|
||||
export * from './useClashCore'
|
||||
export * from './useClashWS'
|
||||
|
||||
export type * from './bindings'
|
||||
|
||||
@@ -44,10 +44,11 @@ export const useProfileContent = (uid: string) => {
|
||||
* ```
|
||||
*/
|
||||
const query = useQuery({
|
||||
queryKey: ['profileContent', uid],
|
||||
queryKey: ['profile-content', uid],
|
||||
queryFn: async () => {
|
||||
return unwrapResult(await commands.readProfileFile(uid))
|
||||
},
|
||||
enabled: !!uid,
|
||||
})
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { unwrapResult } from '../utils'
|
||||
import { commands, ProfileBuilder, ProfilesBuilder } from './bindings'
|
||||
import {
|
||||
commands,
|
||||
Profile,
|
||||
type ProfileBuilder,
|
||||
type ProfilesBuilder,
|
||||
} from './bindings'
|
||||
|
||||
type URLImportParams = Parameters<typeof commands.importProfile>
|
||||
|
||||
@@ -22,68 +27,98 @@ type CreateParams =
|
||||
}
|
||||
}
|
||||
|
||||
type ProfileHelperFn = {
|
||||
view: () => Promise<null | undefined>
|
||||
update: (profile: ProfileBuilder) => Promise<null | undefined>
|
||||
drop: () => Promise<null | undefined>
|
||||
}
|
||||
|
||||
export type ProfileQueryResult = NonNullable<
|
||||
ReturnType<typeof useProfile>['query']['data']
|
||||
>
|
||||
|
||||
export type ProfileQueryResultItem = Profile & Partial<ProfileHelperFn>
|
||||
/**
|
||||
* A custom hook for managing profile operations using React Query.
|
||||
* Provides functionality for CRUD operations on profiles including creation,
|
||||
* updating, reordering, and deletion.
|
||||
* A custom hook for managing profiles with various operations including creation, updating, sorting, and deletion.
|
||||
*
|
||||
* @remarks
|
||||
* This hook provides comprehensive profile management functionality through React Query:
|
||||
* - Fetching profiles with optional helper functions
|
||||
* - Creating/importing profiles from URLs or files
|
||||
* - Updating existing profiles
|
||||
* - Reordering profiles
|
||||
* - Upserting profile configurations
|
||||
* - Deleting profiles
|
||||
*
|
||||
* Each operation automatically handles cache invalidation and refetching when successful.
|
||||
*
|
||||
* @param options - Configuration options for the hook
|
||||
* @param options.without_helper_fn - When true, disables the addition of helper functions to profile items
|
||||
*
|
||||
* @returns An object containing:
|
||||
* - query: {@link UseQueryResult} Hook result for fetching profiles data
|
||||
* - create: {@link UseMutationResult} Mutation for creating/importing profiles
|
||||
* - update: {@link UseMutationResult} Mutation for updating existing profiles
|
||||
* - sort: {@link UseMutationResult} Mutation for reordering profiles
|
||||
* - upsert: {@link UseMutationResult} Mutation for upserting profile configurations
|
||||
* - drop: {@link UseMutationResult} Mutation for deleting profiles
|
||||
* - query: Query result for fetching profiles
|
||||
* - create: Mutation for creating/importing profiles
|
||||
* - update: Mutation for updating existing profiles
|
||||
* - sort: Mutation for reordering profiles
|
||||
* - upsert: Mutation for upserting profile configurations
|
||||
* - drop: Mutation for deleting profiles
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* ```tsx
|
||||
* const { query, create, update, sort, upsert, drop } = useProfile();
|
||||
*
|
||||
* // Fetch profiles
|
||||
* const { data, isLoading } = query;
|
||||
* const profiles = query.data?.items;
|
||||
*
|
||||
* // Create a new profile
|
||||
* create.mutate({
|
||||
* type: 'file',
|
||||
* data: { item: profileData, fileData: 'config' }
|
||||
* });
|
||||
* create.mutate({ type: 'file', data: { item: newProfile, fileData: 'config' }});
|
||||
*
|
||||
* // Update a profile
|
||||
* update.mutate({ uid: 'profile-id', profile: updatedProfile });
|
||||
*
|
||||
* // Reorder profiles
|
||||
* sort.mutate(['uid1', 'uid2', 'uid3']);
|
||||
*
|
||||
* // Upsert profile config
|
||||
* upsert.mutate(profilesConfig);
|
||||
*
|
||||
* // Delete a profile
|
||||
* drop.mutate('profile-id');
|
||||
* ```
|
||||
*/
|
||||
export const useProfile = () => {
|
||||
export const useProfile = (options?: { without_helper_fn?: boolean }) => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
function addHelperFn(item: Profile): Profile & ProfileHelperFn {
|
||||
return {
|
||||
...item,
|
||||
view: async () => unwrapResult(await commands.viewProfile(item.uid)),
|
||||
update: async (profile: ProfileBuilder) =>
|
||||
await update.mutateAsync({ uid: item.uid, profile }),
|
||||
drop: async () => await drop.mutateAsync(item.uid),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A React Query hook that fetches profiles data.
|
||||
* data is the full Profile configuration, including current, chain, valid, and items fields
|
||||
* Uses the `getProfiles` command to retrieve profile information.
|
||||
* Retrieves and processes a list of profiles.
|
||||
*
|
||||
* @returns {UseQueryResult} A query result object containing:
|
||||
* - data: {
|
||||
* current: string | null - Currently selected profile UID
|
||||
* chain: string[] - Global chain of profile UIDs
|
||||
* valid: boolean - Whether the profile configuration is valid
|
||||
* items: Profile[] - Array of profile configurations
|
||||
* }
|
||||
* - `isLoading`: Boolean indicating if the query is in loading state
|
||||
* - `error`: Error object if the query failed
|
||||
* - Other standard React Query result properties
|
||||
* This query uses the `useQuery` hook to fetch profile data by invoking the `commands.getProfiles()` command.
|
||||
* The raw result is first unwrapped using `unwrapResult`, and then each profile item is augmented with additional
|
||||
* helper functions:
|
||||
*
|
||||
* - view: Invokes `commands.viewProfile` with the profile's UID.
|
||||
* - update: Executes the update mutation by passing an object containing the UID and the new profile data.
|
||||
* - drop: Executes the drop mutation using the profile's UID.
|
||||
*
|
||||
* @returns A promise resolving to an object containing the profile list along with the extended helper functions.
|
||||
*/
|
||||
const query = useQuery({
|
||||
queryKey: ['profiles'],
|
||||
queryFn: async () => {
|
||||
return unwrapResult(await commands.getProfiles())
|
||||
const result = unwrapResult(await commands.getProfiles())
|
||||
|
||||
// Skip helper functions if without_helper_fn is set
|
||||
if (options?.without_helper_fn) {
|
||||
return result
|
||||
}
|
||||
|
||||
return {
|
||||
...result,
|
||||
items: result?.items?.map((item) => {
|
||||
return addHelperFn(item)
|
||||
}),
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -191,8 +226,10 @@ export const useProfile = () => {
|
||||
* - Automatically invalidates the 'profiles' query cache on successful mutation
|
||||
*/
|
||||
const upsert = useMutation({
|
||||
mutationFn: async (options: ProfilesBuilder) => {
|
||||
return unwrapResult(await commands.patchProfilesConfig(options))
|
||||
mutationFn: async (options: Partial<ProfilesBuilder>) => {
|
||||
return unwrapResult(
|
||||
await commands.patchProfilesConfig(options as ProfilesBuilder),
|
||||
)
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['profiles'] })
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { unwrapResult } from '../utils'
|
||||
import { commands } from './bindings'
|
||||
|
||||
/**
|
||||
* Custom hook for retrieving the runtime profile.
|
||||
*
|
||||
* This hook leverages the useQuery API to asynchronously retrieve and unwrap the runtime's YAML profile data
|
||||
* via the commands.getRuntimeYaml call. The resulting query object includes properties such as data, error,
|
||||
* status, and other metadata necessary to manage the loading state.
|
||||
*
|
||||
* @returns An object containing the query state and helper methods related to the runtime profile.
|
||||
*/
|
||||
export const useRuntimeProfile = () => {
|
||||
const query = useQuery({
|
||||
queryKey: ['runtime-profile'],
|
||||
queryFn: async () => {
|
||||
return unwrapResult(await commands.getRuntimeYaml())
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
...query,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import { merge } from 'lodash-es'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { unwrapResult } from '../utils'
|
||||
import { commands, type IVerge } from './bindings'
|
||||
|
||||
/**
|
||||
* Custom hook for managing Verge configuration settings using React Query.
|
||||
* Provides functionality to fetch and update settings with automatic cache invalidation.
|
||||
*
|
||||
* @returns An object containing:
|
||||
* - query: UseQueryResult for fetching settings
|
||||
* - data: Current Verge configuration
|
||||
* - status: Query status ('loading', 'error', 'success')
|
||||
* - error: Error object if query fails
|
||||
* - upsert: UseMutationResult for updating settings
|
||||
* - mutate: Function to update configuration
|
||||
* - status: Mutation status
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const { query, upsert } = useSettings();
|
||||
*
|
||||
* // Get current settings
|
||||
* const settings = query.data;
|
||||
*
|
||||
* // Update settings
|
||||
* upsert.mutate({ theme: 'dark' });
|
||||
* ```
|
||||
*/
|
||||
export const useSettings = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
/**
|
||||
* A query hook that fetches Verge configuration settings.
|
||||
* Uses React Query to manage the data fetching state.
|
||||
*
|
||||
* @returns UseQueryResult containing:
|
||||
* - data: The unwrapped Verge configuration data
|
||||
* - status: Current status of the query ('loading', 'error', 'success')
|
||||
* - error: Error object if the query fails
|
||||
* - other standard React Query properties
|
||||
*/
|
||||
const query = useQuery({
|
||||
queryKey: ['settings'],
|
||||
queryFn: async () => {
|
||||
return unwrapResult(await commands.getVergeConfig())
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Mutation hook for updating Verge configuration settings
|
||||
*
|
||||
* @remarks
|
||||
* Uses React Query's useMutation to manage state and side effects
|
||||
*
|
||||
* @param options - Partial configuration options to update
|
||||
* @returns Mutation object containing mutate function and mutation state
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const { mutate } = upsert();
|
||||
* mutate({ theme: 'dark' });
|
||||
* ```
|
||||
*/
|
||||
const upsert = useMutation({
|
||||
// Partial to allow for partial updates
|
||||
mutationFn: async (options: Partial<IVerge>) => {
|
||||
return unwrapResult(await commands.patchVergeConfig(options as IVerge))
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['settings'] })
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
query,
|
||||
upsert,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom hook that manages a specific setting from the Verge configuration.
|
||||
*
|
||||
* @template K - The key type extending keyof IVerge
|
||||
* @param key - The specific setting key to manage
|
||||
* @returns An object containing:
|
||||
* - value: The current value of the specified setting
|
||||
* - upsert: Function to update the setting value
|
||||
* - Additional merged hook status properties
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const { value, upsert } = useSetting('theme');
|
||||
* // value contains current theme setting
|
||||
* // upsert can be used to update theme setting
|
||||
* ```
|
||||
*/
|
||||
export const useSetting = <K extends keyof IVerge>(key: K) => {
|
||||
const {
|
||||
query: { data, ...query },
|
||||
upsert: update,
|
||||
} = useSettings()
|
||||
|
||||
/**
|
||||
* The value retrieved from the data object using the specified key.
|
||||
* May be undefined if either data is undefined or the key doesn't exist in data.
|
||||
*/
|
||||
const value = data?.[key]
|
||||
|
||||
/**
|
||||
* Updates a specific setting value in the Verge configuration
|
||||
* @param value - The new value to be set for the specified key
|
||||
* @returns void
|
||||
* @remarks This function will not execute if the data is not available
|
||||
*/
|
||||
const upsert = async (value: IVerge[K]) => {
|
||||
if (!data) {
|
||||
return
|
||||
}
|
||||
|
||||
await update.mutateAsync({ [key]: value })
|
||||
}
|
||||
|
||||
return {
|
||||
value,
|
||||
upsert,
|
||||
// merge hook status
|
||||
...merge(query, update),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { unwrapResult } from '../utils'
|
||||
import { commands } from './bindings'
|
||||
|
||||
/**
|
||||
* Custom hook to fetch and manage the system proxy settings.
|
||||
*
|
||||
* This hook leverages the `useQuery` hook to perform an asynchronous request
|
||||
* to obtain system proxy data via `commands.getSysProxy()`. The result of the query
|
||||
* is processed with `unwrapResult` to extract the proxy information.
|
||||
*
|
||||
* @returns An object containing the query results and helper properties/methods
|
||||
* (e.g., loading status, error, and refetch function) provided by `useQuery`.
|
||||
*/
|
||||
export const useSystemProxy = () => {
|
||||
const query = useQuery({
|
||||
queryKey: ['system-proxy'],
|
||||
queryFn: async () => {
|
||||
return unwrapResult(await commands.getSysProxy())
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
...query,
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
import useSWR from 'swr'
|
||||
import { ClashConfig, Profile } from '@/index'
|
||||
import {
|
||||
ClashConfig,
|
||||
Profile,
|
||||
ProfilesBuilder,
|
||||
RemoteProfileOptionsBuilder,
|
||||
} from '@/index'
|
||||
import * as tauri from '@/service/tauri'
|
||||
import { clash } from '../service/clash'
|
||||
|
||||
@@ -33,7 +38,7 @@ export const useClash = () => {
|
||||
|
||||
const getProfiles = useSWR('getProfiles', tauri.getProfiles)
|
||||
|
||||
const setProfiles = async (uid: string, profile: Partial<Profile.Item>) => {
|
||||
const setProfiles = async (uid: string, profile: Partial<Profile>) => {
|
||||
await tauri.setProfiles({ uid, profile })
|
||||
|
||||
await getProfiles.mutate()
|
||||
@@ -41,7 +46,7 @@ export const useClash = () => {
|
||||
await getRuntimeLogs.mutate()
|
||||
}
|
||||
|
||||
const setProfilesConfig = async (profiles: Partial<Profile.Config>) => {
|
||||
const setProfilesConfig = async (profiles: ProfilesBuilder) => {
|
||||
await tauri.setProfilesConfig(profiles)
|
||||
|
||||
await getProfiles.mutate()
|
||||
@@ -49,13 +54,16 @@ export const useClash = () => {
|
||||
await getRuntimeLogs.mutate()
|
||||
}
|
||||
|
||||
const createProfile = async (item: Partial<Profile.Item>, data?: string) => {
|
||||
const createProfile = async (item: Partial<Profile>, data?: string) => {
|
||||
await tauri.createProfile(item, data)
|
||||
|
||||
await getProfiles.mutate()
|
||||
}
|
||||
|
||||
const updateProfile = async (uid: string, option?: Profile.Option) => {
|
||||
const updateProfile = async (
|
||||
uid: string,
|
||||
option?: RemoteProfileOptionsBuilder,
|
||||
) => {
|
||||
await tauri.updateProfile(uid, option)
|
||||
|
||||
await getProfiles.mutate()
|
||||
@@ -81,7 +89,10 @@ export const useClash = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const importProfile = async (url: string, option?: Profile.Option) => {
|
||||
const importProfile = async (
|
||||
url: string,
|
||||
option: RemoteProfileOptionsBuilder,
|
||||
) => {
|
||||
await tauri.importProfile(url, option)
|
||||
|
||||
await getProfiles.mutate()
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import type { ClashCore } from '../ipc/bindings'
|
||||
import { fetchLatestCoreVersions, getCoreVersion } from './tauri'
|
||||
import { VergeConfig } from './types'
|
||||
|
||||
export type ClashCore = Required<VergeConfig>['clash_core']
|
||||
|
||||
export interface Core {
|
||||
name: string
|
||||
@@ -56,7 +54,7 @@ export const fetchLatestCore = async () => {
|
||||
|
||||
return {
|
||||
...item,
|
||||
latest: latest,
|
||||
latest,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { IPSBResponse } from '@/openapi'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import type {
|
||||
ClashInfo,
|
||||
Profile,
|
||||
Profiles,
|
||||
ProfilesBuilder,
|
||||
Proxies,
|
||||
RemoteProfileOptionsBuilder,
|
||||
} from '../ipc/bindings'
|
||||
import { ManifestVersion } from './core'
|
||||
import {
|
||||
ClashConfig,
|
||||
ClashInfo,
|
||||
EnvInfos,
|
||||
InspectUpdater,
|
||||
Profile,
|
||||
Proxies,
|
||||
SystemProxy,
|
||||
VergeConfig,
|
||||
} from './types'
|
||||
@@ -37,13 +42,16 @@ export const getRuntimeLogs = async () => {
|
||||
}
|
||||
|
||||
export const createProfile = async (
|
||||
item: Partial<Profile.Item>,
|
||||
item: Partial<Profile>,
|
||||
fileData?: string | null,
|
||||
) => {
|
||||
return await invoke<void>('create_profile', { item, fileData })
|
||||
}
|
||||
|
||||
export const updateProfile = async (uid: string, option?: Profile.Option) => {
|
||||
export const updateProfile = async (
|
||||
uid: string,
|
||||
option?: RemoteProfileOptionsBuilder,
|
||||
) => {
|
||||
return await invoke<void>('update_profile', { uid, option })
|
||||
}
|
||||
|
||||
@@ -56,17 +64,17 @@ export const viewProfile = async (uid: string) => {
|
||||
}
|
||||
|
||||
export const getProfiles = async () => {
|
||||
return await invoke<Profile.Config>('get_profiles')
|
||||
return await invoke<Profiles>('get_profiles')
|
||||
}
|
||||
|
||||
export const setProfiles = async (payload: {
|
||||
uid: string
|
||||
profile: Partial<Profile.Item>
|
||||
profile: Partial<Profile>
|
||||
}) => {
|
||||
return await invoke<void>('patch_profile', payload)
|
||||
}
|
||||
|
||||
export const setProfilesConfig = async (profiles: Partial<Profile.Config>) => {
|
||||
export const setProfilesConfig = async (profiles: ProfilesBuilder) => {
|
||||
return await invoke<void>('patch_profiles_config', { profiles })
|
||||
}
|
||||
|
||||
@@ -80,7 +88,7 @@ export const saveProfileFile = async (uid: string, fileData: string) => {
|
||||
|
||||
export const importProfile = async (
|
||||
url: string,
|
||||
option: Profile.Option = { with_proxy: true },
|
||||
option: RemoteProfileOptionsBuilder,
|
||||
) => {
|
||||
return await invoke<void>('import_profile', {
|
||||
url,
|
||||
|
||||
@@ -54,11 +54,6 @@ export interface VergeConfig {
|
||||
always_on_top?: boolean
|
||||
}
|
||||
|
||||
export interface ClashInfo {
|
||||
port?: number
|
||||
server?: string
|
||||
secret?: string
|
||||
}
|
||||
export interface ClashConfig {
|
||||
port: number
|
||||
mode: string
|
||||
@@ -74,105 +69,12 @@ export interface ClashConfig {
|
||||
secret: string
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace Profile {
|
||||
export interface Config {
|
||||
current: string[]
|
||||
chain: string[]
|
||||
valid: string[]
|
||||
items: Item[]
|
||||
}
|
||||
|
||||
export const Template = {
|
||||
merge: `# Clash Nyanpasu Merge Template (YAML)
|
||||
# Documentation on https://nyanpasu.elaina.moe/
|
||||
# Set the default merge strategy to recursive merge.
|
||||
# Enable the old mode with the override__ prefix.
|
||||
# Use the filter__ prefix to filter lists (removing unwanted content).
|
||||
# All prefixes should support accessing maps or lists with a.b.c syntax.
|
||||
`,
|
||||
javascript: `// Clash Nyanpasu JavaScript Template
|
||||
// Documentation on https://nyanpasu.elaina.moe/
|
||||
|
||||
/** @type {config} */
|
||||
export default function (profile) {
|
||||
return profile;
|
||||
}
|
||||
`,
|
||||
luascript: `-- Clash Nyanpasu Lua Script Template
|
||||
-- Documentation on https://nyanpasu.elaina.moe/
|
||||
|
||||
return config;
|
||||
`,
|
||||
profile: `# Clash Nyanpasu Profile Template
|
||||
# Documentation on https://nyanpasu.elaina.moe/
|
||||
|
||||
proxies:
|
||||
|
||||
proxy-groups:
|
||||
|
||||
rules:
|
||||
`,
|
||||
}
|
||||
|
||||
export const Type = {
|
||||
Local: 'local',
|
||||
Remote: 'remote',
|
||||
Merge: 'merge',
|
||||
JavaScript: {
|
||||
script: 'javascript',
|
||||
},
|
||||
LuaScript: {
|
||||
script: 'lua',
|
||||
},
|
||||
} as const
|
||||
|
||||
export interface Item {
|
||||
uid: string
|
||||
type?: (typeof Type)[keyof typeof Type]
|
||||
name?: string
|
||||
desc?: string
|
||||
file?: string
|
||||
url?: string
|
||||
updated?: number
|
||||
selected?: {
|
||||
name?: string
|
||||
now?: string
|
||||
}[]
|
||||
extra?: {
|
||||
upload: number
|
||||
download: number
|
||||
total: number
|
||||
expire: number
|
||||
}
|
||||
option?: Option
|
||||
chain?: string[]
|
||||
}
|
||||
|
||||
export interface Option {
|
||||
user_agent?: string
|
||||
with_proxy?: boolean
|
||||
self_proxy?: boolean
|
||||
update_interval?: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface SystemProxy {
|
||||
enable: boolean
|
||||
server: string
|
||||
bypass: string
|
||||
}
|
||||
|
||||
export interface Proxies {
|
||||
direct: Clash.Proxy
|
||||
global: Clash.Proxy<Clash.Proxy>
|
||||
groups: Clash.Proxy<Clash.Proxy>[]
|
||||
proxies: Clash.Proxy[]
|
||||
records: {
|
||||
[key: string]: Clash.Proxy
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace Connection {
|
||||
export interface Item {
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// nyanpasu merge profile chain template
|
||||
const merge = `# Clash Nyanpasu Merge Template (YAML)
|
||||
# Documentation on https://nyanpasu.elaina.moe/
|
||||
# Set the default merge strategy to recursive merge.
|
||||
# Enable the old mode with the override__ prefix.
|
||||
# Use the filter__ prefix to filter lists (removing unwanted content).
|
||||
# All prefixes should support accessing maps or lists with a.b.c syntax.
|
||||
`
|
||||
|
||||
// nyanpasu javascript profile chain template
|
||||
const javascript = `// Clash Nyanpasu JavaScript Template
|
||||
// Documentation on https://nyanpasu.elaina.moe/
|
||||
|
||||
/** @type {config} */
|
||||
export default function (profile) {
|
||||
return profile;
|
||||
}
|
||||
`
|
||||
|
||||
// nyanpasu lua profile chain template
|
||||
const luascript = `-- Clash Nyanpasu Lua Script Template
|
||||
-- Documentation on https://nyanpasu.elaina.moe/
|
||||
|
||||
return config;
|
||||
`
|
||||
|
||||
// clash profile template example
|
||||
const profile = `# Clash Nyanpasu Profile Template
|
||||
# Documentation on https://nyanpasu.elaina.moe/
|
||||
|
||||
proxies:
|
||||
|
||||
proxy-groups:
|
||||
|
||||
rules:
|
||||
`
|
||||
|
||||
export const ProfileTemplate = {
|
||||
merge,
|
||||
javascript,
|
||||
luascript,
|
||||
profile,
|
||||
} as const
|
||||
@@ -28,7 +28,7 @@
|
||||
"allotment": "1.20.2",
|
||||
"country-code-emoji": "2.3.0",
|
||||
"dayjs": "1.11.13",
|
||||
"framer-motion": "12.0.6",
|
||||
"framer-motion": "12.0.11",
|
||||
"i18next": "24.2.2",
|
||||
"jotai": "2.11.3",
|
||||
"json-schema": "0.4.0",
|
||||
@@ -58,7 +58,7 @@
|
||||
"@tanstack/react-query": "5.66.0",
|
||||
"@tanstack/react-router": "1.99.0",
|
||||
"@tanstack/router-devtools": "1.99.0",
|
||||
"@tanstack/router-plugin": "1.99.0",
|
||||
"@tanstack/router-plugin": "1.99.3",
|
||||
"@tauri-apps/plugin-clipboard-manager": "2.2.1",
|
||||
"@tauri-apps/plugin-dialog": "2.2.0",
|
||||
"@tauri-apps/plugin-fs": "2.2.0",
|
||||
|
||||
@@ -6,11 +6,10 @@
|
||||
unicode-range: U+1F1E6-1F1FF;
|
||||
}
|
||||
|
||||
// use local emoji font for better backward compatibility
|
||||
@font-face {
|
||||
font-family: 'Color Emoji';
|
||||
src: local('Apple Color Emoji'), local('Segoe UI Emoji'),
|
||||
local('Segoe UI Symbol'), local('Noto Color Emoji'),
|
||||
url('../fonts/Twemoji.Mozilla.ttf');
|
||||
|
||||
// use local emoji font for better backward compatibility
|
||||
}
|
||||
|
||||
@@ -51,7 +51,10 @@ export default function ConnectionTotal() {
|
||||
<div className="flex gap-2">
|
||||
<Paper
|
||||
elevation={0}
|
||||
className="flex min-h-8 items-center justify-center gap-1 rounded-2xl px-2"
|
||||
className="flex min-h-8 items-center justify-center gap-1 px-2"
|
||||
sx={{
|
||||
borderRadius: '1em',
|
||||
}}
|
||||
>
|
||||
<Download
|
||||
className="scale-75"
|
||||
@@ -76,7 +79,10 @@ export default function ConnectionTotal() {
|
||||
</Paper>
|
||||
<Paper
|
||||
elevation={0}
|
||||
className="flex min-h-8 items-center justify-center gap-1 rounded-2xl px-2"
|
||||
className="flex min-h-8 items-center justify-center gap-1 px-2"
|
||||
sx={{
|
||||
borderRadius: '1em',
|
||||
}}
|
||||
>
|
||||
<Upload
|
||||
className="scale-75"
|
||||
|
||||
@@ -12,7 +12,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { Menu as MenuIcon } from '@mui/icons-material'
|
||||
import { LoadingButton } from '@mui/lab'
|
||||
import { alpha, ListItemButton, Menu, MenuItem, useTheme } from '@mui/material'
|
||||
import { Profile, useClash } from '@nyanpasu/interface'
|
||||
import { ProfileQueryResultItem } from '@nyanpasu/interface'
|
||||
import { cleanDeepClickEvent } from '@nyanpasu/ui'
|
||||
|
||||
const longPressDelay = 200
|
||||
@@ -23,7 +23,7 @@ export const ChainItem = memo(function ChainItem({
|
||||
onClick,
|
||||
onChainEdit,
|
||||
}: {
|
||||
item: Profile.Item
|
||||
item: ProfileQueryResultItem
|
||||
selected?: boolean
|
||||
onClick: () => Promise<void>
|
||||
onChainEdit: () => void
|
||||
@@ -32,7 +32,7 @@ export const ChainItem = memo(function ChainItem({
|
||||
|
||||
const { palette } = useTheme()
|
||||
|
||||
const { deleteProfile, viewProfile } = useClash()
|
||||
// const { deleteProfile, viewProfile } = useClash()
|
||||
|
||||
const [isPending, startTransition] = useTransition()
|
||||
|
||||
@@ -45,8 +45,8 @@ export const ChainItem = memo(function ChainItem({
|
||||
const menuMapping = {
|
||||
Apply: () => handleClick(),
|
||||
'Edit Info': () => onChainEdit(),
|
||||
'Open File': () => viewProfile(item.uid),
|
||||
Delete: () => deleteProfile(item.uid),
|
||||
'Open File': () => item.view && item.view(),
|
||||
Delete: () => item.drop && item.drop(),
|
||||
}
|
||||
|
||||
const handleMenuClick = (func: () => void) => {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { alpha, useTheme } from '@mui/material'
|
||||
import { Profile } from '@nyanpasu/interface'
|
||||
import { getLanguage } from '../utils'
|
||||
import { getLanguage, ProfileType } from '../utils'
|
||||
|
||||
export const LanguageChip = ({ type }: { type: Profile.Item['type'] }) => {
|
||||
export const LanguageChip = ({ type }: { type: ProfileType }) => {
|
||||
const { palette } = useTheme()
|
||||
|
||||
const lang = getLanguage(type, true)
|
||||
|
||||
@@ -7,13 +7,17 @@ import { formatError } from '@/utils'
|
||||
import { message } from '@/utils/notification'
|
||||
import { Add } from '@mui/icons-material'
|
||||
import { alpha, ListItemButton, useTheme } from '@mui/material'
|
||||
import { Profile, useClash } from '@nyanpasu/interface'
|
||||
import { filterProfiles } from '../utils'
|
||||
import {
|
||||
ProfileQueryResultItem,
|
||||
useClash,
|
||||
useProfile,
|
||||
} from '@nyanpasu/interface'
|
||||
import { ClashProfile, filterProfiles } from '../utils'
|
||||
import ChainItem from './chain-item'
|
||||
import { atomChainsSelected, atomGlobalChainCurrent } from './store'
|
||||
|
||||
export interface SideChainProps {
|
||||
onChainEdit: (item?: Profile.Item) => void | Promise<void>
|
||||
onChainEdit: (item?: ProfileQueryResultItem) => void | Promise<void>
|
||||
}
|
||||
|
||||
export const SideChain = ({ onChainEdit }: SideChainProps) => {
|
||||
@@ -25,20 +29,19 @@ export const SideChain = ({ onChainEdit }: SideChainProps) => {
|
||||
|
||||
const currentProfileUid = useAtomValue(atomChainsSelected)
|
||||
|
||||
const { getProfiles, setProfilesConfig, setProfiles, reorderProfilesByList } =
|
||||
useClash()
|
||||
const { setProfiles, reorderProfilesByList } = useClash()
|
||||
|
||||
const { scripts, profiles } = filterProfiles(getProfiles.data?.items)
|
||||
const { query, upsert } = useProfile()
|
||||
|
||||
const { clash, chain } = filterProfiles(query.data?.items)
|
||||
|
||||
const currentProfile = useMemo(() => {
|
||||
return getProfiles.data?.items?.find(
|
||||
(item) => item.uid === currentProfileUid,
|
||||
)
|
||||
}, [getProfiles.data?.items, currentProfileUid])
|
||||
return clash?.find((item) => item.uid === currentProfileUid) as ClashProfile
|
||||
}, [clash, currentProfileUid])
|
||||
|
||||
const handleChainClick = useLockFn(async (uid: string) => {
|
||||
const chains = isGlobalChainCurrent
|
||||
? (getProfiles.data?.chain ?? [])
|
||||
? (query.data?.chain ?? [])
|
||||
: (currentProfile?.chain ?? [])
|
||||
|
||||
const updatedChains = chains.includes(uid)
|
||||
@@ -47,7 +50,7 @@ export const SideChain = ({ onChainEdit }: SideChainProps) => {
|
||||
|
||||
try {
|
||||
if (isGlobalChainCurrent) {
|
||||
await setProfilesConfig({ chain: updatedChains })
|
||||
await upsert.mutateAsync({ chain: updatedChains })
|
||||
} else {
|
||||
if (!currentProfile?.uid) {
|
||||
return
|
||||
@@ -63,8 +66,8 @@ export const SideChain = ({ onChainEdit }: SideChainProps) => {
|
||||
})
|
||||
|
||||
const reorderValues = useMemo(
|
||||
() => scripts?.map((item) => item.uid) || [],
|
||||
[scripts],
|
||||
() => chain?.map((item) => item.uid) || [],
|
||||
[chain],
|
||||
)
|
||||
|
||||
return (
|
||||
@@ -73,15 +76,15 @@ export const SideChain = ({ onChainEdit }: SideChainProps) => {
|
||||
axis="y"
|
||||
values={reorderValues}
|
||||
onReorder={(values) => {
|
||||
const profileUids = profiles?.map((item) => item.uid) || []
|
||||
const profileUids = clash?.map((item) => item.uid) || []
|
||||
reorderProfilesByList([...profileUids, ...values])
|
||||
}}
|
||||
layoutScroll
|
||||
style={{ overflowY: 'scroll' }}
|
||||
>
|
||||
{scripts?.map((item, index) => {
|
||||
{chain?.map((item, index) => {
|
||||
const selected = isGlobalChainCurrent
|
||||
? getProfiles.data?.chain?.includes(item.uid)
|
||||
? query.data?.chain?.includes(item.uid)
|
||||
: currentProfile?.chain?.includes(item.uid)
|
||||
|
||||
return (
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { VList } from 'virtua'
|
||||
import { RamenDining, Terminal } from '@mui/icons-material'
|
||||
import { Divider } from '@mui/material'
|
||||
import { useClash } from '@nyanpasu/interface'
|
||||
import { useClash, useProfile } from '@nyanpasu/interface'
|
||||
import { cn } from '@nyanpasu/ui'
|
||||
import { filterProfiles } from '../utils'
|
||||
|
||||
@@ -37,9 +37,11 @@ export interface SideLogProps {
|
||||
export const SideLog = ({ className }: SideLogProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { getRuntimeLogs, getProfiles } = useClash()
|
||||
// const { getRuntimeLogs, getProfiles } = useClash()
|
||||
|
||||
const { scripts } = filterProfiles(getProfiles.data?.items)
|
||||
const { query } = useProfile()
|
||||
|
||||
const { chain } = filterProfiles(query.data?.items)
|
||||
|
||||
return (
|
||||
<div className={cn('w-full', className)}>
|
||||
@@ -54,7 +56,7 @@ export const SideLog = ({ className }: SideLogProps) => {
|
||||
<Divider />
|
||||
|
||||
<VList className="flex flex-col gap-2 overflow-auto p-2 select-text">
|
||||
{!isEmpty(getRuntimeLogs.data) ? (
|
||||
{/* {!isEmpty(getRuntimeLogs.data) ? (
|
||||
Object.entries(getRuntimeLogs.data).map(([uid, content]) => {
|
||||
return content.map((item, index) => {
|
||||
const name = scripts?.find((script) => script.uid === uid)?.name
|
||||
@@ -69,12 +71,12 @@ export const SideLog = ({ className }: SideLogProps) => {
|
||||
)
|
||||
})
|
||||
})
|
||||
) : (
|
||||
<div className="flex h-full min-h-48 w-full flex-col items-center justify-center">
|
||||
<RamenDining className="!size-10" />
|
||||
<p>{t('No Logs')}</p>
|
||||
</div>
|
||||
)}
|
||||
) : ( */}
|
||||
<div className="flex h-full min-h-48 w-full flex-col items-center justify-center">
|
||||
<RamenDining className="!size-10" />
|
||||
<p>{t('No Logs')}</p>
|
||||
</div>
|
||||
{/* )} */}
|
||||
</VList>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { atom } from 'jotai'
|
||||
import type { Profile } from '@nyanpasu/interface'
|
||||
|
||||
export const atomGlobalChainCurrent = atom<boolean>(false)
|
||||
|
||||
export const atomChainsSelected = atom<Profile.Item['uid']>()
|
||||
export const atomChainsSelected = atom<string>()
|
||||
|
||||
@@ -22,7 +22,14 @@ import { useLatest } from 'react-use'
|
||||
import { formatError } from '@/utils'
|
||||
import { message } from '@/utils/notification'
|
||||
import { Divider, InputAdornment } from '@mui/material'
|
||||
import { Profile, useClash } from '@nyanpasu/interface'
|
||||
import {
|
||||
LocalProfile,
|
||||
ProfileQueryResultItem,
|
||||
ProfileTemplate,
|
||||
RemoteProfile,
|
||||
useProfile,
|
||||
useProfileContent,
|
||||
} from '@nyanpasu/interface'
|
||||
import { BaseDialog } from '@nyanpasu/ui'
|
||||
import { LabelSwitch } from '../setting/modules/clash-field'
|
||||
import { ReadProfile } from './read-profile'
|
||||
@@ -30,7 +37,7 @@ import { ReadProfile } from './read-profile'
|
||||
const ProfileMonacoViewer = lazy(() => import('./profile-monaco-viewer'))
|
||||
|
||||
export interface ProfileDialogProps {
|
||||
profile?: Profile.Item
|
||||
profile?: ProfileQueryResultItem
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
@@ -52,27 +59,29 @@ export const ProfileDialog = ({
|
||||
}: ProfileDialogProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { createProfile, setProfiles, getProfileFile, setProfileFile } =
|
||||
useClash()
|
||||
const { create, update } = useProfile()
|
||||
|
||||
const contentFn = useProfileContent(profile?.uid ?? '')
|
||||
|
||||
const localProfile = useRef('')
|
||||
const addProfileCtx = use(AddProfileContext)
|
||||
const [localProfileMessage, setLocalProfileMessage] = useState('')
|
||||
|
||||
const { control, watch, handleSubmit, reset, setValue } =
|
||||
useForm<Profile.Item>({
|
||||
defaultValues: profile || {
|
||||
type: 'remote',
|
||||
name: addProfileCtx?.name || t(`New Profile`),
|
||||
desc: addProfileCtx?.desc || '',
|
||||
url: addProfileCtx?.url || '',
|
||||
option: {
|
||||
// user_agent: "",
|
||||
with_proxy: false,
|
||||
self_proxy: false,
|
||||
},
|
||||
const { control, watch, handleSubmit, reset, setValue } = useForm<
|
||||
RemoteProfile | LocalProfile
|
||||
>({
|
||||
defaultValues: profile || {
|
||||
type: 'remote',
|
||||
name: addProfileCtx?.name || t(`New Profile`),
|
||||
desc: addProfileCtx?.desc || '',
|
||||
url: addProfileCtx?.url || '',
|
||||
option: {
|
||||
// user_agent: "",
|
||||
with_proxy: false,
|
||||
self_proxy: false,
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (addProfileCtx) {
|
||||
@@ -112,10 +121,12 @@ export const ProfileDialog = ({
|
||||
const latestEditor = useLatest(editor)
|
||||
|
||||
const editorMarks = useRef<editor.IMarker[]>([])
|
||||
|
||||
const editorHasError = () =>
|
||||
editorMarks.current.length > 0 &&
|
||||
editorMarks.current.some((m) => m.severity === 8)
|
||||
|
||||
// eslint-disable-next-line react-compiler/react-compiler
|
||||
const onSubmit = handleSubmit(async (form) => {
|
||||
if (editorHasError()) {
|
||||
message('Please fix the error before saving', {
|
||||
@@ -123,23 +134,55 @@ export const ProfileDialog = ({
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const toCreate = async () => {
|
||||
if (isRemote) {
|
||||
await createProfile(form)
|
||||
const data = form as RemoteProfile
|
||||
|
||||
await create.mutateAsync({
|
||||
type: 'url',
|
||||
data: {
|
||||
url: data.url,
|
||||
// TODO: define backend serde(option) to move null
|
||||
option: data.option
|
||||
? {
|
||||
...data.option,
|
||||
user_agent: data.option.user_agent ?? null,
|
||||
with_proxy: data.option.with_proxy ?? null,
|
||||
self_proxy: data.option.self_proxy ?? null,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
if (localProfile.current) {
|
||||
await createProfile(form, localProfile.current)
|
||||
await create.mutateAsync({
|
||||
type: 'manual',
|
||||
data: {
|
||||
item: form,
|
||||
fileData: localProfile.current,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
// setLocalProfileMessage("Not selected profile");
|
||||
await createProfile(form, 'rules: []')
|
||||
await create.mutateAsync({
|
||||
type: 'manual',
|
||||
data: {
|
||||
item: form,
|
||||
fileData: ProfileTemplate.profile,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const toUpdate = async () => {
|
||||
const value = latestEditor.current.value
|
||||
await setProfileFile(form.uid, value)
|
||||
await setProfiles(form.uid, form)
|
||||
await contentFn.upsert.mutateAsync(value)
|
||||
|
||||
await update.mutateAsync({
|
||||
uid: form.uid,
|
||||
profile: form,
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -252,7 +295,7 @@ export const ProfileDialog = ({
|
||||
render={({ field }) => (
|
||||
<LabelSwitch
|
||||
label={t('Use System Proxy')}
|
||||
checked={field.value}
|
||||
checked={Boolean(field.value)}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
@@ -264,7 +307,7 @@ export const ProfileDialog = ({
|
||||
render={({ field }) => (
|
||||
<LabelSwitch
|
||||
label={t('Use Clash Proxy')}
|
||||
checked={field.value}
|
||||
checked={Boolean(field.value)}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
@@ -298,7 +341,7 @@ export const ProfileDialog = ({
|
||||
|
||||
if (isEdit) {
|
||||
try {
|
||||
const value = await getProfileFile(profile?.uid)
|
||||
const value = contentFn.query.data ?? ''
|
||||
setEditor((editor) => ({ ...editor, value }))
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
||||
@@ -26,19 +26,26 @@ import {
|
||||
Tooltip,
|
||||
useTheme,
|
||||
} from '@mui/material'
|
||||
import { Profile, useClash } from '@nyanpasu/interface'
|
||||
import {
|
||||
Profile,
|
||||
ProfileQueryResultItem,
|
||||
RemoteProfile,
|
||||
RemoteProfileOptions,
|
||||
useClash,
|
||||
useProfile,
|
||||
} from '@nyanpasu/interface'
|
||||
import { cleanDeepClickEvent, cn } from '@nyanpasu/ui'
|
||||
import { ProfileDialog } from './profile-dialog'
|
||||
import { GlobalUpdatePendingContext } from './provider'
|
||||
|
||||
export interface ProfileItemProps {
|
||||
item: Profile.Item
|
||||
item: ProfileQueryResultItem
|
||||
selected?: boolean
|
||||
maxLogLevelTriggered?: {
|
||||
global: undefined | 'info' | 'error' | 'warn'
|
||||
current: undefined | 'info' | 'error' | 'warn'
|
||||
}
|
||||
onClickChains: (item: Profile.Item) => void
|
||||
onClickChains: (item: Profile) => void
|
||||
chainsSelected?: boolean
|
||||
}
|
||||
|
||||
@@ -53,13 +60,9 @@ export const ProfileItem = memo(function ProfileItem({
|
||||
|
||||
const { palette } = useTheme()
|
||||
|
||||
const {
|
||||
setProfilesConfig,
|
||||
deleteConnections,
|
||||
updateProfile,
|
||||
deleteProfile,
|
||||
viewProfile,
|
||||
} = useClash()
|
||||
const { deleteConnections } = useClash()
|
||||
|
||||
const { upsert } = useProfile()
|
||||
|
||||
const globalUpdatePending = use(GlobalUpdatePendingContext)
|
||||
|
||||
@@ -73,7 +76,7 @@ export const ProfileItem = memo(function ProfileItem({
|
||||
let total = 0
|
||||
let used = 0
|
||||
|
||||
if (item.extra) {
|
||||
if ('extra' in item && item.extra) {
|
||||
const { download, upload, total: t } = item.extra
|
||||
|
||||
total = t
|
||||
@@ -102,7 +105,7 @@ export const ProfileItem = memo(function ProfileItem({
|
||||
try {
|
||||
setLoading({ card: true })
|
||||
|
||||
await setProfilesConfig({ current: [item.uid] })
|
||||
await upsert.mutateAsync({ current: [item.uid] })
|
||||
|
||||
await deleteConnections()
|
||||
} catch (err) {
|
||||
@@ -124,13 +127,18 @@ export const ProfileItem = memo(function ProfileItem({
|
||||
})
|
||||
|
||||
const handleUpdate = useLockFn(async (proxy?: boolean) => {
|
||||
const options: Profile.Option = item.option || {
|
||||
// TODO: define backend serde(option) to move null
|
||||
const selfOption = 'option' in item ? item.option : undefined
|
||||
|
||||
const options: RemoteProfileOptions = {
|
||||
with_proxy: false,
|
||||
self_proxy: false,
|
||||
update_interval: 0,
|
||||
...selfOption,
|
||||
}
|
||||
|
||||
if (proxy) {
|
||||
if (item.option?.self_proxy) {
|
||||
if (selfOption?.self_proxy) {
|
||||
options.with_proxy = false
|
||||
options.self_proxy = true
|
||||
} else {
|
||||
@@ -142,7 +150,7 @@ export const ProfileItem = memo(function ProfileItem({
|
||||
try {
|
||||
setLoading({ update: true })
|
||||
|
||||
await updateProfile(item.uid, options)
|
||||
await item?.update?.(item)
|
||||
} finally {
|
||||
setLoading({ update: false })
|
||||
}
|
||||
@@ -150,7 +158,8 @@ export const ProfileItem = memo(function ProfileItem({
|
||||
|
||||
const handleDelete = useLockFn(async () => {
|
||||
try {
|
||||
await deleteProfile(item.uid)
|
||||
// await deleteProfile(item.uid)
|
||||
await item?.drop?.()
|
||||
} catch (err) {
|
||||
message(`Delete failed: \n ${JSON.stringify(err)}`, {
|
||||
title: t('Error'),
|
||||
@@ -164,19 +173,12 @@ export const ProfileItem = memo(function ProfileItem({
|
||||
Select: () => handleSelect(),
|
||||
'Edit Info': () => setOpen(true),
|
||||
'Proxy Chains': () => onClickChains(item),
|
||||
'Open File': () => viewProfile(item.uid),
|
||||
'Open File': () => item?.view?.(),
|
||||
Update: () => handleUpdate(),
|
||||
'Update(Proxy)': () => handleUpdate(true),
|
||||
Delete: () => handleDelete(),
|
||||
}),
|
||||
[
|
||||
handleDelete,
|
||||
handleSelect,
|
||||
handleUpdate,
|
||||
item,
|
||||
onClickChains,
|
||||
viewProfile,
|
||||
],
|
||||
[handleDelete, handleSelect, handleUpdate, item, onClickChains],
|
||||
)
|
||||
|
||||
const MenuComp = useMemo(() => {
|
||||
@@ -232,7 +234,7 @@ export const ProfileItem = memo(function ProfileItem({
|
||||
onClick={handleSelect}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<Tooltip title={item.url}>
|
||||
<Tooltip title={(item as RemoteProfile).url}>
|
||||
<Chip
|
||||
className="!pr-2 !pl-2 font-bold"
|
||||
avatar={<IconComponent className="!size-5" color="primary" />}
|
||||
@@ -255,9 +257,9 @@ export const ProfileItem = memo(function ProfileItem({
|
||||
!!item.updated && (
|
||||
<TimeSpan ts={item.updated!} k="Subscription Updated At" />
|
||||
),
|
||||
!!item.extra?.expire && (
|
||||
!!(item as RemoteProfile).extra?.expire && (
|
||||
<TimeSpan
|
||||
ts={item.extra!.expire!}
|
||||
ts={(item as RemoteProfile).extra!.expire!}
|
||||
k="Subscription Expires In"
|
||||
/>
|
||||
),
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Close } from '@mui/icons-material'
|
||||
import { IconButton } from '@mui/material'
|
||||
import { Profile, useClash } from '@nyanpasu/interface'
|
||||
import { Profile, useProfile } from '@nyanpasu/interface'
|
||||
import { SideChain } from './modules/side-chain'
|
||||
import { SideLog } from './modules/side-log'
|
||||
import { atomChainsSelected, atomGlobalChainCurrent } from './modules/store'
|
||||
@@ -21,21 +21,19 @@ export const ProfileSide = ({ onClose }: ProfileSideProps) => {
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const [item, setItem] = useState<Profile.Item>()
|
||||
const [item, setItem] = useState<Profile>()
|
||||
|
||||
const isGlobalChainCurrent = useAtomValue(atomGlobalChainCurrent)
|
||||
|
||||
const currentProfileUid = useAtomValue(atomChainsSelected)
|
||||
|
||||
const { getProfiles } = useClash()
|
||||
const { query } = useProfile()
|
||||
|
||||
const currentProfile = useMemo(() => {
|
||||
return getProfiles.data?.items?.find(
|
||||
(item) => item.uid === currentProfileUid,
|
||||
)
|
||||
}, [getProfiles.data?.items, currentProfileUid])
|
||||
return query.data?.items?.find((item) => item.uid === currentProfileUid)
|
||||
}, [query.data?.items, currentProfileUid])
|
||||
|
||||
const handleEditChain = async (_item?: Profile.Item) => {
|
||||
const handleEditChain = async (_item?: Profile) => {
|
||||
setItem(_item)
|
||||
setOpen(true)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
Tooltip,
|
||||
useTheme,
|
||||
} from '@mui/material'
|
||||
import { useClash } from '@nyanpasu/interface'
|
||||
import { useProfile } from '@nyanpasu/interface'
|
||||
import { readText } from '@tauri-apps/plugin-clipboard-manager'
|
||||
|
||||
export const QuickImport = () => {
|
||||
@@ -22,7 +22,7 @@ export const QuickImport = () => {
|
||||
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const { importProfile } = useClash()
|
||||
const { create } = useProfile()
|
||||
|
||||
const onCopyLink = async () => {
|
||||
const text = await readText()
|
||||
@@ -68,7 +68,13 @@ export const QuickImport = () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
|
||||
await importProfile(url)
|
||||
await create.mutateAsync({
|
||||
type: 'url',
|
||||
data: {
|
||||
url,
|
||||
option: null,
|
||||
},
|
||||
})
|
||||
} finally {
|
||||
setUrl('')
|
||||
setLoading(false)
|
||||
|
||||
+25
-27
@@ -1,10 +1,14 @@
|
||||
import { useCreation } from 'ahooks'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { lazy, Suspense, useMemo } from 'react'
|
||||
import { lazy, Suspense } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useSWR from 'swr'
|
||||
import { themeMode } from '@/store'
|
||||
import { getRuntimeYaml, useClash } from '@nyanpasu/interface'
|
||||
import {
|
||||
useProfile,
|
||||
useProfileContent,
|
||||
useRuntimeProfile,
|
||||
} from '@nyanpasu/interface'
|
||||
import { BaseDialog, cn } from '@nyanpasu/ui'
|
||||
|
||||
const MonacoDiffEditor = lazy(() => import('./profile-monaco-diff-viewer'))
|
||||
@@ -19,30 +23,24 @@ export default function RuntimeConfigDiffDialog({
|
||||
onClose,
|
||||
}: RuntimeConfigDiffDialogProps) {
|
||||
const { t } = useTranslation()
|
||||
const { getProfiles, getProfileFile } = useClash()
|
||||
const currentProfileUid = getProfiles.data?.current
|
||||
|
||||
const { query } = useProfile()
|
||||
|
||||
const currentProfileUid = query.data?.current?.[0]
|
||||
|
||||
const contentFn = useProfileContent(currentProfileUid || '')
|
||||
|
||||
// need manual refetch
|
||||
contentFn.query.refetch()
|
||||
|
||||
const runtimeProfile = useRuntimeProfile()
|
||||
|
||||
const loaded = !contentFn.query.isLoading && !query.isLoading
|
||||
|
||||
const mode = useAtomValue(themeMode)
|
||||
const { data: runtimeConfig, isLoading: isLoadingRuntimeConfig } = useSWR(
|
||||
open ? '/getRuntimeConfigYaml' : null,
|
||||
getRuntimeYaml,
|
||||
{},
|
||||
)
|
||||
const { data: profileConfig, isLoading: isLoadingProfileConfig } = useSWR(
|
||||
open ? `/readProfileFile?uid=${currentProfileUid}` : null,
|
||||
async (key) => {
|
||||
const url = new URL(key, window.location.origin)
|
||||
return await getProfileFile(url.searchParams.get('uid')!)
|
||||
},
|
||||
{
|
||||
revalidateOnFocus: true,
|
||||
refreshInterval: 0,
|
||||
},
|
||||
)
|
||||
|
||||
const loaded = !isLoadingRuntimeConfig && !isLoadingProfileConfig
|
||||
|
||||
const originalModelPath = useMemo(() => `${nanoid()}.clash.yaml`, [])
|
||||
const modifiedModelPath = useMemo(() => `${nanoid()}.runtime.yaml`, [])
|
||||
const originalModelPath = useCreation(() => `${nanoid()}.clash.yaml`, [])
|
||||
const modifiedModelPath = useCreation(() => `${nanoid()}.runtime.yaml`, [])
|
||||
|
||||
if (!currentProfileUid) {
|
||||
return null
|
||||
@@ -68,9 +66,9 @@ export default function RuntimeConfigDiffDialog({
|
||||
<MonacoDiffEditor
|
||||
language="yaml"
|
||||
theme={mode === 'light' ? 'vs' : 'vs-dark'}
|
||||
original={profileConfig}
|
||||
original={contentFn.query.data}
|
||||
originalModelPath={originalModelPath}
|
||||
modified={runtimeConfig}
|
||||
modified={runtimeProfile.data}
|
||||
modifiedModelPath={modifiedModelPath}
|
||||
options={{
|
||||
minimap: { enabled: false },
|
||||
|
||||
@@ -5,10 +5,16 @@ import { SelectElement, TextFieldElement, useForm } from 'react-hook-form-mui'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { message } from '@/utils/notification'
|
||||
import { Divider } from '@mui/material'
|
||||
import { Profile, useClash } from '@nyanpasu/interface'
|
||||
import {
|
||||
Profile,
|
||||
ProfileTemplate,
|
||||
useClash,
|
||||
useProfile,
|
||||
useProfileContent,
|
||||
} from '@nyanpasu/interface'
|
||||
import { BaseDialog, BaseDialogProps } from '@nyanpasu/ui'
|
||||
import LanguageChip from './modules/language-chip'
|
||||
import { getLanguage } from './utils'
|
||||
import { getLanguage, ProfileType, ProfileTypes } from './utils'
|
||||
|
||||
const ProfileMonacoViewer = lazy(() => import('./profile-monaco-viewer'))
|
||||
|
||||
@@ -21,25 +27,25 @@ const formCommonProps = {
|
||||
const optionTypeMapping = [
|
||||
{
|
||||
id: 'js',
|
||||
value: Profile.Type.JavaScript,
|
||||
value: ProfileTypes.JavaScript,
|
||||
language: 'javascript',
|
||||
label: 'JavaScript',
|
||||
},
|
||||
{
|
||||
id: 'lua',
|
||||
value: Profile.Type.LuaScript,
|
||||
value: ProfileTypes.LuaScript,
|
||||
language: 'lua',
|
||||
label: 'LuaScript',
|
||||
},
|
||||
{
|
||||
id: 'merge',
|
||||
value: Profile.Type.Merge,
|
||||
value: ProfileTypes.Merge,
|
||||
language: 'yaml',
|
||||
label: 'Merge',
|
||||
},
|
||||
]
|
||||
|
||||
const convertTypeMapping = (data: Profile.Item) => {
|
||||
const convertTypeMapping = (data: Profile) => {
|
||||
optionTypeMapping.forEach((option) => {
|
||||
if (option.id === data.type) {
|
||||
data.type = option.value
|
||||
@@ -52,7 +58,7 @@ const convertTypeMapping = (data: Profile.Item) => {
|
||||
export interface ScriptDialogProps extends Omit<BaseDialogProps, 'title'> {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
profile?: Profile.Item
|
||||
profile?: Profile
|
||||
}
|
||||
|
||||
export const ScriptDialog = ({
|
||||
@@ -63,10 +69,14 @@ export const ScriptDialog = ({
|
||||
}: ScriptDialogProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { getProfileFile, setProfileFile, createProfile, setProfiles } =
|
||||
useClash()
|
||||
// const { getProfileFile, setProfileFile, createProfile, setProfiles } =
|
||||
// useClash()
|
||||
|
||||
const form = useForm<Profile.Item>()
|
||||
const { create, update } = useProfile()
|
||||
|
||||
const contentFn = useProfileContent(profile?.uid ?? '')
|
||||
|
||||
const form = useForm<Profile>()
|
||||
|
||||
const isEdit = Boolean(profile)
|
||||
|
||||
@@ -81,16 +91,16 @@ export const ScriptDialog = ({
|
||||
desc: '',
|
||||
})
|
||||
}
|
||||
}, [form, isEdit, profile])
|
||||
}, [form, isEdit, profile, t])
|
||||
|
||||
const [openMonaco, setOpenMonaco] = useState(false)
|
||||
|
||||
const editor = useReactive<{
|
||||
value: string
|
||||
language: string
|
||||
rawType: Profile.Item['type']
|
||||
rawType: ProfileType
|
||||
}>({
|
||||
value: Profile.Template.merge,
|
||||
value: ProfileTemplate.merge,
|
||||
language: 'yaml',
|
||||
rawType: 'merge',
|
||||
})
|
||||
@@ -118,10 +128,19 @@ export const ScriptDialog = ({
|
||||
|
||||
try {
|
||||
if (isEdit) {
|
||||
await setProfileFile(data.uid, editorValue)
|
||||
await setProfiles(data.uid, data)
|
||||
await contentFn.upsert.mutateAsync(editorValue)
|
||||
await update.mutateAsync({
|
||||
uid: data.uid,
|
||||
profile: data,
|
||||
})
|
||||
} else {
|
||||
await createProfile(data, editorValue)
|
||||
await create.mutateAsync({
|
||||
type: 'manual',
|
||||
data: {
|
||||
item: data,
|
||||
fileData: editorValue,
|
||||
},
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
onClose()
|
||||
@@ -130,10 +149,12 @@ export const ScriptDialog = ({
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
if (isEdit) {
|
||||
editor.value = await getProfileFile(profile?.uid)
|
||||
editor.language = getLanguage(profile?.type)!
|
||||
await contentFn.query.refetch()
|
||||
|
||||
editor.value = contentFn.query.data ?? ''
|
||||
editor.language = getLanguage(profile!.type)!
|
||||
} else {
|
||||
editor.value = Profile.Template.merge
|
||||
editor.value = ProfileTemplate.merge
|
||||
editor.language = 'yaml'
|
||||
}
|
||||
|
||||
@@ -155,17 +176,17 @@ export const ScriptDialog = ({
|
||||
|
||||
switch (lang) {
|
||||
case 'yaml': {
|
||||
editor.value = Profile.Template.merge
|
||||
editor.value = ProfileTemplate.merge
|
||||
break
|
||||
}
|
||||
|
||||
case 'lua': {
|
||||
editor.value = Profile.Template.luascript
|
||||
editor.value = ProfileTemplate.luascript
|
||||
break
|
||||
}
|
||||
|
||||
case 'javascript': {
|
||||
editor.value = Profile.Template.javascript
|
||||
editor.value = ProfileTemplate.javascript
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -177,7 +198,9 @@ export const ScriptDialog = ({
|
||||
<div className="flex gap-2">
|
||||
<span>{isEdit ? t('Edit Script') : t('New Script')}</span>
|
||||
|
||||
<LanguageChip type={isEdit ? profile?.type : editor.rawType} />
|
||||
<LanguageChip
|
||||
type={isEdit ? (profile?.type ?? editor.rawType) : editor.rawType}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
open={open}
|
||||
@@ -242,7 +265,7 @@ export const ScriptDialog = ({
|
||||
editorMarks.current = marks
|
||||
}}
|
||||
schemaType={
|
||||
editor.rawType === Profile.Type.Merge ? 'merge' : undefined
|
||||
editor.rawType === ProfileTypes.Merge ? 'merge' : undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,54 +1,80 @@
|
||||
import { isEqual } from 'lodash-es'
|
||||
import { Profile } from '@nyanpasu/interface'
|
||||
import type {
|
||||
LocalProfile,
|
||||
MergeProfile,
|
||||
Profile,
|
||||
RemoteProfile,
|
||||
ScriptProfile,
|
||||
} from '@nyanpasu/interface'
|
||||
|
||||
export const filterProfiles = (items?: Profile.Item[]) => {
|
||||
const getItems = (types: (string | { script: string })[]) => {
|
||||
return items?.filter((i) => {
|
||||
if (!i) return false
|
||||
/**
|
||||
* Represents a Clash configuration profile, which can be either locally stored or fetched from a remote source.
|
||||
*/
|
||||
export type ClashProfile = LocalProfile | RemoteProfile
|
||||
|
||||
if (typeof i.type === 'string') {
|
||||
return types.includes(i.type)
|
||||
}
|
||||
/**
|
||||
* Represents a Clash configuration profile that is a chain of multiple profiles.
|
||||
*/
|
||||
export type ChainProfile = MergeProfile | ScriptProfile
|
||||
|
||||
if (typeof i.type === 'object' && i.type !== null) {
|
||||
return types.some(
|
||||
(type) =>
|
||||
typeof type === 'object' &&
|
||||
(i.type as { script: string }).script === type.script,
|
||||
)
|
||||
}
|
||||
/**
|
||||
* Filters an array of profiles into two categories: clash and chain profiles.
|
||||
*
|
||||
* @param items - Array of Profile objects to be filtered
|
||||
* @returns An object containing two arrays:
|
||||
* - clash: Array of profiles where type is 'remote' or 'local'
|
||||
* - chain: Array of profiles where type is 'merge' or has a script property
|
||||
*/
|
||||
export function filterProfiles<T extends Profile>(items?: T[]) {
|
||||
/**
|
||||
* Filters the input array to include only items of type 'remote' or 'local'
|
||||
* @param items - Array of items to filter
|
||||
* @returns {Array} Filtered array containing only remote and local items
|
||||
*/
|
||||
const clash = items?.filter(
|
||||
(item) => item.type === 'remote' || item.type === 'local',
|
||||
)
|
||||
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
const profiles = getItems([Profile.Type.Local, Profile.Type.Remote])
|
||||
|
||||
const scripts = getItems([
|
||||
Profile.Type.Merge,
|
||||
Profile.Type.JavaScript,
|
||||
Profile.Type.LuaScript,
|
||||
])
|
||||
/**
|
||||
* Filters an array of items to get a chain of either 'merge' type items
|
||||
* or items with a script property in their type object.
|
||||
*
|
||||
* @param {Array<{ type: string | { script: 'javascript' | 'lua' } }>} items - The array of items to filter
|
||||
* @returns {Array<{ type: string | { script: 'javascript' | 'lua' } }>} A filtered array containing only merge items or items with scripts
|
||||
*/
|
||||
const chain = items?.filter(
|
||||
(item) =>
|
||||
item.type === 'merge' ||
|
||||
(typeof item.type === 'object' && item.type.script),
|
||||
)
|
||||
|
||||
return {
|
||||
profiles,
|
||||
scripts,
|
||||
clash,
|
||||
chain,
|
||||
}
|
||||
}
|
||||
|
||||
export const getLanguage = (type: Profile.Item['type'], snake?: boolean) => {
|
||||
export type ProfileType = Profile['type']
|
||||
|
||||
export const ProfileTypes = {
|
||||
JavaScript: { script: 'javascript' },
|
||||
LuaScript: { script: 'lua' },
|
||||
Merge: 'merge',
|
||||
} as const
|
||||
|
||||
export const getLanguage = (type: ProfileType, snake?: boolean) => {
|
||||
switch (true) {
|
||||
case isEqual(type, Profile.Type.JavaScript):
|
||||
case isEqual(type, Profile.Type.JavaScript.script): {
|
||||
case isEqual(type, ProfileTypes.JavaScript):
|
||||
case isEqual(type, ProfileTypes.JavaScript.script): {
|
||||
return snake ? 'JavaScript' : 'javascript'
|
||||
}
|
||||
|
||||
case isEqual(type, Profile.Type.LuaScript):
|
||||
case isEqual(type, Profile.Type.LuaScript.script): {
|
||||
case isEqual(type, ProfileTypes.LuaScript):
|
||||
case isEqual(type, ProfileTypes.LuaScript.script): {
|
||||
return snake ? 'Lua' : 'lua'
|
||||
}
|
||||
|
||||
case isEqual(type, Profile.Type.Merge): {
|
||||
case isEqual(type, ProfileTypes.Merge): {
|
||||
return snake ? 'YAML' : 'yaml'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CSSProperties, memo, useMemo } from 'react'
|
||||
import { alpha, useTheme } from '@mui/material'
|
||||
import Box from '@mui/material/Box'
|
||||
import { Clash } from '@nyanpasu/interface'
|
||||
import { ProxyItem } from '@nyanpasu/interface'
|
||||
import { cn } from '@nyanpasu/ui'
|
||||
import { PaperSwitchButton } from '../setting/modules/system-proxy'
|
||||
import DelayChip from './delay-chip'
|
||||
@@ -17,8 +17,8 @@ export const NodeCard = memo(function NodeCard({
|
||||
onClickDelay,
|
||||
style,
|
||||
}: {
|
||||
node: Clash.Proxy<string>
|
||||
now?: string
|
||||
node: ProxyItem
|
||||
now?: string | null
|
||||
disabled?: boolean
|
||||
onClick: () => void
|
||||
onClickDelay: () => Promise<void>
|
||||
|
||||
@@ -13,12 +13,17 @@ import {
|
||||
import { Virtualizer, VListHandle } from 'virtua'
|
||||
import { proxyGroupAtom, proxyGroupSortAtom } from '@/store'
|
||||
import { proxiesFilterAtom } from '@/store/proxies'
|
||||
import { Clash, useClashCore, useNyanpasu } from '@nyanpasu/interface'
|
||||
import {
|
||||
ProxyGroupItem,
|
||||
ProxyItem,
|
||||
useClashCore,
|
||||
useNyanpasu,
|
||||
} from '@nyanpasu/interface'
|
||||
import { cn, useBreakpointValue } from '@nyanpasu/ui'
|
||||
import NodeCard from './node-card'
|
||||
import { nodeSortingFn } from './utils'
|
||||
|
||||
type RenderClashProxy = Clash.Proxy<string> & { renderLayoutKey: string }
|
||||
type RenderClashProxy = ProxyItem & { renderLayoutKey: string }
|
||||
|
||||
export interface NodeListRef {
|
||||
scrollToCurrent: () => void
|
||||
@@ -44,12 +49,13 @@ export const NodeList = forwardRef(function NodeList(
|
||||
|
||||
const proxyGroupSort = useAtomValue(proxyGroupSortAtom)
|
||||
|
||||
const [group, setGroup] = useState<Clash.Proxy<Clash.Proxy<string>>>()
|
||||
const [group, setGroup] = useState<ProxyGroupItem>()
|
||||
|
||||
const sortGroup = useCallback(() => {
|
||||
if (!getCurrentMode.global) {
|
||||
if (proxyGroup.selector !== null) {
|
||||
const selectedGroup = data?.groups[proxyGroup.selector]
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
|
||||
const selectedGroup = data?.groups[proxyGroup.selector]!
|
||||
|
||||
if (selectedGroup) {
|
||||
setGroup(nodeSortingFn(selectedGroup, proxyGroupSort))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Clash } from '@nyanpasu/interface'
|
||||
import type { Clash, ProxyGroupItem } from '@nyanpasu/interface'
|
||||
|
||||
export type History = Clash.Proxy['history']
|
||||
|
||||
@@ -17,7 +17,7 @@ export enum SortType {
|
||||
}
|
||||
|
||||
export const nodeSortingFn = (
|
||||
selectedGroup: Clash.Proxy<Clash.Proxy<string>>,
|
||||
selectedGroup: ProxyGroupItem,
|
||||
type: SortType,
|
||||
) => {
|
||||
let sortedList = selectedGroup.all?.slice()
|
||||
|
||||
@@ -26,7 +26,7 @@ import { openThat } from '@nyanpasu/interface'
|
||||
export const renderChip = (
|
||||
string: string,
|
||||
labels: {
|
||||
[label: string]: string | number | undefined
|
||||
[label: string]: string | number | undefined | null
|
||||
},
|
||||
): (string | ReactElement)[] => {
|
||||
return string.split(/(%[^&?]+)/).map((part, index) => {
|
||||
@@ -95,7 +95,7 @@ export const extractServer = (
|
||||
export const openWebUrl = (
|
||||
string: string,
|
||||
labels: {
|
||||
[label: string]: string | number | undefined
|
||||
[label: string]: string | number | undefined | null
|
||||
},
|
||||
): void => {
|
||||
let url = ''
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import CLASH_FIELD from '@/assets/json/clash-field.json'
|
||||
import { Box, Typography } from '@mui/material'
|
||||
import Grid from '@mui/material/Grid2'
|
||||
import { useClash, useNyanpasu } from '@nyanpasu/interface'
|
||||
import { useClash, useNyanpasu, useProfile } from '@nyanpasu/interface'
|
||||
import { BaseCard, BaseDialog } from '@nyanpasu/ui'
|
||||
import { ClashFieldItem, LabelSwitch } from './modules/clash-field'
|
||||
|
||||
@@ -91,7 +91,7 @@ const ClashFieldSwitch = () => {
|
||||
export const SettingClashField = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { getProfiles, setProfilesConfig } = useClash()
|
||||
const { query, upsert } = useProfile()
|
||||
|
||||
const mergeFields = useMemo(
|
||||
() => [
|
||||
@@ -99,9 +99,9 @@ export const SettingClashField = () => {
|
||||
...Object.keys(CLASH_FIELD.default),
|
||||
...Object.keys(CLASH_FIELD.handle),
|
||||
],
|
||||
...(getProfiles.data?.valid ?? []),
|
||||
...(query.data?.valid ?? []),
|
||||
],
|
||||
[getProfiles.data],
|
||||
[query.data],
|
||||
)
|
||||
|
||||
const filteredField = (fields: { [key: string]: string }): string[] => {
|
||||
@@ -121,7 +121,7 @@ export const SettingClashField = () => {
|
||||
|
||||
const updateFiled = async (key: string) => {
|
||||
const getFields = (): string[] => {
|
||||
const valid = getProfiles.data?.valid ?? []
|
||||
const valid = query.data?.valid ?? []
|
||||
|
||||
if (valid.includes(key)) {
|
||||
return valid.filter((item) => item !== key)
|
||||
@@ -132,7 +132,7 @@ export const SettingClashField = () => {
|
||||
}
|
||||
}
|
||||
|
||||
await setProfilesConfig({ valid: getFields() })
|
||||
await upsert.mutateAsync({ valid: getFields() })
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,20 +1,38 @@
|
||||
import { useAtom } from 'jotai'
|
||||
import { MuiColorInput } from 'mui-color-input'
|
||||
import { useRef, useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { isHexColor } from 'validator'
|
||||
import { defaultTheme } from '@/pages/-theme'
|
||||
import { atomIsDrawerOnlyIcon } from '@/store'
|
||||
import { languageOptions } from '@/utils/language'
|
||||
import Done from '@mui/icons-material/Done'
|
||||
import { Box, Button, List, ListItem, ListItemText } from '@mui/material'
|
||||
import { useNyanpasu, VergeConfig } from '@nyanpasu/interface'
|
||||
import { Button, List, ListItem, ListItemText } from '@mui/material'
|
||||
import { useSetting } from '@nyanpasu/interface'
|
||||
import { BaseCard, Expand, MenuItem, SwitchItem } from '@nyanpasu/ui'
|
||||
|
||||
export const SettingNyanpasuUI = () => {
|
||||
const commonSx = {
|
||||
width: 128,
|
||||
}
|
||||
|
||||
const LanguageSwitch = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { nyanpasuConfig, setNyanpasuConfig } = useNyanpasu()
|
||||
const language = useSetting('language')
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
label={t('Language')}
|
||||
selectSx={commonSx}
|
||||
options={languageOptions}
|
||||
selected={language.value || 'en'}
|
||||
onSelected={(value) => language.upsert(value as string)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const ThemeSwitch = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const themeOptions = {
|
||||
dark: t('theme.dark'),
|
||||
@@ -22,89 +40,80 @@ export const SettingNyanpasuUI = () => {
|
||||
system: t('theme.system'),
|
||||
}
|
||||
|
||||
const [themeColor, setThemeColor] = useState(
|
||||
nyanpasuConfig?.theme_setting?.primary_color,
|
||||
)
|
||||
const themeColorRef = useRef(themeColor)
|
||||
const themeMode = useSetting('theme_mode')
|
||||
|
||||
const commonSx = {
|
||||
width: 128,
|
||||
}
|
||||
return (
|
||||
<MenuItem
|
||||
label={t('Theme Mode')}
|
||||
selectSx={commonSx}
|
||||
options={themeOptions}
|
||||
selected={themeMode.value || 'system'}
|
||||
onSelected={(value) => themeMode.upsert(value as string)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const ThemeColor = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const theme = useSetting('theme_color')
|
||||
|
||||
const [value, setValue] = useState(theme.value ?? defaultTheme.primary_color)
|
||||
|
||||
useEffect(() => {
|
||||
setValue(theme.value ?? defaultTheme.primary_color)
|
||||
}, [theme.value])
|
||||
|
||||
return (
|
||||
<>
|
||||
<ListItem sx={{ pl: 0, pr: 0 }}>
|
||||
<ListItemText primary={t('Theme Setting')} />
|
||||
|
||||
<MuiColorInput
|
||||
size="small"
|
||||
sx={commonSx}
|
||||
value={theme.value ?? '#1867c0'}
|
||||
isAlphaHidden
|
||||
format="hex"
|
||||
onBlur={() => {
|
||||
if (!isHexColor(value ?? defaultTheme.primary_color)) {
|
||||
setValue(value)
|
||||
}
|
||||
}}
|
||||
onChange={(color: string) => setValue(color)}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<Expand open={theme.value !== value}>
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<Done />}
|
||||
onClick={() => {
|
||||
theme.upsert(value)
|
||||
}}
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
</Expand>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const SettingNyanpasuUI = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [onlyIcon, setOnlyIcon] = useAtom(atomIsDrawerOnlyIcon)
|
||||
|
||||
return (
|
||||
<BaseCard label={t('User Interface')}>
|
||||
<List disablePadding>
|
||||
<MenuItem
|
||||
label={t('Language')}
|
||||
selectSx={commonSx}
|
||||
options={languageOptions}
|
||||
selected={nyanpasuConfig?.language || 'en'}
|
||||
onSelected={(value) =>
|
||||
setNyanpasuConfig({ language: value as string })
|
||||
}
|
||||
/>
|
||||
<LanguageSwitch />
|
||||
|
||||
<MenuItem
|
||||
label={t('Theme Mode')}
|
||||
selectSx={commonSx}
|
||||
options={themeOptions}
|
||||
selected={nyanpasuConfig?.theme_mode || 'light'}
|
||||
onSelected={(value) =>
|
||||
setNyanpasuConfig({
|
||||
theme_mode: value as VergeConfig['theme_mode'],
|
||||
})
|
||||
}
|
||||
/>
|
||||
<ThemeSwitch />
|
||||
|
||||
<ListItem sx={{ pl: 0, pr: 0 }}>
|
||||
<ListItemText primary={t('Theme Setting')} />
|
||||
|
||||
<MuiColorInput
|
||||
size="small"
|
||||
sx={commonSx}
|
||||
value={themeColor ?? defaultTheme.primary_color}
|
||||
isAlphaHidden
|
||||
format="hex"
|
||||
onBlur={() => {
|
||||
if (
|
||||
!isHexColor(themeColorRef.current ?? defaultTheme.primary_color)
|
||||
) {
|
||||
setThemeColor(themeColorRef.current)
|
||||
return
|
||||
}
|
||||
themeColorRef.current = themeColor
|
||||
}}
|
||||
onChange={(color: string) => setThemeColor(color)}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<Expand
|
||||
open={nyanpasuConfig?.theme_setting?.primary_color !== themeColor}
|
||||
>
|
||||
<Box
|
||||
sx={{ pb: 1 }}
|
||||
display="flex"
|
||||
justifyContent="end"
|
||||
alignItems="center"
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<Done />}
|
||||
onClick={() => {
|
||||
setNyanpasuConfig({
|
||||
theme_setting: {
|
||||
...nyanpasuConfig?.theme_setting,
|
||||
primary_color: themeColor,
|
||||
},
|
||||
})
|
||||
}}
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
</Box>
|
||||
</Expand>
|
||||
<ThemeColor />
|
||||
|
||||
<SwitchItem
|
||||
label={t('Icon Navigation Bar')}
|
||||
|
||||
+153
-132
@@ -1,63 +1,167 @@
|
||||
import { useLockFn, useReactive } from 'ahooks'
|
||||
import { useLockFn } from 'ahooks'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { message } from '@/utils/notification'
|
||||
import { Done } from '@mui/icons-material'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
InputAdornment,
|
||||
List,
|
||||
ListItem,
|
||||
TextField,
|
||||
Typography,
|
||||
} from '@mui/material'
|
||||
import { InputAdornment, List, ListItem } from '@mui/material'
|
||||
import Grid from '@mui/material/Grid2'
|
||||
import { useNyanpasu } from '@nyanpasu/interface'
|
||||
import { useSetting, useSystemProxy } from '@nyanpasu/interface'
|
||||
import {
|
||||
BaseCard,
|
||||
Expand,
|
||||
ExpandMore,
|
||||
NumberItem,
|
||||
SwitchItem,
|
||||
TextItem,
|
||||
} from '@nyanpasu/ui'
|
||||
import { PaperSwitchButton } from './modules/system-proxy'
|
||||
|
||||
const TunModeButton = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const tunMode = useSetting('enable_tun_mode')
|
||||
|
||||
const handleTunMode = useLockFn(async () => {
|
||||
try {
|
||||
await tunMode.upsert(!tunMode.value)
|
||||
} catch (error) {
|
||||
message(`Activation TUN Mode failed!`, {
|
||||
title: t('Error'),
|
||||
kind: 'error',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<PaperSwitchButton
|
||||
label={t('TUN Mode')}
|
||||
checked={Boolean(tunMode.value)}
|
||||
onClick={handleTunMode}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const SystemProxyButton = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const systemProxy = useSetting('enable_system_proxy')
|
||||
|
||||
const handleSystemProxy = useLockFn(async () => {
|
||||
try {
|
||||
await systemProxy.upsert(!systemProxy.value)
|
||||
} catch (error) {
|
||||
message(`Activation System Proxy failed!`, {
|
||||
title: t('Error'),
|
||||
kind: 'error',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<PaperSwitchButton
|
||||
label={t('System Proxy')}
|
||||
checked={Boolean(systemProxy.value)}
|
||||
onClick={handleSystemProxy}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const ProxyGuardSwitch = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const proxyGuard = useSetting('enable_proxy_guard')
|
||||
|
||||
const handleProxyGuard = useLockFn(async () => {
|
||||
try {
|
||||
await proxyGuard.upsert(!proxyGuard.value)
|
||||
} catch (error) {
|
||||
message(`Activation Proxy Guard failed!`, {
|
||||
title: t('Error'),
|
||||
kind: 'error',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<SwitchItem
|
||||
label={t('Proxy Guard')}
|
||||
checked={Boolean(proxyGuard.value)}
|
||||
onClick={handleProxyGuard}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const ProxyGuardInterval = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const proxyGuardInterval = useSetting('proxy_guard_interval')
|
||||
|
||||
return (
|
||||
<NumberItem
|
||||
label={t('Guard Interval')}
|
||||
value={proxyGuardInterval.value || 0}
|
||||
checkEvent={(input) => input <= 0}
|
||||
checkLabel={t('The interval must be greater than 0 second')}
|
||||
onApply={(value) => {
|
||||
proxyGuardInterval.upsert(value)
|
||||
}}
|
||||
textFieldProps={{
|
||||
inputProps: {
|
||||
'aria-autocomplete': 'none',
|
||||
},
|
||||
InputProps: {
|
||||
endAdornment: <InputAdornment position="end">s</InputAdornment>,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const SystemProxyBypass = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const systemProxyBypass = useSetting('system_proxy_bypass')
|
||||
|
||||
return (
|
||||
<TextItem
|
||||
label={t('Proxy Bypass')}
|
||||
value={systemProxyBypass.data || ''}
|
||||
onApply={(value) => {
|
||||
systemProxyBypass.upsert(value)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const CurrentSystemProxy = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { data } = useSystemProxy()
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
className="!w-full !flex-col !items-start select-text"
|
||||
sx={{ pl: 0, pr: 0 }}
|
||||
>
|
||||
<div className="text-base leading-10">{t('Current System Proxy')}</div>
|
||||
|
||||
{Object.entries(data ?? []).map(([key, value], index) => {
|
||||
return (
|
||||
<div key={index} className="flex w-full leading-8">
|
||||
<div className="w-28 capitalize">{key}:</div>
|
||||
|
||||
<div className="text-warp flex-1 break-all">{String(value)}</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</ListItem>
|
||||
)
|
||||
}
|
||||
|
||||
export const SettingSystemProxy = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { nyanpasuConfig, setNyanpasuConfig, getSystemProxy } = useNyanpasu()
|
||||
|
||||
const loading = useReactive({
|
||||
enable_tun_mode: false,
|
||||
enable_system_proxy: false,
|
||||
})
|
||||
|
||||
const handleClick = useLockFn(
|
||||
async (key: 'enable_system_proxy' | 'enable_tun_mode') => {
|
||||
try {
|
||||
loading[key] = true
|
||||
|
||||
await setNyanpasuConfig({
|
||||
[key]: !nyanpasuConfig?.[key],
|
||||
})
|
||||
} catch (e) {
|
||||
message(`Activation failed!`, {
|
||||
title: t('Error'),
|
||||
kind: 'error',
|
||||
})
|
||||
} finally {
|
||||
loading[key] = false
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
const [expand, setExpand] = useState(false)
|
||||
|
||||
const [proxyBypass, setProxyBypass] = useState(
|
||||
nyanpasuConfig?.system_proxy_bypass || '',
|
||||
)
|
||||
|
||||
return (
|
||||
<BaseCard
|
||||
label={t('System Setting')}
|
||||
@@ -66,107 +170,24 @@ export const SettingSystemProxy = () => {
|
||||
}
|
||||
>
|
||||
<Grid container spacing={2}>
|
||||
<Grid
|
||||
size={{
|
||||
xs: 6,
|
||||
}}
|
||||
>
|
||||
<PaperSwitchButton
|
||||
label={t('TUN Mode')}
|
||||
checked={nyanpasuConfig?.enable_tun_mode || false}
|
||||
loading={loading.enable_tun_mode}
|
||||
onClick={() => handleClick('enable_tun_mode')}
|
||||
/>
|
||||
<Grid size={{ xs: 6 }}>
|
||||
<TunModeButton />
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 6 }}>
|
||||
<PaperSwitchButton
|
||||
label={t('System Proxy')}
|
||||
checked={nyanpasuConfig?.enable_system_proxy || false}
|
||||
loading={loading.enable_system_proxy}
|
||||
onClick={() => handleClick('enable_system_proxy')}
|
||||
/>
|
||||
<SystemProxyButton />
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Expand open={expand}>
|
||||
<List disablePadding sx={{ pt: 1 }}>
|
||||
<SwitchItem
|
||||
label={t('Proxy Guard')}
|
||||
checked={nyanpasuConfig?.enable_proxy_guard || false}
|
||||
onChange={() =>
|
||||
setNyanpasuConfig({
|
||||
enable_proxy_guard: !nyanpasuConfig?.enable_proxy_guard,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<ProxyGuardSwitch />
|
||||
|
||||
<NumberItem
|
||||
label={t('Guard Interval')}
|
||||
value={nyanpasuConfig?.proxy_guard_interval || 0}
|
||||
checkEvent={(input) => input <= 0}
|
||||
checkLabel={t('The interval must be greater than 0 second')}
|
||||
onApply={(value) => {
|
||||
setNyanpasuConfig({ proxy_guard_interval: value })
|
||||
}}
|
||||
textFieldProps={{
|
||||
inputProps: {
|
||||
'aria-autocomplete': 'none',
|
||||
},
|
||||
InputProps: {
|
||||
endAdornment: <InputAdornment position="end">s</InputAdornment>,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<ProxyGuardInterval />
|
||||
|
||||
<ListItem sx={{ pl: 0, pr: 0 }}>
|
||||
<TextField
|
||||
value={proxyBypass}
|
||||
label={t('Proxy Bypass')}
|
||||
variant="outlined"
|
||||
sx={{ width: '100%' }}
|
||||
multiline
|
||||
onChange={(e) => setProxyBypass(e.target.value)}
|
||||
/>
|
||||
</ListItem>
|
||||
<SystemProxyBypass />
|
||||
|
||||
<Expand open={proxyBypass !== nyanpasuConfig?.system_proxy_bypass}>
|
||||
<Box sx={{ pb: 1 }} display="flex" justifyContent="end">
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<Done />}
|
||||
onClick={() => {
|
||||
setNyanpasuConfig({ system_proxy_bypass: proxyBypass })
|
||||
}}
|
||||
>
|
||||
{t('Apply')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Expand>
|
||||
|
||||
<ListItem sx={{ pl: 0, pr: 0 }}>
|
||||
<Box>
|
||||
<Typography variant="body1" sx={{ fontSize: '18px', mb: 1 }}>
|
||||
{t('Current System Proxy')}
|
||||
</Typography>
|
||||
|
||||
{Object.entries(getSystemProxy?.data ?? []).map(
|
||||
([key, value], index) => {
|
||||
return (
|
||||
<Box key={index} display="flex" sx={{ pt: 1 }}>
|
||||
<Typography
|
||||
sx={{ width: 80, textTransform: 'capitalize' }}
|
||||
>
|
||||
{key}:
|
||||
</Typography>
|
||||
|
||||
<Typography>{String(value)}</Typography>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
)}
|
||||
</Box>
|
||||
</ListItem>
|
||||
<CurrentSystemProxy />
|
||||
</List>
|
||||
</Expand>
|
||||
</BaseCard>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"Active Connections": "Active Connections",
|
||||
"Timeout": "Timeout",
|
||||
"Click to Refresh Now": "Click to Refresh Now",
|
||||
"No Proxy": "No Proxy",
|
||||
"No Proxies": "No Proxies",
|
||||
"Direct Mode": "Direct Mode",
|
||||
"Rules": "Rules",
|
||||
"No Rules": "No Rules",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"Active Connections": "Активные соединения",
|
||||
"Timeout": "Тайм-аут",
|
||||
"Click to Refresh Now": "Нажмите для обновления",
|
||||
"No Proxy": "Без прокси",
|
||||
"No Proxies": "Без прокси",
|
||||
"Direct Mode": "Прямой режим",
|
||||
"Rules": "Правила",
|
||||
"No Rules": "Нет правил",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"Active Connections": "活动连接",
|
||||
"Timeout": "超时",
|
||||
"Click to Refresh Now": "点击立即刷新",
|
||||
"No Proxy": "无代理",
|
||||
"No Proxies": "无代理",
|
||||
"Direct Mode": "直连模式",
|
||||
"Rules": "规则",
|
||||
"No Rules": "无规则",
|
||||
@@ -54,7 +54,7 @@
|
||||
"Open": "打开",
|
||||
"Open File": "打开文件",
|
||||
"Update": "更新",
|
||||
"Update(Proxy)": "更新(代理)",
|
||||
"Update(Proxy)": "更新(使用代理)",
|
||||
"Delete": "删除",
|
||||
"Enable": "启用",
|
||||
"Disable": "禁用",
|
||||
@@ -62,9 +62,9 @@
|
||||
"To Top": "移到最前",
|
||||
"To End": "移到末尾",
|
||||
"Update All Profiles": "更新所有配置",
|
||||
"View Runtime Config": "查看运行时配置",
|
||||
"View Runtime Config": "查看运行配置",
|
||||
"Reactivate Profiles": "重新激活配置",
|
||||
"Location": "当前节点",
|
||||
"Location": "当前使用节点",
|
||||
"Delay check": "延迟测试",
|
||||
"Sort by default": "默认排序",
|
||||
"Sort by delay": "按延迟排序",
|
||||
@@ -171,7 +171,7 @@
|
||||
"Nyanpasu Version": "Nyanpasu 版本",
|
||||
"theme.light": "浅色",
|
||||
"theme.dark": "深色",
|
||||
"theme.system": "系统",
|
||||
"theme.system": "跟随系统",
|
||||
"Clash Field": "Clash 字段",
|
||||
"Original Config": "原始配置",
|
||||
"Runtime Config": "运行配置",
|
||||
@@ -239,7 +239,7 @@
|
||||
"Proxy Takeover Status": "代理接管状态",
|
||||
"Subscription Expires In": "{{time}}到期",
|
||||
"Subscription Updated At": "{{time}}更新",
|
||||
"Choose file to import or leave it blank to create new one": "选择文件导入或留空新建。",
|
||||
"Choose file to import or leave it blank to create new one": "选择文件导入,或留空以新建配置。",
|
||||
"updater": {
|
||||
"title": "发现新版本",
|
||||
"close": "忽略",
|
||||
@@ -268,7 +268,7 @@
|
||||
},
|
||||
"service": "服务",
|
||||
"UI": "用户界面",
|
||||
"Service Manual Tips": "服务提示手册",
|
||||
"Service Manual Tips": "有关服务的提示",
|
||||
"Unable to operation the service automatically": "无法自动{{operation}}服务。请导航到内核所在目录,在 Windows 上以管理员身份打开 PowerShell或在 macOS/Linux 上打开终端仿真器,然后执行以下命令:",
|
||||
"Successfully switched to the clash core": "成功切换至 {{core}} 内核。",
|
||||
"Failed to switch. You could see the details in the log": "切换失败,可以在日志中查看详细信息。\n错误:{{error}}",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"Active Connections": "活動連線",
|
||||
"Timeout": "逾時",
|
||||
"Click to Refresh Now": "點擊立即重新整理",
|
||||
"No Proxy": "無代理",
|
||||
"No Proxies": "無代理",
|
||||
"Direct Mode": "直連模式",
|
||||
"Rules": "規則",
|
||||
"No Rules": "無規則",
|
||||
@@ -43,7 +43,7 @@
|
||||
"Close All": "關閉全部",
|
||||
"Menu": "選單",
|
||||
"Select": "使用",
|
||||
"Applying Profile": "正在應用設定檔……",
|
||||
"Applying Profile": "正在套用設定檔……",
|
||||
"Edit Info": "編輯資訊",
|
||||
"Proxy Chains": "代理鏈",
|
||||
"Global Proxy Chains": "全域鏈",
|
||||
@@ -89,7 +89,7 @@
|
||||
"Destination IP": "目標位址",
|
||||
"Destination ASN": "目標 ASN",
|
||||
"Type": "類型",
|
||||
"Connection Detail": "連接詳情",
|
||||
"Connection Detail": "連線詳情",
|
||||
"Metadata": "原始資訊",
|
||||
"Remote": "遠端",
|
||||
"Local": "本機",
|
||||
@@ -113,7 +113,7 @@
|
||||
"Clash Setting": "Clash 設定",
|
||||
"System Setting": "系統設定",
|
||||
"Nyanpasu Setting": "Nyanpasu 設定",
|
||||
"Allow LAN": "區域網路連接",
|
||||
"Allow LAN": "區域網路連線",
|
||||
"IPv6": "IPv6",
|
||||
"TUN Stack": "TUN 堆疊",
|
||||
"Log Level": "日誌等級",
|
||||
@@ -122,7 +122,7 @@
|
||||
"Random Port": "隨機埠",
|
||||
"After restart to take effect": "重啟後生效。",
|
||||
"Clash External Controll": "Clash 外部控制",
|
||||
"External Controller": "外部控制器監聽地址",
|
||||
"External Controller": "外部控制器監聽位址",
|
||||
"Port Strategy": "埠策略",
|
||||
"Allow Fallback": "允許 fallback",
|
||||
"Fixed": "固定",
|
||||
@@ -133,7 +133,7 @@
|
||||
"System Service": "系統服務",
|
||||
"Service Mode": "服務模式",
|
||||
"Initiating Behavior": "啟動行為",
|
||||
"Auto Start": "開機自啟",
|
||||
"Auto Start": "開機啟動",
|
||||
"Silent Start": "靜默啟動",
|
||||
"System Proxy": "系統代理",
|
||||
"Open UWP Tool": "UWP 工具",
|
||||
@@ -142,7 +142,7 @@
|
||||
"Guard Interval": "代理守衛間隔",
|
||||
"The interval must be greater than 0 second": "間隔時間必須大於 0 秒。",
|
||||
"Proxy Bypass": "代理繞過",
|
||||
"Apply": "應用",
|
||||
"Apply": "套用",
|
||||
"Current System Proxy": "目前系統代理",
|
||||
"User Interface": "使用者介面",
|
||||
"Theme Mode": "主題模式",
|
||||
@@ -161,7 +161,7 @@
|
||||
"Page Transition Animation None": "無",
|
||||
"Language": "語言設定",
|
||||
"Path Config": "目錄配置",
|
||||
"Migrate App Path": "遷移應用程式路徑",
|
||||
"Migrate App Path": "遷移 App 路徑",
|
||||
"Open Config Dir": "配置目錄",
|
||||
"Open Data Dir": "資料目錄",
|
||||
"Open Core Dir": "核心目錄",
|
||||
@@ -206,7 +206,7 @@
|
||||
"toggle_tun_mode": "切換 TUN 模式",
|
||||
"enable_tun_mode": "開啟 TUN 模式",
|
||||
"disable_tun_mode": "關閉 TUN 模式",
|
||||
"App Log Level": "應用程式日誌等級",
|
||||
"App Log Level": "App 日誌等級",
|
||||
"Auto Close Connections": "自動結束連線",
|
||||
"Enable Clash Fields Filter": "開啟 Clash 欄位過濾",
|
||||
"Enable Builtin Enhanced": "開啟內建增強功能",
|
||||
@@ -232,14 +232,14 @@
|
||||
"Update All Proxies Providers": "全部更新",
|
||||
"Lighten Up Animation Effects": "減輕動畫效果",
|
||||
"Subscription": "訂閱",
|
||||
"FetchError": "由於網路問題,無法獲取{{content}}內容。請檢查網路連接或稍後再試。",
|
||||
"FetchError": "由於網路問題,無法獲取{{content}}內容。請檢查網路連線或稍後再試。",
|
||||
"tun": "TUN 模式",
|
||||
"normal": "預設",
|
||||
"system_proxy": "系統代理",
|
||||
"Proxy Takeover Status": "代理接管狀態",
|
||||
"Subscription Expires In": "{{time}}過期",
|
||||
"Subscription Updated At": "{{time}}更新",
|
||||
"Choose file to import or leave it blank to create new one": "選取文件匯入,或留空以建立新檔。",
|
||||
"Choose file to import or leave it blank to create new one": "選取檔案匯入,或留空以建立新檔。",
|
||||
"updater": {
|
||||
"title": "發現新版本",
|
||||
"close": "忽略",
|
||||
@@ -274,11 +274,11 @@
|
||||
"Failed to switch. You could see the details in the log": "切換失敗,可以在日誌中查看詳細資訊。\n錯誤:{{error}}",
|
||||
"Successfully restarted the core": "成功重啟核心。",
|
||||
"Failed to restart. You could see the details in the log": "重啟失敗,詳細資訊請檢查日誌。\n\n錯誤:",
|
||||
"Failed to fetch. Please check your network connection": "獲取更新失敗,請檢查你的網路連接。",
|
||||
"Failed to fetch. Please check your network connection": "獲取更新失敗,請檢查你的網路連線。",
|
||||
"Successfully updated the core": "成功更新「{{core}}」核心。",
|
||||
"Failed to update": "更新失敗。{{error}}",
|
||||
"Multiple directories are not supported": "不支援多個目錄。",
|
||||
"Successfully changed the app directory": "應用程式目錄更改成功。",
|
||||
"Successfully changed the app directory": "App 目錄更改成功。",
|
||||
"Failed to migrate": "遷移失敗。{{error}}",
|
||||
"Web UI": "Web UI",
|
||||
"New Item": "添加新項目",
|
||||
|
||||
@@ -16,6 +16,10 @@ if (!window.ResizeObserver) {
|
||||
window.ResizeObserver = ResizeObserver
|
||||
}
|
||||
|
||||
window.addEventListener('error', (event) => {
|
||||
console.error(event)
|
||||
})
|
||||
|
||||
// Set up a Router instance
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
|
||||
@@ -20,13 +20,18 @@ import ProfileSide from '@/components/profiles/profile-side'
|
||||
import { GlobalUpdatePendingContext } from '@/components/profiles/provider'
|
||||
import { QuickImport } from '@/components/profiles/quick-import'
|
||||
import RuntimeConfigDiffDialog from '@/components/profiles/runtime-config-diff-dialog'
|
||||
import { filterProfiles } from '@/components/profiles/utils'
|
||||
import { ClashProfile, filterProfiles } from '@/components/profiles/utils'
|
||||
import { formatError } from '@/utils'
|
||||
import { message } from '@/utils/notification'
|
||||
import { Public, Update } from '@mui/icons-material'
|
||||
import { Badge, Button, CircularProgress, IconButton } from '@mui/material'
|
||||
import Grid from '@mui/material/Grid2'
|
||||
import { Profile, updateProfile, useClash } from '@nyanpasu/interface'
|
||||
import {
|
||||
RemoteProfileOptionsBuilder,
|
||||
useClash,
|
||||
useProfile,
|
||||
type RemoteProfile,
|
||||
} from '@nyanpasu/interface'
|
||||
import { FloatingButton, SidePage } from '@nyanpasu/ui'
|
||||
import { createFileRoute, useLocation } from '@tanstack/react-router'
|
||||
import { zodSearchValidator } from '@tanstack/router-zod-adapter'
|
||||
@@ -44,12 +49,23 @@ export const Route = createFileRoute('/profiles')({
|
||||
|
||||
function ProfilePage() {
|
||||
const { t } = useTranslation()
|
||||
const { getProfiles, getRuntimeLogs } = useClash()
|
||||
|
||||
const { getRuntimeLogs } = useClash()
|
||||
|
||||
const { query, update } = useProfile()
|
||||
|
||||
const profiles = useMemo(() => {
|
||||
return filterProfiles(query.data?.items)
|
||||
}, [query.data?.items])
|
||||
|
||||
const maxLogLevelTriggered = useMemo(() => {
|
||||
const currentProfileChains =
|
||||
getProfiles.data?.items?.find(
|
||||
// TODO: 支持多 Profile
|
||||
(item) => getProfiles.data?.current[0] === item.uid,
|
||||
(
|
||||
query.data?.items?.find(
|
||||
// TODO: 支持多 Profile
|
||||
(item) => query.data?.current?.[0] === item.uid,
|
||||
// TODO: fix any type
|
||||
) as any
|
||||
)?.chain || []
|
||||
return Object.entries(getRuntimeLogs.data || {}).reduce(
|
||||
(acc, [key, value]) => {
|
||||
@@ -78,8 +94,7 @@ function ProfilePage() {
|
||||
current: undefined | 'info' | 'error' | 'warn'
|
||||
},
|
||||
)
|
||||
}, [getRuntimeLogs.data, getProfiles.data])
|
||||
const { profiles } = filterProfiles(getProfiles.data?.items)
|
||||
}, [query.data, getRuntimeLogs.data])
|
||||
|
||||
const [globalChain, setGlobalChain] = useAtom(atomGlobalChainCurrent)
|
||||
|
||||
@@ -90,7 +105,7 @@ function ProfilePage() {
|
||||
setGlobalChain(!globalChain)
|
||||
}
|
||||
|
||||
const onClickChains = (profile: Profile.Item) => {
|
||||
const onClickChains = (profile: ClashProfile) => {
|
||||
setGlobalChain(false)
|
||||
|
||||
if (chainsSelected === profile.uid) {
|
||||
@@ -124,17 +139,31 @@ function ProfilePage() {
|
||||
|
||||
const [globalUpdatePending, startGlobalUpdate] = useTransition()
|
||||
const handleGlobalProfileUpdate = useLockFn(async () => {
|
||||
await startGlobalUpdate(async () => {
|
||||
startGlobalUpdate(async () => {
|
||||
const remoteProfiles =
|
||||
profiles?.filter((item) => item.type === 'remote') || []
|
||||
(profiles.clash?.filter(
|
||||
(item) => item.type === 'remote',
|
||||
) as RemoteProfile[]) || []
|
||||
|
||||
const updates: Array<Promise<void>> = []
|
||||
|
||||
for (const profile of remoteProfiles) {
|
||||
const options: Profile.Option = profile.option || {
|
||||
const option = {
|
||||
with_proxy: false,
|
||||
self_proxy: false,
|
||||
}
|
||||
update_interval: 0,
|
||||
user_agent: profile.option?.user_agent ?? null,
|
||||
...profile.option,
|
||||
} satisfies RemoteProfileOptionsBuilder
|
||||
|
||||
updates.push(updateProfile(profile.uid, options))
|
||||
const result = await update.mutateAsync({
|
||||
uid: profile.uid,
|
||||
profile: {
|
||||
...profile,
|
||||
option,
|
||||
},
|
||||
})
|
||||
updates.push(Promise.resolve(result || undefined))
|
||||
}
|
||||
try {
|
||||
await Promise.all(updates)
|
||||
@@ -201,7 +230,7 @@ function ProfilePage() {
|
||||
|
||||
{profiles && (
|
||||
<Grid container spacing={2}>
|
||||
{profiles.map((item) => (
|
||||
{profiles.clash?.map((item) => (
|
||||
<Grid
|
||||
key={item.uid}
|
||||
size={{
|
||||
@@ -221,7 +250,7 @@ function ProfilePage() {
|
||||
<ProfileItem
|
||||
item={item}
|
||||
onClickChains={onClickChains}
|
||||
selected={getProfiles.data?.current.includes(item.uid)}
|
||||
selected={query.data?.current?.includes(item.uid)}
|
||||
maxLogLevelTriggered={maxLogLevelTriggered}
|
||||
chainsSelected={chainsSelected === item.uid}
|
||||
/>
|
||||
|
||||
@@ -22,7 +22,12 @@ import {
|
||||
TextField,
|
||||
useTheme,
|
||||
} from '@mui/material'
|
||||
import { Clash, useClashCore, useNyanpasu } from '@nyanpasu/interface'
|
||||
import {
|
||||
Clash,
|
||||
ProxyGroupItem,
|
||||
useClashCore,
|
||||
useNyanpasu,
|
||||
} from '@nyanpasu/interface'
|
||||
import { cn, SidePage } from '@nyanpasu/ui'
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
@@ -71,14 +76,13 @@ function ProxyPage() {
|
||||
|
||||
const [proxyGroup] = useAtom(proxyGroupAtom)
|
||||
|
||||
const [group, setGroup] =
|
||||
useState<Clash.Proxy<Clash.Proxy<string> | string>>()
|
||||
const [group, setGroup] = useState<ProxyGroupItem>()
|
||||
|
||||
useEffect(() => {
|
||||
if (getCurrentMode.global) {
|
||||
setGroup(data?.global)
|
||||
} else if (getCurrentMode.direct) {
|
||||
setGroup(data?.direct)
|
||||
setGroup(data?.direct ? { ...data.direct, all: [] } : undefined)
|
||||
} else {
|
||||
if (proxyGroup.selector !== null) {
|
||||
setGroup(data?.groups[proxyGroup.selector])
|
||||
@@ -178,7 +182,7 @@ function ProxyPage() {
|
||||
<DelayButton onClick={handleDelayClick} />
|
||||
</>
|
||||
) : (
|
||||
<ContentDisplay className="absolute" message={t('No Proxy')} />
|
||||
<ContentDisplay className="absolute" message={t('No Proxies')} />
|
||||
)
|
||||
) : (
|
||||
<ContentDisplay className="absolute" message={t('Direct Mode')} />
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"@vitejs/plugin-react": "4.3.4",
|
||||
"ahooks": "3.8.4",
|
||||
"d3": "7.9.0",
|
||||
"framer-motion": "12.0.6",
|
||||
"framer-motion": "12.0.11",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-error-boundary": "5.0.0",
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"ps": "Copy Env (PS)",
|
||||
"sh": "Copy Env (sh)"
|
||||
},
|
||||
"no_proxies": "No Proxies",
|
||||
"select_proxies": "Select Proxies",
|
||||
"dashboard": "Dashboard",
|
||||
"direct_mode": "Direct Mode",
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"ps": "Копировать Env (PS)",
|
||||
"sh": "Копировать Env (sh)"
|
||||
},
|
||||
"no_proxies": "Без прокси",
|
||||
"select_proxies": "Выбрать прокси",
|
||||
"dashboard": "Панель управления",
|
||||
"direct_mode": "Прямой режим",
|
||||
|
||||
@@ -2,17 +2,18 @@
|
||||
"_version": 1,
|
||||
"tray": {
|
||||
"copy_env": {
|
||||
"cmd": "复制环境变量(CMD)",
|
||||
"ps": "复制环境变量(PS)",
|
||||
"sh": "复制环境变量(sh)"
|
||||
"cmd": "复制环境变量 (CMD)",
|
||||
"ps": "复制环境变量 (PS)",
|
||||
"sh": "复制环境变量 (SH)"
|
||||
},
|
||||
"no_proxies": "无代理",
|
||||
"select_proxies": "选择代理",
|
||||
"dashboard": "打开面板",
|
||||
"direct_mode": "直连模式",
|
||||
"global_mode": "全局模式",
|
||||
"more": {
|
||||
"menu": "更多",
|
||||
"restart_app": "重启应用",
|
||||
"restart_app": "重启应用程序",
|
||||
"restart_clash": "重启 Clash"
|
||||
},
|
||||
"open_dir": {
|
||||
@@ -33,11 +34,11 @@
|
||||
"tun_mode": "TUN 模式"
|
||||
},
|
||||
"dialog": {
|
||||
"panic": "请将此问题汇报到 GitHub 问题追踪器",
|
||||
"migrate": "检测到旧版本配置文件\n是否迁移到新版本?\n警告:此操作会覆盖掉现有配置文件",
|
||||
"panic": "请将此问题汇报到 GitHub Issues。",
|
||||
"migrate": "检测到旧版本配置文件,是否迁移到新版本?\n警告:此操作会覆盖掉现有配置文件!",
|
||||
"custom_app_dir_migrate": "你将要更改应用目录至 %{path}。\n需要将现有数据迁移到新目录吗?",
|
||||
"warning": {
|
||||
"enable_tun_with_no_permission": "TUN 模式需要管理员权限,或服务模式,当前都未开启,因此 TUN 模式将无法正常工作"
|
||||
"enable_tun_with_no_permission": "TUN 模式需要授予管理员权限或启用服务模式,当前都未开启,因此 TUN 模式将无法正常工作。"
|
||||
},
|
||||
"info": {
|
||||
"grant_core_permission": "Clash 内核需要管理员权限才能使得 TUN 模式正常工作,是否授予?\n\n请注意:此操作需要输入密码。"
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"ps": "複製環境變數 (PS)",
|
||||
"sh": "複製環境變數 (SH)"
|
||||
},
|
||||
"select_proxies": "選擇代理",
|
||||
"no_proxies": "無代理",
|
||||
"select_proxies": "選取代理",
|
||||
"dashboard": "開啟儀表盤",
|
||||
"direct_mode": "直連模式",
|
||||
"global_mode": "全域模式",
|
||||
@@ -35,9 +36,9 @@
|
||||
"dialog": {
|
||||
"panic": "請將此問題回報至 GitHub Issues。",
|
||||
"migrate": "檢測到舊版本設定檔,是否遷移到新版本?\n警告:此操作會覆蓋掉現有設定檔。",
|
||||
"custom_app_dir_migrate": "你將要更改應用目錄至 %{path}。\n需要將現有資料遷移到新目錄嗎?",
|
||||
"custom_app_dir_migrate": "你將要更改 App 目錄至 %{path}。\n需要將現有資料遷移到新目錄嗎?",
|
||||
"warning": {
|
||||
"enable_tun_with_no_permission": "開啟 TUN 模式需要系統管理員權限或服務模式,目前都未開啟,因此 TUN 模式將無法正常工作。"
|
||||
"enable_tun_with_no_permission": "開啟 TUN 模式需要系統管理員權限或服務模式,目前都未啟用,因此 TUN 模式將無法正常工作。"
|
||||
},
|
||||
"info": {
|
||||
"grant_core_permission": "Clash 核心需要系統管理員權限才能使 TUN 模式正常工作,是否授予?\n請注意:此操作需要輸入密碼。"
|
||||
|
||||
@@ -57,8 +57,8 @@
|
||||
"lodash-es": "4.17.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "19.6.1",
|
||||
"@commitlint/config-conventional": "19.6.0",
|
||||
"@commitlint/cli": "19.7.1",
|
||||
"@commitlint/config-conventional": "19.7.1",
|
||||
"@eslint/compat": "1.2.6",
|
||||
"@eslint/eslintrc": "3.2.0",
|
||||
"@ianvs/prettier-plugin-sort-imports": "4.4.1",
|
||||
|
||||
Generated
+112
-106
@@ -19,11 +19,11 @@ importers:
|
||||
version: 4.17.21
|
||||
devDependencies:
|
||||
'@commitlint/cli':
|
||||
specifier: 19.6.1
|
||||
version: 19.6.1(@types/node@22.13.0)(typescript@5.7.3)
|
||||
specifier: 19.7.1
|
||||
version: 19.7.1(@types/node@22.13.0)(typescript@5.7.3)
|
||||
'@commitlint/config-conventional':
|
||||
specifier: 19.6.0
|
||||
version: 19.6.0
|
||||
specifier: 19.7.1
|
||||
version: 19.7.1
|
||||
'@eslint/compat':
|
||||
specifier: 1.2.6
|
||||
version: 1.2.6(eslint@9.19.0(jiti@2.4.2))
|
||||
@@ -183,6 +183,9 @@ importers:
|
||||
ahooks:
|
||||
specifier: 3.8.4
|
||||
version: 3.8.4(react@19.0.0)
|
||||
lodash-es:
|
||||
specifier: 4.17.21
|
||||
version: 4.17.21
|
||||
ofetch:
|
||||
specifier: 1.4.1
|
||||
version: 1.4.1
|
||||
@@ -193,6 +196,9 @@ importers:
|
||||
specifier: 2.3.0
|
||||
version: 2.3.0(react@19.0.0)
|
||||
devDependencies:
|
||||
'@types/lodash-es':
|
||||
specifier: 4.17.12
|
||||
version: 4.17.12
|
||||
'@types/react':
|
||||
specifier: 19.0.8
|
||||
version: 19.0.8
|
||||
@@ -254,8 +260,8 @@ importers:
|
||||
specifier: 1.11.13
|
||||
version: 1.11.13
|
||||
framer-motion:
|
||||
specifier: 12.0.6
|
||||
version: 12.0.6(@emotion/is-prop-valid@1.3.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
specifier: 12.0.11
|
||||
version: 12.0.11(@emotion/is-prop-valid@1.3.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
i18next:
|
||||
specifier: 24.2.2
|
||||
version: 24.2.2(typescript@5.7.3)
|
||||
@@ -339,8 +345,8 @@ importers:
|
||||
specifier: 1.99.0
|
||||
version: 1.99.0(@tanstack/react-router@1.99.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(csstype@3.1.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
'@tanstack/router-plugin':
|
||||
specifier: 1.99.0
|
||||
version: 1.99.0(@tanstack/react-router@1.99.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(vite@6.0.11(@types/node@22.13.0)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.1)(sass-embedded@1.83.4)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.7.0))
|
||||
specifier: 1.99.3
|
||||
version: 1.99.3(@tanstack/react-router@1.99.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(vite@6.0.11(@types/node@22.13.0)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.1)(sass-embedded@1.83.4)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.7.0))
|
||||
'@tauri-apps/plugin-clipboard-manager':
|
||||
specifier: 2.2.1
|
||||
version: 2.2.1
|
||||
@@ -480,8 +486,8 @@ importers:
|
||||
specifier: 7.9.0
|
||||
version: 7.9.0
|
||||
framer-motion:
|
||||
specifier: 12.0.6
|
||||
version: 12.0.6(@emotion/is-prop-valid@1.3.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
specifier: 12.0.11
|
||||
version: 12.0.11(@emotion/is-prop-valid@1.3.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
react:
|
||||
specifier: 19.0.0
|
||||
version: 19.0.0
|
||||
@@ -1211,13 +1217,13 @@ packages:
|
||||
'@bufbuild/protobuf@2.2.3':
|
||||
resolution: {integrity: sha512-tFQoXHJdkEOSwj5tRIZSPNUuXK3RaR7T1nUrPgbYX1pUbvqqaaZAsfo+NXBPsz5rZMSKVFrgK1WL8Q/MSLvprg==}
|
||||
|
||||
'@commitlint/cli@19.6.1':
|
||||
resolution: {integrity: sha512-8hcyA6ZoHwWXC76BoC8qVOSr8xHy00LZhZpauiD0iO0VYbVhMnED0da85lTfIULxl7Lj4c6vZgF0Wu/ed1+jlQ==}
|
||||
'@commitlint/cli@19.7.1':
|
||||
resolution: {integrity: sha512-iObGjR1tE/PfDtDTEfd+tnRkB3/HJzpQqRTyofS2MPPkDn1mp3DBC8SoPDayokfAy+xKhF8+bwRCJO25Nea0YQ==}
|
||||
engines: {node: '>=v18'}
|
||||
hasBin: true
|
||||
|
||||
'@commitlint/config-conventional@19.6.0':
|
||||
resolution: {integrity: sha512-DJT40iMnTYtBtUfw9ApbsLZFke1zKh6llITVJ+x9mtpHD08gsNXaIRqHTmwTZL3dNX5+WoyK7pCN/5zswvkBCQ==}
|
||||
'@commitlint/config-conventional@19.7.1':
|
||||
resolution: {integrity: sha512-fsEIF8zgiI/FIWSnykdQNj/0JE4av08MudLTyYHm4FlLWemKoQvPNUYU2M/3tktWcCEyq7aOkDDgtjrmgWFbvg==}
|
||||
engines: {node: '>=v18'}
|
||||
|
||||
'@commitlint/config-validator@19.5.0':
|
||||
@@ -1236,12 +1242,12 @@ packages:
|
||||
resolution: {integrity: sha512-yNy088miE52stCI3dhG/vvxFo9e4jFkU1Mj3xECfzp/bIS/JUay4491huAlVcffOoMK1cd296q0W92NlER6r3A==}
|
||||
engines: {node: '>=v18'}
|
||||
|
||||
'@commitlint/is-ignored@19.6.0':
|
||||
resolution: {integrity: sha512-Ov6iBgxJQFR9koOupDPHvcHU9keFupDgtB3lObdEZDroiG4jj1rzky60fbQozFKVYRTUdrBGICHG0YVmRuAJmw==}
|
||||
'@commitlint/is-ignored@19.7.1':
|
||||
resolution: {integrity: sha512-3IaOc6HVg2hAoGleRK3r9vL9zZ3XY0rf1RsUf6jdQLuaD46ZHnXBiOPTyQ004C4IvYjSWqJwlh0/u2P73aIE3g==}
|
||||
engines: {node: '>=v18'}
|
||||
|
||||
'@commitlint/lint@19.6.0':
|
||||
resolution: {integrity: sha512-LRo7zDkXtcIrpco9RnfhOKeg8PAnE3oDDoalnrVU/EVaKHYBWYL1DlRR7+3AWn0JiBqD8yKOfetVxJGdEtZ0tg==}
|
||||
'@commitlint/lint@19.7.1':
|
||||
resolution: {integrity: sha512-LhcPfVjcOcOZA7LEuBBeO00o3MeZa+tWrX9Xyl1r9PMd5FWsEoZI9IgnGqTKZ0lZt5pO3ZlstgnRyY1CJJc9Xg==}
|
||||
engines: {node: '>=v18'}
|
||||
|
||||
'@commitlint/load@19.6.1':
|
||||
@@ -2905,8 +2911,8 @@ packages:
|
||||
'@tanstack/react-router':
|
||||
optional: true
|
||||
|
||||
'@tanstack/router-plugin@1.99.0':
|
||||
resolution: {integrity: sha512-Ue96luAqdwL4QtT4CqNQvegScqECoztt6MfyRsz/agG9JtU/7Mpd6h/vKmdXZpg6MR6iC2R1co164NjzAMod7A==}
|
||||
'@tanstack/router-plugin@1.99.3':
|
||||
resolution: {integrity: sha512-bE4S8MBXRje5VaslZhv+xaj/0rOpE2QaybwJ53ms5t6JrTkQ42UPKF3paHQuZekzb8ZHbduIk7BubfNLiDBxUw==}
|
||||
engines: {node: '>=12'}
|
||||
peerDependencies:
|
||||
'@rsbuild/core': '>=1.0.2'
|
||||
@@ -2920,8 +2926,8 @@ packages:
|
||||
webpack:
|
||||
optional: true
|
||||
|
||||
'@tanstack/router-utils@1.99.0':
|
||||
resolution: {integrity: sha512-TAWImltqT8fS83E4L5tLNVI4Q1ZlefWYoIFh+ATo/+tLlJDa1E0d7p1/VjlPo+S1hlEoQPCe/ppZLcU6SG+8rg==}
|
||||
'@tanstack/router-utils@1.99.3':
|
||||
resolution: {integrity: sha512-aVyDLjuUJ4Uf8Qw+ihuU3kG+gd2f/P78Z81AQAryuF8Qm8bcSJukguCYVI4mL9zAkGAifZ9rVk0lR5BKcnI4qA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
'@tanstack/router-zod-adapter@1.81.5':
|
||||
@@ -4900,8 +4906,8 @@ packages:
|
||||
fraction.js@4.3.7:
|
||||
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
|
||||
|
||||
framer-motion@12.0.6:
|
||||
resolution: {integrity: sha512-LmrXbXF6Vv5WCNmb+O/zn891VPZrH7XbsZgRLBROw6kFiP+iTK49gxTv2Ur3F0Tbw6+sy9BVtSqnWfMUpH+6nA==}
|
||||
framer-motion@12.0.11:
|
||||
resolution: {integrity: sha512-1F+YNXr3bSHxt5sCzeCVL56sc4MngbOhdU5ptv02vaepdFYcQd0fZtuAHvFJgMbn5V7SOsaX/3hVqr21ZaCKhA==}
|
||||
peerDependencies:
|
||||
'@emotion/is-prop-valid': '*'
|
||||
react: ^18.0.0 || ^19.0.0
|
||||
@@ -8415,8 +8421,8 @@ snapshots:
|
||||
'@babel/helpers': 7.26.0
|
||||
'@babel/parser': 7.26.3
|
||||
'@babel/template': 7.25.9
|
||||
'@babel/traverse': 7.26.4
|
||||
'@babel/types': 7.26.3
|
||||
'@babel/traverse': 7.26.7
|
||||
'@babel/types': 7.26.7
|
||||
convert-source-map: 2.0.0
|
||||
debug: 4.4.0
|
||||
gensync: 1.0.0-beta.2
|
||||
@@ -8455,7 +8461,7 @@ snapshots:
|
||||
'@babel/generator@7.26.3':
|
||||
dependencies:
|
||||
'@babel/parser': 7.26.3
|
||||
'@babel/types': 7.26.3
|
||||
'@babel/types': 7.26.7
|
||||
'@jridgewell/gen-mapping': 0.3.5
|
||||
'@jridgewell/trace-mapping': 0.3.25
|
||||
jsesc: 3.0.2
|
||||
@@ -8466,16 +8472,16 @@ snapshots:
|
||||
'@babel/types': 7.26.7
|
||||
'@jridgewell/gen-mapping': 0.3.5
|
||||
'@jridgewell/trace-mapping': 0.3.25
|
||||
jsesc: 3.0.2
|
||||
jsesc: 3.1.0
|
||||
|
||||
'@babel/helper-annotate-as-pure@7.25.9':
|
||||
dependencies:
|
||||
'@babel/types': 7.26.3
|
||||
'@babel/types': 7.26.7
|
||||
|
||||
'@babel/helper-builder-binary-assignment-operator-visitor@7.25.9':
|
||||
dependencies:
|
||||
'@babel/traverse': 7.26.4
|
||||
'@babel/types': 7.26.3
|
||||
'@babel/traverse': 7.26.7
|
||||
'@babel/types': 7.26.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -8503,7 +8509,7 @@ snapshots:
|
||||
'@babel/helper-optimise-call-expression': 7.25.9
|
||||
'@babel/helper-replace-supers': 7.25.9(@babel/core@7.26.0)
|
||||
'@babel/helper-skip-transparent-expression-wrappers': 7.25.9
|
||||
'@babel/traverse': 7.26.4
|
||||
'@babel/traverse': 7.26.7
|
||||
semver: 6.3.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -8518,7 +8524,7 @@ snapshots:
|
||||
'@babel/helper-define-polyfill-provider@0.6.2(@babel/core@7.26.0)':
|
||||
dependencies:
|
||||
'@babel/core': 7.26.0
|
||||
'@babel/helper-compilation-targets': 7.25.9
|
||||
'@babel/helper-compilation-targets': 7.26.5
|
||||
'@babel/helper-plugin-utils': 7.25.9
|
||||
debug: 4.4.0
|
||||
lodash.debounce: 4.0.8
|
||||
@@ -8544,8 +8550,8 @@ snapshots:
|
||||
|
||||
'@babel/helper-member-expression-to-functions@7.25.9':
|
||||
dependencies:
|
||||
'@babel/traverse': 7.26.4
|
||||
'@babel/types': 7.26.3
|
||||
'@babel/traverse': 7.26.7
|
||||
'@babel/types': 7.26.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -8561,7 +8567,7 @@ snapshots:
|
||||
'@babel/core': 7.26.0
|
||||
'@babel/helper-module-imports': 7.25.9
|
||||
'@babel/helper-validator-identifier': 7.25.9
|
||||
'@babel/traverse': 7.26.4
|
||||
'@babel/traverse': 7.26.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -8570,13 +8576,13 @@ snapshots:
|
||||
'@babel/core': 7.26.7
|
||||
'@babel/helper-module-imports': 7.25.9
|
||||
'@babel/helper-validator-identifier': 7.25.9
|
||||
'@babel/traverse': 7.26.4
|
||||
'@babel/traverse': 7.26.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@babel/helper-optimise-call-expression@7.25.9':
|
||||
dependencies:
|
||||
'@babel/types': 7.26.3
|
||||
'@babel/types': 7.26.7
|
||||
|
||||
'@babel/helper-plugin-utils@7.25.9': {}
|
||||
|
||||
@@ -8585,7 +8591,7 @@ snapshots:
|
||||
'@babel/core': 7.26.0
|
||||
'@babel/helper-annotate-as-pure': 7.25.9
|
||||
'@babel/helper-wrap-function': 7.25.9
|
||||
'@babel/traverse': 7.26.4
|
||||
'@babel/traverse': 7.26.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -8594,21 +8600,21 @@ snapshots:
|
||||
'@babel/core': 7.26.0
|
||||
'@babel/helper-member-expression-to-functions': 7.25.9
|
||||
'@babel/helper-optimise-call-expression': 7.25.9
|
||||
'@babel/traverse': 7.26.4
|
||||
'@babel/traverse': 7.26.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@babel/helper-simple-access@7.25.9':
|
||||
dependencies:
|
||||
'@babel/traverse': 7.26.4
|
||||
'@babel/types': 7.26.3
|
||||
'@babel/traverse': 7.26.7
|
||||
'@babel/types': 7.26.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@babel/helper-skip-transparent-expression-wrappers@7.25.9':
|
||||
dependencies:
|
||||
'@babel/traverse': 7.26.4
|
||||
'@babel/types': 7.26.3
|
||||
'@babel/traverse': 7.26.7
|
||||
'@babel/types': 7.26.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -8626,15 +8632,15 @@ snapshots:
|
||||
'@babel/helper-wrap-function@7.25.9':
|
||||
dependencies:
|
||||
'@babel/template': 7.25.9
|
||||
'@babel/traverse': 7.26.4
|
||||
'@babel/types': 7.26.3
|
||||
'@babel/traverse': 7.26.7
|
||||
'@babel/types': 7.26.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@babel/helpers@7.26.0':
|
||||
dependencies:
|
||||
'@babel/template': 7.25.9
|
||||
'@babel/types': 7.26.3
|
||||
'@babel/types': 7.26.7
|
||||
|
||||
'@babel/helpers@7.26.7':
|
||||
dependencies:
|
||||
@@ -8643,7 +8649,7 @@ snapshots:
|
||||
|
||||
'@babel/parser@7.26.3':
|
||||
dependencies:
|
||||
'@babel/types': 7.26.3
|
||||
'@babel/types': 7.26.7
|
||||
|
||||
'@babel/parser@7.26.7':
|
||||
dependencies:
|
||||
@@ -8653,7 +8659,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@babel/core': 7.26.0
|
||||
'@babel/helper-plugin-utils': 7.25.9
|
||||
'@babel/traverse': 7.26.4
|
||||
'@babel/traverse': 7.26.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -8680,7 +8686,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@babel/core': 7.26.0
|
||||
'@babel/helper-plugin-utils': 7.25.9
|
||||
'@babel/traverse': 7.26.4
|
||||
'@babel/traverse': 7.26.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -8732,7 +8738,7 @@ snapshots:
|
||||
'@babel/core': 7.26.0
|
||||
'@babel/helper-plugin-utils': 7.25.9
|
||||
'@babel/helper-remap-async-to-generator': 7.25.9(@babel/core@7.26.0)
|
||||
'@babel/traverse': 7.26.4
|
||||
'@babel/traverse': 7.26.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -8775,10 +8781,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@babel/core': 7.26.0
|
||||
'@babel/helper-annotate-as-pure': 7.25.9
|
||||
'@babel/helper-compilation-targets': 7.25.9
|
||||
'@babel/helper-compilation-targets': 7.26.5
|
||||
'@babel/helper-plugin-utils': 7.25.9
|
||||
'@babel/helper-replace-supers': 7.25.9(@babel/core@7.26.0)
|
||||
'@babel/traverse': 7.26.4
|
||||
'@babel/traverse': 7.26.7
|
||||
globals: 11.12.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -8840,9 +8846,9 @@ snapshots:
|
||||
'@babel/plugin-transform-function-name@7.25.9(@babel/core@7.26.0)':
|
||||
dependencies:
|
||||
'@babel/core': 7.26.0
|
||||
'@babel/helper-compilation-targets': 7.25.9
|
||||
'@babel/helper-compilation-targets': 7.26.5
|
||||
'@babel/helper-plugin-utils': 7.25.9
|
||||
'@babel/traverse': 7.26.4
|
||||
'@babel/traverse': 7.26.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -8889,7 +8895,7 @@ snapshots:
|
||||
'@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0)
|
||||
'@babel/helper-plugin-utils': 7.25.9
|
||||
'@babel/helper-validator-identifier': 7.25.9
|
||||
'@babel/traverse': 7.26.4
|
||||
'@babel/traverse': 7.26.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -8925,7 +8931,7 @@ snapshots:
|
||||
'@babel/plugin-transform-object-rest-spread@7.25.9(@babel/core@7.26.0)':
|
||||
dependencies:
|
||||
'@babel/core': 7.26.0
|
||||
'@babel/helper-compilation-targets': 7.25.9
|
||||
'@babel/helper-compilation-targets': 7.26.5
|
||||
'@babel/helper-plugin-utils': 7.25.9
|
||||
'@babel/plugin-transform-parameters': 7.25.9(@babel/core@7.26.0)
|
||||
|
||||
@@ -9134,7 +9140,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@babel/core': 7.26.0
|
||||
'@babel/helper-plugin-utils': 7.25.9
|
||||
'@babel/types': 7.26.3
|
||||
'@babel/types': 7.26.7
|
||||
esutils: 2.0.3
|
||||
|
||||
'@babel/runtime@7.26.0':
|
||||
@@ -9144,7 +9150,7 @@ snapshots:
|
||||
'@babel/template@7.25.9':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.26.2
|
||||
'@babel/parser': 7.26.3
|
||||
'@babel/parser': 7.26.7
|
||||
'@babel/types': 7.26.7
|
||||
|
||||
'@babel/traverse@7.23.2':
|
||||
@@ -9169,7 +9175,7 @@ snapshots:
|
||||
'@babel/generator': 7.26.3
|
||||
'@babel/parser': 7.26.3
|
||||
'@babel/template': 7.25.9
|
||||
'@babel/types': 7.26.3
|
||||
'@babel/types': 7.26.7
|
||||
debug: 4.4.0
|
||||
globals: 11.12.0
|
||||
transitivePeerDependencies:
|
||||
@@ -9181,7 +9187,7 @@ snapshots:
|
||||
'@babel/generator': 7.26.3
|
||||
'@babel/parser': 7.26.3
|
||||
'@babel/template': 7.25.9
|
||||
'@babel/types': 7.26.3
|
||||
'@babel/types': 7.26.7
|
||||
debug: 4.4.0
|
||||
globals: 11.12.0
|
||||
transitivePeerDependencies:
|
||||
@@ -9222,10 +9228,10 @@ snapshots:
|
||||
|
||||
'@bufbuild/protobuf@2.2.3': {}
|
||||
|
||||
'@commitlint/cli@19.6.1(@types/node@22.13.0)(typescript@5.7.3)':
|
||||
'@commitlint/cli@19.7.1(@types/node@22.13.0)(typescript@5.7.3)':
|
||||
dependencies:
|
||||
'@commitlint/format': 19.5.0
|
||||
'@commitlint/lint': 19.6.0
|
||||
'@commitlint/lint': 19.7.1
|
||||
'@commitlint/load': 19.6.1(@types/node@22.13.0)(typescript@5.7.3)
|
||||
'@commitlint/read': 19.5.0
|
||||
'@commitlint/types': 19.5.0
|
||||
@@ -9235,7 +9241,7 @@ snapshots:
|
||||
- '@types/node'
|
||||
- typescript
|
||||
|
||||
'@commitlint/config-conventional@19.6.0':
|
||||
'@commitlint/config-conventional@19.7.1':
|
||||
dependencies:
|
||||
'@commitlint/types': 19.5.0
|
||||
conventional-changelog-conventionalcommits: 7.0.2
|
||||
@@ -9261,14 +9267,14 @@ snapshots:
|
||||
'@commitlint/types': 19.5.0
|
||||
chalk: 5.4.1
|
||||
|
||||
'@commitlint/is-ignored@19.6.0':
|
||||
'@commitlint/is-ignored@19.7.1':
|
||||
dependencies:
|
||||
'@commitlint/types': 19.5.0
|
||||
semver: 7.6.3
|
||||
|
||||
'@commitlint/lint@19.6.0':
|
||||
'@commitlint/lint@19.7.1':
|
||||
dependencies:
|
||||
'@commitlint/is-ignored': 19.6.0
|
||||
'@commitlint/is-ignored': 19.7.1
|
||||
'@commitlint/parse': 19.5.0
|
||||
'@commitlint/rules': 19.6.0
|
||||
'@commitlint/types': 19.5.0
|
||||
@@ -10556,54 +10562,54 @@ snapshots:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
'@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.26.0)':
|
||||
'@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.26.7)':
|
||||
dependencies:
|
||||
'@babel/core': 7.26.0
|
||||
'@babel/core': 7.26.7
|
||||
|
||||
'@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.26.0)':
|
||||
'@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.26.7)':
|
||||
dependencies:
|
||||
'@babel/core': 7.26.0
|
||||
'@babel/core': 7.26.7
|
||||
|
||||
'@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.26.0)':
|
||||
'@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.26.7)':
|
||||
dependencies:
|
||||
'@babel/core': 7.26.0
|
||||
'@babel/core': 7.26.7
|
||||
|
||||
'@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.26.0)':
|
||||
'@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.26.7)':
|
||||
dependencies:
|
||||
'@babel/core': 7.26.0
|
||||
'@babel/core': 7.26.7
|
||||
|
||||
'@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.26.0)':
|
||||
'@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.26.7)':
|
||||
dependencies:
|
||||
'@babel/core': 7.26.0
|
||||
'@babel/core': 7.26.7
|
||||
|
||||
'@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.26.0)':
|
||||
'@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.26.7)':
|
||||
dependencies:
|
||||
'@babel/core': 7.26.0
|
||||
'@babel/core': 7.26.7
|
||||
|
||||
'@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.26.0)':
|
||||
'@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.26.7)':
|
||||
dependencies:
|
||||
'@babel/core': 7.26.0
|
||||
'@babel/core': 7.26.7
|
||||
|
||||
'@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.26.0)':
|
||||
'@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.26.7)':
|
||||
dependencies:
|
||||
'@babel/core': 7.26.0
|
||||
'@babel/core': 7.26.7
|
||||
|
||||
'@svgr/babel-preset@8.1.0(@babel/core@7.26.0)':
|
||||
'@svgr/babel-preset@8.1.0(@babel/core@7.26.7)':
|
||||
dependencies:
|
||||
'@babel/core': 7.26.0
|
||||
'@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.26.0)
|
||||
'@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.26.0)
|
||||
'@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.26.0)
|
||||
'@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.26.0)
|
||||
'@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.26.0)
|
||||
'@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.26.0)
|
||||
'@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.26.0)
|
||||
'@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.26.0)
|
||||
'@babel/core': 7.26.7
|
||||
'@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.26.7)
|
||||
'@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.26.7)
|
||||
'@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.26.7)
|
||||
'@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.26.7)
|
||||
'@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.26.7)
|
||||
'@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.26.7)
|
||||
'@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.26.7)
|
||||
'@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.26.7)
|
||||
|
||||
'@svgr/core@8.1.0(typescript@5.7.3)':
|
||||
dependencies:
|
||||
'@babel/core': 7.26.0
|
||||
'@svgr/babel-preset': 8.1.0(@babel/core@7.26.0)
|
||||
'@babel/core': 7.26.7
|
||||
'@svgr/babel-preset': 8.1.0(@babel/core@7.26.7)
|
||||
camelcase: 6.3.0
|
||||
cosmiconfig: 8.3.6(typescript@5.7.3)
|
||||
snake-case: 3.0.4
|
||||
@@ -10613,13 +10619,13 @@ snapshots:
|
||||
|
||||
'@svgr/hast-util-to-babel-ast@8.0.0':
|
||||
dependencies:
|
||||
'@babel/types': 7.26.3
|
||||
'@babel/types': 7.26.7
|
||||
entities: 4.5.0
|
||||
|
||||
'@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(typescript@5.7.3))':
|
||||
dependencies:
|
||||
'@babel/core': 7.26.0
|
||||
'@svgr/babel-preset': 8.1.0(@babel/core@7.26.0)
|
||||
'@babel/core': 7.26.7
|
||||
'@svgr/babel-preset': 8.1.0(@babel/core@7.26.7)
|
||||
'@svgr/core': 8.1.0(typescript@5.7.3)
|
||||
'@svgr/hast-util-to-babel-ast': 8.0.0
|
||||
svg-parser: 2.0.4
|
||||
@@ -10810,7 +10816,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@tanstack/react-router': 1.99.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
|
||||
'@tanstack/router-plugin@1.99.0(@tanstack/react-router@1.99.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(vite@6.0.11(@types/node@22.13.0)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.1)(sass-embedded@1.83.4)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.7.0))':
|
||||
'@tanstack/router-plugin@1.99.3(@tanstack/react-router@1.99.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(vite@6.0.11(@types/node@22.13.0)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.1)(sass-embedded@1.83.4)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.7.0))':
|
||||
dependencies:
|
||||
'@babel/core': 7.26.7
|
||||
'@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.7)
|
||||
@@ -10819,7 +10825,7 @@ snapshots:
|
||||
'@babel/traverse': 7.26.7
|
||||
'@babel/types': 7.26.7
|
||||
'@tanstack/router-generator': 1.99.0(@tanstack/react-router@1.99.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0))
|
||||
'@tanstack/router-utils': 1.99.0
|
||||
'@tanstack/router-utils': 1.99.3
|
||||
'@tanstack/virtual-file-routes': 1.99.0
|
||||
'@types/babel__core': 7.20.5
|
||||
'@types/babel__template': 7.4.4
|
||||
@@ -10834,7 +10840,7 @@ snapshots:
|
||||
- '@tanstack/react-router'
|
||||
- supports-color
|
||||
|
||||
'@tanstack/router-utils@1.99.0':
|
||||
'@tanstack/router-utils@1.99.3':
|
||||
dependencies:
|
||||
'@babel/generator': 7.26.5
|
||||
'@babel/parser': 7.26.7
|
||||
@@ -10960,7 +10966,7 @@ snapshots:
|
||||
|
||||
'@types/babel__core@7.20.5':
|
||||
dependencies:
|
||||
'@babel/parser': 7.26.3
|
||||
'@babel/parser': 7.26.7
|
||||
'@babel/types': 7.26.7
|
||||
'@types/babel__generator': 7.6.8
|
||||
'@types/babel__template': 7.4.4
|
||||
@@ -10972,7 +10978,7 @@ snapshots:
|
||||
|
||||
'@types/babel__template@7.4.4':
|
||||
dependencies:
|
||||
'@babel/parser': 7.26.3
|
||||
'@babel/parser': 7.26.7
|
||||
'@babel/types': 7.26.7
|
||||
|
||||
'@types/babel__traverse@7.20.6':
|
||||
@@ -11374,7 +11380,7 @@ snapshots:
|
||||
|
||||
'@vue/compiler-core@3.5.13':
|
||||
dependencies:
|
||||
'@babel/parser': 7.26.3
|
||||
'@babel/parser': 7.26.7
|
||||
'@vue/shared': 3.5.13
|
||||
entities: 4.5.0
|
||||
estree-walker: 2.0.2
|
||||
@@ -11666,7 +11672,7 @@ snapshots:
|
||||
babel-dead-code-elimination@1.0.8:
|
||||
dependencies:
|
||||
'@babel/core': 7.26.7
|
||||
'@babel/parser': 7.26.3
|
||||
'@babel/parser': 7.26.7
|
||||
'@babel/traverse': 7.26.7
|
||||
'@babel/types': 7.26.7
|
||||
transitivePeerDependencies:
|
||||
@@ -13230,7 +13236,7 @@ snapshots:
|
||||
|
||||
fraction.js@4.3.7: {}
|
||||
|
||||
framer-motion@12.0.6(@emotion/is-prop-valid@1.3.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
||||
framer-motion@12.0.11(@emotion/is-prop-valid@1.3.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
||||
dependencies:
|
||||
motion-dom: 12.0.0
|
||||
motion-utils: 12.0.0
|
||||
|
||||
+332
@@ -0,0 +1,332 @@
|
||||
From 78997e9a5e4d8a4df561e083a92c91ae23010e07 Mon Sep 17 00:00:00 2001
|
||||
From: Daniel Golle <daniel@makrotopia.org>
|
||||
Date: Tue, 1 Oct 2024 01:17:18 +0100
|
||||
Subject: [PATCH] net: phy: mxl-gpy: add basic LED support
|
||||
|
||||
Add basic support for LEDs connected to MaxLinear GPY2xx and GPY115 PHYs.
|
||||
The PHYs allow up to 4 LEDs to be connected.
|
||||
Implement controlling LEDs in software as well as netdev trigger offloading
|
||||
and LED polarity setup.
|
||||
|
||||
The hardware claims to support 16 PWM brightness levels but there is no
|
||||
documentation on how to use that feature, hence this is not supported.
|
||||
|
||||
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
|
||||
Reviewed-by: Andrew Lunn <andrew@lunn.ch>
|
||||
Link: https://patch.msgid.link/b6ec9050339f8244ff898898a1cecc33b13a48fc.1727741563.git.daniel@makrotopia.org
|
||||
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
|
||||
---
|
||||
drivers/net/phy/mxl-gpy.c | 218 ++++++++++++++++++++++++++++++++++++++
|
||||
1 file changed, 218 insertions(+)
|
||||
|
||||
--- a/drivers/net/phy/mxl-gpy.c
|
||||
+++ b/drivers/net/phy/mxl-gpy.c
|
||||
@@ -38,6 +38,7 @@
|
||||
#define PHY_MIISTAT 0x18 /* MII state */
|
||||
#define PHY_IMASK 0x19 /* interrupt mask */
|
||||
#define PHY_ISTAT 0x1A /* interrupt status */
|
||||
+#define PHY_LED 0x1B /* LEDs */
|
||||
#define PHY_FWV 0x1E /* firmware version */
|
||||
|
||||
#define PHY_MIISTAT_SPD_MASK GENMASK(2, 0)
|
||||
@@ -61,6 +62,11 @@
|
||||
PHY_IMASK_ADSC | \
|
||||
PHY_IMASK_ANC)
|
||||
|
||||
+#define GPY_MAX_LEDS 4
|
||||
+#define PHY_LED_POLARITY(idx) BIT(12 + (idx))
|
||||
+#define PHY_LED_HWCONTROL(idx) BIT(8 + (idx))
|
||||
+#define PHY_LED_ON(idx) BIT(idx)
|
||||
+
|
||||
#define PHY_FWV_REL_MASK BIT(15)
|
||||
#define PHY_FWV_MAJOR_MASK GENMASK(11, 8)
|
||||
#define PHY_FWV_MINOR_MASK GENMASK(7, 0)
|
||||
@@ -72,6 +78,23 @@
|
||||
#define PHY_MDI_MDI_X_CD 0x1
|
||||
#define PHY_MDI_MDI_X_CROSS 0x0
|
||||
|
||||
+/* LED */
|
||||
+#define VSPEC1_LED(idx) (1 + (idx))
|
||||
+#define VSPEC1_LED_BLINKS GENMASK(15, 12)
|
||||
+#define VSPEC1_LED_PULSE GENMASK(11, 8)
|
||||
+#define VSPEC1_LED_CON GENMASK(7, 4)
|
||||
+#define VSPEC1_LED_BLINKF GENMASK(3, 0)
|
||||
+
|
||||
+#define VSPEC1_LED_LINK10 BIT(0)
|
||||
+#define VSPEC1_LED_LINK100 BIT(1)
|
||||
+#define VSPEC1_LED_LINK1000 BIT(2)
|
||||
+#define VSPEC1_LED_LINK2500 BIT(3)
|
||||
+
|
||||
+#define VSPEC1_LED_TXACT BIT(0)
|
||||
+#define VSPEC1_LED_RXACT BIT(1)
|
||||
+#define VSPEC1_LED_COL BIT(2)
|
||||
+#define VSPEC1_LED_NO_CON BIT(3)
|
||||
+
|
||||
/* SGMII */
|
||||
#define VSPEC1_SGMII_CTRL 0x08
|
||||
#define VSPEC1_SGMII_CTRL_ANEN BIT(12) /* Aneg enable */
|
||||
@@ -827,6 +850,156 @@ static int gpy115_loopback(struct phy_de
|
||||
return genphy_soft_reset(phydev);
|
||||
}
|
||||
|
||||
+static int gpy_led_brightness_set(struct phy_device *phydev,
|
||||
+ u8 index, enum led_brightness value)
|
||||
+{
|
||||
+ int ret;
|
||||
+
|
||||
+ if (index >= GPY_MAX_LEDS)
|
||||
+ return -EINVAL;
|
||||
+
|
||||
+ /* clear HWCONTROL and set manual LED state */
|
||||
+ ret = phy_modify(phydev, PHY_LED,
|
||||
+ ((value == LED_OFF) ? PHY_LED_HWCONTROL(index) : 0) |
|
||||
+ PHY_LED_ON(index),
|
||||
+ (value == LED_OFF) ? 0 : PHY_LED_ON(index));
|
||||
+ if (ret)
|
||||
+ return ret;
|
||||
+
|
||||
+ /* ToDo: set PWM brightness */
|
||||
+
|
||||
+ /* clear HW LED setup */
|
||||
+ if (value == LED_OFF)
|
||||
+ return phy_write_mmd(phydev, MDIO_MMD_VEND1, VSPEC1_LED(index), 0);
|
||||
+ else
|
||||
+ return 0;
|
||||
+}
|
||||
+
|
||||
+static const unsigned long supported_triggers = (BIT(TRIGGER_NETDEV_LINK) |
|
||||
+ BIT(TRIGGER_NETDEV_LINK_100) |
|
||||
+ BIT(TRIGGER_NETDEV_LINK_1000) |
|
||||
+ BIT(TRIGGER_NETDEV_LINK_2500) |
|
||||
+ BIT(TRIGGER_NETDEV_RX) |
|
||||
+ BIT(TRIGGER_NETDEV_TX));
|
||||
+
|
||||
+static int gpy_led_hw_is_supported(struct phy_device *phydev, u8 index,
|
||||
+ unsigned long rules)
|
||||
+{
|
||||
+ if (index >= GPY_MAX_LEDS)
|
||||
+ return -EINVAL;
|
||||
+
|
||||
+ /* All combinations of the supported triggers are allowed */
|
||||
+ if (rules & ~supported_triggers)
|
||||
+ return -EOPNOTSUPP;
|
||||
+
|
||||
+ return 0;
|
||||
+}
|
||||
+
|
||||
+static int gpy_led_hw_control_get(struct phy_device *phydev, u8 index,
|
||||
+ unsigned long *rules)
|
||||
+{
|
||||
+ int val;
|
||||
+
|
||||
+ if (index >= GPY_MAX_LEDS)
|
||||
+ return -EINVAL;
|
||||
+
|
||||
+ val = phy_read_mmd(phydev, MDIO_MMD_VEND1, VSPEC1_LED(index));
|
||||
+ if (val < 0)
|
||||
+ return val;
|
||||
+
|
||||
+ if (FIELD_GET(VSPEC1_LED_CON, val) & VSPEC1_LED_LINK10)
|
||||
+ *rules |= BIT(TRIGGER_NETDEV_LINK_10);
|
||||
+
|
||||
+ if (FIELD_GET(VSPEC1_LED_CON, val) & VSPEC1_LED_LINK100)
|
||||
+ *rules |= BIT(TRIGGER_NETDEV_LINK_100);
|
||||
+
|
||||
+ if (FIELD_GET(VSPEC1_LED_CON, val) & VSPEC1_LED_LINK1000)
|
||||
+ *rules |= BIT(TRIGGER_NETDEV_LINK_1000);
|
||||
+
|
||||
+ if (FIELD_GET(VSPEC1_LED_CON, val) & VSPEC1_LED_LINK2500)
|
||||
+ *rules |= BIT(TRIGGER_NETDEV_LINK_2500);
|
||||
+
|
||||
+ if (FIELD_GET(VSPEC1_LED_CON, val) == (VSPEC1_LED_LINK10 |
|
||||
+ VSPEC1_LED_LINK100 |
|
||||
+ VSPEC1_LED_LINK1000 |
|
||||
+ VSPEC1_LED_LINK2500))
|
||||
+ *rules |= BIT(TRIGGER_NETDEV_LINK);
|
||||
+
|
||||
+ if (FIELD_GET(VSPEC1_LED_PULSE, val) & VSPEC1_LED_TXACT)
|
||||
+ *rules |= BIT(TRIGGER_NETDEV_TX);
|
||||
+
|
||||
+ if (FIELD_GET(VSPEC1_LED_PULSE, val) & VSPEC1_LED_RXACT)
|
||||
+ *rules |= BIT(TRIGGER_NETDEV_RX);
|
||||
+
|
||||
+ return 0;
|
||||
+}
|
||||
+
|
||||
+static int gpy_led_hw_control_set(struct phy_device *phydev, u8 index,
|
||||
+ unsigned long rules)
|
||||
+{
|
||||
+ u16 val = 0;
|
||||
+ int ret;
|
||||
+
|
||||
+ if (index >= GPY_MAX_LEDS)
|
||||
+ return -EINVAL;
|
||||
+
|
||||
+ if (rules & BIT(TRIGGER_NETDEV_LINK) ||
|
||||
+ rules & BIT(TRIGGER_NETDEV_LINK_10))
|
||||
+ val |= FIELD_PREP(VSPEC1_LED_CON, VSPEC1_LED_LINK10);
|
||||
+
|
||||
+ if (rules & BIT(TRIGGER_NETDEV_LINK) ||
|
||||
+ rules & BIT(TRIGGER_NETDEV_LINK_100))
|
||||
+ val |= FIELD_PREP(VSPEC1_LED_CON, VSPEC1_LED_LINK100);
|
||||
+
|
||||
+ if (rules & BIT(TRIGGER_NETDEV_LINK) ||
|
||||
+ rules & BIT(TRIGGER_NETDEV_LINK_1000))
|
||||
+ val |= FIELD_PREP(VSPEC1_LED_CON, VSPEC1_LED_LINK1000);
|
||||
+
|
||||
+ if (rules & BIT(TRIGGER_NETDEV_LINK) ||
|
||||
+ rules & BIT(TRIGGER_NETDEV_LINK_2500))
|
||||
+ val |= FIELD_PREP(VSPEC1_LED_CON, VSPEC1_LED_LINK2500);
|
||||
+
|
||||
+ if (rules & BIT(TRIGGER_NETDEV_TX))
|
||||
+ val |= FIELD_PREP(VSPEC1_LED_PULSE, VSPEC1_LED_TXACT);
|
||||
+
|
||||
+ if (rules & BIT(TRIGGER_NETDEV_RX))
|
||||
+ val |= FIELD_PREP(VSPEC1_LED_PULSE, VSPEC1_LED_RXACT);
|
||||
+
|
||||
+ /* allow RX/TX pulse without link indication */
|
||||
+ if ((rules & BIT(TRIGGER_NETDEV_TX) || rules & BIT(TRIGGER_NETDEV_RX)) &&
|
||||
+ !(val & VSPEC1_LED_CON))
|
||||
+ val |= FIELD_PREP(VSPEC1_LED_PULSE, VSPEC1_LED_NO_CON) | VSPEC1_LED_CON;
|
||||
+
|
||||
+ ret = phy_write_mmd(phydev, MDIO_MMD_VEND1, VSPEC1_LED(index), val);
|
||||
+ if (ret)
|
||||
+ return ret;
|
||||
+
|
||||
+ return phy_set_bits(phydev, PHY_LED, PHY_LED_HWCONTROL(index));
|
||||
+}
|
||||
+
|
||||
+static int gpy_led_polarity_set(struct phy_device *phydev, int index,
|
||||
+ unsigned long modes)
|
||||
+{
|
||||
+ bool active_low = false;
|
||||
+ u32 mode;
|
||||
+
|
||||
+ if (index >= GPY_MAX_LEDS)
|
||||
+ return -EINVAL;
|
||||
+
|
||||
+ for_each_set_bit(mode, &modes, __PHY_LED_MODES_NUM) {
|
||||
+ switch (mode) {
|
||||
+ case PHY_LED_ACTIVE_LOW:
|
||||
+ active_low = true;
|
||||
+ break;
|
||||
+ default:
|
||||
+ return -EINVAL;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ return phy_modify(phydev, PHY_LED, PHY_LED_POLARITY(index),
|
||||
+ active_low ? 0 : PHY_LED_POLARITY(index));
|
||||
+}
|
||||
+
|
||||
static struct phy_driver gpy_drivers[] = {
|
||||
{
|
||||
PHY_ID_MATCH_MODEL(PHY_ID_GPY2xx),
|
||||
@@ -844,6 +1017,11 @@ static struct phy_driver gpy_drivers[] =
|
||||
.set_wol = gpy_set_wol,
|
||||
.get_wol = gpy_get_wol,
|
||||
.set_loopback = gpy_loopback,
|
||||
+ .led_brightness_set = gpy_led_brightness_set,
|
||||
+ .led_hw_is_supported = gpy_led_hw_is_supported,
|
||||
+ .led_hw_control_get = gpy_led_hw_control_get,
|
||||
+ .led_hw_control_set = gpy_led_hw_control_set,
|
||||
+ .led_polarity_set = gpy_led_polarity_set,
|
||||
},
|
||||
{
|
||||
.phy_id = PHY_ID_GPY115B,
|
||||
@@ -862,6 +1040,11 @@ static struct phy_driver gpy_drivers[] =
|
||||
.set_wol = gpy_set_wol,
|
||||
.get_wol = gpy_get_wol,
|
||||
.set_loopback = gpy115_loopback,
|
||||
+ .led_brightness_set = gpy_led_brightness_set,
|
||||
+ .led_hw_is_supported = gpy_led_hw_is_supported,
|
||||
+ .led_hw_control_get = gpy_led_hw_control_get,
|
||||
+ .led_hw_control_set = gpy_led_hw_control_set,
|
||||
+ .led_polarity_set = gpy_led_polarity_set,
|
||||
},
|
||||
{
|
||||
PHY_ID_MATCH_MODEL(PHY_ID_GPY115C),
|
||||
@@ -879,6 +1062,11 @@ static struct phy_driver gpy_drivers[] =
|
||||
.set_wol = gpy_set_wol,
|
||||
.get_wol = gpy_get_wol,
|
||||
.set_loopback = gpy115_loopback,
|
||||
+ .led_brightness_set = gpy_led_brightness_set,
|
||||
+ .led_hw_is_supported = gpy_led_hw_is_supported,
|
||||
+ .led_hw_control_get = gpy_led_hw_control_get,
|
||||
+ .led_hw_control_set = gpy_led_hw_control_set,
|
||||
+ .led_polarity_set = gpy_led_polarity_set,
|
||||
},
|
||||
{
|
||||
.phy_id = PHY_ID_GPY211B,
|
||||
@@ -897,6 +1085,11 @@ static struct phy_driver gpy_drivers[] =
|
||||
.set_wol = gpy_set_wol,
|
||||
.get_wol = gpy_get_wol,
|
||||
.set_loopback = gpy_loopback,
|
||||
+ .led_brightness_set = gpy_led_brightness_set,
|
||||
+ .led_hw_is_supported = gpy_led_hw_is_supported,
|
||||
+ .led_hw_control_get = gpy_led_hw_control_get,
|
||||
+ .led_hw_control_set = gpy_led_hw_control_set,
|
||||
+ .led_polarity_set = gpy_led_polarity_set,
|
||||
},
|
||||
{
|
||||
PHY_ID_MATCH_MODEL(PHY_ID_GPY211C),
|
||||
@@ -914,6 +1107,11 @@ static struct phy_driver gpy_drivers[] =
|
||||
.set_wol = gpy_set_wol,
|
||||
.get_wol = gpy_get_wol,
|
||||
.set_loopback = gpy_loopback,
|
||||
+ .led_brightness_set = gpy_led_brightness_set,
|
||||
+ .led_hw_is_supported = gpy_led_hw_is_supported,
|
||||
+ .led_hw_control_get = gpy_led_hw_control_get,
|
||||
+ .led_hw_control_set = gpy_led_hw_control_set,
|
||||
+ .led_polarity_set = gpy_led_polarity_set,
|
||||
},
|
||||
{
|
||||
.phy_id = PHY_ID_GPY212B,
|
||||
@@ -932,6 +1130,11 @@ static struct phy_driver gpy_drivers[] =
|
||||
.set_wol = gpy_set_wol,
|
||||
.get_wol = gpy_get_wol,
|
||||
.set_loopback = gpy_loopback,
|
||||
+ .led_brightness_set = gpy_led_brightness_set,
|
||||
+ .led_hw_is_supported = gpy_led_hw_is_supported,
|
||||
+ .led_hw_control_get = gpy_led_hw_control_get,
|
||||
+ .led_hw_control_set = gpy_led_hw_control_set,
|
||||
+ .led_polarity_set = gpy_led_polarity_set,
|
||||
},
|
||||
{
|
||||
PHY_ID_MATCH_MODEL(PHY_ID_GPY212C),
|
||||
@@ -949,6 +1152,11 @@ static struct phy_driver gpy_drivers[] =
|
||||
.set_wol = gpy_set_wol,
|
||||
.get_wol = gpy_get_wol,
|
||||
.set_loopback = gpy_loopback,
|
||||
+ .led_brightness_set = gpy_led_brightness_set,
|
||||
+ .led_hw_is_supported = gpy_led_hw_is_supported,
|
||||
+ .led_hw_control_get = gpy_led_hw_control_get,
|
||||
+ .led_hw_control_set = gpy_led_hw_control_set,
|
||||
+ .led_polarity_set = gpy_led_polarity_set,
|
||||
},
|
||||
{
|
||||
.phy_id = PHY_ID_GPY215B,
|
||||
@@ -967,6 +1175,11 @@ static struct phy_driver gpy_drivers[] =
|
||||
.set_wol = gpy_set_wol,
|
||||
.get_wol = gpy_get_wol,
|
||||
.set_loopback = gpy_loopback,
|
||||
+ .led_brightness_set = gpy_led_brightness_set,
|
||||
+ .led_hw_is_supported = gpy_led_hw_is_supported,
|
||||
+ .led_hw_control_get = gpy_led_hw_control_get,
|
||||
+ .led_hw_control_set = gpy_led_hw_control_set,
|
||||
+ .led_polarity_set = gpy_led_polarity_set,
|
||||
},
|
||||
{
|
||||
PHY_ID_MATCH_MODEL(PHY_ID_GPY215C),
|
||||
@@ -984,6 +1197,11 @@ static struct phy_driver gpy_drivers[] =
|
||||
.set_wol = gpy_set_wol,
|
||||
.get_wol = gpy_get_wol,
|
||||
.set_loopback = gpy_loopback,
|
||||
+ .led_brightness_set = gpy_led_brightness_set,
|
||||
+ .led_hw_is_supported = gpy_led_hw_is_supported,
|
||||
+ .led_hw_control_get = gpy_led_hw_control_get,
|
||||
+ .led_hw_control_set = gpy_led_hw_control_set,
|
||||
+ .led_polarity_set = gpy_led_polarity_set,
|
||||
},
|
||||
{
|
||||
PHY_ID_MATCH_MODEL(PHY_ID_GPY241B),
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
From f95b4725e796b12e5f347a0d161e1d3843142aa8 Mon Sep 17 00:00:00 2001
|
||||
From: Daniel Golle <daniel@makrotopia.org>
|
||||
Date: Fri, 4 Oct 2024 16:56:35 +0100
|
||||
Subject: [PATCH] net: phy: mxl-gpy: add missing support for
|
||||
TRIGGER_NETDEV_LINK_10
|
||||
|
||||
The PHY also support 10MBit/s links as well as the corresponding link
|
||||
indication trigger to be offloaded. Add TRIGGER_NETDEV_LINK_10 to the
|
||||
supported triggers.
|
||||
|
||||
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
|
||||
Reviewed-by: Andrew Lunn <andrew@lunn.ch>
|
||||
Link: https://patch.msgid.link/cc5da0a989af8b0d49d823656d88053c4de2ab98.1728057367.git.daniel@makrotopia.org
|
||||
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
|
||||
---
|
||||
drivers/net/phy/mxl-gpy.c | 1 +
|
||||
1 file changed, 1 insertion(+)
|
||||
|
||||
--- a/drivers/net/phy/mxl-gpy.c
|
||||
+++ b/drivers/net/phy/mxl-gpy.c
|
||||
@@ -876,6 +876,7 @@ static int gpy_led_brightness_set(struct
|
||||
}
|
||||
|
||||
static const unsigned long supported_triggers = (BIT(TRIGGER_NETDEV_LINK) |
|
||||
+ BIT(TRIGGER_NETDEV_LINK_10) |
|
||||
BIT(TRIGGER_NETDEV_LINK_100) |
|
||||
BIT(TRIGGER_NETDEV_LINK_1000) |
|
||||
BIT(TRIGGER_NETDEV_LINK_2500) |
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
From eb89c79c1b8f17fc1611540768678e60df89ac42 Mon Sep 17 00:00:00 2001
|
||||
From: Daniel Golle <daniel@makrotopia.org>
|
||||
Date: Thu, 10 Oct 2024 13:55:17 +0100
|
||||
Subject: [PATCH 3/4] net: phy: mxl-gpy: correctly describe LED polarity
|
||||
|
||||
According the datasheet covering the LED (0x1b) register:
|
||||
0B Active High LEDx pin driven high when activated
|
||||
1B Active Low LEDx pin driven low when activated
|
||||
|
||||
Make use of the now available 'active-high' property and correctly
|
||||
reflect the polarity setting which was previously inverted.
|
||||
|
||||
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
|
||||
Reviewed-by: Andrew Lunn <andrew@lunn.ch>
|
||||
Link: https://patch.msgid.link/180ccafa837f09908b852a8a874a3808c5ecd2d0.1728558223.git.daniel@makrotopia.org
|
||||
Signed-off-by: Paolo Abeni <pabeni@redhat.com>
|
||||
---
|
||||
drivers/net/phy/mxl-gpy.c | 16 ++++++++++++----
|
||||
1 file changed, 12 insertions(+), 4 deletions(-)
|
||||
|
||||
--- a/drivers/net/phy/mxl-gpy.c
|
||||
+++ b/drivers/net/phy/mxl-gpy.c
|
||||
@@ -981,7 +981,7 @@ static int gpy_led_hw_control_set(struct
|
||||
static int gpy_led_polarity_set(struct phy_device *phydev, int index,
|
||||
unsigned long modes)
|
||||
{
|
||||
- bool active_low = false;
|
||||
+ bool force_active_low = false, force_active_high = false;
|
||||
u32 mode;
|
||||
|
||||
if (index >= GPY_MAX_LEDS)
|
||||
@@ -990,15 +990,23 @@ static int gpy_led_polarity_set(struct p
|
||||
for_each_set_bit(mode, &modes, __PHY_LED_MODES_NUM) {
|
||||
switch (mode) {
|
||||
case PHY_LED_ACTIVE_LOW:
|
||||
- active_low = true;
|
||||
+ force_active_low = true;
|
||||
+ break;
|
||||
+ case PHY_LED_ACTIVE_HIGH:
|
||||
+ force_active_high = true;
|
||||
break;
|
||||
default:
|
||||
return -EINVAL;
|
||||
}
|
||||
}
|
||||
|
||||
- return phy_modify(phydev, PHY_LED, PHY_LED_POLARITY(index),
|
||||
- active_low ? 0 : PHY_LED_POLARITY(index));
|
||||
+ if (force_active_low)
|
||||
+ return phy_set_bits(phydev, PHY_LED, PHY_LED_POLARITY(index));
|
||||
+
|
||||
+ if (force_active_high)
|
||||
+ return phy_clear_bits(phydev, PHY_LED, PHY_LED_POLARITY(index));
|
||||
+
|
||||
+ unreachable();
|
||||
}
|
||||
|
||||
static struct phy_driver gpy_drivers[] = {
|
||||
+379
@@ -0,0 +1,379 @@
|
||||
From 1758af47b98c17da464cb45f476875150955dd48 Mon Sep 17 00:00:00 2001
|
||||
From: Daniel Golle <daniel@makrotopia.org>
|
||||
Date: Thu, 10 Oct 2024 13:55:29 +0100
|
||||
Subject: [PATCH 4/4] net: phy: intel-xway: add support for PHY LEDs
|
||||
|
||||
The intel-xway PHY driver predates the PHY LED framework and currently
|
||||
initializes all LED pins to equal default values.
|
||||
|
||||
Add PHY LED functions to the drivers and don't set default values if
|
||||
LEDs are defined in device tree.
|
||||
|
||||
According the datasheets 3 LEDs are supported on all Intel XWAY PHYs.
|
||||
|
||||
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
|
||||
Reviewed-by: Andrew Lunn <andrew@lunn.ch>
|
||||
Link: https://patch.msgid.link/81f4717ab9acf38f3239727a4540ae96fd01109b.1728558223.git.daniel@makrotopia.org
|
||||
Signed-off-by: Paolo Abeni <pabeni@redhat.com>
|
||||
---
|
||||
drivers/net/phy/intel-xway.c | 253 +++++++++++++++++++++++++++++++++--
|
||||
1 file changed, 244 insertions(+), 9 deletions(-)
|
||||
|
||||
--- a/drivers/net/phy/intel-xway.c
|
||||
+++ b/drivers/net/phy/intel-xway.c
|
||||
@@ -151,6 +151,13 @@
|
||||
#define XWAY_MMD_LED3H 0x01E8
|
||||
#define XWAY_MMD_LED3L 0x01E9
|
||||
|
||||
+#define XWAY_GPHY_MAX_LEDS 3
|
||||
+#define XWAY_GPHY_LED_INV(idx) BIT(12 + (idx))
|
||||
+#define XWAY_GPHY_LED_EN(idx) BIT(8 + (idx))
|
||||
+#define XWAY_GPHY_LED_DA(idx) BIT(idx)
|
||||
+#define XWAY_MMD_LEDxH(idx) (XWAY_MMD_LED0H + 2 * (idx))
|
||||
+#define XWAY_MMD_LEDxL(idx) (XWAY_MMD_LED0L + 2 * (idx))
|
||||
+
|
||||
#define PHY_ID_PHY11G_1_3 0x030260D1
|
||||
#define PHY_ID_PHY22F_1_3 0x030260E1
|
||||
#define PHY_ID_PHY11G_1_4 0xD565A400
|
||||
@@ -229,20 +236,12 @@ static int xway_gphy_rgmii_init(struct p
|
||||
XWAY_MDIO_MIICTRL_TXSKEW_MASK, val);
|
||||
}
|
||||
|
||||
-static int xway_gphy_config_init(struct phy_device *phydev)
|
||||
+static int xway_gphy_init_leds(struct phy_device *phydev)
|
||||
{
|
||||
int err;
|
||||
u32 ledxh;
|
||||
u32 ledxl;
|
||||
|
||||
- /* Mask all interrupts */
|
||||
- err = phy_write(phydev, XWAY_MDIO_IMASK, 0);
|
||||
- if (err)
|
||||
- return err;
|
||||
-
|
||||
- /* Clear all pending interrupts */
|
||||
- phy_read(phydev, XWAY_MDIO_ISTAT);
|
||||
-
|
||||
/* Ensure that integrated led function is enabled for all leds */
|
||||
err = phy_write(phydev, XWAY_MDIO_LED,
|
||||
XWAY_MDIO_LED_LED0_EN |
|
||||
@@ -276,6 +275,26 @@ static int xway_gphy_config_init(struct
|
||||
phy_write_mmd(phydev, MDIO_MMD_VEND2, XWAY_MMD_LED2H, ledxh);
|
||||
phy_write_mmd(phydev, MDIO_MMD_VEND2, XWAY_MMD_LED2L, ledxl);
|
||||
|
||||
+ return 0;
|
||||
+}
|
||||
+
|
||||
+static int xway_gphy_config_init(struct phy_device *phydev)
|
||||
+{
|
||||
+ struct device_node *np = phydev->mdio.dev.of_node;
|
||||
+ int err;
|
||||
+
|
||||
+ /* Mask all interrupts */
|
||||
+ err = phy_write(phydev, XWAY_MDIO_IMASK, 0);
|
||||
+ if (err)
|
||||
+ return err;
|
||||
+
|
||||
+ /* Use default LED configuration if 'leds' node isn't defined */
|
||||
+ if (!of_get_child_by_name(np, "leds"))
|
||||
+ xway_gphy_init_leds(phydev);
|
||||
+
|
||||
+ /* Clear all pending interrupts */
|
||||
+ phy_read(phydev, XWAY_MDIO_ISTAT);
|
||||
+
|
||||
err = xway_gphy_rgmii_init(phydev);
|
||||
if (err)
|
||||
return err;
|
||||
@@ -347,6 +366,172 @@ static irqreturn_t xway_gphy_handle_inte
|
||||
return IRQ_HANDLED;
|
||||
}
|
||||
|
||||
+static int xway_gphy_led_brightness_set(struct phy_device *phydev,
|
||||
+ u8 index, enum led_brightness value)
|
||||
+{
|
||||
+ int ret;
|
||||
+
|
||||
+ if (index >= XWAY_GPHY_MAX_LEDS)
|
||||
+ return -EINVAL;
|
||||
+
|
||||
+ /* clear EN and set manual LED state */
|
||||
+ ret = phy_modify(phydev, XWAY_MDIO_LED,
|
||||
+ ((value == LED_OFF) ? XWAY_GPHY_LED_EN(index) : 0) |
|
||||
+ XWAY_GPHY_LED_DA(index),
|
||||
+ (value == LED_OFF) ? 0 : XWAY_GPHY_LED_DA(index));
|
||||
+ if (ret)
|
||||
+ return ret;
|
||||
+
|
||||
+ /* clear HW LED setup */
|
||||
+ if (value == LED_OFF) {
|
||||
+ ret = phy_write_mmd(phydev, MDIO_MMD_VEND2, XWAY_MMD_LEDxH(index), 0);
|
||||
+ if (ret)
|
||||
+ return ret;
|
||||
+
|
||||
+ return phy_write_mmd(phydev, MDIO_MMD_VEND2, XWAY_MMD_LEDxL(index), 0);
|
||||
+ } else {
|
||||
+ return 0;
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+static const unsigned long supported_triggers = (BIT(TRIGGER_NETDEV_LINK) |
|
||||
+ BIT(TRIGGER_NETDEV_LINK_10) |
|
||||
+ BIT(TRIGGER_NETDEV_LINK_100) |
|
||||
+ BIT(TRIGGER_NETDEV_LINK_1000) |
|
||||
+ BIT(TRIGGER_NETDEV_RX) |
|
||||
+ BIT(TRIGGER_NETDEV_TX));
|
||||
+
|
||||
+static int xway_gphy_led_hw_is_supported(struct phy_device *phydev, u8 index,
|
||||
+ unsigned long rules)
|
||||
+{
|
||||
+ if (index >= XWAY_GPHY_MAX_LEDS)
|
||||
+ return -EINVAL;
|
||||
+
|
||||
+ /* activity triggers are not possible without combination with a link
|
||||
+ * trigger.
|
||||
+ */
|
||||
+ if (rules & (BIT(TRIGGER_NETDEV_RX) | BIT(TRIGGER_NETDEV_TX)) &&
|
||||
+ !(rules & (BIT(TRIGGER_NETDEV_LINK) |
|
||||
+ BIT(TRIGGER_NETDEV_LINK_10) |
|
||||
+ BIT(TRIGGER_NETDEV_LINK_100) |
|
||||
+ BIT(TRIGGER_NETDEV_LINK_1000))))
|
||||
+ return -EOPNOTSUPP;
|
||||
+
|
||||
+ /* All other combinations of the supported triggers are allowed */
|
||||
+ if (rules & ~supported_triggers)
|
||||
+ return -EOPNOTSUPP;
|
||||
+
|
||||
+ return 0;
|
||||
+}
|
||||
+
|
||||
+static int xway_gphy_led_hw_control_get(struct phy_device *phydev, u8 index,
|
||||
+ unsigned long *rules)
|
||||
+{
|
||||
+ int lval, hval;
|
||||
+
|
||||
+ if (index >= XWAY_GPHY_MAX_LEDS)
|
||||
+ return -EINVAL;
|
||||
+
|
||||
+ hval = phy_read_mmd(phydev, MDIO_MMD_VEND2, XWAY_MMD_LEDxH(index));
|
||||
+ if (hval < 0)
|
||||
+ return hval;
|
||||
+
|
||||
+ lval = phy_read_mmd(phydev, MDIO_MMD_VEND2, XWAY_MMD_LEDxL(index));
|
||||
+ if (lval < 0)
|
||||
+ return lval;
|
||||
+
|
||||
+ if (hval & XWAY_MMD_LEDxH_CON_LINK10)
|
||||
+ *rules |= BIT(TRIGGER_NETDEV_LINK_10);
|
||||
+
|
||||
+ if (hval & XWAY_MMD_LEDxH_CON_LINK100)
|
||||
+ *rules |= BIT(TRIGGER_NETDEV_LINK_100);
|
||||
+
|
||||
+ if (hval & XWAY_MMD_LEDxH_CON_LINK1000)
|
||||
+ *rules |= BIT(TRIGGER_NETDEV_LINK_1000);
|
||||
+
|
||||
+ if ((hval & XWAY_MMD_LEDxH_CON_LINK10) &&
|
||||
+ (hval & XWAY_MMD_LEDxH_CON_LINK100) &&
|
||||
+ (hval & XWAY_MMD_LEDxH_CON_LINK1000))
|
||||
+ *rules |= BIT(TRIGGER_NETDEV_LINK);
|
||||
+
|
||||
+ if (lval & XWAY_MMD_LEDxL_PULSE_TXACT)
|
||||
+ *rules |= BIT(TRIGGER_NETDEV_TX);
|
||||
+
|
||||
+ if (lval & XWAY_MMD_LEDxL_PULSE_RXACT)
|
||||
+ *rules |= BIT(TRIGGER_NETDEV_RX);
|
||||
+
|
||||
+ return 0;
|
||||
+}
|
||||
+
|
||||
+static int xway_gphy_led_hw_control_set(struct phy_device *phydev, u8 index,
|
||||
+ unsigned long rules)
|
||||
+{
|
||||
+ u16 hval = 0, lval = 0;
|
||||
+ int ret;
|
||||
+
|
||||
+ if (index >= XWAY_GPHY_MAX_LEDS)
|
||||
+ return -EINVAL;
|
||||
+
|
||||
+ if (rules & BIT(TRIGGER_NETDEV_LINK) ||
|
||||
+ rules & BIT(TRIGGER_NETDEV_LINK_10))
|
||||
+ hval |= XWAY_MMD_LEDxH_CON_LINK10;
|
||||
+
|
||||
+ if (rules & BIT(TRIGGER_NETDEV_LINK) ||
|
||||
+ rules & BIT(TRIGGER_NETDEV_LINK_100))
|
||||
+ hval |= XWAY_MMD_LEDxH_CON_LINK100;
|
||||
+
|
||||
+ if (rules & BIT(TRIGGER_NETDEV_LINK) ||
|
||||
+ rules & BIT(TRIGGER_NETDEV_LINK_1000))
|
||||
+ hval |= XWAY_MMD_LEDxH_CON_LINK1000;
|
||||
+
|
||||
+ if (rules & BIT(TRIGGER_NETDEV_TX))
|
||||
+ lval |= XWAY_MMD_LEDxL_PULSE_TXACT;
|
||||
+
|
||||
+ if (rules & BIT(TRIGGER_NETDEV_RX))
|
||||
+ lval |= XWAY_MMD_LEDxL_PULSE_RXACT;
|
||||
+
|
||||
+ ret = phy_write_mmd(phydev, MDIO_MMD_VEND2, XWAY_MMD_LEDxH(index), hval);
|
||||
+ if (ret)
|
||||
+ return ret;
|
||||
+
|
||||
+ ret = phy_write_mmd(phydev, MDIO_MMD_VEND2, XWAY_MMD_LEDxL(index), lval);
|
||||
+ if (ret)
|
||||
+ return ret;
|
||||
+
|
||||
+ return phy_set_bits(phydev, XWAY_MDIO_LED, XWAY_GPHY_LED_EN(index));
|
||||
+}
|
||||
+
|
||||
+static int xway_gphy_led_polarity_set(struct phy_device *phydev, int index,
|
||||
+ unsigned long modes)
|
||||
+{
|
||||
+ bool force_active_low = false, force_active_high = false;
|
||||
+ u32 mode;
|
||||
+
|
||||
+ if (index >= XWAY_GPHY_MAX_LEDS)
|
||||
+ return -EINVAL;
|
||||
+
|
||||
+ for_each_set_bit(mode, &modes, __PHY_LED_MODES_NUM) {
|
||||
+ switch (mode) {
|
||||
+ case PHY_LED_ACTIVE_LOW:
|
||||
+ force_active_low = true;
|
||||
+ break;
|
||||
+ case PHY_LED_ACTIVE_HIGH:
|
||||
+ force_active_high = true;
|
||||
+ break;
|
||||
+ default:
|
||||
+ return -EINVAL;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ if (force_active_low)
|
||||
+ return phy_set_bits(phydev, XWAY_MDIO_LED, XWAY_GPHY_LED_INV(index));
|
||||
+
|
||||
+ if (force_active_high)
|
||||
+ return phy_clear_bits(phydev, XWAY_MDIO_LED, XWAY_GPHY_LED_INV(index));
|
||||
+
|
||||
+ unreachable();
|
||||
+}
|
||||
+
|
||||
static struct phy_driver xway_gphy[] = {
|
||||
{
|
||||
.phy_id = PHY_ID_PHY11G_1_3,
|
||||
@@ -359,6 +544,11 @@ static struct phy_driver xway_gphy[] = {
|
||||
.config_intr = xway_gphy_config_intr,
|
||||
.suspend = genphy_suspend,
|
||||
.resume = genphy_resume,
|
||||
+ .led_brightness_set = xway_gphy_led_brightness_set,
|
||||
+ .led_hw_is_supported = xway_gphy_led_hw_is_supported,
|
||||
+ .led_hw_control_get = xway_gphy_led_hw_control_get,
|
||||
+ .led_hw_control_set = xway_gphy_led_hw_control_set,
|
||||
+ .led_polarity_set = xway_gphy_led_polarity_set,
|
||||
}, {
|
||||
.phy_id = PHY_ID_PHY22F_1_3,
|
||||
.phy_id_mask = 0xffffffff,
|
||||
@@ -370,6 +560,11 @@ static struct phy_driver xway_gphy[] = {
|
||||
.config_intr = xway_gphy_config_intr,
|
||||
.suspend = genphy_suspend,
|
||||
.resume = genphy_resume,
|
||||
+ .led_brightness_set = xway_gphy_led_brightness_set,
|
||||
+ .led_hw_is_supported = xway_gphy_led_hw_is_supported,
|
||||
+ .led_hw_control_get = xway_gphy_led_hw_control_get,
|
||||
+ .led_hw_control_set = xway_gphy_led_hw_control_set,
|
||||
+ .led_polarity_set = xway_gphy_led_polarity_set,
|
||||
}, {
|
||||
.phy_id = PHY_ID_PHY11G_1_4,
|
||||
.phy_id_mask = 0xffffffff,
|
||||
@@ -381,6 +576,11 @@ static struct phy_driver xway_gphy[] = {
|
||||
.config_intr = xway_gphy_config_intr,
|
||||
.suspend = genphy_suspend,
|
||||
.resume = genphy_resume,
|
||||
+ .led_brightness_set = xway_gphy_led_brightness_set,
|
||||
+ .led_hw_is_supported = xway_gphy_led_hw_is_supported,
|
||||
+ .led_hw_control_get = xway_gphy_led_hw_control_get,
|
||||
+ .led_hw_control_set = xway_gphy_led_hw_control_set,
|
||||
+ .led_polarity_set = xway_gphy_led_polarity_set,
|
||||
}, {
|
||||
.phy_id = PHY_ID_PHY22F_1_4,
|
||||
.phy_id_mask = 0xffffffff,
|
||||
@@ -392,6 +592,11 @@ static struct phy_driver xway_gphy[] = {
|
||||
.config_intr = xway_gphy_config_intr,
|
||||
.suspend = genphy_suspend,
|
||||
.resume = genphy_resume,
|
||||
+ .led_brightness_set = xway_gphy_led_brightness_set,
|
||||
+ .led_hw_is_supported = xway_gphy_led_hw_is_supported,
|
||||
+ .led_hw_control_get = xway_gphy_led_hw_control_get,
|
||||
+ .led_hw_control_set = xway_gphy_led_hw_control_set,
|
||||
+ .led_polarity_set = xway_gphy_led_polarity_set,
|
||||
}, {
|
||||
.phy_id = PHY_ID_PHY11G_1_5,
|
||||
.phy_id_mask = 0xffffffff,
|
||||
@@ -402,6 +607,11 @@ static struct phy_driver xway_gphy[] = {
|
||||
.config_intr = xway_gphy_config_intr,
|
||||
.suspend = genphy_suspend,
|
||||
.resume = genphy_resume,
|
||||
+ .led_brightness_set = xway_gphy_led_brightness_set,
|
||||
+ .led_hw_is_supported = xway_gphy_led_hw_is_supported,
|
||||
+ .led_hw_control_get = xway_gphy_led_hw_control_get,
|
||||
+ .led_hw_control_set = xway_gphy_led_hw_control_set,
|
||||
+ .led_polarity_set = xway_gphy_led_polarity_set,
|
||||
}, {
|
||||
.phy_id = PHY_ID_PHY22F_1_5,
|
||||
.phy_id_mask = 0xffffffff,
|
||||
@@ -412,6 +622,11 @@ static struct phy_driver xway_gphy[] = {
|
||||
.config_intr = xway_gphy_config_intr,
|
||||
.suspend = genphy_suspend,
|
||||
.resume = genphy_resume,
|
||||
+ .led_brightness_set = xway_gphy_led_brightness_set,
|
||||
+ .led_hw_is_supported = xway_gphy_led_hw_is_supported,
|
||||
+ .led_hw_control_get = xway_gphy_led_hw_control_get,
|
||||
+ .led_hw_control_set = xway_gphy_led_hw_control_set,
|
||||
+ .led_polarity_set = xway_gphy_led_polarity_set,
|
||||
}, {
|
||||
.phy_id = PHY_ID_PHY11G_VR9_1_1,
|
||||
.phy_id_mask = 0xffffffff,
|
||||
@@ -422,6 +637,11 @@ static struct phy_driver xway_gphy[] = {
|
||||
.config_intr = xway_gphy_config_intr,
|
||||
.suspend = genphy_suspend,
|
||||
.resume = genphy_resume,
|
||||
+ .led_brightness_set = xway_gphy_led_brightness_set,
|
||||
+ .led_hw_is_supported = xway_gphy_led_hw_is_supported,
|
||||
+ .led_hw_control_get = xway_gphy_led_hw_control_get,
|
||||
+ .led_hw_control_set = xway_gphy_led_hw_control_set,
|
||||
+ .led_polarity_set = xway_gphy_led_polarity_set,
|
||||
}, {
|
||||
.phy_id = PHY_ID_PHY22F_VR9_1_1,
|
||||
.phy_id_mask = 0xffffffff,
|
||||
@@ -432,6 +652,11 @@ static struct phy_driver xway_gphy[] = {
|
||||
.config_intr = xway_gphy_config_intr,
|
||||
.suspend = genphy_suspend,
|
||||
.resume = genphy_resume,
|
||||
+ .led_brightness_set = xway_gphy_led_brightness_set,
|
||||
+ .led_hw_is_supported = xway_gphy_led_hw_is_supported,
|
||||
+ .led_hw_control_get = xway_gphy_led_hw_control_get,
|
||||
+ .led_hw_control_set = xway_gphy_led_hw_control_set,
|
||||
+ .led_polarity_set = xway_gphy_led_polarity_set,
|
||||
}, {
|
||||
.phy_id = PHY_ID_PHY11G_VR9_1_2,
|
||||
.phy_id_mask = 0xffffffff,
|
||||
@@ -442,6 +667,11 @@ static struct phy_driver xway_gphy[] = {
|
||||
.config_intr = xway_gphy_config_intr,
|
||||
.suspend = genphy_suspend,
|
||||
.resume = genphy_resume,
|
||||
+ .led_brightness_set = xway_gphy_led_brightness_set,
|
||||
+ .led_hw_is_supported = xway_gphy_led_hw_is_supported,
|
||||
+ .led_hw_control_get = xway_gphy_led_hw_control_get,
|
||||
+ .led_hw_control_set = xway_gphy_led_hw_control_set,
|
||||
+ .led_polarity_set = xway_gphy_led_polarity_set,
|
||||
}, {
|
||||
.phy_id = PHY_ID_PHY22F_VR9_1_2,
|
||||
.phy_id_mask = 0xffffffff,
|
||||
@@ -452,6 +682,11 @@ static struct phy_driver xway_gphy[] = {
|
||||
.config_intr = xway_gphy_config_intr,
|
||||
.suspend = genphy_suspend,
|
||||
.resume = genphy_resume,
|
||||
+ .led_brightness_set = xway_gphy_led_brightness_set,
|
||||
+ .led_hw_is_supported = xway_gphy_led_hw_is_supported,
|
||||
+ .led_hw_control_get = xway_gphy_led_hw_control_get,
|
||||
+ .led_hw_control_set = xway_gphy_led_hw_control_set,
|
||||
+ .led_polarity_set = xway_gphy_led_polarity_set,
|
||||
},
|
||||
};
|
||||
module_phy_driver(xway_gphy);
|
||||
@@ -31,45 +31,21 @@ Signed-off-by: David Bauer <mail@david-bauer.net>
|
||||
#include <linux/phy.h>
|
||||
#include <linux/polynomial.h>
|
||||
#include <linux/property.h>
|
||||
@@ -38,6 +39,7 @@
|
||||
#define PHY_MIISTAT 0x18 /* MII state */
|
||||
#define PHY_IMASK 0x19 /* interrupt mask */
|
||||
#define PHY_ISTAT 0x1A /* interrupt status */
|
||||
+#define PHY_LED 0x1B /* LED control */
|
||||
#define PHY_FWV 0x1E /* firmware version */
|
||||
|
||||
#define PHY_MIISTAT_SPD_MASK GENMASK(2, 0)
|
||||
@@ -61,10 +63,15 @@
|
||||
PHY_IMASK_ADSC | \
|
||||
PHY_IMASK_ANC)
|
||||
|
||||
+#define PHY_LED_NUM_LEDS 4
|
||||
+
|
||||
#define PHY_FWV_REL_MASK BIT(15)
|
||||
#define PHY_FWV_MAJOR_MASK GENMASK(11, 8)
|
||||
#define PHY_FWV_MINOR_MASK GENMASK(7, 0)
|
||||
|
||||
+/* LED */
|
||||
+#define VSPEC1_LED(x) (0x1 + x)
|
||||
+
|
||||
#define PHY_PMA_MGBT_POLARITY 0x82
|
||||
#define PHY_MDI_MDI_X_MASK GENMASK(1, 0)
|
||||
#define PHY_MDI_MDI_X_NORMAL 0x3
|
||||
@@ -270,10 +277,39 @@ out:
|
||||
@@ -293,10 +294,39 @@ out:
|
||||
return ret;
|
||||
}
|
||||
|
||||
+static int gpy_led_write(struct phy_device *phydev)
|
||||
+{
|
||||
+ struct device_node *node = phydev->mdio.dev.of_node;
|
||||
+ u32 led_regs[PHY_LED_NUM_LEDS];
|
||||
+ u32 led_regs[GPY_MAX_LEDS];
|
||||
+ int i, ret;
|
||||
+ u16 val = 0xff00;
|
||||
+
|
||||
+ if (!IS_ENABLED(CONFIG_OF_MDIO))
|
||||
+ return 0;
|
||||
+
|
||||
+ if (of_property_read_u32_array(node, "mxl,led-config", led_regs, PHY_LED_NUM_LEDS))
|
||||
+ if (of_property_read_u32_array(node, "mxl,led-config", led_regs, GPY_MAX_LEDS))
|
||||
+ return 0;
|
||||
+
|
||||
+ if (of_property_read_bool(node, "mxl,led-drive-vdd"))
|
||||
@@ -79,7 +55,7 @@ Signed-off-by: David Bauer <mail@david-bauer.net>
|
||||
+ phy_write(phydev, PHY_LED, val);
|
||||
+
|
||||
+ /* Write LED register values */
|
||||
+ for (i = 0; i < PHY_LED_NUM_LEDS; i++) {
|
||||
+ for (i = 0; i < GPY_MAX_LEDS; i++) {
|
||||
+ ret = phy_write_mmd(phydev, MDIO_MMD_VEND1, VSPEC1_LED(i), (u16)led_regs[i]);
|
||||
+ if (ret < 0)
|
||||
+ return ret;
|
||||
|
||||
+4
-4
@@ -14,7 +14,7 @@ Signed-off-by: Daniel Golle <daniel@makrotopia.org>
|
||||
|
||||
--- a/drivers/net/phy/mxl-gpy.c
|
||||
+++ b/drivers/net/phy/mxl-gpy.c
|
||||
@@ -385,8 +385,11 @@ static bool gpy_2500basex_chk(struct phy
|
||||
@@ -402,8 +402,11 @@ static bool gpy_2500basex_chk(struct phy
|
||||
|
||||
phydev->speed = SPEED_2500;
|
||||
phydev->interface = PHY_INTERFACE_MODE_2500BASEX;
|
||||
@@ -28,7 +28,7 @@ Signed-off-by: Daniel Golle <daniel@makrotopia.org>
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -437,6 +440,14 @@ static int gpy_config_aneg(struct phy_de
|
||||
@@ -454,6 +457,14 @@ static int gpy_config_aneg(struct phy_de
|
||||
u32 adv;
|
||||
int ret;
|
||||
|
||||
@@ -43,7 +43,7 @@ Signed-off-by: Daniel Golle <daniel@makrotopia.org>
|
||||
if (phydev->autoneg == AUTONEG_DISABLE) {
|
||||
/* Configure half duplex with genphy_setup_forced,
|
||||
* because genphy_c45_pma_setup_forced does not support.
|
||||
@@ -559,6 +570,8 @@ static int gpy_update_interface(struct p
|
||||
@@ -576,6 +587,8 @@ static int gpy_update_interface(struct p
|
||||
switch (phydev->speed) {
|
||||
case SPEED_2500:
|
||||
phydev->interface = PHY_INTERFACE_MODE_2500BASEX;
|
||||
@@ -52,7 +52,7 @@ Signed-off-by: Daniel Golle <daniel@makrotopia.org>
|
||||
ret = phy_modify_mmd(phydev, MDIO_MMD_VEND1, VSPEC1_SGMII_CTRL,
|
||||
VSPEC1_SGMII_CTRL_ANEN, 0);
|
||||
if (ret < 0) {
|
||||
@@ -572,7 +585,7 @@ static int gpy_update_interface(struct p
|
||||
@@ -589,7 +602,7 @@ static int gpy_update_interface(struct p
|
||||
case SPEED_100:
|
||||
case SPEED_10:
|
||||
phydev->interface = PHY_INTERFACE_MODE_SGMII;
|
||||
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
"github.com/metacubex/mihomo/component/resolver"
|
||||
tlsC "github.com/metacubex/mihomo/component/tls"
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
"github.com/metacubex/mihomo/log"
|
||||
"github.com/metacubex/mihomo/transport/gun"
|
||||
"github.com/metacubex/mihomo/transport/socks5"
|
||||
"github.com/metacubex/mihomo/transport/vless"
|
||||
@@ -513,8 +512,6 @@ func NewVless(option VlessOption) (*Vless, error) {
|
||||
if option.Flow != vless.XRV {
|
||||
return nil, fmt.Errorf("unsupported xtls flow type: %s", option.Flow)
|
||||
}
|
||||
|
||||
log.Warnln("To use %s, ensure your server is upgrade to Xray-core v1.8.0+", vless.XRV)
|
||||
addons = &vless.Addons{
|
||||
Flow: option.Flow,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package generater
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
|
||||
"github.com/gofrs/uuid/v5"
|
||||
)
|
||||
|
||||
func Main(args []string) {
|
||||
if len(args) < 1 {
|
||||
panic("Using: generate uuid/reality-keypair/wg-keypair")
|
||||
}
|
||||
switch args[0] {
|
||||
case "uuid":
|
||||
newUUID, err := uuid.NewV4()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println(newUUID.String())
|
||||
case "reality-keypair":
|
||||
privateKey, err := GeneratePrivateKey()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
publicKey := privateKey.PublicKey()
|
||||
fmt.Println("PrivateKey: " + base64.RawURLEncoding.EncodeToString(privateKey[:]))
|
||||
fmt.Println("PublicKey: " + base64.RawURLEncoding.EncodeToString(publicKey[:]))
|
||||
case "wg-keypair":
|
||||
privateKey, err := GeneratePrivateKey()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println("PrivateKey: " + privateKey.String())
|
||||
fmt.Println("PublicKey: " + privateKey.PublicKey().String())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// Copy from https://github.com/WireGuard/wgctrl-go/blob/a9ab2273dd1075ea74b88c76f8757f8b4003fcbf/wgtypes/types.go#L71-L155
|
||||
|
||||
package generater
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/crypto/curve25519"
|
||||
)
|
||||
|
||||
// KeyLen is the expected key length for a WireGuard key.
|
||||
const KeyLen = 32 // wgh.KeyLen
|
||||
|
||||
// A Key is a public, private, or pre-shared secret key. The Key constructor
|
||||
// functions in this package can be used to create Keys suitable for each of
|
||||
// these applications.
|
||||
type Key [KeyLen]byte
|
||||
|
||||
// GenerateKey generates a Key suitable for use as a pre-shared secret key from
|
||||
// a cryptographically safe source.
|
||||
//
|
||||
// The output Key should not be used as a private key; use GeneratePrivateKey
|
||||
// instead.
|
||||
func GenerateKey() (Key, error) {
|
||||
b := make([]byte, KeyLen)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return Key{}, fmt.Errorf("wgtypes: failed to read random bytes: %v", err)
|
||||
}
|
||||
|
||||
return NewKey(b)
|
||||
}
|
||||
|
||||
// GeneratePrivateKey generates a Key suitable for use as a private key from a
|
||||
// cryptographically safe source.
|
||||
func GeneratePrivateKey() (Key, error) {
|
||||
key, err := GenerateKey()
|
||||
if err != nil {
|
||||
return Key{}, err
|
||||
}
|
||||
|
||||
// Modify random bytes using algorithm described at:
|
||||
// https://cr.yp.to/ecdh.html.
|
||||
key[0] &= 248
|
||||
key[31] &= 127
|
||||
key[31] |= 64
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// NewKey creates a Key from an existing byte slice. The byte slice must be
|
||||
// exactly 32 bytes in length.
|
||||
func NewKey(b []byte) (Key, error) {
|
||||
if len(b) != KeyLen {
|
||||
return Key{}, fmt.Errorf("wgtypes: incorrect key size: %d", len(b))
|
||||
}
|
||||
|
||||
var k Key
|
||||
copy(k[:], b)
|
||||
|
||||
return k, nil
|
||||
}
|
||||
|
||||
// ParseKey parses a Key from a base64-encoded string, as produced by the
|
||||
// Key.String method.
|
||||
func ParseKey(s string) (Key, error) {
|
||||
b, err := base64.StdEncoding.DecodeString(s)
|
||||
if err != nil {
|
||||
return Key{}, fmt.Errorf("wgtypes: failed to parse base64-encoded key: %v", err)
|
||||
}
|
||||
|
||||
return NewKey(b)
|
||||
}
|
||||
|
||||
// PublicKey computes a public key from the private key k.
|
||||
//
|
||||
// PublicKey should only be called when k is a private key.
|
||||
func (k Key) PublicKey() Key {
|
||||
var (
|
||||
pub [KeyLen]byte
|
||||
priv = [KeyLen]byte(k)
|
||||
)
|
||||
|
||||
// ScalarBaseMult uses the correct base value per https://cr.yp.to/ecdh.html,
|
||||
// so no need to specify it.
|
||||
curve25519.ScalarBaseMult(&pub, &priv)
|
||||
|
||||
return Key(pub)
|
||||
}
|
||||
|
||||
// String returns the base64-encoded string representation of a Key.
|
||||
//
|
||||
// ParseKey can be used to produce a new Key from this string.
|
||||
func (k Key) String() string {
|
||||
return base64.StdEncoding.EncodeToString(k[:])
|
||||
}
|
||||
@@ -25,6 +25,7 @@ const (
|
||||
SOCKS5
|
||||
SHADOWSOCKS
|
||||
VMESS
|
||||
VLESS
|
||||
REDIR
|
||||
TPROXY
|
||||
TUNNEL
|
||||
@@ -69,6 +70,8 @@ func (t Type) String() string {
|
||||
return "ShadowSocks"
|
||||
case VMESS:
|
||||
return "Vmess"
|
||||
case VLESS:
|
||||
return "Vless"
|
||||
case REDIR:
|
||||
return "Redir"
|
||||
case TPROXY:
|
||||
@@ -103,6 +106,8 @@ func ParseType(t string) (*Type, error) {
|
||||
res = SHADOWSOCKS
|
||||
case "VMESS":
|
||||
res = VMESS
|
||||
case "VLESS":
|
||||
res = VLESS
|
||||
case "REDIR":
|
||||
res = REDIR
|
||||
case "TPROXY":
|
||||
|
||||
@@ -1176,6 +1176,30 @@ listeners:
|
||||
network: [tcp, udp]
|
||||
target: target.com
|
||||
|
||||
- name: vless-in-1
|
||||
type: vless
|
||||
port: 10817
|
||||
listen: 0.0.0.0
|
||||
# rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules
|
||||
# proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错)
|
||||
users:
|
||||
- username: 1
|
||||
uuid: 9d0cb9d0-964f-4ef6-897d-6c6b3ccf9e68
|
||||
flow: xtls-rprx-vision
|
||||
# ws-path: "/" # 如果不为空则开启 websocket 传输层
|
||||
# 下面两项如果填写则开启 tls(需要同时填写)
|
||||
# certificate: ./server.crt
|
||||
# private-key: ./server.key
|
||||
# 如果填写reality-config则开启reality(注意不可与certificate和private-key同时填写)
|
||||
reality-config:
|
||||
dest: test.com:443
|
||||
private-key: jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0 # 可由 mihomo generate reality-keypair 命令生成
|
||||
short-id:
|
||||
- 0123456789abcdef
|
||||
server-names:
|
||||
- test.com
|
||||
### 注意,对于vless listener, 至少需要填写 “certificate和private-key” 或 “reality-config” 的其中一项 ###
|
||||
|
||||
- name: tun-in-1
|
||||
type: tun
|
||||
# rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules
|
||||
|
||||
+2
-1
@@ -27,7 +27,7 @@ require (
|
||||
github.com/metacubex/sing-shadowsocks v0.2.8
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.2
|
||||
github.com/metacubex/sing-tun v0.4.5
|
||||
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9
|
||||
github.com/metacubex/sing-vmess v0.1.14-0.20250203033000-f61322b3dbe3
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589
|
||||
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422
|
||||
github.com/metacubex/utls v1.6.6
|
||||
@@ -40,6 +40,7 @@ require (
|
||||
github.com/sagernet/cors v1.2.1
|
||||
github.com/sagernet/fswatch v0.1.1
|
||||
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a
|
||||
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691
|
||||
github.com/sagernet/sing v0.5.1
|
||||
github.com/sagernet/sing-mux v0.2.1
|
||||
github.com/sagernet/sing-shadowtls v0.1.5
|
||||
|
||||
+4
-2
@@ -122,8 +122,8 @@ github.com/metacubex/sing-shadowsocks2 v0.2.2 h1:eaf42uVx4Lr21S6MDYs0ZdTvGA0GEhD
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.2/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q=
|
||||
github.com/metacubex/sing-tun v0.4.5 h1:kWSyQzuzHI40r50OFBczfWIDvMBMy1RIk+JsXeBPRB0=
|
||||
github.com/metacubex/sing-tun v0.4.5/go.mod h1:V0N4rr0dWPBEE20ESkTXdbtx2riQYcb6YtwC5w/9wl0=
|
||||
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 h1:OAXiCosqY8xKDp3pqTW3qbrCprZ1l6WkrXSFSCwyY4I=
|
||||
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9/go.mod h1:olVkD4FChQ5gKMHG4ZzuD7+fMkJY1G8vwOKpRehjrmY=
|
||||
github.com/metacubex/sing-vmess v0.1.14-0.20250203033000-f61322b3dbe3 h1:2kq6azIvsTjTnyw66xXDl5zMzIJqF7GTbvLpkroHssg=
|
||||
github.com/metacubex/sing-vmess v0.1.14-0.20250203033000-f61322b3dbe3/go.mod h1:nE7Mdzj/QUDwgRi/8BASPtsxtIFZTHA4Yst5GgwbGCQ=
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 h1:Z6bNy0HLTjx6BKIkV48sV/yia/GP8Bnyb5JQuGgSGzg=
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589/go.mod h1:4NclTLIZuk+QkHVCGrP87rHi/y8YjgPytxTgApJNMhc=
|
||||
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 h1:zGeQt3UyNydIVrMRB97AA5WsYEau/TyCnRtTf1yUmJY=
|
||||
@@ -170,6 +170,8 @@ github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZN
|
||||
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
|
||||
github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I=
|
||||
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
|
||||
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc=
|
||||
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU=
|
||||
github.com/sagernet/sing-mux v0.2.1 h1:N/3MHymfnFZRd29tE3TaXwPUVVgKvxhtOkiCMLp9HVo=
|
||||
github.com/sagernet/sing-mux v0.2.1/go.mod h1:dm3BWL6NvES9pbib7llpylrq7Gq+LjlzG+0RacdxcyE=
|
||||
github.com/sagernet/sing-shadowtls v0.1.5 h1:uXxmq/HXh8DIiBGLzpMjCbWnzIAFs+lIxiTOjdgG5qo=
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/metacubex/mihomo/listener/sing"
|
||||
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
type VlessUser struct {
|
||||
Username string
|
||||
UUID string
|
||||
Flow string
|
||||
}
|
||||
|
||||
type VlessServer struct {
|
||||
Enable bool
|
||||
Listen string
|
||||
Users []VlessUser
|
||||
WsPath string
|
||||
Certificate string
|
||||
PrivateKey string
|
||||
RealityConfig RealityConfig
|
||||
MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"`
|
||||
}
|
||||
|
||||
func (t VlessServer) String() string {
|
||||
b, _ := json.Marshal(t)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
type RealityConfig struct {
|
||||
Dest string
|
||||
PrivateKey string
|
||||
ShortID []string
|
||||
ServerNames []string
|
||||
MaxTimeDifference int
|
||||
Proxy string
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package inbound
|
||||
|
||||
import (
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
LC "github.com/metacubex/mihomo/listener/config"
|
||||
"github.com/metacubex/mihomo/listener/sing_vless"
|
||||
"github.com/metacubex/mihomo/log"
|
||||
)
|
||||
|
||||
type VlessOption struct {
|
||||
BaseOption
|
||||
Users []VlessUser `inbound:"users"`
|
||||
WsPath string `inbound:"ws-path,omitempty"`
|
||||
Certificate string `inbound:"certificate,omitempty"`
|
||||
PrivateKey string `inbound:"private-key,omitempty"`
|
||||
RealityConfig RealityConfig `inbound:"reality-config,omitempty"`
|
||||
MuxOption MuxOption `inbound:"mux-option,omitempty"`
|
||||
}
|
||||
|
||||
type VlessUser struct {
|
||||
Username string `inbound:"username,omitempty"`
|
||||
UUID string `inbound:"uuid"`
|
||||
Flow string `inbound:"flow,omitempty"`
|
||||
}
|
||||
|
||||
type RealityConfig struct {
|
||||
Dest string `inbound:"dest"`
|
||||
PrivateKey string `inbound:"private-key"`
|
||||
ShortID []string `inbound:"short-id"`
|
||||
ServerNames []string `inbound:"server-names"`
|
||||
MaxTimeDifference int `inbound:"max-time-difference,omitempty"`
|
||||
Proxy string `inbound:"proxy,omitempty"`
|
||||
}
|
||||
|
||||
func (c RealityConfig) Build() LC.RealityConfig {
|
||||
return LC.RealityConfig{
|
||||
Dest: c.Dest,
|
||||
PrivateKey: c.PrivateKey,
|
||||
ShortID: c.ShortID,
|
||||
ServerNames: c.ServerNames,
|
||||
MaxTimeDifference: c.MaxTimeDifference,
|
||||
Proxy: c.Proxy,
|
||||
}
|
||||
}
|
||||
|
||||
func (o VlessOption) Equal(config C.InboundConfig) bool {
|
||||
return optionToString(o) == optionToString(config)
|
||||
}
|
||||
|
||||
type Vless struct {
|
||||
*Base
|
||||
config *VlessOption
|
||||
l C.MultiAddrListener
|
||||
vs LC.VlessServer
|
||||
}
|
||||
|
||||
func NewVless(options *VlessOption) (*Vless, error) {
|
||||
base, err := NewBase(&options.BaseOption)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
users := make([]LC.VlessUser, len(options.Users))
|
||||
for i, v := range options.Users {
|
||||
users[i] = LC.VlessUser{
|
||||
Username: v.Username,
|
||||
UUID: v.UUID,
|
||||
Flow: v.Flow,
|
||||
}
|
||||
}
|
||||
return &Vless{
|
||||
Base: base,
|
||||
config: options,
|
||||
vs: LC.VlessServer{
|
||||
Enable: true,
|
||||
Listen: base.RawAddress(),
|
||||
Users: users,
|
||||
WsPath: options.WsPath,
|
||||
Certificate: options.Certificate,
|
||||
PrivateKey: options.PrivateKey,
|
||||
RealityConfig: options.RealityConfig.Build(),
|
||||
MuxOption: options.MuxOption.Build(),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Config implements constant.InboundListener
|
||||
func (v *Vless) Config() C.InboundConfig {
|
||||
return v.config
|
||||
}
|
||||
|
||||
// Address implements constant.InboundListener
|
||||
func (v *Vless) Address() string {
|
||||
if v.l != nil {
|
||||
for _, addr := range v.l.AddrList() {
|
||||
return addr.String()
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Listen implements constant.InboundListener
|
||||
func (v *Vless) Listen(tunnel C.Tunnel) error {
|
||||
var err error
|
||||
users := make([]LC.VlessUser, len(v.config.Users))
|
||||
for i, v := range v.config.Users {
|
||||
users[i] = LC.VlessUser{
|
||||
Username: v.Username,
|
||||
UUID: v.UUID,
|
||||
Flow: v.Flow,
|
||||
}
|
||||
}
|
||||
v.l, err = sing_vless.New(v.vs, tunnel, v.Additions()...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infoln("Vless[%s] proxy listening at: %s", v.Name(), v.Address())
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close implements constant.InboundListener
|
||||
func (v *Vless) Close() error {
|
||||
return v.l.Close()
|
||||
}
|
||||
|
||||
var _ C.InboundListener = (*Vless)(nil)
|
||||
@@ -86,6 +86,13 @@ func ParseListener(mapping map[string]any) (C.InboundListener, error) {
|
||||
return nil, err
|
||||
}
|
||||
listener, err = IN.NewVmess(vmessOption)
|
||||
case "vless":
|
||||
vlessOption := &IN.VlessOption{}
|
||||
err = decoder.Decode(mapping, vlessOption)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
listener, err = IN.NewVless(vlessOption)
|
||||
case "hysteria2":
|
||||
hysteria2Option := &IN.Hysteria2Option{}
|
||||
err = decoder.Decode(mapping, hysteria2Option)
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
package sing_vless
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/metacubex/mihomo/adapter/inbound"
|
||||
N "github.com/metacubex/mihomo/common/net"
|
||||
tlsC "github.com/metacubex/mihomo/component/tls"
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
LC "github.com/metacubex/mihomo/listener/config"
|
||||
"github.com/metacubex/mihomo/listener/inner"
|
||||
"github.com/metacubex/mihomo/listener/sing"
|
||||
"github.com/metacubex/mihomo/log"
|
||||
"github.com/metacubex/mihomo/ntp"
|
||||
mihomoVMess "github.com/metacubex/mihomo/transport/vmess"
|
||||
|
||||
"github.com/metacubex/sing-vmess/vless"
|
||||
utls "github.com/metacubex/utls"
|
||||
"github.com/sagernet/reality"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/metadata"
|
||||
)
|
||||
|
||||
func init() {
|
||||
vless.RegisterTLS(func(conn net.Conn) (loaded bool, netConn net.Conn, reflectType reflect.Type, reflectPointer unsafe.Pointer) {
|
||||
tlsConn, loaded := common.Cast[*reality.Conn](conn)
|
||||
if !loaded {
|
||||
return
|
||||
}
|
||||
return true, tlsConn.NetConn(), reflect.TypeOf(tlsConn).Elem(), unsafe.Pointer(tlsConn)
|
||||
})
|
||||
|
||||
vless.RegisterTLS(func(conn net.Conn) (loaded bool, netConn net.Conn, reflectType reflect.Type, reflectPointer unsafe.Pointer) {
|
||||
tlsConn, loaded := common.Cast[*utls.UConn](conn)
|
||||
if !loaded {
|
||||
return
|
||||
}
|
||||
return true, tlsConn.NetConn(), reflect.TypeOf(tlsConn.Conn).Elem(), unsafe.Pointer(tlsConn.Conn)
|
||||
})
|
||||
|
||||
vless.RegisterTLS(func(conn net.Conn) (loaded bool, netConn net.Conn, reflectType reflect.Type, reflectPointer unsafe.Pointer) {
|
||||
tlsConn, loaded := common.Cast[*tlsC.UConn](conn)
|
||||
if !loaded {
|
||||
return
|
||||
}
|
||||
return true, tlsConn.NetConn(), reflect.TypeOf(tlsConn.Conn).Elem(), unsafe.Pointer(tlsConn.Conn)
|
||||
})
|
||||
}
|
||||
|
||||
type Listener struct {
|
||||
closed bool
|
||||
config LC.VlessServer
|
||||
listeners []net.Listener
|
||||
service *vless.Service[string]
|
||||
}
|
||||
|
||||
func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition) (sl *Listener, err error) {
|
||||
if len(additions) == 0 {
|
||||
additions = []inbound.Addition{
|
||||
inbound.WithInName("DEFAULT-VLESS"),
|
||||
inbound.WithSpecialRules(""),
|
||||
}
|
||||
}
|
||||
h, err := sing.NewListenerHandler(sing.ListenerConfig{
|
||||
Tunnel: tunnel,
|
||||
Type: C.VLESS,
|
||||
Additions: additions,
|
||||
MuxOption: config.MuxOption,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
service := vless.NewService[string](log.SingLogger, h)
|
||||
service.UpdateUsers(
|
||||
common.Map(config.Users, func(it LC.VlessUser) string {
|
||||
return it.Username
|
||||
}),
|
||||
common.Map(config.Users, func(it LC.VlessUser) string {
|
||||
return it.UUID
|
||||
}),
|
||||
common.Map(config.Users, func(it LC.VlessUser) string {
|
||||
return it.Flow
|
||||
}))
|
||||
|
||||
sl = &Listener{false, config, nil, service}
|
||||
|
||||
tlsConfig := &tls.Config{}
|
||||
var realityConfig *reality.Config
|
||||
var httpMux *http.ServeMux
|
||||
|
||||
if config.Certificate != "" && config.PrivateKey != "" {
|
||||
cert, err := N.ParseCert(config.Certificate, config.PrivateKey, C.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsConfig.Certificates = []tls.Certificate{cert}
|
||||
}
|
||||
if config.WsPath != "" {
|
||||
httpMux = http.NewServeMux()
|
||||
httpMux.HandleFunc(config.WsPath, func(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := mihomoVMess.StreamUpgradedWebsocketConn(w, r)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
sl.HandleConn(conn, tunnel)
|
||||
})
|
||||
tlsConfig.NextProtos = append(tlsConfig.NextProtos, "http/1.1")
|
||||
}
|
||||
if config.RealityConfig.PrivateKey != "" {
|
||||
if tlsConfig.Certificates != nil {
|
||||
return nil, errors.New("certificate is unavailable in reality")
|
||||
}
|
||||
realityConfig = &reality.Config{}
|
||||
realityConfig.SessionTicketsDisabled = true
|
||||
realityConfig.Type = "tcp"
|
||||
realityConfig.Dest = config.RealityConfig.Dest
|
||||
realityConfig.Time = ntp.Now
|
||||
realityConfig.ServerNames = make(map[string]bool)
|
||||
for _, it := range config.RealityConfig.ServerNames {
|
||||
realityConfig.ServerNames[it] = true
|
||||
}
|
||||
privateKey, err := base64.RawURLEncoding.DecodeString(config.RealityConfig.PrivateKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decode private key: %w", err)
|
||||
}
|
||||
if len(privateKey) != 32 {
|
||||
return nil, errors.New("invalid private key")
|
||||
}
|
||||
realityConfig.PrivateKey = privateKey
|
||||
|
||||
realityConfig.MaxTimeDiff = time.Duration(config.RealityConfig.MaxTimeDifference) * time.Microsecond
|
||||
|
||||
realityConfig.ShortIds = make(map[[8]byte]bool)
|
||||
for i, shortIDString := range config.RealityConfig.ShortID {
|
||||
var shortID [8]byte
|
||||
decodedLen, err := hex.Decode(shortID[:], []byte(shortIDString))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decode short_id[%d] '%s': %w", i, shortIDString, err)
|
||||
}
|
||||
if decodedLen > 8 {
|
||||
return nil, fmt.Errorf("invalid short_id[%d]: %s", i, shortIDString)
|
||||
}
|
||||
realityConfig.ShortIds[shortID] = true
|
||||
}
|
||||
|
||||
realityConfig.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
return inner.HandleTcp(address, config.RealityConfig.Proxy)
|
||||
}
|
||||
}
|
||||
|
||||
for _, addr := range strings.Split(config.Listen, ",") {
|
||||
addr := addr
|
||||
|
||||
//TCP
|
||||
l, err := inbound.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if realityConfig != nil {
|
||||
l = reality.NewListener(l, realityConfig)
|
||||
// Due to low implementation quality, the reality server intercepted half close and caused memory leaks.
|
||||
// We fixed it by calling Close() directly.
|
||||
l = realityListenerWrapper{l}
|
||||
} else if len(tlsConfig.Certificates) > 0 {
|
||||
l = tls.NewListener(l, tlsConfig)
|
||||
} else {
|
||||
return nil, errors.New("disallow using Vless without both certificates/reality config")
|
||||
}
|
||||
sl.listeners = append(sl.listeners, l)
|
||||
|
||||
go func() {
|
||||
if httpMux != nil {
|
||||
_ = http.Serve(l, httpMux)
|
||||
return
|
||||
}
|
||||
for {
|
||||
c, err := l.Accept()
|
||||
if err != nil {
|
||||
if sl.closed {
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
go sl.HandleConn(c, tunnel)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return sl, nil
|
||||
}
|
||||
|
||||
func (l *Listener) Close() error {
|
||||
l.closed = true
|
||||
var retErr error
|
||||
for _, lis := range l.listeners {
|
||||
err := lis.Close()
|
||||
if err != nil {
|
||||
retErr = err
|
||||
}
|
||||
}
|
||||
return retErr
|
||||
}
|
||||
|
||||
func (l *Listener) Config() string {
|
||||
return l.config.String()
|
||||
}
|
||||
|
||||
func (l *Listener) AddrList() (addrList []net.Addr) {
|
||||
for _, lis := range l.listeners {
|
||||
addrList = append(addrList, lis.Addr())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (l *Listener) HandleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) {
|
||||
ctx := sing.WithAdditions(context.TODO(), additions...)
|
||||
err := l.service.NewConnection(ctx, conn, metadata.Metadata{
|
||||
Protocol: "vless",
|
||||
Source: metadata.ParseSocksaddr(conn.RemoteAddr().String()),
|
||||
})
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type realityConnWrapper struct {
|
||||
*reality.Conn
|
||||
}
|
||||
|
||||
func (c realityConnWrapper) Upstream() any {
|
||||
return c.Conn
|
||||
}
|
||||
|
||||
func (c realityConnWrapper) CloseWrite() error {
|
||||
return c.Close()
|
||||
}
|
||||
|
||||
type realityListenerWrapper struct {
|
||||
net.Listener
|
||||
}
|
||||
|
||||
func (l realityListenerWrapper) Accept() (net.Conn, error) {
|
||||
c, err := l.Listener.Accept()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return realityConnWrapper{c.(*reality.Conn)}, nil
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/metacubex/mihomo/component/generater"
|
||||
"github.com/metacubex/mihomo/component/geodata"
|
||||
"github.com/metacubex/mihomo/component/updater"
|
||||
"github.com/metacubex/mihomo/config"
|
||||
@@ -71,6 +72,11 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
if len(os.Args) > 1 && os.Args[1] == "generate" {
|
||||
generater.Main(os.Args[2:])
|
||||
return
|
||||
}
|
||||
|
||||
if version {
|
||||
fmt.Printf("Mihomo Meta %s %s %s with %s %s\n",
|
||||
C.Version, runtime.GOOS, runtime.GOARCH, runtime.Version(), C.BuildTime)
|
||||
|
||||
@@ -7,6 +7,9 @@ local appname = "passwall"
|
||||
local fs = api.fs
|
||||
local split = api.split
|
||||
|
||||
local local_version = api.get_app_version("singbox")
|
||||
local version_ge_1_11_0 = api.compare_versions(local_version:match("[^v]+"), ">=", "1.11.0")
|
||||
|
||||
local new_port
|
||||
|
||||
local function get_new_port()
|
||||
@@ -729,6 +732,26 @@ function gen_config_server(node)
|
||||
end
|
||||
end
|
||||
|
||||
if version_ge_1_11_0 then
|
||||
-- Migrate logics
|
||||
-- https://sing-box.sagernet.org/migration/
|
||||
for i = #config.outbounds, 1, -1 do
|
||||
local value = config.outbounds[i]
|
||||
if value.type == "block" then
|
||||
-- https://sing-box.sagernet.org/migration/#migrate-legacy-special-outbounds-to-rule-actions
|
||||
table.remove(config.outbounds, i)
|
||||
end
|
||||
end
|
||||
-- https://sing-box.sagernet.org/migration/#migrate-legacy-special-outbounds-to-rule-actions
|
||||
for i = #config.route.rules, 1, -1 do
|
||||
local value = config.route.rules[i]
|
||||
if value.outbound == "block" then
|
||||
value.action = "reject"
|
||||
value.outbound = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return config
|
||||
end
|
||||
|
||||
@@ -1098,7 +1121,6 @@ function gen_config(var)
|
||||
local rule = {
|
||||
inbound = inboundTag,
|
||||
outbound = outboundTag,
|
||||
invert = false, --匹配反选
|
||||
protocol = protocols
|
||||
}
|
||||
|
||||
@@ -1487,6 +1509,90 @@ function gen_config(var)
|
||||
end
|
||||
end
|
||||
end
|
||||
if version_ge_1_11_0 then
|
||||
-- Migrate logics
|
||||
-- https://sing-box.sagernet.org/migration/
|
||||
local endpoints = {}
|
||||
for i = #config.outbounds, 1, -1 do
|
||||
local value = config.outbounds[i]
|
||||
if value.type == "wireguard" then
|
||||
-- https://sing-box.sagernet.org/migration/#migrate-wireguard-outbound-to-endpoint
|
||||
local endpoint = {
|
||||
type = "wireguard",
|
||||
tag = value.tag,
|
||||
system = value.system_interface,
|
||||
name = value.interface_name,
|
||||
mtu = value.mtu,
|
||||
address = value.local_address,
|
||||
private_key = value.private_key,
|
||||
peers = {
|
||||
{
|
||||
address = value.server,
|
||||
port = value.server_port,
|
||||
public_key = value.peer_public_key,
|
||||
pre_shared_key = value.pre_shared_key,
|
||||
allowed_ips = {"0.0.0.0/0"},
|
||||
reserved = value.reserved
|
||||
}
|
||||
},
|
||||
domain_strategy = value.domain_strategy,
|
||||
detour = value.detour
|
||||
}
|
||||
endpoints[#endpoints + 1] = endpoint
|
||||
table.remove(config.outbounds, i)
|
||||
end
|
||||
if value.type == "block" or value.type == "dns" then
|
||||
-- https://sing-box.sagernet.org/migration/#migrate-legacy-special-outbounds-to-rule-actions
|
||||
table.remove(config.outbounds, i)
|
||||
end
|
||||
end
|
||||
if #endpoints > 0 then
|
||||
config.endpoints = endpoints
|
||||
end
|
||||
|
||||
-- https://sing-box.sagernet.org/migration/#migrate-legacy-special-outbounds-to-rule-actions
|
||||
for i = #config.route.rules, 1, -1 do
|
||||
local value = config.route.rules[i]
|
||||
if value.outbound == "block" then
|
||||
value.action = "reject"
|
||||
value.outbound = nil
|
||||
elseif value.outbound == "dns-out" then
|
||||
value.action = "hijack-dns"
|
||||
value.outbound = nil
|
||||
else
|
||||
value.action = "route"
|
||||
end
|
||||
end
|
||||
|
||||
-- https://sing-box.sagernet.org/migration/#migrate-legacy-inbound-fields-to-rule-actions
|
||||
for i = #config.inbounds, 1, -1 do
|
||||
local value = config.inbounds[i]
|
||||
if value.sniff == true then
|
||||
table.insert(config.route.rules, 1, {
|
||||
inbound = value.tag,
|
||||
action = "sniff"
|
||||
})
|
||||
value.sniff = nil
|
||||
value.sniff_override_destination = nil
|
||||
end
|
||||
if value.domain_strategy then
|
||||
table.insert(config.route.rules, 1, {
|
||||
inbound = value.tag,
|
||||
action = "resolve",
|
||||
strategy = value.domain_strategy,
|
||||
--server = ""
|
||||
})
|
||||
value.domain_strategy = nil
|
||||
end
|
||||
end
|
||||
|
||||
if config.route.final == "block" then
|
||||
config.route.final = nil
|
||||
table.insert(config.route.rules, {
|
||||
action = "reject"
|
||||
})
|
||||
end
|
||||
end
|
||||
return jsonc.stringify(config, 1)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2026,6 +2026,8 @@ start() {
|
||||
get_config
|
||||
export V2RAY_LOCATION_ASSET=$(config_t_get global_rules v2ray_location_asset "/usr/share/v2ray/")
|
||||
export XRAY_LOCATION_ASSET=$V2RAY_LOCATION_ASSET
|
||||
export ENABLE_DEPRECATED_GEOSITE=true
|
||||
export ENABLE_DEPRECATED_GEOIP=true
|
||||
ulimit -n 65535
|
||||
start_haproxy
|
||||
start_socks
|
||||
|
||||
@@ -85,10 +85,10 @@ local function is_filter_keyword(value)
|
||||
end
|
||||
|
||||
local nodeResult = {} -- update result
|
||||
local debug = false
|
||||
local isDebug = false
|
||||
|
||||
local log = function(...)
|
||||
if debug == true then
|
||||
if isDebug == true then
|
||||
local result = os.date("%Y-%m-%d %H:%M:%S: ") .. table.concat({...}, " ")
|
||||
print(result)
|
||||
else
|
||||
@@ -1728,7 +1728,9 @@ if arg[1] then
|
||||
log('开始订阅...')
|
||||
xpcall(execute, function(e)
|
||||
log(e)
|
||||
log(debug.traceback())
|
||||
if type(debug) == "table" and type(debug.traceback) == "function" then
|
||||
log(debug.traceback())
|
||||
end
|
||||
log('发生错误, 正在恢复服务')
|
||||
end)
|
||||
log('订阅完毕...')
|
||||
|
||||
@@ -8,6 +8,9 @@ local fs = api.fs
|
||||
local CACHE_PATH = api.CACHE_PATH
|
||||
local split = api.split
|
||||
|
||||
local local_version = api.get_app_version("singbox")
|
||||
local version_ge_1_11_0 = api.compare_versions(local_version:match("[^v]+"), ">=", "1.11.0")
|
||||
|
||||
local new_port
|
||||
|
||||
local function get_new_port()
|
||||
@@ -726,6 +729,26 @@ function gen_config_server(node)
|
||||
end
|
||||
end
|
||||
|
||||
if version_ge_1_11_0 then
|
||||
-- Migrate logics
|
||||
-- https://sing-box.sagernet.org/migration/
|
||||
for i = #config.outbounds, 1, -1 do
|
||||
local value = config.outbounds[i]
|
||||
if value.type == "block" then
|
||||
-- https://sing-box.sagernet.org/migration/#migrate-legacy-special-outbounds-to-rule-actions
|
||||
table.remove(config.outbounds, i)
|
||||
end
|
||||
end
|
||||
-- https://sing-box.sagernet.org/migration/#migrate-legacy-special-outbounds-to-rule-actions
|
||||
for i = #config.route.rules, 1, -1 do
|
||||
local value = config.route.rules[i]
|
||||
if value.outbound == "block" then
|
||||
value.action = "reject"
|
||||
value.outbound = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return config
|
||||
end
|
||||
|
||||
@@ -1087,7 +1110,6 @@ function gen_config(var)
|
||||
local rule = {
|
||||
inbound = inboundTag,
|
||||
outbound = outboundTag,
|
||||
invert = false, --匹配反选
|
||||
protocol = protocols
|
||||
}
|
||||
|
||||
@@ -1480,6 +1502,90 @@ function gen_config(var)
|
||||
end
|
||||
end
|
||||
end
|
||||
if version_ge_1_11_0 then
|
||||
-- Migrate logics
|
||||
-- https://sing-box.sagernet.org/migration/
|
||||
local endpoints = {}
|
||||
for i = #config.outbounds, 1, -1 do
|
||||
local value = config.outbounds[i]
|
||||
if value.type == "wireguard" then
|
||||
-- https://sing-box.sagernet.org/migration/#migrate-wireguard-outbound-to-endpoint
|
||||
local endpoint = {
|
||||
type = "wireguard",
|
||||
tag = value.tag,
|
||||
system = value.system_interface,
|
||||
name = value.interface_name,
|
||||
mtu = value.mtu,
|
||||
address = value.local_address,
|
||||
private_key = value.private_key,
|
||||
peers = {
|
||||
{
|
||||
address = value.server,
|
||||
port = value.server_port,
|
||||
public_key = value.peer_public_key,
|
||||
pre_shared_key = value.pre_shared_key,
|
||||
allowed_ips = {"0.0.0.0/0"},
|
||||
reserved = value.reserved
|
||||
}
|
||||
},
|
||||
domain_strategy = value.domain_strategy,
|
||||
detour = value.detour
|
||||
}
|
||||
endpoints[#endpoints + 1] = endpoint
|
||||
table.remove(config.outbounds, i)
|
||||
end
|
||||
if value.type == "block" or value.type == "dns" then
|
||||
-- https://sing-box.sagernet.org/migration/#migrate-legacy-special-outbounds-to-rule-actions
|
||||
table.remove(config.outbounds, i)
|
||||
end
|
||||
end
|
||||
if #endpoints > 0 then
|
||||
config.endpoints = endpoints
|
||||
end
|
||||
|
||||
-- https://sing-box.sagernet.org/migration/#migrate-legacy-special-outbounds-to-rule-actions
|
||||
for i = #config.route.rules, 1, -1 do
|
||||
local value = config.route.rules[i]
|
||||
if value.outbound == "block" then
|
||||
value.action = "reject"
|
||||
value.outbound = nil
|
||||
elseif value.outbound == "dns-out" then
|
||||
value.action = "hijack-dns"
|
||||
value.outbound = nil
|
||||
else
|
||||
value.action = "route"
|
||||
end
|
||||
end
|
||||
|
||||
-- https://sing-box.sagernet.org/migration/#migrate-legacy-inbound-fields-to-rule-actions
|
||||
for i = #config.inbounds, 1, -1 do
|
||||
local value = config.inbounds[i]
|
||||
if value.sniff == true then
|
||||
table.insert(config.route.rules, 1, {
|
||||
inbound = value.tag,
|
||||
action = "sniff"
|
||||
})
|
||||
value.sniff = nil
|
||||
value.sniff_override_destination = nil
|
||||
end
|
||||
if value.domain_strategy then
|
||||
table.insert(config.route.rules, 1, {
|
||||
inbound = value.tag,
|
||||
action = "resolve",
|
||||
strategy = value.domain_strategy,
|
||||
--server = ""
|
||||
})
|
||||
value.domain_strategy = nil
|
||||
end
|
||||
end
|
||||
|
||||
if config.route.final == "block" then
|
||||
config.route.final = nil
|
||||
table.insert(config.route.rules, {
|
||||
action = "reject"
|
||||
})
|
||||
end
|
||||
end
|
||||
return jsonc.stringify(config, 1)
|
||||
end
|
||||
end
|
||||
@@ -1563,183 +1669,8 @@ function gen_proto_config(var)
|
||||
return jsonc.stringify(config, 1)
|
||||
end
|
||||
|
||||
function gen_dns_config(var)
|
||||
local dns_listen_port = var["-dns_listen_port"]
|
||||
local dns_query_strategy = var["-dns_query_strategy"]
|
||||
local dns_out_tag = var["-dns_out_tag"]
|
||||
local direct_dns_udp_server = var["-direct_dns_udp_server"]
|
||||
local direct_dns_udp_port = var["-direct_dns_udp_port"]
|
||||
local direct_dns_tcp_server = var["-direct_dns_tcp_server"]
|
||||
local direct_dns_tcp_port = var["-direct_dns_tcp_port"]
|
||||
local direct_dns_doh_url = var["-direct_dns_doh_url"]
|
||||
local direct_dns_doh_host = var["-direct_dns_doh_host"]
|
||||
local direct_dns_doh_ip = var["-direct_dns_doh_ip"]
|
||||
local direct_dns_doh_port = var["-direct_dns_doh_port"]
|
||||
local remote_dns_udp_server = var["-remote_dns_udp_server"]
|
||||
local remote_dns_udp_port = var["-remote_dns_udp_port"]
|
||||
local remote_dns_tcp_server = var["-remote_dns_tcp_server"]
|
||||
local remote_dns_tcp_port = var["-remote_dns_tcp_port"]
|
||||
local remote_dns_doh_url = var["-remote_dns_doh_url"]
|
||||
local remote_dns_doh_host = var["-remote_dns_doh_host"]
|
||||
local remote_dns_doh_ip = var["-remote_dns_doh_ip"]
|
||||
local remote_dns_doh_port = var["-remote_dns_doh_port"]
|
||||
local remote_dns_detour = var["-remote_dns_detour"]
|
||||
local remote_dns_client_ip = var["-remote_dns_client_ip"]
|
||||
local remote_dns_outbound_socks_address = var["-remote_dns_outbound_socks_address"]
|
||||
local remote_dns_outbound_socks_port = var["-remote_dns_outbound_socks_port"]
|
||||
local dns_cache = var["-dns_cache"]
|
||||
local log = var["-log"] or "0"
|
||||
local loglevel = var["-loglevel"] or "warn"
|
||||
local logfile = var["-logfile"] or "/dev/null"
|
||||
|
||||
local inbounds = {}
|
||||
local outbounds = {}
|
||||
local dns = nil
|
||||
local route = nil
|
||||
|
||||
if dns_listen_port then
|
||||
route = {
|
||||
rules = {}
|
||||
}
|
||||
|
||||
dns = {
|
||||
servers = {},
|
||||
rules = {},
|
||||
disable_cache = (dns_cache and dns_cache == "0") and true or false,
|
||||
disable_expire = false, --禁用 DNS 缓存过期。
|
||||
independent_cache = false, --使每个 DNS 服务器的缓存独立,以满足特殊目的。如果启用,将轻微降低性能。
|
||||
reverse_mapping = true, --在响应 DNS 查询后存储 IP 地址的反向映射以为路由目的提供域名。
|
||||
}
|
||||
|
||||
if dns_out_tag == "remote" then
|
||||
local out_tag = nil
|
||||
if remote_dns_detour == "direct" then
|
||||
out_tag = "direct-out"
|
||||
table.insert(outbounds, 1, {
|
||||
type = "direct",
|
||||
tag = out_tag,
|
||||
routing_mark = 255,
|
||||
domain_strategy = (dns_query_strategy and dns_query_strategy ~= "UseIP") and "ipv4_only" or "prefer_ipv6",
|
||||
})
|
||||
else
|
||||
if remote_dns_outbound_socks_address and remote_dns_outbound_socks_port then
|
||||
out_tag = "remote-out"
|
||||
table.insert(outbounds, 1, {
|
||||
type = "socks",
|
||||
tag = out_tag,
|
||||
server = remote_dns_outbound_socks_address,
|
||||
server_port = tonumber(remote_dns_outbound_socks_port),
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
local server = {
|
||||
tag = dns_out_tag,
|
||||
address_strategy = "prefer_ipv4",
|
||||
strategy = (dns_query_strategy and dns_query_strategy ~= "UseIP") and "ipv4_only" or "prefer_ipv6",
|
||||
detour = out_tag,
|
||||
}
|
||||
|
||||
if remote_dns_udp_server then
|
||||
local server_port = tonumber(remote_dns_udp_port) or 53
|
||||
server.address = "udp://" .. remote_dns_udp_server .. ":" .. server_port
|
||||
end
|
||||
|
||||
if remote_dns_tcp_server then
|
||||
local server_port = tonumber(remote_dns_tcp_port) or 53
|
||||
server.address = "tcp://" .. remote_dns_tcp_server .. ":" .. server_port
|
||||
end
|
||||
|
||||
if remote_dns_doh_url then
|
||||
server.address = remote_dns_doh_url
|
||||
end
|
||||
|
||||
table.insert(dns.servers, server)
|
||||
|
||||
route.final = out_tag
|
||||
elseif dns_out_tag == "direct" then
|
||||
local out_tag = "direct-out"
|
||||
table.insert(outbounds, 1, {
|
||||
type = "direct",
|
||||
tag = out_tag,
|
||||
routing_mark = 255,
|
||||
domain_strategy = (dns_query_strategy and dns_query_strategy ~= "UseIP") and "ipv4_only" or "prefer_ipv6",
|
||||
})
|
||||
|
||||
local server = {
|
||||
tag = dns_out_tag,
|
||||
address_strategy = "prefer_ipv6",
|
||||
strategy = (dns_query_strategy and dns_query_strategy ~= "UseIP") and "ipv4_only" or "prefer_ipv6",
|
||||
detour = out_tag,
|
||||
client_subnet = (remote_dns_client_ip and remote_dns_client_ip ~= "") and remote_dns_client_ip or nil,
|
||||
}
|
||||
|
||||
if direct_dns_udp_server then
|
||||
local server_port = tonumber(direct_dns_udp_port) or 53
|
||||
server.address = "udp://" .. direct_dns_udp_server .. ":" .. server_port
|
||||
end
|
||||
|
||||
if direct_dns_tcp_server then
|
||||
local server_port = tonumber(direct_dns_tcp_port) or 53
|
||||
server.address = "tcp://" .. direct_dns_tcp_server .. ":" .. server_port
|
||||
end
|
||||
|
||||
if direct_dns_doh_url then
|
||||
server.address = direct_dns_doh_url
|
||||
end
|
||||
|
||||
table.insert(dns.servers, server)
|
||||
|
||||
route.final = out_tag
|
||||
end
|
||||
|
||||
table.insert(inbounds, {
|
||||
type = "direct",
|
||||
tag = "dns-in",
|
||||
listen = "127.0.0.1",
|
||||
listen_port = tonumber(dns_listen_port),
|
||||
sniff = true,
|
||||
})
|
||||
|
||||
table.insert(outbounds, {
|
||||
type = "dns",
|
||||
tag = "dns-out",
|
||||
})
|
||||
|
||||
table.insert(route.rules, 1, {
|
||||
protocol = "dns",
|
||||
inbound = {
|
||||
"dns-in"
|
||||
},
|
||||
outbound = "dns-out"
|
||||
})
|
||||
end
|
||||
|
||||
if inbounds or outbounds then
|
||||
local config = {
|
||||
log = {
|
||||
disabled = log == "0" and true or false,
|
||||
level = loglevel,
|
||||
timestamp = true,
|
||||
output = logfile,
|
||||
},
|
||||
-- DNS
|
||||
dns = dns,
|
||||
-- 传入连接
|
||||
inbounds = inbounds,
|
||||
-- 传出连接
|
||||
outbounds = outbounds,
|
||||
-- 路由
|
||||
route = route
|
||||
}
|
||||
return jsonc.stringify(config, 1)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
_G.gen_config = gen_config
|
||||
_G.gen_proto_config = gen_proto_config
|
||||
_G.gen_dns_config = gen_dns_config
|
||||
|
||||
if arg[1] then
|
||||
local func =_G[arg[1]]
|
||||
|
||||
@@ -715,7 +715,7 @@ function gen_config(var)
|
||||
local blc_node_tag = "blc-" .. blc_node_id
|
||||
local is_new_blc_node = true
|
||||
for _, outbound in ipairs(outbounds) do
|
||||
if outbound.tag:find("^" .. blc_node_tag) == 1 then
|
||||
if string.sub(outbound.tag, 1, #blc_node_tag) == blc_node_tag then
|
||||
is_new_blc_node = false
|
||||
valid_nodes[#valid_nodes + 1] = outbound.tag
|
||||
break
|
||||
@@ -740,7 +740,7 @@ function gen_config(var)
|
||||
if fallback_node_id then
|
||||
local is_new_node = true
|
||||
for _, outbound in ipairs(outbounds) do
|
||||
if outbound.tag:find("^" .. fallback_node_id) == 1 then
|
||||
if string.sub(outbound.tag, 1, #fallback_node_id) == fallback_node_id then
|
||||
is_new_node = false
|
||||
fallback_node_tag = outbound.tag
|
||||
break
|
||||
|
||||
@@ -1238,6 +1238,8 @@ start() {
|
||||
get_config
|
||||
export V2RAY_LOCATION_ASSET=$(config_t_get global_rules v2ray_location_asset "/usr/share/v2ray/")
|
||||
export XRAY_LOCATION_ASSET=$V2RAY_LOCATION_ASSET
|
||||
export ENABLE_DEPRECATED_GEOSITE=true
|
||||
export ENABLE_DEPRECATED_GEOIP=true
|
||||
ulimit -n 65535
|
||||
start_haproxy
|
||||
start_socks
|
||||
|
||||
@@ -9,20 +9,21 @@ probe_file="/tmp/etc/passwall2/haproxy/Probe_URL"
|
||||
probeUrl="https://www.google.com/generate_204"
|
||||
if [ -f "$probe_file" ]; then
|
||||
firstLine=$(head -n 1 "$probe_file" | tr -d ' \t')
|
||||
if [ -n "$firstLine" ]; then
|
||||
probeUrl="$firstLine"
|
||||
fi
|
||||
[ -n "$firstLine" ] && probeUrl="$firstLine"
|
||||
fi
|
||||
|
||||
status=$(/usr/bin/curl -I -o /dev/null -skL -x socks5h://${server_address}:${server_port} --connect-timeout 3 --retry 3 -w %{http_code} "${probeUrl}")
|
||||
extra_params="-x socks5h://${server_address}:${server_port}"
|
||||
if /usr/bin/curl --help all | grep -q "\-\-retry-all-errors"; then
|
||||
extra_params="${extra_params} --retry-all-errors"
|
||||
fi
|
||||
|
||||
status=$(/usr/bin/curl -I -o /dev/null -skL ${extra_params} --connect-timeout 3 --retry 1 -w "%{http_code}" "${probeUrl}")
|
||||
|
||||
case "$status" in
|
||||
204|\
|
||||
200)
|
||||
status=200
|
||||
200|204)
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
return_code=1
|
||||
if [ "$status" = "200" ]; then
|
||||
return_code=0
|
||||
fi
|
||||
exit ${return_code}
|
||||
|
||||
@@ -24,9 +24,10 @@ test_url() {
|
||||
local timeout=2
|
||||
[ -n "$3" ] && timeout=$3
|
||||
local extra_params=$4
|
||||
curl --help all | grep "\-\-retry-all-errors" > /dev/null
|
||||
[ $? == 0 ] && extra_params="--retry-all-errors ${extra_params}"
|
||||
status=$(/usr/bin/curl -I -o /dev/null -skL --user-agent "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36" ${extra_params} --connect-timeout ${timeout} --retry ${try} -w %{http_code} "$url")
|
||||
if /usr/bin/curl --help all | grep -q "\-\-retry-all-errors"; then
|
||||
extra_params="--retry-all-errors ${extra_params}"
|
||||
fi
|
||||
status=$(/usr/bin/curl -I -o /dev/null -skL ${extra_params} --connect-timeout ${timeout} --retry ${try} -w %{http_code} "$url")
|
||||
case "$status" in
|
||||
204)
|
||||
status=200
|
||||
|
||||
@@ -106,43 +106,6 @@ function updateResVersion(El, version) {
|
||||
return El;
|
||||
}
|
||||
|
||||
function renderNATBehaviorTest(El) {
|
||||
let resEl = E('div', { 'class': 'control-group' }, [
|
||||
E('select', {
|
||||
'id': '_status_nattest_l4proto',
|
||||
'class': 'cbi-input-select',
|
||||
'style': 'width: 5em'
|
||||
}, [
|
||||
E('option', { 'value': 'udp' }, 'UDP'),
|
||||
E('option', { 'value': 'tcp' }, 'TCP')
|
||||
]),
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-apply',
|
||||
'click': ui.createHandlerFn(this, function() {
|
||||
const stun = this.formvalue(this.section.section);
|
||||
const l4proto = document.getElementById('_status_nattest_l4proto').value;
|
||||
const l4proto_idx = document.getElementById('_status_nattest_l4proto').selectedIndex;
|
||||
|
||||
return fs.exec_direct('/etc/fchomo/scripts/natcheck.sh', [stun, l4proto, getRandom(32768, 61000)]).then((stdout) => {
|
||||
this.description = '<details><summary>' + _('Expand/Collapse result') + '</summary>' + stdout + '</details>';
|
||||
|
||||
return this.map.reset().then((res) => {
|
||||
document.getElementById('_status_nattest_l4proto').selectedIndex = l4proto_idx;
|
||||
});
|
||||
});
|
||||
})
|
||||
}, [ _('Check') ])
|
||||
]);
|
||||
|
||||
let newEl = E('div', { style: 'font-weight: bold; align-items: center; display: flex' }, []);
|
||||
if (El) {
|
||||
newEl.appendChild(E([El, resEl]));
|
||||
} else
|
||||
newEl.appendChild(resEl);
|
||||
|
||||
return newEl;
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
load() {
|
||||
return Promise.all([
|
||||
@@ -247,7 +210,7 @@ return view.extend({
|
||||
}
|
||||
|
||||
so = ss.option(form.Value, '_nattest', _('Check routerself NAT Behavior'));
|
||||
so.default = hm.stunserver[0][0];
|
||||
so.default = `udp://${hm.stunserver[0][0]}`;
|
||||
hm.stunserver.forEach((res) => {
|
||||
so.value.apply(so, res);
|
||||
})
|
||||
@@ -257,14 +220,60 @@ return view.extend({
|
||||
.format('https://github.com/muink/openwrt-stuntman');
|
||||
so.readonly = true;
|
||||
} else {
|
||||
so.renderWidget = function(/* ... */) {
|
||||
let El = form.Value.prototype.renderWidget.apply(this, arguments);
|
||||
so.renderWidget = function(section_id, option_index, cfgvalue) {
|
||||
const cval = new URL(cfgvalue || this.default);
|
||||
//console.info(cval.toString());
|
||||
let El = form.Value.prototype.renderWidget.call(this, section_id, option_index, cval.host);
|
||||
|
||||
return renderNATBehaviorTest.call(this, El);
|
||||
let resEl = E('div', { 'class': 'control-group' }, [
|
||||
E('select', {
|
||||
'id': '_status_nattest_l4proto',
|
||||
'class': 'cbi-input-select',
|
||||
'style': 'width: 5em'
|
||||
}, [
|
||||
...[
|
||||
['udp', 'UDP'], // default
|
||||
['tcp', 'TCP']
|
||||
].map(res => E('option', {
|
||||
value: res[0],
|
||||
selected: (cval.protocol === `${res[0]}:`) ? "" : null
|
||||
}, res[1]))
|
||||
]),
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-apply',
|
||||
'click': ui.createHandlerFn(this, function() {
|
||||
const stun = this.formvalue(this.section.section);
|
||||
const l4proto = document.getElementById('_status_nattest_l4proto').value;
|
||||
|
||||
return fs.exec_direct('/etc/fchomo/scripts/natcheck.sh', [stun, l4proto, getRandom(32768, 61000)]).then((stdout) => {
|
||||
this.description = '<details><summary>' + _('Expand/Collapse result') + '</summary>' + stdout + '</details>';
|
||||
|
||||
return this.map.reset().then((res) => {
|
||||
});
|
||||
});
|
||||
})
|
||||
}, [ _('Check') ])
|
||||
]);
|
||||
ui.addValidator(resEl.querySelector('#_status_nattest_l4proto'), 'string', false, (v) => {
|
||||
const section_id = this.section.section;
|
||||
const stun = this.formvalue(section_id);
|
||||
|
||||
this.onchange.call(this, {}, section_id, stun);
|
||||
return true;
|
||||
}, 'change');
|
||||
|
||||
let newEl = E('div', { style: 'font-weight: bold; align-items: center; display: flex' }, []);
|
||||
if (El) {
|
||||
newEl.appendChild(E([El, resEl]));
|
||||
} else
|
||||
newEl.appendChild(resEl);
|
||||
|
||||
return newEl;
|
||||
}
|
||||
}
|
||||
so.onchange = function(ev, section_id, value) {
|
||||
this.default = value;
|
||||
const l4proto = document.getElementById('_status_nattest_l4proto').value;
|
||||
this.default = `${l4proto}://${value}`;
|
||||
}
|
||||
so.write = function() {};
|
||||
so.remove = function() {};
|
||||
|
||||
@@ -352,15 +352,15 @@ o:value("119.28.28.28")
|
||||
o:depends("direct_dns_mode", "tcp")
|
||||
|
||||
o = s:taboption("DNS", Value, "direct_dns_dot", translate("Direct DNS DoT"))
|
||||
o.default = "tls://dot.pub@1.12.12.12"
|
||||
o:value("tls://dot.pub@1.12.12.12")
|
||||
o:value("tls://dot.pub@120.53.53.53")
|
||||
o:value("tls://dot.360.cn@36.99.170.86")
|
||||
o:value("tls://dot.360.cn@101.198.191.4")
|
||||
o:value("tls://dns.alidns.com@223.5.5.5")
|
||||
o:value("tls://dns.alidns.com@223.6.6.6")
|
||||
o:value("tls://dns.alidns.com@2400:3200::1")
|
||||
o:value("tls://dns.alidns.com@2400:3200:baba::1")
|
||||
o.default = "tls://1.12.12.12"
|
||||
o:value("tls://1.12.12.12")
|
||||
o:value("tls://120.53.53.53")
|
||||
o:value("tls://36.99.170.86")
|
||||
o:value("tls://101.198.191.4")
|
||||
o:value("tls://223.5.5.5")
|
||||
o:value("tls://223.6.6.6")
|
||||
o:value("tls://2400:3200::1")
|
||||
o:value("tls://2400:3200:baba::1")
|
||||
o.validate = chinadns_dot_validate
|
||||
o:depends("direct_dns_mode", "dot")
|
||||
|
||||
@@ -502,17 +502,17 @@ o:depends({singbox_dns_mode = "tcp"})
|
||||
|
||||
---- DoT
|
||||
o = s:taboption("DNS", Value, "remote_dns_dot", translate("Remote DNS DoT"))
|
||||
o.default = "tls://dns.google@8.8.4.4"
|
||||
o:value("tls://1dot1dot1dot1.cloudflare-dns.com@1.0.0.1", "1.0.0.1 (CloudFlare)")
|
||||
o:value("tls://1dot1dot1dot1.cloudflare-dns.com@1.1.1.1", "1.1.1.1 (CloudFlare)")
|
||||
o:value("tls://dns.google@8.8.4.4", "8.8.4.4 (Google)")
|
||||
o:value("tls://dns.google@8.8.8.8", "8.8.8.8 (Google)")
|
||||
o:value("tls://dns.quad9.net@9.9.9.9", "9.9.9.9 (Quad9)")
|
||||
o:value("tls://dns.quad9.net@149.112.112.112", "149.112.112.112 (Quad9)")
|
||||
o:value("tls://dns.adguard.com@94.140.14.14", "94.140.14.14 (AdGuard)")
|
||||
o:value("tls://dns.adguard.com@94.140.15.15", "94.140.15.15 (AdGuard)")
|
||||
o:value("tls://dns.opendns.com@208.67.222.222", "208.67.222.222 (OpenDNS)")
|
||||
o:value("tls://dns.opendns.com@208.67.220.220", "208.67.220.220 (OpenDNS)")
|
||||
o.default = "tls://1.1.1.1"
|
||||
o:value("tls://1.0.0.1", "1.0.0.1 (CloudFlare)")
|
||||
o:value("tls://1.1.1.1", "1.1.1.1 (CloudFlare)")
|
||||
o:value("tls://8.8.4.4", "8.8.4.4 (Google)")
|
||||
o:value("tls://8.8.8.8", "8.8.8.8 (Google)")
|
||||
o:value("tls://9.9.9.9", "9.9.9.9 (Quad9)")
|
||||
o:value("tls://149.112.112.112", "149.112.112.112 (Quad9)")
|
||||
o:value("tls://94.140.14.14", "94.140.14.14 (AdGuard)")
|
||||
o:value("tls://94.140.15.15", "94.140.15.15 (AdGuard)")
|
||||
o:value("tls://208.67.222.222", "208.67.222.222 (OpenDNS)")
|
||||
o:value("tls://208.67.220.220", "208.67.220.220 (OpenDNS)")
|
||||
o.validate = chinadns_dot_validate
|
||||
o:depends("dns_mode", "dot")
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user